Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/decoder/tagxl/v1/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon
{Name: "TTF", Start: 17, Length: 1, Transform: ttf},
{Name: "PDOP", Start: 18, Length: 1, Transform: pdop},
{Name: "Satellites", Start: 19, Length: 1},
// optional: older firmware sends 20 bytes without hAcc and speed
{Name: "Accuracy", Start: 20, Length: 1, Optional: true, Transform: func(v any) any {
return float64(v.([]byte)[0])
}},
{Name: "Speed", Start: 21, Length: 1, Optional: true, Transform: func(v any) any {
return float64(v.([]byte)[0])
}},
},
TargetType: reflect.TypeOf(Port10Payload{}),
Features: []decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp, decoder.FeatureBattery, decoder.FeatureMoving},
Expand Down
19 changes: 18 additions & 1 deletion pkg/decoder/tagxl/v1/port10.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import (
// | 17 | 1 | Time to fix | uint8, s |
// | 18 | 1 | Position dilution of precision | uint8, 1/2 meter |
// | 19 | 1 | Number of satellites | uint8 |
// | 20 | 1 | Horizontal accuracy estimate (hAcc) | uint8, m (optional) |
// | 21 | 1 | Speed over ground | uint8, km/h (optional) |
// +------+------+-------------------------------------------+------------------------+
// Bytes 20-21 were added later; 20-byte uplinks from older firmware leave them nil.

type Port10Payload struct {
Moving bool `json:"moving"`
Expand All @@ -35,6 +38,8 @@ type Port10Payload struct {
TTF *time.Duration `json:"ttf"`
PDOP *float64 `json:"pdop"`
Satellites *uint8 `json:"satellites" validate:"gte=3,lte=27"`
Accuracy *float64 `json:"accuracy"`
Speed *float64 `json:"speed"`
}

func (p Port10Payload) MarshalJSON() ([]byte, error) {
Expand All @@ -47,6 +52,14 @@ func (p Port10Payload) MarshalJSON() ([]byte, error) {
if p.PDOP != nil {
pdop = common.StringPtr(fmt.Sprintf("%.1fm", *p.PDOP))
}
var accuracy *string
if p.Accuracy != nil {
accuracy = common.StringPtr(fmt.Sprintf("%.0fm", *p.Accuracy))
}
var speed *string
if p.Speed != nil {
speed = common.StringPtr(fmt.Sprintf("%.0fkm/h", *p.Speed))
}
return json.Marshal(&struct {
*Alias
Altitude string `json:"altitude"`
Expand All @@ -55,6 +68,8 @@ func (p Port10Payload) MarshalJSON() ([]byte, error) {
TTF *string `json:"ttf"`
PDOP *string `json:"pdop"`
Satellites *uint8 `json:"satellites"`
Accuracy *string `json:"accuracy"`
Speed *string `json:"speed"`
}{
Alias: (*Alias)(&p),
Altitude: fmt.Sprintf("%.1fm", p.Altitude),
Expand All @@ -63,6 +78,8 @@ func (p Port10Payload) MarshalJSON() ([]byte, error) {
TTF: ttf,
PDOP: pdop,
Satellites: p.Satellites,
Accuracy: accuracy,
Speed: speed,
})
}

Expand All @@ -88,7 +105,7 @@ func (p Port10Payload) GetAltitude() float64 {
}

func (p Port10Payload) GetAccuracy() *float64 {
return nil
return p.Accuracy
}

func (p Port10Payload) GetTTF() *time.Duration {
Expand Down
68 changes: 68 additions & 0 deletions pkg/decoder/tagxl/v1/port10_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package tagxl

import (
"context"
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/truvami/decoder/internal/logger"
"github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/solver"
)

func TestPort10Payload_GNSSAndBatteryMethods(t *testing.T) {
Expand Down Expand Up @@ -62,3 +68,65 @@ func TestPort10Payload_GNSSAndBatteryMethods(t *testing.T) {
t.Fatalf("IsMoving() = %v, want true", got)
}
}

func TestPort10_OptionalAccuracyAndSpeed(t *testing.T) {
t.Parallel()

// 20-byte uplink from older firmware, then with hAcc (12 m) and speed over ground (34 km/h) appended
base := "0002d30b070082491f11256718d9fe0ede190505"
tests := []struct {
name string
payload string
accuracy *float64
speed *float64
}{
{name: "20 bytes, older firmware", payload: base},
{name: "21 bytes, hAcc only", payload: base + "0c", accuracy: common.Float64Ptr(12)},
{name: "22 bytes, hAcc and speed", payload: base + "0c22", accuracy: common.Float64Ptr(12), speed: common.Float64Ptr(34)},
{name: "22 bytes, hAcc capped at 255, standing still", payload: base + "ff00", accuracy: common.Float64Ptr(255), speed: common.Float64Ptr(0)},
}

dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, logger.Logger)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
decoded, err := dec.Decode(context.TODO(), tt.payload, 10)
require.NoError(t, err)
data, ok := decoded.Data.(Port10Payload)
require.True(t, ok, "unexpected payload type %T", decoded.Data)

// the existing fields decode the same regardless of length
assert.Equal(t, 47.385351, data.Latitude)
assert.Equal(t, common.Uint8Ptr(5), data.Satellites)

assert.Equal(t, tt.accuracy, data.Accuracy)
assert.Equal(t, tt.accuracy, data.GetAccuracy())
assert.Equal(t, tt.speed, data.Speed)
})
}
}

func TestPort10_AccuracyAndSpeedJSON(t *testing.T) {
t.Parallel()

p := Port10Payload{Accuracy: common.Float64Ptr(12), Speed: common.Float64Ptr(34)}
encoded, err := json.Marshal(p)
require.NoError(t, err)
assert.Contains(t, string(encoded), `"accuracy":"12m"`)
assert.Contains(t, string(encoded), `"speed":"34km/h"`)

encoded, err = json.Marshal(Port10Payload{})
require.NoError(t, err)
assert.Contains(t, string(encoded), `"accuracy":null`)
assert.Contains(t, string(encoded), `"speed":null`)
}

func TestPort10_RejectsUnknownTrailingBytes(t *testing.T) {
t.Parallel()

// the maximum length is derived from the configured fields: a byte the decoder doesn't
// know yet is rejected, so the decoder must be updated before firmware appends more
dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, logger.Logger)
_, err := dec.Decode(context.TODO(), "0002d30b070082491f11256718d9fe0ede1905050c22ff", 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "payload too long")
}
15 changes: 14 additions & 1 deletion pkg/decoder/tagxl/v1/port151.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import (
// | | | gps hdop threshold (0 = disabled) | uint16, x10|
// | | | gps vdop threshold (0 = disabled) | uint16, x10|
// | | | gps pdop threshold (0 = disabled) | uint16, x10|
// | d2 | 2 | gps constellation | uint8 |
// | d2 | 2-5 | gps constellation | uint8 |
// | | | 0: default (GPS+QZSS+SBAS+Galileo+BeiDou) | |
// | | | 1: GPS (+QZSS+SBAS) | |
// | | | 2: GPS+Galileo | |
Expand All @@ -64,6 +64,13 @@ import (
// | | | 6: GPS+Galileo+GLONASS | |
// | | | 7: GPS+Galileo+GLONASS+BeiDou | |
// | | | gps power mode (0 = full power, 1 = low power) | uint8 |
// | | | gps dynamic model (u-blox CFG-NAVSPG-DYNMODEL) | uint8 |
// | | | 0: portable, 2: stationary, 3: pedestrian, | |
// | | | 4: automotive, 5: sea, 6-8: airborne <1/2/4g,| |
// | | | 9: wrist, 10: bike | |
// | | | gps static hold speed (0 = off) | uint8, cm/s|
// | | | gps static hold exit distance | uint8, m |
// | | | (older ts2 firmware sends only the first 2) | |
// +-----+------+------------------------------------------------+------------+

const (
Expand Down Expand Up @@ -155,6 +162,9 @@ type Port151Payload struct {
GpsPdopThreshold *float32 `json:"gpsPdopThreshold"`
GpsConstellation *uint8 `json:"gpsConstellation"`
GpsPowerMode *uint8 `json:"gpsPowerMode"`
GpsDynamicModel *uint8 `json:"gpsDynamicModel"`
GpsStaticHoldSpeed *uint8 `json:"gpsStaticHoldSpeed"`
GpsStaticHoldDistance *uint8 `json:"gpsStaticHoldDistance"`
}

var _ decoder.UplinkFeatureBattery = &Port151Payload{}
Expand Down Expand Up @@ -379,6 +389,9 @@ func port151PayloadConfig() common.PayloadConfig {
{Name: "GpsPdopThreshold", Tag: tlvTagGpsConfig, Optional: true, Feature: decoder.FeatureConfig, Transform: dopAt(8)},
{Name: "GpsConstellation", Tag: tlvTagGpsConstellation, Optional: true, Feature: decoder.FeatureConfig, Transform: uint8At(0)},
{Name: "GpsPowerMode", Tag: tlvTagGpsConstellation, Optional: true, Feature: decoder.FeatureConfig, Transform: uint8At(1)},
{Name: "GpsDynamicModel", Tag: tlvTagGpsConstellation, Optional: true, Feature: decoder.FeatureConfig, Transform: uint8At(2)},
{Name: "GpsStaticHoldSpeed", Tag: tlvTagGpsConstellation, Optional: true, Feature: decoder.FeatureConfig, Transform: uint8At(3)},
{Name: "GpsStaticHoldDistance", Tag: tlvTagGpsConstellation, Optional: true, Feature: decoder.FeatureConfig, Transform: uint8At(4)},
},
TargetType: reflect.TypeOf(Port151Payload{}),
Features: []decoder.Feature{decoder.FeatureDataRate},
Expand Down
38 changes: 38 additions & 0 deletions pkg/decoder/tagxl/v1/port151_lte_gnss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ func TestPort151_GpsSettings(t *testing.T) {
assert.Equal(t, common.Float32Ptr(0), data.GpsHdopThreshold)
assert.Equal(t, common.Float32Ptr(0), data.GpsVdopThreshold)
assert.Equal(t, common.Float32Ptr(3), data.GpsPdopThreshold)

// 2-byte 0xD2 from older ts2 firmware: no dynamic model / static hold
assert.Nil(t, data.GpsDynamicModel)
assert.Nil(t, data.GpsStaticHoldSpeed)
assert.Nil(t, data.GpsStaticHoldDistance)
}

func TestPort151_GpsReceiverConfig(t *testing.T) {
t.Parallel()

// 5-byte 0xD2: all constellations, full power, automotive, static hold < 50 cm/s, exit after 10 m
decoded, data := decodePort151(t, "4c0701d205070004320a")

assert.True(t, decoded.Is(decoder.FeatureConfig))
assert.Equal(t, common.Uint8Ptr(7), data.GpsConstellation)
assert.Equal(t, common.Uint8Ptr(0), data.GpsPowerMode)
assert.Equal(t, common.Uint8Ptr(4), data.GpsDynamicModel)
assert.Equal(t, common.Uint8Ptr(50), data.GpsStaticHoldSpeed)
assert.Equal(t, common.Uint8Ptr(10), data.GpsStaticHoldDistance)
}

func TestPort151_LteSettings(t *testing.T) {
Expand Down Expand Up @@ -99,3 +118,22 @@ func TestPort151_LteGpsNullWhenNotReported(t *testing.T) {
assert.Contains(t, string(encoded), `"gpsConstellation":null`)
assert.Contains(t, string(encoded), `"lteApn":null`)
}

func TestPort151_GpsReceiverConfigPartialAndExtended(t *testing.T) {
t.Parallel()

// 3-byte 0xD2: dynamic model only, static hold fields not reported
_, data := decodePort151(t, "4c0501d203020104")
assert.Equal(t, common.Uint8Ptr(2), data.GpsConstellation)
assert.Equal(t, common.Uint8Ptr(1), data.GpsPowerMode)
assert.Equal(t, common.Uint8Ptr(4), data.GpsDynamicModel)
assert.Nil(t, data.GpsStaticHoldSpeed)
assert.Nil(t, data.GpsStaticHoldDistance)

// 6-byte 0xD2: a byte from future firmware is ignored, known fields still decode
_, data = decodePort151(t, "4c0801d206070004320aff")
assert.Equal(t, common.Uint8Ptr(7), data.GpsConstellation)
assert.Equal(t, common.Uint8Ptr(4), data.GpsDynamicModel)
assert.Equal(t, common.Uint8Ptr(50), data.GpsStaticHoldSpeed)
assert.Equal(t, common.Uint8Ptr(10), data.GpsStaticHoldDistance)
}
Loading