Initial commit. Everything is working so far.
This commit is contained in:
commit
c3d0d2007e
20
library.json
Normal file
20
library.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
419
simple_iot.h
Normal file
419
simple_iot.h
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266mDNS.h>
|
||||||
|
#include <ESP8266WebServer.h>
|
||||||
|
#include <ArduinoOTA.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
typedef std::function<bool(String)> IOTActionHandlerFunction;
|
||||||
|
typedef std::function<String()> 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<IOTActionHandler> _action_handlers;
|
||||||
|
std::list<IOTReportHandler> _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("<html><head><title>");
|
||||||
|
response.concat(_hostname);
|
||||||
|
response.concat("</title></head><body><h1>");
|
||||||
|
response.concat(_hostname);
|
||||||
|
response.concat("<h2>Available report endpoints:</h2><table><tr><th>Endpoint</th><th>Last value</th></tr>");
|
||||||
|
for (std::list<IOTReportHandler>::iterator it = _report_handlers.begin(); it!=_report_handlers.end(); ++it) {
|
||||||
|
response.concat("<tr><td><a href='/");
|
||||||
|
response.concat(it->topic);
|
||||||
|
response.concat("'>/");
|
||||||
|
response.concat(it->topic);
|
||||||
|
response.concat("</a></td><td>");
|
||||||
|
response.concat(it->last_result);
|
||||||
|
response.concat("</td></tr>");
|
||||||
|
}
|
||||||
|
response.concat("</table>");
|
||||||
|
|
||||||
|
response.concat("<h2>Available action endpoints:</h2><table>");
|
||||||
|
for (std::list<IOTActionHandler>::iterator it = _action_handlers.begin(); it!=_action_handlers.end(); ++it) {
|
||||||
|
response.concat("<form method='post' action='/");
|
||||||
|
response.concat(it->topic);
|
||||||
|
response.concat("'><tr><td>/");
|
||||||
|
response.concat(it->topic);
|
||||||
|
response.concat("</td><td><input type='text' placeholder='command' name='command' /><input type='submit' value='Send' /></td></tr></form>");
|
||||||
|
}
|
||||||
|
response.concat("</table></body></html>");
|
||||||
|
_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<IOTActionHandler>::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<IOTReportHandler>::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<IOTActionHandler>::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<IOTReportHandler>::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<IOTReportHandler>::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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user