SpoolBuddy turns a Raspberry Pi 4B with a PN5180 NFC reader and
NAU7802 scale into a filament management station that integrates
with Bambuddy via REST API and WebSocket.
Backend: SpoolBuddyDevice model, 10 REST endpoints (/spoolbuddy/*),
6 WebSocket broadcast types, background offline-detection watchdog.
RPi daemon: asyncio service with concurrent NFC polling (300ms,
MIFARE Classic + Bambu HKDF key derivation), scale reading (10 SPS,
5-sample moving average, stability detection), and 10s heartbeat
with exponential backoff reconnect.
Frontend: kiosk-optimized 1024x600 UI at /spoolbuddy with Dashboard
(live weight + NFC spool detection), AMS Overview, Inventory,
Printers, and Settings pages. useSpoolBuddyState reducer hook
driven by WebSocket CustomEvents.
i18n: all 6 locales (en, de, fr, it, ja, pt-BR).
Group toggle in inventory toolbar collapses identical unused/unassigned
spools into expandable rows/cards with count badges. Grouping key uses
material, subtype, brand, color, and label weight. Brand and subtype
fields now appear in quick-add mode as optional (no asterisk, no
validation). Quantity field restricted to quick-add mode only.
When the 3MF source file wasn't downloaded from the printer (e.g. FTP
failure on P2S), archive.file_path was null. The finish photo capture
silently skipped because it derived the save directory from file_path.
Now falls back to archive/{id}/ directory and logs a warning.
The rename endpoint updated `filename` but not `file_metadata.print_name`,
which the UI uses as the primary display name. Since print_name is extracted
from the 3MF at upload, it always took precedence over the renamed filename.
Now also updates print_name in file metadata when present.
The MQTT "fun" field is an integer in the JSON payload, but the
parser used int(value, 16) which requires a string. This raised
TypeError on every message, silently swallowed by the except clause,
so developer_mode was never set.
Two bugs caused spool assignments to always configure AMS slots with
generic Bambu filament IDs (e.g. GFB99 "Generic ABS") instead of the
spool's actual slicer preset:
1. PFUS* IDs (cloud-synced custom presets) were blanket-rejected and
replaced with generic IDs in both assign_spool and configure_ams_slot
2. Generic fallback IDs (GFB99, GFL99, etc.) were treated as "good"
presets by the slot-reuse logic, making them sticky once set
New priority: spool's own slicer_filament > slot's non-generic preset
(same material) > generic fallback.
Parse the MQTT "fun" field bit 0x20000000 to detect whether connected
printers have Developer LAN Mode enabled. Show a persistent orange
warning banner when any printer lacks it, since newer firmware silently
rejects MQTT write commands without developer mode.
- Parse fun field into developer_mode on PrinterState
- Add /printers/developer-mode-warnings lightweight polling endpoint
- Include developer_mode in printer status API and support bundle
- Orange banner with affected printer names and wiki link
- Translations for all 6 locales (en, de, fr, it, ja, pt-BR)
- 7 backend + 4 frontend tests
When FTP download of a 3MF fails (e.g. BambuStudio-initiated prints),
the fallback archive has file_path="". Path.exists() on
settings.base_dir / "" resolves to the base directory itself and
returns True, so ZipFile() then fails with [Errno 21] Is a directory.
Replace .exists() with .is_file() across all 15 archive route checks
and 1 in main.py. Add file_path truthiness guard for finish photo
capture to prevent saving photos under the base directory.
P2S firmware sends local slot IDs (0-3) in tray_now, not global tray
IDs, when multiple AMS units are connected. The UI highlighted the
wrong AMS unit (e.g., AMS-A instead of AMS-B). Usage tracking was
unaffected because it uses the MQTT mapping field.
Added disambiguation in _handle_ams_data for single-nozzle printers
with ams_exist_bits indicating >1 AMS: cross-references tray_now with
the MQTT mapping field (snow-encoded) to resolve the correct global
tray ID. Falls back to raw value when no mapping is available or
ambiguous. No behavior change for single-AMS printers or dual-nozzle
(H2D) printers.
Add Quick Add mode to the spool form for simplified entry (material +
color + weight only), and a quantity field (1-100) for creating multiple
identical spools at once. Stock spools (no slicer profile) are computed
rather than stored — any spool without slicer_filament shows an amber
"Stock" badge. New filter chips (All/Stock/Configured) on the inventory
page. Backend bulk endpoint creates N spools in one transaction.
Backend:
- SpoolBulkCreate schema with quantity validation (1-100)
- POST /inventory/spools/bulk endpoint
Frontend:
- Quick Add toggle in SpoolFormModal (create mode only)
- Quantity field in FilamentSection (both modes)
- bulkCreateSpools API method and bulkCreateMutation
- Stock column (hidden by default) and stock filter chips
- Inline error rendering moved into FilamentSection
- CellCtx extended with t() for translatable cell content
Tests:
- 13 backend tests (schema validation + endpoint logic)
- 10 frontend tests (validateForm quickAdd + UI behavior)
i18n: 6 locales (en, de, fr, it, ja, pt-BR)
Docs: CHANGELOG, README, website features, wiki inventory
Three bugs allowed uploaded gcode files to survive on the printer's SD
card, causing firmware restarts to auto-start old prints hours later.
1. The post-print cleanup retry loop always broke after attempt 1
regardless of delete success — delete_file_async returns False on
failure instead of raising, so the except-based retry never fired.
2. When start_print() failed after uploading, the file was never cleaned
up since on_print_complete never fires for prints that never started.
3. Cleanup failures were logged at DEBUG level, invisible in production.
When a spool runs empty mid-print and the AMS auto-switches to a backup,
the entire print weight was attributed to the original spool. Track tray
changes during the print via tray_change_log on PrinterState, then split
the 3MF weight across trays using per-layer gcode segment boundaries at
print completion. Falls back to linear layer-ratio splitting when gcode
data is unavailable.
When a print was started from BambuStudio (not Bambuddy), the
auto-archive has an empty file_path. The photo save code uses
base_dir / Path("").parent which resolves correctly, but the
serve/upload/delete endpoints used (base_dir / "").parent which
goes one directory level too high — returning 404 despite the
photo existing on disk.
ntfy servers with attachments disabled reject thumbnail uploads with
"attachments not allowed" (HTTP 400 / code 40014), causing the entire
notification to fail. Now automatically retries without the image so
the notification still gets delivered.
Infrastructure/auth HMS error codes like 0500_0007 (MQTT command
verification failed) triggered false-alarm printer error notifications
even though they don't indicate actual print problems.
- Add suppress list for non-actionable error codes: 0500_0007 (MQTT
auth failure), 0500_4001 (cloud connection failure), 0500_400E
(print cancelled by user)
- Skip suppressed codes in the notification loop so they are still
tracked in HMS state and shown in the UI, but don't fire notifications
The log sanitizer only used regex patterns, missing arbitrary user-chosen
strings (printer names, usernames). Tasmota smart plug credentials were
logged verbatim in URLs by httpx.
- Make _sanitize_log_content() database-aware: query Printer names/serials,
User usernames, and Bambu Cloud email for exact-string replacement
(longest-first, skip <3 chars to prevent over-redaction)
- Fix serial regex leaking first 3 chars (remove capture group partial
redaction), add case-insensitive flag
- Move Tasmota credentials from URL-embedded (http://user:pass@host) to
httpx auth= parameter so they never appear in logs
- Add URL credentials regex as defense-in-depth for user:pass@ in logs
- Add 'username' and 'path' to settings sensitive_keys filter (catches
smtp_username, slicer_binary_path in support-info.json)
When sending notifications with image attachments (progress/error
events), the message body was placed in an HTTP Message header. Messages
containing newlines (e.g. "Printer: file.stl\nRemaining: 4h 52m") were
rejected by httpx since HTTP headers cannot contain newline characters.
Escape newlines to literal \n which ntfy renders as line breaks.
* feat(spool-form): enhance brand and material selection in color section
* feat(spool-form): change input field behavoiur when deleting all chars, fix scroll menu theming
* feat(spool-form): add available materials to SpoolFormModal and FilamentSection
* refactor(ColorSection): remove unnecessary hasCatalogMatch variable
* feat(SpoolFormModal): add bilateral brand and material filtering logic
* feat(ColorCatalogSettings): improve input field sizes and layout for color and material selection
* Refactor colorCatalog settings and dependencies, allow custom materials, fix ml-12 marging, show catalog colors on brand/material selection
* Refactor ColorCatalogSettings and ColorSection components; remove onBlur logic in FilamentSection
* Prioritize exact matches in search results
* Added inventory.useCustomMaterial to locales
* Remove unused filteredCatalogColors logic and update button rendering; update types file to import Printer and SpoolKProfile
* Fix indentation for useCustomMaterial
* Refactor showCatalogSection logic to only depend on matched catalog colors
* Remove extra newline before brand dropdown
* Clean up ColorSection.tsx by removing blank lines
Removed unnecessary blank lines in ColorSection.tsx.
* Ensure material property is a string in ColorSection component
---------
Co-authored-by: MartinNYHC <mz@v8w.de>
* Adding the energy cost from 2 to 3 decimal precision
* Complying with pr
* Complying with PR
* Complying with PR
* Fix energy cost display precision in ProjectDetailPage
* Change energy cost formatting to two decimal places
* Change decimal precision for energy cost display
---------
Co-authored-by: MartinNYHC <mz@v8w.de>
Two bugs in the inventory page:
1. Dates always displayed as DD/MM/YY regardless of the user's date
format setting. The page had a local formatDate() hardcoding en-GB
locale. Replaced with the shared formatDateOnly() from utils/date.ts.
2. AMS-HT slot locations showed accented characters (e.g., "Á1") instead
of proper labels (e.g., "HT-A"). The code used String.fromCharCode(65
+ ams_id), which produces garbled chars for AMS-HT IDs (128+). Now
uses the shared formatSlotLabel() from utils/amsHelpers.ts.
The post-print SD card cleanup used printer_manager.get_printer(),
which returns a PrinterInfo object with only name and serial_number.
The code then accessed .ip_address, .access_code, and .model, which
don't exist on PrinterInfo — raising AttributeError on every print
completion. The outer except Exception handler silently swallowed it
at debug level.
Result: uploaded gcode files were never deleted from the printer's
SD card root. On some printers (e.g. P1S), files left in root
auto-start on power cycle, causing phantom prints.
Replaced with a DB query for the Printer model, matching the pattern
used everywhere else in on_print_complete().
MQTT raw data returns AMS unit and tray IDs as strings (e.g. "0"),
but _build_loaded_filaments() compared them to integers without
casting. After the scheduler assigned an idle printer to an "Any
Model" job, it crashed computing AMS mapping with:
'>=' not supported between instances of 'str' and 'int'
The exception prevented the assignment from committing, so the
scheduler retried every 30 seconds in an infinite loop. The job
stayed in "Waiting" and the "Clear Plate" button kept reappearing.
Cast ams_id and tray_id to int(), matching the pattern already used
for external spool IDs on line 550.
When a spool ran empty during a print and the AMS auto-switched to a
backup, on_ams_change eagerly deleted the SpoolAssignment (fingerprint
mismatch). on_print_complete then queried the now-deleted assignment
and silently dropped usage.
Snapshot all spool assignments into PrintSession at print start. At
completion, prefer the snapshot over live DB queries in both the 3MF
primary path and the AMS remain% delta fallback. Fall back to live
query for backward compatibility with in-flight sessions started
before upgrade.