Files
MeshCore/test/test_native_displays.py
T
mikecarper fd47fe0b1a Merge upstream dev into keymindCascade
Bring in the new Companion WiFi profiles while preserving existing OLED detection, message queues and OTA safeguards. Include dynamic WiFi helpers consistently and refresh the affected test fixtures.
2026-10-23 09:57:14 -07:00

128 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Exercise actual non-Indicator driver geometry/drawing with recording panels."""
import os
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
UI = ROOT / "src/helpers/ui"
def method(source, cls, name):
start = source.index(f"{cls}::{name}(")
start = source.rfind("\n", 0, start) + 1
end = source.index("\n}", start) + 2
return source[start:end] + "\n"
class NativeDisplaysTest(unittest.TestCase):
def test_real_headers_and_rendering(self):
cases = [
("ST7789Display", 240, 135, []),
("ST7789Display", 320, 170, ["HELTEC_VISION_MASTER_T190"]),
("ST7789Display", 320, 240, ["THINKNODE_M9"]),
("ST7789LCDDisplay", 320, 240, []),
("ST7789LCDDisplay", 320, 240, ["USE_PIN_TFT"]),
("ST7789LCDDisplay", 320, 240, ["LILYGO_TDECK"]),
("ST7789LCDDisplay", 240, 320, ["HELTEC_V4_R8_TFT", "ST7789_PORTRAIT_PROFILE", "DISPLAY_ROTATION=0"]),
("NV3001BDisplay", 220, 128, []),
("GxEPDDisplay", 200, 200, []),
("GxEPDDisplay", 250, 122, ["EINK_DISPLAY_MODEL=WidePanel"]),
("GxEPDDisplay", 250, 122, ["EINK_DISPLAY_MODEL=WidePanel", "DISPLAY_ROTATION=1"]),
("GxEPDDisplay", 192, 256, ["EINK_DISPLAY_MODEL=TallPanel", "DISPLAY_ROTATION=4"]),
]
with tempfile.TemporaryDirectory(prefix="meshcore-native-panels-") as temp:
for cls, width, height, defines in cases:
with self.subTest(driver=cls, defines=defines):
source = (ROOT / "test/fixtures/native_displays/recording.cpp").read_text()
header = (UI / (cls + ".h")).read_text()
source += re.sub(r'^\s*#(?:include|pragma).*$', '', header, flags=re.M)
driver = (UI / (cls + ".cpp")).read_text()
if cls == "NV3001BDisplay":
source += "Canvas nv_canvas;\n"
source += f"bool {cls}::begin() {{ is_on=true; return true; }}\n"
source += f"void {cls}::fillPhysicalRect(int x,int y,int w,int h) {{ Canvas::active->fillRect(x,y,w,h); }}\n"
a = driver.index("static const uint8_t font5x7")
b = driver.index("static void setupOptionalOutput", a)
source += driver[a:b]
source += method(driver, cls, "drawChar")
else:
awake = "_isOn=true; " if cls == "ST7789LCDDisplay" else ""
source += f"bool {cls}::begin() {{ {awake}return true; }}\n"
for name in ("turnOn", "turnOff", "clear", "endFrame"):
source += f"void {cls}::{name}() {{}}\n"
source += f"void {cls}::startFrame(ColorVal) {{}}\n"
if cls == "ST7789LCDDisplay":
source += f"void {cls}::setFlipped(bool) {{}}\n"
if "LILYGO_TDECK" in defines:
source += f"{cls}::~{cls}() {{}}\n"
source += method(driver, cls, "isBufferedRendering")
source += f"void {cls}::finishCanvasPrimitive(int,int,int,int) {{ assert(false); }}\n"
names = ["setTextSize", "setColor", "setCursor", "print", "fillRect", "drawRect", "drawXbm", "getTextWidth"]
if cls == "ST7789LCDDisplay":
names += ["setRotationDegrees"]
if cls == "ST7789Display":
names += ["printWordWrap"]
source += "\n".join(method(driver, cls, name) for name in names)
source += f"\nusing Driver = {cls};\n"
source += r'''
int main() {
Driver d; d.begin(); d.setColor(1); d.setTextSize(1);
#ifdef LILYGO_TDECK
assert(!d.isBufferedRendering());
#endif
auto& c=*Canvas::active;
assert(d.width()==EXPECTED_WIDTH && d.height()==EXPECTED_HEIGHT);
assert(!d.useSmallMessageFont());
c.reset();
for(int y=0;y<d.height();++y) for(int x=0;x<d.width();++x) d.fillRect(x,y,1,1);
for(int pixel:c.pixels) assert(pixel==1);
c.reset(); d.drawRect(0,0,d.width(),d.height());
for(int y=0;y<d.height();++y) for(int x=0;x<d.width();++x)
assert((c.pixels[y*d.width()+x]>0)==(x==0||y==0||x==d.width()-1||y==d.height()-1));
// Non-byte-aligned bitmap and the bottom/right panel boundary.
const uint8_t bitmap[]={0x81,0x80,0x42,0x00};
c.reset(); d.drawXbm(d.width()-9,d.height()-2,bitmap,9,2);
assert(std::count(c.pixels.begin(),c.pixels.end(),1)==5);
assert(c.pixels.back()==0 && c.pixels[(d.height()-2)*d.width()+d.width()-1]==1);
assert(d.getTextWidth("MMMM") >= 24);
assert(d.getTextWidth("MMMMMMMM") > d.getTextWidth("MMMM"));
assert(d.getTextWidth("")==0);
d.setCursor(7,9); d.print("M");
#ifdef NV_NATIVE_TEST
// Native 5x7 glyph: no historical vertical doubling.
c.reset(); d.setCursor(0,0); d.print("M");
for(int y=7;y<d.height();++y) for(int x=0;x<d.width();++x) assert(c.pixels[y*d.width()+x]==0);
#else
assert(c.x==7 && c.y==9+c.ascent);
#endif
}
'''
flags = [f"-D{x}" for x in defines]
if cls == "NV3001BDisplay":
flags += ["-DNV_NATIVE_TEST"]
if os.name != "nt":
flags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
binary = Path(temp) / "display.exe"
result = subprocess.run(["c++", "-std=c++11", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-unused-variable", "-Wno-type-limits", *flags,
f"-DEXPECTED_WIDTH={width}", f"-DEXPECTED_HEIGHT={height}",
"-I", str(ROOT / "src"), "-x", "c++", "-", "-o", str(binary)],
input=source, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_no_obsolete_panel_scaling_flags(self):
for driver in ("ST7735Display", "ST7789Display", "ST7789LCDDisplay", "NV3001BDisplay", "GxEPDDisplay"):
for suffix in (".h", ".cpp"):
source = (UI / (driver + suffix)).read_text()
self.assertNotRegex(source, r"DISPLAY_SCALE|EINK_SCALE|SCALE_[XY]|portraitViewport")
if __name__ == "__main__":
unittest.main()