Fix T096 Companion runtime RAM with RGB332 framebuffer

This commit is contained in:
mikecarper committed 2026-10-17 11:22:15 -07:00
1 parent eb4f837969
commit 4805b5cdbb
8 files changed
+438 -2

No files matched your search

+6
View File
@@ -34,6 +34,11 @@ jobs:
--firmware-version v1.17.1.10-halo-keymind-cascade-dev
--radio-preset usa-cascade-fixed --profile cascade --require-ota
- name: Compile both legacy T096 BLE FEM settings with their RAM gates
# Sequential environments in one process; do not replace these with
# Full overlays, which have different OTA and runtime allocations.
run: pio run -e Heltec_t096_companion_radio_ble_femoff -e Heltec_t096_companion_radio_ble_femon
nrf52-sensor-smoke:
runs-on: ubuntu-latest
steps:
@@ -750,6 +755,7 @@ jobs:
python3 -B test/test_local_release_full_selection.py
python3 -B test/test_local_release_capacity_archive.py
python3 -B test/test_t096_full_memory.py
python3 -B test/test_st7735_framebuffer.py -v
python3 -B test/test_nrf52_ble_startup.py
python3 -B test/test_nrf52_ble_backpressure.py
python3 -B test/test_nrf52_ble_rx_queue.py -v
+25 -1
View File
@@ -50,7 +50,25 @@ current startup policy; these figures describe historical linked checks.
Its ordinary release retains separate UART/ESP-NOW and MQTT/ESP-NOW Full
images; routing capacities remain unchanged.
A 160x80 ST7735
framebuffer needs 25,602 bytes. ESP32 SSD1306 uses a source-bounded 2 KiB
RGB565 framebuffer needs 25,602 bytes. T096 recipes instead select
`ST7735_BUFFER_COLOR_DEPTH=8`: the RGB332 canvas needs 12,801 bytes, plus
a source-bounded 1 KiB allowance for the complete sprite object and allocator
metadata. Each frame expands one 160-pixel scanline into a 320-byte stack buffer
and sends the same RGB565 wire format. The current primary UI colors remain
exact; resolution, fonts, all 32 message previews, 350 contacts, 40 channels and
the 256-frame offline queue are unchanged. Other ST7735 recipes retain the
16-bit default. A future sprite-object increase beyond the allowance fails a
C++ assertion rather than silently reducing available heap. The existing loop
stack and transient margin remain additive.
The ordinary T096 BLE FEM-on/off policy includes its receiving OTA source
scratch and full-length previews: the reported 81,474-byte requirement becomes
69,697 bytes with the smaller canvas and explicit sprite-object allowance.
The old 75,396-byte linked capacity therefore failed correctly; the check is
not bypassed or relaxed to accept it. Full Companion's independent 72 KiB
floor remains in place.
ESP32 SSD1306 uses a source-bounded 2 KiB
allowance for its fixed 1,024-byte framebuffer, driver and metadata; other
OLED and ordinary non-ESP32 allowances remain unchanged. nRF52 Full with that
color framebuffer must have at least 72 KiB available before startup allocations.
@@ -401,6 +419,7 @@ Run PlatformIO commands sequentially in this checkout:
```sh
python3 -B test/test_firmware_ram.py
python3 -B test/test_t096_full_memory.py
python3 -B test/test_st7735_framebuffer.py
python3 -B test/test_nrf52_ble_startup.py
python3 -B test/test_shared_mota_queue.py
python3 -B test/test_cascade_release_package.py
@@ -416,6 +435,11 @@ startup failures. The manual staging buffer also has allocation-failure and
repeated release tests. ESP32 tests run the actual WiFi mOTA listener and source
framing through complete transfers, idle polling, queue-full refusal, network
loss, CLI detach and USB/TCP ownership changes under address/leak sanitizers.
The ST7735 tests check all 256 RGB332 values, exact UI primary colors, full
160x80 scanout and byte order on both SPI APIs, the unchanged 16-bit path,
allocation failures, and retained-buffer wake/retry behavior. CI also compiles
both ordinary T096 BLE FEM variants with their real runtime-RAM gates, not only
the distinct Full Companion overlay.
For older releases without saved ELFs, an audit can compare their ESP allocator
tables against a matching pinned SDK ELF and read reservations from the
+11 -1
View File
@@ -27,7 +27,6 @@ from firmware_elf import FirmwareElf
SUPPORTED = {"NRF52_PLATFORM", "ESP32_PLATFORM", "RP2040_PLATFORM", "STM32_PLATFORM"}
DISPLAY_HEAP = {
"": 0, "NullDisplayDriver": 0,
"ST7735Display": 25602,
"SSD1306Display": 4096, "SH1106Display": 4096, "U8g2Display": 4096,
"SH1107Display": 4096,
# These drivers render directly or embed their pixel storage in globals.
@@ -144,6 +143,17 @@ def requirements(platform, defines, target):
if platform != "ESP32_PLATFORM":
raise ValueError("Wio Tracker L2 display budget requires ESP32")
display_heap = wio_tracker_l2_display_heap(defines)
elif display == "ST7735Display":
depth = integer(defines, "ST7735_BUFFER_COLOR_DEPTH", 16)
if depth not in (8, 16):
raise ValueError("ST7735_BUFFER_COLOR_DEPTH must be 8 or 16")
# TFT_eSprite adds one trailing pixel. The opt-in RGB332 path also
# source-bounds its complete sprite object plus allocator metadata to
# 1 KiB. Its 320-byte wire scanline lives on the unchanged loop stack;
# the existing transient allowance remains additive.
display_heap = (160 * 80 + 1) * (depth // 8)
if depth == 8:
display_heap += 1024
elif display in DISPLAY_HEAP:
display_heap = DISPLAY_HEAP[display]
else:
+36
View File
@@ -5,6 +5,21 @@
#endif
#include <TFT_eSPI.h>
// Most ST7735 recipes keep the original RGB565 canvas. RAM-constrained
// targets can retain the complete UI in RGB332 and expand only one scanline
// while sending it to the panel; the panel still receives RGB565 pixels.
#ifndef ST7735_BUFFER_COLOR_DEPTH
#define ST7735_BUFFER_COLOR_DEPTH 16
#endif
static_assert(ST7735_BUFFER_COLOR_DEPTH == 8 || ST7735_BUFFER_COLOR_DEPTH == 16,
"ST7735_BUFFER_COLOR_DEPTH must be 8 or 16");
#if ST7735_BUFFER_COLOR_DEPTH == 8
// Bind the separate sprite/allocator allowance to the actual dependency. The
// framebuffer itself is 160*80+1 bytes (including TFT_eSPI's sentinel pixel).
static_assert(sizeof(TFT_eSprite) + 2 * 16 <= 1024,
"Increase the ST7735 sprite-object runtime RAM allowance");
#endif
#if defined(NRF52_PLATFORM)
#include <nrf_gpio.h>
#endif
@@ -452,6 +467,7 @@ bool ST7735Display::begin() {
mesh::usbLoggingPort().println("ST7735Display: failed to alloc canvas");
return false;
}
sprite->setColorDepth(ST7735_BUFFER_COLOR_DEPTH);
if (!sprite->createSprite(160, 80)) {
mesh::usbLoggingPort().println(
"ST7735Display: failed to alloc canvas pixels");
@@ -669,6 +685,25 @@ void ST7735Display::endFrame() {
set_CS(LOW);
_spi->beginTransaction(_spiSettings);
uint16_t x, y;
#if ST7735_BUFFER_COLOR_DEPTH == 8
const uint8_t* pixels = static_cast<const uint8_t*>(sprite->getPointer());
// This bounded stack buffer replaces a second full-size canvas. Explicit
// wire bytes preserve the panel's high-byte-first order on every MCU.
uint8_t scanline[160 * 2];
for (y = 0; y < 80; y++, pixels += 160) {
for (x = 0; x < 160; ++x) {
const uint16_t color = lcd.color8to16(pixels[x]);
scanline[2 * x] = static_cast<uint8_t>(color >> 8);
scanline[2 * x + 1] = static_cast<uint8_t>(color);
}
setAddrWindow(0, y, 160, 1);
#ifdef ESP_PLATFORM
_spi->transferBytes(scanline, NULL, sizeof(scanline));
#else
_spi->transfer(scanline, NULL, sizeof(scanline));
#endif
}
#else
uint16_t* pixels = (uint16_t *) ((TFT_eSprite *) sprite)->getPointer();
for (y = 0; y < 80; y++, pixels += 160) {
setAddrWindow(0, y, 160, 1);
@@ -678,6 +713,7 @@ void ST7735Display::endFrame() {
_spi->transfer(pixels, NULL, 2 * 160);
#endif
}
#endif
_spi->endTransaction();
set_CS(HIGH);
+43
View File
@@ -121,6 +121,49 @@ class FirmwareRamTest(unittest.TestCase):
}, "muzi_base_uno_superIO_companion_radio_ble")
self.assertEqual(policy["components"]["display_pixels_and_driver"], 4096)
def test_t096_legacy_ble_rgb332_fits_without_reducing_other_reserves(self):
flags = {"DISPLAY_CLASS": "ST7735Display", "BLE_PIN_CODE": 123456,
"ENABLE_OTA": 1}
for suffix in ("femon", "femoff"):
target = "Heltec_t096_companion_radio_ble_" + suffix
with self.subTest(target=target):
original = ram.requirements("NRF52_PLATFORM", flags, target)
compact = ram.requirements("NRF52_PLATFORM", {
**flags, "ST7735_BUFFER_COLOR_DEPTH": 8,
}, target)
self.assertEqual(original["required_heap_bytes"], 81474)
self.assertEqual(compact["required_heap_bytes"], 69697)
self.assertEqual(compact["components"]["display_pixels_and_driver"], 12801 + 1024)
self.assertGreater(original["required_heap_bytes"], 75396)
self.assertLess(compact["required_heap_bytes"], 75396)
for name, value in original["components"].items():
if name != "display_pixels_and_driver":
self.assertEqual(compact["components"][name], value)
self.assertEqual(compact["components"]["expanded_message_previews"], 2816)
self.assertEqual(compact["components"]["ota_generic_source_buffers"], 6176)
self.assertEqual(compact["components"]["allocation_and_transient_margin"], 4096)
def test_st7735_color_depth_default_and_invalid_values_fail_closed(self):
flags = {"DISPLAY_CLASS": "ST7735Display"}
default = ram.requirements("NRF52_PLATFORM", flags, "other_companion")
explicit = ram.requirements("NRF52_PLATFORM", {
**flags, "ST7735_BUFFER_COLOR_DEPTH": 16,
}, "other_companion")
self.assertEqual(default, explicit)
self.assertEqual(default["components"]["display_pixels_and_driver"], 25602)
for depth in (0, 1, 4, 12, 24, 32):
with self.subTest(depth=depth), self.assertRaisesRegex(ValueError, "ST7735_BUFFER_COLOR_DEPTH"):
ram.requirements("NRF52_PLATFORM", {
**flags, "ST7735_BUFFER_COLOR_DEPTH": depth,
}, "other_companion")
def test_t096_canvas_configuration_does_not_reduce_companion_capacities(self):
config = (ROOT / "variants/heltec_t096/platformio.ini").read_text()
self.assertIn("-D ST7735_BUFFER_COLOR_DEPTH=8", config)
self.assertIn("-D MAX_CONTACTS=350", config)
self.assertIn("-D MAX_GROUP_CHANNELS=40", config)
self.assertIn("-D OFFLINE_QUEUE_SIZE=256", config)
def test_wio_l2_counts_internal_sprite_fallback_and_dma_beside_psram(self):
flags = {"DISPLAY_CLASS": "WioTrackerL2Display", "UI_ZOOM": 2}
for target in ("Wio_Tracker_L2_repeater", "Wio_Tracker_L2_companion_radio_ble"):
+20
View File
@@ -31,6 +31,7 @@ HARNESS = r'''
#include <helpers/BleMotaProtocol.h>
#include <helpers/nrf52/SecuritySessionTimer.h>
#include <helpers/nrf52/BleDebugTrace.h>
#include <helpers/nrf52/SleepCycleState.h>
#ifndef COMPANION_FEATURE_BLE_MOTA_SOURCE
#define COMPANION_FEATURE_BLE_MOTA_SOURCE 1
#endif
@@ -194,7 +195,17 @@ int main() {
// services are extensions: registration failure must leave UART BLE
// available for pairing, diagnostics, and recovery.
const bool expected = fault != 1 && fault != 2;
auto& sleep_state = mesh_nrf52::sleepCycleState();
const uint32_t before_start = sleep_state.begin();
assert(sleep_state.publish(before_start, false, 42));
mesh_nrf52::SleepCycleSnapshot snapshot;
assert(sleep_state.snapshot(snapshot));
assert(snapshot.generation == before_start);
assert(port.begin("MC-", "T096", 123456) == expected);
// Even a failed/partial SoftDevice start invalidates the pre-start view:
// the next sleep decision must query and publish fresh SDK state.
assert(!sleep_state.current(before_start));
assert(!sleep_state.snapshot(snapshot));
assert(Bluefruit.mtu == COMPANION_BLE_PRPH_MTU);
assert(Bluefruit.event_length == COMPANION_BLE_PRPH_EVENT_LENGTH);
assert(Bluefruit.hvn_queue == COMPANION_BLE_PRPH_HVN_QUEUE);
@@ -202,9 +213,18 @@ int main() {
assert(Bluefruit.starts == starts + 1);
assert((instance == &port) == expected);
// Retrying a failed partial init must never allocate another stack/FIFO.
// Likewise, an already-attempted begin does not invalidate a fresh sleep
// snapshot because it never attempts SoftDevice startup again.
for (int retry = 0; retry < 4; ++retry) {
const uint32_t before_retry = sleep_state.begin();
assert(sleep_state.publish(before_retry, expected, 43 + retry));
assert(port.begin("MC-", "T096", 123456) == expected);
assert(Bluefruit.starts == starts + 1);
assert(sleep_state.current(before_retry));
assert(sleep_state.snapshot(snapshot));
assert(snapshot.generation == before_retry);
assert(snapshot.softdevice_enabled == expected);
assert(snapshot.usb_status == unsigned(43 + retry));
}
}
}
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""Exercise real ST7735 allocation, RGB332 scanout, and retained wake paths.
The fixture records SPI wire bytes instead of driving a physical panel. It
compiles the production methods for both framebuffer depths and SPI APIs.
"""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
from test_serial_wifi_sessions import SANITIZER_FLAGS
ROOT = Path(__file__).resolve().parents[1]
DRIVER = ROOT / "src/helpers/ui/ST7735Display.cpp"
HARNESS = r'''
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
constexpr int INPUT = 0, OUTPUT = 1, LOW = 0, HIGH = 1;
constexpr int PIN_TFT_RST = 1, PIN_TFT_CS = 2, PIN_TFT_DC = 3;
constexpr int PIN_TFT_LEDA_CTL = 4, PIN_TFT_SDA = 5, PIN_TFT_SCL = 6;
constexpr int PIN_TFT_LEDA_CTL_ACTIVE = HIGH, DISPLAY_ROTATION = 1;
constexpr int SPI_CLOCK_DIV2 = 2, ST77XX_NORON = 0x13;
constexpr int ST77XX_DISPON = 0x29, ST77XX_DISPOFF = 0x28, ST77XX_SLPIN = 0x10;
constexpr uint16_t ST77XX_BLACK = 0, ST77XX_WHITE = 0xffff;
constexpr int NRF_GPIO_PIN_DIR_OUTPUT = 0, NRF_GPIO_PIN_INPUT_CONNECT = 1;
constexpr int NRF_GPIO_PIN_INPUT_DISCONNECT = 2, NRF_GPIO_PIN_NOPULL = 3;
constexpr int NRF_GPIO_PIN_H0H1 = 4, NRF_GPIO_PIN_NOSENSE = 5;
static unsigned gpio_calls, high_drive_calls, errors, resets;
static unsigned objects, destroyed, allocations, fills, color_calls;
static bool fail_pixels;
static std::vector<uint8_t> wire;
static unsigned transactions, finishes, windows, cs_calls;
void digitalWrite(int, int) { ++gpio_calls; }
void pinMode(int, int) { ++gpio_calls; }
void delay(unsigned) {}
void nrf_gpio_cfg(int, int, int, int, int, int) { ++high_drive_calls; }
void sendCommand(int) {}
void set_CS(int) { ++cs_calls; }
void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
assert(x == 0 && y == windows && w == 160 && h == 1);
++windows;
}
static void resetWire() {
wire.clear(); transactions = finishes = windows = cs_calls = color_calls = 0;
}
struct Spi {
void begin() {}
void begin(int, int, int, int) {}
void setClockDivider(int) {}
void beginTransaction(int) { ++transactions; }
void endTransaction() { ++finishes; }
void transfer(const void* data, void* output, size_t length) {
assert(output == nullptr && length == 320);
auto* bytes = static_cast<const uint8_t*>(data);
wire.insert(wire.end(), bytes, bytes + length);
}
void transferBytes(const uint8_t* data, uint8_t* output, size_t length) {
transfer(data, output, length);
}
} spi;
static Spi* _spi = &spi;
static int _spiSettings;
static int _height, _width, _colstart, _rowstart;
struct TFT_eSPI {
// Same public RGB332-to-RGB565 mapping as the pinned TFT_eSPI dependency.
uint16_t color8to16(uint8_t color) {
++color_calls;
const uint8_t blue[] = {0, 11, 21, 31};
return (color & 0x1c) << 6 | (color & 0xc0) << 5
| (color & 0xe0) << 8 | (color & 0x1c) << 3 | blue[color & 3];
}
} lcd;
struct TFT_eSprite {
int depth = 16;
bool depth_selected = false;
bool valid = false;
std::vector<uint8_t> pixels;
explicit TFT_eSprite(TFT_eSPI*) { ++objects; }
~TFT_eSprite() { ++destroyed; }
void setColorDepth(int value) {
assert(!valid && pixels.empty());
depth = value; depth_selected = true;
}
void* createSprite(int width, int height) {
assert(depth_selected && width == 160 && height == 80);
++allocations;
if (fail_pixels) { fail_pixels = false; return nullptr; }
pixels.resize((width * height + 1) * (depth / 8));
valid = true;
return getPointer();
}
void* getPointer() { return valid ? pixels.data() : nullptr; }
void fillScreen(uint16_t color) {
assert(valid && color == 0); ++fills;
std::fill(pixels.begin(), pixels.end(), 0);
}
void setTextColor(uint16_t color) { assert(color == 0xffff); }
};
@DEPTH_POLICY@
static TFT_eSprite* sprite;
static uint32_t curr_color;
@READY@
namespace mesh {
struct Logger { void println(const char*) { ++errors; } } logger;
Logger& usbLoggingPort() { return logger; }
}
struct Rail {
unsigned claims = 0;
void claim() { ++claims; }
void release() { assert(claims); --claims; }
};
class ST7735Display {
public:
bool _isOn = false;
Rail* _peripher_power;
explicit ST7735Display(Rail* rail) : _peripher_power(rail) {}
bool begin();
void turnOn();
void turnOff();
void endFrame();
void _resetAndInit() { ++resets; }
};
@METHODS@
static uint16_t expandExpected(uint8_t value) {
// Independent component arithmetic, not the driver's bit-expression.
unsigned r = value >> 5, g = value >> 2 & 7, b = value & 3;
unsigned red = r * 4 + r / 2;
unsigned green = g * 8 + g;
unsigned blue = (b * 31 + 2) / 3;
return static_cast<uint16_t>((red << 11) | (green << 5) | blue);
}
static uint8_t pack332(uint16_t value) {
return static_cast<uint8_t>((value >> 8 & 0xe0)
| (value >> 6 & 0x1c) | (value >> 3 & 3));
}
static void checkWire() {
assert(transactions == 1 && finishes == 1 && windows == 80 && cs_calls == 2);
assert(wire.size() == 160 * 80 * 2);
for (size_t pixel = 0; pixel < 160 * 80; ++pixel) {
#if ST7735_BUFFER_COLOR_DEPTH == 8
uint16_t expected = expandExpected(static_cast<uint8_t>(pixel));
assert(wire[2 * pixel] == expected >> 8);
assert(wire[2 * pixel + 1] == (expected & 0xff));
#else
// RGB565 storage is already in panel wire order; retain every byte.
assert(wire[2 * pixel] == static_cast<uint8_t>(pixel >> 5));
assert(wire[2 * pixel + 1] == static_cast<uint8_t>(pixel));
#endif
}
#if ST7735_BUFFER_COLOR_DEPTH == 8
assert(color_calls == 160 * 80);
#else
assert(color_calls == 0);
#endif
}
int main() {
Rail rail;
ST7735Display display(&rail);
// An absent or failed canvas must never be dereferenced or touch SPI.
display.endFrame();
assert(wire.empty() && transactions == 0);
fail_pixels = true;
assert(!display.begin());
assert(sprite == nullptr && objects == 1 && destroyed == 1);
assert(errors == 1 && gpio_calls == 0 && rail.claims == 0);
assert(display.begin());
assert(objects == 2 && allocations == 2 && fills == 1);
assert(sprite->pixels.size() == (160 * 80 + 1) * (ST7735_BUFFER_COLOR_DEPTH / 8));
assert(display._isOn && rail.claims == 1 && resets == 1);
assert(wire.size() == 160 * 80 * 2);
assert(std::all_of(wire.begin(), wire.end(), [](uint8_t value) { return value == 0; }));
#if defined(NRF52_PLATFORM)
assert(high_drive_calls == 2);
#endif
const unsigned first_gpio_calls = gpio_calls;
const void* original_pixels = sprite->getPointer();
assert(display.begin());
assert(objects == 2 && allocations == 2 && fills == 1 && resets == 1);
assert(gpio_calls == first_gpio_calls && rail.claims == 1);
// Every one of the 256 RGB332 codes appears repeatedly, including row
// boundaries. The sentinel byte/pixel is not transmitted.
for (size_t pixel = 0; pixel < 160 * 80; ++pixel) {
#if ST7735_BUFFER_COLOR_DEPTH == 8
sprite->pixels[pixel] = static_cast<uint8_t>(pixel);
#else
sprite->pixels[2 * pixel] = static_cast<uint8_t>(pixel >> 5);
sprite->pixels[2 * pixel + 1] = static_cast<uint8_t>(pixel);
#endif
}
resetWire();
display.endFrame();
checkWire();
// The UI's actual eight named colors remain exact, not merely similar.
for (uint16_t color : {0x0000, 0xffff, 0xf800, 0x07e0,
0x001f, 0x07ff, 0xf81f, 0xffe0}) {
assert(expandExpected(pack332(color)) == color);
}
// Sleep/wake repairs the bus but does not allocate, clear, or replace the
// retained frame. A second wake while already on is a complete no-op.
display.turnOff();
assert(!display._isOn && rail.claims == 0);
resetWire();
display.turnOn();
checkWire();
assert(display._isOn && rail.claims == 1 && resets == 2);
assert(sprite->getPointer() == original_pixels);
assert(objects == 2 && allocations == 2 && fills == 1);
resetWire();
display.turnOn();
assert(wire.empty() && transactions == 0 && allocations == 2);
// Corrupt/failed library canvas state also fails closed before any SPI or
// reinitialization. It is not silently reinterpreted as another depth.
sprite->valid = false;
resetWire();
assert(!display.begin());
display.endFrame();
assert(wire.empty() && transactions == 0 && allocations == 2);
delete sprite;
sprite = nullptr;
}
'''
class ST7735FramebufferTests(unittest.TestCase):
def source(self):
driver = DRIVER.read_text(encoding="utf-8")
start = driver.index("#ifndef ST7735_BUFFER_COLOR_DEPTH")
end = driver.index("#if defined(NRF52_PLATFORM)", start)
methods = "\n".join(body(driver, signature) for signature in (
"bool ST7735Display::begin()", "void ST7735Display::endFrame()",
"void ST7735Display::turnOff()", "void ST7735Display::turnOn()"))
return (HARNESS.replace("@DEPTH_POLICY@", driver[start:end])
.replace("@READY@", body(driver, "static bool spriteReady()"))
.replace("@METHODS@", methods))
def test_depths_spi_wire_order_allocation_and_wake(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-st7735-buffer-") as directory:
cpp = Path(directory) / "framebuffer.cpp"
cpp.write_text(self.source(), encoding="utf-8")
for depth in (8, 16):
for platform in ("ESP_PLATFORM", "NRF52_PLATFORM"):
with self.subTest(depth=depth, platform=platform):
binary = Path(directory) / f"framebuffer-{depth}-{platform}.exe"
result = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-variable", "-Wno-unused-function", *SANITIZER_FLAGS,
f"-DST7735_BUFFER_COLOR_DEPTH={depth}", f"-D{platform}=1",
str(cpp), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(binary)], capture_output=True,
text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_unsupported_depth_is_rejected(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-st7735-depth-") as directory:
cpp = Path(directory) / "invalid.cpp"
cpp.write_text(self.source(), encoding="utf-8")
result = subprocess.run([
compiler, "-std=c++17", "-DST7735_BUFFER_COLOR_DEPTH=4", "-c",
str(cpp), "-o", str(Path(directory) / "invalid.o"),
], capture_output=True, text=True, timeout=60)
self.assertNotEqual(result.returncode, 0)
self.assertIn("ST7735_BUFFER_COLOR_DEPTH must be 8 or 16", result.stderr)
if __name__ == "__main__":
unittest.main()
+3
View File
@@ -49,6 +49,9 @@ build_flags = ${rak4631_hw.build_flags}
-D PIN_VBAT_READ=BATTERY_PIN
-D PIN_BAT_CTL=47
-D DISPLAY_CLASS=ST7735Display
; The UI uses RGB332-exact primary colors. Keep every 160x80 pixel and
; expand one scanline to RGB565 on SPI, saving 12,801 bytes of canvas RAM.
-D ST7735_BUFFER_COLOR_DEPTH=8
-D DISPLAY_ROTATION=1
; The 160x80 T096 has no touch/navigation control for the message channel
; selector, so let message text use its 24 logical (30 physical) rows.