Files
Home-Assistant/esphome/bambu-display/preview.py
T

154 lines
8.7 KiB
Python

#!/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, 'aux_fan': 45, 'exhaust_fan': 30, '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',
'hms_count': '0', 'print_error_message': '', 'print_error_code': '',
'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]
elif key.startswith(('hms_1_', 'hms_2_', 'hms_3_')):
text = ''
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 <lvgl.h>
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': 'color_test_page'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'color_test.bmp'}}]
demo_printer = printers[0]['id']
for key, value in {'hms_error': 'on', 'hms_count': '1', 'hms_1_code': 'DEMO_CODE',
'hms_1_severity': 'common',
'hms_1_message': 'DEMO: A printer maintenance notification. This is synthetic preview data, not an actual fault diagnosis.'}.items():
steps.append({'text_sensor.template.publish': {'id': f'{demo_printer}_{key}', 'state': value}})
steps += [{'lvgl.page.show': demo_printer + '_page'},
{'script.execute': demo_printer + '_refresh'},
{'lvgl.widget.show': demo_printer + '_health_panel'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'health_notice.bmp'}},
{'lvgl.widget.hide': demo_printer + '_health_panel'},
{'text_sensor.template.publish': {'id': demo_printer + '_hms_error', 'state': 'off'}},
{'script.execute': demo_printer + '_refresh'}]
steps += [{'lvgl.page.show': printers[0]['id'] + '_page'}, {'delay': '250ms'},
{'lambda': Lambda('bambu_preview::setup();')}]
for x, y, expect_hidden in [(690, 30, False), (700, 394, True)]:
steps += [
{'lambda': Lambda(f'bambu_preview::x = {x}; bambu_preview::y = {y}; bambu_preview::pressed = true;')},
{'delay': '70ms'}, {'lambda': Lambda('bambu_preview::pressed = false;')}, {'delay': '150ms'},
{'lambda': Lambda(f'if (lv_obj_has_flag(id({demo_printer}_health_panel), LV_OBJ_FLAG_HIDDEN) != {str(expect_hidden).lower()}) {{ ESP_LOGE("preview", "Health panel tap failed"); exit(3); }}')},
]
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'},
'time': [{'platform': 'host', 'id': 'ha_time', 'timezone': 'America/New_York'}],
'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': 0x000000, 'text_color': 0xEEEEEE,
'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 name in [printer['id'] for printer in printers] + ['color_test', 'health_notice']:
bmp = root / '.esphome/snapshots/bambu-display-preview' / (name + '.bmp')
with Image.open(bmp) as image:
image.save(args.output / (name + '.png'))
print(f'Saved {len(printers)} demo screenshots to {args.output}')
print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.')
print('Passed status-tap health panel open and Close-button tests.')
if __name__ == '__main__':
main()