소스 검색

fix(diagnostic): skip external_storage check on A1 / A1 Mini (#1703)

  A1 and A1 Mini ship without a MicroSD slot at all - there is no
  firmware-side "Store sent files on external storage" toggle and the
  slicers don't surface a slicer-side equivalent either. The connection
  diagnostic was reading state.store_to_sdcard (home_flag bit 11), which
  is never set on these models, so the check fell through to fail for
  every A1-series user. Combined with the absent slicer UI it left users
  thinking Bambuddy was wrong about a setting their hardware does not
  have.

  New NO_EXTERNAL_STORAGE_MODELS frozenset in utils/printer_models.py
  enumerates A1, A1 Mini, and their internal codes (N1, N2S, A04, A11,
  A12). has_external_storage() returns False for those, True for
  everything else. Unknown models default to True so the check stays
  active for future Bambu lineup additions - new no-slot models must be
  added to the set explicitly.

  The diagnostic now short-circuits to skip before reading
  store_to_sdcard when printer.model is in the set. X1, P1, P2S, H2,
  and X2D are unchanged - the bit-off -> fail signal is still the right
  read for them.

  The companion FTP-upload-timeout symptom in the same bug report (ftp
  code 28 from BambuStudio when sending to the proxy VP) is a separate
  Docker-bridge-mode networking constraint, not addressed here.
maziggy 2 달 전
부모
커밋
e737c84c6e

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
CHANGELOG.md


+ 8 - 1
backend/app/services/printer_diagnostic.py

@@ -18,6 +18,7 @@ from backend.app.models.printer import Printer
 from backend.app.schemas.printer import DiagnosticCheck, PrinterDiagnosticResult
 from backend.app.services.discovery import is_running_in_docker
 from backend.app.services.printer_manager import printer_manager
+from backend.app.utils.printer_models import has_external_storage
 
 logger = logging.getLogger(__name__)
 
@@ -176,8 +177,14 @@ async def run_connection_diagnostic(
     # banner. An FTP upload-and-verify probe was tried and rejected — the
     # /cache directory is always writable from Bambuddy regardless of
     # either toggle, so the probe always passes and detects nothing.
+    #
+    # Skip entirely on models with no external-storage slot at all (A1
+    # and A1 Mini). They never set home_flag bit 11, so a naive read of
+    # `store_to_sdcard` would fall through to a false `fail` for every
+    # A1-series user (#1703).
     state = printer_manager.get_status(printer.id) if printer else None
-    if state is None or not state.connected:
+    model_has_slot = has_external_storage(getattr(printer, "model", None)) if printer else True
+    if not model_has_slot or state is None or not state.connected:
         checks.append(DiagnosticCheck(id="external_storage", status="skip"))
     elif getattr(state, "store_to_sdcard", None) is True:
         checks.append(DiagnosticCheck(id="external_storage", status="pass"))

+ 36 - 0
backend/app/utils/printer_models.py

@@ -111,6 +111,27 @@ LINEAR_RAIL_MODELS = frozenset(
 )
 
 
+# Models without any external storage (MicroSD / SD card slot).
+# The A1 and A1 Mini ship with internal storage only — there is no
+# firmware-side "Store sent files on external storage" toggle and no
+# slicer-side equivalent surfaces one. The connection diagnostic's
+# external_storage check (printer_diagnostic.py) must skip on these
+# models instead of reporting fail from a 0-valued home_flag bit.
+NO_EXTERNAL_STORAGE_MODELS = frozenset(
+    [
+        # Display names (uppercase, no spaces)
+        "A1",
+        "A1MINI",
+        # Internal codes
+        "N1",  # A1 Mini
+        "N2S",  # A1
+        "A04",  # A1 Mini (alternate)
+        "A11",  # A1
+        "A12",  # A1 Mini
+    ]
+)
+
+
 # Models with an ethernet port.
 # X1, P1P, A1, A1 Mini do NOT have ethernet.
 ETHERNET_MODELS = frozenset(
@@ -173,6 +194,21 @@ def has_ethernet(model: str | None) -> bool:
     return normalized in ETHERNET_MODELS
 
 
+def has_external_storage(model: str | None) -> bool:
+    """Return True if the printer model can have a MicroSD / external storage slot.
+
+    Defaults to True when the model is unknown — the diagnostic only flips
+    its check on for the explicit no-storage list. New models added to the
+    Bambu lineup without a slot must be added to ``NO_EXTERNAL_STORAGE_MODELS``
+    or the diagnostic will continue to evaluate ``store_to_sdcard`` against
+    a hardware feature the printer doesn't have.
+    """
+    if not model:
+        return True
+    normalized = model.strip().upper().replace(" ", "").replace("-", "")
+    return normalized not in NO_EXTERNAL_STORAGE_MODELS
+
+
 def is_dual_nozzle_model(model: str | None) -> bool:
     """Return True if the printer model has two nozzles (H2D family / X2D)."""
     if not model:

+ 26 - 2
backend/tests/unit/services/test_printer_diagnostic.py

@@ -85,8 +85,8 @@ class _Env:
         return False
 
 
-def _printer(ip="192.168.1.50"):
-    return types.SimpleNamespace(id=1, ip_address=ip)
+def _printer(ip="192.168.1.50", model=None):
+    return types.SimpleNamespace(id=1, ip_address=ip, model=model)
 
 
 class TestSameSubnet:
@@ -295,3 +295,27 @@ class TestExternalStorageCheck:
         with _Env(state=bare):
             result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
         assert _statuses(result)["external_storage"] == "skip"
+
+    async def test_skips_on_a1_no_external_storage_slot(self):
+        # Regression for #1703: A1 and A1 Mini ship without a MicroSD slot
+        # at all, so home_flag bit 11 is never set and a naive read would
+        # report `fail` for every A1-series user. The model-aware skip
+        # branch suppresses that — and the overall result must NOT escalate
+        # to "problems" purely because of this check.
+        with _Env(state=_state(store_to_sdcard=False)):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer(model="A1"))
+        assert _statuses(result)["external_storage"] == "skip"
+        assert result.overall == "ok"
+
+    async def test_skips_on_a1_mini_no_external_storage_slot(self):
+        with _Env(state=_state(store_to_sdcard=False)):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer(model="A1 Mini"))
+        assert _statuses(result)["external_storage"] == "skip"
+
+    async def test_still_fails_on_x1c_when_toggle_off(self):
+        # Sanity: the model-aware skip MUST NOT silently let X1C-class
+        # printers off the hook. The store_to_sdcard=False path is the
+        # one real bit of value this check provides for those models.
+        with _Env(state=_state(store_to_sdcard=False)):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer(model="X1C"))
+        assert _statuses(result)["external_storage"] == "fail"

+ 33 - 0
backend/tests/unit/test_printer_models.py

@@ -8,6 +8,7 @@ from backend.app.utils.printer_models import (
     STEEL_ROD_MODELS,
     get_rod_type,
     has_ethernet,
+    has_external_storage,
     is_dual_nozzle_model,
     normalize_printer_model,
     normalize_printer_model_id,
@@ -146,3 +147,35 @@ class TestDualNozzleModel:
     def test_none_and_empty_are_not_dual(self):
         assert is_dual_nozzle_model(None) is False
         assert is_dual_nozzle_model("") is False
+
+
+class TestHasExternalStorage:
+    """Pins which Bambu models have a MicroSD slot. The connection
+    diagnostic flips its ``external_storage`` check from ``fail`` to
+    ``skip`` based on this — a false add (X1C marked as no-storage) would
+    silently disable a genuine fail signal for X1/P1/P2S/H2 users."""
+
+    @pytest.mark.parametrize("model", ["A1", "A1 Mini", "A1MINI", "A1-Mini", "a1"])
+    def test_a1_series_has_no_external_storage(self, model: str):
+        assert has_external_storage(model) is False
+
+    @pytest.mark.parametrize("model", ["N1", "N2S", "A04", "A11", "A12"])
+    def test_a1_internal_codes_have_no_external_storage(self, model: str):
+        assert has_external_storage(model) is False
+
+    @pytest.mark.parametrize(
+        "model",
+        ["X1C", "X1E", "X1", "P1S", "P1P", "P2S", "H2D", "H2D Pro", "H2C", "H2S", "X2D"],
+    )
+    def test_other_models_have_external_storage(self, model: str):
+        assert has_external_storage(model) is True
+
+    def test_unknown_model_defaults_to_true(self):
+        # Default-true keeps the diagnostic active for new Bambu models;
+        # add them to NO_EXTERNAL_STORAGE_MODELS explicitly when they ship
+        # without a slot.
+        assert has_external_storage("BrandNewModel2027") is True
+
+    def test_none_and_empty_default_to_true(self):
+        assert has_external_storage(None) is True
+        assert has_external_storage("") is True

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.