mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 11:25:48 +02:00
73e0787bfd650d4fafdd03b2faebe1a4e32b4fd0
389
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5211fd4575 |
Store a failure reason in one vocabulary, not three (issue #2974)
failure_reason was written three different ways and nothing reconciled
them. derive_failure_reason wrote English display labels ("Layer shift"),
older builds of the archive editor wrote the translated label in whatever
locale that user was running, and the two stale-archive paths wrote
English prose sentences. All three reach one column -- the archive PATCH
has mirrored the field onto the latest print-log entry since #1444 -- and
the Failure Analysis widget groups on the raw value, so one real cause
occupied several buckets. On a live install before this landed:
print_log_entries held 91 rows reading "User cancelled" beside 1 reading
"userCancelled".
In an English UI those two render as the same words twice with different
counts, which is why nobody spotted it. In any other locale one of them
stays English, because a stored label has no key for t() to resolve. The
editor was worse than cosmetic about it: its reverse lookup compared the
stored value against t() in the current locale, so for a non-English user
nothing matched and the dropdown opened empty over an archive that
plainly showed a reason.
The keys were already canonical and already enforced.
_FAILURE_REASON_KEYS in api/routes/print_log.py rejects anything else
with a 400 and explains why in its own comment -- the widget renders
values back through t(), so an unrecognised one surfaces as a raw string.
derive_failure_reason had simply never been held to that rule. It now
produces keys, and the cancel branch returns userCancelled.
The two "Stale - ..." sentences become one new noStatusUpdate key. Both
describe the same observation, that no end-of-print status ever arrived;
which of the two situations occurred is already carried by status --
cancelled at the stale-cleanup site, the reconciled outcome at the
reconnect site -- so collapsing them loses nothing and gives Statistics
one bucket instead of two sentences that could never be translated. It
had to enter the vocabulary rather than merely be tolerated, because the
editor discards any value it does not recognise.
Existing rows are converted by a startup migration folding 168 historical
labels onto the 12 keys across both columns. It is exact rather than a
guess: every label across all 14 locales resolves to exactly one key,
with no collisions. The map is a frozen snapshot rather than something
read from the locale files at run time -- it maps what was written
historically, so regenerating it from the current translations would
silently stop recognising the very rows it exists to convert. A value
outside the map is left alone; guessing would be worse than leaving one
honest string in its own bucket. There is no one-shot settings flag, on
purpose: the statement only matches values in the map and a key is never
a label, so it is self-terminating, and a flag would permanently skip
anyone who restores an older database.
The last part is a data-loss bug that was not in the report. The editor's
fallback to '' was not merely a wrong-looking dropdown -- the empty
selection was then saved over the stored text, so opening the editor on
an archive whose reason was free text and pressing Save destroyed the
classification. An unrecognised value now keeps its own option and
survives a save.
|
||
|
|
e5a18bf58b |
Key a K profile on its nozzle's flow type
A printer files each calibration under a nozzle id of the form HH00-0.4 (high flow) or HS00-0.4 (standard) and can hold both for one diameter -- a maintainer's H2D carries 102 high-flow entries against 6 standard -- because the same filament reads a different K through each. Nothing read that, so a standard-flow profile could be selected for a high-flow nozzle and vice versa. The flow is now stored with the profile, shown against each option in the picker, and checked before a stored profile is applied. Two spellings have to agree for that: a calibration entry says HH00-0.4 while the fitted nozzle reports HH01, so the comparison is two characters rather than four -- the trailing digits are a hardware variant the calibration table normalises to 00. Unknown flow on either side matches anything, which is what it has to do. Every profile stored before this has none. And an X1C declares none on any profile at all -- probed live, all eight come back with an empty nozzle id, against a four-digit cali_idx and a populated setting_id -- even though the machine really does take either nozzle. supports_nozzle_flow_type is therefore the wrong thing to gate on: it returns True for an X1C, and treating that silence as Standard would have dropped every X1C profile the moment a high-flow nozzle was fitted. What the printer's own table declares per profile is the test. NozzleInfo.nozzle_type carries two vocabularies by printer generation -- the nozzle material on legacy printers, the flow code on H2 -- and the comment claiming only the former is corrected. Anything that is not HH or HS reads as unknown, which is what makes the material spelling harmless. Storing both flows for one hotend and diameter is deliberately not done: spoolman_k_profile is UNIQUE on (spool, printer, extruder, diameter) with no flow column, and allowing a second row in internal mode alone would break inventory-mode parity. The picker marks a profile whose flow does not match what is fitted instead of letting it look configured while doing nothing. |
||
|
|
a7b563334e |
Configure a spool's filament preset and K profile per nozzle
A slicer preset is bound to a printer model: "Bambu PLA Basic @BBL X1C" is not the same preset as "@BBL H2C", and Bambu names a nozzle size in it as well. A spool carried exactly one, which was right until the same spool was used on a second machine -- the AMS slot on the other one was then configured with a preset that machine has no profile for. K profiles had the matching gap from the other side: the tables have always been keyed per hotend, but the picker could not express it. spool_filament_preset and its Spoolman twin store the exceptions, keyed (spool, printer_model, nozzle_diameter). Model rather than printer because the preset is a property of the model -- "@BBL X1C" is the same preset on every X1C, and asking per machine would mean picking the identical value twice. K profiles stay on printer_id, because a K value is measured on one physical hotend and two machines of the same model legitimately differ. Resolution is exact (model, diameter) -> (model, "") -> the spool's own preset, so a spool nobody has configured behaves exactly as it did before. The form writes one row per nozzle size and never the "" row; that level is kept for API clients wanting one value to cover a model. Both halves cover every standard nozzle size rather than the size currently fitted, because a spool is configured once and nozzles get swapped. The PA Profile tab becomes a Printers tab: a model list beside a detail pane holding a preset row per size and a K-profile grid of size by hotend. Each model is offered only the presets that name it, through the same matcher the Configure AMS Slot modal filters with, which moves out of that component into utils/slicerPrinterMatch. Presets whose name identifies no model -- most user-authored and OrcaSlicer ones -- stay offered everywhere, as does whatever is already selected, so a saved override cannot vanish from the control that shows it. Every preset carries an origin badge in the wording and colours that modal already uses. Every path that configures a slot now respects both: manual assign in either inventory mode, RFID auto-assign, the Spoolman tag link, the re-fire when a slot goes empty to loaded, the re-apply after a calibration-table refresh, and the re-selection when a Filament Track Switch moves an AMS to the other nozzle. Which nozzle a slot feeds, and how wide it is, was worked out independently in seven of those places, each reading nozzles[0] for every slot on the machine -- correct on a single-nozzle printer and on a dual-nozzle printer with matching nozzles, wrong the moment two sizes are fitted. That resolution is now services/slot_nozzle. Which array entry belongs to which hotend is no longer inferred. Measured on an H2D fitted with a 0.4 high flow on the left and a 0.6 on the right, nozzles[0] reads the right hotend, so the array is indexed by extruder id and the H2/X2 parser's convention is the one that holds. The legacy parser's opposite convention never governs a real dual-nozzle machine: every model in DUAL_NOZZLE_MODELS reports device.nozzle.info, and left_nozzle_diameter appears in no log or wire capture. Two comments that said otherwise were wrong and are fixed; amsHelpers' code was right all along and only its comment lied. Four defects surfaced while wiring it, all pre-existing except the last. The picker identified a chosen calibration by cali_idx alone, and the printer numbers its calibration table per nozzle -- on a dual-nozzle machine the same index exists on both hotends meaning different things, so saving could persist the other hotend's K value and diameter; SpoolBuddy's write-tag page carried a verbatim copy and gets the same fix. RFID auto-assign chose a K profile with no extruder test at all, so a spool calibrated on both hotends had a coin toss decide which pressure-advance value the slot got, on the path that runs unattended every time a Bambu spool is loaded. The Spoolman tag-link path resolved no preset whatsoever, configuring every linked slot with a generic material id and discarding a preset set in inventory -- the same defect #1713 fixed on the assign path, one function over. And an FTS inlet move re-selected K for nozzle 0 rather than for the nozzle the AMS had just been moved to. The last one is new here: a per-model override can be a cloud USER preset, whose PFUS-prefixed id the slicer rejects, and passing it straight into extrusion_cali_sel would silently lose the K-profile link. Reached the printer only where such an override exists, which is why nothing in the suite caught it. printer_safe_filament_id falls through to the spool's own preset and then the tray's RFID value instead. Reading a printer's calibration table asks for one nozzle size at a time. H2-series firmware answers only the first one or two of a concurrent burst of extrusion_cali_get and silently drops the rest, each dropped request costing a five-second timeout before its retry: measured at 11 and 23 seconds on an H2C and an H2D for four parallel requests, against roughly one second in series. An X1C answers all four at once, which is why this only ever surfaced on dual-diameter printers. Printers themselves are read in parallel -- separate machines are separate connections. The Configure AMS Slot dialog opens on the spool's own configured values, falling back to the slot's last manual configuration and then the tray's RFID data. The spool form is wider for the two-pane layout, colour, weight, cost and location move to their own tab in two columns, and a printer card in expanded view lists every fitted nozzle size rather than the first entry alone. |
||
|
|
59d2713acf |
Give a no-3MF archive the timelapse baseline it never took (issue #2957)
_capture_timelapse_baseline_at_start says in its own docstring that it must be called from every on_print_start path that proceeds to a real print, and what breaks otherwise: the completion scan falls back to snapshotting the card after the printer has written the video, so the new file lands inside the baseline and no diff can ever match. There are three such paths. The fallback branch was not calling it, and nothing else covered the gap -- on_print_running_observed is restart-recovery only and is suppressed whenever on_print_start fires. Every no-3MF archive therefore reached completion with no baseline in memory and none on the row, and kept its timelapse only by accident. Not confined to the reported cool-off. The same branch serves the internal-storage verdict, so every H2C/H2D/P2S print that Bambu Studio's Print button sends to eMMC lost its timelapse the same way, off a card that was holding it the whole time. Measured on a live install: 0 of 9 fallback archives had a baseline, against 73 of 275 normal ones. The branch now takes one like the other two, and last like they are: it lists the printer's timelapse directory, so a slow card must not delay the active-print registration, the energy reading, the archive-created event or the start notification ahead of it. The other half is a print shorter than the five-minute cool-off, where the card is still unreadable at the one moment a baseline has to be taken. list_files_async answers [] when its connect fails rather than raising, so that is indistinguishable from a card holding no videos. When the cool-off expired inside the 900-second poll window every video on the card read as new, the first in listing order won, and a stale unclaimed video was attached to the print and then deleted off the printer. The empty baseline is still recorded rather than refused. Bambuddy deletes each video once it is attached, so the usual card holds exactly one at completion and an empty baseline resolves it correctly; refusing outright would lose that common case to protect a rare one, and persisting NULL instead would send completion to snapshot a card that by then has this print's video on it. Instead the scan marks such a baseline untrusted and the attach step declines to choose between several candidates, leaving them for the manual Scan for Timelapse button. require_unambiguous defaults to off, so the only caller whose behaviour changes is that scan. |
||
|
|
9500c046c0 |
Ask which nozzle to feed when a Filament Track Switch is fitted
Load and Unload in the AMS slot menu did nothing on an H2C with the switch fitted. The ams_change_filament command carries an optional extruder_id and Bambuddy never sent it. That is correct on every printer without the switch, and is what BambuStudio does there too -- each AMS is wired to one hotend, so the firmware works the target out for itself and an explicit value would only be a guess at something it already knows. Fit the switch and every AMS is bound to one of its two inlets instead, either hotend is reachable from any slot, and a command naming neither leaves the firmware nothing to act on. It was discarded in silence. Load now asks which hotend to feed, on the same terms as Bambu Studio: no preselection, so a stray Enter cannot feed the wrong one, and the hotend already fed from that very slot greyed out. Printers without a switch send a byte-identical command and still load in one click. A switch fitted but not yet set up -- any AMS still unassigned to an inlet -- refuses the load up front rather than publishing one the firmware will drop, mirroring DevFilaSwitch::IsReady, which likewise demands a switcher position on every AMS. Unload was addressed at the same time. It was aimed with tray_now, a single value for the whole printer, so on any dual-nozzle machine with both hotends loaded it unloaded whichever that field happened to name regardless of which slot's menu was used. It now names the slot and resolves the holding hotend from device.extruder.info, previously read for temperatures only. That resolution is gated on the printer having reported two extruders: single-nozzle machines do send the block, but nobody has read a single-nozzle snow value off the wire, and staking every X1C, P1S and A1 unload on an unverified encoding buys nothing where tray_now is already unambiguous. Both new state fields ride the WebSocket and are in the broadcast key, and both are computed in the REST status route as well -- that response is what the page has before any push arrives, and leaving them at their defaults would have told a correctly set-up machine that its switch was not set up. Verified on H2C-1, AMS-A slot 3: loaded and unloaded from each hotend in turn, all four correct. Covered by 18 MQTT unit tests, 4 status-dict tests, 7 integration tests and 6 component tests. Two known stragglers, both deliberately left alone. Load on an AMS-HT slot has never worked -- an HT unit is addressed by its unit id rather than ams*4+slot, which these endpoints do not accept -- so unload there keeps the printer-wide form it always used instead of gaining a slot it cannot name. And a slot's K-profile still follows the AMS's plumbing rather than the nozzle just loaded, so loading to the far hotend leaves the other one's calibration bound; that is the same per-nozzle problem the filament and K-profile redesign is scoped to fix. |
||
|
|
14f46510ba |
File the printer's calibration table under the nozzle it belongs to (issue #2854)
The K value on an AMS slot card went blank after a while and came back after a backend restart. It is not the MQTT merge: that preserves a tray's k correctly. H2-series trays have no k to preserve. Verified against the H2 wire capture in logs/vp_wire -- every tray reports cali_idx and nothing else -- so the number on the card is resolved from that index against the printer's calibration table in state.kprofiles, and that table was a single global list. An extrusion_cali_get response is the complete table for one nozzle diameter, and the printer answers whoever asks; BambuStudio's queries arrive on the same report topic we subscribe to. Every response was assigned straight to state.kprofiles, so any one answer stood for the whole printer. The nightly GitHub backup asks for 0.2, 0.4, 0.6 and 0.8 in turn and finishes on 0.8, which holds nothing on a 0.4+0.6 machine: logs/bambuddy.log records exactly that at 17:15 on 2026-08-25, and the table was empty from then until something refilled it. Responses are now bucketed by the diameter they describe, so an empty answer for a size the printer does not have clears only that size. The three assign paths that look an index up by nozzle_diameter get the same fix for free -- they were quietly finding nothing whenever the last response was for another nozzle, which is what spoolman_inventory has been logging as a stale kp. Bucketing makes state.kprofiles a union, and cali_idx is numbered per nozzle, so the index alone no longer identifies a profile. The REST serializer has keyed on (extruder, cali_idx) since c5e005586; the WebSocket one still keyed on the index alone, which meant the first render of a card could be right and every update after it wrong. Both now share one resolver. It goes through the extruder the slot feeds, and where that does not single out one profile -- a single-nozzle printer that has been swapped, so both its tables sit under extruder 0 -- it falls back to which diameters are actually fitted. Where neither settles it the card shows nothing, because a blank space is a smaller error than confidently printing the other nozzle's number. Deliberately no loosening to a bare cali_idx lookup on a miss: that is the cross-nozzle bleed the extruder keying was added to stop. Nothing read the table on connect, which is the other half of the report. It arrived by luck -- a visit to Profiles or Configure Slot, a backup, or the printer answering someone else -- so a Bambuddy nobody had opened showed a card with no K values at all, and "restart and they come back" was the printer happening to broadcast rather than anything we did. It is now read once per connection, on the same latch the stale-print reconcile uses. Only the fitted diameters are asked for, one request on a single-nozzle printer and two on a dual; probing the four sizes blind is what the backup does and what blanked the table. The edge is gated on a nozzle diameter being known as well as on the state being known, because the first push_status is what makes the state known and does not always carry the nozzle fields -- latching there would spend the connection's one attempt on a printer that could not yet say what was fitted. Adopting an unsolicited table now logs at debug. It was the quietest way for the card to change underneath us and there was no way to see it in a support bundle. Three test files gained nozzles=[] on their PrinterState stubs. The field has always been on the dataclass; the connect edge is simply the first thing on that path to read it. |
||
|
|
08a58b1ef4 |
Keep both modes' slot assignments across an inventory mode switch (issue #2812)
Turning Spoolman mode on ran an unfiltered delete(SpoolAssignment) across every printer. Turning it straight back off cleared the other table instead, so the two directions were symmetric in code and one-way in effect, and the setting auto-saves on a 500 ms debounce with no save button and no confirmation. Opening the settings page to see what the option did was enough to destroy the configuration: the reporter's log shows four toggles in 85 seconds, which is someone looking and reverting, and the assignments never came back. The deletion was not careless. Checks that read both assignment tables would otherwise let a row in the mode you are not using answer for the mode you are, which is how #1473 was fixed, and emptying the inactive table made that impossible by construction. The cost was that the guarantee was bought with the user's data. That decision belongs to the readers -- the mode is a property of the install, not of the rows -- so spoolman_owns_assignments now answers it and nothing is deleted on a toggle. Each mode keeps its own assignments and switching is reversible. Existing installs need no migration: their inactive table is already empty, because it was being emptied. Six sites had to be told which mode they meant, and only two of them are the reads you would guess at, the missing-assignment notification and the queue cost estimate. The per-slot K-profile lookup consults the built-in table first and, on a hit with no matching profile, deliberately stops rather than falling through to Spoolman, so a leftover row would have shadowed the Spoolman binding for that slot -- the symptom #1556 reported from the other direction. configure_ams_slot *writes* a K-profile against whichever table answers first, so the same leftover would have filed a calibration against a spool the printer is not drawing on and never written the local one, leaving a calibration that appeared to succeed and then did not apply. The auto-unlink pass in on_ams_change is the one that would have made this change worthless. It drops any assignment whose tray no longer matches the fingerprint it recorded, and it ends in db.delete. Ungated, it would have removed the preserved rows one slot at a time as the AMS contents changed under the other mode -- the same loss, arriving slowly enough not to be connected to the toggle that caused it. The sixth is the built-in remaining-weight fallback inside the Spoolman AMS sync, and it is deliberately left inert rather than woken up. It could never fire while the table it reads was being emptied, it is keyed by slot rather than by spool, and create_spool writes remaining_weight unconditionally where the update path does not -- so preserving the rows would have seeded a stale figure into a brand new Spoolman spool the first time a tray reported an unusable remain%. The query stays, gated off, so the intent survives for whoever revisits the cross-mode fallback. Separately, a print that could not debit a spool said nothing about it, and that is what turned a mis-click into lost filament. The reporter's print was already running when they toggled. At completion it resolved its 3MF, read its per-filament grams, resolved its tray, and then skipped the debit because the assignment row no longer existed -- logged at INFO, invisible under the default log level, while the completion notification fired as usual. 65.49 g was never deducted and they only noticed because a spool's remaining weight looked wrong. _resolve_spool_id_for_tray has no tag or fingerprint fallback, so there was nothing else to catch it. The skip is now a warning naming the grams, and a completed print that failed to charge a tray it drew from raises the missing-spool-assignment notification. The print-start check cannot cover this and was right to stay quiet: the assignments existed when it ran. The two are different statements -- the first says the weight may not be tracked, the second says it was not -- so a print warned at start will notify twice, which is the right trade. Collected across the print rather than fired per slot, and given the caller's session, because this runs inside on_print_complete's transaction and opening a second one to read the printer's name would deadlock against it on SQLite. This is independent of the toggle and catches any other cause of an assignment disappearing mid-print. |
||
|
|
c3677865b6 | Give the AMS temperature alarm its own threshold (issue #2905) (#2943) | ||
|
|
54af3146a3 | [Feature]: Bind Home Assistant sensors to storage locations (dryboxes/bins) (#2827) | ||
|
|
4e79f9c2f7 |
Fill in a fallback archive when the 3MF finally arrives (issue #2957)
A failed TLS handshake pauses a printer's file service for five minutes, and the archive flow checks that pause at the top of its path loop and gives up before opening a connection. A print that starts inside one gets an empty fallback archive 13 milliseconds later, having never touched the network. Four minutes on, the pause clears and the cover endpoint downloads the same file, parses it, takes a thumbnail out of it, and publishes it to the shared 3MF cache under the exact key the archive flow looks up. Nothing ever looks: all three readers of that cache run before or during the print-start handler that already gave up, and print completion drops the cache as its first act, deleting the file. No path existed by which a fallback archive could become a real one. Offer a later 3MF to the running print's archive, filling the existing row rather than adding a second -- the row id carries the energy reading, the timelapse session and the start notification. That covers the reported case at no network cost, since opening the printer card already downloads the file. Schedule a bounded retry when the pause is what caused the fallback, spending the cache first and the printer only if that misses. Not scheduled for the other cause: a print kept on internal eMMC has no FTPS copy to come back for, and retrying it is the sweep removed in #2780. The two reasons are now recorded separately instead of both landing as "no 3MF". Recovery refuses anything that is not a readable 3MF -- a truncated download would replace an honest empty archive with wrong metadata -- and leaves an archive alone once it has a real file. Three things the recovery path has to get right, each with a test: Reduce every retry candidate to a bare name. MQTT hands `filename` over as "/data/Metadata/plate_1.gcode" on some firmware, and joining that onto the temp directory yields the absolute path itself, so the retry is fed the flow's own sanitised candidate list and strips the path again on its own account. Serialise recovery per printer. The cover endpoint's single-flight coalesces by view, so two views race each other, and the retry task and print completion can land on top of either -- each reads file_path == "" and runs a full copy, leaving the row on one timestamped directory and the rest orphaned. Keep looking for photos in the pre-recovery directory. `archive_dir` derives from `file_path`, so filling the row moves the archive's directory, and a photo uploaded to the empty card while the print ran stays where it was put. |
||
|
|
6988a30eae |
Carry a fault's description in the status response (issue #2926)
The HMS catalogue has been in the backend all along and the status response never carried it, so every consumer that wanted to tell a user why a print halted resolved the same 853 codes from its own duplicate of the same sentences -- this repo's Python table, the frontend modal's, and at least one third-party client whose catalogue exists purely because the server would not say. Each ages separately, and a relay watching a printer could only manage "your printer needs attention" while the server already knew it was "Filament ran out. Please load new filament." hms_errors[] entries now carry a description, defaulting to null so a client that has never seen the field is unaffected. It is resolved where the fault is parsed rather than at the boundary that prompted the request, because there are three serializers of a fault, not one: the status response, the WebSocket broadcast, and the completion payload the queue's failure reason is built from. Adding it to only the first would have handed half the feature to a relay watching the stream, which is the likelier consumer of the three. The queue's failure reason now quotes the resolved sentence instead of looking the code up a fourth time, and the notification path reads it rather than re-deriving. That they cannot report different text for one fault is the point, and a test asserts they agree. describe_fault is the single mapping from either code shape onto the table. An 8-char print_error is the catalogue's MMMM_EEEE key with the separator removed -- the parser derives full_code and that key from the same 32-bit value -- so it resolves exactly. A 16-char hms[] identifier is tried whole and then collapsed to its first and last groups. That collapse is lossy, and keeping it was the decision worth making carefully. #2728 counts 65 documented faults falling onto 0300_0001 alone, so a hit can attribute a neighbour's sentence to this fault, and refusing it looks like the stricter reading. It is not: the notification path, the queue's failure-reason helper and the frontend modal have all resolved hms[] faults this way for as long as they have existed, and it resolves real ones -- a 0500_4038 nozzle mismatch arrives in that shape. Declining to collapse would have stopped describing faults that are described today, silently suppressed the notifications they raise, and left this field null while the UI showed text for the same fault. Narrowing it belongs with #2728, where both key spaces can move together. So the lookup is exactly what it was, verified rather than asserted: a test walks every catalogue code in both fault shapes across all three alert levels and checks the result against the derivation this replaces. A future change to the lookup cannot quietly stop notifications firing. The catalogue ships one language, so the field is English and unlocalized, which the schema and the API reference both say next to it. The camwall feed is deliberately left alone -- it is code-only because its token travels in a URL on a screen, and a readable sentence discloses more than the camera picture already does. The frontend keeps resolving its own text: switching it would change what filterKnownHMSErrors counts across eight call sites, which is #1840 and #2728's argument to have. HMSError.message goes with this -- a text field that was never set or read anywhere, and an invitation to populate the wrong one now that a live description sits beside it. ----- Record a failure code the user can actually look up The queue's failure reason formats a fault's module and error into MMMM_EEEE, and that one derivation never masked the error to 16 bits. A fault arriving from the printer's hms[] array carries its alert level in the code's high half, so the label came out as 0500_24038 -- five digits in a group that has four. It is not a code anyone can find on Bambu's HMS index, and because it matches no catalogue key the sentence explaining the failure was dropped along with it, leaving the bare number alone. The nozzle-size mismatch behind #1111 is exactly such a fault. Reported one way it read "[0500_4038] The nozzle diameter in sliced file is not consistent with the current nozzle setting"; reported the other, the same physical fault read "[0500_24038]" and nothing else. There is already a helper that gets this right, used by the archive's own failure-reason lookup, so this calls it instead of keeping a fourth copy of the derivation. It also takes the raw integer code the MQTT payload carries, which the local version only handled as a string. |
||
|
|
7b181b84f0 |
Let a filled or foamed filament keep its own name (issue #2902)
The reduction that gave an AMS slot a material type read PLA-AERO, PLA-GF, ASA-GF and PPS-GF as their base material, so a slot loaded with foaming or glass-filled filament went out saying plain PLA or ASA. That is worse than the bug it replaced. "PLA-AERO" matched nothing before, which was useless but honest; "PLA" matches every PLA plate in the queue, so the dispatcher would have sent one to filament that will not print it -- and the contract the first fix claimed, that it could only ever repair a slot, no longer held. @doncaruana caught PLA Aero on the issue. All four are values Bambuddy itself offers: filament_fields.json is the material list the Profiles editor puts in a dropdown, and the reduction table was assembled from the cloud filament names and the frontend preset parser without ever being checked against it. It is checked now, so the next type added to one and not the other fails a test rather than a print. ASA-AERO joins them from the cloud catalogue (GFB02). The table hyphenates because the slicers do, while a spool says "PLA Aero" and every Bambu preset name says "Bambu PLA Aero". Adjacent words are joined and taken when the join is a type exactly -- exactly, because letting the prefix and suffix rules reach across a space would make "Support for PLA" a type by its tail. Also from @doncaruana, and the better half of his point: a preset is chosen from a list the slicer defines, so it already knows its own type and nothing has to be read out of a product name. The resolver now hands that answer back and both assign routes prefer it. It cannot be the only source -- material is required on a spool and slicer_filament is not, and the spool this issue was reported for had no preset at all -- so the reduction stays as the fallback for spools without one. Two things had to move with it. The auto-unlink guard compared the slot's reported type against the reduced material, so a spool whose preset outranked its material column would have been unlinked from the slot it had just been assigned to; it now accepts any type the assign path could have written. And two lookups keyed by material took the catch-all for a type they had no row for, which sent an ASA-GF spool out at 200/240 -- too cold to extrude -- and preheated its chamber to nothing. Both fall back to the base material last, so PLA-CF, PETG-CF and PA-CF keep the rows they are listed with, and ASA-CF and ABS-GF pick up ranges they had been missing all along. What counts as a material name is decided by the base for the same reason: saying yes throws the value away and rescues the slot from the generic-material fallback, so the answer has to be no when that fallback has nothing to offer. ABS-GF reduces to a generic ABS the printer can resolve; PPS-CF reduces to nothing and is left as it stands. Adding a type to the table therefore cannot quietly change that answer, which is how these five slipped through in the first place. ------ Hand the bundled chamber-preheat table back the way it is read Every lookup of the per-filament chamber map happens after the keys are upper-cased, and the parser documents exactly that: keys uppercased, DEFAULT always present so the resolution loop can index it unconditionally. The three fallback paths returned the bundled constant as declared, with the lowercase "default" row the Settings editor writes and displays, so an install that had never opened the setting got a dict the loop could not read its fallback out of and used a hardcoded 0 for any filament without a row of its own. It reported the right number only because that bundled default is 0. Raising it would have changed nothing for everyone who had not customised the map, with the map in Settings still showing the value that was not being used. The test that should have caught this asserted the fallback under either spelling, and its docstring contradicted itself between title and comment. It pins the contract now, over all four ways the parser can fall back. |
||
|
|
55cc64c87d | Add printer video downloads and range selection (#2853) | ||
|
|
88e8ca81c3 |
Give an AMS slot a material type, not a product name (issue #2902)
Assigning a spool wrote its material straight into the slot's tray_type. A slot that says "PLA+" satisfies nothing that asks for PLA: not OrcaSlicer, not Bambu Studio, and not Bambuddy's own dispatch matcher, which compares the type the printer reports to the one the 3MF declares as plain equality. The reporter's slot was unusable for every PLA plate he had. Not only a label, either. The same string went into the generic-filament lookup, which missed, so the slot went out with no tray_info_idx at all -- the half-configured state #2604 documents the printer as reverting from -- and took the 200/240 catch-all nozzle range instead of PLA's 190/230. PLA+ is not a special case. Bambuddy's own colour catalogue supplies the material dropdown, and around forty of its values are vendor product lines rather than filament types: HTPLA, PolyTerra PLA, PLA Matte, ASA Extrafill, Flexfill TPU 98A. So the four routes that configure a slot reduce the material to a name the printer knows before sending it, and the product name moves to tray_sub_brands -- which is where Bambu Lab puts it too: their catalogue carries a preset named "eSUN PLA+" whose type is PLA. A name the reduction cannot place is sent exactly as before rather than guessed at, so this can only repair a slot, never break a working one. The spool's own wording still leads the id and temperature lookups with the reduced type appended behind it, so "PETG HF" keeps its own generic preset (GFG96) rather than being traded down to plain PETG's. Two guards decide whether a candidate filament id is really a material name -- the resolver's, which discards one, and slot reuse, which will not carry one forward. Both saw only bare types, so "PLA+" passed as a filament id. They share one answer now, which also refuses to read an id-shaped value: "GFPLA" ends in a material name, and reducing it would throw away the calibrated preset in the slot. One thing had to move with it. on_ams_change auto-unlinks an assignment whose slot stopped looking the way it did when the spool was assigned, and the check that spares a slot Bambuddy itself reconfigured compared the printer's reported type against the spool's raw material. With the slot now carrying the reduced type, every spool this issue is about would have been unlinked from the slot it had just been assigned to. Both sides are reduced there -- the printer's too, so slots configured by an older version, still reporting "PLA+", keep matching. Something starts working as a result: a slot holding a calibrated preset is reused when a same-material spool is assigned to it, which could not happen for these spools while "PLA" and "PLA+" compared unequal. Reverting any one of the behaviours above fails a distinct test -- the reduction's four matching rules and its pass-through contract included, since that contract is what makes the rest of it safe. |
||
|
|
7c8f1f9435 |
Keep a dispatch's retries out of the FTPS cool-off (issue #2898)
A failed TLS handshake arms a 300s per-IP cool-off, and connect() consulted it for every caller. A print dispatch retries after 2s, so once the cool-off was armed all four attempts were answered from the gate rather than the network, and every further job queued for that printer failed the same way for the rest of the window. The reporter's farm lost three jobs to one handshake error, with the retry budget contributing nothing to any of them. The gate was serving two callers that want opposite things from it. The background sweeps -- the post-print 3MF, cover and timelapse fetches -- walk ~110 candidate paths against one wedged printer with nobody waiting, and backing off for minutes is right for them. A dispatch is one delete plus at most four upload attempts with someone watching a progress bar. So the split is by caller: a client built with respect_handshake_cooloff=False goes to the printer regardless, and the dispatch's delete and upload -- and a firmware upload, same shape -- opt out. Everything else keeps #2780's behaviour untouched. In the reported trace it is the pre-upload delete that takes the SSL error and arms the cool-off, 8ms before the upload's first attempt, so exempting the upload alone would have left one dispatch's worth of the problem in place. Callers that do respect the cool-off no longer sleep out a retry loop against it: with_ftp_retry takes the printer's IP and stops at the attempt that armed the gate, instead of spending three more attempts and six seconds on connections that cannot happen. It also reports the attempts it really made -- "failed after 4 attempts" for one attempt is part of how this read as a network problem. Two diagnosis fixes go with it. The cool-off skip was the one connect() failure path that reported without naming its cause, and at DEBUG, so four identical reason-free warnings were all the operator saw. It now says at WARNING that nothing was sent and how long the printer has left, once per cool-off rather than once per attempt -- not every caller is gated, and a download-zip of 200 files would otherwise repeat the sentence 200 times, which is the flood #2780 set out to stop. And a dispatch that fails this way no longer tells anyone to check whether the SD card is inserted and formatted -- nothing reached the printer's filesystem, so the card is the one part of the machine that was working. The message names the file service and rules the card out. It is used only when a handshake failed during the dispatch itself, read from the cool-off deadline MOVING rather than merely being armed: the dispatch ignores the gate, so it can be running underneath one an unrelated background fetch left behind, and blaming TLS for an upload that really hit a full disk would repeat the mistake in the other direction. Tests count sockets rather than return values, since "returned False" looks identical whether or not anything was attempted -- which is what made the original report a log dive. Reverting any one of the five behaviours above fails a distinct test. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
08df660f6c |
Replace the embedded G-code viewer with the slicer's own renderer
Sliced files previewed through a vendored copy of PrettyGCode in an iframe. It drew each move as a screen-space line -- a line has no thickness in the scene, so it cannot occlude the layer behind it, which is why prints came out stringy and shimmered where layers crossed. Being a separate app in a frame, it could be neither themed nor translated, and carried its own machinery for detecting a proxy refusing the embed. Now built on libvgcode, the renderer OrcaSlicer draws its own preview with, vendored from three-slicer (AGPL, same as us). It takes the THREE namespace as an argument and imports nothing, so it runs on our 0.181 rather than the 0.160 its package pins. The parser is ours; upstream renders its own kernel's output and ships no G-code parser at all. Two things it has to get right, both found by checking a real plate rather than assuming: - BambuStudio does not use the OrcaSlicer/PrusaSlicer annotations. It writes "; FEATURE:", "; LINE_WIDTH:", "; CHANGE_LAYER" and "; Z_HEIGHT:", not ";TYPE:", ";WIDTH:" and ";LAYER_CHANGE". Reading only the latter showed a 52-layer print as 23,165 layers in one colour, because with no layer marker recognised every travel Z-hop split a layer and every segment took the fallback feature. - It emits a tenth of its moves as G2/G3 arcs -- 706 extruding ones in a single plate. Ignoring them punched holes through curved walls and tree supports. Arcs with no X/Y are the helical travel lift and lay down nothing, so they interpolate as travels. Four colour modes: filament (default, from the AMS slots the file was sliced with), feature, layer height, line width. Speed, fan and temperature are deliberately absent -- upstream derives those from settings rather than the toolpath, and guesses dressed as measurements are worse than an honest omission. The parser now carries the data to do them properly later. Legend entries are switches. Hiding removes the records before the mesh is built rather than recolouring them: the shader packs colour into a single float with no alpha, so there is no transparent to set, and removal is the useful behaviour anyway -- a hidden support stops occluding what it covered. The scene is built once and only the toolpath rebuilds. Doing otherwise constructed a new WebGLRenderer on every render, because the buildVolume default is an object literal and so a fresh identity each time; browsers cap live WebGL contexts and drop the oldest, which blanked the canvas after a few interactions. utils/framing.ts goes with the iframe, along with six now-orphaned strings in all 13 locales. src/lib/vendor is excluded from eslint -- acting on findings in vendored code makes it impossible to re-copy on the next upstream release. |
||
|
|
bf525661d3 |
Three fixes on top of the billing branch, all found by running the suite against
both dialects rather than one. Postgres upgrades never got as far as the finance schema. database.py added on_billing_charge_failed with BOOLEAN DEFAULT 1. The 1 is a SQLite-ism; Postgres answers DatatypeMismatchError, and _safe_execute deliberately re-raises anything that is not an idempotency error, so run_migrations died there and rolled the whole transaction back. No finance tables, no columns, and the app does not start. Six lines above, the same change gets is_voided right with an is_sqlite() branch, so this was an oversight rather than a decision. Now branched the same way. This also explains the four test_security.py::TestBackupKeyFiles failures reporting "column print_archives.cost_center_id does not exist". That column's migration exists and works -- it simply never ran, because every startup aborted before committing. Reproduced against Postgres 16 by building a pre-billing schema from dev and upgrading over it: fails without this, completes with it, and re-running the migrations or starting from an empty database are both clean. test_billing_run_id_migration.py failed on any Postgres-configured checkout. It builds its own SQLite engine, but run_migrations branches on the global dialect rather than the connection in hand, so on a box whose DATABASE_URL points at Postgres it emitted md5(random()::text) and btrim() into SQLite. Given the same fixture test_ldap_migration.py already carries for exactly this reason. The suite now agrees across dialects -- 9190 passed either way, where it used to be 9184 on one and 9183 on the other. The kill switch could not tell a print Bambuddy started from one it merely watched. Authorization fell back to a print_archives row in status="printing" matched on subtask_id. But on_print_start archives every print it observes, including ones started from Bambu Studio or Handy, and stamps them with the same status and subtask_id -- the code says as much where it notes "a print Bambuddy didn't dispatch". So a foreign print became authorized the moment its 3MF finished downloading, and _active_prints was rehydrated from it, making that permanent. The switch fired only inside the download race, and never afterwards. Neither test caught it: one stubs the authorization call to False, the other stubs the query to return an archive, so the real lookup was never exercised against a foreign print. Authorization now requires a marker Bambuddy writes itself: billing_run_id, minted per dispatch in the scheduler, or created_by_id carried over from the queue item. Failing that, it looks for a queue row in status="printing" on that printer -- committed before the MQTT send, and the only durable trace a library-file dispatch leaves, since those have no archive at send time and the row created for them moments later carries neither marker. That row cannot be tied to a subtask_id, so it defers rather than authorizes. Deferring also closes a false positive the previous version shared: a restart in the window between the send and the download left no archive at all, and a Bambuddy print was stopped as unauthorized. Stopping a print is irreversible and declining to act costs a log line, so ambiguity resolves that way. Tests cover an unmarked archive not being authorization and not entering _active_prints, either marker alone authorizing and rehydrating the fast path without touching the queue, an unmarked archive with a live dispatch deferring, a dispatch not yet archived deferring, and nothing at all being unauthorized. |
||
|
|
406cf71149 | Merge branch 'dev' into feature/billing | ||
|
|
91acac2b35 |
Stop retrying a printer whose FTPS handshake fails, and name the cause (#2780)
Two printers went on printing while every archive they produced held nothing but a filename. Bambuddy opened port 990, the printer accepted the connection and answered with something that was not TLS, and connect() logged a warning and returned False -- indistinguishable, to every caller, from "the file is not at this path". So the 3MF lookup walked all six filename variants across five directories with four retries each, the cover endpoint ran its own sixteen-path sweep, and the timelapse scan added four more, all against a sixteen-path sweep, and the timelapse scan added four more, all against a printer that could not have answered any of them. One reporter's log carried 1813 identical handshake failures, another's 3511. The evidence says this is the printer's own file service getting stuck, not a model, firmware or TLS-configuration problem. In #2780's bundle the same two printers ran clean from 22 July to 4 August and failed again from the 5th; a second bundle shows an X2D serving files for five days, flipping on 19 July, then failing every connection for eight days with zero successes. The same models and firmware appear in roughly twenty other bundles with no occurrences at all. Both bundles show it happening with cap_tls_v1_2 in effect -- the X2D and H2C entries in ftp_profiles were added on analogy with P2S to fix exactly this symptom, and the reporter's own debug line proves they do not. An ssl.SSLError from connect() now opens a five-minute cool-off for that printer. Subsequent connects return False without touching the network, so a wedged printer is contacted twice an hour instead of hundreds of times a minute, and the single warning that is logged names the remedy. The cool-off is dropped on expiry rather than kept, so the map holds one key per currently wedged printer. ftps_handshake_blocked() lets the sweeps stop: the 3MF lookup abandons the remaining paths and skips the directory-walk fallback, the cover endpoint returns 503 naming the file service instead of a 404 that reads as "this print has no thumbnail", and the timelapse scan separates 503 (cannot reach the printer) from 404 (no timelapse directory) -- one 500 used to cover both, which is what the reporter hit when reproducing. The Connection Diagnostic completed a bare TCP connect to 990, which is why it reported the port green throughout: the port is open, it is what is behind it that is broken. It now completes a real implicit-TLS handshake using the model's own ftp_profiles cap, so a pass means the FTP client would also get through. An open port that cannot negotiate reports warn with reason no_tls, selecting a new message in all 13 locales that points at a printer restart rather than at the firewall. No login is attempted, so this stays valid in the pre-save Add Printer flow. The cool-off tests run against a real socket that accepts on 990 and replies with a plaintext FTP banner, reproducing WRONG_VERSION_NUMBER rather than mocking ssl. The autouse fixture clearing _mode_cache now clears the cool-off map too -- every test here talks to 127.0.0.1, so one left behind would make the next test's connect() a no-op. |
||
|
|
dd1d40b0d4 |
implemented pr (worth fixing) feedback
update commit |
||
|
|
9a397f46e9 | implemented pr feedback #2 | ||
|
|
43bf854bd5 | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
684a328d6f |
Broadcast AMS slot changes that keep the same material
Configuring a slot from the printer card left the card showing the old filament until a reload or the 30s fallback poll. The command reached the printer and the printer applied it; the update just never got broadcast. on_printer_status_change deduplicates WebSocket pushes against a status_key whose AMS part carried id, tray_type and state. Configure Slot writes none of those -- it writes tray_info_idx, tray_color, tray_sub_brands and cali_idx. So PLA to another brand or colour of PLA produced an identical key and was dropped, while PLA to PETG came through. Reset always worked because it clears tray_type. Those four fields only move when someone configures a slot or swaps a spool, so this costs no broadcasts mid-print. remain stays out of the key for the opposite reason. |
||
|
|
cd004df817 |
Show Home Assistant sensors on the printer card (#1148, #448)
Binds binary_sensor and reading-carrying sensor entities to a printer and renders their state on its card, worded by Home Assistant's device_class. Optional per-sensor alert condition drives a notification on the transition into the alert state and an opt-in interlock that holds queued prints while alerting -- a hold with a readable waiting_reason, never a failure, and only ever on a sensor that was read successfully. Sensors get their own table rather than a wider entity pattern on SmartPlug: get_smart_plug_by_printer would otherwise hand the card's power button a door contact to switch. The hold is passed to the model matcher directly rather than merged into busy_printers: _check_auto_drying reads that set as "is currently printing" and would put an idle-but-held printer down the mid-print drying path. The notification_providers migration spells its default FALSE, not 0 -- Postgres rejects an integer default for a boolean and _safe_execute swallows the error. |
||
|
|
33ab5f1ead |
Add temperatures to the streaming overlay and a URL builder (#1422)
The overlay at /overlay/{printer} draws live print data over a
full-screen camera view for OBS, a wall display or any browser source.
It has been tunable since it shipped -- which fields, what size, what
frame rate -- but only through query parameters documented in the wiki,
and temperatures were not among the fields on offer. The request asked
for temperatures first and for the field set to be selectable in the web
UI; this addresses both.
Nozzle, bed and chamber readings join the list. The target is drawn only
while the heater is still climbing, so a settled hotend reads "220°C"
for the rest of the print instead of the noisier "220 / 220°C" -- 219.6
against a target of 220 rounds to the same number, and repeating it says
nothing. Both nozzles appear on a dual-nozzle machine. They are drawn
whether or not a print is running, because a preheating printer is
exactly when they are worth watching, and each reading appears only when
the printer genuinely reports one: chamber temperature stays absent on
P1 and A1 models, which publish a chamber_temper with no sensor behind
it, so the overlay never puts a measurement on screen that does not
exist. Labels reuse the heater chart's strings rather than inventing a
second vocabulary for the same three things.
The feed sends an allow-list rather than the temperatures dict. That
dict doubles as the MQTT client's working memory -- derived heater flags
and private target-set timestamps live alongside the readings -- and an
overlay token is a narrower grant than a login, so it gets exactly what
the overlay draws and does not pick up fields as the dict grows. The
same chamber-sensor gate the full status payload already applies is
applied here. The integration test that asserts the payload's exact key
set, which exists to catch that surface widening silently, is updated
deliberately.
Temperatures are not in the default field set, so an overlay URL already
pasted into a scene renders identically after upgrading.
Settings -> API Keys -> Streaming Overlay now builds the URL: printer,
field checkboxes, size, frame rate, camera toggle, an optional token,
and a copy button. It persists nothing and calls nothing new -- the URL
is the configuration, which keeps a scene reproducible by copy-paste and
lets two displays show different fields off one token. Fields are
emitted in the overlay's own top-to-bottom order rather than click
order, and parameters left at their default are omitted, so the same
selection always produces the same URL. The preview alongside it stays
off until asked for: an always-live iframe would hold a subscriber on
the printer's single camera connection for as long as the settings tab
stayed open.
The preview needed one narrow security-header change. Every SPA route
sent frame-ancestors 'none', which is stricter than the SAMEORIGIN in
X-Frame-Options beside it and refuses even a same-origin frame, so the
preview showed Firefox's "another site has embedded it" page instead of
the overlay. The overlay path now sends 'self', mirroring /gcode-viewer,
which admits a framer only on this origin -- Bambuddy's own UI. Every
other path keeps 'none', and embedding the overlay from another host
still requires TRUSTED_FRAME_ORIGINS.
|
||
|
|
71a06f3638 |
Add batch orders with a quantity per plate (#342)
Printing a multi-plate file in different quantities per plate meant queueing each plate separately and tracking the counts by hand: one shared Quantity field cannot say "plate 1 once, plate 2 twice, plate 3 three times". Each selected plate now carries its own quantity, and the submission becomes an order on a new Batches tab. The point is the distinction the old flat batch could not express. print_batch_plates stores how many runs of each plate were wanted, separately from what was queued, so a run that fails, is cancelled or is skipped does not satisfy a target -- the order goes on saying it owes a print instead of quietly under-delivering. Queue remaining re-queues exactly what is missing, for the whole order or one plate, by cloning the most recent item for that plate: that inherits the printer or model target, AMS mapping, filament overrides and print options along with the validation they already passed, rather than re-serialising twenty fields through a template that would drift from the model the first time someone adds a column. Clones append to the end of the relevant printer's queue and take the same advisory lock the add-to-queue route does; positions are per-printer sequences, not global. Cost is measured, not estimated. print_log_entries gains queue_item_id, set where the queue item is already in scope, so each run's material and energy are attributed through the item that produced them -- an unrelated reprint of the same archive never lands in an order's total, and a multi-plate order gets each plate's own cost rather than the whole file's via the plate-scoped estimate from #2614. Before any run has completed there is no honest figure, so cost reads as unknown instead of a fabricated 0.00. The Batches tab wires up GET /queue/batches, which has been unreferenced since the batch MVP shipped, along with six locale keys that were translated and never used. It is a separate tab because an order outlives the queue that produced it: once its runs finish they leave the active queue, so Queue and History each hold half the picture. completed was not a reachable status before now, so every batch created since April is still marked active however long ago its last print finished -- 73 of them on the development install. A startup pass closes out the finished ones: those whose runs all completed become completed, and groupings whose items were all cancelled become cancelled, which is what they are. Not applied to orders, which state their intent independently of their runs and still owe the work. Only batches with nothing queued or printing are considered, and repeating the pass also catches an order whose last run landed while the process was down. Batches with neither items nor targets are no longer listed at all -- empty shells left when a grouping's items went with their source archive. Dispatch applies the same source-file gates as POST /queue/. It creates queue items, so without them it would be a weaker door to the same outcome; the archive and library-file checks move into shared helpers so a third route cannot drift from them. |
||
|
|
a9b57ccd3c |
Add variant-group endpoints and cross-model queue creation (#671, #2570)
Adds /library/variant-groups for declaring that several sliced files are the same job for different printers, and a variants payload on queue creation that turns such a set into one queue item with a candidate per file. The candidate set is validated as a set: one file per printer model, each file sliced for the model it is offered as, and at least one model with an active printer. A cross-model item deliberately holds no file of its own, because print_queue.library_file_id is ON DELETE CASCADE and would destroy the whole job when a single alternative is deleted. Fixes internal printer-model codes never being resolved on queue create and update: normalize_printer_model returns unknown input unchanged, so the or-chain never reached the code map and a "C13" target matched no printer and waited forever. Skips candidates whose file is trashed or missing. Library deletes are soft, and SQLite runs with PRAGMA foreign_keys off, so neither case is covered by the schema; the hard-delete paths now also drop the rows. Adds library_files.variant_target_model so a user can say which printer a file without slicer metadata is for, kept out of file_metadata so the assertion is never mistaken for parsed data. |
||
|
|
21d61c3535 | Merge branch 'dev' into feature/oidc-env-config | ||
|
|
037688514a | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
3abab1fd45 |
fix(printers): recover MQTT sessions that stopped reconnecting (#2732)
The reporter's printer lost its session to a keep-alive timeout at 02:19 and did not come back until 11:24 — nine hours offline, with the web UI open throughout. check_staleness() was never going to catch it. Its first line is `if self.state.connected and self.is_stale()`, so it only ever handles the half-broken session that is still connected but has gone quiet. This client had connected=False from 02:19:42 (the offline notification fired a minute later), so every call returned immediately, and paho's own retry was the only thing left watching. When that stopped making progress nothing noticed. Adds a sweep every 60s that rebuilds a client when all four hold: it is disconnected, it had a working session before, it has been silent for five minutes, and its MQTT port still answers. The port check is what keeps this from becoming a nuisance — a switched-off printer is left to paho, so a farm powering down overnight causes no client churn and no log spam. The five-minute grace sits well past the 60s stale timeout and the 30s max reconnect backoff, so a session recovering on its own is never interrupted. The rebuild goes through force_reconnect_stale_session from async context, which takes the hard-reset path: fresh client_id and paho's QoS 1 queue dropped, so a project_file left unacked on the dead session cannot replay into the new one and trip 0500_4003 (#1136). Rate-limited per printer, cooldown cleared when the printer returns, and the sweep continues past a client that throws rather than abandoning the rest of the farm. The log line names how long the printer was gone and the last connect error, so a session that dies repeatedly leaves a trail. check_port gains a public alias in printer_diagnostic rather than having the watchdog reach for the private name. Also corrects the Developer Mode path added in the previous commit: the wiki documents it under Settings > Network, not Settings > General. The menu path is dropped from the translated string entirely, since it varies by model and firmware and the wiki carries the detail. |
||
|
|
db6cdb0745 |
fix(camera): take the finish photo when the print ends, not when its last layer starts (#2547)
The photo fired the moment layer_num reached total_layer_num. That edge is where the printer *starts* its final layer, not where it finishes it: the reporter's H2C capture shows it arriving at 92% with mc_remaining_time=2, three minutes and seventeen seconds and one filament change before the print actually ended, so the frame caught the toolhead mid-print over the model. The trigger also latched _finish_photo_captured, which locked out both the stage-22 and FINISH triggers for the rest of the print — so on firmware that never reports an end-of-print filament unload (H2C and A1 Mini confirmed) nothing could replace the bad frame. Remove the last-layer trigger. The photo is now taken at the FINISH-state trigger, which every model sends and which lands after the toolhead parks. Since Bambu's end G-code drops the plate ~100mm just before that, restore the framing before capturing: absolute G90/G1 Z to max_z_height + 10mm clearance, settle, capture, then drop it back so the print is as reachable as the printer left it. Absolute is the safety argument — that Z is a height the toolhead occupied seconds earlier, so it is inside the travel limits by construction and leaves the nozzle above the part, and it is unambiguous across model families because Z is the nozzle-to-bed gap whether the bed moves or the toolhead does. M211 is never touched (#2579). This is what #1145, #1397 and #1565 asked for. The height is only trusted when two independent sources agree: the archive is matched by the finished print's subtask_name by equality (not LIKE, so "Cube" cannot resolve to "Cube v2"), and its layer count from the 3MF must match the layer count the printer reported over MQTT. Matching on "most recent archive for this printer" was not safe — on_print_complete pops the _active_prints binding concurrently, and a print Bambuddy failed to archive would have resolved to its predecessor. A wrong height is the one failure that could drive the nozzle into the model. The move is additionally skipped when the print height is unknown, when a queue item is pending for the printer, when the printer has left FINISH, and when the new finish_photo_restore_plate setting is off. for every FINISH-state capture — which is what shipped the mid-print photo — the bank is used only when the dispatcher recorded that it injected End G-code into this print, since a SwapMod snippet may have ejected the plate. The flag is handed over in two steps (mark_pending at dispatch, adopt at print start) so it can never outlive its print: a job started from the slicer or SD card adopts False rather than inheriting its predecessor's answer. Those prints also skip the plate move outright, bank or no bank. The bank now refreshes on mc_percent advances as well as layer changes, via a new on_print_progress callback. Layer changes stop the instant the final layer begins, which left the #1867 fallback frame stale by the whole length of that layer; progress keeps ticking there and freezes before the End G-code runs, so a swapped plate still cannot reach the bank. The last-layer throttle exemption is dropped, since it would now fire a grab on every percent tick. On the timelapse path the moment producer returns early, so the consumer does the restore itself before its live-grab fallback — the documented usual outcome on P1-series, where the video has not transferred by the time the notification goes out and the shipped photo was of an already-dropped plate. The two waits are now derived from the settle window and the video poll timeout rather than hardcoded; at the old flat 75s that fallback was guaranteed to be cut off mid-settle. extract_max_z_height_from_3mf reads only a bounded prefix of the plate G-code, since a sliced plate is routinely tens of megabytes and the header is ~40 lines. It returns None for missing, unparseable, zero and negative values so callers must treat "don't know" as such rather than defaulting. |
||
|
|
cd7b869419 | Merge branch 'dev' into fix/camera-rotation-finish-photo-timelapse | ||
|
|
432e956eff |
fix(camera): rotate every still exactly once, and cover the sources that let ffmpeg write the file
Review follow-ups on applying camera_rotation to finish photos and layer-timelapse frames. Rotating the frame popped from _stage22_finish_frames rotated one of its sources twice. The cache has two kinds of feeder: live grabs, which are raw, and the #1867 in-print bank, whose bytes come from _capture_snapshot_for_notification and have already been rotated on the way in. The consumer cannot tell them apart, so on the finish_state trigger - the path the bank exists to serve, on firmware that never emits stg_cur=22 - a 180 degree rotation cancelled itself out and the photo was upside-down again, which is the reported symptom exactly; 90 and 270 landed 180 out. Rotation now happens where each frame is captured, so every entry in the cache carries one rotation whatever produced it, and the invariant is stated both where the cache is declared and where it is consumed. Two finish-photo sources were still writing unrotated files: the built-in camera's own capture_finish_photo, and the still extracted from a printer-recorded timelapse - which is the *preferred* source for a built-in camera print, so a user with a rotation set got a correctly oriented photo or not depending on which source happened to win. Neither ever holds the frame as bytes; ffmpeg writes the file and they return a filename. apply_camera_rotation_to_file handles that case and is best-effort - a failed rotate leaves the unrotated file rather than losing a delivered photo. The archived video itself is the printer's own file and is not re-encoded, so it still plays at the camera's native orientation; the CHANGELOG says so rather than leaving it to be discovered. apply_camera_rotation logs at debug, not info. It was on a path that runs once per layer, where a tall print would have put hundreds of lines in the log for something the surrounding capture already reports at debug. The moved rotation logic had no test of its own - every existing test patches it out and asserts the call, so a flipped sign or a dropped expand=True would have shipped green. test_camera_rotation.py drives the real round trip: a corner marker pins which way it turns, the dimensions pin that the frame is not cropped, and an undecodable frame comes back by identity because a capture path must not lose a frame to a failed rotate. Tests for the fix itself sit on both sides of the cache. The producer half is driven directly; the consumer half is a closure nested inside on_print_complete with nothing able to reach it, so it is pinned by an AST guard - checked against the source because the alternative is no check at all. Reverting main.py to the pre-fix shape fails three of the five, the guard among them. The three new tests used Path("/tmp/test") for a patched base_dir, which Bandit flagged (B108); they take tmp_path now. |
||
|
|
c3448dae91 | Merge branch 'dev' into feature/oidc-env-config | ||
|
|
449060ce0a | bugfix (aborted prints were not billed in specific cases) |