Merge pull request #1951 from blazoncek/user-configurable-usermods
User configurable usermods.
This commit is contained in:
commit
119826cb9b
@ -333,6 +333,18 @@ const char PAGE_settings_dmx[] PROGMEM = R"=====()=====";
|
||||
"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"
|
||||
);
|
||||
|
@ -9,25 +9,31 @@
|
||||
*/
|
||||
#pragma once
|
||||
#include "wled.h"
|
||||
#include "Animated_Staircase_config.h"
|
||||
|
||||
#define USERMOD_ID_ANIMATED_STAIRCASE 1011
|
||||
|
||||
/* Initial configuration (available in API and stored in flash) */
|
||||
bool enabled = true; // Enable this usermod
|
||||
unsigned long segment_delay_ms = 150; // Time between switching each segment
|
||||
unsigned long on_time_ms = 5 * 1000; // The time for the light to stay on
|
||||
#ifndef TOP_PIR_PIN
|
||||
unsigned int topMaxTimeUs = 1749; // default echo timout, top
|
||||
#endif
|
||||
#ifndef BOTTOM_PIR_PIN
|
||||
unsigned int bottomMaxTimeUs = 1749; // default echo timout, bottom
|
||||
#endif
|
||||
|
||||
// Time between checking of the sensors
|
||||
const int scanDelay = 50;
|
||||
|
||||
class Animated_Staircase : public Usermod {
|
||||
private:
|
||||
|
||||
/* 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.
|
||||
// Flipping this will start a transition.
|
||||
bool on = false;
|
||||
@ -63,8 +69,6 @@ class Animated_Staircase : public Usermod {
|
||||
byte maxSegmentId = 1;
|
||||
byte mainSegmentId = 0;
|
||||
|
||||
bool saveState = false;
|
||||
|
||||
// These values are used by the API to read the
|
||||
// last sensor state, or trigger a sensor
|
||||
// through the API
|
||||
@ -73,9 +77,24 @@ class Animated_Staircase : public Usermod {
|
||||
bool bottomSensorRead = 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() {
|
||||
mainSegmentId = strip.getMainSegmentId();
|
||||
WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId);
|
||||
// mainSegmentId = strip.getMainSegmentId();
|
||||
// WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId);
|
||||
WS2812FX::Segment* segments = strip.getSegments();
|
||||
for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) {
|
||||
if (!segments->isActive()) {
|
||||
@ -134,17 +153,15 @@ class Animated_Staircase : public Usermod {
|
||||
if ((millis() - lastScanTime) > scanDelay) {
|
||||
lastScanTime = millis();
|
||||
|
||||
#ifdef BOTTOM_PIR_PIN
|
||||
bottomSensorRead = bottomSensorWrite || (digitalRead(BOTTOM_PIR_PIN) == HIGH);
|
||||
#else
|
||||
bottomSensorRead = bottomSensorWrite || ultrasoundRead(BOTTOM_TRIGGER_PIN, BOTTOM_ECHO_PIN, bottomMaxTimeUs);
|
||||
#endif
|
||||
if (!useUSSensorBottom)
|
||||
bottomSensorRead = bottomSensorWrite || (digitalRead(bottomPIRorTriggerPin) == HIGH);
|
||||
else
|
||||
bottomSensorRead = bottomSensorWrite || ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxTimeUs);
|
||||
|
||||
#ifdef TOP_PIR_PIN
|
||||
topSensorRead = topSensorWrite || (digitalRead(TOP_PIR_PIN) == HIGH);
|
||||
#else
|
||||
topSensorRead = topSensorWrite || ultrasoundRead(TOP_TRIGGER_PIN, TOP_ECHO_PIN, topMaxTimeUs);
|
||||
#endif
|
||||
if (!useUSSensorTop)
|
||||
topSensorRead = topSensorWrite || (digitalRead(topPIRorTriggerPin) == HIGH);
|
||||
else
|
||||
topSensorRead = topSensorWrite || ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxTimeUs);
|
||||
|
||||
// Values read, reset the flags for next API call
|
||||
topSensorWrite = false;
|
||||
@ -160,9 +177,9 @@ class Animated_Staircase : public Usermod {
|
||||
swipe = bottomSensorRead;
|
||||
|
||||
if (swipe) {
|
||||
Serial.println("ON -> Swipe up.");
|
||||
DEBUG_PRINTLN(F("ON -> Swipe up."));
|
||||
} else {
|
||||
Serial.println("ON -> Swipe down.");
|
||||
DEBUG_PRINTLN(F("ON -> Swipe down."));
|
||||
}
|
||||
|
||||
if (onIndex == offIndex) {
|
||||
@ -181,15 +198,16 @@ class Animated_Staircase : public Usermod {
|
||||
}
|
||||
|
||||
void autoPowerOff() {
|
||||
// TODO: add logic to wait until PIR sensor deactivates
|
||||
if (on && ((millis() - lastSwitchTime) > on_time_ms)) {
|
||||
// Swipe OFF in the direction of the last sensor detection
|
||||
swipe = lastSensor;
|
||||
on = false;
|
||||
|
||||
if (swipe) {
|
||||
Serial.println("OFF -> Swipe up.");
|
||||
DEBUG_PRINTLN(F("OFF -> Swipe up."));
|
||||
} 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) {
|
||||
lastTime = millis();
|
||||
|
||||
byte oldOnIndex = onIndex;
|
||||
byte oldOffIndex = offIndex;
|
||||
// byte oldOnIndex = onIndex;
|
||||
// byte oldOffIndex = offIndex;
|
||||
|
||||
if (on) {
|
||||
// Turn on all segments
|
||||
@ -217,103 +235,44 @@ class Animated_Staircase : public Usermod {
|
||||
}
|
||||
}
|
||||
|
||||
void writeSettingsToJson(JsonObject& root) {
|
||||
JsonObject staircase = root["staircase"];
|
||||
if (staircase.isNull()) {
|
||||
staircase = root.createNestedObject("staircase");
|
||||
}
|
||||
staircase["enabled"] = enabled;
|
||||
staircase["segment-delay-ms"] = segment_delay_ms;
|
||||
staircase["on-time-s"] = on_time_ms / 1000;
|
||||
|
||||
#ifdef TOP_TRIGGER_PIN
|
||||
staircase["top-echo-us"] = topMaxTimeUs;
|
||||
#endif
|
||||
#ifdef BOTTOM_TRIGGER_PIN
|
||||
staircase["bottom-echo-us"] = bottomMaxTimeUs;
|
||||
#endif
|
||||
// send sesnor values to JSON API
|
||||
void writeSensorsToJson(JsonObject& staircase) {
|
||||
staircase[F("top-sensor")] = topSensorRead;
|
||||
staircase[F("bottom-sensor")] = bottomSensorRead;
|
||||
}
|
||||
|
||||
void writeSensorsToJson(JsonObject& root) {
|
||||
JsonObject staircase = root["staircase"];
|
||||
if (staircase.isNull()) {
|
||||
staircase = root.createNestedObject("staircase");
|
||||
}
|
||||
staircase["top-sensor"] = topSensorRead;
|
||||
staircase["bottom-sensor"] = bottomSensorRead;
|
||||
}
|
||||
|
||||
bool readSettingsFromJson(JsonObject& root) {
|
||||
JsonObject staircase = root["staircase"];
|
||||
bool changed = false;
|
||||
|
||||
bool shouldEnable = staircase["enabled"] | enabled;
|
||||
if (shouldEnable != enabled) {
|
||||
enable(shouldEnable);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
unsigned long c_segment_delay_ms = staircase["segment-delay-ms"] | segment_delay_ms;
|
||||
if (c_segment_delay_ms != segment_delay_ms) {
|
||||
segment_delay_ms = c_segment_delay_ms;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
unsigned long c_on_time_ms = (staircase["on-time-s"] | (on_time_ms / 1000)) * 1000;
|
||||
if (c_on_time_ms != on_time_ms) {
|
||||
on_time_ms = c_on_time_ms;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
#ifdef TOP_TRIGGER_PIN
|
||||
unsigned int c_topMaxTimeUs = staircase["top-echo-us"] | topMaxTimeUs;
|
||||
if (c_topMaxTimeUs != topMaxTimeUs) {
|
||||
topMaxTimeUs = c_topMaxTimeUs;
|
||||
changed = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef BOTTOM_TRIGGER_PIN
|
||||
unsigned int c_bottomMaxTimeUs = staircase["bottom-echo-us"] | bottomMaxTimeUs;
|
||||
if (c_bottomMaxTimeUs != bottomMaxTimeUs) {
|
||||
bottomMaxTimeUs = c_bottomMaxTimeUs;
|
||||
changed = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void readSensorsFromJson(JsonObject& root) {
|
||||
JsonObject staircase = root["staircase"];
|
||||
bottomSensorWrite = bottomSensorRead || (staircase["bottom-sensor"].as<bool>());
|
||||
topSensorWrite = topSensorRead || (staircase["top-sensor"].as<bool>());
|
||||
// allow overrides from JSON API
|
||||
void readSensorsFromJson(JsonObject& staircase) {
|
||||
bottomSensorWrite = bottomSensorRead || (staircase[F("bottom-sensor")].as<bool>());
|
||||
topSensorWrite = topSensorRead || (staircase[F("top-sensor")].as<bool>());
|
||||
}
|
||||
|
||||
void enable(bool enable) {
|
||||
if (enable) {
|
||||
Serial.println("Animated Staircase enabled.");
|
||||
Serial.print("Delay between steps: ");
|
||||
Serial.print(segment_delay_ms, DEC);
|
||||
Serial.print(" milliseconds.\nStairs switch off after: ");
|
||||
Serial.print(on_time_ms / 1000, DEC);
|
||||
Serial.println(" seconds.");
|
||||
DEBUG_PRINTLN(F("Animated Staircase enabled."));
|
||||
DEBUG_PRINT(F("Delay between steps: "));
|
||||
DEBUG_PRINT(segment_delay_ms);
|
||||
DEBUG_PRINT(F(" milliseconds.\nStairs switch off after: "));
|
||||
DEBUG_PRINT(on_time_ms / 1000);
|
||||
DEBUG_PRINTLN(F(" seconds."));
|
||||
|
||||
#ifdef BOTTOM_PIR_PIN
|
||||
pinMode(BOTTOM_PIR_PIN, INPUT);
|
||||
#else
|
||||
pinMode(BOTTOM_TRIGGER_PIN, OUTPUT);
|
||||
pinMode(BOTTOM_ECHO_PIN, INPUT);
|
||||
#endif
|
||||
// TODO: attach interrupts
|
||||
if (!useUSSensorBottom)
|
||||
pinMode(bottomPIRorTriggerPin, INPUT);
|
||||
else {
|
||||
pinMode(bottomPIRorTriggerPin, OUTPUT);
|
||||
pinMode(bottomEchoPin, INPUT);
|
||||
}
|
||||
|
||||
#ifdef TOP_PIR_PIN
|
||||
pinMode(TOP_PIR_PIN, INPUT);
|
||||
#else
|
||||
pinMode(TOP_TRIGGER_PIN, OUTPUT);
|
||||
pinMode(TOP_ECHO_PIN, INPUT);
|
||||
#endif
|
||||
if (!useUSSensorTop)
|
||||
pinMode(topPIRorTriggerPin, INPUT);
|
||||
else {
|
||||
pinMode(topPIRorTriggerPin, OUTPUT);
|
||||
pinMode(topEchoPin, INPUT);
|
||||
}
|
||||
} else {
|
||||
// Restore segment options
|
||||
WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId);
|
||||
// WS2812FX::Segment mainsegment = strip.getSegment(mainSegmentId);
|
||||
WS2812FX::Segment* segments = strip.getSegments();
|
||||
for (int i = 0; i < MAX_NUM_SEGMENTS; i++, segments++) {
|
||||
if (!segments->isActive()) {
|
||||
@ -323,47 +282,57 @@ class Animated_Staircase : public Usermod {
|
||||
segments->setOption(SEG_OPTION_ON, 1, 1);
|
||||
}
|
||||
colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE);
|
||||
Serial.println("Animated Staircase disabled.");
|
||||
DEBUG_PRINTLN(F("Animated Staircase disabled."));
|
||||
}
|
||||
enabled = enable;
|
||||
}
|
||||
|
||||
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() {
|
||||
// Write changed settings from to flash (see readFromJsonState())
|
||||
if (saveState) {
|
||||
serializeConfig();
|
||||
saveState = false;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) return;
|
||||
checkSensors();
|
||||
autoPowerOff();
|
||||
updateSwipe();
|
||||
|
||||
}
|
||||
|
||||
uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; }
|
||||
|
||||
/*
|
||||
* Shows configuration settings to the json API. This object looks like:
|
||||
*
|
||||
* "staircase" : {
|
||||
* "enabled" : true
|
||||
* "segment-delay-ms" : 150,
|
||||
* "on-time-s" : 5
|
||||
* }
|
||||
*
|
||||
*/
|
||||
void addToJsonState(JsonObject& root) {
|
||||
writeSettingsToJson(root);
|
||||
writeSensorsToJson(root);
|
||||
Serial.println("Staircase config exposed in API.");
|
||||
JsonObject staircase = root[FPSTR(_name)];
|
||||
if (staircase.isNull()) {
|
||||
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)
|
||||
*/
|
||||
void readFromJsonState(JsonObject& root) {
|
||||
// The call to serializeConfig() must be done in the main loop,
|
||||
// so we set a flag to signal the main loop to save state.
|
||||
saveState = readSettingsFromJson(root);
|
||||
readSensorsFromJson(root);
|
||||
Serial.println("Staircase config read from API.");
|
||||
if (!initDone) return; // prevent crash on boot applyPreset()
|
||||
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
|
||||
}
|
||||
readSensorsFromJson(root);
|
||||
DEBUG_PRINTLN(F("Staircase sensor state read from API."));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Writes the configuration to internal flash memory.
|
||||
*/
|
||||
void addToConfig(JsonObject& root) {
|
||||
writeSettingsToJson(root);
|
||||
Serial.println("Staircase config saved.");
|
||||
JsonObject staircase = root[FPSTR(_name)];
|
||||
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.
|
||||
*/
|
||||
void readFromConfig(JsonObject& root) {
|
||||
readSettingsFromJson(root);
|
||||
Serial.println("Staircase config loaded.");
|
||||
bool oldUseUSSensorTop = useUSSensorTop;
|
||||
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) {
|
||||
JsonArray usermodEnabled =
|
||||
staircase.createNestedArray("Staircase enabled"); // name
|
||||
JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase enabled")); // name
|
||||
usermodEnabled.add("yes"); // value
|
||||
|
||||
JsonArray segmentDelay =
|
||||
staircase.createNestedArray("Delay between stairs"); // name
|
||||
JsonArray segmentDelay = staircase.createNestedArray(F("Delay between stairs")); // name
|
||||
segmentDelay.add(segment_delay_ms); // value
|
||||
segmentDelay.add(" milliseconds"); // unit
|
||||
segmentDelay.add("ms"); // unit
|
||||
|
||||
JsonArray onTime =
|
||||
staircase.createNestedArray("Power-off stairs after"); // name
|
||||
JsonArray onTime = staircase.createNestedArray(F("Power-off stairs after")); // name
|
||||
onTime.add(on_time_ms / 1000); // value
|
||||
onTime.add(" seconds"); // unit
|
||||
onTime.add("s"); // unit
|
||||
} else {
|
||||
JsonArray usermodEnabled =
|
||||
staircase.createNestedArray("Staircase enabled"); // name
|
||||
JsonArray usermodEnabled = staircase.createNestedArray(F("Staircase enabled")); // name
|
||||
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";
|
||||
|
@ -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
|
@ -20,44 +20,10 @@ Edit `usermods_list.cpp`:
|
||||
2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file
|
||||
3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function.
|
||||
|
||||
Edit `Animated_Staircase_config.h`:
|
||||
1. Open `usermods/Animated_Staircase/Animated_Staircase_config.h`
|
||||
2. To use PIR sensors, change these lines to match your setup:
|
||||
Using D7 and D6 pin notation as used on several boards:
|
||||
|
||||
```cpp
|
||||
#define TOP_PIR_PIN D7
|
||||
#define BOTTOM_PIR_PIN D6
|
||||
```
|
||||
|
||||
Or using GPIO numbering for pins 25 and 26:
|
||||
```cpp
|
||||
#define TOP_PIR_PIN 26
|
||||
#define BOTTOM_PIR_PIN 25
|
||||
```
|
||||
|
||||
To use Ultrasonic HC-SR04 sensors instead of (one of the) PIR sensors,
|
||||
uncomment one of the PIR sensor lines and adjust the pin numbers for the
|
||||
connected Ultrasonic sensor. In the example below we use an Ultrasonic
|
||||
sensor at the bottom of the stairs:
|
||||
|
||||
```cpp
|
||||
#define TOP_PIR_PIN 32
|
||||
//#define BOTTOM_PIR_PIN D6 /* This PIR sensor is disabled */
|
||||
|
||||
#ifndef TOP_PIR_PIN
|
||||
#define TOP_SIGNAL_PIN D2
|
||||
#define TOP_ECHO_PIN D3
|
||||
#endif
|
||||
|
||||
#ifndef BOTTOM_PIR_PIN /* If the bottom PIR is disabled, */
|
||||
#define BOTTOM_SIGNAL_PIN 25 /* This Ultrasonic sensor is used */
|
||||
#define BOTTOM_ECHO_PIN 26
|
||||
#endif
|
||||
```
|
||||
|
||||
After these modifications, compile and upload your WLED binary to your board
|
||||
and check the WLED info page to see if this usermod is enabled.
|
||||
You can configure usermod using Usermods settings page.
|
||||
Please enter GPIO pins for PIR sensors or ultrasonic sensor (trigger and echo).
|
||||
If you use PIR sensor enter -1 for echo pin.
|
||||
Maximum distance for ultrasonic sensor can be configured as a time needed for echo (see below).
|
||||
|
||||
## Hardware installation
|
||||
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/>
|
||||
www.rolfje.com
|
||||
|
||||
## Change log
|
||||
2021-04
|
||||
* Adaptation for runtime configuration.
|
@ -10,27 +10,14 @@ The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wik
|
||||
## Webinterface
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
**I recommend to deactivate the sensor before an OTA update and activate it again afterwards**.
|
||||
|
||||
## Sensor connection
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### There are two options to get access to the usermod instance:
|
||||
@ -98,12 +83,19 @@ class MyUsermod : public Usermod {
|
||||
//...
|
||||
|
||||
void togglePIRSensor() {
|
||||
if (PIRsensorSwitch::GetInstance() != nullptr) {
|
||||
PIRsensorSwitch::GetInstance()->EnablePIRsensor(!PIRsensorSwitch::GetInstance()->PIRsensorEnabled());
|
||||
#ifdef USERMOD_PIR_SENSOR_SWITCH
|
||||
PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH);
|
||||
if (PIRsensor != nullptr) {
|
||||
PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
//...
|
||||
};
|
||||
```
|
||||
|
||||
Have fun - @gegu
|
||||
|
||||
## Change log
|
||||
2021-04
|
||||
* Adaptation for runtime configuration.
|
@ -2,6 +2,15 @@
|
||||
|
||||
#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.
|
||||
* The strip will be switched on and the off timer will be resetted when the sensor goes HIGH.
|
||||
@ -30,24 +39,11 @@ public:
|
||||
/**
|
||||
* constructor
|
||||
*/
|
||||
PIRsensorSwitch()
|
||||
{
|
||||
// set static instance pointer
|
||||
PIRsensorSwitchInstance(this);
|
||||
}
|
||||
PIRsensorSwitch() {}
|
||||
/**
|
||||
* desctructor
|
||||
*/
|
||||
~PIRsensorSwitch()
|
||||
{
|
||||
PIRsensorSwitchInstance(nullptr, true);
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* return the instance pointer of the class
|
||||
*/
|
||||
static PIRsensorSwitch *GetInstance() { return PIRsensorSwitchInstance(); }
|
||||
~PIRsensorSwitch() {}
|
||||
|
||||
/**
|
||||
* Enable/Disable the PIR sensor
|
||||
@ -60,19 +56,24 @@ public:
|
||||
|
||||
private:
|
||||
// PIR sensor pin
|
||||
const uint8_t PIRsensorPin = 13; // D7 on D1 mini
|
||||
int8_t PIRsensorPin = PIR_SENSOR_PIN;
|
||||
// notification mode for colorUpdated()
|
||||
const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE
|
||||
// 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
|
||||
uint32_t m_offTimerStart = 0;
|
||||
// current PIR sensor pin state
|
||||
byte m_PIRsensorPinState = LOW;
|
||||
// PIR sensor enabled - ISR attached
|
||||
bool m_PIRenabled = true;
|
||||
// state if serializeConfig() should be called
|
||||
bool m_updateConfig = false;
|
||||
// status of initialisation
|
||||
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
|
||||
@ -84,11 +85,6 @@ private:
|
||||
*/
|
||||
static void IRAM_ATTR ISR_PIRstateChange();
|
||||
|
||||
/**
|
||||
* Set/get instance pointer
|
||||
*/
|
||||
static PIRsensorSwitch *PIRsensorSwitchInstance(PIRsensorSwitch *pInstance = nullptr, bool bRemoveInstance = false);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Initilize/reset switch off timer
|
||||
@ -121,6 +128,7 @@ private:
|
||||
{
|
||||
m_offTimerStart = 0;
|
||||
switchStrip(true);
|
||||
publishMqtt("on");
|
||||
}
|
||||
else if (bri != 0)
|
||||
{
|
||||
@ -143,6 +151,7 @@ private:
|
||||
if (m_PIRenabled == true)
|
||||
{
|
||||
switchStrip(false);
|
||||
publishMqtt("off");
|
||||
}
|
||||
m_offTimerStart = 0;
|
||||
return true;
|
||||
@ -159,13 +168,20 @@ public:
|
||||
*/
|
||||
void setup()
|
||||
{
|
||||
// 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);
|
||||
// pin retrieved from cfg.json (readFromConfig()) prior to running setup()
|
||||
if (!pinManager.allocatePin(PIRsensorPin,false)) {
|
||||
PIRsensorPin = -1; // allocation failed
|
||||
m_PIRenabled = false;
|
||||
DEBUG_PRINTLN(F("PIRSensorSwitch pin allocation failed."));
|
||||
} 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()
|
||||
{
|
||||
if (!updatePIRsensorState())
|
||||
{
|
||||
if (!updatePIRsensorState()) {
|
||||
handleOffTimer();
|
||||
if (m_updateConfig)
|
||||
{
|
||||
serializeConfig();
|
||||
m_updateConfig = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,69 +209,70 @@ public:
|
||||
*/
|
||||
void addToJsonInfo(JsonObject &root)
|
||||
{
|
||||
//this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object
|
||||
// the value contains a button to toggle the sensor enabled/disabled
|
||||
JsonObject user = root["u"];
|
||||
if (user.isNull())
|
||||
user = root.createNestedObject("u");
|
||||
|
||||
JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name
|
||||
String uiDomString = "<button class=\"btn infobtn\" onclick=\"requestJson({PIRenabled:";
|
||||
/*
|
||||
JsonArray infoArr = user.createNestedArray(F("<i class=\"icons\"></i> PIR sensor state")); //name
|
||||
String uiDomString = F("<button class=\"btn infobtn\" onclick=\"requestJson({PIRenabled:");
|
||||
String sensorStateInfo;
|
||||
|
||||
// PIR sensor state
|
||||
if (m_PIRenabled)
|
||||
{
|
||||
uiDomString += "false";
|
||||
sensorStateInfo = (m_PIRsensorPinState != LOW ? "active" : "inactive"); //value
|
||||
sensorStateInfo = (m_PIRsensorPinState != LOW ? FPSTR(F("active")) : FPSTR(F("inactive"))); //value
|
||||
}
|
||||
else
|
||||
{
|
||||
uiDomString += "true";
|
||||
sensorStateInfo = "Disabled !";
|
||||
sensorStateInfo = F("Disabled!");
|
||||
}
|
||||
uiDomString += "});return false;\">";
|
||||
uiDomString += F("});return false;\">");
|
||||
uiDomString += sensorStateInfo;
|
||||
uiDomString += "</button>";
|
||||
uiDomString += F("</button>");
|
||||
infoArr.add(uiDomString); //value
|
||||
|
||||
//this code adds "u":{"⏲ switch off timer":uiDomString} to the info object
|
||||
uiDomString = "⏲ 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)
|
||||
*/
|
||||
if (m_PIRenabled)
|
||||
{
|
||||
uiDomString = "";
|
||||
unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000;
|
||||
if (offSeconds >= 3600)
|
||||
/*
|
||||
JsonArray infoArr = user.createNestedArray(F("PIR switch-off timer after")); //name
|
||||
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\"></i>");
|
||||
JsonArray infoArr = user.createNestedArray(uiDomString); // timer value
|
||||
if (m_offTimerStart > 0)
|
||||
{
|
||||
uiDomString += (offSeconds / 3600);
|
||||
uiDomString += " hours ";
|
||||
offSeconds %= 3600;
|
||||
uiDomString = "";
|
||||
unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000;
|
||||
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)
|
||||
{
|
||||
root["PIRenabled"] = m_PIRenabled;
|
||||
root["PIRoffSec"] = (m_switchOffDelay / 1000);
|
||||
root[FPSTR(_enabled)] = m_PIRenabled;
|
||||
root[FPSTR(_switchOffDelay)] = (m_switchOffDelay / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -285,26 +296,40 @@ after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
|
||||
*/
|
||||
void readFromJsonState(JsonObject &root)
|
||||
{
|
||||
if (root["PIRoffSec"] != nullptr)
|
||||
{
|
||||
m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as<unsigned long>())));
|
||||
m_updateConfig = true;
|
||||
if (root[FPSTR(_switchOffDelay)] != nullptr) {
|
||||
m_switchOffDelay = (1000 * max(60UL, min(43200UL, root[FPSTR(_switchOffDelay)].as<unsigned long>())));
|
||||
}
|
||||
/*
|
||||
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["PIRenabled"] && !m_PIRenabled)
|
||||
{
|
||||
if (root[FPSTR(_enabled)] != nullptr) {
|
||||
if (root[FPSTR(_enabled)] && !m_PIRenabled && PIRsensorPin >= 0) {
|
||||
attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE);
|
||||
newPIRsensorState(true, true);
|
||||
}
|
||||
else if (m_PIRenabled)
|
||||
{
|
||||
} else if (m_PIRenabled && PIRsensorPin >= 0) {
|
||||
detachInterrupt(PIRsensorPin);
|
||||
}
|
||||
m_PIRenabled = root["PIRenabled"];
|
||||
m_updateConfig = true;
|
||||
m_PIRenabled = root[FPSTR(_enabled)];
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
@ -312,19 +337,72 @@ after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
|
||||
*/
|
||||
void addToConfig(JsonObject &root)
|
||||
{
|
||||
JsonObject top = root.createNestedObject("PIRsensorSwitch");
|
||||
top["PIRenabled"] = m_PIRenabled;
|
||||
top["PIRoffSec"] = m_switchOffDelay;
|
||||
JsonObject top = root.createNestedObject(FPSTR(_name));
|
||||
top[FPSTR(_enabled)] = m_PIRenabled;
|
||||
top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000;
|
||||
top["pin"] = PIRsensorPin;
|
||||
DEBUG_PRINTLN(F("PIR config saved."));
|
||||
}
|
||||
|
||||
/**
|
||||
* restore the changeable values
|
||||
* readFromConfig() is called before setup() to populate properties from values stored in cfg.json
|
||||
*/
|
||||
void readFromConfig(JsonObject &root)
|
||||
{
|
||||
JsonObject top = root["PIRsensorSwitch"];
|
||||
m_PIRenabled = (top["PIRenabled"] != nullptr ? top["PIRenabled"] : true);
|
||||
m_switchOffDelay = top["PIRoffSec"] | m_switchOffDelay;
|
||||
bool oldEnabled = m_PIRenabled;
|
||||
int8_t oldPin = PIRsensorPin;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
PIRsensorSwitch *PIRsensorSwitch::PIRsensorSwitchInstance(PIRsensorSwitch *pInstance, bool bRemoveInstance)
|
||||
{
|
||||
static PIRsensorSwitch *s_pPIRsensorSwitch = nullptr;
|
||||
if (pInstance != nullptr || bRemoveInstance)
|
||||
{
|
||||
s_pPIRsensorSwitch = pInstance;
|
||||
}
|
||||
return s_pPIRsensorSwitch;
|
||||
}
|
||||
// strings to reduce flash memory usage (used more than twice)
|
||||
const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch";
|
||||
const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled";
|
||||
const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec";
|
||||
|
@ -14,10 +14,10 @@ Copy the example `platformio_override.ini` to the root directory. This file sho
|
||||
### Define Your Options
|
||||
|
||||
* `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
|
||||
|
||||
All parameters can be configured at runtime using Usermods settings page.
|
||||
|
||||
## 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 =
|
||||
...
|
||||
#For use SSD1306 OLED display uncomment following
|
||||
U8g2@~2.27.3
|
||||
#For Dallas sensor uncomment following 2 lines
|
||||
DallasTemperature@~3.8.0
|
||||
#For Dallas sensor uncomment following line
|
||||
OneWire@~2.3.5
|
||||
...
|
||||
```
|
||||
@ -56,3 +53,5 @@ lib_deps =
|
||||
* Do not report low temperatures that indicate an error to mqtt
|
||||
* Disable plugin if temperature sensor not detected
|
||||
* Report the number of seconds until the first read in the info screen instead of sensor error
|
||||
2021-04
|
||||
* Adaptation for runtime configuration.
|
@ -2,15 +2,16 @@
|
||||
|
||||
#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
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#define TEMPERATURE_PIN 18
|
||||
#else //ESP8266 boards
|
||||
#define TEMPERATURE_PIN 14
|
||||
#endif
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#define TEMPERATURE_PIN 18
|
||||
#else //ESP8266 boards
|
||||
#define TEMPERATURE_PIN 14
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// the frequency to check temperature, 1 minute
|
||||
@ -20,19 +21,20 @@
|
||||
|
||||
// how many seconds after boot to take first measurement, 20 seconds
|
||||
#ifndef USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT
|
||||
#define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000
|
||||
#define USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT 20000
|
||||
#endif
|
||||
|
||||
OneWire oneWire(TEMPERATURE_PIN);
|
||||
DallasTemperature sensor(&oneWire);
|
||||
|
||||
class UsermodTemperature : public Usermod {
|
||||
|
||||
private:
|
||||
// The device's unique 64-bit serial code stored in on-board ROM.
|
||||
// Reading directly from the sensor device address is faster than
|
||||
// reading from index. When reading by index, DallasTemperature
|
||||
// must first look up the device address at the specified index.
|
||||
DeviceAddress sensorDeviceAddress;
|
||||
|
||||
bool initDone = false;
|
||||
OneWire *oneWire;
|
||||
// GPIO pin used for sensor (with a default compile-time fallback)
|
||||
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
|
||||
unsigned long lastMeasurement = UINT32_MAX - (USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT);
|
||||
// last time requestTemperatures was called
|
||||
@ -42,85 +44,120 @@ class UsermodTemperature : public Usermod {
|
||||
float temperature = -100; // default to -100, DS18B20 only goes down to -50C
|
||||
// indicates requestTemperatures has been called but the sensor measurement is not complete
|
||||
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
|
||||
// measurement
|
||||
bool getTemperatureComplete = false;
|
||||
bool readTemperatureComplete = false;
|
||||
// flag set at startup if DS18B20 sensor not found, avoids trying to keep getting
|
||||
// temperature if flashed to a board without a sensor attached
|
||||
bool disabled = false;
|
||||
|
||||
void requestTemperatures() {
|
||||
// there is requestTemperaturesByAddress however it
|
||||
// appears to do more work,
|
||||
// TODO: measure exection time difference
|
||||
sensor.requestTemperatures();
|
||||
lastTemperaturesRequest = millis();
|
||||
waitingForConversion = true;
|
||||
// strings to reduce flash memory usage (used more than twice)
|
||||
static const char _name[];
|
||||
static const char _enabled[];
|
||||
static const char _readInterval[];
|
||||
|
||||
//Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013
|
||||
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() {
|
||||
if (strip.isUpdating()) return;
|
||||
#ifdef USERMOD_DALLASTEMPERATURE_CELSIUS
|
||||
temperature = sensor.getTempC(sensorDeviceAddress);
|
||||
#else
|
||||
temperature = sensor.getTempF(sensorDeviceAddress);
|
||||
#endif
|
||||
void requestTemperatures() {
|
||||
readDallas();
|
||||
lastTemperaturesRequest = millis();
|
||||
waitingForConversion = true;
|
||||
DEBUG_PRINTLN(F("Requested temperature."));
|
||||
}
|
||||
|
||||
void readTemperature() {
|
||||
temperature = readDallas();
|
||||
lastMeasurement = millis();
|
||||
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:
|
||||
|
||||
|
||||
void setup() {
|
||||
sensor.begin();
|
||||
|
||||
// get the unique 64-bit serial code stored in on-board ROM
|
||||
// if getAddress returns false, the sensor was not found
|
||||
disabled = !sensor.getAddress(sensorDeviceAddress, 0);
|
||||
|
||||
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;
|
||||
int retries = 10;
|
||||
// pin retrieved from cfg.json (readFromConfig()) prior to running setup()
|
||||
if (!pinManager.allocatePin(temperaturePin,false)) {
|
||||
temperaturePin = -1; // allocation failed
|
||||
disabled = true;
|
||||
DEBUG_PRINTLN(F("Temperature pin allocation failed."));
|
||||
} 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() {
|
||||
if (disabled || strip.isUpdating()) return;
|
||||
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
// check to see if we are due for taking a measurement
|
||||
// lastMeasurement will not be updated until the conversion
|
||||
// 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
|
||||
if (!waitingForConversion)
|
||||
{
|
||||
if (!waitingForConversion) {
|
||||
requestTemperatures();
|
||||
return;
|
||||
}
|
||||
|
||||
// we were waiting for a conversion to complete, have we waited log enough?
|
||||
if (now - lastTemperaturesRequest >= 94 /* 93.75ms per the datasheet */)
|
||||
{
|
||||
getTemperature();
|
||||
|
||||
if (now - lastTemperaturesRequest >= 100 /* 93.75ms per the datasheet but can be up to 750ms */) {
|
||||
readTemperature();
|
||||
|
||||
if (WLED_MQTT_CONNECTED) {
|
||||
char subuf[45];
|
||||
char subuf[64];
|
||||
strcpy(subuf, mqttDeviceTopic);
|
||||
if (-100 <= temperature) {
|
||||
// dont publish super low temperature as the graph will get messed up
|
||||
@ -128,6 +165,8 @@ class UsermodTemperature : public Usermod {
|
||||
// reading the sensor
|
||||
strcat_P(subuf, PSTR("/temperature"));
|
||||
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 {
|
||||
// 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) {
|
||||
// dont add temperature to info if we are disabled
|
||||
if (disabled) return;
|
||||
|
||||
JsonObject user = root[F("u")];
|
||||
if (user.isNull()) user = root.createNestedObject(F("u"));
|
||||
JsonObject user = root["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
|
||||
// that we are still waiting for the first measurement
|
||||
temp.add((USERMOD_DALLASTEMPERATURE_FIRST_MEASUREMENT_AT - millis()) / 1000);
|
||||
@ -158,12 +213,85 @@ class UsermodTemperature : public Usermod {
|
||||
return;
|
||||
}
|
||||
|
||||
temp.add(temperature);
|
||||
#ifdef USERMOD_DALLASTEMPERATURE_CELSIUS
|
||||
temp.add(F("°C"));
|
||||
#else
|
||||
temp.add(F("°F"));
|
||||
#endif
|
||||
temp.add(degC ? temperature : (float)temperature * 1.8f + 32);
|
||||
if (degC) temp.add(F("°C"));
|
||||
else temp.add(F("°F"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
@ -171,3 +299,8 @@ class UsermodTemperature : public Usermod {
|
||||
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";
|
55
usermods/multi_relay/readme.md
Normal file
55
usermods/multi_relay/readme.md
Normal 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.
|
434
usermods/multi_relay/usermod_multi_relay.h
Normal file
434
usermods/multi_relay/usermod_multi_relay.h
Normal 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";
|
@ -29,9 +29,9 @@ This file should be placed in the same directory as `platformio.ini`.
|
||||
### Define Your Options
|
||||
|
||||
* `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)
|
||||
* `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
|
||||
* `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)
|
||||
|
||||
You can configure auto-save parameters using Usermods settings page.
|
||||
|
||||
### PlatformIO requirements
|
||||
|
||||
@ -43,3 +43,5 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`.
|
||||
|
||||
2021-02
|
||||
* First public release
|
||||
2021-04
|
||||
* Adaptation for runtime configuration.
|
@ -2,9 +2,8 @@
|
||||
|
||||
#include "wled.h"
|
||||
|
||||
//
|
||||
// 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
|
||||
// * effect speed
|
||||
@ -12,45 +11,34 @@
|
||||
// * mode (effect)
|
||||
// * 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
|
||||
// extend the "settle" window).
|
||||
//
|
||||
// It will additionally load preset AUTOSAVE_PRESET_NUM at startup.
|
||||
// during the first `loop()`. Reasoning below.
|
||||
// It can be configured to load auto saved preset at startup,
|
||||
// during the first `loop()`.
|
||||
//
|
||||
// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod
|
||||
// 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
|
||||
#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"
|
||||
// format: "~ MM-DD HH:MM:SS ~"
|
||||
#define PRESET_NAME_BUFFER_SIZE 25
|
||||
|
||||
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 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 knownEffectSpeed = 0;
|
||||
@ -58,35 +46,65 @@ class AutoSaveUsermod : public Usermod {
|
||||
uint8_t knownMode = 0;
|
||||
uint8_t knownPalette = 0;
|
||||
|
||||
#ifdef USERMOD_FOUR_LINE_DISLAY
|
||||
#ifdef USERMOD_FOUR_LINE_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:
|
||||
|
||||
// gets called once at boot. Do all initialization that doesn't depend on
|
||||
// network here
|
||||
void setup() {
|
||||
#ifdef USERMOD_FOUR_LINE_DISLAY
|
||||
// This Usermod has enhanced funcionality if
|
||||
// FourLineDisplayUsermod is available.
|
||||
display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP);
|
||||
#endif
|
||||
#ifdef USERMOD_FOUR_LINE_DISPLAY
|
||||
// This Usermod has enhanced funcionality if
|
||||
// FourLineDisplayUsermod is available.
|
||||
display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP);
|
||||
#endif
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
// gets called every time WiFi is (re-)connected. Initialize own network
|
||||
// interfaces here
|
||||
void connected() {}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Da loop.
|
||||
*/
|
||||
void loop() {
|
||||
if (!autoSaveAfterSec || !enabled) return; // setting 0 as autosave seconds disables autosave
|
||||
|
||||
unsigned long now = millis();
|
||||
uint8_t currentMode = strip.getMode();
|
||||
uint8_t currentPalette = strip.getSegment(0).palette;
|
||||
if (firstLoop) {
|
||||
firstLoop = false;
|
||||
applyPreset(AUTOSAVE_PRESET_NUM);
|
||||
if (applyAutoSaveOnBoot) applyPreset(autoSavePreset);
|
||||
knownBrightness = bri;
|
||||
knownEffectSpeed = effectSpeed;
|
||||
knownEffectIntensity = effectIntensity;
|
||||
@ -95,7 +113,7 @@ class AutoSaveUsermod : public Usermod {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long wouldAutoSaveAfter = now + AUTOSAVE_SETTLE_MS;
|
||||
unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000;
|
||||
if (knownBrightness != bri) {
|
||||
knownBrightness = bri;
|
||||
autoSaveAfter = wouldAutoSaveAfter;
|
||||
@ -121,37 +139,32 @@ class AutoSaveUsermod : public Usermod {
|
||||
}
|
||||
}
|
||||
|
||||
void saveSettings() {
|
||||
updateLocalTime();
|
||||
sprintf(presetNameBuffer,
|
||||
"Auto save %02d-%02d %02d:%02d:%02d",
|
||||
month(localTime), day(localTime),
|
||||
hour(localTime), minute(localTime), second(localTime));
|
||||
savePreset(AUTOSAVE_PRESET_NUM, true, presetNameBuffer);
|
||||
}
|
||||
|
||||
void displayOverlay() {
|
||||
#ifdef USERMOD_FOUR_LINE_DISLAY
|
||||
if (display != nullptr) {
|
||||
display->wakeDisplay();
|
||||
display->overlay("Settings", "Auto Saved", 1500);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
/*
|
||||
* 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("Autosave"));
|
||||
//data.add(F("Loaded."));
|
||||
//}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
}
|
||||
//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) {
|
||||
}
|
||||
//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.
|
||||
@ -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!
|
||||
*/
|
||||
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 :)
|
||||
*/
|
||||
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!).
|
||||
@ -188,5 +234,11 @@ class AutoSaveUsermod : public Usermod {
|
||||
uint16_t getId() {
|
||||
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";
|
||||
|
@ -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.
|
||||
|
||||
@ -19,13 +19,11 @@ This file should be placed in the same directory as `platformio.ini`.
|
||||
|
||||
### 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_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
|
||||
|
||||
@ -37,3 +35,5 @@ UI usermod folder for how to include these using `platformio_override.ini`.
|
||||
|
||||
2021-02
|
||||
* First public release
|
||||
2021-04
|
||||
* Adaptation for runtime configuration.
|
@ -24,50 +24,25 @@
|
||||
//
|
||||
|
||||
//The SCL and SDA pins are defined here.
|
||||
#ifndef FLD_PIN_SCL
|
||||
#define FLD_PIN_SCL 5
|
||||
#endif
|
||||
|
||||
#ifndef FLD_PIN_SDA
|
||||
#define FLD_PIN_SDA 4
|
||||
#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
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#ifndef FLD_PIN_SCL
|
||||
#define FLD_PIN_SCL 22
|
||||
#endif
|
||||
#ifndef FLD_PIN_SDA
|
||||
#define FLD_PIN_SDA 21
|
||||
#endif
|
||||
#else
|
||||
#define SLEEP_MODE_ENABLED false
|
||||
#define CLOCK_MODE_ENABLED false
|
||||
#ifndef FLD_PIN_SCL
|
||||
#define FLD_PIN_SCL 5
|
||||
#endif
|
||||
#ifndef FLD_PIN_SDA
|
||||
#define FLD_PIN_SDA 4
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// When to time out to the clock or blank the screen
|
||||
// if SLEEP_MODE_ENABLED.
|
||||
#define SCREEN_TIMEOUT_MS 15*1000
|
||||
#define SCREEN_TIMEOUT_MS 60*1000 // 1 min
|
||||
|
||||
#define TIME_INDENT 0
|
||||
#define DATE_INDENT 2
|
||||
@ -75,33 +50,43 @@ U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8(
|
||||
// Minimum time between redrawing screen in ms
|
||||
#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
|
||||
#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
|
||||
#define TIME_LINE 1
|
||||
#else
|
||||
#define TIME_LINE 0
|
||||
#endif
|
||||
typedef enum {
|
||||
FLD_LINE_4_BRIGHTNESS = 0,
|
||||
FLD_LINE_4_EFFECT_SPEED,
|
||||
FLD_LINE_4_EFFECT_INTENSITY,
|
||||
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 {
|
||||
|
||||
private:
|
||||
|
||||
bool initDone = false;
|
||||
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.
|
||||
bool needRedraw = true;
|
||||
|
||||
@ -118,38 +103,72 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
uint8_t knownHour = 99;
|
||||
|
||||
bool displayTurnedOff = false;
|
||||
long lastUpdate = 0;
|
||||
long lastRedraw = 0;
|
||||
long overlayUntil = 0;
|
||||
byte lineThreeType = FLD_LINE_3_BRIGHTNESS;
|
||||
unsigned long lastUpdate = 0;
|
||||
unsigned long lastRedraw = 0;
|
||||
unsigned long overlayUntil = 0;
|
||||
Line4Type lineFourType = FLD_LINE_4_BRIGHTNESS;
|
||||
// Set to 2 or 3 to mark lines 2 or 3. Other values ignored.
|
||||
byte markLineNum = 0;
|
||||
|
||||
char lineBuffer[LINE_BUFFER_SIZE];
|
||||
|
||||
char **modes_qstrings = nullptr;
|
||||
char **palettes_qstrings = nullptr;
|
||||
// strings to reduce flash memory usage (used more than twice)
|
||||
static const char _name[];
|
||||
static const char _contrast[];
|
||||
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
|
||||
// constructor reference:
|
||||
// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp
|
||||
// or check the gallery:
|
||||
// https://github.com/olikraus/u8g2/wiki/gallery
|
||||
|
||||
public:
|
||||
|
||||
// gets called once at boot. Do all initialization that doesn't depend on
|
||||
// network here
|
||||
void setup() {
|
||||
u8x8.begin();
|
||||
u8x8.setFlipMode(FLIP_MODE);
|
||||
u8x8.setPowerSave(0);
|
||||
u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255
|
||||
u8x8.setFont(u8x8_font_chroma48medium8_r);
|
||||
u8x8.DRAW_STRING(0, 0*LINE_HEIGHT, "Loading...");
|
||||
|
||||
ModeSortUsermod *modeSortUsermod = (ModeSortUsermod*) usermods.lookup(USERMOD_ID_MODE_SORT);
|
||||
modes_qstrings = modeSortUsermod->getModesQStrings();
|
||||
palettes_qstrings = modeSortUsermod->getPalettesQStrings();
|
||||
if (type==NONE) return;
|
||||
if (!pinManager.allocatePin(sclPin)) { sclPin = -1; type = NONE; return;}
|
||||
if (!pinManager.allocatePin(sdaPin)) { pinManager.deallocatePin(sclPin); sclPin = sdaPin = -1; type = NONE; return; }
|
||||
switch (type) {
|
||||
case SSD1306:
|
||||
#ifdef ESP8266
|
||||
if (!(sclPin==5 && sdaPin==4))
|
||||
u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset
|
||||
else
|
||||
#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
|
||||
@ -160,7 +179,7 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
* Da loop.
|
||||
*/
|
||||
void loop() {
|
||||
if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {
|
||||
if (millis() - lastUpdate < (clockMode?1000:refreshRate)) {
|
||||
return;
|
||||
}
|
||||
lastUpdate = millis();
|
||||
@ -168,18 +187,59 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
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
|
||||
* or if forceRedraw).
|
||||
*/
|
||||
void redraw(bool forceRedraw) {
|
||||
if (type==NONE) return;
|
||||
if (overlayUntil > 0) {
|
||||
if (millis() >= overlayUntil) {
|
||||
// Time to display the overlay has elapsed.
|
||||
overlayUntil = 0;
|
||||
forceRedraw = true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// We are still displaying the overlay
|
||||
// Don't redraw.
|
||||
return;
|
||||
@ -208,22 +268,46 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
if (!needRedraw) {
|
||||
// Nothing to change.
|
||||
// Turn off display after 3 minutes with no change.
|
||||
if(SLEEP_MODE_ENABLED && !displayTurnedOff &&
|
||||
(millis() - lastRedraw > SCREEN_TIMEOUT_MS)) {
|
||||
if(sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) {
|
||||
// We will still check if there is a change in redraw()
|
||||
// and turn it back on if it changed.
|
||||
knownHour = 99; // force screen clear
|
||||
sleepOrClock(true);
|
||||
}
|
||||
else if (displayTurnedOff && CLOCK_MODE_ENABLED) {
|
||||
} else if (displayTurnedOff && clockMode) {
|
||||
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;
|
||||
} else {
|
||||
knownHour = 99; // force time display
|
||||
clear();
|
||||
}
|
||||
|
||||
needRedraw = false;
|
||||
lastRedraw = millis();
|
||||
|
||||
if (displayTurnedOff)
|
||||
{
|
||||
if (displayTurnedOff) {
|
||||
// Turn the display back on
|
||||
sleepOrClock(false);
|
||||
}
|
||||
@ -242,79 +326,94 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
knownEffectIntensity = effectIntensity;
|
||||
|
||||
// Do the actual drawing
|
||||
u8x8.clear();
|
||||
u8x8.setFont(u8x8_font_chroma48medium8_r);
|
||||
|
||||
// First row with Wifi name
|
||||
String ssidString = knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0);
|
||||
u8x8.DRAW_STRING(1, 0*LINE_HEIGHT, ssidString.c_str());
|
||||
// Print `~` char to indicate that SSID is longer, than owr dicplay
|
||||
if (knownSsid.length() > u8x8.getCols()) {
|
||||
u8x8.DRAW_STRING(u8x8.getCols() - 1, 0*LINE_HEIGHT, "~");
|
||||
drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // wifi icon
|
||||
String ssidString = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0);
|
||||
drawString(1, 0, ssidString.c_str());
|
||||
// Print `~` char to indicate that SSID is longer, than our display
|
||||
if (knownSsid.length() > getCols()) {
|
||||
drawString(getCols() - 1, 0, "~");
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (apActive && bri == 0) {
|
||||
u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, apPass);
|
||||
}
|
||||
else {
|
||||
String ipString = knownIp.toString();
|
||||
u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, ipString.c_str());
|
||||
drawString(1, lineHeight, apPass);
|
||||
} else {
|
||||
drawString(1, lineHeight, (knownIp.toString()).c_str());
|
||||
}
|
||||
|
||||
// Third row with mode name
|
||||
showCurrentEffectOrPalette(modes_qstrings[knownMode], 2);
|
||||
// Third row with mode name or current time
|
||||
if (clockMode) showTime(false);
|
||||
else showCurrentEffectOrPalette(knownMode, JSON_mode_names, 2);
|
||||
|
||||
switch(lineThreeType) {
|
||||
case FLD_LINE_3_BRIGHTNESS:
|
||||
sprintf(lineBuffer, "Brightness %d", bri);
|
||||
u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer);
|
||||
// Fourth row
|
||||
drawLineFour();
|
||||
|
||||
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;
|
||||
case FLD_LINE_3_EFFECT_SPEED:
|
||||
sprintf(lineBuffer, "FX Speed %d", effectSpeed);
|
||||
u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer);
|
||||
case FLD_LINE_4_EFFECT_SPEED:
|
||||
sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed);
|
||||
drawString(2, 3*lineHeight, lineBuffer);
|
||||
break;
|
||||
case FLD_LINE_3_EFFECT_INTENSITY:
|
||||
sprintf(lineBuffer, "FX Intense %d", effectIntensity);
|
||||
u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer);
|
||||
case FLD_LINE_4_EFFECT_INTENSITY:
|
||||
sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity);
|
||||
drawString(2, 3*lineHeight, lineBuffer);
|
||||
break;
|
||||
case FLD_LINE_3_PALETTE:
|
||||
showCurrentEffectOrPalette(palettes_qstrings[knownPalette], 3);
|
||||
case FLD_LINE_4_MODE:
|
||||
showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3);
|
||||
break;
|
||||
case FLD_LINE_4_PALETTE:
|
||||
default:
|
||||
showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 3);
|
||||
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)
|
||||
* 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) {
|
||||
uint8_t printedChars = 1;
|
||||
void showCurrentEffectOrPalette(int knownMode, const char *qstring, uint8_t row) {
|
||||
char lineBuffer[LINE_BUFFER_SIZE];
|
||||
uint8_t qComma = 0;
|
||||
bool insideQuotes = false;
|
||||
uint8_t printedChars = 0;
|
||||
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);
|
||||
if (singleJsonSymbol == '"' || singleJsonSymbol == '\0' ) {
|
||||
break;
|
||||
if (singleJsonSymbol == '\0') 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);
|
||||
printedChars++;
|
||||
if ( (printedChars > u8x8.getCols() - 2)) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
if ((qComma > knownMode) || (printedChars > getCols()-2) || printedChars > sizeof(lineBuffer)-2) break;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
bool wakeDisplay() {
|
||||
knownHour = 99;
|
||||
if (displayTurnedOff) {
|
||||
// Turn the display back on
|
||||
sleepOrClock(false);
|
||||
@ -345,36 +445,31 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
}
|
||||
|
||||
// Print the overlay
|
||||
u8x8.clear();
|
||||
u8x8.setFont(u8x8_font_chroma48medium8_r);
|
||||
if (line1) {
|
||||
u8x8.DRAW_STRING(0, 1*LINE_HEIGHT, line1);
|
||||
}
|
||||
if (line2) {
|
||||
u8x8.DRAW_STRING(0, 2*LINE_HEIGHT, line2);
|
||||
}
|
||||
clear();
|
||||
if (line1) drawString(0, 1*lineHeight, line1);
|
||||
if (line2) drawString(0, 2*lineHeight, line2);
|
||||
overlayUntil = millis() + showHowLong;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify what data should be defined on line 3
|
||||
* Specify what data should be defined on line 4
|
||||
* (the last line).
|
||||
*/
|
||||
void setLineThreeType(byte newLineThreeType) {
|
||||
if (newLineThreeType == FLD_LINE_3_BRIGHTNESS ||
|
||||
newLineThreeType == FLD_LINE_3_EFFECT_SPEED ||
|
||||
newLineThreeType == FLD_LINE_3_EFFECT_INTENSITY ||
|
||||
newLineThreeType == FLD_LINE_3_PALETTE) {
|
||||
lineThreeType = newLineThreeType;
|
||||
}
|
||||
else {
|
||||
// Unknown value.
|
||||
lineThreeType = FLD_LINE_3_BRIGHTNESS;
|
||||
void setLineFourType(Line4Type newLineFourType) {
|
||||
if (newLineFourType == FLD_LINE_4_BRIGHTNESS ||
|
||||
newLineFourType == FLD_LINE_4_EFFECT_SPEED ||
|
||||
newLineFourType == FLD_LINE_4_EFFECT_INTENSITY ||
|
||||
newLineFourType == FLD_LINE_4_MODE ||
|
||||
newLineFourType == FLD_LINE_4_PALETTE) {
|
||||
lineFourType = newLineFourType;
|
||||
} else {
|
||||
// Unknown value
|
||||
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
|
||||
* specify which line to mark with an arrow.
|
||||
* 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.
|
||||
*/
|
||||
void sleepOrClock(bool enabled) {
|
||||
if (enabled) {
|
||||
if (CLOCK_MODE_ENABLED) {
|
||||
showTime();
|
||||
}
|
||||
else {
|
||||
u8x8.setPowerSave(1);
|
||||
}
|
||||
if (clockMode) showTime();
|
||||
else setPowerSave(1);
|
||||
displayTurnedOff = true;
|
||||
}
|
||||
else {
|
||||
if (!CLOCK_MODE_ENABLED) {
|
||||
u8x8.setPowerSave(0);
|
||||
}
|
||||
setPowerSave(0);
|
||||
displayTurnedOff = false;
|
||||
}
|
||||
}
|
||||
@ -433,23 +503,28 @@ class FourLineDisplayUsermod : public Usermod {
|
||||
* on the middle rows. Based 24 or 12 hour depending on
|
||||
* the useAMPM configuration.
|
||||
*/
|
||||
void showTime() {
|
||||
void showTime(bool fullScreen = true) {
|
||||
char lineBuffer[LINE_BUFFER_SIZE];
|
||||
|
||||
updateLocalTime();
|
||||
byte minuteCurrent = minute(localTime);
|
||||
byte hourCurrent = hour(localTime);
|
||||
byte secondCurrent = second(localTime);
|
||||
if (knownMinute == minuteCurrent && knownHour == hourCurrent) {
|
||||
// Time hasn't changed.
|
||||
return;
|
||||
if (!fullScreen) return;
|
||||
} else {
|
||||
if (fullScreen) clear();
|
||||
}
|
||||
knownMinute = minuteCurrent;
|
||||
knownHour = hourCurrent;
|
||||
|
||||
u8x8.clear();
|
||||
u8x8.setFont(u8x8_font_chroma48medium8_r);
|
||||
|
||||
int currentMonth = month(localTime);
|
||||
sprintf(lineBuffer, "%s %d", monthShortStr(currentMonth), day(localTime));
|
||||
u8x8.DRAW_BIG_STRING(DATE_INDENT, TIME_LINE*LINE_HEIGHT, lineBuffer);
|
||||
byte currentMonth = month(localTime);
|
||||
sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(currentMonth), day(localTime));
|
||||
if (fullScreen)
|
||||
draw2x2String(DATE_INDENT, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays
|
||||
else
|
||||
drawString(2, lineHeight*2, lineBuffer);
|
||||
|
||||
byte showHour = hourCurrent;
|
||||
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
|
||||
// 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).
|
||||
* 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).
|
||||
* 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.
|
||||
@ -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!
|
||||
*/
|
||||
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 :)
|
||||
*/
|
||||
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() {
|
||||
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";
|
||||
|
@ -11,12 +11,24 @@
|
||||
#define DEFAULT_OTA_PASS "wledota"
|
||||
|
||||
//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
|
||||
#define WLED_MAX_BUSSES 3
|
||||
#else
|
||||
#define WLED_MAX_BUSSES 10
|
||||
#ifndef WLED_MAX_BUSSES
|
||||
#ifdef ESP8266
|
||||
#define WLED_MAX_BUSSES 3
|
||||
#else
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S2
|
||||
#define WLED_MAX_BUSSES 5
|
||||
#else
|
||||
#define WLED_MAX_BUSSES 10
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//Usermod IDs
|
||||
@ -33,6 +45,7 @@
|
||||
#define USERMOD_ID_DHT 10 //Usermod "usermod_dht.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_MULTI_RELAY 101 //Usermod "usermod_multi_relay.h"
|
||||
|
||||
//Access point behavior
|
||||
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
|
||||
@ -116,10 +129,10 @@
|
||||
#define TYPE_LPD8806 52
|
||||
#define TYPE_P9813 53
|
||||
|
||||
#define IS_DIGITAL(t) (t & 0x10) //digital are 16-31 and 48-63
|
||||
#define IS_PWM(t) (t > 40 && t < 46)
|
||||
#define NUM_PWM_PINS(t) (t - 40) //for analog PWM 41-45 only
|
||||
#define IS_2PIN(t) (t > 47)
|
||||
#define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63
|
||||
#define IS_PWM(t) ((t) > 40 && (t) < 46)
|
||||
#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only
|
||||
#define IS_2PIN(t) ((t) > 47)
|
||||
|
||||
//Color orders
|
||||
#define COL_ORDER_GRB 0 //GRB(w),defaut
|
||||
@ -241,7 +254,11 @@
|
||||
|
||||
//this is merely a default now and can be changed at runtime
|
||||
#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
|
||||
|
||||
#ifdef WLED_ENABLE_DMX
|
||||
|
@ -10,18 +10,17 @@
|
||||
margin: 0;
|
||||
}
|
||||
html {
|
||||
--h: 11.55vh;
|
||||
--h: 10.2vh;
|
||||
}
|
||||
button {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-family: Verdana, Helvetica, sans-serif;
|
||||
border: 0.3ch solid #333;
|
||||
display: inline-block;
|
||||
font-size: 8vmin;
|
||||
border: 1px solid #333;
|
||||
font-size: 6vmin;
|
||||
height: var(--h);
|
||||
width: 95%;
|
||||
margin-top: 2.4vh;
|
||||
margin-top: 2vh;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
@ -41,6 +40,7 @@
|
||||
<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/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>
|
||||
</body>
|
||||
</html>
|
140
wled00/data/settings_um.htm
Normal file
140
wled00/data/settings_um.htm
Normal 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">✔ Configuration saved!</span>
|
||||
<span id="lserr" style="color:red; display:none">⚠ 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>
|
@ -150,6 +150,7 @@ void _overlayCronixie();
|
||||
void _drawOverlayCronixie();
|
||||
|
||||
//playlist.cpp
|
||||
void shufflePlaylist();
|
||||
void unloadPlaylist();
|
||||
void loadPlaylist(JsonObject playlistObject);
|
||||
void handlePlaylist();
|
||||
@ -186,6 +187,8 @@ class Usermod {
|
||||
virtual void readFromJsonState(JsonObject& obj) {}
|
||||
virtual void addToConfig(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;}
|
||||
};
|
||||
|
||||
@ -206,7 +209,8 @@ class UsermodManager {
|
||||
|
||||
void addToConfig(JsonObject& obj);
|
||||
void readFromConfig(JsonObject& obj);
|
||||
|
||||
void onMqttConnect(bool sessionPresent);
|
||||
bool onMqttMessage(char* topic, char* payload);
|
||||
bool add(Usermod* um);
|
||||
Usermod* lookup(uint16_t mod_id);
|
||||
byte getModCount();
|
||||
|
@ -12,7 +12,7 @@ const char PAGE_settingsCss[] PROGMEM = R"=====(<style>body{font-family:Verdana,
|
||||
// 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
|
||||
</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>
|
||||
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"
|
||||
@ -22,7 +22,8 @@ LED Preferences</button></form><form action="/settings/ui"><button
|
||||
type="submit">User Interface</button></form>%DMXMENU%<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/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>)=====";
|
||||
|
||||
|
||||
@ -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
|
||||
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">✔ Configuration saved!
|
||||
</span> <span id="lserr" style="color:red;display:none">
|
||||
⚠ 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>)=====";
|
||||
|
||||
|
1354
wled00/html_ui.h
1354
wled00/html_ui.h
File diff suppressed because it is too large
Load Diff
@ -25,28 +25,28 @@ void onMqttConnect(bool sessionPresent)
|
||||
//(re)subscribe to required topics
|
||||
char subuf[38];
|
||||
|
||||
if (mqttDeviceTopic[0] != 0)
|
||||
{
|
||||
if (mqttDeviceTopic[0] != 0) {
|
||||
strcpy(subuf, mqttDeviceTopic);
|
||||
mqtt->subscribe(subuf, 0);
|
||||
strcat(subuf, "/col");
|
||||
strcat_P(subuf, PSTR("/col"));
|
||||
mqtt->subscribe(subuf, 0);
|
||||
strcpy(subuf, mqttDeviceTopic);
|
||||
strcat(subuf, "/api");
|
||||
strcat_P(subuf, PSTR("/api"));
|
||||
mqtt->subscribe(subuf, 0);
|
||||
}
|
||||
|
||||
if (mqttGroupTopic[0] != 0)
|
||||
{
|
||||
if (mqttGroupTopic[0] != 0) {
|
||||
strcpy(subuf, mqttGroupTopic);
|
||||
mqtt->subscribe(subuf, 0);
|
||||
strcat(subuf, "/col");
|
||||
strcat_P(subuf, PSTR("/col"));
|
||||
mqtt->subscribe(subuf, 0);
|
||||
strcpy(subuf, mqttGroupTopic);
|
||||
strcat(subuf, "/api");
|
||||
strcat_P(subuf, PSTR("/api"));
|
||||
mqtt->subscribe(subuf, 0);
|
||||
}
|
||||
|
||||
usermods.onMqttConnect(sessionPresent);
|
||||
|
||||
doPublishMqtt = true;
|
||||
DEBUG_PRINTLN(F("MQTT ready"));
|
||||
}
|
||||
@ -66,25 +66,24 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties
|
||||
|
||||
size_t topicPrefixLen = strlen(mqttDeviceTopic);
|
||||
if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) {
|
||||
topic += topicPrefixLen;
|
||||
topic += topicPrefixLen;
|
||||
} else {
|
||||
topicPrefixLen = strlen(mqttGroupTopic);
|
||||
if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) {
|
||||
topic += topicPrefixLen;
|
||||
} else {
|
||||
// Topic not used here. Probably a usermod subscribed to this topic.
|
||||
return;
|
||||
}
|
||||
topicPrefixLen = strlen(mqttGroupTopic);
|
||||
if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) {
|
||||
topic += topicPrefixLen;
|
||||
} else {
|
||||
// Non-Wled Topic used here. Probably a usermod subscribed to this topic.
|
||||
usermods.onMqttMessage(topic, payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//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);
|
||||
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
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
deserializeJson(doc, payload);
|
||||
@ -94,8 +93,11 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties
|
||||
apireq += (char*)payload;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -110,24 +112,24 @@ void publishMqtt()
|
||||
char s[10];
|
||||
char subuf[38];
|
||||
|
||||
sprintf(s, "%u", bri);
|
||||
sprintf_P(s, PSTR("%u"), bri);
|
||||
strcpy(subuf, mqttDeviceTopic);
|
||||
strcat(subuf, "/g");
|
||||
strcat_P(subuf, PSTR("/g"));
|
||||
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);
|
||||
strcat(subuf, "/c");
|
||||
strcat_P(subuf, PSTR("/c"));
|
||||
mqtt->publish(subuf, 0, true, s);
|
||||
|
||||
strcpy(subuf, mqttDeviceTopic);
|
||||
strcat(subuf, "/status");
|
||||
strcat_P(subuf, PSTR("/status"));
|
||||
mqtt->publish(subuf, 0, true, "online");
|
||||
|
||||
char apires[1024];
|
||||
XML_response(nullptr, apires);
|
||||
strcpy(subuf, mqttDeviceTopic);
|
||||
strcat(subuf, "/v");
|
||||
strcat_P(subuf, PSTR("/v"));
|
||||
mqtt->publish(subuf, 0, true, apires);
|
||||
}
|
||||
|
||||
@ -157,7 +159,7 @@ bool initMqtt()
|
||||
if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass);
|
||||
|
||||
strcpy(mqttStatusTopic, mqttDeviceTopic);
|
||||
strcat(mqttStatusTopic, "/status");
|
||||
strcat_P(mqttStatusTopic, PSTR("/status"));
|
||||
mqtt->setWill(mqttStatusTopic, 0, true, "offline");
|
||||
mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME);
|
||||
mqtt->connect();
|
||||
|
@ -31,8 +31,8 @@ bool isAsterisksOnly(const char* str, byte maxLen)
|
||||
void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
{
|
||||
|
||||
//0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX
|
||||
if (subPage <1 || subPage >7) return;
|
||||
//0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX 8: usermods
|
||||
if (subPage <1 || subPage >8) return;
|
||||
|
||||
//WIFI SETTINGS
|
||||
if (subPage == 1)
|
||||
@ -407,6 +407,57 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
||||
}
|
||||
#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 == 4) alexaInit();
|
||||
}
|
||||
|
@ -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::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::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.
|
||||
|
@ -20,7 +20,10 @@
|
||||
#include "../usermods/buzzer/usermod_v2_buzzer.h"
|
||||
#endif
|
||||
#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
|
||||
|
||||
#ifdef USERMOD_MODE_SORT
|
||||
@ -31,7 +34,7 @@
|
||||
#ifdef USERMOD_BME280
|
||||
#include "../usermods/BME280_v2/usermod_bme280.h"
|
||||
#endif
|
||||
#ifdef USERMOD_FOUR_LINE_DISLAY
|
||||
#ifdef USERMOD_FOUR_LINE_DISPLAY
|
||||
#include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h"
|
||||
#endif
|
||||
#ifdef USERMOD_ROTARY_ENCODER_UI
|
||||
@ -50,6 +53,14 @@
|
||||
#include "../usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h"
|
||||
#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()
|
||||
{
|
||||
/*
|
||||
@ -72,28 +83,39 @@ void registerUsermods()
|
||||
#ifdef USERMOD_BME280
|
||||
usermods.add(new UsermodBME280());
|
||||
#endif
|
||||
#ifdef USERMOD_SENSORSTOMQTT
|
||||
#ifdef 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());
|
||||
#endif
|
||||
#ifdef USERMOD_FOUR_LINE_DISLAY
|
||||
#endif
|
||||
#ifdef USERMOD_FOUR_LINE_DISPLAY
|
||||
usermods.add(new FourLineDisplayUsermod());
|
||||
#endif
|
||||
#ifdef USERMOD_ROTARY_ENCODER_UI
|
||||
usermods.add(new RotaryEncoderUIUsermod());
|
||||
#endif
|
||||
#ifdef USERMOD_AUTO_SAVE
|
||||
usermods.add(new AutoSaveUsermod());
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USERMOD_ROTARY_ENCODER_UI
|
||||
usermods.add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY
|
||||
#endif
|
||||
#ifdef USERMOD_AUTO_SAVE
|
||||
usermods.add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY
|
||||
#endif
|
||||
|
||||
#ifdef USERMOD_DHT
|
||||
usermods.add(new UsermodDHT());
|
||||
#endif
|
||||
#ifdef USERMOD_DHT
|
||||
usermods.add(new UsermodDHT());
|
||||
#endif
|
||||
|
||||
#ifdef USERMOD_VL53L0X_GESTURES
|
||||
#ifdef USERMOD_VL53L0X_GESTURES
|
||||
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
|
||||
}
|
||||
|
@ -314,6 +314,7 @@ String settingsProcessor(const String& var)
|
||||
{
|
||||
if (var == "CSS") {
|
||||
char buf[2048];
|
||||
buf[0] = 0;
|
||||
getSettingsJS(optionType, buf);
|
||||
return String(buf);
|
||||
}
|
||||
@ -365,6 +366,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
|
||||
#ifdef WLED_ENABLE_DMX // include only if DMX is enabled
|
||||
else if (url.indexOf("dmx") > 0) subPage = 7;
|
||||
#endif
|
||||
else if (url.indexOf("um") > 0) subPage = 8;
|
||||
} else subPage = 255; //welcome page
|
||||
|
||||
if (subPage == 1 && wifiLock && otaLock)
|
||||
@ -386,6 +388,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post)
|
||||
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 7: strcpy_P(s, PSTR("DMX")); break;
|
||||
case 8: strcpy_P(s, PSTR("Usermods")); break;
|
||||
}
|
||||
|
||||
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 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 8: request->send_P(200, "text/html", PAGE_settings_um , settingsProcessor); break;
|
||||
case 255: request->send_P(200, "text/html", PAGE_welcome); break;
|
||||
default: request->send_P(200, "text/html", PAGE_settings , settingsProcessor);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user