1
1
mirror of https://github.com/wader/fq.git synced 2024-10-26 20:06:29 +03:00

fit: Added support for "invalid" value checking.

Added support for array values.
Added documentation and tests.
This commit is contained in:
Mikael Lofjärd 2024-02-07 23:16:13 +01:00
parent 46dbf5b7da
commit 54c6f0cdc1
18 changed files with 141176 additions and 1802 deletions

14
format/fit/README.md Normal file
View File

@ -0,0 +1,14 @@
### Generated files
- format/fit/mappers/messages_generated.go
- format/fit/mappers/types_generated.go
### How to generate them if needed
1. Download the Fit SDK from: https://developer.garmin.com/fit/download/
2. Install NodeJS and NPM
3. Go to the `format/fit/testdata/generator` folder.
4. Run `npm install` if it's your first time
5. Run `node index.js t /PathToSDK/Profile.xlsx > ../../mappers/types_generated.go`
6. Run `node index.js m /PathToSDK/Profile.xlsx > ../../mappers/messages_generated.go`
7. Edit `messages_generated.go` and remove the incorrect "Scale" from line ~461
8. Correct spelling of farenheit->fahrenheit and bondary->boundary to please Go linter

View File

@ -10,9 +10,9 @@ import (
"github.com/wader/fq/pkg/decode"
"github.com/wader/fq/pkg/interp"
"github.com/wader/fq/pkg/scalar"
"golang.org/x/text/encoding"
)
//go:embed fit.md
var fitFS embed.FS
func init() {
@ -23,6 +23,7 @@ func init() {
Groups: []*decode.Group{format.Probe},
DecodeFn: decodeFIT,
})
interp.RegisterFS(fitFS)
}
@ -36,27 +37,27 @@ func calcCRC(bytes []byte) uint16 {
crc = 0
for i := 0; i < len(bytes); i++ {
// compute checksum of lower four bits of byte
var byte = bytes[i]
var checkByte = bytes[i]
var tmp = fitCRCTable[crc&0xF]
crc = (crc >> 4) & 0x0FFF
crc = crc ^ tmp ^ fitCRCTable[byte&0xF]
crc = crc ^ tmp ^ fitCRCTable[checkByte&0xF]
tmp = fitCRCTable[crc&0xF]
crc = (crc >> 4) & 0x0FFF
crc = crc ^ tmp ^ fitCRCTable[(byte>>4)&0xF]
crc = crc ^ tmp ^ fitCRCTable[(checkByte>>4)&0xF]
}
return crc
}
type fitContext struct {
dataSize int
headerSize int
dataSize uint64
headerSize uint64
}
type dataRecordContext struct {
compressed bool
data bool
localMessageType int
localMessageType uint64
hasDeveloperFields bool
}
@ -70,24 +71,13 @@ type fileDescriptionContext struct {
nativeMsgNo uint64
}
type fieldDef struct {
name string
typ string
format string
unit string
scale float64
offset int64
size int
}
type devFieldDefMap map[uint64]map[uint64]fieldDef
type localFieldDefMap map[uint64]map[uint64]fieldDef
type devFieldDefMap map[uint64]map[uint64]mappers.FieldDef
type localFieldDefMap map[uint64]map[uint64]mappers.FieldDef
type localMsgIsDevDef map[uint64]bool
func fitDecodeFileHeader(d *decode.D, fc *fitContext) {
frameStart := d.Pos()
// d.FieldStruct("ident", func(d *decode.D) {
headerSize := d.FieldU8("headerSize")
d.FieldU8("protocolVersion")
d.FieldU16("profileVersion")
@ -98,28 +88,27 @@ func fitDecodeFileHeader(d *decode.D, fc *fitContext) {
headerCRC := calcCRC(d.BytesRange(frameStart, int(headerSize)-2))
d.FieldU16("crc", d.UintValidate(uint64(headerCRC)))
}
fc.headerSize = int(headerSize)
fc.dataSize = int(dataSize)
fc.headerSize = headerSize
fc.dataSize = dataSize
}
func fitDecodeDataRecordHeader(d *decode.D, drc *dataRecordContext) {
drc.compressed = d.FieldBool("normalHeader", scalar.BoolMapDescription{false: "Normal header",
true: "Compressed header"})
headerType := d.FieldU1("headerType", scalar.UintMapDescription{0: "Normal header", 1: "Compressed header"})
drc.compressed = headerType == 1
if drc.compressed {
localMessageType := d.FieldU2("localMessageType")
d.FieldU32("timeOffset")
drc.localMessageType = int(localMessageType)
drc.localMessageType = localMessageType
drc.data = true
} else {
mTypeIsDef := d.FieldBool("messageType", scalar.BoolMap{true: {Sym: 1, Description: "Definition message"},
false: {Sym: 0, Description: "Data message"}})
mTypeIsDef := d.FieldU1("messageType", scalar.UintMapDescription{0: "Data message", 1: "Definition message"})
hasDeveloperFields := d.FieldBool("hasDeveloperFields")
d.FieldBool("reserved")
localMessageType := d.FieldU4("localMessageType")
drc.hasDeveloperFields = hasDeveloperFields
drc.localMessageType = int(localMessageType)
drc.data = !mTypeIsDef
drc.localMessageType = localMessageType
drc.data = mTypeIsDef == 0
}
}
@ -136,14 +125,14 @@ func fitDecodeDefinitionMessage(d *decode.D, drc *dataRecordContext, lmfd localF
}
messageNo := d.FieldU16("globalMessageNumber", mappers.TypeDefMap["mesg_num"])
if messageNo == 206 { // developer field_description
isDevMap[uint64(drc.localMessageType)] = true
isDevMap[drc.localMessageType] = true
} else {
isDevMap[uint64(drc.localMessageType)] = false
isDevMap[drc.localMessageType] = false
}
numFields := d.FieldU8("fields")
lmfd[uint64(drc.localMessageType)] = make(map[uint64]fieldDef, numFields)
lmfd[drc.localMessageType] = make(map[uint64]mappers.FieldDef, numFields)
d.FieldArray("fieldDefinitions", func(d *decode.D) {
for i := 0; i < int(numFields); i++ {
for i := uint64(0); i < numFields; i++ {
d.FieldStruct("fieldDefinition", func(d *decode.D) {
fieldDefNo := d.FieldU8("fieldDefNo", mappers.FieldDefMap[messageNo])
size := d.FieldU8("size")
@ -153,10 +142,10 @@ func fitDecodeDefinitionMessage(d *decode.D, drc *dataRecordContext, lmfd localF
fDefLookup, isSet := mappers.FieldDefMap[messageNo][fieldDefNo]
if isSet {
var foundName = fDefLookup.Name
lmfd[uint64(drc.localMessageType)][uint64(i)] = fieldDef{name: foundName, typ: typ, size: int(size), format: fDefLookup.Type, unit: fDefLookup.Unit, scale: fDefLookup.Scale, offset: fDefLookup.Offset}
lmfd[drc.localMessageType][i] = mappers.FieldDef{Name: foundName, Type: typ, Size: size, Format: fDefLookup.Type, Unit: fDefLookup.Unit, Scale: fDefLookup.Scale, Offset: fDefLookup.Offset}
} else {
var foundName = fmt.Sprintf("UNKOWN_%d", fieldDefNo)
lmfd[uint64(drc.localMessageType)][uint64(i)] = fieldDef{name: foundName, typ: typ, size: int(size), format: "unknown"}
var foundName = fmt.Sprintf("UNKNOWN_%d", fieldDefNo)
lmfd[drc.localMessageType][i] = mappers.FieldDef{Name: foundName, Type: typ, Size: size, Format: "unknown"}
}
})
}
@ -171,18 +160,14 @@ func fitDecodeDefinitionMessage(d *decode.D, drc *dataRecordContext, lmfd localF
size := d.FieldU8("size")
devDataIdx := d.FieldU8("devDataIdx")
//baseType := d.FieldU8("baseType", mappers.TypeDefMap["fit_base_type"])
//var typ = mappers.TypeDefMap["fit_base_type"][baseType].Name
typ := dmfd[devDataIdx][fieldDefNo].typ
//fDefLookup, isSet := mappers.FieldDefMap[messageNo][fieldDefNo]
fDefLookup, isSet := dmfd[devDataIdx][fieldDefNo]
if isSet {
var foundName = fDefLookup.name
lmfd[uint64(drc.localMessageType)][uint64(i)] = fieldDef{name: foundName, typ: typ, size: int(size), unit: fDefLookup.unit, scale: fDefLookup.scale, offset: fDefLookup.offset}
var foundName = fDefLookup.Name
lmfd[drc.localMessageType][i] = mappers.FieldDef{Name: foundName, Type: fDefLookup.Type, Size: size, Unit: fDefLookup.Unit, Scale: fDefLookup.Scale, Offset: fDefLookup.Offset}
} else {
var foundName = fmt.Sprintf("UNKOWN_%d", fieldDefNo)
lmfd[uint64(drc.localMessageType)][uint64(i)] = fieldDef{name: foundName, typ: typ, size: int(size), format: "unknown"}
var foundName = fmt.Sprintf("UNKNOWN_%d", fieldDefNo)
lmfd[drc.localMessageType][i] = mappers.FieldDef{Name: foundName, Type: "UNKNOWN", Size: size, Format: "unknown"}
}
})
}
@ -191,25 +176,31 @@ func fitDecodeDefinitionMessage(d *decode.D, drc *dataRecordContext, lmfd localF
}
func ensureDevFieldMap(dmfd devFieldDefMap, devIdx uint64, fieldDefNo uint64) {
func ensureDevFieldMap(dmfd devFieldDefMap, devIdx uint64) {
_, devIsSet := dmfd[devIdx]
if !devIsSet {
dmfd[devIdx] = make(map[uint64]fieldDef)
dmfd[devIdx] = make(map[uint64]mappers.FieldDef)
}
}
func fieldUint(fieldFn func(string, ...scalar.UintMapper) uint64, d *decode.D, fdc *fileDescriptionContext, expectedSize int, fDef fieldDef, uintFormatter scalar.UintFn) {
func fieldUint(fieldFn func(string, ...scalar.UintMapper) uint64, expectedSize uint64, fDef mappers.FieldDef, uintFormatter scalar.UintFn, fdc *fileDescriptionContext) {
var val uint64
if fDef.size != expectedSize {
d.FieldStr(fDef.name, fDef.size, encoding.Nop)
if fDef.Size != expectedSize {
arrayCount := fDef.Size / expectedSize
for i := uint64(0); i < arrayCount; i++ {
fieldFn(fmt.Sprintf("%s_%d", fDef.Name, i), uintFormatter)
}
} else {
if uintFormatter != nil {
val = fieldFn(fDef.name, uintFormatter)
val = fieldFn(fDef.Name, uintFormatter)
} else {
val = fieldFn(fDef.name)
val = fieldFn(fDef.Name)
}
switch fDef.name {
// Save developer field definitions
switch fDef.Name {
case "developer_data_index":
fdc.devIdx = val
case "field_definition_number":
@ -224,35 +215,37 @@ func fieldUint(fieldFn func(string, ...scalar.UintMapper) uint64, d *decode.D, f
}
}
func fieldSint(fieldFn func(string, ...scalar.SintMapper) int64, d *decode.D, fdc *fileDescriptionContext, expectedSize int, fDef fieldDef, sintFormatter scalar.SintFn) {
//var val uint64
if fDef.size != expectedSize {
d.FieldStr(fDef.name, fDef.size, encoding.Nop)
func fieldSint(fieldFn func(string, ...scalar.SintMapper) int64, expectedSize uint64, fDef mappers.FieldDef, sintFormatter scalar.SintFn) {
if fDef.Size != expectedSize {
arrayCount := fDef.Size / expectedSize
for i := uint64(0); i < arrayCount; i++ {
fieldFn(fmt.Sprintf("%s_%d", fDef.Name, i), sintFormatter)
}
} else {
if sintFormatter != nil {
fieldFn(fDef.name, sintFormatter)
fieldFn(fDef.Name, sintFormatter)
} else {
fieldFn(fDef.name)
fieldFn(fDef.Name)
}
//setDevFieldUint(isDevDep, dmfd, fDef.name, val);
}
}
func fieldFloat(fieldFn func(string, ...scalar.FltMapper) float64, d *decode.D, fdc *fileDescriptionContext, expectedSize int, fDef fieldDef) {
//var val uint64
if fDef.size != expectedSize {
d.FieldStr(fDef.name, fDef.size, encoding.Nop)
func fieldFloat(fieldFn func(string, ...scalar.FltMapper) float64, expectedSize uint64, fDef mappers.FieldDef, floatFormatter scalar.FltFn) {
if fDef.Size != expectedSize {
arrayCount := fDef.Size / expectedSize
for i := uint64(0); i < arrayCount; i++ {
fieldFn(fmt.Sprintf("%s_%d", fDef.Name, i), floatFormatter)
}
} else {
fieldFn(fDef.name)
//setDevFieldUint(isDevDep, dmfd, fDef.name, val);
fieldFn(fDef.Name)
}
}
func fieldString(d *decode.D, fdc *fileDescriptionContext, fDef fieldDef) {
val := d.FieldUTF8NullFixedLen(fDef.name, fDef.size)
func fieldString(d *decode.D, fDef mappers.FieldDef, fdc *fileDescriptionContext) {
val := d.FieldUTF8NullFixedLen(fDef.Name, int(fDef.Size), scalar.StrMapSymStr{"": "[invalid]"})
switch fDef.name {
// Save developer field definitions
switch fDef.Name {
case "field_name":
fdc.name = val
case "units":
@ -262,55 +255,54 @@ func fieldString(d *decode.D, fdc *fileDescriptionContext, fDef fieldDef) {
func fitDecodeDataMessage(d *decode.D, drc *dataRecordContext, lmfd localFieldDefMap, dmfd devFieldDefMap, isDevMap localMsgIsDevDef) {
var fdc fileDescriptionContext
keys := make([]int, len(lmfd[uint64(drc.localMessageType)]))
keys := make([]int, len(lmfd[drc.localMessageType]))
i := 0
for k := range lmfd[uint64(drc.localMessageType)] {
for k := range lmfd[drc.localMessageType] {
keys[i] = int(k)
i++
}
sort.Ints(keys)
isDevDep := isDevMap[uint64(drc.localMessageType)]
isDevDep := isDevMap[drc.localMessageType]
for _, k := range keys {
fDef := lmfd[uint64(drc.localMessageType)][uint64(k)]
fDef := lmfd[drc.localMessageType][uint64(k)]
var uintFormatter = mappers.GetUintFormatter(fDef.format, fDef.unit, fDef.scale, fDef.offset)
var sintFormatter = mappers.GetSintFormatter(fDef.format, fDef.unit, fDef.scale, fDef.offset)
var uintFormatter = mappers.GetUintFormatter(fDef)
var sintFormatter = mappers.GetSintFormatter(fDef)
var floatFormatter = mappers.GetFloatFormatter(fDef)
switch fDef.typ {
// case "byte":
// d.FieldStr(fDef.name, fDef.size, encoding.Nop)
switch fDef.Type {
case "enum", "uint8", "uint8z", "byte":
fieldUint(d.FieldU8, d, &fdc, 1, fDef, uintFormatter)
case "sint8":
fieldSint(d.FieldS8, d, &fdc, 1, fDef, sintFormatter)
case "sint16":
fieldSint(d.FieldS16, d, &fdc, 2, fDef, sintFormatter)
fieldUint(d.FieldU8, 1, fDef, uintFormatter, &fdc)
case "uint16", "uint16z":
fieldUint(d.FieldU16, d, &fdc, 2, fDef, uintFormatter)
case "sint32":
fieldSint(d.FieldS32, d, &fdc, 4, fDef, sintFormatter)
fieldUint(d.FieldU16, 2, fDef, uintFormatter, &fdc)
case "uint32", "uint32z":
fieldUint(d.FieldU32, d, &fdc, 4, fDef, uintFormatter)
case "float32":
fieldFloat(d.FieldF32, d, &fdc, 4, fDef)
case "float64":
fieldFloat(d.FieldF64, d, &fdc, 8, fDef)
case "sint64":
fieldSint(d.FieldS64, d, &fdc, 4, fDef, sintFormatter)
fieldUint(d.FieldU32, 4, fDef, uintFormatter, &fdc)
case "uint64", "uint64z":
fieldUint(d.FieldU64, d, &fdc, 8, fDef, uintFormatter)
fieldUint(d.FieldU64, 8, fDef, uintFormatter, &fdc)
case "sint8":
fieldSint(d.FieldS8, 1, fDef, sintFormatter)
case "sint16":
fieldSint(d.FieldS16, 2, fDef, sintFormatter)
case "sint32":
fieldSint(d.FieldS32, 4, fDef, sintFormatter)
case "sint64":
fieldSint(d.FieldS64, 8, fDef, sintFormatter)
case "float32":
fieldFloat(d.FieldF32, 4, fDef, floatFormatter)
case "float64":
fieldFloat(d.FieldF64, 8, fDef, floatFormatter)
case "string":
fieldString(d, &fdc, fDef)
fieldString(d, fDef, &fdc)
default:
d.Fatalf("Unknown type %s", fDef.typ)
d.Fatalf("Unknown type %s", fDef.Type)
}
}
if isDevDep {
ensureDevFieldMap(dmfd, fdc.devIdx, fdc.fDefNo)
dmfd[fdc.devIdx][fdc.fDefNo] = fieldDef{name: fdc.name, typ: fdc.typ, unit: fdc.unit, scale: 0, offset: 0}
ensureDevFieldMap(dmfd, fdc.devIdx)
dmfd[fdc.devIdx][fdc.fDefNo] = mappers.FieldDef{Name: fdc.name, Type: fdc.typ, Unit: fdc.unit, Scale: 0, Offset: 0}
}
}
@ -322,12 +314,9 @@ func decodeFIT(d *decode.D) any {
var dmfd devFieldDefMap = make(devFieldDefMap)
var isDevMap localMsgIsDevDef = make(localMsgIsDevDef)
//decodeBSONDocument(d)
d.FieldStruct("header", func(d *decode.D) { fitDecodeFileHeader(d, &fc) })
d.FieldArray("dataRecords", func(d *decode.D) {
// // headerPos := d.Pos()
for d.Pos() < int64((fc.headerSize+fc.dataSize)*8) {
d.FieldStruct("dataRecord", func(d *decode.D) {
var drc dataRecordContext
@ -344,9 +333,9 @@ func decodeFIT(d *decode.D) any {
var fileCRC uint16
if fc.headerSize == 12 {
fileCRC = calcCRC(d.BytesRange(0, fc.dataSize+fc.headerSize)) // 12 byte header - CRC whole file except the CRC itself
fileCRC = calcCRC(d.BytesRange(0, int(fc.dataSize+fc.headerSize))) // 12 byte header - CRC whole file except the CRC itself
} else {
fileCRC = calcCRC(d.BytesRange(14*8, fc.dataSize)) // 14 byte header - CRC everything below header except the CRC itself
fileCRC = calcCRC(d.BytesRange(14*8, int(fc.dataSize))) // 14 byte header - CRC everything below header except the CRC itself
}
d.FieldU16("crc", d.UintValidate(uint64(fileCRC)))

18
format/fit/fit.md Normal file
View File

@ -0,0 +1,18 @@
### Limitations
- "compressed_speed_distance" field on globalMessageNumber 20 is not represented correctly.
The field is read as 3 separate bytes where the first 12 bits are speed and the last 12 bits are distance.
- There are still lots of UNKOWN fields due to gaps in Garmins SDK Profile documentation. (Currently FIT SDK 21.126)
- Dynamically referenced fields are named incorrectly and lacks scaling, offset and units (just raw values)
### Convert stream of data messages to JSON array
```
$ fq '[.dataRecords[] | select(.dataRecordHeader.messageType == 0).dataMessage]' file.fit
```
### Authors
- Mikael Lofjärd mikael.lofjard@gmail.com, original author
### References
- https://developer.garmin.com/fit/protocol/

View File

@ -4,15 +4,17 @@ import (
"github.com/wader/fq/pkg/scalar"
)
type FieldDefLookup struct {
Name string
Type string
Formatter string
Unit string
Scale float64
Offset int64
type FieldDef struct {
Name string
Type string
Format string
Unit string
Scale float64
Offset int64
Size uint64
}
type fieldDefMap map[uint64]FieldDefLookup
type fieldDefMap map[uint64]FieldDef
func (m fieldDefMap) MapUint(s scalar.Uint) (scalar.Uint, error) {
if t, ok := m[s.Actual]; ok {

View File

@ -1247,7 +1247,7 @@ var FieldDefMap = map[uint64]fieldDefMap{
5: {Name: "time_in_power_zone", Unit: "s", Scale: 1000},
6: {Name: "hr_zone_high_boundary", Unit: "bpm"},
7: {Name: "speed_zone_high_boundary", Unit: "m/s", Scale: 1000},
8: {Name: "cadence_zone_high_bondary", Unit: "rpm"},
8: {Name: "cadence_zone_high_boundary", Unit: "rpm"},
9: {Name: "power_zone_high_boundary", Unit: "watts"},
10: {Name: "hr_calc_type", Type: "hr_zone_calc"},
11: {Name: "max_heart_rate"},

View File

@ -16,4 +16,4 @@ func (m typeDefMap) MapUint(s scalar.Uint) (scalar.Uint, error) {
s.Sym = t.Name
}
return s, nil
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,24 +7,53 @@ import (
"github.com/wader/fq/pkg/scalar"
)
// Convertion from semicircles to decimal longitude latitude
var scConst = float64(180 / math.Pow(2, 31))
// Used for conversion from semicircles to decimal longitude latitude
var scConst = 180 / math.Pow(2, 31)
func GetUintFormatter(formatter string, unit string, scale float64, offset int64) scalar.UintFn {
var invalidUint = map[string]uint64{
"byte": 0xFF,
"enum": 0xFF,
"uint8": 0xFF,
"uint8z": 0x00,
"uint16": 0xFFFF,
"uint16z": 0x0000,
"uint32": 0xFFFFFFFF,
"uint32z": 0x00000000,
"uint64": 0xFFFFFFFFFFFFFFFF,
"uint64z": 0x0000000000000000,
}
var invalidSint = map[string]int64{
"sint8": 0x7F,
"sint16": 0x7FFF,
"sint32": 0x7FFFFFFF,
"sint64": 0x7FFFFFFFFFFFFFFF,
}
var invalidFloat = map[string]float64{
"float32": 0xFFFFFFFF,
"float64": 0xFFFFFFFFFFFFFFFF,
}
func GetUintFormatter(fDef FieldDef) scalar.UintFn {
return scalar.UintFn(func(s scalar.Uint) (scalar.Uint, error) {
if scale != 0.0 && offset != 0 {
s.Sym = (float64(s.Actual) / scale) - float64(offset)
if s.Actual == invalidUint[fDef.Type] {
s.Sym = "[invalid]"
return s, nil
}
if fDef.Scale != 0.0 && fDef.Offset != 0 {
s.Sym = (float64(s.Actual) / fDef.Scale) - float64(fDef.Offset)
} else {
if scale != 0.0 {
s.Sym = float64(s.Actual) / scale
if fDef.Scale != 0.0 {
s.Sym = float64(s.Actual) / fDef.Scale
}
if offset != 0 {
s.Sym = int64(s.Actual) - (offset)
if fDef.Offset != 0 {
s.Sym = int64(s.Actual) - fDef.Offset
}
}
s.Description = unit
if t, ok := TypeDefMap[formatter]; ok {
s.Description = fDef.Unit
if t, ok := TypeDefMap[fDef.Format]; ok {
if u, innerok := t[s.Actual]; innerok {
s.Sym = u.Name
}
@ -33,24 +62,50 @@ func GetUintFormatter(formatter string, unit string, scale float64, offset int64
})
}
func GetSintFormatter(formatter string, unit string, scale float64, offset int64) scalar.SintFn {
func GetSintFormatter(fDef FieldDef) scalar.SintFn {
return scalar.SintFn(func(s scalar.Sint) (scalar.Sint, error) {
if unit == "semicircles" {
if s.Actual == invalidSint[fDef.Type] {
s.Sym = "[invalid]"
return s, nil
}
if fDef.Unit == "semicircles" {
s.Sym = fmt.Sprintf("%f", float64(s.Actual)*scConst)
} else {
if scale != 0.0 && offset != 0 {
s.Sym = (float64(s.Actual) / scale) - float64(offset)
if fDef.Scale != 0.0 && fDef.Offset != 0 {
s.Sym = (float64(s.Actual) / fDef.Scale) - float64(fDef.Offset)
} else {
if scale != 0.0 {
s.Sym = float64(s.Actual) / scale
if fDef.Scale != 0.0 {
s.Sym = float64(s.Actual) / fDef.Scale
}
if offset != 0 {
s.Sym = int64(s.Actual) - (offset)
if fDef.Offset != 0 {
s.Sym = s.Actual - fDef.Offset
}
}
s.Description = unit
s.Description = fDef.Unit
}
return s, nil
})
}
func GetFloatFormatter(fDef FieldDef) scalar.FltFn {
return scalar.FltFn(func(s scalar.Flt) (scalar.Flt, error) {
if s.Actual == invalidFloat[fDef.Type] {
s.Sym = "[invalid]"
return s, nil
}
if fDef.Scale != 0.0 && fDef.Offset != 0 {
s.Sym = (s.Actual / fDef.Scale) - float64(fDef.Offset)
} else {
if fDef.Scale != 0.0 {
s.Sym = s.Actual / fDef.Scale
}
if fDef.Offset != 0 {
s.Sym = s.Actual - float64(fDef.Offset)
}
}
s.Description = fDef.Unit
return s, nil
})
}

BIN
format/fit/testdata/activity.fit vendored Normal file

Binary file not shown.

37316
format/fit/testdata/activity.fqtest vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
format/fit/testdata/activity_dev.fit vendored Normal file

Binary file not shown.

59060
format/fit/testdata/activity_dev.fqtest vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

138
format/fit/testdata/generator/index.js vendored Normal file
View File

@ -0,0 +1,138 @@
// import xlsx from 'node-xlsx';
var xlsx = require('node-xlsx').default;
const command = process.argv[2];
const profilePath = process.argv[3];
if (!command) {
console.log("Usage: node index.js [command] [profilePath]");
console.log("");
console.log("t - generate TypeDefMap");
console.log("m - generate MsgDefMap");
return 0;
}
const workSheetsFromFile = xlsx.parse(profilePath);
const inTypes = workSheetsFromFile[0].data;
const inMessages = workSheetsFromFile[1].data;
let currentType = '';
const outTypes = {};
const outMessages = {};
const outFormatters = {};
for (let li = 1; li < inTypes.length; li++) {
const row = inTypes[li];
if (row[0]) {
currentType = row[0];
outTypes[currentType] = { type: row[1], fields: [] };
outFormatters[currentType] = { type: row[1] };
} else {
if (row[4] && row[4].indexOf("Deprecated") > -1) {
continue;
}
const val = row[3];
outTypes[currentType].fields[val] = row[2];
}
}
for (let li = 1; li < inMessages.length; li++) {
const row = inMessages[li];
if (row[0]) {
currentType = row[0];
currentMsgNum = outTypes.mesg_num.fields.indexOf(currentType);
outMessages[currentMsgNum] = { msgNum: currentMsgNum, type: currentType, fields: {} };
} else {
if (row[1] == undefined) {
continue;
}
const fDefNo = row[1];
const name = row[2];
const type = row[3];
const scale = row[6];
const offset = row[7];
const unit = row[8];
outMessages[currentMsgNum].fields[name] = { fDefNo, name, type, unit, scale, offset };
outFormatters[type] = { ...outFormatters[type], unit, scale, offset };
}
}
if (command == "t") {
console.log("package mappers");
console.log("");
console.log("var TypeDefMap = map[string]typeDefMap{");
for (const key in outTypes) {
if (Object.hasOwnProperty.call(outTypes, key)) {
const element = outTypes[key];
console.log(`\t\"${key}\": {`);
for (let index = 0; index < element.fields.length; index++) {
const field = element.fields[index];
if (field) {
console.log(`\t\t${index}: {Name: \"${field}\"},`);
}
}
console.log(`\t},`);
}
}
console.log(`}`);
}
if (command == "m") {
console.log("package mappers");
console.log("");
console.log("var FieldDefMap = map[uint64]fieldDefMap{");
const baseTypes = ["bool", "byte", "enum", "uint8", "uint8z", "sint8", "sint16", "uint16", "uint16z", "sint32",
"uint32", "uint32z", "float32", "float64", "sint64", "uint64", "uint64z", "string"];
for (const key in outMessages) {
if (Object.hasOwnProperty.call(outMessages, key)) {
const element = outMessages[key];
console.log(`\t${key}: {`);
for (const msgKey in element.fields) {
if (Object.hasOwnProperty.call(element.fields, msgKey)) {
const field = element.fields[msgKey];
if (field) {
let type = "";
let unit = "";
let scale = "";
let offset = "";
if (baseTypes.indexOf(field.type) == -1) {
type = `, Type: \"${field.type}\"`;
}
if (field.unit) {
unit = `, Unit: \"${field.unit}\"`;
}
if (field.scale) {
scale = `, Scale: ${field.scale}`;
}
if (field.offset) {
offset = `, Offset: ${field.offset}`
}
console.log(`\t\t${field.fDefNo}: {Name: \"${field.name}\"${type}${unit}${scale}${offset}},`);
}
}
}
console.log(`\t},`);
}
}
console.log(`}`);
}

View File

@ -0,0 +1,14 @@
{
"name": "parser",
"version": "1.0.0",
"description": "Parse ANT Fit Profile to GoLang/fq",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Mikael Lofjärd",
"license": "MIT",
"dependencies": {
"node-xlsx": "^0.23.0"
}
}

BIN
format/fit/testdata/settings.fit vendored Normal file

Binary file not shown.

125
format/fit/testdata/settings.fqtest vendored Normal file
View File

@ -0,0 +1,125 @@
$ fq -d fit dv settings.fit
|00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef|.{}: settings.fit (fit) 0x0-0x52 (82)
| | | header{}: 0x0-0xc (12)
0x00|0c |. | headerSize: 12 0x0-0x1 (1)
0x00| 10 | . | protocolVersion: 16 0x1-0x2 (1)
0x00| 47 00 | G. | profileVersion: 71 0x2-0x4 (2)
0x00| 44 00 00 00 | D... | dataSize: 68 0x4-0x8 (4)
0x00| 2e 46 49 54 | .FIT | dataType: raw bits (valid) 0x8-0xc (4)
| | | dataRecords[0:6]: 0xc-0x50 (68)
| | | [0]{}: dataRecord 0xc-0x1e (18)
| | | dataRecordHeader{}: 0xc-0xd (1)
0x00| 40 | @ | headerType: 0 (Normal header) 0xc-0xc.1 (0.1)
0x00| 40 | @ | messageType: 1 (Definition message) 0xc.1-0xc.2 (0.1)
0x00| 40 | @ | hasDeveloperFields: false 0xc.2-0xc.3 (0.1)
0x00| 40 | @ | reserved: false 0xc.3-0xc.4 (0.1)
0x00| 40 | @ | localMessageType: 0 0xc.4-0xd (0.4)
| | | definitionMessage{}: 0xd-0x1e (17)
0x00| 00 | . | reserved: 0 0xd-0xe (1)
0x00| 01 | . | architecture: 1 0xe-0xf (1)
0x00| 00| .| globalMessageNumber: "file_id" (0) 0xf-0x11 (2)
0x10|00 |. |
0x10| 04 | . | fields: 4 0x11-0x12 (1)
| | | fieldDefinitions[0:4]: 0x12-0x1e (12)
| | | [0]{}: fieldDefinition 0x12-0x15 (3)
0x10| 01 | . | fieldDefNo: "manufacturer" (1) 0x12-0x13 (1)
0x10| 02 | . | size: 2 0x13-0x14 (1)
0x10| 84 | . | baseType: "uint16" (132) 0x14-0x15 (1)
| | | [1]{}: fieldDefinition 0x15-0x18 (3)
0x10| 02 | . | fieldDefNo: "product" (2) 0x15-0x16 (1)
0x10| 02 | . | size: 2 0x16-0x17 (1)
0x10| 84 | . | baseType: "uint16" (132) 0x17-0x18 (1)
| | | [2]{}: fieldDefinition 0x18-0x1b (3)
0x10| 03 | . | fieldDefNo: "serial_number" (3) 0x18-0x19 (1)
0x10| 04 | . | size: 4 0x19-0x1a (1)
0x10| 8c | . | baseType: "uint32z" (140) 0x1a-0x1b (1)
| | | [3]{}: fieldDefinition 0x1b-0x1e (3)
0x10| 00 | . | fieldDefNo: "type" (0) 0x1b-0x1c (1)
0x10| 01 | . | size: 1 0x1c-0x1d (1)
0x10| 00 | . | baseType: "enum" (0) 0x1d-0x1e (1)
| | | [1]{}: dataRecord 0x1e-0x28 (10)
| | | dataRecordHeader{}: 0x1e-0x1f (1)
0x10| 00 | . | headerType: 0 (Normal header) 0x1e-0x1e.1 (0.1)
0x10| 00 | . | messageType: 0 (Data message) 0x1e.1-0x1e.2 (0.1)
0x10| 00 | . | hasDeveloperFields: false 0x1e.2-0x1e.3 (0.1)
0x10| 00 | . | reserved: false 0x1e.3-0x1e.4 (0.1)
0x10| 00 | . | localMessageType: 0 0x1e.4-0x1f (0.4)
| | | dataMessage{}: 0x1f-0x28 (9)
0x10| 00| .| manufacturer: 256 0x1f-0x21 (2)
0x20|01 |. |
0x20| 03 dc | .. | product: 56323 0x21-0x23 (2)
0x20| 00 01 e2 40 | ...@ | serial_number: 1088553216 0x23-0x27 (4)
0x20| 02 | . | type: "settings" (2) 0x27-0x28 (1)
| | | [2]{}: dataRecord 0x28-0x3d (21)
| | | dataRecordHeader{}: 0x28-0x29 (1)
0x20| 40 | @ | headerType: 0 (Normal header) 0x28-0x28.1 (0.1)
0x20| 40 | @ | messageType: 1 (Definition message) 0x28.1-0x28.2 (0.1)
0x20| 40 | @ | hasDeveloperFields: false 0x28.2-0x28.3 (0.1)
0x20| 40 | @ | reserved: false 0x28.3-0x28.4 (0.1)
0x20| 40 | @ | localMessageType: 0 0x28.4-0x29 (0.4)
| | | definitionMessage{}: 0x29-0x3d (20)
0x20| 00 | . | reserved: 0 0x29-0x2a (1)
0x20| 01 | . | architecture: 1 0x2a-0x2b (1)
0x20| 00 03 | .. | globalMessageNumber: "user_profile" (3) 0x2b-0x2d (2)
0x20| 05 | . | fields: 5 0x2d-0x2e (1)
| | | fieldDefinitions[0:5]: 0x2e-0x3d (15)
| | | [0]{}: fieldDefinition 0x2e-0x31 (3)
0x20| 04 | . | fieldDefNo: "weight" (4) 0x2e-0x2f (1)
0x20| 02| .| size: 2 0x2f-0x30 (1)
0x30|84 |. | baseType: "uint16" (132) 0x30-0x31 (1)
| | | [1]{}: fieldDefinition 0x31-0x34 (3)
0x30| 01 | . | fieldDefNo: "gender" (1) 0x31-0x32 (1)
0x30| 01 | . | size: 1 0x32-0x33 (1)
0x30| 00 | . | baseType: "enum" (0) 0x33-0x34 (1)
| | | [2]{}: fieldDefinition 0x34-0x37 (3)
0x30| 02 | . | fieldDefNo: "age" (2) 0x34-0x35 (1)
0x30| 01 | . | size: 1 0x35-0x36 (1)
0x30| 02 | . | baseType: "uint8" (2) 0x36-0x37 (1)
| | | [3]{}: fieldDefinition 0x37-0x3a (3)
0x30| 03 | . | fieldDefNo: "height" (3) 0x37-0x38 (1)
0x30| 01 | . | size: 1 0x38-0x39 (1)
0x30| 02 | . | baseType: "uint8" (2) 0x39-0x3a (1)
| | | [4]{}: fieldDefinition 0x3a-0x3d (3)
0x30| 05 | . | fieldDefNo: "language" (5) 0x3a-0x3b (1)
0x30| 01 | . | size: 1 0x3b-0x3c (1)
0x30| 00 | . | baseType: "enum" (0) 0x3c-0x3d (1)
| | | [3]{}: dataRecord 0x3d-0x44 (7)
| | | dataRecordHeader{}: 0x3d-0x3e (1)
0x30| 00 | . | headerType: 0 (Normal header) 0x3d-0x3d.1 (0.1)
0x30| 00 | . | messageType: 0 (Data message) 0x3d.1-0x3d.2 (0.1)
0x30| 00 | . | hasDeveloperFields: false 0x3d.2-0x3d.3 (0.1)
0x30| 00 | . | reserved: false 0x3d.3-0x3d.4 (0.1)
0x30| 00 | . | localMessageType: 0 0x3d.4-0x3e (0.4)
| | | dataMessage{}: 0x3e-0x44 (6)
0x30| 03 84| ..| weight: 3379.5 (33795) (kg) 0x3e-0x40 (2)
0x40|01 |. | gender: "male" (1) 0x40-0x41 (1)
0x40| 1c | . | age: 28 (years) 0x41-0x42 (1)
0x40| be | . | height: 1.9 (190) (m) 0x42-0x43 (1)
0x40| 00 | . | language: "english" (0) 0x43-0x44 (1)
| | | [4]{}: dataRecord 0x44-0x4d (9)
| | | dataRecordHeader{}: 0x44-0x45 (1)
0x40| 40 | @ | headerType: 0 (Normal header) 0x44-0x44.1 (0.1)
0x40| 40 | @ | messageType: 1 (Definition message) 0x44.1-0x44.2 (0.1)
0x40| 40 | @ | hasDeveloperFields: false 0x44.2-0x44.3 (0.1)
0x40| 40 | @ | reserved: false 0x44.3-0x44.4 (0.1)
0x40| 40 | @ | localMessageType: 0 0x44.4-0x45 (0.4)
| | | definitionMessage{}: 0x45-0x4d (8)
0x40| 00 | . | reserved: 0 0x45-0x46 (1)
0x40| 01 | . | architecture: 1 0x46-0x47 (1)
0x40| 00 04 | .. | globalMessageNumber: "hrm_profile" (4) 0x47-0x49 (2)
0x40| 01 | . | fields: 1 0x49-0x4a (1)
| | | fieldDefinitions[0:1]: 0x4a-0x4d (3)
| | | [0]{}: fieldDefinition 0x4a-0x4d (3)
0x40| 01 | . | fieldDefNo: "hrm_ant_id" (1) 0x4a-0x4b (1)
0x40| 02 | . | size: 2 0x4b-0x4c (1)
0x40| 8b | . | baseType: "uint16z" (139) 0x4c-0x4d (1)
| | | [5]{}: dataRecord 0x4d-0x50 (3)
| | | dataRecordHeader{}: 0x4d-0x4e (1)
0x40| 00 | . | headerType: 0 (Normal header) 0x4d-0x4d.1 (0.1)
0x40| 00 | . | messageType: 0 (Data message) 0x4d.1-0x4d.2 (0.1)
0x40| 00 | . | hasDeveloperFields: false 0x4d.2-0x4d.3 (0.1)
0x40| 00 | . | reserved: false 0x4d.3-0x4d.4 (0.1)
0x40| 00 | . | localMessageType: 0 0x4d.4-0x4e (0.4)
| | | dataMessage{}: 0x4e-0x50 (2)
0x40| 00 64| .d| hrm_ant_id: 25600 0x4e-0x50 (2)
0x50|39 50| |9P| | crc: 20537 (valid) 0x50-0x52 (2)