/* * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com) * * 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 "WkoRideFile.h" #include #include #include #include #include // for std::sort #include #include "math.h" static int wkoFileReaderRegistered = RideFileFactory::instance().registerReader( "wko", "WKO+ Files", new WkoFileReader()); RideFile *WkoFileReader::openRideFile(QFile &file, QStringList &errors, QList*rides) const { WkoParser parser(file,errors,rides); return parser.result(); } /*---------------------------------------------------------------------- * WKO Parser for parsing .wko file format, ride is parsed as part * of the constructor, so no need to explicitly call any parsing functions *--------------------------------------------------------------------*/ WkoParser::WkoParser(QFile &file, QStringList &errors, QList*rides) : file(file), results(NULL), errors(errors), rides(rides) { // we reference the filename to parse start date/time filename = file.fileName(); QFileInfo fileinfo(file); // open the source file, we never update so read only and then // read in the whole file, proved faster when bit twiddling the // raw data and avoids unbounded memory allocations when ride data // is corrupt or we parse it incorrectly if (!file.open(QFile::ReadOnly)) { errors << ("Could not open ride file: \"" + file.fileName() + "\""); return; } // is it big enough? if (file.size() < 0x200) { file.close(); errors << "Not a WKO+ file"; return; } bufferSize = file.size(); QScopedArrayPointer entirefile(new WKO_UCHAR[bufferSize]); QDataStream *rawstream(new QDataStream(&file)); headerdata = &entirefile[0]; rawstream->readRawData(reinterpret_cast(headerdata), file.size()); file.close(); // Check the header to make sure it really is a WKO // file, the magic number for WKO is 'W' 'K' 'O' ^Z if (*headerdata != 'W' || *(headerdata+1) != 'K' || *(headerdata+2) != 'O' || *(headerdata+3) != 0x1A) { errors << "Not a WKO+ file"; return; } // We only support versions of the WKO file format we have seen // sufficient source files to reverse engineer and test these // are for CP v1.0 and v1.1 and then WKO v2.2 *or higher* donumber(headerdata+4, &version); // versions we don't support are rejected if (version < 28 && version != 1 && version != 12 && version != 7) { errors << (QString("Version of file (%1) is too old, open and save in WKO then retry: \"").arg(version) + file.fileName() + "\""); return; // later versions may change so support but warn } else if (version >31) { errors << ("Version of file is new and not fully supported yet: \"" + file.fileName() + "\""); } // Allocate space for newly parsed ride results = new RideFile; //results->setTag("File Format", QString("WKO v%1").arg(version)); results->setFileFormat(QString("WKO v%1 (wko)").arg(version)); // read header data and store details into rideFile structure rawdata = parseHeaderData(headerdata); // Parse raw data (which calls rideFile->appendPoint() with each sample if (rawdata) footerdata = parseRawData(rawdata); else { delete results; results = NULL; return; } // is recIntSecs a daft value? if (results->recIntSecs() < 0.1) { // lets see what the most popular recording interval is... QMap ints; bool first = true; double last = 0; foreach(RideFilePoint *p, results->dataPoints()) { if (first) { last = p->secs; first = false; } else { double delta = p->secs-last; last = p->secs; // lookup int count = ints.value(delta); count++; ints.insert(delta, count); } } // which is most popular? double populardelta=1.0; int count=0; QMapIterator i(ints); while (i.hasNext()) { i.next(); if (i.value() > count) { count = i.value(); populardelta = i.key(); } } results->setRecIntSecs(populardelta); } // adjust times to start at zero, some ridefiles have // a start time that really blows up the CPX calculator // e.g. first sample at 19 hrs .. if (results->dataPoints().count() && results->dataPoints().first()->secs > 0) { double sub = results->dataPoints().first()->secs; foreach(RideFilePoint *p, results->dataPoints()) p->secs -= sub; } // Post process the ride intervals to convert from point offsets to time in seconds QVector datapoints = results->dataPoints(); for (int i=0; iname; if (references.at(i)->start < datapoints.count()) add.start = datapoints.at(references.at(i)->start)->secs; else continue; if (references.at(i)->stop < datapoints.count()) add.stop = datapoints.at(references.at(i)->stop)->secs + results->recIntSecs()-.001; else continue; results->addInterval(add.start, add.stop, add.name); } // free up temporary storage for range post processing for (int i=0; isetRecIntSecs(interval); #if 0 // used when debugging qDebug()<appendPoint((double)rtime/1000, cad, hr, km, kph, nm, watts, alt, lon, lat, wind, slope, temp, 0.0, 0); } // increment time - even for null records (perhaps especially for null // records since they are there specifically to handle pause in recording! rtime += inc; } else { // pause record increments time /* set the increment */ unsigned long pausetime; int pausesize; // pause record different in version 1 if (version == 1) pausesize=31; else pausesize=42; /* set increment value -> if followed by a null record it is to show a pause in recording -- velotrons seem to cause lots and lots of these */ pausetime = get_bits(thelot, bit, 32); if (version != 1) inc = pausetime; #if 0 fprintf(stderr, "pausetime: "); for (int i=0; isetTag("Objective", (const char*)&txtbuf[0]); notes = p; p += dotext(p, &txtbuf[0]); /* 5: notes */ p += dotext(p, &txtbuf[0]); /* 6: graphs */ strcpy(reinterpret_cast(WKO_GRAPHS), reinterpret_cast(&txtbuf[0])); // save those graphs away if (version != 1) { //!!! Version 1 beta support p += donumber(p, &sport); /* 7: sport */ } else { sport = 0x02; // only bike was supported in files this old } switch (sport) { case 0x01 : results->setTag("Sport", "Swim") ; break; case 0x02 : results->setTag("Sport", "Bike") ; break; case 0x03 : results->setTag("Sport", "Run") ; break; case 0x04 : results->setTag("Sport", "Brick") ; break; case 0x05 : results->setTag("Sport", "Cross Train") ; break; case 0x06 : results->setTag("Sport", "Race ") ; break; case 0x07 : results->setTag("Sport", "Day Off") ; break; case 0x08 : results->setTag("Sport", "Mountain Bike") ; break; case 0x09 : results->setTag("Sport", "Strength") ; break; case 0x0B : results->setTag("Sport", "XC Ski") ; break; case 0x0C : results->setTag("Sport", "Rowing") ; break; default : case 0x64 : results->setTag("Sport", "Other"); break; } QString notesTag; notesTag = results->getTag("Sport", "Bike"); // we just set it, so default is meaningless. notesTag += "\n"; if (version != 1) { //!!! Version 1 beta support // workout code and duration not in v1 files code = p; p += dotext(p, &txtbuf[0]); /* 8: workout code */ results->setTag("Workout Code", (const char*)&txtbuf[0]); p += donumber(p, &ul); /* 9: duration 000s of seconds */ } p += dotext(p, &txtbuf[0]); /* 10: lastname */ p += dotext(p, &txtbuf[0]); /* 11: firstname */ p += donumber(p, &ul); /* 12: time of day */ sincemidnight = ul; char rideFileRegExp[] = "^((\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)" "_(\\d\\d)_(\\d\\d)_(\\d\\d))\\.(.+)$"; QRegExp rx(rideFileRegExp); if (rx.exactMatch(filename)) { // set the date and time from the filename QDate date(rx.cap(2).toInt(), rx.cap(3).toInt(),rx.cap(4).toInt()); QTime time(rx.cap(5).toInt(), rx.cap(6).toInt(),rx.cap(7).toInt()); QDateTime datetime(date, time); results->setStartTime(datetime); } else { // date not available from filename use WKO metadata QDateTime datetime(QDate::fromJulianDay(julian), QTime(sincemidnight/360000, (sincemidnight%360000)/6000, (sincemidnight%6000)/100)); results->setStartTime(datetime); } QString scode, sgoal, snote; if (version != 1) { // Workout Code dotext(code, &txtbuf[0]); scode = (const char *)&txtbuf[0]; notesTag += scode; notesTag += "\n"; } dotext(goal, &txtbuf[0]); sgoal = (const char *)&txtbuf[0]; notesTag += "WORKOUT GOAL"; notesTag += "\n"; notesTag += sgoal; notesTag += "\n"; dotext(notes, &txtbuf[0]); snote = (const char *)&txtbuf[0]; notesTag += "WORKOUT NOTES"; notesTag += "\n"; notesTag += snote; notesTag += "\n"; results->setTag("Notes", notesTag); if (version != 1) { p += donumber(p, &ul); /* 13: distance travelled in meters */ p += donumber(p, &ul); /* 14: device recording interval */ p += donumber(p, &ul); /* 15: athlete max heartrate */ p += donumber(p, &ul); /* 16: athlete threshold heart rate */ p += donumber(p, &ul); /* 17: athlete threshold power */ if (version != 12 && version != 7) p += dodouble(p, &g); /* 18: athlete threshold pace */ p += donumber(p, &ul); /* 19: weight in grams/10 */ results->setTag("Weight", QString("%1").arg((double)ul/100.00)); p += 28; //p += donumber(p, &ul); /* 20: unknown */ //p += donumber(p, &ul); /* 21: unknown */ //p += donumber(p, &ul); /* 22: unknown */ //p += donumber(p, &ul); /* 23: unknown */ //p += donumber(p, &ul); /* 24: unknown */ //p += donumber(p, &ul); /* 25: unknown */ //p += donumber(p, &ul); /* 26: unknown */ } else { //!!! Version 1 Beta Support p += 52; // not decoded at present } /*************************************************** * 2: GRAPH TAB - MAX OF 16 CHARTING GRAPHS ***************************************************/ p += donumber(p, &ul); /* 27: graph view */ p += donumber(p, &ul); /* 28: WKO_device type */ WKO_device = ul; // save WKO_device switch (WKO_device) { case 0x00 : // early versions set device to zero for powertap case 0x01 : results->setDeviceType("Powertap"); break; case 0x1a : // also SRM - PowerControl VI case 0x04 : results->setDeviceType("SRM"); break; // powercontrol V case 0x05 : results->setDeviceType("Polar"); break; case 0x06 : results->setDeviceType("Computrainer/Velotron"); break; case 0x0e : // also ergomo case 0x11 : results->setDeviceType("Ergomo"); break; case 0x12 : results->setDeviceType("Garmin Edge 205/305"); break; case 0x13 : results->setDeviceType("Garmin Edge 705"); break; case 0x14 : results->setDeviceType("iBike"); break; case 0x15 : results->setDeviceType("Suunto"); break; case 0x16 : results->setDeviceType("Cycleops 300PT"); break; case 0x19 : results->setDeviceType("Ergomo"); break; default : results->setDeviceType(QString("WKO (0x%1)").arg(WKO_device,0,16)); break; } p += donumber(p, &ul); // not in version 12? int arraysize = 0; if (version < 12) arraysize = 8; if (version == 12) arraysize = 14; if (version >= 28) arraysize = 16; for (int i=0; i< arraysize; i++) { // 16 types of chart data p += 44; p += doshort(p, &us); /* 41: number of gridlines XXVARIABLEXX */ p += (us * 8); // 2 longs x number of gridlines } /* Ranges */ // The ranges are stored as references to data points // whilst the RideFileInterval structure uses start and stop // in seconds. We cannot translate from one to the other at this // point because the raw data has not been parsed yet. // So intervals are created here with point references // and they are post-processed after the call to // parseRawData in openRideFile above. p += doshort(p, &us); /* 237: Number of ranges XXVARIABLEXX */ for (int i=0; iname = reinterpret_cast(&txtbuf[0]); p += dotext(p, &txtbuf[0]); p += donumber(p, &ul); p += donumber(p, &ul); add->start = ul; p += donumber(p, &ul); add->stop = ul; if (version != 1) { //!!! Version 1 Beta Support p += 12; } else { p += 8; } //p += donumber(p, &ul); //p += donumber(p, &ul); //p += donumber(p, &ul); // add to intervals - referencing data point for interval references.append(add); } /*************************************************** * 3: DEVICE SPECIFIC DATA ***************************************************/ QString deviceInfo; p += doshort(p, &us); /* 249: Device/Token pairs XXVARIABLEXX */ for (int i=0; isetTag("Device Info", deviceInfo); /*************************************************** * 4: PERSPECTIVE CHARTS & CACHES ***************************************************/ p += doshort(p, &us); /* 251: Number of charts XXVARIABLEXX */ charts = us; num=0; while (charts) { // keep parsing until we have no charts left enum configtype type=INVALID; p += donumber(p, &ul); num = ul; // Each config section is preceded with // 0xff 0xff 0x01 0x00 if (num==0x01ffff) { char buf[32]; // Config Type p += doshort(p, &us); // got here... strncpy (reinterpret_cast(buf), reinterpret_cast(p), us); buf[us]=0; p += us; /* What type? */ if (!strcmp(buf, "CRideSettingsConfig")) type=CRIDESETTINGSCONFIG; if (!strcmp(buf, "CRideGoalConfig")) type=CRIDEGOALCONFIG; if (!strcmp(buf, "CRideNotesConfig")) type=CRIDENOTESCONFIG; if (!strcmp(buf, "CDistributionChartConfig")) type=CDISTRIBUTIONCHARTCONFIG; if (!strcmp(buf, "CRideSummaryConfig")) type=CRIDESUMMARYCONFIG; if (!strcmp(buf, "CMeanMaxChartConfig"))type=CMEANMAXCHARTCONFIG; if (!strcmp(buf, "CMeanMaxChartCache")) type=CMEANMAXCHARTCACHE; if (!strcmp(buf, "CDistributionChartCache")) type=CDISTRIBUTIONCHARTCACHE; if (type == CDISTRIBUTIONCHARTCACHE) { p = parseCDistributionChartCache(p); } else if (type == CMEANMAXCHARTCACHE) { p = parseCMeanMaxChartCache(p); } else if (type == CRIDESUMMARYCONFIG) { p = parseCRideSummaryConfig(p); } else if (type == CRIDENOTESCONFIG) { p = parseCRideNotesConfig(p); } else if (type == CRIDEGOALCONFIG) { p = parseCRideGoalConfig(p); } else if (type == CRIDESETTINGSCONFIG) { p = parseCRideSettingsConfig(p); } else if (type == CDISTRIBUTIONCHARTCONFIG) { p = parseCDistributionChartConfig(p); } else if (type == CMEANMAXCHARTCONFIG) { p = parseCMeanMaxChartConfig(p); } } else if (num == 1) { // version 12 leaves this at the end of ride summary } else if (num==2) { /* Perspective */ p += dotext(p, &txtbuf[0]); } else if (num==3) { p = parseChart(p); } else { errors << "Could not parse chart data" << QString("0x%1 @0x%2").arg(num, 0, 16).arg(p-fb, 0, 16); return NULL; } } if (WKO_GRAPHS[0] == '\0') { errors << ("Manual files not supported"); return (WKO_UCHAR *)NULL; } else { /* PHEW! We're on the raw data, out job here is done */ return (p); } } /*============================================================================== * Parse chart segments - the really hard stuff! *==============================================================================*/ WKO_UCHAR *WkoParser::parsePerspective(WKO_UCHAR *p) { // perspective - not present in early releases if (version != 1 && version != 7 && version != 12) { p += donumber(p, &ul); /* always 2 */ p += dotext(p, &txtbuf[0]); /* perspective */ } return p; } WKO_UCHAR *WkoParser::parseChart(WKO_UCHAR *p, int type) { charts--; // chart name p += dotext(p, &txtbuf[0]); // preamble if (version == 1) p+= 24; else p += 32; // earlier versions don't have a type marker // so we let the caller tell us what we're parsing // otherwise we work it when we can if (!type) { p += donumber(p, &ul); type = ul; } else { p+= 4; } if (type == 0x02) { // distribution chart p += 64; // record count p += doshort(p, &us); for (int i=0; i 7) return (524287L); // max is for 19 bits, sign bit 0 for null values else return (32767L); // max is for 15 bits, sign bit 0 for null values break; case 'T' : return (2047L); break; case 'D' : return (0L); break; // distance is ignored for null purposes case 'G' : return (0L); break; // GPS is ignored for null purposes case 'W' : return (2047L); break; case '+' : return (127L); break; // max is for 7 bits, sign bit 0 for null value case '^' : return (524287L); break; // max is for 19 bits, sign bit 0 for null values case 'm' : return (1L); break; default: return (0); } } /************************************************************************ * HANDLE OPTIONAL CHARTING DATA * * optpad() - main entry point for handling optional chart data ***********************************************************************/ unsigned int WkoParser::optpad(WKO_UCHAR *p) { WKO_USHORT us; unsigned int bytes = 0; /* Opening bytes are * ffff - gone too far! * 8007 - stop * 800f - data cache * 0001 - onebyte field * Any other value and you've gone * too far and need to rewind. */ p += doshort(p, &us); bytes = 2; switch (us) { case 0x8007 : /* all done */ case 0x800a : /* after fixup for distchart Jan 2010 */ case 0x800b : case 0x800c : /* after fixup for distchart Jan 2010 */ case 0x800d : /* from Jim B 2nd Oct 2009 */ case 0x800e : /* from Phil S 4th Oct 2009 */ case 0x800f : /* after fixup for distchart Jan 2010 */ case 0x8010 : /* after fixup for distchart Jan 2010 */ case 0x8011 : /* after fixup for distchart Jan 2010 */ case 0x8012 : /* after fixup for distchart Jan 2010 */ case 0x8013 : /* after fixup for distchart Jan 2010 */ case 0x8014 : /* Q's v2 file */ case 0x8015 : /* Q's v2 file */ case 0x8016 : /* Q's v2 file */ case 0x8017 : /* Q's v2 file */ case 0x8018 : /* Q's v2 file */ case 0x8019 : /* Rainer's running file */ case 0x801a : /* Rainer's running file */ case 0x801b : /* Rainer's running file */ break; case 0x0000 : bytes += doshort(p, &us); p += 4; break; case 0xffff : /* too far, rewind */ default : bytes -= 2; break; } return (bytes); } /************************************************************************************ * BIT TWIDDLING FUNCTIONS * * get_bit() - return 0 or 1 for the given bit from a large array * get_bits() - return a range of bits read right to left (high bit first) * ************************************************************************************/ int WkoParser::get_bit(WKO_UCHAR *data, unsigned bitoffset) // returns the n-th bit { WKO_UCHAR c = data[bitoffset >> 3]; // X>>3 is X/8 WKO_UCHAR bitmask = 1 << (bitoffset %8); // X&7 is X%8 return ((c & bitmask)!=0) ? 1 : 0; } unsigned int WkoParser::get_bits(WKO_UCHAR* data, unsigned bitOffset, unsigned numBits) { unsigned int bits = 0; unsigned int currentbit; bits=0; for (currentbit = bitOffset+numBits-1; numBits--; currentbit--) { bits = bits << 1; bits = bits | get_bit(data, currentbit); } return bits; } /***************************************************************************** * READ NUMBERS AND TEXTS FROM THE FILE, OPTIONALLY OUTPUTTING * IN DIFFERENT FORMATS TO DEBUG OR ANALYSE DATA * * dofloat() - read and retuen a 4 byte float * dodouble() - read and return an 8 byte double * donumber() - read and return a 4 byte long * doshort() - read and return a 2 byte short * dotext() - read a wko text (1byte len, n bytes string without terminator) * pbin() - print a number in binary ****************************************************************************/ unsigned int WkoParser::dofloat(WKO_UCHAR *p, float *pnum) { memcpy(pnum, p, 4); return 4; } unsigned int WkoParser::dodouble(WKO_UCHAR *p, double *pnum) { memcpy(pnum, p, 8); return 8; } unsigned int WkoParser::donumber(WKO_UCHAR *p, WKO_ULONG *pnum) { *pnum = qFromLittleEndian(p); return 4; } void WkoParser::pbin(WKO_UCHAR x) { static WKO_UCHAR bits[]={ 128, 64, 32, 16, 8, 4, 2, 1 }; int i; for (i=0; i<8; i++) printf("%c", ((x&bits[i]) == bits[i]) ? '1' : '0'); } unsigned int WkoParser::dotext(WKO_UCHAR *p, WKO_UCHAR *buf) { WKO_USHORT us; if (*p == 0) { *buf = '\0'; return 1; } else if (*p < 255) { strncpy(reinterpret_cast(buf), reinterpret_cast(p+1), *p); buf[*p]=0; return (*p)+1; } else { p += 1; p += doshort(p, &us); strncpy(reinterpret_cast(buf), reinterpret_cast(p), us); buf[us]=0; return (us)+3; } } unsigned int WkoParser::doshort(WKO_UCHAR *p, WKO_USHORT *pnum) { *pnum = qFromLittleEndian(p); return 2; }