Animated Staircase Usermod (#1763)

* Initial version of the PIR Staircase wipe up/down.

* Add pulldown reminder

* Workaround for missing D6 and D7 definitions on esp32dev

* Add pinouts for d1 mini (esp8266 and esp32) and NodeMCU (and Arduino)

* These IO pins on all these boards... NodeMCU and d1_mini esp32 supported. The others? Fingers crossed.

* Changed to not collide with led strip pins

* PIR on GPIO 15 and 16 on esp32 d1 mini

* Removed PIN number ifdefs, it is impossible to match all boards

* Settings in flash, info and API

* Update README.md

* Small doc changes

* Improved README

* Fixed error in reading configuration

* Add API documentation

* Documentation and code cleanup

* Add enable/disable to API settings

* Restore segment state when disabling plugin

* Add debounce

* Set segments in animation mode

* Set segments in animation mode

* Add support for HR04 sensors

* Add preliminary description for  using an HR04 sensor

* Fixed typenumber and linked to datasheet

* Moved config away from defines to prevent user error

* Trigger Sensors through API

* Rename scripts folder so that it's name doesn't clash with the `pio` command (prevents platformIO in VSCode to work properly on Mac)

* Bugfix for detection problems

* Separated config from code

* Renamed Signal to Trigger pin

* Filename adjusted

* Clarifications and additions to README

* Fixed references to pio scripts

* Fixed API trigger bug

* Adjustments to README.md

* More efficient use of flash cycles, better naming

* Bugfix: bottom sensor was not read properly

* Renamed to Animated_Staircase

* Add note on ultrasonic sensor and esp32

* Better naming of defines

* Bugfix: Swipe down started with two steps.

* Removed upload port in nodemcuv2 section

Co-authored-by: Rolf <rolf@phobos.local>
Co-authored-by: Rolf <>
This commit is contained in:
Rolf 2021-02-25 09:52:48 +01:00 committed by GitHub
parent d7790a04c5
commit 746a8badac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 656 additions and 5 deletions

View File

@ -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:

View File

@ -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<bool>());
topSensorWrite = topSensorRead || (staircase["top-sensor"].as<bool>());
}
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
}
}
};

View File

@ -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

View File

@ -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.<br/>
www.rolfje.com