332
fitparser/decoder.go
Normal file
332
fitparser/decoder.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user