From 40174eb8ed0adb4c47b66b07ec2c3d00bca363e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cejchan?= Date: Fri, 22 May 2026 15:07:48 +0200 Subject: [PATCH 1/4] CompactFloat.decode BFW-8783 --- utils/fields.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utils/fields.py b/utils/fields.py index 9107af1..68e0d71 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -41,6 +41,11 @@ def __init__(self, num: float): else: self.value = num + @staticmethod + def decode(data): + num = float(data) + return int(num) if num.is_integer() else round(num, CompactFloat.decimal_precision) + # Represent a raw CBOR data that are to be encoded verbatim class RawCBORData: @@ -81,8 +86,7 @@ def encode(self, data): class NumberField(Field): def decode(self, data): - num = float(data) - return int(num) if num.is_integer() else round(num, CompactFloat.decimal_precision) + return CompactFloat.decode(data) def encode(self, data): return CompactFloat(data) From 1f0e7eaa66a6e32b14c95f8f0e83cb71f6e7bdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cejchan?= Date: Fri, 22 May 2026 15:10:01 +0200 Subject: [PATCH 2/4] fields: SImplify CBOR encoding BFW-8783 --- utils/fields.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/utils/fields.py b/utils/fields.py index 68e0d71..515c9af 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -46,6 +46,11 @@ def decode(data): num = float(data) return int(num) if num.is_integer() else round(num, CompactFloat.decimal_precision) + def encode_cbor(self, encoder: cbor2.CBOREncoder): + # Always encode floats canonically + # Noncanonically, floats would always be encoded in 8 B, which is a lot of wasted space + cbor2.CBOREncoder(encoder.fp, canonical=True).encode(self.value) + # Represent a raw CBOR data that are to be encoded verbatim class RawCBORData: @@ -54,6 +59,9 @@ class RawCBORData: def __init__(self, data: bytes): self.data = data + def encode_cbor(self, encoder: cbor2.CBOREncoder): + encoder.fp.write(self.data) + class Field: key: int @@ -308,24 +316,12 @@ def update( for key, value in update_unknown_fields.items(): result[RawCBORData(bytes.fromhex(key))] = RawCBORData(bytes.fromhex(value)) - def default_enc(enc: cbor2.CBOREncoder, data: typing.Any): - if isinstance(data, CompactFloat): - # Always encode floats canonically - # Noncanonically, floats would always be encoded in 8 B, which is a lot of wasted space - cbor2.CBOREncoder(enc.fp, canonical=True).encode(data.value) - - elif isinstance(data, RawCBORData): - enc.fp.write(data.data) - - else: - raise RuntimeError(f"Unsupported type {type(data)} to encode") - data_io = io.BytesIO() encoder = cbor2.CBOREncoder( data_io, canonical=config.canonical, indefinite_containers=config.indefinite_containers, - default=default_enc, + default=lambda enc, data: data.encode_cbor(enc), ) encoder.encode(result) From 0e6db964d7e501c4c76be1fa0df80c7279d4946c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cejchan?= Date: Fri, 22 May 2026 15:49:29 +0200 Subject: [PATCH 3/4] Add `primary_color_lab`, `primary_color_ral` The LAB and RAL are currently added only to the primary color, as adding them for multiple secondary colors would probably not fit into the tag. This may be revised in the future. BFW-8783 --- data/main_fields.yaml | 24 +++++++++++++++++++++++- docs_src/nfc_data_format.md | 5 +++-- tests/encode_decode/01_data.bin | Bin 312 -> 312 bytes tests/encode_decode/01_info.yaml | 13 +++++++++---- tests/encode_decode/01_input.yaml | 2 ++ utils/fields.py | 24 ++++++++++++++++++++++++ utils/schema/field_types.schema.json | 23 +++++++++++++++++++++++ utils/schema/fields.schema.json | 6 ++++++ 8 files changed, 90 insertions(+), 7 deletions(-) diff --git a/data/main_fields.yaml b/data/main_fields.yaml index 00b535e..cdc33cc 100644 --- a/data/main_fields.yaml +++ b/data/main_fields.yaml @@ -195,6 +195,28 @@ - The alpha channel can be left out, in which case the data should have 3 bytes instead of 4 and the color will be considered fully opaque. - If a material doesn't have a single primary color (for example rainbow or coextruded filaments), this field can be null. +- key: 59 + name: primary_color_lab + type: color_lab + unit: '[L*, a*, b*]' + example: '[53.24, 111.12, -27.3]' + description: + - "Color of a material in the device-independent CIE L*a*b* (CIELAB 1976) color space with reference white D65/2\xB0." + - If present, the value MUST be obtained by physical spectrometry measurement; it MUST NOT be approximated (for example from RGB). + - "`L*` is bound to [0, 100], `a*` and `b*` values are dimensionless and are typically between \xB1127, but can theoretically get in the \xB1150 range." + +- key: 60 + name: primary_color_ral + type: string + unit: RAL code + max_length: 16 + example: 270 30 20 + description: + - RAL color identifier, without the "RAL" prefix. + - The value MUST correspond exactly to an official identifier (see https://www.ral-farben.de/en/all-ral-colours). + - If present, the physical material MUST match the referenced RAL swatch; it MUST NOT be approximated (for example from RGB/LAB). + - 'Examples of valid values: `3020`, `9005`, `1023`, `7016`, `270 30 20`, `190 50 35`, `530-1`, `850-M`, `P1 3020`.' + - key: 20 name: secondary_color_0 type: color_rgba @@ -485,4 +507,4 @@ description: - Recommended drying time (at `drying_temperature`). -# First unused key: 59 +# First unused key: 61 diff --git a/docs_src/nfc_data_format.md b/docs_src/nfc_data_format.md index 3a4cefe..64f631f 100644 --- a/docs_src/nfc_data_format.md +++ b/docs_src/nfc_data_format.md @@ -85,11 +85,12 @@ 1. `enum` fields are encoded as an integer, according to the enum field mapping 1. `enum_array` fields are encoded as CBOR arrays of integers, according to the field mapping 1. `timestamp` fields are encoded as UNIX timestamp integers -1. `bytes` and `uuid` types are encoded as CBOR byte string (type 2) -1. `color_rgba` fields are encoded as a CBOR byte string (type 2) with 3 to 4 bytes representing `[R, G, B]` or `[R, G, B, A]` values 1. `number` types can be encoded as either unsigned integers (type 0), signed integers (type 1), half floats or floats +1. `bytes` and `uuid` types are encoded as CBOR byte string (type 2) 1. `string` types are encoded as CBOR text string (type 3, UTF-8 is enforced by the CBOR specification) 1. The `X` in the `string:X` or `bytes:X` notation defines maximum permissible length of the data in bytes. +1. `color_rgba` fields are encoded as a CBOR byte string (type 2) with 3 to 4 bytes representing `[R, G, B]` or `[R, G, B, A]` values +1. `color_lab` fields are encoded as a CBOR array of 3 `number` type elements ### 3.2 UUIDs Some entities referenced in the data (see [Terminology](terminology.md)) can be identified by a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier). The UUID MAY be explicitly specified through a `XX_uuid` field, however that might not be desirable due to space constraints. As an alternative, the following algorithm defines a way to derive UUIDs from other fields. diff --git a/tests/encode_decode/01_data.bin b/tests/encode_decode/01_data.bin index 1442939a5a16766da70a3218a35c8c87da989100..f0e3c21a65c6515db762914557f42bb033ebb65d 100644 GIT binary patch delta 38 ucmdnNw1a8FEeQ$hW(lL8p38nla7ox?8krj?7#k=U8T_C4P-(IVqY3~H>J1bC delta 25 ZcmdnNw1a8Ft%)C%CmS%b!?6&f8UTDY2eAME diff --git a/tests/encode_decode/01_info.yaml b/tests/encode_decode/01_info.yaml index c622cea..76523af 100644 --- a/tests/encode_decode/01_info.yaml +++ b/tests/encode_decode/01_info.yaml @@ -8,7 +8,7 @@ regions: payload_offset: 4 absolute_offset: 70 size: 206 - used_size: 149 + used_size: 172 aux: payload_offset: 210 absolute_offset: 276 @@ -21,8 +21,8 @@ root: data_size: 312 payload_size: 245 overhead: 67 - payload_used_size: 154 - total_used_size: 221 + payload_used_size: 177 + total_used_size: 244 data: meta: aux_region_offset: 210 @@ -56,10 +56,15 @@ data: certifications: - ul_2818 - ul_94_v0 + primary_color_lab: + - 50 + - 11.297 + - 129.25 + primary_color_ral: 270 30 20 aux: {} raw_data: meta: a10218d2 - main: bf041b000007d0fcab45f9056a33333463353466303838080009000a76504c412050727573612047616c61787920426c61636b0b6950727573616d656e740e1a68d3c7d7101903e8111903f41219011813443d3e3dff181c9f17ff181df93cf6182218cd182318e1182418aa182518281826183c18271218281828182914182a1840182b18c8182c1864182d183418389f0001ffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + main: bf041b000007d0fcab45f9056a33333463353466303838080009000a76504c412050727573612047616c61787920426c61636b0b6950727573616d656e740e1a68d3c7d7101903e8111903f41219011813443d3e3dff181c9f17ff181df93cf6182218cd182318e1182418aa182518281826183c18271218281828182914182a1840182b18c8182c1864182d183418389f0001ff183b831832f949a6f9580a183c69323730203330203230ff00000000000000000000000000000000000000000000000000000000000000000000 aux: a000000000000000000000000000000000000000000000000000000000000000000000 uri: https://3dtag.org/s/334c54f088 validate: diff --git a/tests/encode_decode/01_input.yaml b/tests/encode_decode/01_input.yaml index 6e5c38f..520d041 100644 --- a/tests/encode_decode/01_input.yaml +++ b/tests/encode_decode/01_input.yaml @@ -9,6 +9,8 @@ data: brand_name: Prusament material_name: PLA Prusa Galaxy Black primary_color: "#3d3e3dff" + primary_color_lab: [50, 11.3, 129.3] + primary_color_ral: 270 30 20 tags: [glitter] certifications: [ul_2818, ul_94_v0] density: 1.24 diff --git a/utils/fields.py b/utils/fields.py index 515c9af..fbe53d4 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -197,6 +197,29 @@ def encode(self, data): return bytes.fromhex(m.group(1)) +class LABCborData: + data: list + + def __init__(self, data: list): + self.data = data + + def encode_cbor(self, encoder: cbor2.CBOREncoder): + # Encode with definite container, it's smaller and the record is fixed size + cbor2.CBOREncoder(encoder.fp, canonical=True, indefinite_containers=False, default=encoder.default).encode(self.data) + + +class ColorLABField(Field): + def decode(self, data): + assert type(data) is list + assert len(data) == 3 + return [CompactFloat.decode(x) for x in data] + + def encode(self, data): + assert type(data) is list + assert len(data) == 3 + return LABCborData([CompactFloat(x) for x in data]) + + class UUIDField(Field): def decode(self, data): return str(uuid.UUID(bytes=data)) @@ -214,6 +237,7 @@ def encode(self, data): "enum_array": EnumArrayField, "timestamp": IntField, "color_rgba": ColorRGBAField, + "color_lab": ColorLABField, "uuid": UUIDField, } diff --git a/utils/schema/field_types.schema.json b/utils/schema/field_types.schema.json index f6912b1..d1d201d 100644 --- a/utils/schema/field_types.schema.json +++ b/utils/schema/field_types.schema.json @@ -42,6 +42,29 @@ "type": "string", "description": "RGB(A) color in a standard hex notation '#RRGGBB(AA)'", "pattern": "^#[0-9a-f]{6}([0-9a-f]{2})?$" + }, + "color_lab": { + "type": "array", + "prefixItems": [ + { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + { + "type": "number", + "minimum": -150, + "maximum": 150 + }, + { + "type": "number", + "minimum": -150, + "maximum": 150 + } + ], + "items": false, + "minItems": 3, + "maxItems": 3 } } } diff --git a/utils/schema/fields.schema.json b/utils/schema/fields.schema.json index 7c74980..0c78f47 100644 --- a/utils/schema/fields.schema.json +++ b/utils/schema/fields.schema.json @@ -93,6 +93,12 @@ "primary_color": { "$ref": "field_types.schema.json#/definitions/color_rgba" }, + "primary_color_lab": { + "$ref": "field_types.schema.json#/definitions/color_lab" + }, + "primary_color_ral": { + "$ref": "field_types.schema.json#/definitions/string" + }, "secondary_color_0": { "$ref": "field_types.schema.json#/definitions/color_rgba" }, From ea74e4b540e816054fe1f82fcf2390aad5177749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C4=8Cejchan?= Date: Fri, 22 May 2026 15:30:22 +0200 Subject: [PATCH 4/4] Fix CompactFloat precision `num` was being downcasted to float16, which made the if true when it shouldn't have been. BFW-8783 --- tests/encode_decode/01_data.bin | Bin 312 -> 312 bytes tests/encode_decode/01_info.yaml | 12 ++++++------ utils/fields.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/encode_decode/01_data.bin b/tests/encode_decode/01_data.bin index f0e3c21a65c6515db762914557f42bb033ebb65d..8f537577004d414e2c7b0f0005dabe128074ddf9 100644 GIT binary patch delta 24 gcmdnNw1a8FeXd`QCTGt6a%S{7JMpRZWD!Oc0FT)Ux&QzG delta 20 ccmdnNw1a8FeYT&T%YH_1O?;_6S%6Uk0Ao-Ho&W#< diff --git a/tests/encode_decode/01_info.yaml b/tests/encode_decode/01_info.yaml index 76523af..0c7251d 100644 --- a/tests/encode_decode/01_info.yaml +++ b/tests/encode_decode/01_info.yaml @@ -8,7 +8,7 @@ regions: payload_offset: 4 absolute_offset: 70 size: 206 - used_size: 172 + used_size: 176 aux: payload_offset: 210 absolute_offset: 276 @@ -21,8 +21,8 @@ root: data_size: 312 payload_size: 245 overhead: 67 - payload_used_size: 177 - total_used_size: 244 + payload_used_size: 181 + total_used_size: 248 data: meta: aux_region_offset: 210 @@ -58,13 +58,13 @@ data: - ul_94_v0 primary_color_lab: - 50 - - 11.297 - - 129.25 + - 11.3 + - 129.3 primary_color_ral: 270 30 20 aux: {} raw_data: meta: a10218d2 - main: bf041b000007d0fcab45f9056a33333463353466303838080009000a76504c412050727573612047616c61787920426c61636b0b6950727573616d656e740e1a68d3c7d7101903e8111903f41219011813443d3e3dff181c9f17ff181df93cf6182218cd182318e1182418aa182518281826183c18271218281828182914182a1840182b18c8182c1864182d183418389f0001ff183b831832f949a6f9580a183c69323730203330203230ff00000000000000000000000000000000000000000000000000000000000000000000 + main: bf041b000007d0fcab45f9056a33333463353466303838080009000a76504c412050727573612047616c61787920426c61636b0b6950727573616d656e740e1a68d3c7d7101903e8111903f41219011813443d3e3dff181c9f17ff181df93cf6182218cd182318e1182418aa182518281826183c18271218281828182914182a1840182b18c8182c1864182d183418389f0001ff183b831832fa4134cccdfa43014ccd183c69323730203330203230ff000000000000000000000000000000000000000000000000000000000000 aux: a000000000000000000000000000000000000000000000000000000000000000000000 uri: https://3dtag.org/s/334c54f088 validate: diff --git a/utils/fields.py b/utils/fields.py index fbe53d4..72c2c1f 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -32,10 +32,10 @@ def __init__(self, num: float): if num.is_integer(): self.value = int(num) - elif abs(num - numpy.float16(num)) < CompactFloat.required_precision: + elif abs(num - float(numpy.float16(num))) < CompactFloat.required_precision: self.value = float(numpy.float16(num)) - elif abs(num - numpy.float32(num)) < CompactFloat.required_precision: + elif abs(num - float(numpy.float32(num))) < CompactFloat.required_precision: self.value = float(numpy.float32(num)) else: