mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
The internal slicer picked PETG for a PLA plate and an A1 process for a P1S. Both come from the sidecar's bundled-profile listing, fixed in the sidecar repo; this is the consuming half plus the hardening that keeps an older sidecar degrading rather than breaking. Standard-tier presets now carry the compatible_printers the sidecar reports. That list is the only truthful account of which printer a preset belongs to, because the bundle ships no process preset named after a P1S, an X1, an X1E or an H2D Pro -- all ten of the P1S's are named "@BBL X1C" and name the P1S only in that list. Reading the printer out of the preset NAME therefore made a P1S look like it had no compatible process at all: all 198 hid behind "Show all" and the auto-pick fell through to an alphabetically-first 0.06mm Fine @BBL A1 0.2 nozzle the CLI refused. A P1S now gets 0.20mm Standard @BBL X1C and 73 filaments instead of 4. An older sidecar reports nothing here, which leaves the name matcher in place -- degraded as before, not broken. Material is now a hard partition in the filament pre-pick rather than a +10 bonus. A preset stating a different material than the plate asks for is the wrong preset, not a worse one: wrong nozzle temperature, wrong bed temperature, wrong flow. A preset stating NO material stays eligible -- unknown is not wrong, and 32 shipped profiles genuinely have none. The same rule reaches the retain path, which held a slot on printer-compatibility alone and so cemented a wrong-material pick through every re-pick. A preset the user chose themselves is exempt: printing PETG on a plate a designer labelled PLA is a legitimate thing to do, and this rule exists to correct the auto-pick, not to overrule the user. Two more, both found while tracing this and neither reported: Among process presets equally valid for the selected printer, the one nearest a 0.2mm layer height now wins. Within a tier the list is alphabetical and Bambu's naming puts the finest height first, so every slice that did not name its own process silently got 0.08mm Extra Fine on an X1 Carbon and 0.06mm Fine on an A1 mini -- correct presets, nobody's default. Ties break toward the coarser, faster height; a name with no readable height is still pickable when it is the only candidate; a process the 3MF named still wins outright. H2DP is aliased to H2D Pro, the same shape as the A1M rename in #1649 -- the bundle spells the model one way in preset names and another in the printer preset, so an H2D Pro classified all 198 processes as another printer's. Deliberately narrow: H2DP and a plain H2D are different machines and must not collapse. A dropdown the printer filter would empty now shows the unfiltered list instead. That state was reachable for four printer models and told the user nothing; a visible preset for the wrong printer can be changed, an empty dropdown cannot. Verified against live Orca 2.4.2 and BambuStudio 02.08.02.61 sidecars over the real 1156- and 1792-profile trees: every one of the eight printer models tested now auto-picks a 0.20mm process for its own printer, a PLA plate draws a PLA preset and a PETG plate a PETG one. Each change was confirmed to fail its tests when reverted.
176 lines
6.6 KiB
Python
176 lines
6.6 KiB
Python
"""Standard-tier presets carry the slicer's own ``compatible_printers`` (#2982).
|
|
|
|
The sidecar's ``/profiles/bundled`` listing used to report only a name and a
|
|
``base_id``, which left the SliceModal inferring a preset's printer from its
|
|
NAME. That inference cannot work for several Bambu printers, because the bundle
|
|
ships no preset named after them: all ten of a P1S's process presets are named
|
|
``@BBL X1C`` and name the P1S only in ``compatible_printers``. Reading the name
|
|
classified every one of them as belonging to an X1 Carbon, so a P1S had zero
|
|
compatible processes, the dropdown hid all 198, and the auto-pick fell through
|
|
to an alphabetically-first ``0.06mm Fine @BBL A1 0.2 nozzle`` that the CLI then
|
|
refused.
|
|
|
|
These pin the pass-through, including the graceful degrade for a sidecar too
|
|
old to report the field.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.api.routes import slicer_presets as sp
|
|
|
|
|
|
def _sidecar(payload: dict) -> MagicMock:
|
|
svc = MagicMock()
|
|
svc.list_bundled_profiles = AsyncMock(return_value=payload)
|
|
svc.__aenter__ = AsyncMock(return_value=svc)
|
|
svc.__aexit__ = AsyncMock(return_value=False)
|
|
return svc
|
|
|
|
|
|
async def _fetch(payload: dict) -> dict:
|
|
sp._bundled_cache = None
|
|
svc = _sidecar(payload)
|
|
with (
|
|
patch.object(sp, "_resolve_slicer_api_url", AsyncMock(return_value="http://ok")),
|
|
patch.object(sp, "SlicerApiService", return_value=svc),
|
|
):
|
|
return await sp._fetch_bundled_presets(MagicMock())
|
|
|
|
|
|
P1S = "Bambu Lab P1S 0.4 nozzle"
|
|
|
|
# The real shape of the shipped bundle: a process preset named for one printer
|
|
# that names several others, the P1S among them.
|
|
X1C_PROCESS = {
|
|
"name": "0.20mm Standard @BBL X1C",
|
|
"base_id": "fdm_process_single_0.20",
|
|
"compatible_printers": [
|
|
"Bambu Lab X1 Carbon 0.4 nozzle",
|
|
"Bambu Lab X1 0.4 nozzle",
|
|
P1S,
|
|
"Bambu Lab X1E 0.4 nozzle",
|
|
],
|
|
}
|
|
A1_FILAMENT = {
|
|
"name": "Bambu ABS @BBL A1",
|
|
"base_id": "Bambu ABS @base",
|
|
"compatible_printers": ["Bambu Lab A1 0.4 nozzle", "Bambu Lab A1 0.6 nozzle"],
|
|
"filament_type": "ABS",
|
|
"filament_colour": None,
|
|
}
|
|
|
|
|
|
def _payload(**slots) -> dict:
|
|
base: dict = {"printer": [], "process": [], "filament": []}
|
|
base.update(slots)
|
|
return base
|
|
|
|
|
|
class TestTheProcessSlot:
|
|
@pytest.mark.asyncio
|
|
async def test_carries_the_declared_printer_list(self):
|
|
slots = await _fetch(_payload(process=[X1C_PROCESS]))
|
|
assert slots["process"][0].compatible_printers == X1C_PROCESS["compatible_printers"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keeps_a_printer_no_preset_is_named_after(self):
|
|
slots = await _fetch(_payload(process=[X1C_PROCESS]))
|
|
assert P1S in (slots["process"][0].compatible_printers or [])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_older_sidecar_leaves_the_field_unset(self):
|
|
"""No field is not an empty list: unset means "said nothing", which
|
|
keeps the name matcher in play, while an empty list would read as
|
|
"compatible with no printer at all" and hide the preset everywhere."""
|
|
slots = await _fetch(
|
|
_payload(process=[{"name": "0.20mm Standard @BBL X1C", "base_id": None}]),
|
|
)
|
|
assert slots["process"][0].compatible_printers is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalises_a_bare_string(self):
|
|
slots = await _fetch(
|
|
_payload(process=[{"name": "Solo", "base_id": None, "compatible_printers": P1S}]),
|
|
)
|
|
assert slots["process"][0].compatible_printers == [P1S]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_empty_list_reads_as_no_data(self):
|
|
slots = await _fetch(
|
|
_payload(process=[{"name": "Solo", "base_id": None, "compatible_printers": []}]),
|
|
)
|
|
assert slots["process"][0].compatible_printers is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_malformed_value_reads_as_no_data(self):
|
|
slots = await _fetch(
|
|
_payload(process=[{"name": "Solo", "base_id": None, "compatible_printers": 7}]),
|
|
)
|
|
assert slots["process"][0].compatible_printers is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_drops_non_string_entries_but_keeps_the_rest(self):
|
|
slots = await _fetch(
|
|
_payload(
|
|
process=[
|
|
{"name": "Solo", "base_id": None, "compatible_printers": [P1S, None, 3, " "]},
|
|
],
|
|
),
|
|
)
|
|
assert slots["process"][0].compatible_printers == [P1S]
|
|
|
|
|
|
class TestTheFilamentSlot:
|
|
@pytest.mark.asyncio
|
|
async def test_carries_both_the_printer_list_and_the_material(self):
|
|
slots = await _fetch(_payload(filament=[A1_FILAMENT]))
|
|
preset = slots["filament"][0]
|
|
assert preset.compatible_printers == A1_FILAMENT["compatible_printers"]
|
|
assert preset.filament_type == "ABS"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_colourless_bundled_profile_stays_colourless(self):
|
|
"""True of the whole BBL tree at every inheritance depth — colour is a
|
|
spool attribute, not a profile one — so this must not be invented."""
|
|
slots = await _fetch(_payload(filament=[A1_FILAMENT]))
|
|
assert slots["filament"][0].filament_colour is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_unresolvable_material_stays_none(self):
|
|
"""32 shipped filament profiles inherit from a parent the bundle does
|
|
not contain, so the sidecar reports no material for them. They must
|
|
still be listed — the picker treats "unknown" as eligible."""
|
|
slots = await _fetch(
|
|
_payload(
|
|
filament=[
|
|
{
|
|
"name": "PolyLite PLA @BBL H2S",
|
|
"base_id": "PolyLite PLA @base",
|
|
"filament_type": None,
|
|
"compatible_printers": ["Bambu Lab H2S 0.4 nozzle"],
|
|
},
|
|
],
|
|
),
|
|
)
|
|
assert len(slots["filament"]) == 1
|
|
assert slots["filament"][0].filament_type is None
|
|
assert slots["filament"][0].compatible_printers == ["Bambu Lab H2S 0.4 nozzle"]
|
|
|
|
|
|
class TestThePrinterSlot:
|
|
@pytest.mark.asyncio
|
|
async def test_printer_presets_carry_no_compatibility_of_their_own(self):
|
|
"""A printer is what compatibility is measured against; a list on one
|
|
would be meaningless, and the SliceModal never filters that dropdown."""
|
|
slots = await _fetch(
|
|
_payload(
|
|
printer=[
|
|
{"name": P1S, "base_id": None, "compatible_printers": ["nonsense"]},
|
|
],
|
|
),
|
|
)
|
|
assert slots["printer"][0].compatible_printers is None
|
|
assert slots["printer"][0].name == P1S
|