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

This commit is contained in:
pelgraine
2026-03-11 20:56:27 +11:00
parent 9b15458927
commit 479673e90f
17 changed files with 1507 additions and 15 deletions

View File

@@ -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"
}

View File

@@ -4,6 +4,10 @@
#include <Mesh.h>
#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)

View File

@@ -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

View File

@@ -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();

View File

@@ -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 <WiFi.h>
@@ -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()) {

View File

@@ -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

View File

@@ -0,0 +1,286 @@
#include "FastEPDDisplay.h"
#include "FastEPD.h"
#include <string.h>
// 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<int>(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<Color>(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<int>(x);
_frameCRC.update<int>(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<char>(str, strlen(str));
_canvas->print(str);
}
void FastEPDDisplay::fillRect(int x, int y, int w, int h) {
if (!_canvas) return;
_frameCRC.update<int>(x);
_frameCRC.update<int>(y);
_frameCRC.update<int>(w);
_frameCRC.update<int>(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<int>(x);
_frameCRC.update<int>(y);
_frameCRC.update<int>(w);
_frameCRC.update<int>(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<int>(x);
_frameCRC.update<int>(y);
_frameCRC.update<int>(w);
_frameCRC.update<int>(h);
_frameCRC.update<uint8_t>(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();
}

View File

@@ -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 <Adafruit_GFX.h>
#include "variant.h" // EPD_WIDTH, EPD_HEIGHT (only compiled for T5S3 builds)
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <Fonts/FreeSans24pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>
#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<typename T> 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<typename T> 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; }
};

View File

@@ -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 <SPI.h>
#include <Wire.h>
@@ -105,3 +111,5 @@ public:
// (needed because drawPixelRaw bypasses CRC tracking)
void invalidateFrameCRC() { last_display_crc_value = 0; }
};
#endif // !LilyGo_T5S3_EPaper_Pro

View File

@@ -0,0 +1,70 @@
#pragma once
#include <Arduino.h>
// 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

View File

@@ -0,0 +1,305 @@
#include <Arduino.h>
#include "variant.h"
#include "T5S3Board.h"
#include <Mesh.h> // 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
}

View File

@@ -0,0 +1,97 @@
#pragma once
#include "variant.h"
#include <Wire.h>
#include <Arduino.h>
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
// 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";
}
};

View File

@@ -0,0 +1,19 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#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 */

View File

@@ -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}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/FastEPDDisplay.cpp>
+<../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}
; +<helpers/esp32/*.cpp>
; +<helpers/ui/MomentaryButton.cpp>
; +<helpers/ui/FastEPDDisplay.cpp>
; +<../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}
; +<helpers/esp32/*.cpp>
; +<helpers/ui/MomentaryButton.cpp>
; +<helpers/ui/FastEPDDisplay.cpp>
; +<../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

View File

@@ -0,0 +1,95 @@
#include <Arduino.h>
#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);
}

View File

@@ -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 <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <T5S3Board.h>
#include <helpers/AutoDiscoverRTCClock.h>
// Display support — FastEPDDisplay for parallel e-ink (not GxEPD2)
#ifdef DISPLAY_CLASS
#include <helpers/ui/FastEPDDisplay.h>
#include <helpers/ui/MomentaryButton.h>
#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 <helpers/SensorManager.h>
#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();

View File

@@ -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.