From 2fa258ccb0a8dac419f926ff15ba8f7d66b18d0f Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 5 Apr 2019 18:07:29 +0200 Subject: [PATCH] 1.1.7 --- lib/MyESP/MyESP.cpp | 1536 +++++++++++++++++++++++++++++++++++++++++++ lib/MyESP/MyESP.h | 294 +++++++++ 2 files changed, 1830 insertions(+) create mode 100644 lib/MyESP/MyESP.cpp create mode 100644 lib/MyESP/MyESP.h diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp new file mode 100644 index 000000000..c8513d94b --- /dev/null +++ b/lib/MyESP/MyESP.cpp @@ -0,0 +1,1536 @@ +/* + * MyESP - my ESP helper class to handle WiFi, MQTT and Telnet + * + * Paul Derbyshire - December 2018 + * + * Ideas borrowed from Espurna https://github.com/xoseperez/espurna + */ + +#include "MyESP.h" + +#ifdef CRASH +EEPROM_Rotate EEPROMr; +#endif + +// constructor +MyESP::MyESP() { + _app_hostname = strdup("MyESP"); + _app_name = strdup("MyESP"); + _app_version = strdup(MYESP_VERSION); + + _boottime = NULL; + _load_average = 100; // calculated load average + + _telnetcommand_callback = NULL; + _telnet_callback = NULL; + + _command[0] = '\0'; + + _fs_callback = NULL; + _fs_settings_callback = NULL; + + _helpProjectCmds = NULL; + _helpProjectCmds_count = 0; + + _use_serial = false; + _mqtt_host = NULL; + _mqtt_password = NULL; + _mqtt_username = NULL; + _mqtt_retain = false; + _mqtt_keepalive = 300; + _mqtt_will_topic = NULL; + _mqtt_will_online_payload = NULL; + _mqtt_will_offline_payload = NULL; + _mqtt_base = NULL; + _mqtt_topic = NULL; + _mqtt_qos = 0; + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + _mqtt_last_connection = 0; + _mqtt_connecting = false; + + _wifi_password = NULL; + _wifi_ssid = NULL; + _wifi_callback = NULL; + _wifi_connected = false; + + _ota_pre_callback = NULL; + _ota_post_callback = NULL; + + _suspendOutput = false; +} + +MyESP::~MyESP() { + end(); +} + +// end +void MyESP::end() { + SerialAndTelnet.end(); + jw.disconnect(); +} + +// general debug to the telnet or serial channels +void MyESP::myDebug(const char * format, ...) { + if (_suspendOutput) + return; + + va_list args; + va_start(args, format); + char test[1]; + + int len = ets_vsnprintf(test, 1, format, args) + 1; + + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + va_end(args); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// for flashmemory. Must use PSTR() +void MyESP::myDebug_P(PGM_P format_P, ...) { + if (_suspendOutput) + return; + + char format[strlen_P(format_P) + 1]; + memcpy_P(format, format_P, sizeof(format)); + + va_list args; + va_start(args, format_P); + char test[1]; + int len = ets_vsnprintf(test, 1, format, args) + 1; + + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + + va_end(args); + +#ifdef MYESP_TIMESTAMP + // capture & print timestamp + char timestamp[10] = {0}; + snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); + SerialAndTelnet.print(timestamp); +#endif + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// use Serial? +bool MyESP::getUseSerial() { + return (_use_serial); +} + +// called when WiFi is connected, and used to start OTA, MQTT +void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { + if ((code == MESSAGE_CONNECTED)) { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + + myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str()); + myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel()); + myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str()); + myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str()); + myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] HOST %s"), hostname.c_str()); + + // start OTA + ArduinoOTA.begin(); // moved to support esp32 + myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); + + // MQTT Setup + _mqtt_setup(); + + _wifi_connected = true; + + // finally if we don't want Serial anymore, turn it off + if (!_use_serial) { + myDebug_P(PSTR("Disabling serial port")); + SerialAndTelnet.setSerial(NULL); + } else { + myDebug_P(PSTR("Using serial port output")); + } + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + + jw.enableAPFallback(false); // Disable AP mode after initial connect was succesfull. Thanks @JewelZB + } + + if (code == MESSAGE_ACCESSPOINT_CREATED) { + _wifi_connected = true; + + myDebug_P(PSTR("[WIFI] MODE AP --------------------------------------")); + myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); + + // we could be in panic mode so enable Serial again + if (!_use_serial) { + SerialAndTelnet.setSerial(&Serial); + _use_serial = true; + } + + myDebug_P(PSTR("Enabling serial port output")); + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + } + + if (code == MESSAGE_CONNECTING) { + myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_CONNECT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_DISCONNECTED) { + myDebug_P(PSTR("[WIFI] Disconnected")); + _wifi_connected = false; + } +} + +// return true if in WiFi AP mode +// does not work after wifi reset on ESP32 yet. See https://github.com/espressif/arduino-esp32/issues/1306 +bool MyESP::isAPmode() { + return (WiFi.getMode() & WIFI_AP); +} + +// received MQTT message +// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT +void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { + if (len == 0) + return; + + char message[len + 1]; + strlcpy(message, (char *)payload, len + 1); + + // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging + + // topics are in format MQTT_BASE/HOSTNAME/TOPIC + char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / + if (topic_magnitude != nullptr) { + topic = topic_magnitude + 1; + } + + // check for standard messages + // Restart the device + if (strcmp(topic, MQTT_TOPIC_RESTART) == 0) { + myDebug_P(PSTR("[MQTT] Received restart command"), message); + myESP.resetESP(); + return; + } + + // handle response from a start message + // for example with HA it sends the system time from the server + if (strcmp(topic, MQTT_TOPIC_START) == 0) { + myDebug_P(PSTR("[MQTT] Received boottime: %s"), message); + myESP.setBoottime(message); + return; + } + + // Send message event to custom service + (_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message); +} + +// MQTT subscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttSubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos); + myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT unsubscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttUnsubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); + myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT Publish +void MyESP::mqttPublish(const char * topic, const char * payload) { + // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); + mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload); +} + +// MQTT onConnect - when a connect is established +void MyESP::_mqttOnConnect() { + myDebug_P(PSTR("[MQTT] Connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + + _mqtt_last_connection = millis(); + + // say we're alive to the Last Will topic + mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); + + // subscribe to general subs + mqttSubscribe(MQTT_TOPIC_RESTART); + + // subscribe to a start message and send the first publish + myESP.mqttSubscribe(MQTT_TOPIC_START); + myESP.mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD); + + // call custom function to handle mqtt receives + (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); +} + +// MQTT setup +void MyESP::_mqtt_setup() { + if (!_mqtt_host) { + myDebug_P(PSTR("[MQTT] disabled")); + } + + mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); + + mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { + myDebug_P(PSTR("[MQTT] TCP Disconnected")); + (_mqtt_callback)(MQTT_DISCONNECT_EVENT, NULL, NULL); // call callback with disconnect + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { + myDebug_P(PSTR("[MQTT] Identifier Rejected")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { + myDebug_P(PSTR("[MQTT] Server unavailable")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { + myDebug_P(PSTR("[MQTT] Malformed credentials")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { + myDebug_P(PSTR("[MQTT] Not authorized")); + } + + // Reset reconnection delay + _mqtt_last_connection = millis(); + _mqtt_connecting = false; + }); + + //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); }); + + //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); }); + + mqttClient.onMessage( + [this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + _mqttOnMessage(topic, payload, len); + }); +} + +// WiFI setup +void MyESP::_wifi_setup() { + jw.setHostname(_app_hostname); // Set WIFI hostname + jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); + jw.enableAP(false); + jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); + jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL); + jw.enableAPFallback(true); // AP mode only as fallback + jw.enableSTA(true); // Enable STA mode (connecting to a router) + jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm + jw.cleanNetworks(); // Clean existing network configuration + jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network + +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 +#endif +} + +// set the callback function for the OTA onstart +void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post) { + _ota_pre_callback = OTACallback_pre; + _ota_post_callback = OTACallback_post; +} + +// OTA callback when the upload process starts +void MyESP::_OTACallback() { + myDebug_P(PSTR("[OTA] Start")); + +#ifdef CRASH + // If we are not specifically reserving the sectors we are using as + // EEPROM in the memory layout then any OTA upgrade will overwrite + // all but the last one. + // Calling rotate(false) disables rotation so all writes will be done + // to the last sector. It also sets the dirty flag to true so the next commit() + // will actually persist current configuration to that last sector. + // Calling rotate(false) will also prevent any other EEPROM write + // to overwrite the OTA image. + // In case the OTA process fails, reenable rotation. + // See onError callback below. + EEPROMr.rotate(false); + EEPROMr.commit(); +#endif + + if (_ota_pre_callback) { + (_ota_pre_callback)(); // call custom function + } +} + +// OTA Setup +void MyESP::_ota_setup() { + if (!_wifi_ssid) { + return; + } + + //ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(_app_hostname); + + ArduinoOTA.onStart([this]() { _OTACallback(); }); + ArduinoOTA.onEnd([this]() { myDebug_P(PSTR("[OTA] Done, restarting...")); }); + ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { + static unsigned int _progOld; + unsigned int _prog = (progress / (total / 100)); + if (_prog != _progOld) { + myDebug_P(PSTR("[OTA] Progress: %u%%\r"), _prog); + _progOld = _prog; + } + }); + + ArduinoOTA.onError([this](ota_error_t error) { + if (error == OTA_AUTH_ERROR) + myDebug_P(PSTR("[OTA] Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + myDebug_P(PSTR("[OTA] Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + myDebug_P(PSTR("[OTA] Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + myDebug_P(PSTR("[OTA] Receive Failed")); + else if (error == OTA_END_ERROR) + myDebug_P(PSTR("[OTA] End Failed")); + +#ifdef CRASH + // There's been an error, reenable rotation + EEPROMr.rotate(true); +#endif + }); +} + +// sets boottime +void MyESP::setBoottime(const char * boottime) { + if (_boottime) { + free(_boottime); + } + _boottime = strdup(boottime); +} + +// eeprom +void MyESP::_eeprom_setup() { +#ifdef CRASH + EEPROMr.size(4); + EEPROMr.begin(SPI_FLASH_SEC_SIZE); +#endif +} + +// Set callback of sketch function to process project messages +void MyESP::setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { + _helpProjectCmds = cmds; // command list + _helpProjectCmds_count = count; // number of commands + _telnetcommand_callback = callback_cmd; // external function to handle commands + _telnet_callback = callback; +} + +void MyESP::_telnetConnected() { + myDebug_P(PSTR("[TELNET] Telnet connection established")); + _consoleShowHelp(); // Show the initial message + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_CONNECT); // call callback + } +} + +void MyESP::_telnetDisconnected() { + myDebug_P(PSTR("[TELNET] Telnet connection closed")); + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_DISCONNECT); // call callback + } +} + +// Initialize the telnet server +void MyESP::_telnet_setup() { + SerialAndTelnet.setWelcomeMsg(""); + SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); + SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); + SerialAndTelnet.setDebugOutput(false); + SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200 + + // init command buffer for console commands + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); +} + +// Show help of commands +void MyESP::_consoleShowHelp() { + myDebug_P(PSTR("")); + myDebug_P(PSTR("* Connected to: %s version %s"), _app_name, _app_version); + + if (isAPmode()) { + myDebug_P(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + } else { + myDebug_P(PSTR("* Hostname: %s (%s)"), _getESPhostname().c_str(), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); + myDebug_P(PSTR("* MQTT is %s"), mqttClient.connected() ? "connected" : "disconnected"); + } + + myDebug_P(PSTR("*")); + myDebug_P(PSTR("* Commands:")); + myDebug_P(PSTR("* ?=help, CTRL-D=quit telnet")); + myDebug_P(PSTR("* set, system, reboot")); +#ifdef CRASH + myDebug_P(PSTR("* crash ")); +#endif + + // print custom commands if available. Taken from progmem + if (_telnetcommand_callback) { + // find the longest key length so we can right align it + uint8_t max_len = 0; + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if ((strlen(_helpProjectCmds[i].key) > max_len) && (!_helpProjectCmds[i].set)) { + max_len = strlen(_helpProjectCmds[i].key); + } + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (!_helpProjectCmds[i].set) { + SerialAndTelnet.print(FPSTR("* ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); + } + } + } + myDebug_P(PSTR("")); // newline +} + +// print all set commands and current values +void MyESP::_printSetCommands() { + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("The following set commands are available:")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("* set erase")); + myDebug_P(PSTR("* set [value]")); + myDebug_P(PSTR("* set [value]")); + myDebug_P(PSTR("* set serial ")); + + // print custom commands if available. Taken from progmem + if (_telnetcommand_callback) { + // find the longest key length so we can right align it + uint8_t max_len = 0; + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if ((strlen(_helpProjectCmds[i].key) > max_len) && (_helpProjectCmds[i].set)) { + max_len = strlen(_helpProjectCmds[i].key); + } + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (_helpProjectCmds[i].set) { + SerialAndTelnet.print(FPSTR("* set ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); + } + } + } + + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("Stored settings:")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" wifi_ssid=%s "), (!_wifi_ssid) ? "" : _wifi_ssid); + SerialAndTelnet.print(FPSTR(" wifi_password=")); + if (!_wifi_password) { + SerialAndTelnet.print(FPSTR("")); + } else { + for (uint8_t i = 0; i < strlen(_wifi_password); i++) { + SerialAndTelnet.print(FPSTR("*")); + } + } + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" mqtt_host=%s"), (!_mqtt_host) ? "" : _mqtt_host); + myDebug_P(PSTR(" mqtt_username=%s"), (!_mqtt_username) ? "" : _mqtt_username); + SerialAndTelnet.print(FPSTR(" mqtt_password=")); + if (!_mqtt_password) { + SerialAndTelnet.print(FPSTR("")); + } else { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) { + SerialAndTelnet.print(FPSTR("*")); + } + } + + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" serial=%s"), (_use_serial) ? "on" : "off"); + + // print any custom settings + (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + + myDebug_P(PSTR("")); // newline +} + +// reset / restart +void MyESP::resetESP() { + myDebug_P(PSTR("* Reboot ESP...")); + end(); +#if defined(ARDUINO_ARCH_ESP32) + ESP.restart(); +#else + ESP.restart(); +#endif +} + +// read next word from string buffer +// if parameter true then a word is only terminated by a newline +char * MyESP::_telnet_readWord(bool allow_all_chars) { + if (allow_all_chars) { + return (strtok(NULL, "\n")); // allow only newline + } else { + return (strtok(NULL, ", \n")); // allow space and comma + } +} + +// change settings - always as strings +// messy code but effective since we don't have too many settings +// wc is word count, number of parameters after the 'set' command +bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { + bool ok = false; + + // check for our internal commands first + if (strcmp(setting, "erase") == 0) { + _fs_eraseConfig(); + return true; + + } else if (strcmp(setting, "wifi_ssid") == 0) { + if (_wifi_ssid) + free(_wifi_ssid); + _wifi_ssid = NULL; // just to be sure + if (value) { + _wifi_ssid = strdup(value); + } + ok = true; + jw.enableSTA(false); + myDebug_P(PSTR("Note: please reboot to apply new WiFi settings")); + } else if (strcmp(setting, "wifi_password") == 0) { + if (_wifi_password) + free(_wifi_password); + _wifi_password = NULL; // just to be sure + if (value) { + _wifi_password = strdup(value); + } + ok = true; + jw.enableSTA(false); + myDebug_P(PSTR("Note: please reboot to apply new WiFi settings")); + + } else if (strcmp(setting, "mqtt_host") == 0) { + if (_mqtt_host) + free(_mqtt_host); + _mqtt_host = NULL; // just to be sure + if (value) { + _mqtt_host = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_username") == 0) { + if (_mqtt_username) + free(_mqtt_username); + _mqtt_username = NULL; // just to be sure + if (value) { + _mqtt_username = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_password") == 0) { + if (_mqtt_password) + free(_mqtt_password); + _mqtt_password = NULL; // just to be sure + if (value) { + _mqtt_password = strdup(value); + } + ok = true; + + } else if (strcmp(setting, "serial") == 0) { + ok = true; + _use_serial = false; + if (value) { + if (strcmp(value, "on") == 0) { + _use_serial = true; + ok = true; + myDebug_P(PSTR("Reboot ESP to activate Serial mode.")); + } else if (strcmp(value, "off") == 0) { + _use_serial = false; + ok = true; + myDebug_P(PSTR("Reboot ESP to deactivate Serial mode.")); + } else { + ok = false; + } + } + } else { + // finally check for any custom commands + ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); + } + + // if we were able to recognize the set command, continue + if (ok) { + // check for 2 params + if (value == nullptr) { + myDebug_P(PSTR("%s setting reset to its default value."), setting); + } else { + // must be 3 params + myDebug_P(PSTR("%s changed."), setting); + } + + myDebug_P(PSTR("")); // newline + + (void)fs_saveConfig(); // always save the values + } + + return ok; +} + +// force the serial on/off +void MyESP::setUseSerial(bool toggle) { + //(void)fs_saveConfig(); // save the setting for next reboot + + if (toggle) { + SerialAndTelnet.setSerial(&Serial); + _use_serial = true; + } else { + SerialAndTelnet.setSerial(NULL); + _use_serial = false; + } +} + +void MyESP::_telnetCommand(char * commandLine) { + char * str = commandLine; + bool state = false; + + // count the number of arguments + unsigned wc = 0; + while (*str) { + if (*str == ' ' || *str == '\n' || *str == '\t') { + state = false; + } else if (state == false) { + state = true; + ++wc; + } + ++str; + } + + // check first for reserved commands + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, " \n"); // space and newline + + // set command + if (strcmp(ptrToCommandName, "set") == 0) { + bool ok = false; + if (wc == 1) { + _printSetCommands(); + ok = true; + } else if (wc == 2) { // set + char * setting = _telnet_readWord(false); + ok = _changeSetting(wc - 1, setting, NULL); + } else { // set + char * setting = _telnet_readWord(false); + char * value = _telnet_readWord(true); // allow strange characters + ok = _changeSetting(wc - 1, setting, value); + } + + if (!ok) { + myDebug_P(PSTR("\nInvalid parameter for set command.")); + } + + return; + } + + // reboot command + if ((strcmp(ptrToCommandName, "reboot") == 0) && (wc == 1)) { + resetESP(); + } + + // show system stats + if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) { + showSystemStats(); + return; + } + +// crash command +#ifdef CRASH + if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { + char * cmd = _telnet_readWord(false); + if (strcmp(cmd, "dump") == 0) { + crashDump(); + } else if (strcmp(cmd, "clear") == 0) { + crashClear(); + } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) { + char * value = _telnet_readWord(false); + crashTest(atoi(value)); + } + return; // don't call custom command line callback + } +#endif + + // call callback function + (_telnetcommand_callback)(wc, commandLine); +} + +// returns WiFi hostname as a String object +String MyESP::_getESPhostname() { + String hostname; + +#if defined(ARDUINO_ARCH_ESP32) + hostname = String(WiFi.getHostname()); +#else + hostname = WiFi.hostname(); +#endif + + return (hostname); +} + +// returns build time as a String - copied for espurna. see (c) +// takes the time from the gcc during compilation +String MyESP::_buildTime() { + const char time_now[] = __TIME__; // hh:mm:ss + unsigned int hour = atoi(&time_now[0]); + unsigned int minute = atoi(&time_now[3]); + unsigned int second = atoi(&time_now[6]); + + const char date_now[] = __DATE__; // Mmm dd yyyy + const char * months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + unsigned int month = 0; + for (int i = 0; i < 12; i++) { + if (strncmp(date_now, months[i], 3) == 0) { + month = i + 1; + break; + } + } + unsigned int day = atoi(&date_now[3]); + unsigned int year = atoi(&date_now[7]); + + char buffer[20]; + snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); + + return String(buffer); +} + +// returns system uptime in seconds - copied for espurna. see (c) +unsigned long MyESP::_getUptime() { + static unsigned long last_uptime = 0; + static unsigned char uptime_overflows = 0; + + if (millis() < last_uptime) + ++uptime_overflows; + last_uptime = millis(); + unsigned long uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); + + return uptime_seconds; +} + +// print out ESP system stats +// for battery power is ESP.getVcc() +void MyESP::showSystemStats() { +#if defined(ESP8266) + myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); +#else + myDebug_P(PSTR("ESP32 System stats:")); +#endif + myDebug_P(PSTR("")); + + myDebug_P(PSTR(" [APP] %s version: %s"), _app_name, _app_version); + myDebug_P(PSTR(" [APP] MyESP version: %s"), MYESP_VERSION); + myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime().c_str()); + if (_boottime != NULL) { + myDebug_P(PSTR(" [APP] Boot time: %s"), _boottime); + } + uint32_t t = _getUptime(); // seconds + uint32_t h = (uint32_t)t / (uint32_t)3600L; + uint32_t rem = (uint32_t)t % (uint32_t)3600L; + uint32_t m = rem / 60; + uint32_t s = rem % 60; + myDebug_P(PSTR(" [APP] Uptime: %d seconds (%02d:%02d:%02d)"), t, h, m, s); + myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); + + if (isAPmode()) { + myDebug_P(PSTR(" [WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + } else { + myDebug_P(PSTR(" [WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi signal strength: %d%%"), getWifiQuality()); + } + + myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); + +#ifdef CRASH + char output_str[80] = {0}; + char buffer[16] = {0}; + /* Crash info */ + myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); + strlcpy(output_str, PSTR(" [EEPROM] EEPROM Sector pool size is "), sizeof(output_str)); + strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str)); + strlcat(output_str, PSTR(", and in use are: "), sizeof(output_str)); + for (uint32_t i = 0; i < EEPROMr.size(); i++) { + strlcat(output_str, itoa(EEPROMr.base() - i, buffer, 10), sizeof(output_str)); + strlcat(output_str, PSTR(" "), sizeof(output_str)); + } + myDebug_P(output_str); +#endif + +#ifdef ARDUINO_BOARD + myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD); +#endif + + myDebug_P(PSTR(" [SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); + myDebug_P(PSTR(" [SYSTEM] SDK version: %s"), ESP.getSdkVersion()); + +#if defined(ESP8266) + myDebug_P(PSTR(" [SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); + myDebug_P(PSTR(" [SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); + myDebug_P(PSTR(" [SYSTEM] Boot version: %d"), ESP.getBootVersion()); + myDebug_P(PSTR(" [SYSTEM] Boot mode: %d"), ESP.getBootMode()); + //myDebug_P(PSTR("[SYSTEM] Firmware MD5: %s"), (char *)ESP.getSketchMD5().c_str()); +#endif + + FlashMode_t mode = ESP.getFlashChipMode(); +#if defined(ESP8266) + myDebug_P(PSTR(" [FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); +#endif + myDebug_P(PSTR(" [FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); + myDebug_P(PSTR(" [FLASH] Flash mode: %s"), + mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); +#if defined(ESP8266) + myDebug_P(PSTR(" [FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); +#endif + myDebug_P(PSTR(" [FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); + myDebug_P(PSTR(" [FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR(" [MEM] Firmware size: %d"), ESP.getSketchSize()); + myDebug_P(PSTR(" [MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + myDebug_P(PSTR(" [MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR(" [MEM] Free Heap: %d"), ESP.getFreeHeap()); + + myDebug_P(PSTR("")); +} + +// handler for Telnet +void MyESP::_telnetHandle() { + SerialAndTelnet.handle(); + + static uint8_t charsRead = 0; + // read asynchronously until full command input + while (SerialAndTelnet.available()) { + char c = SerialAndTelnet.read(); + + SerialAndTelnet.serialPrint(c); // echo to Serial (if connected) + + switch (c) { + case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF + case '\n': + _command[charsRead] = '\0'; // null terminate our command char array + if (charsRead > 0) { + charsRead = 0; // is static, so have to reset + _suspendOutput = false; + if (_use_serial) { + SerialAndTelnet.serialPrint('\n'); // force newline if in Serial + } + _telnetCommand(_command); + } + break; + + case '\b': // (^H) + case 0x7F: // (^?) + if (charsRead > 0) { + _command[--charsRead] = '\0'; + + SerialAndTelnet.write(' '); + SerialAndTelnet.write('\b'); + } + + break; + + case '?': + if (!_suspendOutput) { + _consoleShowHelp(); + } else { + _command[charsRead++] = c; // add it to buffer as its part of the string entered + } + break; + case 0x04: // EOT, CTRL-D + myDebug_P(PSTR("[TELNET] exiting telnet session")); + SerialAndTelnet.disconnectClient(); + break; + default: + _suspendOutput = true; + if (charsRead < TELNET_MAX_COMMAND_LENGTH) { + _command[charsRead++] = c; + } + _command[charsRead] = '\0'; // just in case + break; + } + } +} + +// ensure we have a connection to MQTT broker +void MyESP::_mqttConnect() { + if (!_mqtt_host) + return; // MQTT not enabled + + // Do not connect if already connected or still trying to connect + if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) { + return; + } + + // Check reconnect interval + if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) { + return; + } + + _mqtt_connecting = true; // we're doing a connection + + // Increase the reconnect delay + _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP; + if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) { + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; + } + + mqttClient.setServer(_mqtt_host, MQTT_PORT); + mqttClient.setClientId(_app_hostname); + mqttClient.setKeepAlive(_mqtt_keepalive); + mqttClient.setCleanSession(false); + + // last will + if (_mqtt_will_topic) { + //myDebug_P(PSTR("[MQTT] Setting last will topic %s"), _mqttTopic(_mqtt_will_topic)); + mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, + _mqtt_will_offline_payload); // retain always true + } + + if (_mqtt_username && _mqtt_password) { + myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _mqtt_username); + mqttClient.setCredentials(_mqtt_username, _mqtt_password); + } else { + myDebug_P(PSTR("[MQTT] Connecting to MQTT...")); + } + + // Connect to the MQTT broker + mqttClient.connect(); +} + +// Setup everything we need +void MyESP::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback) { + // Check SSID too long or missing + if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > 31) { + _wifi_ssid = NULL; + } else { + _wifi_ssid = strdup(wifi_ssid); + } + + // Check PASS too long + if (!wifi_password || *wifi_ssid == 0x00 || strlen(wifi_password) > 31) { + _wifi_password = NULL; + } else { + _wifi_password = strdup(wifi_password); + } + + // callback + _wifi_callback = callback; +} + +// init MQTT settings +void MyESP::setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback) { + // can be empty + if (!mqtt_host || *mqtt_host == 0x00) { + _mqtt_host = NULL; + } else { + _mqtt_host = strdup(mqtt_host); + } + + // mqtt username and password can be empty + if (!mqtt_username || *mqtt_username == 0x00) { + _mqtt_username = NULL; + } else { + _mqtt_username = strdup(mqtt_username); + } + + // can be empty + if (!mqtt_password || *mqtt_password == 0x00) { + _mqtt_password = NULL; + } else { + _mqtt_password = strdup(mqtt_password); + } + + // base + if (_mqtt_base) { + free(_mqtt_base); + } + _mqtt_base = strdup(mqtt_base); + + // callback + _mqtt_callback = callback; + + // various mqtt settings + _mqtt_keepalive = mqtt_keepalive; + _mqtt_qos = mqtt_qos; + _mqtt_retain = mqtt_retain; + + // last will + if (!mqtt_will_topic || *mqtt_will_topic == 0x00) { + _mqtt_will_topic = NULL; + } else { + _mqtt_will_topic = strdup(mqtt_will_topic); + } + + if (!mqtt_will_online_payload || *mqtt_will_online_payload == 0x00) { + _mqtt_will_online_payload = NULL; + } else { + _mqtt_will_online_payload = strdup(mqtt_will_online_payload); + } + + if (!mqtt_will_offline_payload || *mqtt_will_offline_payload == 0x00) { + _mqtt_will_offline_payload = NULL; + } else { + _mqtt_will_offline_payload = strdup(mqtt_will_offline_payload); + } +} + +// builds up a topic by prefixing the base and hostname +char * MyESP::_mqttTopic(const char * topic) { + char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; + + strlcpy(buffer, _mqtt_base, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, _app_hostname, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, topic, sizeof(buffer)); + + if (_mqtt_topic) { + free(_mqtt_topic); + } + _mqtt_topic = strdup(buffer); + + return _mqtt_topic; +} + +// print contents of file +// assumes Serial is open +void MyESP::_fs_printConfig() { + myDebug_P(PSTR("[FS] Contents:")); + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + if (!configFile) { + myDebug_P(PSTR("[FS] Failed to read file for printing")); + return; + } + + while (configFile.available()) { + SerialAndTelnet.print((char)configFile.read()); + } + myDebug_P(PSTR("")); // newline + + configFile.close(); +} + +// format File System +void MyESP::_fs_eraseConfig() { + myDebug_P(PSTR("[FS] Erasing settings, please wait a few seconds. ESP will " + "automatically restart when finished.")); + + if (SPIFFS.format()) { + delay(1000); // wait 1 seconds + resetESP(); + } +} + +void MyESP::setSettings(fs_callback_f callback_fs, fs_settings_callback_f callback_settings_fs) { + _fs_callback = callback_fs; + _fs_settings_callback = callback_settings_fs; +} + +// load from spiffs +bool MyESP::_fs_loadConfig() { + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + + size_t size = configFile.size(); + if (size > 1024) { + myDebug_P(PSTR("[FS] Config file size is too large")); + return false; + } else if (size == 0) { + myDebug_P(PSTR("[FS] Failed to open config file")); + // file does not exist, so assume its the first install. Set serial to on + _use_serial = true; + return false; + } + + StaticJsonDocument doc; + JsonObject json = doc.to(); + + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, configFile); + if (error) { + myDebug_P(PSTR("[FS] Failed to read config file")); + return false; + } + + const char * value; + + // fetch the standard system parameters + value = json["wifi_ssid"]; + _wifi_ssid = (value) ? strdup(value) : NULL; + + value = json["wifi_password"]; + _wifi_password = (value) ? strdup(value) : NULL; + + value = json["mqtt_host"]; + _mqtt_host = (value) ? strdup(value) : NULL; + + value = json["mqtt_username"]; + _mqtt_username = (value) ? strdup(value) : NULL; + + value = json["mqtt_password"]; + _mqtt_password = (value) ? strdup(value) : NULL; + + _use_serial = (bool)json["use_serial"]; + + // callback for loading custom settings + // ok is false if there's a problem loading a custom setting (e.g. does not exist) + bool ok = (_fs_callback)(MYESP_FSACTION_LOAD, json); + + configFile.close(); + + return ok; +} + +// save settings to spiffs +bool MyESP::fs_saveConfig() { + bool ok = true; + + // call any custom functions before handling SPIFFS + if (_ota_pre_callback) { + (_ota_pre_callback)(); + } + + StaticJsonDocument doc; + JsonObject json = doc.to(); + + json["app_version"] = _app_version; + json["wifi_ssid"] = _wifi_ssid; + json["wifi_password"] = _wifi_password; + json["mqtt_host"] = _mqtt_host; + json["mqtt_username"] = _mqtt_username; + json["mqtt_password"] = _mqtt_password; + json["use_serial"] = _use_serial; + + // callback for saving custom settings + (void)(_fs_callback)(MYESP_FSACTION_SAVE, json); + + // if file exists, remove it just to be safe + if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { + SPIFFS.remove(MYEMS_CONFIG_FILE); + } + + // open for writing + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); + if (!configFile) { + myDebug_P(PSTR("[FS] Failed to open config file for writing")); + return false; + } + + + // Serialize JSON to file + if (serializeJson(json, configFile) == 0) { + myDebug_P(PSTR("[FS] Failed to write config file")); + ok = false; + } + + configFile.close(); + + // call any custom functions before handling SPIFFS + if (_ota_post_callback) { + (_ota_post_callback)(); + } + + return ok; // it worked +} + +// init the SPIFF file system and load the config +// if it doesn't exist try and create it +void MyESP::_fs_setup() { + if (!SPIFFS.begin()) { + myDebug_P(PSTR("[FS] Failed to mount the file system. Erasing...")); + _fs_eraseConfig(); // fix for ESP32 + return; + } + + // load the config file. if it doesn't exist (function returns false) create it + if (!_fs_loadConfig()) { + //myDebug_P(PSTR("[FS] Re-creating config file")); + fs_saveConfig(); + } + + // _fs_printConfig(); // enable for debugging +} + +uint16_t MyESP::getSystemLoadAverage() { + return _load_average; +} + +// calculate load average +void MyESP::_calculateLoad() { + static unsigned long last_loadcheck = 0; + static unsigned long load_counter_temp = 0; + load_counter_temp++; + + if (millis() - last_loadcheck > LOADAVG_INTERVAL) { + static unsigned long load_counter = 0; + static unsigned long load_counter_max = 1; + + load_counter = load_counter_temp; + load_counter_temp = 0; + if (load_counter > load_counter_max) { + load_counter_max = load_counter; + } + _load_average = 100 - (100 * load_counter / load_counter_max); + last_loadcheck = millis(); + } +} + +// returns true is MQTT is alive +bool MyESP::isMQTTConnected() { + return mqttClient.connected(); +} + +// return true if wifi is connected +// WL_NO_SHIELD = 255, // for compatibility with WiFi Shield library +// WL_IDLE_STATUS = 0, +// WL_NO_SSID_AVAIL = 1, +// WL_SCAN_COMPLETED = 2, +// WL_CONNECTED = 3, +// WL_CONNECT_FAILED = 4, +// WL_CONNECTION_LOST = 5, +// WL_DISCONNECTED = 6 +bool MyESP::isWifiConnected() { + return (_wifi_connected); +} + +/* + Return the quality (Received Signal Strength Indicator) + of the WiFi network. + Returns a number between 0 and 100 if WiFi is connected. + Returns -1 if WiFi is disconnected. + + High quality: 90% ~= -55dBm + Medium quality: 50% ~= -75dBm + Low quality: 30% ~= -85dBm + Unusable quality: 8% ~= -96dBm +*/ +int MyESP::getWifiQuality() { + if (WiFi.status() != WL_CONNECTED) + return -1; + int dBm = WiFi.RSSI(); + if (dBm <= -100) + return 0; + if (dBm >= -50) + return 100; + return 2 * (dBm + 100); +} + +#ifdef CRASH +/** + * Save crash information in EEPROM + * This function is called automatically if ESP8266 suffers an exception + * It should be kept quick / consise to be able to execute before hardware wdt may kick in + */ +extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) { + // write crash time to EEPROM + uint32_t crash_time = millis(); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + + // write reset info to EEPROM + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); + + // write epc1, epc2, epc3, excvaddr and depc to EEPROM + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); + + // write stack start and end address to EEPROM + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + // write stack trace to EEPROM and avoid overwriting settings + int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + for (uint32_t i = stack_start; i < stack_end; i++) { + byte * byteValue = (byte *)i; + EEPROMr.write(current_address++, *byteValue); + } + + EEPROMr.commit(); +} + +void MyESP::crashTest(uint8_t t) { + if (t == 1) { + myDebug("[CRASH] Attempting to divide by zero ..."); + int result, zero; + zero = 0; + result = 1 / zero; + myDebug("Result = %d", result); + } + + if (t == 2) { + myDebug("[CRASH] Attempting to read through a pointer to no object ..."); + int * nullPointer; + nullPointer = NULL; + // null pointer dereference - read + // attempt to read a value through a null pointer + Serial.println(*nullPointer); + } + + if (t == 3) { + Serial.printf("[CRASH] Crashing with hardware WDT (%ld ms) ...\n", millis()); + ESP.wdtDisable(); + while (true) { + // stay in an infinite loop doing nothing + // this way other process can not be executed + // + // Note: + // Hardware wdt kicks in if software wdt is unable to perfrom + // Nothing will be saved in EEPROM for the hardware wdt + } + } + + if (t == 4) { + Serial.printf("[CRASH] Crashing with software WDT (%ld ms) ...\n", millis()); + while (true) { + // stay in an infinite loop doing nothing + // this way other process can not be executed + } + } +} + +/** + * Clears crash info + */ +void MyESP::crashClear() { + myDebug_P(PSTR("[CRASH] Clearing crash dump")); + uint32_t crash_time = 0xFFFFFFFF; + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROMr.commit(); +} + +/** + * Print out crash information that has been previously saved in EEPROM + */ +void MyESP::crashDump() { + uint32_t crash_time; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) { + myDebug_P(PSTR("[CRASH] No crash info")); + return; + } + + myDebug_P(PSTR("[CRASH] Latest crash was at %lu ms after boot"), crash_time); + myDebug_P(PSTR("[CRASH] Reason of restart: %u"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON)); + myDebug_P(PSTR("[CRASH] Exception cause: %u"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE)); + + uint32_t epc1, epc2, epc3, excvaddr, depc; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); + + myDebug_P(PSTR("[CRASH] epc1=0x%08x epc2=0x%08x epc3=0x%08x"), epc1, epc2, epc3); + myDebug_P(PSTR("[CRASH] excvaddr=0x%08x depc=0x%08x"), excvaddr, depc); + + uint32_t stack_start, stack_end; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + myDebug_P(PSTR("[CRASH] sp=0x%08x end=0x%08x"), stack_start, stack_end); + + int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + int16_t stack_len = stack_end - stack_start; + + uint32_t stack_trace; + + myDebug(">>>stack>>>"); + + for (int16_t i = 0; i < stack_len; i += 0x10) { + SerialAndTelnet.printf("%08x: ", stack_start + i); + for (byte j = 0; j < 4; j++) { + EEPROMr.get(current_address, stack_trace); + SerialAndTelnet.printf("%08x ", stack_trace); + current_address += 4; + } + SerialAndTelnet.println(); + } + myDebug("<< +#include +#include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127 +#include +#include +#include // https://github.com/xoseperez/justwifi +#include // modified from https://github.com/yasheena/telnetspy + +#ifdef CRASH +#include +extern "C" { +void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); +} +#endif + +#if defined(ARDUINO_ARCH_ESP32) +//#include +#include // added for ESP32 +#define ets_vsnprintf vsnprintf // added for ESP32 +#define OTA_PORT 8266 +#else +//#include +#include +#define OTA_PORT 3232 +#endif + +#define MYEMS_CONFIG_FILE "/config.json" + +#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) + +// WIFI +#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms +#define WIFI_RECONNECT_INTERVAL 60000 // If could not connect to WIFI, retry after this time in ms + +// MQTT +#define MQTT_PORT 1883 // MQTT port +#define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection +#define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt +#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most +#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT message +#define MQTT_TOPIC_START "start" +#define MQTT_TOPIC_START_PAYLOAD "start" +#define MQTT_TOPIC_RESTART "restart" + +// Internal MQTT events +#define MQTT_CONNECT_EVENT 0 +#define MQTT_DISCONNECT_EVENT 1 +#define MQTT_MESSAGE_EVENT 2 + +// Telnet +#define TELNET_SERIAL_BAUD 115200 +#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command +#define TELNET_EVENT_CONNECT 1 +#define TELNET_EVENT_DISCONNECT 0 + +// ANSI Colors +#define COLOR_RESET "\x1B[0m" +#define COLOR_BLACK "\x1B[0;30m" +#define COLOR_RED "\x1B[0;31m" +#define COLOR_GREEN "\x1B[0;32m" +#define COLOR_YELLOW "\x1B[0;33m" +#define COLOR_BLUE "\x1B[0;34m" +#define COLOR_MAGENTA "\x1B[0;35m" +#define COLOR_CYAN "\x1B[0;36m" +#define COLOR_WHITE "\x1B[0;37m" +#define COLOR_BOLD_ON "\x1B[1m" +#define COLOR_BOLD_OFF "\x1B[22m" +#define COLOR_BRIGHT_BLACK "\x1B[0;90m" +#define COLOR_BRIGHT_RED "\x1B[0;91m" +#define COLOR_BRIGHT_GREEN "\x1B[0;92m" +#define COLOR_BRIGHT_YELLOW "\x1B[0;99m" +#define COLOR_BRIGHT_BLUE "\x1B[0;94m" +#define COLOR_BRIGHT_MAGENTA "\x1B[0;95m" +#define COLOR_BRIGHT_CYAN "\x1B[0;96m" +#define COLOR_BRIGHT_WHITE "\x1B[0;97m" + +// SPIFFS +#define SPIFFS_MAXSIZE 600 // https://arduinojson.org/v6/assistant/ + +// CRASH +/** + * Structure of the single crash data set + * + * 1. Crash time + * 2. Restart reason + * 3. Exception cause + * 4. epc1 + * 5. epc2 + * 6. epc3 + * 7. excvaddr + * 8. depc + * 9. address of stack start + * 10. address of stack end + * 11. stack trace bytes + * ... + */ +#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data +#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes +#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte +#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte +#define SAVE_CRASH_EPC1 0x06 // 4 bytes +#define SAVE_CRASH_EPC2 0x0A // 4 bytes +#define SAVE_CRASH_EPC3 0x0E // 4 bytes +#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes +#define SAVE_CRASH_DEPC 0x16 // 4 bytes +#define SAVE_CRASH_STACK_START 0x1A // 4 bytes +#define SAVE_CRASH_STACK_END 0x1E // 4 bytes +#define SAVE_CRASH_STACK_TRACE 0x22 // variable + +typedef struct { + bool set; // is it a set command + char key[50]; + char description[100]; +} command_t; + +typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION; + +typedef std::function mqtt_callback_f; +typedef std::function wifi_callback_f; +typedef std::function ota_callback_f; +typedef std::function telnetcommand_callback_f; +typedef std::function telnet_callback_f; +typedef std::function fs_callback_f; +typedef std::function fs_settings_callback_f; + +// calculates size of an 2d array at compile time +template +constexpr size_t ArraySize(T (&)[N]) { + return N; +} + +#define UPTIME_OVERFLOW 4294967295 // Uptime overflow value + +// class definition +class MyESP { + public: + MyESP(); + ~MyESP(); + + // wifi + void setWIFICallback(void (*callback)()); + void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); + bool isWifiConnected(); + bool isAPmode(); + + // mqtt + bool isMQTTConnected(); + void mqttSubscribe(const char * topic); + void mqttUnsubscribe(const char * topic); + void mqttPublish(const char * topic, const char * payload); + void setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback); + + // OTA + void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post); + + // debug & telnet + void myDebug(const char * format, ...); + void myDebug_P(PGM_P format_P, ...); + void setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback); + bool getUseSerial(); + void setUseSerial(bool toggle); + + // FS + void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); + bool fs_saveConfig(); + + // Crash + void crashClear(); + void crashDump(); + void crashTest(uint8_t t); + void crashInfo(); + + // general + void end(); + void loop(); + void begin(const char * app_hostname, const char * app_name, const char * app_version); + void setBoottime(const char * boottime); + void resetESP(); + uint16_t getSystemLoadAverage(); + int getWifiQuality(); + void showSystemStats(); + + private: + // mqtt + AsyncMqttClient mqttClient; + unsigned long _mqtt_reconnect_delay; + void _mqttOnMessage(char * topic, char * payload, size_t len); + void _mqttConnect(); + void _mqtt_setup(); + mqtt_callback_f _mqtt_callback; + void _mqttOnConnect(); + void _sendStart(); + char * _mqttTopic(const char * topic); + char * _mqtt_host; + char * _mqtt_username; + char * _mqtt_password; + char * _mqtt_base; + unsigned long _mqtt_keepalive; + unsigned char _mqtt_qos; + bool _mqtt_retain; + char * _mqtt_will_topic; + char * _mqtt_will_online_payload; + char * _mqtt_will_offline_payload; + char * _mqtt_topic; + unsigned long _mqtt_last_connection; + bool _mqtt_connecting; + + // wifi + DNSServer dnsServer; // For Access Point (AP) support + void _wifiCallback(justwifi_messages_t code, char * parameter); + void _wifi_setup(); + wifi_callback_f _wifi_callback; + char * _wifi_ssid; + char * _wifi_password; + bool _wifi_connected; + String _getESPhostname(); + + // ota + ota_callback_f _ota_pre_callback; + ota_callback_f _ota_post_callback; + void _ota_setup(); + void _OTACallback(); + + // crash + void _eeprom_setup(); + + // telnet & debug + TelnetSpy SerialAndTelnet; + void _telnetConnected(); + void _telnetDisconnected(); + void _telnetHandle(); + void _telnetCommand(char * commandLine); + char * _telnet_readWord(bool allow_all_chars); + void _telnet_setup(); + char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet + command_t * _helpProjectCmds; // Help of commands setted by project + uint8_t _helpProjectCmds_count; // # available commands + void _consoleShowHelp(); + telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands + telnet_callback_f _telnet_callback; // callback for connect/disconnect + bool _changeSetting(uint8_t wc, const char * setting, const char * value); + + // fs + void _fs_setup(); + bool _fs_loadConfig(); + void _fs_printConfig(); + void _fs_eraseConfig(); + + // settings + fs_callback_f _fs_callback; + fs_settings_callback_f _fs_settings_callback; + void _printSetCommands(); + + // general + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + unsigned long _getUptime(); + String _buildTime(); + + // load average (0..100) + void _calculateLoad(); + unsigned short int _load_average; +}; + +extern MyESP myESP; + +#endif