#pragma once #include #include #include #include #include #include #include typedef std::function IOTActionHandlerFunction; typedef std::function IOTReportHandlerFunction; struct IOTActionHandler { String topic; IOTActionHandlerFunction function; }; struct IOTReportHandler { String topic; IOTReportHandlerFunction function; bool cache_only; unsigned long update_interval; unsigned long next_update_at; String last_result; }; class SimpleIOT { private: WiFiClient _wifi; PubSubClient _mqtt_client; ESP8266WebServer _http_server; const char* _wifi_ssid; const char* _wifi_pass; String _hostname; std::list _action_handlers; std::list _report_handlers; bool _mqtt_enabled = false; long _mqtt_last_reconnect_attempt = 0; const char* _mqtt_host; uint16_t _mqtt_port; bool _mqtt_auth = false; const char* _mqtt_user; const char* _mqtt_pass; const char* _mqtt_topic; unsigned long _startup_complete_at = 0; bool _initialized = false; void _setup(); void _wifi_setup(); void _ota_setup(); void _ota_loop(); bool _mqtt_connect(); void _mqtt_setup(); void _mqtt_loop(); void _mqtt_callback(char* topic, byte* pl, unsigned int len); void _http_setup(); void _http_loop(); void _mqtt_publish_report(String topic, String report); bool _handle_action_request(String topic, String payload); String _handle_report_request(String topic); bool _topic_in_use(String topic); bool _has_action_handler(String topic); bool _has_report_handler(String topic); void _check_report_handlers(); String _get_report_handler_result(IOTReportHandler* h, bool force_update); public: SimpleIOT(const char* wifi_ssid, const char* wifi_pass, String hostname, bool append_chip_id=false); void setStartupDelay(uint16_t delay); void setMQTTData(const char* mqtt_host, uint16_t mqtt_port, const char* mqtt_user, const char* mqtt_pass, const char* mqtt_topic); void setMQTTData(const char* mqtt_host, uint16_t mqtt_port, const char* mqtt_topic); void begin(); void loop(); bool act_on(String topic, IOTActionHandlerFunction f); bool report_on(String topic, IOTReportHandlerFunction f, unsigned long update_interval, bool use_cache); void log(const char* fmt, ...) __attribute__((format (printf, 2, 3))); void publish(String topic, String payload, bool retain=false); }; /** * Constructs a new instance of SimpleIOT. * * @param wifi_ssid SSID of the WiFi to join. * @param wifi_pass Password of the WiFi to join. * @param hostname The hostname this device will use to identify itself. * It will be used as WiFi client name and for the mDNS stuff * of the OTA daemon. * @param append_chip_id If true, the ESP's chipID will be appended to the hostname. * This can be useful in networks with more than one ESPs with * the given hostname. `test` becomes `test-13D93B0A`. */ SimpleIOT::SimpleIOT(const char* wifi_ssid, const char* wifi_pass, String hostname, bool append_chip_id) { Serial.begin(74880); _wifi_ssid = wifi_ssid; _wifi_pass = wifi_pass; if (append_chip_id) { char* temp = new char[9]; snprintf(temp, 9, "-%08X", ESP.getChipId()); hostname.concat(temp); delete temp; } _hostname = hostname; } /** * Defines a startup delay to give a chance of doing an OTA update. * During development there are situations where a bug in code leads to quickly * rebooting devices. Since this can be quite a nuisance when working with non- * local devices, you can set this to delay the startup a bit to give yourself a * chance of starting an OTA upgrade at the right moment. * Startup will be more or less paused with only OTA being active. This happens * in begin(). * * @param delay Delay in seconds to wait during startup. Set to 0 to disable (default). * * @warning This method has to be called before calling begin(). */ void SimpleIOT::setStartupDelay(uint16_t delay) { if (_initialized) return; if (delay > 0) { _startup_complete_at = millis() + delay * 1000; } else { _startup_complete_at = 0; } } /** * Sets connection data for use with an MQTT server. This also *enables* using * an MQTT server - not calling this method will lead to SimpleIOT working * happily without MQTT. * This is for connecting with authentication. * * @param host Hostname or IP address of an MQTT server. * @param port Port of the MQTT server (usually 1883 for unencrypted connections). * @param user Username for authentication. * @param pass Password for authentication. * @param topic Base topic to publish and subscribe to. Must end with a slash. * * @warning This method has to be called before calling begin(). */ void SimpleIOT::setMQTTData(const char* host, uint16_t port, const char* user, const char* pass, const char* topic) { if (_initialized) return; setMQTTData(host, port, topic); _mqtt_user = user; _mqtt_pass = pass; _mqtt_auth = true; } /** * Sets connection data for use with an MQTT server. This also *enables* using * an MQTT server - not calling this method will lead to SimpleIOT working * happily without MQTT. * This is for connecting anonymously. * * @param host Hostname or IP address of an MQTT server. * @param port Port of the MQTT server (usually 1883 for unencrypted connections). * @param topic Base topic to publish and subscribe to. Must end with a slash. * * @warning This method has to be called before calling begin(). */ void SimpleIOT::setMQTTData(const char* host, uint16_t port, const char* topic) { if (_initialized) return; _mqtt_host = host; _mqtt_port = port; _mqtt_topic = topic; _mqtt_enabled = true; } /** * Starts connecting to services. * 1. A Wifi connection is attempted. If it fails, the ESP is restarted. * 2. OTA is set up. * 3. If setStartupDelay() was used to set a startup delay, this delay will be * spent waiting for a possible upgrade via OTA. * 4. If MQTT was enabled using setMQTTData(), a connection to the server will * be initialized. * 5. A HTTP server will be configured and started on port 80. * * @note You should call this method in your setup() block. */ void SimpleIOT::begin() { log("Core * Setting up Wifi..."); _wifi_setup(); log("Core * Setting up OTA..."); _ota_setup(); while (_startup_complete_at > millis()) { log("Core * Startup delay remaining: %ld ms", _startup_complete_at - millis()); _ota_loop(); delay(100); } if (_mqtt_enabled) { log("Core * Setting up MQTT..."); _mqtt_setup(); } else { log("Core * Not setting up MQTT since setMQTTData wasn't called"); } log("Core * Setting up HTTP..."); _http_setup(); _initialized = true; } /** * Checks for updates. * 1. OTA, MQTT (if enabled) and HTTP are checked for incoming data. * 2. Methods defined using report_on() are checked if an update is in order. * * @note This method should be called from your loop() block. */ void SimpleIOT::loop() { _ota_loop(); if (_mqtt_enabled) { _mqtt_loop(); } _http_loop(); _check_report_handlers(); } void SimpleIOT::_wifi_setup() { WiFi.mode(WIFI_STA); WiFi.hostname(_hostname); WiFi.begin(_wifi_ssid, _wifi_pass); while (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println("WiFi * Connection Failed! Rebooting..."); delay(5000); ESP.restart(); } Serial.println("WiFi * Ready"); Serial.print("WiFi * IP address: "); Serial.println(WiFi.localIP()); } void SimpleIOT::_ota_setup() { ArduinoOTA.onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; } else { type = "filesystem"; } Serial.println("OTA * Start updating " + type); }); ArduinoOTA.onEnd([]() { Serial.println("\nOTA * End"); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("OTA * Progress: %u%%\r", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("OTA * Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); ArduinoOTA.setHostname(_hostname.c_str()); ArduinoOTA.begin(); } void SimpleIOT::_ota_loop() { ArduinoOTA.handle(); } /***** MQTT stuff *****/ bool SimpleIOT::_mqtt_connect() { String topic(_mqtt_topic); topic.concat("status"); bool connection_successful = false; if (_mqtt_auth) { connection_successful = _mqtt_client.connect(_hostname.c_str(), _mqtt_user, _mqtt_pass, topic.c_str(), 0, true, "OFFLINE"); } else { connection_successful = _mqtt_client.connect(_hostname.c_str(), topic.c_str(), 0, true, "OFFLINE"); } if (connection_successful) { char buffer[60]; #ifdef DEBUG snprintf(buffer, 60, "ONLINE %s %s %s", _hostname.c_str(), WiFi.localIP().toString().c_str(), DEBUG); #else snprintf(buffer, 60, "ONLINE %s %s", _hostname.c_str(), WiFi.localIP().toString().c_str()); #endif _mqtt_client.publish(topic.c_str(), buffer, true); topic = String(_mqtt_topic); topic.concat("#"); _mqtt_client.subscribe(topic.c_str()); } else { log("Could not connect to MQTT server. Error was %d", _mqtt_client.state()); } return _mqtt_client.connected(); } void SimpleIOT::_mqtt_setup() { _mqtt_client.setClient(_wifi); _mqtt_client.setServer(_mqtt_host, _mqtt_port); _mqtt_client.setCallback([=](char* t, byte* b, uint l){this->_mqtt_callback(t, b, l);}); _mqtt_last_reconnect_attempt = 0; } void SimpleIOT::_mqtt_loop() { if (!_mqtt_client.connected()) { long now = millis(); if (now - _mqtt_last_reconnect_attempt > 5000) { _mqtt_last_reconnect_attempt = now; if (_mqtt_connect()) { _mqtt_last_reconnect_attempt = 0; } } } else { _mqtt_client.loop(); } } void SimpleIOT::_mqtt_callback(char* top, byte* pl, uint len) { pl[len] = '\0'; String payload((char*)pl); String topic(top); if (topic.startsWith(_mqtt_topic)) { topic.remove(0, strlen(_mqtt_topic)); _handle_action_request(topic, payload); } } void SimpleIOT::_mqtt_publish_report(String topic, String report) { publish(topic, report, true); } /** * Publishes a message via MQTT. * * @param topic The topic to publish to. Will be appended to base_topic. * @param payload The payload to publish. * @param retain Whether to ask the broker to retain the message. */ void SimpleIOT::publish(String topic, String payload, bool retain) { if (!_mqtt_enabled) return; String final_topic = String(_mqtt_topic); final_topic.concat(topic); _mqtt_client.publish(final_topic.c_str(), payload.c_str(), retain); } /***** End of MQTT stuff *****/ /***** HTTP stuff *****/ void SimpleIOT::_http_setup() { _http_server.on("/", HTTP_GET, [=]() { String response(""); response.concat(_hostname); response.concat("

"); response.concat(_hostname); response.concat("

Available report endpoints:

"); for (std::list::iterator it = _report_handlers.begin(); it!=_report_handlers.end(); ++it) { response.concat(""); } response.concat("
EndpointLast value
/"); response.concat(it->topic); response.concat(""); response.concat(it->last_result); response.concat("
"); response.concat("

Available action endpoints:

"); for (std::list::iterator it = _action_handlers.begin(); it!=_action_handlers.end(); ++it) { response.concat("
"); } response.concat("
/"); response.concat(it->topic); response.concat("
"); _http_server.send(200, "text/html", response); }); _http_server.onNotFound([=]() { String uri = _http_server.uri(); uri.remove(0, 1); // Strip leading slash if (_http_server.method() == HTTP_POST) { if (_http_server.args()!=1 || !_has_action_handler(uri)) { _http_server.send(404, "text/plain", "Not found"); return; } if (_handle_action_request(uri, _http_server.arg(0))) { _http_server.send(200, "text/plain", "OK"); return; } } else if (_http_server.method() == HTTP_GET) { if (!_has_report_handler(uri)) { _http_server.send(404, "text/plain", "Not found"); return; } String result = _handle_report_request(uri); if (result.length() > 0) { _http_server.send(200, "text/plain", result); return; } } _http_server.send(404, "text/plain", "Not found"); }); _http_server.begin(80); } void SimpleIOT::_http_loop() { _http_server.handleClient(); } /***** End of HTTP stuff *****/ bool SimpleIOT::_topic_in_use(String topic) { return _has_action_handler(topic) || _has_report_handler(topic); } bool SimpleIOT::_has_action_handler(String topic) { for (std::list::iterator it = _action_handlers.begin(); it!=_action_handlers.end(); ++it) { if (topic.compareTo(it->topic)==0) { return true; } } return false; } bool SimpleIOT::_has_report_handler(String topic) { for (std::list::iterator it = _report_handlers.begin(); it!=_report_handlers.end(); ++it) { if (topic.compareTo(it->topic)==0) { return true; } } return false; } /** * Defines a topic to act on incoming data. * Actions can be anything you want: Switching on a relay, rebooting the ESP, ... * You can trigger this by (up to) two ways: * 1. By sending a POST request to `/` containing the payload, or * 2. (If you enabled MQTT) by publishing your payload to `/`, * with BASE_TOPIC being the topic you set with setMQTTData(). * * @param topic The topic to listen on. May not start or end with a slash and may * also not be `status` or `log`. * @param f A function to call when data for this topic is received. This function * will be given the payload of the request. * * @returns true if the handler function could be added. */ bool SimpleIOT::act_on(String topic, IOTActionHandlerFunction f) { if (topic.startsWith("/") || topic.endsWith("/") || topic.compareTo("status")==0 || topic.compareTo("log")==0) { return false; } if (_topic_in_use(topic)) return false; _action_handlers.push_back({topic, f}); return true; } /** * Defines a topic to report data to. * Data is reported using (up to) two ways: * 1. As answer to a GET request to `/`, or * 2. (If you enabled MQTT) by publishing the data to `/`, * with BASE_TOPIC being the topic you set with setMQTTData(). * * @param topic The topic to listen on. May not start or end with a slash and may * also not be `status` or `log`. * @param f A function to call to receive the current data for this topic. This * function has to return the data as a String object. * @param interval Interval in ms to report this data. If MQTT is enabled, this * method will be called in this interval and its result will be * published. * @param cache_only If set to `true`, requesting the data via a GET request to * the HTTP server will return a cached value, unless at least * `interval` has passed. Use this for functions that take a * long time to process. * * @returns true if the handler function could be added. */ bool SimpleIOT::report_on(String topic, IOTReportHandlerFunction f, unsigned long interval, bool cache_only) { if (topic.startsWith("/") || topic.endsWith("/") || topic.compareTo("status")==0 || topic.compareTo("log")==0) { return false; } if (_topic_in_use(topic)) return false; IOTReportHandler h = {topic, f, cache_only, interval, 0, String()}; _report_handlers.push_back(h); return true; } bool SimpleIOT::_handle_action_request(String topic, String payload) { for (std::list::iterator it = _action_handlers.begin(); it!=_action_handlers.end(); ++it) { if (it->topic.compareTo(topic)==0) { return it->function(payload); } } return false; } String SimpleIOT::_handle_report_request(String topic) { for (std::list::iterator it = _report_handlers.begin(); it!=_report_handlers.end(); ++it) { if (it->topic.compareTo(topic)==0) { return _get_report_handler_result(&(*it), false); } } return String(); } String SimpleIOT::_get_report_handler_result(IOTReportHandler* handler, bool force_update) { if (handler->cache_only && !force_update) { return handler->last_result; } else { String result = handler->function(); handler->last_result = result; return result; } } void SimpleIOT::_check_report_handlers() { unsigned long now = millis(); for (std::list::iterator it = _report_handlers.begin(); it!=_report_handlers.end(); ++it) { bool update_needed = it->next_update_at <= now; bool millis_overflowed = it->next_update_at - it->update_interval > now; if (it->update_interval > 0 && (update_needed || millis_overflowed)) { log("Updating %s...", it->topic.c_str()); String result = _get_report_handler_result(&(*it), true); if (_mqtt_enabled) { _mqtt_publish_report(it->topic, result); } it->next_update_at = now + it->update_interval; } } } /** * Log a message using Serial and (if enabled) MQTT. * Message is processed by sprintf with a maximum length of 128. * If MQTT is enabled, log messages are published at `/log`. * * @param fmt Message or format string in printf syntax. * @param ... Data to insert into the format string. */ void SimpleIOT::log(const char* fmt, ...) { va_list arg; va_start(arg, fmt); char buffer[128]; vsnprintf(buffer, 128, fmt, arg); va_end(arg); String topic(_mqtt_topic); topic.concat("log"); if (_mqtt_enabled && _mqtt_client.connected()) { _mqtt_client.publish(topic.c_str(), buffer); } Serial.println(buffer); } /** * @example 01_actions.ino * * This example shows how to use actions. * Three actions are defined: * * "on" and "off" set Pin 13 to HIGH or LOW, respectively. They are provided as lambda functions * and don't care about the payload they're given. * * "set" is defined as function reference triggering only if the payload is "ON" or "OFF". * * You can access them via HTTP by sending a POST request to `/on`, `/off` or `/set`. There's a * nice-ish website at `/`, listing all registered handlers. If you kept the suggested hostname, * this link should lead you there: http://basic-test/. * * If you enabled MQTT, cou can also publish data via MQTT to trigger those actions. Again, if you * didn't change the topic below, it would be `basic-test-mqtt-topic/on`, `basic-test-mqtt-topic/off` * and `basic-test-mqtt-topic/set`. */ /** * @example 02_reports.ino * * This example shows how to use reports. * Two reports are defined: * * "free_heap" returns the amount of free heap space. The method is provided as lambda function. * Since `interval` ist set to 0, it will not be reported via MQTT, making it accessible only * via HTTP. * * "uptime" returns the current uptime in seconds. It will be reported via MQTT (as long as you * keep MQTT enabled) every 10 seconds. Since `cache_only` is set to true, the method won't be * called any more often than that. * * You can access them via HTTP GET request to `/free_heap` or `/uptime`. There's a * nice-ish website at `/`, listing all registered reports along with (cached) data from them. * If you kept the suggested hostname, this link should lead you there: http://basic-test/. * * If you enabled MQTT, "free_heap" will be published every 10 seconds. Again, if you * didn't change the topic below, it would be `basic-test-mqtt-topic/free_heap`. */