Merge branch 'Aircoookie:master' into master

This commit is contained in:
Artacus 2021-05-16 22:00:15 -07:00
commit 94af6d0561
50 changed files with 3110 additions and 1742 deletions

View File

@ -2,6 +2,30 @@
### Builds after release 0.12.0
#### Build 2105120
- Fixed possibility of non-0-terminated MQTT payloads
- Fixed two warnings regarding integer comparison
#### Build 2105112
- Usermod settings page no usermods message
- Lowered min speed for Drip effect
#### Build 2105111
- Fixed various Codacy code style and logic issues
#### Build 2105110
- Added Usermod settings page and configurable usermods (PR #1951)
- Added experimental `/json/cfg` endpoint for changing settings from JSON (see #1944, not part of official API)
#### Build 2105070
- Fixed not turning on after pressing "Off" on IR remote twice (#1950)
- Fixed OTA update file selection from Android app (TODO: file type verification in JS, since android can't deal with accept='.bin' attribute)
#### Build 2104220
- Version bump to 0.12.1-b1 "Hikari"

View File

@ -114,9 +114,6 @@ build_unflags =
# enables all features for travis CI
build_flags_all_features =
-D WLED_USE_ANALOG_LED
-D WLED_USE_H801
-D WLED_ENABLE_5CH_LEDS
-D WLED_ENABLE_ADALIGHT
-D WLED_ENABLE_DMX
-D WLED_ENABLE_MQTT
@ -283,7 +280,7 @@ platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA -D WLED_USE_ANALOG_LEDS
build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA
[env:esp8285_4CH_H801]
board = esp8285
@ -291,7 +288,7 @@ platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA -D WLED_USE_ANALOG_LEDS -D WLED_USE_H801
build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA
[env:esp8285_5CH_H801]
board = esp8285
@ -299,7 +296,7 @@ platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA -D WLED_USE_ANALOG_LEDS -D WLED_USE_H801 -D WLED_ENABLE_5CH_LEDS
build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA
[env:d1_mini_5CH_Shojo_PCB]
board = d1_mini
@ -307,7 +304,7 @@ platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags_esp8266} -D WLED_USE_ANALOG_LEDS -D WLED_USE_SHOJO_PCB -D WLED_ENABLE_5CH_LEDS
build_flags = ${common.build_flags_esp8266} -D WLED_USE_SHOJO_PCB
# ------------------------------------------------------------------------------
# DEVELOPMENT BOARDS

View File

@ -39,12 +39,8 @@ build_flags = ${common.build_flags_esp8266}
; PIN defines for 2 wire LEDs
-D CLKPIN=0
-D DATAPIN=2
; to drive analog LED strips (aka 5050), uncomment the following
; PWM pins 5,12,13,15 are used with Magic Home LED Controller (default)
-D WLED_USE_ANALOG_LEDS
; for the H801 controller (PINs 15,13,12,14 (W2 = 04)) uncomment this
; -D WLED_USE_H801
; for the BW-LT11 controller (PINs 12,4,14,5 ) uncomment this
; -D WLED_USE_BWLT11
; and to enable channel 5 for RGBW-CT led strips this
; -D WLED_USE_5CH_LEDS
; to drive analog LED strips (aka 5050) hardware configuration is no longer necessary
; configure the settings in the UI as follows (hard):
; for the Magic Home LED Controller use PWM pins 5,12,13,15
; for the H801 controller use PINs 15,13,12,14 (W2 = 04)
; for the BW-LT11 controller use PINs 12,4,14,5

View File

@ -333,6 +333,22 @@ 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%")
.replace(
/function GetV().*\<\/script\>/gms,
"function GetV() {var d=document;\n"
),
}
],
"wled00/html_settings.h"
);

232
tools/fps_test.htm Normal file
View File

@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>WLED frame rate test tool</title>
<style>
body {
background-color: #222;
color: #fff;
font-family: Helvetica, Verdana, sans-serif;
}
input {
background-color: #333;
color: #fff;
}
#ip {
width: 100px;
}
#secs {
width: 36px;
}
#csva {
position: absolute;
top: -100px; /*gtfo*/
}
button {
background-color: #333;
color: #fff;
}
table, th, td {
border: 1px solid #aaa;
border-collapse: collapse;
text-align: center;
}
.red {
color: #d20;
}
</style>
<script>
var gotfx = false, running = false;
var pos = 0, prev = 0, min = 999, max = 0, fpslist = [], names = [], names_checked = [];
var to;
function S() {
document.getElementById('ip').value = localStorage.getItem('locIpFps');
if (document.getElementById('ip').value) req(false);
}
function loadC() {
hide(false);
var list = localStorage.getItem('fpsFxSelection');
if (!list) return;
list = JSON.parse(list);
var chks = document.querySelectorAll('.fxcheck');
for (let i = 0; i < chks.length; i++) {
if (i < list.length) chks[i].checked = list[i];
}
}
function saveC() {
var list = [];
var chks = document.querySelectorAll('.fxcheck');
for (let i = 0; i < chks.length; i++) {
list.push(chks[i].checked);
}
localStorage.setItem('fpsFxSelection', JSON.stringify(list));
}
function setC(c) {
hide(false);
var chks = document.querySelectorAll('.fxcheck');
for (let i = 0; i < chks.length; i++) {
chks[i].checked = (c == 255);
}
if (c == 1 && chks.length > 100) {
chks[1].checked = true; //Blink
chks[15].checked = true; //Running
chks[16].checked = true; //Saw
chks[37].checked = true; //Running 2
chks[44].checked = true; //Tetrix
chks[63].checked = true; //Pride 2015
chks[74].checked = true; //Colortwinkles
chks[101].checked = true;//Pacifica
}
}
function hide(h) {
var trs = document.querySelectorAll('.trs');
var chks = document.querySelectorAll('.fxcheck');
for (let i = 0; i < trs.length; i++) {
trs[i].style.display = (h && !chks[i].checked) ? "none":"table-row";
}
}
function run(init) {
if (init) {
running = !running;
document.getElementById('runbtn').innerText = running ? 'Stop':'Run';
if (running) {pos = 0; prev = -1; min = 999; max = 0; fpslist = []; names_checked = []; hide(true);}
clearTimeout(to);
if (!running) {req({seg:{fx:0},v:true,stop:true}); return;}
}
if (!gotfx) {req(false); return;}
var chks = document.querySelectorAll('.fxcheck');
var fpsb = document.querySelectorAll('.fps');
if (prev >= 0) {pos++};
if (pos >= chks.length) {run(true); return;} //end
while (!chks[pos].checked) {
fpsb[pos].innerText = "-";
pos++;
if (pos >= chks.length) {run(true); return;} //end
}
names_checked.push(names[pos]);
var extra = {};
try {
extra = JSON.parse(document.getElementById('ej').value);
} catch (e) {
}
var cmd = {seg:{fx:pos},v:true};
Object.assign(cmd, extra);
req(cmd);
}
function req(command) {
var ip = document.getElementById('ip').value;
if (!ip) {alert("Please enter WLED IP"); return;}
if (ip != localStorage.getItem('locIpFps')) localStorage.setItem('locIpFps', document.getElementById('ip').value);
var url = command ? `http://${ip}/json/si` : `http://${ip}/json/effects`;
var type = command ? 'post':'get';
var req = undefined;
if (command)
{
req = JSON.stringify(command);
}
fetch
(url, {
method: type,
headers: {
"Content-type": "application/json; charset=UTF-8"
},
body: req
})
.then(res => {
if (!res.ok) {
alert('Data malfunction');
}
return res.json();
})
.then(json => {
if (!json) {
alert('Empty response'); return;
}
if (!command) {
names = json;
var tblc = '';
for (let i = 0; i < json.length; i++) {
tblc += `<tr class="trs"><td><input type="checkbox" class="fxcheck" /></td><td>${i}</td><td>${json[i]}</td><td class="fps"></td></tr>`
}
var tbl = `<table>
<tr>
<th>Test?</th><th>ID</th><th>Effect Name</th><th>FPS</th>
</tr>
${tblc}
</table>`;
document.getElementById('tablecon').innerHTML = tbl;
setC(1);
loadC();
gotfx = true;
document.getElementById('runbtn').innerText = "Run";
} else {
if (!json.info) return;
document.getElementById('leds').innerText = json.info.leds.count;
document.getElementById('seg').innerText = json.state.seg[0].len;
document.getElementById('bri').innerText = json.state.bri;
if (prev >= 0) {
var lastfps = parseInt(json.info.leds.fps); //previous FX
if (lastfps < min) min = lastfps;
if (lastfps > max) max = lastfps;
fpslist.push(lastfps);
var sum = 0;
for (let i = 0; i < fpslist.length; i++) {
sum += fpslist[i];
}
sum /= fpslist.length;
document.getElementById('fps_min').innerText = min;
document.getElementById('fps_max').innerText = max;
document.getElementById('fps_avg').innerText = Math.round(sum*10)/10;
var fpsb = document.querySelectorAll('.fps');
fpsb[prev].innerHTML = lastfps;
}
prev = pos;
var delay = parseInt(document.getElementById('secs').value)*1000;
delay = Math.min(Math.max(delay, 2000), 15000)
if (!command.stop) to = setTimeout(run,delay);
}
})
.catch(function (error) {
alert('Comms malfunction');
console.log(error);
});
}
function csv(n) {
var txt = "";
for (let i = 0; i < fpslist.length; i++) {
if (!n) txt += names_checked[i] + ',';
txt += fpslist[i]; txt += "\n";
}
document.getElementById('csva').value = txt;
var copyText = document.getElementById('csva');
copyText.select();
copyText.setSelectionRange(0, 999999);
document.execCommand("copy");
}
</script>
</head>
<body onload="S()">
<h2>Starship monitoring dashboard</h2>
(or rather just a WLED frame rate tester lol)<br><br>
IP: <input id="ip" /><br>
Time per effect: <input type=number id=secs value=5 max=15 min=2 />s<br>
Effects to test:
<button type="button" onclick="setC(255)">All</button>
<button type="button" onclick="setC(1)">Selection 1</button>
<button type="button" onclick="setC(0)">None</button>
<button type="button" onclick="loadC()">Get LS</button>
<button type="button" class="red" onclick="saveC()">Save to LS</button><br>
Extra JSON: <input id="ej" /><br>
<button type="button" onclick="run(true)" id="runbtn">Fetch FX list</button><br>
LEDs: <span id="leds">-</span>, Seg: <span id="seg">-</span>, Bri: <span id="bri">-</span><br>
FPS min: <span id="fps_min">-</span>, max: <span id="fps_max">-</span>, avg: <span id="fps_avg">-</span><br><br>
<div id="tablecon">
</div><br>
<button type="button" onclick="csv(false)">Copy csv to clipboard</button>
<button type="button" onclick="csv(true)">Copy csv (FPS only)</button>
<textarea id=csva></textarea>
</body>
</html>

View File

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

View File

@ -1,21 +0,0 @@
/*
* Animated_Staircase compiletime confguration.
*
* Please see README.md on how to change this file.
*/
// Please change the pin numbering below to match your board.
#define TOP_PIR_PIN D5
#define BOTTOM_PIR_PIN D6
// Or uncumment and a pir and use an ultrasound HC-SR04 sensor,
// see README.md for details
#ifndef TOP_PIR_PIN
#define TOP_TRIGGER_PIN D2
#define TOP_ECHO_PIN D3
#endif
#ifndef BOTTOM_PIR_PIN
#define BOTTOM_TRIGGER_PIN D4
#define BOTTOM_ECHO_PIN D5
#endif

View File

@ -20,44 +20,10 @@ Edit `usermods_list.cpp`:
2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file
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.

View File

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

View File

@ -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":{"&#x23F2; 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("&#x23F2; PIR sensor state"); //name
String uiDomString = "<button class=\"btn infobtn\" onclick=\"requestJson({PIRenabled:";
/*
JsonArray infoArr = user.createNestedArray(F("<i class=\"icons\">&#xe08f;</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":{"&#x23F2; switch off timer":uiDomString} to the info object
uiDomString = "&#x23F2; switch off timer<span style=\"display:block;padding-left:25px;\">\
after <input type=\"number\" min=\"1\" max=\"720\" value=\"";
uiDomString += (m_switchOffDelay / 60000);
uiDomString += "\" onchange=\"requestJson({PIRoffSec:parseInt(this.value)*60});\">min</span>";
infoArr = user.createNestedArray(uiDomString); //name
// off timer
if (m_offTimerStart > 0)
*/
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\">&#xe325;</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";

View File

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

View File

@ -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
@ -23,16 +24,17 @@
#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,58 +44,95 @@ 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() {
@ -104,23 +143,21 @@ class UsermodTemperature : public Usermod {
// 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
// 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";

View File

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

View File

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

View File

@ -29,9 +29,9 @@ This file should be placed in the same directory as `platformio.ini`.
### Define Your Options
* `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.

View File

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

View File

@ -1,4 +1,4 @@
# Rotary Encoder UI Usermod
# I2C 4 Line Display Usermod
First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod.
@ -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.

View File

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

Binary file not shown.

View File

@ -34,7 +34,7 @@
*/
uint16_t WS2812FX::mode_static(void) {
fill(SEGCOLOR(0));
return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 380; //update faster if in transition
return (SEGMENT.getOption(SEG_OPTION_TRANSITIONAL)) ? FRAMETIME : 350; //update faster if in transition
}
@ -1467,8 +1467,8 @@ uint16_t WS2812FX::mode_tricolor_fade(void)
}
byte stp = prog; // % 256
uint32_t color = 0;
for(uint16_t i = 0; i < SEGLEN; i++) {
uint32_t color;
if (stage == 2) {
color = color_blend(color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp);
} else if (stage == 1) {
@ -3085,7 +3085,7 @@ uint16_t WS2812FX::mode_drip(void)
numDrops = 1 + (SEGMENT.intensity >> 6);
float gravity = -0.001 - (SEGMENT.speed/50000.0);
float gravity = -0.0005 - (SEGMENT.speed/50000.0);
gravity *= SEGLEN;
int sourcedrop = 12;

View File

@ -606,6 +606,7 @@ class WS2812FX {
bool
isRgbw = false,
isOffRefreshRequred = false, //periodic refresh is required for the strip to remain off.
gammaCorrectBri = false,
gammaCorrectCol = true,
applyToAllSelected = true,

View File

@ -1,124 +0,0 @@
/*
Editor: https://www.visualmicro.com/
This file is for intellisense purpose only.
Visual micro (and the arduino ide) ignore this code during compilation. This code is automatically maintained by visualmicro, manual changes to this file will be overwritten
The contents of the _vm sub folder can be deleted prior to publishing a project
All non-arduino files created by visual micro and all visual studio project or solution files can be freely deleted and are not required to compile a sketch (do not delete your own code!).
Note: debugger breakpoints are stored in '.sln' or '.asln' files, knowledge of last uploaded breakpoints is stored in the upload.vmps.xml file. Both files are required to continue a previous debug session without needing to compile and upload again
Hardware: ESP32 Dev Module, Platform=esp32, Package=esp32
*/
#if defined(_VMICRO_INTELLISENSE)
#ifndef _VSARDUINO_H_
#define _VSARDUINO_H_
#define __ESP32_esp32__
#define __ESP32_ESP32__
#define ESP_PLATFORM
#define HAVE_CONFIG_H
#define GCC_NOT_5_2_0 0
#define WITH_POSIX
#define F_CPU 240000000L
#define ARDUINO 108011
#define ARDUINO_ESP32_DEV
#define ARDUINO_ARCH_ESP32
#define ESP32
#define CORE_DEBUG_LEVEL 0
#define __cplusplus 201103L
#define _Pragma(x)
#undef __cplusplus
#define __cplusplus 201103L
#define __STDC__
#define __ARM__
#define __arm__
#define __inline__
#define __asm__(...)
#define __extension__
#define __ATTR_PURE__
#define __ATTR_CONST__
#define __volatile__
#define __ASM
#define __INLINE
#define __attribute__(noinline)
//#define _STD_BEGIN
//#define EMIT
#define WARNING
#define _Lockit
#define __CLR_OR_THIS_CALL
#define C4005
#define _NEW
typedef bool _Bool;
typedef int _read;
typedef int _seek;
typedef int _write;
typedef int _close;
typedef int __cleanup;
//#define inline
#define __builtin_clz
#define __builtin_clzl
#define __builtin_clzll
#define __builtin_labs
#define __builtin_va_list
typedef int __gnuc_va_list;
#define __ATOMIC_ACQ_REL
#define __CHAR_BIT__
#define _EXFUN()
typedef unsigned char byte;
extern "C" void __cxa_pure_virtual() {;}
typedef long __INTPTR_TYPE__ ;
typedef long __UINTPTR_TYPE__ ;
typedef long __SIZE_TYPE__ ;
typedef long __PTRDIFF_TYPE__;
typedef long pthread_t;
typedef long pthread_key_t;
typedef long pthread_once_t;
typedef long pthread_mutex_t;
typedef long pthread_mutex_t;
typedef long pthread_cond_t;
#include "arduino.h"
#include <pins_arduino.h>
#define interrupts() sei()
#define noInterrupts() cli()
#define ESP_LOGI(tag, ...)
#include "wled00.ino"
#include "wled01_eeprom.ino"
#include "wled02_xml.ino"
#include "wled03_set.ino"
#include "wled04_file.ino"
#include "wled05_init.ino"
#include "wled06_usermod.ino"
#include "wled07_notify.ino"
#include "wled08_led.ino"
#include "wled09_button.ino"
#include "wled10_ntp.ino"
#include "wled11_ol.ino"
#include "wled12_alexa.ino"
#include "wled13_cronixie.ino"
#include "wled14_colors.ino"
#include "wled15_hue.ino"
#include "wled16_blynk.ino"
#include "wled17_mqtt.ino"
#include "wled18_server.ino"
#include "wled19_json.ino"
#include "wled20_ir.ino"
#endif
#endif

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -399,6 +399,11 @@ class BusManager {
return false;
}
//Return true if the strip requires a refresh to stay off.
static bool isOffRefreshRequred(uint8_t type) {
return type == TYPE_TM1814;
}
private:
uint8_t numBusses = 0;
Bus* busses[WLED_MAX_BUSSES];

View File

@ -12,24 +12,7 @@ void getStringFromJson(char* dest, const char* src, size_t len) {
if (src != nullptr) strlcpy(dest, src, len);
}
void deserializeConfig() {
bool fromeep = false;
bool success = deserializeConfigSec();
if (!success) { //if file does not exist, try reading from EEPROM
deEEPSettings();
fromeep = true;
}
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
DEBUG_PRINTLN(F("Reading settings from /cfg.json..."));
success = readObjectFromFile("/cfg.json", nullptr, &doc);
if (!success) { //if file does not exist, try reading from EEPROM
if (!fromeep) deEEPSettings();
return;
}
bool deserializeConfig(JsonObject doc, bool fromFS) {
//int rev_major = doc["rev"][0]; // 1
//int rev_minor = doc["rev"][1]; // 0
@ -98,52 +81,58 @@ void deserializeConfig() {
CJSON(strip.rgbwMode, hw_led[F("rgbwm")]);
JsonArray ins = hw_led["ins"];
uint8_t s = 0; //bus iterator
strip.isRgbw = false;
busses.removeAll();
uint32_t mem = 0;
for (JsonObject elm : ins) {
if (s >= WLED_MAX_BUSSES) break;
uint8_t pins[5] = {255, 255, 255, 255, 255};
JsonArray pinArr = elm[F("pin")];
if (pinArr.size() == 0) continue;
pins[0] = pinArr[0];
uint8_t i = 0;
for (int p : pinArr) {
pins[i] = p;
i++;
if (i>4) break;
}
if (fromFS || !ins.isNull()) {
uint8_t s = 0; //bus iterator
strip.isRgbw = false;
busses.removeAll();
uint32_t mem = 0;
for (JsonObject elm : ins) {
if (s >= WLED_MAX_BUSSES) break;
uint8_t pins[5] = {255, 255, 255, 255, 255};
JsonArray pinArr = elm[F("pin")];
if (pinArr.size() == 0) continue;
pins[0] = pinArr[0];
uint8_t i = 0;
for (int p : pinArr) {
pins[i] = p;
i++;
if (i>4) break;
}
uint16_t length = elm[F("len")];
if (length==0) continue;
uint8_t colorOrder = (int)elm[F("order")];
//only use skip from the first strip (this shouldn't have been in ins obj. but remains here for compatibility)
if (s==0) skipFirstLed = elm[F("skip")];
uint16_t start = elm[F("start")] | 0;
if (start >= ledCount) continue;
//limit length of strip if it would exceed total configured LEDs
if (start + length > ledCount) length = ledCount - start;
uint8_t ledType = elm["type"] | TYPE_WS2812_RGB;
bool reversed = elm["rev"];
//RGBW mode is enabled if at least one of the strips is RGBW
strip.isRgbw = (strip.isRgbw || BusManager::isRgbw(ledType));
s++;
BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed);
mem += busses.memUsage(bc);
if (mem <= MAX_LED_MEMORY) busses.add(bc);
uint16_t length = elm[F("len")];
if (length==0) continue;
uint8_t colorOrder = (int)elm[F("order")];
//only use skip from the first strip (this shouldn't have been in ins obj. but remains here for compatibility)
if (s==0) skipFirstLed = elm[F("skip")];
uint16_t start = elm[F("start")] | 0;
if (start >= ledCount) continue;
//limit length of strip if it would exceed total configured LEDs
if (start + length > ledCount) length = ledCount - start;
uint8_t ledType = elm["type"] | TYPE_WS2812_RGB;
bool reversed = elm["rev"];
//RGBW mode is enabled if at least one of the strips is RGBW
strip.isRgbw = (strip.isRgbw || BusManager::isRgbw(ledType));
//refresh is required to remain off if at least one of the strips requires the refresh.
strip.isOffRefreshRequred |= BusManager::isOffRefreshRequred(ledType);
s++;
BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed);
mem += busses.memUsage(bc);
if (mem <= MAX_LED_MEMORY) busses.add(bc);
}
strip.finalizeInit(ledCount, skipFirstLed);
}
strip.finalizeInit(ledCount, skipFirstLed);
if (hw_led["rev"]) busses.getBus(0)->reversed = true; //set 0.11 global reversed setting for first bus
JsonObject hw_btn_ins_0 = hw[F("btn")][F("ins")][0];
CJSON(buttonType, hw_btn_ins_0["type"]);
int hw_btn_pin = hw_btn_ins_0[F("pin")][0];
if (pinManager.allocatePin(hw_btn_pin,false)) {
btnPin = hw_btn_pin;
pinMode(btnPin, INPUT_PULLUP);
} else {
btnPin = -1;
int hw_btn_pin = hw_btn_ins_0[F("pin")][0] | -2; //-2 = not present in doc, keep current. -1 = disable
if (hw_btn_pin > -2) {
if (pinManager.allocatePin(hw_btn_pin,false)) {
btnPin = hw_btn_pin;
pinMode(btnPin, INPUT_PULLUP);
} else {
btnPin = -1;
}
}
JsonArray hw_btn_ins_0_macros = hw_btn_ins_0[F("macros")];
@ -151,26 +140,27 @@ void deserializeConfig() {
CJSON(macroLongPress,hw_btn_ins_0_macros[1]);
CJSON(macroDoublePress, hw_btn_ins_0_macros[2]);
//int hw_btn_ins_0_type = hw_btn_ins_0["type"]; // 0
#ifndef WLED_DISABLE_INFRARED
int hw_ir_pin = hw["ir"]["pin"] | -1; // 4
if (pinManager.allocatePin(hw_ir_pin,false)) {
irPin = hw_ir_pin;
} else {
irPin = -1;
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
if (hw_ir_pin > -2) {
if (pinManager.allocatePin(hw_ir_pin,false)) {
irPin = hw_ir_pin;
} else {
irPin = -1;
}
}
#endif
CJSON(irEnabled, hw["ir"]["type"]);
JsonObject relay = hw[F("relay")];
int hw_relay_pin = relay["pin"];
if (pinManager.allocatePin(hw_relay_pin,true)) {
rlyPin = hw_relay_pin;
pinMode(rlyPin, OUTPUT);
} else {
rlyPin = -1;
int hw_relay_pin = relay["pin"] | -2;
if (hw_relay_pin > -2) {
if (pinManager.allocatePin(hw_relay_pin,true)) {
rlyPin = hw_relay_pin;
pinMode(rlyPin, OUTPUT);
} else {
rlyPin = -1;
}
}
if (relay.containsKey("rev")) {
rlyMde = !relay["rev"];
@ -193,12 +183,13 @@ void deserializeConfig() {
CJSON(fadeTransition, light_tr[F("mode")]);
int tdd = light_tr[F("dur")] | -1;
if (tdd >= 0) transitionDelayDefault = tdd * 100;
CJSON(strip.paletteFade, light_tr[F("pal")]);
CJSON(strip.paletteFade, light_tr["pal"]);
JsonObject light_nl = light["nl"];
CJSON(nightlightMode, light_nl[F("mode")]);
byte prev = nightlightDelayMinsDefault;
CJSON(nightlightDelayMinsDefault, light_nl[F("dur")]);
nightlightDelayMins = nightlightDelayMinsDefault;
if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault;
CJSON(nightlightTargetBri, light_nl[F("tbri")]);
CJSON(macroNl, light_nl[F("macro")]);
@ -227,11 +218,13 @@ void deserializeConfig() {
CJSON(receiveNotificationBrightness, if_sync_recv["bri"]);
CJSON(receiveNotificationColor, if_sync_recv["col"]);
CJSON(receiveNotificationEffects, if_sync_recv[F("fx")]);
//! following line might be a problem if called after boot
receiveNotifications = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects);
JsonObject if_sync_send = if_sync["send"];
prev = notifyDirectDefault;
CJSON(notifyDirectDefault, if_sync_send[F("dir")]);
notifyDirect = notifyDirectDefault;
if (notifyDirectDefault != prev) notifyDirect = notifyDirectDefault;
CJSON(notifyButton, if_sync_send[F("btn")]);
CJSON(notifyAlexa, if_sync_send[F("va")]);
CJSON(notifyHue, if_sync_send[F("hue")]);
@ -310,9 +303,10 @@ void deserializeConfig() {
CJSON(latitude, if_ntp[F("lt")]);
JsonObject ol = doc[F("ol")];
prev = overlayDefault;
CJSON(overlayDefault ,ol[F("clock")]); // 0
CJSON(countdownMode, ol[F("cntdwn")]);
overlayCurrent = overlayDefault;
if (prev != overlayDefault) overlayCurrent = overlayDefault;
CJSON(overlayMin, ol[F("min")]);
CJSON(overlayMax, ol[F("max")]);
@ -386,7 +380,32 @@ void deserializeConfig() {
#endif
JsonObject usermods_settings = doc["um"];
usermods.readFromConfig(usermods_settings);
if (!usermods_settings.isNull()) usermods.readFromConfig(usermods_settings);
if (fromFS) return false;
doReboot = doc[F("rb")] | doReboot;
return (doc["sv"] | true);
}
void deserializeConfigFromFS() {
bool fromeep = false;
bool success = deserializeConfigSec();
if (!success) { //if file does not exist, try reading from EEPROM
deEEPSettings();
fromeep = true;
}
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
DEBUG_PRINTLN(F("Reading settings from /cfg.json..."));
success = readObjectFromFile("/cfg.json", nullptr, &doc);
if (!success) { //if file does not exist, try reading from EEPROM
if (!fromeep) deEEPSettings();
return;
}
deserializeConfig(doc.as<JsonObject>(), true);
}
void serializeConfig() {
@ -513,7 +532,7 @@ void serializeConfig() {
JsonObject light_tr = light.createNestedObject("tr");
light_tr[F("mode")] = fadeTransition;
light_tr[F("dur")] = transitionDelayDefault / 100;
light_tr[F("pal")] = strip.paletteFade;
light_tr["pal"] = strip.paletteFade;
JsonObject light_nl = light.createNestedObject("nl");
light_nl[F("mode")] = nightlightMode;

View File

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

View File

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

View File

@ -182,9 +182,7 @@
}
}
function GetV()
{
}
function GetV(){var d=document;}
</script>
<style>
@import url("style.css");

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

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=500">
<title>Usermod Settings</title>
<script>
var d = document;
var umCfg = {};
var pins = [6,7,8,9,10,11];
var pinO = ["rsvd","rsvd","rsvd","rsvd","rsvd","rsvd"], owner;
var loc = false, locip;
var urows;
var numM = 0;
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);
}
}
GetV();
if (numM > 0 || locip) ldS();
else gId("um").innerHTML = "No Usermods installed.";
}
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 = "Usermods configuration not found.<br>Press <i>Save</i> to initialize defaults.";
gId("um").innerHTML = urows;
})
.catch(function (error) {
gId('lserr').style.display = "inline"
console.log(error);
});
}
function svS(e) {
e.preventDefault();
console.log(d.Sf);
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
}
function GetV() {}
</script>
<style>
@import url("style.css");
</style>
</head>
<body onload="S()">
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
<span id="lssuc" style="color:green; display:none">&#10004; Configuration saved!</span>
<span id="lserr" style="color:red; display:none">&#9888; Could not load configuration.</span><hr>
</div>
<h2>Usermod Setup</h2>
<div id="um">Loading settings...</div>
<hr><button type="button" onclick="B()">Back</button><button type="submit">Save</button>
</form>
</body>
</html>

View File

@ -43,7 +43,7 @@
Installed version: ##VERSION##<br>
Download the latest binary: <a href="https://github.com/Aircoookie/WLED/releases" target="_blank">
<img src="https://img.shields.io/github/release/Aircoookie/WLED.svg?style=flat-square"></a><br>
<input type='file' class="bt" name='update' accept=".bin" required><br>
<input type='file' class="bt" name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<input type='submit' class="bt" value='Update!' ><br>
<button type="button" class="bt" onclick="B()">Back</button></form>
<div id="msg"><b>Updating...</b><br>Please do not close or refresh the page :)</div>

View File

@ -26,7 +26,8 @@ void handleButton();
void handleIO();
//cfg.cpp
void deserializeConfig();
bool deserializeConfig(JsonObject doc, bool fromFS = false);
void deserializeConfigFromFS();
bool deserializeConfigSec();
void serializeConfig();
void serializeConfigSec();
@ -151,6 +152,7 @@ void _overlayCronixie();
void _drawOverlayCronixie();
//playlist.cpp
void shufflePlaylist();
void unloadPlaylist();
void loadPlaylist(JsonObject playlistObject);
void handlePlaylist();
@ -187,6 +189,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;}
};
@ -207,7 +211,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();

View File

@ -55,13 +55,12 @@ bool bufferedFind(const char *target, bool fromStart = true) {
size_t targetLen = strlen(target);
size_t index = 0;
uint16_t bufsize = 0, count = 0;
byte buf[FS_BUFSIZE];
if (fromStart) f.seek(0);
while (f.position() < f.size() -1) {
bufsize = f.read(buf, FS_BUFSIZE);
count = 0;
uint16_t bufsize = f.read(buf, FS_BUFSIZE);
uint16_t count = 0;
while (count < bufsize) {
if(buf[count] != target[index])
index = 0; // reset index if any char does not match
@ -97,13 +96,12 @@ bool bufferedFindSpace(uint16_t targetLen, bool fromStart = true) {
if (!f || !f.size()) return false;
uint16_t index = 0;
uint16_t bufsize = 0, count = 0;
byte buf[FS_BUFSIZE];
if (fromStart) f.seek(0);
while (f.position() < f.size() -1) {
bufsize = f.read(buf, FS_BUFSIZE);
count = 0;
uint16_t bufsize = f.read(buf, FS_BUFSIZE);
uint16_t count = 0;
while (count < bufsize) {
if(buf[count] == ' ') {
@ -140,13 +138,12 @@ bool bufferedFindObjectEnd() {
if (!f || !f.size()) return false;
uint16_t objDepth = 0; //num of '{' minus num of '}'. return once 0
uint16_t bufsize = 0, count = 0;
//size_t start = f.position();
byte buf[FS_BUFSIZE];
while (f.position() < f.size() -1) {
bufsize = f.read(buf, FS_BUFSIZE);
count = 0;
uint16_t bufsize = f.read(buf, FS_BUFSIZE);
uint16_t count = 0;
while (count < bufsize) {
if (buf[count] == '{') objDepth++;

View File

@ -45,10 +45,10 @@ action="/update" id="uf" enctype="multipart/form-data" onsubmit="U()">
Installed version: 0.12.1-b1<br>Download the latest binary: <a
href="https://github.com/Aircoookie/WLED/releases" target="_blank"><img
src="https://img.shields.io/github/release/Aircoookie/WLED.svg?style=flat-square">
</a><br><input type="file" class="bt" name="update" accept=".bin" required><br>
<input type="submit" class="bt" value="Update!"><br><button type="button"
class="bt" onclick="B()">Back</button></form><div id="msg"><b>Updating...</b>
<br>Please do not close or refresh the page :)</div></body></html>)=====";
</a><br><input type="file" class="bt" name="update" required><br><input
type="submit" class="bt" value="Update!"><br><button type="button" class="bt"
onclick="B()">Back</button></form><div id="msg"><b>Updating...</b><br>
Please do not close or refresh the page :)</div></body></html>)=====";
// Autogenerated from wled00/data/welcome.htm, do not edit!!

View File

@ -12,7 +12,7 @@ const char PAGE_settingsCss[] PROGMEM = R"=====(<style>body{font-family:Verdana,
// Autogenerated from wled00/data/settings.htm, do not edit!!
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>Usermod Settings</title><script>
var owner,locip,urows,d=document,umCfg={},pins=[6,7,8,9,10,11],pinO=["rsvd","rsvd","rsvd","rsvd","rsvd","rsvd"],loc=!1,numM=0;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))),GetV(),numM>0||locip?ldS():gId("um").innerHTML="No Usermods installed."}function check(e,n){var o=e.name.replace("[]","").substr(-3);if("number"==e.type&&"pin"==o.substr(0,3))for(var i=0;i<pins.length;i++)if(n!=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[o,i]of Object.entries(e))if(isO(i))owner=o,getPins(i);else if("pin"==o.replace("[]","").substr(-3))if(Array.isArray(i))for(var n=0;n<i.length;n++)i[n]>=0&&(pins.push(i[n]),pinO.push(owner));else i>=0&&(pins.push(i),pinO.push(owner));else if(Array.isArray(i))for(n=0;n<i.length;n++)getPins(i[n])}function addField(e,n,o,i=!1){if(isO(o))for(const[n,i]of Object.entries(o))addField(e,n,i);else if(Array.isArray(o))for(var t=0;t<o.length;t++)addField(e,n,o[t],!0);else{var r,s;switch(typeof o){case"boolean":r="checkbox",s=o?'checked value="on"':"";break;case"number":r="number",s=`value="${parseInt(o,10)}"`;break;case"string":default:r="text",s=`value="${o}"`}"checkbox"==r&&(urows+=`<input type="hidden" name="${e}_${n}${i?"[]":""}" value="off">`),urows+=`${n}: <input type="${r}" name="${e}_${n}${i?"[]":""}" ${s} 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,n]of Object.entries(umCfg))urows+=`<hr><h3>${e}</h3>`,addField(e,"unknown",n);""===urows&&(urows="Usermods configuration not found.<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() {var d=document;
%CSS%%SCSS%</head><body onload="S()"><form
id="form_s" name="Sf" method="post" onsubmit="svS(event)"><div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div><button
type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
<span id="lssuc" style="color:green;display:none">&#10004; Configuration saved!
</span> <span id="lserr" style="color:red;display:none">
&#9888; Could not load configuration.</span><hr></div><h2>Usermod Setup</h2><div
id="um">Loading settings...</div><hr><button type="button" onclick="B()">Back
</button><button type="submit">Save</button></form></body></html>)=====";

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@ const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t);
void incBrightness()
{
// dumb incremental search is efficient enough for so few items
for (int index = 0; index < numBrightnessSteps; ++index)
for (uint8_t index = 0; index < numBrightnessSteps; ++index)
{
if (brightnessSteps[index] > bri)
{
@ -234,7 +234,7 @@ void decodeIR24(uint32_t code)
switch (code) {
case IR24_BRIGHTER : incBrightness(); break;
case IR24_DARKER : decBrightness(); break;
case IR24_OFF : briLast = bri; bri = 0; break;
case IR24_OFF : if (bri > 0) briLast = bri; bri = 0; break;
case IR24_ON : bri = briLast; break;
case IR24_RED : colorFromUint32(COLOR_RED); break;
case IR24_REDDISH : colorFromUint32(COLOR_REDDISH); break;
@ -266,7 +266,7 @@ void decodeIR24OLD(uint32_t code)
switch (code) {
case IR24_OLD_BRIGHTER : incBrightness(); break;
case IR24_OLD_DARKER : decBrightness(); break;
case IR24_OLD_OFF : briLast = bri; bri = 0; break;
case IR24_OLD_OFF : if (bri > 0) briLast = bri; bri = 0; break;
case IR24_OLD_ON : bri = briLast; break;
case IR24_OLD_RED : colorFromUint32(COLOR_RED); break;
case IR24_OLD_REDDISH : colorFromUint32(COLOR_REDDISH); break;
@ -298,7 +298,7 @@ void decodeIR24CT(uint32_t code)
switch (code) {
case IR24_CT_BRIGHTER : incBrightness(); break;
case IR24_CT_DARKER : decBrightness(); break;
case IR24_CT_OFF : briLast = bri; bri = 0; break;
case IR24_CT_OFF : if (bri > 0) briLast = bri; bri = 0; break;
case IR24_CT_ON : bri = briLast; break;
case IR24_CT_RED : colorFromUint32(COLOR_RED); break;
case IR24_CT_REDDISH : colorFromUint32(COLOR_REDDISH); break;
@ -332,7 +332,7 @@ void decodeIR40(uint32_t code)
switch (code) {
case IR40_BPLUS : incBrightness(); break;
case IR40_BMINUS : decBrightness(); break;
case IR40_OFF : briLast = bri; bri = 0; break;
case IR40_OFF : if (bri > 0) briLast = bri; bri = 0; break;
case IR40_ON : bri = briLast; break;
case IR40_RED : colorFromUint24(COLOR_RED); break;
case IR40_REDDISH : colorFromUint24(COLOR_REDDISH); break;
@ -389,7 +389,7 @@ void decodeIR44(uint32_t code)
switch (code) {
case IR44_BPLUS : incBrightness(); break;
case IR44_BMINUS : decBrightness(); break;
case IR44_OFF : briLast = bri; bri = 0; break;
case IR44_OFF : if (bri > 0) briLast = bri; bri = 0; break;
case IR44_ON : bri = briLast; break;
case IR44_RED : colorFromUint24(COLOR_RED); break;
case IR44_REDDISH : colorFromUint24(COLOR_REDDISH); break;
@ -452,7 +452,7 @@ void decodeIR21(uint32_t code)
switch (code) {
case IR21_BRIGHTER: incBrightness(); break;
case IR21_DARKER: decBrightness(); break;
case IR21_OFF: briLast = bri; bri = 0; break;
case IR21_OFF: if (bri > 0) briLast = bri; bri = 0; break;
case IR21_ON: bri = briLast; break;
case IR21_RED: colorFromUint32(COLOR_RED); break;
case IR21_REDDISH: colorFromUint32(COLOR_REDDISH); break;

View File

@ -98,13 +98,13 @@ void deserializeSegment(JsonObject elem, byte it)
effectCurrent = elem[F("fx")] | effectCurrent;
effectSpeed = elem[F("sx")] | effectSpeed;
effectIntensity = elem[F("ix")] | effectIntensity;
effectPalette = elem[F("pal")] | effectPalette;
effectPalette = elem["pal"] | effectPalette;
} else { //permanent
byte fx = elem[F("fx")] | seg.mode;
if (fx != seg.mode && fx < strip.getModeCount()) strip.setMode(id, fx);
seg.speed = elem[F("sx")] | seg.speed;
seg.intensity = elem[F("ix")] | seg.intensity;
seg.palette = elem[F("pal")] | seg.palette;
seg.palette = elem["pal"] | seg.palette;
}
JsonArray iarr = elem[F("i")]; //set individual LEDs
@ -134,7 +134,7 @@ void deserializeSegment(JsonObject elem, byte it)
if (icol.isNull()) break;
byte sz = icol.size();
if (sz == 0 && sz > 4) break;
if (sz == 0 || sz > 4) break;
int rgbw[] = {0,0,0,0};
copyArray(icol, rgbw);
@ -339,7 +339,7 @@ void serializeSegment(JsonObject& root, WS2812FX::Segment& seg, byte id, bool fo
root[F("fx")] = seg.mode;
root[F("sx")] = seg.speed;
root[F("ix")] = seg.intensity;
root[F("pal")] = seg.palette;
root["pal"] = seg.palette;
root[F("sel")] = seg.isSelected();
root["rev"] = seg.getOption(SEG_OPTION_REVERSED);
root[F("mi")] = seg.getOption(SEG_OPTION_MIRROR);
@ -729,21 +729,24 @@ void serveJson(AsyncWebServerRequest* request)
const String& url = request->url();
if (url.indexOf("state") > 0) subJson = 1;
else if (url.indexOf("info") > 0) subJson = 2;
else if (url.indexOf("si") > 0) subJson = 3;
else if (url.indexOf("si") > 0) subJson = 3;
else if (url.indexOf("nodes") > 0) subJson = 4;
else if (url.indexOf("palx") > 0) subJson = 5;
else if (url.indexOf("palx") > 0) subJson = 5;
else if (url.indexOf("live") > 0) {
serveLiveLeds(request);
return;
}
else if (url.indexOf(F("eff")) > 0) {
else if (url.indexOf(F("eff")) > 0) {
request->send_P(200, "application/json", JSON_mode_names);
return;
}
else if (url.indexOf(F("pal")) > 0) {
else if (url.indexOf("pal") > 0) {
request->send_P(200, "application/json", JSON_palette_names);
return;
}
else if (url.indexOf("cfg") > 0 && handleFileRead(request, "/cfg.json")) {
return;
}
else if (url.length() > 6) { //not just /json
request->send( 501, "application/json", F("{\"error\":\"Not implemented\"}"));
return;

View File

@ -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"));
}
@ -62,42 +62,58 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties
DEBUG_PRINTLN(F("no payload -> leave"));
return;
}
DEBUG_PRINTLN(payload);
char* payloadStr;
bool alloc = false;
// check if payload is 0-terminated
if (payload[len-1] == '\0') {
payloadStr = payload;
} else {
payloadStr = new char[len+1];
strncpy(payloadStr, payload, len);
payloadStr[len] = '\0';
alloc = true;
}
if (payloadStr == nullptr) return; //no mem
DEBUG_PRINTLN(payloadStr);
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, payloadStr);
if (alloc) delete[] payloadStr;
return;
}
}
//Prefix is stripped from the topic at this point
if (strcmp(topic, "/col") == 0)
{
colorFromDecOrHexString(col, (char*)payload);
if (strcmp_P(topic, PSTR("/col")) == 0) {
colorFromDecOrHexString(col, (char*)payloadStr);
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);
deserializeJson(doc, payloadStr);
deserializeState(doc.as<JsonObject>());
} else { //HTTP API
String apireq = "win&";
apireq += (char*)payload;
apireq += (char*)payloadStr;
handleSet(nullptr, apireq);
}
} else if (strcmp(topic, "") == 0)
{
parseMQTTBriPayload(payload);
} else if (strlen(topic) != 0) {
// non standard topic, check with usermods
usermods.onMqttMessage(topic, payloadStr);
} else {
// topmost topic (just wled/MAC)
parseMQTTBriPayload(payloadStr);
}
if (alloc) delete[] payloadStr;
}
@ -110,24 +126,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 +173,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();

View File

@ -112,7 +112,7 @@ void updateTimezone() {
break;
}
case TZ_AUSTRALIA_NORTHERN : {
tcrStandard = {First, Sun, Apr, 3, 570}; //ACST = UTC + 9.5 hours
tcrDaylight = {First, Sun, Apr, 3, 570}; //ACST = UTC + 9.5 hours
tcrStandard = tcrDaylight;
break;
}

View File

@ -63,10 +63,9 @@ void _overlayAnalogClock()
}
if (analogClock5MinuteMarks)
{
int pix;
for (int i = 0; i <= 12; i++)
for (byte i = 0; i <= 12; i++)
{
pix = analogClock12pixel + round((overlaySize / 12.0) *i);
int pix = analogClock12pixel + round((overlaySize / 12.0) *i);
if (pix > overlayMax) pix -= overlaySize;
strip.setPixelColor(pix, 0x00FFAA);
}
@ -80,7 +79,7 @@ void _overlayAnalogClock()
void _overlayAnalogCountdown()
{
if (now() < countdownTime)
if ((unsigned long)now() < countdownTime)
{
long diff = countdownTime - now();
double pval = 60;

View File

@ -19,14 +19,14 @@ uint16_t playlistEntryDur = 0;
void shufflePlaylist() {
int currentIndex = playlistLen, randomIndex;
int currentIndex = playlistLen;
PlaylistEntry temporaryValue, *entries = reinterpret_cast<PlaylistEntry*>(playlistEntries);
// While there remain elements to shuffle...
while (currentIndex--) {
// Pick a random element...
randomIndex = random(0, currentIndex);
int randomIndex = random(0, currentIndex);
// And swap it with the current element.
temporaryValue = entries[currentIndex];
entries[currentIndex] = entries[randomIndex];

View File

@ -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)
@ -124,7 +124,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
doInitBusses = true;
}
ledCount = request->arg(F("LC")).toInt();
t = request->arg(F("LC")).toInt();
if (t > 0 && t <= MAX_LEDS) ledCount = t;
// upate other pins
@ -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();
}

View File

@ -14,6 +14,11 @@ void UsermodManager::addToJsonInfo(JsonObject& obj) { for (byte i = 0; i < n
void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); }
void UsermodManager::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.

View File

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

View File

@ -221,7 +221,7 @@ void WLED::loop()
yield();
if (!offMode)
if (!offMode || strip.isOffRefreshRequred)
strip.service();
#ifdef ESP8266
else if (!noWifiSleep)
@ -333,7 +333,7 @@ void WLED::setup()
errorFlag = ERR_FS_BEGIN;
} else deEEP();
updateFSInfo();
deserializeConfig();
deserializeConfigFromFS();
#if STATUSLED
bool lStatusLed = false;

View File

@ -8,7 +8,7 @@
*/
// version code in format yymmddb (b = daily build)
#define VERSION 2104220
#define VERSION 2105120
//uncomment this if you have a "my_config.h" file you'd like to use
//#define WLED_USE_MY_CONFIG

View File

@ -84,6 +84,7 @@ void initServer()
AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler("/json", [](AsyncWebServerRequest *request) {
bool verboseResponse = false;
bool isConfig = false;
{ //scope JsonDocument so it releases its buffer
DynamicJsonDocument jsonBuffer(JSON_BUFFER_SIZE);
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject));
@ -91,12 +92,22 @@ void initServer()
if (error || root.isNull()) {
request->send(400, "application/json", F("{\"error\":9}")); return;
}
fileDoc = &jsonBuffer;
verboseResponse = deserializeState(root);
fileDoc = nullptr;
const String& url = request->url();
isConfig = url.indexOf("cfg") > -1;
if (!isConfig) {
fileDoc = &jsonBuffer;
verboseResponse = deserializeState(root);
fileDoc = nullptr;
} else {
verboseResponse = deserializeConfig(root); //use verboseResponse to determine whether cfg change should be saved immediately
}
}
if (verboseResponse) { //if JSON contains "v"
serveJson(request); return;
if (verboseResponse) {
if (!isConfig) {
serveJson(request); return; //if JSON contains "v"
} else {
serializeConfig(); //Save new settings to FS
}
}
request->send(200, "application/json", F("{\"success\":true}"));
});
@ -314,6 +325,7 @@ String settingsProcessor(const String& var)
{
if (var == "CSS") {
char buf[2048];
buf[0] = 0;
getSettingsJS(optionType, buf);
return String(buf);
}
@ -365,6 +377,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 +399,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 +426,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);
}

View File

@ -185,7 +185,7 @@ void getSettingsJS(byte subPage, char* dest)
obuf = dest;
olen = 0;
if (subPage <1 || subPage >7) return;
if (subPage <1 || subPage >8) return;
if (subPage == 1) {
sappends('s',SET_F("CS"),clientSSID);
@ -518,7 +518,15 @@ void getSettingsJS(byte subPage, char* dest)
sappend('i',SET_F("CH13"),DMXFixtureMap[12]);
sappend('i',SET_F("CH14"),DMXFixtureMap[13]);
sappend('i',SET_F("CH15"),DMXFixtureMap[14]);
}
}
#endif
if (subPage == 8) //usermods
{
oappend(SET_F("numM="));
oappendi(usermods.getModCount());
oappend(";");
}
oappend(SET_F("}</script>"));
}