diff --git a/CHANGELOG.md b/CHANGELOG.md index 2321c9a0..0877c494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ### Builds after release 0.12.0 +#### Build 2106250 + +- Fixed preset only disabling on second effect/color change + +#### Build 2106241 + +- BREAKING: Added ability for usermods to force a config save if config incomplete. `readFromConfig()` needs to return a `bool` to indicate if the config is complete +- Updated usermods implementing `readFromConfig()` +- Auto-create segments based on configured busses + +#### Build 2106200 + +- Added 2 Ethernet boards and split Ethernet configs into separate file + #### Build 2106180 - Fixed DOS on Chrome tab restore causing reboot diff --git a/tools/WLED_ESP32_16MB.csv b/tools/WLED_ESP32_16MB.csv new file mode 100644 index 00000000..de78209d --- /dev/null +++ b/tools/WLED_ESP32_16MB.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x200000, +app1, app, ota_1, 0x210000,0x200000, +spiffs, data, spiffs, 0x410000,0xBE0000, \ No newline at end of file diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 696d33f3..e85db95b 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -427,7 +427,7 @@ class Animated_Staircase : public Usermod { /* * Reads the configuration to internal flash memory before setup() is called. */ - void readFromConfig(JsonObject& root) { + bool readFromConfig(JsonObject& root) { bool oldUseUSSensorTop = useUSSensorTop; bool oldUseUSSensorBottom = useUSSensorBottom; int8_t oldTopAPin = topPIRorTriggerPin; @@ -435,6 +435,8 @@ class Animated_Staircase : public Usermod { int8_t oldBottomAPin = bottomPIRorTriggerPin; int8_t oldBottomBPin = bottomEchoPin; + bool configComplete = true; + JsonObject staircase = root[FPSTR(_name)]; if (!staircase.isNull()) { if (staircase[FPSTR(_enabled)].is()) { @@ -468,6 +470,7 @@ class Animated_Staircase : public Usermod { bottomMaxDist = min(150,max(30,staircase[FPSTR(_bottomEchoCm)].as())); // max distance ~1.5m (a lag of 9ms may be expected) } else { DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + configComplete = false; } if (!initDone) { // first run: reading from cfg.json @@ -490,6 +493,7 @@ class Animated_Staircase : public Usermod { } if (changed) setup(); } + return configComplete; } /* diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index ef68e907..ac466935 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -133,11 +133,28 @@ class MyExampleUsermod : public Usermod { * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case your config was complete, or false if you'd like WLED to save your defaults to disk + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated */ - void readFromConfig(JsonObject& root) + bool readFromConfig(JsonObject& root) { - JsonObject top = root["top"]; - userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) + userVar0 = 42; //set your variables to their boot default value (this can also be done when declaring the variable) + + JsonObject top = root["exampleUsermod"]; + if (!top.isNull()) { + bool configComplete = true; + + //check if value is there + if (top.containsKey("great")) { + //convert value to the correct type + userVar0 = top["great"].as(); + } else configComplete = false; + + if (configComplete) return true; + } + return false; } diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h index cb2f1b0c..2e9f43c6 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h @@ -149,11 +149,13 @@ Delay ())); // check bounds @@ -398,6 +398,8 @@ public: DEBUG_PRINTLN(F("PIR config (re)loaded.")); } } + + return true; } /** diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h index 3fd1fafa..ac86f390 100644 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h @@ -169,7 +169,7 @@ public: /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json */ - void readFromConfig(JsonObject &root) + bool readFromConfig(JsonObject &root) { // we look for JSON object. JsonObject top = root[FPSTR(_name)]; @@ -196,7 +196,9 @@ public: else { DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + return false; } + return true; } }; diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index a21d8368..46cb9fa1 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -234,12 +234,13 @@ class UsermodTemperature : public Usermod { /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json */ - void readFromConfig(JsonObject &root) { + bool 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()) return; + if (top.isNull()) return true; + bool configComplete = true; if (top["pin"] != nullptr) { if (top[FPSTR(_enabled)].is()) { disabled = !top[FPSTR(_enabled)].as(); @@ -282,6 +283,7 @@ class UsermodTemperature : public Usermod { DEBUG_PRINTLN(F("Temperature config loaded.")); } else { DEBUG_PRINTLN(F("Temperature config re-loaded.")); + configComplete = false; // changing paramters from settings page if (newTemperaturePin != temperaturePin) { // deallocate pin and release memory @@ -292,6 +294,7 @@ class UsermodTemperature : public Usermod { setup(); } } + return configComplete; } uint16_t getId() diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 92a0f060..426fe49b 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -348,11 +348,11 @@ class MultiRelay : public Usermod { * restore the changeable values * readFromConfig() is called before setup() to populate properties from values stored in cfg.json */ - void readFromConfig(JsonObject &root) { + bool readFromConfig(JsonObject &root) { int8_t oldPin[MULTI_RELAY_MAX_RELAYS]; JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) return; + if (top.isNull()) return false; if (top[FPSTR(_enabled)] != nullptr) { if (top[FPSTR(_enabled)].is()) { @@ -413,6 +413,7 @@ class MultiRelay : public Usermod { } DEBUG_PRINTLN(F("MultiRelay config (re)loaded.")); } + return true; } /** diff --git a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h index 8d8bcf0b..816b6e8d 100644 --- a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h +++ b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h @@ -82,18 +82,6 @@ class StairwayWipeUsermod : public Usermod { //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); } - void addToConfig(JsonObject& root) - { - JsonObject top = root.createNestedObject("exampleUsermod"); - top["great"] = userVar0; //save this var persistently whenever settings are saved - } - - void readFromConfig(JsonObject& root) - { - JsonObject top = root["top"]; - userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) - } - uint16_t getId() { return USERMOD_ID_EXAMPLE; diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index 43eb1fc3..68489b56 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -1,244 +1,247 @@ -#pragma once - -#include "wled.h" - -// v2 Usermod to automatically save settings -// to configurable preset after a change to any of -// -// * brightness -// * effect speed -// * effect intensity -// * mode (effect) -// * palette -// -// 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 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. - -// format: "~ MM-DD HH:MM:SS ~" -#define PRESET_NAME_BUFFER_SIZE 25 - -class AutoSaveUsermod : public Usermod { - - 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; - uint8_t knownEffectIntensity = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; - - #ifdef USERMOD_FOUR_LINE_DISPLAY - FourLineDisplayUsermod* display; - #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_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 || strip.isUpdating()) 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; - if (applyAutoSaveOnBoot) applyPreset(autoSavePreset); - knownBrightness = bri; - knownEffectSpeed = effectSpeed; - knownEffectIntensity = effectIntensity; - knownMode = currentMode; - knownPalette = currentPalette; - return; - } - - unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; - if (knownBrightness != bri) { - knownBrightness = bri; - autoSaveAfter = wouldAutoSaveAfter; - } else if (knownEffectSpeed != effectSpeed) { - knownEffectSpeed = effectSpeed; - autoSaveAfter = wouldAutoSaveAfter; - } else if (knownEffectIntensity != effectIntensity) { - knownEffectIntensity = effectIntensity; - autoSaveAfter = wouldAutoSaveAfter; - } else if (knownMode != currentMode) { - knownMode = currentMode; - autoSaveAfter = wouldAutoSaveAfter; - } else if (knownPalette != currentPalette) { - knownPalette = currentPalette; - autoSaveAfter = wouldAutoSaveAfter; - } - - if (autoSaveAfter && now > autoSaveAfter) { - autoSaveAfter = 0; - // Time to auto save. You may have some flickry? - saveSettings(); - displayOverlay(); - } - } - - /* - * 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) { - //} - - /* - * 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) { - // 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. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will also not yet add your setting to one of the settings pages automatically. - * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * - * 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.")); - } - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * 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()) { - // reading from cfg.json - enabled = top[FPSTR(_autoSaveEnabled)].as(); - } 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())); - autoSavePreset = min(250,max(100,top[FPSTR(_autoSavePreset)].as())); - if (top[FPSTR(_autoSaveApplyOnBoot)].is()) { - // reading from cfg.json - applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)].as(); - } 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!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - 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"; +#pragma once + +#include "wled.h" + +// v2 Usermod to automatically save settings +// to configurable preset after a change to any of +// +// * brightness +// * effect speed +// * effect intensity +// * mode (effect) +// * palette +// +// 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 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. + +// format: "~ MM-DD HH:MM:SS ~" +#define PRESET_NAME_BUFFER_SIZE 25 + +class AutoSaveUsermod : public Usermod { + + 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; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + + #ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod* display; + #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_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 || strip.isUpdating()) 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; + if (applyAutoSaveOnBoot) applyPreset(autoSavePreset); + knownBrightness = bri; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownMode = currentMode; + knownPalette = currentPalette; + return; + } + + unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; + if (knownBrightness != bri) { + knownBrightness = bri; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectSpeed != effectSpeed) { + knownEffectSpeed = effectSpeed; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectIntensity != effectIntensity) { + knownEffectIntensity = effectIntensity; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownMode != currentMode) { + knownMode = currentMode; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownPalette != currentPalette) { + knownPalette = currentPalette; + autoSaveAfter = wouldAutoSaveAfter; + } + + if (autoSaveAfter && now > autoSaveAfter) { + autoSaveAfter = 0; + // Time to auto save. You may have some flickry? + saveSettings(); + displayOverlay(); + } + } + + /* + * 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) { + //} + + /* + * 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) { + // 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. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * 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.")); + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + bool 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 false; + } + + bool configComplete = true; + + if (top[FPSTR(_autoSaveEnabled)].is()) { + // reading from cfg.json + enabled = top[FPSTR(_autoSaveEnabled)].as(); + } 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())); + autoSavePreset = min(250,max(100,top[FPSTR(_autoSavePreset)].as())); + if (top[FPSTR(_autoSaveApplyOnBoot)].is()) { + // reading from cfg.json + applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)].as(); + } 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.")); + return true; + } + + /* + * 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_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"; diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h index dc2b65f1..7547ca61 100644 --- a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -1,703 +1,707 @@ -#pragma once - -#include "wled.h" -#include // from https://github.com/olikraus/u8g2/ - -// -// Insired by the v1 usermod: ssd1306_i2c_oled_u8g2 -// -// v2 usermod for using 128x32 or 128x64 i2c -// OLED displays to provide a four line display -// for WLED. -// -// Dependencies -// * This usermod REQURES the ModeSortUsermod -// * This Usermod works best, by far, when coupled -// with RotaryEncoderUIUsermod. -// -// Make sure to enable NTP and set your time zone in WLED Config | Time. -// -// REQUIREMENT: You must add the following requirements to -// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini -// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) -// REQUIREMENT: * Wire -// - -//The SCL and SDA pins are defined here. -#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 - #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 60*1000 // 1 min - -#define TIME_INDENT 0 -#define DATE_INDENT 2 - -// Minimum time between redrawing screen in ms -#define USER_LOOP_REFRESH_RATE_MS 1000 - -// Extra char (+1) for null -#define LINE_BUFFER_SIZE 16+1 - -typedef enum { - FLD_LINE_BRIGHTNESS = 0, - FLD_LINE_EFFECT_SPEED, - FLD_LINE_EFFECT_INTENSITY, - FLD_LINE_MODE, - FLD_LINE_PALETTE, - FLD_LINE_TIME -} 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 - SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C - SSD1305_64 // U8X8_SSD1305_128X64_ADAFRUIT_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 - - // Next variables hold the previous known values to determine if redraw is - // required. - String knownSsid = ""; - IPAddress knownIp; - uint8_t knownBrightness = 0; - uint8_t knownEffectSpeed = 0; - uint8_t knownEffectIntensity = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; - uint8_t knownMinute = 99; - uint8_t knownHour = 99; - - bool displayTurnedOff = false; - unsigned long lastUpdate = 0; - unsigned long lastRedraw = 0; - unsigned long overlayUntil = 0; - Line4Type lineType = FLD_LINE_BRIGHTNESS; - // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. - byte markLineNum = 0; - - // 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() { - 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 - lineHeight = 1; - 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 - lineHeight = 2; - 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 - lineHeight = 2; - break; - case SSD1305: - #ifdef ESP8266 - if (!(sclPin==5 && sdaPin==4)) - u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset - else - #endif - u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA - lineHeight = 1; - break; - case SSD1305_64: - #ifdef ESP8266 - if (!(sclPin==5 && sdaPin==4)) - u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset - else - #endif - u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA - lineHeight = 2; - break; - default: - u8x8 = nullptr; - type = NONE; - return; - } - (static_cast(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 - // interfaces here - void connected() {} - - /** - * Da loop. - */ - void loop() { - if (millis() - lastUpdate < (clockMode?1000:refreshRate) || strip.isUpdating()) return; - lastUpdate = millis(); - - redraw(false); - } - - /** - * Wrappers for screen drawing - */ - void setFlipMode(uint8_t mode) { - if (type==NONE) return; - (static_cast(u8x8))->setFlipMode(mode); - } - void setContrast(uint8_t contrast) { - if (type==NONE) return; - (static_cast(u8x8))->setContrast(contrast); - } - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { - if (type==NONE) return; - (static_cast(u8x8))->setFont(u8x8_font_chroma48medium8_r); - if (!ignoreLH && lineHeight==2) (static_cast(u8x8))->draw1x2String(col, row, string); - else (static_cast(u8x8))->drawString(col, row, string); - } - void draw2x2String(uint8_t col, uint8_t row, const char *string) { - if (type==NONE) return; - (static_cast(u8x8))->setFont(u8x8_font_chroma48medium8_r); - (static_cast(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))->setFont(font); - if (!ignoreLH && lineHeight==2) (static_cast(u8x8))->draw1x2Glyph(col, row, glyph); - else (static_cast(u8x8))->drawGlyph(col, row, glyph); - } - uint8_t getCols() { - if (type==NONE) return 0; - return (static_cast(u8x8))->getCols(); - } - void clear() { - if (type==NONE) return; - (static_cast(u8x8))->clear(); - } - void setPowerSave(uint8_t save) { - if (type==NONE) return; - (static_cast(u8x8))->setPowerSave(save); - } - - /** - * Redraw the screen (but only if things have changed - * or if forceRedraw). - */ - void redraw(bool forceRedraw) { - static bool showName = false; - unsigned long now = millis(); - - if (type==NONE) return; - if (overlayUntil > 0) { - if (now >= overlayUntil) { - // Time to display the overlay has elapsed. - overlayUntil = 0; - forceRedraw = true; - } else { - // We are still displaying the overlay - // Don't redraw. - return; - } - } - - // Check if values which are shown on display changed from the last time. - if (forceRedraw || - (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || - (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || - (knownBrightness != bri) || - (knownEffectSpeed != effectSpeed) || - (knownEffectIntensity != effectIntensity) || - (knownMode != strip.getMode()) || - (knownPalette != strip.getSegment(0).palette)) { - knownHour = 99; // force time update - clear(); - } else if (sleepMode && !displayTurnedOff && ((now - lastRedraw)/1000)%5 == 0) { - // change line every 5s - showName = !showName; - switch (lineType) { - case FLD_LINE_BRIGHTNESS: - lineType = FLD_LINE_EFFECT_SPEED; - break; - case FLD_LINE_MODE: - lineType = FLD_LINE_BRIGHTNESS; - break; - case FLD_LINE_PALETTE: - lineType = clockMode ? FLD_LINE_MODE : FLD_LINE_BRIGHTNESS; - break; - case FLD_LINE_EFFECT_SPEED: - lineType = FLD_LINE_EFFECT_INTENSITY; - break; - case FLD_LINE_EFFECT_INTENSITY: - lineType = FLD_LINE_PALETTE; - break; - default: - lineType = FLD_LINE_MODE; - break; - } - knownHour = 99; // force time update - } else { - // Nothing to change. - // Turn off display after 3 minutes with no change. - 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. - clear(); // force screen clear - sleepOrClock(true); - } else if (displayTurnedOff && clockMode) { - showTime(); - } - return; - } - - // do not update lastRedraw marker if just switching row contenet - if (((now - lastRedraw)/1000)%5 != 0) lastRedraw = now; - - // Turn the display back on - if (displayTurnedOff) sleepOrClock(false); - - // Update last known values. - knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); - knownIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); - knownBrightness = bri; - knownMode = strip.getMode(); - knownPalette = strip.getSegment(0).palette; - knownEffectSpeed = effectSpeed; - knownEffectIntensity = effectIntensity; - - // Do the actual drawing - - // First row with Wifi name - drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // home 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); // wifi icon - // Print password in AP mode and if led is OFF. - if (apActive && bri == 0) { - drawString(1, lineHeight, apPass); - } else { - // alternate IP address and server name - String secondLine = knownIp.toString(); - if (showName && strcmp(serverDescription, "WLED") != 0) { - secondLine = serverDescription; - } - for (uint8_t i=secondLine.length(); i 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 drawLine(uint8_t line, Line4Type lineType) { - char lineBuffer[LINE_BUFFER_SIZE]; - switch(lineType) { - case FLD_LINE_BRIGHTNESS: - sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri); - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_EFFECT_SPEED: - sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed); - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_EFFECT_INTENSITY: - sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity); - drawString(2, line*lineHeight, lineBuffer); - break; - case FLD_LINE_MODE: - showCurrentEffectOrPalette(knownMode, JSON_mode_names, line); - break; - case FLD_LINE_PALETTE: - showCurrentEffectOrPalette(knownPalette, JSON_palette_names, line); - break; - case FLD_LINE_TIME: - showTime(false); - break; - default: - // unknown type, do nothing - break; - } - } - - /** - * Display the current effect or palette (desiredEntry) - * on the appropriate line (row). - */ - 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; - - // 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 == '\0') break; - switch (singleJsonSymbol) { - case '"': - insideQuotes = !insideQuotes; - break; - case '[': - case ']': - break; - case ',': - qComma++; - default: - if (!insideQuotes || (qComma != knownMode)) break; - lineBuffer[printedChars++] = singleJsonSymbol; - } - 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); - } - - /** - * If there screen is off or in clock is displayed, - * this will return true. This allows us to throw away - * the first input from the rotary encoder but - * to wake up the screen. - */ - bool wakeDisplay() { - knownHour = 99; - if (displayTurnedOff) { - // Turn the display back on - sleepOrClock(false); - redraw(true); - return true; - } - return false; - } - - /** - * Allows you to show up to two lines as overlay for a - * period of time. - * Clears the screen and prints on the middle two lines. - */ - void overlay(const char* line1, const char *line2, long showHowLong) { - if (displayTurnedOff) { - // Turn the display back on - sleepOrClock(false); - } - - // Print the overlay - clear(); - if (line1) drawString(0, 1*lineHeight, line1); - if (line2) drawString(0, 2*lineHeight, line2); - overlayUntil = millis() + showHowLong; - } - - /** - * 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. - */ - void setMarkLine(byte newMarkLineNum) { - if (newMarkLineNum == 2 || newMarkLineNum == 3) { - markLineNum = newMarkLineNum; - } - else { - markLineNum = 0; - } - } - - /** - * Enable sleep (turn the display off) or clock mode. - */ - void sleepOrClock(bool enabled) { - if (enabled) { - if (clockMode) showTime(); - else setPowerSave(1); - displayTurnedOff = true; - } else { - setPowerSave(0); - displayTurnedOff = false; - } - } - - /** - * Display the current date and time in large characters - * on the middle rows. Based 24 or 12 hour depending on - * the useAMPM configuration. - */ - 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. - if (!fullScreen) return; - } else { - //if (fullScreen) clear(); - } - knownMinute = minuteCurrent; - knownHour = hourCurrent; - - 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*3, lineBuffer); - - byte showHour = hourCurrent; - boolean isAM = false; - if (useAMPM) { - if (showHour == 0) { - showHour = 12; - isAM = true; - } - else if (showHour > 12) { - showHour -= 12; - isAM = false; - } - else { - isAM = true; - } - } - - 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. - if (fullScreen) { - draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); - sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); - if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true); - else drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line - } else { - drawString(9+(useAMPM?0:2), lineHeight*3, lineBuffer); - if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*3, (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) { - //} - - /* - * 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) { - // 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. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will also not yet add your setting to one of the settings pages automatically. - * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * - * 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.")); - } - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * 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()) { - flip = top[FPSTR(_flip)].as(); - } else { - String str = top[FPSTR(_flip)]; // checkbox -> off or on - flip = (bool)(str!="off"); // off is guaranteed to be present - needsRedraw |= true; - } - contrast = top[FPSTR(_contrast)].as(); - refreshRate = top[FPSTR(_refreshRate)].as() * 1000; - screenTimeout = top[FPSTR(_screenTimeOut)].as() * 1000; - if (top[FPSTR(_sleepMode)].is()) { - sleepMode = top[FPSTR(_sleepMode)].as(); - } else { - String str = top[FPSTR(_sleepMode)]; // checkbox -> off or on - sleepMode = (bool)(str!="off"); // off is guaranteed to be present - needsRedraw |= true; - } - if (top[FPSTR(_clockMode)].is()) { - clockMode = top[FPSTR(_clockMode)].as(); - } else { - String str = top[FPSTR(_clockMode)]; // checkbox -> off or on - clockMode = (bool)(str!="off"); // off is guaranteed to be present - needsRedraw |= 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; - } else { - // changing paramters from settings page - if (sclPin!=newScl || sdaPin!=newSda || type!=newType) { - if (type != NONE) delete (static_cast(u8x8)); - pinManager.deallocatePin(sclPin); - pinManager.deallocatePin(sdaPin); - sclPin = newScl; - sdaPin = newSda; - if (newScl<0 || newSda<0) { - type = NONE; - return; - } else - type = newType; - setup(); - needsRedraw |= true; - } - setContrast(contrast); - setFlipMode(flip); - if (needsRedraw && !wakeDisplay()) redraw(true); - } - } - - /* - * 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_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"; +#pragma once + +#include "wled.h" +#include // from https://github.com/olikraus/u8g2/ + +// +// Insired by the v1 usermod: ssd1306_i2c_oled_u8g2 +// +// v2 usermod for using 128x32 or 128x64 i2c +// OLED displays to provide a four line display +// for WLED. +// +// Dependencies +// * This usermod REQURES the ModeSortUsermod +// * This Usermod works best, by far, when coupled +// with RotaryEncoderUIUsermod. +// +// Make sure to enable NTP and set your time zone in WLED Config | Time. +// +// REQUIREMENT: You must add the following requirements to +// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini +// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) +// REQUIREMENT: * Wire +// + +//The SCL and SDA pins are defined here. +#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 + #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 60*1000 // 1 min + +#define TIME_INDENT 0 +#define DATE_INDENT 2 + +// Minimum time between redrawing screen in ms +#define USER_LOOP_REFRESH_RATE_MS 1000 + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 + +typedef enum { + FLD_LINE_BRIGHTNESS = 0, + FLD_LINE_EFFECT_SPEED, + FLD_LINE_EFFECT_INTENSITY, + FLD_LINE_MODE, + FLD_LINE_PALETTE, + FLD_LINE_TIME +} 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 + SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C + SSD1305_64 // U8X8_SSD1305_128X64_ADAFRUIT_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 + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = ""; + IPAddress knownIp; + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + + bool displayTurnedOff = false; + unsigned long lastUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + Line4Type lineType = FLD_LINE_BRIGHTNESS; + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 0; + + // 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() { + 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 + lineHeight = 1; + 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 + lineHeight = 2; + 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 + lineHeight = 2; + break; + case SSD1305: + #ifdef ESP8266 + if (!(sclPin==5 && sdaPin==4)) + u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_NONAME_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA + lineHeight = 1; + break; + case SSD1305_64: + #ifdef ESP8266 + if (!(sclPin==5 && sdaPin==4)) + u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_SW_I2C(sclPin, sdaPin); // SCL, SDA, reset + else + #endif + u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(U8X8_PIN_NONE, sclPin, sdaPin); // Pins are Reset, SCL, SDA + lineHeight = 2; + break; + default: + u8x8 = nullptr; + type = NONE; + return; + } + (static_cast(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 + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + if (millis() - lastUpdate < (clockMode?1000:refreshRate) || strip.isUpdating()) return; + lastUpdate = millis(); + + redraw(false); + } + + /** + * Wrappers for screen drawing + */ + void setFlipMode(uint8_t mode) { + if (type==NONE) return; + (static_cast(u8x8))->setFlipMode(mode); + } + void setContrast(uint8_t contrast) { + if (type==NONE) return; + (static_cast(u8x8))->setContrast(contrast); + } + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) { + if (type==NONE) return; + (static_cast(u8x8))->setFont(u8x8_font_chroma48medium8_r); + if (!ignoreLH && lineHeight==2) (static_cast(u8x8))->draw1x2String(col, row, string); + else (static_cast(u8x8))->drawString(col, row, string); + } + void draw2x2String(uint8_t col, uint8_t row, const char *string) { + if (type==NONE) return; + (static_cast(u8x8))->setFont(u8x8_font_chroma48medium8_r); + (static_cast(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))->setFont(font); + if (!ignoreLH && lineHeight==2) (static_cast(u8x8))->draw1x2Glyph(col, row, glyph); + else (static_cast(u8x8))->drawGlyph(col, row, glyph); + } + uint8_t getCols() { + if (type==NONE) return 0; + return (static_cast(u8x8))->getCols(); + } + void clear() { + if (type==NONE) return; + (static_cast(u8x8))->clear(); + } + void setPowerSave(uint8_t save) { + if (type==NONE) return; + (static_cast(u8x8))->setPowerSave(save); + } + + /** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ + void redraw(bool forceRedraw) { + static bool showName = false; + unsigned long now = millis(); + + if (type==NONE) return; + if (overlayUntil > 0) { + if (now >= overlayUntil) { + // Time to display the overlay has elapsed. + overlayUntil = 0; + forceRedraw = true; + } else { + // We are still displaying the overlay + // Don't redraw. + return; + } + } + + // Check if values which are shown on display changed from the last time. + if (forceRedraw || + (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || + (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || + (knownBrightness != bri) || + (knownEffectSpeed != effectSpeed) || + (knownEffectIntensity != effectIntensity) || + (knownMode != strip.getMode()) || + (knownPalette != strip.getSegment(0).palette)) { + knownHour = 99; // force time update + clear(); + } else if (sleepMode && !displayTurnedOff && ((now - lastRedraw)/1000)%5 == 0) { + // change line every 5s + showName = !showName; + switch (lineType) { + case FLD_LINE_BRIGHTNESS: + lineType = FLD_LINE_EFFECT_SPEED; + break; + case FLD_LINE_MODE: + lineType = FLD_LINE_BRIGHTNESS; + break; + case FLD_LINE_PALETTE: + lineType = clockMode ? FLD_LINE_MODE : FLD_LINE_BRIGHTNESS; + break; + case FLD_LINE_EFFECT_SPEED: + lineType = FLD_LINE_EFFECT_INTENSITY; + break; + case FLD_LINE_EFFECT_INTENSITY: + lineType = FLD_LINE_PALETTE; + break; + default: + lineType = FLD_LINE_MODE; + break; + } + knownHour = 99; // force time update + } else { + // Nothing to change. + // Turn off display after 3 minutes with no change. + 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. + clear(); // force screen clear + sleepOrClock(true); + } else if (displayTurnedOff && clockMode) { + showTime(); + } + return; + } + + // do not update lastRedraw marker if just switching row contenet + if (((now - lastRedraw)/1000)%5 != 0) lastRedraw = now; + + // Turn the display back on + if (displayTurnedOff) sleepOrClock(false); + + // Update last known values. + knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); + knownIp = apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); + knownBrightness = bri; + knownMode = strip.getMode(); + knownPalette = strip.getSegment(0).palette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + + // Do the actual drawing + + // First row with Wifi name + drawGlyph(0, 0, 80, u8x8_font_open_iconic_embedded_1x1); // home 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); // wifi icon + // Print password in AP mode and if led is OFF. + if (apActive && bri == 0) { + drawString(1, lineHeight, apPass); + } else { + // alternate IP address and server name + String secondLine = knownIp.toString(); + if (showName && strcmp(serverDescription, "WLED") != 0) { + secondLine = serverDescription; + } + for (uint8_t i=secondLine.length(); i 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 drawLine(uint8_t line, Line4Type lineType) { + char lineBuffer[LINE_BUFFER_SIZE]; + switch(lineType) { + case FLD_LINE_BRIGHTNESS: + sprintf_P(lineBuffer, PSTR("Brightness %3d"), bri); + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_EFFECT_SPEED: + sprintf_P(lineBuffer, PSTR("FX Speed %3d"), effectSpeed); + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_EFFECT_INTENSITY: + sprintf_P(lineBuffer, PSTR("FX Intens. %3d"), effectIntensity); + drawString(2, line*lineHeight, lineBuffer); + break; + case FLD_LINE_MODE: + showCurrentEffectOrPalette(knownMode, JSON_mode_names, line); + break; + case FLD_LINE_PALETTE: + showCurrentEffectOrPalette(knownPalette, JSON_palette_names, line); + break; + case FLD_LINE_TIME: + showTime(false); + break; + default: + // unknown type, do nothing + break; + } + } + + /** + * Display the current effect or palette (desiredEntry) + * on the appropriate line (row). + */ + 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; + + // 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 == '\0') break; + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + break; + case '[': + case ']': + break; + case ',': + qComma++; + default: + if (!insideQuotes || (qComma != knownMode)) break; + lineBuffer[printedChars++] = singleJsonSymbol; + } + 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); + } + + /** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ + bool wakeDisplay() { + knownHour = 99; + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + redraw(true); + return true; + } + return false; + } + + /** + * Allows you to show up to two lines as overlay for a + * period of time. + * Clears the screen and prints on the middle two lines. + */ + void overlay(const char* line1, const char *line2, long showHowLong) { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + + // Print the overlay + clear(); + if (line1) drawString(0, 1*lineHeight, line1); + if (line2) drawString(0, 2*lineHeight, line2); + overlayUntil = millis() + showHowLong; + } + + /** + * 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. + */ + void setMarkLine(byte newMarkLineNum) { + if (newMarkLineNum == 2 || newMarkLineNum == 3) { + markLineNum = newMarkLineNum; + } + else { + markLineNum = 0; + } + } + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled) { + if (enabled) { + if (clockMode) showTime(); + else setPowerSave(1); + displayTurnedOff = true; + } else { + setPowerSave(0); + displayTurnedOff = false; + } + } + + /** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ + 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. + if (!fullScreen) return; + } else { + //if (fullScreen) clear(); + } + knownMinute = minuteCurrent; + knownHour = hourCurrent; + + 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*3, lineBuffer); + + byte showHour = hourCurrent; + boolean isAM = false; + if (useAMPM) { + if (showHour == 0) { + showHour = 12; + isAM = true; + } + else if (showHour > 12) { + showHour -= 12; + isAM = false; + } + else { + isAM = true; + } + } + + 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. + if (fullScreen) { + draw2x2String(TIME_INDENT+2, lineHeight*2, lineBuffer); + sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); + if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*2, (isAM ? "AM" : "PM"), true); + else drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line + } else { + drawString(9+(useAMPM?0:2), lineHeight*3, lineBuffer); + if (useAMPM) drawString(12+(fullScreen?0:2), lineHeight*3, (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) { + //} + + /* + * 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) { + // 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. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * 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.")); + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + bool readFromConfig(JsonObject& root) { + bool needsRedraw = false; + DisplayType newType = type; + int8_t newScl = sclPin; + int8_t newSda = sdaPin; + + bool configComplete = true; + + 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()) { + flip = top[FPSTR(_flip)].as(); + } else { + String str = top[FPSTR(_flip)]; // checkbox -> off or on + flip = (bool)(str!="off"); // off is guaranteed to be present + needsRedraw |= true; + } + contrast = top[FPSTR(_contrast)].as(); + refreshRate = top[FPSTR(_refreshRate)].as() * 1000; + screenTimeout = top[FPSTR(_screenTimeOut)].as() * 1000; + if (top[FPSTR(_sleepMode)].is()) { + sleepMode = top[FPSTR(_sleepMode)].as(); + } else { + String str = top[FPSTR(_sleepMode)]; // checkbox -> off or on + sleepMode = (bool)(str!="off"); // off is guaranteed to be present + needsRedraw |= true; + } + if (top[FPSTR(_clockMode)].is()) { + clockMode = top[FPSTR(_clockMode)].as(); + } else { + String str = top[FPSTR(_clockMode)]; // checkbox -> off or on + clockMode = (bool)(str!="off"); // off is guaranteed to be present + needsRedraw |= true; + } + DEBUG_PRINTLN(F("4 Line Display config (re)loaded.")); + } else { + DEBUG_PRINTLN(F("No config found. (Using defaults.)")); + configComplete = false; + } + + if (!initDone) { + // first run: reading from cfg.json + sclPin = newScl; + sdaPin = newSda; + type = newType; + } else { + // changing paramters from settings page + if (sclPin!=newScl || sdaPin!=newSda || type!=newType) { + if (type != NONE) delete (static_cast(u8x8)); + pinManager.deallocatePin(sclPin); + pinManager.deallocatePin(sdaPin); + sclPin = newScl; + sdaPin = newSda; + if (newScl<0 || newSda<0) { + type = NONE; + return configComplete; + } else + type = newType; + setup(); + needsRedraw |= true; + } + setContrast(contrast); + setFlipMode(flip); + if (needsRedraw && !wakeDisplay()) redraw(true); + } + return configComplete; + } + + /* + * 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_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"; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3cbf5ea1..2792bb20 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -60,6 +60,10 @@ #define DEFAULT_LED_TYPE TYPE_WS2812_RGB #endif +#if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES + #error "Max segments must be at least max number of busses!" +#endif + //do not call this method from system context (network callback) void WS2812FX::finalizeInit(void) { diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 3612a0bd..4b5674f1 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -405,7 +405,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { DEBUG_PRINTLN(F("Starting usermod config.")); JsonObject usermods_settings = doc["um"]; - if (!usermods_settings.isNull()) usermods.readFromConfig(usermods_settings); + if (!usermods_settings.isNull()) { + bool allComplete = usermods.readFromConfig(usermods_settings); + if (!allComplete && fromFS) serializeConfig(); + } if (fromFS) return false; doReboot = doc[F("rb")] | doReboot; @@ -413,11 +416,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } void deserializeConfigFromFS() { - bool fromeep = false; bool success = deserializeConfigSec(); if (!success) { //if file does not exist, try reading from EEPROM deEEPSettings(); - fromeep = true; + return; } DynamicJsonDocument doc(JSON_BUFFER_SIZE); @@ -426,7 +428,7 @@ void deserializeConfigFromFS() { success = readObjectFromFile("/cfg.json", nullptr, &doc); if (!success) { //if file does not exist, try reading from EEPROM - if (!fromeep) deEEPSettings(); + deEEPSettings(); return; } diff --git a/wled00/data/welcome.htm b/wled00/data/welcome.htm index 1b2bb60f..8d02e665 100644 --- a/wled00/data/welcome.htm +++ b/wled00/data/welcome.htm @@ -27,7 +27,6 @@ color: white; border: 0px solid white; border-radius: 25px; - filter: drop-shadow(0px 0px 1px #000); } img { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index c50e14d4..6269e25e 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -192,7 +192,7 @@ class Usermod { virtual void addToJsonInfo(JsonObject& obj) {} virtual void readFromJsonState(JsonObject& obj) {} virtual void addToConfig(JsonObject& obj) {} - virtual void readFromConfig(JsonObject& obj) {} + virtual bool readFromConfig(JsonObject& obj) { return true; } //Heads up! readFromConfig() now needs to return a bool virtual void onMqttConnect(bool sessionPresent) {} virtual bool onMqttMessage(char* topic, char* payload) { return false; } virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} @@ -211,7 +211,7 @@ class UsermodManager { void addToJsonInfo(JsonObject& obj); void readFromJsonState(JsonObject& obj); void addToConfig(JsonObject& obj); - void readFromConfig(JsonObject& obj); + bool readFromConfig(JsonObject& obj); void onMqttConnect(bool sessionPresent); bool onMqttMessage(char* topic, char* payload); bool add(Usermod* um); diff --git a/wled00/html_other.h b/wled00/html_other.h index 4a825137..362ba393 100644 --- a/wled00/html_other.h +++ b/wled00/html_other.h @@ -55,7 +55,7 @@ Please do not close or refresh the page :))====="; const char PAGE_welcome[] PROGMEM = R"=====(Welcome!

Welcome to WLED!

diff --git a/wled00/led.cpp b/wled00/led.cpp index 2031ef1a..a4d4f935 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -111,8 +111,7 @@ void colorUpdated(int callMode) { effectChanged = false; if (realtimeTimeout == UINT32_MAX) realtimeTimeout = 0; - if (isPreset) {isPreset = false;} - else {currentPreset = -1;} + currentPreset = -1; //something changed, so we are no longer in the preset notify(callMode); diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 568ff5fc..cdbd82a3 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -29,7 +29,6 @@ bool applyPreset(byte index) if (!errorFlag) { currentPreset = index; - isPreset = true; return true; } return false; diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index e516be17..040a8450 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -13,7 +13,13 @@ void UsermodManager::addToJsonState(JsonObject& obj) { for (byte i = 0; i < n void UsermodManager::addToJsonInfo(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } -void UsermodManager::readFromConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromConfig(obj); } +bool UsermodManager::readFromConfig(JsonObject& obj) { + bool allComplete = true; + for (byte i = 0; i < numMods; i++) { + if (!ums[i]->readFromConfig(obj)) allComplete = false; + } + return allComplete; +} 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; diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index e1586a23..21a7de8d 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -9,7 +9,8 @@ * || || || * \/ \/ \/ */ -//#include "usermod_v2_example.h" +//#include "../usermods/EXAMPLE_v2/usermod_v2_example.h" + #ifdef USERMOD_DALLASTEMPERATURE #include "../usermods/Temperature/usermod_temperature.h" #endif diff --git a/wled00/wled.h b/wled00/wled.h index 05ccc5a1..e97cdb97 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2106202 +#define VERSION 2106251 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -558,7 +558,6 @@ WLED_GLOBAL bool doCloseFile _INIT(false); // presets WLED_GLOBAL int16_t currentPreset _INIT(-1); -WLED_GLOBAL bool isPreset _INIT(false); WLED_GLOBAL byte errorFlag _INIT(0); diff --git a/wled00/wled_eeprom.cpp b/wled00/wled_eeprom.cpp index f1fd7b5a..bab8d305 100644 --- a/wled00/wled_eeprom.cpp +++ b/wled00/wled_eeprom.cpp @@ -479,5 +479,9 @@ void deEEPSettings() { loadSettingsFromEEPROM(); EEPROM.end(); + //call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving + JsonObject empty = JsonObject(); + usermods.readFromConfig(empty); + serializeConfig(); } \ No newline at end of file