Files
Home-Assistant/esphome/bambu-display/dashboard.h
T
2026-10-17 17:09:47 -04:00

173 lines
6.7 KiB
C++

#pragma once
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdint>
#include <string>
#include <vector>
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<char>(std::tolower(static_cast<unsigned char>(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<char>(std::toupper(static_cast<unsigned char>(text[0])));
return text;
}
inline int gauge_value(float value, int maximum) {
return std::isfinite(value) ? static_cast<int>(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<int>(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<char>(std::tolower(static_cast<unsigned char>(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<Tray> 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<char>(std::toupper(static_cast<unsigned char>(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 <lvgl.h>
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<TrayWidgets> 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