Parcourir la source

fix(printers): stop H2D SD-card badge flipping red on heartbeat pushes

  H2D sends heartbeat-style home_flag pushes where bits 8-9 are clear
  even when a card is inserted, so a single heartbeat flipped the badge
  to red until the next full push. Downgrades true->false now require
  three consecutive clear reads; upgrades apply immediately.
maziggy il y a 1 mois
Parent
commit
dd349954a6

+ 1 - 1
CHANGELOG.md

@@ -34,7 +34,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Clear Plate Confirmation Bypassed on Power Cycle** ([#961](https://github.com/maziggy/bambuddy/issues/961)) — With Auto Off enabled and another job queued, the smart plug would cut power when a print finished and immediately re-power when the scheduler saw the queue, at which point the printer booted fresh into `IDLE` and the next job auto-dispatched without the "Clear Plate & Start Next" confirmation. Root cause: the plate-cleared gate lived only in the in-memory `PrinterManager._plate_cleared` set, and the scheduler's idle check treated `IDLE` as always-idle regardless of whether a previous finish had been acknowledged — so the gate was lost across both Bambuddy restarts and the IDLE-on-boot state transition. The gate is now an `awaiting_plate_clear` column on the `printers` table, set by `on_print_complete` when a print finishes or fails, cleared by the `/printers/{id}/clear-plate` endpoint and by the scheduler when it dispatches the next job, and rehydrated from the DB into `PrinterManager` on startup. `_is_printer_idle` now short-circuits to not-idle whenever `require_plate_clear` is on and the printer is awaiting ack, regardless of the currently reported state — so the prompt survives Auto Off cycles, Bambuddy restarts, and the printer booting back into `IDLE`. The clear-plate endpoint no longer requires the printer to currently report `FINISH`/`FAILED` (it accepts the ack whenever the awaiting flag is set), and the Printers page widget prompts based on the flag rather than the reported state. Thanks to @miaopas for reporting.
 - **Insecure Temp File Creation in Backup Export** — The manual backup download endpoint used `tempfile.mktemp()`, which is vulnerable to a symlink race condition (CWE-377). Replaced with `tempfile.mkstemp()` which atomically creates the file, eliminating the TOCTOU window.
 - **Spoolman Iframe Blocked After 0.2.3b4 Security Headers** — The Spoolman page (Inventory → Spoolman iframe) failed to load when Spoolman was served from the same host as Bambuddy via a reverse proxy. The security-headers middleware added in 0.2.3b4 set `X-Frame-Options: DENY` on every response, which blocked even same-origin iframing. Relaxed to `SAMEORIGIN` so Spoolman (and any other same-origin tool behind the same reverse proxy) can be embedded again, while still preventing cross-origin clickjacking.
-- **SD Card Badge Flapping on H2D** — The new SD card status badge on the Printers page card toggled between "inserted" (green) and "not inserted" (red) every few seconds on H2D. Root cause: the MQTT parser used a strict identity check (`data["sdcard"] is True`) on the top-level `sdcard` field, but real firmware ships that field inconsistently — bool on some models, int `1`, or a string enum like `"HAS_SDCARD_NORMAL"` on others — so any message carrying a non-bool value flipped the state to `False`. Fixed by deriving the badge from `home_flag` bits 8–9 (`HAS_SDCARD_NORMAL` / `HAS_SDCARD_ABNORMAL`) when present — the canonical firmware source, same as door and store-to-SD parsing — and falling back to a truthy check on the top-level field for firmwares that only send that. Follow-up: the badge was still flapping because Bambu firmwares send partial MQTT pushes that carry the legacy `sdcard` field alone (without `home_flag`), and the fallback was re-engaging on every such push. The parser now latches `home_flag` as the canonical source for the session once seen, so partial pushes carrying only `sdcard` can no longer flip the badge; the latch resets on reconnect so a firmware change still re-learns.
+- **SD Card Badge Flapping on H2D** — The new SD card status badge on the Printers page card toggled between "inserted" (green) and "not inserted" (red) every few seconds on H2D. Root cause: the MQTT parser used a strict identity check (`data["sdcard"] is True`) on the top-level `sdcard` field, but real firmware ships that field inconsistently — bool on some models, int `1`, or a string enum like `"HAS_SDCARD_NORMAL"` on others — so any message carrying a non-bool value flipped the state to `False`. Fixed by deriving the badge from `home_flag` bits 8–9 (`HAS_SDCARD_NORMAL` / `HAS_SDCARD_ABNORMAL`) when present — the canonical firmware source, same as door and store-to-SD parsing — and falling back to a truthy check on the top-level field for firmwares that only send that. Follow-up: the badge was still flapping because Bambu firmwares send partial MQTT pushes that carry the legacy `sdcard` field alone (without `home_flag`), and the fallback was re-engaging on every such push. The parser now latches `home_flag` as the canonical source for the session once seen, so partial pushes carrying only `sdcard` can no longer flip the badge; the latch resets on reconnect so a firmware change still re-learns. Second follow-up: on H2D the badge still showed red on initial Printers-page navigation and flipped to green on reload, because H2D also sends heartbeat-style `home_flag` pushes where bits 8–9 are clear even when a card is inserted. Downgrades from true→false now require three consecutive clear reads (upgrades false→true still apply immediately), so a single heartbeat no longer turns the badge red.
 - **CSP Blocked Sidebar Iframes, Service-Worker Registration, and Google Fonts** — The strict `Content-Security-Policy` header added in 0.2.3b4 broke three things at once: (1) custom sidebar links pointing at external HTTPS URLs (e.g. a Grafana/telemetry dashboard) rendered in `ExternalLinkPage` were blocked because no `frame-src` was declared and iframes fell back to `default-src 'self'`; (2) the inline service-worker registration `<script>` at the bottom of `index.html` was blocked by `script-src 'self'`, silently preventing the PWA service worker from installing; (3) the `@import` of Google Fonts' Inter from `index.css` was blocked by `style-src` and `font-src`. Fixed by adding `frame-src 'self' https:` for user-configured HTTPS iframe targets, moving the inline SW-registration script into `/sw-register.js` so `script-src 'self'` covers it without needing `'unsafe-inline'` or per-build hashes, and allowing `https://fonts.googleapis.com` in `style-src` and `https://fonts.gstatic.com` in `font-src`. `frame-ancestors 'none'` is preserved so Bambuddy itself still cannot be framed cross-origin.
 
 

+ 11 - 1
backend/app/services/bambu_mqtt.py

@@ -2275,7 +2275,17 @@ class BambuMQTTClient:
         # badge to flap on H2D. Prefer home_flag when available, fall back to a truthy
         # check on the `sdcard` field for firmwares that only send that.
         if home_flag is not None:
-            self.state.sdcard = ((home_flag >> 8) & 0x3) != 0
+            sd_bits_set = ((home_flag >> 8) & 0x3) != 0
+            # H2D sometimes sends heartbeat-style home_flag pushes where bits 8-9
+            # are clear even when an SD card is inserted. Only downgrade true->false
+            # after several consecutive clear reads; upgrade false->true immediately.
+            if sd_bits_set:
+                self.state.sdcard = True
+                self._sdcard_clear_streak = 0
+            else:
+                self._sdcard_clear_streak = getattr(self, "_sdcard_clear_streak", 0) + 1
+                if self._sdcard_clear_streak >= 3 or not self.state.sdcard:
+                    self.state.sdcard = False
             self._home_flag_seen = True
         elif "sdcard" in data and not self._home_flag_seen:
             # Only trust the legacy top-level field on firmwares that never send

+ 7 - 1
backend/tests/unit/services/test_bambu_mqtt.py

@@ -3604,6 +3604,11 @@ class TestSdCardParsing:
     def test_home_flag_no_sdcard_bits(self):
         client = self._make_client()
         client.state.sdcard = True
+        # Downgrade requires 3 consecutive clear reads (H2D heartbeat workaround).
+        client._update_state({"home_flag": 0x00000000})
+        assert client.state.sdcard is True
+        client._update_state({"home_flag": 0x00000000})
+        assert client.state.sdcard is True
         client._update_state({"home_flag": 0x00000000})
         assert client.state.sdcard is False
 
@@ -3612,7 +3617,8 @@ class TestSdCardParsing:
         client = self._make_client()
         client._update_state({"home_flag": 0x00000100, "sdcard": "HAS_SDCARD_NORMAL"})
         assert client.state.sdcard is True
-        client._update_state({"home_flag": 0x00000000, "sdcard": 1})
+        for _ in range(3):
+            client._update_state({"home_flag": 0x00000000, "sdcard": 1})
         assert client.state.sdcard is False
 
     def test_sdcard_string_fallback_when_no_home_flag(self):