diff --git a/app/bridge.py b/app/bridge.py index 34b27b5..842c3d1 100644 --- a/app/bridge.py +++ b/app/bridge.py @@ -199,6 +199,7 @@ class PrinterWorker: self.record: dict | None = None self.cc2_data: dict | None = None self.print_info: dict | None = None # CC2 Control /api/printer, only read while a job runs + self.noted_questions: dict[int, str] = {} # slot -> question already reported @property def id(self) -> str: @@ -267,6 +268,7 @@ class PrinterWorker: self._reconcile_cc2(cc2) self._process_cc2_log(cc2) self._consumption(cc2) + self._note_open_questions(cc2) def _record_id(self, n: int) -> int | None: for s in self.record_slots: @@ -406,6 +408,23 @@ class PrinterWorker: elif loaded: self._answer_again(n, m, question) + def _note_open_questions(self, cc2: dict) -> None: + """A tray printed while CC2 Control was still asking which spool it holds: say so once, after the print.""" + if (cc2.get("job") or {}).get("active"): + return + for slot in cc2.get("slots") or []: + n = slot.get("slot") + mm = _num(slot.get("question_mm")) + if n not in (0, 1, 2, 3) or not slot.get("question") or mm < 1 or str(n) in self.st["slots"]: + continue + key = f"{n}|{slot.get('question_since')}" + if self.noted_questions.get(n) == key: + continue + self.noted_questions[n] = key + self._log("warn", f"Slot {n + 1}: {mm:.0f} mm (ca. {grams_from_mm(mm, 1.75, 1.24):.1f} g) ohne Spule gedruckt; " + "CC2 Control wartet auf die Zuordnung. Rolle taggen und scannen, dann wird der Verbrauch " + "nachgebucht, oder in CC2 Control „keine Spule“ wählen.") + 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.""" @@ -531,6 +550,11 @@ class PrinterWorker: if settled: self._weighin(cid, b, name, smid, rem, newest) continue + if any(e.get("kind") == "runout" for e in fresh): + # CC2 Control sets a roll that ran out to zero; Spoolman follows absolutely, so a count + # that was off (e.g. a mirror left with an old value) does not book too much or too little + if not settled or self._weighin(cid, b, name, smid, rem, newest, describe(fresh)): + continue delta = b["remaining"] - rem if delta < -MIN_BOOKING: self._log("warn", f"{name}: Rest in CC2 Control um {-delta:.2f} g gestiegen; " @@ -589,24 +613,32 @@ class PrinterWorker: 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.""" + def _weighin(self, cid: str, b: dict, name: str, smid: int, rem: float, log_time: float, + runout: str | None = None) -> bool: + """Weighed in CC2 Control, or ran out (`runout` holds the print details): Spoolman takes CC2 Control's + value as it is. False when Spoolman has no start weight to set it against.""" sm = self.bridge.spoolman.spool(smid) base = sm.get("initial_weight") or (sm.get("filament") or {}).get("weight") if not base: + what = "Leerlaufen per Verbrauch gebucht" if runout is not None else "Nachwiegen nicht übernommen" self.bridge._once_error(f"{self.id}:weigh:{cid}", f"{self.name}: {name}: Spoolman-Spule {smid} hat kein " - "Startgewicht; Nachwiegen nicht übernommen") - return + f"Startgewicht; {what}") + return False 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)", + prev = _num(sm.get("remaining_weight"), float(base) - _num(sm.get("used_weight"))) + why = f"(Rolle leergelaufen){runout}" if runout is not None else "(in CC2 Control nachgewogen)" + _, done = self._write(f"weigh:{cid}", f"in Spoolman den Rest von {name} auf {target:.1f} g setzen {why}", 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) + if runout is not None: + b["last"] = {"time": self.bridge.clock(), "grams": round(max(0.0, prev - target), 3), + "details": ("Rolle leergelaufen" + runout).replace("leergelaufen – ", "leergelaufen: ")} self.bridge.spool_cache.pop(smid, None) self.bridge.state.save() + return True # ---------- CC2 Control's log: use of a tray left without spool after "Take out" ---------- @@ -691,6 +723,7 @@ class PrinterWorker: "spoolman": summary(cached[1]) if cached else None, "cc2_spool": cc2_spools.get(sid) if sid else None, "cc2_question": cs.get("question") or "", + "cc2_question_mm": _num(cs.get("question_mm")) if cs.get("question") else 0.0, "cc2_matches": bool(m and m.get("cc2") and sid == m["cc2"]), "last_booking": (b or {}).get("last"), }) diff --git a/app/templates/status.html b/app/templates/status.html index 4acfdbb..d7daca4 100644 --- a/app/templates/status.html +++ b/app/templates/status.html @@ -120,6 +120,14 @@ function info(s) { out.title = 'Ohne Spoolman-Tag'; out.hint = 'Verbrauch wird nicht gebucht. Rolle taggen und scannen, um sie zu verwalten.'; } + if (s.cc2_question) { + const mm = s.cc2_question_mm || 0; + out.question = 'CC2 fragt: Spule ' + (QUESTION[s.cc2_question] || s.cc2_question); + out.hint = mm >= 1 + ? `Gedruckt ohne Spule: ${fmtNum(mm)} mm (ca. ${fmtNum(mm * Math.PI * 0.765625 * 1.24 / 1000, 1)} g). CC2 Control hält den Verbrauch, bis die Spule feststeht: Rolle taggen und scannen, dann wird er nachgebucht – oder in CC2 Control „keine Spule“ wählen.` + : 'CC2 Control fragt, welche Spule das ist. Rolle taggen und scannen oder in CC2 Control beantworten.'; + out.warn = mm >= 1; + } } return out; } @@ -195,7 +203,8 @@ function slotCard(p, s) { ${esc(i.title)} ${esc(i.sub)} -
${esc(i.hint)}
`; + ${i.question ? `${esc(i.hint)}
`; } return `${esc(i.hint)}
` : ''} + ${i.hint ? `${esc(i.hint)}
` : ''}