- 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
137 lines
5.7 KiB
Python
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"]}
|