From fa6befd81a9be8904f6b1655bc4790e27e2f1120 Mon Sep 17 00:00:00 2001 From: thirug010 <30732819+thirug010@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:00:30 -0500 Subject: [PATCH] Lanbon L10: TCA9554 I2C GPIO, RGB panel, LEDC, HTTP/async; user setup Made-with: Cursor --- .../Arduino_RGB_Display_mod.cpp | 94 ++++- .../Arduino_RGB_Display_mod.h | 214 +++++++++++ src/dev/esp32/esp32.cpp | 3 + src/dev/esp32/esp32.h | 11 +- src/dev/esp32/esp32_i2c_ledc.cpp | 61 ++++ src/dev/esp32/esp32_i2c_ledc.h | 32 ++ src/drv/gpio/i2c_gpio_stc_l10.cpp | 268 ++++++++++++++ src/drv/gpio/i2c_gpio_stc_l10.h | 39 ++ src/drv/tft/Arduino_PCA9535SWSPI.h | 1 + src/drv/tft/Arduino_TCA9554SWSPI.cpp | 333 ++++++++++++++++++ src/drv/tft/Arduino_TCA9554SWSPI.h | 98 ++++++ src/drv/tft/tft_driver_arduinogfx.cpp | 50 +++ src/hasp/hasp.cpp | 17 + src/hasp/hasp_dispatch.cpp | 4 + src/hasp_config.h | 2 + src/hasp_debug.cpp | 3 + src/hasp_debug.h | 1 + src/hasp_gui.cpp | 10 + src/hasp_gui.h | 2 + src/sys/gpio/hasp_gpio.cpp | 182 +++++++++- src/sys/gpio/hasp_gpio.h | 20 +- src/sys/gpio/i2c_gpio_bus.cpp | 24 ++ src/sys/gpio/i2c_gpio_bus.h | 54 +++ src/sys/svc/hasp_http.cpp | 70 +++- src/sys/svc/hasp_http_async.cpp | 75 +++- user_setups/esp32s3/lanbon_l10.ini | 106 ++++++ 26 files changed, 1748 insertions(+), 26 deletions(-) create mode 100644 src/dev/esp32/esp32_i2c_ledc.cpp create mode 100644 src/dev/esp32/esp32_i2c_ledc.h create mode 100644 src/drv/gpio/i2c_gpio_stc_l10.cpp create mode 100644 src/drv/gpio/i2c_gpio_stc_l10.h create mode 100644 src/drv/tft/Arduino_TCA9554SWSPI.cpp create mode 100644 src/drv/tft/Arduino_TCA9554SWSPI.h create mode 100644 src/sys/gpio/i2c_gpio_bus.cpp create mode 100644 src/sys/gpio/i2c_gpio_bus.h create mode 100644 user_setups/esp32s3/lanbon_l10.ini diff --git a/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.cpp b/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.cpp index 0b997a9e7..a58b97e20 100644 --- a/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.cpp +++ b/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.cpp @@ -6,6 +6,96 @@ #include "Arduino_DataBus.h" #include "Arduino_RGB_Display_mod.h" +namespace { + +bool execute_custom_init_ops(Arduino_DataBus* bus, const uint8_t* ops, size_t len) +{ + if(!bus || !ops || len == 0) return true; + + size_t i = 0; + while(i < len) { + const uint8_t op = ops[i++]; + switch(op) { + case BEGIN_WRITE: + bus->beginWrite(); + break; + case END_WRITE: + bus->endWrite(); + break; + case WRITE_COMMAND_8: + if(i >= len) return false; + bus->writeCommand(ops[i++]); + break; + case WRITE_C8_D8: + if(i + 1 >= len) return false; + bus->writeCommand(ops[i++]); + bus->write(ops[i++]); + break; + case WRITE_C8_D16: + if(i + 2 >= len) return false; + bus->writeCommand(ops[i++]); + bus->write(ops[i++]); + bus->write(ops[i++]); + break; + case WRITE_BYTES: { + if(i >= len) return false; + uint8_t n = ops[i++]; + if(i + n > len) return false; + for(uint8_t k = 0; k < n; k++) bus->write(ops[i++]); + break; + } + case DELAY: + if(i >= len) return false; + delay(ops[i++]); + break; + case I2C_WRITE_REG: + if(i + 1 >= len) return false; +#if defined(USE_I2C_SW_SPI) + bus->writeRegisterI2COpcode(ops[i], ops[i + 1]); + i += 2; +#else + return false; +#endif + break; + case I2C_WRITE_SEQ: + if(i + 1 >= len) return false; + { + const uint8_t reg = ops[i++]; + const uint8_t n = ops[i++]; + if(i + n > len) return false; +#if defined(USE_I2C_SW_SPI) + bus->writeRegisterI2CSeqOpcode(reg, &ops[i], n); + i += n; +#else + return false; +#endif + } + break; + case REPEAT_IN: + if(i + 3 >= len) return false; + { + const uint8_t count = ops[i++]; + const uint8_t nested_op = ops[i++]; + if(nested_op != I2C_WRITE_REG) return false; + const uint8_t reg = ops[i++]; + const uint8_t val = ops[i++]; +#if defined(USE_I2C_SW_SPI) + for(uint8_t r = 0; r < count; r++) bus->writeRegisterI2COpcode(reg, val); +#else + return false; +#endif + } + break; + default: + return false; + } + } + + return true; +} + +} // namespace + Arduino_RGB_Display_Mod::Arduino_RGB_Display_Mod(int16_t w, int16_t h, Arduino_RGBPanel_Mod* rgbpanel, uint8_t r, bool auto_flush, Arduino_DataBus* bus, int8_t rst, const uint8_t* init_operations, size_t init_operations_len) @@ -42,7 +132,9 @@ bool Arduino_RGB_Display_Mod::begin(int32_t speed) if(_bus) { if(_init_operations_len > 0) { - _bus->batchOperation((uint8_t*)_init_operations, _init_operations_len); + if(!execute_custom_init_ops(_bus, _init_operations, _init_operations_len)) { + _bus->batchOperation((uint8_t*)_init_operations, _init_operations_len); + } } } diff --git a/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.h b/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.h index b9c78a6e9..ea465d7a2 100644 --- a/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.h +++ b/lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.h @@ -11,6 +11,18 @@ #include "Arduino_GFX.h" #include "Arduino_RGBPanel_mod.h" +#ifndef I2C_WRITE_REG +#define I2C_WRITE_REG 0xE9 +#endif + +#ifndef REPEAT_IN +#define REPEAT_IN 0xEA +#endif + +#ifndef I2C_WRITE_SEQ +#define I2C_WRITE_SEQ 0xEB +#endif + static const uint8_t st7701_4848S040_init_operations[] = { BEGIN_WRITE, WRITE_COMMAND_8, 0xFF, @@ -396,6 +408,208 @@ static const uint8_t st7701_t_panel_init_operations[] = { END_WRITE}; +#if defined(HASP_LANBON_L10_TCA9554_GC9503V) +/* Lanbon L10 — GC9503V init via TCA9554 3-wire bus (same encoding as st7701_t_panel_init_operations). */ +static const uint8_t gc9503v_l10_init_operations[] = { + BEGIN_WRITE, + DELAY, 120, + WRITE_C8_D8, 0xB1, 0x10, + WRITE_C8_D8, 0x3A, 0x50, + WRITE_COMMAND_8, 0xF0, + WRITE_BYTES, 5, + 0x55, 0xAA, 0x52, 0x08, + 0x00, + WRITE_C8_D16, 0xF6, 0x5A, 0x87, + WRITE_C8_D8, 0xC1, 0x3F, + WRITE_C8_D8, 0xC2, 0x0E, + WRITE_C8_D8, 0xC6, 0xF8, + WRITE_C8_D8, 0xC9, 0x10, + WRITE_C8_D8, 0xCD, 0x25, + WRITE_COMMAND_8, 0x87, + WRITE_BYTES, 3, + 0x04, 0x03, 0x66, + WRITE_COMMAND_8, 0x86, + WRITE_BYTES, 4, + 0x99, 0xA3, 0xA3, 0x51, + WRITE_C8_D8, 0xF8, 0x8A, + WRITE_C8_D8, 0xAC, 0x65, + WRITE_C8_D8, 0xA7, 0x47, + WRITE_C8_D8, 0xA0, 0xDD, + WRITE_COMMAND_8, 0xFA, + WRITE_BYTES, 4, + 0x08, 0x08, 0x00, 0x04, + WRITE_C8_D8, 0xA3, 0x2E, + WRITE_COMMAND_8, 0xFD, + WRITE_BYTES, 3, + 0x28, 0x3C, 0x00, + WRITE_C8_D8, 0x71, 0x48, + WRITE_C8_D8, 0x72, 0x48, + WRITE_C8_D16, 0x73, 0x00, 0x44, + WRITE_C8_D8, 0x97, 0xEE, + WRITE_C8_D8, 0x83, 0x93, + WRITE_C8_D8, 0x9A, 0x84, + WRITE_C8_D8, 0x9B, 0x54, + WRITE_C8_D16, 0x82, 0x5D, 0x5D, + WRITE_C8_D8, 0xB1, 0x10, + WRITE_C8_D16, 0x7A, 0x13, 0x1A, + WRITE_C8_D16, 0x7B, 0x13, 0x1A, + WRITE_COMMAND_8, 0x64, + WRITE_BYTES, 16, + 0x18, 0x09, 0x03, 0x59, + 0x03, 0x03, 0x18, 0x08, + 0x03, 0x5A, 0x03, 0x03, + 0x7A, 0x7A, 0x7A, 0x7A, + WRITE_COMMAND_8, 0x67, + WRITE_BYTES, 16, + 0x18, 0x07, 0x03, 0x5B, + 0x03, 0x03, 0x18, 0x06, + 0x03, 0x5C, 0x03, 0x03, + 0x7A, 0x7A, 0x7A, 0x7A, + WRITE_COMMAND_8, 0x68, + WRITE_BYTES, 13, + 0x00, 0x08, 0x0A, 0x08, + 0x09, 0x00, 0x00, 0x08, + 0x0A, 0x08, 0x09, 0x00, + 0x00, + WRITE_COMMAND_8, 0x60, + WRITE_BYTES, 8, + 0x18, 0x08, 0x7A, 0x7A, + 0x18, 0x02, 0x7A, 0x7A, + WRITE_COMMAND_8, 0x63, + WRITE_BYTES, 8, + 0x18, 0x02, 0x6D, 0x6D, + 0x18, 0x07, 0x7A, 0x7A, + WRITE_COMMAND_8, 0x69, + WRITE_BYTES, 7, + 0x14, 0x22, 0x14, 0x22, + 0x44, 0x22, 0x08, + WRITE_COMMAND_8, 0xD1, + WRITE_BYTES, 52, + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x1D, 0x00, 0x5F, + 0x00, 0x91, 0x00, 0xCE, + 0x00, 0xF5, 0x01, 0x2B, + 0x01, 0x7F, 0x01, 0xED, + 0x02, 0x23, 0x02, 0x78, + 0x02, 0xB2, 0x02, 0xB4, + 0x02, 0xF1, 0x03, 0x29, + 0x03, 0x49, 0x03, 0x6D, + 0x03, 0x82, 0x03, 0x9B, + 0x03, 0xA5, 0x03, 0xB0, + 0x03, 0xB5, 0x03, 0xBF, + 0x03, 0xDE, 0x03, 0xFF, + WRITE_COMMAND_8, 0xD2, + WRITE_BYTES, 52, + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x1D, 0x00, 0x5F, + 0x00, 0x91, 0x00, 0xCE, + 0x00, 0xF5, 0x01, 0x2B, + 0x01, 0x7F, 0x01, 0xED, + 0x02, 0x23, 0x02, 0x78, + 0x02, 0xB2, 0x02, 0xB4, + 0x02, 0xF1, 0x03, 0x29, + 0x03, 0x49, 0x03, 0x6D, + 0x03, 0x82, 0x03, 0x9B, + 0x03, 0xA5, 0x03, 0xB0, + 0x03, 0xB5, 0x03, 0xBF, + 0x03, 0xDE, 0x03, 0xFF, + WRITE_COMMAND_8, 0xD3, + WRITE_BYTES, 52, + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x1D, 0x00, 0x5F, + 0x00, 0x91, 0x00, 0xCE, + 0x00, 0xF5, 0x01, 0x2B, + 0x01, 0x7F, 0x01, 0xED, + 0x02, 0x23, 0x02, 0x78, + 0x02, 0xB2, 0x02, 0xB4, + 0x02, 0xF1, 0x03, 0x29, + 0x03, 0x49, 0x03, 0x6D, + 0x03, 0x82, 0x03, 0x9B, + 0x03, 0xA5, 0x03, 0xB0, + 0x03, 0xB5, 0x03, 0xBF, + 0x03, 0xDE, 0x03, 0xFF, + WRITE_COMMAND_8, 0xD4, + WRITE_BYTES, 52, + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x1D, 0x00, 0x5F, + 0x00, 0x91, 0x00, 0xCE, + 0x00, 0xF5, 0x01, 0x2B, + 0x01, 0x7F, 0x01, 0xED, + 0x02, 0x23, 0x02, 0x78, + 0x02, 0xB2, 0x02, 0xB4, + 0x02, 0xF1, 0x03, 0x29, + 0x03, 0x49, 0x03, 0x6D, + 0x03, 0x82, 0x03, 0x9B, + 0x03, 0xA5, 0x03, 0xB0, + 0x03, 0xB5, 0x03, 0xBF, + 0x03, 0xDE, 0x03, 0xFF, + WRITE_COMMAND_8, 0xD5, + WRITE_BYTES, 52, + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x1D, 0x00, 0x5F, + 0x00, 0x91, 0x00, 0xCE, + 0x00, 0xF5, 0x01, 0x2B, + 0x01, 0x7F, 0x01, 0xED, + 0x02, 0x23, 0x02, 0x78, + 0x02, 0xB2, 0x02, 0xB4, + 0x02, 0xF1, 0x03, 0x29, + 0x03, 0x49, 0x03, 0x6D, + 0x03, 0x82, 0x03, 0x9B, + 0x03, 0xA5, 0x03, 0xB0, + 0x03, 0xB5, 0x03, 0xBF, + 0x03, 0xDE, 0x03, 0xFF, + WRITE_COMMAND_8, 0xD6, + WRITE_BYTES, 52, + 0x00, 0x00, 0x00, 0x08, + 0x00, 0x1D, 0x00, 0x5F, + 0x00, 0x91, 0x00, 0xCE, + 0x00, 0xF5, 0x01, 0x2B, + 0x01, 0x7F, 0x01, 0xED, + 0x02, 0x23, 0x02, 0x78, + 0x02, 0xB2, 0x02, 0xB4, + 0x02, 0xF1, 0x03, 0x29, + 0x03, 0x49, 0x03, 0x6D, + 0x03, 0x82, 0x03, 0x9B, + 0x03, 0xA5, 0x03, 0xB0, + 0x03, 0xB5, 0x03, 0xBF, + 0x03, 0xDE, 0x03, 0xFF, + DELAY, 5, + WRITE_COMMAND_8, 0x11, + DELAY, 121, + + WRITE_COMMAND_8, 0x29, + DELAY, 22, + + WRITE_COMMAND_8, 0x29, + DELAY, 4, + + WRITE_C8_D8, 0xB1, 0x13, + DELAY,40, + WRITE_C8_D8, 0xB1, 0x13, + END_WRITE, + + DELAY, 1, + + BEGIN_WRITE, + I2C_WRITE_REG, 0x01, 0xF1, + I2C_WRITE_REG, 0x01, 0xF3, + END_WRITE, + + DELAY, 1, + // 00 to 2F for 19 times + BEGIN_WRITE, + REPEAT_IN, 19, I2C_WRITE_REG, 0x2F, 0x00, + END_WRITE, + DELAY,255, + // Brightness control + // 0x01, Level, 0x00, 0xFF,0xFF screen always on + + BEGIN_WRITE, + I2C_WRITE_SEQ, 0x21, 5, 0x01, 0x64, 0x00, 0x00, 0x14, + END_WRITE, +}; +#endif /* HASP_LANBON_L10_TCA9554_GC9503V */ + class Arduino_RGB_Display_Mod : public Arduino_GFX{ public: Arduino_RGB_Display_Mod( diff --git a/src/dev/esp32/esp32.cpp b/src/dev/esp32/esp32.cpp index ca0c7ee4d..5870a7b45 100644 --- a/src/dev/esp32/esp32.cpp +++ b/src/dev/esp32/esp32.cpp @@ -461,6 +461,9 @@ long Esp32Device::get_uptime() #warning Building for TTGo T-Watch #include "dev/esp32/twatch.h" +#elif defined(HASP_ESP32_DEVICE_I2C_BACKLIGHT) && defined(HASP_USE_I2C_GPIO) +#include "dev/esp32/esp32_i2c_ledc.h" + #else dev::Esp32Device haspDevice; #endif diff --git a/src/dev/esp32/esp32.h b/src/dev/esp32/esp32.h index 9fca83f2b..4ce0b3a3d 100644 --- a/src/dev/esp32/esp32.h +++ b/src/dev/esp32/esp32.h @@ -60,6 +60,14 @@ class Esp32Device : public BaseDevice { bool is_system_pin(uint8_t pin) override; + protected: + /** For board-specific subclasses (e.g. I2C virtual backlight). Base = LEDC when pin is a real GPIO. */ + virtual void update_backlight(); + uint8_t get_backlight_pin() const + { + return _backlight_pin; + } + private: std::string _hardware_id; std::string _chip_model; @@ -73,7 +81,6 @@ class Esp32Device : public BaseDevice { bool _backlight_fading; bool _backlight_fade; - void update_backlight(); static bool cb_backlight(const ledc_cb_param_t *param, void *user_arg); void end_backlight_fade(); }; @@ -88,6 +95,8 @@ class Esp32Device : public BaseDevice { #include "m5stackcore2.h" #elif defined(TWATCH) #include "twatch.h" +#elif defined(HASP_ESP32_DEVICE_I2C_BACKLIGHT) && defined(HASP_USE_I2C_GPIO) +#include "esp32_i2c_ledc.h" #else using dev::Esp32Device; extern dev::Esp32Device haspDevice; diff --git a/src/dev/esp32/esp32_i2c_ledc.cpp b/src/dev/esp32/esp32_i2c_ledc.cpp new file mode 100644 index 000000000..5589eb7d3 --- /dev/null +++ b/src/dev/esp32/esp32_i2c_ledc.cpp @@ -0,0 +1,61 @@ +/* MIT License - Copyright (c) 2019-2024 Francis Van Roie + For full license information read the LICENSE file in the project folder */ + +#include "hasp_conf.h" + +#if defined(ESP32) && defined(HASP_ESP32_DEVICE_I2C_BACKLIGHT) && defined(HASP_USE_I2C_GPIO) \ + && defined(ARDUINO_ARCH_ESP32) + +#include +#include + +#include "esp32_i2c_ledc.h" +#include "hasp_debug.h" +#include "sys/gpio/i2c_gpio_bus.h" +#if defined(EXPANDER_ADDRESS) && defined(HASP_EXPANDER_GPIO_STC_L10) +#include "drv/gpio/i2c_gpio_stc_l10.h" +#endif + +namespace dev { + +void Esp32_I2c_ledc::update_backlight() +{ + uint8_t pin = get_backlight_pin(); +#if defined(EXPANDER_BACKLIGHT_CHANNEL) + if(pin == 0u || pin == 255u) pin = (uint8_t)EXPANDER_BACKLIGHT_CHANNEL; +#endif + const bool pin_ok = (pin != 0u && pin != 255u); + const bool i2c_ready = pin_ok && i2c_gpio && i2c_gpio->handles_pin(pin); + + const bool power = Esp32Device::get_backlight_power(); + const uint8_t blv = Esp32Device::get_backlight_level(); + uint32_t duty = power ? (uint32_t)map((long)blv, 0L, 255L, 0L, 1023L) : 0U; + if(Esp32Device::get_backlight_invert()) duty = 1023U - duty; + const uint8_t out = (uint8_t)map((long)duty, 0L, 1023L, 0L, 255L); + + if(i2c_ready) { + (void)i2c_gpio->analogWrite8(pin, out); + return; + } + else{ + #if defined(EXPANDER_ADDRESS) + /* Global i2c_gpio not ready yet (gpioSetup later); one-shot STC driver still reaches BL on shared Wire. */ + LOG_WARNING(TAG_LEDC, F("LEDI2C i2c_gpio-->handles_pin()=>%u is not ready"), (unsigned)pin); + i2c_gpio_stc_l10 local_exp((uint8_t)EXPANDER_ADDRESS, Wire); + const bool wr = local_exp.analogWrite8(pin, out); + LOG_INFO(TAG_LEDC, F("LEDI2C i2c_gpio-->analogWrite8(%u, %u) after init=>%d"), (unsigned)pin, (unsigned)out, + wr ? 1 : 0); + #else + LOG_WARNING(TAG_LEDC, F("LEDI2C not ready (pin %u)"), (unsigned)pin); + #endif + return; + } + + Esp32Device::update_backlight(); +} + +} // namespace dev + +dev::Esp32_I2c_ledc haspDevice; + +#endif diff --git a/src/dev/esp32/esp32_i2c_ledc.h b/src/dev/esp32/esp32_i2c_ledc.h new file mode 100644 index 000000000..29f01eb7e --- /dev/null +++ b/src/dev/esp32/esp32_i2c_ledc.h @@ -0,0 +1,32 @@ +/* MIT License - Copyright (c) 2019-2024 Francis Van Roie + For full license information read the LICENSE file in the project folder */ + +#ifndef HASP_ESP32_I2C_LEDC_H +#define HASP_ESP32_I2C_LEDC_H + +#if defined(ESP32) && defined(HASP_ESP32_DEVICE_I2C_BACKLIGHT) && defined(HASP_USE_I2C_GPIO) + +#include "esp32.h" + +namespace dev { + +/** + * ESP32 device with LCD backlight on an I2C virtual GPIO (via global @c i2c_gpio). + * Uses @c get_backlight_pin() for UI changes; values @c 0 and @c 255 are treated as unset (GPIO0 / out-of-range) and + * replaced by @c EXPANDER_BACKLIGHT_CHANNEL when defined. Uses @c i2c_gpio_bus::handles_pin() and @c analogWrite8(). + * For MCU GPIO backlights, falls back to @c Esp32Device LEDC behavior. + */ +class Esp32_I2c_ledc : public Esp32Device { + + protected: + void update_backlight() override; +}; + +} // namespace dev + +using dev::Esp32_I2c_ledc; +extern dev::Esp32_I2c_ledc haspDevice; + +#endif /* ESP32 && I2C backlight device */ + +#endif /* HASP_ESP32_I2C_LEDC_H */ diff --git a/src/drv/gpio/i2c_gpio_stc_l10.cpp b/src/drv/gpio/i2c_gpio_stc_l10.cpp new file mode 100644 index 000000000..a7aadeb02 --- /dev/null +++ b/src/drv/gpio/i2c_gpio_stc_l10.cpp @@ -0,0 +1,268 @@ +/* MIT License - Copyright (c) 2019-2024 Francis Van Roie */ + +#include "hasp_conf.h" + +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO) && defined(HASP_EXPANDER_GPIO_STC_L10) + +#include "i2c_gpio_stc_l10.h" + +namespace { + +/** Max WPR read length across k_pin_map (e.g. motion @ 0x1B). */ +constexpr size_t k_read_buf_max = 32u; + +uint8_t read_nbytes(const i2c_gpio_pin_map_t* m) +{ + if(m->read_pl_byte > 0) return m->read_pl_byte; + if(m->isWPR && m->pre_read_len > 0) return m->pre_read_len; + return 1; +} + +/** read_pin_byte is 1-based in buffer; 0 or 0xFF selects last byte. */ +uint8_t data_byte_index(const i2c_gpio_pin_map_t* m, uint8_t nbytes) +{ + if(nbytes == 0) return 0; + if(m->read_pin_byte == 0 || m->read_pin_byte == 0xFFu) + return (uint8_t)(nbytes - 1u); + if(m->read_pin_byte >= 1u && m->read_pin_byte <= nbytes) + return (uint8_t)(m->read_pin_byte - 1u); + return (uint8_t)(nbytes - 1u); +} + +void append_post_write(TwoWire* wire, const i2c_gpio_pin_map_t* m) +{ + if(!m || m->post_write_len == 0) return; + for(uint8_t i = 0; i < m->post_write_len; i++) { + wire->write(m->post_write[i]); + } +} + +/* Relays 129–132: keep init_reg on STC path (0x21); 0x01 is TCA9554 output on this bridge and can blank the panel. */ +const i2c_gpio_pin_map_t k_pin_map[] = { + {.pin = 129u, .read_reg = 0x24, .isWPR = true, .read_pin_byte = 4u, .pre_read_len = 4u, .write_reg = 0x25, .init_reg = 0x21, .bit = 0u, .pre_write = {}, .pre_write_len = 0u, .write_seq = {0x01, 0x00, 0x00, 0x00}, .write_seq_len = 4u, .is_digital = false}, + {.pin = 130u, .read_reg = 0x24, .isWPR = true, .read_pin_byte = 4u, .pre_read_len = 4u, .write_reg = 0x25, .init_reg = 0x21, .bit = 1u, .pre_write = {}, .pre_write_len = 0u, .write_seq = {0x01, 0x00, 0x00, 0x00}, .write_seq_len = 4u, .is_digital = false}, + {.pin = 131u, .read_reg = 0x24, .isWPR = true, .read_pin_byte = 4u, .pre_read_len = 4u, .write_reg = 0x25, .init_reg = 0x21, .bit = 2u, .pre_write = {}, .pre_write_len = 0u, .write_seq = {0x01, 0x00, 0x00, 0x00}, .write_seq_len = 4u, .is_digital = false}, + {.pin = 132u, .read_reg = 0x24, .isWPR = true, .read_pin_byte = 4u, .pre_read_len = 4u, .write_reg = 0x25, .init_reg = 0x21, .bit = 3u, .pre_write = {}, .pre_write_len = 0u, .write_seq = {0x01, 0x00, 0x00, 0x00}, .write_seq_len = 4u, .is_digital = false}, + /* Display brightness */ + {.pin = 133u, .read_reg = 0x21, .isWPR = true, .read_pin_byte = 2u, .pre_read_len = 5u, .write_reg = 0x21, .init_reg = 0x21, .bit = 3u, .pre_write_len = 0u, .write_seq = {0x01}, .write_seq_len = 1u, .post_write = {0x00, 0x00, 0x14}, .post_write_len = 3u, .is_digital = false}, + /* Motion: write pointer 0x1B, 20-byte read; byte 18 == 0x07 => present, 0x00 => not (I2C capture). */ + {.pin = 134u, .read_reg = 0x1B, .isWPR = true, .read_pin_byte = 16u, .pre_read_len = 20u, .write_reg = 0x1B, .init_reg = 0x1B, .bit = 0u, .pre_write_len = 0u, .write_seq_len = 0u, .is_digital = true, .on_value = 0x01, .off_value = 0x00}, +}; +const size_t k_pin_map_count = sizeof(k_pin_map) / sizeof(k_pin_map[0]); + +} // namespace + +const i2c_gpio_pin_map_t* i2c_gpio_stc_l10::find_map(uint8_t pin) const +{ + for(size_t i = 0; i < k_pin_map_count; i++) { + if(k_pin_map[i].pin == pin) return &k_pin_map[i]; + } + return nullptr; +} + +bool i2c_gpio_stc_l10::handles_pin(uint8_t pin) const +{ + return find_map(pin) != nullptr; +} + +i2c_gpio_stc_l10::i2c_gpio_stc_l10(uint8_t i2c_addr, TwoWire& wire) +{ + _addr = i2c_addr; + _wire = &wire; +} + +bool i2c_gpio_stc_l10::begin() +{ + if(k_pin_map_count == 0) return false; + uint8_t v; + return read_reg(k_pin_map[0].read_reg, v); +} + +bool i2c_gpio_stc_l10::pre_write(const i2c_gpio_pin_map_t* m) +{ + if(!m || m->pre_write_len == 0) return true; + + _wire->beginTransmission(_addr); + for(uint8_t i = 0; i < m->pre_write_len; i++) { + _wire->write(m->pre_write[i]); + } + append_post_write(_wire, m); + return _wire->endTransmission() == 0; +} + +bool i2c_gpio_stc_l10::read_reg(uint8_t reg, uint8_t& val) +{ + _wire->beginTransmission(_addr); + _wire->write(reg); + if(_wire->endTransmission(false) != 0) return false; + if(_wire->requestFrom((int)_addr, 1) != 1) return false; + val = _wire->read(); + return true; +} + +bool i2c_gpio_stc_l10::writepointer_read(uint8_t reg, uint8_t* val, uint8_t pre_read_len) +{ + _wire->beginTransmission(_addr); + _wire->write(reg); + if(_wire->endTransmission(false) != 0) return false; + if(_wire->requestFrom((int)_addr, (int)pre_read_len) != (int)pre_read_len) return false; + for(uint8_t i = 0; i < pre_read_len; i++) { + if(!_wire->available()) return false; + val[i] = _wire->read(); + } + return true; +} + +bool i2c_gpio_stc_l10::write_reg(const i2c_gpio_pin_map_t* m, uint8_t reg, uint8_t val) +{ + _wire->beginTransmission(_addr); + _wire->write(reg); + if(m && m->write_seq_len > 0) { + for(uint8_t i = 0; i < m->write_seq_len; i++) { + _wire->write(m->write_seq[i]); + } + } + _wire->write(val); + append_post_write(_wire, m); + return _wire->endTransmission() == 0; +} + +bool i2c_gpio_stc_l10::pinMode(uint8_t pin, uint8_t mode) +{ + const i2c_gpio_pin_map_t* m = find_map(pin); + if(!m) return false; + + uint8_t v; + if(!read_reg(m->read_reg, v)) return false; + + uint8_t mask = (uint8_t)(1U << m->bit); + if(mode == OUTPUT) + v &= (uint8_t)~mask; + else + v |= mask; + if(!pre_write(m)) return false; + return write_reg(m, m->read_reg, v); +} + +bool i2c_gpio_stc_l10::digitalWrite(uint8_t pin, uint8_t val) +{ + const i2c_gpio_pin_map_t* m = find_map(pin); + if(!m) return false; + + uint8_t rd[k_read_buf_max] = {0}; + const uint8_t nbytes = read_nbytes(m); + if(nbytes == 0 || nbytes > sizeof(rd)) return false; + + _wire->beginTransmission(_addr); + _wire->write(m->read_reg); + if(_wire->endTransmission(false) != 0) return false; + + uint8_t got = _wire->requestFrom((int)_addr, (int)nbytes); + if(got != nbytes) return false; + + for(uint8_t i = 0; i < nbytes; i++) { + if(!_wire->available()) return false; + rd[i] = _wire->read(); + } + + uint8_t relay = rd[data_byte_index(m, nbytes)]; + uint8_t mask = (uint8_t)(1U << m->bit); + + if(val) + relay |= mask; + else + relay &= (uint8_t)~mask; + + if(m->pre_write_len > 0) { + _wire->beginTransmission(_addr); + _wire->write(m->init_reg); + for(uint8_t i = 0; i < m->pre_write_len; i++) { + _wire->write(m->pre_write[i]); + } + if(_wire->endTransmission() != 0) return false; + } + + if(m->write_seq_len > sizeof(m->write_seq) || m->post_write_len > sizeof(m->post_write)) return false; + + _wire->beginTransmission(_addr); + _wire->write(m->write_reg); + for(uint8_t i = 0; i < m->write_seq_len; i++) { + _wire->write(m->write_seq[i]); + } + _wire->write(relay); + append_post_write(_wire, m); + if(_wire->endTransmission() != 0) return false; + + return true; +} + +int i2c_gpio_stc_l10::digitalRead(uint8_t pin) +{ + const i2c_gpio_pin_map_t* m = find_map(pin); + if(!m) return -1; + if(m->is_digital) return analogReadAndCompare(m, pin); + return digitalRead_internal(m); +} + +int i2c_gpio_stc_l10::digitalRead_internal(const i2c_gpio_pin_map_t* m) +{ + const uint8_t nbytes = read_nbytes(m); + if(nbytes <= 1) { + uint8_t v; + if(!read_reg(m->read_reg, v)) return -1; + return (int)((v >> m->bit) & 1u); + } + + uint8_t buf[k_read_buf_max]; + if(nbytes > sizeof(buf)) return -1; + if(!writepointer_read(m->read_reg, buf, nbytes)) return -1; + + uint8_t b = buf[data_byte_index(m, nbytes)]; + return (int)((b >> m->bit) & 1u); +} + +int i2c_gpio_stc_l10::analogReadAndCompare(const i2c_gpio_pin_map_t* m, uint8_t pin) +{ + const int raw = analogRead8(pin); + if(raw < 0) return -1; + const uint8_t b = (uint8_t)raw; + if(b == m->on_value) return 1; + if(b == m->off_value) return 0; + return -1; +} + +bool i2c_gpio_stc_l10::digitalReadBytes(uint8_t pin, uint8_t* val, uint8_t read_len) +{ + const i2c_gpio_pin_map_t* m = find_map(pin); + if(!m) return false; + return writepointer_read(m->read_reg, val, read_len); +} + +bool i2c_gpio_stc_l10::analogWrite8(uint8_t pin, uint8_t value) +{ + const i2c_gpio_pin_map_t* m = find_map(pin); + if(!m) return false; + if(!pre_write(m)) return false; + return write_reg(m, m->write_reg, value); +} + +int i2c_gpio_stc_l10::analogRead8(uint8_t pin) +{ + const i2c_gpio_pin_map_t* m = find_map(pin); + if(!m) return -1; + + const uint8_t nbytes = read_nbytes(m); + if(nbytes <= 1) { + uint8_t v; + if(!read_reg(m->read_reg, v)) return -1; + return (int)v; + } + + uint8_t buf[k_read_buf_max]; + if(nbytes > sizeof(buf)) return -1; + if(!writepointer_read(m->read_reg, buf, nbytes)) return -1; + const uint8_t idx = data_byte_index(m, nbytes); + return (int)buf[idx]; +} + +#endif diff --git a/src/drv/gpio/i2c_gpio_stc_l10.h b/src/drv/gpio/i2c_gpio_stc_l10.h new file mode 100644 index 000000000..d834b52c1 --- /dev/null +++ b/src/drv/gpio/i2c_gpio_stc_l10.h @@ -0,0 +1,39 @@ +/* MIT License - Copyright (c) 2019-2024 Francis Van Roie + Lanbon L10 / STC-style I2C relay & backlight expander @ 0x20. */ + +#pragma once + +#include "hasp_conf.h" + +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO) && defined(HASP_EXPANDER_GPIO_STC_L10) + +#include "sys/gpio/i2c_gpio_bus.h" + +class i2c_gpio_stc_l10 : public i2c_gpio_bus { + public: + explicit i2c_gpio_stc_l10(uint8_t i2c_addr, TwoWire& wire); + + bool begin() override; + bool handles_pin(uint8_t pin) const override; + bool pinMode(uint8_t pin, uint8_t mode) override; + bool digitalWrite(uint8_t pin, uint8_t val) override; + int digitalRead(uint8_t pin) override; + bool digitalReadBytes(uint8_t pin, uint8_t* val, uint8_t read_len) override; + bool analogWrite8(uint8_t pin, uint8_t value) override; + int analogRead8(uint8_t pin) override; + + private: + int digitalRead_internal(const i2c_gpio_pin_map_t* m); + int analogReadAndCompare(const i2c_gpio_pin_map_t* m, uint8_t pin); + + const i2c_gpio_pin_map_t* find_map(uint8_t pin) const; + bool pre_write(const i2c_gpio_pin_map_t* m); + bool read_reg(uint8_t reg, uint8_t& val); + bool write_reg(const i2c_gpio_pin_map_t* m, uint8_t reg, uint8_t val); + bool writepointer_read(uint8_t reg, uint8_t* val, uint8_t pre_read_len); + + uint8_t _addr; + TwoWire* _wire; +}; + +#endif diff --git a/src/drv/tft/Arduino_PCA9535SWSPI.h b/src/drv/tft/Arduino_PCA9535SWSPI.h index c54c7904e..6a038d4f1 100644 --- a/src/drv/tft/Arduino_PCA9535SWSPI.h +++ b/src/drv/tft/Arduino_PCA9535SWSPI.h @@ -48,6 +48,7 @@ class Arduino_PCA9535SWSPI : public Arduino_DataBus protected: void writeRegister(uint8_t reg, uint8_t *data, size_t len); uint8_t readRegister(uint8_t reg, uint8_t *data, size_t len); + void writeRegisterI2C(uint8_t reg, uint8_t data); uint8_t _address; bool is_found; diff --git a/src/drv/tft/Arduino_TCA9554SWSPI.cpp b/src/drv/tft/Arduino_TCA9554SWSPI.cpp new file mode 100644 index 000000000..727ba4e55 --- /dev/null +++ b/src/drv/tft/Arduino_TCA9554SWSPI.cpp @@ -0,0 +1,333 @@ +#include "Arduino_TCA9554SWSPI.h" + +#if defined(HASP_USE_ARDUINOGFX) + +#include "driver/gpio.h" +#include "hasplib.h" + +Arduino_TCA9554SWSPI::Arduino_TCA9554SWSPI(int8_t i2c_sda, int8_t i2c_scl, int8_t spi_sdio, int8_t spi_sclk, int8_t spi_cs, TwoWire *wire) + : _address(TCA9554_IIC_ADDRESS), + is_found(false), + _i2c_sda(i2c_sda), + _i2c_scl(i2c_scl), + _spi_sdio(spi_sdio), + _spi_sclk(spi_sclk), + _spi_cs(spi_cs), + _wire(wire), + _output_cache(TCA9554_IDLE_STATE) +{ +} + +bool Arduino_TCA9554SWSPI::begin(int32_t speed, int8_t dataMode) { + _wire->begin(_i2c_sda, _i2c_scl); + + // 1. [03 FF] -> [01 FF] Reset Jolt + uint8_t val_ff = 0xFF; + writeRegister(0x03, &val_ff, 1); + writeRegister(0x01, &val_ff, 1); + delay(5); + + // 2. [03 F1] Handshake Config + uint8_t val_f1 = 0xF1; + writeRegister(0x03, &val_f1, 1); + delayMicroseconds(55); + + // 3. [01 FB] -> [01 F3] Power Enable + uint8_t val_fb = 0xFB; + writeRegister(0x01, &val_fb, 1); + delayMicroseconds(50); + uint8_t val_f3 = 0xF3; + writeRegister(0x01, &val_f3, 1); + + delay(55); + + // 4. THE MAGIC SYNC [00 00] + // Manual I2C Write to Register 0x00 to avoid syntax errors + _wire->beginTransmission(_address); + _wire->write(0x00); // Register Pointer + _wire->write(0x00); // Data Value + _wire->endTransmission(); + + delay(120); + + is_found = true; + return true; +} + +// Internal helper to update the TCA9554 pins over I2C +void Arduino_TCA9554SWSPI::_update_tca(uint8_t state) { + Wire.beginTransmission(TCA9554_IIC_ADDRESS); + Wire.write(TCA9554_OUTPUT_PORT_REG); + Wire.write(state); + Wire.endTransmission(true); +} + +// The core dispatcher that aligns everything +void Arduino_TCA9554SWSPI::_send_9bit(bool is_data, uint8_t value) { + // 1. Prepare 9th bit + if (is_data) _current_pins |= BIT_MOSI; + else _current_pins &= ~BIT_MOSI; + + // Pulse Clock (3 I2C writes) + _update_tca(_current_pins & ~BIT_CLK); + _update_tca(_current_pins | BIT_CLK); + _update_tca(_current_pins & ~BIT_CLK); + + // 2. Data Bits + for (int i = 7; i >= 0; i--) { + if ((value >> i) & 0x01) _current_pins |= BIT_MOSI; + else _current_pins &= ~BIT_MOSI; + + // Optimized Clock Pulse + _update_tca(_current_pins & ~BIT_CLK); + _update_tca(_current_pins | BIT_CLK); + _update_tca(_current_pins & ~BIT_CLK); + } + + // 3. Latch (Yellow Pulse) + _update_tca(_current_pins | BIT_EN); + _update_tca(_current_pins & ~BIT_EN); +} + + +// Internal helper to pulse the clock +void Arduino_TCA9554SWSPI::_pulse_clock() { + _update_tca(_current_pins & ~BIT_CLK); // CLK Low + _update_tca(_current_pins | BIT_CLK); // CLK High (Rising Edge) + _update_tca(_current_pins & ~BIT_CLK); // CLK Low +} + +void Arduino_TCA9554SWSPI::write9(bool is_data, uint8_t byte) { + _send_9bit(is_data, byte); +} + +void Arduino_TCA9554SWSPI::write(uint8_t d) +{ + _send_9bit(true, d); +} + +void Arduino_TCA9554SWSPI::beginWrite() +{ +} + +void Arduino_TCA9554SWSPI::endWrite() +{ +} + +void Arduino_TCA9554SWSPI::writeCommand(uint8_t c) +{ + _send_9bit(false, c); +} + +void Arduino_TCA9554SWSPI::writeCommand16(uint16_t c) +{ + writeCommand((uint8_t)(c >> 8)); + writeCommand((uint8_t)(c & 0xFF)); +} + +void Arduino_TCA9554SWSPI::write16(uint16_t d) +{ + write((uint8_t)(d >> 8)); + write((uint8_t)(d & 0xFF)); +} + +void Arduino_TCA9554SWSPI::writeCommandBytes(uint8_t *data, uint32_t len) +{ + if (!data || !len) return; + + beginWrite(); + writeCommand(data[0]); + for (uint32_t i = 1; i < len; ++i) { + write(data[i]); + } + endWrite(); +} + +void Arduino_TCA9554SWSPI::writeBytes(uint8_t *data, uint32_t len) +{ + if (!data || !len) return; + + beginWrite(); + for (uint32_t i = 0; i < len; ++i) { + write(data[i]); + } + endWrite(); +} + +// Exposed through Arduino_DataBus when USE_I2C_SW_SPI is enabled. +#if defined(USE_I2C_SW_SPI) +void Arduino_TCA9554SWSPI::writeRegisterI2COpcode(uint8_t reg, uint8_t data) +{ + writeRegisterI2C(reg, data); +} + +void Arduino_TCA9554SWSPI::writeRegisterI2CSeqOpcode(uint8_t reg, const uint8_t *data, size_t len) +{ + writeRegister(reg, data, len); +} +#endif + +// Custom Methods + +void Arduino_TCA9554SWSPI::flushOutput() +{ + if (!is_found) return; + + uint8_t out = (_output_cache | MASK_POWER_ON); + writeRegister(TCA9554_OUTPUT_PORT_REG, &out, 1); +} + +void Arduino_TCA9554SWSPI::closeOut() { + // --- Phase 1: SPI Wake & Calibration --- + + // 11 - Sleep Out + beginWrite(); + write9(false, 0x11); + endWrite(); + delay(121); + + // 29 - Display ON + beginWrite(); + write9(false, 0x29); + endWrite(); + delay(22); // The 'delay 29' from stock + + // 29 - Display ON + beginWrite(); + write9(false, 0x29); + endWrite(); + delay(2); // The 'delay 29' from stock + + // B1 13 - First Tap + beginWrite(); + write9(false, 0xB1); + write9(true, 0x13); + endWrite(); + delay(40); // The 'delay 35' from stock + + // B1 13 - Second Tap + beginWrite(); + write9(false, 0xB1); + write9(true, 0x13); + endWrite(); + + // --- Phase 2: Hardware Cool Down (The Hand-off) --- + + // 01 F1 - MOSI Low + // uint8_t mosi_low = 0xF1; + // writeRegister(0x01, &mosi_low, 1); + + // 01 F3 - CS High + // uint8_t cs_high = 0xF3; + // writeRegister(0x01, &cs_high, 1); + + writeRegisterI2C(0x01, 0xB1); + delay(435); + writeRegisterI2C(0x01, 0xB1); + delay(435); + + writeRegisterI2C(0x01, 0xF1); + writeRegisterI2C(0x01, 0xF3); + + // 2F 00 - 19 Times + for(int i = 0; i < 19; i++) { + writeRegisterI2C(0x2F, 0x00); + } +} + +void Arduino_TCA9554SWSPI::lcd_power_and_brightness(uint8_t level) { + + // 2. Set Brightness (Verified 6-byte sequence from your CSV) + // Address 0x20, Command 0x21, Sub 0x01, Level, 0x00, 0x00, 0x14 + if (level > 100) level = 100; + + LOG_INFO(TAG_TFT, F("Lanbon L10 brightness set to %u"), level); + + Wire.beginTransmission(0x20); + Wire.write(0x21); + Wire.write(0x01); + Wire.write(level); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x14); + Wire.endTransmission(); +} + + +// 0x27·0x00·0x01·0x01·0x00·0x00·0x0B +void Arduino_TCA9554SWSPI::initMotionSensorRegistor() { + Wire.beginTransmission(TCA9554_IIC_ADDRESS); + Wire.write(0x27); + Wire.write(0x00); + Wire.write(0x01); + Wire.write(0x01); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x0B); + Wire.endTransmission(); +} + +// W[0x20]·0x1B·0x90·0x80·0x00·0x00·0x00·0x0D·0xAE·0x33·0xAF·0x8D·0xAE·0x33·0xAF· +// 0x80·0x00·0x00·0x00·0x00·0x01·0x80·0x00 +void Arduino_TCA9554SWSPI::initMotionSensor() { + Wire.beginTransmission(TCA9554_IIC_ADDRESS); + Wire.write(0x1B); + Wire.write(0x90); + Wire.write(0x80); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x0D); + Wire.write(0xAE); + Wire.write(0x33); + Wire.write(0xAF); + Wire.write(0x8D); + Wire.write(0xAE); + Wire.write(0x33); + Wire.write(0xAF); + Wire.write(0x80); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x00); + Wire.write(0x01); + Wire.write(0x80); + Wire.write(0x00); + Wire.endTransmission(); +} + +void Arduino_TCA9554SWSPI::writeRegister(uint8_t reg, const uint8_t *data, size_t len) +{ + _wire->beginTransmission(_address); + _wire->write(reg); + for (size_t i = 0; i < len; ++i) { + _wire->write(data[i]); + } + + _wire->endTransmission(); +} + +void Arduino_TCA9554SWSPI::writeRegisterI2C(uint8_t reg, uint8_t data) +{ + _wire->beginTransmission(_address); + _wire->write(reg); + _wire->write(data); + _wire->endTransmission(); +} + +uint8_t Arduino_TCA9554SWSPI::readRegister(uint8_t reg, uint8_t *data, size_t len) +{ + _wire->beginTransmission(_address); + _wire->write(reg); + if (_wire->endTransmission(false) != 0) { + return 0; + } + + size_t got = _wire->requestFrom((int)_address, (int)len); + for (size_t i = 0; i < got; ++i) { + data[i] = _wire->read(); + } + return (uint8_t)got; +} + +#endif \ No newline at end of file diff --git a/src/drv/tft/Arduino_TCA9554SWSPI.h b/src/drv/tft/Arduino_TCA9554SWSPI.h new file mode 100644 index 000000000..57b3ce945 --- /dev/null +++ b/src/drv/tft/Arduino_TCA9554SWSPI.h @@ -0,0 +1,98 @@ +#ifndef _Arduino_TCA9554SWSPI_H_ +#define _Arduino_TCA9554SWSPI_H_ + +#if defined(HASP_USE_ARDUINOGFX) +#warning Arduino_TCA9554SWSPI.h + +#include +#include +#include "Arduino_DataBus.h" + +#define TCA9554_IIC_ADDRESS 0x20 + +#define TCA9554_INPUT_PORT_REG 0x00 +#define TCA9554_OUTPUT_PORT_REG 0x01 +#define TCA9554_INVERSION_PORT_REG 0x02 +#define TCA9554_CONFIG_PORT_REG 0x03 + +// Verified from stock trace +#define PIN_TCA_BL 0 +#define PIN_TCA_CS 1 +#define PIN_TCA_CLK 2 +#define PIN_TCA_SDIO 3 +#define MASK_POWER_ON 0xF0 + +#define BIT_TCA_BL (1U << PIN_TCA_BL) +#define BIT_TCA_CS (1U << PIN_TCA_CS) +#define BIT_TCA_CLK (1U << PIN_TCA_CLK) +#define BIT_TCA_SDIO (1U << PIN_TCA_SDIO) + +// Pin definitions on the TCA9554 +#define BIT_MOSI (1 << 3) +#define BIT_CLK (1 << 2) +#define BIT_EN (1 << 1) + +#define TCA9554_IDLE_STATE (MASK_POWER_ON | BIT_TCA_CS) + +static uint8_t _current_pins = 0; + +class Arduino_TCA9554SWSPI : public Arduino_DataBus +{ +public: + Arduino_TCA9554SWSPI(int8_t i2c_sda, int8_t i2c_scl, int8_t spi_sdio, int8_t spi_sclk, int8_t spi_cs, TwoWire *wire = &Wire); + + bool begin(int32_t speed = GFX_NOT_DEFINED, int8_t dataMode = GFX_NOT_DEFINED) override; + + void _send_9bit(bool is_data, uint8_t value); + + void _update_tca(uint8_t state); + + void _pulse_clock(); + + void beginWrite() override; + void endWrite() override; + + void writeCommand(uint8_t c) override; + void writeCommand16(uint16_t c) override; + + void write(uint8_t d) override; + void write16(uint16_t d) override; + + void writeCommandBytes(uint8_t *data, uint32_t len) override; + void writeBytes(uint8_t *data, uint32_t len) override; +#if defined(USE_I2C_SW_SPI) + void writeRegisterI2COpcode(uint8_t reg, uint8_t data) override; + void writeRegisterI2CSeqOpcode(uint8_t reg, const uint8_t *data, size_t len) override; +#endif + void closeOut(); + void lcd_power_and_brightness(uint8_t level); + void initMotionSensor(); + void initMotionSensorRegistor(); + +protected: + void writeRegister(uint8_t reg, const uint8_t *data, size_t len); + void writeRegisterI2C(uint8_t reg, uint8_t data); + uint8_t readRegister(uint8_t reg, uint8_t *data, size_t len); + + uint8_t _address; + bool is_found; + + int8_t _i2c_sda, _i2c_scl, _spi_sdio, _spi_sclk, _spi_cs; + TwoWire *_wire; + +private: + uint8_t _output_cache; + + void flushOutput(); + void send_spi_bit(bool bit_val); + void write9(bool is_data, uint8_t byte); + +public: + // Required by Arduino_DataBus, intentionally unused for this bus. + void writeRepeat(uint16_t, uint32_t) override {} + void writePixels(uint16_t *, uint32_t) override {} +}; + +#endif + +#endif // _Arduino_TCA9554SWSPI_H_ \ No newline at end of file diff --git a/src/drv/tft/tft_driver_arduinogfx.cpp b/src/drv/tft/tft_driver_arduinogfx.cpp index 402601ad8..9fd1f8e08 100644 --- a/src/drv/tft/tft_driver_arduinogfx.cpp +++ b/src/drv/tft/tft_driver_arduinogfx.cpp @@ -8,6 +8,11 @@ #include "Arduino_RGBPanel_mod.h" #include "Arduino_RGB_Display_mod.h" #include "Arduino_PCA9535SWSPI.h" +#include "Arduino_TCA9554SWSPI.h" + +#ifndef TFT_AUTO_FLUSH +#define TFT_AUTO_FLUSH 1 +#endif namespace dev { @@ -125,6 +130,44 @@ void ArduinoGfx::init(int w, int h) tft = new Arduino_NV3041A(bus, TFT_RST, TFT_ROTATION, TFT_IPS); tft->begin(GFX_NOT_DEFINED); // Used for RFB displays +#elif defined(HASP_LANBON_L10_TCA9554_GC9503V) + LOG_INFO(TAG_TFT, F("Lanbon L10: GC9503V RGB panel + TCA9554 3-wire init bus")); + // Lanbon L10: GC9503V RGB panel + TCA9554 3-wire init bus + // I2C bus: GPIO 8 / 18 + // TCA9554 bits: MOSI=1, SCK=2, CS=3, RST=4, BL=7 + + // 1. Setup the 9-bit I2C Expand Bus + Arduino_DataBus* bus = new Arduino_TCA9554SWSPI( + EXPANDER_I2C_SDA, + EXPANDER_I2C_SCL, + EXPANDER_MOSI, + EXPANDER_SCK, + EXPANDER_CS, + &Wire); + + Arduino_ESP32RGBPanel* rgbpanel = new Arduino_ESP32RGBPanel( + TFT_DE, TFT_VSYNC, TFT_HSYNC, TFT_PCLK, TFT_R0, TFT_R1, TFT_R2, TFT_R3, TFT_R4, TFT_G0, TFT_G1, TFT_G2, TFT_G3, + TFT_G4, TFT_G5, TFT_B0, TFT_B1, TFT_B2, TFT_B3, TFT_B4, TFT_HSYNC_POLARITY, TFT_HSYNC_FRONT_PORCH, + TFT_HSYNC_PULSE_WIDTH, TFT_HSYNC_BACK_PORCH, TFT_VSYNC_POLARITY, TFT_VSYNC_FRONT_PORCH, TFT_VSYNC_PULSE_WIDTH, + TFT_VSYNC_BACK_PORCH + ); + + tft = new Arduino_RGB_Display(TFT_WIDTH, TFT_HEIGHT, rgbpanel, 0, true /* auto_flush */, bus, GFX_NOT_DEFINED, + gc9503v_l10_init_operations, sizeof(gc9503v_l10_init_operations)); + + tft->begin(GFX_NOT_DEFINED); + + // ((Arduino_TCA9554SWSPI*)bus)->closeOut(); + // Data 13 +//delay(10); +//((Arduino_TCA9554SWSPI*)bus)->lcd_power_and_brightness(0x64); + +delay(10); +((Arduino_TCA9554SWSPI*)bus)->initMotionSensor(); + + + LOG_INFO(TAG_TFT, F("Brightness set to max")); + #elif 1 /* Reset is not implemented in the panel */ if(TFT_RST != GFX_NOT_DEFINED) { @@ -298,6 +341,13 @@ void IRAM_ATTR ArduinoGfx::flush_pixels(lv_disp_drv_t* disp, const lv_area_t* ar bool ArduinoGfx::is_driver_pin(uint8_t pin) { +#if defined(HASP_LANBON_L10_TCA9554_GC9503V) + // On Lanbon L10 these GPIOs are not used by the TFT bus and should be available for GPIO config + if(pin == 32 || pin == 35 || pin == 0) { + return false; + } +#endif + if(false // start condition is always needed // Use individual checks instead of switch statement, as some case labels could be duplicated diff --git a/src/hasp/hasp.cpp b/src/hasp/hasp.cpp index c02067648..36917a53b 100644 --- a/src/hasp/hasp.cpp +++ b/src/hasp/hasp.cpp @@ -76,6 +76,9 @@ bool hasp_first_touch_state = false; // Track first touch state static uint16_t sleepTimeShort = 60; // 1 second resolution static uint16_t sleepTimeLong = 120; // 1 second resolution static uint32_t sleepTimeOffset = 0; // 1 second resolution +static bool auto_dim_active = false; +static bool auto_dim_prev_power = true; +static uint8_t auto_dim_prev_level = 255; uint8_t haspStartDim = HASP_START_DIM; uint8_t haspStartPage = HASP_START_PAGE; @@ -117,6 +120,20 @@ HASP_ATTRIBUTE_FAST_MEM void hasp_update_sleep_state() uint32_t idle = lv_disp_get_inactive_time(lv_disp_get_default()) / 1000; idle += sleepTimeOffset; // To force a specific state + bool should_dim = gui_get_auto_dim_on_idle() && sleepTimeShort > 0 && idle >= sleepTimeShort; + if(should_dim && !auto_dim_active) { + auto_dim_prev_power = haspDevice.get_backlight_power(); + auto_dim_prev_level = haspDevice.get_backlight_level(); + auto_dim_active = true; + dispatch_backlight(NULL, "{\"state\":\"on\",\"brightness\":1}", TAG_HASP); + } else if(!should_dim && auto_dim_active) { + char payload[64]; + snprintf_P(payload, sizeof(payload), PSTR("{\"state\":\"%s\",\"brightness\":%u}"), + auto_dim_prev_power ? "on" : "off", auto_dim_prev_level); + dispatch_backlight(NULL, payload, TAG_HASP); + auto_dim_active = false; + } + if(sleepTimeLong > 0 && idle >= (sleepTimeShort + sleepTimeLong)) { if(hasp_sleep_state != HASP_SLEEP_LONG) { gui_hide_pointer(true); diff --git a/src/hasp/hasp_dispatch.cpp b/src/hasp/hasp_dispatch.cpp index 7a51e29c0..67962e910 100644 --- a/src/hasp/hasp_dispatch.cpp +++ b/src/hasp/hasp_dispatch.cpp @@ -1179,6 +1179,9 @@ void dispatch_backlight(const char*, const char* payload, uint8_t source) bool power = haspDevice.get_backlight_power(); uint8_t level = haspDevice.get_backlight_level(); + LOG_INFO(TAG_MSGR, F("dispatch_backlight src=%u payload='%s' start power=%u level=%u"), + source, payload ? payload : "", power, level); + // Set the current state if(strlen(payload) != 0) { StaticJsonDocument<128> json; @@ -1224,6 +1227,7 @@ void dispatch_backlight(const char*, const char* payload, uint8_t source) } // toggle power and wakeup touch if changed + LOG_INFO(TAG_MSGR, F("dispatch_backlight applying power=%u level=%u"), power, level); if(power) haspDevice.set_backlight_level(level); // set level before power on if(haspDevice.get_backlight_power() != power) { haspDevice.set_backlight_power(power); diff --git a/src/hasp_config.h b/src/hasp_config.h index 04160d30d..bdbf07a99 100644 --- a/src/hasp_config.h +++ b/src/hasp_config.h @@ -87,12 +87,14 @@ const char FP_GUI_IDLEPERIOD2[] PROGMEM = "idle2"; const char FP_GUI_CALIBRATION[] PROGMEM = "calibration"; const char FP_GUI_BACKLIGHTPIN[] PROGMEM = "bckl"; const char FP_GUI_BACKLIGHTINVERT[] PROGMEM = "bcklinv"; +const char FP_GUI_AUTODIM[] PROGMEM = "autodim"; const char FP_GUI_POINTER[] PROGMEM = "cursor"; const char FP_GUI_LONG_TIME[] PROGMEM = "long"; 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_ACTION[] PROGMEM = "action"; const char FP_HASP_CONFIG_FILE[] PROGMEM = "/config.json"; diff --git a/src/hasp_debug.cpp b/src/hasp_debug.cpp index c249ece58..9bdab1b55 100644 --- a/src/hasp_debug.cpp +++ b/src/hasp_debug.cpp @@ -289,6 +289,9 @@ void debug_get_tag(uint8_t tag, char* buffer) case TAG_GPIO: memcpy_P(buffer, PSTR("GPIO"), 5); break; + case TAG_LEDC: + memcpy_P(buffer, PSTR("LEDC"), 5); + break; case TAG_TIME: memcpy_P(buffer, PSTR("TIME"), 5); diff --git a/src/hasp_debug.h b/src/hasp_debug.h index ea77d54a1..ddda97808 100644 --- a/src/hasp_debug.h +++ b/src/hasp_debug.h @@ -182,6 +182,7 @@ enum { TAG_FILE = 31, TAG_NVS = 32, TAG_GPIO = 40, + TAG_LEDC = 41, TAG_ETH = 60, TAG_WIFI = 61, diff --git a/src/hasp_gui.cpp b/src/hasp_gui.cpp index 17eb72bef..159b605b9 100644 --- a/src/hasp_gui.cpp +++ b/src/hasp_gui.cpp @@ -52,6 +52,7 @@ static TaskHandle_t g_lvgl_task_handle; gui_conf_t gui_settings = {.show_pointer = false, .backlight_pin = TFT_BCKL, + .auto_dim_on_idle = false, .rotation = TFT_ROTATION, .invert_display = INVERT_COLORS, .cal_data = {0, 65535, 0, 65535, 0}}; @@ -496,6 +497,9 @@ bool guiGetConfig(const JsonObject& settings) if(gui_settings.backlight_pin != settings[FPSTR(FP_GUI_BACKLIGHTPIN)].as()) changed = true; settings[FPSTR(FP_GUI_BACKLIGHTPIN)] = gui_settings.backlight_pin; + if(gui_settings.auto_dim_on_idle != settings[FPSTR(FP_GUI_AUTODIM)].as()) changed = true; + settings[FPSTR(FP_GUI_AUTODIM)] = (uint8_t)gui_settings.auto_dim_on_idle; + if(backlight_invert != settings[FPSTR(FP_GUI_BACKLIGHTINVERT)].as()) changed = true; settings[FPSTR(FP_GUI_BACKLIGHTINVERT)] = (uint8_t)backlight_invert; @@ -572,6 +576,7 @@ bool guiSetConfig(const JsonObject& settings) // changed |= configSet(guiTickPeriod, settings[FPSTR(FP_GUI_TICKPERIOD)], F("guiTickPeriod")); changed |= configSet(gui_settings.backlight_pin, settings[FPSTR(FP_GUI_BACKLIGHTPIN)], F("guiBacklightPin")); + changed |= configSet(gui_settings.auto_dim_on_idle, settings[FPSTR(FP_GUI_AUTODIM)], F("guiAutoDimOnIdle")); changed |= configSet(backlight_invert, settings[FPSTR(FP_GUI_BACKLIGHTINVERT)], F("guiBacklightInvert")); changed |= configSet(guiSleepTime1, settings[FPSTR(FP_GUI_IDLEPERIOD1)], F("guiSleepTime1")); changed |= configSet(guiSleepTime2, settings[FPSTR(FP_GUI_IDLEPERIOD2)], F("guiSleepTime2")); @@ -629,6 +634,11 @@ bool guiSetConfig(const JsonObject& settings) return changed; } + +bool gui_get_auto_dim_on_idle() +{ + return gui_settings.auto_dim_on_idle; +} #endif // HASP_USE_CONFIG /* **************************** SCREENSHOTS ************************************** */ diff --git a/src/hasp_gui.h b/src/hasp_gui.h index dd3680a80..257639e62 100644 --- a/src/hasp_gui.h +++ b/src/hasp_gui.h @@ -31,6 +31,7 @@ struct gui_conf_t { bool show_pointer; int8_t backlight_pin; + bool auto_dim_on_idle; uint8_t rotation; uint8_t invert_display; #if defined(USER_SETUP_LOADED) @@ -55,6 +56,7 @@ void guiTakeScreenshot(const char* pFileName); // to file void guiTakeScreenshot(void); // webclient bool guiScreenshotIsDirty(); uint32_t guiScreenshotEtag(); +bool gui_get_auto_dim_on_idle(); /* ===== Callbacks ===== */ void gui_flush_cb(lv_disp_drv_t* disp, const lv_area_t* area, lv_color_t* color_p); diff --git a/src/sys/gpio/hasp_gpio.cpp b/src/sys/gpio/hasp_gpio.cpp index ec083dcd0..35b509308 100644 --- a/src/sys/gpio/hasp_gpio.cpp +++ b/src/sys/gpio/hasp_gpio.cpp @@ -7,6 +7,13 @@ #include "hasp_gpio.h" +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO_ARCH_ESP32) +#include "sys/gpio/i2c_gpio_bus.h" +#endif +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) && defined(ARDUINO_ARCH_ESP32) +#include "drv/gpio/i2c_gpio_stc_l10.h" +#endif + // Device Drivers #include "dev/device.h" #include "drv/tft/tft_driver.h" @@ -20,8 +27,19 @@ #include "AceButton.h" using namespace ace_button; + +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO_ARCH_ESP32) +/** AceButton reads virtual expander pins via i2c_gpio; others use digitalRead(). */ +class HaspI2cButtonConfig : public ButtonConfig { + public: + int readButton(uint8_t pin) override; +}; +HaspI2cButtonConfig buttonConfig; // Clicks, double-clicks and long presses +HaspI2cButtonConfig switchConfig; // Clicks only (switch, motion, …) +#else ButtonConfig buttonConfig; // Clicks, double-clicks and long presses ButtonConfig switchConfig; // Clicks only +#endif #else #define HIGH 1 @@ -159,6 +177,17 @@ static void gpio_event_handler(AceButton* button, uint8_t eventType, uint8_t but gpioConfig[btnid].power = Parser::get_event_state(eventid); gpio_input_event(gpioConfig[btnid].pin, eventid); +#if defined(USE_MOTION_WAKEUP) + /* GPIO Input "Action" = Backlight-ON (binary sensors SWITCH..WINDOW). */ + if(eventid == HASP_EVENT_ON && gpioConfig[btnid].input_action == hasp_gpio_input_action_t::INPUT_ACTION_BACKLIGHT_ON + && gpioConfig[btnid].type >= hasp_gpio_type_t::SWITCH && gpioConfig[btnid].type <= hasp_gpio_type_t::WINDOW) { + bool antiburn_changed = hasp_stop_antiburn(); + dispatch_wakeup(TAG_GPIO); // clears idle first (wakeup path) + hasp_update_sleep_state(); + if(antiburn_changed) dispatch_state_antiburn(hasp_get_antiburn()); + } +#endif + // update objects and gpios in this group if(gpioConfig[btnid].group && eventid != HASP_EVENT_LONG) // do not repeat DOWN + LONG gpio_update_group(gpioConfig[btnid].group, NULL, gpioConfig[btnid].power, state, HASP_EVENT_OFF, HASP_EVENT_ON); @@ -209,6 +238,34 @@ void aceButtonSetup(void) #endif } +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO_ARCH_ESP32) +namespace { +/** Per-pin last known level when i2c digitalRead is 0/1; if read returns -1, replay last. */ +uint8_t s_i2c_prev_level[256]; +bool s_i2c_prev_valid[256]; +} // namespace + +int HaspI2cButtonConfig::readButton(uint8_t pin) +{ + if(i2c_gpio && i2c_gpio->handles_pin(pin)) { + const int v = i2c_gpio->digitalRead(pin); + if(v > 0) { + s_i2c_prev_level[pin] = HIGH; + s_i2c_prev_valid[pin] = true; + return HIGH; + } + if(v == 0) { + s_i2c_prev_level[pin] = LOW; + s_i2c_prev_valid[pin] = true; + return LOW; + } + if(s_i2c_prev_valid[pin]) return (int)s_i2c_prev_level[pin]; + return LOW; + } + return digitalRead(pin); +} +#endif + // Can be called ad-hoc to change a setup static void gpio_setup_pin(uint8_t index) { @@ -250,14 +307,20 @@ static void gpio_setup_pin(uint8_t index) if(gpio->btn) delete gpio->btn; gpio->btn = new AceButton(&switchConfig, gpio->pin, default_state, index); gpio->power = gpio->btn->isPressedRaw(); - pinMode(gpio->pin, input_mode); +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO_ARCH_ESP32) + if(!(i2c_gpio && i2c_gpio->handles_pin(gpio->pin))) +#endif + pinMode(gpio->pin, input_mode); gpio->max = 0; break; case hasp_gpio_type_t::BUTTON_TYPE: if(gpio->btn) delete gpio->btn; gpio->btn = new AceButton(&buttonConfig, gpio->pin, default_state, index); gpio->power = gpio->btn->isPressedRaw(); - pinMode(gpio->pin, input_mode); +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO_ARCH_ESP32) + if(!(i2c_gpio && i2c_gpio->handles_pin(gpio->pin))) +#endif + pinMode(gpio->pin, input_mode); gpio->max = 0; break; #if defined(ARDUINO_ARCH_ESP32) @@ -272,6 +335,16 @@ static void gpio_setup_pin(uint8_t index) case hasp_gpio_type_t::POWER_RELAY: case hasp_gpio_type_t::LIGHT_RELAY: +#if defined(HASP_USE_I2C_GPIO) + if(i2c_gpio && i2c_gpio->handles_pin(gpio->pin)) { + i2c_gpio->pinMode(gpio->pin, OUTPUT); + gpio->power = gpio->inverted; + gpio->max = 1; + gpio->val = gpio->power; + i2c_gpio->digitalWrite(gpio->pin, gpio->power ? HIGH : LOW); + break; + } +#endif pinMode(gpio->pin, OUTPUT); gpio->power = gpio->inverted; // gpio is off, state is set to reflect the true output state of the gpio gpio->max = 1; // on-off @@ -282,6 +355,15 @@ static void gpio_setup_pin(uint8_t index) gpio->max = 4095; case hasp_gpio_type_t::LED... hasp_gpio_type_t::LED_W: // case hasp_gpio_type_t::BACKLIGHT: +#if defined(HASP_USE_I2C_GPIO) + if(i2c_gpio && i2c_gpio->handles_pin(gpio->pin)) { + gpio->power = gpio->inverted; + gpio->val = gpio->inverted ? 0 : gpio->max; + gpio->channel = 0xFF; + i2c_gpio->analogWrite8(gpio->pin, gpio->power ? (uint8_t)gpio->val : 0); + break; + } +#endif pinMode(gpio->pin, OUTPUT); gpio->power = gpio->inverted; // gpio is off, state is set to reflect the true output state of the gpio gpio->val = gpio->inverted ? 0 : gpio->max; @@ -343,6 +425,22 @@ void gpioSetup() aceButtonSetup(); +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) && defined(ARDUINO_ARCH_ESP32) \ + && defined(EXPANDER_ADDRESS) + if(!i2c_gpio) { + i2c_gpio = new i2c_gpio_stc_l10((uint8_t)EXPANDER_ADDRESS, Wire); + if(!i2c_gpio->begin()) { + LOG_WARNING(TAG_GPIO, F("STC L10 I2C GPIO (0x%02X) not responding"), (unsigned)EXPANDER_ADDRESS); + } + else { + LOG_INFO(TAG_GPIO, F("STC L10 I2C GPIO (0x%02X) i2cgpio ready"), (unsigned)EXPANDER_ADDRESS); + } + } +#if defined(HASP_LANBON_L10_TCA9554_GC9503V) + haspDevice.set_backlight_level(haspDevice.get_backlight_level()); +#endif +#endif + for(uint8_t i = 0; i < HASP_NUM_GPIO_CONFIG; i++) { gpio_setup_pin(i); } @@ -491,6 +589,14 @@ static inline bool gpio_set_analog_value(hasp_gpio_config_t* gpio) return true; // sent #elif defined(ARDUINO_ARCH_ESP32) +#if defined(HASP_USE_I2C_GPIO) + if(i2c_gpio && i2c_gpio->handles_pin(gpio->pin)) { + val = gpio_limit(gpio->val, 0, 255); + if(!gpio->power) val = 0; + if(gpio->inverted) val = 255 - val; + return i2c_gpio->analogWrite8(gpio->pin, (uint8_t)val); + } +#endif if(gpio->max == 255) val = SCALE_8BIT_TO_10BIT(gpio->val); @@ -528,6 +634,11 @@ static inline bool gpio_set_digital_value(hasp_gpio_config_t* gpio) if(gpio->inverted) state = !state; #if defined(ARDUINO_ARCH_ESP32) +#if defined(HASP_USE_I2C_GPIO) + if(i2c_gpio && i2c_gpio->handles_pin(gpio->pin)) { + return i2c_gpio->digitalWrite(gpio->pin, state ? HIGH : LOW); + } +#endif digitalWrite(gpio->pin, state); return true; // sent @@ -789,10 +900,15 @@ bool gpioIsSystemPin(uint8_t gpio) // Serial GPIOs // Tasmota Client GPIOs +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) + if(i2c_gpio && gpio >= HASP_I2C_GPIO_UI_PIN_FIRST && gpio <= HASP_I2C_GPIO_UI_PIN_LAST) return false; +#endif + return false; } -bool gpioSavePinConfig(uint8_t config_num, uint8_t pin, uint8_t type, uint8_t group, uint8_t pinfunc, bool inverted) +bool gpioSavePinConfig(uint8_t config_num, uint8_t pin, uint8_t type, uint8_t group, uint8_t pinfunc, bool inverted, + uint8_t input_action) { // TODO: Input validation @@ -806,8 +922,12 @@ bool gpioSavePinConfig(uint8_t config_num, uint8_t pin, uint8_t type, uint8_t gr gpioConfig[config_num].group = group; gpioConfig[config_num].gpio_function = pinfunc; gpioConfig[config_num].inverted = inverted; - LOG_TRACE(TAG_GPIO, F("Saving Pin config #%d pin %d - type %d - group %d - func %d"), config_num, pin, type, - group, pinfunc); + if(type >= hasp_gpio_type_t::SWITCH && type <= hasp_gpio_type_t::WINDOW) + gpioConfig[config_num].input_action = input_action; + else + gpioConfig[config_num].input_action = 0; + LOG_TRACE(TAG_GPIO, F("Saving Pin config #%d pin %d - type %d - group %d - func %d act %u"), config_num, pin, + type, group, pinfunc, (unsigned)gpioConfig[config_num].input_action); return true; } @@ -997,6 +1117,27 @@ bool gpioGetConfig(const JsonObject& settings) changed = true; } +#if defined(USE_MOTION_WAKEUP) + { + JsonArray act = settings[FPSTR(FP_GPIO_ACTION)].as(); + if(act.isNull()) { + act = const_cast(settings).createNestedArray(FPSTR(FP_GPIO_ACTION)); + changed = true; + } + while(act.size() < HASP_NUM_GPIO_CONFIG) { + act.add((uint8_t)0); + changed = true; + } + for(uint8_t j = 0; j < HASP_NUM_GPIO_CONFIG; j++) { + const uint8_t cur = gpioConfig[j].input_action; + if(j < act.size() && act[j].as() != cur) { + act[j].set(cur); + changed = true; + } + } + } +#endif + if(changed) configOutput(settings, TAG_GPIO); return changed; } @@ -1013,9 +1154,10 @@ bool gpioSetConfig(const JsonObject& settings) { configOutput(settings, TAG_GPIO); bool changed = false; + bool status = false; if(!settings[FPSTR(FP_GPIO_CONFIG)].isNull()) { - bool status = false; + status = false; int i = 0; JsonArray array = settings[FPSTR(FP_GPIO_CONFIG)].as(); @@ -1038,6 +1180,34 @@ bool gpioSetConfig(const JsonObject& settings) changed |= status; } +#if defined(USE_MOTION_WAKEUP) + { + status = false; + if(!settings[FPSTR(FP_GPIO_ACTION)].isNull()) { + JsonArray act = settings[FPSTR(FP_GPIO_ACTION)].as(); + int j = 0; + for(JsonVariant v : act) { + if(j < HASP_NUM_GPIO_CONFIG) { + const uint8_t na = v.as(); + if(gpioConfig[j].input_action != na) status = true; + gpioConfig[j].input_action = na; + } + j++; + } + } else { + for(uint8_t j = 0; j < HASP_NUM_GPIO_CONFIG; j++) { + if(gpioConfig[j].type == hasp_gpio_type_t::MOTION) { + if(gpioConfig[j].input_action != hasp_gpio_input_action_t::INPUT_ACTION_BACKLIGHT_ON) { + gpioConfig[j].input_action = hasp_gpio_input_action_t::INPUT_ACTION_BACKLIGHT_ON; + status = true; + } + } + } + } + changed |= status; + } +#endif + return changed; } #endif // HASP_USE_CONFIG diff --git a/src/sys/gpio/hasp_gpio.h b/src/sys/gpio/hasp_gpio.h index 559bc7620..97409ab34 100644 --- a/src/sys/gpio/hasp_gpio.h +++ b/src/sys/gpio/hasp_gpio.h @@ -20,6 +20,7 @@ struct hasp_gpio_config_t uint8_t inverted : 1; uint8_t channel : 4; // pwmchannel uint8_t power : 1; + uint8_t input_action; /** @see hasp_gpio_input_action_t; HTTP "Action" for inputs when USE_MOTION_WAKEUP */ uint16_t val; uint16_t max; #ifdef ARDUINO @@ -49,7 +50,8 @@ void gpio_set_moodlight(moodlight_t& moodlight); void gpio_discovery(JsonObject& input, JsonArray& relay, JsonArray& light, JsonArray& dimmer, JsonArray& event); -bool gpioSavePinConfig(uint8_t config_num, uint8_t pin, uint8_t type, uint8_t group, uint8_t pinfunc, bool inverted); +bool gpioSavePinConfig(uint8_t config_num, uint8_t pin, uint8_t type, uint8_t group, uint8_t pinfunc, bool inverted, + uint8_t input_action = 0); bool gpioIsSystemPin(uint8_t gpio); bool gpioInUse(uint8_t pin); bool gpioConfigInUse(uint8_t num); @@ -69,6 +71,12 @@ enum hasp_gpio_function_t { EXTERNAL_PULLDOWN = 5 }; +/** Per-input GPIO action when @c USE_MOTION_WAKEUP (HTTP "Action" dropdown). Stored in @c config.json gpio.action[] */ +enum hasp_gpio_input_action_t : uint8_t { + INPUT_ACTION_NONE = 0, + INPUT_ACTION_BACKLIGHT_ON = 1, +}; + enum hasp_gpio_type_t { FREE = 0x00, USED = 0x01, @@ -149,6 +157,16 @@ enum hasp_gpio_type_t { USER = 0xFF }; +/** Virtual pin numbers for STC/L10 I2C expander (shown in GPIO / backlight UI). Override in build flags if needed. */ +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) +#ifndef HASP_I2C_GPIO_UI_PIN_FIRST +#define HASP_I2C_GPIO_UI_PIN_FIRST 129u +#endif +#ifndef HASP_I2C_GPIO_UI_PIN_LAST +#define HASP_I2C_GPIO_UI_PIN_LAST 150u +#endif +#endif + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/src/sys/gpio/i2c_gpio_bus.cpp b/src/sys/gpio/i2c_gpio_bus.cpp new file mode 100644 index 000000000..cf70ec869 --- /dev/null +++ b/src/sys/gpio/i2c_gpio_bus.cpp @@ -0,0 +1,24 @@ +/* MIT License - Copyright (c) 2019-2024 Francis Van Roie */ + +#include "hasp_conf.h" + +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO) + +#include "sys/gpio/i2c_gpio_bus.h" + +i2c_gpio_bus* i2c_gpio = nullptr; + +bool i2c_gpio_bus::analogWrite8(uint8_t pin, uint8_t value) +{ + (void)pin; + (void)value; + return false; +} + +int i2c_gpio_bus::analogRead8(uint8_t pin) +{ + (void)pin; + return -1; +} + +#endif diff --git a/src/sys/gpio/i2c_gpio_bus.h b/src/sys/gpio/i2c_gpio_bus.h new file mode 100644 index 000000000..d427108e5 --- /dev/null +++ b/src/sys/gpio/i2c_gpio_bus.h @@ -0,0 +1,54 @@ +/* MIT License - Copyright (c) 2019-2024 Francis Van Roie + Generic I2C-backed virtual GPIO (expander protocols). */ + +#pragma once + +#include "hasp_conf.h" + +#if defined(HASP_USE_I2C_GPIO) && defined(ARDUINO) + +#include +#include +#include + +/** Per-pin map for STC/L10-style transactions (read pointer, write reg, suffix bytes). */ +typedef struct { + uint8_t pin; + uint8_t read_reg; + uint8_t read_pl_byte; + bool isWPR; + /** 1-based byte index in read buffer; 0 or 0xFF = last byte. */ + uint8_t read_pin_byte; + uint8_t pre_read_len; + uint8_t write_reg; + uint8_t init_reg; + uint8_t bit; + uint8_t pre_write[4]; + uint8_t pre_write_len; + uint8_t write_seq[8]; + uint8_t write_seq_len; + uint8_t post_write[4]; + uint8_t post_write_len; + /** If true, digitalRead() uses analogRead8 byte vs on_value/off_value (1/0/-1). */ + bool is_digital; + uint8_t on_value; + uint8_t off_value; +} i2c_gpio_pin_map_t; + +class i2c_gpio_bus { + public: + virtual ~i2c_gpio_bus() {} + virtual bool begin() = 0; + virtual bool handles_pin(uint8_t pin) const = 0; + virtual bool pinMode(uint8_t pin, uint8_t mode) = 0; + virtual bool digitalWrite(uint8_t pin, uint8_t val) = 0; + virtual int digitalRead(uint8_t pin) = 0; + virtual bool digitalReadBytes(uint8_t pin, uint8_t* val, uint8_t read_len) = 0; + virtual bool analogWrite8(uint8_t pin, uint8_t value); + virtual int analogRead8(uint8_t pin); +}; + +/** Global expander instance (set in gpioSetup). */ +extern i2c_gpio_bus* i2c_gpio; + +#endif /* HASP_USE_I2C_GPIO && ARDUINO */ diff --git a/src/sys/svc/hasp_http.cpp b/src/sys/svc/hasp_http.cpp index 9a64f47a6..9220da651 100644 --- a/src/sys/svc/hasp_http.cpp +++ b/src/sys/svc/hasp_http.cpp @@ -397,6 +397,7 @@ bool http_save_config() settings[FPSTR(FP_GUI_POINTER)] = webServer.hasArg("cursor"); settings[FPSTR(FP_GUI_INVERT)] = webServer.hasArg("invert"); settings[FPSTR(FP_GUI_BACKLIGHTINVERT)] = webServer.hasArg("bcklinv"); + settings[FPSTR(FP_GUI_AUTODIM)] = webServer.hasArg("autodim"); updated = guiSetConfig(settings.as()); } else if(save == FP_DEBUG) { @@ -1581,7 +1582,7 @@ static void http_handle_gui() httpGpio.reserve(256); httpGpio += getOption(-1, "None"); #if defined(ARDUINO_ARCH_ESP32) - char buffer[10]; + char buffer[24]; for(uint8_t gpio = 0; gpio < NUM_DIGITAL_PINS; gpio++) { if(!gpioIsSystemPin(gpio)) { snprintf_P(buffer, sizeof(buffer), PSTR("GPIO %d"), gpio); @@ -1590,6 +1591,14 @@ static void http_handle_gui() LOG_WARNING(TAG_HTTP, F("pin %d"), gpio); } } +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) + for(uint8_t gpio = HASP_I2C_GPIO_UI_PIN_FIRST; gpio <= HASP_I2C_GPIO_UI_PIN_LAST; gpio++) { + if(!gpioIsSystemPin(gpio)) { + snprintf_P(buffer, sizeof(buffer), PSTR("I2C GPIO %u"), (unsigned)gpio); + httpGpio += getOption(gpio, buffer); + } + } +#endif #endif html[min(i++, len)] = httpGpio.c_str(); html[min(i++, len)] = R"( @@ -1599,6 +1608,12 @@ static void http_handle_gui()
+)"; + html[min(i++, len)] = R"( +
+
+
+
)"; html[min(i++, len)] = R"()"; #if TOUCH_DRIVER == 0x2046 && defined(TOUCH_CS) @@ -1768,7 +1783,11 @@ static void webHandleGpioConfig() uint8_t group = webServer.arg("group").toInt(); uint8_t pinfunc = webServer.arg("func").toInt(); bool inverted = webServer.arg("state").toInt(); - gpioSavePinConfig(id, pin, type, group, pinfunc, inverted); + uint8_t inact = 0; +#if defined(USE_MOTION_WAKEUP) + if(webServer.hasArg("inact")) inact = (uint8_t)webServer.arg("inact").toInt(); +#endif + gpioSavePinConfig(id, pin, type, group, pinfunc, inverted, inact); } if(webServer.hasArg("del")) { @@ -1787,9 +1806,9 @@ static void webHandleGpioConfig() httpMessage += F("
"); httpMessage += F(""); + ""); - for(uint8_t gpio = 0; gpio < NUM_DIGITAL_PINS; gpio++) { + auto append_gpio_config_table_rows = [&](uint8_t gpio) { for(uint8_t id = 0; id < HASP_NUM_GPIO_CONFIG; id++) { hasp_gpio_config_t conf = gpioGetPinConfig(id); if((conf.pin == gpio) && gpioConfigInUse(id) && !gpioIsSystemPin(gpio)) { @@ -1832,6 +1851,10 @@ static void webHandleGpioConfig() break; case hasp_gpio_type_t::MOTION: httpMessage += "motion"; +#if defined(USE_MOTION_WAKEUP) + if(conf.input_action == hasp_gpio_input_action_t::INPUT_ACTION_BACKLIGHT_ON) + httpMessage += " · Backlight-ON"; +#endif break; case hasp_gpio_type_t::OCCUPANCY: httpMessage += "occupancy"; @@ -1909,7 +1932,7 @@ static void webHandleGpioConfig() httpMessage += F("
" D_GPIO_PIN "Type" D_GPIO_GROUP - "DefaultAction
Default
"); httpMessage += (conf.inverted) ? D_GPIO_STATE_INVERTED : D_GPIO_STATE_NORMAL; - httpMessage += (""); @@ -1968,6 +1997,15 @@ static void webHandleGpioOutput() httpMessage += getOption(io, haspDevice.gpio_name(io).c_str(), conf.pin); } } +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) + for(uint8_t io = HASP_I2C_GPIO_UI_PIN_FIRST; io <= HASP_I2C_GPIO_UI_PIN_LAST; io++) { + if(((conf.pin == io) || !gpioInUse(io)) && !gpioIsSystemPin(io)) { + char ilab[20]; + snprintf_P(ilab, sizeof(ilab), PSTR("I2C %u"), (unsigned)io); + httpMessage += getOption(io, ilab, conf.pin); + } + } +#endif httpMessage += F("

"); httpMessage += F("

Type

"); @@ -2048,6 +2086,15 @@ static void webHandleGpioInput() httpMessage += getOption(io, haspDevice.gpio_name(io).c_str(), conf.pin); } } +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) + for(uint8_t io = HASP_I2C_GPIO_UI_PIN_FIRST; io <= HASP_I2C_GPIO_UI_PIN_LAST; io++) { + if(((conf.pin == io) || !gpioInUse(io)) && !gpioIsSystemPin(io)) { + char ilab[20]; + snprintf_P(ilab, sizeof(ilab), PSTR("I2C %u"), (unsigned)io); + httpMessage += getOption(io, ilab, conf.pin); + } + } +#endif httpMessage += F("

"); httpMessage += F("

Type

"); +#if defined(USE_MOTION_WAKEUP) + httpMessage += F("

Action

"); +#endif + httpMessage += F("

"); diff --git a/src/sys/svc/hasp_http_async.cpp b/src/sys/svc/hasp_http_async.cpp index 45c4e0f6a..161738909 100644 --- a/src/sys/svc/hasp_http_async.cpp +++ b/src/sys/svc/hasp_http_async.cpp @@ -164,13 +164,24 @@ String getOption(String value, String label, bool selected) return buffer; } -static void add_gpio_select_option(String& str, uint8_t gpio, uint8_t bcklpin) +static void add_gpio_select_option(String& str, uint8_t gpio, int32_t bcklpin) { - char buffer[10]; + char buffer[24]; snprintf_P(buffer, sizeof(buffer), PSTR("GPIO %d"), gpio); - str += getOption(gpio, buffer, bcklpin == gpio); + str += getOption(gpio, buffer, bcklpin == (int32_t)gpio); } +#if defined(HASP_USE_I2C_GPIO) && defined(HASP_EXPANDER_GPIO_STC_L10) +static void add_i2c_gpio_select_options(String& str, int32_t bcklpin) +{ + char buffer[24]; + for(uint8_t gpio = HASP_I2C_GPIO_UI_PIN_FIRST; gpio <= HASP_I2C_GPIO_UI_PIN_LAST; gpio++) { + snprintf_P(buffer, sizeof(buffer), PSTR("I2C GPIO %u"), (unsigned)gpio); + str += getOption(gpio, buffer, bcklpin == (int32_t)gpio); + } +} +#endif + static void add_button(String& str, const __FlashStringHelper* label, const __FlashStringHelper* extra) { str += F("