Browse Source

● fix(mqtt): stop SD card badge flap from partial pushes

  Follow-up to the earlier H2D SD card badge fix. The badge was still
  flapping because Bambu firmwares send partial MQTT pushes carrying only
  the legacy `sdcard` field (without home_flag), and the fallback path
  re-engaged on every such push. Latch home_flag as the canonical source
  once seen; reset the latch on reconnect so a firmware change still
  re-learns.
maziggy 1 month ago
parent
commit
55cd050635
2 changed files with 12 additions and 2 deletions
  1. 1 1
      CHANGELOG.md
  2. 11 1
      backend/app/services/bambu_mqtt.py

+ 1 - 1
CHANGELOG.md

@@ -23,7 +23,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.
+- **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.
 - **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

@@ -360,6 +360,11 @@ class BambuMQTTClient:
         self._dev_mode_probe_failures: int = 0  # consecutive unanswered probes
         self._connect_time: float = 0.0  # monotonic timestamp of last _on_connect
 
+        # Once we've seen home_flag from this printer, it's the canonical SD-card
+        # source. The legacy top-level `sdcard` field arrives in partial pushes
+        # with inconsistent typing and caused badge flap (#936 follow-up).
+        self._home_flag_seen: bool = False
+
         # Set when check_staleness() force-closes the socket to trigger reconnect.
         # Prevents _on_disconnect from redundantly broadcasting state (already done).
         self._stale_reconnecting: bool = False
@@ -456,6 +461,7 @@ class BambuMQTTClient:
             self._dev_mode_probe_seq = None
             self._dev_mode_probe_time = 0.0
             self._dev_mode_probe_failures = 0
+            self._home_flag_seen = False
             self._connect_time = time.monotonic()
             client.subscribe(self.topic_subscribe)
             # Subscribe to request topic for ams_mapping capture (if supported by broker)
@@ -2270,7 +2276,11 @@ class BambuMQTTClient:
         # 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
-        elif "sdcard" in data:
+            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
+            # home_flag. Once we've seen home_flag on this session, partial
+            # pushes carrying only `sdcard` must not flip the badge.
             raw_sdcard = data["sdcard"]
             if isinstance(raw_sdcard, str):
                 self.state.sdcard = "HAS_SDCARD" in raw_sdcard.upper() or raw_sdcard.lower() in ("true", "normal", "1")