| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- 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.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))}"
- 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}"
- assignments_result = await db.execute(
- SpoolAssignment.__table__.select().where(SpoolAssignment.printer_id == printer_id)
- )
- assignments = assignments_result.fetchall()
- assigned_global_trays = {
- _global_tray_from_assignment(assignment.ams_id, assignment.tray_id) for assignment in assignments
- }
- 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)
|