mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 14:11:35 +02:00
20d0f49ecd5ff7fe7c127a6ff4faafef924ad032
1976
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e5b8e76ea1 |
Recognise our own print dispatch instead of guessing at a magic number
(#2843 follow-up) Bambuddy records the project_file behind every print, because that command names where the sliced file was put and so decides whether the archive can have a thumbnail and slicer metadata at all. Its own dispatches were told apart from a slicer's by testing sequence_id against "20000", on the belief that 20000 was Bambuddy's alone. It never was. 20000 is the slicer convention Bambuddy copied -- virtual_printer/bind_server documents the slicer sending exactly that during detect -- and both slicers count up from it. Measured on the wire: OrcaSlicer dispatched 20000 and then 20001, BambuStudio 20009 and 20010. So whichever dispatch happened to land on the shared value was filed as ours and never recorded, and after a slicer restart that is the first print you send. The test was wrong in the other direction too: it called every value above 20000 external, including ones we had sent ourselves, which only stayed invisible because we always send exactly 20000. Ownership is now established by remembering the job actually dispatched -- sequence id, file, url and subtask name -- and consuming that marker on the echo. One-shot deliberately: a slicer reprint of the same file a moment later is somebody else's print and must not hide behind our last one. Nothing about printing or archiving changes. current_project_url and ams_mapping are both captured before this branch and always were, so the storage verdict that gates the FTPS sweep is untouched; two tests pin that, because it is the part that would actually cost archives if it drifted. What changes is that the diagnostic entry stops lying, and it is the entry that tells an operator whether their printer stores files somewhere Bambuddy can read -- which is the whole subject of #2780 and would have to count, and an undercount there would have been silent. |
||
|
|
2b38cff16d |
Stop a print with no 3MF borrowing another model's data (#2843)
H2-series and P2S firmware keeps a slicer-sent file on internal eMMC.
Port 990 serves external storage only, so there is no file to fetch, and
the print becomes an archive with no 3MF -- the ordinary outcome for
anyone who sends from Bambu Studio rather than through Bambuddy.
Confirmed on the maintainer's own machines: an H2C and an H2D both
dispatched brtc://emmc for the same model one minute apart, while an X1C
sent ftp:// for it. Three separate defects live in what happens next.
The first is the serious one. An archive with no 3MF keeps the path the
printer is executing as its filename, and on a sliced job that is always
Metadata/plate_1.gcode. The fallback that looks for the same model in the
Library or among earlier prints took its search term from there, so it
searched for `plate_1` -- a name every Bambu print in existence has --
and matched on a substring, so it also matched any name merely ending
that way. On the H2D a 1.6 g Cube resolved to lid_plate_1.gcode.3mf and
was costed at 207 g across three real spools. It was not confined to
plate names either: in the same database `Bank.3mf` matched "Piggo the
piggy bank", and `x1c.gcode.3mf` matched "slice-test-x1c". The matcher
now takes the model name the printer reports when the filename is only a
plate path, refuses a bare plate stem rather than searching for it, and
anchors to a whole filename with LIKE metacharacters escaped, because `_`
is a wildcard and model names are full of them. A print that cannot be
identified is now left untracked, which is the honest answer -- the
previous behaviour was to charge the operator's spools for a model they
had not printed.
Checked against every row rather than argued from the code. Across 273
library stems the result sets are identical. Across 241 archive stems 14
differ, all of them strictly narrower, and every dropped match is one of
the false positives above; all 233 archives still match their own
filename, so no legitimate donor was lost. Of the eight no-3MF archives
on that install the old matcher picked a wrong donor for two -- one of
them a calibration run that would have been charged the 207 g -- and the
new one picks none.
The second defect is that those archives could not receive a timelapse at
all. attach_timelapse derived its destination from the missing file's
path, and (base_dir / "").parent is the parent of base_dir, one level
outside the data directory. In Docker that is /app, so every attempt
failed EACCES and the scan retried and discarded the video 25 times over
twelve minutes, roughly a hundred FTPS connections for bytes that had
already downloaded successfully. Where that location happened to be
writable it was worse: the file landed beside the installation and the
attach then failed anyway, because the path could not be made relative to
base_dir. #1820 introduced a shared helper precisely so these derivations
could not drift apart, and this was the one site still doing it by hand.
The directory is created only after the filename has passed the traversal
check, so a rejected name still leaves nothing behind.
The third is silence. When a print's filament cannot be read from a 3MF,
the remaining-percentage delta is the fallback, and that needs a reading
at print start -- which a spool without RFID does not have until someone
sets a remaining amount by hand. Those slots were skipped with a bare
continue. Every other reason for skipping a slot in that loop is logged,
and the comment a few lines below argues the case explicitly: charging
nothing silently is indistinguishable from having nothing to charge. It
now says so, for slots the print actually used.
Four existing tests needed updating rather than the production path.
They patch backend.app.services.archive.settings by name, and the shared
helper reads its own module-level binding, so they kept the real data
directory and wrote outside tmp_path -- which is how the first draft of
this change littered a working tree. They now patch both bindings.
|
||
|
|
f855d8dcca |
Pin the PostgreSQL session to UTC so defaulted timestamps are UTC (#2855)
On a UTC+3 install every AMS humidity reading and every archive was
stamped three hours ahead of when it happened. Bambuddy stores naive
timestamps that hold UTC and the frontend's parseUTCDate reads an
offsetless timestamp as UTC, so the display added the offset to a value
that was already local.
The Python side has honoured that contract since #504. The reporter's
timestamps were not written by Python. Around ninety-six columns take
their value from server_default=func.now() and the migration DDL carries
another forty-nine on DEFAULT CURRENT_TIMESTAMP -- the database fills
those, and on PostgreSQL now() is a timestamptz, so storing it into a
timestamp without time zone casts it through the session TimeZone. A
Postgres container started with TZ=Europe/Istanbul bakes that zone into
postgresql.conf at initdb, and every defaulted column then receives local
wall-clock. recorded_at is the clearest case: nothing in the codebase
ever assigns it, so its value is entirely whatever the database decided.
Connections now carry timezone=UTC, which makes the cast a no-op whatever
the server is set to. Measured through the real engine factory against a
live PostgreSQL, a session on the reporter's configuration stored +10800s
and the fixed one +0s. Pinning the session was preferred over a hundred
and forty-five individual edits partly for its size but mostly because
half of those sites are raw DDL that no model-level change can reach.
SQLite needed nothing and gets nothing: its CURRENT_TIMESTAMP is UTC by
definition and it has no session timezone to get wrong, which is why this
survived two years of timezone fixes without showing itself. That also
makes it the reference -- the change moves Postgres onto SQLite's
behaviour rather than introducing a third convention -- so the SQLite
behaviour is now pinned by a test instead of being assumed. asyncpg is
the documented driver and takes the setting in its startup packet; any
other Postgres driver gets the same setting the libpq way, so a psycopg
URL does not fail at connect on a keyword asyncpg alone accepts.
Rows already written are deliberately left alone. The inverse cast is
computable and DST-correct, but it cannot be applied safely: created_at
is assigned explicitly on some paths and defaulted on others, an install
that began on SQLite holds correct and shifted rows side by side, and
nothing distinguishes them after the fact. Timestamps are right from the
upgrade forward and history keeps the times it was given.
One related mismatch goes with it, because fixing the database side alone
would have made it start lying on exactly the installs this repairs. The
support package's oldest_pending_age_seconds subtracted a naive local
clock from a naive UTC column, with a comment claiming it was UTC; on the
reporter's install the two errors cancelled. It reported a job queued
five minutes ago as three hours old east of Greenwich and a negative age
west of it. The two AMS and printer-sensor retention cutoffs move to the
same utcnow_naive helper -- correct in value already, but deprecated in
3.12 and emitting warnings on every sweep.
|
||
|
|
019fb1c8af |
Track which nozzle each AMS feeds through the Filament Track Switch
With a switch fitted, an AMS is not wired to a nozzle any more. It is
plumbed into one of the switch's two inlets and reaches both nozzles
through it, so every unit reports its extruder as "not fixed" (0xE) and
ams_extruder_map comes back empty. Bambuddy had nothing to fall back on
but the AMS unit number, so AMS-A was badged R and AMS-B was badged L
purely because their unit ids are 0 and 1, a third unit got no badge at
all, and every one of those labels was wrong.
The binding needed no new telemetry. BambuStudio reads it out of bits
24-27 of the same AMS info string we already parse for the type and the
extruder id -- 0 is In-B, 1 is In-A -- and it only means anything when a
switch is installed, because without one 0xE really does mean an
uninitialised unit. That gates the read, which forced the switch block to
be parsed before the AMS block: _handle_ams_data runs early in
_process_message and _update_state only much later, so the binding was
lost on every frame that carried both.
The badge letters stay L and R, In-A reading as L, with the inlet named
in full in the tooltip -- the letter is the inlet's position and not a
claim about which nozzle that AMS feeds, since the switch can route
either inlet to either outlet. Both views update live now. The switch
fields were missing from the WebSocket payload, and the frontend
shallow-merges each push over its cached status, so an absent field kept
whatever the last full fetch left behind. The broadcast dedup key had no
term for them either, so "Join IN-B" on the printer screen moved nothing:
the binding is not in the tray component of that key, and not in the AMS
change-hash, which covers tray fields only and must stay that way because
it drives Spoolman sync.
The rest of this is the calibration half, which is where it actually
bites. K-profiles are calibrated per nozzle and the printer numbers its
calibration table per nozzle too, so entry 16 exists on both hotends and
means a different profile on each. A tray holds exactly one index. Move
an AMS to the other inlet and every configured slot in it silently keeps
pointing at the old hotend's table -- measured on the maintainer's H2C, a
black PLA calibrated 0.018 left and 0.020 right stayed on the left
profile after the move, and a manual RFID re-read only re-asserted the
same wrong one. Bambu has not solved this either; their AMS dialog
carries "TODO: fila_switcher broken the connection of ams->extruder"
above the line that decides which nozzle's profiles to offer.
Three copies of "which extruder is this slot on" each ended in else 0,
which on a switch machine filed every profile under the right-hand
nozzle. They now share one resolver that returns None for "unknown",
because unknown and extruder 0 are very different answers on a
dual-nozzle machine and conflating them is what bound a left-nozzle
profile to a slot sitting on the right. The per-slot K value on the
printer card had the same confusion from the other direction: its lookup
was keyed on cali_idx alone, so one nozzle's K silently overwrote the
other's.
Moving an AMS now re-selects each configured slot's counterpart profile
for the nozzle it has arrived on. Only the calibration binding moves, and
only for slots whose spool already has a profile for that nozzle:
configuring a slot is a deliberate preparation step, so a slot we know
nothing about, or a spool calibrated on one hotend only, is left exactly
as the operator set it. Nothing fires on the first sighting of a binding
either, since every reconnect learns them afresh and re-applying there
would overwrite a choice made by hand.
Configure Slot resolves against the slot's own nozzle throughout. Option
identity carries the extruder, so a filament calibrated on both hotends
gives two distinguishable entries instead of two that collapse into
whichever the printer listed first; options name the hotend; matches are
scoped to the nozzle the slot feeds, with the other hotend's profiles
still reachable under Other K profiles; and the slot's active index is
resolved as a pair rather than followed into the wrong table.
Inlet to nozzle is one table, In-A to the left hotend and In-B to the
right, measured rather than assumed -- fila_switch.out cannot be used for
it, reporting [1, 1] unchanged across a 90-second capture, both outlets
claiming the same extruder. The print dialog picks up the same inlet
labelling in its slot dropdown, replacing a left/right hint that never
rendered because it matched snow-encoded values against global tray ids;
decoding it correctly would not have saved it, since the firmware never
reports which inlet is currently paired with which outlet. The dialog
also notes when every filament a print needs sits behind one inlet, which
is legal but slow -- a change between two filaments on the same inlet
retracts the outgoing spool all the way back to its AMS, where a change
across the two only retracts as far as the switch.
Assigning an AMS to an inlet remains printer-side. BambuStudio can read
that binding and has no command to write it, so there is no wire format
to copy.
|
||
|
|
a0778f931b |
Show which Filament Track Switch inlet each AMS feeds
With a switch fitted, an AMS is not wired to a nozzle any more. It is
plumbed into one of the switch's two inlets and reaches both nozzles
through it, so every unit reports its extruder as "not fixed" (0xE) and
ams_extruder_map comes back empty on these machines.
The printer card had nothing to fall back on but the AMS unit number, so
AMS-A was badged R and AMS-B was badged L purely because their unit ids
are 0 and 1, a third unit got no badge at all, and every one of those
labels was wrong. The SpoolBuddy assign modal had the same fallback in a
worse form, mapping anything that was not extruder 1 to R.
The binding turned out to need no new telemetry. BambuStudio reads it out
of bits 24-27 of the same AMS info string we already parse for the AMS
type and the extruder id -- 0 is In-B, 1 is In-A -- and it is only
meaningful when a switch is installed, because without one 0xE really
does mean an uninitialised unit and those bits carry nothing. That gates
the read, which in turn forced the switch block to be parsed before the
AMS block: _handle_ams_data runs early in _process_message and
_update_state only much later, so the binding was lost on every frame
that carried both. _parse_fila_switch is split out and called first, and
left in _update_state as well so that stays a complete absorb step.
The badge keeps L and R rather than A and B, because the lettering is
familiar and matches the physical layout. It is a different colour from
the plain nozzle badge, and its tooltip names the inlet in full, since
the letter is the inlet's position and not a claim about which nozzle
that AMS feeds -- the switch can route either inlet to either outlet. An
AMS still reporting a real extruder id keeps its ordinary badge, which
BambuStudio also treats as authoritative over any switch binding, and a
switch that has been fitted but not yet set up on the printer shows
nothing rather than a guess.
The print dialog's slot dropdown gets the same label. It replaces a
left/right hint that never once rendered: ftsExtruderForSlot compared
snow-encoded in[] values against global tray ids and could not match.
Decoding it correctly would not have saved it -- the firmware reports
which slot sits in each inlet and which nozzle each outlet feeds, but
never which inlet is currently paired with which outlet, so no per-slot
nozzle can be derived. That function is gone rather than fixed.
The dialog also points out when every filament a print needs sits behind
one inlet. Bambu's own guidance is that this is legal but slow: a change
between two filaments on the same inlet retracts the outgoing spool all
the way back to its AMS before the next can be fed up the shared tube,
where a change across the two inlets only retracts as far as the switch.
All on one inlet means every change in the job takes the slow path, and
moving a single spool fixes it. So it advises, it does not block.
Both views update live. Two things were stopping that. fila_switch and
ams_switch_inlet were absent from printer_state_to_dict, and the frontend
shallow-merges each WebSocket push over its cached status, so a field the
push omits keeps whatever the last full fetch left behind. And the
broadcast dedup key had no term for either, so "Join IN-B" on the printer
screen moved nothing: the binding is not in the tray component of that
key, and it is not in the AMS change-hash either, which covers tray
fields only and must stay that way because it drives Spoolman sync.
Assigning an AMS to an inlet remains printer-side. BambuStudio can read
the binding and has no command to write it -- its switch class is parse
and getters only, and the recommended-arrangement popup draws and
publishes nothing -- so there is no wire format for us to copy.
Adding the two fields to PrinterState broke four test modules whose
SimpleNamespace stubs predate them. The stubs are fixed rather than the
production reads made defensive: the real dataclass always carries both,
and a getattr in the dedup key would silently stop tracking the field if
it were ever renamed.
|
||
|
|
88feb69bc3 |
Stop auto K-profile calibration leaving an archive behind
With flow dynamics calibration on, the printer lays down a
pressure-advance line before the print itself and announces it over MQTT
through the same print-start event a real print uses. Bambuddy archived
it: a row named auto_pa_line_calib_mode marked as having no 3MF, sitting
in among the user's actual prints, with a Print Started and a Print
Completed notification for each one.
The printer's other internal jobs were already skipped, but only by the
/usr/ path they carry -- bed levelling reports as
/usr/etc/print/auto_cali_for_user.gcode. The pressure-advance line
carries no path at all. It arrives as a bare subtask name, so a rule
that only ever looked at the filename could not see it. Falling past the
guard it reached the no-3MF fallback, where print_name is subtask_name
or filename, and named the row after the calibration.
Internal jobs are now recognised by name as well as by path, from either
field, in one place both callbacks share. The match is exact after
normalising away the directory, one print-file suffix and case, rather
than a prefix or substring rule: "auto" and "calib" are ordinary words in
a user's own filenames, and a rule loose enough to catch some unnamed
future calibration would quietly swallow somebody's print.
The completion is suppressed too, and that half matters more than the
noise. With no archive to close, the completion falls into the
no-archive notification path -- which attributes an unmatched completion
to any queue item the printer finished in the last five minutes and
emails its owner. This calibration runs alongside a real print, so
silencing only the start would have told that print's owner their job
was done, early, and again for real later. The guard sits inside the
no-archive branch, so the plate-clear gate, the queue reconciliation and
the SD-card cleanup all still run; only the notification is skipped.
Skipping the run early also drops the FTP sweep that preceded the
fallback -- six candidate names across five directories with retries,
around a hundred connections looking for a file that cannot exist, aimed
at a printer that is in the middle of calibrating.
auto_cali_for_user no longer sends a Print Started notification either.
It is the same event about the same kind of job, and the archive was
never the only thing wrong with treating it as a user's print.
|
||
|
|
64a1defa7a |
Stop the AMS temperature alert firing for heat the user asked for (#1802)
The alert compares against ams_temp_fair, the same threshold that colours
the printer card, which defaults to 35C. Drying deliberately runs at 45C
for PLA, 65C for PETG and up to 85C on an AMS-HT, and the alert repeats
once an hour for as long as the condition holds, so a twelve-hour dry
sent twelve notifications about a temperature the user chose. It then
kept sending them while the unit cooled back down, which is the half the
reporter confirmed on an AMS 2 Pro and an H2C.
Dispatch now consults the drying state the firmware already reports.
dry_time alone is not enough: it reads 0 through the cooling phase that
closes a cycle, so dry_status -- info bits 4-7, already parsed for the
drying-complete edge -- carries the rest. That constant moves out of
bambu_mqtt into a leaf util rather than being duplicated; drying_preflight
would have been the natural home, but it imports printer_manager, which
imports bambu_mqtt, and bambu_mqtt is one of the callers.
The cool-down afterwards is held by a latch released as soon as the unit
reads back at or below the threshold, rather than after a fixed delay, so
a 65C cycle in a cold basement and a 45C one in a warm room each get the
time they actually need. A two-hour cap bounds the one case the latch
cannot resolve on its own -- a unit that never returns below the
threshold -- and since such a unit would have been alarming with no
drying involved, releasing there restores the ordinary behaviour instead
of inventing a new alert.
Two exclusions are deliberate. Humidity is untouched, because during
drying that reading falling is the whole point. And dry_status 6,
HeatOutOfControl, is kept out of the active set: an AMS that has lost
thermal control is exactly when the alert should still arrive, so it must
never read as expected heat.
A cycle plus its cool-down outlasts a restart, so the latch is a settings
row rather than a dict beside _ams_alarm_cooldown -- the internal
timestamp-row pattern support.py already uses. It is read once per pass
and written back only when a unit changed it. Stamps ahead of now are
clamped on read, since a box whose clock jumps backwards writes them and
suppression is measured as now minus the stamp; without the clamp the cap
would measure from a moment that has not happened yet and hold the alert
quiet for the skew on top of it.
No new setting. The reporter was offered the opt-out checkbox they asked
for and said they would not want it if the alert simply never fired
during drying.
|
||
|
|
89ea337612 |
Recognise our own print dispatch instead of guessing at a magic number
(#2843 follow-up) Bambuddy records the project_file behind every print, because that command names where the sliced file was put and so decides whether the archive can have a thumbnail and slicer metadata at all. Its own dispatches were told apart from a slicer's by testing sequence_id against "20000", on the belief that 20000 was Bambuddy's alone. It never was. 20000 is the slicer convention Bambuddy copied -- virtual_printer/bind_server documents the slicer sending exactly that during detect -- and both slicers count up from it. Measured on the wire: OrcaSlicer dispatched 20000 and then 20001, BambuStudio 20009 and 20010. So whichever dispatch happened to land on the shared value was filed as ours and never recorded, and after a slicer restart that is the first print you send. The test was wrong in the other direction too: it called every value above 20000 external, including ones we had sent ourselves, which only stayed invisible because we always send exactly 20000. Ownership is now established by remembering the job actually dispatched -- sequence id, file, url and subtask name -- and consuming that marker on the echo. One-shot deliberately: a slicer reprint of the same file a moment later is somebody else's print and must not hide behind our last one. Nothing about printing or archiving changes. current_project_url and ams_mapping are both captured before this branch and always were, so the storage verdict that gates the FTPS sweep is untouched; two tests pin that, because it is the part that would actually cost archives if it drifted. What changes is that the diagnostic entry stops lying, and it is the entry that tells an operator whether their printer stores files somewhere Bambuddy can read -- which is the whole subject of #2780 and would have to count, and an undercount there would have been silent. |
||
|
|
6488a33488 |
Stop a print with no 3MF borrowing another model's data (#2843)
H2-series and P2S firmware keeps a slicer-sent file on internal eMMC. Port 990 serves external storage only, so there is no file to fetch, and the print becomes an archive with no 3MF -- the ordinary outcome for anyone who sends from Bambu Studio rather than through Bambuddy. Confirmed on the maintainer's own machines: an H2C and an H2D both dispatched brtc://emmc for the same model one minute apart, while an X1C sent ftp:// for it. Three separate defects live in what happens next. The first is the serious one. An archive with no 3MF keeps the path the printer is executing as its filename, and on a sliced job that is always Metadata/plate_1.gcode. The fallback that looks for the same model in the Library or among earlier prints took its search term from there, so it searched for `plate_1` -- a name every Bambu print in existence has -- and matched on a substring, so it also matched any name merely ending that way. On the H2D a 1.6 g Cube resolved to lid_plate_1.gcode.3mf and was costed at 207 g across three real spools. It was not confined to plate names either: in the same database `Bank.3mf` matched "Piggo the piggy bank", and `x1c.gcode.3mf` matched "slice-test-x1c". The matcher now takes the model name the printer reports when the filename is only a plate path, refuses a bare plate stem rather than searching for it, and anchors to a whole filename with LIKE metacharacters escaped, because `_` is a wildcard and model names are full of them. A print that cannot be identified is now left untracked, which is the honest answer -- the previous behaviour was to charge the operator's spools for a model they had not printed. Checked against every row rather than argued from the code. Across 273 library stems the result sets are identical. Across 241 archive stems 14 differ, all of them strictly narrower, and every dropped match is one of the false positives above; all 233 archives still match their own filename, so no legitimate donor was lost. Of the eight no-3MF archives on that install the old matcher picked a wrong donor for two -- one of them a calibration run that would have been charged the 207 g -- and the new one picks none. The second defect is that those archives could not receive a timelapse at all. attach_timelapse derived its destination from the missing file's path, and (base_dir / "").parent is the parent of base_dir, one level outside the data directory. In Docker that is /app, so every attempt failed EACCES and the scan retried and discarded the video 25 times over twelve minutes, roughly a hundred FTPS connections for bytes that had already downloaded successfully. Where that location happened to be writable it was worse: the file landed beside the installation and the attach then failed anyway, because the path could not be made relative to base_dir. #1820 introduced a shared helper precisely so these derivations could not drift apart, and this was the one site still doing it by hand. The directory is created only after the filename has passed the traversal check, so a rejected name still leaves nothing behind. The third is silence. When a print's filament cannot be read from a 3MF, the remaining-percentage delta is the fallback, and that needs a reading at print start -- which a spool without RFID does not have until someone sets a remaining amount by hand. Those slots were skipped with a bare continue. Every other reason for skipping a slot in that loop is logged, and the comment a few lines below argues the case explicitly: charging nothing silently is indistinguishable from having nothing to charge. It now says so, for slots the print actually used. Four existing tests needed updating rather than the production path. They patch backend.app.services.archive.settings by name, and the shared helper reads its own module-level binding, so they kept the real data directory and wrote outside tmp_path -- which is how the first draft of this change littered a working tree. They now patch both bindings. |
||
|
|
28b2b9f151 |
Pin the PostgreSQL session to UTC so defaulted timestamps are UTC (#2855)
On a UTC+3 install every AMS humidity reading and every archive was stamped three hours ahead of when it happened. Bambuddy stores naive timestamps that hold UTC and the frontend's parseUTCDate reads an offsetless timestamp as UTC, so the display added the offset to a value that was already local. The Python side has honoured that contract since #504. The reporter's timestamps were not written by Python. Around ninety-six columns take their value from server_default=func.now() and the migration DDL carries another forty-nine on DEFAULT CURRENT_TIMESTAMP -- the database fills those, and on PostgreSQL now() is a timestamptz, so storing it into a timestamp without time zone casts it through the session TimeZone. A Postgres container started with TZ=Europe/Istanbul bakes that zone into postgresql.conf at initdb, and every defaulted column then receives local wall-clock. recorded_at is the clearest case: nothing in the codebase ever assigns it, so its value is entirely whatever the database decided. Connections now carry timezone=UTC, which makes the cast a no-op whatever the server is set to. Measured through the real engine factory against a live PostgreSQL, a session on the reporter's configuration stored +10800s and the fixed one +0s. Pinning the session was preferred over a hundred and forty-five individual edits partly for its size but mostly because half of those sites are raw DDL that no model-level change can reach. SQLite needed nothing and gets nothing: its CURRENT_TIMESTAMP is UTC by definition and it has no session timezone to get wrong, which is why this survived two years of timezone fixes without showing itself. That also makes it the reference -- the change moves Postgres onto SQLite's behaviour rather than introducing a third convention -- so the SQLite behaviour is now pinned by a test instead of being assumed. asyncpg is the documented driver and takes the setting in its startup packet; any other Postgres driver gets the same setting the libpq way, so a psycopg URL does not fail at connect on a keyword asyncpg alone accepts. Rows already written are deliberately left alone. The inverse cast is computable and DST-correct, but it cannot be applied safely: created_at is assigned explicitly on some paths and defaulted on others, an install that began on SQLite holds correct and shifted rows side by side, and nothing distinguishes them after the fact. Timestamps are right from the upgrade forward and history keeps the times it was given. One related mismatch goes with it, because fixing the database side alone would have made it start lying on exactly the installs this repairs. The support package's oldest_pending_age_seconds subtracted a naive local clock from a naive UTC column, with a comment claiming it was UTC; on the reporter's install the two errors cancelled. It reported a job queued five minutes ago as three hours old east of Greenwich and a negative age west of it. The two AMS and printer-sensor retention cutoffs move to the same utcnow_naive helper -- correct in value already, but deprecated in 3.12 and emitting warnings on every sweep. |
||
|
|
c5e0055864 |
Track which nozzle each AMS feeds through the Filament Track Switch
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty. Bambuddy had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The binding needed no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the type and the extruder id -- 0 is In-B, 1 is In-A -- and it only means anything when a switch is installed, because without one 0xE really does mean an uninitialised unit. That gates the read, which forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. The badge letters stay L and R, In-A reading as L, with the inlet named in full in the tooltip -- the letter is the inlet's position and not a claim about which nozzle that AMS feeds, since the switch can route either inlet to either outlet. Both views update live now. The switch fields were missing from the WebSocket payload, and the frontend shallow-merges each push over its cached status, so an absent field kept whatever the last full fetch left behind. The broadcast dedup key had no term for them either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and not in the AMS change-hash, which covers tray fields only and must stay that way because it drives Spoolman sync. The rest of this is the calibration half, which is where it actually bites. K-profiles are calibrated per nozzle and the printer numbers its calibration table per nozzle too, so entry 16 exists on both hotends and means a different profile on each. A tray holds exactly one index. Move an AMS to the other inlet and every configured slot in it silently keeps pointing at the old hotend's table -- measured on the maintainer's H2C, a black PLA calibrated 0.018 left and 0.020 right stayed on the left profile after the move, and a manual RFID re-read only re-asserted the same wrong one. Bambu has not solved this either; their AMS dialog carries "TODO: fila_switcher broken the connection of ams->extruder" above the line that decides which nozzle's profiles to offer. Three copies of "which extruder is this slot on" each ended in else 0, which on a switch machine filed every profile under the right-hand nozzle. They now share one resolver that returns None for "unknown", because unknown and extruder 0 are very different answers on a dual-nozzle machine and conflating them is what bound a left-nozzle profile to a slot sitting on the right. The per-slot K value on the printer card had the same confusion from the other direction: its lookup was keyed on cali_idx alone, so one nozzle's K silently overwrote the other's. Moving an AMS now re-selects each configured slot's counterpart profile for the nozzle it has arrived on. Only the calibration binding moves, and only for slots whose spool already has a profile for that nozzle: configuring a slot is a deliberate preparation step, so a slot we know nothing about, or a spool calibrated on one hotend only, is left exactly as the operator set it. Nothing fires on the first sighting of a binding either, since every reconnect learns them afresh and re-applying there would overwrite a choice made by hand. Configure Slot resolves against the slot's own nozzle throughout. Option identity carries the extruder, so a filament calibrated on both hotends gives two distinguishable entries instead of two that collapse into whichever the printer listed first; options name the hotend; matches are scoped to the nozzle the slot feeds, with the other hotend's profiles still reachable under Other K profiles; and the slot's active index is resolved as a pair rather than followed into the wrong table. Inlet to nozzle is one table, In-A to the left hotend and In-B to the right, measured rather than assumed -- fila_switch.out cannot be used for it, reporting [1, 1] unchanged across a 90-second capture, both outlets claiming the same extruder. The print dialog picks up the same inlet labelling in its slot dropdown, replacing a left/right hint that never rendered because it matched snow-encoded values against global tray ids; decoding it correctly would not have saved it, since the firmware never reports which inlet is currently paired with which outlet. The dialog also notes when every filament a print needs sits behind one inlet, which is legal but slow -- a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS, where a change across the two only retracts as far as the switch. Assigning an AMS to an inlet remains printer-side. BambuStudio can read that binding and has no command to write it, so there is no wire format to copy. |
||
|
|
7a42e0a7e5 |
Show which Filament Track Switch inlet each AMS feeds
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty on these machines. The printer card had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The SpoolBuddy assign modal had the same fallback in a worse form, mapping anything that was not extruder 1 to R. The binding turned out to need no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the AMS type and the extruder id -- 0 is In-B, 1 is In-A -- and it is only meaningful when a switch is installed, because without one 0xE really does mean an uninitialised unit and those bits carry nothing. That gates the read, which in turn forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. _parse_fila_switch is split out and called first, and left in _update_state as well so that stays a complete absorb step. The badge keeps L and R rather than A and B, because the lettering is familiar and matches the physical layout. It is a different colour from the plain nozzle badge, and its tooltip names the inlet in full, since the letter is the inlet's position and not a claim about which nozzle that AMS feeds -- the switch can route either inlet to either outlet. An AMS still reporting a real extruder id keeps its ordinary badge, which BambuStudio also treats as authoritative over any switch binding, and a switch that has been fitted but not yet set up on the printer shows nothing rather than a guess. The print dialog's slot dropdown gets the same label. It replaces a left/right hint that never once rendered: ftsExtruderForSlot compared snow-encoded in[] values against global tray ids and could not match. Decoding it correctly would not have saved it -- the firmware reports which slot sits in each inlet and which nozzle each outlet feeds, but never which inlet is currently paired with which outlet, so no per-slot nozzle can be derived. That function is gone rather than fixed. The dialog also points out when every filament a print needs sits behind one inlet. Bambu's own guidance is that this is legal but slow: a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS before the next can be fed up the shared tube, where a change across the two inlets only retracts as far as the switch. All on one inlet means every change in the job takes the slow path, and moving a single spool fixes it. So it advises, it does not block. Both views update live. Two things were stopping that. fila_switch and ams_switch_inlet were absent from printer_state_to_dict, and the frontend shallow-merges each WebSocket push over its cached status, so a field the push omits keeps whatever the last full fetch left behind. And the broadcast dedup key had no term for either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and it is not in the AMS change-hash either, which covers tray fields only and must stay that way because it drives Spoolman sync. Assigning an AMS to an inlet remains printer-side. BambuStudio can read the binding and has no command to write it -- its switch class is parse and getters only, and the recommended-arrangement popup draws and publishes nothing -- so there is no wire format for us to copy. Adding the two fields to PrinterState broke four test modules whose SimpleNamespace stubs predate them. The stubs are fixed rather than the production reads made defensive: the real dataclass always carries both, and a getattr in the dedup key would silently stop tracking the field if it were ever renamed. |
||
|
|
9c93884397 |
Stop auto K-profile calibration leaving an archive behind
With flow dynamics calibration on, the printer lays down a pressure-advance line before the print itself and announces it over MQTT through the same print-start event a real print uses. Bambuddy archived it: a row named auto_pa_line_calib_mode marked as having no 3MF, sitting in among the user's actual prints, with a Print Started and a Print Completed notification for each one. The printer's other internal jobs were already skipped, but only by the /usr/ path they carry -- bed levelling reports as /usr/etc/print/auto_cali_for_user.gcode. The pressure-advance line carries no path at all. It arrives as a bare subtask name, so a rule that only ever looked at the filename could not see it. Falling past the guard it reached the no-3MF fallback, where print_name is subtask_name or filename, and named the row after the calibration. Internal jobs are now recognised by name as well as by path, from either field, in one place both callbacks share. The match is exact after normalising away the directory, one print-file suffix and case, rather than a prefix or substring rule: "auto" and "calib" are ordinary words in a user's own filenames, and a rule loose enough to catch some unnamed future calibration would quietly swallow somebody's print. The completion is suppressed too, and that half matters more than the noise. With no archive to close, the completion falls into the no-archive notification path -- which attributes an unmatched completion to any queue item the printer finished in the last five minutes and emails its owner. This calibration runs alongside a real print, so silencing only the start would have told that print's owner their job was done, early, and again for real later. The guard sits inside the no-archive branch, so the plate-clear gate, the queue reconciliation and the SD-card cleanup all still run; only the notification is skipped. Skipping the run early also drops the FTP sweep that preceded the fallback -- six candidate names across five directories with retries, around a hundred connections looking for a file that cannot exist, aimed at a printer that is in the middle of calibrating. auto_cali_for_user no longer sends a Print Started notification either. It is the same event about the same kind of job, and the archive was never the only thing wrong with treating it as a user's print. |
||
|
|
7a9b4921bd |
Stop the AMS temperature alert firing for heat the user asked for (#1802)
The alert compares against ams_temp_fair, the same threshold that colours the printer card, which defaults to 35C. Drying deliberately runs at 45C for PLA, 65C for PETG and up to 85C on an AMS-HT, and the alert repeats once an hour for as long as the condition holds, so a twelve-hour dry sent twelve notifications about a temperature the user chose. It then kept sending them while the unit cooled back down, which is the half the reporter confirmed on an AMS 2 Pro and an H2C. Dispatch now consults the drying state the firmware already reports. dry_time alone is not enough: it reads 0 through the cooling phase that closes a cycle, so dry_status -- info bits 4-7, already parsed for the drying-complete edge -- carries the rest. That constant moves out of bambu_mqtt into a leaf util rather than being duplicated; drying_preflight would have been the natural home, but it imports printer_manager, which imports bambu_mqtt, and bambu_mqtt is one of the callers. The cool-down afterwards is held by a latch released as soon as the unit reads back at or below the threshold, rather than after a fixed delay, so a 65C cycle in a cold basement and a 45C one in a warm room each get the time they actually need. A two-hour cap bounds the one case the latch cannot resolve on its own -- a unit that never returns below the threshold -- and since such a unit would have been alarming with no drying involved, releasing there restores the ordinary behaviour instead of inventing a new alert. Two exclusions are deliberate. Humidity is untouched, because during drying that reading falling is the whole point. And dry_status 6, HeatOutOfControl, is kept out of the active set: an AMS that has lost thermal control is exactly when the alert should still arrive, so it must never read as expected heat. A cycle plus its cool-down outlasts a restart, so the latch is a settings row rather than a dict beside _ams_alarm_cooldown -- the internal timestamp-row pattern support.py already uses. It is read once per pass and written back only when a unit changed it. Stamps ahead of now are clamped on read, since a box whose clock jumps backwards writes them and suppression is measured as now minus the stamp; without the clamp the cap would measure from a moment that has not happened yet and hold the alert quiet for the skew on top of it. No new setting. The reporter was offered the opt-out checkbox they asked for and said they would not want it if the alert simply never fired during drying. |
||
|
|
907de4d64d |
Suppress two Bandit false positives in the new FTP and batch-order tests
The 1.2.5.3 code-scanning run flagged two new alerts, both in test files added this release, and both false positives. B402, the ftplib import in the #2780 connect-cleanup tests, is the HIGH finding that failed the check. The test imports ftplib to construct the exceptions BambuFTPClient.connect has to survive -- error_perm and error_temp, at lines 50, 51 and 75. Nothing in the file opens a connection, and bambu_ftp.py already carries the same marker on its own import. B108, the /tmp path in the batch-order archive fixture, is the MEDIUM one. The value is a string written into PrintArchive.file_path so the row has a path; nothing ever opens it. Every other archive fixture in the suite carries the same marker on the same idiom. Both markers follow the wording already in test_bambu_ftp.py and test_sjf_scheduling.py. Bandit's medium+ count over backend/ drops from 17 to 15, and neither file contributes to what is left. |
||
|
|
0e591b2199 |
Suppress two Bandit false positives in the new FTP and batch-order tests
The 1.2.5.3 code-scanning run flagged two new alerts, both in test files added this release, and both false positives. B402, the ftplib import in the #2780 connect-cleanup tests, is the HIGH finding that failed the check. The test imports ftplib to construct the exceptions BambuFTPClient.connect has to survive -- error_perm and error_temp, at lines 50, 51 and 75. Nothing in the file opens a connection, and bambu_ftp.py already carries the same marker on its own import. B108, the /tmp path in the batch-order archive fixture, is the MEDIUM one. The value is a string written into PrintArchive.file_path so the row has a path; nothing ever opens it. Every other archive fixture in the suite carries the same marker on the same idiom. Both markers follow the wording already in test_bambu_ftp.py and test_sjf_scheduling.py. Bandit's medium+ count over backend/ drops from 17 to 15, and neither file contributes to what is left. |
||
|
|
95e28e39ce | Bumped version | ||
|
|
e899022116 |
fix(profiles): read the companion files that hold a preset's real gcode
A bundled preset can keep a setting in `<preset> template <key>.json`, a
file the preset itself does not reference -- the desktop slicer finds it
by name. Walking only `inherits` never reached it, so every one of the 56
instantiable BBL machine presets resolved `machine_start_gcode` to the
577-character generic block on fdm_machine_common instead of its own
6.5-21 KB one. That block holds the M620 AMS load and the M1002
gcode_claim_action calls, so a print sliced from it heats the bed, moves
the toolhead and extrudes nothing (bambuddy#2838).
Companions are now folded into each ancestor as the chain is walked, at
that ancestor's precedence, so a caller's own value still wins and the
0.2/0.6/0.8 variants reach their 0.4 sibling's companion. They are found
by listing rather than by a fixed set of keys.
Covered against the shipped bundle, not fixtures: a new e2e spec resolves
all 56 presets inside the image and fails on any that still lands on the
generic block.
|
||
|
|
2d324dc7d0 |
Let API keys read and run slicer pipelines (#1425 follow-up)
Every pipeline endpoint answered 403 for API keys whatever scopes the key
carried. PR A parked all three permissions on the admin denylist until the
run dispatch existed to decide about; it landed in PR C and the parking was
never revisited.
PIPELINES_READ now rides can_read_status. PIPELINES_RUN requires
can_queue AND can_manage_library together, so the allowlist gained tuple
values: a run slices into the library and then queues prints, and mapping
it to either flag alone would hand that flag the other one's authority.
The 403 names every flag the key is short of. PIPELINES_WRITE stays
admin-only -- a key can run the recipe, not rewrite it or clear the log.
Opening the run route also needed the cloud-owner fallback the direct
slice route makes: a pipeline can carry Bambu/Orca Cloud presets, and
resolving those reads a token off a user record that an API-keyed request
does not have. retry_failed forwards the new dependency explicitly,
since a direct call receives the Depends marker rather than None.
|
||
|
|
aa4df0c57a |
Build the slice output's path from a name a folder can have (#2832)
A print's display name comes from inside the 3MF, not from the filename,
so a MakerWorld title arrives with its punctuation: "Planter Pot with
Drip Tray, 12 cm / 5 inches". The slice-to-archive sink used it verbatim
for the output folder and the output file, and a slash in a folder name
is not a character -- it is another folder. mkdir(parents=True) created
the level it implied and the file's own join added a third that nobody
had made, so the slice failed with ENOENT on a path that half existed.
Renaming the print first was the only way through.
Reduce a display name to a single path component before it becomes one.
Characters a name cannot hold are replaced rather than dropped, so the
folder still reads like the model's title, and the set is the one the SD
card already rejects -- which covers a Windows install too, where the
colon in "Model: v2" fails the same way. The name shown in Bambuddy is
untouched: a title is allowed its punctuation, and refusing the slash
would reject the name this was reported about.
The joins are asserted to stay under the archive directory. That was
already claimed by a SEC-PATH-OK marker on both lines, citing a
sanitiser that is defined in another module and was never called here;
without the marker the path-join backstop flags them both. The claim is
now true, and a future edit that reaches around the reduction is caught
rather than trusted.
The library sink takes the same embedded name, so it gets the same
reduction: managed storage names the file after a UUID and never saw
this, but an external folder writes the name as given.
Display names are also stripped of control characters on the way into
the database, in the schema and in the archive service. The validator
hands back anything that is not a string rather than iterating it, so
the field still answers a list or a bare int with a 422 instead of
accepting the one and failing on the other.
Display names are also stripped of control characters on the way into
the database, in the schema and in the archive service, cleaned before
the filename fallback rather than after it so a whitespace-only embedded
name still falls through to the filename. The validator hands back
anything that is not a string rather than iterating it, so the field
still answers a list or a bare int with a 422 instead of accepting the
one and failing on the other.
|
||
|
|
dde724f83c |
Repair no-3MF archives' photos and their silent filament writes (#1820)
Two faults behind the same kind of print: one that arrives without a
retrievable 3MF, which on an H2S is any job started from the printer's
own internal library.
Such an archive has no file_path, and Path("").parent is Path("."), so
every site that derived the archive's folder from it landed on the data
directory itself. The finish-photo capture spotted that and wrote to
<archive_dir>/<id>/photos instead. Nothing else did. The photo was
written in one place and looked for in another: reads 404'd, deletes
dropped the name and left the file, and the notification attachment
never found the image. Hand-uploaded photos worked only because upload
and read agreed with each other rather than with the capture. Give the
question one owner in utils/archive_paths and have all four sites ask
it. Lookups check the old shared location too, so photos already
uploaded there stay reachable; uploads now go where captures go.
Separately, the remain%-delta fallback that stands in for a missing 3MF
can charge nothing for several reasons, and did so without a word. The
AMS reading is coarse and, on the reporter's printer, noisy: it rises
mid-print, swings five points over a job, sits at 100% through a
36-minute print on a fresh spool, and goes negative on a nearly empty
one -- which the start-of-print gate rejects, dropping the only slot
that was printing. Two of their prints went uncounted for two different
reasons and both read as "no spools updated", which is also what a print
with nothing to charge prints. Name the slot and the two readings in
each case, on the Spoolman path and on the internal-inventory path,
which has carried the same gates since #1119.
The Spoolman path also had no notion of which slots the print used, so a
spool swapped into an idle slot mid-print reads as consumption and is
billed to whoever that slot is assigned to -- the fault #1269 fixed for
the internal tracker, still open here, and likeliest on exactly the
prints this fallback serves, where nothing else narrows the field. Use
the same three pieces of evidence it does: the print's mapping, its
mid-print tray changes, and the tray it started on. The last needs
storing, because the internal tracker's row is deleted before this runs
and a screen-started print has no mapping to fall back on -- hence a new
nullable column, and no backfill, since a row from before it existed has
nothing to say. Where no evidence exists at all, every slot is still
considered.
Both paths also treated tray_now == 255 as naming a slot. It does not:
it is the field's initial value, the fallback for an unparseable
reading, and what it reports with nothing loaded. Mapped as a tray id it
becomes (255, 1), so as the only evidence it excluded every real slot
and charged nothing at all -- this issue's own bug, arriving by a new
route. On the internal path that is live today; on the Spoolman path it
would have shipped with the guard above. The external holder reports 254
when it is genuinely in use.
The arithmetic is untouched: at one percent per step this cannot resolve
a small print, and pretending otherwise would be worse than saying so.
|
||
|
|
15eace71d6 |
Show the plug that powers the printer in the card's Power row (#2830)
A printer card has one Power row: a plug name, its draw, and the auto-off
and on/off buttons. Which plug filled it was decided by nothing -- the
endpoint returned the first row the database handed back that was not a
Home Assistant script, from a query with no ORDER BY.
For the reporter that was an enclosure exhaust fan, added before the
outlet their X1C is plugged into. The card showed the fan's name with
'--' for watts, offered to switch the printer off by cutting the fan,
and demoted the metered outlet to the small HA button row. The fan was
marked as not powering the printer and hidden from the card; neither
setting was consulted here, though controls_printer_power has decided
the scheduler's power-on pick since #2629.
Rank the candidates instead: switchable at all, controls_printer_power,
enabled, show_on_printer_card, reports power, lowest id. The first rules
out a script, which can only be run, and an MQTT plug, which the control
endpoint rejects as monitor-only -- and an MQTT plug is exactly the kind
that reports watts, so without it ahead of the power tiebreak the row
could land on a plug whose on/off button answers with an error. The last
is not cosmetic: with no ORDER BY, a plain UPDATE on PostgreSQL can move
a row and silently swap which plug the card calls the printer's power.
None of these excludes a plug. A printer whose only plug is hidden,
disabled or monitor-only still needs its Power row, because that row
holds the on/off button and the HA buttons are drawn inside it.
controls_printer_power sits above show_on_printer_card because the two
only disagree when the plug that really feeds the printer is hidden, and
letting a display preference win there points the power buttons at an
accessory -- the fault #2629 fixed. Power capability is read from the
configuration, not measured: this runs on every card render, and it is
approximate both ways, so it only breaks a tie.
The scripts endpoint shares the same pick and excludes it, so a
switchable main plug is not repeated as a button directly below itself.
A script is left in place: a printer whose only entities are scripts
falls back to showing one in the power row, and taking it out of the
button row too would cost it the one-click run it has always had.
|
||
|
|
46aa6affd3 |
Match a print completion to its queue job the way the printer names it (#2829)
Bambuddy has no run identifier to tie a completion to a queue row, so it
finds the row by printer and status='printing' alone.
|
||
|
|
b83eea07cd |
Explain a print that never reached the printer's card, instead of sweeping for it (#2780)
Bambuddy reads a print's 3MF, cover and timelapse over FTPS on port 990,
which on every Bambu model serves external storage only. Under some
configurations H2-series and P2S firmware keeps the sliced file on
internal storage, where Bambu Studio put it over the port-6000 service,
and then no path on 990 can find it.
The print command has always said which of the two it used -- `url` reads
ftp://<name> or brtc://emmc/<name>. We discarded it and swept anyway:
~110 connections per print, all certain to fail, ending in an archive
card with nothing on it and no stated reason. In the reporter's bundle
all 35 dispatches to their H2C and P2S said internal storage, all 25 to
their X1C said external, and all 44 empty cards belonged to the first two.
Read the field, skip the sweep when it cannot succeed, and record which
reason applied. A printer that uses the card is unaffected, and so is one
we have no answer for -- silence is not evidence, and reading it as bad
news would break archives that work today.
The answer is held per print and dropped when that print ends, rather
than kept as a standing fact about the printer. Plenty of prints never
announce themselves: 14 of the 79 print starts in that bundle arrived
with nothing on the request topic, started from the printer's own screen
or picked up after a restart. Left standing, one slicer print to internal
storage would suppress the lookup for every screen-started print after
it, on a printer whose files really are on the card. The sticky reading
is kept for the connection diagnostic alone, which is run after the print
that prompted it and would otherwise have nothing to report.
Two things that pointed the wrong way go with it. The archives banner
told everyone to enable "Store sent files on external storage"; the
reporter had it on for the whole three weeks and it would not have
helped. The diagnostic passed a printer whose slot was empty, because it
read only the toggle -- an empty slot is now a failure naming the slot,
and a printer that has storage and still used its own is a warning. On
P1-series that empty-slot failure yields to the existing unsupported-model
skip: the toggle cannot be switched on there at all, so telling the
operator to insert a card would promise a fix inserting a card does not
deliver (#2524).
Also close FTP sockets on the failure paths, which dropped them for the
garbage collector -- 1813 in a day in that bundle -- and drop the advice
to restart the printer, which the reporter tried twice while a single
manual connection to the same printer handshook cleanly.
This does not make the affected prints archive in full; that needs the
port-6000 protocol tracked in #2762.
|
||
|
|
8e553289db |
Choose which rack nozzle each filament prints from on an H2C (#1784)
The Vortek rack holds six hotends, and a multi-colour plate is sliced to
use a different one per colour so it can skip the purge. Which of the six
each colour takes is not in the 3MF. The same plate, sliced and sent twice
from Bambu Studio with a different choice each time, produces two files
that differ only in rounding in the last digit of a few extrusion figures
-- the filament grouping, the toolchange stream, the 120 nozzle-change
markers and project_settings.config are all identical. The choice travels
only in the dispatched nozzle_mapping.
Bambuddy had no way to state it, so those plates went out with no nozzle
assignment at all and the printer chose for itself. That is what levelled
on one hotend and printed with another, millimetres above the plate.
Every rack-bound filament now carries a position picker beside its AMS
slot dropdown, listing all six with the nozzle each holds. An empty
position, or one holding the wrong diameter or flow type, is shown greyed
out with the reason rather than hidden, so someone looking for position 4
finds it. The choice is per filament *group* rather than per slot, because
a group is one hotend: two filaments the slicer grouped together share it
and cannot point at different positions.
Nothing has to be picked. Positions are assigned automatically, preferring
one already loaded with that colour, which on the plate this was built
against reproduces Bambu Studio's own pick exactly.
A nozzle currently picked up onto the carriage is offered too. The
firmware drops its rack position from the report entirely rather than
sending a placeholder (#943), and refusing it would rule out the position
most likely to be wanted -- the one the last print left mounted. Only
recoverable when exactly one position is missing; two gaps are genuinely
ambiguous and stay unavailable.
Positions are re-checked at dispatch, not just when queued, because the
rack can be re-loaded in between. The two failure modes differ on purpose:
an explicitly chosen position that no longer fits stops the print, names
what the position now holds, and deletes the uploaded file from the SD
card so it cannot be started by hand either -- an operator who named a
hotend must not silently get a different one. An automatic assignment that
cannot be made instead falls back to letting the firmware choose, which is
what happened before any of this existed.
The pick is stored as {group: position} rather than as the expanded
nozzle_mapping, though that is what goes on the wire. That column means
"Bambu Studio decided, forward verbatim", and only the group-and-position
form can be re-checked against what is actually mounted at dispatch.
The existing multi-rack refusal in extract_nozzle_mapping_from_3mf stays.
It still guards the #2800 fallback, which can only ever name one rack id.
Measured on the maintainer's H2C: rack position n is physical nozzle id
15 + n, confirmed by cross-referencing two captured dispatches against
Bambu Studio's own dialog. extruder_max_nozzle_count names which carriage
is the rack straight from the file, and is read rather than assumed -- a
fourth independent confirmation of the carriage indices fixed in
|
||
|
|
58594c60c9 |
Print an H2C two-nozzle plate from the carriage it was levelled on
The two carriages were the wrong way round: extruder index 0 was treated as
the fixed hotend and index 1 as the swappable rack, and it is the other way
about. A plate using both was levelled with one nozzle and printed with the
other, several millimetres off the plate.
Three sources agree, and disagreed with the code. Telemetry reports
ams_extruder_map {'0': 1, '1': 0, '2': 0}. BambuStudio, dispatching a plate
that used all three of those AMS units, sent the filament from the unit on
extruder 1 to physical nozzle 1 and the ones on extruder 0 to rack positions
16 and 18, and that print completed. And the two constants could not both
have been right: _FIXED_NOZZLE_ID is 1 while the fixed extruder was 0, in a
scheme where physical nozzle id N sits on extruder N.
The old value came from the #2800 hardware A/B, where [17, -1, -1, 1] printed
in mid-air and [1, -1, -1, 17] printed correctly. That result stands -- it
established which wire worked. The extruder indices were not measured by it;
they were inferred by pairing the working wire with a slot_extruders list
produced by the 3MF reader that has since turned out to mis-read exactly
these files. The reasoning is recorded at the constants so a future
regression report is not re-litigated from scratch.
Also withholds nozzle_mapping entirely when more than one filament group
needs the rack. Two groups on one extruder means that extruder is a rack and
the plate wants a different hotend per group -- which physical slot each
takes is the slicer's choice against the live rack and is stated nowhere in
the file, since both groups can carry identical diameter and nozzle type.
Studio dispatched such a plate to 16 and 18; nothing here can reproduce that,
and answering anyway is what printed in mid-air, so the firmware picks.
Restores the fixed 32-entry padding that
|
||
|
|
3ce4ecfcf7 |
Report a print stage we cannot name at the default log level
STAGE_NAMES is hand-maintained and every new model adds to it, so a printer
occasionally reports a number that is not in it and the card reads "Unknown
stage (72)" -- which an H2C did, where the table runs to 66 and then jumps
to 74. Stage transitions were logged only at DEBUG, off in normal running,
so the sole record that it had happened was the card itself, and by the
time anyone looked the printer had moved on.
The asymmetry is the point: a stage we can name is worth DEBUG, and the one
we cannot is the interesting one. An unnamed stage is now logged at INFO,
once per stage number per session, with the model, the stage it came from
and the print state at the time -- which is what naming it afterwards
needs. Named stages are unchanged, so a normal print logs nothing new. -1
is excluded: it is Bambuddy's own "not in a stage" sentinel and the field's
initial value, so every print would otherwise report it on the way out of
its last real stage.
Fixes a latent crash found while testing this. The stage-change log line
builds its text before the log level is consulted, so get_stage_name runs
on every transition whatever the level is set to; a stg_cur that was not
hashable -- malformed telemetry rather than an unknown stage -- raised
TypeError out of STAGE_NAMES.get and aborted the whole state update.
Labelling a value can no longer do that.
|
||
|
|
c5cd0dbe77 |
Stop an H2C refusing a multi-colour print as a hotend mismatch
The print uploaded, the printer took the command, and stopped at once with
HMS 0500-4047 -- "the available hotend quantity or model does not match the
sliced file". nozzle_mapping told the printer one of the plate's filaments
went to no hotend while ams_mapping named the tray it comes from, and the
firmware will not start a job on that contradiction.
Each filament in a 3MF names the group it belongs to, and on every other
dual-nozzle Bambu the group number is also the extruder index, so it was
read as one. On a rack machine it is not: the rack carriage holds six
hotends to the fixed carriage's one, so the slicer writes a group per
nozzle rather than per carriage. The failing plate carried groups 0, 1 and
2 against a two-entry physical_extruder_map, and the filament in group 2
was dropped -- indistinguishable downstream from a slot the plate does not
print, which is what reached the wire as -1.
extract_nozzle_mapping_from_3mf now resolves the group through the table
the file states for itself, the <nozzle id extruder_id> elements in
slice_info.config. Files carrying no such table keep the direct index, so
H2D slices are unaffected. A filament that still cannot be placed drops
the whole mapping with a logged reason instead of half an answer: the
firmware then picks its own nozzle, which is the pre-existing behaviour
and far better than an answer that contradicts itself.
Two related faults fixed in the same pass. The mapping was read across
every plate in the file, so on a multi-plate project a slot took its
extruder from whichever plate came last; it is now scoped to the plate
being dispatched, in extract_filament_requirements as well. And the array
is now one entry per filament slot, matching BambuStudio's own dispatch of
[1, 16, 16] for a three-filament plate, rather than padded to a fixed 32.
Verified against the file that failed: slot extruders [-1, 1, 0] became
[0, 1, 0], and the wire [-1, 16, 1, -1 x29] became [1, 16, 1].
|
||
|
|
12a34448f9 |
Rank near-colour matches by how they look, and share one filament type table (#2804)
Three follow-ups to #2804, all bearing on one decision: which spool a print uses when the exact colour is not loaded. Colour ranking is now perceptual. The ranking added in #2804 measured RGB distance, which rates a colour by how far apart the numbers are rather than how far apart they look, and it overweights blue badly enough to invert the answer: against a required #1E4821 green, a purple #38202F is the nearer of two eligible spools by RGB and four times the further once measured properly. Both sides now use CIEDE2000 -- perceptual_color_distance in backend/app/utils/color_utils.py and colorDistance in amsHelpers.ts, kept structurally identical so they can be read side by side. Verified against the Sharma/Wu/Dalal published reference set, all 31 pairs to 1e-4, and the two implementations agree to within 1e-9 across 800 sampled pairs. Eligibility is untouched, still the per-channel RGB box, so this only reorders spools that already qualified. Type matching now agrees between the interface and the scheduler. Bambu firmware treats PA-CF, PA12-CF and PAHT-CF as one material and the scheduler has always matched them accordingly, but the interface compared raw type strings and called that same pairing a mismatch. The badge contradicted what the printer was about to do, and the manual override picker, which groups by canonical type, offered the very spool the badge then rejected. The fifteen comparison sites in useFilamentMapping.ts, useMultiPrinterFilamentMapping.ts and PrinterSelector.tsx now call filamentTypesCompatible. The pipeline pre-flight reads the matcher's table instead of its own copy. That copy had drifted into disagreeing in both directions: it aliased PLA Basic to PLA where the matcher never has, so a run could clear the check and then fail to map its slots, and it lacked the nylon grouping, so it flagged runs the matcher handles without complaint. A check whose job is to predict dispatch is wrong whenever it disagrees with dispatch, whichever way it leans, so it and the scheduler now both read backend/app/utils/filament_types.py. That canonicaliser deliberately does not strip surrounding whitespace. It looks like a free improvement, but it would collapse a junk tray_type to "" just as a 3MF declaring no filament type yields "", and a typeless requirement would start matching a junk-typed tray instead of reporting the slot unmapped. Padded type strings are worth handling on their own terms, with that case addressed. One behaviour change outside the ranking: the pre-flight is stricter for a printer reporting a product name such as "PLA Basic" where the generic material belongs, which it now flags rather than passes. Rare in practice, since the printer reports material and product name in separate fields, and it is the answer the matcher would give. Nothing about which spool a print actually uses changed outside the colour ranking itself. Adds 203 backend and 6 frontend tests. The #2804 tie-break test now uses identical colours: two colours at equal RGB distance are not perceptually tied, which is rather the point. |
||
|
|
292b1b416b | Pick the nearest eligible filament colour instead of the first one in tray order (#2804) (#2823) | ||
|
|
3031e52ddf |
fix(slicer): stop a 3MF from switching off supports its process preset turned on (#2820)
--load-settings is authoritative, so since #1881 four support fields travel the other way -- enable_support, the two filament slots, and support_type -- lifted out of the source 3MF and written over the picked process preset. Bambu's shipped presets all set enable_support: 0 because supports are a per-print decision, and without the carry a project exported with PVA in the interface slot sliced single-material. But the carry ran in both directions, and the off direction is the one nobody asked for. Nearly every published model ships with supports off, so slicing one against a custom preset that deliberately enabled them stripped them back out. The reporter's preset sets enable_support 1, support_type normal(auto), support_style snug; the slice came back disabled and tree(auto). Only the style survived -- it is not one of the four carried, and they had re-entered it in the slice dialog. The source can now switch supports on, never off. Nothing is lost: every shipped preset has them off, so a preset that has them on is a deliberate choice by whoever wrote it, and a file that wants supports still gets them with its slot assignments. A file that never declares enable_support is treated as off -- no intent to act on. The truthiness rule ("1", true, 1, and the forks that write neither) now lives in one place as supports_enabled_in_config(), shared with extract_support_filament_slots_from_3mf, which had it inline. Also log the carry with the fields it took. The slice dialog shows the picked preset's values, so a carried field silently disagrees with what was on screen and this step logged nothing at all -- the report chased an unrelated sanitiser line about the source file's own settings, which was the only thing in the log that mentioned any of these keys. |
||
|
|
558159c95c |
fix(queue): stop a library-file delete from destroying the jobs queued against it (#2819)
Nothing tied a library file to the queue rows pointing at it, and the FK
that describes the relationship is ON DELETE CASCADE -- which SQLite does
not enforce and PostgreSQL does. So the same fault had two faces: rows
left pointing at a file that no longer existed, failing at the printer
with "Library file not found" days later, or rows deleted outright with
no error and no history.
Two routes into it, both fixed by taking the queue off the file before
the row goes.
Dispatch (the reported case): quantity>1 on the printer-card
upload-and-print flow puts cleanup_library_after_dispatch on every copy,
and _clone_queue_item copies library_file_id onto batch clones, so the
first dispatch consumed the file the rest were waiting on. The copies are
now pointed at the archive that dispatch just created -- it holds its own
copy of the 3MF -- and the consume flag is cleared on them. A copy already
printing from its own archive keeps it, a finished one keeps its outcome,
and a cross-model item (#671) keeps any candidate this does not consume.
Deletion: the File Manager, bulk delete, folder delete, emptying the trash
and the retention sweeper all removed rows with queued work against them.
Folder delete did not even clear the cross-model candidates, because the
file-id walk it already performs threw its result away. Jobs waiting on a
deleted file are now cancelled at that moment, naming the file, and every
other row referring to it is detached rather than destroyed -- print
history and batch progress are counted from those rows. A job that is
printing is left alone: what is deleted is the library copy, not the copy
on the machine. The trash is reversible so it still changes nothing about
the queue, and a job dispatched while its file is in the trash now says so
instead of "not found".
Verified row for row on PostgreSQL 16 as well as SQLite: without this,
PostgreSQL deletes every queue row referencing the file.
|
||
|
|
3ac289fccb |
Attribute filament correctly when AMS backup swaps spools mid-print
Everything the completion path needs to split a print's filament across
the trays it fed from lived only in memory: the dispatched plate and
slot-to-tray mapping, the spool-assignment snapshot, and the tray-change
log. A print that outlived a restart lost all of it and fell back to
what the printer reports at completion -- which, with AMS Filament
Backup on, is the substitute tray. The whole print was charged to the
spool that only finished it while the spool that ran dry was charged
nothing.
Persist that context in a new active_print_sessions row, append tray
changes as they happen, and restore both the session and the printer's
tray-change log at restart recovery. Seed the log from the current tray
when there is nothing to restore, since last_loaded_tray advances even
when no change is logged.
Rank the queue item's stored ams_mapping above the printer's live
mapping field, which is what backup rewrites. Recover plate_id from the
archive or queue item, and give extract_layer_filament_usage_from_3mf a
plate_id instead of taking the first .gcode member -- a Bambu Studio
export stores plate 2 first, so per-layer figures were measured against
the wrong plate for both inventory backends.
Stop auto-unlinking a spool assignment when its slot reports empty
during a running print. At a runout the spool is still in the AMS, and
dropping the link leaves the completion path nothing to charge.
Capture the print-start context for both inventory backends. Spoolman's
own durable row (#1820) carries its plate-scoped figures and dispatched
mapping but not the tray-change log, and its slot assignments -- the
way. Registration in _active_sessions stays gated, since on_ams_change
reads it to decide whether to skip the remain%-based weight sync (#880).
|
||
|
|
0cdc9944a4 |
Do not close a queue item on a completion for another print
on_print_complete finds the row to close by printer and status='printing'
alone. The MQTT payload carries a subtask name but no run identifier, so
nothing tied the event to the row: any completion delivered for a printer
closed whichever job was printing on it. A job closed that way is marked
completed while the printer is still working, leaves the queue for
history, and strands the rest of its batch, because the queue correctly
refuses to dispatch onto a busy printer.
The handler now checks the completion against the file the row was
dispatched with, recovered from its archive, and leaves the row alone
when they disagree. Only a positive disagreement refuses: no archive, no
file name or no subtask name is unverifiable rather than wrong, and
refusing those would strand the item in 'printing' and wedge the queue --
the failure the loose lookup was avoiding in the first place.
This surfaced through the test suite, which could reach a real database.
conftest built its own SQLite engine, but core/config.py snapshots
DATABASE_URL at import time and core/database.py builds the module-level
engine and async_session from it. Tests reaching code that opens its own
session -- run_with_retry, which the completion path uses, takes its
sessions from core.database and so is untouched by the widespread
patch("backend.app.main.async_session") -- therefore talked to whatever
database .env named: the developer's own SQLite file on a plain checkout,
a live install with a PostgreSQL .env. DATABASE_URL is now redirected to
a throwaway file before any app import, and the run aborts rather than
starts if that did not take.
|
||
|
|
9b2bd911d8 |
Open multi-plate G-code on the plate that was asked for
Previewing a sliced multi-plate 3MF from the File Manager showed a plate
nobody picked. The library route took no plate parameter at all, so the
one the viewer has always put in the URL was dropped -- FastAPI discards
unknown query parameters silently. Both routes then fell back to the
first .gcode member of the zip, and member order is whatever the slicer
wrote: the reported file stores plate_2.gcode ahead of plate_1.gcode.
Nothing that opens the viewer from the File Manager passes a plate, so
there was no way to ask for another one either.
Plate resolution now lives in threemf_tools and both routes share it.
select_plate_gcode_name() returns the named plate or None, so a caller
serving an explicit choice can 404 instead of rendering something else;
default_plate_gcode_name() returns the lowest-numbered plate. The viewer
gained a plate switcher, and keeps the choice in its URL so a link to one
plate survives a reload. Filament colours follow it too -- they were
taken from the first plate regardless of which one was on screen.
G-code injection and the finish-photo max_z_height read shared the old
first-member fallback and now resolve the lowest plate as well.
|
||
|
|
26e49e76dd |
Pool AMS backup spools in the print dialog's filament check
The dialog weighed each plate against the spool in the slot it mapped to
and knew nothing about AMS Filament Backup, so a two-plate job needing
1441 g of ABS was refused against a 1000 g spool while the identical full
spool in the next slot went uncounted. The dispatcher has pooled matching
spools since #1762 and would have run the print -- "Print anyway" was
always the right answer to this warning.
The rule for which spools back each other up now lives in one place:
build_slot_materials() in filament_deficit, which the dispatcher's pool
and the new slot_materials half of GET /printers/{id}/inventory-remain
both draw on. The dialog groups on the keys it is handed rather than
resolving spools a second time, which is what let the two answers drift
apart, and which also gives the check to Spoolman users -- it read the
internal inventory only, so in Spoolman mode it approved everything.
Where a pool really is short the warning quotes the pooled totals, since
the per-slot figure reads as a contradiction next to a full peer spool.
|
||
|
|
b5330b2f88 |
chore(settings): drop the Slicer Bundles notice and rebalance the columns
Bundle import was withdrawn in 0.2.5 and the panel was kept behind as a
static notice pointing at the alternatives. It has been on screen for
several releases, it was shown to everyone running the slicer sidecar
whether or not they had ever imported a bundle, and it was a card in
Settings -> Queue & Dispatch that could not be acted on. Component,
render site and all thirteen locales' strings are gone; no slicing
behaviour is touched.
Four docstrings still described the removed feature as a live fallback:
SliceModal was said to fall back to "the user's uploaded Slicer Bundles"
when a preset carries no compatible_printers. There is no bundle model
and no bundle endpoint left -- the actual fallback is the @BBL <code>
printer-model registry, which is what SliceModal has been doing since
Removing the card left the left column of that tab noticeably longer, so
G-code Injection moves to the foot of the right column. The card is
unchanged and keeps its card-gcode anchor, so settings search still
jumps to it.
|
||
|
|
4fc72c0ddb |
fix(h2c): send physical nozzle IDs for both carriages, not extruder indices (#2800)
The first pass at the H2C rack mapping had both of its hardware-derived
values wrong, and the reporter's follow-up A/B on real hardware settled
them.
The rack does not feed extruder 0. A mixed-nozzle plate extracted as
slots=[0, -1, -1, 1] with rack position 17 dispatched as
[17, -1, -1, 1], and the rack nozzle printed several millimetres above
the bed. The rack is extruder 1.
Correcting only that is not enough. The fixed hotend answers to physical
ID 1, not to its extruder index of 0, and forwarding the index produced
[0, -1, -1, 17] -- a command the printer rejected outright rather than
mis-printing. Translating both carriages gives [1, -1, -1, 17], and the
same sliced file then cleaned, levelled and printed on the correct
nozzle at the correct Z through to completion.
Both values agree with three native Bambu Studio captures from the same
machine, which carry [1, 17, ...] and [17, 1, ...] depending on filament
slot order.
An extruder index naming neither carriage now omits the field instead of
reaching the wire as a physical ID that identifies no nozzle. The
fixed-hotend-only path is unchanged but no longer a guess: Bambu Studio
sends no nozzle_mapping at all for such a plate, which is what Bambuddy
already did.
Reported, diagnosed and hardware-verified by @tru3l3gend, who ran the
mixed-nozzle A/B on both nozzles and captured what Bambu Studio sends
for fixed-only and mixed plates.
|
||
|
|
c0c0c611b8 |
fix(slicer): write slice output to the source's external folder (#2810)
slice_and_persist always wrote to get_library_files_dir() while giving the
new row the source folder's id, so slicing a file on a NAS mount produced
a .gcode.3mf that showed up in the right folder in the UI and never
reached the share -- invisible from the web UI, which is why it did not
reproduce.
Resolve the destination from the target folder like uploads (#1112) and
moves already do, set is_external and store the absolute path. Collisions
uniquify to "Model (2).gcode.3mf": a 409 would throw away minutes of CPU
on a routine re-slice, and overwriting a file on someone's NAS is worse.
An external folder that cannot take the file (read-only, unreachable, not
writable) falls back to managed storage rather than discarding the slice,
and reports why on SliceResponse.external_write_fallback -- surfaced as a
warning toast. Silent fallback is what made this bug invisible.
|
||
|
|
7bd85692f5 |
fix(slicer): name the sidecar service in the update command (#2802)
The "update your sidecar image" advice told users to run a bare
`docker compose pull`. bambu-studio-api is declared with
`profiles: [bambu]`, and compose skips profile-gated services silently,
so the pull was a no-op for exactly the users the message was written
for -- and `restart: unless-stopped` kept the old container serving.
The reporter pulled, restarted, set MAX_MODEL_UPLOAD_MB and got the same
100 MB rejection, because the image never changed.
Name the service in both commands instead. Naming enables the profile
implicitly, for pull and up alike. `--profile bambu` would also work but
downloads the 220 MB Bambu image on an OrcaSlicer-only host and then
starts a sidecar the user never asked for.
Same correction in the sidecar README, the compose header and the
changelog entry, which all carried the bare form.
|
||
|
|
49dffd32fd |
Drop the restore's foreign keys in the database, not in the ORM metadata
Restoring a SQLite backup into PostgreSQL died part-way with
insert or update on table "library_files" violates foreign key
constraint "library_files_folder_id_fkey"
DETAIL: Key (folder_id)=(1) is not present in table "library_folders".
The import recreates the schema and is supposed to create every table
without foreign keys, so the order rows arrive in cannot matter; the
constraints are added back once the data has landed. Phase 1 did that by
discarding each ForeignKeyConstraint from table.constraints before
create_all -- which only suppresses the inline REFERENCES clause.
Table.foreign_key_constraints is derived from the columns' ForeignKey
objects and was never touched, and when create_all meets a dependency
cycle it cannot sort, it falls back to emitting those tables' keys as
separate ALTER TABLE ... ADD FOREIGN KEY statements read from exactly
that property.
library_files, library_folders and print_archives form such a cycle, so
twelve constraints survived across the three of them -- measured against
a real PostgreSQL by running the old phase verbatim. The same cycle also
costs those tables their place in sorted_tables, so they were imported
alphabetically, putting library_files ahead of the library_folders rows
its folder_id references.
Phase 1 now creates the tables normally and drops every foreign key from
pg_constraint afterwards, in the same transaction, scoped to contype 'f'
in the public schema. That is indifferent to how create_all chose to
emit them, so a future cycle between other tables cannot bring this
back. Phase 3 is unchanged.
This also removes a second fault: the keys were stripped from the
process-wide Base.metadata and only restored after the drop/create
transaction, so a failure in between left the running app without them
until restart. The metadata is no longer modified at all.
Verified end to end against a real PostgreSQL -- a backup whose child
rows import before their parents restores cleanly, with all 90
constraints back afterwards. Four regression tests added.
|
||
|
|
0a0c31cca0 |
Give the variant-group backfill query the nosec marker that applies
The line carried "# noqa: S608", which is ruff's flake8-bandit code -- but S is
not in ruff's select list in pyproject.toml, so ruff never ran that rule and the
marker suppressed nothing. Bandit itself only honours "# nosec", so the query
went on being reported as B608 while the line read as already handled.
The finding is a false positive. The only interpolated fragments are source_expr
and model_expr, assigned just above from a two-branch is_sqlite() check where
both branches are string literals; no caller value reaches the string. They are
JSON expressions rather than values, so a bind parameter cannot express them.
Replaces the inert marker with "# nosec B608", matching the convention already
used across the test suite, and moves the reasoning into a comment above the
statement. Bandit's medium+ count drops to 16, none of them B608.
|
||
|
|
60f8529a3c |
Recover the preview slice from custom G-code the sidecar cannot parse
Opening the slice dialog on an unsliced project runs a preview slice purely
to ask the slicer which AMS slots the chosen plate consumes. Bambu Studio 2.8
writes {if timelapse_inline_photo} into the machine's time_lapse_gcode but
exports no definition for that variable, so the template is unresolvable the
moment it leaves Studio: an older sidecar stops with a placeholder parse error
before producing any slice_info. The preview returned nothing and the caller
fell back to guessing from painted faces, silently. On the H2D project this
was found with, the guess dropped the support material -- a whole slot off a
four-filament plate.
Retry the preview once with just the named template emptied, still on the
file's own settings. Keeping the embedded settings is what keeps the answer
honest: overriding the process preset instead discards the project's support
configuration, which loses that slot and moves used_g by up to 2x. Measured
against the same file: retry reproduces all four slots gram for gram, a
printer+process override returns three.
Only templates that cannot extrude are eligible -- a start or filament-change
template lays a prime line or purges, so emptying one would move the very
grams the preview reports, and returning nothing beats a confident wrong
number. Verified on a working H2D slice that emptying time_lapse_gcode leaves
every used_g/used_m in slice_info identical.
Match on a normalised option name: the slicer reports timelapse_gcode while
the 3MF stores time_lapse_gcode, so a literal comparison finds nothing.
Decide whether a retry applies before logging, so a slice that recovers does
not announce itself at WARNING twenty seconds before it succeeds.
|
||
|
|
61b2a20f38 |
Stop offering server-side slicing for STEP files
The Slice action appeared on .step / .stp and the endpoint accepted the job,
but neither slicer can load one from its command line -- both answer
"Unknown file format. Input file must have .stl, .obj, .amf(.xml) extension."
So the file was read, converted and uploaded before failing as "The input
model file to the slicer can not be parsed", which reads as a corrupt model
rather than an unsupported format.
The endpoint refuses a STEP up front with a message saying to export it as
STL or 3MF, and the Slice and pipeline buttons no longer appear on one.
Open in Slicer is unchanged and still hands STEP to the desktop application,
which opens it fine -- that was always the working path. isSliceableFilename
(desktop) and isApiSliceableFilename (sidecar) are now separate predicates so
the two cannot drift back together.
|
||
|
|
270990faf4 |
Explain an oversized model instead of reporting a slicer crash (#2802)
The sidecar caps model uploads and reports a rejection as a bare
HTTP 500 "File too large" -- multer's MulterError is not the sidecar's
AppError, so its handler falls through to the default status. A 500
reads as a crash inside the slicer, and the one message Bambuddy had
about request size was written for the 413 a reverse proxy sends, so
it never appeared. The reporter tried MAX_FILE_SIZE, BODY_PARSER_LIMIT
and EXPRESS_PAYLOAD_LIMIT, stopped nginx, and moved from Windows to
Docker -- none of which the sidecar reads.
Match the rejection by what it says rather than by its status, so an
installation still on an older sidecar image gets the same explanation.
The 500 match is strict -- the body must be only multer's message --
because a genuine CLI failure is also a 500 and has to keep reaching
the embedded-settings fallback. Old images are told to update, since
they have no setting to change; current ones are told which one to set.
Raising SlicerInputError rather than SlicerApiServerError is also what
skips the fallback retry, which had been re-uploading the identical
oversized file after a second 25-second 3MF conversion.
Log the model size on every slice. Nothing recorded it, so a support
package from a slice that died on an upload cap looked exactly like one
that died on a bad profile, and this had to be sized by hand.
Fall back to the exception class name when a transport error stringifies
empty -- three lines of the reporter's log read "Slicer sidecar
unreachable: " and stopped there.
Needs a sidecar image update to take full effect; MAX_MODEL_UPLOAD_MB is
documented in slicer-api/.env.example.
|
||
|
|
42946df211 |
Let API clients resolve user ids to names (#1894)
Archives, the queue and statistics report ownership as a numeric
created_by_id, and statistics accept it as a filter, but nothing let an
API key discover whose id was whose -- the only user listing returns
emails, roles, group membership and full permission sets, so it is
administrative and rejects keys.
Add GET /users/slim returning id + username only, gated on a new
users:read_slim permission mapped to can_read_status. That grants no
data a key could not already reach: for API-keyed requests the
permission deps return None as current_user, so the stats:filter_by_user
guard short-circuits and ?created_by_id=N is already honoured for every
N. What was missing was the ability to address the filter, not
permission to use it. The full listing stays unmapped = admin-only.
Also fix /auth/me, which answered an API key with a synthetic
administrator: id 0, role admin, is_admin true and every permission in
the enum. A key cannot reach an administrative route at all, so clients
building their UI from that response rendered actions that 403 on use.
It now reports the key owner's identity, is_admin false, and the
permissions the key's scopes actually admit. Ownerless legacy keys keep
id 0 but no longer claim admin.
---
Source user names from the slim listing where only names are needed (#1894)
Stats filter-by-user, the Archives print log filter, the File Manager
username autocomplete, the camera-token owner column and the Finance
member picker all render nothing but a username, but all of them read
the full user listing, which is gated on the admin-level users:read.
An operator granted stats:filter_by_user but not users:read got an
empty filter with no indication why.
Point them at /users/slim under a separate react-query key, since the
full listing shares the 'users' key and the two shapes would clobber
each other in the cache.
|
||
|
|
228ef36a84 |
Stop tearing down AMS drying for a print that cannot start (#2801)
A printer in FINISH with an unacknowledged plate and something pending in
its queue stopped and restarted drying once per scheduler tick, for as
long as the plate stayed unacknowledged. The reporter's Home Assistant
history recorded about 2000 state changes over ten days. No cycle ever
ran long enough to remove moisture, and cycles the user had started by
hand on other AMS units of the same printer were torn down with it.
Two concerns had become tangled. Plate-clear answers "is the bed ready
for the next job" and says nothing about whether the AMS may heat. The
gap between a finished print and the acknowledgment is when drying is
most useful -- the printer is free and nobody is waiting on it -- and
leaving the plate unacknowledged is also how people hold the queue by
hand, so the hold was costing them the drying it should have enabled.
Four faults, all in print_scheduler.
The "print takes priority" stop sat inside the not-idle branch. Drying is
not one of the things _is_printer_idle looks at, so stopping a cycle can
never turn a non-idle printer into an idle one: the stop was futile every
time it fired, and never fired on the dispatches where it was supposed to
mean something. It now runs when the printer is actually dispatchable,
and only where the model cannot dry through a print -- #2758 settled that
capable hardware should keep its cycle.
mid_print was inferred from busy_printers, which means "the queue could
not dispatch here this pass", not "is printing". A plate-held printer was
therefore treated as printing: the mid-print spool-protection cap
silently lowered its drying temperature, the cycle was logged as
(mid-print) in FINISH, and it bypassed the very gate meant to hold it.
busy_printers keeps its dispatch role; auto-drying now gets a narrow set
snapshotted before the item loop -- running, held post-dispatch, or
mid-upload -- and mid_print comes from the printer's own state. The
interlock comment at the seed already documented this hazard and worked
around it by staying out of the set; this generalises that instead of
adding a third special case. The other call site was already passing the
narrow set, so the wide one was the inconsistency.
_stop_drying sent a stop to every AMS reporting dry_time > 0. One
auto-dried unit was enough to kill a manual cycle on a different unit of
the same printer, contradicting the contract _sync_drying_state already
documents: the entry gate only knows about cycles Bambuddy began, so the
action must not reach past them. Consequence worth stating -- after a
restart Bambuddy cannot prove a running cycle is its own, so it leaves it
alone rather than risk stopping somebody's manual dry.
Fourth, and the reason #2770's guard did not catch this: a reading at or
below the threshold popped the unit's whole entry, ended_at included, so
the 30-minute re-arm cooldown went with it. An AMS reads higher warm than
cool, which is #2770's own finding, so a unit a point or two above the
threshold dipped below it as it cooled, wiped its history, and re-armed
immediately. Lifting a suspension now clears the judgement and keeps the
clock.
queue_drying_block changes behaviour as a result. It previously had no
effect on dispatch at all -- both branches skipped anyway, and it only
decided whether drying was needlessly killed. With the stop on the
dispatch path it now does what it says: a queued print waits for a
running cycle. Off by default.
Reported by @superflyer11, who traced both defects to the line and
brought ten days of external sensor history to date the cadence.
|
||
|
|
f648439536 |
Resolve the H2C rack nozzle at dispatch instead of letting firmware pick (#2800)
An H2C ran its startup clean and bed levelling on one hotend, switched,
and then printed several millimetres above the plate. The same job from
Bambu Studio was fine.
The H2C is the only model that mounts its nozzle from a rack of six, and
a print command names that nozzle by physical rack position -- the
firmware reports those as 16 to 21 -- not by the extruder index, 0 or 1,
every other dual-nozzle printer uses. Bambuddy only ever had a rack
position when a job arrived through the Virtual Printer, which captures
Bambu Studio's pick and replays it (#1780). Anything queued from the
library, an archive, the webhook or a slicer pipeline carried none, so
the field was omitted and the firmware chose -- and its choice need not
match what the file was sliced for.
The scheduler now derives the per-slot extruder assignment from the file
it is about to send, and the MQTT layer resolves it against the rack
position the printer reports live. Both are needed: the file knows which
side a slot prints from, only the printer knows which hotend is in the
carriage, and it can be swapped from the touchscreen between queueing a
job and printing it.
Derived at dispatch rather than at creation because that is the first
point knowing both the real printer and the real file -- an item can be
created unassigned, reassigned later, or have its file swapped for a
G-code-injected copy. One call therefore covers the print dialog, bulk
library adds, the webhook and pipeline runs, and no column is needed.
extract_nozzle_mapping_from_3mf is deliberately untouched. Its output
feeds the AMS matcher, where nozzle_id is compared against a tray's
extruder_id as a hard filter, and physical_extruder_map is what makes
that comparison correct -- on an H2D it is [1, 0] and flips the two.
Dropping the translation to suit the rack would send every dual-nozzle
AMS match to the wrong extruder. The dense per-slot form is a separate
function reusing the same output.
Nothing here can fail a dispatch. The command is built and published with
no exception handler above it, and the queue item is already committed as
printing by then, so a bad input has to degrade to "firmware picks"
rather than wedge the item. resolve_rack_nozzle_mapping validates every
input and raises nothing; an unresolvable mapping, an unparseable value
or an unknown rack position all omit the field, which is the behaviour
that existed before. Slot IDs are bounded before the dense list is built:
they come from the file, and one declaring filament id="50000000" would
otherwise allocate a fifty-million-entry list on the dispatch path.
Two things are not guessed. A job printing only from the fixed hotend is
still left to the firmware, because that nozzle's physical ID is not
confirmed by a known-good capture. And the rack is taken to feed extruder
0 from a single hardware observation -- if that is flipped, a one-sided
job matches nothing and falls back to the old behaviour, so only a job
using both nozzles at once could be harmed, which is what a second
capture needs to confirm.
Confined to the H2C throughout. Building the print command for 21 model
spellings with and without the new argument changes exactly three of them
-- H2C, O1C and O1C2. The other 18, including H2D and X2D, are identical.
Reported by @tru3l3gend, who diagnosed it on real hardware against a
working Bambu Studio dispatch, established the rack ID range and supplied
a patch.
|
||
|
|
493dadb1a8 |
Replace the embedded G-code viewer with the slicer's own renderer
Sliced files previewed through a vendored copy of PrettyGCode in an
iframe. It drew each move as a screen-space line -- a line has no
thickness in the scene, so it cannot occlude the layer behind it, which
is why prints came out stringy and shimmered where layers crossed. Being
a separate app in a frame, it could be neither themed nor translated, and
carried its own machinery for detecting a proxy refusing the embed.
Now built on libvgcode, the renderer OrcaSlicer draws its own preview
with, vendored from three-slicer (AGPL, same as us). It takes the THREE
namespace as an argument and imports nothing, so it runs on our 0.181
rather than the 0.160 its package pins.
The parser is ours; upstream renders its own kernel's output and ships no
G-code parser at all. Two things it has to get right, both found by
checking a real plate rather than assuming:
- BambuStudio does not use the OrcaSlicer/PrusaSlicer annotations. It
writes "; FEATURE:", "; LINE_WIDTH:", "; CHANGE_LAYER" and
"; Z_HEIGHT:", not ";TYPE:", ";WIDTH:" and ";LAYER_CHANGE". Reading
only the latter showed a 52-layer print as 23,165 layers in one colour,
because with no layer marker recognised every travel Z-hop split a
layer and every segment took the fallback feature.
- It emits a tenth of its moves as G2/G3 arcs -- 706 extruding ones in a
single plate. Ignoring them punched holes through curved walls and tree
supports. Arcs with no X/Y are the helical travel lift and lay down
nothing, so they interpolate as travels.
Four colour modes: filament (default, from the AMS slots the file was
sliced with), feature, layer height, line width. Speed, fan and
temperature are deliberately absent -- upstream derives those from
settings rather than the toolpath, and guesses dressed as measurements
are worse than an honest omission. The parser now carries the data to do
them properly later.
Legend entries are switches. Hiding removes the records before the mesh
is built rather than recolouring them: the shader packs colour into a
single float with no alpha, so there is no transparent to set, and
removal is the useful behaviour anyway -- a hidden support stops
occluding what it covered.
The scene is built once and only the toolpath rebuilds. Doing otherwise
constructed a new WebGLRenderer on every render, because the buildVolume
default is an object literal and so a fresh identity each time; browsers
cap live WebGL contexts and drop the oldest, which blanked the canvas
after a few interactions.
utils/framing.ts goes with the iframe, along with six now-orphaned
strings in all 13 locales. src/lib/vendor is excluded from eslint --
acting on findings in vendored code makes it impossible to re-copy on the
next upstream release.
|
||
|
|
84cd400685 |
Say why a preset's values are unavailable, not just that they are
The settings panel collapsed four causes into one message -- "the picked
preset's own values could not be read" -- with no indication of what to do
about it.
The overwhelmingly common cause has an obvious fix, and it isn't an edge
case: an install pulls its sidecar as SIDECAR_TAG:-latest regardless of
which Bambuddy channel it is on, so a current Bambuddy talking to a
sidecar that predates POST /profiles/resolve is the normal state, not a
misconfiguration. Those users would have seen an amber warning on every
slice with nothing pointing at the sidecar image.
resolve_profile now returns ResolvedProfile(values, reason) instead of
None for everything, the route passes the reason through, and the panel
picks its message from it:
sidecar_outdated -> name the fix: update the sidecar image
sidecar_unavailable -> the sidecar did not answer
not_configured -> no sidecar is configured
preset_unresolved -> the previous generic wording
A request that fails outright maps to sidecar_unavailable, since a
backend we cannot reach and a sidecar that will not answer are the same
thing from the dialog.
Every variant still ends with "anything you don't change still uses the
preset" -- that reassurance is the point of the notice, and it is true
whichever way the lookup failed.
Tests pin the distinction rather than just the happy path: a 404 and a
500 must produce different reasons, and each panel case asserts both that
its own message appears and that the "update the sidecar image" line does
not leak into the others.
|