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 }