mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 22:21:29 +02:00
3fe4cedba5180a6c77bc7a2aa0e82ed9220110bd
143
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e7aed30c0c | Show resolved filament colour and AMS mapping on queue cards (#3184) | ||
|
|
ad3b5f1982 |
Map the P-series model codes to the right printers
C11 is the P1P, C12 the P1S, C13 the X1E and N7 the P2S, as the virtual printer and a real P1P 3MF already say. The frontend map had them shifted, so discovery pre-filled a P1S as a P1P and an X1E as a P2S. The backend map read C11/C12 as X1C/X1 and lacked N7, the firmware check sent C13 to the P2S line, and the capability lists never matched BL-P001 because their lookup strips the dash. ----- Post work PR #3134 |
||
|
|
2b6dbe5ae5 | fix: display friendly legacy printer model names in stream overlay (#3134) | ||
|
|
1c316e2ad6 | Answer the outcome prompt by reacting to the Telegram message (#3129) | ||
|
|
71d4b2f70d | Material number as a first-class spool field (#2994) | ||
|
|
632930f02a | Improve spool ID search and AMS assignment (issue #2978) (#2998) | ||
|
|
053cfa73ad | Suppliers as a managed list with per-spool assignments (#2996) | ||
|
|
6c0292b09a | fix(ams): show a parked drying command as not started instead of an active cycle (#3096) | ||
|
|
44c7e6fb39 | Post-print outcome confirmation: good/reject verdicts, one-tap links, yield stats (#3047) | ||
|
|
89d94796ee |
fix(queue): keep the filament override when a model job moves to one printer (issue #3133)
Switching an "Any P2S" job to a specific P2S cleared its filament override: "Specific Printer" empties the target model and the reset effect counted that as a model change. Printer mode also matched trays against the 3MF's colours, never sent the override, and left the old one on the row. The reset now compares against the last model actually targeted, so the switch keeps the override while a real model or plate change still clears it. Printer-mode tray matching (single, per-plate, multi-printer and the selector's per-printer editor) runs against the requirements with the overrides applied, mirroring the scheduler's _apply_filament_overrides; an entry naming the slot's own filament is not a swap and keeps its tray_info_idx. Printer-mode submits carry the user's overrides, and the create endpoint stores them for a printer-targeted job, so a dispatch-time recompute of an unresolved mapping looks for the same filament. Saving re-attaches the tray_info_idx an unchanged entry already had, so a virtual printer's force-colour PLA-variant pin (#2650) survives an edit in either assignment mode. The printer card's compatibility filter skips printer-targeted jobs: it mirrors the model scheduler, and hiding a job on filament would hide it from the printer it is going to run on. |
||
|
|
e4a9ef4550 |
fix(spoolbuddy): show the colour name the rest of Bambuddy shows (issue #3090)
SpoolBuddy said "Unknown color" under a correctly-coloured swatch for spools the inventory page names without trouble. The name was never in the spool record. Bambu's RFID tags frequently carry no readable colour name -- some carry an internal code instead -- so Bambuddy has always resolved the swatch's own hex against the colour catalog, and the kiosk was rendering the empty column. Exactly one SpoolBuddy file already did it right, which is what marks this as an inconsistency rather than a kiosk simplification. Route every SpoolBuddy colour-name display through resolveSpoolColorName, which also stops the spools that do carry a code from showing "A06-D0" at the user. The write-tag edit form keeps the raw stored value on purpose: offering a derived name for editing invites the user to save it as though they had typed it. Spoolman has no colour-name field at all, so _map_spoolman_spool puts the spool's subtype there and sets color_name_is_synthesized. That flag now travels on the tag-matched broadcast, and resolveSpoolColorName takes a third argument to honour it -- a synthesised name loses to the catalog and survives only as a last resort. Spoolman installs were reading "Silk+" as a colour on the Inventory page and the AMS hover card too, so those call sites pass the flag as well. Searching by a colour you can read on screen now finds it, in the kiosk and in Bambuddy: the shared inventory filter matches the resolved name as well as the stored one. That makes the filter depend on the catalog, which loads asynchronously, so the three memoised call sites take its version as a dependency -- without that, a query typed before the catalog arrives keeps its empty result and reproduces the very symptom being fixed. The fallback label was hardcoded English in components that already import useTranslation; it is now spoolbuddy.spool.unknownColor in all 14 |
||
|
|
2c7c97c130 |
fix(jog): send the nozzle-bed gap the API promises on every model (issue #1334)
POST /printers/{id}/bed-jog takes a signed nozzle-bed gap, documented since it
was written: positive asks for more room between the nozzle and the plate. On
an A1 it did the opposite. The reporter sent distance=5 for clearance and
watched the toolhead come down.
The sign had been flipped on A1 models since the original report on this issue,
where an A1 Mini owner clicked an arrow labelled "move the plate up" and watched
the nozzle dive. That is a labelling problem -- a bed-slinger's plate does not
move in Z at all, so closing the gap shows up as the toolhead descending -- and
it was solved in the transport layer, which turned a parameter documented as
model-independent into one that meant the opposite thing on part of the fleet.
Z is the nozzle-to-bed distance on every Bambu model, by definition of the
coordinate system rather than by convention: G1 Z+ opens the gap whether the bed
drops away from a fixed nozzle (X1/P1/H2, whose end G-code parks with
G1 Z{max_layer_z + 100}) or the nozzle rises off a fixed bed (A1/A2L). The
finish-photo plate restore already relies on exactly that and carries no model
branch. So distance goes onto the wire unchanged and one call means one physical
outcome everywhere: positive is the safe direction on every printer.
Which way an arrow points is a different question, about the machine in front of
the user rather than about G-code, so the printer card answers it and asks for
the gap it wants. The buttons move what you would expect them to move, exactly
as before; on a bed-slinger they now say toolhead rather than plate.
The A2L never had the old fix. It slings its bed the same way the A1 does, but
the inversion listed the A1 names and the A2L was not among them, so its up
arrow has been sending the toolhead at the plate for as long as the machine has
been supported. The new classifier also covers the alternate internal codes
A04 / A11 / A12, which LINEAR_RAIL_MODELS and SINGLE_NOZZLE_FLOW_MODELS both
carry and the old gate did not.
is_bed_slinger is gone from the backend rather than widened: with the route
model-independent it had no caller, and a kinematics helper sitting unused in
the service layer invites the next person to assume the backend handles
direction. It does not, deliberately.
Separately, the soft-endstop comments on both jog routes claimed the firmware
clamps a bare move at the travel limit. It does not, and #2579 measured that:
an H2D at its Z limit ran straight past a clean G91/G1 Z-1.00/G90, while its own
touchscreen refuses the identical move. What #2579 removed was M211 S0, which
disabled the limits globally and took the touchscreen's protection with them.
The jog popover has warned about this correctly the whole time; only the code
comments disagreed with it.
|
||
|
|
48244bcfcf |
fix(queue): tell a pinned queue item why it is waiting (issue #3074)
A job queued as "Any X1C" explains itself when it cannot start: the model-based branch builds a reason for every candidate printer and puts it on the row, so the queue shows "Busy: X1C-01" or "Waiting for filament: X1C-02 (needs PETG)". The same job pinned to one printer showed nothing. It sat at Pending with waiting_reason NULL for as long as that printer was busy, which from the outside is indistinguishable from a queue that has stopped working -- the reporter watched fourteen minutes of it while his X1C ran a print he had started from its own screen. The fixed-printer branch had six ways out and none of them wrote the field. The sensor interlock (#1148) was its only writer, and it cleared the field up front on every pass where no sensor was holding the printer, so NULL was not an oversight on those paths but a guarantee. Every exit now writes, through one helper. The reasons reuse the model-based branch's vocabulary so _is_busy_only() keeps deciding what is worth a notification: a printer that is printing, drying, or working through the item ahead of this one reads as "Busy: <printer>" and stays silent, because it resolves itself. A printer that is off with no Auto On plug, and one whose plug could not switch it on, are worth saying. A finished plate nobody has acknowledged is split out from plain busy and named as itself. _is_printer_idle() returns the same plain False for that and for a running print, but they are not the same thing to the person looking at the queue: one clears itself and the other needs somebody to walk over to the printer. That notification fires on the transition into asking, where a busy-only reason counts as not asking. Testing whether the item was waiting at all -- which is what the model-based branch does -- would never fire it here: nobody's queue goes straight from idle to an unconfirmed plate, it waits behind the print first. The cost is that a printer dropping offline, returning busy and dropping again asks twice rather than once. The interlock stays silent. It has never sent this notification, and a change about what the queue displays is not the place to start. Clearing the field up front is gone with it. It existed so a shut door could not leave "Waiting on Enclosure Door" standing while the printer stayed busy with something else, and the new rule carries that guarantee instead -- whichever exit runs next overwrites it, and the dispatch path clears it. Two paths clear it that the report did not mention. A staged item and a future-scheduled one skip before this branch and never reach it again, so anything written on an earlier pass would outlive its condition for the life of the row. That includes the filament-deficit check, which stages the item itself. The notification is wrapped: a queue that cannot say why it is waiting is the bug being fixed, and a queue that stops dispatching because a provider timed out would be a worse one. On the frontend, the queue timeline drops any pending item carrying a reason, on the grounds that such an item will not auto-dispatch. That held while only the model-based branch wrote the field; "Busy: <printer>" is now the commonest reason there is, and it describes the very chain the timeline forecasts, so the rule would have emptied the view for anyone whose queue is pinned. It now asks whether the reason needs the user, via a small shared reader of the same shape the scheduler encodes. Which job goes out, and when, is unchanged: running the previous scheduler and this one over the same 768 states dispatches the same items in the same order with the same statuses, across 1452 rows that now carry a reason. |
||
|
|
4440738904 |
fix(drying): dry a composite spool as its base material (issue #3067)
The reporter's AMS-HT would not auto-dry PA6-CF, and drying the same spool by hand worked. The scheduler reduced a tray to a preset key by splitting on spaces only, so "PA6-CF" stayed "PA6-CF", matched none of the eight rows the preset table has, and the tray was read as holding nothing worth drying. The AMS was then passed over on every scheduler sweep, silently, because every caller reads "no row" as "nothing to do for this tray". It was never only nylon. Of the 41 types a printer can report, 33 had no row under that rule, and 20 of those have a base material sitting right there: every -CF, -GF and -AERO variant of PLA, PETG, ABS, ASA, PC and PA. Doing it by hand worked because the drying popover has resolved composites since exact key first, so a row the user added for the exact type still wins, then the suffix, then an alias map reading PA6, PA11, PA12, PAHT, PPA and Nylon as PA. PPA is the one alias that is a judgement rather than a spelling. Polyphthalamide is a distinct polymer, not a grade of nylon -- but it is an aromatic polyamide, it takes up moisture the same way, and PA's row is the hottest the table has. A material with no row and no alias is still skipped rather than dried at a number nothing here can source. That is where this parts company with the popover, which falls back to PLA because a dropdown has to show something. The table is user-editable JSON, so a preset row can be present and empty. That has always meant "skip this material" and still does: the temp and hours reads fall back per field to 55C/12h, which would dry a PLA spool at 55 degrees. Two more callers had the same line and move with it: per-filament humidity thresholds, where the override set for a material never applied to that material's composites, and the chamber preheat target, which had the suffix half of this from #2902 but not the aliases. The preheat test for that half reimplemented the lookup inline rather than calling it, so it would have passed whatever the function did. It calls it now. |
||
|
|
f5cdc86689 |
fix(queue): chain the Timeline in the scheduler's order, not queue position (issue #3043)
Turning Shortest Job First on reordered the pending list and the scheduler, and left the Timeline drawing the pre-SJF queue for good. It chained each swimlane's bars by queue position alone and was never told the setting existed -- so the one view whose whole job is to say when each print will run was the one view answering for an order the scheduler had no intention of using. Bars now chain in the order the scheduler will dispatch: jumped items first, then shortest print time with an unknown duration last, then position. The starvation guard is visible there too, so a long print that has finally come up reads as next rather than staying buried behind every short job on the lane. Three surfaces claim to show queue order -- the pending list, the "if started now" ETA, and the Timeline -- and each had its own copy of the comparator, which is how one of them came to be missing a whole clause. They now share one, written against the scheduler's ORDER BY. Grouping came along with it: the pending list folded a model name down to its first character to build a lane key, so Any X1C and Any X2D both landed on -88 (as did Any P1S and Any P1P) and the two lanes interleaved into a single run of rows. Backend untouched. The scheduler was dispatching correctly the whole time; only the drawing of it was wrong. |
||
|
|
e2493132bf |
fix(slicer): offer the desktop handoff only for formats the target slicer accepts (issue #3029)
The Slice action and Open in Slicer offered .stl, .step and .stp alongside .3mf, on the assumption that a slicer which opens an STL from its own File menu will open one from a link. Bambu Studio does not. Every bambustudio:// and bambustudioopen:// URL reaches one import path that refuses any filename which is not .3mf, and refuses it before fetching anything: "Download failed, unknown file format." The message names the format, so the failure reads as a broken model rather than an unsupported handoff. OrcaSlicer has no such limit. Only MakerWorld links take its equivalent path; a link to the user's own Bambuddy goes to its general downloader, which does not inspect the extension. So the format list becomes per slicer. isSliceableFilename and isSliceableFileType take the target as a required argument -- a default would quietly reintroduce the handoff that cannot work -- and an unrecognised value falls back to the Bambu Studio list, which is where openInSlicer sends anything that is not exactly 'orcaslicer'. The File Manager offers Slice only when the configured desktop slicer will take the file. The 3D preview reaches both slicers from one split button, so instead of hiding it promotes whichever can take the file to the primary action, naming that slicer when it is not the configured one; the split collapses to a plain button when no alternative is left. The sidecar path is untouched: with Use Slicer API on, Slice still takes STL and 3MF whatever the desktop target is. |
||
|
|
2e405afcd1 |
Decide whether a 3MF is sliced by looking inside it (issue #2993)
An archive that showed the green GCODE badge could re-import into the File Manager as a source-only project with no Print button, seemingly at random. Nothing was ever lost from the file. The download serves the stored bytes verbatim and the G-code was still in the zip; the two sides simply asked different questions. Archives looked inside the file. The library looked at the filename. So a sliced 3MF stored as Foo.3mf rather than Foo.gcode.3mf earned the badge and lost the Print button, and which one you got depended on how the print had reached the printer -- a slicer's LAN send names it .gcode.3mf, a per-plate export or a cloud-dispatched print does not. Both sides now ask one shared predicate about the zip itself, and every route into the library classifies on content. Only the central directory is read, and only when the name has not already settled it, so ingest costs nothing extra -- the external scan opens each 3MF for its thumbnail regardless. Rows already stored are re-checked once, internal ones only: an external row points at a mount that may be slow or absent, and startup is the worst place to discover that. The Slice action moves with it. Its refusal to slice an output was as name-bound as the Print gate, and without that a file that correctly gained a Print button would have offered to re-slice its own G-code. |
||
|
|
7363d5fd33 |
Show the spool that is in the AMS slot, not the one that was
Pull a Bambu ABS Orange out of A1, put a PLA Matte Dark Blue in, and the slot card still read "Bambu ABS" against the new colour until the page was reloaded. Three things stood between the swap and a correct card. The RFID auto-assign rewrites the slot's slot_preset_mappings row and then broadcast an event that refreshed everything except the query that reads it. Only the manual assign path invalidated that one. Those queries then sat behind the 3s cascade debounce, which exists for print completion, where one event fans out across half the app. A swap touches one slot and the user is standing at the printer looking at the card; worse, the timer restarts on every further event, so a busy moment could defer it indefinitely. Slot changes now invalidate immediately. And the card trusted the stored preset over live telemetry outright. That priority is why a hand-picked preset name stays on a slot, but it also let a cached row outrank what the printer was reporting. The row is now skipped when it names a different official Bambu filament than the tray does, so the card is right from the status push alone. User and local presets carry ids that genuinely cannot be compared and are left exactly as they were. Spoolman mode was the worse half of the same bug: its AMS sync writes the same row but announced nothing at all, so there was no event to refresh on. It now reports each slot it changed or cleared. |
||
|
|
e9daa2124e |
Match slicer presets on what they declare, not what they are named (issue #2982)
The internal slicer picked PETG for a PLA plate and an A1 process for a P1S. Both come from the sidecar's bundled-profile listing, fixed in the sidecar repo; this is the consuming half plus the hardening that keeps an older sidecar degrading rather than breaking. Standard-tier presets now carry the compatible_printers the sidecar reports. That list is the only truthful account of which printer a preset belongs to, because the bundle ships no process preset named after a P1S, an X1, an X1E or an H2D Pro -- all ten of the P1S's are named "@BBL X1C" and name the P1S only in that list. Reading the printer out of the preset NAME therefore made a P1S look like it had no compatible process at all: all 198 hid behind "Show all" and the auto-pick fell through to an alphabetically-first 0.06mm Fine @BBL A1 0.2 nozzle the CLI refused. A P1S now gets 0.20mm Standard @BBL X1C and 73 filaments instead of 4. An older sidecar reports nothing here, which leaves the name matcher in place -- degraded as before, not broken. Material is now a hard partition in the filament pre-pick rather than a +10 bonus. A preset stating a different material than the plate asks for is the wrong preset, not a worse one: wrong nozzle temperature, wrong bed temperature, wrong flow. A preset stating NO material stays eligible -- unknown is not wrong, and 32 shipped profiles genuinely have none. The same rule reaches the retain path, which held a slot on printer-compatibility alone and so cemented a wrong-material pick through every re-pick. A preset the user chose themselves is exempt: printing PETG on a plate a designer labelled PLA is a legitimate thing to do, and this rule exists to correct the auto-pick, not to overrule the user. Two more, both found while tracing this and neither reported: Among process presets equally valid for the selected printer, the one nearest a 0.2mm layer height now wins. Within a tier the list is alphabetical and Bambu's naming puts the finest height first, so every slice that did not name its own process silently got 0.08mm Extra Fine on an X1 Carbon and 0.06mm Fine on an A1 mini -- correct presets, nobody's default. Ties break toward the coarser, faster height; a name with no readable height is still pickable when it is the only candidate; a process the 3MF named still wins outright. H2DP is aliased to H2D Pro, the same shape as the A1M rename in #1649 -- the bundle spells the model one way in preset names and another in the printer preset, so an H2D Pro classified all 198 processes as another printer's. Deliberately narrow: H2DP and a plain H2D are different machines and must not collapse. A dropdown the printer filter would empty now shows the unfiltered list instead. That state was reachable for four printer models and told the user nothing; a visible preset for the wrong printer can be changed, an empty dropdown cannot. Verified against live Orca 2.4.2 and BambuStudio 02.08.02.61 sidecars over the real 1156- and 1792-profile trees: every one of the eight printer models tested now auto-picks a 0.20mm process for its own printer, a PLA plate draws a PLA preset and a PETG plate a PETG one. Each change was confirmed to fail its tests when reverted. |
||
|
|
b3c67c6943 |
Keep a lookbehind Safari 16 cannot parse out of the bundle (issue #2971)
An iPhone on iOS 16 loaded nothing at all -- no error, no partial render, just white, over LAN IP and over an HTTPS domain alike, while the same install was fine on Android, macOS, Windows and Linux. remark-gfm, added in v1.2.5 for the folder README panel, reaches mdast-util-gfm-autolink-literal, whose module body carries a lookbehind assertion. Safari did not support lookbehind until 16.4. A regex literal is validated when its module is compiled, not when the function holding it runs, so this was never going to fail as a broken README panel. FolderReadmePanel -> FileManagerPage -> App is a plain static import chain, the regex landed in the entry chunk, and the browser refused to compile all 10 MB of it. Nothing executed, so nothing rendered. v1.2.4 is the last release that loads on those iOS versions. The panel now renders GFM through a locally composed plugin holding four of remark-gfm's five sub-extensions -- tables, strikethrough, task lists, footnotes -- and omitting autolink literals, the only one carrying the lookbehind. Composing rather than configuring is forced by the bug: importing remark-gfm at all is what breaks the page, so no runtime option could have reached it. Parity was measured rather than assumed. Serialized ASTs against real remark-gfm over a 34-case corpus, position data included, are identical in 29; the five that differ are exactly the autolink cases, where the only change is link -> text with table and list structure intact. Across 26 hostile inputs -- NUL bytes, a BOM, an RTL override, a lone surrogate, combining marks, a 200 KB line, 500 stacked tables, 60-deep nesting, malformed and ragged tables -- neither implementation throws and none diverge, and applying the plugin twice is idempotent for both. The visible cost is that a bare https://example.com or foo@example.com typed into a folder README no longer links itself; [text](url) and <https://example.com> are core markdown and still do. The wiki claimed "links all render" and now says which. remark-gfm, mdast-util-gfm and micromark-extension-gfm leave the dependency tree and their eight surviving sub-extensions are declared directly, at ranges equal to or tighter than the ^2.0.0 those two packages declared, so the resolution surface did not widen. The bundle is 23 KB smaller. Vite's build.target governs syntax lowering and esbuild does not rewrite regular expressions -- measured, a lookbehind builds silently under safari15, safari16.0 and es2020 alike, which is how this shipped and then sat unnoticed for two months. So the guard is a real check rather than a compiler setting: npm run build now ends in check-browser-baseline.mjs, which scans the emitted bundles for syntax Safari 16.0 cannot parse and fails with the offending snippet. It is scoped to parse-time failures only -- a missing runtime API breaks one feature, while one of these takes down the whole app and has no graceful degradation to fall back on. Verified firing on the stale bundle before the rebuild, and running correctly inside the Docker frontend stage where only frontend/ is copied. Seven renderer tests pin both halves of the trade: each surviving GFM feature still renders, and both forms of autolinking stay off on purpose so a future dependency bump cannot quietly bring the lookbehind back. |
||
|
|
e5a18bf58b |
Key a K profile on its nozzle's flow type
A printer files each calibration under a nozzle id of the form HH00-0.4 (high flow) or HS00-0.4 (standard) and can hold both for one diameter -- a maintainer's H2D carries 102 high-flow entries against 6 standard -- because the same filament reads a different K through each. Nothing read that, so a standard-flow profile could be selected for a high-flow nozzle and vice versa. The flow is now stored with the profile, shown against each option in the picker, and checked before a stored profile is applied. Two spellings have to agree for that: a calibration entry says HH00-0.4 while the fitted nozzle reports HH01, so the comparison is two characters rather than four -- the trailing digits are a hardware variant the calibration table normalises to 00. Unknown flow on either side matches anything, which is what it has to do. Every profile stored before this has none. And an X1C declares none on any profile at all -- probed live, all eight come back with an empty nozzle id, against a four-digit cali_idx and a populated setting_id -- even though the machine really does take either nozzle. supports_nozzle_flow_type is therefore the wrong thing to gate on: it returns True for an X1C, and treating that silence as Standard would have dropped every X1C profile the moment a high-flow nozzle was fitted. What the printer's own table declares per profile is the test. NozzleInfo.nozzle_type carries two vocabularies by printer generation -- the nozzle material on legacy printers, the flow code on H2 -- and the comment claiming only the former is corrected. Anything that is not HH or HS reads as unknown, which is what makes the material spelling harmless. Storing both flows for one hotend and diameter is deliberately not done: spoolman_k_profile is UNIQUE on (spool, printer, extruder, diameter) with no flow column, and allowing a second row in internal mode alone would break inventory-mode parity. The picker marks a profile whose flow does not match what is fitted instead of letting it look configured while doing nothing. |
||
|
|
a7b563334e |
Configure a spool's filament preset and K profile per nozzle
A slicer preset is bound to a printer model: "Bambu PLA Basic @BBL X1C" is not the same preset as "@BBL H2C", and Bambu names a nozzle size in it as well. A spool carried exactly one, which was right until the same spool was used on a second machine -- the AMS slot on the other one was then configured with a preset that machine has no profile for. K profiles had the matching gap from the other side: the tables have always been keyed per hotend, but the picker could not express it. spool_filament_preset and its Spoolman twin store the exceptions, keyed (spool, printer_model, nozzle_diameter). Model rather than printer because the preset is a property of the model -- "@BBL X1C" is the same preset on every X1C, and asking per machine would mean picking the identical value twice. K profiles stay on printer_id, because a K value is measured on one physical hotend and two machines of the same model legitimately differ. Resolution is exact (model, diameter) -> (model, "") -> the spool's own preset, so a spool nobody has configured behaves exactly as it did before. The form writes one row per nozzle size and never the "" row; that level is kept for API clients wanting one value to cover a model. Both halves cover every standard nozzle size rather than the size currently fitted, because a spool is configured once and nozzles get swapped. The PA Profile tab becomes a Printers tab: a model list beside a detail pane holding a preset row per size and a K-profile grid of size by hotend. Each model is offered only the presets that name it, through the same matcher the Configure AMS Slot modal filters with, which moves out of that component into utils/slicerPrinterMatch. Presets whose name identifies no model -- most user-authored and OrcaSlicer ones -- stay offered everywhere, as does whatever is already selected, so a saved override cannot vanish from the control that shows it. Every preset carries an origin badge in the wording and colours that modal already uses. Every path that configures a slot now respects both: manual assign in either inventory mode, RFID auto-assign, the Spoolman tag link, the re-fire when a slot goes empty to loaded, the re-apply after a calibration-table refresh, and the re-selection when a Filament Track Switch moves an AMS to the other nozzle. Which nozzle a slot feeds, and how wide it is, was worked out independently in seven of those places, each reading nozzles[0] for every slot on the machine -- correct on a single-nozzle printer and on a dual-nozzle printer with matching nozzles, wrong the moment two sizes are fitted. That resolution is now services/slot_nozzle. Which array entry belongs to which hotend is no longer inferred. Measured on an H2D fitted with a 0.4 high flow on the left and a 0.6 on the right, nozzles[0] reads the right hotend, so the array is indexed by extruder id and the H2/X2 parser's convention is the one that holds. The legacy parser's opposite convention never governs a real dual-nozzle machine: every model in DUAL_NOZZLE_MODELS reports device.nozzle.info, and left_nozzle_diameter appears in no log or wire capture. Two comments that said otherwise were wrong and are fixed; amsHelpers' code was right all along and only its comment lied. Four defects surfaced while wiring it, all pre-existing except the last. The picker identified a chosen calibration by cali_idx alone, and the printer numbers its calibration table per nozzle -- on a dual-nozzle machine the same index exists on both hotends meaning different things, so saving could persist the other hotend's K value and diameter; SpoolBuddy's write-tag page carried a verbatim copy and gets the same fix. RFID auto-assign chose a K profile with no extruder test at all, so a spool calibrated on both hotends had a coin toss decide which pressure-advance value the slot got, on the path that runs unattended every time a Bambu spool is loaded. The Spoolman tag-link path resolved no preset whatsoever, configuring every linked slot with a generic material id and discarding a preset set in inventory -- the same defect #1713 fixed on the assign path, one function over. And an FTS inlet move re-selected K for nozzle 0 rather than for the nozzle the AMS had just been moved to. The last one is new here: a per-model override can be a cloud USER preset, whose PFUS-prefixed id the slicer rejects, and passing it straight into extrusion_cali_sel would silently lose the K-profile link. Reached the printer only where such an override exists, which is why nothing in the suite caught it. printer_safe_filament_id falls through to the spool's own preset and then the tray's RFID value instead. Reading a printer's calibration table asks for one nozzle size at a time. H2-series firmware answers only the first one or two of a concurrent burst of extrusion_cali_get and silently drops the rest, each dropped request costing a five-second timeout before its retry: measured at 11 and 23 seconds on an H2C and an H2D for four parallel requests, against roughly one second in series. An X1C answers all four at once, which is why this only ever surfaced on dual-diameter printers. Printers themselves are read in parallel -- separate machines are separate connections. The Configure AMS Slot dialog opens on the spool's own configured values, falling back to the slot's last manual configuration and then the tray's RFID data. The spool form is wider for the two-pane layout, colour, weight, cost and location move to their own tab in two columns, and a printer card in expanded view lists every fitted nozzle size rather than the first entry alone. |
||
|
|
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) | ||
|
|
06e5114a03 |
Do not report a printer as Safe when nothing is checking it (issue #2952)
The printer card's AI badge collapsed every class that was not Warning or Failure into green Safe, and the service reported `safe` whenever it had no verdict. The state entry is created when a monitored print is first seen -- before the first snapshot, let alone the first inference -- so a rejected ML API token, an unreachable ML API, a failed capture and an unset External URL all rendered as a healthy watched print: green Safe at score 0.000. For a safety feature that is the worst failure mode available: it asserts the print is being watched exactly when it is not. The reporter read that badge and concluded the loop had never started. It had been calling the ML API every ten seconds and being turned away with a 401 -- invisible because Obico's auth layer rejects a bad token before its request log sees it, and because successful checks log nothing there either. Add two honest states. Not checking (amber) when the last poll produced no result, carrying the reason; Starting while a monitored print waits for its first result. Score and frame count are withheld while not checking, since 0.000 beside "Not checking" reads as a measurement rather than its absence. The reason is per printer, so a card names its own problem rather than whichever printer failed most recently, and stays behind settings:read because it can quote configured URLs -- the badge state does not, because whether a print is watched is not configuration. An unrecognised class now falls back to Starting, not Safe. Test Connection saves the form before probing, so a green result describes the configuration the loop actually runs with rather than what is typed in the boxes. |
||
|
|
cbbdab86f9 |
Say which blue a colour mismatch is about (issue #2941)
A print was refused a colour match between two filaments the dialog itself labelled "Blue". The comparison was right: the slicer profile asked for a near-pure #0028FF and the slot held Bambu's navy #0A2989, 118 apart in the blue channel alone and a CIEDE2000 distance of 15, where 1 is a just-noticeable difference. Nothing on screen said so. A hex that misses the colour catalogue is named by a coarse family bucket, so both sides resolved to the same word, and the warning sat between two identical labels with nothing to reconcile it against. Read as a broken matcher, which is what the report said, and a fair reading of what was shown. Where both sides of a mismatch carry the same name they are now qualified by their hex, and the tooltip names them together: "Same type, different color: needs Blue (#0028FF), slot has Blue (#0A2989)". Names that already differ are left alone -- the hex is noise once the words separate them -- and a side with no name falls back to its hex rather than growing an empty bracket. The comparison is untouched. It was correct, and its tolerance is not something to widen on one report: a difference that size would start matching navy to cyan, and eligibility is the same rule the queue scheduler dispatches on, so loosening it would change which spool gets printed rather than only which warning gets shown. This issue was about being able to see why the warning fired, and a test pins that it still fires. The strings around it were hardcoded English: the panel's status line, the required-filament tooltip, the auto-matched marker, the slot placeholder, the mismatch detail and the type-not-found message. All nine now go through translation, in all thirteen locales. Two of them -- "Same type, different color" and "Filament type not loaded" -- turned out to have had translations sitting in every locale file the whole time, unused, while the component rendered the English literal beside them. The parity gate could not have caught any of this: keys that were never added have nothing to compare against. |
||
|
|
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. |
||
|
|
e4aab7b44b |
Leave archived projects out of the pickers that file work (issue #2888)
The reporter opens a project per job and archives it when the job is done, so the Project dropdown in Edit Archive listed five live projects behind thirty-odd finished ones, in one unscrolled run with nothing to tell them apart. Archived is the state that means "put this away", so that is what the pickers now drop: the Edit Archive dropdown, the pending-uploads panel, the bulk Add to Project dialog and the File Manager's folder link. Completed stays. It says the work is finished, not that it should be hidden, and filing a reprint under a finished project is ordinary. The Archives right-click submenu had the opposite bug and offered active projects only, so a completed project was reachable from the edit dialog and not from the menu next to it. All five surfaces share one rule now. Whatever a thing is already filed under survives the filter whatever its status. A select holding a value that matches none of its options is reset by the browser to the first one, and here that reads "No project" -- an archive sitting in an archived project would have said in as many words that it was filed nowhere. The stored id does survive an untouched save; it is the field that lies. The parent-project picker is deliberately untouched. A finished or archived project is still a legal parent, and its own comment says so. Fixed alongside it, and reported separately: the status tabs counted only the projects the selected filter had already let through, so every tab but the current one counted zero and dropped its badge. Switching tabs moved the number rather than showing four of them. They are counted from the unfiltered list the page already fetches for the sub-project captions -- same query key, no extra request. The new rule is one function with its own unit tests, since five callers now depend on it reading the same way. Each half is pinned separately: dropping the filter, dropping the kept id, restoring active-only, and counting from the filtered list each fail their own tests and nothing else. |
||
|
|
3901043238 |
Name an AMS slot's colour by its material, not its hex alone (issue #2875)
A hex is not one colour in Bambu's range. #FFFFFF is Jade White in PLA Basic, Ivory White in PLA Matte and plain White in six other materials; popover resolved its title from the hex alone, against a map that keeps one name per hex, so an ivory Matte spool read "Jade White" while the profile line beside it correctly read Matte Ivory. /inventory/colors/map now carries the names collapsing loses, keyed "<material>|<hex>". An entry is emitted only when it recovers a name the same manufacturer's own range lost -- 11 of them against the 608 colours in the shipped catalog. Both halves matter: a name equal to the flat answer is weight, and a name from another brand is not a recovery, it would put Prusament's "Pristine White" on every generic white PLA slot. A slot with a spool assigned from Inventory is titled with that spool's own colour name: it is the roll the user said is in there. Bambu internal codes are still rejected as non-names (#857). |
||
|
|
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. |
||
|
|
61a6ed1f20 |
Move Camera View Mode from Settings onto the camera button
Whether a camera opened in its own browser window or as a floating overlay was one dropdown in Settings > General > Camera, applied to every camera on the install. Deciding per printer meant leaving the Printers page, changing the setting, coming back, opening the camera, and going back again to undo it -- five steps for a choice you make while looking at the printer you want to watch. The camera button on the printer card is now a split control. The icon opens the camera whichever way you opened the last one; the caret beside it offers both modes, and picking one opens the camera that way as well as making it the mode the icon uses from then on. A menu that only changed a preference would have left the user a second click to do the thing they had already asked for. The mode in effect is ticked. The choice lives in the user's own browser, so two people watching the same farm can each have the view they want. camera_view_mode survives as the default a browser that has never chosen starts from, and is written back when the user holds settings:update. The local value wins on read: someone below that permission cannot write theirs back, and a preference that silently reverted on the next render would be worse than none. Two things were consolidated on the way through. The popup-opening code -- saved geometry, and deliberately no noopener so the browser copies sessionStorage and its auth token into the new window -- was duplicated between the printer card and the Cam Wall tile handler; it is now utils/camera, with the geometry parse wrapped so a corrupt cameraWindowState falls back to defaults instead of throwing, which neither copy did. The Cam Wall follows the same remembered mode, since a tile has no room for a split button of its own. The effect that force-closed every open overlay when the setting flipped to window is gone. It made sense for a global switch; with the choice made per click, closing viewers someone deliberately opened does not. No new locale strings: the four the settings control used are reused as the menu's labels and tooltips and the caret's own label, so all 13 locales stay in parity untouched. |
||
|
|
47a37618a0 |
Let a busy or offline printer take a dropped file (#2849)
Dragging a sliced file onto a printer card refused the drop unless the
printer was connected and neither RUNNING nor PAUSE. The overlay went red
with "Printer busy", handleCardDrop returned early, and the file was
discarded with no toast and nothing uploaded. The card's Print button was
hidden by the same condition, so both routes into "Print from Printer
Card" closed at once and the way through was the File Manager, uploading
and queueing by hand.
The gate never described a real constraint. Every print Bambuddy sends
becomes a queue item; dropping onto an idle printer only looks instant
because the scheduler dispatches it on the next pass. Busy is a timing
difference, not a different path. The modal has always passed
disableBusy={false} to PrinterSelector, and asapToastShouldPromiseLaterStart
exists precisely to say "this will start later" when the target cannot
take it now. cleanup_library_after_dispatch is a print_queue column
consumed at dispatch, not on close, so the transient upload survives
however long the item waits.
Offline is included for the same reason: the queue dispatches when the
printer comes back, so a machine that is powered down can be given work.
The overlay now says which one is happening -- "Drop to print" when the
job would start immediately, "Drop to queue" when it would wait, covering
a print in progress, a paused job, an AMS mid-cycle, a plate not yet
cleared, and a printer that is offline. The predicate behind that wording
is the one the modal already used for its own later-start notice, lifted
out of PrintModal into utils/printer as isPrinterCurrentlyDispatchable so
the card cannot promise something the modal contradicts a second later.
The drop is also gated on the permissions the flow actually exercises. It
uploads to the library and creates a queue item, so library:upload and
queue:create -- the pair the Print button beside it has always checked.
printers:control, which it checked before and never uses, meant someone
holding that alone got the file uploaded and then rejected by the queue,
leaving a library row behind with nothing pointing at it. The refusal now
names whichever of the two is missing instead of claiming the printer is
busy.
printers.cannotPrint is dropped in favour of printers.dropToQueue across
all 13 locales; its text was both unused and, after this, wrong.
The Print button stays inside the expanded-card block, so S-size cards
still show the drop zone and no button, exactly as before.
|
||
|
|
d37ce94f81 |
Feature: Scheduled drying (#2703)
* feat: add ScheduledDrying model for delayed drying runs (#2638) * Release the printer when a scheduled dry ends (#2638) _check_scheduled_dryings marks a printer as drying in _drying_in_progress, which is shared with auto-drying. Auto-drying prunes that map in _sync_drying_state(), but that call sits behind its enabled check, and this is the first writer that runs whether auto-drying is on or not. With it off -- the default -- nothing dropped the entry short of a print being dispatched to the same printer, so the next scheduled run parked on "already_drying" forever and queue_drying_block held that printer's prints too. A nightly off-peak dry with no printing in between is exactly the workflow this feature is for: night one worked, every night after it silently did not. The check now releases what it acquired, covering both a run that ends mid-pass and one cancelled through the route between passes. The retention prune ran on every pass. Issuing the DELETE is what opens a write transaction, this method is called every 3s while the queue dispatches, and rows only become prunable a week after they finish, so it is now gated to hourly on a monotonic stamp -- with the first pass after a restart still reaping whatever the dead process left behind. Both drying paths now pick the blocking dry_sf_reason through one rule. The immediate endpoint quoted whichever code the firmware listed first while the scheduler prioritised power over retract, so one blocked AMS read two ways depending on which button you pressed. drying_preflight.primary_reason_code holds the order and both call it, including the flame button's tooltip, which had no wording for filament at the outlet at all and sent those users to the generic "can't start drying right now". scheduled_drying joins the model list in init_db. The table was already created -- importing the package registers it -- but it was the only model relying on that indirection. Tests: the release (completion and route-cancel), the prune throttle, the shared reason rule, the tooltip priority, and four driving the real check_queue, which nothing covered before -- a pass with no rows still dispatching prints, a due row dispatching, and a failed row not stalling the queue behind it. Each one fails against the code it guards. --------- Co-authored-by: MartinNYHC <martin@bambuddy.cool> Co-authored-by: maziggy <mz@v8w.de> |
||
|
|
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
|
||
|
|
e6842e1d3c |
Rank near-colour matches by how they look, and share one filament type table (#2804)
Three follow-ups to #2804, all bearing on one decision: which spool a print uses when the exact colour is not loaded. Colour ranking is now perceptual. The ranking added in #2804 measured RGB distance, which rates a colour by how far apart the numbers are rather than how far apart they look, and it overweights blue badly enough to invert the answer: against a required #1E4821 green, a purple #38202F is the nearer of two eligible spools by RGB and four times the further once measured properly. Both sides now use CIEDE2000 -- perceptual_color_distance in backend/app/utils/color_utils.py and colorDistance in amsHelpers.ts, kept structurally identical so they can be read side by side. Verified against the Sharma/Wu/Dalal published reference set, all 31 pairs to 1e-4, and the two implementations agree to within 1e-9 across 800 sampled pairs. Eligibility is untouched, still the per-channel RGB box, so this only reorders spools that already qualified. Type matching now agrees between the interface and the scheduler. Bambu firmware treats PA-CF, PA12-CF and PAHT-CF as one material and the scheduler has always matched them accordingly, but the interface compared raw type strings and called that same pairing a mismatch. The badge contradicted what the printer was about to do, and the manual override picker, which groups by canonical type, offered the very spool the badge then rejected. The fifteen comparison sites in useFilamentMapping.ts, useMultiPrinterFilamentMapping.ts and PrinterSelector.tsx now call filamentTypesCompatible. The pipeline pre-flight reads the matcher's table instead of its own copy. That copy had drifted into disagreeing in both directions: it aliased PLA Basic to PLA where the matcher never has, so a run could clear the check and then fail to map its slots, and it lacked the nylon grouping, so it flagged runs the matcher handles without complaint. A check whose job is to predict dispatch is wrong whenever it disagrees with dispatch, whichever way it leans, so it and the scheduler now both read backend/app/utils/filament_types.py. That canonicaliser deliberately does not strip surrounding whitespace. It looks like a free improvement, but it would collapse a junk tray_type to "" just as a 3MF declaring no filament type yields "", and a typeless requirement would start matching a junk-typed tray instead of reporting the slot unmapped. Padded type strings are worth handling on their own terms, with that case addressed. One behaviour change outside the ranking: the pre-flight is stricter for a printer reporting a product name such as "PLA Basic" where the generic material belongs, which it now flags rather than passes. Rare in practice, since the printer reports material and product name in separate fields, and it is the answer the matcher would give. Nothing about which spool a print actually uses changed outside the colour ranking itself. Adds 203 backend and 6 frontend tests. The #2804 tie-break test now uses identical colours: two colours at equal RGB distance are not perceptually tied, which is rather the point. |
||
|
|
4f7a02b393 | Pick the nearest eligible filament colour instead of the first one in tray order (#2804) (#2823) | ||
|
|
57190b10ba |
Stop offering server-side slicing for STEP files
The Slice action appeared on .step / .stp and the endpoint accepted the job, but neither slicer can load one from its command line -- both answer "Unknown file format. Input file must have .stl, .obj, .amf(.xml) extension." So the file was read, converted and uploaded before failing as "The input model file to the slicer can not be parsed", which reads as a corrupt model rather than an unsupported format. The endpoint refuses a STEP up front with a message saying to export it as STL or 3MF, and the Slice and pipeline buttons no longer appear on one. Open in Slicer is unchanged and still hands STEP to the desktop application, which opens it fine -- that was always the working path. isSliceableFilename (desktop) and isApiSliceableFilename (sidecar) are now separate predicates so the two cannot drift back together. |
||
|
|
08df660f6c |
Replace the embedded G-code viewer with the slicer's own renderer
Sliced files previewed through a vendored copy of PrettyGCode in an iframe. It drew each move as a screen-space line -- a line has no thickness in the scene, so it cannot occlude the layer behind it, which is why prints came out stringy and shimmered where layers crossed. Being a separate app in a frame, it could be neither themed nor translated, and carried its own machinery for detecting a proxy refusing the embed. Now built on libvgcode, the renderer OrcaSlicer draws its own preview with, vendored from three-slicer (AGPL, same as us). It takes the THREE namespace as an argument and imports nothing, so it runs on our 0.181 rather than the 0.160 its package pins. The parser is ours; upstream renders its own kernel's output and ships no G-code parser at all. Two things it has to get right, both found by checking a real plate rather than assuming: - BambuStudio does not use the OrcaSlicer/PrusaSlicer annotations. It writes "; FEATURE:", "; LINE_WIDTH:", "; CHANGE_LAYER" and "; Z_HEIGHT:", not ";TYPE:", ";WIDTH:" and ";LAYER_CHANGE". Reading only the latter showed a 52-layer print as 23,165 layers in one colour, because with no layer marker recognised every travel Z-hop split a layer and every segment took the fallback feature. - It emits a tenth of its moves as G2/G3 arcs -- 706 extruding ones in a single plate. Ignoring them punched holes through curved walls and tree supports. Arcs with no X/Y are the helical travel lift and lay down nothing, so they interpolate as travels. Four colour modes: filament (default, from the AMS slots the file was sliced with), feature, layer height, line width. Speed, fan and temperature are deliberately absent -- upstream derives those from settings rather than the toolpath, and guesses dressed as measurements are worse than an honest omission. The parser now carries the data to do them properly later. Legend entries are switches. Hiding removes the records before the mesh is built rather than recolouring them: the shader packs colour into a single float with no alpha, so there is no transparent to set, and removal is the useful behaviour anyway -- a hidden support stops occluding what it covered. The scene is built once and only the toolpath rebuilds. Doing otherwise constructed a new WebGLRenderer on every render, because the buildVolume default is an object literal and so a fresh identity each time; browsers cap live WebGL contexts and drop the oldest, which blanked the canvas after a few interactions. utils/framing.ts goes with the iframe, along with six now-orphaned strings in all 13 locales. src/lib/vendor is excluded from eslint -- acting on findings in vendored code makes it impossible to re-copy on the next upstream release. |
||
|
|
c384911f7c | Sync | ||
|
|
595dc5844a |
Say which header blocked the 3D preview, instead of leaving the browser's page (#2787)
A reporter uploaded an STL, sliced it in Bambuddy, and got a frowny icon and
"<hostname> refused to connect" when previewing the sliced file -- while the
STL's own preview worked. That is Chrome's ERR_BLOCKED_BY_RESPONSE page, drawn
inside our layout shell, and the split between the two previews is where the
cause is: an STL or source 3MF renders in the page, a sliced file opens the
embedded G-code viewer, which is the only thing in Bambuddy that frames a
Bambuddy page (FileManagerPage.tsx:2472, GCodeViewerPage.tsx:47).
Our headers permit that frame -- frame-ancestors 'self' plus SAMEORIGIN on
everything under /gcode-viewer (main.py:7709) -- and the frame is same-origin,
so a refusal means a stricter header was added after we replied: a reverse
proxy, a security add-on, an auth gateway. None of which the user could see.
The browser drew its own page and nothing said what was refused, by whom, or
that the viewer opens perfectly well in a tab.
The frame cannot report this itself. A frame blocked by X-Frame-Options or
frame-ancestors still fires onLoad -- the browser commits an error document --
so there is no failure to catch. The page now asks for the same URL directly:
same-origin, so every response header is readable, and it goes through whatever
proxy the browser reaches Bambuddy by.
findFramingRefusal reads the verdict the way a browser does. frame-ancestors
wins outright when present, because CSP requires X-Frame-Options to be ignored
in that case -- reading both would blame a proxy-added DENY the browser never
consulted. Multiple CSP headers are intersected and fetch joins them into one
comma-separated string, so every frame-ancestors occurrence has to permit us,
not just the first; that is the shape a proxy appending its own policy to ours
actually takes. Failing that, a legacy header that is anything other than a
single SAMEORIGIN refuses us, including the conflicting "SAMEORIGIN, DENY" that
appears when a second copy is appended.
On refusal the frame is replaced with the header named verbatim, so an operator
can go and find the rule in their proxy config, and a link that opens the viewer
in its own tab -- a top-level page, which no framing header applies to. A
non-200 is reported the same way rather than as raw {"detail":"Not Found"}
inside the frame, which the startup-time warning at main.py:8120 already calls
out as easy to miss. A probe that cannot reach a verdict changes nothing: the
iframe stays, because guessing at a cause we cannot see is worse than the
browser's own page.
The working case is unaffected -- the iframe renders immediately as before and
the probe only ever replaces it.
|
||
|
|
afa0ba0dc0 |
Nest projects under a master project and roll their figures up (#1264)
Projects were flat. The parent_id column and the sub-project list existed but nothing could set a parent outside the API, and a master project's stats only ever covered its own prints. The project dialog gets a parent picker, and a project with sub-projects gets a second card covering the whole tree -- jobs, parts, time, filament, cost, and progress against every target in the tree added together. That card is separate from the project's own stats, which keep their existing meaning; widening them would have restated the figures of anyone who had already nested projects over the API. Each listed sub-project carries its own branch's roll-up, so the rows add up to the card above them. On the Projects page a sub-project is drawn inside its parent's group rather than as another card in the grid -- two cards columns apart cannot show that they belong together, whatever the caption says. A sub-project whose parent the status filter has hidden stays put and names its parent instead. compute_project_stats now goes through the same grouped aggregation as the roll-up rather than its own copy of the SQL, since the two must agree. Three things the interface made reachable: - PATCH refused only a project as its own direct parent, so A -> B -> A was two calls away. A cycle has no root to roll up to, and the walk keeps its seen-set for databases that already contain one. - A sub-project's percentage was completed quantities against the plate target, disagreeing with the page it linked to. - Deleting a mid-tree project orphaned its children at top level; they now move up to its own parent. |
||
|
|
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. |
||
|
|
fb4c130bb2 |
fix(slicer): drop the legacy library:read from the slice gate (#2725)
The desktop handoff accepted library:read alongside read_all/read_own, on the reasoning that default groups do not carry it and requiring it would lock out Operators and Viewers. The permission grants nothing in that position: the slicer-token endpoint gates on require_ownership_permission(LIBRARY_READ_ALL, LIBRARY_READ_OWN), and neither that dependency nor User.has_permission expands the legacy name, so a group holding only library:read gets a 403 there. It cannot reach the File Manager to try, either - GET /library/folders gates on the same pair - and the library:read -> library:read_own migration in core/database.py runs only over the groups named in DEFAULT_GROUPS, so a custom role that still carries it stays stuck rather than being upgraded. custom role that still carries it stays stuck rather than being upgraded. Accepting it only enabled a menu item the server refuses, and the failure is indistinguishable from "no slicer installed" once the catch hands the unauthenticated URL over. Removed, with the comment recording the reason so the next reader does not re-add it, and a test that pins it. --- refactor(slicer): share one sliceable-file-type rule (#2725) The File Manager and the 3D preview decide the same thing about the same file and each held its own list of extensions - which is how they came to disagree, offering a desktop handoff for an STL whose own preview showed "Open in Slicer" greyed out. Making the two lists identical fixed the symptom and left the drift, so SLICEABLE_FILE_TYPES now lives in utils/slicer.ts with isSliceableFileType for a stored file_type and isSliceableFilename for a name. The filename form still rules out the compound extensions explicitly, since .gcode.3mf ends with .3mf; the type form does not need to, because classify_file_type stores that one whole. Both test files mocked the whole slicer module, which would have replaced the new predicates with undefined - switched to importOriginal so only openInSlicer is stubbed. That is the better shape regardless: the tests now exercise the rule the component runs instead of a copy declared beside them. Carries the rebuilt bundle. The CSS hash moves with it - the split button introduces Tailwind classes the previous build had no reason to emit. |
||
|
|
af7908ca1e | Improve local slicer selection with code review suggestions, adding fail toast and handle (legacy) read permissions | ||
|
|
3db8ac9da7 |
Let the external spool be hidden from the printer card (#1782)
An external spool holder that never gets used still takes a full card's width in the Filaments row, next to the AMS units that are actually in use. An eye icon at the right-hand end of that row's header now hides it, and clicking it again brings it back -- the affordance stays in place rather than moving to a settings page, so the choice is discoverable and reversible where it applies. Per printer rather than global. A global flag would suit a toolbar button, but an icon on the card that silently rearranged every other card would surprise; it is keyed by printer id in one localStorage entry, the same shape as printerCollapsedSections, and sits alongside the other browser-local printer-page view preferences. The toggle is offered only when the printer has at least one AMS. On a machine with no AMS the external spool is the entire filament section, so hiding it would leave an empty row with no control to undo it. The icon and the hide condition read the same canHideExternalSpool, so a preference stored before an AMS was unplugged cannot blank the row either -- the spool reappears instead. The store lives in a new utils/printerCardPrefs.ts rather than in the 9,157-line page. It re-reads before writing so two cards toggled in one session cannot clobber each other's entry, deletes the key instead of storing false, and treats a malformed or unavailable localStorage as "nothing hidden" so a private-mode browser cannot throw out of a render. |
||
|
|
dbf674561c |
Sort the inventory by colour, not colour name (#2729)
The Color column was missing from the page's sort-extractor map, so its header ignored clicks. Sorts by family first — rainbow, then browns, then neutrals light to dark — with the hue sort running inside each. The issue asked for a straight hue/saturation/lightness sort. Measured against a real 30-spool inventory that puts Titan Gray (hue 210, sat 0.04) among the blues and a warm grey next to the reds, and splits the oranges around brown. Neutrals order by lightness because their hue is noise. Families come from the classifier that already names colours missing from the catalog, so the Color and Color Name columns cannot disagree. |
||
|
|
ea63355fde |
Fix cross-model queue items being misrepresented and editable into a broken state (#671)
The edit dialog offered a printer picker and target-model dropdown for an item with alternatives. Saving left a row with variants AND a printer_id, and the scheduler's fixed-printer branch wins that race, so it dispatched a row whose library_file_id is still null and failed in the upload. PATCH now refuses printer/model changes on such an item — comparing against the current value, since the dialog re-sends target_model unchanged — and the route eager-loads variants, without which the guard could not see them and every PATCH response dropped the alternatives from its payload. Names come from a shared helper now. A cross-model item holds neither archive_id nor library_file_id until dispatch, so five separate inlined fallbacks all rendered "File #null"; they now read "x1c.gcode.3mf +1 more". The queue also grouped these under "Any H2D" — the first candidate mirrored onto the row — filing a job under a printer it might never run on. It groups as "Any H2D / X1C", matching the row beneath it. |
||
|
|
b04664c64a |
Raise the chamber-temperature ceiling from 60 to 65 C
Every field that takes a chamber target stopped at 60: the per-filament chamber map and per-print override in Preheat & Heat Soak, the chamber quick-select presets, and the printer-card chamber control. 60 is the X1E's ceiling and the X1E was the only heated-chamber model when that limit was written; the H2 series and X2D heat to 65, so the top of their range was unreachable. The ceiling now lives in one constant per side (MAX_CHAMBER_TEMP_C in backend/app/utils/printer_models.py and frontend/src/utils/printer.ts) rather than as a literal at each call site. X1E firmware clamps a higher request to its own maximum, so a shared ceiling is safe. Also fixes a live bug at PrintersPage.tsx:7985: parsePresetTriple was bounded to 60 there, and it rejects the whole triple on any out-of-range entry, so a saved 65 preset would have silently reverted the printer card to the defaults while Settings still showed 65. |
||
|
|
30daed2756 |
Fix per-job queue ETA showing for jobs that cannot start now
The scheduler only writes waiting_reason on the model-based assignment path, so a job pinned to a specific printer sits behind a running print with no marker at all. Every such job rendered an identical "starts now" ETA that was wrong by the length of everything ahead of it. Decide eligibility on the page instead: an item gets an ETA only when its printer is idle and it is the item the scheduler would dispatch next, following the same ordering the scheduler uses. Staged and future- scheduled items do not block the item behind them, matching the scheduler, and items conditional on a previous print are excluded. The value also froze at first render, since react-query's structural sharing keeps the queue reference stable and nothing re-rendered the row. formatETA now accepts a base instant and the page drives it from a 30s clock shared by every visible row. Retire the borrowed printers.estimatedCompletion tooltip for a queue key that says what the number means, translated into all 13 locales. |