mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
cfecfa360ec444e73c66e5e23dbf7ee209f3aeca
3780
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cfecfa360e |
Restore the skip-objects list after a restart mid-print
The object list lives in PrinterState and is filled by the print-start path, which bambu_mqtt suppresses on the first RUNNING push after startup so a running print is not archived twice (#1304). Everything else that moment restores came back - the archive into _active_prints, the filament attribution session, the timelapse baseline - and the object list did not. So the card saw zero objects and greyed out its Skip button for the rest of the print. Measured on the maintainer's H2C: 8 objects loaded at 09:02, a restart at 09:17, Skip dead for the remaining hour. Nothing could bring it back either. GET /print/objects rebuilds the list whenever it is empty, but its only caller is the modal that the greyed-out button opens. on_print_running_observed now reloads the objects from the archive of the print that is still running, anchored on subtask_id - the firmware mints one per print, so a leftover status="printing" row from a completion that was never seen cannot lend its objects to another job. Without an id nothing is loaded rather than guessed; the endpoint's own reload covers that on demand. That endpoint now reads the archived 3MF from disk before it asks the printer. The archive of a running print normally holds the very file the printer is executing, so the fan-out was fetching back 15 MB Bambuddy already had, over the printer's single FTP socket, while it was printing - and on a printer that kept the file on internal storage it cannot succeed at all. FTP stays as the fallback. skipped_objects is left alone: a reload is not a new print, and what the user has already skipped only lives there. The plate image had the same fault one layer down. Opening the modal asks for the cover, the top view and the object-ID mask, and the in-memory 3MF cache those share dies with the process - so after a restart all three went back to the printer at once: three fan-outs, thirteen seconds, and a 0-byte read from socket contention, which is the storm #972 was about. The cover flow takes the running print's archived file too, resolved in the caller's short-lived session and passed in so _produce_cover_image still does no DB work, and marked as a shared file so the cleanup cannot delete the archive. Finally the card: a running print always has at least one object, so a count of zero means "not loaded", not "nothing to skip". Exactly one object is the real nothing-to-skip case and still disables the button. --- Stop a test's printer client leaking into the next test POST /api/v1/printers really connects, so a test that creates a printer through the API leaves a live client in the printer_manager singleton. The singleton outlives the per-test in-memory database, so the next test on that xdist worker - whose own first printer is handed the same primary key - reads that leftover client as its own live status. test_scheduled_drying_routes was the visible victim: an "online" printer with no firmware version fails the drying preflight, so scheduling came back 400 instead of 200. It only bites when --dist load happens to put victim and leaker on one worker, which is why it passes on its own and flakes under -n. Registrations made during a test are now undone after it, ids the test did not add are left alone, and disconnect_printer is what also drops the model and printer-info caches and stops the paho thread the leaked client was keeping alive against an unreachable address for the rest of the run. |
||
|
|
d227d42272 |
Let a print with no 3MF be given its filament weight (issue #1820)
When the sliced file stays somewhere Bambuddy cannot read, the archive is built from the printer's report alone and carries no weight. Nothing could supply one afterwards: rescan reads the figure out of the 3MF, and that archive has no file to read. The reporter's H2S print left 46.16 g on the spool with nothing recording it, and he corrected Spoolman by hand. Edit Archive now has a Filament used (g) field. It is written to the archive's most recent run as well, because the Projects roll-up and the Prometheus counter sum PrintLogEntry rather than the cards - correcting only the archive would fix the display and leave every aggregate reading the old figure, or none at all. But not over a figure the run measured for itself. A run's grams come from the tracked spool delta when there is one and only fall back to copying the archive's estimate when there is not, so mirroring unconditionally would overwrite a measurement with a typed estimate. The mirror now takes a run that has no figure, or one holding exactly what this archive held - which also makes the undo complete, since clearing the archive clears the copy it made and leaves a measured run alone. The field is text rather than a number input. A number input reports an empty string for anything the browser judges malformed, a decimal comma in a locale that does not expect one included, and that reads here as "the user cleared it" - it would have wiped a good figure while the field still showed what was typed. Filtering on the way in keeps what is displayed and what would be sent the same string, and clamps it to the range the API accepts: this modal has no error surface, so a refused save looks like nothing happened at all. Saving also invalidates the archive's runs query. The Print Log this modal renders at its top reads them separately and kept serving the pre-edit row, so a correction looked like it had not taken - true for the status and failure-reason mirrors since #1444 as well. Second half of the same report: the internal-storage probe (#2856) logged which file it found but not where. On a printer that keeps uploads for weeks - the reporter has months of them in /cache - a reprint of a name that was re-sliced but never re-sent can match an older copy, and without the directory that mismatch is invisible rather than merely rare. The download helper returns the path that served the file instead of a bare flag; every caller only ever tested it for truth. |
||
|
|
90fac7b529 |
Keep card and row actions reachable without a hover-capable pointer (issue #2865)
Tailwind v4 compiles group-hover: inside @media (hover: hover) - the shipped CSS has .group-hover\:opacity-100 sitting in exactly that block. On a touch-only device the media query never matches, so the rule that reveals the control is not merely never triggered: it is never applied. A control written as opacity-0 group-hover:opacity-100 is invisible for good. The reporter's iPhone screenshot shows the project card with nothing where the "..." belongs, and Edit and Delete live only there. So the hiding half is what has to depend on the pointer, not the revealing half. A can-hover variant carries the query - the same one the history thumbnail preview has used since it was written - and the six controls behind it become can-hover:opacity-0 group-hover:opacity-100. Without a hover-capable pointer no rule hides them and they simply render; with one, nothing changes. Every reveal selector is specificity (0,2,0) against the hider's (0,1,0), so which one wins does not depend on where they land in the stylesheet. Six controls were affected: the project card menu, the File Manager's folder actions (reachable by accident today - the "wrap names" toggle drops the hover class), duplicate preset, rename and delete tag, delete print photo, delete plate reference. Six more already tried to handle this, by viewport width under 768px on the Archives cards and the File Manager's file cards. That covers a phone and misses an iPad in landscape, which is touch-only at 1024px. They move to the capability check and useIsMobile goes with them, along with the isMobile prop threaded into FileCard. opacity-0 also leaves a button focusable while invisible, so tabbing through a card stopped on a control nobody could see. Focus now reveals them, through group-focus-within on the wrappers and focus-visible on the standalone buttons - neither is hover-gated. jsdom does not evaluate media queries, so the tests pin the class contract instead: a bare opacity-0 is the defect, because it applies unconditionally while everything that undoes it does not. The decorative hover reveals are left alone - the archive hash badge, the project name overlay, the thumbnail preview, the swatch tooltips. Nothing is unreachable there, only unseen. |
||
|
|
05d87a9740 |
Release the plate-clear gate on a powered-down printer (issue #2864)
POST /printers/{id}/clear-plate answered 400 "Printer not connected" for
anything without a live MQTT client, and the printer card hid the button
under the same condition. With Auto Power Off that is the ordinary end of
every print: the reporter's log has printer 1 marked offline at 12:00:55
by the plug and the clear-plate POST rejected at 12:03:12, with the plate
already cleared by hand. Nothing could release the gate short of powering
each printer back on, clearing, and switching it off again.
Nothing in the clear path talks to the printer. set_awaiting_plate_clear
writes an in-memory set and the printers.awaiting_plate_clear column, and
that column exists precisely so the gate survives an Auto Off cycle
(#961). The guard came in with the endpoint in
|
||
|
|
607b34e94d |
Check the card before writing a print off as internal-storage-only (issue #2856)
A print's dispatch says where the printer put the sliced file: ftp://<name> for external storage, brtc://emmc/<name> for internal. Since that there is then no file to find at any path. That is where the printer chose to put it, which is not the same as where port 990 can read it. The reporter's H2D - firmware 01.03.00.00, card in the slot - reports brtc://emmc and keeps the same file under /cache: his log has every print from 08-12 downloading from there, 19 MB included, until the skip landed and two days of archives came out as a name and nothing else. #2780's P2S and H2C really did 550 on every path, so both are true and the URL alone cannot tell them apart. So ask the printer rather than the model. The dispatch names the exact file, which turns the question into one connection walking five directories - against the sweep's ~110, which is the cost that made skipping worth doing. A hit archives normally and is shared with the cover endpoint; a miss keeps #2780's fallback archive and its reason, so the archives banner still explains itself. Not probed when the printer reports an empty slot, or while its file service is in TLS cool-off: both have already answered the question. The connection diagnostic asked the same question off the URL and warned that the last print was out of reach. On this reporter's printer that warning would have sent him to a setting that was already right, so it now probes too - by directory listing, since the file it is asking about can be tens of megabytes and the answer is a yes or a no. Capped at 6s to stay inside the support bundle's per-printer budget, and "could not check" leaves the warning standing. The probe filename arrives over MQTT and becomes both a remote path and a local temp filename, so names carrying separators, traversal or control characters are declined rather than cleaned. |
||
|
|
5a05e03c8e |
Use the printer's own plug for energy when several are linked (issue #2859)
Per-print energy is one plug's meter read at the start of a print and again at the end. Both readings asked for "the plug on this printer" with scalar_one_or_none(), which raises on two rows. Linking a second plug to a printer - a dry box, a filter fan, a lights script - therefore stopped energy tracking on that printer outright, and did it silently: the print-start handler logged the exception as an ordinary failure and the print-end handler then reported "no start kWh recorded", which is also what it says for a printer with nothing linked to it. The assumption was never enforced anywhere else. The plug API rejects a second Tasmota plug and deliberately allows any number of Home Assistant entities, the UNIQUE constraint on smart_plugs.printer_id was dropped on purpose, and every other consumer reads a list. These two call sites were the last ones left from before that. Energy now ranks a printer's plugs - the one that powers it first, then by id so the start and end readings agree - and takes the first that actually reports a counter, so accessories drop out with nothing configured. Ranking rather than filtering: a printer whose only linked row is disabled, or a script, used it before and still does. When none of them measures anything the log names the ones it tried, so that stops reading like "no plug configured". Also: the plug page counted an online plug as offline unless it reported energy, so a switch with no power sensor showed as offline for as long as it stayed linked. Existing archives cannot be backfilled - the starting reading was never taken, so there is nothing to compute a delta from. |
||
|
|
e9eff1a276 |
Point at the upstream issue behind the internal-storage behaviour
The README callout and the #2843 changelog entries described what Bambu Studio does without saying it is tracked anywhere, so a reader had no way to follow it. Both now link bambulab/BambuStudio#10481. The #2843 entry also still blamed H2-series and P2S firmware for keeping sliced files internally. That was the reading before measuring it: the same printers archive in full when sliced in OrcaSlicer, so the destination is the slicer's choice, not the printer's. Corrected here rather than left to mislead, since 1.2.6b1 is unreleased. |
||
|
|
2c7df27d1a |
Look for a print's file when the printer says it is on the card
(#2780 regression) A print of a file already on the printer -- a reprint from the touchscreen, from Handy, or a slicer send-to-storage followed by a print -- reports its location as a path rather than as a fresh upload: file:///media/usb0/<name>. Since #2780 landed on 2026-08-14 Bambuddy read anything that was not ftp:// as "the printer kept this internally", skipped the FTPS sweep, and archived the print with a name and timing only. Measured on an H2D: the file was listable and downloadable over FTPS at the moment Bambuddy declared it unreachable. Before that change those prints archived normally, so this is a regression, and it is not confined to the H2 series the change was about -- an X1C reprint from its own screen loses its thumbnail exactly the same way. That module's own rule is to skip only on positive evidence, and a file:// path is not evidence of internal storage. It now reads the path: the printer's model cache under /userdata is a genuine skip, anything else is unknown and sweeps, which is what it did before. Unknown rather than external on purpose -- the empty-slot check still runs ahead of it, so a file:// print on a printer with nothing in the slot reports the missing card instead of sweeping for something that cannot be there. The user-facing copy shipped this morning is corrected in the same change, because it was written before we understood how Bambu Studio actually chooses. Its Print button always uses internal memory; only Send offers Cache or External, and that defaults to Cache too. So the advice now leads with the two routes that take one step -- start the print from Bambuddy, or slice in OrcaSlicer -- and offers Send-with-External and a separate print start as the way to stay in Bambu Studio. The earlier wording named no remedy at all and blamed the printer's firmware for a choice the slicer makes. Banner and diagnostic, thirteen locales, README, bundle rebuilt. |
||
|
|
034f8ac2c3 |
Tell H2-series owners what to do about incomplete archives (#2843)
The no-3MF banner and the connection diagnostic both explained why a print archived with only a name and offered nothing to do about it. The explanation was also wrong in a way that mattered: both blamed the printer's firmware and said no setting changes it, which reads as "your machine is broken and nothing will help". Measured on hardware today. Same Bambu Studio, same model, three printers a minute apart, all reporting the external-storage option as on: the H2C and H2D went to internal storage and archived with a name only, the X1C went to the card and archived in full. The same H2C and H2D sliced in OrcaSlicer put the file on the card and archived in full, and turning the option off changed nothing about that -- OrcaSlicer always uploads over FTPS. So the printer does whatever the slicer asks, and the option governs neither slicer on this generation. Both strings now name Bambu Studio rather than the firmware, and end with the two routes that do work: start the print from Bambuddy, or slice in OrcaSlicer. Both mention that a card or stick is still needed, because "use OrcaSlicer" on its own trades one confusion for another -- with an empty slot it refuses to send at all. Translated in all twelve locales, each reusing the term it already uses for the setting name and for slicer metadata. Bundle rebuilt, since the strings compile in. |
||
|
|
89ea337612 |
Recognise our own print dispatch instead of guessing at a magic number
(#2843 follow-up) Bambuddy records the project_file behind every print, because that command names where the sliced file was put and so decides whether the archive can have a thumbnail and slicer metadata at all. Its own dispatches were told apart from a slicer's by testing sequence_id against "20000", on the belief that 20000 was Bambuddy's alone. It never was. 20000 is the slicer convention Bambuddy copied -- virtual_printer/bind_server documents the slicer sending exactly that during detect -- and both slicers count up from it. Measured on the wire: OrcaSlicer dispatched 20000 and then 20001, BambuStudio 20009 and 20010. So whichever dispatch happened to land on the shared value was filed as ours and never recorded, and after a slicer restart that is the first print you send. The test was wrong in the other direction too: it called every value above 20000 external, including ones we had sent ourselves, which only stayed invisible because we always send exactly 20000. Ownership is now established by remembering the job actually dispatched -- sequence id, file, url and subtask name -- and consuming that marker on the echo. One-shot deliberately: a slicer reprint of the same file a moment later is somebody else's print and must not hide behind our last one. Nothing about printing or archiving changes. current_project_url and ams_mapping are both captured before this branch and always were, so the storage verdict that gates the FTPS sweep is untouched; two tests pin that, because it is the part that would actually cost archives if it drifted. What changes is that the diagnostic entry stops lying, and it is the entry that tells an operator whether their printer stores files somewhere Bambuddy can read -- which is the whole subject of #2780 and would have to count, and an undercount there would have been silent. |
||
|
|
6488a33488 |
Stop a print with no 3MF borrowing another model's data (#2843)
H2-series and P2S firmware keeps a slicer-sent file on internal eMMC. Port 990 serves external storage only, so there is no file to fetch, and the print becomes an archive with no 3MF -- the ordinary outcome for anyone who sends from Bambu Studio rather than through Bambuddy. Confirmed on the maintainer's own machines: an H2C and an H2D both dispatched brtc://emmc for the same model one minute apart, while an X1C sent ftp:// for it. Three separate defects live in what happens next. The first is the serious one. An archive with no 3MF keeps the path the printer is executing as its filename, and on a sliced job that is always Metadata/plate_1.gcode. The fallback that looks for the same model in the Library or among earlier prints took its search term from there, so it searched for `plate_1` -- a name every Bambu print in existence has -- and matched on a substring, so it also matched any name merely ending that way. On the H2D a 1.6 g Cube resolved to lid_plate_1.gcode.3mf and was costed at 207 g across three real spools. It was not confined to plate names either: in the same database `Bank.3mf` matched "Piggo the piggy bank", and `x1c.gcode.3mf` matched "slice-test-x1c". The matcher now takes the model name the printer reports when the filename is only a plate path, refuses a bare plate stem rather than searching for it, and anchors to a whole filename with LIKE metacharacters escaped, because `_` is a wildcard and model names are full of them. A print that cannot be identified is now left untracked, which is the honest answer -- the previous behaviour was to charge the operator's spools for a model they had not printed. Checked against every row rather than argued from the code. Across 273 library stems the result sets are identical. Across 241 archive stems 14 differ, all of them strictly narrower, and every dropped match is one of the false positives above; all 233 archives still match their own filename, so no legitimate donor was lost. Of the eight no-3MF archives on that install the old matcher picked a wrong donor for two -- one of them a calibration run that would have been charged the 207 g -- and the new one picks none. The second defect is that those archives could not receive a timelapse at all. attach_timelapse derived its destination from the missing file's path, and (base_dir / "").parent is the parent of base_dir, one level outside the data directory. In Docker that is /app, so every attempt failed EACCES and the scan retried and discarded the video 25 times over twelve minutes, roughly a hundred FTPS connections for bytes that had already downloaded successfully. Where that location happened to be writable it was worse: the file landed beside the installation and the attach then failed anyway, because the path could not be made relative to base_dir. #1820 introduced a shared helper precisely so these derivations could not drift apart, and this was the one site still doing it by hand. The directory is created only after the filename has passed the traversal check, so a rejected name still leaves nothing behind. The third is silence. When a print's filament cannot be read from a 3MF, the remaining-percentage delta is the fallback, and that needs a reading at print start -- which a spool without RFID does not have until someone sets a remaining amount by hand. Those slots were skipped with a bare continue. Every other reason for skipping a slot in that loop is logged, and the comment a few lines below argues the case explicitly: charging nothing silently is indistinguishable from having nothing to charge. It now says so, for slots the print actually used. Four existing tests needed updating rather than the production path. They patch backend.app.services.archive.settings by name, and the shared helper reads its own module-level binding, so they kept the real data directory and wrote outside tmp_path -- which is how the first draft of this change littered a working tree. They now patch both bindings. |
||
|
|
28b2b9f151 |
Pin the PostgreSQL session to UTC so defaulted timestamps are UTC (#2855)
On a UTC+3 install every AMS humidity reading and every archive was stamped three hours ahead of when it happened. Bambuddy stores naive timestamps that hold UTC and the frontend's parseUTCDate reads an offsetless timestamp as UTC, so the display added the offset to a value that was already local. The Python side has honoured that contract since #504. The reporter's timestamps were not written by Python. Around ninety-six columns take their value from server_default=func.now() and the migration DDL carries another forty-nine on DEFAULT CURRENT_TIMESTAMP -- the database fills those, and on PostgreSQL now() is a timestamptz, so storing it into a timestamp without time zone casts it through the session TimeZone. A Postgres container started with TZ=Europe/Istanbul bakes that zone into postgresql.conf at initdb, and every defaulted column then receives local wall-clock. recorded_at is the clearest case: nothing in the codebase ever assigns it, so its value is entirely whatever the database decided. Connections now carry timezone=UTC, which makes the cast a no-op whatever the server is set to. Measured through the real engine factory against a live PostgreSQL, a session on the reporter's configuration stored +10800s and the fixed one +0s. Pinning the session was preferred over a hundred and forty-five individual edits partly for its size but mostly because half of those sites are raw DDL that no model-level change can reach. SQLite needed nothing and gets nothing: its CURRENT_TIMESTAMP is UTC by definition and it has no session timezone to get wrong, which is why this survived two years of timezone fixes without showing itself. That also makes it the reference -- the change moves Postgres onto SQLite's behaviour rather than introducing a third convention -- so the SQLite behaviour is now pinned by a test instead of being assumed. asyncpg is the documented driver and takes the setting in its startup packet; any other Postgres driver gets the same setting the libpq way, so a psycopg URL does not fail at connect on a keyword asyncpg alone accepts. Rows already written are deliberately left alone. The inverse cast is computable and DST-correct, but it cannot be applied safely: created_at is assigned explicitly on some paths and defaulted on others, an install that began on SQLite holds correct and shifted rows side by side, and nothing distinguishes them after the fact. Timestamps are right from the upgrade forward and history keeps the times it was given. One related mismatch goes with it, because fixing the database side alone would have made it start lying on exactly the installs this repairs. The support package's oldest_pending_age_seconds subtracted a naive local clock from a naive UTC column, with a comment claiming it was UTC; on the reporter's install the two errors cancelled. It reported a job queued five minutes ago as three hours old east of Greenwich and a negative age west of it. The two AMS and printer-sensor retention cutoffs move to the same utcnow_naive helper -- correct in value already, but deprecated in 3.12 and emitting warnings on every sweep. |
||
|
|
c5e0055864 |
Track which nozzle each AMS feeds through the Filament Track Switch
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty. Bambuddy had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The binding needed no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the type and the extruder id -- 0 is In-B, 1 is In-A -- and it only means anything when a switch is installed, because without one 0xE really does mean an uninitialised unit. That gates the read, which forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. The badge letters stay L and R, In-A reading as L, with the inlet named in full in the tooltip -- the letter is the inlet's position and not a claim about which nozzle that AMS feeds, since the switch can route either inlet to either outlet. Both views update live now. The switch fields were missing from the WebSocket payload, and the frontend shallow-merges each push over its cached status, so an absent field kept whatever the last full fetch left behind. The broadcast dedup key had no term for them either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and not in the AMS change-hash, which covers tray fields only and must stay that way because it drives Spoolman sync. The rest of this is the calibration half, which is where it actually bites. K-profiles are calibrated per nozzle and the printer numbers its calibration table per nozzle too, so entry 16 exists on both hotends and means a different profile on each. A tray holds exactly one index. Move an AMS to the other inlet and every configured slot in it silently keeps pointing at the old hotend's table -- measured on the maintainer's H2C, a black PLA calibrated 0.018 left and 0.020 right stayed on the left profile after the move, and a manual RFID re-read only re-asserted the same wrong one. Bambu has not solved this either; their AMS dialog carries "TODO: fila_switcher broken the connection of ams->extruder" above the line that decides which nozzle's profiles to offer. Three copies of "which extruder is this slot on" each ended in else 0, which on a switch machine filed every profile under the right-hand nozzle. They now share one resolver that returns None for "unknown", because unknown and extruder 0 are very different answers on a dual-nozzle machine and conflating them is what bound a left-nozzle profile to a slot sitting on the right. The per-slot K value on the printer card had the same confusion from the other direction: its lookup was keyed on cali_idx alone, so one nozzle's K silently overwrote the other's. Moving an AMS now re-selects each configured slot's counterpart profile for the nozzle it has arrived on. Only the calibration binding moves, and only for slots whose spool already has a profile for that nozzle: configuring a slot is a deliberate preparation step, so a slot we know nothing about, or a spool calibrated on one hotend only, is left exactly as the operator set it. Nothing fires on the first sighting of a binding either, since every reconnect learns them afresh and re-applying there would overwrite a choice made by hand. Configure Slot resolves against the slot's own nozzle throughout. Option identity carries the extruder, so a filament calibrated on both hotends gives two distinguishable entries instead of two that collapse into whichever the printer listed first; options name the hotend; matches are scoped to the nozzle the slot feeds, with the other hotend's profiles still reachable under Other K profiles; and the slot's active index is resolved as a pair rather than followed into the wrong table. Inlet to nozzle is one table, In-A to the left hotend and In-B to the right, measured rather than assumed -- fila_switch.out cannot be used for it, reporting [1, 1] unchanged across a 90-second capture, both outlets claiming the same extruder. The print dialog picks up the same inlet labelling in its slot dropdown, replacing a left/right hint that never rendered because it matched snow-encoded values against global tray ids; decoding it correctly would not have saved it, since the firmware never reports which inlet is currently paired with which outlet. The dialog also notes when every filament a print needs sits behind one inlet, which is legal but slow -- a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS, where a change across the two only retracts as far as the switch. Assigning an AMS to an inlet remains printer-side. BambuStudio can read that binding and has no command to write it, so there is no wire format to copy. |
||
|
|
7a42e0a7e5 |
Show which Filament Track Switch inlet each AMS feeds
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty on these machines. The printer card had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The SpoolBuddy assign modal had the same fallback in a worse form, mapping anything that was not extruder 1 to R. The binding turned out to need no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the AMS type and the extruder id -- 0 is In-B, 1 is In-A -- and it is only meaningful when a switch is installed, because without one 0xE really does mean an uninitialised unit and those bits carry nothing. That gates the read, which in turn forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. _parse_fila_switch is split out and called first, and left in _update_state as well so that stays a complete absorb step. The badge keeps L and R rather than A and B, because the lettering is familiar and matches the physical layout. It is a different colour from the plain nozzle badge, and its tooltip names the inlet in full, since the letter is the inlet's position and not a claim about which nozzle that AMS feeds -- the switch can route either inlet to either outlet. An AMS still reporting a real extruder id keeps its ordinary badge, which BambuStudio also treats as authoritative over any switch binding, and a switch that has been fitted but not yet set up on the printer shows nothing rather than a guess. The print dialog's slot dropdown gets the same label. It replaces a left/right hint that never once rendered: ftsExtruderForSlot compared snow-encoded in[] values against global tray ids and could not match. Decoding it correctly would not have saved it -- the firmware reports which slot sits in each inlet and which nozzle each outlet feeds, but never which inlet is currently paired with which outlet, so no per-slot nozzle can be derived. That function is gone rather than fixed. The dialog also points out when every filament a print needs sits behind one inlet. Bambu's own guidance is that this is legal but slow: a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS before the next can be fed up the shared tube, where a change across the two inlets only retracts as far as the switch. All on one inlet means every change in the job takes the slow path, and moving a single spool fixes it. So it advises, it does not block. Both views update live. Two things were stopping that. fila_switch and ams_switch_inlet were absent from printer_state_to_dict, and the frontend shallow-merges each WebSocket push over its cached status, so a field the push omits keeps whatever the last full fetch left behind. And the broadcast dedup key had no term for either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and it is not in the AMS change-hash either, which covers tray fields only and must stay that way because it drives Spoolman sync. Assigning an AMS to an inlet remains printer-side. BambuStudio can read the binding and has no command to write it -- its switch class is parse and getters only, and the recommended-arrangement popup draws and publishes nothing -- so there is no wire format for us to copy. Adding the two fields to PrinterState broke four test modules whose SimpleNamespace stubs predate them. The stubs are fixed rather than the production reads made defensive: the real dataclass always carries both, and a getattr in the dedup key would silently stop tracking the field if it were ever renamed. |
||
|
|
9c93884397 |
Stop auto K-profile calibration leaving an archive behind
With flow dynamics calibration on, the printer lays down a pressure-advance line before the print itself and announces it over MQTT through the same print-start event a real print uses. Bambuddy archived it: a row named auto_pa_line_calib_mode marked as having no 3MF, sitting in among the user's actual prints, with a Print Started and a Print Completed notification for each one. The printer's other internal jobs were already skipped, but only by the /usr/ path they carry -- bed levelling reports as /usr/etc/print/auto_cali_for_user.gcode. The pressure-advance line carries no path at all. It arrives as a bare subtask name, so a rule that only ever looked at the filename could not see it. Falling past the guard it reached the no-3MF fallback, where print_name is subtask_name or filename, and named the row after the calibration. Internal jobs are now recognised by name as well as by path, from either field, in one place both callbacks share. The match is exact after normalising away the directory, one print-file suffix and case, rather than a prefix or substring rule: "auto" and "calib" are ordinary words in a user's own filenames, and a rule loose enough to catch some unnamed future calibration would quietly swallow somebody's print. The completion is suppressed too, and that half matters more than the noise. With no archive to close, the completion falls into the no-archive notification path -- which attributes an unmatched completion to any queue item the printer finished in the last five minutes and emails its owner. This calibration runs alongside a real print, so silencing only the start would have told that print's owner their job was done, early, and again for real later. The guard sits inside the no-archive branch, so the plate-clear gate, the queue reconciliation and the SD-card cleanup all still run; only the notification is skipped. Skipping the run early also drops the FTP sweep that preceded the fallback -- six candidate names across five directories with retries, around a hundred connections looking for a file that cannot exist, aimed at a printer that is in the middle of calibrating. auto_cali_for_user no longer sends a Print Started notification either. It is the same event about the same kind of job, and the archive was never the only thing wrong with treating it as a user's print. |
||
|
|
61a6ed1f20 |
Move Camera View Mode from Settings onto the camera button
Whether a camera opened in its own browser window or as a floating overlay was one dropdown in Settings > General > Camera, applied to every camera on the install. Deciding per printer meant leaving the Printers page, changing the setting, coming back, opening the camera, and going back again to undo it -- five steps for a choice you make while looking at the printer you want to watch. The camera button on the printer card is now a split control. The icon opens the camera whichever way you opened the last one; the caret beside it offers both modes, and picking one opens the camera that way as well as making it the mode the icon uses from then on. A menu that only changed a preference would have left the user a second click to do the thing they had already asked for. The mode in effect is ticked. The choice lives in the user's own browser, so two people watching the same farm can each have the view they want. camera_view_mode survives as the default a browser that has never chosen starts from, and is written back when the user holds settings:update. The local value wins on read: someone below that permission cannot write theirs back, and a preference that silently reverted on the next render would be worse than none. Two things were consolidated on the way through. The popup-opening code -- saved geometry, and deliberately no noopener so the browser copies sessionStorage and its auth token into the new window -- was duplicated between the printer card and the Cam Wall tile handler; it is now utils/camera, with the geometry parse wrapped so a corrupt cameraWindowState falls back to defaults instead of throwing, which neither copy did. The Cam Wall follows the same remembered mode, since a tile has no room for a split button of its own. The effect that force-closed every open overlay when the setting flipped to window is gone. It made sense for a global switch; with the choice made per click, closing viewers someone deliberately opened does not. No new locale strings: the four the settings control used are reused as the menu's labels and tooltips and the caret's own label, so all 13 locales stay in parity untouched. |
||
|
|
47a37618a0 |
Let a busy or offline printer take a dropped file (#2849)
Dragging a sliced file onto a printer card refused the drop unless the
printer was connected and neither RUNNING nor PAUSE. The overlay went red
with "Printer busy", handleCardDrop returned early, and the file was
discarded with no toast and nothing uploaded. The card's Print button was
hidden by the same condition, so both routes into "Print from Printer
Card" closed at once and the way through was the File Manager, uploading
and queueing by hand.
The gate never described a real constraint. Every print Bambuddy sends
becomes a queue item; dropping onto an idle printer only looks instant
because the scheduler dispatches it on the next pass. Busy is a timing
difference, not a different path. The modal has always passed
disableBusy={false} to PrinterSelector, and asapToastShouldPromiseLaterStart
exists precisely to say "this will start later" when the target cannot
take it now. cleanup_library_after_dispatch is a print_queue column
consumed at dispatch, not on close, so the transient upload survives
however long the item waits.
Offline is included for the same reason: the queue dispatches when the
printer comes back, so a machine that is powered down can be given work.
The overlay now says which one is happening -- "Drop to print" when the
job would start immediately, "Drop to queue" when it would wait, covering
a print in progress, a paused job, an AMS mid-cycle, a plate not yet
cleared, and a printer that is offline. The predicate behind that wording
is the one the modal already used for its own later-start notice, lifted
out of PrintModal into utils/printer as isPrinterCurrentlyDispatchable so
the card cannot promise something the modal contradicts a second later.
The drop is also gated on the permissions the flow actually exercises. It
uploads to the library and creates a queue item, so library:upload and
queue:create -- the pair the Print button beside it has always checked.
printers:control, which it checked before and never uses, meant someone
holding that alone got the file uploaded and then rejected by the queue,
leaving a library row behind with nothing pointing at it. The refusal now
names whichever of the two is missing instead of claiming the printer is
busy.
printers.cannotPrint is dropped in favour of printers.dropToQueue across
all 13 locales; its text was both unused and, after this, wrong.
The Print button stays inside the expanded-card block, so S-size cards
still show the drop zone and no button, exactly as before.
|
||
|
|
7a9b4921bd |
Stop the AMS temperature alert firing for heat the user asked for (#1802)
The alert compares against ams_temp_fair, the same threshold that colours the printer card, which defaults to 35C. Drying deliberately runs at 45C for PLA, 65C for PETG and up to 85C on an AMS-HT, and the alert repeats once an hour for as long as the condition holds, so a twelve-hour dry sent twelve notifications about a temperature the user chose. It then kept sending them while the unit cooled back down, which is the half the reporter confirmed on an AMS 2 Pro and an H2C. Dispatch now consults the drying state the firmware already reports. dry_time alone is not enough: it reads 0 through the cooling phase that closes a cycle, so dry_status -- info bits 4-7, already parsed for the drying-complete edge -- carries the rest. That constant moves out of bambu_mqtt into a leaf util rather than being duplicated; drying_preflight would have been the natural home, but it imports printer_manager, which imports bambu_mqtt, and bambu_mqtt is one of the callers. The cool-down afterwards is held by a latch released as soon as the unit reads back at or below the threshold, rather than after a fixed delay, so a 65C cycle in a cold basement and a 45C one in a warm room each get the time they actually need. A two-hour cap bounds the one case the latch cannot resolve on its own -- a unit that never returns below the threshold -- and since such a unit would have been alarming with no drying involved, releasing there restores the ordinary behaviour instead of inventing a new alert. Two exclusions are deliberate. Humidity is untouched, because during drying that reading falling is the whole point. And dry_status 6, HeatOutOfControl, is kept out of the active set: an AMS that has lost thermal control is exactly when the alert should still arrive, so it must never read as expected heat. A cycle plus its cool-down outlasts a restart, so the latch is a settings row rather than a dict beside _ams_alarm_cooldown -- the internal timestamp-row pattern support.py already uses. It is read once per pass and written back only when a unit changed it. Stamps ahead of now are clamped on read, since a box whose clock jumps backwards writes them and suppression is measured as now minus the stamp; without the clamp the cap would measure from a moment that has not happened yet and hold the alert quiet for the skew on top of it. No new setting. The reporter was offered the opt-out checkbox they asked for and said they would not want it if the alert simply never fired during drying. |
||
|
|
c839841063 |
Pin Trivy to a release that still exists (#2844)
The scan pinned Trivy v0.69.1, which aquasecurity have since deleted -- retained releases now run v0.74.0 down to v0.69.2 and then jump back to v0.26.0. The tag survives, so setup-trivy resolves it, reports "found version: 0.69.1" and then exits 1 with no asset to fetch. This repository did not notice because the binary was coming back from the Actions cache on every run, which skips the download. Forks have no such cache, which is where it was reported from -- and the same failure was due here the first time that entry went cold. Both scans move to trivy-action v0.36.0 and Trivy v0.74.0; every input they pass is still declared in the new action. The comment records that this pin has to be bumped rather than left, and that a green run is not evidence it still resolves. The config scan is clean on v0.74.0, so the bump adds no new misconfiguration alerts. |
||
|
|
713a85d114 |
Let a bug-report capture outlive the panel that started it (#2847)
Step 2 asks the user to reproduce the problem, and the panel sits over the part of the app they have to reach to do it. Closing it was the obvious move and it was wrong in two different ways, picked by timing alone. Reopen inside five minutes and the reset-on-open effect put you on an empty step 1 while the server stayed at DEBUG, with nothing left in the flow that could stop it -- only Stop & Submit ever did. Leave it closed and the cap fired behind you: the panel is hidden but mounted, so the timer kept running, stopped logging and filed the report with no window open and no confirmation. A capture is now written down -- description, email, was_debug and a start timestamp -- and the reset skips a run in progress, so the panel reopens on the step it left. The disc turns amber while a capture is going and Layout marks the compact header's button and offers Resume report on the debug-logging banner, since a run started there ends at that panel's button and not at the System page's raw toggle. If the cap fires while the panel is closed, it opens first, so the submission happens in front of the user. Elapsed is measured against the start time rather than counted in ticks, which a background tab throttles hard enough that five minutes was not five minutes. That timestamp also lets a run survive a reload, which matters because reloading is an ordinary step in reproducing a bug: on mount a stored run is reconciled against /support/debug-logging and resumed. One that outlived the cap unattended is not resumed and not filed -- an hour-old description is not a report anyone still expects -- but its log level is put back, which is what stayed wrong indefinitely before. The screenshot is deliberately not persisted: a 1920px JPEG runs to hundreds of kilobytes against an origin-wide budget, and it survives a close either way. |
||
|
|
a72f49be02 |
Stop the File Manager card menu from clipping its own top entry (#2846)
A grid card drew its action menu inside itself and clipped its own overflow, so a card shorter than its menu lost whichever entry sat at the top. An STL card is the shortest in the library -- a square thumbnail plus a name and a size, around 270px against a seven-entry menu needing closer to 310px -- and the entry it lost was Slice, since Print is only offered for an already-sliced file. A 3MF carries a target model and a print count, two more rows, so its card was tall enough and the button appeared, which made this read as a rule about file types. It was not: the shortest card lost its first item, whatever that item was. The menu is now the shared ContextMenu, anchored to the kebab in viewport coordinates the way the archive card has always opened its own. Being fixed, it escapes both clipping ancestors -- the card and the grid's scroll container, which would have cropped the top row of cards even without the card's own overflow. The card drops overflow-hidden anyway and the thumbnail rounds its own corners instead, so nothing a child positions outside the card can be cut off again. While rewriting the block, 3D Preview and its permission tooltip stop being hardcoded English; fileManager.preview3d and fileManager.noPermissionPreview are added to all 13 locales. List view was never affected: it has no menu, only inline buttons. |
||
|
|
907de4d64d |
Suppress two Bandit false positives in the new FTP and batch-order tests
The 1.2.5.3 code-scanning run flagged two new alerts, both in test files added this release, and both false positives. B402, the ftplib import in the #2780 connect-cleanup tests, is the HIGH finding that failed the check. The test imports ftplib to construct the exceptions BambuFTPClient.connect has to survive -- error_perm and error_temp, at lines 50, 51 and 75. Nothing in the file opens a connection, and bambu_ftp.py already carries the same marker on its own import. B108, the /tmp path in the batch-order archive fixture, is the MEDIUM one. The value is a string written into PrintArchive.file_path so the row has a path; nothing ever opens it. Every other archive fixture in the suite carries the same marker on the same idiom. Both markers follow the wording already in test_bambu_ftp.py and test_sjf_scheduling.py. Bandit's medium+ count over backend/ drops from 17 to 15, and neither file contributes to what is left. |
||
|
|
6f6d16eb8f | Updated CHANGELOG | ||
|
|
98182d81f3 | Updated CHANGELOG.md | ||
|
|
d37ce94f81 |
Feature: Scheduled drying (#2703)
* feat: add ScheduledDrying model for delayed drying runs (#2638) * Release the printer when a scheduled dry ends (#2638) _check_scheduled_dryings marks a printer as drying in _drying_in_progress, which is shared with auto-drying. Auto-drying prunes that map in _sync_drying_state(), but that call sits behind its enabled check, and this is the first writer that runs whether auto-drying is on or not. With it off -- the default -- nothing dropped the entry short of a print being dispatched to the same printer, so the next scheduled run parked on "already_drying" forever and queue_drying_block held that printer's prints too. A nightly off-peak dry with no printing in between is exactly the workflow this feature is for: night one worked, every night after it silently did not. The check now releases what it acquired, covering both a run that ends mid-pass and one cancelled through the route between passes. The retention prune ran on every pass. Issuing the DELETE is what opens a write transaction, this method is called every 3s while the queue dispatches, and rows only become prunable a week after they finish, so it is now gated to hourly on a monotonic stamp -- with the first pass after a restart still reaping whatever the dead process left behind. Both drying paths now pick the blocking dry_sf_reason through one rule. The immediate endpoint quoted whichever code the firmware listed first while the scheduler prioritised power over retract, so one blocked AMS read two ways depending on which button you pressed. drying_preflight.primary_reason_code holds the order and both call it, including the flame button's tooltip, which had no wording for filament at the outlet at all and sent those users to the generic "can't start drying right now". scheduled_drying joins the model list in init_db. The table was already created -- importing the package registers it -- but it was the only model relying on that indirection. Tests: the release (completion and route-cancel), the prune throttle, the shared reason rule, the tooltip priority, and four driving the real check_queue, which nothing covered before -- a pass with no rows still dispatching prints, a due row dispatching, and a failed row not stalling the queue behind it. Each one fails against the code it guards. --------- Co-authored-by: MartinNYHC <martin@bambuddy.cool> Co-authored-by: maziggy <mz@v8w.de> |
||
|
|
db94ea7971 | Updated CHANGELOG | ||
|
|
b11a15f1c0 |
[Feature]: Server-Side Slicing on linux/arm64 systems (#1900)
* Add override file for ARM64 setups. This commit adds an override file which explicitly specifies the container platform to be linux/amd64. It forces docker to pull/build/run the amd64 image (even on arm64 hosts). Assuming binfmt support is set up, this will run the amd64 applications via emulation. * Add arm64 override file information to README. Adds a section covering the experimental setup for arm64 hosts to the README. * Make the ARM64 override survive the next compose command (#1900) The override only applies while both -f flags are on the command line, and every other instruction in this README is written bare. An ARM64 user who followed the update steps would drop the platform pin without noticing: a manifest error today, and a silent switch off emulation once native ARM64 images ship. The quick start now writes COMPOSE_FILE into .env, so the rest of the file works unchanged on ARM64 -- verified both ways, with and without that line. Two things the setup needs stated where it is read rather than one hop away in the wiki: binfmt has to be registered on the host or the container dies with "exec format error", and emulation costs roughly 3-6x native slice time. Both now lead the section, and the separate-x86_64-box route stays the recommendation it was -- emulation is the fallback for people who have no second machine, not a replacement. The compose file's own header said ARM64 was a dead end. It now points at the override, for anyone who reads the stack instead of the README. --------- Co-authored-by: MartinNYHC <martin@bambuddy.cool> Co-authored-by: maziggy <mz@v8w.de> |
||
|
|
f3b6a503bd |
fix(profiles): read the companion files that hold a preset's real gcode
A bundled preset can keep a setting in `<preset> template <key>.json`, a file the preset itself does not reference -- the desktop slicer finds it by name. Walking only `inherits` never reached it, so every one of the 56 instantiable BBL machine presets resolved `machine_start_gcode` to the 577-character generic block on fdm_machine_common instead of its own 6.5-21 KB one. That block holds the M620 AMS load and the M1002 gcode_claim_action calls, so a print sliced from it heats the bed, moves the toolhead and extrudes nothing (bambuddy#2838). Companions are now folded into each ancestor as the chain is walked, at that ancestor's precedence, so a caller's own value still wins and the 0.2/0.6/0.8 variants reach their 0.4 sibling's companion. They are found by listing rather than by a fixed set of keys. Covered against the shipped bundle, not fixtures: a new e2e spec resolves all 56 presets inside the image and fails on any that still lands on the generic block. |
||
|
|
214617c3ea | Updated BACKERS.md | ||
|
|
a4795c5ca3 |
Let API keys read and run slicer pipelines (#1425 follow-up)
Every pipeline endpoint answered 403 for API keys whatever scopes the key carried. PR A parked all three permissions on the admin denylist until the run dispatch existed to decide about; it landed in PR C and the parking was never revisited. PIPELINES_READ now rides can_read_status. PIPELINES_RUN requires can_queue AND can_manage_library together, so the allowlist gained tuple values: a run slices into the library and then queues prints, and mapping it to either flag alone would hand that flag the other one's authority. The 403 names every flag the key is short of. PIPELINES_WRITE stays admin-only -- a key can run the recipe, not rewrite it or clear the log. Opening the run route also needed the cloud-owner fallback the direct slice route makes: a pipeline can carry Bambu/Orca Cloud presets, and resolving those reads a token off a user record that an API-keyed request does not have. retry_failed forwards the new dependency explicitly, since a direct call receives the Depends marker rather than None. |
||
|
|
8fe93169ca |
Remember the Printers page's status and location filters (#2833)
Pick a location, navigate away, come back, and every printer was showing again. Both filters were plain useState, and the only preferences on that page that were not remembered -- sort order, card size, view mode, collapsed sections and hide-disconnected all persist, each with the same initializer-plus-setItem shape. Give these two the same treatment. A saved filter needs a way out, though. The location dropdown is only rendered while at least one printer has a location, so a saved location that was later renamed or removed would match nothing and take its own dropdown off screen with it -- an empty page and no control to undo it. A location that is not among the available ones now resets to all, and the same for a status the dropdown does not offer. That check waits for the printers query to resolve. The list is undefined while it is in flight, so the available locations start out empty, and acting on that would throw the saved filter away on every page load. Search stays unpersisted: a box that silently refills itself on return is a surprise rather than a convenience. |
||
|
|
aff737999f |
Build the slice output's path from a name a folder can have (#2832)
A print's display name comes from inside the 3MF, not from the filename, so a MakerWorld title arrives with its punctuation: "Planter Pot with Drip Tray, 12 cm / 5 inches". The slice-to-archive sink used it verbatim for the output folder and the output file, and a slash in a folder name is not a character -- it is another folder. mkdir(parents=True) created the level it implied and the file's own join added a third that nobody had made, so the slice failed with ENOENT on a path that half existed. Renaming the print first was the only way through. Reduce a display name to a single path component before it becomes one. Characters a name cannot hold are replaced rather than dropped, so the folder still reads like the model's title, and the set is the one the SD card already rejects -- which covers a Windows install too, where the colon in "Model: v2" fails the same way. The name shown in Bambuddy is untouched: a title is allowed its punctuation, and refusing the slash would reject the name this was reported about. The joins are asserted to stay under the archive directory. That was already claimed by a SEC-PATH-OK marker on both lines, citing a sanitiser that is defined in another module and was never called here; without the marker the path-join backstop flags them both. The claim is now true, and a future edit that reaches around the reduction is caught rather than trusted. The library sink takes the same embedded name, so it gets the same reduction: managed storage names the file after a UUID and never saw this, but an external folder writes the name as given. Display names are also stripped of control characters on the way into the database, in the schema and in the archive service. The validator hands back anything that is not a string rather than iterating it, so the field still answers a list or a bare int with a 422 instead of accepting the one and failing on the other. Display names are also stripped of control characters on the way into the database, in the schema and in the archive service, cleaned before the filename fallback rather than after it so a whitespace-only embedded name still falls through to the filename. The validator hands back anything that is not a string rather than iterating it, so the field still answers a list or a bare int with a 422 instead of accepting the one and failing on the other. |
||
|
|
0623cc46df |
Repair no-3MF archives' photos and their silent filament writes (#1820)
Two faults behind the same kind of print: one that arrives without a
retrievable 3MF, which on an H2S is any job started from the printer's
own internal library.
Such an archive has no file_path, and Path("").parent is Path("."), so
every site that derived the archive's folder from it landed on the data
directory itself. The finish-photo capture spotted that and wrote to
<archive_dir>/<id>/photos instead. Nothing else did. The photo was
written in one place and looked for in another: reads 404'd, deletes
dropped the name and left the file, and the notification attachment
never found the image. Hand-uploaded photos worked only because upload
and read agreed with each other rather than with the capture. Give the
question one owner in utils/archive_paths and have all four sites ask
it. Lookups check the old shared location too, so photos already
uploaded there stay reachable; uploads now go where captures go.
Separately, the remain%-delta fallback that stands in for a missing 3MF
can charge nothing for several reasons, and did so without a word. The
AMS reading is coarse and, on the reporter's printer, noisy: it rises
mid-print, swings five points over a job, sits at 100% through a
36-minute print on a fresh spool, and goes negative on a nearly empty
one -- which the start-of-print gate rejects, dropping the only slot
that was printing. Two of their prints went uncounted for two different
reasons and both read as "no spools updated", which is also what a print
with nothing to charge prints. Name the slot and the two readings in
each case, on the Spoolman path and on the internal-inventory path,
which has carried the same gates since #1119.
The Spoolman path also had no notion of which slots the print used, so a
spool swapped into an idle slot mid-print reads as consumption and is
billed to whoever that slot is assigned to -- the fault #1269 fixed for
the internal tracker, still open here, and likeliest on exactly the
prints this fallback serves, where nothing else narrows the field. Use
the same three pieces of evidence it does: the print's mapping, its
mid-print tray changes, and the tray it started on. The last needs
storing, because the internal tracker's row is deleted before this runs
and a screen-started print has no mapping to fall back on -- hence a new
nullable column, and no backfill, since a row from before it existed has
nothing to say. Where no evidence exists at all, every slot is still
considered.
Both paths also treated tray_now == 255 as naming a slot. It does not:
it is the field's initial value, the fallback for an unparseable
reading, and what it reports with nothing loaded. Mapped as a tray id it
becomes (255, 1), so as the only evidence it excluded every real slot
and charged nothing at all -- this issue's own bug, arriving by a new
route. On the internal path that is live today; on the Spoolman path it
would have shipped with the guard above. The external holder reports 254
when it is genuinely in use.
The arithmetic is untouched: at one percent per step this cannot resolve
a small print, and pretending otherwise would be worse than saying so.
|
||
|
|
9a2b811566 |
Show the plug that powers the printer in the card's Power row (#2830)
A printer card has one Power row: a plug name, its draw, and the auto-off and on/off buttons. Which plug filled it was decided by nothing -- the endpoint returned the first row the database handed back that was not a Home Assistant script, from a query with no ORDER BY. For the reporter that was an enclosure exhaust fan, added before the outlet their X1C is plugged into. The card showed the fan's name with '--' for watts, offered to switch the printer off by cutting the fan, and demoted the metered outlet to the small HA button row. The fan was marked as not powering the printer and hidden from the card; neither setting was consulted here, though controls_printer_power has decided the scheduler's power-on pick since #2629. Rank the candidates instead: switchable at all, controls_printer_power, enabled, show_on_printer_card, reports power, lowest id. The first rules out a script, which can only be run, and an MQTT plug, which the control endpoint rejects as monitor-only -- and an MQTT plug is exactly the kind that reports watts, so without it ahead of the power tiebreak the row could land on a plug whose on/off button answers with an error. The last is not cosmetic: with no ORDER BY, a plain UPDATE on PostgreSQL can move a row and silently swap which plug the card calls the printer's power. None of these excludes a plug. A printer whose only plug is hidden, disabled or monitor-only still needs its Power row, because that row holds the on/off button and the HA buttons are drawn inside it. controls_printer_power sits above show_on_printer_card because the two only disagree when the plug that really feeds the printer is hidden, and letting a display preference win there points the power buttons at an accessory -- the fault #2629 fixed. Power capability is read from the configuration, not measured: this runs on every card render, and it is approximate both ways, so it only breaks a tie. The scripts endpoint shares the same pick and excludes it, so a switchable main plug is not repeated as a button directly below itself. A script is left in place: a printer whose only entities are scripts falls back to showing one in the power row, and taking it out of the button row too would cost it the one-click run it has always had. |
||
|
|
a9624d3887 |
Match a print completion to its queue job the way the printer names it (#2829)
Bambuddy has no run identifier to tie a completion to a queue row, so it
finds the row by printer and status='printing' alone.
|
||
|
|
71d506be0c |
Show the printer card thumbnail again after navigating back to the page (#2826)
The cover URL is cache-busted on the print name, which does not change while a print runs. Leaving the printers page and returning therefore re-mounts with a byte-identical src, which the browser serves from its in-memory cache with no network request -- which is why the reporter's network panel was empty while the placeholder sat there. `loaded` could only ever be set by onLoad, and a mount effect reset it to false unconditionally. For a cache hit those are two racing tasks with no ordering between them: when the load event won, the effect undid it, and nothing put it right afterwards because the URL does not change again for the rest of the print. Read the element's own complete/naturalWidth in that effect instead of assuming nothing has loaded. That settles it whichever task wins, and also covers the variant the reporter proposed, where the handler is not live in time. Being a race explains why it reproduced 100% for the reporter and not at all here. Only the printer card was affected; archive thumbnails build a fresh URL every mount, so they always hit the network. CoverImage is exported so the regression tests can mount it directly. |
||
|
|
fffa68ec55 |
Explain a print that never reached the printer's card, instead of sweeping for it (#2780)
Bambuddy reads a print's 3MF, cover and timelapse over FTPS on port 990, which on every Bambu model serves external storage only. Under some configurations H2-series and P2S firmware keeps the sliced file on internal storage, where Bambu Studio put it over the port-6000 service, and then no path on 990 can find it. The print command has always said which of the two it used -- `url` reads ftp://<name> or brtc://emmc/<name>. We discarded it and swept anyway: ~110 connections per print, all certain to fail, ending in an archive card with nothing on it and no stated reason. In the reporter's bundle all 35 dispatches to their H2C and P2S said internal storage, all 25 to their X1C said external, and all 44 empty cards belonged to the first two. Read the field, skip the sweep when it cannot succeed, and record which reason applied. A printer that uses the card is unaffected, and so is one we have no answer for -- silence is not evidence, and reading it as bad news would break archives that work today. The answer is held per print and dropped when that print ends, rather than kept as a standing fact about the printer. Plenty of prints never announce themselves: 14 of the 79 print starts in that bundle arrived with nothing on the request topic, started from the printer's own screen or picked up after a restart. Left standing, one slicer print to internal storage would suppress the lookup for every screen-started print after it, on a printer whose files really are on the card. The sticky reading is kept for the connection diagnostic alone, which is run after the print that prompted it and would otherwise have nothing to report. Two things that pointed the wrong way go with it. The archives banner told everyone to enable "Store sent files on external storage"; the reporter had it on for the whole three weeks and it would not have helped. The diagnostic passed a printer whose slot was empty, because it read only the toggle -- an empty slot is now a failure naming the slot, and a printer that has storage and still used its own is a warning. On P1-series that empty-slot failure yields to the existing unsupported-model skip: the toggle cannot be switched on there at all, so telling the operator to insert a card would promise a fix inserting a card does not deliver (#2524). Also close FTP sockets on the failure paths, which dropped them for the garbage collector -- 1813 in a day in that bundle -- and drop the advice to restart the printer, which the reporter tried twice while a single manual connection to the same printer handshook cleanly. This does not make the affected prints archive in full; that needs the port-6000 protocol tracked in #2762. |
||
|
|
3954d3a7e6 |
Choose which rack nozzle each filament prints from on an H2C (#1784)
The Vortek rack holds six hotends, and a multi-colour plate is sliced to
use a different one per colour so it can skip the purge. Which of the six
each colour takes is not in the 3MF. The same plate, sliced and sent twice
from Bambu Studio with a different choice each time, produces two files
that differ only in rounding in the last digit of a few extrusion figures
-- the filament grouping, the toolchange stream, the 120 nozzle-change
markers and project_settings.config are all identical. The choice travels
only in the dispatched nozzle_mapping.
Bambuddy had no way to state it, so those plates went out with no nozzle
assignment at all and the printer chose for itself. That is what levelled
on one hotend and printed with another, millimetres above the plate.
Every rack-bound filament now carries a position picker beside its AMS
slot dropdown, listing all six with the nozzle each holds. An empty
position, or one holding the wrong diameter or flow type, is shown greyed
out with the reason rather than hidden, so someone looking for position 4
finds it. The choice is per filament *group* rather than per slot, because
a group is one hotend: two filaments the slicer grouped together share it
and cannot point at different positions.
Nothing has to be picked. Positions are assigned automatically, preferring
one already loaded with that colour, which on the plate this was built
against reproduces Bambu Studio's own pick exactly.
A nozzle currently picked up onto the carriage is offered too. The
firmware drops its rack position from the report entirely rather than
sending a placeholder (#943), and refusing it would rule out the position
most likely to be wanted -- the one the last print left mounted. Only
recoverable when exactly one position is missing; two gaps are genuinely
ambiguous and stay unavailable.
Positions are re-checked at dispatch, not just when queued, because the
rack can be re-loaded in between. The two failure modes differ on purpose:
an explicitly chosen position that no longer fits stops the print, names
what the position now holds, and deletes the uploaded file from the SD
card so it cannot be started by hand either -- an operator who named a
hotend must not silently get a different one. An automatic assignment that
cannot be made instead falls back to letting the firmware choose, which is
what happened before any of this existed.
The pick is stored as {group: position} rather than as the expanded
nozzle_mapping, though that is what goes on the wire. That column means
"Bambu Studio decided, forward verbatim", and only the group-and-position
form can be re-checked against what is actually mounted at dispatch.
The existing multi-rack refusal in extract_nozzle_mapping_from_3mf stays.
It still guards the #2800 fallback, which can only ever name one rack id.
Measured on the maintainer's H2C: rack position n is physical nozzle id
15 + n, confirmed by cross-referencing two captured dispatches against
Bambu Studio's own dialog. extruder_max_nozzle_count names which carriage
is the rack straight from the file, and is read rather than assumed -- a
fourth independent confirmation of the carriage indices fixed in
|
||
|
|
4d458a5202 |
Grow the H2C nozzle rack with the printer card size
The six rack chips were a hard-coded 28 pixels at every card size, while the body type and icons around them scale by 20% at L and 40% at XL. Set the card larger and the rack stayed put -- a shrunken strip beside neighbours that had grown around it, with the diameter figures pressing against the edges of chips that had not moved. The chips now read their size from the same scale as everything else, which needed one new rung: the icon tokens run in quarter-rem steps (i2 is 8px, i3 12, i4 16, i5 20), so 28px is i7. S and M are unchanged, as they are for every other property that control scales. The card is sized to its contents, so it simply takes the extra width rather than being told a new one -- and at L and XL that row has the room to give, so the temperature readings beside it do not go back to wrapping. Two tests: one pins i7 to 33.6px at L and adds it to the sweep asserting every token is set at XL, the other asserts the chip reads the token, so a revert to a fixed class fails rather than silently regressing. |
||
|
|
45dc139c41 |
Print an H2C two-nozzle plate from the carriage it was levelled on
The two carriages were the wrong way round: extruder index 0 was treated as
the fixed hotend and index 1 as the swappable rack, and it is the other way
about. A plate using both was levelled with one nozzle and printed with the
other, several millimetres off the plate.
Three sources agree, and disagreed with the code. Telemetry reports
ams_extruder_map {'0': 1, '1': 0, '2': 0}. BambuStudio, dispatching a plate
that used all three of those AMS units, sent the filament from the unit on
extruder 1 to physical nozzle 1 and the ones on extruder 0 to rack positions
16 and 18, and that print completed. And the two constants could not both
have been right: _FIXED_NOZZLE_ID is 1 while the fixed extruder was 0, in a
scheme where physical nozzle id N sits on extruder N.
The old value came from the #2800 hardware A/B, where [17, -1, -1, 1] printed
in mid-air and [1, -1, -1, 17] printed correctly. That result stands -- it
established which wire worked. The extruder indices were not measured by it;
they were inferred by pairing the working wire with a slot_extruders list
produced by the 3MF reader that has since turned out to mis-read exactly
these files. The reasoning is recorded at the constants so a future
regression report is not re-litigated from scratch.
Also withholds nozzle_mapping entirely when more than one filament group
needs the rack. Two groups on one extruder means that extruder is a rack and
the plate wants a different hotend per group -- which physical slot each
takes is the slicer's choice against the live rack and is stated nowhere in
the file, since both groups can carry identical diameter and nozzle type.
Studio dispatched such a plate to 16 and 18; nothing here can reproduce that,
and answering anyway is what printed in mid-air, so the firmware picks.
Restores the fixed 32-entry padding that
|
||
|
|
d1c65a6659 |
Report a print stage we cannot name at the default log level
STAGE_NAMES is hand-maintained and every new model adds to it, so a printer occasionally reports a number that is not in it and the card reads "Unknown stage (72)" -- which an H2C did, where the table runs to 66 and then jumps to 74. Stage transitions were logged only at DEBUG, off in normal running, so the sole record that it had happened was the card itself, and by the time anyone looked the printer had moved on. The asymmetry is the point: a stage we can name is worth DEBUG, and the one we cannot is the interesting one. An unnamed stage is now logged at INFO, once per stage number per session, with the model, the stage it came from and the print state at the time -- which is what naming it afterwards needs. Named stages are unchanged, so a normal print logs nothing new. -1 is excluded: it is Bambuddy's own "not in a stage" sentinel and the field's initial value, so every print would otherwise report it on the way out of its last real stage. Fixes a latent crash found while testing this. The stage-change log line builds its text before the log level is consulted, so get_stage_name runs on every transition whatever the level is set to; a stg_cur that was not hashable -- malformed telemetry rather than an unknown stage -- raised TypeError out of STAGE_NAMES.get and aborted the whole state update. Labelling a value can no longer do that. |
||
|
|
dfeac792fb |
Stop an H2C refusing a multi-colour print as a hotend mismatch
The print uploaded, the printer took the command, and stopped at once with HMS 0500-4047 -- "the available hotend quantity or model does not match the sliced file". nozzle_mapping told the printer one of the plate's filaments went to no hotend while ams_mapping named the tray it comes from, and the firmware will not start a job on that contradiction. Each filament in a 3MF names the group it belongs to, and on every other dual-nozzle Bambu the group number is also the extruder index, so it was read as one. On a rack machine it is not: the rack carriage holds six hotends to the fixed carriage's one, so the slicer writes a group per nozzle rather than per carriage. The failing plate carried groups 0, 1 and 2 against a two-entry physical_extruder_map, and the filament in group 2 was dropped -- indistinguishable downstream from a slot the plate does not print, which is what reached the wire as -1. extract_nozzle_mapping_from_3mf now resolves the group through the table the file states for itself, the <nozzle id extruder_id> elements in slice_info.config. Files carrying no such table keep the direct index, so H2D slices are unaffected. A filament that still cannot be placed drops the whole mapping with a logged reason instead of half an answer: the firmware then picks its own nozzle, which is the pre-existing behaviour and far better than an answer that contradicts itself. Two related faults fixed in the same pass. The mapping was read across every plate in the file, so on a multi-plate project a slot took its extruder from whichever plate came last; it is now scoped to the plate being dispatched, in extract_filament_requirements as well. And the array is now one entry per filament slot, matching BambuStudio's own dispatch of [1, 16, 16] for a three-filament plate, rather than padded to a fixed 32. Verified against the file that failed: slot extruders [-1, 1, 0] became [0, 1, 0], and the wire [-1, 16, 1, -1 x29] became [1, 16, 1]. |
||
|
|
d0e217f65a |
Size the H2C nozzle rack card to its contents and number its slots
The rack card shared a row with the nozzle, bed and chamber readings but was set to flex: 2 1 190px -- a 190px floor plus twice their growth share -- to draw six fixed 28px chips. On anything wider than a compact card it claimed several hundred pixels and left most of them empty, and the width came out of the cards that needed it: the combined dual-nozzle reading was wrapping "220 / 220" onto two lines beside a mostly blank rack. It is now flex: 0 1 auto, so it takes its content width and gives the remainder back. Shrink stays enabled so it still gives way on a narrow card instead of overflowing. Each slot also carries its physical rack position 1-6 below the chip, so a nozzle can be named rather than counted along. The numbering is positional -- an empty slot keeps its number -- so "the nozzle in slot 4" means the same thing however many of the six are occupied. The #943 regression test read every span in the slot row, which the new number spans would have interleaved with the diameters; it now matches the diameter spans specifically. A second test pins the 1-6 labelling across a rack with empty positions. |
||
|
|
e6842e1d3c |
Rank near-colour matches by how they look, and share one filament type table (#2804)
Three follow-ups to #2804, all bearing on one decision: which spool a print uses when the exact colour is not loaded. Colour ranking is now perceptual. The ranking added in #2804 measured RGB distance, which rates a colour by how far apart the numbers are rather than how far apart they look, and it overweights blue badly enough to invert the answer: against a required #1E4821 green, a purple #38202F is the nearer of two eligible spools by RGB and four times the further once measured properly. Both sides now use CIEDE2000 -- perceptual_color_distance in backend/app/utils/color_utils.py and colorDistance in amsHelpers.ts, kept structurally identical so they can be read side by side. Verified against the Sharma/Wu/Dalal published reference set, all 31 pairs to 1e-4, and the two implementations agree to within 1e-9 across 800 sampled pairs. Eligibility is untouched, still the per-channel RGB box, so this only reorders spools that already qualified. Type matching now agrees between the interface and the scheduler. Bambu firmware treats PA-CF, PA12-CF and PAHT-CF as one material and the scheduler has always matched them accordingly, but the interface compared raw type strings and called that same pairing a mismatch. The badge contradicted what the printer was about to do, and the manual override picker, which groups by canonical type, offered the very spool the badge then rejected. The fifteen comparison sites in useFilamentMapping.ts, useMultiPrinterFilamentMapping.ts and PrinterSelector.tsx now call filamentTypesCompatible. The pipeline pre-flight reads the matcher's table instead of its own copy. That copy had drifted into disagreeing in both directions: it aliased PLA Basic to PLA where the matcher never has, so a run could clear the check and then fail to map its slots, and it lacked the nylon grouping, so it flagged runs the matcher handles without complaint. A check whose job is to predict dispatch is wrong whenever it disagrees with dispatch, whichever way it leans, so it and the scheduler now both read backend/app/utils/filament_types.py. That canonicaliser deliberately does not strip surrounding whitespace. It looks like a free improvement, but it would collapse a junk tray_type to "" just as a 3MF declaring no filament type yields "", and a typeless requirement would start matching a junk-typed tray instead of reporting the slot unmapped. Padded type strings are worth handling on their own terms, with that case addressed. One behaviour change outside the ranking: the pre-flight is stricter for a printer reporting a product name such as "PLA Basic" where the generic material belongs, which it now flags rather than passes. Rare in practice, since the printer reports material and product name in separate fields, and it is the answer the matcher would give. Nothing about which spool a print actually uses changed outside the colour ranking itself. Adds 203 backend and 6 frontend tests. The #2804 tie-break test now uses identical colours: two colours at equal RGB distance are not perceptually tied, which is rather the point. |
||
|
|
4f7a02b393 | Pick the nearest eligible filament colour instead of the first one in tray order (#2804) (#2823) | ||
|
|
36d996e453 |
fix(slicer): stop a 3MF from switching off supports its process preset turned on (#2820)
--load-settings is authoritative, so since #1881 four support fields travel the other way -- enable_support, the two filament slots, and support_type -- lifted out of the source 3MF and written over the picked process preset. Bambu's shipped presets all set enable_support: 0 because supports are a per-print decision, and without the carry a project exported with PVA in the interface slot sliced single-material. But the carry ran in both directions, and the off direction is the one nobody asked for. Nearly every published model ships with supports off, so slicing one against a custom preset that deliberately enabled them stripped them back out. The reporter's preset sets enable_support 1, support_type normal(auto), support_style snug; the slice came back disabled and tree(auto). Only the style survived -- it is not one of the four carried, and they had re-entered it in the slice dialog. The source can now switch supports on, never off. Nothing is lost: every shipped preset has them off, so a preset that has them on is a deliberate choice by whoever wrote it, and a file that wants supports still gets them with its slot assignments. A file that never declares enable_support is treated as off -- no intent to act on. The truthiness rule ("1", true, 1, and the forks that write neither) now lives in one place as supports_enabled_in_config(), shared with extract_support_filament_slots_from_3mf, which had it inline. Also log the carry with the fields it took. The slice dialog shows the picked preset's values, so a carried field silently disagrees with what was on screen and this step logged nothing at all -- the report chased an unrelated sanitiser line about the source file's own settings, which was the only thing in the log that mentioned any of these keys. |
||
|
|
02616f0c91 |
fix(queue): stop a library-file delete from destroying the jobs queued against it (#2819)
Nothing tied a library file to the queue rows pointing at it, and the FK that describes the relationship is ON DELETE CASCADE -- which SQLite does not enforce and PostgreSQL does. So the same fault had two faces: rows left pointing at a file that no longer existed, failing at the printer with "Library file not found" days later, or rows deleted outright with no error and no history. Two routes into it, both fixed by taking the queue off the file before the row goes. Dispatch (the reported case): quantity>1 on the printer-card upload-and-print flow puts cleanup_library_after_dispatch on every copy, and _clone_queue_item copies library_file_id onto batch clones, so the first dispatch consumed the file the rest were waiting on. The copies are now pointed at the archive that dispatch just created -- it holds its own copy of the 3MF -- and the consume flag is cleared on them. A copy already printing from its own archive keeps it, a finished one keeps its outcome, and a cross-model item (#671) keeps any candidate this does not consume. Deletion: the File Manager, bulk delete, folder delete, emptying the trash and the retention sweeper all removed rows with queued work against them. Folder delete did not even clear the cross-model candidates, because the file-id walk it already performs threw its result away. Jobs waiting on a deleted file are now cancelled at that moment, naming the file, and every other row referring to it is detached rather than destroyed -- print history and batch progress are counted from those rows. A job that is printing is left alone: what is deleted is the library copy, not the copy on the machine. The trash is reversible so it still changes nothing about the queue, and a job dispatched while its file is in the trash now says so instead of "not found". Verified row for row on PostgreSQL 16 as well as SQLite: without this, PostgreSQL deletes every queue row referencing the file. |
||
|
|
12e0a60e8b |
fix(ui): stop the Spool Inventory header from scrolling the page sideways (#2813)
Five header buttons in a row that could neither wrap nor shrink came to ~600px, so on a 390px screen the header ran past the viewport and took the whole page with it -- <main> is the scroll container, so everything inside it panned. Stack below sm and wrap the actions, matching the pattern the Statistics, Settings and Archives headers and this page's own filter bar already use. Identical at >=640px. The System Information header had the same construction with one button and gets the same treatment. |
||
|
|
454457a0af |
Attribute filament correctly when AMS backup swaps spools mid-print
Everything the completion path needs to split a print's filament across the trays it fed from lived only in memory: the dispatched plate and slot-to-tray mapping, the spool-assignment snapshot, and the tray-change log. A print that outlived a restart lost all of it and fell back to what the printer reports at completion -- which, with AMS Filament Backup on, is the substitute tray. The whole print was charged to the spool that only finished it while the spool that ran dry was charged nothing. Persist that context in a new active_print_sessions row, append tray changes as they happen, and restore both the session and the printer's tray-change log at restart recovery. Seed the log from the current tray when there is nothing to restore, since last_loaded_tray advances even when no change is logged. Rank the queue item's stored ams_mapping above the printer's live mapping field, which is what backup rewrites. Recover plate_id from the archive or queue item, and give extract_layer_filament_usage_from_3mf a plate_id instead of taking the first .gcode member -- a Bambu Studio export stores plate 2 first, so per-layer figures were measured against the wrong plate for both inventory backends. Stop auto-unlinking a spool assignment when its slot reports empty during a running print. At a runout the spool is still in the AMS, and dropping the link leaves the completion path nothing to charge. Capture the print-start context for both inventory backends. Spoolman's own durable row (#1820) carries its plate-scoped figures and dispatched mapping but not the tray-change log, and its slot assignments -- the way. Registration in _active_sessions stays gated, since on_ams_change reads it to decide whether to skip the remain%-based weight sync (#880). |
||
|
|
b5a34b7ba7 |
Do not close a queue item on a completion for another print
on_print_complete finds the row to close by printer and status='printing'
alone. The MQTT payload carries a subtask name but no run identifier, so
nothing tied the event to the row: any completion delivered for a printer
closed whichever job was printing on it. A job closed that way is marked
completed while the printer is still working, leaves the queue for
history, and strands the rest of its batch, because the queue correctly
refuses to dispatch onto a busy printer.
The handler now checks the completion against the file the row was
dispatched with, recovered from its archive, and leaves the row alone
when they disagree. Only a positive disagreement refuses: no archive, no
file name or no subtask name is unverifiable rather than wrong, and
refusing those would strand the item in 'printing' and wedge the queue --
the failure the loose lookup was avoiding in the first place.
This surfaced through the test suite, which could reach a real database.
conftest built its own SQLite engine, but core/config.py snapshots
DATABASE_URL at import time and core/database.py builds the module-level
engine and async_session from it. Tests reaching code that opens its own
session -- run_with_retry, which the completion path uses, takes its
sessions from core.database and so is untouched by the widespread
patch("backend.app.main.async_session") -- therefore talked to whatever
database .env named: the developer's own SQLite file on a plain checkout,
a live install with a PostgreSQL .env. DATABASE_URL is now redirected to
a throwaway file before any app import, and the run aborts rather than
starts if that did not take.
|