mirror of
https://github.com/wader/fq.git
synced 2024-09-11 20:07:11 +03:00
Merge pull request #863 from mlofjard/fit
fit: Added support for ANT+ FIT format (used by Garmin devices)
This commit is contained in:
commit
5684bc43c5
@ -10,6 +10,7 @@ $ fq -n _registry.groups.probe
|
||||
"bzip2",
|
||||
"caff",
|
||||
"elf",
|
||||
"fit",
|
||||
"flac",
|
||||
"gif",
|
||||
"gzip",
|
||||
@ -85,6 +86,7 @@ elf Executable and Linkable Format
|
||||
ether8023_frame Ethernet 802.3 frame
|
||||
exif Exchangeable Image File Format
|
||||
fairplay_spc FairPlay Server Playback Context
|
||||
fit Garmin Flexible and Interoperable Data Transfer
|
||||
flac Free Lossless Audio Codec file
|
||||
flac_frame FLAC frame
|
||||
flac_metadatablock FLAC metadatablock
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
_ "github.com/wader/fq/format/dns"
|
||||
_ "github.com/wader/fq/format/elf"
|
||||
_ "github.com/wader/fq/format/fairplay"
|
||||
_ "github.com/wader/fq/format/fit"
|
||||
_ "github.com/wader/fq/format/flac"
|
||||
_ "github.com/wader/fq/format/gif"
|
||||
_ "github.com/wader/fq/format/gzip"
|
||||
|
13
format/fit/README.md
Normal file
13
format/fit/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
### 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`
|
||||
8. Correct formating and spelling of farenheit->fahrenheit and bondary->boundary in generated files to please Go linter
|
399
format/fit/fit.go
Normal file
399
format/fit/fit.go
Normal file
@ -0,0 +1,399 @@
|
||||
package fit
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/wader/fq/format"
|
||||
"github.com/wader/fq/format/fit/mappers"
|
||||
"github.com/wader/fq/pkg/decode"
|
||||
"github.com/wader/fq/pkg/interp"
|
||||
"github.com/wader/fq/pkg/scalar"
|
||||
)
|
||||
|
||||
//go:embed fit.md
|
||||
var fitFS embed.FS
|
||||
|
||||
func init() {
|
||||
interp.RegisterFormat(
|
||||
format.FIT,
|
||||
&decode.Format{
|
||||
Description: "Garmin Flexible and Interoperable Data Transfer",
|
||||
Groups: []*decode.Group{format.Probe},
|
||||
DecodeFn: decodeFIT,
|
||||
})
|
||||
|
||||
interp.RegisterFS(fitFS)
|
||||
}
|
||||
|
||||
var fitCRCTable = [16]uint16{
|
||||
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
|
||||
0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
|
||||
}
|
||||
|
||||
func calcCRC(bytes []byte) uint16 {
|
||||
var crc uint16
|
||||
crc = 0
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
// compute checksum of lower four bits of byte
|
||||
var checkByte = bytes[i]
|
||||
var tmp = fitCRCTable[crc&0xF]
|
||||
crc = (crc >> 4) & 0x0FFF
|
||||
crc = crc ^ tmp ^ fitCRCTable[checkByte&0xF]
|
||||
tmp = fitCRCTable[crc&0xF]
|
||||
crc = (crc >> 4) & 0x0FFF
|
||||
crc = crc ^ tmp ^ fitCRCTable[(checkByte>>4)&0xF]
|
||||
}
|
||||
|
||||
return crc
|
||||
}
|
||||
|
||||
type fitContext struct {
|
||||
dataSize uint64
|
||||
headerSize uint64
|
||||
}
|
||||
|
||||
type dataRecordContext struct {
|
||||
compressed bool
|
||||
data bool
|
||||
localMessageType uint64
|
||||
hasDeveloperFields bool
|
||||
}
|
||||
|
||||
type fileDescriptionContext struct {
|
||||
devIdx uint64
|
||||
fDefNo uint64
|
||||
typ string
|
||||
name string
|
||||
unit string
|
||||
nativeFieldNo uint64
|
||||
nativeMsgNo uint64
|
||||
}
|
||||
|
||||
type valueType struct {
|
||||
value uint64
|
||||
typ string
|
||||
}
|
||||
|
||||
type devFieldDefMap map[uint64]map[uint64]mappers.FieldDef
|
||||
type localFieldDefMap map[uint64]map[uint64]mappers.LocalFieldDef
|
||||
type localMsgIsDevDef map[uint64]bool
|
||||
type valueMap map[string]valueType
|
||||
|
||||
// "Magic" numbers
|
||||
const (
|
||||
developerFieldDescMesgNo = 206 // Special data message used as dynamic field definition message
|
||||
)
|
||||
|
||||
func fitDecodeFileHeader(d *decode.D, fc *fitContext) {
|
||||
frameStart := d.Pos()
|
||||
|
||||
headerSize := d.FieldU8("header_size")
|
||||
d.FieldU8("protocol_version")
|
||||
d.FieldU16("profile_version")
|
||||
dataSize := d.FieldU32("data_size")
|
||||
|
||||
d.FieldRawLen("data_type", 4*8, d.AssertBitBuf([]byte(".FIT")))
|
||||
if headerSize == 14 {
|
||||
headerCRC := calcCRC(d.BytesRange(frameStart, int(headerSize)-2))
|
||||
d.FieldU16("crc", d.UintValidate(uint64(headerCRC)))
|
||||
}
|
||||
fc.headerSize = headerSize
|
||||
fc.dataSize = dataSize
|
||||
}
|
||||
|
||||
func fitDecodeDataRecordHeader(d *decode.D, drc *dataRecordContext) {
|
||||
headerType := d.FieldU1("header_type", scalar.UintMapSymStr{0: "normal", 1: "compressed"})
|
||||
drc.compressed = headerType == 1
|
||||
if drc.compressed {
|
||||
localMessageType := d.FieldU2("local_message_type")
|
||||
d.FieldU32("time_offset")
|
||||
drc.localMessageType = localMessageType
|
||||
drc.data = true
|
||||
} else {
|
||||
mTypeIsDef := d.FieldU1("message_type", scalar.UintMapSymStr{0: "data", 1: "definition"})
|
||||
hasDeveloperFields := d.FieldBool("has_developer_fields")
|
||||
d.FieldBool("reserved")
|
||||
localMessageType := d.FieldU4("local_message_type")
|
||||
|
||||
drc.hasDeveloperFields = hasDeveloperFields
|
||||
drc.localMessageType = localMessageType
|
||||
drc.data = mTypeIsDef == 0
|
||||
}
|
||||
}
|
||||
|
||||
func fitDecodeDefinitionMessage(d *decode.D, drc *dataRecordContext, lmfd localFieldDefMap, dmfd devFieldDefMap, isDevMap localMsgIsDevDef) {
|
||||
d.FieldU8("reserved")
|
||||
endian := d.FieldU8("architecture", scalar.UintMapSymStr{0: "little_endian", 1: "big_endian"})
|
||||
switch endian {
|
||||
case 0:
|
||||
d.Endian = decode.LittleEndian
|
||||
case 1:
|
||||
d.Endian = decode.BigEndian
|
||||
default:
|
||||
d.Fatalf("Unknown endian %d", endian)
|
||||
}
|
||||
|
||||
messageNo := d.FieldU16("global_message_number", mappers.TypeDefMap["mesg_num"])
|
||||
isDevMap[drc.localMessageType] = messageNo == developerFieldDescMesgNo
|
||||
|
||||
numFields := d.FieldU8("fields")
|
||||
lmfd[drc.localMessageType] = make(map[uint64]mappers.LocalFieldDef, numFields)
|
||||
|
||||
d.FieldArray("field_definitions", func(d *decode.D) {
|
||||
for i := uint64(0); i < numFields; i++ {
|
||||
d.FieldStruct("field_definition", func(d *decode.D) {
|
||||
fieldDefNo := d.FieldU8("field_definition_number", mappers.FieldDefMap[messageNo])
|
||||
size := d.FieldU8("size")
|
||||
baseType := d.FieldU8("base_type", mappers.TypeDefMap["fit_base_type"])
|
||||
|
||||
var typ = mappers.TypeDefMap["fit_base_type"][baseType].Name
|
||||
fDefLookup, isSet := mappers.FieldDefMap[messageNo][fieldDefNo]
|
||||
if isSet {
|
||||
var foundName = fDefLookup.Name
|
||||
lmfd[drc.localMessageType][i] = mappers.LocalFieldDef{
|
||||
Name: foundName,
|
||||
Type: typ,
|
||||
Size: size,
|
||||
Format: fDefLookup.Type,
|
||||
Unit: fDefLookup.Unit,
|
||||
Scale: fDefLookup.Scale,
|
||||
Offset: fDefLookup.Offset,
|
||||
GlobalFieldDef: fDefLookup,
|
||||
GlobalMessageNo: messageNo,
|
||||
GlobalFieldDefNo: fieldDefNo,
|
||||
}
|
||||
} else {
|
||||
var foundName = fmt.Sprintf("UNKNOWN_%d", fieldDefNo)
|
||||
lmfd[drc.localMessageType][i] = mappers.LocalFieldDef{
|
||||
Name: foundName,
|
||||
Type: typ,
|
||||
Size: size,
|
||||
Format: "unknown",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if drc.hasDeveloperFields {
|
||||
numDevFields := d.FieldU8("developer_fields")
|
||||
|
||||
d.FieldArray("developer_field_definitions", func(d *decode.D) {
|
||||
for i := numFields; i < (numDevFields + numFields); i++ {
|
||||
d.FieldStruct("developer_field_definition", func(d *decode.D) {
|
||||
fieldDefNo := d.FieldU8("field_definition_number")
|
||||
size := d.FieldU8("size")
|
||||
devDataIdx := d.FieldU8("developer_data_index")
|
||||
|
||||
fDefLookup, isSet := dmfd[devDataIdx][fieldDefNo]
|
||||
|
||||
if isSet {
|
||||
var foundName = fDefLookup.Name
|
||||
lmfd[drc.localMessageType][i] = mappers.LocalFieldDef{
|
||||
Name: foundName,
|
||||
Type: fDefLookup.Type,
|
||||
Size: size,
|
||||
Unit: fDefLookup.Unit,
|
||||
Scale: fDefLookup.Scale,
|
||||
Offset: fDefLookup.Offset,
|
||||
}
|
||||
} else {
|
||||
var foundName = fmt.Sprintf("UNKNOWN_%d", fieldDefNo)
|
||||
lmfd[drc.localMessageType][i] = mappers.LocalFieldDef{
|
||||
Name: foundName,
|
||||
Type: "UNKNOWN",
|
||||
Size: size,
|
||||
Format: "UNKNOWN",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fieldUint(fieldFn func(string, ...scalar.UintMapper) uint64, expectedSize uint64, fDef mappers.LocalFieldDef, uintFormatter scalar.UintFn, fdc *fileDescriptionContext, valMap valueMap) {
|
||||
var val uint64
|
||||
|
||||
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 fDef.GlobalFieldDef.HasSubField {
|
||||
var found = false
|
||||
if subFieldValueMap, ok := mappers.SubFieldDefMap[fDef.GlobalMessageNo][fDef.GlobalFieldDefNo]; ok {
|
||||
for k := range subFieldValueMap {
|
||||
if subFieldDef, ok := subFieldValueMap[k][mappers.TypeDefMap[valMap[k].typ][valMap[k].value].Name]; ok {
|
||||
subUintFormatter := mappers.GetUintFormatter(mappers.LocalFieldDef{
|
||||
Name: subFieldDef.Name,
|
||||
Type: fDef.Type,
|
||||
Size: fDef.Size,
|
||||
Format: subFieldDef.Type,
|
||||
Unit: subFieldDef.Unit,
|
||||
Scale: subFieldDef.Scale,
|
||||
Offset: subFieldDef.Offset,
|
||||
})
|
||||
val = fieldFn(subFieldDef.Name, subUintFormatter)
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found { // SubField conditions could not be resolved
|
||||
val = fieldFn(fDef.Name, uintFormatter)
|
||||
}
|
||||
} else {
|
||||
val = fieldFn(fDef.Name, uintFormatter)
|
||||
}
|
||||
valMap[fDef.Name] = valueType{value: val, typ: fDef.Format}
|
||||
|
||||
// Save developer field definitions
|
||||
switch fDef.Name {
|
||||
case "developer_data_index":
|
||||
fdc.devIdx = val
|
||||
case "field_definition_number":
|
||||
fdc.fDefNo = val
|
||||
case "fit_base_type_id":
|
||||
fdc.typ = mappers.TypeDefMap["fit_base_type"][val].Name
|
||||
case "native_field_num":
|
||||
fdc.nativeFieldNo = val
|
||||
case "native_mesg_num":
|
||||
fdc.nativeMsgNo = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fieldSint(fieldFn func(string, ...scalar.SintMapper) int64, expectedSize uint64, fDef mappers.LocalFieldDef, 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 {
|
||||
fieldFn(fDef.Name, sintFormatter)
|
||||
}
|
||||
}
|
||||
|
||||
func fieldFloat(fieldFn func(string, ...scalar.FltMapper) float64, expectedSize uint64, fDef mappers.LocalFieldDef, 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)
|
||||
}
|
||||
}
|
||||
|
||||
func fieldString(d *decode.D, fDef mappers.LocalFieldDef, fdc *fileDescriptionContext) {
|
||||
val := d.FieldUTF8NullFixedLen(fDef.Name, int(fDef.Size), scalar.StrMapDescription{"": "Invalid"})
|
||||
|
||||
// Save developer field definitions
|
||||
switch fDef.Name {
|
||||
case "field_name":
|
||||
fdc.name = val
|
||||
case "units":
|
||||
fdc.unit = val
|
||||
}
|
||||
}
|
||||
|
||||
func fitDecodeDataMessage(d *decode.D, drc *dataRecordContext, lmfd localFieldDefMap, dmfd devFieldDefMap, isDevMap localMsgIsDevDef) {
|
||||
var fdc fileDescriptionContext
|
||||
valMap := make(valueMap, len(lmfd[drc.localMessageType]))
|
||||
keys := make([]int, len(lmfd[drc.localMessageType]))
|
||||
i := 0
|
||||
for k := range lmfd[drc.localMessageType] {
|
||||
keys[i] = int(k)
|
||||
i++
|
||||
}
|
||||
sort.Ints(keys)
|
||||
|
||||
isDevDep := isDevMap[drc.localMessageType]
|
||||
|
||||
for _, k := range keys {
|
||||
fDef := lmfd[drc.localMessageType][uint64(k)]
|
||||
|
||||
var uintFormatter = mappers.GetUintFormatter(fDef)
|
||||
var sintFormatter = mappers.GetSintFormatter(fDef)
|
||||
var floatFormatter = mappers.GetFloatFormatter(fDef)
|
||||
|
||||
switch fDef.Type {
|
||||
case "enum", "uint8", "uint8z", "byte":
|
||||
fieldUint(d.FieldU8, 1, fDef, uintFormatter, &fdc, valMap)
|
||||
case "uint16", "uint16z":
|
||||
fieldUint(d.FieldU16, 2, fDef, uintFormatter, &fdc, valMap)
|
||||
case "uint32", "uint32z":
|
||||
fieldUint(d.FieldU32, 4, fDef, uintFormatter, &fdc, valMap)
|
||||
case "uint64", "uint64z":
|
||||
fieldUint(d.FieldU64, 8, fDef, uintFormatter, &fdc, valMap)
|
||||
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, fDef, &fdc)
|
||||
default:
|
||||
d.Fatalf("Unknown type %s", fDef.Type)
|
||||
}
|
||||
}
|
||||
|
||||
if isDevDep {
|
||||
if _, ok := dmfd[fdc.devIdx]; !ok {
|
||||
dmfd[fdc.devIdx] = make(map[uint64]mappers.FieldDef)
|
||||
}
|
||||
dmfd[fdc.devIdx][fdc.fDefNo] = mappers.FieldDef{
|
||||
Name: fdc.name,
|
||||
Type: fdc.typ,
|
||||
Unit: fdc.unit,
|
||||
Scale: 0,
|
||||
Offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeFIT(d *decode.D) any {
|
||||
var fc fitContext
|
||||
d.Endian = decode.LittleEndian
|
||||
|
||||
var lmfd localFieldDefMap = make(localFieldDefMap)
|
||||
var dmfd devFieldDefMap = make(devFieldDefMap)
|
||||
var isDevMap localMsgIsDevDef = make(localMsgIsDevDef)
|
||||
|
||||
d.FieldStruct("header", func(d *decode.D) { fitDecodeFileHeader(d, &fc) })
|
||||
|
||||
d.FieldArray("data_records", func(d *decode.D) {
|
||||
for d.Pos() < int64((fc.headerSize+fc.dataSize)*8) {
|
||||
d.FieldStruct("data_record", func(d *decode.D) {
|
||||
var drc dataRecordContext
|
||||
d.FieldStruct("record_header", func(d *decode.D) { fitDecodeDataRecordHeader(d, &drc) })
|
||||
switch drc.data {
|
||||
case true:
|
||||
d.FieldStruct("data_message", func(d *decode.D) { fitDecodeDataMessage(d, &drc, lmfd, dmfd, isDevMap) })
|
||||
case false:
|
||||
d.FieldStruct("definition_message", func(d *decode.D) { fitDecodeDefinitionMessage(d, &drc, lmfd, dmfd, isDevMap) })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
var fileCRC uint16
|
||||
if fc.headerSize == 12 {
|
||||
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, int(fc.dataSize))) // 14 byte header - CRC everything below header except the CRC itself
|
||||
}
|
||||
d.FieldU16("crc", d.UintValidate(uint64(fileCRC)))
|
||||
|
||||
return nil
|
||||
}
|
18
format/fit/fit.md
Normal file
18
format/fit/fit.md
Normal file
@ -0,0 +1,18 @@
|
||||
### Limitations
|
||||
|
||||
- Fields with subcomponents, such as "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)
|
||||
- Compressed timestamp messages are not accumulated against last known full timestamp.
|
||||
|
||||
### Convert stream of data messages to JSON array
|
||||
|
||||
```
|
||||
$ fq '[.data_records[] | select(.record_header.message_type == "data").data_message]' file.fit
|
||||
```
|
||||
|
||||
### Authors
|
||||
- Mikael Lofjärd mikael.lofjard@gmail.com, original author
|
||||
|
||||
### References
|
||||
- https://developer.garmin.com/fit/protocol/
|
37
format/fit/mappers/messages.go
Normal file
37
format/fit/mappers/messages.go
Normal file
@ -0,0 +1,37 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"github.com/wader/fq/pkg/scalar"
|
||||
)
|
||||
|
||||
type FieldDef struct {
|
||||
Name string
|
||||
Type string
|
||||
Unit string
|
||||
Scale float64
|
||||
Offset int64
|
||||
Size uint64
|
||||
HasSubField bool
|
||||
}
|
||||
|
||||
type LocalFieldDef struct {
|
||||
Name string
|
||||
Type string
|
||||
Format string
|
||||
Unit string
|
||||
Scale float64
|
||||
Offset int64
|
||||
Size uint64
|
||||
GlobalFieldDef FieldDef
|
||||
GlobalMessageNo uint64
|
||||
GlobalFieldDefNo uint64
|
||||
}
|
||||
|
||||
type fieldDefMap map[uint64]FieldDef
|
||||
|
||||
func (m fieldDefMap) MapUint(s scalar.Uint) (scalar.Uint, error) {
|
||||
if t, ok := m[s.Actual]; ok {
|
||||
s.Sym = t.Name
|
||||
}
|
||||
return s, nil
|
||||
}
|
1714
format/fit/mappers/messages_generated.go
Normal file
1714
format/fit/mappers/messages_generated.go
Normal file
File diff suppressed because it is too large
Load Diff
19
format/fit/mappers/types.go
Normal file
19
format/fit/mappers/types.go
Normal file
@ -0,0 +1,19 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"github.com/wader/fq/pkg/scalar"
|
||||
)
|
||||
|
||||
type TypeDefLookup struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
type typeDefMap map[uint64]TypeDefLookup
|
||||
|
||||
func (m typeDefMap) MapUint(s scalar.Uint) (scalar.Uint, error) {
|
||||
if t, ok := m[s.Actual]; ok {
|
||||
s.Sym = t.Name
|
||||
}
|
||||
return s, nil
|
||||
}
|
3711
format/fit/mappers/types_generated.go
Normal file
3711
format/fit/mappers/types_generated.go
Normal file
File diff suppressed because it is too large
Load Diff
110
format/fit/mappers/values.go
Normal file
110
format/fit/mappers/values.go
Normal file
@ -0,0 +1,110 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/wader/fq/pkg/scalar"
|
||||
)
|
||||
|
||||
// Used for conversion from semicircles to decimal longitude latitude
|
||||
var scConst = 180 / math.Pow(2, 31)
|
||||
|
||||
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 LocalFieldDef) scalar.UintFn {
|
||||
return scalar.UintFn(func(s scalar.Uint) (scalar.Uint, error) {
|
||||
if s.Actual == invalidUint[fDef.Type] {
|
||||
s.Description = "Invalid"
|
||||
return s, nil
|
||||
}
|
||||
if fDef.Scale != 0.0 && fDef.Offset != 0 {
|
||||
s.Sym = (float64(s.Actual) / fDef.Scale) - float64(fDef.Offset)
|
||||
} else {
|
||||
if fDef.Scale != 0.0 {
|
||||
s.Sym = float64(s.Actual) / fDef.Scale
|
||||
}
|
||||
if fDef.Offset != 0 {
|
||||
s.Sym = int64(s.Actual) - fDef.Offset
|
||||
}
|
||||
}
|
||||
|
||||
s.Description = fDef.Unit
|
||||
if t, ok := TypeDefMap[fDef.Format]; ok {
|
||||
if u, innerok := t[s.Actual]; innerok {
|
||||
s.Sym = u.Name
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetSintFormatter(fDef LocalFieldDef) scalar.SintFn {
|
||||
return scalar.SintFn(func(s scalar.Sint) (scalar.Sint, error) {
|
||||
if s.Actual == invalidSint[fDef.Type] {
|
||||
s.Description = "Invalid"
|
||||
return s, nil
|
||||
}
|
||||
if fDef.Unit == "semicircles" {
|
||||
s.Sym = float64(s.Actual) * scConst
|
||||
} else {
|
||||
if fDef.Scale != 0.0 && fDef.Offset != 0 {
|
||||
s.Sym = (float64(s.Actual) / fDef.Scale) - float64(fDef.Offset)
|
||||
} else {
|
||||
if fDef.Scale != 0.0 {
|
||||
s.Sym = float64(s.Actual) / fDef.Scale
|
||||
}
|
||||
if fDef.Offset != 0 {
|
||||
s.Sym = s.Actual - fDef.Offset
|
||||
}
|
||||
}
|
||||
|
||||
s.Description = fDef.Unit
|
||||
}
|
||||
return s, nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetFloatFormatter(fDef LocalFieldDef) scalar.FltFn {
|
||||
return scalar.FltFn(func(s scalar.Flt) (scalar.Flt, error) {
|
||||
if s.Actual == invalidFloat[fDef.Type] {
|
||||
s.Description = "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
BIN
format/fit/testdata/activity.fit
vendored
Normal file
Binary file not shown.
37316
format/fit/testdata/activity.fqtest
vendored
Normal file
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
BIN
format/fit/testdata/activity_dev.fit
vendored
Normal file
Binary file not shown.
59060
format/fit/testdata/activity_dev.fqtest
vendored
Normal file
59060
format/fit/testdata/activity_dev.fqtest
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
format/fit/testdata/activity_invalid_strings.fit
vendored
Normal file
BIN
format/fit/testdata/activity_invalid_strings.fit
vendored
Normal file
Binary file not shown.
42672
format/fit/testdata/activity_invalid_strings.fqtest
vendored
Normal file
42672
format/fit/testdata/activity_invalid_strings.fqtest
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
format/fit/testdata/generator/.gitignore
vendored
Normal file
3
format/fit/testdata/generator/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
234
format/fit/testdata/generator/index.js
vendored
Normal file
234
format/fit/testdata/generator/index.js
vendored
Normal file
@ -0,0 +1,234 @@
|
||||
// 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 = '';
|
||||
let dynamicFieldName = '';
|
||||
let currentFDefNo = null;
|
||||
const outTypes = {};
|
||||
const outMessages = {};
|
||||
const outSubFields = {};
|
||||
|
||||
for (let li = 1; li < inTypes.length; li++) {
|
||||
const row = inTypes[li];
|
||||
|
||||
if (row[0]) {
|
||||
currentType = row[0];
|
||||
outTypes[currentType] = { type: row[1], fields: [] };
|
||||
} 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];
|
||||
const refFields = {}
|
||||
|
||||
if (row[0]) {
|
||||
currentType = row[0];
|
||||
currentMsgNum = outTypes.mesg_num.fields.indexOf(currentType);
|
||||
outMessages[currentMsgNum] = { msgNum: currentMsgNum, type: currentType, fields: {} };
|
||||
outSubFields[currentMsgNum] = { msgNum: currentMsgNum, fields: {} }
|
||||
} else {
|
||||
if (row[2] == 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];
|
||||
|
||||
if (fDefNo != null) {
|
||||
outMessages[currentMsgNum].fields[name] = { fDefNo, name, type, unit, scale, offset };
|
||||
currentFDefNo = fDefNo
|
||||
dynamicFieldName = name
|
||||
} else {
|
||||
const refField = row[11].split(",")[0]
|
||||
const refVals = row[12].split(",")
|
||||
|
||||
if (!Object.hasOwnProperty.call(refFields, refField)) {
|
||||
refFields[refField] = {}
|
||||
}
|
||||
refVals.forEach(element => {
|
||||
refFields[refField][element] = { name, type, unit, scale, offset }
|
||||
});
|
||||
|
||||
outMessages[currentMsgNum].fields[dynamicFieldName]["hasSub"] = true;
|
||||
outSubFields[currentMsgNum].fields[currentFDefNo] = refFields
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
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("");
|
||||
|
||||
const baseTypes = ["bool", "byte", "enum", "uint8", "uint8z", "sint8", "sint16", "uint16", "uint16z", "sint32",
|
||||
"uint32", "uint32z", "float32", "float64", "sint64", "uint64", "uint64z", "string"];
|
||||
|
||||
console.log("var SubFieldDefMap = map[uint64]map[uint64]map[string]map[string]FieldDef{");
|
||||
for (const key in outSubFields) {
|
||||
if (Object.hasOwnProperty.call(outSubFields, key)) {
|
||||
const element = outSubFields[key];
|
||||
if (Object.keys(element.fields).length == 0) {
|
||||
continue
|
||||
}
|
||||
console.log(`\t${key}: {`);
|
||||
|
||||
for (const fieldKey in element.fields) {
|
||||
const field = element.fields[fieldKey];
|
||||
console.log(`\t\t${fieldKey}: {`);
|
||||
|
||||
for (const refFieldKey in field) {
|
||||
const refField = field[refFieldKey];
|
||||
console.log(`\t\t\t"${refFieldKey}": {`);
|
||||
|
||||
for (const refValKey in refField) {
|
||||
const subField = refField[refValKey];
|
||||
|
||||
if (subField) {
|
||||
let type = "";
|
||||
let unit = "";
|
||||
let scale = "";
|
||||
let offset = "";
|
||||
|
||||
if (baseTypes.indexOf(subField.type) == -1) {
|
||||
type = `, Type: \"${subField.type}\"`;
|
||||
}
|
||||
|
||||
if (subField.unit) {
|
||||
unit = `, Unit: \"${subField.unit}\"`;
|
||||
}
|
||||
|
||||
if (subField.scale) {
|
||||
if (typeof (subField.scale) == "string") {
|
||||
// ignore multi scale (for component fields) for now
|
||||
const testScale = subField.scale.split(",");
|
||||
if (testScale.length == 1) {
|
||||
scale = `, Scale: ${testScale[0]}`;
|
||||
}
|
||||
} else {
|
||||
scale = `, Scale: ${subField.scale}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (subField.offset) {
|
||||
offset = `, Offset: ${subField.offset}`
|
||||
}
|
||||
|
||||
console.log(`\t\t\t\t"${refValKey}": {Name: \"${subField.name}\"${type}${unit}${scale}${offset}},`);
|
||||
}
|
||||
}
|
||||
console.log(`\t\t\t},`);
|
||||
}
|
||||
console.log(`\t\t},`);
|
||||
}
|
||||
console.log(`\t},`);
|
||||
}
|
||||
}
|
||||
console.log("}");
|
||||
console.log("");
|
||||
|
||||
console.log("var FieldDefMap = map[uint64]fieldDefMap{");
|
||||
|
||||
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 = "";
|
||||
let hasSub = "";
|
||||
|
||||
if (baseTypes.indexOf(field.type) == -1) {
|
||||
type = `, Type: \"${field.type}\"`;
|
||||
}
|
||||
|
||||
if (field.unit) {
|
||||
unit = `, Unit: \"${field.unit}\"`;
|
||||
}
|
||||
|
||||
if (field.scale) {
|
||||
if (typeof (field.scale) == "string") {
|
||||
// ignore multi scale (for component fields) for now
|
||||
const testScale = field.scale.split(",");
|
||||
if (testScale.length == 1) {
|
||||
scale = `, Scale: ${testScale[0]}`;
|
||||
}
|
||||
} else {
|
||||
scale = `, Scale: ${field.scale}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.offset) {
|
||||
offset = `, Offset: ${field.offset}`
|
||||
}
|
||||
|
||||
if (field.hasSub) {
|
||||
hasSub = `, HasSubField: ${field.hasSub}`
|
||||
}
|
||||
|
||||
console.log(`\t\t${field.fDefNo}: {Name: \"${field.name}\"${type}${unit}${scale}${offset}${hasSub}},`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\t},`);
|
||||
}
|
||||
}
|
||||
console.log(`}`);
|
||||
}
|
14
format/fit/testdata/generator/package.json
vendored
Normal file
14
format/fit/testdata/generator/package.json
vendored
Normal 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
BIN
format/fit/testdata/settings.fit
vendored
Normal file
Binary file not shown.
125
format/fit/testdata/settings.fqtest
vendored
Normal file
125
format/fit/testdata/settings.fqtest
vendored
Normal 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 |. | header_size: 12 0x0-0x1 (1)
|
||||
0x00| 10 | . | protocol_version: 16 0x1-0x2 (1)
|
||||
0x00| 47 00 | G. | profile_version: 71 0x2-0x4 (2)
|
||||
0x00| 44 00 00 00 | D... | data_size: 68 0x4-0x8 (4)
|
||||
0x00| 2e 46 49 54 | .FIT | data_type: raw bits (valid) 0x8-0xc (4)
|
||||
| | | data_records[0:6]: 0xc-0x50 (68)
|
||||
| | | [0]{}: data_record 0xc-0x1e (18)
|
||||
| | | record_header{}: 0xc-0xd (1)
|
||||
0x00| 40 | @ | header_type: "normal" (0) 0xc-0xc.1 (0.1)
|
||||
0x00| 40 | @ | message_type: "definition" (1) 0xc.1-0xc.2 (0.1)
|
||||
0x00| 40 | @ | has_developer_fields: false 0xc.2-0xc.3 (0.1)
|
||||
0x00| 40 | @ | reserved: false 0xc.3-0xc.4 (0.1)
|
||||
0x00| 40 | @ | local_message_type: 0 0xc.4-0xd (0.4)
|
||||
| | | definition_message{}: 0xd-0x1e (17)
|
||||
0x00| 00 | . | reserved: 0 0xd-0xe (1)
|
||||
0x00| 01 | . | architecture: "big_endian" (1) 0xe-0xf (1)
|
||||
0x00| 00| .| global_message_number: "file_id" (0) 0xf-0x11 (2)
|
||||
0x10|00 |. |
|
||||
0x10| 04 | . | fields: 4 0x11-0x12 (1)
|
||||
| | | field_definitions[0:4]: 0x12-0x1e (12)
|
||||
| | | [0]{}: field_definition 0x12-0x15 (3)
|
||||
0x10| 01 | . | field_definition_number: "manufacturer" (1) 0x12-0x13 (1)
|
||||
0x10| 02 | . | size: 2 0x13-0x14 (1)
|
||||
0x10| 84 | . | base_type: "uint16" (132) 0x14-0x15 (1)
|
||||
| | | [1]{}: field_definition 0x15-0x18 (3)
|
||||
0x10| 02 | . | field_definition_number: "product" (2) 0x15-0x16 (1)
|
||||
0x10| 02 | . | size: 2 0x16-0x17 (1)
|
||||
0x10| 84 | . | base_type: "uint16" (132) 0x17-0x18 (1)
|
||||
| | | [2]{}: field_definition 0x18-0x1b (3)
|
||||
0x10| 03 | . | field_definition_number: "serial_number" (3) 0x18-0x19 (1)
|
||||
0x10| 04 | . | size: 4 0x19-0x1a (1)
|
||||
0x10| 8c | . | base_type: "uint32z" (140) 0x1a-0x1b (1)
|
||||
| | | [3]{}: field_definition 0x1b-0x1e (3)
|
||||
0x10| 00 | . | field_definition_number: "type" (0) 0x1b-0x1c (1)
|
||||
0x10| 01 | . | size: 1 0x1c-0x1d (1)
|
||||
0x10| 00 | . | base_type: "enum" (0) 0x1d-0x1e (1)
|
||||
| | | [1]{}: data_record 0x1e-0x28 (10)
|
||||
| | | record_header{}: 0x1e-0x1f (1)
|
||||
0x10| 00 | . | header_type: "normal" (0) 0x1e-0x1e.1 (0.1)
|
||||
0x10| 00 | . | message_type: "data" (0) 0x1e.1-0x1e.2 (0.1)
|
||||
0x10| 00 | . | has_developer_fields: false 0x1e.2-0x1e.3 (0.1)
|
||||
0x10| 00 | . | reserved: false 0x1e.3-0x1e.4 (0.1)
|
||||
0x10| 00 | . | local_message_type: 0 0x1e.4-0x1f (0.4)
|
||||
| | | data_message{}: 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]{}: data_record 0x28-0x3d (21)
|
||||
| | | record_header{}: 0x28-0x29 (1)
|
||||
0x20| 40 | @ | header_type: "normal" (0) 0x28-0x28.1 (0.1)
|
||||
0x20| 40 | @ | message_type: "definition" (1) 0x28.1-0x28.2 (0.1)
|
||||
0x20| 40 | @ | has_developer_fields: false 0x28.2-0x28.3 (0.1)
|
||||
0x20| 40 | @ | reserved: false 0x28.3-0x28.4 (0.1)
|
||||
0x20| 40 | @ | local_message_type: 0 0x28.4-0x29 (0.4)
|
||||
| | | definition_message{}: 0x29-0x3d (20)
|
||||
0x20| 00 | . | reserved: 0 0x29-0x2a (1)
|
||||
0x20| 01 | . | architecture: "big_endian" (1) 0x2a-0x2b (1)
|
||||
0x20| 00 03 | .. | global_message_number: "user_profile" (3) 0x2b-0x2d (2)
|
||||
0x20| 05 | . | fields: 5 0x2d-0x2e (1)
|
||||
| | | field_definitions[0:5]: 0x2e-0x3d (15)
|
||||
| | | [0]{}: field_definition 0x2e-0x31 (3)
|
||||
0x20| 04 | . | field_definition_number: "weight" (4) 0x2e-0x2f (1)
|
||||
0x20| 02| .| size: 2 0x2f-0x30 (1)
|
||||
0x30|84 |. | base_type: "uint16" (132) 0x30-0x31 (1)
|
||||
| | | [1]{}: field_definition 0x31-0x34 (3)
|
||||
0x30| 01 | . | field_definition_number: "gender" (1) 0x31-0x32 (1)
|
||||
0x30| 01 | . | size: 1 0x32-0x33 (1)
|
||||
0x30| 00 | . | base_type: "enum" (0) 0x33-0x34 (1)
|
||||
| | | [2]{}: field_definition 0x34-0x37 (3)
|
||||
0x30| 02 | . | field_definition_number: "age" (2) 0x34-0x35 (1)
|
||||
0x30| 01 | . | size: 1 0x35-0x36 (1)
|
||||
0x30| 02 | . | base_type: "uint8" (2) 0x36-0x37 (1)
|
||||
| | | [3]{}: field_definition 0x37-0x3a (3)
|
||||
0x30| 03 | . | field_definition_number: "height" (3) 0x37-0x38 (1)
|
||||
0x30| 01 | . | size: 1 0x38-0x39 (1)
|
||||
0x30| 02 | . | base_type: "uint8" (2) 0x39-0x3a (1)
|
||||
| | | [4]{}: field_definition 0x3a-0x3d (3)
|
||||
0x30| 05 | . | field_definition_number: "language" (5) 0x3a-0x3b (1)
|
||||
0x30| 01 | . | size: 1 0x3b-0x3c (1)
|
||||
0x30| 00 | . | base_type: "enum" (0) 0x3c-0x3d (1)
|
||||
| | | [3]{}: data_record 0x3d-0x44 (7)
|
||||
| | | record_header{}: 0x3d-0x3e (1)
|
||||
0x30| 00 | . | header_type: "normal" (0) 0x3d-0x3d.1 (0.1)
|
||||
0x30| 00 | . | message_type: "data" (0) 0x3d.1-0x3d.2 (0.1)
|
||||
0x30| 00 | . | has_developer_fields: false 0x3d.2-0x3d.3 (0.1)
|
||||
0x30| 00 | . | reserved: false 0x3d.3-0x3d.4 (0.1)
|
||||
0x30| 00 | . | local_message_type: 0 0x3d.4-0x3e (0.4)
|
||||
| | | data_message{}: 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]{}: data_record 0x44-0x4d (9)
|
||||
| | | record_header{}: 0x44-0x45 (1)
|
||||
0x40| 40 | @ | header_type: "normal" (0) 0x44-0x44.1 (0.1)
|
||||
0x40| 40 | @ | message_type: "definition" (1) 0x44.1-0x44.2 (0.1)
|
||||
0x40| 40 | @ | has_developer_fields: false 0x44.2-0x44.3 (0.1)
|
||||
0x40| 40 | @ | reserved: false 0x44.3-0x44.4 (0.1)
|
||||
0x40| 40 | @ | local_message_type: 0 0x44.4-0x45 (0.4)
|
||||
| | | definition_message{}: 0x45-0x4d (8)
|
||||
0x40| 00 | . | reserved: 0 0x45-0x46 (1)
|
||||
0x40| 01 | . | architecture: "big_endian" (1) 0x46-0x47 (1)
|
||||
0x40| 00 04 | .. | global_message_number: "hrm_profile" (4) 0x47-0x49 (2)
|
||||
0x40| 01 | . | fields: 1 0x49-0x4a (1)
|
||||
| | | field_definitions[0:1]: 0x4a-0x4d (3)
|
||||
| | | [0]{}: field_definition 0x4a-0x4d (3)
|
||||
0x40| 01 | . | field_definition_number: "hrm_ant_id" (1) 0x4a-0x4b (1)
|
||||
0x40| 02 | . | size: 2 0x4b-0x4c (1)
|
||||
0x40| 8b | . | base_type: "uint16z" (139) 0x4c-0x4d (1)
|
||||
| | | [5]{}: data_record 0x4d-0x50 (3)
|
||||
| | | record_header{}: 0x4d-0x4e (1)
|
||||
0x40| 00 | . | header_type: "normal" (0) 0x4d-0x4d.1 (0.1)
|
||||
0x40| 00 | . | message_type: "data" (0) 0x4d.1-0x4d.2 (0.1)
|
||||
0x40| 00 | . | has_developer_fields: false 0x4d.2-0x4d.3 (0.1)
|
||||
0x40| 00 | . | reserved: false 0x4d.3-0x4d.4 (0.1)
|
||||
0x40| 00 | . | local_message_type: 0 0x4d.4-0x4e (0.4)
|
||||
| | | data_message{}: 0x4e-0x50 (2)
|
||||
0x40| 00 64| .d| hrm_ant_id: 25600 0x4e-0x50 (2)
|
||||
0x50|39 50| |9P| | crc: 20537 (valid) 0x50-0x52 (2)
|
@ -97,6 +97,7 @@ var (
|
||||
Ether_8023_Frame = &decode.Group{Name: "ether8023_frame"}
|
||||
Exif = &decode.Group{Name: "exif"}
|
||||
Fairplay_SPC = &decode.Group{Name: "fairplay_spc"}
|
||||
FIT = &decode.Group{Name: "fit"}
|
||||
FLAC = &decode.Group{Name: "flac"}
|
||||
FLAC_Frame = &decode.Group{Name: "flac_frame"}
|
||||
FLAC_Metadatablock = &decode.Group{Name: "flac_metadatablock"}
|
||||
|
Loading…
Reference in New Issue
Block a user