Add stable CYD firmware, USB calibration wizard and complete setup guide

This commit is contained in:
Max Gramser
2026-09-12 10:18:49 +02:00
parent 1eb65d8f71
commit 94546cf727
43 changed files with 9312 additions and 45 deletions
+126
View File
@@ -0,0 +1,126 @@
"""Offline regression tests for installation, calibration and safe export."""
import copy
from pathlib import Path
import re
import sys
import tempfile
import unittest
import zipfile
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import calibrate
import new_device
import export_bundle
def measurements():
points = []
for name, (x, y) in calibrate.TARGETS:
rx = round(280 + y * 3580 / 240)
ry = round(340 + x * 3520 / 320)
samples = [dict(raw_x=rx+d, raw_y=ry+d, native_x=239-y,
native_y=x, pressure=1500) for d in (-2, 0, 2)]
points.append(dict(name=name, screen=[x, y], samples=samples))
return dict(schema=1, screen=[320, 240], points=points)
class CalibrationTests(unittest.TestCase):
def test_identity_fit_and_independent_verification(self):
coefficients, report = calibrate.fit(measurements())
self.assertAlmostEqual(coefficients[0], 1, places=3)
self.assertAlmostEqual(coefficients[4], 1, places=3)
self.assertTrue(all(p['error_px'] < 1 for p in report))
self.assertEqual(len(calibrate.verify(measurements())), 5)
def test_affine_skew_is_corrected(self):
data = measurements()
for point in data['points']:
for s in point['samples']:
x, y = s['raw_x'], s['raw_y']
s['raw_x'] = round(.9*x - .03*y + 190)
s['raw_y'] = round(.04*x + .95*y + 50)
_, report = calibrate.fit(data)
self.assertTrue(all(p['error_px'] < 1 for p in report))
def test_center_is_not_used_to_hide_bad_fit(self):
data = measurements()
for s in data['points'][4]['samples']: s['raw_x'] += 600
with self.assertRaisesRegex(ValueError, 'midden'): calibrate.fit(data)
def test_jitter_rejected(self):
data = measurements()
data['points'][0]['samples'][0]['raw_x'] += 700
with self.assertRaises(ValueError): calibrate.fit(data)
def test_repeated_corner_rejected(self):
data = measurements()
for p in data['points']: p['samples'] = copy.deepcopy(data['points'][0]['samples'])
with self.assertRaises(ValueError): calibrate.fit(data)
def test_wrong_firmware_coordinates_rejected(self):
data = measurements()
for s in data['points'][0]['samples']: s['native_y'] += 40
with self.assertRaises(ValueError): calibrate.verify(data)
def test_capture_requires_isolated_mode(self):
line = 'press native=20,30 raw=500,600 pressure=1500 calibration='
self.assertEqual(calibrate.parse_touch(line+'1')['raw_x'], 500)
with self.assertRaises(ValueError): calibrate.parse_touch(line+'0')
self.assertIsNone(calibrate.parse_touch('unrelated log'))
def test_wrong_screen_or_order_rejected(self):
for field in ('screen', 'order'):
data = measurements()
if field == 'screen': data['screen'] = [240,320]
else: data['points'].reverse()
with self.assertRaises(ValueError): calibrate.fit(data)
def test_output_requires_explicit_replace_and_preserves_backup(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp)/'calibration.yaml'
calibrate.write_file(path, 'old')
with self.assertRaises(ValueError): calibrate.write_file(path, 'new')
calibrate.write_file(path, 'new', replace=True)
self.assertEqual(path.read_text(), 'new')
self.assertEqual(next(Path(tmp).glob('*.bak-*')).read_text(), 'old')
class InstallationTests(unittest.TestCase):
def test_unique_secrets_and_no_overwrite(self):
with tempfile.TemporaryDirectory() as a, tempfile.TemporaryDirectory() as b:
new_device.create(a, 'display-a', 'Display A')
new_device.create(b, 'display-b', 'Display B')
original = (Path(a)/'secrets.yaml').read_text()
self.assertNotEqual(original, (Path(b)/'secrets.yaml').read_text())
self.assertNotIn('WORDT_UNIEK_GEGENEREERD', original)
with self.assertRaises(ValueError): new_device.create(a, 'new', 'New')
self.assertEqual(original, (Path(a)/'secrets.yaml').read_text())
def test_invalid_device_name(self):
with tempfile.TemporaryDirectory() as tmp:
for name in ('../oops', 'UPPER', 'has space', 'trailing-'):
with self.assertRaises(ValueError): new_device.create(tmp, name, 'Test')
self.assertEqual(list(Path(tmp).iterdir()), [])
def test_export_is_neutral_and_has_required_sources(self):
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp)/'starter.zip'
export_bundle.export(ROOT, output)
with zipfile.ZipFile(output) as archive:
names = archive.namelist()
for forbidden in ('secrets.yaml', 'device.yaml', 'calibration.yaml', '.git/', '.esphome/', '.log', '.bin', '.json'):
self.assertFalse(any(('/'+forbidden in n if forbidden.endswith('/') else n.endswith(forbidden) if forbidden.startswith('.') else n.endswith('/'+forbidden)) for n in names), forbidden)
self.assertIn('cyd-display/.gitignore', names)
base = archive.read('cyd-display/home-like-2432s028.yaml').decode()
values = dict(re.findall(r'^ (\w+): "([^"]*)"', base, re.M))
self.assertEqual(values['TOUCH_AFFINE_XX'], '1.000000000')
self.assertEqual(values['DIRECT_ACTIONS'], 'false')
for i in range(1,11):
self.assertRegex(values[f'TILE{i}_ENTITY'], r'\.(voorbeeld_|ongebruikt_)')
archive.extractall(tmp)
new_device.create(Path(tmp)/'cyd-display', 'fresh-board', 'Fresh board')
with self.assertRaises(FileExistsError): export_bundle.export(ROOT, output)
if __name__ == '__main__': unittest.main()