Spoolman leads CC2 Control's copies; new spools from the bridge
- Master data of CC2 Control's copies follows Spoolman (name, brand, colour, diameter, density, net, tare, price; not the material, which was fitted to the tray). - A loaded spool's rest in CC2 Control is set to Spoolman's via /api/spools/adjust once everything is booked (old copy after the roll printed in the other printer, weigh-in in Spoolman); the resulting correction is not booked back. - Copies of spools archived or deleted in Spoolman are deleted from CC2 Control when no tray holds them (library limit 128); an outage never counts as deleted. - A roll that ran out is archived in Spoolman (setting, default on); a scan of an archived spool maps nothing. - A roll scanned again with unchanged tag data and put back into its old slot (after printing in the other printer) is recognised. - Tag generator: "Neue Spule" creates spools in Spoolman from an existing or a new filament (and vendor), labelled vendor + id, then opens the tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
1 parent
d03668e3c5
commit
b37cd64909
13 files changed
+873
-27
No files matched your search
+163
-4
@@ -93,8 +93,10 @@ def test_existing_mirror_is_reused(env):
|
||||
env["cc2"].calls.clear()
|
||||
insert(env, 0, 5)
|
||||
poll(env)
|
||||
assert [c for c in env["cc2"].calls if c[0] == "save"] == []
|
||||
assert [c for c in env["cc2"].calls if c[0] == "save" and "id" not in c[1]] == [] # not created again
|
||||
assert ("assign", 0, "s0001") in env["cc2"].calls
|
||||
# the copy made by hand lacked brand and empty weight: taken from Spoolman
|
||||
assert ("save", {"id": "s0001", "brand": "SUNLU", "tare": 150.0}) in env["cc2"].calls
|
||||
|
||||
|
||||
def test_unknown_spool_id_is_ignored(env):
|
||||
@@ -526,7 +528,7 @@ def test_migration_from_v1(tmp_path):
|
||||
assert slots(e)["1"]["spool"] == 1 and booked(e)["abc"]["remaining"] == 250.0
|
||||
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 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] == "assign" or (c[0] == "save" and "id" not in c[1])] # nothing re-created
|
||||
|
||||
|
||||
# ---------- v3: putting rolls back, CC2 Control's log ----------
|
||||
@@ -743,8 +745,9 @@ def test_runout_and_backup_roll(env):
|
||||
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 booked(env)["s0001"]["last"]["grams"] == 40.0
|
||||
assert any("Rolle leergelaufen" in m and "archivieren" in m for m in msgs(env))
|
||||
assert env["sm"].db[5]["archived"] # empty: archived in Spoolman ...
|
||||
assert ("delete", "s0001") in env["cc2"].calls # ... and its copy leaves CC2 Control
|
||||
|
||||
|
||||
def test_runout_sets_spoolman_to_zero_even_if_the_mirror_was_off(env):
|
||||
@@ -825,3 +828,159 @@ def test_printer_names_written_are_remembered(env):
|
||||
insert(env, 0, 5)
|
||||
poll(env)
|
||||
assert env["bridge"].state.data["written_printers"] == ["Rechts"]
|
||||
|
||||
|
||||
def test_rest_follows_spoolman_when_roll_comes_back(tmp_path):
|
||||
e = make_env(tmp_path, printers=2)
|
||||
for c in e["cc2s"].values():
|
||||
c.clock = e["clock"]
|
||||
e["sm"].add(5, remaining=800)
|
||||
insert(e, 0, 5)
|
||||
poll(e, 2)
|
||||
take_out_of_tray(e, 0)
|
||||
e["readers"]["p1"].record["channels0"] = channel("")
|
||||
insert(e, 1, 5, pid="p2") # into the other printer: a fresh copy there with 800 g
|
||||
poll(e, 6)
|
||||
e["cc2s"]["p2"].consume("s0001", 100.0)
|
||||
poll(e, 15)
|
||||
assert e["sm"].db[5]["remaining_weight"] == 700
|
||||
take_out_of_tray(e, 1, pid="p2")
|
||||
e["readers"]["p2"].record["channels1"] = channel("")
|
||||
insert(e, 0, 5) # back into the first printer: its old copy still says 800 g
|
||||
poll(e, 30)
|
||||
assert ("adjust", "s0001", 700.0) in e["cc2s"]["p1"].calls
|
||||
assert e["cc2s"]["p1"].library[0]["remaining"] == 700.0
|
||||
assert [c for c in e["sm"].calls if c[0] in ("use", "used")] == [("use", 5, 100.0)] # nothing booked back
|
||||
poll(e, 30)
|
||||
assert len([c for c in e["cc2s"]["p1"].calls if c[0] == "adjust"]) == 1
|
||||
|
||||
|
||||
def test_weighed_in_spoolman_goes_to_cc2(env):
|
||||
env["cc2"].clock = env["clock"]
|
||||
env["sm"].add(5, remaining=800)
|
||||
insert(env, 0, 5)
|
||||
poll(env, 2)
|
||||
env["sm"].db[5]["remaining_weight"] = 612.0 # weighed and entered in Spoolman
|
||||
poll(env, 30)
|
||||
assert ("adjust", "s0001", 612.0) in env["cc2"].calls
|
||||
assert not [c for c in env["sm"].calls if c[0] in ("use", "used")]
|
||||
assert booked(env)["s0001"]["remaining"] == 612.0 and "adjusted" not in booked(env)["s0001"]
|
||||
|
||||
|
||||
def test_rest_not_adjusted_before_use_is_booked(env):
|
||||
env["sm"].add(5, remaining=800)
|
||||
insert(env, 0, 5)
|
||||
poll(env, 2)
|
||||
env["cc2"].job = {"active": True}
|
||||
env["cc2"].consume("s0001", 50.0)
|
||||
poll(env, 20) # printing: neither booked nor adjusted
|
||||
assert not [c for c in env["cc2"].calls if c[0] == "adjust"]
|
||||
env["cc2"].job = {"active": False}
|
||||
poll(env, 30)
|
||||
assert uses(env) == [("use", 5, 50.0)]
|
||||
assert not [c for c in env["cc2"].calls if c[0] == "adjust"]
|
||||
|
||||
|
||||
def test_master_data_follows_spoolman(env):
|
||||
env["sm"].add(5, color="4A4A4A")
|
||||
insert(env, 0, 5)
|
||||
poll(env, 2)
|
||||
env["sm"].db[5]["filament"]["color_hex"] = "898989"
|
||||
env["sm"].db[5]["filament"]["density"] = 1.3
|
||||
env["bridge"].spool_cache.clear()
|
||||
poll(env, 14)
|
||||
assert ("save", {"id": "s0001", "color": "#898989", "density": 1.3}) in env["cc2"].calls
|
||||
assert env["cc2"].library[0]["color"] == "#898989"
|
||||
poll(env, 14)
|
||||
assert len([c for c in env["cc2"].calls if c[0] == "save" and "id" in c[1]]) == 1
|
||||
|
||||
|
||||
def test_copies_of_archived_or_deleted_spools_are_removed_when_not_loaded(env):
|
||||
env["sm"].add(5)
|
||||
env["sm"].add(6)
|
||||
env["sm"].add(7)
|
||||
insert(env, 0, 5)
|
||||
insert(env, 1, 6)
|
||||
insert(env, 2, 7)
|
||||
poll(env, 2)
|
||||
take_out_of_tray(env, 0)
|
||||
take_out_of_tray(env, 1)
|
||||
poll(env, 6)
|
||||
env["sm"].db[5]["archived"] = True
|
||||
del env["sm"].db[6]
|
||||
env["sm"].db[7]["archived"] = True # still in a tray: kept
|
||||
env["bridge"].spool_cache.clear()
|
||||
poll(env, 14)
|
||||
deleted = [c[1] for c in env["cc2"].calls if c[0] == "delete"]
|
||||
assert deleted == ["s0001", "s0002"]
|
||||
assert [s["id"] for s in env["cc2"].library] == ["s0003"]
|
||||
|
||||
|
||||
def test_scan_of_archived_spool_is_not_mapped(env):
|
||||
env["sm"].add(5)["archived"] = True
|
||||
insert(env, 0, 5)
|
||||
poll(env, 2)
|
||||
assert slots(env) == {} and not env["cc2"].calls
|
||||
assert any("ist archiviert" in m for m in msgs(env))
|
||||
|
||||
|
||||
def test_runout_does_not_archive_when_switched_off(env):
|
||||
env["settings"].save({"archive_empty": False})
|
||||
env["sm"].add(5, remaining=40)
|
||||
insert(env, 0, 5)
|
||||
poll(env)
|
||||
cc2 = env["cc2"]
|
||||
cc2.job = {"active": True}
|
||||
cc2.consume("s0001", 40.0)
|
||||
poll(env)
|
||||
cc2.slots[0]["printer"] = {"status": 0}
|
||||
cc2.slots[0]["spool"] = ""
|
||||
cc2.add_log(env["clock"](), "runout", "s0001", 0, 0.0, 0, "vase.gcode", "runout")
|
||||
cc2.job = {"active": False}
|
||||
poll(env, 15)
|
||||
assert env["sm"].db[5]["remaining_weight"] == 0 and not env["sm"].db[5]["archived"]
|
||||
|
||||
|
||||
def test_roll_back_in_its_old_slot_after_the_other_printer(tmp_path):
|
||||
"""Scanned again with identical tag data: the record does not change, CC2 Control asks; the roll is recognised."""
|
||||
e = make_env(tmp_path, printers=2)
|
||||
e["sm"].add(5, remaining=800)
|
||||
insert(e, 3, 5)
|
||||
poll(e, 2)
|
||||
take_out_of_tray(e, 3)
|
||||
insert(e, 0, 5, pid="p2") # scanned and loaded in the other printer
|
||||
poll(e, 6)
|
||||
assert slots(e, "p2")["0"]["spool"] == 5 and slots(e) == {}
|
||||
take_out_of_tray(e, 0, pid="p2")
|
||||
e["readers"]["p2"].record["channels0"] = channel("")
|
||||
poll(e, 6)
|
||||
assert slots(e, "p2") == {}
|
||||
put_back(e, 3) # scanned at the side again (same data) and back into its old tray
|
||||
poll(e, 2)
|
||||
assert slots(e)["3"]["spool"] == 5 and slots(e)["3"].get("reinserted")
|
||||
assert e["cc2s"]["p1"].slots[3]["spool"] == "s0001"
|
||||
|
||||
|
||||
def test_archived_spool_with_roll_in_tray_is_not_deleted(env):
|
||||
env["sm"].add(5)
|
||||
insert(env, 0, 5)
|
||||
poll(env, 2)
|
||||
env["bridge"].release(env["bridge"].workers["p1"], 0, "Test")
|
||||
env["cc2"].slots[0]["spool"] = "" # unbound, roll still in the tray with its tag
|
||||
env["sm"].db[5]["archived"] = True
|
||||
env["bridge"].spool_cache.clear()
|
||||
poll(env, 14)
|
||||
assert not [c for c in env["cc2"].calls if c[0] == "delete"]
|
||||
|
||||
|
||||
def test_spoolman_outage_does_not_delete_copies(env):
|
||||
env["sm"].add(5)
|
||||
insert(env, 0, 5)
|
||||
poll(env, 2)
|
||||
take_out_of_tray(env, 0)
|
||||
poll(env, 6)
|
||||
env["bridge"].spool_cache.clear()
|
||||
env["sm"].fail = True
|
||||
poll(env, 14)
|
||||
assert not [c for c in env["cc2"].calls if c[0] == "delete"]
|
||||
assert len(env["cc2"].library) == 1
|
||||
Reference in new issue
Block a user