From 1f40e622251d2cf4460340534d5d7fc664b29a62 Mon Sep 17 00:00:00 2001 From: Christos Themelis Date: Tue, 14 Apr 2026 16:09:23 +0300 Subject: [PATCH] wip --- examples/simple_secure_chat_ui/main.cpp | 33 +++-- src/helpers/esp32/SenseCapHAL.h | 131 ++++++++++-------- src/helpers/radiolib/RadioLibWrappers.cpp | 5 +- .../SCIndicatorDisplay.h | 38 ++++- .../sensecap_indicator-espnow/platformio.ini | 4 +- 5 files changed, 139 insertions(+), 72 deletions(-) diff --git a/examples/simple_secure_chat_ui/main.cpp b/examples/simple_secure_chat_ui/main.cpp index e3f0bd893..29ef05417 100644 --- a/examples/simple_secure_chat_ui/main.cpp +++ b/examples/simple_secure_chat_ui/main.cpp @@ -123,20 +123,25 @@ void parse_group_message(const char *input, // Elecrow Display callbacks /* Display flushing */ +static uint32_t s_flush_count = 0; void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = (area->x2 - area->x1 + 1); - uint32_t h = (area->y2 - area->y1 + 1); + uint32_t h = (area->y2 - area->y1 + 1); + + s_flush_count++; + if (s_flush_count <= 3) { + Serial.printf("[display] flush #%u x=%d y=%d w=%d h=%d\n", + s_flush_count, area->x1, area->y1, w, h); + } - //lcd.fillScreen(TFT_WHITE); #if (LV_COLOR_16_SWAP != 0) - lcd.pushImageDMA(area->x1, area->y1, w, h,(lgfx::rgb565_t*)&color_p->full); + lcd.pushImageDMA(area->x1, area->y1, w, h,(lgfx::rgb565_t*)&color_p->full); #else - lcd.pushImageDMA(area->x1, area->y1, w, h,(lgfx::rgb565_t*)&color_p->full);// + lcd.pushImageDMA(area->x1, area->y1, w, h,(lgfx::rgb565_t*)&color_p->full); #endif lv_disp_flush_ready(disp); - } void my_touchpad_read(lv_indev_drv_t * indev_driver, lv_indev_data_t * data) @@ -803,11 +808,18 @@ void configureDisplay() { void initializeDisplay() { ESP_LOGI(TAG, "Initializing display..."); - lcd.begin(); - lcd.fillScreen(0x000000u); + bool ok = lcd.begin(); + Serial.printf("[display] lcd.begin() = %s w=%d h=%d\n", ok ? "OK" : "FAIL", lcd.width(), lcd.height()); + lcd.setBrightness(255); + // Fallback: force GPIO 45 HIGH in case LovyanGFX Light_PWM LEDC setup failed. + pinMode(45, OUTPUT); + digitalWrite(45, HIGH); + Serial.printf("[display] BL GPIO45 after force HIGH: %d\n", digitalRead(45)); + lcd.fillScreen(TFT_RED); // DEBUG: should show solid RED for 3s before LVGL + Serial.println("[display] RED fill done — waiting 3s for visual check"); + delay(3000); lcd.setTextSize(2); lcd.setRotation(1); - //lcd.setBrightness(127); } void initializeTouchScreen() { @@ -824,6 +836,7 @@ void initializeMesh() { Serial.println("[mesh] board.begin()"); board.begin(); +#ifndef DISABLE_LORA_FOR_DISPLAY_TEST Serial.println("[mesh] radio_init()..."); if (!radio_init()) { Serial.println("[mesh] FATAL: radio_init() failed - halting"); @@ -831,6 +844,10 @@ void initializeMesh() { } fast_rng.begin(radio_get_rng_seed()); +#else + Serial.println("[mesh] *** LORA DISABLED FOR DISPLAY TEST ***"); + fast_rng.begin(42); +#endif Serial.println("[mesh] SPIFFS.begin()"); #if defined(NRF52_PLATFORM) diff --git a/src/helpers/esp32/SenseCapHAL.h b/src/helpers/esp32/SenseCapHAL.h index 04bd48aac..b2f901316 100644 --- a/src/helpers/esp32/SenseCapHAL.h +++ b/src/helpers/esp32/SenseCapHAL.h @@ -107,18 +107,38 @@ public: writeReg(0x02, _out0); // Output Port 0 writeReg(0x06, _cfg0); // Config Port 0 - Serial.printf("[SenseCapHAL] TCA9535@0x%02X init: out=0x%02X cfg=0x%02X\n", + + // Port 1 (pins 8-15) — appears unused on the SenseCAP Indicator. + // TCA9535 /INT fires on ANY input change on either port and only clears + // when the triggering port register is read. Floating Port 1 inputs + // continuously re-assert /INT, preventing DIO1 edges from ever firing. + // Fix: make all Port 1 pins OUTPUT HIGH so they can never float. + writeReg(0x03, 0xFF); // Output Port 1: all HIGH + writeReg(0x07, 0x00); // Config Port 1: all OUTPUTS + + // Read both input ports now to establish a clean /INT baseline. + readReg(0x00); + readReg(0x01); + + Serial.printf("[SenseCapHAL] TCA9535@0x%02X init: out0=0x%02X cfg0=0x%02X\n", _addr, _out0, _cfg0); } // ── Overrides ────────────────────────────────────────────────────────── // - // SOFTWARE SPI — we bit-bang GPIO 41/47/48 directly instead of using - // the hardware FSPI peripheral. Calling spi.begin(41,47,48) routes - // those pins through the ESP32-S3 GPIO matrix to FSPI, which silently - // disables LovyanGFX's LCD_CAM framebuffer output (blank display). - // Software SPI avoids that conflict entirely; the SX1262 works fine - // at the ~500 kHz rate the bit-bang produces. + // SOFTWARE (bit-bang) SPI — intentionally avoids the hardware FSPI peripheral. + // + // Calling SPI.begin() (hardware FSPI) AFTER lcd.begin() disrupts the + // LCD_CAM peripheral that is already running the RGB parallel bus, + // causing the display to go blank. Since the LCD_CAM uses GPIOs 0–21 + // and the SX1262 SPI uses GPIOs 41/47/48, there is no pin conflict — + // the problem is at the peripheral / DMA level when FSPI is initialised + // while LCD_CAM is active. + // + // Bit-bang SPI on the same GPIOs avoids hardware SPI entirely. + // The SX1262 requires at most ~16 MHz SPI; bit-bang on ESP32-S3 gives + // ~1–2 MHz which is more than adequate for LoRa configuration and DIO + // interrupt latency is unaffected. // ArduinoHal::init() only calls spiBegin() when initInterface==true, // which is false when an explicit SPIClass& is supplied. Override to @@ -126,61 +146,40 @@ public: void init() override { spiBegin(); } void spiBegin() override { - // ── IMPORTANT ────────────────────────────────────────────────────────── - // We must NOT call ::pinMode() here. ::pinMode() calls gpio_reset_pin() - // which disrupts the GPIO matrix signal routing used by LovyanGFX's - // Bus_RGB LCD_CAM driver — blanking the display permanently. - // - // We also must NOT use gpio_set_direction() alone: it does not call - // PIN_FUNC_SELECT, so the IO_MUX for GPIO 41/48 may remain pointed at - // the JTAG MTDI/MTCK function (restored by LovyanGFX's pin_backup_t - // after lcd.begin()). With IO_MUX in JTAG function the physical pins - // bypass the GPIO matrix entirely, making soft-SPI write nothing. - // - // Fix: gpio_config() sets IO_MUX → PIN_FUNC_GPIO (via PIN_FUNC_SELECT) - // AND enables GPIO output via the GPIO matrix (SIG_GPIO_OUT_IDX), all - // without ever calling gpio_reset_pin(). - // ─────────────────────────────────────────────────────────────────────── - gpio_config_t out_conf = {}; - out_conf.pin_bit_mask = (1ULL << _sclk) | (1ULL << _mosi); - out_conf.mode = GPIO_MODE_OUTPUT; - out_conf.pull_up_en = GPIO_PULLUP_DISABLE; - out_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; - out_conf.intr_type = GPIO_INTR_DISABLE; - gpio_config(&out_conf); - - gpio_config_t in_conf = {}; - in_conf.pin_bit_mask = (1ULL << _miso); - in_conf.mode = GPIO_MODE_INPUT; - in_conf.pull_up_en = GPIO_PULLUP_DISABLE; - in_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; - in_conf.intr_type = GPIO_INTR_DISABLE; - gpio_config(&in_conf); - - gpio_set_level((gpio_num_t)_sclk, 0); - gpio_set_level((gpio_num_t)_mosi, 0); - Serial.printf("[SenseCapHAL] SPI (soft) SCLK=%d MOSI=%d MISO=%d\n", + // Configure GPIO pins for direct CPU control (disconnects any + // peripheral / GPIO-matrix function, including leftover FSPI routing). + ::pinMode(_sclk, OUTPUT); + ::pinMode(_mosi, OUTPUT); + ::pinMode(_miso, INPUT); + ::digitalWrite(_sclk, LOW); + Serial.printf("[SenseCapHAL] SPI (bit-bang) SCLK=%d MOSI=%d MISO=%d\n", _sclk, _mosi, _miso); } - void spiBeginTransaction() override { /* no-op for soft SPI */ } - void spiEndTransaction() override { /* no-op for soft SPI */ } - void spiEnd() override { /* no-op for soft SPI */ } + void spiEnd() override { + // nothing to tear down for bit-bang + } + + void spiBeginTransaction() override { + // nothing needed — CS is managed via the expander in digitalWrite() + } + + void spiEndTransaction() override { + // nothing needed + } - // SPI Mode 0: CPOL=0 (CLK idles LOW), CPHA=0 (sample on rising edge) + // Bit-bang SPI Mode 0 (CPOL=0, CPHA=0): data sampled on rising edge. void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override { for (size_t i = 0; i < len; i++) { - uint8_t txByte = out ? out[i] : 0xFF; - uint8_t rxByte = 0; + uint8_t tx = out ? out[i] : 0x00; + uint8_t rx = 0; for (int b = 7; b >= 0; b--) { - ::digitalWrite(_mosi, (txByte >> b) & 1); - ::delayMicroseconds(1); // MOSI setup time + ::digitalWrite(_mosi, (tx >> b) & 1); ::digitalWrite(_sclk, HIGH); - rxByte |= (uint8_t)(::digitalRead(_miso) << b); // sample on rising edge - ::delayMicroseconds(1); // hold time + rx = (rx << 1) | (::digitalRead(_miso) ? 1 : 0); ::digitalWrite(_sclk, LOW); } - if (in) in[i] = rxByte; + if (in) in[i] = rx; } } @@ -213,18 +212,40 @@ public: // DIO1 is expander pin 3; redirect the interrupt to IO_EXPANDER_IRQ (GPIO 42). // TCA9535 /INT fires FALLING when any input changes (DIO1 going HIGH = packet ready). - void attachInterrupt(uint32_t pin, void (*cb)(void), uint32_t mode) override { + // RadioLib 7.x calls pinToInterrupt(irq_pin) BEFORE attachInterrupt(). + // Without this override, pinToInterrupt(0x43) → digitalPinToInterrupt(67) + // which is invalid on ESP32-S3, so attachInterrupt() silently fails. + // Return IO_EXPANDER_IRQ (GPIO 42) directly for any expander pin. + uint32_t pinToInterrupt(uint32_t pin) override { if (isExp(pin)) { + return (uint32_t)IO_EXPANDER_IRQ; + } + return ArduinoHal::pinToInterrupt(pin); + } + + // Called with either: + // a) raw expander pin (e.g. 0x43) — direct call path + // b) IO_EXPANDER_IRQ (42) — via pinToInterrupt() path (RadioLib 7.x) + // In both cases redirect to GPIO 42 FALLING (TCA9535 /INT goes LOW when DIO1 rises). + void attachInterrupt(uint32_t pin, void (*cb)(void), uint32_t mode) override { + if (isExp(pin) || pin == (uint32_t)IO_EXPANDER_IRQ) { + // Clear any pending TCA9535 /INT by reading BOTH port registers. + // /INT stays LOW until the port that triggered it is read; floating + // Port 1 inputs keep /INT stuck even after reading Port 0 alone. + readReg(0x00); // Input Port 0 (radio pins) + readReg(0x01); // Input Port 1 (prevent spurious /INT from port 1) ::pinMode(IO_EXPANDER_IRQ, INPUT_PULLUP); + int gpio42 = ::digitalRead(IO_EXPANDER_IRQ); ::attachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ), cb, FALLING); - Serial.printf("[SenseCapHAL] DIO1 interrupt → GPIO %d (FALLING)\n", IO_EXPANDER_IRQ); + Serial.printf("[SenseCapHAL] DIO1 interrupt → GPIO %d state=%d (FALLING)\n", + IO_EXPANDER_IRQ, gpio42); } else { ArduinoHal::attachInterrupt(pin, cb, mode); } } void detachInterrupt(uint32_t pin) override { - if (isExp(pin)) { + if (isExp(pin) || pin == (uint32_t)IO_EXPANDER_IRQ) { ::detachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ)); } else { ArduinoHal::detachInterrupt(pin); diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index cf3e1266b..91de8de55 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -99,6 +99,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { int len = 0; if (state & STATE_INT_READY) { len = _radio->getPacketLength(); + MESH_DEBUG_PRINTLN("RadioLibWrapper: IRQ fired, pkt_len=%d", len); if (len > 0) { if (len > sz) { len = sz; } int err = _radio->readData(bytes, len); @@ -107,14 +108,14 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { len = 0; n_recv_errors++; } else { - // Serial.print(" readData() -> "); Serial.println(len); + MESH_DEBUG_PRINTLN("RadioLibWrapper: recv %d bytes RSSI=%.1f SNR=%.1f", len, getLastRSSI(), getLastSNR()); n_recv++; } } state = STATE_IDLE; // need another startReceive() } - if (state != STATE_RX) { + if (state != STATE_RX && !isReceivingPacket()) { int err = _radio->startReceive(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; diff --git a/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h b/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h index aabedd240..07682d8b2 100644 --- a/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h +++ b/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h @@ -8,9 +8,35 @@ #include #include +// Custom ST7701 panel subclass with SenseCap Indicator-specific init commands. +// The default Panel_ST7701 init sequence does not set MADCTL or SDIR, so the +// display outputs pixels in an orientation that doesn't match the physical +// RGB bus wiring on the SenseCap Indicator — resulting in a blank screen. +// These extra commands (ported from Meshtastic's LGFX_INDICATOR.h) apply the +// required vertical flip (MADCTL) and horizontal flip (SDIR) after the +// standard init sequence. +class Panel_SCIndicator : public lgfx::Panel_ST7701 +{ +public: + const uint8_t *getInitCommands(uint8_t listno) const override + { + static constexpr const uint8_t list1[] = { + 0x36, 1, 0x10, // MADCTL: vertical flip + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL + 0xC7, 1, 0x04, // SDIR: horizontal flip + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS + 0xFF, 0xFF + }; + switch (listno) { + case 1: return list1; + default: return lgfx::Panel_ST7701::getInitCommands(listno); + } + } +}; + class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_ST7701 _panel_instance; + Panel_SCIndicator _panel_instance; lgfx::Bus_RGB _bus_instance; lgfx::Light_PWM _light_instance; lgfx::Touch_FT5x06 _touch_instance; @@ -31,7 +57,7 @@ public: cfg.panel_height = screenHeight; cfg.offset_x = 0; cfg.offset_y = 0; - cfg.offset_rotation = 1; + cfg.offset_rotation = 0; _panel_instance.config(cfg); } @@ -48,7 +74,7 @@ public: auto cfg = _bus_instance.config(); cfg.panel = &_panel_instance; - cfg.freq_write = 8000000; + cfg.freq_write = 6000000; // 6 MHz — matches Meshtastic LGFX_INDICATOR cfg.pin_henable = 18; cfg.pin_pclk = 21; @@ -103,9 +129,9 @@ public: cfg.x_max = 479; cfg.y_min = 0; cfg.y_max = 479; - cfg.pin_int = GPIO_NUM_NC; - cfg.pin_rst = GPIO_NUM_NC; - cfg.bus_shared = true; + cfg.pin_int = GPIO_NUM_NC; // do NOT use IO_EXPANDER for touch pins + cfg.pin_rst = GPIO_NUM_NC; // do NOT use IO_EXPANDER for touch pins + cfg.bus_shared = false; cfg.offset_rotation = 0; cfg.i2c_port = 0; diff --git a/variants/sensecap_indicator-espnow/platformio.ini b/variants/sensecap_indicator-espnow/platformio.ini index 736d57bbc..aba9a2525 100644 --- a/variants/sensecap_indicator-espnow/platformio.ini +++ b/variants/sensecap_indicator-espnow/platformio.ini @@ -57,6 +57,8 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=8 -D MESH_DEBUG=1 + -D RADIOLIB_SPI_PARANOID=0 + -D DISABLE_LORA_FOR_DISPLAY_TEST ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 ; NOTE: DO NOT ENABLE --> -D ESPNOW_DEBUG_LOGGING=1 @@ -68,6 +70,6 @@ lib_deps = ${SenseCapIndicator-ESPNow.lib_deps} fbiego/ESP32Time@^2.0.6 lvgl/lvgl@8.3.11 - lovyan03/LovyanGFX@^1.1.16 + lovyan03/LovyanGFX@^1.2.7 bitbank2/PNGdec@^1.1.6 densaugeo/base64 @ ~1.4.0