feat(drying): enable AMS drying for H2C at firmware 01.02.00.00+ (issue #1624)

H2C was in _DRYING_UNSUPPORTED_MODELS. Move to _DRYING_MIN_FIRMWARE
  with the same 01.02.00.00 floor as H2S / P2S. Both SSDP model codes
  the H2C advertises (O1C single-nozzle, O1C2 dual-nozzle) get the
  same gate so supports_drying() fires correctly regardless of which
  form is stored on the printer record.
This commit is contained in:
maziggy
2026-06-04 10:57:46 +02:00
parent c571ad86dd
commit 51abc4b7a8
3 changed files with 14 additions and 2 deletions
+3
View File
@@ -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.
+4 -1
View File
@@ -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:
@@ -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):