Files

349 lines
11 KiB
C++

#pragma once
#include <helpers/ui/LGFXDisplay.h>
#include "IndicatorFontClient.h"
#include "IndicatorTouch.h"
#include <driver/gpio.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
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<uint16_t>(data[1]) << 8;
return true;
}
static bool writeExpanderRegister(uint8_t address, uint8_t reg,
uint16_t value) {
uint8_t data[] = {
reg,
static_cast<uint8_t>(value),
static_cast<uint8_t>(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;
}
};