Skip to content

Support for multiple printers and the direct spool input#2

Open
davidkinnes wants to merge 95 commits intojkef80:mainfrom
davidkinnes:spoolman
Open

Support for multiple printers and the direct spool input#2
davidkinnes wants to merge 95 commits intojkef80:mainfrom
davidkinnes:spoolman

Conversation

@davidkinnes
Copy link
Copy Markdown

image

koen01 and others added 30 commits February 27, 2026 23:17
Introduce lightweight internationalization without external dependencies.
The UI auto-detects the browser language (falling back to English), and a
DE/EN toggle in the header lets users switch manually. The choice is
persisted in localStorage.

- New static/i18n.js with translation dictionaries and t() helper
- Static HTML elements use data-i18n attributes, dynamic JS strings use t()
- Language switcher styled to match existing badge/pill aesthetic
- /api/ui/help accepts ?lang=en for English help text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bridge between Spoolman spool manager and CFS slot tracking:
- Link Spoolman spools to CFS slots via modal dropdown (sorted by material/color match)
- Import remaining_weight on link, sync consumption back on print finalize and manual allocation
- Sync measured weight on "Übernehmen", auto-unlink on roll change
- All Spoolman calls are fire-and-forget — local tracking is never blocked
- Set spoolman_url in config.json to enable; hidden when unconfigured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Drop slot_history, spool_ref_*, spool_epoch_consumed_g_total,
  remaining_g, last_accounted_* from schema and state
- Remove _hist_push, _hist_upsert_by_src, _inc_slot_epoch_consumed,
  _apply_job_usage, _slot_consumed_g_epoch helpers
- Poll loop finalization now only calls _spoolman_report_usage per slot
- Remove POST /api/spool/reset, /apply_usage, /ui/slot/reset,
  /ui/spool/set_remaining endpoints
- Add idempotency guard to /api/moonraker/allocate (prevents double
  Spoolman sync); stored value drops alloc_g
- Add GET /api/ui/spoolman/spool_detail?slot= proxy endpoint (graceful
  error, never HTTP 502)
- Replace renderHistory() with fetchAndRenderSpoolmanStatus() in UI;
  right-side panel now shows live Spoolman spool status for active slot
- Remove Istgewicht/Übernehmen form from spool modal; roll-change
  increments epoch and auto-unlinks Spoolman spool
- Update i18n keys: remove history/spool-stats keys, add spoolman
  status keys (DE + EN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add websockets>=12.0 dependency
- Remove all Moonraker polling code (~490 lines): _moonraker_fetch_history,
  _moonraker_build_url, _moonraker_list_objects, _extract_cfs_slot_data,
  _walk, moonraker_poll_loop
- Add _printer_ws_url, _normalize_ws_color, _parse_ws_cfs_data,
  _ws_connect_and_run, printer_ws_loop (exponential backoff 2→60s)
- Config: printer_url replaces moonraker_url; backward compat migrates
  hostname from old moonraker_url automatically
- Spoolman usage reported incrementally via usedMaterialLength deltas
  (m → mm → g via mm_to_g); roll change clears ws_slot_length_m baseline
- Remove moonraker_history, moonraker_allocations, cfs_raw, job_track_*,
  current_job* from AppState; add ws_slot_length_m
- Remove allocation + job set/update endpoints
- Frontend: remove renderMoonHistory, captureUiState/restoreUiState,
  buildSlotIds, jobKeyFromMoon; add spoolPct percent badge on slot tiles
- i18n: remove moon.* and assign.* keys; add slot.percent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The printer pushes an unsolicited JSON status message immediately on
connect. This caused _ws_connect_and_run to receive the status dump
instead of the heartbeat "ok" reply, crashing the loop.

Fix: drain all messages (1.5s timeout loop) after connecting, then
send the heartbeat. Also downgrade unexpected heartbeat replies from
errors to warnings so a timing quirk doesn't drop the connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WS fix: disable websockets keepalive pings (ping_interval=None,
ping_timeout=None) — the printer doesn't respond to WebSocket ping
frames, causing the library to drop the connection after ~20s.

Slot chip display:
- Line 2 (.slotSub): shows "{manufacturer} {name}" when CFS/Spoolman
  data is available; falls back to "MATERIAL · #COLOR" otherwise
- Line 3 (.slotDetail): shows material type + "SP #{id}" when a
  Spoolman spool is linked
- Add .slotDetail and .spoolPct CSS rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On manual spool link: write the slot's CFS RFID to the Spoolman spool's
extra.cfs_rfid field via PATCH.

On each WS snapshot: if an RFID-tagged spool (state==2) appears on an
unlinked slot and the RFID is new since last seen, search Spoolman for a
spool with matching extra.cfs_rfid and auto-link it.

On roll change: clear the RFID cache for the slot so re-inserting any
spool (even the same one) triggers auto-link again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If the CFS RFID changes on a slot that already has a Spoolman link,
treat it as an implicit spool swap: clear the old spoolman_id and
ws_slot_length_m baseline, then try to auto-link to the new spool via
extra.cfs_rfid — same as if the slot had been unlinked first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously cfs_active_slot was only written when selected==1 was found,
so an old value (e.g. 2A from a previous session) would persist
indefinitely. Always write the value — None when the printer is idle —
so the Active section stays blank when nothing is printing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spoolman requires extra field values to be double-encoded — the value
itself must be a JSON string. Sending "06001" caused a 400 Bad Request;
the correct form is json.dumps("06001") → "\"06001\"".

Also decode the stored value (json.loads) when comparing RFID during
auto-link lookup so the match works correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The printer never sets selected==1 on any slot (all 0 in WS data), so
cfs_active_slot is always null. The JS was falling back to state.active_slot
which defaulted to "2A" from the Pydantic schema, permanently showing
"Box 2 · Slot A" as active.

- JS: remove || state.active_slot fallbacks in render() and
  fetchAndRenderSpoolmanStatus() — cfs_active_slot is the only source
- Schema: AppState.active_slot default changed from "2A" to None
- Migration: clear existing "2A" values from state.json on load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… cases

Optional[Literal[...]] = None can behave unexpectedly across Pydantic
versions. active_slot is now driven by WS only and not used by the
frontend, so the Literal constraint adds no value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drain loop: replaced per-message 1.5s timeout with a 2-second total
deadline. The printer sends status messages continuously, so the old
loop never timed out and the WS was stuck in drain forever, never
reaching printer_connected = True.

State protection: load_state() now sets _state_load_failed when it
falls back to default_state(). save_state() checks this flag and
refuses to write, preventing a failed load from wiping real spool data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace WS-delta Spoolman reporting with Moonraker-driven end-of-job
  attribution: snapshot ws_slot_length_m at job start, then at job
  complete proportionally split filament_used across linked slots using
  WS deltas, and report to Spoolman via _spoolman_report_usage()
- Add _moonraker_base_url(), _moon_report_job_usage(),
  moonraker_job_poll_loop() to main.py; launch loop in _startup()
- Remove WS Spoolman delta block from _parse_ws_cfs_data() (ws_slot_length_m
  still updated for attribution tracking)
- Delete static/i18n.js; hardcode English throughout app.js and index.html
- Remove language switcher buttons and .langSwitch/.langBtn CSS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace native <select> with a custom scrollable list so each spool
entry can display a colored swatch next to its label. Color comes from
filament.color_hex already returned by /api/ui/spoolman/spools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace flat list cards with a horizontal row per box: narrow box
header on the left (number + temp/humidity), then 4 slot pods showing
a colored SVG spool disk, material name, percent, and edit/active icon.
Empty slots render as a dark disk with a diagonal slash. Active slot
gets a green border and eye icon. All existing click/modal functionality
preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WS fix: replace drain+request/response pattern with a continuous read
loop. Old code drained 2s of real CFS data, then only read ONE message
per request cycle — causing "ok" heartbeat acks to land as the "response"
and parse-fail, while the actual boxsInfo reply went unread. New loop:
- Minimal drain (5 msgs × 0.15s) so real data is not lost
- Heartbeat handshake scans up to 10 messages for the "ok" ack
- Continuous recv() loop processes every incoming message in order
- "ok" and "heart_beat" strings handled inline, not mistaken for JSON
- Re-requests boxsInfo every 5s; re-requests on 6s recv timeout

Branding: rename to CFSync, add Creality triangle logo (logo.png),
update page title and header. FastAPI title updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add printer_name and printer_firmware fields to AppState
- Add _parse_ws_printer_info(): tries common Creality WS key names
  (machineName, softVersion, etc.) and persists any found values;
  also logs all newly-seen top-level message keys once per session
  so the exact field names can be identified from logs
- Call _parse_ws_printer_info() for every parsed JSON WS message
- Header subtitle (#printerSubtitle) updated dynamically by render()
  to show "PrinterName · FirmwareVersion"; empty until WS delivers data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RFID slots (state=2) already report a real sensor percent from the WS.
Manual slots (state=1) always report 100% from the WS (no sensor),
which is misleading. Fix:

- state=2: use WS percent as-is (unchanged)
- state=1 + Spoolman link: calculate remaining/nominal*100 from
  Spoolman filament.weight; falls back to remaining/(remaining+used)
  if nominal weight is not set; stored in _spoolman_manual_pct cache
- state=1 without link: show no percent (None)
- state=0 (empty): show no percent

_refresh_manual_slot_pcts() is an async task triggered after each
boxsInfo parse; per-slot TTL of 60s prevents hammering Spoolman.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CFSync now generates a bookmarklet (footer "Fluidd panel" link) that,
when clicked in Fluidd, replaces the Runout Sensors card with a live
CFS slot grid pulled from CFSync's /api/ui/state API.

- static/fluidd-panel.js: self-contained IIFE; finds Vuetify card by
  title text (Runout Sensors / Filament Sensor), replaces content with
  colour-coded slot grid; falls back to floating panel after 15 s;
  polls every 3 s; handles both Vuetify v2 and v3 DOM structures
- static/app.js: initFluiddBookmarklet() generates javascript: href
  with CFSYNC_URL embedded from window.location.origin; copy button
  fallback via navigator.clipboard
- static/index.html: footer bookmarklet link + copy button
- static/style.css: footer link / copy button styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes for the 'no WS slot deltas to attribute' failure:

1. Re-snapshot on next tick if WS was empty at job start
   When the service starts while a print is already running, the
   Moonraker poll may take the ws_slot_length_m snapshot before the
   WebSocket loop has received any boxsInfo data. Now we set
   _moon_snapshot_pending=True in that case and retry the snapshot
   on each subsequent 5 s tick until WS data arrives.

2. Active slot fallback when no WS deltas at completion
   If deltas are still zero at job end (snapshot was empty or WS
   never updated usedMaterialLength), fall back to attributing the
   full filament_used to the active CFS slot (cfs_active_slot /
   active_slot) if it has a Spoolman link. This covers the common
   single-spool restart-mid-print scenario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Fluidd panel bookmarklet fetches from CFSync (different host/port)
so the browser blocks it without an Access-Control-Allow-Origin header.
Allow all origins since CFSync is a local-network-only service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rsync --delete was wiping the venv/ directory because it was not in
the exclude list. Then pip ran against the system Python which is
PEP 668 externally-managed and rejects installs without a venv.

- Add --exclude "venv/" to rsync so the existing venv is preserved
- Recreate venv with python3 -m venv if it is missing (safety net)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
koen01 and others added 30 commits March 11, 2026 21:30
The WS boxsInfo payload doesn't include serialNum — it's only in the
printer's material_box_info.json file. Replace paramiko (which failed
with "EOF during negotiation" due to legacy KEX algorithm mismatch) with
sshpass + system ssh binary, which handles legacy algorithms automatically.

Also keep the WS serialNum path as a fast-path in case a future firmware
includes it in the WS payload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	main.py
#	static/app.js
#	static/fluidd-panel.js
#	static/index.html
…vements

- Introduced `/api/ui/jobs/reallocate_spool` endpoint to handle spool reallocation for completed jobs.
- Updated job history card to display detailed spool usage, including colors and link buttons.
- Added `Relink`/`Link` functionality for job spools in the frontend.
- Improved visual styling for displayed spools and job history.
- Bumped cache-busting version for updated frontend assets.
- Improved `_normalize_color_hex` to handle various printer color formats.
- Replaced `_normalize_ws_color` with `_normalize_color_hex` for consistency.
- Expanded spool metadata with normalized color, material, name, and manufacturer.
- Cached spool colors from Spoolman for improved frontend accuracy.
- Updated frontend to utilize `s.color` fallback for color display.
- Bumped cache-busting version for updated `app.js`.
- Introduced a new "Relink Job Spool" modal to streamline spool reallocation for completed jobs.
- Refactored spool list rendering into reusable `renderSpoolmanList` for improved maintainability.
- Added modal initialization logic and keyboard shortcuts for closing modal.
- Updated job history cards to open the new modal for spool linking.
- Bumped frontend cache version to ensure updated assets are loaded.
- Added `fmtDuration` function to calculate and format print duration.
- Updated job history cards to show print time alongside start and end timestamps.
- Bumped cache-busting version for updated `app.js`.
…` determination

- Added `_resolve_tracking_slot` function to centralize slot resolution logic.
- Updated filament tracking to use `_resolve_tracking_slot` for improved accuracy.
- Ensured proper handling of CFS-active and fallback slots.
- Bumped cache-busting version for `app.js` to ensure clients load updated assets.
- Enhanced spool label text to display material type, if available.
- Implemented rolling 24-hour temperature and humidity history sampling for CFS boxes.
- Added frontend modal for environmental data visualization with interactive charts.
- Enhanced UI with buttons to display historical temperature and humidity data per box.
- Updated backend schema and handling logic to support environmental data storage.
- Bumped cache-busting version for `app.js` to ensure updated assets are loaded.
- Introduced multi-range buttons (24h, 7d, 30d) in the environmental modal for selecting dataset views.
- Enhanced environmental data retention logic with bucket compaction to manage long-term history efficiently.
- Updated frontend to display range-specific aggregated data and interactive charts.
- Improved backend sampling logic to reduce memory usage for historical data.
- Updated styles and interactions for range buttons in the modal.
- Added `_write_json_atomic` function to prevent file truncation on interruptions.
- Updated `save_state_all` and `state.json` creation to use atomic writes.
- Improved error handling with recovery and logging for state load/save operations.
- Added `_moon_sync_missing_history_jobs` to recover jobs completed while offline.
- Updated job history syncing to include filament, metadata, and linkage statuses.
- Enhanced frontend to display recovered jobs with flags (e.g., "Recovered while offline").
- Introduced material and vendor filters for spool selection UI.
- Added reusable `renderSpoolmanPicker` for dropdown rendering with filters.
- Enhanced backend to provide preferred material/vendor information for slots.
- Updated styles and interactions for filter dropdowns.
- Bumped cache-busting version for `app.js` to ensure updated assets are loaded.
…ement

- Migrated job history storage from JSON to SQLite for improved scalability and query performance.
- Backfilled legacy job history into the database during startup if necessary.
- Replaced ad-hoc job history handling with `_jobdb` functions for insertion, fetching, and updating.
- Updated frontend to use database-backed job history with unlimited retention.
- Bumped cache-busting version for `app.js` to ensure updated assets are loaded.
- Introduced new `/jobs` page to display job history with filtering and pagination.
-
- Introduced `/jobs` page to display job history with advanced filtering (e.g., printer, material, date range).
- Added pagination controls for navigating through job records.
- Implemented spool usage display with material, color, and usage details.
- Integrated frontend with API-backed job history retrieval.
- Updated styles and scripts for the new page.
- Added `connectedCfsBoxesForState` utility to determine connected CFS boxes for each printer.
- Enhanced CFS badge display to list connected boxes and corresponding printers.
- Refactored CFS connectivity logic for clarity and accuracy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants