mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
The A2L reports its 4-slot AMS Lite as unit id 16, but its slot-presence
bitmasks sit at bit base 24 (id 6) and it reports tray_now as a local 0-3
slot. Fed the raw id 16, the ams_id*4+slot convention probed bits 64-67
(always zero) and marked loaded slots empty; the local tray_now was read as
global, so usage deducted from the wrong spool (or not at all); and the
ams_id<=7 DB constraint rejected id-16 Spoolman links.
Normalise the Lite 16->6 at the MQTT ingest boundary so global tray ids land
at 24-27 - matching the firmware's own bit base, working with every existing
ams_id*4+slot consumer, colliding with nothing, and passing the DB
constraint. Globalise tray_now to 24+slot, widen the valid-tray guards, label
the unit "AMS Lite", and build the confirmed ams_mapping2 {ams_id:16,
slot_id:0-3} / flat 0-3 for dispatch. Outbound slot commands translate 6->16
on the wire via a single helper. Self-scoping: only unit id 16 is touched, so
all other printers/AMS types are unaffected. One uncaptured wire field (the
physical global tray on load/cali) is extrapolated and isolated to the helper.
183 lines
7.0 KiB
Python
183 lines
7.0 KiB
Python
import logging
|
|
|
|
from backend.app.core.database import async_session
|
|
from backend.app.core.websocket import ws_manager
|
|
from backend.app.models.printer import Printer
|
|
from backend.app.models.spool_assignment import SpoolAssignment
|
|
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
|
from backend.app.services.bambu_mqtt import PrinterState
|
|
from backend.app.services.notification_service import notification_service
|
|
from backend.app.services.printer_manager import printer_manager
|
|
|
|
|
|
def _global_tray_from_assignment(ams_id: int, tray_id: int) -> int:
|
|
"""Convert an assignment tuple to Bambuddy global tray ID."""
|
|
if ams_id in (254, 255):
|
|
return 254 + tray_id
|
|
if ams_id >= 128:
|
|
return ams_id
|
|
return ams_id * 4 + tray_id
|
|
|
|
|
|
def _slot_label_from_global_tray(global_tray_id: int) -> str:
|
|
"""Return a human-readable slot label from a global tray ID."""
|
|
if global_tray_id == 254:
|
|
return "Ext-L"
|
|
if global_tray_id == 255:
|
|
return "Ext-R"
|
|
if global_tray_id >= 128:
|
|
return f"HT-{chr(65 + (global_tray_id - 128))}"
|
|
# 24-27 = A2L AMS-Lite (normalised unit 6); see a2l-am-unit-16.
|
|
if 24 <= global_tray_id <= 27:
|
|
return f"Lite-{(global_tray_id % 4) + 1}"
|
|
ams_id = global_tray_id // 4
|
|
tray_id = global_tray_id % 4
|
|
return f"{chr(65 + ams_id)}{tray_id + 1}"
|
|
|
|
|
|
def _tray_profile_and_color_for_global_id(state: PrinterState | None, global_tray_id: int) -> tuple[str, str]:
|
|
"""Resolve expected tray material/profile and color for a global tray ID from current printer state."""
|
|
if not state or not state.raw_data:
|
|
return ("Unknown", "Unknown")
|
|
|
|
ams_raw = state.raw_data.get("ams", {})
|
|
ams_units = ams_raw.get("ams", []) if isinstance(ams_raw, dict) else ams_raw if isinstance(ams_raw, list) else []
|
|
|
|
vt_trays = state.raw_data.get("vt_tray", [])
|
|
if not isinstance(vt_trays, list):
|
|
vt_trays = []
|
|
|
|
for tray in vt_trays:
|
|
if not isinstance(tray, dict):
|
|
continue
|
|
if int(tray.get("id", -1)) == global_tray_id:
|
|
profile = tray.get("tray_sub_brands") or tray.get("tray_type") or "Unknown"
|
|
color = tray.get("tray_color") or "Unknown"
|
|
return (profile, color)
|
|
|
|
for ams in ams_units:
|
|
if not isinstance(ams, dict):
|
|
continue
|
|
ams_id = int(ams.get("id", -1))
|
|
trays = ams.get("tray", [])
|
|
if not isinstance(trays, list):
|
|
continue
|
|
for tray in trays:
|
|
if not isinstance(tray, dict):
|
|
continue
|
|
tray_id = int(tray.get("id", -1))
|
|
candidate = ams_id if ams_id >= 128 else (ams_id * 4 + tray_id)
|
|
if candidate == global_tray_id:
|
|
profile = tray.get("tray_sub_brands") or tray.get("tray_type") or "Unknown"
|
|
color = tray.get("tray_color") or "Unknown"
|
|
return (profile, color)
|
|
|
|
return ("Unknown", "Unknown")
|
|
|
|
|
|
def _decode_mqtt_mapping_to_global_trays(mapping_raw: object) -> list[int]:
|
|
"""Decode printer MQTT mapping values into Bambuddy global tray IDs."""
|
|
if not isinstance(mapping_raw, list) or not mapping_raw:
|
|
return []
|
|
|
|
decoded: list[int] = []
|
|
for value in mapping_raw:
|
|
try:
|
|
if isinstance(value, int):
|
|
encoded = value
|
|
elif isinstance(value, str):
|
|
encoded = int(value, 10)
|
|
else:
|
|
continue
|
|
except ValueError:
|
|
continue
|
|
|
|
if encoded >= 65535:
|
|
continue
|
|
|
|
ams_hw_id = (encoded >> 8) & 0xFF
|
|
slot = encoded & 0xFF
|
|
|
|
if 0 <= ams_hw_id <= 3:
|
|
decoded.append(ams_hw_id * 4 + (slot & 0x03))
|
|
elif 128 <= ams_hw_id <= 135:
|
|
decoded.append(ams_hw_id)
|
|
elif ams_hw_id in (254, 255):
|
|
decoded.append(255 if slot == 255 else 254)
|
|
|
|
return decoded
|
|
|
|
|
|
async def notify_missing_spool_assignments_on_print_start(
|
|
printer_id: int,
|
|
data: dict,
|
|
logger: logging.Logger,
|
|
) -> None:
|
|
"""Send notification when print-start mapping references unassigned trays."""
|
|
explicit_mapping = data.get("ams_mapping")
|
|
explicit_values = (
|
|
[value for value in explicit_mapping if isinstance(value, int)] if isinstance(explicit_mapping, list) else []
|
|
)
|
|
raw_mapping = data.get("raw_data", {}).get("mapping") if isinstance(data.get("raw_data"), dict) else None
|
|
decoded_values = _decode_mqtt_mapping_to_global_trays(raw_mapping)
|
|
mapping_values = explicit_values if explicit_values else decoded_values
|
|
|
|
used_global_trays = {value for value in mapping_values if value >= 0}
|
|
if not used_global_trays:
|
|
return
|
|
|
|
try:
|
|
async with async_session() as db:
|
|
printer = await db.get(Printer, printer_id)
|
|
printer_name = printer.name if printer else f"Printer {printer_id}"
|
|
|
|
# A tray is "assigned" if it has a row in EITHER table: the legacy
|
|
# spool_assignment table (internal-inventory mode) or
|
|
# spoolman_slot_assignments (Spoolman mode — the binding
|
|
# source-of-truth since #1119). Querying only the legacy table
|
|
# flagged every used tray as missing on every Spoolman-mode print
|
|
# (#1473). Both tables expose printer_id / ams_id / tray_id in the
|
|
# same shape, so _global_tray_from_assignment works on either.
|
|
legacy_rows = (
|
|
await db.execute(SpoolAssignment.__table__.select().where(SpoolAssignment.printer_id == printer_id))
|
|
).fetchall()
|
|
spoolman_rows = (
|
|
await db.execute(
|
|
SpoolmanSlotAssignment.__table__.select().where(SpoolmanSlotAssignment.printer_id == printer_id)
|
|
)
|
|
).fetchall()
|
|
assigned_global_trays = {
|
|
_global_tray_from_assignment(row.ams_id, row.tray_id) for row in (*legacy_rows, *spoolman_rows)
|
|
}
|
|
|
|
missing_global = sorted(used_global_trays - assigned_global_trays)
|
|
if not missing_global:
|
|
return
|
|
|
|
state = printer_manager.get_status(printer_id)
|
|
missing_slots = []
|
|
for global_id in missing_global:
|
|
profile, color = _tray_profile_and_color_for_global_id(state, global_id)
|
|
missing_slots.append(
|
|
{
|
|
"slot": _slot_label_from_global_tray(global_id),
|
|
"profile": profile,
|
|
"color": color,
|
|
}
|
|
)
|
|
|
|
await ws_manager.send_missing_spool_assignment(
|
|
printer_id=printer_id,
|
|
printer_name=printer_name,
|
|
missing_slots=missing_slots,
|
|
)
|
|
|
|
await notification_service.on_print_missing_spool_assignment(
|
|
printer_id=printer_id,
|
|
printer_name=printer_name,
|
|
missing_slots=missing_slots,
|
|
db=db,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Missing spool-assignment notification failed: %s", e)
|