From 479673e90f5c464a76ae8edbe92142a74db3af21 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:56:27 +1100 Subject: [PATCH] LilyGo T5 S3 Epaper Pro No GPS version implementation stage 1 - H752-B; set backlight double click boot to 153 full brightness on, triple click to 40 brightness, double click off --- boards/t5s3-epaper-pro.json | 38 +++ examples/companion_radio/MyMesh.cpp | 33 ++ examples/companion_radio/MyMesh.h | 2 +- examples/companion_radio/main.cpp | 25 +- examples/companion_radio/ui-new/UITask.cpp | 38 ++- examples/companion_radio/ui-new/UITask.h | 2 + src/helpers/ui/FastEPDDisplay.cpp | 286 ++++++++++++++++ src/helpers/ui/FastEPDDisplay.h | 120 +++++++ src/helpers/ui/GxEPDDisplay.h | 10 +- .../lilygo_t5s3_epaper_pro/CPUPowerManager.h | 70 ++++ variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp | 305 ++++++++++++++++++ variants/lilygo_t5s3_epaper_pro/T5S3Board.h | 97 ++++++ .../lilygo_t5s3_epaper_pro/pins_arduino.h | 19 ++ .../lilygo_t5s3_epaper_pro/platformio.ini | 144 +++++++++ variants/lilygo_t5s3_epaper_pro/target.cpp | 95 ++++++ variants/lilygo_t5s3_epaper_pro/target.h | 50 +++ variants/lilygo_t5s3_epaper_pro/variant.h | 188 +++++++++++ 17 files changed, 1507 insertions(+), 15 deletions(-) create mode 100644 boards/t5s3-epaper-pro.json create mode 100644 src/helpers/ui/FastEPDDisplay.cpp create mode 100644 src/helpers/ui/FastEPDDisplay.h create mode 100644 variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h create mode 100644 variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp create mode 100644 variants/lilygo_t5s3_epaper_pro/T5S3Board.h create mode 100644 variants/lilygo_t5s3_epaper_pro/pins_arduino.h create mode 100644 variants/lilygo_t5s3_epaper_pro/platformio.ini create mode 100644 variants/lilygo_t5s3_epaper_pro/target.cpp create mode 100644 variants/lilygo_t5s3_epaper_pro/target.h create mode 100644 variants/lilygo_t5s3_epaper_pro/variant.h diff --git a/boards/t5s3-epaper-pro.json b/boards/t5s3-epaper-pro.json new file mode 100644 index 0000000..93c6edf --- /dev/null +++ b/boards/t5s3-epaper-pro.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T5S3 E-Paper Pro", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://lilygo.cc/products/t5-e-paper-s3-pro", + "vendor": "LILYGO" +} diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ab74c54..2dbb91f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -4,6 +4,10 @@ #include #include "RadioPresets.h" // Shared radio presets (serial CLI + settings screen) +#if defined(LilyGo_T5S3_EPaper_Pro) + #include "target.h" // for board.setBacklight() CLI command +#endif + #ifdef HAS_4G_MODEM #include "ModemManager.h" // Serial CLI modem commands #endif @@ -2662,6 +2666,30 @@ void MyMesh::checkCLIRescueCmd() { Serial.println(" Error: use 0 (default), 4800, 9600, 19200, 38400, 57600, or 115200"); } + // Backlight control (T5S3 E-Paper Pro only) + } else if (memcmp(config, "backlight ", 10) == 0) { +#if defined(LilyGo_T5S3_EPaper_Pro) + const char* val = &config[10]; + if (strcmp(val, "on") == 0) { + board.setBacklight(true); + Serial.println(" > backlight ON"); + } else if (strcmp(val, "off") == 0) { + board.setBacklight(false); + Serial.println(" > backlight OFF"); + } else { + int brightness = atoi(val); + if (brightness >= 0 && brightness <= 255) { + board.setBacklightBrightness((uint8_t)brightness); + board.setBacklight(brightness > 0); + Serial.printf(" > backlight brightness = %d\n", brightness); + } else { + Serial.println(" Error: use 'on', 'off', or 0-255"); + } + } +#else + Serial.println(" Error: backlight not available on this device"); +#endif + } else { Serial.printf(" Error: unknown setting '%s' (try 'help')\n", config); } @@ -2707,6 +2735,11 @@ void MyMesh::checkCLIRescueCmd() { Serial.println(" erase Format filesystem"); Serial.println(" reboot Restart device"); Serial.println(" ls / cat / rm File operations"); +#if defined(LilyGo_T5S3_EPaper_Pro) + Serial.println(""); + Serial.println(" Display:"); + Serial.println(" set backlight on/off/0-255 Control front-light"); +#endif // ===================================================================== // Existing system commands (unchanged) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 7b34d90..2c2b2a6 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,7 +8,7 @@ #define FIRMWARE_VER_CODE 10 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "8 March 2026" +#define FIRMWARE_BUILD_DATE "11 March 2026" #endif #ifndef FIRMWARE_VERSION diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 96eaa61..b4e7de8 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -49,10 +49,6 @@ static bool webReaderNeedsRefresh = false; static bool webReaderTextEntry = false; // True when URL/password entry active #endif - // AGC reset - periodically re-assert RX boosted gain to prevent sensitivity drift - #define AGC_RESET_INTERVAL_MS 500 - static unsigned long lastAGCReset = 0; - // Emoji picker state #include "EmojiPicker.h" static bool emojiPickerMode = false; @@ -90,8 +86,6 @@ TouchInput touchInput(&Wire); #endif - CPUPowerManager cpuPower; - void initKeyboard(); void handleKeyboardInput(); void drawComposeScreen(); @@ -343,6 +337,11 @@ } #endif +// Board-agnostic: CPU frequency scaling and AGC reset +CPUPowerManager cpuPower; +#define AGC_RESET_INTERVAL_MS 500 +static unsigned long lastAGCReset = 0; + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -436,7 +435,9 @@ static uint32_t _atoi(const char* sp) { /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - #include "MapScreen.h" // After BLE — PNGdec headers conflict with BLE if included earlier + #if HAS_GPS + #include "MapScreen.h" // After BLE — PNGdec headers conflict with BLE if included earlier + #endif UITask ui_task(&board, &serial_interface); #endif @@ -749,8 +750,8 @@ void setup() { initKeyboard(); #endif - // Initialize touch input (CST328) - #ifdef HAS_TOUCHSCREEN + // Initialize touch input (CST328 on T-Deck Pro) + #if defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN) if (touchInput.begin(CST328_PIN_INT)) { MESH_DEBUG_PRINTLN("setup() - Touch input initialized"); } else { @@ -880,6 +881,7 @@ void loop() { sensors.loop(); // Map screen: periodically update own GPS position and contact markers + #if HAS_GPS if (ui_task.isOnMapScreen()) { static unsigned long lastMapUpdate = 0; if (millis() - lastMapUpdate > 30000) { // Every 30 seconds @@ -887,9 +889,7 @@ void loop() { MapScreen* ms = (MapScreen*)ui_task.getMapScreen(); if (ms) { // Update own GPS position when GPS is enabled - #if HAS_GPS ms->updateGPSPosition(sensors.node_lat, sensors.node_lon); - #endif // Always refresh contact markers (new contacts arrive via radio) ms->clearMarkers(); @@ -905,12 +905,13 @@ void loop() { } } } + #endif // CPU frequency auto-timeout back to idle cpuPower.loop(); // Audiobook: service audio decode regardless of which screen is active - #ifndef HAS_4G_MODEM + #if defined(LilyGo_TDeck_Pro) && !defined(HAS_4G_MODEM) { AudiobookPlayerScreen* abPlayer = (AudiobookPlayerScreen*)ui_task.getAudiobookScreen(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index bed18b9..0ca5932 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -4,7 +4,9 @@ #include "NotesScreen.h" #include "RepeaterAdminScreen.h" #include "DiscoveryScreen.h" -#include "MapScreen.h" +#if HAS_GPS + #include "MapScreen.h" +#endif #include "target.h" #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) #include @@ -340,7 +342,11 @@ public: y += 10; display.drawTextCentered(display.width() / 2, y, "[N] Notes [S] Settings "); y += 10; +#if HAS_GPS display.drawTextCentered(display.width() / 2, y, "[E] Reader [G] Maps "); +#else + display.drawTextCentered(display.width() / 2, y, "[E] Reader "); +#endif y += 10; #if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER) display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser "); @@ -359,7 +365,11 @@ public: // Nav hint display.setColor(DisplayDriver::GREEN); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.drawTextCentered(display.width() / 2, y, "Tap screen to cycle home views"); +#else display.drawTextCentered(display.width() / 2, y, "Press A/D to cycle home views"); +#endif display.setTextSize(1); // restore } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -952,7 +962,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #ifdef HAS_4G_MODEM sms_screen = new SMSScreen(this); #endif +#if HAS_GPS map_screen = new MapScreen(this); +#else + map_screen = nullptr; +#endif setCurrScreen(splash); } @@ -1362,6 +1376,16 @@ char UITask::handleLongPress(char c) { char UITask::handleDoubleClick(char c) { MESH_DEBUG_PRINTLN("UITask: double click triggered"); +#if defined(LilyGo_T5S3_EPaper_Pro) + // Double-click boot button → full brightness backlight toggle + if (board.isBacklightOn()) { + board.setBacklight(false); + } else { + board.setBacklightBrightness(153); + board.setBacklight(true); + } + c = 0; // consume event — don't pass through as navigation +#endif checkDisplayOn(c); return c; } @@ -1369,7 +1393,17 @@ char UITask::handleDoubleClick(char c) { char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); +#if defined(LilyGo_T5S3_EPaper_Pro) + // Triple-click → half brightness backlight (comfortable reading) + if (board.isBacklightOn()) { + board.setBacklight(false); // If already on, turn off + } else { + board.setBacklightBrightness(80); + board.setBacklight(true); + } +#else toggleBuzzer(); +#endif c = 0; return c; } @@ -1643,6 +1677,7 @@ void UITask::gotoWebReader() { } #endif +#if HAS_GPS void UITask::gotoMapScreen() { MapScreen* map = (MapScreen*)map_screen; if (_display != NULL) { @@ -1655,6 +1690,7 @@ void UITask::gotoMapScreen() { _auto_off = millis() + AUTO_OFF_MILLIS; _next_refresh = 100; } +#endif void UITask::onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) { if (repeater_admin && isOnRepeaterAdmin()) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 4b86f4c..5a02627 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -121,7 +121,9 @@ public: void gotoAudiobookPlayer(); // Navigate to audiobook player void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin void gotoDiscoveryScreen(); // Navigate to node discovery scan +#if HAS_GPS void gotoMapScreen(); // Navigate to map tile screen +#endif #ifdef MECK_WEB_READER void gotoWebReader(); // Navigate to web reader (browser) #endif diff --git a/src/helpers/ui/FastEPDDisplay.cpp b/src/helpers/ui/FastEPDDisplay.cpp new file mode 100644 index 0000000..187dcc5 --- /dev/null +++ b/src/helpers/ui/FastEPDDisplay.cpp @@ -0,0 +1,286 @@ +#include "FastEPDDisplay.h" +#include "FastEPD.h" +#include + +// Fallback if FastEPD doesn't define these constants +#ifndef BBEP_SUCCESS +#define BBEP_SUCCESS 0 +#endif +#ifndef CLEAR_FAST +#define CLEAR_FAST 0 +#endif +#ifndef CLEAR_SLOW +#define CLEAR_SLOW 1 +#endif +#ifndef BB_MODE_1BPP +#define BB_MODE_1BPP 0 +#endif + +// FastEPD constants (defined in FastEPD.h) +// BB_PANEL_LILYGO_T5PRO_V2 — board ID for V2 hardware +// BB_MODE_1BPP — 1-bit per pixel mode +// CLEAR_FAST, CLEAR_SLOW — full refresh modes + +// Periodic slow (deep) refresh to clear ghosting +#define FULL_SLOW_PERIOD 20 // every 20 fast-refreshes, do a slow refresh + +FastEPDDisplay::~FastEPDDisplay() { + delete _canvas; + delete _epd; +} + +bool FastEPDDisplay::begin() { + if (_init) return true; + + Serial.println("[FastEPD] Initializing T5S3 E-Paper Pro V2..."); + + // Create FastEPD instance and init hardware + _epd = new FASTEPD; + // Meshtastic-proven init for V2 hardware (pinned FastEPD fork commit) + Serial.println("[FastEPD] Using BB_PANEL_LILYGO_T5PRO_V2"); + int rc = _epd->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + if (rc != BBEP_SUCCESS) { + Serial.printf("[FastEPD] initPanel FAILED: %d\n", rc); + delete _epd; + _epd = nullptr; + return false; + } + Serial.printf("[FastEPD] Panel initialized (rc=%d)\n", rc); + + // Enable display via PCA9535 GPIO (required for V2 hardware) + // Pin 0 on PCA9535 = EP_OE (output enable for source driver) + _epd->ioPinMode(0, OUTPUT); + _epd->ioWrite(0, HIGH); + Serial.println("[FastEPD] PCA9535 EP_OE set HIGH"); + + // Set 1-bit per pixel mode + _epd->setMode(BB_MODE_1BPP); + Serial.println("[FastEPD] Mode set to 1BPP"); + + // Create Adafruit_GFX canvas for drawing (960×540, 1-bit) + // ~64KB, should auto-allocate in PSRAM on ESP32-S3 with PSRAM enabled + _canvas = new GFXcanvas1(EPD_WIDTH, EPD_HEIGHT); + if (!_canvas || !_canvas->getBuffer()) { + Serial.println("[FastEPD] Canvas allocation FAILED!"); + return false; + } + Serial.printf("[FastEPD] Canvas allocated: %dx%d (%d bytes)\n", + EPD_WIDTH, EPD_HEIGHT, (EPD_WIDTH * EPD_HEIGHT) / 8); + + // Initial clear — white screen + Serial.println("[FastEPD] Calling clearWhite()..."); + _epd->clearWhite(); + Serial.println("[FastEPD] Calling fullUpdate(true) for initial clear..."); + _epd->fullUpdate(true); // blocking initial clear + _epd->backupPlane(); // Save clean state for subsequent diffs + Serial.println("[FastEPD] Initial clear complete"); + + // Set canvas defaults + _canvas->fillScreen(1); // White background (bit=1 → white in FastEPD) + _canvas->setTextColor(0); // Black text (bit=0 → black in FastEPD) + _canvas->setFont(&FreeSans24pt7b); + _canvas->setTextWrap(false); + + _curr_color = GxEPD_BLACK; + _init = true; + _isOn = true; + + Serial.println("[FastEPD] Display ready (960x540, 1BPP)"); + return true; +} + +void FastEPDDisplay::turnOn() { + if (!_init) begin(); + _isOn = true; +} + +void FastEPDDisplay::turnOff() { + _isOn = false; +} + +void FastEPDDisplay::clear() { + if (!_canvas) return; + _canvas->fillScreen(1); // White + _canvas->setTextColor(0); + _frameCRC.reset(); +} + +void FastEPDDisplay::startFrame(Color bkg) { + if (!_canvas) return; + _canvas->fillScreen(1); // White background + _canvas->setTextColor(0); // Black text + _curr_color = GxEPD_BLACK; + _frameCRC.reset(); +} + +void FastEPDDisplay::setTextSize(int sz) { + if (!_canvas) return; + _frameCRC.update(sz); + + // Font mapping for 960×540 display at ~234 DPI + // The T-Deck Pro at 240×320 (~140 DPI) uses FreeSans9pt for body text. + // At 234 DPI we need roughly 2.5× larger fonts for equivalent physical size. + // Built-in 5×7 font scaled 4× = 20×28px — readable for status bar items. + switch(sz) { + case 0: // Tiny — node name, clock, battery %, menu shortcuts + _canvas->setFont(NULL); + _canvas->setTextSize(4); // 5×7 × 4 = 20×28 physical pixels + break; + case 1: // Small/normal — body text, contact list items + _canvas->setFont(&FreeSans24pt7b); + _canvas->setTextSize(1); + break; + case 2: // Medium bold — MSG count, headings, labels + _canvas->setFont(&FreeSansBold24pt7b); + _canvas->setTextSize(1); + break; + case 3: // Large — splash screen title, onboarding + _canvas->setFont(&FreeSansBold24pt7b); + _canvas->setTextSize(1); + break; + default: + _canvas->setFont(&FreeSans24pt7b); + _canvas->setTextSize(1); + break; + } +} + +void FastEPDDisplay::setColor(Color c) { + if (!_canvas) return; + _frameCRC.update(c); + + // Colours are inverted for e-paper: + // DARK = background colour = WHITE on e-paper + // LIGHT = foreground colour = BLACK on e-paper + if (c == DARK) { + _canvas->setTextColor(1); // White (background) + _curr_color = GxEPD_WHITE; + } else { + _canvas->setTextColor(0); // Black (foreground) + _curr_color = GxEPD_BLACK; + } +} + +void FastEPDDisplay::setCursor(int x, int y) { + if (!_canvas) return; + _frameCRC.update(x); + _frameCRC.update(y); + + // Scale virtual coordinates to physical, with baseline offset + // The +5 pushes text baseline down so ascenders don't overlap elements above + // (Same convention as GxEPDDisplay for T-Deck Pro) + _canvas->setCursor( + (int)((x + offset_x) * scale_x), + (int)((y + offset_y + 5) * scale_y) + ); +} + +void FastEPDDisplay::print(const char* str) { + if (!_canvas || !str) return; + _frameCRC.update(str, strlen(str)); + _canvas->print(str); +} + +void FastEPDDisplay::fillRect(int x, int y, int w, int h) { + if (!_canvas) return; + _frameCRC.update(x); + _frameCRC.update(y); + _frameCRC.update(w); + _frameCRC.update(h); + + // Canvas uses 1-bit color: convert GxEPD color + uint16_t canvasColor = (_curr_color == GxEPD_BLACK) ? 0 : 1; + _canvas->fillRect( + (int)((x + offset_x) * scale_x), + (int)((y + offset_y) * scale_y), + (int)(w * scale_x), + (int)(h * scale_y), + canvasColor + ); +} + +void FastEPDDisplay::drawRect(int x, int y, int w, int h) { + if (!_canvas) return; + _frameCRC.update(x); + _frameCRC.update(y); + _frameCRC.update(w); + _frameCRC.update(h); + + uint16_t canvasColor = (_curr_color == GxEPD_BLACK) ? 0 : 1; + _canvas->drawRect( + (int)((x + offset_x) * scale_x), + (int)((y + offset_y) * scale_y), + (int)(w * scale_x), + (int)(h * scale_y), + canvasColor + ); +} + +void FastEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + if (!_canvas || !bits) return; + _frameCRC.update(x); + _frameCRC.update(y); + _frameCRC.update(w); + _frameCRC.update(h); + _frameCRC.update(bits, (w * h + 7) / 8); + + uint16_t canvasColor = (_curr_color == GxEPD_BLACK) ? 0 : 1; + uint16_t startX = (int)((x + offset_x) * scale_x); + uint16_t startY = (int)((y + offset_y) * scale_y); + uint16_t widthInBytes = (w + 7) / 8; + + for (uint16_t by = 0; by < h; by++) { + int y1 = startY + (int)(by * scale_y); + int y2 = startY + (int)((by + 1) * scale_y); + int block_h = y2 - y1; + + for (uint16_t bx = 0; bx < w; bx++) { + int x1 = startX + (int)(bx * scale_x); + int x2 = startX + (int)((bx + 1) * scale_x); + int block_w = x2 - x1; + + uint16_t byteOffset = (by * widthInBytes) + (bx / 8); + uint8_t bitMask = 0x80 >> (bx & 7); + bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask; + + if (bitSet) { + _canvas->fillRect(x1, y1, block_w, block_h, canvasColor); + } + } + } +} + +uint16_t FastEPDDisplay::getTextWidth(const char* str) { + if (!_canvas || !str) return 0; + int16_t x1, y1; + uint16_t w, h; + _canvas->getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return (uint16_t)ceil((w + 1) / scale_x); +} + +void FastEPDDisplay::endFrame() { + if (!_epd || !_canvas) return; + + uint32_t crc = _frameCRC.finalize(); + if (crc == _lastCRC) { + return; // Frame unchanged, skip display update + } + _lastCRC = crc; + + // Copy GFXcanvas1 buffer to FastEPD's current buffer — direct copy. + // Both use same polarity: bit 1 = white, bit 0 = black. + // (Meshtastic inverts because OLEDDisplay uses opposite convention — not us.) + uint8_t* src = _canvas->getBuffer(); + uint8_t* dst = _epd->currentBuffer(); + size_t bufSize = ((uint32_t)EPD_WIDTH * EPD_HEIGHT) / 8; + + if (!src || !dst) return; + + memcpy(dst, src, bufSize); + + // fullUpdate(true) is the only refresh mode that gives clean transitions + // on the ED047TC1 panel. CLEAR_FAST causes ghosting/mashing on this hardware. + // The brief white flash between frames is inherent to this panel's waveform. + _epd->fullUpdate(true); + _epd->backupPlane(); +} \ No newline at end of file diff --git a/src/helpers/ui/FastEPDDisplay.h b/src/helpers/ui/FastEPDDisplay.h new file mode 100644 index 0000000..23f2eb9 --- /dev/null +++ b/src/helpers/ui/FastEPDDisplay.h @@ -0,0 +1,120 @@ +#pragma once + +// ============================================================================= +// FastEPDDisplay — Parallel e-ink display driver for T5 S3 E-Paper Pro +// +// Architecture: +// - FastEPD handles hardware init, power management, and display refresh +// - Adafruit_GFX GFXcanvas1 handles all drawing/text rendering +// - On endFrame(), canvas buffer is copied to FastEPD and display is updated +// +// This avoids depending on FastEPD's drawing API — only uses its well-tested +// hardware interface (initPanel, fullUpdate, partialUpdate, currentBuffer). +// ============================================================================= + +#include +#include "variant.h" // EPD_WIDTH, EPD_HEIGHT (only compiled for T5S3 builds) +#include +#include +#include +#include +#include +#include +#include + +#include "DisplayDriver.h" + +// GxEPD2 color constant compatibility — MapScreen uses these directly +#ifndef GxEPD_BLACK +#define GxEPD_BLACK 0x0000 +#endif +#ifndef GxEPD_WHITE +#define GxEPD_WHITE 0xFFFF +#endif + +// Forward declare FastEPD class (actual include in .cpp) +class FASTEPD; + +// Inline CRC32 for frame change detection +// (Copied from GxEPDDisplay.h — avoids CRC32/PNGdec name collision) +class FrameCRC32 { + uint32_t _crc = 0xFFFFFFFF; +public: + void reset() { _crc = 0xFFFFFFFF; } + template void update(T val) { + const uint8_t* p = (const uint8_t*)&val; + for (size_t i = 0; i < sizeof(T); i++) { + _crc ^= p[i]; + for (int b = 0; b < 8; b++) + _crc = (_crc >> 1) ^ (0xEDB88320 & -(int32_t)(_crc & 1)); + } + } + template void update(const T* data, size_t len) { + const uint8_t* p = (const uint8_t*)data; + for (size_t i = 0; i < len * sizeof(T); i++) { + _crc ^= p[i]; + for (int b = 0; b < 8; b++) + _crc = (_crc >> 1) ^ (0xEDB88320 & -(int32_t)(_crc & 1)); + } + } + uint32_t finalize() { return _crc ^ 0xFFFFFFFF; } +}; + + +class FastEPDDisplay : public DisplayDriver { + FASTEPD* _epd; + GFXcanvas1* _canvas; // Adafruit_GFX 1-bit drawing surface (960×540) + bool _init = false; + bool _isOn = false; + uint16_t _curr_color; // GxEPD_BLACK or GxEPD_WHITE for canvas drawing + FrameCRC32 _frameCRC; + uint32_t _lastCRC = 0; + int _fullRefreshCount = 0; // Track for periodic slow refresh + uint32_t _lastUpdateMs = 0; // Rate limiting — minimum interval between refreshes + + // Virtual 128×128 → physical 960×540 mapping + // Non-square scaling (1.78:1 aspect stretch) — acceptable for initial bringup + static constexpr float scale_x = 7.5f; // 960 / 128 + static constexpr float scale_y = 4.21875f; // 540 / 128 + static constexpr float offset_x = 0.0f; + static constexpr float offset_y = 0.0f; + +public: + FastEPDDisplay() : DisplayDriver(128, 128), _epd(nullptr), _canvas(nullptr) {} + ~FastEPDDisplay(); + + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; + + // --- Raw pixel access for MapScreen (bypasses scaling) --- + void drawPixelRaw(int16_t x, int16_t y, uint16_t color) { + if (_canvas) _canvas->drawPixel(x, y, color ? 1 : 0); + } + int16_t rawWidth() { return EPD_WIDTH; } + int16_t rawHeight() { return EPD_HEIGHT; } + + void drawTextRaw(int16_t x, int16_t y, const char* text, uint16_t color) { + if (!_canvas) return; + _canvas->setFont(NULL); + _canvas->setTextSize(4); // 4× built-in 5×7 = 20×28, readable on 960×540 + _canvas->setTextColor(color ? 1 : 0); + _canvas->setCursor(x, y); + _canvas->print(text); + } + + void invalidateFrameCRC() { _lastCRC = 0; } +}; \ No newline at end of file diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index e7c6f33..96c927d 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -1,5 +1,11 @@ #pragma once +// T5S3 E-Paper Pro uses parallel e-ink (FastEPD), not SPI (GxEPD2) +#if defined(LilyGo_T5S3_EPaper_Pro) + #include "FastEPDDisplay.h" + using GxEPDDisplay = FastEPDDisplay; +#else + #include #include @@ -104,4 +110,6 @@ public: // Force endFrame() to push to display even if CRC unchanged // (needed because drawPixelRaw bypasses CRC tracking) void invalidateFrameCRC() { last_display_crc_value = 0; } -}; \ No newline at end of file +}; + +#endif // !LilyGo_T5S3_EPaper_Pro \ No newline at end of file diff --git a/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h b/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h new file mode 100644 index 0000000..d1f33a5 --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/CPUPowerManager.h @@ -0,0 +1,70 @@ +#pragma once + +#include + +// CPU Frequency Scaling for ESP32-S3 +// +// Typical current draw (CPU only, rough): +// 240 MHz ~70-80 mA +// 160 MHz ~50-60 mA +// 80 MHz ~30-40 mA +// +// SPI peripherals and UART use their own clock dividers from the APB clock, +// so LoRa, e-ink, and GPS serial all work fine at 80MHz. + +#ifdef ESP32 + +#ifndef CPU_FREQ_IDLE +#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening +#endif + +#ifndef CPU_FREQ_BOOST +#define CPU_FREQ_BOOST 240 // MHz — heavy processing +#endif + +#ifndef CPU_BOOST_TIMEOUT_MS +#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds +#endif + +class CPUPowerManager { +public: + CPUPowerManager() : _boosted(false), _boost_started(0) {} + + void begin() { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + _boosted = false; + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + + void loop() { + if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) { + setIdle(); + } + } + + void setBoost() { + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_BOOST); + _boosted = true; + MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST); + } + _boost_started = millis(); + } + + void setIdle() { + if (_boosted) { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + _boosted = false; + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + } + + bool isBoosted() const { return _boosted; } + uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); } + +private: + bool _boosted; + unsigned long _boost_started; +}; + +#endif // ESP32 \ No newline at end of file diff --git a/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp b/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp new file mode 100644 index 0000000..4d60ace --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp @@ -0,0 +1,305 @@ +#include +#include "variant.h" +#include "T5S3Board.h" +#include // For MESH_DEBUG_PRINTLN + +void T5S3Board::begin() { + MESH_DEBUG_PRINTLN("T5S3Board::begin() - starting"); + + // Initialize I2C with T5S3 V2 pins + // Note: No explicit peripheral power enable needed on T5S3 + // (unlike T-Deck Pro's PIN_PERF_POWERON) + Wire.begin(I2C_SDA, I2C_SCL); + Wire.setClock(100000); // 100kHz for reliable fuel gauge communication + MESH_DEBUG_PRINTLN("T5S3Board::begin() - I2C initialized (SDA=%d, SCL=%d)", I2C_SDA, I2C_SCL); + + // Call parent class begin (handles CPU freq, etc.) + // Note: ESP32Board::begin() also calls Wire.begin() but with our + // PIN_BOARD_SDA/SCL defines it will use the same pins — harmless. + ESP32Board::begin(); + + // Configure backlight (off by default — save power) + #ifdef BOARD_BL_EN + pinMode(BOARD_BL_EN, OUTPUT); + digitalWrite(BOARD_BL_EN, LOW); + MESH_DEBUG_PRINTLN("T5S3Board::begin() - backlight pin configured (GPIO%d)", BOARD_BL_EN); + #endif + + // Configure user button + pinMode(PIN_USER_BTN, INPUT); + + // Configure LoRa SPI MISO pullup + pinMode(P_LORA_MISO, INPUT_PULLUP); + + // Handle wake from deep sleep + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1ULL << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } + + // Test BQ27220 communication and configure design capacity + #if HAS_BQ27220 + uint16_t voltage = getBattMilliVolts(); + MESH_DEBUG_PRINTLN("T5S3Board::begin() - Battery voltage: %d mV", voltage); + configureFuelGauge(); + #endif + + // Early low-voltage protection + #if HAS_BQ27220 && defined(AUTO_SHUTDOWN_MILLIVOLTS) + { + uint16_t bootMv = getBattMilliVolts(); + if (bootMv > 0 && bootMv < AUTO_SHUTDOWN_MILLIVOLTS) { + Serial.printf("CRITICAL: Boot voltage %dmV < %dmV — sleeping immediately\n", + bootMv, AUTO_SHUTDOWN_MILLIVOLTS); + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_enable_ext1_wakeup(1ULL << PIN_USER_BTN, ESP_EXT1_WAKEUP_ANY_HIGH); + esp_deep_sleep_start(); + } + } + #endif + + MESH_DEBUG_PRINTLN("T5S3Board::begin() - complete"); +} + +// ---- BQ27220 register helpers (static, file-local) ---- + +#if HAS_BQ27220 +static uint16_t bq27220_read16(uint8_t reg) { + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + if (Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2) != 2) return 0; + uint16_t val = Wire.read(); + val |= (Wire.read() << 8); + return val; +} + +static uint8_t bq27220_read8(uint8_t reg) { + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + if (Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)1) != 1) return 0; + return Wire.read(); +} + +static bool bq27220_writeControl(uint16_t subcmd) { + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x00); + Wire.write(subcmd & 0xFF); + Wire.write((subcmd >> 8) & 0xFF); + return Wire.endTransmission() == 0; +} +#endif + +// ---- BQ27220 public interface ---- + +uint16_t T5S3Board::getBattMilliVolts() { + #if HAS_BQ27220 + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(BQ27220_REG_VOLTAGE); + if (Wire.endTransmission(false) != 0) return 0; + uint8_t count = Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2); + if (count != 2) return 0; + uint16_t voltage = Wire.read(); + voltage |= (Wire.read() << 8); + return voltage; + #else + return 0; + #endif +} + +uint8_t T5S3Board::getBatteryPercent() { + #if HAS_BQ27220 + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(BQ27220_REG_SOC); + if (Wire.endTransmission(false) != 0) return 0; + uint8_t count = Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2); + if (count != 2) return 0; + uint16_t soc = Wire.read(); + soc |= (Wire.read() << 8); + return (uint8_t)min(soc, (uint16_t)100); + #else + return 0; + #endif +} + +int16_t T5S3Board::getAvgCurrent() { + #if HAS_BQ27220 + return (int16_t)bq27220_read16(BQ27220_REG_AVG_CURRENT); + #else + return 0; + #endif +} + +int16_t T5S3Board::getAvgPower() { + #if HAS_BQ27220 + return (int16_t)bq27220_read16(BQ27220_REG_AVG_POWER); + #else + return 0; + #endif +} + +uint16_t T5S3Board::getTimeToEmpty() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_TIME_TO_EMPTY); + #else + return 0xFFFF; + #endif +} + +uint16_t T5S3Board::getRemainingCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_REMAIN_CAP); + #else + return 0; + #endif +} + +uint16_t T5S3Board::getFullChargeCapacity() { + #if HAS_BQ27220 + uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP); + if (fcc > BQ27220_DESIGN_CAPACITY_MAH) fcc = BQ27220_DESIGN_CAPACITY_MAH; + return fcc; + #else + return 0; + #endif +} + +uint16_t T5S3Board::getDesignCapacity() { + #if HAS_BQ27220 + return bq27220_read16(BQ27220_REG_DESIGN_CAP); + #else + return 0; + #endif +} + +int16_t T5S3Board::getBattTemperature() { + #if HAS_BQ27220 + uint16_t raw = bq27220_read16(BQ27220_REG_TEMPERATURE); + return (int16_t)(raw - 2731); // 0.1°K to 0.1°C + #else + return 0; + #endif +} + +// ---- BQ27220 Design Capacity configuration ---- +// Identical procedure to TDeckBoard — sets 1500 mAh for T5S3's larger cell. +// The BQ27220 ships with 3000 mAh default. This writes once on first boot +// and persists in battery-backed RAM. + +bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { +#if HAS_BQ27220 + uint16_t currentDC = bq27220_read16(BQ27220_REG_DESIGN_CAP); + Serial.printf("BQ27220: Design Capacity = %d mAh (target %d)\n", currentDC, designCapacity_mAh); + + if (currentDC == designCapacity_mAh) { + uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: Design Capacity correct, FCC=%d mAh\n", fcc); + if (fcc < designCapacity_mAh * 3 / 2) { + return true; // FCC is sane, nothing to do + } + // FCC is stale from factory — fall through to reconfigure + Serial.printf("BQ27220: FCC %d >> DC %d, reconfiguring\n", fcc, designCapacity_mAh); + } + + // Unseal + bq27220_writeControl(0x0414); delay(2); + bq27220_writeControl(0x3672); delay(2); + // Full Access + bq27220_writeControl(0xFFFF); delay(2); + bq27220_writeControl(0xFFFF); delay(2); + + // Enter CFG_UPDATE + bq27220_writeControl(0x0090); + bool cfgReady = false; + for (int i = 0; i < 50; i++) { + delay(20); + uint16_t opStatus = bq27220_read16(BQ27220_REG_OP_STATUS); + if (opStatus & 0x0400) { cfgReady = true; break; } + } + if (!cfgReady) { + Serial.println("BQ27220: Timeout waiting for CFGUPDATE"); + bq27220_writeControl(0x0092); + bq27220_writeControl(0x0030); + return false; + } + + // Write Design Capacity at 0x929F + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0x9F); Wire.write(0x92); + Wire.endTransmission(); + delay(10); + + uint8_t oldMSB = bq27220_read8(0x40); + uint8_t oldLSB = bq27220_read8(0x41); + uint8_t oldChk = bq27220_read8(0x60); + uint8_t dataLen = bq27220_read8(0x61); + + uint8_t newMSB = (designCapacity_mAh >> 8) & 0xFF; + uint8_t newLSB = designCapacity_mAh & 0xFF; + uint8_t temp = (255 - oldChk - oldMSB - oldLSB); + uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF); + + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0x9F); Wire.write(0x92); + Wire.write(newMSB); Wire.write(newLSB); + Wire.endTransmission(); + delay(5); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); Wire.write(newChk); Wire.write(dataLen); + Wire.endTransmission(); + delay(10); + + // Write Design Energy at 0x92A1 + { + uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.endTransmission(); + delay(10); + uint8_t deOldMSB = bq27220_read8(0x40); + uint8_t deOldLSB = bq27220_read8(0x41); + uint8_t deOldChk = bq27220_read8(0x60); + uint8_t deLen = bq27220_read8(0x61); + uint8_t deNewMSB = (designEnergy >> 8) & 0xFF; + uint8_t deNewLSB = designEnergy & 0xFF; + uint8_t deTemp = (255 - deOldChk - deOldMSB - deOldLSB); + uint8_t deNewChk = 255 - ((deTemp + deNewMSB + deNewLSB) & 0xFF); + + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.write(deNewMSB); Wire.write(deNewLSB); + Wire.endTransmission(); + delay(5); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); Wire.write(deNewChk); Wire.write(deLen); + Wire.endTransmission(); + delay(10); + } + + // Exit CFG_UPDATE with reinit + bq27220_writeControl(0x0091); + delay(200); + + // Seal + bq27220_writeControl(0x0030); + delay(5); + + // Force RESET to reinitialize FCC + bq27220_writeControl(0x0041); + delay(1000); + + uint16_t verifyDC = bq27220_read16(BQ27220_REG_DESIGN_CAP); + uint16_t newFCC = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: Post-config DC=%d FCC=%d mAh\n", verifyDC, newFCC); + + return verifyDC == designCapacity_mAh; +#else + return false; +#endif +} diff --git a/variants/lilygo_t5s3_epaper_pro/T5S3Board.h b/variants/lilygo_t5s3_epaper_pro/T5S3Board.h new file mode 100644 index 0000000..7b4887f --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/T5S3Board.h @@ -0,0 +1,97 @@ +#pragma once + +#include "variant.h" +#include +#include +#include "helpers/ESP32Board.h" +#include + +// BQ27220 Fuel Gauge Registers (shared with TDeckBoard) +#define BQ27220_REG_TEMPERATURE 0x06 +#define BQ27220_REG_VOLTAGE 0x08 +#define BQ27220_REG_CURRENT 0x0C +#define BQ27220_REG_SOC 0x2C +#define BQ27220_REG_REMAIN_CAP 0x10 +#define BQ27220_REG_FULL_CAP 0x12 +#define BQ27220_REG_AVG_CURRENT 0x14 +#define BQ27220_REG_TIME_TO_EMPTY 0x16 +#define BQ27220_REG_AVG_POWER 0x24 +#define BQ27220_REG_DESIGN_CAP 0x3C +#define BQ27220_REG_OP_STATUS 0x3A + +class T5S3Board : public ESP32Board { +public: + void begin(); + + void powerOff() override { + btStop(); + // Turn off backlight before sleeping + #ifdef BOARD_BL_EN + digitalWrite(BOARD_BL_EN, LOW); + #endif + } + + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Hold LoRa DIO1 and NSS during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); + } + + esp_deep_sleep_start(); + } + + // BQ27220 fuel gauge interface (identical register protocol to TDeckBoard) + uint16_t getBattMilliVolts() override; + uint8_t getBatteryPercent(); + int16_t getAvgCurrent(); + int16_t getAvgPower(); + uint16_t getTimeToEmpty(); + uint16_t getRemainingCapacity(); + uint16_t getFullChargeCapacity(); + uint16_t getDesignCapacity(); + int16_t getBattTemperature(); + bool configureFuelGauge(uint16_t designCapacity_mAh = BQ27220_DESIGN_CAPACITY_MAH); + + // Backlight control (GPIO11 — functional warm-tone front-light, PWM capable) + // Brightness 0-255 (0=off, 153=comfortable reading, 255=max) + bool _backlightOn = false; + uint8_t _backlightBrightness = 153; // Same default as Meshtastic + + void setBacklight(bool on) { + #ifdef BOARD_BL_EN + _backlightOn = on; + analogWrite(BOARD_BL_EN, on ? _backlightBrightness : 0); + #endif + } + + void setBacklightBrightness(uint8_t brightness) { + #ifdef BOARD_BL_EN + _backlightBrightness = brightness; + if (_backlightOn) { + analogWrite(BOARD_BL_EN, brightness); + } + #endif + } + + bool isBacklightOn() const { return _backlightOn; } + + void toggleBacklight() { + setBacklight(!_backlightOn); + } + + const char* getManufacturerName() const { + return "LilyGo T5S3 E-Paper Pro"; + } +}; \ No newline at end of file diff --git a/variants/lilygo_t5s3_epaper_pro/pins_arduino.h b/variants/lilygo_t5s3_epaper_pro/pins_arduino.h new file mode 100644 index 0000000..02d37c8 --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/pins_arduino.h @@ -0,0 +1,19 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Default Wire will be mapped to RTC, Touch, PCA9535, BQ25896, BQ27220, TPS65185 +static const uint8_t SDA = 39; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to LoRa + SD card +static const uint8_t SS = 46; // LoRa CS +static const uint8_t MOSI = 13; +static const uint8_t MISO = 21; +static const uint8_t SCK = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/lilygo_t5s3_epaper_pro/platformio.ini b/variants/lilygo_t5s3_epaper_pro/platformio.ini new file mode 100644 index 0000000..c63e65a --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/platformio.ini @@ -0,0 +1,144 @@ +; =========================================================================== +; LilyGo T5 S3 E-Paper Pro (H752-B / V2 hardware) +; 4.7" parallel e-ink (960x540), GT911 touch, SX1262 LoRa, no keyboard +; =========================================================================== +; +; Place t5s3-epaper-pro.json in boards/ directory. +; Place variant files in variants/LilyGo_T5S3_EPaper_Pro/ +; Place FastEPDDisplay.h/.cpp in src/helpers/ui/ +; + +[LilyGo_T5S3_EPaper_Pro] +extends = esp32_base +board = t5s3-epaper-pro +board_build.flash_mode = qio +board_build.f_flash = 80000000L +board_build.arduino.memory_type = qio_opi +board_upload.flash_size = 16MB +build_flags = + ${esp32_base.build_flags} + -I variants/LilyGo_T5S3_EPaper_Pro + -D LilyGo_T5S3_EPaper_Pro + -D T5_S3_EPAPER_PRO_V2 + -D BOARD_HAS_PSRAM=1 + -D CORE_DEBUG_LEVEL=1 + -D FORMAT_SPIFFS_IF_FAILED=1 + -D FORMAT_LITTLEFS_IF_FAILED=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_DIO2_AS_RF_SWITCH + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_DIO3_TCXO_VOLTAGE=2.4f + -D P_LORA_DIO_1=10 + -D P_LORA_NSS=46 + -D P_LORA_RESET=1 + -D P_LORA_BUSY=47 + -D P_LORA_SCLK=14 + -D P_LORA_MISO=21 + -D P_LORA_MOSI=13 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 + -D HAS_BQ27220=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 + -D PIN_USER_BTN=0 + -D SDCARD_USE_SPI1 + -D ARDUINO_LOOP_STACK_SIZE=32768 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/LilyGo_T5S3_EPaper_Pro> +lib_deps = + ${esp32_base.lib_deps} + +; --------------------------------------------------------------------------- +; T5S3 standalone — touch UI (stub), verify display rendering +; Uses FastEPD for parallel e-ink, Adafruit GFX for drawing +; --------------------------------------------------------------------------- +[env:meck_t5s3_headless] +extends = LilyGo_T5S3_EPaper_Pro +build_flags = + ${LilyGo_T5S3_EPaper_Pro.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=1500 + -D MAX_GROUP_CHANNELS=20 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=FastEPDDisplay + -D USE_EINK +build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T5S3_EPaper_Pro.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit GFX Library@^1.11.0 + https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + +; --------------------------------------------------------------------------- +; Phase 3+ variants (uncomment when touch input is implemented) +; --------------------------------------------------------------------------- + +; T5S3 BLE companion — touch UI, BLE phone bridging +;[env:meck_t5s3_ble] +;extends = LilyGo_T5S3_EPaper_Pro +;build_flags = +; ${LilyGo_T5S3_EPaper_Pro.build_flags} +; -I examples/companion_radio/ui-new +; -D MAX_CONTACTS=500 +; -D MAX_GROUP_CHANNELS=20 +; -D BLE_PIN_CODE=123456 +; -D OFFLINE_QUEUE_SIZE=256 +; -D DISPLAY_CLASS=FastEPDDisplay +; -D USE_EINK +; -D MECK_WEB_READER=1 +;build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter} +; + +; + +; + +; +<../examples/companion_radio/*.cpp> +; +<../examples/companion_radio/ui-new/*.cpp> +;lib_deps = +; ${LilyGo_T5S3_EPaper_Pro.lib_deps} +; densaugeo/base64 @ ~1.4.0 +; bitbank2/PNGdec@^1.0.1 +; adafruit/Adafruit GFX Library@^1.11.0 +; https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + +; T5S3 standalone — touch UI, no BLE/WiFi, maximum battery life +;[env:meck_t5s3_standalone] +;extends = LilyGo_T5S3_EPaper_Pro +;build_flags = +; ${LilyGo_T5S3_EPaper_Pro.build_flags} +; -I examples/companion_radio/ui-new +; -D MAX_CONTACTS=1500 +; -D MAX_GROUP_CHANNELS=20 +; -D OFFLINE_QUEUE_SIZE=256 +; -D DISPLAY_CLASS=FastEPDDisplay +; -D USE_EINK +;build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter} +; + +; + +; + +; +<../examples/companion_radio/*.cpp> +; +<../examples/companion_radio/ui-new/*.cpp> +;lib_deps = +; ${LilyGo_T5S3_EPaper_Pro.lib_deps} +; densaugeo/base64 @ ~1.4.0 +; bitbank2/PNGdec@^1.0.1 +; adafruit/Adafruit GFX Library@^1.11.0 +; https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip \ No newline at end of file diff --git a/variants/lilygo_t5s3_epaper_pro/target.cpp b/variants/lilygo_t5s3_epaper_pro/target.cpp new file mode 100644 index 0000000..919ce7b --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/target.cpp @@ -0,0 +1,95 @@ +#include +#include "variant.h" +#include "target.h" + +T5S3Board board; + +// LoRa radio on separate SPI bus +// T5S3 V2 SPI pins: SCLK=14, MISO=21, MOSI=13 (shared with SD card) +#if defined(P_LORA_SCLK) + static SPIClass loraSpi(HSPI); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, loraSpi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +// No GPS on H752-B +#if HAS_GPS + GPSStreamCounter gpsStream(Serial2); + MicroNMEALocationProvider gps(gpsStream, &rtc_clock); + EnvironmentSensorManager sensors(gps); +#else + SensorManager sensors; +#endif + +// Phase 2: Display +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + MESH_DEBUG_PRINTLN("radio_init() - starting"); + + // NOTE: board.begin() is called by main.cpp setup() before radio_init() + // I2C is already initialized there with correct pins + + fallback_clock.begin(); + MESH_DEBUG_PRINTLN("radio_init() - fallback_clock started"); + + // Use existing Wire for RTC discovery + // AutoDiscoverRTCClock will find PCF85063 at 0x51 if present + rtc_clock.begin(Wire); + MESH_DEBUG_PRINTLN("radio_init() - rtc_clock started"); + +#if defined(P_LORA_SCLK) + MESH_DEBUG_PRINTLN("radio_init() - initializing LoRa SPI (SCLK=%d, MISO=%d, MOSI=%d, NSS=%d)...", + P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); + loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); + MESH_DEBUG_PRINTLN("radio_init() - SPI initialized, calling radio.std_init()..."); + bool result = radio.std_init(&loraSpi); + if (result) { + radio.setPreambleLength(32); + MESH_DEBUG_PRINTLN("radio_init() - preamble set to 32 symbols"); + } + MESH_DEBUG_PRINTLN("radio_init() - radio.std_init() returned: %s", result ? "SUCCESS" : "FAILED"); + return result; +#else + MESH_DEBUG_PRINTLN("radio_init() - calling radio.std_init() without custom SPI..."); + bool result = radio.std_init(); + if (result) { + radio.setPreambleLength(32); + MESH_DEBUG_PRINTLN("radio_init() - preamble set to 32 symbols"); + } + return result; +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} + +void radio_reset_agc() { + radio.setRxBoostedGainMode(true); +} diff --git a/variants/lilygo_t5s3_epaper_pro/target.h b/variants/lilygo_t5s3_epaper_pro/target.h new file mode 100644 index 0000000..395014f --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/target.h @@ -0,0 +1,50 @@ +#pragma once + +// Include variant.h first to ensure all board-specific defines are available +#include "variant.h" + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include + +// Display support — FastEPDDisplay for parallel e-ink (not GxEPD2) +#ifdef DISPLAY_CLASS + #include + #include +#endif + +// No GPS on H752-B (non-GPS variant) +// If porting to H752-01/H752-02 with GPS, enable this: +#if HAS_GPS + #include "helpers/sensors/EnvironmentSensorManager.h" + #include "helpers/sensors/MicroNMEALocationProvider.h" + #include "GPSStreamCounter.h" +#else + #include +#endif + +extern T5S3Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +#if HAS_GPS + extern GPSStreamCounter gpsStream; + extern EnvironmentSensorManager sensors; +#else + extern SensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); +void radio_reset_agc(); diff --git a/variants/lilygo_t5s3_epaper_pro/variant.h b/variants/lilygo_t5s3_epaper_pro/variant.h new file mode 100644 index 0000000..24fc110 --- /dev/null +++ b/variants/lilygo_t5s3_epaper_pro/variant.h @@ -0,0 +1,188 @@ +#pragma once + +// ============================================================================= +// LilyGo T5 S3 E-Paper Pro V2 (H752-01/H752-B) - Pin Definitions for Meck +// +// 4.7" parallel e-ink (ED047TC1, 960x540, 16-grey) — NO SPI display +// GT911 capacitive touch (no physical keyboard) +// SX1262 LoRa, BQ27220+BQ25896 battery, PCF85063 RTC, PCA9535 IO expander +// ============================================================================= + +// Board identifier +#define LilyGo_T5S3_EPaper_Pro 1 +#define T5_S3_EPAPER_PRO_V2 1 + +// ----------------------------------------------------------------------------- +// I2C Bus — shared by GT911, PCF85063, PCA9535, BQ25896, BQ27220, TPS65185 +// ----------------------------------------------------------------------------- +#define I2C_SDA 39 +#define I2C_SCL 40 + +// Aliases for ESP32Board base class compatibility +#define PIN_BOARD_SDA I2C_SDA +#define PIN_BOARD_SCL I2C_SCL + +// I2C Device Addresses +#define I2C_ADDR_GT911 0x5D // Touch controller +#define I2C_ADDR_PCF85063 0x51 // RTC +#define I2C_ADDR_PCA9535 0x20 // IO expander (e-ink power control) +#define I2C_ADDR_BQ27220 0x55 // Fuel gauge +#define I2C_ADDR_BQ25896 0x6B // Battery charger +#define I2C_ADDR_TPS65185 0x68 // E-ink power driver + +// ----------------------------------------------------------------------------- +// SPI Bus — shared by LoRa and SD card +// Different from T-Deck Pro! (T-Deck: 33/47/36, T5S3: 13/21/14) +// ----------------------------------------------------------------------------- +#define BOARD_SPI_SCLK 14 +#define BOARD_SPI_MISO 21 +#define BOARD_SPI_MOSI 13 + +// ----------------------------------------------------------------------------- +// LoRa Radio (SX1262) +// SPI bus shared with SD card, different chip selects +// ----------------------------------------------------------------------------- +#define P_LORA_NSS 46 +#define P_LORA_DIO_1 10 // IRQ +#define P_LORA_RESET 1 +#define P_LORA_BUSY 47 +#define P_LORA_SCLK BOARD_SPI_SCLK +#define P_LORA_MISO BOARD_SPI_MISO +#define P_LORA_MOSI BOARD_SPI_MOSI +// Note: No P_LORA_EN on T5S3 — LoRa is always powered + +// ----------------------------------------------------------------------------- +// E-Ink Display (ED047TC1 — 8-bit parallel, NOT SPI) +// Driven by epdiy/FastEPD library via TPS65185 + PCA9535 +// GxEPD2 is NOT used on this board. +// ----------------------------------------------------------------------------- +// Parallel data bus (directly wired to ESP32-S3 GPIOs) +#define EP_D0 5 +#define EP_D1 6 +#define EP_D2 7 +#define EP_D3 15 +#define EP_D4 16 +#define EP_D5 17 +#define EP_D6 18 +#define EP_D7 8 + +// Control signals +#define EP_CKV 48 // Clock vertical +#define EP_STH 41 // Start horizontal +#define EP_LEH 42 // Latch enable horizontal +#define EP_STV 45 // Start vertical +#define EP_CKH 4 // Clock horizontal (edge) + +// E-ink power is managed by TPS65185 through PCA9535 IO expander: +// PCA9535 IO10 -> EP_OE (output enable, source driver) +// PCA9535 IO11 -> EP_MODE (output mode, gate driver) +// PCA9535 IO13 -> TPS_PWRUP +// PCA9535 IO14 -> VCOM_CTRL +// PCA9535 IO15 -> TPS_WAKEUP +// PCA9535 IO16 -> TPS_PWR_GOOD (input) +// PCA9535 IO17 -> TPS_INT (input) + +// Display dimensions — native resolution of ED047TC1 +#define EPD_WIDTH 960 +#define EPD_HEIGHT 540 + +// Backlight (warm-tone front-light — functional on V2!) +#define BOARD_BL_EN 11 + +// We do NOT define DISPLAY_CLASS or EINK_DISPLAY_MODEL here. +// The parallel display uses FastEPD, not GxEPD2. +// DISPLAY_CLASS will be defined in platformio.ini as FastEPDDisplay +// for builds that include display support. + +// ----------------------------------------------------------------------------- +// Touch Controller (GT911) +// No physical keyboard on this board — touch-only input +// ----------------------------------------------------------------------------- +#define HAS_TOUCHSCREEN 1 +#define GT911_PIN_INT 3 +#define GT911_PIN_RST 9 +#define GT911_PIN_SDA I2C_SDA +#define GT911_PIN_SCL I2C_SCL + +// No keyboard +// #define HAS_PHYSICAL_KEYBOARD 0 + +// Compatibility: main.cpp references CST328 touch (T-Deck Pro). +// Map to GT911 equivalents so shared code compiles. +// The actual touch init for T5S3 will use GT911 in Phase 2. +#define CST328_PIN_INT GT911_PIN_INT +#define CST328_PIN_RST GT911_PIN_RST + +// ----------------------------------------------------------------------------- +// SD Card — shares SPI bus with LoRa +// ----------------------------------------------------------------------------- +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS 12 +#define SPI_CS SDCARD_CS + +// ----------------------------------------------------------------------------- +// GPS — Not present on H752-B (non-GPS variant) +// If a GPS model is used (H752-01/H752-02), define HAS_GPS=1 +// and uncomment the GPS pins below. +// ----------------------------------------------------------------------------- +// #define HAS_GPS 1 +// #define GPS_BAUDRATE 38400 +// #define GPS_RX_PIN 44 +// #define GPS_TX_PIN 43 + +// Fallback for code that references GPS_BAUDRATE without HAS_GPS guard +// (e.g. MyMesh.cpp CLI rescue command) +#ifndef GPS_BAUDRATE +#define GPS_BAUDRATE 9600 +#endif + +// ----------------------------------------------------------------------------- +// RTC — PCF85063 (proper hardware RTC, battery-backed!) +// This is a significant upgrade over T-Deck Pro which has no RTC. +// ----------------------------------------------------------------------------- +#define HAS_PCF85063_RTC 1 +#define PCF85063_I2C_ADDR 0x51 +#define PCF85063_INT_PIN 2 + +// ----------------------------------------------------------------------------- +// PCA9535 IO Expander +// Controls e-ink power sequencing and has a user button +// ----------------------------------------------------------------------------- +#define HAS_PCA9535 1 +#define PCA9535_I2C_ADDR 0x20 +#define PCA9535_INT_PIN 38 + +// PCA9535 pin assignments (directly from LilyGo schematic): +// Port 0 (IO0x): IO00-IO07 — mostly unused/reserved +// Port 1 (IO1x): +#define PCA9535_EP_OE 0 // IO10 — EP output enable (source driver) +#define PCA9535_EP_MODE 1 // IO11 — EP mode (gate driver) +#define PCA9535_BUTTON 2 // IO12 — User button via IO expander +#define PCA9535_TPS_PWRUP 3 // IO13 — TPS65185 power up +#define PCA9535_VCOM_CTRL 4 // IO14 — VCOM control +#define PCA9535_TPS_WAKEUP 5 // IO15 — TPS65185 wakeup +#define PCA9535_TPS_PWRGOOD 6 // IO16 — TPS65185 power good (input) +#define PCA9535_TPS_INT 7 // IO17 — TPS65185 interrupt (input) + +// ----------------------------------------------------------------------------- +// Buttons & Controls +// ----------------------------------------------------------------------------- +#define BUTTON_PIN 0 // Boot button (GPIO0) +#define PIN_USER_BTN 0 + +// ----------------------------------------------------------------------------- +// Power Management +// ----------------------------------------------------------------------------- +#define HAS_BQ27220 1 +#define BQ27220_I2C_ADDR 0x55 + +// T5S3 E-Paper Pro battery (1500 mAh — larger than T-Deck Pro's 1400 mAh) +#ifndef BQ27220_DESIGN_CAPACITY_MAH +#define BQ27220_DESIGN_CAPACITY_MAH 1500 +#endif + +#define AUTO_SHUTDOWN_MILLIVOLTS 2800 + +// No explicit peripheral power pin on T5S3 (unlike T-Deck Pro's PIN_PERF_POWERON) +// Peripherals are always powered when the board is on. \ No newline at end of file