diff --git a/src/MqttPublisher.h b/src/MqttPublisher.h index 26eb6b1..29b7a8d 100644 --- a/src/MqttPublisher.h +++ b/src/MqttPublisher.h @@ -25,6 +25,7 @@ struct MqttConfig char username[128] = ""; char password[128] = ""; char topic[128] = "iot/smartmeter/"; + char jsonPayload[9] = ""; }; class MqttPublisher @@ -50,10 +51,20 @@ class MqttPublisher client.setCredentials(config.username, config.password); } client.setCleanSession(true); - client.setWill(lastWillTopic.c_str(), MQTT_LWT_QOS, MQTT_LWT_RETAIN, MQTT_LWT_PAYLOAD_OFFLINE); + if(config.jsonPayload[0] == 's') + { + if(lastWillJsonPayload != 0){ + delete lastWillJsonPayload; + } + lastWillJsonPayload = new_json_wrap(lastWillTopic.c_str(), MQTT_LWT_PAYLOAD_OFFLINE); + client.setWill(lastWillTopic.c_str(), MQTT_LWT_QOS, MQTT_LWT_RETAIN, lastWillJsonPayload); + } + else + { + client.setWill(lastWillTopic.c_str(), MQTT_LWT_QOS, MQTT_LWT_RETAIN, MQTT_LWT_PAYLOAD_OFFLINE); + } client.setKeepAlive(MQTT_RECONNECT_DELAY * 3); this->registerHandlers(); - } void debug(const char *message) @@ -145,7 +156,11 @@ class MqttPublisher } DEBUG(F("MQTT: Disconnecting from broker...")); client.disconnect(); - this->reconnectTimer.detach(); + } + + bool isConnected() + { + return connected; } private: @@ -155,6 +170,7 @@ class MqttPublisher Ticker reconnectTimer; String baseTopic; String lastWillTopic; + const char* lastWillJsonPayload = 0; void publish(const String &topic, const String &payload, uint8_t qos=0, bool retain=false) { @@ -175,14 +191,48 @@ class MqttPublisher if (this->connected) { DEBUG(F("MQTT: Publishing to %s:"), topic); - DEBUG(F("%s\n"), payload); - client.publish(topic, qos, retain, payload, strlen(payload)); + if(config.jsonPayload[0] == 's') + { + const char* buf = new_json_wrap(topic, payload); + DEBUG(F("%s\n"), buf); + client.publish(topic, qos, retain, buf, strlen(buf)); + delete buf; + } + else + { + DEBUG(F("%s\n"), payload); + client.publish(topic, qos, retain, payload, strlen(payload)); + } } } - void registerHandlers() + const char* new_json_wrap(const char* topic, const char* payload) { + const char* subtopic = topic + baseTopic.length(); + size_t bufsize = strlen(payload) + strlen(subtopic) + 8; + char* buf = new char[bufsize]; + bool payloadIsNumber = true; + for (const char* i = payload; *i != '\0'; i++) + { + if (!isdigit(*i)) + { + payloadIsNumber = false; + break; + } + } + if(payloadIsNumber) + { + sniprintf(buf, bufsize, "{\"%s\":%s}", subtopic, payload); + } + else + { + sniprintf(buf, bufsize, "{\"%s\":\"%s\"}", subtopic, payload); + } + return buf; + } + void registerHandlers() + { client.onConnect([this](bool sessionPresent) { this->connected = true; this->reconnectTimer.detach(); @@ -197,7 +247,7 @@ class MqttPublisher DEBUG(F("MQTT: Disconnected. Reason: %d."), reason); reconnectTimer.attach(MQTT_RECONNECT_DELAY, [this]() { if (WiFi.isConnected()) { - this->connect(); + this->connect(); } }); }); diff --git a/src/Sensor.h b/src/Sensor.h index 151877a..6b9d267 100644 --- a/src/Sensor.h +++ b/src/Sensor.h @@ -37,13 +37,13 @@ uint64_t millis64() class SensorConfig { public: - const uint8_t pin; - const char *name; - const bool numeric_only; - const bool status_led_enabled; - const bool status_led_inverted; - const uint8_t status_led_pin; - const uint8_t interval; + uint8_t pin; + char* name; + bool numeric_only; + bool status_led_enabled; + bool status_led_inverted; + uint8_t status_led_pin; + uint16_t interval; }; class Sensor @@ -73,6 +73,11 @@ class Sensor this->init_state(); } + bool hasProcessedMessage() + { + return processedMessage; + } + void loop() { this->run_current_state(); @@ -95,6 +100,7 @@ class Sensor State state = INIT; void (*callback)(byte *buffer, size_t len, Sensor *sensor) = NULL; unique_ptr status_led; + bool processedMessage; void run_current_state() { @@ -283,6 +289,7 @@ class Sensor // Call listener if (this->callback != NULL) { + this->processedMessage = true; this->callback(this->buffer, this->position, this); } diff --git a/src/config.h b/src/config.h index 4fba9a4..b15653f 100644 --- a/src/config.h +++ b/src/config.h @@ -8,20 +8,9 @@ const char *VERSION = "3.0.0"; // Modifying the config version will probably cause a loss of the existig configuration. // Be careful! -const char *CONFIG_VERSION = "1.0.2"; +const char *CONFIG_VERSION = "2.0.0"; const char *WIFI_AP_SSID = "SMLReader"; const char *WIFI_AP_DEFAULT_PASSWORD = ""; -static const SensorConfig SENSOR_CONFIGS[] = { - {.pin = D2, - .name = "1", - .numeric_only = false, - .status_led_enabled = true, - .status_led_inverted = true, - .status_led_pin = LED_BUILTIN, - .interval = 0}}; - -const uint8_t NUM_OF_SENSORS = sizeof(SENSOR_CONFIGS) / sizeof(SensorConfig); - #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c6d53c6..c7ac4d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,143 +1,126 @@ #include -#include "config.h" -#include "debug.h" #include -#include "Sensor.h" #include +#include "debug.h" #include "MqttPublisher.h" -#include "EEPROM.h" -#include -#include -#include - -std::list *sensors = new std::list(); +#include "config.h" +#include "webconf.h" +#include "Sensor.h" void wifiConnected(); +void status(WebServer*); void configSaved(); -DNSServer dnsServer; -WebServer server(80); -ESP8266HTTPUpdateServer httpUpdater; -WiFiClient net; +WebConf *webConf; MqttConfig mqttConfig; MqttPublisher publisher; -IotWebConf iotWebConf(WIFI_AP_SSID, &dnsServer, &server, WIFI_AP_DEFAULT_PASSWORD, CONFIG_VERSION); +uint8_t numOfSensors; +SensorConfig sensorConfigs[MAX_SENSORS]; +Sensor *sensors[MAX_SENSORS]; + +uint16_t deepSleepInterval; -iotwebconf::TextParameter mqttServerParam = iotwebconf::TextParameter("MQTT server", "mqttServer", mqttConfig.server, sizeof(mqttConfig.server), nullptr, mqttConfig.server); -iotwebconf::NumberParameter mqttPortParam = iotwebconf::NumberParameter("MQTT port", "mqttPort", mqttConfig.port, sizeof(mqttConfig.port), nullptr, mqttConfig.port); -iotwebconf::TextParameter mqttUsernameParam = iotwebconf::TextParameter("MQTT username", "mqttUsername", mqttConfig.username, sizeof(mqttConfig.username), nullptr, mqttConfig.username); -iotwebconf::PasswordParameter mqttPasswordParam = iotwebconf::PasswordParameter("MQTT password", "mqttPassword", mqttConfig.password, sizeof(mqttConfig.password), nullptr, mqttConfig.password); -iotwebconf::TextParameter mqttTopicParam = iotwebconf::TextParameter("MQTT topic", "mqttTopic", mqttConfig.topic, sizeof(mqttConfig.topic), nullptr, mqttConfig.topic); -iotwebconf::ParameterGroup paramGroup = iotwebconf::ParameterGroup("MQTT Settings", ""); +uint64_t lastMessageTime = 0; -boolean needReset = false; +bool connected = false; void process_message(byte *buffer, size_t len, Sensor *sensor) { - // Parse - sml_file *file = sml_file_parse(buffer + 8, len - 16); + lastMessageTime = millis64(); + // Parse + sml_file *file = sml_file_parse(buffer + 8, len - 16); - DEBUG_SML_FILE(file); + DEBUG_SML_FILE(file); - publisher.publish(sensor, file); + publisher.publish(sensor, file); - // free the malloc'd memory - sml_file_free(file); + // free the malloc'd memory + sml_file_free(file); } void setup() { - // Setup debugging stuff - SERIAL_DEBUG_SETUP(115200); + // Setup debugging stuff + SERIAL_DEBUG_SETUP(115200); #ifdef DEBUG - // Delay for getting a serial console attached in time - delay(2000); + // Delay for getting a serial console attached in time + delay(2000); #endif + webConf = new WebConf(&wifiConnected, &status); + + webConf->loadWebconf(mqttConfig, sensorConfigs, numOfSensors, deepSleepInterval); - // Setup reading heads - DEBUG("Setting up %d configured sensors...", NUM_OF_SENSORS); - const SensorConfig *config = SENSOR_CONFIGS; - for (uint8_t i = 0; i < NUM_OF_SENSORS; i++, config++) - { - Sensor *sensor = new Sensor(config, process_message); - sensors->push_back(sensor); - } - DEBUG("Sensor setup done."); - - // Initialize publisher - // Setup WiFi and config stuff - DEBUG("Setting up WiFi and config stuff."); - - paramGroup.addItem(&mqttServerParam); - paramGroup.addItem(&mqttPortParam); - paramGroup.addItem(&mqttUsernameParam); - paramGroup.addItem(&mqttPasswordParam); - paramGroup.addItem(&mqttTopicParam); - - iotWebConf.addParameterGroup(¶mGroup); - - iotWebConf.setConfigSavedCallback(&configSaved); - iotWebConf.setWifiConnectionCallback(&wifiConnected); - - - WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected& event) { - publisher.disconnect(); - }); - - // -- Define how to handle updateServer calls. - iotWebConf.setupUpdateServer( - [](const char *updatePath) - { httpUpdater.setup(&server, updatePath); }, - [](const char *userName, char *password) - { httpUpdater.updateCredentials(userName, password); }); - - boolean validConfig = iotWebConf.init(); - if (!validConfig) - { - DEBUG("Missing or invalid config. MQTT publisher disabled."); - } - else - { - // Setup MQTT publisher - publisher.setup(mqttConfig); - } - - server.on("/", []() { iotWebConf.handleConfig(); }); - server.on("/reset", []() { needReset = true; }); - server.onNotFound([]() { iotWebConf.handleNotFound(); }); - - DEBUG("Setup done."); + // Setup MQTT publisher + publisher.setup(mqttConfig); + + DEBUG("Setting up %d configured sensors...", numOfSensors); + const SensorConfig *config = sensorConfigs; + for (uint8_t i = 0; i < numOfSensors; i++, config++) + { + Sensor *sensor = new Sensor(config, process_message); + sensors[i] = sensor; + } + DEBUG("Sensor setup done."); + + DEBUG("Setup done."); } void loop() -{ - if (needReset) - { - // Doing a chip reset caused by config changes - DEBUG("Rebooting after 1 second."); - delay(1000); - ESP.restart(); - } - - // Execute sensor state machines - for (std::list::iterator it = sensors->begin(); it != sensors->end(); ++it){ - (*it)->loop(); - } - iotWebConf.doLoop(); - yield(); +{ + if (webConf->needReset) + { + // Doing a chip reset caused by config changes + DEBUG("Rebooting after 1 second."); + delay(1000); + ESP.restart(); + } + + bool allSensorsProcessedMessage=true; + // Execute sensor state machines + for (uint8_t i = 0; i < numOfSensors; i++) + { + sensors[i]->loop(); + allSensorsProcessedMessage&=sensors[i]->hasProcessedMessage(); + } + + webConf->doLoop(); + + if(connected && deepSleepInterval > 0 && allSensorsProcessedMessage) + { + if(publisher.isConnected()) + { + DEBUG("Disconnecting MQTT before deep sleep."); + publisher.disconnect(); + } + else + { + DEBUG("Going to deep sleep for %d seconds.", deepSleepInterval); + ESP.deepSleep(deepSleepInterval * 1000000); + } + } + delay(1); } -void configSaved() +void status(WebServer* server) { - DEBUG("Configuration was updated."); - needReset = true; + char buffer[1024], *b = buffer; + b+=sprintf(b, "{\n"); + b+=sprintf(b, " \"chipId\":\"%08X\",\n", ESP.getChipId()); + b+=sprintf(b, " \"uptime64\":%llu,\n", millis64()); + b+=sprintf(b, " \"uptime\":%lu,\n", millis()); + b+=sprintf(b, " \"lastMessageTime\":%llu,\n", lastMessageTime); + b+=sprintf(b, " \"mqttConnected\":%u,\n", publisher.isConnected()); + b+=sprintf(b, " \"version\":\"%s\"\n", VERSION); + b+=sprintf(b, "}"); + server->send(200, "application/json", buffer); } void wifiConnected() { - DEBUG("WiFi connection established."); - publisher.connect(); + DEBUG("WiFi connection established."); + connected = true; + publisher.connect(); } \ No newline at end of file diff --git a/src/webconf.h b/src/webconf.h new file mode 100644 index 0000000..f426877 --- /dev/null +++ b/src/webconf.h @@ -0,0 +1,215 @@ +#ifndef WEBCONF_H +#define WEBCONF_H + +#include +#include +#include +#include "debug.h" +#include "MqttPublisher.h" + +const uint8_t MAX_SENSORS = 9; + +struct GeneralWebConfig +{ + char numberOfSensors[2] = "0"; + char deepSleepInterval[5] = ""; +}; + +struct MqttWebConfig +{ + char server[128] = "mosquitto"; + char port[8] = "1883"; + char username[128] = ""; + char password[128] = ""; + char topic[128] = "iot/smartmeter/"; + char jsonPayload[9] = ""; +}; + +struct SensorWebConfig +{ + char pin[2] = { D2+'A','\0' }; + char name[32] = "sensor0"; + char numeric_only[9] = "selected"; + char status_led_enabled[9] = "selected"; + char status_led_inverted[9] = "selected"; + char status_led_pin[2] = { D4+'A','\0' }; + char interval[5] = "0"; +}; + +struct WebConfGroups +{ + iotwebconf::ParameterGroup *generalGroup; + iotwebconf::ParameterGroup *mqttGroup; + iotwebconf::ParameterGroup *sensorGroups[MAX_SENSORS]; +}; + +struct SensorStrings{ + char grpid[8] = "sensor0"; + char grpname[9] = "Sensor 0"; + char pin[6] = "s0pin"; + char name[7] = "s0name"; + char numOnly[10] = "s0numOnly"; + char ledEnabled[10] = "s0ledE"; + char ledInverted[10] = "s0ledI"; + char ledPin[10] = "s0ledP"; + char interval[9] = "s0int"; +}; + +SensorStrings sensorStrings[MAX_SENSORS]; +const uint8_t NUMBER_OF_PINS=9; +const char pinOptions[] = { D0+'A', '\0', D1+'A', '\0', D2+'A', '\0', D3+'A', '\0', D4+'A', '\0', D5+'A', '\0', D6+'A', '\0', D7+'A', '\0', D8+'A', '\0'}; +const uint8_t PIN_LABEL_LENGTH = 3; +const char *pinNames[] = {"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8"}; + + +class WebConf +{ +private: + GeneralWebConfig general; + MqttWebConfig mqtt; + SensorWebConfig sensors[9]; + WebConfGroups groups; + IotWebConf *iotWebConf; + WebServer *webServer; + DNSServer *dnsServer; + ESP8266HTTPUpdateServer *httpUpdater; + std::function status; + +public: + bool needReset = false; + + WebConf(std::function wifiConnected, std::function status){ + webServer = new WebServer(); + dnsServer = new DNSServer(); + httpUpdater = new ESP8266HTTPUpdateServer(); + iotWebConf = new IotWebConf(WIFI_AP_SSID, dnsServer, webServer, WIFI_AP_DEFAULT_PASSWORD, CONFIG_VERSION); + + this->status = status; + + DEBUG("Setting up WiFi and config stuff."); + + this->setupWebconf(); + + iotWebConf->setConfigSavedCallback([this] { this->configSaved(); } ); + iotWebConf->setWifiConnectionCallback(wifiConnected); + iotWebConf->setupUpdateServer( + [this](const char* updatePath) { this->httpUpdater->setup(this->webServer, updatePath); }, + [this](const char* userName, char* password) { this->httpUpdater->updateCredentials(userName, password); } + ); + + webServer->on("/", [this] { this->iotWebConf->handleConfig(); }); + webServer->on("/status", [this] { this->status(this->webServer); } ); + webServer->on("/reset", [this] { needReset = true; }); + webServer->onNotFound([this] { this->iotWebConf->handleNotFound(); }); + } + + void doLoop(){ + iotWebConf->doLoop(); + } + + void setupWebconf(){ + GeneralWebConfig &generalConfig = this->general; + MqttWebConfig &mqttConfig = this->mqtt; + + using namespace iotwebconf; + iotWebConf->getApTimeoutParameter()->visible = true; + + ParameterGroup* &generalGroup = this->groups.generalGroup = new ParameterGroup("general","General"); + static char numOfSensorsValidator[] = "min='0' max='0'"; + numOfSensorsValidator[13] = MAX_SENSORS + '0'; + generalGroup->addItem(new NumberParameter("Number of sensors", "numOfSensors", generalConfig.numberOfSensors, sizeof(generalConfig.numberOfSensors), generalConfig.numberOfSensors, nullptr, numOfSensorsValidator)); + generalGroup->addItem(new NumberParameter("Deep sleep interval (s)", "deepSleep", generalConfig.deepSleepInterval, sizeof(generalConfig.deepSleepInterval), generalConfig.deepSleepInterval, nullptr, "min='0' max='3600'")); + iotWebConf->addParameterGroup(generalGroup); + + ParameterGroup* &mqttGroup = this->groups.mqttGroup = new ParameterGroup("mqtt","MQTT"); + mqttGroup->addItem(new TextParameter("MQTT server", "mqttServer", mqttConfig.server, sizeof(mqttConfig.server), mqttConfig.server)); + mqttGroup->addItem(new TextParameter("MQTT port", "mqttPort", mqttConfig.port, sizeof(mqttConfig.port), mqttConfig.port)); + mqttGroup->addItem(new TextParameter("MQTT username", "mqttUsername", mqttConfig.username, sizeof(mqttConfig.username), mqttConfig.username)); + mqttGroup->addItem(new PasswordParameter("MQTT password", "mqttPassword", mqttConfig.password, sizeof(mqttConfig.password), mqttConfig.password)); + mqttGroup->addItem(new TextParameter("MQTT topic", "mqttTopic", mqttConfig.topic, sizeof(mqttConfig.topic), mqttConfig.topic)); + mqttGroup->addItem(new CheckboxParameter("MQTT JSON Payload", "mqttJsonPayload", mqttConfig.jsonPayload, sizeof(mqttConfig.jsonPayload), mqttConfig.jsonPayload)); + iotWebConf->addParameterGroup(mqttGroup); + + for(byte i=0; isensors[i]; + + ParameterGroup* &sensorGroup = this->groups.sensorGroups[i] = new ParameterGroup(strs.grpid, strs.grpname); + sensorGroup->visible = false; + sensorGroup->addItem(new SelectParameter("Pin", strs.pin, cfg.pin, sizeof(cfg.pin), pinOptions, *pinNames, NUMBER_OF_PINS, PIN_LABEL_LENGTH, cfg.pin)); + sensorGroup->addItem(new TextParameter("Name", strs.name, cfg.name, sizeof(cfg.name), cfg.name)); + sensorGroup->addItem(new CheckboxParameter("Numeric Values Only", strs.numOnly, cfg.numeric_only, sizeof(cfg.numeric_only), cfg.numeric_only)); + sensorGroup->addItem(new CheckboxParameter("Led Enabled", strs.ledEnabled, cfg.status_led_enabled, sizeof(cfg.status_led_enabled), cfg.status_led_enabled)); + sensorGroup->addItem(new CheckboxParameter("Led Inverted", strs.ledInverted, cfg.status_led_inverted, sizeof(cfg.status_led_inverted), cfg.status_led_inverted)); + sensorGroup->addItem(new SelectParameter("Led Pin", strs.ledPin, cfg.status_led_pin, sizeof(cfg.status_led_pin), pinOptions, *pinNames, NUMBER_OF_PINS, PIN_LABEL_LENGTH, cfg.status_led_pin)); + sensorGroup->addItem(new NumberParameter("Interval (s)", strs.interval, cfg.interval, sizeof(cfg.interval), cfg.interval)); + iotWebConf->addParameterGroup(sensorGroup); + } + } + + void loadWebconf(MqttConfig &mqttConfig, SensorConfig sensorConfigs[MAX_SENSORS], uint8_t &numOfSensors, uint16_t &deepSleepInterval){ + boolean validConfig = iotWebConf->init(); + if (!validConfig) + { + DEBUG("Missing or invalid config. MQTT publisher disabled."); + MqttConfig defaults; + // Resetting to default values + strcpy(mqttConfig.server, defaults.server); + strcpy(mqttConfig.port, defaults.port); + strcpy(mqttConfig.username, defaults.username); + strcpy(mqttConfig.password, defaults.password); + strcpy(mqttConfig.topic, defaults.topic); + + numOfSensors = 0; + deepSleepInterval = 0; + + for (uint8_t i = 0; i < MAX_SENSORS; i++) + { + this->groups.sensorGroups[i]->visible = false; + } + } + else + { + strcpy(mqttConfig.jsonPayload, this->mqtt.jsonPayload); + strcpy(mqttConfig.password, this->mqtt.password); + strcpy(mqttConfig.port, this->mqtt.port); + strcpy(mqttConfig.server, this->mqtt.server); + strcpy(mqttConfig.topic, this->mqtt.topic); + strcpy(mqttConfig.username, this->mqtt.username); + + numOfSensors = this->general.numberOfSensors[0] - '0'; + numOfSensors = numOfSensors < MAX_SENSORS ? numOfSensors : MAX_SENSORS; + for (uint8_t i = 0; i < numOfSensors; i++) + { + this->groups.sensorGroups[i]->visible = i < numOfSensors; + sensorConfigs[i].interval = atoi(this->sensors[i].interval); + sensorConfigs[i].name = this->sensors[i].name; + sensorConfigs[i].numeric_only = this->sensors[i].numeric_only[0] == 's'; + sensorConfigs[i].pin = this->sensors[i].pin[0] - 'A'; + sensorConfigs[i].status_led_enabled = this->sensors[i].status_led_enabled[0] == 's'; + sensorConfigs[i].status_led_inverted = this->sensors[i].status_led_inverted[0] == 's'; + sensorConfigs[i].status_led_pin = this->sensors[i].status_led_pin[0] - 'A'; + } + + deepSleepInterval = atoi(this->general.deepSleepInterval); + } + } + + void configSaved() + { + DEBUG("Configuration was updated."); + needReset = true; + } +}; + +#endif \ No newline at end of file