This commit is contained in:
2025-08-27 11:58:01 -07:00
parent f24d21033a
commit f4b9f350ae
25 changed files with 2184 additions and 485 deletions

View File

@@ -1,121 +1,135 @@
package fit
import (
"bytes"
"encoding/binary"
"time"
"io"
)
// FitBaseType represents FIT base type definitions
type FitBaseType struct {
ID int
Name string
Size int
Invalid uint64
Field byte
}
// Base types definitions
var (
FitEnum = FitBaseType{0, "enum", 1, 0xFF, 0x00}
FitUint8 = FitBaseType{2, "uint8", 1, 0xFF, 0x02}
FitUint16 = FitBaseType{4, "uint16", 2, 0xFFFF, 0x84}
FitUint32 = FitBaseType{6, "uint32", 4, 0xFFFFFFFF, 0x86}
FitString = FitBaseType{7, "string", 1, 0x00, 0x07}
FitFloat32 = FitBaseType{8, "float32", 4, 0xFFFFFFFF, 0x88}
FitByte = FitBaseType{13, "byte", 1, 0xFF, 0x0D}
)
// FitEncoder encodes FIT activity files
// FitEncoder encodes FIT activity files using streaming writes with optimized CRC calculation
type FitEncoder struct {
buf bytes.Buffer
w io.WriteSeeker
crc uint16
dataSize int
headerSize int
activityDefined bool
startPos int64 // position after header
}
const (
FitHeaderSize = 12
FileTypeActivity = 4
GarminEpochOffset = 631065600 // UTC 00:00 Dec 31 1989
)
// 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
}
// NewFitEncoder creates a new FIT encoder
func NewFitEncoder() *FitEncoder {
e := &FitEncoder{headerSize: FitHeaderSize}
e.writeHeader(0) // Initial header with 0 data size
return e
}
// Get current position for header
var err error
if encoder.startPos, err = w.Seek(0, io.SeekCurrent); err != nil {
return nil, err
}
// writeHeader writes the FIT file header
func (e *FitEncoder) writeHeader(dataSize int) {
e.buf.Reset()
// Write header placeholder
header := []byte{
byte(e.headerSize), // Header size
16, // Protocol version
0, 0, 0, 108, // Profile version (108.0)
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)
}
e.buf.Write(header)
// Write data size (4 bytes, little-endian)
sizeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(sizeBytes, uint32(dataSize))
e.buf.Write(sizeBytes)
// Write file type signature
e.buf.Write([]byte(".FIT"))
}
// AddActivity adds activity data to the FIT file
func (e *FitEncoder) AddActivity(activity FitActivity) error {
// TODO: Implement activity message encoding
return nil
}
// Encode returns the encoded FIT file bytes
func (e *FitEncoder) Encode() ([]byte, error) {
dataSize := e.buf.Len() - e.headerSize
e.writeHeader(dataSize)
e.writeCRC()
return e.buf.Bytes(), nil
}
// writeCRC calculates and appends the FIT CRC
func (e *FitEncoder) writeCRC() {
data := e.buf.Bytes()
crc := uint16(0)
for _, b := range data {
crc = calcCRC(crc, b)
// Write header and calculate CRC
if _, err := w.Write(header); err != nil {
return nil, err
}
crcBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(crcBytes, crc)
e.buf.Write(crcBytes)
encoder.updateCRC(header)
return encoder, nil
}
// calcCRC calculates FIT CRC
func calcCRC(crc uint16, byteVal byte) uint16 {
table := [...]uint16{
// 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,
}
tmp := table[crc & 0xF]
crc = (crc >> 4) & 0x0FFF
crc = crc ^ tmp ^ table[byteVal & 0xF]
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
}
tmp = table[crc & 0xF]
crc = (crc >> 4) & 0x0FFF
return crc ^ tmp ^ table[(byteVal >> 4) & 0xF]
}
// Calculate header CRC with clean state
e.crc = 0
e.updateCRC(header)
headerCRC := e.crc
// timestamp converts Go time to FIT timestamp
func timestamp(t time.Time) uint32 {
return uint32(t.Unix() - GarminEpochOffset)
}
// 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
}
// FitActivity represents basic activity data for FIT encoding
type FitActivity struct {
Name string
Type string
StartTime time.Time
Duration time.Duration
Distance float32 // in meters
// 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
}