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
@@ -85,18 +85,30 @@ kleinere Werte sind echte Gewichte von Original- oder Fremdtags und werden ignor
|
|||||||
im Slot gegenüber dem letzten Datensatz ändert.
|
im Slot gegenüber dem letzten Datensatz ändert.
|
||||||
|
|
||||||
**Zuordnung.** Die Bridge speichert Drucker/Slot ↔ Spule selbst (`/data/state.json`). Eine Spule liegt höchstens
|
**Zuordnung.** Die Bridge speichert Drucker/Slot ↔ Spule selbst (`/data/state.json`). Eine Spule liegt höchstens
|
||||||
in einem Slot eines Druckers. Fällt `weight` nach einer Bearbeitung am Display auf 1000 zurück, bleibt die
|
in einem Slot eines Druckers. Fällt `weight` nach einer Bearbeitung am Display auf 1000 zurück, bleibt eine
|
||||||
Zuordnung. Aufgehoben wird sie nur, wenn
|
**gescannte** Zuordnung bestehen; fragt CC2 Control deshalb nach der Spule, weist die Bridge sie erneut zu (höchstens
|
||||||
|
einmal pro Minute). Aufgehoben wird sie, wenn
|
||||||
- der Slot länger als 20 s leer ist (Tray-Status 0),
|
- der Slot länger als 20 s leer ist (Tray-Status 0),
|
||||||
- CC2 Control die Spule herausnimmt („Take out“) oder ins Lager legt (Slot ohne Spule und ohne offene Rückfrage),
|
- CC2 Control die Spule herausnimmt („Take out“) oder ins Lager legt (Slot ohne Spule und ohne offene Rückfrage),
|
||||||
- im Slot ein Tag mit einer anderen ID gescannt wird oder
|
- im Slot ein Tag mit einer anderen ID gescannt wird,
|
||||||
- die Spule in einem anderen Slot oder im anderen Drucker gescannt wird (Umzug).
|
- die Spule in einem anderen Slot oder im anderen Drucker gescannt wird (Umzug) oder
|
||||||
|
- nach dem Einlegen einer Rolle die Tag-Daten des Slots nicht mehr ihre ID tragen (siehe unten).
|
||||||
|
|
||||||
Weist du eine gespiegelte Spule in CC2 Control von Hand einem belegten Slot zu, übernimmt die Bridge das.
|
Weist du eine gespiegelte Spule in CC2 Control von Hand einem belegten Slot zu, übernimmt die Bridge das.
|
||||||
|
|
||||||
|
**Rolle wieder einlegen.** Der Scanner sitzt an der Seite, die Rollen werden zum Scannen abgenommen. Der Drucker
|
||||||
|
behält die Tag-Daten eines Slots auch, wenn die Rolle draußen ist. Kommt eine Rolle in den Slot zurück (CC2 Control
|
||||||
|
fragt „eingelegt“) und stehen dort noch unverändert die Daten der Rolle, die diesen Slot zuletzt verlassen hat, gilt
|
||||||
|
sie als dieselbe Rolle: Die Bridge ordnet sie wieder zu und beantwortet die Rückfrage, mit oder ohne erneuten Scan.
|
||||||
|
Wird danach am Display ein Filament gewählt (die ID verschwindet), war es eine andere Rolle ohne Tag: Die Zuordnung
|
||||||
|
wird aufgehoben, die Rolle kommt ins Lager. Das gilt bis zum ersten Druck mit der Rolle; danach ist eine
|
||||||
|
Display-Bearbeitung wieder eine Bearbeitung derselben Rolle.
|
||||||
|
|
||||||
**„Take out“ in CC2 Control** zählt als Entnahme: Ort „Lager“, Felder leer. Bleibt die Rolle physisch im Tray,
|
**„Take out“ in CC2 Control** zählt als Entnahme: Ort „Lager“, Felder leer. Bleibt die Rolle physisch im Tray,
|
||||||
rechnet CC2 Control ihren Verbrauch keiner Spule mehr zu; der Status zeigt dann „Tag steckt noch, ist aber keinem
|
rechnet CC2 Control ihren Verbrauch keiner Spule mehr zu, protokolliert ihn aber für den Slot. Diesen Verbrauch
|
||||||
Slot zugeordnet“. Zum erneuten Zuordnen die Rolle neu einlegen und den Scan bestätigen.
|
(Millimeter aus dem CC2-Protokoll, mit Durchmesser und Dichte der Spoolman-Spule in Gramm umgerechnet) bucht die
|
||||||
|
Bridge trotzdem auf die Rolle, bis sie den Slot verlässt oder CC2 Control nach einer Spule fragt. Zum erneuten
|
||||||
|
Zuordnen die Rolle herausnehmen und wieder einlegen.
|
||||||
|
|
||||||
**Spoolman synchron halten.** Nach jeder Änderung schreibt die Bridge Ort und Zusatzfelder (die Felder legt sie beim
|
**Spoolman synchron halten.** Nach jeder Änderung schreibt die Bridge Ort und Zusatzfelder (die Felder legt sie beim
|
||||||
ersten Mal selbst an). Zusätzlich vergleicht sie jede Minute alle Spoolman-Spulen: Geladene Rollen bekommen ihren
|
ersten Mal selbst an). Zusätzlich vergleicht sie jede Minute alle Spoolman-Spulen: Geladene Rollen bekommen ihren
|
||||||
@@ -116,9 +128,26 @@ CC2 Control der Zeitstempel der letzten Nutzung, keine Menge.) Die Bridge bucht
|
|||||||
Abnahme von `remaining` seit der letzten Buchung per `PUT /spool/{id}/use` nach Spoolman. Das geschieht erst, wenn kein
|
Abnahme von `remaining` seit der letzten Buchung per `PUT /spool/{id}/use` nach Spoolman. Das geschieht erst, wenn kein
|
||||||
Druck läuft (`job.active`) und `remaining` 60 s unverändert ist. Der gebuchte Stand wird persistent gespeichert.
|
Druck läuft (`job.active`) und `remaining` 60 s unverändert ist. Der gebuchte Stand wird persistent gespeichert.
|
||||||
Vor jeder Buchung schreibt die Bridge die Absicht in den Zustand. Bricht eine Buchung ab, prüft sie nach dem
|
Vor jeder Buchung schreibt die Bridge die Absicht in den Zustand. Bricht eine Buchung ab, prüft sie nach dem
|
||||||
Neustart an `used_weight` in Spoolman, ob sie schon angekommen ist, statt doppelt zu buchen. Steigt `remaining`
|
Neustart an `used_weight` in Spoolman, ob sie schon angekommen ist, statt doppelt zu buchen.
|
||||||
(z. B. durch Nachwiegen in CC2 Control), wird nichts gebucht, nur protokolliert, und der neue Stand gilt als
|
|
||||||
Ausgangspunkt.
|
CC2 Control misst den echten Extruderweg (`print_stats.filament_used`, mit Spülen und Farbwechseln, mit Flow-Faktor)
|
||||||
|
und rechnet ihn dem Slot zu, der gerade fördert. Ein abgebrochener oder fehlgeschlagener Druck zählt also mit dem,
|
||||||
|
was er wirklich verbraucht hat; bei Mehrfarbdrucken bekommt jede Rolle ihren Anteil. Die Bridge nimmt Dateiname und
|
||||||
|
Ergebnis aus dem CC2-Protokoll in die Buchung auf, z. B. „2.98 g von SUNLU001 … gebucht – benchy.gcode
|
||||||
|
(abgebrochen)“.
|
||||||
|
|
||||||
|
**Rolle leer.** Läuft eine Rolle im Druck leer, setzt CC2 Control sie auf 0 g; die Bridge bucht den Rest
|
||||||
|
(„Rolle leergelaufen“), sodass auch Spoolman 0 g zeigt. Druckt der Drucker mit einer anderen Rolle weiter, zählt
|
||||||
|
alles ab dem Wechsel für deren Slot. Archiviert wird die leere Rolle nicht.
|
||||||
|
|
||||||
|
**Nachwiegen.** Wird eine Spule in CC2 Control nachgewogen (Protokolleintrag „correction“), setzt die Bridge den
|
||||||
|
Rest in Spoolman auf denselben Wert (`used_weight` = Startgewicht − Rest), statt die Differenz als Verbrauch zu
|
||||||
|
buchen. Steigt `remaining` ohne Nachwiegen, wird nichts gebucht, nur protokolliert, und der neue Stand gilt als
|
||||||
|
Ausgangspunkt. Ein Nachwiegen in Spoolman geht nicht zurück an CC2 Control.
|
||||||
|
|
||||||
|
**Grenzen.** Slots ohne Spoolman-Tag und mit „keine Spule“ beantwortete Rückfragen werden nicht gebucht. Bei einem
|
||||||
|
Stromausfall des Druckers gehen bis zu 2 min Verbrauch verloren (CC2 Control speichert beim Drucken höchstens alle
|
||||||
|
2 min). Bekommt CC2 Control eine eigene Spoolman-Anbindung, darf nur eins von beidem buchen.
|
||||||
|
|
||||||
**Spulenverwaltung aus.** Ist sie in CC2 Control ausgeschaltet, zählt CC2 Control keinen Verbrauch. Die Oberfläche
|
**Spulenverwaltung aus.** Ist sie in CC2 Control ausgeschaltet, zählt CC2 Control keinen Verbrauch. Die Oberfläche
|
||||||
zeigt das deutlich an und bietet einen Button zum Einschalten (mit Rückfrage, im Dry-Run nur protokolliert).
|
zeigt das deutlich an und bietet einen Button zum Einschalten (mit Rückfrage, im Dry-Run nur protokolliert).
|
||||||
|
|||||||
+245
-20
@@ -4,10 +4,16 @@ Rules:
|
|||||||
- The printer is only read (SSH `cat` of the canvas record). No MQTT, no printer commands.
|
- 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.
|
- 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
|
- 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.
|
- 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
|
- Spoolman shows where a loaded spool is (location "<printer> Slot N", extra fields
|
||||||
"drucker"/"slot"); the printer wins over manual edits there.
|
"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
|
from __future__ import annotations
|
||||||
@@ -33,7 +39,54 @@ MIN_BOOKING = 0.05 # grams; smaller changes accumulate until they reach this
|
|||||||
SPOOL_CACHE_SECONDS = 300
|
SPOOL_CACHE_SECONDS = 300
|
||||||
SYNC_SECONDS = 60 # full Spoolman comparison (locations, extra fields)
|
SYNC_SECONDS = 60 # full Spoolman comparison (locations, extra fields)
|
||||||
LOG_KEEP = 1000
|
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"}
|
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:
|
class ActionLog:
|
||||||
@@ -205,8 +258,21 @@ class PrinterWorker:
|
|||||||
self._process_trays(cc2)
|
self._process_trays(cc2)
|
||||||
self._process_cc2_slots(cc2)
|
self._process_cc2_slots(cc2)
|
||||||
self._reconcile_cc2(cc2)
|
self._reconcile_cc2(cc2)
|
||||||
|
self._process_cc2_log(cc2)
|
||||||
self._consumption(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 ----------
|
# ---------- record / scans ----------
|
||||||
|
|
||||||
def _process_record(self, record: dict) -> None:
|
def _process_record(self, record: dict) -> None:
|
||||||
@@ -217,6 +283,13 @@ class PrinterWorker:
|
|||||||
mappings = self.st["slots"]
|
mappings = self.st["slots"]
|
||||||
for s in slots:
|
for s in slots:
|
||||||
n, tid = s["slot"], s["spool_id"]
|
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:
|
if tid is None:
|
||||||
continue
|
continue
|
||||||
if prev is not None:
|
if prev is not None:
|
||||||
@@ -271,6 +344,7 @@ class PrinterWorker:
|
|||||||
if st is None:
|
if st is None:
|
||||||
continue
|
continue
|
||||||
if st == 0:
|
if st == 0:
|
||||||
|
self._close_takeout(n)
|
||||||
since = self.empty_since.setdefault(n, now)
|
since = self.empty_since.setdefault(n, now)
|
||||||
if now - since >= self.settings.empty_grace and str(n) in self.st["slots"]:
|
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")
|
self.bridge.release(self, n, f"Slot seit {int(now - since)} s leer")
|
||||||
@@ -287,14 +361,20 @@ class PrinterWorker:
|
|||||||
sid = slot_spool_id(slot)
|
sid = slot_spool_id(slot)
|
||||||
m = mappings.get(str(n))
|
m = mappings.get(str(n))
|
||||||
mirrored = spoolman_id_of(by_id[sid]) if sid in by_id else None
|
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
|
# A spool we already map elsewhere (fresh scan, maybe in the other printer) wins
|
||||||
# over this CC2 Control's older binding.
|
# over this CC2 Control's older binding.
|
||||||
elsewhere = mirrored is not None and self.bridge.mapped_elsewhere(mirrored, self.id, n)
|
elsewhere = mirrored is not None and self.bridge.mapped_elsewhere(mirrored, self.id, n)
|
||||||
if m is None:
|
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
|
# assigned in CC2 Control by hand
|
||||||
self.bridge.map(self, n, mirrored, by_id[sid].get("name") or str(mirrored),
|
self.bridge.map(self, n, mirrored, by_id[sid].get("name") or str(mirrored),
|
||||||
"in CC2 Control zugewiesen", sid)
|
"in CC2 Control zugewiesen", sid)
|
||||||
|
elif not sid and question == "inserted" and loaded:
|
||||||
|
self._reinserted(n)
|
||||||
continue
|
continue
|
||||||
if m.get("cc2") and sid == m["cc2"]:
|
if m.get("cc2") and sid == m["cc2"]:
|
||||||
m["cc2_seen"] = m["assigned"] = True
|
m["cc2_seen"] = m["assigned"] = True
|
||||||
@@ -309,8 +389,44 @@ class PrinterWorker:
|
|||||||
"in CC2 Control zugewiesen", sid)
|
"in CC2 Control zugewiesen", sid)
|
||||||
elif mirrored is None:
|
elif mirrored is None:
|
||||||
self.bridge.release(self, n, "in CC2 Control mit anderer Spule belegt")
|
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")
|
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:
|
def _reconcile_cc2(self, cc2: dict) -> None:
|
||||||
mirrors: dict[int, dict] = {}
|
mirrors: dict[int, dict] = {}
|
||||||
@@ -347,7 +463,7 @@ class PrinterWorker:
|
|||||||
continue
|
continue
|
||||||
self.st["booked"][cc2_id] = {
|
self.st["booked"][cc2_id] = {
|
||||||
"spoolman": m["spool"], "remaining": float(fields.get("remaining", fields.get("net", 1000))),
|
"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
|
m["cc2"] = cc2_id
|
||||||
else:
|
else:
|
||||||
@@ -371,8 +487,13 @@ class PrinterWorker:
|
|||||||
|
|
||||||
def _consumption(self, cc2: dict) -> None:
|
def _consumption(self, cc2: dict) -> None:
|
||||||
job_active = bool((cc2.get("job") or {}).get("active"))
|
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"]
|
booked = self.st["booked"]
|
||||||
now = self.bridge.clock()
|
now = self.bridge.clock()
|
||||||
|
entries = [e for e in cc2.get("log") or [] if isinstance(e, dict)]
|
||||||
for sp in cc2.get("spools") or []:
|
for sp in cc2.get("spools") or []:
|
||||||
smid = spoolman_id_of(sp)
|
smid = spoolman_id_of(sp)
|
||||||
if smid is None:
|
if smid is None:
|
||||||
@@ -382,48 +503,63 @@ class PrinterWorker:
|
|||||||
rem = float(sp.get("remaining"))
|
rem = float(sp.get("remaining"))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
continue
|
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)
|
seen = self.remaining_seen.get(cid)
|
||||||
if seen is None or abs(seen[0] - rem) > 1e-6:
|
if seen is None or abs(seen[0] - rem) > 1e-6:
|
||||||
self.remaining_seen[cid] = (rem, now)
|
self.remaining_seen[cid] = (rem, now)
|
||||||
b = booked.get(cid)
|
b = booked.get(cid)
|
||||||
if b is None or b.get("spoolman") != smid:
|
if b is None or b.get("spoolman") != smid:
|
||||||
booked[cid] = {"spoolman": smid, "remaining": rem, "pending": None, "last": None}
|
booked[cid] = {"spoolman": smid, "remaining": rem, "pending": None, "last": None, "log_time": newest}
|
||||||
self._log("info", f"{sp.get('name')}: Ausgangsstand {rem:.1f} g übernommen (Spoolman {smid})")
|
self._log("info", f"{name}: Ausgangsstand {rem:.1f} g übernommen (Spoolman {smid})")
|
||||||
continue
|
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:
|
try:
|
||||||
if b.get("pending") and not self._resolve_pending(cid, b):
|
if b.get("pending") and not self._resolve_pending(cid, b):
|
||||||
continue
|
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
|
delta = b["remaining"] - rem
|
||||||
if delta < -MIN_BOOKING:
|
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")
|
"nicht gebucht, neuer Ausgangsstand")
|
||||||
b["remaining"] = rem
|
b["remaining"] = rem
|
||||||
|
b["log_time"] = newest
|
||||||
continue
|
continue
|
||||||
if delta < MIN_BOOKING or job_active:
|
if delta < MIN_BOOKING or not settled:
|
||||||
continue
|
continue
|
||||||
if now - self.remaining_seen[cid][1] < self.settings.settle_seconds:
|
self._book(cid, b, name, smid, delta, rem, describe(fresh), newest)
|
||||||
continue
|
|
||||||
self._book(cid, b, sp.get("name") or cid, smid, delta, rem)
|
|
||||||
except SpoolmanNotFound:
|
except SpoolmanNotFound:
|
||||||
self.bridge._once_error(f"{self.id}:book:{cid}",
|
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:
|
except SpoolmanError as e:
|
||||||
self.bridge._spoolman_error(str(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:
|
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
|
return
|
||||||
before = float(self.bridge.spoolman.spool(smid).get("used_weight") or 0)
|
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.state.save() # intent first: a crash after the PUT must not book twice
|
||||||
self.bridge.spoolman.use(smid, delta)
|
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["remaining"] = target
|
||||||
b["pending"] = None
|
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.bridge.state.save()
|
||||||
self._log("info", msg)
|
self._log("info", msg)
|
||||||
|
|
||||||
@@ -431,18 +567,98 @@ class PrinterWorker:
|
|||||||
"""A booking was interrupted: check whether Spoolman already has it."""
|
"""A booking was interrupted: check whether Spoolman already has it."""
|
||||||
p = b["pending"]
|
p = b["pending"]
|
||||||
smid = b["spoolman"]
|
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)
|
used = float(self.bridge.spoolman.spool(smid).get("used_weight") or 0)
|
||||||
if used >= p["before"] + p["grams"] - 0.01:
|
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
|
return True
|
||||||
if self.settings.dry_run:
|
if self.settings.dry_run:
|
||||||
self._write(f"book:{cid}", f"unterbrochene Buchung über {p['grams']:.2f} g auf Spoolman-Spule {smid} wiederholen",
|
self._write(f"book:{cid}", f"unterbrochene Buchung über {p['grams']:.2f} g auf Spoolman-Spule {smid} wiederholen",
|
||||||
lambda: None)
|
lambda: None)
|
||||||
return False
|
return False
|
||||||
self.bridge.spoolman.use(smid, p["grams"])
|
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
|
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 ----------
|
# ---------- UI ----------
|
||||||
|
|
||||||
def snapshot(self) -> dict:
|
def snapshot(self) -> dict:
|
||||||
@@ -457,7 +673,9 @@ class PrinterWorker:
|
|||||||
sid = slot_spool_id(cs)
|
sid = slot_spool_id(cs)
|
||||||
cached = self.bridge.spool_cache.get(m["spool"]) if m else None
|
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
|
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({
|
slots.append({
|
||||||
|
"takeout": {"spool": takeout["spool"], "name": takeout["name"]} if takeout else None,
|
||||||
"slot": n,
|
"slot": n,
|
||||||
"record": record_slots[n],
|
"record": record_slots[n],
|
||||||
"tray_status": self.tray_status.get(n),
|
"tray_status": self.tray_status.get(n),
|
||||||
@@ -638,6 +856,7 @@ class Bridge:
|
|||||||
if cur and cur["spool"] == tid:
|
if cur and cur["spool"] == tid:
|
||||||
if cc2_id is None:
|
if cc2_id is None:
|
||||||
cur["assigned"] = False # rescan: make sure CC2 Control has it in the slot
|
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)
|
self.dry_logged.pop(f"{w.id}:assign:{n}", None)
|
||||||
return
|
return
|
||||||
for pid, k, m in list(self.mappings()):
|
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,
|
"cc2": cc2_id, "assigned": cc2_id is not None, "cc2_seen": cc2_id is not None,
|
||||||
}
|
}
|
||||||
w.empty_since.pop(n, 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})")
|
w._log("info", f"Slot {n + 1}: {name} (Spoolman {tid}) zugeordnet ({reason})")
|
||||||
|
|
||||||
def release(self, w: PrinterWorker, n: int, reason: str) -> None:
|
def release(self, w: PrinterWorker, n: int, reason: str) -> None:
|
||||||
m = w.st["slots"].pop(str(n), None)
|
m = w.st["slots"].pop(str(n), None)
|
||||||
if not m:
|
if not m:
|
||||||
return
|
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)
|
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})")
|
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("booked", {})
|
||||||
p.setdefault("last_record", None)
|
p.setdefault("last_record", None)
|
||||||
p.setdefault("scans", [])
|
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
|
return p
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
|
const QUESTION = { inserted: 'eingelegt', changed: 'geändert' };
|
||||||
const TRAY = { 0: ['leer', 'warn'], 1: ['eingelegt', 'ok'], 2: ['fördert', 'ok'] };
|
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'] };
|
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>'
|
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>');
|
: (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>`;
|
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.record.spool_id != null && (s.tray_status === 1 || s.tray_status === 2);
|
const orphan = !m && !s.takeout && s.record.spool_id != null && (s.tray_status === 1 || s.tray_status === 2);
|
||||||
const lb = s.last_booking;
|
const lb = s.last_booking;
|
||||||
return `<div class="panel">
|
return `<div class="panel">
|
||||||
<h3>Slot ${s.slot + 1} ${tray ? `<span class="tag ${tray[1]}">${tray[0]}</span>` : ''}</h3>
|
<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>` +
|
<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>
|
(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>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>
|
</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
|
${s.takeout ? `<p class="small" style="margin:10px 0 0;color:var(--warn)">${esc(s.takeout.name)} wurde in CC2 Control herausgenommen
|
||||||
(z. B. nach „Take out“ in CC2 Control). Verbrauch wird so nicht gezählt – zum Zuordnen die Spule neu einlegen und scannen.</p>` : ''}
|
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">
|
${m ? `<div class="row" style="margin-top:10px">
|
||||||
<button onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Zuordnung erneut senden</button>
|
<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>` : ''}
|
<button class="danger" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button></div>` : ''}
|
||||||
|
|||||||
+17
-2
@@ -48,6 +48,7 @@ class FakeCC2:
|
|||||||
self.slots[4]["printer"] = None
|
self.slots[4]["printer"] = None
|
||||||
self.library: list[dict] = []
|
self.library: list[dict] = []
|
||||||
self.job = {"active": False}
|
self.job = {"active": False}
|
||||||
|
self.log: list[dict] = [] # newest first, as CC2 Control lists it
|
||||||
self.calls: list[tuple] = []
|
self.calls: list[tuple] = []
|
||||||
self.fail = False
|
self.fail = False
|
||||||
self._ids = (f"s{i:04d}" for i in itertools.count(1))
|
self._ids = (f"s{i:04d}" for i in itertools.count(1))
|
||||||
@@ -57,7 +58,7 @@ class FakeCC2:
|
|||||||
raise CC2Error("CC2 Control nicht erreichbar")
|
raise CC2Error("CC2 Control nicht erreichbar")
|
||||||
|
|
||||||
def spools_json(self):
|
def spools_json(self):
|
||||||
return {"enabled": self.enabled, "slots": self.slots, "spools": self.library, "job": self.job, "log": []}
|
return {"enabled": self.enabled, "slots": self.slots, "spools": self.library, "job": self.job, "log": self.log}
|
||||||
|
|
||||||
def spools(self):
|
def spools(self):
|
||||||
self._check()
|
self._check()
|
||||||
@@ -103,6 +104,15 @@ class FakeCC2:
|
|||||||
sp = next(s for s in self.library if s["id"] == cc2_id)
|
sp = next(s for s in self.library if s["id"] == cc2_id)
|
||||||
sp["remaining"] -= grams
|
sp["remaining"] -= grams
|
||||||
|
|
||||||
|
def add_log(self, time, kind="print", spool="", slot=0, grams=0.0, mm=0.0, job="", result=""):
|
||||||
|
self.log.insert(0, {"time": int(time), "spool": spool, "slot": slot, "kind": kind, "grams": grams, "mm": mm,
|
||||||
|
"job": job, "result": result})
|
||||||
|
|
||||||
|
def weigh(self, cc2_id, remaining, time):
|
||||||
|
sp = next(s for s in self.library if s["id"] == cc2_id)
|
||||||
|
self.add_log(time, "correction", cc2_id, -1, remaining - sp["remaining"], 0, "", "weighed")
|
||||||
|
sp["remaining"] = remaining
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -148,7 +158,12 @@ class FakeSpoolman:
|
|||||||
for key, value in (body.get("extra") or {}).items():
|
for key, value in (body.get("extra") or {}).items():
|
||||||
if value is not None and key not in self.field_defs:
|
if value is not None and key not in self.field_defs:
|
||||||
raise SpoolmanError("Spoolman PATCH: HTTP 400")
|
raise SpoolmanError("Spoolman PATCH: HTTP 400")
|
||||||
self.calls.append(("location", sid, body.get("location")))
|
if "location" in body:
|
||||||
|
self.calls.append(("location", sid, body.get("location")))
|
||||||
|
if "used_weight" in body:
|
||||||
|
self.calls.append(("used", sid, body["used_weight"]))
|
||||||
|
sp["used_weight"] = body["used_weight"]
|
||||||
|
sp["remaining_weight"] = sp["initial_weight"] - body["used_weight"]
|
||||||
if "extra" in body:
|
if "extra" in body:
|
||||||
self.calls.append(("extra", sid, dict(body["extra"])))
|
self.calls.append(("extra", sid, dict(body["extra"])))
|
||||||
if "location" in body:
|
if "location" in body:
|
||||||
|
|||||||
@@ -526,3 +526,236 @@ def test_migration_from_v1(tmp_path):
|
|||||||
assert (tmp_path / "ssh" / "host_key_p1.json").exists()
|
assert (tmp_path / "ssh" / "host_key_p1.json").exists()
|
||||||
assert sm.db[1]["location"] == "Rechts Slot 2" and sm.extra(1) == {"drucker": "Rechts", "slot": 2}
|
assert sm.db[1]["location"] == "Rechts Slot 2" and sm.extra(1) == {"drucker": "Rechts", "slot": 2}
|
||||||
assert not [c for c in cc2.calls if c[0] in ("save", "assign")] # nothing re-created
|
assert not [c for c in cc2.calls if c[0] in ("save", "assign")] # nothing re-created
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- v3: putting rolls back, CC2 Control's log ----------
|
||||||
|
|
||||||
|
def take_out_of_tray(e, slot, pid="p1"):
|
||||||
|
"""Roll physically out: tray empty; CC2 Control gives the spool back to storage (record keeps the tag)."""
|
||||||
|
cc2 = e["cc2s"][pid]
|
||||||
|
cc2.slots[slot]["printer"] = {"status": 0}
|
||||||
|
cc2.slots[slot]["spool"] = ""
|
||||||
|
cc2.slots[slot]["question"] = ""
|
||||||
|
|
||||||
|
|
||||||
|
def put_back(e, slot, pid="p1"):
|
||||||
|
"""Roll in again without a new scan: the record is unchanged, CC2 Control asks."""
|
||||||
|
cc2 = e["cc2s"][pid]
|
||||||
|
cc2.slots[slot]["printer"] = {"status": 1}
|
||||||
|
cc2.slots[slot]["spool"] = ""
|
||||||
|
cc2.slots[slot]["question"] = "inserted"
|
||||||
|
|
||||||
|
|
||||||
|
def uses(e):
|
||||||
|
return [c for c in e["sm"].calls if c[0] == "use"]
|
||||||
|
|
||||||
|
|
||||||
|
def msgs(e):
|
||||||
|
return [x["msg"] for x in e["bridge"].log.recent()]
|
||||||
|
|
||||||
|
|
||||||
|
def test_roll_put_back_after_long_absence_is_mapped_again(env):
|
||||||
|
env["sm"].add(5)
|
||||||
|
insert(env, 1, 5)
|
||||||
|
poll(env)
|
||||||
|
take_out_of_tray(env, 1)
|
||||||
|
poll(env, 6) # > 20 s
|
||||||
|
assert slots(env) == {} and env["sm"].db[5]["location"] == "Lager"
|
||||||
|
put_back(env, 1)
|
||||||
|
poll(env)
|
||||||
|
assert slots(env)["1"]["spool"] == 5 and slots(env)["1"]["reinserted"]
|
||||||
|
assert env["cc2"].slots[1]["spool"] == "s0001" and env["cc2"].slots[1]["question"] == ""
|
||||||
|
assert env["sm"].db[5]["location"] == "Rechts Slot 2"
|
||||||
|
assert len([c for c in env["cc2"].calls if c[0] == "save"]) == 1 # same mirror
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_back_then_touchscreen_edit_means_another_roll(env):
|
||||||
|
env["sm"].add(5)
|
||||||
|
insert(env, 1, 5)
|
||||||
|
poll(env)
|
||||||
|
take_out_of_tray(env, 1)
|
||||||
|
poll(env, 6)
|
||||||
|
put_back(env, 1) # really an untagged roll ...
|
||||||
|
poll(env)
|
||||||
|
assert slots(env)["1"]["spool"] == 5
|
||||||
|
env["reader"].record["channels1"] = channel(1000) # ... chosen on the touchscreen
|
||||||
|
env["cc2"].slots[1]["spool"] = ""
|
||||||
|
env["cc2"].slots[1]["question"] = "changed"
|
||||||
|
poll(env, 2)
|
||||||
|
assert slots(env) == {} and env["sm"].db[5]["location"] == "Lager"
|
||||||
|
poll(env, 10)
|
||||||
|
assert slots(env) == {} # and it does not come back
|
||||||
|
|
||||||
|
|
||||||
|
def test_untagged_roll_edited_before_the_bridge_looked(env):
|
||||||
|
env["sm"].add(5)
|
||||||
|
insert(env, 1, 5)
|
||||||
|
poll(env)
|
||||||
|
put_back(env, 1) # swapped within seconds ...
|
||||||
|
env["reader"].record["channels1"] = channel(1000) # ... and chosen on the touchscreen
|
||||||
|
poll(env, 2)
|
||||||
|
assert slots(env) == {} and env["sm"].db[5]["location"] == "Lager"
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_removal_answers_cc2_again_and_counts(env):
|
||||||
|
env["sm"].add(5, remaining=800)
|
||||||
|
insert(env, 1, 5)
|
||||||
|
poll(env)
|
||||||
|
env["cc2"].slots[1]["printer"] = {"status": 0}
|
||||||
|
poll(env, 2) # 10 s out, mapping kept
|
||||||
|
put_back(env, 1)
|
||||||
|
poll(env)
|
||||||
|
assert env["cc2"].slots[1]["spool"] == "s0001" and env["cc2"].slots[1]["question"] == ""
|
||||||
|
assert slots(env)["1"]["reinserted"]
|
||||||
|
assert any("erneut zugewiesen" in m for m in msgs(env))
|
||||||
|
env["cc2"].consume("s0001", 2.0)
|
||||||
|
poll(env, 10)
|
||||||
|
assert uses(env) == [("use", 5, 2.0)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_touchscreen_edit_of_scanned_roll_keeps_counting(env):
|
||||||
|
env["sm"].add(5)
|
||||||
|
insert(env, 2, 5)
|
||||||
|
poll(env)
|
||||||
|
env["reader"].record["channels2"] = channel(1000)
|
||||||
|
env["cc2"].slots[2]["spool"] = ""
|
||||||
|
env["cc2"].slots[2]["question"] = "changed"
|
||||||
|
poll(env)
|
||||||
|
assert slots(env)["2"]["spool"] == 5
|
||||||
|
assert env["cc2"].slots[2]["spool"] == "s0001" # question answered with the mapped roll
|
||||||
|
assign_calls = len([c for c in env["cc2"].calls if c[0] == "assign"])
|
||||||
|
env["cc2"].slots[2]["spool"] = ""
|
||||||
|
env["cc2"].slots[2]["question"] = "changed"
|
||||||
|
poll(env, 3, step=5) # not again within a minute
|
||||||
|
assert len([c for c in env["cc2"].calls if c[0] == "assign"]) == assign_calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_take_out_with_roll_still_in_tray_books_from_cc2_log(env):
|
||||||
|
env["sm"].add(5, remaining=800)
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env, 2)
|
||||||
|
env["cc2"].assign(0, None) # "Take out", roll stays
|
||||||
|
poll(env)
|
||||||
|
assert slots(env) == {} and env["sm"].db[5]["location"] == "Lager"
|
||||||
|
t = env["clock"]()
|
||||||
|
env["cc2"].add_log(t + 30, "print", "", 0, -2.98, 1000.0, "/local/benchy.gcode", "complete")
|
||||||
|
env["cc2"].add_log(t + 30, "print", "", 1, -1.0, 300.0, "/local/benchy.gcode", "complete") # other tray
|
||||||
|
poll(env, 3)
|
||||||
|
expected = round(1000 * 3.141592653589793 * 0.875 ** 2 * 1.24 / 1000, 3)
|
||||||
|
assert uses(env) == [("use", 5, expected)]
|
||||||
|
assert any("nachgebucht" in m and "benchy.gcode (fertig)" in m for m in msgs(env))
|
||||||
|
e2 = make_env(env["path"], reader=env["reader"], cc2=env["cc2"], spoolman=env["sm"], clock=env["clock"])
|
||||||
|
poll(e2, 3)
|
||||||
|
assert uses(env) == [("use", 5, expected)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_take_out_window_closes_when_roll_leaves(env):
|
||||||
|
env["sm"].add(5)
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env, 2)
|
||||||
|
env["cc2"].assign(0, None)
|
||||||
|
poll(env)
|
||||||
|
env["cc2"].slots[0]["printer"] = {"status": 0}
|
||||||
|
poll(env)
|
||||||
|
env["clock"].tick(600)
|
||||||
|
env["cc2"].add_log(env["clock"](), "print", "", 0, -3.0, 1000.0, "x.gcode", "complete")
|
||||||
|
poll(env, 2)
|
||||||
|
assert uses(env) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_log_entries_are_not_booked_on_first_start(env):
|
||||||
|
env["sm"].add(5)
|
||||||
|
env["cc2"].add_log(env["clock"]() - 50, "print", "", 0, -3.0, 1000.0, "old.gcode", "complete")
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env, 2)
|
||||||
|
env["cc2"].assign(0, None)
|
||||||
|
poll(env, 3)
|
||||||
|
assert uses(env) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_booking_names_print_and_result(env):
|
||||||
|
env["sm"].add(5, remaining=800)
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env)
|
||||||
|
env["cc2"].job = {"active": True}
|
||||||
|
env["cc2"].consume("s0001", 3.21)
|
||||||
|
poll(env)
|
||||||
|
env["cc2"].job = {"active": False}
|
||||||
|
env["cc2"].add_log(env["clock"](), "print", "s0001", 0, -3.21, 1076.0, "/local/benchy.gcode", "cancelled")
|
||||||
|
poll(env, 10)
|
||||||
|
assert uses(env) == [("use", 5, 3.21)]
|
||||||
|
assert any(m.endswith("gebucht – benchy.gcode (abgebrochen)") for m in msgs(env))
|
||||||
|
assert booked(env)["s0001"]["last"]["details"] == "benchy.gcode (abgebrochen)"
|
||||||
|
env["cc2"].consume("s0001", 1.0) # next booking does not repeat the old print
|
||||||
|
poll(env, 10)
|
||||||
|
assert msgs(env)[0].endswith("auf Spoolman-Spule 5 gebucht")
|
||||||
|
|
||||||
|
|
||||||
|
def test_weigh_in_in_cc2_sets_spoolman_remaining(env):
|
||||||
|
env["sm"].add(5, remaining=800)
|
||||||
|
env["sm"].db[5]["used_weight"] = 200.0
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env)
|
||||||
|
env["cc2"].weigh("s0001", 812.0, env["clock"]()) # more than counted
|
||||||
|
poll(env, 10)
|
||||||
|
assert uses(env) == []
|
||||||
|
assert ("used", 5, 188.0) in env["sm"].calls and env["sm"].db[5]["remaining_weight"] == 812.0
|
||||||
|
env["cc2"].consume("s0001", 2.5)
|
||||||
|
poll(env, 10)
|
||||||
|
assert uses(env) == [("use", 5, 2.5)]
|
||||||
|
assert len([c for c in env["sm"].calls if c[0] == "used"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_weigh_in_lower_is_not_booked_as_use(env):
|
||||||
|
env["sm"].add(5, remaining=800)
|
||||||
|
env["sm"].db[5]["used_weight"] = 200.0
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env)
|
||||||
|
env["cc2"].weigh("s0001", 790.0, env["clock"]())
|
||||||
|
poll(env, 10)
|
||||||
|
assert uses(env) == [] and env["sm"].db[5]["remaining_weight"] == 790.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_runout_and_backup_roll(env):
|
||||||
|
env["sm"].add(5, remaining=40)
|
||||||
|
env["sm"].add(8, remaining=1000, color="FFFFFF")
|
||||||
|
insert(env, 0, 5)
|
||||||
|
insert(env, 1, 8, color="0xFFFFFF")
|
||||||
|
poll(env)
|
||||||
|
cc2 = env["cc2"]
|
||||||
|
cc2.job = {"active": True}
|
||||||
|
cc2.consume("s0001", 35.0) # counted 5 g left when it ran out
|
||||||
|
poll(env)
|
||||||
|
t = env["clock"]()
|
||||||
|
cc2.slots[0]["printer"] = {"status": 0}
|
||||||
|
cc2.slots[0]["spool"] = ""
|
||||||
|
cc2.add_log(t, "print", "s0001", 0, -35.0, 11700.0, "vase.gcode", "runout")
|
||||||
|
cc2.add_log(t, "runout", "s0001", 0, -5.0, 0, "vase.gcode", "runout")
|
||||||
|
cc2.consume("s0001", 5.0)
|
||||||
|
cc2.consume("s0002", 12.0) # the printer goes on with the other roll
|
||||||
|
poll(env, 6)
|
||||||
|
assert uses(env) == [] # still printing
|
||||||
|
assert "0" not in slots(env) and env["sm"].db[5]["location"] == "Lager"
|
||||||
|
cc2.job = {"active": False}
|
||||||
|
cc2.add_log(env["clock"](), "print", "s0002", 1, -12.0, 4000.0, "vase.gcode", "complete")
|
||||||
|
poll(env, 10)
|
||||||
|
assert sorted(uses(env)) == [("use", 5, 40.0), ("use", 8, 12.0)]
|
||||||
|
assert any("Rolle leergelaufen" in m for m in msgs(env))
|
||||||
|
|
||||||
|
|
||||||
|
def test_multicolour_print_books_each_roll(env):
|
||||||
|
for sid in (5, 6, 7):
|
||||||
|
env["sm"].add(sid, remaining=900)
|
||||||
|
insert(env, 0, 5)
|
||||||
|
insert(env, 1, 6)
|
||||||
|
insert(env, 3, 7)
|
||||||
|
poll(env)
|
||||||
|
cc2 = env["cc2"]
|
||||||
|
cc2.job = {"active": True}
|
||||||
|
for _ in range(4):
|
||||||
|
cc2.consume("s0001", 1.5)
|
||||||
|
cc2.consume("s0002", 0.75)
|
||||||
|
poll(env)
|
||||||
|
cc2.job = {"active": False}
|
||||||
|
poll(env, 10)
|
||||||
|
assert sorted(uses(env)) == [("use", 5, 6.0), ("use", 6, 3.0)]
|
||||||
+60
-4
@@ -3,8 +3,11 @@
|
|||||||
python tools/mock_env.py --host mock --printers 2
|
python tools/mock_env.py --host mock --printers 2
|
||||||
|
|
||||||
Per printer i: SSH 2222+i (keys added via POST /authorize), CC2 Control 8081+i. Spoolman 7912, control 8099.
|
Per printer i: SSH 2222+i (keys added via POST /authorize), CC2 Control 8081+i. Spoolman 7912, control 8099.
|
||||||
Control endpoints (port 8099, optional &printer=i): POST /insert?slot=&spool=, /remove?slot=,
|
Control endpoints (port 8099, optional &printer=i): POST /insert?slot=&spool= (scanned roll in),
|
||||||
/consume?slot=&grams=, /job?active=0|1, /touch?slot= (touchscreen edit: weight falls back to 1000).
|
/remove?slot= (roll out, the record keeps its tag data), /putback?slot= (roll in without a scan),
|
||||||
|
/takeout?slot= (CC2 Control's "Take out"), /consume?slot=&grams=, /job?active=0|1,
|
||||||
|
/touch?slot= (touchscreen edit: weight falls back to 1000), /print?slot=&mm=&job=&result= (logged use of
|
||||||
|
a tray, charged to its spool if it has one), /weigh?spool=&remaining= (weigh-in in CC2 Control).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -15,6 +18,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
@@ -32,6 +36,7 @@ def printer_state():
|
|||||||
"slots": [{"slot": i, "spool": "", "last": "", "question": "", "printer": {"status": 0}} for i in range(5)],
|
"slots": [{"slot": i, "spool": "", "last": "", "question": "", "printer": {"status": 0}} for i in range(5)],
|
||||||
"spools": [],
|
"spools": [],
|
||||||
"job": {"active": False, "file": "", "slots": []},
|
"job": {"active": False, "file": "", "slots": []},
|
||||||
|
"log": [],
|
||||||
"ids": (f"m{i:04d}" for i in itertools.count(1)),
|
"ids": (f"m{i:04d}" for i in itertools.count(1)),
|
||||||
}
|
}
|
||||||
st["slots"][4]["printer"] = None
|
st["slots"][4]["printer"] = None
|
||||||
@@ -91,7 +96,7 @@ def make_cc2(host_header: str, P: dict) -> FastAPI:
|
|||||||
with LOCK:
|
with LOCK:
|
||||||
STATE["requests"].append("GET /api/spools")
|
STATE["requests"].append("GET /api/spools")
|
||||||
return {"available": True, "enabled": P["enabled"], "slots": P["slots"], "spools": P["spools"],
|
return {"available": True, "enabled": P["enabled"], "slots": P["slots"], "spools": P["spools"],
|
||||||
"job": P["job"], "log": []}
|
"job": P["job"], "log": P["log"]}
|
||||||
|
|
||||||
@app.get("/api/canvas")
|
@app.get("/api/canvas")
|
||||||
def canvas():
|
def canvas():
|
||||||
@@ -178,6 +183,9 @@ def make_spoolman() -> FastAPI:
|
|||||||
if value is not None and key not in STATE["fields"]:
|
if value is not None and key not in STATE["fields"]:
|
||||||
raise HTTPException(400, f"Unknown extra field {key}")
|
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",)})
|
||||||
|
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"]
|
||||||
for key, value in (body.get("extra") or {}).items():
|
for key, value in (body.get("extra") or {}).items():
|
||||||
if value is None:
|
if value is None:
|
||||||
sp["extra"].pop(key, None)
|
sp["extra"].pop(key, None)
|
||||||
@@ -235,7 +243,55 @@ def make_control() -> FastAPI:
|
|||||||
P = PRINTERS[printer]
|
P = PRINTERS[printer]
|
||||||
with LOCK:
|
with LOCK:
|
||||||
P["slots"][slot]["printer"] = {"status": 0}
|
P["slots"][slot]["printer"] = {"status": 0}
|
||||||
P["record"][f"channels{slot}"] = channel("")
|
if P["slots"][slot]["spool"]:
|
||||||
|
P["slots"][slot]["last"] = P["slots"][slot]["spool"]
|
||||||
|
P["slots"][slot]["spool"] = ""
|
||||||
|
P["slots"][slot]["question"] = ""
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
@app.post("/putback")
|
||||||
|
def putback(slot: int, printer: int = 0):
|
||||||
|
P = PRINTERS[printer]
|
||||||
|
with LOCK:
|
||||||
|
P["slots"][slot]["printer"] = {"status": 1}
|
||||||
|
P["slots"][slot]["spool"] = ""
|
||||||
|
P["slots"][slot]["question"] = "inserted"
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
@app.post("/takeout")
|
||||||
|
def takeout(slot: int, printer: int = 0):
|
||||||
|
P = PRINTERS[printer]
|
||||||
|
with LOCK:
|
||||||
|
P["slots"][slot]["last"] = P["slots"][slot]["spool"]
|
||||||
|
P["slots"][slot]["spool"] = ""
|
||||||
|
P["slots"][slot]["question"] = ""
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
def log(P, kind, spool, slot, grams, mm, job="", result=""):
|
||||||
|
P["log"].insert(0, {"time": int(time.time()), "spool": spool, "slot": slot, "kind": kind, "grams": grams,
|
||||||
|
"mm": mm, "job": job, "result": result})
|
||||||
|
del P["log"][150:]
|
||||||
|
|
||||||
|
@app.post("/print")
|
||||||
|
def print_(slot: int, mm: float, job: str = "test.gcode", result: str = "complete", printer: int = 0):
|
||||||
|
P = PRINTERS[printer]
|
||||||
|
with LOCK:
|
||||||
|
sid = P["slots"][slot]["spool"]
|
||||||
|
sp = next((s for s in P["spools"] if s["id"] == sid), None)
|
||||||
|
d, rho = (sp["diameter"], sp["density"]) if sp else (1.75, 1.24)
|
||||||
|
grams = mm * 3.141592653589793 * (d / 2) ** 2 * rho / 1000
|
||||||
|
if sp:
|
||||||
|
sp["remaining"] -= grams
|
||||||
|
log(P, "print", sid, slot, -round(grams, 3), mm, job, result)
|
||||||
|
return {"ok": True, "grams": grams}
|
||||||
|
|
||||||
|
@app.post("/weigh")
|
||||||
|
def weigh(spool: str, remaining: float, printer: int = 0):
|
||||||
|
P = PRINTERS[printer]
|
||||||
|
with LOCK:
|
||||||
|
sp = next(s for s in P["spools"] if s["id"] == spool)
|
||||||
|
log(P, "correction", spool, -1, remaining - sp["remaining"], 0, "", "weighed")
|
||||||
|
sp["remaining"] = remaining
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
@app.post("/touch")
|
@app.post("/touch")
|
||||||
|
|||||||
Reference in new issue
Block a user