mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
[Fix] Preserve AMS spool assignments across printer restart (#765)
When a printer shuts down it sends a final MQTT message with tray_exist_bits=0 and power_on_flag=false. The tray_exist_bits clearing code processed this all-zero value, wiping every AMS slot's filament data. On reconnect, the auto-unlink check saw empty tray data (no color, no type) and deleted all spool assignments as "fingerprint mismatch". Fix: skip tray_exist_bits slot clearing when power_on_flag is false. Defaults to true when absent for backwards compatibility. Adds 3 regression tests covering shutdown preservation, genuine removal still working, and missing power_on_flag fallback.
This commit is contained in:
@@ -27,6 +27,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Virtual Printer Proxy A1 Diagnostics** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — Added diagnostic port probing (ports 21, 80, 443) on proxy VP bind IPs to detect if BambuStudio tries to connect on ports the proxy doesn't handle. Logs a warning when an unexpected connection is detected. Helps diagnose A1/A1 Mini proxy issues where the slicer may use a different connection flow.
|
||||
- **File Rename Removes Extension** ([#751](https://github.com/maziggy/bambuddy/issues/751)) — Renaming a file in the File Manager included the file extension in the editable text, so users could accidentally remove it (e.g. renaming `bracket.gcode.3mf` to `bracket`), making the file unprintable. The rename modal now only lets users edit the base name, with the extension shown as a non-editable suffix. Reported by @fleishmaab, confirmed by @cadtoolbox.
|
||||
- **Spurious "Job Waiting for Filament" Notification** ([#753](https://github.com/maziggy/bambuddy/issues/753)) — When all printers of a model were busy and a job was queued with ASAP timing, a "Job Waiting for Filament" notification fired immediately even though no filament issue existed. The job was simply waiting for a printer to finish. The scheduler now skips the waiting notification when all matching printers are just busy, since the job will auto-start when one finishes. Also renamed the default notification title from "Job Waiting for Filament" to "Queue Job Waiting" to accurately reflect all waiting reasons. Reported by @maziggy.
|
||||
- **AMS Spools Removed After Printer Restart** ([#765](https://github.com/maziggy/bambuddy/issues/765)) — AMS spool assignments and slot configurations were lost after restarting the printer. When the printer shuts down, it sends a final MQTT message with `tray_exist_bits=0` and `power_on_flag=false`, which caused Bambuddy to clear all AMS slot data and auto-unlink every spool assignment. On reconnect, the assignments were gone. Fixed by skipping `tray_exist_bits` slot clearing when `power_on_flag` is `false` (shutdown message), preserving AMS data across printer restarts. Reported by @Woyteck1.
|
||||
|
||||
### Added
|
||||
- **Quick Print Speed Control** ([#256](https://github.com/maziggy/bambuddy/issues/256)) — Added a print speed control badge to the printer card controls row, next to the fan status badges. Click to choose between Silent (50%), Standard (100%), Sport (124%), and Ludicrous (166%) speed presets. The badge shows the current speed percentage with a gauge icon, always visible but disabled when no print is active. Includes optimistic UI updates for instant feedback. Requested by @Sllepper.
|
||||
|
||||
@@ -1399,8 +1399,11 @@ class BambuMQTTClient:
|
||||
# Check tray_exist_bits to clear empty slots (Issue #147)
|
||||
# New AMS models don't send empty tray data - they just update tray_exist_bits
|
||||
# Each bit in tray_exist_bits represents a slot: bit=0 means empty, bit=1 means has spool
|
||||
# Skip when power_on_flag=False: printer shutdown sends all-zero bits which would
|
||||
# wipe all slot data and cause auto-unlink to remove spool assignments (#765)
|
||||
tray_exist_bits_str = ams_data.get("tray_exist_bits") if isinstance(ams_data, dict) else None
|
||||
if tray_exist_bits_str:
|
||||
power_on = ams_data.get("power_on_flag", True) if isinstance(ams_data, dict) else True
|
||||
if tray_exist_bits_str and power_on:
|
||||
try:
|
||||
tray_exist_bits = int(tray_exist_bits_str, 16)
|
||||
for ams_unit in merged_ams:
|
||||
|
||||
@@ -606,6 +606,133 @@ class TestAMSDataMerging:
|
||||
assert ams_data[0]["tray"][0]["tray_type"] == "PLA", "A1 should still have PLA"
|
||||
assert ams_data[1]["tray"][0]["tray_type"] == "PLA", "B1 should still have PLA"
|
||||
|
||||
def test_shutdown_message_preserves_ams_data(self, mqtt_client):
|
||||
"""Printer shutdown (power_on_flag=False) must not wipe AMS slot data (#765).
|
||||
|
||||
When a printer shuts down it sends a final MQTT message with
|
||||
tray_exist_bits='0' and power_on_flag=False. This all-zero value
|
||||
previously caused every slot to be cleared, which then triggered
|
||||
auto-unlink of all spool assignments on reconnect.
|
||||
"""
|
||||
# Initial state: two AMS units with loaded spools
|
||||
initial_ams = {
|
||||
"ams": [
|
||||
{
|
||||
"id": 0,
|
||||
"tray": [
|
||||
{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF", "remain": 80},
|
||||
{"id": 1, "tray_type": "PETG", "tray_color": "00FF00FF", "remain": 60},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"tray": [
|
||||
{"id": 0, "tray_type": "PETG", "tray_color": "DBDDD9FF", "remain": 90},
|
||||
{"id": 1, "tray_type": "PETG", "tray_color": "67DB25FF", "remain": 70},
|
||||
],
|
||||
},
|
||||
],
|
||||
"tray_exist_bits": "33", # Slots 0,1 of each AMS (0b00110011)
|
||||
"power_on_flag": True,
|
||||
}
|
||||
mqtt_client._handle_ams_data(initial_ams)
|
||||
|
||||
# Verify initial state
|
||||
ams_data = mqtt_client.state.raw_data["ams"]
|
||||
assert ams_data[0]["tray"][0]["tray_type"] == "PLA"
|
||||
assert ams_data[1]["tray"][0]["tray_type"] == "PETG"
|
||||
|
||||
# Simulate printer shutdown — all-zero bits with power_on_flag=False
|
||||
shutdown_ams = {
|
||||
"ams_exist_bits": "0",
|
||||
"tray_exist_bits": "0",
|
||||
"power_on_flag": False,
|
||||
"insert_flag": False,
|
||||
"tray_now": "0",
|
||||
"version": 0,
|
||||
}
|
||||
mqtt_client._handle_ams_data(shutdown_ams)
|
||||
|
||||
# AMS slot data MUST be preserved — shutdown should not clear it
|
||||
ams_data = mqtt_client.state.raw_data["ams"]
|
||||
assert ams_data[0]["tray"][0]["tray_type"] == "PLA", "Shutdown must not clear AMS 0 slot 0"
|
||||
assert ams_data[0]["tray"][0]["tray_color"] == "FF0000FF", "Shutdown must not clear AMS 0 slot 0 color"
|
||||
assert ams_data[0]["tray"][1]["tray_type"] == "PETG", "Shutdown must not clear AMS 0 slot 1"
|
||||
assert ams_data[1]["tray"][0]["tray_type"] == "PETG", "Shutdown must not clear AMS 1 slot 0"
|
||||
assert ams_data[1]["tray"][1]["tray_type"] == "PETG", "Shutdown must not clear AMS 1 slot 1"
|
||||
|
||||
def test_genuine_removal_still_clears_with_power_on(self, mqtt_client):
|
||||
"""Genuine spool removal (power_on_flag=True) must still clear slot data.
|
||||
|
||||
Ensures the #765 fix doesn't break normal spool removal detection.
|
||||
"""
|
||||
# Initial state: AMS with loaded spool
|
||||
initial_ams = {
|
||||
"ams": [
|
||||
{
|
||||
"id": 0,
|
||||
"tray": [
|
||||
{"id": 0, "tray_type": "PLA", "tray_color": "FF0000", "remain": 80},
|
||||
{"id": 1, "tray_type": "PETG", "tray_color": "00FF00", "remain": 60},
|
||||
],
|
||||
},
|
||||
],
|
||||
"tray_exist_bits": "3", # Both slots occupied (0b11)
|
||||
"power_on_flag": True,
|
||||
}
|
||||
mqtt_client._handle_ams_data(initial_ams)
|
||||
|
||||
# Spool removed from slot 1 while printer is running
|
||||
removal_ams = {
|
||||
"ams": [
|
||||
{
|
||||
"id": 0,
|
||||
"tray": [{"id": 0}, {"id": 1}],
|
||||
},
|
||||
],
|
||||
"tray_exist_bits": "1", # Only slot 0 occupied (0b01)
|
||||
"power_on_flag": True,
|
||||
}
|
||||
mqtt_client._handle_ams_data(removal_ams)
|
||||
|
||||
# Slot 0 preserved, slot 1 cleared
|
||||
ams_data = mqtt_client.state.raw_data["ams"]
|
||||
assert ams_data[0]["tray"][0]["tray_type"] == "PLA", "Slot 0 should be preserved"
|
||||
assert ams_data[0]["tray"][1]["tray_type"] == "", "Slot 1 should be cleared on removal"
|
||||
assert ams_data[0]["tray"][1]["tray_color"] == "", "Slot 1 color should be cleared"
|
||||
|
||||
def test_power_on_flag_defaults_true_when_absent(self, mqtt_client):
|
||||
"""When power_on_flag is not in the MQTT data, clearing must proceed normally.
|
||||
|
||||
Ensures backwards compatibility with firmware that doesn't send power_on_flag.
|
||||
"""
|
||||
# Initial state
|
||||
initial_ams = {
|
||||
"ams": [
|
||||
{
|
||||
"id": 0,
|
||||
"tray": [
|
||||
{"id": 0, "tray_type": "PLA", "tray_color": "FF0000", "remain": 80},
|
||||
],
|
||||
},
|
||||
],
|
||||
"tray_exist_bits": "1",
|
||||
}
|
||||
mqtt_client._handle_ams_data(initial_ams)
|
||||
|
||||
# Update WITHOUT power_on_flag — should still clear when bit=0
|
||||
update_ams = {
|
||||
"ams": [{"id": 0, "tray": [{"id": 0}]}],
|
||||
"tray_exist_bits": "0",
|
||||
# No power_on_flag key at all
|
||||
}
|
||||
mqtt_client._handle_ams_data(update_ams)
|
||||
|
||||
ams_data = mqtt_client.state.raw_data["ams"]
|
||||
assert ams_data[0]["tray"][0]["tray_type"] == "", (
|
||||
"Without power_on_flag, clearing should proceed (defaults to True)"
|
||||
)
|
||||
|
||||
|
||||
class TestNozzleRackData:
|
||||
"""Tests for nozzle rack data parsing from H2 series device.nozzle.info."""
|
||||
|
||||
Reference in New Issue
Block a user