From 3ed7821adfb091f366ec27452652a38e1502d04e Mon Sep 17 00:00:00 2001 From: yanshay Date: Thu, 16 Apr 2026 17:06:26 +0300 Subject: [PATCH 1/9] Add support for SpoolEase tag format --- examples/u1.cfg | 2 + src/main.py | 5 +- src/tag/ndef_tag_processor.py | 15 ++- src/tag/spoolease/__init__.py | 1 + src/tag/spoolease/constants.py | 53 ++++++++++ src/tag/spoolease/processor.py | 175 +++++++++++++++++++++++++++++++++ 6 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 src/tag/spoolease/__init__.py create mode 100644 src/tag/spoolease/constants.py create mode 100644 src/tag/spoolease/processor.py diff --git a/examples/u1.cfg b/examples/u1.cfg index dcd819d..40f5fec 100644 --- a/examples/u1.cfg +++ b/examples/u1.cfg @@ -71,6 +71,8 @@ gpio_pins_low = upper_rfid_coils_enable_pin [openspool_tag_processor] +[spoolease_tag_processor] + #[snapmaker_tag_processor] #key = diff --git a/src/main.py b/src/main.py index 3553733..1124c7f 100644 --- a/src/main.py +++ b/src/main.py @@ -24,6 +24,7 @@ from tag.openspool import OpenspoolTagProcessor from tag.qidi.processor import QidiTagProcessor from tag.snapmaker import SnapmakerTagProcessor +from tag.spoolease import SpooleaseTagProcessor from tag.tigertag import TigerTagProcessor from controllers.moonraker_on_property_change import MoonrakerOnPropertyChangeController @@ -58,6 +59,8 @@ def create_configurable_entity(key: str, config: dict) -> ConfigurableEntity: return CrealityTagProcessor(config) case "openspool_tag_processor": return OpenspoolTagProcessor(config) + case "spoolease_tag_processor": + return SpooleaseTagProcessor(config) case "snapmaker_tag_processor": return SnapmakerTagProcessor(config) case "tigertag_tag_processor": @@ -112,4 +115,4 @@ def main(): run.loop() if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/tag/ndef_tag_processor.py b/src/tag/ndef_tag_processor.py index 6604a2f..b77cd0c 100644 --- a/src/tag/ndef_tag_processor.py +++ b/src/tag/ndef_tag_processor.py @@ -15,7 +15,9 @@ NDEF_NOT_FOUND_ERR = -3 class NdefRecord: - def __init__(self, mime_type: str, payload: bytes): + def __init__(self, payload: bytes, tnf: int, record_type: str = "", mime_type: str = ""): + self.tnf = tnf + self.type = record_type self.mime_type = mime_type self.payload = payload @@ -133,7 +135,7 @@ def __ndef_parse(self, data_buf : bytes|list) -> tuple[int, list[NdefRecord]]: if ndef_offset + type_len + id_len + payload_len > len(ndef_data): break - mime_type = ndef_data[ndef_offset:ndef_offset + type_len].decode('ascii', errors='ignore') + record_type = ndef_data[ndef_offset:ndef_offset + type_len].decode('ascii', errors='ignore') ndef_offset += type_len if id_len > 0: @@ -143,8 +145,11 @@ def __ndef_parse(self, data_buf : bytes|list) -> tuple[int, list[NdefRecord]]: ndef_offset += payload_len if tnf == 0x02: - records.append(NdefRecord(mime_type, payload)) - self.logger.debug(f"NDEF record found: mime_type='{mime_type}', payload_len={len(payload)}") + records.append(NdefRecord(payload, tnf, record_type=record_type, mime_type=record_type)) + self.logger.debug(f"NDEF MIME record found: mime_type='{record_type}', payload_len={len(payload)}") + elif tnf == 0x01 and record_type == 'U': + records.append(NdefRecord(payload, tnf, record_type=record_type)) + self.logger.debug(f"NDEF URI record found: payload_len={len(payload)}") else: data_io.seek(tlv_len, 1) @@ -155,4 +160,4 @@ def __ndef_parse(self, data_buf : bytes|list) -> tuple[int, list[NdefRecord]]: except Exception as e: self.logger.exception("NDEF parsing failed: %s", str(e)) - return NDEF_ERR, [] \ No newline at end of file + return NDEF_ERR, [] diff --git a/src/tag/spoolease/__init__.py b/src/tag/spoolease/__init__.py new file mode 100644 index 0000000..091c5b0 --- /dev/null +++ b/src/tag/spoolease/__init__.py @@ -0,0 +1 @@ +from .processor import SpooleaseTagProcessor diff --git a/src/tag/spoolease/constants.py b/src/tag/spoolease/constants.py new file mode 100644 index 0000000..be9b54f --- /dev/null +++ b/src/tag/spoolease/constants.py @@ -0,0 +1,53 @@ +class FilamentTypeExtendedData: + def __init__(self, bed_temp_c: float, drying_temp_c: float, drying_time_hours: float): + self.bed_temp_c = bed_temp_c + self.drying_temp_c = drying_temp_c + self.drying_time_hours = drying_time_hours + + +FILAMENT_TYPE_ALIASES = { + "ABS-S": "ABS", + "PA-S": "PA", + "PLA-S": "PLA", + "TPU-AMS": "TPU", +} + + +FILAMENT_TYPE_TO_EXTENDED_DATA = { + "ABS": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "ABS-GF": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "ABS-S": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "ASA": FilamentTypeExtendedData(90.0, 80.0, 8.0), + "ASA-AERO": FilamentTypeExtendedData(90.0, 80.0, 8.0), + "ASA-CF": FilamentTypeExtendedData(90.0, 80.0, 8.0), + "BVOH": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "EVA": FilamentTypeExtendedData(50.0, 50.0, 8.0), + "HIPS": FilamentTypeExtendedData(100.0, 70.0, 8.0), + "PA": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "PA-CF": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "PA-GF": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "PA-S": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "PA6-CF": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "PC": FilamentTypeExtendedData(110.0, 80.0, 8.0), + "PCTG": FilamentTypeExtendedData(70.0, 65.0, 8.0), + "PE": FilamentTypeExtendedData(60.0, 60.0, 8.0), + "PE-CF": FilamentTypeExtendedData(60.0, 60.0, 8.0), + "PET-CF": FilamentTypeExtendedData(70.0, 65.0, 8.0), + "PETG": FilamentTypeExtendedData(70.0, 65.0, 8.0), + "PETG-CF": FilamentTypeExtendedData(70.0, 65.0, 8.0), + "PHA": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "PLA": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "PLA-AERO": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "PLA-CF": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "PLA-S": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "PP": FilamentTypeExtendedData(80.0, 80.0, 8.0), + "PP-CF": FilamentTypeExtendedData(80.0, 80.0, 8.0), + "PP-GF": FilamentTypeExtendedData(80.0, 80.0, 8.0), + "PPA-CF": FilamentTypeExtendedData(100.0, 100.0, 8.0), + "PPA-GF": FilamentTypeExtendedData(100.0, 100.0, 8.0), + "PPS": FilamentTypeExtendedData(110.0, 100.0, 8.0), + "PPS-CF": FilamentTypeExtendedData(110.0, 100.0, 8.0), + "PVA": FilamentTypeExtendedData(60.0, 50.0, 8.0), + "TPU": FilamentTypeExtendedData(50.0, 70.0, 8.0), + "TPU-AMS": FilamentTypeExtendedData(50.0, 70.0, 8.0), +} diff --git a/src/tag/spoolease/processor.py b/src/tag/spoolease/processor.py new file mode 100644 index 0000000..d7adc63 --- /dev/null +++ b/src/tag/spoolease/processor.py @@ -0,0 +1,175 @@ +from filament import GenericFilament +from reader.scan_result import ScanResult +from tag.ndef_tag_processor import NdefRecord, NdefTagProcessor +import urllib.parse + +from . import constants as Constants + + +NDEF_URI_PREFIX_MAP = { + 0x00: "", + 0x01: "http://www.", + 0x02: "https://www.", + 0x03: "http://", + 0x04: "https://", + 0x05: "tel:", + 0x06: "mailto:", + 0x07: "ftp://anonymous:anonymous@", + 0x08: "ftp://ftp.", + 0x09: "ftps://", + 0x0A: "sftp://", + 0x0B: "smb://", + 0x0C: "nfs://", + 0x0D: "ftp://", + 0x0E: "dav://", + 0x0F: "news:", + 0x10: "telnet://", + 0x11: "imap:", + 0x12: "rtsp://", + 0x13: "urn:", + 0x14: "pop:", + 0x15: "sip:", + 0x16: "sips:", + 0x17: "tftp:", + 0x18: "btspp://", + 0x19: "btl2cap://", + 0x1A: "btgoep://", + 0x1B: "tcpobex://", + 0x1C: "irdaobex://", + 0x1D: "file://", + 0x1E: "urn:epc:id:", + 0x1F: "urn:epc:tag:", + 0x20: "urn:epc:pat:", + 0x21: "urn:epc:raw:", + 0x22: "urn:epc:", + 0x23: "urn:nfc:", +} + + +class SpooleaseTagProcessor(NdefTagProcessor): + def __init__(self, config: dict): + super().__init__(config) + + def process_ndef(self, scan_result: ScanResult, ndef_records: list[NdefRecord]) -> GenericFilament | None: + for record in ndef_records: + if record.tnf == 0x01 and record.type == "U": + filament = self.__parse_spoolease_payload(record.payload) + if filament is not None: + return filament + + self.logger.error("SpoolEase processing failed: No valid SpoolEase NDEF URI record found") + return None + + def __parse_spoolease_payload(self, payload: bytes) -> GenericFilament | None: + if payload is None or not isinstance(payload, (bytes, bytearray)): + self.logger.error("SpoolEase payload parsing failed: Invalid payload parameter") + return None + + try: + url = self.__decode_uri_payload(payload) + parsed = urllib.parse.urlparse(url) + + if parsed.netloc.lower() != "tag.spoolease.io" or not parsed.path.startswith("/S1"): + self.logger.error("SpoolEase payload parsing failed: Unsupported URL '%s'", url) + return None + + query = urllib.parse.parse_qs(parsed.query, keep_blank_values=False) + + raw_material = (query.get("M", ["PLA"])[0] or "PLA").upper() + material = Constants.FILAMENT_TYPE_ALIASES.get(raw_material, raw_material) + subtype = query.get("MS", [""])[0] or "" + brand = query.get("B", ["Generic"])[0] or "Generic" + color_field = query.get("CC", ["FFFFFFFF"])[0] or "FFFFFFFF" + + hotend_min_temp_c = self.__parse_required_int(query, "NN") + hotend_max_temp_c = self.__parse_required_int(query, "NX") + weight_grams = self.__parse_optional_int(query, "WL", 1000) + + if hotend_max_temp_c < hotend_min_temp_c: + self.logger.error("SpoolEase payload parsing failed: Invalid temperature values") + return None + + colors = self.__parse_colors(color_field) + extra_data = Constants.FILAMENT_TYPE_TO_EXTENDED_DATA.get(material) + + bed_temp_c = extra_data.bed_temp_c if extra_data is not None else 0.0 + drying_temp_c = extra_data.drying_temp_c if extra_data is not None else 0.0 + drying_time_hours = extra_data.drying_time_hours if extra_data is not None else 0.0 + + return GenericFilament( + source_processor=self.name, + unique_id=GenericFilament.generate_unique_id("SpoolEase", brand, material, subtype, *colors, weight_grams), + manufacturer=brand, + type=material, + modifiers=[subtype] if subtype else [], + colors=colors, + diameter_mm=1.75, + weight_grams=weight_grams, + hotend_min_temp_c=hotend_min_temp_c, + hotend_max_temp_c=hotend_max_temp_c, + bed_temp_c=bed_temp_c, + drying_temp_c=drying_temp_c, + drying_time_hours=drying_time_hours, + manufacturing_date="0001-01-01", + ) + except ValueError as e: + self.logger.error("SpoolEase payload parsing failed: %s", e) + return None + except Exception as e: + self.logger.exception("SpoolEase payload parsing failed: %s", e) + return None + + def __decode_uri_payload(self, payload: bytes) -> str: + if len(payload) < 1: + raise ValueError("Invalid NDEF URI payload") + + prefix = NDEF_URI_PREFIX_MAP.get(payload[0], "") + suffix = payload[1:].decode("utf-8", errors="ignore") + return (prefix + suffix).strip() + + def __parse_required_int(self, query: dict[str, list[str]], key: str) -> int: + values = query.get(key) + if not values or values[0] == "": + raise ValueError(f"Missing required field '{key}'") + return int(values[0]) + + def __parse_optional_int(self, query: dict[str, list[str]], key: str, default: int) -> int: + values = query.get(key) + if not values or values[0] == "": + return default + return int(values[0]) + + def __parse_colors(self, color_field: str) -> list[int]: + colors = [] + alpha = 0xFF + + for i, color_code in enumerate(color_field.split(";")): + if len(colors) >= 5: + break + + color_code = color_code.strip() + if not color_code: + continue + + rgb, parsed_alpha = self.__parse_rgba_hex(color_code) + colors.append((parsed_alpha << 24) | rgb) + + if i == 0: + alpha = parsed_alpha + + if not colors: + colors = [(alpha << 24) | 0xFFFFFF] + + return colors + + def __parse_rgba_hex(self, value: str) -> tuple[int, int]: + hex_str = str(value).strip() + if hex_str.startswith("#"): + hex_str = hex_str[1:] + + if len(hex_str) == 6: + return int(hex_str, 16), 0xFF + if len(hex_str) == 8: + return int(hex_str[:6], 16), int(hex_str[6:], 16) + + raise ValueError(f"Unsupported RGBA hex length: {len(hex_str)}") From ae5f26000cb41b699ef9c43cd3d1c2c445c255b9 Mon Sep 17 00:00:00 2001 From: yanshay Date: Thu, 16 Apr 2026 17:42:31 +0300 Subject: [PATCH 2/9] Add support for testing an tag based on NfcTools tag Data export --- src/tag/spoolease/test_dump_processor.py | 68 ++++++++++++ src/tag/spoolease/test_tag_dump.txt | 135 +++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 src/tag/spoolease/test_dump_processor.py create mode 100644 src/tag/spoolease/test_tag_dump.txt diff --git a/src/tag/spoolease/test_dump_processor.py b/src/tag/spoolease/test_dump_processor.py new file mode 100644 index 0000000..01aab9e --- /dev/null +++ b/src/tag/spoolease/test_dump_processor.py @@ -0,0 +1,68 @@ +"""Standalone SpoolEase dump test. + +Usage: + + from OpenRFID root folder: + python3 src/tag/spoolease/test_dump_processor.py + +How it works: + - Reads the NFC Tools-style page dump from `test_tag_dump.txt` + - Converts the dumped hex bytes into the raw tag byte buffer expected by OpenRFID + - Passes the raw bytes to `SpooleaseTagProcessor.process_tag(...)` + - Prints the parsed filament structure as JSON + +To test another tag: + - Replace the contents of `test_tag_dump.txt` with another NFC Tools dump + - Re-run the command above +""" + +import json +import logging +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from reader.scan_result import ScanResult +from tag.spoolease import SpooleaseTagProcessor +from tag.tag_types import TagType + + +PAGE_PATTERN = re.compile(r"\[\s*([0-9A-Fa-f:]+)\s*\]") + + +def parse_nfc_tools_dump(dump_text: str) -> bytes: + raw_bytes = bytearray() + + for line in dump_text.splitlines(): + match = PAGE_PATTERN.search(line) + if not match: + continue + + for byte_text in match.group(1).split(":"): + raw_bytes.append(int(byte_text, 16)) + + return bytes(raw_bytes) + + +def main() -> None: + logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") + + fixture_path = Path(__file__).with_name("test_tag_dump.txt") + raw_tag_data = parse_nfc_tools_dump(fixture_path.read_text()) + + processor = SpooleaseTagProcessor({"__name": "spoolease_test"}) + scan_result = ScanResult(TagType.MifareUltralight, b"\x04\x7B\x1F\x31\xC9\x2A\x81", b"", b"", b"\x00") + + filament = processor.process_tag(scan_result, raw_tag_data) + + if filament is None: + print("No filament data was parsed.") + return + + print(json.dumps(filament.to_dict(), indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/src/tag/spoolease/test_tag_dump.txt b/src/tag/spoolease/test_tag_dump.txt new file mode 100644 index 0000000..ea37f19 --- /dev/null +++ b/src/tag/spoolease/test_tag_dump.txt @@ -0,0 +1,135 @@ +[ 04:7B:1F:E8 ] Addr. 00 : UID0 - UID2 / BCC0 +[ 31:C9:2A:81 ] Addr. 01 : UID3 - UID6 +[ 53:48:00:00 ] Addr. 02 : BCC1 / INT. / LOCK0 - LOCK1 +[ E1:10:3E:00 ] Addr. 03 : OTP0 - OTP3 +[ 03:BB:D1:01 ] Addr. 04 : DATA +[ B7:55:04:74 ] Addr. 05 : DATA +[ 61:67:2E:73 ] Addr. 06 : DATA +[ 70:6F:6F:6C ] Addr. 07 : DATA +[ 65:61:73:65 ] Addr. 08 : DATA +[ 2E:69:6F:2F ] Addr. 09 : DATA +[ 53:31:2F:3F ] Addr. 0A : DATA +[ 54:47:3D:30 ] Addr. 0B : DATA +[ 34:37:42:31 ] Addr. 0C : DATA +[ 46:33:31:43 ] Addr. 0D : DATA +[ 39:32:41:38 ] Addr. 0E : DATA +[ 31:26:49:44 ] Addr. 0F : DATA +[ 3D:35:33:26 ] Addr. 10 : DATA +[ 44:45:3D:31 ] Addr. 11 : DATA +[ 37:37:36:33 ] Addr. 12 : DATA +[ 35:30:33:36 ] Addr. 13 : DATA +[ 34:26:44:41 ] Addr. 14 : DATA +[ 3D:31:37:37 ] Addr. 15 : DATA +[ 36:31:32:37 ] Addr. 16 : DATA +[ 30:39:32:26 ] Addr. 17 : DATA +[ 4D:3D:50:4C ] Addr. 18 : DATA +[ 41:26:4D:53 ] Addr. 19 : DATA +[ 3D:50:6C:75 ] Addr. 1A : DATA +[ 73:26:43:43 ] Addr. 1B : DATA +[ 3D:42:38:37 ] Addr. 1C : DATA +[ 33:33:33:46 ] Addr. 1D : DATA +[ 46:3B:35:30 ] Addr. 1E : DATA +[ 43:38:37:38 ] Addr. 1F : DATA +[ 46:46:3B:44 ] Addr. 20 : DATA +[ 34:41:46:33 ] Addr. 21 : DATA +[ 37:46:46:26 ] Addr. 22 : DATA +[ 43:4E:3D:52 ] Addr. 23 : DATA +[ 61:69:6E:62 ] Addr. 24 : DATA +[ 6F:77:26:42 ] Addr. 25 : DATA +[ 3D:65:53:55 ] Addr. 26 : DATA +[ 4E:26:57:4C ] Addr. 27 : DATA +[ 3D:31:30:30 ] Addr. 28 : DATA +[ 30:26:53:43 ] Addr. 29 : DATA +[ 3D:47:46:4C ] Addr. 2A : DATA +[ 30:33:26:53 ] Addr. 2B : DATA +[ 4E:3D:65:53 ] Addr. 2C : DATA +[ 55:4E:25:32 ] Addr. 2D : DATA +[ 30:50:4C:41 ] Addr. 2E : DATA +[ 25:32:42:26 ] Addr. 2F : DATA +[ 4E:4E:3D:31 ] Addr. 30 : DATA +[ 39:30:26:4E ] Addr. 31 : DATA +[ 58:3D:32:34 ] Addr. 32 : DATA +[ 30:FE:00:00 ] Addr. 33 : DATA +[ 30:26:4E:58 ] Addr. 34 : DATA +[ 3D:32:34:30 ] Addr. 35 : DATA +[ FE:00:00:00 ] Addr. 36 : DATA +[ 00:00:00:00 ] Addr. 37 : DATA +[ 00:00:00:00 ] Addr. 38 : DATA +[ 00:00:00:00 ] Addr. 39 : DATA +[ 00:00:00:00 ] Addr. 3A : DATA +[ 00:00:00:00 ] Addr. 3B : DATA +[ 00:00:00:00 ] Addr. 3C : DATA +[ 00:00:00:00 ] Addr. 3D : DATA +[ 00:00:00:00 ] Addr. 3E : DATA +[ 00:00:00:00 ] Addr. 3F : DATA +[ 00:00:00:00 ] Addr. 40 : DATA +[ 00:00:00:00 ] Addr. 41 : DATA +[ 00:00:00:00 ] Addr. 42 : DATA +[ 00:00:00:00 ] Addr. 43 : DATA +[ 00:00:00:00 ] Addr. 44 : DATA +[ 00:00:00:00 ] Addr. 45 : DATA +[ 00:00:00:00 ] Addr. 46 : DATA +[ 00:00:00:00 ] Addr. 47 : DATA +[ 00:00:00:00 ] Addr. 48 : DATA +[ 00:00:00:00 ] Addr. 49 : DATA +[ 00:00:00:00 ] Addr. 4A : DATA +[ 00:00:00:00 ] Addr. 4B : DATA +[ 00:00:00:00 ] Addr. 4C : DATA +[ 00:00:00:00 ] Addr. 4D : DATA +[ 00:00:00:00 ] Addr. 4E : DATA +[ 00:00:00:00 ] Addr. 4F : DATA +[ 00:00:00:00 ] Addr. 50 : DATA +[ 00:00:00:00 ] Addr. 51 : DATA +[ 00:00:00:00 ] Addr. 52 : DATA +[ 00:00:00:00 ] Addr. 53 : DATA +[ 00:00:00:00 ] Addr. 54 : DATA +[ 00:00:00:00 ] Addr. 55 : DATA +[ 00:00:00:00 ] Addr. 56 : DATA +[ 00:00:00:00 ] Addr. 57 : DATA +[ 00:00:00:00 ] Addr. 58 : DATA +[ 00:00:00:00 ] Addr. 59 : DATA +[ 00:00:00:00 ] Addr. 5A : DATA +[ 00:00:00:00 ] Addr. 5B : DATA +[ 00:00:00:00 ] Addr. 5C : DATA +[ 00:00:00:00 ] Addr. 5D : DATA +[ 00:00:00:00 ] Addr. 5E : DATA +[ 00:00:00:00 ] Addr. 5F : DATA +[ 00:00:00:00 ] Addr. 60 : DATA +[ 00:00:00:00 ] Addr. 61 : DATA +[ 00:00:00:00 ] Addr. 62 : DATA +[ 00:00:00:00 ] Addr. 63 : DATA +[ 00:00:00:00 ] Addr. 64 : DATA +[ 00:00:00:00 ] Addr. 65 : DATA +[ 00:00:00:00 ] Addr. 66 : DATA +[ 00:00:00:00 ] Addr. 67 : DATA +[ 00:00:00:00 ] Addr. 68 : DATA +[ 00:00:00:00 ] Addr. 69 : DATA +[ 00:00:00:00 ] Addr. 6A : DATA +[ 00:00:00:00 ] Addr. 6B : DATA +[ 00:00:00:00 ] Addr. 6C : DATA +[ 00:00:00:00 ] Addr. 6D : DATA +[ 00:00:00:00 ] Addr. 6E : DATA +[ 00:00:00:00 ] Addr. 6F : DATA +[ 00:00:00:00 ] Addr. 70 : DATA +[ 00:00:00:00 ] Addr. 71 : DATA +[ 00:00:00:00 ] Addr. 72 : DATA +[ 00:00:00:00 ] Addr. 73 : DATA +[ 00:00:00:00 ] Addr. 74 : DATA +[ 00:00:00:00 ] Addr. 75 : DATA +[ 00:00:00:00 ] Addr. 76 : DATA +[ 00:00:00:00 ] Addr. 77 : DATA +[ 00:00:00:00 ] Addr. 78 : DATA +[ 00:00:00:00 ] Addr. 79 : DATA +[ 00:00:00:00 ] Addr. 7A : DATA +[ 00:00:00:00 ] Addr. 7B : DATA +[ 00:00:00:00 ] Addr. 7C : DATA +[ 00:00:00:00 ] Addr. 7D : DATA +[ 00:00:00:00 ] Addr. 7E : DATA +[ 00:00:00:00 ] Addr. 7F : DATA +[ 00:00:00:00 ] Addr. 80 : DATA +[ 00:00:00:00 ] Addr. 81 : DATA +[ 00:00:00:BD ] Addr. 82 : LOCK2 - LOCK4 +[ 04:00:00:FF ] Addr. 83 : CFG 0 (MIRROR / AUTH0) +[ 00:05:00:00 ] Addr. 84 : CFG 1 (ACCESS) +[ 00:00:00:00 ] Addr. 85 : PWD0 - PWD3 +[ 00:00:00:00 ] Addr. 86 : PACK0 - PACK1 From 64daf37cc3ab36f8a0bb3f23cb27770e0ef0f183 Mon Sep 17 00:00:00 2001 From: yanshay Date: Sun, 19 Apr 2026 20:25:26 +0300 Subject: [PATCH 3/9] Remove log + move relevant code to general ndef processing --- src/tag/ndef_tag_processor.py | 39 ++++++++++++++++++++++++++++++ src/tag/spoolease/processor.py | 43 +--------------------------------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/src/tag/ndef_tag_processor.py b/src/tag/ndef_tag_processor.py index b77cd0c..51365ee 100644 --- a/src/tag/ndef_tag_processor.py +++ b/src/tag/ndef_tag_processor.py @@ -14,6 +14,45 @@ NDEF_PARAMETER_ERR = -2 NDEF_NOT_FOUND_ERR = -3 +NDEF_URI_PREFIX_MAP = { + 0x00: "", + 0x01: "http://www.", + 0x02: "https://www.", + 0x03: "http://", + 0x04: "https://", + 0x05: "tel:", + 0x06: "mailto:", + 0x07: "ftp://anonymous:anonymous@", + 0x08: "ftp://ftp.", + 0x09: "ftps://", + 0x0A: "sftp://", + 0x0B: "smb://", + 0x0C: "nfs://", + 0x0D: "ftp://", + 0x0E: "dav://", + 0x0F: "news:", + 0x10: "telnet://", + 0x11: "imap:", + 0x12: "rtsp://", + 0x13: "urn:", + 0x14: "pop:", + 0x15: "sip:", + 0x16: "sips:", + 0x17: "tftp:", + 0x18: "btspp://", + 0x19: "btl2cap://", + 0x1A: "btgoep://", + 0x1B: "tcpobex://", + 0x1C: "irdaobex://", + 0x1D: "file://", + 0x1E: "urn:epc:id:", + 0x1F: "urn:epc:tag:", + 0x20: "urn:epc:pat:", + 0x21: "urn:epc:raw:", + 0x22: "urn:epc:", + 0x23: "urn:nfc:", +} + class NdefRecord: def __init__(self, payload: bytes, tnf: int, record_type: str = "", mime_type: str = ""): self.tnf = tnf diff --git a/src/tag/spoolease/processor.py b/src/tag/spoolease/processor.py index d7adc63..dd03c80 100644 --- a/src/tag/spoolease/processor.py +++ b/src/tag/spoolease/processor.py @@ -1,51 +1,11 @@ from filament import GenericFilament from reader.scan_result import ScanResult -from tag.ndef_tag_processor import NdefRecord, NdefTagProcessor +from tag.ndef_tag_processor import NDEF_URI_PREFIX_MAP, NdefRecord, NdefTagProcessor import urllib.parse from . import constants as Constants -NDEF_URI_PREFIX_MAP = { - 0x00: "", - 0x01: "http://www.", - 0x02: "https://www.", - 0x03: "http://", - 0x04: "https://", - 0x05: "tel:", - 0x06: "mailto:", - 0x07: "ftp://anonymous:anonymous@", - 0x08: "ftp://ftp.", - 0x09: "ftps://", - 0x0A: "sftp://", - 0x0B: "smb://", - 0x0C: "nfs://", - 0x0D: "ftp://", - 0x0E: "dav://", - 0x0F: "news:", - 0x10: "telnet://", - 0x11: "imap:", - 0x12: "rtsp://", - 0x13: "urn:", - 0x14: "pop:", - 0x15: "sip:", - 0x16: "sips:", - 0x17: "tftp:", - 0x18: "btspp://", - 0x19: "btl2cap://", - 0x1A: "btgoep://", - 0x1B: "tcpobex://", - 0x1C: "irdaobex://", - 0x1D: "file://", - 0x1E: "urn:epc:id:", - 0x1F: "urn:epc:tag:", - 0x20: "urn:epc:pat:", - 0x21: "urn:epc:raw:", - 0x22: "urn:epc:", - 0x23: "urn:nfc:", -} - - class SpooleaseTagProcessor(NdefTagProcessor): def __init__(self, config: dict): super().__init__(config) @@ -57,7 +17,6 @@ def process_ndef(self, scan_result: ScanResult, ndef_records: list[NdefRecord]) if filament is not None: return filament - self.logger.error("SpoolEase processing failed: No valid SpoolEase NDEF URI record found") return None def __parse_spoolease_payload(self, payload: bytes) -> GenericFilament | None: From d6d61043c8ac3f511b07cf4b3e4a4afa0a789cdd Mon Sep 17 00:00:00 2001 From: yanshay Date: Sun, 19 Apr 2026 21:13:43 +0300 Subject: [PATCH 4/9] Add tests --- .../eSun PLA Plus Multicolor 1kg.bin | Bin 0 -> 540 bytes .../eSun PLA Plus Multicolor 1kg.yml | 20 ++++++++++++++++++ test/test_tags.py | 15 +++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.bin create mode 100644 test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.yml diff --git a/test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.bin b/test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.bin new file mode 100644 index 0000000000000000000000000000000000000000..49f33c2ed6fe209dbac0f5b20e35fb623adcab28 GIT binary patch literal 540 zcmZRnmVaS*QmZl8gMs0pfE@$#?u(4uLs?1^)AfoA^7C_26N^*zGV}F=4fX9q+-(g^ z%$*F~j18SFjT|iu)jVBnO^wxDTx|`_&CQHW4UEl9)Lb0FJVPUM14|<{U)um5M>XGI z+kl+XVl`)HTPF*1V`F1CH)~S^XA5%+Aj8GP(aqT0%}veO&o(GAGcPH>T+PWgH8|8y zE!@Y}(7?bzE!f%C-Ob0qSS{EOETL*-05n$B$Vtu5&(_e=K+P}0*2u)*9|HptlVKE) Q6asr$7#RLDup;yT0EgN TagProcessor: return processor +def _build_spoolease_processor() -> TagProcessor: + processor = SpooleaseTagProcessor( + { + "__name": "SpooleaseTagProcessor", + } + ) + processor.enabled = True + return processor + + def _build_snapmaker_processor() -> TagProcessor: processor = SnapmakerTagProcessor( { @@ -155,6 +166,10 @@ def _build_tigertag_processor() -> TagProcessor: "build_processor": _build_qidi_processor, "tag_type": TagType.MifareClassic1k, }, + "SpoolEase": { + "build_processor": _build_spoolease_processor, + "tag_type": TagType.MifareUltralight, + }, "Snapmaker": { "build_processor": _build_snapmaker_processor, "tag_type": TagType.MifareClassic1k, From d488069f0ea9406ec040d38cac0e242356fce820 Mon Sep 17 00:00:00 2001 From: yanshay Date: Sun, 19 Apr 2026 21:33:00 +0300 Subject: [PATCH 5/9] Update filaments dats --- src/tag/spoolease/constants.py | 72 ++++++++++--------- .../eSun PLA Plus Multicolor 1kg.yml | 2 +- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/tag/spoolease/constants.py b/src/tag/spoolease/constants.py index be9b54f..6be4c0d 100644 --- a/src/tag/spoolease/constants.py +++ b/src/tag/spoolease/constants.py @@ -1,5 +1,7 @@ class FilamentTypeExtendedData: - def __init__(self, bed_temp_c: float, drying_temp_c: float, drying_time_hours: float): + def __init__( + self, bed_temp_c: float, drying_temp_c: float, drying_time_hours: float + ): self.bed_temp_c = bed_temp_c self.drying_temp_c = drying_temp_c self.drying_time_hours = drying_time_hours @@ -14,40 +16,40 @@ def __init__(self, bed_temp_c: float, drying_temp_c: float, drying_time_hours: f FILAMENT_TYPE_TO_EXTENDED_DATA = { - "ABS": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "ABS-GF": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "ABS-S": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "ASA": FilamentTypeExtendedData(90.0, 80.0, 8.0), - "ASA-AERO": FilamentTypeExtendedData(90.0, 80.0, 8.0), - "ASA-CF": FilamentTypeExtendedData(90.0, 80.0, 8.0), - "BVOH": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "EVA": FilamentTypeExtendedData(50.0, 50.0, 8.0), - "HIPS": FilamentTypeExtendedData(100.0, 70.0, 8.0), - "PA": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "PA-CF": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "PA-GF": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "PA-S": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "PA6-CF": FilamentTypeExtendedData(100.0, 80.0, 8.0), - "PC": FilamentTypeExtendedData(110.0, 80.0, 8.0), - "PCTG": FilamentTypeExtendedData(70.0, 65.0, 8.0), - "PE": FilamentTypeExtendedData(60.0, 60.0, 8.0), - "PE-CF": FilamentTypeExtendedData(60.0, 60.0, 8.0), - "PET-CF": FilamentTypeExtendedData(70.0, 65.0, 8.0), + "ABS": FilamentTypeExtendedData(95.0, 80.0, 8.0), + "ABS-GF": FilamentTypeExtendedData(95.0, 80.0, 8.0), + "ABS-S": FilamentTypeExtendedData(95.0, 80.0, 8.0), + "ASA": FilamentTypeExtendedData(95.0, 80.0, 8.0), + "ASA-AERO": FilamentTypeExtendedData(85.0, 80.0, 8.0), + "ASA-CF": FilamentTypeExtendedData(95.0, 80.0, 8.0), + "BVOH": FilamentTypeExtendedData(60.0, 60.0, 6.0), + "EVA": FilamentTypeExtendedData(55.0, 50.0, 6.0), + "HIPS": FilamentTypeExtendedData(100.0, 70.0, 6.0), + "PA": FilamentTypeExtendedData(110.0, 80.0, 10.0), + "PA-CF": FilamentTypeExtendedData(110.0, 80.0, 10.0), + "PA-GF": FilamentTypeExtendedData(110.0, 80.0, 10.0), + "PA-S": FilamentTypeExtendedData(110.0, 80.0, 10.0), + "PA6-CF": FilamentTypeExtendedData(110.0, 80.0, 10.0), + "PC": FilamentTypeExtendedData(100.0, 80.0, 8.0), + "PCTG": FilamentTypeExtendedData(80.0, 65.0, 6.0), + "PE": FilamentTypeExtendedData(60.0, 60.0, 6.0), + "PE-CF": FilamentTypeExtendedData(60.0, 60.0, 6.0), + "PET-CF": FilamentTypeExtendedData(85.0, 80.0, 10.0), "PETG": FilamentTypeExtendedData(70.0, 65.0, 8.0), "PETG-CF": FilamentTypeExtendedData(70.0, 65.0, 8.0), - "PHA": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "PLA": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "PLA-AERO": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "PLA-CF": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "PLA-S": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "PP": FilamentTypeExtendedData(80.0, 80.0, 8.0), - "PP-CF": FilamentTypeExtendedData(80.0, 80.0, 8.0), - "PP-GF": FilamentTypeExtendedData(80.0, 80.0, 8.0), - "PPA-CF": FilamentTypeExtendedData(100.0, 100.0, 8.0), - "PPA-GF": FilamentTypeExtendedData(100.0, 100.0, 8.0), - "PPS": FilamentTypeExtendedData(110.0, 100.0, 8.0), - "PPS-CF": FilamentTypeExtendedData(110.0, 100.0, 8.0), - "PVA": FilamentTypeExtendedData(60.0, 50.0, 8.0), - "TPU": FilamentTypeExtendedData(50.0, 70.0, 8.0), - "TPU-AMS": FilamentTypeExtendedData(50.0, 70.0, 8.0), + "PHA": FilamentTypeExtendedData(60.0, 50.0, 6.0), + "PLA": FilamentTypeExtendedData(55.0, 50.0, 8.0), + "PLA-AERO": FilamentTypeExtendedData(45.0, 50.0, 8.0), + "PLA-CF": FilamentTypeExtendedData(60.0, 55.0, 8.0), + "PLA-S": FilamentTypeExtendedData(45.0, 50.0, 8.0), + "PP": FilamentTypeExtendedData(90.0, 70.0, 12.0), + "PP-CF": FilamentTypeExtendedData(90.0, 70.0, 12.0), + "PP-GF": FilamentTypeExtendedData(90.0, 70.0, 12.0), + "PPA-CF": FilamentTypeExtendedData(110.0, 120.0, 10.0), + "PPA-GF": FilamentTypeExtendedData(110.0, 120.0, 10.0), + "PPS": FilamentTypeExtendedData(110.0, 120.0, 10.0), + "PPS-CF": FilamentTypeExtendedData(110.0, 120.0, 10.0), + "PVA": FilamentTypeExtendedData(45.0, 60.0, 8.0), + "TPU": FilamentTypeExtendedData(38.0, 70.0, 8.0), + "TPU-AMS": FilamentTypeExtendedData(33.0, 70.0, 8.0), } diff --git a/test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.yml b/test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.yml index 8ec6cec..ecbc499 100644 --- a/test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.yml +++ b/test/tags/SpoolEase/eSun PLA Plus Multicolor 1kg.yml @@ -12,7 +12,7 @@ diameter_mm: 1.75 weight_grams: 1000 hotend_min_temp_c: 190 hotend_max_temp_c: 240 -bed_temp_c: 60 +bed_temp_c: 55 drying_temp_c: 50 drying_time_hours: 8 manufacturing_date: "0001-01-01" From 7bf17ca179800059d26184f22f10d5ffbf555849 Mon Sep 17 00:00:00 2001 From: yanshay Date: Sun, 19 Apr 2026 22:19:43 +0300 Subject: [PATCH 6/9] Remove old tests --- src/tag/spoolease/test_dump_processor.py | 68 ------------ src/tag/spoolease/test_tag_dump.txt | 135 ----------------------- 2 files changed, 203 deletions(-) delete mode 100644 src/tag/spoolease/test_dump_processor.py delete mode 100644 src/tag/spoolease/test_tag_dump.txt diff --git a/src/tag/spoolease/test_dump_processor.py b/src/tag/spoolease/test_dump_processor.py deleted file mode 100644 index 01aab9e..0000000 --- a/src/tag/spoolease/test_dump_processor.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Standalone SpoolEase dump test. - -Usage: - - from OpenRFID root folder: - python3 src/tag/spoolease/test_dump_processor.py - -How it works: - - Reads the NFC Tools-style page dump from `test_tag_dump.txt` - - Converts the dumped hex bytes into the raw tag byte buffer expected by OpenRFID - - Passes the raw bytes to `SpooleaseTagProcessor.process_tag(...)` - - Prints the parsed filament structure as JSON - -To test another tag: - - Replace the contents of `test_tag_dump.txt` with another NFC Tools dump - - Re-run the command above -""" - -import json -import logging -import re -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) - -from reader.scan_result import ScanResult -from tag.spoolease import SpooleaseTagProcessor -from tag.tag_types import TagType - - -PAGE_PATTERN = re.compile(r"\[\s*([0-9A-Fa-f:]+)\s*\]") - - -def parse_nfc_tools_dump(dump_text: str) -> bytes: - raw_bytes = bytearray() - - for line in dump_text.splitlines(): - match = PAGE_PATTERN.search(line) - if not match: - continue - - for byte_text in match.group(1).split(":"): - raw_bytes.append(int(byte_text, 16)) - - return bytes(raw_bytes) - - -def main() -> None: - logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") - - fixture_path = Path(__file__).with_name("test_tag_dump.txt") - raw_tag_data = parse_nfc_tools_dump(fixture_path.read_text()) - - processor = SpooleaseTagProcessor({"__name": "spoolease_test"}) - scan_result = ScanResult(TagType.MifareUltralight, b"\x04\x7B\x1F\x31\xC9\x2A\x81", b"", b"", b"\x00") - - filament = processor.process_tag(scan_result, raw_tag_data) - - if filament is None: - print("No filament data was parsed.") - return - - print(json.dumps(filament.to_dict(), indent=2, sort_keys=True)) - - -if __name__ == "__main__": - main() diff --git a/src/tag/spoolease/test_tag_dump.txt b/src/tag/spoolease/test_tag_dump.txt deleted file mode 100644 index ea37f19..0000000 --- a/src/tag/spoolease/test_tag_dump.txt +++ /dev/null @@ -1,135 +0,0 @@ -[ 04:7B:1F:E8 ] Addr. 00 : UID0 - UID2 / BCC0 -[ 31:C9:2A:81 ] Addr. 01 : UID3 - UID6 -[ 53:48:00:00 ] Addr. 02 : BCC1 / INT. / LOCK0 - LOCK1 -[ E1:10:3E:00 ] Addr. 03 : OTP0 - OTP3 -[ 03:BB:D1:01 ] Addr. 04 : DATA -[ B7:55:04:74 ] Addr. 05 : DATA -[ 61:67:2E:73 ] Addr. 06 : DATA -[ 70:6F:6F:6C ] Addr. 07 : DATA -[ 65:61:73:65 ] Addr. 08 : DATA -[ 2E:69:6F:2F ] Addr. 09 : DATA -[ 53:31:2F:3F ] Addr. 0A : DATA -[ 54:47:3D:30 ] Addr. 0B : DATA -[ 34:37:42:31 ] Addr. 0C : DATA -[ 46:33:31:43 ] Addr. 0D : DATA -[ 39:32:41:38 ] Addr. 0E : DATA -[ 31:26:49:44 ] Addr. 0F : DATA -[ 3D:35:33:26 ] Addr. 10 : DATA -[ 44:45:3D:31 ] Addr. 11 : DATA -[ 37:37:36:33 ] Addr. 12 : DATA -[ 35:30:33:36 ] Addr. 13 : DATA -[ 34:26:44:41 ] Addr. 14 : DATA -[ 3D:31:37:37 ] Addr. 15 : DATA -[ 36:31:32:37 ] Addr. 16 : DATA -[ 30:39:32:26 ] Addr. 17 : DATA -[ 4D:3D:50:4C ] Addr. 18 : DATA -[ 41:26:4D:53 ] Addr. 19 : DATA -[ 3D:50:6C:75 ] Addr. 1A : DATA -[ 73:26:43:43 ] Addr. 1B : DATA -[ 3D:42:38:37 ] Addr. 1C : DATA -[ 33:33:33:46 ] Addr. 1D : DATA -[ 46:3B:35:30 ] Addr. 1E : DATA -[ 43:38:37:38 ] Addr. 1F : DATA -[ 46:46:3B:44 ] Addr. 20 : DATA -[ 34:41:46:33 ] Addr. 21 : DATA -[ 37:46:46:26 ] Addr. 22 : DATA -[ 43:4E:3D:52 ] Addr. 23 : DATA -[ 61:69:6E:62 ] Addr. 24 : DATA -[ 6F:77:26:42 ] Addr. 25 : DATA -[ 3D:65:53:55 ] Addr. 26 : DATA -[ 4E:26:57:4C ] Addr. 27 : DATA -[ 3D:31:30:30 ] Addr. 28 : DATA -[ 30:26:53:43 ] Addr. 29 : DATA -[ 3D:47:46:4C ] Addr. 2A : DATA -[ 30:33:26:53 ] Addr. 2B : DATA -[ 4E:3D:65:53 ] Addr. 2C : DATA -[ 55:4E:25:32 ] Addr. 2D : DATA -[ 30:50:4C:41 ] Addr. 2E : DATA -[ 25:32:42:26 ] Addr. 2F : DATA -[ 4E:4E:3D:31 ] Addr. 30 : DATA -[ 39:30:26:4E ] Addr. 31 : DATA -[ 58:3D:32:34 ] Addr. 32 : DATA -[ 30:FE:00:00 ] Addr. 33 : DATA -[ 30:26:4E:58 ] Addr. 34 : DATA -[ 3D:32:34:30 ] Addr. 35 : DATA -[ FE:00:00:00 ] Addr. 36 : DATA -[ 00:00:00:00 ] Addr. 37 : DATA -[ 00:00:00:00 ] Addr. 38 : DATA -[ 00:00:00:00 ] Addr. 39 : DATA -[ 00:00:00:00 ] Addr. 3A : DATA -[ 00:00:00:00 ] Addr. 3B : DATA -[ 00:00:00:00 ] Addr. 3C : DATA -[ 00:00:00:00 ] Addr. 3D : DATA -[ 00:00:00:00 ] Addr. 3E : DATA -[ 00:00:00:00 ] Addr. 3F : DATA -[ 00:00:00:00 ] Addr. 40 : DATA -[ 00:00:00:00 ] Addr. 41 : DATA -[ 00:00:00:00 ] Addr. 42 : DATA -[ 00:00:00:00 ] Addr. 43 : DATA -[ 00:00:00:00 ] Addr. 44 : DATA -[ 00:00:00:00 ] Addr. 45 : DATA -[ 00:00:00:00 ] Addr. 46 : DATA -[ 00:00:00:00 ] Addr. 47 : DATA -[ 00:00:00:00 ] Addr. 48 : DATA -[ 00:00:00:00 ] Addr. 49 : DATA -[ 00:00:00:00 ] Addr. 4A : DATA -[ 00:00:00:00 ] Addr. 4B : DATA -[ 00:00:00:00 ] Addr. 4C : DATA -[ 00:00:00:00 ] Addr. 4D : DATA -[ 00:00:00:00 ] Addr. 4E : DATA -[ 00:00:00:00 ] Addr. 4F : DATA -[ 00:00:00:00 ] Addr. 50 : DATA -[ 00:00:00:00 ] Addr. 51 : DATA -[ 00:00:00:00 ] Addr. 52 : DATA -[ 00:00:00:00 ] Addr. 53 : DATA -[ 00:00:00:00 ] Addr. 54 : DATA -[ 00:00:00:00 ] Addr. 55 : DATA -[ 00:00:00:00 ] Addr. 56 : DATA -[ 00:00:00:00 ] Addr. 57 : DATA -[ 00:00:00:00 ] Addr. 58 : DATA -[ 00:00:00:00 ] Addr. 59 : DATA -[ 00:00:00:00 ] Addr. 5A : DATA -[ 00:00:00:00 ] Addr. 5B : DATA -[ 00:00:00:00 ] Addr. 5C : DATA -[ 00:00:00:00 ] Addr. 5D : DATA -[ 00:00:00:00 ] Addr. 5E : DATA -[ 00:00:00:00 ] Addr. 5F : DATA -[ 00:00:00:00 ] Addr. 60 : DATA -[ 00:00:00:00 ] Addr. 61 : DATA -[ 00:00:00:00 ] Addr. 62 : DATA -[ 00:00:00:00 ] Addr. 63 : DATA -[ 00:00:00:00 ] Addr. 64 : DATA -[ 00:00:00:00 ] Addr. 65 : DATA -[ 00:00:00:00 ] Addr. 66 : DATA -[ 00:00:00:00 ] Addr. 67 : DATA -[ 00:00:00:00 ] Addr. 68 : DATA -[ 00:00:00:00 ] Addr. 69 : DATA -[ 00:00:00:00 ] Addr. 6A : DATA -[ 00:00:00:00 ] Addr. 6B : DATA -[ 00:00:00:00 ] Addr. 6C : DATA -[ 00:00:00:00 ] Addr. 6D : DATA -[ 00:00:00:00 ] Addr. 6E : DATA -[ 00:00:00:00 ] Addr. 6F : DATA -[ 00:00:00:00 ] Addr. 70 : DATA -[ 00:00:00:00 ] Addr. 71 : DATA -[ 00:00:00:00 ] Addr. 72 : DATA -[ 00:00:00:00 ] Addr. 73 : DATA -[ 00:00:00:00 ] Addr. 74 : DATA -[ 00:00:00:00 ] Addr. 75 : DATA -[ 00:00:00:00 ] Addr. 76 : DATA -[ 00:00:00:00 ] Addr. 77 : DATA -[ 00:00:00:00 ] Addr. 78 : DATA -[ 00:00:00:00 ] Addr. 79 : DATA -[ 00:00:00:00 ] Addr. 7A : DATA -[ 00:00:00:00 ] Addr. 7B : DATA -[ 00:00:00:00 ] Addr. 7C : DATA -[ 00:00:00:00 ] Addr. 7D : DATA -[ 00:00:00:00 ] Addr. 7E : DATA -[ 00:00:00:00 ] Addr. 7F : DATA -[ 00:00:00:00 ] Addr. 80 : DATA -[ 00:00:00:00 ] Addr. 81 : DATA -[ 00:00:00:BD ] Addr. 82 : LOCK2 - LOCK4 -[ 04:00:00:FF ] Addr. 83 : CFG 0 (MIRROR / AUTH0) -[ 00:05:00:00 ] Addr. 84 : CFG 1 (ACCESS) -[ 00:00:00:00 ] Addr. 85 : PWD0 - PWD3 -[ 00:00:00:00 ] Addr. 86 : PACK0 - PACK1 From 311bf15de82ba6104e1b7698551420c6fadb7441 Mon Sep 17 00:00:00 2001 From: yanshay Date: Mon, 20 Apr 2026 16:31:46 +0300 Subject: [PATCH 7/9] Remove unneeded if --- src/tag/spoolease/processor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tag/spoolease/processor.py b/src/tag/spoolease/processor.py index dd03c80..c229775 100644 --- a/src/tag/spoolease/processor.py +++ b/src/tag/spoolease/processor.py @@ -14,8 +14,7 @@ def process_ndef(self, scan_result: ScanResult, ndef_records: list[NdefRecord]) for record in ndef_records: if record.tnf == 0x01 and record.type == "U": filament = self.__parse_spoolease_payload(record.payload) - if filament is not None: - return filament + return filament return None From c784f19ae01a0c8b8d00a17308b75f02985f724b Mon Sep 17 00:00:00 2001 From: yanshay Date: Mon, 20 Apr 2026 16:33:46 +0300 Subject: [PATCH 8/9] format --- src/tag/spoolease/processor.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/tag/spoolease/processor.py b/src/tag/spoolease/processor.py index c229775..27cb8f4 100644 --- a/src/tag/spoolease/processor.py +++ b/src/tag/spoolease/processor.py @@ -10,25 +10,33 @@ class SpooleaseTagProcessor(NdefTagProcessor): def __init__(self, config: dict): super().__init__(config) - def process_ndef(self, scan_result: ScanResult, ndef_records: list[NdefRecord]) -> GenericFilament | None: + def process_ndef( + self, scan_result: ScanResult, ndef_records: list[NdefRecord] + ) -> GenericFilament | None: for record in ndef_records: if record.tnf == 0x01 and record.type == "U": filament = self.__parse_spoolease_payload(record.payload) return filament - return None def __parse_spoolease_payload(self, payload: bytes) -> GenericFilament | None: if payload is None or not isinstance(payload, (bytes, bytearray)): - self.logger.error("SpoolEase payload parsing failed: Invalid payload parameter") + self.logger.error( + "SpoolEase payload parsing failed: Invalid payload parameter" + ) return None try: url = self.__decode_uri_payload(payload) parsed = urllib.parse.urlparse(url) - if parsed.netloc.lower() != "tag.spoolease.io" or not parsed.path.startswith("/S1"): - self.logger.error("SpoolEase payload parsing failed: Unsupported URL '%s'", url) + if ( + parsed.netloc.lower() != "tag.spoolease.io" + or not parsed.path.startswith("/S1") + ): + self.logger.error( + "SpoolEase payload parsing failed: Unsupported URL '%s'", url + ) return None query = urllib.parse.parse_qs(parsed.query, keep_blank_values=False) @@ -44,7 +52,9 @@ def __parse_spoolease_payload(self, payload: bytes) -> GenericFilament | None: weight_grams = self.__parse_optional_int(query, "WL", 1000) if hotend_max_temp_c < hotend_min_temp_c: - self.logger.error("SpoolEase payload parsing failed: Invalid temperature values") + self.logger.error( + "SpoolEase payload parsing failed: Invalid temperature values" + ) return None colors = self.__parse_colors(color_field) @@ -52,11 +62,15 @@ def __parse_spoolease_payload(self, payload: bytes) -> GenericFilament | None: bed_temp_c = extra_data.bed_temp_c if extra_data is not None else 0.0 drying_temp_c = extra_data.drying_temp_c if extra_data is not None else 0.0 - drying_time_hours = extra_data.drying_time_hours if extra_data is not None else 0.0 + drying_time_hours = ( + extra_data.drying_time_hours if extra_data is not None else 0.0 + ) return GenericFilament( source_processor=self.name, - unique_id=GenericFilament.generate_unique_id("SpoolEase", brand, material, subtype, *colors, weight_grams), + unique_id=GenericFilament.generate_unique_id( + "SpoolEase", brand, material, subtype, *colors, weight_grams + ), manufacturer=brand, type=material, modifiers=[subtype] if subtype else [], @@ -91,7 +105,9 @@ def __parse_required_int(self, query: dict[str, list[str]], key: str) -> int: raise ValueError(f"Missing required field '{key}'") return int(values[0]) - def __parse_optional_int(self, query: dict[str, list[str]], key: str, default: int) -> int: + def __parse_optional_int( + self, query: dict[str, list[str]], key: str, default: int + ) -> int: values = query.get(key) if not values or values[0] == "": return default From 68ad1bd9e4057fa0a6a58a176177fed2f0d948bb Mon Sep 17 00:00:00 2001 From: yanshay Date: Mon, 20 Apr 2026 16:39:46 +0300 Subject: [PATCH 9/9] Bring back the test for None --- src/tag/spoolease/processor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tag/spoolease/processor.py b/src/tag/spoolease/processor.py index 27cb8f4..59353e0 100644 --- a/src/tag/spoolease/processor.py +++ b/src/tag/spoolease/processor.py @@ -16,7 +16,8 @@ def process_ndef( for record in ndef_records: if record.tnf == 0x01 and record.type == "U": filament = self.__parse_spoolease_payload(record.payload) - return filament + if filament is not None: + return filament return None def __parse_spoolease_payload(self, payload: bytes) -> GenericFilament | None: