Merge pull request #1951 from blazoncek/user-configurable-usermods

User configurable usermods.
This commit is contained in:
Aircoookie 2021-05-09 23:10:37 +02:00 committed by GitHub
commit 119826cb9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2648 additions and 1444 deletions

View File

@ -333,6 +333,18 @@ const char PAGE_settings_dmx[] PROGMEM = R"=====()=====";
"function GetV() {var d=document;\n" "function GetV() {var d=document;\n"
), ),
}, },
{
file: "settings_um.htm",
name: "PAGE_settings_um",
prepend: "=====(",
append: ")=====",
method: "plaintext",
filter: "html-minify",
mangle: (str) =>
str
.replace(/\<link rel="stylesheet".*\>/gms, "")
.replace(/\<style\>.*\<\/style\>/gms, "%CSS%%SCSS%")
}
], ],
"wled00/html_settings.h" "wled00/html_settings.h"
); );

View File

@ -9,25 +9,31 @@
*/ */
#pragma once #pragma once
#include "wled.h" #include "wled.h"
#include "Animated_Staircase_config.h"
#define USERMOD_ID_ANIMATED_STAIRCASE 1011 #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 { class Animated_Staircase : public Usermod {
private: private:
/* configuration (available in API and stored in flash) */
bool enabled = false; // 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
int8_t topPIRorTriggerPin = -1; // disabled
int8_t bottomPIRorTriggerPin = -1; // disabled
int8_t topEchoPin = -1; // disabled
int8_t bottomEchoPin = -1; // disabled
bool useUSSensorTop = false; // using PIR or UltraSound sensor?
bool useUSSensorBottom = false; // using PIR or UltraSound sensor?
unsigned int topMaxTimeUs = 1749; // default echo timout, top
unsigned int bottomMaxTimeUs = 1749; // default echo timout, bottom
/* runtime variables */
bool initDone = false;
// Time between checking of the sensors
const unsigned int scanDelay = 50;
// Lights on or off. // Lights on or off.
// Flipping this will start a transition. // Flipping this will start a transition.
bool on = false; bool on = false;
@ -63,8 +69,6 @@ class Animated_Staircase : public Usermod {
byte maxSegmentId = 1; byte maxSegmentId = 1;
byte mainSegmentId = 0; byte mainSegmentId = 0;
bool saveState = false;
// These values are used by the API to read the // These values are used by the API to read the
// last sensor state, or trigger a sensor // last sensor state, or trigger a sensor
// through the API // through the API
@ -73,9 +77,24 @@ class Animated_Staircase : public Usermod {
bool bottomSensorRead = false; bool bottomSensorRead = false;
bool bottomSensorWrite = false; bool bottomSensorWrite = false;
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _enabled[];
static const char _segmentDelay[];
static const char _onTime[];
static const char _useTopUltrasoundSensor[];
static const char _topPIRorTrigger_pin[];
static const char _topEcho_pin[];
static const char _useBottomUltrasoundSensor[];
static const char _bottomPIRorTrigger_pin[];
static const char _bottomEcho_pin[];
static const char _topEchoTime[];
static const char _bottomEchoTime[];
static const char _[];
void updateSegments() { void updateSegments() {
mainSegmentId = strip.getMainSegmentId(); // mainSegmentId = strip.getMainSegmentId();
WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); // WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId);
WS2812FX::Segment* segments = strip.getSegments(); WS2812FX::Segment* segments = strip.getSegments();
for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) {
if (!segments->isActive()) { if (!segments->isActive()) {
@ -134,17 +153,15 @@ class Animated_Staircase : public Usermod {
if ((millis() - lastScanTime) > scanDelay) { if ((millis() - lastScanTime) > scanDelay) {
lastScanTime = millis(); lastScanTime = millis();
#ifdef BOTTOM_PIR_PIN if (!useUSSensorBottom)
bottomSensorRead = bottomSensorWrite || (digitalRead(BOTTOM_PIR_PIN) == HIGH); bottomSensorRead = bottomSensorWrite || (digitalRead(bottomPIRorTriggerPin) == HIGH);
#else else
bottomSensorRead = bottomSensorWrite || ultrasoundRead(BOTTOM_TRIGGER_PIN, BOTTOM_ECHO_PIN, bottomMaxTimeUs); bottomSensorRead = bottomSensorWrite || ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxTimeUs);
#endif
#ifdef TOP_PIR_PIN if (!useUSSensorTop)
topSensorRead = topSensorWrite || (digitalRead(TOP_PIR_PIN) == HIGH); topSensorRead = topSensorWrite || (digitalRead(topPIRorTriggerPin) == HIGH);
#else else
topSensorRead = topSensorWrite || ultrasoundRead(TOP_TRIGGER_PIN, TOP_ECHO_PIN, topMaxTimeUs); topSensorRead = topSensorWrite || ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxTimeUs);
#endif
// Values read, reset the flags for next API call // Values read, reset the flags for next API call
topSensorWrite = false; topSensorWrite = false;
@ -160,9 +177,9 @@ class Animated_Staircase : public Usermod {
swipe = bottomSensorRead; swipe = bottomSensorRead;
if (swipe) { if (swipe) {
Serial.println("ON -> Swipe up."); DEBUG_PRINTLN(F("ON -> Swipe up."));
} else { } else {
Serial.println("ON -> Swipe down."); DEBUG_PRINTLN(F("ON -> Swipe down."));
} }
if (onIndex == offIndex) { if (onIndex == offIndex) {
@ -181,15 +198,16 @@ class Animated_Staircase : public Usermod {
} }
void autoPowerOff() { void autoPowerOff() {
// TODO: add logic to wait until PIR sensor deactivates
if (on && ((millis() - lastSwitchTime) > on_time_ms)) { if (on && ((millis() - lastSwitchTime) > on_time_ms)) {
// Swipe OFF in the direction of the last sensor detection // Swipe OFF in the direction of the last sensor detection
swipe = lastSensor; swipe = lastSensor;
on = false; on = false;
if (swipe) { if (swipe) {
Serial.println("OFF -> Swipe up."); DEBUG_PRINTLN(F("OFF -> Swipe up."));
} else { } else {
Serial.println("OFF -> Swipe down."); DEBUG_PRINTLN(F("OFF -> Swipe down."));
} }
} }
} }
@ -198,8 +216,8 @@ class Animated_Staircase : public Usermod {
if ((millis() - lastTime) > segment_delay_ms) { if ((millis() - lastTime) > segment_delay_ms) {
lastTime = millis(); lastTime = millis();
byte oldOnIndex = onIndex; // byte oldOnIndex = onIndex;
byte oldOffIndex = offIndex; // byte oldOffIndex = offIndex;
if (on) { if (on) {
// Turn on all segments // Turn on all segments
@ -217,103 +235,44 @@ class Animated_Staircase : public Usermod {
} }
} }
void writeSettingsToJson(JsonObject& root) { // send sesnor values to JSON API
JsonObject staircase = root["staircase"]; void writeSensorsToJson(JsonObject& staircase) {
if (staircase.isNull()) { staircase[F("top-sensor")] = topSensorRead;
staircase = root.createNestedObject("staircase"); staircase[F("bottom-sensor")] = bottomSensorRead;
}
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) { // allow overrides from JSON API
JsonObject staircase = root["staircase"]; void readSensorsFromJson(JsonObject& staircase) {
if (staircase.isNull()) { bottomSensorWrite = bottomSensorRead || (staircase[F("bottom-sensor")].as<bool>());
staircase = root.createNestedObject("staircase"); topSensorWrite = topSensorRead || (staircase[F("top-sensor")].as<bool>());
}
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) { void enable(bool enable) {
if (enable) { if (enable) {
Serial.println("Animated Staircase enabled."); DEBUG_PRINTLN(F("Animated Staircase enabled."));
Serial.print("Delay between steps: "); DEBUG_PRINT(F("Delay between steps: "));
Serial.print(segment_delay_ms, DEC); DEBUG_PRINT(segment_delay_ms);
Serial.print(" milliseconds.\nStairs switch off after: "); DEBUG_PRINT(F(" milliseconds.\nStairs switch off after: "));
Serial.print(on_time_ms / 1000, DEC); DEBUG_PRINT(on_time_ms / 1000);
Serial.println(" seconds."); DEBUG_PRINTLN(F(" seconds."));
#ifdef BOTTOM_PIR_PIN // TODO: attach interrupts
pinMode(BOTTOM_PIR_PIN, INPUT); if (!useUSSensorBottom)
#else pinMode(bottomPIRorTriggerPin, INPUT);
pinMode(BOTTOM_TRIGGER_PIN, OUTPUT); else {
pinMode(BOTTOM_ECHO_PIN, INPUT); pinMode(bottomPIRorTriggerPin, OUTPUT);
#endif pinMode(bottomEchoPin, INPUT);
}
#ifdef TOP_PIR_PIN if (!useUSSensorTop)
pinMode(TOP_PIR_PIN, INPUT); pinMode(topPIRorTriggerPin, INPUT);
#else else {
pinMode(TOP_TRIGGER_PIN, OUTPUT); pinMode(topPIRorTriggerPin, OUTPUT);
pinMode(TOP_ECHO_PIN, INPUT); pinMode(topEchoPin, INPUT);
#endif }
} else { } else {
// Restore segment options // Restore segment options
WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId); // WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId);
WS2812FX::Segment* segments = strip.getSegments(); WS2812FX::Segment* segments = strip.getSegments();
for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) { for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) {
if (!segments->isActive()) { if (!segments->isActive()) {
@ -323,47 +282,57 @@ class Animated_Staircase : public Usermod {
segments->setOption(SEG_OPTION_ON, 1, 1); segments->setOption(SEG_OPTION_ON, 1, 1);
} }
colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE);
Serial.println("Animated Staircase disabled."); DEBUG_PRINTLN(F("Animated Staircase disabled."));
} }
enabled = enable; enabled = enable;
} }
public: public:
void setup() { enable(enabled); } void setup() {
// allocate pins
if (topPIRorTriggerPin >= 0) {
if (!pinManager.allocatePin(topPIRorTriggerPin,useUSSensorTop))
topPIRorTriggerPin = -1;
}
if (topEchoPin >= 0) {
if (!pinManager.allocatePin(topEchoPin,false))
topEchoPin = -1;
}
if (bottomPIRorTriggerPin >= 0) {
if (!pinManager.allocatePin(bottomPIRorTriggerPin,useUSSensorBottom))
bottomPIRorTriggerPin = -1;
}
if (bottomEchoPin >= 0) {
if (!pinManager.allocatePin(bottomPIRorTriggerPin,false))
bottomEchoPin = -1;
}
// TODO: attach interrupts in enable()
// validate pins
if ( topPIRorTriggerPin < 0 || bottomPIRorTriggerPin < 0 ||
(useUSSensorTop && topEchoPin < 0) || (useUSSensorBottom && bottomEchoPin < 0) )
enabled = false;
enable(enabled);
initDone = true;
}
void loop() { void loop() {
// Write changed settings from to flash (see readFromJsonState()) if (!enabled) return;
if (saveState) {
serializeConfig();
saveState = false;
}
if (!enabled) {
return;
}
checkSensors(); checkSensors();
autoPowerOff(); autoPowerOff();
updateSwipe(); updateSwipe();
} }
uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } 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) { void addToJsonState(JsonObject& root) {
writeSettingsToJson(root); JsonObject staircase = root[FPSTR(_name)];
writeSensorsToJson(root); if (staircase.isNull()) {
Serial.println("Staircase config exposed in API."); staircase = root.createNestedObject(FPSTR(_name));
}
writeSensorsToJson(staircase);
DEBUG_PRINTLN(F("Staircase sensor state exposed in API."));
} }
/* /*
@ -371,27 +340,107 @@ class Animated_Staircase : public Usermod {
* See void addToJsonState(JsonObject& root) * See void addToJsonState(JsonObject& root)
*/ */
void readFromJsonState(JsonObject& root) { void readFromJsonState(JsonObject& root) {
// The call to serializeConfig() must be done in the main loop, if (!initDone) return; // prevent crash on boot applyPreset()
// so we set a flag to signal the main loop to save state. JsonObject staircase = root[FPSTR(_name)];
saveState = readSettingsFromJson(root); if (!staircase.isNull()) {
readSensorsFromJson(root); if (staircase[FPSTR(_enabled)].is<bool>()) {
Serial.println("Staircase config read from API."); enabled = staircase[FPSTR(_enabled)].as<bool>();
} else {
String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on
enabled = (bool)(str!="off"); // off is guaranteed to be present
}
readSensorsFromJson(root);
DEBUG_PRINTLN(F("Staircase sensor state read from API."));
}
} }
/* /*
* Writes the configuration to internal flash memory. * Writes the configuration to internal flash memory.
*/ */
void addToConfig(JsonObject& root) { void addToConfig(JsonObject& root) {
writeSettingsToJson(root); JsonObject staircase = root[FPSTR(_name)];
Serial.println("Staircase config saved."); if (staircase.isNull()) {
staircase = root.createNestedObject(FPSTR(_name));
}
staircase[FPSTR(_enabled)] = enabled;
staircase[FPSTR(_segmentDelay)] = segment_delay_ms;
staircase[FPSTR(_onTime)] = on_time_ms / 1000;
staircase[FPSTR(_useTopUltrasoundSensor)] = useUSSensorTop;
staircase[FPSTR(_topPIRorTrigger_pin)] = topPIRorTriggerPin;
staircase[FPSTR(_topEcho_pin)] = useUSSensorTop ? topEchoPin : -1;
staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom;
staircase[FPSTR(_bottomPIRorTrigger_pin)] = bottomPIRorTriggerPin;
staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1;
staircase[FPSTR(_topEchoTime)] = topMaxTimeUs;
staircase[FPSTR(_bottomEchoTime)] = bottomMaxTimeUs;
DEBUG_PRINTLN(F("Staircase config saved."));
} }
/* /*
* Reads the configuration to internal flash memory before setup() is called. * Reads the configuration to internal flash memory before setup() is called.
*/ */
void readFromConfig(JsonObject& root) { void readFromConfig(JsonObject& root) {
readSettingsFromJson(root); bool oldUseUSSensorTop = useUSSensorTop;
Serial.println("Staircase config loaded."); bool oldUseUSSensorBottom = useUSSensorBottom;
int8_t oldTopAPin = topPIRorTriggerPin;
int8_t oldTopBPin = topEchoPin;
int8_t oldBottomAPin = bottomPIRorTriggerPin;
int8_t oldBottomBPin = bottomEchoPin;
JsonObject staircase = root[FPSTR(_name)];
if (!staircase.isNull()) {
if (staircase[FPSTR(_enabled)].is<bool>()) {
enabled = staircase[FPSTR(_enabled)].as<bool>();
} else {
String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on
enabled = (bool)(str!="off"); // off is guaranteed to be present
}
segment_delay_ms = min(10000,max(10,staircase[FPSTR(_segmentDelay)].as<int>())); // max delay 10s
on_time_ms = min(900,max(10,staircase[FPSTR(_onTime)].as<int>())) * 1000; // min 10s, max 15min
if (staircase[FPSTR(_useTopUltrasoundSensor)].is<bool>()) {
useUSSensorTop = staircase[FPSTR(_useTopUltrasoundSensor)].as<bool>();
} else {
String str = staircase[FPSTR(_useTopUltrasoundSensor)]; // checkbox -> off or on
useUSSensorTop = (bool)(str!="off"); // off is guaranteed to be present
}
topPIRorTriggerPin = min(39,max(-1,staircase[FPSTR(_topPIRorTrigger_pin)].as<int>()));
topEchoPin = min(39,max(-1,staircase[FPSTR(_topEcho_pin)].as<int>()));
if (staircase[FPSTR(_useBottomUltrasoundSensor)].is<bool>()) {
useUSSensorBottom = staircase[FPSTR(_useBottomUltrasoundSensor)].as<bool>();
} else {
String str = staircase[FPSTR(_useBottomUltrasoundSensor)]; // checkbox -> off or on
useUSSensorBottom = (bool)(str!="off"); // off is guaranteed to be present
}
bottomPIRorTriggerPin = min(39,max(-1,staircase[FPSTR(_bottomPIRorTrigger_pin)].as<int>()));
bottomEchoPin = min(39,max(-1,staircase[FPSTR(_bottomEcho_pin)].as<int>()));
topMaxTimeUs = min(18000,max(300,staircase[FPSTR(_topEchoTime)].as<int>())); // max distnace ~3m (a noticable lag of 18ms may be expected)
bottomMaxTimeUs = min(18000,max(300,staircase[FPSTR(_bottomEchoTime)].as<int>())); // max distance ~3m (a noticable lag of 18ms may be expected)
DEBUG_PRINTLN(F("Staircase config (re)loaded."));
} else {
DEBUG_PRINTLN(F("No config found. (Using defaults.)"));
}
if (!initDone) {
// first run: reading from cfg.json
} else {
// changing paramters from settings page
bool changed = false;
if ((oldUseUSSensorTop != useUSSensorTop) ||
(oldUseUSSensorBottom != useUSSensorBottom) ||
(oldTopAPin != topPIRorTriggerPin) ||
(oldTopBPin != topEchoPin) ||
(oldBottomAPin != bottomPIRorTriggerPin) ||
(oldBottomBPin != bottomEchoPin)) {
changed = true;
pinManager.deallocatePin(oldTopAPin);
pinManager.deallocatePin(oldTopBPin);
pinManager.deallocatePin(oldBottomAPin);
pinManager.deallocatePin(oldBottomBPin);
}
if (changed) setup();
}
} }
/* /*
@ -405,23 +454,33 @@ class Animated_Staircase : public Usermod {
} }
if (enabled) { if (enabled) {
JsonArray usermodEnabled = JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase enabled")); // name
staircase.createNestedArray("Staircase enabled"); // name
usermodEnabled.add("yes"); // value usermodEnabled.add("yes"); // value
JsonArray segmentDelay = JsonArray segmentDelay = staircase.createNestedArray(F("Delay between stairs")); // name
staircase.createNestedArray("Delay between stairs"); // name
segmentDelay.add(segment_delay_ms); // value segmentDelay.add(segment_delay_ms); // value
segmentDelay.add(" milliseconds"); // unit segmentDelay.add("ms"); // unit
JsonArray onTime = JsonArray onTime = staircase.createNestedArray(F("Power-off stairs after")); // name
staircase.createNestedArray("Power-off stairs after"); // name
onTime.add(on_time_ms / 1000); // value onTime.add(on_time_ms / 1000); // value
onTime.add(" seconds"); // unit onTime.add("s"); // unit
} else { } else {
JsonArray usermodEnabled = JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase enabled")); // name
staircase.createNestedArray("Staircase enabled"); // name
usermodEnabled.add("no"); // value usermodEnabled.add("no"); // value
} }
} }
}; };
// strings to reduce flash memory usage (used more than twice)
const char Animated_Staircase::_name[] PROGMEM = "staircase";
const char Animated_Staircase::_enabled[] PROGMEM = "enabled";
const char Animated_Staircase::_segmentDelay[] PROGMEM = "segment-delay-ms";
const char Animated_Staircase::_onTime[] PROGMEM = "on-time-s";
const char Animated_Staircase::_useTopUltrasoundSensor[] PROGMEM = "useTopUltrasoundSensor";
const char Animated_Staircase::_topPIRorTrigger_pin[] PROGMEM = "topPIRorTrigger_pin";
const char Animated_Staircase::_topEcho_pin[] PROGMEM = "topEcho_pin";
const char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = "useBottomUltrasoundSensor";
const char Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIRorTrigger_pin";
const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEcho_pin";
const char Animated_Staircase::_topEchoTime[] PROGMEM = "top-echo-us";
const char Animated_Staircase::_bottomEchoTime[] PROGMEM = "bottom-echo-us";

View File

@ -1,21 +0,0 @@
/*
* 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

@ -20,44 +20,10 @@ Edit `usermods_list.cpp`:
2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file 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. 3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function.
Edit `Animated_Staircase_config.h`: You can configure usermod using Usermods settings page.
1. Open `usermods/Animated_Staircase/Animated_Staircase_config.h` Please enter GPIO pins for PIR sensors or ultrasonic sensor (trigger and echo).
2. To use PIR sensors, change these lines to match your setup: If you use PIR sensor enter -1 for echo pin.
Using D7 and D6 pin notation as used on several boards: Maximum distance for ultrasonic sensor can be configured as a time needed for echo (see below).
```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 ## Hardware installation
1. Stick the LED strip under each step of the stairs. 1. Stick the LED strip under each step of the stairs.
@ -201,3 +167,7 @@ curl -X POST -H "Content-Type: application/json" \
Have fun with this usermod.<br/> Have fun with this usermod.<br/>
www.rolfje.com www.rolfje.com
## Change log
2021-04
* Adaptation for runtime configuration.

View File

@ -10,27 +10,14 @@ The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wik
## Webinterface ## Webinterface
The info page in the web interface shows the items below The info page in the web interface shows the items below
- the state of the sensor. By clicking on the state the sensor can be deactivated/activated. Changes persist after a reboot.
**I recommend to deactivate the sensor before an OTA update and activate it again afterwards**.
- the remaining time of the off timer. - the remaining time of the off timer.
**I recommend to deactivate the sensor before an OTA update and activate it again afterwards**.
## JSON API
The usermod supports the following state changes:
| JSON key | Value range | Description |
|------------|-------------|---------------------------------|
| PIRenabled | bool | Deactivdate/activate the sensor |
| PIRoffSec | 60 to 43200 | Off timer seconds |
Changes also persist after a reboot.
## Sensor connection ## Sensor connection
My setup uses an HC-SR501 sensor, a HC-SR505 should also work. My setup uses an HC-SR501 sensor, a HC-SR505 should also work.
The usermod uses GPIO13 (D1 mini pin D7) for the sensor signal. The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal but can be changed in the Usermod settings page.
[This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor. [This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor.
Use the potentiometers on the sensor to set the time-delay to the minimum and the sensitivity to about half, or slightly above. Use the potentiometers on the sensor to set the time-delay to the minimum and the sensitivity to about half, or slightly above.
@ -76,8 +63,6 @@ void registerUsermods()
## API to enable/disable the PIR sensor from outside. For example from another usermod. ## API to enable/disable the PIR sensor from outside. For example from another usermod.
The class provides the static method `PIRsensorSwitch* PIRsensorSwitch::GetInstance()` to get a pointer to the usermod object.
To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available.
### There are two options to get access to the usermod instance: ### There are two options to get access to the usermod instance:
@ -98,12 +83,19 @@ class MyUsermod : public Usermod {
//... //...
void togglePIRSensor() { void togglePIRSensor() {
if (PIRsensorSwitch::GetInstance() != nullptr) { #ifdef USERMOD_PIR_SENSOR_SWITCH
PIRsensorSwitch::GetInstance()->EnablePIRsensor(!PIRsensorSwitch::GetInstance()->PIRsensorEnabled()); PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH);
if (PIRsensor != nullptr) {
PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled());
} }
#endif
} }
//... //...
}; };
``` ```
Have fun - @gegu Have fun - @gegu
## Change log
2021-04
* Adaptation for runtime configuration.

View File

@ -2,6 +2,15 @@
#include "wled.h" #include "wled.h"
#ifndef PIR_SENSOR_PIN
// compatible with QuinLED-Dig-Uno
#ifdef ARDUINO_ARCH_ESP32
#define PIR_SENSOR_PIN 23 // Q4
#else //ESP8266 boards
#define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini)
#endif
#endif
/* /*
* This usermod handles PIR sensor states. * This usermod handles PIR sensor states.
* The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH.
@ -30,24 +39,11 @@ public:
/** /**
* constructor * constructor
*/ */
PIRsensorSwitch() PIRsensorSwitch() {}
{
// set static instance pointer
PIRsensorSwitchInstance(this);
}
/** /**
* desctructor * desctructor
*/ */
~PIRsensorSwitch() ~PIRsensorSwitch() {}
{
PIRsensorSwitchInstance(nullptr, true);
;
}
/**
* return the instance pointer of the class
*/
static PIRsensorSwitch *GetInstance() { return PIRsensorSwitchInstance(); }
/** /**
* Enable/Disable the PIR sensor * Enable/Disable the PIR sensor
@ -60,19 +56,24 @@ public:
private: private:
// PIR sensor pin // PIR sensor pin
const uint8_t PIRsensorPin = 13; // D7 on D1 mini int8_t PIRsensorPin = PIR_SENSOR_PIN;
// notification mode for colorUpdated() // notification mode for colorUpdated()
const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE
// delay before switch off after the sensor state goes LOW // delay before switch off after the sensor state goes LOW
uint32_t m_switchOffDelay = 600000; uint32_t m_switchOffDelay = 600000; // 10min
// off timer start time // off timer start time
uint32_t m_offTimerStart = 0; uint32_t m_offTimerStart = 0;
// current PIR sensor pin state // current PIR sensor pin state
byte m_PIRsensorPinState = LOW; byte m_PIRsensorPinState = LOW;
// PIR sensor enabled - ISR attached // PIR sensor enabled - ISR attached
bool m_PIRenabled = true; bool m_PIRenabled = true;
// state if serializeConfig() should be called // status of initialisation
bool m_updateConfig = false; bool initDone = false;
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _switchOffDelay[];
static const char _enabled[];
/** /**
* return or change if new PIR sensor state is available * return or change if new PIR sensor state is available
@ -84,11 +85,6 @@ private:
*/ */
static void IRAM_ATTR ISR_PIRstateChange(); static void IRAM_ATTR ISR_PIRstateChange();
/**
* Set/get instance pointer
*/
static PIRsensorSwitch *PIRsensorSwitchInstance(PIRsensorSwitch *pInstance = nullptr, bool bRemoveInstance = false);
/** /**
* switch strip on/off * switch strip on/off
*/ */
@ -107,6 +103,17 @@ private:
} }
} }
void publishMqtt(const char* state)
{
//Check if MQTT Connected, otherwise it will crash the 8266
if (WLED_MQTT_CONNECTED){
char subuf[64];
strcpy(subuf, mqttDeviceTopic);
strcat_P(subuf, PSTR("/motion"));
mqtt->publish(subuf, 0, true, state);
}
}
/** /**
* Read and update PIR sensor state. * Read and update PIR sensor state.
* Initilize/reset switch off timer * Initilize/reset switch off timer
@ -121,6 +128,7 @@ private:
{ {
m_offTimerStart = 0; m_offTimerStart = 0;
switchStrip(true); switchStrip(true);
publishMqtt("on");
} }
else if (bri != 0) else if (bri != 0)
{ {
@ -143,6 +151,7 @@ private:
if (m_PIRenabled == true) if (m_PIRenabled == true)
{ {
switchStrip(false); switchStrip(false);
publishMqtt("off");
} }
m_offTimerStart = 0; m_offTimerStart = 0;
return true; return true;
@ -159,13 +168,20 @@ public:
*/ */
void setup() void setup()
{ {
// PIR Sensor mode INPUT_PULLUP // pin retrieved from cfg.json (readFromConfig()) prior to running setup()
pinMode(PIRsensorPin, INPUT_PULLUP); if (!pinManager.allocatePin(PIRsensorPin,false)) {
if (m_PIRenabled) PIRsensorPin = -1; // allocation failed
{ m_PIRenabled = false;
// assign interrupt function and set CHANGE mode DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed."));
attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); } else {
// PIR Sensor mode INPUT_PULLUP
pinMode(PIRsensorPin, INPUT_PULLUP);
if (m_PIRenabled) {
// assign interrupt function and set CHANGE mode
attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE);
}
} }
initDone = true;
} }
/** /**
@ -181,14 +197,8 @@ public:
*/ */
void loop() void loop()
{ {
if (!updatePIRsensorState()) if (!updatePIRsensorState()) {
{
handleOffTimer(); handleOffTimer();
if (m_updateConfig)
{
serializeConfig();
m_updateConfig = false;
}
} }
} }
@ -199,69 +209,70 @@ public:
*/ */
void addToJsonInfo(JsonObject &root) void addToJsonInfo(JsonObject &root)
{ {
//this code adds "u":{"&#x23F2; PIR sensor state":uiDomString} to the info object
// the value contains a button to toggle the sensor enabled/disabled
JsonObject user = root["u"]; JsonObject user = root["u"];
if (user.isNull()) if (user.isNull())
user = root.createNestedObject("u"); user = root.createNestedObject("u");
/*
JsonArray infoArr = user.createNestedArray("&#x23F2; PIR sensor state"); //name JsonArray infoArr = user.createNestedArray(F("<i class=\"icons\">&#xe08f;</i> PIR sensor state")); //name
String uiDomString = "<button class=\"btn infobtn\" onclick=\"requestJson({PIRenabled:"; String uiDomString = F("<button class=\"btn infobtn\" onclick=\"requestJson({PIRenabled:");
String sensorStateInfo; String sensorStateInfo;
// PIR sensor state // PIR sensor state
if (m_PIRenabled) if (m_PIRenabled)
{ {
uiDomString += "false"; uiDomString += "false";
sensorStateInfo = (m_PIRsensorPinState != LOW ? "active" : "inactive"); //value sensorStateInfo = (m_PIRsensorPinState != LOW ? FPSTR(F("active")) : FPSTR(F("inactive"))); //value
} }
else else
{ {
uiDomString += "true"; uiDomString += "true";
sensorStateInfo = "Disabled !"; sensorStateInfo = F("Disabled!");
} }
uiDomString += "});return false;\">"; uiDomString += F("});return false;\">");
uiDomString += sensorStateInfo; uiDomString += sensorStateInfo;
uiDomString += "</button>"; uiDomString += F("</button>");
infoArr.add(uiDomString); //value infoArr.add(uiDomString); //value
*/
//this code adds "u":{"&#x23F2; switch off timer":uiDomString} to the info object if (m_PIRenabled)
uiDomString = "&#x23F2; switch off timer<span style=\"display:block;padding-left:25px;\">\
after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
uiDomString += (m_switchOffDelay / 60000);
uiDomString += "\" onchange=\"requestJson({PIRoffSec:parseInt(this.value)*60});\">min</span>";
infoArr = user.createNestedArray(uiDomString); //name
// off timer
if (m_offTimerStart > 0)
{ {
uiDomString = ""; /*
unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; JsonArray infoArr = user.createNestedArray(F("PIR switch-off timer after")); //name
if (offSeconds >= 3600) String uiDomString = F("<input type=\"number\" min=\"1\" max=\"720\" value=\"");
uiDomString += (m_switchOffDelay / 60000);
uiDomString += F("\" onchange=\"requestJson({PIRoffSec:parseInt(this.value)*60});\">min");
infoArr.add(uiDomString);
*/
// off timer
String uiDomString = F("PIR <i class=\"icons\">&#xe325;</i>");
JsonArray infoArr = user.createNestedArray(uiDomString); // timer value
if (m_offTimerStart > 0)
{ {
uiDomString += (offSeconds / 3600); uiDomString = "";
uiDomString += " hours "; unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000;
offSeconds %= 3600; if (offSeconds >= 3600)
{
uiDomString += (offSeconds / 3600);
uiDomString += F("h ");
offSeconds %= 3600;
}
if (offSeconds >= 60)
{
uiDomString += (offSeconds / 60);
offSeconds %= 60;
}
else if (uiDomString.length() > 0)
{
uiDomString += 0;
}
if (uiDomString.length() > 0)
{
uiDomString += F("min ");
}
uiDomString += (offSeconds);
infoArr.add(uiDomString + F("s"));
} else {
infoArr.add(F("inactive"));
} }
if (offSeconds >= 60)
{
uiDomString += (offSeconds / 60);
offSeconds %= 60;
}
else if (uiDomString.length() > 0)
{
uiDomString += 0;
}
if (uiDomString.length() > 0)
{
uiDomString += " min ";
}
uiDomString += (offSeconds);
infoArr.add(uiDomString + " sec");
}
else
{
infoArr.add("inactive");
} }
} }
@ -273,8 +284,8 @@ after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
*/ */
void addToJsonState(JsonObject &root) void addToJsonState(JsonObject &root)
{ {
root["PIRenabled"] = m_PIRenabled; root[FPSTR(_enabled)] = m_PIRenabled;
root["PIRoffSec"] = (m_switchOffDelay / 1000); root[FPSTR(_switchOffDelay)] = (m_switchOffDelay / 1000);
} }
/** /**
@ -285,26 +296,40 @@ after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
*/ */
void readFromJsonState(JsonObject &root) void readFromJsonState(JsonObject &root)
{ {
if (root["PIRoffSec"] != nullptr) if (root[FPSTR(_switchOffDelay)] != nullptr) {
{ m_switchOffDelay = (1000 * max(60UL, min(43200UL, root[FPSTR(_switchOffDelay)].as<unsigned long>())));
m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as<unsigned long>()))); }
m_updateConfig = true; /*
if (root["pin"] != nullptr) {
int8_t pin = (int)root["pin"];
// check if pin is OK
if (pin != PIRsensorPin && pin>=0 && pinManager.allocatePin(pin,false)) {
// deallocate old pin
pinManager.deallocatePin(PIRsensorPin);
// PIR Sensor mode INPUT_PULLUP
pinMode(pin, INPUT_PULLUP);
if (m_PIRenabled)
{
// remove old ISR
detachInterrupt(PIRsensorPin);
// assign interrupt function and set CHANGE mode
attachInterrupt(digitalPinToInterrupt(pin), ISR_PIRstateChange, CHANGE);
newPIRsensorState(true, true);
}
PIRsensorPin = pin;
}
} }
if (root["PIRenabled"] != nullptr) if (root[FPSTR(_enabled)] != nullptr) {
{ if (root[FPSTR(_enabled)] && !m_PIRenabled && PIRsensorPin >= 0) {
if (root["PIRenabled"] && !m_PIRenabled)
{
attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE);
newPIRsensorState(true, true); newPIRsensorState(true, true);
} } else if (m_PIRenabled && PIRsensorPin >= 0) {
else if (m_PIRenabled)
{
detachInterrupt(PIRsensorPin); detachInterrupt(PIRsensorPin);
} }
m_PIRenabled = root["PIRenabled"]; m_PIRenabled = root[FPSTR(_enabled)];
m_updateConfig = true;
} }
*/
} }
/** /**
@ -312,19 +337,72 @@ after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
*/ */
void addToConfig(JsonObject &root) void addToConfig(JsonObject &root)
{ {
JsonObject top = root.createNestedObject("PIRsensorSwitch"); JsonObject top = root.createNestedObject(FPSTR(_name));
top["PIRenabled"] = m_PIRenabled; top[FPSTR(_enabled)] = m_PIRenabled;
top["PIRoffSec"] = m_switchOffDelay; top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000;
top["pin"] = PIRsensorPin;
DEBUG_PRINTLN(F("PIR config saved."));
} }
/** /**
* restore the changeable values * restore the changeable values
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
*/ */
void readFromConfig(JsonObject &root) void readFromConfig(JsonObject &root)
{ {
JsonObject top = root["PIRsensorSwitch"]; bool oldEnabled = m_PIRenabled;
m_PIRenabled = (top["PIRenabled"] != nullptr ? top["PIRenabled"] : true); int8_t oldPin = PIRsensorPin;
m_switchOffDelay = top["PIRoffSec"] | m_switchOffDelay;
JsonObject top = root[FPSTR(_name)];
if (top.isNull()) return;
if (top["pin"] != nullptr) {
PIRsensorPin = min(39,max(-1,top["pin"].as<int>())); // check bounds
}
if (top[FPSTR(_enabled)] != nullptr) {
if (top[FPSTR(_enabled)].is<bool>()) {
m_PIRenabled = top[FPSTR(_enabled)].as<bool>(); // reading from cfg.json
} else {
// change from settings page
String str = top[FPSTR(_enabled)]; // checkbox -> off or on
m_PIRenabled = (bool)(str!="off"); // off is guaranteed to be present
}
}
if (top[FPSTR(_switchOffDelay)] != nullptr) {
m_switchOffDelay = (top[FPSTR(_switchOffDelay)].as<int>() * 1000);
}
if (!initDone) {
// reading config prior to setup()
DEBUG_PRINTLN(F("PIR config loaded."));
} else {
if (oldPin != PIRsensorPin || oldEnabled != m_PIRenabled) {
if (oldEnabled) {
// remove old ISR if disabling usermod
detachInterrupt(oldPin);
}
// check if pin is OK
if (oldPin != PIRsensorPin && oldPin >= 0) {
// if we are changing pin in settings page
// deallocate old pin
pinManager.deallocatePin(oldPin);
if (pinManager.allocatePin(PIRsensorPin,false)) {
pinMode(PIRsensorPin, INPUT_PULLUP);
} else {
// allocation failed
PIRsensorPin = -1;
m_PIRenabled = false;
}
}
if (m_PIRenabled) {
attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE);
newPIRsensorState(true, true);
}
DEBUG_PRINTLN(F("PIR config (re)loaded."));
}
}
} }
/** /**
@ -355,12 +433,7 @@ void IRAM_ATTR PIRsensorSwitch::ISR_PIRstateChange()
newPIRsensorState(true, true); newPIRsensorState(true, true);
} }
PIRsensorSwitch *PIRsensorSwitch::PIRsensorSwitchInstance(PIRsensorSwitch *pInstance, bool bRemoveInstance) // strings to reduce flash memory usage (used more than twice)
{ const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch";
static PIRsensorSwitch *s_pPIRsensorSwitch = nullptr; const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled";
if (pInstance != nullptr || bRemoveInstance) const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec";
{
s_pPIRsensorSwitch = pInstance;
}
return s_pPIRsensorSwitch;
}

View File

@ -14,10 +14,10 @@ Copy the example `platformio_override.ini` to the root directory. This file sho
### Define Your Options ### Define Your Options
* `USERMOD_DALLASTEMPERATURE` - define this to have this user mod included wled00\usermods_list.cpp * `USERMOD_DALLASTEMPERATURE` - define this to have this user mod included wled00\usermods_list.cpp
* `USERMOD_DALLASTEMPERATURE_CELSIUS` - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported
* `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60 seconds
* `USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds * `USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 20 seconds
All parameters can be configured at runtime using Usermods settings page.
## Project link ## Project link
* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link
@ -41,10 +41,7 @@ default_envs = d1_mini
... ...
lib_deps = lib_deps =
... ...
#For use SSD1306 OLED display uncomment following #For Dallas sensor uncomment following line
U8g2@~2.27.3
#For Dallas sensor uncomment following 2 lines
DallasTemperature@~3.8.0
OneWire@~2.3.5 OneWire@~2.3.5
... ...
``` ```
@ -56,3 +53,5 @@ lib_deps =
* Do not report low temperatures that indicate an error to mqtt * Do not report low temperatures that indicate an error to mqtt
* Disable plugin if temperature sensor not detected * Disable plugin if temperature sensor not detected
* Report the number of seconds until the first read in the info screen instead of sensor error * Report the number of seconds until the first read in the info screen instead of sensor error
2021-04
* Adaptation for runtime configuration.

View File

@ -2,15 +2,16 @@
#include "wled.h" #include "wled.h"
#include <DallasTemperature.h> //DS18B20 //#include <DallasTemperature.h> //DS18B20
#include "OneWire.h"
//Pin defaults for QuinLed Dig-Uno //Pin defaults for QuinLed Dig-Uno if not overriden
#ifndef TEMPERATURE_PIN #ifndef TEMPERATURE_PIN
#ifdef ARDUINO_ARCH_ESP32 #ifdef ARDUINO_ARCH_ESP32
#define TEMPERATURE_PIN 18 #define TEMPERATURE_PIN 18
#else //ESP8266 boards #else //ESP8266 boards
#define TEMPERATURE_PIN 14 #define TEMPERATURE_PIN 14
#endif #endif
#endif #endif
// the frequency to check temperature, 1 minute // the frequency to check temperature, 1 minute
@ -23,16 +24,17 @@
#define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000 #define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000
#endif #endif
OneWire oneWire(TEMPERATURE_PIN);
DallasTemperature sensor(&oneWire);
class UsermodTemperature : public Usermod { class UsermodTemperature : public Usermod {
private: private:
// The device's unique 64-bit serial code stored in on-board ROM.
// Reading directly from the sensor device address is faster than bool initDone = false;
// reading from index. When reading by index, DallasTemperature OneWire *oneWire;
// must first look up the device address at the specified index. // GPIO pin used for sensor (with a default compile-time fallback)
DeviceAddress sensorDeviceAddress; int8_t temperaturePin = TEMPERATURE_PIN;
// measurement unit (true==°C, false==°F)
bool degC = true;
unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL;
// set last reading as "40 sec before boot", so first reading is taken after 20 sec // set last reading as "40 sec before boot", so first reading is taken after 20 sec
unsigned long lastMeasurement = UINT32_MAX - (USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT); unsigned long lastMeasurement = UINT32_MAX - (USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT);
// last time requestTemperatures was called // last time requestTemperatures was called
@ -42,58 +44,95 @@ class UsermodTemperature : public Usermod {
float temperature = -100; // default to -100, DS18B20 only goes down to -50C float temperature = -100; // default to -100, DS18B20 only goes down to -50C
// indicates requestTemperatures has been called but the sensor measurement is not complete // indicates requestTemperatures has been called but the sensor measurement is not complete
bool waitingForConversion = false; bool waitingForConversion = false;
// flag to indicate we have finished the first getTemperature call // flag to indicate we have finished the first readTemperature call
// allows this library to report to the user how long until the first // allows this library to report to the user how long until the first
// measurement // measurement
bool getTemperatureComplete = false; bool readTemperatureComplete = false;
// flag set at startup if DS18B20 sensor not found, avoids trying to keep getting // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting
// temperature if flashed to a board without a sensor attached // temperature if flashed to a board without a sensor attached
bool disabled = false; bool disabled = false;
void requestTemperatures() { // strings to reduce flash memory usage (used more than twice)
// there is requestTemperaturesByAddress however it static const char _name[];
// appears to do more work, static const char _enabled[];
// TODO: measure exection time difference static const char _readInterval[];
sensor.requestTemperatures();
lastTemperaturesRequest = millis(); //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013
waitingForConversion = true; int16_t readDallas() {
byte i;
byte data[2];
int16_t result; // raw data from sensor
oneWire->reset();
oneWire->write(0xCC); // skip ROM
oneWire->write(0xBE); // read (temperature) from EEPROM
for (i=0; i < 2; i++) data[i] = oneWire->read(); // first 2 bytes contain temperature
for (i=2; i < 8; i++) oneWire->read(); // read unused bytes
result = (data[1]<<8) | data[0];
result >>= 4; // 9-bit precision accurate to 1°C (/16)
if (data[1]&0x80) result |= 0xF000; // fix negative value
//if (data[0]&0x08) ++result;
oneWire->reset();
oneWire->write(0xCC); // skip ROM
oneWire->write(0x44,0); // request new temperature reading (without parasite power)
return result;
} }
void getTemperature() { void requestTemperatures() {
if (strip.isUpdating()) return; readDallas();
#ifdef USERMOD_DALLASTEMPERATURE_CELSIUS lastTemperaturesRequest = millis();
temperature = sensor.getTempC(sensorDeviceAddress); waitingForConversion = true;
#else DEBUG_PRINTLN(F("Requested temperature."));
temperature = sensor.getTempF(sensorDeviceAddress); }
#endif
void readTemperature() {
temperature = readDallas();
lastMeasurement = millis(); lastMeasurement = millis();
waitingForConversion = false; waitingForConversion = false;
getTemperatureComplete = true; readTemperatureComplete = true;
DEBUG_PRINTF("Read temperature %2.1f.\n", temperature);
}
bool findSensor() {
DEBUG_PRINTLN(F("Searching for sensor..."));
uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0};
// find out if we have DS18xxx sensor attached
oneWire->reset_search();
while (oneWire->search(deviceAddress)) {
if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {
switch (deviceAddress[0]) {
case 0x10: // DS18S20
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x3B: // DS1825
case 0x42: // DS28EA00
DEBUG_PRINTLN(F("Sensor found."));
return true;
}
}
}
return false;
} }
public: public:
void setup() { void setup() {
sensor.begin(); int retries = 10;
// pin retrieved from cfg.json (readFromConfig()) prior to running setup()
// get the unique 64-bit serial code stored in on-board ROM if (!pinManager.allocatePin(temperaturePin,false)) {
// if getAddress returns false, the sensor was not found temperaturePin = -1; // allocation failed
disabled = !sensor.getAddress(sensorDeviceAddress, 0); disabled = true;
DEBUG_PRINTLN(F("Temperature pin allocation failed."));
if (!disabled) {
DEBUG_PRINTLN(F("Dallas Temperature found"));
// set the resolution for this specific device
sensor.setResolution(sensorDeviceAddress, 9, true);
// do not block waiting for reading
sensor.setWaitForConversion(false);
// allocate pin & prevent other use
if (!pinManager.allocatePin(TEMPERATURE_PIN,false))
disabled = true;
} else { } else {
DEBUG_PRINTLN(F("Dallas Temperature not found")); if (!disabled) {
// config says we are enabled
oneWire = new OneWire(temperaturePin);
if (!oneWire->reset())
disabled = true; // resetting 1-Wire bus yielded an error
else
while ((disabled=!findSensor()) && retries--) delay(25); // try to find sensor
}
} }
initDone = true;
} }
void loop() { void loop() {
@ -104,23 +143,21 @@ class UsermodTemperature : public Usermod {
// check to see if we are due for taking a measurement // check to see if we are due for taking a measurement
// lastMeasurement will not be updated until the conversion // lastMeasurement will not be updated until the conversion
// is complete the the reading is finished // is complete the the reading is finished
if (now - lastMeasurement < USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL) return; if (now - lastMeasurement < readingInterval) return;
// we are due for a measurement, if we are not already waiting // we are due for a measurement, if we are not already waiting
// for a conversion to complete, then make a new request for temps // for a conversion to complete, then make a new request for temps
if (!waitingForConversion) if (!waitingForConversion) {
{
requestTemperatures(); requestTemperatures();
return; return;
} }
// we were waiting for a conversion to complete, have we waited log enough? // we were waiting for a conversion to complete, have we waited log enough?
if (now - lastTemperaturesRequest >= 94 /* 93.75ms per the datasheet */) if (now - lastTemperaturesRequest >= 100 /* 93.75ms per the datasheet but can be up to 750ms */) {
{ readTemperature();
getTemperature();
if (WLED_MQTT_CONNECTED) { if (WLED_MQTT_CONNECTED) {
char subuf[45]; char subuf[64];
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
if (-100 <= temperature) { if (-100 <= temperature) {
// dont publish super low temperature as the graph will get messed up // dont publish super low temperature as the graph will get messed up
@ -128,6 +165,8 @@ class UsermodTemperature : public Usermod {
// reading the sensor // reading the sensor
strcat_P(subuf, PSTR("/temperature")); strcat_P(subuf, PSTR("/temperature"));
mqtt->publish(subuf, 0, true, String(temperature).c_str()); mqtt->publish(subuf, 0, true, String(temperature).c_str());
strcat_P(subuf, PSTR("_f"));
mqtt->publish(subuf, 0, true, String((float)temperature * 1.8f + 32).c_str());
} else { } else {
// publish something else to indicate status? // publish something else to indicate status?
} }
@ -135,16 +174,32 @@ class UsermodTemperature : public Usermod {
} }
} }
/*
* API calls te enable data exchange between WLED modules
*/
inline float getTemperatureC() {
return (float)temperature;
}
inline float getTemperatureF() {
return (float)temperature * 1.8f + 32;
}
/*
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
* Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI.
* Below it is shown how this could be used for e.g. a light sensor
*/
void addToJsonInfo(JsonObject& root) { void addToJsonInfo(JsonObject& root) {
// dont add temperature to info if we are disabled // dont add temperature to info if we are disabled
if (disabled) return; if (disabled) return;
JsonObject user = root[F("u")]; JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject(F("u")); if (user.isNull()) user = root.createNestedObject("u");
JsonArray temp = user.createNestedArray(F("Temperature")); JsonArray temp = user.createNestedArray(FPSTR(_name));
//temp.add(F("Loaded."));
if (!getTemperatureComplete) { if (!readTemperatureComplete) {
// if we haven't read the sensor yet, let the user know // if we haven't read the sensor yet, let the user know
// that we are still waiting for the first measurement // that we are still waiting for the first measurement
temp.add((USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT - millis()) / 1000); temp.add((USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT - millis()) / 1000);
@ -158,12 +213,85 @@ class UsermodTemperature : public Usermod {
return; return;
} }
temp.add(temperature); temp.add(degC ? temperature : (float)temperature * 1.8f + 32);
#ifdef USERMOD_DALLASTEMPERATURE_CELSIUS if (degC) temp.add(F("°C"));
temp.add(F("°C")); else temp.add(F("°F"));
#else }
temp.add(F("°F"));
#endif /**
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
//void addToJsonState(JsonObject &root)
//{
//}
/**
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
* Read "<usermodname>_<usermodparam>" from json state and and change settings (i.e. GPIO pin) used.
*/
//void readFromJsonState(JsonObject &root) {
// if (!initDone) return; // prevent crash on boot applyPreset()
//}
/**
* addToConfig() (called from set.cpp) stores persistent properties to cfg.json
*/
void addToConfig(JsonObject &root) {
// we add JSON object: {"Temperature": {"pin": 0, "degC": true}}
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
top[FPSTR(_enabled)] = !disabled;
top["pin"] = temperaturePin; // usermodparam
top["degC"] = degC; // usermodparam
top[FPSTR(_readInterval)] = readingInterval / 1000;
DEBUG_PRINTLN(F("Temperature config saved."));
}
/**
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
*/
void readFromConfig(JsonObject &root) {
// we look for JSON object: {"Temperature": {"pin": 0, "degC": true}}
JsonObject top = root[FPSTR(_name)];
int8_t newTemperaturePin = temperaturePin;
if (!top.isNull() && top["pin"] != nullptr) {
if (top[FPSTR(_enabled)].is<bool>()) {
disabled = !top[FPSTR(_enabled)].as<bool>();
} else {
String str = top[FPSTR(_enabled)]; // checkbox -> off or on
disabled = (bool)(str=="off"); // off is guaranteed to be present
}
newTemperaturePin = min(39,max(-1,top["pin"].as<int>()));
if (top["degC"].is<bool>()) {
// reading from cfg.json
degC = top["degC"].as<bool>();
} else {
// new configuration from set.cpp
String str = top["degC"]; // checkbox -> off or on
degC = (bool)(str!="off"); // off is guaranteed to be present
}
readingInterval = min(120,max(10,top[FPSTR(_readInterval)].as<int>())) * 1000; // convert to ms
DEBUG_PRINTLN(F("Temperature config (re)loaded."));
} else {
DEBUG_PRINTLN(F("No config found. (Using defaults.)"));
}
if (!initDone) {
// first run: reading from cfg.json
temperaturePin = newTemperaturePin;
} else {
// changing paramters from settings page
if (newTemperaturePin != temperaturePin) {
// deallocate pin and release memory
delete oneWire;
pinManager.deallocatePin(temperaturePin);
temperaturePin = newTemperaturePin;
// initialise
setup();
}
}
} }
uint16_t getId() uint16_t getId()
@ -171,3 +299,8 @@ class UsermodTemperature : public Usermod {
return USERMOD_ID_TEMPERATURE; return USERMOD_ID_TEMPERATURE;
} }
}; };
// strings to reduce flash memory usage (used more than twice)
const char UsermodTemperature::_name[] PROGMEM = "Temperature";
const char UsermodTemperature::_enabled[] PROGMEM = "enabled";
const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s";

View File

@ -0,0 +1,55 @@
# Multi Relay
This usermod-v2 modification allows the connection of multiple relays each with individual delay and on/off mode.
## Usermod installation
1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`.
or
2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY`in your platformio.ini
You can override the default maximum number (4) of relays by defining MULTI_RELAY_MAX_RELAYS.
Example **usermods_list.cpp**:
```cpp
#include "wled.h"
/*
* Register your v2 usermods here!
* (for v1 usermods using just usermod.cpp, you can ignore this file)
*/
/*
* Add/uncomment your usermod filename here (and once more below)
* || || ||
* \/ \/ \/
*/
//#include "usermod_v2_example.h"
//#include "usermod_temperature.h"
#include "../usermods/usermod_multi_relay.h"
void registerUsermods()
{
/*
* Add your usermod class name here
* || || ||
* \/ \/ \/
*/
//usermods.add(new MyExampleUsermod());
//usermods.add(new UsermodTemperature());
usermods.add(new MultiRelay());
}
```
## Configuration
Usermod can be configured in Usermods settings page.
If there is no MultiRelay section, just save current configuration and re-open Usermods settings page.
Have fun - @blazoncek
## Change log
2021-04
* First implementation.

View File

@ -0,0 +1,434 @@
#pragma once
#include "wled.h"
#ifndef MULTI_RELAY_MAX_RELAYS
#define MULTI_RELAY_MAX_RELAYS 4
#endif
#define ON true
#define OFF false
/*
* This usermod handles multiple relay outputs.
* These outputs complement built-in relay output in a way that the activation can be delayed.
* They can also activate/deactivate in reverse logic independently.
*/
typedef struct relay_t {
int8_t pin;
bool active;
bool mode;
bool state;
bool external;
uint16_t delay;
} Relay;
class MultiRelay : public Usermod {
private:
// array of relays
Relay _relay[MULTI_RELAY_MAX_RELAYS];
// switch timer start time
uint32_t _switchTimerStart = 0;
// old brightness
uint8_t _oldBrightness = 0;
// usermod enabled
bool enabled = false; // needs to be configured (no default config)
// status of initialisation
bool initDone = false;
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _enabled[];
static const char _relay_str[];
static const char _delay_str[];
static const char _activeHigh[];
static const char _external[];
void publishMqtt(const char* state, int relay) {
//Check if MQTT Connected, otherwise it will crash the 8266
if (WLED_MQTT_CONNECTED){
char subuf[64];
sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay);
mqtt->publish(subuf, 0, true, state);
}
}
/**
* switch off the strip if the delay has elapsed
*/
void handleOffTimer() {
bool activeRelays = false;
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].active && _switchTimerStart > 0 && millis() - _switchTimerStart > (_relay[i].delay*1000)) {
if (!_relay[i].external) toggleRelay(i);
_relay[i].active = false;
}
activeRelays = activeRelays || _relay[i].active;
}
if (!activeRelays) _switchTimerStart = 0;
}
/**
* HTTP API handler
* borrowed from:
* https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h
*/
#define GEOGABVERSION "0.1.3"
void InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer
DEBUG_PRINTLN(F("Relays: Initialize HTML API"));
server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) {
DEBUG_PRINTLN("Relays: HTML API");
String janswer;
String error = "";
int params = request->params();
janswer = F("{\"NoOfRelays\":");
janswer += String(MULTI_RELAY_MAX_RELAYS) + ",";
if (getActiveRelayCount()) {
// Commands
if(request->hasParam("switch")) {
/**** Switch ****/
AsyncWebParameter* p = request->getParam("switch");
// Get Values
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
int value = getValue(p->value(), ',', i);
if (value==-1) {
error = F("There must be as much arugments as relays");
} else {
// Switch
if (_relay[i].external) switchRelay(i, (bool)value);
}
}
} else if(request->hasParam("toggle")) {
/**** Toggle ****/
AsyncWebParameter* p = request->getParam("toggle");
// Get Values
for (int i=0;i<MULTI_RELAY_MAX_RELAYS;i++) {
int value = getValue(p->value(), ',', i);
if (value==-1) {
error = F("There must be as mutch arugments as relays");
} else {
// Toggle
if (value && _relay[i].external) toggleRelay(i);
}
}
} else {
error = F("No valid command found");
}
} else {
error = F("No active relays");
}
// Status response
char sbuf[16];
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
sprintf_P(sbuf, PSTR("\"%d\":%d,"), i, (_relay[i].pin<0 ? -1 : (int)_relay[i].state));
janswer += sbuf;
}
janswer += F("\"error\":\"");
janswer += error;
janswer += F("\",");
janswer += F("\"SW Version\":\"");
janswer += String(GEOGABVERSION);
janswer += F("\"}");
request->send(200, "application/json", janswer);
});
}
int getValue(String data, char separator, int index) {
int found = 0;
int strIndex[] = {0, -1};
int maxIndex = data.length()-1;
for(int i=0; i<=maxIndex && found<=index; i++){
if(data.charAt(i)==separator || i==maxIndex){
found++;
strIndex[0] = strIndex[1]+1;
strIndex[1] = (i == maxIndex) ? i+1 : i;
}
}
return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1;
}
public:
/**
* constructor
*/
MultiRelay() {
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
_relay[i].pin = -1;
_relay[i].delay = 0;
_relay[i].mode = false;
_relay[i].active = false;
_relay[i].state = false;
_relay[i].external = false;
}
}
/**
* desctructor
*/
~MultiRelay() {}
/**
* Enable/Disable the usermod
*/
inline void enable(bool enable) { enabled = enable; }
/**
* Get usermod enabled/disabled state
*/
inline bool isEnabled() { return enabled; }
/**
* switch relay on/off
*/
void switchRelay(uint8_t relay, bool mode) {
if (relay>=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return;
_relay[relay].state = mode;
pinMode(_relay[relay].pin, OUTPUT);
digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode);
publishMqtt(mode ? "on" : "off", relay);
}
/**
* toggle relay
*/
inline void toggleRelay(uint8_t relay) {
switchRelay(relay, !_relay[relay].state);
}
uint8_t getActiveRelayCount() {
uint8_t count = 0;
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) if (_relay[i].pin>=0) count++;
return count;
}
//Functions called by WLED
/**
* handling of MQTT message
* topic only contains stripped topic (part after /wled/MAC)
* topic should look like: /relay/X/command; where X is relay number, 0 based
*/
bool onMqttMessage(char* topic, char* payload) {
if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) {
uint8_t relay = strtoul(topic+7, NULL, 10);
if (relay<MULTI_RELAY_MAX_RELAYS) {
String action = payload;
if (action == "on") {
if (_relay[relay].external) switchRelay(relay, true);
return true;
} else if (action == "off") {
if (_relay[relay].external) switchRelay(relay, false);
return true;
} else if (action == "toggle") {
if (_relay[relay].external) toggleRelay(relay);
return true;
}
}
}
return false;
}
/**
* subscribe to MQTT topic for controlling relays
*/
void onMqttConnect(bool sessionPresent) {
//(re)subscribe to required topics
char subuf[64];
if (mqttDeviceTopic[0] != 0) {
strcpy(subuf, mqttDeviceTopic);
strcat_P(subuf, PSTR("/relay/#"));
mqtt->subscribe(subuf, 0);
}
}
/**
* setup() is called once at boot. WiFi is not yet connected at this point.
* You can use it to initialize variables, sensors or similar.
*/
void setup() {
// pins retrieved from cfg.json (readFromConfig()) prior to running setup()
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].pin<0) continue;
if (!pinManager.allocatePin(_relay[i].pin,true)) {
_relay[i].pin = -1; // allocation failed
} else {
switchRelay(i, _relay[i].state = (bool)bri);
_relay[i].active = false;
}
}
_oldBrightness = bri;
initDone = true;
}
/**
* connected() is called every time the WiFi is (re)connected
* Use it to initialize network interfaces
*/
void connected() {
InitHtmlAPIHandle();
}
/**
* loop() is called continuously. Here you can check for events, read sensors, etc.
*/
void loop() {
if (!enabled) return;
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate < 200) return; // update only 5 times/s
lastUpdate = millis();
//set relay when LEDs turn on
if (_oldBrightness != bri) {
_oldBrightness = bri;
_switchTimerStart = millis();
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].pin>=0) _relay[i].active = true;
}
}
handleOffTimer();
}
/**
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
*/
void addToJsonInfo(JsonObject &root) {
if (enabled) {
JsonObject user = root["u"];
if (user.isNull())
user = root.createNestedObject("u");
JsonArray infoArr = user.createNestedArray(F("Number of relays")); //name
infoArr.add(String(getActiveRelayCount()));
}
}
/**
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void addToJsonState(JsonObject &root) {
}
/**
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients
*/
void readFromJsonState(JsonObject &root) {
}
/**
* provide the changeable values
*/
void addToConfig(JsonObject &root) {
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
String parName = FPSTR(_relay_str); parName += "-"; parName += i; parName += "-";
top[parName+"pin"] = _relay[i].pin;
top[parName+FPSTR(_activeHigh)] = _relay[i].mode;
top[parName+FPSTR(_delay_str)] = _relay[i].delay;
top[parName+FPSTR(_external)] = _relay[i].external;
}
DEBUG_PRINTLN(F("MultiRelay config saved."));
}
/**
* restore the changeable values
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
*/
void readFromConfig(JsonObject &root) {
int8_t oldPin[MULTI_RELAY_MAX_RELAYS];
JsonObject top = root[FPSTR(_name)];
if (top.isNull()) return;
if (top[FPSTR(_enabled)] != nullptr) {
if (top[FPSTR(_enabled)].is<bool>()) {
enabled = top[FPSTR(_enabled)].as<bool>(); // reading from cfg.json
} else {
// change from settings page
String str = top[FPSTR(_enabled)]; // checkbox -> off or on
enabled = (bool)(str!="off"); // off is guaranteed to be present
}
}
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
String parName = FPSTR(_relay_str); parName += "-"; parName += i; parName += "-";
oldPin[i] = _relay[i].pin;
if (top[parName+"pin"] != nullptr) _relay[i].pin = min(39,max(-1,top[parName+"pin"].as<int>()));
if (top[parName+FPSTR(_activeHigh)] != nullptr) {
if (top[parName+FPSTR(_activeHigh)].is<bool>()) {
_relay[i].mode = top[parName+FPSTR(_activeHigh)].as<bool>(); // reading from cfg.json
} else {
// change from settings page
String str = top[parName+FPSTR(_activeHigh)]; // checkbox -> off or on
_relay[i].mode = (bool)(str!="off"); // off is guaranteed to be present
}
}
if (top[parName+FPSTR(_external)] != nullptr) {
if (top[parName+FPSTR(_external)].is<bool>()) {
_relay[i].external = top[parName+FPSTR(_external)].as<bool>(); // reading from cfg.json
} else {
// change from settings page
String str = top[parName+FPSTR(_external)]; // checkbox -> off or on
_relay[i].external = (bool)(str!="off"); // off is guaranteed to be present
}
}
_relay[i].delay = min(600,max(0,abs(top[parName+FPSTR(_delay_str)].as<int>())));
}
if (!initDone) {
// reading config prior to setup()
DEBUG_PRINTLN(F("MultiRelay config loaded."));
} else {
// deallocate all pins 1st
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++)
if (oldPin[i]>=0) {
pinManager.deallocatePin(oldPin[i]);
}
// allocate new pins
for (uint8_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].pin>=0 && pinManager.allocatePin(_relay[i].pin,true)) {
if (!_relay[i].external) switchRelay(i, _relay[i].state = (bool)bri);
} else {
_relay[i].pin = -1;
}
_relay[i].active = false;
}
DEBUG_PRINTLN(F("MultiRelay config (re)loaded."));
}
}
/**
* getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
* This could be used in the future for the system to determine whether your usermod is installed.
*/
uint16_t getId()
{
return USERMOD_ID_MULTI_RELAY;
}
};
// strings to reduce flash memory usage (used more than twice)
const char MultiRelay::_name[] PROGMEM = "MultiRelay";
const char MultiRelay::_enabled[] PROGMEM = "enabled";
const char MultiRelay::_relay_str[] PROGMEM = "relay";
const char MultiRelay::_delay_str[] PROGMEM = "delay-s";
const char MultiRelay::_activeHigh[] PROGMEM = "active-high";
const char MultiRelay::_external[] PROGMEM = "external";

View File

@ -29,9 +29,9 @@ This file should be placed in the same directory as `platformio.ini`.
### Define Your Options ### Define Your Options
* `USERMOD_AUTO_SAVE` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp * `USERMOD_AUTO_SAVE` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp
* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) * `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details)
* `AUTOSAVE_SETTLE_MS` - Minimum time to wave before auto saving, defaults to 10000 (10s)
* `AUTOSAVE_PRESET_NUM` - Preset number to auto-save to, auto-load at startup from, defaults to 99 You can configure auto-save parameters using Usermods settings page.
### PlatformIO requirements ### PlatformIO requirements
@ -43,3 +43,5 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`.
2021-02 2021-02
* First public release * First public release
2021-04
* Adaptation for runtime configuration.

View File

@ -2,9 +2,8 @@
#include "wled.h" #include "wled.h"
//
// v2 Usermod to automatically save settings // v2 Usermod to automatically save settings
// to preset number AUTOSAVE_PRESET_NUM after a change to any of // to configurable preset after a change to any of
// //
// * brightness // * brightness
// * effect speed // * effect speed
@ -12,45 +11,34 @@
// * mode (effect) // * mode (effect)
// * palette // * palette
// //
// but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" // but it will wait for configurable number of seconds, a "settle"
// period in case there are other changes (any change will // period in case there are other changes (any change will
// extend the "settle" window). // extend the "settle" window).
// //
// It will additionally load preset AUTOSAVE_PRESET_NUM at startup. // It can be configured to load auto saved preset at startup,
// during the first `loop()`. Reasoning below. // during the first `loop()`.
// //
// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod // AutoSaveUsermod is standalone, but if FourLineDisplayUsermod
// is installed, it will notify the user of the saved changes. // is installed, it will notify the user of the saved changes.
//
// Note: I don't love that WLED doesn't respect the brightness
// of the preset being auto loaded, so the AutoSaveUsermod
// will set the AUTOSAVE_PRESET_NUM preset in the first loop,
// so brightness IS honored. This means WLED will effectively
// ignore Default brightness and Apply N preset at boot when
// the AutoSaveUsermod is installed.
//How long to wait after settings change to auto-save // format: "~ MM-DD HH:MM:SS ~"
#ifndef AUTOSAVE_SETTLE_MS
#define AUTOSAVE_SETTLE_MS 10*1000
#endif
//Preset number to save to
#ifndef AUTOSAVE_PRESET_NUM
#define AUTOSAVE_PRESET_NUM 99
#endif
// "Auto save MM-DD HH:MM:SS"
#define PRESET_NAME_BUFFER_SIZE 25 #define PRESET_NAME_BUFFER_SIZE 25
class AutoSaveUsermod : public Usermod { class AutoSaveUsermod : public Usermod {
private:
// If we've detected the need to auto save, this will
// be non zero.
unsigned long autoSaveAfter = 0;
char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; private:
bool firstLoop = true; bool firstLoop = true;
bool initDone = false;
bool enabled = true;
// configurable parameters
unsigned long autoSaveAfterSec = 15; // 15s by default
uint8_t autoSavePreset = 250; // last possible preset
bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot?
// If we've detected the need to auto save, this will be non zero.
unsigned long autoSaveAfter = 0;
uint8_t knownBrightness = 0; uint8_t knownBrightness = 0;
uint8_t knownEffectSpeed = 0; uint8_t knownEffectSpeed = 0;
@ -58,35 +46,65 @@ class AutoSaveUsermod : public Usermod {
uint8_t knownMode = 0; uint8_t knownMode = 0;
uint8_t knownPalette = 0; uint8_t knownPalette = 0;
#ifdef USERMOD_FOUR_LINE_DISLAY #ifdef USERMOD_FOUR_LINE_DISPLAY
FourLineDisplayUsermod* display; FourLineDisplayUsermod* display;
#endif #endif
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _autoSaveEnabled[];
static const char _autoSaveAfterSec[];
static const char _autoSavePreset[];
static const char _autoSaveApplyOnBoot[];
void inline saveSettings() {
char presetNameBuffer[PRESET_NAME_BUFFER_SIZE];
updateLocalTime();
sprintf_P(presetNameBuffer,
PSTR("~ %02d-%02d %02d:%02d:%02d ~"),
month(localTime), day(localTime),
hour(localTime), minute(localTime), second(localTime));
savePreset(autoSavePreset, true, presetNameBuffer);
}
void inline displayOverlay() {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display != nullptr) {
display->wakeDisplay();
display->overlay("Settings", "Auto Saved", 1500);
}
#endif
}
public: public:
// gets called once at boot. Do all initialization that doesn't depend on // gets called once at boot. Do all initialization that doesn't depend on
// network here // network here
void setup() { void setup() {
#ifdef USERMOD_FOUR_LINE_DISLAY #ifdef USERMOD_FOUR_LINE_DISPLAY
// This Usermod has enhanced funcionality if // This Usermod has enhanced funcionality if
// FourLineDisplayUsermod is available. // FourLineDisplayUsermod is available.
display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP);
#endif #endif
initDone = true;
} }
// gets called every time WiFi is (re-)connected. Initialize own network // gets called every time WiFi is (re-)connected. Initialize own network
// interfaces here // interfaces here
void connected() {} void connected() {}
/** /*
* Da loop. * Da loop.
*/ */
void loop() { void loop() {
if (!autoSaveAfterSec || !enabled) return; // setting 0 as autosave seconds disables autosave
unsigned long now = millis(); unsigned long now = millis();
uint8_t currentMode = strip.getMode(); uint8_t currentMode = strip.getMode();
uint8_t currentPalette = strip.getSegment(0).palette; uint8_t currentPalette = strip.getSegment(0).palette;
if (firstLoop) { if (firstLoop) {
firstLoop = false; firstLoop = false;
applyPreset(AUTOSAVE_PRESET_NUM); if (applyAutoSaveOnBoot) applyPreset(autoSavePreset);
knownBrightness = bri; knownBrightness = bri;
knownEffectSpeed = effectSpeed; knownEffectSpeed = effectSpeed;
knownEffectIntensity = effectIntensity; knownEffectIntensity = effectIntensity;
@ -95,7 +113,7 @@ class AutoSaveUsermod : public Usermod {
return; return;
} }
unsigned long wouldAutoSaveAfter = now + AUTOSAVE_SETTLE_MS; unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000;
if (knownBrightness != bri) { if (knownBrightness != bri) {
knownBrightness = bri; knownBrightness = bri;
autoSaveAfter = wouldAutoSaveAfter; autoSaveAfter = wouldAutoSaveAfter;
@ -121,37 +139,32 @@ class AutoSaveUsermod : public Usermod {
} }
} }
void saveSettings() { /*
updateLocalTime(); * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
sprintf(presetNameBuffer, * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI.
"Auto save %02d-%02d %02d:%02d:%02d", * Below it is shown how this could be used for e.g. a light sensor
month(localTime), day(localTime), */
hour(localTime), minute(localTime), second(localTime)); //void addToJsonInfo(JsonObject& root) {
savePreset(AUTOSAVE_PRESET_NUM, true, presetNameBuffer); //JsonObject user = root["u"];
} //if (user.isNull()) user = root.createNestedObject("u");
//JsonArray data = user.createNestedArray(F("Autosave"));
void displayOverlay() { //data.add(F("Loaded."));
#ifdef USERMOD_FOUR_LINE_DISLAY //}
if (display != nullptr) {
display->wakeDisplay();
display->overlay("Settings", "Auto Saved", 1500);
}
#endif
}
/* /*
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients * Values in the state object may be modified by connected clients
*/ */
void addToJsonState(JsonObject& root) { //void addToJsonState(JsonObject& root) {
} //}
/* /*
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients * Values in the state object may be modified by connected clients
*/ */
void readFromJsonState(JsonObject& root) { //void readFromJsonState(JsonObject& root) {
} // if (!initDone) return; // prevent crash on boot applyPreset()
//}
/* /*
* addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object.
@ -168,6 +181,13 @@ class AutoSaveUsermod : public Usermod {
* I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!
*/ */
void addToConfig(JsonObject& root) { void addToConfig(JsonObject& root) {
// we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}}
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
top[FPSTR(_autoSaveEnabled)] = enabled;
top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam
top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam
top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot;
DEBUG_PRINTLN(F("Autosave config saved."));
} }
/* /*
@ -179,7 +199,33 @@ class AutoSaveUsermod : public Usermod {
* If you don't know what that is, don't fret. It most likely doesn't affect your use case :) * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)
*/ */
void readFromConfig(JsonObject& root) { void readFromConfig(JsonObject& root) {
} // we look for JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}}
JsonObject top = root[FPSTR(_name)];
if (top.isNull()) {
DEBUG_PRINTLN(F("No config found. (Using defaults.)"));
return;
}
if (top[FPSTR(_autoSaveEnabled)].is<bool>()) {
// reading from cfg.json
enabled = top[FPSTR(_autoSaveEnabled)].as<bool>();
} else {
// reading from POST message
String str = top[FPSTR(_autoSaveEnabled)]; // checkbox -> off or on
enabled = (bool)(str!="off"); // off is guaranteed to be present
}
autoSaveAfterSec = min(3600,max(10,top[FPSTR(_autoSaveAfterSec)].as<int>()));
autoSavePreset = min(250,max(100,top[FPSTR(_autoSavePreset)].as<int>()));
if (top[FPSTR(_autoSaveApplyOnBoot)].is<bool>()) {
// reading from cfg.json
applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)].as<bool>();
} else {
// reading from POST message
String str = top[FPSTR(_autoSaveApplyOnBoot)]; // checkbox -> off or on
applyAutoSaveOnBoot = (bool)(str!="off"); // off is guaranteed to be present
}
DEBUG_PRINTLN(F("Autosave config (re)loaded."));
}
/* /*
* getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
@ -188,5 +234,11 @@ class AutoSaveUsermod : public Usermod {
uint16_t getId() { uint16_t getId() {
return USERMOD_ID_AUTO_SAVE; return USERMOD_ID_AUTO_SAVE;
} }
}; };
// strings to reduce flash memory usage (used more than twice)
const char AutoSaveUsermod::_name[] PROGMEM = "Autosave";
const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled";
const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec";
const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset";
const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot";

View File

@ -1,4 +1,4 @@
# Rotary Encoder UI Usermod # I2C 4 Line Display Usermod
First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod.
@ -19,13 +19,11 @@ This file should be placed in the same directory as `platformio.ini`.
### Define Your Options ### Define Your Options
* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available * `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available
* `FLD_PIN_SCL` - The display SCL pin, defaults to 5 * `FLD_PIN_SCL` - The display SCL pin, defaults to 5
* `FLD_PIN_SDA` - The display SDA pin, defaults to 4 * `FLD_PIN_SDA` - The display SDA pin, defaults to 4
* `FLIP_MODE` - Set to 0 or 1
* `LINE_HEIGHT` - Set to 1 or 2
There are other `#define` values in the Usermod that might be of interest. All of the parameters can be configured using Usermods settings page, inluding GPIO pins.
### PlatformIO requirements ### PlatformIO requirements
@ -37,3 +35,5 @@ UI usermod folder for how to include these using `platformio_override.ini`.
2021-02 2021-02
* First public release * First public release
2021-04
* Adaptation for runtime configuration.

View File

@ -24,50 +24,25 @@
// //
//The SCL and SDA pins are defined here. //The SCL and SDA pins are defined here.
#ifndef FLD_PIN_SCL #ifdef ARDUINO_ARCH_ESP32
#define FLD_PIN_SCL 5 #ifndef FLD_PIN_SCL
#endif #define FLD_PIN_SCL 22
#endif
#ifndef FLD_PIN_SDA #ifndef FLD_PIN_SDA
#define FLD_PIN_SDA 4 #define FLD_PIN_SDA 21
#endif #endif
// U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(
// U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA);
U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8(
U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA);
// Screen upside down? Change to 0 or 1
#ifndef FLIP_MODE
#define FLIP_MODE 0
#endif
// LINE_HEIGHT 1 is single height, for 128x32 displays.
// LINE_HEIGHT 2 makes the 128x64 screen display at double height.
#ifndef LINE_HEIGHT
#define LINE_HEIGHT 2
#endif
// If you aren't also including RotaryEncoderUIUsermod
// you probably want to set both
// SLEEP_MODE_ENABLED false
// CLOCK_MODE_ENABLED false
// as you will never be able wake the display / disable the clock.
#ifdef USERMOD_ROTARY_ENCODER_UI
#ifndef SLEEP_MODE_ENABLED
#define SLEEP_MODE_ENABLED true
#endif
#ifndef CLOCK_MODE_ENABLED
#define CLOCK_MODE_ENABLED true
#endif
#else #else
#define SLEEP_MODE_ENABLED false #ifndef FLD_PIN_SCL
#define CLOCK_MODE_ENABLED false #define FLD_PIN_SCL 5
#endif
#ifndef FLD_PIN_SDA
#define FLD_PIN_SDA 4
#endif
#endif #endif
// When to time out to the clock or blank the screen // When to time out to the clock or blank the screen
// if SLEEP_MODE_ENABLED. // if SLEEP_MODE_ENABLED.
#define SCREEN_TIMEOUT_MS 15*1000 #define SCREEN_TIMEOUT_MS 60*1000 // 1 min
#define TIME_INDENT 0 #define TIME_INDENT 0
#define DATE_INDENT 2 #define DATE_INDENT 2
@ -75,33 +50,43 @@ U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8(
// Minimum time between redrawing screen in ms // Minimum time between redrawing screen in ms
#define USER_LOOP_REFRESH_RATE_MS 1000 #define USER_LOOP_REFRESH_RATE_MS 1000
#if LINE_HEIGHT == 2
#define DRAW_STRING draw1x2String
#define DRAW_GLYPH draw1x2Glyph
#define DRAW_BIG_STRING draw2x2String
#else
#define DRAW_STRING drawString
#define DRAW_GLYPH drawGlyph
#define DRAW_BIG_STRING draw2x2String
#endif
// Extra char (+1) for null // Extra char (+1) for null
#define LINE_BUFFER_SIZE 16+1 #define LINE_BUFFER_SIZE 16+1
#define FLD_LINE_3_BRIGHTNESS 0
#define FLD_LINE_3_EFFECT_SPEED 1
#define FLD_LINE_3_EFFECT_INTENSITY 2
#define FLD_LINE_3_PALETTE 3
#if LINE_HEIGHT == 2 typedef enum {
#define TIME_LINE 1 FLD_LINE_4_BRIGHTNESS = 0,
#else FLD_LINE_4_EFFECT_SPEED,
#define TIME_LINE 0 FLD_LINE_4_EFFECT_INTENSITY,
#endif FLD_LINE_4_MODE,
FLD_LINE_4_PALETTE
} Line4Type;
typedef enum {
NONE = 0,
SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C
SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C
SSD1306_64 // U8X8_SSD1306_128X64_NONAME_HW_I2C
} DisplayType;
class FourLineDisplayUsermod : public Usermod { class FourLineDisplayUsermod : public Usermod {
private: private:
bool initDone = false;
unsigned long lastTime = 0; unsigned long lastTime = 0;
// HW interface & configuration
U8X8 *u8x8 = nullptr; // pointer to U8X8 display object
int8_t sclPin=FLD_PIN_SCL, sdaPin=FLD_PIN_SDA; // I2C pins for interfacing, get initialised in readFromConfig()
DisplayType type = SSD1306; // display type
bool flip = false; // flip display 180°
uint8_t contrast = 10; // screen contrast
uint8_t lineHeight = 1; // 1 row or 2 rows
uint32_t refreshRate = USER_LOOP_REFRESH_RATE_MS; // in ms
uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms
bool sleepMode = true; // allow screen sleep?
bool clockMode = false; // display clock
// needRedraw marks if redraw is required to prevent often redrawing. // needRedraw marks if redraw is required to prevent often redrawing.
bool needRedraw = true; bool needRedraw = true;
@ -118,38 +103,72 @@ class FourLineDisplayUsermod : public Usermod {
uint8_t knownHour = 99; uint8_t knownHour = 99;
bool displayTurnedOff = false; bool displayTurnedOff = false;
long lastUpdate = 0; unsigned long lastUpdate = 0;
long lastRedraw = 0; unsigned long lastRedraw = 0;
long overlayUntil = 0; unsigned long overlayUntil = 0;
byte lineThreeType = FLD_LINE_3_BRIGHTNESS; Line4Type lineFourType = FLD_LINE_4_BRIGHTNESS;
// Set to 2 or 3 to mark lines 2 or 3. Other values ignored. // Set to 2 or 3 to mark lines 2 or 3. Other values ignored.
byte markLineNum = 0; byte markLineNum = 0;
char lineBuffer[LINE_BUFFER_SIZE]; // strings to reduce flash memory usage (used more than twice)
static const char _name[];
char **modes_qstrings = nullptr; static const char _contrast[];
char **palettes_qstrings = nullptr; static const char _refreshRate[];
static const char _screenTimeOut[];
static const char _flip[];
static const char _sleepMode[];
static const char _clockMode[];
// If display does not work or looks corrupted check the // If display does not work or looks corrupted check the
// constructor reference: // constructor reference:
// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp
// or check the gallery: // or check the gallery:
// https://github.com/olikraus/u8g2/wiki/gallery // https://github.com/olikraus/u8g2/wiki/gallery
public: public:
// gets called once at boot. Do all initialization that doesn't depend on // gets called once at boot. Do all initialization that doesn't depend on
// network here // network here
void setup() { void setup() {
u8x8.begin(); if (type==NONE) return;
u8x8.setFlipMode(FLIP_MODE); if (!pinManager.allocatePin(sclPin)) { sclPin = -1; type = NONE; return;}
u8x8.setPowerSave(0); if (!pinManager.allocatePin(sdaPin)) { pinManager.deallocatePin(sclPin); sclPin = sdaPin = -1; type = NONE; return; }
u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 switch (type) {
u8x8.setFont(u8x8_font_chroma48medium8_r); case SSD1306:
u8x8.DRAW_STRING(0, 0*LINE_HEIGHT, "Loading..."); #ifdef ESP8266
if (!(sclPin==5 && sdaPin==4))
ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT); u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset
modes_qstrings = modeSortUsermod->getModesQStrings(); else
palettes_qstrings = modeSortUsermod->getPalettesQStrings(); #endif
u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA
break;
case SH1106:
#ifdef ESP8266
if (!(sclPin==5 && sdaPin==4))
u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset
else
#endif
u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA
break;
case SSD1306_64:
#ifdef ESP8266
if (!(sclPin==5 && sdaPin==4))
u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset
else
#endif
u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA
break;
default:
u8x8 = nullptr;
type = NONE;
return;
}
(static_cast<U8X8*>(u8x8))->begin();
setFlipMode(flip);
setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255
setPowerSave(0);
drawString(0, 0, "Loading...");
initDone = true;
} }
// gets called every time WiFi is (re-)connected. Initialize own network // gets called every time WiFi is (re-)connected. Initialize own network
@ -160,7 +179,7 @@ class FourLineDisplayUsermod : public Usermod {
* Da loop. * Da loop.
*/ */
void loop() { void loop() {
if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { if (millis() - lastUpdate < (clockMode?1000:refreshRate)) {
return; return;
} }
lastUpdate = millis(); lastUpdate = millis();
@ -168,18 +187,59 @@ class FourLineDisplayUsermod : public Usermod {
redraw(false); redraw(false);
} }
/**
* Wrappers for screen drawing
*/
void setFlipMode(uint8_t mode) {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->setFlipMode(mode);
}
void setContrast(uint8_t contrast) {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->setContrast(contrast);
}
void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->setFont(u8x8_font_chroma48medium8_r);
if (!ignoreLH && lineHeight==2) (static_cast<U8X8*>(u8x8))->draw1x2String(col, row, string);
else (static_cast<U8X8*>(u8x8))->drawString(col, row, string);
}
void draw2x2String(uint8_t col, uint8_t row, const char *string) {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->setFont(u8x8_font_chroma48medium8_r);
(static_cast<U8X8*>(u8x8))->draw2x2String(col, row, string);
}
void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->setFont(font);
if (!ignoreLH && lineHeight==2) (static_cast<U8X8*>(u8x8))->draw1x2Glyph(col, row, glyph);
else (static_cast<U8X8*>(u8x8))->drawGlyph(col, row, glyph);
}
uint8_t getCols() {
if (type==NONE) return 0;
return (static_cast<U8X8*>(u8x8))->getCols();
}
void clear() {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->clear();
}
void setPowerSave(uint8_t save) {
if (type==NONE) return;
(static_cast<U8X8*>(u8x8))->setPowerSave(save);
}
/** /**
* Redraw the screen (but only if things have changed * Redraw the screen (but only if things have changed
* or if forceRedraw). * or if forceRedraw).
*/ */
void redraw(bool forceRedraw) { void redraw(bool forceRedraw) {
if (type==NONE) return;
if (overlayUntil > 0) { if (overlayUntil > 0) {
if (millis() >= overlayUntil) { if (millis() >= overlayUntil) {
// Time to display the overlay has elapsed. // Time to display the overlay has elapsed.
overlayUntil = 0; overlayUntil = 0;
forceRedraw = true; forceRedraw = true;
} } else {
else {
// We are still displaying the overlay // We are still displaying the overlay
// Don't redraw. // Don't redraw.
return; return;
@ -208,22 +268,46 @@ class FourLineDisplayUsermod : public Usermod {
if (!needRedraw) { if (!needRedraw) {
// Nothing to change. // Nothing to change.
// Turn off display after 3 minutes with no change. // Turn off display after 3 minutes with no change.
if(SLEEP_MODE_ENABLED && !displayTurnedOff && if(sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) {
(millis() - lastRedraw > SCREEN_TIMEOUT_MS)) {
// We will still check if there is a change in redraw() // We will still check if there is a change in redraw()
// and turn it back on if it changed. // and turn it back on if it changed.
knownHour = 99; // force screen clear
sleepOrClock(true); sleepOrClock(true);
} } else if (displayTurnedOff && clockMode) {
else if (displayTurnedOff && CLOCK_MODE_ENABLED) {
showTime(); showTime();
} else if ((millis() - lastRedraw)/1000%3 == 0) {
// change 4th line every 3s
switch (lineFourType) {
case FLD_LINE_4_BRIGHTNESS:
setLineFourType(FLD_LINE_4_EFFECT_SPEED);
break;
case FLD_LINE_4_MODE:
setLineFourType(FLD_LINE_4_BRIGHTNESS);
break;
case FLD_LINE_4_PALETTE:
setLineFourType(clockMode ? FLD_LINE_4_MODE : FLD_LINE_4_BRIGHTNESS);
break;
case FLD_LINE_4_EFFECT_SPEED:
setLineFourType(FLD_LINE_4_EFFECT_INTENSITY);
break;
case FLD_LINE_4_EFFECT_INTENSITY:
setLineFourType(FLD_LINE_4_PALETTE);
break;
default:
break;
}
drawLineFour();
} }
return; return;
} else {
knownHour = 99; // force time display
clear();
} }
needRedraw = false; needRedraw = false;
lastRedraw = millis(); lastRedraw = millis();
if (displayTurnedOff) if (displayTurnedOff) {
{
// Turn the display back on // Turn the display back on
sleepOrClock(false); sleepOrClock(false);
} }
@ -242,79 +326,94 @@ class FourLineDisplayUsermod : public Usermod {
knownEffectIntensity = effectIntensity; knownEffectIntensity = effectIntensity;
// Do the actual drawing // Do the actual drawing
u8x8.clear();
u8x8.setFont(u8x8_font_chroma48medium8_r);
// First row with Wifi name // First row with Wifi name
String ssidString = knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0); drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // wifi icon
u8x8.DRAW_STRING(1, 0*LINE_HEIGHT, ssidString.c_str()); String ssidString = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0);
// Print `~` char to indicate that SSID is longer, than owr dicplay drawString(1, 0, ssidString.c_str());
if (knownSsid.length() > u8x8.getCols()) { // Print `~` char to indicate that SSID is longer, than our display
u8x8.DRAW_STRING(u8x8.getCols() - 1, 0*LINE_HEIGHT, "~"); if (knownSsid.length() > getCols()) {
drawString(getCols() - 1, 0, "~");
} }
// Second row with IP or Psssword // Second row with IP or Psssword
drawGlyph(0, lineHeight, 68, u8x8_font_open_iconic_embedded_1x1); // home icon
// Print password in AP mode and if led is OFF. // Print password in AP mode and if led is OFF.
if (apActive && bri == 0) { if (apActive && bri == 0) {
u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, apPass); drawString(1, lineHeight, apPass);
} } else {
else { drawString(1, lineHeight, (knownIp.toString()).c_str());
String ipString = knownIp.toString();
u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, ipString.c_str());
} }
// Third row with mode name // Third row with mode name or current time
showCurrentEffectOrPalette(modes_qstrings[knownMode], 2); if (clockMode) showTime(false);
else showCurrentEffectOrPalette(knownMode, JSON_mode_names, 2);
switch(lineThreeType) { // Fourth row
case FLD_LINE_3_BRIGHTNESS: drawLineFour();
sprintf(lineBuffer, "Brightness %d", bri);
u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); drawGlyph(0, 2*lineHeight, 66 + (bri > 0 ? 3 : 0), u8x8_font_open_iconic_weather_2x2); // sun/moon icon
//if (markLineNum>1) drawGlyph(2, markLineNum*lineHeight, 66, u8x8_font_open_iconic_arrow_1x1); // arrow icon
}
void drawLineFour() {
char lineBuffer[LINE_BUFFER_SIZE];
switch(lineFourType) {
case FLD_LINE_4_BRIGHTNESS:
sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri);
drawString(2, 3*lineHeight, lineBuffer);
break; break;
case FLD_LINE_3_EFFECT_SPEED: case FLD_LINE_4_EFFECT_SPEED:
sprintf(lineBuffer, "FX Speed %d", effectSpeed); sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed);
u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); drawString(2, 3*lineHeight, lineBuffer);
break; break;
case FLD_LINE_3_EFFECT_INTENSITY: case FLD_LINE_4_EFFECT_INTENSITY:
sprintf(lineBuffer, "FX Intense %d", effectIntensity); sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity);
u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); drawString(2, 3*lineHeight, lineBuffer);
break; break;
case FLD_LINE_3_PALETTE: case FLD_LINE_4_MODE:
showCurrentEffectOrPalette(palettes_qstrings[knownPalette], 3); showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3);
break;
case FLD_LINE_4_PALETTE:
default:
showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 3);
break; break;
} }
u8x8.setFont(u8x8_font_open_iconic_arrow_1x1);
u8x8.DRAW_GLYPH(0, markLineNum*LINE_HEIGHT, 66); // arrow icon
u8x8.setFont(u8x8_font_open_iconic_embedded_1x1);
u8x8.DRAW_GLYPH(0, 0*LINE_HEIGHT, 80); // wifi icon
u8x8.DRAW_GLYPH(0, 1*LINE_HEIGHT, 68); // home icon
} }
/** /**
* Display the current effect or palette (desiredEntry) * Display the current effect or palette (desiredEntry)
* on the appropriate line (row). * on the appropriate line (row).
*
* TODO: Should we cache the current effect and
* TODO: palette name? This seems expensive.
*/ */
void showCurrentEffectOrPalette(char *qstring, uint8_t row) { void showCurrentEffectOrPalette(int knownMode, const char *qstring, uint8_t row) {
uint8_t printedChars = 1; char lineBuffer[LINE_BUFFER_SIZE];
uint8_t qComma = 0;
bool insideQuotes = false;
uint8_t printedChars = 0;
char singleJsonSymbol; char singleJsonSymbol;
int i = 0;
while (true) { // Find the mode name in JSON
for (size_t i = 0; i < strlen_P(qstring); i++) {
singleJsonSymbol = pgm_read_byte_near(qstring + i); singleJsonSymbol = pgm_read_byte_near(qstring + i);
if (singleJsonSymbol == '"' || singleJsonSymbol == '\0' ) { if (singleJsonSymbol == '\0') break;
break; switch (singleJsonSymbol) {
case '"':
insideQuotes = !insideQuotes;
break;
case '[':
case ']':
break;
case ',':
qComma++;
default:
if (!insideQuotes || (qComma != knownMode)) break;
lineBuffer[printedChars++] = singleJsonSymbol;
} }
u8x8.DRAW_GLYPH(printedChars, row * LINE_HEIGHT, singleJsonSymbol); if ((qComma > knownMode) || (printedChars > getCols()-2) || printedChars > sizeof(lineBuffer)-2) break;
printedChars++;
if ( (printedChars > u8x8.getCols() - 2)) {
break;
}
i++;
} }
for (;printedChars < getCols()-2 || printedChars > sizeof(lineBuffer)-2; printedChars++) lineBuffer[printedChars]=' ';
lineBuffer[printedChars] = 0;
drawString(2, row*lineHeight, lineBuffer);
} }
/** /**
@ -324,6 +423,7 @@ class FourLineDisplayUsermod : public Usermod {
* to wake up the screen. * to wake up the screen.
*/ */
bool wakeDisplay() { bool wakeDisplay() {
knownHour = 99;
if (displayTurnedOff) { if (displayTurnedOff) {
// Turn the display back on // Turn the display back on
sleepOrClock(false); sleepOrClock(false);
@ -345,36 +445,31 @@ class FourLineDisplayUsermod : public Usermod {
} }
// Print the overlay // Print the overlay
u8x8.clear(); clear();
u8x8.setFont(u8x8_font_chroma48medium8_r); if (line1) drawString(0, 1*lineHeight, line1);
if (line1) { if (line2) drawString(0, 2*lineHeight, line2);
u8x8.DRAW_STRING(0, 1*LINE_HEIGHT, line1);
}
if (line2) {
u8x8.DRAW_STRING(0, 2*LINE_HEIGHT, line2);
}
overlayUntil = millis() + showHowLong; overlayUntil = millis() + showHowLong;
} }
/** /**
* Specify what data should be defined on line 3 * Specify what data should be defined on line 4
* (the last line). * (the last line).
*/ */
void setLineThreeType(byte newLineThreeType) { void setLineFourType(Line4Type newLineFourType) {
if (newLineThreeType == FLD_LINE_3_BRIGHTNESS || if (newLineFourType == FLD_LINE_4_BRIGHTNESS ||
newLineThreeType == FLD_LINE_3_EFFECT_SPEED || newLineFourType == FLD_LINE_4_EFFECT_SPEED ||
newLineThreeType == FLD_LINE_3_EFFECT_INTENSITY || newLineFourType == FLD_LINE_4_EFFECT_INTENSITY ||
newLineThreeType == FLD_LINE_3_PALETTE) { newLineFourType == FLD_LINE_4_MODE ||
lineThreeType = newLineThreeType; newLineFourType == FLD_LINE_4_PALETTE) {
} lineFourType = newLineFourType;
else { } else {
// Unknown value. // Unknown value
lineThreeType = FLD_LINE_3_BRIGHTNESS; lineFourType = FLD_LINE_4_BRIGHTNESS;
} }
} }
/** /**
* Line 2 or 3 (last two lines) can be marked with an * Line 3 or 4 (last two lines) can be marked with an
* arrow in the first column. Pass 2 or 3 to this to * arrow in the first column. Pass 2 or 3 to this to
* specify which line to mark with an arrow. * specify which line to mark with an arrow.
* Any other values are ignored. * Any other values are ignored.
@ -388,42 +483,17 @@ class FourLineDisplayUsermod : public Usermod {
} }
} }
/*
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
* Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI.
* Below it is shown how this could be used for e.g. a light sensor
*/
/*
void addToJsonInfo(JsonObject& root)
{
int reading = 20;
//this code adds "u":{"Light":[20," lux"]} to the info object
JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject("u");
JsonArray lightArr = user.createNestedArray("Light"); //name
lightArr.add(reading); //value
lightArr.add(" lux"); //unit
}
*/
/** /**
* Enable sleep (turn the display off) or clock mode. * Enable sleep (turn the display off) or clock mode.
*/ */
void sleepOrClock(bool enabled) { void sleepOrClock(bool enabled) {
if (enabled) { if (enabled) {
if (CLOCK_MODE_ENABLED) { if (clockMode) showTime();
showTime(); else setPowerSave(1);
}
else {
u8x8.setPowerSave(1);
}
displayTurnedOff = true; displayTurnedOff = true;
} }
else { else {
if (!CLOCK_MODE_ENABLED) { setPowerSave(0);
u8x8.setPowerSave(0);
}
displayTurnedOff = false; displayTurnedOff = false;
} }
} }
@ -433,23 +503,28 @@ class FourLineDisplayUsermod : public Usermod {
* on the middle rows. Based 24 or 12 hour depending on * on the middle rows. Based 24 or 12 hour depending on
* the useAMPM configuration. * the useAMPM configuration.
*/ */
void showTime() { void showTime(bool fullScreen = true) {
char lineBuffer[LINE_BUFFER_SIZE];
updateLocalTime(); updateLocalTime();
byte minuteCurrent = minute(localTime); byte minuteCurrent = minute(localTime);
byte hourCurrent = hour(localTime); byte hourCurrent = hour(localTime);
byte secondCurrent = second(localTime);
if (knownMinute == minuteCurrent && knownHour == hourCurrent) { if (knownMinute == minuteCurrent && knownHour == hourCurrent) {
// Time hasn't changed. // Time hasn't changed.
return; if (!fullScreen) return;
} else {
if (fullScreen) clear();
} }
knownMinute = minuteCurrent; knownMinute = minuteCurrent;
knownHour = hourCurrent; knownHour = hourCurrent;
u8x8.clear(); byte currentMonth = month(localTime);
u8x8.setFont(u8x8_font_chroma48medium8_r); sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(currentMonth), day(localTime));
if (fullScreen)
int currentMonth = month(localTime); draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays
sprintf(lineBuffer, "%s %d", monthShortStr(currentMonth), day(localTime)); else
u8x8.DRAW_BIG_STRING(DATE_INDENT, TIME_LINE*LINE_HEIGHT, lineBuffer); drawString(2, lineHeight*2, lineBuffer);
byte showHour = hourCurrent; byte showHour = hourCurrent;
boolean isAM = false; boolean isAM = false;
@ -467,25 +542,45 @@ class FourLineDisplayUsermod : public Usermod {
} }
} }
sprintf(lineBuffer, "%02d:%02d %s", showHour, minuteCurrent, useAMPM ? (isAM ? "AM" : "PM") : ""); sprintf_P(lineBuffer, (secondCurrent%2 || !fullScreen) ? PSTR("%2d:%02d") : PSTR("%2d %02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent);
// For time, we always use LINE_HEIGHT of 2 since // For time, we always use LINE_HEIGHT of 2 since
// we are printing it big. // we are printing it big.
u8x8.DRAW_BIG_STRING(TIME_INDENT + (useAMPM ? 0 : 2), (TIME_LINE + 1) * 2, lineBuffer); if (fullScreen) {
draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer);
sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent);
if (!useAMPM) drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line
} else {
drawString(9+(useAMPM?0:2), lineHeight*2, lineBuffer);
}
if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true);
} }
/*
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
* Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI.
* Below it is shown how this could be used for e.g. a light sensor
*/
//void addToJsonInfo(JsonObject& root) {
//JsonObject user = root["u"];
//if (user.isNull()) user = root.createNestedObject("u");
//JsonArray data = user.createNestedArray(F("4LineDisplay"));
//data.add(F("Loaded."));
//}
/* /*
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients * Values in the state object may be modified by connected clients
*/ */
void addToJsonState(JsonObject& root) { //void addToJsonState(JsonObject& root) {
} //}
/* /*
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
* Values in the state object may be modified by connected clients * Values in the state object may be modified by connected clients
*/ */
void readFromJsonState(JsonObject& root) { //void readFromJsonState(JsonObject& root) {
} // if (!initDone) return; // prevent crash on boot applyPreset()
//}
/* /*
* addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object.
@ -502,6 +597,18 @@ class FourLineDisplayUsermod : public Usermod {
* I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!
*/ */
void addToConfig(JsonObject& root) { void addToConfig(JsonObject& root) {
JsonObject top = root.createNestedObject(FPSTR(_name));
JsonArray i2c_pin = top.createNestedArray("pin");
i2c_pin.add(sclPin);
i2c_pin.add(sdaPin);
top["type"] = type;
top[FPSTR(_flip)] = (bool) flip;
top[FPSTR(_contrast)] = contrast;
top[FPSTR(_refreshRate)] = refreshRate/1000;
top[FPSTR(_screenTimeOut)] = screenTimeout/1000;
top[FPSTR(_sleepMode)] = (bool) sleepMode;
top[FPSTR(_clockMode)] = (bool) clockMode;
DEBUG_PRINTLN(F("4 Line Display config saved."));
} }
/* /*
@ -513,6 +620,74 @@ class FourLineDisplayUsermod : public Usermod {
* If you don't know what that is, don't fret. It most likely doesn't affect your use case :) * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)
*/ */
void readFromConfig(JsonObject& root) { void readFromConfig(JsonObject& root) {
bool needsRedraw = false;
DisplayType newType = type;
int8_t newScl = sclPin;
int8_t newSda = sdaPin;
JsonObject top = root[FPSTR(_name)];
if (!top.isNull() && top["pin"] != nullptr) {
newScl = top["pin"][0];
newSda = top["pin"][1];
newType = top["type"];
if (top[FPSTR(_flip)].is<bool>()) {
flip = top[FPSTR(_flip)].as<bool>();
} else {
String str = top[FPSTR(_flip)]; // checkbox -> off or on
flip = (bool)(str!="off"); // off is guaranteed to be present
needRedraw |= true;
}
contrast = top[FPSTR(_contrast)].as<int>();
refreshRate = top[FPSTR(_refreshRate)].as<int>() * 1000;
screenTimeout = top[FPSTR(_screenTimeOut)].as<int>() * 1000;
if (top[FPSTR(_sleepMode)].is<bool>()) {
sleepMode = top[FPSTR(_sleepMode)].as<bool>();
} else {
String str = top[FPSTR(_sleepMode)]; // checkbox -> off or on
sleepMode = (bool)(str!="off"); // off is guaranteed to be present
needRedraw |= true;
}
if (top[FPSTR(_clockMode)].is<bool>()) {
clockMode = top[FPSTR(_clockMode)].as<bool>();
} else {
String str = top[FPSTR(_clockMode)]; // checkbox -> off or on
clockMode = (bool)(str!="off"); // off is guaranteed to be present
needRedraw |= true;
}
DEBUG_PRINTLN(F("4 Line Display config (re)loaded."));
} else {
DEBUG_PRINTLN(F("No config found. (Using defaults.)"));
}
if (!initDone) {
// first run: reading from cfg.json
sclPin = newScl;
sdaPin = newSda;
type = newType;
lineHeight = type==SSD1306 ? 1 : 2;
} else {
// changing paramters from settings page
if (sclPin!=newScl || sdaPin!=newSda || type!=newType) {
if (type==SSD1306) delete (static_cast<U8X8*>(u8x8));
if (type==SH1106) delete (static_cast<U8X8*>(u8x8));
if (type==SSD1306_64) delete (static_cast<U8X8*>(u8x8));
pinManager.deallocatePin(sclPin);
pinManager.deallocatePin(sdaPin);
sclPin = newScl;
sdaPin = newSda;
if (newScl<0 || newSda<0) {
type = NONE;
return;
} else
type = newType;
lineHeight = type==SSD1306 ? 1 : 2;
setup();
needRedraw |= true;
}
setContrast(contrast);
setFlipMode(flip);
if (needsRedraw && !wakeDisplay()) redraw(true);
}
} }
/* /*
@ -522,5 +697,13 @@ class FourLineDisplayUsermod : public Usermod {
uint16_t getId() { uint16_t getId() {
return USERMOD_ID_FOUR_LINE_DISP; return USERMOD_ID_FOUR_LINE_DISP;
} }
}; };
// strings to reduce flash memory usage (used more than twice)
const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay";
const char FourLineDisplayUsermod::_contrast[] PROGMEM = "contrast";
const char FourLineDisplayUsermod::_refreshRate[] PROGMEM = "refreshRateSec";
const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec";
const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip";
const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode";
const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode";

View File

@ -11,12 +11,24 @@
#define DEFAULT_OTA_PASS "wledota" #define DEFAULT_OTA_PASS "wledota"
//increase if you need more //increase if you need more
#define WLED_MAX_USERMODS 4 #ifndef WLED_MAX_USERMODS
#ifdef ESP8266
#define WLED_MAX_USERMODS 4
#else
#define WLED_MAX_USERMODS 6
#endif
#endif
#ifdef ESP8266 #ifndef WLED_MAX_BUSSES
#define WLED_MAX_BUSSES 3 #ifdef ESP8266
#else #define WLED_MAX_BUSSES 3
#define WLED_MAX_BUSSES 10 #else
#ifdef CONFIG_IDF_TARGET_ESP32S2
#define WLED_MAX_BUSSES 5
#else
#define WLED_MAX_BUSSES 10
#endif
#endif
#endif #endif
//Usermod IDs //Usermod IDs
@ -33,6 +45,7 @@
#define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h" #define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h"
#define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" #define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h"
#define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h" #define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h"
#define USERMOD_ID_MULTI_RELAY 101 //Usermod "usermod_multi_relay.h"
//Access point behavior //Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
@ -116,10 +129,10 @@
#define TYPE_LPD8806 52 #define TYPE_LPD8806 52
#define TYPE_P9813 53 #define TYPE_P9813 53
#define IS_DIGITAL(t) (t & 0x10) //digital are 16-31 and 48-63 #define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63
#define IS_PWM(t) (t > 40 && t < 46) #define IS_PWM(t) ((t) > 40 && (t) < 46)
#define NUM_PWM_PINS(t) (t - 40) //for analog PWM 41-45 only #define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only
#define IS_2PIN(t) (t > 47) #define IS_2PIN(t) ((t) > 47)
//Color orders //Color orders
#define COL_ORDER_GRB 0 //GRB(w),defaut #define COL_ORDER_GRB 0 //GRB(w),defaut
@ -241,7 +254,11 @@
//this is merely a default now and can be changed at runtime //this is merely a default now and can be changed at runtime
#ifndef LEDPIN #ifndef LEDPIN
#define LEDPIN 2 #ifdef ESP8266
#define LEDPIN 2 // GPIO2 (D4) on Wemod D1 mini compatible boards
#else
#define LEDPIN 16 // alligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards
#endif
#endif #endif
#ifdef WLED_ENABLE_DMX #ifdef WLED_ENABLE_DMX

View File

@ -10,18 +10,17 @@
margin: 0; margin: 0;
} }
html { html {
--h: 11.55vh; --h: 10.2vh;
} }
button { button {
background: #333; background: #333;
color: #fff; color: #fff;
font-family: Verdana, Helvetica, sans-serif; font-family: Verdana, Helvetica, sans-serif;
border: 0.3ch solid #333; border: 1px solid #333;
display: inline-block; font-size: 6vmin;
font-size: 8vmin;
height: var(--h); height: var(--h);
width: 95%; width: 95%;
margin-top: 2.4vh; margin-top: 2vh;
} }
</style> </style>
<script> <script>
@ -41,6 +40,7 @@
<form action="/settings/ui"><button type="submit">User Interface</button></form> <form action="/settings/ui"><button type="submit">User Interface</button></form>
<form action="/settings/sync"><button type="submit">Sync Interfaces</button></form> <form action="/settings/sync"><button type="submit">Sync Interfaces</button></form>
<form action="/settings/time"><button type="submit">Time & Macros</button></form> <form action="/settings/time"><button type="submit">Time & Macros</button></form>
<form action="/settings/um"><button type="submit">Usermods</button></form>
<form action="/settings/sec"><button type="submit">Security & Updates</button></form> <form action="/settings/sec"><button type="submit">Security & Updates</button></form>
</body> </body>
</html> </html>

140
wled00/data/settings_um.htm Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=500">
<title>UI Settings</title>
<script>
var d = document;
var umCfg = {};
var pins = [6,7,8,9,10,11];
var pinO = ["reserved","reserved","reserved","reserved","reserved","reserved"], owner;
var loc = false, locip;
var urows;
function gId(s) { return d.getElementById(s); }
function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); }
function H() { window.open("https://github.com/Aircoookie/WLED/wiki/Settings#usermod-settings"); }
function B() { window.open("/settings","_self"); }
function S() {
if (window.location.protocol == "file:") {
loc = true;
locip = localStorage.getItem('locIp');
if (!locip) {
locip = prompt("File Mode. Please enter WLED IP!");
localStorage.setItem('locIp', locip);
}
}
ldS();
}
function check(o,k) {
var n = o.name.replace("[]","").substr(-3);
if (o.type=="number" && n.substr(0,3)=="pin") {
for (var i=0; i<pins.length; i++) {
if (k==pinO[i]) continue;
if (o.value==pins[i] || o.value<-1 || o.value>39) { o.style.color="red"; break; } else o.style.color=o.value>33?"orange":"#fff";
}
}
}
function getPins(o) {
if (isO(o)) {
for (const [k,v] of Object.entries(o)) {
if (isO(v)) {
owner = k;
getPins(v);
continue;
}
if (k.replace("[]","").substr(-3)=="pin") {
if (Array.isArray(v)) {
for (var i=0; i<v.length; i++) if (v[i]>=0) { pins.push(v[i]); pinO.push(owner); }
} else {
if (v>=0) { pins.push(v); pinO.push(owner); }
}
} else if (Array.isArray(v)) {
for (var i=0; i<v.length; i++) getPins(v[i]);
}
}
}
}
function addField(k,f,o,a=false) {
if (isO(o)) {
for (const [s,v] of Object.entries(o)) {
addField(k,s,v);
}
} else if (Array.isArray(o)) {
for (var j=0; j<o.length; j++) {
addField(k,f,o[j],true);
}
} else {
var t,c;
switch (typeof o) {
case "boolean":
t = "checkbox"; c = o ? `checked value="on"` : ""; break;
case "number":
t = "number"; c = `value="${parseInt(o,10)}"`; break;
case "string":
t = "text"; c = `value="${o}"`; break;
default:
t = "text"; c = `value="${o}"`; break;
}
// https://stackoverflow.com/questions/11657123/posting-both-checked-and-unchecked-checkboxes
if (t=="checkbox") urows += `<input type="hidden" name="${k}_${f}${a?"[]":""}" value="off">`;
urows += `${f}: <input type="${t}" name="${k}_${f}${a?"[]":""}" ${c} oninput="check(this,'${k}')"><br>`;
}
}
function ldS() {
var url = (loc?`http://${locip}`:'') + '/cfg.json';
fetch(url, {
method: 'get'
})
.then(res => {
if (!res.ok) gId('lserr').style.display = "inline";
return res.json();
})
.then(json => {
umCfg = json.um;
getPins(json);
urows="";
if (isO(umCfg)) {
for (const [k,o] of Object.entries(umCfg)) {
urows += `<hr><h3>${k}</h3>`;
addField(k,'unknown',o);
}
if (urows==="")
urows = "No Usermods configuration found.<br>Press <i>Save</i> to initialize defaults.";
} else {
urows = "Usermods configuration not found.<br>Most likely no Usermods exist.<br>Press <i>Save</i> to initialize defaults.";
}
gId("um").innerHTML = urows;
})
.catch(function (error) {
gId('lserr').style.display = "inline"
console.log(error);
});
}
function svS(e) {
e.preventDefault();
console.log(d.Sf);
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
}
function GetV() {}
</script>
<style>
@import url("style.css");
</style>
</head>
<body onload="S()">
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
<span id="lssuc" style="color:green; display:none">&#10004; Configuration saved!</span>
<span id="lserr" style="color:red; display:none">&#9888; Could not load configuration.</span><hr>
</div>
<h2>Usermod Setup</h2>
<div id="um">Loading settings...</div>
<hr><button type="button" onclick="B()">Back</button><button type="submit">Save</button>
</form>
</body>
</html>

View File

@ -150,6 +150,7 @@ void _overlayCronixie();
void _drawOverlayCronixie(); void _drawOverlayCronixie();
//playlist.cpp //playlist.cpp
void shufflePlaylist();
void unloadPlaylist(); void unloadPlaylist();
void loadPlaylist(JsonObject playlistObject); void loadPlaylist(JsonObject playlistObject);
void handlePlaylist(); void handlePlaylist();
@ -186,6 +187,8 @@ class Usermod {
virtual void readFromJsonState(JsonObject& obj) {} virtual void readFromJsonState(JsonObject& obj) {}
virtual void addToConfig(JsonObject& obj) {} virtual void addToConfig(JsonObject& obj) {}
virtual void readFromConfig(JsonObject& obj) {} virtual void readFromConfig(JsonObject& obj) {}
virtual void onMqttConnect(bool sessionPresent) {}
virtual bool onMqttMessage(char* topic, char* payload) { return false; }
virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;}
}; };
@ -206,7 +209,8 @@ class UsermodManager {
void addToConfig(JsonObject& obj); void addToConfig(JsonObject& obj);
void readFromConfig(JsonObject& obj); void readFromConfig(JsonObject& obj);
void onMqttConnect(bool sessionPresent);
bool onMqttMessage(char* topic, char* payload);
bool add(Usermod* um); bool add(Usermod* um);
Usermod* lookup(uint16_t mod_id); Usermod* lookup(uint16_t mod_id);
byte getModCount(); byte getModCount();

View File

@ -12,7 +12,7 @@ const char PAGE_settingsCss[] PROGMEM = R"=====(<style>body{font-family:Verdana,
// Autogenerated from wled00/data/settings.htm, do not edit!! // Autogenerated from wled00/data/settings.htm, do not edit!!
const char PAGE_settings[] PROGMEM = R"=====(<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>WLED Settings const char PAGE_settings[] PROGMEM = R"=====(<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>WLED Settings
</title><style> </title><style>
body{text-align:center;background:#222;height:100px;margin:0}html{--h:11.55vh}button{background:#333;color:#fff;font-family:Verdana,Helvetica,sans-serif;border:.3ch solid #333;display:inline-block;font-size:8vmin;height:var(--h);width:95%%;margin-top:2.4vh} body{text-align:center;background:#222;height:100px;margin:0}html{--h:10.2vh}button{background:#333;color:#fff;font-family:Verdana,Helvetica,sans-serif;border:1px solid #333;font-size:6vmin;height:var(--h);width:95%%;margin-top:2vh}
</style><script> </style><script>
function BB(){window.frameElement&&(document.getElementById("b").style.display="none",document.documentElement.style.setProperty("--h","13.86vh"))} function BB(){window.frameElement&&(document.getElementById("b").style.display="none",document.documentElement.style.setProperty("--h","13.86vh"))}
</script></head><body onload="BB()"><form action="/"><button type="submit" </script></head><body onload="BB()"><form action="/"><button type="submit"
@ -22,7 +22,8 @@ LED Preferences</button></form><form action="/settings/ui"><button
type="submit">User Interface</button></form>%DMXMENU%<form action="/settings/sync"> type="submit">User Interface</button></form>%DMXMENU%<form action="/settings/sync">
<button type="submit">Sync Interfaces</button></form><form <button type="submit">Sync Interfaces</button></form><form
action="/settings/time"><button type="submit">Time & Macros</button></form><form action="/settings/time"><button type="submit">Time & Macros</button></form><form
action="/settings/sec"><button type="submit">Security & Updates</button></form> action="/settings/um"><button type="submit">Usermods</button></form><form
action="/settings/sec"><button type="submit">Security & Updates</button></form>
</body></html>)====="; </body></html>)=====";
@ -390,3 +391,18 @@ MIT license</a></i><br><br>Server message: <span class="sip">Response error!
</span><hr><button type="button" onclick="B()">Back</button><button </span><hr><button type="button" onclick="B()">Back</button><button
type="submit">Save & Reboot</button></form></body></html>)====="; type="submit">Save & Reboot</button></form></body></html>)=====";
// Autogenerated from wled00/data/settings_um.htm, do not edit!!
const char PAGE_settings_um[] PROGMEM = R"=====(<!DOCTYPE html><html><head lang="en"><meta charset="utf-8"><meta
name="viewport" content="width=500"><title>UI Settings</title><script>
var owner,locip,urows,d=document,umCfg={},pins=[6,7,8,9,10,11],pinO=["reserved","reserved","reserved","reserved","reserved","reserved"],loc=!1;function gId(e){return d.getElementById(e)}function isO(e){return e&&"object"==typeof e&&!Array.isArray(e)}function H(){window.open("https://github.com/Aircoookie/WLED/wiki/Settings#usermod-settings")}function B(){window.open("/settings","_self")}function S(){"file:"==window.location.protocol&&(loc=!0,(locip=localStorage.getItem("locIp"))||(locip=prompt("File Mode. Please enter WLED IP!"),localStorage.setItem("locIp",locip))),ldS()}function check(e,o){var n=e.name.replace("[]","").substr(-3);if("number"==e.type&&"pin"==n.substr(0,3))for(var i=0;i<pins.length;i++)if(o!=pinO[i]){if(e.value==pins[i]||e.value<-1||e.value>39){e.style.color="red";break}e.style.color=e.value>33?"orange":"#fff"}}function getPins(e){if(isO(e))for(const[n,i]of Object.entries(e))if(isO(i))owner=n,getPins(i);else if("pin"==n.replace("[]","").substr(-3))if(Array.isArray(i))for(var o=0;o<i.length;o++)i[o]>=0&&(pins.push(i[o]),pinO.push(owner));else i>=0&&(pins.push(i),pinO.push(owner));else if(Array.isArray(i))for(o=0;o<i.length;o++)getPins(i[o])}function addField(e,o,n,i=!1){if(isO(n))for(const[o,i]of Object.entries(n))addField(e,o,i);else if(Array.isArray(n))for(var r=0;r<n.length;r++)addField(e,o,n[r],!0);else{var s,t;switch(typeof n){case"boolean":s="checkbox",t=n?'checked value="on"':"";break;case"number":s="number",t=`value="${parseInt(n,10)}"`;break;case"string":default:s="text",t=`value="${n}"`}"checkbox"==s&&(urows+=`<input type="hidden" name="${e}_${o}${i?"[]":""}" value="off">`),urows+=`${o}: <input type="${s}" name="${e}_${o}${i?"[]":""}" ${t} oninput="check(this,'${e}')"><br>`}}function ldS(){fetch((loc?"http://"+locip:"")+"/cfg.json",{method:"get"}).then(e=>(e.ok||(gId("lserr").style.display="inline"),e.json())).then(e=>{if(umCfg=e.um,getPins(e),urows="",isO(umCfg)){for(const[e,o]of Object.entries(umCfg))urows+=`<hr><h3>${e}</h3>`,addField(e,"unknown",o);""===urows&&(urows="No Usermods configuration found.<br>Press <i>Save</i> to initialize defaults.")}else urows="Usermods configuration not found.<br>Most likely no Usermods exist.<br>Press <i>Save</i> to initialize defaults.";gId("um").innerHTML=urows}).catch((function(e){gId("lserr").style.display="inline",console.log(e)}))}function svS(e){e.preventDefault(),console.log(d.Sf),d.Sf.checkValidity()&&d.Sf.submit()}function GetV(){}
</script>%CSS%%SCSS%</head><body onload="S()"><form
id="form_s" name="Sf" method="post" onsubmit="svS(event)"><div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div><button
type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
<span id="lssuc" style="color:green;display:none">&#10004; Configuration saved!
</span> <span id="lserr" style="color:red;display:none">
&#9888; Could not load configuration.</span><hr></div><h2>Usermod Setup</h2><div
id="um">Loading settings...</div><hr><button type="button" onclick="B()">Back
</button><button type="submit">Save</button></form></body></html>)=====";

File diff suppressed because it is too large Load Diff

View File

@ -25,28 +25,28 @@ void onMqttConnect(bool sessionPresent)
//(re)subscribe to required topics //(re)subscribe to required topics
char subuf[38]; char subuf[38];
if (mqttDeviceTopic[0] != 0) if (mqttDeviceTopic[0] != 0) {
{
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strcat(subuf, "/col"); strcat_P(subuf, PSTR("/col"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
strcat(subuf, "/api"); strcat_P(subuf, PSTR("/api"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
} }
if (mqttGroupTopic[0] != 0) if (mqttGroupTopic[0] != 0) {
{
strcpy(subuf, mqttGroupTopic); strcpy(subuf, mqttGroupTopic);
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strcat(subuf, "/col"); strcat_P(subuf, PSTR("/col"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strcpy(subuf, mqttGroupTopic); strcpy(subuf, mqttGroupTopic);
strcat(subuf, "/api"); strcat_P(subuf, PSTR("/api"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
} }
usermods.onMqttConnect(sessionPresent);
doPublishMqtt = true; doPublishMqtt = true;
DEBUG_PRINTLN(F("MQTT ready")); DEBUG_PRINTLN(F("MQTT ready"));
} }
@ -66,25 +66,24 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties
size_t topicPrefixLen = strlen(mqttDeviceTopic); size_t topicPrefixLen = strlen(mqttDeviceTopic);
if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) {
topic += topicPrefixLen; topic += topicPrefixLen;
} else { } else {
topicPrefixLen = strlen(mqttGroupTopic); topicPrefixLen = strlen(mqttGroupTopic);
if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) {
topic += topicPrefixLen; topic += topicPrefixLen;
} else { } else {
// Topic not used here. Probably a usermod subscribed to this topic. // Non-Wled Topic used here. Probably a usermod subscribed to this topic.
return; usermods.onMqttMessage(topic, payload);
} return;
}
} }
//Prefix is stripped from the topic at this point //Prefix is stripped from the topic at this point
if (strcmp(topic, "/col") == 0) if (strcmp_P(topic, PSTR("/col")) == 0) {
{
colorFromDecOrHexString(col, (char*)payload); colorFromDecOrHexString(col, (char*)payload);
colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE);
} else if (strcmp(topic, "/api") == 0) } else if (strcmp_P(topic, PSTR("/api")) == 0) {
{
if (payload[0] == '{') { //JSON API if (payload[0] == '{') { //JSON API
DynamicJsonDocument doc(JSON_BUFFER_SIZE); DynamicJsonDocument doc(JSON_BUFFER_SIZE);
deserializeJson(doc, payload); deserializeJson(doc, payload);
@ -94,8 +93,11 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties
apireq += (char*)payload; apireq += (char*)payload;
handleSet(nullptr, apireq); handleSet(nullptr, apireq);
} }
} else if (strcmp(topic, "") == 0) } else if (strlen(topic) != 0) {
{ // non standard topic, check with usermods
usermods.onMqttMessage(topic, payload);
} else {
// topmost topic (just wled/MAC)
parseMQTTBriPayload(payload); parseMQTTBriPayload(payload);
} }
} }
@ -110,24 +112,24 @@ void publishMqtt()
char s[10]; char s[10];
char subuf[38]; char subuf[38];
sprintf(s, "%u", bri); sprintf_P(s, PSTR("%u"), bri);
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
strcat(subuf, "/g"); strcat_P(subuf, PSTR("/g"));
mqtt->publish(subuf, 0, true, s); mqtt->publish(subuf, 0, true, s);
sprintf(s, "#%06X", (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2]));
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
strcat(subuf, "/c"); strcat_P(subuf, PSTR("/c"));
mqtt->publish(subuf, 0, true, s); mqtt->publish(subuf, 0, true, s);
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
strcat(subuf, "/status"); strcat_P(subuf, PSTR("/status"));
mqtt->publish(subuf, 0, true, "online"); mqtt->publish(subuf, 0, true, "online");
char apires[1024]; char apires[1024];
XML_response(nullptr, apires); XML_response(nullptr, apires);
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
strcat(subuf, "/v"); strcat_P(subuf, PSTR("/v"));
mqtt->publish(subuf, 0, true, apires); mqtt->publish(subuf, 0, true, apires);
} }
@ -157,7 +159,7 @@ bool initMqtt()
if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass);
strcpy(mqttStatusTopic, mqttDeviceTopic); strcpy(mqttStatusTopic, mqttDeviceTopic);
strcat(mqttStatusTopic, "/status"); strcat_P(mqttStatusTopic, PSTR("/status"));
mqtt->setWill(mqttStatusTopic, 0, true, "offline"); mqtt->setWill(mqttStatusTopic, 0, true, "offline");
mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME);
mqtt->connect(); mqtt->connect();

View File

@ -31,8 +31,8 @@ bool isAsterisksOnly(const char* str, byte maxLen)
void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
{ {
//0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX 8: usermods
if (subPage <1 || subPage >7) return; if (subPage <1 || subPage >8) return;
//WIFI SETTINGS //WIFI SETTINGS
if (subPage == 1) if (subPage == 1)
@ -407,6 +407,57 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
} }
#endif #endif
//USERMODS
if (subPage == 8)
{
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
JsonObject um = doc.createNestedObject("um");
size_t args = request->args();
uint j=0;
for (size_t i=0; i<args; i++) {
String name = request->argName(i);
String value = request->arg(i);
// POST request parameters are combined as <usermodname>_<usermodparameter>
uint8_t umNameEnd = name.indexOf("_");
if (!umNameEnd) break; // parameter does not contain "_" -> wrong
JsonObject mod = um[name.substring(0,umNameEnd)]; // get a usermod JSON object
if (mod.isNull()) {
mod = um.createNestedObject(name.substring(0,umNameEnd)); // if it does not exist create it
}
DEBUG_PRINT(name.substring(0,umNameEnd));
DEBUG_PRINT(":");
name = name.substring(umNameEnd+1); // remove mod name from string
// check if parameters represent array
if (name.endsWith("[]")) {
name.replace("[]","");
if (!mod[name].is<JsonArray>()) {
JsonArray ar = mod.createNestedArray(name);
ar.add(value);
j=0;
} else {
mod[name].add(value);
j++;
}
DEBUG_PRINT(name);
DEBUG_PRINT("[");
DEBUG_PRINT(j);
DEBUG_PRINT("] = ");
DEBUG_PRINTLN(value);
} else {
mod.remove(name); // checkboxes get two fields (first is always "off", existence of second depends on checkmark and may be "on")
mod[name] = value;
DEBUG_PRINT(name);
DEBUG_PRINT(" = ");
DEBUG_PRINTLN(value);
}
}
usermods.readFromConfig(um); // force change of usermod parameters
}
if (subPage != 2 && (subPage != 6 || !doReboot)) serializeConfig(); //do not save if factory reset or LED settings (which are saved after LED re-init) if (subPage != 2 && (subPage != 6 || !doReboot)) serializeConfig(); //do not save if factory reset or LED settings (which are saved after LED re-init)
if (subPage == 4) alexaInit(); if (subPage == 4) alexaInit();
} }

View File

@ -14,6 +14,11 @@ void UsermodManager::addToJsonInfo(JsonObject& obj) { for (byte i = 0; i < n
void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); }
void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); }
void UsermodManager::readFromConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromConfig(obj); } void UsermodManager::readFromConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromConfig(obj); }
void UsermodManager::onMqttConnect(bool sessionPresent) { for (byte i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); }
bool UsermodManager::onMqttMessage(char* topic, char* payload) {
for (byte i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true;
return false;
}
/* /*
* Enables usermods to lookup another Usermod. * Enables usermods to lookup another Usermod.

View File

@ -20,7 +20,10 @@
#include "../usermods/buzzer/usermod_v2_buzzer.h" #include "../usermods/buzzer/usermod_v2_buzzer.h"
#endif #endif
#ifdef USERMOD_SENSORSTOMQTT #ifdef USERMOD_SENSORSTOMQTT
#include "usermod_v2_SensorsToMqtt.h" #include "../usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h"
#endif
#ifdef USERMOD_PIRSWITCH
#include "../usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h"
#endif #endif
#ifdef USERMOD_MODE_SORT #ifdef USERMOD_MODE_SORT
@ -31,7 +34,7 @@
#ifdef USERMOD_BME280 #ifdef USERMOD_BME280
#include "../usermods/BME280_v2/usermod_bme280.h" #include "../usermods/BME280_v2/usermod_bme280.h"
#endif #endif
#ifdef USERMOD_FOUR_LINE_DISLAY #ifdef USERMOD_FOUR_LINE_DISPLAY
#include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h" #include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h"
#endif #endif
#ifdef USERMOD_ROTARY_ENCODER_UI #ifdef USERMOD_ROTARY_ENCODER_UI
@ -50,6 +53,14 @@
#include "../usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h" #include "../usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h"
#endif #endif
#ifdef USERMOD_ANIMATED_STAIRCASE
#include "../usermods/Animated_Staircase/Animated_Staircase.h"
#endif
#ifdef USERMOD_MULTI_RELAY
#include "../usermods/multi_relay/usermod_multi_relay.h"
#endif
void registerUsermods() void registerUsermods()
{ {
/* /*
@ -72,28 +83,39 @@ void registerUsermods()
#ifdef USERMOD_BME280 #ifdef USERMOD_BME280
usermods.add(new UsermodBME280()); usermods.add(new UsermodBME280());
#endif #endif
#ifdef USERMOD_SENSORSTOMQTT #ifdef USERMOD_SENSORSTOMQTT
usermods.add(new UserMod_SensorsToMQTT()); usermods.add(new UserMod_SensorsToMQTT());
#endif #endif
#ifdef USERMOD_PIRSWITCH
usermods.add(new PIRsensorSwitch());
#endif
#ifdef USERMOD_MODE_SORT #ifdef USERMOD_MODE_SORT
usermods.add(new ModeSortUsermod()); usermods.add(new ModeSortUsermod());
#endif #endif
#ifdef USERMOD_FOUR_LINE_DISLAY #ifdef USERMOD_FOUR_LINE_DISPLAY
usermods.add(new FourLineDisplayUsermod()); usermods.add(new FourLineDisplayUsermod());
#endif #endif
#ifdef USERMOD_ROTARY_ENCODER_UI #ifdef USERMOD_ROTARY_ENCODER_UI
usermods.add(new RotaryEncoderUIUsermod()); usermods.add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY
#endif #endif
#ifdef USERMOD_AUTO_SAVE #ifdef USERMOD_AUTO_SAVE
usermods.add(new AutoSaveUsermod()); usermods.add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY
#endif #endif
#ifdef USERMOD_DHT #ifdef USERMOD_DHT
usermods.add(new UsermodDHT()); usermods.add(new UsermodDHT());
#endif #endif
#ifdef USERMOD_VL53L0X_GESTURES #ifdef USERMOD_VL53L0X_GESTURES
usermods.add(new UsermodVL53L0XGestures()); usermods.add(new UsermodVL53L0XGestures());
#endif #endif
#ifdef USERMOD_ANIMATED_STAIRCASE
usermods.add(new Animated_Staircase());
#endif
#ifdef USERMOD_MULTI_RELAY
usermods.add(new MultiRelay());
#endif
} }

View File

@ -314,6 +314,7 @@ String settingsProcessor(const String& var)
{ {
if (var == "CSS") { if (var == "CSS") {
char buf[2048]; char buf[2048];
buf[0] = 0;
getSettingsJS(optionType, buf); getSettingsJS(optionType, buf);
return String(buf); return String(buf);
} }
@ -365,6 +366,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
#ifdef WLED_ENABLE_DMX // include only if DMX is enabled #ifdef WLED_ENABLE_DMX // include only if DMX is enabled
else if (url.indexOf("dmx") > 0) subPage = 7; else if (url.indexOf("dmx") > 0) subPage = 7;
#endif #endif
else if (url.indexOf("um") > 0) subPage = 8;
} else subPage = 255; //welcome page } else subPage = 255; //welcome page
if (subPage == 1 && wifiLock && otaLock) if (subPage == 1 && wifiLock && otaLock)
@ -386,6 +388,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
case 5: strcpy_P(s, PSTR("Time")); break; case 5: strcpy_P(s, PSTR("Time")); break;
case 6: strcpy_P(s, PSTR("Security")); strcpy_P(s2, PSTR("Rebooting, please wait ~10 seconds...")); break; case 6: strcpy_P(s, PSTR("Security")); strcpy_P(s2, PSTR("Rebooting, please wait ~10 seconds...")); break;
case 7: strcpy_P(s, PSTR("DMX")); break; case 7: strcpy_P(s, PSTR("DMX")); break;
case 8: strcpy_P(s, PSTR("Usermods")); break;
} }
strcat_P(s, PSTR(" settings saved.")); strcat_P(s, PSTR(" settings saved."));
@ -412,6 +415,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
case 5: request->send_P(200, "text/html", PAGE_settings_time, settingsProcessor); break; case 5: request->send_P(200, "text/html", PAGE_settings_time, settingsProcessor); break;
case 6: request->send_P(200, "text/html", PAGE_settings_sec , settingsProcessor); break; case 6: request->send_P(200, "text/html", PAGE_settings_sec , settingsProcessor); break;
case 7: request->send_P(200, "text/html", PAGE_settings_dmx , settingsProcessor); break; case 7: request->send_P(200, "text/html", PAGE_settings_dmx , settingsProcessor); break;
case 8: request->send_P(200, "text/html", PAGE_settings_um , settingsProcessor); break;
case 255: request->send_P(200, "text/html", PAGE_welcome); break; case 255: request->send_P(200, "text/html", PAGE_welcome); break;
default: request->send_P(200, "text/html", PAGE_settings , settingsProcessor); default: request->send_P(200, "text/html", PAGE_settings , settingsProcessor);
} }