mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
8cce10b60a86bccfd159d9eb6c8db290dcc431eb
4180
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8cce10b60a |
Skip the manual K calibration the way the automatic one is skipped
Bambuddy already recognises the printer's automatic pressure-advance run,
auto_pa_line_calib_mode, and leaves no archive and sends no notification
for it. Started by hand rather than automatically before a print, the
same calibration reports under a different name: it prints a pattern
where the automatic one prints a line, and carries no auto_ prefix, so
pa_pattern_calib_mode matched nothing.
It arrives exactly the way the automatic one does -- a bare subtask name
with no /usr/ path -- so it hit the same outcome the module was written
to prevent: an FTP sweep for a 3MF that cannot exist, six candidate names
across five directories with retries, and then a no-3MF archive named
after the calibration, on a printer in the middle of calibrating.
One entry on INTERNAL_JOB_NAMES covers it. That set is the single place
both the print-start and print-complete callbacks consult, so archiving,
the 3MF sweep and the notifications are all handled by the one line.
Matching stays exact after normalising path, suffix and case. The
negative cases are extended alongside the positive ones, so a file
somebody deliberately named pa_pattern_calib_mode_v2.3mf is still
archived as the print it is.
The manual PA *line* method, if it reports its own name, is not covered
here -- the list is deliberately limited to names that have actually been
observed rather than ones that seem likely.
|
||
|
|
9b83e8eba4 |
Send AMS tray colours as uppercase hex (issue #2987)
Assigning a spool to an AMS slot unassigned it again seconds later, and
the slot's colour changed at the same time. It presented as Bambu Studio
and Bambuddy fighting over the slot. The reporter's log shows Bambuddy
losing to itself.
P1S firmware 01.10.00.00 reads every lowercase hex letter in an AMS
tray_color as a zero, and hides it completely: the command response
echoes back the value that was sent and reports result "success", so
only the next AMS push says what was really stored. The spool-assign
path sent spool.rgba verbatim and that column stores lowercase. From the
bundle:
sent 09ff00ff -> AMS reports 09000000
sent ff5100ff -> AMS reports 00510000
sent 090000FF -> AMS reports 090000FF
That is the visible colour change, and it is also what deleted the
assignment. The auto-unlink sweep asks whether the slot still matches
the spool assigned to it; the mangled colour no longer did, so the
assignment Bambuddy had made four seconds earlier was removed.
colors_similar('09000000', '09FF00FF') is False, which is the whole of
it.
Re-assigning could not recover, because the Configure Slot dialog seeds
its colour from whatever the printer currently reports. It wrote the
mangled colour back and cemented it, which is the loop the report
describes in its steps 4 and 5.
Colours are now uppercased where the command is assembled rather than in
each of the four routes that configure a slot. A caller that forgets is
exactly how this arrived. Nothing else changes: no padding, no invented
alpha, no six-to-eight widening, and tray_type and tray_sub_brands keep
their case, where it carries meaning -- "PLA Matte" is a product line,
"PLA MATTE" is not.
Two paths deliberately left alone. The developer-mode probe re-sends the
colour the printer itself just reported so that the probe is inert;
uppercasing there would turn it into a write. And the Virtual Printer
forwards the slicer's own command verbatim -- Studio could in principle
hit the same firmware bug, but nothing here evidences that it sends
lowercase, and rewriting a slicer payload inside a transparent proxy is
not a change to make on a hunch.
Two more defects from the same log.
A spool with a brand and no subtype was configured with the string
"None" in its name: the branded branch interpolated spool.subtype
without checking it while the unbranded branch guarded it, so
"Sunlu PLA Matte None" went on the wire and into Studio's display.
And the FTP log is readable again. A 426 whose bytes Bambuddy has
already verified against the printer is how Bambu FTPS normally ends a
transfer, not a fault, so it drops from WARNING to INFO. It fired 54
times in this one bundle, every one followed by a completed upload, and
it was burying the 26 TLS handshake failures in the same log that
actually cost the reporter two prints. A 426 whose bytes do not verify
is still an error and still fails the upload.
The handshake failures themselves are printer-side FTPS cool-off under
load and are not touched here.
|
||
|
|
450f9a6f20 |
Derive the chamber target from the trays the print loads (issue #2886)
Preheat took the maximum chamber target across every loaded AMS tray,
with no reference to the job. The reporter's P2S holds PETG Pro, PLA,
ASA and PETG; the ASA row of the filament map says 45C, so a PLA-only
plate was dispatched with chamber_target=45C, the bed driven to 90C to
reach it, and the full 900s max-wait plus 300s soak burned before the
upload started. Every time -- a P2S has no chamber heater and its
chamber tops out around 33C, so the wait can only ever end on the
timeout. Their log carries fifteen of these.
The intent was never in doubt. The resolution order documented one
screen above _derive_chamber_target reads "PLA-only print derives 0 ->
chamber phase auto-skips", but it was implemented as PLA-only AMS
rather than PLA-only print, and only misfires on a mixed load.
The derivation now reads the trays the item's ams_mapping names -- the
same array the print command puts on the wire, [-1, -1, -1, 1] in their
case, addressing exactly the PLA slot -- so the ASA two slots over
contributes nothing and the stage skips outright. Multi-material prints
are unaffected: the maximum is still taken, across the trays the plate
actually loads, so an ASA the print does use is still binding.
An item whose mapping is missing or still unresolved keeps the
whole-unit scan. That is the only signal left, and narrowing to nothing
would disable preheat for prints that need it -- the failure mode worth
avoiding here is the silent one.
The bed hold between jobs is gated on the same derivation and was
holding beds at 90C for the same wrong reason. It now reads the next
item's mapping too, where that item has one.
The external spool is no longer invisible to this. The scan only ever
looked at raw_data['ams'], so an ASA print fed from the external feed
derived 0 and got no preheat at all; a mapping naming 254/255 is now
honoured. An item with no mapping still derives from the AMS alone, so
nothing starts preheating that did not before.
Tray addressing matches _build_loaded_filaments, which is what produced
the ids in the mapping being read back: ams_id * 4 + tray_id, the bare
unit id for an AMS-HT from 128, and the firmware's own vt_tray id for
an external feed. Ids are coerced because this firmware reports them as
strings.
|
||
|
|
21fd476668 |
Ask Spoolman which extra fields it has, once (issue #2983)
Bambuddy checked whether one of its four custom spool fields existed with
GET /field/spool/{name}. Spoolman has never served that. Its API declares
only POST and DELETE at that path -- confirmed against the live server's
own OpenAPI document -- so the probe answered 405 Method Not Allowed
every time and the check could not succeed on any version.
Every call therefore fell through to POST /field/spool/{name}, and that
endpoint is an upsert rather than a create. It answers 200 whether or not
the field is already there, so a field the user had renamed, retyped or
given a default to in Spoolman's own UI was reset to Bambuddy's version
of it, and an untrue "Created Spoolman extra field" was logged beside it.
The reporter's log carried 60 of those lines over three days -- once per
field per client init, which is every restart and every settings save.
Existence now comes from GET /field/spool, the listing endpoint, matched
on each row's `key`. Matching on `key` rather than the display `name` is
the part that fixes the overwrite: a renamed field is the same field, and
reading it as a missing one is what re-created it. A field that already
exists is now left completely alone.
The listing is read once per client and banked, so registering all four
fields costs one request instead of four, and a client that has already
looked makes none at all. Only a successful read is banked -- a client
that could not reach the listing asks again for the next field it has not
seen, so one transient failure does not leave it posting blind, and
overwriting, for the rest of its life.
An unreadable listing still falls back to attempting the POST. Registration
is best-effort by contract: it must not turn a write that might still
succeed into one that never happens, so an unexpected Spoolman build is no
worse off than before.
Measured against Spoolman 0.23.1: a field renamed to "Bambu RFID Tag"
survives a full registration pass that previously reset it, the pass makes
one GET and one POST for the single genuinely-missing field where it used
to make four POSTs, and a second pass on the same client makes no requests
at all.
The fake in test_spoolman_extra_field_registration_2903 modelled the
per-field path as a working probe, which is the assumption this bug was
built on; it now answers 405 as the real server does, and its assertions
follow the listing. 18 new tests cover the rest, 10 of which fail against
the old code.
|
||
|
|
112e58e5b9 |
Match slicer presets on what they declare, not what they are named (issue #2982)
The internal slicer picked PETG for a PLA plate and an A1 process for a
P1S. Both come from the sidecar's bundled-profile listing, fixed in the
sidecar repo; this is the consuming half plus the hardening that keeps an
older sidecar degrading rather than breaking.
Standard-tier presets now carry the compatible_printers the sidecar
reports. That list is the only truthful account of which printer a preset
belongs to, because the bundle ships no process preset named after a P1S,
an X1, an X1E or an H2D Pro -- all ten of the P1S's are named "@BBL X1C"
and name the P1S only in that list. Reading the printer out of the preset
NAME therefore made a P1S look like it had no compatible process at all:
all 198 hid behind "Show all" and the auto-pick fell through to an
alphabetically-first 0.06mm Fine @BBL A1 0.2 nozzle the CLI refused. A
P1S now gets 0.20mm Standard @BBL X1C and 73 filaments instead of 4. An
older sidecar reports nothing here, which leaves the name matcher in
place -- degraded as before, not broken.
Material is now a hard partition in the filament pre-pick rather than a
+10 bonus. A preset stating a different material than the plate asks for
is the wrong preset, not a worse one: wrong nozzle temperature, wrong bed
temperature, wrong flow. A preset stating NO material stays eligible --
unknown is not wrong, and 32 shipped profiles genuinely have none. The
same rule reaches the retain path, which held a slot on
printer-compatibility alone and so cemented a wrong-material pick through
every re-pick. A preset the user chose themselves is exempt: printing
PETG on a plate a designer labelled PLA is a legitimate thing to do, and
this rule exists to correct the auto-pick, not to overrule the user.
Two more, both found while tracing this and neither reported:
Among process presets equally valid for the selected printer, the one
nearest a 0.2mm layer height now wins. Within a tier the list is
alphabetical and Bambu's naming puts the finest height first, so every
slice that did not name its own process silently got 0.08mm Extra Fine on
an X1 Carbon and 0.06mm Fine on an A1 mini -- correct presets, nobody's
default. Ties break toward the coarser, faster height; a name with no
readable height is still pickable when it is the only candidate; a
process the 3MF named still wins outright.
H2DP is aliased to H2D Pro, the same shape as the A1M rename in #1649 --
the bundle spells the model one way in preset names and another in the
printer preset, so an H2D Pro classified all 198 processes as another
printer's. Deliberately narrow: H2DP and a plain H2D are different
machines and must not collapse.
A dropdown the printer filter would empty now shows the unfiltered list
instead. That state was reachable for four printer models and told the
user nothing; a visible preset for the wrong printer can be changed, an
empty dropdown cannot.
Verified against live Orca 2.4.2 and BambuStudio 02.08.02.61 sidecars
over the real 1156- and 1792-profile trees: every one of the eight
printer models tested now auto-picks a 0.20mm process for its own
printer, a PLA plate draws a PLA preset and a PETG plate a PETG one.
Each change was confirmed to fail its tests when reverted.
|
||
|
|
26a827ea94 |
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.
|
||
|
|
2d0eca9354 |
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.
|
||
|
|
b2dda1fa5f |
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.
|
||
|
|
659d77c205 |
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.
|
||
|
|
bccfae82b9 |
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.
|
||
|
|
66f7558532 | Updated README | ||
|
|
012d4df3a5 |
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.
|
||
|
|
c45096cdaf |
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.
|
||
|
|
2253afbf45 |
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.
|
||
|
|
4705a3027a |
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.
|
||
|
|
56f26cb013 |
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.
|
||
|
|
c6c732ea39 |
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.
|
||
|
|
03d8310cf0 |
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.
|
||
|
|
a4a1f4c58b |
Skip the manual K calibration the way the automatic one is skipped
Bambuddy already recognises the printer's automatic pressure-advance run, auto_pa_line_calib_mode, and leaves no archive and sends no notification for it. Started by hand rather than automatically before a print, the same calibration reports under a different name: it prints a pattern where the automatic one prints a line, and carries no auto_ prefix, so pa_pattern_calib_mode matched nothing. It arrives exactly the way the automatic one does -- a bare subtask name with no /usr/ path -- so it hit the same outcome the module was written to prevent: an FTP sweep for a 3MF that cannot exist, six candidate names across five directories with retries, and then a no-3MF archive named after the calibration, on a printer in the middle of calibrating. One entry on INTERNAL_JOB_NAMES covers it. That set is the single place both the print-start and print-complete callbacks consult, so archiving, the 3MF sweep and the notifications are all handled by the one line. Matching stays exact after normalising path, suffix and case. The negative cases are extended alongside the positive ones, so a file somebody deliberately named pa_pattern_calib_mode_v2.3mf is still archived as the print it is. The manual PA *line* method, if it reports its own name, is not covered here -- the list is deliberately limited to names that have actually been observed rather than ones that seem likely. |
||
|
|
0d21239e18 |
Send AMS tray colours as uppercase hex (issue #2987)
Assigning a spool to an AMS slot unassigned it again seconds later, and
the slot's colour changed at the same time. It presented as Bambu Studio
and Bambuddy fighting over the slot. The reporter's log shows Bambuddy
losing to itself.
P1S firmware 01.10.00.00 reads every lowercase hex letter in an AMS
tray_color as a zero, and hides it completely: the command response
echoes back the value that was sent and reports result "success", so
only the next AMS push says what was really stored. The spool-assign
path sent spool.rgba verbatim and that column stores lowercase. From the
bundle:
sent 09ff00ff -> AMS reports 09000000
sent ff5100ff -> AMS reports 00510000
sent 090000FF -> AMS reports 090000FF
That is the visible colour change, and it is also what deleted the
assignment. The auto-unlink sweep asks whether the slot still matches
the spool assigned to it; the mangled colour no longer did, so the
assignment Bambuddy had made four seconds earlier was removed.
colors_similar('09000000', '09FF00FF') is False, which is the whole of
it.
Re-assigning could not recover, because the Configure Slot dialog seeds
its colour from whatever the printer currently reports. It wrote the
mangled colour back and cemented it, which is the loop the report
describes in its steps 4 and 5.
Colours are now uppercased where the command is assembled rather than in
each of the four routes that configure a slot. A caller that forgets is
exactly how this arrived. Nothing else changes: no padding, no invented
alpha, no six-to-eight widening, and tray_type and tray_sub_brands keep
their case, where it carries meaning -- "PLA Matte" is a product line,
"PLA MATTE" is not.
Two paths deliberately left alone. The developer-mode probe re-sends the
colour the printer itself just reported so that the probe is inert;
uppercasing there would turn it into a write. And the Virtual Printer
forwards the slicer's own command verbatim -- Studio could in principle
hit the same firmware bug, but nothing here evidences that it sends
lowercase, and rewriting a slicer payload inside a transparent proxy is
not a change to make on a hunch.
Two more defects from the same log.
A spool with a brand and no subtype was configured with the string
"None" in its name: the branded branch interpolated spool.subtype
without checking it while the unbranded branch guarded it, so
"Sunlu PLA Matte None" went on the wire and into Studio's display.
And the FTP log is readable again. A 426 whose bytes Bambuddy has
already verified against the printer is how Bambu FTPS normally ends a
transfer, not a fault, so it drops from WARNING to INFO. It fired 54
times in this one bundle, every one followed by a completed upload, and
it was burying the 26 TLS handshake failures in the same log that
actually cost the reporter two prints. A 426 whose bytes do not verify
is still an error and still fails the upload.
The handshake failures themselves are printer-side FTPS cool-off under
load and are not touched here.
|
||
|
|
28d386f766 |
Derive the chamber target from the trays the print loads (issue #2886)
Preheat took the maximum chamber target across every loaded AMS tray, with no reference to the job. The reporter's P2S holds PETG Pro, PLA, ASA and PETG; the ASA row of the filament map says 45C, so a PLA-only plate was dispatched with chamber_target=45C, the bed driven to 90C to reach it, and the full 900s max-wait plus 300s soak burned before the upload started. Every time -- a P2S has no chamber heater and its chamber tops out around 33C, so the wait can only ever end on the timeout. Their log carries fifteen of these. The intent was never in doubt. The resolution order documented one screen above _derive_chamber_target reads "PLA-only print derives 0 -> chamber phase auto-skips", but it was implemented as PLA-only AMS rather than PLA-only print, and only misfires on a mixed load. The derivation now reads the trays the item's ams_mapping names -- the same array the print command puts on the wire, [-1, -1, -1, 1] in their case, addressing exactly the PLA slot -- so the ASA two slots over contributes nothing and the stage skips outright. Multi-material prints are unaffected: the maximum is still taken, across the trays the plate actually loads, so an ASA the print does use is still binding. An item whose mapping is missing or still unresolved keeps the whole-unit scan. That is the only signal left, and narrowing to nothing would disable preheat for prints that need it -- the failure mode worth avoiding here is the silent one. The bed hold between jobs is gated on the same derivation and was holding beds at 90C for the same wrong reason. It now reads the next item's mapping too, where that item has one. The external spool is no longer invisible to this. The scan only ever looked at raw_data['ams'], so an ASA print fed from the external feed derived 0 and got no preheat at all; a mapping naming 254/255 is now honoured. An item with no mapping still derives from the AMS alone, so nothing starts preheating that did not before. Tray addressing matches _build_loaded_filaments, which is what produced the ids in the mapping being read back: ams_id * 4 + tray_id, the bare unit id for an AMS-HT from 128, and the firmware's own vt_tray id for an external feed. Ids are coerced because this firmware reports them as strings. |
||
|
|
4d2c6debf7 |
Ask Spoolman which extra fields it has, once (issue #2983)
Bambuddy checked whether one of its four custom spool fields existed with
GET /field/spool/{name}. Spoolman has never served that. Its API declares
only POST and DELETE at that path -- confirmed against the live server's
own OpenAPI document -- so the probe answered 405 Method Not Allowed
every time and the check could not succeed on any version.
Every call therefore fell through to POST /field/spool/{name}, and that
endpoint is an upsert rather than a create. It answers 200 whether or not
the field is already there, so a field the user had renamed, retyped or
given a default to in Spoolman's own UI was reset to Bambuddy's version
of it, and an untrue "Created Spoolman extra field" was logged beside it.
The reporter's log carried 60 of those lines over three days -- once per
field per client init, which is every restart and every settings save.
Existence now comes from GET /field/spool, the listing endpoint, matched
on each row's `key`. Matching on `key` rather than the display `name` is
the part that fixes the overwrite: a renamed field is the same field, and
reading it as a missing one is what re-created it. A field that already
exists is now left completely alone.
The listing is read once per client and banked, so registering all four
fields costs one request instead of four, and a client that has already
looked makes none at all. Only a successful read is banked -- a client
that could not reach the listing asks again for the next field it has not
seen, so one transient failure does not leave it posting blind, and
overwriting, for the rest of its life.
An unreadable listing still falls back to attempting the POST. Registration
is best-effort by contract: it must not turn a write that might still
succeed into one that never happens, so an unexpected Spoolman build is no
worse off than before.
Measured against Spoolman 0.23.1: a field renamed to "Bambu RFID Tag"
survives a full registration pass that previously reset it, the pass makes
one GET and one POST for the single genuinely-missing field where it used
to make four POSTs, and a second pass on the same client makes no requests
at all.
The fake in test_spoolman_extra_field_registration_2903 modelled the
per-field path as a working probe, which is the assumption this bug was
built on; it now answers 405 as the real server does, and its assertions
follow the listing. 18 new tests cover the rest, 10 of which fail against
the old code.
|
||
|
|
e9daa2124e |
Match slicer presets on what they declare, not what they are named (issue #2982)
The internal slicer picked PETG for a PLA plate and an A1 process for a P1S. Both come from the sidecar's bundled-profile listing, fixed in the sidecar repo; this is the consuming half plus the hardening that keeps an older sidecar degrading rather than breaking. Standard-tier presets now carry the compatible_printers the sidecar reports. That list is the only truthful account of which printer a preset belongs to, because the bundle ships no process preset named after a P1S, an X1, an X1E or an H2D Pro -- all ten of the P1S's are named "@BBL X1C" and name the P1S only in that list. Reading the printer out of the preset NAME therefore made a P1S look like it had no compatible process at all: all 198 hid behind "Show all" and the auto-pick fell through to an alphabetically-first 0.06mm Fine @BBL A1 0.2 nozzle the CLI refused. A P1S now gets 0.20mm Standard @BBL X1C and 73 filaments instead of 4. An older sidecar reports nothing here, which leaves the name matcher in place -- degraded as before, not broken. Material is now a hard partition in the filament pre-pick rather than a +10 bonus. A preset stating a different material than the plate asks for is the wrong preset, not a worse one: wrong nozzle temperature, wrong bed temperature, wrong flow. A preset stating NO material stays eligible -- unknown is not wrong, and 32 shipped profiles genuinely have none. The same rule reaches the retain path, which held a slot on printer-compatibility alone and so cemented a wrong-material pick through every re-pick. A preset the user chose themselves is exempt: printing PETG on a plate a designer labelled PLA is a legitimate thing to do, and this rule exists to correct the auto-pick, not to overrule the user. Two more, both found while tracing this and neither reported: Among process presets equally valid for the selected printer, the one nearest a 0.2mm layer height now wins. Within a tier the list is alphabetical and Bambu's naming puts the finest height first, so every slice that did not name its own process silently got 0.08mm Extra Fine on an X1 Carbon and 0.06mm Fine on an A1 mini -- correct presets, nobody's default. Ties break toward the coarser, faster height; a name with no readable height is still pickable when it is the only candidate; a process the 3MF named still wins outright. H2DP is aliased to H2D Pro, the same shape as the A1M rename in #1649 -- the bundle spells the model one way in preset names and another in the printer preset, so an H2D Pro classified all 198 processes as another printer's. Deliberately narrow: H2DP and a plain H2D are different machines and must not collapse. A dropdown the printer filter would empty now shows the unfiltered list instead. That state was reachable for four printer models and told the user nothing; a visible preset for the wrong printer can be changed, an empty dropdown cannot. Verified against live Orca 2.4.2 and BambuStudio 02.08.02.61 sidecars over the real 1156- and 1792-profile trees: every one of the eight printer models tested now auto-picks a 0.20mm process for its own printer, a PLA plate draws a PLA preset and a PETG plate a PETG one. Each change was confirmed to fail its tests when reverted. |
||
|
|
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. |
||
|
|
ca0aa9952f | Updated CHANGELOG | ||
|
|
468c52b0fa |
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.
|
||
|
|
72b097362f | Allow Avery label sheets to start at an unused position (#2879) (#2918) | ||
|
|
eee94ce9c8 |
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.
|
||
|
|
90d66b7bba |
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.
|
||
|
|
f9372607e8 |
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. |
||
|
|
3bc96254ef |
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.
|
||
|
|
3b29543f8d | Give the AMS temperature alarm its own threshold (issue #2905) (#2943) | ||
|
|
93585beb7b | Let a clear spool stay clear on the way to Spoolman (issue #2912) (#2924) | ||
|
|
8f182dc181 |
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.
|
||
|
|
920c6d1549 |
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.
|
||
|
|
1b2ebe0577 |
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.
|
||
|
|
f6e8767aeb |
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.
|