From cae03c1104a58793489bbac81b47efec99b25ec6 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:56:12 +0200 Subject: [PATCH 1/3] feat(tagxl): add `DataRate` support to `Port151Payload` and update decoder tests --- .secrets.baseline | 62 ++++++++++++++-------------- pkg/decoder/decoder.go | 41 +++++++++--------- pkg/decoder/tagxl/v1/decoder.go | 1 + pkg/decoder/tagxl/v1/decoder_test.go | 7 ++++ pkg/decoder/tagxl/v1/port151.go | 2 + 5 files changed, 62 insertions(+), 51 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 8c7e8de..5f3f392 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1421,7 +1421,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", "is_verified": false, - "line_number": 331, + "line_number": 338, "is_secret": false }, { @@ -1429,7 +1429,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "10e8fe5b6a5342c5ead45cffec2d001a28e0c1bb", "is_verified": false, - "line_number": 344, + "line_number": 351, "is_secret": false }, { @@ -1437,7 +1437,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", "is_verified": false, - "line_number": 349, + "line_number": 356, "is_secret": false }, { @@ -1445,7 +1445,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", "is_verified": false, - "line_number": 354, + "line_number": 361, "is_secret": false }, { @@ -1453,7 +1453,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", "is_verified": false, - "line_number": 359, + "line_number": 366, "is_secret": false }, { @@ -1461,7 +1461,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", "is_verified": false, - "line_number": 364, + "line_number": 371, "is_secret": false }, { @@ -1469,7 +1469,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 385, + "line_number": 392, "is_secret": false }, { @@ -1477,7 +1477,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 396, + "line_number": 403, "is_secret": false }, { @@ -1485,7 +1485,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 408, + "line_number": 415, "is_secret": false }, { @@ -1493,7 +1493,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 421, + "line_number": 428, "is_secret": false }, { @@ -1501,7 +1501,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 435, + "line_number": 442, "is_secret": false }, { @@ -1509,7 +1509,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 445, + "line_number": 452, "is_secret": false }, { @@ -1517,7 +1517,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 457, + "line_number": 464, "is_secret": false }, { @@ -1525,7 +1525,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 471, + "line_number": 478, "is_secret": false }, { @@ -1533,7 +1533,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 487, + "line_number": 494, "is_secret": false }, { @@ -1541,7 +1541,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", "is_verified": false, - "line_number": 505, + "line_number": 512, "is_secret": false }, { @@ -1549,7 +1549,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 659, + "line_number": 666, "is_secret": false }, { @@ -1557,7 +1557,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 665, + "line_number": 672, "is_secret": false }, { @@ -1565,7 +1565,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 676, + "line_number": 683, "is_secret": false }, { @@ -1573,7 +1573,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 688, + "line_number": 695, "is_secret": false }, { @@ -1581,7 +1581,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 701, + "line_number": 708, "is_secret": false }, { @@ -1589,7 +1589,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 715, + "line_number": 722, "is_secret": false }, { @@ -1597,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 730, + "line_number": 737, "is_secret": false }, { @@ -1605,7 +1605,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 741, + "line_number": 748, "is_secret": false }, { @@ -1613,7 +1613,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 754, + "line_number": 761, "is_secret": false }, { @@ -1621,7 +1621,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 769, + "line_number": 776, "is_secret": false }, { @@ -1629,7 +1629,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 786, + "line_number": 793, "is_secret": false }, { @@ -1637,7 +1637,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", "is_verified": false, - "line_number": 811, + "line_number": 818, "is_secret": false }, { @@ -1645,7 +1645,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1045, + "line_number": 1052, "is_secret": false }, { @@ -1653,7 +1653,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1228, + "line_number": 1235, "is_secret": false } ], @@ -2228,5 +2228,5 @@ } ] }, - "generated_at": "2025-09-24T19:16:14Z" + "generated_at": "2025-10-17T14:56:08Z" } diff --git a/pkg/decoder/decoder.go b/pkg/decoder/decoder.go index 2a7d09d..a7fe9ce 100644 --- a/pkg/decoder/decoder.go +++ b/pkg/decoder/decoder.go @@ -12,26 +12,27 @@ type Decoder interface { type Feature string const ( - FeatureTimestamp Feature = "timestamp" - FeatureResetReason Feature = "resetReason" - FeatureGNSS Feature = "gnss" - FeatureBuffered Feature = "buffered" - FeatureBattery Feature = "battery" - FeaturePhotovoltaic Feature = "photovoltaic" - FeatureTemperature Feature = "temperature" - FeatureHumidity Feature = "humidity" - FeaturePressure Feature = "pressure" - FeatureWiFi Feature = "wifi" - FeatureBle Feature = "ble" - FeatureButton Feature = "button" - FeatureConfig Feature = "config" - FeatureConfigChange Feature = "configChange" - FeatureMoving Feature = "moving" - FeatureDutyCycle Feature = "dutyCycle" - FeatureFirmwareVersion Feature = "firmwareVersion" - FeatureHardwareVersion Feature = "hardwareVersion" - FeatureRotationState Feature = "rotationState" - FeatureSequenceNumber Feature = "sequenceNumber" + FeatureTimestamp Feature = "timestamp" + FeatureResetReason Feature = "resetReason" + FeatureGNSS Feature = "gnss" + FeatureBuffered Feature = "buffered" + FeatureBattery Feature = "battery" + FeaturePhotovoltaic Feature = "photovoltaic" + FeatureTemperature Feature = "temperature" + FeatureHumidity Feature = "humidity" + FeaturePressure Feature = "pressure" + FeatureWiFi Feature = "wifi" + FeatureBle Feature = "ble" + FeatureButton Feature = "button" + FeatureConfig Feature = "config" + FeatureConfigChange Feature = "configChange" + FeatureMoving Feature = "moving" + FeatureDutyCycle Feature = "dutyCycle" + FeatureFirmwareVersion Feature = "firmwareVersion" + FeatureHardwareVersion Feature = "hardwareVersion" + FeatureRotationState Feature = "rotationState" + FeatureSequenceNumber Feature = "sequenceNumber" + FeatureAdaptiveDataRate Feature = "adaptiveDataRate" ) type DecodedUplink struct { diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index fbd771c..16279f4 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -136,6 +136,7 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon {Name: "WifiScans", Tag: 0x4b, Optional: true, Transform: func(v any) any { return uint16(common.BytesToUint32(v.([]byte)) & 0xffff) }}, + {Name: "DataRate", Tag: 0x4e, Optional: true}, }, TargetType: reflect.TypeOf(Port151Payload{}), Features: []decoder.Feature{}, diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index f1d4b07..583a602 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -257,6 +257,13 @@ func TestDecode(t *testing.T) { ResetCount: helpers.Uint16Ptr(3), }, }, + { + port: 151, + payload: "4c04014e0107", + expected: Port151Payload{ + DataRate: helpers.Uint8Ptr(7), + }, + }, { port: 152, payload: "ff", diff --git a/pkg/decoder/tagxl/v1/port151.go b/pkg/decoder/tagxl/v1/port151.go index db0a57c..75dde49 100644 --- a/pkg/decoder/tagxl/v1/port151.go +++ b/pkg/decoder/tagxl/v1/port151.go @@ -29,6 +29,7 @@ import ( // | 4a | 4 | reset cause register value | uint32 | // | 4b | 4 | gnss scans since reset | uint16 | // | | | wifi scans since reset | uint16 | +// | 4e | 1 | Data rate setting | uint8 | // +-----+------+------------------------------------------------+------------+ type Port151Payload struct { @@ -50,6 +51,7 @@ type Port151Payload struct { ResetCause *uint32 `json:"resetCause"` GnssScans *uint16 `json:"gnssScans"` WifiScans *uint16 `json:"wifiScans"` + DataRate *uint8 `json:"dataRate"` } var _ decoder.UplinkFeatureBattery = &Port151Payload{} From 1b3d21a295520b59eb362a7dd3b536e9a963e8ab Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 1 Jan 2026 19:55:50 +0000 Subject: [PATCH 2/3] feat(tagxl): implement DataRate support with helper functions and update Port151Payload --- .secrets.baseline | 62 +++++++++++------------ pkg/common/helpers.go | 9 ++++ pkg/decoder/data_rate.go | 10 ++++ pkg/decoder/decoder.go | 42 ++++++++-------- pkg/decoder/tagxl/v1/decoder.go | 9 +++- pkg/decoder/tagxl/v1/decoder_test.go | 30 ++++++++++- pkg/decoder/tagxl/v1/port151.go | 75 ++++++++++++++++++++-------- 7 files changed, 161 insertions(+), 76 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 5f3f392..8b84af7 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1421,7 +1421,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", "is_verified": false, - "line_number": 338, + "line_number": 366, "is_secret": false }, { @@ -1429,7 +1429,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "10e8fe5b6a5342c5ead45cffec2d001a28e0c1bb", "is_verified": false, - "line_number": 351, + "line_number": 379, "is_secret": false }, { @@ -1437,7 +1437,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", "is_verified": false, - "line_number": 356, + "line_number": 384, "is_secret": false }, { @@ -1445,7 +1445,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", "is_verified": false, - "line_number": 361, + "line_number": 389, "is_secret": false }, { @@ -1453,7 +1453,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", "is_verified": false, - "line_number": 366, + "line_number": 394, "is_secret": false }, { @@ -1461,7 +1461,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", "is_verified": false, - "line_number": 371, + "line_number": 399, "is_secret": false }, { @@ -1469,7 +1469,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 392, + "line_number": 420, "is_secret": false }, { @@ -1477,7 +1477,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 403, + "line_number": 431, "is_secret": false }, { @@ -1485,7 +1485,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 415, + "line_number": 443, "is_secret": false }, { @@ -1493,7 +1493,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 428, + "line_number": 456, "is_secret": false }, { @@ -1501,7 +1501,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 442, + "line_number": 470, "is_secret": false }, { @@ -1509,7 +1509,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 452, + "line_number": 480, "is_secret": false }, { @@ -1517,7 +1517,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 464, + "line_number": 492, "is_secret": false }, { @@ -1525,7 +1525,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 478, + "line_number": 506, "is_secret": false }, { @@ -1533,7 +1533,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 494, + "line_number": 522, "is_secret": false }, { @@ -1541,7 +1541,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", "is_verified": false, - "line_number": 512, + "line_number": 540, "is_secret": false }, { @@ -1549,7 +1549,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 666, + "line_number": 694, "is_secret": false }, { @@ -1557,7 +1557,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 672, + "line_number": 700, "is_secret": false }, { @@ -1565,7 +1565,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 683, + "line_number": 711, "is_secret": false }, { @@ -1573,7 +1573,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 695, + "line_number": 723, "is_secret": false }, { @@ -1581,7 +1581,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 708, + "line_number": 736, "is_secret": false }, { @@ -1589,7 +1589,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 722, + "line_number": 750, "is_secret": false }, { @@ -1597,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 737, + "line_number": 765, "is_secret": false }, { @@ -1605,7 +1605,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 748, + "line_number": 776, "is_secret": false }, { @@ -1613,7 +1613,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 761, + "line_number": 789, "is_secret": false }, { @@ -1621,7 +1621,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 776, + "line_number": 804, "is_secret": false }, { @@ -1629,7 +1629,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 793, + "line_number": 821, "is_secret": false }, { @@ -1637,7 +1637,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", "is_verified": false, - "line_number": 818, + "line_number": 846, "is_secret": false }, { @@ -1645,7 +1645,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1052, + "line_number": 1080, "is_secret": false }, { @@ -1653,7 +1653,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1235, + "line_number": 1263, "is_secret": false } ], @@ -2228,5 +2228,5 @@ } ] }, - "generated_at": "2025-10-17T14:56:08Z" + "generated_at": "2026-01-01T19:55:45Z" } diff --git a/pkg/common/helpers.go b/pkg/common/helpers.go index 95eb0a6..b1f8cda 100644 --- a/pkg/common/helpers.go +++ b/pkg/common/helpers.go @@ -459,6 +459,15 @@ func TimePointer(timestamp float64) *time.Time { return &time } +// DataRatePtr is a generic helper to create a pointer to any DataRate value. +// Returns interface{} to avoid circular dependencies. +func DataRatePtr(value interface{}) interface{} { + v := reflect.ValueOf(value) + ptr := reflect.New(v.Type()) + ptr.Elem().Set(v) + return ptr.Interface() +} + func TimePointerCompare(alpha *time.Time, bravo *time.Time) bool { if alpha == nil && bravo == nil { return true diff --git a/pkg/decoder/data_rate.go b/pkg/decoder/data_rate.go index ec8c350..3685f00 100644 --- a/pkg/decoder/data_rate.go +++ b/pkg/decoder/data_rate.go @@ -11,3 +11,13 @@ const DataRateGlacial DataRate = "glacial" const DataRateAutomaticNarrow DataRate = "automatic-narrow" const DataRateAutomaticWide DataRate = "automatic-wide" + +// TagXL specific data rates +const DataRateTagXLDR5 DataRate = "dr5-sf7" // 0: DR5 (EU868 SF7) +const DataRateTagXLDR4 DataRate = "dr4-sf8" // 1: DR4 (EU868 SF8) +const DataRateTagXLDR3 DataRate = "dr3-sf9" // 2: DR3 (EU868 SF9, US915 SF7) +const DataRateTagXLDR2 DataRate = "dr2-sf10" // 3: DR2 (EU868 SF10, US915 SF8) +const DataRateTagXLDR1 DataRate = "dr1-sf11" // 4: DR1 (EU868 SF11, US915 SF9) +const DataRateTagXLDR0 DataRate = "dr0-sf12" // 5: DR0 (EU868 SF12) +const DataRateTagXLDR1To3 DataRate = "dr1-3-array" // 6: DR1-3 array (EU868 SF9-11, US915 SF7-9) +const DataRateTagXLADR DataRate = "adr" // 7: ADR (SF7-12) for EU868 diff --git a/pkg/decoder/decoder.go b/pkg/decoder/decoder.go index a7fe9ce..2152a15 100644 --- a/pkg/decoder/decoder.go +++ b/pkg/decoder/decoder.go @@ -12,27 +12,27 @@ type Decoder interface { type Feature string const ( - FeatureTimestamp Feature = "timestamp" - FeatureResetReason Feature = "resetReason" - FeatureGNSS Feature = "gnss" - FeatureBuffered Feature = "buffered" - FeatureBattery Feature = "battery" - FeaturePhotovoltaic Feature = "photovoltaic" - FeatureTemperature Feature = "temperature" - FeatureHumidity Feature = "humidity" - FeaturePressure Feature = "pressure" - FeatureWiFi Feature = "wifi" - FeatureBle Feature = "ble" - FeatureButton Feature = "button" - FeatureConfig Feature = "config" - FeatureConfigChange Feature = "configChange" - FeatureMoving Feature = "moving" - FeatureDutyCycle Feature = "dutyCycle" - FeatureFirmwareVersion Feature = "firmwareVersion" - FeatureHardwareVersion Feature = "hardwareVersion" - FeatureRotationState Feature = "rotationState" - FeatureSequenceNumber Feature = "sequenceNumber" - FeatureAdaptiveDataRate Feature = "adaptiveDataRate" + FeatureTimestamp Feature = "timestamp" + FeatureResetReason Feature = "resetReason" + FeatureGNSS Feature = "gnss" + FeatureBuffered Feature = "buffered" + FeatureBattery Feature = "battery" + FeaturePhotovoltaic Feature = "photovoltaic" + FeatureTemperature Feature = "temperature" + FeatureHumidity Feature = "humidity" + FeaturePressure Feature = "pressure" + FeatureWiFi Feature = "wifi" + FeatureBle Feature = "ble" + FeatureButton Feature = "button" + FeatureConfig Feature = "config" + FeatureConfigChange Feature = "configChange" + FeatureMoving Feature = "moving" + FeatureDutyCycle Feature = "dutyCycle" + FeatureFirmwareVersion Feature = "firmwareVersion" + FeatureHardwareVersion Feature = "hardwareVersion" + FeatureRotationState Feature = "rotationState" + FeatureSequenceNumber Feature = "sequenceNumber" + FeatureDataRate Feature = "dataRate" ) type DecodedUplink struct { diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index 16279f4..7134f2e 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -136,10 +136,15 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon {Name: "WifiScans", Tag: 0x4b, Optional: true, Transform: func(v any) any { return uint16(common.BytesToUint32(v.([]byte)) & 0xffff) }}, - {Name: "DataRate", Tag: 0x4e, Optional: true}, + {Name: "DataRate", Tag: 0x4e, Optional: true, Transform: func(v any) any { + if b, ok := v.([]byte); ok && len(b) > 0 { + return DataRateFromUint8(b[0]) + } + return nil + }}, }, TargetType: reflect.TypeOf(Port151Payload{}), - Features: []decoder.Feature{}, + Features: []decoder.Feature{decoder.FeatureDataRate}, }, nil case 152: if len(payload) < 1 { diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index 583a602..fcb1ff9 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -261,7 +261,35 @@ func TestDecode(t *testing.T) { port: 151, payload: "4c04014e0107", expected: Port151Payload{ - DataRate: helpers.Uint8Ptr(7), + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLADR).(*decoder.DataRate), // 7: ADR (SF7-12) for EU868 + }, + }, + { + port: 151, + payload: "4c04014e0100", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR5).(*decoder.DataRate), // 0: DR5 (EU868 SF7) + }, + }, + { + port: 151, + payload: "4c04014e0103", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR2).(*decoder.DataRate), // 3: DR2 (EU868 SF10, US915 SF8) + }, + }, + { + port: 151, + payload: "4c04014e0105", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR0).(*decoder.DataRate), // 5: DR0 (EU868 SF12) + }, + }, + { + port: 151, + payload: "4c0401", // No DataRate field + expected: Port151Payload{ + DataRate: nil, }, }, { diff --git a/pkg/decoder/tagxl/v1/port151.go b/pkg/decoder/tagxl/v1/port151.go index 75dde49..fcad3c7 100644 --- a/pkg/decoder/tagxl/v1/port151.go +++ b/pkg/decoder/tagxl/v1/port151.go @@ -29,29 +29,38 @@ import ( // | 4a | 4 | reset cause register value | uint32 | // | 4b | 4 | gnss scans since reset | uint16 | // | | | wifi scans since reset | uint16 | -// | 4e | 1 | Data rate setting | uint8 | +// | 4e | 1 | Data rate setting (0-7) | uint8 | +// | | | 0: DR5 (EU868 SF7) | | +// | | | 1: DR4 (EU868 SF8) | | +// | | | 2: DR3 (EU868 SF9, US915 SF7) | | +// | | | 3: DR2 (EU868 SF10, US915 SF8) | | +// | | | 4: DR1 (EU868 SF11, US915 SF9) | | +// | | | 5: DR0 (EU868 SF12) | | +// | | | 6: DR1-3 array (EU868 SF9-11, US915 SF7-9) | | +// | | | 7: ADR (SF7-12) for EU868 | | +// | | | See: https://docs.truvami.com/docs/Devices/tag%20XL%20/Payload%20Format%20%20tag%20XL/#settings-downlink // +-----+------+------------------------------------------------+------------+ type Port151Payload struct { - AccelerometerEnabled *bool `json:"accelerometerEnabled"` - WifiEnabled *bool `json:"wifiEnabled"` - GnssEnabled *bool `json:"gnssEnabled"` - FirmwareUpgrade *bool `json:"firmwareUpgrade"` - LocalizationIntervalWhileMoving *uint16 `json:"movingInterval" validate:"gte=60,lte=86400"` - LocalizationIntervalWhileSteady *uint16 `json:"steadyInterval" validate:"gte=120,lte=86400"` - AccelerometerWakeupThreshold *uint16 `json:"accelerometerWakeupThreshold" validate:"gte=10,lte=8000"` - AccelerometerDelay *uint16 `json:"accelerometerDelay" validate:"gte=1000,lte=10000"` - HeartbeatInterval *uint8 `json:"heartbeatInterval" validate:"gte=0,lte=168"` - AdvertisementFirmwareUpgradeInterval *uint8 `json:"advertisementFirmwareUpgradeInterval" validate:"gte=1,lte=86400"` - Battery *float32 `json:"battery" validate:"gte=1,lte=5"` - FirmwareHash *string `json:"firmwareHash"` - RotationInvert *bool `json:"rotationInvert"` - RotationConfirmed *bool `json:"rotationConfirmed"` - ResetCount *uint16 `json:"resetCount"` - ResetCause *uint32 `json:"resetCause"` - GnssScans *uint16 `json:"gnssScans"` - WifiScans *uint16 `json:"wifiScans"` - DataRate *uint8 `json:"dataRate"` + AccelerometerEnabled *bool `json:"accelerometerEnabled"` + WifiEnabled *bool `json:"wifiEnabled"` + GnssEnabled *bool `json:"gnssEnabled"` + FirmwareUpgrade *bool `json:"firmwareUpgrade"` + LocalizationIntervalWhileMoving *uint16 `json:"movingInterval" validate:"gte=60,lte=86400"` + LocalizationIntervalWhileSteady *uint16 `json:"steadyInterval" validate:"gte=120,lte=86400"` + AccelerometerWakeupThreshold *uint16 `json:"accelerometerWakeupThreshold" validate:"gte=10,lte=8000"` + AccelerometerDelay *uint16 `json:"accelerometerDelay" validate:"gte=1000,lte=10000"` + HeartbeatInterval *uint8 `json:"heartbeatInterval" validate:"gte=0,lte=168"` + AdvertisementFirmwareUpgradeInterval *uint8 `json:"advertisementFirmwareUpgradeInterval" validate:"gte=1,lte=86400"` + Battery *float32 `json:"battery" validate:"gte=1,lte=5"` + FirmwareHash *string `json:"firmwareHash"` + RotationInvert *bool `json:"rotationInvert"` + RotationConfirmed *bool `json:"rotationConfirmed"` + ResetCount *uint16 `json:"resetCount"` + ResetCause *uint32 `json:"resetCause"` + GnssScans *uint16 `json:"gnssScans"` + WifiScans *uint16 `json:"wifiScans"` + DataRate *decoder.DataRate `json:"dataRate"` } var _ decoder.UplinkFeatureBattery = &Port151Payload{} @@ -158,7 +167,7 @@ func (p Port151Payload) GetBufferSize() *uint16 { } func (p Port151Payload) GetDataRate() *decoder.DataRate { - return nil + return p.DataRate } func (p Port151Payload) GetFirmwareHash() *string { @@ -168,3 +177,27 @@ func (p Port151Payload) GetFirmwareHash() *string { func (p Port151Payload) GetFirmwareVersion() *string { return nil } + +// DataRateFromUint8 converts a uint8 data rate value to the corresponding TagXL DataRate enum. +func DataRateFromUint8(value uint8) decoder.DataRate { + switch value { + case 0: + return decoder.DataRateTagXLDR5 + case 1: + return decoder.DataRateTagXLDR4 + case 2: + return decoder.DataRateTagXLDR3 + case 3: + return decoder.DataRateTagXLDR2 + case 4: + return decoder.DataRateTagXLDR1 + case 5: + return decoder.DataRateTagXLDR0 + case 6: + return decoder.DataRateTagXLDR1To3 + case 7: + return decoder.DataRateTagXLADR + default: + return decoder.DataRate("unknown") + } +} From 15697e080d3ec330665a5a0f7492e124348f7ecd Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:37:31 +0100 Subject: [PATCH 3/3] refactor(tagxl): update DataRatePtr to use generics, add DataRateUnknown constant --- pkg/common/helpers.go | 8 ++------ pkg/decoder/data_rate.go | 2 ++ pkg/decoder/tagxl/v1/decoder_test.go | 8 ++++---- pkg/decoder/tagxl/v1/port151.go | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pkg/common/helpers.go b/pkg/common/helpers.go index b1f8cda..fb03580 100644 --- a/pkg/common/helpers.go +++ b/pkg/common/helpers.go @@ -460,12 +460,8 @@ func TimePointer(timestamp float64) *time.Time { } // DataRatePtr is a generic helper to create a pointer to any DataRate value. -// Returns interface{} to avoid circular dependencies. -func DataRatePtr(value interface{}) interface{} { - v := reflect.ValueOf(value) - ptr := reflect.New(v.Type()) - ptr.Elem().Set(v) - return ptr.Interface() +func DataRatePtr[T any](value T) *T { + return &value } func TimePointerCompare(alpha *time.Time, bravo *time.Time) bool { diff --git a/pkg/decoder/data_rate.go b/pkg/decoder/data_rate.go index 3685f00..c709cfa 100644 --- a/pkg/decoder/data_rate.go +++ b/pkg/decoder/data_rate.go @@ -12,6 +12,8 @@ const DataRateGlacial DataRate = "glacial" const DataRateAutomaticNarrow DataRate = "automatic-narrow" const DataRateAutomaticWide DataRate = "automatic-wide" +const DataRateUnknown DataRate = "unknown" + // TagXL specific data rates const DataRateTagXLDR5 DataRate = "dr5-sf7" // 0: DR5 (EU868 SF7) const DataRateTagXLDR4 DataRate = "dr4-sf8" // 1: DR4 (EU868 SF8) diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index fcb1ff9..e659531 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -261,28 +261,28 @@ func TestDecode(t *testing.T) { port: 151, payload: "4c04014e0107", expected: Port151Payload{ - DataRate: helpers.DataRatePtr(decoder.DataRateTagXLADR).(*decoder.DataRate), // 7: ADR (SF7-12) for EU868 + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLADR), // 7: ADR (SF7-12) for EU868 }, }, { port: 151, payload: "4c04014e0100", expected: Port151Payload{ - DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR5).(*decoder.DataRate), // 0: DR5 (EU868 SF7) + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR5), // 0: DR5 (EU868 SF7) }, }, { port: 151, payload: "4c04014e0103", expected: Port151Payload{ - DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR2).(*decoder.DataRate), // 3: DR2 (EU868 SF10, US915 SF8) + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR2), // 3: DR2 (EU868 SF10, US915 SF8) }, }, { port: 151, payload: "4c04014e0105", expected: Port151Payload{ - DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR0).(*decoder.DataRate), // 5: DR0 (EU868 SF12) + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR0), // 5: DR0 (EU868 SF12) }, }, { diff --git a/pkg/decoder/tagxl/v1/port151.go b/pkg/decoder/tagxl/v1/port151.go index fcad3c7..f9af34e 100644 --- a/pkg/decoder/tagxl/v1/port151.go +++ b/pkg/decoder/tagxl/v1/port151.go @@ -198,6 +198,6 @@ func DataRateFromUint8(value uint8) decoder.DataRate { case 7: return decoder.DataRateTagXLADR default: - return decoder.DataRate("unknown") + return decoder.DataRateUnknown } }