Add a fleet overview page that shows every printer's status at once.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Joshua King
2026-10-20 13:05:32 -04:00
co-authored by Cursor
parent 0ec94a909e
commit 43f416ab69
7 changed files with 1283 additions and 61 deletions
+71
View File
@@ -7,6 +7,7 @@
#include <cstdint>
#include <cstdlib>
#include <string>
#include <utility>
#include <vector>
namespace bambu_display {
@@ -212,6 +213,19 @@ struct Snapshot {
for (char &c : text) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return text;
}
std::string overview_task() const {
if (!live()) return connected ? "Waiting for printer data" : "Waiting for Home Assistant";
return valid_text(task) ? task : "No active print";
}
std::string overview_meta() const {
if (!live()) return "--";
const std::string progress_text = std::isfinite(progress)
? number(std::clamp(progress, 0.0f, 100.0f)) + "%" : "--";
const std::string time_line = remaining(time_left, remaining_unit);
if (!finish_time.empty() && time_line != "--")
return progress_text + " " + finish_time + " " + time_line;
return progress_text + " " + time_line;
}
uint32_t status_color() const {
if (!live()) return MUTED;
const auto state = lower(status);
@@ -223,6 +237,42 @@ struct Snapshot {
}
};
struct FleetCounts {
int attention = 0, printing = 0, paused = 0, idle = 0, offline = 0, syncing = 0;
};
inline void tally(FleetCounts &counts, const Snapshot &data) {
if (!data.connected || lower(data.online) == "off" || lower(data.online) == "false") {
++counts.offline;
return;
}
if (!data.live()) {
++counts.syncing;
return;
}
if (data.has_print_error() || is_on(data.hms_error) || lower(data.status) == "failed") {
++counts.attention;
return;
}
const auto state = lower(data.status);
if (state == "running" || state == "printing") ++counts.printing;
else if (state == "pause" || state == "paused" || state == "pausing") ++counts.paused;
else ++counts.idle;
}
inline std::string fleet_summary(const FleetCounts &counts) {
const std::pair<int, const char *> items[] = {
{counts.attention, "attention"}, {counts.printing, "printing"}, {counts.paused, "paused"},
{counts.idle, "idle"}, {counts.offline, "offline"}, {counts.syncing, "syncing"}};
std::string text;
for (const auto &item : items) {
if (!item.first) continue;
if (!text.empty()) text += ", ";
text += std::to_string(item.first) + " " + item.second;
}
return text.empty() ? "Syncing" : text;
}
} // namespace bambu_display
#ifdef USE_LVGL
@@ -239,6 +289,9 @@ struct Widgets {
GaugeWidgets uv{}, layer{};
lv_obj_t *elapsed = nullptr;
};
struct OverviewWidgets {
lv_obj_t *status, *task, *meta, *progress, *accent;
};
inline void set_text(lv_obj_t *label, const std::string &text) {
if (text != lv_label_get_text(label)) lv_label_set_text(label, text.c_str());
@@ -306,5 +359,23 @@ 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);
}
inline void render_fleet_summary(lv_obj_t *label, const FleetCounts &counts) {
set_text(label, fleet_summary(counts));
const uint32_t color = counts.attention ? RED : counts.printing ? GREEN : MUTED;
lv_obj_set_style_text_color(label, lv_color_hex(color), 0);
}
} // namespace bambu_display
#endif