#pragma once #include #include #include #include #include #include #include namespace bambu_display { constexpr uint32_t GREEN = 0x60DE9A; constexpr uint32_t MUTED = 0x91A3B5; constexpr uint32_t RED = 0xFF737B; constexpr uint32_t AMBER = 0xF2C572; constexpr uint32_t TRACK = 0x263442; inline std::string lower(std::string text) { for (char &c : text) c = static_cast(std::tolower(static_cast(c))); return text; } inline bool valid_text(const std::string &text) { const auto value = lower(text); return !value.empty() && value != "unknown" && value != "unavailable" && value != "none"; } inline bool is_on(const std::string &text) { const auto value = lower(text); return value == "on" || value == "true" || value == "1"; } inline std::string humanize(std::string text) { if (!valid_text(text)) return "--"; std::replace(text.begin(), text.end(), '_', ' '); if (!text.empty()) text[0] = static_cast(std::toupper(static_cast(text[0]))); return text; } inline int gauge_value(float value, int maximum) { return std::isfinite(value) ? static_cast(std::clamp(value, 0.0f, float(maximum))) : 0; } inline std::string number(float value) { if (!std::isfinite(value) || value < 0 || value > 1000000) return "--"; char buffer[24]; snprintf(buffer, sizeof(buffer), "%.0f", value); return buffer; } inline std::string temperature(float current, float target, const std::string &unit) { return number(current) + " / " + number(target) + (valid_text(unit) ? " " + unit : ""); } inline std::string remaining(float value, const std::string &unit) { if (!std::isfinite(value) || value < 0) return "--"; double minutes; if (unit == "h") minutes = double(value) * 60; else if (unit == "min") minutes = value; else if (unit == "s") minutes = double(value) / 60; else return "--"; // Never silently interpret hours as minutes. if (minutes > 1000000) return "--"; const auto rounded = static_cast(std::ceil(minutes - 0.001)); char buffer[32]; snprintf(buffer, sizeof(buffer), "%dh %02dm", rounded / 60, rounded % 60); return buffer; } inline uint32_t filament_color(std::string value) { if (!value.empty() && value[0] == '#') value.erase(0, 1); // Bambu publishes RGB or RGBA, with alpha at the end. if (value.size() != 6 && value.size() != 8) return TRACK; uint32_t rgb = 0; for (size_t i = 0; i < value.size(); ++i) { const char c = static_cast(std::tolower(static_cast(value[i]))); const int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; if (digit < 0) return TRACK; if (i < 6) rgb = (rgb << 4) | digit; } return rgb; } struct Tray { std::string label, type, color, active; }; struct Snapshot { bool connected = false; std::string online, status, task, stage, print_error, hms_error; std::string remaining_unit, nozzle_unit, bed_unit; float progress = NAN, nozzle = NAN, nozzle_target = NAN, bed = NAN, bed_target = NAN; float time_left = NAN, layer = NAN, total_layers = NAN, fan = NAN; bool dual_nozzle = false; float left_nozzle = NAN, left_target = NAN, right_nozzle = NAN, right_target = NAN; std::vector trays; bool live() const { return connected && is_on(online) && valid_text(status); } std::string status_text() const { if (!connected) return "HA OFFLINE"; if (lower(online) == "off" || lower(online) == "false") return "PRINTER OFFLINE"; if (!live()) return "SYNCING"; if (is_on(print_error)) return "PRINT ERROR"; if (is_on(hms_error)) return "HMS ALERT"; const auto state = lower(status); if (state == "running") return "PRINTING"; if (state == "pause" || state == "paused") return "PAUSED"; if (state == "finish" || state == "finished") return "COMPLETE"; auto text = humanize(status); for (char &c : text) c = static_cast(std::toupper(static_cast(c))); return text; } uint32_t status_color() const { if (!live()) return MUTED; const auto state = lower(status); if (is_on(print_error) || is_on(hms_error) || state == "failed") return RED; if (state == "pause" || state == "paused") return AMBER; return GREEN; } }; } // namespace bambu_display #ifdef USE_LVGL #include namespace bambu_display { struct TrayWidgets { lv_obj_t *card, *swatch, *label; }; struct Widgets { lv_obj_t *status, *task, *arc, *progress, *nozzle, *bed, *remaining, *layers, *stage; std::vector trays; }; 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()); } inline void render(const Snapshot &data, const Widgets &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); lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(color), LV_PART_INDICATOR); lv_arc_set_value(widgets.arc, live ? gauge_value(data.progress, 100) : 0); set_text(widgets.progress, live && std::isfinite(data.progress) ? number(std::clamp(data.progress, 0.0f, 100.0f)) + "%" : "--"); set_text(widgets.task, live ? (valid_text(data.task) ? data.task : "No active print") : data.connected ? "Waiting for printer data" : "Waiting for Home Assistant"); if (live && data.dual_nozzle) { set_text(widgets.nozzle, "L " + temperature(data.left_nozzle, data.left_target, data.nozzle_unit) + "\nR " + temperature(data.right_nozzle, data.right_target, data.nozzle_unit)); } else { set_text(widgets.nozzle, live ? temperature(data.nozzle, data.nozzle_target, data.nozzle_unit) : "--"); } set_text(widgets.bed, live ? temperature(data.bed, data.bed_target, data.bed_unit) : "--"); set_text(widgets.remaining, live ? remaining(data.time_left, data.remaining_unit) : "--"); set_text(widgets.layers, live ? number(data.layer) + " / " + number(data.total_layers) : "--"); std::string stage = live ? humanize(data.stage) : "Telemetry unavailable"; if (live && std::isfinite(data.fan)) stage += " | Part fan " + number(data.fan) + "%"; set_text(widgets.stage, stage); for (size_t i = 0; i < std::min(data.trays.size(), widgets.trays.size()); ++i) { const auto &tray = data.trays[i]; const auto &widget = widgets.trays[i]; set_text(widget.label, tray.label + " " + (live && valid_text(tray.type) ? tray.type : "--")); lv_obj_set_style_bg_color(widget.swatch, lv_color_hex(live ? filament_color(tray.color) : TRACK), 0); lv_obj_set_style_border_color(widget.card, lv_color_hex(live && is_on(tray.active) ? GREEN : TRACK), 0); } } } // namespace bambu_display #endif