Write NTAG213/215/216 tags for third-party spools via the SpoolBuddy
kiosk UI. New "Write" page with three workflows: existing spool, new
spool creation, and tag replacement. Backend encodes 133-byte OpenTag3D
NDEF payloads (material, color, brand, weight, temp). Daemon writes
page-by-page via PN5180 NTAG WRITE command with read-back verification.
Write commands flow through heartbeat polling with WebSocket status
updates. Includes 39 new tests and translations for all 6 languages.
- Add printer status badges to dashboard left column (green/gray
online/offline dots, compact wrapping pills, overflow-hidden)
- Remove SpoolBuddy inventory page — inventory management belongs
in the main Bambuddy frontend, not the kiosk
- Remove inventory nav item from bottom nav (now 3 tabs)
- Replace "Add to Inventory" navigate with quick-add modal that
creates a basic PLA spool entry via API, with a hint to use
the main Bambuddy UI for full spool details instead
- Redesign settings page with tabbed layout (Device, Display, Scale, Updates)
- Add screen blank timeout: blanks after touch inactivity, tap to wake
- Add CSS brightness filter for HDMI displays (no sysfs on HDMI)
- Add backend `language` field to app settings for server-side persistence
- Sync UI language from backend on kiosk load (separate Chromium instance)
- Top bar clock respects user's time format setting (system/12h/24h)
- Add SpoolBuddy settings translations for all 6 languages (en/de/fr/ja/it/pt-BR)
- Disable Chromium swipe-to-navigate in kiosk install script
- Add `video` group for DSI backlight access
1. Device tab — repo URL unlinked
Replaced the <a> tag with a <span> so the GitHub URL is displayed as plain text (no clickable link in kiosk mode).
2. Display controls — moved to frontend
Root cause: The daemon runs as a system user without Wayland access, and the kiosk uses an HDMI display (no sysfs backlight). Neither wlopm, vcgencmd, nor sysfs writes
can work in this setup.
Fix — frontend-controlled display in SpoolBuddyLayout.tsx:
- Brightness: CSS filter: brightness(X) applied to the layout root. Immediate visual dimming that works on any display type.
- Screen blanking: Full-screen black overlay (z-[9999]) shown after the configured inactivity timeout. Touch anywhere to wake.
- Activity tracking: Resets on pointerdown/keydown events AND on WebSocket-driven NFC/scale changes (weight update, tag scan).
- Queries device data every 15s to read the current brightness and blank timeout values.
Daemon display_control.py — Simplified:
- Removed vcgencmd/wlopm/subprocess usage entirely
- _blank/_unblank are now state-tracking no-ops (log only)
- DSI backlight brightness via sysfs is preserved as a bonus for DSI displays
- Updated docstring to explain the architecture
3. Scale tab — redesigned with step indicator
Idle state: Same as before — weight card with tare/calibrate buttons.
Calibration wizard: Two-column layout:
- Left column (~64px): Vertical step indicator with numbered circles, checkmark for completed steps, and a connecting line
- Right column: Live weight bar (with stable/settling indicator), step-specific content (instructions or numpad), and action buttons pinned to bottom via mt-auto
The numpad buttons are min-h-[52px] with text-lg — large enough for touch on 1024x600. The weight info card is hidden during the weight entry step to maximize vertical
space.
Root cause: The daemon used wlopm for screen blanking, but wlopm was never installed. Additionally, the daemon runs as the spoolbuddy system user which has no access to
the Wayland socket, so Wayland-based tools can't work.
Fix in display_control.py:
- Replaced wlopm --off/--on with vcgencmd display_power 0/1 — this is a Raspberry Pi firmware-level command that's pre-installed and works without Wayland socket access
- Added shutil.which("vcgencmd") check at init to avoid repeated failures on non-RPi hardware
- Added explicit PermissionError handling for brightness writes with a helpful message about the video group
Fix in install.sh:
- Added video group to the spoolbuddy service user's groups (was: gpio, spi, i2c → now: gpio, spi, i2c, video), which grants access to both vcgencmd and sysfs backlight
files
Scale Tab Numpad
Root cause: On the 1024x600 kiosk screen (~376px available content height), the weight info card + numpad + action buttons exceeded the space, causing tiny buttons and
overlapping.
Fix in SpoolBuddySettingsPage.tsx:
- Hide the weight info card during weight entry step (calStep !== 'weight'), reclaiming ~70px
- Compact inline weight reading in the step header (small dot + monospace text) so users can still see the live scale value
- Larger numpad buttons: min-h-[56px] with text-lg font size (was no min-height, text-sm)
- Added active:scale-95 for tactile touch feedback
- mt-auto on action buttons to push them to the bottom, preventing overlap
- Removed the wrapping card around the calibration flow to save vertical padding
The Raspberry Pi kiosk has no physical keyboard and system-level virtual
keyboards (squeekboard, wvkbd) don't auto-show/hide with labwc/Chromium.
Add a react-simple-keyboard QWERTY keyboard that auto-shows on input
focus, with dark theme, shift/caps/backspace, email keys (@, .), and a
two-phase close that prevents ghost-click passthrough to elements below.
Inputs with data-vkb="false" opt out (e.g. SpoolBuddySettingsPage numpad).
- Show inventory-based fill levels for non-BL spools in AMS slots by
fetching spool assignments and computing fill from weight_used/label_weight,
falling back to AMS remain when no assignment exists
- Fix ext-R slot falsely highlighted as active when idle: tray_now=255
(no tray loaded) matched ext-R's id=255 without a guard
- Improve single-slot card layout: use responsive grid aligned with AMS
card columns, fix vertical stretching with few AMS units
- Remove redundant L/R nozzle badges from ext slots (label already shows
Ext-L/Ext-R)
- Add 16 tests for ext active state logic and fill override fallback
SpoolBuddy's "Link to Spool" used the generic updateSpool API which only
set tag_uid, leaving tag_type and data_origin empty. Now uses linkTagToSpool
with tag_type='generic' and data_origin='nfc_link'.
Added a "Weight Check" inventory column (hidden by default) that compares
each spool's last scale measurement against calculated gross weight with
±50g tolerance. Shows green check for match, yellow warning + sync button
for mismatch. Backend stores last_scale_weight and last_weighed_at on each
spool when weight is synced via SpoolBuddy. Includes edge case handling
when scale weight < core weight. i18n keys added for all 6 locales.
Builds multi-arch Docker image from the current APP_VERSION in config.py
and pushes to GHCR + Docker Hub, overwriting the same beta tag. Creates
or updates a GitHub prerelease with changelog notes. Users re-pull the
tag or use Watchtower to get daily updates. Supports --parallel,
--ghcr-only, --dockerhub-only, and --skip-release flags. Documents
daily beta builds in README and DOCKERHUB.md.
Automates the full beta release workflow: version bump in config.py,
CHANGELOG date update, git commit + tag, multi-arch Docker build+push
to GHCR and Docker Hub, and GitHub prerelease creation with changelog
notes. Supports --parallel, --ghcr-only, --dockerhub-only, and
--skip-release flags. Documents daily beta builds in README and
DOCKERHUB.md.
The printer card only showed progress bar, print name, ETA, layers,
and cover image when gcode_state was RUNNING. A paused print (state
PAUSE) fell through to the idle display, hiding all print context.
Added PAUSE to the display conditions in both compact and expanded
view modes, and replaced the hardcoded "Printing" status label with
getStatusDisplay() so it correctly shows "Paused".
The assign_spool endpoint sent wrong MQTT field values for user presets,
causing the slicer's AMS slot detail card to show all fields empty.
Two bugs: (1) cloud API was called with the raw slicer_filament value
including its version suffix (e.g. PFUS9ac902733670a9_07), returning 404;
the silent fallback sent setting_id as tray_info_idx instead of the real
filament_id; (2) no SlotPresetMapping was saved after assignment.
Now strips version suffixes before cloud lookup, resolves the real
filament_id via cloud API (with local preset and generic fallbacks),
includes brand in tray_sub_brands, and saves slot preset mapping.
Print Activity now shows an hourly heatmap (hours x days) for timeframes
≤7 days and dynamically adjusts calendar months for longer ranges.
Adds hourly granularity to Filament Trends, persists timeframe selection,
fixes optional chaining in QuickStatsWidget, and fixes UTC/local timezone
mismatches in date key generation. Also hardens the /archives/slim limit
param and fixes empty query string handling in API client.
- Add lightweight GET /archives/slim endpoint with column-level SELECT
(13 fields vs 46), skipping duplicate detection for ~70-80% payload
reduction on the stats dashboard
- Add ArchiveSlim Pydantic schema and TypeScript type
- Switch StatsPage and FilamentTrends to use ArchiveSlim
- Fix critical bug in failure_analysis.py: use effective_days for week
count, separate non-date filters, build fresh week_filter per loop
- Fix busiestDay timezone bug: parse YYYY-MM-DD with split() + local
Date constructor instead of new Date() which creates UTC midnight
- Fix success streak ordering: sort by completed_at || created_at
instead of created_at alone
- Add 6 integration tests for /archives/slim endpoint
Use MetricToggle component with new exclude prop instead of inline
buttons, default to weight-first order, and redesign chart layout
with centered donut, total label, and 2-column legend grid.
Add Print Time of Day, Color Distribution, and Records widgets to the
Stats page. Extract shared MetricToggle component for consistent
weight/prints/time toggles. Add date range filtering to archives and
stats API endpoints. Reorganize widgets into parent cards (Printer Stats
and Filament Trends) and fix Dashboard layout persistence for new widgets.
_get_mac_id() used Path.iterdir() to find a network interface for the
device ID, but iteration order is non-deterministic. On different boots
it could pick eth0 (MAC …3100) or wlan0 (MAC …3102), producing a
different device_id each time. Since calibration is stored per device ID
in the backend DB, a new ID meant registering as an uncalibrated device.
Sort interfaces alphabetically so the same one is always selected.
BambuStudio uses TLS on port 3002 for certain printer models (e.g. A1
Mini / N1). The bind server only spoke plain TCP on both ports, so the
TLS ClientHello was rejected as "invalid frame" and the slicer could
never discover or connect to the virtual printer.
Port 3002 now uses TLS (reusing the VP's existing certificate), port
3000 remains plain TCP. Also updated proxy-mode to use TLSProxy for
the port 3002 bind proxy instead of raw TCPProxy.
- Replace TagDetectedModal with inline SpoolInfoCard/UnknownTagCard
in dashboard right panel (known spools show assign/sync/close,
unknown tags show add-to-inventory/link/close)
- Rewrite AssignToAmsModal as full-screen overlay reusing AmsUnitCard,
with AMS-HT and external slot support, single assignSpool API call
- Remove printer selector from assign modal (uses top bar selection)
- Extract filament_id <-> setting_id conversion to shared utility
(backend/app/utils/filament_ids.py), used by inventory + cloud routes
- Normalize slicer_filament in assign_spool to derive proper
tray_info_idx and setting_id for MQTT (was sending setting_id="")
- Rename SpoolBuddy top bar status label "Online" -> "Backend"
- Remove weightStable guard from sync weight button
The MQTT completion handler stored "aborted" as the queue item status
when a print was cancelled mid-print, but the response schema only
allows "cancelled". Pydantic validation failed on the invalid status
when listing all queue items, returning 500. Filtering by specific
status excluded the bad row so those still worked.
Normalise "aborted" → "cancelled" before storing. A startup fixup
also converts any existing "aborted" rows in the database.
sync_from_db() skipped VPs already in self._instances without checking
if their config had changed. Mode, model, access code, bind IP, remote
interface IP, and target printer changes were silently ignored until
manual toggle off/on or full restart. Now detects config drift and
restarts affected instances.
Four support package improvements:
1. Mask first two octets of subnet IPs in support info
(192.168.1.0/24 → x.x.1.0/24) to avoid leaking private network
addresses.
2. Fix Docker network_mode_hint detection. The old heuristic
(interface count > 2) always reported "bridge" on single-NIC
hosts because get_network_interfaces() excludes Docker
interfaces. Now checks for docker0/br-*/veth* visibility via
socket.if_nameindex() — these are only visible in host mode.
3. Parse MQTT "fun" field at top level of payload (not just inside
"print" key). Some firmware versions send it there, which
explains why developer_mode was null for most users.
4. Add virtual_printers section to support info with mode, model,
enabled/running status, and pending file count.
The Updates card on the Settings page mixed printer firmware and
Bambuddy software update toggles with no visual distinction. Now
groups them into labeled sections ("Printer Firmware" at top,
"Bambuddy Software" below) with a divider between them.
The Python hash verification in start_bambuddy.bat used a multi-line
`for /f "usebackq"` with a backtick-delimited command split across
lines. Windows CMD cannot parse line breaks inside backtick-delimited
for /f commands, causing "The syntax of the command is incorrect" at
step 1/6. Removed the entire redundant verification block — the
verify_sha256 subroutine already checks the archive against the
pinned hash. The removed block also had a secondary bug: it always
downloaded the amd64 checksum from python.org even on arm64 systems.
v0.2.1.1
Just one day ago after the last release, but I decided to make life a little better for those of you guys who are just received their new H2C :)
What happened?
Recent H2C deliveries from Bambu Lab ship with an updated internal model identifier: O1C2 instead of the previously known O1C. Bambuddy only recognized O1C, so these newer H2C units were not properly identified.
Also included some fixes.
- Fix sidebar navigation not respecting user permissions
- Fix camera button permissions & ffmpeg process leak (#550)
- Fix A1 Mini "unknown" status from non-UTF-8 MQTT payload (#549)
- Fix Windows install syntax error from LF line endings (#544)
- Fix queue badge showing on printers without matching filament (#486)
- Fix naive-vs-aware datetime crash from 0.2.1 timezone migration
Sidebar nav items (Archives, Queue, Stats, Profiles, Maintenance,
Projects, Inventory, Files) were visible to all users regardless of
role permissions — only Settings was gated. Now each item is hidden
when the user lacks the corresponding read permission. Printers
remains always visible as the home page.
Also adds missing inventory:read|create|update|delete to the frontend
Permission type (existed in backend but was absent from the frontend
type definition).
Fix camera button permissions & ffmpeg process leak (#550)
Camera button on printer card was clickable without camera:view
permission. ffmpeg processes (~240MB each) accumulated after closing
camera streams because: (1) stop endpoint called terminate() without
wait()/kill(), (2) HTTP disconnect detection only ran between frames
so was blocked when the generator was stuck on stdout read, and
(3) no mechanism caught processes orphaned by generator abandonment
or app restarts.
- Add camera:view permission check + tooltip to camera button
- Fix stop endpoint: terminate() → wait(2s) → kill() → wait()
- Add background disconnect monitor (polls every 2s, kills ffmpeg
directly on disconnect)
- Add periodic /proc scan (every 60s) that SIGKILLs any ffmpeg
with rtsps://bblp: not in an active stream
- Add noCamera i18n key to all 6 locales
- Fix camera API test mocks for async wait() and pid attribute
The H2C dual nozzle variant reports model code O1C2 via MQTT, but only
O1C was recognized. This caused the camera to use the wrong protocol
(chamber image on port 6000 instead of RTSP on port 322), producing a
reconnect loop. Added O1C2 to all model ID maps across 8 files.
Some firmware versions send MQTT payloads with non-UTF-8 bytes.
UnicodeDecodeError was uncaught, silently dropping the entire message
and leaving printer status as "unknown" with 0°C temps and no AMS.
Fall back to decode(errors="replace") to keep JSON parseable.
The start_bambuddy.bat launcher had Unix (LF) line endings. When a
user's git is configured with core.autocrlf=false or input, the file
is checked out with LF endings and cmd.exe fails with "the syntax of
the command is incorrect" before reaching the hash check.
Add .gitattributes forcing CRLF for *.bat files so they always get
correct line endings on checkout regardless of git config.
The purple queue counter badge in the printer card header used the raw
unfiltered queue item count, so it appeared on ALL printers of the same
model when a job was scheduled for "any [model]" — even printers without
the matching filament color. The PrinterQueueWidget below it (which
shows "Clear Plate & Start") already filtered by filament type + color.
Apply the same filament compatibility filter (type check + color override
check) to the badge count so it only shows on printers that can actually
run the queued jobs.