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

332
fitparser/decoder.go Normal file
View File

@@ -0,0 +1,332 @@
package fitparser
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
// Decoder decodes FIT files
type Decoder struct {
reader *bytes.Reader
header *Header
localMessageTypes [16]*MessageDefinition // Local message type -> definition
messages []*Message
crc uint16
enableCRCCheck bool
offset int64
}
// NewDecoder creates a new FIT file decoder
func NewDecoder(data []byte) (*Decoder, error) {
if len(data) < HeaderMinSize {
return nil, fmt.Errorf("file too small to be a valid FIT file: %d bytes", len(data))
}
if !IsFIT(data) {
return nil, fmt.Errorf("not a valid FIT file: missing .FIT signature")
}
return &Decoder{
reader: bytes.NewReader(data),
enableCRCCheck: true,
crc: 0,
}, nil
}
// EnableCRCCheck enables or disables CRC checking
func (d *Decoder) EnableCRCCheck(enable bool) {
d.enableCRCCheck = enable
}
// Decode decodes the entire FIT file and returns all messages
func (d *Decoder) Decode() ([]*Message, error) {
// Parse header
headerData := make([]byte, HeaderWithCRCSize)
n, err := d.reader.Read(headerData)
if err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
header, err := ParseHeader(headerData[:n])
if err != nil {
return nil, fmt.Errorf("failed to parse header: %w", err)
}
d.header = header
// Position reader at start of data records
d.reader.Seek(int64(header.Size), io.SeekStart)
d.offset = int64(header.Size)
// Initialize CRC with entire header (all bytes up to data start)
// For 14-byte headers, this includes the header CRC (bytes 12-13)
// For 12-byte headers, this is just bytes 0-11
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, headerData[:header.Size])
}
// Read data records
dataEnd := int64(header.Size) + int64(header.DataSize)
for d.offset < dataEnd {
msg, err := d.readRecord()
if err != nil {
return nil, fmt.Errorf("error reading record at offset %d: %w", d.offset, err)
}
if msg != nil {
d.messages = append(d.messages, msg)
}
}
// Verify file CRC if enabled
if d.enableCRCCheck {
fileCRCData := make([]byte, 2)
_, err := d.reader.Read(fileCRCData)
if err != nil {
return nil, fmt.Errorf("failed to read file CRC: %w", err)
}
fileCRC := binary.LittleEndian.Uint16(fileCRCData)
if fileCRC != d.crc {
return nil, fmt.Errorf("file CRC mismatch: calculated 0x%04X, expected 0x%04X", d.crc, fileCRC)
}
}
return d.messages, nil
}
// readRecord reads a single record (definition or data message)
func (d *Decoder) readRecord() (*Message, error) {
recordHeader := make([]byte, 1)
_, err := d.reader.Read(recordHeader)
if err != nil {
return nil, fmt.Errorf("failed to read record header: %w", err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, recordHeader)
}
d.offset++
header := recordHeader[0]
// Check if this is a definition message or data message
if (header & MesgDefinitionMask) != 0 {
// Definition message
return d.readDefinitionMessage(header)
} else {
// Data message (normal or compressed timestamp)
if (header & RecordHeaderCompressedMask) != 0 {
// Compressed timestamp header
return d.readCompressedTimestampMessage(header)
} else {
// Normal data message
return d.readDataMessage(header)
}
}
}
// readDefinitionMessage reads a message definition
func (d *Decoder) readDefinitionMessage(header byte) (*Message, error) {
localMesgNum := header & LocalMesgNum
// Read definition header (5 bytes: reserved, architecture, global_mesg_num, num_fields)
defHeader := make([]byte, 5)
_, err := d.reader.Read(defHeader)
if err != nil {
return nil, fmt.Errorf("failed to read definition header: %w", err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, defHeader)
}
d.offset += 5
msgDef := &MessageDefinition{
Reserved: defHeader[0],
Architecture: defHeader[1],
NumFields: defHeader[4],
}
// Parse global message number based on architecture
if msgDef.IsLittleEndian() {
msgDef.GlobalMesgNum = binary.LittleEndian.Uint16(defHeader[2:4])
} else {
msgDef.GlobalMesgNum = binary.BigEndian.Uint16(defHeader[2:4])
}
// Read field definitions (3 bytes each: field_def_num, size, base_type)
msgDef.FieldDefinitions = make([]FieldDefinition, msgDef.NumFields)
for i := 0; i < int(msgDef.NumFields); i++ {
fieldDefData := make([]byte, 3)
_, err := d.reader.Read(fieldDefData)
if err != nil {
return nil, fmt.Errorf("failed to read field definition %d: %w", i, err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, fieldDefData)
}
d.offset += 3
msgDef.FieldDefinitions[i] = FieldDefinition{
Num: fieldDefData[0],
Size: fieldDefData[1],
BaseType: fieldDefData[2],
}
}
// Check for developer fields
if (header & DevDataMask) != 0 {
devFieldsData := make([]byte, 1)
_, err := d.reader.Read(devFieldsData)
if err != nil {
return nil, fmt.Errorf("failed to read num dev fields: %w", err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, devFieldsData)
}
d.offset++
msgDef.NumDevFields = devFieldsData[0]
// Read developer field definitions (3 bytes each)
msgDef.DevFieldDefinitions = make([]DeveloperFieldDefinition, msgDef.NumDevFields)
for i := 0; i < int(msgDef.NumDevFields); i++ {
devFieldDefData := make([]byte, 3)
_, err := d.reader.Read(devFieldDefData)
if err != nil {
return nil, fmt.Errorf("failed to read dev field definition %d: %w", i, err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, devFieldDefData)
}
d.offset += 3
msgDef.DevFieldDefinitions[i] = DeveloperFieldDefinition{
Num: devFieldDefData[0],
Size: devFieldDefData[1],
DeveloperDataIndex: devFieldDefData[2],
}
}
}
// Store the definition for this local message number
d.localMessageTypes[localMesgNum] = msgDef
return nil, nil // Definition messages don't produce output messages
}
// readDataMessage reads a data message using a previously defined message definition
func (d *Decoder) readDataMessage(header byte) (*Message, error) {
localMesgNum := header & LocalMesgNum
msgDef := d.localMessageTypes[localMesgNum]
if msgDef == nil {
return nil, fmt.Errorf("no definition found for local message number %d", localMesgNum)
}
msg := NewMessage(msgDef.GlobalMesgNum)
// Read and decode each field
for _, fieldDef := range msgDef.FieldDefinitions {
fieldData := make([]byte, fieldDef.Size)
_, err := d.reader.Read(fieldData)
if err != nil {
return nil, fmt.Errorf("failed to read field %d: %w", fieldDef.Num, err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, fieldData)
}
d.offset += int64(fieldDef.Size)
// Decode the field value
value, err := DecodeField(fieldData, fieldDef, msgDef.IsLittleEndian())
if err != nil {
return nil, fmt.Errorf("failed to decode field %d: %w", fieldDef.Num, err)
}
msg.Fields[fieldDef.Num] = value
}
// Read developer fields if present
for _, devFieldDef := range msgDef.DevFieldDefinitions {
devFieldData := make([]byte, devFieldDef.Size)
_, err := d.reader.Read(devFieldData)
if err != nil {
return nil, fmt.Errorf("failed to read dev field %d: %w", devFieldDef.Num, err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, devFieldData)
}
d.offset += int64(devFieldDef.Size)
msg.DevFields[devFieldDef.Num] = devFieldData
}
return msg, nil
}
// readCompressedTimestampMessage reads a compressed timestamp data message
func (d *Decoder) readCompressedTimestampMessage(header byte) (*Message, error) {
localMesgNum := header & RecordHeaderLocalMesgNumMask
timeOffset := header & RecordHeaderTimeMask
msgDef := d.localMessageTypes[localMesgNum]
if msgDef == nil {
return nil, fmt.Errorf("no definition found for local message number %d", localMesgNum)
}
msg := NewMessage(msgDef.GlobalMesgNum)
// Read and decode each field
for _, fieldDef := range msgDef.FieldDefinitions {
fieldData := make([]byte, fieldDef.Size)
_, err := d.reader.Read(fieldData)
if err != nil {
return nil, fmt.Errorf("failed to read field %d: %w", fieldDef.Num, err)
}
if d.enableCRCCheck {
d.crc = UpdateCRC(d.crc, fieldData)
}
d.offset += int64(fieldDef.Size)
// Decode the field value
value, err := DecodeField(fieldData, fieldDef, msgDef.IsLittleEndian())
if err != nil {
return nil, fmt.Errorf("failed to decode field %d: %w", fieldDef.Num, err)
}
msg.Fields[fieldDef.Num] = value
}
// Store the compressed timestamp offset
// Note: Full timestamp reconstruction would require tracking the last full timestamp
// For now, we just store the offset value
msg.Fields[253] = uint32(timeOffset) // Field 253 is typically timestamp
return msg, nil
}
// GetHeader returns the parsed header
func (d *Decoder) GetHeader() *Header {
return d.header
}
// CheckIntegrity checks if the file is a valid FIT file with correct CRC
func CheckIntegrity(data []byte) error {
decoder, err := NewDecoder(data)
if err != nil {
return err
}
decoder.EnableCRCCheck(true)
_, err = decoder.Decode()
return err
}