diff --git a/README.md b/README.md index bfdad17..c900d0c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ HomeGenie mini *(code name **Sbirulino**)* is an **open source library** for building custom firmware for smart devices based on *ESP32* or *ESP8266* chip. +https://homegenie.it/mini ## Features @@ -237,79 +238,6 @@ pio run -e playground-c3 -t upload -## HomeGenie API - -HomeGenie Mini API is a subset of HomeGenie Server API that makes HomeGenie Mini a real -fully working light version of HomeGenie Server specifically designed for microcontrollers. - -### [HomeAutomation.HomeGenie](https://genielabs.github.io/HomeGenie/api/mig/core_api_config.html) - -Implemented subset: - -- [`/api/HomeAutomation.HomeGenie/Logging/RealTime.EventStream/`](https://genielabs.github.io/HomeGenie/api/mig/core_api_logging.html#1) -- [`/api/HomeAutomation.HomeGenie/Config/Modules.Get`](https://genielabs.github.io/HomeGenie/api/mig/core_api_config.html#2) -- [`/api/HomeAutomation.HomeGenie/Config/Modules.List`](https://genielabs.github.io/HomeGenie/api/mig/core_api_config.html#3) -- [`/api/HomeAutomation.HomeGenie/Config/Groups.List`](https://genielabs.github.io/HomeGenie/api/mig/core_api_config.html#4) - -`EXAMPLE Request` -``` -GET /api/HomeAutomation.HomeGenie/Config/Modules.Get/HomeAutomation.HomeGenie/mini -``` - -`Response` -``` -{ - "Name": "HG-Mini", - "Description": "HomeGenie Mini node", - "DeviceType": "Sensor", - "Domain": "HomeAutomation.HomeGenie", - "Address": "mini", - "Properties": [{ - "Name": "Sensor.Luminance", - "Value": "114", - "Description": "", - "FieldType": "", - "UpdateTime": "2019-01-30T13:34:02.293Z" - },{ - "Name": "Sensor.Temperature", - "Value": "18.25", - "Description": "", - "FieldType": "", - "UpdateTime": "2019-01-30T13:34:02.293Z" - }], - "RoutingNode": "" -} -``` - -### HomeGenie Mini builtin API - -It's possible to control the 4 GPIOs on the `P1` expansion port using the following API: - -- `/api/HomeAutomation.HomeGenie//Control.On` -- `/api/HomeAutomation.HomeGenie//Control.Off` -- `/api/HomeAutomation.HomeGenie//Control.Level/` -- `/api/HomeAutomation.HomeGenie//Control.Toggle` - -Where `` can be `D5`, `D6`, `D7` or `D8` and `` a integer between `0` and `100`. - -**Examples** - -``` -# Set output D6 to 50% (1.65V) -/api/HomeAutomation.HomeGenie/D6/Control.Level/50 - -# Set output D5 to 100% (3.3V) -/api/HomeAutomation.HomeGenie/D5/Control.Level/100 -# or -/api/HomeAutomation.HomeGenie/D5/Control.On - -# Set output D8 to 0% (0V) -/api/HomeAutomation.HomeGenie/D8/Control.Level/0 -# or -/api/HomeAutomation.HomeGenie/D8/Control.Off -``` - - --- # Disclaimer diff --git a/src/HomeGenie.cpp b/src/HomeGenie.cpp index fc5910b..eabbe13 100644 --- a/src/HomeGenie.cpp +++ b/src/HomeGenie.cpp @@ -87,9 +87,10 @@ namespace Service { // HomeGenie-Mini Terminal CLI if (Serial.available() > 0) { String cmd = Serial.readStringUntil('\n'); - auto apiCommand = APIRequest::parse(cmd); - // TODO: implement API commands from console input as well - // - see `HomeGenie::api(...)` method + if (!cmd.isEmpty()) { + // TODO: implement SerialCallback + onNetRequest(this, cmd.c_str(), nullptr); + } } // TODO: sort of system load index could be obtained by measuring time elapsed for `TaskManager::loop()` method diff --git a/src/net/MQTTServer.cpp b/src/net/MQTTServer.cpp index bda30a2..f629443 100644 --- a/src/net/MQTTServer.cpp +++ b/src/net/MQTTServer.cpp @@ -31,8 +31,11 @@ namespace Net { - WebSocketsServer *ws; - MQTTBrokerMini *mb; + WebSocketsServer* ws; + MQTTBrokerMini* mb; + + uint8_t* buf = nullptr; + size_t totalLength = 0; void MQTTServer::begin() { @@ -44,10 +47,14 @@ namespace Net { mb = mqttBroker; webSocket->begin(); - webSocket->onEvent(webSocketEventStatic); + webSocket->onEvent([this](uint8_t n, WStype_t t, uint8_t * p, size_t l) { + webSocketEvent(n, t, p, l); + }); mqttBroker->begin(); - mqttBroker->setCallback(mqttCallbackStatic); + mqttBroker->setCallback([this](uint8_t n, const Net::MQTT::Events_t* e, const String* topic_name, uint8_t* payload, uint16_t payload_length) { + mqttCallback(n, e, topic_name, payload, payload_length); + }); } @@ -57,43 +64,113 @@ namespace Net { } } - void MQTTServer::mqttCallbackStatic(uint8_t num, Events_t event, String topic_name, uint8_t *payload, + void MQTTServer::mqttCallback(uint8_t num, const Events_t* event, const String* topic_name, uint8_t *payload, uint16_t length_payload) { - auto msg = String((char*)payload); - switch (event){ - case EVENT_CONNECT: - IO::Logger::trace(":%s [%d] >> CONNECT from '%s'", MQTTBROKER_NS_PREFIX, num, topic_name.c_str()); + switch (*event){ + case EVENT_CONNECT: { + IO::Logger::trace(":%s [%d] >> CONNECT from '%s'", MQTTBROKER_NS_PREFIX, num, (*topic_name).c_str()); break; - case EVENT_SUBSCRIBE: - IO::Logger::trace(":%s [%d] >> SUBSCRIBE to '%s'", MQTTBROKER_NS_PREFIX, num, topic_name.c_str()); + } + case EVENT_SUBSCRIBE: { + IO::Logger::trace(":%s [%d] >> SUBSCRIBE to '%s'", MQTTBROKER_NS_PREFIX, num, (*topic_name).c_str()); break; - case EVENT_PUBLISH: - IO::Logger::trace(":%s [%d] >> PUBLISH to '%s'", MQTTBROKER_NS_PREFIX, num, topic_name.c_str()); - // TODO: ... IMPLEMENT HG API HANDLE TOPIC - if (topic_name == "TODO_CHANGE_WITH_MY_ID/control") { - // TODO: Control API + } + case EVENT_PUBLISH: { + IO::Logger::trace(":%s [%d] >> PUBLISH to '%s'", MQTTBROKER_NS_PREFIX, num, (*topic_name).c_str()); + + auto controlTopic = String ("/") + WiFi.macAddress() + String("/command"); + auto msg = String(payload, length_payload); + if ((*topic_name).endsWith(controlTopic)) { // initial part is the source node id, ending part is the destination node + + JsonDocument doc; + deserializeJson(doc, msg); + if (apiCallback != nullptr && doc.containsKey("Domain") && doc.containsKey("Address") && doc.containsKey("Command")) { + auto domain = String((const char*)doc["Domain"]); + auto address = String((const char*)doc["Address"]); + auto command = String((const char*)doc["Command"]); + apiCallback(num, domain.c_str(), address.c_str(), command.c_str()); + } + doc.clear(); + } else { - // broadcast message to subscribed clients - mb->broadcast(num, (topic_name).c_str(), payload, length_payload); + + // broadcast message to subscribed clients + mb->broadcast((*topic_name).c_str(), payload, length_payload); + } + break; - case EVENT_DISCONNECT: - IO::Logger::trace(":%s [%d] >> DISCONNECT =/", MQTTBROKER_NS_PREFIX, num); + } + case EVENT_DISCONNECT: { + IO::Logger::trace(":%s [%d] >> DISCONNECT =/", MQTTBROKER_NS_PREFIX, num); break; + } } } - void MQTTServer::webSocketEventStatic(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { + void MQTTServer::webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { switch(type) { - case WStype_DISCONNECTED: + case WStype_DISCONNECTED: { if (mb->clientIsConnected(num)) mb->disconnect(num); + } + break; + case WStype_TEXT: { + } + break; + case WStype_FRAGMENT_BIN_START: { + if (buf != nullptr) { + free(buf); + } + buf = (uint8_t*)malloc(sizeof(uint8_t) * length); + if (buf != nullptr) { + memcpy(buf, payload, length); + totalLength = length; + } + } break; - case WStype_BIN: - mb->parsing(num, payload, (uint16_t)length); + case WStype_FRAGMENT: { + if (buf != nullptr) { + uint8_t* old = buf; + buf = (uint8_t*) malloc(sizeof(uint8_t) * (totalLength + length)); + if (buf != nullptr) { + memcpy(buf, old, totalLength); + memcpy(&buf[totalLength], payload, length); + totalLength += length; + } + free(old); + } + } break; + case WStype_FRAGMENT_FIN: { + if (buf != nullptr) { + uint8_t* old = buf; + buf = (uint8_t*) malloc(sizeof(uint8_t) * (totalLength + length)); + if (buf != nullptr) { + memcpy(buf, old, totalLength); + memcpy(&buf[totalLength], payload, length); + totalLength += length; + } + free(old); + } + if (buf != nullptr && totalLength > 0) { + mb->parsing(num, buf, (uint16_t) totalLength); + } + totalLength = 0; + free(buf); + buf = nullptr; + } + break; + case WStype_BIN: { + mb->parsing(num, payload, (uint16_t) length); + } + break; } } + void MQTTServer::broadcast(uint8_t num, String *topic, String *payload) { + mb->broadcast(num, *topic, (uint8_t *)payload->c_str(), (uint16_t)payload->length()); + } + void MQTTServer::broadcast(String *topic, String *payload) { mb->broadcast(*topic, (uint8_t *)payload->c_str(), (uint16_t)payload->length()); } diff --git a/src/net/MQTTServer.h b/src/net/MQTTServer.h index 61585ed..b51f1e8 100644 --- a/src/net/MQTTServer.h +++ b/src/net/MQTTServer.h @@ -38,19 +38,27 @@ namespace Net { using namespace MQTT; -/// Simple MQTT Broker implementation over WebSockets + typedef std::function ApiRequestEvent; + + /// Simple MQTT Broker implementation over WebSockets class MQTTServer : Task { public: void begin(); void loop() override; - void broadcast(String *topic, String *payload); + void broadcast(uint8_t num, String* topic, String* payload); + void broadcast(String* topic, String* payload); + void onRequest(ApiRequestEvent cb) { + apiCallback = cb; + } + + void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length); + void mqttCallback(uint8_t num, const Events_t* event, const String* topic_name, uint8_t* payload, uint16_t length_payload); - static void webSocketEventStatic(uint8_t num, WStype_t type, uint8_t * payload, size_t length); - static void mqttCallbackStatic(uint8_t num, Events_t event, String topic_name, uint8_t * payload, uint16_t length_payload); private: - WebSocketsServer *webSocket = nullptr; - MQTTBrokerMini *mqttBroker = nullptr; + WebSocketsServer* webSocket = nullptr; + MQTTBrokerMini* mqttBroker = nullptr; + ApiRequestEvent apiCallback = nullptr; }; } diff --git a/src/net/NetManager.cpp b/src/net/NetManager.cpp index 1d70e57..00c6799 100644 --- a/src/net/NetManager.cpp +++ b/src/net/NetManager.cpp @@ -73,7 +73,7 @@ namespace Net { } break; case WStype_TEXT: { - // message received + // clear-text message received Serial.printf("[%u] TEXT\t%s\n", num, payload); char message[length + 5]; sprintf(message, "api/%s", payload); @@ -84,12 +84,13 @@ namespace Net { case WStype_ERROR: break; case WStype_BIN: { + // binary-packed message received MsgPack::Unpacker unpacker; std::array req; unpacker.feed(payload, length); unpacker.unpack(req); unpacker.clear(); - + // route message with response callback String rid = req[0]; String request = "api/" + req[1]; Serial.printf("[%u] BIN\t%s\t%s\n", num, rid.c_str(), request.c_str()); @@ -116,6 +117,21 @@ namespace Net { #ifndef DISABLE_MQTT mqttServer = new MQTTServer(); + mqttServer->onRequest([this](uint8_t num, const char* domain, const char* address, const char* command) { + + auto c = String(command); + if (c == "Module.Describe") { + String topic = WiFi.macAddress() + "/" + domain + "/" + address + "/description"; + String apiCommand = "/api/" + String(IOEventDomains::HomeAutomation_HomeGenie) + "/Config/Modules.Get/" + domain + "/" + address; + auto cb = MQTTResponseCallback(mqttServer, num, &topic); + netRequestHandler->onNetRequest(mqttServer, apiCommand.c_str(), &cb); + } else { + String apiCommand = "/api/" + String(domain) + "/" + String(address) + "/" + c; + auto cb = MQTTResponseCallback(mqttServer, 0, nullptr); + netRequestHandler->onNetRequest(mqttServer, apiCommand.c_str(), &cb); + } + + }); mqttServer->begin(); #endif timeClient = new TimeClient(); diff --git a/src/net/NetManager.h b/src/net/NetManager.h index 0a230c2..475a4bf 100644 --- a/src/net/NetManager.h +++ b/src/net/NetManager.h @@ -53,6 +53,8 @@ #include #endif +#include "io/IOEventDomains.h" + #include "TimeClient.h" #define NETMANAGER_LOG_PREFIX "@Net::NetManager" @@ -70,6 +72,31 @@ namespace Net { virtual void error(const char* s) = 0; }; + class MQTTResponseCallback : public ResponseCallback { + public: + MQTTResponseCallback(MQTTServer *server, uint8_t clientId, String* destinationTopic) { + mq = server; + cid = clientId; + topic = destinationTopic; + } + void beginGetLength() override { + buffer = ""; + }; + void endGetLength() override { + mq->broadcast(topic, &buffer); + }; + void write(const char* s) override { + buffer += s; + }; + void writeAll(const char* s) override {}; + void error(const char* s) override {}; + private: + MQTTServer* mq; + uint8_t cid; + String* topic; + String buffer; + }; + // WebSocketResponseCallback class WebSocketResponseCallback : public ResponseCallback { public: diff --git a/src/net/mqtt/MQTTBrokerMini.cpp b/src/net/mqtt/MQTTBrokerMini.cpp index 7db4da8..9cca154 100644 --- a/src/net/mqtt/MQTTBrokerMini.cpp +++ b/src/net/mqtt/MQTTBrokerMini.cpp @@ -49,8 +49,9 @@ namespace Net { namespace MQTT { void MQTTBrokerMini::runCallback(uint8_t num, Events_t event, uint8_t *topic_name, uint16_t length_topic_name, uint8_t *payload, uint16_t length_payload) { if (callback) { - delay(0); - callback(num, event, data_to_string(topic_name, length_topic_name), payload, length_payload); + delay(0); // TODO: <-- not sure what this delay is for + String topic = data_to_string(topic_name, length_topic_name); + callback(num, &event, &topic, payload, length_payload); } } diff --git a/src/net/mqtt/MQTTBrokerMini.h b/src/net/mqtt/MQTTBrokerMini.h index 20cbc78..5345168 100644 --- a/src/net/mqtt/MQTTBrokerMini.h +++ b/src/net/mqtt/MQTTBrokerMini.h @@ -101,8 +101,8 @@ namespace Net { namespace MQTT { EVENT_DISCONNECT, } Events_t; - typedef void(*callback_t)(uint8_t num, Events_t event, String topic_name, uint8_t *payload, - uint16_t length_payload); + typedef std::function callback_t; using namespace IO; diff --git a/src/service/api/APIRequest.h b/src/service/api/APIRequest.h index 97a5f3e..85c55b0 100644 --- a/src/service/api/APIRequest.h +++ b/src/service/api/APIRequest.h @@ -50,6 +50,14 @@ namespace Service { namespace API { static const char Control_Close[] PROGMEM = {"Control.Close"}; } + namespace ConfigApi { + static const char Modules_List[] PROGMEM = {"Modules.List"}; + static const char Modules_Get[] PROGMEM = {"Modules.Get"}; + static const char Modules_ParameterSet[] PROGMEM = {"Modules.ParameterSet"}; + static const char Groups_List[] PROGMEM = {"Groups.List"}; + static const char WebSocket_GetToken[] PROGMEM = {"WebSocket.GetToken"}; + } + class APIRequest { public: String Prefix; diff --git a/src/service/api/HomeGenieHandler.cpp b/src/service/api/HomeGenieHandler.cpp index d66ac3e..e987566 100644 --- a/src/service/api/HomeGenieHandler.cpp +++ b/src/service/api/HomeGenieHandler.cpp @@ -77,19 +77,19 @@ namespace Service { namespace API { bool HomeGenieHandler::handleRequest(Service::APIRequest *request, ResponseCallback* responseCallback) { auto homeGenie = HomeGenie::getInstance(); if (request->Address == F("Config")) { - if (request->Command == F("Modules.List")) { + if (request->Command == ConfigApi::Groups_List) { responseCallback->beginGetLength(); - homeGenie->writeModuleListJSON(responseCallback); + homeGenie->writeGroupListJSON(responseCallback); responseCallback->endGetLength(); - homeGenie->writeModuleListJSON(responseCallback); + homeGenie->writeGroupListJSON(responseCallback); return true; - } else if (request->Command == F("Groups.List")) { + } else if (request->Command == ConfigApi::Modules_List) { responseCallback->beginGetLength(); - homeGenie->writeGroupListJSON(responseCallback); + homeGenie->writeModuleListJSON(responseCallback); responseCallback->endGetLength(); - homeGenie->writeGroupListJSON(responseCallback); + homeGenie->writeModuleListJSON(responseCallback); return true; - } else if (request->Command == F("Modules.Get")) { + } else if (request->Command == ConfigApi::Modules_Get) { String domain = request->getOption(0); String address = request->getOption(1); responseCallback->beginGetLength(); @@ -98,7 +98,7 @@ namespace Service { namespace API { if (contentLength == 0) return false; homeGenie->writeModuleJSON(responseCallback, &domain, &address); return true; - } else if (request->Command == F("Modules.ParameterSet")) { + } else if (request->Command == ConfigApi::Modules_ParameterSet) { String domain = request->getOption(0); String address = request->getOption(1); String propName = request->getOption(2); @@ -115,7 +115,7 @@ namespace Service { namespace API { responseCallback->writeAll(R"({ "ResponseText": "OK" })"); return true; } - } else if (request->Command == F("WebSocket.GetToken")) { + } else if (request->Command == ConfigApi::WebSocket_GetToken) { // TODO: implement random token with expiration (like in HG server) for websocket client verification diff --git a/src/ui/activities/utilities/SystemInfoActivity.cpp b/src/ui/activities/utilities/SystemInfoActivity.cpp index 5298e5d..a5991f2 100644 --- a/src/ui/activities/utilities/SystemInfoActivity.cpp +++ b/src/ui/activities/utilities/SystemInfoActivity.cpp @@ -95,7 +95,7 @@ namespace UI { namespace Activities { namespace Utilities { canvas->setCursor(27, 120); canvas->setTextColor(ActivityColors::ACCENT); canvas->print("WI-FI "); - if (WiFiClass::status() == WL_CONNECTED) { + if (ESP_WIFI_STATUS == WL_CONNECTED) { canvas->setTextColor(ActivityColors::TEXT); canvas->print("CONNECTED"); } else {