diff --git a/.gitignore b/.gitignore index a5d67c91..ebb6f739 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /site/ /config.json +*.filters +*.vcxitems diff --git a/docs/advanced-usage/deep-sleep.md b/docs/advanced-usage/deep-sleep.md index 92235fcf..734da88b 100644 --- a/docs/advanced-usage/deep-sleep.md +++ b/docs/advanced-usage/deep-sleep.md @@ -11,7 +11,7 @@ void onHomieEvent(const HomieEvent& event) { break; case HomieEventType::READY_TO_SLEEP: Homie.getLogger() << "Ready to sleep" << endl; - ESP.deepSleep(); + Homie.doDeepSleep(); break; } } diff --git a/docs/others/cpp-api-reference.md b/docs/others/cpp-api-reference.md index dd02ccf5..1cabf21c 100644 --- a/docs/others/cpp-api-reference.md +++ b/docs/others/cpp-api-reference.md @@ -175,6 +175,12 @@ void prepareToSleep(); Prepare the device for deep sleep. It ensures messages are sent and disconnects cleanly from the MQTT broker, triggering a `READY_TO_SLEEP` event when done. +```c++ +void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); +``` + +Puth the device into deep sleep. It ensures the Serial is flushed. + ```c++ bool isConfigured() const; ``` diff --git a/keywords.txt b/keywords.txt index 52ababff..a983208d 100644 --- a/keywords.txt +++ b/keywords.txt @@ -41,6 +41,7 @@ getConfiguration KEYWORD2 getMqttClient KEYWORD2 getLogger KEYWORD2 prepareToSleep KEYWORD2 +doDeepSleep KEYWORD2 # HomieNode diff --git a/src/Homie.cpp b/src/Homie.cpp index fb985179..1a08e91c 100644 --- a/src/Homie.cpp +++ b/src/Homie.cpp @@ -3,9 +3,9 @@ using namespace HomieInternals; HomieClass::HomieClass() -: _setupCalled(false) -, _firmwareSet(false) -, __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { + : _setupCalled(false) + , _firmwareSet(false) + , __HOMIE_SIGNATURE("\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25") { strlcpy(Interface::get().brand, DEFAULT_BRAND, MAX_BRAND_LENGTH); Interface::get().bootMode = HomieBootMode::UNDEFINED; Interface::get().configurationAp.secured = false; @@ -17,7 +17,8 @@ HomieClass::HomieClass() Interface::get().reset.triggerPin = DEFAULT_RESET_PIN; Interface::get().reset.triggerState = DEFAULT_RESET_STATE; Interface::get().reset.triggerTime = DEFAULT_RESET_TIME; - Interface::get().reset.flaggedBySketch = false; + Interface::get().reset.resetFlag = false; + Interface::get().disable = false; Interface::get().flaggedForSleep = false; Interface::get().globalInputHandler = [](const HomieNode& node, const String& property, const HomieRange& range, const String& value) { return false; }; Interface::get().broadcastHandler = [](const String& level, const String& value) { return false; }; @@ -51,14 +52,18 @@ void HomieClass::setup() { _setupCalled = true; // Check if firmware is set - if (!_firmwareSet) { Helpers::abort(F("✖ Firmware name must be set before calling setup()")); return; // never reached, here for clarity } - // Check if default settings values are valid + // Check the max allowed setting elements + if (IHomieSetting::settings.size() > MAX_CONFIG_SETTING_SIZE) { + Helpers::abort(F("✖ Settings exceed set limit of elelement.")); + return; // never reached, here for clarity + } + // Check if default settings values are valid bool defaultSettingsValuesValid = true; for (IHomieSetting* iSetting : IHomieSetting::settings) { if (iSetting->isBool()) { @@ -67,19 +72,22 @@ void HomieClass::setup() { defaultSettingsValuesValid = false; break; } - } else if (iSetting->isLong()) { + } + else if (iSetting->isLong()) { HomieSetting* setting = static_cast*>(iSetting); if (!setting->isRequired() && !setting->validate(setting->get())) { defaultSettingsValuesValid = false; break; } - } else if (iSetting->isDouble()) { + } + else if (iSetting->isDouble()) { HomieSetting* setting = static_cast*>(iSetting); if (!setting->isRequired() && !setting->validate(setting->get())) { defaultSettingsValuesValid = false; break; } - } else if (iSetting->isConstChar()) { + } + else if (iSetting->isConstChar()) { HomieSetting* setting = static_cast*>(iSetting); if (!setting->isRequired() && !setting->validate(setting->get())) { defaultSettingsValuesValid = false; @@ -107,9 +115,11 @@ void HomieClass::setup() { // select boot mode source if (_applicationHomieBootMode != HomieBootMode::UNDEFINED) { _selectedHomieBootMode = _applicationHomieBootMode; - } else if (_nextHomieBootMode != HomieBootMode::UNDEFINED) { + } + else if (_nextHomieBootMode != HomieBootMode::UNDEFINED) { _selectedHomieBootMode = _nextHomieBootMode; - } else { + } + else { _selectedHomieBootMode = HomieBootMode::NORMAL; } @@ -125,17 +135,20 @@ void HomieClass::setup() { Interface::get().event.type = HomieEventType::NORMAL_MODE; Interface::get().eventHandler(Interface::get().event); - } else if (_selectedHomieBootMode == HomieBootMode::CONFIGURATION) { + } + else if (_selectedHomieBootMode == HomieBootMode::CONFIGURATION) { _boot = &_bootConfig; Interface::get().event.type = HomieEventType::CONFIGURATION_MODE; Interface::get().eventHandler(Interface::get().event); - } else if (_selectedHomieBootMode == HomieBootMode::STANDALONE) { + } + else if (_selectedHomieBootMode == HomieBootMode::STANDALONE) { _boot = &_bootStandalone; Interface::get().event.type = HomieEventType::STANDALONE_MODE; Interface::get().eventHandler(Interface::get().event); - } else { + } + else { Helpers::abort(F("✖ Boot mode invalid")); return; // never reached, here for clarity } @@ -225,10 +238,14 @@ void HomieClass::__setBrand(const char* brand) const { } void HomieClass::reset() { - Interface::get().reset.flaggedBySketch = true; + Interface::get().getLogger() << F("Flagged for reset by sketch") << endl; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; } void HomieClass::reboot() { + Interface::get().getLogger() << F("Flagged for reboot by sketch") << endl; + Interface::get().disable = true; _flaggedForReboot = true; } @@ -327,13 +344,23 @@ Logger& HomieClass::getLogger() { } void HomieClass::prepareToSleep() { + Interface::get().getLogger() << F("Flagged for sleep by sketch") << endl; if (Interface::get().ready) { + Interface::get().disable = true; Interface::get().flaggedForSleep = true; - } else { + } + else { + Interface::get().disable = true; Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; Interface::get().event.type = HomieEventType::READY_TO_SLEEP; Interface::get().eventHandler(Interface::get().event); } } +void HomieClass::doDeepSleep(uint32_t time_us, RFMode mode){ + Interface::get().getLogger() << F("💤 Device is deep sleeping...") << endl; + Serial.flush(); + ESP.deepSleep(time_us, mode); +} + HomieClass Homie; diff --git a/src/Homie.hpp b/src/Homie.hpp index 76f69e77..66c21de7 100644 --- a/src/Homie.hpp +++ b/src/Homie.hpp @@ -2,7 +2,7 @@ #include "Arduino.h" -#include +#include "AsyncMqttClient.h" #include "Homie/Datatypes/Interface.hpp" #include "Homie/Constants.hpp" #include "Homie/Limits.hpp" @@ -22,66 +22,69 @@ #include "HomieSetting.hpp" #include "StreamingOperator.hpp" +// Define DEBUG for debug + #define Homie_setFirmware(name, version) const char* __FLAGGED_FW_NAME = "\xbf\x84\xe4\x13\x54" name "\x93\x44\x6b\xa7\x75"; const char* __FLAGGED_FW_VERSION = "\x6a\x3f\x3e\x0e\xe1" version "\xb0\x30\x48\xd4\x1a"; Homie.__setFirmware(__FLAGGED_FW_NAME, __FLAGGED_FW_VERSION); #define Homie_setBrand(brand) const char* __FLAGGED_BRAND = "\xfb\x2a\xf5\x68\xc0" brand "\x6e\x2f\x0f\xeb\x2d"; Homie.__setBrand(__FLAGGED_BRAND); namespace HomieInternals { -class HomieClass { - friend class ::HomieNode; - friend SendingPromise; + class HomieClass { + friend class ::HomieNode; + friend SendingPromise; - public: - HomieClass(); - ~HomieClass(); - void setup(); - void loop(); + public: + HomieClass(); + ~HomieClass(); + void setup(); + void loop(); - void __setFirmware(const char* name, const char* version); - void __setBrand(const char* brand) const; + void __setFirmware(const char* name, const char* version); + void __setBrand(const char* brand) const; - HomieClass& disableLogging(); - HomieClass& setLoggingPrinter(Print* printer); - HomieClass& disableLedFeedback(); - HomieClass& setLedPin(uint8_t pin, uint8_t on); - HomieClass& setConfigurationApPassword(const char* password); - HomieClass& setGlobalInputHandler(const GlobalInputHandler& globalInputHandler); - HomieClass& setBroadcastHandler(const BroadcastHandler& broadcastHandler); - HomieClass& onEvent(const EventHandler& handler); - HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); - HomieClass& disableResetTrigger(); - HomieClass& setSetupFunction(const OperationFunction& function); - HomieClass& setLoopFunction(const OperationFunction& function); - HomieClass& setHomieBootMode(HomieBootMode bootMode); - HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); + HomieClass& disableLogging(); + HomieClass& setLoggingPrinter(Print* printer); + HomieClass& disableLedFeedback(); + HomieClass& setLedPin(uint8_t pin, uint8_t on); + HomieClass& setConfigurationApPassword(const char* password); + HomieClass& setGlobalInputHandler(const GlobalInputHandler& globalInputHandler); + HomieClass& setBroadcastHandler(const BroadcastHandler& broadcastHandler); + HomieClass& onEvent(const EventHandler& handler); + HomieClass& setResetTrigger(uint8_t pin, uint8_t state, uint16_t time); + HomieClass& disableResetTrigger(); + HomieClass& setSetupFunction(const OperationFunction& function); + HomieClass& setLoopFunction(const OperationFunction& function); + HomieClass& setHomieBootMode(HomieBootMode bootMode); + HomieClass& setHomieBootModeOnNextBoot(HomieBootMode bootMode); - static void reset(); - void reboot(); - static void setIdle(bool idle); - static bool isConfigured(); - static bool isConnected(); - static const ConfigStruct& getConfiguration(); - AsyncMqttClient& getMqttClient(); - Logger& getLogger(); - static void prepareToSleep(); + static void reset(); + void reboot(); + static void setIdle(bool idle); + static bool isConfigured(); + static bool isConnected(); + static const ConfigStruct& getConfiguration(); + AsyncMqttClient& getMqttClient(); + Logger& getLogger(); + static void prepareToSleep(); + static void doDeepSleep(uint32_t time_us = 0, RFMode mode = RF_DEFAULT); - private: - bool _setupCalled; - bool _firmwareSet; - Boot* _boot; - BootStandalone _bootStandalone; - BootNormal _bootNormal; - BootConfig _bootConfig; - bool _flaggedForReboot; - SendingPromise _sendingPromise; - Logger _logger; - Blinker _blinker; - Config _config; - AsyncMqttClient _mqttClient; + private: + bool _setupCalled; + bool _firmwareSet; + Boot* _boot; + BootStandalone _bootStandalone; + BootNormal _bootNormal; + BootConfig _bootConfig; + bool _flaggedForReboot; + SendingPromise _sendingPromise; + Logger _logger; + Blinker _blinker; + Config _config; + AsyncMqttClient _mqttClient; - void _checkBeforeSetup(const __FlashStringHelper* functionName) const; + void _checkBeforeSetup(const __FlashStringHelper* functionName) const; - const char* __HOMIE_SIGNATURE; -}; + const char* __HOMIE_SIGNATURE; + }; } // namespace HomieInternals extern HomieInternals::HomieClass Homie; diff --git a/src/Homie/Boot/BootConfig.cpp b/src/Homie/Boot/BootConfig.cpp index ad0cdfb4..c460ab8d 100644 --- a/src/Homie/Boot/BootConfig.cpp +++ b/src/Homie/Boot/BootConfig.cpp @@ -45,7 +45,7 @@ void BootConfig::setup() { WiFi.softAP(apName); } - _apIpStr = ACCESS_POINT_IP.toString(); + Helpers::ipToString(ACCESS_POINT_IP, _apIpStr); Interface::get().getLogger() << F("AP started as ") << apName << F(" with IP ") << _apIpStr << endl; _dns.setTTL(30); @@ -58,34 +58,79 @@ void BootConfig::setup() { }); _http.on("/device-info", HTTP_GET, [this](AsyncWebServerRequest *request) { _onDeviceInfoRequest(request); }); _http.on("/networks", HTTP_GET, [this](AsyncWebServerRequest *request) { _onNetworksRequest(request); }); - _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::_parsePost); + _http.on("/config", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onConfigRequest(request); }).onBody(BootConfig::__parsePost); _http.on("/config", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS Interface::get().getLogger() << F("Received CORS request for /config") << endl; - _sendCORS(request); + __sendCORS(request); }); - _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::_parsePost); + _http.on("/wifi/connect", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onWifiConnectRequest(request); }).onBody(BootConfig::__parsePost); _http.on("/wifi/connect", HTTP_OPTIONS, [this](AsyncWebServerRequest *request) { // CORS Interface::get().getLogger() << F("Received CORS request for /wifi/connect") << endl; - _sendCORS(request); + __sendCORS(request); }); _http.on("/wifi/status", HTTP_GET, [this](AsyncWebServerRequest *request) { _onWifiStatusRequest(request); }); - _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::_parsePost); + _http.on("/proxy/control", HTTP_PUT, [this](AsyncWebServerRequest *request) { _onProxyControlRequest(request); }).onBody(BootConfig::__parsePost); _http.onNotFound([this](AsyncWebServerRequest *request) { _onCaptivePortal(request); }); _http.begin(); } +void BootConfig::loop() { + Boot::loop(); + + _dns.processNextRequest(); + + if (_flaggedForReboot) { + if (millis() - _flaggedForRebootAt >= 3000UL) { + Interface::get().getLogger() << F("↻ Rebooting into normal mode...") << endl; + Serial.flush(); + ESP.restart(); + } + + return; + } + + if (!_lastWifiScanEnded) { + int8_t scanResult = WiFi.scanComplete(); + + switch (scanResult) { + case WIFI_SCAN_RUNNING: + return; + case WIFI_SCAN_FAILED: + Interface::get().getLogger() << F("✖ Wi-Fi scan failed") << endl; + _ssidCount = 0; + _wifiScanTimer.reset(); + break; + default: + Interface::get().getLogger() << F("✔ Wi-Fi scan completed") << endl; + _ssidCount = scanResult; + _generateNetworksJson(); + _wifiScanAvailable = true; + break; + } + + _lastWifiScanEnded = true; + } + + if (_lastWifiScanEnded && _wifiScanTimer.check()) { + Interface::get().getLogger() << F("Triggering Wi-Fi scan...") << endl; + WiFi.scanNetworks(true); + _wifiScanTimer.tick(); + _lastWifiScanEnded = false; + } +} + void BootConfig::_onWifiConnectRequest(AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received Wi-Fi connect request") << endl; DynamicJsonBuffer parseJsonBuffer(JSON_OBJECT_SIZE(2)); const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); if (!parsedJson.success()) { - _SendJSONError(request, F("✖ Invalid or too big JSON")); + __SendJSONError(request, F("✖ Invalid or too big JSON")); return; } if (!parsedJson.containsKey("ssid") || !parsedJson["ssid"].is() || !parsedJson.containsKey("password") || !parsedJson["password"].is()) { - _SendJSONError(request, F("✖ SSID and password required")); + __SendJSONError(request, F("✖ SSID and password required")); return; } @@ -141,12 +186,12 @@ void BootConfig::_onProxyControlRequest(AsyncWebServerRequest *request) { const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); // do not use plain String, else fails if (!parsedJson.success()) { - _SendJSONError(request, F("✖ Invalid or too big JSON")); + __SendJSONError(request, F("✖ Invalid or too big JSON")); return; } if (!parsedJson.containsKey("enable") || !parsedJson["enable"].is()) { - _SendJSONError(request, F("✖ enable parameter is required")); + __SendJSONError(request, F("✖ enable parameter is required")); return; } @@ -296,17 +341,16 @@ void BootConfig::_onDeviceInfoRequest(AsyncWebServerRequest *request) { for (IHomieSetting* iSetting : IHomieSetting::settings) { JsonObject& jsonSetting = jsonBuffer.createObject(); - if (iSetting->type() != "unknown") { + if (iSetting->getType() != "unknown") { jsonSetting["name"] = iSetting->getName(); jsonSetting["description"] = iSetting->getDescription(); - jsonSetting["type"] = iSetting->type(); + jsonSetting["type"] = iSetting->getType(); jsonSetting["required"] = iSetting->isRequired(); if (!iSetting->isRequired()) { if (iSetting->isBool()) { HomieSetting* setting = static_cast*>(iSetting); jsonSetting["default"] = setting->get(); - } else if (iSetting->isLong()) { HomieSetting* setting = static_cast*>(iSetting); @@ -338,28 +382,28 @@ void BootConfig::_onNetworksRequest(AsyncWebServerRequest *request) { request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), _jsonWifiNetworks); } else { - _SendJSONError(request, F("Initial Wi-Fi scan not finished yet"), 503); + __SendJSONError(request, F("Initial Wi-Fi scan not finished yet"), 503); } } void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { Interface::get().getLogger() << F("Received config request") << endl; if (_flaggedForReboot) { - _SendJSONError(request, F("✖ Device already configured"), 403); + __SendJSONError(request, F("✖ Device already configured"), 403); return; } - DynamicJsonBuffer parseJsonBuffer(MAX_POST_SIZE); + DynamicJsonBuffer parseJsonBuffer(MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE); const char* body = (const char*)(request->_tempObject); JsonObject& parsedJson = parseJsonBuffer.parseObject(body); if (!parsedJson.success()) { - _SendJSONError(request, F("✖ Invalid or too big JSON")); + __SendJSONError(request, F("✖ Invalid or too big JSON")); return; } ConfigValidationResult configValidationResult = Validation::validateConfig(parsedJson); if (!configValidationResult.valid) { - _SendJSONError(request, String(F("✖ Config file is not valid, reason: ")) + configValidationResult.reason); + __SendJSONError(request, String(F("✖ Config file is not valid, reason: ")) + configValidationResult.reason); return; } @@ -369,56 +413,12 @@ void BootConfig::_onConfigRequest(AsyncWebServerRequest *request) { request->send(200, FPSTR(PROGMEM_CONFIG_APPLICATION_JSON), FPSTR(PROGMEM_CONFIG_JSON_SUCCESS)); + Interface::get().disable = true; _flaggedForReboot = true; // We don't reboot immediately, otherwise the response above is not sent _flaggedForRebootAt = millis(); } -void BootConfig::loop() { - Boot::loop(); - - _dns.processNextRequest(); - - if (_flaggedForReboot) { - if (millis() - _flaggedForRebootAt >= 3000UL) { - Interface::get().getLogger() << F("↻ Rebooting into normal mode...") << endl; - Serial.flush(); - ESP.restart(); - } - - return; - } - - if (!_lastWifiScanEnded) { - int8_t scanResult = WiFi.scanComplete(); - - switch (scanResult) { - case WIFI_SCAN_RUNNING: - return; - case WIFI_SCAN_FAILED: - Interface::get().getLogger() << F("✖ Wi-Fi scan failed") << endl; - _ssidCount = 0; - _wifiScanTimer.reset(); - break; - default: - Interface::get().getLogger() << F("✔ Wi-Fi scan completed") << endl; - _ssidCount = scanResult; - _generateNetworksJson(); - _wifiScanAvailable = true; - break; - } - - _lastWifiScanEnded = true; - } - - if (_lastWifiScanEnded && _wifiScanTimer.check()) { - Interface::get().getLogger() << F("Triggering Wi-Fi scan...") << endl; - WiFi.scanNetworks(true); - _wifiScanTimer.tick(); - _lastWifiScanEnded = false; - } -} - -void BootConfig::_sendCORS(AsyncWebServerRequest *request) { +void BootConfig::__sendCORS(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(204); response->addHeader(F("Access-Control-Allow-Origin"), F("*")); response->addHeader(F("Access-Control-Allow-Methods"), F("PUT")); @@ -426,7 +426,7 @@ void BootConfig::_sendCORS(AsyncWebServerRequest *request) { request->send(response); } -void BootConfig::_parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { +void BootConfig::__parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { if (!index && total > MAX_POST_SIZE) { Interface::get().getLogger() << "Request is to large to be proccessed." << endl; } @@ -446,7 +446,7 @@ static const String ConfigJSONError(const String error) { return BEGINNING + error + END; } -void HomieInternals::BootConfig::_SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) +void HomieInternals::BootConfig::__SendJSONError(AsyncWebServerRequest * request, String msg, int16_t code) { Interface::get().getLogger() << msg << endl; const String BEGINNING = String(FPSTR(PROGMEM_CONFIG_JSON_FAILURE_BEGINNING)); diff --git a/src/Homie/Boot/BootConfig.hpp b/src/Homie/Boot/BootConfig.hpp index 6f28f1f7..91710b12 100644 --- a/src/Homie/Boot/BootConfig.hpp +++ b/src/Homie/Boot/BootConfig.hpp @@ -42,7 +42,7 @@ namespace HomieInternals { bool _flaggedForReboot; uint32_t _flaggedForRebootAt; bool _proxyEnabled; - String _apIpStr; + char _apIpStr[MAX_IP_STRING_LENGTH]; void _onCaptivePortal(AsyncWebServerRequest *request); void _onDeviceInfoRequest(AsyncWebServerRequest *request); @@ -54,9 +54,10 @@ namespace HomieInternals { void _proxyHttpRequest(AsyncWebServerRequest *request); void _onWifiStatusRequest(AsyncWebServerRequest *request); - void _sendCORS(AsyncWebServerRequest *request); + // Helpers + static void __sendCORS(AsyncWebServerRequest *request); static const int MAX_POST_SIZE = 1500; - static void _parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); - static void _SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); + static void __parsePost(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + static void __SendJSONError(AsyncWebServerRequest *request, String msg, int16_t code = 400); }; } // namespace HomieInternals diff --git a/src/Homie/Boot/BootNormal.cpp b/src/Homie/Boot/BootNormal.cpp index a234f429..68e319bf 100644 --- a/src/Homie/Boot/BootNormal.cpp +++ b/src/Homie/Boot/BootNormal.cpp @@ -4,7 +4,6 @@ using namespace HomieInternals; BootNormal::BootNormal() : Boot("normal") - , ResetButton() , _mqttReconnectTimer(MQTT_RECONNECT_INITIAL_INTERVAL, MQTT_RECONNECT_MAX_BACKOFF) , _setupFunctionCalled(false) , _mqttConnectNotified(false) @@ -30,6 +29,143 @@ BootNormal::BootNormal() BootNormal::~BootNormal() { } +void BootNormal::setup() { + Boot::setup(); + + Update.runAsync(true); + + if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); + + // Generate topic buffer + size_t baseTopicLength = strlen(Interface::get().getConfig().get().mqtt.baseTopic) + strlen(Interface::get().getConfig().get().deviceId); + size_t longestSubtopicLength = 29 + 1; // /$implementation/ota/firmware + for (HomieNode* iNode : HomieNode::nodes) { + size_t nodeMaxTopicLength = 1 + strlen(iNode->getId()) + 12 + 1; // /id/$properties + if (nodeMaxTopicLength > longestSubtopicLength) longestSubtopicLength = nodeMaxTopicLength; + + for (Property* iProperty : iNode->getProperties()) { + size_t propertyMaxTopicLength = 1 + strlen(iNode->getId()) + 1 + strlen(iProperty->getProperty()) + 1; + if (iProperty->isSettable()) propertyMaxTopicLength += 4; // /set + + if (propertyMaxTopicLength > longestSubtopicLength) longestSubtopicLength = propertyMaxTopicLength; + } + } + _mqttTopic = std::unique_ptr(new char[baseTopicLength + longestSubtopicLength]); + + _wifiGotIpHandler = WiFi.onStationModeGotIP(std::bind(&BootNormal::_onWifiGotIp, this, std::placeholders::_1)); + _wifiDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&BootNormal::_onWifiDisconnected, this, std::placeholders::_1)); + + Interface::get().getMqttClient().onConnect(std::bind(&BootNormal::_onMqttConnected, this)); + Interface::get().getMqttClient().onDisconnect(std::bind(&BootNormal::_onMqttDisconnected, this, std::placeholders::_1)); + Interface::get().getMqttClient().onMessage(std::bind(&BootNormal::_onMqttMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + Interface::get().getMqttClient().onPublish(std::bind(&BootNormal::_onMqttPublish, this, std::placeholders::_1)); + + Interface::get().getMqttClient().setServer(Interface::get().getConfig().get().mqtt.server.host, Interface::get().getConfig().get().mqtt.server.port); + Interface::get().getMqttClient().setMaxTopicLength(MAX_MQTT_TOPIC_LENGTH); + _mqttClientId = std::unique_ptr(new char[strlen(Interface::get().brand) + 1 + strlen(Interface::get().getConfig().get().deviceId) + 1]); + strcpy(_mqttClientId.get(), Interface::get().brand); + strcat_P(_mqttClientId.get(), PSTR("-")); + strcat(_mqttClientId.get(), Interface::get().getConfig().get().deviceId); + Interface::get().getMqttClient().setClientId(_mqttClientId.get()); + char* mqttWillTopic = _prefixMqttTopic(PSTR("/$online")); + _mqttWillTopic = std::unique_ptr(new char[strlen(mqttWillTopic) + 1]); + memcpy(_mqttWillTopic.get(), mqttWillTopic, strlen(mqttWillTopic) + 1); + Interface::get().getMqttClient().setWill(_mqttWillTopic.get(), 1, true, "false"); + + if (Interface::get().getConfig().get().mqtt.auth) Interface::get().getMqttClient().setCredentials(Interface::get().getConfig().get().mqtt.username, Interface::get().getConfig().get().mqtt.password); + + ResetHandler::Attach(); + + Interface::get().getConfig().log(); + + for (HomieNode* iNode : HomieNode::nodes) { + iNode->setup(); + } + + _wifiConnect(); +} + +void BootNormal::loop() { + Boot::loop(); + + if (_flaggedForReboot && Interface::get().reset.idle) { + Interface::get().getLogger() << F("Device is idle") << endl; + + Interface::get().getLogger() << F("↻ Rebooting...") << endl; + Serial.flush(); + ESP.restart(); + } + + if (_mqttReconnectTimer.check()) { + _mqttConnect(); + return; + } + + if (!Interface::get().getMqttClient().connected()) return; + + // here, we are connected to the broker + + if (!_advertisementProgress.done) { + _advertise(); + return; + } + + // here, we finished the advertisement + + if (!_mqttConnectNotified) { + Interface::get().ready = true; + if (Interface::get().led.enabled) Interface::get().getBlinker().stop(); + + Interface::get().getLogger() << F("✔ MQTT ready") << endl; + Interface::get().getLogger() << F("Triggering MQTT_READY event...") << endl; + Interface::get().event.type = HomieEventType::MQTT_READY; + Interface::get().eventHandler(Interface::get().event); + + for (HomieNode* iNode : HomieNode::nodes) { + iNode->onReadyToOperate(); + } + + if (!_setupFunctionCalled) { + Interface::get().getLogger() << F("Calling setup function...") << endl; + Interface::get().setupFunction(); + _setupFunctionCalled = true; + } + + _mqttConnectNotified = true; + return; + } + + // here, we have notified the sketch we are ready + + if (_mqttOfflineMessageId == 0 && Interface::get().flaggedForSleep) { + Interface::get().getLogger() << F("Device in preparation to sleep...") << endl; + _mqttOfflineMessageId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "false"); + } + + if (_statsTimer.check()) { + uint8_t quality = Helpers::rssiToPercentage(WiFi.RSSI()); + char qualityStr[3 + 1]; + itoa(quality, qualityStr, 10); + Interface::get().getLogger() << F("〽 Sending statistics...") << endl; + Interface::get().getLogger() << F(" • Wi-Fi signal quality: ") << qualityStr << F("%") << endl; + uint16_t signalPacketId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/signal")), 1, true, qualityStr); + + _uptime.update(); + char uptimeStr[20 + 1]; + itoa(_uptime.getSeconds(), uptimeStr, 10); + Interface::get().getLogger() << F(" • Uptime: ") << uptimeStr << F("s") << endl; + uint16_t uptimePacketId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/uptime")), 1, true, uptimeStr); + + if (signalPacketId != 0 && uptimePacketId != 0) _statsTimer.tick(); + } + + Interface::get().loopFunction(); + + for (HomieNode* iNode : HomieNode::nodes) { + iNode->loop(); + } +} + void BootNormal::_prefixMqttTopic() { strcpy(_mqttTopic.get(), Interface::get().getConfig().get().mqtt.baseTopic); strcat(_mqttTopic.get(), Interface::get().getConfig().get().deviceId); @@ -52,10 +188,6 @@ bool BootNormal::_publishOtaStatus(int status, const char* info) { return Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/ota/status")), 0, true, payload.c_str()) != 0; } -bool BootNormal::_publishOtaStatus_P(int status, PGM_P info) { - return _publishOtaStatus(status, String(info).c_str()); -} - void BootNormal::_endOtaUpdate(bool success, uint8_t update_error) { if (success) { Interface::get().getLogger() << F("✔ OTA succeeded") << endl; @@ -65,7 +197,8 @@ void BootNormal::_endOtaUpdate(bool success, uint8_t update_error) { _publishOtaStatus(200); // 200 OK _flaggedForReboot = true; - } else { + } + else { int code; String info; switch (update_error) { @@ -107,7 +240,7 @@ void BootNormal::_endOtaUpdate(bool success, uint8_t update_error) { } void BootNormal::_wifiConnect() { - if (!Interface::get().flaggedForSleep) { + if (!Interface::get().disable) { if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); Interface::get().getLogger() << F("↕ Attempting to connect to Wi-Fi...") << endl; @@ -115,25 +248,26 @@ void BootNormal::_wifiConnect() { WiFi.hostname(Interface::get().getConfig().get().deviceId); if (strcmp_P(Interface::get().getConfig().get().wifi.ip, PSTR("")) != 0) { // on _validateConfigWifi there is a requirement for mask and gateway - byte convertedBytes[4]; - Helpers::stringToBytes(Interface::get().getConfig().get().wifi.ip, '.', convertedBytes, 4, 10); - IPAddress convertedIp(convertedBytes[0], convertedBytes[1], convertedBytes[2], convertedBytes[3]); - Helpers::stringToBytes(Interface::get().getConfig().get().wifi.mask, '.', convertedBytes, 4, 10); - IPAddress convertedMask(convertedBytes[0], convertedBytes[1], convertedBytes[2], convertedBytes[3]); - Helpers::stringToBytes(Interface::get().getConfig().get().wifi.gw, '.', convertedBytes, 4, 10); - IPAddress convertedGateway(convertedBytes[0], convertedBytes[1], convertedBytes[2], convertedBytes[3]); + IPAddress convertedIp; + convertedIp.fromString(Interface::get().getConfig().get().wifi.ip); + IPAddress convertedMask; + convertedMask.fromString(Interface::get().getConfig().get().wifi.mask); + IPAddress convertedGateway; + convertedGateway.fromString(Interface::get().getConfig().get().wifi.gw); if (strcmp_P(Interface::get().getConfig().get().wifi.dns1, PSTR("")) != 0) { - Helpers::stringToBytes(Interface::get().getConfig().get().wifi.dns1, '.', convertedBytes, 4, 10); - IPAddress convertedDns1(convertedBytes[0], convertedBytes[1], convertedBytes[2], convertedBytes[3]); + IPAddress convertedDns1; + convertedDns1.fromString(Interface::get().getConfig().get().wifi.dns1); if ((strcmp_P(Interface::get().getConfig().get().wifi.dns2, PSTR("")) != 0)) { // on _validateConfigWifi there is requirement that we need dns1 if we want to define dns2 - Helpers::stringToBytes(Interface::get().getConfig().get().wifi.dns2, '.', convertedBytes, 4, 10); - IPAddress convertedDns2(convertedBytes[0], convertedBytes[1], convertedBytes[2], convertedBytes[3]); + IPAddress convertedDns2; + convertedDns2.fromString(Interface::get().getConfig().get().wifi.dns2); WiFi.config(convertedIp, convertedGateway, convertedMask, convertedDns1, convertedDns2); - } else { + } + else { WiFi.config(convertedIp, convertedGateway, convertedMask, convertedDns1); } - } else { + } + else { WiFi.config(convertedIp, convertedGateway, convertedMask); } } @@ -142,7 +276,8 @@ void BootNormal::_wifiConnect() { byte bssidBytes[6]; Helpers::stringToBytes(Interface::get().getConfig().get().wifi.bssid, ':', bssidBytes, 6, 16); WiFi.begin(Interface::get().getConfig().get().wifi.ssid, Interface::get().getConfig().get().wifi.password, Interface::get().getConfig().get().wifi.channel, bssidBytes); - } else { + } + else { WiFi.begin(Interface::get().getConfig().get().wifi.ssid, Interface::get().getConfig().get().wifi.password); } @@ -179,9 +314,11 @@ void BootNormal::_onWifiDisconnected(const WiFiEventStationModeDisconnected& eve } void BootNormal::_mqttConnect() { - if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_MQTT_DELAY); - Interface::get().getLogger() << F("↕ Attempting to connect to MQTT...") << endl; - Interface::get().getMqttClient().connect(); + if (!Interface::get().disable) { + if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_MQTT_DELAY); + Interface::get().getLogger() << F("↕ Attempting to connect to MQTT...") << endl; + Interface::get().getMqttClient().connect(); + } } void BootNormal::_advertise() { @@ -202,8 +339,7 @@ void BootNormal::_advertise() { case AdvertisementProgress::GlobalStep::PUB_LOCALIP: { IPAddress localIp = WiFi.localIP(); char localIpStr[MAX_IP_STRING_LENGTH]; - snprintf(localIpStr, MAX_IP_STRING_LENGTH, "%d.%d.%d.%d", localIp[0], localIp[1], localIp[2], localIp[3]); - + Helpers::ipToString(localIp, localIpStr); packetId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$localip")), 1, true, localIpStr); if (packetId != 0) _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_STATS_INTERVAL; break; @@ -248,7 +384,8 @@ void BootNormal::_advertise() { _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::PUB_NODES; _advertisementProgress.nodeStep = AdvertisementProgress::NodeStep::PUB_TYPE; _advertisementProgress.currentNodeIndex = 0; - } else { + } + else { _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_OTA; } } @@ -287,7 +424,8 @@ void BootNormal::_advertise() { if (_advertisementProgress.currentNodeIndex < HomieNode::nodes.size() - 1) { _advertisementProgress.currentNodeIndex++; _advertisementProgress.nodeStep = AdvertisementProgress::NodeStep::PUB_TYPE; - } else { + } + else { _advertisementProgress.globalStep = AdvertisementProgress::GlobalStep::SUB_IMPLEMENTATION_OTA; } } @@ -351,7 +489,7 @@ void BootNormal::_onMqttDisconnected(AsyncMqttClientDisconnectReason reason) { _mqttDisconnectNotified = true; - if (_mqttOfflineMessageId != 0) { + if (Interface::get().flaggedForSleep) { _mqttOfflineMessageId = 0; Interface::get().getLogger() << F("Triggering READY_TO_SLEEP event...") << endl; Interface::get().event.type = HomieEventType::READY_TO_SLEEP; @@ -362,7 +500,8 @@ void BootNormal::_onMqttDisconnected(AsyncMqttClientDisconnectReason reason) { _mqttConnect(); - } else { + } + else { _mqttReconnectTimer.activate(); } } @@ -372,33 +511,96 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa // split topic on each "/" if (index == 0) { - char* afterBaseTopic = topic + strlen(Interface::get().getConfig().get().mqtt.baseTopic); + __splitTopic(topic); + } - uint8_t topicLevelsCount = 1; - for (uint8_t i = 0; i < strlen(afterBaseTopic); i++) { - if (afterBaseTopic[i] == '/') topicLevelsCount++; - } + // 1. Handle OTA firmware (not copied to payload buffer) + if (__handleOTAUpdates(topic, payload, properties, len, index, total)) + return; - _mqttTopicLevels = std::unique_ptr(new char*[topicLevelsCount]); - _mqttTopicLevelsCount = topicLevelsCount; + // 2. Fill Payload Buffer + if (__fillPayloadBuffer(topic, payload, properties, len, index, total)) + return; - const char* delimiter = "/"; - uint8_t topicLevelIndex = 0; + /* Arrived here, the payload is complete */ - char* token = strtok(afterBaseTopic, delimiter); - while (token != nullptr) { - _mqttTopicLevels.get()[topicLevelIndex++] = token; + // 3. handle broadcasts + if (__handleBroadcasts(topic, payload, properties, len, index, total)) + return; - token = strtok(nullptr, delimiter); - } + // 4.all following messages are only for this deviceId + if (strcmp(_mqttTopicLevels.get()[0], Interface::get().getConfig().get().deviceId) != 0) + return; + + // 5. handle reset + if (__handleResets(topic, payload, properties, len, index, total)) + return; + + // 6. handle config set + if (__handleConfig(topic, payload, properties, len, index, total)) + return; + + // 7. here, we're sure we have a node property + if (__handleNodeProperty(topic, payload, properties, len, index, total)) + return; + +} + +void BootNormal::_onMqttPublish(uint16_t id) { + Interface::get().event.type = HomieEventType::MQTT_PACKET_ACKNOWLEDGED; + Interface::get().event.packetId = id; + Interface::get().eventHandler(Interface::get().event); + + if (Interface::get().flaggedForSleep && id == _mqttOfflineMessageId) { + Interface::get().getLogger() << F("Offline message acknowledged. Disconnecting MQTT...") << endl; + Interface::get().getMqttClient().disconnect(); } +} - // initialize HomieRange - HomieRange range; - range.isRange = false; - range.index = 0; +// _onMqttMessage Helpers - // 1. Handle OTA firmware (not copied to payload buffer) +void BootNormal::__splitTopic(char* topic) +{ + // split topic on each "/" + char* afterBaseTopic = topic + strlen(Interface::get().getConfig().get().mqtt.baseTopic); + + uint8_t topicLevelsCount = 1; + for (uint8_t i = 0; i < strlen(afterBaseTopic); i++) { + if (afterBaseTopic[i] == '/') topicLevelsCount++; + } + + _mqttTopicLevels = std::unique_ptr(new char*[topicLevelsCount]); + _mqttTopicLevelsCount = topicLevelsCount; + + const char* delimiter = "/"; + uint8_t topicLevelIndex = 0; + + char* token = strtok(afterBaseTopic, delimiter); + while (token != nullptr) { + _mqttTopicLevels[topicLevelIndex++] = token; + + token = strtok(nullptr, delimiter); + } +} + +bool HomieInternals::BootNormal::__fillPayloadBuffer(char * topic, char * payload, AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) +{ + // Reallocate Buffer everytime a new message is received + if (_mqttPayloadBuffer == nullptr || index == 0) _mqttPayloadBuffer = std::unique_ptr(new char[total + 1]); + + // copy payload into buffer + memcpy(_mqttPayloadBuffer.get() + index, payload, len); + + // return if payload buffer is not complete + if (index + len != total) + return true; + // terminate buffer + _mqttPayloadBuffer.get()[total] = '\0'; + return false; +} + +bool HomieInternals::BootNormal::__handleOTAUpdates(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total) +{ if ( _mqttTopicLevelsCount == 5 && strcmp(_mqttTopicLevels.get()[0], Interface::get().getConfig().get().deviceId) == 0 @@ -411,19 +613,21 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (!Interface::get().getConfig().get().ota.enabled) { _publishOtaStatus(403); // 403 Forbidden Interface::get().getLogger() << F("✖ Aborting, OTA not enabled") << endl; - return; + return true; } char* firmwareMd5 = _mqttTopicLevels.get()[4]; if (!Helpers::validateMd5(firmwareMd5)) { _endOtaUpdate(false, UPDATE_ERROR_MD5); Interface::get().getLogger() << F("✖ Aborting, invalid MD5") << endl; - return; - } else if (strcmp(firmwareMd5, _fwChecksum) == 0) { + return true; + } + else if (strcmp(firmwareMd5, _fwChecksum) == 0) { _publishOtaStatus(304); // 304 Not Modified Interface::get().getLogger() << F("✖ Aborting, firmware is the same") << endl; - return; - } else { + return true; + } + else { Update.setMD5(firmwareMd5); _publishOtaStatus(202); _otaOngoing = true; @@ -433,8 +637,9 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa Interface::get().event.type = HomieEventType::OTA_STARTED; Interface::get().eventHandler(Interface::get().event); } - } else if (!_otaOngoing) { - return; // we've not validated the checksum + } + else if (!_otaOngoing) { + return true; // we've not validated the checksum } // here, we need to flash the payload @@ -444,7 +649,8 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (*payload == 0xE9) { _otaIsBase64 = false; Interface::get().getLogger() << F("Firmware is binary") << endl; - } else { + } + else { // Base64-decode first two bytes. Compare decoded value against magic byte. char plain[2]; // need 12 bits base64_init_decodestate(&_otaBase64State); @@ -456,15 +662,16 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (total % 4) { // Base64 encoded length not a multiple of 4 bytes _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); - return; + return true; } // Restart base64-decoder base64_init_decodestate(&_otaBase64State); - } else { + } + else { // Bad firmware format _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); - return; + return true; } } _otaSizeDone = 0; @@ -473,7 +680,7 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (!success) { // Detected error during begin (e.g. size == 0 or size > space) _endOtaUpdate(false, Update.getError()); - return; + return true; } } @@ -489,18 +696,20 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa bool b64 = ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) || ((c >= '0') && (c <= '9')) || (c == '+') || (c == '/'); if (b64) { bin_len++; - } else if (c == '=') { + } + else if (c == '=') { // Ignore "=" padding (but only at the end and only up to 2) if (index + i < total - 2) { _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); - return; + return true; } // Note the number of pad characters at the end _otaBase64Pads++; - } else { + } + else { // Non-base64 character in firmware _endOtaUpdate(false, UPDATE_ERROR_MAGIC_BYTE); - return; + return true; } } if (bin_len > 0) { @@ -518,10 +727,12 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (bin_len > 1) { write_len += (size_t)base64_decode_block((const char*)payload + dec_len, bin_len - dec_len, payload + write_len, &_otaBase64State); } - } else { + } + else { write_len = 0; } - } else { + } + else { // Binary firmware write_len = len; } @@ -551,7 +762,7 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa _publishOtaStatus(206, progress.c_str()); // 206 Partial Content - // Done with the update? + // Done with the update? if (index + len == total) { // With base64-coded firmware, we may have provided a length off by one or two // to Update.begin() because the base64-coded firmware may use padding (one or @@ -559,32 +770,24 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa // Check the real length here and ask Update::end() to skip this test. if ((_otaIsBase64) && (_otaSizeDone != _otaSizeTotal)) { _endOtaUpdate(false, UPDATE_ERROR_SIZE); - return; + return true; } success = Update.end(_otaIsBase64); _endOtaUpdate(success, Update.getError()); } - } else { + } + else { // Error erasing or writing flash _endOtaUpdate(false, Update.getError()); } } - return; + return true; } + return false; +} - // 2. Fill Payload Buffer - - // Reallocate Buffer everytime a new message is received - if (_mqttPayloadBuffer == nullptr || index == 0) _mqttPayloadBuffer = std::unique_ptr(new char[total + 1]); - - memcpy(_mqttPayloadBuffer.get() + index, payload, len); - - if (index + len != total) return; // return if payload buffer is not complete - _mqttPayloadBuffer.get()[total] = '\0'; - - /* Arrived here, the payload is complete */ - - // handle broadcasts +bool HomieInternals::BootNormal::__handleBroadcasts(char * topic, char * payload, AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) +{ if ( _mqttTopicLevelsCount == 2 && strcmp_P(_mqttTopicLevels.get()[0], PSTR("$broadcast")) == 0 @@ -597,13 +800,13 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa Interface::get().getLogger() << F(" • Level: ") << broadcastLevel << endl; Interface::get().getLogger() << F(" • Value: ") << _mqttPayloadBuffer.get() << endl; } - return; + return true; } + return false; +} - // all following messages are only for this deviceId - if (strcmp(_mqttTopicLevels.get()[0], Interface::get().getConfig().get().deviceId) != 0) return; - - // handle reset +bool HomieInternals::BootNormal::__handleResets(char * topic, char * payload, AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) +{ if ( _mqttTopicLevelsCount == 3 && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 @@ -611,12 +814,16 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa && strcmp_P(_mqttPayloadBuffer.get(), PSTR("true")) == 0 ) { Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$implementation/reset")), 1, true, "false"); - _flaggedForReset = true; Interface::get().getLogger() << F("Flagged for reset by network") << endl; - return; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; + return true; } + return false; +} - // handle config set +bool HomieInternals::BootNormal::__handleConfig(char * topic, char * payload, AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) +{ if ( _mqttTopicLevelsCount == 4 && strcmp_P(_mqttTopicLevels.get()[1], PSTR("$implementation")) == 0 @@ -628,22 +835,35 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa Interface::get().getLogger() << F("✔ Configuration updated") << endl; _flaggedForReboot = true; Interface::get().getLogger() << F("Flagged for reboot") << endl; - } else { + } + else { Interface::get().getLogger() << F("✖ Configuration not updated") << endl; } - return; + return true; } + return false; +} - // here, we're sure we have a node property +bool HomieInternals::BootNormal::__handleNodeProperty(char * topic, char * payload, AsyncMqttClientMessageProperties & properties, size_t len, size_t index, size_t total) +{ + + // initialize HomieRange + HomieRange range; + range.isRange = false; + range.index = 0; char* node = _mqttTopicLevels.get()[1]; char* property = _mqttTopicLevels.get()[2]; HomieNode* homieNode = HomieNode::find(node); if (!homieNode) { Interface::get().getLogger() << F("Node ") << node << F(" not registered") << endl; - return; + return true; } +#ifdef DEBUG + Interface::get().getLogger() << F("Recived network message for ") << homieNode->getId() << endl; +#endif // DEBUG + int16_t rangeSeparator = -1; for (uint16_t i = 0; i < strlen(property); i++) { if (property[i] == '_') { @@ -659,7 +879,7 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa for (uint8_t i = 0; i < rangeIndexTest.length(); i++) { if (!isDigit(rangeIndexTest.charAt(i))) { Interface::get().getLogger() << F("Range index ") << rangeIndexStr << F(" is not valid") << endl; - return; + return true; } } range.index = rangeIndexTest.toInt(); @@ -672,12 +892,14 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (range.index >= iProperty->getLower() && range.index <= iProperty->getUpper()) { propertyObject = iProperty; break; - } else { + } + else { Interface::get().getLogger() << F("Range index ") << range.index << F(" is not within the bounds of ") << property << endl; - return; + return true; } } - } else if (strcmp(property, iProperty->getProperty()) == 0) { + } + else if (strcmp(property, iProperty->getProperty()) == 0) { propertyObject = iProperty; break; } @@ -685,18 +907,24 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa if (!propertyObject || !propertyObject->isSettable()) { Interface::get().getLogger() << F("Node ") << node << F(": ") << property << F(" property not settable") << endl; - return; + return true; } +#ifdef DEBUG Interface::get().getLogger() << F("Calling global input handler...") << endl; +#endif // DEBUG bool handled = Interface::get().globalInputHandler(*homieNode, String(property), range, String(_mqttPayloadBuffer.get())); - if (handled) return; + if (handled) return true; +#ifdef DEBUG Interface::get().getLogger() << F("Calling node input handler...") << endl; +#endif // DEBUG handled = homieNode->handleInput(String(property), range, String(_mqttPayloadBuffer.get())); - if (handled) return; + if (handled) return true; +#ifdef DEBUG Interface::get().getLogger() << F("Calling property input handler...") << endl; +#endif // DEBUG handled = propertyObject->getInputHandler()(range, String(_mqttPayloadBuffer.get())); if (!handled) { @@ -706,173 +934,12 @@ void BootNormal::_onMqttMessage(char* topic, char* payload, AsyncMqttClientMessa Interface::get().getLogger() << F(" • Is range? "); if (range.isRange) { Interface::get().getLogger() << F("yes (") << range.index << F(")") << endl; - } else { + } + else { Interface::get().getLogger() << F("no") << endl; } Interface::get().getLogger() << F(" • Value: ") << _mqttPayloadBuffer.get() << endl; } -} - -void BootNormal::_onMqttPublish(uint16_t id) { - Interface::get().event.type = HomieEventType::MQTT_PACKET_ACKNOWLEDGED; - Interface::get().event.packetId = id; - Interface::get().eventHandler(Interface::get().event); - - if (Interface::get().flaggedForSleep && id == _mqttOfflineMessageId) { - Interface::get().getLogger() << F("Offline message acknowledged. Disconnecting MQTT...") << endl; - Interface::get().getMqttClient().disconnect(); - } -} - -void BootNormal::setup() { - Boot::setup(); - - Update.runAsync(true); - - if (Interface::get().led.enabled) Interface::get().getBlinker().start(LED_WIFI_DELAY); - - // Generate topic buffer - size_t baseTopicLength = strlen(Interface::get().getConfig().get().mqtt.baseTopic) + strlen(Interface::get().getConfig().get().deviceId); - size_t longestSubtopicLength = 29 + 1; // /$implementation/ota/firmware - for (HomieNode* iNode : HomieNode::nodes) { - size_t nodeMaxTopicLength = 1 + strlen(iNode->getId()) + 12 + 1; // /id/$properties - if (nodeMaxTopicLength > longestSubtopicLength) longestSubtopicLength = nodeMaxTopicLength; - - for (Property* iProperty : iNode->getProperties()) { - size_t propertyMaxTopicLength = 1 + strlen(iNode->getId()) + 1 + strlen(iProperty->getProperty()) + 1; - if (iProperty->isSettable()) propertyMaxTopicLength += 4; // /set - - if (propertyMaxTopicLength > longestSubtopicLength) longestSubtopicLength = propertyMaxTopicLength; - } - } - _mqttTopic = std::unique_ptr(new char[baseTopicLength + longestSubtopicLength]); - - _wifiGotIpHandler = WiFi.onStationModeGotIP(std::bind(&BootNormal::_onWifiGotIp, this, std::placeholders::_1)); - _wifiDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&BootNormal::_onWifiDisconnected, this, std::placeholders::_1)); - - Interface::get().getMqttClient().onConnect(std::bind(&BootNormal::_onMqttConnected, this)); - Interface::get().getMqttClient().onDisconnect(std::bind(&BootNormal::_onMqttDisconnected, this, std::placeholders::_1)); - Interface::get().getMqttClient().onMessage(std::bind(&BootNormal::_onMqttMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); - Interface::get().getMqttClient().onPublish(std::bind(&BootNormal::_onMqttPublish, this, std::placeholders::_1)); - - Interface::get().getMqttClient().setServer(Interface::get().getConfig().get().mqtt.server.host, Interface::get().getConfig().get().mqtt.server.port); - Interface::get().getMqttClient().setMaxTopicLength(MAX_MQTT_TOPIC_LENGTH); - _mqttClientId = std::unique_ptr(new char[strlen(Interface::get().brand) + 1 + strlen(Interface::get().getConfig().get().deviceId) + 1]); - strcpy(_mqttClientId.get(), Interface::get().brand); - strcat_P(_mqttClientId.get(), PSTR("-")); - strcat(_mqttClientId.get(), Interface::get().getConfig().get().deviceId); - Interface::get().getMqttClient().setClientId(_mqttClientId.get()); - char* mqttWillTopic = _prefixMqttTopic(PSTR("/$online")); - _mqttWillTopic = std::unique_ptr(new char[strlen(mqttWillTopic) + 1]); - memcpy(_mqttWillTopic.get(), mqttWillTopic, strlen(mqttWillTopic) + 1); - Interface::get().getMqttClient().setWill(_mqttWillTopic.get(), 1, true, "false"); - - if (Interface::get().getConfig().get().mqtt.auth) Interface::get().getMqttClient().setCredentials(Interface::get().getConfig().get().mqtt.username, Interface::get().getConfig().get().mqtt.password); - ResetButton::Attach(); - - Interface::get().getConfig().log(); - - for (HomieNode* iNode : HomieNode::nodes) { - iNode->setup(); - } - - _wifiConnect(); -} - -void BootNormal::loop() { - Boot::loop(); - - ResetButton::_handleReset(); - - if (ResetButton::_flaggedForReset && Interface::get().reset.idle) { - Interface::get().getLogger() << F("Device is idle") << endl; - Interface::get().getConfig().erase(); - Interface::get().getLogger() << F("Configuration erased") << endl; - - Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; - Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; - Interface::get().eventHandler(Interface::get().event); - - Interface::get().getLogger() << F("↻ Rebooting into config mode...") << endl; - Serial.flush(); - ESP.restart(); - } - - if (_flaggedForReboot && Interface::get().reset.idle) { - Interface::get().getLogger() << F("Device is idle") << endl; - - Interface::get().getLogger() << F("↻ Rebooting...") << endl; - Serial.flush(); - ESP.restart(); - } - - if (_mqttReconnectTimer.check()) { - _mqttConnect(); - return; - } - - if (!Interface::get().getMqttClient().connected()) return; - - // here, we are connected to the broker - - if (!_advertisementProgress.done) { - _advertise(); - return; - } - - // here, we finished the advertisement - - if (!_mqttConnectNotified) { - Interface::get().ready = true; - if (Interface::get().led.enabled) Interface::get().getBlinker().stop(); - - Interface::get().getLogger() << F("✔ MQTT ready") << endl; - Interface::get().getLogger() << F("Triggering MQTT_READY event...") << endl; - Interface::get().event.type = HomieEventType::MQTT_READY; - Interface::get().eventHandler(Interface::get().event); - - for (HomieNode* iNode : HomieNode::nodes) { - iNode->onReadyToOperate(); - } - - if (!_setupFunctionCalled) { - Interface::get().getLogger() << F("Calling setup function...") << endl; - Interface::get().setupFunction(); - _setupFunctionCalled = true; - } - - _mqttConnectNotified = true; - return; - } - - // here, we have notified the sketch we are ready - - if (_mqttOfflineMessageId == 0 && Interface::get().flaggedForSleep) { - Interface::get().getLogger() << F("Device in preparation to sleep...") << endl; - _mqttOfflineMessageId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$online")), 1, true, "false"); - } - - if (_statsTimer.check()) { - uint8_t quality = Helpers::rssiToPercentage(WiFi.RSSI()); - char qualityStr[3 + 1]; - itoa(quality, qualityStr, 10); - Interface::get().getLogger() << F("〽 Sending statistics...") << endl; - Interface::get().getLogger() << F(" • Wi-Fi signal quality: ") << qualityStr << F("%") << endl; - uint16_t signalPacketId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/signal")), 1, true, qualityStr); - - _uptime.update(); - char uptimeStr[20 + 1]; - itoa(_uptime.getSeconds(), uptimeStr, 10); - Interface::get().getLogger() << F(" • Uptime: ") << uptimeStr << F("s") << endl; - uint16_t uptimePacketId = Interface::get().getMqttClient().publish(_prefixMqttTopic(PSTR("/$stats/uptime")), 1, true, uptimeStr); - - if (signalPacketId != 0 && uptimePacketId != 0) _statsTimer.tick(); - } - - Interface::get().loopFunction(); - - for (HomieNode* iNode : HomieNode::nodes) { - iNode->loop(); - } + return false; } diff --git a/src/Homie/Boot/BootNormal.hpp b/src/Homie/Boot/BootNormal.hpp index 56b504ca..0d8df1a2 100644 --- a/src/Homie/Boot/BootNormal.hpp +++ b/src/Homie/Boot/BootNormal.hpp @@ -18,10 +18,10 @@ #include "../Timer.hpp" #include "../ExponentialBackoffTimer.hpp" #include "Boot.hpp" -#include "../Utils/ResetButton.hpp" +#include "../Utils/ResetHandler.hpp" namespace HomieInternals { - class BootNormal : public Boot, public ResetButton { + class BootNormal : public Boot { public: BootNormal(); ~BootNormal(); @@ -98,7 +98,15 @@ namespace HomieInternals { void _prefixMqttTopic(); char* _prefixMqttTopic(PGM_P topic); bool _publishOtaStatus(int status, const char* info = nullptr); - bool _publishOtaStatus_P(int status, PGM_P info); void _endOtaUpdate(bool success, uint8_t update_error = UPDATE_ERROR_OK); + + // _onMqttMessage Helpers + void __splitTopic(char* topic); + bool __fillPayloadBuffer(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleOTAUpdates(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleBroadcasts(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleResets(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleConfig(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); + bool __handleNodeProperty(char* topic, char* payload, AsyncMqttClientMessageProperties& properties, size_t len, size_t index, size_t total); }; } // namespace HomieInternals diff --git a/src/Homie/Boot/BootStandalone.cpp b/src/Homie/Boot/BootStandalone.cpp index 98f02d83..bd326782 100644 --- a/src/Homie/Boot/BootStandalone.cpp +++ b/src/Homie/Boot/BootStandalone.cpp @@ -3,8 +3,7 @@ using namespace HomieInternals; BootStandalone::BootStandalone() - : Boot("standalone") - , ResetButton() { + : Boot("standalone") { } BootStandalone::~BootStandalone() { @@ -15,24 +14,9 @@ void BootStandalone::setup() { WiFi.mode(WIFI_OFF); - ResetButton::Attach(); + ResetHandler::Attach(); } void BootStandalone::loop() { Boot::loop(); - - ResetButton::_handleReset(); - - if (ResetButton::_flaggedForReset && Interface::get().reset.idle) { - Interface::get().getLogger() << F("Device is idle") << endl; - Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::CONFIGURATION); - - Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; - Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; - Interface::get().eventHandler(Interface::get().event); - - Interface::get().getLogger() << F("↻ Rebooting into config mode...") << endl; - Serial.flush(); - ESP.restart(); - } } diff --git a/src/Homie/Boot/BootStandalone.hpp b/src/Homie/Boot/BootStandalone.hpp index 82d46c7b..2b795b1d 100644 --- a/src/Homie/Boot/BootStandalone.hpp +++ b/src/Homie/Boot/BootStandalone.hpp @@ -4,10 +4,10 @@ #include "Boot.hpp" #include "../../StreamingOperator.hpp" -#include "../Utils/ResetButton.hpp" +#include "../Utils/ResetHandler.hpp" namespace HomieInternals { - class BootStandalone : public Boot, public ResetButton { + class BootStandalone : public Boot { public: BootStandalone(); ~BootStandalone(); diff --git a/src/Homie/Config.cpp b/src/Homie/Config.cpp index 22e94fd7..1d60c0f9 100644 --- a/src/Homie/Config.cpp +++ b/src/Homie/Config.cpp @@ -280,6 +280,8 @@ bool Config::patch(const char* patch) { StaticJsonBuffer configJsonBuffer; JsonObject& configObject = configJsonBuffer.parseObject(configJson); + // To do alow object that dont currently exist to be added like settings. + // if settings wasnt there origionally then it should be allowed to be added by incremental. for (JsonObject::iterator it = patchObject.begin(); it != patchObject.end(); ++it) { if (patchObject[it->key].is()) { JsonObject& subObject = patchObject[it->key].as(); diff --git a/src/Homie/Datatypes/Interface.cpp b/src/Homie/Datatypes/Interface.cpp index c35ad27c..f0fbb99f 100644 --- a/src/Homie/Datatypes/Interface.cpp +++ b/src/Homie/Datatypes/Interface.cpp @@ -5,20 +5,21 @@ using namespace HomieInternals; InterfaceData Interface::_interface; // need to define the static variable InterfaceData::InterfaceData() -: brand{'\0'} -, bootMode{HomieBootMode::UNDEFINED} -, configurationAp { .secured = false, .password = {'\0'} } -, firmware { .name = {'\0'}, .version = {'\0'} } -, led { .enabled = false, .pin = 0, .on = 0 } -, reset { .enabled = false, .idle = false, .triggerPin = 0, .triggerState = 0, .triggerTime = 0, .flaggedBySketch = false } -, flaggedForSleep{false} -, event{} -, ready{false} -, _logger{nullptr} -, _blinker{nullptr} -, _config{nullptr} -, _mqttClient{nullptr} -, _sendingPromise{nullptr} { + : brand{ '\0' } + , bootMode{ HomieBootMode::UNDEFINED } + , configurationAp{ .secured = false,.password = {'\0'} } + , firmware{ .name = {'\0'},.version = {'\0'} } + , led{ .enabled = false,.pin = 0,.on = 0 } + , reset{ .enabled = false,.idle = false,.triggerPin = 0,.triggerState = 0,.triggerTime = 0,.resetFlag = false } + , disable{ false } + , flaggedForSleep{ false } + , event{} + , ready{ false } + , _logger{ nullptr } + , _blinker{ nullptr } + , _config{ nullptr } + , _mqttClient{ nullptr } + , _sendingPromise{ nullptr } { } InterfaceData& Interface::get() { diff --git a/src/Homie/Datatypes/Interface.hpp b/src/Homie/Datatypes/Interface.hpp index f704cbfd..82695f6d 100644 --- a/src/Homie/Datatypes/Interface.hpp +++ b/src/Homie/Datatypes/Interface.hpp @@ -13,77 +13,79 @@ #include "../../HomieEvent.hpp" namespace HomieInternals { -class Logger; -class Blinker; -class Config; -class SendingPromise; -class HomieClass; -class InterfaceData { - friend HomieClass; - - public: - InterfaceData(); - - /***** User configurable data *****/ - char brand[MAX_BRAND_LENGTH]; - - HomieBootMode bootMode; - - struct ConfigurationAP { - bool secured; - char password[MAX_WIFI_PASSWORD_LENGTH]; - } configurationAp; - - struct Firmware { - char name[MAX_FIRMWARE_NAME_LENGTH]; - char version[MAX_FIRMWARE_VERSION_LENGTH]; - } firmware; - - struct LED { - bool enabled; - uint8_t pin; - uint8_t on; - } led; - - struct Reset { - bool enabled; - bool idle; - uint8_t triggerPin; - uint8_t triggerState; - uint16_t triggerTime; - bool flaggedBySketch; - } reset; - - bool flaggedForSleep; - - GlobalInputHandler globalInputHandler; - BroadcastHandler broadcastHandler; - OperationFunction setupFunction; - OperationFunction loopFunction; - EventHandler eventHandler; - - /***** Runtime data *****/ - HomieEvent event; - bool ready; - Logger& getLogger() { return *_logger; } - Blinker& getBlinker() { return *_blinker; } - Config& getConfig() { return *_config; } - AsyncMqttClient& getMqttClient() { return *_mqttClient; } - SendingPromise& getSendingPromise() { return *_sendingPromise; } - - private: - Logger* _logger; - Blinker* _blinker; - Config* _config; - AsyncMqttClient* _mqttClient; - SendingPromise* _sendingPromise; -}; - -class Interface { - public: - static InterfaceData& get(); - - private: - static InterfaceData _interface; -}; + class Logger; + class Blinker; + class Config; + class SendingPromise; + class HomieClass; + + class InterfaceData { + friend HomieClass; + + public: + InterfaceData(); + + /***** User configurable data *****/ + char brand[MAX_BRAND_LENGTH]; + + HomieBootMode bootMode; + + struct ConfigurationAP { + bool secured; + char password[MAX_WIFI_PASSWORD_LENGTH]; + } configurationAp; + + struct Firmware { + char name[MAX_FIRMWARE_NAME_LENGTH]; + char version[MAX_FIRMWARE_VERSION_LENGTH]; + } firmware; + + struct LED { + bool enabled; + uint8_t pin; + uint8_t on; + } led; + + struct Reset { + bool enabled; + bool idle; + uint8_t triggerPin; + uint8_t triggerState; + uint16_t triggerTime; + bool resetFlag; + } reset; + + bool disable; + bool flaggedForSleep; + + GlobalInputHandler globalInputHandler; + BroadcastHandler broadcastHandler; + OperationFunction setupFunction; + OperationFunction loopFunction; + EventHandler eventHandler; + + /***** Runtime data *****/ + HomieEvent event; + bool ready; + Logger& getLogger() { return *_logger; } + Blinker& getBlinker() { return *_blinker; } + Config& getConfig() { return *_config; } + AsyncMqttClient& getMqttClient() { return *_mqttClient; } + SendingPromise& getSendingPromise() { return *_sendingPromise; } + + private: + Logger* _logger; + Blinker* _blinker; + Config* _config; + AsyncMqttClient* _mqttClient; + SendingPromise* _sendingPromise; + }; + + class Interface { + public: + static InterfaceData& get(); + + private: + static InterfaceData _interface; + }; } // namespace HomieInternals diff --git a/src/Homie/Limits.hpp b/src/Homie/Limits.hpp index fd9b9d3c..e4686dc2 100644 --- a/src/Homie/Limits.hpp +++ b/src/Homie/Limits.hpp @@ -4,29 +4,32 @@ namespace HomieInternals { const uint16_t MAX_JSON_CONFIG_FILE_SIZE = 1000; - // 6 elements at root, 9 elements at wifi, 6 elements at mqtt, 1 element at ota, max 10 elements at settings - const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(9) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(10); + + // max setting elements + const uint8_t MAX_CONFIG_SETTING_SIZE = 10; + // 6 elements at root, 9 elements at wifi, 6 elements at mqtt, 1 element at ota, max settings elements + const uint16_t MAX_JSON_CONFIG_ARDUINOJSON_BUFFER_SIZE = JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(9) + JSON_OBJECT_SIZE(6) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(MAX_CONFIG_SETTING_SIZE); const uint8_t MAX_WIFI_SSID_LENGTH = 32 + 1; const uint8_t MAX_WIFI_PASSWORD_LENGTH = 64 + 1; const uint16_t MAX_HOSTNAME_LENGTH = 255 + 1; const uint8_t MAX_MQTT_CREDS_LENGTH = 32 + 1; - const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = sizeof("shared-broker/username-lolipop/homie/sensors/"); - const uint8_t MAX_MQTT_TOPIC_LENGTH = 128; + const uint8_t MAX_MQTT_BASE_TOPIC_LENGTH = 48 + 1; + const uint8_t MAX_MQTT_TOPIC_LENGTH = 128 + 1; - const uint8_t MAX_FRIENDLY_NAME_LENGTH = sizeof("My awesome friendly name of the living room"); - const uint8_t MAX_DEVICE_ID_LENGTH = sizeof("my-awesome-device-id-living-room"); + const uint8_t MAX_FRIENDLY_NAME_LENGTH = 64 + 1; + const uint8_t MAX_DEVICE_ID_LENGTH = 32 + 1; - const uint8_t MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - sizeof("-0123abcd") + 1; - const uint8_t MAX_FIRMWARE_NAME_LENGTH = sizeof("my-awesome-home-firmware-name"); - const uint8_t MAX_FIRMWARE_VERSION_LENGTH = sizeof("v1.0.0-alpha+001"); + const uint8_t MAX_BRAND_LENGTH = MAX_WIFI_SSID_LENGTH - 10 - 1; + const uint8_t MAX_FIRMWARE_NAME_LENGTH = 32 + 1; + const uint8_t MAX_FIRMWARE_VERSION_LENGTH = 16 + 1; - const uint8_t MAX_NODE_ID_LENGTH = sizeof("my-super-awesome-node-id"); - const uint8_t MAX_NODE_TYPE_LENGTH = sizeof("my-super-awesome-type"); - const uint8_t MAX_NODE_PROPERTY_LENGTH = sizeof("my-super-awesome-property"); + const uint8_t MAX_NODE_ID_LENGTH = 24 + 1; + const uint8_t MAX_NODE_TYPE_LENGTH = 24 + 1; + const uint8_t MAX_NODE_PROPERTY_LENGTH = 24 + 1; - const uint8_t MAX_IP_STRING_LENGTH = sizeof("123.123.123.123"); + const uint8_t MAX_IP_STRING_LENGTH = 16 + 1; const uint8_t MAX_MAC_STRING_LENGTH = 12; } // namespace HomieInternals diff --git a/src/Homie/Utils/Helpers.cpp b/src/Homie/Utils/Helpers.cpp index a15c313d..30ab10b2 100644 --- a/src/Homie/Utils/Helpers.cpp +++ b/src/Homie/Utils/Helpers.cpp @@ -13,9 +13,11 @@ uint8_t Helpers::rssiToPercentage(int32_t rssi) { uint8_t quality; if (rssi <= -100) { quality = 0; - } else if (rssi >= -50) { + } + else if (rssi >= -50) { quality = 100; - } else { + } + else { quality = 2 * (rssi + 100); } @@ -34,6 +36,12 @@ void Helpers::stringToBytes(const char* str, char sep, byte* bytes, int maxBytes } } +bool Helpers::validateIP(const char* ip) +{ + IPAddress test; + return test.fromString(ip); +} + bool Helpers::validateMacAddress(const char* mac) { // taken from http://stackoverflow.com/a/4792211 int i = 0; @@ -41,12 +49,14 @@ bool Helpers::validateMacAddress(const char* mac) { while (*mac) { if (isxdigit(*mac)) { i++; - } else if (*mac == ':' || *mac == '-') { + } + else if (*mac == ':' || *mac == '-') { if (i == 0 || i / 2 - 1 != s) break; ++s; - } else { - s = -1; + } + else { + s = -1; } ++mac; } @@ -73,3 +83,8 @@ std::unique_ptr Helpers::cloneString(const String& string) { return copy; } + +void Helpers::ipToString(const IPAddress& ip, char * str) +{ + snprintf(str, MAX_IP_STRING_LENGTH, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); +} diff --git a/src/Homie/Utils/Helpers.hpp b/src/Homie/Utils/Helpers.hpp index e159db13..3a7f1518 100644 --- a/src/Homie/Utils/Helpers.hpp +++ b/src/Homie/Utils/Helpers.hpp @@ -1,18 +1,21 @@ #pragma once #include "Arduino.h" +#include #include "../../StreamingOperator.hpp" #include "../Limits.hpp" #include namespace HomieInternals { -class Helpers { - public: - static void abort(const String& message); - static uint8_t rssiToPercentage(int32_t rssi); - static void stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); - static bool validateMacAddress(const char* mac); - static bool validateMd5(const char* md5); - static std::unique_ptr cloneString(const String& string); -}; + class Helpers { + public: + static void abort(const String& message); + static uint8_t rssiToPercentage(int32_t rssi); + static void stringToBytes(const char* str, char sep, byte* bytes, int maxBytes, int base); + static bool validateIP(const char* ip); + static bool validateMacAddress(const char* mac); + static bool validateMd5(const char* md5); + static std::unique_ptr cloneString(const String& string); + static void ipToString(const IPAddress& ip, char* str); + }; } // namespace HomieInternals diff --git a/src/Homie/Utils/ResetButton.cpp b/src/Homie/Utils/ResetButton.cpp deleted file mode 100644 index 68cbb1ac..00000000 --- a/src/Homie/Utils/ResetButton.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "ResetButton.hpp" - -using namespace HomieInternals; - - - -ResetButton::ResetButton() - : _flaggedForReset(false) { -} - -void ResetButton::Attach() -{ - if (Interface::get().reset.enabled) { - pinMode(Interface::get().reset.triggerPin, INPUT_PULLUP); - - _resetDebouncer.attach(Interface::get().reset.triggerPin); - _resetDebouncer.interval(Interface::get().reset.triggerTime); - } -} - -void ResetButton::_handleReset() { - if (Interface::get().reset.enabled) { - _resetDebouncer.update(); - - if (_resetDebouncer.read() == Interface::get().reset.triggerState) { - _flaggedForReset = true; - Interface::get().getLogger() << F("Flagged for reset by pin") << endl; - } - } - - if (Interface::get().reset.flaggedBySketch) { - _flaggedForReset = true; - Interface::get().getLogger() << F("Flagged for reset by sketch") << endl; - } -} diff --git a/src/Homie/Utils/ResetButton.hpp b/src/Homie/Utils/ResetButton.hpp deleted file mode 100644 index 88f51119..00000000 --- a/src/Homie/Utils/ResetButton.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "Arduino.h" - -#include -#include "../../StreamingOperator.hpp" -#include "../Datatypes/Interface.hpp" - -namespace HomieInternals { - class ResetButton { - protected: - explicit ResetButton(); - void Attach(); - bool _flaggedForReset; - Bounce _resetDebouncer; - virtual void _handleReset(); - }; -} // namespace HomieInternals diff --git a/src/Homie/Utils/ResetHandler.cpp b/src/Homie/Utils/ResetHandler.cpp new file mode 100644 index 00000000..ad3b2919 --- /dev/null +++ b/src/Homie/Utils/ResetHandler.cpp @@ -0,0 +1,53 @@ +#include "ResetHandler.hpp" + +using namespace HomieInternals; + +Ticker ResetHandler::_resetBTNTicker; +Bounce ResetHandler::_resetBTNDebouncer; +Ticker ResetHandler::_resetTicker; +bool ResetHandler::_sentReset = false; + +void ResetHandler::Attach() +{ + if (Interface::get().reset.enabled) { + pinMode(Interface::get().reset.triggerPin, INPUT_PULLUP); + _resetBTNDebouncer.attach(Interface::get().reset.triggerPin); + _resetBTNDebouncer.interval(Interface::get().reset.triggerTime); + + _resetBTNTicker.attach_ms(10, _tick); + _resetTicker.attach_ms(100, _handleReset); + } +} + +void ResetHandler::_tick() +{ + if (!Interface::get().reset.resetFlag && Interface::get().reset.enabled) { + _resetBTNDebouncer.update(); + if (_resetBTNDebouncer.read() == Interface::get().reset.triggerState) { + Interface::get().getLogger() << F("Flagged for reset by pin") << endl; + Interface::get().disable = true; + Interface::get().reset.resetFlag = true; + } + } +} + +void ResetHandler::_handleReset() { + if (Interface::get().reset.resetFlag && !_sentReset && Interface::get().reset.idle) { + Interface::get().getLogger() << F("Device is idle") << endl; + + Interface::get().getConfig().erase(); + Interface::get().getLogger() << F("Configuration erased") << endl; + + // Set boot mode + Interface::get().getConfig().setHomieBootModeOnNextBoot(HomieBootMode::CONFIGURATION); + + Interface::get().getLogger() << F("Triggering ABOUT_TO_RESET event...") << endl; + Interface::get().event.type = HomieEventType::ABOUT_TO_RESET; + Interface::get().eventHandler(Interface::get().event); + + Interface::get().getLogger() << F("↻ Rebooting into config mode...") << endl; + Serial.flush(); + ESP.restart(); + _sentReset = true; + } +} diff --git a/src/Homie/Utils/ResetHandler.hpp b/src/Homie/Utils/ResetHandler.hpp new file mode 100644 index 00000000..5e4f5445 --- /dev/null +++ b/src/Homie/Utils/ResetHandler.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "Arduino.h" + +#include +#include +#include "../../StreamingOperator.hpp" +#include "../Datatypes/Interface.hpp" + +namespace HomieInternals { + class ResetHandler { + public: + static void Attach(); + + private: + // Disallow creating an instance of this object + ResetHandler() {} + static Ticker _resetBTNTicker; + static Bounce _resetBTNDebouncer; + static void _tick(); + static Ticker _resetTicker; + static bool _sentReset; + static void _handleReset(); + }; +} // namespace HomieInternals diff --git a/src/Homie/Utils/Validation.cpp b/src/Homie/Utils/Validation.cpp index 21315797..c2caa3ec 100644 --- a/src/Homie/Utils/Validation.cpp +++ b/src/Homie/Utils/Validation.cpp @@ -81,8 +81,8 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.bssid is not a string"); return result; } - if ( (object["wifi"].as().containsKey("bssid") && !object["wifi"].as().containsKey("channel")) || - (!object["wifi"].as().containsKey("bssid") && object["wifi"].as().containsKey("channel")) ) { + if ((object["wifi"].as().containsKey("bssid") && !object["wifi"].as().containsKey("channel")) || + (!object["wifi"].as().containsKey("bssid") && object["wifi"].as().containsKey("channel"))) { result.reason = F("wifi.channel_bssid channel and BSSID is required"); return result; } @@ -94,7 +94,6 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.channel is not an integer"); return result; } - IPAddress ipAddress; if (object["wifi"].as().containsKey("ip") && !object["wifi"]["ip"].is()) { result.reason = F("wifi.ip is not a string"); return result; @@ -103,7 +102,7 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.ip is too long"); return result; } - if (object["wifi"]["ip"] && !ipAddress.fromString(object["wifi"].as().get("ip"))) { + if (object["wifi"]["ip"] && !Helpers::validateIP(object["wifi"].as().get("ip"))) { result.reason = F("wifi.ip is not valid ip address"); return result; } @@ -115,7 +114,7 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.mask is too long"); return result; } - if (object["wifi"]["mask"] && !ipAddress.fromString(object["wifi"].as().get("mask"))) { + if (object["wifi"]["mask"] && !Helpers::validateIP(object["wifi"].as().get("mask"))) { result.reason = F("wifi.mask is not valid mask"); return result; } @@ -127,11 +126,11 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.gw is too long"); return result; } - if (object["wifi"]["gw"] && !ipAddress.fromString(object["wifi"].as().get("gw"))) { + if (object["wifi"]["gw"] && !Helpers::validateIP(object["wifi"].as().get("gw"))) { result.reason = F("wifi.gw is not valid gateway address"); return result; } - if ( (object["wifi"].as().containsKey("ip") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("gw"))) || + if ((object["wifi"].as().containsKey("ip") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("gw"))) || (object["wifi"].as().containsKey("gw") && (!object["wifi"].as().containsKey("mask") || !object["wifi"].as().containsKey("ip"))) || (object["wifi"].as().containsKey("mask") && (!object["wifi"].as().containsKey("ip") || !object["wifi"].as().containsKey("gw")))) { result.reason = F("wifi.staticip ip, gw and mask is required"); @@ -145,7 +144,7 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.dns1 is too long"); return result; } - if (object["wifi"]["dns1"] && !ipAddress.fromString(object["wifi"].as().get("dns1"))) { + if (object["wifi"]["dns1"] && !Helpers::validateIP(object["wifi"].as().get("dns1"))) { result.reason = F("wifi.dns1 is not valid dns address"); return result; } @@ -161,7 +160,7 @@ ConfigValidationResult Validation::_validateConfigWifi(const JsonObject& object) result.reason = F("wifi.dns2 is too long"); return result; } - if (object["wifi"]["dns2"] && !ipAddress.fromString(object["wifi"].as().get("dns2"))) { + if (object["wifi"]["dns2"] && !Helpers::validateIP(object["wifi"].as().get("dns2"))) { result.reason = F("wifi.dns2 is not valid dns address"); return result; } @@ -272,77 +271,101 @@ ConfigValidationResult Validation::_validateConfigSettings(const JsonObject& obj settingsObject = &(object["settings"].as()); } + if (settingsObject->size() > MAX_CONFIG_SETTING_SIZE) {//max settings here and in isettings + result.reason = F("settings contains more elements than the set limit"); + return result; + } + for (IHomieSetting* iSetting : IHomieSetting::settings) { + enum class Issue { + Type, + Validator, + Missing + }; + auto setReason = [&result, &iSetting](Issue issue) { + switch (issue) + { + case Issue::Type: + result.reason = String(iSetting->getName()) + F(" setting is not a ") + String(iSetting->getType()); + break; + case Issue::Validator: + result.reason = String(iSetting->getName()) + F(" setting does not pass the validator function"); + break; + case Issue::Missing: + result.reason = String(iSetting->getName()) + F(" setting is missing"); + break; + } + }; + if (iSetting->isBool()) { HomieSetting* setting = static_cast*>(iSetting); if (settingsObject->containsKey(setting->getName())) { if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a boolean")); + setReason(Issue::Type); return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting does not pass the validator function")); + } + else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); return result; } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + } + else if (setting->isRequired()) { + setReason(Issue::Missing); return result; } - } else if (iSetting->isLong()) { + } + else if (iSetting->isLong()) { HomieSetting* setting = static_cast*>(iSetting); if (settingsObject->containsKey(setting->getName())) { if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a long")); + setReason(Issue::Type); return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting does not pass the validator function")); + } + else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); return result; } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + } + else if (setting->isRequired()) { + setReason(Issue::Missing); return result; } - } else if (iSetting->isDouble()) { + } + else if (iSetting->isDouble()) { HomieSetting* setting = static_cast*>(iSetting); if (settingsObject->containsKey(setting->getName())) { if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a double")); + setReason(Issue::Type); return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat((" setting does not pass the validator function")); + } + else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); return result; } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + } + else if (setting->isRequired()) { + setReason(Issue::Missing); return result; } - } else if (iSetting->isConstChar()) { + } + else if (iSetting->isConstChar()) { HomieSetting* setting = static_cast*>(iSetting); if (settingsObject->containsKey(setting->getName())) { if (!(*settingsObject)[setting->getName()].is()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is not a const char*")); + setReason(Issue::Type); return result; - } else if (!setting->validate((*settingsObject)[setting->getName()].as())) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting does not pass the validator function")); + } + else if (!setting->validate((*settingsObject)[setting->getName()].as())) { + setReason(Issue::Validator); return result; } - } else if (setting->isRequired()) { - result.reason = String(setting->getName()); - result.reason.concat(F(" setting is missing")); + } + else if (setting->isRequired()) { + setReason(Issue::Missing); return result; } } diff --git a/src/Homie/Utils/Validation.hpp b/src/Homie/Utils/Validation.hpp index ecb52b92..efa39fc7 100644 --- a/src/Homie/Utils/Validation.hpp +++ b/src/Homie/Utils/Validation.hpp @@ -3,7 +3,6 @@ #include "Arduino.h" #include -#include #include "Helpers.hpp" #include "../Limits.hpp" #include "../../HomieSetting.hpp" diff --git a/src/HomieSetting.cpp b/src/HomieSetting.cpp index eaf807a5..ea03f459 100644 --- a/src/HomieSetting.cpp +++ b/src/HomieSetting.cpp @@ -82,22 +82,22 @@ bool HomieSetting::isConstChar() const { return false; } template<> bool HomieSetting::isBool() const { return true; } template<> -const char* HomieSetting::type() const { return "bool"; } +const char* HomieSetting::getType() const { return "bool"; } template<> bool HomieSetting::isLong() const { return true; } template<> -const char* HomieSetting::type() const { return "long"; } +const char* HomieSetting::getType() const { return "long"; } template<> bool HomieSetting::isDouble() const { return true; } template<> -const char* HomieSetting::type() const { return "double"; } +const char* HomieSetting::getType() const { return "double"; } template<> bool HomieSetting::isConstChar() const { return true; } template<> -const char* HomieSetting::type() const { return "string"; } +const char* HomieSetting::getType() const { return "string"; } // Needed because otherwise undefined reference to template class HomieSetting; diff --git a/src/HomieSetting.hpp b/src/HomieSetting.hpp index 09c40bf5..2b46bd08 100644 --- a/src/HomieSetting.hpp +++ b/src/HomieSetting.hpp @@ -25,7 +25,7 @@ namespace HomieInternals { virtual bool isDouble() const { return false; } virtual bool isConstChar() const { return false; } - virtual const char* type() const { return "unknown"; } + virtual const char* getType() const { return "unknown"; } protected: explicit IHomieSetting(const char* name, const char* description); @@ -63,5 +63,5 @@ class HomieSetting : public HomieInternals::IHomieSetting { bool isDouble() const; bool isConstChar() const; - const char* type() const; + const char* getType() const; };