Skip to content

Commit add7c60

Browse files
authored
Merge pull request #75 from Xento/main
better, logging, printhistory print status, color mistmatch detection and spool handling when no color is set
2 parents e447ed5 + 430a458 commit add7c60

30 files changed

Lines changed: 1019 additions & 7341 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,5 @@ node_modules/
143143

144144
static/prints/
145145
data/
146-
146+
logs/*.log
147+
logs/*.json

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525
{
2626
"name": "Debug MQTT Replay",
27-
"type": "python",
27+
"type": "debugpy",
2828
"request": "launch",
2929
"module": "pytest",
3030
"console": "integratedTerminal",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ SpoolMan can print QR-code stickers for every spool; follow the SpoolMan label g
180180
- set `AUTO_SPEND` to `True` if you want automatic filament usage tracking (see the AUTO SPEND notes below).
181181
- set `DISABLE_MISMATCH_WARNING` to `True` to hide mismatch warnings in the UI (mismatches are still detected and logged to `data/filament_mismatch.json`).
182182
- set `CLEAR_ASSIGNMENT_WHEN_EMPTY` to `True` if you want OpenSpoolMan to clear any SpoolMan assignment and reset the AMS tray whenever the printer reports no spool in that slot.
183+
- set `COLOR_DISTANCE_TOLERANCE` to an integer (default `40`) if you want to make the perceptual ΔE threshold for tray/spool color mismatch warnings stricter or more lenient; when either side (AMS tray or SpoolMan spool) lacks a color the warning is skipped and the UI shows "Color not set".
183184
- By default, the app reads `data/3d_printer_logs.db` for print history; override it through `OPENSPOOLMAN_PRINT_HISTORY_DB` or via the screenshot helper (which targets `data/demo.db` by default).
184185

185186
- Run SpoolMan.

agents.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# agents.md — OpenSpoolMan
2+
3+
This document is for AI coding agents (and humans) making changes to **OpenSpoolMan**.
4+
Follow it as the default “operating manual” when creating PRs.
5+
6+
## 1) Project intent (do not drift)
7+
OpenSpoolMan augments SpoolMan with Bambu Lab / AMS awareness and optional NFC workflows:
8+
- Keep all operations **local-first** (LAN where possible).
9+
- NFC is **optional**; the web UI must remain fully usable without NFC.
10+
- The system is an “adapter + UI” on top of SpoolMan, not a replacement.
11+
12+
If a proposed change alters any of these fundamentals, stop and propose it as a design discussion first.
13+
14+
---
15+
16+
## 2) Non-negotiables (hard rules)
17+
### Security & privacy
18+
- Never commit secrets (printer access codes, API keys, cookies, tokens, personal URLs).
19+
- Do not log secrets. Mask them if you must log configuration.
20+
- Treat everything coming from MQTT / HTTP as untrusted input.
21+
22+
### Backwards compatibility
23+
- Preserve existing env vars and default behaviors unless explicitly versioned.
24+
- UI behavior must remain functional for:
25+
- No NFC usage
26+
- SpoolMan available/unavailable (graceful handling)
27+
- AUTO_SPEND disabled
28+
29+
### Reliability
30+
- Network calls must have **timeouts**, error handling, and retry/backoff where appropriate.
31+
- Never introduce busy loops. Prefer event-driven updates or bounded polling.
32+
33+
---
34+
35+
## 3) Repository map (high-level)
36+
Key folders/files you will interact with:
37+
- `app.py` / `wsgi.py`: application entry points
38+
- `templates/`, `static/`: server-rendered UI assets
39+
- `mqtt_bambulab.py`: Bambu printer connectivity (LAN / MQTT)
40+
- `spoolman_client.py`, `spoolman_service.py`: SpoolMan integration layer
41+
- `filament.py`, `filament_usage_tracker.py`, `print_history.py`: domain logic
42+
- `scripts/`: helper scripts (e.g., initialization / tooling)
43+
- `data/`: runtime artifacts (DBs, mismatch logs)
44+
- `tests/`: Python tests
45+
- `e2e/`, `playwright.config.js`, `package.json`: end-to-end UI tests
46+
- `docker-compose.yaml` / `compose.yaml` / `Dockerfile`: containerization
47+
- `helm/openspoolman`: Helm chart
48+
49+
---
50+
51+
## 4) How to run locally (known-good paths)
52+
53+
### 4.1 Local Python run (development)
54+
1. Configure environment (see §5). Create `config.env` from `config.env.template` or export env vars.
55+
2. Start the server:
56+
- `python wsgi.py`
57+
58+
Notes:
59+
- Default listen port is `8001` (to avoid clashing with SpoolMan).
60+
- Depending on SSL mode and mapping you may also access `https://<host>:8443`.
61+
62+
### 4.2 Docker (deployment / reproducible dev)
63+
- Configure env vars, then:
64+
- `docker compose up -d`
65+
66+
Use `docker compose port openspoolman 8001` to see mapped host port if needed.
67+
68+
### 4.3 Kubernetes (Helm)
69+
- Use the bundled chart:
70+
- `helm dependency update helm/openspoolman`
71+
- `helm upgrade --install openspoolman helm/openspoolman -f values.yaml --namespace openspoolman --create-namespace`
72+
- Validate:
73+
- `kubectl get pods -n openspoolman`
74+
75+
---
76+
77+
## 5) Configuration contract (environment variables)
78+
### Required / core
79+
- `OPENSPOOLMAN_BASE_URL`
80+
- HTTPS URL where OpenSpoolMan is reachable
81+
- **No trailing slash**
82+
- Required for NFC writes
83+
- `PRINTER_ID`
84+
- Printer settings → Setting → Device → Printer SN
85+
- `PRINTER_ACCESS_CODE`
86+
- Setting → LAN Only Mode → Access Code
87+
- (LAN Only Mode toggle may stay off)
88+
- `PRINTER_IP`
89+
- Setting → LAN Only Mode → IP Address
90+
- `SPOOLMAN_BASE_URL`
91+
- URL of SpoolMan without trailing slash
92+
93+
### Feature toggles
94+
- `AUTO_SPEND`
95+
- `True` enables legacy slicer-estimate tracking.
96+
- `TRACK_LAYER_USAGE`
97+
- `True` switches to per-layer tracking/consumption **only if** `AUTO_SPEND=True`.
98+
- If `AUTO_SPEND=False`, tracking remains disabled regardless of `TRACK_LAYER_USAGE`.
99+
- `DISABLE_MISMATCH_WARNING`
100+
- `True` hides mismatch warnings in the UI (still detected and logged).
101+
- `CLEAR_ASSIGNMENT_WHEN_EMPTY`
102+
- `True` clears SpoolMan assignment and resets AMS tray when the printer reports an empty slot.
103+
104+
### Data sources
105+
- Print history DB default: `data/3d_printer_logs.db`
106+
- Override via: `OPENSPOOLMAN_PRINT_HISTORY_DB`
107+
- Mismatch log output: `data/filament_mismatch.json`
108+
109+
### Important operational note
110+
If you change `OPENSPOOLMAN_BASE_URL`, NFC tags must be reconfigured.
111+
112+
---
113+
114+
## 6) SpoolMan integration contract (must remain stable)
115+
### SpoolMan label workflow
116+
- SpoolMan can print QR-code labels. When using them with OpenSpoolMan:
117+
- Set SpoolMan’s base URL to OpenSpoolMan **before** generating labels
118+
- Otherwise labels point back to SpoolMan, not OpenSpoolMan
119+
120+
### Required extra fields in SpoolMan
121+
Agents must not “simplify away” these fields without an explicit migration plan.
122+
123+
Add these extra fields in SpoolMan:
124+
- Filaments:
125+
- `type` (Choice)
126+
- `nozzle_temperature` (Integer Range)
127+
- `filament_id` (Text)
128+
- Spools:
129+
- `tag` (Text)
130+
- `active_tray` (Text)
131+
132+
(Exact choice values are defined in the README; keep behavior compatible with existing installations.)
133+
134+
### Windows note (Bambu Studio)
135+
Filament IDs can be sourced from Bambu Studio’s filament base directory (see README). Do not hardcode user paths; keep it documentation-only.
136+
137+
---
138+
139+
## 7) Filament matching rules (do not regress)
140+
OpenSpoolMan matches SpoolMan spools to AMS tray metadata:
141+
- Spool `material` must match AMS `tray_type` (main type).
142+
- For Bambu filaments, AMS reports a sub-brand; it must match the spool’s sub-brand.
143+
- Model this either as:
144+
- `material = full Bambu material` (e.g., `PLA Wood`) and `type` empty, OR
145+
- `material = base` (e.g., `PLA`) and `type = add-on` (e.g., `Wood`)
146+
- Parenthesized notes in `material` are ignored during matching (e.g., `PLA CF (recycled)`).
147+
148+
If matching fails:
149+
- Prefer improving diagnostics and tooling.
150+
- The UI warning can be hidden with `DISABLE_MISMATCH_WARNING=true` but mismatches must still be logged.
151+
152+
---
153+
154+
## 8) Change workflow for agents (how to work in this repo)
155+
156+
### 8.1 Before coding
157+
1. Read `README.md` sections: installation, environment configuration, matching rules, AUTO_SPEND notes.
158+
2. Identify the minimal module(s) involved:
159+
- Printer connectivity: `mqtt_bambulab.py`
160+
- SpoolMan calls: `spoolman_client.py` / `spoolman_service.py`
161+
- Domain logic: `filament*.py`, `print_history.py`
162+
- UI: `templates/`, `static/`
163+
3. Decide whether you need:
164+
- Python tests (`tests/`)
165+
- E2E tests (`e2e/` via Playwright)
166+
167+
### 8.2 Coding standards (practical)
168+
- Keep functions small and testable.
169+
- Prefer explicit types where they improve clarity (especially for payloads).
170+
- Validate external payloads defensively (missing keys, type mismatches).
171+
- When reading runtime state (e.g., `PRINTER_STATE`, MQTT payloads), prefer accessing the original object via `.get(...)` rather than copying into temporary locals unless the value needs transformation; this keeps guard logic close to the source and avoids stale snapshots.
172+
- Avoid introducing new dependencies without a strong justification.
173+
- Keep logging structured and helpful; never leak secrets.
174+
175+
### 8.3 Testing expectations
176+
Minimum expectations before PR:
177+
- If logic changes: update/add Python tests under `tests/`.
178+
- If UI changes: ensure at least a smoke check and, when possible, run the E2E suite.
179+
- If env/config changes: update README + `config.env.template` accordingly.
180+
181+
Notes:
182+
- Python tests are configured via `pytest.ini`.
183+
- E2E is set up via `playwright.config.js` and `package.json`. Use the existing npm scripts rather than inventing new ones unless necessary.
184+
185+
### 8.4 PR checklist (agents must include in PR description)
186+
- [ ] Scope is minimal; no unrelated refactors
187+
- [ ] No secrets or sensitive values introduced
188+
- [ ] Errors handled (timeouts, retries/backoff if applicable)
189+
- [ ] Tests added/updated (or justification if not)
190+
- [ ] README/config updated if behavior or configuration changed
191+
- [ ] Docker/Helm impact considered (ports, env vars, volumes)
192+
- [ ] Filament matching rules preserved (or explicitly enhanced with tests)
193+
194+
---
195+
196+
## 9) Deployment artifacts (keep in sync)
197+
If you touch runtime behavior, check:
198+
- Docker:
199+
- `Dockerfile`
200+
- `docker-compose.yaml` / `compose.yaml` env var passing and volumes
201+
- Helm:
202+
- `helm/openspoolman` chart values and templates
203+
- Ensure env vars and defaults align with README
204+
205+
Do not silently change exposed ports or default bindings without updating:
206+
- README
207+
- Compose
208+
- Helm chart
209+
210+
---
211+
212+
## 10) Troubleshooting guidance (for maintainers and future agents)
213+
When debugging:
214+
- Confirm `SPOOLMAN_BASE_URL` and `OPENSPOOLMAN_BASE_URL` have **no trailing slash**.
215+
- Confirm printer values:
216+
- `PRINTER_IP` reachable from the OpenSpoolMan host/container
217+
- `PRINTER_ACCESS_CODE` correct
218+
- Inspect mismatch log:
219+
- `data/filament_mismatch.json`
220+
- Confirm print history DB path:
221+
- `data/3d_printer_logs.db` or `OPENSPOOLMAN_PRINT_HISTORY_DB`
222+
223+
For AUTO_SPEND / tracking:
224+
- Ensure `AUTO_SPEND=True` before expecting any tracking.
225+
- `TRACK_LAYER_USAGE=True` only matters when `AUTO_SPEND=True`.
226+
227+
---
228+
229+
## 11) What not to do (common failure modes)
230+
- Do not hardcode user-specific paths, hostnames, or ports.
231+
- Do not break “no NFC” operation.
232+
- Do not require cloud access for core workflows.
233+
- Do not change matching semantics without tests and clear migration notes.
234+
- Do not broaden logs to include access codes or private URLs.
235+
236+
---
237+
238+
## 12) AMS tray assignment behavior
239+
- Cloud prints already contain `ams_mapping` in their `project_file` payload, so OpenSpoolMan can map every logical filament to a tray immediately.
240+
- Local prints (LAN mode) do not ship `ams_mapping` upfront, so we delay applying AMS mappings until the printer reports a concrete `tray_tar` (typically during stage 4 / filament change). That’s why the MQTT log often shows `tray_tar=255` for seconds and only flips to the real tray once the tray itself is loaded.
241+
242+
---
243+
244+
## 13) When you are unsure
245+
Prefer these options, in order:
246+
1. Add instrumentation and tests rather than guessing.
247+
2. Make the smallest change that improves correctness.
248+
3. Document assumptions in the PR description and in code comments where necessary.
249+
250+
End of file.

app.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
import spoolman_client
2525
import spoolman_service
2626
import test_data
27-
from spoolman_service import augmentTrayDataWithSpoolMan, trayUid
27+
from spoolman_service import augmentTrayDataWithSpoolMan, trayUid, normalize_color_hex
28+
from logger import log
2829

2930
_TEST_PATCH_CONTEXT = None
3031
if test_data.TEST_MODE_FLAG:
@@ -101,6 +102,23 @@ def _augment_tray(spool_list, tray_data, ams_id, tray_id):
101102
spoolman_service.clear_active_spool_for_tray(ams_id, tray_id)
102103
mqtt_bambulab.clear_ams_tray_assignment(ams_id, tray_id)
103104

105+
106+
def _select_spool_color_hex(spool_data):
107+
filament = spool_data.get("filament", {})
108+
multi = filament.get("multi_color_hexes")
109+
candidate = ""
110+
111+
if multi:
112+
if isinstance(multi, (list, tuple)) and multi:
113+
candidate = multi[0]
114+
elif isinstance(multi, str):
115+
candidate = multi.split(",")[0]
116+
117+
if not candidate:
118+
candidate = filament.get("color_hex") or ""
119+
120+
return normalize_color_hex(candidate)
121+
104122
@app.route("/issue")
105123
def issue():
106124
if not mqtt_bambulab.isMqttClientConnected():
@@ -381,11 +399,11 @@ def setActiveSpool(ams_id, tray_id, spool_data):
381399
ams_message["print"]["sequence_id"] = 0
382400
ams_message["print"]["ams_id"] = int(ams_id)
383401
ams_message["print"]["tray_id"] = int(tray_id)
384-
385-
if "color_hex" in spool_data["filament"]:
386-
ams_message["print"]["tray_color"] = spool_data["filament"]["color_hex"].upper() + "FF"
402+
color_hex = _select_spool_color_hex(spool_data)
403+
if color_hex:
404+
ams_message["print"]["tray_color"] = color_hex.upper() + "FF"
387405
else:
388-
ams_message["print"]["tray_color"] = spool_data["filament"]["multi_color_hexes"].split(',')[0].upper() + "FF"
406+
ams_message["print"]["tray_color"] = ""
389407

390408
if "nozzle_temperature" in spool_data["filament"]["extra"]:
391409
nozzle_temperature_range = spool_data["filament"]["extra"]["nozzle_temperature"].strip("[]").split(",")
@@ -414,7 +432,7 @@ def setActiveSpool(ams_id, tray_id, spool_data):
414432
# ams_message["print"]["tray_sub_brands"] = filament_brand_code["sub_brand_code"]
415433
ams_message["print"]["tray_sub_brands"] = ""
416434

417-
print(ams_message)
435+
log(ams_message)
418436
mqtt_bambulab.publish(mqtt_bambulab.getMqttClient(), ams_message)
419437

420438
@app.route("/")
@@ -555,15 +573,35 @@ def _to_int(value):
555573
if READ_ONLY_MODE and all([ams_slot, print_id, spool_id]):
556574
return render_template('error.html', exception="Live read-only mode: updating print-to-spool assignments is disabled.")
557575

576+
def _consume_for_spool(spool_id_value, grams_value=None, length_value=None):
577+
if spool_id_value is None:
578+
return
579+
if length_value is not None:
580+
spoolman_client.consumeSpool(spool_id_value, use_length=length_value)
581+
elif grams_value is not None:
582+
spoolman_client.consumeSpool(spool_id_value, use_weight=grams_value)
583+
558584
if all([ams_slot, print_id, spool_id]):
559585
filament = print_history_service.get_filament_for_slot(print_id, ams_slot)
560586
print_history_service.update_filament_spool(print_id, ams_slot, spool_id)
561587

562588
if(filament["spool_id"] != int(spool_id) and (not old_spool_id or (old_spool_id and filament["spool_id"] == int(old_spool_id)))):
563-
if old_spool_id and int(old_spool_id) != -1:
564-
spoolman_client.consumeSpool(old_spool_id, filament["grams_used"] * -1)
589+
grams_used = _to_float(filament.get("grams_used"))
590+
length_used = _to_float(filament.get("length_used"))
591+
use_length = length_used is not None and length_used > 0
565592

566-
spoolman_client.consumeSpool(spool_id, filament["grams_used"])
593+
if old_spool_id and int(old_spool_id) != -1:
594+
_consume_for_spool(
595+
old_spool_id,
596+
grams_value=-(grams_used or 0),
597+
length_value=-(length_used or 0) if use_length else None,
598+
)
599+
600+
_consume_for_spool(
601+
spool_id,
602+
grams_value=grams_used,
603+
length_value=length_used if use_length else None,
604+
)
567605

568606
prints, total_prints = print_history_service.get_prints_with_filament(limit=per_page, offset=offset)
569607
layer_tracking_map = print_history_service.get_layer_tracking_for_prints([print["id"] for print in prints])

config.env.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ AUTO_SPEND=False
99
TRACK_LAYER_USAGE=False
1010
SPOOL_SORTING=filament.material:asc,filament.vendor.name:asc,filament.name:asc
1111
CLEAR_ASSIGNMENT_WHEN_EMPTY=False
12+
COLOR_DISTANCE_TOLERANCE=40

0 commit comments

Comments
 (0)