From 0f25d999ee8dd9a07462d67dfbbbb118671b1998 Mon Sep 17 00:00:00 2001 From: TigerTag-Project <206083157+TigerTag-Project@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:00:31 +0200 Subject: [PATCH] Read the remaining filament weight (TigerTag and OpenTag3D) Add a shared remaining_weight_grams field to GenericFilament for the measured remaining filament, as written back by smart scales. 0 means unknown, like the other optional fields. It is exported in to_dict(), so webhooks and templates can use it, and shown in pretty_text() when known. - TigerTag: read Measure Available (+76, u24 BE), converted with the same unit as the measure. Factory-written tags leave it at 0 (not weighed). - OpenTag3D: export Measured Filament Weight, which was decoded but not exported. - Tests: an eSun TigerTag+ copy with 742 g written at +76 (outside the signed region, so the signature stays valid), the original dump as the unknown case, and an OpenTag3D measured-weight case. --- src/filament/generic.py | 9 ++++++--- src/tag/opentag3d/README.md | 3 ++- src/tag/opentag3d/processor.py | 4 +++- src/tag/tigertag/constants.py | 1 + src/tag/tigertag/processor.py | 10 ++++++++++ .../Polar Filament PLA Pure Light Blue.yml | 1 + ...fill Cold White TigerTag+ Weighed 742g.bin | Bin 0 -> 180 bytes ...fill Cold White TigerTag+ Weighed 742g.yml | 18 ++++++++++++++++++ ... PLA Basic Refill Cold White TigerTag+.yml | 1 + test/test_opentag3d.py | 9 +++++++++ 10 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.bin create mode 100644 test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.yml diff --git a/src/filament/generic.py b/src/filament/generic.py index 81a71bc..fb29a9a 100644 --- a/src/filament/generic.py +++ b/src/filament/generic.py @@ -26,7 +26,8 @@ def __init__(self, drying_temp_c: float, drying_time_hours: float, manufacturing_date: str, # ISO 8601 date string - td: float = 0.0 # Transmission Distance in mm for HueForge/OrcaSlicer-FullSpectrum + td: float = 0.0, # Transmission Distance in mm for HueForge/OrcaSlicer-FullSpectrum + remaining_weight_grams: float = 0.0, # Measured remaining filament (e.g. written back by a smart scale), 0 = unknown ): self.source_processor = source_processor self.unique_id = unique_id @@ -43,6 +44,7 @@ def __init__(self, self.drying_time_hours = drying_time_hours self.manufacturing_date = manufacturing_date self.td = td + self.remaining_weight_grams = remaining_weight_grams if "CF" in self.modifiers: self.type += "-CF" @@ -71,7 +73,7 @@ def pretty_text(self) -> str: f"- Drying: {self.drying_temp_c:.1f}C for {self.drying_time_hours:.1f} hours", f"- Manufactured on: {self.manufacturing_date}", f"- TD: {self.td:.1f} mm" - ]) + ] + ([f"- Remaining: {self.remaining_weight_grams} grams"] if self.remaining_weight_grams else [])) @property def rgba(self) -> int: @@ -102,7 +104,8 @@ def to_dict(self) -> dict: "drying_temp_c": self.drying_temp_c, "drying_time_hours": self.drying_time_hours, "manufacturing_date": self.manufacturing_date, - "td": self.td + "td": self.td, + "remaining_weight_grams": self.remaining_weight_grams } @staticmethod diff --git a/src/tag/opentag3d/README.md b/src/tag/opentag3d/README.md index f7fcd5d..ef6285c 100644 --- a/src/tag/opentag3d/README.md +++ b/src/tag/opentag3d/README.md @@ -36,7 +36,8 @@ types, and scaling come from JSON; the mapping to `GenericFilament` is explicit. - Missing print temperature bounds fall back to the target temperature. - Primary color is retained even when transparent; transparent-black secondary colors are omitted. Exported colors use ARGB. -- Weight is the nominal filament weight, not measured weight or remaining weight. +- Weight is the nominal filament weight. The measured filament weight is exported + as `remaining_weight_grams` (0 when not measured). - The unique ID hashes the physical tag UID, since a serial may identify a batch. - Material names still follow `GenericFilament`'s existing supported-material validation; unrecognized materials fail cleanly rather than becoming PLA. diff --git a/src/tag/opentag3d/processor.py b/src/tag/opentag3d/processor.py index 57f2610..450dd04 100644 --- a/src/tag/opentag3d/processor.py +++ b/src/tag/opentag3d/processor.py @@ -95,7 +95,7 @@ def __to_filament(self, scan_result: ScanResult, data: dict) -> GenericFilament: modifiers=[data["material_mod"]] if data["material_mod"] else [], colors=colors, diameter_mm=data["diameter"], - # Target Weight excludes the spool and is not measured/remaining weight. + # Target Weight excludes the spool; the measured weight is exported separately. weight_grams=data["weight"], hotend_min_temp_c=hotend_min_temp_c, hotend_max_temp_c=hotend_max_temp_c, @@ -105,4 +105,6 @@ def __to_filament(self, scan_result: ScanResult, data: dict) -> GenericFilament: # Reuse the other processors' unknown-date sentinel for absent dates. manufacturing_date=data["mfg_date"] or "0001-01-01", td=data["td"], + # Measured Filament Weight: actual filament weight, 0 when not measured. + remaining_weight_grams=data["measured_weight"], ) diff --git a/src/tag/tigertag/constants.py b/src/tag/tigertag/constants.py index 75cdd52..5710835 100644 --- a/src/tag/tigertag/constants.py +++ b/src/tag/tigertag/constants.py @@ -22,6 +22,7 @@ OFF_TIMESTAMP = 32 OFF_TD = 44 OFF_METADATA = 48 +OFF_MEASURE_AVAILABLE = 76 # u24 BE, remaining quantity in the same unit as the measure OFF_SIGNATURE = 80 MIN_DATA_LENGTH = 96 diff --git a/src/tag/tigertag/processor.py b/src/tag/tigertag/processor.py index 975670b..60998dd 100644 --- a/src/tag/tigertag/processor.py +++ b/src/tag/tigertag/processor.py @@ -68,6 +68,13 @@ def __parse_tigertag(self, scan_result: ScanResult, user_data: bytes, tag_id: in timestamp_raw = struct.unpack_from('>I', user_data, Constants.OFF_TIMESTAMP)[0] + # Measure Available: remaining quantity, written back by a smart scale + # (e.g. Tiger Scale). Factory-written tags leave it at 0 (not measured). + available_value = 0 + if len(user_data) >= Constants.OFF_MEASURE_AVAILABLE + 3: + available_bytes = user_data[Constants.OFF_MEASURE_AVAILABLE:Constants.OFF_MEASURE_AVAILABLE + 3] + available_value = (available_bytes[0] << 16) | (available_bytes[1] << 8) | available_bytes[2] + material_label = self.registry.material_ids.get(material_id, f"Unknown({material_id})") material_type = self.registry.material_type_ids.get(material_id, material_label) filled_type = self.registry.filled_type_ids.get(material_id, "") @@ -80,6 +87,7 @@ def __parse_tigertag(self, scan_result: ScanResult, user_data: bytes, tag_id: in weight_grams = self.__convert_to_grams(measure_value, unit_id) manufacturing_date = self.__timestamp_to_date(timestamp_raw) + remaining_weight_grams = self.__convert_to_grams(available_value, unit_id) if available_value else 0.0 modifiers = [] for aspect_label in (aspect1_label, aspect2_label): @@ -95,6 +103,7 @@ def __parse_tigertag(self, scan_result: ScanResult, user_data: bytes, tag_id: in self.logger.debug(" Aspect1: %s, Aspect2: %s", aspect1_label, aspect2_label) self.logger.debug(" Color (ARGB): 0x%08X", argb_color) self.logger.debug(" Measure: %d %s (%.1f g)", measure_value, unit_label, weight_grams) + self.logger.debug(" Measure Available: %d %s (%.1f g)", available_value, unit_label, remaining_weight_grams) self.logger.debug(" Nozzle Temp: %d-%d °C", temp_min, temp_max) self.logger.debug(" Dry: %d °C for %d hours", dry_temp, dry_time) self.logger.debug(" Bed Temp: %d-%d °C", bed_temp_min, bed_temp_max) @@ -120,6 +129,7 @@ def __parse_tigertag(self, scan_result: ScanResult, user_data: bytes, tag_id: in drying_time_hours=float(dry_time), manufacturing_date=manufacturing_date, td=td_mm, + remaining_weight_grams=remaining_weight_grams, ) except Exception as e: diff --git a/test/tags/OpenTag3D/Polar Filament PLA Pure Light Blue.yml b/test/tags/OpenTag3D/Polar Filament PLA Pure Light Blue.yml index d57e4da..4529276 100644 --- a/test/tags/OpenTag3D/Polar Filament PLA Pure Light Blue.yml +++ b/test/tags/OpenTag3D/Polar Filament PLA Pure Light Blue.yml @@ -13,3 +13,4 @@ drying_temp_c: 65 drying_time_hours: 0 manufacturing_date: '2026-04-03' td: 0.0 +remaining_weight_grams: 0 diff --git a/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.bin b/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.bin new file mode 100644 index 0000000000000000000000000000000000000000..32b87ba803023d471324523e87f55c02e11c777e GIT binary patch literal 180 zcmb3BDpHaxB+&5i00YBA0U?Gx{HLc+TPt{bs&@uMpT%ygyHDT#XJCFI%5aI{nGuJs zqrtL2uNjC1OwSmy-WBqg_sq}_U>5r4_ER}ws^G`dIgD%X^j`}MTPj;BEHTHYg!@v< n>iNa8&%2JTnQzk{aNC5>ee=(1OD`#%`;UND?FHih$W{UX$+I@4 literal 0 HcmV?d00001 diff --git a/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.yml b/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.yml new file mode 100644 index 0000000..95e2802 --- /dev/null +++ b/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+ Weighed 742g.yml @@ -0,0 +1,18 @@ +bed_temp_c: 0.0 +colors: +- 0xFFDDE5ED +diameter_mm: 1.75 +drying_temp_c: 50.0 +drying_time_hours: 8.0 +hotend_max_temp_c: 230.0 +hotend_min_temp_c: 210.0 +manufacturer: eSun +manufacturing_date: '2025-11-12' +modifiers: +- Basic +source_processor: TigerTagProcessor +td: 0.0 +type: PLA +unique_id: c97d79c4ec83c89e5063834556625468b602e492996bfd674fbbeb238469d031 +weight_grams: 1000.0 +remaining_weight_grams: 742.0 diff --git a/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+.yml b/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+.yml index fa50170..8d2d79e 100644 --- a/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+.yml +++ b/test/tags/Tigertag/eSun PLA Basic Refill Cold White TigerTag+.yml @@ -15,3 +15,4 @@ td: 0.0 type: PLA unique_id: c97d79c4ec83c89e5063834556625468b602e492996bfd674fbbeb238469d031 weight_grams: 1000.0 +remaining_weight_grams: 0.0 diff --git a/test/test_opentag3d.py b/test/test_opentag3d.py index 95565f6..29a451d 100644 --- a/test/test_opentag3d.py +++ b/test/test_opentag3d.py @@ -175,6 +175,15 @@ def test_long_payload_ignores_reserved_extension_bytes(processor, scan, payload) assert result.weight_grams == 1000 +def test_measured_weight_is_exported_as_remaining_weight(processor, scan, payload): + # The captured spool was never weighed (measured weight 0 = unknown). + assert processor.process_tag(scan, tag(record(payload))).remaining_weight_grams == 0 + payload[164:166] = (742).to_bytes(2, "big") + result = processor.process_tag(scan, tag(record(payload))) + assert result.weight_grams == 1000 + assert result.remaining_weight_grams == 742 + + def test_configuration_runtime_and_webhook(scan, payload, monkeypatch): # Import the application's real configuration factory without Linux-only # GPIO/SPI modules; this test never constructs or accesses physical hardware.