mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
73e0787bfd650d4fafdd03b2faebe1a4e32b4fd0
3845
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
73e0787bfd |
Name an unnamed print stage "Preparing" on the card
New printers report stage numbers before Bambuddy learns their names, and the H2C still has several. Those reached the printer card verbatim, as "Unknown stage (72)" -- a number that means nothing to the person reading it, on the one line that otherwise says what the printer is doing. Every stage that has turned out to be unnamed so far has been part of the run-up to printing, so an unnamed one now reads as "Preparing". That is the same literal stage 74 already carries rather than a second spelling of the same idea, so a card cannot show two different words for the same situation depending on which number the firmware picked. Display only, and deliberately not pushed down into get_stage_name. That function also feeds the stage-transition log line and the once-per-session warning added to capture unnamed stages so they can be named in a later release; there the number is the entire diagnostic value, and replacing it with "Preparing" would hide the only thing that reports these. Both paths are pinned by tests asserting they disagree for an unnamed stage and agree for a named one, so a later tidy-up cannot quietly collapse them. The idle sentinels are untouched: 255 on A1/P1 and -1 on X1 mean "no stage", not an unnamed one, and still resolve to nothing rather than being swept up by the fallback. Nothing keys logic off stg_cur_name -- it is display-only in the printer card, the print dialog's printer selector and the stream overlay -- so all three improve and none change behaviour. No i18n either way: the whole stage table has always been English. |
||
|
|
699fc419fe |
Paint an AMS slot card with the spool's colours, not the tray's (issue #2967)
A Ziro "Colorful Mist" -- yellow, cyan and pink, effect Tri Color -- hovered on the printer card as a single flat pink rectangle. A printer reports exactly one tray_color hex per tray and nothing else, so telemetry cannot describe a gradient or a surface effect and never will. The header now paints the bound spool's own swatch whenever that spool declares extra colour stops or an effect, through buildFilamentBackground -- the builder the Inventory swatches already use, so the two surfaces cannot drift apart. A plain single-colour spool keeps the flat backgroundColor it has always had, and a slot with nothing bound is untouched, so the common case goes nowhere near the gradient path. The gate is "any stop at all", not "more than one". buildColorLayer ignores rgba the moment stops exist, so a one-stop spool renders that stop rather than the slot hex; skipping it would leave this card showing a different colour from the Inventory row for the same spool, which is the class of disagreement the shared builder exists to prevent. isLightColor now tests the colour actually on screen. Once the spool's swatch is painted the base is no longer the slot hex -- a single stop replaces it outright, and an effect-only spool paints the spool's own rgba -- so testing the slot hex would pick the text colour for a background that is not there. Above one band no single hex can decide legibility, and the name sits dead centre where a multi-stop background is likeliest to change under it. So a genuinely multi-band header puts the name on the same scrim the vendor badge already uses. One stop, or an effect over one colour, still vendor badge already uses. One stop, or an effect over one colour, still leaves a real base colour to test and keeps the contrast rule it had. Spoolman mode gains the gradient in the process. Spoolman has held the stops in filament.multi_color_hexes all along and the label renderer has been reading them for releases, but _map_spoolman_spool never returned them -- so the identical roll registered in Spoolman rendered flat while the internally-managed one did not. Both now share one parser rather than reading the same field two ways. What stays asymmetric is Spoolman's own limitation, and it is pinned by a test rather than left to be rediscovered: Spoolman has no field for a surface effect at all. Its only neighbouring field, multi_color_direction, says how the stops are laid out, not that the roll is silk or glitter. effect_type is therefore None for a Spoolman spool instead of guessed at, and silk/sparkle/wood remain internal-only. The other two halves of the report -- the header naming the colour "White" instead of "Colorful Mist", and the print dialog offering "A3: PLA (White)" -- were already fixed on dev by #2875 and by the slot-naming change that landed the day after this was filed. Neither is in 1.2.5.3, which is what the reporter is running. |
||
|
|
4f10d15584 |
Record the colour a slice is actually printed in (issue #2977)
Every file the internal slicer produced came back with filament_colour = #00AE42 whatever filament was picked: a green plate thumbnail, green metadata, and a "Color mismatch" in the Print dialog against the AMS slot the job had just been correctly mapped to. A colour is not a property of a filament preset in either slicer. It belongs to the project, and Bambu Studio and OrcaSlicer set it from the plate in their GUIs -- so nothing was attached to the preset Bambuddy sends by name, and the CLI fell back to its own compiled-in default, which is Bambu green. None of the shipped BBL filament profiles define filament_colour; the whole tree has zero occurrences. default_filament_colour is not the answer on its own. Measured against a 02.08.02.61 sidecar, a profile carrying only that still slices to filament_colour ["#00AE42"] -- Bambu Studio consumes it in the GUI when a project is created, not in --load-filaments. So it is read and rewritten as filament_colour, which the same sidecar does honour: the reporter's exact triplet returns ["#E8B00C"] when patched this way, and the colour lands in both project_settings.config and slice_info.config, which is what the thumbnail and the AMS mapping actually read. Each filament row in the slice dialog gets a colour control, and the resolved value flows through a chain: the user's pick, then the preset's own default_filament_colour, then the colour that slot was designed with in the source 3MF. A slot with none of the three is left untouched rather than given a guess. The designed colour is read from project_settings.config, not slice_info.config. The latter records what the file was last sliced with, which for a source that never carried a colour is #00AE42 itself -- measured -- so using it would have been circular. The control is offered on single-filament sources too, because an STL, and equally a mesh-only 3MF exported from CAD, has no colour anywhere else to inherit. That is the case this exists for, and it took three shapes to make it visible: a swatch in the label row was identical to the read-only dot multi-colour rows have carried for releases, and adding the hex beside it only made it look like a caption. It now sits beside the dropdown, styled like it and the same height, with the swatch and hex wrapped in one label bound to the input so a click anywhere on it opens the picker. An untouched slot with no designed colour submits an empty string rather than the control's displayed default. A sent colour outranks the preset's own, so pinning the placeholder would silently discard the real colour of an imported OrcaSlicer profile that carries one. Slicer Pipelines pick up the same chain without carrying a colour of their own. Also adds a warning for a defect found while investigating this: a filament preset whose name the sidecar's bundle cannot resolve is not rejected. The CLI inherits nothing, falls back to its defaults for every field, and returns a well-formed success -- measured, an unresolvable name slices as filament_type ["PLA"] at nozzle_temperature ["200"] with filament_ids [""] and filament_vendor ["(Undefined)"], so a PETG preset a sidecar image predates prints at PLA temperatures with no diagnostic anywhere. Both signals are required together, which keeps it off the two legitimate lookalikes: a hand-written profile that never named a vendor still carries a real filament id, and a user's own cloud preset carries a vendor while legitimately having no bundled id. The file is kept rather than refused, unlike the missing start G-code of whoever can see the temperatures. |
||
|
|
5211fd4575 |
Store a failure reason in one vocabulary, not three (issue #2974)
failure_reason was written three different ways and nothing reconciled
them. derive_failure_reason wrote English display labels ("Layer shift"),
older builds of the archive editor wrote the translated label in whatever
locale that user was running, and the two stale-archive paths wrote
English prose sentences. All three reach one column -- the archive PATCH
has mirrored the field onto the latest print-log entry since #1444 -- and
the Failure Analysis widget groups on the raw value, so one real cause
occupied several buckets. On a live install before this landed:
print_log_entries held 91 rows reading "User cancelled" beside 1 reading
"userCancelled".
In an English UI those two render as the same words twice with different
counts, which is why nobody spotted it. In any other locale one of them
stays English, because a stored label has no key for t() to resolve. The
editor was worse than cosmetic about it: its reverse lookup compared the
stored value against t() in the current locale, so for a non-English user
nothing matched and the dropdown opened empty over an archive that
plainly showed a reason.
The keys were already canonical and already enforced.
_FAILURE_REASON_KEYS in api/routes/print_log.py rejects anything else
with a 400 and explains why in its own comment -- the widget renders
values back through t(), so an unrecognised one surfaces as a raw string.
derive_failure_reason had simply never been held to that rule. It now
produces keys, and the cancel branch returns userCancelled.
The two "Stale - ..." sentences become one new noStatusUpdate key. Both
describe the same observation, that no end-of-print status ever arrived;
which of the two situations occurred is already carried by status --
cancelled at the stale-cleanup site, the reconciled outcome at the
reconnect site -- so collapsing them loses nothing and gives Statistics
one bucket instead of two sentences that could never be translated. It
had to enter the vocabulary rather than merely be tolerated, because the
editor discards any value it does not recognise.
Existing rows are converted by a startup migration folding 168 historical
labels onto the 12 keys across both columns. It is exact rather than a
guess: every label across all 14 locales resolves to exactly one key,
with no collisions. The map is a frozen snapshot rather than something
read from the locale files at run time -- it maps what was written
historically, so regenerating it from the current translations would
silently stop recognising the very rows it exists to convert. A value
outside the map is left alone; guessing would be worse than leaving one
honest string in its own bucket. There is no one-shot settings flag, on
purpose: the statement only matches values in the map and a key is never
a label, so it is self-terminating, and a flag would permanently skip
anyone who restores an older database.
The last part is a data-loss bug that was not in the report. The editor's
fallback to '' was not merely a wrong-looking dropdown -- the empty
selection was then saved over the stored text, so opening the editor on
an archive whose reason was free text and pressing Save destroyed the
classification. An unrecognised value now keeps its own option and
survives a save.
|
||
|
|
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. |
||
|
|
ea0ceae42d | Updated README | ||
|
|
d5c7047765 |
Name an AMS slot after the spool assigned to it
The print dialog described every slot from the printer's own telemetry, and
a printer cannot describe a spool it did not sell: a tray record carries no
brand field, tray_sub_brands is left empty for anything that is not a Bambu
spool, and the colour arrives as a bare hex the client resolves against
Bambu's own colour catalogue. A Devil Design PLA Basic Orange assigned in
Bambuddy therefore read as "PLA (Sunflower Yellow)" -- Bambu sell a
Sunflower Yellow at the same FEC600 -- while the printer card, which reads
the assignment, named it correctly. Two views of one slot, disagreeing.
GET /printers/{id}/inventory-remain now carries each bound slot's brand,
material, subtype, colour name and hex alongside the pooling key it already
sent, and the dialog prefers that over telemetry. The fallback is per field,
not all or nothing, so a spool with no stored colour name still gets the
catalogue lookup it had before while its brand and subtype come from the
binding. Resolved server-side because the identity rule differs per
inventory mode -- brand is a column in internal mode and a nested vendor in
Spoolman's, where the subtype is the filament name with its material prefix
stripped and the colour name has a three-step read order Spoolman has no
field for. Spoolman's synthesised colour name, which falls back to the
subtype, is withheld rather than rendered as "PLA Basic (Basic)".
Matching is deliberately untouched and still runs on the printer's
telemetry. The auto-assignment, the colour-mismatch test and the mapping
that actually gets dispatched all read type, colour hex and tray_info_idx,
so renaming a slot cannot make the panel and the dispatcher draw different
conclusions from it. The payload is re-read on every open of the dialog: it
names the slots now, and a spool assigned moments earlier would otherwise
keep its old name for the rest of the thirty-second stale window. Done at
the two readers rather than by invalidating the key from each of the
eighteen places a binding or a spool can change, half of which are internal
paths and half Spoolman ones -- covering some would make freshness depend on
which mode you run.
Two hardening fixes fall out of putting a mapper on this path.
build_slot_materials runs before every queue start through
compute_deficit_for_queue_item, and _map_spoolman_spool walks a dozen nested
fields off the wire, any of which arriving as the wrong type raises
AttributeError rather than ValueError. Naming a slot must never cost a
dispatch, so that call fails soft to no name. The same inputs also reached
_material_identity_spoolman and _normalize_color_for_id, which have always
been on this path and would fail a queue start on a Spoolman record whose
filament is not a dict or whose color_hex is a number; both now read those
as "nothing to pool with". Behaviour for well-formed input is unchanged --
the guards only intercept types that previously raised -- so no pooling key
moves and AMS Filament Backup is untouched.
|
||
|
|
426063e829 | Updated CHANGELOG | ||
|
|
35ee7352d3 |
Merge pull request #2973 from maziggy/refactor/default-profiles
Configure a spool's filament preset and K profile per nozzle Adds per-printer-model filament presets and per-hotend K profiles to a spool, and makes every path that configures an AMS slot respect them. See the CHANGELOG entry for the user-facing description. |
||
|
|
e5a18bf58b |
Key a K profile on its nozzle's flow type
A printer files each calibration under a nozzle id of the form HH00-0.4 (high flow) or HS00-0.4 (standard) and can hold both for one diameter -- a maintainer's H2D carries 102 high-flow entries against 6 standard -- because the same filament reads a different K through each. Nothing read that, so a standard-flow profile could be selected for a high-flow nozzle and vice versa. The flow is now stored with the profile, shown against each option in the picker, and checked before a stored profile is applied. Two spellings have to agree for that: a calibration entry says HH00-0.4 while the fitted nozzle reports HH01, so the comparison is two characters rather than four -- the trailing digits are a hardware variant the calibration table normalises to 00. Unknown flow on either side matches anything, which is what it has to do. Every profile stored before this has none. And an X1C declares none on any profile at all -- probed live, all eight come back with an empty nozzle id, against a four-digit cali_idx and a populated setting_id -- even though the machine really does take either nozzle. supports_nozzle_flow_type is therefore the wrong thing to gate on: it returns True for an X1C, and treating that silence as Standard would have dropped every X1C profile the moment a high-flow nozzle was fitted. What the printer's own table declares per profile is the test. NozzleInfo.nozzle_type carries two vocabularies by printer generation -- the nozzle material on legacy printers, the flow code on H2 -- and the comment claiming only the former is corrected. Anything that is not HH or HS reads as unknown, which is what makes the material spelling harmless. Storing both flows for one hotend and diameter is deliberately not done: spoolman_k_profile is UNIQUE on (spool, printer, extruder, diameter) with no flow column, and allowing a second row in internal mode alone would break inventory-mode parity. The picker marks a profile whose flow does not match what is fitted instead of letting it look configured while doing nothing. |
||
|
|
a7b563334e |
Configure a spool's filament preset and K profile per nozzle
A slicer preset is bound to a printer model: "Bambu PLA Basic @BBL X1C" is not the same preset as "@BBL H2C", and Bambu names a nozzle size in it as well. A spool carried exactly one, which was right until the same spool was used on a second machine -- the AMS slot on the other one was then configured with a preset that machine has no profile for. K profiles had the matching gap from the other side: the tables have always been keyed per hotend, but the picker could not express it. spool_filament_preset and its Spoolman twin store the exceptions, keyed (spool, printer_model, nozzle_diameter). Model rather than printer because the preset is a property of the model -- "@BBL X1C" is the same preset on every X1C, and asking per machine would mean picking the identical value twice. K profiles stay on printer_id, because a K value is measured on one physical hotend and two machines of the same model legitimately differ. Resolution is exact (model, diameter) -> (model, "") -> the spool's own preset, so a spool nobody has configured behaves exactly as it did before. The form writes one row per nozzle size and never the "" row; that level is kept for API clients wanting one value to cover a model. Both halves cover every standard nozzle size rather than the size currently fitted, because a spool is configured once and nozzles get swapped. The PA Profile tab becomes a Printers tab: a model list beside a detail pane holding a preset row per size and a K-profile grid of size by hotend. Each model is offered only the presets that name it, through the same matcher the Configure AMS Slot modal filters with, which moves out of that component into utils/slicerPrinterMatch. Presets whose name identifies no model -- most user-authored and OrcaSlicer ones -- stay offered everywhere, as does whatever is already selected, so a saved override cannot vanish from the control that shows it. Every preset carries an origin badge in the wording and colours that modal already uses. Every path that configures a slot now respects both: manual assign in either inventory mode, RFID auto-assign, the Spoolman tag link, the re-fire when a slot goes empty to loaded, the re-apply after a calibration-table refresh, and the re-selection when a Filament Track Switch moves an AMS to the other nozzle. Which nozzle a slot feeds, and how wide it is, was worked out independently in seven of those places, each reading nozzles[0] for every slot on the machine -- correct on a single-nozzle printer and on a dual-nozzle printer with matching nozzles, wrong the moment two sizes are fitted. That resolution is now services/slot_nozzle. Which array entry belongs to which hotend is no longer inferred. Measured on an H2D fitted with a 0.4 high flow on the left and a 0.6 on the right, nozzles[0] reads the right hotend, so the array is indexed by extruder id and the H2/X2 parser's convention is the one that holds. The legacy parser's opposite convention never governs a real dual-nozzle machine: every model in DUAL_NOZZLE_MODELS reports device.nozzle.info, and left_nozzle_diameter appears in no log or wire capture. Two comments that said otherwise were wrong and are fixed; amsHelpers' code was right all along and only its comment lied. Four defects surfaced while wiring it, all pre-existing except the last. The picker identified a chosen calibration by cali_idx alone, and the printer numbers its calibration table per nozzle -- on a dual-nozzle machine the same index exists on both hotends meaning different things, so saving could persist the other hotend's K value and diameter; SpoolBuddy's write-tag page carried a verbatim copy and gets the same fix. RFID auto-assign chose a K profile with no extruder test at all, so a spool calibrated on both hotends had a coin toss decide which pressure-advance value the slot got, on the path that runs unattended every time a Bambu spool is loaded. The Spoolman tag-link path resolved no preset whatsoever, configuring every linked slot with a generic material id and discarding a preset set in inventory -- the same defect #1713 fixed on the assign path, one function over. And an FTS inlet move re-selected K for nozzle 0 rather than for the nozzle the AMS had just been moved to. The last one is new here: a per-model override can be a cloud USER preset, whose PFUS-prefixed id the slicer rejects, and passing it straight into extrusion_cali_sel would silently lose the K-profile link. Reached the printer only where such an override exists, which is why nothing in the suite caught it. printer_safe_filament_id falls through to the spool's own preset and then the tray's RFID value instead. Reading a printer's calibration table asks for one nozzle size at a time. H2-series firmware answers only the first one or two of a concurrent burst of extrusion_cali_get and silently drops the rest, each dropped request costing a five-second timeout before its retry: measured at 11 and 23 seconds on an H2C and an H2D for four parallel requests, against roughly one second in series. An X1C answers all four at once, which is why this only ever surfaced on dual-diameter printers. Printers themselves are read in parallel -- separate machines are separate connections. The Configure AMS Slot dialog opens on the spool's own configured values, falling back to the slot's last manual configuration and then the tray's RFID data. The spool form is wider for the two-pane layout, colour, weight, cost and location move to their own tab in two columns, and a printer card in expanded view lists every fitted nozzle size rather than the first entry alone. |
||
|
|
59d2713acf |
Give a no-3MF archive the timelapse baseline it never took (issue #2957)
_capture_timelapse_baseline_at_start says in its own docstring that it must be called from every on_print_start path that proceeds to a real print, and what breaks otherwise: the completion scan falls back to snapshotting the card after the printer has written the video, so the new file lands inside the baseline and no diff can ever match. There are three such paths. The fallback branch was not calling it, and nothing else covered the gap -- on_print_running_observed is restart-recovery only and is suppressed whenever on_print_start fires. Every no-3MF archive therefore reached completion with no baseline in memory and none on the row, and kept its timelapse only by accident. Not confined to the reported cool-off. The same branch serves the internal-storage verdict, so every H2C/H2D/P2S print that Bambu Studio's Print button sends to eMMC lost its timelapse the same way, off a card that was holding it the whole time. Measured on a live install: 0 of 9 fallback archives had a baseline, against 73 of 275 normal ones. The branch now takes one like the other two, and last like they are: it lists the printer's timelapse directory, so a slow card must not delay the active-print registration, the energy reading, the archive-created event or the start notification ahead of it. The other half is a print shorter than the five-minute cool-off, where the card is still unreadable at the one moment a baseline has to be taken. list_files_async answers [] when its connect fails rather than raising, so that is indistinguishable from a card holding no videos. When the cool-off expired inside the 900-second poll window every video on the card read as new, the first in listing order won, and a stale unclaimed video was attached to the print and then deleted off the printer. The empty baseline is still recorded rather than refused. Bambuddy deletes each video once it is attached, so the usual card holds exactly one at completion and an empty baseline resolves it correctly; refusing outright would lose that common case to protect a rare one, and persisting NULL instead would send completion to snapshot a card that by then has this print's video on it. Instead the scan marks such a baseline untrusted and the attach step declines to choose between several candidates, leaving them for the manual Scan for Timelapse button. require_unambiguous defaults to off, so the only caller whose behaviour changes is that scan. |
||
|
|
88152dc0c6 |
Add Dutch to the interface languages (issue #2891)
The translation was contributed as a file on the issue and needed three corrections before it could be wired up, all of which the parity gate found. The nine stats.timeframe.* entries had their keys translated along with their values -- 'today' had become 'vandaag'. Code resolves those by the English key, so the Statistics timeframe selector would have found nothing and rendered raw key names for every Dutch user. The values are kept and the keys restored. The file was translated against an older en.ts and was 84 leaves short: the Filament Track Switch feed prompts, the AI-detection status strings, the no-3MF internal-history banner, the batch-order stranded-plate notices, the Avery starting-position field, and the whole locationHaSensors section from #2824. Rather than splice those in, nl.ts is regenerated from the en.ts skeleton with the contributor's strings carried over by key, so its structure, key order and section comments match the reference exactly and a later diff against en.ts reads as content rather than as reordering. The generator fails rather than emit a key it has no translation for, so nothing fell back to English silently. 229 leaves are identical to English. Each was checked and all are kept: Dutch takes most technical UI vocabulary verbatim -- printer, filament, status, nozzle, timelapse, dashboard -- and Dutch slicer users use the English feature names untranslated, so support, ironing, prime tower and gap fill stay as they are. The 123 distinct values are enumerated in a NL_COGNATES list in check-i18n-parity.mjs, the same shape the other twelve locales use, so the exemption is a listed decision per string rather than a blanket skip for the locale. Backend app/i18n still carries English and German only, so push notification text falls back to English for Dutch. That is true of the eleven other non-German locales too and is left alone here. Parity green at 6264 leaves across 14 locales. |
||
|
|
9500c046c0 |
Ask which nozzle to feed when a Filament Track Switch is fitted
Load and Unload in the AMS slot menu did nothing on an H2C with the switch fitted. The ams_change_filament command carries an optional extruder_id and Bambuddy never sent it. That is correct on every printer without the switch, and is what BambuStudio does there too -- each AMS is wired to one hotend, so the firmware works the target out for itself and an explicit value would only be a guess at something it already knows. Fit the switch and every AMS is bound to one of its two inlets instead, either hotend is reachable from any slot, and a command naming neither leaves the firmware nothing to act on. It was discarded in silence. Load now asks which hotend to feed, on the same terms as Bambu Studio: no preselection, so a stray Enter cannot feed the wrong one, and the hotend already fed from that very slot greyed out. Printers without a switch send a byte-identical command and still load in one click. A switch fitted but not yet set up -- any AMS still unassigned to an inlet -- refuses the load up front rather than publishing one the firmware will drop, mirroring DevFilaSwitch::IsReady, which likewise demands a switcher position on every AMS. Unload was addressed at the same time. It was aimed with tray_now, a single value for the whole printer, so on any dual-nozzle machine with both hotends loaded it unloaded whichever that field happened to name regardless of which slot's menu was used. It now names the slot and resolves the holding hotend from device.extruder.info, previously read for temperatures only. That resolution is gated on the printer having reported two extruders: single-nozzle machines do send the block, but nobody has read a single-nozzle snow value off the wire, and staking every X1C, P1S and A1 unload on an unverified encoding buys nothing where tray_now is already unambiguous. Both new state fields ride the WebSocket and are in the broadcast key, and both are computed in the REST status route as well -- that response is what the page has before any push arrives, and leaving them at their defaults would have told a correctly set-up machine that its switch was not set up. Verified on H2C-1, AMS-A slot 3: loaded and unloaded from each hotend in turn, all four correct. Covered by 18 MQTT unit tests, 4 status-dict tests, 7 integration tests and 6 component tests. Two known stragglers, both deliberately left alone. Load on an AMS-HT slot has never worked -- an HT unit is addressed by its unit id rather than ams*4+slot, which these endpoints do not accept -- so unload there keeps the printer-wide form it always used instead of gaining a slot it cannot name. And a slot's K-profile still follows the AMS's plumbing rather than the nozzle just loaded, so loading to the far hotend leaves the other one's calibration bound; that is the same per-nozzle problem the filament and K-profile redesign is scoped to fix. |
||
|
|
196fcaf15b |
Keep the last run a batch order can re-queue a plate from
An order produces what it still owes by cloning an existing queue item for the same plate. That row is the only record of the printer target, AMS mapping and print options the user chose, so deleting the last one left the order reporting work outstanding that nothing could produce, and no way to close it out: Cancel was hidden unless there were pending items to cancel, which by then there were none. Deleting an order's last surviving run for a plate now cancels it instead. A cancelled run does not satisfy a target, so the order still owes the print and can still make it. A completed run is exempt and still deletes outright -- rewriting it as cancelled would falsify what the order produced. Also: the response reports per plate whether anything is left to clone, so the card explains a stranded plate rather than offering a button that can only fail; dispatch skips a stranded plate instead of aborting the whole order; and Cancel is offered for any active order. |
||
|
|
573dde8a37 | Updated CHANGELOG | ||
|
|
a68933cb2f | Allow Avery label sheets to start at an unused position (#2879) (#2918) | ||
|
|
14f46510ba |
File the printer's calibration table under the nozzle it belongs to (issue #2854)
The K value on an AMS slot card went blank after a while and came back after a backend restart. It is not the MQTT merge: that preserves a tray's k correctly. H2-series trays have no k to preserve. Verified against the H2 wire capture in logs/vp_wire -- every tray reports cali_idx and nothing else -- so the number on the card is resolved from that index against the printer's calibration table in state.kprofiles, and that table was a single global list. An extrusion_cali_get response is the complete table for one nozzle diameter, and the printer answers whoever asks; BambuStudio's queries arrive on the same report topic we subscribe to. Every response was assigned straight to state.kprofiles, so any one answer stood for the whole printer. The nightly GitHub backup asks for 0.2, 0.4, 0.6 and 0.8 in turn and finishes on 0.8, which holds nothing on a 0.4+0.6 machine: logs/bambuddy.log records exactly that at 17:15 on 2026-08-25, and the table was empty from then until something refilled it. Responses are now bucketed by the diameter they describe, so an empty answer for a size the printer does not have clears only that size. The three assign paths that look an index up by nozzle_diameter get the same fix for free -- they were quietly finding nothing whenever the last response was for another nozzle, which is what spoolman_inventory has been logging as a stale kp. Bucketing makes state.kprofiles a union, and cali_idx is numbered per nozzle, so the index alone no longer identifies a profile. The REST serializer has keyed on (extruder, cali_idx) since c5e005586; the WebSocket one still keyed on the index alone, which meant the first render of a card could be right and every update after it wrong. Both now share one resolver. It goes through the extruder the slot feeds, and where that does not single out one profile -- a single-nozzle printer that has been swapped, so both its tables sit under extruder 0 -- it falls back to which diameters are actually fitted. Where neither settles it the card shows nothing, because a blank space is a smaller error than confidently printing the other nozzle's number. Deliberately no loosening to a bare cali_idx lookup on a miss: that is the cross-nozzle bleed the extruder keying was added to stop. Nothing read the table on connect, which is the other half of the report. It arrived by luck -- a visit to Profiles or Configure Slot, a backup, or the printer answering someone else -- so a Bambuddy nobody had opened showed a card with no K values at all, and "restart and they come back" was the printer happening to broadcast rather than anything we did. It is now read once per connection, on the same latch the stale-print reconcile uses. Only the fitted diameters are asked for, one request on a single-nozzle printer and two on a dual; probing the four sizes blind is what the backup does and what blanked the table. The edge is gated on a nozzle diameter being known as well as on the state being known, because the first push_status is what makes the state known and does not always carry the nozzle fields -- latching there would spend the connection's one attempt on a printer that could not yet say what was fitted. Adopting an unsolicited table now logs at debug. It was the quietest way for the card to change underneath us and there was no way to see it in a support bundle. Three test files gained nozzles=[] on their PrinterState stubs. The field has always been on the dataclass; the connect edge is simply the first thing on that path to read it. |
||
|
|
08a58b1ef4 |
Keep both modes' slot assignments across an inventory mode switch (issue #2812)
Turning Spoolman mode on ran an unfiltered delete(SpoolAssignment) across every printer. Turning it straight back off cleared the other table instead, so the two directions were symmetric in code and one-way in effect, and the setting auto-saves on a 500 ms debounce with no save button and no confirmation. Opening the settings page to see what the option did was enough to destroy the configuration: the reporter's log shows four toggles in 85 seconds, which is someone looking and reverting, and the assignments never came back. The deletion was not careless. Checks that read both assignment tables would otherwise let a row in the mode you are not using answer for the mode you are, which is how #1473 was fixed, and emptying the inactive table made that impossible by construction. The cost was that the guarantee was bought with the user's data. That decision belongs to the readers -- the mode is a property of the install, not of the rows -- so spoolman_owns_assignments now answers it and nothing is deleted on a toggle. Each mode keeps its own assignments and switching is reversible. Existing installs need no migration: their inactive table is already empty, because it was being emptied. Six sites had to be told which mode they meant, and only two of them are the reads you would guess at, the missing-assignment notification and the queue cost estimate. The per-slot K-profile lookup consults the built-in table first and, on a hit with no matching profile, deliberately stops rather than falling through to Spoolman, so a leftover row would have shadowed the Spoolman binding for that slot -- the symptom #1556 reported from the other direction. configure_ams_slot *writes* a K-profile against whichever table answers first, so the same leftover would have filed a calibration against a spool the printer is not drawing on and never written the local one, leaving a calibration that appeared to succeed and then did not apply. The auto-unlink pass in on_ams_change is the one that would have made this change worthless. It drops any assignment whose tray no longer matches the fingerprint it recorded, and it ends in db.delete. Ungated, it would have removed the preserved rows one slot at a time as the AMS contents changed under the other mode -- the same loss, arriving slowly enough not to be connected to the toggle that caused it. The sixth is the built-in remaining-weight fallback inside the Spoolman AMS sync, and it is deliberately left inert rather than woken up. It could never fire while the table it reads was being emptied, it is keyed by slot rather than by spool, and create_spool writes remaining_weight unconditionally where the update path does not -- so preserving the rows would have seeded a stale figure into a brand new Spoolman spool the first time a tray reported an unusable remain%. The query stays, gated off, so the intent survives for whoever revisits the cross-mode fallback. Separately, a print that could not debit a spool said nothing about it, and that is what turned a mis-click into lost filament. The reporter's print was already running when they toggled. At completion it resolved its 3MF, read its per-filament grams, resolved its tray, and then skipped the debit because the assignment row no longer existed -- logged at INFO, invisible under the default log level, while the completion notification fired as usual. 65.49 g was never deducted and they only noticed because a spool's remaining weight looked wrong. _resolve_spool_id_for_tray has no tag or fingerprint fallback, so there was nothing else to catch it. The skip is now a warning naming the grams, and a completed print that failed to charge a tray it drew from raises the missing-spool-assignment notification. The print-start check cannot cover this and was right to stay quiet: the assignments existed when it ran. The two are different statements -- the first says the weight may not be tracked, the second says it was not -- so a print warned at start will notify twice, which is the right trade. Collected across the print rather than fired per slot, and given the caller's session, because this runs inside on_print_complete's transaction and opening a second one to read the printer's name would deadlock against it on SQLite. This is independent of the toggle and catches any other cause of an assignment disappearing mid-print. |
||
|
|
39835437a3 |
Price a print from the spool that fed it, not the default rate (issue #2591)
Spoolman holds per-spool pricing, and #261 gave that as the reason for integrating with it. Nothing ever read it. A print's cost is set once, at archive time, from the built-in Filament catalogue matched on the primary type and falling back to a global default rate -- and in Spoolman mode nothing revisited that figure afterwards. The per-spool recompute that would have fixed it, in usage_tracker.on_print_complete, runs only over rows the built-in inventory writes, and Spoolman mode hands the usage tracker spoolman_owns_usage at print start so it writes none. The reporter's catalogue was empty, which is the ordinary state of one in Spoolman mode, so every print came out at the default no matter what the linked spool cost. Multi-material was wrong twice over there: the primary type's rate applied to the whole print's weight, so a slot of expensive PA was billed at the price of the PLA beside it. Each slot is now priced from the spool it was actually charged to, at the moment of the charge, and the per-slot costs are summed -- which is what fixes the multi-material case, rather than a separate change. All three charge paths feed it: per-slot, tray-split, and the remain%-delta fallback. The rate is the spool's own price when set, else the filament's, over filament.weight. That is net grams excluding the core, and the same field the remain-delta path already divides by to turn a percentage into weight, so a spool that can be charged by percentage can always be priced. The price comes out of the get_spool call the colour and material rewrites already pay for, so the tagged path costs no extra round trip. Grams no spool could price are covered at the global default in one subtraction against the archive's own total. A spool with no price, a tray with no Spoolman row, and filament the sliced file never attributed are the same case from here, and without the top-up a print with one priced slot out of four would report a quarter of its cost -- #1344 in the other inventory mode. Only the first run writes the archive, matching the built-in writer (#1378); reprint actuals live in PrintLogEntry. If no slot could be priced at all, whatever archive.py recorded is left alone, so an install with prices in neither place stays where it was. Applied even when the slot-to-tray mapping was a positional guess, unlike the colour and material rewrites beside it. Those overwrite what the slicer recorded, which is why a guess must not touch them. The cost has no such original -- archive.py's figure is itself derived from a default rate -- and the grams have already been deducted from these spools, so the archive should say what that deduction was worth. Both cost recalculations would have undone it on the next run. /rescan and /recalculate-costs rebuild an archive's cost from SpoolUsageHistory and fall back to the catalogue or the default when there are no rows, which in Spoolman mode is always, so the fallback was not a recalculation but a downgrade. The spool-to-slot resolution a price is derived from exists only while a print is completing and cannot be rebuilt from the archive row, so both now leave a cost alone rather than replacing it with a worse one, and the bulk endpoint reports how many it kept. An archive with no cost yet is still priced, and with Spoolman off both behave exactly as before. The rate parser refuses more than it looks like it needs to, because everything it refuses was reachable. A non-dict filament raised through a call that sits after a successful use_spool, which would have abandoned the remaining slots of a multi-material print with the charges already made. NaN compares False against every bound, including the applier's own total <= 0, so a NaN price would have been written to the archive with nothing downstream able to clear it; two finite operands can produce it by overflow, so the quotient is checked as well as the inputs. A bool is an int in Python, and float(True) is 1.0 -- a weight of 1 g prices a spool per-gram at its whole cost. And a spool-level price of 0 now falls through to the catalogue rather than reading as free: Spoolman leaves the override null when unset, but importers write 0 often enough that treating it literally would price a whole print at the default with a good catalogue price one level down. |
||
|
|
935d4b5bbf |
Charge the tray the printer said it used, not the first one loaded (issue #2953)
A sliced file numbers its filaments 1..4; which AMS tray each came from is decided when the job is sent. #2768 gave the Spoolman writer two ways to recover that decision when the print did not come through Bambuddy: the printer's own mapping field, and a colour match of the 3MF's slots against the loaded trays. An A1 satisfies neither. It publishes no mapping field, and it drops the MQTT connection when we subscribe to its request topic, so the slicer's instruction never arrives either. That leaves the colour match, and it compares hex strings exactly. The reporter sliced with a generic black profile against a tray they had set to charged slot 1 to whatever sat in the first tray -- 2.17 g onto a grey PLA+ spool, while the print was fed from tray 3. Their bundle carries the printer's own answer: "Tray change during print: tray=3 at layer=0", recorded 90 seconds in, and read further down the same completion pass by _print_used_tray_keys to decide which slots the print had touched. The same pass then charged tray 0 on a guess, and logged "AMS0-T3: remain% did not fall over the print" about the tray that had actually done the work. _single_slot_tray_from_state adds the third rung. For a print with exactly one slot carrying usage, the one slot came from the one tray, so the printer's tray reporting answers the question directly: the mid-print tray-change log, then the tray loaded at print start, then the current one, then the last real tray seen. That is the ladder usage_tracker.on_print_complete has consulted since it started resolving mappings at completion -- Spoolman users were the only ones not getting it, which is why an install running the built-in inventory has never shown this. On this printer only the first and last rungs can fire: tray_now_at_start is 255 because print start runs before the filament is loaded, and the A1 parks tray_now back at 255 the moment a print ends. Gated on exactly one slot with usage, like the internal writer: a multi-colour print moves tray_now on every change, so one reading cannot then be attributed to one slot. It also declines when the log holds more than one switch, because an AMS-backup runout is split per segment (#1793) and a single-tray mapping would land the whole print on one spool. That gate needed the guess warning to exclude the split path too. The split never reads slot_to_tray at all -- it charges each segment to the tray the printer announced switching to, which is the same evidence this fallback is built on -- so a declined mapping there is not a guess, and calling it one suppressed the archive rewrite for exactly the prints whose attribution is best supported. Nothing covered that combination; a test does now. Where nothing names a tray the positional default still stands, because it is right for an AMS loaded in slicer order. It now says so at warning level so a support bundle carries the reason, and it no longer restamps the archive's filament colour and material from a spool it picked by position. That restamp is what made the fault read as data loss: the grams can be put back, whereas overwriting what the slicer recorded leaves nothing to compare against, and the reporter's archive had already been rewritten from #000000 to the wrong spool's grey. A slot that consumed nothing also stops claiming a tray in the handled set -- it was never charged, so the remain-delta path should stay free to cover it rather than be suppressed by an estimate of zero. The request-topic probe is the same failure reached from the other side. A printer that refuses kills the TCP connection instead of returning a SUBACK failure, so the only signal is "we subscribed, then got disconnected", and that was believed the first time it happened. Every other reason a connection drops inside the same window looks identical -- a network blip, the printer rebooting, the container stopped mid-probe -- and the verdict was cached per serial with no re-probe anywhere, so on a printer that supports the topic one unlucky drop cost mapping capture for the rest of the process and every slicer print after it was charged by tray position. It now takes two consecutive drops, and a disconnect we asked for is not counted. A printer that genuinely refuses answers the same way every time and pays one extra reconnect; one already known to refuse still skips the subscription outright rather than reopening a reconnect loop. |
||
|
|
4dc1aa7b10 | Updated CHANGELOG | ||
|
|
c3677865b6 | Give the AMS temperature alarm its own threshold (issue #2905) (#2943) | ||
|
|
8fe536fb38 | Updated CHANGELOG | ||
|
|
73912d4f05 | Let a clear spool stay clear on the way to Spoolman (issue #2912) (#2924) | ||
|
|
d9bc7ae47a |
Read a NULL notification flag as off instead of dropping every provider (issue #2827)
Adding on_stock_reorder_alert and on_stock_break_alert to the provider schema made them required on the way out as well as in: the response model inherits the write model. Every on_* column on notification_providers is nullable with no server default, and where the table was created from Base.metadata before run_migrations, the ALTER ... DEFAULT false that introduced those columns was swallowed as a duplicate and never backfilled existing rows. Those NULLs were harmless until the flags were read, at which point the row failed validation -- and a list is validated as a whole, so one row took every provider with it. The route returned 500 and the UI rendered an empty list, so configured providers looked deleted. Backfill them to off, which is what the sender already assumed: it selects providers with IS TRUE, so a NULL flag never sent anything. A NULL flag now also reads as off rather than failing the response, across all of them, so the next flag added to this schema cannot repeat it. Writes are unchanged. |
||
|
|
df57e5213b | Updated CHANGELOG | ||
|
|
54af3146a3 | [Feature]: Bind Home Assistant sensors to storage locations (dryboxes/bins) (#2827) | ||
|
|
5dd7bd213f |
Read a print's destination from the report topic, not just the request one (issue #1820)
current_project_url was assigned in exactly one place, _handle_request_message, and _on_message calls that only for the request topic. A print started from the printer's own screen publishes nothing there, so the field stayed None for the one case the storage verdict exists for: the file is already in the printer's model library under /userdata/model/history/, which port 990 does not serve. The verdict then fell through to the sdcard flag, and @ojimpo's H2S reports that flag true -- its "card" is the internal eMMC -- so every such print ran the full sweep before giving up. He measured one: 16 filename-and-directory attempts over 22 FTPS connections, 18 of them refused, 6.4 seconds, then a fallback archive holding a name and nothing else. The printer does announce where the file lives, as an unsolicited project_file *response* on the report topic about two seconds before gcode_state reaches PREPARE. _process_message now reads the url off it, gated on result SUCCESS and a non-empty value so a refused dispatch cannot name a file that was never written. Reading it there rather than only at the request topic also covers an install neither of us had in view: some brokers refuse the request-topic subscription, and on those no print of any kind had ever populated the field. The new branch captures state and nothing else. The "External project_file payload" diagnostic stays with the request-topic handler: our own dispatch is echoed on both topics, the request-topic echo lands first and clears _own_project_file_key, so reusing the diagnostic here would have logged every Bambuddy-started print as somebody else's. A test pins that. What the print names is now what gets tried -- the five directories a copy could be in, rather than the ~110 connections that cannot succeed. The probe is still worth running: an H2S keeps recently used jobs under /cache and archives them in full while they last, which is why the reporter's two prints on the same day behaved differently. Slicer-sent prints are unchanged. The banner no longer describes a step that never happened. With no reason recorded, a blank archive fell back to the original wording -- "Store sent files on external storage" is off in your slicer -- which on that printer is on, and which the internal-storage wording from #2780 already explains would not help on an H2. The archive that most needed that explanation was the only one that could not be given it. So file:///userdata/ now earns its own reason, internal_history, separate from the brtc://emmc dispatch case. A dispatch chose internal storage and can be aimed elsewhere; a print of a file that was already there had no dispatch at all, and telling that operator to pick External in Send names a dialog they never opened. The banner and the connection diagnostic both read the verdict's reason rather than a fixed one, so the two surfaces cannot give the same printer different advice. Thirteen locales, and a wiki section the banner links to. ----- Read the K-profile selection when the mutation runs, not when it is captured Configure Slot sends cali_idx from selectedKProfile, and the mutation read it through its own closure. React Query hands a mutation its options from an effect, so a click landing between a commit and that effect flushing runs the previous render's mutationFn -- one that captured the selection as it was before the K-profile query resolved. The payload then carries cali_idx -1 and the printer binds the default 0.020 instead of the calibrated K, while the dialog shows the right profile selected throughout. It surfaced as an intermittent failure of the per-nozzle K-profile test, about one full-suite run in six. Reproducing it with staggered query resolution showed the divergence directly: the select element held the correct profile immediately before and after the click, and the payload still carried -1. That test's slot is the most exposed case in the file -- a right-hotend slot carrying the left hotend's index, where the "keep showing the active profile" safety net cannot repair an empty recompute. The selection now goes through a ref written during render, so the mutation resolves it at execute time. An effect would have inherited the same flush ordering this exists to escape. The K value and the profile's ids travel in the same payload and had the same exposure, so they move with it. Measured over a staggered-resolution grid: 2 failures in 15 runs before, 0 in 12 after. api.getSlicerPrinterModels was also missing from the test file's mock, so that query ran with no query function and rejected in all 37 tests -- mocked now, though on its own it changed nothing, which is how the ref was confirmed as the fix rather than assumed. |
||
|
|
4e79f9c2f7 |
Fill in a fallback archive when the 3MF finally arrives (issue #2957)
A failed TLS handshake pauses a printer's file service for five minutes, and the archive flow checks that pause at the top of its path loop and gives up before opening a connection. A print that starts inside one gets an empty fallback archive 13 milliseconds later, having never touched the network. Four minutes on, the pause clears and the cover endpoint downloads the same file, parses it, takes a thumbnail out of it, and publishes it to the shared 3MF cache under the exact key the archive flow looks up. Nothing ever looks: all three readers of that cache run before or during the print-start handler that already gave up, and print completion drops the cache as its first act, deleting the file. No path existed by which a fallback archive could become a real one. Offer a later 3MF to the running print's archive, filling the existing row rather than adding a second -- the row id carries the energy reading, the timelapse session and the start notification. That covers the reported case at no network cost, since opening the printer card already downloads the file. Schedule a bounded retry when the pause is what caused the fallback, spending the cache first and the printer only if that misses. Not scheduled for the other cause: a print kept on internal eMMC has no FTPS copy to come back for, and retrying it is the sweep removed in #2780. The two reasons are now recorded separately instead of both landing as "no 3MF". Recovery refuses anything that is not a readable 3MF -- a truncated download would replace an honest empty archive with wrong metadata -- and leaves an archive alone once it has a real file. Three things the recovery path has to get right, each with a test: Reduce every retry candidate to a bare name. MQTT hands `filename` over as "/data/Metadata/plate_1.gcode" on some firmware, and joining that onto the temp directory yields the absolute path itself, so the retry is fed the flow's own sanitised candidate list and strips the path again on its own account. Serialise recovery per printer. The cover endpoint's single-flight coalesces by view, so two views race each other, and the retry task and print completion can land on top of either -- each reads file_path == "" and runs a full copy, leaving the row on one timestamped directory and the rest orphaned. Keep looking for photos in the pre-recovery directory. `archive_dir` derives from `file_path`, so filling the row moves the archive's directory, and a photo uploaded to the empty card while the print ran stays where it was put. |
||
|
|
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. |
||
|
|
ea898141f0 |
Decide migration idempotency by SQLSTATE, not by English error text (issue #2949)
PostgreSQL renders its messages in the server's lc_messages locale. _safe_execute recognised an already-applied statement by searching the error text for "already exists", so a Russian-locale server -- which says "уже существует" -- re-raised it and aborted startup. The column already existing is the expected outcome: create_all() builds the tables from the models before the migration list runs, so on a fresh database essentially every ADD COLUMN in that list is a duplicate by design, and all 382 of them relied on that recognition. No PostgreSQL server outside an English locale could start Bambuddy at all, fresh install or upgrade. Classify on SQLSTATE instead -- 42701, 42P07, 42710, 23505 -- which PostgreSQL never translates. The existing narrowing is kept and now rests on a code rather than a phrase: a missing column counts as already-applied only for RENAME COLUMN, so a missing column during ADD COLUMN or CREATE INDEX still aborts rather than hiding a corrupt schema. SQLite keeps the text match; its driver publishes no SQLSTATE and it does not localise. The OIDC auto-link constraint read message text the same way and gets the same treatment. Verified against PostgreSQL 15 under ru_RU, en_US and C: init_db() completes on a fresh database and on a re-run in all three, and the schema the Russian server ends up with is byte-identical to the English one. |
||
|
|
ed84f0f74c |
Anchor a plug-energy test to local midnight, not the wall clock (issue #2938)
test_nothing_derivable_before_the_first_midnight failed for 31 minutes of every day and passed for the other 23.5 hours -- the shape that reads as ordinary flakiness and gets re-run rather than fixed. @ojimpo hit it running the full suite at 22:10 UTC, stashed his branch to confirm it reproduced on clean dev, and measured the window minute by minute instead of guessing. The test asserts that nothing can be derived when the only snapshot was taken after this local midnight, and it placed that snapshot at a raw wall-clock offset -- now minus thirty minutes. Its comment, "taken this morning, after midnight", is the premise, and it is only true away from the boundary. For the first half hour of each local day, now minus thirty minutes lands before local midnight, where it is a perfectly good baseline: _counter_at finds it and today comes back 1.5 rather than None. The window is local 00:00 to 00:30, which is 22:00 to 22:30 UTC under CEST and 23:00 to 23:30 under CET -- it moves with DST, since the module pins Europe/Berlin in an autouse fixture and an outer TZ makes no difference. Nothing is wrong with the production code. A snapshot from before local midnight genuinely is a valid baseline for today, and derive_today_yesterday is right to treat it as one. Only the test's premise breaks at the boundary. The snapshot is now anchored to local_day_start(now) plus thirty minutes, which is the idiom the other nine snapshot writes in this file already use and the reason none of them can drift. Replayed across 5760 minutes covering four days, including both DST switch days: the old expression fails 31 minutes per day, the new one fails none. |
||
|
|
e2a2e06da3 | Updated CHANGELOG | ||
|
|
f95c81e6cd | Take an RFID spool's core weight from the row that names it (issue #2909) (#2923) | ||
|
|
b1f5ec9642 |
Let one checkbox say where a slice's settings come from (issue #2942)
Two features in the slice dialog read as one. "Use the file's built-in settings" slices a 3MF the way its designer set it up, ignoring the picked profiles. The per-option "from file" ticks beside each setting carry the designer's individual deviations onto the profile you picked, and those arrived pre-ticked whatever the checkbox said. So a slice run deliberately without the file's settings still took sixteen values out of it -- the reporter's log names them, enable_support and support_type among them, landing on a process preset they had chosen on purpose. The ticks now follow the checkbox. Off, nothing comes out of the file until it is asked for by name; on, every setting the file changed shows ticked, because on that path the file really does drive the whole slice. Taking the designer's work in bulk is still one click, from a line at the top of the panel that says how many settings the file changed -- it is the only way left to reach them without hunting for chips across six pages of 348 options -- and it still leaves the machine-tuned keys and the two that are the picked preset for a per-key decision. The panel greys out options the slicer's own rules switch off, and it was evaluating those rules against what the user had typed alone, falling back to the compiled-in schema defaults for the rest. A preset with supports on therefore read as enable_support: false and greyed out the whole Support page while the slice ran supports. A greyed row greyed its tick too, which is how the reporter's screenshot shows a support type marked "from file", applied to the slice, and impossible to clear. The rules now see what the slice will actually run with: the preset's values, the file's values for the keys that are on, and anything typed on top. The tick is no longer gated on those rules at all -- it answers a different question, not whether an option is in play but where its value comes from. Underneath both, the support carry-over ran outside the ticks entirely, lifting four keys out of any 3MF that had supports on with nothing on screen able to decline. It now stands down for the keys that were offered and turned down, which the request can say for the first time: an empty design_overrides list means the caller was shown the file's settings and took none, where no list at all is a caller that predates the choice. That distinction is what keeps the carry whole for sources with no deviations to tick, an OrcaSlicer export among them, rather than trading one silent default for another. Worth knowing: a Bambu Studio file with supports enabled no longer switches supports on for you. Tick Enable support, or the checkbox above the panel. Measured against the reporter's own sixteen keys, and covered by backend and frontend tests -- reverting any one of the three changes fails tests. |
||
|
|
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. |
||
|
|
6988a30eae |
Carry a fault's description in the status response (issue #2926)
The HMS catalogue has been in the backend all along and the status response never carried it, so every consumer that wanted to tell a user why a print halted resolved the same 853 codes from its own duplicate of the same sentences -- this repo's Python table, the frontend modal's, and at least one third-party client whose catalogue exists purely because the server would not say. Each ages separately, and a relay watching a printer could only manage "your printer needs attention" while the server already knew it was "Filament ran out. Please load new filament." hms_errors[] entries now carry a description, defaulting to null so a client that has never seen the field is unaffected. It is resolved where the fault is parsed rather than at the boundary that prompted the request, because there are three serializers of a fault, not one: the status response, the WebSocket broadcast, and the completion payload the queue's failure reason is built from. Adding it to only the first would have handed half the feature to a relay watching the stream, which is the likelier consumer of the three. The queue's failure reason now quotes the resolved sentence instead of looking the code up a fourth time, and the notification path reads it rather than re-deriving. That they cannot report different text for one fault is the point, and a test asserts they agree. describe_fault is the single mapping from either code shape onto the table. An 8-char print_error is the catalogue's MMMM_EEEE key with the separator removed -- the parser derives full_code and that key from the same 32-bit value -- so it resolves exactly. A 16-char hms[] identifier is tried whole and then collapsed to its first and last groups. That collapse is lossy, and keeping it was the decision worth making carefully. #2728 counts 65 documented faults falling onto 0300_0001 alone, so a hit can attribute a neighbour's sentence to this fault, and refusing it looks like the stricter reading. It is not: the notification path, the queue's failure-reason helper and the frontend modal have all resolved hms[] faults this way for as long as they have existed, and it resolves real ones -- a 0500_4038 nozzle mismatch arrives in that shape. Declining to collapse would have stopped describing faults that are described today, silently suppressed the notifications they raise, and left this field null while the UI showed text for the same fault. Narrowing it belongs with #2728, where both key spaces can move together. So the lookup is exactly what it was, verified rather than asserted: a test walks every catalogue code in both fault shapes across all three alert levels and checks the result against the derivation this replaces. A future change to the lookup cannot quietly stop notifications firing. The catalogue ships one language, so the field is English and unlocalized, which the schema and the API reference both say next to it. The camwall feed is deliberately left alone -- it is code-only because its token travels in a URL on a screen, and a readable sentence discloses more than the camera picture already does. The frontend keeps resolving its own text: switching it would change what filterKnownHMSErrors counts across eight call sites, which is #1840 and #2728's argument to have. HMSError.message goes with this -- a text field that was never set or read anywhere, and an invitation to populate the wrong one now that a live description sits beside it. ----- Record a failure code the user can actually look up The queue's failure reason formats a fault's module and error into MMMM_EEEE, and that one derivation never masked the error to 16 bits. A fault arriving from the printer's hms[] array carries its alert level in the code's high half, so the label came out as 0500_24038 -- five digits in a group that has four. It is not a code anyone can find on Bambu's HMS index, and because it matches no catalogue key the sentence explaining the failure was dropped along with it, leaving the bare number alone. The nozzle-size mismatch behind #1111 is exactly such a fault. Reported one way it read "[0500_4038] The nozzle diameter in sliced file is not consistent with the current nozzle setting"; reported the other, the same physical fault read "[0500_24038]" and nothing else. There is already a helper that gets this right, used by the archive's own failure-reason lookup, so this calls it instead of keeping a fourth copy of the derivation. It also takes the raw integer code the MQTT payload carries, which the local version only handled as a string. |
||
|
|
c094102614 |
Let a virtual printer be told which address to advertise
BambuStudio reads its FTP upload destination out of net.info[].ip in the MQTT status, and the bridge fills that field from the VP's bind address. On Docker bridge networking the two are different machines' worth of address: slicers reach Bambuddy on the host's LAN IP, the container binds something like 172.24.0.2, and that private address is what the slicer was handed -- so it opened an FTP connection to an address that does not exist on its network and the send stalled around 10%. VIRTUAL_PRINTER_ADVERTISE_ADDRESS supplies the address slicers actually use, taking precedence over the bind address and over the same-subnet host interface the bridge falls back to. The armed log line names its source, so (VIRTUAL_PRINTER_ADVERTISE_ADDRESS) against (bind_address) tells an operator whether the variable reached the container at all, and the not-armed diagnostic now names it as the remedy -- a bridge-network install that has not set it is exactly the one that cannot auto-resolve either, and until now saw only that nothing worked. An environment variable rather than a change to how the advertised address is resolved, which is the decision worth recording. The VP already has a "Network Interface Override" field, and reading it here is the smaller patch, but it feeds SSDP and the certificate SANs only: honouring it would silently move the upload destination on every install that has one set -- the multi-NIC, VLAN and Tailscale setups, which are the ones most likely to have been arrived at by hand and least likely to survive being second-guessed. Unset, this changes nothing, and a test pins that. A value that is not a dotted-quad IPv4 is refused with one warning naming it and the previous address is used instead. That direction is deliberate: declining to rewrite would put the real printer's IP back in front of the slicer, which is the leak the rewrite exists to close, so a typo must not be able to reopen it. 0.0.0.0 counts as unset and whitespace is stripped, for values pasted into a compose file. Host and macvlan networking need none of this and stay what Virtual Printer is developed against. The variable removes one blocker; it does not make bridge mode equivalent. The wiki said in three places that the host address could not be discovered at all, which is no longer true, so those now describe the variable and keep the recommendation. |
||
|
|
537b4d2509 |
Stop offering AMS slots as places to store a spool
The Storage Location dropdown listed entries like "H2D-1 - AMS A1" next to
real locations, and they could not be got rid of.
They were never locations. Bambuddy used to record which slot a spool was
loaded into by writing that string into Spoolman's location field, and the
writer went away when Storage Location became something the user picks --
but the strings stayed on people's Spoolman spools, and the location sync
imports every distinct one it finds, so they have been coming back in
through the front door ever since. A printer slot is where a spool is
loaded, not where it is put away, and slot assignments already track the
first.
Deleting one by hand did not work either, which is what made this a dead
end rather than an annoyance: the delete route refuses a location that has
spools, and in Spoolman mode it counts them by matching that same string,
so every marker still sitting on a loaded spool answered 409 -- and the two
that were empty were back on the next sync a minute later.
The import now skips them and a one-shot migration clears the ones already
in the catalogue. The shape is defined once and used by both: an optional
printer-name prefix followed by AMS A1, AMS-HT A1 or External Spool, which
is exactly what convert_ams_slot_to_location produced. It stays narrow on
purpose -- "AMS Drybox" and "Spare AMS trays" are somebody's shelf, and
anything the filter swallowed would be a place they could no longer file a
spool under -- so both directions are pinned by tests.
A row is only removed when no spool in this database points at it, by id or
by legacy free-text name, so an internal-mode user who has deliberately
filed spools under such a name keeps it. Spools in Spoolman are neither
consulted nor touched: their location strings are the user's data on the
user's server, and one that still reads "H2D-1 - AMS A1" in the inventory
list is telling the truth about what Spoolman holds. It simply stops being
offered as a destination.
Verified on a live Postgres instance carrying the reported symptom: 13
locations down to 3, all ten markers removed, the two real shelves and one
hand-typed Spoolman name left alone.
|
||
|
|
b38022ec5c |
Draw a spool the way the AMS described it, and correct the tare it was added with
Two faults in the same auto-add path, both found while tracing why an H2C
slot named a wood roll as plain PLA.
A spool's swatch is composed from effect_type and extra_colors, and the
RFID auto-add set neither. It reads the colour catalogue to name the
colour and took the name alone, even though the row it had in hand also
carries those two columns -- the spool form's own colour picker hands both
to a spool a user adds by hand, so the same roll rendered one way when you
typed it in and another when the printer identified it for you.
Both columns now travel with the name. That alone changes nothing on a
stock install, because the shipped catalogue carries an effect on none of
its 600-odd rows, so the subtype is read where the catalogue has none: it
is already derived from what the printer reports, and the two vocabularies
line up -- Wood, Silk, Sparkle, Marble, Glow, Galaxy, Metal, Rainbow,
Translucent, Matte, and the Gradient, Dual Color and Tri Color that the
M*/T* colour codes upgrade a subtype to. "Silk+" reads as Silk, since the
plus is on the product name rather than the finish. A subtype that names
no effect -- Basic, Tough, CF -- leaves the column empty rather than
inventing an overlay, and a value already set is never overwritten, so the
column stays what it is documented to be: a rendering hint the user can
override without touching Bambu's categorical label.
------
Correct the spool tare an RFID roll was added with (#2909)
The lookup that gave an auto-added spool its core_weight asked for the
first catalogue row whose name starts "Bambu Lab" and took whatever came
back. There are three, and which is first is the database's business:
SQLite returns insertion order in practice, Postgres promises nothing once
a table has seen an update. The same roll was therefore recorded with the
216 g High Temp tare on one install and correctly with the 250 g Low Temp
one on another. @ojimpo's forward fix picks the row by name; this repairs
the rows already written, which the forward fix cannot reach -- 22 of 26
RFID-added spools on the instance this was traced on.
The tare is not cosmetic. A spool weighed on SpoolBuddy has its remaining
filament worked out as the scale reading minus the tare, so a 34 g low
tare credits the roll with 34 g that is not there and writes a used weight
34 g short. That error is a constant -- every later print adds to the used
weight on top of it -- so adding the difference back is exact however much
has been printed since. It is applied only to spools that have been on the
scale; one that never was has a used weight derived from the AMS remaining
percentage, which the tare never entered into.
Rows are identified by the signature of the broken lookup: added by RFID,
carrying the weight of one of the other Bambu catalogue rows, with the
weights read out of the catalogue rather than hardcoded so an install
whose rows have been re-measured is repaired to its own numbers. Keying on
whether a catalogue row had been recorded would not have worked -- the
weight picker auto-selects the only row matching the weight and writes its
id on the next save, so that column says only whether the form was ever
opened. The one case that cannot be told apart is stated rather than
hidden: someone who moved an RFID roll onto a genuine High Temp spool and
set 216 g by hand is normalised with the rest. Runs exactly once, so a
tare set afterwards is kept.
|
||
|
|
87e0a4c3b6 |
Name a spool by its subtype on the slot it is assigned to
A spool's subtype is half of what it is called: "PLA" and "PLA Wood" are
different filaments. The AMS slot's hover card built the assigned-spool
line out of brand, material and colour name and left the subtype out, so
a roll of Bambu PLA Wood Classic Birch in an H2C's A4 was announced as
"Bambu Lab PLA - Classic Birch".
Everything else named it correctly at the same moment -- the RFID read,
the inventory row, the slot's own profile line, which is built from the
spool's slicer preset rather than reassembled, and Bambu Studio -- so the
one wrong line read like a bad tag read rather than a display fault.
It was not only the render. The card's assignedSpool prop had no subtype
field at all, and the six places the printer card fills it in -- regular
AMS, AMS-HT and external spool, each in both Spoolman and internal-
inventory mode -- never passed one, so the value could not reach the
component. The field is required rather than optional, which is what
stops the next call site from quietly omitting it; that omission is the
whole of this bug.
Three more surfaces rebuilt the name the same way and are fixed with it:
the SpoolBuddy AMS slot panel in both inventory modes, and the write-tag
confirmation. Every other place a spool is named -- the assign dialogs,
the inventory cards, the forecast rows, the label picker -- already
included the subtype, so these four were the outliers.
This is the display-side half of #2902, which stopped the backend
reducing a filled or foamed filament onto its base material. The card was
doing the same thing to the same spools, one layer further out.
|
||
|
|
7f8d79fe50 |
Split the Windows installer build so signing can wait for approval
The SignPath Foundation production certificate does not sign on demand the way the self-signed test certificate does. Every request has to be approved by hand in the SignPath UI, because the Foundation verifies what is being signed and which build produced it. The submitting action waits for that approval with a default timeout of 600 seconds, which is ample while the test policy approves in seconds and far too short once the wait is a person noticing a tag went out. A tag pushed at night would have failed the run ten minutes later with the installer already compiled and thrown away. The compile now ends in its own job, which uploads the unsigned artifact and exposes its id. A second job downloads it, signs it, and does the release-facing work, with the wait raised to an hour and the job timeout sized to sit outside it. Separating them is what buys the recovery: the artifact is uploaded before the wait begins and is addressed by id, so a missed approval window costs a re-run of the second job alone rather than a rebuild. Raising the timeout in place would not have given that. The second job runs for unsigned builds too. Daily prereleases are deliberately left unsigned to preserve the signing quota, and gating the whole job on the signing decision would have meant a second copy of the alias, artifact and release steps for them to run through. The decision itself moves into a named step that echoes it, so a tag that came out unsigned can be explained from the run log rather than by re-reading the expression. It is one source of truth feeding both jobs, which a job-level env could not be. Every step body is otherwise unchanged. The property worth keeping is that none of the alias, upload and release-attach steps carry always(), so GitHub skips all three when signing fails or times out and an unsigned .exe cannot reach a release; that is now written next to them, because it is easy to break by adding a condition without noticing. The policy slug stays at test-signing and the signature check stays lenient -- the test certificate is self-signed and reports UnknownError, so requiring Valid would fail every run until the production certificate is imported. Both are the cutover. The restructure behaves identically under the test policy, the request simply completing at once instead of waiting, so it can be proven green beforehand. |
||
|
|
1d011ecf60 | Updated BACKERS | ||
|
|
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. |
||
|
|
0f3063aa1a | Point the Watchtower recommendation at the maintained fork (issue #2917) | ||
|
|
20bf108e55 | Updated BACKERS | ||
|
|
ed85677913 | Light the generated thumbnails so one model differs from another (#2816) (#2861) | ||
|
|
68140747f2 | Post work PR #2853 | ||
|
|
55cc64c87d | Add printer video downloads and range selection (#2853) |