Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
0db34b8
Remove local history tracking — Spoolman-first refactor
koen01 Feb 28, 2026
ee92c85
Update repository URL in install script
koen01 Feb 28, 2026
098663c
Replace Moonraker HTTP poll with native WebSocket (ws://printer:9999)
koen01 Feb 28, 2026
3d0431e
Fix WS connect: drain initial status dump before heartbeat handshake
koen01 Feb 28, 2026
b3ab94f
Fix WS ping timeout; show brand/name/material/Spoolman on slot chips
koen01 Feb 28, 2026
51da7ca
Add RFID-based Spoolman auto-link via extra.cfs_rfid
koen01 Feb 28, 2026
7193387
Auto-unlink + re-link on RFID change for already-linked slots
koen01 Feb 28, 2026
a577197
Clear stale cfs_active_slot when no spool is selected
koen01 Feb 28, 2026
ab4bb6c
Fix Spoolman extra field format: values must be JSON-encoded strings
koen01 Feb 28, 2026
e17ddf2
Fix stale active_slot: remove JS fallback, clear bad schema default
koen01 Feb 28, 2026
84e286c
Use Optional[str] for active_slot to avoid Pydantic Literal type edge…
koen01 Mar 1, 2026
1ec3120
Fix WS drain loop stuck + protect state from being wiped on load failure
koen01 Mar 1, 2026
4df92dc
Add Moonraker job-end usage reporting; remove i18n
koen01 Mar 1, 2026
a0d699b
Show color swatch in Spoolman spool link list
koen01 Mar 1, 2026
cebd531
Redesign slot grid to Creality-style spool pod layout
koen01 Mar 1, 2026
8ea7f24
Fix WS polling loop + rebrand to CFSync
koen01 Mar 1, 2026
c2e36bf
Show printer name + firmware from WS in header subtitle
koen01 Mar 1, 2026
cfea2e6
Calculate Spoolman percent for manual (non-RFID) CFS slots
koen01 Mar 1, 2026
289b105
Add README and screenshot for CFSync
koen01 Mar 1, 2026
63158f2
Fix repo URL in update.sh to match install.sh
koen01 Mar 1, 2026
9dedad7
README: document Spoolman cfs_rfid extra field and auto-link setup
koen01 Mar 1, 2026
2ff75bc
README: broaden supported printers to all K1 series with CFS
koen01 Mar 1, 2026
d211302
Add Fluidd runout sensor panel replacement via bookmarklet
koen01 Mar 1, 2026
85a4e9e
Fix Spoolman not updated when service starts mid-print
koen01 Mar 1, 2026
c5e7d20
Add CORS middleware to allow Fluidd panel cross-origin requests
koen01 Mar 1, 2026
bd9717a
Add Spoolman link modal screenshot to README
koen01 Mar 1, 2026
955739d
Add credits section to README
koen01 Mar 1, 2026
83425a2
Fix update.sh deleting venv and failing on Debian 13
koen01 Mar 1, 2026
f3db1c9
Fix update.sh: drop --delete from rsync to preserve venv
koen01 Mar 1, 2026
84153bf
Hide material and percent on empty CFS slots
koen01 Mar 1, 2026
8ba5c45
Fix RFID auto-link writing wrong cfs_rfid to non-RFID spools
koen01 Mar 1, 2026
f076d09
Replace slot pod edit icon with Spoolman link indicator dot
koen01 Mar 1, 2026
031d2d1
Add Spoolman link to status card header
koen01 Mar 1, 2026
87afa61
Add workflow section to README with SpoolID app reference
koen01 Mar 1, 2026
c524de3
Simplify install.sh: remove unused prompts, fix config keys
koen01 Mar 2, 2026
1ad11ae
Add ws_dump.py: passive WebSocket stream dumper for protocol analysis
koen01 Mar 2, 2026
fd0c46c
Add SSH-based serialNum auto-link and fix spool swap unlink
koen01 Mar 2, 2026
079849b
Rename repo references from Filament-Management to CFSync
koen01 Mar 2, 2026
0aa960f
Update README install/update URLs to CFSync repo
koen01 Mar 2, 2026
b5e3aad
Replace SpoolID with CFTag in workflow section
koen01 Mar 2, 2026
365ca6c
Rewrite RFID auto-linking section to reflect current dual-mechanism b…
koen01 Mar 2, 2026
55c0997
Fix install/update curl commands to use correct branch URL
koen01 Mar 2, 2026
c40d385
Replace WS-snapshot attribution with real-time per-slot delta tracking
koen01 Mar 3, 2026
7a81305
Show spool fill level in icon based on percent remaining
koen01 Mar 3, 2026
457d675
Replace Spoolman Status panel with CFS Stats lifetime wear tracking
koen01 Mar 3, 2026
75843c0
Render CFS Stats boxes based on active slots from state
koen01 Mar 4, 2026
b3742aa
Use hostname and modelVersion from WS payload for page title and heading
koen01 Mar 4, 2026
89f6962
Fix printer name overlapping CFSync title
koen01 Mar 6, 2026
9a59b89
Parse initial WS burst for printer identity (hostname/modelVersion)
koen01 Mar 6, 2026
6849038
Replace logo with Creality logo
koen01 Mar 6, 2026
e432f23
Fix logo alignment - use contain instead of clipping
koen01 Mar 6, 2026
086bd92
Add CFS favicon
koen01 Mar 6, 2026
da14582
Swap logo to CFS unit image
koen01 Mar 6, 2026
21f3bd1
Show filament colors in Fluidd bookmarklet panel
koen01 Mar 7, 2026
507b2e4
Add Tampermonkey userscript generator for permanent Fluidd panel
koen01 Mar 7, 2026
026683e
Remove roll change option from filament modals
koen01 Mar 7, 2026
71ae7fa
Use Spoolman-based % calculation for RFID spools (same as manual)
koen01 Mar 9, 2026
a9cb0f5
Update CLAUDE.md to reflect current codebase state
koen01 Mar 10, 2026
df534c5
Use exec_command instead of SFTP to read material_box_info.json
koen01 Mar 12, 2026
f4fc769
Show Spoolman spool ID in linked modal info line
koen01 Mar 13, 2026
f4852cd
docker deployment
YogevBokobza Mar 13, 2026
db4405f
Update .gitignore
YogevBokobza Mar 13, 2026
2cf91ed
Update Dockerfile
YogevBokobza Mar 13, 2026
aa4a389
Update Makefile
YogevBokobza Mar 13, 2026
636a56b
fix
YogevBokobza Mar 13, 2026
de647e5
Show live remaining grams and live-adjusted percent for Spoolman-link…
koen01 Mar 14, 2026
3a65524
Add dual Spoolman sync mode: direct vs Moonraker plugin
koen01 Mar 14, 2026
e363855
Fix SET_ACTIVE_SPOOL not firing on service restart; add spoolman_mode…
koen01 Mar 14, 2026
ad4a70d
Fix syntax error in _SENTINEL assignment
koen01 Mar 14, 2026
1d7036b
Move settings into a cogwheel modal; remove inline settings card
koen01 Mar 14, 2026
dbef7a4
Add Spoolman URL setup to settings modal
koen01 Mar 14, 2026
d80cb1d
Add Moonraker Spoolman docs link in settings modal
koen01 Mar 14, 2026
a3e7c6c
Move Fluidd/Tampermonkey links to settings modal; remove footer tip
koen01 Mar 14, 2026
6880ca3
Add error logging to _moonraker_send_gcode to debug SET_ACTIVE_SPOOL …
koen01 Mar 14, 2026
fc40203
Fix _moonraker_send_gcode using requests which was never imported
koen01 Mar 14, 2026
76a0cd5
Call SET_ACTIVE_SPOOL after SSH serialNum-based spool linking
koen01 Mar 14, 2026
af0467c
Remove RFID-based Spoolman auto-link
koen01 Mar 14, 2026
b1d2fa3
Update README: sync modes, Moonraker Spoolman setup, remove RFID auto…
koen01 Mar 14, 2026
2d8a821
Handle empty spool detection and updates
davidkinnes Mar 15, 2026
7b93e1c
Reset Spoolman link when filament metadata changes on loaded slot
davidkinnes Mar 15, 2026
9eb18c6
Filter phantom CFS boxes and infer boxes only from live signals
davidkinnes Mar 15, 2026
43a2026
Update CLAUDE.md to reflect current auto-link/unlink behavior
koen01 Mar 15, 2026
810a682
fix
YogevBokobza Mar 15, 2026
b641790
fix
YogevBokobza Mar 15, 2026
17d08e1
Fix cfs_stats not updating for boxes 2-4
koen01 Mar 15, 2026
95dd972
Fix SSH auto-link skipping boxes 2-4 on startup
koen01 Mar 15, 2026
13ef2a6
Switch SSH fetch from paramiko to sshpass + system ssh
koen01 Mar 15, 2026
9ad6ce3
Try multiple SSH passwords for printer compatibility
koen01 Mar 15, 2026
51baf4b
Try both stock and K2-Improvements file paths for material_box_info.json
koen01 Mar 15, 2026
7138612
Merge branch 'koen01:spoolman' into improvements
YogevBokobza Mar 16, 2026
3b52b20
Merge PR #1: multi-printer, job history, spool reallocation, SP slot
koen01 Mar 17, 2026
ab2e42d
Add mock printer server for multi-printer testing
koen01 Mar 17, 2026
72fe575
Update CLAUDE.md and README with latest features
koen01 Mar 17, 2026
acf47ea
Show printer name in spool modal title for multi-printer setups
koen01 Mar 17, 2026
ca7ab8e
Fix background gradient stopping mid-page when scrolling
koen01 Mar 17, 2026
c0d9ff3
Request boxsInfo on connect in ws_dump.py to capture CFS slot data
koen01 Mar 18, 2026
53bf541
Merge branch 'koen01:spoolman' into improvements
YogevBokobza Mar 19, 2026
5f53bb3
fixes
YogevBokobza Mar 19, 2026
5353853
fix
YogevBokobza Mar 19, 2026
1732302
fixes
YogevBokobza Mar 19, 2026
e5d48e6
fix
YogevBokobza Mar 19, 2026
7637e05
Fix SET_ACTIVE_SPOOL blocking event loop with run_in_executor
koen01 Mar 20, 2026
f16142c
Add environmental tracking for CFS boxes and update cache version
davidkinnes Mar 20, 2026
5160b65
Fix Pydantic warning: store CfsEnvSample instances not plain dicts
koen01 Mar 20, 2026
3721ddd
Fix Fluidd panel broken by multi-printer API response shape change
koen01 Mar 20, 2026
37e6198
Add job history pagination (10 per page, up to 50 stored)
koen01 Mar 20, 2026
bc030e6
Remove Fluidd panel and Tampermonkey links from footer
koen01 Mar 20, 2026
13336e6
Merge branch 'koen01:spoolman' into improvements
YogevBokobza Mar 21, 2026
7d41718
Fix SP slot usage not synced to Spoolman when CFS is also connected
koen01 Mar 29, 2026
62e175e
Fix SP slot Moonraker mode tracking and add print state to UI
koen01 Mar 29, 2026
6fb6347
Add printer status card and camera stream to CFSync UI
koen01 Mar 30, 2026
a48beb9
Fix camera stream hiding on each render cycle
koen01 Mar 30, 2026
c0b347a
Incremental DOM rendering: eliminate full teardown on every tick
koen01 Mar 30, 2026
cff4c6b
Fix Moonraker mode spurious SET_ACTIVE_SPOOL during CFS slot transitions
koen01 Mar 30, 2026
77566c9
Merge branch 'koen01:spoolman' into improvements
YogevBokobza Mar 30, 2026
a862d04
Redesign UI to Material Design 3 dark theme
koen01 Mar 30, 2026
400d09d
Add left nav drawer; fix camera layout shift
koen01 Mar 30, 2026
11f2396
Refactor drawer to full page navigation (Dashboard / Jobs / Settings)
koen01 Mar 30, 2026
78e4312
Remove settings icon from topbar (navigation is in drawer)
koen01 Mar 30, 2026
e9d77ae
Move filament usage stats card to left column with CFS boxes
koen01 Mar 30, 2026
9545365
Add per-printer camera enable/disable in Settings
koen01 Mar 30, 2026
11db2ca
Switch UI to MD3 light theme (green/teal on white)
koen01 Mar 30, 2026
9d869b1
Darken background surface for better card contrast
koen01 Mar 30, 2026
54e32ca
Remove card-internal divider lines (MD3 Expressive style)
koen01 Mar 30, 2026
56a95b4
Tone down primary container from fluorescent mint to muted sage green
koen01 Mar 30, 2026
2440d26
Add responsive layout for tablet and phone screen sizes
koen01 Mar 30, 2026
a7cd64d
Add dark theme with settings toggle
koen01 Mar 30, 2026
b1965ff
Fix Spoolman unlink not reflecting in UI
koen01 Mar 31, 2026
ebaee1b
Keep spool modal open after unlink, show spool picker
koen01 Mar 31, 2026
f97d392
Add error handling to Spoolman unlink and await dropdown load
koen01 Mar 31, 2026
92cca4e
Fix RuntimeError when unlinking Spoolman spool from holder modal
koen01 Mar 31, 2026
58e7219
Remove RFID-based Spoolman auto-link in favour of SSH serialNum only
koen01 Mar 31, 2026
d213f43
Update README: clarify SSH-only auto-linking and CFTag workflow
koen01 Mar 31, 2026
b218ce4
Redesign CFS boxes as cards matching the rest of the UI
koen01 Mar 31, 2026
d851430
Add files via upload
koen01 Mar 31, 2026
e453f91
Fix spool holder shown as active during startup/homing/bed mesh
koen01 Mar 31, 2026
57395d4
Merge branch 'koen01:spoolman' into improvements
YogevBokobza Apr 1, 2026
a3624cb
Use atomic write for state.json to prevent corruption
koen01 Apr 4, 2026
22d00b9
Merge pull request #2 from YogevBokobza/improvements
koen01 Apr 29, 2026
ffb2541
fix: healthcheck no longer requires curl
koen01 Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PRINTER_URLS=["127.0.0.1"]
FILAMENT_DIAMETER=1.75
SPOOLMAN_URL=http://127.0.0.1:7912
SPOOLMAN_MODE=direct

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
data/
.env
54 changes: 49 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Filament-Management is a local web application for tracking 3D printer filament/spool usage, built for Creality K2 Plus CFS (4x4 slot grid) and Klipper/Moonraker-based printers. It runs as a FastAPI backend with a vanilla JavaScript SPA frontend. The UI supports German and English via `static/i18n.js` (auto-detects browser language, persists choice in localStorage).
CFSync is a local web application for tracking 3D printer filament/spool usage, built for Creality K2 Plus CFS (4x4 slot grid) and Klipper/Moonraker-based printers. It runs as a FastAPI backend with a vanilla JavaScript SPA frontend.

## Development Commands

Expand All @@ -21,30 +21,74 @@ uvicorn main:app --reload --host 0.0.0.0 --port 8000
curl http://localhost:8000/api/health
```

### Mock printer server (no real hardware needed)

`mock_printer.py` simulates one or more K2 Plus printers (WebSocket CFS server on port 9999 + Moonraker HTTP on port 7125). Run multiple instances on different loopback IPs to test multi-printer setup:

```bash
# Terminal 1
python3 mock_printer.py --host 127.0.0.2 --name "Printer Alpha"
# Terminal 2
python3 mock_printer.py --host 127.0.0.3 --name "Printer Beta"
```

Then set `"printer_urls": ["127.0.0.2", "127.0.0.3"]` in `data/config.json`.

Control API (same port 7125):
```bash
curl http://127.0.0.2:7125/mock/state
curl -X POST http://127.0.0.2:7125/mock/print/start
curl -X POST http://127.0.0.2:7125/mock/print/complete
curl -X POST http://127.0.0.2:7125/mock/slot/1A \
-H 'Content-Type: application/json' \
-d '{"state": 2, "type": "PLA", "color": "#FF0000", "name": "Red PLA", "vendor": "Bambu"}'
```

SSH auto-linking can also be tested without a real SSH server using `mock_sshpass.sh`. Copy it to `sshpass` and put it on PATH before starting CFSync:

```bash
cp mock_sshpass.sh sshpass && chmod +x sshpass
PATH="$PWD:$PATH" uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Set rfid to a plain integer matching a Spoolman spool ID to trigger auto-link:
curl -X POST http://127.0.0.2:7125/mock/slot/1A \
-H 'Content-Type: application/json' \
-d '{"state": 2, "rfid": "42"}'
```

There are no automated tests, linting tools, or CI/CD pipelines configured.

## Architecture

**Backend:** Single-file FastAPI app (`main.py`, ~1500 lines) with Pydantic models in `models/schemas.py`. Data is persisted as JSON files in `data/` (state.json, config.json, profiles.json) — no database.

**Frontend:** Vanilla JS SPA in `static/` (index.html, app.js, app.css, style.css). No build step, no framework — pure DOM manipulation.
**Frontend:** Vanilla JS SPA in `static/` (index.html, app.js, app.css, style.css). No build step, no framework — pure DOM manipulation. `fluidd-panel.js` is a standalone script injected into the Fluidd UI via bookmarklet or Tampermonkey userscript (generated from the settings page).

**Moonraker integration:** Optional async background polling loop that queries the printer's Moonraker API for print job status, filament usage, and CFS slot info. Includes Creality K2 Plus-specific object parsing (box.T1-T4, filament_rack). Printer identity (`printer_name`, `printer_firmware`) is parsed from the Moonraker WebSocket.

**Moonraker integration:** Optional async background polling loop that queries the printer's Moonraker API for print job status, filament usage, and CFS slot info. Includes Creality K2 Plus-specific object parsing (box.T1-T4, filament_rack).
**Multi-printer support:** Multiple printers are stored as separate entries under `state["printers"][printer_id]`. Each printer runs its own independent WebSocket loop and Moonraker poll loop. Config accepts `printer_urls` (array of IPs/hostnames) or the legacy `printer_url` (single), both are migrated into the same internal `printers` list at startup.

## Key Patterns

- **Pydantic v1/v2 compatibility:** Helper functions `_model_dump()`, `_model_validate()`, `_req_dump()` abstract over version differences. Always use these instead of calling `.dict()` or `.model_dump()` directly.
- **State migration:** `_migrate_state_dict()` handles legacy field names (e.g., `color` → `color_hex`, `vendor` → `manufacturer`) and older state.json formats.
- **Two API tiers:** `/api/*` returns raw JSON; `/api/ui/*` wraps responses in `{"result": {...}}` for the frontend.
- **Slot IDs:** Literal type `SlotId` = `"1A"` through `"4D"` (4 boxes × 4 colors, 16 total).
- **Slot IDs:** Literal type `SlotId` = `"1A"` through `"4D"` (4 boxes × 4 colors, 16 total) plus `"SP"` for the printer's direct spool holder (box type=1 in the WebSocket protocol).
- **Spool epochs:** Incrementing `spool_epoch` counter tracks spool changes per slot, enabling per-spool history filtering.
- **History conventions:** `_hist_push()` prepends (newest-first); `_hist_upsert_by_src()` updates existing entries by source marker during live prints.
- **Internal functions** are prefixed with `_` (e.g., `_http_get_json`, `_hist_push`).
- **Filament calculation:** grams = density × π × (diameter/2)² × length, with material-specific density from profiles.json.

## Spoolman Integration (Optional)

Set `spoolman_url` in `data/config.json` to enable. This app acts as the only bridge between Spoolman and the printer (Moonraker's Spoolman plugin is not used). Spools are linked manually via the slot modal dropdown. On link, `remaining_weight` is imported from Spoolman. Consumption is synced back via `PUT /api/v1/spool/{id}/use` (fire-and-forget) when prints finalize or manual allocations are made. Roll changes auto-unlink the Spoolman spool. All Spoolman calls are best-effort and never block local tracking.
Set `spoolman_url` in `data/config.json` to enable. This app acts as the only bridge between Spoolman and the printer (Moonraker's Spoolman plugin is not used). Spools are linked manually via the slot modal dropdown, or auto-linked via SSH serialNum lookup (see below). On link, `remaining_weight` is imported from Spoolman. Consumption is synced back via `PUT /api/v1/spool/{id}/use` (fire-and-forget) when prints finalize or manual allocations are made. Roll changes auto-unlink the Spoolman spool. All Spoolman calls are best-effort (`_spoolman_*` helpers) and never block local tracking.

**Auto-linking (SSH serialNum):** When a slot transitions to RFID state (CFS state=2), `_ssh_fetch_and_apply()` is triggered (at most every 30s per printer). It SSHes into the printer using `sshpass` + system `ssh`, tries multiple passwords (`creality_2023`, `creality_2024`, `creality`) and two file paths (stock firmware: `/usr/data/creality/userdata/box/material_box_info.json`; K2-Improvements: `/mnt/UDISK/creality/userdata/box/material_box_info.json`). Reads each slot's `serialNum` field; if it's a valid integer matching a Spoolman spool ID, that slot is auto-linked. Requires `sshpass` installed and SSH access to the printer.

**Auto-unlinking:** Spoolman is auto-unlinked when: (a) a slot transitions from RFID to any other state, (b) any loaded slot goes to empty, or (c) a loaded slot's material/name/vendor/color fingerprint changes (catches manual spool swaps).

**Spoolman API endpoints:** `GET /api/ui/spoolman/spools`, `POST /api/ui/spoolman/link`, `POST /api/ui/spoolman/unlink`, `GET /api/ui/spoolman/spool_detail`.

**Percentage calculation:** Remaining % is calculated the same way for all linked slots — using Spoolman's `remaining_weight` divided by the spool's initial weight (`filament.weight`). Cached per-slot with a 60s TTL (`_SPOOLMAN_PCT_TTL`).

## Production Deployment

Expand Down
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM python:3.12-slim AS builder

# Set working directory
WORKDIR /app

COPY . .

# Install dependencies to a specific folder
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim

# Install packages
RUN apt-get update && apt-get install -y sshpass && rm -rf /var/lib/apt/lists/*

# Set environment variables
ENV APP_DIR=/opt/filament-management \
PYTHONPATH=/opt/filament-management \
UI_PORT=8005

WORKDIR $APP_DIR

# Copy Python libs and app code
COPY --from=builder /install /usr/local
COPY --from=builder /app $APP_DIR

# Setup Entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Expose the UI port
EXPOSE $UI_PORT

ENTRYPOINT ["/entrypoint.sh"]

# Start the application
CMD uvicorn main:app --host 0.0.0.0 --port $UI_PORT

12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
up:
docker-compose up -d
down:
docker-compose down
restart:
docker-compose down && docker-compose up -d
logs:
docker-compose logs -f
pull:
docker-compose pull
build:
docker-compose build
209 changes: 202 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,209 @@
# Filament-Management
# CFSync

Local filament / spool tracking for Creality CFS & Klipper via Moonraker.
A local web dashboard for managing filament spools on **Creality K1 series printers with CFS** (Colour Filament System), including the K1, K1C, K1 Max, K1 SE, and K2 Plus. CFSync connects directly to the printer over WebSocket, reads live spool data from all CFS slots, and optionally syncs consumption back to [Spoolman](https://github.com/Donkie/Spoolman).

Track filament usage per slot, handle color changes during prints and keep everything fully local.
![CFSync screenshot](docs/screenshot.png)

No cloud. No external services.
## Features

---
- Live CFS slot view — filament colour, material, and fill level per slot
- RFID spool percent from printer sensor; calculated percent for non-RFID spools via Spoolman
- Spoolman integration — link spools, track remaining weight, auto-report usage at job end
- **Two Spoolman sync modes** — direct reporting or Moonraker plugin (`SET_ACTIVE_SPOOL`)
- Serial-number-based auto-linking via SSH (CFTag-written spools link instantly on insert)
- Moonraker job tracking — attributes `filament_used` proportionally across active slots at print completion
- **Multi-printer support** — manage multiple printers from a single CFSync instance
- **Job history** — per-printer log of recent print jobs with filament usage breakdown
- **Spool reallocation** — manually assign usage to a specific slot after a print
- **SP slot** — tracks filament loaded directly into the printer (outside the CFS)
- Printer name and firmware version shown in header (read from WebSocket)
- Dark UI, no build step, runs as a systemd service

## 🚀 Installation (One-Liner)
## Requirements

- Linux host on the same network as the printer (e.g. a Pi or the printer's companion board)
- Creality K1 series printer with CFS (K1, K1C, K1 Max, K1 SE, K2 Plus)
- Python 3.10+
- Optional: [Spoolman](https://github.com/Donkie/Spoolman) for spool tracking

## Install

```bash
curl -fsSL https://raw.githubusercontent.com/jkef80/Filament-Management/main/install.sh | sudo bash
curl -fsSL https://raw.githubusercontent.com/koen01/CFSync/refs/heads/spoolman/install.sh | sudo bash
```

The installer will prompt for:

| Prompt | Example |
|---|---|
| UI port | `8005` |
| Printer IP | `192.168.1.144` |
| Spoolman URL *(optional)* | `http://192.168.1.10:7912` |

After install, open `http://<host-ip>:<port>` in your browser.

## Configuration

All settings are available through the **cogwheel (⚙) modal** in the UI. They are stored in `data/config.json`:

```json
{
"printer_urls": ["192.168.1.144"],
"filament_diameter_mm": 1.75,
"spoolman_url": "http://192.168.1.10:7912",
"spoolman_mode": "direct"
}
```

The legacy `"printer_url"` (single string) is still accepted and migrated automatically.

### Multiple printers

Add all printer IPs to the `printer_urls` array:

```json
{
"printer_urls": ["192.168.1.144", "192.168.1.145"]
}
```

Each printer gets its own state, job history, and Spoolman spool links. The UI shows a printer selector when more than one printer is configured.

## Spoolman integration

### Sync modes

CFSync supports two ways to report filament consumption to Spoolman. Set the mode in the settings modal (⚙).

**Direct mode** *(default)*

CFSync calls `PUT /api/v1/spool/{id}/use` directly on Spoolman when a print finishes or a manual allocation is made. No Moonraker configuration needed.

**Moonraker plugin mode**

CFSync calls the `SET_ACTIVE_SPOOL` / `CLEAR_ACTIVE_SPOOL` G-code macros via Moonraker. Moonraker's built-in Spoolman plugin then tracks consumption itself. This requires the `[spoolman]` section in `moonraker.conf` — see setup below.

### Moonraker plugin setup

1. **Configure Moonraker** — add to `moonraker.conf`:

```ini
[spoolman]
server: http://192.168.1.10:7912
```

2. **Restart Moonraker** after saving.

3. **In CFSync settings (⚙)**, set Spoolman URL to the same address and switch the sync mode to **Moonraker plugin**.

> Moonraker must be reachable from the CFSync host. CFSync uses the printer IP (port 7125) for Moonraker API calls. If your setup uses a different Moonraker URL you can configure it separately in `data/config.json` under `moonraker_url`.

> **Trusted clients:** CFSync's host IP must be in Moonraker's `trusted_clients` list. The default Moonraker config already includes `192.168.0.0/16` and `10.0.0.0/8`, so most LAN setups work without changes.

### Auto-linking via serial number (SSH)

When an RFID-tagged spool is inserted (CFS slot transitions to RFID state), CFSync SSHes into the printer and reads `material_box_info.json` to extract each slot's `serialNum` field. If the serial number is a valid integer that matches a Spoolman spool ID, the slot is linked immediately — no manual action required.

**Why SSH and not the RFID material code?**
Creality uses shared RFID material-type codes — every spool of the same filament type carries the same code (e.g. all Creality Hyper PLA White rolls share one code). This makes RFID codes unsuitable for identifying individual spools in Spoolman. The `serialNum` read via SSH is written to the physical RFID chip and can be made unique per spool using **[CFTag](https://github.com/koen01/cftag)**.

**SSH requirements:**
- `sshpass` must be installed on the CFSync host (`apt install sshpass`)
- The printer must be reachable over SSH (default on Creality firmware, no extra config needed)
- CFSync tries the standard Creality passwords automatically

> Spools without a CFTag-written serial number (e.g. plain Creality stock spools) will not auto-link and must be linked manually via the slot modal.

**Auto-unlink** happens automatically when:
- A spool is removed from a slot
- The RFID value changes on a loaded slot (different spool inserted)
- The filament material, name, or colour changes while the slot is loaded

## Workflow — adding a new spool with RFID

1. **Open CFTag** on Android → fill in filament details → tap **Create in Spoolman**. CFTag creates the spool entry in Spoolman and immediately prompts you to write the RFID tags. Hold your phone to the tag on each side of the spool — done in one flow.
2. **Load the spool** into a CFS slot.
3. **CFSync detects the insert**, SSHes into the printer, reads the serial number from the chip, and links the slot to the Spoolman spool automatically.

From this point on, inserting that spool into any CFS slot will auto-link it instantly. Filament consumption is reported back to Spoolman after each print.

## Fluidd panel

CFSync can inject a spool status panel into the **Fluidd** web UI. The panel script and setup instructions are available in the settings modal (⚙) under **Fluidd panel**.

Two delivery methods are supported:

- **Bookmarklet** — paste a one-liner into your browser's bookmarks bar and click it to activate
- **Tampermonkey** — install the userscript for automatic injection on every page load

## Development & testing

CFSync includes a mock printer server for local development without real hardware.

### Mock server

`mock_printer.py` simulates a K2 Plus — it runs a WebSocket CFS server (port 9999) and a Moonraker HTTP API (port 7125). Run multiple instances on different loopback IPs to test the multi-printer setup:

```bash
# In separate terminals:
python3 mock_printer.py --host 127.0.0.2 --name "Printer Alpha"
python3 mock_printer.py --host 127.0.0.3 --name "Printer Beta"
```

Then set `"printer_urls": ["127.0.0.2", "127.0.0.3"]` in `data/config.json` and start CFSync normally. All `127.x.x.x` addresses work as loopback aliases on Linux without extra configuration.

Each mock exposes a control API for triggering state changes:

```bash
# Simulate a print job lifecycle
curl -X POST http://127.0.0.2:7125/mock/print/start
curl -X POST http://127.0.0.2:7125/mock/print/complete

# Change a CFS slot (state 2 = RFID)
curl -X POST http://127.0.0.2:7125/mock/slot/1A \
-H 'Content-Type: application/json' \
-d '{"state": 2, "type": "PLA", "color": "#FF5733", "name": "Orange PLA", "vendor": "Bambu"}'

# Empty a slot
curl -X POST http://127.0.0.2:7125/mock/slot/2C/empty

# Switch active slot
curl -X POST http://127.0.0.2:7125/mock/slot/1B/select

# View full mock state
curl http://127.0.0.2:7125/mock/state
```

### Mocking SSH auto-linking

SSH auto-linking can be tested without a real SSH server using `mock_sshpass.sh`. It intercepts the `sshpass` call and fetches `material_box_info.json` from the mock's HTTP endpoint instead:

```bash
cp mock_sshpass.sh sshpass && chmod +x sshpass
PATH="$PWD:$PATH" uvicorn main:app --reload --host 0.0.0.0 --port 8000
```

To trigger auto-linking, set a slot's RFID to a plain integer matching a Spoolman spool ID:

```bash
curl -X POST http://127.0.0.2:7125/mock/slot/1A \
-H 'Content-Type: application/json' \
-d '{"state": 2, "rfid": "42"}' # auto-links to Spoolman spool #42
```

## Update

```bash
curl -fsSL https://raw.githubusercontent.com/koen01/CFSync/refs/heads/spoolman/update.sh | sudo bash
```

## Logs

```bash
sudo journalctl -u filament-management -f
```

## Credits

- [jkef80/Filament-Management](https://github.com/jkef80/Filament-Management) — original Moonraker-based filament management that this project evolved from
- [DaviBe92/k2-websocket-re](https://github.com/DaviBe92/k2-websocket-re) — reverse-engineered Creality K2 WebSocket protocol documentation that made the CFS integration possible
23 changes: 23 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
services:
cfsync:
build: .
container_name: cfsync
restart: unless-stopped
ports:
- "8005:8005"
# Create .env file here and override the settings below (See .env.template for example)
environment:
- UI_PORT=8005
- TZ=${TZ:-Asia/Jerusalem}
- PRINTER_URLS=${PRINTER_URLS:-["127.0.0.1"]}
- FILAMENT_DIAMETER=${FILAMENT_DIAMETER:-1.75}
- SPOOLMAN_URL=${SPOOLMAN_URL:-http://127.0.0.1:7912}
- SPOOLMAN_MODE=${SPOOLMAN_MODE:-direct} # direct or moonraker
volumes:
# This ensures your filament data survives container updates
- ./data:/opt/filament-management/data
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8005/api/health')"]
interval: 30s
timeout: 10s
retries: 3
Binary file added docs/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/spoolman-link.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading