Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 100 additions & 35 deletions app.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
100 changes: 70 additions & 30 deletions mqtt_bambulab.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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())
Expand All @@ -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"]
Comment on lines +244 to +246

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Change detection always dirty after dashboard render

The new change-detection guard compares incoming MQTT vt_tray/ams payloads to the cached LAST_AMS_CONFIG before bumping the dashboard version, but build_dashboard_payload mutates that same cache in place (see app.py lines 251‑264) by adding Spoolman enrichment fields. After any dashboard or /api/dashboard request, LAST_AMS_CONFIG no longer matches the raw telemetry, so this equality check always evaluates true and _increment_dashboard_version() fires on every MQTT report even when nothing changed. That means connected clients continuously rebuild the dashboard and refetch Spoolman data, generating unnecessary load and defeating the version gating intended here.

Useful? React with 👍 / 👎.

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"]:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ <h5 style="margin: 0px 5px 5px 5px;">{{ PRINTER_MODEL["model"]}} - {{PRINTER_MOD
<li><a href="{{ url_for('print_history') }}" class="nav-link px-2 link-body-emphasis">Print History</a></li>
<li><a href="{{ SPOOLMAN_BASE_URL }}" target="_blank" class="nav-link px-2 link-body-emphasis">SpoolMan</a></li>
</ul>
<div class="ms-lg-3">
<span id="live-status" class="badge text-bg-secondary ms-lg-3">Offline</span>
</div>
</div>
</div>
</header>
Expand Down
Loading