RideFile * run() { errors.clear(); rideFile = new RideFile; rideFile->setDeviceType("Joule GPS"); rideFile->setFileFormat("CycleOps Joule (bin2)"); rideFile->setRecIntSecs(1); if (!file.open(QIODevice::ReadOnly)) { delete rideFile; return NULL; } bool stop = false; int data_size = file.size(); int bytes_read = 0; bytes_read += read_version(); bytes_read += read_system_info(); bytes_read += read_summary_page(); while (!stop && (bytes_read < data_size)) { bytes_read += read_detail_page(); // read_page(stop, errors); } rideFile->setTag("Device Info", deviceInfo); if (stop) { delete rideFile; return NULL; } else { return rideFile; } }
RideFile *SrmFileReader::openRideFile(QFile &file, QStringList &errorStrings, QList<RideFile*>*) const { if (!file.open(QFile::ReadOnly)) { errorStrings << QString("can't open file %1").arg(file.fileName()); return NULL; } QDataStream in(&file); in.setByteOrder( QDataStream::LittleEndian ); RideFile *result = new RideFile; result->setDeviceType("SRM"); result->setFileFormat("SRM training files (srm)"); result->setTag("Sport", "Bike" ); char magic[4]; in.readRawData(magic, sizeof(magic)); if( strncmp(magic, "SRM", 3)){ errorStrings << QString("Unrecognized file type, missing magic." ); return NULL; } int version = magic[3] - '0'; switch( version ){ case 5: case 6: case 7: // ok break; default: errorStrings << QString("Unsupported SRM file format version: %1") .arg(version); return NULL; } quint16 dayssince1880 = readShort(in); quint16 wheelcirc = readShort(in); quint8 recint1 = readByte(in); quint8 recint2 = readByte(in); quint16 blockcnt = readShort(in); quint16 markercnt = readShort(in); readByte(in); // padding quint8 commentlen = readByte(in); if( commentlen > 70 ) commentlen = 70; char comment[71]; in.readRawData(comment, sizeof(comment) - 1); comment[commentlen] = '\0'; result->setTag("Notes", QString(comment) ); // assert propper markercnt to avoid segfaults if( in.status() != QDataStream::Ok ){ errorStrings << QString("failed to read file header" ); return NULL; } result->setRecIntSecs(((double) recint1) / recint2); unsigned recintms = (unsigned) round(result->recIntSecs() * 1000.0); result->setTag("Wheel Circumference", QString("%1").arg(wheelcirc) ); QDate date(1880, 1, 1); date = date.addDays(dayssince1880); QVector<marker> markers(markercnt + 1); for (int i = 0; i <= markercnt; ++i) { char mcomment[256]; size_t mcommentlen = version < 6 ? 3 : 255; assert( mcommentlen < sizeof(mcomment) ); in.readRawData(mcomment, mcommentlen ); mcomment[mcommentlen] = '\0'; quint8 active = readByte(in); quint16 start = readShort(in); quint16 end = readShort(in); quint16 avgwatts = readShort(in); quint16 avghr = readShort(in); quint16 avgcad = readShort(in); quint16 avgspeed = readShort(in); quint16 pwc150 = readShort(in); // data fixup: Although the data chunk index in srm files starts // with 1, some srmwin wrote files referencing index 0. if( end < 1 ) end = 1; if( start < 1 ) start = 1; // data fixup: some srmwin versions wrote markers with start > end if( end < start ){ markers[i].start = end; markers[i].end = start; } else { markers[i].start = start; markers[i].end = end; } markers[i].note = QString( mcomment); if( i == 0 ){ result->setTag("Athlete Name", QString(mcomment) ); } (void) active; (void) avgwatts; (void) avghr; (void) avgcad; (void) avgspeed; (void) pwc150; (void) wheelcirc; } // fail early to tell devs whats wrong with file if( in.status() != QDataStream::Ok ){ errorStrings << QString("failed to read marker" ); return NULL; } blockhdr *blockhdrs = new blockhdr[blockcnt+1]; for (int i = 0; i < blockcnt; ++i) { // In the .srm files generated by Rainer Clasen's srmcmd, // hsecsincemidn is a *signed* 32-bit integer. I haven't seen a // negative value in any .srm files generated by srmwin.exe, but // since the number of hundredths of a second in a day is << 2^31, // it seems safe to always treat this number as signed. qint32 hsecsincemidn = readLong(in); blockhdrs[i].chunkcnt = readShort(in); blockhdrs[i].dt = QDateTime(date); blockhdrs[i].dt = blockhdrs[i].dt.addMSecs(hsecsincemidn * 10); } // fail early to tell devs whats wrong with file if( in.status() != QDataStream::Ok ){ errorStrings << QString("failed to read block headers" ); return NULL; } quint16 zero = readShort(in); quint16 slope = readShort(in); quint16 datacnt = readShort(in); readByte(in); // padding // fail early to tell devs whats wrong with file if( in.status() != QDataStream::Ok ){ errorStrings << QString("failed to read calibration data" ); return NULL; } result->setTag("Slope", QString("%1") .arg( 140.0 / 42781 * slope, 0, 'f', 2) ); result->setTag("Zero Offset", QString("%1").arg(zero) ); // SRM5 files have no blocks - synthesize one if( blockcnt < 1 ){ blockcnt = 0; blockhdrs[0].chunkcnt = datacnt; blockhdrs[0].dt = QDateTime(date); } int blknum = 0, blkidx = 0, mrknum = 0, interval = 0; double km = 0.0, secs = 0.0; if (markercnt > 0) mrknum = 1; for (int i = 0; i < datacnt; ++i) { int cad, hr, watts; double kph, alt; double temp=-255; if (version < 7) { quint8 ps[3]; in.readRawData((char*) ps, sizeof(ps)); cad = readByte(in); hr = readByte(in); kph = (((((unsigned) ps[1]) & 0xf0) << 3) | (ps[0] & 0x7f)) * 3.0 / 26.0; watts = (ps[1] & 0x0f) | (ps[2] << 0x4); alt = 0.0; } else { assert(version == 7); watts = readShort(in); cad = readByte(in); hr = readByte(in); qint32 kph_tmp = readSignedLong(in); kph = kph_tmp < 0 ? 0 : kph_tmp * 3.6 / 1000.0; alt = readSignedLong(in); temp = 0.1 * readSignedShort(in); } if (i == 0) { result->setStartTime(blockhdrs[blknum].dt); } if (mrknum < markers.size() && i == markers[mrknum].end) { ++interval; ++mrknum; } // markers count from 1 if ((i > 0) && (mrknum < markers.size()) && (i == markers[mrknum].start - 1)) ++interval; km += result->recIntSecs() * kph / 3600.0; double nm = watts / 2.0 / PI / cad * 60.0; result->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, 0.0, 0.0, 0.0, 0.0, temp, 0.0, interval); ++blkidx; if ((blkidx == blockhdrs[blknum].chunkcnt) && (blknum + 1 < blockcnt)) { QDateTime end = blockhdrs[blknum].dt.addMSecs( recintms * blockhdrs[blknum].chunkcnt); ++blknum; blkidx = 0; QDateTime start = blockhdrs[blknum].dt; qint64 endms = ((qint64) end.toTime_t()) * 1000 + end.time().msec(); qint64 startms = ((qint64) start.toTime_t()) * 1000 + start.time().msec(); double diff_secs = (startms - endms) / 1000.0; if (diff_secs < result->recIntSecs()) { errorStrings << QString("ERROR: time goes backwards by %1 s" " on trans " "to block %2" ).arg(diff_secs).arg(blknum); secs += result->recIntSecs(); // for lack of a better option } else { secs += diff_secs; } } else { secs += result->recIntSecs(); } } // assert some points were found. prevents segfault when looking at // the overall markers[0].start/.end // note: we're not checking in.status() to cope with truncated files if( result->dataPoints().size() < 1 ){ errorStrings << QString("file contains no data points"); return NULL; } double last = 0.0; for (int i = 1; i < markers.size(); ++i) { const marker &marker = markers[i]; int start = qMin(marker.start, result->dataPoints().size()) - 1; double start_secs = result->dataPoints()[start]->secs; int end = qMin(marker.end - 1, result->dataPoints().size() - 1); double end_secs = result->dataPoints()[end]->secs + result->recIntSecs(); if( last < start_secs ) result->addInterval(last, start_secs, ""); QString note = QString("%1").arg(i); if( marker.note.length() ) note += QString(" ") + marker.note; if( start_secs <= end_secs ) result->addInterval(start_secs, end_secs, note ); last = end_secs; } if (!markers.empty() && markers.last().end < result->dataPoints().size()) { double start_secs = result->dataPoints().last()->secs + result->recIntSecs(); result->addInterval(last, start_secs, ""); } file.close(); return result; }
RideFile *PolarFileReader::openRideFile(QFile &file, QStringList &errors, QList<RideFile*>*rideList) const { /* * 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; QDate date; QString note(""); 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; bool haveGPX = false; int igpx = 0; int ngpx = 0; double lat=0,lon=0; int recInterval = 1; // Read Polar GPX file (if exist with same name as hrm file). RideFile *gpxresult=NULL; RideFilePoint *p; QString suffix = file.fileName(); int dot = suffix.lastIndexOf("."); assert(dot >= 0); QFile gpxfile(suffix.left(dot)+".gpx"); haveGPX = gpxfile.exists(); if (haveGPX) { GpxFileReader reader; gpxresult = reader.openRideFile(gpxfile,errors,rideList); ngpx = gpxresult->dataPoints().count(); } if (!file.open(QFile::ReadOnly)) { errors << ("Could not open ride file: \"" + file.fileName() + "\""); return NULL; } int lineno = 1; double next_interval=0; QList<double> intervals; QTextStream is(&file); RideFile *rideFile = new RideFile(); QString section = NULL; while (!is.atEnd()) { // the readLine() method doesn't handle old Macintosh CR line endings // this workaround will load the 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); if (smode.at(0)=='1') speed = true; if (smode.length()>0 && smode.at(1)=='1') cadence = true; if (smode.length()>1 && smode.at(2)=='1') altitude = true; if (smode.length()>2 && smode.at(3)=='1') power = true; if (smode.length()>3 && smode.at(4)=='1') balance = true; //if (smode.length()>4 && smode.at(5)=='1') pedaling_index = true; // // 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){ hr = 60000.0/hrm; } else { hr = hrm; } if (haveGPX && gpxresult && (igpx<ngpx)) { p = gpxresult->dataPoints()[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(); return rideFile; }