├── ESP8266_Interface ├── AbstractPubSub │ └── AbstractPubSub.h ├── ArduinoOTAFirmwareUpdater │ ├── ArduinoOTAFirmwareUpdater.cpp │ ├── ArduinoOTAFirmwareUpdater.h │ └── Examples │ │ └── Arduino_Flasher │ │ └── Arduino_Flasher.ino ├── ArduinoRemoteDebug │ ├── ArduinoRemoteDebug.cpp │ ├── ArduinoRemoteDebug.h │ └── Examples │ │ └── Arduino_Debugger │ │ └── Arduino_Debugger.ino ├── ArduinoRemoteInterface │ ├── ArduinoRemoteInterface.h │ └── Examples │ │ └── Arduino_Remote │ │ └── Arduino_Remote.ino └── README.md ├── Host ├── .gitignore ├── ArduinoRemote.py ├── README.md ├── app.py ├── requirements.txt ├── res │ ├── arrow_16.png │ ├── cloud_16.png │ ├── debug_24.png │ ├── device_16.png │ ├── icon.png │ ├── ota_24.png │ ├── refresh.png │ ├── refresh_16.png │ ├── refresh_24.png │ ├── refresh_32.png │ └── settings_24.png └── temp │ └── .gitignore ├── README.md └── docs └── img ├── connection.png ├── copy_lib.PNG └── paste_lib.PNG /ESP8266_Interface/AbstractPubSub/AbstractPubSub.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file AbstractPubSub.h 3 | * @author Looi Kian Seong 4 | * @date 10-9-2020 5 | * @brief Abstract class which defines some common methods 6 | * which will be derived by the child class 7 | * 8 | */ 9 | 10 | #ifndef AbstractPubSub_H 11 | #define AbstractPubSub_H 12 | 13 | #ifndef DEVICEID 14 | #define DEVICEID Unknown 15 | #endif 16 | 17 | #define CREATE_TOPIC(X, Y) STR(X)STR(/)STR(Y) 18 | #define HELPER_STR(X) #X 19 | #define STR(X) HELPER_STR(X) 20 | 21 | #define DEFAULT_BAUDRATE 115200UL 22 | #define CMD_UPDATE_BAUDRATE 0x55 // 'U' 23 | #define DEVICE_ONLINE "Online" 24 | #define DEVICE_OFFLINE "Offline" 25 | #define WILL_TOPIC STR(DEVICEID)"/device_status" 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | class AbstractPubSub: virtual public PubSubClient { 33 | public: 34 | AbstractPubSub(WiFiClient *wifi_client, uint8_t reset_pin, int8_t ind_LED=-1): PubSubClient(*wifi_client) { 35 | _client_id = STR(DEVICEID); 36 | _reset_pin = reset_pin; 37 | _ind_LED = ind_LED; 38 | 39 | _setBaudrate(DEFAULT_BAUDRATE); 40 | Serial.begin(_baudrate); 41 | pinMode(_reset_pin, OUTPUT); 42 | digitalWrite(_reset_pin, HIGH); 43 | 44 | if(_ind_LED > -1) { 45 | pinMode(_ind_LED, OUTPUT); 46 | digitalWrite(_ind_LED, LOW); 47 | } 48 | } 49 | 50 | void clientLoop() { 51 | if(!connected()) { 52 | if(_ind_LED > -1) 53 | digitalWrite(_ind_LED, LOW); 54 | _reconnect(); 55 | } else { 56 | loop(); 57 | #ifdef EXTRA_FUNC_CALL_FLAG 58 | _appLoop(); 59 | #endif 60 | } 61 | } 62 | 63 | void setUsername(const char *username) { _username = username; } 64 | void setPassword(const char *password) { _password = password; } 65 | void setTxTopic(const char *tx_topic) { _tx_topic = tx_topic; } 66 | void setRxTopic(const char *rx_topic) { _rx_topic = rx_topic; } 67 | void setMQTTCallback() { setCallback([this] (char *topic, byte *payload, unsigned int length) { this->_callback(topic, payload, length); }); } 68 | 69 | protected: 70 | const char *_client_id; 71 | const char *_username; 72 | const char *_password; 73 | const char *_tx_topic; 74 | const char *_rx_topic; 75 | uint32_t _baudrate; 76 | uint8_t _reset_pin; 77 | int8_t _ind_LED; 78 | 79 | #ifdef EXTRA_FUNC_CALL_FLAG 80 | std::function _appLoop; 81 | #endif 82 | 83 | void _flushRxBuff() { while(Serial.available()) Serial.read(); } 84 | 85 | void _setBaudrate(uint32_t baudrate) { 86 | _baudrate = baudrate; 87 | Serial.flush(); 88 | Serial.updateBaudRate(_baudrate); 89 | } 90 | 91 | void _setBaudrate(byte *payload, uint32_t length) { 92 | uint32_t baudrate = 0; 93 | for(uint8_t i=1;i= '0') && (payload_i <= '9')) { 97 | baudrate *= 10; 98 | baudrate += payload_i - '0'; 99 | } else { 100 | baudrate = _baudrate; 101 | break; 102 | } 103 | } 104 | 105 | _setBaudrate(baudrate? baudrate:_baudrate); 106 | } 107 | 108 | void _reconnect() { 109 | if (connect(_client_id, _username, _password, WILL_TOPIC, 0, true, DEVICE_OFFLINE)) { 110 | digitalWrite(_ind_LED, HIGH); 111 | publish(WILL_TOPIC, DEVICE_ONLINE, true); 112 | subscribe(_rx_topic); 113 | } 114 | } 115 | 116 | virtual void _callback(char* topic, byte* payload, unsigned int length) = 0; 117 | }; 118 | 119 | #endif /* AbstractPubSub_H */ 120 | 121 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoOTAFirmwareUpdater/ArduinoOTAFirmwareUpdater.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ArduinoOTAFirmwareUpdater.cpp 3 | * @author Looi Kian Seong 4 | * @date 10-9-2020 5 | * @brief A class for ESP8266 to perform OTA firmware update 6 | * on a connected Arduino Uno 7 | * 8 | */ 9 | 10 | #include "ArduinoOTAFirmwareUpdater.h" 11 | 12 | bool ArduinoOTAFirmwareUpdater::_waitOptibootRes_1s() { 13 | _start_time = millis(); 14 | while((Serial.available() < 2) && !(_is_timeout = ((millis() - _start_time) > TIMEOUT))); 15 | if(_is_timeout || (Serial.read() != 0x14) || (Serial.read() != 0x10)) { 16 | publish(_tx_topic, WARNING_STK_FAILED); 17 | return false; 18 | } 19 | 20 | return true; 21 | } 22 | 23 | void ArduinoOTAFirmwareUpdater::_getSync() { 24 | Serial.write(0x30); 25 | Serial.write(0x20); 26 | if(_waitOptibootRes_1s()) { 27 | publish(_tx_topic, OK); 28 | _is_flashing = true; 29 | _start_time = millis(); 30 | } else { 31 | _is_flashing = false; 32 | } 33 | } 34 | 35 | bool ArduinoOTAFirmwareUpdater::_sendHex(const uint8_t *hex, uint8_t len) { // STK500 Protocol 36 | Serial.write(0x55); 37 | Serial.write(_addr & 0xFF); 38 | Serial.write(_addr >> 8); 39 | Serial.write(0x20); 40 | _hex_size -= len; 41 | 42 | if(_waitOptibootRes_1s()) { 43 | Serial.write(0x64); 44 | Serial.write(0x00); 45 | Serial.write(len); 46 | Serial.write(0x46); 47 | 48 | for(uint8_t i=0;i> 1); 63 | publish(_tx_topic, OK); 64 | } else { 65 | Serial.write(0x51); 66 | Serial.write(0x20); 67 | publish(_tx_topic, "Done flashing!"); 68 | _waitOptibootRes_1s(); 69 | publish(_tx_topic, "Exiting bootloader..."); 70 | publish(_tx_topic, "END"); 71 | return false; 72 | } 73 | } else { 74 | publish(_tx_topic, FLASH_FAILED); 75 | publish(_tx_topic, "END"); 76 | return false; 77 | } 78 | 79 | return true; 80 | } 81 | 82 | void ArduinoOTAFirmwareUpdater::_startFlashing(byte *payload, uint32_t length) { 83 | _hex_size = 0; 84 | 85 | for(uint8_t i=1;i= '0') && (payload_i <= '9')) { 89 | _hex_size *= 10; 90 | _hex_size += payload_i - '0'; 91 | } else { 92 | publish(_tx_topic, "[FAILED] Invalid hex length payload received."); 93 | publish(_tx_topic, "END"); 94 | return; 95 | } 96 | } 97 | 98 | _hex_size_copy = _hex_size; 99 | _addr = 0; 100 | digitalWrite(_reset_pin, LOW); 101 | _flushRxBuff(); 102 | char msg[128]; 103 | sprintf(msg, "[STATUS] Connected Device: %s\n\t\t ESP Programmer Serial Baudrate: %d", _client_id, _baudrate); 104 | publish(_tx_topic, msg); 105 | delay(200); 106 | digitalWrite(_reset_pin, HIGH); 107 | delay(300); 108 | _getSync(); 109 | } 110 | 111 | void ArduinoOTAFirmwareUpdater::_setStartFlashTimeout() { 112 | if(_hex_size == _hex_size_copy) { 113 | if(_is_flashing && ((millis() - _start_time) > TIMEOUT)) { 114 | _is_flashing = false; 115 | } 116 | } 117 | } 118 | 119 | void ArduinoOTAFirmwareUpdater::_callback(char *topic, byte *payload, uint32_t length) { 120 | if(_is_flashing) { 121 | _is_flashing = _sendHex((uint8_t *)payload, (uint8_t)length); 122 | } else { 123 | byte cmd = payload[0]; 124 | if(cmd == CMD_FLASH_START) { 125 | _startFlashing(payload, length); 126 | } else if(cmd == CMD_UPDATE_BAUDRATE) { 127 | _setBaudrate(payload, length); 128 | } 129 | } 130 | } 131 | 132 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoOTAFirmwareUpdater/ArduinoOTAFirmwareUpdater.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ArduinoOTAFirmwareUpdater.h 3 | * @author Looi Kian Seong 4 | * @date 10-9-2020 5 | * @brief A class for ESP8266 to perform OTA firmware update 6 | * on a connected Arduino Uno 7 | * 8 | */ 9 | 10 | #ifndef ArduinoOTAFirmwareUpdater_H 11 | #define ArduinoOTAFirmwareUpdater_H 12 | 13 | #ifndef EXTRA_FUNC_CALL_FLAG 14 | #define EXTRA_FUNC_CALL_FLAG 15 | #endif 16 | 17 | #ifndef DEVICEID 18 | #define DEVICEID Unknown 19 | #endif 20 | 21 | #include 22 | 23 | #define CMD_FLASH_START 0x46 // 'F' 24 | #define OK "OK" 25 | #define WARNING_STK_FAILED "[WARNING] STK FAILED" 26 | #define FLASH_FAILED "[FAILED] FLASHING FAILED" 27 | #define TIMEOUT 1000UL 28 | 29 | class ArduinoOTAFirmwareUpdater: virtual public AbstractPubSub { 30 | public: 31 | ArduinoOTAFirmwareUpdater(WiFiClient *wifi_client, uint8_t reset_pin, int8_t ind_LED=-1): AbstractPubSub(wifi_client, reset_pin, ind_LED), PubSubClient(*wifi_client) { 32 | // _setMQTTCallback(); 33 | _appLoop = ([this] () { this->_setStartFlashTimeout(); }); 34 | } 35 | 36 | protected: 37 | uint16_t _addr; 38 | uint32_t _hex_size; 39 | uint32_t _hex_size_copy; 40 | bool _is_flashing = false; 41 | uint32_t _start_time; 42 | bool _is_timeout = false; 43 | 44 | void _startFlashing(byte *payload, uint32_t length); 45 | void _setStartFlashTimeout(); 46 | void _callback(char *topic, byte *payload, uint32_t length); 47 | bool _waitOptibootRes_1s(); 48 | void _getSync(); 49 | bool _sendHex(const uint8_t *hex, uint8_t len); 50 | }; 51 | 52 | #endif /* ArduinoOTAFirmwareUpdater_H */ 53 | 54 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoOTAFirmwareUpdater/Examples/Arduino_Flasher/Arduino_Flasher.ino: -------------------------------------------------------------------------------- 1 | #define DEVICEID Arduino-1 // Change to a custom id for your deivce, if not define, DEVICEID will be defaulted as Unknown 2 | #include "ArduinoOTAFirmwareUpdater.h" 3 | 4 | #define ssid "" 5 | #define password "" 6 | #define mqtt_server "" 7 | #define mqtt_username "" 8 | #define mqtt_password "" 9 | #define TX tx 10 | #define RX rx 11 | #define tx_topic CREATE_TOPIC(DEVICEID, TX) // Output: "/" 12 | #define rx_topic CREATE_TOPIC(DEVICEID, RX) // Output: "/" 13 | 14 | #define IND_LED 15 | #define RESET_PIN 16 | 17 | WiFiClient espClient; 18 | ArduinoOTAFirmwareUpdater ArduinoFlasher(&espClient, RESET_PIN, IND_LED); 19 | 20 | void wifiSetup() { 21 | WiFi.mode(WIFI_STA); 22 | WiFi.begin(ssid, password); 23 | 24 | while(WiFi.status() != WL_CONNECTED) { 25 | digitalWrite(IND_LED, HIGH); 26 | delay(200); 27 | digitalWrite(IND_LED, LOW); 28 | delay(200); 29 | } 30 | } 31 | 32 | void setup() { 33 | wifiSetup(); 34 | ArduinoFlasher.setServer(mqtt_server, 1883); 35 | ArduinoFlasher.setMQTTCallback(); 36 | ArduinoFlasher.setUsername(mqtt_username); 37 | ArduinoFlasher.setPassword(mqtt_password); 38 | ArduinoFlasher.setTxTopic(tx_topic); 39 | ArduinoFlasher.setRxTopic(rx_topic); 40 | } 41 | 42 | void loop() { 43 | ArduinoFlasher.clientLoop(); 44 | } 45 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoRemoteDebug/ArduinoRemoteDebug.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ArduinoRemoteDebug.cpp 3 | * @author Looi Kian Seong 4 | * @date 10-9-2020 5 | * @brief A class for ESP8266 to perform remote debug printing 6 | * from a connected Arduino Uno 7 | * 8 | */ 9 | 10 | #include "ArduinoRemoteDebug.h" 11 | 12 | void ArduinoRemoteDebug::setReadTimeout(uint32_t timeout) { 13 | _timeout = MIN(timeout, MAX_TIMEOUT); 14 | } 15 | 16 | void ArduinoRemoteDebug::_debugLoop() { 17 | if(_is_debugging) { 18 | char debug_log[128]; 19 | memset(debug_log, 0, 128); 20 | uint8_t n = 0; 21 | uint32_t start_time = millis(); 22 | while(((millis() - start_time) < _timeout) && (n < 127)) { 23 | if(Serial.available()) { 24 | debug_log[n++] = (char)Serial.read(); 25 | start_time = millis(); 26 | } 27 | } 28 | 29 | if(n > 0) { 30 | publish(_tx_topic, debug_log); 31 | } 32 | } 33 | } 34 | 35 | void ArduinoRemoteDebug::_callback(char *topic, byte *payload, uint32_t length) { 36 | byte cmd = payload[0]; 37 | switch(cmd) { 38 | case CMD_RESET_DEVICE: 39 | _resetDevice(); 40 | break; 41 | 42 | case CMD_DEBUG_START: 43 | _startDebugging(); 44 | break; 45 | 46 | case CMD_DEBUG_STOP: 47 | _stopDebugging(); 48 | break; 49 | 50 | case CMD_UPDATE_BAUDRATE: 51 | _setBaudrate(payload, length); 52 | break; 53 | } 54 | } 55 | 56 | void ArduinoRemoteDebug::_resetDevice() { 57 | digitalWrite(_reset_pin, LOW); 58 | _flushRxBuff(); 59 | delay(200); 60 | digitalWrite(_reset_pin, HIGH); 61 | } 62 | 63 | void ArduinoRemoteDebug::_startDebugging() { 64 | _is_debugging = true; 65 | } 66 | 67 | void ArduinoRemoteDebug::_stopDebugging() { 68 | _is_debugging = false; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoRemoteDebug/ArduinoRemoteDebug.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ArduinoRemoteDebug.h 3 | * @author Looi Kian Seong 4 | * @date 10-9-2020 5 | * @brief A class for ESP8266 to perform remote debug printing 6 | * from a connected Arduino Uno 7 | * 8 | */ 9 | 10 | #ifndef ArduinoRemoteDebug_H 11 | #define ArduinoRemoteDebug_H 12 | 13 | #ifndef EXTRA_FUNC_CALL_FLAG 14 | #define EXTRA_FUNC_CALL_FLAG 15 | #endif 16 | 17 | #ifndef DEVICEID 18 | #define DEVICEID Unknown 19 | #endif 20 | 21 | #include 22 | 23 | #define CMD_DEBUG_START 0x44 // 'D' 24 | #define CMD_DEBUG_STOP 0x45 // 'E' 25 | #define CMD_RESET_DEVICE 0x52 // 'R' 26 | #define CMD_PING_DEVICE 0x50 // 'P' 27 | #define MAX_TIMEOUT 1000UL 28 | #define MIN(A, B) (A) < (B)? (A):(B) 29 | 30 | class ArduinoRemoteDebug: virtual public AbstractPubSub { 31 | public: 32 | ArduinoRemoteDebug(WiFiClient *wifi_client, uint8_t reset_pin, int8_t ind_LED=-1): AbstractPubSub(wifi_client, reset_pin, ind_LED), PubSubClient(*wifi_client) { 33 | // _setMQTTCallback(); 34 | _appLoop = ([this] () { this->_debugLoop(); }); 35 | } 36 | void setReadTimeout(uint32_t timeout); 37 | 38 | protected: 39 | uint32_t _timeout; 40 | bool _is_debugging = false; 41 | 42 | void _debugLoop(); 43 | void _callback(char *topic, byte *payload, uint32_t length); 44 | void _resetDevice(); 45 | void _startDebugging(); 46 | void _stopDebugging(); 47 | }; 48 | 49 | #endif /* ArduinoRemoteDebug_H */ 50 | 51 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoRemoteDebug/Examples/Arduino_Debugger/Arduino_Debugger.ino: -------------------------------------------------------------------------------- 1 | #define DEVICEID Arduino-1 // Change to a custom id for your deivce, if not define, DEVICEID will be defaulted as Unknown 2 | #include "ArduinoRemoteDebug.h" 3 | 4 | #define ssid "" 5 | #define password "" 6 | #define mqtt_server "" 7 | #define mqtt_username "" 8 | #define mqtt_password "" 9 | #define TX tx 10 | #define RX rx 11 | #define tx_topic CREATE_TOPIC(DEVICEID, TX) // Output: "/" 12 | #define rx_topic CREATE_TOPIC(DEVICEID, RX) // Output: "/" 13 | 14 | #define IND_LED 15 | #define RESET_PIN 16 | 17 | WiFiClient espClient; 18 | ArduinoRemoteDebug ArduinoDebugger(&espClient, RESET_PIN, IND_LED); 19 | 20 | void wifiSetup() { 21 | WiFi.mode(WIFI_STA); 22 | WiFi.begin(ssid, password); 23 | 24 | while(WiFi.status() != WL_CONNECTED) { 25 | digitalWrite(IND_LED, HIGH); 26 | delay(200); 27 | digitalWrite(IND_LED, LOW); 28 | delay(200); 29 | } 30 | } 31 | 32 | void setup() { 33 | wifiSetup(); 34 | ArduinoDebugger.setServer(mqtt_server, 1883); 35 | ArduinoDebugger.setMQTTCallback(); 36 | ArduinoDebugger.setUsername(mqtt_username); 37 | ArduinoDebugger.setPassword(mqtt_password); 38 | ArduinoDebugger.setTxTopic(tx_topic); 39 | ArduinoDebugger.setRxTopic(rx_topic); 40 | ArduinoDebugger.setReadTimeout(500); 41 | } 42 | 43 | void loop() { 44 | ArduinoDebugger.clientLoop(); 45 | } 46 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoRemoteInterface/ArduinoRemoteInterface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ArduinoRemoteInterface.h 3 | * @author Looi Kian Seong 4 | * @date 10-9-2020 5 | * @brief A class for ESP8266 to perform remote debug printing from and 6 | * OTA firmware update on a connected Arduino Uno 7 | * 8 | */ 9 | 10 | #ifndef ArduinoRemoteInterface_H 11 | #define ArduinoRemoteInterface_H 12 | 13 | #ifndef DEVICEID 14 | #define DEVICEID Unknown 15 | #endif 16 | 17 | #include 18 | #include 19 | 20 | class ArduinoRemoteInterface: public ArduinoOTAFirmwareUpdater, public ArduinoRemoteDebug { 21 | public: 22 | ArduinoRemoteInterface(WiFiClient *wifi_client, uint8_t reset_pin, int8_t ind_LED=-1) 23 | : PubSubClient(*wifi_client), 24 | AbstractPubSub(wifi_client, reset_pin, ind_LED), 25 | ArduinoOTAFirmwareUpdater(wifi_client, reset_pin, ind_LED), 26 | ArduinoRemoteDebug(wifi_client, reset_pin, ind_LED) { 27 | 28 | _appLoop = ([this] () { this->_setStartFlashTimeout(); this->_debugLoop(); }); 29 | // _setMQTTCallback(); 30 | } 31 | 32 | private: 33 | void _callback(char *topic, byte *payload, uint32_t length) { 34 | if(_is_flashing) { 35 | _is_flashing = _sendHex((uint8_t *)payload, (uint8_t)length); 36 | } else { 37 | byte cmd = payload[0]; 38 | switch(cmd) { 39 | case CMD_FLASH_START: 40 | _startFlashing(payload, length); 41 | if(_is_flashing) { 42 | _is_debugging = false; 43 | } 44 | break; 45 | 46 | case CMD_RESET_DEVICE: 47 | _resetDevice(); 48 | break; 49 | 50 | case CMD_DEBUG_START: 51 | _startDebugging(); 52 | break; 53 | 54 | case CMD_DEBUG_STOP: 55 | _stopDebugging(); 56 | break; 57 | 58 | case CMD_UPDATE_BAUDRATE: 59 | _setBaudrate(payload, length); 60 | break; 61 | } 62 | } 63 | } 64 | }; 65 | 66 | #endif /* ArduinoRemoteInterface_H */ 67 | 68 | -------------------------------------------------------------------------------- /ESP8266_Interface/ArduinoRemoteInterface/Examples/Arduino_Remote/Arduino_Remote.ino: -------------------------------------------------------------------------------- 1 | #define DEVICEID Arduino-1 // Change to a custom id for your deivce, if not define, DEVICEID will be defaulted as Unknown 2 | #include "ArduinoRemoteInterface.h" 3 | 4 | #define ssid "" 5 | #define password "" 6 | #define mqtt_server "" 7 | #define mqtt_username "" 8 | #define mqtt_password "" 9 | #define TX tx 10 | #define RX rx 11 | #define tx_topic CREATE_TOPIC(DEVICEID, TX) // Output: "/" 12 | #define rx_topic CREATE_TOPIC(DEVICEID, RX) // Output: "/" 13 | 14 | #define IND_LED 15 | #define RESET_PIN 16 | 17 | WiFiClient espClient; 18 | ArduinoRemoteInterface ArduinoRemote(&espClient, RESET_PIN, IND_LED); 19 | 20 | void wifiSetup() { 21 | WiFi.mode(WIFI_STA); 22 | WiFi.begin(ssid, password); 23 | 24 | while(WiFi.status() != WL_CONNECTED) { 25 | digitalWrite(IND_LED, HIGH); 26 | delay(200); 27 | digitalWrite(IND_LED, LOW); 28 | delay(200); 29 | } 30 | } 31 | 32 | void setup() { 33 | wifiSetup(); 34 | ArduinoRemote.setServer(mqtt_server, 1883); 35 | ArduinoRemote.setMQTTCallback(); 36 | ArduinoRemote.setUsername(mqtt_username); 37 | ArduinoRemote.setPassword(mqtt_password); 38 | ArduinoRemote.setTxTopic(tx_topic); 39 | ArduinoRemote.setRxTopic(rx_topic); 40 | ArduinoRemote.setReadTimeout(500); 41 | } 42 | 43 | void loop() { 44 | ArduinoRemote.clientLoop(); 45 | } 46 | -------------------------------------------------------------------------------- /ESP8266_Interface/README.md: -------------------------------------------------------------------------------- 1 | # Libraries for ESP8266 WiFi module to interface with a connected Arduino Uno. 2 | 3 | ## ArduinoOTAFirmwareUpdater 4 | This library enables **OTA firmware update** function for ESP8266 to update the firmware on the connected Arduino through Serial communication. 5 | It receives the compiled binaries from a host machine through MQTT, then transmits the binaries to the Arduino. 6 | 7 | ## ArduinoRemoteDebug 8 | This library enables **remote debug log** from the connected Arduino through ESP8266. 9 | 10 | ## ArduinoRemoteInterface 11 | This library enables both the abovementioned functions with **OTA firmware update** having higher priority. 12 | 13 | 14 | ## 15 | Any further explanation on the OTA firmwre update can be found from this [article](https://medium.com/@kslooi/ota-firmware-update-on-arduino-85ce78ca2e23). 16 | -------------------------------------------------------------------------------- /Host/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the compiled bytecode 2 | __pycache__/ 3 | 4 | # Ignore the config file for GUI 5 | *.cfg 6 | -------------------------------------------------------------------------------- /Host/ArduinoRemote.py: -------------------------------------------------------------------------------- 1 | import os 2 | import paho.mqtt.client as mqtt 3 | import struct 4 | from time import time, sleep 5 | 6 | HEX_BUFF_SIZE = 128 7 | 8 | 9 | class ArduinoRemote: 10 | def __init__(self, hex_stream_fp, client_id, host, port, username, pw, tx_topic, rx_topic, status_topic, statusLog=print, deviceStatusLog=print, flashLog=print, debugLog=print, exceptionLog=print): 11 | self.n_of_bytes = 0 12 | self.sent_bytes = 0 13 | self.hex_stream_f = None 14 | self.hex_stream_fp = hex_stream_fp 15 | self.host = host 16 | self.port = port 17 | self.username = username 18 | self.pw = pw 19 | self.tx_topic = tx_topic 20 | self.rx_topic = rx_topic 21 | self.status_topic = status_topic 22 | self.is_flashing = False 23 | self.is_debugging = False 24 | self.statusLog = statusLog 25 | self.deviceStatusLog = deviceStatusLog 26 | self.flashLog = flashLog 27 | self.debugLog = debugLog 28 | self.exceptionLog = exceptionLog 29 | self.n_packet = 0 30 | self.is_connected = False 31 | self._start_time = 0 32 | 33 | self.client = mqtt.Client(client_id=client_id) 34 | self.client.on_connect = self._onConnect 35 | self.client.on_disconnect = self._onDisconnect 36 | self.client.on_publish = self._onPublish 37 | self.client.on_message = self._onMessage 38 | self.client.on_subscribe = self._onSubscribe 39 | 40 | def _publish(self, payload): 41 | try: 42 | self.client.publish(topic=self.tx_topic, payload=payload) 43 | 44 | except ValueError as e: 45 | self.exceptionLog(e) 46 | 47 | def _subscribe(self): 48 | try: 49 | self.client.subscribe(topic=self.rx_topic) 50 | self.client.subscribe(topic=self.status_topic) 51 | 52 | except ValueError as e: 53 | self.exceptionLog(e) 54 | 55 | def _onConnect(self, client, userdata, flags, rc): 56 | if rc == 0: 57 | self._subscribe() 58 | self.is_connected = True 59 | self.statusLog('Connected') 60 | 61 | else: 62 | self.statusLog('Connect fail [%d]' % rc) 63 | 64 | def _onDisconnect(self, client, userdata, rc): 65 | self.is_connected = False 66 | self.is_flashing = False 67 | self.is_debugging = False 68 | self.resetOnMessageCallback() 69 | self.statusLog('Disconnected') 70 | self.deviceStatusLog('Unknown') 71 | if rc != 0: 72 | self.client.unsubscribe(self.rx_topic) 73 | self.client.unsubscribe(self.status_topic) 74 | 75 | def _onPublish(self, client, userdata, mid): 76 | pass 77 | 78 | def _onMessage(self, client, userdata, msg): 79 | if msg.topic == self.status_topic: 80 | self.deviceStatusLog(msg.payload.decode()) 81 | 82 | def _onSubscribe(self, client, userdata, mid, granted_qos): 83 | pass 84 | 85 | def _onMessage_flashing(self, client, userdata, msg): 86 | if msg.topic == self.rx_topic: 87 | payload = msg.payload.decode() 88 | if payload == 'OK': 89 | hex_line = self.hex_stream_f.read(HEX_BUFF_SIZE) 90 | if hex_line: 91 | self._publish(hex_line) 92 | n = len(hex_line) 93 | self.sent_bytes = self.sent_bytes + n 94 | self.n_packet = self.n_packet + 1 95 | self.flashLog('[INFO] Sending packet %d: %d bytes' % (self.n_packet, n)) 96 | self.is_flashing = True 97 | 98 | elif payload == 'END': 99 | self.is_flashing = False 100 | self.resetOnMessageCallback() 101 | 102 | else: 103 | self.flashLog('Device: ' + payload) 104 | 105 | else: 106 | self._onMessage(client, userdata, msg) 107 | 108 | def _onMessage_debugging(self, client, userdata, msg): 109 | if msg.topic == self.rx_topic: 110 | self.debugLog(msg.payload) 111 | 112 | else: 113 | self._onMessage(client, userdata, msg) 114 | 115 | def _flushIncomingPayload(self, client, userdata, msg): 116 | if msg.topic == self.rx_topic: 117 | self._start_time = time() 118 | 119 | else: 120 | self._onMessage(client, userdata, msg) 121 | 122 | def _parseHexFile(self, hex_fp): 123 | bytes_count = 0 124 | with open(hex_fp, 'r') as f: 125 | with open(self.hex_stream_fp, 'wb+') as stream_f: 126 | for line in f: 127 | if line[1:3] != '00': 128 | nb = int(line[1:3], 16) 129 | bytes_count = bytes_count + nb 130 | l = line[9:-3] 131 | x = [int(l[i:i+2], 16) for i in range(0, len(l)-1, 2)] 132 | stream_f.write(struct.pack('B' * len(x), *x)) 133 | 134 | return bytes_count 135 | 136 | def _connect(self): 137 | if self.is_debugging: 138 | self.debugStop() 139 | 140 | try: 141 | self.client.unsubscribe(self.rx_topic) 142 | self.client.unsubscribe(self.status_topic) 143 | self.client.loop_stop() 144 | self.client.disconnect() 145 | self.client.username_pw_set(self.username, self.pw) 146 | self.client.will_set(topic=self.tx_topic, payload='E') 147 | self.client.connect(self.host, self.port, 60) 148 | self.client.loop_start() 149 | 150 | except Exception as e: 151 | self.exceptionLog(e) 152 | 153 | def resetOnMessageCallback(self): 154 | self.client.on_message = self._onMessage 155 | 156 | def chooseHexFile(self, hex_fp): 157 | self.flashLog('[INFO] Parsing hex files...') 158 | self.n_of_bytes = self._parseHexFile(hex_fp) 159 | self.hex_stream_f = open(self.hex_stream_fp, 'rb') 160 | self.flashLog('[INFO] Done parsing hex file. Total %d bytes.' % self.n_of_bytes) 161 | 162 | def flashStart(self): 163 | self.n_packet = 0 164 | self.sent_bytes = 0 165 | if self.is_debugging: 166 | self.debugStop() 167 | self.client.on_message = self._flushIncomingPayload 168 | self._start_time = time() 169 | 170 | while (time() - self._start_time) < 1: # set timeout to flush previous incoming payloads 171 | pass 172 | 173 | self.client.on_message = self._onMessage_flashing 174 | if not self.is_connected: 175 | self._connect() 176 | 177 | self._publish('F' + str(self.n_of_bytes)) 178 | 179 | def updateBaudrate(self, baudrate): 180 | if not self.is_connected: 181 | self._connect() 182 | self._publish('U' + str(baudrate)) 183 | 184 | def debugStart(self): 185 | if not self.is_connected: 186 | self._connect() 187 | 188 | self.is_debugging = True 189 | self.client.on_message = self._onMessage_debugging 190 | self._publish('D') 191 | 192 | def debugStop(self): 193 | if not self.is_connected: 194 | self._connect() 195 | 196 | self.is_debugging = False 197 | self.client.on_message = self._onMessage 198 | self._publish('E') 199 | 200 | def resetDevice(self): 201 | if not self.is_connected: 202 | self._connect() 203 | 204 | self._publish('R') 205 | 206 | -------------------------------------------------------------------------------- /Host/README.md: -------------------------------------------------------------------------------- 1 | # Host Application for Remotely Interfacing with Arduino 2 | 3 | ## Install required package(s) 4 | ``` 5 | > cd Host 6 | > pip install -r requirements.txt 7 | ``` 8 | 9 | ## Run the GUI 10 | ``` 11 | > cd Host 12 | > python app.py 13 | ``` 14 | -------------------------------------------------------------------------------- /Host/app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import Tk, Frame, Button, PhotoImage, Label, Entry, Scrollbar, Text, Canvas, filedialog, messagebox, ttk 3 | import threading 4 | import os 5 | from ArduinoRemote import ArduinoRemote 6 | from time import time, sleep 7 | import random 8 | import string 9 | 10 | 11 | def pathJoinCwd(file): 12 | return os.path.join(os.getcwd(), file) 13 | 14 | 15 | def randomId(length): 16 | return ''.join([random.choice(string.ascii_letters + string.digits) for i in range(length)]) 17 | 18 | 19 | APP_ICON = pathJoinCwd('res/icon.png') 20 | CONNECTION_STATUS_IMG = pathJoinCwd('res/cloud_16.png') 21 | DEVICE_STATUS_IMG = pathJoinCwd('res/device_16.png') 22 | OTA_IMG = pathJoinCwd('res/ota_24.png') 23 | DEBUG_IMG = pathJoinCwd('res/debug_24.png') 24 | SETTING_IMG = pathJoinCwd('res/settings_24.png') 25 | UPLOAD_BUTTON_IMG = pathJoinCwd('res/arrow_16.png') 26 | REFRESH_IMG = pathJoinCwd('res/refresh.png') 27 | 28 | 29 | class App(Tk): 30 | def __init__(self, icon='', *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | self.host = '' 33 | self.port = None 34 | self.username = '' 35 | self.pw = '' 36 | self.target_device = '' 37 | self.rx_topic = '' # subscribe topic 38 | self.tx_topic = '' # publish topic 39 | self.status_topic = '' 40 | self.client_id = 'Arduino-Remote-' + randomId(8) 41 | self.stream_fp = os.path.join(os.getcwd(), 'temp/parsed_hex') 42 | 43 | self.protocol("WM_DELETE_WINDOW", self.onClosing) 44 | self.iconphoto(False, PhotoImage(file=icon)) 45 | self.minsize(720, 540) 46 | self.geometry('720x540') 47 | self.side_pane = Frame(self, bg='#d9d9d9') 48 | self.side_pane.pack(side='left', fill='both') 49 | self.status_frame = Frame(self.side_pane, padx=5, bg='#d9d9d9') 50 | self.status_frame.grid_columnconfigure(0, weight=1) 51 | self.status_frame.pack(pady=10, anchor='w') 52 | self.menu_frame = Frame(self.side_pane, bg='#d9d9d9') 53 | self.menu_frame.pack(pady=30) 54 | self.selection_ind = Frame(self.menu_frame, bg='#00d5ff', width=5, height=15) 55 | self.action_pane = Frame(self, bg='white', width=620) 56 | self.action_pane.pack(side='left', fill='both', expand=True) 57 | 58 | # Side pane 59 | self.refresh_img = PhotoImage(file=REFRESH_IMG) 60 | self.refresh_button = Button(self.status_frame, bg='#d9d9d9', relief='flat', bd=0, activebackground='#d9d9d9', image=self.refresh_img, command=self.refreshConnection) 61 | self.refresh_button.grid(row=0, column=0, sticky='nsw', padx=5, pady=10) 62 | self.conn_status_frame = Frame(self.status_frame, bg='#d9d9d9') 63 | self.conn_status_frame.grid(row=1, column=0, sticky='nsew') 64 | self.conn_status_img = PhotoImage(file=CONNECTION_STATUS_IMG) 65 | self.conn_status_img_label = Label(self.conn_status_frame, bg='#d9d9d9', image=self.conn_status_img) 66 | self.conn_status_img_label.pack(side='left', padx=5) 67 | self.connection_status = tk.StringVar(self, value='Disconnected') 68 | self.connection_status_label = Label(self.conn_status_frame, bg='#d9d9d9', fg='#ff0000', textvariable=self.connection_status) 69 | self.connection_status_label.pack(side='left') 70 | 71 | self.dev_status_frame = Frame(self.status_frame, bg='#d9d9d9') 72 | self.dev_status_frame.grid(row=2, column=0, sticky='nsew') 73 | self.dev_status_img = PhotoImage(file=DEVICE_STATUS_IMG) 74 | self.dev_status_img_label = Label(self.dev_status_frame, bg='#d9d9d9', image=self.dev_status_img) 75 | self.dev_status_img_label.pack(side='left', padx=5) 76 | self.device_status = tk.StringVar(self, value='Unknown') 77 | self.device_status_label = Label(self.dev_status_frame, bg='#d9d9d9', fg='#ff0000', textvariable=self.device_status) 78 | self.device_status_label.pack(side='left') 79 | 80 | self.OTA_button = CustomButton(master=self.menu_frame, width=120, height=60, 81 | img_fp=OTA_IMG, text='OTA', compound='left', 82 | anchor='w', indicator=self.selection_ind, padx=20) 83 | self.debug_button = CustomButton(master=self.menu_frame, width=120, height=60, 84 | img_fp=DEBUG_IMG, text='Log', compound='left', 85 | anchor='w', indicator=self.selection_ind, padx=20) 86 | self.setting_button = CustomButton(master=self.menu_frame, width=120, height=60, 87 | img_fp=SETTING_IMG, text='Setting', compound='left', 88 | anchor='w', indicator=self.selection_ind, padx=20) 89 | 90 | self.selection_ind.tkraise() # Bring selection indicator forward 91 | 92 | # Action pane 93 | 94 | ## -> OTA frame 95 | self.OTA_frame = OTAFrame(main_win=self, master=self.action_pane, button=self.OTA_button, bg='#fbfbfb') 96 | 97 | ## -> Debug frame 98 | self.debug_frame = DebugFrame(main_win=self, master=self.action_pane, button=self.debug_button, bg='#fbfbfb') 99 | 100 | ## -> Setting frame 101 | self.setting_frame = SettingFrame(main_win=self, master=self.action_pane, button=self.setting_button, bg='#fbfbfb') 102 | 103 | self.remote = ArduinoRemote(self.stream_fp, self.client_id, self.host, self.port, self.username, self.pw, self.tx_topic, self.rx_topic, 104 | self.status_topic, self.connectionStatusLog, self.deviceStatusLog, self.OTA_frame.log, self.debug_frame.log) 105 | 106 | def refreshConnection(self): 107 | self.remote._connect() 108 | 109 | def connectionStatusLog(self, log): 110 | self.connection_status.set(log) 111 | self.connection_status_label['fg'] = '#11d438' if self.remote.is_connected else '#ff0000' 112 | 113 | def deviceStatusLog(self, log): 114 | self.device_status.set(log) 115 | if log.strip().lower() == 'online': 116 | self.device_status_label['fg'] = '#11d438' 117 | 118 | else: 119 | self.remote.debugStop() 120 | self.debug_frame.debug_button['text'] = 'Debug' 121 | self.device_status_label['fg'] = '#ff0000' 122 | 123 | 124 | def onClosing(self): 125 | if messagebox.askokcancel("Quit", "Do you want to quit?"): 126 | self.destroy() 127 | 128 | 129 | class CustomButton(Button): 130 | count = 0 131 | def __init__(self, master, height, width, bd=0, bg='#d9d9d9', activebackground='#e3e3e3', 132 | relief='flat',img_fp=None, indicator=None, *args, **kwargs): 133 | self.button_frame = Frame(master, height=height, width=width) 134 | self.button_frame.pack_propagate(0) 135 | self.button_frame.pack() 136 | if img_fp: 137 | self.img = PhotoImage(file=img_fp) 138 | 139 | else: 140 | self.img = None 141 | 142 | super().__init__(self.button_frame, height=height, width=width, bd=bd, bg=bg, relief=relief, 143 | activebackground=activebackground, image=self.img, *args, **kwargs) 144 | self.index = CustomButton.count 145 | CustomButton.count = CustomButton.count + 1 146 | self.bind('', self.onEnter) 147 | self.bind('', self.onLeave) 148 | self.indicator = indicator 149 | if self.indicator: 150 | self.indicator_callback = lambda : self.indicator.place(relx=0, rely=0, x=self.indicator['width']/2, 151 | y=(self.index + 0.5)*(self['height']), anchor='center') 152 | self['command'] = self.indicator_callback 153 | self.indicator_callback() 154 | 155 | else: 156 | self.indicator_callback =lambda : None 157 | 158 | self.pack(anchor='n') 159 | 160 | def onEnter(self, e): 161 | self['bg'] = '#ededed' 162 | 163 | def onLeave(self, e): 164 | self['bg'] = '#d9d9d9' 165 | 166 | def setCallback(self, callback): 167 | self['command'] = lambda : (self.indicator_callback(), callback()) 168 | 169 | 170 | class ActionFrame(Frame): 171 | def __init__(self, main_win, button, *args, **kwargs): 172 | super().__init__(*args, **kwargs) 173 | self.main = main_win 174 | self.button = button 175 | self.button.setCallback(lambda: self.tkraise()) 176 | self.place(x=0, y=0, relwidth=1, relheight=1) 177 | 178 | 179 | class OTAFrame(ActionFrame): 180 | def __init__(self, bg, *args, **kwargs): 181 | super().__init__(bg=bg, *args, **kwargs) 182 | self.hex_fp = tk.StringVar(self, value='') 183 | self.grid_columnconfigure(0, weight=1) 184 | self.style = ttk.Style() 185 | 186 | # First row 187 | self.first_row = Frame(self, height=50, bg=bg) 188 | self.first_row.grid(row=0, column=0, sticky='nsew') 189 | self.browsed_label = ttk.Entry(self.first_row, state='readonly', textvariable=self.hex_fp, width=50) 190 | self.browsed_label.pack(side='left', anchor='n', padx=10, pady=10, fill='x', expand=True) 191 | 192 | self.browse_hex_button = ttk.Button(self.first_row, text='Browse', command=self.browseHexFile) 193 | self.browse_hex_button.pack(side='left', anchor='n', padx=10, pady=7) 194 | 195 | self.upload_button_img = PhotoImage(file=UPLOAD_BUTTON_IMG) 196 | self.upload_button = ttk.Button(self.first_row, image=self.upload_button_img, command=self.uploadHex) 197 | self.upload_button.pack(side='left', anchor='n', padx=10, pady=6) 198 | 199 | # Second row 200 | self.second_row = Frame(self, bg=bg) 201 | self.second_row.grid(row=1, column=0, sticky='nsew') 202 | self.ota_progress_bar = ttk.Progressbar(self.second_row, orient='horizontal', mode='determinate') 203 | self.style.layout('upload.Horizontal.TProgressbar', 204 | [*self.style.layout('upload.Horizontal.TProgressbar'), 205 | ('Horizontal.TProgressbar.label', {'sticky': ''})]) 206 | self.style.configure('upload.Horizontal.TProgressbar', text='') 207 | self.ota_progress_bar['style'] = 'upload.Horizontal.TProgressbar' 208 | self.ota_progress_bar.pack(side='left', padx=10, pady=10, anchor='w', fill='x', expand=True) 209 | 210 | # Third row 211 | self.grid_rowconfigure(2, weight=1) 212 | self.third_row = Frame(self, bg='white', bd=0, highlightbackground='#f0f0f0', highlightcolor='#f0f0f0', highlightthickness=1) 213 | self.third_row.grid(row=2, column=0, padx=10, pady=10, sticky='nsew') 214 | self.log_scrollbar = Scrollbar(self.third_row, orient='vertical') 215 | self.log_scrollbar.pack(side='right', anchor='w', fill='y') 216 | self.log_text_box = Text(self.third_row, relief='solid', bd=0, yscrollcommand=self.log_scrollbar.set, padx=5) 217 | self.log_text_box.pack(side='left', anchor='w', fill='both', expand=True) 218 | self.log_text_box.bind('', lambda key: 'break') 219 | self.log_scrollbar['command'] = self.log_text_box.yview 220 | 221 | def browseHexFile(self): 222 | self.hex_fp.set(filedialog.askopenfilename(title='Select hex file', filetypes=[('Hex files', '*.hex')])) 223 | 224 | def log(self, log): 225 | self.log_text_box.insert('end', log + '\n') 226 | self.log_text_box.see('end') 227 | 228 | def uploadHex(self): 229 | self.upload_button['state'] = 'disabled' 230 | self.browse_hex_button['state'] = 'disabled' 231 | self.main.debug_button['state'] = 'disabled' 232 | self.main.setting_button['state'] = 'disabled' 233 | self.main.refresh_button['state'] = 'disabled' 234 | self.style.configure('upload.Horizontal.TProgressbar', text='') 235 | self.ota_progress_bar['value'] = 0 236 | self.log_text_box.delete(1.0, 'end') 237 | fp = self.hex_fp.get() 238 | if os.path.isfile(fp): 239 | flasher = self.main.remote 240 | TIMEOUT = 5 241 | flasher.chooseHexFile(self.hex_fp.get()) 242 | flasher.flashStart() 243 | self.main.debug_frame.debug_button['text'] = 'Debug' 244 | self.thread = threading.Thread(target=self.setTimeout, args=(TIMEOUT, )) 245 | self.thread.start() 246 | 247 | else: 248 | messagebox.showerror(title='Error', message='Invalid hex file!') 249 | self.upload_button['state'] = 'normal' 250 | self.browse_hex_button['state'] = 'normal' 251 | self.main.debug_button['state'] = 'normal' 252 | self.main.setting_button['state'] = 'normal' 253 | self.main.refresh_button['state'] = 'normal' 254 | 255 | # set timeout - if no response from device, then end the process. 256 | def setTimeout(self, timeout): 257 | flasher = self.main.remote 258 | start_time = time() 259 | self.ota_progress_bar['mode'] = 'indeterminate' 260 | self.ota_progress_bar['maximum'] = 50 261 | self.ota_progress_bar['value'] = 0 262 | self.ota_progress_bar.start() 263 | self.log('[INFO] Preparing to flash %s.' % self.main.target_device) 264 | while 1: 265 | sleep(0.01) 266 | self.update_idletasks() 267 | if flasher.is_flashing or (time() - start_time) >= timeout: 268 | break 269 | 270 | self.ota_progress_bar.stop() 271 | self.ota_progress_bar['value'] = 0 272 | self.ota_progress_bar['mode'] = 'determinate' 273 | self.ota_progress_bar['maximum'] = flasher.n_of_bytes 274 | if flasher.is_flashing: 275 | start_time = time() 276 | prev_sent_bytes = 0 277 | update_time = time() 278 | while flasher.is_flashing: 279 | self.ota_progress_bar['value'] = flasher.sent_bytes 280 | self.style.configure('upload.Horizontal.TProgressbar', text='%.2f%%' % ((flasher.sent_bytes / flasher.n_of_bytes) * 100)) 281 | is_timeout = time() - update_time > 3 282 | sleep(0.001) 283 | if (flasher.sent_bytes != prev_sent_bytes ) and not is_timeout: 284 | prev_sent_bytes = flasher.sent_bytes 285 | update_time = time() 286 | 287 | elif is_timeout: 288 | self.log('[FAILED] Timeout in waiting response from the device!') 289 | flasher.is_flashing = False 290 | 291 | self.update_idletasks() 292 | 293 | end_time = time() 294 | self.log('[INFO] Time elapsed: %.2f s' % (end_time - start_time)) 295 | 296 | else: 297 | self.log('[FAILED] Not response from device!') 298 | 299 | flasher.resetOnMessageCallback() 300 | flasher.hex_stream_f.close() 301 | 302 | self.upload_button['state'] = 'normal' 303 | self.browse_hex_button['state'] = 'normal' 304 | self.main.debug_button['state'] = 'normal' 305 | self.main.setting_button['state'] = 'normal' 306 | self.main.refresh_button['state'] = 'normal' 307 | 308 | 309 | class DebugFrame(ActionFrame): 310 | def __init__(self, bg, *args, **kwargs): 311 | super().__init__(bg=bg, *args, **kwargs) 312 | self.grid_columnconfigure(0, weight=1) 313 | 314 | # First row 315 | self.first_row = Frame(self, bg=bg) 316 | self.first_row.grid(row=0, column=0, padx=10, sticky='nsew') 317 | self.frame_label = Label(self.first_row, text='Debug Log', bg=bg, font=('Calibri', 12)) 318 | self.frame_label.pack(side='top', anchor='w', pady=10) 319 | 320 | # Second row 321 | self.grid_rowconfigure(1, weight=1) 322 | self.second_row = Frame(self, bg='white', bd=0, highlightbackground='#f0f0f0', highlightcolor='#f0f0f0', highlightthickness=1) 323 | self.second_row.grid(row=1, column=0, padx=10, pady=10, sticky='nsew') 324 | self.log_scrollbar = Scrollbar(self.second_row, orient='vertical') 325 | self.log_scrollbar.pack(side='right', anchor='w', fill='y') 326 | self.log_text_box = Text(self.second_row, relief='solid', bd=0, yscrollcommand=self.log_scrollbar.set, padx=5) 327 | self.log_text_box.pack(side='left', anchor='w', fill='both', expand=True) 328 | self.log_text_box.bind('', lambda key: 'break') 329 | self.log_scrollbar['command'] = self.log_text_box.yview 330 | 331 | # Third row 332 | self.third_row = Frame(self, height=50, bg=bg) 333 | self.third_row.grid(row=2, column=0, pady=10, sticky='nsew') 334 | 335 | self.debug_button = ttk.Button(self.third_row, text='Debug', command=self.toggleDebug) 336 | self.debug_button.pack(side='left', anchor='n', padx=10) 337 | 338 | self.clearlog_button = ttk.Button(self.third_row, text='Clear', command=self.clearLog) 339 | self.clearlog_button.pack(side='left', anchor='n', padx=10) 340 | 341 | self.reset_button = ttk.Button(self.third_row, text='Reset', command=lambda: self.main.remote.resetDevice()) 342 | self.reset_button.pack(side='right', padx=10) 343 | 344 | self.baudrate = tk.IntVar(self, value=115200) 345 | self.baudrate_list = (300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200, 230400, 250000, 500000, 1000000, 2000000) 346 | self.baudrate_menu = ttk.Combobox(self.third_row, state='readonly', textvariable=self.baudrate, values=self.baudrate_list) 347 | self.baudrate_menu.pack(side='right', padx=10) 348 | self.baudrate_menu.bind('<>', lambda e: self.main.remote.updateBaudrate(self.baudrate.get())) 349 | 350 | def toggleDebug(self): 351 | debugger = self.main.remote 352 | if debugger.is_debugging: 353 | debugger.debugStop() 354 | self.debug_button['text'] = 'Debug' 355 | 356 | else: 357 | debugger.debugStart() 358 | self.debug_button['text'] = 'Stop' 359 | 360 | def clearLog(self): 361 | self.log_text_box.delete(1.0, 'end') 362 | 363 | def log(self, log): 364 | self.log_text_box.insert('end', log) 365 | self.log_text_box.see('end') 366 | 367 | 368 | class CustomEntry(ttk.Entry): 369 | row = 0 370 | def __init__(self, master, bg, label, entry_prefix=None, *args, **kwargs): 371 | row = CustomEntry.row 372 | CustomEntry.row = CustomEntry.row + 1 373 | self.label = Label(master=master, text=label, bg=bg) 374 | self.label.grid(row=row, column=0, sticky='nse', padx=10, pady=10) 375 | 376 | if entry_prefix is None: 377 | super().__init__(master=master, *args, **kwargs) 378 | self.grid(row=row, column=1, sticky='nsew', padx=10, pady=10) 379 | 380 | else: 381 | self.entry_frame = Frame(master=master, bg=bg) 382 | self.entry_frame.grid(row=row, column=1, sticky='nsew', padx=10, pady=10) 383 | self.entry_prefix = ttk.Entry(self.entry_frame, textvariable=entry_prefix, state='readonly') 384 | self.entry_prefix.pack(side='left', anchor='w') 385 | Label(self.entry_frame, text='/', bg=bg).pack(side='left') 386 | super().__init__(master=self.entry_frame, *args, **kwargs) 387 | self.pack(side='left', expand=True, fill='x') 388 | 389 | 390 | class SettingFrame(ActionFrame): 391 | def __init__(self, bg, *args, **kwargs): 392 | super().__init__(bg=bg, *args, **kwargs) 393 | self.mqtt_config_fp = os.path.join(os.getcwd(), 'MQTTConfig.cfg') 394 | self.grid_rowconfigure(0, weight=1) 395 | self.grid_columnconfigure(0, weight=1) 396 | 397 | 398 | # Setting frame 399 | self.setting_frame = Frame(self, bg=bg) 400 | self.setting_frame.grid(row=0, column=0, pady=20, sticky='nsew') 401 | self.setting_frame.grid_columnconfigure(1, weight=1) 402 | 403 | ## -> Host 404 | self.host = tk.StringVar(self, value='') 405 | self.host_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Host', textvariable=self.host) 406 | 407 | ## -> Port 408 | self.port = tk.IntVar(self) 409 | self.port_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Port', textvariable=self.port) 410 | 411 | ## -> Username 412 | self.username = tk.StringVar(self, value='') 413 | self.username_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Username', textvariable=self.username) 414 | 415 | ## -> Password 416 | self.pw = tk.StringVar(self, value='') 417 | self.pw_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Password', textvariable=self.pw, show='\u2022') 418 | 419 | ## -> Target device 420 | self.target_device = tk.StringVar(self, value='') 421 | self.tDevice_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Target Device', textvariable=self.target_device) 422 | 423 | ## -> Rx topic 424 | self.rx_topic = tk.StringVar(self, value='') 425 | self.rx_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Rx Topic', entry_prefix=self.target_device, textvariable=self.rx_topic) 426 | 427 | ## -> Tx topic 428 | self.tx_topic = tk.StringVar(self, value='') 429 | self.tx_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Tx Topic', entry_prefix=self.target_device, textvariable=self.tx_topic) 430 | 431 | ## -> Device status topic 432 | self.status_topic = tk.StringVar(self, value='') 433 | self.status_entry = CustomEntry(master=self.setting_frame, bg=bg, label='Device Status Topic', entry_prefix=self.target_device, textvariable=self.status_topic) 434 | 435 | self.readConfig() 436 | 437 | # Button frame 438 | self.button_frame = Frame(self, bg=bg) 439 | self.button_frame.grid(row=1, column=0, pady=20, sticky='nsew') 440 | 441 | ## Apply settings button 442 | self.apply_button = ttk.Button(self.button_frame, text='Apply', command=self.apply) 443 | self.apply_button.pack(side='right', padx=10) 444 | 445 | def saveConfig(self): 446 | cfg_key_list = ('host', 'port', 'username', 'target_device', 'rx_topic', 'tx_topic', 'device_status_topic') 447 | cfg_value_list = ( 448 | self.host.get(), 449 | self.port.get(), 450 | self.username.get(), 451 | self.target_device.get(), 452 | self.rx_topic.get(), 453 | self.tx_topic.get(), 454 | self.status_topic.get() 455 | ) 456 | 457 | with open(self.mqtt_config_fp, 'w+') as f: 458 | f.write('\n'.join(['%s=%s' % (k, v) for k, v in zip(cfg_key_list, cfg_value_list)])) 459 | 460 | def readConfig(self): 461 | if os.path.isfile(self.mqtt_config_fp): 462 | cfg_key_list = ('host', 'port', 'username', 'target_device', 'rx_topic', 'tx_topic', 'device_status_topic') 463 | cfg_dict = {k: '' for k in cfg_key_list} 464 | with open(self.mqtt_config_fp, 'r+') as f: 465 | config_list = f.readlines() 466 | for cfg in config_list: 467 | key, value = [el.strip() for el in cfg.split('=')] 468 | if key in cfg_key_list: 469 | cfg_dict[key] = value 470 | 471 | self.host.set(cfg_dict['host']) 472 | self.port.set(int(cfg_dict['port']) if cfg_dict['port'].isdigit() else '') 473 | self.username.set(cfg_dict['username']) 474 | self.target_device.set(cfg_dict['target_device']) 475 | self.rx_topic.set(cfg_dict['rx_topic'] if cfg_dict['rx_topic'] else 'tx') 476 | self.tx_topic.set(cfg_dict['tx_topic'] if cfg_dict['rx_topic'] else 'rx') 477 | self.status_topic.set(cfg_dict['device_status_topic']) 478 | 479 | main = self.main 480 | main.host = self.host.get() 481 | main.port = self.port.get() 482 | main.username = self.username.get() 483 | main.pw = self.pw.get() 484 | main.target_device = self.target_device.get() 485 | main.rx_topic = main.target_device + '/' + self.rx_topic.get() 486 | main.tx_topic = main.target_device + '/' + self.tx_topic.get() 487 | main.status_topic = main.target_device + '/' + self.status_topic.get() 488 | 489 | def apply(self): 490 | self.saveConfig() 491 | self.main.debug_frame.debug_button['text'] = 'Debug' 492 | remote = self.main.remote 493 | remote.host = self.host.get() 494 | remote.port = self.port.get() 495 | remote.username = self.username.get() 496 | remote.pw = self.pw.get() 497 | remote.target_device = self.target_device.get() 498 | remote.tx_topic = remote.target_device + '/' + self.tx_topic.get() 499 | remote.rx_topic = remote.target_device + '/' + self.rx_topic.get() 500 | remote.status_topic = remote.target_device + '/' + self.status_topic.get() 501 | remote._connect() 502 | 503 | 504 | app = App(icon=APP_ICON, className=' Remote Arduino') 505 | app.mainloop() 506 | -------------------------------------------------------------------------------- /Host/requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt==1.5.0 2 | -------------------------------------------------------------------------------- /Host/res/arrow_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/arrow_16.png -------------------------------------------------------------------------------- /Host/res/cloud_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/cloud_16.png -------------------------------------------------------------------------------- /Host/res/debug_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/debug_24.png -------------------------------------------------------------------------------- /Host/res/device_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/device_16.png -------------------------------------------------------------------------------- /Host/res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/icon.png -------------------------------------------------------------------------------- /Host/res/ota_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/ota_24.png -------------------------------------------------------------------------------- /Host/res/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/refresh.png -------------------------------------------------------------------------------- /Host/res/refresh_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/refresh_16.png -------------------------------------------------------------------------------- /Host/res/refresh_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/refresh_24.png -------------------------------------------------------------------------------- /Host/res/refresh_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/refresh_32.png -------------------------------------------------------------------------------- /Host/res/settings_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/Host/res/settings_24.png -------------------------------------------------------------------------------- /Host/temp/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the temp file created when uploading hex file 2 | * 3 | !.gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Arduino 2 | _By Looi Kian Seong_ 3 | 4 | ## Source Code Organisation 5 | ``` 6 | Repository 7 | ├── docs 8 | ├── ESP8266_Interface 9 | │ ├──AbstractPubSub 10 | │ ├──ArduinoOTAFirmwareUpdater 11 | │ ├──ArduinoRemoteDebug 12 | │ └──ArduinoRemoteInterface 13 | └── Host 14 | ``` 15 | 16 | The `docs` folder contains the image(s) used for explanation. 17 | 18 | The `ESP8266_Interface` folder contains the libraries developed for ESP8266 WiFi module to interface with Arduino Uno 19 | * **AbstractPubSub**: Defines an abstract class which contains some basic methods which will be implemented by child classes and a pure virtual method which will be defined and implemented by child classes. 20 | * **ArduinoOTAFirmwareUpdater**: Defines the class used for performing OTA firmware update on the connected Arduino Uno. 21 | * **ArduinoRemoteDebug**: Defines the class used for remote log debug messages from the connected Arduino Uno. 22 | * **ArduinoRemoteInterface**: Defines the class used for both OTA firmware update and remote debug log. 23 | 24 | The `Host` folder contains the Python script developed for the host machine to interface with the remote Arduino connected by ESP8266 WiFi module. 25 | 26 | ## Circuit Connection 27 | 28 | 29 | ## Include the ESP8266 libraries for Arduino IDE 30 | After cloning this repo, goes to `ESP8266_Interface` folder, manually copy all the folders in `ESP8266_Interface` to the Arduino libraries location. 31 | Guide to manual library installation can be found from this [link](https://www.arduino.cc/en/guide/libraries#toc5). 32 | 33 | **Step 1:** 34 | 35 | ![](docs/img/copy_lib.PNG) 36 | 37 | **Step 2:** 38 | 39 | ![](docs/img/paste_lib.PNG) 40 | -------------------------------------------------------------------------------- /docs/img/connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/docs/img/connection.png -------------------------------------------------------------------------------- /docs/img/copy_lib.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/docs/img/copy_lib.PNG -------------------------------------------------------------------------------- /docs/img/paste_lib.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayLooi/RemoteArduino/25b313d0fc6ca7307efc3cfa7ac9bd52ce817f22/docs/img/paste_lib.PNG --------------------------------------------------------------------------------