Files
GoldenCheetah/src/WkoRideFile.cpp
Damien 4005e27039 Add FileType to RideFile
modified:   src/Bin2RideFile.cpp
	modified:   src/BinRideFile.cpp
	modified:   src/Computrainer3dpFile.cpp
	modified:   src/CsvRideFile.cpp
	modified:   src/FitRideFile.cpp
	modified:   src/FitlogParser.cpp
	modified:   src/FitlogRideFile.cpp
	modified:   src/GcRideFile.cpp
	modified:   src/GpxRideFile.cpp
	modified:   src/ManualRideFile.cpp
	modified:   src/PolarRideFile.cpp
	modified:   src/PolarRideFile.cpp
	modified:   src/PwxRideFile.cpp
	modified:   src/QuarqRideFile.cpp
	modified:   src/RawRideFile.cpp
	modified:   src/SlfRideFile.cpp
	modified:   src/SmfRideFile.cpp
	modified:   src/SplitActivityWizard.cpp
	modified:   src/SplitRideDialog.cpp
	modified:   src/SrdRideFile.cpp
	modified:   src/SrmRideFile.cpp
	modified:   src/SyncRideFile.cpp
	modified:   src/TacxCafRideFile.cpp
	modified:   src/TcxParser.cpp
	modified:   src/TcxRideFile.cpp
	modified:   src/TxtRideFile.cpp
	modified:   src/WkoRideFile.cpp
2012-11-14 00:37:24 +01:00

1495 lines
46 KiB
C++

/*
* 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 <QRegExp>
#include <QDebug>
#include <QFile>
#include <QTextStream>
#include <algorithm> // for std::sort
#include <assert.h>
#include "math.h"
static int wkoFileReaderRegistered = RideFileFactory::instance().registerReader(
"wko", "WKO+ Files", new WkoFileReader());
RideFile *WkoFileReader::openRideFile(QFile &file,
QStringList &errors,
QList<RideFile*>*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<RideFile*>*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();
boost::scoped_array<WKO_UCHAR> entirefile(new WKO_UCHAR[bufferSize]);
QDataStream *rawstream(new QDataStream(&file));
headerdata = &entirefile[0];
rawstream->readRawData(reinterpret_cast<char *>(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<double, int> 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<double, int> 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<RideFilePoint*> datapoints = results->dataPoints();
for (int i=0; i<references.count(); i++) {
RideFileInterval add;
// name is ok, but start stop need to be in time rather than datapoint no.
add.name = references.at(i)->name;
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; i<references.count(); i++) delete references.at(i);
references.clear();
// if we got to the end then close with success
if (footerdata) return;
else {
// failed, so wipe what we got and return
delete results;
results = NULL;
return;
}
}
/***************************************************************************************
* PROCESS THE RAW DATA
*
* parseRawData() - read through all the raw data adding record points and return
* a pointer to the footer record
**************************************************************************************/
WKO_UCHAR *
WkoParser::parseRawData(WKO_UCHAR *fb)
{
WKO_ULONG WKO_xormasks[32]; // xormasks used all over
double cad=0, hr=0, km=0, kph=0, nm=0, watts=0, slope=0, alt=0, lon=0, lat=0, wind=0, temp=RideFile::noTemp, interval=0;
int isnull=0;
WKO_ULONG records, data;
WKO_UCHAR *thelot;
WKO_USHORT us;
int imperialflag=0;
int i,j;
int bit=0;
int calculate_distance=0; // if speed is available but not distance we need to calculate it
unsigned long inc;
unsigned long rtime=0,rdist=0;
int WKO_graphbits[32]; // number of bits for each GRAPH
WKO_ULONG WKO_nullval[32]; // null value for each graph
char GRAPHDATA[32][32]; // formatted values for each sample
// setup decoding controls -- xormasks
for (i=1; i<32; i++) {
WKO_xormasks[i]=0;
for (j=0; j<i; j++) {
WKO_xormasks[i] <<= 1;
WKO_xormasks[i] |= 1;
}
}
// setup decoding controls -- bit sizes and null values
for (i=0; WKO_GRAPHS[i] != '\0'; i++) {
WKO_nullval[i] = nullvals(WKO_GRAPHS[i], version); // setup nullvalue
if ((WKO_graphbits[i] = bitsize(WKO_GRAPHS[i], WKO_device, version)) == 0) { // setup & check field size
errors << ("Unknown channel " + WKO_GRAPHS[i]);
return (NULL);
}
}
// do we need to calculate distance ?
if (!strchr(WKO_GRAPHS, 'D') && strchr(WKO_GRAPHS, 'S')) calculate_distance = 1;
else calculate_distance = 0;
/* So how many records are there? */
fb += doshort(fb, &us);
if (us == 0xffff) {
fb += donumber(fb, &records);
} else {
records = us;
}
if (records == 0) {
errors << ("Workout is empty.");
return NULL;
}
/* how much data is there? */
fb += doshort(fb, &us);
if (us == 0xffff) {
fb += donumber(fb, &data);
} else {
data = us;
}
thelot = fb;
/* does data stream at offset 0 ? */
if (get_bit(thelot, 0)){
bit = 0;
inc = 1000;
} else {
inc = get_bits(thelot, 1, 15); // record interval
if (version == 1) bit = 32;
else bit = 43;
}
interval = inc;
interval /= 1000;
results->setRecIntSecs(interval);
#if 0
// used when debugging
qDebug()<<QString("0x%1").arg(WKO_device,0,16)<<version<<WKO_GRAPHS<<"records="<<records;
for (int xbit=bit; xbit < 500; xbit++)
if (get_bits(thelot, xbit, 1)) fprintf(stderr, "1");
else fprintf(stderr, "0");
qDebug()<<"";
#endif
int maxbit = 8 * (bufferSize - (rawdata-headerdata));
/*------------------------------------------------------------------------------
* RUN THROUGH EACH RAW DATA RECORD
*==============================================================================*/
rdist=rtime=0;
while (records && bit < maxbit) {
unsigned int marker;
WKO_LONG sval=0;
WKO_ULONG val=0;
unsigned long valp; // for printf
long svalp; // for printf
// reset point values;
alt = slope = wind = cad= hr= km= kph= nm= watts= 0.0;
temp= RideFile::noTemp;
marker = get_bits(thelot, bit++, 1);
if (marker) {
// OK so we have a sample to collect, format and get ready to output
records--;
for (i=0; WKO_GRAPHS[i] != '\0'; i++) {
val = get_bits(thelot, bit, WKO_graphbits[i]);
/* move onto next value */
bit += WKO_graphbits[i];
if (WKO_GRAPHS[i] != 'D' && WKO_GRAPHS[i] != 'G' && val==WKO_nullval[i]) {
// null value
GRAPHDATA[i][0] = '\0';
} else {
/* apply conversion etc */
switch (WKO_GRAPHS[i]) {
case '+' : /* Temperature - signed */
if (get_bit(thelot, bit-1)) { /* is negative */
val ^= WKO_xormasks[WKO_graphbits[i]];
sval = val * -1;
} else {
sval = val;
}
svalp = sval;
temp = sval;
sprintf(GRAPHDATA[i], "%8ld", svalp);
break;
case '^' : /* Slope */
if (get_bit(thelot, bit-1)) { /* is negative */
val ^= WKO_xormasks[WKO_graphbits[i]];
sval = val * -1;
} else sval = val;
slope = sval;
slope /=10;
break;
case 'W' : /* Wind speed */
if (get_bit(thelot, bit-1)) { /* is negative */
val ^= WKO_xormasks[WKO_graphbits[i]];
sval = val * -1;
} else sval = val;
wind = sval;
wind /=10;
break;
case 'A' : /* Altitude */
if (get_bit(thelot, bit-1)) { /* is negative */
val ^= WKO_xormasks[WKO_graphbits[i]];
sval = val * -1;
} else sval = val;
alt = sval;
alt /=10;
break;
case 'T' : /* Torque */
if (imperialflag && WKO_GRAPHS[i]=='S') val = long((double)val * KMTOMI);
valp=val;
sprintf(GRAPHDATA[i], "%6ld.%1ld", valp/10, valp%10);
nm = val; nm /=10;
break;
case 'S' : /* Speed */
if (imperialflag && WKO_GRAPHS[i]=='S') val = long((double)val * KMTOMI);
valp=val;
sprintf(GRAPHDATA[i], "%6ld.%1ld", valp/10, valp%10);
kph = val; kph/= 10;
// distance is not available so we need to calculate it
if (calculate_distance) {
double distance;
double f, xi, xf;
// inc is in 1000ths of a second kph val is kph*10
rdist += (inc * kph) / 36;
distance = rdist;
distance /= 100000;
// convert to imperial units ?
if (imperialflag) distance *= KMTOMI;
// round to max of 3 decimal places
xf = modf(distance,&xi);
f = floor(xf*1000+0.5)/1000.0;
sprintf(GRAPHDATA[i], "%g", xi+f);
km = xi+f;
}
break;
case 'D' : /* Distance - running total to 3 decimal places */
double distance;
double f, xi, xf;
// increment BEFORE not AFTER
rdist += val;
// conversion may be required
distance = rdist;
if (version == 1 || version == 7) distance /= 1000;
else distance /= 100000;
if (imperialflag) distance *= KMTOMI;
// to max of 3 decimal places
xf = modf(distance,&xi);
f = floor(xf*1000+0.5)/1000.0;
sprintf(GRAPHDATA[i], "%g", xi+f);
km = xi+f;
break;
case 'G' : /* two longs */
{
int32_t llat, llon;
char slat[20], slon[20];
// stored 2s complement
// conversion is * 180 / 2^31
val = get_bits(thelot, bit-64,32);
memcpy(&llat, &val, 4);
lat = (double)llat;
lat *= 0.00000008381903171539306640625;
val = get_bits(thelot, bit-32,32);
memcpy(&llon, &val, 4);
lon = (double)llon;
lon *= 0.00000008381903171539306640625;
// WKO handles drops in recording of GPS data
// as 180,180 -- we expect 0,0
llat=round(lat); llon=round(lon);
if (llat == 180 && llon == 180) lat=lon=0;
sprintf(slon, "%-3.9g", lon);
sprintf(slat, "%-3.9g", lat);
sprintf(GRAPHDATA[i], "%13s %13s", slat,slon);
}
break;
case 'P' :
watts = val;
valp=val;
sprintf(GRAPHDATA[i], "%8lu", valp); // just output as numeric
break;
case 'H' :
hr = val;
valp=val;
sprintf(GRAPHDATA[i], "%8lu", valp); // just output as numeric
break;
case 'C' :
cad = val;
valp = val;
sprintf(GRAPHDATA[i], "%8lu", valp); // just output as numeric
break;
}
}
}
// Lets check to see if it is a null record?
// We ignores checking GPS data cause its too hard to do at this point
// and rarely (if ever?) affects the outcome
for (i=0, isnull=1; WKO_GRAPHS[i] != '\0'; i++)
if (WKO_GRAPHS[i] != 'G' && WKO_GRAPHS[i] != 'D' && GRAPHDATA[i][0] != '\0') isnull=0;
// Now output this sample if it is not a null record
if (!isnull) {
// !! needs to be modified to support the new alt patch
results->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; i<pausesize+42; i++) {
int x = get_bits(thelot, bit+i, 1);
fputc(x ? '1' : '0', stderr);
}
fprintf(stderr, " %ld ", pausetime);
fputc('\n', stderr);
#endif
bit += pausesize;
}
}
return thelot + (bit/8);
}
/**********************************************************************
* ZIP THROUGH ALL THE WKO SPECIFIC DATA, LAND ON THE RAW DATA
* AND RETURN A POINTER TO THE FIRST BYTE OF THE RAW DATA
*
* parseHeadeData() - read through file and land on the raw data
*
*********************************************************************/
WKO_UCHAR *
WkoParser::parseHeaderData(WKO_UCHAR *fb)
{
unsigned long julian, sincemidnight;
WKO_UCHAR *goal=NULL, *notes=NULL, *code=NULL; // save location of WKO metadata
enum configtype lastchart;
/* utility holders */
WKO_ULONG num;
WKO_ULONG ul;
WKO_ULONG sport;
WKO_USHORT us;
double g;
/* working through filebuf */
WKO_UCHAR *p = fb;
/***************************************************
* 0: FILE HEADER
***************************************************/
p += donumber(p, &ul); /* 1: magic */
p += donumber(p, &ul); /* 2: version */
/***************************************************
* 1: JOURNAL TAB
***************************************************/
p += donumber(p, &ul); /* 3: date days since 01/01/1901 */
julian = ul + 2415386; // 1/1/1901 is day 2415386 in julian days god bless google.
goal = p; p += dotext(p, &txtbuf[0]); /* 4: goal */
results->setTag("Objective", (const char*)&txtbuf[0]);
notes = p; p += dotext(p, &txtbuf[0]); /* 5: notes */
p += dotext(p, &txtbuf[0]); /* 6: graphs */
strcpy(reinterpret_cast<char *>(WKO_GRAPHS), reinterpret_cast<char *>(&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; i<us; i++) {
RideFileInterval *add = new RideFileInterval(); // add new interval
p += 12;
//p += donumber(p, &ul);
//p += donumber(p, &ul);
//p += donumber(p, &ul);
p += dotext(p, &txtbuf[0]);
add->name = reinterpret_cast<char *>(&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; i<us; i++) {
p += dotext(p, &txtbuf[0]);
deviceInfo += QString("%1 = ").arg((char*)&txtbuf[0]);
p += dotext(p, &txtbuf[0]);
deviceInfo += QString("%1\n").arg((char*)&txtbuf[0]);
}
results->setTag("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
WKO_ULONG prev=0;
enum configtype type=INVALID;
prev=num;
p += donumber(p, &ul);
num = ul;
lastchart = OTHER;
// 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<char *>(buf), reinterpret_cast<char *>(p), us); buf[us]=0;
p += us;
/* What type? */
if (!strcmp(buf, "CRideSettingsConfig")) lastchart=type=CRIDESETTINGSCONFIG;
if (!strcmp(buf, "CRideGoalConfig")) lastchart=type=CRIDEGOALCONFIG;
if (!strcmp(buf, "CRideNotesConfig")) lastchart=type=CRIDENOTESCONFIG;
if (!strcmp(buf, "CDistributionChartConfig")) lastchart=type=CDISTRIBUTIONCHARTCONFIG;
if (!strcmp(buf, "CRideSummaryConfig")) lastchart=type=CRIDESUMMARYCONFIG;
if (!strcmp(buf, "CMeanMaxChartConfig"))lastchart=type=CMEANMAXCHARTCONFIG;
if (!strcmp(buf, "CMeanMaxChartCache")) lastchart=type=CMEANMAXCHARTCACHE;
if (!strcmp(buf, "CDistributionChartCache")) lastchart=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<us; i++) {
p += donumber(p, &ul);
p += dotext(p, &txtbuf[0]);
p += dotext(p, &txtbuf[0]);
p += donumber(p, &ul);
p += donumber(p, &ul);
}
// trailing data, especially for speed/pace distribution
doshort(p, &us);
// version 1 trailing data is different
if (version == 1 && us == 0) p += 8;
else if (version != 1 && us == 1) {
p += 6;
doshort(p, &us);
if (us != 0xffff) {
p += 18;
p += doshort(p, &us);
p += us*8;
}
}
p += optpad(p);
} else { // Mean Max Chart
WKO_ULONG recs, term;
WKO_ULONG two;
double g;
p += donumber(p, &ul); /* 253.4: Log or Linear meanmax ? */
p += donumber(p, &ul); /* 253.4: Which Channel ? */
p += donumber(p, &ul); /* 253.4: UNKNOWN */
p += donumber(p, &ul); /* 253.4: Units */
p += donumber(p, &ul); /* 253.4: Conversion type ? */
two = ul;
p += dodouble(p, &g); /* 253.4: Conversion Factor 1 */
if (two == 2) p += donumber(p, &ul); /* 253.4: Conversion Factor 2 */
// record count
p += doshort(p, &us);
recs = us;
// loop through the records
if (recs) {
while (recs--) {
p += donumber(p, &ul);
term = ul;
if (!term) return p;
else {
p += 28;
}
}
// preamble
p += 12;
// get record count
p += doshort(p, &us);
recs=us;
term=1;
// loop through the records
while (recs--) {
p += donumber(p, &ul);
term=ul;
if (!term) {
break;
} else {
p += 28;
}
}
/* as long as it didn't get terminated we need to read the end of the data */
if (term) {
if (version != 1) p += 24; //_read(fd, buf, 24);
else p += 10;
}
}
/* might still be padding ? */
p += optpad(p);
}
return p;
}
WKO_UCHAR *WkoParser::parseCRideSettingsConfig(WKO_UCHAR *p)
{
p = parsePerspective(p);
p += donumber(p, &ul);
// additional number in earlier release
if (version == 12 || version == 1 || version == 7) p += donumber(p,&ul);
charts--;
return p;
}
WKO_UCHAR *WkoParser::parseCRideGoalConfig(WKO_UCHAR *p)
{
p = parsePerspective(p);
p += donumber(p, &ul);
p += donumber(p, &ul);
// additional number in earlier release
if (version == 12 || version == 1 || version == 7) p += donumber(p,&ul);
charts--;
return p;
}
WKO_UCHAR *WkoParser::parseCRideNotesConfig(WKO_UCHAR *p)
{
p = parsePerspective(p);
p += donumber(p, &ul);
p += donumber(p, &ul);
// additional number in earlier release
if (version == 12 || version == 1 || version == 7) p += donumber(p,&ul);
charts--;
return p;
}
WKO_UCHAR *WkoParser::parseCDistributionChartConfig(WKO_UCHAR *p)
{
do {
// always an extra number in earlier versions
if (version == 1 || version == 7 || version == 12) p += donumber(p,&ul);
p = parsePerspective(p);
p += donumber(p, &ul);
if (version != 1 && ul != 3) {
errors << QString("Error Parsing CDistributionChartConfig got 0x%1 expected 0x03").arg(ul,0,16);
return p;
}
p = parseChart(p, version == 1 ? 2 : 0);
doshort(p, &us);
} while (charts && us != 0xffff);
return p;
}
WKO_UCHAR *WkoParser::parseCRideSummaryConfig(WKO_UCHAR *p)
{
p = parsePerspective(p);
if (version == 1 || version == 12 || version == 7) {
p += 12;
charts--;
} else {
p += donumber(p, &ul);
p += donumber(p, &ul);
charts--;
/* might still be padding ? */
}
p += optpad(p);
return p;
}
WKO_UCHAR *WkoParser::parseCMeanMaxChartConfig(WKO_UCHAR *p)
{
do {
// always an extra number in earlier versions
if (version == 1 || version == 7 || version == 12) p += donumber(p,&ul);
p = parsePerspective(p);
p += donumber(p, &ul);
if (version != 1 && ul != 3) {
errors << QString("Error Parsing CMeanMaxChartConfig got 0x%1 expected 0x03").arg(ul,0,16);
return p;
}
p = parseChart(p, version == 1 ? 0x0c : 0);
doshort(p, &us);
} while (charts && us != 0xffff);
return p;
}
WKO_UCHAR *WkoParser::parseCMeanMaxChartCache(WKO_UCHAR *p) // only present in v28 onwards
{
WKO_ULONG recs, term;
p += 20;
p += donumber(p, &ul);
p += (ul * 8);
p += 28;
p += donumber(p, &ul);
p += (ul * 8);
/* now its the main structures from a meanmax chart */
/* How many meanmax records in first set ? */
p += doshort(p, &us);
recs = us;
while (recs--) {
p += donumber(p, &ul);
if (!ul) return p;
else {
p += 28;
}
}
// preamble
p += 12;
// record count
p += doshort(p, &us);
recs = us;
term=1;
// loop over records
while (recs--) {
p += donumber(p, &ul);
if (!ul) break;
else {
p += 28;
}
}
// trail (if not a truncated data set
if (term) p += 24;
// skip over markers
p += optpad(p);
return p;
}
WKO_UCHAR *WkoParser::parseCDistributionChartCache(WKO_UCHAR *p) // only in v2.2 onwards
{
// we just position after the initial block since it is then
// followed by chart arrays that can be handled in the main loop
// of parseHeaderData (num ==2 and num == 3)
p += donumber(p, &ul);
p += donumber(p, &ul);
p += donumber(p, &ul);
p += donumber(p, &ul);
// count
p += doshort(p, &us);
while (us) {
us--;
p += donumber(p, &ul);
p += donumber(p, &ul);
}
p += optpad(p);
return p;
}
/*==============================================================================
* UTILITY FUNCTIONS
*==============================================================================*/
/**********************************************************************************
* SETUP DECODING CONTROLS
*
* bitsize() - return size in bits of a given graph for a specific WKO_device
* nullvals() - return nullvalue for this GRAPH on this WKO_device
**********************************************************************************/
unsigned int
WkoParser::bitsize(char g, int WKO_device, WKO_ULONG version)
{
if (version != 1 && version != 7) {
switch (g) {
case 'P' : return (12); break;
case 'H' : return (8); break;
case 'C' : return (8); break;
case 'S' : return (11); break;
case 'A' : return (20); break;
case 'T' : return (11); break;
case 'D' :
/* distance */
switch (WKO_device) {
case 0x04:
case 0x05:
case 0x06:
case 0x19:
default:
return 19;
break;
case 0x01:
case 0x11:
case 0x00:
case 0x12: // Garmin Edge 205/305
case 0x13:
case 0x14:
case 0x15: // suunto
case 0x16: // Cycleops PT300
case 0x1a: // SRM Powercontrol VI
// Alex Simmons files Oct 2010
return 22;
break;
}
break;
case 'G' : return (64); break;
case 'W' : return (11); break;
case '+' : return (8); break;
case '^' : return (20); break;
case 'm' : return (1); break;
default:
return (0);
}
} else { //!!! version 1
switch (g) {
case 'P' : if (version == 1) return 11;
else if (version == 7) return 12; // was 12?
break;
case 'H' : return (8); break;
case 'C' : return (8); break;
case 'S' : return (11); break;
case 'A' : if (version == 7) return 16;
else return(14);
break;
case 'T' : return (11); break;
case 'D' :
/* distance */
switch (WKO_device) {
case 0x04:
case 0x05:
case 0x06:
case 0x19:
case 0x1a:
default:
return 11;
break;
case 0x01:
case 0x11:
case 0x16: // Cycleops PT300
case 0x00:
case 0x12: // Garmin Edge 205/305
case 0x13:
case 0x14:
return 11;
break;
}
break;
case 'G' : return (64); break;
case 'W' : return (11); break;
case '+' : return (8); break;
case '^' : return (20); break;
case 'm' : return (1); break;
default:
return (0);
}
}
return 0; // impossible (in theory) but keeps compiler happy
}
WKO_ULONG
WkoParser::nullvals(char g, WKO_ULONG version)
{
switch (g) {
case 'P' : if (version != 1) return (4095L);
else return (2047L);
break;
case 'H' : return (255L); break;
case 'C' : return (255L); break;
case 'S' : return (2047L); break;
case 'A' : if (version > 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<quint32>(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<char *>(buf), reinterpret_cast<char *>(p+1), *p);
buf[*p]=0;
return (*p)+1;
} else {
p += 1;
p += doshort(p, &us);
strncpy(reinterpret_cast<char *>(buf), reinterpret_cast<char *>(p), us);
buf[us]=0;
return (us)+3;
}
}
unsigned int
WkoParser::doshort(WKO_UCHAR *p, WKO_USHORT *pnum)
{
*pnum = qFromLittleEndian<quint16>(p);
return 2;
}