Files
Flipdot-display/tools/display_smoke_test.py
T
mangavalkandClaude Opus 5.5 abd63d070f Tests against lost bytes in dense frames
- test_nano: every hex digit of B data is one drawDots() call, never single
  drawPixel() calls (the slow path that broke dense images in 2.19.2)
- tools/display_smoke_test.py: all yellow, a dense image and a checkerboard
  in live mode on the real display; fails on any frame check problem (wrong
  image, no answer, Nano restart, ERR reply)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:30:31 +02:00

106 lines
3.8 KiB
Python

"""Checks on the real display that the Nano gets every frame right, also very dense ones.
Run it after changes to the Nano firmware, the driver (lib/BROSE9323) or the frames.
python tools/display_smoke_test.py [ESP32 address, default 10.1.1.198]
It shows a few test images in live mode (all yellow, a dense image, a checkerboard), waits
for the frame check of each (the Nano's checksum of what it received) and then goes back
to normal. It fails when the frame check found a wrong image, a missing answer, a Nano
restart or an ERR reply (e.g. "Line too long": lost bytes). This is the speed test the PC
tests can't do: with 2.19.2 the Nano drew dense images slower than they arrived, which
broke all yellow. The dots flip for about 20 s. Only the Python standard library is used.
"""
import json
import sys
import time
import urllib.error
import urllib.request
ESP = 'http://' + (sys.argv[1] if len(sys.argv) > 1 else '10.1.1.198')
W, H, ROW_BYTES = 84, 19, 11
CHECK_TIMEOUT = 20 # s: the ESP asks every 3 s at most, the answer takes up to 2 s
COUNTERS = ('mismatches', 'no_reply', 'errors', 'restarts', 'repairs')
def request(path, body=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(ESP + path, data=data, headers={'Content-Type': 'application/json'})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
sys.exit('%s: %s' % (path, e.read().decode(errors='replace')))
def image(dot):
"""Bitmap hex (logic/bitmap_format.h: rows of 11 bytes, MSB = left dot) of dot(x, y)."""
out = []
for y in range(H):
for i in range(ROW_BYTES):
v = 0
for k in range(8):
x = i * 8 + k
if x < W and dot(x, y):
v |= 0x80 >> k
out.append('%02x' % v)
return ''.join(out)
TESTS = [
('all yellow', image(lambda x, y: True)),
# About 90 % of the dots, scattered: like the panda bitmap that failed
('dense image', image(lambda x, y: (x * 7 + y * 13) % 11 != 0)),
('checkerboard', image(lambda x, y: (x + y) % 2 == 0)),
]
def link():
return request('/api/status')['nano_link']
def answered(counts):
return counts['checks'] + counts['mismatches'] + counts['no_reply']
def main():
status = request('/api/status')
if not status['power_enabled']:
sys.exit('The Power switch is off (Home Assistant): live mode is not possible')
effect_s = request('/api/settings')['drive']['effect_time'] / 1000
start = link()
print('Firmware %s, frame check before: %s' % (status['version'], start))
failed = False
try:
for name, data in TESTS:
before = link()
request('/api/live', {'data': data})
deadline = time.time() + CHECK_TIMEOUT
now = before
while answered(now) == answered(before) and time.time() < deadline:
time.sleep(0.5)
now = link()
if answered(now) == answered(before):
print('%-13s no frame check within %d s' % (name, CHECK_TIMEOUT))
failed = True
continue
bad = {c: now[c] - before[c] for c in COUNTERS if now[c] != before[c]}
print('%-13s %s' % (name, 'ok' if not bad else 'FAILED %s %s' % (bad, now['last_error'])))
failed |= bool(bad)
time.sleep(effect_s + 1) # let it finish flipping, so each image can be seen
finally:
request('/api/live/stop', {})
end = link()
bad = {c: end[c] - start[c] for c in COUNTERS if end[c] != start[c]}
if bad:
print('Frame check after: %s, last error: %s' % (bad, end['last_error']))
failed = True
print('FAILED' if failed else 'All frames arrived right')
sys.exit(1 if failed else 0)
if __name__ == '__main__':
main()