From 59805db47cd3db146b2072b67a8dfa924512f64b Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Mon, 24 Aug 2009 22:57:41 +0100 Subject: [PATCH] Initial support for WKO file import --- src/WkoRideFile.cpp | 1074 +++++++++++++++++++++++++++++++++++++++++++ src/WkoRideFile.h | 78 ++++ 2 files changed, 1152 insertions(+) create mode 100644 src/WkoRideFile.cpp create mode 100644 src/WkoRideFile.h diff --git a/src/WkoRideFile.cpp b/src/WkoRideFile.cpp new file mode 100644 index 000000000..5a5060d21 --- /dev/null +++ b/src/WkoRideFile.cpp @@ -0,0 +1,1074 @@ +/* + * Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net), + * Justin F. Knotzke (jknotzke@shampoo.ca) + * + * 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 // for std::sort +#include +#include "math.h" + + +static int wkoFileReaderRegistered = + RideFileFactory::instance().registerReader("wko", new WkoFileReader()); + +RideFile *WkoFileReader::openRideFile(QFile &file, QStringList &errors) const +{ + unsigned char *headerdata, *rawdata, *footerdata; + + if (!file.open(QFile::ReadOnly)) { + errors << ("Could not open ride file: \"" + + file.fileName() + "\""); + return NULL; + file.close(); + } + + // read in the whole file and pass to parser routines for decoding + headerdata = (unsigned char *) malloc(file.size()); // with apologies to c++ neophytes + + // read entire raw data stream + QDataStream *rawstream = new QDataStream(&file); + rawstream->readRawData((char *)headerdata, (unsigned int) file.size()); + delete rawstream; + file.close(); + + // Golden Cheetah ride file + RideFile *rideFile = new RideFile(); + + // read header data and store details into rideFile structure + rawdata = WkoParseHeaderData(headerdata, rideFile); + + // Parse raw data (which calls rideFile->appendPoint() with each sample + if (rawdata) footerdata = WkoParseRawData(rawdata, rideFile); + else { + delete rideFile; + free (headerdata); + return NULL; + } + + // all done clean-up malloced memory ยง(ignore footer data for no ) + free(headerdata); // header/raw/footer all in same memory buffer + + if (footerdata) return rideFile; + else return NULL; +} + + +/*************************************************************************************** + * PROCESS THE RAW DATA + * + * WkoParseRawData() - read through all the raw data adding record points and return + * a pointer to the footer record + **************************************************************************************/ +unsigned char *WkoParseRawData(unsigned char *fb, RideFile *rideFile) +{ + double cad, hr, km, kph, nm, watts, interval; + + int isnull=0; + unsigned long records, data; + unsigned char *thelot; + unsigned short us; + + int imperialflag=0; + + int i; + int bit=0; + unsigned long inc; + unsigned long rtime=0,rdist=0; + + int WKO_graphbits[32]; // number of bits for each GRAPH + unsigned long WKO_nullval[32]; // null value for each graph + char GRAPHDATA[32][32]; // formatted values for each sample + + // setup decoding controls + setxormasks(); + for (i=0; WKO_GRAPHS[i] != '\0'; i++) { + WKO_nullval[i] = nullvals(WKO_GRAPHS[i]); // setup nullvalue + if ((WKO_graphbits[i] = bitsize(WKO_GRAPHS[i], WKO_device)) == 0) { // setup & check field size + fprintf(stderr, "ERROR: Unknown channel '%c' for WKO_device %x.\n", WKO_GRAPHS[i], WKO_device); + return (NULL); + } + } + + /* So how many records are there? */ + fb += doshort(fb, &us); + if (us == 0xffff) { + fb += donumber(fb, &records); + } else { + records = us; + } + + /* 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 + bit = 43; + } + interval = inc; + interval /= 1000; + rideFile->setRecIntSecs(interval); + + + /*------------------------------------------------------------------------------ + * RUN THROUGH EACH RAW DATA RECORD + *==============================================================================*/ + rdist=rtime=0; + while (records) { + + unsigned int marker; + long sval=0; + unsigned long val=0; + + + // reset point values; + 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; + } + sprintf(GRAPHDATA[i], "%8ld", sval); + break; + case '^' : /* Slope */ + case 'W' : /* Wind speed */ + case 'A' : /* Altitude */ + if (get_bit(thelot, bit-1)) { /* is negative */ + val ^= WKO_xormasks[WKO_graphbits[i]]; + sval = val * -1; + + if (imperialflag && WKO_GRAPHS[i]=='A') val = long((double) val * MTOFT); + if (imperialflag && WKO_GRAPHS[i]=='W') val = long((double) val * KMTOMI); + sprintf(GRAPHDATA[i], "%6ld.%1ld", sval/10, val%10); + } else { + if (imperialflag && WKO_GRAPHS[i]=='A') val = long((double) val * MTOFT); + if (imperialflag && WKO_GRAPHS[i]=='W') val = long((double) val * KMTOMI); + sprintf(GRAPHDATA[i], "%6ld.%1ld", val/10, val%10); + } + break; + case 'T' : /* Torque */ + if (imperialflag && WKO_GRAPHS[i]=='S') val = long((double)val * KMTOMI); + sprintf(GRAPHDATA[i], "%6ld.%1ld", val/10, val%10); + nm = val; nm /=10; + break; + case 'S' : /* Speed */ + if (imperialflag && WKO_GRAPHS[i]=='S') val = long((double)val * KMTOMI); + sprintf(GRAPHDATA[i], "%6ld.%1ld", val/10, val%10); + kph = val; kph/= 10; + 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; + 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 floats (?) */ + { + signed long llat, llon; + double lat,lon; + 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; + sprintf(slat, "%-3.9g", lat); + + val = get_bits(thelot, bit-32,32); + memcpy(&llon, &val, 4); + lon = (double)llon; + lon *= 0.00000008381903171539306640625; + sprintf(slon, "%-3.9g", lon); + + sprintf(GRAPHDATA[i], "%13s %13s", slat,slon); + + } + break; + case 'P' : + watts = val; + sprintf(GRAPHDATA[i], "%8lu", val); // just output as numeric + break; + + case 'H' : + hr = val; + sprintf(GRAPHDATA[i], "%8lu", val); // just output as numeric + break; + + case 'C' : + cad = val; + sprintf(GRAPHDATA[i], "%8lu", val); // 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) { + + + rideFile->appendPoint((double)rtime/1000, cad, hr, km, + kph, nm, watts, (int) 1, 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; + + if (WKO_device != 0x14) pausesize=42; + else pausesize=39; + + pausetime = get_bits(thelot, bit, 32); + + /* 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 + */ + inc = pausetime; + 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 + * + * WkoParseHeadeData() - read through file and land on the raw data + * + *********************************************************************/ +unsigned char *WkoParseHeaderData(unsigned char *fb, RideFile *rideFile) +{ + unsigned long julian, sincemidnight; + + enum configtype lastchart; + unsigned int num,x,z,i; + + /* utility holders */ + unsigned long ul; + unsigned short us; + double g; + unsigned char *txt = (unsigned char *)malloc(1024000); + + /* working through filebuf */ + unsigned char *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. + + p += dotext(p, txt); /* 4: goal */ + p += dotext(p, txt); /* 5: notes */ + + p += dotext(p, txt); /* 6: graphs */ + strcpy((char *)WKO_GRAPHS, (char *)txt); // save those graphs away + + p += donumber(p, &ul); /* 7: sport */ + p += dotext(p, txt); /* 8: workout code */ + p += donumber(p, &ul); /* 9: duration 000s of seconds */ + p += dotext(p, txt); /* 10: lastname */ + p += dotext(p, txt); /* 11: firstname */ + + p += donumber(p, &ul); /* 12: time of day */ + sincemidnight = ul; + + // Set start time + QDateTime datetime(QDate::fromJulianDay(julian), + QTime(sincemidnight/360000, (sincemidnight%360000)/6000, (sincemidnight%6000)/100)); + rideFile->setStartTime(datetime); + + + 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 */ + p += dodouble(p, &g); /* 18: athlete threshold pace */ + p += donumber(p, &ul); /* 19: weight in grams/10 */ + + 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 */ + + /*************************************************** + * 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 + +#if 0 + switch (WKO_device) { + case 0x01 : rideFile->setDeviceType("Powertap (via WKO)"); break; + case 0x04 : rideFile->setDeviceType("SRM (via WKO)"); break; + case 0x05 : rideFile->setDeviceType("Polar (via WKO)"); break; + case 0x06 : rideFile->setDeviceType("Computrainer/Velotron (via WKO)"); break; + case 0x11 : rideFile->setDeviceType("Ergomo (via WKO)"); break; + case 0x13 : rideFile->setDeviceType("Garmin (via WKO)"); break; + case 0x14 : rideFile->setDeviceType("Ergomo (via WKO)"); break; + case 0x19 : rideFile->setDeviceType("Ergomo (via WKO)"); break; + default : rideFile->setDeviceType("Unknown Device (via WKO)"); break; + } +#else + rideFile->setDeviceType("WKO"); +#endif + + p += donumber(p, &ul); /* 29: unknown */ + + for (i=0; i< 16; i++) { // 16 types of chart data + + p += 44; + //p += dofloat(p, &f) /* 30: unknown */ + //p += dofloat(p, &f) /* 31: unknown */ + //p += dofloat(p, &f) /* 32: unknown */ + //p += dofloat(p, &f) /* 33: unknown */ + //p += dofloat(p, &f) /* 34: unknown */ + //p += dofloat(p, &f) /* 35: unknown */ + //p += dofloat(p, &f) /* 36: unknown */ + //p += donumber(p, &ul); /* 37: x-axis maximum */ + //p += donumber(p, &ul); /* 38: show chart? */ + //p += donumber(p, &ul); /* 39: autoscale? */ + //p += donumber(p, &ul); /* 40: autogridlines? */ + + p += doshort(p, &us); /* 41: number of gridlines XXVARIABLEXX */ + p += (us * 8); // 2 longs x number of gridlines + } + + + /* Ranges */ + p += doshort(p, &us); /* 237: Number of ranges XXVARIABLEXX */ + for (i=0; i> 3]; // X>>3 is X/8 + unsigned char bitmask = 1 << (bitoffset %8); // X&7 is X%8 + return ((c & bitmask)!=0) ? 1 : 0; +} + +unsigned int get_bits(unsigned char* 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; +} + +void setxormasks() +{ + int i, j; + + for (i=1; i<32; i++) { + WKO_xormasks[i]=0; + for (j=0; j +#include + +struct WkoFileReader : public RideFileReader { + virtual RideFile *openRideFile(QFile &file, QStringList &errors) const; + +}; + +unsigned char *WkoParseHeaderData(unsigned char *data, RideFile *rideFile) ; +unsigned char *WkoParseRawData(unsigned char *data, RideFile *rideFile) ; + +// Some Globals -- try and remove them as I refactor code from the original WKO2CSV source +int WKO_device; // Original Device ID +char WKO_GRAPHS[32]; // output GRAPHS string +int WKO_graphbits[32]; // number of bits for each GRAPH +unsigned long WKO_nullval[32]; // null value for each graph +unsigned int WKO_xormasks[32]; // xormasks used all over + +// UTILITY FUNCTIONS +// Field decoding +unsigned int doshort(unsigned char *p, unsigned short *pnum); +unsigned int donumber(unsigned char *p, unsigned long *pnum); +unsigned int dotext(unsigned char *p, unsigned char *txt);unsigned int dofloat(unsigned char *p, float *pnum); +unsigned int dodouble(unsigned char *p, double *pnum); +unsigned int optpad(unsigned char *p), + optpad2(unsigned char *p); + +// Bit twiddlers +int get_bit(unsigned char *data, unsigned bitoffset); // returns 0 or 1 +unsigned int get_bits(unsigned char* data, unsigned bitOffset, unsigned numBits); // returns 32 bit unsigned + +// Decoding setup +void setxormasks(); +unsigned long nullvals(char graph); +unsigned long bitget(char *thelot, int offset, int count); +unsigned int bitsize(char graph, int device); + +// different Chart segment types +enum configtype { + CRIDESETTINGSCONFIG, + CRIDEGOALCONFIG, + CRIDENOTESCONFIG, + CDISTRIBUTIONCHARTCONFIG, + CRIDESUMMARYCONFIG, + CMEANMAXCHARTCONFIG, + CMEANMAXCHARTCACHE, + CDISTRIBUTIONCHARTCACHE, + OTHER, + INVALID +}; + +#define KMTOMI 0.6213 +#define MTOFT 3.2808 + +#endif // _WkoRideFile_h +