/* * Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "PolarRideFile.h" #include "Units.h" #include #include #include // for std::sort #include "cmath" #include static int polarFileReaderRegistered = RideFileFactory::instance().registerReader("hrm", "Polar Precision", new PolarFileReader()); bool ScanPddFile(QFile &file, QString &hrmFile, QString &hrvFile, QString &gpxFile, QString &hrmFileCompare, QStringList &errors, QString &comment, QString &sport) { QString section = NULL; bool haveExercise=false; bool useISO8859; int lineno = 1; int nrows = 12; int ntextrows = 5; // file.open(QFile::ReadOnly) if (file.open(QFile::ReadOnly)) { // Read the entire file into a QString -- we avoid using fopen since it // doesn't handle foreign characters well. Instead we use QFile and parse // from a QString QString contents; QTextStream in(&file); #if QT_VERSION < 0x060000 in.setCodec("UTF-8"); #endif contents = in.readAll(); file.close(); // check if the text string contains the replacement character for UTF-8 encoding // if yes, try to read with Latin1/ISO 8859-1 useISO8859 = contents.contains(QChar::ReplacementCharacter); } else { errors << ("Could not open pdd file: \"" + file.fileName() + "\""); return false; } file.open(QFile::ReadOnly); QTextStream is(&file); if (useISO8859) #if QT_VERSION < 0x060000 is.setCodec ("ISO 8859-1"); #else is.setEncoding (QStringConverter::Latin1); #endif while (!is.atEnd()) { // the readLine() method doesn't handle old Macintosh CR line // endings this workaround will load the entire file if it // has CR endings then split and loop through each line // otherwise, there will be nothing to split and it will read // each line as expected. QString linesIn = is.readLine(); QStringList lines = linesIn.split('\r'); for (int li = 0; li < lines.size(); ++li) { QString line = lines[li]; // workaround for empty lines if (lines.size() == 0) { lineno++; continue; } else if (line.startsWith("[ExerciseInfo")) { lineno = 0; comment = QString(""); sport = QString(""); haveExercise = true; } else lineno ++; if (haveExercise) { if (lineno == 1) { nrows = line.section('\t', 2, 2).toInt(); ntextrows = line.section('\t', 4, 4).toInt(); } else if (lineno > (nrows+1) && lineno <= (nrows+3)) { comment += QString(line + "\n"); } else if (lineno == 3) { switch (line.section('\t', 0, 0).toInt()) { case 1: sport=QString("Run"); break; case 2: sport=QString("Bike"); break; case 3: sport=QString("Swim"); break; default: sport=QString("Bike"); break; } } else if (lineno == (nrows+4)) { hrmFile.replace(0, hrmFile.length(), line); } else if (lineno == (nrows+7)) { gpxFile.replace(0, gpxFile.length(), line); } else if (lineno == (nrows+8)) { hrvFile.replace(0, hrvFile.length(), line); } if (lineno == (nrows+ntextrows) && (hrmFile == hrmFileCompare || hrvFile == hrmFileCompare)) { file.close(); return true; } } } } file.close(); errors << ("Could not find in pdd file: \"" + file.fileName() + "\""); return false; } void HrmRideFile(RideFile *rideFile, RideFile*gpxresult, bool haveGPX, XDataSeries *hrvXdata, QFile &file, QStringList &errors, QString ¬e) { /* * Polar HRM file format documented at www.polar.fi/files/Polar_HRM_file%20format.pdf * */ QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive); QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive); bool metric = true; bool useISO8859; QDate date; double version=0; int monitor=0; double seconds=0; double distance=0; int interval = 0; int StartDelay = 0; bool speed = false; bool cadence = false; bool altitude = false; bool power = false; bool balance = false; int igpx = 0; int ngpx = 0; double lat=0,lon=0; int recInterval = 1; RideFilePoint *p; int lineno = 1; double next_interval=0; double hrv_time=0; QList intervals; if (file.open(QFile::ReadOnly)) { // Read the entire file into a QString -- we avoid using fopen since it // doesn't handle foreign characters well. Instead we use QFile and parse // from a QString QString contents; QTextStream in(&file); #if QT_VERSION < 0x060000 in.setCodec("UTF-8"); #endif contents = in.readAll(); file.close(); // check if the text string contains the replacement character for UTF-8 encoding // if yes, try to read with Latin1/ISO 8859-1 useISO8859 = contents.contains(QChar::ReplacementCharacter); } else { errors << ("Could not open ride file: \"" + file.fileName() + "\""); return; } file.open(QFile::ReadOnly); QTextStream is(&file); if (useISO8859) #if QT_VERSION < 0x060000 is.setCodec ("ISO 8859-1"); #else is.setEncoding (QStringConverter::Latin1); #endif QString section = NULL; if (haveGPX) ngpx = gpxresult->dataPoints().count(); while (!is.atEnd()) { // the readLine() method doesn't handle old Macintosh CR line // endings this workaround will load the entire file if it // has CR endings then split and loop through each line // otherwise, there will be nothing to split and it will read // each line as expected. QString linesIn = is.readLine(); QStringList lines = linesIn.split('\r'); // workaround for empty lines if(lines.size() == 0) { lineno++; continue; } for (int li = 0; li < lines.size(); ++li) { QString line = lines[li]; if (line == "") { } else if (line.startsWith("[")) { //fprintf(stderr, "section : %s\n", line.toAscii().constData()); section=line; if (section == "[HRData]") { // Some systems, like the Tacx HRM exporter, do not add an [IntTimes] section, so we need to // specify that the whole ride is one big interval. if (intervals.isEmpty()) intervals.append(seconds); next_interval = intervals.at(0); } } else if (section == "[Params]"){ if (line.contains("Version=")) { QString versionString = QString(line); versionString.remove(0,8).insert(1, "."); version = versionString.toFloat(); rideFile->setFileFormat("Polar HRM v"+versionString+" (hrm)"); } else if (line.contains("Monitor=")) { QString monitorString = QString(line); monitorString.remove(0,8); monitor = monitorString.toInt(); switch (monitor) { case 1: rideFile->setDeviceType("Polar Sport Tester / Vantage XL"); break; case 2: rideFile->setDeviceType("Polar Vantage NV (VNV)"); break; case 3: rideFile->setDeviceType("Polar Accurex Plus"); break; case 4: rideFile->setDeviceType("Polar XTrainer Plus"); break; case 6: rideFile->setDeviceType("Polar S520"); break; case 7: rideFile->setDeviceType("Polar Coach"); break; case 8: rideFile->setDeviceType("Polar S210"); break; case 9: rideFile->setDeviceType("Polar S410"); break; case 10: rideFile->setDeviceType("Polar S510"); break; case 11: rideFile->setDeviceType("Polar S610 / S610i"); break; case 12: rideFile->setDeviceType("Polar S710 / S710i"); break; case 13: rideFile->setDeviceType("Polar S810 / S810i"); break; case 15: rideFile->setDeviceType("Polar E600"); break; case 20: rideFile->setDeviceType("Polar AXN500"); break; case 21: rideFile->setDeviceType("Polar AXN700"); break; case 22: rideFile->setDeviceType("Polar S625X / S725X"); break; case 23: rideFile->setDeviceType("Polar S725"); break; case 33: rideFile->setDeviceType("Polar CS400"); break; case 34: rideFile->setDeviceType("Polar CS600X"); break; case 35: rideFile->setDeviceType("Polar CS600"); break; case 36: rideFile->setDeviceType("Polar RS400"); break; case 37: rideFile->setDeviceType("Polar RS800"); break; case 38: rideFile->setDeviceType("Polar RS800X"); break; default: rideFile->setDeviceType(QString("Unknown Polar Device %1").arg(monitor)); } } else if (line.contains("SMode=")) { line.remove(0,6); QString smode = QString(line); speed = smode.at(0) == '1'; cadence = smode.length()>0 && smode.at(1) == '1'; altitude = smode.length()>1 && smode.at(2) == '1'; power = smode.length()>2 && smode.at(3) == '1'; balance = smode.length()>3 && smode.at(4) == '1'; // pedaling_index = smode.length()>4 && smode.at(5)=='1'; // // It appears that the Polar CS600 exports its data alays in metric when downloaded from the // polar software even when English units are displayed on the unit.. It also never sets // this bit low in the .hrm file. This will have to get changed if other software downloads // this differently // if (smode.length()>6 && smode.at(7)=='1') metric = false; } else if (line.contains("Interval=")) { recInterval = line.remove(0,9).toInt(); if (recInterval==238) { /* This R-R data */ rideFile->setRecIntSecs(1); } else { rideFile->setRecIntSecs(recInterval); } } else if (line.contains("Date=")) { line.remove(0,5); date= QDate(line.left(4).toInt(), line.mid(4,2).toInt(), line.mid(6,2).toInt()); } else if (line.contains("StartTime=")) { line.remove(0,10); QDateTime datetime(date, QTime(line.left(2).toInt(), line.mid(3,2).toInt(), line.mid(6,2).toInt())); rideFile->setStartTime(datetime); } else if (line.contains("StartDelay=")) { StartDelay = line.remove(0,11).toInt(); if (recInterval==238) { seconds = StartDelay/1000.0; } else { seconds = recInterval; } } } else if (section == "[Note]") { note.append(line); } else if (section == "[IntTimes]") { double int_seconds = line.left(2).toInt()*60*60+line.mid(3,2).toInt()*60+line.mid(6,3).toFloat(); intervals.append(int_seconds); if (lines.size()==1) { is.readLine(); is.readLine(); if (version>1.05) { is.readLine(); is.readLine(); } } else { li+=2; if (version>1.05) li+=2; } } else if (section == "[HRData]") { double nm=0,kph=0,watts=0,km=0,cad=0,hr=0,alt=0,hrm=0; double lrbalance=RideFile::NA; int i=0; hrm = line.section('\t', i, i).toDouble(); i++; if (speed) { kph = line.section('\t', i, i).toDouble()/10; distance += kph/60/60*recInterval; km = distance; i++; } if (cadence) { cad = line.section('\t', i, i).toDouble(); i++; } if (altitude) { alt = line.section('\t', i, i).toDouble(); i++; } if (power) { watts = line.section('\t', i, i).toDouble(); i++; } if (balance) { // Power LRB + PI: The value contains : // - Left Right Balance (LRB) and // - Pedaling Index (PI) // // in the following formula: // value = PI * 256 + LRB PI bits 15-8 LRB bits 7-0 // LRB is the value of left foot // for example if LRB = 45, actual balance is L45 - 55R. // PI values are percentages from 0 to 100. // For example value 12857 (= 40 * 256 + 47) // means: PI = 40 and LRB = 47 => L47 - 53R lrbalance = line.section('\t', i, i).toInt() & 0xff; i++; } if (next_interval < seconds) { interval = intervals.indexOf(next_interval); if (intervals.count()>interval+1){ interval++; next_interval = intervals.at(interval); } } if (!metric) { km *= KM_PER_MILE; kph *= KM_PER_MILE; alt *= METERS_PER_FOOT; } if (recInterval==238){ XDataPoint *p_hrv = new XDataPoint(); hrv_time += hrm/1000.0; p_hrv->secs = hrv_time; p_hrv->number[0] = hrm; hrvXdata->datapoints.append(p_hrv); hr = 60000.0/hrm; } else { hr = hrm; } if (haveGPX && gpxresult && (igpxdataPoints()[igpx]; // Use previous value if GPS is momentarely // lost. Should have option for interpolating. if (p->lat!=0.0 && p->lon!=0.0) { lat = p->lat; lon = p->lon; // Must check if current HRM speed is zero while // we have GPX speed if (kph==0.0 && p->kph>1.0) { kph = p->kph; distance += kph/60/60*recInterval; km = distance; } } if (seconds>=p->secs) igpx += 1; } rideFile->appendPoint(seconds, cad, hr, km, kph, nm, watts, alt, lon, lat, 0.0, 0.0, RideFile::NA, lrbalance, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, interval); // fprintf(stderr, " %f, %f, %f, %f, %f, %f, %f, %d\n", seconds, cad, hr, km, kph, nm, watts, alt, interval); if (recInterval==238) { seconds += hrm / 1000.0; } else { seconds += recInterval; } } ++lineno; } } rideFile->setTag("Notes", note); QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_" "(\\d\\d)_(\\d\\d)_(\\d\\d)\\.hrm$"); if (rideTime.indexIn(file.fileName()) >= 0) { QDateTime datetime(QDate(rideTime.cap(1).toInt(), rideTime.cap(2).toInt(), rideTime.cap(3).toInt()), QTime(rideTime.cap(4).toInt(), rideTime.cap(5).toInt(), rideTime.cap(6).toInt())); rideFile->setStartTime(datetime); } file.close(); } RideFile *PolarFileReader::openRideFile(QFile &file, QStringList &errors, QList*rideList) const { /* Quit if file does not exist or cannot be read (use permissions instead)*/ if (!file.open(QFile::ReadOnly)) { errors << ("Could not open ride file: \"" + file.fileName() + "\""); return NULL; } else file.close(); QString hrmFile(""); QString hrvFile(""); QString gpxFile(""); QString comment(""); QFile hrmfile; QFile hrvfile; QFile gpxfile; RideFile *rideFile = new RideFile(); bool haveGPX = false; bool haveHRV = false; bool haveHRM = false; // Read Polar GPX file (if exist with same name as hrm file). RideFile *gpxresult=NULL; QString suffix = file.fileName(); int n_s = suffix.length(); /* Try to scan .pdd file if exsist */ QString location; QString hrmFileDate; QString hrmFile_orig; QFile pddfile; QString sport=""; if (n_s > 12) { location = suffix.mid(0, n_s - 12); hrmFileDate = suffix.mid(n_s - 12, 6); hrmFile_orig = suffix.mid(n_s - 12, n_s); pddfile.setFileName(location + "20" + hrmFileDate + ".pdd"); } if (n_s > 12 && pddfile.exists()) { /* Scan .pdd file for main hrm, R-R version and gpx. Can use either or both if exist */ QString pddComment; if (!ScanPddFile(pddfile, hrmFile, hrvFile, gpxFile, hrmFile_orig, errors, pddComment, sport)) { // Pdd file exist but it does not contain // hrmFile_orig (probably been deleted) delete rideFile; return NULL; } else if (hrmFile_orig == hrvFile && hrmFile != hrvFile) { errors << ("This is a R-R file: \"" + file.fileName() + "\""); delete rideFile; return NULL; } haveHRM = hrmFile.length() > 0 && hrmFile == hrmFile_orig; if (haveHRM) { hrmfile.setFileName(location + hrmFile); comment.append(pddComment); } haveGPX = gpxFile.length() > 0; if (haveGPX) gpxfile.setFileName(location + gpxFile); haveHRV = hrvFile.length() > 0 && haveHRM && hrmFile != hrvFile; if (haveHRV) hrvfile.setFileName(location + hrvFile); if (!haveHRV&&!haveHRM) { // Don't think this will ever happen. haveHRM = true; hrmfile.setFileName(suffix); } } else { /* Search for .gpx and .rr.hrm */ if (suffix.endsWith("rr.hrm")) { hrmfile.setFileName(QString(suffix).replace("rr.hrm",".hrm")); if (hrmfile.exists()){ // If both exist only read when equal to ".hrm" errors << ("This is a R-R file: \"" + file.fileName() + "\""); delete rideFile; return NULL; } else { // We read it as a hrm file and get the Xdata directly. haveHRV = false; } gpxfile.setFileName(QString(suffix).replace("rr.hrm",".gpx")); } else { hrvfile.setFileName(QString(suffix).replace(".hrm",".rr.hrm")); gpxfile.setFileName(QString(suffix).replace(".hrm",".gpx")); haveHRV = hrvfile.exists(); } hrmfile.setFileName(suffix); haveHRM = hrmfile.exists(); if (!haveHRM) { errors << ("Can not find : \"" + file.fileName() + "\""); delete rideFile; return NULL; } haveGPX = gpxfile.exists(); } if (haveGPX) { GpxFileReader reader; gpxresult = reader.openRideFile(gpxfile,errors,rideList); } XDataSeries *hrvXdata = new XDataSeries(); hrvXdata->name = "HRV"; hrvXdata->valuename << "R-R"; hrvXdata->unitname << "msecs"; if (haveHRM) { HrmRideFile(rideFile, gpxresult, haveGPX, hrvXdata, hrmfile, errors, comment); if (haveHRV) { // Get the HRV data RideFile *hrv_rideFile = new RideFile(); HrmRideFile(hrv_rideFile, gpxresult, haveGPX, hrvXdata, hrvfile, errors, comment); delete hrv_rideFile; } } else if (hrvFile.length() > 0 && !file.fileName().contains(hrvFile)) { HrmRideFile(rideFile, gpxresult, haveGPX, hrvXdata, file, errors, comment); } else { // Don't think this will ever happen. errors << ("This is a R-R file?: \"" + file.fileName() + "\""); delete rideFile; delete hrvXdata; return NULL; } if (hrvXdata->datapoints.count()>0) { rideFile->addXData("HRV", hrvXdata); } else delete hrvXdata; rideFile->setTag("Sport", sport); return rideFile; }