From 0ad9b10d14a60050e5f8bef9d69685810dbb12d6 Mon Sep 17 00:00:00 2001 From: "Markus M." Date: Sat, 29 Nov 2025 09:46:49 +0200 Subject: [PATCH] Add live dashboard streaming and UI updates --- app.py | 135 ++++++++++++++++------ mqtt_bambulab.py | 100 +++++++++++----- static/css/style.css | 4 + templates/base.html | 3 + templates/index.html | 264 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 412 insertions(+), 94 deletions(-) diff --git a/app.py b/app.py index 31fa131f..ac8bbae4 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,28 @@ import json import math import traceback -import uuid +import uuid +from queue import Empty -from flask import Flask, request, render_template, redirect, url_for +from flask import Flask, Response, jsonify, redirect, render_template, request, stream_with_context, url_for from config import BASE_URL, AUTO_SPEND, SPOOLMAN_BASE_URL, EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID, PRINTER_NAME from filament import generate_filament_brand_code, generate_filament_temperatures from frontend_utils import color_is_dark from messages import AMS_FILAMENT_SETTING -from mqtt_bambulab import fetchSpools, getLastAMSConfig, publish, getMqttClient, setActiveTray, isMqttClientConnected, init_mqtt, getPrinterModel +from mqtt_bambulab import ( + fetchSpools, + getLastAMSConfig, + getMqttClient, + get_dashboard_version, + getPrinterModel, + init_mqtt, + isMqttClientConnected, + publish, + register_dashboard_listener, + setActiveTray, + unregister_dashboard_listener, +) from spoolman_client import patchExtraTags, getSpoolById, consumeSpool from spoolman_service import augmentTrayDataWithSpoolMan, trayUid, getSettings from print_history import get_prints_with_filament, update_filament_spool, get_filament_for_slot @@ -190,9 +203,9 @@ def tray_load(): traceback.print_exc() return render_template('error.html', exception=str(e)) -def setActiveSpool(ams_id, tray_id, spool_data): - if not isMqttClientConnected(): - return render_template('error.html', exception="MQTT is disconnected. Is the printer online?") +def setActiveSpool(ams_id, tray_id, spool_data): + if not isMqttClientConnected(): + return render_template('error.html', exception="MQTT is disconnected. Is the printer online?") ams_message = AMS_FILAMENT_SETTING ams_message["print"]["sequence_id"] = 0 @@ -231,35 +244,87 @@ def setActiveSpool(ams_id, tray_id, spool_data): # ams_message["print"]["tray_sub_brands"] = filament_brand_code["sub_brand_code"] ams_message["print"]["tray_sub_brands"] = "" - print(ams_message) - publish(getMqttClient(), ams_message) - -@app.route("/") -def home(): - if not isMqttClientConnected(): - return render_template('error.html', exception="MQTT is disconnected. Is the printer online?") - - try: - last_ams_config = getLastAMSConfig() - ams_data = last_ams_config.get("ams", []) - vt_tray_data = last_ams_config.get("vt_tray", {}) - spool_list = fetchSpools() - success_message = request.args.get("success_message") - - issue = False - #TODO: Fix issue when external spool info is reset via bambulab interface - augmentTrayDataWithSpoolMan(spool_list, vt_tray_data, trayUid(EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID)) - issue |= vt_tray_data["issue"] - - for ams in ams_data: - for tray in ams["tray"]: - augmentTrayDataWithSpoolMan(spool_list, tray, trayUid(ams["id"], tray["id"])) - issue |= tray["issue"] - - return render_template('index.html', success_message=success_message, ams_data=ams_data, vt_tray_data=vt_tray_data, issue=issue) - except Exception as e: - traceback.print_exc() - return render_template('error.html', exception=str(e)) + print(ams_message) + publish(getMqttClient(), ams_message) + + +def build_dashboard_payload(): + last_ams_config = getLastAMSConfig() or {} + ams_data = last_ams_config.get("ams", []) + vt_tray_data = last_ams_config.get("vt_tray", {}) + spool_list = fetchSpools() + + issue = False + augmentTrayDataWithSpoolMan(spool_list, vt_tray_data, trayUid(EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID)) + issue |= bool(vt_tray_data.get("issue")) + + for ams in ams_data: + for tray in ams.get("tray", []): + augmentTrayDataWithSpoolMan(spool_list, tray, trayUid(ams.get("id"), tray.get("id"))) + issue |= bool(tray.get("issue")) + + return { + "ams_data": ams_data, + "vt_tray_data": vt_tray_data, + "issue": bool(issue), + "version": get_dashboard_version(), + "mqtt_connected": isMqttClientConnected(), + } + + +def format_sse(data): + return f"data: {json.dumps(data)}\n\n" + +@app.route("/stream/dashboard") +def dashboard_stream(): + def event_stream(): + subscriber = register_dashboard_listener() + last_version = None + + try: + payload = build_dashboard_payload() + last_version = payload.get("version") + yield format_sse(payload) + + while True: + try: + subscriber.get(timeout=15) + except Empty: + yield ": keep-alive\n\n" + continue + + payload = build_dashboard_payload() + if payload.get("version") == last_version: + continue + + last_version = payload.get("version") + yield format_sse(payload) + except GeneratorExit: + pass + finally: + unregister_dashboard_listener(subscriber) + + return Response(stream_with_context(event_stream()), mimetype="text/event-stream") + + +@app.route("/api/dashboard") +def dashboard_json(): + try: + return jsonify(build_dashboard_payload()) + except Exception as e: + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + + +@app.route("/") +def home(): + try: + dashboard_data = build_dashboard_payload() + success_message = request.args.get("success_message") + return render_template('index.html', success_message=success_message, **dashboard_data) + except Exception as e: + traceback.print_exc() + return render_template('error.html', exception=str(e)) def sort_spools(spools): def condition(item): diff --git a/mqtt_bambulab.py b/mqtt_bambulab.py index b29f2628..91c95edc 100644 --- a/mqtt_bambulab.py +++ b/mqtt_bambulab.py @@ -1,13 +1,14 @@ -import json -import ssl -import traceback -from threading import Thread +import json +import ssl +import traceback +from queue import SimpleQueue +from threading import Thread import paho.mqtt.client as mqtt from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP, AUTO_SPEND, EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID from messages import GET_VERSION, PUSH_ALL -from spoolman_service import spendFilaments, setActiveTray, fetchSpools +from spoolman_service import spendFilaments, setActiveTray, fetchSpools from tools_3mf import getMetaDataFrom3mf import time import copy @@ -20,10 +21,13 @@ MQTT_KEEPALIVE = 60 LAST_AMS_CONFIG = {} # Global variable storing last AMS configuration -PRINTER_STATE = {} -PRINTER_STATE_LAST = {} +PRINTER_STATE = {} +PRINTER_STATE_LAST = {} -PENDING_PRINT_METADATA = {} +PENDING_PRINT_METADATA = {} + +DASHBOARD_VERSION = 0 +DASHBOARD_SUBSCRIBERS = set() def getPrinterModel(): global PRINTER_ID @@ -51,8 +55,33 @@ def getPrinterModel(): "devicename": device_name } -def num2letter(num): - return chr(ord("A") + int(num)) +def num2letter(num): + return chr(ord("A") + int(num)) + + +def register_dashboard_listener(): + queue = SimpleQueue() + queue.put_nowait(get_dashboard_version()) + DASHBOARD_SUBSCRIBERS.add(queue) + return queue + + +def unregister_dashboard_listener(queue): + DASHBOARD_SUBSCRIBERS.discard(queue) + + +def _increment_dashboard_version(): + global DASHBOARD_VERSION + DASHBOARD_VERSION += 1 + for queue in list(DASHBOARD_SUBSCRIBERS): + try: + queue.put_nowait(DASHBOARD_VERSION) + except Exception: + continue + + +def get_dashboard_version(): + return DASHBOARD_VERSION def update_dict(original: dict, updates: dict) -> dict: for key, value in updates.items(): @@ -195,8 +224,8 @@ def publish(client, msg): return False # Inspired by https://github.com/Donkie/Spoolman/issues/217#issuecomment-2303022970 -def on_message(client, userdata, msg): - global LAST_AMS_CONFIG, PRINTER_STATE, PRINTER_STATE_LAST, PENDING_PRINT_METADATA, PRINTER_MODEL +def on_message(client, userdata, msg): + global LAST_AMS_CONFIG, PRINTER_STATE, PRINTER_STATE_LAST, PENDING_PRINT_METADATA, PRINTER_MODEL try: data = json.loads(msg.payload.decode()) @@ -210,12 +239,18 @@ def on_message(client, userdata, msg): processMessage(data) # Save external spool tray data - if "print" in data and "vt_tray" in data["print"]: - LAST_AMS_CONFIG["vt_tray"] = data["print"]["vt_tray"] + updated = False + + if "print" in data and "vt_tray" in data["print"]: + if data["print"]["vt_tray"] != LAST_AMS_CONFIG.get("vt_tray"): + LAST_AMS_CONFIG["vt_tray"] = data["print"]["vt_tray"] + updated = True # Save ams spool data - if "print" in data and "ams" in data["print"] and "ams" in data["print"]["ams"]: - LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"] + if "print" in data and "ams" in data["print"] and "ams" in data["print"]["ams"]: + if data["print"]["ams"].get("ams") != LAST_AMS_CONFIG.get("ams"): + LAST_AMS_CONFIG["ams"] = data["print"]["ams"]["ams"] + updated = True for ams in data["print"]["ams"]["ams"]: print(f"AMS [{num2letter(ams['id'])}] (hum: {ams['humidity']}, temp: {ams['temp']}ºC)") for tray in ams["tray"]: @@ -247,24 +282,29 @@ def on_message(client, userdata, msg): if not found and tray_uuid == "00000000000000000000000000000000": print(" - No Spool or non Bambulab Spool!") - elif not found: - print(" - Not found. Update spool tag!") + elif not found: + print(" - Not found. Update spool tag!") + + if updated: + _increment_dashboard_version() except Exception as e: traceback.print_exc() -def on_connect(client, userdata, flags, rc): - global MQTT_CLIENT_CONNECTED - MQTT_CLIENT_CONNECTED = True - print("Connected with result code " + str(rc)) - client.subscribe(f"device/{PRINTER_ID}/report") - publish(client, GET_VERSION) - publish(client, PUSH_ALL) - -def on_disconnect(client, userdata, rc): - global MQTT_CLIENT_CONNECTED - MQTT_CLIENT_CONNECTED = False - print("Disconnected with result code " + str(rc)) +def on_connect(client, userdata, flags, rc): + global MQTT_CLIENT_CONNECTED + MQTT_CLIENT_CONNECTED = True + print("Connected with result code " + str(rc)) + client.subscribe(f"device/{PRINTER_ID}/report") + publish(client, GET_VERSION) + publish(client, PUSH_ALL) + _increment_dashboard_version() + +def on_disconnect(client, userdata, rc): + global MQTT_CLIENT_CONNECTED + MQTT_CLIENT_CONNECTED = False + print("Disconnected with result code " + str(rc)) + _increment_dashboard_version() def async_subscribe(): global MQTT_CLIENT diff --git a/static/css/style.css b/static/css/style.css index 181a460f..eb057840 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -166,6 +166,10 @@ object-fit: contain; /* Sorgt dafür, dass das Bild innerhalb des Containers bleibt */ } +#live-status { + min-width: 96px; +} + /* Responsive Anpassungen */ @media (max-width: 768px) { .print-grid { diff --git a/templates/base.html b/templates/base.html index 2af2acb8..46e58881 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,6 +42,9 @@
{{ PRINTER_MODEL["model"]}} - {{PRINTER_MOD
  • Print History
  • SpoolMan
  • +
    + Offline +
    diff --git a/templates/index.html b/templates/index.html index b11e28ca..92659080 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,38 +1,244 @@ {% extends 'base.html' %} {% block content %} - -
    - -
    - {% with tray_data=vt_tray_data, ams_id=EXTERNAL_SPOOL_AMS_ID, pick_tray=False, tray_id=EXTERNAL_SPOOL_ID %} - {% include 'fragments/tray.html' %} - {% endwith %} -
    - - - {% for ams in ams_data %} -
    -
    +
    + + {% endblock %}