mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
feature/orca-cloud-plugin
158
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6e8d543d2a |
fix(ams): stop showing the humidity drop index as a percentage (issue #3140)
Bambu sends two humidity fields that are not the same quantity. humidity_raw is relative humidity in percent; humidity is a 1-5 drop index, and it runs the other way -- OpenBambuAPI's push_info sample pairs "humidity:30%" with "humidity_idx:4", so a high index means dry where a high percentage means wet. Four call sites used the index whenever no percentage arrived. A unit sending only the index therefore rendered as "2%" in the green band while being the second-wettest of the five steps, charted an average of index values as a percentage, and sat under every humidity threshold forever, since no index can reach one -- the alarm and auto-drying could not fire for such a unit at all. - utils/ams_humidity: one leaf helper, a percentage or None. The index is never converted; None is what every caller already handles. - routes/printers, printer_manager, print_scheduler, main, bambu_mqtt: all five readings go through it, so the card, the websocket, the chart, the alarm and auto-drying cannot answer differently. - main: a unit that reports the index and no usable percentage says so once per unit in the log, with its firmware versions requested. No supported printer is known to do this, and "known" is doing work there -- the alternative is a card that goes blank with no trace. Three faults found while checking what else those paths touched: - main: humidity_raw=float(x) if x else None stored NULL for a numeric 0% while writing 0.0 to humidity on the same row. - main: that same expression was unguarded, unlike the parse above it, so a non-numeric humidity_raw raised inside record_ams_history and aborted the pass for every printer, not just the one that sent it. - routes/ams_history: the averages were tested for truthiness, so a window averaging exactly 0 reported no average while the min and max beside it reported 0.0. An affected unit now reports no humidity rather than a number that means the opposite: the indicator is hidden, the chart leaves a gap, the alarm and auto-drying skip the unit. Temperature is untouched. Auto-drying's outcome is unchanged either way -- an index could never cross the threshold -- so only the intent moves. No supported printer is known to be affected; the report came from an install running X1Plus, which Bambuddy does not support. Verified against 7927 recorded samples from seven AMS units including an AMS-HT: not one used the fallback. Two percentages that did fall through to the index no longer do -- a reading with a decimal point, and "38.0", which int() rejected. |
||
|
|
48244bcfcf |
fix(queue): tell a pinned queue item why it is waiting (issue #3074)
A job queued as "Any X1C" explains itself when it cannot start: the model-based branch builds a reason for every candidate printer and puts it on the row, so the queue shows "Busy: X1C-01" or "Waiting for filament: X1C-02 (needs PETG)". The same job pinned to one printer showed nothing. It sat at Pending with waiting_reason NULL for as long as that printer was busy, which from the outside is indistinguishable from a queue that has stopped working -- the reporter watched fourteen minutes of it while his X1C ran a print he had started from its own screen. The fixed-printer branch had six ways out and none of them wrote the field. The sensor interlock (#1148) was its only writer, and it cleared the field up front on every pass where no sensor was holding the printer, so NULL was not an oversight on those paths but a guarantee. Every exit now writes, through one helper. The reasons reuse the model-based branch's vocabulary so _is_busy_only() keeps deciding what is worth a notification: a printer that is printing, drying, or working through the item ahead of this one reads as "Busy: <printer>" and stays silent, because it resolves itself. A printer that is off with no Auto On plug, and one whose plug could not switch it on, are worth saying. A finished plate nobody has acknowledged is split out from plain busy and named as itself. _is_printer_idle() returns the same plain False for that and for a running print, but they are not the same thing to the person looking at the queue: one clears itself and the other needs somebody to walk over to the printer. That notification fires on the transition into asking, where a busy-only reason counts as not asking. Testing whether the item was waiting at all -- which is what the model-based branch does -- would never fire it here: nobody's queue goes straight from idle to an unconfirmed plate, it waits behind the print first. The cost is that a printer dropping offline, returning busy and dropping again asks twice rather than once. The interlock stays silent. It has never sent this notification, and a change about what the queue displays is not the place to start. Clearing the field up front is gone with it. It existed so a shut door could not leave "Waiting on Enclosure Door" standing while the printer stayed busy with something else, and the new rule carries that guarantee instead -- whichever exit runs next overwrites it, and the dispatch path clears it. Two paths clear it that the report did not mention. A staged item and a future-scheduled one skip before this branch and never reach it again, so anything written on an earlier pass would outlive its condition for the life of the row. That includes the filament-deficit check, which stages the item itself. The notification is wrapped: a queue that cannot say why it is waiting is the bug being fixed, and a queue that stops dispatching because a provider timed out would be a worse one. On the frontend, the queue timeline drops any pending item carrying a reason, on the grounds that such an item will not auto-dispatch. That held while only the model-based branch wrote the field; "Busy: <printer>" is now the commonest reason there is, and it describes the very chain the timeline forecasts, so the rule would have emptied the view for anyone whose queue is pinned. It now asks whether the reason needs the user, via a small shared reader of the same shape the scheduler encodes. Which job goes out, and when, is unchanged: running the previous scheduler and this one over the same 768 states dispatches the same items in the same order with the same statuses, across 1452 rows that now carry a reason. |
||
|
|
5be18ff57a |
fix(queue): print a plate whose filaments are all on the external spool (issue #3087)
The reporter's P1S heated up, sat at Heatbed preheating for ten and a half minutes, then paused with 07FF_8012, "Failed to get AMS mapping table". Resuming only reheated it. Prints that fed from the AMS were fine. The plate was one filament of a seven-filament MakerWorld project, mapped by hand to the external spool. slice_info.config numbers filaments across the whole project, so the mapping for that plate is [-1,-1,-1,-1,-1,-1,254]: six placeholders and the spool holder. The command builder decides whether a print needs the AMS by asking whether the mapping is entirely external, and six -1s answer no. So the print went out as use_ams=true carrying a flat mapping of nothing but -1 -- 254 is deliberately never sent raw, the firmware reads it as AMS tray 0 -- which is exactly the mapping table the firmware then could not find. The builder cannot fix this itself. Down there a -1 is either padding for a filament this plate does not print, which is BambuStudio's own convention, or a slot that never resolved to a tray, and sending the second one to the spool holder is what #2589 exists to prevent. They are the same byte. The scheduler knows. extract_filament_requirements drops every filament with used_g <= 0, so it names precisely the slots the plate prints. When all of those are an explicit 254/255, dispatch now sends use_ams=false and the print runs. When one of them resolved to nothing, the flag is left alone and the firmware rejects the print as it does today -- deliberately, because that is the case where guessing would print a filament in the wrong material without saying so. Single-nozzle only, mirroring the reconcile in the command builder: on a two-extruder printer use_ams selects which nozzle to feed rather than whether to use the AMS, so an H2D with a spool on each side must keep the flag it was given. Judged generously from the model name and from live telemetry -- a second nozzle reporting a diameter, an extruder map, or more than one external feed -- because a wrong yes only preserves existing behaviour while a wrong no would reroute the print. H2S stays single-nozzle (#1386). Nothing in the command builder changed. Its own reconcile keeps the exact semantics #2589, #2595 and #797 gave it, and now usually agrees with a decision that was already made one layer up. Where the file has no parseable filament list the mapping is left exactly as before, the same evidence-only convention as #2771, and the parse itself sits behind a check for anything external at all so an AMS-only print never opens the file. Covered end to end at the dispatcher, including the reporter's seven-filament shape, a plate mixing the spool holder with an AMS tray, a consumed slot that never resolved, both external feeds on a dual-nozzle machine, and a 3MF with no filament list at all. |
||
|
|
4440738904 |
fix(drying): dry a composite spool as its base material (issue #3067)
The reporter's AMS-HT would not auto-dry PA6-CF, and drying the same spool by hand worked. The scheduler reduced a tray to a preset key by splitting on spaces only, so "PA6-CF" stayed "PA6-CF", matched none of the eight rows the preset table has, and the tray was read as holding nothing worth drying. The AMS was then passed over on every scheduler sweep, silently, because every caller reads "no row" as "nothing to do for this tray". It was never only nylon. Of the 41 types a printer can report, 33 had no row under that rule, and 20 of those have a base material sitting right there: every -CF, -GF and -AERO variant of PLA, PETG, ABS, ASA, PC and PA. Doing it by hand worked because the drying popover has resolved composites since exact key first, so a row the user added for the exact type still wins, then the suffix, then an alias map reading PA6, PA11, PA12, PAHT, PPA and Nylon as PA. PPA is the one alias that is a judgement rather than a spelling. Polyphthalamide is a distinct polymer, not a grade of nylon -- but it is an aromatic polyamide, it takes up moisture the same way, and PA's row is the hottest the table has. A material with no row and no alias is still skipped rather than dried at a number nothing here can source. That is where this parts company with the popover, which falls back to PLA because a dropdown has to show something. The table is user-editable JSON, so a preset row can be present and empty. That has always meant "skip this material" and still does: the temp and hours reads fall back per field to 55C/12h, which would dry a PLA spool at 55 degrees. Two more callers had the same line and move with it: per-filament humidity thresholds, where the override set for a material never applied to that material's composites, and the chamber preheat target, which had the suffix half of this from #2902 but not the aliases. The preheat test for that half reimplemented the lookup inline rather than calling it, so it would have passed whatever the function did. It calls it now. |
||
|
|
069ee8fc87 |
fix(queue): skip preheat entirely when no loaded filament wants a chamber (issue #3041)
Preheat & Heat Soak delayed every PLA print by five to seven minutes and gave nothing back. The filament map correctly derived a chamber target of 0, and the chamber phase correctly skipped -- but the stage then heated the bed, waited for it, and held the full soak anyway, because the soak had no idea it was holding for a chamber nobody asked for. The print's own G-code sets the bed the moment it starts, so the bed phase only moved the warm-up ahead of the FTP upload instead of overlapping with it. A 0 that comes out of the filament map now skips the stage before any command goes out. The one thing the skip still does is put the airduct flap back to cooling on the models that have one -- an H2D left in heating mode by the ABS job before it would otherwise cook the PLA that follows, and that costs one MQTT command and no waiting. Explicit instructions are untouched. A chamber target of 0 typed into a print's own override still heats the bed and runs the soak, which is what the queue documentation has always promised it does, as does forcing a print's Preheat override to On. Prints that want chamber heat are unaffected, including the P1S/P1P/A1 tier where the bed and the soak timer are the whole mechanism. The existing unit tests all ran with soak_seconds=0, which is why the production default was never exercised; the PLA test now runs at the real default and asserts nothing is dispatched and nothing is slept. Surfaced in the UI on the way through: the Settings hint claimed the derived 0 skipped "the chamber phase", and the per-print chamber override field said nothing about a typed 0 meaning bed-only -- a user reaching for 0 to turn preheat off got the delay instead. |
||
|
|
09b4584d5f |
fix(queue): say when an unscheduled item runs instead of calling it ASAP (issue #3018)
The print dialog offers ASAP, Queue and Schedule. ASAP and Queue differ only in where the item is inserted, and neither is stored on the item -- scheduleType is a frontend-only concept, and grep finds no "asap" anywhere in the backend. So the queue's time column had nothing to read but scheduled_time, and labelled every unscheduled item "ASAP": the name of the one mode the user may well have chosen against. Someone who picked Queue then watched their row appear as ASAP and start immediately, and concluded Bambuddy had overridden them. Two reporters wrote that same sentence thirteen months apart, and #2557 was closed as A2L-specific after the first of them -- kilrah replied there with an X1C before filing this. The column answers when an item runs, so it now says that. The key is renamed whenFree rather than just retranslated: left called asap, the next translator puts ASAP back. The dispatch is unchanged, because it was right. A print scheduled for later does not reserve the printer until then; an unscheduled item behind it uses the idle printer rather than leaving an X1C dark until 6 AM. Two of the new tests pin that, so it does not get "fixed" later on the strength of a report like this one. What genuinely could not answer the question was the queue's own log. Its per-printer line called every entry in busy_printers "not available" -- but that set holds both printers that cannot take work and printers the pass has just claimed for some, which are opposite facts. It also read printer state at logging time rather than at the decision, so #3018's bundle carries Queue: printer 1 not available — connected=True, state=IDLE, ... Launching 1 upload(s) (pool 0/4 in flight) Starting queue item 18 a printer reported unavailable, evidence that it was available, and a dispatch to it, in three consecutive lines. It is the first line anyone greps for "why did my item not go out". Each of the nine sites that removes a printer from a pass now records why, and the summary reports a claim as a reservation and everything else as an obstruction with its reason. The live fields stay, since a bundle reader wants them next, but are labelled as read now rather than offered as the cause. print_scheduler.py:1210 already documented that these two meanings differ -- the dispatching_printers snapshot exists for it. This carries that distinction into the log. |
||
|
|
28d386f766 |
Derive the chamber target from the trays the print loads (issue #2886)
Preheat took the maximum chamber target across every loaded AMS tray, with no reference to the job. The reporter's P2S holds PETG Pro, PLA, ASA and PETG; the ASA row of the filament map says 45C, so a PLA-only plate was dispatched with chamber_target=45C, the bed driven to 90C to reach it, and the full 900s max-wait plus 300s soak burned before the upload started. Every time -- a P2S has no chamber heater and its chamber tops out around 33C, so the wait can only ever end on the timeout. Their log carries fifteen of these. The intent was never in doubt. The resolution order documented one screen above _derive_chamber_target reads "PLA-only print derives 0 -> chamber phase auto-skips", but it was implemented as PLA-only AMS rather than PLA-only print, and only misfires on a mixed load. The derivation now reads the trays the item's ams_mapping names -- the same array the print command puts on the wire, [-1, -1, -1, 1] in their case, addressing exactly the PLA slot -- so the ASA two slots over contributes nothing and the stage skips outright. Multi-material prints are unaffected: the maximum is still taken, across the trays the plate actually loads, so an ASA the print does use is still binding. An item whose mapping is missing or still unresolved keeps the whole-unit scan. That is the only signal left, and narrowing to nothing would disable preheat for prints that need it -- the failure mode worth avoiding here is the silent one. The bed hold between jobs is gated on the same derivation and was holding beds at 90C for the same wrong reason. It now reads the next item's mapping too, where that item has one. The external spool is no longer invisible to this. The scan only ever looked at raw_data['ams'], so an ASA print fed from the external feed derived 0 and got no preheat at all; a mapping naming 254/255 is now honoured. An item with no mapping still derives from the AMS alone, so nothing starts preheating that did not before. Tray addressing matches _build_loaded_filaments, which is what produced the ids in the mapping being read back: ams_id * 4 + tray_id, the bare unit id for an AMS-HT from 128, and the firmware's own vt_tray id for an external feed. Ids are coerced because this firmware reports them as strings. |
||
|
|
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. |
||
|
|
70ee53464d |
Say why an upload failed instead of blaming the SD card (issue #2899)
Every dispatch upload that failed carried one sentence: "Failed to upload file to printer. Check if SD card is inserted and properly formatted (FAT32/exFAT)." The reporter got it after a TLS handshake failure and restarted the printer on the strength of it. That could not have helped. The handshake never reached the printer's filesystem, and the cool-off that made the next dispatch fail the same way lives in Bambuddy's own memory, where power-cycling a printer does not reach. because the advice was known not to work. It survived in the string people actually read, so the failure mode that fix closed was still reachable through the UI. reachable through the UI. The information was never missing. connect() separates five failure classes and upload_file() separates 553/552/550, each with its own log line -- 553 even logs a spelled-out list of storage causes -- and then both returned a bare False. The dispatch had nothing left to work with and guessed storage for all of them. So the reason now travels with the result. The client records an FtpFailure (kind, detail, and the reply code where the server gave one) on every failure branch, and upload_file_async fills in a report object the CALLER owns. Not a per-IP dict beside _mode_cache: those describe a printer and are right to share, while this describes one operation, and a background timelapse fetch running beside a dispatch would overwrite the dispatch's reason with its own -- reporting the wrong cause with total confidence, which is this bug again rather than a fix for it. describe_upload_failure() picks the wording, and lives next to the kinds so the two cannot drift. A 553 or 552 keeps the card advice, which is the case it was written for, and quotes the reply code so a queue entry and a support bundle line up. A handshake failure says the file service answered without TLS and that the card is not involved. A refusal points at the access code, a timeout at the network, and anything unclassified says so and points at the log rather than picking a plausible cause. A wrong instruction costs more than a vague one: it sends someone to work on hardware that is fine. Nothing prescribes a power cycle. The failure notification now carries the same sentence the queue shows, instead of its own fixed "Failed to upload file to printer", so a push and the screen cannot disagree about what happened. Tests cover the classification, the report reaching the caller through the retry loop, two callers not crossing, the queue entry itself, and one line per message branch -- without that last one, deleting the access-code or timeout branch left every other assertion passing, since they only check that the card is not named and the generic message satisfies that too. The card-advice test keys on the instruction rather than the words "SD card", because the handshake message names the card in order to rule it out. |
||
|
|
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. |
||
|
|
4961990a7a |
Let a print start on a nozzle the printer can fetch (issue #2885)
The nozzle-diameter guard compared the sliced diameter against the mounted hotends alone. On an H2C both hotends commonly read the same size, so a job sliced for anything else was failed before upload with "install the matching nozzle before printing" -- even with that nozzle sitting in the tool-changer rack, which the printer fetches by itself as part of starting a print. The reporter saw it as an asymmetry: going to 0.4mm always worked, going to 0.2mm never did, and only fetching the nozzle by hand on the printer's own screen let the print run. It was never only about 0.2mm -- with a 0.6mm docked and 0.4mm hotends, a 0.6mm slice was refused identically. Placement is what made it fatal rather than merely wrong. The guard runs near the top of _start_print and the rack picker near the bottom, so the item was failed before the code that would have chosen the dock ever ran. Bambuddy had the rack contents the whole time. nozzle_info carries an entry per nozzle -- ids 0/1 for the hotends, 16-21 for the docks -- each with its diameter, so the guard now tests the slice against both sets. It keys off the ids rather than the printer model: only a rack machine reports 16-21, which leaves no registry to keep in sync. An empty dock is absent from the payload entirely, so an id appearing there already means a nozzle is in it. stat is left uninterpreted; it read 0 on every entry, occupied and empty alike. A diameter in neither a hotend nor a dock still stops the print before it uploads, and the message now names both sets so the machine's real stock is visible. The same telemetry showed a second fault, pointing the other way. A hotend with nothing mounted is still reported, keeping the diameter of the nozzle it last held -- measured at idle, where the rack-side hotend read 0.4mm with max_temp 0 and serial "N/A" after parking its nozzle back in the dock. That stale value counted as installed. Presence now comes from the serial and the temperature rating, and emptiness has to be stated rather than merely unstated: the serial must be the firmware's explicit "N/A" and the rating absent. A firmware that reports neither field normalises to exactly that, and reading it as empty would switch the guard off on that machine. Confirmed on an H2C with 0.4mm hotends and a 0.2mm in R6: the job dispatches with nozzle_mapping [21], chosen by Bambuddy rather than left to the firmware. |
||
|
|
dd50c51c1b |
Stop waking printers that cannot print the queued job (issue #2876)
The queue's smart-plug step chose a printer to switch on by model alone. With a class-targeted job and every matching printer off, it walked the farm in printer-ID order, woke the first machine with an Auto On plug, and only then read the loaded filament -- so a job for a colour loaded at the far end of the farm woke every earlier printer in turn and left each one running until its own auto-power-off timer expired. The colours were known the whole time. A printer keeps its last reported AMS and external-spool trays after the power goes; mark_power_off blanks connected and state and leaves raw_data alone. The wake step now asks the same three questions the matcher asks a live printer -- required types, forced colours, preferred colours -- of that reading, and passes over a printer it rules out. A printer with no reading at all is still woken: never having heard is not the same as nothing being loaded. The matcher reports such a printer as needing filament rather than as offline, so the waiting reason explains why nothing was switched on. The manager now keeps a printer's last tray reading when it drops the client, and the queue falls back to that. _power_on_and_wait calls connect_printer in a retry loop, so an attempt that timed out used to erase the reading the next one depends on. The record is kept beside the clients, not inside one: the AMS merge is additive, so feeding it back into live status would merge an unplugged AMS unit back in for good. |
||
|
|
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> |
||
|
|
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.
|
||
|
|
3954d3a7e6 |
Choose which rack nozzle each filament prints from on an H2C (#1784)
The Vortek rack holds six hotends, and a multi-colour plate is sliced to
use a different one per colour so it can skip the purge. Which of the six
each colour takes is not in the 3MF. The same plate, sliced and sent twice
from Bambu Studio with a different choice each time, produces two files
that differ only in rounding in the last digit of a few extrusion figures
-- the filament grouping, the toolchange stream, the 120 nozzle-change
markers and project_settings.config are all identical. The choice travels
only in the dispatched nozzle_mapping.
Bambuddy had no way to state it, so those plates went out with no nozzle
assignment at all and the printer chose for itself. That is what levelled
on one hotend and printed with another, millimetres above the plate.
Every rack-bound filament now carries a position picker beside its AMS
slot dropdown, listing all six with the nozzle each holds. An empty
position, or one holding the wrong diameter or flow type, is shown greyed
out with the reason rather than hidden, so someone looking for position 4
finds it. The choice is per filament *group* rather than per slot, because
a group is one hotend: two filaments the slicer grouped together share it
and cannot point at different positions.
Nothing has to be picked. Positions are assigned automatically, preferring
one already loaded with that colour, which on the plate this was built
against reproduces Bambu Studio's own pick exactly.
A nozzle currently picked up onto the carriage is offered too. The
firmware drops its rack position from the report entirely rather than
sending a placeholder (#943), and refusing it would rule out the position
most likely to be wanted -- the one the last print left mounted. Only
recoverable when exactly one position is missing; two gaps are genuinely
ambiguous and stay unavailable.
Positions are re-checked at dispatch, not just when queued, because the
rack can be re-loaded in between. The two failure modes differ on purpose:
an explicitly chosen position that no longer fits stops the print, names
what the position now holds, and deletes the uploaded file from the SD
card so it cannot be started by hand either -- an operator who named a
hotend must not silently get a different one. An automatic assignment that
cannot be made instead falls back to letting the firmware choose, which is
what happened before any of this existed.
The pick is stored as {group: position} rather than as the expanded
nozzle_mapping, though that is what goes on the wire. That column means
"Bambu Studio decided, forward verbatim", and only the group-and-position
form can be re-checked against what is actually mounted at dispatch.
The existing multi-rack refusal in extract_nozzle_mapping_from_3mf stays.
It still guards the #2800 fallback, which can only ever name one rack id.
Measured on the maintainer's H2C: rack position n is physical nozzle id
15 + n, confirmed by cross-referencing two captured dispatches against
Bambu Studio's own dialog. extruder_max_nozzle_count names which carriage
is the rack straight from the file, and is read rather than assumed -- a
fourth independent confirmation of the carriage indices fixed in
|
||
|
|
dfeac792fb |
Stop an H2C refusing a multi-colour print as a hotend mismatch
The print uploaded, the printer took the command, and stopped at once with HMS 0500-4047 -- "the available hotend quantity or model does not match the sliced file". nozzle_mapping told the printer one of the plate's filaments went to no hotend while ams_mapping named the tray it comes from, and the firmware will not start a job on that contradiction. Each filament in a 3MF names the group it belongs to, and on every other dual-nozzle Bambu the group number is also the extruder index, so it was read as one. On a rack machine it is not: the rack carriage holds six hotends to the fixed carriage's one, so the slicer writes a group per nozzle rather than per carriage. The failing plate carried groups 0, 1 and 2 against a two-entry physical_extruder_map, and the filament in group 2 was dropped -- indistinguishable downstream from a slot the plate does not print, which is what reached the wire as -1. extract_nozzle_mapping_from_3mf now resolves the group through the table the file states for itself, the <nozzle id extruder_id> elements in slice_info.config. Files carrying no such table keep the direct index, so H2D slices are unaffected. A filament that still cannot be placed drops the whole mapping with a logged reason instead of half an answer: the firmware then picks its own nozzle, which is the pre-existing behaviour and far better than an answer that contradicts itself. Two related faults fixed in the same pass. The mapping was read across every plate in the file, so on a multi-plate project a slot took its extruder from whichever plate came last; it is now scoped to the plate being dispatched, in extract_filament_requirements as well. And the array is now one entry per filament slot, matching BambuStudio's own dispatch of [1, 16, 16] for a three-filament plate, rather than padded to a fixed 32. Verified against the file that failed: slot extruders [-1, 1, 0] became [0, 1, 0], and the wire [-1, 16, 1, -1 x29] became [1, 16, 1]. |
||
|
|
e6842e1d3c |
Rank near-colour matches by how they look, and share one filament type table (#2804)
Three follow-ups to #2804, all bearing on one decision: which spool a print uses when the exact colour is not loaded. Colour ranking is now perceptual. The ranking added in #2804 measured RGB distance, which rates a colour by how far apart the numbers are rather than how far apart they look, and it overweights blue badly enough to invert the answer: against a required #1E4821 green, a purple #38202F is the nearer of two eligible spools by RGB and four times the further once measured properly. Both sides now use CIEDE2000 -- perceptual_color_distance in backend/app/utils/color_utils.py and colorDistance in amsHelpers.ts, kept structurally identical so they can be read side by side. Verified against the Sharma/Wu/Dalal published reference set, all 31 pairs to 1e-4, and the two implementations agree to within 1e-9 across 800 sampled pairs. Eligibility is untouched, still the per-channel RGB box, so this only reorders spools that already qualified. Type matching now agrees between the interface and the scheduler. Bambu firmware treats PA-CF, PA12-CF and PAHT-CF as one material and the scheduler has always matched them accordingly, but the interface compared raw type strings and called that same pairing a mismatch. The badge contradicted what the printer was about to do, and the manual override picker, which groups by canonical type, offered the very spool the badge then rejected. The fifteen comparison sites in useFilamentMapping.ts, useMultiPrinterFilamentMapping.ts and PrinterSelector.tsx now call filamentTypesCompatible. The pipeline pre-flight reads the matcher's table instead of its own copy. That copy had drifted into disagreeing in both directions: it aliased PLA Basic to PLA where the matcher never has, so a run could clear the check and then fail to map its slots, and it lacked the nylon grouping, so it flagged runs the matcher handles without complaint. A check whose job is to predict dispatch is wrong whenever it disagrees with dispatch, whichever way it leans, so it and the scheduler now both read backend/app/utils/filament_types.py. That canonicaliser deliberately does not strip surrounding whitespace. It looks like a free improvement, but it would collapse a junk tray_type to "" just as a 3MF declaring no filament type yields "", and a typeless requirement would start matching a junk-typed tray instead of reporting the slot unmapped. Padded type strings are worth handling on their own terms, with that case addressed. One behaviour change outside the ranking: the pre-flight is stricter for a printer reporting a product name such as "PLA Basic" where the generic material belongs, which it now flags rather than passes. Rare in practice, since the printer reports material and product name in separate fields, and it is the answer the matcher would give. Nothing about which spool a print actually uses changed outside the colour ranking itself. Adds 203 backend and 6 frontend tests. The #2804 tie-break test now uses identical colours: two colours at equal RGB distance are not perceptually tied, which is rather the point. |
||
|
|
4f7a02b393 | Pick the nearest eligible filament colour instead of the first one in tray order (#2804) (#2823) | ||
|
|
02616f0c91 |
fix(queue): stop a library-file delete from destroying the jobs queued against it (#2819)
Nothing tied a library file to the queue rows pointing at it, and the FK that describes the relationship is ON DELETE CASCADE -- which SQLite does not enforce and PostgreSQL does. So the same fault had two faces: rows left pointing at a file that no longer existed, failing at the printer with "Library file not found" days later, or rows deleted outright with no error and no history. Two routes into it, both fixed by taking the queue off the file before the row goes. Dispatch (the reported case): quantity>1 on the printer-card upload-and-print flow puts cleanup_library_after_dispatch on every copy, and _clone_queue_item copies library_file_id onto batch clones, so the first dispatch consumed the file the rest were waiting on. The copies are now pointed at the archive that dispatch just created -- it holds its own copy of the 3MF -- and the consume flag is cleared on them. A copy already printing from its own archive keeps it, a finished one keeps its outcome, and a cross-model item (#671) keeps any candidate this does not consume. Deletion: the File Manager, bulk delete, folder delete, emptying the trash and the retention sweeper all removed rows with queued work against them. Folder delete did not even clear the cross-model candidates, because the file-id walk it already performs threw its result away. Jobs waiting on a deleted file are now cancelled at that moment, naming the file, and every other row referring to it is detached rather than destroyed -- print history and batch progress are counted from those rows. A job that is printing is left alone: what is deleted is the library copy, not the copy on the machine. The trash is reversible so it still changes nothing about the queue, and a job dispatched while its file is in the trash now says so instead of "not found". Verified row for row on PostgreSQL 16 as well as SQLite: without this, PostgreSQL deletes every queue row referencing the file. |
||
|
|
b03603c4e2 |
Release the keep-warm bed on the dispatch paths that skip the rollback (#2727)
Selecting an item hands any keep-warm hold on its printer over to the preheat
pin: `_sweep_keep_warm` drops the `_keep_warm` entry and records "bed" instead,
on the promise that `_dispatch_one` will unwind it on any non-success exit. Two
of that function's exits never reached the `finally` that keeps the promise --
the claim failure returns before the `try` opens, and the vanished-row return
sat inside it but left `item_printer_id` at None, which the rollback guards on.
Either one left the bed hot with nothing tracking it. The keep-warm entry was
already gone, so the max-duration cap no longer applied and `_release_keep_warm`
had nothing to act on; if the cancelled item was that printer's last pending
one, the printer also dropped out of the candidate set, and nothing would ever
switch the bed off. Reachable whenever a cancel or delete lands between
selection and the claim -- narrow, but the outcome is exactly what the cap was
added to prevent.
`_dispatch_one` now takes the printer it was selected for. `_launch_uploads`
already had it (it stores the same value in `_inflight`), so nothing new is
plumbed, and the parameter is optional so the tests that call `_dispatch_one`
directly keep their existing behaviour.
Two pieces of hardening found while tracing that:
* The rollback switched the bed off unconditionally, where the keep-warm
release deliberately checks first that firmware still reports the target it
set. It now records what it pinned and declines when someone else owns the
bed. Every uncertain case still switches off -- no recorded target, or a
status that cannot be read -- because a bed left hot with no owner is the
worse failure, and this runs in a `finally` where raising would mask the
real exception. That is why the status read is factored out into a total
helper returning None for "no evidence" rather than 0.
* `_apply_keep_warm` ran unguarded between selection and `_launch_uploads`, so
anything raising there discarded the tick's selections, computed AMS
mappings included, and on a persistent fault stopped the queue dispatching
altogether. Wrapped, for the same reason the deficit check is: an auxiliary
comfort feature must never wedge dispatch.
Also documents why the max-duration check sits behind the FINISH and client
guards rather than ahead of them, since the ordering looks like a hole and is
not: with no client there is no M140 to send and the elapsed check fires on the
first tick after the printer returns, and leaving FINISH means the plate was
cleared, which routes the printer to `_release_keep_warm` instead. The invariant
to preserve if that is ever reordered is that every path out of an engaged hold
ends in a bed-off.
Six tests: the handover recording its target, both early returns releasing, a
call with no printer id staying a no-op, the reassigned-bed skip, the matching
and unreadable cases switching off, and eviction on deregistration.
|
||
|
|
37b0e25a2b | Merge branch 'dev' into feature/queue-keep-warm-chamber-history | ||
|
|
71709c16aa |
Stop tearing down AMS drying for a print that cannot start (#2801)
A printer in FINISH with an unacknowledged plate and something pending in its queue stopped and restarted drying once per scheduler tick, for as long as the plate stayed unacknowledged. The reporter's Home Assistant history recorded about 2000 state changes over ten days. No cycle ever ran long enough to remove moisture, and cycles the user had started by hand on other AMS units of the same printer were torn down with it. Two concerns had become tangled. Plate-clear answers "is the bed ready for the next job" and says nothing about whether the AMS may heat. The gap between a finished print and the acknowledgment is when drying is most useful -- the printer is free and nobody is waiting on it -- and leaving the plate unacknowledged is also how people hold the queue by hand, so the hold was costing them the drying it should have enabled. Four faults, all in print_scheduler. The "print takes priority" stop sat inside the not-idle branch. Drying is not one of the things _is_printer_idle looks at, so stopping a cycle can never turn a non-idle printer into an idle one: the stop was futile every time it fired, and never fired on the dispatches where it was supposed to mean something. It now runs when the printer is actually dispatchable, and only where the model cannot dry through a print -- #2758 settled that capable hardware should keep its cycle. mid_print was inferred from busy_printers, which means "the queue could not dispatch here this pass", not "is printing". A plate-held printer was therefore treated as printing: the mid-print spool-protection cap silently lowered its drying temperature, the cycle was logged as (mid-print) in FINISH, and it bypassed the very gate meant to hold it. busy_printers keeps its dispatch role; auto-drying now gets a narrow set snapshotted before the item loop -- running, held post-dispatch, or mid-upload -- and mid_print comes from the printer's own state. The interlock comment at the seed already documented this hazard and worked around it by staying out of the set; this generalises that instead of adding a third special case. The other call site was already passing the narrow set, so the wide one was the inconsistency. _stop_drying sent a stop to every AMS reporting dry_time > 0. One auto-dried unit was enough to kill a manual cycle on a different unit of the same printer, contradicting the contract _sync_drying_state already documents: the entry gate only knows about cycles Bambuddy began, so the action must not reach past them. Consequence worth stating -- after a restart Bambuddy cannot prove a running cycle is its own, so it leaves it alone rather than risk stopping somebody's manual dry. Fourth, and the reason #2770's guard did not catch this: a reading at or below the threshold popped the unit's whole entry, ended_at included, so the 30-minute re-arm cooldown went with it. An AMS reads higher warm than cool, which is #2770's own finding, so a unit a point or two above the threshold dipped below it as it cooled, wiped its history, and re-armed immediately. Lifting a suspension now clears the judgement and keeps the clock. queue_drying_block changes behaviour as a result. It previously had no effect on dispatch at all -- both branches skipped anyway, and it only decided whether drying was needlessly killed. With the stop on the dispatch path it now does what it says: a queued print waits for a running cycle. Off by default. Reported by @superflyer11, who traced both defects to the line and brought ten days of external sensor history to date the cadence. |
||
|
|
ec26cba927 |
Resolve the H2C rack nozzle at dispatch instead of letting firmware pick (#2800)
An H2C ran its startup clean and bed levelling on one hotend, switched, and then printed several millimetres above the plate. The same job from Bambu Studio was fine. The H2C is the only model that mounts its nozzle from a rack of six, and a print command names that nozzle by physical rack position -- the firmware reports those as 16 to 21 -- not by the extruder index, 0 or 1, every other dual-nozzle printer uses. Bambuddy only ever had a rack position when a job arrived through the Virtual Printer, which captures Bambu Studio's pick and replays it (#1780). Anything queued from the library, an archive, the webhook or a slicer pipeline carried none, so the field was omitted and the firmware chose -- and its choice need not match what the file was sliced for. The scheduler now derives the per-slot extruder assignment from the file it is about to send, and the MQTT layer resolves it against the rack position the printer reports live. Both are needed: the file knows which side a slot prints from, only the printer knows which hotend is in the carriage, and it can be swapped from the touchscreen between queueing a job and printing it. Derived at dispatch rather than at creation because that is the first point knowing both the real printer and the real file -- an item can be created unassigned, reassigned later, or have its file swapped for a G-code-injected copy. One call therefore covers the print dialog, bulk library adds, the webhook and pipeline runs, and no column is needed. extract_nozzle_mapping_from_3mf is deliberately untouched. Its output feeds the AMS matcher, where nozzle_id is compared against a tray's extruder_id as a hard filter, and physical_extruder_map is what makes that comparison correct -- on an H2D it is [1, 0] and flips the two. Dropping the translation to suit the rack would send every dual-nozzle AMS match to the wrong extruder. The dense per-slot form is a separate function reusing the same output. Nothing here can fail a dispatch. The command is built and published with no exception handler above it, and the queue item is already committed as printing by then, so a bad input has to degrade to "firmware picks" rather than wedge the item. resolve_rack_nozzle_mapping validates every input and raises nothing; an unresolvable mapping, an unparseable value or an unknown rack position all omit the field, which is the behaviour that existed before. Slot IDs are bounded before the dense list is built: they come from the file, and one declaring filament id="50000000" would otherwise allocate a fifty-million-entry list on the dispatch path. Two things are not guessed. A job printing only from the fixed hotend is still left to the firmware, because that nozzle's physical ID is not confirmed by a known-good capture. And the rack is taken to feed extruder 0 from a single hardware observation -- if that is flipped, a one-sided job matches nothing and falls back to the old behaviour, so only a job using both nozzles at once could be harmed, which is what a second capture needs to confirm. Confined to the H2C throughout. Building the print command for 21 model spellings with and without the new argument changes exactly three of them -- H2C, O1C and O1C2. The other 18, including H2D and X2D, are identical. Reported by @tru3l3gend, who diagnosed it on real hardware against a working Bambu Studio dispatch, established the rack ID range and supplied a patch. |
||
|
|
f86f5a7c34 |
feat(queue): keep the chamber warm between prints and skip redundant soak
Back-to-back prints in chamber-heated materials (ASA, ABS, PA, PC) each paid
a full heat-soak from cold, even when the print that just finished had left
the chamber at temperature. Two changes remove that cost.
Keep bed warm between prints
While a printer sits in FINISH awaiting plate-clear and the next queued item
needs chamber heat, hold the bed hot so the chamber does not cool during the
bed-clearing window. The bed is the chamber's heating element here, not a
print surface, so the hold runs at the new `queue_keep_warm_bed_temp`
(default 90C, which also satisfies bed-threshold-linked aftermarket chamber
heaters), raised to the item's own bed temperature when that is higher.
Gated on `queue_keep_bed_warm` AND `require_plate_clear` AND
`preheat_enabled`, all re-checked in the backend so a stale UI cannot leave
the feature running. `queue_keep_warm_max_minutes` (default 120) bounds the
hold: when it elapses the bed is switched off and the hold latches until the
printer is next a candidate, so a plate nobody clears cannot leave the bed
hot indefinitely. The hold is also released when the item is deleted, the
queue empties, or a gate is toggled off mid-hold, and never when firmware
reports a target other than the one it set — a temperature the user or a
print changed is left alone. Publishing is idempotent.
Smart soak reduction from chamber history
The scheduler samples each connected printer's chamber temperature every tick
into a 2h rolling history. Preheat credits time the chamber has already spent
at temperature against the configured soak, shortening or skipping it.
Credit starts no earlier than the newest sample, the most recent unbroken run
of samples, or the end of the last real dip below target. A dip only counts
once it outlasts a grace period: an enclosed chamber cannot lose and regain
several degrees quickly (measured on an X1C, cooling from 55C to below 48C
takes 23-73 minutes, ~0.2 C/min), so a brief low reading is a door opening or
sensor noise rather than lost soak — and a plate swap, which is exactly when
keep-warm runs, produces one. A stale history credits nothing: at that
cooling rate the chamber can cross the threshold unobserved, so the full soak
runs instead.
Three supporting changes to preheat itself:
* Cancelling or deleting a queued item now stops a preheat already running
for it. Those routes only write `status` to the database, which a dispatch
coroutine parked in `asyncio.sleep` cannot observe, so the heaters ran for
the rest of max_wait + soak — 45 minutes at the default settings — and the
printer stayed in `busy_printers`, blocking every other queued item behind
a print that was not happening. The routes now signal the scheduler
directly, and the stage sleeps in slices so it notices promptly and
abandons the dispatch, letting the existing rollback shut the heaters off.
* A chamber-heated print whose slicer metadata carries no bed temperature
(common for Orca-exported 3MFs) used to skip preheat entirely and start
with a cold chamber. It now heats the bed to `queue_keep_warm_bed_temp`.
A parsed bed temperature still wins, and a print with no chamber
requirement still skips — no bed temperature is invented for the print
itself. Preheat's bed target is transient regardless: the print's own
gcode issues its M140/M190 at start.
* Preheat records which commands it sent (bed, chamber, airduct) and unwinds
them if the dispatch aborts before the print starts — a failed upload, a
cancelled item, an exception — instead of leaving the printer heating for
a job that is not happening.
|
||
|
|
328bac450a |
Stop auto-drying re-arming into a threshold it can never reach (#2770)
An H2D armed five 12-hour drying cycles inside four hours, one of them six seconds after the previous one ended, and none ran more than a couple of hours. Two things combine. The firmware ends a cycle when it decides the filament is dry rather than when the clock runs out, and reports no fault doing it -- across this printer's history the run length tracks how wet the spools were, from nearly the full 12 hours starting at 32% down to minutes once the unit sat at 10-13%. That part is the AMS doing its job. The loop is ours. An AMS reports higher relative humidity while it is warm than once it has cooled: the same unit read 10-13% cold and 15-20% through every cycle. With the threshold at 14% the reading at the moment a cycle ended was always still above it, so the next 30-second pass armed another 12-hour cycle. Nothing counted, nothing waited, and it only stopped when the box finally cooled enough to read 13%. Auto-drying now waits 30 minutes after a cycle ends before arming another on the same unit, and gives up on a unit after two consecutive cycles that bring the reading no lower -- logging why and sending a new notification, on by default because it reports that Bambuddy has stopped acting. Progress is judged against the lowest reading any cycle on that unit has ended at, not against the threshold, so a genuinely wet spool in a humid room coming down 40-37-35 keeps drying however far it still is from the target; comparing against the best so far rather than the previous end stops a sensor wobbling by one point reading as progress every other cycle. The suspension lifts by itself once the reading falls below the threshold. Neither guard can stop a running cycle, and a cycle Bambuddy cut short for a print, or that the user stopped by hand, is not counted against the unit -- so a farm that dries between queue jobs is unaffected. The threshold field now warns below 20%, and every cycle end logs the unit's temperature and humidity, which is what made this diagnosable. The same bundle showed unrelated tasks failing with "database is locked", each inside a 30.000-second Discord connect timeout. Alarms are raised from inside the loop that records sensor history, at a point where the new rows are added but not committed; the first read in the notification path flushed them to satisfy itself, opening a write transaction, and the provider was then contacted over the network with that transaction still open. SQLite allows one writer and 30 seconds outlives the 15-second busy timeout, so every other write in that window failed. The two reads that run before a provider is contacted no longer flush the caller's pending work, and the connect timeout is 5 seconds rather than 30 -- the body keeps the full 30, so image uploads on a slow uplink are unaffected. SQLite only; Postgres has no single-writer limit. |
||
|
|
406cf71149 | Merge branch 'dev' into feature/billing | ||
|
|
14d0d14365 |
Power on a printer for jobs queued to a printer class (#2786)
Queue a print against a printer class -- "Any X1C", or a Slicer Pipeline whose target type is Printer class -- with every printer of that class switched off, and nothing happened. The job sat pending and no smart plug was touched, while the same file pinned to a specific printer powered that printer on within one queue check. The reporter's log holds both halves: thirteen minutes of the item being polled as (133, None, ...) and passed over, then a PATCH onto printer 2, then "Printer 2 offline, attempting to power on via smart plug(s)" on the very next tick. Same item, same plug, same Auto On setting. Powering a printer on had only ever been written inside `if item.printer_id:`. The model-based branch below it walks the same queue but its matcher classes an offline printer as a reason to keep waiting -- printers_offline collects the *name*, for the waiting reason -- and nothing on that path ever looks at plugs. _wake_printer_for_model adds it. The model query moves into _printers_for_model so the matcher and the wake step answer "which printers can this job run on" from one place: a job can only be woken onto a printer the matcher would also have considered. Candidates that failed the cross-model gate are excluded -- switching a printer on for a file that can never legally run on it leaves the job just as stuck, with the printer now drawing power. Two things it does that the fixed-printer branch does not: A printer awaiting plate-clear acknowledgment is skipped. Waking it buys nothing; it boots into IDLE and is held by the gate. That is what the reporter's log shows for the eighty minutes after their manual edit -- "printer 2 not available -- connected=True, state=IDLE, awaiting_plate_clear=True" every thirty seconds to the end of the capture. The flag is Bambuddy-side and persisted, so it is readable while the printer is still off. At most one printer per pass, because each wake blocks the queue loop for the boot wait. Several queued jobs bring several printers up over the following minutes rather than a whole shelf at once. A failed power-on opens a 600s per-printer cool-off. Without it the walk is by id, the pass spends its single attempt on the same broken printer every time, and a healthy sibling two slots down is never reached -- one unreachable plug starves its whole model, and costs a 180s boot timeout out of every 30s pass. Entries expire on read: a printer inside its cool-off is skipped before the power-on is reached, so a live entry can never be overwritten by a success. The failed printer is deliberately NOT added to busy_printers. It is off, not busy; labelling it busy would misdescribe it in every later item's waiting reason and, because an all-busy reason is treated as needing no user action, suppress the notification too. Assignment is left to the next pass. AMS trays arrive with the first status push after connect, so matching filament against a printer that booted five seconds ago can reject the printer we just woke. Finally, the waiting reason separates "Offline: X1C-1" from "Offline, no Auto On smart plug: X1C-2". Those are different problems and only the second is one the user has to go and fix -- it was also the first question the reporter had to be asked, and the queue could not answer it. Tests cover the wake, the plate-clear skip in both gate states, all-candidates- awaiting-plate-clear waking nothing, one wake per pass, the starvation case over two passes, cool-off expiry, no-Auto-On-plug being left alone and named, an incompatible sliced model waking nothing, connected printers being left alone, scheduled-for-later and manual-start jobs switching nothing on, and a regression pin on the fixed-printer branch. |
||
|
|
dd1d40b0d4 |
implemented pr (worth fixing) feedback
update commit |
||
|
|
9a397f46e9 | implemented pr feedback #2 | ||
|
|
43bf854bd5 | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
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. |
||
|
|
945d4ca6eb |
Route external spools to a nozzle when the printer has no AMS (#2771)
Five X2Ds with no AMS, each printing from its external spool holder, took a job sent to a named printer and refused the same job sent to "Any X2D": the file uploaded, the firmware answered 0700_8012 "Failed to get AMS mapping table", and the item failed after three attempts. A named-printer job carries a mapping the frontend resolved at queue time, so the scheduler's matcher never runs. A model-based job has no printer until dispatch, so the matcher does run -- and could not see an external spool on a dual-nozzle printer. _build_loaded_filaments derived dual-nozzle status from ams_extruder_map, which is built from AMS info bits, so a printer with zero AMS units reported an empty map; every external spool got extruder_id=None, and the nozzle-aware hard filter in _match_filaments_to_slots discarded it because None equals neither 0 nor 1. The mapping came back all -1, was cleared to None, and the print command went out as use_ams:true with no ams_mapping and no ams_mapping2 at all. This is the backend half of #1257, which fixed the same logic in useFilamentMapping.ts and left this copy behind. Mirror its inference: a populated nozzles[1].nozzle_diameter, a non-empty ams_extruder_map, or more than one vt_tray entry. Replaying the reporter's own push-status now yields extruder 1 for Ext-L and 0 for Ext-R, and a nozzle-1 requirement resolves to [254] -- what their working named-printer dispatch sent. Single-nozzle printers keep extruder_id=None; nozzles always has two entries, so its length alone must not be the signal. Also stop dispatching a job the firmware is certain to reject. When the matcher ran, matched nothing, and the printer has no AMS, fail the item with the filament and nozzle it wants instead of spending an upload and two retries on it -- that path already ended in a failed item, just an opaque one. With an AMS attached the firmware error still stands, since there the user can load a spool and press Resume. Fail-safe like the nozzle-diameter guard (#1899): every branch short of a positive finding returns None and dispatches as before. _apply_filament_overrides is extracted from _compute_ams_mapping_for_printer so the message names the filament the matcher looked for rather than the one the 3MF was sliced with. |
||
|
|
ebc73e46ba |
Say when AMS drying was running and a print never started (#2758)
Dispatching to an X2D with two AMS units mid-drying failed silently: the file uploaded, the printer accepted it and stayed idle. The watchdog waits for an active state or HMS_MQTT_VERIFY_FAILED, and a drying refusal is neither, so it timed out, re-uploaded the whole 3MF twice more, and closed with advice about the printer screen and the SD card. Studio, asked directly, said it could not start the job because of the drying. Latch the AMS dry_time telemetry across both watchdog phases and name the units in the give-up message, plus an INFO log on every failed window so the correlation reaches a support bundle from the first attempt. Detection only, no gate. These models support drying CONTINUING through a print (supports_drying_while_printing covers X2D from 01.01.00.00), so drying is not incompatible with printing and stopping it before every dispatch would tear down cycles the hardware is happy to run. One of the two units was also drying without its external PSU, which would make this a power budget problem at start-of-print calibration rather than a drying one -- dry_sf_reason 1/8 exist for exactly that. The message names both possibilities rather than asserting one. Also correct _sync_drying_state's docstring, which claimed to adopt drying it did not start; it only prunes. Behaviour unchanged -- populating it would let the scheduler stop a cycle the user started by hand. |
||
|
|
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. |
||
|
|
752e345d1a |
Add cross-model variant resolution to the queue scheduler (#671)
Adds print_queue_variants: the candidate files a queue item may run, each with its own model, plate, AMS mapping and nozzle mapping. The scheduler walks them in priority order and takes the first whose model has an idle printer, then folds that candidate onto the queue row before the selection commit — so upload, archive creation, print history and reprint keep seeing an ordinary single-file item. Candidates are ordered least-attempted first, so a printer that accepts the file and never starts hands the job to the alternative on the next lap instead of spending the item's whole retry budget on the machine that is wedged. The item-level DISPATCH_MAX_ATTEMPTS bound is unchanged. An item whose candidate files have all been deleted is held pending with an actionable reason rather than failing deep in the upload, and waiting notifications name the job and every model it is waiting on. |
||
|
|
037688514a | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
5e2b7b53e6 |
fix(printers): surface the printer's own "command verification failed"
A P1S on firmware 01.10.00.00 rejected every control command and said so: HMS 0500-0500-0001-0007, "MQTT command verification failed". Bambuddy received that, dropped it, and reported a healthy printer instead. The frontend filtered it out. This code's meaning lives in attr's low half (0500) and code's high half (0001), both of which the MMMM_EEEE short form discards, so it collapsed to "0500_0007" — no catalog entry, no firmware actions, and filterKnownHMSErrors drops uncatalogued action-less errors. Catalog lookups now try full_code first, in both the description and the filter, and errors matched that way display the four-group code the printer's own screen shows. The remedy line is ours, not Bambu's: their wiki says to update Studio or Handy, which does not apply to a print sent from Bambuddy. The developer-mode probe made it worse. It read anything that was not an explicit refusal as confirmation, and this firmware answers the probe with an empty result while refusing everything else — so an inference drawn from a non-answer became "developer_mode: pass" in the support bundle of a printer that had not accepted a command all day. The probe now has three outcomes: explicit success enables, explicit verify-failure disables, anything else stays unknown and the diagnostic reports skip. The HMS is authoritative over that inference in both directions. It forces developer_mode False when present, and clears back to unknown when the printer stops reporting it, so enabling Developer Mode and restarting the printer is picked up without restarting Bambuddy. Dispatch no longer treats a refusal as a wedge. The watchdog latches the HMS across both phases and fails the item on the first attempt naming the code and the fix, rather than spending three uploads and 270s a lap to arrive at a message about SD cards. The check runs after the active-state exit in both phases, so a lingering HMS can never abort a print that is visibly running. Also: the "wrong or mis-cased serial number" hint no longer fires in the moment after a reconnect. _report_messages_since_connect is reset by _on_connect, so a reconnect landing microseconds before the staleness check leaves it at 0 for reasons that have nothing to do with the serial — this reporter's healthy printer was told to go check its serial 1 ms after reconnecting. |
||
|
|
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. |
||
|
|
b74862d7d2 | PR feedback integrated | ||
|
|
0d6a28cd67 | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
ad785a95cb |
fix(queue): withdraw an expected print when the command never goes out
feat(db): warn when the connection pool can outgrow the PostgreSQL server fix(mqtt): an unusable layer_num must not drop the printer connection test: patch settings.base_dir via monkeypatch so it unwinds on error test: restore the config module after reloading it |
||
|
|
af7874546a |
feat(projects): per-file print progress and complete-sets tracking (#1897)
Projects made of many distinct files that each need N prints (e.g. 13
plates x 10 sets = 130 prints) only had aggregate progress. Finding out
"how many times have I printed plate_7?" meant reading the Activity
Timeline line by line, unusable at 130 events.
Projects now take an optional Copies per File target. Every printable
file in the project's linked folders shows an X / N badge with a mini
progress bar (gray not started, amber in progress, green done), and the
progress card gains a Complete Sets bar - the minimum per-file count,
i.e. how many finished assemblies can be shipped right now. Without the
target, printable files show a plain printed-count badge.
Counting matches the aggregate project stats: completed runs only,
served by a new /projects/{id}/file-progress endpoint. Runs attribute
to a file via a new library_file_id stamp on queue-dispatched archives,
falling back to content hash and then filename for historical rows.
Also fixed: files queued from a project-linked File Manager folder now
inherit that project, so their prints count toward project statistics -
previously only prints started from the project page were attributed.
Test-harness fix along the way: the test suite's get_db override never
committed, unlike production get_db, so endpoints relying on the
request-scoped commit silently lost their writes in tests. The override
now mirrors production commit/rollback semantics.
|
||
|
|
800c45536e |
fix(scheduler): pin the force-color variant when selecting the AMS slot (#2650)
Follow-up to
|
||
|
|
0f203ce7ca |
fix(scheduler): force color match now distinguishes PLA variants (#2650)
Force color match dispatched onto the wrong PLA sub-variant: a job sliced for White PLA Matte was treated as an exact match by printers loaded with White PLA Basic or Silk+, because Bambu reports every variant as tray_type "PLA" and the distinction lives only in tray_info_idx (GFA00=Basic, GFA01=Matte, GFA06=Silk). Two places dropped the field: the VP queue built each force override without the parsed tray_info_idx, and _get_missing_force_color_slots compared loaded trays on (type, colour) only. Carry tray_info_idx into the override and require it to match when both the override and a candidate tray have one; a blank idx on either side (custom/third-party spools, older 3MFs) falls back to the historical type+colour behaviour, so those setups are unaffected. |
||
|
|
56accd24de |
fix(smart-plugs): don't blank printer state when an accessory plug switches off (#2629)
An end-of-print auto-off on a plug that powers a filter fan marked the linked printer offline and forced its state to "unknown". The mark was unrecoverable: connected heals on the next MQTT message but state does not (only frames carrying gcode_state rewrite it, and steady-state push_status frames are partial), so the printer stayed "unknown" until a manual Force Refresh and the queue never dispatched to it again. The offline mark is now an explicit presumption: mark_power_off records the state it overwrites and _on_message undoes it as soon as the printer sends another report on its own topic, since inbound traffic proves the power was never cut. A reconnect discards the saved state, so a genuine power cut is unaffected. Each plug also gains a controls_printer_power flag (default true, backfilled) that gates all five power-off paths, and the queue's power-on step now picks the flagged plug instead of whichever linked plug came first. |
||
|
|
64f9d04c80 |
fix(queue): claim a queue item before dispatch so it can't be reassigned mid-upload (#2615)
A queue row stays status='pending' for the whole FTP upload; status only flips to 'printing' at the end. The edit routes only blocked non-pending rows, so a PATCH during the upload window was accepted while the in-flight dispatch kept using its snapshotted printer -- splitting the queue row from the archive / expected-print / physical command across two printers, and enabling a duplicate dispatch on restart. The #1853 CAS guards cancellation, not reassignment. Add a dispatching_at claim, stamped atomically (WHERE status='pending' AND dispatching_at IS NULL) before any slow I/O and cleared on every exit. While held, the single-item PATCH returns 409 (re-checked just before the write), bulk edits skip the row, and the scheduler won't re-select it. Startup reconciliation clears claims orphaned by a crash mid-dispatch. The row stays pending throughout, so no status/UI/completion/reconciliation path changes. New column print_queue.dispatching_at (nullable, dialect-safe DDL). Covered by scheduler tests (claim exclusivity, non-pending rejection, release-on-exit, skip-already-claimed, startup stale-clear) and API tests (reassign 409, printer_id unchanged, bulk skip, unclaimed row still edits). |
||
|
|
83a7b75b14 |
fix(queue): persist selected plate to the archive; reconcile archive on offline stop (#2603)
A print queued from a specific plate of a multi-plate 3MF showed as Plate 1 in Print History after cancellation: the archive derives its plate from the filename, but a whole multi-plate 3MF uploads under one name with no plate suffix, so the parser defaulted to plate 1 and nothing copied the queue item's plate_id onto the archive (which had no plate field). Add a nullable print_archives.plate_id, copy it from the queue item at dispatch (archive- and library-file paths), expose it in the archive API, and render it in Print History. A startup backfill copies the plate onto existing archives from their linked queue rows. Column add + backfill are identical on SQLite and Postgres. Also fix a related lifecycle bug: stopping a printing item while the printer was offline left the linked archive stuck at "printing" (queue row cancelled, but no MQTT completion ever arrives to reconcile the archive). The offline-stop path now closes the archive out directly; the online path still defers to the MQTT completion event. |
||
|
|
4a0b14ed0e |
fix(queue): make upload concurrency a refillable pool, not a per-batch cap (#2602)
check_queue awaited asyncio.gather() over the whole selected batch before returning, so the scheduler run loop was blocked until the slowest FTP upload in the batch finished. On a large farm a 513s upload left 15 of 16 configured upload slots idle for 8.5 minutes while other printers came free — the setting behaved as a per-batch cap, not a worker pool. Launch uploads as independent background tasks tracked in a _inflight pool. Each tick excludes in-flight item rows and their printers from selection, launches at most limit - len(_inflight) new uploads, and returns immediately, so a freed slot refills on the next fast tick. The no-double- dispatch invariant the batch-await provided (rows stay pending until upload completes) is now carried by the in-flight exclusion; the pending->printing CAS, busy-printer guard (#2598), per-printer hold, auto-drying exclusion, and per-item failure isolation are all preserved per task. Rewrites the concurrent-dispatch tests around pool/reservation/refill semantics and adds coverage for slot refill, in-flight exclusion, and the non-blocking return. |
||
|
|
d7093c7fe4 |
fix(dispatch): don't send start-print to a busy printer; scheduler defers (#2598)
start_print() published project_file guarding only on connection state, so a re-dispatch onto a printer that had already started — e.g. a watchdog revert (#2555) after the printer sat in FINISH past accepting the job — collided with the live print. The firmware answers 0500_4004 ("Device is busy and cannot start a new task"), which on an A1 mini cancels the running job. Defense-in-depth at the paths that can reach a busy printer: - bambu_mqtt: refuse to publish project_file when gcode_state is PREPARE/SLICING/RUNNING/PAUSE and return without sending. This is the one publish choke point every dispatch path funnels through (queue scheduler, manual start, webhook, Virtual-Printer forward). IDLE/FINISH/FAILED still start. - print_scheduler: re-check the live printer state right before the FTP upload and defer a busy printer (leave the item pending for a later tick) instead of uploading and dispatching. If the printer goes busy in the upload window and the start is refused, revert the item to pending rather than marking it failed — a busy printer is a deferral, not a failure. A transport-level MQTT QoS-1 replay on reconnect would bypass the client guard, but the dispatch/watchdog reconnect path already hard-resets the client with a fresh session, so it has no inflight project_file to replay. |
||
|
|
80687982c1 |
fix(db): release scheduler/cloud/cover sessions across slow I/O, add LIFO pool (#2572)
Three more idle-in-transaction / thundering-herd paths from farm testing: - print_scheduler: _start_print commits before the FTP delete/upload and _preheat_and_soak commits before the heat-soak wait, so the per-item session no longer sits idle-in-transaction across preheat + upload. - cloud/filament-info: rollback the request transaction after the token read and before the sequential Bambu Cloud calls; single-flight concurrent misses for the same setting_id through one shared call. - printers/cover: coalesce identical in-flight cover requests so followers serve from the cache the leader fills instead of duplicating the multi-path FTP + 3MF extraction. Also adds pool_use_lifo (PostgreSQL default on, DB_POOL_USE_LIFO override, shown in /system/db-pool) so a bursty farm keeps a small hot connection set. |