package de.saring.exerciseviewer.parser.impl;
import de.saring.exerciseviewer.core.EVException;
import de.saring.exerciseviewer.data.*;
import de.saring.exerciseviewer.parser.AbstractExerciseParser;
import de.saring.exerciseviewer.parser.ExerciseParserInfo;
import de.saring.util.unitcalc.ConvertUtils;
import java.time.LocalDateTime;
/**
* This implementation of an ExerciseParser is for reading RAW files of the
* Polar S4- and S5-Series devices. The exercises of these devices are very
* similar, better models are containing more informations, which can be
* found on different indices, depending on the model.
* <br/>
* Currently only the S510 device is supported: Other models in the S4 and S5
* series should not be too hard to add.
* <br/>
* It is assumed that the exercise files have the extension ".hsr". You can use
* SonicRead (http://code.google.com/p/sonicread/) to extract the exercise data
* from your Polar device to a hsr file.
* <p/>
* This file is based on PolarSRawParser.java by Stefan Saring
*
* @author Remco den Breeje
* @version 1.0
*/
public class PolarHsrRawParser extends AbstractExerciseParser {
/**
* Informations about this parser.
*/
private final ExerciseParserInfo info = new ExerciseParserInfo("Polar HSR", new String[]{"hsr", "HSR"});
/**
* The binary data of the exercise file.
*/
private int[] fileContent;
/**
* section data
*/
private int[][] s;
@Override
public ExerciseParserInfo getInfo() {
return info;
}
private int sdata(int section, int index) throws EVException {
section += index / 60;
index %= 60;
if (section >= s.length) {
throw new EVException(String.format("Error! Section %d does not exist", section));
}
if (index >= s[section].length) {
throw new EVException(String.format("Error! Byte %d in section %d does not exist (%d)",
index, section, s[section].length));
}
return s[section][index];
}
@Override
public EVExercise parseExercise(String filename) throws EVException {
// interval values used by Polar
short[] interval = {5, 15, 30, 60, 120, 240, 300, 480};
// read binary file content to array
fileContent = readFileToByteArray(filename);
// create an PVExercise object from this data and set file type
// TODO - support S410 and S520
EVExercise exercise = new EVExercise();
exercise.setFileType(EVExercise.ExerciseFileType.S510RAW);
exercise.setDeviceName("Polar S4xx/S5xx Series");
// get bytes in file
int bytesInFile = (fileContent[1] * 0x100) + fileContent[0];
if (bytesInFile != fileContent.length) {
throw new EVException("The exercise file is not valid, the file length is not correct ...");
}
// get data bytes
int bytesInData = bytesInFile - 2;
int[] data = new int[bytesInData];
System.arraycopy(fileContent, 2, data, 0, bytesInData);
// process data
int ii = 0;
int sectionIx = 0;
int sectionsInData = 0;
s = new int[1][1];
while (ii < bytesInData) {
if (sectionIx == 0) {
// get first section
if (data[0] == 85) {
sectionsInData = data[3];
// alloc mem
s = new int[sectionsInData + 1][];
// ok, first section read, continue
sectionIx++;
ii += 8;
continue;
} else {
throw new EVException("The exercise file is not valid, the first section could not be found");
}
} else {
// find new section
if (data[ii] == 85) {
// check section number
if (data[ii + 1] != sectionIx) {
throw new EVException("Wrong section index in file");
}
// allocate memory for this section
int sectionLength = data[ii + 2];
s[sectionIx] = new int[sectionLength];
// set data in sections array (s)
System.arraycopy(data, ii + 3, s[sectionIx], 0, sectionLength);
//System.out.format(">>> new section #%d(%d) found at %d with %d bytes\n",
// sectionIx, sectionsInData, ii, sectionLength);
// ok, section read, continue
sectionIx++;
ii += sectionLength + 5; // +5 -> section header length
continue;
}
}
// done. check for no-more-sections byte at the end of the file
if ((sectionIx - 1) != sectionsInData) {
throw new EVException("Could not find all sections");
}
if (data[ii] != 7) {
throw new EVException("Could not find no-more-sections byte in file");
}
// ok, all set
break;
}
// get the number of samples recorded. floor of (seconds / hertz).
// Watch seems to always record a last entry, even partial seconds: +1;
int numberOfSamples = sdata(1, 0);
// get recording interval
int intix = sdata(1, 1) - 95;
if (intix >= interval.length)
throw new EVException("Recording interval is not valid ...");
exercise.setRecordingInterval(interval[intix]);
// TODO does the heartrate ranges are specified by absolute or percentual values?
boolean fHeartRateRangeAbsolute = true; //(sdata(1,1) & 0x10) == 0;
// get exercise type
byte typeNr = (byte) sdata(1, 2);
if (typeNr > 0) {
// get exercise type label
StringBuilder sbExerciseLabel = new StringBuilder();
for (int i = 0; i < 7; i++) {
sbExerciseLabel.append(decodeChar(sdata(1, i + 3)));
}
exercise.setType(sbExerciseLabel.toString());
} else {
exercise.setType("BasicUse");
}
// get exercise date
int dateSeconds = decodeBCD(sdata(1, 10));
int dateMinutes = decodeBCD(sdata(1, 11));
int dateHours = decodeBCD(sdata(1, 12) & 0x7f);
int dateDay = decodeBCD(sdata(1, 13) & 0x7f);
int dateYear = (2000 + sdata(1, 14));
int dateMonth = sdata(1, 15) & 0x0f;
// when bit 7 of byte 12 is set, then the hour is in PM mode
if (dateHours < 12 && (sdata(1, 12) & 0x80) == 0x80) {
dateHours += 12;
}
exercise.setDateTime(LocalDateTime.of(dateYear, dateMonth, dateDay, dateHours, dateMinutes, dateSeconds));
// get duration
int durationTenthSeconds = (sdata(1, 15) >> 4);
int durationSeconds = decodeBCD(sdata(1, 16));
int durationMinutes = decodeBCD(sdata(1, 17));
int durationHours = decodeBCD(sdata(1, 18));
exercise.setDuration((durationHours * 60 * 60 * 10) + (durationMinutes * 60 * 10) + durationSeconds * 10 + durationTenthSeconds);
// get heartrate data
exercise.setHeartRateAVG((short) sdata(1, 19));
exercise.setHeartRateMax((short) sdata(1, 20));
// get number of laps
int numberOfMeas = decodeBCD(sdata(1, 21));
int numberOfLaps = decodeBCD(sdata(1, 22));
// get unit format from bit 1 of byte 25
// => 0 = metric, 1 = english
boolean fMetricUnits = (sdata(1, 25) & 0x02) == 0x00;
// decode recording mode (heartrate is always recorded)
RecordingMode recMode = new RecordingMode();
exercise.setRecordingMode(recMode);
boolean fBike2 = (sdata(1, 26) & 0x20) == 0x20;
boolean fBike1 = (sdata(1, 26) & 0x10) == 0x10;
recMode.setPower(false);
recMode.setCadence(false);
recMode.setAltitude(false);
if (!fBike1 && !fBike2) {
recMode.setSpeed(false);
recMode.setBikeNumber((byte) 0);
} else {
recMode.setSpeed(true);
if (fBike1) {
recMode.setBikeNumber((byte) 1);
} else {
recMode.setBikeNumber((byte) 2);
}
}
// if more measurements than laps are taken -> interval training
recMode.setIntervalExercise(numberOfMeas != numberOfLaps);
// get the heartrate limit data (Polar S510 has 3 limits)
int indexHRLimitStart = 28;
exercise.setHeartRateLimits(new HeartRateLimit[3]);
exercise.getHeartRateLimits()[0] = decodeHeartRateLimit(indexHRLimitStart + 0, indexHRLimitStart + 9);
exercise.getHeartRateLimits()[1] = decodeHeartRateLimit(indexHRLimitStart + 2, indexHRLimitStart + 18);
exercise.getHeartRateLimits()[2] = decodeHeartRateLimit(indexHRLimitStart + 4, indexHRLimitStart + 27);
for (HeartRateLimit hrLimit : exercise.getHeartRateLimits()) {
hrLimit.setAbsoluteRange(fHeartRateRangeAbsolute);
}
// get energy (in kCal)
int energyPart1 = decodeBCD(sdata(1, 69 + 0));
int energyPart2 = decodeBCD(sdata(1, 69 + 1));
int energyPart3 = decodeBCD(sdata(1, 69 + 2));
exercise.setEnergy((energyPart1 + (energyPart2 * 100) + (energyPart3 * 10000)) / 10);
// get total energy (in kCal)
int energyTotalPart1 = decodeBCD(sdata(1, 72 + 0));
int energyTotalPart2 = decodeBCD(sdata(1, 72 + 1));
int energyTotalPart3 = decodeBCD(sdata(1, 72 + 2));
exercise.setEnergyTotal(energyTotalPart1 + (energyTotalPart2 * 100) + (energyTotalPart3 * 10000));
// get cumulative workout time
int cumWorkoutPart1 = decodeBCD(sdata(1, 75 + 0));
int cumWorkoutPart2 = decodeBCD(sdata(1, 75 + 1));
int cumWorkoutPart3 = decodeBCD(sdata(1, 75 + 2));
exercise.setSumExerciseTime(cumWorkoutPart3 + (cumWorkoutPart1 * 60) + (cumWorkoutPart2 * 60 * 100));
// get cumulative ride time
int cumRidePart1 = decodeBCD(sdata(1, 78 + 0));
int cumRidePart2 = decodeBCD(sdata(1, 78 + 1));
int cumRidePart3 = decodeBCD(sdata(1, 78 + 2));
exercise.setSumRideTime(cumRidePart3 + (cumRidePart1 * 60) + (cumRidePart2 * 60 * 100));
// get odometer
int odometerPart1 = decodeBCD(sdata(1, 81 + 0));
int odometerPart2 = decodeBCD(sdata(1, 81 + 1));
int odometerPart3 = decodeBCD(sdata(1, 81 + 2));
int odometer = odometerPart1 + (odometerPart2 * 100) + (odometerPart3 * 10000);
if (fMetricUnits) {
exercise.setOdometer(odometer);
} else {
exercise.setOdometer(ConvertUtils.convertMiles2Kilometer(odometer));
}
// get speed (bicycle) related data of exercise (if recorded)
if (recMode.isSpeed()) {
ExerciseSpeed speed = new ExerciseSpeed();
exercise.setSpeed(speed);
// get exercise distance (in 1/10th of km)
int distance = (sdata(1, 84) + (sdata(1, 85) << 8)) * 100;
if (fMetricUnits) {
speed.setDistance(distance);
} else {
speed.setDistance(ConvertUtils.convertMiles2Kilometer(distance));
}
// get AVG speed
int avgSpeedPart1 = sdata(1, 86);
int avgSpeedPart2 = (sdata(1, 87) & 0x0f);
float avgSpeed = ((avgSpeedPart2 << 8) | avgSpeedPart1) / 16f;
if (fMetricUnits) {
speed.setSpeedAVG(avgSpeed);
} else {
speed.setSpeedAVG((float) ConvertUtils.convertMiles2Kilometer(avgSpeed));
}
// get max speed
int maxSpeedPart1 = sdata(1, 87) >> 4;
int maxSpeedPart2 = sdata(1, 88);
float maxSpeed = ((maxSpeedPart2 << 4) | maxSpeedPart1) / 16f;
if (fMetricUnits) {
speed.setSpeedMax(maxSpeed);
} else {
speed.setSpeedMax((float) ConvertUtils.convertMiles2Kilometer(maxSpeed));
}
}
// get cadence (bicycle) data of exercise (if recorded)
/*
if (recMode.isCadence ()) {
ExerciseCadence cadence = new ExerciseCadence ();
exercise.setCadence (cadence);
cadence.setCadenceAVG ((short) sdata(1, 89)); // TODO, does this work?
cadence.setCadenceMax ((short) sdata(1, 90));
} */
////////// decode lap data /////////////
// calculate lap length (bytes) => depends on what is recorded
int lapSize = 11;
if (recMode.isIntervalExercise())
lapSize += 5;
if (recMode.isAltitude()) lapSize += 5;
//if (recMode.isSpeed ()) other size?
// determine in which section the lap data starts
int lapsec = 4 + (numberOfSamples - 1) / 60;
// add offset for speed data
if (recMode.isSpeed())
lapsec += 1 + (numberOfSamples - 1) / 60;
// determine in how _many_ sections the lap data resides
int lapseccnt = (int) (((double) (numberOfMeas * lapSize) / 60) + .9999);
// check section size
if (s[lapsec + lapseccnt - 1].length != (numberOfMeas * lapSize) % 60) {
throw new EVException(String.format("Lap-data section (%d) has wrong size (%d instead of %d)",
lapsec + lapseccnt - 1, s[lapsec + lapseccnt - 1].length, (numberOfMeas * lapSize) % 60));
}
// process all laps
if (recMode.isIntervalExercise()) {
exercise.setLapList(new Lap[numberOfMeas]);
} else {
exercise.setLapList(new Lap[numberOfLaps]);
}
for (int l = 0; l < exercise.getLapList().length; l++) {
int os = l * lapSize; // data offset
// get offset where the current lap starts
Lap lap = new Lap();
exercise.getLapList()[l] = lap;
// get lap split time (in 1/10th seconds)
int bLapEndHour = sdata(lapsec, os + 2);
int bLapEndMinute = sdata(lapsec, os + 1) & 0x3f;
int bLapEndSecond = sdata(lapsec, os + 0) & 0x3f;
int bLapEndTenthSecond = 4 * (sdata(lapsec, os + 1) >> 6) + (sdata(lapsec, os + 0) >> 6);
lap.setTimeSplit(bLapEndTenthSecond + (bLapEndSecond * 10) + (bLapEndMinute * 60 * 10) + (bLapEndHour * 60 * 60 * 10));
// get heartrate data of lap
lap.setHeartRateSplit((short) sdata(lapsec, os + 3));
lap.setHeartRateAVG((short) sdata(lapsec, os + 4));
lap.setHeartRateMax((short) sdata(lapsec, os + 5));
// get speed (bicycle) related data of lap (if recorded)
if (recMode.isSpeed()) {
lap.setSpeed(new LapSpeed());
// TODO get lap distance (in 1/10th of km)
int lapDistance = sdata(lapsec, os + 6);
lapDistance += (sdata(lapsec, os + 7) << 8);
//lapDistance += (sdata(lapsec, os + 8) << 16); not sure about this byte..
lapDistance *= 100;
if (fMetricUnits) {
lap.getSpeed().setDistance(lapDistance);
} else {
lap.getSpeed().setDistance(ConvertUtils.convertMiles2Kilometer(lapDistance));
}
// get speed at end of lap
float lapEndSpeed = sdata(lapsec, os + 9);
lapEndSpeed += sdata(lapsec, os + 10) << 8;
lapEndSpeed *= 5.0f / 80;
if (fMetricUnits) {
lap.getSpeed().setSpeedEnd(lapEndSpeed);
} else {
lap.getSpeed().setSpeedEnd((float) ConvertUtils.convertMiles2Kilometer(lapEndSpeed));
}
} // end of if(isSpeed())
// process exercise interval data | TODO, implement in polarviewer
// flag stands for (TBC): 1 == warmup, 2 == interval, 3 == cooling down, 0 == basicuse
//System.out.format("flag: %d\n", sdata(lapsec, os + 15) >> 5);
//System.out.format("recovery time: %d [sec]\n", sdata(lapsec, os + 13)*60, sdata(lapsec, os + 12));
//System.out.format("recovery heart rate: %d [bpm]\n", ( (sdata(lapsec, os+14) < 254 ) ? sdata(lapsec, os+14) : -1));
}
////////// decode sample data /////////////
int hrsec = 3;
int spdsec = 4 + (numberOfSamples - 1) / 60;
// create sample list
exercise.setSampleList(new ExerciseSample[numberOfSamples]);
// process all recorded samples
for (int i = 0; i < numberOfSamples; i++) {
ExerciseSample exeSample = new ExerciseSample();
exeSample.setTimestamp(i * exercise.getRecordingInterval() * 1000L);
exercise.getSampleList()[i] = exeSample;
// get sample heartrate
exeSample.setHeartRate((short) sdata(hrsec, i));
// get bicycle related data (if recorded)
if (recMode.isSpeed()) {
// get sample speed
float sampleSpeed = (float) (sdata(spdsec, i)) / 2f;
if (fMetricUnits) {
exeSample.setSpeed(sampleSpeed);
} else {
exeSample.setSpeed((float) ConvertUtils.convertMiles2Kilometer(sampleSpeed));
}
}
}
// when speed is recorded => calculate distance for each recorded sample
// (distance is not recorded for each sample)
if (recMode.isSpeed()) {
// helper for compute sample distance (meters)
double distanceAccum = 0f;
// process all recorded samples
for (ExerciseSample exeSample : exercise.getSampleList()) {
// compute sample distance (it's not recorded)
exeSample.setDistance((int) distanceAccum);
distanceAccum += (exeSample.getSpeed() * exercise.getRecordingInterval()) / 3.6f;
}
}
// repair distance values of samples
exercise.repairSamples();
// done :-)
return exercise;
}
/**
* Decodes a character in Polar format to normal character value.
*
* @param value value to decode
* @return normal character value of Polar character value
*/
private char decodeChar(int value) {
char cDecoded = '?';
switch (value) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
cDecoded = (char) ('0' + value);
break;
case 10:
cDecoded = ' ';
break;
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
case 18:
case 19:
case 20:
case 21:
case 22:
case 23:
case 24:
case 25:
case 26:
case 27:
case 28:
case 29:
case 30:
case 31:
case 32:
case 33:
case 34:
case 35:
case 36:
cDecoded = (char) ('A' + value - 11);
break;
case 37:
case 38:
case 39:
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
case 58:
case 59:
case 60:
case 61:
case 62:
cDecoded = (char) ('a' + value - 37);
break;
case 63:
cDecoded = '-';
break;
case 64:
cDecoded = '%';
break;
case 65:
cDecoded = '/';
break;
case 66:
cDecoded = '(';
break;
case 67:
cDecoded = ')';
break;
case 68:
cDecoded = '*';
break;
case 69:
cDecoded = '+';
break;
case 70:
cDecoded = '.';
break;
case 71:
cDecoded = ':';
break;
case 72:
cDecoded = '?';
break;
default:
break;
}
return cDecoded;
}
/**
* Decodes a BCD "byte" (in Polar format) to normal int value.
*
* @param value BCD "byte" to decode
* @return normal int value of BCD value
*/
private int decodeBCD(int value) {
// (upper 4 bits * 10) + lower 4 bits
return ((value >> 4) * 10) + (value & 0x0f);
}
/**
* This method decodes the data for a HeartRateLimit object (limits and times
* below, within and above) and returns it.
*
* @param offsetLimits offset in fileContent, where the limit data starts
* @param offsetTimes offset in fileContent, where the times data starts
* @return the filled HeartRateLimit object
*/
private HeartRateLimit decodeHeartRateLimit(int offsetLimits, int offsetTimes) throws EVException {
HeartRateLimit hrLimit = new HeartRateLimit();
hrLimit.setLowerHeartRate((short) sdata(1, offsetLimits + 0));
hrLimit.setUpperHeartRate((short) sdata(1, offsetLimits + 1));
int hrLimitBelowSecs = decodeBCD(sdata(1, offsetTimes + 0));
hrLimitBelowSecs += decodeBCD(sdata(1, offsetTimes + 1)) * 60;
hrLimitBelowSecs += decodeBCD(sdata(1, offsetTimes + 2)) * 60 * 60;
hrLimit.setTimeBelow(hrLimitBelowSecs);
int hrLimitWithinSecs = decodeBCD(sdata(1, offsetTimes + 3));
hrLimitWithinSecs += decodeBCD(sdata(1, offsetTimes + 4)) * 60;
hrLimitWithinSecs += decodeBCD(sdata(1, offsetTimes + 5)) * 60 * 60;
hrLimit.setTimeWithin(hrLimitWithinSecs);
int hrLimitAboveSecs = decodeBCD(sdata(1, offsetTimes + 6));
hrLimitAboveSecs += decodeBCD(sdata(1, offsetTimes + 7)) * 60;
hrLimitAboveSecs += decodeBCD(sdata(1, offsetTimes + 8)) * 60 * 60;
hrLimit.setTimeAbove(hrLimitAboveSecs);
return hrLimit;
}
}