diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa399f84..e8c04050e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b1] - Unreleased +### Changed +- **AMS drying now enabled for H2C starting at firmware 01.02.00.00** — H2C was previously in `_DRYING_UNSUPPORTED_MODELS` alongside the A1 family. Moved to `_DRYING_MIN_FIRMWARE` with the same `01.02.00.00` floor as H2S / P2S. Both SSDP model codes the H2C advertises (`O1C`, `O1C2` — single- and dual-nozzle variants) get the same firmware gate so the `supports_drying()` check fires correctly regardless of which form is in the printer record. Test coverage extended in `TestSupportsDrying`: H2C / O1C / O1C2 cases added to the with-firmware pass set, the old-firmware fail set, and removed from the unsupported-models loop. + ### Added - **Connection diagnostic now verifies the printer is actually publishing on its report topic (#1622)** — The existing checks proved TCP + TLS + auth + SUBSCRIBE, but a printer with a wrong-cased serial — or one that simply isn't publishing for some other reason — would still pass `mqtt_auth` because the broker accepts the subscription regardless. The user-visible symptom in that case was "AMS / K-profiles / custom filaments missing on the slicer side": the VP bridge had nothing cached to mirror because no reports ever arrived. Bambuddy already logged `Connected and subscribed, but the printer has sent zero status reports. The most common cause is a wrong or mis-cased serial number…` at `bambu_mqtt.py:498` when this happened, but the only way to see it was to grep container logs. New `printer_publishing` check turns that warning into a structured diagnostic result. Pass = the bridge has seen at least one report since the latest (re)connect; fail = zero reports across the wait window with a fix-text pointing at the case-sensitive serial. The check exposes `report_messages_since_connect` as a public property on `BambuMQTTClient` so the diagnostic doesn't reach into private state. **Bounded wait with countdown UX**: the bridge resets the counter to 0 on every (re)connect, so a fresh reconnect would otherwise be reported as fail before the printer's first idle push lands. The on-demand UI check polls for up to 10s (`PUBLISH_WAIT_DEFAULT`) at 0.5s intervals and exits the moment a message arrives — typical wall-clock is 1-2s, not the full 10. The check returns `max_wait_seconds` in its `params` so the frontend can render a countdown next to the spinner instead of looking hung. The Connection Diagnostic modal (`ConnectionDiagnostic.tsx`) now displays an elapsed-seconds counter (`Running diagnostic... (3s)`) plus the `waitingForReportHint` line (`Listening for the printer to publish a status report — this can take up to 10 seconds.`) during the pending state for the existing-printer flow. `PUBLISH_WAIT_DEFAULT_SECONDS = 10` is pinned in the frontend to match the backend constant; the 2 new i18n keys ship in all 11 locales. The support-package gathering path stays fast: it calls `run_connection_diagnostic` without `wait_for_publish_seconds`, getting an instant pass/fail with no `max_wait_seconds` exposed. 6 new tests covering pass-on-reports-seen, fail-on-zero-after-wait, skip-on-disconnect, skip-on-missing-client, instant-no-wait-path, plus updated all-healthy + disconnected-state assertions to include the new check. i18n strings (`title` / `pass` / `fail` / `skip`) shipped in all 10 non-English locales with real translations — no English fallbacks per the project's hard rule. 5011 leaves × 11 locales in parity. **Why this directly closes #1622**: the reporter's bridge to printers 2 + 4 (P1S + A1 Mini real targets) repeatedly hit keep-alive timeouts and force-reconnected; on every reconnect the printer published nothing in the stale window, leaving the VP cached state empty. The slicer Device tab pulls AMS / cali_id / custom filaments from cached state — empty cache = empty dropdown. The reporter's H2D bridge stayed healthy throughout and its slicer Device tab populated correctly. The in-app Connection Diagnostic had passed (`port_mqtt: pass`, `mqtt_auth: pass`) because it didn't observe publish behaviour. The new check catches this class of failure on the user's first try. diff --git a/backend/app/services/printer_manager.py b/backend/app/services/printer_manager.py index d2d867133..edc731862 100644 --- a/backend/app/services/printer_manager.py +++ b/backend/app/services/printer_manager.py @@ -122,6 +122,9 @@ def is_bed_slinger(model: str | None) -> bool: _DRYING_MIN_FIRMWARE: dict[str, str] = { "H2D": "01.02.30.00", "H2S": "01.02.00.00", + "H2C": "01.02.00.00", + "O1C": "01.02.00.00", # H2C SSDP model code + "O1C2": "01.02.00.00", # H2C dual-nozzle SSDP model code "X1": "01.09.00.00", "X1C": "01.09.00.00", "P1P": "01.08.00.00", @@ -130,7 +133,7 @@ _DRYING_MIN_FIRMWARE: dict[str, str] = { "N7": "01.02.00.00", # P2S internal model code } # Models that definitely don't support AMS drying (no AMS 2 Pro / AMS-HT compatibility) -_DRYING_UNSUPPORTED_MODELS = frozenset({"A1", "A1MINI", "A1-MINI", "A1 MINI", "H2C", "O1C", "O1C2", "O1S", "N1", "N2S"}) +_DRYING_UNSUPPORTED_MODELS = frozenset({"A1", "A1MINI", "A1-MINI", "A1 MINI", "O1S", "N1", "N2S"}) def supports_drying(model: str | None, firmware: str | None) -> bool: diff --git a/backend/tests/unit/services/test_printer_manager.py b/backend/tests/unit/services/test_printer_manager.py index c31e2559a..16067479d 100644 --- a/backend/tests/unit/services/test_printer_manager.py +++ b/backend/tests/unit/services/test_printer_manager.py @@ -1362,6 +1362,9 @@ class TestSupportsDrying: assert supports_drying("P1S", "01.08.00.00") is True assert supports_drying("H2D", "01.02.30.00") is True assert supports_drying("H2S", "01.02.00.00") is True + assert supports_drying("H2C", "01.02.00.00") is True + assert supports_drying("O1C", "01.02.00.00") is True + assert supports_drying("O1C2", "01.02.00.00") is True assert supports_drying("P2S", "01.02.00.00") is True assert supports_drying("N7", "01.02.00.00") is True @@ -1370,6 +1373,9 @@ class TestSupportsDrying: assert supports_drying("X1C", "01.08.00.00") is False assert supports_drying("P1S", "01.07.00.00") is False assert supports_drying("H2S", "01.01.00.00") is False + assert supports_drying("H2C", "01.01.99.99") is False + assert supports_drying("O1C", "01.01.99.99") is False + assert supports_drying("O1C2", "01.01.99.99") is False assert supports_drying("P2S", "01.01.99.99") is False assert supports_drying("N7", "01.01.99.99") is False @@ -1380,7 +1386,7 @@ class TestSupportsDrying: def test_unsupported_models(self): """Verify models without AMS drying support return False regardless of firmware.""" - for model in ["A1", "A1MINI", "A1-MINI", "H2C", "N1", "N2S"]: + for model in ["A1", "A1MINI", "A1-MINI", "N1", "N2S"]: assert supports_drying(model, "99.99.99.99") is False, f"Expected False for {model}" def test_unknown_models_allowed(self):