From f76db1225ca3c9f596fb1a8a8b8599476be6917d Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Sun, 16 Aug 2026 18:48:31 +0200 Subject: [PATCH 1/6] fix(button): calibrate NORVI ADC thresholds and make them configurable Measured ADC levels on the NORVI AE01-R: no press ~2610-2990 (oscillates), S1 ~3275-3456, S2 ~3579-3765, S3 ~4095 (full scale). The old 2700/3800 boundaries sat inside the resting oscillation, so phantom S3 presses repeatedly switched the operation mode. - Calibrate thresholds to measured levels (B1 3100-3520, B2 3520-3880, B3 3880-4095, no-press 4096 as no-op since S3 reads full scale) - Make thresholds configurable via NVS (ConfigManager) with the calibrated values as defaults, editable in the web UI Pool settings tab --- data/web/app.js | 16 ++++++++++++- data/web/index.html | 48 ++++++++++++++++++++++++++++++++++++++ src/ConfigManager.cpp | 21 +++++++++++++++++ src/ConfigManager.hpp | 7 ++++++ src/NorviButtonHandler.cpp | 22 ++++++++++++++++- src/NorviButtonHandler.hpp | 38 ++++++++++++++++++------------ src/WebPortal.cpp | 21 +++++++++++++++++ 7 files changed, 156 insertions(+), 17 deletions(-) diff --git a/data/web/app.js b/data/web/app.js index 1997d9cf..5198b92b 100644 --- a/data/web/app.js +++ b/data/web/app.js @@ -589,6 +589,13 @@ async function saveControllerSettings() { const circFactor = document.getElementById('tempCircFactor').value; const circMaxRuntime = document.getElementById('tempCircMaxRuntime').value; const tz = document.getElementById('timezone').value; + const btn1Min = document.getElementById('btn1Min').value; + const btn1Max = document.getElementById('btn1Max').value; + const btn2Min = document.getElementById('btn2Min').value; + const btn2Max = document.getElementById('btn2Max').value; + const btn3Min = document.getElementById('btn3Min').value; + const btn3Max = document.getElementById('btn3Max').value; + const btnNoPress = document.getElementById('btnNoPress').value; // Validate time fields included alongside pool fields const green = parseInt(document.getElementById('timeLossGreen').value, 10); @@ -608,7 +615,7 @@ async function saveControllerSettings() { const res = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'type=settings&mode=' + mode + '&interval=' + interval + '&max_pool=' + maxPool + '&min_solar=' + minSolar + '&hysteresis=' + hysteresis + '&circ_threshold=' + circThreshold + '&circ_factor=' + circFactor + '&circ_max_runtime=' + circMaxRuntime + '&timezone=' + tz + '&green=' + green + '&red=' + red + timerParams() + '&ntp_server=' + ntpServer + body: 'type=settings&mode=' + mode + '&interval=' + interval + '&max_pool=' + maxPool + '&min_solar=' + minSolar + '&hysteresis=' + hysteresis + '&circ_threshold=' + circThreshold + '&circ_factor=' + circFactor + '&circ_max_runtime=' + circMaxRuntime + '&timezone=' + tz + '&green=' + green + '&red=' + red + timerParams() + '&ntp_server=' + ntpServer + '&btn1_min=' + btn1Min + '&btn1_max=' + btn1Max + '&btn2_min=' + btn2Min + '&btn2_max=' + btn2Max + '&btn3_min=' + btn3Min + '&btn3_max=' + btn3Max + '&btn_no_press=' + btnNoPress }); if (res.status === 200) { document.getElementById('poolThreshold').textContent = 'max ' + parseFloat(maxPool).toFixed(1) + 'ยฐC'; @@ -729,6 +736,13 @@ async function loadConfig() { document.getElementById('ntpServer').value = data.ntp.server; document.getElementById('timeLossGreen').value = data.settings.time_loss_green_hours; document.getElementById('timeLossRed').value = data.settings.time_loss_red_hours; + document.getElementById('btn1Min').value = data.settings.btn1_min; + document.getElementById('btn1Max').value = data.settings.btn1_max; + document.getElementById('btn2Min').value = data.settings.btn2_min; + document.getElementById('btn2Max').value = data.settings.btn2_max; + document.getElementById('btn3Min').value = data.settings.btn3_min; + document.getElementById('btn3Max').value = data.settings.btn3_max; + document.getElementById('btnNoPress').value = data.settings.btn_no_press; const pad2 = (n) => n.toString().padStart(2, '0'); document.getElementById('timerStart').value = pad2(data.settings.timer_start_hour) + ':' + pad2(data.settings.timer_start_min); document.getElementById('timerEnd').value = pad2(data.settings.timer_end_hour) + ':' + pad2(data.settings.timer_end_min); diff --git a/data/web/index.html b/data/web/index.html index 09cccd07..c3444524 100644 --- a/data/web/index.html +++ b/data/web/index.html @@ -307,6 +307,54 @@

๐ŸŒก๏ธ Temperature-Based Circulation

+

๐Ÿ”˜ Button Thresholds (NORVI)

+
+
+ + + Lower bound of Button 1 range ยท Measured: ~3275โ€“3456 +
+
+ + + Upper bound of Button 1 range +
+
+
+
+ + + Lower bound of Button 2 range ยท Measured: ~3579โ€“3765 +
+
+ + + Upper bound of Button 2 range +
+
+
+
+ + + Lower bound of Button 3 range ยท Measured: ~4095 (full scale) +
+
+ + + Upper bound of Button 3 range +
+
+
+
+ + + Values above this are ignored ยท 4096 = no-op (S3 reads full scale) +
+ +
+

โฑ๏ธ Timer Schedule

diff --git a/src/ConfigManager.cpp b/src/ConfigManager.cpp index a50b6078..0c557be1 100644 --- a/src/ConfigManager.cpp +++ b/src/ConfigManager.cpp @@ -68,6 +68,13 @@ static constexpr const char *kSetRed = "set_red"; static constexpr const char *kSetCircThresh = "set_circth"; static constexpr const char *kSetCircFactor = "set_circfa"; static constexpr const char *kSetCircMax = "set_circmx"; +static constexpr const char *kSetBtn1Min = "set_btn1min"; +static constexpr const char *kSetBtn1Max = "set_btn1max"; +static constexpr const char *kSetBtn2Min = "set_btn2min"; +static constexpr const char *kSetBtn2Max = "set_btn2max"; +static constexpr const char *kSetBtn3Min = "set_btn3min"; +static constexpr const char *kSetBtn3Max = "set_btn3max"; +static constexpr const char *kSetBtnNoPress = "set_btnnop"; static constexpr const char *kAdmPass = "adm_pass"; static constexpr const char *kCfgConfigured = "cfg_configured"; @@ -108,6 +115,13 @@ bool ConfigManager::load() { settings_.tempCircThreshold = prefs.getDouble(kSetCircThresh, 24.0); settings_.tempCircFactor = prefs.getUShort(kSetCircFactor, 30); settings_.tempCircMaxRuntime = prefs.getUShort(kSetCircMax, 720); + settings_.btn1Min = prefs.getUShort(kSetBtn1Min, 3100); + settings_.btn1Max = prefs.getUShort(kSetBtn1Max, 3520); + settings_.btn2Min = prefs.getUShort(kSetBtn2Min, 3520); + settings_.btn2Max = prefs.getUShort(kSetBtn2Max, 3880); + settings_.btn3Min = prefs.getUShort(kSetBtn3Min, 3880); + settings_.btn3Max = prefs.getUShort(kSetBtn3Max, 4095); + settings_.btnNoPress = prefs.getUShort(kSetBtnNoPress, 4096); adminPasswordHash_ = prefs.getString(kAdmPass, kDefaultPasswordHash); configured_ = prefs.getBool(kCfgConfigured, false); @@ -147,6 +161,13 @@ bool ConfigManager::save() { prefs.putDouble(kSetCircThresh, settings_.tempCircThreshold); prefs.putUShort(kSetCircFactor, settings_.tempCircFactor); prefs.putUShort(kSetCircMax, settings_.tempCircMaxRuntime); + prefs.putUShort(kSetBtn1Min, settings_.btn1Min); + prefs.putUShort(kSetBtn1Max, settings_.btn1Max); + prefs.putUShort(kSetBtn2Min, settings_.btn2Min); + prefs.putUShort(kSetBtn2Max, settings_.btn2Max); + prefs.putUShort(kSetBtn3Min, settings_.btn3Min); + prefs.putUShort(kSetBtn3Max, settings_.btn3Max); + prefs.putUShort(kSetBtnNoPress, settings_.btnNoPress); prefs.putString(kAdmPass, adminPasswordHash_); prefs.putBool(kCfgConfigured, configured_); diff --git a/src/ConfigManager.hpp b/src/ConfigManager.hpp index 4865bd2d..dbff28e5 100644 --- a/src/ConfigManager.hpp +++ b/src/ConfigManager.hpp @@ -46,6 +46,13 @@ struct ControllerSettings { long timeLossGreenHours = 1; long timeLossRedHours = 24; int timezoneIndex = 0; ///< Index into TimeClientHelper timezone table + uint16_t btn1Min = 3100; ///< NORVI button 1 ADC range min + uint16_t btn1Max = 3520; ///< NORVI button 1 ADC range max + uint16_t btn2Min = 3520; ///< NORVI button 2 ADC range min + uint16_t btn2Max = 3880; ///< NORVI button 2 ADC range max + uint16_t btn3Min = 3880; ///< NORVI button 3 ADC range min + uint16_t btn3Max = 4095; ///< NORVI button 3 ADC range max + uint16_t btnNoPress = 4096; ///< NORVI no-press threshold (no-op, > ADC max) }; /** diff --git a/src/NorviButtonHandler.cpp b/src/NorviButtonHandler.cpp index bdbcaeb6..470a3677 100644 --- a/src/NorviButtonHandler.cpp +++ b/src/NorviButtonHandler.cpp @@ -19,6 +19,7 @@ #include #include "Config.hpp" +#include "ConfigManager.hpp" #include "LogCapture.hpp" namespace PoolController { @@ -51,6 +52,16 @@ NorviButtonHandler::ButtonLongPressCallback NorviButtonHandler::cbButton3Long_ = uint32_t NorviButtonHandler::pressStartMs_ = 0; uint32_t NorviButtonHandler::releasePendingMs_ = 0; +// ADC thresholds โ€” defaults are the 2026-08-16 calibrated values; begin() +// overwrites them from ConfigManager (NVS) so they are user-configurable. +uint16_t NorviButtonHandler::THRESH_BTN1_MIN{3100}; +uint16_t NorviButtonHandler::THRESH_BTN1_MAX{3520}; +uint16_t NorviButtonHandler::THRESH_BTN2_MIN{3520}; +uint16_t NorviButtonHandler::THRESH_BTN2_MAX{3880}; +uint16_t NorviButtonHandler::THRESH_BTN3_MIN{3880}; +uint16_t NorviButtonHandler::THRESH_BTN3_MAX{4095}; +uint16_t NorviButtonHandler::THRESH_NO_PRESS{4096}; + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• void NorviButtonHandler::begin() { @@ -58,6 +69,15 @@ void NorviButtonHandler::begin() { pinMode(PIN_BUTTON_ADC, INPUT); + // Load configurable ADC thresholds from NVS (defaults = calibrated values) + THRESH_BTN1_MIN = ConfigManager::getSettings().btn1Min; + THRESH_BTN1_MAX = ConfigManager::getSettings().btn1Max; + THRESH_BTN2_MIN = ConfigManager::getSettings().btn2Min; + THRESH_BTN2_MAX = ConfigManager::getSettings().btn2Max; + THRESH_BTN3_MIN = ConfigManager::getSettings().btn3Min; + THRESH_BTN3_MAX = ConfigManager::getSettings().btn3Max; + THRESH_NO_PRESS = ConfigManager::getSettings().btnNoPress; + // Take an initial sample to let the ADC stabilise analogRead(PIN_BUTTON_ADC); delay(10); @@ -89,7 +109,7 @@ void NorviButtonHandler::loop() { static uint32_t lastStableTime_ = 0; // Fast-attack: a genuine press/release changes the button range โ€” even - // when the ADC delta is small (no-press 3800 โ†’ S3 3700 = 100). Base the + // when the ADC delta is small (no-press ~2700 โ†’ S1 ~3400 = 700). Base the // re-initialization on button-range transitions so every valid press is // caught within one sample instead of a full moving-average window. The // transition also restarts the stability window, so a single-sample glitch diff --git a/src/NorviButtonHandler.hpp b/src/NorviButtonHandler.hpp index 0cc53775..36bd5b90 100644 --- a/src/NorviButtonHandler.hpp +++ b/src/NorviButtonHandler.hpp @@ -36,11 +36,11 @@ namespace PoolController { * ~50 ms intervals, applies hysteresis for noise rejection, and fires * callbacks on press events. * - * Default thresholds (12-bit ADC, 0โ€“4095): - * Button 1: 200 โ€“ 1200 - * Button 2: 1400 โ€“ 2500 - * Button 3: 2700 โ€“ 3700 - * No press: > 3800 + * Calibrated thresholds (12-bit ADC, 0โ€“4095) from live measurements: + * No press: < 3100 (resting level oscillates ~2610โ€“2990) + * Button 1: 3100 โ€“ 3520 + * Button 2: 3520 โ€“ 3880 + * Button 3: 3880 โ€“ 4095 */ class NorviButtonHandler { public: @@ -172,16 +172,24 @@ class NorviButtonHandler { static uint32_t releasePendingMs_; // โ”€โ”€ ADC thresholds (12-bit, 0โ€“4095) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - // These are typical ranges for the NORVI AE01-R resistor ladder. - // Adjust if needed based on serial debug output. - - static constexpr uint16_t THRESH_BTN1_MIN{200}; - static constexpr uint16_t THRESH_BTN1_MAX{1200}; - static constexpr uint16_t THRESH_BTN2_MIN{1400}; - static constexpr uint16_t THRESH_BTN2_MAX{2500}; - static constexpr uint16_t THRESH_BTN3_MIN{2700}; - static constexpr uint16_t THRESH_BTN3_MAX{3700}; - static constexpr uint16_t THRESH_NO_PRESS{3800}; + // Calibrated from live measurements on the NORVI AE01-R (2026-08-16): + // No press: ~2610โ€“2990 (oscillates; must map to NONE) + // Button 1: ~3275โ€“3456 + // Button 2: ~3579โ€“3765 + // Button 3: ~4095 (full scale) + // The resistor ladder pulls the ADC pin UP toward VCC on press. + // Boundaries sit at the midpoints between adjacent levels. + // THRESH_NO_PRESS is a no-op (4096 > ADC max) โ€” S3 reads full scale. + // Values are loaded from ConfigManager (NVS) in begin(); the defaults + // below are the calibrated values and can be overridden via the web UI. + + static uint16_t THRESH_BTN1_MIN; + static uint16_t THRESH_BTN1_MAX; + static uint16_t THRESH_BTN2_MIN; + static uint16_t THRESH_BTN2_MAX; + static uint16_t THRESH_BTN3_MIN; + static uint16_t THRESH_BTN3_MAX; + static uint16_t THRESH_NO_PRESS; }; } // namespace PoolController diff --git a/src/WebPortal.cpp b/src/WebPortal.cpp index c49c2b8e..73c265cc 100644 --- a/src/WebPortal.cpp +++ b/src/WebPortal.cpp @@ -666,6 +666,13 @@ void WebPortal::apiGetConfig() { settingsObj["timezone"] = ConfigManager::getSettings().timezoneIndex; settingsObj["time_loss_green_hours"] = ConfigManager::getSettings().timeLossGreenHours; settingsObj["time_loss_red_hours"] = ConfigManager::getSettings().timeLossRedHours; + settingsObj["btn1_min"] = ConfigManager::getSettings().btn1Min; + settingsObj["btn1_max"] = ConfigManager::getSettings().btn1Max; + settingsObj["btn2_min"] = ConfigManager::getSettings().btn2Min; + settingsObj["btn2_max"] = ConfigManager::getSettings().btn2Max; + settingsObj["btn3_min"] = ConfigManager::getSettings().btn3Min; + settingsObj["btn3_max"] = ConfigManager::getSettings().btn3Max; + settingsObj["btn_no_press"] = ConfigManager::getSettings().btnNoPress; settingsObj["timer_start_hour"] = operationModeNode.getTimerSetting().timerStartHour; settingsObj["timer_start_min"] = operationModeNode.getTimerSetting().timerStartMinutes; settingsObj["timer_end_hour"] = operationModeNode.getTimerSetting().timerEndHour; @@ -750,6 +757,20 @@ void WebPortal::apiSaveConfig() { ConfigManager::getSettings().tempCircFactor = server_.arg("circ_factor").toInt(); if (server_.hasArg("circ_max_runtime")) ConfigManager::getSettings().tempCircMaxRuntime = server_.arg("circ_max_runtime").toInt(); + if (server_.hasArg("btn1_min")) + ConfigManager::getSettings().btn1Min = server_.arg("btn1_min").toInt(); + if (server_.hasArg("btn1_max")) + ConfigManager::getSettings().btn1Max = server_.arg("btn1_max").toInt(); + if (server_.hasArg("btn2_min")) + ConfigManager::getSettings().btn2Min = server_.arg("btn2_min").toInt(); + if (server_.hasArg("btn2_max")) + ConfigManager::getSettings().btn2Max = server_.arg("btn2_max").toInt(); + if (server_.hasArg("btn3_min")) + ConfigManager::getSettings().btn3Min = server_.arg("btn3_min").toInt(); + if (server_.hasArg("btn3_max")) + ConfigManager::getSettings().btn3Max = server_.arg("btn3_max").toInt(); + if (server_.hasArg("btn_no_press")) + ConfigManager::getSettings().btnNoPress = server_.arg("btn_no_press").toInt(); ConfigManager::getSettings().timezoneIndex = server_.arg("timezone").toInt(); ConfigManager::getSettings().timeLossGreenHours = server_.arg("green").toInt(); ConfigManager::getSettings().timeLossRedHours = server_.arg("red").toInt(); From 5cc5a1a2a6af93ff700f186cd188b571e46a0d9e Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Sun, 16 Aug 2026 21:22:47 +0200 Subject: [PATCH 2/6] fix(web): apply button thresholds to running handler The settings handler updated ConfigManager but NorviButtonHandler only copied the thresholds in begin(), so changes required a reboot. Add NorviButtonHandler::applySettings() and call it after saving settings. Addresses PR #181 review comment. --- src/NorviButtonHandler.cpp | 22 ++++++++++++++-------- src/NorviButtonHandler.hpp | 7 +++++++ src/WebPortal.cpp | 10 ++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/NorviButtonHandler.cpp b/src/NorviButtonHandler.cpp index 470a3677..d6a6a681 100644 --- a/src/NorviButtonHandler.cpp +++ b/src/NorviButtonHandler.cpp @@ -69,14 +69,7 @@ void NorviButtonHandler::begin() { pinMode(PIN_BUTTON_ADC, INPUT); - // Load configurable ADC thresholds from NVS (defaults = calibrated values) - THRESH_BTN1_MIN = ConfigManager::getSettings().btn1Min; - THRESH_BTN1_MAX = ConfigManager::getSettings().btn1Max; - THRESH_BTN2_MIN = ConfigManager::getSettings().btn2Min; - THRESH_BTN2_MAX = ConfigManager::getSettings().btn2Max; - THRESH_BTN3_MIN = ConfigManager::getSettings().btn3Min; - THRESH_BTN3_MAX = ConfigManager::getSettings().btn3Max; - THRESH_NO_PRESS = ConfigManager::getSettings().btnNoPress; + applySettings(); // Take an initial sample to let the ADC stabilise analogRead(PIN_BUTTON_ADC); @@ -92,6 +85,19 @@ void NorviButtonHandler::begin() { // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +void NorviButtonHandler::applySettings() { + // Load configurable ADC thresholds from NVS (defaults = calibrated values) + THRESH_BTN1_MIN = ConfigManager::getSettings().btn1Min; + THRESH_BTN1_MAX = ConfigManager::getSettings().btn1Max; + THRESH_BTN2_MIN = ConfigManager::getSettings().btn2Min; + THRESH_BTN2_MAX = ConfigManager::getSettings().btn2Max; + THRESH_BTN3_MIN = ConfigManager::getSettings().btn3Min; + THRESH_BTN3_MAX = ConfigManager::getSettings().btn3Max; + THRESH_NO_PRESS = ConfigManager::getSettings().btnNoPress; +} + +// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + void NorviButtonHandler::loop() { const uint32_t now = millis(); diff --git a/src/NorviButtonHandler.hpp b/src/NorviButtonHandler.hpp index 36bd5b90..fd2ab3b8 100644 --- a/src/NorviButtonHandler.hpp +++ b/src/NorviButtonHandler.hpp @@ -62,6 +62,13 @@ class NorviButtonHandler { */ static void begin(); + /** + * @brief Reload ADC thresholds from ConfigManager (NVS). + * Called after settings changes so new thresholds apply to the running + * handler without a reboot. + */ + static void applySettings(); + /** * @brief Sample and debounce buttons. * Must be called regularly from PoolController::loop(). diff --git a/src/WebPortal.cpp b/src/WebPortal.cpp index 73c265cc..8fa4a59c 100644 --- a/src/WebPortal.cpp +++ b/src/WebPortal.cpp @@ -40,6 +40,10 @@ #include "Version.h" #include "LogCapture.hpp" +#ifdef NORVI_AE01_R +#include "NorviButtonHandler.hpp" +#endif + namespace PoolController { // File-scoped state for streaming LittleFS upload (used by handleFsUploadStream) @@ -777,6 +781,12 @@ void WebPortal::apiSaveConfig() { ConfigManager::save(); +#ifdef NORVI_AE01_R + // Apply button thresholds to the running handler immediately so the + // new values take effect without a reboot (P2 review fix). + NorviButtonHandler::applySettings(); +#endif + // Apply timezone change to running clock immediately (P2) setTimezoneIndex(ConfigManager::getSettings().timezoneIndex); From 3f32e6690637e1e397b007c1be7140670e1ed09f Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Sun, 16 Aug 2026 21:22:47 +0200 Subject: [PATCH 3/6] fix(web): validate button threshold inputs Empty or out-of-range threshold fields were stored as 0 by toInt(), reintroducing phantom presses. Validate presence, 0-4095 range, and coherent non-overlapping min/max ranges before saving. Addresses PR #181 review comment. --- data/web/app.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/data/web/app.js b/data/web/app.js index 5198b92b..e5f5875f 100644 --- a/data/web/app.js +++ b/data/web/app.js @@ -553,6 +553,13 @@ function validateSettings() { { id: 'tempCircThreshold', name: 'Circ. Temp Threshold', min: 0, max: 40, type: 'float' }, { id: 'tempCircFactor', name: 'Circ. Temp Factor', min: 0, max: 120, type: 'int' }, { id: 'tempCircMaxRuntime', name: 'Circ. Max Runtime', min: 60, max: 1440, type: 'int' }, + { id: 'btn1Min', name: 'Button 1 Min ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn1Max', name: 'Button 1 Max ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn2Min', name: 'Button 2 Min ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn2Max', name: 'Button 2 Max ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn3Min', name: 'Button 3 Min ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn3Max', name: 'Button 3 Max ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btnNoPress', name: 'No-Press Threshold', min: 0, max: 4095, type: 'int' }, ]; for (const f of fields) { const el = document.getElementById(f.id); @@ -572,6 +579,19 @@ function validateSettings() { return false; } } + // Button ADC thresholds must form coherent, non-overlapping ranges + const btnVal = (id) => parseInt(document.getElementById(id).value, 10); + const b1Min = btnVal('btn1Min'), b1Max = btnVal('btn1Max'); + const b2Min = btnVal('btn2Min'), b2Max = btnVal('btn2Max'); + const b3Min = btnVal('btn3Min'), b3Max = btnVal('btn3Max'); + if (b1Min >= b1Max || b2Min >= b2Max || b3Min >= b3Max) { + alert('Each button Min must be less than its Max.'); + return false; + } + if (b1Max > b2Min || b2Max > b3Min) { + alert('Button ADC ranges must not overlap.'); + return false; + } return true; } From b45aebe1a6d4a1deb419e3fd339dbc650edb3abe Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Sun, 16 Aug 2026 21:25:15 +0200 Subject: [PATCH 4/6] fix(ci): repair native test build and lint for button thresholds The native test build defines NORVI_AE01_R but did not compile NorviButtonHandler.cpp, so the new applySettings() call failed to link. Add it to SERVICE_SOURCES and extend the mock Settings struct with the button threshold fields. Align ConfigManager field comments per clang-format (MegaLinter). --- src/ConfigManager.hpp | 16 ++++++++-------- test/native/CMakeLists.txt | 1 + test/native/mocks/ConfigManager.hpp | 7 +++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ConfigManager.hpp b/src/ConfigManager.hpp index dbff28e5..4849432a 100644 --- a/src/ConfigManager.hpp +++ b/src/ConfigManager.hpp @@ -45,14 +45,14 @@ struct ControllerSettings { String opMode = "auto"; long timeLossGreenHours = 1; long timeLossRedHours = 24; - int timezoneIndex = 0; ///< Index into TimeClientHelper timezone table - uint16_t btn1Min = 3100; ///< NORVI button 1 ADC range min - uint16_t btn1Max = 3520; ///< NORVI button 1 ADC range max - uint16_t btn2Min = 3520; ///< NORVI button 2 ADC range min - uint16_t btn2Max = 3880; ///< NORVI button 2 ADC range max - uint16_t btn3Min = 3880; ///< NORVI button 3 ADC range min - uint16_t btn3Max = 4095; ///< NORVI button 3 ADC range max - uint16_t btnNoPress = 4096; ///< NORVI no-press threshold (no-op, > ADC max) + int timezoneIndex = 0; ///< Index into TimeClientHelper timezone table + uint16_t btn1Min = 3100; ///< NORVI button 1 ADC range min + uint16_t btn1Max = 3520; ///< NORVI button 1 ADC range max + uint16_t btn2Min = 3520; ///< NORVI button 2 ADC range min + uint16_t btn2Max = 3880; ///< NORVI button 2 ADC range max + uint16_t btn3Min = 3880; ///< NORVI button 3 ADC range min + uint16_t btn3Max = 4095; ///< NORVI button 3 ADC range max + uint16_t btnNoPress = 4096; ///< NORVI no-press threshold (no-op, > ADC max) }; /** diff --git a/test/native/CMakeLists.txt b/test/native/CMakeLists.txt index 8454fc4d..84f4238c 100644 --- a/test/native/CMakeLists.txt +++ b/test/native/CMakeLists.txt @@ -58,6 +58,7 @@ set(SERVICE_SOURCES ${PROJ_ROOT}/src/LocalSettingsMenu.cpp ${PROJ_ROOT}/src/Ky040Decoder.cpp ${PROJ_ROOT}/src/OlimexEncoderHandler.cpp + ${PROJ_ROOT}/src/NorviButtonHandler.cpp ) # Mock sources (compiled once) diff --git a/test/native/mocks/ConfigManager.hpp b/test/native/mocks/ConfigManager.hpp index 6f3d216e..d1f2300e 100644 --- a/test/native/mocks/ConfigManager.hpp +++ b/test/native/mocks/ConfigManager.hpp @@ -20,6 +20,13 @@ struct Settings { float tempCircThreshold = 24.0f; int tempCircFactor = 30; int tempCircMaxRuntime = 720; + uint16_t btn1Min = 3100; + uint16_t btn1Max = 3520; + uint16_t btn2Min = 3520; + uint16_t btn2Max = 3880; + uint16_t btn3Min = 3880; + uint16_t btn3Max = 4095; + uint16_t btnNoPress = 4096; }; struct WiFiConfig { From 3a137e0c77aaf84a2572d07b290e242e05357358 Mon Sep 17 00:00:00 2001 From: Stephan Strittmatter Date: Sun, 16 Aug 2026 21:47:18 +0200 Subject: [PATCH 5/6] fix(web): permit 4096 no-press sentinel in threshold validation --- data/web/app.js | 2 +- data/web/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/app.js b/data/web/app.js index e5f5875f..10c7380c 100644 --- a/data/web/app.js +++ b/data/web/app.js @@ -559,7 +559,7 @@ function validateSettings() { { id: 'btn2Max', name: 'Button 2 Max ADC', min: 0, max: 4095, type: 'int' }, { id: 'btn3Min', name: 'Button 3 Min ADC', min: 0, max: 4095, type: 'int' }, { id: 'btn3Max', name: 'Button 3 Max ADC', min: 0, max: 4095, type: 'int' }, - { id: 'btnNoPress', name: 'No-Press Threshold', min: 0, max: 4095, type: 'int' }, + { id: 'btnNoPress', name: 'No-Press Threshold', min: 0, max: 4096, type: 'int' }, ]; for (const f of fields) { const el = document.getElementById(f.id); diff --git a/data/web/index.html b/data/web/index.html index c3444524..2e2d309e 100644 --- a/data/web/index.html +++ b/data/web/index.html @@ -347,7 +347,7 @@

๐Ÿ”˜ Button Thresholds (NORVI)

- + Values above this are ignored ยท 4096 = no-op (S3 reads full scale)