mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
7f74f831c4e0a39f2f27d54e7a09fa14f52ad00a
240
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
89ea337612 |
Recognise our own print dispatch instead of guessing at a magic number
(#2843 follow-up) Bambuddy records the project_file behind every print, because that command names where the sliced file was put and so decides whether the archive can have a thumbnail and slicer metadata at all. Its own dispatches were told apart from a slicer's by testing sequence_id against "20000", on the belief that 20000 was Bambuddy's alone. It never was. 20000 is the slicer convention Bambuddy copied -- virtual_printer/bind_server documents the slicer sending exactly that during detect -- and both slicers count up from it. Measured on the wire: OrcaSlicer dispatched 20000 and then 20001, BambuStudio 20009 and 20010. So whichever dispatch happened to land on the shared value was filed as ours and never recorded, and after a slicer restart that is the first print you send. The test was wrong in the other direction too: it called every value above 20000 external, including ones we had sent ourselves, which only stayed invisible because we always send exactly 20000. Ownership is now established by remembering the job actually dispatched -- sequence id, file, url and subtask name -- and consuming that marker on the echo. One-shot deliberately: a slicer reprint of the same file a moment later is somebody else's print and must not hide behind our last one. Nothing about printing or archiving changes. current_project_url and ams_mapping are both captured before this branch and always were, so the storage verdict that gates the FTPS sweep is untouched; two tests pin that, because it is the part that would actually cost archives if it drifted. What changes is that the diagnostic entry stops lying, and it is the entry that tells an operator whether their printer stores files somewhere Bambuddy can read -- which is the whole subject of #2780 and would have to count, and an undercount there would have been silent. |
||
|
|
c5e0055864 |
Track which nozzle each AMS feeds through the Filament Track Switch
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty. Bambuddy had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The binding needed no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the type and the extruder id -- 0 is In-B, 1 is In-A -- and it only means anything when a switch is installed, because without one 0xE really does mean an uninitialised unit. That gates the read, which forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. The badge letters stay L and R, In-A reading as L, with the inlet named in full in the tooltip -- the letter is the inlet's position and not a claim about which nozzle that AMS feeds, since the switch can route either inlet to either outlet. Both views update live now. The switch fields were missing from the WebSocket payload, and the frontend shallow-merges each push over its cached status, so an absent field kept whatever the last full fetch left behind. The broadcast dedup key had no term for them either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and not in the AMS change-hash, which covers tray fields only and must stay that way because it drives Spoolman sync. The rest of this is the calibration half, which is where it actually bites. K-profiles are calibrated per nozzle and the printer numbers its calibration table per nozzle too, so entry 16 exists on both hotends and means a different profile on each. A tray holds exactly one index. Move an AMS to the other inlet and every configured slot in it silently keeps pointing at the old hotend's table -- measured on the maintainer's H2C, a black PLA calibrated 0.018 left and 0.020 right stayed on the left profile after the move, and a manual RFID re-read only re-asserted the same wrong one. Bambu has not solved this either; their AMS dialog carries "TODO: fila_switcher broken the connection of ams->extruder" above the line that decides which nozzle's profiles to offer. Three copies of "which extruder is this slot on" each ended in else 0, which on a switch machine filed every profile under the right-hand nozzle. They now share one resolver that returns None for "unknown", because unknown and extruder 0 are very different answers on a dual-nozzle machine and conflating them is what bound a left-nozzle profile to a slot sitting on the right. The per-slot K value on the printer card had the same confusion from the other direction: its lookup was keyed on cali_idx alone, so one nozzle's K silently overwrote the other's. Moving an AMS now re-selects each configured slot's counterpart profile for the nozzle it has arrived on. Only the calibration binding moves, and only for slots whose spool already has a profile for that nozzle: configuring a slot is a deliberate preparation step, so a slot we know nothing about, or a spool calibrated on one hotend only, is left exactly as the operator set it. Nothing fires on the first sighting of a binding either, since every reconnect learns them afresh and re-applying there would overwrite a choice made by hand. Configure Slot resolves against the slot's own nozzle throughout. Option identity carries the extruder, so a filament calibrated on both hotends gives two distinguishable entries instead of two that collapse into whichever the printer listed first; options name the hotend; matches are scoped to the nozzle the slot feeds, with the other hotend's profiles still reachable under Other K profiles; and the slot's active index is resolved as a pair rather than followed into the wrong table. Inlet to nozzle is one table, In-A to the left hotend and In-B to the right, measured rather than assumed -- fila_switch.out cannot be used for it, reporting [1, 1] unchanged across a 90-second capture, both outlets claiming the same extruder. The print dialog picks up the same inlet labelling in its slot dropdown, replacing a left/right hint that never rendered because it matched snow-encoded values against global tray ids; decoding it correctly would not have saved it, since the firmware never reports which inlet is currently paired with which outlet. The dialog also notes when every filament a print needs sits behind one inlet, which is legal but slow -- a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS, where a change across the two only retracts as far as the switch. Assigning an AMS to an inlet remains printer-side. BambuStudio can read that binding and has no command to write it, so there is no wire format to copy. |
||
|
|
7a42e0a7e5 |
Show which Filament Track Switch inlet each AMS feeds
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty on these machines. The printer card had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The SpoolBuddy assign modal had the same fallback in a worse form, mapping anything that was not extruder 1 to R. The binding turned out to need no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the AMS type and the extruder id -- 0 is In-B, 1 is In-A -- and it is only meaningful when a switch is installed, because without one 0xE really does mean an uninitialised unit and those bits carry nothing. That gates the read, which in turn forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. _parse_fila_switch is split out and called first, and left in _update_state as well so that stays a complete absorb step. The badge keeps L and R rather than A and B, because the lettering is familiar and matches the physical layout. It is a different colour from the plain nozzle badge, and its tooltip names the inlet in full, since the letter is the inlet's position and not a claim about which nozzle that AMS feeds -- the switch can route either inlet to either outlet. An AMS still reporting a real extruder id keeps its ordinary badge, which BambuStudio also treats as authoritative over any switch binding, and a switch that has been fitted but not yet set up on the printer shows nothing rather than a guess. The print dialog's slot dropdown gets the same label. It replaces a left/right hint that never once rendered: ftsExtruderForSlot compared snow-encoded in[] values against global tray ids and could not match. Decoding it correctly would not have saved it -- the firmware reports which slot sits in each inlet and which nozzle each outlet feeds, but never which inlet is currently paired with which outlet, so no per-slot nozzle can be derived. That function is gone rather than fixed. The dialog also points out when every filament a print needs sits behind one inlet. Bambu's own guidance is that this is legal but slow: a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS before the next can be fed up the shared tube, where a change across the two inlets only retracts as far as the switch. All on one inlet means every change in the job takes the slow path, and moving a single spool fixes it. So it advises, it does not block. Both views update live. Two things were stopping that. fila_switch and ams_switch_inlet were absent from printer_state_to_dict, and the frontend shallow-merges each WebSocket push over its cached status, so a field the push omits keeps whatever the last full fetch left behind. And the broadcast dedup key had no term for either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and it is not in the AMS change-hash either, which covers tray fields only and must stay that way because it drives Spoolman sync. Assigning an AMS to an inlet remains printer-side. BambuStudio can read the binding and has no command to write it -- its switch class is parse and getters only, and the recommended-arrangement popup draws and publishes nothing -- so there is no wire format for us to copy. Adding the two fields to PrinterState broke four test modules whose SimpleNamespace stubs predate them. The stubs are fixed rather than the production reads made defensive: the real dataclass always carries both, and a getattr in the dedup key would silently stop tracking the field if it were ever renamed. |
||
|
|
7a9b4921bd |
Stop the AMS temperature alert firing for heat the user asked for (#1802)
The alert compares against ams_temp_fair, the same threshold that colours the printer card, which defaults to 35C. Drying deliberately runs at 45C for PLA, 65C for PETG and up to 85C on an AMS-HT, and the alert repeats once an hour for as long as the condition holds, so a twelve-hour dry sent twelve notifications about a temperature the user chose. It then kept sending them while the unit cooled back down, which is the half the reporter confirmed on an AMS 2 Pro and an H2C. Dispatch now consults the drying state the firmware already reports. dry_time alone is not enough: it reads 0 through the cooling phase that closes a cycle, so dry_status -- info bits 4-7, already parsed for the drying-complete edge -- carries the rest. That constant moves out of bambu_mqtt into a leaf util rather than being duplicated; drying_preflight would have been the natural home, but it imports printer_manager, which imports bambu_mqtt, and bambu_mqtt is one of the callers. The cool-down afterwards is held by a latch released as soon as the unit reads back at or below the threshold, rather than after a fixed delay, so a 65C cycle in a cold basement and a 45C one in a warm room each get the time they actually need. A two-hour cap bounds the one case the latch cannot resolve on its own -- a unit that never returns below the threshold -- and since such a unit would have been alarming with no drying involved, releasing there restores the ordinary behaviour instead of inventing a new alert. Two exclusions are deliberate. Humidity is untouched, because during drying that reading falling is the whole point. And dry_status 6, HeatOutOfControl, is kept out of the active set: an AMS that has lost thermal control is exactly when the alert should still arrive, so it must never read as expected heat. A cycle plus its cool-down outlasts a restart, so the latch is a settings row rather than a dict beside _ams_alarm_cooldown -- the internal timestamp-row pattern support.py already uses. It is read once per pass and written back only when a unit changed it. Stamps ahead of now are clamped on read, since a box whose clock jumps backwards writes them and suppression is measured as now minus the stamp; without the clamp the cap would measure from a moment that has not happened yet and hold the alert quiet for the skew on top of it. No new setting. The reporter was offered the opt-out checkbox they asked for and said they would not want it if the alert simply never fired during drying. |
||
|
|
fffa68ec55 |
Explain a print that never reached the printer's card, instead of sweeping for it (#2780)
Bambuddy reads a print's 3MF, cover and timelapse over FTPS on port 990, which on every Bambu model serves external storage only. Under some configurations H2-series and P2S firmware keeps the sliced file on internal storage, where Bambu Studio put it over the port-6000 service, and then no path on 990 can find it. The print command has always said which of the two it used -- `url` reads ftp://<name> or brtc://emmc/<name>. We discarded it and swept anyway: ~110 connections per print, all certain to fail, ending in an archive card with nothing on it and no stated reason. In the reporter's bundle all 35 dispatches to their H2C and P2S said internal storage, all 25 to their X1C said external, and all 44 empty cards belonged to the first two. Read the field, skip the sweep when it cannot succeed, and record which reason applied. A printer that uses the card is unaffected, and so is one we have no answer for -- silence is not evidence, and reading it as bad news would break archives that work today. The answer is held per print and dropped when that print ends, rather than kept as a standing fact about the printer. Plenty of prints never announce themselves: 14 of the 79 print starts in that bundle arrived with nothing on the request topic, started from the printer's own screen or picked up after a restart. Left standing, one slicer print to internal storage would suppress the lookup for every screen-started print after it, on a printer whose files really are on the card. The sticky reading is kept for the connection diagnostic alone, which is run after the print that prompted it and would otherwise have nothing to report. Two things that pointed the wrong way go with it. The archives banner told everyone to enable "Store sent files on external storage"; the reporter had it on for the whole three weeks and it would not have helped. The diagnostic passed a printer whose slot was empty, because it read only the toggle -- an empty slot is now a failure naming the slot, and a printer that has storage and still used its own is a warning. On P1-series that empty-slot failure yields to the existing unsupported-model skip: the toggle cannot be switched on there at all, so telling the operator to insert a card would promise a fix inserting a card does not deliver (#2524). Also close FTP sockets on the failure paths, which dropped them for the garbage collector -- 1813 in a day in that bundle -- and drop the advice to restart the printer, which the reporter tried twice while a single manual connection to the same printer handshook cleanly. This does not make the affected prints archive in full; that needs the port-6000 protocol tracked in #2762. |
||
|
|
3954d3a7e6 |
Choose which rack nozzle each filament prints from on an H2C (#1784)
The Vortek rack holds six hotends, and a multi-colour plate is sliced to
use a different one per colour so it can skip the purge. Which of the six
each colour takes is not in the 3MF. The same plate, sliced and sent twice
from Bambu Studio with a different choice each time, produces two files
that differ only in rounding in the last digit of a few extrusion figures
-- the filament grouping, the toolchange stream, the 120 nozzle-change
markers and project_settings.config are all identical. The choice travels
only in the dispatched nozzle_mapping.
Bambuddy had no way to state it, so those plates went out with no nozzle
assignment at all and the printer chose for itself. That is what levelled
on one hotend and printed with another, millimetres above the plate.
Every rack-bound filament now carries a position picker beside its AMS
slot dropdown, listing all six with the nozzle each holds. An empty
position, or one holding the wrong diameter or flow type, is shown greyed
out with the reason rather than hidden, so someone looking for position 4
finds it. The choice is per filament *group* rather than per slot, because
a group is one hotend: two filaments the slicer grouped together share it
and cannot point at different positions.
Nothing has to be picked. Positions are assigned automatically, preferring
one already loaded with that colour, which on the plate this was built
against reproduces Bambu Studio's own pick exactly.
A nozzle currently picked up onto the carriage is offered too. The
firmware drops its rack position from the report entirely rather than
sending a placeholder (#943), and refusing it would rule out the position
most likely to be wanted -- the one the last print left mounted. Only
recoverable when exactly one position is missing; two gaps are genuinely
ambiguous and stay unavailable.
Positions are re-checked at dispatch, not just when queued, because the
rack can be re-loaded in between. The two failure modes differ on purpose:
an explicitly chosen position that no longer fits stops the print, names
what the position now holds, and deletes the uploaded file from the SD
card so it cannot be started by hand either -- an operator who named a
hotend must not silently get a different one. An automatic assignment that
cannot be made instead falls back to letting the firmware choose, which is
what happened before any of this existed.
The pick is stored as {group: position} rather than as the expanded
nozzle_mapping, though that is what goes on the wire. That column means
"Bambu Studio decided, forward verbatim", and only the group-and-position
form can be re-checked against what is actually mounted at dispatch.
The existing multi-rack refusal in extract_nozzle_mapping_from_3mf stays.
It still guards the #2800 fallback, which can only ever name one rack id.
Measured on the maintainer's H2C: rack position n is physical nozzle id
15 + n, confirmed by cross-referencing two captured dispatches against
Bambu Studio's own dialog. extruder_max_nozzle_count names which carriage
is the rack straight from the file, and is read rather than assumed -- a
fourth independent confirmation of the carriage indices fixed in
|
||
|
|
45dc139c41 |
Print an H2C two-nozzle plate from the carriage it was levelled on
The two carriages were the wrong way round: extruder index 0 was treated as
the fixed hotend and index 1 as the swappable rack, and it is the other way
about. A plate using both was levelled with one nozzle and printed with the
other, several millimetres off the plate.
Three sources agree, and disagreed with the code. Telemetry reports
ams_extruder_map {'0': 1, '1': 0, '2': 0}. BambuStudio, dispatching a plate
that used all three of those AMS units, sent the filament from the unit on
extruder 1 to physical nozzle 1 and the ones on extruder 0 to rack positions
16 and 18, and that print completed. And the two constants could not both
have been right: _FIXED_NOZZLE_ID is 1 while the fixed extruder was 0, in a
scheme where physical nozzle id N sits on extruder N.
The old value came from the #2800 hardware A/B, where [17, -1, -1, 1] printed
in mid-air and [1, -1, -1, 17] printed correctly. That result stands -- it
established which wire worked. The extruder indices were not measured by it;
they were inferred by pairing the working wire with a slot_extruders list
produced by the 3MF reader that has since turned out to mis-read exactly
these files. The reasoning is recorded at the constants so a future
regression report is not re-litigated from scratch.
Also withholds nozzle_mapping entirely when more than one filament group
needs the rack. Two groups on one extruder means that extruder is a rack and
the plate wants a different hotend per group -- which physical slot each
takes is the slicer's choice against the live rack and is stated nowhere in
the file, since both groups can carry identical diameter and nozzle type.
Studio dispatched such a plate to 16 and 18; nothing here can reproduce that,
and answering anyway is what printed in mid-air, so the firmware picks.
Restores the fixed 32-entry padding that
|
||
|
|
d1c65a6659 |
Report a print stage we cannot name at the default log level
STAGE_NAMES is hand-maintained and every new model adds to it, so a printer occasionally reports a number that is not in it and the card reads "Unknown stage (72)" -- which an H2C did, where the table runs to 66 and then jumps to 74. Stage transitions were logged only at DEBUG, off in normal running, so the sole record that it had happened was the card itself, and by the time anyone looked the printer had moved on. The asymmetry is the point: a stage we can name is worth DEBUG, and the one we cannot is the interesting one. An unnamed stage is now logged at INFO, once per stage number per session, with the model, the stage it came from and the print state at the time -- which is what naming it afterwards needs. Named stages are unchanged, so a normal print logs nothing new. -1 is excluded: it is Bambuddy's own "not in a stage" sentinel and the field's initial value, so every print would otherwise report it on the way out of its last real stage. Fixes a latent crash found while testing this. The stage-change log line builds its text before the log level is consulted, so get_stage_name runs on every transition whatever the level is set to; a stg_cur that was not hashable -- malformed telemetry rather than an unknown stage -- raised TypeError out of STAGE_NAMES.get and aborted the whole state update. Labelling a value can no longer do that. |
||
|
|
dfeac792fb |
Stop an H2C refusing a multi-colour print as a hotend mismatch
The print uploaded, the printer took the command, and stopped at once with HMS 0500-4047 -- "the available hotend quantity or model does not match the sliced file". nozzle_mapping told the printer one of the plate's filaments went to no hotend while ams_mapping named the tray it comes from, and the firmware will not start a job on that contradiction. Each filament in a 3MF names the group it belongs to, and on every other dual-nozzle Bambu the group number is also the extruder index, so it was read as one. On a rack machine it is not: the rack carriage holds six hotends to the fixed carriage's one, so the slicer writes a group per nozzle rather than per carriage. The failing plate carried groups 0, 1 and 2 against a two-entry physical_extruder_map, and the filament in group 2 was dropped -- indistinguishable downstream from a slot the plate does not print, which is what reached the wire as -1. extract_nozzle_mapping_from_3mf now resolves the group through the table the file states for itself, the <nozzle id extruder_id> elements in slice_info.config. Files carrying no such table keep the direct index, so H2D slices are unaffected. A filament that still cannot be placed drops the whole mapping with a logged reason instead of half an answer: the firmware then picks its own nozzle, which is the pre-existing behaviour and far better than an answer that contradicts itself. Two related faults fixed in the same pass. The mapping was read across every plate in the file, so on a multi-plate project a slot took its extruder from whichever plate came last; it is now scoped to the plate being dispatched, in extract_filament_requirements as well. And the array is now one entry per filament slot, matching BambuStudio's own dispatch of [1, 16, 16] for a three-filament plate, rather than padded to a fixed 32. Verified against the file that failed: slot extruders [-1, 1, 0] became [0, 1, 0], and the wire [-1, 16, 1, -1 x29] became [1, 16, 1]. |
||
|
|
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). |
||
|
|
e9d9e51a81 |
fix(h2c): send physical nozzle IDs for both carriages, not extruder indices (#2800)
The first pass at the H2C rack mapping had both of its hardware-derived values wrong, and the reporter's follow-up A/B on real hardware settled them. The rack does not feed extruder 0. A mixed-nozzle plate extracted as slots=[0, -1, -1, 1] with rack position 17 dispatched as [17, -1, -1, 1], and the rack nozzle printed several millimetres above the bed. The rack is extruder 1. Correcting only that is not enough. The fixed hotend answers to physical ID 1, not to its extruder index of 0, and forwarding the index produced [0, -1, -1, 17] -- a command the printer rejected outright rather than mis-printing. Translating both carriages gives [1, -1, -1, 17], and the same sliced file then cleaned, levelled and printed on the correct nozzle at the correct Z through to completion. Both values agree with three native Bambu Studio captures from the same machine, which carry [1, 17, ...] and [17, 1, ...] depending on filament slot order. An extruder index naming neither carriage now omits the field instead of reaching the wire as a physical ID that identifies no nozzle. The fixed-hotend-only path is unchanged but no longer a guess: Bambu Studio sends no nozzle_mapping at all for such a plate, which is what Bambuddy already did. Reported, diagnosed and hardware-verified by @tru3l3gend, who ran the mixed-nozzle A/B on both nozzles and captured what Bambu Studio sends for fixed-only and mixed plates. |
||
|
|
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. |
||
|
|
a9e23910fc | Merge branch 'dev' into feature/billing | ||
|
|
dd1d40b0d4 |
implemented pr (worth fixing) feedback
update commit |
||
|
|
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. |
||
|
|
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. |
||
|
|
48c231d8ce |
Stop a drying cycle reporting itself finished a minute in (#2759)
Starting the dryer on an AMS 2 Pro holding two PETG and two PLA spools and picking PLA showed "PLA @ 45°C" for about a minute and then switched to "PETG @ 65°C" for the remaining twelve hours. Bambu never echoes back which filament or temperature a cycle is running, so the badge reads the target cached when the command went out, and that cache had been dropped. Between accepting the command and settling its countdown the firmware publishes one update with the remaining time at zero while the unit is still in its Checking phase -- the reporter's log has 720, then 0, then 719, and four seconds later the same unit's info hex decodes to dry_status 2, Drying. The falling-edge detector read that zero as the cycle ending. Losing the cached target left the badge to guess from the first loaded slot, which was PETG, and its RFID-recommended 65°C. The same false ending fired on_drying_complete, so anyone with smart-plug auto-off-after-drying switched on had power scheduled to cut one minute into a twelve-hour dry; the reporter had it off, which is the only reason this reads as a cosmetic bug. A remaining time of zero now ends a cycle only when the unit is not also reporting an active phase. dry_status comes from the same info hex already parsed a few lines above, so this costs nothing to check. Stopping and Error are deliberately not treated as active -- those should end it -- and a unit that reports no phase at all still ends its cycles, so the gate can only ever suppress on positive evidence that the cycle is live. A suppressed edge leaves the remembered dry_time alone, exactly as the #1462 absent-value skip does, so the push that really ends the cycle still sees a non-zero previous. The fallback guess is tightened to match. On a mixed unit the first tray is evidence of nothing, and naming a temperature the cycle is not using is worse than naming none, so it now answers only when every loaded spool is the same filament and otherwise leaves the badge showing the countdown alone. Both the websocket and REST status builders carried their own copy of that loop; they now share one helper, which also takes the temperature from the first slot that carries an RFID one rather than giving up when slot 1 holds a third-party spool. |
||
|
|
4af782cc2f |
Report a refused AMS filament setting instead of discarding it (#2756)
Configuring a slot publishes ams_filament_setting and the printer answers with a verdict. The answer was received and dropped at DEBUG, so a refusal left no trace at the level support bundles are collected at: the reporter saw six Configure Slot attempts on an X1C all return success, all read back by the #2582 verification as holding the previous profile, and no record of what the printer said about any of them. Promote a non-success response to INFO with result, reason, ams_id and tray_id. Refusals only -- unlike extrusion_cali_set (#2718) and ams_filament_drying (#1447) this command is not rare, since every spool assignment and K-profile re-apply sends one, so promoting each ack would bury the interesting line. The developer-mode probe is excluded: it sends this same command to the external slot expecting a refusal on P1 firmware, so promoting it would put an alarming line in every P1 bundle on every reconnect. Matched by sequence id, which user commands cannot collide with -- they publish a hardcoded "0". Diagnostics only; no change to which commands are sent or how they are built. |
||
|
|
18938a10ee |
fix(kprofiles): stop reporting rejected K-profile writes as saved
Saving a K-profile was fire-and-forget. set_kprofiles_batch published and returned True, and the printer's extrusion_cali_set answer was logged at DEBUG and dropped, so a write the printer refused was reported to the user as saved (#2718, reporter @jmoore-skild). The reason it could not simply be gated on: the answer itself was wrong. Single-nozzle firmware returned result:"fail" with reason:"invalid tray_id" on writes that demonstrably applied. Measured against an X1C and an H2D over MQTT, the cause is the tray_id:-1 Bambuddy itself put in the payload. Sending three otherwise identical writes isolated it: tray_id:-1 fails, tray_id:0 succeeds, and cali_idx:-1 is accepted either way, so only that one field is at fault. The H2D ignores the value entirely; the X1C validates it, complains, and applies the write anyway. BambuStudio always sends a real tray_id and defaults it to 0 for a manually entered profile. With tray_id:0 the acknowledgement is honest, and the printer echoes back the sequence_id we sent -- confirmed for extrusion_cali_get, _set and _del on both printer classes -- so it can be matched to the write that caused it. Writes now return their sequence_id and the routes await the verdict, turning a real failure into an error that carries the printer's own reason. A printer that stays silent is still treated as success: no answer is not evidence of refusal, and firmware that never answers must not turn every save into an error. Raises the ack to INFO. It sat at DEBUG, so the one line that explains a failed save was absent from every support bundle -- the same reasoning that put ams_filament_drying at INFO for #1447. Also fixes extrusion_cali_set building its payload from str(self._sequence_id) without incrementing first, reusing the previous command's id. Harmless while nothing correlated on it, fatal now that the write path does. Adds supports_nozzle_flow_type() for the Standard / High Flow choice, which the K-Profiles UI previously showed as "Not reported by printer" -- not a value anyone can save. Most printers omit the nozzle identity from their calibration table entirely, and the slicer treats that as Standard rather than unknown; Bambuddy now does the same and keeps the choice editable. The field is hidden only where the model ships a single nozzle variant, using the slicer's own rule (len(nozzle_volume) // len(nozzle_diameter) > 1 over the machine preset) evaluated across every bundled Bambu profile. That puts only A1, A1 Mini and A2L on the hidden side -- it is not the single- versus-dual-nozzle split, since P1P, P1S, P2S, X1, X1C, X1E and H2S are all single-nozzle and all carry two variants. Editing a profile also no longer writes back an empty nozzle_id. Wiki records that on printers which omit the field the chosen flow type is discarded by the firmware and reads back as Standard, in Bambu Studio as well, so it does not get filed as a bug again. |
||
|
|
a35ba8fa5f |
fix(kprofiles): read the nozzle diameter the printer actually sent (issue #1748)
Every K-profile came back as 0.4mm on printers running any other nozzle (#1748, reporters @Liquidmasl and @jmoore-skild). The printer puts nozzle_diameter on the extrusion_cali_get envelope only; the per-filament entries carry setting_id, filament_id, name, k_value, n_coef and cali_idx, and nothing else. The parser read the field per entry with a hardcoded "0.4" fallback, so the fallback fired on every profile of every response. The envelope value was already in scope, read into response_nozzle and used only to match the request. This never reproduced on H2D because that firmware does include the field per entry. Both construction sites are in the same handler, so the code path is shared; what differs is the payload, and every single-nozzle model omits it. The display was the least of it. Editing is delete-and-re-add on single-nozzle printers, and the dialog rebuilt nozzle_id and nozzle_diameter from its own greyed-out selects, so saving an untouched 0.6mm profile rewrote it on the printer as HH00-0.4. Deleting aimed extrusion_cali_del at the wrong nozzle the same way. Both now pass through what the printer reported. The cali_idx cascade in inventory.py, spoolman_inventory.py and spoolman.py matches on nozzle_diameter, so on a 0.6 or 0.8 nozzle it never found the printer-side entry and the assignment silently failed to stick -- that is the "cannot auto-map a K-profile" half of the report, fixed at the source without touching those three call sites. nozzle_id has no source in the payload at all, and state.nozzles carries material (hardened_steel), not flow, so it cannot honestly produce HH/HS. Rather than keep inventing one, the UI now says the printer did not report it: the card shows the diameter alone, the dialog shows "Not reported by printer", and the High Flow / Standard filter is hidden instead of being offered as a control that can only ever empty the list. Import stops stamping HH00 on profiles whose source reported none. Also correlates K-profile requests by sequence_id. Responses were matched by nozzle diameter through a single shared expectation slot, so a second request overwrote the first's and the first's valid answer was discarded as a mismatch -- the "Failed to get K-profiles after 3 attempts" in the same logs, with the printer having answered correctly both times. Pending state is now one entry per request, keyed by the id we already send, with the nozzle match kept as a fallback for firmware that does not echo it back. Fixes the flow-type select naming a new profile with the opposite label, which contradicted the identical expression 44 lines above it. |
||
|
|
037688514a | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
5e2b7b53e6 |
fix(printers): surface the printer's own "command verification failed"
A P1S on firmware 01.10.00.00 rejected every control command and said so: HMS 0500-0500-0001-0007, "MQTT command verification failed". Bambuddy received that, dropped it, and reported a healthy printer instead. The frontend filtered it out. This code's meaning lives in attr's low half (0500) and code's high half (0001), both of which the MMMM_EEEE short form discards, so it collapsed to "0500_0007" — no catalog entry, no firmware actions, and filterKnownHMSErrors drops uncatalogued action-less errors. Catalog lookups now try full_code first, in both the description and the filter, and errors matched that way display the four-group code the printer's own screen shows. The remedy line is ours, not Bambu's: their wiki says to update Studio or Handy, which does not apply to a print sent from Bambuddy. The developer-mode probe made it worse. It read anything that was not an explicit refusal as confirmation, and this firmware answers the probe with an empty result while refusing everything else — so an inference drawn from a non-answer became "developer_mode: pass" in the support bundle of a printer that had not accepted a command all day. The probe now has three outcomes: explicit success enables, explicit verify-failure disables, anything else stays unknown and the diagnostic reports skip. The HMS is authoritative over that inference in both directions. It forces developer_mode False when present, and clears back to unknown when the printer stops reporting it, so enabling Developer Mode and restarting the printer is picked up without restarting Bambuddy. Dispatch no longer treats a refusal as a wedge. The watchdog latches the HMS across both phases and fails the item on the first attempt naming the code and the fix, rather than spending three uploads and 270s a lap to arrive at a message about SD cards. The check runs after the active-state exit in both phases, so a lingering HMS can never abort a print that is visibly running. Also: the "wrong or mis-cased serial number" hint no longer fires in the moment after a reconnect. _report_messages_since_connect is reset by _on_connect, so a reconnect landing microseconds before the staleness check leaves it at 0 for reasons that have nothing to do with the serial — this reporter's healthy printer was told to go check its serial 1 ms after reconnecting. |
||
|
|
db6cdb0745 |
fix(camera): take the finish photo when the print ends, not when its last layer starts (#2547)
The photo fired the moment layer_num reached total_layer_num. That edge is where the printer *starts* its final layer, not where it finishes it: the reporter's H2C capture shows it arriving at 92% with mc_remaining_time=2, three minutes and seventeen seconds and one filament change before the print actually ended, so the frame caught the toolhead mid-print over the model. The trigger also latched _finish_photo_captured, which locked out both the stage-22 and FINISH triggers for the rest of the print — so on firmware that never reports an end-of-print filament unload (H2C and A1 Mini confirmed) nothing could replace the bad frame. Remove the last-layer trigger. The photo is now taken at the FINISH-state trigger, which every model sends and which lands after the toolhead parks. Since Bambu's end G-code drops the plate ~100mm just before that, restore the framing before capturing: absolute G90/G1 Z to max_z_height + 10mm clearance, settle, capture, then drop it back so the print is as reachable as the printer left it. Absolute is the safety argument — that Z is a height the toolhead occupied seconds earlier, so it is inside the travel limits by construction and leaves the nozzle above the part, and it is unambiguous across model families because Z is the nozzle-to-bed gap whether the bed moves or the toolhead does. M211 is never touched (#2579). This is what #1145, #1397 and #1565 asked for. The height is only trusted when two independent sources agree: the archive is matched by the finished print's subtask_name by equality (not LIKE, so "Cube" cannot resolve to "Cube v2"), and its layer count from the 3MF must match the layer count the printer reported over MQTT. Matching on "most recent archive for this printer" was not safe — on_print_complete pops the _active_prints binding concurrently, and a print Bambuddy failed to archive would have resolved to its predecessor. A wrong height is the one failure that could drive the nozzle into the model. The move is additionally skipped when the print height is unknown, when a queue item is pending for the printer, when the printer has left FINISH, and when the new finish_photo_restore_plate setting is off. for every FINISH-state capture — which is what shipped the mid-print photo — the bank is used only when the dispatcher recorded that it injected End G-code into this print, since a SwapMod snippet may have ejected the plate. The flag is handed over in two steps (mark_pending at dispatch, adopt at print start) so it can never outlive its print: a job started from the slicer or SD card adopts False rather than inheriting its predecessor's answer. Those prints also skip the plate move outright, bank or no bank. The bank now refreshes on mc_percent advances as well as layer changes, via a new on_print_progress callback. Layer changes stop the instant the final layer begins, which left the #1867 fallback frame stale by the whole length of that layer; progress keeps ticking there and freezes before the End G-code runs, so a swapped plate still cannot reach the bank. The last-layer throttle exemption is dropped, since it would now fire a grab on every percent tick. On the timelapse path the moment producer returns early, so the consumer does the restore itself before its live-grab fallback — the documented usual outcome on P1-series, where the video has not transferred by the time the notification goes out and the shipped photo was of an already-dropped plate. The two waits are now derived from the settle window and the video poll timeout rather than hardcoded; at the old flat 75s that fallback was guaranteed to be cut off mid-settle. extract_max_z_height_from_3mf reads only a bounded prefix of the plate G-code, since a sliced plate is routinely tens of megabytes and the header is ~40 lines. It returns None for missing, unparseable, zero and negative values so callers must treat "don't know" as such rather than defaulting. |
||
|
|
449060ce0a | bugfix (aborted prints were not billed in specific cases) | ||
|
|
ac3e3cc60f |
fix(printers): don't retract a fan kit on a partial airduct frame
device.airduct is pushed field by field - the modeCur handler reads it with an "in" check for that reason - so a frame can carry parts without carrying every fan. Absence in that list is what tells us a kit is not fitted, and taken from a truncated frame it made both accessory badges vanish mid-print and started rejecting fan=aux2 on a printer that has the fan. A parts list now counts as a full inventory only when it carries ids 1 (part cooling) and 2 (aux). Neither is optional on a machine that reports an airduct at all, and both appear in every layout in the support-package archive - P2S base 1,2 / P2S+kit 1,2,3 / X2D 1,2,3,10 / H2C,H2D,H2S 1,2,3,6. Anything narrower is a diff frame: its speeds are applied, presence is left alone. Presence can still be added from a partial frame; only retraction needs the full list, so a kit that really is removed still disappears. Also compose showChamberFan from both model lists rather than branching between them, so the P2S/X2D entries in MODELS_WITH_CHAMBER_FAN stay reachable instead of reading as dead, and note in the fan-speed docstring that the aux2 gate also rejects between connect and the first airduct push. |
||
|
|
9ebfcddbdb | Merge branch 'dev' into feature/p2s-x2d-accessory-fans | ||
|
|
ad785a95cb |
fix(queue): withdraw an expected print when the command never goes out
feat(db): warn when the connection pool can outgrow the PostgreSQL server fix(mqtt): an unusable layer_num must not drop the printer connection test: patch settings.base_dir via monkeypatch so it unwinds on error test: restore the config module after reloading it |
||
|
|
beca3a8d73 |
fix(mqtt): keep the layer total that arrives with the print-start frame (#2702)
fix(support): redact push_status values, not the serialised JSON (#2702) |
||
|
|
b938b83136 |
fix(tests): add the new fan fields to the plate-clear status fixture
mqtt_relay reads state.left_aux_fan_speed, but the SimpleNamespace fixture in test_plate_clear_mqtt_notification enumerates its fields explicitly, so the two status-payload tests raised AttributeError. I updated the equivalent fixture in test_printer_manager_status_broadcast and missed this one — running only the touched suites is what hid it. Also addresses the round-2 review notes: - exhaust_fan_present: documented that the H2 series reports part 3 too, so the flag is not model-specific despite the name. - Mask the part id after shifting, matching get_flag_bits(id, 4, 8), for consistency with the state decode. No behaviour change for any observed id. - Noted the unmapped H2 id 6 beside the id branches. - Reject fan=aux2 when the printer reports no left_aux_fan_speed, so a POST against an A1 no longer sends M106 P10 for absent hardware. The UI already hid the badge; this closes the same hole on the API. - Added a test asserting EXHAUST_FAN_LABEL_MODELS and the frontend's MODELS_WITH_EXHAUST_LABEL cannot drift apart. |
||
|
|
f2babc9027 | Merge branch 'dev' into feature/p2s-x2d-accessory-fans | ||
|
|
c1a4c99059 |
feat(diagnostics): log end-of-print telemetry for finish-photo trigger research (#2547)
The finish photo needs a "printing done, toolhead parked, filament unload not started" moment. stg_cur=22 was meant to be it (#1721) and fires on no model in the field: across 247 support bundles there is not one FINISH PHOTO MOMENT (stage-22), including the 2026-06-13..07-08 window where it was the only pre-FINISH trigger in the code — 104 captures on A1, A1 Mini, H2C, H2D, P1S, P2S, X1C and X2D, all of them the FINISH fallback. A replacement can't be designed from the bundles we have. Out of that window Bambuddy parses only stg_cur and mc_print_sub_stage; every other stage/action field arrives and is dropped unread. The candidates that sound right (print_real_action, mc_action, mc_stage) are absent from A1/A1 Mini/P1S payloads, so none of them can be the universal answer alone. Dump the raw fields for the window between the last object layer and gcode_state=FINISH at DEBUG. Opens on the first end-of-print signal (last layer, progress >= 99, or no remaining time) so a dropped layer_num packet doesn't lose it, logs only what changed frame to frame, closes on the transition out of RUNNING, and arms once per print. Instrumentation only: gated on DEBUG being enabled, read-only against printer state, wrapped so it cannot break ingest, and capped at 400 frames per print. The probed fields are stage codes, counters and bitfields — nothing identifying, and no access code. |
||
|
|
91269f14fe |
fix(mqtt): report why a printer refused the connection instead of looping silently
A printer with a wrong access code gave no explanation anywhere. The connect callback's failure branch was a bare `state.connected = False`, discarding the CONNACK reason code the printer had just sent, so the only trace was paho's follow-up disconnect -- logged every 30 seconds as "rc=Unspecified error", which is exactly what a powered-off printer produces. In the report behind this fix one of three printers had been in that loop for the whole capture, and neither the log nor the support bundle could say why. Bambu speaks MQTT 3.1.1, whose CONNACK return codes 4 and 5 paho maps onto reason codes 134 and 135. Both are now logged with the printer's own reason string and, for those two, the remedy: the access code is regenerated whenever LAN Only or Developer Mode is toggled, so it has to be re-read from the screen. The access code itself is never logged -- it would land in every bundle. The reason is kept on the client as a stable slug and plumbed through test_connection into the connection diagnostic, which now distinguishes two cases it previously conflated. "The printer refused our credentials" is asserted only when the printer said so; when all Bambuddy knows is that there is no session, the text hedges and names the alternatives (rebooting, or already at its limit of simultaneous connections). The old wording claimed the access code was most likely wrong in both cases. Frontend needed no change -- ConnectionDiagnostic already renders `<status>_<reason>` variants with fallback to the plain per-status text, so an unrecognised slug degrades to today's wording rather than a missing key. |
||
|
|
d0efb9db9e |
fix(vp): relay A2L AMS filament to the slicer instead of blanking every slot
Every slot of the A2L's AMS Lite rendered as "?" in Bambu Studio through the
Virtual Printer while Bambuddy's own AMS card was correct, and a filament set
by hand in Studio reverted about a second later.
The A2L reports its AMS Lite as physical unit id 16 but packs the slot presence
bits at base 24, so bambu_mqtt normalises the id to 6 at the ingest boundary and
every internal reader gets the right bits. The VP bridge is not downstream of
that: BambuMQTTClient._on_message fans raw payload bytes out to raw-message
handlers before parsing, so mqtt_bridge._on_printer_raw does its own json.loads
and still holds id 16. It then called the shared apply_tray_exist_bits, which
computed 16*4 = bits 64-67 -- never set -- concluded all four slots were empty,
and wiped tray_type / tray_color / tray_info_idx / tag_uid / tray_uuid / remain
from the copy sent to the slicer. That runs on every push, which is why a manual
pick could not survive the next 1 Hz cached-as-base report.
apply_tray_exist_bits now folds the unit id through normalize_am_unit_id, so 16
and 6 land on the same bit base whichever id the caller holds. The bridge's
cached ids stay physical on purpose -- Studio addresses the Lite as 16, sending
ams_get_rfid {ams_id: 16} through the VP -- so normalising the cache instead
would have broken the slicer's own command path.
Confirmed from the reporter's debug log, which shows the cleanup clearing slots
at bits 64-67 under the VP's log label. Before #2670 added the
0 <= ams_id <= 15 range guard this wiped the slots; after it, unit 16 fell out
of the guard and the A2L got no empty-slot cleanup at all -- two different wrong
answers, both fixed here.
|
||
|
|
84a7b797cd |
fix(printers): decode airduct part state from its low 8 bits
Review feedback on #2691: `state` is bit-packed like its sibling `range` (end << 16 | start), and Bambu Studio decodes it with get_flag_bits(state, 0, 8). Masking with & 0xFF before clamping means a packed value decodes to the real percentage instead of clamping to 100. Also moves the uses_exhaust_fan_label import to the top of printers.py with the other imports. Tests: packed value (60 << 16 | 45) decodes to 45, and plain 0-100 values round-trip unchanged. |
||
|
|
9ee162d51a |
feat(printers): expose P2S/X2D accessory fans (left aux + exhaust)
The P2S/X2D have two fans bambuddy didn't fully handle. On the P2S both are add-on kits; on the X2D they ship from the factory. 1. Left auxiliary part cooling fan — not shown or controllable. It is reported ONLY as device.airduct part id 10 (raw id 160 >> 4; FAN_REMOTE_COOLING_1 in Bambu Studio's DevFan::ParseV3_0) and is never mirrored into a flat big_fanX_speed field, which is why it was invisible. This is the gap identified in #2576, where the single 'Auxiliary' fan (big_fan1 / M106 P2) only reaches the right-hand aux fan. 2. Chamber exhaust fan — shown on every P2S labelled 'Chamber Fan'. On P2S/X2D Bambu's firmware/UI (and Bambu Studio's FAN_CHAMBER_0_IDX) call it 'Exhaust', and it is a kit on the P2S rather than built in. Both are now detected from device.airduct.parts, which lists only the fans that physically exist, so each tile appears only when the hardware is present. - bambu_mqtt: parse airduct part 10 -> left_aux_fan_speed (None when absent) and part 3 presence -> exhaust_fan_present; set_fan_speed() accepts index 10 plus a set_left_aux_fan() helper - schema / status route / printer_manager broadcast / mqtt_relay expose both fields - POST /printers/{id}/fan-speed accepts fan=aux2 -> M106 P10, the command Bambu's official P2S machine profiles use - frontend: 'Left Auxiliary Fan' tile shown when reported; big_fan2 tile labelled 'Exhaust' and presence-gated on P2S/X2D, unchanged 'Chamber Fan' elsewhere - i18n: leftAuxiliary + exhaust for all 12 locales Verified fan -> field map on a live P2S (fw 01.02.00.00), stable across cooling and heating airduct modes: Part cooling -> cooling_fan_speed / airduct id 1 (built in) Aux -> big_fan1_speed / airduct id 2 (built in) Exhaust -> big_fan2_speed / airduct id 3 (kit) Left aux -> airduct id 10 only (kit; forced off in heating by mode config) Tests: airduct id-10 parsing (raw 160 -> id 10, not literal 160), id-3 presence, base-P2S absence, diff-push survival, clamping, malformed entries, M106 P10 emission, invalid-index rejection; fan-speed API aux2->10 mapping; frontend tile presence and labelling per model/kit. |
||
|
|
13183e365a |
fix(ams): clear empty AMS-HT slot via its real tray_exist_bits position (#2670)
After #2594 every empty-slot clearing path skipped AMS-HT units, so a removed HT spool never cleared on the printer card while Bambu Studio correctly showed Empty. Root cause: the HT presence bit is packed as a single consecutive bit in tray_exist_bits at 16 + (ams_id - 128) (HT-A=16, HT-B=17, ...), not the regular ams_id*4 position -- so the bitmask cleanup, which skipped id>=128, never touched it. The HT's state field is firmware-variant (loaded reports 11 on H2D, 9 on the #2594 firmware) and it keeps echoing stale tray_type after removal, so the bitmask is the only reliable, firmware-independent signal. Confirmed against a live H2D capture (loaded=0x10f7f, empty=0xf7f) and the OrcaSlicer DevFilaSystem.cpp reference (is_exists = tray_exist_bits >> (16 + (ams_id-128))). - apply_tray_exist_bits: handle AMS-HT (128-135) at 16+(ams_id-128) instead of skipping it; clears an empty HT and, because a loaded HT keeps its bit set, never wrongly clears one (keeps the #2594 fix intact). Ids outside the known regular (0-15) and HT (128-135) ranges are left untouched rather than guessed. - Build the AMS change-hash from the merged state, not the raw payload, so a removal signalled only by the bitmask (firmware still echoing tray_type) still flips the hash and fires on_ams_change to unbind the spool_assignment row. - Emit the exists presence bit in the websocket status serializer (REST already did) so the card renders "Empty" instead of "?" where state is ambiguous. |
||
|
|
1234f0830a |
fix(mqtt): don't let K-profile responses clobber the nozzle size (#2663)
Fetching K-profiles probes every nozzle size in turn (extrusion_cali_get for 0.2/0.4/0.6/0.8mm), and each response echoes the requested diameter at the top level. _process_message passed every "print" message -- including these responses -- to _update_state, which treats a top-level nozzle_diameter as the installed hardware. So the last size probed (0.8) overwrote the real nozzle in memory; a genuine status push corrected it and the next K-profile fetch broke it again, which is why it flickered between 0.8, empty and the correct 0.4. Since 1.2.5 the #1899 mismatch guard then refused to dispatch, failing prints with a bogus "printer has 0.8mm". Handle extrusion_cali_get responses only via the K-profile parser and skip _update_state for them, mirroring the existing get_accessories guard. The nozzle size now comes solely from the real status push. In-memory only -- affected printers self-correct on the next push after updating. |
||
|
|
56accd24de |
fix(smart-plugs): don't blank printer state when an accessory plug switches off (#2629)
An end-of-print auto-off on a plug that powers a filter fan marked the linked printer offline and forced its state to "unknown". The mark was unrecoverable: connected heals on the next MQTT message but state does not (only frames carrying gcode_state rewrite it, and steady-state push_status frames are partial), so the printer stayed "unknown" until a manual Force Refresh and the queue never dispatched to it again. The offline mark is now an explicit presumption: mark_power_off records the state it overwrites and _on_message undoes it as soon as the printer sends another report on its own topic, since inbound traffic proves the power was never cut. A reconnect discards the saved state, so a genuine power cut is unaffected. Each plug also gains a controls_printer_power flag (default true, backfilled) that gates all five power-off paths, and the queue's power-on step now picks the flagged plug instead of whichever linked plug came first. |
||
|
|
fb11adc8fb |
fix(a2l): normalise AMS Lite unit 16->6 so slots load and deduct (#a2l-am-unit-16)
The A2L reports its 4-slot AMS Lite as unit id 16, but its slot-presence
bitmasks sit at bit base 24 (id 6) and it reports tray_now as a local 0-3
slot. Fed the raw id 16, the ams_id*4+slot convention probed bits 64-67
(always zero) and marked loaded slots empty; the local tray_now was read as
global, so usage deducted from the wrong spool (or not at all); and the
ams_id<=7 DB constraint rejected id-16 Spoolman links.
Normalise the Lite 16->6 at the MQTT ingest boundary so global tray ids land
at 24-27 - matching the firmware's own bit base, working with every existing
ams_id*4+slot consumer, colliding with nothing, and passing the DB
constraint. Globalise tray_now to 24+slot, widen the valid-tray guards, label
the unit "AMS Lite", and build the confirmed ams_mapping2 {ams_id:16,
slot_id:0-3} / flat 0-3 for dispatch. Outbound slot commands translate 6->16
on the wire via a single helper. Self-scoping: only unit id 16 is touched, so
all other printers/AMS types are unaffected. One uncaptured wire field (the
physical global tray on load/cali) is extrapolated and isolated to the helper.
|
||
|
|
2e74f2ad41 |
feat(ams): confirm spool assignments landed instead of fire-and-forget (#2582)
Assigning a spool to an AMS tray pushed ams_filament_setting + extrusion_cali_sel and reported success immediately, whether or not the tray accepted it. A silently-dropped assignment never surfaced, and since a print only deducts from the spool on the exact tray it pulls from, it also recorded no filament usage - which made the whole thing feel random. Read the AMS telemetry back after every assign (inventory assign_spool and the Configure Slot modal) and toast the outcome: loaded when the tray echoes the pushed tray_info_idx, a warning when the filament loaded but the K-profile (cali_idx) did not, or not-confirmed after ~30s. Verification uses the periodic per-tray push (the command ack hardcodes sequence_id 0 and can't be correlated); an on-demand pushall is nudged so it lands quickly. Covers regular AMS, AMS-HT and external slots; stays silent rather than inventing a failure if the printer goes quiet. The read-back check runs on every AMS push because the change-hash excludes tray_info_idx. |
||
|
|
2e45893dd5 |
feat(print-options): add "Auto" state to bed levelling, flow & nozzle-offset calibration
Bed levelling, flow calibration, and nozzle-offset calibration were on/off only, so the sole way to run bed levelling was to force a full level before every print. Bambu Studio has always offered a third "Auto" state that lets the printer skip the calibration when it was done recently -- the state most users actually want. Make these three options tri-state (off/on/auto), defaulting to auto, and leave vibration/layer-inspect/timelapse as on/off (Bambu Studio exposes no auto for those). Wire encoding follows Bambu Studio's source exactly: each option sends a JSON bool (true only for "on") plus a companion int -- off=0, on=1, auto=2. The bool fields stay booleans (the #1478 H2S regression); only the companion int widened from {0,1} to {0,1,2}. #1721's observation that stage 8/39 stays queued when sending 2 is the auto contract (queued, skipped at runtime if recent), not a broken "off". - schemas: TriState = Literal[off/on/auto] with a BeforeValidator coercing legacy bool / 0-1 / true-false so old clients and un-migrated rows validate - model + migration: boolean columns -> String; SQLite via column affinity + data backfill, PostgreSQL via ALTER COLUMN TYPE guarded on information_schema (verified on both dialects); settings rows normalised true/false -> on/off - MQTT: start_print takes the tri-state strings and emits the paired bool+int - Virtual Printer: reconstructs the slicer's auto/on/off from the int companion (auto_bed_leveling / extrude_cali_flag) in both capture paths - frontend: CalibrationMode type; off/auto/on segmented controls in the print dialog, queue bulk-edit, and Settings -> Workflow; calibrationMode_* strings in all 11 locales |
||
|
|
059b733c41 |
fix(ams): don't wipe an AMS-HT spool on a partial state=9 update (#2594)
The AMS merge clears a tray on a partial {id, state} update when state != 11
(the 4-slot AMS "emptied slot" signal, #784). An AMS-HT (single-tray high-temp
dry box, id >= 128) reports its loaded tray as state=9, so the partial the
printer sends on power-on was misread as "emptied" and wiped the HT-A spool's
tray_type/RFID/assignment seconds after power-on.
Skip the state-heuristic for HT units (id >= 128). Genuine HT removal still
clears via the explicit tray_type="" update and tray_exist_bits cleanup;
regular AMS (id < 128) is unchanged.
|
||
|
|
d7093c7fe4 |
fix(dispatch): don't send start-print to a busy printer; scheduler defers (#2598)
start_print() published project_file guarding only on connection state, so a re-dispatch onto a printer that had already started — e.g. a watchdog revert (#2555) after the printer sat in FINISH past accepting the job — collided with the live print. The firmware answers 0500_4004 ("Device is busy and cannot start a new task"), which on an A1 mini cancels the running job. Defense-in-depth at the paths that can reach a busy printer: - bambu_mqtt: refuse to publish project_file when gcode_state is PREPARE/SLICING/RUNNING/PAUSE and return without sending. This is the one publish choke point every dispatch path funnels through (queue scheduler, manual start, webhook, Virtual-Printer forward). IDLE/FINISH/FAILED still start. - print_scheduler: re-check the live printer state right before the FTP upload and defer a busy printer (leave the item pending for a later tick) instead of uploading and dispatching. If the printer goes busy in the upload window and the start is refused, revert the item to pending rather than marking it failed — a busy printer is a deferral, not a failure. A transport-level MQTT QoS-1 replay on reconnect would bypass the client guard, but the dispatch/watchdog reconnect path already hard-resets the client with a fresh session, so it has no inflight project_file to replay. |
||
|
|
88ae595268 |
fix(dispatch): honor a resolved AMS mapping over a stale use_ams=false (#2595)
A print sliced against a Virtual Printer carries use_ams=false — a VP advertises no AMS, so the slicer sends it and VP intake stamps it on the queue item. But an "Any [model]" item is colour-matched to a real printer at dispatch, resolving a real AMS slot in ams_mapping. The command builder only ever forced use_ams off (all-external) and never back on, so the stale false shipped with a real-tray mapping and the printer aborted at layer 0 on the empty external spool. For single-nozzle printers the mapping is now authoritative: a real tray (0-253) forces use_ams=true, explicit external (254/255) forces it false, and an unresolved -1 does neither (preserving the #2589 contract). Dual-nozzle is untouched — use_ams is nozzle routing there. The correction sits at the single command-builder choke point, covering the VP, queue, and manual paths. |
||
|
|
e77e10896f |
feat(ams): name the expected slot when a paused print hits an AMS runout (#2587)
The firmware's runout HMS text says "insert into the same AMS slot", which is wrong under AMS Filament Backup: the firmware won't re-accept the depleted slot and advances to the next compatible one. Bambuddy parsed print.ams.tray_now only and dropped tray_tar/tray_pre, so the expected slot never reached the UI. Capture tray_tar/tray_pre on PrinterState and, while paused, resolve them to global tray IDs (expected_tray/previous_tray) on both the REST and WebSocket status payloads via a shared resolver: single-AMS passthrough, multi-AMS snow-mapping resolution, AMS-HT/external passthrough, and an honest null when the slot can't be placed. The AMS graphic highlights the expected slot (amber) and the ran-out slot (red); the HMS modal re-describes runout codes to name both, falling back to "check the printer" when unresolved. Runout copy translated in all 11 locales. Reporter @Jostxxl confirmed tray_pre=1/tray_tar=2 during the pause (ran out in Slot 2, printer expected Slot 3). |
||
|
|
47a2a77cd3 |
fix(filament): don't dispatch an unresolved AMS mapping to the external spool (#2589)
A P1S queue row with use_ams=true but ams_mapping=[-1] was silently printed with no AMS, starting against the empty external feed and pausing with a runout. Two faults combined: - start_print treated -1 (unresolved) the same as >=254 (explicit external) when deciding to force use_ams=False. Only genuine external now downgrades; -1 never does. - The scheduler trusted a stored [-1] as "already resolved" and passed it through. It now recomputes from live AMS trays whenever the stored mapping is entirely unresolved, and clears it if nothing matches rather than sending a doomed command. Frontend: the Print dialog no longer serializes an all-[-1] mapping while the printer status is still loading (the hook returns no mapping), and submit waits for AMS status with a "Waiting for AMS status" notice. Tests: new backend + frontend regression coverage; corrected one existing test that pinned the old [-1] -> use_ams=False behavior. |
||
|
|
e336eb53e7 |
fix(inventory): reconcile external-spool assignment when its filament type changes (#2575)
Assigning a new filament to the external spool (e.g. generic ABS over generic TPU) left the previous inventory spool assigned. The reconciliation that unlinks a stale external-spool assignment lives in on_ams_change, but that callback only fired on regular AMS-unit changes: its change-hash never included the external spool (vt_tray/vir_slot), and the external-spool data is stored after the AMS handler runs. Detect external-spool identity changes (type, colour, tag, or reset to empty) and re-fire on_ams_change so the stale assignment is unlinked. The fill percentage (remain) is excluded from the fingerprint so a running print doesn't trigger it on every push. |
||
|
|
ce807fb1cc |
fix(queue): upload to printers in parallel, cap wedge retries, make debug logs survive a farm
The reporter's 19-printer farm started prints "one by one", up to an hour apart. check_queue awaited each dispatch inline, and a dispatch includes the FTP upload, so every printer queued behind every other printer's transfer despite being an independent machine. His logs give the arithmetic: 40978500 bytes in 254.1s, 157 KB/s - a Bambu printer's SD write, not the network, is the bottleneck. Nineteen of those in series is ~80 minutes, and the next upload started 131 ms after the previous one finished. The delay is linear in fleet size, which is why it got worse the more printers he selected. Dispatch is now collected during the (still sequential) selection loop and run concurrently afterwards, capped by queue_max_concurrent_uploads - Settings -> Workflow -> Queue & Dispatch, default 4, 1 restores the old behaviour. Every gate is untouched; only the transfers overlap. The pass still awaits its uploads before returning: _start_print flips the row pending -> printing only after the upload, so an early return would let the next tick re-dispatch the same rows. FTP work moves to its own thread pool. It was on asyncio's default executor - min(32, cpu+4), six threads on a 2-core NAS, shared with everything else - which was survivable only while uploads were serial. Two problems the same bundle exposed: A printer that accepts project_file but never starts (#1678) was retried forever: 270s watchdog, revert to pending, re-upload the whole file, repeat. Hence his "printer who, since the morning, still not launch" - and on a farm each lap also eats an upload slot the other printers are waiting on. Attempts are now counted on the queue item; after three it fails with a message pointing at the printer instead of queueing a fourth re-upload. The debug bundle we asked him for held 4m49s of history. The push_status dumps fired on every frame rather than on change - several while their own comment claimed otherwise - which is 27,727 of the bundle's 29,830 lines and rolls 5 MB in under five minutes on 19 printers. They now log transitions only. The bundle also read just the live log while three rotated backups sat next to it, under a byte budget four times larger than the file it was reading. Migration verified on SQLite and Postgres: idempotent, backfills legacy NULLs (dispatch_attempts + 1 is NULL for a NULL row, which would silently disable the cap). Tests: 6 on concurrent dispatch (overlap, cap honoured, 1 == serial, default applies with no settings row, a failed printer does not cancel its siblings, no early return), 4 on the retry budget, 6 on the bundle's rotated-log span, 7 on the debug gating. Each verified to fail against the unfixed code - the first end-to-end log assertion I wrote passed without the fix and had to be tightened. |