Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-02-08 07:28:32 +00:00
commit 2945d90d24
25 changed files with 3177 additions and 0 deletions

308
fitparser/message.go Normal file
View File

@@ -0,0 +1,308 @@
package fitparser
import (
"encoding/binary"
"fmt"
"math"
)
// FieldDefinition represents a field definition in a message definition
type FieldDefinition struct {
Num uint8 // Field definition number
Size uint8 // Size in bytes
BaseType uint8 // Base type
}
// DeveloperFieldDefinition represents a developer field definition
type DeveloperFieldDefinition struct {
Num uint8
Size uint8
DeveloperDataIndex uint8
}
// MessageDefinition represents a message definition record
type MessageDefinition struct {
Reserved uint8
Architecture uint8 // 0 = little endian, 1 = big endian
GlobalMesgNum uint16
NumFields uint8
FieldDefinitions []FieldDefinition
NumDevFields uint8
DevFieldDefinitions []DeveloperFieldDefinition
}
// Message represents a decoded FIT message
type Message struct {
Num uint16 // Global message number
Fields map[uint8]interface{} // Field number -> value
DevFields map[uint8]interface{} // Developer field number -> value
}
// NewMessage creates a new message
func NewMessage(num uint16) *Message {
return &Message{
Num: num,
Fields: make(map[uint8]interface{}),
DevFields: make(map[uint8]interface{}),
}
}
// GetFieldValue returns the value of a field by field number
func (m *Message) GetFieldValue(fieldNum uint8) (interface{}, bool) {
val, ok := m.Fields[fieldNum]
return val, ok
}
// GetFieldValueUint8 returns a field value as uint8
func (m *Message) GetFieldValueUint8(fieldNum uint8) (uint8, bool) {
if val, ok := m.Fields[fieldNum]; ok {
if v, ok := val.(uint8); ok {
return v, true
}
}
return 0, false
}
// GetFieldValueUint16 returns a field value as uint16
func (m *Message) GetFieldValueUint16(fieldNum uint8) (uint16, bool) {
if val, ok := m.Fields[fieldNum]; ok {
if v, ok := val.(uint16); ok {
return v, true
}
}
return 0, false
}
// GetFieldValueUint32 returns a field value as uint32
func (m *Message) GetFieldValueUint32(fieldNum uint8) (uint32, bool) {
if val, ok := m.Fields[fieldNum]; ok {
if v, ok := val.(uint32); ok {
return v, true
}
}
return 0, false
}
// GetFieldValueString returns a field value as string
func (m *Message) GetFieldValueString(fieldNum uint8) (string, bool) {
if val, ok := m.Fields[fieldNum]; ok {
if v, ok := val.(string); ok {
return v, true
}
}
return "", false
}
// IsLittleEndian returns true if the message definition uses little endian byte order
func (md *MessageDefinition) IsLittleEndian() bool {
return md.Architecture == 0
}
// DecodeField decodes a single field value from raw bytes
func DecodeField(data []byte, fieldDef FieldDefinition, littleEndian bool) (interface{}, error) {
if len(data) < int(fieldDef.Size) {
return nil, fmt.Errorf("insufficient data for field: need %d bytes, got %d", fieldDef.Size, len(data))
}
// Extract the base type number by masking (removes endian flag)
baseTypeNum := fieldDef.BaseType & BaseTypeNumMask
fieldData := data[:fieldDef.Size]
switch baseTypeNum {
case 0x00, 0x02, 0x0A, 0x0D: // Enum (0x00), Uint8 (0x02), Uint8z (0x0A), Byte (0x0D)
if fieldDef.Size == 1 {
return uint8(fieldData[0]), nil
}
// Array of bytes
result := make([]uint8, fieldDef.Size)
copy(result, fieldData)
return result, nil
case 0x01: // Sint8
if fieldDef.Size == 1 {
return int8(fieldData[0]), nil
}
// Array of int8
result := make([]int8, fieldDef.Size)
for i := range result {
result[i] = int8(fieldData[i])
}
return result, nil
case 0x04, 0x0B: // Uint16 (0x84 masked), Uint16z (0x8B masked)
if fieldDef.Size == 2 {
if littleEndian {
return binary.LittleEndian.Uint16(fieldData), nil
}
return binary.BigEndian.Uint16(fieldData), nil
}
// Array of uint16
count := fieldDef.Size / 2
result := make([]uint16, count)
for i := range result {
if littleEndian {
result[i] = binary.LittleEndian.Uint16(fieldData[i*2:])
} else {
result[i] = binary.BigEndian.Uint16(fieldData[i*2:])
}
}
return result, nil
case 0x03: // Sint16 (0x83 masked)
if fieldDef.Size == 2 {
if littleEndian {
return int16(binary.LittleEndian.Uint16(fieldData)), nil
}
return int16(binary.BigEndian.Uint16(fieldData)), nil
}
// Array of int16
count := fieldDef.Size / 2
result := make([]int16, count)
for i := range result {
if littleEndian {
result[i] = int16(binary.LittleEndian.Uint16(fieldData[i*2:]))
} else {
result[i] = int16(binary.BigEndian.Uint16(fieldData[i*2:]))
}
}
return result, nil
case 0x06, 0x0C: // Uint32 (0x86 masked), Uint32z (0x8C masked)
if fieldDef.Size == 4 {
if littleEndian {
return binary.LittleEndian.Uint32(fieldData), nil
}
return binary.BigEndian.Uint32(fieldData), nil
}
// Array of uint32
count := fieldDef.Size / 4
result := make([]uint32, count)
for i := range result {
if littleEndian {
result[i] = binary.LittleEndian.Uint32(fieldData[i*4:])
} else {
result[i] = binary.BigEndian.Uint32(fieldData[i*4:])
}
}
return result, nil
case 0x05: // Sint32 (0x85 masked)
if fieldDef.Size == 4 {
if littleEndian {
return int32(binary.LittleEndian.Uint32(fieldData)), nil
}
return int32(binary.BigEndian.Uint32(fieldData)), nil
}
// Array of int32
count := fieldDef.Size / 4
result := make([]int32, count)
for i := range result {
if littleEndian {
result[i] = int32(binary.LittleEndian.Uint32(fieldData[i*4:]))
} else {
result[i] = int32(binary.BigEndian.Uint32(fieldData[i*4:]))
}
}
return result, nil
case 0x08: // Float32 (0x88 masked)
if fieldDef.Size == 4 {
var bits uint32
if littleEndian {
bits = binary.LittleEndian.Uint32(fieldData)
} else {
bits = binary.BigEndian.Uint32(fieldData)
}
return math.Float32frombits(bits), nil
}
// Array of float32
count := fieldDef.Size / 4
result := make([]float32, count)
for i := range result {
var bits uint32
if littleEndian {
bits = binary.LittleEndian.Uint32(fieldData[i*4:])
} else {
bits = binary.BigEndian.Uint32(fieldData[i*4:])
}
result[i] = math.Float32frombits(bits)
}
return result, nil
case 0x09: // Float64 (0x89 masked)
if fieldDef.Size == 8 {
var bits uint64
if littleEndian {
bits = binary.LittleEndian.Uint64(fieldData)
} else {
bits = binary.BigEndian.Uint64(fieldData)
}
return math.Float64frombits(bits), nil
}
// Array of float64
count := fieldDef.Size / 8
result := make([]float64, count)
for i := range result {
var bits uint64
if littleEndian {
bits = binary.LittleEndian.Uint64(fieldData[i*8:])
} else {
bits = binary.BigEndian.Uint64(fieldData[i*8:])
}
result[i] = math.Float64frombits(bits)
}
return result, nil
case 0x0F, 0x10: // Uint64 (0x8F masked), Uint64z (0x90 masked)
if fieldDef.Size == 8 {
if littleEndian {
return binary.LittleEndian.Uint64(fieldData), nil
}
return binary.BigEndian.Uint64(fieldData), nil
}
// Array of uint64
count := fieldDef.Size / 8
result := make([]uint64, count)
for i := range result {
if littleEndian {
result[i] = binary.LittleEndian.Uint64(fieldData[i*8:])
} else {
result[i] = binary.BigEndian.Uint64(fieldData[i*8:])
}
}
return result, nil
case 0x0E: // Sint64 (0x8E masked)
if fieldDef.Size == 8 {
if littleEndian {
return int64(binary.LittleEndian.Uint64(fieldData)), nil
}
return int64(binary.BigEndian.Uint64(fieldData)), nil
}
// Array of int64
count := fieldDef.Size / 8
result := make([]int64, count)
for i := range result {
if littleEndian {
result[i] = int64(binary.LittleEndian.Uint64(fieldData[i*8:]))
} else {
result[i] = int64(binary.BigEndian.Uint64(fieldData[i*8:]))
}
}
return result, nil
case 0x07: // String
// String field - find null terminator
end := len(fieldData)
for i, b := range fieldData {
if b == 0 {
end = i
break
}
}
return string(fieldData[:end]), nil
default:
return nil, fmt.Errorf("unsupported base type: 0x%02X (num: 0x%02X)", fieldDef.BaseType, baseTypeNum)
}
}