Fix T096 Companion runtime RAM with RGB332 framebuffer
This commit is contained in:
1 parent
eb4f837969
commit
4805b5cdbb
8 files changed
+438
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user