feat: dual external spool support, AMS slot model filtering & pre-population

Backend:
- Add dual external spool support for H2D (vt_tray as list: Ext-L/Ext-R)
- Add cloud filament ID map endpoint (/cloud/filament-id-map)
- Fix RFID spool data erased by periodic AMS updates (skip tag matcher
  for RFID-tagged trays)
- Fix AMS slot config overwrites RFID spool state
- Fix K-profile selection corrupts existing profiles on X1C/P1S
- Resolve K-profiles filament name via cloud filament ID map
- Update print scheduler and usage tracker for dual external spools

Frontend:
- Add printer model filtering to ConfigureAmsSlotModal (cloud/local/builtin
  presets filtered by @BBL model suffix and compatible_printers)
- Add pre-population for configured slots (preset, color, K-profile)
- Add K-Profiles view with accurate filament name resolution
- Internationalize all ConfigureAmsSlotModal strings (en/de/fr/it/ja — 21 keys)
- Add 5 new ConfigureAmsSlotModal tests (model filtering, pre-selection,
  color pre-population, i18n)
- Update PrintersPage for dual external spool rendering

Docs:
- Update CHANGELOG, README, website features, and wiki AMS docs
This commit is contained in:
maziggy
2026-02-15 08:04:01 +01:00
parent 12e92a301d
commit a37dfaf7fb
40 changed files with 10007 additions and 9175 deletions
+151 -57
View File
@@ -128,7 +128,7 @@ class PrinterState:
chamber_light: bool = False
# Active extruder for dual nozzle (0=right, 1=left) - from device.extruder.info[X].hnow
active_extruder: int = 0
# Currently loaded tray (global ID): 254 = external spool, 255 = no filament
# Currently loaded tray (global ID): 254/255 = external spools, 255 = no filament on legacy printers
tray_now: int = 255
# Pending load target - used to track what tray we're loading for H2D disambiguation
pending_tray_target: int | None = None
@@ -451,14 +451,33 @@ class BambuMQTTClient:
except Exception as e:
logger.error("[%s] Error handling AMS data from print: %s", self.serial_number, e)
# Handle vir_slot (H2-series external spool data) — list of external trays
# Process vir_slot FIRST so it takes priority over vt_tray
if "vir_slot" in print_data:
vir_slot = print_data["vir_slot"]
if isinstance(vir_slot, list) and vir_slot:
# Fix: single-nozzle printers (X1C, P1S, A1) report their single
# external slot with id=255 in vir_slot, but tray_now=254 when active.
# Remap id=255→254 for single-slot printers so active detection works.
# Dual-nozzle (H2D) has 2 slots: id=254 (Ext-L) and id=255 (Ext-R).
if len(vir_slot) == 1 and str(vir_slot[0].get("id", "")) == "255":
vir_slot[0]["id"] = "254"
self.state.raw_data["vt_tray"] = vir_slot
# Handle vt_tray (virtual tray / external spool) data
if "vt_tray" in print_data:
# Only use vt_tray if vir_slot is NOT in this message AND we don't already
# have vir_slot data (H2-series sends vt_tray as a single active spool dict
# which would overwrite the correct multi-slot vir_slot data)
if "vt_tray" in print_data and "vir_slot" not in print_data:
vt_tray = print_data["vt_tray"]
self.state.raw_data["vt_tray"] = vt_tray
# Log vt_tray to investigate per-extruder data for H2D
if not hasattr(self, "_vt_tray_logged") or not self._vt_tray_logged:
logger.info("[%s] vt_tray data: %s", self.serial_number, vt_tray)
self._vt_tray_logged = True
existing = self.state.raw_data.get("vt_tray")
# Don't let a single-spool vt_tray dict overwrite multi-slot vir_slot data
if isinstance(vt_tray, dict) and isinstance(existing, list) and len(existing) > 1:
pass # Keep the vir_slot data
else:
if isinstance(vt_tray, dict):
vt_tray = [vt_tray]
self.state.raw_data["vt_tray"] = vt_tray
# Parse ams_status directly from print data (NOT from print.ams)
# ams_status is a combined value: lower 8 bits = sub status, bits 8-15 = main status
@@ -486,7 +505,10 @@ class BambuMQTTClient:
# Check for K-profile response (extrusion_cali)
if "command" in print_data:
logger.debug("[%s] Received command response: %s", self.serial_number, print_data.get("command"))
cmd = print_data.get("command")
logger.debug("[%s] Received command response: %s", self.serial_number, cmd)
if cmd in ("extrusion_cali_sel", "extrusion_cali_set", "extrusion_cali_del", "ams_filament_setting"):
logger.info("[%s] %s response: %s", self.serial_number, cmd, print_data)
if "command" in print_data and print_data.get("command") == "extrusion_cali_get":
self._handle_kprofile_response(print_data)
@@ -941,13 +963,20 @@ class BambuMQTTClient:
if tray_id is not None and tray_id in existing_trays:
# Merge: start with existing, update with new non-empty values
merged_tray = existing_trays[tray_id].copy()
# Detect slot-clearing updates (spool removal):
# When tray_type is explicitly empty, clear everything
# including RFID data (tag_uid/tray_uuid).
slot_clearing = new_tray.get("tray_type") == ""
for key, value in new_tray.items():
# Fields that should always be updated (even with empty/zero values):
# - remain, k, id, cali_idx: status indicators where 0 is valid
# - tray_type, tray_sub_brands, tag_uid, tray_uuid, tray_info_idx,
# tray_color, tray_id_name: slot content indicators that must
# be cleared when a spool is removed (fixes #147 - old AMS
# empty slot)
# - tray_type, tray_sub_brands, tray_info_idx, tray_color,
# tray_id_name: slot content indicators that must be cleared
# when a spool is removed (fixes #147 - old AMS empty slot)
# NOTE: tag_uid and tray_uuid are NOT in always_update_fields.
# They are only cleared during spool removal (slot_clearing=True).
# Periodic AMS updates often include empty RFID fields which
# would overwrite valid data from the initial pushall.
always_update_fields = (
"remain",
"k",
@@ -955,17 +984,20 @@ class BambuMQTTClient:
"cali_idx",
"tray_type",
"tray_sub_brands",
"tag_uid",
"tray_uuid",
"tray_info_idx",
"tray_color",
"tray_id_name",
)
if key in always_update_fields or value not in (
None,
"",
"0000000000000000",
"00000000000000000000000000000000",
if (
key in always_update_fields
or slot_clearing
or value
not in (
None,
"",
"0000000000000000",
"00000000000000000000000000000000",
)
):
merged_tray[key] = value
merged_trays.append(merged_tray)
@@ -2424,7 +2456,7 @@ class BambuMQTTClient:
def _handle_kprofile_response(self, data: dict):
"""Handle K-profile response from printer."""
response_nozzle = data.get("nozzle_diameter")
_response_seq_id = data.get("sequence_id", "?")
response_seq_id = data.get("sequence_id", "?")
filaments = data.get("filaments", [])
expected_nozzle = getattr(self, "_expected_kprofile_nozzle", None)
has_pending_request = self._pending_kprofile_response is not None
@@ -2432,7 +2464,8 @@ class BambuMQTTClient:
# Log all incoming responses when we have a pending request (for debugging)
if has_pending_request:
logger.info(
f"[{self.serial_number}] K-profile response: nozzle={response_nozzle}, {len(filaments)} profiles, expected={expected_nozzle}"
f"[{self.serial_number}] K-profile response: nozzle={response_nozzle}, "
f"seq_id={response_seq_id}, {len(filaments)} profiles, expected={expected_nozzle}"
)
# If we have a pending request, only accept responses with matching nozzle_diameter
@@ -3305,6 +3338,7 @@ class BambuMQTTClient:
command = {"print": {"command": "ams_get_rfid", "ams_id": ams_id, "slot_id": tray_id, "sequence_id": "0"}}
self._client.publish(self.topic_publish, json.dumps(command), qos=1)
logger.info("[%s] Triggering RFID re-read: AMS %s, slot %s", self.serial_number, ams_id, tray_id)
return True, f"Refreshing AMS {ams_id} tray {tray_id}"
def ams_set_filament_setting(
@@ -3341,18 +3375,33 @@ class BambuMQTTClient:
logger.warning("[%s] Cannot set AMS filament setting: not connected", self.serial_number)
return False
# Calculate slot_id based on AMS type
if ams_id <= 3:
# Calculate mqtt IDs based on AMS type
if ams_id == 255:
vt_tray = self.state.raw_data.get("vt_tray", []) if self.state.raw_data else []
if len(vt_tray) > 1:
# Dual external slots (H2D): each ext slot is its own virtual AMS unit
# (254=ext-L / slot 0, 255=ext-R / slot 1)
mqtt_ams_id = 254 + tray_id
else:
# Single external slot (X1C, P1S, A1): always ams_id=255
mqtt_ams_id = 255
mqtt_tray_id = 0
slot_id = 0
elif ams_id <= 3:
mqtt_ams_id = ams_id
mqtt_tray_id = tray_id
slot_id = tray_id
else:
# AMS-HT or external: slot_id = 0
# AMS-HT: single tray per unit
mqtt_ams_id = ams_id
mqtt_tray_id = tray_id
slot_id = 0
command = {
"print": {
"command": "ams_filament_setting",
"ams_id": ams_id,
"tray_id": tray_id,
"ams_id": mqtt_ams_id,
"tray_id": mqtt_tray_id,
"slot_id": slot_id,
"tray_info_idx": tray_info_idx,
"tray_type": tray_type,
@@ -3390,17 +3439,32 @@ class BambuMQTTClient:
logger.warning("[%s] Cannot reset AMS slot: not connected", self.serial_number)
return False
# Calculate slot_id based on AMS type
if ams_id <= 3:
# Calculate mqtt IDs based on AMS type
if ams_id == 255:
vt_tray = self.state.raw_data.get("vt_tray", []) if self.state.raw_data else []
if len(vt_tray) > 1:
# Dual external slots (H2D): each ext slot is its own virtual AMS unit
mqtt_ams_id = 254 + tray_id
else:
# Single external slot (X1C, P1S, A1): always ams_id=255
mqtt_ams_id = 255
mqtt_tray_id = 0
slot_id = 0
elif ams_id <= 3:
mqtt_ams_id = ams_id
mqtt_tray_id = tray_id
slot_id = tray_id
else:
# AMS-HT: single tray per unit
mqtt_ams_id = ams_id
mqtt_tray_id = tray_id
slot_id = 0
command = {
"print": {
"command": "ams_filament_setting",
"ams_id": ams_id,
"tray_id": tray_id,
"ams_id": mqtt_ams_id,
"tray_id": mqtt_tray_id,
"slot_id": slot_id,
"tray_info_idx": "",
"tray_type": "",
@@ -3425,20 +3489,21 @@ class BambuMQTTClient:
cali_idx: int,
filament_id: str,
nozzle_diameter: str = "0.4",
setting_id: str | None = None,
) -> bool:
"""Set calibration profile (K value) for an AMS slot.
This command selects a K profile from the printer's calibration list.
Use cali_idx=-1 to use the default K value (0.020).
Note: Do NOT send setting_id in this command — BambuStudio never includes
it, and adding it causes the firmware to mislink the profile on X1C/P1S.
Args:
ams_id: AMS unit ID (0-3 for regular AMS, 128-135 for HT AMS)
tray_id: Tray ID within the AMS (0-3)
cali_idx: Calibration profile index (-1 for default)
filament_id: Filament preset ID (same as tray_info_idx)
nozzle_diameter: Nozzle diameter string (e.g., "0.4")
setting_id: Full setting ID with version (e.g., "GFSL05_07") - optional
Returns:
True if command was sent, False otherwise
@@ -3447,13 +3512,34 @@ class BambuMQTTClient:
logger.warning("[%s] Cannot set calibration: not connected", self.serial_number)
return False
# Calculate slot_id based on AMS type
# tray_id in the command should be the local tray index (0-3)
if ams_id <= 3:
# Calculate mqtt IDs based on AMS type.
# IMPORTANT: extrusion_cali_sel uses GLOBAL tray_id (unlike ams_filament_setting
# which uses LOCAL). BambuStudio confirms: tray_id = ams_id * 4 + slot.
if ams_id == 255:
# External spool: extrusion_cali_sel uses GLOBAL tray_id (unlike
# ams_filament_setting which uses LOCAL tray_id=0).
vt_tray = self.state.raw_data.get("vt_tray", []) if self.state.raw_data else []
if len(vt_tray) > 1:
# Dual external slots (H2D): each ext slot is its own virtual AMS unit
# Confirmed from BambuStudio logs: ext-R sends ams_id=255, tray_id=255
mqtt_ams_id = 254 + tray_id
mqtt_tray_id = 254 + tray_id
else:
# Single external slot (X1C, P1S, A1): global tray_id=254
mqtt_ams_id = 254
mqtt_tray_id = 254
slot_id = 0
elif ams_id <= 3:
mqtt_ams_id = ams_id
mqtt_tray_id = ams_id * 4 + tray_id
slot_id = tray_id
elif ams_id >= 128 and ams_id <= 135:
mqtt_ams_id = ams_id
mqtt_tray_id = tray_id
slot_id = 0
else:
mqtt_ams_id = ams_id
mqtt_tray_id = tray_id
slot_id = 0
command = {
@@ -3462,20 +3548,16 @@ class BambuMQTTClient:
"cali_idx": cali_idx,
"filament_id": filament_id,
"nozzle_diameter": nozzle_diameter,
"ams_id": ams_id,
"tray_id": tray_id, # Local tray index (0-3), not global
"ams_id": mqtt_ams_id,
"tray_id": mqtt_tray_id,
"slot_id": slot_id,
"sequence_id": "0",
}
}
# Include setting_id if provided (helps slicer show correct K profile)
if setting_id:
command["print"]["setting_id"] = setting_id
command_json = json.dumps(command)
logger.info(
f"[{self.serial_number}] Publishing extrusion_cali_sel: AMS {ams_id}, tray {tray_id}, cali_idx={cali_idx}, setting_id={setting_id}"
f"[{self.serial_number}] Publishing extrusion_cali_sel: AMS {ams_id}, tray {tray_id}, cali_idx={cali_idx}"
)
logger.debug("[%s] extrusion_cali_sel command: %s", self.serial_number, command_json)
self._client.publish(self.topic_publish, command_json, qos=1)
@@ -3485,25 +3567,26 @@ class BambuMQTTClient:
self,
tray_id: int,
k_value: float,
n_coef: float = 0.0,
nozzle_diameter: str = "0.4",
bed_temp: int = 60,
nozzle_temp: int = 220,
max_volumetric_speed: float = 20.0,
filament_id: str = "",
setting_id: str = "",
name: str = "",
cali_idx: int = -1,
) -> bool:
"""Directly set K value (pressure advance) for a tray.
This command sets the K value directly without selecting from stored profiles.
Use this when you want to apply a specific K value to a tray.
Uses the filaments array format required by current firmware.
Args:
tray_id: Global tray ID (ams_id * 4 + slot)
k_value: Pressure advance K value (e.g., 0.020)
n_coef: N coefficient (usually 0.0 for manual, 1.4 for auto-calibration)
nozzle_diameter: Nozzle diameter string (e.g., "0.4")
bed_temp: Bed temperature for calibration reference
nozzle_temp: Nozzle temperature for calibration reference
max_volumetric_speed: Max volumetric speed for calibration reference
filament_id: Filament preset ID (e.g., "GFA02")
setting_id: Setting ID (e.g., "GFSA02_07")
name: Profile display name
cali_idx: Calibration index (-1 for new)
Returns:
True if command was sent, False otherwise
@@ -3512,17 +3595,28 @@ class BambuMQTTClient:
logger.warning("[%s] Cannot set K value: not connected", self.serial_number)
return False
nozzle_id = f"HS00-{nozzle_diameter}"
filament_entry = {
"ams_id": 0,
"cali_idx": cali_idx,
"extruder_id": 0,
"filament_id": filament_id,
"k_value": f"{k_value:.6f}",
"n_coef": "1.400000",
"name": name,
"nozzle_diameter": nozzle_diameter,
"nozzle_id": nozzle_id,
"setting_id": setting_id,
"tray_id": tray_id,
}
command = {
"print": {
"command": "extrusion_cali_set",
"tray_id": tray_id,
"k_value": k_value,
"n_coef": n_coef,
"filaments": [filament_entry],
"nozzle_diameter": nozzle_diameter,
"bed_temp": bed_temp,
"nozzle_temp": nozzle_temp,
"max_volumetric_speed": max_volumetric_speed,
"sequence_id": "0",
"sequence_id": str(self._sequence_id),
}
}
+21 -21
View File
@@ -333,10 +333,9 @@ class PrintScheduler:
if tray_type:
loaded_types.add(tray_type.upper())
# Check external spool (virtual tray, stored in raw_data["vt_tray"])
vt_tray = status.raw_data.get("vt_tray")
if vt_tray:
vt_type = vt_tray.get("tray_type")
# Check external spool(s) (virtual tray, stored in raw_data["vt_tray"] as list)
for vt in status.raw_data.get("vt_tray") or []:
vt_type = vt.get("tray_type")
if vt_type:
loaded_types.add(vt_type.upper())
@@ -538,23 +537,24 @@ class PrintScheduler:
}
)
# Check external spool (vt_tray)
vt_tray = status.raw_data.get("vt_tray")
if vt_tray and vt_tray.get("tray_type"):
color = self._normalize_color(vt_tray.get("tray_color", ""))
filaments.append(
{
"type": vt_tray["tray_type"],
"color": color,
"tray_info_idx": vt_tray.get("tray_info_idx", ""),
"ams_id": -1,
"tray_id": 0,
"is_ht": False,
"is_external": True,
"global_tray_id": 254,
"extruder_id": 0 if ams_extruder_map else None,
}
)
# Check external spool(s) (vt_tray is a list)
for idx, vt in enumerate(status.raw_data.get("vt_tray") or []):
if vt.get("tray_type"):
color = self._normalize_color(vt.get("tray_color", ""))
tray_id = int(vt.get("id", 254))
filaments.append(
{
"type": vt["tray_type"],
"color": color,
"tray_info_idx": vt.get("tray_info_idx", ""),
"ams_id": -1,
"tray_id": idx,
"is_ht": False,
"is_external": True,
"global_tray_id": tray_id,
"extruder_id": (tray_id - 254) if ams_extruder_map else None,
}
)
return filaments
+32 -29
View File
@@ -502,7 +502,7 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo
"""
# Parse AMS data from raw_data
ams_units = []
vt_tray = None
vt_tray = []
raw_data = state.raw_data or {}
# Build K-profile lookup map: cali_idx -> k_value
@@ -578,37 +578,40 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo
}
)
# Parse virtual tray (external spool)
# Parse virtual tray (external spool) — now a list
if "vt_tray" in raw_data:
vt_data = raw_data["vt_tray"]
vt_tag_uid = vt_data.get("tag_uid")
if vt_tag_uid in ("", "0000000000000000"):
vt_tag_uid = None
vt_tray_uuid = vt_data.get("tray_uuid")
if vt_tray_uuid in ("", "00000000000000000000000000000000"):
vt_tray_uuid = None
for vt_data in raw_data["vt_tray"]:
vt_tag_uid = vt_data.get("tag_uid")
if vt_tag_uid in ("", "0000000000000000"):
vt_tag_uid = None
vt_tray_uuid = vt_data.get("tray_uuid")
if vt_tray_uuid in ("", "00000000000000000000000000000000"):
vt_tray_uuid = None
# Get K value for vt_tray
vt_k_value = vt_data.get("k")
vt_cali_idx = vt_data.get("cali_idx")
if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map:
vt_k_value = kprofile_map[vt_cali_idx]
# Get K value for vt_tray
vt_k_value = vt_data.get("k")
vt_cali_idx = vt_data.get("cali_idx")
if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map:
vt_k_value = kprofile_map[vt_cali_idx]
vt_tray = {
"id": 254,
"tray_color": vt_data.get("tray_color"),
"tray_type": vt_data.get("tray_type"),
"tray_sub_brands": vt_data.get("tray_sub_brands"),
"tray_id_name": vt_data.get("tray_id_name"),
"tray_info_idx": vt_data.get("tray_info_idx"),
"remain": vt_data.get("remain", 0),
"k": vt_k_value,
"cali_idx": vt_cali_idx,
"tag_uid": vt_tag_uid,
"tray_uuid": vt_tray_uuid,
"nozzle_temp_min": vt_data.get("nozzle_temp_min"),
"nozzle_temp_max": vt_data.get("nozzle_temp_max"),
}
tray_id = int(vt_data.get("id", 254))
vt_tray.append(
{
"id": tray_id,
"tray_color": vt_data.get("tray_color"),
"tray_type": vt_data.get("tray_type"),
"tray_sub_brands": vt_data.get("tray_sub_brands"),
"tray_id_name": vt_data.get("tray_id_name"),
"tray_info_idx": vt_data.get("tray_info_idx"),
"remain": vt_data.get("remain", 0),
"k": vt_k_value,
"cali_idx": vt_cali_idx,
"tag_uid": vt_tag_uid,
"tray_uuid": vt_tray_uuid,
"nozzle_temp_min": vt_data.get("nozzle_temp_min"),
"nozzle_temp_max": vt_data.get("nozzle_temp_max"),
}
)
# Get ams_extruder_map from raw_data (populated by MQTT handler from AMS info field)
ams_extruder_map = raw_data.get("ams_extruder_map", {})
+34 -34
View File
@@ -196,16 +196,19 @@ async def auto_assign_spool(
spool: Spool,
printer_manager,
db: AsyncSession,
tray_info_idx: str = "",
) -> SpoolAssignment:
"""Create a SpoolAssignment and auto-configure the AMS slot via MQTT.
Reuses the same MQTT configuration logic as the manual assign endpoint.
For BL spools (RFID-detected), only K-profile commands are sent.
ams_set_filament_setting is NOT sent because the firmware already has
filament configuration from the RFID tag, and sending it would destroy
the RFID-detected state (eye → pen icon in BambuStudio).
"""
from backend.app.api.routes.inventory import MATERIAL_TEMPS
# Get current tray state for fingerprint
fingerprint_color = None
fingerprint_type = None
tray = None
state = printer_manager.get_status(printer_id)
if state and state.raw_data:
from backend.app.api.routes.inventory import _find_tray_in_ams_data
@@ -246,34 +249,14 @@ async def auto_assign_spool(
db.add(assignment)
await db.flush()
# Auto-configure AMS slot via MQTT
# Apply K-profile via MQTT (if available)
# NOTE: Do NOT send ams_set_filament_setting here. This function is only
# called for BL spools (RFID-detected). The firmware already has the filament
# configuration from the RFID tag. Sending ams_set_filament_setting would
# destroy the RFID-detected state (eye → pen icon in BambuStudio/OrcaSlicer).
try:
client = printer_manager.get_client(printer_id)
if client:
tray_type = spool.material
tray_sub_brands = f"{spool.material} {spool.subtype}" if spool.subtype else spool.material
tray_color = spool.rgba or "FFFFFFFF"
tray_info_idx = spool.slicer_filament or ""
setting_id = ""
temp_min, temp_max = MATERIAL_TEMPS.get(spool.material.upper(), (200, 240))
if spool.nozzle_temp_min is not None:
temp_min = spool.nozzle_temp_min
if spool.nozzle_temp_max is not None:
temp_max = spool.nozzle_temp_max
client.ams_set_filament_setting(
ams_id=ams_id,
tray_id=tray_id,
tray_info_idx=tray_info_idx,
tray_type=tray_type,
tray_sub_brands=tray_sub_brands,
tray_color=tray_color,
nozzle_temp_min=temp_min,
nozzle_temp_max=temp_max,
setting_id=setting_id,
)
# Apply K-profile if available
nozzle_diameter = "0.4"
if state and state.nozzles:
@@ -288,23 +271,40 @@ async def auto_assign_spool(
break
if matching_kp and matching_kp.cali_idx is not None:
# The filament_id in extrusion_cali_sel must match the filament preset
# under which the K-profile was calibrated. Use spool.slicer_filament
# (the preset assigned in inventory), falling back to tray's RFID value.
cali_filament_id = spool.slicer_filament or tray_info_idx or ""
client.extrusion_cali_sel(
ams_id=ams_id,
tray_id=tray_id,
cali_idx=matching_kp.cali_idx,
filament_id=tray_info_idx,
filament_id=cali_filament_id,
nozzle_diameter=nozzle_diameter,
setting_id=matching_kp.setting_id,
)
# NOTE: Do NOT send extrusion_cali_set here. extrusion_cali_sel already
# selected the correct profile by cali_idx. Sending extrusion_cali_set
# with the same cali_idx would MODIFY the existing profile's metadata
# (extruder_id, nozzle_id, name), corrupting it.
logger.info(
"Applied K-profile cali_idx=%d for spool %d on printer %d AMS%d-T%d",
matching_kp.cali_idx,
spool.id,
printer_id,
ams_id,
tray_id,
)
logger.info(
"Auto-configured AMS slot ams=%d tray=%d for spool %d on printer %d (RFID match)",
ams_id,
tray_id,
"Auto-assigned spool %d to printer %d AMS%d-T%d (RFID match)",
spool.id,
printer_id,
ams_id,
tray_id,
)
except Exception as e:
logger.warning("MQTT auto-configure failed for spool %d (RFID match): %s", spool.id, e)
logger.warning("K-profile apply failed for spool %d (RFID match): %s", spool.id, e)
return assignment
+9 -8
View File
@@ -66,14 +66,15 @@ def build_ams_tray_lookup(raw_data: dict) -> dict[int, dict]:
"tray_type": tray.get("tray_type", ""),
}
# External spool (global_tray_id = 254)
vt_tray = raw_data.get("vt_tray")
if vt_tray and vt_tray.get("tray_type"):
lookup[254] = {
"tray_uuid": vt_tray.get("tray_uuid", ""),
"tag_uid": vt_tray.get("tag_uid", ""),
"tray_type": vt_tray.get("tray_type", ""),
}
# External spool(s) (vt_tray is a list, global_tray_id from each entry's "id")
for vt in raw_data.get("vt_tray") or []:
if vt.get("tray_type"):
tray_id = int(vt.get("id", 254))
lookup[tray_id] = {
"tray_uuid": vt.get("tray_uuid", ""),
"tag_uid": vt.get("tag_uid", ""),
"tray_type": vt.get("tray_type", ""),
}
return lookup
+11 -2
View File
@@ -263,8 +263,13 @@ async def _track_from_3mf(
tray_now_override: int | None = None
if not slot_to_tray and len(nonzero_slots) == 1:
state = printer_manager.get_status(printer_id)
if state and state.tray_now < 255:
if state and 0 <= state.tray_now <= 254:
tray_now_override = state.tray_now
elif state and state.tray_now == 255:
# 255 = "no filament" on legacy printers, but valid 2nd external spool on H2-series
vt_tray = state.raw_data.get("vt_tray") or []
if any(int(vt.get("id", 0)) == 255 for vt in vt_tray if isinstance(vt, dict)):
tray_now_override = state.tray_now
# Scale factor for partial prints (failed/aborted)
if status == "completed":
@@ -322,7 +327,11 @@ async def _track_from_3mf(
if isinstance(mapped, int) and mapped >= 0:
global_tray_id = mapped
if global_tray_id >= 128:
if global_tray_id >= 254:
# External spool: ams_id=255 (sentinel), tray_id=slot index (0 or 1)
ams_id = 255
tray_id = global_tray_id - 254
elif global_tray_id >= 128:
ams_id = global_tray_id
tray_id = 0
else: