diff --git a/.secrets.baseline b/.secrets.baseline index 8c7e8de..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": 331, + "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": 344, + "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": 349, + "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": 354, + "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": 359, + "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": 364, + "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": 385, + "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": 396, + "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": 408, + "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": 421, + "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": 435, + "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": 445, + "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": 457, + "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": 471, + "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": 487, + "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": 505, + "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": 659, + "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": 665, + "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": 676, + "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": 688, + "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": 701, + "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": 715, + "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": 730, + "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": 741, + "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": 754, + "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": 769, + "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": 786, + "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": 811, + "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": 1045, + "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": 1228, + "line_number": 1263, "is_secret": false } ], @@ -2228,5 +2228,5 @@ } ] }, - "generated_at": "2025-09-24T19:16:14Z" + "generated_at": "2026-01-01T19:55:45Z" } diff --git a/pkg/common/helpers.go b/pkg/common/helpers.go index 95eb0a6..fb03580 100644 --- a/pkg/common/helpers.go +++ b/pkg/common/helpers.go @@ -459,6 +459,11 @@ func TimePointer(timestamp float64) *time.Time { return &time } +// DataRatePtr is a generic helper to create a pointer to any DataRate value. +func DataRatePtr[T any](value T) *T { + return &value +} + 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..c709cfa 100644 --- a/pkg/decoder/data_rate.go +++ b/pkg/decoder/data_rate.go @@ -11,3 +11,15 @@ 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) +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 2a7d09d..2152a15 100644 --- a/pkg/decoder/decoder.go +++ b/pkg/decoder/decoder.go @@ -32,6 +32,7 @@ const ( 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 fbd771c..7134f2e 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -136,9 +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, 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 f1d4b07..e659531 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -257,6 +257,41 @@ func TestDecode(t *testing.T) { ResetCount: helpers.Uint16Ptr(3), }, }, + { + port: 151, + payload: "4c04014e0107", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLADR), // 7: ADR (SF7-12) for EU868 + }, + }, + { + port: 151, + payload: "4c04014e0100", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR5), // 0: DR5 (EU868 SF7) + }, + }, + { + port: 151, + payload: "4c04014e0103", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR2), // 3: DR2 (EU868 SF10, US915 SF8) + }, + }, + { + port: 151, + payload: "4c04014e0105", + expected: Port151Payload{ + DataRate: helpers.DataRatePtr(decoder.DataRateTagXLDR0), // 5: DR0 (EU868 SF12) + }, + }, + { + port: 151, + payload: "4c0401", // No DataRate field + expected: Port151Payload{ + DataRate: nil, + }, + }, { port: 152, payload: "ff", diff --git a/pkg/decoder/tagxl/v1/port151.go b/pkg/decoder/tagxl/v1/port151.go index db0a57c..f9af34e 100644 --- a/pkg/decoder/tagxl/v1/port151.go +++ b/pkg/decoder/tagxl/v1/port151.go @@ -29,27 +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 (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"` + 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{} @@ -156,7 +167,7 @@ func (p Port151Payload) GetBufferSize() *uint16 { } func (p Port151Payload) GetDataRate() *decoder.DataRate { - return nil + return p.DataRate } func (p Port151Payload) GetFirmwareHash() *string { @@ -166,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.DataRateUnknown + } +}