mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user