mirror of
https://github.com/sstent/go-garminconnect.git
synced 2025-12-05 23:52:03 +00:00
136 lines
3.3 KiB
Go
136 lines
3.3 KiB
Go
package fit
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"io"
|
|
)
|
|
|
|
// FitEncoder encodes FIT activity files using streaming writes with optimized CRC calculation
|
|
type FitEncoder struct {
|
|
w io.WriteSeeker
|
|
crc uint16
|
|
dataSize int
|
|
headerSize int
|
|
startPos int64 // position after header
|
|
}
|
|
|
|
// NewFitEncoder creates a new streaming FIT encoder
|
|
func NewFitEncoder(w io.WriteSeeker) (*FitEncoder, error) {
|
|
encoder := &FitEncoder{
|
|
w: w,
|
|
crc: 0,
|
|
dataSize: 0,
|
|
headerSize: 14, // Standard header size with CRC
|
|
}
|
|
|
|
// Get current position for header
|
|
var err error
|
|
if encoder.startPos, err = w.Seek(0, io.SeekCurrent); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write header placeholder
|
|
header := []byte{
|
|
14, // Header size
|
|
0x10, // Protocol version
|
|
0x00, 0x2D, // Profile version (little endian 45)
|
|
0x00, 0x00, 0x00, 0x00, // Data size (4 bytes, will be updated later)
|
|
'.', 'F', 'I', 'T', // ".FIT" data type
|
|
0x00, 0x00, // Header CRC (will be calculated later)
|
|
}
|
|
|
|
// Write header and calculate CRC
|
|
if _, err := w.Write(header); err != nil {
|
|
return nil, err
|
|
}
|
|
encoder.updateCRC(header)
|
|
|
|
return encoder, nil
|
|
}
|
|
|
|
// updateCRC calculates CRC-16 checksum without hash/crc16 dependency
|
|
func (e *FitEncoder) updateCRC(data []byte) {
|
|
crcTable := [...]uint16{
|
|
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
|
|
0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
|
|
}
|
|
|
|
currentCRC := e.crc
|
|
for _, b := range data {
|
|
// Compute checksum of lower four bits
|
|
tmp := crcTable[currentCRC&0xF]
|
|
currentCRC = (currentCRC >> 4) & 0x0FFF
|
|
currentCRC = currentCRC ^ tmp ^ crcTable[b&0xF]
|
|
|
|
// Compute checksum of upper four bits
|
|
tmp = crcTable[currentCRC&0xF]
|
|
currentCRC = (currentCRC >> 4) & 0x0FFF
|
|
currentCRC = currentCRC ^ tmp ^ crcTable[(b>>4)&0xF]
|
|
}
|
|
e.crc = currentCRC
|
|
}
|
|
|
|
// Write writes activity data in chunks
|
|
func (e *FitEncoder) Write(p []byte) (int, error) {
|
|
n, err := e.w.Write(p)
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
|
|
e.updateCRC(p)
|
|
e.dataSize += n
|
|
return n, nil
|
|
}
|
|
|
|
// Close finalizes the FIT file
|
|
func (e *FitEncoder) Close() error {
|
|
// Save current position
|
|
currentPos, err := e.w.Seek(0, io.SeekCurrent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update data size in header
|
|
if _, err := e.w.Seek(e.startPos+4, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
dataSizeBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(dataSizeBytes, uint32(e.dataSize))
|
|
if _, err := e.w.Write(dataSizeBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Recalculate header CRC with original data
|
|
header := []byte{
|
|
14, // Header size
|
|
0x10, // Protocol version
|
|
0x00, 0x2D, // Profile version
|
|
dataSizeBytes[0], dataSizeBytes[1], dataSizeBytes[2], dataSizeBytes[3],
|
|
'.', 'F', 'I', 'T', // ".FIT" data type
|
|
}
|
|
|
|
// Calculate header CRC with clean state
|
|
e.crc = 0
|
|
e.updateCRC(header)
|
|
headerCRC := e.crc
|
|
|
|
// Update header CRC
|
|
if _, err := e.w.Seek(e.startPos+12, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
crcBytes := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(crcBytes, headerCRC)
|
|
if _, err := e.w.Write(crcBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write final file CRC
|
|
if _, err := e.w.Seek(currentPos, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
fileCRCBytes := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(fileCRCBytes, e.crc)
|
|
_, err = e.w.Write(fileCRCBytes)
|
|
return err
|
|
}
|