#!/usr/bin/env python3 """Render the real LVGL pages with demo telemetry using ESPHome's headless SDL host.""" import argparse import json from pathlib import Path import subprocess import tempfile from PIL import Image import yaml from generate import Dumper, HERE, Lambda, generate def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output', type=Path, default=HERE / 'previews') args = parser.parse_args() printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers'] package = generate(printers) demo_values = {'progress': 67, 'nozzle': 218, 'nozzle_target': 220, 'bed': 60, 'bed_target': 60, 'time_left': 1.75, 'layer': 134, 'total_layers': 200, 'fan': 80, 'left_nozzle': 219, 'left_target': 220, 'right_nozzle': 37, 'right_target': 0} demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2', 'stage': 'printing', 'online': 'on', 'print_error': 'off', 'hms_error': 'off', 'remaining_unit': 'h', 'nozzle_unit': '°C', 'bed_unit': '°C'} colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF', '#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF'] for printer in printers: prefix = printer['id'] + '_' for section in ('sensor', 'text_sensor'): for entry in package[section]: if not entry['id'].startswith(prefix): continue key = entry['id'][len(prefix):] if section == 'sensor': value = str(demo_values[key]) else: if key.startswith('tray_'): _, index, attribute = key.split('_') text = {'type': 'PLA', 'color': colors[int(index) % len(colors)], 'active': 'on' if index == '0' else 'off'}[attribute] else: text = demo_text[key] value = 'std::string(' + json.dumps(text, ensure_ascii=False) + ')' entry.clear() entry.update({'platform': 'template', 'id': prefix + key, 'internal': True, 'lambda': Lambda('return ' + value + ';'), 'update_interval': '60s'}) for script in package['script']: code = script['then'][0]['lambda'] script['then'][0]['lambda'] = Lambda(code.replace( 'id(ha_api).is_connected_with_state_subscription()', 'true')) args.output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='bambu-lvgl-preview-') as work: root = Path(work) # Inject real LVGL pointer samples; test bubbling through cards/buttons # without a GUI window or a physical touchscreen. touch_header = root / 'preview_touch.h' touch_header.write_text('''#pragma once #include namespace bambu_preview { inline int x = 0, y = 0; inline bool pressed = false; inline void setup() { auto *input = lv_indev_create(); lv_indev_set_type(input, LV_INDEV_TYPE_POINTER); lv_indev_set_display(input, lv_display_get_default()); lv_indev_set_read_cb(input, [](lv_indev_t *, lv_indev_data_t *data) { data->point.x = x; data->point.y = y; data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; }); } } ''') steps = [{'component.update': entry['id']} for section in ('sensor', 'text_sensor') for entry in package[section]] steps += [{'delay': '1s'}] for printer in printers: steps += [{'lvgl.page.show': printer['id'] + '_page'}, {'script.execute': printer['id'] + '_refresh'}, {'delay': '300ms'}, {'snapshot.take': {'id': 'panel_display', 'filename': printer['id'] + '.bmp'}}] steps += [{'lvgl.page.show': printers[0]['id'] + '_page'}, {'delay': '250ms'}, {'lambda': Lambda('bambu_preview::setup();')}] if len(printers) > 1: # First -> second -> first -> last -> first, then a swipe starting # on NEXT must advance exactly once even when released over PREV. gestures = [(650, 180, 180, printers[1]['id']), (180, 650, 180, printers[0]['id']), (180, 650, 180, printers[-1]['id']), (650, 180, 180, printers[0]['id']), (730, 60, 460, printers[1]['id'])] for start, end, y, expected in gestures: steps += [ {'lambda': Lambda(f'bambu_preview::x = {start}; bambu_preview::y = {y}; bambu_preview::pressed = true;')}, {'delay': '70ms'}, {'lambda': Lambda(f'bambu_preview::x = {end};')}, {'delay': '70ms'}, {'lambda': Lambda('bambu_preview::pressed = false;')}, {'delay': '350ms'}, {'lambda': Lambda(f'if (!id({expected}_page).is_showing()) {{ ESP_LOGE("preview", "Swipe navigation failed"); exit(2); }}')}, ] steps += [{'lambda': Lambda('exit(0);')}] config = {'esphome': {'name': 'bambu-display-preview', 'includes': [str(HERE / 'dashboard.h'), str(touch_header)], 'on_boot': {'priority': -100, 'then': steps}}, 'host': {}, 'logger': {'level': 'WARN'}, 'display': [{'platform': 'sdl', 'id': 'panel_display', 'dimensions': '800x480', 'headless': True, 'auto_clear_enabled': False, 'update_interval': 'never'}], 'lvgl': {'id': 'dashboard', 'displays': ['panel_display'], 'buffer_size': '100%', 'color_depth': 16, 'bg_color': 0x0D131B, 'text_color': 0xEDF3F7, 'default_font': 'montserrat_18', **package.pop('lvgl')}, **package} filename = root / 'preview.yaml' filename.write_text(yaml.dump(config, Dumper=Dumper, sort_keys=False, allow_unicode=True)) subprocess.run(['esphome', 'compile', str(filename)], check=True) binary = root / '.esphome/build/bambu-display-preview/.pioenvs/bambu-display-preview/program' subprocess.run([str(binary)], check=True, timeout=30, cwd=root) for printer in printers: bmp = root / '.esphome/snapshots/bambu-display-preview' / (printer['id'] + '.bmp') with Image.open(bmp) as image: image.save(args.output / (printer['id'] + '.png')) print(f'Saved {len(printers)} demo screenshots to {args.output}') print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.') if __name__ == '__main__': main()