Spoolman leads CC2 Control's copies; new spools from the bridge
CI / test (push) Successful in 2m35s
CI / image (push) Successful in 42s

- Master data of CC2 Control's copies follows Spoolman (name, brand,
  colour, diameter, density, net, tare, price; not the material, which was
  fitted to the tray).
- A loaded spool's rest in CC2 Control is set to Spoolman's via
  /api/spools/adjust once everything is booked (old copy after the roll
  printed in the other printer, weigh-in in Spoolman); the resulting
  correction is not booked back.
- Copies of spools archived or deleted in Spoolman are deleted from CC2
  Control when no tray holds them (library limit 128); an outage never
  counts as deleted.
- A roll that ran out is archived in Spoolman (setting, default on); a
  scan of an archived spool maps nothing.
- A roll scanned again with unchanged tag data and put back into its old
  slot (after printing in the other printer) is recognised.
- Tag generator: "Neue Spule" creates spools in Spoolman from an existing
  or a new filament (and vendor), labelled vendor + id, then opens the tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
Jonas MenzelandClaude Opus 5.5 committed 2026-10-25 21:26:49 +01:00
1 parent d03668e3c5
commit b37cd64909
13 files changed
+873 -27

No files matched your search

+79 -3
View File
@@ -44,7 +44,7 @@ def printer_state():
PRINTERS: list[dict] = []
STATE = {"spoolman": {}, "fields": {}, "requests": []}
STATE = {"spoolman": {}, "fields": {}, "requests": [], "vendors": [], "filaments": []}
LOCK = threading.Lock()
@@ -58,7 +58,8 @@ def seed_spoolman():
"extra": {},
"filament": {"id": sid, "name": f"{mat} {'Silk Gold' if sid == 14 else color}", "material": mat,
"color_hex": color, "diameter": 1.75, "density": 1.24, "weight": 1000.0,
"settings_extruder_temp": 215, "vendor": {"id": 1, "name": vendor}},
"settings_extruder_temp": 215,
"vendor": {"id": 1 if vendor == "Sunlu" else 2 if vendor == "Elegoo" else 3, "name": vendor}},
}
@@ -134,6 +135,9 @@ def make_cc2(host_header: str, P: dict) -> FastAPI:
for k in ("name", "brand", "material", "color", "note"):
if k in f:
sp[k] = f[k]
for k in ("diameter", "density", "net", "tare", "low", "price"):
if k in f:
sp[k] = float(f[k])
if "archived" in f:
sp["archived"] = f["archived"] == "1"
return {"ok": True, "id": sp["id"]}
@@ -147,6 +151,33 @@ def make_cc2(host_header: str, P: dict) -> FastAPI:
P["spools"].append(sp)
return JSONResponse({"ok": True, "id": sp["id"]}, 201)
@app.post("/api/spools/adjust")
async def adjust(request: Request):
f = form(await request.body())
with LOCK:
STATE["requests"].append("POST adjust " + json.dumps(f))
sp = next((s for s in P["spools"] if s["id"] == f.get("id")), None)
if not sp:
return JSONResponse({"ok": False, "error": "Unknown spool"}, 404)
value = float(f["remaining"])
P["log"].insert(0, {"time": int(time.time()), "spool": sp["id"], "slot": -1, "kind": "correction",
"grams": value - sp["remaining"], "mm": 0.0, "job": "", "result": "set"})
sp["remaining"] = value
return {"ok": True, "remaining": value}
@app.post("/api/spools/delete")
async def delete(request: Request):
f = form(await request.body())
with LOCK:
STATE["requests"].append("POST delete " + json.dumps(f))
if not any(s["id"] == f.get("id") for s in P["spools"]):
return JSONResponse({"ok": False, "error": "Unknown spool"}, 404)
P["spools"][:] = [s for s in P["spools"] if s["id"] != f["id"]]
for s in P["slots"]:
if s["spool"] == f["id"]:
s["spool"] = ""
return {"ok": True}
@app.post("/api/spools/assign")
async def assign(request: Request):
f = form(await request.body())
@@ -189,7 +220,7 @@ def make_spoolman() -> FastAPI:
for key, value in (body.get("extra") or {}).items():
if value is not None and key not in STATE["fields"]:
raise HTTPException(400, f"Unknown extra field {key}")
sp.update({k: v for k, v in body.items() if k in ("location",)})
sp.update({k: v for k, v in body.items() if k in ("location", "lot_nr", "archived")})
if "used_weight" in body:
sp["used_weight"] = float(body["used_weight"])
sp["remaining_weight"] = (sp.get("initial_weight") or 1000.0) - sp["used_weight"]
@@ -201,6 +232,51 @@ def make_spoolman() -> FastAPI:
STATE["requests"].append(f"PATCH spoolman {sid} {json.dumps(body, ensure_ascii=False)}")
return sp
@app.get("/api/v1/vendor")
def vendors():
found = {s["filament"]["vendor"]["id"]: s["filament"]["vendor"] for s in STATE["spoolman"].values()}
found.update({v["id"]: v for v in STATE["vendors"]})
return list(found.values())
@app.get("/api/v1/filament")
def filaments():
found = {s["filament"]["id"]: s["filament"] for s in STATE["spoolman"].values()}
found.update({f["id"]: f for f in STATE["filaments"]})
return list(found.values())
@app.post("/api/v1/vendor")
async def add_vendor(request: Request):
body = await request.json()
with LOCK:
v = {"id": max([x["id"] for x in vendors()], default=0) + 1, **body}
STATE["vendors"].append(v)
STATE["requests"].append(f"POST vendor {json.dumps(body, ensure_ascii=False)}")
return v
@app.post("/api/v1/filament")
async def add_filament(request: Request):
body = await request.json()
with LOCK:
vendor = next(v for v in vendors() if v["id"] == body["vendor_id"])
f = {"id": max([x["id"] for x in filaments()], default=0) + 1, "vendor": vendor, **body}
STATE["filaments"].append(f)
STATE["requests"].append(f"POST filament {json.dumps(body, ensure_ascii=False)}")
return f
@app.post("/api/v1/spool")
async def add_spool(request: Request):
body = await request.json()
with LOCK:
fil = next(f for f in filaments() if f["id"] == body["filament_id"])
sid = max(STATE["spoolman"], default=0) + 1
weight = body.get("initial_weight") or fil.get("weight") or 1000.0
STATE["spoolman"][sid] = {"id": sid, "lot_nr": None, "remaining_weight": weight, "used_weight": 0.0,
"initial_weight": weight, "spool_weight": body.get("spool_weight"),
"location": body.get("location", ""), "archived": False, "price": body.get("price"),
"extra": {}, "filament": fil}
STATE["requests"].append(f"POST spool {json.dumps(body, ensure_ascii=False)}")
return STATE["spoolman"][sid]
@app.get("/api/v1/field/spool")
def fields():
return [{"key": k, "entity_type": "spool", **v} for k, v in STATE["fields"].items()]