mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
c765d2f2fbdfac40a9f2be1f0f3f46eb0cee13df
831
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
18938a10ee |
fix(kprofiles): stop reporting rejected K-profile writes as saved
Saving a K-profile was fire-and-forget. set_kprofiles_batch published and returned True, and the printer's extrusion_cali_set answer was logged at DEBUG and dropped, so a write the printer refused was reported to the user as saved (#2718, reporter @jmoore-skild). The reason it could not simply be gated on: the answer itself was wrong. Single-nozzle firmware returned result:"fail" with reason:"invalid tray_id" on writes that demonstrably applied. Measured against an X1C and an H2D over MQTT, the cause is the tray_id:-1 Bambuddy itself put in the payload. Sending three otherwise identical writes isolated it: tray_id:-1 fails, tray_id:0 succeeds, and cali_idx:-1 is accepted either way, so only that one field is at fault. The H2D ignores the value entirely; the X1C validates it, complains, and applies the write anyway. BambuStudio always sends a real tray_id and defaults it to 0 for a manually entered profile. With tray_id:0 the acknowledgement is honest, and the printer echoes back the sequence_id we sent -- confirmed for extrusion_cali_get, _set and _del on both printer classes -- so it can be matched to the write that caused it. Writes now return their sequence_id and the routes await the verdict, turning a real failure into an error that carries the printer's own reason. A printer that stays silent is still treated as success: no answer is not evidence of refusal, and firmware that never answers must not turn every save into an error. Raises the ack to INFO. It sat at DEBUG, so the one line that explains a failed save was absent from every support bundle -- the same reasoning that put ams_filament_drying at INFO for #1447. Also fixes extrusion_cali_set building its payload from str(self._sequence_id) without incrementing first, reusing the previous command's id. Harmless while nothing correlated on it, fatal now that the write path does. Adds supports_nozzle_flow_type() for the Standard / High Flow choice, which the K-Profiles UI previously showed as "Not reported by printer" -- not a value anyone can save. Most printers omit the nozzle identity from their calibration table entirely, and the slicer treats that as Standard rather than unknown; Bambuddy now does the same and keeps the choice editable. The field is hidden only where the model ships a single nozzle variant, using the slicer's own rule (len(nozzle_volume) // len(nozzle_diameter) > 1 over the machine preset) evaluated across every bundled Bambu profile. That puts only A1, A1 Mini and A2L on the hidden side -- it is not the single- versus-dual-nozzle split, since P1P, P1S, P2S, X1, X1C, X1E and H2S are all single-nozzle and all carry two variants. Editing a profile also no longer writes back an empty nozzle_id. Wiki records that on printers which omit the field the chosen flow type is discarded by the firmware and reads back as Standard, in Bambu Studio as well, so it does not get filed as a bug again. |
||
|
|
455a9e4ba7 |
fix(backup): collect cloud profiles from every connected account (#2717)
Enabling Cloud Profiles for a Git backup produced nothing, and said it had
worked. Two independent faults, either one sufficient.
The collector looked for a "setting" list. The Bambu Cloud listing endpoint
is keyed by preset type instead, each key holding private and public arrays,
so the loop body never executed once — and the entries carry no type of
their own either, which routes/cloud.py already knew: it takes the type from
the outer key and maps Bambu's "print" to process. Two bugs on one line.
It also asked build_authenticated_cloud for the credential store used when
authentication is disabled. With auth on, tokens live on User rows, so the
collector returned at "Cloud not authenticated" before ever reaching the bad
key. Every multi-user install was collecting from zero accounts.
Neither failure surfaced. backup_metadata.json recorded the configured flag
rather than the outcome, so it claimed cloud_profiles: true on runs that
wrote nothing, and the log read "Collected cloud profiles: 0 filament, 0
printer, 0 process" at INFO — which is exactly what a successful backup of
an empty account looks like.
Cloud profiles now come from every connected account across both clouds. The
toggle predates Orca Cloud entirely, and Orca has the same three preset
types, so both are collected and grouped the same way:
cloud_profiles/bambu/user-3/{filament,printer,process}.json
cloud_profiles/orca/user-3/{filament,printer,process}.json
Accounts are keyed by Bambuddy user id, "global" when auth is off. Never by
email: a backup repository can be public, and the Bambu listing's user_id is
dropped for the same reason. Both credential stores are read on every run,
because a Settings row survives someone enabling auth later and dropping it
would silently stop backing that account up.
Bambu costs one get_setting_detail per private preset. The listing is
metadata only, and without base_id and setting the backup is a list of names
that create_setting cannot rebuild from. Public presets are skipped — Bambu's
bundled catalogue is the same hundreds of entries for everyone, always
re-downloadable, not recreatable under your account, and would rewrite the
repository on every run. Orca needs no second call; its sync-pull carries
each profile's content inline. Where the Orca route drops a profile whose
content.type it cannot map, the backup writes it to other.json instead:
silently omitting a profile because Orca added a type is the same class of
bug as this one.
Failures are contained per account and per preset, and counted rather than
swallowed. A partial backup that looks complete is how this stayed invisible.
The metadata now reports what was collected, per cloud and per account, and a
run that collects nothing while the category is enabled warns with the reason
instead of an INFO line that reads like success.
The checkbox gated on the viewer's own Bambu sign-in, which is not the same
question as whether there is anything to back up — with auth enabled the
accounts belong to individual users, and an administrator who never signed
in personally saw the category disabled with plenty in scope. It now gates
on the total across both clouds and shows the counts. That comes from its
own endpoint rather than a field on /config, since /config answers null
until the first save and would disable the toggle during the very setup it
belongs to. Counts only, never identities.
One deliberate restraint. _build_authenticated_service clears stored
credentials when a refresh is rejected, which is right for a route — the
user is on the page and can pair again — and wrong for a scheduled job.
Orca reports every rejection with one composite reason ("unknown, expired,
revoked, or already used"), so a genuine revocation cannot be told apart
from a lost token-rotation race, and acting destructively on a signal that
cannot be disambiguated is the #2562 mistake in a different cloud. It also
gains nothing: the Profiles route hits the same failure and clears it then,
with the user present. Background callers now pass clear_on_auth_failure=
False and skip the account. A successful refresh is still persisted either
way — by that point the old token is consumed, so dropping the new pair
would break a working pairing for real.
Restore is not part of this. Nothing reads cloud_profiles/* yet; the format
carries base_id/setting for Bambu and content for Orca so that it can.
|
||
|
|
4f2c073a34 |
fix(vp): gate the slicer's AMS pick behind the toggle and scope its badges (#2700)
Round-3 review of the "Save AMS mapping" PR. The queue item's ams_mapping was set unconditionally, on the reasoning that honouring the slicer's own pick is a correctness fix rather than a feature. It is both. Storing a resolved mapping makes _ensure_ams_mapping return early, so _compute_ams_mapping_for_printer never runs — and that function is where prefer_lowest_filament lives, along with the AMS-filament-backup gate that qualifies it (#1766), the inventory-remain overrides, and the per-slot force-colour overrides. Every existing queue-mode VP pointed at a printer would have quietly lost all of it on upgrade, without a setting to turn it back on. So save_ams_mapping now gates the queue item too, not just the archive persistence. Off is exactly the old behaviour. The correctness case the PR was written for — two spools of the same red PLA, and the slot the user picked in the slicer thrown away — is still fixed, for anyone who asks for it. Force color match wins over it when both are on. Its only effect on a fixed-printer item is the filament_overrides written onto the queue item, and those are read inside the function a stored mapping skips, so the two toggles sitting next to each other on the same card silently cancelled. The dispatch now matches strictly, as asked, while the slicer's pick is still saved onto the archive — that is what the toggle's name promises, and a later reprint is a separate decision from this print. The queue-add fallback applies the same rule to a request that carries force-colour overrides. A mapping shorter than a plate's highest slot id cannot address that plate's own slots, and _ensure_ams_mapping would have kept it anyway, since it only rejects an all-unresolved one. Each plate now checks the length it needs and falls back to a computed mapping if the array does not reach. Bambu Studio sends a file-global array, so this normally never fires; it also means a multi-plate Send All degrades safely if that ever stops being true. The badges claimed more than they delivered. Both rendered whenever a saved mapping existed, ignoring which printer it belonged to, while the tooltips promised the reprint would reuse those exact spools — true only on the printer the trays were resolved against. The queue row's flag is now computed against that row's own printer, which is precisely when dispatch reuses the mapping, and the archive card names the printer instead of implying any of them will do. It hides itself when that printer no longer exists. Retranslated in all 13 locales. Frontend tests, which the PR had none of. The printer-scoping rule is now a pure function rather than an inline expression, covered for the mismatched printer, the no-printer-selected case that would otherwise compare undefined against undefined, and malformed extra_data. The toggle's undo bookkeeping is covered for unresolved slots, short mappings, and hand-made picks — preserved when the toggle never wrote that slot, replaced when it did, which is behaviour worth pinning either way. Also reverts all three queue-mode switches when a save fails, not just the new one; without it the card shows a setting the server rejected. |
||
|
|
4b4cb18a64 | Merge branch 'dev' into feature/save-ams-mapping-toggle | ||
|
|
c457cf54bf |
feat(support): record process memory, threads and children in bundles (#2734)
A bundle described everything except the process it runs in. So a report of memory climbing over days until the OOM killer fires arrives with no way to act on it: the numbers that name the mechanism only exist while it is happening, and by the time anyone asks, the container has been restarted. The new `process` section carries what actually separates the candidates. Resident against virtual memory: 650MB RSS with 12.9GB VMS is address space — thread stacks or allocator arenas — not a heap full of live data, and that reading is the opposite of the one the reporter drew from the same figures. Thread count and child-process count then split those two apart, and a census of live objects by type names what a growing heap is filling up with. Open files, sockets and uptime round it out. Three constraints worth keeping: The heap census is skipped above 2GB. gc.get_objects() materialises every tracked object, so it costs most on exactly the process that can least afford it — a bundle generated to diagnose runaway memory must not be the allocation that tips the host over. Everything else is still collected, and the skip is recorded with its reason rather than silently omitted. Children are recorded by executable name only. An ffmpeg command line carries the camera URL, and with it the camera's password. Collection runs off the event loop and every metric is independently best-effort. psutil raises on hardened kernels and in restricted containers, and the bundle is how someone reports a problem in the first place — it has to be produced even when half the numbers are unavailable. This does not fix #2734, and nothing here should be read as having found its cause. The bundle's own evidence contradicts both proposed causes: the orphan janitor ran 7 times in 26 days over 725 stream-ends and killed no orphaned ffmpeg, which is not the #776 signature; and the 5 "database is locked" errors all fall between two OOM kills, making them a symptom of the memory pressure rather than a source of it. |
||
|
|
ce3e59884a |
fix(slicer): bound slices by silence, not by total slicing time (#2730)
A heavy MakerWorld model — one Bambu Studio also takes a long time over —
failed after five minutes with "Slicer sidecar unreachable". The sidecar
was reachable the whole time and still slicing when we hung up on it.
SlicerApiService carried a hardcoded 300s timeout, passed to httpx as a
bare float so it covered connect, read, write and pool alike. On a single
long request that is not a health check, it is a cap on how long a model is
allowed to take. And because httpx.ReadTimeout subclasses RequestError,
expiry landed in the same handler as a refused connection and was reported
as an unreachable sidecar — so the reporter went and updated their sidecar
container, which was never the problem.
The information to do better was already being collected. _poll_progress
polls /slice/progress/{id} once a second alongside the blocking POST to
drive the live progress toast, so at minute five Bambuddy had fresh
evidence the slicer was working. It killed the request anyway.
So the read timeout comes off the HTTP call and the poller supervises
instead: the deadline moves forward on every progress update, and only
genuine silence ends the wait. A model that keeps reporting runs to
completion however long it takes. Connect and pool keep short timeouts —
a sidecar that will not accept a connection is unreachable and should
still say so quickly.
Only a *changed* progress payload counts as alive. The sidecar re-serves
its last snapshot on every poll, so counting repeats would leave the
watchdog unable to detect a stall at all.
The window is floored at three poll intervals: liveness can only be
observed as fast as the poller ticks, so anything shorter would expire in
the gap between two polls and fail every slice instantly.
New setting slicer_stall_timeout_minutes (Settings > Workflow > Slicer),
default 15, range 1-240, alongside the sidecar URL and gated on
use_slicer_api like its neighbours. Sidecars too old to report progress
have no liveness signal, so for those the same number bounds total elapsed
time — the old behaviour, configurable and no longer 300s flat. The
message says which case applies and where to change it.
SlicerTimeoutError is its own type and maps to 504, not 502: the sidecar
answered throughout, we stopped waiting. Connection failures keep
SlicerApiUnavailableError. The preview slice path gets the same treatment.
|
||
|
|
284709f850 |
fix(projects): drop deleted prints from their project, and refresh the view (#2731)
Deleting a print that belonged to a project left it on the project page as a card with a missing thumbnail, and there was no way to remove it. Deleting a print is a soft delete by default (#1343): the files go from disk, the row stays so global Quick Stats keeps counting its filament, time and cost. Every other consumer filters those rows out. The projects module filtered none of them — the only deleted_at check in the whole file was for LibraryFile — so a deleted print kept its project_id and kept being listed, pointing at a thumbnail that no longer existed. The same broken previews appeared on the overview cards, and in the timeline, where the entry links to an archive that no longer opens. Unassigning was impossible because the only UI that can change a print's project lives on the Archives page, which correctly hides deleted prints: visible on the project, unreachable from anywhere. All eight project-scoped archive queries now filter, counts included. That last part is a deliberate divergence from #1343, where the whole point of the soft delete is that the contribution survives: a project is a piece of work with a definite membership, not a lifetime total, so a project that lists eleven prints must not claim twelve. The reasoning is recorded at the constant so nobody later "fixes" it back. remove_archives_from_project keeps working on hidden rows on purpose — it is the repair path for links written before this. The BOM print_name lookups are left alone; naming a since-deleted print is still correct. Two more consumers had the same gap. The CSV/Excel export handed back rows the interface says are gone — filtered at the base query, since the export is the list you are looking at saved to a file. Per-project failure analysis measured a failure rate against prints deleted from the project, and disagreed with the project's own numbers; only the project-scoped branch filters, global analysis still counts every run including orphans as #1390 established. Finally, the project page needed a manual reload to catch up. staleTime is 60s and the delete mutations invalidated only ['archives'], so a project visited within the minute served its cached copy, print still there. The project-assign mutations had the mirror-image bug: ['projects'] refreshed the overview cards but never ['project', id]. Both now go through one shared helper covering every project-derived key, as bare prefixes so all cached project ids are matched. |
||
|
|
11dc612bc4 |
feat(obico): authenticate to a token-protected ML API (#2733)
Obico's ml_api container takes an optional ML_API_TOKEN environment variable.
With it set, ml_api/auth.py answers a bare 401 to any request whose
Authorization header isn't "Bearer <token>"; with it unset it ignores the
header entirely. Bambuddy never sent one, so pointing it at a protected server
meant deleting the token there — which the reporter had set for their Home
Assistant integration and did not want to undo.
Settings -> Failure Detection gains an ML API Token field. When it is empty no
header is sent, so an unconfigured install's request stays byte-identical to
what shipped before the setting existed.
This failed in the worst possible way, and that is the more important half of
the change. Obico decorates /p/ with token_required but leaves /hc/ open. Test
Connection pinged /hc/, so it reported success against a server that was
rejecting every real detection call, the settings looked right, and detection
silently never ran. The only symptom was a generic "ML API call failed" buried
in the status card.
So the test now proves what it claims. After health passes it probes GET /p/
with no img parameter: the auth decorator runs before the handler, so 401 means
the token was rejected and 422 ("Invalid request params") means it was
accepted. No inference work is done either way. A probe that itself errors
reports the token as unknown rather than as working — the UI says it could not
be checked instead of claiming success.
The detection loop checks for 401 before raise_for_status, so a rejected token
is reported as a rejected token, naming the setting and the environment
variable, instead of surfacing "401 Unauthorized" with no hint of what to do.
The message never contains the token; a test pins that.
The setting name carries "token", so the support bundle's keyword redactor
masks it with no new rule. Resolving "field omitted" to the saved token is the
route's job, keeping test_connection a pure outbound call with no database
access.
Second fix, same issue: support bundles misreported which printers Obico
watches. The bundle split obico_enabled_printers on commas and read an empty
value as "no printers". The settings UI writes a JSON array, and empty means
*all* printers — the default — so a working Obico setup showed obico_enabled
false against every printer in its own bundle. That is the reporter's bundle
exactly, and it points anyone reading it at the wrong subsystem. The bundle now
parses the setting the way ObicoDetectionService does, keeps a comma fallback
for any install that stored the legacy shape, and factors in the global switch.
|
||
|
|
db6cdb0745 |
fix(camera): take the finish photo when the print ends, not when its last layer starts (#2547)
The photo fired the moment layer_num reached total_layer_num. That edge is where the printer *starts* its final layer, not where it finishes it: the reporter's H2C capture shows it arriving at 92% with mc_remaining_time=2, three minutes and seventeen seconds and one filament change before the print actually ended, so the frame caught the toolhead mid-print over the model. The trigger also latched _finish_photo_captured, which locked out both the stage-22 and FINISH triggers for the rest of the print — so on firmware that never reports an end-of-print filament unload (H2C and A1 Mini confirmed) nothing could replace the bad frame. Remove the last-layer trigger. The photo is now taken at the FINISH-state trigger, which every model sends and which lands after the toolhead parks. Since Bambu's end G-code drops the plate ~100mm just before that, restore the framing before capturing: absolute G90/G1 Z to max_z_height + 10mm clearance, settle, capture, then drop it back so the print is as reachable as the printer left it. Absolute is the safety argument — that Z is a height the toolhead occupied seconds earlier, so it is inside the travel limits by construction and leaves the nozzle above the part, and it is unambiguous across model families because Z is the nozzle-to-bed gap whether the bed moves or the toolhead does. M211 is never touched (#2579). This is what #1145, #1397 and #1565 asked for. The height is only trusted when two independent sources agree: the archive is matched by the finished print's subtask_name by equality (not LIKE, so "Cube" cannot resolve to "Cube v2"), and its layer count from the 3MF must match the layer count the printer reported over MQTT. Matching on "most recent archive for this printer" was not safe — on_print_complete pops the _active_prints binding concurrently, and a print Bambuddy failed to archive would have resolved to its predecessor. A wrong height is the one failure that could drive the nozzle into the model. The move is additionally skipped when the print height is unknown, when a queue item is pending for the printer, when the printer has left FINISH, and when the new finish_photo_restore_plate setting is off. for every FINISH-state capture — which is what shipped the mid-print photo — the bank is used only when the dispatcher recorded that it injected End G-code into this print, since a SwapMod snippet may have ejected the plate. The flag is handed over in two steps (mark_pending at dispatch, adopt at print start) so it can never outlive its print: a job started from the slicer or SD card adopts False rather than inheriting its predecessor's answer. Those prints also skip the plate move outright, bank or no bank. The bank now refreshes on mc_percent advances as well as layer changes, via a new on_print_progress callback. Layer changes stop the instant the final layer begins, which left the #1867 fallback frame stale by the whole length of that layer; progress keeps ticking there and freezes before the End G-code runs, so a swapped plate still cannot reach the bank. The last-layer throttle exemption is dropped, since it would now fire a grab on every percent tick. On the timelapse path the moment producer returns early, so the consumer does the restore itself before its live-grab fallback — the documented usual outcome on P1-series, where the video has not transferred by the time the notification goes out and the shipped photo was of an already-dropped plate. The two waits are now derived from the settle window and the video poll timeout rather than hardcoded; at the old flat 75s that fallback was guaranteed to be cut off mid-settle. extract_max_z_height_from_3mf reads only a bounded prefix of the plate G-code, since a sliced plate is routinely tens of megabytes and the header is ~40 lines. It returns None for missing, unparseable, zero and negative values so callers must treat "don't know" as such rather than defaulting. |
||
|
|
13c37ffe51 |
fix(vp): scope saved AMS mapping to the printer it was resolved against
Round-2 review fixes for #2700. Blocking: the toggle didn't actually gate the archive write. archive.py's promotion fired for any print_data carrying ams_mapping, but bambu_mqtt's request-topic interception captures ams_mapping unconditionally for every print source (slicer-direct LAN prints included). Since main.py's real-printer auto-archive path forwards the full MQTT payload as print_data, every archive on any install — VP or not — grew extra_data.slicer_ams_mapping. Fixed by replacing the print_data-sniffing with an explicit `slicer_ams_mapping` param on archive_print() that only the VP-queue path (already gated on save_ams_mapping) ever passes. Blocking: a saved mapping could get reused on a printer it was never resolved against — tray IDs only mean something relative to one printer's AMS layout. extra_data.slicer_ams_mapping is now stored as {mapping, printer_id} instead of a bare array: - add_to_queue's fallback only fires when the reprint's target printer_id matches the mapping's origin printer. - The frontend's archiveAmsMapping only surfaces (and the Mapping button only appears) when the print modal's selected printer matches too. - A model-based VP (target_printer_id=None, no MQTT bridge to any real printer) never stamps a mapping in the first place — there's no live AMS layout for the slicer to have resolved tray IDs against. Also from review: - Multi-plate archives now get the Mapping button too (the per-plate FilamentMapping loop was missing archiveAmsMapping entirely). - Added coverage for the previously-untested late-MQTT archive patch path (_restamp_recent_queue_item), including the model-based-VP skip case. - usingArchiveMapping now also resets on printer change, not just plate/archive (it already worked via the printer-scoping above, but is now an explicit dependency too). - The Mapping button's revert (OFF) now undoes only the slots it itself set, not every manual pick in scope — matches the comment above it. - Added a comment on why negative-value slots (external spool) are skipped rather than cleared when applying a saved mapping. |
||
|
|
bab1cfb906 |
feat(vp): per-VP "Save AMS mapping" toggle + reprint auto-apply
Lets a reprint reuse the AMS slot the slicer itself picked, instead of re-deriving one from the file's static type/color. When a Print Queue VP has "Save AMS mapping" on, the slicer's own live-resolved ams_mapping (from the project_file MQTT command) is persisted onto the archive as extra_data.slicer_ams_mapping. A later reprint can reuse it via a new "Mapping" button in the filament-mapping panel — one click snaps every slot to the saved pick, click again reverts to auto-match. Archive cards and queue rows get an "AMS mapping saved" badge so it's visible beforehand. add_to_queue also falls back to the saved mapping automatically when the caller sends no explicit ams_mapping (e.g. a plain reprint with no per-slot edits). The queue item's own ams_mapping (used for that dispatch) is still captured unconditionally whenever the slicer provides it — that part is a correctness fix, not gated behind the toggle. Only the archive persistence for future reprints is opt-in. Split out from the original combined PR per review: this half is genuinely opt-in and low-risk (#2684). The dispatch-time validation gate that keeps a stored mapping honest (#1308) changes behaviour for every existing user and will land as its own PR. Review fixes applied: - _extract_slicer_ams_mapping_json: dropped the unreachable `v is None` arm and rejected bool explicitly (isinstance(v, int) accepts bool). - Translated the Russian docstring text to English. - save_ams_mapping's model comment moved to a trailing comment on the column line, matching the file's convention. - usingArchiveMapping now resets when the plate or archive changes, so the Mapping button can't read ON against a mapping it never applied. - Translated "Click to change slot assignment" and "Re-read". - add_to_queue's fallback is now called out explicitly in code comments and covered by three new integration tests (fallback fires, explicit mapping wins, unrelated extra_data doesn't false-trigger). Closes #2684 |
||
|
|
3daae22f3d | Security hardening (maziggy/bambuddy-security #8) | ||
|
|
ac3e3cc60f |
fix(printers): don't retract a fan kit on a partial airduct frame
device.airduct is pushed field by field - the modeCur handler reads it with an "in" check for that reason - so a frame can carry parts without carrying every fan. Absence in that list is what tells us a kit is not fitted, and taken from a truncated frame it made both accessory badges vanish mid-print and started rejecting fan=aux2 on a printer that has the fan. A parts list now counts as a full inventory only when it carries ids 1 (part cooling) and 2 (aux). Neither is optional on a machine that reports an airduct at all, and both appear in every layout in the support-package archive - P2S base 1,2 / P2S+kit 1,2,3 / X2D 1,2,3,10 / H2C,H2D,H2S 1,2,3,6. Anything narrower is a diff frame: its speeds are applied, presence is left alone. Presence can still be added from a partial frame; only retraction needs the full list, so a kit that really is removed still disappears. Also compose showChamberFan from both model lists rather than branching between them, so the P2S/X2D entries in MODELS_WITH_CHAMBER_FAN stay reachable instead of reading as dead, and note in the fan-speed docstring that the aux2 gate also rejects between connect and the first airduct push. |
||
|
|
9ebfcddbdb | Merge branch 'dev' into feature/p2s-x2d-accessory-fans | ||
|
|
db538e43f1 |
fix(slice): give the slice modal one filament row per project slot (#2712)
The filament list is positional from the modal down to the CLI's filament_N.json parts, but for a source that already carries slice_info the requirements endpoint returns only the slots the plate consumes. A MakerWorld model declaring four filaments and painting with slot 4 alone therefore showed one dropdown, whose PETG pick the CLI bound to slot 1 — slot 4 sliced with the profile baked into the source, and the print came out PLA. The endpoint now takes full_slots, which widens that answer to every project slot with used_in_plate flags, and only the slice modal passes it. Print-time AMS matching shares the endpoint and keeps the used-only list, so it still asks for exactly the spools the job needs. |
||
|
|
83142c726c |
fix(slice): report a finished slice once, not once per queued poll
setInterval does not await an async callback. Slicing a large project blocks the backend for seconds, so poll ticks piled up behind one stalled request, each holding a snapshot taken while the job was still active. They resolved together, and every one of them ran the completion path — one toast and two query invalidations each. A 20s stall against the 1.5s interval produced 13 "Sliced X" toasts from a single slice. Only one poll round is now in flight at a time, which also stops queueing requests against a backend that is already saturated. Completion is recorded once per job id, and a round still awaiting a response when the effect tears down now returns instead of acting. |
||
|
|
beca3a8d73 |
fix(mqtt): keep the layer total that arrives with the print-start frame (#2702)
fix(support): redact push_status values, not the serialised JSON (#2702) |
||
|
|
88dc56d6e1 |
Security hardening (maziggy/bambuddy-security #7)
fix(settings): accept JSON booleans on the Spoolman settings endpoint |
||
|
|
d9da60dd8d |
fix(camera): reuse the live view's frame for external-camera captures (#2707)
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 went out with no image. The reporter measured 0 of 87 layer captures on one print and 0 of 105 on another, both watched throughout. A USB camera allows one V4L2 handle, so a capture during a live view fails outright. The built-in camera has had this rule since #1348 and #1271: reuse the viewer's buffered frame rather than opening a second connection. It was never extended to the external paths, and it could not have been -- the buffer it depends on was only ever populated by the built-in paths. generate_mjpeg_stream yields multipart-wrapped chunks, so the route layer could not recover the JPEG, and a guarded caller would have found an empty buffer and skipped every time. So the stream now publishes each raw frame through a new on_frame callback (parallel to on_process from #2675), and the six one-shot consumers reuse it: layer timelapse, the finish-photo moment and its background fallback, the notification snapshot, Obico polling, and the plate check. A viewer attached with nothing buffered yet skips that one attempt rather than competing -- kicking the viewer off is worse than missing a frame. on_frame exceptions are logged and swallowed, like iter_subscriber's on_unsubscribe: buffering is a side effect and must never be able to take the live stream down with it. The external stream's teardown now releases the buffered frame too, ownership-checked so a concurrent viewer of the same printer keeps its own. Two side effects on paths not touched here, both improvements: the snapshot endpoint and the finish-photo fallback chain consult get_buffered_frame and can now serve an external camera's live frame. plate_detection's docstring already claimed this behaviour while implementing it only for the built-in fallback; that drift is resolved. |
||
|
|
40e7b60e8c |
fix(camera): drain a streaming ffmpeg's stderr continuously (#2707)
ffmpeg is spawned with stderr=PIPE and it was only read on the error paths, so for the life of a working stream nobody read that pipe. ffmpeg writes its banner, the input analysis, then a progress line at a steady rate; a 64 KiB pipe fills eventually, ffmpeg blocks writing to it, frames stop, and the stream's own 30s timeout fires -- logged as "RTSP read timeout" with no hint that we starved it ourselves. How long that takes is unmeasured and evidently long: one H2D upstream ran 21m36s without stalling, and an earlier 512 B/s extrapolation of mine was mostly the one-off startup banner. So this is a bounded resource being treated as unbounded, not a fault anyone has reported. _FfmpegStderrTail drains the pipe continuously and keeps a 16 KiB rolling tail. That tail is what the error paths now report, which is better material than before: it holds what ffmpeg said as things went wrong, where the on-demand read returned whatever was printed first -- usually the banner, which the summariser strips anyway. Three readers wanted this one pipe, and asyncio rejects concurrent reads on a StreamReader, so the collector is authoritative: it registers by pid, _read_ffmpeg_stderr returns its tail when present and otherwise reads the pipe unchanged, and _terminate_ffmpeg skips its own stderr drain when the collector owns it (the collector keeps draining through teardown, which is all wait() needs). The generator starts it after the immediate-failure check, which reads the pipe directly because the process is already dead, and releases it after _terminate_ffmpeg. text() goes through _summarize_ffmpeg_stderr like every other stderr log here, so the access code ffmpeg echoes in its input URL stays masked. aclose() awaits the cancelled pump rather than firing and forgetting, so no pending task survives into loop teardown. |
||
|
|
f26bcbbcce |
fix(camera): one registry key per stream, not per printer (issue #2707)
Closing a camera view and reopening it immediately could leave the new stream unregistered while it was running and delivering frames. The damage was all indirect: is_stream_active() reported no viewer, so Obico polling and snapshots opened a second camera connection against the live view (the thing #1348 and #1271 exist to prevent); the janitor's /proc scan found an ffmpeg missing from _active_streams and killed the live stream as an orphan; and /camera/stop reported "Stopped 0" with a stream running. The fan-out stream id was f"{printer_id}-fanout" -- constant per printer, so every successive stream shared one registry key, and the departing generator's finally popped whatever was under it, including its successor's entry. The same finally also cleared the per-printer frame buffer unconditionally, discarding the new stream's frame. It needed the two streams to overlap, which the 4s teardown made easy. Each stream now gets its own key via _new_fanout_stream_id(), so a generator can only clean up after itself -- the external-camera path already does this (#2675) and this brings the fan-out path in line. The per-printer dicts are released through _release_printer_frame_state(), which checks that no other stream for the printer is still running; both the RTSP and chamber-image cleanups had the same unconditional pop. Also hoisted time and uuid to module level and dropped four function-local `import time` statements. A local import shadows the name for the whole function, so any use on a branch that doesn't reach the import raises UnboundLocalError -- a real hazard in camera_stream, whose external-camera branch imported both while the RTSP path needs them too. A test pins camera_stream as free of function-local imports. |
||
|
|
18cc906fad |
fix(camera): drain ffmpeg's pipes during teardown (#NNNN)
Closing a camera view logged "ffmpeg didn't terminate gracefully, killing" followed by "ffmpeg did not exit within 2.0s of SIGKILL; abandoning wait", on every single close. Both waits expired every time, so teardown took a fixed 4.00s -- and since the firmware allows one camera connection, that was 4s in which nothing else could use it. ffmpeg is spawned with stdout and stderr as pipes and the teardown paths have stopped reading them, so it sits blocked in write() on a full 64 KiB pipe. SIGTERM cannot be acted on there: the handler only sets a flag that the main loop polls, and the loop never gets back to the check. SIGKILL does kill it, but asyncio resolves Process.wait()'s waiter through _try_finish(), which requires every pipe transport to report disconnected; paused, unread pipes never reach EOF, so wait() blocks with returncode already set. A negative-control test shows returncode=-9 at the instant the abandon fires. Draining both pipes while stopping the process fixes both halves: 4.00s becomes ~0.15s. The signal ladder and its bounds stay as backstops, so a genuinely wedged process still cannot hang a stream, a Stop request or the janitor. This corrects _FFMPEG_KILL_TIMEOUT's premise and #2580's conclusion. That 12-hour hang was the unbounded form of this same self-inflicted stall, not an ffmpeg stuck in uninterruptible I/O -- the process observed doing it was in state S, which cannot survive a delivered SIGKILL. Bounding the wait capped the symptom without removing the cause. |
||
|
|
b938b83136 |
fix(tests): add the new fan fields to the plate-clear status fixture
mqtt_relay reads state.left_aux_fan_speed, but the SimpleNamespace fixture in test_plate_clear_mqtt_notification enumerates its fields explicitly, so the two status-payload tests raised AttributeError. I updated the equivalent fixture in test_printer_manager_status_broadcast and missed this one — running only the touched suites is what hid it. Also addresses the round-2 review notes: - exhaust_fan_present: documented that the H2 series reports part 3 too, so the flag is not model-specific despite the name. - Mask the part id after shifting, matching get_flag_bits(id, 4, 8), for consistency with the state decode. No behaviour change for any observed id. - Noted the unmapped H2 id 6 beside the id branches. - Reject fan=aux2 when the printer reports no left_aux_fan_speed, so a POST against an A1 no longer sends M106 P10 for absent hardware. The UI already hid the badge; this closes the same hole on the API. - Added a test asserting EXHAUST_FAN_LABEL_MODELS and the frontend's MODELS_WITH_EXHAUST_LABEL cannot drift apart. |
||
|
|
f2babc9027 | Merge branch 'dev' into feature/p2s-x2d-accessory-fans | ||
|
|
e325948dcc |
Security hardening (maziggy/bambuddy-security #N)
Subprocess output and user-supplied URLs are scrubbed of credentials before they reach the application log. Adds a shared redaction helper in core/logging_filters and routes the existing support-bundle sanitizer through the same pattern. |
||
|
|
5a67dffe4f |
fix(timelapse): poll longer, diff without a clock, delete once archived (#2704)
Timelapse was on, the video never reached the archive, and Scan for Timelapse found nothing afterwards. Across 247 support bundles this was the norm, not an edge case: 457 automatic scans scheduled, 262 attached. The scan looked four times over ~65s. The attempt that found the video was #1 272 times, then 17 / 13 / 13 — flat against the cutoff, not decaying, i.e. files were still arriving when we stopped. What ran afterwards searched for the print name inside the filename; Bambu only writes "video_<timestamp>", so it fired 159 times and matched zero. The manual Scan had no baseline at all and matched on filename timestamp, FTP mtime, or "there is only one video" — all reading a clock a LAN-only printer cannot sync. The reporter's P1S was six and a half days out. - Poll for minutes instead of ~65s; drop the name-match fallback. - Persist the print-start baseline on the archive, so the diff survives a restart mid-print and the manual Scan runs the same comparison. With a baseline present the clock-based strategies are skipped entirely — they can only turn an honest "pick one" into a confident wrong answer. - When several files are new (a previous print's video landing late), exclude the ones already attached to another archive instead of ordering the candidates. Ordering could only be done on the printer's clock. - Delete the video from the printer once archived. Keeps /timelapse to unclaimed files, which is what makes the diff unambiguous, and stops P1S cards filling with AVIs. - Gate that delete on a verified transfer: download_file now compares against the size from the listing. An FTPS connection closing early does not always raise, so a partial buffer was being attached as a complete video — which would also have been the one case where deleting the source lost data. Bounded twice on purpose: wall-clock deadline plus a derived round cap, since the deadline stops bounding the loop as soon as the sleeps are shortened. Per-round logging only speaks when the listing changed — 31 rounds of full listings would bury the interesting line in the support bundle. Migration adds print_archives.timelapse_baseline as JSON, spelled the same on both dialects so a migrated database matches a fresh one. ----------- fix(finish-photo): add the timelapse frame to the archive after the notification (#2704) When a print records a timelapse, its last frame is the better finish photo: the firmware stops recording with the toolhead parked and before the end G-code drops the bed, where a live grab at that moment catches a lowered plate. Bambuddy waited 60s for the video and then gave up, because the print-complete notification blocks on that photo and holding a notification for minutes is worse than sending it with the live grab. P1-series printers write MJPEG AVI rather than H.264 MP4 and serve it slowly. Measured over 261 attaches in the support bundles: P1S median 33s, p90 167s, worst 546s, while every other model finished inside 26s. So the printers that most needed the better framing were the ones that never got it. Keep the notification on the same bound, and keep waiting off to the side. _capture_finish_photo_from_timelapse now reports whether it ran out of time or concluded — a video that landed and failed extraction is not worth retrying, one that never arrived is. On the first, schedule a background task that waits up to 15 minutes and inserts the extracted frame at the front of the archive's photo list, where the gallery opens. The live grab stays on disk: the notification already links to that exact file, so removing it would leave a broken image in Discord or Telegram. The length check proves we received what the listing said, not that the file was finished. The first look happens ~5s after the print ends, while the printer may still be writing, so a growing file can be listed short, served short, and pass. Re-list after the download and only accept the video once its size has stopped changing — a failed re-list counts as not settled, since "could not check" must not mean "safe to delete". |
||
|
|
84a7b797cd |
fix(printers): decode airduct part state from its low 8 bits
Review feedback on #2691: `state` is bit-packed like its sibling `range` (end << 16 | start), and Bambu Studio decodes it with get_flag_bits(state, 0, 8). Masking with & 0xFF before clamping means a packed value decodes to the real percentage instead of clamping to 100. Also moves the uses_exhaust_fan_label import to the top of printers.py with the other imports. Tests: packed value (60 << 16 | 45) decodes to 45, and plain 0-100 values round-trip unchanged. |
||
|
|
15ec0bf1c5 |
fix(printers): use the model-appropriate name in the fan-speed response
The fan-speed endpoint always reported 'Chamber fan set to N%', so on P2S/X2D — where the printer card labels that fan 'Exhaust' — clicking Exhaust produced a toast saying Chamber fan. Adds uses_exhaust_fan_label() to printer_models so the badge label and the API response share one source of truth, and uses it to pick 'Exhaust fan' vs 'Chamber fan' in the response message. Tests: helper coverage for P2S/X2D (incl. internal codes N7/N6), other enclosed models, and unknown/missing model; API test asserting the message matches the badge label per model. |
||
|
|
9ee162d51a |
feat(printers): expose P2S/X2D accessory fans (left aux + exhaust)
The P2S/X2D have two fans bambuddy didn't fully handle. On the P2S both are add-on kits; on the X2D they ship from the factory. 1. Left auxiliary part cooling fan — not shown or controllable. It is reported ONLY as device.airduct part id 10 (raw id 160 >> 4; FAN_REMOTE_COOLING_1 in Bambu Studio's DevFan::ParseV3_0) and is never mirrored into a flat big_fanX_speed field, which is why it was invisible. This is the gap identified in #2576, where the single 'Auxiliary' fan (big_fan1 / M106 P2) only reaches the right-hand aux fan. 2. Chamber exhaust fan — shown on every P2S labelled 'Chamber Fan'. On P2S/X2D Bambu's firmware/UI (and Bambu Studio's FAN_CHAMBER_0_IDX) call it 'Exhaust', and it is a kit on the P2S rather than built in. Both are now detected from device.airduct.parts, which lists only the fans that physically exist, so each tile appears only when the hardware is present. - bambu_mqtt: parse airduct part 10 -> left_aux_fan_speed (None when absent) and part 3 presence -> exhaust_fan_present; set_fan_speed() accepts index 10 plus a set_left_aux_fan() helper - schema / status route / printer_manager broadcast / mqtt_relay expose both fields - POST /printers/{id}/fan-speed accepts fan=aux2 -> M106 P10, the command Bambu's official P2S machine profiles use - frontend: 'Left Auxiliary Fan' tile shown when reported; big_fan2 tile labelled 'Exhaust' and presence-gated on P2S/X2D, unchanged 'Chamber Fan' elsewhere - i18n: leftAuxiliary + exhaust for all 12 locales Verified fan -> field map on a live P2S (fw 01.02.00.00), stable across cooling and heating airduct modes: Part cooling -> cooling_fan_speed / airduct id 1 (built in) Aux -> big_fan1_speed / airduct id 2 (built in) Exhaust -> big_fan2_speed / airduct id 3 (kit) Left aux -> airduct id 10 only (kit; forced off in heating by mode config) Tests: airduct id-10 parsing (raw 160 -> id 10, not literal 160), id-3 presence, base-P2S absence, diff-push survival, clamping, malformed entries, M106 P10 emission, invalid-index rejection; fan-speed API aux2->10 mapping; frontend tile presence and labelling per model/kit. |
||
|
|
8551e32f14 |
feat(slicer): keep the designer's print settings when re-slicing for another printer (#2622)
Published models often deviate from the stock Bambu profile on purpose - five walls, 100% infill, a 0.1mm first layer. Re-slicing one for a different printer discarded all of it: the picked process preset overrides the file's embedded settings, and that override is precisely what makes cross-printer re-slicing work, so it cannot just be dropped. "Slice as designed" (#2611) does not help - it is all-or-nothing and only offered when the picked printer already matches the design's target. The deviation list does not have to be computed. Bambu Studio writes it into the 3MF as different_settings_to_system, laid out as [process, *filaments, printer] - verified against real files at 2, 3 and 4 filament slots. The parser refuses any file whose array length contradicts its own filament count rather than guessing an index, since reading the printer slot as the process slot would carry the designer's machine_start_gcode onto a foreign printer. The slice dialog now lists exactly which print settings the author changed and what each was set to, with a checkbox per setting. Design intent - wall count, infill, layer and first-layer height, supports, seam, brim, ironing - is ticked by default. Printer-specific values - speeds, accelerations, jerk, fans, temperatures, prime-tower geometry - are listed with a badge but start unticked: tuned for the author's machine, they can be merely wrong on the target or outside the range its profile accepts, which fails the slice outright. Only ticked keys are sent, and only keys the source actually flags as changed are applied. Values are written into the outgoing process JSON, the same mechanism the support carry-over has used since #1881: for a Standard preset pick that JSON is an inherits stub, so the patch is the child in the chain and wins over the flattened parent. Process slot only - filament picks are honoured as chosen. The wiki's "this is not a settings merge" note under Slice as designed described the gap this closes; rewritten to point at the new panel. Translated in all locales; wiki updated. Covered by backend and frontend tests. |
||
|
|
8fd1f884dc |
feat(mqtt): publish the plate-clear gate and add a notification for it (#2525)
When a print reaches a terminal state Bambuddy holds the queue until
someone confirms the build plate is clear. That gate was visible only in
the Web UI: the printer's own MQTT push reports nothing beyond RUNNING,
PAUSE, FAILED, FINISH and IDLE, so an external automation could not tell
"finished" from "finished and still waiting for a human".
The per-printer status topic now carries an awaiting_plate_clear field,
and every transition is additionally published on a new retained topic,
bambuddy/printers/{serial}/plate_clear. Retained, and published from the
flag itself rather than from printer telemetry: a subscriber learns the
state of every printer the moment it connects, and the state stays
correct after Auto Off powers a printer down - telemetry stops there,
which would otherwise leave the status topic frozen at false.
Publishing is edge-triggered. The queue clears the gate on every
dispatch whether or not it was up, and no subscriber should see a
"plate cleared" for a plate that was never dirty. Persistence and the
WebSocket broadcast stay unconditional; they are idempotent and predate
this.
A matching Plate Clear Required notification event was added, off by
default on every provider because it fires after every print at the
same moment as the print-complete alert. Only the rising edge notifies.
Acknowledging still goes through POST /printers/{id}/clear-plate.
Two tests in test_printer_manager_status_broadcast.py asserted
_schedule_async.call_count == 2 for the setter. The new emission makes
it three on a transition, so they now assert that the persist and
broadcast coroutines are actually scheduled - which is the contract
Translated in all locales; wiki updated. Covered by backend and
frontend tests.
|
||
|
|
af7874546a |
feat(projects): per-file print progress and complete-sets tracking (#1897)
Projects made of many distinct files that each need N prints (e.g. 13
plates x 10 sets = 130 prints) only had aggregate progress. Finding out
"how many times have I printed plate_7?" meant reading the Activity
Timeline line by line, unusable at 130 events.
Projects now take an optional Copies per File target. Every printable
file in the project's linked folders shows an X / N badge with a mini
progress bar (gray not started, amber in progress, green done), and the
progress card gains a Complete Sets bar - the minimum per-file count,
i.e. how many finished assemblies can be shipped right now. Without the
target, printable files show a plain printed-count badge.
Counting matches the aggregate project stats: completed runs only,
served by a new /projects/{id}/file-progress endpoint. Runs attribute
to a file via a new library_file_id stamp on queue-dispatched archives,
falling back to content hash and then filename for historical rows.
Also fixed: files queued from a project-linked File Manager folder now
inherit that project, so their prints count toward project statistics -
previously only prints started from the project page were attributed.
Test-harness fix along the way: the test suite's get_db override never
committed, unlike production get_db, so endpoints relying on the
request-scoped commit silently lost their writes in tests. The override
now mirrors production commit/rollback semantics.
|
||
|
|
1fb6978ee1 |
feat(library): let users delete empty folders (#1781)
Library folders have no ownership tracking, so folder deletion was gated entirely behind library:delete_all - a user with library:delete_own could create folders and delete their own files, but the emptied folder sat there until an admin removed it. Users with library:delete_own can now delete folders that are truly empty: no subfolders and no files, including trashed ones - folder deletion cascades, so removing a folder that holds another user's trashed file would silently break trash restore. External folders (operator-configured mounts) and folders linked to a project or archive still require library:delete_all even when empty, since deleting them affects more than the folder itself. The bulk-delete endpoint applies the same rule instead of skipping all folders for non-admin users. The folder tree's Delete entry enables accordingly and shows a "You can only delete empty folders" hint on non-empty folders. The backend stays authoritative - a folder that only contains trashed files is invisible in the tree but still refuses deletion. |
||
|
|
eae5359fbc |
feat(printers): show AI failure detection state on printer cards (#1546)
The live Obico classification was only visible under Settings -> Failure Detection, so tracking how detection matched an ongoing print meant flipping between the Printers screen and Settings. Each printer card's badge row now shows an AI badge whenever detection is enabled for that printer, like the other health badges: gray Idle while no print is being watched, then green Safe, amber Warning, or red Failure while a print is actively monitored. The tooltip carries the current smoothed score; clicking jumps to the full detection status and history in Settings. Printers excluded from the monitored subset show no badge. Served by a new lightweight /obico/printer-status endpoint readable with printer permissions alone - it exposes only the enabled flag, the monitored-printer set, and per-printer classification, keeping ML URL and other configuration behind the existing settings-gated endpoint. |
||
|
|
d68724c689 |
feat(stats): energy usage in cost records and trends (#1432)
The Most Expensive record on the Statistics page ranked prints by filament cost alone, ignoring the per-print energy cost Bambuddy already measures via an attached smart plug. It now ranks by filament + measured energy cost; prints without smart-plug data compete on filament cost alone, as before. Filament Trends gains an Energy Over Time chart: kWh per day (per hour for ranges of a week or less, per week for long ranges), with the range's total kWh and energy cost in the header. The chart only renders when the selected range contains measured energy data, so setups without smart plugs see no change. The /archives/slim stats feed now carries each run's energy_kwh / energy_cost from print_log_entries. Translated in all locales. Covered by backend and frontend tests. |
||
|
|
7c83316797 |
fix(camera): reap leaked ffmpeg for external USB/RTSP streams (#2675)
Closing an external USB (V4L2) camera view abruptly could leave its ffmpeg running and holding /dev/videoN open -- LED stuck on, and reopening the view failed or took 10-30s fighting for exclusive device access. Same class of leak as #776 (built-in RTSP path), but the external path was never wired into that fix: external streams registered into none of the _active_streams / _disconnect_events / spawned-PID registries, so /camera/stop returned {"stopped": 0} for a live USB stream and the orphan janitor's /proc net matched only rtsp(s)://bblp: cmdlines. Cleanup ran only via the stream generator's own finally, which an abrupt disconnect can skip. - Thread an on_process callback + stop_event through generate_mjpeg_stream into _stream_usb / _stream_rtsp; register the process before the startup probe so a process that hangs on a locked device (not just one that exits) is reapable. - Register external streams into the shared registries under a unique {printer_id}-ext-{token} id so /camera/stop and cleanup_orphaned_streams find and kill them; stop_event prevents the reconnect loops from respawning. - Extend the /proc safety-net scan to also match USB (-f v4l2) ffmpeg, excluding still-active streams and unrelated ffmpeg. |
||
|
|
1bdd7d224a |
fix(library): sort File Manager by real filesystem mtime, recursively (#2680)
The folder tree's "sort by recent activity" and the file pane's date sort put external (mapped/NAS) files in a near-random order instead of ls -t's newest-first. Nothing captured the files' on-disk mtime: the sort keyed off the DB updated_at/created_at, which for a bulk external scan is the same scan instant for every row, so a whole block tied and sorted arbitrarily; only rows Bambuddy had later touched individually looked "partially right." The tree also bubbled up only immediate child-file activity, so a file added deep in a subtree never lifted its parent folders. - Add nullable fs_modified_at to LibraryFile and LibraryFolder (dialect- branched migration, mirroring the #2615 dispatching_at pattern). - External scan records each file's and directory's real os.stat().st_mtime and refreshes it on every re-scan, so a file edited over the mount re-sorts and existing installs backfill on the next scan. - list_folders computes each folder's activity as a recursive newest- descendant roll-up (post-order), so a fresh deep file lifts every ancestor. - Folder tree sort and the file pane's date sort now use the real mtime, falling back to created_at for managed uploads with none. - New toolbar toggle shows/hides each item's last-modified date in the right pane (grid + list), with strings in all locales. Store the mtime as naive UTC to match the other timestamp columns so activity comparisons never mix naive and aware values on either dialect. Covered by integration tests (mtime capture, re-scan refresh, deep-file recursive bubble, folder mtime) and a frontend test proving fs_modified_at is preferred over created_at. |
||
|
|
83ac5b361c | Security fix (security-issue #6) | ||
|
|
41ad1d65c7 |
feat(skip-objects): select items directly on the build plate
Pairs the top-down plate preview with the slicer's per-object pick mask (Metadata/pick_N.png), whose pixel colours encode the same identify_id the firmware's skip command takes, so a click resolves to a real object rather than an inferred bounding box. Several objects can be selected before one confirmation; selected and already-skipped items are highlighted on the plate; the checklist stays available when no mask exists. view=pick serves only the active plate's mask and 404s otherwise, unlike every other view. A render returned in a mask's place would be decoded as object IDs — dark pixels yield small integers that collide with real ones — and a click would then skip an arbitrary object, mid-print, irreversibly. The 404 is what tells the UI to fall back to the checklist. Click mapping goes through the contained rect, since the canvas paints at mask resolution under object-contain; clicks on a letterbox bar are rejected rather than clamped onto whichever object touches the border. Confirming names the object when one is selected and counts them when several are, which is what plates of identically-named clones need. No printer-control command path was added or changed; the layer, permission and existing skip-command guards are untouched. |
||
|
|
fdd6ec416f |
fix(slicer): classify filament profiles by their real printer scope, not just their name (#2628 follow-up)
Slicing for a P2S failed with "filament preset Bambu PLA Basic @BBL X1C 0.2 nozzle (slot 1) is not compatible with printer Bambu Lab P2S 0.4 nozzle" — naming a profile shown nowhere in the dialog. The picked profile was "Overture PLA Matte @0.2", whose inheritance chain roots in that X1C profile. The dialog classifies a profile by its compatible_printers list and falls back to reading the printer out of its name. That name carries no model, and the list — present on the imported copy — is not shipped by every source: Bambu Cloud omits it deliberately (rate limits), and Orca Cloud shipped it but Bambuddy only mined filament type and colour from the same content. Orca Cloud entries now carry their own compatible_printers, and the existing same-name enrichment bridge carries the list onto entries that lack one, in both directions between the cloud tiers. A bare "@<size>" name tag is read as a nozzle size as a last resort: it can rule a printer out but never rules one in, and implausible values are ignored rather than guessed at. |
||
|
|
56accd24de |
fix(smart-plugs): don't blank printer state when an accessory plug switches off (#2629)
An end-of-print auto-off on a plug that powers a filter fan marked the linked printer offline and forced its state to "unknown". The mark was unrecoverable: connected heals on the next MQTT message but state does not (only frames carrying gcode_state rewrite it, and steady-state push_status frames are partial), so the printer stayed "unknown" until a manual Force Refresh and the queue never dispatched to it again. The offline mark is now an explicit presumption: mark_power_off records the state it overwrites and _on_message undoes it as soon as the printer sends another report on its own topic, since inbound traffic proves the power was never cut. A reconnect discards the saved state, so a genuine power cut is unaffected. Each plug also gains a controls_printer_power flag (default true, backfilled) that gates all five power-off paths, and the queue's power-on step now picks the flagged plug instead of whichever linked plug came first. |
||
|
|
fb11adc8fb |
fix(a2l): normalise AMS Lite unit 16->6 so slots load and deduct (#a2l-am-unit-16)
The A2L reports its 4-slot AMS Lite as unit id 16, but its slot-presence
bitmasks sit at bit base 24 (id 6) and it reports tray_now as a local 0-3
slot. Fed the raw id 16, the ams_id*4+slot convention probed bits 64-67
(always zero) and marked loaded slots empty; the local tray_now was read as
global, so usage deducted from the wrong spool (or not at all); and the
ams_id<=7 DB constraint rejected id-16 Spoolman links.
Normalise the Lite 16->6 at the MQTT ingest boundary so global tray ids land
at 24-27 - matching the firmware's own bit base, working with every existing
ams_id*4+slot consumer, colliding with nothing, and passing the DB
constraint. Globalise tray_now to 24+slot, widen the valid-tray guards, label
the unit "AMS Lite", and build the confirmed ams_mapping2 {ams_id:16,
slot_id:0-3} / flat 0-3 for dispatch. Outbound slot commands translate 6->16
on the wire via a single helper. Self-scoping: only unit id 16 is touched, so
all other printers/AMS types are unaffected. One uncaptured wire field (the
physical global tray on load/cali) is extrapolated and isolated to the helper.
|
||
|
|
2e74f2ad41 |
feat(ams): confirm spool assignments landed instead of fire-and-forget (#2582)
Assigning a spool to an AMS tray pushed ams_filament_setting + extrusion_cali_sel and reported success immediately, whether or not the tray accepted it. A silently-dropped assignment never surfaced, and since a print only deducts from the spool on the exact tray it pulls from, it also recorded no filament usage - which made the whole thing feel random. Read the AMS telemetry back after every assign (inventory assign_spool and the Configure Slot modal) and toast the outcome: loaded when the tray echoes the pushed tray_info_idx, a warning when the filament loaded but the K-profile (cali_idx) did not, or not-confirmed after ~30s. Verification uses the periodic per-tray push (the command ack hardcodes sequence_id 0 and can't be correlated); an on-demand pushall is nudged so it lands quickly. Covers regular AMS, AMS-HT and external slots; stays silent rather than inventing a failure if the printer goes quiet. The read-back check runs on every AMS push because the change-hash excludes tray_info_idx. |
||
|
|
2e45893dd5 |
feat(print-options): add "Auto" state to bed levelling, flow & nozzle-offset calibration
Bed levelling, flow calibration, and nozzle-offset calibration were on/off only, so the sole way to run bed levelling was to force a full level before every print. Bambu Studio has always offered a third "Auto" state that lets the printer skip the calibration when it was done recently -- the state most users actually want. Make these three options tri-state (off/on/auto), defaulting to auto, and leave vibration/layer-inspect/timelapse as on/off (Bambu Studio exposes no auto for those). Wire encoding follows Bambu Studio's source exactly: each option sends a JSON bool (true only for "on") plus a companion int -- off=0, on=1, auto=2. The bool fields stay booleans (the #1478 H2S regression); only the companion int widened from {0,1} to {0,1,2}. #1721's observation that stage 8/39 stays queued when sending 2 is the auto contract (queued, skipped at runtime if recent), not a broken "off". - schemas: TriState = Literal[off/on/auto] with a BeforeValidator coercing legacy bool / 0-1 / true-false so old clients and un-migrated rows validate - model + migration: boolean columns -> String; SQLite via column affinity + data backfill, PostgreSQL via ALTER COLUMN TYPE guarded on information_schema (verified on both dialects); settings rows normalised true/false -> on/off - MQTT: start_print takes the tri-state strings and emits the paired bool+int - Virtual Printer: reconstructs the slicer's auto/on/off from the int companion (auto_bed_leveling / extrude_cali_flag) in both capture paths - frontend: CalibrationMode type; off/auto/on segmented controls in the print dialog, queue bulk-edit, and Settings -> Workflow; calibrationMode_* strings in all 11 locales |
||
|
|
258db95483 |
fix(overlay): authenticate the OBS overlay with a token when login is enabled (#2613)
The /overlay/{id} route renders without a login, but everything it draws is
auth-gated: printer status and name (PRINTERS_READ), one setting (SETTINGS_READ),
and the camera stream (a camera-stream token). A signed-in browser rides its JWT
from local storage; OBS is a fresh browser with no session, so the overlay stayed
blank whenever authentication was enabled. Cloudflare/remote access was never the
cause -- an incognito window fails identically.
Give the overlay a self-contained kiosk-token mode, mirroring the Cam Wall:
- New `overlay` long-lived-token scope, kept separate from `camwall`: the overlay
names the printed file on screen, which a Cam Wall token is trusted never to
expose, so folding it in would silently widen every existing wall token.
- New token-authed GET /printers/{id}/overlay-status returning exactly the fields
the overlay draws and nothing else; added to the auth-middleware allowlist so it
reaches its own RequireOverlayTokenIfAuthEnabled gate.
- StreamOverlayPage reads ?token= and, in that mode, authenticates its status and
camera calls with the token and skips the WebSocket (the 2s poll is the feed).
The logged-in path is unchanged.
- Token-mint UI (Settings > API Keys) offers the scope with a ready-made
/overlay/{id}?token= URL copied once on creation.
|
||
|
|
64f9d04c80 |
fix(queue): claim a queue item before dispatch so it can't be reassigned mid-upload (#2615)
A queue row stays status='pending' for the whole FTP upload; status only flips to 'printing' at the end. The edit routes only blocked non-pending rows, so a PATCH during the upload window was accepted while the in-flight dispatch kept using its snapshotted printer -- splitting the queue row from the archive / expected-print / physical command across two printers, and enabling a duplicate dispatch on restart. The #1853 CAS guards cancellation, not reassignment. Add a dispatching_at claim, stamped atomically (WHERE status='pending' AND dispatching_at IS NULL) before any slow I/O and cleared on every exit. While held, the single-item PATCH returns 409 (re-checked just before the write), bulk edits skip the row, and the scheduler won't re-select it. Startup reconciliation clears claims orphaned by a crash mid-dispatch. The row stays pending throughout, so no status/UI/completion/reconciliation path changes. New column print_queue.dispatching_at (nullable, dialect-safe DDL). Covered by scheduler tests (claim exclusivity, non-pending rejection, release-on-exit, skip-already-claimed, startup stale-clear) and API tests (reassign 409, printer_id unchanged, bulk skip, unclaimed row still edits). |
||
|
|
c469aa3407 |
feat(slicer): add "slice as designed" mode honouring a 3MF's embedded settings (#2611)
Server-side slicing always applied the picked printer/process/filament triplet via --load-settings, which overrides the designer's embedded project_settings.config — so a MakerWorld model set up for 5 walls came out at the picked profile's default 2. That override is correct for re-slicing a design onto your own printer/AMS, but there was no way to slice a file the way its author configured it. SliceModal now offers a "Use the file's built-in settings" checkbox when the source 3MF carries embedded settings AND the picked printer matches the design's target model. It routes to the existing embedded-settings slice path (previously only a crash fallback), so walls/infill/filament come from the file. Ticking it locks all four preset dropdowns — printer included, since it's unused on this path and changing it would drop the match and hide the toggle. The printer-match gate stops embedded settings being honoured across models (wrong bed); there is no cross-printer re-targeting on this path. - schema: use_embedded_settings on SliceRequest - route: embedded_mode branch; crash-fallback guarded against re-running - frontend: gated checkbox locking all four dropdowns, resets on mismatch - 2 i18n keys across all 11 locales - tests: backend (flag skips triplet / ignored for STL) + frontend (toggle offered on match, locks dropdowns + sends flag / hidden on mismatch) |
||
|
|
83a7b75b14 |
fix(queue): persist selected plate to the archive; reconcile archive on offline stop (#2603)
A print queued from a specific plate of a multi-plate 3MF showed as Plate 1 in Print History after cancellation: the archive derives its plate from the filename, but a whole multi-plate 3MF uploads under one name with no plate suffix, so the parser defaulted to plate 1 and nothing copied the queue item's plate_id onto the archive (which had no plate field). Add a nullable print_archives.plate_id, copy it from the queue item at dispatch (archive- and library-file paths), expose it in the archive API, and render it in Print History. A startup backfill copies the plate onto existing archives from their linked queue rows. Column add + backfill are identical on SQLite and Postgres. Also fix a related lifecycle bug: stopping a printing item while the printer was offline left the linked archive stuck at "printing" (queue row cancelled, but no MQTT completion ever arrives to reconcile the archive). The offline-stop path now closes the archive out directly; the online path still defers to the MQTT completion event. |
||
|
|
a8fc453d3d |
fix(ams): derive setting_id when configuring a built-in filament on a slot (#2604)
The Configure AMS Slot modal sends built-in / local / Orca-generic presets with a GF* tray_info_idx but an empty setting_id, and configure_ams_slot forwarded that empty value to ams_filament_setting. The firmware treats a filament-id-without-setting-id slot as half configured: it shows the new material briefly, then reverts to its previously stored profile. Back-fill setting_id from the resolved tray_info_idx via filament_id_to_setting_id when the client sent none (e.g. GFB99 -> GFSB99), mirroring the derivation the inventory/assignment path already does. Doing it server-side also protects API callers and future frontends. P* user presets and already-GFS* values are left unchanged, and an explicit setting_id still passes through untouched. |
||
|
|
80687982c1 |
fix(db): release scheduler/cloud/cover sessions across slow I/O, add LIFO pool (#2572)
Three more idle-in-transaction / thundering-herd paths from farm testing: - print_scheduler: _start_print commits before the FTP delete/upload and _preheat_and_soak commits before the heat-soak wait, so the per-item session no longer sits idle-in-transaction across preheat + upload. - cloud/filament-info: rollback the request transaction after the token read and before the sequential Bambu Cloud calls; single-flight concurrent misses for the same setting_id through one shared call. - printers/cover: coalesce identical in-flight cover requests so followers serve from the cache the leader fills instead of duplicating the multi-path FTP + 3MF extraction. Also adds pool_use_lifo (PostgreSQL default on, DB_POOL_USE_LIFO override, shown in /system/db-pool) so a bursty farm keeps a small hot connection set. |