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 }