Skip to content

Commit

Permalink
Merge pull request #32 from truvami/downlink-encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbeutler authored Jan 30, 2025
2 parents 218dd71 + 8bc599b commit 67d28e9
Show file tree
Hide file tree
Showing 27 changed files with 547 additions and 189 deletions.
2 changes: 1 addition & 1 deletion cmd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/truvami/decoder/internal/logger"
helpers "github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder"
"github.com/truvami/decoder/pkg/decoder/helpers"
"github.com/truvami/decoder/pkg/decoder/nomadxl/v1"
"github.com/truvami/decoder/pkg/decoder/nomadxs/v1"
"github.com/truvami/decoder/pkg/decoder/smartlabel/v1"
Expand Down
2 changes: 1 addition & 1 deletion cmd/nomadxl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/spf13/cobra"
"github.com/truvami/decoder/internal/logger"
"github.com/truvami/decoder/pkg/decoder/helpers"
helpers "github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder/nomadxl/v1"
"go.uber.org/zap"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/nomadxs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/spf13/cobra"
"github.com/truvami/decoder/internal/logger"
"github.com/truvami/decoder/pkg/decoder/helpers"
helpers "github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder/nomadxs/v1"
"go.uber.org/zap"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/smartlabel.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/spf13/cobra"
"github.com/truvami/decoder/internal/logger"
"github.com/truvami/decoder/pkg/decoder/helpers"
helpers "github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder/smartlabel/v1"
"github.com/truvami/decoder/pkg/loracloud"
"go.uber.org/zap"
Expand Down
2 changes: 1 addition & 1 deletion cmd/tagsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/spf13/cobra"
"github.com/truvami/decoder/internal/logger"
"github.com/truvami/decoder/pkg/decoder/helpers"
helpers "github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder/tagsl/v1"
"go.uber.org/zap"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/tagxl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/truvami/decoder/internal/logger"
"github.com/truvami/decoder/pkg/decoder/helpers"
helpers "github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder/tagxl/v1"
"github.com/truvami/decoder/pkg/loracloud"
"go.uber.org/zap"
Expand Down
19 changes: 19 additions & 0 deletions pkg/common/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package common

import "reflect"

type FieldConfig struct {
Name string
Start int
Length int
Transform func(interface{}) interface{}
Optional bool
Hex bool
}

// PayloadConfig defines the overall structure of the payload, including the target struct type
type PayloadConfig struct {
Fields []FieldConfig
TargetType reflect.Type
StatusByteIndex *int // can be nil
}
91 changes: 81 additions & 10 deletions pkg/decoder/helpers/helpers.go → pkg/common/helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package helpers
package common

import (
h "encoding/hex"
"encoding/hex"
"errors"
"fmt"

Expand All @@ -10,11 +10,10 @@ import (
"time"

"github.com/go-playground/validator"
"github.com/truvami/decoder/pkg/decoder"
)

func HexStringToBytes(hexString string) ([]byte, error) {
bytes, err := h.DecodeString(hexString)
bytes, err := hex.DecodeString(hexString)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -62,7 +61,7 @@ func convertFieldToType(value interface{}, fieldType reflect.Kind) interface{} {
}
}

func extractFieldValue(payloadBytes []byte, start int, length int, optional bool, hex bool) (interface{}, error) {
func extractFieldValue(payloadBytes []byte, start int, length int, optional bool, hexadecimal bool) (interface{}, error) {
if length == -1 {
if start >= len(payloadBytes) {
return nil, fmt.Errorf("field start out of bounds")
Expand All @@ -78,8 +77,8 @@ func extractFieldValue(payloadBytes []byte, start int, length int, optional bool

// Extract the field value based on its length
var value interface{}
if hex {
value = h.EncodeToString(payloadBytes[start : start+length])
if hexadecimal {
value = hex.EncodeToString(payloadBytes[start : start+length])
} else {
value = 0
for i := 0; i < length; i++ {
Expand Down Expand Up @@ -110,7 +109,7 @@ func UnwrapError(err error) []error {
}

// DecodeLoRaWANPayload decodes the payload based on the provided configuration and populates the target struct
func Parse(payloadHex string, config decoder.PayloadConfig) (interface{}, error) {
func Parse(payloadHex string, config *PayloadConfig) (interface{}, error) {
// Convert hex payload to bytes
payloadBytes, err := HexStringToBytes(payloadHex)
if err != nil {
Expand Down Expand Up @@ -189,7 +188,7 @@ func ToIntPointer(value int) *int {
return &value
}

func HexNullPad(payload *string, config *decoder.PayloadConfig) string {
func HexNullPad(payload *string, config *PayloadConfig) string {
var requiredBits = 0
for _, field := range config.Fields {
if !field.Optional {
Expand All @@ -205,7 +204,7 @@ func HexNullPad(payload *string, config *decoder.PayloadConfig) string {
return *payload
}

func ValidateLength(payload *string, config *decoder.PayloadConfig) error {
func ValidateLength(payload *string, config *PayloadConfig) error {
var payloadLength = len(*payload) / 2

var minLength = 0
Expand All @@ -230,3 +229,75 @@ func ValidateLength(payload *string, config *decoder.PayloadConfig) error {

return nil
}

func Encode(data interface{}, config PayloadConfig) (string, error) {
v := reflect.ValueOf(data)

// Validate input data is a struct
if v.Kind() != reflect.Struct {
return "", fmt.Errorf("data must be a struct")
}

// Determine total payload length
var length int
for _, field := range config.Fields {
if field.Start+field.Length > length {
length = field.Start + field.Length
}
}
payload := make([]byte, length)

// Encode fields into the payload
for _, field := range config.Fields {
fieldValue := v.FieldByName(field.Name)

// Check if the field exists
if !fieldValue.IsValid() {
return "", fmt.Errorf("field %s not found in data", field.Name)
}

// Convert the value to bytes
var fieldBytes []byte
switch fieldValue.Kind() {
case reflect.Slice:
fieldBytes = fieldValue.Bytes()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fieldBytes = uintToBytes(fieldValue.Uint(), field.Length)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fieldBytes = intToBytes(fieldValue.Int(), field.Length)
default:
return "", fmt.Errorf("unsupported field type: %s", fieldValue.Kind())
}

// Apply the transform function if provided
if field.Transform != nil {
fieldBytes = field.Transform(fieldBytes).([]byte)
}

// Copy the bytes into the payload at the correct position
copy(payload[field.Start:field.Start+field.Length], fieldBytes)
}

// Convert the payload to a hexadecimal string
return hex.EncodeToString(payload), nil
}

// intToBytes converts an integer value to a byte slice
func intToBytes(value int64, length int) []byte {
buf := make([]byte, length)
for i := length - 1; i >= 0; i-- {
buf[i] = byte(value & 0xFF)
value >>= 8
}
return buf
}

// uintToBytes converts an unsigned integer value to a byte slice
func uintToBytes(value uint64, length int) []byte {
buf := make([]byte, length)
for i := length - 1; i >= 0; i-- {
buf[i] = byte(value & 0xFF)
value >>= 8
}
return buf
}
24 changes: 11 additions & 13 deletions pkg/decoder/helpers/helpers_test.go → pkg/common/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package helpers
package common

import (
"fmt"
"reflect"
"testing"

"github.com/truvami/decoder/pkg/decoder"
)

func TestInvalidHexString(t *testing.T) {
Expand Down Expand Up @@ -37,8 +35,8 @@ type Port1Payload struct {
}

func TestParse(t *testing.T) {
config := decoder.PayloadConfig{
Fields: []decoder.FieldConfig{
config := PayloadConfig{
Fields: []FieldConfig{
{Name: "Moving", Start: 0, Length: 1},
{Name: "Lat", Start: 1, Length: 4, Transform: func(v interface{}) interface{} {
return float64(v.(int)) / 1000000
Expand All @@ -59,7 +57,7 @@ func TestParse(t *testing.T) {

tests := []struct {
payload string
config decoder.PayloadConfig
config PayloadConfig
expected interface{}
}{
{
Expand All @@ -82,7 +80,7 @@ func TestParse(t *testing.T) {

for _, test := range tests {
t.Run(test.payload, func(t *testing.T) {
decodedData, err := Parse(test.payload, test.config)
decodedData, err := Parse(test.payload, &test.config)
if err != nil {
t.Fatalf("error decoding payload: %v", err)
}
Expand Down Expand Up @@ -181,8 +179,8 @@ func TestConvertFieldToType(t *testing.T) {
}

func TestInvalidPayload(t *testing.T) {
_, err := Parse("", decoder.PayloadConfig{
Fields: []decoder.FieldConfig{
_, err := Parse("", &PayloadConfig{
Fields: []FieldConfig{
{Name: "Moving", Start: 0, Length: 1},
},
TargetType: reflect.TypeOf(Port1Payload{}),
Expand All @@ -191,8 +189,8 @@ func TestInvalidPayload(t *testing.T) {
t.Fatal("expected field out of bounds")
}

_, err = Parse("01", decoder.PayloadConfig{
Fields: []decoder.FieldConfig{
_, err = Parse("01", &PayloadConfig{
Fields: []FieldConfig{
{Name: "Moving", Start: 0, Length: 2},
},
TargetType: reflect.TypeOf(Port1Payload{}),
Expand All @@ -201,8 +199,8 @@ func TestInvalidPayload(t *testing.T) {
t.Fatal("expected field out of bounds")
}

_, err = Parse("01", decoder.PayloadConfig{
Fields: []decoder.FieldConfig{
_, err = Parse("01", &PayloadConfig{
Fields: []FieldConfig{
{Name: "Moving", Start: 10, Length: 1},
},
TargetType: reflect.TypeOf(Port1Payload{}),
Expand Down
19 changes: 0 additions & 19 deletions pkg/decoder/decoder.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
package decoder

import "reflect"

// FieldConfig defines the structure of a single field in the payload
type FieldConfig struct {
Name string
Start int
Length int
Transform func(interface{}) interface{}
Optional bool
Hex bool
}

// PayloadConfig defines the overall structure of the payload, including the target struct type
type PayloadConfig struct {
Fields []FieldConfig
TargetType reflect.Type
StatusByteIndex *int // can be nil
}

type Decoder interface {
Decode(string, int16, string) (interface{}, interface{}, error)
}
20 changes: 10 additions & 10 deletions pkg/decoder/nomadxl/v1/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"fmt"
"reflect"

"github.com/truvami/decoder/pkg/common"
"github.com/truvami/decoder/pkg/decoder"
"github.com/truvami/decoder/pkg/decoder/helpers"
)

type Option func(*NomadXLv1Decoder)
Expand Down Expand Up @@ -38,11 +38,11 @@ func WithSkipValidation(skipValidation bool) Option {
}

// https://docs.truvami.com/docs/payloads/nomad-XL
func (t NomadXLv1Decoder) getConfig(port int16) (decoder.PayloadConfig, error) {
func (t NomadXLv1Decoder) getConfig(port int16) (common.PayloadConfig, error) {
switch port {
case 101:
return decoder.PayloadConfig{
Fields: []decoder.FieldConfig{
return common.PayloadConfig{
Fields: []common.FieldConfig{
{Name: "SystemTime", Start: 0, Length: 8},
{Name: "UTCDate", Start: 8, Length: 4},
{Name: "UTCTime", Start: 12, Length: 4},
Expand All @@ -68,8 +68,8 @@ func (t NomadXLv1Decoder) getConfig(port int16) (decoder.PayloadConfig, error) {
TargetType: reflect.TypeOf(Port101Payload{}),
}, nil
case 103:
return decoder.PayloadConfig{
Fields: []decoder.FieldConfig{
return common.PayloadConfig{
Fields: []common.FieldConfig{

{Name: "UTCDate", Start: 0, Length: 4},
{Name: "UTCTime", Start: 4, Length: 4},
Expand All @@ -88,7 +88,7 @@ func (t NomadXLv1Decoder) getConfig(port int16) (decoder.PayloadConfig, error) {
}, nil
}

return decoder.PayloadConfig{}, fmt.Errorf("port %v not supported", port)
return common.PayloadConfig{}, fmt.Errorf("port %v not supported", port)
}

func (t NomadXLv1Decoder) Decode(data string, port int16, devEui string) (interface{}, interface{}, error) {
Expand All @@ -98,16 +98,16 @@ func (t NomadXLv1Decoder) Decode(data string, port int16, devEui string) (interf
}

if t.autoPadding {
data = helpers.HexNullPad(&data, &config)
data = common.HexNullPad(&data, &config)
}

if !t.skipValidation {
err := helpers.ValidateLength(&data, &config)
err := common.ValidateLength(&data, &config)
if err != nil {
return nil, nil, err
}
}

decodedData, err := helpers.Parse(data, config)
decodedData, err := common.Parse(data, &config)
return decodedData, nil, err
}
2 changes: 1 addition & 1 deletion pkg/decoder/nomadxl/v1/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestDecode(t *testing.T) {
func TestInvalidPort(t *testing.T) {
decoder := NewNomadXLv1Decoder()
_, _, err := decoder.Decode("00", 0, "")
if err == nil {
if err == nil || err.Error() != "port 0 not supported" {
t.Fatal("expected port not supported")
}
}
Expand Down
Loading

0 comments on commit 67d28e9

Please sign in to comment.