From 882542318c19633ec3a6a85170d4402f63a46eaf Mon Sep 17 00:00:00 2001 From: Broersma Date: Mon, 13 Nov 2023 22:05:15 +0100 Subject: [PATCH] Update to v0.0.3, allowing all WLED JSON giving much more possibilities --- .../usermod_v2_HttpPullLightControl/readme.md | 110 +++++++ .../usermod_v2_HttpPullLightControl.cpp | 270 ++++++++++++++++++ .../usermod_v2_HttpPullLightControl.h | 92 ++++++ wled00/const.h | 1 + wled00/usermods_list.cpp | 10 +- 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 usermods/usermod_v2_HttpPullLightControl/readme.md create mode 100644 usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp create mode 100644 usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h diff --git a/usermods/usermod_v2_HttpPullLightControl/readme.md b/usermods/usermod_v2_HttpPullLightControl/readme.md new file mode 100644 index 00000000..3faff65a --- /dev/null +++ b/usermods/usermod_v2_HttpPullLightControl/readme.md @@ -0,0 +1,110 @@ +# usermod_v2_HttpPullLightControl + +The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enables remote control over the lighting state and color through HTTP requests. It periodically polls a specified URL to obtain a JSON response containing instructions for controlling individual lights. + +## Features + +* Configure the URL endpoint (only support HTTP for now, no HTTPS) and polling interval via the WLED user interface. +* All options from the JSON API are supported (since v0.0.3). See: https://kno.wled.ge/interfaces/json-api/ +* The ability to control the brightness of all lights and the state (on/off) and color of individual lights remotely. +* Start or stop an effect and when you run the same effect when its's already running, it won't restart. +* The ability to control all these settings per segment. +* Remotely turn on/off relays, change segments or presets. +* Unique ID generation based on the device's MAC address and a configurable salt value, appended to the request URL for identification. + +## Configuration +* Enable the `usermod_v2_HttpPullLightControl` via the WLED user interface. +* Specify the URL endpoint and polling interval. + +## JSON Format and examples +* The module sends a GET request to the configured URL, appending a unique identifier as a query parameter: `https://www.example.com/mycustompage.php?id=xxxxxxxx` where xxxxxxx is a hash of the MAC address combined with a given salt. + +* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: https://kno.wled.ge/interfaces/json-api/ +After getting the URL (it can be a static file like static.json or a mylogic.php which gives a dynamic response), the response is read and parsed to WLED. + +* An example of a response to set the individual lights: 0 to RED, 12 to Green and 14 to BLUE. Remember that is will SET lights, you might want to set all the others to black. +`{ + "seg": + { + "i": [ + 0, "FF0000", + 12, "00FF00", + 14, "0000FF" + ] + } +}` + +* Another example setting the first 10 LEDs to RED, LED 40 to a PURPLE (using RGB values) and all LEDs in between OFF (black color) +`{ + "seg": + { + "i": [ + 0,10, "FF0000", + 10,40, "00FF00", + 40, [0,100,100] + ] + } +}` + +* Or first set all lights to black (off), then the LED5 to color RED: +`{ + "seg": + { + "i": [ + 0,40, "000000", + 5, "FF0000" + ] + } +}` + +* Or use the following example to start an effect, but first we UNFREEZE (frz=false) the segment because it was frozen by individual light control in the previous examples (28=Chase effect, Speed=180m Intensity=128). The three color slots are the slots you see under the color wheel and used by the effect. RED, Black, White in this case. +`{ + "seg": + { + "frz": false, + "fx": 28, + "sx": 200, + "ix": 128, + "col": [ + "FF0000", + "000000", + "FFFFFF" + ] + } +}` + + +## Installation + +1. Add `usermod_v2_HttpPullLightControl` to your WLED project following the instructions provided in the WLED documentation. +2. Compile by setting the build_flag: -D USERMOD_HTTP_PULL_LIGHT_CONTROL and upload to your ESP32/ESP8266! +3. There are several compile options which you can put in your platformio.ini or platformio_override.ini: +- -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod +- -DHTTP_PULL_LIGHT_CONTROL_URL="\"http://mydomain.com/json-response.php\"" ; The URL which will be requested all the time to set the lights/effects +- -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe +- -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds +- -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting + +- -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them +- -DWLED_AP_PASS="\"christmas\"" +- -DWLED_OTA_PASS="\"otapw-secret\"" +- -DMDNS_NAME="\"christmascard\"" +- -DSERVERNAME="\"CHRISTMASCARD\"" +- -D ABL_MILLIAMPS_DEFAULT=450 +- -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs +- -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 +- -D LEDPIN=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 +- -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it +- -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 + +- -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) +- -DDEBUG_LEVEL=5 +- -DWLED_DEBUG + +## Use Case: Interactive Christmas Cards + +Imagine distributing interactive Christmas cards embedded with a tiny ESP32 and a string of 20 LEDs to 20 friends. When a friend powers on their card, it connects to their Wi-Fi network and starts polling your server via the `usermod_v2_HttpPullLightControl`. (Tip: Let them scan a QR code to connect to the WLED WiFi, from there they configure their own WiFi). + +Your server keeps track of how many cards are active at any given time. If all 20 cards are active, your server instructs each card to light up all of its LEDs. However, if only 4 cards are active, your server instructs each card to light up only 4 LEDs. This creates a real-time interactive experience, symbolizing the collective spirit of the holiday season. Each lit LED represents a friend who's thinking about the others, and the visual feedback creates a sense of connection among the group, despite the physical distance. + +This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp new file mode 100644 index 00000000..ac11f56b --- /dev/null +++ b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp @@ -0,0 +1,270 @@ +#include "usermod_v2_HttpPullLightControl.h" + +// add more strings here to reduce flash memory usage +const char HttpPullLightControl::_name[] PROGMEM = "HttpPullLightControl"; +const char HttpPullLightControl::_enabled[] PROGMEM = "Enable"; + +void HttpPullLightControl::setup() { + //Serial.begin(115200); + + // Print version number + DEBUG_PRINT(F("HttpPullLightControl version: ")); + DEBUG_PRINTLN(HTTP_PULL_LIGHT_CONTROL_VERSION); + + // Start a nice chase so we know its booting and searching for its first http pull. + DEBUG_PRINTLN(F("Starting a nice chase so we now it is booting.")); + Segment& seg = strip.getMainSegment(); + seg.setMode(28); // Set to chase + seg.speed = 200; + seg.intensity = 255; + seg.setPalette(128); + seg.setColor(0, 5263440); + seg.setColor(1, 0); + seg.setColor(2, 4605510); + + // Go on with generating a unique ID and splitting the URL into parts + uniqueId = generateUniqueId(); // Cache the unique ID + parseUrl(); + DEBUG_PRINTLN(F("HttpPullLightControl successfully setup")); +} + +// This is the main loop function, from here we check the URL and handle the response. +// Effects or individual lights are set as a result from this. +void HttpPullLightControl::loop() { + if (!enabled || offMode) return; // Do nothing when not enabled or powered off + if (millis() - lastCheck >= checkInterval * 1000) { + DEBUG_PRINTLN(F("Calling checkUrl function")); + checkUrl(); + lastCheck = millis(); + } + +} + +// Generate a unique ID based on the MAC address and a SALT +String HttpPullLightControl::generateUniqueId() { + // We use an easy to implement Fowler–Noll–Vo hash function so we dont need any Sha1.h or Crypto.h dependencies + uint8_t mac[6]; + WiFi.macAddress(mac); + char macStr[18]; + sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + String input = String(macStr) + salt; + unsigned long hashValue = FNV_offset_basis; + for (char c : input) { + hashValue *= FNV_prime; + hashValue ^= c; + } + DEBUG_PRINT(F("Unique ID generated: ")); + DEBUG_PRINTLN(hashValue); + return String(hashValue); +} + +// This function is called when the user updates the Sald and so we need to re-calculate the unique ID +void HttpPullLightControl::updateSalt(String newSalt) { + DEBUG_PRINTLN(F("Salt updated")); + this->salt = newSalt; + uniqueId = generateUniqueId(); +} + +// The function is used to separate the URL in a host part and a path part +void HttpPullLightControl::parseUrl() { + int firstSlash = url.indexOf('/', 7); // Skip http(s):// + host = url.substring(7, firstSlash); + path = url.substring(firstSlash); +} + +// This function is called by WLED when the USERMOD config is read +bool HttpPullLightControl::readFromConfig(JsonObject& root) { + // Attempt to retrieve the nested object for this usermod + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); // check if the object exists + + // Retrieve the values using the getJsonValue function for better error handling + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, enabled); // default value=enabled + configComplete &= getJsonValue(top["checkInterval"], checkInterval, checkInterval); // default value=60 + #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_URL + configComplete &= getJsonValue(top["url"], url, url); // default value="http://example.com" + #endif + #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_SALT + configComplete &= getJsonValue(top["salt"], salt, salt); // default value=your_salt_here + #endif + + return configComplete; +} + +// This function is called by WLED when the USERMOD config is saved in the frontend +void HttpPullLightControl::addToConfig(JsonObject& root) { + // Create a nested object for this usermod + JsonObject top = root.createNestedObject(FPSTR(_name)); + + // Write the configuration parameters to the nested object + top[FPSTR(_enabled)] = enabled; + top["checkInterval"] = checkInterval; + #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_URL + top["url"] = url; + #endif + #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_SALT + top["salt"] = salt; + updateSalt(salt); // Update the UniqueID + #endif + parseUrl(); // Re-parse the URL, maybe path and host is changed +} + +// Do the http request here. Note that we can not do https requests with the AsyncTCP library +// We do everything Asynchronous, so all callbacks are defined here +void HttpPullLightControl::checkUrl() { + if (client != nullptr && client->connected()) { + DEBUG_PRINTLN(F("We are still connected, do nothing")); + // Do nothing, Client is still connected + return; + } + if (client != nullptr) { + // Delete previous client instance if exists, just to prevent any memory leaks + DEBUG_PRINTLN(F("Delete previous instances")); + delete client; + client = nullptr; + } + + DEBUG_PRINTLN(F("Creating new AsyncClient instance.")); + client = new AsyncClient(); + if(client) { + client->onData([](void *arg, AsyncClient *c, void *data, size_t len) { + DEBUG_PRINTLN(F("Data received.")); + // Cast arg back to the usermod class instance + HttpPullLightControl *instance = (HttpPullLightControl *)arg; + // Convertert to Safe-String + char *strData = new char[len + 1]; + strncpy(strData, (char*)data, len); + strData[len] = '\0'; + String responseData = String(strData); + //String responseData = String((char *)data); + // Make sure its zero-terminated String + //responseData[len] = '\0'; + delete[] strData; // Do not forget to remove this one + instance->handleResponse(responseData); + }, this); + client->onDisconnect([](void *arg, AsyncClient *c) { + DEBUG_PRINTLN(F("Disconnected.")); + //Set the class-own client pointer to nullptr if its the current client + HttpPullLightControl *instance = static_cast(arg); + if (instance->client == c) { + instance->client = nullptr; + } + // Do not remove client here, it is maintained by AsyncClient + }, this); + client->onTimeout([](void *arg, AsyncClient *c, uint32_t time) { + DEBUG_PRINTLN(F("Timeout")); + //Set the class-own client pointer to nullptr if its the current client + HttpPullLightControl *instance = static_cast(arg); + if (instance->client == c) { + delete instance->client; + instance->client = nullptr; + } + // Do not remove client here, it is maintained by AsyncClient + }, this); + client->onError([](void *arg, AsyncClient *c, int8_t error) { + DEBUG_PRINTLN("Connection error occurred!"); + DEBUG_PRINT("Error code: "); + DEBUG_PRINTLN(error); + //Set the class-own client pointer to nullptr if its the current client + HttpPullLightControl *instance = static_cast(arg); + if (instance->client == c) { + delete instance->client; + instance->client = nullptr; + } + // Do not remove client here, it is maintained by AsyncClient + }, this); + client->onConnect([](void *arg, AsyncClient *c) { + // Cast arg back to the usermod class instance + HttpPullLightControl *instance = (HttpPullLightControl *)arg; + instance->onClientConnect(c); // Call a method on the instance when the client connects + }, this); + client->setAckTimeout(ackTimeout); // Just some safety measures because we do not want any memory fillup + client->setRxTimeout(rxTimeout); + DEBUG_PRINT(F("Connecting to: ")); + DEBUG_PRINT(host); + DEBUG_PRINT(F(" via port ")); + DEBUG_PRINTLN((url.startsWith("https")) ? 443 : 80); + //Try to connect + if (!client->connect(host.c_str(), (url.startsWith("https")) ? 443 : 80)) { + DEBUG_PRINTLN(F("Failed to initiate connection.")); + // Connection failed, so cleanup + delete client; + client = nullptr; + } else { + // Connection successfull, wait for callbacks to go on. + DEBUG_PRINTLN(F("Connection initiated, awaiting response...")); + } + } else { + DEBUG_PRINTLN(F("Failed to create AsyncClient instance.")); + } +} + +// This function is called from the checkUrl function when the connection is establised +// We request the data here +void HttpPullLightControl::onClientConnect(AsyncClient *c) { + DEBUG_PRINT(F("Client connected: ")); + DEBUG_PRINTLN(c->connected() ? F("Yes") : F("No")); + + if (c->connected()) { + String request = "GET " + path + (path.indexOf('?') > 0 ? "&id=" : "?id=") + uniqueId + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n" + "Accept: application/json\r\n" + "Accept-Encoding: identity\r\n" // No compression + "User-Agent: ESP32 HTTP Client\r\n\r\n"; // Optional: User-Agent and end with a double rnrn ! + DEBUG_PRINT(request.c_str()); + auto bytesSent = c->write(request.c_str()); + if (bytesSent == 0) { + // Connection could not be made + DEBUG_PRINT(F("Failed to send HTTP request.")); + } else { + DEBUG_PRINT(F("Request sent successfully, bytes sent: ")); + DEBUG_PRINTLN(bytesSent ); + } + } +} + + +// This function is called when we receive data after connecting and doing our request +// It parses the JSON data to WLED +void HttpPullLightControl::handleResponse(String& responseStr) { + DEBUG_PRINTLN(F("Received response for handleResponse.")); + + // Get a Bufferlock, we can not use doc + if (!requestJSONBufferLock(myLockId)) { + DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: ")); + DEBUG_PRINTLN(myLockId); + return; + } + + // Search for two linebreaks between headers and content + int bodyPos = responseStr.indexOf("\r\n\r\n"); + if (bodyPos > 0) { + String jsonStr = responseStr.substring(bodyPos + 4); // +4 Skip the two CRLFs + jsonStr.trim(); + + DEBUG_PRINTLN("Response: "); + DEBUG_PRINTLN(jsonStr); + + // Attempt to deserialize the JSON response + DeserializationError error = deserializeJson(doc, jsonStr); + if (error) { + // If there is an error in deserialization, exit the function + DEBUG_PRINT(F("DeserializationError: ")); + DEBUG_PRINTLN(error.c_str()); + return; + } + } else { + DEBUG_PRINTLN(F("No body found in the response")); + return; + } + + // Get JSON object from th doc + JsonObject obj = doc.as(); + + // Parse the object throuhg deserializeState (use CALL_MODE_NO_NOTIFY or OR CALL_MODE_DIRECT_CHANGE) + deserializeState(obj, CALL_MODE_NO_NOTIFY); + + // Release the BufferLock again + releaseJSONBufferLock(); +} \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h new file mode 100644 index 00000000..ae93a193 --- /dev/null +++ b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h @@ -0,0 +1,92 @@ +#pragma once +/* + * Usermod: HttpPullLightControl + * Versie: 0.0.3 + * Repository: https://github.com/roelbroersma/WLED-usermodv2_HttpPullLightControl + * Author: Roel Broersma + * Website: https://www.roelbroersma.nl + * Github author: github.com/roelbroersma + * Description: This usermod for WLED will request a given URL to know which effects + * or individual lights it should turn on/off. So you can remote control a WLED + * installation without having access to it (if no port forward, vpn or public IP is available). + * Use Case: Create a WLED 'Ring of Thought' christmas card. Sent a LED ring with 60 LEDs to 60 friends. + * When they turn it on and put it at their WiFi, it will contact your server. Now you can reply with a given + * number of lights that should turn on. Each light is a friend who did contact your server in the past 5 minutes. + * So on each of your friends LED rings, the number of lights will be the number of friends who have it turned on. + * Features: It sends a unique ID (has of MAC and salt) to the URL, so you can define each client without a need to map their IP address. + * Tested: Tested on WLED v0.14 with ESP32-S3 (M5Stack Atom S3 Lite), but should also workd for other ESPs and ESP8266. + */ + +#include "wled.h" + +#define HTTP_PULL_LIGHT_CONTROL_VERSION "0.0.3" + +class HttpPullLightControl : public Usermod { +private: + static const char _name[]; + static const char _enabled[]; + static const char _salt[]; + static const char _url[]; + + bool enabled = true; + + #ifdef HTTP_PULL_LIGHT_CONTROL_INTERVAL + uint16_t checkInterval = HTTP_PULL_LIGHT_CONTROL_INTERVAL; + #else + uint16_t checkInterval = 60; // Default interval of 1 minute + #endif + + #ifdef HTTP_PULL_LIGHT_CONTROL_URL + String url = HTTP_PULL_LIGHT_CONTROL_URL; + #else + String url = "http://example.org/example.php"; // Default-URL (http only!), can also be url with IP address in it. HttpS urls are not supported (yet) because of AsyncTCP library + #endif + + #ifdef HTTP_PULL_LIGHT_CONTROL_SALT + String salt = HTTP_PULL_LIGHT_CONTROL_SALT; + #else + String salt = "1just_a_very-secret_salt2"; // Salt for generating a unique ID when requesting the URL (in this way you can give different answers based on the WLED device who does the request) + #endif + // NOTE THAT THERE IS ALSO A #ifdef HTTP_PULL_LIGHT_CONTROL_HIDE_URL and a HTTP_PULL_LIGHT_CONTROL_HIDE_SALT IF YOU DO NOT WANT TO SHOW THE OPTIONS IN THE USERMOD SETTINGS + + // Define constants + static const uint8_t myLockId = USERMOD_ID_HTTP_PULL_LIGHT_CONTROL ; // Used for the requestJSONBufferLock(id) function + static const int16_t ackTimeout = 10000; // ACK timeout in milliseconds when doing the URL request + static const uint16_t rxTimeout = 10000; // RX timeout in milliseconds when doing the URL request + static const unsigned long FNV_offset_basis = 2166136261; + static const unsigned long FNV_prime = 16777619; + + unsigned long lastCheck = 0; // Timestamp of last check + String host; // Host extracted from the URL + String path; // Path extracted from the URL + String uniqueId; // Cached unique ID + AsyncClient *client = nullptr; // Used very often, beware of closing and freeing + String generateUniqueId(); + + void parseUrl(); + void updateSalt(String newSalt); // Update the salt value and recalculate the unique ID + void checkUrl(); // Check the specified URL for light control instructions + void handleResponse(String& response); + void onClientConnect(AsyncClient *c); + +public: + void setup(); + void loop(); + bool readFromConfig(JsonObject& root); + void addToConfig(JsonObject& root); + uint16_t getId() { return USERMOD_ID_HTTP_PULL_LIGHT_CONTROL; } + inline void enable(bool enable) { enabled = enable; } // Enable or Disable the usermod + inline bool isEnabled() { return enabled; } // Get usermod enabled or disabled state + virtual ~HttpPullLightControl() { + // Remove the cached client if needed + if (client) { + client->onDisconnect(nullptr); + client->onError(nullptr); + client->onTimeout(nullptr); + client->onData(nullptr); + client->onConnect(nullptr); + // Now it is safe to delete the client. + delete client; // This is safe even if client is nullptr. + } + } +}; \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index 6ee83451..e4be6517 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -150,6 +150,7 @@ #define USERMOD_ID_KLIPPER 40 //Usermod Klipper percentage #define USERMOD_ID_WIREGUARD 41 //Usermod "wireguard.h" #define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h" +#define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 43 //usermod "usermod_v2_HttpPullLightControl.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 6184571a..eac68ea2 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -194,7 +194,11 @@ #endif #ifdef USERMOD_PWM_OUTPUTS -#include "../usermods/pwm_outputs/usermod_pwm_outputs.h" + #include "../usermods/pwm_outputs/usermod_pwm_outputs.h" +#endif + +#ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL + #include "../usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h" #endif @@ -373,4 +377,8 @@ void registerUsermods() #ifdef USERMOD_INTERNAL_TEMPERATURE usermods.add(new InternalTemperatureUsermod()); #endif + + #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL + usermods.add(new HttpPullLightControl()); + #endif }