43
fitparser/crc.go
Normal file
43
fitparser/crc.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package fitparser
|
||||||
|
|
||||||
|
// CRC lookup table for FIT files
|
||||||
|
var crcTable = [16]uint16{
|
||||||
|
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
|
||||||
|
0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateCRC calculates the CRC-16 checksum for FIT files
|
||||||
|
// This uses the CRC-16-ANSI algorithm with polynomial 0x8005
|
||||||
|
func CalculateCRC(data []byte) uint16 {
|
||||||
|
crc := uint16(0)
|
||||||
|
for _, b := range data {
|
||||||
|
// Compute CRC for lower nibble (bits 0-3)
|
||||||
|
tmp := crcTable[crc&0xF]
|
||||||
|
crc = (crc >> 4) & 0x0FFF
|
||||||
|
crc = crc ^ tmp ^ crcTable[b&0xF]
|
||||||
|
|
||||||
|
// Compute CRC for upper nibble (bits 4-7)
|
||||||
|
tmp = crcTable[crc&0xF]
|
||||||
|
crc = (crc >> 4) & 0x0FFF
|
||||||
|
crc = crc ^ tmp ^ crcTable[(b>>4)&0xF]
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCRC updates an existing CRC with new data
|
||||||
|
func UpdateCRC(crc uint16, data []byte) uint16 {
|
||||||
|
for _, b := range data {
|
||||||
|
// Compute CRC for lower nibble (bits 0-3)
|
||||||
|
tmp := crcTable[crc&0xF]
|
||||||
|
crc = (crc >> 4) & 0x0FFF
|
||||||
|
crc = crc ^ tmp ^ crcTable[b&0xF]
|
||||||
|
|
||||||
|
// Compute CRC for upper nibble (bits 4-7)
|
||||||
|
tmp = crcTable[crc&0xF]
|
||||||
|
crc = (crc >> 4) & 0x0FFF
|
||||||
|
crc = crc ^ tmp ^ crcTable[(b>>4)&0xF]
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
246
fitparser/decoder_test.go
Normal file
246
fitparser/decoder_test.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package fitparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsFIT(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid FIT signature",
|
||||||
|
data: []byte{0x0E, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '.', 'F', 'I', 'T', 0x00, 0x00},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid signature",
|
||||||
|
data: []byte{0x0E, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'N', 'O', 'T', 'F', 0x00, 0x00},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too short",
|
||||||
|
data: []byte{0x0E, 0x10},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := IsFIT(tt.data)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("IsFIT() = %v, want %v", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateCRC(t *testing.T) {
|
||||||
|
// Test with known CRC values
|
||||||
|
data := []byte{0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, '.', 'F', 'I', 'T'}
|
||||||
|
crc := CalculateCRC(data)
|
||||||
|
|
||||||
|
// CRC should be non-zero for this data
|
||||||
|
if crc == 0 {
|
||||||
|
t.Errorf("CalculateCRC() returned 0, expected non-zero value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that same data produces same CRC
|
||||||
|
crc2 := CalculateCRC(data)
|
||||||
|
if crc != crc2 {
|
||||||
|
t.Errorf("CalculateCRC() not deterministic: got %d and %d", crc, crc2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHeader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid 12-byte header",
|
||||||
|
data: []byte{0x0C, 0x10, 0xD9, 0x07, 0x10, 0x00, 0x00, 0x00, '.', 'F', 'I', 'T'},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too short",
|
||||||
|
data: []byte{0x0C, 0x10},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid signature",
|
||||||
|
data: []byte{0x0C, 0x10, 0xD9, 0x07, 0x10, 0x00, 0x00, 0x00, 'N', 'O', 'P', 'E'},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
header, err := ParseHeader(tt.data)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ParseHeader() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr && header == nil {
|
||||||
|
t.Errorf("ParseHeader() returned nil header without error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeActivityFile(t *testing.T) {
|
||||||
|
// Test with a real FIT file from the SDK examples
|
||||||
|
filePath := "../example/testdata/Activity.fit"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading test file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := NewDecoder(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewDecoder() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := decoder.Decode()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
t.Error("Decode() returned no messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print some statistics
|
||||||
|
t.Logf("Successfully decoded %d messages", len(messages))
|
||||||
|
|
||||||
|
// Count message types
|
||||||
|
msgCounts := make(map[uint16]int)
|
||||||
|
for _, msg := range messages {
|
||||||
|
msgCounts[msg.Num]++
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Message type breakdown:")
|
||||||
|
for mesgNum, count := range msgCounts {
|
||||||
|
t.Logf(" %s (%d): %d messages", GetMessageName(mesgNum), mesgNum, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we have essential messages
|
||||||
|
if msgCounts[MesgNumFileId] == 0 {
|
||||||
|
t.Error("Expected at least one FileId message")
|
||||||
|
}
|
||||||
|
if msgCounts[MesgNumRecord] == 0 {
|
||||||
|
t.Error("Expected at least one Record message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeSettingsFile(t *testing.T) {
|
||||||
|
filePath := "../example/testdata/Settings.fit"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading test file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := NewDecoder(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewDecoder() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := decoder.Decode()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
t.Error("Decode() returned no messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully decoded %d messages from Settings.fit", len(messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckIntegrity(t *testing.T) {
|
||||||
|
filePath := "../example/testdata/Activity.fit"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading test file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CheckIntegrity(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("CheckIntegrity() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertSemicirclesToDegrees(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
semicircles int32
|
||||||
|
expected float64
|
||||||
|
}{
|
||||||
|
{0, 0.0},
|
||||||
|
{2147483647, 180.0}, // Max positive (approximately)
|
||||||
|
{-2147483648, -180.0}, // Max negative (approximately)
|
||||||
|
{1073741824, 90.0}, // Quarter circle
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d_semicircles", tt.semicircles), func(t *testing.T) {
|
||||||
|
result := ConvertSemicirclesToDegrees(tt.semicircles)
|
||||||
|
// Allow small floating point error
|
||||||
|
if result < tt.expected-0.1 || result > tt.expected+0.1 {
|
||||||
|
t.Errorf("ConvertSemicirclesToDegrees(%d) = %f, want %f", tt.semicircles, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecode(b *testing.B) {
|
||||||
|
filePath := "../example/testdata/Activity.fit"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Error reading test file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
decoder, err := NewDecoder(data)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("NewDecoder() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = decoder.Decode()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Decode() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeNoCRC(b *testing.B) {
|
||||||
|
filePath := "../example/testdata/Activity.fit"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Error reading test file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
decoder, err := NewDecoder(data)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("NewDecoder() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder.EnableCRCCheck(false)
|
||||||
|
_, err = decoder.Decode()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Decode() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
fitparser/header.go
Normal file
110
fitparser/header.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package fitparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Header represents a FIT file header
|
||||||
|
type Header struct {
|
||||||
|
Size uint8 // Header size (12 or 14 bytes)
|
||||||
|
ProtocolVersion uint8 // Protocol version
|
||||||
|
ProfileVersion uint16 // Profile version
|
||||||
|
DataSize uint32 // Size of data records in bytes
|
||||||
|
DataType [4]byte // ".FIT" signature
|
||||||
|
CRC uint16 // CRC of header (only present if Size == 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHeader parses a FIT file header from the given data
|
||||||
|
func ParseHeader(data []byte) (*Header, error) {
|
||||||
|
if len(data) < HeaderMinSize {
|
||||||
|
return nil, fmt.Errorf("header too small: got %d bytes, need at least %d", len(data), HeaderMinSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &Header{
|
||||||
|
Size: data[0],
|
||||||
|
ProtocolVersion: data[1],
|
||||||
|
DataType: [4]byte{data[8], data[9], data[10], data[11]},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate header size
|
||||||
|
if h.Size != HeaderWithCRCSize && h.Size != HeaderWithoutCRCSize {
|
||||||
|
return nil, fmt.Errorf("invalid header size: %d (expected %d or %d)",
|
||||||
|
h.Size, HeaderWithoutCRCSize, HeaderWithCRCSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have enough data
|
||||||
|
if len(data) < int(h.Size) {
|
||||||
|
return nil, fmt.Errorf("insufficient data for header: got %d bytes, need %d", len(data), h.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse profile version (little endian)
|
||||||
|
h.ProfileVersion = binary.LittleEndian.Uint16(data[2:4])
|
||||||
|
|
||||||
|
// Parse data size (little endian)
|
||||||
|
h.DataSize = binary.LittleEndian.Uint32(data[4:8])
|
||||||
|
|
||||||
|
// Validate signature
|
||||||
|
if h.DataType != FITSignature {
|
||||||
|
return nil, fmt.Errorf("invalid FIT signature: got %s, expected .FIT", string(h.DataType[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CRC if present
|
||||||
|
if h.Size == HeaderWithCRCSize {
|
||||||
|
h.CRC = binary.LittleEndian.Uint16(data[12:14])
|
||||||
|
|
||||||
|
// Validate header CRC
|
||||||
|
calculatedCRC := CalculateCRC(data[0:12])
|
||||||
|
if calculatedCRC != h.CRC {
|
||||||
|
return nil, fmt.Errorf("header CRC mismatch: got 0x%04X, expected 0x%04X", h.CRC, calculatedCRC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation on the header
|
||||||
|
func (h *Header) Validate() error {
|
||||||
|
if h.Size != HeaderWithCRCSize && h.Size != HeaderWithoutCRCSize {
|
||||||
|
return fmt.Errorf("invalid header size: %d", h.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.DataType != FITSignature {
|
||||||
|
return fmt.Errorf("invalid FIT signature: %s", string(h.DataType[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
protocolMajor := h.ProtocolVersion >> ProtocolVersionMajorShift
|
||||||
|
if protocolMajor < 1 || protocolMajor > 2 {
|
||||||
|
return fmt.Errorf("unsupported protocol version: %d.%d",
|
||||||
|
protocolMajor, h.ProtocolVersion&ProtocolVersionMinorMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtocolVersionMajor returns the major version of the protocol
|
||||||
|
func (h *Header) ProtocolVersionMajor() uint8 {
|
||||||
|
return h.ProtocolVersion >> ProtocolVersionMajorShift
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtocolVersionMinor returns the minor version of the protocol
|
||||||
|
func (h *Header) ProtocolVersionMinor() uint8 {
|
||||||
|
return h.ProtocolVersion & ProtocolVersionMinorMask
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFIT checks if the given data starts with a valid FIT file signature
|
||||||
|
func IsFIT(data []byte) bool {
|
||||||
|
if len(data) < HeaderMinSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := [4]byte{data[8], data[9], data[10], data[11]}
|
||||||
|
return bytes.Equal(signature[:], FITSignature[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the header
|
||||||
|
func (h *Header) String() string {
|
||||||
|
return fmt.Sprintf("FIT Header: Protocol v%d.%d, Profile v%d, DataSize=%d bytes, HeaderSize=%d bytes",
|
||||||
|
h.ProtocolVersionMajor(), h.ProtocolVersionMinor(), h.ProfileVersion, h.DataSize, h.Size)
|
||||||
|
}
|
||||||
308
fitparser/message.go
Normal file
308
fitparser/message.go
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
package fitparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FieldDefinition represents a field definition in a message definition
|
||||||
|
type FieldDefinition struct {
|
||||||
|
Num uint8 // Field definition number
|
||||||
|
Size uint8 // Size in bytes
|
||||||
|
BaseType uint8 // Base type
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeveloperFieldDefinition represents a developer field definition
|
||||||
|
type DeveloperFieldDefinition struct {
|
||||||
|
Num uint8
|
||||||
|
Size uint8
|
||||||
|
DeveloperDataIndex uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageDefinition represents a message definition record
|
||||||
|
type MessageDefinition struct {
|
||||||
|
Reserved uint8
|
||||||
|
Architecture uint8 // 0 = little endian, 1 = big endian
|
||||||
|
GlobalMesgNum uint16
|
||||||
|
NumFields uint8
|
||||||
|
FieldDefinitions []FieldDefinition
|
||||||
|
NumDevFields uint8
|
||||||
|
DevFieldDefinitions []DeveloperFieldDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents a decoded FIT message
|
||||||
|
type Message struct {
|
||||||
|
Num uint16 // Global message number
|
||||||
|
Fields map[uint8]interface{} // Field number -> value
|
||||||
|
DevFields map[uint8]interface{} // Developer field number -> value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessage creates a new message
|
||||||
|
func NewMessage(num uint16) *Message {
|
||||||
|
return &Message{
|
||||||
|
Num: num,
|
||||||
|
Fields: make(map[uint8]interface{}),
|
||||||
|
DevFields: make(map[uint8]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldValue returns the value of a field by field number
|
||||||
|
func (m *Message) GetFieldValue(fieldNum uint8) (interface{}, bool) {
|
||||||
|
val, ok := m.Fields[fieldNum]
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldValueUint8 returns a field value as uint8
|
||||||
|
func (m *Message) GetFieldValueUint8(fieldNum uint8) (uint8, bool) {
|
||||||
|
if val, ok := m.Fields[fieldNum]; ok {
|
||||||
|
if v, ok := val.(uint8); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldValueUint16 returns a field value as uint16
|
||||||
|
func (m *Message) GetFieldValueUint16(fieldNum uint8) (uint16, bool) {
|
||||||
|
if val, ok := m.Fields[fieldNum]; ok {
|
||||||
|
if v, ok := val.(uint16); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldValueUint32 returns a field value as uint32
|
||||||
|
func (m *Message) GetFieldValueUint32(fieldNum uint8) (uint32, bool) {
|
||||||
|
if val, ok := m.Fields[fieldNum]; ok {
|
||||||
|
if v, ok := val.(uint32); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldValueString returns a field value as string
|
||||||
|
func (m *Message) GetFieldValueString(fieldNum uint8) (string, bool) {
|
||||||
|
if val, ok := m.Fields[fieldNum]; ok {
|
||||||
|
if v, ok := val.(string); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLittleEndian returns true if the message definition uses little endian byte order
|
||||||
|
func (md *MessageDefinition) IsLittleEndian() bool {
|
||||||
|
return md.Architecture == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeField decodes a single field value from raw bytes
|
||||||
|
func DecodeField(data []byte, fieldDef FieldDefinition, littleEndian bool) (interface{}, error) {
|
||||||
|
if len(data) < int(fieldDef.Size) {
|
||||||
|
return nil, fmt.Errorf("insufficient data for field: need %d bytes, got %d", fieldDef.Size, len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the base type number by masking (removes endian flag)
|
||||||
|
baseTypeNum := fieldDef.BaseType & BaseTypeNumMask
|
||||||
|
fieldData := data[:fieldDef.Size]
|
||||||
|
|
||||||
|
switch baseTypeNum {
|
||||||
|
case 0x00, 0x02, 0x0A, 0x0D: // Enum (0x00), Uint8 (0x02), Uint8z (0x0A), Byte (0x0D)
|
||||||
|
if fieldDef.Size == 1 {
|
||||||
|
return uint8(fieldData[0]), nil
|
||||||
|
}
|
||||||
|
// Array of bytes
|
||||||
|
result := make([]uint8, fieldDef.Size)
|
||||||
|
copy(result, fieldData)
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x01: // Sint8
|
||||||
|
if fieldDef.Size == 1 {
|
||||||
|
return int8(fieldData[0]), nil
|
||||||
|
}
|
||||||
|
// Array of int8
|
||||||
|
result := make([]int8, fieldDef.Size)
|
||||||
|
for i := range result {
|
||||||
|
result[i] = int8(fieldData[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x04, 0x0B: // Uint16 (0x84 masked), Uint16z (0x8B masked)
|
||||||
|
if fieldDef.Size == 2 {
|
||||||
|
if littleEndian {
|
||||||
|
return binary.LittleEndian.Uint16(fieldData), nil
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint16(fieldData), nil
|
||||||
|
}
|
||||||
|
// Array of uint16
|
||||||
|
count := fieldDef.Size / 2
|
||||||
|
result := make([]uint16, count)
|
||||||
|
for i := range result {
|
||||||
|
if littleEndian {
|
||||||
|
result[i] = binary.LittleEndian.Uint16(fieldData[i*2:])
|
||||||
|
} else {
|
||||||
|
result[i] = binary.BigEndian.Uint16(fieldData[i*2:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x03: // Sint16 (0x83 masked)
|
||||||
|
if fieldDef.Size == 2 {
|
||||||
|
if littleEndian {
|
||||||
|
return int16(binary.LittleEndian.Uint16(fieldData)), nil
|
||||||
|
}
|
||||||
|
return int16(binary.BigEndian.Uint16(fieldData)), nil
|
||||||
|
}
|
||||||
|
// Array of int16
|
||||||
|
count := fieldDef.Size / 2
|
||||||
|
result := make([]int16, count)
|
||||||
|
for i := range result {
|
||||||
|
if littleEndian {
|
||||||
|
result[i] = int16(binary.LittleEndian.Uint16(fieldData[i*2:]))
|
||||||
|
} else {
|
||||||
|
result[i] = int16(binary.BigEndian.Uint16(fieldData[i*2:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x06, 0x0C: // Uint32 (0x86 masked), Uint32z (0x8C masked)
|
||||||
|
if fieldDef.Size == 4 {
|
||||||
|
if littleEndian {
|
||||||
|
return binary.LittleEndian.Uint32(fieldData), nil
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(fieldData), nil
|
||||||
|
}
|
||||||
|
// Array of uint32
|
||||||
|
count := fieldDef.Size / 4
|
||||||
|
result := make([]uint32, count)
|
||||||
|
for i := range result {
|
||||||
|
if littleEndian {
|
||||||
|
result[i] = binary.LittleEndian.Uint32(fieldData[i*4:])
|
||||||
|
} else {
|
||||||
|
result[i] = binary.BigEndian.Uint32(fieldData[i*4:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x05: // Sint32 (0x85 masked)
|
||||||
|
if fieldDef.Size == 4 {
|
||||||
|
if littleEndian {
|
||||||
|
return int32(binary.LittleEndian.Uint32(fieldData)), nil
|
||||||
|
}
|
||||||
|
return int32(binary.BigEndian.Uint32(fieldData)), nil
|
||||||
|
}
|
||||||
|
// Array of int32
|
||||||
|
count := fieldDef.Size / 4
|
||||||
|
result := make([]int32, count)
|
||||||
|
for i := range result {
|
||||||
|
if littleEndian {
|
||||||
|
result[i] = int32(binary.LittleEndian.Uint32(fieldData[i*4:]))
|
||||||
|
} else {
|
||||||
|
result[i] = int32(binary.BigEndian.Uint32(fieldData[i*4:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x08: // Float32 (0x88 masked)
|
||||||
|
if fieldDef.Size == 4 {
|
||||||
|
var bits uint32
|
||||||
|
if littleEndian {
|
||||||
|
bits = binary.LittleEndian.Uint32(fieldData)
|
||||||
|
} else {
|
||||||
|
bits = binary.BigEndian.Uint32(fieldData)
|
||||||
|
}
|
||||||
|
return math.Float32frombits(bits), nil
|
||||||
|
}
|
||||||
|
// Array of float32
|
||||||
|
count := fieldDef.Size / 4
|
||||||
|
result := make([]float32, count)
|
||||||
|
for i := range result {
|
||||||
|
var bits uint32
|
||||||
|
if littleEndian {
|
||||||
|
bits = binary.LittleEndian.Uint32(fieldData[i*4:])
|
||||||
|
} else {
|
||||||
|
bits = binary.BigEndian.Uint32(fieldData[i*4:])
|
||||||
|
}
|
||||||
|
result[i] = math.Float32frombits(bits)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x09: // Float64 (0x89 masked)
|
||||||
|
if fieldDef.Size == 8 {
|
||||||
|
var bits uint64
|
||||||
|
if littleEndian {
|
||||||
|
bits = binary.LittleEndian.Uint64(fieldData)
|
||||||
|
} else {
|
||||||
|
bits = binary.BigEndian.Uint64(fieldData)
|
||||||
|
}
|
||||||
|
return math.Float64frombits(bits), nil
|
||||||
|
}
|
||||||
|
// Array of float64
|
||||||
|
count := fieldDef.Size / 8
|
||||||
|
result := make([]float64, count)
|
||||||
|
for i := range result {
|
||||||
|
var bits uint64
|
||||||
|
if littleEndian {
|
||||||
|
bits = binary.LittleEndian.Uint64(fieldData[i*8:])
|
||||||
|
} else {
|
||||||
|
bits = binary.BigEndian.Uint64(fieldData[i*8:])
|
||||||
|
}
|
||||||
|
result[i] = math.Float64frombits(bits)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x0F, 0x10: // Uint64 (0x8F masked), Uint64z (0x90 masked)
|
||||||
|
if fieldDef.Size == 8 {
|
||||||
|
if littleEndian {
|
||||||
|
return binary.LittleEndian.Uint64(fieldData), nil
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint64(fieldData), nil
|
||||||
|
}
|
||||||
|
// Array of uint64
|
||||||
|
count := fieldDef.Size / 8
|
||||||
|
result := make([]uint64, count)
|
||||||
|
for i := range result {
|
||||||
|
if littleEndian {
|
||||||
|
result[i] = binary.LittleEndian.Uint64(fieldData[i*8:])
|
||||||
|
} else {
|
||||||
|
result[i] = binary.BigEndian.Uint64(fieldData[i*8:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x0E: // Sint64 (0x8E masked)
|
||||||
|
if fieldDef.Size == 8 {
|
||||||
|
if littleEndian {
|
||||||
|
return int64(binary.LittleEndian.Uint64(fieldData)), nil
|
||||||
|
}
|
||||||
|
return int64(binary.BigEndian.Uint64(fieldData)), nil
|
||||||
|
}
|
||||||
|
// Array of int64
|
||||||
|
count := fieldDef.Size / 8
|
||||||
|
result := make([]int64, count)
|
||||||
|
for i := range result {
|
||||||
|
if littleEndian {
|
||||||
|
result[i] = int64(binary.LittleEndian.Uint64(fieldData[i*8:]))
|
||||||
|
} else {
|
||||||
|
result[i] = int64(binary.BigEndian.Uint64(fieldData[i*8:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case 0x07: // String
|
||||||
|
// String field - find null terminator
|
||||||
|
end := len(fieldData)
|
||||||
|
for i, b := range fieldData {
|
||||||
|
if b == 0 {
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(fieldData[:end]), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported base type: 0x%02X (num: 0x%02X)", fieldDef.BaseType, baseTypeNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
346
fitparser/profile.go
Normal file
346
fitparser/profile.go
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
package fitparser
|
||||||
|
|
||||||
|
// Common message numbers
|
||||||
|
const (
|
||||||
|
MesgNumFileId = 0
|
||||||
|
MesgNumCapabilities = 1
|
||||||
|
MesgNumDeviceSettings = 2
|
||||||
|
MesgNumUserProfile = 3
|
||||||
|
MesgNumHrmProfile = 4
|
||||||
|
MesgNumSdmProfile = 5
|
||||||
|
MesgNumBikeProfile = 6
|
||||||
|
MesgNumZonesTarget = 7
|
||||||
|
MesgNumHrZone = 8
|
||||||
|
MesgNumPowerZone = 9
|
||||||
|
MesgNumMetZone = 10
|
||||||
|
MesgNumSport = 12
|
||||||
|
MesgNumGoal = 15
|
||||||
|
MesgNumSession = 18
|
||||||
|
MesgNumLap = 19
|
||||||
|
MesgNumRecord = 20
|
||||||
|
MesgNumEvent = 21
|
||||||
|
MesgNumDeviceInfo = 23
|
||||||
|
MesgNumWorkout = 26
|
||||||
|
MesgNumWorkoutStep = 27
|
||||||
|
MesgNumSchedule = 28
|
||||||
|
MesgNumActivity = 34
|
||||||
|
MesgNumSoftware = 35
|
||||||
|
MesgNumFileCapabilities = 37
|
||||||
|
MesgNumMesgCapabilities = 38
|
||||||
|
MesgNumFieldCapabilities = 39
|
||||||
|
MesgNumFileCreator = 49
|
||||||
|
MesgNumBloodPressure = 51
|
||||||
|
MesgNumSpeedZone = 53
|
||||||
|
MesgNumMonitoring = 55
|
||||||
|
MesgNumHrv = 78
|
||||||
|
MesgNumLength = 101
|
||||||
|
MesgNumMonitoringInfo = 103
|
||||||
|
MesgNumPad = 105
|
||||||
|
MesgNumSegmentLap = 142
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common field numbers for FileId message
|
||||||
|
const (
|
||||||
|
FieldFileIdType = 0
|
||||||
|
FieldFileIdManufacturer = 1
|
||||||
|
FieldFileIdProduct = 2
|
||||||
|
FieldFileIdSerialNumber = 3
|
||||||
|
FieldFileIdTimeCreated = 4
|
||||||
|
FieldFileIdNumber = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common field numbers for Record message
|
||||||
|
const (
|
||||||
|
FieldRecordTimestamp = 253
|
||||||
|
FieldRecordPositionLat = 0
|
||||||
|
FieldRecordPositionLong = 1
|
||||||
|
FieldRecordAltitude = 2
|
||||||
|
FieldRecordHeartRate = 3
|
||||||
|
FieldRecordCadence = 4
|
||||||
|
FieldRecordDistance = 5
|
||||||
|
FieldRecordSpeed = 6
|
||||||
|
FieldRecordPower = 7
|
||||||
|
FieldRecordCompressedSpeedDistance = 8
|
||||||
|
FieldRecordGrade = 9
|
||||||
|
FieldRecordResistance = 10
|
||||||
|
FieldRecordTimeFromCourse = 11
|
||||||
|
FieldRecordCycleLength = 12
|
||||||
|
FieldRecordTemperature = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common field numbers for Lap message
|
||||||
|
const (
|
||||||
|
FieldLapTimestamp = 253
|
||||||
|
FieldLapEvent = 0
|
||||||
|
FieldLapEventType = 1
|
||||||
|
FieldLapStartTime = 2
|
||||||
|
FieldLapStartPositionLat = 3
|
||||||
|
FieldLapStartPositionLong = 4
|
||||||
|
FieldLapEndPositionLat = 5
|
||||||
|
FieldLapEndPositionLong = 6
|
||||||
|
FieldLapTotalElapsedTime = 7
|
||||||
|
FieldLapTotalTimerTime = 8
|
||||||
|
FieldLapTotalDistance = 9
|
||||||
|
FieldLapTotalCycles = 10
|
||||||
|
FieldLapTotalCalories = 11
|
||||||
|
FieldLapTotalFatCalories = 12
|
||||||
|
FieldLapAvgSpeed = 13
|
||||||
|
FieldLapMaxSpeed = 14
|
||||||
|
FieldLapAvgHeartRate = 15
|
||||||
|
FieldLapMaxHeartRate = 16
|
||||||
|
FieldLapAvgCadence = 17
|
||||||
|
FieldLapMaxCadence = 18
|
||||||
|
FieldLapAvgPower = 19
|
||||||
|
FieldLapMaxPower = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common field numbers for Session message
|
||||||
|
const (
|
||||||
|
FieldSessionTimestamp = 253
|
||||||
|
FieldSessionEvent = 0
|
||||||
|
FieldSessionEventType = 1
|
||||||
|
FieldSessionStartTime = 2
|
||||||
|
FieldSessionStartPositionLat = 3
|
||||||
|
FieldSessionStartPositionLong = 4
|
||||||
|
FieldSessionSport = 5
|
||||||
|
FieldSessionSubSport = 6
|
||||||
|
FieldSessionTotalElapsedTime = 7
|
||||||
|
FieldSessionTotalTimerTime = 8
|
||||||
|
FieldSessionTotalDistance = 9
|
||||||
|
FieldSessionTotalCycles = 10
|
||||||
|
FieldSessionTotalCalories = 11
|
||||||
|
FieldSessionTotalFatCalories = 13
|
||||||
|
FieldSessionAvgSpeed = 14
|
||||||
|
FieldSessionMaxSpeed = 15
|
||||||
|
FieldSessionAvgHeartRate = 16
|
||||||
|
FieldSessionMaxHeartRate = 17
|
||||||
|
FieldSessionAvgCadence = 18
|
||||||
|
FieldSessionMaxCadence = 19
|
||||||
|
FieldSessionAvgPower = 20
|
||||||
|
FieldSessionMaxPower = 21
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common field numbers for Activity message
|
||||||
|
const (
|
||||||
|
FieldActivityTimestamp = 253
|
||||||
|
FieldActivityTotalTimerTime = 0
|
||||||
|
FieldActivityNumSessions = 1
|
||||||
|
FieldActivityType = 2
|
||||||
|
FieldActivityEvent = 3
|
||||||
|
FieldActivityEventType = 4
|
||||||
|
FieldActivityLocalTimestamp = 5
|
||||||
|
FieldActivityEventGroup = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// File types
|
||||||
|
const (
|
||||||
|
FileTypeDevice = 1
|
||||||
|
FileTypeSettings = 2
|
||||||
|
FileTypeSport = 3
|
||||||
|
FileTypeActivity = 4
|
||||||
|
FileTypeWorkout = 5
|
||||||
|
FileTypeCourse = 6
|
||||||
|
FileTypeSchedules = 7
|
||||||
|
FileTypeWeight = 9
|
||||||
|
FileTypeTotals = 10
|
||||||
|
FileTypeGoals = 11
|
||||||
|
FileTypeBloodPressure = 14
|
||||||
|
FileTypeMonitoringA = 15
|
||||||
|
FileTypeActivitySummary = 20
|
||||||
|
FileTypeMonitoringDaily = 28
|
||||||
|
FileTypeMonitoringB = 32
|
||||||
|
FileTypeSegment = 34
|
||||||
|
FileTypeSegmentList = 35
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sport types
|
||||||
|
const (
|
||||||
|
SportGeneric = 0
|
||||||
|
SportRunning = 1
|
||||||
|
SportCycling = 2
|
||||||
|
SportTransition = 3
|
||||||
|
SportFitnessEquipment = 4
|
||||||
|
SportSwimming = 5
|
||||||
|
SportBasketball = 6
|
||||||
|
SportSoccer = 7
|
||||||
|
SportTennis = 8
|
||||||
|
SportAmericanFootball = 9
|
||||||
|
SportTraining = 10
|
||||||
|
SportWalking = 11
|
||||||
|
SportCrossCountrySkiing = 12
|
||||||
|
SportAlpineSkiing = 13
|
||||||
|
SportSnowboarding = 14
|
||||||
|
SportRowing = 15
|
||||||
|
SportMountaineering = 16
|
||||||
|
SportHiking = 17
|
||||||
|
SportMultisport = 18
|
||||||
|
SportPaddling = 19
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMessageName returns a human-readable name for a message number
|
||||||
|
func GetMessageName(mesgNum uint16) string {
|
||||||
|
switch mesgNum {
|
||||||
|
case MesgNumFileId:
|
||||||
|
return "FileId"
|
||||||
|
case MesgNumCapabilities:
|
||||||
|
return "Capabilities"
|
||||||
|
case MesgNumDeviceSettings:
|
||||||
|
return "DeviceSettings"
|
||||||
|
case MesgNumUserProfile:
|
||||||
|
return "UserProfile"
|
||||||
|
case MesgNumHrmProfile:
|
||||||
|
return "HrmProfile"
|
||||||
|
case MesgNumSdmProfile:
|
||||||
|
return "SdmProfile"
|
||||||
|
case MesgNumBikeProfile:
|
||||||
|
return "BikeProfile"
|
||||||
|
case MesgNumZonesTarget:
|
||||||
|
return "ZonesTarget"
|
||||||
|
case MesgNumHrZone:
|
||||||
|
return "HrZone"
|
||||||
|
case MesgNumPowerZone:
|
||||||
|
return "PowerZone"
|
||||||
|
case MesgNumMetZone:
|
||||||
|
return "MetZone"
|
||||||
|
case MesgNumSport:
|
||||||
|
return "Sport"
|
||||||
|
case MesgNumGoal:
|
||||||
|
return "Goal"
|
||||||
|
case MesgNumSession:
|
||||||
|
return "Session"
|
||||||
|
case MesgNumLap:
|
||||||
|
return "Lap"
|
||||||
|
case MesgNumRecord:
|
||||||
|
return "Record"
|
||||||
|
case MesgNumEvent:
|
||||||
|
return "Event"
|
||||||
|
case MesgNumDeviceInfo:
|
||||||
|
return "DeviceInfo"
|
||||||
|
case MesgNumActivity:
|
||||||
|
return "Activity"
|
||||||
|
case MesgNumSoftware:
|
||||||
|
return "Software"
|
||||||
|
case MesgNumFileCapabilities:
|
||||||
|
return "FileCapabilities"
|
||||||
|
case MesgNumMesgCapabilities:
|
||||||
|
return "MesgCapabilities"
|
||||||
|
case MesgNumFieldCapabilities:
|
||||||
|
return "FieldCapabilities"
|
||||||
|
case MesgNumFileCreator:
|
||||||
|
return "FileCreator"
|
||||||
|
case MesgNumBloodPressure:
|
||||||
|
return "BloodPressure"
|
||||||
|
case MesgNumSpeedZone:
|
||||||
|
return "SpeedZone"
|
||||||
|
case MesgNumMonitoring:
|
||||||
|
return "Monitoring"
|
||||||
|
case MesgNumHrv:
|
||||||
|
return "Hrv"
|
||||||
|
case MesgNumLength:
|
||||||
|
return "Length"
|
||||||
|
case MesgNumMonitoringInfo:
|
||||||
|
return "MonitoringInfo"
|
||||||
|
case MesgNumPad:
|
||||||
|
return "Pad"
|
||||||
|
case MesgNumSegmentLap:
|
||||||
|
return "SegmentLap"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileTypeName returns a human-readable name for a file type
|
||||||
|
func GetFileTypeName(fileType uint8) string {
|
||||||
|
switch fileType {
|
||||||
|
case FileTypeDevice:
|
||||||
|
return "Device"
|
||||||
|
case FileTypeSettings:
|
||||||
|
return "Settings"
|
||||||
|
case FileTypeSport:
|
||||||
|
return "Sport"
|
||||||
|
case FileTypeActivity:
|
||||||
|
return "Activity"
|
||||||
|
case FileTypeWorkout:
|
||||||
|
return "Workout"
|
||||||
|
case FileTypeCourse:
|
||||||
|
return "Course"
|
||||||
|
case FileTypeSchedules:
|
||||||
|
return "Schedules"
|
||||||
|
case FileTypeWeight:
|
||||||
|
return "Weight"
|
||||||
|
case FileTypeTotals:
|
||||||
|
return "Totals"
|
||||||
|
case FileTypeGoals:
|
||||||
|
return "Goals"
|
||||||
|
case FileTypeBloodPressure:
|
||||||
|
return "BloodPressure"
|
||||||
|
case FileTypeMonitoringA:
|
||||||
|
return "MonitoringA"
|
||||||
|
case FileTypeActivitySummary:
|
||||||
|
return "ActivitySummary"
|
||||||
|
case FileTypeMonitoringDaily:
|
||||||
|
return "MonitoringDaily"
|
||||||
|
case FileTypeMonitoringB:
|
||||||
|
return "MonitoringB"
|
||||||
|
case FileTypeSegment:
|
||||||
|
return "Segment"
|
||||||
|
case FileTypeSegmentList:
|
||||||
|
return "SegmentList"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSportName returns a human-readable name for a sport type
|
||||||
|
func GetSportName(sport uint8) string {
|
||||||
|
switch sport {
|
||||||
|
case SportGeneric:
|
||||||
|
return "Generic"
|
||||||
|
case SportRunning:
|
||||||
|
return "Running"
|
||||||
|
case SportCycling:
|
||||||
|
return "Cycling"
|
||||||
|
case SportTransition:
|
||||||
|
return "Transition"
|
||||||
|
case SportFitnessEquipment:
|
||||||
|
return "FitnessEquipment"
|
||||||
|
case SportSwimming:
|
||||||
|
return "Swimming"
|
||||||
|
case SportBasketball:
|
||||||
|
return "Basketball"
|
||||||
|
case SportSoccer:
|
||||||
|
return "Soccer"
|
||||||
|
case SportTennis:
|
||||||
|
return "Tennis"
|
||||||
|
case SportAmericanFootball:
|
||||||
|
return "AmericanFootball"
|
||||||
|
case SportTraining:
|
||||||
|
return "Training"
|
||||||
|
case SportWalking:
|
||||||
|
return "Walking"
|
||||||
|
case SportCrossCountrySkiing:
|
||||||
|
return "CrossCountrySkiing"
|
||||||
|
case SportAlpineSkiing:
|
||||||
|
return "AlpineSkiing"
|
||||||
|
case SportSnowboarding:
|
||||||
|
return "Snowboarding"
|
||||||
|
case SportRowing:
|
||||||
|
return "Rowing"
|
||||||
|
case SportMountaineering:
|
||||||
|
return "Mountaineering"
|
||||||
|
case SportHiking:
|
||||||
|
return "Hiking"
|
||||||
|
case SportMultisport:
|
||||||
|
return "Multisport"
|
||||||
|
case SportPaddling:
|
||||||
|
return "Paddling"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertSemicirclesToDegrees converts a position in semicircles to degrees
|
||||||
|
// FIT files store lat/long as semicircles (2^31 semicircles = 180 degrees)
|
||||||
|
func ConvertSemicirclesToDegrees(semicircles int32) float64 {
|
||||||
|
return float64(semicircles) * (180.0 / 2147483648.0)
|
||||||
|
}
|
||||||
182
fitparser/types.go
Normal file
182
fitparser/types.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Package fitparser provides functionality to decode FIT (Flexible and Interoperable Data Transfer) files.
|
||||||
|
// FIT is a binary file format used by Garmin and other fitness device manufacturers.
|
||||||
|
package fitparser
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Protocol and Profile versions
|
||||||
|
const (
|
||||||
|
ProtocolVersionMajorShift = 4
|
||||||
|
ProtocolVersionMajorMask = 0xF0
|
||||||
|
ProtocolVersionMinorMask = 0x0F
|
||||||
|
ProtocolVersion10 = 0x10
|
||||||
|
ProtocolVersion20 = 0x20
|
||||||
|
ProfileVersionMajor = 21
|
||||||
|
ProfileVersionMinor = 188
|
||||||
|
)
|
||||||
|
|
||||||
|
// Base type definitions
|
||||||
|
const (
|
||||||
|
BaseTypeEnum = 0x00
|
||||||
|
BaseTypeSint8 = 0x01
|
||||||
|
BaseTypeUint8 = 0x02
|
||||||
|
BaseTypeSint16 = 0x83
|
||||||
|
BaseTypeUint16 = 0x84
|
||||||
|
BaseTypeSint32 = 0x85
|
||||||
|
BaseTypeUint32 = 0x86
|
||||||
|
BaseTypeString = 0x07
|
||||||
|
BaseTypeFloat32 = 0x88
|
||||||
|
BaseTypeFloat64 = 0x89
|
||||||
|
BaseTypeUint8z = 0x0A
|
||||||
|
BaseTypeUint16z = 0x8B
|
||||||
|
BaseTypeUint32z = 0x8C
|
||||||
|
BaseTypeByte = 0x0D
|
||||||
|
BaseTypeSint64 = 0x8E
|
||||||
|
BaseTypeUint64 = 0x8F
|
||||||
|
BaseTypeUint64z = 0x90
|
||||||
|
)
|
||||||
|
|
||||||
|
// Base type flags
|
||||||
|
const (
|
||||||
|
BaseTypeEndianFlag = 0x80
|
||||||
|
BaseTypeNumMask = 0x1F
|
||||||
|
)
|
||||||
|
|
||||||
|
// Invalid values for each type
|
||||||
|
const (
|
||||||
|
EnumInvalid = 0xFF
|
||||||
|
Sint8Invalid = 0x7F
|
||||||
|
Uint8Invalid = 0xFF
|
||||||
|
Sint16Invalid = 0x7FFF
|
||||||
|
Uint16Invalid = 0xFFFF
|
||||||
|
Sint32Invalid = 0x7FFFFFFF
|
||||||
|
Uint32Invalid = 0xFFFFFFFF
|
||||||
|
StringInvalid = 0x00
|
||||||
|
Float32Invalid = 0xFFFFFFFF
|
||||||
|
Float64Invalid = 0xFFFFFFFFFFFFFFFF
|
||||||
|
Uint8zInvalid = 0x00
|
||||||
|
Uint16zInvalid = 0x0000
|
||||||
|
Uint32zInvalid = 0x00000000
|
||||||
|
ByteInvalid = 0xFF
|
||||||
|
Sint64Invalid = 0x7FFFFFFFFFFFFFFF
|
||||||
|
Uint64Invalid = 0xFFFFFFFFFFFFFFFF
|
||||||
|
Uint64zInvalid = 0x0000000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Header sizes
|
||||||
|
const (
|
||||||
|
HeaderWithCRCSize = 14
|
||||||
|
HeaderWithoutCRCSize = 12
|
||||||
|
HeaderMinSize = 12
|
||||||
|
CRCSize = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Record header bits
|
||||||
|
const (
|
||||||
|
RecordHeaderMask = 0x80
|
||||||
|
RecordHeaderNormal = 0x00
|
||||||
|
RecordHeaderCompressedMask = 0x80
|
||||||
|
RecordHeaderLocalMesgNumMask = 0x0F
|
||||||
|
RecordHeaderTimeMask = 0x1F
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message definition bits
|
||||||
|
const (
|
||||||
|
MesgDefinitionMask = 0x40
|
||||||
|
MesgHeaderMask = 0xF0
|
||||||
|
LocalMesgNum = 0x0F
|
||||||
|
MesgDefinitionReserved = 0x00
|
||||||
|
DevDataMask = 0x20
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIT file signature
|
||||||
|
var FITSignature = [4]byte{'.', 'F', 'I', 'T'}
|
||||||
|
|
||||||
|
// FIT Epoch - Number of seconds between Unix Epoch and FIT Epoch (Dec 31, 1989 00:00:00 UTC)
|
||||||
|
const FITEpochSeconds = 631065600
|
||||||
|
|
||||||
|
// ConvertFITTimestamp converts a FIT timestamp to a Go time.Time
|
||||||
|
func ConvertFITTimestamp(fitTime uint32) time.Time {
|
||||||
|
if fitTime == Uint32Invalid {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return time.Unix(int64(fitTime)+FITEpochSeconds, 0).UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseTypeSize returns the size in bytes of the given base type
|
||||||
|
func BaseTypeSize(baseType byte) int {
|
||||||
|
switch baseType & BaseTypeNumMask {
|
||||||
|
case BaseTypeEnum, BaseTypeSint8, BaseTypeUint8, BaseTypeByte, BaseTypeUint8z:
|
||||||
|
return 1
|
||||||
|
case BaseTypeSint16, BaseTypeUint16, BaseTypeUint16z:
|
||||||
|
return 2
|
||||||
|
case BaseTypeSint32, BaseTypeUint32, BaseTypeUint32z, BaseTypeFloat32:
|
||||||
|
return 4
|
||||||
|
case BaseTypeSint64, BaseTypeUint64, BaseTypeUint64z, BaseTypeFloat64:
|
||||||
|
return 8
|
||||||
|
case BaseTypeString:
|
||||||
|
return 1 // Variable length, but each character is 1 byte
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEndianType returns true if the base type requires endian conversion
|
||||||
|
func IsEndianType(baseType byte) bool {
|
||||||
|
return (baseType & BaseTypeEndianFlag) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInvalidValue checks if a value is invalid for its type
|
||||||
|
func IsInvalidValue(baseType byte, value interface{}) bool {
|
||||||
|
switch baseType & BaseTypeNumMask {
|
||||||
|
case BaseTypeEnum, BaseTypeUint8:
|
||||||
|
if v, ok := value.(uint8); ok {
|
||||||
|
return v == Uint8Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeSint8:
|
||||||
|
if v, ok := value.(int8); ok {
|
||||||
|
return v == Sint8Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint16:
|
||||||
|
if v, ok := value.(uint16); ok {
|
||||||
|
return v == Uint16Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeSint16:
|
||||||
|
if v, ok := value.(int16); ok {
|
||||||
|
return v == Sint16Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint32:
|
||||||
|
if v, ok := value.(uint32); ok {
|
||||||
|
return v == Uint32Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeSint32:
|
||||||
|
if v, ok := value.(int32); ok {
|
||||||
|
return v == Sint32Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint64:
|
||||||
|
if v, ok := value.(uint64); ok {
|
||||||
|
return v == Uint64Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeSint64:
|
||||||
|
if v, ok := value.(int64); ok {
|
||||||
|
return v == Sint64Invalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint8z:
|
||||||
|
if v, ok := value.(uint8); ok {
|
||||||
|
return v == Uint8zInvalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint16z:
|
||||||
|
if v, ok := value.(uint16); ok {
|
||||||
|
return v == Uint16zInvalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint32z:
|
||||||
|
if v, ok := value.(uint32); ok {
|
||||||
|
return v == Uint32zInvalid
|
||||||
|
}
|
||||||
|
case BaseTypeUint64z:
|
||||||
|
if v, ok := value.(uint64); ok {
|
||||||
|
return v == Uint64zInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user