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

137 lines
5.7 KiB
Python

"""Elegoo Canvas material and subtype codes.
Taken verbatim from cjtallman/canvas-rfid-tool (index.html, MIT License,
Copyright (c) 2026 cjtallman, see THIRD_PARTY_NOTICES.md), which in turn credits
DnG-Crafts/ELG-RFID and Savion/elegoo-rfid-editor.
"""
from __future__ import annotations
import re
# name -> (4-byte material code, subtype family byte, suggested nozzle min/max)
MATERIALS: dict[str, dict] = {
"PLA": {"code": 0x00807665, "family": 0x00, "temps": (190, 230)},
"PETG": {"code": 0x80698471, "family": 0x01, "temps": (230, 260)},
"ABS": {"code": 0x00656683, "family": 0x02, "temps": (240, 270)},
"TPU": {"code": 0x00848085, "family": 0x03, "temps": (210, 240)},
"PA": {"code": 0x00008065, "family": 0x04, "temps": (260, 290)},
"CPE": {"code": 0x00678069, "family": 0x05, "temps": (240, 270)},
"PC": {"code": 0x00008067, "family": 0x06, "temps": (260, 290)},
"PVA": {"code": 0x00808665, "family": 0x07, "temps": (190, 220)},
"ASA": {"code": 0x00658365, "family": 0x08, "temps": (240, 270)},
"BVOH": {"code": 0x42564F48, "family": 0x09, "temps": (190, 220)},
"EVA": {"code": 0x00455641, "family": 0x0A, "temps": (190, 220)},
"HIPS": {"code": 0x48495053, "family": 0x0B, "temps": (230, 250)},
"PP": {"code": 0x00005050, "family": 0x0C, "temps": (220, 250)},
"PPA": {"code": 0x00505041, "family": 0x0D, "temps": (280, 310)},
"PPS": {"code": 0x00505053, "family": 0x0E, "temps": (300, 330)},
}
# (code, name, hidden on the CC2 display)
SUBTYPES: list[tuple[int, str, bool]] = [
(0x0000, "PLA", False), (0x0001, "PLA+", False), (0x0002, "PLA Pro", False),
(0x0003, "PLA Silk", False), (0x0004, "PLA-CF", False), (0x0005, "PLA Carbon", True),
(0x0006, "PLA Matte", False), (0x0007, "PLA Fluo", False), (0x0008, "PLA Wood", False),
(0x0009, "PLA Basic", False), (0x000A, "RAPID PLA+", False), (0x000B, "PLA Marble", False),
(0x000C, "PLA Galaxy", False), (0x000D, "PLA Red Copper", False), (0x000E, "PLA Sparkle", True),
(0x0100, "PETG", False), (0x0101, "PETG-CF", False), (0x0102, "PETG-GF", False),
(0x0103, "PETG Pro", True), (0x0104, "PETG Translucent", False), (0x0105, "RAPID PETG", False),
(0x0200, "ABS", False), (0x0201, "ABS-GF", True),
(0x0300, "TPU", False), (0x0301, "TPU 95A", False), (0x0302, "RAPID TPU 95A", False),
(0x0400, "PA", False), (0x0401, "PA-CF", True), (0x0403, "PAHT-CF", False),
(0x0404, "PA6", True), (0x0405, "PA6-CF", True), (0x0406, "PA12", True), (0x0407, "PA12-CF", True),
(0x0500, "CPE", False),
(0x0600, "PC", False), (0x0601, "PCTG", True), (0x0602, "PC-FR", False),
(0x0700, "PVA", False), (0x0800, "ASA", False), (0x0900, "BVOH", False), (0x0A00, "EVA", False),
(0x0B00, "HIPS", False),
(0x0C00, "PP", False), (0x0C01, "PP-CF", True), (0x0C02, "PP-GF", True),
(0x0D00, "PPA", False), (0x0D01, "PPA-CF", True), (0x0D02, "PPA-GF", True),
(0x0E00, "PPS", False), (0x0E02, "PPS-CF", True),
]
SUBTYPE_BY_CODE = {code: name for code, name, _ in SUBTYPES}
MATERIAL_BY_CODE = {m["code"]: name for name, m in MATERIALS.items()}
def material_of_subtype(code: int) -> str | None:
family = code >> 8
for name, m in MATERIALS.items():
if m["family"] == family:
return name
return None
def subtypes_for(material: str) -> list[tuple[int, str, bool]]:
family = MATERIALS[material]["family"]
return [s for s in SUBTYPES if s[0] >> 8 == family]
def _tokens(text: str) -> set[str]:
text = text.upper().replace("+", " PLUS ")
return set(re.findall(r"[A-Z0-9]+", text))
# Spoolman material spellings that do not name the Elegoo base material directly.
_ALIASES = {"PCTG": "PC", "PA6": "PA", "PA12": "PA", "PAHT": "PA", "NYLON": "PA", "PET": "PETG"}
_MODIFIERS = [
# (tokens that must all be present, subtype name suffix)
({"RAPID", "PLUS"}, "RAPID {m}+"),
({"RAPID"}, "RAPID {m}"),
({"95A"}, "{m} 95A"),
({"SILK"}, "{m} Silk"),
({"MATTE"}, "{m} Matte"),
({"MATT"}, "{m} Matte"),
({"CF"}, "{m}-CF"),
({"CARBON"}, "{m}-CF"),
({"GF"}, "{m}-GF"),
({"WOOD"}, "{m} Wood"),
({"MARBLE"}, "{m} Marble"),
({"GALAXY"}, "{m} Galaxy"),
({"SPARKLE"}, "{m} Sparkle"),
({"FLUO"}, "{m} Fluo"),
({"TRANSLUCENT"}, "{m} Translucent"),
({"TRANSPARENT"}, "{m} Translucent"),
({"PRO"}, "{m} Pro"),
({"BASIC"}, "{m} Basic"),
({"PLUS"}, "{m}+"),
]
def suggest(material: str | None, name: str | None = "") -> dict:
"""Suggest Elegoo material, subtype and nozzle temps from Spoolman text."""
material = material or ""
tokens = _tokens(f"{material} {name or ''}")
mat_tokens = _tokens(material)
base = None
# Prefer the Spoolman material field; fall back to the filament name.
for pool in (mat_tokens, tokens):
for tok in sorted(pool, key=len, reverse=True):
cand = _ALIASES.get(tok, tok)
if cand in MATERIALS:
base = cand
break
if tok.startswith("PLA"): # e.g. "PLAPLUS"
base = "PLA"
break
if base:
break
if base is None:
base = "PLA"
subtype = MATERIALS[base]["family"] << 8
names = {n.upper(): c for c, n, _ in subtypes_for(base)}
# Exact subtype names first (e.g. "PCTG", "PAHT-CF").
for tok in tokens:
if tok in names and tok != base:
subtype = names[tok]
break
else:
for needed, pattern in _MODIFIERS:
if needed <= tokens:
cand = pattern.format(m=base).upper()
if cand in names:
subtype = names[cand]
break
return {"material": base, "subtype": subtype, "temps": MATERIALS[base]["temps"]}