Put ESP Screens in English and keep layouts when entity IDs change (0.2.40)
Everything a user sees or reads is now English: both boards' labels, status replies and logs, the ESP Screens editor and Alerts cheatsheet, and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md and TROUBLESHOOTING.md replace the Dutch file names). Labels follow Home Assistant's own English names (Heat/Cool modes, Heating/Cooling actions, Away, Returning to dock, weather conditions), dates use English abbreviations and the top bar writes numbers the English way. Docs use HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools). Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed. Firmware 0.2.34 names its entities in English. Home Assistant registers a renamed ESPHome entity under a new entity ID, so the inbox text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and the layout stored under the old ID would be lost. The manager now recognises the screen by its device, during a run or after a restart, and moves its layout and update history along; the Update button and the nightly round follow the screen through the rename. Screens that still run 0.2.33 or older keep working: both languages are matched. Main is the only release branch from now on. Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards: see docs/TEST_RESULTS_0240.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+31
-31
@@ -13,8 +13,8 @@ import re
|
||||
import statistics
|
||||
import time
|
||||
|
||||
TARGETS = [('linksboven', (20, 20)), ('rechtsboven', (299, 20)),
|
||||
('rechtsonder', (299, 219)), ('linksonder', (20, 219)), ('midden', (160, 120))]
|
||||
TARGETS = [('top-left', (20, 20)), ('top-right', (299, 20)),
|
||||
('bottom-right', (299, 219)), ('bottom-left', (20, 219)), ('center', (160, 120))]
|
||||
BOUNDS = (280, 3860, 340, 3860)
|
||||
PATTERN = re.compile(r'press native=(\d+),(\d+) raw=(\d+),(\d+) pressure=(\d+) calibration=([01])')
|
||||
|
||||
@@ -25,7 +25,7 @@ def parse_touch(line):
|
||||
return None
|
||||
nx, ny, rx, ry, pressure, isolated = map(int, match.groups())
|
||||
if not isolated:
|
||||
raise ValueError('Geen geïsoleerd meetscherm: start met CALIBRATION_ON_BOOT=true. Geen data opgeslagen.')
|
||||
raise ValueError('Not an isolated measurement screen: start with CALIBRATION_ON_BOOT=true. No data saved.')
|
||||
return dict(native_x=nx, native_y=ny, raw_x=rx, raw_y=ry, pressure=pressure)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def write_file(path, contents, replace=False):
|
||||
path = Path(path)
|
||||
if path.exists():
|
||||
if not replace:
|
||||
raise ValueError(f'{path} bestaat al; kies een andere naam of expliciet --replace.')
|
||||
raise ValueError(f'{path} already exists; choose a different name or pass --replace explicitly.')
|
||||
backup = path.with_name(path.name + '.bak-' + datetime.now().strftime('%Y%m%d-%H%M%S-%f'))
|
||||
backup.write_bytes(path.read_bytes())
|
||||
# Write completely before replacing an existing configuration.
|
||||
@@ -48,20 +48,20 @@ def write_file(path, contents, replace=False):
|
||||
|
||||
def validate(data):
|
||||
if data.get('schema') != 1 or data.get('screen') != [320, 240]:
|
||||
raise ValueError('Verwacht schema 1 en landschap 320×240; andere oriëntaties zijn niet ondersteund.')
|
||||
raise ValueError('Expected schema 1 and landscape 320×240; other orientations are not supported.')
|
||||
points = data.get('points', [])
|
||||
if len(points) != len(TARGETS):
|
||||
raise ValueError('Alle vier hoeken én het onafhankelijke middenpunt zijn nodig.')
|
||||
raise ValueError('All four corners and the independent center point are required.')
|
||||
for point, (name, xy) in zip(points, TARGETS):
|
||||
if point.get('name') != name or point.get('screen') != list(xy):
|
||||
raise ValueError('Meetpunten/volgorde komen niet overeen met de wizard.')
|
||||
raise ValueError("Measurement points/order don't match the wizard.")
|
||||
if len(point.get('samples', [])) < 3:
|
||||
raise ValueError(f'{name}: minimaal drie afzonderlijke tikken nodig.')
|
||||
raise ValueError(f'{name}: at least three separate taps required.')
|
||||
for sample in point['samples']:
|
||||
for key, maximum in [('raw_x', 4095), ('raw_y', 4095), ('native_x', 239), ('native_y', 319), ('pressure', 8190)]:
|
||||
value = sample.get(key)
|
||||
if type(value) is not int or not 0 <= value <= maximum:
|
||||
raise ValueError(f'{name}: ongeldige {key}.')
|
||||
raise ValueError(f'{name}: invalid {key}.')
|
||||
return points
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def least_squares(rows, values):
|
||||
matrix[column], matrix[pivot] = matrix[pivot], matrix[column]
|
||||
scale = matrix[column][column]
|
||||
if abs(scale) < 1e-9:
|
||||
raise ValueError('Meetpunten zijn niet onafhankelijk; waarschijnlijk dezelfde hoek meermaals gemeten.')
|
||||
raise ValueError('Measurement points are not independent; likely the same corner was measured more than once.')
|
||||
matrix[column] = [v / scale for v in matrix[column]]
|
||||
for row in range(3):
|
||||
if row != column:
|
||||
@@ -104,10 +104,10 @@ def fit(data, tolerance=12):
|
||||
cx, cy = least_squares(rows, values_x), least_squares(rows, values_y)
|
||||
coefficients = [cx[0] / 4095, cx[1] / 4095, cx[2], cy[0] / 4095, cy[1] / 4095, cy[2]]
|
||||
if not all(math.isfinite(v) for v in coefficients):
|
||||
raise ValueError('Ongeldige kalibratiecoëfficiënten.')
|
||||
raise ValueError('Invalid calibration coefficients.')
|
||||
# Reject captures that amplify small sensor jitter into huge screen movements.
|
||||
if max(abs(v) for v in (coefficients[0], coefficients[1], coefficients[3], coefficients[4])) > 4:
|
||||
raise ValueError('Te klein meetbereik; tik de echte schermhoeken aan.')
|
||||
raise ValueError('Measurement range too small; tap the actual screen corners.')
|
||||
report = []
|
||||
for point, median in zip(points, medians):
|
||||
predicted = project(coefficients, *median)
|
||||
@@ -115,14 +115,14 @@ def fit(data, tolerance=12):
|
||||
spread = max(math.dist(project(coefficients, s['raw_x'], s['raw_y']), predicted) for s in point['samples'])
|
||||
report.append(dict(name=point['name'], median=list(median), error_px=round(error, 2), spread_px=round(spread, 2)))
|
||||
if error > tolerance or spread > 18:
|
||||
raise ValueError(f"{point['name']}: fout {error:.1f}px, spreiding {spread:.1f}px. "
|
||||
'Meet opnieuw; een instabiel paneel niet wegkalibreren.')
|
||||
raise ValueError(f"{point['name']}: error {error:.1f}px, spread {spread:.1f}px. "
|
||||
"Measure again; don't calibrate away an unstable panel.")
|
||||
return coefficients, report
|
||||
|
||||
|
||||
def calibration_yaml(coefficients):
|
||||
names = ['TOUCH_AFFINE_XX', 'TOUCH_AFFINE_XY', 'TOUCH_AFFINE_XC', 'TOUCH_AFFINE_YX', 'TOUCH_AFFINE_YY', 'TOUCH_AFFINE_YC']
|
||||
lines = ['# Gemeten met tools/calibrate.py; uitsluitend voor dit fysieke paneel.', 'substitutions:']
|
||||
lines = ['# Measured with tools/calibrate.py; only valid for this physical panel.', 'substitutions:']
|
||||
for name, value in zip(['TOUCH_CAL_X_MIN', 'TOUCH_CAL_X_MAX', 'TOUCH_CAL_Y_MIN', 'TOUCH_CAL_Y_MAX'], BOUNDS):
|
||||
lines.append(f' {name}: "{value}"')
|
||||
lines += [f' {name}: "{value:.9f}"' for name, value in zip(names, coefficients)]
|
||||
@@ -138,21 +138,21 @@ def verify(data, tolerance=12):
|
||||
spread = max(math.dist(p, center) for p in measured)
|
||||
results.append(dict(name=point['name'], error_px=round(error, 2), spread_px=round(spread, 2)))
|
||||
if error > tolerance or spread > 18:
|
||||
raise ValueError(f"{point['name']}: echte firmwarecoördinaten wijken {error:.1f}px af (spreiding {spread:.1f}px).")
|
||||
raise ValueError(f"{point['name']}: real firmware coordinates deviate {error:.1f}px (spread {spread:.1f}px).")
|
||||
return results
|
||||
|
||||
|
||||
def capture(args):
|
||||
import serial
|
||||
if args.output.exists():
|
||||
raise ValueError('Meetbestand bestaat al; kies een nieuwe naam.')
|
||||
raise ValueError('Measurement file already exists; choose a new name.')
|
||||
data = dict(schema=1, device_name=args.device_name, screen=[320, 240],
|
||||
created_at=datetime.now(timezone.utc).isoformat(), points=[])
|
||||
print('Alleen voor 320×240 / LVGL 90° / swap_xy=false / mirror_x=true / mirror_y=false.')
|
||||
print('Het scherm moet de VIJF kruisjes tonen. Sluit andere seriële loglezers af.')
|
||||
print('Only for 320×240 / LVGL 90° / swap_xy=false / mirror_x=true / mirror_y=false.')
|
||||
print('The screen must show the FIVE crosshairs. Close other serial log readers.')
|
||||
with serial.Serial(args.port, 115200, timeout=0.2) as port:
|
||||
for name, xy in TARGETS:
|
||||
input(f'\nVolgende punt: {name} {xy}. Druk ENTER en tik daarna alleen dit kruisje {args.samples} keer: ')
|
||||
input(f'\nNext point: {name} {xy}. Press ENTER, then tap only this crosshair {args.samples} times: ')
|
||||
port.reset_input_buffer()
|
||||
samples = []
|
||||
deadline = time.monotonic() + args.timeout
|
||||
@@ -164,19 +164,19 @@ def capture(args):
|
||||
last = time.monotonic()
|
||||
print(f' {name}: {len(samples)}/{args.samples}', flush=True)
|
||||
if len(samples) != args.samples:
|
||||
raise ValueError(f'{name}: te weinig geldige tikken. Controleer de firmware, het meetscherm en USB-poort.')
|
||||
raise ValueError(f'{name}: not enough valid taps. Check the firmware, the measurement screen, and the USB port.')
|
||||
data['points'].append(dict(name=name, screen=list(xy), samples=samples))
|
||||
write_file(args.output, json.dumps(data, indent=2) + '\n')
|
||||
print(f'Meting opgeslagen: {args.output}. Het script heeft niets geflasht of aangestuurd.')
|
||||
print(f'Measurement saved: {args.output}. The script did not flash or control anything.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest='command', required=True)
|
||||
c = commands.add_parser('capture', help='Lees geleide tikken via USB; firmware moet al het meetscherm tonen.')
|
||||
c = commands.add_parser('capture', help='Read guided taps over USB; firmware must already show the measurement screen.')
|
||||
c.add_argument('--port', required=True)
|
||||
c.add_argument('--output', type=Path, required=True)
|
||||
c.add_argument('--device-name', default='onbekend')
|
||||
c.add_argument('--device-name', default='unknown')
|
||||
c.add_argument('--samples', type=int, choices=range(3, 10), default=3)
|
||||
c.add_argument('--timeout', type=float, default=60)
|
||||
for name in ('fit', 'verify'):
|
||||
@@ -189,24 +189,24 @@ def main():
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.command == 'capture':
|
||||
if args.timeout <= 0: raise ValueError('Timeout moet positief zijn.')
|
||||
if args.timeout <= 0: raise ValueError('Timeout must be positive.')
|
||||
capture(args)
|
||||
else:
|
||||
if args.tolerance <= 0 or not math.isfinite(args.tolerance): raise ValueError('Tolerantie moet eindig en positief zijn.')
|
||||
if args.tolerance <= 0 or not math.isfinite(args.tolerance): raise ValueError('Tolerance must be finite and positive.')
|
||||
data = json.loads(args.input.read_text())
|
||||
if args.command == 'fit':
|
||||
coefficients, report = fit(data, args.tolerance)
|
||||
write_file(args.output, calibration_yaml(coefficients), args.replace)
|
||||
print(f'Kalibratie geschreven: {args.output}. Het middenpunt is onafhankelijk gecontroleerd.')
|
||||
print(f'Calibration written: {args.output}. The center point was independently checked.')
|
||||
else:
|
||||
report = verify(data, args.tolerance)
|
||||
print('PASS: onafhankelijke fysieke controle van de geflashte coördinaten.')
|
||||
print('PASS: independent physical check of the flashed coordinates.')
|
||||
for item in report:
|
||||
print(f"{item['name']}: fout={item['error_px']}px, spreiding={item['spread_px']}px")
|
||||
print(f"{item['name']}: error={item['error_px']}px, spread={item['spread_px']}px")
|
||||
except (ValueError, OSError, KeyError, TypeError, EOFError) as exc:
|
||||
parser.exit(1, f'Fout: {exc}\n')
|
||||
parser.exit(1, f'Error: {exc}\n')
|
||||
except KeyboardInterrupt:
|
||||
parser.exit(130, 'Afgebroken; geen gedeeltelijke meting opgeslagen.\n')
|
||||
parser.exit(130, 'Aborted; no partial measurement saved.\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -36,9 +36,9 @@ def export(root, output):
|
||||
paths.append(path)
|
||||
for path in paths:
|
||||
if path.is_symlink() or not path.resolve().is_relative_to(root):
|
||||
raise ValueError(f'Geen bestanden buiten de bronmap of symlinks toegestaan: {path}')
|
||||
raise ValueError(f'No files outside the source folder or symlinks allowed: {path}')
|
||||
if not path.is_file():
|
||||
raise ValueError(f'Ontbrekend bundelbestand: {path}')
|
||||
raise ValueError(f'Missing bundle file: {path}')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Exclusive mode refuses to replace a previously delivered bundle.
|
||||
with zipfile.ZipFile(output, 'x', zipfile.ZIP_DEFLATED) as archive:
|
||||
@@ -53,10 +53,10 @@ def main():
|
||||
parser.add_argument('--output', type=Path, default=ROOT / 'dist' / 'cyd-starter.zip')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(f'Deel deze ZIP: {export(ROOT, args.output)}')
|
||||
print('Geen lokale secrets, device.yaml, kalibratiemetingen, logs of firmwarebackups opgenomen.')
|
||||
print(f'Share this ZIP: {export(ROOT, args.output)}')
|
||||
print('No local secrets, device.yaml, calibration measurements, logs or firmware backups included.')
|
||||
except (OSError, ValueError) as exc:
|
||||
parser.exit(1, f'Fout: {exc}\n')
|
||||
parser.exit(1, f'Error: {exc}\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -14,7 +14,7 @@ def generate(board):
|
||||
defaults += '\n' + (ROOT / 'calibration.example.yaml').read_text()
|
||||
replacements = dict(re.findall(r'^ (\w+): ("[^"\n]*")', defaults, re.M))
|
||||
replacements.update(DYNAMIC_TILES='"true"', DASHBOARD_INBOX_INTERNAL='"false"', DIRECT_ACTIONS='"true"',
|
||||
ROOM_NAME='"Kies tegels in HA"', TILE_COUNT='"1"')
|
||||
ROOM_NAME='"Choose tiles in HA"', TILE_COUNT='"1"')
|
||||
s = re.sub(r'^ (\w+): ("[^"\n]*")', lambda m: f' {m[1]}: {replacements.get(m[1],m[2])}', s, flags=re.M)
|
||||
# The owner's wizard YAML supplies wifi/API/OTA credentials. No shared keys.
|
||||
s = re.sub(r'^ encryption:\n key: !secret api_encryption_key\n', '', s, flags=re.M)
|
||||
|
||||
+12
-12
@@ -11,24 +11,24 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def create(directory, name, friendly_name, board="cyd"):
|
||||
if not re.fullmatch(r'[a-z][a-z0-9-]{0,30}', name) or name.endswith('-'):
|
||||
raise ValueError('Gebruik een unieke naam van 1–31 tekens: kleine letters, cijfers en koppeltekens.')
|
||||
raise ValueError('Use a unique name of 1-31 characters: lowercase letters, digits, and dashes.')
|
||||
if board not in ("cyd", "guition"):
|
||||
raise ValueError("Onbekend bordprofiel")
|
||||
raise ValueError("Unknown board profile")
|
||||
import json
|
||||
device = (ROOT / ('guition-device.example.yaml' if board == 'guition' else 'device.example.yaml')).read_text().replace('"guition-new"' if board == 'guition' else '"cyd-new"', json.dumps(name), 1)
|
||||
device = device.replace('"Mijn Guition"' if board == 'guition' else '"Mijn CYD"', json.dumps(friendly_name, ensure_ascii=False), 1)
|
||||
device = device.replace('"My Guition"' if board == 'guition' else '"My CYD"', json.dumps(friendly_name, ensure_ascii=False), 1)
|
||||
secret_text = (ROOT / 'secrets.yaml.example').read_text()
|
||||
replacements = [base64.b64encode(secrets.token_bytes(32)).decode(), secrets.token_urlsafe(24), secrets.token_urlsafe(18)]
|
||||
for value in replacements:
|
||||
secret_text = secret_text.replace('WORDT_UNIEK_GEGENEREERD', value, 1)
|
||||
secret_text = secret_text.replace('WILL_BE_UNIQUELY_GENERATED', value, 1)
|
||||
files = {'guition-device.yaml' if board == 'guition' else 'device.yaml': device, 'secrets.yaml': secret_text}
|
||||
if board == 'cyd': files['calibration.yaml'] = (ROOT / 'calibration.example.yaml').read_text()
|
||||
directory = Path(directory)
|
||||
if not directory.is_dir():
|
||||
raise ValueError('De doelmap moet al bestaan en de uitgepakte code bevatten.')
|
||||
raise ValueError('The target folder must already exist and contain the unpacked code.')
|
||||
for name in files:
|
||||
if (directory / name).exists():
|
||||
raise ValueError(f'{directory / name} bestaat al; niets overschreven. Gebruik een nieuwe kopie van de map.')
|
||||
raise ValueError(f'{directory / name} already exists; nothing overwritten. Use a fresh copy of the folder.')
|
||||
created = []
|
||||
try:
|
||||
for name, contents in files.items():
|
||||
@@ -47,18 +47,18 @@ def create(directory, name, friendly_name, board="cyd"):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--board', choices=['cyd', 'guition'], default='cyd')
|
||||
parser.add_argument('--name', required=True, help='Unieke ESPHome-naam, bijvoorbeeld display-keuken')
|
||||
parser.add_argument('--name', required=True, help='Unique ESPHome name, for example display-kitchen')
|
||||
parser.add_argument('--friendly-name')
|
||||
parser.add_argument('--directory', type=Path, default=ROOT)
|
||||
args = parser.parse_args()
|
||||
args.friendly_name = args.friendly_name or ('Mijn Guition' if args.board == 'guition' else 'Mijn CYD')
|
||||
args.friendly_name = args.friendly_name or ('My Guition' if args.board == 'guition' else 'My CYD')
|
||||
try:
|
||||
for path in create(args.directory, args.name, args.friendly_name, args.board):
|
||||
print(f'Gemaakt: {path}')
|
||||
print('Vul wifi lokaal in secrets.yaml in. Sleutels zijn uniek gegenereerd en worden niet getoond.')
|
||||
print('Volg docs/GUITION.md; flash guition-device.yaml.' if args.board == 'guition' else 'Volg README.md; flash device.yaml.')
|
||||
print(f'Created: {path}')
|
||||
print('Fill in Wi-Fi locally in secrets.yaml. Keys are uniquely generated and are not shown.')
|
||||
print('Follow docs/GUITION.md; flash guition-device.yaml.' if args.board == 'guition' else 'Follow README.md; flash device.yaml.')
|
||||
except (ValueError, OSError) as exc:
|
||||
parser.exit(1, f'Fout: {exc}\n')
|
||||
parser.exit(1, f'Error: {exc}\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+12
-12
@@ -29,13 +29,13 @@ BANDS = {'guition': (480, 72), 'cyd': (320, 40)}
|
||||
|
||||
# (slug, title, items) with items as C++ expressions of the render lambda's helpers.
|
||||
SCENARIOS = (
|
||||
('1-klok', 'Studio 1', ['builtin(Kind::clock)']),
|
||||
('2-mockup', 'Woonkamer', ['text(0xF050F, "—")', 'text(0xF050F, "21,3 °C")', 'text(0xF058E, "—")', 'ago(0xF0004, 26 * 3600)']),
|
||||
('3-deur-alarm', 'Studio 1', ['text(0xF081C, "Open", 0xFFB300)', 'text(0xF068A, "Afwezig", 0x43A047)', 'builtin(Kind::clock)']),
|
||||
('4-analoog-datum', 'Keuken', ['text(0xF050F, "23,5 °C")', 'builtin(Kind::date)', 'builtin(Kind::analog)']),
|
||||
('5-thuis-weer-stroom', 'Studio 1', ['text(0xF0849, "4")', 'text(0xF0595, "22 °C")', 'text(0xF0241, "1.249 W")', 'builtin(Kind::clock)']),
|
||||
('6-te-vol', 'Woonkamer beneden', ['text(0xF050F, "21,3 °C")', 'text(0xF058E, "48%")', 'ago(0xF0D91, 5 * 60)', 'text(0xF0241, "78,0 W")', 'builtin(Kind::analog)', 'builtin(Kind::clock)']),
|
||||
('7-mensen', 'Studio 1', ['text(0xF0004, "Weg")', 'text(0xF0849, "4")', 'builtin(Kind::analog)', 'builtin(Kind::clock)']),
|
||||
('1-clock', 'Studio 1', ['builtin(Kind::clock)']),
|
||||
('2-mockup', 'Living room', ['text(0xF050F, "—")', 'text(0xF050F, "21.3 °C")', 'text(0xF058E, "—")', 'ago(0xF0004, 26 * 3600)']),
|
||||
('3-door-alarm', 'Studio 1', ['text(0xF081C, "Open", 0xFFB300)', 'text(0xF068A, "Armed away", 0x43A047)', 'builtin(Kind::clock)']),
|
||||
('4-analog-date', 'Kitchen', ['text(0xF050F, "23.5 °C")', 'builtin(Kind::date)', 'builtin(Kind::analog)']),
|
||||
('5-home-weather-power', 'Studio 1', ['text(0xF0849, "4")', 'text(0xF0595, "22 °C")', 'text(0xF0241, "1,249 W")', 'builtin(Kind::clock)']),
|
||||
('6-too-full', 'Living room downstairs', ['text(0xF050F, "21.3 °C")', 'text(0xF058E, "48%")', 'ago(0xF0D91, 5 * 60)', 'text(0xF0241, "78.0 W")', 'builtin(Kind::analog)', 'builtin(Kind::clock)']),
|
||||
('7-people', 'Studio 1', ['text(0xF0004, "Away")', 'text(0xF0849, "4")', 'builtin(Kind::analog)', 'builtin(Kind::clock)']),
|
||||
)
|
||||
|
||||
def font_blocks(board):
|
||||
@@ -152,7 +152,7 @@ for (auto &scenario : scenarios) {{
|
||||
lv_draw_buf_destroy(buf);
|
||||
}}
|
||||
}}
|
||||
ESP_LOGI("render", "klaar");
|
||||
ESP_LOGI("render", "done");
|
||||
fflush(stdout);
|
||||
exit(0);
|
||||
'''
|
||||
@@ -192,7 +192,7 @@ api:
|
||||
|
||||
text:
|
||||
- platform: smart_display
|
||||
name: "Tegelinstellingen"
|
||||
name: "Tile settings"
|
||||
|
||||
display:
|
||||
- platform: sdl
|
||||
@@ -272,7 +272,7 @@ def main():
|
||||
parser.add_argument('--esphome', default=os.environ.get('ESPHOME') or shutil.which('esphome') or str(Path.home() / '.local/pipx/venvs/esphome/bin/esphome'))
|
||||
args = parser.parse_args()
|
||||
if not shutil.which('sdl2-config'):
|
||||
raise SystemExit('SDL2 ontbreekt: installeer het eerst (bijvoorbeeld brew install sdl2).')
|
||||
raise SystemExit('SDL2 is missing: install it first (for example brew install sdl2).')
|
||||
out = args.out.resolve()
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for old in out.glob('*.p[np][gm]'):
|
||||
@@ -288,10 +288,10 @@ def main():
|
||||
build = subprocess.run([args.esphome, 'compile', str(config)], capture_output=True, text=True)
|
||||
if build.returncode:
|
||||
sys.stdout.write(build.stdout[-4000:] + build.stderr[-2000:])
|
||||
raise SystemExit('Build mislukt.')
|
||||
raise SystemExit('Build failed.')
|
||||
program = next(WORK.glob('.esphome/build/render-topbar/.pioenvs/render-topbar/program'), None)
|
||||
if not program:
|
||||
raise SystemExit('Geen host-programma gevonden na de build.')
|
||||
raise SystemExit('No host program found after the build.')
|
||||
subprocess.run([str(program)], capture_output=True, timeout=60, check=True)
|
||||
print(sheet(out))
|
||||
|
||||
|
||||
+17
-17
@@ -8,8 +8,8 @@ import statistics
|
||||
import time
|
||||
from calibrate import write_file
|
||||
|
||||
TARGETS = [('linksboven', (24, 24)), ('rechtsboven', (455, 24)),
|
||||
('rechtsonder', (455, 455)), ('linksonder', (24, 455)), ('midden', (240, 240))]
|
||||
TARGETS = [('top-left', (24, 24)), ('top-right', (455, 24)),
|
||||
('bottom-right', (455, 455)), ('bottom-left', (24, 455)), ('center', (240, 240))]
|
||||
PATTERN = re.compile(r'GT911 press x=(\d+) y=(\d+) test=([01])')
|
||||
|
||||
|
||||
@@ -18,35 +18,35 @@ def screen_point(x, y, rotation=0):
|
||||
if rotation == 90: return y, 479-x
|
||||
if rotation == 180: return 479-x, 479-y
|
||||
if rotation == 270: return 479-y, x
|
||||
raise ValueError('Rotatie moet 0, 90, 180 of 270 zijn.')
|
||||
raise ValueError('Rotation must be 0, 90, 180, or 270.')
|
||||
|
||||
|
||||
def verify(data):
|
||||
if data.get('screen') != [480,480] or len(data.get('points',[])) != 5:
|
||||
raise ValueError('Verwacht vijf meetpunten op een 480x480 GT911-scherm.')
|
||||
raise ValueError('Expected five measurement points on a 480x480 GT911 screen.')
|
||||
result=[]
|
||||
for p,(name,target) in zip(data['points'],TARGETS):
|
||||
if p.get('name') != name or len(p.get('samples',[])) != 3:
|
||||
raise ValueError('Volg de vijf doelen; ieder doel vereist drie aparte tikken.')
|
||||
raise ValueError('Follow the five targets; each target requires three separate taps.')
|
||||
samples=[]
|
||||
for xy in p['samples']:
|
||||
if len(xy)!=2 or any(type(v) is not int or not 0<=v<480 for v in xy):
|
||||
raise ValueError('Touchcoördinaten vallen buiten 480x480.')
|
||||
raise ValueError('Touch coordinates fall outside 480x480.')
|
||||
samples.append(screen_point(*xy, data.get('rotation',0)))
|
||||
center=tuple(statistics.median(x[i] for x in samples) for i in (0,1))
|
||||
error=math.dist(center,target)
|
||||
spread=max(math.dist(x,center) for x in samples)
|
||||
result.append(dict(name=name,error_px=round(error,1),spread_px=round(spread,1)))
|
||||
if error>16 or spread>18:
|
||||
raise ValueError(f'{name}: fout {error:.1f}px / spreiding {spread:.1f}px. Controleer rotatie en GT911-transform; geen ADC-fit gebruiken.')
|
||||
raise ValueError(f'{name}: error {error:.1f}px / spread {spread:.1f}px. Check rotation and the GT911 transform; do not use an ADC fit.')
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--port', help='USB-poort; firmware moet het GT911-meetscherm tonen')
|
||||
parser.add_argument('--port', help='USB port; firmware must show the GT911 measurement screen')
|
||||
parser.add_argument('--output',type=Path,default=Path('measurements-guition.json'))
|
||||
parser.add_argument('--input',type=Path,help='Controleer een bestaand meetbestand zonder USB')
|
||||
parser.add_argument('--input',type=Path,help='Check an existing measurement file without USB')
|
||||
parser.add_argument('--rotation',type=int,choices=(0,90,180,270),default=0)
|
||||
args=parser.parse_args()
|
||||
try:
|
||||
@@ -54,30 +54,30 @@ def main():
|
||||
data=json.loads(args.input.read_text())
|
||||
else:
|
||||
import serial
|
||||
if not args.port: raise ValueError('--port is nodig voor een fysieke meting.')
|
||||
if args.output.exists(): raise ValueError('Meetbestand bestaat al; kies een andere naam.')
|
||||
if not args.port: raise ValueError('--port is required for a physical measurement.')
|
||||
if args.output.exists(): raise ValueError('Measurement file already exists; choose a different name.')
|
||||
data=dict(screen=[480,480],rotation=args.rotation,points=[])
|
||||
with serial.Serial(args.port,115200,timeout=.2) as port:
|
||||
for name,xy in TARGETS:
|
||||
input(f'{name} {xy}: druk Enter en tik daarna drie keer alleen dit kruisje, met loslaten ertussen. ')
|
||||
input(f'{name} {xy}: press Enter, then tap only this crosshair three times, releasing in between. ')
|
||||
port.reset_input_buffer()
|
||||
samples=[]; deadline=time.monotonic()+90; last=0
|
||||
while len(samples)<3 and time.monotonic()<deadline:
|
||||
m=PATTERN.search(port.readline().decode(errors='replace'))
|
||||
if not m: continue
|
||||
if m[3]!='1': raise ValueError('Het geïsoleerde GT911-meetscherm is niet actief.')
|
||||
if m[3]!='1': raise ValueError('The isolated GT911 measurement screen is not active.')
|
||||
if time.monotonic()-last < .4:continue
|
||||
samples.append([int(m[1]),int(m[2])]);last=time.monotonic()
|
||||
print(f' {len(samples)}/3',flush=True)
|
||||
if len(samples)!=3:raise ValueError(f'{name}: onvoldoende tikken ontvangen.')
|
||||
if len(samples)!=3:raise ValueError(f'{name}: not enough taps received.')
|
||||
data['points'].append(dict(name=name,samples=samples))
|
||||
write_file(args.output,json.dumps(data,indent=2)+'\n')
|
||||
for row in verify(data):print(row)
|
||||
print('PASS: alle vijf GT911-doelen geverifieerd; geen HA-acties uitgevoerd.')
|
||||
print('PASS: all five GT911 targets verified; no HA actions performed.')
|
||||
except (ValueError,OSError,TypeError,KeyError,EOFError) as exc:
|
||||
parser.exit(1,f'Fout: {exc}\n')
|
||||
parser.exit(1,f'Error: {exc}\n')
|
||||
except KeyboardInterrupt:
|
||||
parser.exit(130,'Meting afgebroken.\n')
|
||||
parser.exit(130,'Measurement aborted.\n')
|
||||
|
||||
|
||||
if __name__=='__main__': main()
|
||||
|
||||
Reference in New Issue
Block a user