|
@@ -88,7 +88,7 @@ from backend.app.services.printer_media import (
|
|
|
remove_printer_files_zip,
|
|
remove_printer_files_zip,
|
|
|
start_printer_files_job,
|
|
start_printer_files_job,
|
|
|
)
|
|
)
|
|
|
-from backend.app.services.slicer_filament_resolver import _ORCA_PROFILE_ID
|
|
|
|
|
|
|
+from backend.app.services.slicer_filament_resolver import _ORCA_PROFILE_ID, lookup_orca_filament_id
|
|
|
from backend.app.services.slot_nozzle import resolve_slot_nozzle
|
|
from backend.app.services.slot_nozzle import resolve_slot_nozzle
|
|
|
from backend.app.utils.ams_humidity import ams_humidity_percent
|
|
from backend.app.utils.ams_humidity import ams_humidity_percent
|
|
|
from backend.app.utils.filament_ids import filament_id_to_setting_id
|
|
from backend.app.utils.filament_ids import filament_id_to_setting_id
|
|
@@ -2556,6 +2556,7 @@ async def get_slot_presets(
|
|
|
"tray_id": mapping.tray_id,
|
|
"tray_id": mapping.tray_id,
|
|
|
"preset_id": mapping.preset_id,
|
|
"preset_id": mapping.preset_id,
|
|
|
"preset_name": mapping.preset_name,
|
|
"preset_name": mapping.preset_name,
|
|
|
|
|
+ "tray_info_idx": mapping.tray_info_idx,
|
|
|
}
|
|
}
|
|
|
for mapping in mappings
|
|
for mapping in mappings
|
|
|
}
|
|
}
|
|
@@ -2587,6 +2588,7 @@ async def get_slot_preset(
|
|
|
"tray_id": mapping.tray_id,
|
|
"tray_id": mapping.tray_id,
|
|
|
"preset_id": mapping.preset_id,
|
|
"preset_id": mapping.preset_id,
|
|
|
"preset_name": mapping.preset_name,
|
|
"preset_name": mapping.preset_name,
|
|
|
|
|
+ "tray_info_idx": mapping.tray_info_idx,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2598,10 +2600,20 @@ async def save_slot_preset(
|
|
|
preset_id: str,
|
|
preset_id: str,
|
|
|
preset_name: str,
|
|
preset_name: str,
|
|
|
preset_source: str = "cloud",
|
|
preset_source: str = "cloud",
|
|
|
|
|
+ tray_info_idx: str | None = None,
|
|
|
_=RequirePermissionIfAuthEnabled(Permission.PRINTERS_UPDATE),
|
|
_=RequirePermissionIfAuthEnabled(Permission.PRINTERS_UPDATE),
|
|
|
db: AsyncSession = Depends(get_db),
|
|
db: AsyncSession = Depends(get_db),
|
|
|
):
|
|
):
|
|
|
- """Save a preset mapping for a specific slot."""
|
|
|
|
|
|
|
+ """Save a preset mapping for a specific slot.
|
|
|
|
|
+
|
|
|
|
|
+ ``tray_info_idx`` is the filament id the slot was configured with (what
|
|
|
|
|
+ ``configure`` returns). Recording it lets the slot card notice a later
|
|
|
|
|
+ re-configuration from elsewhere and stop showing this preset (#3216).
|
|
|
|
|
+ Omitted, it is cleared, so a stale id never outlives the preset it came with.
|
|
|
|
|
+ """
|
|
|
|
|
+ # Longer than the column is not an id the printer holds: record nothing,
|
|
|
|
|
+ # which keeps the row shown, rather than refuse the whole save.
|
|
|
|
|
+ tray_info_idx = tray_info_idx if tray_info_idx and len(tray_info_idx) <= 32 else None
|
|
|
# Check printer exists
|
|
# Check printer exists
|
|
|
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
|
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
|
|
if not result.scalar_one_or_none():
|
|
if not result.scalar_one_or_none():
|
|
@@ -2622,6 +2634,7 @@ async def save_slot_preset(
|
|
|
mapping.preset_id = preset_id
|
|
mapping.preset_id = preset_id
|
|
|
mapping.preset_name = preset_name
|
|
mapping.preset_name = preset_name
|
|
|
mapping.preset_source = preset_source
|
|
mapping.preset_source = preset_source
|
|
|
|
|
+ mapping.tray_info_idx = tray_info_idx
|
|
|
else:
|
|
else:
|
|
|
# Create new
|
|
# Create new
|
|
|
mapping = SlotPresetMapping(
|
|
mapping = SlotPresetMapping(
|
|
@@ -2631,6 +2644,7 @@ async def save_slot_preset(
|
|
|
preset_id=preset_id,
|
|
preset_id=preset_id,
|
|
|
preset_name=preset_name,
|
|
preset_name=preset_name,
|
|
|
preset_source=preset_source,
|
|
preset_source=preset_source,
|
|
|
|
|
+ tray_info_idx=tray_info_idx,
|
|
|
)
|
|
)
|
|
|
db.add(mapping)
|
|
db.add(mapping)
|
|
|
|
|
|
|
@@ -2643,6 +2657,7 @@ async def save_slot_preset(
|
|
|
"preset_id": mapping.preset_id,
|
|
"preset_id": mapping.preset_id,
|
|
|
"preset_name": mapping.preset_name,
|
|
"preset_name": mapping.preset_name,
|
|
|
"preset_source": mapping.preset_source,
|
|
"preset_source": mapping.preset_source,
|
|
|
|
|
+ "tray_info_idx": mapping.tray_info_idx,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2771,6 +2786,46 @@ async def get_slot_spool_defaults(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# Generic Bambu filament ids by material, as the Configure dialog has always
|
|
|
|
|
+# picked them for a preset without an id of its own (#3216 moved the Orca case
|
|
|
|
|
+# here). Wider than configure_ams_slot's own table: Silk, High Speed, PCTG, PE
|
|
|
|
|
+# and PP have generics of their own.
|
|
|
|
|
+_ORCA_GENERIC_IDS = {
|
|
|
|
|
+ "PLA": "GFL99",
|
|
|
|
|
+ "PLA-CF": "GFL98",
|
|
|
|
|
+ "PLA SILK": "GFL96",
|
|
|
|
|
+ "PLA HIGH SPEED": "GFL95",
|
|
|
|
|
+ "PETG": "GFG99",
|
|
|
|
|
+ "PETG HF": "GFG96",
|
|
|
|
|
+ "PETG-CF": "GFG98",
|
|
|
|
|
+ "PCTG": "GFG97",
|
|
|
|
|
+ "ABS": "GFB99",
|
|
|
|
|
+ "ASA": "GFB98",
|
|
|
|
|
+ "PC": "GFC99",
|
|
|
|
|
+ "PA": "GFN99",
|
|
|
|
|
+ "PA-CF": "GFN98",
|
|
|
|
|
+ "NYLON": "GFN99",
|
|
|
|
|
+ "TPU": "GFU99",
|
|
|
|
|
+ "PVA": "GFS99",
|
|
|
|
|
+ "HIPS": "GFS98",
|
|
|
|
|
+ "PE": "GFP99",
|
|
|
|
|
+ "PP": "GFP97",
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _orca_generic_filament_id(material: str) -> str:
|
|
|
|
|
+ """The generic for a material, tried as given, without a CF suffix, without
|
|
|
|
|
+ a trailing "+", then by its first word -- the dialog's order."""
|
|
|
|
|
+ material = (material or "").upper().strip()
|
|
|
|
|
+ return (
|
|
|
|
|
+ _ORCA_GENERIC_IDS.get(material)
|
|
|
|
|
+ or _ORCA_GENERIC_IDS.get(re.sub(r"[-\s]?CF$", "", material))
|
|
|
|
|
+ or _ORCA_GENERIC_IDS.get(re.sub(r"\+$", "", material))
|
|
|
|
|
+ or _ORCA_GENERIC_IDS.get(re.split(r"[-\s]", material)[0])
|
|
|
|
|
+ or ""
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@router.post("/{printer_id}/slots/{ams_id}/{tray_id}/configure")
|
|
@router.post("/{printer_id}/slots/{ams_id}/{tray_id}/configure")
|
|
|
async def configure_ams_slot(
|
|
async def configure_ams_slot(
|
|
|
printer_id: int,
|
|
printer_id: int,
|
|
@@ -2788,8 +2843,9 @@ async def configure_ams_slot(
|
|
|
kprofile_filament_id: str = Query(""),
|
|
kprofile_filament_id: str = Query(""),
|
|
|
kprofile_setting_id: str = Query(""),
|
|
kprofile_setting_id: str = Query(""),
|
|
|
k_value: float = Query(0.0),
|
|
k_value: float = Query(0.0),
|
|
|
|
|
+ orca_profile_id: str = Query(""),
|
|
|
db: AsyncSession = Depends(get_db),
|
|
db: AsyncSession = Depends(get_db),
|
|
|
- _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
|
|
|
|
|
|
|
+ current_user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
|
|
|
):
|
|
):
|
|
|
"""Configure an AMS slot with a specific filament setting and K profile.
|
|
"""Configure an AMS slot with a specific filament setting and K profile.
|
|
|
|
|
|
|
@@ -2812,6 +2868,9 @@ async def configure_ams_slot(
|
|
|
setting_id: Full setting ID with version (e.g., "GFSL05_07") - optional
|
|
setting_id: Full setting ID with version (e.g., "GFSL05_07") - optional
|
|
|
kprofile_filament_id: K profile's filament_id for proper K profile linking
|
|
kprofile_filament_id: K profile's filament_id for proper K profile linking
|
|
|
k_value: Direct K value to set (0.0 to skip direct K value setting)
|
|
k_value: Direct K value to set (0.0 to skip direct K value setting)
|
|
|
|
|
+ orca_profile_id: Orca Cloud profile the slot is being set to. With no
|
|
|
|
|
+ tray_info_idx, the profile's own filament id is looked up here
|
|
|
|
|
+ (#3216); the response says when none was found.
|
|
|
"""
|
|
"""
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
logger.info("[configure_ams_slot] printer_id=%s, ams_id=%s, tray_id=%s", printer_id, ams_id, tray_id)
|
|
logger.info("[configure_ams_slot] printer_id=%s, ams_id=%s, tray_id=%s", printer_id, ams_id, tray_id)
|
|
@@ -2877,6 +2936,40 @@ async def configure_ams_slot(
|
|
|
setting_id = tray_info_idx
|
|
setting_id = tray_info_idx
|
|
|
tray_info_idx = ""
|
|
tray_info_idx = ""
|
|
|
|
|
|
|
|
|
|
+ # An Orca Cloud profile: look up the filament id it puts in the slot. It is
|
|
|
|
|
+ # the one value OrcaSlicer's "Sync filaments" matches a slot by, so a
|
|
|
|
|
+ # generic here is what turned every Orca custom filament into "Generic
|
|
|
|
|
+ # <material>" in the slicer (#3216). Done here rather than in the browser so
|
|
|
|
|
+ # the outcome is in the log, and so a failure can be reported back.
|
|
|
|
|
+ orca_fallback_reason = ""
|
|
|
|
|
+ if orca_profile_id and not tray_info_idx:
|
|
|
|
|
+ orca_lookup = await lookup_orca_filament_id(db, current_user, orca_profile_id)
|
|
|
|
|
+ found = orca_lookup.filament_id
|
|
|
|
|
+ reason = orca_lookup.reason
|
|
|
|
|
+ if found and (
|
|
|
|
|
+ found.startswith("PFUS")
|
|
|
|
|
+ or found.startswith("PFCN")
|
|
|
|
|
+ or _ORCA_PROFILE_ID.fullmatch(found)
|
|
|
|
|
+ or is_material_name(found)
|
|
|
|
|
+ ):
|
|
|
|
|
+ # Same refusal as the guard above: never put an id the printer
|
|
|
|
|
+ # cannot store in the slot.
|
|
|
|
|
+ found, reason = "", "no_filament_id"
|
|
|
|
|
+ if found:
|
|
|
|
|
+ tray_info_idx = found
|
|
|
|
|
+ else:
|
|
|
|
|
+ orca_fallback_reason = reason or "no_filament_id"
|
|
|
|
|
+ # The generic the Configure dialog picked for an Orca profile before
|
|
|
|
|
+ # the lookup moved here -- its table, not the shorter one below, so
|
|
|
|
|
+ # PLA Silk, PCTG, PP and PE keep their own generics.
|
|
|
|
|
+ tray_info_idx = _orca_generic_filament_id(requested_tray_type)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[configure_ams_slot] Orca profile %r → tray_info_idx=%r (%s)",
|
|
|
|
|
+ orca_profile_id,
|
|
|
|
|
+ tray_info_idx,
|
|
|
|
|
+ orca_lookup.source or orca_fallback_reason,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
# Resolve tray_info_idx for the MQTT command.
|
|
# Resolve tray_info_idx for the MQTT command.
|
|
|
# Priority:
|
|
# Priority:
|
|
|
# 1. Use the provided tray_info_idx if set, once the guard above has had
|
|
# 1. Use the provided tray_info_idx if set, once the guard above has had
|
|
@@ -2934,7 +3027,11 @@ async def configure_ams_slot(
|
|
|
current_tray_type = cur_tray.get("tray_type", "")
|
|
current_tray_type = cur_tray.get("tray_type", "")
|
|
|
|
|
|
|
|
if (
|
|
if (
|
|
|
- current_tray_info_idx
|
|
|
|
|
|
|
+ # An Orca profile without an id must not inherit whatever specific
|
|
|
|
|
+ # filament the slot held before: that would put the previous spool's
|
|
|
|
|
+ # preset in front of the slicer. It gets the generic for its material.
|
|
|
|
|
+ not orca_profile_id
|
|
|
|
|
+ and current_tray_info_idx
|
|
|
and current_tray_info_idx not in _GENERIC_ID_VALUES
|
|
and current_tray_info_idx not in _GENERIC_ID_VALUES
|
|
|
and current_tray_type
|
|
and current_tray_type
|
|
|
and current_tray_type.upper() == tray_type.upper()
|
|
and current_tray_type.upper() == tray_type.upper()
|
|
@@ -2981,11 +3078,13 @@ async def configure_ams_slot(
|
|
|
# valid tray_info_idx (GF* official, P* local — not PFUS* cloud-user
|
|
# valid tray_info_idx (GF* official, P* local — not PFUS* cloud-user
|
|
|
# which the slicer rejects in tray_info_idx).
|
|
# which the slicer rejects in tray_info_idx).
|
|
|
effective_setting_id = setting_id
|
|
effective_setting_id = setting_id
|
|
|
|
|
+ realigned_to_kprofile = False
|
|
|
if (
|
|
if (
|
|
|
kprofile_filament_id
|
|
kprofile_filament_id
|
|
|
and kprofile_filament_id != effective_tray_info_idx
|
|
and kprofile_filament_id != effective_tray_info_idx
|
|
|
and not kprofile_filament_id.startswith("PFUS")
|
|
and not kprofile_filament_id.startswith("PFUS")
|
|
|
):
|
|
):
|
|
|
|
|
+ realigned_to_kprofile = True
|
|
|
logger.info(
|
|
logger.info(
|
|
|
"[configure_ams_slot] realigning slot filament context to kp: tray_info_idx %r → %r, setting_id %r → %r",
|
|
"[configure_ams_slot] realigning slot filament context to kp: tray_info_idx %r → %r, setting_id %r → %r",
|
|
|
effective_tray_info_idx,
|
|
effective_tray_info_idx,
|
|
@@ -3243,6 +3342,13 @@ async def configure_ams_slot(
|
|
|
return {
|
|
return {
|
|
|
"success": True,
|
|
"success": True,
|
|
|
"message": f"Configured AMS {ams_id} tray {tray_id} with {tray_sub_brands}",
|
|
"message": f"Configured AMS {ams_id} tray {tray_id} with {tray_sub_brands}",
|
|
|
|
|
+ # What the slot was actually given, for the slot preset row (#3216).
|
|
|
|
|
+ "tray_info_idx": effective_tray_info_idx,
|
|
|
|
|
+ # Set when an Orca profile's own filament id could not be used, so the
|
|
|
|
|
+ # slicer will see the generic for the material: "no_filament_id",
|
|
|
|
|
+ # "lookup_failed" or "no_permission". Cleared when a K-profile realigned
|
|
|
|
|
+ # the slot to its own specific filament after all.
|
|
|
|
|
+ "orca_fallback_reason": "" if realigned_to_kprofile else orca_fallback_reason,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|