312 lines
14 KiB
Python
312 lines
14 KiB
Python
"""Exercise the RC32 optional T108 probe and actual NV3001B driver lifecycle."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UI = ROOT / "src/helpers/ui"
|
|
|
|
ARDUINO = r'''
|
|
#pragma once
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#define LOW 0
|
|
#define HIGH 1
|
|
#define INPUT 0
|
|
#define OUTPUT 1
|
|
#define INPUT_PULLUP 2
|
|
#define INPUT_PULLDOWN 3
|
|
#define PROGMEM
|
|
#define pgm_read_byte(p) (*(p))
|
|
#define PIN_TFT_SCL 17
|
|
#define PIN_TFT_SDA 38
|
|
#define PIN_TFT_CS 39
|
|
#define PIN_TFT_DC 16
|
|
#define PIN_TFT_RST 4
|
|
#define PIN_TFT_EN 6
|
|
#define PIN_TFT_BL 5
|
|
#ifndef PIN_TFT_EN_ACTIVE
|
|
#define PIN_TFT_EN_ACTIVE LOW
|
|
#endif
|
|
#ifndef PIN_TFT_BL_ACTIVE
|
|
#define PIN_TFT_BL_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, spi_ends = 0;
|
|
inline unsigned spi_transfers = 0, pixel_bytes = 0, reset_count = 0;
|
|
inline unsigned clock_edges = 0, command_bits = 0, data_bits = 0;
|
|
inline unsigned per_command[2][4] = {};
|
|
inline uint8_t command_value = 0, spi_command = 0;
|
|
inline bool transaction = false, spi_active = false, init_done = false, rail_on = false;
|
|
inline const char* scenario = "valid";
|
|
inline uint32_t read_value = 0;
|
|
inline bool is(const char* value) { return strcmp(scenario, value) == 0; }
|
|
inline bool automatic() {
|
|
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
inline void panelPin(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_EN || pin == PIN_TFT_BL || pin == 30)
|
|
&& "touch only panel pins and explicitly shared power");
|
|
}
|
|
inline int commandIndex() {
|
|
if (command_value == 0x04) return 0;
|
|
if (command_value == 0xDA) return 1;
|
|
if (command_value == 0xDB) return 2;
|
|
assert(command_value == 0xDC); return 3;
|
|
}
|
|
inline unsigned responseBits() { return command_value == 0x04 ? 24 : 8; }
|
|
inline void pinMode(int pin, int mode) {
|
|
panelPin(pin); ++gpio_events; modes[pin] = mode;
|
|
if (pin == PIN_TFT_SDA && (mode == INPUT_PULLUP || mode == INPUT_PULLDOWN)) {
|
|
assert(transaction && command_bits == 8);
|
|
const unsigned occurrence = per_command[mode == INPUT_PULLUP ? 0 : 1][commandIndex()]++;
|
|
uint32_t id = 0x300101;
|
|
if (is("wrong-id")) id = 0x123456;
|
|
if (is("biased") && mode == INPUT_PULLDOWN) id = 0x300102;
|
|
if (is("noise") && occurrence % 3 == 2) id = 0x300102;
|
|
if (is("prime") && occurrence % 3 == 0) id = 0;
|
|
if (is("recover") && reset_count == 1) id = 0;
|
|
if ((is("fallback") || is("bad-fallback")) && command_value == 0x04) id = 0;
|
|
if (is("bad-fallback") && command_value == 0xDC) id = 0x300102;
|
|
if (is("floating")) id = mode == INPUT_PULLUP ? 0xFFFFFF : 0;
|
|
if (is("stuck-high")) id = 0xFFFFFF;
|
|
if (is("stuck-low")) id = 0;
|
|
if (command_value == 0x04) read_value = id;
|
|
else read_value = (id >> ((3-commandIndex())*8)) & 0xFF;
|
|
}
|
|
}
|
|
inline void digitalWrite(int pin, int value) {
|
|
panelPin(pin); ++gpio_events;
|
|
if (automatic() && pin == PIN_TFT_BL && value == PIN_TFT_BL_ACTIVE)
|
|
assert(init_done && pixel_bytes >= 220u*128u*2u && "backlight only after initialization and clear");
|
|
if (pin == PIN_TFT_EN) {
|
|
if (automatic() && rail_on && value != PIN_TFT_EN_ACTIVE) {
|
|
assert(!spi_active);
|
|
for (int p : {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL})
|
|
assert(modes[p] == INPUT && "park bus before removing panel power");
|
|
}
|
|
rail_on = value == PIN_TFT_EN_ACTIVE;
|
|
}
|
|
if (pin == PIN_TFT_RST && value == LOW) reset_low_at = now_us;
|
|
if (pin == PIN_TFT_RST && value == HIGH && levels[pin] == LOW) {
|
|
reset_low_us = now_us-reset_low_at; reset_high_at = now_us; ++reset_count;
|
|
}
|
|
if (pin == PIN_TFT_CS && value == LOW && levels[pin] == HIGH && !spi_active) {
|
|
assert(automatic());
|
|
assert(rail_on && levels[PIN_TFT_BL] != PIN_TFT_BL_ACTIVE);
|
|
assert(now_us-reset_high_at >= 120000 && "probe reset settle");
|
|
assert(reset_low_us >= 120000 && "probe reset pulse");
|
|
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 == responseBits());
|
|
assert(clock_edges == 8+responseBits()+(command_value == 0x04 ? 1 : 0));
|
|
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 == 8+data_bits+(command_value == 0x04 ? 1 : 0));
|
|
assert(data_bits < responseBits() && levels[PIN_TFT_SCL] == LOW && "sample after turnaround falling edge");
|
|
++reads;
|
|
return (read_value >> (responseBits()-1-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 <Arduino.h>
|
|
#define HSPI 1
|
|
#define MSBFIRST 1
|
|
#define SPI_MODE0 0
|
|
struct SPISettings { SPISettings(int, int, int) {} };
|
|
struct SPIClass {
|
|
explicit SPIClass(int) {}
|
|
bool begin(int sck, int miso, int mosi, int cs) {
|
|
assert(sck == PIN_TFT_SCL && miso == -1 && mosi == PIN_TFT_SDA && cs == PIN_TFT_CS);
|
|
++spi_begins; spi_active = true; init_done = false; pixel_bytes = 0;
|
|
modes[sck] = modes[mosi] = OUTPUT;
|
|
return !is("init-fail") && !(is("wake-fail") && spi_begins == 2);
|
|
}
|
|
void end() { assert(spi_active); ++spi_ends; spi_active = false; }
|
|
void beginTransaction(SPISettings) { assert(spi_active); }
|
|
void endTransaction() { assert(spi_active); }
|
|
uint8_t transfer(uint8_t data) {
|
|
assert(spi_active && rail_on);
|
|
assert(levels[PIN_TFT_CS] == LOW); ++spi_transfers;
|
|
if (levels[PIN_TFT_DC] == LOW) {
|
|
assert(now_us-reset_high_at >= 120000);
|
|
spi_command = data;
|
|
if (data == 0x29) init_done = true;
|
|
} else if (spi_command == 0x2C) {
|
|
assert(init_done); ++pixel_bytes;
|
|
}
|
|
return 0;
|
|
}
|
|
};
|
|
'''
|
|
|
|
MAIN = r'''
|
|
#include "NV3001BDisplay.h"
|
|
void frame(NV3001BDisplay& 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("b",10); d.fillRect(0,0,1,1); d.drawRect(0,0,1,1);
|
|
d.drawXbm(0,0,bits,1,1); d.endFrame(); assert(d.getTextWidth("a") == 6);
|
|
}
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2); scenario = argv[1];
|
|
RefCountedDigitalPin power(30, HIGH); power.begin();
|
|
NV3001BDisplay d(&power);
|
|
if (automatic()) {
|
|
const auto events = gpio_events; d.turnOn(); frame(d); d.turnOff();
|
|
assert(gpio_events == events && spi_begins == 0 && spi_transfers == 0);
|
|
}
|
|
const bool absent = is("floating") || is("stuck-high") || is("stuck-low")
|
|
|| is("noise") || is("biased") || is("wrong-id") || is("bad-fallback");
|
|
const bool begun = d.begin();
|
|
if (automatic() && is("init-fail")) {
|
|
assert(!begun && !d.isOn() && spi_begins == 1 && spi_transfers == 0 && spi_ends == 1);
|
|
assert(transactions == 6 && levels[30] == LOW && !rail_on);
|
|
assert(levels[PIN_TFT_BL] != PIN_TFT_BL_ACTIVE && levels[PIN_TFT_RST] == LOW);
|
|
const auto events=gpio_events; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin());
|
|
assert(gpio_events == events);
|
|
return 0;
|
|
}
|
|
if (automatic() && absent) {
|
|
assert(!begun && !d.isOn() && "invalid panel must remain headless");
|
|
assert(spi_begins == 0 && spi_transfers == 0 && spi_ends == 0);
|
|
assert(transactions == 48 && reads == 576 && now_us < 800000);
|
|
assert(levels[PIN_TFT_EN] != PIN_TFT_EN_ACTIVE && levels[PIN_TFT_RST] == LOW);
|
|
assert(levels[PIN_TFT_BL] != PIN_TFT_BL_ACTIVE && levels[30] == LOW);
|
|
for (int p : {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL}) assert(modes[p] == INPUT);
|
|
const auto events=gpio_events; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin());
|
|
assert(events == gpio_events && spi_begins == 0 && transactions == 48);
|
|
return 0;
|
|
}
|
|
assert(begun && d.isOn() && init_done && spi_begins == 1);
|
|
assert(levels[PIN_TFT_BL] == PIN_TFT_BL_ACTIVE && levels[30] == HIGH);
|
|
if (automatic()) assert(transactions == (is("fallback") ? 24u : is("recover") ? 30u : 6u));
|
|
else assert(transactions == 0 && reads == 0);
|
|
frame(d); const auto count=transactions, tx=spi_transfers;
|
|
assert(d.begin() && spi_begins == 1 && spi_transfers == tx);
|
|
d.turnOff(); assert(!d.isOn() && levels[PIN_TFT_EN] != PIN_TFT_EN_ACTIVE && levels[30] == LOW);
|
|
frame(d); assert(spi_transfers == tx);
|
|
if (automatic()) {
|
|
assert(spi_ends == 1 && !spi_active);
|
|
for (int p : {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL}) assert(modes[p] == INPUT);
|
|
}
|
|
d.turnOn();
|
|
if (automatic() && is("wake-fail")) {
|
|
assert(!d.isOn() && spi_begins == 2 && spi_ends == 2 && transactions == count);
|
|
assert(!rail_on && levels[30] == LOW && levels[PIN_TFT_RST] == LOW);
|
|
const auto events=gpio_events; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin());
|
|
assert(gpio_events == events && spi_transfers == tx);
|
|
return 0;
|
|
}
|
|
assert(d.isOn() && spi_begins == 2 && transactions == count);
|
|
assert(levels[PIN_TFT_BL] == PIN_TFT_BL_ACTIVE && levels[30] == HIGH);
|
|
frame(d); assert(spi_transfers > tx);
|
|
d.turnOff(); d.turnOff(); assert(levels[30] == LOW);
|
|
}
|
|
'''
|
|
|
|
|
|
class RC32DisplayDetectionTest(unittest.TestCase):
|
|
def build(self, work, flags=(), *, old_bias=False, old_turnaround=False):
|
|
work.mkdir()
|
|
for filename in ("NV3001BDisplay.cpp", "NV3001BDisplay.h", "RC32DisplayProbe.h"):
|
|
source = (UI / filename).read_text()
|
|
if old_bias and filename == "RC32DisplayProbe.h":
|
|
mutated = source.replace("ids[0] == ids[1]", "true")
|
|
self.assertNotEqual(mutated, source)
|
|
source = mutated
|
|
if old_turnaround and filename == "RC32DisplayProbe.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)
|
|
for filename, source in {"Arduino.h": ARDUINO, "SPI.h": SPI, "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", "-Wno-type-limits", *sanitizer, *flags,
|
|
"-I"+str(work), "-I"+str(UI), "-I"+str(ROOT / "src"),
|
|
str(work / "main.cpp"), str(work / "NV3001BDisplay.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_absent_unstable_wrong_controller_stays_headless(self):
|
|
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
|
binary = self.build(Path(directory)/"auto", ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=1"])
|
|
self.assertIn(b"meshcore.capability.display.autodetect.rc32.v1", binary.read_bytes())
|
|
for scenario in ("floating", "stuck-high", "stuck-low", "noise", "biased", "wrong-id", "bad-fallback", "init-fail"):
|
|
with self.subTest(scenario=scenario): self.run_case(binary, scenario)
|
|
|
|
def test_both_id_protocols_recovery_and_sleep_wake_preserve_screen(self):
|
|
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
|
for index, active in enumerate(([], ["-DPIN_TFT_EN_ACTIVE=HIGH", "-DPIN_TFT_BL_ACTIVE=LOW"])):
|
|
flags = ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=1", *active]
|
|
binary = self.build(Path(directory)/str(index), flags)
|
|
for scenario in ("valid", "prime", "fallback", "recover", "wake-fail"):
|
|
with self.subTest(active=active, scenario=scenario): self.run_case(binary, scenario)
|
|
|
|
def test_legacy_and_other_nv3001b_boards_do_not_probe(self):
|
|
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
|
for index, flags in enumerate((["-DHELTEC_RC32"], ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=0"],
|
|
["-DRC32_DISPLAY_AUTODETECT=1"])):
|
|
binary = self.build(Path(directory)/str(index), flags)
|
|
self.run_case(binary, "valid")
|
|
self.assertNotIn(b"meshcore.capability.display.autodetect.rc32.v1", binary.read_bytes())
|
|
|
|
def test_negative_controls_detect_bias_and_sda_turnaround_regressions(self):
|
|
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
|
flags = ["-DHELTEC_RC32", "-DRC32_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)/"turnaround", flags, old_turnaround=True)
|
|
self.run_case(binary, "valid", negative="release SDA before command falling edge")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|