Files
GoldenCheetah/src/WkoRideFile.cpp
Damien Grauser 70c8f0223c Add Temperature and Slope Data Series
This patch adds support for temperature and slope
across the ridefile readers.

For the most part their is no functional change
although it is now possible to view and edit these
data series in the editor.

File formats that can provide temp or slope include;
.bin, .fit, .srm, .sync, .wko

Further updates will be required to display the data
in the ride plot and histograms.
2011-12-03 23:17:11 +00:00

1496 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
*/
// NOTE:
// Windspeed, Temperature and Slope are not imported because
// GC does not support them (yet).
#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));
// 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, 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;
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;
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, 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;
}