Keep T-Deck radio2 scanning active during screen redraws

Render into a PSRAM canvas and release the shared SPI bus between bounded TFT strips. Service only the radio driver at cooperative checkpoints and use the buffered radio HAL, preserving packet and transmit guards.

Cover pixel parity, transaction boundaries, allocation failure, partial updates and sleep/wake behavior with native tests.
This commit is contained in:
mikecarper committed 2026-10-18 23:18:55 -07:00
1 parent 4ad1aec200
commit a944b1779d
6 files changed
+607 -14

No files matched your search

+1
View File
@@ -969,6 +969,7 @@ jobs:
python3 -B test/test_wireless_tracker_display_detection.py -v
python3 -B test/test_heltec_r8_display_detection.py -v
python3 -B test/test_heltec_r8_display_rotation.py -v
python3 -B test/test_tdeck_display_cooperation.py -v
python3 -B test/test_muzi_accessory_detection.py -v
python3 -B test/test_techo_lite_display_detection.py -v
python3 -B test/test_companion_headless_buttons.py -v
+262 -10
View File
@@ -1,6 +1,154 @@
#include "ST7789LCDDisplay.h"
#include "ColorTheme.h"
#ifdef LILYGO_TDECK
#include <esp_heap_caps.h>
#include <new>
#include <string.h>
#include <helpers/UsbLogging.h>
// Adafruit_GFX supplies text and bitmap rasterization without allocating its
// own framebuffer. The 153,600-byte RGB565 frame must live in external RAM.
class TDeckCanvas : public Adafruit_GFX {
ST7789LCDDisplay& _owner;
uint16_t* _pixels;
uint16_t _pixel_work = 0;
void cooperate(uint16_t pixels) {
_pixel_work += pixels;
if (_pixel_work >= 128) {
_pixel_work = 0;
_owner.serviceRender();
}
}
public:
TDeckCanvas(ST7789LCDDisplay& owner, int w, int h)
: Adafruit_GFX(w, h), _owner(owner),
_pixels(static_cast<uint16_t*>(heap_caps_calloc(
size_t(w) * h, sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT))) {
setTextWrap(false);
cp437(true);
}
~TDeckCanvas() { heap_caps_free(_pixels); }
uint16_t* pixels() const { return _pixels; }
void drawPixel(int16_t x, int16_t y, uint16_t color) override {
if (_pixels && x >= 0 && y >= 0 && x < WIDTH && y < HEIGHT)
_pixels[size_t(y) * WIDTH + x] = color;
cooperate(1);
}
void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) override {
if (!_pixels || y < 0 || y >= HEIGHT || w <= 0) return;
const int left = x < 0 ? 0 : x;
const int right = int(x) + w > WIDTH ? WIDTH : int(x) + w;
if (left >= right) return;
uint16_t* row = _pixels + size_t(y) * WIDTH;
for (int xx = left; xx < right; ++xx) row[xx] = color;
cooperate(right - left);
}
void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) override {
if (!_pixels || x < 0 || x >= WIDTH || h <= 0) return;
const int top = y < 0 ? 0 : y;
const int bottom = int(y) + h > HEIGHT ? HEIGHT : int(y) + h;
for (int yy = top; yy < bottom; ++yy) {
_pixels[size_t(yy) * WIDTH + x] = color;
cooperate(1);
}
}
void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override {
if (w <= 0 || h <= 0) return;
const int top = y < 0 ? 0 : y;
const int bottom = int(y) + h > HEIGHT ? HEIGHT : int(y) + h;
for (int yy = top; yy < bottom; ++yy) drawFastHLine(x, yy, w, color);
}
void fillScreen(uint16_t color) override { fillRect(0, 0, WIDTH, HEIGHT, color); }
void writeFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) override {
drawFastHLine(x, y, w, color);
}
void writeFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) override {
drawFastVLine(x, y, h, color);
}
void writeFillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override {
fillRect(x, y, w, h, color);
}
};
ST7789LCDDisplay::~ST7789LCDDisplay() { delete _canvas; }
bool ST7789LCDDisplay::prepareCanvas() {
if (_canvas_attempted) return _canvas != nullptr;
_canvas_attempted = true;
_canvas = new (std::nothrow) TDeckCanvas(*this, width(), height());
if (!_canvas || !_canvas->pixels()) {
delete _canvas;
_canvas = nullptr;
mesh::usbConsolePort().println("WARN: T-Deck display PSRAM allocation failed; using direct redraw");
return false;
}
return true;
}
bool ST7789LCDDisplay::isBufferedRendering() const {
return _buffered_rendering && _canvas != nullptr;
}
void ST7789LCDDisplay::setBufferedRendering(bool enabled) {
_frame_active = false;
_buffered_rendering = enabled;
if (enabled && _panel_ready) prepareCanvas();
}
void ST7789LCDDisplay::serviceRender(bool force) {
if (!_buffered_rendering || !_render_yield || _render_callback_active) return;
if (!force && uint32_t(micros() - _last_render_yield_us) < 250) return;
_render_callback_active = true;
_render_yield();
_render_callback_active = false;
_last_render_yield_us = micros();
}
void ST7789LCDDisplay::flushCanvas(int x, int y, int w, int h) {
if (!isBufferedRendering() || !_isOn || w <= 0 || h <= 0) return;
const int right = int32_t(x) + w > width() ? width() : int32_t(x) + w;
const int bottom = int32_t(y) + h > height() ? height() : int32_t(y) + h;
x = x < 0 ? 0 : x;
y = y < 0 ? 0 : y;
w = right - x;
if (w <= 0 || y >= bottom) return;
// At most 960 pixels (0.384 ms at 40 MHz), plus command/driver overhead,
// per transaction. Copy out of PSRAM so SPI never depends on DMA from it.
constexpr size_t StripPixels = 960;
uint16_t strip[StripPixels];
const int rows_per_strip = StripPixels / w;
serviceRender(true);
while (y < bottom) {
const int rows = bottom - y < rows_per_strip ? bottom - y : rows_per_strip;
for (int row = 0; row < rows; ++row) {
memcpy(strip + size_t(row) * w,
_canvas->pixels() + size_t(y + row) * width() + x,
size_t(w) * sizeof(uint16_t));
}
display.startWrite();
display.setAddrWindow(x, y, w, rows);
display.writePixels(strip, size_t(w) * rows, true);
display.endWrite();
// CS is high and the transfer is complete before the radio can use the
// shared physical clock/data pins. No recursive application-loop call.
serviceRender(true);
y += rows;
}
}
void ST7789LCDDisplay::finishCanvasPrimitive(int x, int y, int w, int h) {
serviceRender();
// Dashboard rows can be updated without startFrame/endFrame. Preserve their
// immediate behavior while sending only their changed rectangle in strips.
if (!_frame_active) flushCanvas(x, y, w, h);
}
#endif
#ifndef PIN_TFT_MISO
#define PIN_TFT_MISO -1
#endif
@@ -93,6 +241,16 @@ bool ST7789LCDDisplay::begin() {
if (!_probe_complete) mesh::ui::heltec_r8::backlightOff();
#endif
if (!_isOn) {
#ifdef LILYGO_TDECK
// This board has no TFT reset pin or separately switched panel supply.
// Blanking its backlight does not require a slow controller reinit on wake.
if (_panel_ready) {
if (_peripher_power) _peripher_power->claim();
if (PIN_TFT_LEDA_CTL != -1) digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
_isOn = true;
return true;
}
#endif
#ifdef HELTEC_V4_R8_TFT
// turnOff() leaves this panel configured and powered - its reset line is
// shared with the touch controller, so it is never parked low - which makes
@@ -186,6 +344,9 @@ bool ST7789LCDDisplay::begin() {
_panel_ready = true;
_isOn = true;
#ifdef LILYGO_TDECK
if (_buffered_rendering) prepareCanvas();
#endif
}
return true;
@@ -224,15 +385,35 @@ void ST7789LCDDisplay::turnOff() {
}
void ST7789LCDDisplay::clear() {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
if (!_isOn) return;
_canvas->fillScreen(ST77XX_BLACK);
finishCanvasPrimitive(0, 0, width(), height());
return;
}
#endif
display.fillScreen(ST77XX_BLACK);
}
void ST7789LCDDisplay::startFrame(ColorVal bkg) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
if (!_isOn) return;
_frame_active = true;
serviceRender(true);
_canvas->fillScreen(bkg);
_canvas->setTextColor(_color = UIColor::primary_txt);
_canvas->setTextSize(1);
_canvas->cp437(true);
return;
}
#endif
display.fillScreen(bkg);
display.setTextColor(_color = UIColor::primary_txt);
@@ -241,58 +422,119 @@ void ST7789LCDDisplay::startFrame(ColorVal bkg) {
}
void ST7789LCDDisplay::setTextSize(int sz) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
_canvas->setTextSize(sz > 0 ? sz : 1);
return;
}
#endif
display.setTextSize(sz > 0 ? sz : 1);
}
void ST7789LCDDisplay::setColor(ColorVal c) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
_canvas->setTextColor(_color = c);
return;
}
#endif
display.setTextColor(_color = c);
}
void ST7789LCDDisplay::setCursor(int x, int y) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
_canvas->setCursor(x, y);
return;
}
#endif
display.setCursor(x, y);
}
void ST7789LCDDisplay::print(const char* str) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
if (!_isOn) return;
int16_t x, y;
uint16_t w, h;
_canvas->getTextBounds(str, _canvas->getCursorX(), _canvas->getCursorY(), &x, &y, &w, &h);
_canvas->print(str);
finishCanvasPrimitive(x, y, w, h);
return;
}
#endif
display.print(str);
}
void ST7789LCDDisplay::fillRect(int x, int y, int w, int h) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
if (!_isOn) return;
_canvas->fillRect(x, y, w, h, _color);
finishCanvasPrimitive(x, y, w, h);
return;
}
#endif
display.fillRect(x, y, w, h, _color);
}
void ST7789LCDDisplay::drawRect(int x, int y, int w, int h) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
if (!_isOn) return;
_canvas->drawRect(x, y, w, h, _color);
finishCanvasPrimitive(x, y, w, h);
return;
}
#endif
display.drawRect(x, y, w, h, _color);
}
void ST7789LCDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
if (!_isOn) return;
_canvas->drawBitmap(x, y, bits, w, h, _color);
finishCanvasPrimitive(x, y, w, h);
return;
}
#endif
// The common UI bitmaps are MSB-first, as expected by drawBitmap.
display.drawBitmap(x, y, bits, w, h, _color);
}
uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
#if defined(LILYGO_TDECK) || defined(MESHCORE_HELTEC_R8_TFT_DETECTION)
if (!_isOn) return 0;
#endif
#ifdef LILYGO_TDECK
if (isBufferedRendering()) {
int16_t x, y;
uint16_t w, h;
_canvas->getTextBounds(str, 0, 0, &x, &y, &w, &h);
return w;
}
#endif
int16_t x1, y1;
uint16_t w, h;
@@ -301,5 +543,15 @@ uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
}
void ST7789LCDDisplay::endFrame() {
#ifdef LILYGO_TDECK
if (!_isOn) {
_frame_active = false;
return;
}
if (isBufferedRendering()) {
_frame_active = false;
flushCanvas(0, 0, width(), height());
}
#endif
// display.display();
}
+31
View File
@@ -7,6 +7,10 @@
#include <Adafruit_ST7789.h>
#include <helpers/RefCountedDigitalPin.h>
#ifdef LILYGO_TDECK
class TDeckCanvas;
#endif
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
@@ -45,6 +49,22 @@ class ST7789LCDDisplay : public DisplayDriver {
static constexpr int kWidth = (DISPLAY_ROTATION & 1) ? 320 : 240;
static constexpr int kHeight = (DISPLAY_ROTATION & 1) ? 240 : 320;
#ifdef LILYGO_TDECK
friend class TDeckCanvas;
TDeckCanvas* _canvas = nullptr;
bool _buffered_rendering = true;
bool _canvas_attempted = false;
bool _frame_active = false;
bool _render_callback_active = false;
void (*_render_yield)() = nullptr;
uint32_t _last_render_yield_us = 0;
bool prepareCanvas();
void serviceRender(bool force = false);
void flushCanvas(int x, int y, int w, int h);
void finishCanvasPrimitive(int x, int y, int w, int h);
#endif
uint8_t effectiveRotation() const;
void applyRotation();
bool i2c_probe(TwoWire& wire, uint8_t addr);
@@ -74,6 +94,17 @@ public:
#endif
bool begin();
#ifdef LILYGO_TDECK
~ST7789LCDDisplay();
// Call only between frames. Disabling buffering retains the allocation for
// same-image comparisons with the original direct-to-panel renderer.
void setBufferedRendering(bool enabled);
bool isBufferedRendering() const;
// The callback runs synchronously with TFT CS released and no SPI transaction.
// It may service the radio, but must not redraw or re-enter the application loop.
void setRenderYieldCallback(void (*callback)()) { _render_yield = callback; }
#endif
bool isOn() override { return _isOn; }
#ifdef MESHCORE_HELTEC_R8_RUNTIME_ROTATION
bool supportsRotation() const override { return true; }
+4 -1
View File
@@ -246,7 +246,10 @@ class HeltecR8DisplayDetectionTest(unittest.TestCase):
"Adafruit_GFX.h": "#pragma once\n",
"Adafruit_ST7789.h": PANEL, "main.cpp": MAIN}.items():
(directory / name).write_text(source)
binary = directory / "probe"
if "-DLILYGO_TDECK" in flags:
from test_tdeck_display_cooperation import extend_legacy_stubs
extend_legacy_stubs(directory)
binary = directory / ("probe.exe" if os.name == "nt" else "probe")
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else [])
compiled = subprocess.run([
+294
View File
@@ -0,0 +1,294 @@
"""Run the production T-Deck renderer against a recording SPI panel and PSRAM."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_serial_wifi_sessions import SANITIZER_FLAGS
ROOT = Path(__file__).resolve().parents[1]
ARDUINO = r'''
#pragma once
#include <cassert>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <string>
#include <vector>
#include <algorithm>
#define LOW 0
#define HIGH 1
#define OUTPUT 1
#define HSPI 1
#define PIN_TFT_CS 12
#define PIN_TFT_DC 11
#define PIN_TFT_SCL 40
#define PIN_TFT_SDA 41
#define PIN_TFT_RST -1
#define PIN_TFT_LEDA_CTL 42
inline uint32_t now_us=0;
inline unsigned initializations=0, direct_pixels=0, spi_begins=0;
inline bool transaction=false, cs_high=true;
inline int levels[64]={};
inline void pinMode(int,int) {}
inline void digitalWrite(int pin,int value) { assert(pin>=0 && pin<64); levels[pin]=value; }
inline void delay(unsigned ms) { now_us+=ms*1000; }
inline uint32_t micros() { now_us+=25; return now_us; }
'''
# This stand-in supplies GFX's primitive dispatch and cursor contract. The
# implementation under test is the real TDeckCanvas and LCD driver, including
# every framebuffer write, clipping decision, strip copy and callback boundary.
GFX = r'''
#pragma once
#include <Arduino.h>
#include <algorithm>
#include <cstdlib>
#include <vector>
class Adafruit_GFX {
protected:
const int16_t WIDTH, HEIGHT;
int16_t cursor_x=0,cursor_y=0;
uint8_t textsize=1;
uint16_t textcolor=0xffff;
public:
Adafruit_GFX(int w,int h):WIDTH(w),HEIGHT(h) {}
virtual ~Adafruit_GFX()=default;
virtual void drawPixel(int16_t,int16_t,uint16_t)=0;
virtual void drawFastHLine(int16_t x,int16_t y,int16_t w,uint16_t c) {
for(int i=0;i<w;++i) drawPixel(x+i,y,c);
}
virtual void drawFastVLine(int16_t x,int16_t y,int16_t h,uint16_t c) {
for(int i=0;i<h;++i) drawPixel(x,y+i,c);
}
virtual void fillRect(int16_t x,int16_t y,int16_t w,int16_t h,uint16_t c) {
for(int row=0;row<h;++row) drawFastHLine(x,y+row,w,c);
}
virtual void fillScreen(uint16_t c) { fillRect(0,0,WIDTH,HEIGHT,c); }
virtual void writeFastHLine(int16_t x,int16_t y,int16_t w,uint16_t c) {drawFastHLine(x,y,w,c);}
virtual void writeFastVLine(int16_t x,int16_t y,int16_t h,uint16_t c) {drawFastVLine(x,y,h,c);}
virtual void writeFillRect(int16_t x,int16_t y,int16_t w,int16_t h,uint16_t c) {fillRect(x,y,w,h,c);}
void drawRect(int16_t x,int16_t y,int16_t w,int16_t h,uint16_t c) {
if(w<=0 || h<=0)return;
drawFastHLine(x,y,w,c); drawFastHLine(x,y+h-1,w,c);
drawFastVLine(x,y,h,c); drawFastVLine(x+w-1,y,h,c);
}
void drawBitmap(int16_t x,int16_t y,const uint8_t* bits,int16_t w,int16_t h,uint16_t c) {
for(int row=0;row<h;++row)for(int col=0;col<w;++col)
if(bits[row*((w+7)/8)+col/8] & (0x80>>(col&7)))drawPixel(x+col,y+row,c);
}
void setTextWrap(bool) {}
void cp437(bool) {}
void setTextSize(uint8_t size) {textsize=size;}
void setTextColor(uint16_t c) {textcolor=c;}
void setCursor(int16_t x,int16_t y) {cursor_x=x;cursor_y=y;}
int16_t getCursorX() const {return cursor_x;}
int16_t getCursorY() const {return cursor_y;}
void getTextBounds(const char* s,int16_t x,int16_t y,int16_t* bx,int16_t* by,uint16_t* w,uint16_t* h) {
*bx=x;*by=y;*w=strlen(s)*6*textsize;*h=*w?8*textsize:0;
}
void print(const char* s) {
while(*s++) {writeFillRect(cursor_x,cursor_y,5*textsize,7*textsize,textcolor);cursor_x+=6*textsize;}
}
};
'''
PANEL = r'''
#pragma once
#include <Adafruit_GFX.h>
#include <SPI.h>
#define ST77XX_BLACK 0
#define ST77XX_WHITE 0xffff
inline std::vector<uint16_t> panel(320*240);
struct Window {int x,y,w,h;};
inline std::vector<Window> windows;
class Adafruit_ST7789:public Adafruit_GFX {
int _w=320,_h=240;
Window window{};
public:
template<class... T> explicit Adafruit_ST7789(T...):Adafruit_GFX(320,240) {}
void init(int,int) {++initializations;}
void setRotation(int r) {_w=(r&1)?320:240;_h=(r&1)?240:320;}
int width()const{return _w;} int height()const{return _h;}
void setSPISpeed(double) {}
void drawPixel(int16_t x,int16_t y,uint16_t c) override {
++direct_pixels;
if(x>=0 && y>=0 && x<_w && y<_h)panel[size_t(y)*_w+x]=c;
}
void startWrite() {assert(!transaction && cs_high);transaction=true;cs_high=false;}
void setAddrWindow(int x,int y,int w,int h) {
assert(transaction && !cs_high && x>=0 && y>=0 && w>0 && h>0 && x+w<=_w && y+h<=_h);
window={x,y,w,h};windows.push_back(window);
}
void writePixels(uint16_t* pixels,uint32_t count,bool blocking) {
assert(transaction && !cs_high && blocking && count==unsigned(window.w*window.h) && count<=960);
for(int row=0;row<window.h;++row)
std::copy_n(pixels+row*window.w,window.w,panel.begin()+(window.y+row)*_w+window.x);
}
void endWrite() {assert(transaction && !cs_high);transaction=false;cs_high=true;}
};
'''
HEAP = r'''
#pragma once
#include <Arduino.h>
#define MALLOC_CAP_SPIRAM 1
#define MALLOC_CAP_8BIT 2
inline bool fail_psram=false;
inline unsigned allocation_calls=0, frees=0;
inline size_t allocated_bytes=0;
inline uint8_t* allocation=nullptr;
inline void* heap_caps_calloc(size_t count,size_t size,unsigned caps) {
++allocation_calls;assert(caps==(MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT));
if(fail_psram)return nullptr;
assert(!allocation);allocated_bytes=count*size;
allocation=static_cast<uint8_t*>(malloc(allocated_bytes+64));
memset(allocation,0xA5,allocated_bytes+64);memset(allocation+32,0,allocated_bytes);
return allocation+32;
}
inline void heap_caps_free(void* pointer) {
if(!pointer)return;
assert(pointer==allocation+32);
for(unsigned i=0;i<32;++i)assert(allocation[i]==0xA5 && allocation[allocated_bytes+32+i]==0xA5);
free(allocation);allocation=nullptr;++frees;
}
'''
USB = r'''
#pragma once
#include <Arduino.h>
inline std::vector<std::string> warnings;
namespace mesh {
struct Console {void println(const char* text){warnings.emplace_back(text);}};
inline Console console;
inline Console& usbConsolePort(){return console;}
}
'''
def write_stubs(directory):
"""Also used by the R8 guards to compile the newly complete T-Deck branch."""
(directory / "helpers").mkdir(exist_ok=True)
for name, source in {
"Arduino.h": ARDUINO, "Adafruit_GFX.h": GFX,
"Adafruit_ST7789.h": PANEL, "esp_heap_caps.h": HEAP,
"helpers/UsbLogging.h": USB,
"Wire.h": "#pragma once\nclass TwoWire {};\n",
"SPI.h": "#pragma once\n#include <Arduino.h>\nstruct SPIClass {explicit SPIClass(int=0) {} void begin(int,int,int,int){++spi_begins;}};\ninline SPIClass SPI;\n",
}.items():
(directory / name).write_text(source)
def extend_legacy_stubs(directory):
"""Keep legacy R8 guard cases compiling the actual T-Deck code as well."""
(directory / "helpers").mkdir(exist_ok=True)
for name, source in {"Adafruit_GFX.h": GFX, "esp_heap_caps.h": HEAP,
"helpers/UsbLogging.h": USB}.items():
(directory / name).write_text(source)
with (directory / "Arduino.h").open("a") as stream:
stream.write("\ninline uint32_t micros() {now_us+=25;return now_us;}\n")
panel_path = directory / "Adafruit_ST7789.h"
panel = panel_path.read_text()
panel = panel.replace("public:\n", "public:\n" + r'''
void startWrite() {assert(initialized);}
void endWrite() {assert(initialized);}
void setAddrWindow(int x,int y,int w,int h) {
assert(initialized && x>=0 && y>=0 && w>0 && h>0 && x+w<=_w && y+h<=_h);
}
void writePixels(uint16_t*,uint32_t count,bool blocking) {
assert(initialized && blocking && count<=960);++draws;
}
''', 1)
panel_path.write_text(panel)
MAIN = r'''
#include <helpers/ui/ST7789LCDDisplay.h>
#include <esp_heap_caps.h>
#include <helpers/UsbLogging.h>
inline unsigned callbacks=0, depth=0, max_depth=0;
inline ST7789LCDDisplay* current=nullptr;
inline bool reenter=false;
void cooperate() {
assert(!transaction && cs_high && "radio callback must run with completed TFT transaction");
++callbacks;++depth;max_depth=std::max(max_depth,depth);
if(reenter){reenter=false;current->fillRect(1,1,2,2);}
--depth;
}
void scene(ST7789LCDDisplay& d) {
const uint8_t bits[]={0x81,0x42};
d.startFrame(0x1234);d.setColor(0xCAFE);
d.fillRect(-3,-4,8,9);d.fillRect(318,237,20,20);
d.drawRect(20,20,9,7);d.drawRect(-2,60,6,7);
d.drawXbm(35,35,bits,8,2);d.setTextSize(2);d.setCursor(60,60);d.print("AB");d.endFrame();
}
int main(int argc,char** argv) {
assert(argc==2);const std::string scenario=argv[1];
fail_psram=scenario=="allocation-failure";
{
ST7789LCDDisplay d;current=&d;d.setRenderYieldCallback(cooperate);
assert(!d.isBufferedRendering());assert(d.begin());
assert(allocation_calls==1 && d.isBufferedRendering()==!fail_psram);
if(fail_psram) {
assert(warnings.size()==1 && warnings[0].find("PSRAM allocation failed")!=std::string::npos);
scene(d);assert(direct_pixels>0 && callbacks==0 && windows.empty());
d.setBufferedRendering(false);d.setBufferedRendering(true);d.turnOff();d.turnOn();
assert(allocation_calls==1 && warnings.size()==1 && initializations==1);
} else {
assert(allocated_bytes==153600 && warnings.empty());
windows.clear();const auto direct_before=direct_pixels;
d.startFrame(0x1234);d.setColor(0xCAFE);d.fillRect(-3,-4,8,9);
assert(windows.empty() && direct_pixels==direct_before && callbacks>0);
d.endFrame();assert(windows.size()==80);
for(const auto& w:windows)assert(w.x==0 && w.w==320 && w.h==3);
assert(panel[0]==0xCAFE && panel[4*320+4]==0xCAFE && panel[5*320+5]==0x1234);
windows.clear();d.fillRect(-2,238,7,9);
assert(windows.size()==1 && windows[0].x==0 && windows[0].y==238 && windows[0].w==5 && windows[0].h==2);
assert(panel[239*320+4]==0xCAFE && panel[239*320+5]==0x1234);
windows.clear();d.fillRect(900,900,10,10);d.fillRect(0,0,-2,5);assert(windows.empty());
scene(d);const auto buffered=panel;const auto buffered_callbacks=callbacks;
d.setBufferedRendering(false);assert(!d.isBufferedRendering());scene(d);
assert(panel==buffered && callbacks==buffered_callbacks && direct_pixels>direct_before);
d.setBufferedRendering(true);assert(d.isBufferedRendering() && allocation_calls==1);scene(d);
assert(panel==buffered && callbacks>buffered_callbacks);
reenter=true;d.startFrame(0x0010);d.endFrame();assert(max_depth==1 && !reenter);
// Unframed text must flush the actual cursor rectangle, not a full screen.
windows.clear();d.setTextSize(1);d.setCursor(20,30);d.print("A");
assert(windows.size()==1 && windows[0].x==20 && windows[0].y==30 && windows[0].w==6 && windows[0].h==8);
}
for(bool buffered:{true,false}) {
d.setBufferedRendering(buffered);d.turnOff();
const auto old_panel=panel;const auto draws=direct_pixels;const auto yields=callbacks;windows.clear();
scene(d);d.clear();assert(panel==old_panel && direct_pixels==draws && callbacks==yields && windows.empty());
assert(!d.isOn() && levels[PIN_TFT_LEDA_CTL]==LOW);
d.turnOn();assert(d.isOn() && initializations==1 && spi_begins==1 && levels[PIN_TFT_LEDA_CTL]==HIGH);
}
}
assert(allocation==nullptr && frees==unsigned(!fail_psram));
}
'''
class TDeckDisplayCooperationTests(unittest.TestCase):
def test_production_renderer_pixels_transactions_psram_fallback_and_sleep(self):
with tempfile.TemporaryDirectory(prefix="tdeck-display-") as temporary:
directory = Path(temporary)
write_stubs(directory)
(directory / "main.cpp").write_text(MAIN)
binary = directory / "driver"
result = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", *SANITIZER_FLAGS, "-DLILYGO_TDECK", "-I"+str(directory),
"-I"+str(ROOT / "src"), str(directory / "main.cpp"),
str(ROOT / "src/helpers/ui/ST7789LCDDisplay.cpp"), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout+result.stderr)
for scenario in ("buffered", "allocation-failure"):
with self.subTest(scenario=scenario):
result = subprocess.run([str(binary), scenario], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout+result.stderr)
if __name__ == "__main__":
unittest.main()
+15 -3
View File
@@ -1,11 +1,13 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/radiolib/ESP32BufferedRadioHal.h>
TDeckBoard board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
static ESP32BufferedRadioHal radio_hal(spi);
RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
@@ -28,10 +30,20 @@ bool radio_init() {
Wire.begin(18, 8);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
const bool ready = radio.std_init(&spi);
#else
return radio.std_init();
const bool ready = radio.std_init();
#endif
#ifdef DISPLAY_CLASS
if (ready) {
// The TFT releases its shared SPI transaction before yielding. Service
// only the radio here: recursively entering the mesh/UI loop is unsafe.
display.setRenderYieldCallback([]() {
if (radio_driver.profiles()->enabled()) radio_driver.loop();
});
}
#endif
return ready;
}
mesh::LocalIdentity radio_new_identity() {