From 749ba61ca90b3fb4538092d717fc081806186096 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:56:09 +0200 Subject: [PATCH 1/2] feat(ble): add BLE ports 160-163 to smartlabel and tagxl decoders Decode the ts2 BLE uplink payloads (RSSI + MAC beacon lists) on ports 160 (steady), 161 (moving), 162 (steady + timestamp) and 163 (moving + timestamp) for both the smartlabel and tagxl v1 decoders. Each payload exposes the Ble and Moving features (plus Timestamp/Buffered for the timestamped variants), mirroring the existing Wi-Fi port layout. Also refresh .secrets.baseline for the new test payload vectors. --- .secrets.baseline | 100 +++++++++++++------ pkg/decoder/smartlabel/v1/decoder.go | 94 ++++++++++++++++++ pkg/decoder/smartlabel/v1/decoder_test.go | 79 +++++++++++++++ pkg/decoder/smartlabel/v1/port160.go | 98 ++++++++++++++++++ pkg/decoder/smartlabel/v1/port161.go | 98 ++++++++++++++++++ pkg/decoder/smartlabel/v1/port162.go | 107 ++++++++++++++++++++ pkg/decoder/smartlabel/v1/port163.go | 107 ++++++++++++++++++++ pkg/decoder/tagxl/v1/decoder.go | 86 ++++++++++++++++ pkg/decoder/tagxl/v1/decoder_test.go | 79 +++++++++++++++ pkg/decoder/tagxl/v1/port160.go | 98 ++++++++++++++++++ pkg/decoder/tagxl/v1/port161.go | 98 ++++++++++++++++++ pkg/decoder/tagxl/v1/port162.go | 116 ++++++++++++++++++++++ pkg/decoder/tagxl/v1/port163.go | 116 ++++++++++++++++++++++ 13 files changed, 1247 insertions(+), 29 deletions(-) create mode 100644 pkg/decoder/smartlabel/v1/port160.go create mode 100644 pkg/decoder/smartlabel/v1/port161.go create mode 100644 pkg/decoder/smartlabel/v1/port162.go create mode 100644 pkg/decoder/smartlabel/v1/port163.go create mode 100644 pkg/decoder/tagxl/v1/port160.go create mode 100644 pkg/decoder/tagxl/v1/port161.go create mode 100644 pkg/decoder/tagxl/v1/port162.go create mode 100644 pkg/decoder/tagxl/v1/port163.go diff --git a/.secrets.baseline b/.secrets.baseline index c79bc67..1900123 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -409,15 +409,29 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "395900609a0d94e6b75d0f0cb6b647d1d554c442", "is_verified": false, - "line_number": 411, + "line_number": 412, "is_secret": false }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "3a7e66ddf331d70868f554601fa617e041defb0f", + "is_verified": false, + "line_number": 426 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "4d3e8fa3f017be4abc77d60232e4fb4860ef42d5", + "is_verified": false, + "line_number": 439 + }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "81de30f2e09afabf90f74a4addd1ddbc354277d4", "is_verified": false, - "line_number": 424, + "line_number": 478, "is_secret": false }, { @@ -425,7 +439,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "a158f37d59e2f1ea505e92294149581814a5ffe3", "is_verified": false, - "line_number": 441, + "line_number": 495, "is_secret": false }, { @@ -433,7 +447,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 619, + "line_number": 689, "is_secret": false } ], @@ -1499,12 +1513,40 @@ "line_number": 406, "is_secret": false }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "bc4afa2f3630480ca72a32c5c8421b39f3ce1a71", + "is_verified": false, + "line_number": 433 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "395900609a0d94e6b75d0f0cb6b647d1d554c442", + "is_verified": false, + "line_number": 445 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "3a7e66ddf331d70868f554601fa617e041defb0f", + "is_verified": false, + "line_number": 459 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "4d3e8fa3f017be4abc77d60232e4fb4860ef42d5", + "is_verified": false, + "line_number": 472 + }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 449, + "line_number": 503, "is_secret": false }, { @@ -1512,7 +1554,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 460, + "line_number": 514, "is_secret": false }, { @@ -1520,7 +1562,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 472, + "line_number": 526, "is_secret": false }, { @@ -1528,7 +1570,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 485, + "line_number": 539, "is_secret": false }, { @@ -1536,7 +1578,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 499, + "line_number": 553, "is_secret": false }, { @@ -1544,7 +1586,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 509, + "line_number": 563, "is_secret": false }, { @@ -1552,7 +1594,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 521, + "line_number": 575, "is_secret": false }, { @@ -1560,7 +1602,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 535, + "line_number": 589, "is_secret": false }, { @@ -1568,7 +1610,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 551, + "line_number": 605, "is_secret": false }, { @@ -1576,7 +1618,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", "is_verified": false, - "line_number": 569, + "line_number": 623, "is_secret": false }, { @@ -1584,7 +1626,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 723, + "line_number": 777, "is_secret": false }, { @@ -1592,7 +1634,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 729, + "line_number": 783, "is_secret": false }, { @@ -1600,7 +1642,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 740, + "line_number": 794, "is_secret": false }, { @@ -1608,7 +1650,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 752, + "line_number": 806, "is_secret": false }, { @@ -1616,7 +1658,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 765, + "line_number": 819, "is_secret": false }, { @@ -1624,7 +1666,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 779, + "line_number": 833, "is_secret": false }, { @@ -1632,7 +1674,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 794, + "line_number": 848, "is_secret": false }, { @@ -1640,7 +1682,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 805, + "line_number": 859, "is_secret": false }, { @@ -1648,7 +1690,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 818, + "line_number": 872, "is_secret": false }, { @@ -1656,7 +1698,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 833, + "line_number": 887, "is_secret": false }, { @@ -1664,7 +1706,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 850, + "line_number": 904, "is_secret": false }, { @@ -1672,7 +1714,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", "is_verified": false, - "line_number": 875, + "line_number": 929, "is_secret": false }, { @@ -1680,7 +1722,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1205, + "line_number": 1259, "is_secret": false }, { @@ -1688,7 +1730,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1404, + "line_number": 1474, "is_secret": false } ], @@ -2263,5 +2305,5 @@ } ] }, - "generated_at": "2026-05-29T14:09:23Z" + "generated_at": "2026-06-19T07:55:45Z" } diff --git a/pkg/decoder/smartlabel/v1/decoder.go b/pkg/decoder/smartlabel/v1/decoder.go index 678ebe4..8cfff94 100644 --- a/pkg/decoder/smartlabel/v1/decoder.go +++ b/pkg/decoder/smartlabel/v1/decoder.go @@ -154,6 +154,92 @@ func (t SmartLabelv1Decoder) getConfig(port uint8, data string) (common.PayloadC TargetType: reflect.TypeOf(Port150Payload{}), Features: []decoder.Feature{}, }, nil + case 160: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Tag", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysFalse}, + {Name: "Rssi1", Start: 1, Length: 1}, + {Name: "Mac1", Start: 2, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 8, Length: 1, Optional: true}, + {Name: "Mac2", Start: 9, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 15, Length: 1, Optional: true}, + {Name: "Mac3", Start: 16, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 22, Length: 1, Optional: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 29, Length: 1, Optional: true}, + {Name: "Mac5", Start: 30, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 36, Length: 1, Optional: true}, + {Name: "Mac6", Start: 37, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port160Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving}, + }, nil + case 161: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Tag", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysTrue}, + {Name: "Rssi1", Start: 1, Length: 1}, + {Name: "Mac1", Start: 2, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 8, Length: 1, Optional: true}, + {Name: "Mac2", Start: 9, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 15, Length: 1, Optional: true}, + {Name: "Mac3", Start: 16, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 22, Length: 1, Optional: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 29, Length: 1, Optional: true}, + {Name: "Mac5", Start: 30, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 36, Length: 1, Optional: true}, + {Name: "Mac6", Start: 37, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port161Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving}, + }, nil + case 162: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Tag", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysFalse}, + {Name: "Rssi1", Start: 5, Length: 1}, + {Name: "Mac1", Start: 6, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 12, Length: 1, Optional: true}, + {Name: "Mac2", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 19, Length: 1, Optional: true}, + {Name: "Mac3", Start: 20, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 26, Length: 1, Optional: true}, + {Name: "Mac4", Start: 27, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 33, Length: 1, Optional: true}, + {Name: "Mac5", Start: 34, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 40, Length: 1, Optional: true}, + {Name: "Mac6", Start: 41, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port162Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving, decoder.FeatureTimestamp}, + }, nil + case 163: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Tag", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysTrue}, + {Name: "Rssi1", Start: 5, Length: 1}, + {Name: "Mac1", Start: 6, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 12, Length: 1, Optional: true}, + {Name: "Mac2", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 19, Length: 1, Optional: true}, + {Name: "Mac3", Start: 20, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 26, Length: 1, Optional: true}, + {Name: "Mac4", Start: 27, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 33, Length: 1, Optional: true}, + {Name: "Mac5", Start: 34, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 40, Length: 1, Optional: true}, + {Name: "Mac6", Start: 41, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port163Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving, decoder.FeatureTimestamp}, + }, nil case 197: return common.PayloadConfig{ Fields: []common.FieldConfig{ @@ -259,3 +345,11 @@ func ttf(v any) any { func pdop(v any) any { return float64(common.BytesToUint8(v.([]byte))) / 2 } + +func alwaysTrue(v any) any { + return true +} + +func alwaysFalse(v any) any { + return false +} diff --git a/pkg/decoder/smartlabel/v1/decoder_test.go b/pkg/decoder/smartlabel/v1/decoder_test.go index 743ad3f..3514c02 100644 --- a/pkg/decoder/smartlabel/v1/decoder_test.go +++ b/pkg/decoder/smartlabel/v1/decoder_test.go @@ -396,6 +396,60 @@ func TestDecode(t *testing.T) { Battery20Voltage: 2.990, }, }, + { + payload: "00d63385f8ee30c2d0a0382c2601db", + port: 160, + expected: Port160Payload{ + Tag: byte(0x00), + Moving: false, + Rssi1: -42, + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + }, + }, + { + payload: "64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + port: 161, + expected: Port161Payload{ + Tag: byte(0x64), + Moving: true, + Rssi1: -56, + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + }, + }, + { + payload: "68bae3ab00d63385f8ee30c2d0a0382c2601db", + port: 162, + expected: Port162Payload{ + Timestamp: time.Date(2025, 9, 5, 13, 20, 43, 0, time.UTC), + Tag: byte(0x00), + Moving: false, + Rssi1: -42, + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + }, + }, + { + payload: "68bae3ab64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + port: 163, + expected: Port163Payload{ + Timestamp: time.Date(2025, 9, 5, 13, 20, 43, 0, time.UTC), + Tag: byte(0x64), + Moving: true, + Rssi1: -56, + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + }, + }, { payload: "00d63385f8ee30c2d0a0382c2601db", port: 197, @@ -604,6 +658,22 @@ func TestFeatures(t *testing.T) { payload: "fdb7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900ba262b95a6ffc86", port: 197, }, + { + payload: "00d63385f8ee30c2d0a0382c2601db", + port: 160, + }, + { + payload: "64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + port: 161, + }, + { + payload: "68bae3ab00d63385f8ee30c2d0a0382c2601db", + port: 162, + }, + { + payload: "68bae3ab64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + port: 163, + }, { payload: "0002d30b070082491f11256718d9fe0ede190505", port: 10, @@ -710,6 +780,15 @@ func TestFeatures(t *testing.T) { t.Fatalf("expected non nil access points") } } + if decodedPayload.Is(decoder.FeatureBle) { + ble, ok := decodedPayload.Data.(decoder.UplinkFeatureBle) + if !ok { + t.Fatalf("expected UplinkFeatureBle, got %T", decodedPayload) + } + if ble.GetBeacons() == nil { + t.Fatalf("expected non nil beacons") + } + } if decodedPayload.Is(decoder.FeaturePhotovoltaic) { photovoltaicVoltage, ok := decodedPayload.Data.(decoder.UplinkFeaturePhotovoltaic) if !ok { diff --git a/pkg/decoder/smartlabel/v1/port160.go b/pkg/decoder/smartlabel/v1/port160.go new file mode 100644 index 0000000..8f8b50d --- /dev/null +++ b/pkg/decoder/smartlabel/v1/port160.go @@ -0,0 +1,98 @@ +package smartlabel + +import ( + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 160 - BLE steady (no timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 1 | 1 | rssi beacon 1 | int8 | +// | 2 | 6 | mac address beacon 1 | byte[6] | +// | 8 | 1 | rssi beacon 2 | int8 | +// | 9 | 6 | mac address beacon 2 | byte[6] | +// | 15 | 1 | rssi beacon 3 | int8 | +// | 16 | 6 | mac address beacon 3 | byte[6] | +// | 22 | 1 | rssi beacon 4 | int8 | +// | 23 | 6 | mac address beacon 4 | byte[6] | +// | 29 | 1 | rssi beacon 5 | int8 | +// | 30 | 6 | mac address beacon 5 | byte[6] | +// | 36 | 1 | rssi beacon 6 | int8 | +// | 37 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port160Payload struct { + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always false for port 160 (steady) + Mac1 string `json:"mac1"` + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` +} + +var _ decoder.UplinkFeatureBle = &Port160Payload{} +var _ decoder.UplinkFeatureMoving = &Port160Payload{} + +func (p Port160Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" && p.Rssi1 != 0 { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil && p.Rssi2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil && p.Rssi3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil && p.Rssi4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil && p.Rssi5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil && p.Rssi6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 160 reports a steady (non-moving) state. +func (p Port160Payload) IsMoving() bool { + return false +} diff --git a/pkg/decoder/smartlabel/v1/port161.go b/pkg/decoder/smartlabel/v1/port161.go new file mode 100644 index 0000000..7e301f4 --- /dev/null +++ b/pkg/decoder/smartlabel/v1/port161.go @@ -0,0 +1,98 @@ +package smartlabel + +import ( + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 161 - BLE moving (no timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 1 | 1 | rssi beacon 1 | int8 | +// | 2 | 6 | mac address beacon 1 | byte[6] | +// | 8 | 1 | rssi beacon 2 | int8 | +// | 9 | 6 | mac address beacon 2 | byte[6] | +// | 15 | 1 | rssi beacon 3 | int8 | +// | 16 | 6 | mac address beacon 3 | byte[6] | +// | 22 | 1 | rssi beacon 4 | int8 | +// | 23 | 6 | mac address beacon 4 | byte[6] | +// | 29 | 1 | rssi beacon 5 | int8 | +// | 30 | 6 | mac address beacon 5 | byte[6] | +// | 36 | 1 | rssi beacon 6 | int8 | +// | 37 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port161Payload struct { + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always true for port 161 (moving) + Mac1 string `json:"mac1"` + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` +} + +var _ decoder.UplinkFeatureBle = &Port161Payload{} +var _ decoder.UplinkFeatureMoving = &Port161Payload{} + +func (p Port161Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" && p.Rssi1 != 0 { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil && p.Rssi2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil && p.Rssi3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil && p.Rssi4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil && p.Rssi5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil && p.Rssi6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 161 reports a moving state. +func (p Port161Payload) IsMoving() bool { + return true +} diff --git a/pkg/decoder/smartlabel/v1/port162.go b/pkg/decoder/smartlabel/v1/port162.go new file mode 100644 index 0000000..f6eaecc --- /dev/null +++ b/pkg/decoder/smartlabel/v1/port162.go @@ -0,0 +1,107 @@ +package smartlabel + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 162 - BLE steady (with timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 5 | 1 | rssi beacon 1 | int8 | +// | 6 | 6 | mac address beacon 1 | byte[6] | +// | 12 | 1 | rssi beacon 2 | int8 | +// | 13 | 6 | mac address beacon 2 | byte[6] | +// | 19 | 1 | rssi beacon 3 | int8 | +// | 20 | 6 | mac address beacon 3 | byte[6] | +// | 26 | 1 | rssi beacon 4 | int8 | +// | 27 | 6 | mac address beacon 4 | byte[6] | +// | 33 | 1 | rssi beacon 5 | int8 | +// | 34 | 6 | mac address beacon 5 | byte[6] | +// | 40 | 1 | rssi beacon 6 | int8 | +// | 41 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port162Payload struct { + Timestamp time.Time `json:"timestamp"` + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always false for port 162 (steady) + Mac1 string `json:"mac1"` + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` +} + +var _ decoder.UplinkFeatureBle = &Port162Payload{} +var _ decoder.UplinkFeatureMoving = &Port162Payload{} +var _ decoder.UplinkFeatureTimestamp = &Port162Payload{} + +func (p Port162Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port162Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" && p.Rssi1 != 0 { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil && p.Rssi2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil && p.Rssi3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil && p.Rssi4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil && p.Rssi5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil && p.Rssi6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 162 reports a steady (non-moving) state. +func (p Port162Payload) IsMoving() bool { + return false +} diff --git a/pkg/decoder/smartlabel/v1/port163.go b/pkg/decoder/smartlabel/v1/port163.go new file mode 100644 index 0000000..fb2a65f --- /dev/null +++ b/pkg/decoder/smartlabel/v1/port163.go @@ -0,0 +1,107 @@ +package smartlabel + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 163 - BLE moving (with timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 5 | 1 | rssi beacon 1 | int8 | +// | 6 | 6 | mac address beacon 1 | byte[6] | +// | 12 | 1 | rssi beacon 2 | int8 | +// | 13 | 6 | mac address beacon 2 | byte[6] | +// | 19 | 1 | rssi beacon 3 | int8 | +// | 20 | 6 | mac address beacon 3 | byte[6] | +// | 26 | 1 | rssi beacon 4 | int8 | +// | 27 | 6 | mac address beacon 4 | byte[6] | +// | 33 | 1 | rssi beacon 5 | int8 | +// | 34 | 6 | mac address beacon 5 | byte[6] | +// | 40 | 1 | rssi beacon 6 | int8 | +// | 41 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port163Payload struct { + Timestamp time.Time `json:"timestamp"` + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always true for port 163 (moving) + Mac1 string `json:"mac1"` + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` +} + +var _ decoder.UplinkFeatureBle = &Port163Payload{} +var _ decoder.UplinkFeatureMoving = &Port163Payload{} +var _ decoder.UplinkFeatureTimestamp = &Port163Payload{} + +func (p Port163Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port163Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" && p.Rssi1 != 0 { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil && p.Rssi2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil && p.Rssi3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil && p.Rssi4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil && p.Rssi5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil && p.Rssi6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 163 reports a moving state. +func (p Port163Payload) IsMoving() bool { + return true +} diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index 8674396..8b02b7e 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -194,6 +194,92 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon default: return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } + case 160: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Tag", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysFalse}, + {Name: "Rssi1", Start: 1, Length: 1}, + {Name: "Mac1", Start: 2, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 8, Length: 1, Optional: true}, + {Name: "Mac2", Start: 9, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 15, Length: 1, Optional: true}, + {Name: "Mac3", Start: 16, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 22, Length: 1, Optional: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 29, Length: 1, Optional: true}, + {Name: "Mac5", Start: 30, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 36, Length: 1, Optional: true}, + {Name: "Mac6", Start: 37, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port160Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving}, + }, nil + case 161: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Tag", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysTrue}, + {Name: "Rssi1", Start: 1, Length: 1}, + {Name: "Mac1", Start: 2, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 8, Length: 1, Optional: true}, + {Name: "Mac2", Start: 9, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 15, Length: 1, Optional: true}, + {Name: "Mac3", Start: 16, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 22, Length: 1, Optional: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 29, Length: 1, Optional: true}, + {Name: "Mac5", Start: 30, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 36, Length: 1, Optional: true}, + {Name: "Mac6", Start: 37, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port161Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving}, + }, nil + case 162: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Tag", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysFalse}, + {Name: "Rssi1", Start: 5, Length: 1}, + {Name: "Mac1", Start: 6, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 12, Length: 1, Optional: true}, + {Name: "Mac2", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 19, Length: 1, Optional: true}, + {Name: "Mac3", Start: 20, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 26, Length: 1, Optional: true}, + {Name: "Mac4", Start: 27, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 33, Length: 1, Optional: true}, + {Name: "Mac5", Start: 34, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 40, Length: 1, Optional: true}, + {Name: "Mac6", Start: 41, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port162Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving, decoder.FeatureTimestamp, decoder.FeatureBuffered}, + }, nil + case 163: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Tag", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysTrue}, + {Name: "Rssi1", Start: 5, Length: 1}, + {Name: "Mac1", Start: 6, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 12, Length: 1, Optional: true}, + {Name: "Mac2", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 19, Length: 1, Optional: true}, + {Name: "Mac3", Start: 20, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 26, Length: 1, Optional: true}, + {Name: "Mac4", Start: 27, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 33, Length: 1, Optional: true}, + {Name: "Mac5", Start: 34, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi6", Start: 40, Length: 1, Optional: true}, + {Name: "Mac6", Start: 41, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port163Payload{}), + Features: []decoder.Feature{decoder.FeatureBle, decoder.FeatureMoving, decoder.FeatureTimestamp, decoder.FeatureBuffered}, + }, nil case 197: if len(payload) < 1 { return common.PayloadConfig{}, common.ErrPayloadTooShort diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index 6acad35..61d7953 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -428,6 +428,60 @@ func TestDecode(t *testing.T) { payload: "68bad3c58aab4581b9e73a0eb580da120d7f85a75e770c6acad3dc2acdacbdcd576ab8147f5902557379b18d0f676a35fb9a6ae5ee03", expected: &exampleResponse, }, + { + port: 160, + payload: "00d63385f8ee30c2d0a0382c2601db", + expected: Port160Payload{ + Tag: byte(0x00), + Moving: false, + Rssi1: -42, + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + }, + }, + { + port: 161, + payload: "64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + expected: Port161Payload{ + Tag: byte(0x64), + Moving: true, + Rssi1: -56, + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + }, + }, + { + port: 162, + payload: "68bae3ab00d63385f8ee30c2d0a0382c2601db", + expected: Port162Payload{ + Timestamp: time.Date(2025, 9, 5, 13, 20, 43, 0, time.UTC), + Tag: byte(0x00), + Moving: false, + Rssi1: -42, + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + }, + }, + { + port: 163, + payload: "68bae3ab64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + expected: Port163Payload{ + Timestamp: time.Date(2025, 9, 5, 13, 20, 43, 0, time.UTC), + Tag: byte(0x64), + Moving: true, + Rssi1: -56, + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + }, + }, { port: 197, payload: "ff", @@ -1393,6 +1447,22 @@ func TestFeatures(t *testing.T) { payload: "68b9ac2101b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", port: 213, }, + { + payload: "00d63385f8ee30c2d0a0382c2601db", + port: 160, + }, + { + payload: "64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + port: 161, + }, + { + payload: "68bae3ab00d63385f8ee30c2d0a0382c2601db", + port: 162, + }, + { + payload: "68bae3ab64c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + port: 163, + }, } mux := http.NewServeMux() @@ -1498,6 +1568,15 @@ func TestFeatures(t *testing.T) { t.Fatalf("expected non nil access points") } } + if decodedPayload.Is(decoder.FeatureBle) { + ble, ok := decodedPayload.Data.(decoder.UplinkFeatureBle) + if !ok { + t.Fatalf("expected UplinkFeatureBle, got %T", decodedPayload) + } + if ble.GetBeacons() == nil { + t.Fatalf("expected non nil beacons") + } + } if decodedPayload.Is(decoder.FeatureMoving) { moving, ok := decodedPayload.Data.(decoder.UplinkFeatureMoving) if !ok { diff --git a/pkg/decoder/tagxl/v1/port160.go b/pkg/decoder/tagxl/v1/port160.go new file mode 100644 index 0000000..39c689a --- /dev/null +++ b/pkg/decoder/tagxl/v1/port160.go @@ -0,0 +1,98 @@ +package tagxl + +import ( + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 160 - BLE steady (no timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 1 | 1 | rssi beacon 1 | int8 | +// | 2 | 6 | mac address beacon 1 | byte[6] | +// | 8 | 1 | rssi beacon 2 | int8 | +// | 9 | 6 | mac address beacon 2 | byte[6] | +// | 15 | 1 | rssi beacon 3 | int8 | +// | 16 | 6 | mac address beacon 3 | byte[6] | +// | 22 | 1 | rssi beacon 4 | int8 | +// | 23 | 6 | mac address beacon 4 | byte[6] | +// | 29 | 1 | rssi beacon 5 | int8 | +// | 30 | 6 | mac address beacon 5 | byte[6] | +// | 36 | 1 | rssi beacon 6 | int8 | +// | 37 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port160Payload struct { + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always false for port 160 (steady) + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` +} + +var _ decoder.UplinkFeatureBle = &Port160Payload{} +var _ decoder.UplinkFeatureMoving = &Port160Payload{} + +func (p Port160Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 160 reports a steady (non-moving) state. +func (p Port160Payload) IsMoving() bool { + return false +} diff --git a/pkg/decoder/tagxl/v1/port161.go b/pkg/decoder/tagxl/v1/port161.go new file mode 100644 index 0000000..56e456d --- /dev/null +++ b/pkg/decoder/tagxl/v1/port161.go @@ -0,0 +1,98 @@ +package tagxl + +import ( + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 161 - BLE moving (no timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 1 | 1 | rssi beacon 1 | int8 | +// | 2 | 6 | mac address beacon 1 | byte[6] | +// | 8 | 1 | rssi beacon 2 | int8 | +// | 9 | 6 | mac address beacon 2 | byte[6] | +// | 15 | 1 | rssi beacon 3 | int8 | +// | 16 | 6 | mac address beacon 3 | byte[6] | +// | 22 | 1 | rssi beacon 4 | int8 | +// | 23 | 6 | mac address beacon 4 | byte[6] | +// | 29 | 1 | rssi beacon 5 | int8 | +// | 30 | 6 | mac address beacon 5 | byte[6] | +// | 36 | 1 | rssi beacon 6 | int8 | +// | 37 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port161Payload struct { + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always true for port 161 (moving) + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` +} + +var _ decoder.UplinkFeatureBle = &Port161Payload{} +var _ decoder.UplinkFeatureMoving = &Port161Payload{} + +func (p Port161Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 161 reports a moving state. +func (p Port161Payload) IsMoving() bool { + return true +} diff --git a/pkg/decoder/tagxl/v1/port162.go b/pkg/decoder/tagxl/v1/port162.go new file mode 100644 index 0000000..a79d942 --- /dev/null +++ b/pkg/decoder/tagxl/v1/port162.go @@ -0,0 +1,116 @@ +package tagxl + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 162 - BLE steady (with timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 5 | 1 | rssi beacon 1 | int8 | +// | 6 | 6 | mac address beacon 1 | byte[6] | +// | 12 | 1 | rssi beacon 2 | int8 | +// | 13 | 6 | mac address beacon 2 | byte[6] | +// | 19 | 1 | rssi beacon 3 | int8 | +// | 20 | 6 | mac address beacon 3 | byte[6] | +// | 26 | 1 | rssi beacon 4 | int8 | +// | 27 | 6 | mac address beacon 4 | byte[6] | +// | 33 | 1 | rssi beacon 5 | int8 | +// | 34 | 6 | mac address beacon 5 | byte[6] | +// | 40 | 1 | rssi beacon 6 | int8 | +// | 41 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port162Payload struct { + Timestamp time.Time `json:"timestamp"` + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always false for port 162 (steady) + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` +} + +var _ decoder.UplinkFeatureBle = &Port162Payload{} +var _ decoder.UplinkFeatureMoving = &Port162Payload{} +var _ decoder.UplinkFeatureTimestamp = &Port162Payload{} +var _ decoder.UplinkFeatureBuffered = &Port162Payload{} + +func (p Port162Payload) GetBufferLevel() *uint16 { + return nil +} + +func (p Port162Payload) IsBuffered() bool { + return time.Since(p.Timestamp) > bufferedAgeThreshold +} + +func (p Port162Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port162Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 162 reports a steady (non-moving) state. +func (p Port162Payload) IsMoving() bool { + return false +} diff --git a/pkg/decoder/tagxl/v1/port163.go b/pkg/decoder/tagxl/v1/port163.go new file mode 100644 index 0000000..a3f228b --- /dev/null +++ b/pkg/decoder/tagxl/v1/port163.go @@ -0,0 +1,116 @@ +package tagxl + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// Port 163 - BLE moving (with timestamp) +// +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | tag (movement / timestamp / sequence flags) | byte | +// | 5 | 1 | rssi beacon 1 | int8 | +// | 6 | 6 | mac address beacon 1 | byte[6] | +// | 12 | 1 | rssi beacon 2 | int8 | +// | 13 | 6 | mac address beacon 2 | byte[6] | +// | 19 | 1 | rssi beacon 3 | int8 | +// | 20 | 6 | mac address beacon 3 | byte[6] | +// | 26 | 1 | rssi beacon 4 | int8 | +// | 27 | 6 | mac address beacon 4 | byte[6] | +// | 33 | 1 | rssi beacon 5 | int8 | +// | 34 | 6 | mac address beacon 5 | byte[6] | +// | 40 | 1 | rssi beacon 6 | int8 | +// | 41 | 6 | mac address beacon 6 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +type Port163Payload struct { + Timestamp time.Time `json:"timestamp"` + Tag byte `json:"tag"` + Moving bool `json:"moving"` // Always true for port 163 (moving) + Rssi1 int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` + Rssi6 *int8 `json:"rssi6" validate:"gte=-120,lte=-20"` + Mac6 *string `json:"mac6"` +} + +var _ decoder.UplinkFeatureBle = &Port163Payload{} +var _ decoder.UplinkFeatureMoving = &Port163Payload{} +var _ decoder.UplinkFeatureTimestamp = &Port163Payload{} +var _ decoder.UplinkFeatureBuffered = &Port163Payload{} + +func (p Port163Payload) GetBufferLevel() *uint16 { + return nil +} + +func (p Port163Payload) IsBuffered() bool { + return time.Since(p.Timestamp) > bufferedAgeThreshold +} + +func (p Port163Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port163Payload) GetBeacons() []decoder.Beacon { + beacons := []decoder.Beacon{} + + if p.Mac1 != "" { + beacons = append(beacons, decoder.Beacon{ + MAC: p.Mac1, + RSSI: &p.Rssi1, + }) + } + + if p.Mac2 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + if p.Mac6 != nil { + beacons = append(beacons, decoder.Beacon{ + MAC: *p.Mac6, + RSSI: p.Rssi6, + }) + } + + return beacons +} + +// Port 163 reports a moving state. +func (p Port163Payload) IsMoving() bool { + return true +} From e85c972de1499b1ce54b3249e929dd396add3e7c Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:12:22 +0200 Subject: [PATCH 2/2] test(ble): assert non-empty beacons for BLE ports Strengthen the FeatureBle assertions in the smartlabel and tagxl feature tests to require at least one decoded beacon instead of a non-nil slice, catching empty-decode regressions. --- pkg/decoder/smartlabel/v1/decoder_test.go | 4 ++-- pkg/decoder/tagxl/v1/decoder_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/decoder/smartlabel/v1/decoder_test.go b/pkg/decoder/smartlabel/v1/decoder_test.go index 3514c02..d3657ae 100644 --- a/pkg/decoder/smartlabel/v1/decoder_test.go +++ b/pkg/decoder/smartlabel/v1/decoder_test.go @@ -785,8 +785,8 @@ func TestFeatures(t *testing.T) { if !ok { t.Fatalf("expected UplinkFeatureBle, got %T", decodedPayload) } - if ble.GetBeacons() == nil { - t.Fatalf("expected non nil beacons") + if len(ble.GetBeacons()) == 0 { + t.Fatalf("expected at least one beacon") } } if decodedPayload.Is(decoder.FeaturePhotovoltaic) { diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index 61d7953..b4a2954 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -1573,8 +1573,8 @@ func TestFeatures(t *testing.T) { if !ok { t.Fatalf("expected UplinkFeatureBle, got %T", decodedPayload) } - if ble.GetBeacons() == nil { - t.Fatalf("expected non nil beacons") + if len(ble.GetBeacons()) == 0 { + t.Fatalf("expected at least one beacon") } } if decodedPayload.Is(decoder.FeatureMoving) {