mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(mqtt): don't let K-profile responses clobber the nozzle size (#2663)
Fetching K-profiles probes every nozzle size in turn (extrusion_cali_get for 0.2/0.4/0.6/0.8mm), and each response echoes the requested diameter at the top level. _process_message passed every "print" message -- including these responses -- to _update_state, which treats a top-level nozzle_diameter as the installed hardware. So the last size probed (0.8) overwrote the real nozzle in memory; a genuine status push corrected it and the next K-profile fetch broke it again, which is why it flickered between 0.8, empty and the correct 0.4. Since 1.2.5 the #1899 mismatch guard then refused to dispatch, failing prints with a bogus "printer has 0.8mm". Handle extrusion_cali_get responses only via the K-profile parser and skip _update_state for them, mirroring the existing get_accessories guard. The nozzle size now comes solely from the real status push. In-memory only -- affected printers self-correct on the next push after updating.
This commit is contained in:
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [1.2.6b1] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- **A printer's nozzle size got overwritten to the wrong value (often 0.8mm), then blocked prints as a nozzle mismatch (#2663, reporter @huykent)** — A1 printers with a 0.4mm nozzle intermittently showed **0.8mm** (or no size at all) on the dashboard, and since 1.2.5 that wrong value made the nozzle-mismatch guard (#1899) refuse to dispatch the job — "File sliced for a 0.4mm nozzle, but the printer has 0.8mm installed." It was intermittent and could flip *after* a job was sent. **Root cause.** Bambuddy fetches K-profiles by probing every nozzle size in turn — it sends an `extrusion_cali_get` request for 0.2, 0.4, 0.6 **and** 0.8mm. The printer's response to each echoes the *requested* nozzle diameter at the top level, and the MQTT handler passed every `print` message — including these K-profile responses — through `_update_state`, which treats a top-level `nozzle_diameter` as the installed hardware. So the last size probed (0.8) clobbered the real nozzle size in memory; a later genuine status push would correct it, and the next K-profile fetch would break it again, which is why it flickered and "changed after the job was sent." The raw MQTT status always reported the correct 0.4 — only the derived hardware-nozzle field was corrupted. **Fix.** `extrusion_cali_get` responses are now handled *only* by the K-profile parser and no longer fed to `_update_state`, so they can't touch the nozzle hardware state — mirroring the existing guard that already stops `get_accessories` responses from doing the same thing. The installed nozzle size now comes solely from the printer's real status push, where it was always correct. No configuration or migration needed: the value lives in memory and self-corrects on the next status push after updating. Covered by tests: a 0.8mm K-profile response leaves a 0.4mm nozzle untouched, the response's profiles are still parsed into `state.kprofiles`, and a genuine status push still sets (and corrects) the nozzle.
|
||||
- **The print queue couldn't be reordered on a phone, and the reorder controls were invisible in portrait (#2667, reporter @aporlebeke)** — On mobile there was no way to reorder the queue: in portrait the reorder controls simply weren't visible, and even in landscape (where the desktop drag handle appears) touch-dragging didn't move anything. **Root cause.** The drag grip and selection checkbox on every pending row are `hidden sm:flex`, so below the 640px breakpoint (phone portrait) they disappear entirely — there's no affordance to grab. Above it (landscape phone/tablet) the grip shows, but it carried `touch-action: manipulation` and the only drag sensor is dnd-kit's `PointerSensor` with an 8px activation distance, so on touch the browser claimed the vertical gesture as a scroll before the drag ever started. The whole reorder mechanism was effectively mouse-only. **Fix.** Pending rows now get tap-friendly **up/down arrow buttons** on mobile (the "arrow select" the reporter asked for), shown below `sm` where the drag handle is hidden. They move a row one step among its siblings — standalone items, whole batches, and items within a batch, in both the flat and per-printer layouts — and persist through the same `POST /queue/reorder` path as drag, so arrows and drag agree. Arrows appear only in the manual "position" sort (with shortest-job-first off), where a position actually has meaning, and are gated on the same `queue:reorder` permission; the up arrow on the first row and the down arrow on the last are shown disabled. Separately, the desktop drag handle's `touch-action` is now `none`, so mouse-style drag also works on touch (landscape phones, tablets). Reuses the existing `queue.moveUp` / `queue.moveDown` translations (already present in all locales). Covered by tests: the controls render for pending items, moving the first item down persists the swapped order, and the boundary arrows are disabled.
|
||||
- **3D Preview plate thumbnails were broken (401) in File Manager when login was enabled (#2661, reporter @fbordonaro)** — Opening a multi-plate 3MF via **File Manager → 3D Preview** showed broken-image icons for every plate thumbnail, and the network tab showed `GET /api/v1/library/files/<id>/plate-thumbnail/<plate>` returning **401 "Valid camera stream token required."** The Slice dialog displayed the same file's thumbnails correctly, which is what made it look inconsistent. **Root cause.** The plate-thumbnail endpoints (both archive and library) are gated behind a **camera stream token** passed as a `?token=` query param, because an `<img>` tag can't send an `Authorization: Bearer` header. Every place that renders these thumbnails is supposed to append the token via the `withStreamToken()` helper — `PlatePickerModal` (the Slice dialog's multi-plate picker) and the Print modal's `PlateSelector` both do — but the **3D Preview dialog** (`ModelViewerModal`) rendered the raw `thumbnail_url` with no token, so with auth enabled the browser fetched without one and got a 401. **Fix.** `ModelViewerModal` now wraps the plate thumbnail `src` in `withStreamToken()`, matching the two existing call sites. The token is already synced app-wide (the same global the working pickers read), and `withStreamToken()` is a no-op when auth is off, so nothing changes for non-auth setups. Covered by a component test asserting the plate thumbnail `<img>` carries the `?token=` query param.
|
||||
- **Force color match dispatched a print onto the wrong PLA variant — Matte jobs went to Basic and Silk printers alike, and the wrong AMS slot on a printer holding two same-colour variants (#2650, reporter @MartinNYHC)** — With **Force color match** on, a job sliced for **White PLA Matte** was dispatched to every printer that had *any* white PLA loaded — the ones holding White PLA **Basic** and White PLA **Silk+** included — so a matte model came out glossy on the wrong machine. **Root cause.** Bambu's MQTT status reports every PLA sub-variant as `tray_type == "PLA"`; the Basic/Matte/Silk distinction is carried only in `tray_info_idx` (`GFA00` = Basic, `GFA01` = Matte, `GFA06` = Silk, …), which the 3MF's `slice_info.config` also records per filament. Three places dropped it: the Virtual-Printer queue built each force override as `{slot_id, type, color, force_color_match}` without the parsed `tray_info_idx`; the scheduler's eligibility check (`_get_missing_force_color_slots`) compared loaded trays on `(type, colour)` only — so `(PLA, #FFFFFF)` matched Basic, Matte and Silk indiscriminately and all three printers looked eligible; and the AMS slot mapper cleared `tray_info_idx` when applying the override, so even on the correct printer it could pick a different-variant tray of the same colour. **Fix.** The force override now carries the 3MF's `tray_info_idx`; a slot counts as satisfied only when a loaded tray matches type **and** colour **and** the variant (identical `tray_info_idx`, *or* either side lacks one); and the slot mapper now keeps the variant for force-colour overrides so it pins the matching tray. A blank idx on either side (custom/third-party spools report none, and older 3MFs carry none) falls back to the historical type+colour behaviour, so those setups are unaffected, and a manual filament *swap* (a preference override) still clears the idx so it matches the swapped-in spool rather than the old one. A job sliced for GFA01 now goes only to a printer with GFA01 loaded, and lands on that printer's GFA01 tray. The printer-card queue-compatibility hint (which printers show a pending job as runnable) now applies the same variant rule. Covered by scheduler tests (Matte requirement rejects Basic/Silk, accepts Matte, blank loaded idx falls back, requirement without an idx unchanged; the mapper pins the GFA01 tray over a same-colour GFA00 on both the 3MF and no-3MF paths; a preference swap still matches by colour), a Virtual-Printer test asserting the override carries `tray_info_idx`, and frontend tests for the variant-aware queue hint (rejects other variants, accepts the match, blank-idx and no-variant-data fall back).
|
||||
|
||||
@@ -1427,10 +1427,19 @@ class BambuMQTTClient:
|
||||
elif cmd == "ams_filament_setting":
|
||||
self._last_ams_cmd_time = 0.0
|
||||
self._ams_cmd_unanswered = 0
|
||||
if "command" in print_data and print_data.get("command") == "extrusion_cali_get":
|
||||
is_kprofile_response = "command" in print_data and print_data.get("command") == "extrusion_cali_get"
|
||||
if is_kprofile_response:
|
||||
self._handle_kprofile_response(print_data)
|
||||
|
||||
self._update_state(print_data)
|
||||
# An extrusion_cali_get response echoes the *requested* nozzle
|
||||
# diameter (get_kprofiles probes 0.2/0.4/0.6/0.8 in turn), not the
|
||||
# installed hardware. Feeding it to _update_state clobbered the real
|
||||
# nozzle size (#2663) — typically leaving 0.8, the last size probed,
|
||||
# which then failed the #1899 dispatch guard. The response carries no
|
||||
# status telemetry, so skip it; the true nozzle comes from pushall.
|
||||
# (Same reasoning as get_accessories in _handle_system_response.)
|
||||
if not is_kprofile_response:
|
||||
self._update_state(print_data)
|
||||
|
||||
def _handle_system_response(self, data: dict):
|
||||
"""Handle system responses including accessories info.
|
||||
|
||||
@@ -6468,3 +6468,72 @@ class TestPresumedPowerOffRecovery:
|
||||
self._report(mqtt_client, {"print": {"wifi_signal": "-30dBm"}})
|
||||
|
||||
assert mqtt_client.state.state == "FINISH"
|
||||
|
||||
|
||||
class TestKProfileResponseDoesNotClobberNozzle:
|
||||
"""#2663: the K-profile fetch (get_kprofiles) probes every nozzle size
|
||||
0.2/0.4/0.6/0.8 with an ``extrusion_cali_get`` request. Each response
|
||||
echoes the *requested* nozzle_diameter at the top level, which is NOT the
|
||||
installed hardware. _process_message must not feed those responses to
|
||||
_update_state, or the real nozzle size gets overwritten (typically to 0.8,
|
||||
the last size probed) and the #1899 dispatch guard then blocks prints.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mqtt_client(self):
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient
|
||||
|
||||
return BambuMQTTClient(
|
||||
ip_address="192.168.1.100",
|
||||
serial_number="A1TEST",
|
||||
access_code="12345678",
|
||||
)
|
||||
|
||||
def test_kprofile_response_does_not_overwrite_nozzle_diameter(self, mqtt_client):
|
||||
# A genuine pushall reports the real 0.4mm nozzle.
|
||||
mqtt_client._process_message({"print": {"nozzle_diameter": "0.4"}})
|
||||
assert mqtt_client.state.nozzles[0].nozzle_diameter == "0.4"
|
||||
|
||||
# The K-profile probe's 0.8mm response arrives (as it did on the
|
||||
# reporter's A1s). It must NOT clobber the hardware nozzle.
|
||||
mqtt_client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"command": "extrusion_cali_get",
|
||||
"nozzle_diameter": "0.8",
|
||||
"filaments": [],
|
||||
"sequence_id": "1501",
|
||||
}
|
||||
}
|
||||
)
|
||||
assert mqtt_client.state.nozzles[0].nozzle_diameter == "0.4"
|
||||
|
||||
def test_kprofile_response_is_still_parsed(self, mqtt_client):
|
||||
# Skipping _update_state must not skip the K-profile handler: the
|
||||
# response's profiles still populate state.kprofiles.
|
||||
mqtt_client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"command": "extrusion_cali_get",
|
||||
"nozzle_diameter": "0.4",
|
||||
"filaments": [
|
||||
{
|
||||
"cali_idx": 0,
|
||||
"nozzle_diameter": "0.4",
|
||||
"filament_id": "GFA00",
|
||||
"name": "PLA",
|
||||
"k_value": "0.020000",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert len(mqtt_client.state.kprofiles) == 1
|
||||
assert mqtt_client.state.kprofiles[0].filament_id == "GFA00"
|
||||
|
||||
def test_genuine_pushall_still_updates_nozzle(self, mqtt_client):
|
||||
# The one legitimate source of the hardware nozzle still works, and a
|
||||
# later pushall corrects a value an old build left wrong.
|
||||
mqtt_client.state.nozzles[0].nozzle_diameter = "0.8"
|
||||
mqtt_client._process_message({"print": {"nozzle_diameter": "0.4"}})
|
||||
assert mqtt_client.state.nozzles[0].nozzle_diameter == "0.4"
|
||||
|
||||
Reference in New Issue
Block a user