mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-07 06:31:22 +02:00
35405dd79e992301a0aa367239bc089eb4ecccc3
456
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0dfcff5925 |
Keep the RTSPS proxy's handler set off the server object (issue #3001)
asyncio's Server has a __dict__ and uvloop's, a Cython cdef class, does not, so the attribute added in 1.2.5.4 raised AttributeError under uvloop. Every RTSP camera failed before opening a socket, which is the diagnostic's capture_exception at 0 ms. Our own unit files all pin --loop asyncio for #1896 and were never affected. The reports come from units we do not write: the Proxmox VE Helper-Scripts LXC pins no loop, and installs predating that fix never gained the flag because update.sh does not rewrite unit files. The loop is not ours to assume, so fix the code rather than add another flag. The set moves to a module-level WeakKeyDictionary, keyed weakly so an abandoned proxy retires its own entry rather than leaking one and later handing a new server a dead one's handlers. Pinned on a real uvloop loop and, for hosts without uvloop, against a __slots__ server; conftest builds its loop from the default policy, so nothing in the suite had ever run the branch that broke. Also routes the two external-camera teardowns through close_tls_proxy, which #2968 introduced and left them out of. ----- Say so at startup when running on uvloop (issue #3001) An install on the wrong loop had no way to find out it was. #3001 was loud enough to notice; the #1896 upload truncation it is also exposed to is silent, and shows up as a print failing from a file that was corrupt on arrival. One WARNING in the lifespan naming the loop, the risk and the flag to add. A warning and not a refusal: uvicorn has already chosen its loop by the time any application code runs, and a server that answers requests beats one that will not boot. Asks the running loop what it is rather than whether uvloop imports -- uvicorn[standard] installs uvloop everywhere, so its presence says nothing -- and matches on the module name so the question never imports uvloop on a host without it. ----- Repair a service file written before the --loop asyncio pin (issue #3001) install.sh has pinned the loop since #1896, but nothing has ever rewritten an existing service file, so every native install created between 2025-11-28 (when uvicorn[standard] brought uvloop into the venv) and 2026-07-05 still runs on uvloop no matter how often it is updated. Both update scripts now add the flag themselves while the service is stopped, so it takes effect on the same restart -- systemd via sed, launchd via PlistBuddy, each backing the file up first and inserting nothing but the flag. Refuses to edit and explains instead when the shape is not a plain single-line uvicorn unit: a wrapper script, a continued ExecStart, several of them, a read-only file, or a service with drop-ins, since a drop-in may be what defines ExecStart and editing the fragment would change nothing while reporting success. A deliberate --loop uvloop is left alone. Reads the effective ExecStart from systemd rather than the file, so it is idempotent. |
||
|
|
49c948d01a |
Pin the TLS floor on the cleartext-probe test's context
CodeQL reports the context as allowing TLS 1.0 and 1.1, and it is right about the mechanism: create_default_context() leaves minimum_version at MINIMUM_SUPPORTED, which is the build's floor rather than a guarantee. That is the reason every context in backend/app pins it, the reason bambu_ftp.py carries a comment saying so, and the reason this same file already pins it for the TLS-1.3 case further down. Line 127 was the one that did not. The floor cannot change what the test measures. The fixture answers with a plain FTP banner and speaks no TLS, so the handshake still fails as WRONG_VERSION_NUMBER, which is the assertion this test exists to make. |
||
|
|
0d21239e18 |
Send AMS tray colours as uppercase hex (issue #2987)
Assigning a spool to an AMS slot unassigned it again seconds later, and
the slot's colour changed at the same time. It presented as Bambu Studio
and Bambuddy fighting over the slot. The reporter's log shows Bambuddy
losing to itself.
P1S firmware 01.10.00.00 reads every lowercase hex letter in an AMS
tray_color as a zero, and hides it completely: the command response
echoes back the value that was sent and reports result "success", so
only the next AMS push says what was really stored. The spool-assign
path sent spool.rgba verbatim and that column stores lowercase. From the
bundle:
sent 09ff00ff -> AMS reports 09000000
sent ff5100ff -> AMS reports 00510000
sent 090000FF -> AMS reports 090000FF
That is the visible colour change, and it is also what deleted the
assignment. The auto-unlink sweep asks whether the slot still matches
the spool assigned to it; the mangled colour no longer did, so the
assignment Bambuddy had made four seconds earlier was removed.
colors_similar('09000000', '09FF00FF') is False, which is the whole of
it.
Re-assigning could not recover, because the Configure Slot dialog seeds
its colour from whatever the printer currently reports. It wrote the
mangled colour back and cemented it, which is the loop the report
describes in its steps 4 and 5.
Colours are now uppercased where the command is assembled rather than in
each of the four routes that configure a slot. A caller that forgets is
exactly how this arrived. Nothing else changes: no padding, no invented
alpha, no six-to-eight widening, and tray_type and tray_sub_brands keep
their case, where it carries meaning -- "PLA Matte" is a product line,
"PLA MATTE" is not.
Two paths deliberately left alone. The developer-mode probe re-sends the
colour the printer itself just reported so that the probe is inert;
uppercasing there would turn it into a write. And the Virtual Printer
forwards the slicer's own command verbatim -- Studio could in principle
hit the same firmware bug, but nothing here evidences that it sends
lowercase, and rewriting a slicer payload inside a transparent proxy is
not a change to make on a hunch.
Two more defects from the same log.
A spool with a brand and no subtype was configured with the string
"None" in its name: the branded branch interpolated spool.subtype
without checking it while the unbranded branch guarded it, so
"Sunlu PLA Matte None" went on the wire and into Studio's display.
And the FTP log is readable again. A 426 whose bytes Bambuddy has
already verified against the printer is how Bambu FTPS normally ends a
transfer, not a fault, so it drops from WARNING to INFO. It fired 54
times in this one bundle, every one followed by a completed upload, and
it was burying the 26 TLS handshake failures in the same log that
actually cost the reporter two prints. A 426 whose bytes do not verify
is still an error and still fails the upload.
The handshake failures themselves are printer-side FTPS cool-off under
load and are not touched here.
|
||
|
|
4d2c6debf7 |
Ask Spoolman which extra fields it has, once (issue #2983)
Bambuddy checked whether one of its four custom spool fields existed with
GET /field/spool/{name}. Spoolman has never served that. Its API declares
only POST and DELETE at that path -- confirmed against the live server's
own OpenAPI document -- so the probe answered 405 Method Not Allowed
every time and the check could not succeed on any version.
Every call therefore fell through to POST /field/spool/{name}, and that
endpoint is an upsert rather than a create. It answers 200 whether or not
the field is already there, so a field the user had renamed, retyped or
given a default to in Spoolman's own UI was reset to Bambuddy's version
of it, and an untrue "Created Spoolman extra field" was logged beside it.
The reporter's log carried 60 of those lines over three days -- once per
field per client init, which is every restart and every settings save.
Existence now comes from GET /field/spool, the listing endpoint, matched
on each row's `key`. Matching on `key` rather than the display `name` is
the part that fixes the overwrite: a renamed field is the same field, and
reading it as a missing one is what re-created it. A field that already
exists is now left completely alone.
The listing is read once per client and banked, so registering all four
fields costs one request instead of four, and a client that has already
looked makes none at all. Only a successful read is banked -- a client
that could not reach the listing asks again for the next field it has not
seen, so one transient failure does not leave it posting blind, and
overwriting, for the rest of its life.
An unreadable listing still falls back to attempting the POST. Registration
is best-effort by contract: it must not turn a write that might still
succeed into one that never happens, so an unexpected Spoolman build is no
worse off than before.
Measured against Spoolman 0.23.1: a field renamed to "Bambu RFID Tag"
survives a full registration pass that previously reset it, the pass makes
one GET and one POST for the single genuinely-missing field where it used
to make four POSTs, and a second pass on the same client makes no requests
at all.
The fake in test_spoolman_extra_field_registration_2903 modelled the
per-field path as a working probe, which is the assumption this bug was
built on; it now answers 405 as the real server does, and its assertions
follow the listing. 18 new tests cover the rest, 10 of which fail against
the old code.
|
||
|
|
73e0787bfd |
Name an unnamed print stage "Preparing" on the card
New printers report stage numbers before Bambuddy learns their names, and the H2C still has several. Those reached the printer card verbatim, as "Unknown stage (72)" -- a number that means nothing to the person reading it, on the one line that otherwise says what the printer is doing. Every stage that has turned out to be unnamed so far has been part of the run-up to printing, so an unnamed one now reads as "Preparing". That is the same literal stage 74 already carries rather than a second spelling of the same idea, so a card cannot show two different words for the same situation depending on which number the firmware picked. Display only, and deliberately not pushed down into get_stage_name. That function also feeds the stage-transition log line and the once-per-session warning added to capture unnamed stages so they can be named in a later release; there the number is the entire diagnostic value, and replacing it with "Preparing" would hide the only thing that reports these. Both paths are pinned by tests asserting they disagree for an unnamed stage and agree for a named one, so a later tidy-up cannot quietly collapse them. The idle sentinels are untouched: 255 on A1/P1 and -1 on X1 mean "no stage", not an unnamed one, and still resolve to nothing rather than being swept up by the fallback. Nothing keys logic off stg_cur_name -- it is display-only in the printer card, the print dialog's printer selector and the stream overlay -- so all three improve and none change behaviour. No i18n either way: the whole stage table has always been English. |
||
|
|
d5c7047765 |
Name an AMS slot after the spool assigned to it
The print dialog described every slot from the printer's own telemetry, and
a printer cannot describe a spool it did not sell: a tray record carries no
brand field, tray_sub_brands is left empty for anything that is not a Bambu
spool, and the colour arrives as a bare hex the client resolves against
Bambu's own colour catalogue. A Devil Design PLA Basic Orange assigned in
Bambuddy therefore read as "PLA (Sunflower Yellow)" -- Bambu sell a
Sunflower Yellow at the same FEC600 -- while the printer card, which reads
the assignment, named it correctly. Two views of one slot, disagreeing.
GET /printers/{id}/inventory-remain now carries each bound slot's brand,
material, subtype, colour name and hex alongside the pooling key it already
sent, and the dialog prefers that over telemetry. The fallback is per field,
not all or nothing, so a spool with no stored colour name still gets the
catalogue lookup it had before while its brand and subtype come from the
binding. Resolved server-side because the identity rule differs per
inventory mode -- brand is a column in internal mode and a nested vendor in
Spoolman's, where the subtype is the filament name with its material prefix
stripped and the colour name has a three-step read order Spoolman has no
field for. Spoolman's synthesised colour name, which falls back to the
subtype, is withheld rather than rendered as "PLA Basic (Basic)".
Matching is deliberately untouched and still runs on the printer's
telemetry. The auto-assignment, the colour-mismatch test and the mapping
that actually gets dispatched all read type, colour hex and tray_info_idx,
so renaming a slot cannot make the panel and the dispatcher draw different
conclusions from it. The payload is re-read on every open of the dialog: it
names the slots now, and a spool assigned moments earlier would otherwise
keep its old name for the rest of the thirty-second stale window. Done at
the two readers rather than by invalidating the key from each of the
eighteen places a binding or a spool can change, half of which are internal
paths and half Spoolman ones -- covering some would make freshness depend on
which mode you run.
Two hardening fixes fall out of putting a mapper on this path.
build_slot_materials runs before every queue start through
compute_deficit_for_queue_item, and _map_spoolman_spool walks a dozen nested
fields off the wire, any of which arriving as the wrong type raises
AttributeError rather than ValueError. Naming a slot must never cost a
dispatch, so that call fails soft to no name. The same inputs also reached
_material_identity_spoolman and _normalize_color_for_id, which have always
been on this path and would fail a queue start on a Spoolman record whose
filament is not a dict or whose color_hex is a number; both now read those
as "nothing to pool with". Behaviour for well-formed input is unchanged --
the guards only intercept types that previously raised -- so no pooling key
moves and AMS Filament Backup is untouched.
|
||
|
|
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. |
||
|
|
a68933cb2f | Allow Avery label sheets to start at an unused position (#2879) (#2918) | ||
|
|
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. |
||
|
|
39835437a3 |
Price a print from the spool that fed it, not the default rate (issue #2591)
Spoolman holds per-spool pricing, and #261 gave that as the reason for integrating with it. Nothing ever read it. A print's cost is set once, at archive time, from the built-in Filament catalogue matched on the primary type and falling back to a global default rate -- and in Spoolman mode nothing revisited that figure afterwards. The per-spool recompute that would have fixed it, in usage_tracker.on_print_complete, runs only over rows the built-in inventory writes, and Spoolman mode hands the usage tracker spoolman_owns_usage at print start so it writes none. The reporter's catalogue was empty, which is the ordinary state of one in Spoolman mode, so every print came out at the default no matter what the linked spool cost. Multi-material was wrong twice over there: the primary type's rate applied to the whole print's weight, so a slot of expensive PA was billed at the price of the PLA beside it. Each slot is now priced from the spool it was actually charged to, at the moment of the charge, and the per-slot costs are summed -- which is what fixes the multi-material case, rather than a separate change. All three charge paths feed it: per-slot, tray-split, and the remain%-delta fallback. The rate is the spool's own price when set, else the filament's, over filament.weight. That is net grams excluding the core, and the same field the remain-delta path already divides by to turn a percentage into weight, so a spool that can be charged by percentage can always be priced. The price comes out of the get_spool call the colour and material rewrites already pay for, so the tagged path costs no extra round trip. Grams no spool could price are covered at the global default in one subtraction against the archive's own total. A spool with no price, a tray with no Spoolman row, and filament the sliced file never attributed are the same case from here, and without the top-up a print with one priced slot out of four would report a quarter of its cost -- #1344 in the other inventory mode. Only the first run writes the archive, matching the built-in writer (#1378); reprint actuals live in PrintLogEntry. If no slot could be priced at all, whatever archive.py recorded is left alone, so an install with prices in neither place stays where it was. Applied even when the slot-to-tray mapping was a positional guess, unlike the colour and material rewrites beside it. Those overwrite what the slicer recorded, which is why a guess must not touch them. The cost has no such original -- archive.py's figure is itself derived from a default rate -- and the grams have already been deducted from these spools, so the archive should say what that deduction was worth. Both cost recalculations would have undone it on the next run. /rescan and /recalculate-costs rebuild an archive's cost from SpoolUsageHistory and fall back to the catalogue or the default when there are no rows, which in Spoolman mode is always, so the fallback was not a recalculation but a downgrade. The spool-to-slot resolution a price is derived from exists only while a print is completing and cannot be rebuilt from the archive row, so both now leave a cost alone rather than replacing it with a worse one, and the bulk endpoint reports how many it kept. An archive with no cost yet is still priced, and with Spoolman off both behave exactly as before. The rate parser refuses more than it looks like it needs to, because everything it refuses was reachable. A non-dict filament raised through a call that sits after a successful use_spool, which would have abandoned the remaining slots of a multi-material print with the charges already made. NaN compares False against every bound, including the applier's own total <= 0, so a NaN price would have been written to the archive with nothing downstream able to clear it; two finite operands can produce it by overflow, so the quotient is checked as well as the inputs. A bool is an int in Python, and float(True) is 1.0 -- a weight of 1 g prices a spool per-gram at its whole cost. And a spool-level price of 0 now falls through to the catalogue rather than reading as free: Spoolman leaves the override null when unset, but importers write 0 often enough that treating it literally would price a whole print at the default with a good catalogue price one level down. |
||
|
|
935d4b5bbf |
Charge the tray the printer said it used, not the first one loaded (issue #2953)
A sliced file numbers its filaments 1..4; which AMS tray each came from is decided when the job is sent. #2768 gave the Spoolman writer two ways to recover that decision when the print did not come through Bambuddy: the printer's own mapping field, and a colour match of the 3MF's slots against the loaded trays. An A1 satisfies neither. It publishes no mapping field, and it drops the MQTT connection when we subscribe to its request topic, so the slicer's instruction never arrives either. That leaves the colour match, and it compares hex strings exactly. The reporter sliced with a generic black profile against a tray they had set to charged slot 1 to whatever sat in the first tray -- 2.17 g onto a grey PLA+ spool, while the print was fed from tray 3. Their bundle carries the printer's own answer: "Tray change during print: tray=3 at layer=0", recorded 90 seconds in, and read further down the same completion pass by _print_used_tray_keys to decide which slots the print had touched. The same pass then charged tray 0 on a guess, and logged "AMS0-T3: remain% did not fall over the print" about the tray that had actually done the work. _single_slot_tray_from_state adds the third rung. For a print with exactly one slot carrying usage, the one slot came from the one tray, so the printer's tray reporting answers the question directly: the mid-print tray-change log, then the tray loaded at print start, then the current one, then the last real tray seen. That is the ladder usage_tracker.on_print_complete has consulted since it started resolving mappings at completion -- Spoolman users were the only ones not getting it, which is why an install running the built-in inventory has never shown this. On this printer only the first and last rungs can fire: tray_now_at_start is 255 because print start runs before the filament is loaded, and the A1 parks tray_now back at 255 the moment a print ends. Gated on exactly one slot with usage, like the internal writer: a multi-colour print moves tray_now on every change, so one reading cannot then be attributed to one slot. It also declines when the log holds more than one switch, because an AMS-backup runout is split per segment (#1793) and a single-tray mapping would land the whole print on one spool. That gate needed the guess warning to exclude the split path too. The split never reads slot_to_tray at all -- it charges each segment to the tray the printer announced switching to, which is the same evidence this fallback is built on -- so a declined mapping there is not a guess, and calling it one suppressed the archive rewrite for exactly the prints whose attribution is best supported. Nothing covered that combination; a test does now. Where nothing names a tray the positional default still stands, because it is right for an AMS loaded in slicer order. It now says so at warning level so a support bundle carries the reason, and it no longer restamps the archive's filament colour and material from a spool it picked by position. That restamp is what made the fault read as data loss: the grams can be put back, whereas overwriting what the slicer recorded leaves nothing to compare against, and the reporter's archive had already been rewritten from #000000 to the wrong spool's grey. A slot that consumed nothing also stops claiming a tray in the handled set -- it was never charged, so the remain-delta path should stay free to cover it rather than be suppressed by an estimate of zero. The request-topic probe is the same failure reached from the other side. A printer that refuses kills the TCP connection instead of returning a SUBACK failure, so the only signal is "we subscribed, then got disconnected", and that was believed the first time it happened. Every other reason a connection drops inside the same window looks identical -- a network blip, the printer rebooting, the container stopped mid-probe -- and the verdict was cached per serial with no re-probe anywhere, so on a printer that supports the topic one unlucky drop cost mapping capture for the rest of the process and every slicer print after it was charged by tray position. It now takes two consecutive drops, and a disconnect we asked for is not counted. A printer that genuinely refuses answers the same way every time and pays one extra reconnect; one already known to refuse still skips the subscription outright rather than reopening a reconnect loop. |
||
|
|
73912d4f05 | Let a clear spool stay clear on the way to Spoolman (issue #2912) (#2924) | ||
|
|
54af3146a3 | [Feature]: Bind Home Assistant sensors to storage locations (dryboxes/bins) (#2827) | ||
|
|
5dd7bd213f |
Read a print's destination from the report topic, not just the request one (issue #1820)
current_project_url was assigned in exactly one place, _handle_request_message, and _on_message calls that only for the request topic. A print started from the printer's own screen publishes nothing there, so the field stayed None for the one case the storage verdict exists for: the file is already in the printer's model library under /userdata/model/history/, which port 990 does not serve. The verdict then fell through to the sdcard flag, and @ojimpo's H2S reports that flag true -- its "card" is the internal eMMC -- so every such print ran the full sweep before giving up. He measured one: 16 filename-and-directory attempts over 22 FTPS connections, 18 of them refused, 6.4 seconds, then a fallback archive holding a name and nothing else. The printer does announce where the file lives, as an unsolicited project_file *response* on the report topic about two seconds before gcode_state reaches PREPARE. _process_message now reads the url off it, gated on result SUCCESS and a non-empty value so a refused dispatch cannot name a file that was never written. Reading it there rather than only at the request topic also covers an install neither of us had in view: some brokers refuse the request-topic subscription, and on those no print of any kind had ever populated the field. The new branch captures state and nothing else. The "External project_file payload" diagnostic stays with the request-topic handler: our own dispatch is echoed on both topics, the request-topic echo lands first and clears _own_project_file_key, so reusing the diagnostic here would have logged every Bambuddy-started print as somebody else's. A test pins that. What the print names is now what gets tried -- the five directories a copy could be in, rather than the ~110 connections that cannot succeed. The probe is still worth running: an H2S keeps recently used jobs under /cache and archives them in full while they last, which is why the reporter's two prints on the same day behaved differently. Slicer-sent prints are unchanged. The banner no longer describes a step that never happened. With no reason recorded, a blank archive fell back to the original wording -- "Store sent files on external storage" is off in your slicer -- which on that printer is on, and which the internal-storage wording from #2780 already explains would not help on an H2. The archive that most needed that explanation was the only one that could not be given it. So file:///userdata/ now earns its own reason, internal_history, separate from the brtc://emmc dispatch case. A dispatch chose internal storage and can be aimed elsewhere; a print of a file that was already there had no dispatch at all, and telling that operator to pick External in Send names a dialog they never opened. The banner and the connection diagnostic both read the verdict's reason rather than a fixed one, so the two surfaces cannot give the same printer different advice. Thirteen locales, and a wiki section the banner links to. ----- Read the K-profile selection when the mutation runs, not when it is captured Configure Slot sends cali_idx from selectedKProfile, and the mutation read it through its own closure. React Query hands a mutation its options from an effect, so a click landing between a commit and that effect flushing runs the previous render's mutationFn -- one that captured the selection as it was before the K-profile query resolved. The payload then carries cali_idx -1 and the printer binds the default 0.020 instead of the calibrated K, while the dialog shows the right profile selected throughout. It surfaced as an intermittent failure of the per-nozzle K-profile test, about one full-suite run in six. Reproducing it with staggered query resolution showed the divergence directly: the select element held the correct profile immediately before and after the click, and the payload still carried -1. That test's slot is the most exposed case in the file -- a right-hotend slot carrying the left hotend's index, where the "keep showing the active profile" safety net cannot repair an empty recompute. The selection now goes through a ref written during render, so the mutation resolves it at execute time. An effect would have inherited the same flush ordering this exists to escape. The K value and the profile's ids travel in the same payload and had the same exposure, so they move with it. Measured over a staggered-resolution grid: 2 failures in 15 runs before, 0 in 12 after. api.getSlicerPrinterModels was also missing from the test file's mock, so that query ran with no query function and rejected in all 37 tests -- mocked now, though on its own it changed nothing, which is how the ref was confirmed as the fix rather than assumed. |
||
|
|
ed84f0f74c |
Anchor a plug-energy test to local midnight, not the wall clock (issue #2938)
test_nothing_derivable_before_the_first_midnight failed for 31 minutes of every day and passed for the other 23.5 hours -- the shape that reads as ordinary flakiness and gets re-run rather than fixed. @ojimpo hit it running the full suite at 22:10 UTC, stashed his branch to confirm it reproduced on clean dev, and measured the window minute by minute instead of guessing. The test asserts that nothing can be derived when the only snapshot was taken after this local midnight, and it placed that snapshot at a raw wall-clock offset -- now minus thirty minutes. Its comment, "taken this morning, after midnight", is the premise, and it is only true away from the boundary. For the first half hour of each local day, now minus thirty minutes lands before local midnight, where it is a perfectly good baseline: _counter_at finds it and today comes back 1.5 rather than None. The window is local 00:00 to 00:30, which is 22:00 to 22:30 UTC under CEST and 23:00 to 23:30 under CET -- it moves with DST, since the module pins Europe/Berlin in an autouse fixture and an outer TZ makes no difference. Nothing is wrong with the production code. A snapshot from before local midnight genuinely is a valid baseline for today, and derive_today_yesterday is right to treat it as one. Only the test's premise breaks at the boundary. The snapshot is now anchored to local_day_start(now) plus thirty minutes, which is the idiom the other nine snapshot writes in this file already use and the reason none of them can drift. Replayed across 5760 minutes covering four days, including both DST switch days: the old expression fails 31 minutes per day, the new one fails none. |
||
|
|
f95c81e6cd | Take an RFID spool's core weight from the row that names it (issue #2909) (#2923) | ||
|
|
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. |
||
|
|
b38022ec5c |
Draw a spool the way the AMS described it, and correct the tare it was added with
Two faults in the same auto-add path, both found while tracing why an H2C
slot named a wood roll as plain PLA.
A spool's swatch is composed from effect_type and extra_colors, and the
RFID auto-add set neither. It reads the colour catalogue to name the
colour and took the name alone, even though the row it had in hand also
carries those two columns -- the spool form's own colour picker hands both
to a spool a user adds by hand, so the same roll rendered one way when you
typed it in and another when the printer identified it for you.
Both columns now travel with the name. That alone changes nothing on a
stock install, because the shipped catalogue carries an effect on none of
its 600-odd rows, so the subtype is read where the catalogue has none: it
is already derived from what the printer reports, and the two vocabularies
line up -- Wood, Silk, Sparkle, Marble, Glow, Galaxy, Metal, Rainbow,
Translucent, Matte, and the Gradient, Dual Color and Tri Color that the
M*/T* colour codes upgrade a subtype to. "Silk+" reads as Silk, since the
plus is on the product name rather than the finish. A subtype that names
no effect -- Basic, Tough, CF -- leaves the column empty rather than
inventing an overlay, and a value already set is never overwritten, so the
column stays what it is documented to be: a rendering hint the user can
override without touching Bambu's categorical label.
------
Correct the spool tare an RFID roll was added with (#2909)
The lookup that gave an auto-added spool its core_weight asked for the
first catalogue row whose name starts "Bambu Lab" and took whatever came
back. There are three, and which is first is the database's business:
SQLite returns insertion order in practice, Postgres promises nothing once
a table has seen an update. The same roll was therefore recorded with the
216 g High Temp tare on one install and correctly with the 250 g Low Temp
one on another. @ojimpo's forward fix picks the row by name; this repairs
the rows already written, which the forward fix cannot reach -- 22 of 26
RFID-added spools on the instance this was traced on.
The tare is not cosmetic. A spool weighed on SpoolBuddy has its remaining
filament worked out as the scale reading minus the tare, so a 34 g low
tare credits the roll with 34 g that is not there and writes a used weight
34 g short. That error is a constant -- every later print adds to the used
weight on top of it -- so adding the difference back is exact however much
has been printed since. It is applied only to spools that have been on the
scale; one that never was has a used weight derived from the AMS remaining
percentage, which the tare never entered into.
Rows are identified by the signature of the broken lookup: added by RFID,
carrying the weight of one of the other Bambu catalogue rows, with the
weights read out of the catalogue rather than hardcoded so an install
whose rows have been re-measured is repaired to its own numbers. Keying on
whether a catalogue row had been recorded would not have worked -- the
weight picker auto-selects the only row matching the weight and writes its
id on the next save, so that column says only whether the form was ever
opened. The one case that cannot be told apart is stated rather than
hidden: someone who moved an RFID roll onto a genuine High Temp spool and
set 216 g by hand is normalised with the rest. Runs exactly once, so a
tare set afterwards is kept.
|
||
|
|
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. |
||
|
|
ed85677913 | Light the generated thumbnails so one model differs from another (#2816) (#2861) | ||
|
|
55cc64c87d | Add printer video downloads and range selection (#2853) | ||
|
|
4e40a5022c |
Register a Spoolman extra field with its own write (issue #2903)
Spoolman rejects a spool whose extra dict carries a key it has not been told about, answering 400 "Unknown extra field tag.". Bambuddy keeps the tray UUID in extra.tag, so that key has to exist before the first spool is created. Registration ran from three hand-maintained lists that fire when the integration is set up -- the connect route, startup, and two inline blocks in the inventory routes. Enabling Spoolman from the Settings page reaches none of them, so the first AMS sync on a fresh Spoolman failed on every slot while vendor and filament creation succeeded. Neither fix suggested on the issue is quite the right shape. Adding the block to PUT /settings/spoolman fixes this path and makes a third copy of a list that has already drifted -- it would still omit bambu_color_name. Ensuring at the sync entry point leaves the other four tag writers alone: linking and unlinking a tag, and both inventory edit paths. So the registration moved to the write. create_spool, update_spool and update_spool_full each register the keys of the extra dict they are about to send, once per client, before sending. Every tag writer funnels through one of the three, merge_spool_extra included. This closes the class rather than the instance: a write that carries a key is a write that registers it, and bambu_color_name shows why that matters -- it never made it into the connect or startup lists at all, and works today only because two call sites remembered it by hand. Best-effort, deliberately. ensure_extra_field already logs and returns False rather than raising, so a registration that fails leaves the write to be attempted and to report exactly what it reported before. Failures are not memoised either, so a Spoolman that was merely restarting gets another try on the next write. The older blocks stay. They are redundant now, but the inventory routes' inline calls are pinned by tests that assert them against a mocked client, where the funnel cannot run. The status endpoint is the other half. The Connect button would have registered the fields, and the reason nobody reaches it is that GET /spoolman/status reported "connected" whenever an earlier request had left a client object behind. Roughly twenty call sites build one lazily, and saving the Settings page builds one as a side effect of syncing locations, so the flag turned on which page had been opened rather than on anything about Spoolman. The UI reads it twice -- Connect only while disconnected, the sync section only while connected -- so those two controls landed in states the user cannot explain. It now asks the Spoolman that is configured, including the stale-URL check every other route already does, and does not probe at all when the integration is switched off, which used to let a leftover client report a disabled Spoolman as connected. That leaves nothing for Disconnect to do, so it is gone. Spoolman is a stateless HTTP API with no session to close; the button dropped the client object, the next request rebuilt it lazily, and the status flipped back on its own within the 30s poll -- an action that looked like it worked and then quietly undid itself. The enable toggle owns turning the integration off. Connect stays as what it always was in practice, a way to re-check a Spoolman that is not answering, and is shown only then. Its translation keys are left in place; only the control is removed. Resolving the client there means the status poll can now fail in ways a read-only check could not, so it no longer reports failure by failing. Replacing a client closes the previous one and httpx's aclose() is not guaranteed not to raise; a poll that runs every 30 seconds answering 500 is worse than one answering what is true either way, which is that Spoolman could not be reached. The SSRF rejection keeps its own message rather than folding into the general one -- a URL the guard refuses is the admin's to correct, and that is only actionable if the log says so. Tests drive the real client against a fake Spoolman that enforces the unknown-extra-field rule rather than mocking it away, so each one fails against the old code for the reason the reporter's install did. The concurrency test needed the fake to be async: MockTransport answers without suspending, so the first version ran each request to completion in turn and passed just as happily with the lock removed. |
||
|
|
cc39acfc74 |
Ask a printer that refuses FTPS what it actually said (issue #2780)
@grolmus measured a 9-printer farm and the numbers settle what this
failure is not. Reproduced here, three results:
cleartext "421" banner on the TLS port
-> [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1032)
1.2-only server, client forced to 1.3
-> [SSL: TLSV1_ALERT_PROTOCOL_VERSION]
1.2-only server, an uncapped client
-> negotiates 1.2 and connects
The first is byte-for-byte what the farm logs. So WRONG_VERSION_NUMBER
means the printer's first bytes were not a TLS record, a version
mismatch cannot produce it, and reaching a 1.2-only peer needs no cap.
What it still does not say is WHICH cleartext message, and that is the
part that would name the fault. OpenSSL has eaten those bytes by the
time the exception surfaces, so on this error the client now opens one
plain connection and reads them. The log then carries the printer's own
words -- an FTP refusal such as "421 Too many connections" would settle
it outright -- marked as the line to quote in a report. This gets the
answer from every affected install rather than from the one farm able
to take a packet capture.
Three things keep it from making the suspected fault worse:
- The failed socket is closed BEFORE the probe opens its connection.
Holding a dead handshake open across a second connect to a printer
that may be out of connection slots is the leak #2780's own cleanup
was added to stop.
- It asks once per cool-off window, not once per attempt. Checked
before the new deadline is written, so a live entry means an earlier
failure already asked -- which matters because a dispatch ignores the
cool-off (#2898) and reaches this branch four times.
- Connect and read share one timeout budget rather than getting one
each.
Only WRONG_VERSION_NUMBER is probed. A protocol-version alert means the
peer did speak TLS, so there is nothing in the clear to read and the
probe would only sit out its timeout. A vsFTPd answering its connection
limit by accepting and staying silent -- the other half of the standing
theory -- arrives as a handshake timeout and lands on that branch
instead; there is a test saying so, because widening the trigger later
would look like an improvement.
The profile registry is corrected to what was measured. Its docstring
claimed "the P2S evidently does offer 1.3"; six P2S units refuse it.
Worse, the X2D (#1638) and H2C (#2582) entries were capped on the
reading that WRONG_VERSION_NUMBER came from a TLS-1.3 ClientHello,
which cannot happen -- so the cap is not what changed those outcomes
and both are now marked RE-TEST WANTED. They are kept rather than
removed: their reporters saw the symptom clear, nobody here has that
hardware, and the entry costs nothing on a printer that does not offer
1.3 anyway. The P2S entry (#1401) is a different symptom -- a 426
truncation mid-transfer -- and is the only one a session-ticket problem
could explain, though grolmus's firmware refuses 1.3 there too.
Both measurements are pinned by tests, so the explanation stays
falsifiable instead of becoming the next set of confident wrong
comments. Two existing cool-off tests now count two connections where
they counted one; the promise they exist for -- contacted twice, not
~110 -- is unchanged, and they say why rather than carrying a new
number.
|
||
|
|
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. |
||
|
|
7e77bf5833 |
Take the layer height from the plate that actually printed
The archive card, the library file details and the slice dialog all read the layer height from a 3MF's project_settings.config. That records the project's settings and can still describe an earlier process or another plate; the plate's own G-code - what the printer executes - was never consulted for it, because the parser read the first 4KB, enough for the layer count in the header block but not for the config block that carries layer_height 14-25KB in. A print running at 0.08 on the H2C archived as 0.2 with the layer count from the same file correct beside it. The plate G-code now wins wherever the two disagree, and the plate that was printed is the one read - the header parse used to take the first gcode entry in the zip regardless of which plate the archive was for. Source 3MFs, which carry no G-code, keep the project value as before. --- Stop carrying a file's layer height over the preset you picked Bambuddy carries a designer's process deviations across a re-slice (#2622) and pre-ticked every one that was not machine-coupled. layer_height is one MakerWorld projects routinely carry, so picking "0.08mm High Quality" for a file whose designer had moved layer height to 0.2 sliced at 0.2 while the dropdown still read 0.08 - the same 0.2 the settings panel showed, tagged "from file". Layer height and first layer height are now classified preset_defining and treated like the machine-coupled keys: offered, never pre-selected. The flag travels on DesignOverride so the modal and the backend agree, and the panel's badge names the conflict and shows the preset's own value next to the file's, so ticking one is a deliberate choice. |
||
|
|
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. |
||
|
|
6488a33488 |
Stop a print with no 3MF borrowing another model's data (#2843)
H2-series and P2S firmware keeps a slicer-sent file on internal eMMC. Port 990 serves external storage only, so there is no file to fetch, and the print becomes an archive with no 3MF -- the ordinary outcome for anyone who sends from Bambu Studio rather than through Bambuddy. Confirmed on the maintainer's own machines: an H2C and an H2D both dispatched brtc://emmc for the same model one minute apart, while an X1C sent ftp:// for it. Three separate defects live in what happens next. The first is the serious one. An archive with no 3MF keeps the path the printer is executing as its filename, and on a sliced job that is always Metadata/plate_1.gcode. The fallback that looks for the same model in the Library or among earlier prints took its search term from there, so it searched for `plate_1` -- a name every Bambu print in existence has -- and matched on a substring, so it also matched any name merely ending that way. On the H2D a 1.6 g Cube resolved to lid_plate_1.gcode.3mf and was costed at 207 g across three real spools. It was not confined to plate names either: in the same database `Bank.3mf` matched "Piggo the piggy bank", and `x1c.gcode.3mf` matched "slice-test-x1c". The matcher now takes the model name the printer reports when the filename is only a plate path, refuses a bare plate stem rather than searching for it, and anchors to a whole filename with LIKE metacharacters escaped, because `_` is a wildcard and model names are full of them. A print that cannot be identified is now left untracked, which is the honest answer -- the previous behaviour was to charge the operator's spools for a model they had not printed. Checked against every row rather than argued from the code. Across 273 library stems the result sets are identical. Across 241 archive stems 14 differ, all of them strictly narrower, and every dropped match is one of the false positives above; all 233 archives still match their own filename, so no legitimate donor was lost. Of the eight no-3MF archives on that install the old matcher picked a wrong donor for two -- one of them a calibration run that would have been charged the 207 g -- and the new one picks none. The second defect is that those archives could not receive a timelapse at all. attach_timelapse derived its destination from the missing file's path, and (base_dir / "").parent is the parent of base_dir, one level outside the data directory. In Docker that is /app, so every attempt failed EACCES and the scan retried and discarded the video 25 times over twelve minutes, roughly a hundred FTPS connections for bytes that had already downloaded successfully. Where that location happened to be writable it was worse: the file landed beside the installation and the attach then failed anyway, because the path could not be made relative to base_dir. #1820 introduced a shared helper precisely so these derivations could not drift apart, and this was the one site still doing it by hand. The directory is created only after the filename has passed the traversal check, so a rejected name still leaves nothing behind. The third is silence. When a print's filament cannot be read from a 3MF, the remaining-percentage delta is the fallback, and that needs a reading at print start -- which a spool without RFID does not have until someone sets a remaining amount by hand. Those slots were skipped with a bare continue. Every other reason for skipping a slot in that loop is logged, and the comment a few lines below argues the case explicitly: charging nothing silently is indistinguishable from having nothing to charge. It now says so, for slots the print actually used. Four existing tests needed updating rather than the production path. They patch backend.app.services.archive.settings by name, and the shared helper reads its own module-level binding, so they kept the real data directory and wrote outside tmp_path -- which is how the first draft of this change littered a working tree. They now patch both bindings. |
||
|
|
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. |
||
|
|
907de4d64d |
Suppress two Bandit false positives in the new FTP and batch-order tests
The 1.2.5.3 code-scanning run flagged two new alerts, both in test files added this release, and both false positives. B402, the ftplib import in the #2780 connect-cleanup tests, is the HIGH finding that failed the check. The test imports ftplib to construct the exceptions BambuFTPClient.connect has to survive -- error_perm and error_temp, at lines 50, 51 and 75. Nothing in the file opens a connection, and bambu_ftp.py already carries the same marker on its own import. B108, the /tmp path in the batch-order archive fixture, is the MEDIUM one. The value is a string written into PrintArchive.file_path so the row has a path; nothing ever opens it. Every other archive fixture in the suite carries the same marker on the same idiom. Both markers follow the wording already in test_bambu_ftp.py and test_sjf_scheduling.py. Bandit's medium+ count over backend/ drops from 17 to 15, and neither file contributes to what is left. |
||
|
|
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.
|
||
|
|
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). |
||
|
|
df5aa04df1 |
Pool AMS backup spools in the print dialog's filament check
The dialog weighed each plate against the spool in the slot it mapped to and knew nothing about AMS Filament Backup, so a two-plate job needing 1441 g of ABS was refused against a 1000 g spool while the identical full spool in the next slot went uncounted. The dispatcher has pooled matching spools since #1762 and would have run the print -- "Print anyway" was always the right answer to this warning. The rule for which spools back each other up now lives in one place: build_slot_materials() in filament_deficit, which the dispatcher's pool and the new slot_materials half of GET /printers/{id}/inventory-remain both draw on. The dialog groups on the keys it is handed rather than resolving spools a second time, which is what let the two answers drift apart, and which also gives the check to Spoolman users -- it read the internal inventory only, so in Spoolman mode it approved everything. Where a pool really is short the warning quotes the pooled totals, since the per-slot figure reads as a contradiction next to a full peer spool. |
||
|
|
15ea11e10b |
Recover the preview slice from custom G-code the sidecar cannot parse
Opening the slice dialog on an unsliced project runs a preview slice purely
to ask the slicer which AMS slots the chosen plate consumes. Bambu Studio 2.8
writes {if timelapse_inline_photo} into the machine's time_lapse_gcode but
exports no definition for that variable, so the template is unresolvable the
moment it leaves Studio: an older sidecar stops with a placeholder parse error
before producing any slice_info. The preview returned nothing and the caller
fell back to guessing from painted faces, silently. On the H2D project this
was found with, the guess dropped the support material -- a whole slot off a
four-filament plate.
Retry the preview once with just the named template emptied, still on the
file's own settings. Keeping the embedded settings is what keeps the answer
honest: overriding the process preset instead discards the project's support
configuration, which loses that slot and moves used_g by up to 2x. Measured
against the same file: retry reproduces all four slots gram for gram, a
printer+process override returns three.
Only templates that cannot extrude are eligible -- a start or filament-change
template lays a prime line or purges, so emptying one would move the very
grams the preview reports, and returning nothing beats a confident wrong
number. Verified on a working H2D slice that emptying time_lapse_gcode leaves
every used_g/used_m in slice_info identical.
Match on a normalised option name: the slicer reports timelapse_gcode while
the 3MF stores time_lapse_gcode, so a literal comparison finds nothing.
Decide whether a retry applies before logging, so a slice that recovers does
not announce itself at WARNING twenty seconds before it succeeds.
|
||
|
|
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. |
||
|
|
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 | ||
|
|
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. |
||
|
|
a9e23910fc | Merge branch 'dev' into feature/billing | ||
|
|
dd1d40b0d4 |
implemented pr (worth fixing) feedback
update commit |
||
|
|
9c86a05657 |
Check filament deficit for Library-backed queue items (#2779)
A job needing 20.5 g was dispatched onto a spool holding 9 g and the printer started. _resolve_source_3mf returned LibraryFile.file_path verbatim, but that column stores a path relative to base_dir -- so it resolved against the process working directory, found nothing, and compute_deficit_for_queue_item treated a missing source as "nothing to verify" and returned no deficit. Every library-backed queue item was affected: Slicer Pipeline jobs, which are always library-backed, and everything added through the Library's bulk Add to queue. Both callers share the resolver, so the Play button on the queue was as blind as the auto-dispatcher. Archive-backed items (print history, VP intake) resolved correctly and were never affected, and neither was PrintModal, which resolves the file on its own path. The library branch now uses the same idiom as the eleven other readers of file_path -- absolute stays, relative joins base_dir. The join carries a SEC-PATH-OK marker: the value is DB-stored and generated by the Library ingest, and it is already what resolves the file for upload, so the check has to resolve it identically or it is not checking what gets printed. A source that is configured but absent now logs a warning naming the item and the resolved path. It still dispatches, because the upload needs the same file seconds later and fails there, where blocking would strand a queue on a moved file -- but a safety check that skips itself must not do so in silence, which is what hid this for every library-backed item. Tests cover the relative path (the reporter's 20.5 g against 9 g), the absolute path against a base_dir the file is not under, and the missing-source warning. The existing cases all used archives with absolute paths, which is the gap the bug lived in. |
||
|
|
1eea194953 |
Resolve a spool's material to a known drying preset before starting a cycle (#2774)
The drying popover prefilled its material from the loaded spool without checking the preset table had that material. An AMS-HT holding Support for PLA/PETG (tray_type PLA-S) fell back to PLA's temperature but kept PLA-S as the material, and the dropdown displays its first option when handed a value outside its list -- so it read PLA while PLA-S was sent. Same gap for every composite: PETG-CF prefilled at PLA's 45C. Resolve the tray_type to a key the table has before setting either value. Support materials and composites resolve to their base, nylon is aliased under its several spellings, and anything unrecognised falls back to PLA -- the coolest row, so an unknown material under-dries rather than deforming a PLA spool. Also record request-topic messages in the MQTT debug log. That topic carries every command a printer is given, including Bambu Studio's, and returned before the logging block -- so a capture could show only what the printer said, never what it was told. |
||
|
|
9a397f46e9 | implemented pr feedback #2 | ||
|
|
43bf854bd5 | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
0596ff424e |
Say why a drying cycle ended when the firmware cuts it short (#2770)
An H2D started a twelve-hour PETG dry at 65 degC and the AMS gave up on it twenty minutes in, with 700 of the 720 minutes still on the clock. It cooled, humidity climbed back over the threshold, auto-drying started another twelve-hour cycle, and that one went the same way; the reporter's AMS temperature history shows the loop running all morning. The log had one line for it: "AMS 0 drying complete", which is exactly what it says for a dry that ran its full twelve hours. Nothing in a support bundle told the two apart, and the one number that does -- the time still remaining -- was written into that line as the previous value, where it reads like a duration rather than a shortfall. The reporter took 700 for seconds and concluded the cycle had lasted twelve minutes. Bambuddy did not stop that cycle; every stop it sends is logged with the full outgoing command and there was none. So ending it was the printer's decision, and the account of why lives in three things already received and parsed and never written down: the drying phase and sub-phase from the AMS info hex, the per-unit dry_sf_reason constraint codes, and the live HMS errors. A cycle that ends with most of its countdown left now logs all three alongside how much of the requested duration ran. One that reaches its duration keeps the single line it has always had. A stop Bambuddy sent is named as ours -- it is short of its duration too, and on the telemetry alone is indistinguishable from the firmware abandoning the cycle, so without tracking it the print-takes-priority stop and the Stop button would both have been blamed on the printer. Diagnostics only. Nothing about when drying starts or stops has changed, and the restart loop is not addressed: what the firmware objects to has to be established before Bambuddy can sensibly decide how long to wait before trying again. |
||
|
|
a74dc7932f |
Cover nested data structures in the HA notify pass-through (#1441)
The three tests around it use flat scalars, which is also all the field's placeholder and the wiki showed, so nothing recorded that the value is forwarded verbatim rather than treated as a key/value list. A reporter asked whether action buttons work; they always have, and now that is pinned. The changelog entry said "nested options work" and left it there. It now names actions and the two things that decide whether the buttons do anything - the mobile_app_notification_action automation, and iOS needing a registered category - since neither is set from Bambuddy and both are what a reader would otherwise have to discover the way the reporter did. |
||
|
|
5efbd353ed |
Survive a directory that defines no posixGroup class (#2769)
Every LDAP user on an lldap directory was rejected with "Incorrect username or password", on an install where Test Connection passed and where the same bind DN, filter and group membership all checked out under ldapsearch. The directory never saw the request. _extract_user_info searches for POSIX groups alongside the memberOf ones, and both of those filters name the posixGroup object class. ldap3 fetches the schema at connect time (get_info=ALL) and validates class names in a filter against it while building the request, raising LDAPObjectClassError before anything is sent. lldap marks every account it creates as posixAccount -- which is what makes us look for POSIX groups at all -- but defines no group class beyond groupOfNames. The exception escaped authenticate_ldap_user, and the login route reports any LDAP failure as bad credentials. A directory with no posixGroup class has no posixGroup entries, which is exactly the answer those searches would have returned. Catch it, log it once, and carry on with the memberOf groups collected above. Both searches sit inside the one try: they name the same class, so once one is rejected the other cannot succeed, and attempting it would only produce a second identical exception to swallow. Not a regression from |