diff --git a/.secrets.baseline b/.secrets.baseline index 42f6749..c5b20a3 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -309,7 +309,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "2969e12a864a2091be4082d99c1767a6d225ef9f", "is_verified": false, - "line_number": 170, + "line_number": 171, "is_secret": false }, { @@ -317,7 +317,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "18911f680f30a3d441a7314a5f4131ccdf5a291d", "is_verified": false, - "line_number": 193, + "line_number": 194, "is_secret": false }, { @@ -325,15 +325,43 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "03799cbbbde9a26987fc61ee9d1c7607efa5b6c3", "is_verified": false, - "line_number": 216, + "line_number": 217, "is_secret": false }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "5834801389e8dc5649da5afb03daa5984c109656", + "is_verified": false, + "line_number": 241 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "728a3e88a062cfba092183aca6fe9b07841fd501", + "is_verified": false, + "line_number": 256 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "b5950857a05765aaec2390c6fb6e6d178fde38ec", + "is_verified": false, + "line_number": 271 + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "88d19da2a0e9309b61e89836d80f63274d9faeea", + "is_verified": false, + "line_number": 286 + }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "d85e5644e91feb126f202af26abefd4aadde571e", "is_verified": false, - "line_number": 239, + "line_number": 307, "is_secret": false }, { @@ -341,7 +369,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "9db4104f44fd40f26bce7072ad15bc10f1e833d6", "is_verified": false, - "line_number": 259, + "line_number": 327, "is_secret": false }, { @@ -349,7 +377,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "517accb9519da7da9ae0f3ee9eac83543169ee17", "is_verified": false, - "line_number": 269, + "line_number": 337, "is_secret": false }, { @@ -357,7 +385,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", "is_verified": false, - "line_number": 279, + "line_number": 347, "is_secret": false }, { @@ -365,7 +393,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "07cb4aff970c7271688f1e8eaf214a516d3970e3", "is_verified": false, - "line_number": 299, + "line_number": 367, "is_secret": false }, { @@ -373,7 +401,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "bc4afa2f3630480ca72a32c5c8421b39f3ce1a71", "is_verified": false, - "line_number": 332, + "line_number": 400, "is_secret": false }, { @@ -381,7 +409,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "395900609a0d94e6b75d0f0cb6b647d1d554c442", "is_verified": false, - "line_number": 343, + "line_number": 411, "is_secret": false }, { @@ -389,7 +417,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "81de30f2e09afabf90f74a4addd1ddbc354277d4", "is_verified": false, - "line_number": 356, + "line_number": 424, "is_secret": false }, { @@ -397,7 +425,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "a158f37d59e2f1ea505e92294149581814a5ffe3", "is_verified": false, - "line_number": 373, + "line_number": 441, "is_secret": false }, { @@ -405,7 +433,7 @@ "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 529, + "line_number": 619, "is_secret": false } ], @@ -2235,5 +2263,5 @@ } ] }, - "generated_at": "2026-04-28T18:24:36Z" + "generated_at": "2026-05-29T14:09:23Z" } diff --git a/pkg/decoder/smartlabel/v1/decoder.go b/pkg/decoder/smartlabel/v1/decoder.go index ba38dee..678ebe4 100644 --- a/pkg/decoder/smartlabel/v1/decoder.go +++ b/pkg/decoder/smartlabel/v1/decoder.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "time" "github.com/truvami/decoder/pkg/common" "github.com/truvami/decoder/pkg/decoder" @@ -104,6 +105,22 @@ func (t SmartLabelv1Decoder) getConfig(port uint8, data string) (common.PayloadC TargetType: reflect.TypeOf(Port4Payload{}), Features: []decoder.Feature{decoder.FeatureConfig, decoder.FeatureFirmwareVersion}, }, nil + case 10: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Status", Start: 0, Length: 1}, + {Name: "Latitude", Start: 1, Length: 4, Transform: latitude}, + {Name: "Longitude", Start: 5, Length: 4, Transform: longitude}, + {Name: "Altitude", Start: 9, Length: 2, Transform: port10Altitude}, + {Name: "Timestamp", Start: 11, Length: 4, Transform: timestamp}, + {Name: "Battery", Start: 15, Length: 2, Transform: gnssBattery}, + {Name: "TTF", Start: 17, Length: 1, Transform: ttf}, + {Name: "PDOP", Start: 18, Length: 1, Transform: pdop}, + {Name: "Satellites", Start: 19, Length: 1}, + }, + TargetType: reflect.TypeOf(Port10Payload{}), + Features: []decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp, decoder.FeatureBattery}, + }, nil case 11: return common.PayloadConfig{ Fields: []common.FieldConfig{ @@ -214,3 +231,31 @@ func temperature(v any) any { func humidity(v any) any { return float32(common.BytesToUint8(v.([]byte))) / 2 } + +func latitude(v any) any { + return float64(common.BytesToInt32(v.([]byte))) / 1000000 +} + +func longitude(v any) any { + return float64(common.BytesToInt32(v.([]byte))) / 1000000 +} + +func port10Altitude(v any) any { + return float64(common.BytesToUint16(v.([]byte))) / 100 +} + +func timestamp(v any) any { + return time.Unix(int64(common.BytesToUint32(v.([]byte))), 0).UTC() +} + +func gnssBattery(v any) any { + return float64(common.BytesToUint16(v.([]byte))) / 1000 +} + +func ttf(v any) any { + return time.Duration(int64(common.BytesToUint8(v.([]byte)))) * time.Second +} + +func pdop(v any) any { + return float64(common.BytesToUint8(v.([]byte))) / 2 +} diff --git a/pkg/decoder/smartlabel/v1/decoder_test.go b/pkg/decoder/smartlabel/v1/decoder_test.go index 0e8d396..743ad3f 100644 --- a/pkg/decoder/smartlabel/v1/decoder_test.go +++ b/pkg/decoder/smartlabel/v1/decoder_test.go @@ -12,6 +12,7 @@ import ( "reflect" "strings" "testing" + "time" "github.com/stretchr/testify/assert" helpers "github.com/truvami/decoder/pkg/common" @@ -235,6 +236,73 @@ func TestDecode(t *testing.T) { FirmwareVersionPatch: 12, }, }, + { + // Active GNSS fix near Zurich (tracker sample) + payload: "0002d2eeb40081d77ca3706a196afd0e74000009", + port: 10, + expected: Port10Payload{ + Status: 0, + Latitude: 47.3781, + Longitude: 8.509308, + Altitude: 418.4, + Timestamp: time.Date(2026, 5, 29, 10, 31, 25, 0, time.UTC), + Battery: 3.7, + TTF: helpers.DurationPtr(0), + PDOP: helpers.Float64Ptr(0), + Satellites: helpers.Uint8Ptr(9), + }, + }, + { + payload: "0002d308b50082457f16eb66c4a5cd0ed3000505", + port: 10, + expected: Port10Payload{ + Status: 0, + Latitude: 47.384757, + Longitude: 8.537471, + Altitude: 58.67, + Timestamp: time.Date(2024, 8, 20, 14, 18, 53, 0, time.UTC), + Battery: 3.795, + TTF: helpers.DurationPtr(0), + PDOP: helpers.Float64Ptr(2.5), + Satellites: helpers.Uint8Ptr(5), + }, + }, + { + payload: "0002d30b070082491f11256718d9fe0ede190505", + port: 10, + expected: Port10Payload{ + Status: 0, + Latitude: 47.385351, + Longitude: 8.538399, + Altitude: 43.89, + Timestamp: time.Date(2024, 10, 23, 11, 11, 58, 0, time.UTC), + Battery: 3.806, + PDOP: helpers.Float64Ptr(2.5), + Satellites: helpers.Uint8Ptr(5), + TTF: helpers.DurationPtr(25 * time.Second), + }, + }, + { + payload: "0002d30b070082491f11256718d9fe0e74190505", + port: 10, + expected: Port10Payload{ + Status: 0, + Latitude: 47.385351, + Longitude: 8.538399, + Altitude: 43.89, + Timestamp: time.Date(2024, 10, 23, 11, 11, 58, 0, time.UTC), + Battery: 3.7, + PDOP: helpers.Float64Ptr(2.5), + Satellites: helpers.Uint8Ptr(5), + TTF: helpers.DurationPtr(25 * time.Second), + }, + }, + { + payload: "00deadbeef", + port: 10, + expected: nil, + expectedErr: "invalid payload length", + }, { payload: "0ca90dbd07fa69", port: 11, @@ -445,6 +513,24 @@ func TestDecodeWithNoopSolver(t *testing.T) { } } +func TestPort10Features(t *testing.T) { + d := NewSmartLabelv1Decoder(context.TODO(), solver.MockSolverV1{}, zap.NewNop()) + decoded, err := d.Decode(context.TODO(), "0002d30b070082491f11256718d9fe0ede190505", 10) + assert.NoError(t, err) + assert.NotNil(t, decoded) + + assert.True(t, decoded.Is(decoder.FeatureGNSS)) + assert.True(t, decoded.Is(decoder.FeatureTimestamp)) + assert.True(t, decoded.Is(decoder.FeatureBattery)) + assert.False(t, decoded.Is(decoder.FeatureMoving)) + assert.False(t, decoded.Is(decoder.FeatureDutyCycle)) + assert.False(t, decoded.Is(decoder.FeatureConfigChange)) + + payload, ok := decoded.Data.(Port10Payload) + assert.True(t, ok) + assert.Equal(t, uint8(0), payload.Status) +} + func TestInvalidPort(t *testing.T) { logger := zap.NewExample() defer func() { @@ -518,6 +604,10 @@ func TestFeatures(t *testing.T) { payload: "fdb7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900ba262b95a6ffc86", port: 197, }, + { + payload: "0002d30b070082491f11256718d9fe0ede190505", + port: 10, + }, } mux := http.NewServeMux() @@ -708,6 +798,11 @@ func TestMarshal(t *testing.T) { port: 4, expected: []string{"\"dataRate\": \"automatic-wide\"", "\"gnss\": true", "\"temperatureUpperThreshold\": 40", "\"temperatureLowerThreshold\": -20"}, }, + { + payload: "0002d30b070082491f11256718d9fe0ede190505", + port: 10, + expected: []string{"\"status\": 0", "\"latitude\": 47.385351", "\"battery\": \"3.806v\"", "\"satellites\": 5"}, + }, { payload: "0f50107904da8d", port: 11, diff --git a/pkg/decoder/smartlabel/v1/port10.go b/pkg/decoder/smartlabel/v1/port10.go new file mode 100644 index 0000000..cb75ff2 --- /dev/null +++ b/pkg/decoder/smartlabel/v1/port10.go @@ -0,0 +1,111 @@ +package smartlabel + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/truvami/decoder/pkg/common" + "github.com/truvami/decoder/pkg/decoder" +) + +// Active GNSS uplink (lorawan_gps_ul_ts_t). +// +// +------+------+-------------------------------------------+------------------------+ +// | Byte | Size | Description | Format | +// +------+------+-------------------------------------------+------------------------+ +// | 0 | 1 | Status | uint8 (FW always 0) | +// | 1 | 4 | Latitude | int32, 1/1'000'000 deg | +// | 5 | 4 | Longitude | int32, 1/1'000'000 deg | +// | 9 | 2 | Altitude | uint16, centimeters | +// | 11 | 4 | Unix timestamp | uint32 | +// | 15 | 2 | voltage_temp (battery) | uint16, mV | +// | 17 | 1 | Time to fix | uint8, s | +// | 18 | 1 | Position dilution of precision | uint8, 1/2 meter | +// | 19 | 1 | Number of satellites | uint8 | +// +------+------+-------------------------------------------+------------------------+ + +type Port10Payload struct { + Status uint8 `json:"status"` + Latitude float64 `json:"latitude" validate:"gte=-90,lte=90"` + Longitude float64 `json:"longitude" validate:"gte=-180,lte=180"` + Altitude float64 `json:"altitude"` + Timestamp time.Time `json:"timestamp"` + Battery float64 `json:"battery" validate:"gte=1,lte=5"` + TTF *time.Duration `json:"ttf"` + PDOP *float64 `json:"pdop"` + Satellites *uint8 `json:"satellites" validate:"gte=3,lte=27"` +} + +func (p Port10Payload) MarshalJSON() ([]byte, error) { + type Alias Port10Payload + var ttf *string + if p.TTF != nil { + ttf = common.StringPtr(p.TTF.String()) + } + var pdop *string + if p.PDOP != nil { + pdop = common.StringPtr(fmt.Sprintf("%.1fm", *p.PDOP)) + } + return json.Marshal(&struct { + *Alias + Altitude string `json:"altitude"` + Timestamp string `json:"timestamp"` + Battery string `json:"battery"` + TTF *string `json:"ttf"` + PDOP *string `json:"pdop"` + Satellites *uint8 `json:"satellites"` + }{ + Alias: (*Alias)(&p), + Altitude: fmt.Sprintf("%.1fm", p.Altitude), + Timestamp: p.Timestamp.Format(time.RFC3339), + Battery: fmt.Sprintf("%.3fv", p.Battery), + TTF: ttf, + PDOP: pdop, + Satellites: p.Satellites, + }) +} + +var _ decoder.UplinkFeatureTimestamp = &Port10Payload{} +var _ decoder.UplinkFeatureGNSS = &Port10Payload{} +var _ decoder.UplinkFeatureBattery = &Port10Payload{} + +func (p Port10Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port10Payload) GetLatitude() float64 { + return p.Latitude +} + +func (p Port10Payload) GetLongitude() float64 { + return p.Longitude +} + +func (p Port10Payload) GetAltitude() float64 { + return p.Altitude +} + +func (p Port10Payload) GetAccuracy() *float64 { + return nil +} + +func (p Port10Payload) GetTTF() *time.Duration { + return p.TTF +} + +func (p Port10Payload) GetPDOP() *float64 { + return p.PDOP +} + +func (p Port10Payload) GetSatellites() *uint8 { + return p.Satellites +} + +func (p Port10Payload) GetBatteryVoltage() float64 { + return p.Battery +} + +func (p Port10Payload) GetLowBattery() *bool { + return nil +}