Runout sets Spoolman to zero; surface prints waiting on a spool
- A roll that ran out is set to 0 g in Spoolman (like a weigh-in) instead of booking CC2 Control's delta, so a mirror whose count was off cannot book too much or too little. - A tray that printed while CC2 Control still asked which spool it holds (untagged roll) shows the waiting millimetres on the status page and gets one log warning after the print. 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
c04964066f
commit
d03668e3c5
3 files changed
+85
-9
No files matched your search
+39
-6
@@ -199,6 +199,7 @@ class PrinterWorker:
|
|||||||
self.record: dict | None = None
|
self.record: dict | None = None
|
||||||
self.cc2_data: 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.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
|
@property
|
||||||
def id(self) -> str:
|
def id(self) -> str:
|
||||||
@@ -267,6 +268,7 @@ class PrinterWorker:
|
|||||||
self._reconcile_cc2(cc2)
|
self._reconcile_cc2(cc2)
|
||||||
self._process_cc2_log(cc2)
|
self._process_cc2_log(cc2)
|
||||||
self._consumption(cc2)
|
self._consumption(cc2)
|
||||||
|
self._note_open_questions(cc2)
|
||||||
|
|
||||||
def _record_id(self, n: int) -> int | None:
|
def _record_id(self, n: int) -> int | None:
|
||||||
for s in self.record_slots:
|
for s in self.record_slots:
|
||||||
@@ -406,6 +408,23 @@ class PrinterWorker:
|
|||||||
elif loaded:
|
elif loaded:
|
||||||
self._answer_again(n, m, question)
|
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:
|
def _reinserted(self, n: int) -> None:
|
||||||
"""No mapping, CC2 Control asks about filament put in, and the record still names the roll
|
"""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."""
|
that last left this slot: it is that roll again."""
|
||||||
@@ -531,6 +550,11 @@ class PrinterWorker:
|
|||||||
if settled:
|
if settled:
|
||||||
self._weighin(cid, b, name, smid, rem, newest)
|
self._weighin(cid, b, name, smid, rem, newest)
|
||||||
continue
|
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
|
delta = b["remaining"] - rem
|
||||||
if delta < -MIN_BOOKING:
|
if delta < -MIN_BOOKING:
|
||||||
self._log("warn", f"{name}: Rest in CC2 Control um {-delta:.2f} g gestiegen; "
|
self._log("warn", f"{name}: Rest in CC2 Control um {-delta:.2f} g gestiegen; "
|
||||||
@@ -589,24 +613,32 @@ class PrinterWorker:
|
|||||||
details, log_time)
|
details, log_time)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _weighin(self, cid: str, b: dict, name: str, smid: int, rem: float, log_time: float) -> None:
|
def _weighin(self, cid: str, b: dict, name: str, smid: int, rem: float, log_time: float,
|
||||||
"""Weighed in CC2 Control: Spoolman takes the weighed value (and any use since) as it is."""
|
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)
|
sm = self.bridge.spoolman.spool(smid)
|
||||||
base = sm.get("initial_weight") or (sm.get("filament") or {}).get("weight")
|
base = sm.get("initial_weight") or (sm.get("filament") or {}).get("weight")
|
||||||
if not base:
|
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 "
|
self.bridge._once_error(f"{self.id}:weigh:{cid}", f"{self.name}: {name}: Spoolman-Spule {smid} hat kein "
|
||||||
"Startgewicht; Nachwiegen nicht übernommen")
|
f"Startgewicht; {what}")
|
||||||
return
|
return False
|
||||||
target = max(0.0, rem)
|
target = max(0.0, rem)
|
||||||
used = round(max(0.0, float(base) - target), 3)
|
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 "
|
prev = _num(sm.get("remaining_weight"), float(base) - _num(sm.get("used_weight")))
|
||||||
"(in CC2 Control nachgewogen)",
|
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}))
|
lambda: self.bridge.spoolman.update_spool(smid, {"used_weight": used}))
|
||||||
if done:
|
if done:
|
||||||
b["remaining"] = rem
|
b["remaining"] = rem
|
||||||
b["log_time"] = max(b.get("log_time") or 0.0, log_time)
|
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.spool_cache.pop(smid, None)
|
||||||
self.bridge.state.save()
|
self.bridge.state.save()
|
||||||
|
return True
|
||||||
|
|
||||||
# ---------- CC2 Control's log: use of a tray left without spool after "Take out" ----------
|
# ---------- 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,
|
"spoolman": summary(cached[1]) if cached else None,
|
||||||
"cc2_spool": cc2_spools.get(sid) if sid else None,
|
"cc2_spool": cc2_spools.get(sid) if sid else None,
|
||||||
"cc2_question": cs.get("question") or "",
|
"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"]),
|
"cc2_matches": bool(m and m.get("cc2") and sid == m["cc2"]),
|
||||||
"last_booking": (b or {}).get("last"),
|
"last_booking": (b or {}).get("last"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -120,6 +120,14 @@ function info(s) {
|
|||||||
out.title = 'Ohne Spoolman-Tag';
|
out.title = 'Ohne Spoolman-Tag';
|
||||||
out.hint = 'Verbrauch wird nicht gebucht. Rolle taggen und scannen, um sie zu verwalten.';
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -195,7 +203,8 @@ function slotCard(p, s) {
|
|||||||
<span style="min-width:0"><span class="nm" style="display:block;font-size:15px">${esc(i.title)}</span>
|
<span style="min-width:0"><span class="nm" style="display:block;font-size:15px">${esc(i.title)}</span>
|
||||||
<span class="small muted" style="display:block">${esc(i.sub)}</span></span>
|
<span class="small muted" style="display:block">${esc(i.sub)}</span></span>
|
||||||
</button>
|
</button>
|
||||||
<p class="hint${i.kind === 'takeout' ? ' warn' : ''}" style="margin-top:auto">${esc(i.hint)}</p>`;
|
${i.question ? `<div><span class="chip warn">${esc(i.question)}</span></div>` : ''}
|
||||||
|
<p class="hint${i.kind === 'takeout' || i.warn ? ' warn' : ''}" style="margin-top:auto">${esc(i.hint)}</p>`;
|
||||||
}
|
}
|
||||||
return `<article class="card slot"><div class="sh"><b>Slot ${s.slot + 1}</b>${tray}</div>${body}</article>`;
|
return `<article class="card slot"><div class="sh"><b>Slot ${s.slot + 1}</b>${tray}</div>${body}</article>`;
|
||||||
}
|
}
|
||||||
@@ -205,6 +214,7 @@ function slotRow(p, s) {
|
|||||||
const inner = `<span style="font-weight:800;font-size:13px">${s.slot + 1}</span>`;
|
const inner = `<span style="font-weight:800;font-size:13px">${s.slot + 1}</span>`;
|
||||||
let title = i.title, sub = i.sub, rest = '';
|
let title = i.title, sub = i.sub, rest = '';
|
||||||
if (i.kind === 'empty') { title = 'Leer'; sub = 'Kein Filament'; }
|
if (i.kind === 'empty') { title = 'Leer'; sub = 'Kein Filament'; }
|
||||||
|
if (i.kind === 'unmanaged' && i.question) sub = i.warn ? `${fmtNum(s.cc2_question_mm)} mm ohne Spule gedruckt` : i.question;
|
||||||
if (i.kind === 'managed') {
|
if (i.kind === 'managed') {
|
||||||
rest = i.rest == null ? '?' : Math.round(i.rest) + ' g';
|
rest = i.rest == null ? '?' : Math.round(i.rest) + ' g';
|
||||||
sub = [i.sub, '#' + s.mapping.spool, i.question || i.cc2[0]].filter(Boolean).join(' · ');
|
sub = [i.sub, '#' + s.mapping.spool, i.question || i.cc2[0]].filter(Boolean).join(' · ');
|
||||||
@@ -282,7 +292,7 @@ function sheetHtml(p, s) {
|
|||||||
<h2 id="sheet-title" style="overflow-wrap:anywhere">${esc(i.title || 'Leer')}</h2>${i.sub ? `<div class="small muted">${esc(i.sub)}</div>` : ''}</div>
|
<h2 id="sheet-title" style="overflow-wrap:anywhere">${esc(i.title || 'Leer')}</h2>${i.sub ? `<div class="small muted">${esc(i.sub)}</div>` : ''}</div>
|
||||||
<button aria-label="Schließen" onclick="closeSheet()" style="min-width:40px;padding:0;border-radius:99px">✕</button>
|
<button aria-label="Schließen" onclick="closeSheet()" style="min-width:40px;padding:0;border-radius:99px">✕</button>
|
||||||
</div>
|
</div>
|
||||||
${i.hint ? `<p class="hint${i.kind === 'takeout' ? ' warn' : ''}">${esc(i.hint)}</p>` : ''}
|
${i.hint ? `<p class="hint${i.kind === 'takeout' || i.warn ? ' warn' : ''}">${esc(i.hint)}</p>` : ''}
|
||||||
<dl class="kv">${rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')}</dl>
|
<dl class="kv">${rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')}</dl>
|
||||||
${m ? `<div class="row" style="flex-wrap:nowrap"><button style="flex:1;min-height:48px" onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Erneut senden</button>
|
${m ? `<div class="row" style="flex-wrap:nowrap"><button style="flex:1;min-height:48px" onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Erneut senden</button>
|
||||||
<button class="danger" style="flex:1;min-height:48px" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button></div>` : ''}`;
|
<button class="danger" style="flex:1;min-height:48px" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button></div>` : ''}`;
|
||||||
|
|||||||
+34
-1
@@ -740,8 +740,41 @@ def test_runout_and_backup_roll(env):
|
|||||||
cc2.job = {"active": False}
|
cc2.job = {"active": False}
|
||||||
cc2.add_log(env["clock"](), "print", "s0002", 1, -12.0, 4000.0, "vase.gcode", "complete")
|
cc2.add_log(env["clock"](), "print", "s0002", 1, -12.0, 4000.0, "vase.gcode", "complete")
|
||||||
poll(env, 10)
|
poll(env, 10)
|
||||||
assert sorted(uses(env)) == [("use", 5, 40.0), ("use", 8, 12.0)]
|
assert uses(env) == [("use", 8, 12.0)]
|
||||||
|
assert ("used", 5, 1000.0) in env["sm"].calls # ran out: Spoolman's rest is 0, whatever the count said
|
||||||
|
assert env["sm"].db[5]["remaining_weight"] == 0
|
||||||
assert any("Rolle leergelaufen" in m for m in msgs(env))
|
assert any("Rolle leergelaufen" in m for m in msgs(env))
|
||||||
|
assert booked(env)["s0001"]["last"]["grams"] == 40.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_runout_sets_spoolman_to_zero_even_if_the_mirror_was_off(env):
|
||||||
|
env["sm"].add(5, remaining=300)
|
||||||
|
insert(env, 0, 5)
|
||||||
|
poll(env)
|
||||||
|
cc2 = env["cc2"]
|
||||||
|
cc2.library[0]["remaining"] = 400.0 # e.g. an old mirror reused after the roll printed elsewhere
|
||||||
|
booked(env)["s0001"]["remaining"] = 400.0
|
||||||
|
cc2.job = {"active": True}
|
||||||
|
cc2.consume("s0001", 380.0)
|
||||||
|
poll(env)
|
||||||
|
cc2.slots[0]["printer"] = {"status": 0}
|
||||||
|
cc2.slots[0]["spool"] = ""
|
||||||
|
t = env["clock"]()
|
||||||
|
cc2.add_log(t, "runout", "s0001", 0, -20.0, 0, "big.gcode", "runout")
|
||||||
|
cc2.library[0]["remaining"] = 0.0
|
||||||
|
cc2.job = {"active": False}
|
||||||
|
cc2.add_log(t + 5, "print", "s0001", 0, -380.0, 127000.0, "big.gcode", "runout")
|
||||||
|
poll(env, 15)
|
||||||
|
assert uses(env) == [] and env["sm"].db[5]["remaining_weight"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_question_after_print_is_reported_once(env):
|
||||||
|
cc2 = env["cc2"]
|
||||||
|
cc2.slots[3].update({"printer": {"status": 1}, "question": "changed", "question_since": 100, "question_mm": 2605.0})
|
||||||
|
poll(env, 3)
|
||||||
|
warn = [m for m in msgs(env) if "ohne Spule gedruckt" in m]
|
||||||
|
assert len(warn) == 1 and "2605 mm" in warn[0] and "7.8 g" in warn[0]
|
||||||
|
assert env["bridge"].snapshot["printers"][0]["slots"][3]["cc2_question_mm"] == 2605.0
|
||||||
|
|
||||||
|
|
||||||
def test_multicolour_print_books_each_roll(env):
|
def test_multicolour_print_books_each_roll(env):
|
||||||
|
|||||||
Reference in new issue
Block a user