129 lines
6.6 KiB
Python
129 lines
6.6 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, '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 <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': 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()
|