From 4bd35adc5a865d19850eb9a1267856853bed8d38 Mon Sep 17 00:00:00 2001 From: Chris Wesseling Date: Mon, 22 Jul 2024 22:50:24 +0200 Subject: [PATCH] Add tlv.FloatMember Adds a FloatMember that encodes LE floats/doubles as specified in A.11.5 --- circuitmatter/tlv.py | 34 +++++++++++++++++++++++++++++++++- tests/test_tlv.py | 29 ++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/circuitmatter/tlv.py b/circuitmatter/tlv.py index b74ff11..e09ca3a 100644 --- a/circuitmatter/tlv.py +++ b/circuitmatter/tlv.py @@ -407,14 +407,46 @@ class IntMember(NumberMember[int, _OPT, _NULLABLE]): nullable: _NULLABLE = False, **kwargs, ): + """ + :param octets: Number of octests to use for encoding. + 1, 2, 4, 8 are 8, 16, 32, and 64 bits respectively + :param optional: Indicates whether the value MAY be omitted from the encoding. + Can be used for deprecation. + :param nullable: Indicates whether a TLV Null MAY be encoded in place of a value. + """ + # TODO 7.18.1 mentions other bit lengths (that are not a power of 2) than the TLV Appendix uformat = INT_SIZE[int(math.log2(octets))] - # little-endian + # < = little-endian self.format = f"<{uformat.lower() if signed else uformat}" super().__init__( tag, _format=self.format, optional=optional, nullable=nullable, **kwargs ) +class FloatMember(NumberMember[float, _OPT, _NULLABLE]): + def __init__( + self, + tag, + *, + octets: Literal[4, 8] = 4, + optional: _OPT = False, + nullable: _NULLABLE = False, + **kwargs, + ): + """ + :param octets: Number of octests to use for encoding. + 4, 8 are single and double precision floats respectively. + :param optional: Indicates whether the value MAY be omitted from the encoding. + Can be used for deprecation. + :param nullable: Indicates whether a TLV Null MAY be encoded in place of a value. + """ + # < = little-endian + self.format = f"<{'f' if octets == 4 else 'd'}" + super().__init__( + tag, _format=self.format, optional=optional, nullable=nullable, **kwargs + ) + + class BoolMember(Member[bool, _OPT, _NULLABLE]): max_value_length = 0 diff --git a/tests/test_tlv.py b/tests/test_tlv.py index 7c6f388..bc7faa9 100644 --- a/tests/test_tlv.py +++ b/tests/test_tlv.py @@ -338,11 +338,11 @@ class TestNull: # Double precision floating point negative infinity 0b 00 00 00 00 00 00 f0 ff # (-∞) class FloatSingle(tlv.TLVStructure): - f = tlv.NumberMember(None, "f") + f = tlv.FloatMember(None) class FloatDouble(tlv.TLVStructure): - f = tlv.NumberMember(None, "d") + f = tlv.FloatMember(None, octets=8) class TestFloatSingle: @@ -398,12 +398,12 @@ class TestFloatSingle: assert s.encode().tobytes() == b"\x0a\x00\x00\x80\xff" @given(v=...) - def test_roundtrip(self, v: float): - s = FloatSingle() + def test_roundtrip_double(self, v: float): + s = FloatDouble() s.f = v buffer = s.encode().tobytes() - s2 = FloatSingle(buffer) + s2 = FloatDouble(buffer) assert ( (math.isnan(s.f) and math.isnan(s2.f)) @@ -412,6 +412,25 @@ class TestFloatSingle: or math.isclose(s2.f, s.f, rel_tol=1e-7, abs_tol=1e-9) ) + @given( + v=st.floats( + # encoding to LE float32 raises OverflowError outside these ranges + # TODO: should we raise ValueError with a bounds check or encode -inf/inf? + min_value=(2**-126), + max_value=(2 - 2**-23) * 2**127, + ), + ) + def test_roundtrip_single(self, v: float): + s = FloatSingle() + s.f = v + buffer = s.encode().tobytes() + + s2 = FloatSingle(buffer) + + assert (math.isnan(s.f) and math.isnan(s2.f)) or math.isclose( + s2.f, s.f, rel_tol=1e-7, abs_tol=1e-9 + ) + class TestFloatDouble: def test_precision_float_0_0_decode(self):