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
+286
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();
}
+120
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; }
};
+9 -1
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>
@@ -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; }
};
};
#endif // !LilyGo_T5S3_EPaper_Pro