Fix VP queue mode sending wrong plate_id → HMS 0500_4003 (#529)

When a virtual printer auto-queued a file, PrintQueueItem was created
without plate_id. The scheduler defaulted to plate_id=1, generating
MQTT path "Metadata/plate_1.gcode". For multi-plate 3MF files sliced
on a different plate, the printer couldn't find the gcode and returned
HMS error 0500_4003. Extract plate index from the 3MF's slice_info.config
before creating the queue item.
This commit is contained in:
maziggy
2026-02-26 10:00:33 +01:00
parent f4f09ccc2d
commit a6d326f8fa
2 changed files with 23 additions and 0 deletions
+1
View File
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
## [0.2.1] - Unreleased
### Fixed
- **Virtual Printer Queue Sends Wrong Plate ID** ([#529](https://github.com/maziggy/bambuddy/issues/529)) — Files sent to a virtual printer in queue mode always used `plate_id=1`, generating the MQTT path `Metadata/plate_1.gcode`. For multi-plate 3MF files where the actual plate is different (e.g. plate 19), the printer couldn't find the gcode and returned HMS error 0500_4003. Now extracts the plate index from the 3MF's `slice_info.config` before creating the queue item.
- **Unnecessary Target Model Selector on "Any" Tab** ([#528](https://github.com/maziggy/bambuddy/issues/528)) — When scheduling a print to "Any {model}", a redundant "Target Model" dropdown appeared even though the G-code is already sliced for a specific printer model. Changing the target model would lead to print failures. The dropdown is now hidden when the sliced model is known (the tab label already shows "Any {model}"). It still appears as a fallback for legacy files without model metadata.
- **"Clear Plate & Start Next" Button Shown on Printers Without Correct Filament** ([#527](https://github.com/maziggy/bambuddy/issues/527)) — When a print job was queued for "any printer" of a model (e.g., "any H2S"), the "Clear Plate & Start Next" button appeared on ALL printers of that model, including those without the required filament loaded. Clicking it on a printer without the right filament would start a print that fails. The `PrinterQueueWidget` now filters queue items by filament compatibility — it checks the printer's loaded filament types (from AMS and external spools) against the queue item's `required_filament_types` and only shows items the printer can actually print. If no compatible items exist, the widget is hidden.
- **Manual Spool Weight Overwritten by AMS Auto-Sync** ([#525](https://github.com/maziggy/bambuddy/issues/525)) — When a user manually entered a spool weight (via UI or API), the value was overwritten by the automatic AMS remain% sync that runs on every MQTT update. The AMS remain% is integer-only (~10g resolution for 1kg spool) and can't match precise manual entries. Added a `weight_locked` flag that is automatically set when `weight_used` is explicitly updated via the API. Locked spools are skipped by both the automatic AMS remain% sync and the manual force-sync endpoint. The usage tracker (3MF/gcode delta tracking) is unaffected. Users can re-enable AMS sync by setting `weight_locked: false`.
@@ -310,10 +310,12 @@ class VirtualPrinterInstance:
target_model = None
if not self.target_printer_id and self.model:
target_model = VIRTUAL_PRINTER_MODELS.get(self.model)
plate_id = self._extract_plate_id(file_path)
queue_item = PrintQueueItem(
printer_id=self.target_printer_id,
target_model=target_model,
archive_id=archive.id,
plate_id=plate_id,
position=1,
status="pending",
)
@@ -330,6 +332,26 @@ class VirtualPrinterInstance:
except Exception as e:
logger.error("Error adding to print queue: %s", e)
@staticmethod
def _extract_plate_id(file_path: Path) -> int | None:
"""Extract plate index from 3MF slice_info.config."""
try:
import xml.etree.ElementTree as ET
import zipfile
with zipfile.ZipFile(file_path, "r") as zf:
if "Metadata/slice_info.config" in zf.namelist():
content = zf.read("Metadata/slice_info.config").decode()
root = ET.fromstring(content) # noqa: S314
plate = root.find(".//plate")
if plate is not None:
for meta in plate.findall("metadata"):
if meta.get("key") == "index" and meta.get("value"):
return int(meta.get("value"))
except Exception:
return None
return None
# -- Service lifecycle --
async def start_server(self) -> None: