Files
pyxis/lib/tdeck_ui/Hardware/TDeck/Display.cpp
T
torlando-agent[bot]andClaude Opus 4.8 70d4aa6be9 feat: graft pyxis onto upstream microReticulum 0.4.1
Repins microReticulum + microLXMF onto the upstream-0.4.1 graft and adapts
pyxis to the new src/microReticulum/ layout and 0.4.x APIs. The far-diverged
0.3.0 fork's Resource/Transport/Identity work is subsumed by upstream's
reimplementation; only the still-needed fixes ride on the pinned branches
(PKCS7/HMAC/X25519 crypto -- proven byte-identical to python RNS 1.3.1 --
Packet link-proof callback, Identity short-sig guard, and the bz2 layer +
decompress-on-receive in Resource::assemble()).

Consumer-side changes:
- platformio.ini: pin microReticulum @2f21fee (pyxis-fixes-on-0.4.1) and
  microLXMF @33760d0 (chore/microreticulum-0.4.1-layout); bump microStore
  ceea8f5 -> c5fb69d (0.4.x requires the new BasicFileStore::init API);
  -std=gnu++11 -> gnu++17 (upstream requires C++17).
- Namespace all microReticulum includes (angle + quote) to <microReticulum/...>
  for the relocated layout; shim-local Utilities/Stream.h|Print.h preserved.
- Interface::send_outgoing now returns bool: update TCP/BLE/SX1262/Auto
  overrides with correct success/failure returns.
- SDArchiveFileSystem::init(bool reformatOnFail=true) to match new microStore.
- Static Transport::get_path_table() -> path_table(); instance getter unchanged.
- Remove duplicate shim Cryptography/BZ2 (microReticulum provides it now; keep
  lib/libbz2 as the ESP32 bzlib provider).
- patch_littlefs_paths.py: normalize microStore's LittleFS adapter paths to a
  leading "/" -- ESP32 Arduino LittleFS rejects "./"-prefixed paths, which
  silently broke the path store (no peer paths learned, all messaging blocked).

Validated on T-Deck Plus: builds (RAM 27.5% / Flash 77.7%), boots stable
(no WDT/panic), and a full on-device LXMF e2e (DIRECT + OPPORTUNISTIC +
bz2-compressed-Resource receive) passes 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:49:44 -04:00

375 lines
11 KiB
C++

// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#include "Display.h"
#include "SDAccess.h"
#ifdef ARDUINO
#include <microReticulum/Log.h>
#include <esp_heap_caps.h>
#if __has_include("SplashImage.h")
#include "SplashImage.h"
#endif
using namespace RNS;
namespace Hardware {
namespace TDeck {
SPIClass* Display::_spi = nullptr;
SemaphoreHandle_t Display::_spi_mutex = nullptr;
uint8_t Display::_brightness = Disp::BACKLIGHT_DEFAULT;
bool Display::_initialized = false;
bool Display::_hw_initialized = false;
volatile uint32_t Display::_flush_count = 0;
volatile uint32_t Display::_last_flush_ms = 0;
uint32_t Display::_last_health_log_ms = 0;
void Display::set_spi_mutex(SemaphoreHandle_t mutex) {
_spi_mutex = mutex;
}
bool Display::init() {
if (_initialized) {
return true;
}
INFO("Initializing T-Deck display");
// Initialize hardware first
if (!init_hardware_only()) {
return false;
}
// Allocate LVGL buffers in PSRAM (2 buffers for double buffering)
static lv_disp_draw_buf_t draw_buf;
size_t buf_size = Disp::WIDTH * Disp::HEIGHT * sizeof(lv_color_t);
lv_color_t* buf1 = (lv_color_t*)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
lv_color_t* buf2 = (lv_color_t*)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
if (!buf1 || !buf2) {
ERROR("Failed to allocate LVGL buffers in PSRAM");
if (buf1) heap_caps_free(buf1);
if (buf2) heap_caps_free(buf2);
return false;
}
INFO((" LVGL buffers allocated in PSRAM (" + String(buf_size * 2) + " bytes)").c_str());
// Initialize LVGL draw buffer
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, Disp::WIDTH * Disp::HEIGHT);
// Register display driver with LVGL
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = Disp::WIDTH;
disp_drv.ver_res = Disp::HEIGHT;
disp_drv.flush_cb = lvgl_flush_cb;
disp_drv.draw_buf = &draw_buf;
lv_disp_t* disp = lv_disp_drv_register(&disp_drv);
if (!disp) {
ERROR("Failed to register display driver with LVGL");
return false;
}
_initialized = true;
INFO("Display initialized successfully");
return true;
}
bool Display::init_hardware_only() {
if (_hw_initialized) {
return true;
}
INFO("Initializing display hardware");
// Configure backlight PWM — keep OFF until splash is rendered
// Use ledcWrite directly so _brightness retains the default value for show_splash()
ledcSetup(Disp::BACKLIGHT_CHANNEL, Disp::BACKLIGHT_FREQ, Disp::BACKLIGHT_RESOLUTION);
ledcAttachPin(Pin::DISPLAY_BACKLIGHT, Disp::BACKLIGHT_CHANNEL);
ledcWrite(Disp::BACKLIGHT_CHANNEL, 0);
// Use global SPI (FSPI) — all peripherals (display, LoRa, SD card) must
// share the same SPI peripheral to avoid GPIO matrix pin conflicts.
// SDAccess::init() already called SPI.begin() with correct pins.
_spi = &SPI;
if (!SDAccess::is_ready()) {
// SD didn't init (no card) — we need to init SPI ourselves
_spi->begin(Pin::DISPLAY_SCK, Radio::SPI_MISO, Pin::DISPLAY_MOSI, Pin::DISPLAY_CS);
}
// Configure CS and DC pins
pinMode(Pin::DISPLAY_CS, OUTPUT);
pinMode(Pin::DISPLAY_DC, OUTPUT);
digitalWrite(Pin::DISPLAY_CS, HIGH);
digitalWrite(Pin::DISPLAY_DC, HIGH);
// Initialize ST7789V registers
init_registers();
_hw_initialized = true;
INFO(" Display hardware ready");
return true;
}
void Display::init_registers() {
INFO(" Configuring ST7789V registers");
// Software reset
write_command(Command::SWRESET);
// DELAY RATIONALE: LCD reset pulse width
// ST7789 datasheet specifies minimum 120ms reset low time for reliable initialization.
// Using 150ms for margin. Shorter values cause display initialization failures.
delay(150);
// Sleep out
write_command(Command::SLPOUT);
// DELAY RATIONALE: SPI command settling - allow display controller to process command before next
delay(10);
// Color mode: 16-bit (RGB565)
write_command(Command::COLMOD);
write_data(0x55); // 16-bit color
// Memory data access control (rotation + RGB order)
write_command(Command::MADCTL);
uint8_t madctl = MADCTL::MX | MADCTL::MY | MADCTL::RGB;
if (Disp::ROTATION == 1) {
madctl = MADCTL::MX | MADCTL::MV | MADCTL::RGB; // Landscape
}
write_data(madctl);
// Inversion on (required for ST7789V panels)
write_command(Command::INVON);
// DELAY RATIONALE: SPI command settling - allow display controller to process command before next
delay(10);
// Normal display mode
write_command(Command::NORON);
// DELAY RATIONALE: SPI command settling - allow display controller to process command before next
delay(10);
// Display on
write_command(Command::DISPON);
// DELAY RATIONALE: SPI command settling - allow display controller to process command before next
delay(10);
// Show splash image (or black screen if SplashImage.h not generated)
show_splash();
INFO(" ST7789V initialized");
}
void Display::set_brightness(uint8_t brightness) {
_brightness = brightness;
ledcWrite(Disp::BACKLIGHT_CHANNEL, brightness);
}
uint8_t Display::get_brightness() {
return _brightness;
}
void Display::set_power(bool on) {
if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(200)) != pdTRUE) {
return;
}
if (on) {
write_command(Command::DISPON);
set_brightness(_brightness);
} else {
set_brightness(0);
write_command(Command::DISPOFF);
}
if (_spi_mutex) xSemaphoreGive(_spi_mutex);
}
void Display::show_splash() {
#ifdef HAS_SPLASH_IMAGE
// Splash image is full-screen (320x240), so offsets are 0
static const uint16_t X_OFFSET = (Disp::WIDTH - SPLASH_WIDTH) / 2;
static const uint16_t Y_OFFSET = (Disp::HEIGHT - SPLASH_HEIGHT) / 2;
set_addr_window(X_OFFSET, Y_OFFSET,
X_OFFSET + SPLASH_WIDTH - 1,
Y_OFFSET + SPLASH_HEIGHT - 1);
begin_write();
write_command(Command::RAMWR);
// Stream PROGMEM data in 512-byte chunks to avoid large stack allocation
static const size_t CHUNK_SIZE = 512;
uint8_t buf[CHUNK_SIZE];
size_t total = SPLASH_WIDTH * SPLASH_HEIGHT * 2;
for (size_t offset = 0; offset < total; offset += CHUNK_SIZE) {
size_t len = (offset + CHUNK_SIZE <= total) ? CHUNK_SIZE : (total - offset);
memcpy_P(buf, splash_image + offset, len);
write_data(buf, len);
}
end_write();
INFO(" Splash image rendered");
#else
// No splash image — fill with background color (#1D1A1E -> RGB565 0x18C3)
fill_screen(0x18C3);
#endif
// Backlight on now that screen content is ready
set_brightness(_brightness);
}
void Display::fill_screen(uint16_t color) {
if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(500)) != pdTRUE) {
return;
}
set_addr_window(0, 0, Disp::WIDTH - 1, Disp::HEIGHT - 1);
begin_write();
write_command(Command::RAMWR);
// Send color data for entire screen
uint8_t color_bytes[2];
color_bytes[0] = (color >> 8) & 0xFF;
color_bytes[1] = color & 0xFF;
for (uint32_t i = 0; i < (uint32_t)Disp::WIDTH * Disp::HEIGHT; i++) {
write_data(color_bytes, 2);
}
end_write();
if (_spi_mutex) xSemaphoreGive(_spi_mutex);
}
void Display::draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
if (x < 0 || y < 0 || x + w > Disp::WIDTH || y + h > Disp::HEIGHT) {
return;
}
if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(200)) != pdTRUE) {
return;
}
set_addr_window(x, y, x + w - 1, y + h - 1);
begin_write();
write_command(Command::RAMWR);
uint8_t color_bytes[2];
color_bytes[0] = (color >> 8) & 0xFF;
color_bytes[1] = color & 0xFF;
for (int32_t i = 0; i < w * h; i++) {
write_data(color_bytes, 2);
}
end_write();
if (_spi_mutex) xSemaphoreGive(_spi_mutex);
}
void Display::lvgl_flush_cb(lv_disp_drv_t* drv, const lv_area_t* area, lv_color_t* color_p) {
if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(100)) != pdTRUE) {
// Skip this frame — LVGL will retry next tick
lv_disp_flush_ready(drv);
return;
}
uint32_t start = millis();
int32_t w = area->x2 - area->x1 + 1;
int32_t h = area->y2 - area->y1 + 1;
set_addr_window(area->x1, area->y1, area->x2, area->y2);
begin_write();
write_command(Command::RAMWR);
// Send pixel data
// lv_color_t is RGB565, which matches ST7789V format
size_t len = w * h * sizeof(lv_color_t);
write_data((const uint8_t*)color_p, len);
end_write();
if (_spi_mutex) xSemaphoreGive(_spi_mutex);
_flush_count++;
_last_flush_ms = millis();
uint32_t elapsed = _last_flush_ms - start;
if (elapsed > 50) {
Serial.printf("[DISP] WARNING: flush took %lu ms (%ldx%ld, %zu bytes) - SPI contention?\n",
elapsed, w, h, len);
}
// Tell LVGL we're done flushing
lv_disp_flush_ready(drv);
}
void Display::log_health() {
uint32_t now = millis();
// Log every 10 seconds
if (now - _last_health_log_ms < 10000) return;
_last_health_log_ms = now;
uint32_t since_flush = now - _last_flush_ms;
if (since_flush > 2000) {
Serial.printf("[DISP] STALLED: no flush for %lu ms! flush_count=%lu\n",
since_flush, _flush_count);
} else {
Serial.printf("[DISP] ok: last_flush=%lu ms ago, total_flushes=%lu\n",
since_flush, _flush_count);
}
}
void Display::write_command(uint8_t cmd) {
digitalWrite(Pin::DISPLAY_DC, LOW); // Command mode
digitalWrite(Pin::DISPLAY_CS, LOW);
_spi->transfer(cmd);
digitalWrite(Pin::DISPLAY_CS, HIGH);
}
void Display::write_data(uint8_t data) {
digitalWrite(Pin::DISPLAY_DC, HIGH); // Data mode
digitalWrite(Pin::DISPLAY_CS, LOW);
_spi->transfer(data);
digitalWrite(Pin::DISPLAY_CS, HIGH);
}
void Display::write_data(const uint8_t* data, size_t len) {
digitalWrite(Pin::DISPLAY_DC, HIGH); // Data mode
digitalWrite(Pin::DISPLAY_CS, LOW);
_spi->transferBytes(data, nullptr, len);
digitalWrite(Pin::DISPLAY_CS, HIGH);
}
void Display::set_addr_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
write_command(Command::CASET); // Column address set
write_data(x0 >> 8);
write_data(x0 & 0xFF);
write_data(x1 >> 8);
write_data(x1 & 0xFF);
write_command(Command::RASET); // Row address set
write_data(y0 >> 8);
write_data(y0 & 0xFF);
write_data(y1 >> 8);
write_data(y1 & 0xFF);
}
void Display::begin_write() {
_spi->beginTransaction(SPISettings(Disp::SPI_FREQUENCY, MSBFIRST, SPI_MODE0));
}
void Display::end_write() {
_spi->endTransaction();
}
} // namespace TDeck
} // namespace Hardware
#endif // ARDUINO