Browse Source

Configure: a setting id as tray_info_idx is sent as its filament id

- POST /printers/{id}/slots/{ams}/{tray}/configure passed a Bambu
  setting id ("GFSL99") from an API client on to the printer, which
  knows filament ids only: the slot came back blank and auto-unlink
  dropped the spool (#3444)
- Converted with setting_id_to_filament_id (#3387 table and SUNLU
  rule); support and SUNLU filament ids stay; the original is kept
  as setting_id unless the client sent one
- Same for kprofile_filament_id, which becomes the slot's id when it
  is realigned to the K profile
maziggy 3 days ago
parent
commit
d6de7aa696

+ 1 - 0
CHANGELOG.md

@@ -219,6 +219,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **Configure accepts a setting id as the filament (#3444, reported by @Novastorm4332)** — A client of the REST API that configured a slot with a spool's slicer filament as `tray_info_idx` sent a Bambu setting id such as `GFSL99` to the printer, which knows only filament ids (`GFL99`): the slot came back without a colour or type and auto-unlink dropped the spool. Assigning the spool already converted it. Configure now converts it the same way, the setting id staying as the slot's setting id, and does the same for a K profile's filament id. Support and SUNLU ids, which also begin with GFS, are filament ids and stay as they are. Bambuddy's own Configure dialog was not affected.
 - **A project saved as .gcode.3mf is treated as a project (#3435, reported by @Mounting9612)** — Bambu Studio saves an unsliced project under whatever name it is given, and one called `Foo.gcode.3mf` went into the File Manager as a printable file. Printing it to a cost centre was refused with "Estimated cost is required for cost center prints", which says nothing about the cause; without billing it would have gone to the printer. A `.gcode.3mf` that holds no G-code is now stored as a project and offered for slicing, files already in the library are re-checked once on the update, and the queue refuses such a file with "… isn't sliced yet. Slice it first, then print the sliced file." The File Manager's Add to Queue judges a file by what it holds too, so a sliced plate exported as a plain `.3mf` can be queued from there.
 - **A spool swapped in mid-print shows on the printer card (#3434, reported by @0xCyberFortress)** — After a runout, a new spool loaded into the same slot and assigned in Bambuddy stayed invisible until the print ended: the printer reports the slot loaded but blank and refuses a filament setting for the slot it is printing from, so the card showed "?" and "No filament assigned". The card now shows the assigned spool on such a slot in its colour, with an amber marker, names it and offers Unassign, saying that the printer has not taken it yet. Bambuddy also stops sending that setting on every AMS change while the slot feeds the print, which the printer refused each time, and sends it once the print is over.
 - **Spoolman: a Bambu spool keeps its filament preset (#3433, reported by @ojimpo)** — In Spoolman mode, a spool the AMS sync created from a Bambu RFID tray was stored without the preset the tray reports, which the built-in inventory keeps. Assigned later to a slot without a reader, such as the external spool, it was written as the generic filament for its material: a Bambu PLA Basic became Generic PLA. The sync now stores the tray's preset on a new spool and fills it in on an existing Bambu spool that has none (a preset set by hand stays). And a Spoolman spool without a preset keeps the slot's current filament when it fits, as in built-in mode, before falling back to the generic one.

+ 32 - 0
backend/app/api/routes/printers.py

@@ -107,6 +107,7 @@ from backend.app.utils.filament_ids import (
     GENERIC_FILAMENT_IDS,
     GENERIC_IDS_REPLACED_ON_REUSE,
     filament_id_to_setting_id,
+    setting_id_to_filament_id,
 )
 from backend.app.utils.filament_types import is_material_name, printer_filament_type
 from backend.app.utils.fts_routing import slot_extruder
@@ -2848,6 +2849,12 @@ def _orca_generic_filament_id(material: str) -> str:
     )
 
 
+def _as_filament_id(value: str) -> str:
+    """The filament id of an official Bambu setting id ("GFSL99_02" → "GFL99");
+    anything else unchanged (#3444)."""
+    return setting_id_to_filament_id(value) if value.startswith("GFS") else value
+
+
 @router.post("/{printer_id}/slots/{ams_id}/{tray_id}/configure")
 async def configure_ams_slot(
     printer_id: int,
@@ -2958,6 +2965,31 @@ async def configure_ams_slot(
             setting_id = tray_info_idx
         tray_info_idx = ""
 
+    # A Bambu setting id ("GFSL99", "GFSL99_02") where a filament id belongs
+    # (#3444). The printer knows filament ids ("GFL99") only: sent as it is,
+    # the slot came back blank and auto-unlink dropped the spool. A spool's
+    # slicer_filament can be either kind, and the assignment path converts it
+    # (normalize_slicer_filament); an API client passing it on to Configure
+    # did not get the same. Converted with the #3387 helper, never by dropping
+    # the "S": support ids ("GFS99") and SUNLU's ("GFSNL03") are filament ids
+    # already and come back unchanged. The setting id stays as setting_id,
+    # unless the client sent one.
+    converted = _as_filament_id(tray_info_idx)
+    if converted != tray_info_idx:
+        logger.info("[configure_ams_slot] tray_info_idx %r is a setting id → %r", tray_info_idx, converted)
+        setting_id = setting_id or tray_info_idx
+        tray_info_idx = converted
+    # The K profile's filament id: it becomes the slot's when they differ
+    # (the realignment below), and the printer's calibration table is keyed
+    # by filament ids as well.
+    converted = _as_filament_id(kprofile_filament_id)
+    if converted != kprofile_filament_id:
+        logger.info(
+            "[configure_ams_slot] kprofile_filament_id %r is a setting id → %r", kprofile_filament_id, converted
+        )
+        kprofile_setting_id = kprofile_setting_id or kprofile_filament_id
+        kprofile_filament_id = converted
+
     # 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

+ 119 - 0
backend/tests/integration/test_configure_setting_id_3444.py

@@ -0,0 +1,119 @@
+"""A Bambu setting id sent to Configure as the filament id (#3444).
+
+The reporter's iOS client called ``/slots/{ams}/{tray}/configure`` with a
+spool's ``slicer_filament`` as ``tray_info_idx``: "GFSL99", a setting id. The
+route passed it on, the P1S reported the slot blank and auto-unlink dropped the
+spool. The assignment path converts it (normalize_slicer_filament); Configure
+now does the same, with the #3387 helper.
+"""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+from httpx import AsyncClient
+
+
+def _client():
+    client = MagicMock()
+    client.ams_set_filament_setting.return_value = True
+    client.extrusion_cali_sel.return_value = True
+    return client
+
+
+def _status():
+    status = MagicMock()
+    status.raw_data = {"ams": {"ams": []}}
+    status.nozzles = [MagicMock(nozzle_diameter="0.4")]
+    status.ams_extruder_map = None
+    status.kprofiles = []
+    return status
+
+
+async def _configure(async_client, printer_id, **params):
+    client = _client()
+    with patch("backend.app.api.routes.printers.printer_manager") as pm:
+        pm.get_client.return_value = client
+        pm.get_status.return_value = _status()
+        response = await async_client.post(
+            f"/api/v1/printers/{printer_id}/slots/0/0/configure",
+            params={
+                "tray_type": "PLA",
+                "tray_sub_brands": "Generic PLA",
+                "tray_color": "000000FF",
+                "nozzle_temp_min": 190,
+                "nozzle_temp_max": 250,
+                **params,
+            },
+        )
+    assert response.status_code == 200, response.text
+    return client
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+@pytest.mark.parametrize(
+    ("sent", "filament_id", "setting_id"),
+    [
+        ("GFSL99", "GFL99", "GFSL99"),  # the report
+        ("GFSL99_02", "GFL99", "GFSL99_02"),  # with its version
+        ("GFSA05_10", "GFA01", "GFSA05_10"),  # Bambu gives this one Matte's id (#3387)
+        ("GFSNLS03", "GFSNL03", "GFSNLS03"),  # SUNLU
+    ],
+)
+async def test_a_setting_id_is_sent_as_its_filament_id(
+    async_client: AsyncClient, printer_factory, sent, filament_id, setting_id
+):
+    printer = await printer_factory(model="P1S")
+    client = await _configure(async_client, printer.id, tray_info_idx=sent)
+    slot = client.ams_set_filament_setting.call_args.kwargs
+    assert slot["tray_info_idx"] == filament_id
+    assert slot["setting_id"] == setting_id
+    assert client.extrusion_cali_sel.call_args.kwargs["filament_id"] == filament_id
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+@pytest.mark.parametrize("sent", ["GFL99", "GFS99", "GFSNL03", "P4d64437"])
+async def test_filament_ids_are_left_alone(async_client: AsyncClient, printer_factory, sent):
+    """Support ids ("GFS99") and SUNLU's ("GFSNL03") start with GFS but are filament ids."""
+    printer = await printer_factory(model="P1S")
+    client = await _configure(async_client, printer.id, tray_info_idx=sent)
+    assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == sent
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_the_clients_own_setting_id_is_kept(async_client: AsyncClient, printer_factory):
+    printer = await printer_factory(model="P1S")
+    client = await _configure(async_client, printer.id, tray_info_idx="GFSL99", setting_id="GFSL99_01")
+    slot = client.ams_set_filament_setting.call_args.kwargs
+    assert (slot["tray_info_idx"], slot["setting_id"]) == ("GFL99", "GFSL99_01")
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_a_k_profile_setting_id_is_converted_too(async_client: AsyncClient, printer_factory):
+    """The K profile's filament id becomes the slot's when they differ, and the
+    calibration table is keyed by filament ids."""
+    printer = await printer_factory(model="P1S")
+    client = await _configure(
+        async_client, printer.id, tray_info_idx="GFL99", cali_idx=3, kprofile_filament_id="GFSL99_02"
+    )
+    slot = client.ams_set_filament_setting.call_args.kwargs
+    assert slot["tray_info_idx"] == "GFL99"
+    assert client.extrusion_cali_sel.call_args.kwargs["filament_id"] == "GFL99"
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_a_k_profile_for_another_preset_realigns_the_slot_with_its_filament_id(
+    async_client: AsyncClient, printer_factory
+):
+    """Realigned to the K profile, the slot gets its filament id, and its setting id as setting_id."""
+    printer = await printer_factory(model="P1S")
+    client = await _configure(
+        async_client, printer.id, tray_info_idx="GFL99", cali_idx=3, kprofile_filament_id="GFSL05_07"
+    )
+    slot = client.ams_set_filament_setting.call_args.kwargs
+    assert (slot["tray_info_idx"], slot["setting_id"]) == ("GFL05", "GFSL05_07")
+    assert client.extrusion_cali_sel.call_args.kwargs["filament_id"] == "GFL05"