"""Exercise the T114 probe and real driver lifecycle without a panel or SPI MISO.""" from pathlib import Path import os import subprocess import sys import tempfile import unittest from test_replay_reset_integration import extract_braced ROOT = Path(__file__).resolve().parents[1] UI = ROOT / "src/helpers/ui" ARDUINO = r''' #pragma once #include #include #include #include #include #define LOW 0 #define HIGH 1 #define INPUT 0 #define OUTPUT 1 #define INPUT_PULLUP 2 #define INPUT_PULLDOWN 3 #define PIN_TFT_SCL 40 #define PIN_TFT_SDA 41 #define PIN_TFT_RST 2 #define PIN_TFT_VDD_CTL 3 #define PIN_TFT_LEDA_CTL 15 #define PIN_TFT_CS 11 #define PIN_TFT_DC 12 #define SPI_CLOCK_DIV2 2 #define pgm_read_byte(p) (*(p)) inline int modes[48] = {}, levels[48] = {}; inline uint64_t now_us = 0, reset_high_at = 0, reset_low_at = 0, reset_low_us = 0; inline unsigned gpio_events = 0, reads = 0, transactions = 0, spi_begins = 0; inline unsigned init_calls = 0, allocations = 0, draws = 0, resets = 0; inline unsigned clock_edges = 0, command_bits = 0, data_bits = 0; inline uint8_t command_value = 0; inline bool transaction = false, initialized = false, init_ok = true; inline std::string scenario; inline std::vector biases; inline uint32_t read_value = 0; inline void check_panel_pin(int pin) { assert((pin == PIN_TFT_CS || pin == PIN_TFT_DC || pin == PIN_TFT_SDA || pin == PIN_TFT_SCL || pin == PIN_TFT_RST || pin == PIN_TFT_VDD_CTL || pin == PIN_TFT_LEDA_CTL) && "probe must only touch panel pins, not MISO or the radio bus"); } inline bool automatic() { #if defined(HELTEC_T114) && defined(T114_DISPLAY_AUTODETECT) && T114_DISPLAY_AUTODETECT return true; #else return false; #endif } inline int backlight_on() { #ifdef PIN_TFT_LEDA_CTL_ACTIVE return PIN_TFT_LEDA_CTL_ACTIVE; #else return LOW; #endif } inline void pinMode(int pin, int mode) { check_panel_pin(pin); ++gpio_events; modes[pin] = mode; if (pin == PIN_TFT_SDA && (mode == INPUT_PULLUP || mode == INPUT_PULLDOWN)) { assert(transaction && command_bits == 8 && command_value == 0x04); biases.push_back(mode); const unsigned within_attempt = (transactions - 1) % 6; read_value = 0x858552; if (scenario == "floating") read_value = mode == INPUT_PULLUP ? 0xFFFFFF : 0; if (scenario == "stuck-high") read_value = 0xFFFFFF; if (scenario == "stuck-low") read_value = 0; if (scenario == "biased") read_value = mode == INPUT_PULLUP ? 0x858552 : 0x123456; if (scenario == "noise") read_value = within_attempt == 2 ? 0x858553 : 0x858552; if (scenario == "recover" && transactions <= 6) read_value = 0; // The first read after changing bias can be stale on a real panel. if (scenario == "prime" && within_attempt % 3 == 0) read_value = 0; if (scenario == "programmable") read_value = 0x123456; } } inline void digitalWrite(int pin, int value) { check_panel_pin(pin); ++gpio_events; if (automatic() && pin == PIN_TFT_LEDA_CTL && value == backlight_on()) assert(initialized && "backlight must stay off until initialization succeeds"); if (pin == PIN_TFT_RST && value == HIGH && levels[pin] == LOW) { reset_low_us = now_us - reset_low_at; reset_high_at = now_us; ++resets; } if (pin == PIN_TFT_RST && value == LOW && levels[pin] == HIGH) reset_low_at = now_us; if (pin == PIN_TFT_CS && value == LOW && levels[pin] == HIGH && spi_begins == 0) { assert(automatic()); assert(now_us - reset_high_at >= 120000 && "probe reset settle"); assert(reset_low_us >= 10000 && "probe reset pulse"); assert(levels[PIN_TFT_VDD_CTL] == LOW); assert(levels[PIN_TFT_LEDA_CTL] != backlight_on()); transaction = true; ++transactions; clock_edges = command_bits = data_bits = 0; command_value = 0; } if (pin == PIN_TFT_SCL && value == HIGH && transaction) { ++clock_edges; if (modes[PIN_TFT_SDA] == OUTPUT) { assert(levels[PIN_TFT_DC] == LOW && command_bits < 8); command_value = uint8_t((command_value << 1) | levels[PIN_TFT_SDA]); ++command_bits; } } if (pin == PIN_TFT_SCL && value == LOW && transaction && clock_edges == 8) assert(modes[PIN_TFT_SDA] != OUTPUT && "release SDA before command falling edge"); if (pin == PIN_TFT_CS && value == HIGH && transaction) { assert(command_bits == 8 && data_bits == 24 && clock_edges == 33); transaction = false; } levels[pin] = value; } inline int digitalRead(int pin) { assert(pin == PIN_TFT_SDA && transaction && levels[PIN_TFT_DC] == HIGH); assert(modes[pin] == INPUT_PULLUP || modes[pin] == INPUT_PULLDOWN); assert(clock_edges == 9 + data_bits && data_bits < 24); assert(levels[PIN_TFT_SCL] == LOW && "sample after dummy/falling edge"); ++reads; return (read_value >> (23 - data_bits++)) & 1; } inline void delay(unsigned ms) { now_us += uint64_t(ms) * 1000; } inline void delayMicroseconds(unsigned us) { now_us += us; } ''' SPI = r''' #pragma once #include struct SPIClass { void begin() { ++spi_begins; // The actual SPI1 opener takes SDA/SCK back from bitbang GPIO mode. modes[PIN_TFT_SDA] = modes[PIN_TFT_SCL] = OUTPUT; } void setClockDivider(int) {} }; inline SPIClass SPI, SPI1; ''' PANEL = r''' #pragma once #include enum OLEDDISPLAY_COLOR { BLACK=0, WHITE=1 }; constexpr int GEOMETRY_RAWMODE=0, ST77XX_WHITE=0xffff, ST77XX_BLACK=0; inline uint8_t ArialMT_Plain_16[1], ArialMT_Plain_24[1]; class ST7789Spi { uint8_t _rst, _dc, _cs; int _clk=40, _miso=-1, _mosi=41; uint16_t displayHeight=135; uint8_t _buffheight=0; SPIClass* _spi; public: template ST7789Spi(SPIClass* spi, uint8_t rst, uint8_t dc, uint8_t cs, T...) : _rst(rst), _dc(dc), _cs(cs), _spi(spi) {} @CONNECT@ bool init() { ++init_calls; assert(connect()); if (automatic()) { assert(transactions >= 6); assert(now_us - reset_high_at >= 120000 && "initialization reset settle"); assert(reset_low_us >= 10000); assert(levels[PIN_TFT_LEDA_CTL] != backlight_on()); } ++allocations; initialized = init_ok; return init_ok; } void landscapeScreen() { assert(initialized); } void flipScreenVertically() { assert(initialized); } void displayOn() { assert(initialized); } void clear() { assert(initialized); ++draws; } void display() { assert(initialized); ++draws; } void setFont(const uint8_t*) { assert(initialized); ++draws; } void setColor(OLEDDISPLAY_COLOR) { assert(initialized); ++draws; } void setRGB(uint16_t) { assert(initialized); ++draws; } void drawString(int,int,const char*) { assert(initialized); ++draws; } void drawStringMaxWidth(int,int,int,const char*) { assert(initialized); ++draws; } void fillRect(int,int,int,int) { assert(initialized); ++draws; } void drawRect(int,int,int,int) { assert(initialized); ++draws; } uint16_t getStringWidth(const char* s) { assert(initialized); ++draws; return strlen(s)*8; } }; ''' MAIN = r''' #include "ST7789Display.h" void frame(ST7789Display& d) { const uint8_t bits[] = {0x80}; d.clear(); d.startFrame(); d.setTextSize(1); d.setColor(1); d.setCursor(0,0); d.print("a"); d.printWordWrap("a",10); d.fillRect(0,0,1,1); d.drawRect(0,0,1,1); d.drawXbm(0,0,bits,1,1); d.endFrame(); (void)d.getTextWidth("a"); } int main(int argc, char** argv) { assert(argc == 2); scenario = argv[1]; ST7789Display d; if (!automatic()) { assert(d.begin() && d.isOn()); frame(d); assert(reads == 0 && transactions == 0); assert(now_us < 120000 && "legacy/other-board reset timing unchanged"); return 0; } d.turnOn(); frame(d); assert(gpio_events == 0 && init_calls == 0 && allocations == 0 && draws == 0); const bool invalid = scenario == "floating" || scenario == "stuck-high" || scenario == "stuck-low" || scenario == "noise" || scenario == "biased"; if (scenario == "init-fail") init_ok = false; const bool begun = d.begin(); if (invalid || !init_ok) { assert(!begun && !d.isOn() && "invalid panel must remain headless"); if (invalid) assert(init_calls == 0 && allocations == 0 && spi_begins == 0); assert(levels[PIN_TFT_VDD_CTL] == HIGH && levels[PIN_TFT_RST] == LOW); assert(levels[PIN_TFT_LEDA_CTL] != backlight_on()); for (int pin : {PIN_TFT_CS,PIN_TFT_DC,PIN_TFT_SDA,PIN_TFT_SCL}) assert(modes[pin] == INPUT); const auto events=gpio_events, init=init_calls, allocated=allocations; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin()); assert(gpio_events == events && init_calls == init && allocations == allocated && draws == 0); assert(transactions == (invalid ? 12u : 6u)); assert(now_us < 400000); return 0; } assert(begun && d.isOn() && initialized); assert(init_calls == 1 && allocations == 1); assert(levels[PIN_TFT_LEDA_CTL] == backlight_on()); assert(transactions == (scenario == "recover" ? 12u : 6u)); assert(biases.size() == transactions && reads == 24*transactions); for (unsigned i=0; i 0); const auto count=transactions, drawn=draws; d.turnOff(); assert(!d.isOn()); assert(levels[PIN_TFT_VDD_CTL] == HIGH && levels[PIN_TFT_LEDA_CTL] != backlight_on()); frame(d); assert(draws == drawn); d.turnOn(); assert(d.isOn() && init_calls == 2 && transactions == count); assert(levels[PIN_TFT_LEDA_CTL] == backlight_on()); frame(d); assert(draws > drawn); if (scenario == "wake-fail") { d.turnOff(); init_ok = false; d.turnOn(); assert(!d.isOn() && levels[PIN_TFT_VDD_CTL] == HIGH); const auto events = gpio_events; frame(d); d.turnOn(); assert(!d.begin()); assert(events == gpio_events); } } ''' class T114DisplayDetectionTest(unittest.TestCase): def build(self, work, flags=(), *, old_bias=False, old_settle=False, old_turnaround=False): work.mkdir() for filename in ("ST7789Display.cpp", "ST7789Display.h", "T114DisplayProbe.h"): source = (UI / filename).read_text() if old_bias and filename == "T114DisplayProbe.h": mutated = source.replace("ids[0] == ids[1]", "true") self.assertNotEqual(mutated, source) source = mutated if old_turnaround and filename == "T114DisplayProbe.h": mutated = source.replace("if (mask == 1) pinMode(PIN_TFT_SDA, bias);", "") self.assertNotEqual(mutated, source) source = mutated.replace(" digitalWrite(PIN_TFT_DC, HIGH);", " pinMode(PIN_TFT_SDA, bias);\n digitalWrite(PIN_TFT_DC, HIGH);", 1) (work / filename).write_text(source) connect = extract_braced((UI / "ST7789Spi.h").read_text(), "bool connect()") if old_settle: mutated = connect.replace("delay(120);", "delay(0);") self.assertNotEqual(mutated, connect) connect = mutated for filename, source in { "Arduino.h": ARDUINO, "SPI.h": SPI, "Wire.h": "#pragma once\nclass TwoWire {};\n", "Adafruit_GFX.h": "#pragma once\n", "ST7789Spi.h": PANEL.replace("@CONNECT@", connect), "main.cpp": MAIN}.items(): (work / filename).write_text(source) sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []) binary = work / "test" compiled = subprocess.run([ os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", *sanitizer, "-DST7789=1", *flags, "-I"+str(work), "-I"+str(UI), "-I"+str(ROOT / "src"), str(work / "main.cpp"), str(work / "ST7789Display.cpp"), "-o", str(binary) ], capture_output=True, text=True, timeout=60) self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr) return binary def run_case(self, binary, scenario, *, negative=None): ran = subprocess.run([str(binary), scenario], capture_output=True, text=True, timeout=10) if negative: self.assertNotEqual(ran.returncode, 0) self.assertIn(negative, ran.stderr) else: self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) def test_real_probe_rejects_floating_stuck_and_unstable_lines_without_allocating(self): with tempfile.TemporaryDirectory(prefix="t114-display-") as directory: binary = self.build(Path(directory)/"auto", ["-DHELTEC_T114", "-DT114_DISPLAY_AUTODETECT=1"]) self.assertIn(b"meshcore.capability.display.autodetect.t114.v1", binary.read_bytes()) for scenario in ("floating", "stuck-high", "stuck-low", "noise", "biased", "init-fail"): with self.subTest(scenario=scenario): self.run_case(binary, scenario) def test_real_probe_accepts_programmable_ids_recovery_and_preserves_wake(self): with tempfile.TemporaryDirectory(prefix="t114-display-") as directory: for active in (None, 1): flags = ["-DHELTEC_T114", "-DT114_DISPLAY_AUTODETECT=1"] if active is not None: flags.append(f"-DPIN_TFT_LEDA_CTL_ACTIVE={active}") binary = self.build(Path(directory)/str(active), flags) for scenario in ("valid", "prime", "programmable", "recover", "wake-fail"): with self.subTest(active=active, scenario=scenario): self.run_case(binary, scenario) def test_legacy_t114_and_other_boards_do_not_probe_or_change_reset_timing(self): with tempfile.TemporaryDirectory(prefix="t114-display-") as directory: for index, flags in enumerate((["-DHELTEC_T114"], ["-DHELTEC_T114", "-DT114_DISPLAY_AUTODETECT=0"], ["-DT114_DISPLAY_AUTODETECT=1"])): binary = self.build(Path(directory)/str(index), flags) self.run_case(binary, "valid") self.assertNotIn(b"meshcore.capability.display.autodetect.t114.v1", binary.read_bytes()) def test_bias_and_reset_settle_negative_controls_are_detected(self): with tempfile.TemporaryDirectory(prefix="t114-display-") as directory: flags = ["-DHELTEC_T114", "-DT114_DISPLAY_AUTODETECT=1"] binary = self.build(Path(directory)/"bias", flags, old_bias=True) self.run_case(binary, "biased", negative="invalid panel must remain headless") binary = self.build(Path(directory)/"settle", flags, old_settle=True) self.run_case(binary, "valid", negative="initialization reset settle") binary = self.build(Path(directory)/"turnaround", flags, old_turnaround=True) self.run_case(binary, "valid", negative="release SDA before command falling edge") if __name__ == "__main__": unittest.main()