Show each printer on the fleet page as a status gauge.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,9 +17,12 @@ Home Assistant must be running and its printer integrations must have current pr
|
||||
|
||||
The configured swipe order is:
|
||||
|
||||
1. Fleet — one row per printer: status, job name, progress, finish time and
|
||||
time remaining. The header counts printers that need attention, are printing,
|
||||
paused, idle, offline or still syncing. Tap a row to open that printer.
|
||||
1. Fleet — one steam gauge per printer, matching the progress arcs on the
|
||||
printer pages. The needle is print progress, and the arc color follows
|
||||
status: green while printing, amber while paused, red for an error. The
|
||||
center is the percent complete and the caption is the status. A line under
|
||||
the gauges counts printers that need attention, are printing, paused, idle,
|
||||
offline or still syncing. Tap a gauge to open that printer.
|
||||
2. PrintsCharming — P1S, Office, both four-slot AMS units.
|
||||
3. Polly-Pocket — A1 mini, Office, external spool.
|
||||
4. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS.
|
||||
@@ -36,8 +39,8 @@ The configured swipe order is:
|
||||
|
||||
Swipe left for the next page or right for the previous page. Navigation wraps
|
||||
from the last printer back to the fleet page. Previous/Next buttons provide the
|
||||
same navigation. Swipes propagate through the fleet rows, arc gauges and filament
|
||||
slots. A swipe consumes the rest of that touch so dragging across a row or a
|
||||
same navigation. Swipes propagate through the fleet gauges and filament
|
||||
slots. A swipe consumes the rest of that touch so dragging across a gauge or a
|
||||
navigation button does not also click it.
|
||||
|
||||
FDM pages show print status, job name, progress, nozzle and bed current/target
|
||||
@@ -146,7 +149,7 @@ python esphome/bambu-display/generate.py --check
|
||||
|
||||
The generator requires PyYAML (already included in ESPHome's Python environment).
|
||||
Do not hand-edit `generated.yaml`. There is no two-printer limit in the generator;
|
||||
adding a printer creates its own subscriptions, widgets, swipe page and fleet row. The practical
|
||||
adding a printer creates its own subscriptions, widgets, swipe page and fleet gauge. The practical
|
||||
limit depends on the board's available RAM and flash, and adding printers requires
|
||||
regenerating and installing the firmware. Seven printers are configured; generation
|
||||
is also tested with nine. Each Bambu printer's filament strip supports up to eight slots.
|
||||
|
||||
@@ -290,7 +290,7 @@ struct Widgets {
|
||||
lv_obj_t *elapsed = nullptr;
|
||||
};
|
||||
struct OverviewWidgets {
|
||||
lv_obj_t *status, *task, *meta, *progress, *accent;
|
||||
lv_obj_t *arc, *value, *detail;
|
||||
};
|
||||
|
||||
inline void set_text(lv_obj_t *label, const std::string &text) {
|
||||
@@ -362,13 +362,14 @@ inline void render(const Snapshot &data, const Widgets &widgets) {
|
||||
inline void render_overview(const Snapshot &data, const OverviewWidgets &widgets) {
|
||||
const bool live = data.live();
|
||||
const uint32_t color = data.status_color();
|
||||
set_text(widgets.status, data.status_text());
|
||||
lv_obj_set_style_text_color(widgets.status, lv_color_hex(color), 0);
|
||||
if (widgets.task) set_text(widgets.task, data.overview_task());
|
||||
set_text(widgets.meta, data.overview_meta());
|
||||
lv_bar_set_value(widgets.progress, live ? gauge_value(data.progress, 100) : 0, LV_ANIM_OFF);
|
||||
lv_obj_set_style_bg_color(widgets.progress, lv_color_hex(color), LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_color(widgets.accent, lv_color_hex(color), 0);
|
||||
const float shown = live && std::isfinite(data.progress)
|
||||
? std::clamp(data.progress, 0.0f, 100.0f) : NAN;
|
||||
lv_arc_set_value(widgets.arc, live ? gauge_value(data.progress, 100) : 0);
|
||||
lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(color), LV_PART_INDICATOR);
|
||||
set_text(widgets.value, number(shown));
|
||||
lv_obj_set_style_text_color(widgets.value, lv_color_hex(live ? color : MUTED), 0);
|
||||
set_text(widgets.detail, data.status_text());
|
||||
lv_obj_set_style_text_color(widgets.detail, lv_color_hex(color), 0);
|
||||
}
|
||||
|
||||
inline void render_fleet_summary(lv_obj_t *label, const FleetCounts &counts) {
|
||||
|
||||
@@ -245,48 +245,56 @@ def snapshot_lines(printer, expressions, trays):
|
||||
return lines
|
||||
|
||||
|
||||
def overview_layout(count):
|
||||
row_h = (436 - 46) // count
|
||||
return row_h, row_h >= 52
|
||||
def overview_slots(count):
|
||||
"""Place one steam gauge per printer. Full-size arcs match the printer pages."""
|
||||
columns = 4 if count > 4 else max(count, 1)
|
||||
rows = (count + columns - 1) // columns
|
||||
top, bottom = 8, 400
|
||||
arc = min(154, (800 // columns) - 20, (bottom - top) // rows - 36)
|
||||
col_w = 800 // columns
|
||||
row_h = (bottom - top) // rows
|
||||
slots = []
|
||||
for index in range(count):
|
||||
row, col = divmod(index, columns)
|
||||
in_row = min(columns, count - row * columns)
|
||||
x0 = (800 - in_row * col_w) // 2
|
||||
gauge_w = arc + 16
|
||||
slots.append((x0 + col * col_w + (col_w - gauge_w) // 2,
|
||||
top + row * row_h, arc))
|
||||
return slots
|
||||
|
||||
|
||||
def overview_row(printer, y, row_h, two_line):
|
||||
def overview_gauge(printer, x, y, arc):
|
||||
pid = printer['id']
|
||||
name_w, status_x, status_w = (430, 448, 312) if two_line else (240, 258, 150)
|
||||
meta_x, meta_w, meta_y = (448, 312, 28) if two_line else (414, 346, 8)
|
||||
children = [
|
||||
{'obj': {'id': f'{pid}_overview_accent', 'x': 0, 'y': 8, 'width': 4,
|
||||
'height': max(row_h - 16, 8), 'bg_color': MUTED, 'bg_opa': 'COVER',
|
||||
'border_width': 0, 'radius': 2, 'pad_all': 0, 'scrollable': False,
|
||||
'gesture_bubble': True}},
|
||||
label(printer['name'], 12, 2 if two_line else 6, name_w, 18 if two_line else 16),
|
||||
label('SYNCING', status_x, 6 if two_line else 8, status_w, 16 if two_line else 14,
|
||||
id=f'{pid}_overview_status', text_align='RIGHT', text_color=MUTED),
|
||||
scale = arc / 154
|
||||
value_font = 32 if arc >= 140 else 18
|
||||
detail_font = 14 if arc >= 140 else 12
|
||||
name_font = 14 if arc >= 140 else 12
|
||||
open_page = [{'lvgl.page.show': f'{pid}_page'}]
|
||||
inner = max(8, int(round(13 * scale)))
|
||||
return [
|
||||
{'arc': {'id': f'{pid}_overview_arc', 'x': x + int(round(8 * scale)), 'y': y,
|
||||
'width': arc, 'height': arc, 'min_value': 0, 'max_value': 100, 'value': 0,
|
||||
'start_angle': 135, 'end_angle': 45, 'arc_width': inner, 'arc_color': TRACK,
|
||||
'indicator': {'arc_color': GREEN, 'arc_width': inner}, 'adjustable': False,
|
||||
'gesture_bubble': True, 'clickable': True, 'on_click': open_page}},
|
||||
label('--', x + int(round(14 * scale)), y + int(round(47 * scale)), int(round(142 * scale)),
|
||||
value_font, id=f'{pid}_overview_value', text_align='CENTER',
|
||||
clickable=True, on_click=open_page),
|
||||
label('SYNCING', x + int(round(14 * scale)), y + int(round(86 * scale)),
|
||||
int(round(142 * scale)), detail_font, id=f'{pid}_overview_detail',
|
||||
text_align='CENTER', text_color=MUTED, clickable=True, on_click=open_page),
|
||||
label(printer['name'], x, y + int(round(137 * scale)), arc + 16, name_font,
|
||||
text_align='CENTER', text_color=GREEN, clickable=True, on_click=open_page),
|
||||
]
|
||||
if two_line:
|
||||
children.append(label('Waiting for Home Assistant', 12, 28, 430, 12,
|
||||
id=f'{pid}_overview_task', text_color=MUTED))
|
||||
children += [
|
||||
label('--', meta_x, meta_y, meta_w, 12 if two_line else 14,
|
||||
id=f'{pid}_overview_meta', text_align='RIGHT', text_color=MUTED),
|
||||
{'bar': {'id': f'{pid}_overview_progress', 'x': 12, 'y': row_h - 7, 'width': 748, 'height': 4,
|
||||
'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK,
|
||||
'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}},
|
||||
]
|
||||
return {'obj': {'id': f'{pid}_overview_row', 'x': 8, 'y': y, 'width': 784, 'height': row_h,
|
||||
'bg_color': BG, 'bg_opa': 'COVER', 'border_width': 0, 'pad_all': 0,
|
||||
'radius': 0, 'scrollable': False, 'clickable': True, 'gesture_bubble': True,
|
||||
'on_click': [{'lvgl.page.show': f'{pid}_page'}], 'widgets': children}}
|
||||
|
||||
|
||||
def overview_page(printers):
|
||||
row_h, two_line = overview_layout(len(printers))
|
||||
widgets = [
|
||||
label('FLEET', 16, 8, 180, 26, text_color=GREEN),
|
||||
label('SYNCING', 210, 16, 574, 14, id='fleet_summary', text_align='RIGHT', text_color=MUTED),
|
||||
]
|
||||
widgets += [overview_row(printer, 46 + index * row_h, row_h, two_line)
|
||||
for index, printer in enumerate(printers)]
|
||||
widgets = []
|
||||
for printer, (x, y, arc) in zip(printers, overview_slots(len(printers))):
|
||||
widgets += overview_gauge(printer, x, y, arc)
|
||||
widgets.append(label('SYNCING', 90, 404, 620, 14, id='fleet_summary',
|
||||
text_align='CENTER', text_color=MUTED))
|
||||
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
|
||||
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
|
||||
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
|
||||
@@ -308,16 +316,14 @@ def overview_page(printers):
|
||||
|
||||
|
||||
def fleet_refresh_lines(snapshots):
|
||||
_, two_line = overview_layout(len(snapshots))
|
||||
lines = ['bambu_display::FleetCounts counts;']
|
||||
for pid, snapshot in snapshots:
|
||||
task = f'id({pid}_overview_task)' if two_line else 'nullptr'
|
||||
lines.append('{')
|
||||
lines.extend(snapshot)
|
||||
lines.append('bambu_display::tally(counts, data);')
|
||||
lines.append('bambu_display::render_overview(data, {' + ', '.join([
|
||||
f'id({pid}_overview_status)', task, f'id({pid}_overview_meta)',
|
||||
f'id({pid}_overview_progress)', f'id({pid}_overview_accent)']) + '});')
|
||||
f'id({pid}_overview_arc)', f'id({pid}_overview_value)',
|
||||
f'id({pid}_overview_detail)']) + '});')
|
||||
lines.append('}')
|
||||
lines.append('bambu_display::render_fleet_summary(id(fleet_summary), counts);')
|
||||
return lines
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@ inline void setup() {
|
||||
for printer in printers:
|
||||
pid = printer['id']
|
||||
steps.append({'lambda': Lambda(
|
||||
f'if (std::string(lv_label_get_text(id({pid}_overview_status))) != "PRINTING") '
|
||||
f'if (std::string(lv_label_get_text(id({pid}_overview_detail))) != "PRINTING") '
|
||||
f'{{ ESP_LOGE("preview", "Fleet row render failed: {pid}"); exit(7); }}')})
|
||||
for printer in printers:
|
||||
steps += [{'lvgl.page.show': printer['id'] + '_page'},
|
||||
|
||||
@@ -152,19 +152,19 @@ class FleetTests(unittest.TestCase):
|
||||
page = package['lvgl']['pages'][0]
|
||||
self.assertEqual(page['id'], 'fleet_page')
|
||||
self.assertFalse(page.get('skip'))
|
||||
rows = [widget['obj'] for widget in page['widgets']
|
||||
if 'obj' in widget and str(widget['obj'].get('id', '')).endswith('_overview_row')]
|
||||
self.assertEqual([row['id'][:-len('_overview_row')] for row in rows],
|
||||
arcs = [widget['arc'] for widget in page['widgets'] if 'arc' in widget]
|
||||
self.assertEqual([arc['id'][:-len('_overview_arc')] for arc in arcs],
|
||||
[printer['id'] for printer in self.printers])
|
||||
previous = 46
|
||||
for row, printer in zip(rows, self.printers):
|
||||
self.assertEqual(row['on_click'], [{'lvgl.page.show': printer['id'] + '_page'}])
|
||||
self.assertGreaterEqual(row['y'], previous)
|
||||
self.assertLessEqual(row['y'] + row['height'], 440)
|
||||
previous = row['y'] + row['height']
|
||||
for arc, printer in zip(arcs, self.printers):
|
||||
self.assertEqual(arc['on_click'], [{'lvgl.page.show': printer['id'] + '_page'}])
|
||||
self.assertEqual(arc['width'], 154)
|
||||
self.assertEqual(arc['start_angle'], 135)
|
||||
self.assertLessEqual(arc['x'] + arc['width'], 800)
|
||||
self.assertLessEqual(arc['y'] + arc['height'], 480)
|
||||
code = next(script['then'][0]['lambda'] for script in package['script'] if script['id'] == 'fleet_refresh')
|
||||
for printer in self.printers:
|
||||
self.assertIn(f'id({printer["id"]}_overview_status)', code)
|
||||
self.assertIn(f'id({printer["id"]}_overview_arc)', code)
|
||||
self.assertIn(f'id({printer["id"]}_overview_detail)', code)
|
||||
shown = [step['if']['condition']['lvgl.page.is_showing'] for step in package['interval'][0]['then']]
|
||||
self.assertIn('fleet_page', shown)
|
||||
charming = next(item for item in package['lvgl']['pages'] if item['id'] == 'prints_charming_page')
|
||||
|
||||
Reference in New Issue
Block a user