User configurable usermods.

- Utilizing addToConfig() & readFromConfig()
- UM configuration Settings page
- MQTT support for usermods
- A few sample usermods including rewritten Temparature and new Multi-Relay
This commit is contained in:
Blaz Kristan
2021-05-07 12:41:39 +02:00
parent fa075f6800
commit 136a00a301
23 changed files with 2583 additions and 1437 deletions

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

@@ -41,6 +41,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>

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

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

View File

@@ -150,6 +150,7 @@ void _overlayCronixie();
void _drawOverlayCronixie();
//playlist.cpp
void shufflePlaylist();
void unloadPlaylist();
void loadPlaylist(JsonObject playlistObject);
void handlePlaylist();
@@ -186,10 +187,12 @@ 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;}
};
class UsermodManager {
class UsermodManager : public Usermod {
private:
Usermod* ums[WLED_MAX_USERMODS];
byte numMods = 0;
@@ -206,7 +209,8 @@ class UsermodManager {
void addToConfig(JsonObject& obj);
void readFromConfig(JsonObject& obj);
void onMqttConnect(bool sessionPresent);
bool onMqttMessage(char* topic, char* payload);
bool add(Usermod* um);
Usermod* lookup(uint16_t mod_id);
byte getModCount();

View File

@@ -22,7 +22,8 @@ LED Preferences</button></form><form action="/settings/ui"><button
type="submit">User Interface</button></form>%DMXMENU%<form action="/settings/sync">
<button type="submit">Sync Interfaces</button></form><form
action="/settings/time"><button type="submit">Time & Macros</button></form><form
action="/settings/sec"><button type="submit">Security & Updates</button></form>
action="/settings/um"><button type="submit">Usermods</button></form><form
action="/settings/sec"><button type="submit">Security & Updates</button></form>
</body></html>)=====";
@@ -390,3 +391,18 @@ MIT license</a></i><br><br>Server message: <span class="sip">Response error!
</span><hr><button type="button" onclick="B()">Back</button><button
type="submit">Save & Reboot</button></form></body></html>)=====";
// Autogenerated from wled00/data/settings_um.htm, do not edit!!
const char PAGE_settings_um[] PROGMEM = R"=====(<!DOCTYPE html><html><head lang="en"><meta charset="utf-8"><meta
name="viewport" content="width=500"><title>UI Settings</title><script>
var owner,locip,urows,d=document,umCfg={},pins=[6,7,8,9,10,11],pinO=["reserved","reserved","reserved","reserved","reserved","reserved"],loc=!1;function gId(e){return d.getElementById(e)}function isO(e){return e&&"object"==typeof e&&!Array.isArray(e)}function H(){window.open("https://github.com/Aircoookie/WLED/wiki/Settings#usermod-settings")}function B(){window.open("/settings","_self")}function S(){"file:"==window.location.protocol&&(loc=!0,(locip=localStorage.getItem("locIp"))||(locip=prompt("File Mode. Please enter WLED IP!"),localStorage.setItem("locIp",locip))),ldS()}function check(e,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 r=0;r<o.length;r++)addField(e,n,o[r],!0);else{var t,s;switch(typeof o){case"boolean":t="checkbox",s=o?'checked value="on"':"";break;case"number":t="number",s=`value="${parseInt(o,10)}"`;break;case"string":default:t="text",s=`value="${o}"`}"checkbox"==t&&(urows+=`<input type="hidden" name="${e}_${n}${i?"[]":""}" value="off">`),urows+=`${n}: <input type="${t}" 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);gId("um").innerHTML=urows}}).catch((function(e){gId("lserr").style.display="inline",console.log(e)}))}function svS(e){e.preventDefault(),console.log(d.Sf),d.Sf.checkValidity()&&d.Sf.submit()}function GetV(){}
</script>%CSS%%SCSS%</head><body onload="S()"><form
id="form_s" name="Sf" method="post" onsubmit="svS(event)"><div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div><button
type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
<span id="lssuc" style="color:green;display:none">&#10004; Configuration saved!
</span> <span id="lserr" style="color:red;display:none">
&#9888; Could not load configuration.</span><hr></div><h2>Usermod Setup</h2><div
id="um">Loading settings...</div><hr><button type="button" onclick="B()">Back
</button><button type="submit">Save</button></form></body></html>)=====";

File diff suppressed because it is too large Load Diff

View File

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

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
}