Merge branch 'mdev' into ES8388

This commit is contained in:
Will Tatam
2023-04-03 19:20:41 +01:00
115 changed files with 8166 additions and 11703 deletions

View File

@@ -2,13 +2,12 @@
// #warning **** Included USERMOD_BH1750 ****
#ifndef WLED_ENABLE_MQTT
#error "This user mod requires MQTT to be enabled."
#warning "This user mod expects MQTT to be enabled."
#endif
#pragma once
#include <Arduino.h> // WLEDMM: make sure that I2C drivers have the "right" Wire Object
#include <Wire.h>
#include "wled.h"
#include <BH1750.h>
@@ -20,7 +19,8 @@
// the min frequency to check photoresistor, 500 ms
#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL
#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500
//#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500
#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 2500 // WLEDMM this makes more sense
#endif
// how many seconds after boot to take first measurement, 10 seconds
@@ -30,7 +30,7 @@
// only report if differance grater than offset value
#ifndef USERMOD_BH1750_OFFSET_VALUE
#define USERMOD_BH1750_OFFSET_VALUE 1
#define USERMOD_BH1750_OFFSET_VALUE 2 // WLEDMM this makes more sense
#endif
class Usermod_BH1750 : public Usermod
@@ -91,14 +91,17 @@ private:
// set up Home Assistant discovery entries
void _mqttInitialize()
{
#ifdef WLED_ENABLED_MQTT
mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness");
if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx"));
#endif
}
// Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.
void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)
{
#ifdef WLED_ENABLED_MQTT
String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config");
StaticJsonDocument<600> doc;
@@ -125,11 +128,13 @@ private:
DEBUG_PRINTLN(temp);
mqtt->publish(t.c_str(), 0, true, temp.c_str());
#endif
}
public:
void setup()
{
#if 0
bool HW_Pins_Used = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); // note whether architecture-based hardware SCL/SDA pins used
PinOwner po = PinOwner::UM_BH1750; // defaults to being pinowner for SCL/SDA pins
if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins
@@ -147,13 +152,18 @@ public:
#else
//Wire.begin(); // WLEDMM - i2c pins on 8266 are fixed.
#endif
#endif
if (!enabled) return;
if (!pinManager.joinWire()) { // WLEDMM - this allocates global I2C pins, then starts Wire - if not started previously
sensorFound = false;
enabled = false;
//enabled = false;
USER_PRINTLN(F("BH1750: failed to join I2C bus."));
return;
}
sensorFound = lightMeter.begin();
if (sensorFound) { USER_PRINTLN(F("BH1750 sensor found.")); }
else{ USER_PRINTLN(F("BH1750 sensor not found.")); }
initDone = true;
}
@@ -226,7 +236,7 @@ public:
lux_json.add(F(" sec until read"));
return;
} else {
lux_json.add(lastLux);
lux_json.add(round(lastLux)); // WLEDMM
lux_json.add(F(" lx"));
}
}
@@ -295,6 +305,7 @@ public:
} else {
DEBUG_PRINTLN(F(" config (re)loaded."));
// changing parameters from settings page
#if 0
bool pinsChanged = false;
for (byte i=0; i<2; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } // check if any pins changed
if (pinsChanged) { //if pins changed, deallocate old pins and allocate new ones
@@ -305,6 +316,9 @@ public:
for (byte i=0; i<2; i++) ioPin[i] = newPin[i];
setup();
}
#else
if (enabled && !sensorFound) setup();
#endif
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
return !top[F("pin")].isNull();
}

View File

@@ -20,15 +20,32 @@
* 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp
*/
/* WLEDMM: move usermod variables to class.
As of March 2023 this is work in progress, more variables will be moved in the future.
See Example v2, Temperature, MPU6050 and weather and fastled (rest to be done) as examples which has been converted using the steps below:
Part 1
- remove bool enabled = false/true (now default false)
- remove static const char _name[] and _enabled[]
- add constructor which calls superclass (temp?): XXXUsermod(const char *name, bool enabled):Usermod(name, enabled) {}
- replace _enabled with "enabled"
- remove const char PROGMEM init for _name[] and _enabled[]
Part 2
- Remove bool initDone = false;
- addToConfig: replace createNestedObject with Usermod::addToConfig(root); JsonObject top = root[FPSTR(_name)];
- readFromConfig: replace !top.isNull and enabled with bool configComplete = Usermod::readFromConfig(root);JsonObject top = root[FPSTR(_name)];
Part 3
- remove unsigned long lastTime = 0; //WLEDMM
*/
//class name. Use something descriptive and leave the ": public Usermod" part :)
class MyExampleUsermod : public Usermod {
private:
// Private class members. You can declare variables and functions only accessible to your usermod here
bool enabled = false;
bool initDone = false;
unsigned long lastTime = 0;
// set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)
bool testBool = false;
@@ -41,28 +58,25 @@ class MyExampleUsermod : public Usermod {
long testLong;
int8_t testPins[2];
// string that are used multiple time (this will save some flash memory)
static const char _name[];
static const char _enabled[];
// any private methods should go here (non-inline methosd should be defined out of class)
void publishMqtt(const char* state, bool retain = false); // example for publishing MQTT message
public:
MyExampleUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM
// non WLED related methods, may be used for data exchange between usermods (non-inline methods should be defined out of class)
/**
* Enable/Disable the usermod
*/
inline void enable(bool enable) { enabled = enable; }
// inline void enable(bool enable) { enabled = enable; }
/**
* Get usermod enabled/disabled state
*/
inline bool isEnabled() { return enabled; }
// inline bool isEnabled() { return enabled; }
// in such case add the following to another usermod:
// in private vars:
@@ -222,8 +236,8 @@ class MyExampleUsermod : public Usermod {
*/
void addToConfig(JsonObject& root)
{
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
Usermod::addToConfig(root); JsonObject top = root[FPSTR(_name)]; //WLEDMM
//save these vars persistently whenever settings are saved
top["great"] = userVar0;
top["testBool"] = testBool;
@@ -258,9 +272,7 @@ class MyExampleUsermod : public Usermod {
// default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor
// setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)
JsonObject top = root[FPSTR(_name)];
bool configComplete = !top.isNull();
bool configComplete = Usermod::readFromConfig(root);JsonObject top = root[FPSTR(_name)]; //WLEDMM
configComplete &= getJsonValue(top["great"], userVar0);
configComplete &= getJsonValue(top["testBool"], testBool);
@@ -386,8 +398,6 @@ class MyExampleUsermod : public Usermod {
// add more strings here to reduce flash memory usage
const char MyExampleUsermod::_name[] PROGMEM = "ExampleUsermod";
const char MyExampleUsermod::_enabled[] PROGMEM = "enabled";
// implementation of non-inline member methods

View File

@@ -371,6 +371,7 @@ public:
* onStateChanged() is used to detect WLED state change
*/
void onStateChange(uint8_t mode) {
if (!initDone) return;
DEBUG_PRINT(F("PIR: offTimerStart=")); DEBUG_PRINTLN(offTimerStart);
if (PIRtriggered && offTimerStart) {
// checking PIRtriggered and offTimerStart will prevent cancellation upon On trigger

View File

@@ -6,6 +6,8 @@
#include "src/dependencies/time/DS1307RTC.h"
#include "wled.h"
#define RTC_DELTA 2 // only modify RTC time if delta exceeds this number of seconds
//Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL))
class RTCUsermod : public Usermod {
@@ -36,6 +38,7 @@ class RTCUsermod : public Usermod {
if (rtcTime) {
toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC);
updateLocalTime();
USER_PRINTLN(F("Localtime updated from RTC."));
} else {
if (!RTC.chipPresent()) disabled = true; //don't waste time if H/W error
}
@@ -45,7 +48,24 @@ class RTCUsermod : public Usermod {
if (strip.isUpdating()) return;
if (!disabled && toki.isTick()) {
time_t t = toki.second();
if (abs(t - RTC.get())> 2) RTC.set(t); //set RTC to NTP/UI-provided value - WLEDMM allow up to 3 sec deviation
if (abs(t - RTC.get())> RTC_DELTA) { // WLEDMM only consider time diffs > 2 seconds
if ( (toki.getTimeSource() == TOKI_TS_NTP)
||( (toki.getTimeSource() != TOKI_TS_NONE) && (toki.getTimeSource() != TOKI_TS_RTC)
&& (toki.getTimeSource() != TOKI_TS_BAD) && (toki.getTimeSource() != TOKI_TS_UDP_SEC) && (toki.getTimeSource() != TOKI_TS_UDP)))
{ // WLEMM update RTC if we have a reliable time source
RTC.set(t); //set RTC to NTP/UI-provided value - WLEDMM allow up to 3 sec deviation
USER_PRINTLN(F("RTC updated using localtime."));
} else {
// WLEDMM if no reliable time -> update from RTC
time_t rtcTime = RTC.get();
if (rtcTime) {
toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC);
updateLocalTime();
USER_PRINTLN(F("Localtime updated from RTC."));
}
}
}
}
}

View File

@@ -21,7 +21,6 @@ class UsermodTemperature : public Usermod {
private:
bool initDone = false;
OneWire *oneWire;
// GPIO pin used for sensor (with a default compile-time fallback)
int8_t temperaturePin = TEMPERATURE_PIN;
@@ -45,13 +44,9 @@ class UsermodTemperature : public Usermod {
// temperature if flashed to a board without a sensor attached
byte sensorFound;
bool enabled = true;
bool HApublished = false;
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _enabled[];
static const char _readInterval[];
static const char _parasite[];
static const char _parasitePin[];
@@ -163,6 +158,7 @@ class UsermodTemperature : public Usermod {
#endif
public:
UsermodTemperature(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class
void setup() {
int retries = 10;
@@ -325,9 +321,10 @@ class UsermodTemperature : public Usermod {
* addToConfig() (called from set.cpp) stores persistent properties to cfg.json
*/
void addToConfig(JsonObject &root) {
Usermod::addToConfig(root);
JsonObject top = root[FPSTR(_name)];
// we add JSON object: {"Temperature": {"pin": 0, "degC": true}}
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
top[FPSTR(_enabled)] = enabled;
top["pin"] = temperaturePin; // usermodparam
top["degC"] = degC; // usermodparam
top[FPSTR(_readInterval)] = readingInterval / 1000;
@@ -342,17 +339,18 @@ class UsermodTemperature : public Usermod {
* The function should return true if configuration was successfully loaded or false if there was no configuration.
*/
bool readFromConfig(JsonObject &root) {
bool configComplete = Usermod::readFromConfig(root);
JsonObject top = root[FPSTR(_name)];
// we look for JSON object: {"Temperature": {"pin": 0, "degC": true}}
int8_t newTemperaturePin = temperaturePin;
DEBUG_PRINT(FPSTR(_name));
JsonObject top = root[FPSTR(_name)];
if (top.isNull()) {
DEBUG_PRINTLN(F(": No config found. (Using defaults.)"));
return false;
}
enabled = top[FPSTR(_enabled)] | enabled;
newTemperaturePin = top["pin"] | newTemperaturePin;
degC = top["degC"] | degC;
readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000;
@@ -398,8 +396,6 @@ class UsermodTemperature : public Usermod {
};
// strings to reduce flash memory usage (used more than twice)
const char UsermodTemperature::_name[] PROGMEM = "Temperature";
const char UsermodTemperature::_enabled[] PROGMEM = "enabled";
const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s";
const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr";
const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin";

View File

@@ -93,22 +93,23 @@ function populateCEEditor(name, segID)
{
fetchAndExecute((loc?`http://${locip}`:'.') + "/", name + ".wled", function(text)
{
var cn=`Custom Effects Editor<br>
var cn=`ARTI-FX Editor<br>
<i>${name}.wled</i><br>
<textarea class="ceTextarea" id="ceProgramArea">${text}</textarea><br>
<button class="btn infobtn" onclick="toggleCEEditor()">Close</button>
<button class="btn infobtn" onclick="saveCE('${name}.wled', ${segID})">Save and Run</button><br>
<button class="btn infobtn" onclick="downloadCEFile('CE','${name}.wled')">Download ${name}.wled</button>
<button class="btn infobtn" onclick="downloadGHFile('CE','${name}.wled')">Download ${name}.wled</button>
<button class="btn infobtn" onclick="loadCETemplate('${name}')">Load template</button><br>
<button class="btn infobtn" onclick="downloadCEFile('CE','wledv033.json')">Download wled json</button>
<button class="btn infobtn" onclick="downloadCEFile('CE','presets.json')">Download presets.json</button><br>
<button class="btn infobtn" onclick="location.href='https://github.com/MoonModules/WLED-Effects/tree/master/CustomEffects/wled'" type="button">Custom Effects Library</button>
<button class="btn infobtn btn-xs" onclick="location.href='https://mm.kno.wled.ge/moonmodules/custom-effects'" type="button">?</button><br>
<button class="btn infobtn" onclick="downloadGHFile('CE','wledv033.json',true,true)">Download wled json</button>
<button class="btn infobtn" onclick="downloadGHFile('CE','presets.json',true,true)">Download presets.json</button><br>
<button class="btn infobtn" onclick="location.href='https://github.com/MoonModules/WLED-Effects/tree/master/ARTIFX/wled'" type="button">ARTI-FX Library</button>
<button class="btn infobtn btn-xs" onclick="location.href='https://mm.kno.wled.ge/moonmodules/arti-fx'" type="button">?</button><br>
<br><i>Compile and Run Log</i><br>
<textarea class="ceTextarea" id="ceLogArea"></textarea><br>
<i>Run log > 3 seconds is send to Serial Ouput.</i><br>
<a href="#" onclick="downloadCEFile('HBB','presets.json');return false;" title="Download HBaas Base presets">🥚</a>
<a href="#" onclick="downloadCEFile('HBE','presets.json');return false;" title="Download HBaas Effects presets">🥚</a>`;
<a href="#" onclick="downloadGHFile('HBB','presets.json',true,true);return false;" title="Download HBaas Base presets">🥚</a>
<a href="#" onclick="downloadGHFile('HBE','presets.json',true,true);return false;" title="Download HBaas Effects presets">🥚</a>
<a href="#" onclick="downloadGHFile('LM','presets.json',true,true);return false;" title="Download Ledmap presets">🥚</a>`;
d.getElementById('kceEditor').innerHTML = cn;
@@ -122,14 +123,15 @@ function populateCEEditor(name, segID)
});
}
function downloadCEFile(url, name) {
if (url == "CE") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/CustomEffects/wled/";
function downloadGHFile(url, name, save=false, warn=false) { //Githubfile
if (url == "CE") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/ARTIFX/wled/";
if (url == "HBB") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/Presets/HB_PresetPack210808_32x32_16seg/Base%20pack/";
if (url == "HBE") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/Presets/HB_PresetPack210808_32x32_16seg/Effects%20pack/";
if (url == "LM") url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/Ledmaps/";
fetchAndExecute(url, name, function(text) {
if (name == "wledv033.json" || name == "presets.json") {
if (!confirm('Are you sure to download/overwrite ' + name + '?'))
if (save) {
if (warn && !confirm('Are you sure to download/overwrite ' + name + '?'))
return;
uploadFileWithText("/" + name, text);
}
@@ -140,7 +142,7 @@ function downloadCEFile(url, name) {
}
}, function(error){
showToast(error);
console.log(error);
console.log(url + name,error);
});
return;
@@ -165,7 +167,7 @@ function downloadCEFile(url, name) {
function loadCETemplate(name) {
var ceProgramArea = d.getElementById("ceProgramArea");
ceProgramArea.value = `/*
Custom Effects Template
ARTIFX Template
*/
program ${name}
{

View File

@@ -1,6 +1,6 @@
/*
@title Usermod Custom Effects (CE)
@file usermod_v2_customeffects.h
@title Usermod ARTIFX (AF)
@file usermod_v2_artifx.h
@date 20220818
@author Ewoud Wijma
@Copyright (c) 2023 Ewoud Wijma
@@ -17,7 +17,7 @@
ARTI * arti;
//effect function
uint16_t mode_customEffect(void) {
uint16_t mode_ARTIFX(void) {
//tbd: move statics to SEGMENT.data
static bool succesful;
static bool notEnoughHeap;
@@ -99,9 +99,9 @@ uint16_t mode_customEffect(void) {
return FRAMETIME;
}
static const char _data_FX_MODE_CUSTOMEFFECT[] PROGMEM = "⚙️ Custom Effect ☾@Speed,Intensity,Custom 1, Custom 2, Custom 3;!;!;1;mp12=0";
static const char _data_FX_MODE_ARTIFX[] PROGMEM = "⚙️ ARTI-FX ☾@Speed,Intensity,Custom 1, Custom 2, Custom 3;!;!;1;mp12=0";
class CustomEffectsUserMod : public Usermod {
class ARTIFXUserMod : public Usermod {
private:
// strings to reduce flash memory usage (used more than twice)
static const char _name[]; //usermod name
@@ -115,7 +115,7 @@ class CustomEffectsUserMod : public Usermod {
void setup() {
if (!initDone)
strip.addEffect(FX_MODE_CUSTOMEFFECT, &mode_customEffect, _data_FX_MODE_CUSTOMEFFECT);
strip.addEffect(FX_MODE_ARTIFX, &mode_ARTIFX, _data_FX_MODE_ARTIFX);
initDone = true;
enabled = true;
}
@@ -200,9 +200,9 @@ class CustomEffectsUserMod : public Usermod {
*/
uint16_t getId()
{
return USERMOD_ID_CUSTOMEFFECTS;
return USERMOD_ID_ARTIFX;
}
};
// strings to reduce flash memory usage (used more than twice)
const char CustomEffectsUserMod::_name[] PROGMEM = "CustomEffects";
const char ARTIFXUserMod::_name[] PROGMEM = "ARTIFX";

View File

@@ -29,9 +29,9 @@
// #define SR_DEBUG // generic SR DEBUG messages
#ifdef SR_DEBUG
#define DEBUGSR_PRINT(x) DEBUGOUT.print(x)
#define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x)
#define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x)
#define DEBUGSR_PRINT(x) DEBUGOUT(x)
#define DEBUGSR_PRINTLN(x) DEBUGOUTLN(x)
#define DEBUGSR_PRINTF(x...) DEBUGOUTF(x)
#else
#define DEBUGSR_PRINT(x)
#define DEBUGSR_PRINTLN(x)
@@ -55,10 +55,10 @@
#endif
#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG)
#define PLOT_PRINT(x) DEBUGOUT.print(x)
#define PLOT_PRINTLN(x) DEBUGOUT.println(x)
#define PLOT_PRINTF(x...) DEBUGOUT.printf(x)
#define PLOT_FLUSH() DEBUGOUT.flush()
#define PLOT_PRINT(x) DEBUGOUT(x)
#define PLOT_PRINTLN(x) DEBUGOUTLN(x)
#define PLOT_PRINTF(x...) DEBUGOUTF(x)
#define PLOT_FLUSH() DEBUGOUTFlush()
#else
#define PLOT_PRINT(x)
#define PLOT_PRINTLN(x)
@@ -123,6 +123,15 @@ static AudioSource *audioSource = nullptr;
static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks.
static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT.
//WLEDMM add experimental settings
static uint8_t micLevelMethod = 0; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you)
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
static uint8_t averageByRMS = false; // false: use mean value, true: use RMS (root mean squared). use simpler method on slower MCUs.
#else
static uint8_t averageByRMS = true; // false: use mean value, true: use RMS (root mean squared). use better method on fast MCUs.
#endif
static uint8_t freqDist = 0; // 0=old 1=rightshift mode
// audioreactive variables shared with FFT task
static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point
static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier
@@ -136,7 +145,7 @@ static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending
// peak detection
static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay()
static uint8_t maxVol = 10; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated)
static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated)
static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated)
static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData
static unsigned long timeOfPeak = 0; // time of last sample peak detection.
@@ -182,7 +191,7 @@ static const float fftResultPink[MAX_PINK+1][NUM_GEQ_CHANNELS] = {
{ 12.0f, 6.60f, 2.60f, 1.15f, 1.35f, 2.05f, 2.85f, 2.50f, 2.85f, 3.30f, 2.25f, 4.35f, 3.80f, 3.75f, 6.50f, 9.00f}, // 4 IMNP441 - voice, or small speaker
{ 2.75f, 1.60f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 1.75f, 2.55f, 3.60f }, // 5 ICS-43434 datasheet response * pink noise
{ 2.25f, 1.20f, 1.00f, 1.20f, 1.80f, 3.20f, 5.10f, 5.50f, 4.00f, 4.80f, 6.70f, 6.40f, 5.80f, 3.90f, 6.00f, 5.10f }, // 6 ICS-43434 - big speaker, strong bass
{ 2.90f, 1.25f, 0.75f, 1.08f, 2.35f, 3.55f, 3.60f, 3.40f, 2.75f, 3.45f, 4.40f, 6.35f, 6.80f, 6.80f, 8.50f,10.64f }, // 6 ICS-43434 - big speaker, strong bass
{ 1.65f, 1.00f, 1.05f, 1.30f, 1.48f, 1.30f, 1.80f, 3.00f, 1.50f, 1.65f, 2.56f, 3.00f, 2.60f, 2.30f, 5.00f, 3.00f }, // 7 SPM1423
{ 2.25f, 1.60f, 1.30f, 1.60f, 2.20f, 3.20f, 3.06f, 2.60f, 2.85f, 3.50f, 4.10f, 4.80f, 5.70f, 6.05f,10.50f,14.85f }, // 8 userdef #1 for ewowi (enhance median/high freqs)
@@ -264,7 +273,8 @@ constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - Thi
constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information.
// the following are observed values, supported by a bit of "educated guessing"
//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels
#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels
//#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels
#define FFT_DOWNSCALE 0.40f // downscaling factor for FFT results, RMS averaging
#define LOG_256 5.54517744f // log(256)
// These are the input and output vectors. Input vectors receive computed results from FFT.
@@ -303,23 +313,28 @@ static float mapf(float x, float in_min, float in_max, float out_min, float out_
}
// compute average of several FFT resut bins
#if 1 // linear average
static float fftAddAvg(int from, int to) {
// linear average
static float fftAddAvgLin(int from, int to) {
float result = 0.0f;
for (int i = from; i <= to; i++) {
result += vReal[i];
}
return result / float(to - from + 1);
}
#else // RMS average
static float fftAddAvg(int from, int to) {
// RMS average
static float fftAddAvgRMS(int from, int to) {
double result = 0.0;
for (int i = from; i <= to; i++) {
result += vReal[i] * vReal[i];
}
return sqrtf(result / float(to - from + 1));
}
#endif
static float fftAddAvg(int from, int to) {
if (from == to) return vReal[from]; // small optimization
if (averageByRMS) return fftAddAvgRMS(from, to); // use SMS
else return fftAddAvgLin(from, to); // use linear average
}
#if defined(CONFIG_IDF_TARGET_ESP32C3)
constexpr bool skipSecondFFT = true;
@@ -512,35 +527,67 @@ void FFTcode(void * parameter)
fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900
fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate
#else
/* new mapping, optimized for 22050 Hz by softhack007 */
//WLEDMM: different distributions
if (freqDist == 0) {
/* new mapping, optimized for 22050 Hz by softhack007 --- update: removed overlap */
// bins frequency range
if (useBandPassFilter) {
// skip frequencies below 100hz
fftCalc[ 0] = 0.8f * fftAddAvg(3,4);
fftCalc[ 1] = 0.9f * fftAddAvg(4,5);
fftCalc[ 2] = fftAddAvg(5,6);
fftCalc[ 3] = fftAddAvg(6,7);
fftCalc[ 0] = 0.8f * fftAddAvg(3,3);
fftCalc[ 1] = 0.9f * fftAddAvg(4,4);
fftCalc[ 2] = fftAddAvg(5,5);
fftCalc[ 3] = fftAddAvg(6,6);
// don't use the last bins from 206 to 255.
fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping
} else {
fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass
fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass
fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass
fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange
fftCalc[ 0] = fftAddAvg(1,1); // 1 43 - 86 sub-bass
fftCalc[ 1] = fftAddAvg(2,2); // 1 86 - 129 bass
fftCalc[ 2] = fftAddAvg(3,4); // 2 129 - 216 bass
fftCalc[ 3] = fftAddAvg(5,6); // 2 216 - 301 bass + midrange
// don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise)
fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping
}
fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange
fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange
fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange
fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center !
fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange
fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange
fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid
fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid
fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid
fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid
fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping
fftCalc[ 4] = fftAddAvg(7,9); // 3 301 - 430 midrange
fftCalc[ 5] = fftAddAvg(10,12); // 3 430 - 560 midrange
fftCalc[ 6] = fftAddAvg(13,18); // 5 560 - 818 midrange
fftCalc[ 7] = fftAddAvg(19,25); // 7 818 - 1120 midrange -- 1Khz should always be the center !
fftCalc[ 8] = fftAddAvg(26,32); // 7 1120 - 1421 midrange
fftCalc[ 9] = fftAddAvg(33,43); // 9 1421 - 1895 midrange
fftCalc[10] = fftAddAvg(44,55); // 12 1895 - 2412 midrange + high mid
fftCalc[11] = fftAddAvg(56,69); // 14 2412 - 3015 high mid
fftCalc[12] = fftAddAvg(70,85); // 16 3015 - 3704 high mid
fftCalc[13] = fftAddAvg(86,103); // 18 3704 - 4479 high mid
fftCalc[14] = fftAddAvg(104,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping
}
else if (freqDist == 1) { //WLEDMM: Rightshft: note ewowi: frequencies in comments are not correct
if (useBandPassFilter) {
// skip frequencies below 100hz
fftCalc[ 0] = 0.8f * fftAddAvg(1,1);
fftCalc[ 1] = 0.9f * fftAddAvg(2,2);
fftCalc[ 2] = fftAddAvg(3,3);
fftCalc[ 3] = fftAddAvg(4,4);
// don't use the last bins from 206 to 255.
fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping
} else {
fftCalc[ 0] = fftAddAvg(1,1); // 1 43 - 86 sub-bass
fftCalc[ 1] = fftAddAvg(2,2); // 1 86 - 129 bass
fftCalc[ 2] = fftAddAvg(3,3); // 2 129 - 216 bass
fftCalc[ 3] = fftAddAvg(4,4); // 2 216 - 301 bass + midrange
// don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise)
fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping
}
fftCalc[ 4] = fftAddAvg(5,6); // 3 301 - 430 midrange
fftCalc[ 5] = fftAddAvg(7,8); // 3 430 - 560 midrange
fftCalc[ 6] = fftAddAvg(9,10); // 5 560 - 818 midrange
fftCalc[ 7] = fftAddAvg(11,13); // 7 818 - 1120 midrange -- 1Khz should always be the center !
fftCalc[ 8] = fftAddAvg(14,18); // 7 1120 - 1421 midrange
fftCalc[ 9] = fftAddAvg(19,25); // 9 1421 - 1895 midrange
fftCalc[10] = fftAddAvg(26,36); // 12 1895 - 2412 midrange + high mid
fftCalc[11] = fftAddAvg(37,45); // 14 2412 - 3015 high mid
fftCalc[12] = fftAddAvg(46,66); // 16 3015 - 3704 high mid
fftCalc[13] = fftAddAvg(67,97); // 18 3704 - 4479 high mid
fftCalc[14] = fftAddAvg(98,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping
}
#endif
} else { // noise gate closed - just decay old values
isFirstRun = false;
@@ -861,7 +908,9 @@ class AudioReactive : public Usermod {
static const char _name[];
static const char _enabled[];
static const char _inputLvl[];
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
static const char _analogmic[];
#endif
static const char _digitalmic[];
static const char UDP_SYNC_HEADER[];
static const char UDP_SYNC_HEADER_v1[];
@@ -1057,6 +1106,10 @@ class AudioReactive : public Usermod {
const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release.
const float weighting2 = 0.073f; // Exponential filter weighting, for rising signal (a bit more robust against spikes)
const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function
static bool isFrozen = false;
static bool haveSilence = true;
static unsigned long lastSoundTime = 0; // for delaying un-freeze
static unsigned long startuptime = 0; // "fast freeze" mode: do not interfere during first 12 seconds (filter startup time)
#ifdef WLED_DISABLE_SOUND
micIn = inoise8(millis(), millis()); // Simulated analog read
@@ -1079,8 +1132,15 @@ class AudioReactive : public Usermod {
#endif
#endif
micLev += (micDataReal-micLev) / 12288.0f;
if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal
if (startuptime == 0) startuptime = millis(); // fast freeze mode - remember filter startup time
if ((micLevelMethod < 1) || !isFrozen) { // following the input level, UNLESS mic Level was frozen
micLev += (micDataReal-micLev) / 12288.0f;
}
if(micDataReal < (micLev-0.24)) { // MicLev above input signal:
micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // always align MicLev to lowest input signal
if (!haveSilence) isFrozen = true; // freeze mode: freeze micLevel so it cannot rise again
}
micIn -= micLev; // Let's center it to 0 now
// Using an exponential filter to smooth out the signal. We'll add controls for this in a future release.
@@ -1093,10 +1153,26 @@ class AudioReactive : public Usermod {
expAdjF = fabsf(expAdjF); // Now (!) take the absolute value
if ((micLevelMethod == 2) && !haveSilence && (expAdjF >= (1.5f * float(soundSquelch))))
isFrozen = true; // fast freeze mode: freeze micLevel once the volume rises 50% above squelch
//expAdjF = (micInNoDC <= soundSquelch) ? 0: expAdjF; // simple noise gate - experimental
expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate
if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when "squelch = 0"
if (expAdjF <= 0.5f)
haveSilence = true;
else {
lastSoundTime = millis();
haveSilence = false;
}
// un-freeze micLev
if (micLevelMethod == 0) isFrozen = false;
if ((micLevelMethod == 1) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 4000)) isFrozen = false; // normal freeze: 4 seconds silence needed
if ((micLevelMethod == 2) && isFrozen && haveSilence && ((millis() - lastSoundTime) > 6000)) isFrozen = false; // fast freeze: 6 seconds silence needed
if ((micLevelMethod == 2) && (millis() - startuptime < 12000)) isFrozen = false; // fast freeze: no freeze in first 12 seconds (filter startup phase)
tmpSample = expAdjF;
micIn = abs(micIn); // And get the absolute value of each sample
@@ -1360,6 +1436,7 @@ class AudioReactive : public Usermod {
case 0: //ADC analog
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
case 5: //PDM Microphone
case 51: //legacy PDM Microphone
#endif
#endif
case 1:
@@ -1397,6 +1474,13 @@ class AudioReactive : public Usermod {
delay(100);
if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin);
break;
case 51:
DEBUGSR_PRINT(F("AR: Legacy PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));
audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f);
useBandPassFilter = true;
delay(100);
if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin);
break;
#endif
case 6:
DEBUGSR_PRINTLN(F("AR: ES8388 Source"));
@@ -1537,7 +1621,7 @@ class AudioReactive : public Usermod {
#if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS)
// complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second.
if ((userloopDelay > /*23*/ 30) && !disableSoundProcessing && (audioSyncEnabled == 0)) {
if ((userloopDelay > /*23*/ 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) {
USER_PRINTF("[AR userLoop] hickup detected -> was inactive for last %d millis!\n", userloopDelay);
}
#endif
@@ -1673,14 +1757,15 @@ class AudioReactive : public Usermod {
connected(); // resume UDP
} else
// xTaskCreatePinnedToCore(
xTaskCreate( // no need to "pin" this task to core #0
// xTaskCreate( // no need to "pin" this task to core #0
xTaskCreateUniversal(
FFTcode, // Function to implement the task
"FFT", // Name of the task
5000, // Stack size in words
NULL, // Task input parameter
1, // Priority of the task
&FFT_Task // Task handle
// , 0 // Core where the task should run
, 0 // Core where the task should run
);
}
micDataReal = 0.0f; // just to be sure
@@ -1780,7 +1865,10 @@ class AudioReactive : public Usermod {
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
infoArr.add(F("ADC analog"));
} else {
infoArr.add(F("I2S digital"));
if (dmType != 51)
infoArr.add(F("I2S digital"));
else
infoArr.add(F("legacy I2S PDM"));
}
// input level or "silence"
if (maxSample5sec > 1.0) {
@@ -1793,7 +1881,7 @@ class AudioReactive : public Usermod {
} else {
// error during audio source setup
infoArr.add(F("not initialized"));
infoArr.add(F(" - check GPIO config"));
infoArr.add(F(" - check pin settings"));
}
}
@@ -1957,6 +2045,12 @@ class AudioReactive : public Usermod {
cfg[F("gain")] = sampleGain;
cfg[F("AGC")] = soundAgc;
//WLEDMM: experimental settings
JsonObject poweruser = top.createNestedObject("experiments");
poweruser[F("micLev")] = micLevelMethod;
poweruser[F("freqDist")] = freqDist;
poweruser[F("freqRMS")] = averageByRMS;
JsonObject dynLim = top.createNestedObject("dynamics");
dynLim[F("limiter")] = limiterOn;
dynLim[F("rise")] = attackTime;
@@ -2005,7 +2099,11 @@ class AudioReactive : public Usermod {
if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)
if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM
if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM
#endif
#else
if (dmType == 5) useBandPassFilter = true; // enable filter for PDM
if (dmType == 51) useBandPassFilter = true /*false*/; // switch on filter for legacy PDM
#endif
configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin);
@@ -2019,6 +2117,11 @@ class AudioReactive : public Usermod {
configComplete &= getJsonValue(top["config"][F("gain")], sampleGain);
configComplete &= getJsonValue(top["config"][F("AGC")], soundAgc);
//WLEDMM: experimental settings
configComplete &= getJsonValue(top["experiments"][F("micLev")], micLevelMethod);
configComplete &= getJsonValue(top["experiments"][F("freqDist")], freqDist);
configComplete &= getJsonValue(top["experiments"][F("freqRMS")], averageByRMS);
configComplete &= getJsonValue(top["dynamics"][F("limiter")], limiterOn);
configComplete &= getJsonValue(top["dynamics"][F("rise")], attackTime);
configComplete &= getJsonValue(top["dynamics"][F("fall")], decayTime);
@@ -2038,10 +2141,12 @@ class AudioReactive : public Usermod {
oappend(SET_F("addInfo('AudioReactive:help',0,'<button onclick=\"location.href=&quot;https://mm.kno.wled.ge/soundreactive/Sound-Settings&quot;\" type=\"button\">?</button>');"));
//WLEDMM: add defaults
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // -S3/-S2/-C3 don't support analog audio
#ifdef AUDIOPIN
oappend(SET_F("xOpt('AudioReactive:analogmic:pin',1,' ⎌',")); oappendi(AUDIOPIN); oappend(");");
#endif
oappend(SET_F("aOpt('AudioReactive:analogmic:pin',1);")); //only analog options
#endif
oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');"));
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
@@ -2071,12 +2176,17 @@ class AudioReactive : public Usermod {
#else
oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);"));
#endif
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
#if SR_DMTYPE==5
oappend(SET_F("addOption(dd,'Generic I2S PDM (⎌)',5);"));
#else
oappend(SET_F("addOption(dd,'Generic I2S PDM',5);"));
#endif
#if SR_DMTYPE==51
oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾ (⎌)',51);"));
#else
oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾',51);"));
#endif
#endif
oappend(SET_F("addOption(dd,'ES8388',6);"));
@@ -2093,6 +2203,23 @@ class AudioReactive : public Usermod {
oappend(SET_F("addOption(dd,'Vivid',2);"));
oappend(SET_F("addOption(dd,'Lazy',3);"));
//WLEDMM: experimental settings
oappend(SET_F("dd=addDropdown('AudioReactive','experiments:micLev');"));
oappend(SET_F("addOption(dd,'Floating (⎌)',0);"));
oappend(SET_F("addOption(dd,'Freeze',1);"));
oappend(SET_F("addOption(dd,'Fast Freeze',2);"));
oappend(SET_F("addInfo('AudioReactive:experiments:micLev',1,'☾');"));
oappend(SET_F("dd=addDropdown('AudioReactive','experiments:freqDist');"));
oappend(SET_F("addOption(dd,'Normal (⎌)',0);"));
oappend(SET_F("addOption(dd,'RightShift',1);"));
oappend(SET_F("addInfo('AudioReactive:experiments:freqDist',1,'☾');"));
oappend(SET_F("dd=addDropdown('AudioReactive','experiments:freqRMS');"));
oappend(SET_F("addOption(dd,'Off (⎌)',0);"));
oappend(SET_F("addOption(dd,'On',1);"));
oappend(SET_F("addInfo('AudioReactive:experiments:freqRMS',1,'☾');"));
oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');"));
oappend(SET_F("addOption(dd,'Off',0);"));
oappend(SET_F("addOption(dd,'On',1);"));
@@ -2163,6 +2290,7 @@ class AudioReactive : public Usermod {
#else
oappend(SET_F("addOption(dd,'userdefined #2',9);"));
#endif
oappend(SET_F("addInfo('AudioReactive:frequency:profile',1,'☾');"));
oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');"));
oappend(SET_F("addOption(dd,'Off',0);"));
@@ -2237,7 +2365,9 @@ class AudioReactive : public Usermod {
const char AudioReactive::_name[] PROGMEM = "AudioReactive";
const char AudioReactive::_enabled[] PROGMEM = "enabled";
const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel";
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)
const char AudioReactive::_analogmic[] PROGMEM = "analogmic";
#endif
const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic";
const char AudioReactive::UDP_SYNC_HEADER[] PROGMEM = "00002"; // new sync header version, as format no longer compatible with previous structure
const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature

View File

@@ -1,28 +0,0 @@
# Blynk controllable relay
Enables controlling a relay state via user variables. Allows the user variables to be set via Blynk.
Optionally, the servo can have a reset timer to return to its default state after a user definable interval. The interval is set via userVar1.
## Instalation
Replace the WLED06_usermod.ino file in Aircoookies WLED folder, with the one here.
## Customizations
Update the following parameters in WLED06_usermod.ino to configure the mod's behavior:
```cpp
//Which pin is the relay connected to
#define RELAY_PIN 5
//Which pin state should the relay default to
#define RELAY_PIN_DEFAULT LOW
//If >0 The controller returns to RELAY_PIN_DEFAULT after this time, in milliseconds
#define RELAY_PIN_TIMER_DEFAULT 3000
//Blynk virtual pin for controlling relay
#define BLYNK_USER_VAR0_PIN V9
//Blynk virtual pin for controlling relay timer
#define BLYNK_USER_VAR1_PIN V10
//Number of milliseconds between Blynk updates
#define BLYNK_RELAY_UPDATE_INTERVAL 5000
```

View File

@@ -1,96 +0,0 @@
/*
* This file allows you to add own functionality to WLED more easily
* See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality
* EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in wled_eeprom.h)
* bytes 2400+ are currently ununsed, but might be used for future wled features
*/
//Use userVar0 (API calls &U0=, uint16_t) to set relay state
#define relayPinState userVar0
//Use userVar1 (API calls &U1=, uint16_t) to set relay timer duration
//Ignored if 0, otherwise number of milliseconds to allow relay to stay in
//non default state.
#define relayTimerInterval userVar1
//Which pin is the relay connected to
#define RELAY_PIN 5
//Which pin state should the relay default to
#define RELAY_PIN_DEFAULT LOW
//If >0 The controller returns to RELAY_PIN_DEFAULT after this time in milliseconds
#define RELAY_PIN_TIMER_DEFAULT 3000
//Blynk virtual pin for controlling relay
#define BLYNK_USER_VAR0_PIN V9
//Blynk virtual pin for controlling relay timer
#define BLYNK_USER_VAR1_PIN V10
//Number of milliseconds between updating blynk
#define BLYNK_RELAY_UPDATE_INTERVAL 5000
//Is the timer for resetting the relay active
bool relayTimerStarted = false;
//millis() time after which relay will be reset
unsigned long relayTimeToDefault = 0;
//millis() time after which relay vars in Blynk will be sent
unsigned long relayBlynkUpdateTime = 0;
//gets called once at boot. Do all initialization that doesn't depend on network here
void userSetup()
{
relayPinState = RELAY_PIN_DEFAULT;
relayTimerInterval = RELAY_PIN_TIMER_DEFAULT;
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, relayPinState);
}
//gets called every time WiFi is (re-)connected. Initialize own network interfaces here
void userConnected()
{
}
//loop. You can use "if (WLED_CONNECTED)" to check for successful connection
void userLoop()
{
//Normalize relayPinState to an accepted value
if (relayPinState != HIGH && relayPinState != LOW) {
relayPinState = RELAY_PIN_DEFAULT;
}
//If relay changes and relayTimerInterval is set, start a timer to change back
if (relayTimerInterval != 0 &&
relayPinState != RELAY_PIN_DEFAULT &&
!relayTimerStarted ) {
relayTimerStarted = true;
relayTimeToDefault = millis() + relayTimerInterval;
}
//If manually changed back to default, cancel timer
if (relayTimerStarted && relayPinState == RELAY_PIN_DEFAULT ) {
relayTimerStarted = false;
}
//If timer completes, set relay back to default
if (relayTimerStarted && millis() > relayTimeToDefault) {
relayPinState = RELAY_PIN_DEFAULT;
relayTimerStarted = false;
}
digitalWrite(RELAY_PIN, relayPinState);
updateRelayBlynk();
}
//Update Blynk with state of userVars at BLYNK_RELAY_UPDATE_INTERVAL
void updateRelayBlynk()
{
if (!WLED_CONNECTED) return;
if (relayBlynkUpdateTime > millis()) return;
Blynk.virtualWrite(BLYNK_USER_VAR0_PIN, userVar0);
Blynk.virtualWrite(BLYNK_USER_VAR1_PIN, userVar1);
relayBlynkUpdateTime = millis() + BLYNK_RELAY_UPDATE_INTERVAL;
}
//Add Blynk callback for setting userVar0
BLYNK_WRITE(BLYNK_USER_VAR0_PIN)
{
userVar0 = param.asInt();
}
//Add Blynk callback for setting userVar1
BLYNK_WRITE(BLYNK_USER_VAR1_PIN)
{
userVar1 = param.asInt();
}

View File

@@ -65,9 +65,9 @@
#undef DEBUG_PRINTF
#ifdef WLED_DEBUG
#define DEBUG_PRINT(x) DEBUGOUT.print(x)
#define DEBUG_PRINTLN(x) DEBUGOUT.println(x)
#define DEBUG_PRINTF(x...) DEBUGOUT.printf(x)
#define DEBUG_PRINT(x) DEBUGOUT(x)
#define DEBUG_PRINTLN(x) DEBUGOUTLN(x)
#define DEBUG_PRINTF(x...) DEBUGOUTF(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINTLN(x)
@@ -93,18 +93,22 @@ void IRAM_ATTR dmpDataReady() {
class MPU6050Driver : public Usermod {
private:
MPU6050 mpu;
bool enabled = true;
unsigned long lastUMRun = millis();
// MPU control/status vars
bool dmpReady = false; // set true if DMP init was successful
uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU
uint8_t devStatus; // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize; // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount; // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer
// strings to reduce flash memory usage (used more than twice)
static const char _INT_pin[];
public:
MPU6050Driver(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class
bool dmpReady = false; // set true if DMP init was successful // WLEDMM expose this info in public interface
// orientation/motion vars
Quaternion qat; // [w, x, y, z] quaternion container
VectorInt16 aa; // [x, y, z] accel sensor measurements
@@ -122,7 +126,11 @@ class MPU6050Driver : public Usermod {
#endif
void setup() {
// WLEDMM begin
// WLEDMM begin
if (!enabled) {
dmpReady = false;
return;
}
USER_PRINTLN(F("mpu setup"));
PinManagerPinType pins[2] = { { i2c_scl, true }, { i2c_sda, true } };
if ((i2c_scl < 0) || (i2c_sda < 0)) {
@@ -171,16 +179,26 @@ class MPU6050Driver : public Usermod {
// initialize device
DEBUG_PRINT_IMULN(F("Initializing I2C devices..."));
// WLEDMM begin
if (!pinManager.allocatePin(INTERRUPT_PIN, false, PinOwner::UM_Unspecified))
if ((INTERRUPT_PIN < 0) || (!pinManager.isPinINT(INTERRUPT_PIN))) {
//enabled = false;
USER_PRINTF("mpu6050: warning - interrupt GPIO %d does not support interrupts.\n", INTERRUPT_PIN);
//INTERRUPT_PIN = -1;
//return;
}
if ((INTERRUPT_PIN >= 0) && (pinManager.getPinOwner(INTERRUPT_PIN) != PinOwner::UM_IMU) // only allocate pin if we don't ownn it already
&& !pinManager.allocatePin(INTERRUPT_PIN, false, PinOwner::UM_IMU))
{
//enabled = false;
USER_PRINTF("mpu6050: warning - failed to allocate interrupt GPIO %d\n", INTERRUPT_PIN);
//INTERRUPT_PIN = -1;
//return;
}
// WLEDMM end
mpu.initialize();
pinMode(INTERRUPT_PIN, INPUT);
if (INTERRUPT_PIN >= 0) { // WLEDMM only if pin is valid
pinMode(INTERRUPT_PIN, INPUT);
}
// verify connection
DEBUG_PRINT_IMULN(F("Testing device connections..."));
@@ -214,11 +232,13 @@ class MPU6050Driver : public Usermod {
DEBUG_PRINT_IMULN(F("Enabling DMP..."));
mpu.setDMPEnabled(true);
// enable Arduino interrupt detection
DEBUG_PRINT_IMU(F("Enabling interrupt detection (Arduino external interrupt "));
DEBUG_PRINT_IMU(digitalPinToInterrupt(INTERRUPT_PIN));
DEBUG_PRINT_IMULN(F(")..."));
attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
if (INTERRUPT_PIN >= 0) {
// enable Arduino interrupt detection
DEBUG_PRINT_IMU(F("Enabling interrupt detection (Arduino external interrupt "));
DEBUG_PRINT_IMU(digitalPinToInterrupt(INTERRUPT_PIN));
DEBUG_PRINT_IMULN(F(")..."));
attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
}
mpuIntStatus = mpu.getIntStatus();
// set our DMP Ready flag so the main loop() function knows it's okay to use it
@@ -235,7 +255,9 @@ class MPU6050Driver : public Usermod {
DEBUG_PRINT(F("DMP Initialization failed (code "));
DEBUG_PRINT(devStatus);
DEBUG_PRINTLN(")");
dmpReady = false;
}
initDone = true;
}
void connected() {
@@ -244,6 +266,7 @@ class MPU6050Driver : public Usermod {
void loop() {
// if programming failed, don't try to do anything
if (!initDone) return;
if (!enabled || (strip.isUpdating() && (millis() - lastUMRun < 2))) return; // be nice, but not too nice
lastUMRun = millis(); // update time keeping
@@ -265,12 +288,15 @@ class MPU6050Driver : public Usermod {
void addToJsonInfo(JsonObject& root)
{
if (!initDone) return;
if (!enabled && !dmpReady) return; // WLEDMM no info when usermod disabled
JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject("u");
StaticJsonDocument<800> doc; //measured 528 // WLEDMM added some margin (was 600)
JsonObject imu_meas = doc.createNestedObject("IMU");
//JsonObject imu_meas = user.createNestedObject("IMU");
#ifdef WLED_DEBUG
JsonArray quat_json = imu_meas.createNestedArray("Quat");
quat_json.add(qat.w);
@@ -328,29 +354,64 @@ class MPU6050Driver : public Usermod {
//{
//}
// void addToConfig(JsonObject& root)
// {
// JsonObject top = root.createNestedObject("MPU6050");
// top[FPSTR("enabled")] = enabled;
// void addToConfig(JsonObject& root)
// {
// Usermod::addToConfig(root);
// JsonObject top = root[FPSTR(_name)];
// // //JsonObject interruptPin = top.createNestedObject(FPSTR(_INT_pin));
// // //interruptPin["pin"] = INTERRUPT_PIN;
// // DEBUG_PRINTLN(F("MPU6050 IMU config saved."));
// }
// JsonObject interruptPin = top.createNestedObject(FPSTR("interruptPin"));
// interruptPin["pin"] = interruptPin;
// }
//WLEDMM: add appendConfigData
void appendConfigData()
{
oappend(SET_F("addHB('mpu6050-IMU');"));
/*
#ifdef MPU6050_INT_GPIO
oappend(SET_F("xOpt('mpu6050-IMU:interrupt_pin',0,' ⎌',")); oappendi(MPU6050_INT_GPIO); oappend(");");
#endif
//WLEDMM add errorMessage to um settings
if (strcmp(errorMessage, "") != 0) {
oappend(SET_F("addInfo('errorMessage', 0, '<i>error: ")); oappend(errorMessage); oappend("! Correct and reboot</i>');");
}
*/
}
// bool readFromConfig(JsonObject& root)
// {
// JsonObject top = root[FPSTR("MPU6050")];
// bool configComplete = !top.isNull();
bool readFromConfig(JsonObject& root)
{
bool configComplete = Usermod::readFromConfig(root);
JsonObject top = root[FPSTR(_name)];
// configComplete &= getJsonValue(top[FPSTR("enabled")], enabled);
// configComplete &= getJsonValue(top[FPSTR("interruptPin")]["pin"], interruptPin);
if (top.isNull()) {
DEBUG_PRINT(FPSTR(_name));
DEBUG_PRINTLN(F(": No config found. (Using defaults.)"));
return false;
}
// return configComplete;
// }
//configComplete &= getJsonValue(top[FPSTR(_INT_pin)]["pin"], INTERRUPT_PIN);
DEBUG_PRINT(FPSTR(_name));
if (!initDone) {
// first run: reading from cfg.json
DEBUG_PRINTLN(F(" config loaded."));
} else {
DEBUG_PRINTLN(F(" config (re)loaded."));
if (enabled || dmpReady) setup(); // re-run setup if user has checked "enabled"
if (!enabled) dmpReady = false; // not enabled inplies "no DMP data ready"
}
return configComplete;
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
//return !top[FPSTR(_INT_pin)].isNull();
}
uint16_t getId()
{
return USERMOD_ID_IMU;
}
};
};
// strings to reduce flash memory usage (used more than twice)
const char MPU6050Driver::_INT_pin[] PROGMEM = "interrupt_pin";

View File

@@ -202,11 +202,11 @@ class MultiRelay : public Usermod {
/**
* Enable/Disable the usermod
*/
inline void enable(bool enable) { enabled = enable; }
// inline void enable(bool enable) { enabled = enable; }
/**
* Get usermod enabled/disabled state
*/
inline bool isEnabled() { return enabled; }
// inline bool isEnabled() { return enabled; }
/**
* switch relay on/off

View File

@@ -79,6 +79,10 @@ class AutoSaveUsermod : public Usermod {
month(localTime), day(localTime),
hour(localTime), minute(localTime), second(localTime));
cacheInvalidate++; // force reload of presets
DEBUG_PRINT(F("UM autosave: saving preset "));
DEBUG_PRINT(autoSavePreset);
DEBUG_PRINT(F(" => "));
DEBUG_PRINTLN(presetNameBuffer);
savePreset(autoSavePreset, presetNameBuffer);
}
@@ -86,7 +90,7 @@ class AutoSaveUsermod : public Usermod {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display != nullptr) {
display->wakeDisplay();
display->overlay("Settings", "Auto Saved", 1500);
if (display->canDraw()) display->overlay("Settings", "Auto Saved", 1500); // WLEDMM bugfix
}
#endif
}

View File

@@ -0,0 +1,10 @@
# Usermods API v2 example usermod
In this usermod file you can find the documentation on how to take advantage of the new version 2 usermods!
## Installation
Copy `usermod_v2_fastled.h` to the wled00 directory.
Uncomment the corresponding lines in `usermods_list.cpp` and compile!
_(You shouldn't need to actually install this, it does nothing useful)_

View File

@@ -0,0 +1,358 @@
#pragma once
#include "wled.h"
//WLEDMM
// Polar basics demo for the
// FastLED Podcast #2
// https://www.youtube.com/watch?v=KKjFRZFBUrQ
//
// VO.1 preview version
// by Stefan Petrick 2023
// This code is licenced under a
// Creative Commons Attribution
// License CC BY-NC 3.0
class PolarBasics {
private:
public:
float runtime; // elapse ms since startup
float newdist, newangle; // parameters for image reconstruction
float z; // 3rd dimension for the 3d noise function
float offset_x, offset_y; // wanna shift the cartesians during runtime?
float scale_x, scale_y; // cartesian scaling in 2 dimensions
float dist, angle; // the actual polar coordinates
int x, y; // the cartesian coordiantes
int num_x;// = WIDTH; // horizontal pixel count
int num_y;// = HEIGHT; // vertical pixel count
// Background for setting the following 2 numbers: the FastLED inoise16() function returns
// raw values ranging from 0-65535. In order to improve contrast we filter this output and
// stretch the remains. In histogram (photography) terms this means setting a blackpoint and
// a whitepoint. low_limit MUST be smaller than high_limit.
uint16_t low_limit;// = 30000; // everything lower drawns in black
// higher numer = more black & more contrast present
uint16_t high_limit;// = 50000; // everything higher gets maximum brightness & bleeds out
// lower number = the result will be more bright & shiny
float center_x;// = (num_x / 2) - 0.5; // the reference point for polar coordinates
float center_y;// = (num_y / 2) - 0.5; // (can also be outside of the actual xy matrix)
//float center_x = 20; // the reference point for polar coordinates
//float center_y = 20;
//WLEDMM: assign 32x32 fixed for the time being
float theta [32] [32]; // look-up table for all angles
float distance[32] [32]; // look-up table for all distances
float vignette[32] [32];
float inverse_vignette[32] [32];
// std::vector<std::vector<float>> theta; // look-up table for all angles
// std::vector<std::vector<float>> distance; // look-up table for all distances
// std::vector<std::vector<float>> vignette;
// std::vector<std::vector<float>> inverse_vignette;
float spd; // can be used for animation speed manipulation during runtime
float show1, show2, show3, show4, show5; // to save the rendered values of all animation layers
float red, green, blue; // for the final RGB results after the colormapping
float c, d, e, f; // factors for oscillators
float linear_c, linear_d, linear_e, linear_f; // linear offsets
float angle_c, angle_d, angle_e, angle_f; // angle offsets
float noise_angle_c, noise_angle_d, noise_angle_e, noise_angle_f; // angles based on linear noise travel
float dir_c, dir_d, dir_e, dir_f; // direction multiplicators
void init () {
num_x = SEGMENT.virtualWidth(); // horizontal pixel count
num_y = SEGMENT.virtualHeight(); // vertical pixel count
low_limit = 30000; // everything lower drawns in black
// higher numer = more black & more contrast present
high_limit = 50000; // everything higher gets maximum brightness & bleeds out
// lower number = the result will be more bright & shiny
center_x = (num_x / 2) - 0.5; // the reference point for polar coordinates
center_y = (num_y / 2) - 0.5; // (can also be outside of the actual xy matrix)
//allocate memory for the 2D arrays
// theta.resize(num_x, std::vector<float>(num_y, 0));
// distance.resize(num_x, std::vector<float>(num_y, 0));
// vignette.resize(num_x, std::vector<float>(num_y, 0));
// inverse_vignette.resize(num_x, std::vector<float>(num_y, 0));
}
PolarBasics() {
USER_PRINTLN("constructor");
}
~PolarBasics() {
USER_PRINTLN("destructor");
}
void speedratiosAndOscillators() {
// set speedratios for the offsets & oscillators
spd = 0.05 ;
c = 0.013 ;
d = 0.017 ;
e = 0.2 ;
f = 0.007 ;
calculate_oscillators(); // get linear offsets and oscillators going
}
void forLoop() {
// pick polar coordinates from look the up table
dist = distance [x] [y];
angle = theta [y] [x];
// Generation of one layer. Explore the parameters and what they do.
scale_x = 10000; // smaller value = zoom in, bigger structures, less detail
scale_y = 10000; // higher = zoom out, more pixelated, more detail
z = 0; // must be >= 0
newangle = angle + angle_c;
newdist = dist;
offset_x = 0; // must be >=0
offset_y = 0; // must be >=0
show1 = render_pixel();
// Colormapping - Assign rendered values to colors
red = show1;
green = 0;
blue = 0;
// Check the final results.
// Discard faulty RGB values & write the valid results into the framebuffer.
write_pixel_to_framebuffer();
}
void calculate_oscillators() {
runtime = millis(); // save elapsed ms since start up
runtime = runtime * spd; // global anaimation speed
linear_c = runtime * c; // some linear rising offsets 0 to max
linear_d = runtime * d;
linear_e = runtime * e;
linear_f = runtime * f;
angle_c = fmodf(linear_c, 2 * PI); // some cyclic angle offsets 0 to 2*PI
angle_d = fmodf(linear_d, 2 * PI);
angle_e = fmodf(linear_e, 2 * PI);
angle_f = fmodf(linear_f, 2 * PI);
dir_c = sinf(angle_c); // some direction oscillators -1 to 1
dir_d = sinf(angle_d);
dir_e = sinf(angle_e);
dir_f = sinf(angle_f);
uint16_t noi;
noi = inoise16(10000 + linear_c * 100000); // some noise controlled angular offsets
noise_angle_c = map_float(noi, 0, 65535 , 0, 4*PI);
noi = inoise16(20000 + linear_d * 100000);
noise_angle_d = map_float(noi, 0, 65535 , 0, 4*PI);
noi = inoise16(30000 + linear_e * 100000);
noise_angle_e = map_float(noi, 0, 65535 , 0, 4*PI);
noi = inoise16(40000 + linear_f * 100000);
noise_angle_f = map_float(noi, 0, 65535 , 0, 4*PI);
}
// given a static polar origin we can precalculate
// all the (expensive) polar coordinates
void render_polar_lookup_table() {
for (int xx = 0; xx < num_x; xx++) {
for (int yy = 0; yy < num_y; yy++) {
float dx = xx - center_x;
float dy = yy - center_y;
distance[xx] [yy] = hypotf(dx, dy);
theta[xx] [yy] = atan2f(dy, dx);
}
}
}
// convert polar coordinates back to cartesian
// & render noise value there
float render_pixel() {
// convert polar coordinates back to cartesian ones
float newx = (offset_x + center_x - (cosf(newangle) * newdist)) * scale_x;
float newy = (offset_y + center_y - (sinf(newangle) * newdist)) * scale_y;
// render noisevalue at this new cartesian point
uint16_t raw_noise_field_value = inoise16(newx, newy, z);
// a lot is happening here, namely
// A) enhance histogram (improve contrast) by setting the black and white point
// B) scale the result to a 0-255 range
// it's the contrast boosting & the "colormapping" (technically brightness mapping)
if (raw_noise_field_value < low_limit) raw_noise_field_value = low_limit;
if (raw_noise_field_value > high_limit) raw_noise_field_value = high_limit;
float scaled_noise_value = map_float(raw_noise_field_value, low_limit, high_limit, 0, 255);
return scaled_noise_value;
// done, we've just rendered one color value for one single pixel
}
// float mapping maintaining 32 bit precision
// we keep values with high resolution for potential later usage
float map_float(float x, float in_min, float in_max, float out_min, float out_max) {
float result = (x-in_min) * (out_max-out_min) / (in_max-in_min) + out_min;
if (result < out_min) result = out_min;
if( result > out_max) result = out_max;
return result;
}
// Avoid any possible color flicker by forcing the raw RGB values to be 0-255.
// This enables to play freely with random equations for the colormapping
// without causing flicker by accidentally missing the valid target range.
void rgb_sanity_check() {
// rescue data if possible: when negative return absolute value
if (red < 0) red = abs(red);
if (green < 0) green = abs(green);
if (blue < 0) blue = abs(blue);
// discard everything above the valid 0-255 range
if (red > 255) red = 255;
if (green > 255) green = 255;
if (blue > 255) blue = 255;
}
void write_pixel_to_framebuffer() {
// the final color values shall not exceed 255 (to avoid flickering pixels caused by >255 = black...)
// negative values * -1
rgb_sanity_check();
CRGB finalcolor = CRGB(red, green, blue);
// write the rendered pixel into the framebutter
SEGMENT.setPixelColorXY(x,y,finalcolor);
}
// precalculate a radial brightness mask
void render_vignette_table(float filter_radius) {
for (int xx = 0; xx < num_x; xx++) {
for (int yy = 0; yy < num_y; yy++) {
vignette[xx] [yy] = (filter_radius - distance[xx] [yy]) / filter_radius;
if (vignette[xx] [yy] < 0) vignette[xx] [yy] = 0;
}
}
}
};
//effect functions
uint16_t mode_PolarBasics(void) {
PolarBasics* pb;
if(!SEGENV.allocateData(sizeof(PolarBasics))) {SEGMENT.fill(SEGCOLOR(0)); return 350;} //mode_static(); //allocation failed
pb = reinterpret_cast<PolarBasics*>(SEGENV.data);
//first time init
if (SEGENV.call == 0) {
USER_PRINTF("mode_PolarBasics %d\n", sizeof(PolarBasics));
// if (SEGENV.call == 0) SEGMENT.setUpLeds();
pb->init();
pb->render_polar_lookup_table(); // precalculate all polar coordinates
// to improve the framerate
pb->render_vignette_table(9.5); // the number is the desired radius in pixel
// WIDTH/2 generates a circle
}
pb->speedratiosAndOscillators();
// ...and now let's generate a frame
for (pb->x = 0; pb->x < pb->num_x; pb->x++) {
for (pb->y = 0; pb->y < pb->num_y; pb->y++) {
pb->forLoop();
}
}
// FastLED.show();
return FRAMETIME;
}
static const char _data_FX_mode_PolarBasics[] PROGMEM = "💡Polar Basics ☾@;;;2";
//class name. Use something descriptive and leave the ": public Usermod" part :)
class FastledUsermod : public Usermod {
private:
public:
FastledUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM
void setup() {
strip.addEffect(255, &mode_PolarBasics, _data_FX_mode_PolarBasics);
initDone = true;
}
void connected() {
}
void loop() {
if (!enabled || strip.isUpdating()) return;
// do your magic here
if (millis() - lastTime > 1000) {
//Serial.println("I'm alive!");
lastTime = millis();
}
}
uint16_t getId()
{
return USERMOD_ID_FASTLED;
}
};

View File

@@ -2,35 +2,37 @@
#include <Arduino.h> // WLEDMM: make sure that I2C drivers have the "right" Wire Object
#include <Wire.h>
#include <SPI.h>
#undef U8X8_NO_HW_I2C // WLEDMM: we do want I2C hardware drivers - if possible
//#define WIRE_INTERFACES_COUNT 2 // experimental - tell U8x8Lib that there is a econd Wire unit
//#define WIRE_INTERFACES_COUNT 2 // experimental - tell U8x8Lib that there is a second Wire unit
#include "wled.h"
#include <U8x8lib.h> // from https://github.com/olikraus/u8g2/
#include "4LD_wled_fonts.c"
#ifndef FLD_ESP32_NO_THREADS
#define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!!
#define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!!
#endif
//#define OLD_4LD_FONTS // comment out if you prefer the "classic" look with blocky fonts (saves 1K flash)
//
// Insired by the usermod_v2_four_line_display
// Inspired by the usermod_v2_four_line_display
//
// 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.
// * This usermod does not REQUIRE the ModeSortUsermod any more
// * This usermod works best, by far, when coupled
// with RotaryEncoderUI_ALT usermod.
//
// 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
// REQUIREMENT: olikraus/U8g2@ ^2.34.15 (the version already in platformio.ini is fine)
//
//The SCL and SDA pins are defined here.
@@ -92,7 +94,9 @@ typedef enum {
SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C
SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C
SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI
SSD1306_SPI64 // U8X8_SSD1306_128X64_NONAME_HW_SPI
SSD1306_SPI64=7, // U8X8_SSD1306_128X64_NONAME_HW_SPI
SSD1309_SPI64=8, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI
SSD1327_SPI128=9 // U8X8_SSD1327_WS_128X128_4W_SW_SPI
} DisplayType;
@@ -115,6 +119,23 @@ class FourLineDisplayUsermod : public Usermod {
// HW interface & configuration
U8X8 *u8x8 = nullptr; // pointer to U8X8 display object
#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)
// semaphores - needed on ESP32 only, as we use a separate task to update the display
SemaphoreHandle_t drawMux = xSemaphoreCreateBinary(); // for drawstring and drawglyph functions (to prevent concurrent access to HW)
SemaphoreHandle_t drawMuxBig = xSemaphoreCreateBinary(); // for draw2x2GlyphIcons() and showCurrentEffectOrPalette() - more complex and not thread-safe
const TickType_t maxWait = 300 * portTICK_PERIOD_MS; // wait max. 300ms (drawstring semaphore)
const TickType_t maxWaitLong = 800 * portTICK_PERIOD_MS; // wait max. 800ms (big drawing semaphore)
#define FLD_SemaphoreTake(x,t) xSemaphoreTake((x),(t))
#define FLD_SemaphoreGive(x) xSemaphoreGive(x)
#else
// 8266 or no tasks - no semaphores
#define FLD_SemaphoreTake(x,t) pdTRUE
#define FLD_SemaphoreGive(x)
#if !defined(ARDUINO_ARCH_ESP32) && !defined(pdTRUE)
#define pdTRUE true
#endif
#endif
#ifndef FLD_SPI_DEFAULT
int8_t ioPin[5] = {FLD_PIN_SCL, FLD_PIN_SDA, -1, -1, -1}; // I2C pins: SCL, SDA
uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000)
@@ -124,7 +145,7 @@ class FourLineDisplayUsermod : public Usermod {
#endif
DisplayType type = FLD_TYPE; // display type
bool typeOK = false; //WLEDMM: instead of type == NULL and type=NULL. Intially false, as display was not initialized yet
bool typeOK = false; //WLEDMM: instead of type == NULL and type=NULL. Initially false, as display was not initialized yet
bool flip = false; // flip display 180°
uint8_t contrast = 10; // screen contrast
uint8_t lineHeight = 1; // 1 row or 2 rows
@@ -143,6 +164,8 @@ class FourLineDisplayUsermod : public Usermod {
bool enabled = true;
#endif
bool contrastFix = false;
bool driverHW = false;
bool driverSPI = false;
// Next variables hold the previous known values to determine if redraw is
// required.
@@ -193,7 +216,7 @@ class FourLineDisplayUsermod : public Usermod {
// some displays need this to properly apply contrast
void setVcomh(bool highContrast) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (!canDraw()) return; // don't interfere with ongoing draw
if (u8x8 == nullptr) return;
u8x8_t *u8x8_struct = u8x8->getU8x8();
u8x8_cad_StartTransfer(u8x8_struct);
@@ -207,33 +230,77 @@ class FourLineDisplayUsermod : public Usermod {
*/
void setFlipMode(uint8_t mode) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (canDraw()) u8x8->setFlipMode(mode);
if (u8x8 == nullptr) return;
u8x8->setFlipMode(mode);
}
void setContrast(uint8_t contrast) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (canDraw()) u8x8->setContrast(contrast);
if (u8x8 == nullptr) return;
u8x8->setContrast(contrast);
}
void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
u8x8->setFont(u8x8_font_chroma48medium8_r); // crashes randomly on ESP32
if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); // crashes randomly on ESP32
else u8x8->drawString(col, row, string);
if (u8x8 == nullptr) return;
if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex
#if defined(ARDUINO_ARCH_ESP32) && !defined(OLD_4LD_FONTS) // WLEDMM use nicer 2x2 font on ESP32
if (!ignoreLH && lineHeight>1) {
if(strlen(string) > 3) // WLEDMM little hack - less than 3 chars -> show in bold
u8x8->setFont(u8x8_font_7x14_1x2_r); // normal
else
u8x8->setFont(u8x8_font_8x13B_1x2_r); // bold
u8x8->drawString(col, row, string);
}
else {
u8x8->setFont(u8x8_font_chroma48medium8_r);
u8x8->drawString(col, row, string);
}
#else
u8x8->setFont(u8x8_font_chroma48medium8_r);
if (!ignoreLH && lineHeight>1) u8x8->draw1x2String(col, row, string);
else u8x8->drawString(col, row, string);
#endif
FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex
}
void draw2x2String(uint8_t col, uint8_t row, const char *string) {
if (!typeOK || !enabled) return;
if (u8x8 == nullptr) return;
if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex
#if defined(ARDUINO_ARCH_ESP32) && !defined(OLD_4LD_FONTS) // WLEDMM use nicer 2x2 font on ESP32
if (lineHeight>1) { // WLEDMM use 2x3 on 128x64 displays
//u8x8->setFont(u8x8_font_profont29_2x3_r); // sans serif 2x3
u8x8->setFont(u8x8_font_courB18_2x3_r); // courier bold 2x3
u8x8->drawString(col, row + (row >3? 1:0), string);
} else {
//u8x8->setFont(u8x8_font_lucasarts_scumm_subtitle_o_2x2_r);
//u8x8->setFont(u8x8_font_lucasarts_scumm_subtitle_r_2x2_r);
u8x8->setFont(u8x8_font_px437wyse700b_2x2_r);
u8x8->drawString(col, row, string);
}
#else
u8x8->setFont(u8x8_font_chroma48medium8_r);
u8x8->draw2x2String(col, row, string);
if (lineHeight>1) { // WLEDMM use 2x3 on 128x64 displays
u8x8->draw2x2String(col, row + (row >3? 1:0), string);
} else {
u8x8->draw2x2String(col, row, string);
}
#endif
FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex
}
void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false) {
if (!typeOK || !enabled) return;
if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex
u8x8->setFont(font);
if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph);
if (!ignoreLH && lineHeight>1) u8x8->draw1x2Glyph(col, row, glyph);
else u8x8->drawGlyph(col, row, glyph);
FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex
}
void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) {
if (!typeOK || !enabled) return;
if (FLD_SemaphoreTake(drawMux, maxWait) != pdTRUE) return; // WLEDMM acquire draw mutex
u8x8->setFont(font);
u8x8->draw2x2Glyph(col, row, glyph);
FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex
}
uint8_t getCols() {
if (!typeOK || !enabled) return 0;
@@ -242,10 +309,13 @@ class FourLineDisplayUsermod : public Usermod {
void clear() {
if (!typeOK || !enabled) return;
if (nullptr == u8x8) return; // prevents some crashes
if (FLD_SemaphoreTake(drawMux, maxWaitLong ) != pdTRUE) return; // WLEDMM acquire draw mutex - clear() can take very long in software I2C mode
u8x8->clear(); // crashes randomly on ESP32
FLD_SemaphoreGive(drawMux); // WLEDMM release draw mutex
}
void setPowerSave(uint8_t save) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (u8x8 == nullptr) return;
u8x8->setPowerSave(save);
}
@@ -257,7 +327,8 @@ class FourLineDisplayUsermod : public Usermod {
void draw2x2GlyphIcons() {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (lineHeight == 2) {
if (FLD_SemaphoreTake(drawMuxBig, maxWaitLong) != pdTRUE) return; // WLEDMM acquire BIG draw mutex
if (lineHeight > 1) {
drawGlyph( 1, 0, 1, u8x8_4LineDisplay_WLED_icons_2x2, true); //brightness icon
drawGlyph( 5, 0, 2, u8x8_4LineDisplay_WLED_icons_2x2, true); //speed icon
drawGlyph( 9, 0, 3, u8x8_4LineDisplay_WLED_icons_2x2, true); //intensity icon
@@ -270,6 +341,7 @@ class FourLineDisplayUsermod : public Usermod {
drawGlyph(15, 2, 4, u8x8_4LineDisplay_WLED_icons_1x1); //palette icon
drawGlyph(15, 3, 5, u8x8_4LineDisplay_WLED_icons_1x1); //effect icon
}
FLD_SemaphoreGive(drawMuxBig); // WLEDMM release BIG draw mutex
}
/**
@@ -305,10 +377,25 @@ class FourLineDisplayUsermod : public Usermod {
}
snprintf_P(lineBuffer,LINE_BUFFER_SIZE, PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent);
draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds
if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time
if (useAMPM) drawString(12, lineHeight*2 + (lineHeight-1), (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time
drawStatusIcons(); //icons power, wifi, timer, etc
if (lineHeight > 1) { // WLEDMM use extra space for useful information
#if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS)
snprintf_P(lineBuffer, LINE_BUFFER_SIZE, PSTR(" %-3.3s %-2.2s "), driverSPI? "SPI" : "I2C", driverHW? "hw" : "sw"); // WLEDMM driver info
#else
strncpy_P(lineBuffer, PSTR(" "), LINE_BUFFER_SIZE);
#endif
if (apActive) strncpy_P(lineBuffer, PSTR(" AP mode "), LINE_BUFFER_SIZE);
else if (!WLED_CONNECTED) strncpy_P(lineBuffer, PSTR(" NO NET "), LINE_BUFFER_SIZE);
if (WLED_MQTT_CONNECTED) lineBuffer[9] = 'M'; // "MQTT"
if (realtimeMode && !realtimeOverride) lineBuffer[10] = 'X'; // "eXternal control"
//if (transitionActive) lineBuffer[11] = 'T';
//if (stateChanged) lineBuffer[12] = 'C';
drawString(1, 0, lineBuffer, false);
}
knownMinute = minuteCurrent;
knownHour = hourCurrent;
}
@@ -316,7 +403,10 @@ class FourLineDisplayUsermod : public Usermod {
lastSecond = secondCurrent;
draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":");
snprintf_P(lineBuffer, LINE_BUFFER_SIZE, PSTR("%02d"), secondCurrent);
drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line
if (useAMPM)
drawString(12, lineHeight*2+1 + (lineHeight-1), lineBuffer, true); // even with double sized rows print seconds in 1 line // WLEDMM move it a bit lower
else
drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line
}
drawing = false;
}
@@ -326,16 +416,26 @@ class FourLineDisplayUsermod : public Usermod {
// gets called once at boot. Do all initialization that doesn't depend on
// network here
void setup() {
if (!enabled) return; // typeOK = true will be set after successfull setup
if (!enabled) return; // typeOK = true will be set after successful setup
bool isHW, isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64);
bool isHW, isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type > 7);
PinOwner po = PinOwner::UM_FourLineDisplay;
if (isSPI) {
if (ioPin[0] < 0 || ioPin[1] < 0) {
ioPin[0] = spi_sclk;
ioPin[1] = spi_mosi;
} else {
if ((spi_sclk < 0) && (spi_mosi < 0)) { // WLEDMM UM pins are valid, but global = -1 --> copy pins to "global"
spi_sclk = ioPin[0];
spi_mosi = ioPin[1];
}
}
if ((ioPin[0] < 0 || ioPin[1] < 0) && (spi_sclk < 0 || spi_mosi < 0)) { // invalid pins, or "use global" and global pins not defined
typeOK=false; strcpy(errorMessage, PSTR("SPI No Pins defined")); return; } //WLEDMM bugfix - ensure that "final" GPIO are valid
isHW = (ioPin[0]==spi_sclk && ioPin[1]==spi_mosi);
if ((ioPin[0] == -1) || (ioPin[1] == -1)) isHW = true; // WLEDMM "use global" = hardware
if ((spi_sclk <0) || (spi_mosi < 0)) isHW = false; // no global pins - use software emulation
PinManagerPinType cspins[3] = { { ioPin[2], true }, { ioPin[3], true }, { ioPin[4], true } };
if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { typeOK=false; strcpy(errorMessage, PSTR("SPI3 alloc pins failed")); return; }
if (isHW) po = PinOwner::HW_SPI; // allow multiple allocations of HW I2C bus pins
@@ -346,25 +446,36 @@ class FourLineDisplayUsermod : public Usermod {
strcpy(errorMessage, PSTR("SPI2 alloc pins failed"));
return;
}
// start SPI now!
#ifdef ARDUINO_ARCH_ESP32
if (isHW) SPI.begin(spi_sclk, spi_miso, spi_mosi); // ESP32 - will silently fail if SPI alread active.
#else
if (isHW) SPI.begin(); // ESP8266 - SPI pins are fixed
#endif
} else {
if (ioPin[0] < 0 || ioPin[1] < 0) {
ioPin[0] = i2c_scl;
ioPin[1] = i2c_sda;
}
//if (ioPin[0] < 0 || ioPin[1] < 0) { //WLEDMM do _not_ copy global pins !!
// ioPin[0] = i2c_scl;
// ioPin[1] = i2c_sda;
//}
isHW = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda);
if ((ioPin[0] == -1) || (ioPin[1] == -1)) isHW = true; // WLEDMM "use global" = hardware
// isHW = true;
if (isHW) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins
PinManagerPinType pins[2] = { {ioPin[0], true }, { ioPin[1], true } };
if (ioPin[0] < 0 || ioPin[1] < 0) { typeOK=false; strcpy(errorMessage, PSTR("I2C No Pins defined")); return; } //WLEDMM bugfix - ensure that "final" GPIO are valid
if ((ioPin[0] < 0 || ioPin[1] < 0) && (i2c_scl < 0 || i2c_sda < 0)) { // invalid pins, or "use global" and global pins not defined
typeOK=false; strcpy(errorMessage, PSTR("I2C No Pins defined")); return; } //WLEDMM bugfix - ensure that "final" GPIO are valid
if (isHW) {
if (!pinManager.joinWire(ioPin[1], ioPin[0])) { typeOK=false; strcpy(errorMessage, PSTR("I2C init failed")); return; } // WLEDMM join the HW bus
if (!pinManager.joinWire(i2c_sda, i2c_scl)) { typeOK=false; strcpy(errorMessage, PSTR("I2C HW init failed")); return; } // WLEDMM join the HW bus
} else {
if (!pinManager.allocateMultiplePins(pins, 2, po)) { typeOK=false; strcpy(errorMessage, PSTR("I2C Alloc pins failed")); return; } // WLEDMM use software bus
}
}
driverHW = isHW;
driverSPI= isSPI;
DEBUG_PRINTLN(F("Allocating display."));
/*
// At some point it may be good to not new/delete U8X8 object but use this instead
@@ -436,19 +547,31 @@ class FourLineDisplayUsermod : public Usermod {
if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]);
else u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset
break;
case SSD1309_SPI64:
// u8x8 uses global SPI variable that is attached to VSPI bus on ESP32
if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]);
else u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset
break;
case SSD1327_SPI128:
// u8x8 uses global SPI variable that is attached to VSPI bus on ESP32
if (!isHW) u8x8 = (U8X8 *) new U8X8_SSD1327_WS_128X128_4W_SW_SPI(ioPin[0], ioPin[1], ioPin[2], ioPin[3], ioPin[4]);
else u8x8 = (U8X8 *) new U8X8_SSD1327_WS_128X128_4W_HW_SPI(ioPin[2], ioPin[3], ioPin[4]); // Pins are cs, dc, reset
break;
default:
u8x8 = nullptr;
}
if (nullptr == u8x8) {
USER_PRINTLN(F("Display init failed."));
pinManager.deallocateMultiplePins((const uint8_t*)ioPin, isSPI ? 5 : 2, po);
if (!isHW || !isSPI) pinManager.deallocateMultiplePins((const uint8_t*)ioPin, isSPI ? 5 : 2, po); // WLEDMM do not de-alloc global pins
typeOK=false;
strcpy(errorMessage, PSTR("Display init failed"));
return;
}
lineHeight = u8x8->getRows() > 4 ? 2 : 1;
if (u8x8->getRows() > 8) lineHeight =3;
//if (u8x8->getRows() > 12) lineHeight =4;
if (isSPI) {
USER_PRINTLN(isHW ? F("Starting display (SPI HW).") : F("Starting display (SPI Soft)."));
} else {
@@ -457,15 +580,24 @@ class FourLineDisplayUsermod : public Usermod {
u8x8->setBusClock(ioFrequency); // can be used for SPI too
u8x8->begin();
typeOK = true;
drawing = false;
reDrawing = false;
drawing = true;
setFlipMode(flip);
setVcomh(contrastFix);
setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255
setPowerSave(0);
drawing = false;
// init semaphores to allow drawing
FLD_SemaphoreGive(drawMux);
FLD_SemaphoreGive(drawMuxBig);
onUpdateBegin(false); // create Display task // WLEDMM bugfix: before drawing anything
delay(200);
//drawString(0, 0, "Loading...");
overlayLogo(3500);
onUpdateBegin(false); // create Display task
initDone = true;
}
@@ -502,6 +634,7 @@ class FourLineDisplayUsermod : public Usermod {
//function to to check if a redraw or overlay draw is active. Needed for UM Rotary, to avoid random/concurrent drawing
bool canDraw(void) {
if (!typeOK || !enabled || !initDone) return(false); // WLEDMM make sure the display is initialized before we try to draw on it
#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) // only necessary on ESP32
if (drawing) return(false); // overlay draws someting
if (reDrawing) return(false); // redraw task draws something
@@ -609,7 +742,7 @@ class FourLineDisplayUsermod : public Usermod {
// and turn it back on if it changed.
clear();
sleepOrClock(true);
} else if (displayTurnedOff && ntpEnabled) {
} else if (displayTurnedOff) { // WLEDMM removed "&& ntpEnabled"
showTime();
}
return;
@@ -686,10 +819,10 @@ class FourLineDisplayUsermod : public Usermod {
uint8_t col = 15;
uint8_t row = 0;
drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon
if (lineHeight==2) { col--; } else { row++; }
if (lineHeight>1) { col--; } else { row++; }
drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon
if (lineHeight==2) { col--; } else { col = row = 0; }
drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode
if (lineHeight>1) { col--; } else { col = row = 0; }
drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nightlight mode
}
/**
@@ -702,7 +835,7 @@ class FourLineDisplayUsermod : public Usermod {
markColNum = newMarkColNum;
}
//Draw the arrow for the current setting beiong changed
//Draw the arrow for the current setting being changed
void drawArrow() {
if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1);
}
@@ -712,6 +845,8 @@ class FourLineDisplayUsermod : public Usermod {
void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) {
char lineBuffer[MAX_JSON_CHARS] = { '\0' };
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (FLD_SemaphoreTake(drawMuxBig, maxWaitLong) != pdTRUE) return; // WLEDMM acquire BIG draw mutex
if (overlayUntil == 0) {
// Find the mode name in JSON
uint8_t printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1);
@@ -725,7 +860,7 @@ class FourLineDisplayUsermod : public Usermod {
for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0'
printedChars -= 5;
}
if (lineHeight == 2) { // use this code for 8 line display
if (lineHeight > 1) { // use this code for 8 line display
char smallBuffer1[MAX_MODE_LINE_SPACE+1] = { '\0' };
char smallBuffer2[MAX_MODE_LINE_SPACE+1] = { '\0' };
uint8_t smallChars1 = 0;
@@ -754,11 +889,11 @@ class FourLineDisplayUsermod : public Usermod {
}
while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' ';
smallBuffer1[smallChars1] = 0;
smallBuffer1[MAX_MODE_LINE_SPACE -1] = '\0'; // ensure the string ends where it should (while loop avove can overshoot by 1)
smallBuffer1[MAX_MODE_LINE_SPACE -1] = '\0'; // ensure the string ends where it should (while loop above can overshoot by 1)
drawString(1, row*lineHeight, smallBuffer1, true);
while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' ';
smallBuffer2[smallChars2] = 0;
smallBuffer2[MAX_MODE_LINE_SPACE -1] = '\0'; // ensure the string ends where it should (while loop avove can overshoot by 1)
smallBuffer2[MAX_MODE_LINE_SPACE -1] = '\0'; // ensure the string ends where it should (while loop above can overshoot by 1)
drawString(1, row*lineHeight+1, smallBuffer2, true);
}
} else { // use this code for 4 ling displays
@@ -769,6 +904,8 @@ class FourLineDisplayUsermod : public Usermod {
drawString(1, row*lineHeight, smallBuffer3, true);
}
}
FLD_SemaphoreGive(drawMuxBig); // WLEDMM release BIG draw mutex
}
/**
@@ -779,6 +916,7 @@ class FourLineDisplayUsermod : public Usermod {
*/
bool wakeDisplay() {
if (!typeOK || !enabled) return false;
if (!initDone) return false;
if (displayTurnedOff) {
unsigned long now = millis();
while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing
@@ -800,6 +938,7 @@ class FourLineDisplayUsermod : public Usermod {
*/
void overlay(const char* line1, long showHowLong, byte glyphType) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (!initDone) return; // WLEDMM bugfix
unsigned long now = millis();
while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing
while ((reDrawing && overlayUntil == 0) && (millis()-now < 250)) delay(10); // wait if someone else is re-drawing
@@ -808,7 +947,7 @@ class FourLineDisplayUsermod : public Usermod {
if (!wakeDisplay()) clear();
// Print the overlay
if (glyphType>0 && glyphType<255) {
if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font
if (lineHeight > 1) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font
else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true);
}
if (line1) {
@@ -832,7 +971,7 @@ class FourLineDisplayUsermod : public Usermod {
// Turn the display back on
if (!wakeDisplay()) clear();
// Print the overlay
if (lineHeight == 2) {
if (lineHeight > 1) {
//add a bit of randomness
switch (millis()%3) {
case 0:
@@ -888,6 +1027,7 @@ class FourLineDisplayUsermod : public Usermod {
*/
void overlay(const char* line1, const char* line2, long showHowLong) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (!initDone) return; // WLEDMM bugfix
unsigned long now = millis();
while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing
while ((reDrawing && overlayUntil == 0) && (millis()-now < 250)) delay(10); // wait if someone else is re-drawing
@@ -911,6 +1051,7 @@ class FourLineDisplayUsermod : public Usermod {
void networkOverlay(const char* line1, long showHowLong) {
if (!typeOK || !enabled) return; // WLEDMM make sure the display is initialized before we try to draw on it
if (!initDone) return; // WLEDMM bugfix
unsigned long now = millis();
while (drawing && millis()-now < 250) delay(1); // wait if someone else is drawing
drawing = true;
@@ -952,10 +1093,11 @@ class FourLineDisplayUsermod : public Usermod {
/**
* Enable sleep (turn the display off) or clock mode.
*/
void sleepOrClock(bool enabled) {
if (enabled) {
void sleepOrClock(bool sleepEnable) {
if (sleepEnable) {
displayTurnedOff = true;
if (clockMode && ntpEnabled) {
//setContrast(contrastFix? 2+ contrast/4 : 0); // un-comment to dim display in "clock mode"
if (clockMode) { // WLEDMM removed " && ntpEnabled"
knownMinute = knownHour = 99;
showTime();
} else
@@ -963,6 +1105,7 @@ class FourLineDisplayUsermod : public Usermod {
} else {
displayTurnedOff = false;
setPowerSave(0);
//setContrast(contrast); // un-comment to restore display brightness on wakeup
}
}
@@ -1059,7 +1202,7 @@ class FourLineDisplayUsermod : public Usermod {
xTaskCreateUniversal( // this is guaranteed to work on any ESP32 (single or dual core)
[](void * par) { // Function to implement the task
// see https://www.freertos.org/vtaskdelayuntil.html
const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2;
const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2;
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;) {
delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy.
@@ -1120,7 +1263,9 @@ class FourLineDisplayUsermod : public Usermod {
oappend(SET_F("addOption(dd,'SSD1305 128x64',5);"));
oappend(SET_F("addOption(dd,'SSD1306 SPI',6);"));
oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);"));
bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64);
oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);"));
oappend(SET_F("addOption(dd,'SSD1327 SPI 128x128',9);"));
bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type > 7);
// WLEDMM add defaults
oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','I2C/SPI CLK');"));
oappend(SET_F("dRO('4LineDisplay:pin[]',0);")); // disable read only pins
@@ -1183,7 +1328,7 @@ class FourLineDisplayUsermod : public Usermod {
void addToConfig(JsonObject& root) {
// determine if we are using global HW pins (data & clock)
int8_t hw_dta, hw_clk;
if ((type == SSD1306_SPI || type == SSD1306_SPI64)) {
if ((type == SSD1306_SPI || type == SSD1306_SPI64) || (type > 7)) {
hw_clk = spi_sclk;
hw_dta = spi_mosi;
} else {
@@ -1249,7 +1394,7 @@ class FourLineDisplayUsermod : public Usermod {
clockMode = top[FPSTR(_clockMode)] | clockMode;
showSeconds = top[FPSTR(_showSeconds)] | showSeconds;
contrastFix = top[FPSTR(_contrastFix)] | contrastFix;
if (newType == SSD1306_SPI || newType == SSD1306_SPI64)
if (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType > 7)
ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency
else
ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency
@@ -1279,14 +1424,17 @@ class FourLineDisplayUsermod : public Usermod {
USER_PRINTLN(F("Display terminated."));
}
PinOwner po = PinOwner::UM_FourLineDisplay;
bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64);
bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type > 7);
if (isSPI) {
pinManager.deallocateMultiplePins((const uint8_t *)(&oldPin[2]), 3, po);
bool isHW = (oldPin[0]==spi_sclk && oldPin[1]==spi_mosi);
if (oldPin[0]==-1 && oldPin[1]==-1) isHW = true; // WLEDMM "use global" means hardware driver
if (spi_sclk==-1 && spi_mosi==-1) isHW = false; // WLEDMM global pins not set -> software driver
if (isHW) po = PinOwner::HW_SPI;
} else {
bool isHW = (oldPin[0]==i2c_scl && oldPin[1]==i2c_sda);
if (isHW) po = PinOwner::HW_I2C;
//bool isHW = (oldPin[0]==i2c_scl && oldPin[1]==i2c_sda);
//if ((ioPin[0] == -1) || (ioPin[1] == -1)) isHW = true; // WLEDMM "use global" = hardware
//if (isHW) po = PinOwner::HW_I2C; // WLEDMM don't try to de-alloc HW pins.
}
pinManager.deallocateMultiplePins((const uint8_t *)oldPin, 2, po);
type = newType;
@@ -1333,3 +1481,8 @@ const char FourLineDisplayUsermod::_contrastFix[] PROGMEM = "contrastFix";
#ifdef ARDUINO_ARCH_ESP32
FourLineDisplayUsermod *FourLineDisplayUsermod::instance = nullptr;
#endif
// WLEDMM clean up some macros, so they don't cause problems in other usermods
#undef FLD_SemaphoreTake
#undef FLD_SemaphoreGive

View File

@@ -183,7 +183,7 @@ uint16_t mode_IMUTest(void) {
uint8_t y = 0;
if (IMU != nullptr) {
if ((IMU != nullptr) && (IMU->dmpReady)) {
SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aa.x+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE);
SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aa.y+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE);
SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() * (IMU->aa.z+INT16_MAX)/(2*INT16_MAX), y+=1, BLUE);
@@ -280,7 +280,7 @@ uint16_t mode_3DIMUCube(void) {
float roll = 0;
#ifdef USERMOD_MPU6050_IMU
if (IMU != nullptr) {
if ((IMU != nullptr) && (IMU->dmpReady)) {
yaw = -IMU->ypr[0];
pitch = IMU->ypr[1];
roll = IMU->ypr[2];
@@ -319,7 +319,7 @@ uint16_t mode_3DIMUCube(void) {
return FRAMETIME;
}
static const char _data_FX_MODE_3DIMUCube[] PROGMEM = "🎮 3DIMUCube ☾@,Perspective;!;!;2;pal=1"; //random cycle
static const char _data_FX_MODE_3DIMUCube[] PROGMEM = "🎮 3DIMUCube ☾@,Perspective;!;!;2;pal=1"; //WLEDMM random smooth
class GamesUsermod : public Usermod {
private:
@@ -330,9 +330,9 @@ class GamesUsermod : public Usermod {
strip.addEffect(255, &mode_pongGame, _data_FX_MODE_PONGGAME);
#ifdef USERMOD_MPU6050_IMU
IMU = (MPU6050Driver *)usermods.lookup(USERMOD_ID_IMU);
// #ifdef WLED_DEBUG
#ifdef WLED_DEBUG
strip.addEffect(255, &mode_IMUTest, _data_FX_MODE_IMUTest);
// #endif
#endif
#endif
strip.addEffect(255, &mode_3DIMUCube, _data_FX_MODE_3DIMUCube); //works also without IMU
}

View File

@@ -0,0 +1,40 @@
# Klipper Percentage Usermod
This usermod polls the Klipper API every 10s for the progressvalue.
The leds are then filled with a solid color according to that progress percentage.
the solid color is the secondary color of the segment.
A corresponding curl command would be:
```
curl --location --request GET 'http://[]/printer/objects/query?virtual_sdcard=progress'
```
## Usage
Compile the source with the buildflag `-D USERMOD_KLIPPER_PERCENTAGE` added.
You can also use the WLBD bot in the Discord by simply extending an exsisting build enviroment:
```
[env:esp32klipper]
extends = env:esp32dev
build_flags = ${common.build_flags_esp32} -D USERMOD_KLIPPER_PERCENTAGE
```
## Settings
### Enabled:
Checkbox to enable or disable the overlay
### Klipper IP:
IP adress of your Klipper instance you want to poll. ESP has to be restarted after change
### Direction :
0 = normal
1 = reversed
2 = center
-----
Author:
Sören Willrodt
Discord: Sören#5281

View File

@@ -0,0 +1,222 @@
#pragma once
#include "wled.h"
class klipper_percentage : public Usermod
{
private:
unsigned long lastTime = 0;
String ip = "192.168.25.207";
WiFiClient wifiClient;
char errorMessage[100] = "";
int printPercent = 0;
int direction = 0; // 0 for along the strip, 1 for reversed direction
static const char _name[];
static const char _enabled[];
bool enabled = false;
void httpGet(WiFiClient &client, char *errorMessage)
{
// https://arduinojson.org/v6/example/http-client/
// is this the most compact way to do http get and put it in arduinojson object???
// would like async response ... ???
client.setTimeout(10000);
if (!client.connect(ip.c_str(), 80))
{
strcat(errorMessage, PSTR("Connection failed"));
}
else
{
// Send HTTP request
client.println(F("GET /printer/objects/query?virtual_sdcard=progress HTTP/1.0"));
client.println("Host: " + ip);
client.println(F("Connection: close"));
if (client.println() == 0)
{
strcat(errorMessage, PSTR("Failed to send request"));
}
else
{
// Check HTTP status
char status[32] = {0};
client.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.1 200 OK") != 0)
{
strcat(errorMessage, PSTR("Unexpected response: "));
strcat(errorMessage, status);
}
else
{
// Skip HTTP headers
char endOfHeaders[] = "\r\n\r\n";
if (!client.find(endOfHeaders))
{
strcat(errorMessage, PSTR("Invalid response"));
}
}
}
}
}
public:
void setup()
{
}
void connected()
{
}
void loop()
{
if (enabled)
{
if (WLED_CONNECTED)
{
if (millis() - lastTime > 10000)
{
httpGet(wifiClient, errorMessage);
if (strcmp(errorMessage, "") == 0)
{
PSRAMDynamicJsonDocument klipperDoc(4096); // in practive about 2673
DeserializationError error = deserializeJson(klipperDoc, wifiClient);
if (error)
{
strcat(errorMessage, PSTR("deserializeJson() failed: "));
strcat(errorMessage, error.c_str());
}
printPercent = (int)(klipperDoc["result"]["status"]["virtual_sdcard"]["progress"].as<float>() * 100);
DEBUG_PRINT("Percent: ");
DEBUG_PRINTLN((int)(klipperDoc["result"]["status"]["virtual_sdcard"]["progress"].as<float>() * 100));
DEBUG_PRINT("LEDs: ");
DEBUG_PRINTLN(direction == 2 ? (strip.getLengthTotal() / 2) * printPercent / 100 : strip.getLengthTotal() * printPercent / 100);
}
else
{
DEBUG_PRINTLN(errorMessage);
DEBUG_PRINTLN(ip);
}
lastTime = millis();
}
}
}
}
void addToConfig(JsonObject &root)
{
JsonObject top = root.createNestedObject("Klipper Printing Percentage");
top["Enabled"] = enabled;
top["Klipper IP"] = ip;
top["Direction"] = direction;
}
bool readFromConfig(JsonObject &root)
{
// default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor
// setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)
JsonObject top = root["Klipper Printing Percentage"];
bool configComplete = !top.isNull();
configComplete &= getJsonValue(top["Klipper IP"], ip);
configComplete &= getJsonValue(top["Enabled"], enabled);
configComplete &= getJsonValue(top["Direction"], direction);
return configComplete;
}
/*
* 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 infoArr = user.createNestedArray(FPSTR(_name));
String uiDomString = F("<button class=\"btn btn-xs\" onclick=\"requestJson({");
uiDomString += FPSTR(_name);
uiDomString += F(":{");
uiDomString += FPSTR(_enabled);
uiDomString += enabled ? F(":false}});\">") : F(":true}});\">");
uiDomString += F("<i class=\"icons");
uiDomString += enabled ? F(" on") : F(" off");
uiDomString += F("\">&#xe08f;</i>");
uiDomString += F("</button>");
infoArr.add(uiDomString);
}
void addToJsonState(JsonObject &root)
{
JsonObject usermod = root[FPSTR(_name)];
if (usermod.isNull())
{
usermod = root.createNestedObject(FPSTR(_name));
}
usermod["on"] = enabled;
}
void readFromJsonState(JsonObject &root)
{
JsonObject usermod = root[FPSTR(_name)];
if (!usermod.isNull())
{
if (usermod[FPSTR(_enabled)].is<bool>())
{
enabled = usermod[FPSTR(_enabled)].as<bool>();
}
}
}
/*
* handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.
* Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.
* Commonly used for custom clocks (Cronixie, 7 segment)
*/
void handleOverlayDraw()
{
if (enabled)
{
if (direction == 0) // normal
{
for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++)
{
strip.setPixelColor(i, strip.getSegment(0).colors[1]);
}
}
else if (direction == 1) // reversed
{
for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++)
{
strip.setPixelColor(strip.getLengthTotal() - i, strip.getSegment(0).colors[1]);
}
}
else if (direction == 2) // center
{
for (int i = 0; i < (strip.getLengthTotal() / 2) * printPercent / 100; i++)
{
strip.setPixelColor((strip.getLengthTotal() / 2) + i, strip.getSegment(0).colors[1]);
strip.setPixelColor((strip.getLengthTotal() / 2) - i, strip.getSegment(0).colors[1]);
}
}
else
{
direction = 0;
}
}
}
/*
* 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_KLIPPER;
}
};
const char klipper_percentage::_name[] PROGMEM = "Klipper_Percentage";
const char klipper_percentage::_enabled[] PROGMEM = "enabled";

View File

@@ -348,8 +348,10 @@ public:
findCurrentEffectAndPalette();
}
if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) {
currentEffectAndPaletteInitialized = false;
if (modes_alpha_indexes != nullptr) { // WLEDMM bugfix
if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) {
currentEffectAndPaletteInitialized = false;
}
}
if (currentTime - loopTime >= 2) // 2ms since last check of encoder = 500Hz
@@ -469,11 +471,13 @@ public:
void displayNetworkInfo() {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display != nullptr)
display->networkOverlay(PSTR("NETWORK INFO"), 10000);
#endif
}
void findCurrentEffectAndPalette() {
if (modes_alpha_indexes == nullptr) return; // WLEDMM bugfix
currentEffectAndPaletteInitialized = true;
for (uint8_t i = 0; i < strip.getModeCount(); i++) {
if (modes_alpha_indexes[i] == effectCurrent) {
@@ -510,7 +514,8 @@ public:
// 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa
//setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required)
stateUpdated(CALL_MODE_BUTTON);
updateInterfaces(CALL_MODE_BUTTON);
if ((millis() - lastInterfaceUpdate) > INTERFACE_UPDATE_COOLDOWN) // WLEDMM respect cooldown times, to avoid crash in AsyncWebSocketMessageBuffer
updateInterfaces(CALL_MODE_BUTTON);
}
void changeBrightness(bool increase) {
@@ -522,7 +527,10 @@ public:
}
display->updateRedrawTime();
#endif
bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0);
byte lastBri = bri;
if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // WLEDMM slower steps when brightness < 16%
else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0);
if (lastBri != bri) stateChanged = true; // WLEDMM bugfix
lampUdated();
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display->canDraw()) // only draw if nothing else is drawing
@@ -541,7 +549,7 @@ public:
display->updateRedrawTime();
#endif
effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0);
effectCurrent = modes_alpha_indexes[effectCurrentIndex];
if (modes_alpha_indexes != nullptr) effectCurrent = modes_alpha_indexes[effectCurrentIndex];
stateChanged = true;
if (applyToAll) {
for (byte i=0; i<strip.getSegmentsNum(); i++) {
@@ -936,7 +944,7 @@ public:
enabled = false;
return true;
}
setup();
if (enabled) setup(); // WLEDMM no pin stealing!
}
}
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features

View File

@@ -94,7 +94,7 @@ uint16_t mode_2DWeather(void) {
return FRAMETIME;
}
static const char _data_FX_MODE_2DWEATHER[] PROGMEM = "Weather ☾@;!;!;2;pal=54"; //temperature palette
static const char _data_FX_MODE_2DWEATHER[] PROGMEM = "🌦Weather ☾@;!;!;2;pal=54"; //temperature palette
//utility function, move somewhere else???
void epochToString(time_t time, char *timeString) {
@@ -142,14 +142,13 @@ void httpGet(WiFiClient &client, const char *url, char *errorMessage) {
class WeatherUsermod : public Usermod {
private:
// strings to reduce flash memory usage (used more than twice)
static const char _name[]; //usermod name
String apiKey = ""; //config var
unsigned long lastTime = 0; //will be used to download new forecast every hour
char errorMessage[100] = "";
bool isConnected = false; //only call openweathermap if connected
public:
WeatherUsermod(const char *name, bool enabled):Usermod(name, enabled) {} //WLEDMM: this shouldn't be necessary (passthrough of constructor), maybe because Usermod is an abstract class
void setup() {
strip.addEffect(255, &mode_2DWeather, _data_FX_MODE_2DWEATHER);
@@ -256,7 +255,6 @@ class WeatherUsermod : public Usermod {
/*
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
* Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI.
* Below it is shown how this could be used for e.g. a light sensor
*/
void addToJsonInfo(JsonObject& root)
{
@@ -292,7 +290,8 @@ class WeatherUsermod : public Usermod {
void addToConfig(JsonObject& root)
{
JsonObject top = root.createNestedObject(FPSTR(_name));
Usermod::addToConfig(root);
JsonObject top = root[FPSTR(_name)];
top[F("apiKey")] = apiKey;
top[F("units")] = weather_units;
top[F("minTemp")] = weather_minTemp;
@@ -302,10 +301,9 @@ class WeatherUsermod : public Usermod {
bool readFromConfig(JsonObject& root)
{
bool configComplete = Usermod::readFromConfig(root);
JsonObject top = root[FPSTR(_name)];
bool configComplete = !top.isNull();
configComplete &= getJsonValue(top[F("apiKey")], apiKey);
configComplete &= getJsonValue(top[F("units")], weather_units);
configComplete &= getJsonValue(top[F("minTemp")], weather_minTemp);
@@ -317,8 +315,8 @@ class WeatherUsermod : public Usermod {
void appendConfigData()
{
oappend(SET_F("addInfo('Weather:help',0,'<button onclick=\"location.href=&quot;https://mm.kno.wled.ge/moonmodules/Weather&quot;\" type=\"button\">?</button>');"));
oappend(SET_F("addHB('Weather');")); // WLEDMM
oappend(SET_F("dd=addDropdown('Weather','units');"));
oappend(SET_F("addOption(dd,'Kelvin',0);"));
oappend(SET_F("addOption(dd,'Celcius',1);"));
@@ -349,9 +347,6 @@ class WeatherUsermod : public Usermod {
}
};
// strings to reduce flash memory usage (used more than twice)
const char WeatherUsermod::_name[] PROGMEM = "Weather";
// example openweathermap data
// {"cod":"200","message":0,"cnt":40,"list":[
// {"dt":1663945200,"main":{"temp":18.05,"feels_like":17.79,"temp_min":17.64,"temp_max":18.05,"pressure":1014,"sea_level":1014,"grnd_level":1013,"humidity":72,"temp_kf":0.41},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":100},"wind":{"speed":4.32,"deg":238,"gust":5.6},"visibility":10000,"pop":0.48,"rain":{"3h":0.15},"sys":{"pod":"d"},"dt_txt":"2022-09-23 15:00:00"},