mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
yyUpdated CHANGELOG
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
All notable changes to Bambuddy will be documented in this file.
|
||||
|
||||
|
||||
|
||||
## [1.2.6b1] - Unreleased
|
||||
|
||||
### Fixed
|
||||
@@ -91,6 +93,61 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Pinned `react-router` to its most-patched 7.x (7.18.1) and documented the one remaining, unreachable advisory (GHSA-qwww-vcr4-c8h2)** — Staying current on the 7.x line matters: 7.18.1 clears 14 advisories that older 7.x releases carry, several reachable in a browser SPA (open-redirect XSS in `<Link>`/`useNavigate`, route-matching DoS). The single advisory that still flags 7.18.1 — a CSRF bypass — applies only to React Router's **RSC mode**, which requires the server runtime (`@react-router/server`, not installed); Bambuddy is a Vite SPA using `BrowserRouter`, so the vulnerable path is unreachable. There is no non-major fix (the patch landed only in the 8.3.0 major, and `react-router-dom` has no 8.x — adopting it would mean migrating every import to `react-router` plus a React peer bump), so `react-router`/`react-router-dom` are pinned to 7.18.1 and the finding is carried as a documented, fail-closed exception in the CI audit gate: a *different* react-router advisory still fails CI, and the exemption is dropped automatically the moment a non-major fix ships. `npm audit fix --force` is deliberately avoided — its suggested "fix" is a downgrade to 7.11.0, which reintroduces those 14 advisories.
|
||||
|
||||
|
||||
## [1.2.5.2] - 2026-08-02
|
||||
|
||||
### Added
|
||||
- **The print queue now shows when each job would finish (#2736, contributor @mpl1337)** — A queue row carried a job's print duration but not the clock time that maps to, so "if I start this one now, when is it done?" meant doing the arithmetic yourself, once per row — while the card for the running print has shown an ETA all along. Pending and staged rows now show one too, beside the duration, in your configured 12/24-hour format and styled to match the live one. It is deliberately a per-job answer and not a forecast of the whole queue: "start this now and it finishes at 14:30", not a projection of everything ahead of it. That only means anything for a job that could actually start now, so the ETA appears on exactly those. A job sits behind whatever is printing on its printer, and behind anything the scheduler would dispatch ahead of it — resolved in the same order the scheduler itself uses, including **Shortest Job First** when that is on — and stays quiet until it is genuinely next up, so three jobs stacked on one printer no longer quote the same finishing time three times over. Staged jobs are the exception in both directions: the scheduler skips them without claiming the printer, so they neither hold up the job behind them nor wait for it, and they show an ETA whenever their printer is free — which is the honest answer to "what happens if I press Start". Jobs scheduled for later, jobs blocked with a stated reason, jobs conditional on an earlier print succeeding, and jobs with no duration in their metadata show nothing at all. The time also keeps up with the clock instead of freezing at the moment the page was opened, and every row on screen is measured from the same instant, so two rows are always comparable. Frontend-only. Translated in all locales; wiki updated. Covered by frontend tests.
|
||||
- **Keep the AMS slots the slicer picked (#2700, contributor @Striker72rus)** — Bambu Studio and OrcaSlicer resolve which physical AMS tray feeds each filament themselves, right before sending. Bambuddy threw that away: a queue-mode virtual printer worked the mapping out again at dispatch time, from the filament type and colour baked into the 3MF. That is usually the better answer — it is computed against the printer's live trays and it respects **Prefer lowest filament** and the AMS-backup gate that goes with it — but it has nothing to go on when the match isn't unique. Two spools of the same red PLA, and the slot you deliberately chose in the slicer is a coin toss. A new per-virtual-printer **Save AMS mapping** toggle keeps the slicer's pick instead: the print dispatches to exactly those trays, and the mapping is stored on the archive so a reprint can reuse the same physical spools — a **Mapping** button in the print modal selects every slot from it in one click, and the archive card and queue row say so. Because a tray number only means something on the AMS it was resolved against, the mapping records its printer and is only ever offered on that same printer; a model-based ("Any [model]") virtual printer has no fixed printer and is unaffected. Off by default, so nothing changes for existing virtual printers until you turn it on, and **Force color match** still wins for the print being dispatched when both are on. Translated in all locales; wiki updated. Covered by backend and frontend tests.
|
||||
- **P2S/X2D accessory fans: left auxiliary cooling and chamber exhaust (#2691, contributor @gzimbric, requested in #2660)** — The P2S and X2D have two fans Bambuddy could not show or drive. The **left auxiliary part cooling fan** had no tile and no control at all, because the printer only reports it inside its air-duct data and never in the ordinary fan fields Bambuddy was reading. The **chamber exhaust fan** had the opposite problem: its tile appeared on every P2S whether or not the fan was fitted, so owners of a base machine had a control that did nothing. Both are add-on kits on the P2S and fitted at the factory on the X2D. Both tiles now appear only when the printer itself reports the hardware, so a base P2S looks exactly as it does today and a kitted one gains the fans it actually has. The left auxiliary fan is set from the same speed popover as the others, and the enclosure fan is labelled **Exhaust** on the P2S and X2D — matching the printer's own screen and Bambu Studio — while every other enclosed model keeps **Chamber Fan**. The confirmation message after changing a speed uses the same name as the tile that was clicked. The four tiles are ordered part cooling, left auxiliary, auxiliary, exhaust, so they read left to right in the same order as the physical fans. Both fields are also published through the status endpoint, the WebSocket feed and the MQTT relay, so external automations can read them. Translated in all locales; wiki updated. Covered by backend and frontend tests, including the case where the printer sends a partial fan report — a tile must not disappear mid-print just because one update didn't mention it.
|
||||
- **Live print progress in the browser tab (#2693, contributor @Chachigo, requested in #1041)** — Watching a print meant keeping the Bambuddy tab in view, or switching back to it every few minutes. Enable **Print progress in tab** under Settings → Appearance and the tab title becomes `42% · Bambuddy` while the favicon turns into a progress ring in your theme accent colour, both updating live over the WebSocket the rest of the UI already uses. With several printers running, the tab follows the one finishing soonest, tie-broken by highest progress; title and favicon return to their defaults as soon as nothing is printing or the toggle goes off. Off by default, and stored per browser (like the light/dark toggle) so a wall-mounted dashboard and a laptop can each have their own setting. Translated in all locales; wiki updated. Covered by frontend tests.
|
||||
- **Telegram notifications can target a forum topic (#1518, reporter @vmhomelab)** — Telegram groups with Topics enabled always received Bambuddy's notifications in the **General** topic, because only Bot Token and Chat ID were configurable. Getting a per-printer split therefore meant creating a separate chat per printer. The Telegram provider now takes an optional **Forum Topic ID** — the last number in a topic's link (`t.me/c/1234567890/25`) — and routes its messages into that topic, so a single group can carry one topic per printer. Left empty, the behaviour is unchanged. The ID is sent on both the plain-text and the thumbnail code paths, and is validated as a number in the form and again server-side, so a typo is reported instead of silently breaking only text notifications. Translated in all locales; wiki updated. Covered by backend and frontend tests.
|
||||
- **Support bundles now record Bambuddy's own memory, threads and child processes (#2734)** — A bundle described everything except the thing it runs in. That made reports of memory climbing over days impossible to act on: the numbers that identify what is actually growing only exist while it is happening, and by the time anyone asked, the container had been restarted. Bundles now carry resident and virtual memory, thread count, child processes by name, open files and sockets, process uptime, and a census of live objects by type. Those figures separate causes that look identical from outside — a large virtual size against a modest resident one is address space rather than data, a rising thread count points somewhere quite different from a rising child-process count, and the object census names what a growing heap is filling up with. The object census is skipped on processes already above 2 GB, because walking the heap costs most on exactly the process that can least afford it; everything else is still collected. Child processes are recorded by executable name only — an ffmpeg command line carries the camera URL and its password. Collection happens off the main loop and every metric is best-effort, so a hardened kernel or restricted container that refuses one of them still produces a complete bundle.
|
||||
- **Folder rows in the File Manager now show when anything inside them last changed (#2680 follow-up, reporter @cadtoolbox)** — The calendar toggle only put dates on the file pane, so the folder tree had no way to show the timestamp it was already sorting on. Folders now render that value under their name whenever the toggle is on, nested folders included. It is labelled **last activity** rather than "last modified" deliberately: the value is the newest timestamp among the folder, its files and everything below it, so a folder can legitimately read as newer than its own directory mtime — calling that "modified" would look like a fresh instance of the `ls -lt` mismatch the issue was originally about. Folders with no activity render nothing rather than an `Invalid Date` placeholder. Frontend-only — the field was already on the wire from the sort fix. Covered by frontend tests.
|
||||
|
||||
### Changed
|
||||
- **Debug logs now record what the printer reports between the last layer and the end of a print (#2547, reporter @anthonyma94)** — The finish photo wants a moment that Bambu firmware does not obviously announce: printing done, toolhead parked, filament unload not yet started. Bambuddy has been driving that capture from `stg_cur=22` ("Filament unloading"), which turns out to fire on no model at all — across 247 support bundles there is not a single stage-22 capture, including the window in which it was the only trigger in the code, where all 104 captures on A1, A1 Mini, H2C, H2D, P1S, P2S, X1C and X2D fell through to the after-the-fact fallback. Choosing a replacement was not possible from the bundles we had, because outside `stg_cur` and `mc_print_sub_stage` every stage and action field the printers send is dropped unread, and the most promising candidates (`print_real_action`, `mc_action`, `mc_stage`) are absent from A1, A1 Mini and P1S payloads entirely. With debug logging enabled, Bambuddy now dumps those raw fields for the window between the last object layer and the end of the print — opening on the first end-of-print signal (last layer reached, progress at 99+, or no remaining time), logging only what changed frame to frame, and closing on the state transition — so a single debug bundle per model can show whether any firmware marks that moment. Diagnostics only: nothing reads these values, they are printer telemetry with nothing identifying in them, and at normal log levels the probe does no work at all. Covered by tests for the window boundaries, the frame budget and the guarantee that the probe cannot break status ingest.
|
||||
|
||||
### Fixed
|
||||
- **Bambu Cloud sign-in with a TOTP (authenticator app) account always failed with "Invalid code" (#2696, reporter @cmerkle)** — Every TOTP verification was rejected regardless of the code. Bambu Lab added double-submit CSRF protection to the `bambulab.com` web origin, which is where — and only where — Bambuddy posts the two-factor code; the endpoint refused the request with `403 CSRF error: missing_cookie` **before evaluating the code at all**, and Bambuddy surfaced that as "Invalid code". Reproduced against the live endpoint with a deliberately invalid key: a bare POST returns `missing_cookie`, `GET /api/csrf` mints a `bbl_csrf_token` cookie, a POST carrying only that cookie returns `missing_header`, and a POST carrying the cookie plus an `x-bbl-csrf-token` header reaches application logic. Bambuddy now performs that handshake before submitting the code. Note that landing on the sign-in page first — the intuitive fix — does **not** work: that page sets only Cloudflare's `__cf_bm`. **Also fixed:** a CSRF refusal no longer masquerades as a wrong code; it now says the code was never checked, so nobody else loses an evening to clock drift and leading-zero theories. Only TOTP sign-ins were affected — every other cloud call, including the email-code two-factor path, goes to `api.bambulab.com`, which is not gated, and existing stored tokens kept working throughout. Covered by tests that pin the exact header name and the origin used per region.
|
||||
- **A2L AMS filament showed as "?" in Bambu Studio through the Virtual Printer, and manual filament picks reverted (#2697, reporter @qoatzelcoat)** — Every slot of the A2L's AMS Lite rendered as an empty question mark in the slicer's Device tab while Bambuddy's own AMS card showed type, colour and spool correctly; setting a filament by hand in Studio held for a second and then snapped back to "?". **Root cause.** The A2L reports its AMS Lite as physical unit id 16, but packs the slots' presence bits at bit base 24 — so Bambuddy normalises the id to 6 at the MQTT ingest boundary and every internal reader gets the right bits. The Virtual Printer's bridge, however, parses the printer's raw payload itself (by design — the slicer-facing cache has to keep the physical ids, since Bambu Studio addresses the Lite as 16) and so still held id 16 when it ran the shared empty-slot cleanup. That cleanup read bits 64-67, where nothing is ever set, concluded all four slots were empty and wiped `tray_type`, `tray_color`, `tray_info_idx` and the RFID fields from the copy sent to the slicer — once per second, which is also why a manual pick could not survive. **Fix.** The presence-bit helper now folds the physical id 16 onto the same bit base as the normalised 6, so it computes bits 24-27 whichever id reaches it; the cached ids the slicer sees are left untouched. Only the A2L was affected — every other AMS type already reached the helper with an id whose bit base was correct, and Bambuddy's own printer card was correct throughout. Confirmed against the reporter's debug log, which shows the cleanup clearing slots at bits 64-67. Covered by tests pinning the bit base for both ids and a bridge-level regression test built from the reporter's capture.
|
||||
- **The Settings page no longer reverts settings changed from anywhere else (#2716, reporter @jmoore-skild)** — While the Settings page was open it held its own copy of every setting and only ever took one from the server, on first load. A background effect then compared that copy against the server's and saved the whole thing back on any difference — with no way to tell "the user edited this field" from "this field changed on the server". So anything written while the page sat open was silently undone: a change made in a second tab, another user's change on a shared install, a restore from a backup. It needed no click to trigger. The page's data goes stale after a minute and refreshes when the window regains focus, and around thirty other places in the app read the same settings, so a refresh from any of them was enough — after which the page wrote its page-load copy back over all 77 settings it manages, and showed **Settings saved** while doing it. The page now keeps track of the last server state it reconciled with. A field still matching that state has not been touched, so a newer value from the server is adopted and displayed; a field the user has edited keeps their value and is saved over the top, so the newer of the two writes wins either way. Typing into a text field while a refresh lands is still safe, which is what the old behaviour was protecting. Covered by frontend tests.
|
||||
- **A rejected K-profile write is now reported as rejected (#2718, reporter @jmoore-skild)** — Saving a K-profile was fire-and-forget: Bambuddy published the command and reported success the moment the bytes left the process. The printer does answer, and the answer was received, matched, and thrown away at debug level — so a write the printer refused for a real reason still told you it was saved. The complication was that the answer itself was wrong: on single-nozzle printers it came back `result: "fail", reason: "invalid tray_id"` on writes that demonstrably applied, which made gating on it look impossible. Measuring against an X1C and an H2D found the cause — the `tray_id: -1` Bambuddy itself put in the payload. The X1C's firmware validates that field and rejects the value while applying the write anyway; the H2D ignores it. Sending `0`, as BambuStudio does, makes the acknowledgement honest, and the printer echoes back the sequence number we sent, so it can be matched to the write that caused it. Saving or deleting a profile now waits for that answer and surfaces a genuine rejection as an error instead of a success toast. A printer that stays silent is still treated as success — no answer is not evidence of refusal. The acknowledgement is also logged at INFO now, so it appears in a support bundle. Covered by backend tests.
|
||||
- **The K-profile flow type is a real choice again** — On most printers the calibration table comes back with no nozzle identity at all, and Bambuddy had started showing "Not reported by printer" in the Flow Type field as a result. That is not a value you can save, and it isn't what the slicer does: BambuStudio treats a missing nozzle identity as **Standard** and leaves the choice editable. Bambuddy now does the same. The field is hidden only on models sold with a single nozzle variant — the A1, A1 Mini and A2L — using the same rule the slicer applies. This is not the single-versus-dual-nozzle split: the P1P, P1S, P2S, X1, X1 Carbon, X1E and H2S are all single-nozzle and all offer both flows. Editing a profile also no longer strips the nozzle identity from what it writes back.
|
||||
- **Dialogs no longer act after they have closed** — The AMS slot configuration and K-Profile dialogs hold their success state briefly and then close themselves, between 1.5 and 4 seconds after the command is sent so the printer has time to process it. That timer ran whether or not the dialog was still open, so dismissing it — or the printer card refreshing underneath it — within that window left a pending close that fired later, dismissing whatever dialog happened to be open by then. The deferred close is now cancelled when the dialog goes away. Covered by frontend tests.
|
||||
- **A printer with no K-profiles can now be given its first one (#2719, reporter @jmoore-skild)** — **Add K-Profile** built its Filament dropdown out of the profiles already on the printer, so on a printer with none the field was empty, required, and impossible to satisfy — the modal even said so, telling you to go and create the profile in Bambu Studio instead. The filament picker is now populated the way every other one in Bambuddy is, in the same order: **Imported** presets first, then **Orca Cloud**, then **Bambu Cloud**, then Bambuddy's built-in Bambu filament table. That last tier is compiled in, so the list is never empty — a brand-new printer with no cloud account and nothing imported still gets you a profile. The per-printer-model copies a cloud account carries ("Bambu PLA Basic" once for the X1C, once for the P1S, once for the A1) are collapsed into a single row, and the built-in table — a static copy of the same Bambu catalogue — no longer echoes back filaments the groups above already list. Your imported and Orca Cloud libraries are both shown in full even where they overlap by name, because they are usually the same profiles reached two ways and each group is worth seeing under its own heading. The picker is a searchable list with the source heading shown as a real, legible group header — a native dropdown can't do that, since browsers render the group label of a `<select>` in small grey italics and ignore styling on it. Imported and Orca Cloud presets carry no Bambu filament ID, and the printer indexes its calibration table by one, so those are filed under the closest generic for their material — the same rule the AMS slot configuration already uses, so a profile created here matches the slot configured there. A filament whose material Bambuddy can't place is refused with an explanation rather than written under a wrong ID. Also drops a second, redundant K-profile fetch the old dropdown needed: it ran concurrently with the main one whenever a non-0.4mm nozzle was selected, which is exactly the case that made K-profile requests time out. Translated in all locales; wiki updated. Covered by frontend tests.
|
||||
- **K-profiles no longer all report 0.4mm / High Flow (#1748, reporters @Liquidmasl and @jmoore-skild)** — On any printer running a nozzle other than 0.4mm, every K-profile showed up as `0.4` with a flow type nobody had set, and the same profile disagreed with itself: the list said **S**, the edit dialog said **High Flow**. The printer reports the nozzle diameter once, on the response envelope; the individual profile entries carry no diameter and no nozzle id at all. Bambuddy read the diameter *per entry* and fell back to a hardcoded `0.4` when it wasn't there — which was always. The envelope value is now used, so profiles report the nozzle they were actually calibrated for. This was not only cosmetic. Editing a profile is delete-and-re-add on single-nozzle printers, and the dialog rebuilt the nozzle fields from its own (greyed-out) dropdowns, so saving an untouched 0.6mm profile rewrote it on the printer as 0.4mm High Flow. Deleting one aimed the command at the wrong nozzle for the same reason. Both now pass through exactly what the printer reported. Assigning a spool's stored calibration to an AMS slot was affected too: that lookup matches on nozzle diameter, so on a 0.6 or 0.8 nozzle it never found the printer-side entry and the cali_idx silently failed to stick — the "can't auto-map a K-profile" half of the report. Where the printer sends no nozzle id, Bambuddy now says so instead of picking one: the list shows the diameter alone, the dialog shows **Not reported by printer**, and the High Flow / Standard filter is hidden rather than offered as a control that can only ever empty the list. Also fixes the flow type filter selecting the opposite label when naming a new profile. Translated in all locales. Covered by backend tests.
|
||||
- **K-profile requests no longer time out when two run at once (#1748)** — Fetching profiles for one nozzle size while another fetch was open made the first one time out, with `Failed to get K-profiles after 3 attempts` in the log, even though the printer had answered both correctly. Responses were matched to requests by nozzle diameter held in a single shared slot, so the second request overwrote the first's expectation and the first's valid answer was discarded as a mismatch. Requests are now correlated by the sequence id Bambuddy already sends and tracked one entry per request, with the old nozzle match kept as a fallback for firmware that doesn't echo the id back. An unsolicited broadcast arriving mid-fetch also no longer replaces the profile list the fetch is waiting on. Covered by backend tests.
|
||||
- **Git backup now actually writes cloud profiles (#2717, reporter @jmoore-skild)** — Enabling **Cloud Profiles** for a Git backup produced nothing. The collector looked for a `setting` list in the Bambu Cloud response, which is keyed by preset type instead, so the loop never ran once — and it asked for the credential store used when authentication is *disabled*, so on any install with authentication on it found no account to collect from in the first place. Neither failure was visible: `backup_metadata.json` still recorded `cloud_profiles: true`, and the log line read `Collected cloud profiles: 0 filament, 0 printer, 0 process`, which looks like a successful backup of an empty account. Cloud profiles are now collected from **every connected account across both Bambu Cloud and Orca Cloud**, one directory per cloud per account, keyed by user ID so no email address is written into a backup repository. Bambu presets are stored with the payload needed to recreate them rather than just their names, and Bambu's bundled public catalogue is skipped — it is identical for everyone, re-downloadable, and would rewrite the repository on every run. The metadata now records what was actually collected, per cloud and per account, and a run that collects nothing while the category is enabled says so as a warning instead of an INFO line that reads like success. The Cloud Profiles checkbox no longer keys off your own Bambu sign-in — it enables when *any* account is connected and shows how many are in scope, which matters on a multi-user install where the presets being backed up are other people's. A backup also no longer disconnects an Orca Cloud account whose session it can't refresh: Orca reports every rejection with one composite reason, so a genuine revocation is indistinguishable from a lost token-rotation race, and an unattended job should not be the thing that guesses. The account is skipped with a warning, and the dead credentials are cleared the next time you open Orca Cloud Profiles — where you can pair again on the spot. Translated in all locales; wiki updated. Covered by backend tests.
|
||||
- **A heavy model failed to slice after five minutes with "Slicer sidecar unreachable" (#2730, reporter @kpp39)** — A MakerWorld model that Bambu Studio also takes a long time over never finished slicing in Bambuddy: five minutes in, it failed claiming the slicer sidecar could not be reached. **Root cause.** The slice request carried a fixed five-minute limit covering the whole operation, and it was applied to the wrong thing. Slicing is a single long request, so the limit was a ceiling on how long a model was allowed to take — not a check on whether anything had gone wrong. When it expired, the resulting error was indistinguishable from a genuine connection failure, so a slice that was progressing normally was reported as an unreachable sidecar. The reporter went and updated their sidecar container, which was never the problem: it was reachable throughout and still slicing when Bambuddy hung up on it. **Fix.** Bambuddy already polls the sidecar once a second for progress — that is what drives the live progress toast — so it can tell a slow slice from a stuck one, and now does. The limit applies to *silence*: a model that keeps reporting progress runs to completion however long it takes, and a slice is only abandoned when the slicer has said nothing for the configured period. The new **Slicer stall timeout** under Settings → Workflow → Slicer sets that period, defaulting to fifteen minutes, and the failure message now says the slice ran out of time and where to change it rather than blaming the connection. Sidecars too old to report progress have no liveness signal to offer, so for those the setting still bounds total slicing time — the previous behaviour, but configurable and no longer five minutes flat. A sidecar that genuinely cannot be reached still fails immediately and still says so. Translated in all locales; wiki documents the setting. Covered by tests for a slow-but-progressing slice completing, a stalled one failing, a frozen progress report not counting as progress, the two failure messages, and the setting falling back safely when unset or unparseable.
|
||||
- **Deleted prints stayed in their project as cards with broken previews, and could not be removed (#2731, reporter @sroesner)** — Deleting a print that belonged to a project left it on the project page with a missing thumbnail, and there was no way to unassign it. **Root cause.** Deleting a print is a soft delete by default: the files go from disk, the row stays so Quick Stats keeps counting its filament, time and cost. Every other part of Bambuddy skips those rows — the projects module skipped none of them, so a deleted print kept its project link and kept being listed, pointing at a thumbnail that no longer existed. The same broken previews appeared on the project cards in the overview, not just the detail page, and in the project timeline, where clicking the entry led to an archive that no longer opens. Unassigning was impossible because the only way to change a print's project is from the Archives page, which correctly hides deleted prints — so the entry could be seen but never reached. **Fix.** A deleted print now leaves its project everywhere: the archive list, the card previews, the timeline, and the counts. Excluding it from the *counts* is a deliberate difference from how Quick Stats treats the same print — a project is a piece of work with a definite membership rather than a lifetime total, so a project that lists eleven prints should not claim twelve. Existing broken entries disappear on upgrade with nothing to clean up; the API can still clear a stale link if anything needs repairing. Two more places were counting deleted prints for the same reason and are fixed with it: the archive CSV/Excel export handed back rows the interface says are gone, and per-project failure analysis measured a failure rate against prints that had been deleted from the project. The project page also no longer needs a manual reload to catch up: deleting a print refreshed the archive list but nothing project-related, and assigning a print to a project refreshed the project cards but not the project page itself, so for the following minute either view could still be showing what was there before. Covered by tests for the listing, the card previews, the timeline, the counts, both services, the cache refresh, and a guard that a project's live prints are untouched by any of it.
|
||||
- **A printer refusing Bambuddy's commands looked healthy, and its queue failed with the wrong advice (#2732, reporter @hennischd)** — Uploads succeeded, the printer echoed the job back, then sat idle for 270 seconds and the job was re-uploaded twice more before failing with a message about SD cards. Temperature changes returned success and did nothing. The connection diagnostic passed every check, and the support bundle said Developer Mode was on. **Root cause.** The printer was rejecting every control command and saying so: HMS `0500-0500-0001-0007`, "MQTT command verification failed" — the firmware's authorization check, which Bambu Lab documents Developer Mode as theway to disable. Nothing in Bambuddy connected that to anything. The error itself was received and then discarded by the frontend, because this code's meaning lives in bits that Bambuddy's short-code form throws away: it collapses to `0500_0007`, matches no catalog entry, and uncatalogued errors without firmware actions are filtered out of the badge count and the error list. Meanwhile the Developer Mode probe reads any response that isn't an explicit refusal as confirmation, and this firmware answers the probe with an empty result while refusing everything else — so Bambuddy inferred a healthy printer from a non-answer, and reported that inference as a passing diagnostic. **Fix.** HMS codes are now looked up by their full identifier before the short form, so this error survives to the screen, shows the four-group code the printer's own display shows, and carries the fix rather than Bambu's "update Studio or Handy" (which does not apply to a print sent from Bambuddy). The error is treated as authoritative about the printer's state: it sets Developer Mode to off regardless of what the probe concluded, which makes the diagnostic and the support bundle report the real situation, and it clears itself when the printer stops reporting it, so enabling Developer Mode and restarting is picked up without restarting Bambuddy. The probe no longer reads an inconclusive answer as confirmation — it reports what it knows, which for this firmware is nothing. A queue item whose command is rejected now fails on the first attempt naming the code and the fix, instead of spending three uploads and fifteen minutes of a farm's upload capacity to arrive at the wrong conclusion; a print that is visibly running is never touched, whatever HMS is lingering. Separately, the log hint suggesting a wrong or mis-cased serial number no longer fires in the moment after a reconnect, when the report counter it reads has just been reset and proves nothing — it cost this reporter a detour through their serial number on a printer whose serial was correct. Translated in all locales. Covered by tests for the code surviving the filter, the display form, the developer-mode override and its self-clearing, the inconclusive probe, first-attempt failure, the running-print guard and the suppressed hint.
|
||||
- **A printer that dropped off MQTT could stay offline indefinitely (#2732, reporter @hennischd)** — In the same bundle, the printer lost its MQTT session to a keep-alive timeout at 02:19 and did not come back until 11:24 — nine hours offline, with the web UI open the whole time. Bambuddy's stale-session detector only covers the other failure: a session that is still connected but has gone quiet. Once the connection is *down*, it returns immediately and the MQTT library's own retry is the only thing still watching; when that stops making progress, nothing notices. Bambuddy now runs a backstop sweep every minute. A printer that had a working session, has been silent for five minutes, and still answers on its MQTT port gets its client rebuilt from scratch with a fresh session — which also drops any command left unacknowledged on the dead one, so it cannot replay into the new session. Printers that are simply switched off are left alone: the port check tells the two apart, so there is no client churn and no nightly log spam for a farm that powers down. The rebuild is rate-limited per printer and never touches a connected one, and the log line names how long the printer was gone and what the last connection error was, so a session that dies repeatedly leaves a trail. Covered by tests for the recovery itself, the grace period, the switched-off case, the retry interval, and a farm sweep continuing past a printer that throws.
|
||||
- **A slot on Generic PLA offered only one K profile, however many the printer held (#2710, reporter @tommyboy180)** — The printer's Flow Dynamics table held nine calibrations, all of them under Generic PLA; Configure AMS Slot offered exactly one, the one already bound to the slot, and after a slot reset it offered none at all. The only way to assign the others was Bambu Studio. **Root cause.** Two independent faults, both triggered by picking a built-in generic preset. First, K profiles are matched to the selected preset by filament ID, but matches on Bambu's generic IDs (`GFL99` for Generic PLA, `GFG99` for Generic PETG, and so on) were deliberately thrown away as too broad — and since the comparison already requires both sides to carry the *same* ID, that exclusion could only ever fire when the chosen preset was itself the generic one, which is precisely the case where the match is right. Second, the matcher read the leading "Generic" in "Generic PLA" as a manufacturer, and so demanded the word "Generic" appear in the K profile's name; no real profile has it, which killed the name-based fallback as well. The one profile that did show up came from an unrelated safety net that always surfaces the slot's active profile, and a reset slot has none — hence the empty list. **Fix.** A K profile whose filament ID equals the selected preset's now matches, generic or not: the printer keeps one calibration table per filament ID, so a slot on Generic PLA offers everything calibrated under Generic PLA, whatever the user named those entries — "Dark Brown", "Marble", "Glow" and the rest now all appear. "Generic" is no longer treated as a brand, so profiles still match by material when a printer reports no filament ID at all. Since no name-and-ID matcher can be perfect against profile names the user invents, the picker now also lists every remaining profile on the printer under **Other K profiles on this printer**, so a profile that exists can always be selected; picking one works exactly as before, because Bambuddy already realigns the slot's filament context to whichever profile is chosen. The same generic-ID rule now applies to the spool form's K profile suggestions, guarded so it can never cross materials — a PETG spool is never offered PLA calibrations — and so that a spool naming its own brand keeps its brand-specific suggestions. Translated in all locales. Covered by tests for the reporter's nine-profile printer, the reset slot, printers that send no filament ID, the other-profiles group and applying a profile from it, and by guards that a brand preset still does not sweep in generic profiles.
|
||||
- **The print-complete photo showed the toolhead still printing, three minutes before the print ended (#2547, reporter @anthonyma94)** — The Discord photo caught the model mid-print with the head over it, instead of the finished print. **Root cause.** Bambuddy fired the photo the moment `layer_num` reached `total_layer_num`. That edge is the moment the printer *starts* its final layer, not the moment it finishes it: on the reporter's H2C it arrived at 92% with two minutes of print still to run, and the last layer took three minutes and seventeen seconds including a filament change. Worse, that trigger latched, which locked out both of the triggers that fire at a genuine end of print — so on printers that never report an end-of-print filament unload (H2C and A1 Mini confirmed) there was no way back to a correct photo. **Fix.** The last-layer trigger is gone. The photo is taken when the print reports itself finished, which is a signal every model sends and which lands after the toolhead has parked. Since the printer's own end G-code drops the plate about 100 mm just before that, Bambuddy now raises it back to just above the last printed layer, takes the photo, then lowers it again — restoring the framing asked for in #1145, #1397 and #1565. The plate move is an absolute Z to a height the toolhead occupied seconds earlier, so it stays inside the travel limits and keeps the nozzle above the part, and it is skipped outright unless Bambuddy can confirm the height belongs to this print: the sliced file is matched by print name, and its layer count is cross-checked against the layer count the printer reported. It is also skipped when another job is queued, when the printer has moved on, and when the new **Restore plate for finish photo** setting is off. Prints whose End G-code Bambuddy injected — SwapMod plate swaps and similar — are detected automatically and keep using a frame from during the print, since their model has left the bed by then (#1867); that frame now also refreshes through the final layer instead of freezing when it began. Prints that record a timelapse normally source the photo from the video's last frame and need no move at all, but when the video has not transferred in time — the usual outcome on P1-series — the live photo that ships in the notification now gets the same plate restore, so it is no longer a shot of an empty-looking lowered bed. The `stg_cur=22` trigger is left in place for any firmware that does emit it, but the bundle survey above still applies — in practice every model reaches the finish-state path. Translated in all locales; wiki updated. Covered by backend tests for the removed trigger, the two surviving ones, the plate move and every condition that suppresses it, and by frontend tests for the new setting.
|
||||
- **A model sliced for PETG printed as PLA, and the print dialog then refused to match PETG (#2712, reporter @kpp39)** — Slicing a MakerWorld model with a PETG profile produced G-code the printer wanted PLA for, and the Filament match step offered no way to correct it. **Root cause.** The list of filaments the slice dialog shows is positional: the first row is the printer's first slot, the second row its second, and so on down to the slicer itself. For a model that already carries slicing information, Bambuddy listed only the slots that print — which is the right answer when you are starting a print and Bambuddy has to match spools in the AMS, and the wrong one here. The reported model declares four filaments and paints with the fourth alone, so the dialog showed a single row; the PETG chosen in it became the *first* slot, and the fourth — the one the model actually prints with — kept the profile baked into the downloaded file. The result was a genuine PLA print, so the Filament match step was right to insist on PLA. **Fix.** When picking profiles for a slice, the dialog now lists every slot the project declares, with the ones this plate doesn't print with shown greyed out as before, so each row lines up with the slot it stands for and a choice made in the fourth row reaches the fourth slot. Starting a print is untouched and still asks only for the spools the job needs. Covered by tests for a source whose only printed slot is the fourth, for the print path keeping the shorter list, and for the chosen profile arriving in the right position.
|
||||
- **A finished slice produced a stream of a dozen "Sliced ..." notifications** — One slice reported itself complete over and over, a notification every second and a half for as long as twenty seconds. **Root cause.** While a slice runs, Bambuddy asks the server how it is getting on every 1.5 seconds — but it never waited for an answer before asking again. Slicing a large project keeps the server busy for seconds at a time, so those questions piled up unanswered, each one still believing the job was running. When the server caught up it answered all of them at once, and every single answer was treated as the moment the slice finished: one notification each, one list refresh each. The bigger the project, the longer the pile and the more notifications. **Fix.** Bambuddy now waits for an answer before asking the next question, so nothing can pile up and a busy server isn't asked to do more work while it is already behind. A job's completion is also recorded once and only once, and a check that was already in flight when the tracker restarts now stops instead of finishing its work — either of which is enough on its own to keep a duplicate off the screen. Covered by tests for a server stalled across many intervals, for two slices finishing where one restarts the tracker, and for the same job being tracked twice in a row still reporting both times.
|
||||
- **A database hiccup during dispatch could leave a queue item stuck and the next print of that file filed under the wrong archive** — When PostgreSQL briefly refused a connection in the middle of dispatching a queue item, the dispatch failed part-way through and left two things behind. **Root cause.** Bambuddy tells itself to expect a print just *before* it sends the print command, because the printer can report the job before the send even returns — but nothing undid that expectation when the command was never sent. The entry does expire after two hours, which is far longer than it takes to react to a failed job by pressing print again: that reprint was folded into the old archive and inherited its filament mapping and plate instead of getting a fresh one. Separately, releasing the row's dispatch lock is best-effort and needed the same database that had just refused, so it gave up after one attempt and the item stayed invisible to the scheduler until a restart. Nothing was ever sent to the printer — the failure happens before the print command — so this cost a stuck item, not a wrong print. **Fix.** An expectation is now withdrawn whenever the print command doesn't go out, which also covers two cases that were silently leaking before: a job cancelled during dispatch, and a print command the printer rejects. The dispatch lock is retried rather than abandoned after one try, and any lock left behind is released on the next quiet moment instead of surviving until a restart. Covered by tests for the withdrawal being an exact inverse of the registration, for a confirmed print keeping its expectation, for two dispatches not disturbing each other, and for the lock recovering from both a brief and a sustained database outage.
|
||||
- **An unusable layer number from a printer could drop its connection (#2702 follow-up)** — Reading the current layer from a status message assumed it would always be a number. Anything else raised an error out of the routine that reads those messages, and nothing above that point catches it, so the connection's listener stopped and the printer looked silent until the staleness check rebuilt it — losing not just the layer number but the print-start and print-finished detection carried in the same message. An unusable value is now ignored, holding the last known layer rather than substituting zero, which the firmware uses to signal a cancellation. This is the same containment applied to the layer *total* in this release, three lines away in the same routine. Covered by tests.
|
||||
- **The layer count stayed empty for a whole print, and First Layer Complete notifications read `1/0` (#2702, reporter @sn8key)** — On a P1S, a job started from Archives or through the Virtual Printer showed no layer information in the Print Status panel, and the Discord **First Layer Complete** notification went out as `1/0` instead of `1/33`. The count usually appeared some minutes into the print, which made it look intermittent, and the reporter noticed it filling in at the exact moment they submitted a bug report. **Root cause.** Bambuddy applies the printer's reported layer total as soon as it arrives, and separately clears that total when it detects a new print starting, so a leftover count from the previous job cannot become the denominator for this job's filament split. Both happen while processing the same status message, in that order — and the message announcing a new print is frequently the same one carrying the new print's layer total, so the value was stored and then immediately discarded. That is not merely late but unrecoverable: Bambu firmware sends only fields whose value has changed, so having published the total once, the printer never mentions it again. It could only come back in a full status refresh, which Bambuddy requests when it connects and when you press Force Refresh, but not during a print. Submitting a bug report happens to trigger one, which is why that appeared to fix it; and an install whose connection drops from time to time recovered by itself within minutes, which is why the fault looked random and why a *stable* connection made it worse rather than better. Everything that shows a layer count reads this single value, so the finish-photo trigger that fires on the last layer and the mid-print filament split were affected the same way. **Fix.** The new-print reset now keeps the total that arrived alongside the starting message, while still discarding anything left over from the previous job. If the starting message carried no total, Bambuddy asks for a full refresh once, and once more if layers are being laid down and there is still no total — by which point the printer certainly knows it. That is at most two extra messages per print, and never a per-layer retry. One incidental fix: an unusable layer total — `null`, or anything that isn't a number — previously raised an error out of the routine that reads status messages. Nothing above that point catches it, so the whole message was thrown away and the connection's listener stopped, leaving the printer to look silent until the staleness check rebuilt the connection. Such a value is now ignored, and the refresh described above then fetches the real total. Covered by tests for the same-message case, the case where the total arrives a message earlier, the previous job's total still being discarded, the one-shot bound, the refresh answer not re-triggering the reset, and the malformed values.
|
||||
- **Support bundles could contain a printer-status file that no tool could open (#2702)** — Every support bundle includes a redacted copy of the printer's last status message, so that a given model and firmware's actual field layout is available when diagnosing a report. For printers whose AMS flow-calibration factor had a particular number of decimal places, that file was not valid JSON. **Root cause.** Redaction ran over the finished text of the file, and one of its patterns recognises Bambu serial numbers by shape — a shape the digits of a decimal number can also take. A calibration value of `0.0199999995529652` came out as `0.[SERIAL]`, which is not a number, so the file failed to parse from that point on. **Fix.** Redaction now walks the structure and rewrites text values only, leaving numbers, true/false and empty values exactly as the printer reported them, so the file always parses. Nothing is redacted less thoroughly than before. Covered by tests using the value from the bundle that exposed this, and for the walk leaving keys, booleans and nested containers alone.
|
||||
- **External-camera timelapses and finish photos came out empty when the live view was open (#2707, reporter @bitbarista)** — On a printer with an external camera, watching the live view while a print ran meant the layer timelapse recorded almost nothing and the finish-photo notification went out with no image attached. The reporter measured zero of 87 layer captures on one print and zero of 105 on another, both watched from start to finish. A USB camera allows exactly one program to hold it open, so every capture taken during a live view failed outright rather than merely degrading. **Root cause.** Bambuddy already knew not to do this for the printer's built-in camera: a snapshot taken while somebody is watching reuses the viewer's frame instead of opening a second connection (#1348, #1271). That rule was never extended to the external-camera paths — and could not have been, because the buffered frame it depends on was only ever published by the built-in paths. The live external stream tracked when frames arrived but never kept one, and the stream hands out frames already wrapped for the browser, so there was nothing for a consumer to reuse. **Fix.** The external stream now publishes each frame as it goes past, and every one-shot consumer — layer timelapse, the finish photo and its fallback, the notification snapshot, Obico polling and the plate check — reuses that frame instead of opening a second handle. If a viewer is attached but no frame has arrived yet, that single attempt is skipped rather than competing, because kicking the viewer off is worse than missing one frame. Two things improve as a side effect: `/camera/snapshot` and the finish-photo fallback chain can now serve an external camera's live frame, where before they found an empty buffer, and the buffer is released when the last viewer of that printer leaves. Covered by tests for the frame plumbing, a buffering failure being unable to break the live stream, and each consumer in all three states — viewer with a frame, viewer without one, and nobody watching.
|
||||
- **A long-running camera stream could eventually stall itself, with nothing in the log to explain it (#2707)** — ffmpeg's error output was only read when something had already gone wrong, which meant that for the entire life of a working stream nobody read it. ffmpeg writes a startup banner, its analysis of the incoming video, and then a progress line at a steady rate, and the operating system only buffers a fixed amount of that before it stops the writer. Once that happened ffmpeg would block trying to write the next line, stop producing frames, and the stream's own timeout would fire — reported as `RTSP read timeout` and a reconnect, with no indication that Bambuddy had starved it. How long it takes to reach that point is unmeasured and clearly long: one stream ran 21 minutes 36 seconds without trouble, so this is a limit that was being ignored rather than a fault anyone has reported. **Fix.** A streaming ffmpeg's error output is now read continuously and the most recent portion kept, so the limit cannot be reached. The kept portion is what gets logged when a stream does fail, which is more useful than before: it holds what ffmpeg said as things went wrong, where reading the buffer on demand returned whatever it had printed first — usually the startup banner, which is then discarded as noise. Credentials are masked on this path through the same single funnel as every other camera log. Covered by tests for continuous draining, the size bound, keeping the newest output, credential masking, and the ownership handover with the shutdown path — reading the same output from two places at once is an error, so only one owner reads it at a time.
|
||||
- **Reopening the camera quickly could leave the new stream invisible to Bambuddy (#2707)** — Closing a camera view and opening it again straight away could leave the newly started stream unregistered, even though it was running and showing frames. The consequences were all indirect, which is what made it hard to spot: Bambuddy believed no viewer was attached, so Obico polling and snapshots would open a second camera connection and fight the live view — precisely what the guards added in #1348 and #1271 exist to prevent; the background cleanup task saw a camera process with no stream attached to it and killed the live stream as an orphan, usually within a minute; and pressing Stop reported that it had stopped nothing while the view was still running. **Root cause.** Each printer's fan-out stream was registered under a key derived from the printer alone, so every successive stream for that printer reused the same key, and the departing stream's cleanup removed whatever was registered under it — including its own replacement. The same cleanup also cleared the printer's most recent camera frame unconditionally, discarding the new stream's frame. It needed the old and new streams to overlap, which the four-second teardown fixed above made easy to hit. **Fix.** Each stream now gets its own registry key, so one stream can only ever clean up after itself — the same approach the external-camera path already uses (#2675) — and the shared per-printer frame is only released when no stream for that printer is left running. Covered by tests for both halves, including one that drives the real cleanup path with a second stream already registered.
|
||||
- **Closing the camera held the printer's camera connection for four more seconds, then logged an error that wasn't true (#2707)** — Every time a camera view closed, the log recorded `ffmpeg didn't terminate gracefully, killing` and then `ffmpeg did not exit within 2.0s of SIGKILL; abandoning wait`. Both waits expired every single time, so each close cost a fixed four seconds — and because Bambu firmware allows exactly one camera connection, that was four seconds in which nothing else could use the camera: reopening the view, a snapshot, Obico, or the diagnostic. **Root cause.** ffmpeg is started with its output and error streams as pipes, and the shutdown path had stopped reading them. A process whose output pipe is full blocks mid-write, and ffmpeg's shutdown signal only sets a flag that its main loop checks on the next pass, so the polite request could never be acted on and the grace period was dead time. The forced kill did work — but Python cannot report an exit while a pipe is still unread, so the second wait expired too and Bambuddy concluded the process was stuck when it had already gone. Measured at 4.00s per close before, ~0.15s after. **Fix.** Both pipes are now drained while the process is being stopped, which makes the polite shutdown effective and the exit observable. The forced-kill path and its time limit remain as backstops, so a genuinely wedged process still can't hang a stream, a Stop request, or the cleanup task. This also corrects the conclusion recorded for #2580: that 12-hour hang was the unbounded form of this same self-inflicted stall rather than a stuck ffmpeg, so bounding the wait had capped the symptom without removing the cause. Covered by tests that drive a real subprocess — the fault lives in Python's pipe bookkeeping, so a stand-in object would pass against the broken code — including one that verifies a process ignoring the polite signal still has its forced exit observed rather than abandoned.
|
||||
- **A crash or restart mid-print left layer-timelapse files behind forever (#2709, reporter @bitbarista)** — `timelapse_frames/` grew slowly and never shrank on its own. After several routine restarts during testing, it had accumulated 38MB: three abandoned frame directories and two 48-byte `.mp4` files from stitches that never finished writing. **Root cause.** Which timelapse session is active lives only in memory. A restart for any reason — a redeploy, a crash, a power loss — loses that bookkeeping instantly, but the frames already written for that session, and any partially-stitched output file, stay on disk with nothing left that knows they exist. `on_print_complete`'s cleanup never runs for them, because nothing calls it: the session it would clean up no longer has an entry to be found by. A restart-recovered print doesn't get a replacement session either (`_maybe_start_layer_timelapse` only fires on a fresh `PRINT_START` event, #1353), so an orphaned directory can never be resumed or claimed by anything — it just sits there. **Fix.** A one-time sweep on startup removes any `timelapse_frames/<printer_id>/` entry that doesn't match a session that printer is currently recording or stitching, and that is old enough not to be a startup race (five minutes). Only this feature's own artifacts are swept — a frames directory or a `timelapse_<id>.mp4` — so anything else that ends up under that folder is left alone rather than deleted for being old. Verified against the real 38MB of leftovers: startup logged each of the five removed by name, and the directory dropped to 8KB. Covered by tests for the orphaned-directory and stray-output-file cases, sparing a genuinely active session, sparing one that is mid-stitch (the window where the session has already been handed to ffmpeg and is no longer listed as active), sparing anything too recent to be sure about, leaving unrelated files alone, not counting a removal that failed as a success, and two defensive cases (no `timelapse_frames/` directory yet, an unrelated non-numeric entry under it).
|
||||
- **Two snapshots taken at the same moment opened two competing camera connections (#2705, reporter @gzimbric)** — Bambu firmware allows exactly one camera connection at a time. Bambuddy already knew this: a snapshot taken while somebody is watching the live view reuses the viewer's frame instead of opening a second socket. What nothing covered was two *snapshots* overlapping with no viewer attached at all — an Obico poll and a printer-wall refresh landing 200 ms apart, each correctly concluding it wasn't competing with a viewer, and then colliding with each other. On the reporter's P2S this knocked over the live stream that was feeding the camera wall, which was then reaped for having received no frames for 58 seconds. Eight paths take one-shot frames independently — Obico polling, `/camera/snapshot`, the finish-photo capture and its disk-writing sibling, plate detection, the camera connection test, and the diagnostic — so any pair of them could overlap, and a shorter Obico interval widened the window. **Fix.** Simultaneous captures for the same printer now share one connection: the first opens it, everyone arriving while it is in flight gets the same frame. Every consumer here wants "a recent frame" rather than a frame stamped at its own microsecond, so identical bytes are the right answer. This shares captures, it does not cache them — a request arriving after the previous capture finished still takes a fresh frame, because plate detection and the finish photo judge a running print from these images and a stale frame there is worse than a slow one. Each caller keeps its own deadline (they range from 10 to 30 seconds) rather than inheriting whichever one happened to open the connection, giving up alone leaves the capture running for whoever else is waiting on it, and a capture that fails doesn't hand its failure to callers that never got an attempt of their own — they retry, which by then competes with nothing. One visible consequence: when the **Diagnose** tool shares a capture this way its frame-capture stage is labelled `coalesced_capture`, because the pass is real but the timing shown is mostly time spent waiting, and a diagnostic must not report on a connection it never opened. Wiki updated. Covered by tests for the reported collision, the five-callers-one-connection case the reporter verified on live hardware, per-printer isolation, staying coalescing rather than becoming a cache, registry cleanup, a failed capture not poisoning its followers, bounded retry, a follower abandoning its wait without sabotaging the capture, and cancellation from either side.
|
||||
- **The same one-shot-capture collision could happen on external cameras too, with no viewer attached (#2707 follow-up, reporter @bitbarista)** — #2705 fixed simultaneous captures colliding on the built-in camera path, keyed by printer IP through `capture_camera_frame_bytes()`. External cameras reach the same kind of collision through a different function — `external_camera.capture_frame()` — that #2705 didn't touch, and a V4L2 USB device allows exactly one open handle just like Bambu's own RTSP limit. Nothing coalesced two one-shot capturers here either: Obico polling, the in-print frame bank, the finish-photo moment, plate detection and the notification snapshot could each open their own connection to the same USB camera and collide, with `is_stream_active()` unable to help since that guard only stops a capturer from competing with an *attached viewer*, not with another capturer. **Fix.** The same shape of fix as #2705, applied to `capture_frame()`: concurrent callers for the same camera (URL, type, and — since #1177's snapshot override routes to a different endpoint entirely — snapshot URL) share one capture rather than opening a second connection. Coalesces, does not cache, so a call after the previous one finishes always captures fresh. Each caller keeps its own timeout, giving up leaves the capture running for whoever else is waiting, and a capture that fails doesn't hand its failure to a caller that never got a turn of its own. One visible consequence, mirroring the label #2705 added to the built-in **Diagnose** tool: pressing **Test** on an external camera while a capture is already running now says the frame was shared with it, because the result is real but the test did not open a connection of its own — and forcing one would be the very second handle this change exists to prevent. Covered by tests mirroring #2705's: the reported-shape collision, five callers sharing one connection, per-camera and per-snapshot-URL isolation, staying coalescing rather than becoming a cache, registry cleanup, a failed capture not poisoning its followers, bounded retry, a follower abandoning its wait without sabotaging the capture, and cancellation from either side — plus, for this path specifically, that an unexpected error is reported as a failed capture rather than raised at every waiting caller at once, and that the shared-capture log lines redact credentials, since an RTSP camera URL routinely carries `user:pass@` where the built-in path's key is only an IP address.
|
||||
- **Auto-matched filament showed a green tick when the colour was plainly wrong (#2687, reporter @pchulpjoost)** — The Filament Mapping panel reported a slot as matched, with the header reading **(Ready)**, while the swatch beside it showed the slice wanted dark red and the tray it had picked held Dark Green. Manually selecting that very same tray from the dropdown correctly reported the colour mismatch, which is what made the disagreement so visible. **Root cause.** Auto-match ranks candidate trays by filament preset ID (`tray_info_idx`) first, and when exactly one loaded tray carried the preset the slice asked for, that tray was accepted as a *definitive* match on the assumption "same preset means same spool, so the colour must agree too". The preset ID names the **variant**, not the spool — `GFA00` is PLA Basic, `GFA01` PLA Matte, `GFA17` PLA Translucent, in every colour Bambu sells it. So a user with one Matte spool loaded matched every Matte requirement regardless of colour, and the colour comparison was never reached. This is why the report came in for PLA Matte in particular: generic PLA Basic is usually loaded several times over, which sent the match down a different path that did compare colours correctly. **Fix.** The colour verdict is now taken from the tray that was actually selected, never from which rule selected it, and the automatic and manual paths share one comparison so they cannot drift apart again. The preset still decides *selection*, because the Basic/Matte/Silk distinction matters ([#2650](https://github.com/maziggy/bambuddy/issues/2650)) — a wrong-coloured tray of the right variant is still chosen, but it is now reported as an amber **Color mismatch** instead of a green tick, and you can print anyway or pick another slot. A near-enough shade still counts as a match, and a 3MF that specifies no colour for a slot is satisfied by any colour rather than being flagged. Dispatch behaviour is unchanged: **Force color match** already required an exact colour before sending a job, so nothing was ever printed in the wrong colour because of this — the panel was simply telling you it was fine when it wasn't. Frontend-only. Wiki updated. Covered by tests for the unique-preset wrong-colour case, agreement between the auto and manual verdicts, the near-shade and colourless-requirement cases, and the multi-preset path that already worked.
|
||||
- **P1-series archives kept the worse finish photo when the timelapse arrived late (#2704 follow-up)** — When a print records a timelapse, Bambuddy prefers the video's last frame as the finish photo: the firmware stops recording after the toolhead parks but before the end G-code drops the bed, so it frames the finished print properly, where a live camera grab at that moment catches an already-lowered plate. Bambuddy waited 60 seconds for the video and then gave up, because the print-complete notification is waiting on that photo and holding a notification for minutes is worse than sending it with the live grab. On P1-series printers the video usually arrives later than that — they write MJPEG AVI instead of H.264 MP4 and serve it slowly, so across the support bundles their median was 33 seconds but the 90th percentile was 167 and the slowest observed was 546; every other model finished inside 26 seconds. The result was that the printers most in need of the better photo were the ones that never got it. **Fix.** The notification still goes out on the same 60-second bound with the live grab, so nothing gets slower. If the video was still on its way when that bound expired, Bambuddy now keeps waiting in the background and adds the extracted frame to the archive when it lands, at the front of the photo list so opening the gallery shows it first. The live grab is kept rather than replaced — the notification that already went out links to that exact file, and removing it would leave a broken image in Discord or Telegram. Covered by tests for the ordering, the longer budget, idempotency and the cases where the video never arrives.
|
||||
- **Timelapses that never got attached, and a Scan button that could not find them (#2704)** — Timelapse was on for the print, the video never arrived in the archive, and pressing **Scan for Timelapse** afterwards turned up nothing. Measured across 247 support bundles, this was not rare: of 457 automatic scans only 262 ever attached a video. **Root cause, part one.** The scan looked four times, at 5, 10, 20 and 30 seconds, then stopped. The printer writes the video only after the print ends and a long print makes a large file, so it often arrived after the last look — the attempt that found the video was the first one 272 times and then 17 / 13 / 13, a flat tail against the cutoff rather than a decaying one. What ran after those four attempts was a fallback that searched for the print's name inside the video filename; Bambu firmware only ever writes `video_<timestamp>`, so in 247 bundles it fired 159 times and matched exactly zero. **Root cause, part two.** The manual Scan button had no such snapshot to work from and matched by filename timestamp, by FTP modification time, or by there being exactly one video on the printer — all of which read a clock the printer cannot set, because a printer in LAN Only mode never reaches Bambu's time server. The reporter's P1S was six and a half days out, which defeats every one of those. **Fix.** The automatic scan now polls for several minutes instead of giving up after about a minute, and the name-match fallback is gone. The list of videos present when the print started is saved with the archive, so the comparison survives a Bambuddy restart mid-print and the manual Scan button can use it too — same clock-independent comparison, no timestamps anywhere. When a previous print's video lands late and two files look new, the one already attached to another archive is ruled out by name rather than by picking whichever the printer listed first, which could attach the wrong video. **Bambuddy now deletes a timelapse from the printer once it has been archived**, which keeps the printer's folder down to unclaimed videos and stops P1-series cards filling up with AVIs; your copy is in the archive, where you can watch, edit, download or remove it. That delete only happens after the transfer has been checked against the size the printer reported — which also fixes a silent truncation: an FTPS transfer that ended early produced a partial video that was attached as though it were complete. Because the first look happens seconds after the print ends — while the printer may still be writing the video — the file is also re-checked afterwards and only accepted once it has stopped growing, so a partial video is never mistaken for a finished one and the printer's copy is never removed on the strength of one. Wiki updated. Covered by tests for candidate selection, the download check gating the delete, the poll bounds, baseline persistence and the manual scan.
|
||||
- **A printer that refuses Bambuddy's access code now says so, instead of reconnecting silently forever (#2698, reporter @djepsylon)** — A printer whose access code or serial was wrong produced no explanation anywhere: the connection attempt was refused, and the only trace was a warning every 30 seconds reading `MQTT disconnected: rc=Unspecified error` — the same line you get from a printer that is simply switched off. The failure branch of the MQTT connect callback set "not connected" and discarded the reason code the printer had just sent, so the one piece of evidence that would have named the cause never reached the log, the support bundle, or the UI. In the report behind this fix, one of three printers had been in that loop for the entire capture and nothing said why. **Fix.** A refused connection is now logged with the printer's own reason ("Not authorized", "Bad user name or password") and, for those two, the remedy — the access code is regenerated every time LAN Only or Developer Mode is toggled, so it has to be re-read from the printer's screen. The reason is kept on the connection, so the **Connection Diagnostic**'s *Printer credentials* check now states plainly that the printer refused the credentials when that is what happened, and falls back to hedged wording when all Bambuddy knows is that there is no session — previously it asserted "the access code is most likely wrong" even for a printer that was merely rebooting or already at its connection limit. The same reason is returned by the pre-add connection test. The access code itself is never written to the log. Translated in all locales; wiki updated. Covered by tests for both refusal codes, the clearing of the reason on a successful reconnect, the diagnostic's reason plumbing, and the two UI variants.
|
||||
- **Camera credentials could reach the log and the support bundle, and a camera test could pass without opening a connection (#2721, contributor @bitbarista)** — Review follow-ups on the external-camera capture coalescing. That logic was transplanted from the built-in camera path, which keys on a printer IP and so has nothing to hide in a log line; these keys are camera URLs, and an RTSP URL routinely embeds `user:pass@`. Five new log lines printed the password, one of them at warning level, where it reaches support bundles. All five now redact **before** truncating — slicing first can cut the URL short of the `@` the pattern anchors on and leave the password intact, which is why every other URL log in the module already does it in that order. **Also fixed:** an unexpected error inside one capture reached every caller waiting on the shared result at once, so one caller's failure became N and none of them retried; the shared wrapper now contains it and reports a failed capture. And **Test connection** returned success for a frame it had been handed from someone else's in-flight capture — the one answer a connection test must not give silently. It still shares rather than forcing its own capture, because forcing one would open the second handle to a single-reader device that this whole mechanism exists to prevent; it now says the frame was shared with a capture already running.
|
||||
- **The orphaned-timelapse sweep could delete a timelapse while it was still being stitched (#2722, contributor @bitbarista)** — The sweep's own docstring claimed its age margin made it safe to run mid-stitch. It was not. A session is dropped from the active list before its frames are handed to ffmpeg, so for the length of a stitch the directory matches no active session, and its mtime is the last layer's frame write — on a tall print's final layer, easily older than the margin. The default margin and the stitch timeout were both 300 seconds, so the two were tied with no headroom at all, and a sweep landing in that window deleted ffmpeg's input from under it. An in-progress stitch is now marked explicitly, and the marker is cleared in a `finally` so a failed stitch cannot leak it and leave that printer's files permanently un-sweepable. **Also fixed:** the sweep deleted any file under its working directory past the margin rather than only the `timelapse_<session_id>.mp4` shape this feature creates — nothing else writes there today, but age alone is not a reason to delete a file this feature did not create. And a removal that failed on a read-only mount or a permissions problem was counted and logged as a success, which matters because that log is the only evidence an operator has of what was deleted.
|
||||
- **Finish photos came out upside-down, or rotated twice, when a camera rotation was set (#2723, contributor @bitbarista)** — `camera_rotation` was only ever wired into the print-start photo and the in-print frame bank; the finish-photo pipeline saved its frames straight to disk unrotated. Fixing that surfaced a second fault: rotating the frame where it was consumed rotated one of its two sources twice, because the in-print bank's frames arrive already rotated while live grabs are raw, and the consumer cannot tell them apart. On firmware that never emits `stg_cur=22` — the path that bank exists to serve — a 180 degree rotation cancelled itself out and the photo was upside-down again, with 90 and 270 landing 180 out. Rotation now happens where each frame is captured, so every cached frame carries exactly one whatever produced it. **Also fixed:** two finish-photo sources were still writing unrotated files — the built-in camera's own capture, and the still extracted from a printer-recorded timelapse, which is the *preferred* source for a built-in camera print, so whether a photo came out the right way up depended on which source happened to win. Layer-timelapse frames are rotated too. Note that the archived video is the printer's own file and is not re-encoded, so it still plays at the camera's native orientation. A failed rotate leaves the unrotated file rather than losing a delivered photo.
|
||||
- **Ukrainian was listed above Russian in the language picker** — Locales appear in the picker in the order they were added, but `uk` had been inserted ahead of `ru` in the import block, the resources map and the list the picker renders. Moved to the end of all three; the alphabetically sorted supported-language list already had it in the right place. Frontend-only, with no behaviour change beyond the row order.
|
||||
- **Setting Spoolman options over the API with a true/false value returned a server error** — `PUT /settings/spoolman` accepts a free-form body, and sending the natural JSON form for a switch — `{"spoolman_enabled": true}` rather than `{"spoolman_enabled": "true"}` — came back as a 500 with nothing useful in it. The shipped UI always sends strings, so this only affected people driving Bambuddy from a script or a Home Assistant `rest_command`, which is exactly where a real boolean is the obvious thing to send. **Root cause.** Settings are stored as text and every reader compares them as text, but the submitted value went in untouched. Deciding whether Spoolman had just been switched on called a string operation on it, which a boolean does not have; and the raw boolean was also written straight to a text column, which SQLite quietly turns into 1/0 while PostgreSQL refuses it outright — so the stored result depended on which database the install used. **Fix.** Boolean-ish settings are now converted to a canonical `true`/`false` on the way in, accepting real booleans, `1`/`0`, and the usual spellings (`True`, `yes`, `on`) case-insensitively, since this is a documented API that scripts talk to. A value with no sensible reading, such as `"banana"`, now returns a 400 naming the field instead of being stored as-is and silently treated as off. Two details are preserved deliberately: a blank value still means "use the default" for the two options that default to on, and reading a stored value stays as strict as it has always been elsewhere in the codebase, so no existing row changes meaning. Text options are checked too, so a JSON object can no longer be stored as its own printed form. One incidental improvement: a value stored as `True` by an earlier API call showed as off in the UI, which compares case-sensitively, while the backend treated it as on — canonical storage removes that disagreement. Covered by tests across the accepted spellings, the rejected values, the blank-means-default behaviour, and the read path.
|
||||
|
||||
|
||||
## [1.2.5] - 2026-07-24
|
||||
|
||||
### Added
|
||||
|
||||
Reference in New Issue
Block a user