From 9017deab8f2485c762385de846db3aacf21f7cb0 Mon Sep 17 00:00:00 2001
From: Mike Bignell
Date: Sat, 2 May 2026 10:43:31 +0100
Subject: [PATCH] Add HASP_ADC ambient-light auto-backlight feature
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implements automatic backlight brightness control driven by an ambient
light sensor (LDR) connected to an ADC-capable GPIO pin.
- hasp_gpio.cpp: Add HASP_ADC (0xF9) case to gpio_setup_pin() — sets
full 12-bit max (4095), applies ADC_11db attenuation on ESP32, and
seeds the smoothing filter with an initial read.
- hasp_gpio.cpp: Add gpioEverySecond() — reads the ADC each second,
applies an exponential moving average (alpha=1/8), maps the smoothed
value to a 0-255 backlight level respecting the inverted flag and a
configurable per-slot ceiling, and calls set_backlight_level().
- hasp_gpio.cpp: Persist per-slot ADC ceiling via a new 'adc_max' JSON
array in gpioGetConfig()/gpioSetConfig(), allowing calibration for
in-case or indoor installations.
- hasp_gpio.h: Declare gpioEverySecond().
- main.cpp: Call gpioEverySecond() in the Arduino every-second loop.
- hasp_task.cpp: Add gpioEverySecond() call and include for non-Arduino
(LVGL-task) builds.
- hasp_config.h: Add FP_GPIO_ADC_MAX config key ("adc_max").
- en_US.h: Add D_GPIO_ADC_BACKLIGHT label.
- hasp_http.cpp / hasp_http_async.cpp: Expose HASP_ADC as "Ambient
Light (Auto Backlight)" in the GPIO input type selector.
- hasp_http.cpp: Replace "Normally Open/Closed" with "Normal/Inverted"
for input GPIO default state — previous labels were switch/relay
terminology, meaningless for analog sensors.
- hasp_http.cpp: Call configWrite() after gpioSavePinConfig() so GPIO
settings survive a reboot. The sync handler was missing this call.
- hasp_gui.cpp: Call configWrite() after touch calibration so the
calibration data survives a reboot.
- esp32-2432s024.ini: Document the stock LDR voltage divider hardware
bug and the required 1.5kΩ resistor replacement.
---
src/hasp/hasp_task.cpp | 8 +++
src/hasp_config.h | 1 +
src/hasp_gui.cpp | 4 ++
src/lang/en_US.h | 1 +
src/main.cpp | 8 +++
src/sys/gpio/hasp_gpio.cpp | 88 ++++++++++++++++++++++++++++
src/sys/gpio/hasp_gpio.h | 1 +
src/sys/svc/hasp_http.cpp | 10 +++-
src/sys/svc/hasp_http_async.cpp | 6 ++
user_setups/esp32/esp32-2432s024.ini | 23 ++++++++
10 files changed, 148 insertions(+), 2 deletions(-)
diff --git a/src/hasp/hasp_task.cpp b/src/hasp/hasp_task.cpp
index 09650380..3957d60d 100644
--- a/src/hasp/hasp_task.cpp
+++ b/src/hasp/hasp_task.cpp
@@ -15,6 +15,10 @@ For full license information read the LICENSE file in the project folder */
#include "hasp_gui.h"
#endif
+#if HASP_USE_GPIO > 0
+#include "sys/gpio/hasp_gpio.h"
+#endif
+
#ifdef HASP_USE_STAT_COUNTER
extern uint16_t statLoopCounter; // measures the average looptime
#endif
@@ -24,6 +28,10 @@ void task_every_second_cb(lv_task_t* task)
{
haspEverySecond(); // sleep timer & statusupdate
+#if HASP_USE_GPIO > 0
+ gpioEverySecond();
+#endif
+
#if HASP_MQTT_TELNET > 0
mqttEverySecond();
#endif
diff --git a/src/hasp_config.h b/src/hasp_config.h
index 04160d30..ad81657b 100644
--- a/src/hasp_config.h
+++ b/src/hasp_config.h
@@ -93,6 +93,7 @@ const char FP_GUI_REPEAT_TIME[] PROGMEM = "repeat";
const char FP_DEBUG_TELEPERIOD[] PROGMEM = "tele";
const char FP_DEBUG_ANSI[] PROGMEM = "ansi";
const char FP_GPIO_CONFIG[] PROGMEM = "config";
+const char FP_GPIO_ADC_MAX[] PROGMEM = "adc_max"; // per-slot ADC ceiling for ambient-light scaling
const char FP_HASP_CONFIG_FILE[] PROGMEM = "/config.json";
diff --git a/src/hasp_gui.cpp b/src/hasp_gui.cpp
index 17eb72be..4cd54c32 100644
--- a/src/hasp_gui.cpp
+++ b/src/hasp_gui.cpp
@@ -159,6 +159,10 @@ void guiCalibrate(void)
// }
lv_obj_invalidate(lv_disp_get_layer_sys(NULL));
+
+#if HASP_USE_CONFIG > 0
+ configWrite(); // Persist calibration data so it survives reboot
+#endif
#endif
}
diff --git a/src/lang/en_US.h b/src/lang/en_US.h
index 0ec1b257..3b49eba2 100644
--- a/src/lang/en_US.h
+++ b/src/lang/en_US.h
@@ -231,6 +231,7 @@
#define D_GPIO_LIGHT_RELAY "Light Relay"
#define D_GPIO_PWM "PWM"
#define D_GPIO_DAC "DAC"
+#define D_GPIO_ADC_BACKLIGHT "Ambient Light (Auto Backlight)"
#define D_GPIO_SERIAL_DIMMER "Serial Dimmer"
#define D_GPIO_UNKNOWN "Unknown"
#define D_GPIO_PIN "Pin"
diff --git a/src/main.cpp b/src/main.cpp
index b21d9e24..2555ee7f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -17,6 +17,10 @@
#include "sys/net/hasp_time.h"
#include "dev/device.h"
+#if HASP_USE_GPIO > 0
+#include "sys/gpio/hasp_gpio.h"
+#endif
+
#if HASP_USE_CONFIG > 0
#include "hasp_debug.h"
#include "hasp_macro.h"
@@ -215,6 +219,10 @@ IRAM_ATTR void loop()
/* Runs Every Second */
haspEverySecond(); // sleep timer & statusupdate
+#if HASP_USE_GPIO > 0
+ gpioEverySecond();
+#endif
+
#if HASP_USE_MQTT > 0
mqttEverySecond();
#endif
diff --git a/src/sys/gpio/hasp_gpio.cpp b/src/sys/gpio/hasp_gpio.cpp
index 48bfc496..70886ec9 100644
--- a/src/sys/gpio/hasp_gpio.cpp
+++ b/src/sys/gpio/hasp_gpio.cpp
@@ -329,6 +329,17 @@ static void gpio_setup_pin(uint8_t index)
break;
}
+ case hasp_gpio_type_t::HASP_ADC:
+ pinMode(gpio->pin, INPUT);
+ gpio->max = 4095; // 12-bit ADC full range (overrides the default 255 set above)
+#if defined(ARDUINO_ARCH_ESP32)
+ analogSetPinAttenuation(gpio->pin, ADC_11db); // full 0-3.3V range
+ gpio->val = analogRead(gpio->pin); // seed the smoothing filter
+#else
+ gpio->val = analogRead(gpio->pin);
+#endif
+ break;
+
case hasp_gpio_type_t::FREE:
return;
@@ -391,6 +402,33 @@ static inline bool gpio_is_output(hasp_gpio_config_t* gpio)
return (gpio->type > hasp_gpio_type_t::USED) && (gpio->type < 0x80);
}
+void gpioEverySecond(void)
+{
+#if defined(ARDUINO_ARCH_ESP32)
+ for(uint8_t i = 0; i < HASP_NUM_GPIO_CONFIG; i++) {
+ if(!gpioConfigInUse(i) || gpioConfig[i].type != hasp_gpio_type_t::HASP_ADC) continue;
+
+ // Exponential moving average (alpha = 1/8) to smooth noisy ADC readings
+ uint16_t raw = analogRead(gpioConfig[i].pin);
+ gpioConfig[i].val = (gpioConfig[i].val * 7 + raw) >> 3;
+
+ // Only adjust backlight when the screen is currently on
+ if(!haspDevice.get_backlight_power()) continue;
+
+ // Map smoothed ADC value (0..ceiling) to backlight level (0-255).
+ // 'max' defaults to 4095 (full 12-bit range) but can be reduced for
+ // in-case installs where the sensor never sees full daylight, so the
+ // full backlight range is still exercised.
+ uint16_t ceiling = gpioConfig[i].max > 0 ? gpioConfig[i].max : 4095;
+ uint8_t level = (uint8_t)min((uint32_t)255, (uint32_t)gpioConfig[i].val * 255 / ceiling);
+ if(gpioConfig[i].inverted) level = 255 - level;
+ if(level < 10) level = 10; // always keep screen readable in darkness
+
+ haspDevice.set_backlight_level(level);
+ }
+#endif
+}
+
void gpioEvery5Seconds(void)
{
for(uint8_t i = 0; i < HASP_NUM_GPIO_CONFIG; i++) {
@@ -1001,6 +1039,40 @@ bool gpioGetConfig(const JsonObject& settings)
changed = true;
}
+ /* Save per-slot ADC ceiling values (for ambient-light auto-backlight calibration).
+ * Only written for slots with a non-default max (i.e. user has calibrated for their install).
+ * A value of 0 in the array means "use type default" (4095 for ADC). */
+ bool hasAdcMax = false;
+ for(uint8_t j = 0; j < HASP_NUM_GPIO_CONFIG; j++) {
+ if(gpioConfig[j].type == hasp_gpio_type_t::HASP_ADC && gpioConfig[j].max != 4095 && gpioConfig[j].max != 0) {
+ hasAdcMax = true;
+ break;
+ }
+ }
+ if(hasAdcMax) {
+ /* Compare against what's already in JSON before marking changed */
+ JsonArray existingAdcArr = settings[FPSTR(FP_GPIO_ADC_MAX)].as();
+ uint8_t k = 0;
+ for(JsonVariant v : existingAdcArr) {
+ if(k < HASP_NUM_GPIO_CONFIG) {
+ uint16_t m = (gpioConfig[k].type == hasp_gpio_type_t::HASP_ADC) ? gpioConfig[k].max : 0;
+ if(v.as() != m) changed = true;
+ } else {
+ changed = true;
+ }
+ k++;
+ }
+ if(k != HASP_NUM_GPIO_CONFIG) changed = true;
+
+ if(changed) { /* Rebuild only when necessary */
+ JsonArray adcArr = settings[FPSTR(FP_GPIO_ADC_MAX)].to();
+ for(uint8_t j = 0; j < HASP_NUM_GPIO_CONFIG; j++) {
+ uint16_t m = (gpioConfig[j].type == hasp_gpio_type_t::HASP_ADC) ? gpioConfig[j].max : 0;
+ adcArr.add(m);
+ }
+ }
+ }
+
if(changed) configOutput(settings, TAG_GPIO);
return changed;
}
@@ -1042,6 +1114,22 @@ bool gpioSetConfig(const JsonObject& settings)
changed |= status;
}
+ /* Load per-slot ADC ceiling values. These are applied before gpioSetup() calls
+ * gpio_setup_pin(), which preserves any non-zero max already set here. */
+ if(!settings[FPSTR(FP_GPIO_ADC_MAX)].isNull()) {
+ int j = 0;
+ JsonArray adcArr = settings[FPSTR(FP_GPIO_ADC_MAX)].as();
+ for(JsonVariant v : adcArr) {
+ if(j < HASP_NUM_GPIO_CONFIG) {
+ uint16_t m = v.as();
+ if(m > 0 && gpioConfig[j].type == hasp_gpio_type_t::HASP_ADC) {
+ gpioConfig[j].max = m;
+ }
+ }
+ j++;
+ }
+ }
+
return changed;
}
#endif // HASP_USE_CONFIG
diff --git a/src/sys/gpio/hasp_gpio.h b/src/sys/gpio/hasp_gpio.h
index 559bc762..d81ba5e4 100644
--- a/src/sys/gpio/hasp_gpio.h
+++ b/src/sys/gpio/hasp_gpio.h
@@ -35,6 +35,7 @@ extern "C" {
void gpioSetup(void);
IRAM_ATTR void gpioLoop(void);
+void gpioEverySecond(void);
void gpioEvery5Seconds(void);
void gpio_set_normalized_group_values(hasp_update_value_t& value);
diff --git a/src/sys/svc/hasp_http.cpp b/src/sys/svc/hasp_http.cpp
index 9a64f47a..02ff9e4e 100644
--- a/src/sys/svc/hasp_http.cpp
+++ b/src/sys/svc/hasp_http.cpp
@@ -1769,10 +1769,12 @@ static void webHandleGpioConfig()
uint8_t pinfunc = webServer.arg("func").toInt();
bool inverted = webServer.arg("state").toInt();
gpioSavePinConfig(id, pin, type, group, pinfunc, inverted);
+ configWrite(); // persist to config.json
}
if(webServer.hasArg("del")) {
gpioSavePinConfig(id, pin, hasp_gpio_type_t::FREE, 0, 0, false);
+ configWrite(); // persist to config.json
}
}
@@ -1861,6 +1863,9 @@ static void webHandleGpioConfig()
case hasp_gpio_type_t::TOUCH:
httpMessage += D_GPIO_TOUCH;
break;
+ case hasp_gpio_type_t::HASP_ADC:
+ httpMessage += D_GPIO_ADC_BACKLIGHT;
+ break;
case hasp_gpio_type_t::LED:
httpMessage += D_GPIO_LED;
break;
@@ -2068,6 +2073,7 @@ static void webHandleGpioInput()
httpMessage += getOption(hasp_gpio_type_t::SMOKE, "Smoke", conf.type);
httpMessage += getOption(hasp_gpio_type_t::VIBRATION, "Vibration", conf.type);
httpMessage += getOption(hasp_gpio_type_t::WINDOW, "Window", conf.type);
+ httpMessage += getOption(hasp_gpio_type_t::HASP_ADC, D_GPIO_ADC_BACKLIGHT, conf.type);
httpMessage += F("
");
httpMessage += F("" D_GPIO_GROUP "
");
httpMessage += F("Default State
");
httpMessage += F("Resistor
");
httpMessage += F("" D_GPIO_GROUP "