From 8351effff15424b9f5ea766470cb545b336ec65b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 31 Aug 2026 19:06:07 -0700 Subject: [PATCH] Recover Indicator display after warm reset --- examples/companion_radio/main.cpp | 7 ++++- test/test_indicator_display_profile.py | 11 +++++++ .../SCIndicatorDisplay.h | 31 +++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 73075cd7..f23a4e0d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -825,6 +825,7 @@ void halt() { static CompanionWiFiDisplayState companion_wifi_display_state = CompanionWiFiDisplayState::NotRendered; static uint32_t companion_wifi_display_frames = 0; + static bool companion_display_available = false; #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) static CompanionTransportMode companion_transport_boot_mode = @@ -1173,9 +1174,10 @@ void halt() { const bool idf_associated = esp_wifi_sta_get_ap_info(&access_point) == ESP_OK; snprintf(reply, reply_size, - "> state=%s, frames=%lu, requested=%u, services=%u, " + "> state=%s, frames=%lu, display=%u, requested=%u, services=%u, " "wl=%d, idf=%u, IP=%s", names[state], (unsigned long)companion_wifi_display_frames, + companion_display_available ? 1U : 0U, companion_wifi_requested ? 1U : 0U, companion_wifi_active ? 1U : 0U, (int)WiFi.status(), idf_associated ? 1U : 0U, @@ -1733,6 +1735,9 @@ void setup() { disp->drawTextCentered(disp->width() / 2, 28, "Loading..."); disp->endFrame(); } +#if defined(ESP32) && defined(WIFI_SSID) + companion_display_available = disp != NULL; +#endif #endif int radioinit_attempts = 0; diff --git a/test/test_indicator_display_profile.py b/test/test_indicator_display_profile.py index d067d272..a553f4f2 100644 --- a/test/test_indicator_display_profile.py +++ b/test/test_indicator_display_profile.py @@ -9,6 +9,9 @@ import unittest ROOT = Path(__file__).resolve().parents[1] PROFILE = ROOT / "variants" / "sensecap_indicator-espnow" / "platformio.ini" DISPLAY = ROOT / "src" / "helpers" / "ui" / "LGFXDisplay.cpp" +INDICATOR_DISPLAY = ( + ROOT / "variants" / "sensecap_indicator-espnow" / "SCIndicatorDisplay.h" +) FONT = ROOT / "variants" / "sensecap_indicator-espnow" / "sd" / "ui-font.vlw" @@ -36,6 +39,14 @@ def numeric_flag(name: str) -> float: class IndicatorDisplayProfileTest(unittest.TestCase): + def test_panel_cs_release_cannot_discard_initialized_display(self): + display = INDICATOR_DISPLAY.read_text() + self.assertIn("releasePanelChipSelectWithRetry()", display) + self.assertIn("if (!initialized) return false;", display) + self.assertNotIn("if (!initialized || !released) return false;", display) + self.assertIn("panel_chip_select_released =", display) + self.assertIn("void startFrame(ColorVal bkg", display) + def test_preserves_original_indicator_memory_profile(self): profile = base_profile() self.assertIn("board_build.arduino.memory_type = qio_opi", profile) diff --git a/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h b/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h index c5dd9353..07c784a6 100644 --- a/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h +++ b/variants/sensecap_indicator-espnow/SCIndicatorDisplay.h @@ -129,6 +129,7 @@ class SCIndicatorDisplay : public LGFXDisplay { LGFX disp; uint8_t expander_address = 0; uint16_t expander_output = 0; + bool panel_chip_select_released = false; static constexpr gpio_num_t BACKLIGHT_PIN = GPIO_NUM_45; static constexpr uint8_t I2C_PORT = 0; @@ -230,6 +231,20 @@ class SCIndicatorDisplay : public LGFXDisplay { expander_output); } + bool releasePanelChipSelectWithRetry() { + // LovyanGFX initializes the touch controller on this same I2C bus. On a + // warm ESP32-S3 reset its final transaction can briefly leave the + // expander unavailable even though the RGB panel and render buffer were + // initialized successfully. Re-open the bus and retry before LoRa starts + // using the panel's shared SCLK/MOSI pins. + for (uint8_t attempt = 0; attempt < 10; ++attempt) { + if (releasePanelChipSelect()) return true; + delay(5); + lgfx::i2c::init(I2C_PORT, PIN_BOARD_SDA, PIN_BOARD_SCL); + } + return false; + } + public: #if defined(INDICATOR_TRANSPORT_RENDER_PROFILE) // Full builds always use the established 160x160 logical layout. State it @@ -245,8 +260,13 @@ public: bool begin() { if (!prepareControllers()) return false; const bool initialized = LGFXDisplay::begin(); - const bool released = releasePanelChipSelect(); - if (!initialized || !released) return false; + if (!initialized) return false; + + // A failed CS release must not discard an otherwise valid 115 KiB render + // buffer and leave the LCD showing its pre-reset scanout forever. Keep the + // UI alive and retry from startFrame(); the common case succeeds here + // before the external LoRa radio is initialized. + panel_chip_select_released = releasePanelChipSelectWithRetry(); setBacklight(true); size_t fontSize; uint8_t* fontData = IndicatorFontClient::load(fontSize); @@ -263,6 +283,13 @@ public: return true; } + void startFrame(ColorVal bkg = UIColor::window_bkg) override { + if (!panel_chip_select_released) { + panel_chip_select_released = releasePanelChipSelectWithRetry(); + } + LGFXDisplay::startFrame(bkg); + } + #ifdef INDICATOR_WIFI_FONT_RECOVERY void serviceFontRecovery() { size_t fontSize = 0;