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 java.time.LocalDateTime;
/**
* This implementation of an ExerciseParser is for reading RAW files of the
* Polar F6 and F11 devices. These devices are limited HRMs only supporting
* exercise statistics but no sampled data.
* <br/>
* Currently only the F6 and F11 is supported due to the lack of other HRMs to
* test.
* <br/>
* Usually the exercise files have the extension ".frd". They can be read
* by using the SonicLink from the Polar device with the tool RS-200 decoder
* (available for free at http://sourceforge.net/projects/rs200-decoder) and
* converted using the f6-split-tool (http://toazter.ch/sites/F6SplitTool).
* This is a little complicated way to go at the moment but a better solution is
* planned.
* <br/>
* The parser is based on the PolarSRawParser by Stefan Saring.
*
* @author Roland Hostettler
* @version 1.1
*/
public class PolarF6RawParser extends AbstractExerciseParser {
/**
* The exercise file length for F6 watches
*/
private static final int F6_EXERCISE_FILE_LENGTH = 49;
/**
* The exercise file length for F11 watches
*/
private static final int F11_EXERCISE_FILE_LENGTH = 50;
/**
* Informations about this parser.
*/
private final ExerciseParserInfo info = new ExerciseParserInfo("Polar F6/F11", new String[]{"frd", "FRD"});
/**
* The binary data of the exercise file.
*/
private int[] fileContent;
@Override
public ExerciseParserInfo getInfo() {
return info;
}
@Override
public EVExercise parseExercise(String filename) throws EVException {
// read binary file content to array
fileContent = readFileToByteArray(filename);
// create an PVExercise object from this data and set file type
EVExercise exercise = new EVExercise();
exercise.setFileType(EVExercise.ExerciseFileType.F6RAW);
exercise.setDeviceName("Polar F Series");
// check wheter the read data fits the expected data length
if (fileContent.length != F6_EXERCISE_FILE_LENGTH && fileContent.length != F11_EXERCISE_FILE_LENGTH) {
throw new EVException("The exercise file is not valid, the file length is not correct ...");
}
// read the exercise name
StringBuilder exerciseName = new StringBuilder();
int i = 0;
while ((fileContent[i] & 0x80) != 0x80 && i < 8) {
exerciseName.append(decodeChar(fileContent[i]));
i++;
}
exerciseName.append(String.valueOf(fileContent[i] - 0x80));
exercise.setType(exerciseName.toString());
// read the exercise date
int dateDay = fileContent[8];
int dateMonth = fileContent[9];
int dateYear = 2000 + fileContent[10];
int dateSeconds = decodeBCD(fileContent[11]);
int dateMinutes = decodeBCD(fileContent[12]);
int dateHours = decodeBCD(fileContent[13]);
// add exercise to the calendar
exercise.setDateTime(LocalDateTime.of(dateYear, dateMonth, dateDay, dateHours, dateMinutes, dateSeconds));
// get duration
int durationSeconds = decodeBCD(fileContent[14]);
int durationMinutes = decodeBCD(fileContent[15]);
int durationHours = decodeBCD(fileContent[16]);
exercise.setDuration((durationHours * 60 * 60 * 10) + (durationMinutes * 60 * 10) + durationSeconds * 10);
// get heartrate data
exercise.setHeartRateAVG((short) fileContent[17]);
exercise.setHeartRateMax((short) fileContent[18]);
// decode recording mode (heartrate is always recorded)
// (not available in F6/F11 files)
RecordingMode recMode = new RecordingMode();
exercise.setRecordingMode(recMode);
// Note: The HR limits 1, 2 and 3 represent the zones "light",
// "moderate" and "hard". With the actual knowledge, it's not possible
// to get the bpm limits out of the watch for these limits. Therefore,
// the percental heartrate representation is used
// get the heartrate limit data (3 zones + the selected zone)
exercise.setHeartRateLimits(new HeartRateLimit[4]);
exercise.getHeartRateLimits()[0] = decodeHeartRateLimit(35, 23); // in-zone
exercise.getHeartRateLimits()[1] = decodeHeartRateLimit(37, 26); // light
exercise.getHeartRateLimits()[1].setLowerHeartRate((short) 60);
exercise.getHeartRateLimits()[1].setUpperHeartRate((short) 70);
exercise.getHeartRateLimits()[1].setAbsoluteRange(false);
exercise.getHeartRateLimits()[2] = decodeHeartRateLimit(39, 29); // moderate
exercise.getHeartRateLimits()[2].setLowerHeartRate((short) 71);
exercise.getHeartRateLimits()[2].setUpperHeartRate((short) 80);
exercise.getHeartRateLimits()[2].setAbsoluteRange(false);
exercise.getHeartRateLimits()[3] = decodeHeartRateLimit(41, 32); // hard
exercise.getHeartRateLimits()[3].setLowerHeartRate((short) 81);
exercise.getHeartRateLimits()[3].setUpperHeartRate((short) 90);
exercise.getHeartRateLimits()[3].setAbsoluteRange(false);
// get energy (in kCal)
int energyLowByte = fileContent[19];
int energyHighByte = fileContent[20];
exercise.setEnergy(energyLowByte + (energyHighByte << 8));
// set an empty LapList and SampleList
exercise.setLapList(new Lap[0]);
exercise.setSampleList(new ExerciseSample[0]);
// Note: the following data is appended from the totals section to each
// exercise by the f6-split-tool to enhance the ExerciseViewer display
// set the offset if the file is a 50 byte F11 file
int offset = 0;
if (fileContent.length == F11_EXERCISE_FILE_LENGTH) {
offset = 1;
}
// get the total exercise time
int cumWorkoutSeconds = decodeBCD(fileContent[43 + offset]);
int cumWorkoutMinutes = decodeBCD(fileContent[44 + offset]);
int cumWorkoutHours = decodeBCD(fileContent[45 + offset]);
exercise.setSumExerciseTime((cumWorkoutHours * 60) + cumWorkoutMinutes + (cumWorkoutSeconds / 60));
// get total energy
int energyTotalPart1 = decodeBCD(fileContent[46 + offset]);
int energyTotalPart2 = decodeBCD(fileContent[47 + offset]);
int energyTotalPart3 = decodeBCD(fileContent[48 + offset]);
exercise.setEnergyTotal((energyTotalPart3 * 10000) + (energyTotalPart2 * 100) + energyTotalPart1);
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) {
HeartRateLimit hrLimit = new HeartRateLimit();
hrLimit.setLowerHeartRate((short) fileContent[offsetLimits + 0]);
hrLimit.setUpperHeartRate((short) fileContent[offsetLimits + 1]);
int hrLimitWithinSecs = decodeBCD(fileContent[offsetTimes]);
hrLimitWithinSecs += decodeBCD(fileContent[offsetTimes + 1]) * 60;
hrLimitWithinSecs += decodeBCD(fileContent[offsetTimes + 2]) * 60 * 60;
hrLimit.setTimeWithin(hrLimitWithinSecs);
return hrLimit;
}
}