#pragma once #include #include "IndicatorFontClient.h" #include "IndicatorTouch.h" #include #define LGFX_USE_V1 #include #include #include class LGFX : public lgfx::LGFX_Device { lgfx::Panel_ST7701 _panel_instance; lgfx::Bus_RGB _bus_instance; lgfx::Light_PWM _light_instance; IndicatorTouch _touch_instance; public: const uint16_t screenWidth = 480; const uint16_t screenHeight = 480; bool hasButton(void) { return true; } LGFX(void) { { auto cfg = _panel_instance.config(); cfg.memory_width = 480; cfg.memory_height = 480; cfg.panel_width = screenWidth; cfg.panel_height = screenHeight; cfg.offset_x = 0; cfg.offset_y = 0; cfg.offset_rotation = 1; _panel_instance.config(cfg); } { auto cfg = _panel_instance.config_detail(); // Chip-select is driven through the board's I2C expander below. cfg.pin_cs = GPIO_NUM_NC; cfg.pin_sclk = 41; cfg.pin_mosi = 48; cfg.use_psram = 1; _panel_instance.config_detail(cfg); } { auto cfg = _bus_instance.config(); cfg.panel = &_panel_instance; cfg.freq_write = 8000000; cfg.pin_henable = 18; cfg.pin_pclk = 21; cfg.pclk_active_neg = 0; cfg.pclk_idle_high = 0; cfg.de_idle_high = 1; cfg.pin_hsync = 16; cfg.hsync_polarity = 0; cfg.hsync_front_porch = 10; cfg.hsync_pulse_width = 8; cfg.hsync_back_porch = 50; cfg.pin_vsync = 17; cfg.vsync_polarity = 0; cfg.vsync_front_porch = 10; cfg.vsync_pulse_width = 8; cfg.vsync_back_porch = 20; cfg.pin_d0 = 15; cfg.pin_d1 = 14; cfg.pin_d2 = 13; cfg.pin_d3 = 12; cfg.pin_d4 = 11; cfg.pin_d5 = 10; cfg.pin_d6 = 9; cfg.pin_d7 = 8; cfg.pin_d8 = 7; cfg.pin_d9 = 6; cfg.pin_d10 = 5; cfg.pin_d11 = 4; cfg.pin_d12 = 3; cfg.pin_d13 = 2; cfg.pin_d14 = 1; cfg.pin_d15 = 0; _bus_instance.config(cfg); } _panel_instance.setBus(&_bus_instance); { auto cfg = _light_instance.config(); cfg.pin_bl = 45; _light_instance.config(cfg); } _panel_instance.light(&_light_instance); { auto cfg = _touch_instance.config(); cfg.pin_cs = GPIO_NUM_NC; cfg.x_min = 0; 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 = false; cfg.offset_rotation = 2; cfg.i2c_port = 0; cfg.i2c_addr = 0x48; cfg.pin_sda = 39; cfg.pin_scl = 40; cfg.freq = 400000; _touch_instance.config(cfg); _panel_instance.setTouch(&_touch_instance); } setPanel(&_panel_instance); } }; class SCIndicatorDisplay : public LGFXDisplay { LGFX disp; uint8_t expander_address = 0; uint16_t expander_output = 0; bool panel_chip_select_released = false; uint8_t begin_status = 0; static constexpr gpio_num_t BACKLIGHT_PIN = GPIO_NUM_45; static constexpr uint8_t I2C_PORT = 0; static constexpr uint32_t I2C_FREQUENCY = 400000; static constexpr uint8_t EXPANDER_OUTPUT_REGISTER = 0x02; static constexpr uint8_t EXPANDER_CONFIG_REGISTER = 0x06; static constexpr uint16_t PANEL_CS_MASK = 1U << 4; static constexpr uint16_t PANEL_RESET_MASK = 1U << 5; static constexpr uint16_t TOUCH_INTERRUPT_MASK = 1U << 6; static constexpr uint16_t TOUCH_RESET_MASK = 1U << 7; static void setBacklight(bool enabled) { // Detach any stale LEDC routing before changing the level. Reasserting a // plain GPIO level makes wake reliable even if a previous image left the // backlight PWM channel stopped or assigned elsewhere. gpio_reset_pin(BACKLIGHT_PIN); gpio_set_direction(BACKLIGHT_PIN, GPIO_MODE_OUTPUT); gpio_set_level(BACKLIGHT_PIN, enabled ? 1 : 0); } static bool readExpanderRegister(uint8_t address, uint8_t reg, uint16_t& value) { uint8_t data[2]; if (!lgfx::i2c::readRegister(I2C_PORT, address, reg, data, sizeof(data), I2C_FREQUENCY) .has_value()) { return false; } value = data[0]; value |= static_cast(data[1]) << 8; return true; } static bool writeExpanderRegister(uint8_t address, uint8_t reg, uint16_t value) { uint8_t data[] = { reg, static_cast(value), static_cast(value >> 8), }; return lgfx::i2c::transactionWrite(I2C_PORT, address, data, sizeof(data), I2C_FREQUENCY) .has_value(); } bool prepareControllersAt(uint8_t address) { uint16_t config; if (!readExpanderRegister(address, EXPANDER_OUTPUT_REGISTER, expander_output) || !readExpanderRegister(address, EXPANDER_CONFIG_REGISTER, config)) { return false; } // Set safe output latches before changing pin directions. Keep LCD CS // inactive while both controllers are held in reset. expander_output |= PANEL_CS_MASK; expander_output &= ~(PANEL_RESET_MASK | TOUCH_RESET_MASK); if (!writeExpanderRegister(address, EXPANDER_OUTPUT_REGISTER, expander_output)) { return false; } config &= ~(PANEL_CS_MASK | PANEL_RESET_MASK | TOUCH_RESET_MASK); config |= TOUCH_INTERRUPT_MASK; if (!writeExpanderRegister(address, EXPANDER_CONFIG_REGISTER, config)) { return false; } delay(10); expander_output |= PANEL_RESET_MASK | TOUCH_RESET_MASK; if (!writeExpanderRegister(address, EXPANDER_OUTPUT_REGISTER, expander_output)) { return false; } delay(120); // Hold LCD CS active while the graphics driver emits the ST7701 init // sequence over the shared clock and data pins. expander_output &= ~PANEL_CS_MASK; if (!writeExpanderRegister(address, EXPANDER_OUTPUT_REGISTER, expander_output)) { return false; } expander_address = address; return true; } bool prepareControllers() { for (uint8_t attempt = 0; attempt < 10; ++attempt) { if (lgfx::i2c::init(I2C_PORT, PIN_BOARD_SDA, PIN_BOARD_SCL).has_value() && (prepareControllersAt(0x20) || prepareControllersAt(0x39))) { return true; } delay(20); } return false; } bool releasePanelChipSelect() { if (expander_address == 0) return false; expander_output |= PANEL_CS_MASK; return writeExpanderRegister(expander_address, EXPANDER_OUTPUT_REGISTER, 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 // explicitly here because PlatformIO's inherited flag ordering can leave // the variant's historical UI_COORD_SCALE value visible in this header. // The 480px canvas is allocated before radio/transport startup; begin() // retains its 320px emergency fallback if that allocation is unavailable. SCIndicatorDisplay() : LGFXDisplay(160, 160, 3, 1.0f, disp) {} #else SCIndicatorDisplay() : LGFXDisplay(480, 480, disp) {} #endif bool getTouch(int* x, int* y) override { if (!LGFXDisplay::getTouch(x, y)) return false; // Touch Y runs bottom-to-top relative to this panel's rendered image. // Correct it once before both gesture detection and debug feedback. *y = height() - 1 - *y; return true; } bool begin() { begin_status = 1; const bool controllers_prepared = prepareControllers(); begin_status = controllers_prepared ? 2 : 0x81; const bool initialized = LGFXDisplay::begin(); if (!initialized) { begin_status = 0x82; return false; } begin_status = controllers_prepared ? 3 : 0x83; // 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. if (controllers_prepared) { panel_chip_select_released = releasePanelChipSelectWithRetry(); } setBacklight(true); size_t fontSize; uint8_t* fontData = IndicatorFontClient::load(fontSize); if (fontData != nullptr) { const bool installed = installRuntimeFont(fontData, fontSize); #ifdef INDICATOR_WIFI_FONT_RECOVERY if (installed) { IndicatorFontClient::noteRuntimeFontInstalled(); } else { IndicatorFontClient::noteRuntimeFontInvalid(); } #endif } begin_status = controllers_prepared ? 4 : 0x84; return true; } void startFrame(ColorVal bkg = UIColor::window_bkg) override { if (expander_address == 0 && prepareControllers()) { // The RGB scanout can start while the shared expander is temporarily // unavailable. Once it recovers, re-run the panel command sequence and // release CS before presenting the next complete frame. if (disp.init()) { panel_chip_select_released = releasePanelChipSelectWithRetry(); begin_status = 5; } } if (!panel_chip_select_released) { panel_chip_select_released = releasePanelChipSelectWithRetry(); } LGFXDisplay::startFrame(bkg); } uint8_t beginStatus() const { return begin_status; } #ifdef INDICATOR_WIFI_FONT_RECOVERY void serviceFontRecovery() { size_t fontSize = 0; uint8_t* fontData = IndicatorFontClient::serviceRecovery(fontSize); if (fontData == nullptr) return; if (installRuntimeFont(fontData, fontSize)) { IndicatorFontClient::noteRuntimeFontInstalled(); } else { IndicatorFontClient::noteRecoveredFontInvalid(); } } #endif void turnOn() override { setBacklight(true); _isOn = true; } void turnOff() override { setBacklight(false); _isOn = false; } };