Compare commits

...

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
b6cea79cc0 Use ternary operator for button pin assignment as suggested by @DedeHai
Simplified fix by using `request->hasArg(bt) ? request->arg(bt).toInt() : -1` instead of break statement. This ensures non-existent button parameters default to -1 instead of 0.

Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-02 11:34:35 +00:00
copilot-swe-agent[bot]
7f395ea350 Fix button auto-creation when saving empty button list
When user removes all buttons and saves, the form handler was processing all WLED_MAX_BUTTONS iterations without checking if parameters exist. Empty request->arg().toInt() returns 0, causing GPIO0 to be auto-assigned. Also fixed cleanup loop to include button index 0.

Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-02 10:10:54 +00:00
copilot-swe-agent[bot]
ff5602840d Fix GPIO0 button pin conflict detection in settings_leds.htm
Modified pin conflict detection to check if buttons are enabled before considering their GPIO pins as "in use". Disabled buttons (type = 0) no longer block GPIO reassignment to LED outputs.

Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-01 19:49:17 +00:00
copilot-swe-agent[bot]
9066a34ca5 Initial plan 2026-01-01 19:44:19 +00:00
Will Tatam
979e3fd1f7 Update GitHub sponsors list in FUNDING.yml
Maintain PayPal link, but sponsor is not appropriate for someone who isn't actively contributing going forward
2026-01-01 11:33:26 +00:00
Will Tatam
600b9b4d8e Update funding sources in FUNDING.yml
Removed a contributor from the GitHub funding list and updated the custom funding sources.
2026-01-01 11:29:50 +00:00
Damian Schneider
787d8a7342 fix FX checkmark sync (#5239)
this fixes an ancient copy-paste bug that apparently went under the radar for years. Now FX checkmarks sync correctly.
2025-12-29 14:54:38 +01:00
Damian Schneider
46e77ea203 revert change to extractModeName, add comment for clarification 2025-12-29 14:50:58 +01:00
Damian Schneider
fa868568af minor bugfixes as suggested by the rabbit
- PulLightControl UM: dont release a lock you do not own
- off-by-one error in extractModeSlider (used only in rotary in UM)
- safety check in playlist in case something goes horribly wrong
2025-12-29 12:56:06 +01:00
Damian Schneider
1c2cacf185 adding link to WLED VidoLab 2025-12-27 19:27:35 +01:00
Damian Schneider
f1f067e93a bugfix in particle collision binning 2025-12-27 09:37:09 +01:00
nename0
8a2a7054ab Usermod Temperature: use full 12-bit precision (#4916)
* Usermod Temperature: use full 12-bit precision

* Temperature Usermod: fix DS1822 and DS18B20 family code

* Add Dropdown to select 9 or 12 bit resolution

* Add 10 and 11 bit resolution, Correct rounding towards negativ inf
2025-12-26 15:32:24 -05:00
11 changed files with 84 additions and 31 deletions

5
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
thanks_dev: u/gh/netmindz
github: [DedeHai,lost-hope,willmmiles,netmindz]
custom: ['https://paypal.me/Aircoookie']

View File

@@ -20,17 +20,19 @@ float UsermodTemperature::readDallas() {
}
#endif
switch(sensorFound) {
case 0x10: // DS18S20 has 9-bit precision
case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part
result = (data[1] << 8) | data[0];
retVal = float(result) * 0.5f;
break;
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x22: // DS1822
case 0x28: // DS18B20
case 0x3B: // DS1825
case 0x42: // DS28EA00
result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning
if (data[1] & 0x80) result |= 0xF000; // fix negative value
retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f);
// 12-bit precision - 4-bit fraction part
result = (data[1] << 8) | data[0];
// Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity
result &= 0xFFFF << (3 - (resolution & 3));
retVal = float(result) * 0.0625f; // 2^(-4)
break;
}
}
@@ -69,8 +71,8 @@ bool UsermodTemperature::findSensor() {
if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {
switch (deviceAddress[0]) {
case 0x10: // DS18S20
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x22: // DS1822
case 0x28: // DS18B20
case 0x3B: // DS1825
case 0x42: // DS28EA00
DEBUG_PRINTLN(F("Sensor found."));
@@ -277,6 +279,7 @@ void UsermodTemperature::addToConfig(JsonObject &root) {
top[FPSTR(_parasite)] = parasite;
top[FPSTR(_parasitePin)] = parasitePin;
top[FPSTR(_domoticzIDX)] = idx;
top[FPSTR(_resolution)] = resolution;
DEBUG_PRINTLN(F("Temperature config saved."));
}
@@ -304,6 +307,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
parasite = top[FPSTR(_parasite)] | parasite;
parasitePin = top[FPSTR(_parasitePin)] | parasitePin;
idx = top[FPSTR(_domoticzIDX)] | idx;
resolution = top[FPSTR(_resolution)] | resolution;
if (!initDone) {
// first run: reading from cfg.json
@@ -324,7 +328,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
}
}
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
return !top[FPSTR(_domoticzIDX)].isNull();
return !top[FPSTR(_resolution)].isNull();
}
void UsermodTemperature::appendConfigData() {
@@ -332,6 +336,14 @@ void UsermodTemperature::appendConfigData() {
oappend(F("',1,'<i>(if no Vcc connected)</i>');")); // 0 is field type, 1 is actual field
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str());
oappend(F("',1,'<i>(for external MOSFET)</i>');")); // 0 is field type, 1 is actual field
oappend(F("dd=addDD('")); oappend(String(FPSTR(_name)).c_str());
oappend(F("','")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("');"));
oappend(F("addO(dd,'0.5 °C (9-bit)',0);"));
oappend(F("addO(dd,'0.25°C (10-bit)',1);"));
oappend(F("addO(dd,'0.125°C (11-bit)',2);"));
oappend(F("addO(dd,'0.0625°C (12-bit)',3);"));
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_resolution)).c_str());
oappend(F("',1,'<i>(ignored on DS18S20)</i>');")); // 0 is field type, 1 is actual field
}
float UsermodTemperature::getTemperature() {
@@ -351,6 +363,7 @@ const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s";
const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr";
const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin";
const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx";
const char UsermodTemperature::_resolution[] PROGMEM = "resolution";
const char UsermodTemperature::_sensor[] PROGMEM = "sensor";
const char UsermodTemperature::_temperature[] PROGMEM = "temperature";
const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature";

View File

@@ -48,6 +48,7 @@ class UsermodTemperature : public Usermod {
bool HApublished = false;
int16_t idx = -1; // Domoticz virtual sensor idx
uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
@@ -56,6 +57,7 @@ class UsermodTemperature : public Usermod {
static const char _parasite[];
static const char _parasitePin[];
static const char _domoticzIDX[];
static const char _resolution[];
static const char _sensor[];
static const char _temperature[];
static const char _Temperature[];

View File

@@ -284,7 +284,6 @@ void HttpPullLightControl::handleResponse(String& responseStr) {
if (!requestJSONBufferLock(myLockId)) {
DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: "));
DEBUG_PRINTLN(myLockId);
releaseJSONBufferLock(); // Just release in any case, maybe there was already a buffer lock
return;
}

View File

@@ -156,7 +156,7 @@ void ParticleSystem2D::setParticleSize(uint8_t size) {
particleHardRadius = PS_P_MINHARDRADIUS + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking)
}
else if (particlesize == 0)
particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
particleHardRadius = PS_P_MINHARDRADIUS >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
}
// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
@@ -595,7 +595,7 @@ void ParticleSystem2D::render() {
if (fireIntesity) { // fire mode
brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 5;
brightness = min(brightness, (uint32_t)255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); // map hue to brightness for fire effect
}
else {
brightness = min((particles[i].ttl << 1), (int)255);
@@ -842,7 +842,7 @@ void ParticleSystem2D::handleCollisions() {
for (uint32_t bin = 0; bin < numBins; bin++) {
binParticleCount = 0; // reset for this bin
int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
int32_t binEnd = binStart + binWidth + overlap; // note: last bin can be out of bounds, see above;
int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above;
// fill the binIndices array for this bin
for (uint32_t i = 0; i < usedParticles; i++) {
@@ -879,7 +879,7 @@ void ParticleSystem2D::handleCollisions() {
massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1
massratio2 = (mass1 << 8) / totalmass;
}
// note: using the same logic as in 1D is much slower though it would be more accurate but it is not really needed in 2D
// note: using the same logic as in 1D is much slower though it would be more accurate but it is not really needed in 2D: particles slipping through each other is much less visible
int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead
if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare)
int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead
@@ -1247,7 +1247,7 @@ void ParticleSystem1D::setParticleSize(const uint8_t size) {
particleHardRadius = PS_P_MINHARDRADIUS_1D + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking)
}
else if (particlesize == 0)
particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
}
// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
@@ -1632,7 +1632,7 @@ void ParticleSystem1D::handleCollisions() {
for (uint32_t bin = 0; bin < numBins; bin++) {
binParticleCount = 0; // reset for this bin
int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
int32_t binEnd = binStart + binWidth + overlap; // note: last bin can be out of bounds, see above
int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above
// fill the binIndices array for this bin
for (uint32_t i = 0; i < usedParticles; i++) {

View File

@@ -397,11 +397,19 @@ button, .btn {
</div>
</div>
<hr>
<div class="ed active">
<div>
<h3>Video Lab</h3>
<div><small>Stream video and generate animated GIFs (beta)</small></div>
<button class="btn" id="t2" style="display:none"></button>
</div>
</div>
<hr>
<div class="ed active">
<div>
<h3>PIXEL MAGIC Tool</h3>
<div><small>Legacy pixel art editor</small></div>
<button class="btn" id="t2" style="display:none"></button>
<button class="btn" id="t3" style="display:none"></button>
</div>
</div>
<hr>
@@ -439,7 +447,8 @@ let fL; // file list
await segLoad(); // load available segments
await flU(); // update file list
toolChk('pixelpaint.htm','t1'); // update buttons of additional tools
toolChk('pxmagic.htm','t2');
toolChk('videolab.htm','t2');
toolChk('pxmagic.htm','t3');
await fsMem(); // show file system memory info
})();

View File

@@ -85,7 +85,22 @@
//check for pin conflicts
if (LC.value!="" && LC.value!="-1") {
let p = d.rsvd.concat(d.um_p); // used pin array
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
d.Sf.querySelectorAll("select.pin").forEach((e)=>{
if(e.value>-1) {
// Check if this is a button pin (name starts with "BT")
if (e.name.startsWith("BT")) {
// Get button type field (BEx where BTx is the pin field)
let btnTypeField = d.Sf["BE" + e.name.substring(2)];
// Only add to used pins if button is not disabled (type != 0)
if (btnTypeField && btnTypeField.value != "0") {
p.push(parseInt(e.value));
}
} else {
// For IR and Relay pins, add normally
p.push(parseInt(e.value));
}
}
}) // buttons, IR & relay
if (p.some((e)=>e==parseInt(LC.value))) {
alert(`Sorry, pins ${JSON.stringify(p)} can't be used.`);
LC.value="";
@@ -387,7 +402,22 @@
if (nm.search(/^L[0-4]/) == 0) // pin fields
if (LC.value!="" && LC.value!="-1") {
let p = d.rsvd.concat(d.um_p); // used pin array
d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay
d.Sf.querySelectorAll("select.pin").forEach((e)=>{
if(e.value>-1) {
// Check if this is a button pin (name starts with "BT")
if (e.name.startsWith("BT")) {
// Get button type field (BEx where BTx is the pin field)
let btnTypeField = d.Sf["BE" + e.name.substring(2)];
// Only add to used pins if button is not disabled (type != 0)
if (btnTypeField && btnTypeField.value != "0") {
p.push(parseInt(e.value));
}
} else {
// For IR and Relay pins, add normally
p.push(parseInt(e.value));
}
}
}) // buttons, IR & relay
for (j=0; j<nList.length; j++) {
if (i==j) continue;
let n2 = nList[j].name.substring(0,2); // field name : /L./

View File

@@ -89,7 +89,8 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) {
it++;
}
}
for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur;
if (it > 0) // should never happen but just in case
for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur;
it = 0;
JsonArray tr = playlistObj[F("transition")];

View File

@@ -279,7 +279,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
int offset = i < 10 ? '0' : 'A' - 10;
char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)
char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)
int hw_btn_pin = request->arg(bt).toInt();
int hw_btn_pin = request->hasArg(bt) ? request->arg(bt).toInt() : -1;
if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector
else {
buttons[i].pin = hw_btn_pin;
@@ -327,7 +327,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
}
}
// we should remove all unused buttons from the vector
for (int i = buttons.size()-1; i > 0; i--) {
for (int i = buttons.size()-1; i >= 0; i--) {
if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {
buttons.erase(buttons.begin() + i); // remove button from vector
}

View File

@@ -334,8 +334,8 @@ static void parseNotifyPacket(const uint8_t *udpIn) {
selseg.custom2 = udpIn[30+ofs];
selseg.custom3 = udpIn[31+ofs] & 0x1F;
selseg.check1 = (udpIn[31+ofs]>>5) & 0x1;
selseg.check1 = (udpIn[31+ofs]>>6) & 0x1;
selseg.check1 = (udpIn[31+ofs]>>7) & 0x1;
selseg.check2 = (udpIn[31+ofs]>>6) & 0x1;
selseg.check3 = (udpIn[31+ofs]>>7) & 0x1;
}
}
if (receiveSegmentBounds) {

View File

@@ -213,7 +213,7 @@ void releaseJSONBufferLock()
// extracts effect mode (or palette) name from names serialized string
// caller must provide large enough buffer for name (including SR extensions)!
// caller must provide large enough buffer for name (including SR extensions)! maxLen is (buffersize - 1)
uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen)
{
if (src == JSON_mode_names || src == nullptr) {
@@ -235,7 +235,7 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe
if (src == JSON_palette_names && mode > 255-customPalettes.size()) {
snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode);
dest[maxLen-1] = '\0';
dest[maxLen] = '\0';
return strlen(dest);
}
@@ -336,7 +336,7 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL
case 0: strncpy_P(dest, PSTR("FX Speed"), maxLen); break;
case 1: strncpy_P(dest, PSTR("FX Intensity"), maxLen); break;
}
dest[maxLen] = '\0'; // strncpy does not necessarily null terminate string
dest[maxLen-1] = '\0'; // strncpy does not necessarily null terminate string
}
}
return strlen(dest);