diff --git a/AVR_Miner.py b/AVR_Miner.py index 9edf6fe3..5bb33f13 100644 --- a/AVR_Miner.py +++ b/AVR_Miner.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """ -Duino-Coin Official AVR Miner 3.33 © MIT licensed +Duino-Coin Official AVR Miner 3.4 © MIT licensed https://duinocoin.com https://github.com/revoxhere/duino-coin -Duino-Coin Team & Community 2019-2022 +Duino-Coin Team & Community 2019-2023 """ from os import _exit, mkdir @@ -109,7 +109,7 @@ def port_num(com): class Settings: - VER = '3.33' + VER = '3.4' SOC_TIMEOUT = 15 REPORT_TIME = 120 AVR_TIMEOUT = 10 @@ -782,7 +782,7 @@ def greeting(): + Style.BRIGHT + get_string('banner') + Style.RESET_ALL + Fore.MAGENTA + f' {Settings.VER}' + Fore.RESET - + ' 2019-2022') + + ' 2019-2023') print( Style.DIM + Fore.MAGENTA diff --git a/ESP32_Code/ESP32_Code.ino b/ESP32_Code/ESP32_Code.ino index ed418a51..cf208f05 100644 --- a/ESP32_Code/ESP32_Code.ino +++ b/ESP32_Code/ESP32_Code.ino @@ -3,7 +3,7 @@ ( _ \( )( )(_ _)( \( )( _ )___ / __)( _ )(_ _)( \( ) )(_) ))(__)( _)(_ ) ( )(_)((___)( (__ )(_)( _)(_ ) ( (____/(______)(____)(_)\_)(_____) \___)(_____)(____)(_)\_) - Official code for ESP32 boards version 3.3 + Official code for ESP32 boards version 3.4 Duino-Coin Team & Community 2019-2022 © MIT Licensed https://duinocoin.com @@ -26,6 +26,16 @@ const char *WIFI_PASS = "WIFI_PASSWORD"; const char *RIG_IDENTIFIER = "None"; // Change this if your board has built-in led on non-standard pin #define LED_BUILTIN 2 + +template +class DuinoIoT { + T sensor; +}; + +typedef struct { + String val; +} SensorData_t; + // Uncomment the line below if you wish to use a DHT sensor (Duino IoT beta) // #define USE_DHT #ifdef USE_DHT @@ -35,16 +45,114 @@ const char *RIG_IDENTIFIER = "None"; #define DHTPIN 2 // Set DHT11 or DHT22 accordingly #define DHTTYPE DHT11 - DHT dht(DHTPIN, DHTTYPE); + + template<> + class DuinoIoT { + + DHT sensor; + + public: + + DuinoIoT() : sensor(DHTPIN, DHTTYPE) {} + + void begin() { + Serial.println("Initializing DHT sensor"); + sensor.begin(); + Serial.println("Test reading: " + String(sensor.readTemperature()) + "*C " + String(sensor.readHumidity()) + "% humidity"); + } + + void getSensorData(SensorData_t *data) { + String temp = String(sensor.readTemperature()); + String hum = String(sensor.readHumidity()); + Serial.println("DHT readings: " + temp + "*C, " + hum + "%"); + data->val = temp + "@" + hum; + } + }; + + DuinoIoT duinoIoT; #endif // Uncomment the line below if you wish to use an AHT10 or AHT20 sensor (Duino IoT beta) -// #define USE_AHT +//#define USE_AHT #ifdef USE_AHT // Install "Adafruit AHTX0 Library" if you get an error #include // AHT10/AHT20 should be connected to ESP32 default I2C pins // i.e. (I2C_SDA: GPIO_21 and I2C_SCL: GPIO_22) - Adafruit_AHTX0 aht; + + template<> + class DuinoIoT { + + Adafruit_AHTX0 sensor; + + public: + + DuinoIoT() : sensor() {} + + void begin() { + Serial.println("Initializing AHT sensor"); + if (! sensor.begin()) { + Serial.println("Could not find AHT Sensor. Check wiring?"); + while (1) delay(10); + } + sensors_event_t hum, temp; + sensor.getEvent(&hum, &temp); + Serial.println("Test reading: " + String(temp.temperature) + "*C, " + String(hum.relative_humidity) + "% rH"); + } + + void getSensorData(SensorData_t *data) { + sensors_event_t hum, temp; + sensor.getEvent(&hum, &temp); + Serial.println("AHT readings: " + String(temp.temperature) + "*C, " + String(hum.relative_humidity) + "% rH"); + data->val = String(temp.temperature) + "@" + String(hum.relative_humidity); + } + }; + + DuinoIoT duinoIoT; +#endif + +// Uncomment the line below if you wish to use an BMP280 sensor (Duino IoT beta) +//#define USE_BMP280 +#ifdef USE_BMP280 +// Install "Adafruit BMP280 Library" if you get an error +# include +# include +# define I2C_SDA_PIN 22 +# define I2C_SCL_PIN 23 +# define BMP280_I2C_ADDR 0x76 + template<> + class DuinoIoT { + + Adafruit_BMP280 sensor; + + public: + + DuinoIoT() : sensor() {} + + void begin() { + Serial.println("Initializing BMP280 sensor"); + Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); + if (!sensor.begin(BMP280_I2C_ADDR)) { + Serial.println("Could not find BMP280 Sensor. Check wiring?"); + while (1); delay(10); + } + sensor.setSampling(Adafruit_BMP280::MODE_NORMAL, + Adafruit_BMP280::SAMPLING_X2, + Adafruit_BMP280::SAMPLING_X16, + Adafruit_BMP280::FILTER_X16, + Adafruit_BMP280::STANDBY_MS_500); + + Serial.print(F("Test reading: "));Serial.print(sensor.readTemperature());Serial.print(sensor.readPressure()); + } + + void getSensorData(SensorData_t *data) { + String temp = String(sensor.readTemperature()); + String pressure = String(sensor.readPressure() / 100.0F); + Serial.println("BMP280 readings: " + temp + "*C, " + pressure + " hPa"); + data->val = "temperature:" + temp + "@pressure:" + pressure; + } + }; + + DuinoIoT duinoIoT; #endif #define BLINK_SETUP_COMPLETE 2 @@ -157,7 +265,7 @@ SemaphoreHandle_t xMutex; const char * DEVICE = "ESP32"; const char * POOLPICKER_URL[] = {"https://server.duinocoin.com/getPool"}; const char * MINER_BANNER = "Official ESP32 Miner"; -const char * MINER_VER = "3.3"; +const char * MINER_VER = "3.4"; String pool_name = ""; String host = ""; String node_id = ""; @@ -646,20 +754,13 @@ void TaskMining(void *pvParameters) { Serial.println(String(taskCoreName + " asking for a new job for user: " + DUCO_USER)); jobClient.flush(); - #if !(defined(USE_DHT) || defined(USE_AHT)) + #if !(defined(USE_DHT) || defined(USE_AHT) || defined(USE_BMP280)) jobClient.print("JOB," + String(DUCO_USER) + ",ESP32," + String(MINER_KEY) + MSGNEWLINE); - #elif defined(USE_DHT) - int temp = dht.readTemperature(); - int hum = dht.readHumidity(); - Serial.println("DHT readings: " + String(temp) + "*C, " + String(hum) + "%"); + #else + SensorData_t data = {""}; + duinoIoT.getSensorData(&data); jobClient.print("JOB," + String(DUCO_USER) + ",ESP32," + String(MINER_KEY) + "," + - String(temp) + "@" + String(hum) + MSGNEWLINE); - #elif defined(USE_AHT) - sensors_event_t hum, temp; - aht.getEvent(&hum, &temp); - Serial.println("AHT readings: " + String(temp.temperature) + "*C, " + String(hum.relative_humidity) + "% rH"); - jobClient.print("JOB," + String(DUCO_USER) + ",ESP32," + String(MINER_KEY) + "," + - String(temp.temperature) + "@" + String(hum.relative_humidity) + MSGNEWLINE); + data.val + MSGNEWLINE); #endif while (!jobClient.available()) { @@ -783,24 +884,10 @@ void TaskMining(void *pvParameters) { void setup() { Serial.begin(500000); // Start serial connection Serial.println("\n\nDuino-Coin " + String(MINER_BANNER)); - - #ifdef USE_DHT - Serial.println("Initializing DHT sensor"); - dht.begin(); - Serial.println("Test reading: " + String(dht.readHumidity()) + "% humidity"); - Serial.println("Test reading: temperature " + String(dht.readTemperature()) + "*C"); - #elif defined(USE_AHT) - Serial.println("Initializing AHT sensor"); - if (! aht.begin()) { - Serial.println("Could not find AHT Sensor. Check wiring?"); - while (1) delay(10); - } - sensors_event_t hum, temp; - aht.getEvent(&hum, &temp); - Serial.println("Test reading: " + String(hum.relative_humidity) + "% humidity"); - Serial.println("Test reading: temperature " + String(temp.temperature) + "*C"); - #endif - +#if (defined(USE_DHT) || defined(USE_AHT) || defined(USE_BMP280)) + duinoIoT.begin(); +#endif + WiFi.setSleep(false); // Better network responsiveness WiFi.mode(WIFI_STA); // Setup ESP in client mode btStop(); WiFi.begin(SSID, WIFI_PASS); // Connect to wifi diff --git a/ESP8266_Code/ESP8266_Code.ino b/ESP8266_Code/ESP8266_Code.ino index 77a93c42..d1d317a3 100644 --- a/ESP8266_Code/ESP8266_Code.ino +++ b/ESP8266_Code/ESP8266_Code.ino @@ -3,7 +3,7 @@ ( _ \( )( )(_ _)( \( )( _ )___ / __)( _ )(_ _)( \( ) )(_) ))(__)( _)(_ ) ( )(_)((___)( (__ )(_)( _)(_ ) ( (____/(______)(____)(_)\_)(_____) \___)(_____)(____)(_)\_) - Official code for ESP8266 boards version 3.3 + Official code for ESP8266 boards version 3.4 Duino-Coin Team & Community 2019-2022 © MIT Licensed https://duinocoin.com @@ -158,7 +158,7 @@ const bool LED_BLINKING = true; const char * DEVICE = "ESP8266"; const char * POOLPICKER_URL[] = {"https://server.duinocoin.com/getPool"}; const char * MINER_BANNER = "Official ESP8266 Miner"; -const char * MINER_VER = "3.3"; +const char * MINER_VER = "3.4"; unsigned int share_count = 0; unsigned int port = 0; unsigned int difficulty = 0; @@ -443,7 +443,7 @@ void SetupWifi() { } } - Serial.println("\n\nnSuccessfully connected to WiFi"); + Serial.println("\n\nSuccessfully connected to WiFi"); Serial.println("Local IP address: " + WiFi.localIP().toString()); Serial.println("Rig name: " + String(RIG_IDENTIFIER)); Serial.println(); @@ -518,23 +518,6 @@ void handleSystemEvents(void) { yield(); } -// https://stackoverflow.com/questions/9072320/split-string-into-string-array -String getValue(String data, char separator, int index) -{ - int found = 0; - int strIndex[] = {0, -1}; - int max_index = data.length() - 1; - - for (int i = 0; i <= max_index && found <= index; i++) { - if (data.charAt(i) == separator || i == max_index) { - found++; - strIndex[0] = strIndex[1] + 1; - strIndex[1] = (i == max_index) ? i + 1 : i; - } - } - return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; -} - void waitForClientData(void) { client_buffer = ""; @@ -610,6 +593,33 @@ uint8_t *hexStringToUint8Array(const String &hexString, uint8_t *uint8Array, con return uint8Array; } +struct MiningJob +{ + String last_block_hash; + String expected_hash_str; + uint8_t expected_hash[20]; + unsigned int difficulty; + + bool parse(char* job_str) + { + String tokens[3]; + char *token = strtok(job_str, ","); + for (int i = 0; token != NULL && i < 3; i++) + { + tokens[i] = token; + token = strtok(NULL, ","); + } + + last_block_hash = tokens[0]; + expected_hash_str = tokens[1]; + hexStringToUint8Array(expected_hash_str, expected_hash, 20); + difficulty = tokens[2].toInt() * 100 + 1; + + return true; + } +}; + + void setup() { Serial.begin(500000); Serial.println("\nDuino-Coin " + String(MINER_VER)); @@ -714,28 +724,25 @@ void loop() { #endif waitForClientData(); - String last_block_hash = getValue(client_buffer, SEP_TOKEN, 0); - String expected_hash_str = getValue(client_buffer, SEP_TOKEN, 1); - difficulty = getValue(client_buffer, SEP_TOKEN, 2).toInt() * 100 + 1; - - if (USE_HIGHER_DIFF) system_update_cpu_freq(160); + Serial.println("Received job with size of " + String(client_buffer)); - int job_len = last_block_hash.length() + expected_hash_str.length() + String(difficulty).length(); + MiningJob job; + job.parse((char*)client_buffer.c_str()); + difficulty = job.difficulty; - Serial.println("Received job with size of " + String(job_len) + " bytes: " + last_block_hash + " " + expected_hash_str + " " + difficulty); + Serial.println("Parsed job: " + job.last_block_hash + " " + job.expected_hash_str + " " + String(job.difficulty)); - uint8_t expected_hash[20]; - hexStringToUint8Array(expected_hash_str, expected_hash, 20); + if (USE_HIGHER_DIFF) system_update_cpu_freq(160); br_sha1_init(&sha1_ctx_base); - br_sha1_update(&sha1_ctx_base, last_block_hash.c_str(), last_block_hash.length()); + br_sha1_update(&sha1_ctx_base, job.last_block_hash.c_str(), job.last_block_hash.length()); float start_time = micros(); max_micros_elapsed(start_time, 0); String result = ""; if (LED_BLINKING) digitalWrite(LED_BUILTIN, LOW); - for (unsigned int duco_numeric_result = 0; duco_numeric_result < difficulty; duco_numeric_result++) { + for (unsigned int duco_numeric_result = 0; duco_numeric_result < job.difficulty; duco_numeric_result++) { // Difficulty loop sha1_ctx = sha1_ctx_base; duco_numeric_result_str = String(duco_numeric_result); @@ -743,7 +750,7 @@ void loop() { br_sha1_update(&sha1_ctx, duco_numeric_result_str.c_str(), duco_numeric_result_str.length()); br_sha1_out(&sha1_ctx, hashArray); - if (memcmp(expected_hash, hashArray, 20) == 0) { + if (memcmp(job.expected_hash, hashArray, 20) == 0) { // If result is found if (LED_BLINKING) digitalWrite(LED_BUILTIN, HIGH); unsigned long elapsed_time = micros() - start_time; diff --git a/PC_Miner.py b/PC_Miner.py index 30bea540..535b9a2d 100644 --- a/PC_Miner.py +++ b/PC_Miner.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """ -Duino-Coin Official PC Miner 3.33 © MIT licensed +Duino-Coin Official PC Miner 3.4 © MIT licensed https://duinocoin.com https://github.com/revoxhere/duino-coin -Duino-Coin Team & Community 2019-2022 +Duino-Coin Team & Community 2019-2023 """ from time import time, sleep, strptime, ctime @@ -24,6 +24,7 @@ import os import json import zipfile +import traceback from pathlib import Path from re import sub @@ -57,7 +58,10 @@ def handler(signal_received, frame): + Fore.RESET + get_string("goodbye"), "warning") - + + if not "raspi_leds" in user_settings: + user_settings["raspi_leds"] = "y" + if running_on_rpi and user_settings["raspi_leds"] == "y": # Reset onboard status LEDs os.system( @@ -136,7 +140,7 @@ class Settings: """ ENCODING = "UTF8" SEPARATOR = "," - VER = 3.33 + VER = 3.4 DATA_DIR = "Duino-Coin PC Miner " + str(VER) TRANSLATIONS = ("https://raw.githubusercontent.com/" + "revoxhere/" @@ -150,6 +154,7 @@ class Settings: REPORT_TIME = 5*60 DONATE_LVL = 0 RASPI_LEDS = "y" + RASPI_CPU_IOT = "y" try: # Raspberry Pi latin encoding users can't display this character @@ -301,7 +306,6 @@ def check_updates(): pretty_print("Update aborted", "warning", "sys0") except Exception as e: print(e) - sys.exit() class Algorithms: @@ -501,12 +505,24 @@ def get_prefix(symbol: str, val = str(round(val)) + " " return val + symbol +def get_rpi_temperature(): + output = Popen(args='cat /sys/class/thermal/thermal_zone0/temp', + stdout=PIPE, + shell=True).communicate()[0].decode() + return round(int(output) / 1000, 2) + def periodic_report(start_time, end_time, shares, blocks, hashrate, uptime): """ Displays nicely formated uptime stats """ + + raspi_iot_reading = "" + + if running_on_rpi and user_settings["raspi_cpu_iot"] == "y": + raspi_iot_reading = f"{get_string('rpi_cpu_temp')} {get_rpi_temperature()}°C" + seconds = round(end_time - start_time) pretty_print(get_string("periodic_mining_report") + Fore.RESET + Style.NORMAL @@ -524,7 +540,8 @@ def periodic_report(start_time, end_time, shares, + str(int(hashrate*seconds)) + get_string("report_body6") + get_string("total_mining_time") - + str(uptime), "success") + + str(uptime) + + raspi_iot_reading, "success") def calculate_uptime(start_time): @@ -600,7 +617,7 @@ def _blink_builtin(led="green"): sleep(0.1) os.system( 'echo 0 | sudo tee /sys/class/leds/led1/brightness >/dev/null 2>&1') - + if type == "accept": if running_on_rpi and user_settings["raspi_leds"] == "y": _blink_builtin() @@ -729,7 +746,7 @@ def greeting(): print("\n" + Style.DIM + Fore.YELLOW + Settings.BLOCK + Fore.YELLOW + Style.BRIGHT + get_string("banner") + Style.RESET_ALL + Fore.MAGENTA + " (" + str(Settings.VER) + ") " - + Fore.RESET + "2019-2022") + + Fore.RESET + "2019-2023") print(Style.DIM + Fore.YELLOW + Settings.BLOCK + Style.NORMAL + Fore.YELLOW + "https://github.com/revoxhere/duino-coin") @@ -951,19 +968,20 @@ def load_cfg(): donation_level = 0 configparser["PC Miner"] = { - "username": username, - "mining_key": mining_key, - "intensity": intensity, - "threads": threads, - "start_diff": start_diff, - "donate": int(donation_level), - "identifier": rig_id, - "algorithm": algorithm, - "language": lang, - "soc_timeout": Settings.SOC_TIMEOUT, - "report_sec": Settings.REPORT_TIME, - "raspi_leds": Settings.RASPI_LEDS, - "discord_rp": "y"} + "username": username, + "mining_key": mining_key, + "intensity": intensity, + "threads": threads, + "start_diff": start_diff, + "donate": int(donation_level), + "identifier": rig_id, + "algorithm": algorithm, + "language": lang, + "soc_timeout": Settings.SOC_TIMEOUT, + "report_sec": Settings.REPORT_TIME, + "raspi_leds": Settings.RASPI_LEDS, + "raspi_cpu_iot": Settings.RASPI_CPU_IOT, + "discord_rp": "y"} with open(Settings.DATA_DIR + Settings.SETTINGS_FILE, "w") as configfile: @@ -996,9 +1014,8 @@ def m_connect(id, pool): pretty_print(get_string("connected") + Fore.RESET + Style.NORMAL + get_string("connected_server") - + str(POOL_VER) + ", " + pool[0] + ":" - + str(pool[1]) + ")", "success", - "net" + str(id)) + + str(POOL_VER) + ", " + pool[0] +")", + "success", "net" + str(id)) else: pretty_print(get_string("outdated_miner") + str(Settings.VER) + ") -" @@ -1025,7 +1042,6 @@ def mine(id: int, user_settings: list, """ Main section that executes the functionalities from the sections above. """ - using_algo = get_string("using_algo") pretty_print(get_string("mining_thread") + str(id) + get_string("mining_thread_starting") @@ -1046,6 +1062,11 @@ def mine(id: int, user_settings: list, else: key = user_settings["mining_key"] + raspi_iot_reading = "" + if user_settings["raspi_cpu_iot"] == "y" and running_on_rpi: + # * instead of the degree symbol because nodes use basic encoding + raspi_iot_reading = f"CPU temperature:{get_rpi_temperature()}*C" + while True: job_req = "JOB" Client.send(job_req @@ -1055,7 +1076,8 @@ def mine(id: int, user_settings: list, + str(user_settings["start_diff"]) + Settings.SEPARATOR + str(key) - ) + + Settings.SEPARATOR + + str(raspi_iot_reading)) job = Client.recv().split(Settings.SEPARATOR) if len(job) == 3: @@ -1155,11 +1177,13 @@ def mine(id: int, user_settings: list, break break except Exception as e: + print(traceback.format_exc()) pretty_print(get_string("error_while_mining") + " " + str(e), "error", "net" + str(id)) sleep(5) break except Exception as e: + print(traceback.format_exc()) pretty_print(get_string("error_while_mining") + " " + str(e), "error", "net" + str(id)) @@ -1173,8 +1197,9 @@ def connect(): Thread(target=Discord_rp.update).start() except Exception as e: pretty_print( - get_string("discord_launch_error" + - Style.NORMAL + Fore.RESET + " " + str(e))) + get_string("discord_launch_error") + + Style.NORMAL + Fore.RESET + " " + str(e), + "warning") def update(): @@ -1197,7 +1222,8 @@ def update(): except Exception as e: pretty_print( get_string("discord_update_error" + - Style.NORMAL + Fore.RESET + " " + str(e))) + Style.NORMAL + Fore.RESET + " " + str(e)), + "warning") sleep(15) @@ -1301,7 +1327,12 @@ def load(): Fasthash.load() Fasthash.init() - + + if not "raspi_leds" in user_settings: + user_settings["raspi_leds"] = "y" + if not "raspi_cpu_iot" in user_settings: + user_settings["raspi_cpu_iot"] = "y" + if user_settings["raspi_leds"] == "y": try: with io.open('/sys/firmware/devicetree/base/model', 'r') as m: @@ -1320,6 +1351,17 @@ def load(): 'echo gpio | sudo tee /sys/class/leds/led1/trigger >/dev/null 2>&1') os.system( 'echo gpio | sudo tee /sys/class/leds/led0/trigger >/dev/null 2>&1') + + if user_settings["raspi_cpu_iot"] == "y" and running_on_rpi: + try: + temp = get_rpi_temperature() + pretty_print(get_string("iot_on_rpi") + + Style.NORMAL + Fore.RESET + " " + + f"{get_string('iot_on_rpi2')} {temp}°C", + "success") + except Exception as e: + print(e) + user_settings["raspi_cpu_iot"] = "n" try: check_mining_key(user_settings) diff --git a/README.md b/README.md index ca025199..66e48640 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,6 @@ All tests were performed using the DUCO-S1 algorithm **without fasthash accelera * [Duino Miner](https://github.com/g7ltt/Duino-Miner) - Arduino Nano based DUCO miner files and documentation by g7ltt * [DUINO Mining Rig](https://repalmakershop.com/pages/duino-mining-rig) - 3D files, PCB designs and instructions for creating your own Duino rig by ReP_AL * [DuinoCoin-balance-Home-Assistant](https://github.com/NL647/DuinoCoin-balance-Home-Assistant) - addon for home assistant displaying your balance by NL647 - * [Duino Coin Status Monitor](https://github.com/TSltd/duino_coin) for 128x64 SSD1306 OLED and ESP8266 by TSltd * [ducopanel](https://github.com/ponsato/ducopanel) - a GUI app for controling your Duino-Coin miners by ponsato * [Duino AVR Monitor](https://www.microsoft.com/store/apps/9NJ7HPFSR9V5) - GUI Windows App for monitoring AVR devices mining DUCO by niknak * [Duino-Coin Arduino library](https://github.com/ricaun/arduino-DuinoCoin) by ricaun @@ -211,7 +210,6 @@ All tests were performed using the DUCO-S1 algorithm **without fasthash accelera * [duco-miners](https://github.com/dansinclair25/duco-miners) CLI mining dashboard made by dansinclair25 * [Duco-Coin Symbol Icon ttf](https://github.com/SandUhrGucker/Duco-Coin-Symbol-Icon-ttf-.h) by SandUhrGucker * [DUCO Monitor](https://siunus.github.io/duco-monitor/) account statistics website by siunus - * [duino-tools](https://github.com/kyngs/duino-tools) written in Java by kyngs * [Duino Stats](https://github.com/Bilaboz/duino-stats) official Discord bot by Bilaboz * [DuCoWallet](https://github.com/viktor02/DuCoWallet) GUI Wallet by viktor02 * [Duco-widget-ios](https://github.com/naphob/duco-widget-ios) - a Duino-Coin iOS widget by Naphob