A printer kept dark between uses showed a black live view and sent black
notification photos (#1655). Settings → Camera can turn the light on for
all printers or for selected ones, for the live view, notification
photos, the finish photo and the snapshot API that Home Assistant uses,
and turns it off 15 seconds after the last use. It only turns off a
light it turned on, and switching the light by hand hands it back.
Snapshots wait an adjustable delay after the light comes on. Timelapse,
the Obico check and the in-print frame bank don't switch it, since they
would make it flash.
The camera card no longer lists every printer: external cameras are
added from a searchable printer picker and only those printers are
listed, so large farms get a short page.
The daemon took tray_uuid from tag blocks 4-5, which hold the filament
type, so every spool of one type sent the same value and the kiosk never
saved it. Read block 9, which both tags of a spool share and the AMS
reports as tray_uuid. Save it on kiosk quick-add and link in local and
Spoolman mode, and give spools added before the fix their tray_uuid when
their exact tag UID is scanned again. Drop any tray_uuid that is not a v4
UUID, so daemons that are not updated yet keep matching by tag UID.
Block 9 read based on #1200 by @Keybored02.
Co-Authored-By: Keybored02 <Keybored02@users.noreply.github.com>
After an RTSP session dropped, ffmpeg could keep writing the same JPEG
indefinitely with no connection to the printer left (two hours at ~29 fps
on a P2S), and every check that counts frames saw a healthy stream. The
stream now restarts ffmpeg after 20 s without a changed frame, on the
existing reconnect path, while viewers stay attached. If the first frame
after the restart is the same picture again, the camera really shows a
still scene (a dark, idle chamber): it is then only re-checked every
5 minutes until the picture changes.
OrcaSlicer's Sync filaments finds a slot's preset by the slot's filament
ID alone. The Configure dialog looked up an Orca profile's ID in the
browser and quietly sent the generic for the material when that came back
empty, so Orca custom filaments reached the slicer as Generic and the log
showed nothing. configure now resolves the ID on the server from
orca_profile_id, follows inherits to the parent profile or the Bambu
filament it was copied from, logs the outcome and reports a fallback,
which the dialog shows as a warning.
Slot presets also store the filament ID they were written with, so the
slot card and the Configure dialog stop showing a preset once the slot is
changed from OrcaSlicer's Device tab or the printer.
Re-configuring a slot for another filament no longer carries over the old
filament's active K-profile, which switched the slot back to the old
filament.
Fetch a signed feed.json from the public bambuddy-notifications repo on
GitHub at startup and every 6 hours. Nothing about the install is sent;
targeting (version, beta channel, install type) is decided locally.
- Ed25519 against a key built into the app; an older serial is refused so a
withdrawn message can't come back. The feed replaces the stored list, and
a failed or rejected fetch keeps the last good one.
- Sidebar entry above System with an unread count, a slide-over list, and a
banner for unread important/critical messages. Read state per user.
- Admins by default; Settings > General > Updates can show them to all
users or switch them off, which also stops the fetch.
- Plain text only; links to github.com and bambuddy.cool only.
ams_get_rfid was published and reported as success without reading the
printer's answer. X1Plus on base 01.08.02.00 answers FAIL / ERROR STATE,
so the user saw "Refreshing" and the K profile was re-applied to a slot
that was never read.
- Wait for the ams_get_rfid answer (matched by sequence_id).
- On a refusal from an idle X1/P1/A1, send the legacy M620 R<ams*4+slot>
gcode Bambu Studio uses for those models, AMS units 0-3 only. Never
during a job, never on newer-protocol models; printers that accept
ams_get_rfid never see it.
- Refused: the route returns 400 with the printer's reason and no PA
re-apply is scheduled. No answer still counts as accepted.
- Log the answers at INFO so refusals reach support bundles.
lldap and OpenLDAP's memberof overlay omit memberOf from "*", so every
lldap login fell through to the default group. Request memberOf by name
when the schema defines it, and on non-AD directories also search the
directory root for groupOfNames/groupOfUniqueNames entries listing the
user, since groups often sit outside the user search base and the
overlay tracks only one group class.
Also: skip ldap3's anonymous schema read after StartTLS, which AD and
Samba AD reject, so StartTLS works there; reword a server's StartTLS
refusal with an LDAPS hint; stop the bundle sanitizer masking part of
an OID as an IP; skip the sync right after auto-provisioning so the
default-group warning logs once.
Spoolman keeps a full spool's net weight on the spool (initial_weight)
and falls back to the filament's weight, but Bambuddy read only the
filament, so a 250 g spool of a 1000 g filament showed and synced as
1000 g. The list, weigh, AMS sync, SpoolBuddy scale, remain-% tracking,
fill bar and cost now share one lookup. Create writes initial_weight,
and a label-weight edit writes it instead of patching or duplicating the
filament. The spool form's cost per kg is converted at the spool's size
to and from Spoolman's per-spool price.
Spoolman resolves a spool's tare from the spool, then the filament, then
the vendor's empty_spool_weight. Bambuddy skipped the vendor and fell
back to 250 g, so weighing a spool whose tare was set only on its vendor
gave the wrong remaining weight. The inventory weigh action, the
SpoolBuddy scale and the displayed core weight now share one lookup, and
"keep old weight" on a filament change stamps a vendor-inherited tare.
A PUT /settings with null for a number setting stored the literal
"None". From then on int()/float() raised inside the response builder,
and every settings read returned 503 until the row was fixed by hand.
Explicit null for any boolean or numeric setting is now refused with a
422 that names the keys, and nothing in that request is saved. A numeric
row that does not parse reads back as its default with a warning, so an
install that already stored one recovers. The typed-key lists moved to
module constants so the save check and the read path share them.
POST /notifications/app-message delivers an app's message to every channel
with the new "Messages from connected apps" switch on (off by default),
through quiet hours, the digest and the log. API keys need the new "Send
notifications" permission, and their owner notifications:update; plain text,
http(s) links, 20 messages a minute per key. The electricity-price door and
this one now share one scoped-key check. /queue?batch=<id> opens and
highlights one batch order.
Minimal OAuth 2.0 authorization-code flow with PKCE (S256): admins register
an app with one exact callback URL (Settings > API Keys > Connected Apps);
/connect/authorize asks for consent once and returns a single-use, 60 s code
bound to app, callback and challenge; POST /api/v1/connect/token swaps it,
with the client secret, for the user's identity and permissions. Codes and
secrets stored hashed, exchanges rate-limited per client and IP, no redirect
before the callback is validated, API keys cannot authorize, refused while
auth is disabled. i18n for all 15 locales.
-----
fix(db): upgrading from 0.2.4.0 or older no longer crashes at startup
The #2974 failure-reason conversion ran before the #1378 migration that adds
print_log_entries.failure_reason, so older databases stopped with "no such
column: failure_reason". It now skips a table without the column, only runs
where a legacy label exists, and on SQLite rebuilds archive_fts first, since
archives created before that index existed trip "database disk image is
malformed" when updated.
POST /queue/batches accepts external_source + external_ref; both are
returned on every batch and filterable on GET /queue/batches. The pair is
unique (index uq_print_batches_external), so a retried create answers 409
instead of queueing the same order twice. Migration covers SQLite and
PostgreSQL.
Follow-ups after merging PR #3128 (with the #2990 preview work):
- PDF preview failed on every browser without
Map.prototype.getOrInsertComputed (Chrome < 145, Firefox < 144,
older Safari): "This file cannot be previewed" for any PDF. Load
pdf.js's legacy build, which bundles the polyfills for page and
worker, and bundle the worker through Vite (?worker&url) so
build.target lowers its class static block for Safari 16.0-16.3.
The browser-baseline check only scanned .js output and never saw
the copied .mjs worker; it scans .mjs too.
- PDF grid thumbnails only existed after someone opened the preview.
Page one is now rendered server-side with PDFium (pypdfium2, new
dependency, prebuilt wheels for every shipped platform) on upload,
ZIP extraction and external-folder scans; Generate Thumbnails
backfills PDFs as well. Renders are serialised behind a lock
(PDFium is not thread-safe), run off the event loop, and scale
from the page size so a huge MediaBox cannot allocate a huge
bitmap. Unreadable PDFs land without a thumbnail and keep the
browser fallback.
- A large STEP file takes over a minute to mesh in the browser and
showed only a spinner. STEP loads now show "Converting STEP
model... N s" and a note that large files can take a minute or
more, in all 15 locales.
- Drop the two occt-import-js "externalized for browser
compatibility" build warnings (path/crypto are only required in
its Node branch); any other externalization still shows.
Bambu sends two humidity fields that are not the same quantity.
humidity_raw is relative humidity in percent; humidity is a 1-5 drop
index, and it runs the other way -- OpenBambuAPI's push_info sample
pairs "humidity:30%" with "humidity_idx:4", so a high index means dry
where a high percentage means wet.
Four call sites used the index whenever no percentage arrived. A unit
sending only the index therefore rendered as "2%" in the green band
while being the second-wettest of the five steps, charted an average of
index values as a percentage, and sat under every humidity threshold
forever, since no index can reach one -- the alarm and auto-drying could
not fire for such a unit at all.
- utils/ams_humidity: one leaf helper, a percentage or None. The index
is never converted; None is what every caller already handles.
- routes/printers, printer_manager, print_scheduler, main, bambu_mqtt:
all five readings go through it, so the card, the websocket, the
chart, the alarm and auto-drying cannot answer differently.
- main: a unit that reports the index and no usable percentage says so
once per unit in the log, with its firmware versions requested. No
supported printer is known to do this, and "known" is doing work
there -- the alternative is a card that goes blank with no trace.
Three faults found while checking what else those paths touched:
- main: humidity_raw=float(x) if x else None stored NULL for a numeric
0% while writing 0.0 to humidity on the same row.
- main: that same expression was unguarded, unlike the parse above it,
so a non-numeric humidity_raw raised inside record_ams_history and
aborted the pass for every printer, not just the one that sent it.
- routes/ams_history: the averages were tested for truthiness, so a
window averaging exactly 0 reported no average while the min and max
beside it reported 0.0.
An affected unit now reports no humidity rather than a number that means
the opposite: the indicator is hidden, the chart leaves a gap, the alarm
and auto-drying skip the unit. Temperature is untouched. Auto-drying's
outcome is unchanged either way -- an index could never cross the
threshold -- so only the intent moves.
No supported printer is known to be affected; the report came from an
install running X1Plus, which Bambuddy does not support. Verified
against 7927 recorded samples from seven AMS units including an AMS-HT:
not one used the fallback. Two percentages that did fall through to the
index no longer do -- a reading with a decimal point, and "38.0", which
int() rejected.
print_archives.library_file_id -> library_files.folder_id ->
library_folders.archive_id -> print_archives. Three nullable SET NULL
links, each reasonable alone, that together made a loop
metadata.sorted_tables could not sort: it dropped those edges, warned on
every backup and every restore, and could return an order placing a
child before its parent -- which once imported library_files ahead of
library_folders and killed a restore on a ForeignKeyViolation.
The restore no longer depends on that order (it strips every foreign key
before importing and adds them back after), but the backup export sorts
the same way, and the warning ends with "may raise an error in a future
release" -- which would break backup and restore on one upgrade.
Marking one edge use_alter removes it from the sort graph, not from the
database: PostgreSQL emits it as ALTER TABLE ADD CONSTRAINT, as it
already did for every constraint on these three tables, and SQLite
inlines it into CREATE TABLE, so ON DELETE SET NULL holds on both.
Verified against PostgreSQL 16 and SQLite.
The post-slice thumbnail loaded the sliced 3MF with trimesh, whose reader
mishandles the Bambu Studio / OrcaSlicer layout: for every component that
references a file in 3D/Objects/ it re-parses that file and appends all
of its meshes again. N copies of a part came back as N^2 copies of its
triangles, and each component carried every other part of the same file.
25 bins of 10k faces loaded as 6.4M faces; the render took 54 s and
8.4 GB on the event loop, and the server was OOM-killed.
Parse the 3MF directly (lxml, entities/DTD/network off, streamed and
freed element by element), walk objects, components and build items
(p:path on either) with their transforms, and keep each mesh once.
Decimate per unique mesh to its share of a face budget before placing
instances, flip winding on mirrored placements, and skip the thumbnail
above a face ceiling checked both on what decimation can reach and on
what it delivered.
Both slice routes run the render in a thread. The renderer uses
matplotlib's Figure/Agg API instead of pyplot, whose process-global
figure state let a threaded plate render and stl_thumbnail's
event-loop render lay out and close each other's figures.