Recognise rolls put back, use CC2 Control's log for bookings
- A roll put back into the slot it left, with the tag data unchanged, is mapped again and CC2 Control's question answered; if the touchscreen then replaces the id, it was another roll and goes to storage. - A touchscreen edit of a scanned roll keeps the mapping and re-answers CC2 Control's question so its use is counted. - Bookings name the print and its result from CC2 Control's log. - A weigh-in in CC2 Control sets Spoolman's remaining weight instead of being booked as use. - After "Take out" with the roll still in the tray, the tray's logged use is booked to that roll until it leaves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
1 parent
a9041eb538
commit
67c3b54903
7 files changed
+607
-40
No files matched your search
+245
-20
@@ -4,10 +4,16 @@ Rules:
|
||||
- The printer is only read (SSH `cat` of the canvas record). No MQTT, no printer commands.
|
||||
- Writes go to CC2 Control's spool endpoints and to Spoolman only, and only when dry-run is off.
|
||||
- Slot <-> spool mappings are kept by the bridge; a tag id vanishing from the record
|
||||
(weight falls back to 1000 after a touchscreen edit) does not drop a mapping.
|
||||
(weight falls back to 1000 after a touchscreen edit) does not drop a mapping made by a scan.
|
||||
- The record keeps a tray's tag data after the roll is taken out. A roll put back into the slot
|
||||
it left, with the record unchanged, is taken to be that roll again ("reinserted"); if the
|
||||
touchscreen then replaces the id, it was another roll and the mapping goes.
|
||||
- Several printers: one worker each; a spool is mapped to at most one slot of one printer.
|
||||
- Spoolman shows where a loaded spool is (location "<printer> Slot N", extra fields
|
||||
"drucker"/"slot"); the printer wins over manual edits there.
|
||||
- Consumption is the drop of CC2 Control's remaining weight, booked once the printer is idle.
|
||||
CC2 Control's log adds what was printed (file, result), weigh-ins (Spoolman takes the weighed
|
||||
value) and usage of a tray left without spool after "Take out" (booked to the roll still in it).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -33,7 +39,54 @@ MIN_BOOKING = 0.05 # grams; smaller changes accumulate until they reach this
|
||||
SPOOL_CACHE_SECONDS = 300
|
||||
SYNC_SECONDS = 60 # full Spoolman comparison (locations, extra fields)
|
||||
LOG_KEEP = 1000
|
||||
LOG_SEEN_KEEP = 400 # CC2 Control lists its newest 150 entries
|
||||
TAKEOUT_MARGIN = 120 # seconds: the printer's clock and ours may differ a little
|
||||
TAKEOUT_KEEP = 86400
|
||||
REANSWER_SECONDS = 60
|
||||
SOURCE_NAMES = {"ssh": "SSH", "cc2": "CC2 Control"}
|
||||
RESULTS = {
|
||||
"complete": "fertig", "completed": "fertig", "cancelled": "abgebrochen", "canceled": "abgebrochen",
|
||||
"error": "Fehler", "failed": "Fehler", "ended": "beendet", "changed": "Spule gewechselt",
|
||||
"moved": "Spule umgezogen", "removed": "entnommen", "runout": "leergelaufen", "late": "nachträglich zugeordnet",
|
||||
}
|
||||
|
||||
|
||||
def _num(value, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def log_key(e: dict) -> str:
|
||||
return f"{e.get('time')}|{e.get('slot')}|{e.get('kind')}|{e.get('spool')}|{_num(e.get('mm')):.1f}|{_num(e.get('grams')):.3f}"
|
||||
|
||||
|
||||
def job_text(e: dict) -> str:
|
||||
"""'benchy.gcode (abgebrochen)' for a CC2 Control print log entry."""
|
||||
if e.get("kind") == "runout":
|
||||
return "Rolle leergelaufen"
|
||||
job = str(e.get("job") or "").rsplit("/", 1)[-1] or "ohne Dateiname"
|
||||
result = str(e.get("result") or "")
|
||||
result = RESULTS.get(result, result)
|
||||
return f"{job} ({result})" if result else job
|
||||
|
||||
|
||||
def describe(entries: list[dict]) -> str:
|
||||
parts: list[str] = []
|
||||
for e in entries:
|
||||
if e.get("kind") in ("print", "runout"):
|
||||
text = job_text(e)
|
||||
if text not in parts:
|
||||
parts.append(text)
|
||||
if not parts:
|
||||
return ""
|
||||
more = f" und {len(parts) - 4} weitere" if len(parts) > 4 else ""
|
||||
return " – " + ", ".join(parts[:4]) + more
|
||||
|
||||
|
||||
def grams_from_mm(mm: float, diameter: float, density: float) -> float:
|
||||
return mm * 3.141592653589793 * (diameter / 2) ** 2 * density / 1000
|
||||
|
||||
|
||||
class ActionLog:
|
||||
@@ -205,8 +258,21 @@ class PrinterWorker:
|
||||
self._process_trays(cc2)
|
||||
self._process_cc2_slots(cc2)
|
||||
self._reconcile_cc2(cc2)
|
||||
self._process_cc2_log(cc2)
|
||||
self._consumption(cc2)
|
||||
|
||||
def _record_id(self, n: int) -> int | None:
|
||||
for s in self.record_slots:
|
||||
if s["slot"] == n:
|
||||
return s["spool_id"]
|
||||
return None
|
||||
|
||||
def _close_takeout(self, n: int | None = None, spool: int | None = None) -> None:
|
||||
now = self.bridge.clock()
|
||||
for w in self.st["takeout"]:
|
||||
if w.get("until") is None and (w["slot"] == n or w["spool"] == spool):
|
||||
w["until"] = now
|
||||
|
||||
# ---------- record / scans ----------
|
||||
|
||||
def _process_record(self, record: dict) -> None:
|
||||
@@ -217,6 +283,13 @@ class PrinterWorker:
|
||||
mappings = self.st["slots"]
|
||||
for s in slots:
|
||||
n, tid = s["slot"], s["spool_id"]
|
||||
if prev is not None and tid != prev[n]["spool_id"]:
|
||||
# the tray's tag data was rewritten: whatever was there before is gone
|
||||
self.st["last"].pop(str(n), None)
|
||||
self._close_takeout(n)
|
||||
m = mappings.get(str(n))
|
||||
if tid is None and m and m.get("reinserted"):
|
||||
self.bridge.release(self, n, "Tag-Daten am Display ersetzt, also eine andere Rolle ohne Spoolman-Tag")
|
||||
if tid is None:
|
||||
continue
|
||||
if prev is not None:
|
||||
@@ -271,6 +344,7 @@ class PrinterWorker:
|
||||
if st is None:
|
||||
continue
|
||||
if st == 0:
|
||||
self._close_takeout(n)
|
||||
since = self.empty_since.setdefault(n, now)
|
||||
if now - since >= self.settings.empty_grace and str(n) in self.st["slots"]:
|
||||
self.bridge.release(self, n, f"Slot seit {int(now - since)} s leer")
|
||||
@@ -287,14 +361,20 @@ class PrinterWorker:
|
||||
sid = slot_spool_id(slot)
|
||||
m = mappings.get(str(n))
|
||||
mirrored = spoolman_id_of(by_id[sid]) if sid in by_id else None
|
||||
question = str(slot.get("question") or "")
|
||||
loaded = self.tray_status.get(n) in (1, 2)
|
||||
if question:
|
||||
self._close_takeout(n) # filament went in or was changed: maybe another roll
|
||||
# A spool we already map elsewhere (fresh scan, maybe in the other printer) wins
|
||||
# over this CC2 Control's older binding.
|
||||
elsewhere = mirrored is not None and self.bridge.mapped_elsewhere(mirrored, self.id, n)
|
||||
if m is None:
|
||||
if mirrored is not None and not elsewhere and self.tray_status.get(n) in (1, 2):
|
||||
if mirrored is not None and not elsewhere and loaded:
|
||||
# assigned in CC2 Control by hand
|
||||
self.bridge.map(self, n, mirrored, by_id[sid].get("name") or str(mirrored),
|
||||
"in CC2 Control zugewiesen", sid)
|
||||
elif not sid and question == "inserted" and loaded:
|
||||
self._reinserted(n)
|
||||
continue
|
||||
if m.get("cc2") and sid == m["cc2"]:
|
||||
m["cc2_seen"] = m["assigned"] = True
|
||||
@@ -309,8 +389,44 @@ class PrinterWorker:
|
||||
"in CC2 Control zugewiesen", sid)
|
||||
elif mirrored is None:
|
||||
self.bridge.release(self, n, "in CC2 Control mit anderer Spule belegt")
|
||||
elif not slot.get("question"):
|
||||
elif not question:
|
||||
if loaded and self._record_id(n) == m["spool"]:
|
||||
# "Take out" with the roll still in the tray: Spoolman says storage, but what the
|
||||
# tray prints now is still that roll's
|
||||
self.st["takeout"].append({"slot": n, "spool": m["spool"], "name": m.get("name") or str(m["spool"]),
|
||||
"since": self.bridge.clock(), "until": None})
|
||||
self.bridge.release(self, n, "in CC2 Control herausgenommen bzw. ins Lager gelegt")
|
||||
elif loaded:
|
||||
self._answer_again(n, m, question)
|
||||
|
||||
def _reinserted(self, n: int) -> None:
|
||||
"""No mapping, CC2 Control asks about filament put in, and the record still names the roll
|
||||
that last left this slot: it is that roll again."""
|
||||
last = self.st["last"].get(str(n))
|
||||
rid = self._record_id(n)
|
||||
if not last or rid != last["spool"] or self.bridge.mapped_elsewhere(rid, self.id, n):
|
||||
return
|
||||
self.bridge.map(self, n, rid, last["name"], "wieder eingelegt, Tag-Daten unverändert")
|
||||
self.st["slots"][str(n)]["reinserted"] = True
|
||||
|
||||
def _answer_again(self, n: int, m: dict, question: str) -> None:
|
||||
"""CC2 Control unbound a mapped slot and asks which spool it is."""
|
||||
rid = self._record_id(n)
|
||||
if question == "inserted":
|
||||
if rid != m["spool"]:
|
||||
self.bridge.release(self, n, "andere Rolle eingelegt")
|
||||
return
|
||||
m["reinserted"] = True # taken out and put back without a new scan
|
||||
elif rid != m["spool"] and (rid is not None or m.get("reinserted")):
|
||||
return
|
||||
# same roll (or a touchscreen edit of it): tell CC2 Control again, at most once a minute
|
||||
if not m.get("assigned") or self.bridge.clock() - m.get("answered_at", 0) < REANSWER_SECONDS:
|
||||
return
|
||||
m["assigned"] = False
|
||||
m["answered_at"] = self.bridge.clock()
|
||||
self.bridge.dry_logged.pop(f"{self.id}:assign:{n}", None)
|
||||
self._log("info", f"Slot {n + 1}: CC2 Control fragt nach der Spule ({question}); "
|
||||
f"{m.get('name') or m['spool']} wird erneut zugewiesen")
|
||||
|
||||
def _reconcile_cc2(self, cc2: dict) -> None:
|
||||
mirrors: dict[int, dict] = {}
|
||||
@@ -347,7 +463,7 @@ class PrinterWorker:
|
||||
continue
|
||||
self.st["booked"][cc2_id] = {
|
||||
"spoolman": m["spool"], "remaining": float(fields.get("remaining", fields.get("net", 1000))),
|
||||
"pending": None, "last": None,
|
||||
"pending": None, "last": None, "log_time": 0.0,
|
||||
}
|
||||
m["cc2"] = cc2_id
|
||||
else:
|
||||
@@ -371,8 +487,13 @@ class PrinterWorker:
|
||||
|
||||
def _consumption(self, cc2: dict) -> None:
|
||||
job_active = bool((cc2.get("job") or {}).get("active"))
|
||||
if job_active:
|
||||
# a print ran with it: a later touchscreen edit is an edit of this roll
|
||||
for m in self.st["slots"].values():
|
||||
m.pop("reinserted", None)
|
||||
booked = self.st["booked"]
|
||||
now = self.bridge.clock()
|
||||
entries = [e for e in cc2.get("log") or [] if isinstance(e, dict)]
|
||||
for sp in cc2.get("spools") or []:
|
||||
smid = spoolman_id_of(sp)
|
||||
if smid is None:
|
||||
@@ -382,48 +503,63 @@ class PrinterWorker:
|
||||
rem = float(sp.get("remaining"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
name = sp.get("name") or cid
|
||||
mine = [e for e in entries if str(e.get("spool")) == cid]
|
||||
newest = max((_num(e.get("time")) for e in mine), default=0.0)
|
||||
seen = self.remaining_seen.get(cid)
|
||||
if seen is None or abs(seen[0] - rem) > 1e-6:
|
||||
self.remaining_seen[cid] = (rem, now)
|
||||
b = booked.get(cid)
|
||||
if b is None or b.get("spoolman") != smid:
|
||||
booked[cid] = {"spoolman": smid, "remaining": rem, "pending": None, "last": None}
|
||||
self._log("info", f"{sp.get('name')}: Ausgangsstand {rem:.1f} g übernommen (Spoolman {smid})")
|
||||
booked[cid] = {"spoolman": smid, "remaining": rem, "pending": None, "last": None, "log_time": newest}
|
||||
self._log("info", f"{name}: Ausgangsstand {rem:.1f} g übernommen (Spoolman {smid})")
|
||||
continue
|
||||
b.setdefault("log_time", newest) # state from before the log was read
|
||||
fresh = [e for e in mine if _num(e.get("time")) > b["log_time"]]
|
||||
try:
|
||||
if b.get("pending") and not self._resolve_pending(cid, b):
|
||||
continue
|
||||
settled = not job_active and now - self.remaining_seen[cid][1] >= self.settings.settle_seconds
|
||||
if any(e.get("kind") == "correction" for e in fresh):
|
||||
if settled:
|
||||
self._weighin(cid, b, name, smid, rem, newest)
|
||||
continue
|
||||
delta = b["remaining"] - rem
|
||||
if delta < -MIN_BOOKING:
|
||||
self._log("warn", f"{sp.get('name')}: Rest in CC2 Control um {-delta:.2f} g gestiegen; "
|
||||
self._log("warn", f"{name}: Rest in CC2 Control um {-delta:.2f} g gestiegen; "
|
||||
"nicht gebucht, neuer Ausgangsstand")
|
||||
b["remaining"] = rem
|
||||
b["log_time"] = newest
|
||||
continue
|
||||
if delta < MIN_BOOKING or job_active:
|
||||
if delta < MIN_BOOKING or not settled:
|
||||
continue
|
||||
if now - self.remaining_seen[cid][1] < self.settings.settle_seconds:
|
||||
continue
|
||||
self._book(cid, b, sp.get("name") or cid, smid, delta, rem)
|
||||
self._book(cid, b, name, smid, delta, rem, describe(fresh), newest)
|
||||
except SpoolmanNotFound:
|
||||
self.bridge._once_error(f"{self.id}:book:{cid}",
|
||||
f"{self.name}: {sp.get('name')}: Spoolman-Spule {smid} existiert nicht; Verbrauch nicht gebucht")
|
||||
f"{self.name}: {name}: Spoolman-Spule {smid} existiert nicht; Verbrauch nicht gebucht")
|
||||
except SpoolmanError as e:
|
||||
self.bridge._spoolman_error(str(e))
|
||||
|
||||
def _book(self, cid: str, b: dict, name: str, smid: int, delta: float, rem: float) -> None:
|
||||
def _book(self, cid: str, b: dict, name: str, smid: int, delta: float, rem: float,
|
||||
details: str = "", log_time: float | None = None) -> None:
|
||||
if self.settings.dry_run:
|
||||
self._write(f"book:{cid}", f"{delta:.2f} g von {name} auf Spoolman-Spule {smid} buchen", lambda: None)
|
||||
self._write(f"book:{cid}", f"{delta:.2f} g von {name} auf Spoolman-Spule {smid} buchen{details}", lambda: None)
|
||||
return
|
||||
before = float(self.bridge.spoolman.spool(smid).get("used_weight") or 0)
|
||||
b["pending"] = {"grams": round(delta, 3), "target": rem, "before": before, "time": self.bridge.clock()}
|
||||
b["pending"] = {"grams": round(delta, 3), "target": rem, "before": before, "time": self.bridge.clock(),
|
||||
"details": details, "log_time": log_time}
|
||||
self.bridge.state.save() # intent first: a crash after the PUT must not book twice
|
||||
self.bridge.spoolman.use(smid, delta)
|
||||
self._booked(b, delta, rem, f"{delta:.2f} g von {name} auf Spoolman-Spule {smid} gebucht")
|
||||
self._booked(b, delta, rem, f"{delta:.2f} g von {name} auf Spoolman-Spule {smid} gebucht{details}",
|
||||
details, log_time)
|
||||
|
||||
def _booked(self, b: dict, grams: float, target: float, msg: str) -> None:
|
||||
def _booked(self, b: dict, grams: float, target: float, msg: str, details: str = "",
|
||||
log_time: float | None = None) -> None:
|
||||
b["remaining"] = target
|
||||
b["pending"] = None
|
||||
b["last"] = {"time": self.bridge.clock(), "grams": round(grams, 3)}
|
||||
b["last"] = {"time": self.bridge.clock(), "grams": round(grams, 3), "details": details.removeprefix(" – ")}
|
||||
if log_time is not None:
|
||||
b["log_time"] = max(b.get("log_time") or 0.0, log_time)
|
||||
self.bridge.state.save()
|
||||
self._log("info", msg)
|
||||
|
||||
@@ -431,18 +567,98 @@ class PrinterWorker:
|
||||
"""A booking was interrupted: check whether Spoolman already has it."""
|
||||
p = b["pending"]
|
||||
smid = b["spoolman"]
|
||||
details, log_time = p.get("details") or "", p.get("log_time")
|
||||
used = float(self.bridge.spoolman.spool(smid).get("used_weight") or 0)
|
||||
if used >= p["before"] + p["grams"] - 0.01:
|
||||
self._booked(b, p["grams"], p["target"], f"Unterbrochene Buchung über {p['grams']:.2f} g war bereits in Spoolman")
|
||||
self._booked(b, p["grams"], p["target"], f"Unterbrochene Buchung über {p['grams']:.2f} g war bereits in Spoolman",
|
||||
details, log_time)
|
||||
return True
|
||||
if self.settings.dry_run:
|
||||
self._write(f"book:{cid}", f"unterbrochene Buchung über {p['grams']:.2f} g auf Spoolman-Spule {smid} wiederholen",
|
||||
lambda: None)
|
||||
return False
|
||||
self.bridge.spoolman.use(smid, p["grams"])
|
||||
self._booked(b, p["grams"], p["target"], f"Unterbrochene Buchung über {p['grams']:.2f} g auf Spoolman-Spule {smid} nachgeholt")
|
||||
self._booked(b, p["grams"], p["target"], f"Unterbrochene Buchung über {p['grams']:.2f} g auf Spoolman-Spule {smid} nachgeholt",
|
||||
details, log_time)
|
||||
return True
|
||||
|
||||
def _weighin(self, cid: str, b: dict, name: str, smid: int, rem: float, log_time: float) -> None:
|
||||
"""Weighed in CC2 Control: Spoolman takes the weighed value (and any use since) as it is."""
|
||||
sm = self.bridge.spoolman.spool(smid)
|
||||
base = sm.get("initial_weight") or (sm.get("filament") or {}).get("weight")
|
||||
if not base:
|
||||
self.bridge._once_error(f"{self.id}:weigh:{cid}", f"{self.name}: {name}: Spoolman-Spule {smid} hat kein "
|
||||
"Startgewicht; Nachwiegen nicht übernommen")
|
||||
return
|
||||
target = max(0.0, rem)
|
||||
used = round(max(0.0, float(base) - target), 3)
|
||||
_, done = self._write(f"weigh:{cid}", f"in Spoolman den Rest von {name} auf {target:.1f} g setzen "
|
||||
"(in CC2 Control nachgewogen)",
|
||||
lambda: self.bridge.spoolman.update_spool(smid, {"used_weight": used}))
|
||||
if done:
|
||||
b["remaining"] = rem
|
||||
b["log_time"] = max(b.get("log_time") or 0.0, log_time)
|
||||
self.bridge.spool_cache.pop(smid, None)
|
||||
self.bridge.state.save()
|
||||
|
||||
# ---------- CC2 Control's log: use of a tray left without spool after "Take out" ----------
|
||||
|
||||
def _process_cc2_log(self, cc2: dict) -> None:
|
||||
st = self.st
|
||||
entries = [e for e in cc2.get("log") or [] if isinstance(e, dict)]
|
||||
keys = [log_key(e) for e in entries]
|
||||
now = self.bridge.clock()
|
||||
if st.get("log_seen") is None:
|
||||
st["log_seen"] = keys[:LOG_SEEN_KEEP] # older entries are not ours to book
|
||||
else:
|
||||
seen = set(st["log_seen"])
|
||||
for e, key in zip(entries, keys):
|
||||
if key in seen:
|
||||
continue
|
||||
st["log_seen"].append(key)
|
||||
seen.add(key)
|
||||
n, t, mm = e.get("slot"), _num(e.get("time")), _num(e.get("mm"))
|
||||
if e.get("kind") != "print" or e.get("spool") or n not in (0, 1, 2, 3) or mm <= 0:
|
||||
continue
|
||||
win = next((w for w in st["takeout"] if w["slot"] == n and
|
||||
w["since"] - TAKEOUT_MARGIN <= t <= (w["until"] or now) + TAKEOUT_MARGIN), None)
|
||||
if win:
|
||||
st["extra"].append({"key": key, "slot": n, "spool": win["spool"], "name": win["name"], "mm": mm,
|
||||
"job": job_text(e), "before": None, "grams": None})
|
||||
st["log_seen"] = st["log_seen"][-LOG_SEEN_KEEP:]
|
||||
st["takeout"] = [w for w in st["takeout"] if w["until"] is None or now - w["until"] < TAKEOUT_KEEP]
|
||||
self._process_extra()
|
||||
|
||||
def _process_extra(self) -> None:
|
||||
rest = []
|
||||
for item in self.st["extra"]:
|
||||
smid = item["spool"]
|
||||
try:
|
||||
sm = self.bridge.spoolman.spool(smid)
|
||||
fil = sm.get("filament") or {}
|
||||
grams = round(grams_from_mm(item["mm"], _num(fil.get("diameter"), 1.75) or 1.75,
|
||||
_num(fil.get("density"), 1.24) or 1.24), 3)
|
||||
msg = (f"{grams:.2f} g von {item['name']} auf Spoolman-Spule {smid} nachgebucht (Slot {item['slot'] + 1} "
|
||||
f"druckte nach „Take out“ ohne Spule) – {item['job']}")
|
||||
if self.settings.dry_run:
|
||||
self._write(f"extra:{item['key']}", msg, lambda: None)
|
||||
rest.append(item)
|
||||
continue
|
||||
used = float(sm.get("used_weight") or 0)
|
||||
if item["before"] is not None and used >= item["before"] + item["grams"] - 0.01:
|
||||
self._log("info", msg) # booked before a crash
|
||||
continue
|
||||
item["before"], item["grams"] = used, grams
|
||||
self.bridge.state.save()
|
||||
self.bridge.spoolman.use(smid, grams)
|
||||
self._log("info", msg)
|
||||
except SpoolmanNotFound:
|
||||
self._log("warn", f"Spoolman-Spule {smid} existiert nicht; {item['mm']:.0f} mm aus Slot {item['slot'] + 1} nicht gebucht")
|
||||
except SpoolmanError as e:
|
||||
self.bridge._spoolman_error(str(e))
|
||||
rest.append(item)
|
||||
self.st["extra"] = rest
|
||||
|
||||
# ---------- UI ----------
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
@@ -457,7 +673,9 @@ class PrinterWorker:
|
||||
sid = slot_spool_id(cs)
|
||||
cached = self.bridge.spool_cache.get(m["spool"]) if m else None
|
||||
b = self.st["booked"].get(m["cc2"]) if m and m.get("cc2") else None
|
||||
takeout = next((t for t in self.st["takeout"] if t["slot"] == n and t["until"] is None), None)
|
||||
slots.append({
|
||||
"takeout": {"spool": takeout["spool"], "name": takeout["name"]} if takeout else None,
|
||||
"slot": n,
|
||||
"record": record_slots[n],
|
||||
"tray_status": self.tray_status.get(n),
|
||||
@@ -638,6 +856,7 @@ class Bridge:
|
||||
if cur and cur["spool"] == tid:
|
||||
if cc2_id is None:
|
||||
cur["assigned"] = False # rescan: make sure CC2 Control has it in the slot
|
||||
cur.pop("reinserted", None)
|
||||
self.dry_logged.pop(f"{w.id}:assign:{n}", None)
|
||||
return
|
||||
for pid, k, m in list(self.mappings()):
|
||||
@@ -653,12 +872,18 @@ class Bridge:
|
||||
"cc2": cc2_id, "assigned": cc2_id is not None, "cc2_seen": cc2_id is not None,
|
||||
}
|
||||
w.empty_since.pop(n, None)
|
||||
for other in self.workers.values():
|
||||
last = other.st["last"]
|
||||
for k in [k for k, v in last.items() if v["spool"] == tid or (other is w and k == str(n))]:
|
||||
del last[k]
|
||||
other._close_takeout(n if other is w else None, tid)
|
||||
w._log("info", f"Slot {n + 1}: {name} (Spoolman {tid}) zugeordnet ({reason})")
|
||||
|
||||
def release(self, w: PrinterWorker, n: int, reason: str) -> None:
|
||||
m = w.st["slots"].pop(str(n), None)
|
||||
if not m:
|
||||
return
|
||||
w.st["last"][str(n)] = {"spool": m["spool"], "name": m.get("name") or str(m["spool"])}
|
||||
self.dry_logged.pop(f"{w.id}:assign:{n}", None)
|
||||
w._log("info", f"Slot {n + 1}: Zuordnung zu {m.get('name') or m['spool']} aufgehoben ({reason})")
|
||||
|
||||
|
||||
@@ -240,6 +240,10 @@ class StateStore:
|
||||
p.setdefault("booked", {})
|
||||
p.setdefault("last_record", None)
|
||||
p.setdefault("scans", [])
|
||||
p.setdefault("last", {}) # slot -> roll that last left it (for putting it back)
|
||||
p.setdefault("takeout", []) # windows in which a tray prints for a roll taken out in CC2 Control
|
||||
p.setdefault("log_seen", None) # CC2 Control log entries already looked at
|
||||
p.setdefault("extra", []) # bookings from CC2 Control's log still to make
|
||||
return p
|
||||
|
||||
def save(self) -> None:
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
const QUESTION = { inserted: 'eingelegt', changed: 'geändert' };
|
||||
const TRAY = { 0: ['leer', 'warn'], 1: ['eingelegt', 'ok'], 2: ['fördert', 'ok'] };
|
||||
const LEVEL = { info: ['Info', ''], warn: ['Warnung', 'warn'], error: ['Fehler', 'err'], dry: ['Dry-Run', 'dry'] };
|
||||
|
||||
@@ -35,8 +36,8 @@ function slotCard(p, s) {
|
||||
assign = s.cc2_matches ? '<span class="tag ok">zugewiesen</span>'
|
||||
: (m.cc2 ? '<span class="tag warn">nicht zugewiesen</span>' : '<span class="tag warn">noch nicht angelegt</span>');
|
||||
}
|
||||
if (s.cc2_question) assign += ` <span class="tag warn">CC2 fragt: ${esc(s.cc2_question)}</span>`;
|
||||
const orphan = !m && s.record.spool_id != null && (s.tray_status === 1 || s.tray_status === 2);
|
||||
if (s.cc2_question) assign += ` <span class="tag warn">CC2 fragt: Spule ${esc(QUESTION[s.cc2_question] || s.cc2_question)}</span>`;
|
||||
const orphan = !m && !s.takeout && s.record.spool_id != null && (s.tray_status === 1 || s.tray_status === 2);
|
||||
const lb = s.last_booking;
|
||||
return `<div class="panel">
|
||||
<h3>Slot ${s.slot + 1} ${tray ? `<span class="tag ${tray[1]}">${tray[0]}</span>` : ''}</h3>
|
||||
@@ -47,10 +48,14 @@ function slotCard(p, s) {
|
||||
<dt>Spoolman</dt><dd>${m ? `${sm && sm.color ? `<span class="swatch" style="background:#${sm.color}"></span> ` : ''}<b>${esc(m.name || m.spool)}</b> <span class="muted">#${m.spool}</span>` +
|
||||
(sm ? `<br><span class="small muted">${esc(sm.vendor)} ${esc(sm.material)}, Rest ${sm.remaining != null ? Math.round(sm.remaining) + ' g' : '?'}</span>` : '') : '<span class="muted">nicht zugeordnet</span>'}</dd>
|
||||
<dt>CC2 Control</dt><dd>${cc ? esc(cc.name) + ` <span class="small muted">Rest ${Number(cc.remaining).toFixed(1)} g</span><br>` : ''}${assign}</dd>
|
||||
<dt>Buchung</dt><dd>${lb ? `${lb.grams.toFixed(2)} g <span class="small muted">${fmtTime(lb.time)}</span>` : '<span class="muted">–</span>'}</dd>
|
||||
<dt>Buchung</dt><dd>${lb ? `${lb.grams.toFixed(2)} g <span class="small muted">${fmtTime(lb.time)}</span>` +
|
||||
(lb.details ? `<br><span class="small muted">${esc(lb.details)}</span>` : '') : '<span class="muted">–</span>'}</dd>
|
||||
</dl>
|
||||
${orphan ? `<p class="small" style="margin:10px 0 0;color:var(--warn)">Tag ${s.record.spool_id} steckt noch, ist aber keinem Slot zugeordnet
|
||||
(z. B. nach „Take out“ in CC2 Control). Verbrauch wird so nicht gezählt – zum Zuordnen die Spule neu einlegen und scannen.</p>` : ''}
|
||||
${s.takeout ? `<p class="small" style="margin:10px 0 0;color:var(--warn)">${esc(s.takeout.name)} wurde in CC2 Control herausgenommen
|
||||
und steht in Spoolman im Lager, steckt aber noch. Was dieser Slot druckt, bucht die Bridge trotzdem auf ${esc(s.takeout.name)},
|
||||
bis die Rolle entnommen wird. Wieder zuordnen: Rolle herausnehmen und neu einlegen.</p>` : ''}
|
||||
${orphan ? `<p class="small" style="margin:10px 0 0;color:var(--warn)">Tag ${s.record.spool_id} steht noch im Slot, ist aber keiner Rolle
|
||||
zugeordnet. Liegt diese Rolle hier, sie herausnehmen und neu einlegen (oder in CC2 Control auswählen).</p>` : ''}
|
||||
${m ? `<div class="row" style="margin-top:10px">
|
||||
<button onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Zuordnung erneut senden</button>
|
||||
<button class="danger" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button></div>` : ''}
|
||||
|
||||
Reference in new issue
Block a user