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 "