Files
GoldenCheetah/src/FileIO/Computrainer3dpFile.cpp
Keith Reynolds 4fdeee1499 Delete new object to prevent memory leak.
Remove unnecessary assignment.
Prevent divide by zero
2017-03-30 23:39:58 -06:00

340 lines
11 KiB
C++

/*
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net),
* Justin F. Knotzke (jknotzke@shampoo.ca)
* Copyright (c) 2009 Greg Lonnon (greg.lonnon@gmail.com)
*
* Additional contributions from:
* Steve Gribble (gribble [at] cs.washington.edu) [December 3, 2009]
* Daniel Stark [December 3, 2009]
*
* 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 "Computrainer3dpFile.h"
#include <QDateTime>
#include <QDebug>
#include <QRegExp>
#include <QString>
#include <QTextStream>
#include <QVector>
#include <algorithm> // for std::sort
#include <stdint.h> // for int8_t, int16_t, etc.
#include <iostream>
#include "cmath"
#include "Units.h"
static int Computrainer3dpFileReaderRegistered =
RideFileFactory::instance().registerReader("3dp",
"Computrainer 3dp",
new
Computrainer3dpFileReader
());
RideFile *Computrainer3dpFileReader::openRideFile(QFile & file,
QStringList & errors,
QList<RideFile*>*)
const
{
// open up the .3dp file, prepare a little-endian-ordered
// QDataStream
if (!file.open(QFile::ReadOnly)) {
errors << ("Could not open ride file: \"" + file.fileName() +
"\"");
return NULL;
}
RideFile *rideFile = new RideFile();
QDataStream is(&file);
// Note that QT4.6 and above default to 64 bit floats. For
// backwards and forwards compatibility, we'll freeze the stream
// version we use for parsing at 4.0, and then add in LittleEndian
// number format, which is what Computrainer3dp has.
is.setVersion(QDataStream::Qt_4_0);
is.setByteOrder(QDataStream::LittleEndian);
// start parsing the header
// looks like the first part is a header... ignore it.
is.skipRawData(4);
// the next 4 bytes are the ASCII characters 'perf'
char perfStr[5];
is.readRawData(perfStr, 4);
perfStr[4] = '\0';
if(strcmp(perfStr,"perf"))
{
errors << "File is encrypted.";
delete rideFile;
return NULL;
}
// not sure what the next 8 bytes are; skip them
is.skipRawData(0x8);
// the next 65 bytes are a null-terminated and padded
// ASCII user name string
char userName[65];
is.readRawData(userName, 65);
// next is a single byte of user age, in years. I guess
// Computrainer doesn't allow people to get older than 255
// years. ;)
uint8_t age;
is >> age;
// not sure what the next 6 bytes are; skip them.
is.skipRawData(6);
// next is a (4 byte) C-style floating point with weight in kg
float weight;
is >> weight;
// next is the upper heart rate limit (4 byte int)
uint32_t upperHR;
is >> upperHR;
// and then the resting heart rate (4 byte int)
uint32_t lowerHR;
is >> lowerHR;
// then year, month, day, hour, minute the exercise started
// (4, 1, 1, 1, 1 bytes)
uint32_t year;
is >> year;
uint8_t month;
is >> month;
uint8_t day;
is >> day;
uint8_t hour;
is >> hour;
uint8_t minute;
is >> minute;
// the number of exercise data points in the file (4 byte int)
uint32_t numberSamples;
is >> numberSamples;
// go back to the start, and skip header to go to
// the start of the data samples.
file.seek(0);
is.skipRawData(0xf8);
// we'll keep track of the altitude over time. since computrainer
// gives us slope, we can calculate change in altitude if we know
// change in distance traveled, so we also need to keep track of
// the previous sample's distance.
float altitude = 100.0; // arbitrary starting altitude of 100m
float lastKM = 0;
// computrainer 3d software lets you start your ride partway into
// a course. if you do this, then the first distance reported in
// the corresponding log file will be that offset, rather than
// zero. so, we'll stash away the first reported distance, and
// use that to offset distances that we report to GC so that they
// are zero-based (i.e., so that the first data point is at
// distance zero).
float firstKM = 0;
bool gotFirstKM = false;
// for computrainer / VELOtron we need to convert from the variable rate
// the file uses to a fixed rate since thats a base assumption across the
// GC codebase. This parameter can be adjusted to the sample (recIntSecs) rate
// but in milliseconds
const int SAMPLERATE = 1000; // we want 1 second samples, re-used below, change to taste
RideFilePoint sample; // we reuse this to aggregate all values
long time = 0L; // current time accumulates as we run through data
double lastT = 0.0f; // last sample time seen in seconds
double lastK = 0.0f; // last sample distance seen in kilometers
// loop through samples
for (; numberSamples; numberSamples--) {
//
// READ A SAMPLE FROM FILE - VARIABLE BUT HI-RESOLUTION SAMPLE RATE
//
// 1 byte heart rate, in BPM
uint8_t hr;
is >> hr;
// 1 byte cadence, in RPM
uint8_t cad;
is >> cad;
// 2 unsigned bytes of watts
uint16_t watts;
is >> watts;
// 4 bytes of floating point speed (in mph/160 !!)
float speed;
is >> speed;
speed = speed * 160 * KM_PER_MILE; // convert to kph
// 4 bytes of total elapsed time, in milliseconds
uint32_t ms;
is >> ms;
// 2 signed bytes of 100 * [percent grade]
// (i.e., grade == 100 * 100 * rise/run !!)
int16_t grade;
is >> grade;
// not sure what the next 2 bytes are
is.skipRawData(2);
// 4 bytes of floating point total distance traveled, in KM
float km;
is >> km;
if (!gotFirstKM) {
firstKM = km;
gotFirstKM = true;
}
// subtract off the first KM so that distances are zero-based.
km -= firstKM;
// calculate change in altitude over the past interval.
// first, calculate grade measured as rise/run.
float floatGrade;
floatGrade = 0.01 * 0.01 * grade; // floatgrade = rise/run
// then, convert grade to angle (in radians).
float angle = atan(floatGrade);
// calculate distance traveled over past interval
float delta_distance_meters = (1000.0) * (km - lastKM);
// change in altitude is:
// sin(angle) * (distance traveled in past interval).
altitude = altitude + delta_distance_meters*sin(angle);
// not sure what the next 28 bytes are.
is.skipRawData(0x1c);
//
// LETS SAVE OUR SAMPLE
//
RideFilePoint value;
value.secs = ms/1000;
value.watts = watts;
value.cad = cad;
value.hr = hr;
value.km = km;
value.kph = speed;
value.alt = altitude;
// whats the dt in microseconds
int dt = (value.secs * 1000) - (lastT * 1000);
int odt = dt;
lastT = value.secs;
// whats the dk in meters
int dk = (value.km * 1000) - (lastK * 1000);
lastK = value.km;
//
// RESAMPLE INTO SAMPLERATE
//
while (dt > 0) {
// we keep track of how much time has been aggregated
// into sample, so 'need' is whats left to aggregate
// for the full sample
int need = SAMPLERATE - sample.secs;
// aggregate
if (dt < need) {
// the entire sample read is less than we need
// so aggregate the whole lot and wait fore more
// data to be read. If there is no more data then
// this will be lost, we don't keep incomplete samples
sample.secs += dt;
sample.watts += float(dt) * value.watts;
sample.cad += float(dt) * value.cad;
sample.hr += float(dt) * value.hr;
sample.kph += float(dt) * value.kph;
dt = 0;
} else {
// dt is more than we need to fill and entire sample
// so lets just take the fraction we need
dt -= need;
// accumulating time and distance
sample.secs = time; time += double(SAMPLERATE) / 1000.0f;
// subtract remains of this sample from the distance for
// the entire sample, remembering that dk is meters and
// dt is milliseconds
sample.km = lastK - ((float(dt)/(float(odt)) * dk) / 1000.0f);
// averaging sample data
sample.watts += float(need) * value.watts;
sample.cad += float(need) * value.cad;
sample.hr += float(need) * value.hr;
sample.kph += float(need) * value.kph;
sample.watts /= double(SAMPLERATE);
sample.cad /= double(SAMPLERATE);
sample.hr /= double(SAMPLERATE);
sample.kph /= double(SAMPLERATE);
// so now we can add to the ride
rideFile->appendPoint(sample.secs, sample.cad, sample.hr, sample.km,
sample.kph, 0.0, sample.watts, sample.alt, 0.0, 0.0,
0.0, 0.0, RideFile::NA, RideFile::NA,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0,0.0,0.0,0.0, 0.0,0.0,0.0,0.0,0.0,0.0, 0);
// reset back to zero so we can aggregate
// the next sample
sample.secs = 0;
sample.watts = 0;
sample.cad = 0;
sample.hr = 0;
sample.km = 0;
sample.kph = 0;
sample.alt = 0;
sample.headwind = 0;
}
}
}
file.close();
// convert the start time we parsed from the header into
// what GC wants.
QDateTime dateTime;
QDate date;
QTime ridetime;
date.setDate(year, month, day);
ridetime.setHMS(hour, minute, 0, 0);
dateTime.setDate(date);
dateTime.setTime(ridetime);
rideFile->setStartTime(dateTime);
rideFile->setRecIntSecs(((double) SAMPLERATE) / 1000.0);
// tell GC what kind of device a computrainer is
rideFile->setDeviceType("Computrainer");
rideFile->setFileFormat("Computrainer 3DP (3dp)");
// all done! close up.
file.close();
return rideFile;
}