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:
|
The configured swipe order is:
|
||||||
|
|
||||||
1. Fleet — one row per printer: status, job name, progress, finish time and
|
1. Fleet — one steam gauge per printer, matching the progress arcs on the
|
||||||
time remaining. The header counts printers that need attention, are printing,
|
printer pages. The needle is print progress, and the arc color follows
|
||||||
paused, idle, offline or still syncing. Tap a row to open that printer.
|
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.
|
2. PrintsCharming — P1S, Office, both four-slot AMS units.
|
||||||
3. Polly-Pocket — A1 mini, Office, external spool.
|
3. Polly-Pocket — A1 mini, Office, external spool.
|
||||||
4. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS.
|
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
|
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
|
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
|
same navigation. Swipes propagate through the fleet gauges and filament
|
||||||
slots. A swipe consumes the rest of that touch so dragging across a row or a
|
slots. A swipe consumes the rest of that touch so dragging across a gauge or a
|
||||||
navigation button does not also click it.
|
navigation button does not also click it.
|
||||||
|
|
||||||
FDM pages show print status, job name, progress, nozzle and bed current/target
|
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).
|
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;
|
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
|
limit depends on the board's available RAM and flash, and adding printers requires
|
||||||
regenerating and installing the firmware. Seven printers are configured; generation
|
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.
|
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;
|
lv_obj_t *elapsed = nullptr;
|
||||||
};
|
};
|
||||||
struct OverviewWidgets {
|
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) {
|
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) {
|
inline void render_overview(const Snapshot &data, const OverviewWidgets &widgets) {
|
||||||
const bool live = data.live();
|
const bool live = data.live();
|
||||||
const uint32_t color = data.status_color();
|
const uint32_t color = data.status_color();
|
||||||
set_text(widgets.status, data.status_text());
|
const float shown = live && std::isfinite(data.progress)
|
||||||
lv_obj_set_style_text_color(widgets.status, lv_color_hex(color), 0);
|
? std::clamp(data.progress, 0.0f, 100.0f) : NAN;
|
||||||
if (widgets.task) set_text(widgets.task, data.overview_task());
|
lv_arc_set_value(widgets.arc, live ? gauge_value(data.progress, 100) : 0);
|
||||||
set_text(widgets.meta, data.overview_meta());
|
lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(color), LV_PART_INDICATOR);
|
||||||
lv_bar_set_value(widgets.progress, live ? gauge_value(data.progress, 100) : 0, LV_ANIM_OFF);
|
set_text(widgets.value, number(shown));
|
||||||
lv_obj_set_style_bg_color(widgets.progress, lv_color_hex(color), LV_PART_INDICATOR);
|
lv_obj_set_style_text_color(widgets.value, lv_color_hex(live ? color : MUTED), 0);
|
||||||
lv_obj_set_style_bg_color(widgets.accent, lv_color_hex(color), 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) {
|
inline void render_fleet_summary(lv_obj_t *label, const FleetCounts &counts) {
|
||||||
|
|||||||
@@ -245,48 +245,56 @@ def snapshot_lines(printer, expressions, trays):
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def overview_layout(count):
|
def overview_slots(count):
|
||||||
row_h = (436 - 46) // count
|
"""Place one steam gauge per printer. Full-size arcs match the printer pages."""
|
||||||
return row_h, row_h >= 52
|
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']
|
pid = printer['id']
|
||||||
name_w, status_x, status_w = (430, 448, 312) if two_line else (240, 258, 150)
|
scale = arc / 154
|
||||||
meta_x, meta_w, meta_y = (448, 312, 28) if two_line else (414, 346, 8)
|
value_font = 32 if arc >= 140 else 18
|
||||||
children = [
|
detail_font = 14 if arc >= 140 else 12
|
||||||
{'obj': {'id': f'{pid}_overview_accent', 'x': 0, 'y': 8, 'width': 4,
|
name_font = 14 if arc >= 140 else 12
|
||||||
'height': max(row_h - 16, 8), 'bg_color': MUTED, 'bg_opa': 'COVER',
|
open_page = [{'lvgl.page.show': f'{pid}_page'}]
|
||||||
'border_width': 0, 'radius': 2, 'pad_all': 0, 'scrollable': False,
|
inner = max(8, int(round(13 * scale)))
|
||||||
'gesture_bubble': True}},
|
return [
|
||||||
label(printer['name'], 12, 2 if two_line else 6, name_w, 18 if two_line else 16),
|
{'arc': {'id': f'{pid}_overview_arc', 'x': x + int(round(8 * scale)), 'y': y,
|
||||||
label('SYNCING', status_x, 6 if two_line else 8, status_w, 16 if two_line else 14,
|
'width': arc, 'height': arc, 'min_value': 0, 'max_value': 100, 'value': 0,
|
||||||
id=f'{pid}_overview_status', text_align='RIGHT', text_color=MUTED),
|
'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):
|
def overview_page(printers):
|
||||||
row_h, two_line = overview_layout(len(printers))
|
widgets = []
|
||||||
widgets = [
|
for printer, (x, y, arc) in zip(printers, overview_slots(len(printers))):
|
||||||
label('FLEET', 16, 8, 180, 26, text_color=GREEN),
|
widgets += overview_gauge(printer, x, y, arc)
|
||||||
label('SYNCING', 210, 16, 574, 14, id='fleet_summary', text_align='RIGHT', text_color=MUTED),
|
widgets.append(label('SYNCING', 90, 404, 620, 14, id='fleet_summary',
|
||||||
]
|
text_align='CENTER', text_color=MUTED))
|
||||||
widgets += [overview_row(printer, 46 + index * row_h, row_h, two_line)
|
|
||||||
for index, printer in enumerate(printers)]
|
|
||||||
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
|
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
|
||||||
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
|
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
|
||||||
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
|
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
|
||||||
@@ -308,16 +316,14 @@ def overview_page(printers):
|
|||||||
|
|
||||||
|
|
||||||
def fleet_refresh_lines(snapshots):
|
def fleet_refresh_lines(snapshots):
|
||||||
_, two_line = overview_layout(len(snapshots))
|
|
||||||
lines = ['bambu_display::FleetCounts counts;']
|
lines = ['bambu_display::FleetCounts counts;']
|
||||||
for pid, snapshot in snapshots:
|
for pid, snapshot in snapshots:
|
||||||
task = f'id({pid}_overview_task)' if two_line else 'nullptr'
|
|
||||||
lines.append('{')
|
lines.append('{')
|
||||||
lines.extend(snapshot)
|
lines.extend(snapshot)
|
||||||
lines.append('bambu_display::tally(counts, data);')
|
lines.append('bambu_display::tally(counts, data);')
|
||||||
lines.append('bambu_display::render_overview(data, {' + ', '.join([
|
lines.append('bambu_display::render_overview(data, {' + ', '.join([
|
||||||
f'id({pid}_overview_status)', task, f'id({pid}_overview_meta)',
|
f'id({pid}_overview_arc)', f'id({pid}_overview_value)',
|
||||||
f'id({pid}_overview_progress)', f'id({pid}_overview_accent)']) + '});')
|
f'id({pid}_overview_detail)']) + '});')
|
||||||
lines.append('}')
|
lines.append('}')
|
||||||
lines.append('bambu_display::render_fleet_summary(id(fleet_summary), counts);')
|
lines.append('bambu_display::render_fleet_summary(id(fleet_summary), counts);')
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@ inline void setup() {
|
|||||||
for printer in printers:
|
for printer in printers:
|
||||||
pid = printer['id']
|
pid = printer['id']
|
||||||
steps.append({'lambda': Lambda(
|
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); }}')})
|
f'{{ ESP_LOGE("preview", "Fleet row render failed: {pid}"); exit(7); }}')})
|
||||||
for printer in printers:
|
for printer in printers:
|
||||||
steps += [{'lvgl.page.show': printer['id'] + '_page'},
|
steps += [{'lvgl.page.show': printer['id'] + '_page'},
|
||||||
|
|||||||
@@ -152,19 +152,19 @@ class FleetTests(unittest.TestCase):
|
|||||||
page = package['lvgl']['pages'][0]
|
page = package['lvgl']['pages'][0]
|
||||||
self.assertEqual(page['id'], 'fleet_page')
|
self.assertEqual(page['id'], 'fleet_page')
|
||||||
self.assertFalse(page.get('skip'))
|
self.assertFalse(page.get('skip'))
|
||||||
rows = [widget['obj'] for widget in page['widgets']
|
arcs = [widget['arc'] for widget in page['widgets'] if 'arc' in widget]
|
||||||
if 'obj' in widget and str(widget['obj'].get('id', '')).endswith('_overview_row')]
|
self.assertEqual([arc['id'][:-len('_overview_arc')] for arc in arcs],
|
||||||
self.assertEqual([row['id'][:-len('_overview_row')] for row in rows],
|
|
||||||
[printer['id'] for printer in self.printers])
|
[printer['id'] for printer in self.printers])
|
||||||
previous = 46
|
for arc, printer in zip(arcs, self.printers):
|
||||||
for row, printer in zip(rows, self.printers):
|
self.assertEqual(arc['on_click'], [{'lvgl.page.show': printer['id'] + '_page'}])
|
||||||
self.assertEqual(row['on_click'], [{'lvgl.page.show': printer['id'] + '_page'}])
|
self.assertEqual(arc['width'], 154)
|
||||||
self.assertGreaterEqual(row['y'], previous)
|
self.assertEqual(arc['start_angle'], 135)
|
||||||
self.assertLessEqual(row['y'] + row['height'], 440)
|
self.assertLessEqual(arc['x'] + arc['width'], 800)
|
||||||
previous = row['y'] + row['height']
|
self.assertLessEqual(arc['y'] + arc['height'], 480)
|
||||||
code = next(script['then'][0]['lambda'] for script in package['script'] if script['id'] == 'fleet_refresh')
|
code = next(script['then'][0]['lambda'] for script in package['script'] if script['id'] == 'fleet_refresh')
|
||||||
for printer in self.printers:
|
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']]
|
shown = [step['if']['condition']['lvgl.page.is_showing'] for step in package['interval'][0]['then']]
|
||||||
self.assertIn('fleet_page', shown)
|
self.assertIn('fleet_page', shown)
|
||||||
charming = next(item for item in package['lvgl']['pages'] if item['id'] == 'prints_charming_page')
|
charming = next(item for item in package['lvgl']['pages'] if item['id'] == 'prints_charming_page')
|
||||||
|
|||||||
Reference in New Issue
Block a user