pm5conv/pm5conv.go

321 lines
8.3 KiB
Go

package pm5conv
import (
"encoding/json"
"errors"
"io/ioutil"
"path/filepath"
"time"
)
func errorMsg(msg string) error {
return errors.New("pm5conv: " + msg)
}
const (
metadataFile = "LogDataAccessTbl.bin"
storageFile = "LogDataStorage.bin"
lenMetadata = 32 // Bytes
lenNormalHeader = 50 // Bytes
lenIntervalHeader = 52 // Bytes
lenSplitFrame = 32 // Bytes
lenVariableInterval = 48 // Bytes
)
const (
baseYear = 2000
)
const (
metadataMagic uint8 = 0xf0
headerMagic uint8 = 0x95
)
type WorkoutType uint8
const (
FreeRow WorkoutType = 0x01
SingleDistance = 0x03
SingleTime = 0x05
TimedInterval = 0x06
DistanceInterval = 0x07
VariableInterval = 0x08
SingleCalorie = 0x0A
)
func (t WorkoutType) MarshalJSON() ([]byte, error) {
var s string
switch t {
case FreeRow:
s = "Free Row"
case SingleDistance:
s = "Single Distance"
case SingleTime:
s = "Single Time"
case TimedInterval:
s = "Timed Interval"
case DistanceInterval:
s = "Distance Interval"
case VariableInterval:
s = "Variable Interval"
case SingleCalorie:
s = "Single Calorie"
default:
s = "Unknown"
}
return json.Marshal(s)
}
type Workout struct {
Index uint16
Type WorkoutType
Date time.Time
UserID uint16
IntervalRestTime time.Duration
TotalDuration time.Duration
TotalDistance uint16
TotalRestDistance uint16
splitInfo uint8
PartDuration time.Duration
PartDistance uint16
PartCalories uint16
Parts []Part
}
type Part struct {
Duration time.Duration
Distance uint16
Heartrate uint8
RestHeartrate uint8
SPM uint8
SplitType uint8
IntervalRestTime time.Duration
IntervalRestDistance uint16
}
func toUint16(high byte, low byte) uint16 {
return uint16(high)<<8 | uint16(low)
}
func duration100ms(duration uint16) time.Duration {
return time.Duration(duration) * 100 * time.Millisecond
}
func dateStructure(data []byte) time.Time {
year := 2000 + int((data[0]&0xFE)>>1)
day := int((data[0]&0x01)<<4) | int((data[1]&0xF0)>>4)
month := int(data[1] & 0x0F)
hour := int(data[2])
minute := int(data[3])
return time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.Local)
}
func readFixedHeader(w *Workout, header []byte) {
w.TotalDuration = duration100ms(toUint16(header[22], header[23]))
w.TotalDistance = toUint16(header[26], header[27])
w.splitInfo = header[29] >> 4
splitSize := toUint16(header[30], header[31])
if w.splitInfo == 0 {
w.PartDuration = duration100ms(splitSize)
} else {
if w.Type == SingleCalorie {
w.PartCalories = splitSize
} else {
w.PartDistance = splitSize
}
}
}
func readIntervalHeader(w *Workout, header []byte) {
distanceDuration := toUint16(header[20], header[21])
if w.Type == DistanceInterval {
w.PartDistance = distanceDuration
} else if w.Type == TimedInterval {
w.PartDuration = duration100ms(distanceDuration)
}
w.IntervalRestTime = duration100ms(toUint16(header[22], header[23]))
sumDurationDistance := toUint16(header[26], header[27])
if w.Type == DistanceInterval {
w.TotalDuration = duration100ms(sumDurationDistance)
} else {
w.TotalDistance = sumDurationDistance
}
w.TotalRestDistance = toUint16(header[28], header[29])
}
func readVariableIntervalHeader(w *Workout, header []byte) {
w.TotalDuration = duration100ms(toUint16(header[22], header[23]))
w.TotalDistance = toUint16(header[26], header[27])
}
func readHeader(w *Workout, header []byte) error {
if len(header) != lenNormalHeader && len(header) != lenIntervalHeader {
return errorMsg("readHeader: programming mistake - not used with header data")
}
if header[0] != headerMagic {
return errorMsg("readHeader: magic byte not found")
}
w.Date = dateStructure(header[8:12])
w.UserID = toUint16(header[12], header[13])
if w.Type == DistanceInterval || w.Type == TimedInterval {
readIntervalHeader(w, header)
} else if w.Type == VariableInterval {
readVariableIntervalHeader(w, header)
} else {
readFixedHeader(w, header)
}
return nil
}
func readVariableInterval(data []byte) Part {
var v Part
v.SplitType = data[0]
v.SPM = data[1]
v.Duration = duration100ms(toUint16(data[4], data[5]))
v.Distance = toUint16(data[8], data[9])
v.Heartrate = data[10]
v.RestHeartrate = data[11]
v.IntervalRestTime = duration100ms(toUint16(data[12], data[13]))
v.IntervalRestDistance = toUint16(data[14], data[15])
return v
}
func readVariableIntervals(w *Workout, data []byte) error {
if len(data)%lenVariableInterval != 0 {
return errorMsg("variable interval data does not match definition")
}
for i := 0; i < len(data); i = i + lenVariableInterval {
part := readVariableInterval(data[i : i+lenVariableInterval])
w.Parts = append(w.Parts, part)
}
return nil
}
func readInterval(w *Workout, data []byte) Part {
var s Part
distanceDuration := toUint16(data[0], data[1])
if w.Type == DistanceInterval {
s.Duration = duration100ms(distanceDuration)
} else {
s.Distance = distanceDuration
}
s.Heartrate = data[2]
s.RestHeartrate = data[3]
s.SPM = data[4]
return s
}
func readSplit(w *Workout, data []byte) Part {
var s Part
distanceDuration := toUint16(data[0], data[1])
if w.splitInfo == 0 {
s.Distance = distanceDuration
} else {
s.Duration = duration100ms(distanceDuration)
}
s.Heartrate = data[2]
s.SPM = data[3]
return s
}
func readSplits(w *Workout, data []byte) error {
if len(data)%lenSplitFrame != 0 {
return errorMsg("split/interval data does not match definition")
}
var reader func(*Workout, []byte) Part
if w.Type == TimedInterval || w.Type == DistanceInterval {
reader = readInterval
} else {
reader = readSplit
}
for i := 0; i < len(data); i = i + lenSplitFrame {
part := reader(w, data[i:i+lenSplitFrame])
w.Parts = append(w.Parts, part)
}
return nil
}
func readParts(w *Workout, data []byte) error {
if w.Type == VariableInterval {
return readVariableIntervals(w, data)
} else {
return readSplits(w, data)
}
}
func readWorkoutData(w *Workout, data []byte, lenWorkoutHeader int) error {
header := data[0:lenWorkoutHeader]
err := readHeader(w, header)
if err == nil && len(data) > lenWorkoutHeader {
err = readParts(w, data[lenWorkoutHeader:])
}
return err
}
func checkMetaValidity(meta []byte, allWorkouts []byte) error {
if len(meta) != 32 {
return errorMsg("fillWorkout: wrong length in metadata")
}
if meta[0] != metadataMagic {
return errorMsg("fillWorkout: magic byte not found")
}
if len(allWorkouts) == 0 {
return errorMsg("fillWorkout: no workout data found")
}
return nil
}
func fillWorkout(meta []byte, allWorkouts []byte) (*Workout, error) {
err := checkMetaValidity(meta, allWorkouts)
if err != nil {
return nil, err
}
w := &Workout{}
w.Parts = make([]Part, 0)
w.Index = toUint16(meta[27], meta[26])
w.Type = WorkoutType(meta[1])
var lenWorkoutHeader int
if w.Type == TimedInterval || w.Type == DistanceInterval {
lenWorkoutHeader = lenIntervalHeader
} else {
lenWorkoutHeader = lenNormalHeader
}
numSplits := int(toUint16(meta[13], meta[12]))
storageOffset := int(toUint16(meta[17], meta[16]))
storageSize := int(toUint16(meta[25], meta[24]))
storageEnd := storageOffset + storageSize
sizePlausible := storageEnd <= len(allWorkouts)
sizePlausible = sizePlausible && storageSize >= lenWorkoutHeader
sizePlausible = sizePlausible && (storageSize-lenWorkoutHeader == numSplits*lenSplitFrame)
if !sizePlausible {
return nil, errorMsg("fillWorkout: invalid record size in access table")
}
readWorkoutData(w, allWorkouts[storageOffset:storageEnd], lenWorkoutHeader)
return w, nil
}
func ConvWorkouts(dirname string) ([]*Workout, []error) {
errors := make([]error, 0)
metadata := make([]*Workout, 0)
meta, err := ioutil.ReadFile(filepath.Join(dirname, metadataFile))
var allWorkouts []byte
if err == nil {
allWorkouts, err = ioutil.ReadFile(filepath.Join(dirname, storageFile))
}
if err == nil {
for i := 0; i < len(meta)/lenMetadata-1; i++ {
metaframe := meta[i*lenMetadata : (i+1)*lenMetadata]
m, err := fillWorkout(metaframe, allWorkouts)
if err != nil {
errors = append(errors, err)
} else {
metadata = append(metadata, m)
}
}
} else {
errors = append(errors, errorMsg(err.Error()))
}
return metadata, errors
}