Files
MeshCore/test/test_wireless_tracker_display_detection.py
T

295 lines
13 KiB
Python

"""Exercise optional Tracker V1 display reads and shared-rail driver lifecycle."""
from pathlib import Path
import os
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]
UI = ROOT / "src/helpers/ui"
ARDUINO = r'''
#pragma once
#include <cassert>
#include <cstdint>
#include <string>
#include <vector>
#define LOW 0
#define HIGH 1
#define INPUT 0
#define OUTPUT 1
#define INPUT_PULLUP 2
#define INPUT_PULLDOWN 3
#define PIN_TFT_SCL 41
#define PIN_TFT_SDA 42
#define PIN_TFT_RST 39
#define PIN_TFT_LEDA_CTL 21
#define PIN_TFT_CS 38
#define PIN_TFT_DC 40
#ifndef PIN_TFT_LEDA_CTL_ACTIVE
#define PIN_TFT_LEDA_CTL_ACTIVE HIGH
#endif
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 initializations = 0, allocations = 0, objects = 0, draws = 0;
inline unsigned clock_edges = 0, command_bits = 0, data_bits = 0;
inline uint8_t command_value = 0;
inline bool transaction = false, fail_pixels = false, initialized = false;
inline std::string scenario;
inline uint32_t read_value = 0;
inline bool automatic() {
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
return true;
#else
return false;
#endif
}
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_LEDA_CTL)
&& "probe must never drive Vext, GNSS, or radio pins");
}
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);
const unsigned within_attempt = (transactions - 1) % 6;
read_value = 0x7c89f0;
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 ? 0x7c89f0 : 0x123456;
if (scenario == "noise") read_value = within_attempt == 2 ? 0x7c89f1 : 0x7c89f0;
if (scenario == "recover" && transactions <= 6) read_value = 0;
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 == PIN_TFT_LEDA_CTL_ACTIVE)
assert(initialized && "backlight must remain off until initialization");
if (pin == PIN_TFT_RST && value == HIGH && levels[pin] == LOW) {
reset_low_us = now_us - reset_low_at; reset_high_at = now_us;
}
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() && now_us - reset_high_at >= 120000 && reset_low_us >= 10000);
assert(levels[PIN_TFT_LEDA_CTL] != PIN_TFT_LEDA_CTL_ACTIVE);
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 && levels[PIN_TFT_SCL] == LOW);
++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; }
'''
HARNESS = r'''
@POLICY@
#include <Arduino.h>
#include "WirelessTrackerDisplayProbe.h"
constexpr int SPI_CLOCK_DIV2=2, ST77XX_NORON=0x13, ST77XX_DISPON=0x29;
constexpr int ST77XX_DISPOFF=0x28, ST77XX_SLPIN=0x10;
constexpr uint16_t ST77XX_BLACK=0, ST77XX_WHITE=0xffff;
constexpr int ST7735_BUFFER_COLOR_DEPTH=16;
struct Spi {
void begin(int scl, int miso, int sda, int cs) {
assert(scl==41 && miso==-1 && sda==42 && cs==-1);
if (automatic()) assert(transactions>=6);
++spi_begins;
}
void setClockDivider(int) {}
} spi;
static Spi* _spi=&spi;
static int _height, _width, _colstart, _rowstart, lcd;
struct TFT_eSprite {
bool valid=false;
explicit TFT_eSprite(int*) { ++objects; }
void setColorDepth(int) {}
void* createSprite(int w,int h) {
assert(w==160 && h==80); ++allocations;
valid=!fail_pixels; fail_pixels=false;
return getPointer();
}
void* getPointer() { return valid ? this : nullptr; }
void fillScreen(uint16_t) { assert(valid); ++draws; }
void setTextColor(uint16_t) { assert(valid); }
};
static TFT_eSprite* sprite=nullptr;
static uint32_t curr_color;
@READY@
namespace mesh {
struct Logger { void println(const char*) {} } logger;
Logger& usbLoggingPort() { return logger; }
}
struct Rail {
unsigned claims;
unsigned taken=0,released=0;
void claim() { ++claims; ++taken; }
void release() { assert(claims); --claims; ++released; }
};
void sendCommand(int) {}
@MARKER@
class ST7735Display {
public:
bool _isOn=false;
Rail* _peripher_power;
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
bool _probeComplete=false,_present=false;
#endif
explicit ST7735Display(Rail* power) : _peripher_power(power) {}
bool begin();
void turnOn();
void turnOff();
void _resetAndInit() { ++initializations; initialized=true; }
void endFrame() { if (spriteReady()) { assert(initialized); ++draws; } }
};
@METHODS@
int main(int argc,char** argv) {
assert(argc==3); scenario=argv[1];
const unsigned gps_claims=unsigned(std::stoul(argv[2]));
Rail rail{gps_claims}; ST7735Display display(&rail);
const bool invalid=scenario=="floating" || scenario=="stuck-high" || scenario=="stuck-low"
|| scenario=="biased" || scenario=="noise";
if (scenario=="alloc-fail") fail_pixels=true;
const bool began=display.begin();
if (automatic() && invalid) {
assert(!began && !display._isOn && "absent panel must remain headless");
assert(allocations==0 && objects==0 && spi_begins==0 && initializations==0);
assert(rail.claims==gps_claims && rail.taken==1 && rail.released==1);
assert(levels[PIN_TFT_RST]==LOW && levels[PIN_TFT_LEDA_CTL]!=PIN_TFT_LEDA_CTL_ACTIVE);
for (int pin : {PIN_TFT_CS,PIN_TFT_DC,PIN_TFT_SDA,PIN_TFT_SCL}) assert(modes[pin]==INPUT);
const auto events= gpio_events;
assert(!display.begin()); display.turnOn(); display.turnOff(); display.endFrame();
assert(events==gpio_events && transactions==12 && now_us<400000);
assert(rail.claims==gps_claims && allocations==0 && draws==0);
return 0;
}
if (scenario=="alloc-fail") {
assert(!began && sprite==nullptr && allocations==1 && spi_begins==0);
assert(rail.claims==gps_claims);
assert(display.begin());
assert(allocations==2 && rail.claims==gps_claims+1);
} else {
assert(began && allocations==1 && rail.claims==gps_claims+1);
}
assert(display._isOn && initializations==1);
const auto probe_count=transactions;
if (automatic()) assert(probe_count==(scenario=="recover" ? 12u : 6u));
else assert(probe_count==0 && reads==0);
assert(levels[PIN_TFT_LEDA_CTL]==PIN_TFT_LEDA_CTL_ACTIVE);
const auto pixels=sprite; const auto allocated=allocations; const auto events=gpio_events;
assert(display.begin() && gpio_events==events && rail.claims==gps_claims+1);
display.turnOff(); assert(rail.claims==gps_claims && !display._isOn);
display.turnOn();
assert(display._isOn && rail.claims==gps_claims+1 && initializations==2);
assert(transactions==probe_count && allocations==allocated && sprite==pixels);
display.turnOff(); assert(rail.claims==gps_claims);
delete sprite;
}
'''
class WirelessTrackerDisplayDetectionTests(unittest.TestCase):
def build(self, work, flags, *, old_bias=False, old_turnaround=False):
work.mkdir()
driver = (UI / "ST7735Display.cpp").read_text()
header = (UI / "ST7735Display.h").read_text()
policy = header[header.index("#if defined(WIRELESS_TRACKER_DISPLAY_AUTODETECT)"):header.index("class ST7735Display")]
marker = driver[driver.index("#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT"):driver.index("// Most ST7735")]
methods = "\n".join(body(driver, signature) for signature in (
"bool ST7735Display::begin()", "void ST7735Display::turnOn()", "void ST7735Display::turnOff()"))
source = (HARNESS.replace("@POLICY@", policy).replace("@MARKER@", marker)
.replace("@READY@", body(driver, "static bool spriteReady()"))
.replace("@METHODS@", methods))
probe = (UI / "WirelessTrackerDisplayProbe.h").read_text()
if old_bias:
probe = probe.replace("ids[0] == ids[1]", "true")
if old_turnaround:
probe = probe.replace("if (mask == 1) pinMode(PIN_TFT_SDA, bias);", "")
probe = probe.replace(" digitalWrite(PIN_TFT_DC, HIGH);", " pinMode(PIN_TFT_SDA, bias);\n digitalWrite(PIN_TFT_DC, HIGH);", 1)
(work / "Arduino.h").write_text(ARDUINO)
(work / "WirelessTrackerDisplayProbe.h").write_text(probe)
(work / "test.cpp").write_text(source)
binary = work / "test"
compiled = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-variable", *SANITIZER_FLAGS, "-DESP_PLATFORM=1", *flags, "-I"+str(work),
str(work / "test.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, gps=1, negative=None):
result = subprocess.run([str(binary), scenario, str(gps)], capture_output=True, text=True, timeout=10)
if negative:
self.assertNotEqual(result.returncode, 0)
self.assertIn(negative, result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_optional_tracker_rejects_absence_noise_and_preserves_gps_power(self):
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
for board in ("HELTEC_LORA_V3", "HELTEC_TRACKER_V1_1"):
binary = self.build(Path(directory)/board, ["-D"+board, "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1"])
self.assertIn(b"meshcore.capability.display.autodetect.tracker.v1", binary.read_bytes())
for scenario in ("floating", "stuck-high", "stuck-low", "biased", "noise"):
for gps in (0, 1):
with self.subTest(board=board, scenario=scenario, gps=gps): self.run_case(binary, scenario, gps)
def test_present_programmable_id_reset_recovery_and_allocation_retry(self):
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
for active in (0, 1):
binary = self.build(Path(directory)/str(active), ["-DHELTEC_LORA_V3", "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1",
f"-DPIN_TFT_LEDA_CTL_ACTIVE={active}"])
for scenario in ("valid", "programmable", "prime", "recover", "alloc-fail"):
for gps in (0, 1):
with self.subTest(active=active, scenario=scenario, gps=gps): self.run_case(binary, scenario, gps)
def test_other_boards_and_disabled_option_keep_existing_initialization(self):
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
variants = (["-DHELTEC_LORA_V3"], ["-DHELTEC_LORA_V3", "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=0"],
["-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1"],
*[["-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1", "-DHELTEC_LORA_V3", "-D"+board]
for board in ("HELTEC_TRACKER_V2", "HELTEC_T096", "HELTEC_T1")])
for index, flags in enumerate(variants):
binary = self.build(Path(directory)/str(index), flags)
self.assertNotIn(b"meshcore.capability.display.autodetect.tracker.v1", binary.read_bytes())
self.run_case(binary, "floating")
def test_opposite_pull_and_turnaround_negative_controls_fail(self):
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
flags = ["-DHELTEC_LORA_V3", "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1"]
binary = self.build(Path(directory)/"bias", flags, old_bias=True)
self.run_case(binary, "biased", negative="absent panel must remain headless")
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()