333 lines
9.3 KiB
Go
333 lines
9.3 KiB
Go
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
|
|
}
|