From c3d0d2007ec7852453833b08612799c091da0c16 Mon Sep 17 00:00:00 2001 From: Fabian Schlenz Date: Fri, 5 Jul 2019 06:19:33 +0200 Subject: [PATCH] Initial commit. Everything is working so far. --- library.json | 20 +++ simple_iot.h | 419 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 library.json create mode 100644 simple_iot.h diff --git a/library.json b/library.json new file mode 100644 index 0000000..937686f --- /dev/null +++ b/library.json @@ -0,0 +1,20 @@ +{ + "name": "SimpleIOT", + "keywords": "iot, mqtt, http", + "description": "Library for simple-ish IOT projects using esp8266.", + "authors": { + "name": "Fabian Schlenz", + "email": "fabian@schle.nz", + "url": "https://blog.fabianonline.de" + }, + "repository": { + "type": "git", + "url": "https://git.schle.nz/fabian/simple-iot" + }, + "version": "0.1.0", + "frameworks": "arduino", + "platforms": "espressif8266", + "dependencies": { + "PubSubClient": "~2.7" + } +} diff --git a/simple_iot.h b/simple_iot.h new file mode 100644 index 0000000..31396cc --- /dev/null +++ b/simple_iot.h @@ -0,0 +1,419 @@ +#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; + const char* _mqtt_user; + const char* _mqtt_pass; + const char* _mqtt_topic; + unsigned long _startup_complete_at = 0; + + 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); + 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 addChipIdToHostname(); + void begin(); + char* hostname; + 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))); +}; + +SimpleIOT::SimpleIOT(const char* wifi_ssid, const char* wifi_pass, String hostname) { + Serial.begin(74880); + _wifi_ssid = wifi_ssid; + _wifi_pass = wifi_pass; + _hostname = hostname; +} + +void SimpleIOT::addChipIdToHostname() { + char* temp = new char[9]; + snprintf(temp, 9, "-%08X", ESP.getChipId()); + String temp2(_hostname); + temp2.concat(temp); + delete temp; + _hostname = temp2.c_str(); +} + +void SimpleIOT::setStartupDelay(uint16_t seconds) { + if (seconds > 0) { + _startup_complete_at = millis() + seconds * 1000; + } else { + _startup_complete_at = 0; + } +} + +void SimpleIOT::setMQTTData(const char* host, uint16_t port, + const char* user, const char* pass, + const char* topic) { + _mqtt_host = host; + _mqtt_port = port; + _mqtt_user = user; + _mqtt_pass = pass; + _mqtt_topic = topic; + _mqtt_enabled = true; +} + +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(); +} + +void SimpleIOT::loop() { + _ota_loop(); + + if (_mqtt_enabled) { + _mqtt_loop(); + } + + _http_loop(); + _check_report_handlers(); +} + +void SimpleIOT::_wifi_setup() { + WiFi.mode(WIFI_STA); + 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"); + if (_mqtt_client.connect(_hostname.c_str(), _mqtt_user, _mqtt_pass, topic.c_str(), 0, true, "OFFLINE", true)) { + 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()); + } + 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) { + String final_topic = String(_mqtt_topic); + final_topic.concat(topic); + _mqtt_client.publish(final_topic.c_str(), report.c_str(), true); +} +/***** 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; +} + + +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; +} + +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; + } + } +} + +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); +}