diff --git a/pio/gzip-firmware.py b/pio-scripts/gzip-firmware.py similarity index 100% rename from pio/gzip-firmware.py rename to pio-scripts/gzip-firmware.py diff --git a/pio/name-firmware.py b/pio-scripts/name-firmware.py similarity index 100% rename from pio/name-firmware.py rename to pio-scripts/name-firmware.py diff --git a/pio/obj-dump.py b/pio-scripts/obj-dump.py similarity index 100% rename from pio/obj-dump.py rename to pio-scripts/obj-dump.py diff --git a/pio/strip-floats.py b/pio-scripts/strip-floats.py similarity index 100% rename from pio/strip-floats.py rename to pio-scripts/strip-floats.py diff --git a/pio/user_config_copy.py b/pio-scripts/user_config_copy.py similarity index 100% rename from pio/user_config_copy.py rename to pio-scripts/user_config_copy.py diff --git a/platformio.ini b/platformio.ini index a6159ac5..6532f467 100644 --- a/platformio.ini +++ b/platformio.ini @@ -164,10 +164,10 @@ build_flags = -w -g -DCONFIG_LITTLEFS_FOR_IDF_3_2 [scripts_defaults] -extra_scripts = pio/name-firmware.py - pio/gzip-firmware.py - pio/strip-floats.py - pio/user_config_copy.py +extra_scripts = pio-scripts/name-firmware.py + pio-scripts/gzip-firmware.py + pio-scripts/strip-floats.py + pio-scripts/user_config_copy.py # ------------------------------------------------------------------------------ # COMMON SETTINGS: diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h new file mode 100644 index 00000000..9717589d --- /dev/null +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -0,0 +1,427 @@ +/* + * Usermod for detecting people entering/leaving a staircase and switching the + * staircase on/off. + * + * Edit the Animated_Staircase_config.h file to compile this usermod for your + * specific configuration. + * + * See the accompanying README.md file for more info. + */ +#pragma once +#include "wled.h" +#include "Animated_Staircase_config.h" +#define USERMOD_ID_ANIMATED_STAIRCASE 1011 + +/* Initial configuration (available in API and stored in flash) */ +bool enabled = true; // Enable this usermod +unsigned long segment_delay_ms = 150; // Time between switching each segment +unsigned long on_time_ms = 5 * 1000; // The time for the light to stay on +#ifndef TOP_PIR_PIN +unsigned int topMaxTimeUs = 1749; // default echo timout, top +#endif +#ifndef BOTTOM_PIR_PIN +unsigned int bottomMaxTimeUs = 1749; // default echo timout, bottom +#endif + +// Time between checking of the sensors +const int scanDelay = 50; + +class Animated_Staircase : public Usermod { + private: + // Lights on or off. + // Flipping this will start a transition. + bool on = false; + + // Swipe direction for current transition +#define SWIPE_UP true +#define SWIPE_DOWN false + bool swipe = SWIPE_UP; + + // Indicates which Sensor was seen last (to determine + // the direction when swiping off) +#define LOWER false +#define UPPER true + bool lastSensor = LOWER; + + // Time of the last transition action + unsigned long lastTime = 0; + + // Time of the last sensor check + unsigned long lastScanTime = 0; + + // Last time the lights were switched on or off + unsigned long lastSwitchTime = 0; + + // segment id between onIndex and offIndex are on. + // controll the swipe by setting/moving these indices around. + // onIndex must be less than or equal to offIndex + byte onIndex = 0; + byte offIndex = 0; + + // The maximum number of configured segments. + // Dynamically updated based on user configuration. + byte maxSegmentId = 1; + byte mainSegmentId = 0; + + bool saveState = false; + + // These values are used by the API to read the + // last sensor state, or trigger a sensor + // through the API + bool topSensorRead = false; + bool topSensorWrite = false; + bool bottomSensorRead = false; + bool bottomSensorWrite = false; + + void updateSegments() { + mainSegmentId = strip.getMainSegmentId(); + WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); + WS2812FX::Segment* segments = strip.getSegments(); + for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { + if (!segments->isActive()) { + maxSegmentId = i - 1; + break; + } + + if (i >= onIndex && i < offIndex) { + segments->setOption(SEG_OPTION_ON, 1, 1); + + // We may need to copy mode and colors from segment 0 to make sure + // changes are propagated even when the config is changed during a wipe + // segments->mode = mainsegment.mode; + // segments->colors[0] = mainsegment.colors[0]; + } else { + segments->setOption(SEG_OPTION_ON, 0, 1); + } + // Always mark segments as "transitional", we are animating the staircase + segments->setOption(SEG_OPTION_TRANSITIONAL, 1, 1); + } + colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + } + + /* + * Detects if an object is within ultrasound range. + * signalPin: The pin where the pulse is sent + * echoPin: The pin where the echo is received + * maxTimeUs: Detection timeout in microseconds. If an echo is + * received within this time, an object is detected + * and the function will return true. + * + * The speed of sound is 343 meters per second at 20 degress Celcius. + * Since the sound has to travel back and forth, the detection + * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. + * + * For practical reasons, here are some useful distances: + * + * Distance = maxtime + * 5 cm = 292 uS + * 10 cm = 583 uS + * 20 cm = 1166 uS + * 30 cm = 1749 uS + * 50 cm = 2915 uS + * 100 cm = 5831 uS + */ + bool ultrasoundRead(uint8_t signalPin, + uint8_t echoPin, + unsigned int maxTimeUs) { + digitalWrite(signalPin, HIGH); + delayMicroseconds(10); + digitalWrite(signalPin, LOW); + return pulseIn(echoPin, HIGH, maxTimeUs) > 0; + } + + void checkSensors() { + if ((millis() - lastScanTime) > scanDelay) { + lastScanTime = millis(); + +#ifdef BOTTOM_PIR_PIN + bottomSensorRead = bottomSensorWrite || (digitalRead(BOTTOM_PIR_PIN) == HIGH); +#else + bottomSensorRead = bottomSensorWrite || ultrasoundRead(BOTTOM_TRIGGER_PIN, BOTTOM_ECHO_PIN, bottomMaxTimeUs); +#endif + +#ifdef TOP_PIR_PIN + topSensorRead = topSensorWrite || (digitalRead(TOP_PIR_PIN) == HIGH); +#else + topSensorRead = topSensorWrite || ultrasoundRead(TOP_TRIGGER_PIN, TOP_ECHO_PIN, topMaxTimeUs); +#endif + + // Values read, reset the flags for next API call + topSensorWrite = false; + bottomSensorWrite = false; + + if (topSensorRead != bottomSensorRead) { + lastSwitchTime = millis(); + + if (on) { + lastSensor = topSensorRead; + } else { + // If the bottom sensor triggered, we need to swipe up, ON + swipe = bottomSensorRead; + + if (swipe) { + Serial.println("ON -> Swipe up."); + } else { + Serial.println("ON -> Swipe down."); + } + + if (onIndex == offIndex) { + // Position the indices for a correct on-swipe + if (swipe == SWIPE_UP) { + onIndex = mainSegmentId; + } else { + onIndex = maxSegmentId+1; + } + offIndex = onIndex; + } + on = true; + } + } + } + } + + void autoPowerOff() { + if (on && ((millis() - lastSwitchTime) > on_time_ms)) { + // Swipe OFF in the direction of the last sensor detection + swipe = lastSensor; + on = false; + + if (swipe) { + Serial.println("OFF -> Swipe up."); + } else { + Serial.println("OFF -> Swipe down."); + } + } + } + + void updateSwipe() { + if ((millis() - lastTime) > segment_delay_ms) { + lastTime = millis(); + + byte oldOnIndex = onIndex; + byte oldOffIndex = offIndex; + + if (on) { + // Turn on all segments + onIndex = MAX(mainSegmentId, onIndex - 1); + offIndex = MIN(maxSegmentId + 1, offIndex + 1); + } else { + if (swipe == SWIPE_UP) { + onIndex = MIN(offIndex, onIndex + 1); + } else { + offIndex = MAX(onIndex, offIndex - 1); + } + } + + updateSegments(); + } + } + + void writeSettingsToJson(JsonObject& root) { + JsonObject staircase = root["staircase"]; + if (staircase.isNull()) { + staircase = root.createNestedObject("staircase"); + } + staircase["enabled"] = enabled; + staircase["segment-delay-ms"] = segment_delay_ms; + staircase["on-time-s"] = on_time_ms / 1000; + +#ifdef TOP_TRIGGER_PIN + staircase["top-echo-us"] = topMaxTimeUs; +#endif +#ifdef BOTTOM_TRIGGER_PIN + staircase["bottom-echo-us"] = bottomMaxTimeUs; +#endif + } + + void writeSensorsToJson(JsonObject& root) { + JsonObject staircase = root["staircase"]; + if (staircase.isNull()) { + staircase = root.createNestedObject("staircase"); + } + staircase["top-sensor"] = topSensorRead; + staircase["bottom-sensor"] = bottomSensorRead; + } + + bool readSettingsFromJson(JsonObject& root) { + JsonObject staircase = root["staircase"]; + bool changed = false; + + bool shouldEnable = staircase["enabled"] | enabled; + if (shouldEnable != enabled) { + enable(shouldEnable); + changed = true; + } + + unsigned long c_segment_delay_ms = staircase["segment-delay-ms"] | segment_delay_ms; + if (c_segment_delay_ms != segment_delay_ms) { + segment_delay_ms = c_segment_delay_ms; + changed = true; + } + + unsigned long c_on_time_ms = (staircase["on-time-s"] | (on_time_ms / 1000)) * 1000; + if (c_on_time_ms != on_time_ms) { + on_time_ms = c_on_time_ms; + changed = true; + } + +#ifdef TOP_TRIGGER_PIN + unsigned int c_topMaxTimeUs = staircase["top-echo-us"] | topMaxTimeUs; + if (c_topMaxTimeUs != topMaxTimeUs) { + topMaxTimeUs = c_topMaxTimeUs; + changed = true; + } +#endif +#ifdef BOTTOM_TRIGGER_PIN + unsigned int c_bottomMaxTimeUs = staircase["bottom-echo-us"] | bottomMaxTimeUs; + if (c_bottomMaxTimeUs != bottomMaxTimeUs) { + bottomMaxTimeUs = c_bottomMaxTimeUs; + changed = true; + } +#endif + + return changed; + } + + void readSensorsFromJson(JsonObject& root) { + JsonObject staircase = root["staircase"]; + bottomSensorWrite = bottomSensorRead || (staircase["bottom-sensor"].as()); + topSensorWrite = topSensorRead || (staircase["top-sensor"].as()); + } + + void enable(bool enable) { + if (enable) { + Serial.println("Animated Staircase enabled."); + Serial.print("Delay between steps: "); + Serial.print(segment_delay_ms, DEC); + Serial.print(" milliseconds.\nStairs switch off after: "); + Serial.print(on_time_ms / 1000, DEC); + Serial.println(" seconds."); + +#ifdef BOTTOM_PIR_PIN + pinMode(BOTTOM_PIR_PIN, INPUT); +#else + pinMode(BOTTOM_TRIGGER_PIN, OUTPUT); + pinMode(BOTTOM_ECHO_PIN, INPUT); +#endif + +#ifdef TOP_PIR_PIN + pinMode(TOP_PIR_PIN, INPUT); +#else + pinMode(TOP_TRIGGER_PIN, OUTPUT); + pinMode(TOP_ECHO_PIN, INPUT); +#endif + } else { + // Restore segment options + WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); + WS2812FX::Segment* segments = strip.getSegments(); + for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { + if (!segments->isActive()) { + maxSegmentId = i - 1; + break; + } + segments->setOption(SEG_OPTION_ON, 1, 1); + } + colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + Serial.println("Animated Staircase disabled."); + } + enabled = enable; + } + + public: + void setup() { enable(enabled); } + + void loop() { + // Write changed settings from to flash (see readFromJsonState()) + if (saveState) { + serializeConfig(); + saveState = false; + } + + if (!enabled) { + return; + } + + checkSensors(); + autoPowerOff(); + updateSwipe(); + + } + + uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } + + /* + * Shows configuration settings to the json API. This object looks like: + * + * "staircase" : { + * "enabled" : true + * "segment-delay-ms" : 150, + * "on-time-s" : 5 + * } + * + */ + void addToJsonState(JsonObject& root) { + writeSettingsToJson(root); + writeSensorsToJson(root); + Serial.println("Staircase config exposed in API."); + } + + /* + * Reads configuration settings from the json API. + * See void addToJsonState(JsonObject& root) + */ + void readFromJsonState(JsonObject& root) { + // The call to serializeConfig() must be done in the main loop, + // so we set a flag to signal the main loop to save state. + saveState = readSettingsFromJson(root); + readSensorsFromJson(root); + Serial.println("Staircase config read from API."); + } + + /* + * Writes the configuration to internal flash memory. + */ + void addToConfig(JsonObject& root) { + writeSettingsToJson(root); + Serial.println("Staircase config saved."); + } + + /* + * Reads the configuration to internal flash memory before setup() is called. + */ + void readFromConfig(JsonObject& root) { + readSettingsFromJson(root); + Serial.println("Staircase config loaded."); + } + + /* + * Shows the delay between steps and power-off time in the "info" + * tab of the web-UI. + */ + void addToJsonInfo(JsonObject& root) { + JsonObject staircase = root["u"]; + if (staircase.isNull()) { + staircase = root.createNestedObject("u"); + } + + if (enabled) { + JsonArray usermodEnabled = + staircase.createNestedArray("Staircase enabled"); // name + usermodEnabled.add("yes"); // value + + JsonArray segmentDelay = + staircase.createNestedArray("Delay between stairs"); // name + segmentDelay.add(segment_delay_ms); // value + segmentDelay.add(" milliseconds"); // unit + + JsonArray onTime = + staircase.createNestedArray("Power-off stairs after"); // name + onTime.add(on_time_ms / 1000); // value + onTime.add(" seconds"); // unit + } else { + JsonArray usermodEnabled = + staircase.createNestedArray("Staircase enabled"); // name + usermodEnabled.add("no"); // value + } + } +}; \ No newline at end of file diff --git a/usermods/Animated_Staircase/Animated_Staircase_config.h b/usermods/Animated_Staircase/Animated_Staircase_config.h new file mode 100644 index 00000000..98050747 --- /dev/null +++ b/usermods/Animated_Staircase/Animated_Staircase_config.h @@ -0,0 +1,21 @@ +/* + * Animated_Staircase compiletime confguration. + * + * Please see README.md on how to change this file. + */ + +// Please change the pin numbering below to match your board. +#define TOP_PIR_PIN D5 +#define BOTTOM_PIR_PIN D6 + +// Or uncumment and a pir and use an ultrasound HC-SR04 sensor, +// see README.md for details +#ifndef TOP_PIR_PIN +#define TOP_TRIGGER_PIN D2 +#define TOP_ECHO_PIN D3 +#endif + +#ifndef BOTTOM_PIR_PIN +#define BOTTOM_TRIGGER_PIN D4 +#define BOTTOM_ECHO_PIN D5 +#endif \ No newline at end of file diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md new file mode 100644 index 00000000..6e84b544 --- /dev/null +++ b/usermods/Animated_Staircase/README.md @@ -0,0 +1,203 @@ +# Usermod Animated Staircase +This usermod makes your staircase look cool by switching it on with an animation. It uses +PIR or ultrasonic sensors at the top and bottom of your stairs to: + +- Light up the steps in your walking direction, leading the way. +- Switch off the steps after you, in the direction of the last detected movement. +- Always switch on when one of the sensors detects movement, even if an effect + is still running. It can therewith handle multiple people on the stairs gracefully. + +The Animated Staircase can be controlled by the WLED API. Change settings such as +speed, on/off time and distance settings by sending an HTTP request, see below. + +## WLED integration +To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://github.com/Aircoookie/WLED/wiki/Compiling-WLED). + +Before compiling, you have to make the following modifications: + +Edit `usermods_list.cpp`: +1. Open `wled00/usermods_list.cpp` +2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file +3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. + +Edit `Animated_Staircase_config.h`: +1. Open `usermods/Animated_Staircase/Animated_Staircase_config.h` +2. To use PIR sensors, change these lines to match your setup: + Using D7 and D6 pin notation as used on several boards: + + ```cpp + #define TOP_PIR_PIN D7 + #define BOTTOM_PIR_PIN D6 + ``` + + Or using GPIO numbering for pins 25 and 26: + ```cpp + #define TOP_PIR_PIN 26 + #define BOTTOM_PIR_PIN 25 + ``` + + To use Ultrasonic HC-SR04 sensors instead of (one of the) PIR sensors, + uncomment one of the PIR sensor lines and adjust the pin numbers for the + connected Ultrasonic sensor. In the example below we use an Ultrasonic + sensor at the bottom of the stairs: + + ```cpp + #define TOP_PIR_PIN 32 + //#define BOTTOM_PIR_PIN D6 /* This PIR sensor is disabled */ + + #ifndef TOP_PIR_PIN + #define TOP_SIGNAL_PIN D2 + #define TOP_ECHO_PIN D3 + #endif + + #ifndef BOTTOM_PIR_PIN /* If the bottom PIR is disabled, */ + #define BOTTOM_SIGNAL_PIN 25 /* This Ultrasonic sensor is used */ + #define BOTTOM_ECHO_PIN 26 + #endif + ``` + +After these modifications, compile and upload your WLED binary to your board +and check the WLED info page to see if this usermod is enabled. + +## Hardware installation +1. Stick the LED strip under each step of the stairs. +2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step + of your stairs. +3. Connect the data-out pin at the end of each strip per step to the data-in pin on the + other end of the next step, creating one large virtual LED strip. +4. Mount sensors of choice at the bottom and top of the stairs and connect them to the ESP. +5. To make sure all LEDs get enough power and have your staircase lighted evenly, power each + step from one side, using at least AWG14 or 2.5mm^2 cable. Don't connect them serial as you + do for the datacable! + +You _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor. + +## WLED configuration +1. In the WLED UI, confgure a segment for each step. The lowest step of the stairs is the + lowest segment id. +2. Save your segments into a preset. +3. Ideally, add the preset in the config > LED setup menu to the "apply + preset **n** at boot" setting. + +## Changing behavior through API +The Staircase settings can be changed through the WLED JSON api. + +**NOTE:** We are using [curl](https://curl.se/) to send HTTP POSTs to the WLED API. +If you're using Windows and want to use the curl commands, replace the `\` with a `^` +or remove them and put everything on one line. + + +| Setting | Description | Default | +|------------------|---------------------------------------------------------------|---------| +| enabled | Enable or disable the usermod | true | +| segment-delay-ms | Delay (milliseconds) between switching on/off each step | 150 | +| on-time-s | Time (seconds) the stairs stay lit after last detection | 5 | +| bottom-echo-us | Detection range of ultrasonic sensor | 1749 | +| bottomsensor | Manually trigger a down to up animation via API | false | +| topsensor | Manually trigger an up to down animation via API | false | + + +To read the current settings, open a browser to `http://xxx.xxx.xxx.xxx/json/state` (use your WLED +device IP address). The device will respond with a json object containing all WLED settings. +The staircase settings and sensor states are inside the WLED status element: + +```json +{ + "state": { + "staircase": { + "enabled": true, + "segment-delay-ms": 150, + "on-time-s": 5, + "bottomsensor": false, + "topsensor": false + }, +} +``` + +### Enable/disable the usermod +By disabling the usermod you will be able to keep the LED's on, independent from the sensor +activity. This enables to play with the lights without the usermod switching them on or off. + +To disable the usermod: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d {"staircase":{"enabled":false}} \ + xxx.xxx.xxx.xxx/json/state +``` + +To enable the usermod again, use `"enabled":true`. + +### Changing animation parameters +To change the delay between the steps to (for example) 100 milliseconds and the on-time to +10 seconds: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"staircase":{"segment-delay-ms":100,"on-time-s":10}}' \ + xxx.xxx.xxx.xxx/json/state +``` + +### Changing detection range of the ultrasonic HC-SR04 sensor +When an ultrasonic sensor is enabled in `Animated_Staircase_config.h`, you'll see a +`bottom-echo-us` setting appear in the json api: + +```json +{ + "state": { + "staircase": { + "enabled": true, + "segment-delay-ms": 150, + "on-time-s": 5, + "bottom-echo-us": 1749 + }, +} +``` + +If the HC-SR04 sensor detects an echo within 1749 microseconds (corresponding to ~30 cm +detection range from the sensor), it will trigger switching on the staircase. This setting +can be changed through the API with an HTTP POST: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"staircase":{"bottom-echo-us":1166}}' \ + xxx.xxx.xxx.xxx/json/state +``` + +Calculating the detection range can be performed as follows: The speed of sound is 343m/s at 20 +degrees Centigrade. Since the sound has to travel back and forth, the detection range for the +sensor in cm is (0.0343 * maxTimeUs) / 2. To get you started, please find delays and distances below: + +| Distance | Detection time | +|---------:|----------------:| +| 5 cm | 292 uS | +| 10 cm | 583 uS | +| 20 cm | 1166 uS | +| 30 cm | 1749 uS | +| 50 cm | 2915 uS | +| 100 cm | 5831 uS | + +**Please note:** that using an HC-SR04 sensor, particularly when detecting echos at longer +distances creates delays in the WLED software, and _might_ introduce timing hickups in your animations or +a less responsive web interface. It is therefore advised to keep the detection time as short as possible. + +### Animation triggering through the API +Instead of stairs activation by one of the sensors, you can also trigger the animation through +the API. To simulate triggering the bottom sensor, use: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"staircase":{"bottomsensor":true}}' \ + xxx.xxx.xxx.xxx/json/state +``` + +Likewise, to trigger the top sensor, use: + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"staircase":{"topsensor":true}}' \ + xxx.xxx.xxx.xxx/json/state +``` + +Have fun with this usermod.
+www.rolfje.com diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 72ea5a7e..518aef3a 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -87,4 +87,4 @@ void registerUsermods() #ifdef USERMOD_DHT usermods.add(new UsermodDHT()); #endif -} +} \ No newline at end of file