Files
Jonas MenzelandClaude Opus 5.5 bf32667f77
CI / test (push) Successful in 1m4s
CI / image (push) Successful in 1m1s
CC2 ↔ Spoolman bridge with NFC tag generator
- Reads /opt/usr/canvas_record_info.json over SSH (pinned host key,
  restricted key), decodes Spoolman spool ids from the tag weight
- Mirrors spools into CC2 Control's spool library and assigns slots,
  keeps its own slot mapping, sets Spoolman locations
- Books the drop of CC2 Control's measured remaining weight to Spoolman
  after each print, idempotent across restarts and interrupted bookings
- Dry-run by default; web UI for settings, SSH key, status and log
- Tag generator for NFC Tools command chains (NTAG213/215/216)
- Tests, mock environment, Dockerfile, Gitea Actions CI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
2026-10-25 03:01:17 +01:00

119 lines
4.9 KiB
Python

import pytest
from app import materials, tagcodec
from app.tagcodec import TagData, encode_commands, parse_commands
TEMPLATE = ("A2:04:0103A00C,A2:05:34030FD1,A2:06:010B5502,A2:07:656C6567,A2:08:6F6F2E63,A2:09:6F6DFE00,"
"A2:10:36EEEEEE,A2:11:EE000000,A2:12:00807665,A2:13:00010000,A2:14:000000FF,A2:15:00BE00E6,"
"A2:16:00000000,A2:17:00AF2711,A2:18:0036C800")
def pla_plus(spool_id, color):
return TagData(spool_id=spool_id, color=color, material="PLA", subtype=0x0001, temp_min=190, temp_max=230)
def test_template_reproduced_exactly():
assert encode_commands(pla_plus(1, "000000")) == TEMPLATE
@pytest.mark.parametrize("spool_id,color,p14,p17", [
(1, "000000", "A2:14:000000FF", "A2:17:00AF2711"),
(5, "2850DF", "A2:14:2850DFFF", "A2:17:00AF2715"),
(8, "F88D36", "A2:14:F88D36FF", "A2:17:00AF2718"),
])
def test_expected_pages(spool_id, color, p14, p17):
cmds = encode_commands(pla_plus(spool_id, color)).split(",")
assert p14 in cmds and p17 in cmds
# only pages 14 and 17 differ from the template
diff = set(cmds) ^ set(TEMPLATE.split(","))
assert {c[:5] for c in diff} <= {"A2:14", "A2:17"}
def test_never_writes_pages_0_to_3():
pages = tagcodec.encode_pages(pla_plus(1, "000000"))
assert min(pages) == 4
with pytest.raises(tagcodec.TagError):
tagcodec.pages_to_commands({3: b"\0\0\0\0"})
def test_decode_roundtrip():
d = tagcodec.decode_pages(parse_commands(TEMPLATE))
assert d["url"] == "https://www.elegoo.com"
assert d["material"] == "PLA" and d["subtype_name"] == "PLA+"
assert (d["temp_min"], d["temp_max"]) == (190, 230)
assert d["diameter"] == 1.75 and d["weight"] == 10001 and d["spool_id"] == 1
assert d["manufacturer"] == "EEEEEEEE"
def test_rapid_petg_codes():
tag = TagData(spool_id=12, color="FFFFFF", material="PETG", subtype=0x0105, temp_min=240, temp_max=255)
pages = tagcodec.encode_pages(tag)
assert pages[0x12].hex().upper() == "80698471"
assert pages[0x13].hex().upper() == "01050000"
assert pages[0x15].hex().upper() == "00F000FF"
def test_validation():
with pytest.raises(tagcodec.TagError):
encode_commands(TagData(spool_id=1, color="000000", material="PLA", subtype=0x0100)) # PETG subtype
with pytest.raises(tagcodec.TagError):
encode_commands(pla_plus(60000, "000000")) # does not fit 2 bytes
with pytest.raises(tagcodec.TagError):
encode_commands(pla_plus(1, "zzzzzz"))
with pytest.raises(tagcodec.TagError):
encode_commands(TagData(spool_id=1, color="000000", temp_min=240, temp_max=200))
def test_custom_url_fits_before_canvas_pages():
tag = pla_plus(123, "000000")
tag.url = "http://10.10.6.10:7912/spool/show/123"
pages = tagcodec.encode_pages(tag)
assert max(p for p in pages if p < 0x10) <= 0x0F
assert tagcodec.decode_pages(pages)["url"] == tag.url
tag.url = "https://example.com/" + "x" * 40
with pytest.raises(tagcodec.TagError):
tagcodec.encode_pages(tag)
@pytest.mark.parametrize("weight,expected", [
("10001", 1), ("10005", 5), ("10000", 0), ("9999", None), ("1000", None), ("", None),
("abc", None), ("70000", None), ("10008.000", 8),
])
def test_spool_id_from_weight(weight, expected):
assert tagcodec.spool_id_from_weight(weight) == expected
def test_parse_record():
rec = {"channels0": {"weight": "10001", "type": "PLA"}, "channels1": {"weight": "", "type": ""},
"channels3": {"weight": "1000", "type": "PETG"}}
slots = tagcodec.parse_record(rec)
assert [s["spool_id"] for s in slots] == [1, None, None, None]
assert [s["occupied"] for s in slots] == [True, False, False, True]
@pytest.mark.parametrize("color,expected", [
("000000", "000000"), ("2850DF", "2850DF"), ("0000FF", "2850DF"), ("0047AB", "2850DF"),
("FF8000", "F88D36"), ("FF0000", "F72221"), ("FFFFFF", "FFFFFF"), ("808080", "898989"),
("1E90FF", "48A7FA"), ("800080", "433089"), (None, "000000"), ("#ffa500", "F88D36"),
])
def test_palette_suggestion(color, expected):
assert tagcodec.nearest_palette_color(color) == expected
@pytest.mark.parametrize("material,name,mat,sub", [
("PLA+", "", "PLA", 0x0001), ("PLA", "Silk Gold", "PLA", 0x0003), ("PETG", "", "PETG", 0x0100),
("PETG", "Rapid PETG", "PETG", 0x0105), ("PLA", "Matte Black", "PLA", 0x0006), ("PETG-CF", "", "PETG", 0x0101),
("ABS", "", "ABS", 0x0200), ("TPU", "95A", "TPU", 0x0301), ("PCTG", "", "PC", 0x0601),
("ASA", "", "ASA", 0x0800), ("", "", "PLA", 0x0000), ("PLA", "Rapid PLA+", "PLA", 0x000A),
])
def test_material_suggestion(material, name, mat, sub):
s = materials.suggest(material, name)
assert (s["material"], s["subtype"]) == (mat, sub)
def test_tables_complete():
assert len(materials.MATERIALS) == 15
assert len(materials.SUBTYPES) == 50
for code, name, _ in materials.SUBTYPES:
assert materials.material_of_subtype(code) is not None, name