From 5f290b72820fe599157ed11f38db5549bc0a861f Mon Sep 17 00:00:00 2001 From: Daniel Breitlauch Date: Tue, 5 Mar 2024 17:10:33 +0100 Subject: [PATCH 1/5] Add Staircase usermod with position measurements --- .../Distance_Staircase/Distance_Staircase.h | 613 ++++++++++++++++++ usermods/Distance_Staircase/README.md | 78 +++ wled00/const.h | 1 + wled00/pin_manager.h | 3 +- wled00/usermods_list.cpp | 8 + 5 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 usermods/Distance_Staircase/Distance_Staircase.h create mode 100644 usermods/Distance_Staircase/README.md diff --git a/usermods/Distance_Staircase/Distance_Staircase.h b/usermods/Distance_Staircase/Distance_Staircase.h new file mode 100644 index 0000000000..dd4c59a981 --- /dev/null +++ b/usermods/Distance_Staircase/Distance_Staircase.h @@ -0,0 +1,613 @@ +/* + * Usermod for detecting people entering/leaving a staircase and switching the + * every stair depending on distance of the person. + * + * Edit the Distance_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" + +class Distance_Staircase : public Usermod { + private: + + class Timer { + unsigned long delay_ms; + unsigned long lastTime = 0; + public: + explicit Timer(unsigned long delay_ms): delay_ms{delay_ms} {} + + void reset() { + lastTime = millis(); + } + + bool wouldBlock() { + return ((millis() - lastTime) < delay_ms); + } + + bool isEarly() { + if (wouldBlock()) { + return true; + } + reset(); + return false; + } + }; + + enum WalkDirection { + Up, Down + }; + + enum AnimationState : uint8_t { + None, Enter, FollowDistance, Finish, CoolDown + }; + + struct State { + private: + AnimationState _animation = None; + public: + WalkDirection direction = Up; + long lastChange = millis(); + + State() {} + State(AnimationState ani, WalkDirection dir): _animation{ani}, direction{dir} { + lastChange = millis(); + } + + const AnimationState& animation() { + return _animation; + } + + void set(AnimationState ani) { + _animation = ani; + lastChange = millis(); + } + + }; + + template + class SmoothMeasure { + int lastDistancesMeasuredPosition = 0; + int lastDistancesMeasured[SIZE] = {0}; + int init = 0; + public: + int value = 0, lastValue = 0; + + SmoothMeasure() {} + + bool hasChanged() { + return value > 0 && value != lastValue; + } + + void reset() { + init = 0; + lastDistancesMeasuredPosition = 0; + } + + void feed(int cm) { + lastDistancesMeasured[lastDistancesMeasuredPosition] = cm; + lastDistancesMeasuredPosition = (lastDistancesMeasuredPosition + 1) % SIZE; + + if (init != SIZE) { + init++; + } + + int min = std::numeric_limits::max(); + int max = std::numeric_limits::min(); + int sum = 0; + for (auto measure : lastDistancesMeasured) { + min = MIN(min, measure); + max = MAX(max, measure); + sum += measure; + } + sum -= min + max; + lastValue = value; + value = sum / (SIZE - 2); + } + }; + + State state; + + Timer animationTimer = Timer(500); + Timer finishSwipeTimer = Timer(250); + Timer scanTimer = Timer(50); + Timer coolDownTimer = Timer(3000); + + /* configuration (available in API and stored in flash) */ + bool enabled = false; // Enable this usermod + int8_t triggerPin = -1; // disabled + int8_t echoPin = -1; // disabled + int8_t topPIRPin = -1; // disabled + int8_t bottomPIRPin = -1; // disabled + + unsigned long on_time_ms = 20000; // The time for the light to stay on + unsigned long invite_time_top_ms = 5000; // The time for the light to stay on without distance + unsigned long invite_time_bottom_ms = 3000; // The time for the light to stay on without distance + + int endOfStairsDistance = 145; + + /* runtime variables */ + bool initDone = false; + + bool HAautodiscovery = true; + + // segment id between onIndex and offIndex are on. + // control 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 minSegmentId = 0; + + bool topSensorState = false; + bool bottomSensorState = false; + SmoothMeasure<6> distanceState; + WalkDirection lastActiveSensor = Down; + + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _onTime[]; + static const char _endOfStairsDistance[]; + static const char _HAautodiscovery[]; + static const char _topPIRPin[]; + static const char _bottomPIRPin[]; + static const char _sonarTriggerPin[]; + static const char _sonarEchoPin[]; + + void publishMqtt(bool bottom, bool state) { +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[64]; + sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); + mqtt->publish(subuf, 0, false, state ? "on" : "off"); + } +#endif + } + + void publishDistanceMqtt() { +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[64]; + char dist[16]; + sprintf_P(subuf, PSTR("%s/distance/0"), mqttDeviceTopic); + sprintf_P(dist, (PGM_P)F("%03d"), distanceState.value); + mqtt->publish(subuf, 0, true, dist); + } +#endif + } + + void publichHomeAssistantAutodiscoveryMotionSensor(bool bottom) { + StaticJsonDocument<600> doc; + char uid[24], json_str[1024], buf[128]; + + const char* topBottom = bottom ? "Bottom" : "Top"; + + sprintf_P(buf, PSTR("%s %s Motion"), serverDescription, topBottom); //max length: 33 + 7 = 40 + doc[F("name")] = buf; + sprintf_P(buf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); //max length: 33 + 7 = 40 + doc[F("stat_t")] = buf; + doc[F("pl_on")] = "on"; + doc[F("pl_off")] = "off"; + sprintf_P(uid, PSTR("%s_%s_motion"), escapedMac.c_str(), topBottom); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = F("motion"); + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = "WLED"; + device[F("mdl")] = F("FOSS"); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/binary_sensor/%s/config"), uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain? + } + + void publichHomeAssistantAutodiscoveryDistanceSensor() { + StaticJsonDocument<600> doc; + char uid[24], json_str[1024], buf[128]; + + sprintf_P(buf, PSTR("%s Distance"), serverDescription); //max length: 33 + 7 = 40 + doc[F("name")] = buf; + sprintf_P(buf, PSTR("%s/distance/0"), mqttDeviceTopic); //max length: 33 + 7 = 40 + doc[F("stat_t")] = buf; + sprintf_P(uid, PSTR("%s_distance"), escapedMac.c_str()); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = F("distance"); + doc[F("unit_of_measurement")] = F("cm"); + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = "WLED"; + device[F("mdl")] = F("FOSS"); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/sensor/%s/config"), uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain? + } + + void onMqttConnect(bool sessionPresent) { +#ifndef WLED_DISABLE_MQTT + if (HAautodiscovery) { + publichHomeAssistantAutodiscoveryMotionSensor(0); + publichHomeAssistantAutodiscoveryMotionSensor(1); + publichHomeAssistantAutodiscoveryDistanceSensor(); + } +#endif + } + + void updateSegments() { + for (int i = minSegmentId; i <= maxSegmentId; i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) { + continue; // skip gaps + } + if (onIndex <= i && i < offIndex) { + seg.setOption(SEG_OPTION_ON, true); + } else { + seg.setOption(SEG_OPTION_ON, false); + } + } + strip.trigger(); // force strip refresh + stateChanged = true; // inform external devices/UI of change + colorUpdated(CALL_MODE_DIRECT_CHANGE); + } + + int ultrasoundRead(int8_t signalPin, int8_t echoPin) { + if (signalPin<0 || echoPin<0) return false; + digitalWrite(signalPin, LOW); + delayMicroseconds(2); + digitalWrite(signalPin, HIGH); + delayMicroseconds(10); + digitalWrite(signalPin, LOW); + int duration = pulseIn(echoPin, HIGH, 100000); + int cm = (duration / 2) / 29.1; // Divide by 29.1 or multiply by 0.0343 + if (cm < 1 || cm > 400) { + return -1; + } + return cm; + } + + void checkSensors() { + if (scanTimer.isEarly()) { + return; + } + + int topMovement = digitalRead(topPIRPin); + if (topMovement != topSensorState) { + topSensorState = topMovement; + publishMqtt(false, topSensorState); + if (topMovement) { + lastActiveSensor = Up; + } + } + + int bottomMovement = digitalRead(bottomPIRPin); + if (bottomMovement != bottomSensorState) { + bottomSensorState = bottomMovement; + publishMqtt(true, bottomSensorState); + if (bottomMovement) { + lastActiveSensor = Down; + } + } + + if (state.animation() != None) { + int distance = ultrasoundRead(triggerPin, echoPin); + distanceState.feed(distance); + if (distanceState.hasChanged()) { + publishDistanceMqtt(); + } + } + } + + void animateNoneState() { + if (topSensorState || bottomSensorState) { + state = State(Enter, topSensorState? Down : Up); + distanceState.reset(); + if (state.direction == Up) { + onIndex = maxSegmentId - 1; + offIndex = maxSegmentId; + } else { + onIndex = minSegmentId; + offIndex = minSegmentId + 2; + } + } + } + + void animateEnterState() { + int after = state.direction == Up? invite_time_bottom_ms : invite_time_top_ms; + if ((millis() - state.lastChange) > after && !bottomSensorState && !topSensorState) { + state.set(None); + } + if (0 < distanceState.value && distanceState.value < endOfStairsDistance) { + state.set(FollowDistance); + } + } + + void animateFollowDistanceState() { + if (animationTimer.isEarly()) { + return; + } + if (lastActiveSensor == state.direction && distanceState.value > endOfStairsDistance) { + state.set(Finish); + } + if (state.direction == Up) { + onIndex = MAX(minSegmentId, distanceState.value / (endOfStairsDistance / strip.getSegmentsNum()) - 4); + } else { + offIndex = MIN(maxSegmentId + 1, distanceState.value / (endOfStairsDistance / strip.getSegmentsNum()) + 3); + } + if ((millis() - state.lastChange) > on_time_ms && !bottomSensorState && !topSensorState) { + state.set(Finish); + } + } + + void animateFinishState() { + if (finishSwipeTimer.isEarly()) { + return; + } + + if (state.direction == Up) { + offIndex--; + onIndex = 0; + } else { + onIndex++; + } + + if (onIndex == offIndex) { + onIndex = 0; + offIndex = 0; + coolDownTimer.reset(); + state.set(CoolDown); + } + } + + void animate() { + byte oldOn = onIndex; + byte oldOff = offIndex; + + switch (state.animation()) { + case None: + animateNoneState(); + break; + case Enter: + animateEnterState(); + break; + case FollowDistance: + animateFollowDistanceState(); + break; + case Finish: + animateFinishState(); + break; + case CoolDown: + if (!coolDownTimer.isEarly()) { + state.set(None); + } + break; + } + + if (oldOn != onIndex || oldOff != offIndex) { + updateSegments(); + } + } + + void enable(bool enable) { + onIndex = 0; + offIndex = 0; + manageSegments(); + updateSegments(); + + if (!enable) { + state.set(None); + } + enabled = enable; + } + + uint8_t getFirstActiveSegmentId(void) { + for (size_t i = 0; i < strip.getSegmentsNum(); i++) { + if (strip.getSegment(i).isActive()) return i; + } + return 0; + } + + void manageSegments() { + minSegmentId = getFirstActiveSegmentId(); + maxSegmentId = strip.getLastActiveSegmentId(); + } + + public: + void setup() { + // standardize invalid pin numbers to -1 + if (triggerPin < 0) triggerPin = -1; + if (echoPin < 0) echoPin = -1; + if (topPIRPin < 0) topPIRPin = -1; + if (bottomPIRPin < 0) bottomPIRPin = -1; + // allocate pins + PinManagerPinType pins[4] = { + { triggerPin, true }, + { topPIRPin, false }, + { bottomPIRPin, false }, + { echoPin, false } + }; + // NOTE: this *WILL* return TRUE if all the pins are set to -1. + // this is *BY DESIGN*. + if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_DistanceStaircase)) { + triggerPin = -1; + echoPin = -1; + topPIRPin = -1; + bottomPIRPin = -1; + enabled = false; + } + enable(enabled); + initDone = true; + } + + void loop() { + if (!enabled || strip.isUpdating()) return; + + manageSegments(); + checkSensors(); + animate(); + } + + uint16_t getId() { return USERMOD_ID_DISTANCE_STAIRCASE; } + + void addToJsonState(JsonObject& root) { + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + staircase[F("top-sensor")] = topSensorState; + staircase[F("bottom-sensor")] = bottomSensorState; + } + + /* + * Reads configuration settings from the json API. + * See void addToJsonState(JsonObject& root) + */ + void readFromJsonState(JsonObject& root) { + if (!initDone) { + return; // prevent crash on boot applyPreset() + } + bool en = enabled; + JsonObject staircase = root[FPSTR(_name)]; + if (!staircase.isNull()) { + if (staircase[FPSTR(_enabled)].is()) { + en = staircase[FPSTR(_enabled)].as(); + } else { + String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on + en = (bool)(str != "off"); // off is guaranteed to be present + } + if (en != enabled) { + enable(en); + } + } + } + + /* + * Writes the configuration to internal flash memory. + */ + void addToConfig(JsonObject& root) { + JsonObject staircase = root[FPSTR(_name)]; + if (staircase.isNull()) { + staircase = root.createNestedObject(FPSTR(_name)); + } + staircase[FPSTR(_enabled)] = enabled; + staircase[FPSTR(_onTime)] = on_time_ms / 1000; + staircase[FPSTR(_sonarTriggerPin)] = triggerPin; + staircase[FPSTR(_sonarEchoPin)] = echoPin; + staircase[FPSTR(_topPIRPin)] = topPIRPin; + staircase[FPSTR(_bottomPIRPin)] = bottomPIRPin; + staircase[FPSTR(_HAautodiscovery)] = HAautodiscovery; + DEBUG_PRINTLN(F("Staircase config saved.")); + } + + /* + * Reads the configuration to internal flash memory before setup() is called. + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) { + int8_t oldTriggerPin = triggerPin; + int8_t oldEchoPin = echoPin; + int8_t oldTopPirPin = topPIRPin; + int8_t oldBottomPirPin = bottomPIRPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_enabled)] | enabled; + + HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery; + + on_time_ms = top[FPSTR(_onTime)] | on_time_ms/1000; + on_time_ms = min(900,max(10,(int)on_time_ms)) * 1000; // min 10s, max 15min + + endOfStairsDistance = top[FPSTR(_endOfStairsDistance)] | endOfStairsDistance; + + triggerPin = top[FPSTR(_sonarTriggerPin)] | triggerPin; + echoPin = top[FPSTR(_sonarEchoPin)] | echoPin; + + topPIRPin = top[FPSTR(_topPIRPin)] | topPIRPin; + bottomPIRPin = top[FPSTR(_bottomPIRPin)] | bottomPIRPin; + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } else { + // changing parameters from settings page + DEBUG_PRINTLN(F(" config (re)loaded.")); + bool changed = false; + if ((oldTriggerPin != triggerPin) || + (oldTopPirPin != topPIRPin) || + (oldBottomPirPin != bottomPIRPin) || + (oldEchoPin != echoPin)) { + changed = true; + pinManager.deallocatePin(oldTriggerPin, PinOwner::UM_DistanceStaircase); + pinManager.deallocatePin(oldEchoPin, PinOwner::UM_DistanceStaircase); + pinManager.deallocatePin(oldTopPirPin, PinOwner::UM_DistanceStaircase); + pinManager.deallocatePin(oldBottomPirPin, PinOwner::UM_DistanceStaircase); + } + if (changed) setup(); + } + return true; + } + + void addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name + + String uiDomString = F(" "); + infoArr.add(uiDomString); + infoArr.add(distanceState.value); + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Distance_Staircase::_name[] PROGMEM = "staircase"; +const char Distance_Staircase::_onTime[] PROGMEM = "maxTimeSeconds"; +const char Distance_Staircase::_enabled[] PROGMEM = "enabled"; +const char Distance_Staircase::_endOfStairsDistance[] PROGMEM = "endOfStairsDistanceCM"; +const char Distance_Staircase::_sonarTriggerPin[] PROGMEM = "SonarTriggerPin"; +const char Distance_Staircase::_sonarEchoPin[] PROGMEM = "SonarEchoPin"; +const char Distance_Staircase::_topPIRPin[] PROGMEM = "TopPIRPin"; +const char Distance_Staircase::_bottomPIRPin[] PROGMEM = "BottomPIRPin"; +const char Distance_Staircase::_HAautodiscovery[] PROGMEM = "HA-autodiscovery"; + diff --git a/usermods/Distance_Staircase/README.md b/usermods/Distance_Staircase/README.md new file mode 100644 index 0000000000..444525939d --- /dev/null +++ b/usermods/Distance_Staircase/README.md @@ -0,0 +1,78 @@ +# Usermod Distance Staircase +This usermod is a modification of the animated staircase usermod and makes your staircase look cool by illuminating it with an animation. It uses PIR and ultrasonic sensors. At the Top and bottom are PIR sensors to detect entering the staircase. +The ultrasonic sensor detects how far one is through the staircase. + +- Light up the steps in the direction you're walking. At the position you are. +- Switch off the steps after you, in the direction of the last detected movement. + +## WLED integration +To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/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. + +Or just add `-D USERMOD_DISTANCE_STAIRCASE` to the build arguments. + +You can configure usermod using the Usermods settings page. +Please enter GPIO pins for PIR and ultrasonic sensors (trigger and echo). + +## Hardware installation +1. Attach the LED strip to each step of the stairs. +3. Connect the data-out pin at the end of each strip per step to the data-in pin on the + next step, creating one large virtual LED strip. +4. Mount PIR sensors at the bottom and top of the stairs and connect them to the ESP. +5. Mount the US sensor in line with the stairs. E.g. at the top looking down. +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, configure a segment for each step. The highest 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. + +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 "state" element: + +```json +{ + "state": { + "staircase": { + "enabled": true, + "bottom-sensor": false, + "top-sensor": 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 you 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`. + +Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. + +**Please note:** using an HC-SR04 sensor, particularly when detecting echos at longer +distances creates delays in the WLED software, _might_ introduce timing hiccups in your animation or +a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. + +**MQTT** +All sensors can be auto detected by Homeassistant and publish their values. diff --git a/wled00/const.h b/wled00/const.h index 388b64c820..9865b143b6 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -152,6 +152,7 @@ #define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h" #define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h" #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" +#define USERMOD_ID_DISTANCE_STAIRCASE 45 //Usermod "Distance_Staircase.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 39f2c6ec60..b9f39a434e 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -61,7 +61,8 @@ enum struct PinOwner : uint8_t { UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE, // 0x20 // Usermod "audio_reactive.h" UM_SdCard = USERMOD_ID_SD_CARD, // 0x25 // Usermod "usermod_sd_card.h" UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" - UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_DistanceStaircase = USERMOD_ID_DISTANCE_STAIRCASE // 0x2C // Usermod "Distance_Staircase.h" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index db016f5508..3da6f8fad2 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -89,6 +89,10 @@ #include "../usermods/Animated_Staircase/Animated_Staircase.h" #endif +#ifdef USERMOD_DISTANCE_STAIRCASE + #include "../usermods/Distance_Staircase/Distance_Staircase.h" +#endif + #ifdef USERMOD_MULTI_RELAY #include "../usermods/multi_relay/usermod_multi_relay.h" #endif @@ -276,6 +280,10 @@ void registerUsermods() usermods.add(new Animated_Staircase()); #endif + #ifdef USERMOD_DISTANCE_STAIRCASE + usermods.add(new Distance_Staircase()); + #endif + #ifdef USERMOD_MULTI_RELAY usermods.add(new MultiRelay()); #endif From 15675c89c98e6c62e78a86a4b13c4ef07db478f1 Mon Sep 17 00:00:00 2001 From: Daniel Breitlauch Date: Tue, 5 Mar 2024 18:20:31 +0100 Subject: [PATCH 2/5] fix shuting down if invitation not taken --- .vscode/tasks.json | 6 +++--- usermods/Distance_Staircase/Distance_Staircase.h | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2ee772ce16..f46f002b40 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,8 +9,8 @@ ], "dependsOrder": "sequence", "problemMatcher": [ - "$platformio", - ], + "$platformio" + ] }, { "type": "PlatformIO", @@ -18,7 +18,7 @@ "task": "Build", "group": { "kind": "build", - "isDefault": true, + "isDefault": true }, "problemMatcher": [ "$platformio" diff --git a/usermods/Distance_Staircase/Distance_Staircase.h b/usermods/Distance_Staircase/Distance_Staircase.h index dd4c59a981..8499e6be96 100644 --- a/usermods/Distance_Staircase/Distance_Staircase.h +++ b/usermods/Distance_Staircase/Distance_Staircase.h @@ -335,6 +335,8 @@ class Distance_Staircase : public Usermod { void animateEnterState() { int after = state.direction == Up? invite_time_bottom_ms : invite_time_top_ms; if ((millis() - state.lastChange) > after && !bottomSensorState && !topSensorState) { + onIndex = 0; + offIndex = 0; state.set(None); } if (0 < distanceState.value && distanceState.value < endOfStairsDistance) { From 48880d23cbc9f890ee2d368ce5409cb2b634e1ee Mon Sep 17 00:00:00 2001 From: Daniel Breitlauch Date: Thu, 7 Mar 2024 12:43:09 +0100 Subject: [PATCH 3/5] add mqtt ha sensors which detect the direction a person went --- .../Distance_Staircase/Distance_Staircase.h | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/usermods/Distance_Staircase/Distance_Staircase.h b/usermods/Distance_Staircase/Distance_Staircase.h index 8499e6be96..fc95f51580 100644 --- a/usermods/Distance_Staircase/Distance_Staircase.h +++ b/usermods/Distance_Staircase/Distance_Staircase.h @@ -41,7 +41,7 @@ class Distance_Staircase : public Usermod { }; enum AnimationState : uint8_t { - None, Enter, FollowDistance, Finish, CoolDown + None, Enter, FollowDistance, Finish, CoolDown, Reset }; struct State { @@ -161,12 +161,26 @@ class Distance_Staircase : public Usermod { static const char _sonarTriggerPin[]; static const char _sonarEchoPin[]; - void publishMqtt(bool bottom, bool state) { + enum MotionType { + Bottom, Top, WentUp, WentDown + }; + + const char* motionTypeName(MotionType type) { + switch (type) { + case Bottom: return "Bottom"; + case Top: return "Top"; + case WentUp: return "WentUp"; + case WentDown: return "WentDown"; + } + return "unknown"; + } + + void publishMqtt(MotionType type, bool state) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED){ char subuf[64]; - sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); + sprintf_P(subuf, PSTR("%s/motion/%s"), mqttDeviceTopic, motionTypeName(type)); mqtt->publish(subuf, 0, false, state ? "on" : "off"); } #endif @@ -185,19 +199,19 @@ class Distance_Staircase : public Usermod { #endif } - void publichHomeAssistantAutodiscoveryMotionSensor(bool bottom) { + void publichHomeAssistantAutodiscoveryMotionSensor(MotionType type) { StaticJsonDocument<600> doc; char uid[24], json_str[1024], buf[128]; - const char* topBottom = bottom ? "Bottom" : "Top"; + const char* name = motionTypeName(type); - sprintf_P(buf, PSTR("%s %s Motion"), serverDescription, topBottom); //max length: 33 + 7 = 40 + sprintf_P(buf, PSTR("%s %s Motion"), serverDescription, name); //max length: 33 + 7 = 40 doc[F("name")] = buf; - sprintf_P(buf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); //max length: 33 + 7 = 40 + sprintf_P(buf, PSTR("%s/motion/%s"), mqttDeviceTopic, name); //max length: 33 + 7 = 40 doc[F("stat_t")] = buf; doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; - sprintf_P(uid, PSTR("%s_%s_motion"), escapedMac.c_str(), topBottom); + sprintf_P(uid, PSTR("%s_%s_motion"), escapedMac.c_str(), name); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = F("motion"); @@ -247,8 +261,10 @@ class Distance_Staircase : public Usermod { void onMqttConnect(bool sessionPresent) { #ifndef WLED_DISABLE_MQTT if (HAautodiscovery) { - publichHomeAssistantAutodiscoveryMotionSensor(0); - publichHomeAssistantAutodiscoveryMotionSensor(1); + publichHomeAssistantAutodiscoveryMotionSensor(Top); + publichHomeAssistantAutodiscoveryMotionSensor(Bottom); + publichHomeAssistantAutodiscoveryMotionSensor(WentUp); + publichHomeAssistantAutodiscoveryMotionSensor(WentDown); publichHomeAssistantAutodiscoveryDistanceSensor(); } #endif @@ -294,7 +310,7 @@ class Distance_Staircase : public Usermod { int topMovement = digitalRead(topPIRPin); if (topMovement != topSensorState) { topSensorState = topMovement; - publishMqtt(false, topSensorState); + publishMqtt(Top, topSensorState); if (topMovement) { lastActiveSensor = Up; } @@ -303,7 +319,7 @@ class Distance_Staircase : public Usermod { int bottomMovement = digitalRead(bottomPIRPin); if (bottomMovement != bottomSensorState) { bottomSensorState = bottomMovement; - publishMqtt(true, bottomSensorState); + publishMqtt(Bottom, bottomSensorState); if (bottomMovement) { lastActiveSensor = Down; } @@ -335,9 +351,7 @@ class Distance_Staircase : public Usermod { void animateEnterState() { int after = state.direction == Up? invite_time_bottom_ms : invite_time_top_ms; if ((millis() - state.lastChange) > after && !bottomSensorState && !topSensorState) { - onIndex = 0; - offIndex = 0; - state.set(None); + state.set(Reset); } if (0 < distanceState.value && distanceState.value < endOfStairsDistance) { state.set(FollowDistance); @@ -348,17 +362,18 @@ class Distance_Staircase : public Usermod { if (animationTimer.isEarly()) { return; } - if (lastActiveSensor == state.direction && distanceState.value > endOfStairsDistance) { - state.set(Finish); + + if ((lastActiveSensor == state.direction && distanceState.value > endOfStairsDistance) || // Person went through + ((millis() - state.lastChange) > on_time_ms && !bottomSensorState && !topSensorState)) // Time is up + { + state.set(Finish); } + if (state.direction == Up) { onIndex = MAX(minSegmentId, distanceState.value / (endOfStairsDistance / strip.getSegmentsNum()) - 4); } else { offIndex = MIN(maxSegmentId + 1, distanceState.value / (endOfStairsDistance / strip.getSegmentsNum()) + 3); } - if ((millis() - state.lastChange) > on_time_ms && !bottomSensorState && !topSensorState) { - state.set(Finish); - } } void animateFinishState() { @@ -374,8 +389,11 @@ class Distance_Staircase : public Usermod { } if (onIndex == offIndex) { - onIndex = 0; - offIndex = 0; + if (state.direction == Up) { + publishMqtt(WentUp, true); + } else { + publishMqtt(WentDown, true); + } coolDownTimer.reset(); state.set(CoolDown); } @@ -400,9 +418,15 @@ class Distance_Staircase : public Usermod { break; case CoolDown: if (!coolDownTimer.isEarly()) { - state.set(None); + state.set(Reset); } break; + case Reset: + onIndex = 0; + offIndex = 0; + publishMqtt(WentUp, false); + publishMqtt(WentDown, false); + state.set(None); } if (oldOn != onIndex || oldOff != offIndex) { @@ -411,13 +435,11 @@ class Distance_Staircase : public Usermod { } void enable(bool enable) { - onIndex = 0; - offIndex = 0; manageSegments(); updateSegments(); if (!enable) { - state.set(None); + state.set(Reset); } enabled = enable; } From df92faab4752cea1cb8daad8846d3cc78576032e Mon Sep 17 00:00:00 2001 From: Daniel Breitlauch Date: Fri, 8 Mar 2024 15:55:00 +0100 Subject: [PATCH 4/5] fix setting pin mode --- usermods/Distance_Staircase/Distance_Staircase.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/usermods/Distance_Staircase/Distance_Staircase.h b/usermods/Distance_Staircase/Distance_Staircase.h index fc95f51580..ad26aea3bd 100644 --- a/usermods/Distance_Staircase/Distance_Staircase.h +++ b/usermods/Distance_Staircase/Distance_Staircase.h @@ -440,6 +440,11 @@ class Distance_Staircase : public Usermod { if (!enable) { state.set(Reset); + } else { + pinMode(topPIRPin, INPUT_PULLUP); + pinMode(bottomPIRPin, INPUT_PULLUP); + pinMode(triggerPin, OUTPUT); + pinMode(echoPin, INPUT); } enabled = enable; } From 6b85ca8f773dce9188873d8681b08e48cd45d4a3 Mon Sep 17 00:00:00 2001 From: Daniel Breitlauch Date: Thu, 14 Mar 2024 16:21:20 +0100 Subject: [PATCH 5/5] same invite time up and down --- .../Distance_Staircase/Distance_Staircase.h | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/usermods/Distance_Staircase/Distance_Staircase.h b/usermods/Distance_Staircase/Distance_Staircase.h index ad26aea3bd..8a68c1a858 100644 --- a/usermods/Distance_Staircase/Distance_Staircase.h +++ b/usermods/Distance_Staircase/Distance_Staircase.h @@ -46,10 +46,13 @@ class Distance_Staircase : public Usermod { struct State { private: + static const long on_time_ms = 20000; // The time for the light to stay on + static const long invite_time_ms = 5000; // The time for the light to stay on without movement + long lastChange = millis(); + AnimationState _animation = None; public: WalkDirection direction = Up; - long lastChange = millis(); State() {} State(AnimationState ani, WalkDirection dir): _animation{ani}, direction{dir} { @@ -65,6 +68,13 @@ class Distance_Staircase : public Usermod { lastChange = millis(); } + bool inviteTimeOver() { + return (millis() - lastChange) > invite_time_ms; + } + + bool onTimeOver() { + return (millis() - lastChange) > on_time_ms; + } }; template @@ -122,10 +132,6 @@ class Distance_Staircase : public Usermod { int8_t topPIRPin = -1; // disabled int8_t bottomPIRPin = -1; // disabled - unsigned long on_time_ms = 20000; // The time for the light to stay on - unsigned long invite_time_top_ms = 5000; // The time for the light to stay on without distance - unsigned long invite_time_bottom_ms = 3000; // The time for the light to stay on without distance - int endOfStairsDistance = 145; /* runtime variables */ @@ -146,7 +152,7 @@ class Distance_Staircase : public Usermod { bool topSensorState = false; bool bottomSensorState = false; - SmoothMeasure<6> distanceState; + SmoothMeasure<3> distanceState; WalkDirection lastActiveSensor = Down; @@ -337,7 +343,6 @@ class Distance_Staircase : public Usermod { void animateNoneState() { if (topSensorState || bottomSensorState) { state = State(Enter, topSensorState? Down : Up); - distanceState.reset(); if (state.direction == Up) { onIndex = maxSegmentId - 1; offIndex = maxSegmentId; @@ -349,9 +354,8 @@ class Distance_Staircase : public Usermod { } void animateEnterState() { - int after = state.direction == Up? invite_time_bottom_ms : invite_time_top_ms; - if ((millis() - state.lastChange) > after && !bottomSensorState && !topSensorState) { - state.set(Reset); + if (state.inviteTimeOver() && !bottomSensorState && !topSensorState) { + state.set(Reset); } if (0 < distanceState.value && distanceState.value < endOfStairsDistance) { state.set(FollowDistance); @@ -364,7 +368,7 @@ class Distance_Staircase : public Usermod { } if ((lastActiveSensor == state.direction && distanceState.value > endOfStairsDistance) || // Person went through - ((millis() - state.lastChange) > on_time_ms && !bottomSensorState && !topSensorState)) // Time is up + (state.onTimeOver() && !bottomSensorState && !topSensorState)) { state.set(Finish); } @@ -418,14 +422,15 @@ class Distance_Staircase : public Usermod { break; case CoolDown: if (!coolDownTimer.isEarly()) { + publishMqtt(WentUp, false); + publishMqtt(WentDown, false); state.set(Reset); } break; case Reset: + distanceState.reset(); onIndex = 0; offIndex = 0; - publishMqtt(WentUp, false); - publishMqtt(WentDown, false); state.set(None); } @@ -539,7 +544,6 @@ class Distance_Staircase : public Usermod { staircase = root.createNestedObject(FPSTR(_name)); } staircase[FPSTR(_enabled)] = enabled; - staircase[FPSTR(_onTime)] = on_time_ms / 1000; staircase[FPSTR(_sonarTriggerPin)] = triggerPin; staircase[FPSTR(_sonarEchoPin)] = echoPin; staircase[FPSTR(_topPIRPin)] = topPIRPin; @@ -570,9 +574,6 @@ class Distance_Staircase : public Usermod { HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery; - on_time_ms = top[FPSTR(_onTime)] | on_time_ms/1000; - on_time_ms = min(900,max(10,(int)on_time_ms)) * 1000; // min 10s, max 15min - endOfStairsDistance = top[FPSTR(_endOfStairsDistance)] | endOfStairsDistance; triggerPin = top[FPSTR(_sonarTriggerPin)] | triggerPin;