Files
GoldenCheetah/src/FileIO/FitRideFile.cpp
Mark Liversedge eb363c3fab Fix Swim FIT parse crash
.. not checking timeIndex() for out of bounds
   when secs goes negative during parsing.

.. also, enable interpolation for swim laps by
   default to ensure no data loss (i.e. don't
   insist on garmin smart recording setting).

Fixes #2953
2018-04-24 10:10:02 +01:00

3579 lines
135 KiB
C++

/*
* Copyright (c) 2007-2008 Sean C. Rhea (srhea@srhea.net)
* Copyright (c) 2016-2017 Damien Grauser (Damien.Grauser@pev-geneve.ch)
*
* 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 "FitRideFile.h"
#include "Settings.h"
#include "Units.h"
#include "RideItem.h"
#include "Specification.h"
#include <QSharedPointer>
#include <QMap>
#include <QSet>
#include <QtEndian>
#include <QDebug>
#include <QTime>
#include <cstdio>
#include <stdint.h>
#include <time.h>
#include <limits>
#include <cmath>
#define FIT_DEBUG false // debug traces
#define FIT_DEBUG_LEVEL 4 // debug level : 1 message, 2 definition, 3 data without record, 4 data
#ifndef MATHCONST_PI
#define MATHCONST_PI 3.141592653589793238462643383279502884L /* pi */
#endif
#define DEFINITION_MSG_HEADER 64
#define FILE_ID_MSG_NUM 0
#define SESSION_MSG_NUM 18
#define LAP_MSG_NUM 19
#define RECORD_MSG_NUM 20
#define EVENT_MSG_NUM 21
#define ACTIVITY_MSG_NUM 34
#define FILE_CREATOR_MSG_NUM 49
#define HRV_MSG_NUM 78
#define SEGMENT_MSG_NUM 142
static int fitFileReaderRegistered =
RideFileFactory::instance().registerReader(
"fit", "Garmin FIT", new FitFileReader());
// 1989-12-31 00:00:00 UTC
static const QDateTime qbase_time(QDate(1989, 12, 31), QTime(0, 0, 0), Qt::UTC);
/* FIT has uint32 as largest integer type. So qint64 is large enough to
* store all integer types - no matter if they're signed or not */
// this will need to change if float or other non-integer values are
// introduced into the file format
typedef qint64 fit_value_t;
#define NA_VALUE std::numeric_limits<fit_value_t>::max()
typedef std::string fit_string_value;
typedef float fit_float_value;
struct FitField {
int num;
int type; // FIT base_type
int size; // in bytes
int deve_idx; // Developer Data Index
};
struct FitDeveField {
int dev_id; // Developer Data Index
int num;
int type; // FIT base_type
int size; // in bytes
int native; // native field number
int scale;
int offset;
fit_string_value name;
fit_string_value unit;
};
struct FitDefinition {
int global_msg_num;
bool is_big_endian;
std::vector<FitField> fields;
};
enum fitValueType { SingleValue, ListValue, FloatValue, StringValue };
typedef enum fitValueType FitValueType;
struct FitValue
{
FitValueType type;
fit_value_t v;
fit_string_value s;
fit_float_value f;
QList<fit_value_t> list;
int size;
};
struct FitFileReaderState
{
QFile &file;
QStringList &errors;
RideFile *rideFile;
time_t start_time;
time_t last_time;
quint32 last_event_timestamp;
double start_timestamp;
double last_distance;
QMap<int, FitDefinition> local_msg_types;
QMap<QString, FitDeveField> local_deve_fields; // All developer fields
QMap<int, int> record_extra_fields;
QMap<QString, int> record_deve_fields; // Developer fields in DEVELOPER XDATA or STANDARD DATA
QMap<QString, int> record_deve_native_fields; // Developer fields with native values
QSet<int> record_native_fields;
QSet<int> unknown_record_fields, unknown_global_msg_nums, unknown_base_type;
int interval;
int calibration;
int devices;
bool stopped;
bool isLapSwim;
double pool_length;
int last_event_type;
int last_event;
int last_msg_type;
double frac_time; // to carry sub-second length time in pool swimming
double last_lap_end; // to align laps for drill mode in pool swimming
QVariant isGarminSmartRecording;
QVariant GarminHWM;
XDataSeries *weatherXdata;
XDataSeries *gearsXdata;
XDataSeries *swimXdata;
XDataSeries *deveXdata;
XDataSeries *extraXdata;
XDataSeries *hrvXdata;
QMap<int, QString> deviceInfos;
QList<QString> dataInfos;
FitFileReaderState(QFile &file, QStringList &errors) :
file(file), errors(errors), rideFile(NULL), start_time(0),
last_time(0), last_distance(0.00f), interval(0), calibration(0),
devices(0), stopped(true), isLapSwim(false), pool_length(0.0),
last_event_type(-1), last_event(-1), last_msg_type(-1), frac_time(0.0),
last_lap_end(0.0)
{}
struct TruncatedRead {};
void read_unknown( int size, int *count = NULL ) {
if (!file.seek(file.pos() + size))
throw TruncatedRead();
if (count)
(*count) += size;
}
fit_string_value read_text(int len, int *count = NULL) {
char c;
fit_string_value res = "";
for (int i = 0; i < len; ++i) {
if (file.read(&c, 1) != 1)
throw TruncatedRead();
if (count)
*count += 1;
if (c != 0)
res += c;
}
return res;
}
fit_value_t read_int8(int *count = NULL) {
qint8 i;
if (file.read(reinterpret_cast<char*>( &i), 1) != 1)
throw TruncatedRead();
if (count)
(*count) += 1;
return i == 0x7f ? NA_VALUE : i;
}
fit_value_t read_uint8(int *count = NULL) {
quint8 i;
if (file.read(reinterpret_cast<char*>( &i), 1) != 1)
throw TruncatedRead();
if (count)
(*count) += 1;
return i == 0xff ? NA_VALUE : i;
}
fit_value_t read_uint8z(int *count = NULL) {
quint8 i;
if (file.read(reinterpret_cast<char*>( &i), 1) != 1)
throw TruncatedRead();
if (count)
(*count) += 1;
return i == 0x00 ? NA_VALUE : i;
}
fit_value_t read_int16(bool is_big_endian, int *count = NULL) {
qint16 i;
if (file.read(reinterpret_cast<char*>(&i), 2) != 2)
throw TruncatedRead();
if (count)
(*count) += 2;
i = is_big_endian
? qFromBigEndian<qint16>( i )
: qFromLittleEndian<qint16>( i );
return i == 0x7fff ? NA_VALUE : i;
}
fit_value_t read_uint16(bool is_big_endian, int *count = NULL) {
quint16 i;
if (file.read(reinterpret_cast<char*>(&i), 2) != 2)
throw TruncatedRead();
if (count)
(*count) += 2;
i = is_big_endian
? qFromBigEndian<quint16>( i )
: qFromLittleEndian<quint16>( i );
return i == 0xffff ? NA_VALUE : i;
}
fit_value_t read_uint16z(bool is_big_endian, int *count = NULL) {
quint16 i;
if (file.read(reinterpret_cast<char*>(&i), 2) != 2)
throw TruncatedRead();
if (count)
(*count) += 2;
i = is_big_endian
? qFromBigEndian<quint16>( i )
: qFromLittleEndian<quint16>( i );
return i == 0x0000 ? NA_VALUE : i;
}
fit_value_t read_int32(bool is_big_endian, int *count = NULL) {
qint32 i;
if (file.read(reinterpret_cast<char*>(&i), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
i = is_big_endian
? qFromBigEndian<qint32>( i )
: qFromLittleEndian<qint32>( i );
return i == 0x7fffffff ? NA_VALUE : i;
}
fit_value_t read_uint32(bool is_big_endian, int *count = NULL) {
quint32 i;
if (file.read(reinterpret_cast<char*>(&i), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
i = is_big_endian
? qFromBigEndian<quint32>( i )
: qFromLittleEndian<quint32>( i );
return i == 0xffffffff ? NA_VALUE : i;
}
fit_value_t read_uint32z(bool is_big_endian, int *count = NULL) {
quint32 i;
if (file.read(reinterpret_cast<char*>(&i), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
i = is_big_endian
? qFromBigEndian<quint32>( i )
: qFromLittleEndian<quint32>( i );
return i == 0x00000000 ? NA_VALUE : i;
}
fit_float_value read_float32(int *count = NULL) {
float f;
if (file.read(reinterpret_cast<char*>(&f), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
return f;
}
void DumpFitValue(const FitValue& v) {
printf("type: %d %llx %s\n", v.type, v.v, v.s.c_str());
}
void convert2Run() {
if (rideFile->areDataPresent()->cad) {
foreach(RideFilePoint *pt, rideFile->dataPoints()) {
pt->rcad = pt->cad;
pt->cad = 0;
}
rideFile->setDataPresent(RideFile::rcad, true);
rideFile->setDataPresent(RideFile::cad, false);
}
}
QString getManuProd(int manu, int prod) {
if (manu == 1) {
// Garmin
if (prod == -1)
return "Garmin";
// Product IDs can be found in c/fit_example.h in the FIT SDK.
// Multiple product IDs refer to different regions e.g. China, Japan etc.
switch (prod) {
case 473: case 474: case 475: case 494: return "Garmin FR301";
case 717: case 987: return "Garmin FR405";
case 782: return "Garmin FR50";
case 988: return "Garmin FR60";
case 1018: return "Garmin FR310XT";
case 1036: case 1199: case 1213: case 1387: return "Garmin Edge 500";
case 1124: case 1274: return "Garmin FR110";
case 1169: case 1333: case 1334: case 1386: return "Garmin Edge 800";
case 1325: return "Garmin Edge 200";
case 1328: return "Garmin FR910XT";
case 1345: case 1410: return "Garmin FR610";
case 1360: return "Garmin FR210";
case 1436: return "Garmin FR70";
case 1446: return "Garmin FR310XT 4T";
case 1482: case 1688: return "Garmin FR10";
case 1499: return "Garmin Swim";
case 1551: return "Garmin Fenix";
case 1561: case 1742: case 1821: return "Garmin Edge 510";
case 1567: return "Garmin Edge 810";
case 1623: case 2173: return "Garmin FR620";
case 1632: case 2174: return "Garmin FR220";
case 1765: case 2130: case 2131: case 2132: return "Garmin FR920XT";
case 1836: case 2052: case 2053: case 2070: case 2100: return "Garmin Edge 1000";
case 1903: return "Garmin FR15";
case 1907: return "Garmin Vivoactive";
case 1967: return "Garmin Fenix 2";
case 2050: case 2188: case 2189: return "Garmin Fenix 3";
case 2067: case 2260: return "Garmin Edge 520";
case 2147: return "Garmin Edge 25";
case 2148: return "Garmin FR25";
case 2153: case 2219: return "Garmin FR225";
case 2156: return "Garmin FR630";
case 2157: return "Garmin FR230";
case 2158: return "Garmin FR735XT";
case 2204: return "Garmin Edge 1000 Explore";
case 2238: return "Garmin Edge 20";
case 2337: return "Garmin Vivoactive HR";
case 2347: return "Garmin Vivosmart HR+";
case 2348: return "Garmin Vivosmart HR";
case 2413: return "Garmin Fenix 3 HR";
case 2431: return "Garmin FR235";
case 2530: return "Garmin Edge 820";
case 2531: return "Garmin Edge 820 Explore";
case 2544: return "Garmin Fenix 5s";
case 2604: return "Garmin Fenix 5x";
case 2691: return "Garmin FR935";
case 2697: return "Garmin Fenix 5";
case 20119: return "Garmin Training Center";
case 65532: return "Android ANT+ Plugin";
case 65534: return "Garmin Connect Website";
default: return QString("Garmin %1").arg(prod);
}
} else if (manu == 6 ) {
// SRM
if (prod == -1)
return "SRM";
// powercontrol now uses FIT files from PC8
switch (prod) {
case 6: return "SRM PC6";
case 7: return "SRM PC7";
case 8: return "SRM PC8";
default: return "SRM Powercontrol";
}
} else if (manu == 7 ) {
// Quarq
if (prod == -1)
return "Quarq";
switch (prod) {
case 1: return "Quarq Cinqo";
case 9479: return "Quarq DZERO";
default: return QString("Quarq %1").arg(prod);
}
} else if (manu == 9 ) {
// Powertap
if (prod == -1)
return "Powertap";
switch (prod) {
case 14: return "Joule 2.0";
case 18: return "Joule";
case 19: return "Joule GPS";
case 22: return "Joule GPS+";
case 272: return "Powertap C1";
case 288: return "Powertap P1";
case 4096: return "Powertap G3";
default: return QString("Powertap Device %1").arg(prod);
}
} else if (manu == 13 ) {
// dynastream_oem
if (prod == -1)
return "Dynastream";
switch (prod) {
default: return QString("Dynastream %1").arg(prod);
}
} else if (manu == 29 ) {
// saxonar
if (prod == -1)
return "Power2max";
switch (prod) {
case 1031: return "Power2max S";
default: return QString("Power2max %1").arg(prod);
}
} else if (manu == 32) {
// wahoo
if (prod == -1)
return "Wahoo";
switch (prod) {
case 0: return "Wahoo fitness";
case 28: return "Wahoo ELEMNT";
case 31: return "Wahoo ELEMNT BOLT";
default: return QString("Wahoo fitness %1").arg(prod);
}
} else if (manu == 38) {
// o_synce
if (prod == -1)
return "o_synce";
switch (prod) {
case 1: return "o_synce navi2coach";
default: return QString("o_synce %1").arg(prod);
}
} else if (manu == 48) {
if (prod == -1)
return "Pioneer";
// Pioneer
switch (prod) {
case 2: return "Pioneer SGX-CA500";
default: return QString("Pioneer %1").arg(prod);
}
} else if (manu == 70) {
// does not set product at this point
return "Sigmasport ROX";
} else if (manu == 76) {
// Moxy
return "Moxy Monitor";
} else if (manu == 95) {
// Stryd
return "Stryd";
} else if (manu == 98) {
// BSX
if (prod == -1)
return "BSX";
switch(prod) {
case 2: return "BSX Insight 2";
default: return QString("BSX %1").arg(prod);
}
} else if (manu == 260) {
// Zwift
return "Zwift";
} else if (manu == 267) {
// Bryton
return "Bryton";
} else if (manu == 282) {
// Bryton
return "The Sufferfest";
} else if (manu == 284) {
// Rouvy
return "Rouvy";
} else {
QString name = "Unknown FIT Device";
return name + QString(" %1:%2").arg(manu).arg(prod);
}
}
QString getDeviceType(int device_type) {
switch (device_type) {
case 4: return "Headunit"; // bike_power
case 11: return "Powermeter"; // bike_power
case 120: return "HR"; // heart_rate
case 121: return "Speed-Cadence"; // bike_speed_cadence
case 122: return "Cadence"; // bike_speed
case 123: return "Speed"; // bike_speed
case 124: return "Stride"; // stride_speed_distance
default: return QString("Type %1").arg(device_type);
}
}
RideFile::SeriesType getSeriesForNative(int native_num) {
switch (native_num) {
case 0: // POSITION_LAT
return RideFile::lat;
case 1: // POSITION_LONG
return RideFile::lon;
case 2: // ALTITUDE
return RideFile::alt;
case 3: // HEART_RATE
return RideFile::hr;
case 4: // CADENCE
return RideFile::cad;
case 5: // DISTANCE
return RideFile::km;
case 6: // SPEED
return RideFile::kph;
case 7: // POWER
return RideFile::watts;
case 9: // GRADE
return RideFile::slope;
case 13: // TEMPERATURE
return RideFile::temp;
case 30: // LEFT_RIGHT_BALANCE
return RideFile::lrbalance;
case 39: // VERTICAL OSCILLATION
return RideFile::rvert;
case 41: // GROUND CONTACT TIME
return RideFile::rcontact;
case 45: // LEFT_PEDAL_SMOOTHNESS
return RideFile::lps;
case 46: // RIGHT_PEDAL_SMOOTHNESS
return RideFile::rps;
//case 47: // COMBINED_PEDAL_SMOOTHNES
// return RideFile::cps;
case 54: // THb
return RideFile::thb;
case 57: // SMO2
return RideFile::smo2;
default:
return RideFile::none;
}
}
QString getNameForExtraNative(int native_num) {
switch (native_num) {
case 47: // COMBINED_PEDAL_SMOOTHNES
return "COMBINEDSMOOTHNESS"; //Combined Pedal Smoothness
case 81: // BATTERY_SOC
return "BATTERYSOC";
default:
return QString("FIELD_%1").arg(native_num);
}
}
float getScaleForExtraNative(int native_num) {
switch (native_num) {
case 47: // COMBINED_PEDAL_SMOOTHNES
case 81: // BATTERY_SOC
return 2.0;
default:
return 1.0;
}
}
int getOffsetForExtraNative(int native_num) {
switch (native_num) {
default:
return 0;
}
}
void addRecordDeveField(QString key, FitDeveField deveField, bool xdata) {
QString name = deveField.name.c_str();
if (deveField.native>-1) {
int i = 0;
RideFile::SeriesType series = getSeriesForNative(deveField.native);
QString nativeName = rideFile->symbolForSeries(series);
if (nativeName.length() == 0)
nativeName = QString("FIELD_%1").arg(deveField.native);
else
i++;
QString typeName;
if (xdata) {
typeName = "DEVELOPER";
do {
i++;
name = nativeName + (i>1?QString("-%1").arg(i):"");
}
while (deveXdata->valuename.contains(name));
}
else {
typeName = "STANDARD";
name = nativeName;
}
if (xdata && dataInfos.contains(QString("CIQ '%1' -> %2 %3").arg(deveField.name.c_str()).arg("STANDARD").arg(nativeName))) {
int secs = last_time-start_time;
int idx = dataInfos.indexOf(QString("CIQ '%1' -> %2 %3").arg(deveField.name.c_str()).arg("STANDARD").arg(nativeName));
dataInfos.replace(idx, QString("CIQ '%1' -> %2 %3 (STANDARD until %4 secs)").arg(deveField.name.c_str()).arg(typeName).arg(name).arg(secs));
} else
dataInfos.append(QString("CIQ '%1' -> %2 %3").arg(deveField.name.c_str()).arg(typeName).arg(name));
}
if (xdata) {
deveXdata->valuename << name;
deveXdata->unitname << deveField.unit.c_str();
record_deve_fields.insert(key, deveXdata->valuename.count()-1);
} else
record_deve_fields.insert(key, -1);
}
void decodeFileId(const FitDefinition &def, int,
const std::vector<FitValue>& values) {
int i = 0;
int manu = -1, prod = -1;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if( value == NA_VALUE )
continue;
switch (field.num) {
case 1: manu = value; break;
case 2: prod = value; break;
// other are ignored at present:
case 0: // file type:
// 4: activity log
// 6: Itinary
// 34: segment
break;
case 3: //serial number
case 4: //timestamp
case 5: //number
default: ; // do nothing
}
}
rideFile->setDeviceType(getManuProd(manu, prod));
}
void decodeSession(const FitDefinition &def, int,
const std::vector<FitValue>& values) {
int i = 0;
QString WorkOutCode = NULL;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if( value == NA_VALUE )
continue;
switch (field.num) {
case 5: // sport field
switch (value) {
case 0: // Generic
rideFile->setTag("Sport","");
break;
case 1: // running:
rideFile->setTag("Sport","Run");
if (rideFile->dataPoints().count()>0)
convert2Run();
break;
case 2: // cycling
rideFile->setTag("Sport","Bike");
break;
case 3: // transition:
rideFile->setTag("Sport","Transition");
break;
case 4: // running:
rideFile->setTag("Sport","Fitness equipment");
break;
case 5: // swimming
rideFile->setTag("Sport","Swim");
break;
case 6: // Basketball:
rideFile->setTag("Sport","Basketball");
break;
case 7: //
rideFile->setTag("Sport","Soccer");
break;
case 8: // running:
rideFile->setTag("Sport","Tennis");
break;
case 9: // running:
rideFile->setTag("Sport","American fotball");
break;
case 10: // running:
rideFile->setTag("Sport","Training");
break;
case 11: // running:
rideFile->setTag("Sport","Walking");
break;
case 12: // running:
rideFile->setTag("Sport","Cross country skiing");
break;
case 13: // running:
rideFile->setTag("Sport","Alpine skiing");
break;
case 14: // running:
rideFile->setTag("Sport","Snowboarding");
break;
case 15: // running:
rideFile->setTag("Sport","Rowing");
break;
case 16: // running:
rideFile->setTag("Sport","Mountaineering");
break;
case 17: // running:
rideFile->setTag("Sport","Hiking");
break;
case 18: // running:
rideFile->setTag("Sport","Multisport");
break;
case 19: // running:
rideFile->setTag("Sport","Paddling");
break;
default: // if we can't work it out, assume bike
// but only if not already set to another sport,
// Garmin Swim send 2 tags for example
if (rideFile->getTag("Sport", "Bike") != "Bike") break;
}
break;
case 6: // sub sport (ignored at present)
switch (value) {
case 0: // generic
rideFile->setTag("SubSport","");
break;
case 1: // treadmill
rideFile->setTag("SubSport","treadmill");
break;
case 2: // street
rideFile->setTag("SubSport","street");
break;
case 3: // trail
rideFile->setTag("SubSport","trail");
break;
case 4: // track
rideFile->setTag("SubSport","track");
break;
case 5: // spin
rideFile->setTag("SubSport","spinning");
break;
case 6: // home trainer
rideFile->setTag("SubSport","home trainer");
break;
case 7: // route
rideFile->setTag("SubSport","route");
break;
case 8: // mountain
rideFile->setTag("SubSport","mountain");
break;
case 9: // downhill
rideFile->setTag("SubSport","downhill");
break;
case 10: // recumbent
rideFile->setTag("SubSport","recumbent");
break;
case 11: // cyclocross
rideFile->setTag("SubSport","cyclocross");
break;
case 12: // hand_cycling
rideFile->setTag("SubSport","hand cycling");
break;
case 13: // piste
rideFile->setTag("SubSport","piste");
break;
case 14: // indoor_rowing
rideFile->setTag("SubSport","indoor rowing");
break;
case 15: // elliptical
rideFile->setTag("SubSport","elliptical");
break;
case 16: // stair climbing
rideFile->setTag("SubSport","stair climbing");
break;
case 17: // lap swimming
rideFile->setTag("SubSport","lap swimming");
break;
case 18: // open water
rideFile->setTag("SubSport","open water");
break;
case 19: // flexibility training
rideFile->setTag("SubSport","flexibility training");
break;
case 20: // strength_training
rideFile->setTag("SubSport","strength_training");
break;
case 21: // warm_up
rideFile->setTag("SubSport","warm_up");
break;
case 22: // match
rideFile->setTag("SubSport","match");
break;
case 23: // exercise
rideFile->setTag("SubSport","exercise");
break;
case 24: // challenge
rideFile->setTag("SubSport","challenge");
break;
case 25: // indoor_skiing
rideFile->setTag("SubSport","indoor_skiing");
break;
case 26: // cardio_training
rideFile->setTag("SubSport","cardio_training");
break;
case 27: // indoor_walking
rideFile->setTag("SubSport","indoor_walking");
break;
case 28: // e_bike_fitness
rideFile->setTag("SubSport","e_bike_fitness");
break;
case 29: // bmx
rideFile->setTag("SubSport","bmx");
break;
case 30: // casual_walking
rideFile->setTag("SubSport","casual_walking");
break;
case 31: // speed_walking
rideFile->setTag("SubSport","speed_walking");
break;
case 32: // bike_to_run_transition
rideFile->setTag("SubSport","bike_to_run_transition");
break;
case 33: // run_to_bike_transition
rideFile->setTag("SubSport","run_to_bike_transition");
break;
case 34: // swim_to_bike_transition
rideFile->setTag("SubSport","swim_to_bike_transition");
break;
case 35: // atv
rideFile->setTag("SubSport","atv");
break;
case 36: // motocross
rideFile->setTag("SubSport","motocross");
break;
case 37: // backcountry
rideFile->setTag("SubSport","backcountry");
break;
case 38: // resort
rideFile->setTag("SubSport","resort");
break;
case 39: // rc_drone
rideFile->setTag("SubSport","rc_drone");
break;
case 40: // wingsuit
rideFile->setTag("SubSport","wingsuit");
break;
case 41: // whitewater
rideFile->setTag("SubSport","whitewater");
break;
case 254: // all
default:
break;
}
break;
case 44: // pool_length
pool_length = value / 100000.0;
rideFile->setTag("Pool Length", // in meters
QString("%1").arg(pool_length*1000.0));
break;
// other fields are ignored at present
case 253: //timestamp
case 254: //index
case 0: //event
case 1: /* event_type */
case 2: /* start_time */
case 3: /* start_position_lat */
case 4: /* start_position_long */
case 7: /* total elapsed time */
case 8: /* total timer time */
case 9: /* total distance */
case 10: /* total_cycles */
case 11: /* total calories */
case 13: /* total fat calories */
case 14: /* avg_speed */
case 15: /* max_speed */
case 16: /* avg_HR */
case 17: /* max_HR */
case 18: /* avg_cad */
case 19: /* max_cad */
case 20: /* avg_pwr */
case 21: /* max_pwr */
case 22: /* total ascent */
case 23: /* total descent */
case 25: /* first lap index */
case 26: /* num lap */
case 29: /* north-east lat = bounding box */
case 30: /* north-east lon = bounding box */
case 31: /* south west lat = bounding box */
case 32: /* south west lon = bounding box */
case 34: /* normalized power */
case 48: /* total work (J) */
case 49: /* avg altitude */
case 50: /* max altitude */
case 52: /* avg grade */
case 53: /* avg positive grade */
case 54: /* avg negative grade */
case 55: /* max pos grade */
case 56: /* max neg grade */
case 57: /* avg temperature (Celsius. deg) */
case 58: /* max temp */
case 59: /* total_moving_time */
case 60: /* avg_pos_vertical_speed (m/s) */
case 61: /* avg_neg_vertical_speed */
case 62: /* max_pos_vertical_speed */
case 63: /* max neg_vertical_speed */
case 64: /* min HR bpm */
case 69: /* avg lap time */
case 70: /* best lap index */
case 71: /* min altitude */
case 92: /* fractional avg cadence (rpm) */
case 93: /* fractional max cadence */
default: ; // do nothing
}
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf("decodeSession field %d: %d bytes, num %d, type %d\n", i, field.size, field.num, field.type );
}
}
rideFile->setTag("Workout Code",WorkOutCode);
}
void decodeDeviceInfo(const FitDefinition &def, int,
const std::vector<FitValue>& values) {
int i = 0;
int index=-1;
int manu = -1, prod = -1, version = -1, type = -1, serial = -1;
fit_string_value name;
QString deviceInfo;
foreach(const FitField &field, def.fields) {
FitValue value = values[i++];
//qDebug() << field.num << field.type << value.v;
switch (field.num) {
case 0: // device index
index = value.v;
break;
case 1: // ANT+ device type
type = value.v;
break;
// details: 0x78 = HRM, 0x79 = Spd&Cad, 0x7A = Cad, 0x7B = Speed
case 2: // manufacturer
manu = value.v;
break;
case 3: // serial number (can be ANT id)
serial = value.v;
break;
case 4: // product
prod = value.v;
break;
case 5: // software version
version = value.v;
break;
case 27: // product name
name = value.s;
break;
// all other fields are ignored at present
case 253: //timestamp
case 10: // battery voltage
case 6: // hardware version
case 11: // battery status
case 22: // ANT network
case 25: // source type
case 24: // equipment ID
default: ; // do nothing
}
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf("decodeDeviceInfo field %d: %d bytes, num %d, type %d\n", i, field.size, field.num, field.type );
}
//qDebug() << field.num << value.v;
}
//deviceInfo += QString("Device %1 ").arg(index);
deviceInfo += QString("%1 ").arg(getDeviceType(type));
if (manu>-1)
deviceInfo += getManuProd(manu, prod);
if (name.length()>0)
deviceInfo += QString(" %1").arg(name.c_str());
if (version>0)
deviceInfo += QString(" (v%1)").arg(version/100.0);
if (serial>0 && serial < 100000)
deviceInfo += QString(" ID:%1").arg(serial);
// What is 7 and 0 ?
// 3 for Moxy ?
if (type>-1 && type != 0 && type != 7 && type != 3)
deviceInfos.insert(index, deviceInfo);
}
void decodeActivity(const FitDefinition &def, int,
const std::vector<FitValue>& values) {
int i = 0;
const int delta = qbase_time.toTime_t();
int event = -1, event_type = -1, local_timestamp = -1, timestamp = -1;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if (value == NA_VALUE)
continue;
switch (field.num) {
case 3: // event
event = value;
break;
case 4: // event_type
event_type = value;
break;
case 5: // local_timestamp
// adjust from seconds since 1989-12-31 00:00:00 UTC
if (0 != value)
{
local_timestamp = value + delta;
}
break;
case 253: // timestamp
// adjust from seconds since 1989-12-31 00:00:00 UTC
if (0 != value)
{
timestamp = value + delta;
}
break;
case 1: // num_sessions
case 2: // type
default:
break;
}
//qDebug() << field.num << value;
}
if (event != 26) // activity
return;
if (event_type != 1) // stop
return;
if (local_timestamp < 0 || timestamp < 0)
return;
if (0 == local_timestamp && 0 == timestamp)
return;
QDateTime t(rideFile->startTime().toUTC());
if (0 == local_timestamp) {
// ZWift FIT files are not reporting local timestamp
rideFile->setStartTime(t.addSecs(timestamp));
} else {
// adjust start time to time zone of the ride
rideFile->setStartTime(t.addSecs(local_timestamp - timestamp));
}
}
void decodeEvent(const FitDefinition &def, int,
const std::vector<FitValue>& values) {
int time = -1;
int event = -1;
int event_type = -1;
qint16 data16 = -1;
qint32 data32 = -1;
int i = 0;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if( value == NA_VALUE )
continue;
switch (field.num) {
case 253: // timestamp field (s)
time = value + qbase_time.toTime_t();
break;
case 0: // event field
event = value; break;
case 1: // event_type field
event_type = value; break;
case 2: // data16 field
data16 = value; break;
case 3: //data32 field
data32 = value; break;
// additional values (ignored at present):
case 4: // event group
default: ; // do nothing
}
}
switch (event) {
case 0: // Timer event
{
switch (event_type) {
case 0: // start
stopped = false;
break;
case 1: // stop
stopped = true;
break;
case 2: // consecutive_depreciated
case 3: // marker
break;
case 4: // stop all
stopped = true;
break;
case 5: // begin_depreciated
case 6: // end_depreciated
case 7: // end_all_depreciated
case 8: // stop_disable
stopped = true;
break;
case 9: // stop_disable_all
stopped = true;
break;
default:
errors << QString("Unknown timer event type %1").arg(event_type);
}
}
break;
case 36: // Calibration event
{
int secs = (start_time==0?0:time-start_time);
switch (event_type) {
case 3: // marker
++calibration;
rideFile->addCalibration(secs, data16, QString("Calibration %1 (%2)").arg(calibration).arg(data16));
//qDebug() << "marker" << secs << data16;
break;
default:
errors << QString("Unknown calibration event type %1").arg(event_type);
break;
}
}
break;
case 42: /* front_gear_change */
case 43: /* rear_gear_change */
{
int secs = (start_time==0?0:time-start_time);
XDataPoint *p = new XDataPoint();
switch (event_type) {
case 3:
p->secs = secs;
p->km = last_distance;
p->number[0] = ((data32 >> 24) & 255);
p->number[1] = ((data32 >> 8) & 255);
p->number[2] = ((data32 >> 16) & 255);
p->number[3] = (data32 & 255);
gearsXdata->datapoints.append(p);
break;
default:
errors << QString("Unknown gear change event %1 type %2 data %3").arg(event).arg(event_type).arg(data32);
break;
}
}
break;
case 3: /* workout */
case 4: /* workout_step */
case 5: /* power_down */
case 6: /* power_up */
case 7: /* off_course */
case 8: /* session */
case 9: /* lap */
case 10: /* course_point */
case 11: /* battery */
case 12: /* virtual_partner_pace */
case 13: /* hr_high_alert */
case 14: /* hr_low_alert */
case 15: /* speed_high_alert */
case 16: /* speed_low_alert */
case 17: /* cad_high_alert */
case 18: /* cad_low_alert */
case 19: /* power_high_alert */
case 20: /* power_low_alert */
case 21: /* recovery_hr */
case 22: /* battery_low */
case 23: /* time_duration_alert */
case 24: /* distance_duration_alert */
case 25: /* calorie_duration_alert */
case 26: /* activity */
case 27: /* fitness_equipment */
case 28: /* length */
case 32: /* user_marker */
case 33: /* sport_point */
default: ;
}
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf("event type %d\n", event_type);
}
last_event = event;
last_event_type = event_type;
}
void decodeHRV(const FitDefinition &def,
const std::vector<FitValue>& values) {
int rrvalue;
int i=0;
double hrv_time=0.0;
int n=hrvXdata->datapoints.count();
if (n>0)
hrv_time = hrvXdata->datapoints[n-1]->secs;
foreach(const FitField &field, def.fields) {
FitValue value = values[i++];
if ( value.type == ListValue && field.num == 0){
for (int j=0; j<value.list.size(); j++)
{
rrvalue = int(value.list.at(j));
hrv_time += rrvalue/1000.0;
if (rrvalue == -1){
break;
}
XDataPoint *p = new XDataPoint();
p->secs = hrv_time;
p->number[0] = rrvalue;
hrvXdata->datapoints.append(p);
}
}
}
}
void decodeLap(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
else
time = last_time;
int i = 0;
time_t this_start_time = 0;
double total_distance = 0.0;
QString lap_name;
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf( " FIT decode lap \n");
}
foreach(const FitField &field, def.fields) {
const FitValue& value = values[i++];
if( value.v == NA_VALUE )
continue;
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf ("\tfield: num: %d ", field.num);
DumpFitValue(value);
}
// ignore any developer fields in laps
if ( field.deve_idx > -1)
continue;
switch (field.num) {
case 253:
time = value.v + qbase_time.toTime_t();
break;
case 2:
this_start_time = value.v + qbase_time.toTime_t();
break;
case 9:
total_distance = value.v / 100000.0;
break;
case 24:
//lap_trigger = value.v;
// other data (ignored at present):
case 254: // lap nbr
case 3: // start_position_lat
case 4: // start_position_lon
case 5: // end_position_lat
case 6: // end_position_lon
case 7: // total_elapsed_time = value.v / 1000.0;
case 8: // total_timer_time
case 10: // total_cycles
case 11: // total calories
case 12: // total fat calories
case 13: // avg_speed
case 14: // max_speed
case 15: // avg HR (bpm)
case 16: // Max HR
case 17: // AvCad
case 18: // MaxCad
case 21: // total ascent
case 22: // total descent
case 27: // north-east lat (bounding box)
case 28: // north-east lon
case 29: // south west lat
case 30: // south west lon
break;
default: ; // ignore it
}
}
if (this_start_time == 0 || this_start_time-start_time < 0) {
//errors << QString("lap %1 has invalid start time").arg(interval);
this_start_time = start_time; // time was corrected after lap start
if (time == 0 || time-start_time < 0) {
errors << QString("lap %1 is ignored (invalid end time)").arg(interval);
return;
}
}
if (isLapSwim) {
// Fill empty laps due to false starts or pauses in some devices
// s.t. Garmin 910xt
double secs = time - start_time;
if ((total_distance == 0.0) && (secs > last_time + 1) &&
//(isGarminSmartRecording.toInt() != 0) && // always do this for swim laps
(secs - last_time < 100*GarminHWM.toInt())) {
double deltaSecs = secs - last_time;
for (int i = 1; i <= deltaSecs; i++) {
rideFile->appendPoint(
last_time+i, 0.0, 0.0,
last_distance,
0.0, 0.0, 0.0, 0.0, 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, interval);
}
last_time += deltaSecs;
}
++interval;
} else if (rideFile->dataPoints().count()) { // no samples means no laps
++interval;
if (lap_name == "") {
lap_name = QObject::tr("Lap %1").arg(interval);
}
rideFile->addInterval(RideFileInterval::DEVICE,
this_start_time - start_time,
time - start_time,
lap_name);
}
}
void decodeRecord(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
if (isLapSwim) return; // We use the length message for Lap Swimming
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
double alt = 0, cad = 0, km = 0, hr = 0, lat = 0, lng = 0, badgps = 0, lrbalance = RideFile::NA;
double kph = 0, temperature = RideFile::NA, watts = 0, slope = 0, headwind = 0;
double leftTorqueEff = 0, rightTorqueEff = 0, leftPedalSmooth = 0, rightPedalSmooth = 0;
double leftPedalCenterOffset = 0;
double rightPedalCenterOffset = 0;
double leftTopDeathCenter = 0;
double rightTopDeathCenter = 0;
double leftBottomDeathCenter = 0;
double rightBottomDeathCenter = 0;
double leftTopPeakPowerPhase = 0;
double rightTopPeakPowerPhase = 0;
double leftBottomPeakPowerPhase = 0;
double rightBottomPeakPowerPhase = 0;
double rvert = 0, rcad = 0, rcontact = 0;
double smO2 = 0, tHb = 0;
//bool run=false;
XDataPoint *p_deve = NULL;
XDataPoint *p_extra = NULL;
fit_value_t lati = NA_VALUE, lngi = NA_VALUE;
int i = 0;
foreach(const FitField &field, def.fields) {
FitValue _values = values[i];
fit_value_t value = values[i].v;
QList<fit_value_t> valueList = values[i++].list;
double deve_value = 0.0;
if( value == NA_VALUE )
continue;
int native_num = field.num;
bool native_profile = true;
if (field.deve_idx>-1) {
QString key = QString("%1.%2").arg(field.deve_idx).arg(field.num);
//qDebug() << "deve_idx" << field.deve_idx << "num" << field.num << "type" << field.type;
//qDebug() << "name" << local_deve_fields[key].name.c_str() << "unit" << local_deve_fields[key].unit.c_str() << local_deve_fields[key].offset << "(" << _values.v << _values.f << ")";
if (record_deve_native_fields.contains(key) && !record_native_fields.contains(record_deve_native_fields[key])) {
native_num = record_deve_native_fields[key];
int scale = local_deve_fields[key].scale;
if (scale == -1)
scale = 1;
int offset = local_deve_fields[key].offset;
if (offset == -1)
offset = 0;
switch (_values.type) {
case SingleValue: deve_value=_values.v/(float)scale+offset; break;
case FloatValue: deve_value=_values.f/(float)scale+offset; break;
default: deve_value = 0.0; break;
}
// For compatibility with old Moxy deve Fields (with the native profile)
if (_values.type == SingleValue && (native_num == 54 || native_num == 57) )
native_profile = true;
else
native_profile = false;// Now Dynastream decided to use float values for CIQ
//qDebug() << "deve_value" << deve_value << native_profile;
}
else
native_num = -1;
//qDebug()<< "native_num"<<native_num << time;
} else {
//qDebug()<< "native_num"<<native_num;
if (!record_native_fields.contains(native_num)) {
record_native_fields.insert(native_num);
}
}
if (native_num>-1) {
switch (native_num) {
case 253: // TIMESTAMP
time = value + qbase_time.toTime_t();
// Time MUST NOT go backwards
// You canny break the laws of physics, Jim
if (time < last_time)
time = last_time; // Not true for Bryton
break;
case 0: // POSITION_LAT
lati = value;
break;
case 1: // POSITION_LONG
lngi = value;
break;
case 2: // ALTITUDE
if (!native_profile && field.deve_idx>-1)
alt = deve_value;
else
alt = value / 5.0 - 500.0;
break;
case 3: // HEART_RATE
hr = value;
break;
case 4: // CADENCE
if (rideFile->getTag("Sport", "Bike") == "Run")
rcad = value;
else
cad = value;
break;
case 5: // DISTANCE
km = value / 100000.0;
break;
case 6: // SPEED
kph = value * 3.6 / 1000.0;
break;
case 7: // POWER
watts = value;
break;
case 8: break; // packed speed/dist
case 9: // GRADE
slope = value / 100.0;
break;
case 10: //resistance = value;
break;
case 11: //time_from_course = value / 1000.0;
break;
case 12: break; // "cycle_length"
case 13: // TEMPERATURE
temperature = value;
break;
case 29: // ACCUMULATED_POWER
break;
case 30: //LEFT_RIGHT_BALANCE
lrbalance = (value & 0x80 ? 100 - (value & 0x7F) : value & 0x7F);
break;
case 31: // GPS Accuracy
break;
case 39: // VERTICAL OSCILLATION
if (!native_profile && field.deve_idx>-1)
rvert = deve_value;
else
rvert = value / 100.0f;
break;
//case 40: // GROUND CONTACT TIME PERCENT
//break;
case 41: // GROUND CONTACT TIME
if (!native_profile && field.deve_idx>-1)
rcontact = deve_value;
else
rcontact = value / 10.0f;
break;
//case 42: // ACTIVITY_TYPE
// // TODO We should know/test value for run
// run = true;
// break;
case 43: // LEFT_TORQUE_EFFECTIVENESS
leftTorqueEff = value / 2.0;
break;
case 44: // RIGHT_TORQUE_EFFECTIVENESS
rightTorqueEff = value / 2.0;
break;
case 45: // LEFT_PEDAL_SMOOTHNESS
leftPedalSmooth = value / 2.0;
break;
case 46: // RIGHT_PEDAL_SMOOTHNESS
rightPedalSmooth = value / 2.0;
break;
case 47: // COMBINED_PEDAL_SMOOTHNES
//qDebug() << "COMBINED_PEDAL_SMOOTHNES" << value;
// --> XDATA
native_num = -1;
break;
case 53: // RUNNING CADENCE FRACTIONAL VALUE
if (rideFile->getTag("Sport", "Bike") == "Run")
rcad += value/128.0f;
else
cad += value/128.0f;
break;
case 54: // tHb
if (!native_profile && field.deve_idx>-1) {
tHb = deve_value;
}
else
tHb= value/100.0f;
break;
case 57: // SMO2
if (!native_profile && field.deve_idx>-1) {
smO2 = deve_value;
}
else
smO2= value/10.0f;
break;
case 61: // ? GPS Altitude ? or atmospheric pressure ?
break;
case 66: // ??
break;
case 67: // ? Left Platform Center Offset ?
leftPedalCenterOffset = value;
break;
case 68: // ? Right Platform Center Offset ?
rightPedalCenterOffset = value;
break;
case 69: // ? Left Power Phase ?
leftTopDeathCenter = round(valueList.at(0) * 360.0/256);
leftBottomDeathCenter = round(valueList.at(1) * 360.0/256);
break;
case 70: // ? Left Peak Phase ?
leftTopPeakPowerPhase = round(valueList.at(0) * 360.0/256);
leftBottomPeakPowerPhase = round(valueList.at(1) * 360.0/256);
break;
case 71: // ? Right Power Phase ?
rightTopDeathCenter = round(valueList.at(0) * 360.0/256);
rightBottomDeathCenter = round(valueList.at(1) * 360.0/256);
break;
case 72: // ? Right Peak Phase ?
rightTopPeakPowerPhase = round(valueList.at(0) * 360.0/256);
rightBottomPeakPowerPhase = round(valueList.at(1) * 360.0/256);
break;
case 84: // Left right balance
lrbalance = value/100.0;
break;
case 87: // ???
break;
default:
unknown_record_fields.insert(native_num);
native_num = -1;
}
}
if (native_num == -1) {
// native, deve_native or deve to record.
int idx = -1;
if (field.deve_idx>-1) {
QString key = QString("%1.%2").arg(field.deve_idx).arg(field.num);
FitDeveField deveField = local_deve_fields[key];
if (!record_deve_fields.contains(key)) {
addRecordDeveField(key, deveField, true);
} else {
if (record_deve_fields[key] == -1) {
addRecordDeveField(key, deveField, true);
}
}
idx = record_deve_fields[key];
if (idx>-1) {
if (p_deve == NULL &&
(_values.type == SingleValue ||
_values.type == FloatValue ||
_values.type == StringValue))
p_deve = new XDataPoint();
int scale = deveField.scale;
if (scale == -1)
scale = 1;
int offset = deveField.offset;
if (offset == -1)
offset = 0;
switch (_values.type) {
case SingleValue: p_deve->number[idx]=_values.v/(float)scale+offset; break;
case FloatValue: p_deve->number[idx]=_values.f/(float)scale+offset; break;
case StringValue: p_deve->string[idx]=_values.s.c_str(); break;
default: break;
}
}
} else {
// Store standard native ignored
if (!record_extra_fields.contains(field.num)) {
RideFile::SeriesType series = getSeriesForNative(field.num);
QString nativeName = rideFile->symbolForSeries(series);
if (nativeName.length() == 0)
nativeName = getNameForExtraNative(field.num);
extraXdata->valuename << nativeName;
extraXdata->unitname << "";
//dataInfos.append(QString("EXTRA %1").arg(nativeName));
record_extra_fields.insert(field.num, record_extra_fields.count());
}
idx = record_extra_fields[field.num];
if (idx>-1) {
float scale = getScaleForExtraNative(field.num);
int offset = getOffsetForExtraNative(field.num);
if (p_extra == NULL &&
(_values.type == SingleValue ||
_values.type == FloatValue ||
_values.type == StringValue))
p_extra = new XDataPoint();
switch (_values.type) {
case SingleValue: p_extra->number[idx]=_values.v/scale+offset; break;
case FloatValue: p_extra->number[idx]=_values.f/scale+offset; break;
case StringValue: p_extra->string[idx]=_values.s.c_str(); break;
default: break;
}
}
}
} else {
if (field.deve_idx>-1) {
QString key = QString("%1.%2").arg(field.deve_idx).arg(field.num);
FitDeveField deveField = local_deve_fields[key];
if (!record_deve_fields.contains(key)) {
addRecordDeveField(key, deveField, false);
}
}
}
}
if (time == last_time)
return; // Not true for Bryton
if (stopped) {
// As it turns out, this happens all the time in some FIT files.
// Since we don't really understand the meaning, don't make noise.
/*
errors << QString("At %1 seconds, time is stopped, but got record "
"anyway. Ignoring it. Last event type was "
"%2 for event %3.").arg(time-start_time).arg(last_event_type).arg(last_event);
return;
*/
}
if (lati != NA_VALUE && lngi != NA_VALUE) {
lat = lati * 180.0 / 0x7fffffff;
lng = lngi * 180.0 / 0x7fffffff;
} else
{
// If lat/lng are missng, set to 0/0 and fill point from last point as 0/0)
lat = 0;
lng = 0;
badgps = 1;
}
if (start_time == 0) {
start_time = time - 1; // recording interval?
QDateTime t;
t.setTime_t(start_time);
rideFile->setStartTime(t);
}
//printf( "point time=%d lat=%.2lf lon=%.2lf alt=%.1lf hr=%.0lf "
// "cad=%.0lf km=%.1lf kph=%.1lf watts=%.0lf grade=%.1lf "
// "resist=%.1lf off=%.1lf temp=%.1lf\n",
// time, lat, lng, alt, hr,
// cad, km, kph, watts, grade,
// resistance, time_from_course, temperature );
double secs = time - start_time;
double nm = 0;
int interval = 0;
// if there are data points && a time difference > 1sec && smartRecording processing is requested at all
if ((!rideFile->dataPoints().empty()) && (last_time != 0) &&
(time > last_time + 1) && (isGarminSmartRecording.toInt() != 0)) {
// Handle smart recording if configured in preferences. Linearly interpolate missing points.
RideFilePoint *prevPoint = rideFile->dataPoints().back();
double deltaSecs = (secs - prevPoint->secs);
//assert(deltaSecs == secs - prevPoint->secs); // no fractional part -- don't CRASH FFS, be graceful
// This is only true if the previous record was of type record:
//assert(deltaSecs == time - last_time); -- don't CRASH FFS, be graceful
// If the last lat/lng was missing (0/0) then all points up to lat/lng are marked as 0/0.
if (prevPoint->lat == 0 && prevPoint->lon == 0 ) {
badgps = 1;
}
double deltaCad = cad - prevPoint->cad;
double deltaHr = hr - prevPoint->hr;
double deltaDist = km - prevPoint->km;
if (km < 0.00001) deltaDist = 0.000f; // effectively zero distance
double deltaSpeed = kph - prevPoint->kph;
double deltaTorque = nm - prevPoint->nm;
double deltaPower = watts - prevPoint->watts;
double deltaAlt = alt - prevPoint->alt;
double deltaLon = lng - prevPoint->lon;
double deltaLat = lat - prevPoint->lat;
// double deltaHeadwind = headwind - prevPoint->headwind;
double deltaSlope = slope - prevPoint->slope;
double deltaLeftRightBalance = lrbalance - prevPoint->lrbalance;
double deltaLeftTE = leftTorqueEff - prevPoint->lte;
double deltaRightTE = rightTorqueEff - prevPoint->rte;
double deltaLeftPS = leftPedalSmooth - prevPoint->lps;
double deltaRightPS = rightPedalSmooth - prevPoint->rps;
double deltaLeftPedalCenterOffset = leftPedalCenterOffset - prevPoint->lpco;
double deltaRightPedalCenterOffset = rightPedalCenterOffset - prevPoint->rpco;
double deltaLeftTopDeathCenter = leftTopDeathCenter - prevPoint->lppb;
double deltaRightTopDeathCenter = rightTopDeathCenter - prevPoint->rppb;
double deltaLeftBottomDeathCenter = leftBottomDeathCenter - prevPoint->lppe;
double deltaRightBottomDeathCenter = rightBottomDeathCenter - prevPoint->rppe;
double deltaLeftTopPeakPowerPhase = leftTopPeakPowerPhase - prevPoint->lpppb;
double deltaRightTopPeakPowerPhase = rightTopPeakPowerPhase - prevPoint->rpppb;
double deltaLeftBottomPeakPowerPhase = leftBottomPeakPowerPhase - prevPoint->lpppe;
double deltaRightBottomPeakPowerPhase = rightBottomPeakPowerPhase - prevPoint->rpppe;
double deltaSmO2 = smO2 - prevPoint->smo2;
double deltaTHb = tHb - prevPoint->thb;
double deltarvert = rvert - prevPoint->rvert;
double deltarcad = rcad - prevPoint->rcad;
double deltarcontact = rcontact - prevPoint->rcontact;
// only smooth the maximal smart recording gap defined in preferences - we don't want to crash / stall on bad
// or corrupt files
if (deltaSecs > 0 && deltaSecs < GarminHWM.toInt()) {
for (int i = 1; i < deltaSecs; i++) {
double weight = i /deltaSecs;
rideFile->appendPoint(
prevPoint->secs + (deltaSecs * weight),
prevPoint->cad + (deltaCad * weight),
prevPoint->hr + (deltaHr * weight),
prevPoint->km + (deltaDist * weight),
prevPoint->kph + (deltaSpeed * weight),
prevPoint->nm + (deltaTorque * weight),
prevPoint->watts + (deltaPower * weight),
prevPoint->alt + (deltaAlt * weight),
(badgps == 1) ? 0 : prevPoint->lon + (deltaLon * weight),
(badgps == 1) ? 0 : prevPoint->lat + (deltaLat * weight),
0.0, // headwind
prevPoint->slope + (deltaSlope * weight),
temperature,
prevPoint->lrbalance + (deltaLeftRightBalance * weight),
prevPoint->lte + (deltaLeftTE * weight),
prevPoint->rte + (deltaRightTE * weight),
prevPoint->lps + (deltaLeftPS * weight),
prevPoint->rps + (deltaRightPS * weight),
prevPoint->lpco + (deltaLeftPedalCenterOffset * weight),
prevPoint->rpco + (deltaRightPedalCenterOffset * weight),
prevPoint->lppb + (deltaLeftTopDeathCenter * weight),
prevPoint->rppb + (deltaRightTopDeathCenter * weight),
prevPoint->lppe + (deltaLeftBottomDeathCenter * weight),
prevPoint->rppe + (deltaRightBottomDeathCenter * weight),
prevPoint->lpppb + (deltaLeftTopPeakPowerPhase * weight),
prevPoint->rpppb + (deltaRightTopPeakPowerPhase * weight),
prevPoint->lpppe + (deltaLeftBottomPeakPowerPhase * weight),
prevPoint->rpppe + (deltaRightBottomPeakPowerPhase * weight),
prevPoint->smo2 + (deltaSmO2 * weight),
prevPoint->thb + (deltaTHb * weight),
prevPoint->rvert + (deltarvert * weight),
prevPoint->rcad + (deltarcad * weight),
prevPoint->rcontact + (deltarcontact * weight),
0.0, // tcore
interval);
}
}
}
if (km < 0.00001f) km = last_distance;
rideFile->appendOrUpdatePoint(secs, cad, hr, km, kph, nm, watts, alt, lng, lat, headwind, slope, temperature,
lrbalance, leftTorqueEff, rightTorqueEff, leftPedalSmooth, rightPedalSmooth,
leftPedalCenterOffset, rightPedalCenterOffset,
leftTopDeathCenter, rightTopDeathCenter, leftBottomDeathCenter, rightBottomDeathCenter,
leftTopPeakPowerPhase, rightTopPeakPowerPhase, leftBottomPeakPowerPhase, rightBottomPeakPowerPhase,
smO2, tHb, rvert, rcad, rcontact, 0.0, interval, false);
last_time = time;
last_distance = km;
if (p_deve != NULL) {
p_deve->secs = secs;
deveXdata->datapoints.append(p_deve);
}
if (p_extra != NULL) {
p_extra->secs = secs;
extraXdata->datapoints.append(p_extra);
}
}
void decodeLength(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
if (!isLapSwim) {
isLapSwim = true;
// reset rideFile if not empty
if (!rideFile->dataPoints().empty()) {
start_time = 0;
last_time = 0;
last_distance = 0.00f;
interval = 1;
QString deviceType = rideFile->deviceType();
QString fileFormat = rideFile->fileFormat();
delete rideFile;
rideFile = new RideFile;
rideFile->setDeviceType(deviceType);
rideFile->setFileFormat(fileFormat);
rideFile->setRecIntSecs(1.0);
}
}
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
double cad = 0, km = 0, kph = 0;
int length_type = 0;
int swim_stroke = 0;
int total_strokes = 0;
double length_duration = 0.0;
int i = 0;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if( value == NA_VALUE )
continue;
switch (field.num) {
case 0: // event
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) qDebug() << " event:" << value;
break;
case 1: // event type
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) qDebug() << " event_type:" << value;
break;
case 2: // start time
time = value + qbase_time.toTime_t();
// Time MUST NOT go backwards
// You canny break the laws of physics, Jim
if (time < last_time)
time = last_time; // Not true for Bryton
break;
case 3: // total elapsed time
length_duration = value / 1000.0;
break;
case 4: // total timer time
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) qDebug() << " total_timer_time:" << value;
break;
case 5: // total strokes
total_strokes = value;
break;
case 6: // avg speed
kph = value * 3.6 / 1000.0;
break;
case 7: // swim stroke: 0-free, 1-back, 2-breast, 3-fly,
// 4-drill, 5-mixed, 6-IM
swim_stroke = value;
break;
case 9: // cadence
cad = value;
break;
case 11: // total_calories
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) qDebug() << " total_calories:" << value;
break;
case 12: // length type: 0-rest, 1-strokes
length_type = value;
break;
case 254: // message_index
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) qDebug() << " message_index:" << value;
break;
default:
unknown_record_fields.insert(field.num);
}
}
XDataPoint *p = new XDataPoint();
p->secs = last_time;
p->km = last_distance;
p->number[0] = length_type + swim_stroke;
p->number[1] = length_duration;
p->number[2] = total_strokes;
swimXdata->datapoints.append(p);
// Rest interval
if (!length_type) {
kph = 0.0;
cad = 0.0;
}
if (time == last_time)
return; // Sketchy, but some FIT files do this.
if (start_time == 0) {
start_time = time - 1; // recording interval?
QDateTime t;
t.setTime_t(start_time);
rideFile->setStartTime(t);
interval = 1;
}
// Normalize distance for the most common pool lengths,
// this is a hack to avoid the need for a double pass when
// pool_length comes in Session message at the end of the file.
if (pool_length == 0.0) {
pool_length = kph*length_duration/3600;
if (fabs(pool_length - 0.050) < 0.004) pool_length = 0.050;
else if (fabs(pool_length - 0.033) < 0.003) pool_length = 0.033;
else if (fabs(pool_length - 0.025) < 0.002) pool_length = 0.025;
else if (fabs(pool_length - 0.025*METERS_PER_YARD) < 0.002) pool_length = 0.025*METERS_PER_YARD;
else if (fabs(pool_length - 0.020) < 0.002) pool_length = 0.020;
}
// another pool length or pause
km = last_distance + (length_type ? pool_length : 0.0);
// Adjust length duration using fractional carry
length_duration += frac_time;
frac_time = modf(length_duration, &length_duration);
// only fill 100x the maximal smart recording gap defined
// in preferences - we don't want to crash / stall on bad
// or corrupt files
if ((isGarminSmartRecording.toInt() != 0) && length_duration > 0 && length_duration < 100*GarminHWM.toInt()) {
double deltaSecs = length_duration;
double deltaDist = km - last_distance;
kph = 3600.0 * deltaDist / deltaSecs;
for (int i = 1; i <= deltaSecs; i++) {
rideFile->appendPoint(
last_time + i, cad, 0.0,
last_distance + (deltaDist * i/deltaSecs),
kph, 0.0, 0.0, 0.0, 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,
interval);
}
last_time += deltaSecs;
last_distance += deltaDist;
}
}
/* weather broadcast as observed at weather station (undocumented) */
void decodeWeather(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
Q_UNUSED(time_offset);
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
double windHeading = 0.0, windSpeed = 0.0, temp = 0.0, humidity = 0.0;
int i = 0;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if( value == NA_VALUE )
continue;
switch (field.num) {
case 253: // Timestamp
time = value + qbase_time.toTime_t();
break;
case 8: // Weather station name
// ignored
break;
case 9: // Weather observation timestamp
// ignored
break;
case 10: // Weather station latitude
// ignored
break;
case 11: // Weather station longitude
// ignored
break;
case 3: // Wind heading (0deg=North)
windHeading = value ; // 180.0 * MATHCONST_PI;
break;
case 4: // Wind speed (mm/s)
windSpeed = value * 0.0036; // km/h
break;
case 1: // Temperature
temp = value;
break;
case 7: // Humidity
humidity = value;
break;
default: ; // ignore it
}
}
double secs = time - start_time;
XDataPoint *p = new XDataPoint();
p->secs = secs;
p->km = last_distance;
p->number[0] = windSpeed;
p->number[1] = windHeading;
p->number[2] = temp;
p->number[3] = humidity;
weatherXdata->datapoints.append(p);
}
void decodeHr(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
time_t time = 0;
if (time_offset > 0) {
time = last_time + time_offset;
}
QList<double> timestamps;
QList<double> hr;
int a = 0;
int j = 0;
foreach(const FitField &field, def.fields) {
FitValue value = values[a++];
if( value.type == SingleValue && value.v == NA_VALUE )
continue;
switch (field.num) {
case 253: // Timestamp
time = value.v + qbase_time.toTime_t();
break;
case 0: // fractional_timestamp
break;
case 1: // time256
break;
case 6: // filtered_bpm
if (value.type == SingleValue) {
hr.append(value.v);
}
else {
for (int i=0;i<value.list.size();i++) {
hr.append(value.list.at(i));
}
}
break;
case 9: // event_timestamp
last_event_timestamp = value.v;
start_timestamp = time-last_event_timestamp/1024.0;
timestamps.append(last_event_timestamp/1024.0);
break;
case 10: // event_timestamp_12
j=0;
for (int i=0;i<value.list.size()-1;i++) {
qint16 last_event_timestamp12 = last_event_timestamp & 0xFFF;
qint16 next_event_timestamp12;
if (j%2 == 0) {
next_event_timestamp12 = value.list.at(i) + ((value.list.at(i+1) & 0xF) << 8);
last_event_timestamp = (last_event_timestamp & 0xFFFFF000) + next_event_timestamp12;
} else {
next_event_timestamp12 = 16 * value.list.at(i+1) + ((value.list.at(i) & 0xF0) >> 4);
last_event_timestamp = (last_event_timestamp & 0xFFFFF000) + next_event_timestamp12;
i++;
}
if (next_event_timestamp12 < last_event_timestamp12)
last_event_timestamp += 0x1000;
timestamps.append(last_event_timestamp/1024.0);
j++;
}
break;
default: ; // ignore it
}
}
for (int i=0;i<timestamps.count(); i++) {
double secs = round(timestamps.at(i) + start_timestamp - start_time);
if (secs>0) {
int idx = rideFile->timeIndex(round(secs));
if (idx < 0 || rideFile->dataPoints().at(idx)->secs==secs)
rideFile->appendOrUpdatePoint(
secs, 0.0, hr.at(i),
0.0,
0.0, 0.0, 0.0, 0.0, 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, false);
}
}
}
void decodeDeviceSettings(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
Q_UNUSED(time_offset);
int i = 0;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++].v;
if( value == NA_VALUE )
continue;
switch (field.num) {
case 0: // Active timezone
// ignored
break;
case 1: // UTC offset
// ignored
break;
case 5: // timezone offset
// ignored
break;
default: ; // ignore it
}
}
}
void decodeSegment(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
else
time = last_time;
int i = 0;
time_t this_start_time = 0;
++interval;
double total_elapsed_time = 0.0;
QString segment_name;
bool fail = false;
foreach(const FitField &field, def.fields) {
const FitValue& value = values[i++];
if( value.type != StringValue && value.v == NA_VALUE )
continue;
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf ("\tfield: num: %d ", field.num);
DumpFitValue(value);
}
switch (field.num) {
case 253: // Message timestamp
time = value.v + qbase_time.toTime_t();
break;
case 2: // start timestamp ?
this_start_time = value.v + qbase_time.toTime_t();
break;
case 3: // start latitude
// ignored
break;
case 4: // start longitude
// ignored
break;
case 5: // end latitude
// ignored
break;
case 6: // end longitude
// ignored
break;
case 7: // personal best (ms) ? segment elapsed time from this activity (ms) ?
// => depends on file / device / version ?
// FIXME: to be investigated/confirmed.
total_elapsed_time = round(value.v / 1000.0);
break;
case 8: // challenger best (ms) ? segment total timer time from this activity (ms) ?
// => depends on file / device / version ?
// FIXME: to be investigated/confirmed.
// ignored
break;
case 9: // leader best (ms) ? segment distance ? FIXME : to be investigated.
// => depends on file / device / version ?
//not used XXX total_distance = value.v / 100000.0;
break;
case 10: // personal rank ? to be confirmed
// ignored
break;
case 25: // north-east latitude (bounding box)
// ignored
break;
case 26: // north-east longitude
// ignored
break;
case 27: // south-west latitude
// ignored
break;
case 28: // south-west longitude
// ignored
break;
case 29: // Segment name
segment_name = QString(value.s.c_str());
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf("Found segment name: %s\n", segment_name.toStdString().c_str());
}
break;
case 64: // status
fail = (value.v == 1);
break;
case 33: /* undocumented, ignored */ break;
case 71: /* undocumented, ignored */ break;
case 75: /* undocumented, ignored */ break;
case 76: /* undocumented, ignored */ break;
case 77: /* undocumented, ignored */ break;
case 78: /* undocumented, ignored */ break;
case 79: /* undocumented, ignored */ break;
case 80: /* undocumented, ignored */ break;
case 254: /* message counter idx, ignored */ break;
case 11: /* undocumented, ignored */ break;
case 12: /* undocumented, ignored */ break;
case 13: /* undocumented, ignored */ break;
case 14: /* undocumented, ignored */ break;
case 19: /* undocumented, ignored */ break;
case 20: /* undocumented, ignored */ break;
case 21: /* total ascent ? ignored */ break;
case 22: /* total descent ? ignored */ break;
case 30: /* undocumented, ignored */ break;
case 31: /* undocumented, ignored */ break;
case 69: /* undocumented, ignored */ break;
case 70: /* undocumented, ignored */ break;
case 72: /* undocumented, ignored */ break;
case 0: /* undocumented, ignored */ break;
case 1: /* undocumented, ignored */ break;
case 15: /* undocumented (HR?), ignored */ break;
case 16: /* undocumented (HR?), ignored */ break;
case 17: /* undocumented (cadence?), ignored */ break;
case 18: /* undocumented (cadence?), ignored */ break;
case 23: /* undocumented, ignored */ break;
case 24: /* undocumented, ignored */ break;
case 32: /* undocumented, ignored */ break;
case 58: /* undocumented, ignored */ break;
case 59: /* undocumented, ignored */ break;
case 60: /* undocumented, ignored */ break;
case 61: /* undocumented, ignored */ break;
case 62: /* undocumented, ignored */ break;
case 63: /* undocumented, ignored */ break;
case 65: // Segment UID
// ignored
break;
case 66: /* undocumented, ignored */ break;
case 67: /* undocumented, ignored */ break;
case 68: /* undocumented, ignored */ break;
case 73: /* undocumented, ignored */ break;
case 74: /* undocumented, ignored */ break;
case 81: /* undocumented, ignored */ break;
case 82: /* undocumented, ignored */ break;
default: ; // ignore it
}
}
if (fail) { // Segment started but not ended
// no interval
return;
}
if (this_start_time == 0 || this_start_time-start_time < 0) {
//errors << QString("lap %1 has invalid start time").arg(interval);
this_start_time = start_time; // time was corrected after lap start
if (time == 0 || time-start_time < 0) {
errors << QString("lap %1 is ignored (invalid end time)").arg(interval);
return;
}
}
if (rideFile->dataPoints().count()) { // no samples means no laps..
if (segment_name == "") {
segment_name = QObject::tr("Lap %1").arg(interval);
}
if (isLapSwim && total_elapsed_time > 0.0) {
rideFile->addInterval(RideFileInterval::DEVICE, this_start_time - start_time,
this_start_time - start_time + total_elapsed_time, segment_name);
} else {
rideFile->addInterval(RideFileInterval::DEVICE, this_start_time - start_time, time - start_time,
segment_name);
}
}
}
void decodeDeveloperFieldDescription(const FitDefinition &def, int time_offset,
const std::vector<FitValue>& values) {
Q_UNUSED(time_offset);
int i = 0;
FitDeveField fieldDef;
fieldDef.scale = -1;
fieldDef.offset = -1;
fieldDef.native = -1;
foreach(const FitField &field, def.fields) {
FitValue value = values[i++];
//qDebug() << "deve : num" << field.num << value.v << value.s.c_str();
switch (field.num) {
case 0: // developer_data_index
fieldDef.dev_id = value.v;
break;
case 1: // field_definition_number
fieldDef.num = value.v;
break;
case 2: // fit_base_type_id
fieldDef.type = value.v;
break;
case 3: // field_name
fieldDef.name = value.s;
break;
case 4: // array
break;
case 5: // components
break;
case 6: // scale
fieldDef.scale = value.v;
break;
case 7: // offset
fieldDef.offset = value.v;
break;
case 8: // units
fieldDef.unit = value.s;
break;
case 9: // bits
case 10: // accumulate
case 13: // fit_base_unit_id
case 14: // native_mesg_num
break;
case 15: // native field number
fieldDef.native = value.v;
break;
default:
// ignore it
break;
}
}
//qDebug() << "num" << fieldDef.num << "deve_idx" << fieldDef.dev_id << "type" << fieldDef.type << "native" << fieldDef.native << "name" << fieldDef.name.c_str() << "unit" << fieldDef.unit.c_str() << "scale" << fieldDef.scale << "offset" << fieldDef.offset;
QString key = QString("%1.%2").arg(fieldDef.dev_id).arg(fieldDef.num);
if (!local_deve_fields.contains(key)) {
local_deve_fields.insert((key), fieldDef);
}
if (fieldDef.native > -1 && !record_deve_native_fields.values().contains(fieldDef.native)) {
record_deve_native_fields.insert(key, fieldDef.native);
/*RideFile::SeriesType series = getSeriesForNative(fieldDef.native);
if (series != RideFile::none) {
QString nativeName = rideFile->symbolForSeries(series);
dataInfos.append(QString("NATIVE %1 : Field %2").arg(nativeName).arg(fieldDef.name.c_str()));
}*/
}
}
void read_header(bool &stop, QStringList &errors, int &data_size) {
stop = false;
try {
// read the header
int header_size = read_uint8();
if (header_size != 12 && header_size != 14) {
errors << QString("bad header size: %1").arg(header_size);
stop = true;
}
int protocol_version = read_uint8();
(void) protocol_version;
// if the header size is 14 we have profile minor then profile major
// version. We still don't do anything with this information
int profile_version = read_uint16(false); // always littleEndian
(void) profile_version;
//qDebug() << "profile_version" << profile_version/100.0; // not sure what to do with this
data_size = read_uint32(false); // always littleEndian
char fit_str[5];
if (file.read(fit_str, 4) != 4) {
errors << "truncated header";
stop = true;
}
fit_str[4] = '\0';
if (strcmp(fit_str, ".FIT") != 0) {
errors << QString("bad header, expected \".FIT\" but got \"%1\"").arg(fit_str);
stop = true;
}
// read the rest of the header
if (header_size == 14) read_uint16(false);
} catch (TruncatedRead &e) {
errors << "truncated file header";
stop = true;
}
}
int read_record(bool &stop, QStringList &errors) {
stop = false;
int count = 0;
int header_byte = read_uint8(&count);
if (!(header_byte & 0x80) && (header_byte & 0x40)) {
// Definition record
int local_msg_type = header_byte & 0xf;
bool with_deve_data = (header_byte & 0x20) == 0x20 ;
local_msg_types.insert(local_msg_type, FitDefinition());
FitDefinition &def = local_msg_types[local_msg_type];
int reserved = read_uint8(&count); (void) reserved; // unused
def.is_big_endian = read_uint8(&count);
def.global_msg_num = read_uint16(def.is_big_endian, &count);
int num_fields = read_uint8(&count);
if (FIT_DEBUG && FIT_DEBUG_LEVEL>0) {
//qDebug() << "definition: local type=" << local_msg_type << "global=" << def.global_msg_num << "arch=" << def.is_big_endian << "fields=" << num_fields;
printf("definition: local type=%d global=%d arch=%d fields=%d\n",
local_msg_type, def.global_msg_num, def.is_big_endian,
num_fields );
}
for (int i = 0; i < num_fields; ++i) {
def.fields.push_back(FitField());
FitField &field = def.fields.back();
field.num = read_uint8(&count);
field.size = read_uint8(&count);
int base_type = read_uint8(&count);
field.type = base_type & 0x1f;
field.deve_idx = -1;
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf(" field %d: %d bytes, num %d, type %d, size %d\n",
i, field.size, field.num, field.type, field.size );
}
}
if (with_deve_data) {
int num_fields = read_uint8(&count);
for (int i = 0; i < num_fields; ++i) {
def.fields.push_back(FitField());
FitField &field = def.fields.back();
field.num = read_uint8(&count);
field.size = read_uint8(&count);
field.deve_idx = read_uint8(&count);
QString key = QString("%1.%2").arg(field.deve_idx).arg(field.num);
FitDeveField devField = local_deve_fields[key];
field.type = devField.type & 0x1f;
//qDebug() << "field" << field.num << "type" << field.type << "size" << field.size << "deve idx" << field.deve_idx;
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf(" field %d: %d bytes, num %d, type %d, size %d\n",
i, field.size, field.num, field.type, field.size );
}
}
}
}
else {
// Data record
int local_msg_type = 0;
int time_offset = 0;
if (header_byte & 0x80) {
// compressed time record
local_msg_type = (header_byte >> 5) & 0x3;
time_offset = header_byte & 0x1f;
}
else {
local_msg_type = header_byte & 0xf;
}
if (!local_msg_types.contains(local_msg_type)) {
printf( "local type %d without previous definition\n", local_msg_type );
errors << QString("local type %1 without previous definition").arg(local_msg_type);
stop = true;
return count;
}
const FitDefinition &def = local_msg_types[local_msg_type];
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf( "read_record message local=%d global=%d\n", local_msg_type,
def.global_msg_num );
}
std::vector<FitValue> values;
foreach(const FitField &field, def.fields) {
FitValue value;
int size;
switch (field.type) {
case 0: size = 1;
if (field.size==size) {
value.type = SingleValue; value.v = read_uint8(&count);;
} else { // Multi-values
value.type = ListValue;
value.list.clear();
for (int i=0;i<field.size/size;i++) {
value.list.append(read_uint8(&count));
}
size = field.size;
}
break;
case 1: value.type = SingleValue; value.v = read_int8(&count); size = 1; break;
case 2: size = 1;
if (field.size==size) {
value.type = SingleValue; value.v = read_uint8(&count);
} else { // Multi-values
value.type = ListValue;
value.list.clear();
for (int i=0;i<field.size/size;i++) {
value.list.append(read_uint8(&count));
}
size = field.size;
}
break;
case 3: value.type = SingleValue; value.v = read_int16(def.is_big_endian, &count); size = 2; break;
case 4: size = 2;
if (field.size==size) {
value.type = SingleValue; value.v = read_uint16(def.is_big_endian, &count);
} else { // Multi-values
value.type = ListValue;
value.list.clear();
for (int i=0;i<field.size/size;i++) {
value.list.append(read_uint16(def.is_big_endian, &count));
}
size = field.size;
}
break;
case 5: value.type = SingleValue; value.v = read_int32(def.is_big_endian, &count); size = 4; break;
case 6: size = 4;
if (field.size==size) {
value.type = SingleValue; value.v = read_uint32(def.is_big_endian, &count);
} else { // Multi-values
value.type = ListValue;
value.list.clear();
for (int i=0;i<field.size/size;i++) {
value.list.append(read_uint32(def.is_big_endian, &count));
}
size = field.size;
}
break;
case 7:
value.type = StringValue;
value.s = read_text(field.size, &count);
size = field.size;
break;
case 8: // FLOAT32
size = 4;
value.type = FloatValue;
value.f = read_float32(&count);
if (value.f != value.f) // No NAN
value.f = 0;
size = field.size;
break;
//case 9: // FLOAT64
case 10: size = 1;
if (field.size==size) {
value.type = SingleValue; value.v = read_uint8z(&count); size = 1;
} else { // Multi-values
value.type = ListValue;
value.list.clear();
for (int i=0;i<field.size/size;i++) {
value.list.append(read_uint8z(&count));
}
size = field.size;
}
break;
case 11: value.type = SingleValue; value.v = read_uint16z(def.is_big_endian, &count); size = 2; break;
case 12: value.type = SingleValue; value.v = read_uint32z(def.is_big_endian, &count); size = 4; break;
case 13: // BYTE
value.type = ListValue;
value.list.clear();
for (int i=0;i<field.size;i++) {
value.list.append(read_uint8(&count));
}
size = value.list.size();
break;
// we may need to add support for float, string + byte base types here
default:
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
// TODO: Dump raw data.
printf("unknown type: %d size: %d \n", field.type,
field.size);
}
read_unknown( field.size, &count );
value.type = SingleValue;
value.v = NA_VALUE;
unknown_base_type.insert(field.type);
size = field.size;
}
// Size is greater than expected
if (size < field.size) {
if (FIT_DEBUG && FIT_DEBUG_LEVEL>1) {
printf( " warning : size=%d for type=%d (num=%d)\n",
field.size, field.type, field.num);
}
read_unknown( field.size-size, &count );
}
values.push_back(value);
if (FIT_DEBUG && ((FIT_DEBUG_LEVEL>2 && def.global_msg_num!=RECORD_MSG_NUM) || FIT_DEBUG_LEVEL>3 )) {
printf( " field: type=%d num=%d size=%d(%d) ",
field.type, field.num, field.size, size);
if (value.type == SingleValue) {
if (value.v == NA_VALUE)
printf( "value=NA\n");
else
printf( "value=%lld\n", value.v );
}
else if (value.type == StringValue)
printf( "value=%s\n", value.s.c_str() );
else if (value.type == ListValue) {
printf( "values=");
for (int i=0;i<value.list.count();i++) {
if (value.v == NA_VALUE)
printf( "NA,");
else
printf( "%lld,", value.list.at(i) );
}
printf( "\n");
}
}
}
// Most of the record types in the FIT format aren't actually all
// that useful. FileId, Lap, and Record clearly are. The one
// other one that might be useful is DeviceInfo, but it doesn't
// seem to be filled in properly. Sean's Cinqo, for example,
// shows up as manufacturer #65535, even though it should be #7.
switch (def.global_msg_num) {
case FILE_ID_MSG_NUM: // #0
decodeFileId(def, time_offset, values); break;
case SESSION_MSG_NUM: // #18
decodeSession(def, time_offset, values); break; /* session */
case LAP_MSG_NUM: // #19
decodeLap(def, time_offset, values);
break;
case RECORD_MSG_NUM: // #20
decodeRecord(def, time_offset, values);
break;
case EVENT_MSG_NUM: // #21
decodeEvent(def, time_offset, values); break;
case 23:
decodeDeviceInfo(def, time_offset, values); /* device info */
break;
case ACTIVITY_MSG_NUM: // #34
decodeActivity(def, time_offset, values);
break;
case 101:
decodeLength(def, time_offset, values);
break; /* lap swimming */
case 128:
decodeWeather(def, time_offset, values);
break; /* weather broadcast */
case 132:
decodeHr(def, time_offset, values); /* HR */
break;
case SEGMENT_MSG_NUM: // #142
decodeSegment(def, time_offset, values); /* segment data */
break;
case 206: // Developer Field Description
decodeDeveloperFieldDescription(def, time_offset, values);
break;
case 1: /* capabilities, device settings and timezone */ break;
case 2: decodeDeviceSettings(def, time_offset, values); break;
case 3: /* USER_PROFILE */
case 4: /* hrm profile */
case 5: /* sdm profile */
case 6: /* bike profile */
case 7: /* ZONES_TARGET field#1 = MaxHR (bpm) */
case 8: /* HR_ZONE */
case 9: /* POWER_ZONE */
case 10: /* MET_ZONE */
case 12: /* SPORT */
case 13: /* unknown */
case 15: /* goal */
case 22: /* source (undocumented) = sensors used for records ; see details below: */
/* #253: timestamp / #0: SPD/DIST / #1: SPD/DIST / #2: cadence / #4: HRM / #5: HRM */
case 26: /* workout */
case 27: /* workout step */
case 28: /* schedule */
case 29: /* location */
case 30: /* weight scale */
case 31: /* course */
case 32: /* course point */
case 33: /* totals */
case 35: /* software */
case 37: /* file capabilities */
case 38: /* message capabilities */
case 39: /* field capabilities */
case FILE_CREATOR_MSG_NUM: /* #49 file creator */
/* #0: software version / #1: hardware version */
case 51: /* blood pressure */
case 53: /* speed zone */
case 55: /* monitoring */
case 72: /* training file (undocumented) : new since garmin 800 */
case HRV_MSG_NUM:
decodeHRV(def, values);
break; /* hrv */
case 79: /* HR zone (undocumented) ; see details below: */
/* #253: timestamp / #1: default Min HR / #2: default Max HR / #5: user Min HR / #6: user Max HR */
case 103: /* monitoring info */
case 104: /* battery */
case 105: /* pad */
case 106: /* salve device */
case 113: /* unknown */
case 125: /* unknown */
case 131: /* cadence zone */
case 140: /* unknown */
case 141: /* unknown */
case 145: /* memo glob */
case 147: /* equipment (undocumented) = sensors presets (sensor name, wheel circumference, etc.) ; see details below: */
/* #0: equipment ID / #2: equipment name / #10: default wheel circ. value / #21: user wheel circ. value / #254: local eqt idx */
case 148: /* segment description & metadata (undocumented) ; see details below: */
/* #0: segment name (string) / #1: segment UID (string) / #2: unknown, seems to be always 2 (enum) / #3: unknown, seems to be always 1 (enum)
/ #4: exporting_user_id ? =user ID from connect ? (uint32) / #6: unknown, seems to be always 0 */
case 149: /* segment leaderboard (undocumented) ; see details below: */
/* #1: who (0=segment leader, 1=personal best, 2=connection, 3=group leader, 4=challenger, 5+=H)
/ #3: ID of source garmin connect activity (uint32) ? OR ? timestamp ? / #4: time to finish (ms) / #254: message counter idx */
case 150: /* segment trackpoint (undocumented) ; see details below: */
/* #1: latitude / #2: longitude / #3: distance from start point / #4: elevation / #5: timer since start (ms) / #6: message counter index */
case 207: /* Developer ID */
break;
default:
unknown_global_msg_nums.insert(def.global_msg_num);
}
last_msg_type = def.global_msg_num;
}
return count;
}
RideFile * run() {
// get the Smart Recording parameters
isGarminSmartRecording = appsettings->value(NULL, GC_GARMIN_SMARTRECORD,Qt::Checked);
GarminHWM = appsettings->value(NULL, GC_GARMIN_HWMARK);
if (GarminHWM.isNull() || GarminHWM.toInt() == 0) GarminHWM.setValue(25); // default to 25 seconds.
// start
rideFile = new RideFile;
rideFile->setDeviceType("Garmin FIT");
rideFile->setFileFormat("Flexible and Interoperable Data Transfer (FIT)");
rideFile->setRecIntSecs(1.0); // this is a terrible assumption!
if (!file.open(QIODevice::ReadOnly)) {
delete rideFile;
return NULL;
}
int data_size = 0;
weatherXdata = new XDataSeries();
weatherXdata->name = "WEATHER";
weatherXdata->valuename << "WINDSPEED";
weatherXdata->unitname << "kph";
weatherXdata->valuename << "WINDHEADING";
weatherXdata->unitname << "";
weatherXdata->valuename << "TEMPERATURE";
weatherXdata->unitname << "C";
weatherXdata->valuename << "HUMIDITY";
weatherXdata->unitname << "relative humidity";
swimXdata = new XDataSeries();
swimXdata->name = "SWIM";
swimXdata->valuename << "TYPE";
swimXdata->unitname << "stroketype";
swimXdata->valuename << "DURATION";
swimXdata->unitname << "secs";
swimXdata->valuename << "STROKES";
swimXdata->unitname << "";
hrvXdata = new XDataSeries();
hrvXdata->name = "HRV";
hrvXdata->valuename << "R-R";
hrvXdata->unitname << "msecs";
gearsXdata = new XDataSeries();
gearsXdata->name = "GEARS";
gearsXdata->valuename << "FRONT";
gearsXdata->unitname << "t";
gearsXdata->valuename << "REAR";
gearsXdata->unitname << "t";
gearsXdata->valuename << "FRONT-NUM";
gearsXdata->unitname << "";
gearsXdata->valuename << "REAR-NUM";
gearsXdata->unitname << "";
deveXdata = new XDataSeries();
deveXdata->name = "DEVELOPER";
extraXdata = new XDataSeries();
extraXdata->name = "EXTRA";
bool stop = false;
bool truncated = false;
// read the header
read_header(stop, errors, data_size);
if (!stop) {
int bytes_read = 0;
try {
while (!stop && (bytes_read < data_size)) {
bytes_read += read_record(stop, errors);
}
}
catch (TruncatedRead &e) {
errors << "truncated file body";
//file.close();
//delete rideFile;
//return NULL;
truncated = true;
}
}
if (stop) {
file.close();
delete rideFile;
return NULL;
}
else {
if (!truncated) {
try {
int crc = read_uint16( false ); // always littleEndian
(void) crc;
}
catch (TruncatedRead &e) {
errors << "truncated file body";
return NULL;
}
// second file ?
try {
while (file.canReadLine()) {
read_header(stop, errors, data_size);
if (!stop) {
int bytes_read = 0;
try {
while (!stop && (bytes_read < data_size)) {
bytes_read += read_record(stop, errors);
}
}
catch (TruncatedRead &e) {
errors << "truncated second file body";
}
}
if (!truncated) {
try {
int crc = read_uint16( false ); // always littleEndian
(void) crc;
}
catch (TruncatedRead &e) {
errors << "truncated file body";
return NULL;
}
}
}
}
catch (TruncatedRead &e) {
}
}
foreach(int num, unknown_global_msg_nums)
qDebug() << QString("FitRideFile: unknown global message number %1; ignoring it").arg(num);
foreach(int num, unknown_record_fields)
qDebug() << QString("FitRideFile: unknown record field %1; ignoring it").arg(num);
foreach(int num, unknown_base_type)
qDebug() << QString("FitRideFile: unknown base type %1; skipped").arg(num);
QStringList uniqueDevices(deviceInfos.values());
uniqueDevices.removeDuplicates();
QString deviceInfo = uniqueDevices.join("\n");
if (! deviceInfo.isEmpty())
rideFile->setTag("Device Info", deviceInfo);
QString dataInfo;
foreach(QString info, dataInfos) {
dataInfo += info + "\n";
}
if (dataInfo.length()>0)
rideFile->setTag("Data Info", dataInfo);
file.close();
if (weatherXdata->datapoints.count()>0)
rideFile->addXData("WEATHER", weatherXdata);
else
delete weatherXdata;
if (swimXdata->datapoints.count()>0)
rideFile->addXData("SWIM", swimXdata);
else
delete swimXdata;
if (hrvXdata->datapoints.count()>0)
rideFile->addXData("HRV", hrvXdata);
else
delete hrvXdata;
if (gearsXdata->datapoints.count()>0)
rideFile->addXData("GEARS", gearsXdata);
else
delete gearsXdata;
if (deveXdata->datapoints.count()>0)
rideFile->addXData("DEVELOPER", deveXdata);
else
delete deveXdata;
if (extraXdata->datapoints.count()>0)
rideFile->addXData("EXTRA", extraXdata);
else
delete extraXdata;
return rideFile;
}
}
};
RideFile *FitFileReader::openRideFile(QFile &file, QStringList &errors, QList<RideFile*>*) const
{
QSharedPointer<FitFileReaderState> state(new FitFileReaderState(file, errors));
return state->run();
}
// ******************************
void write_int8(QByteArray *array, fit_value_t value) {
array->append(value);
}
void write_int16(QByteArray *array, fit_value_t value, bool is_big_endian) {
value = is_big_endian
? qFromBigEndian<qint16>( value )
: qFromLittleEndian<qint16>( value );
for (int i=0; i<16; i=i+8) {
array->append(value >> i);
}
}
void write_int32(QByteArray *array, fit_value_t value, bool is_big_endian) {
value = is_big_endian
? qFromBigEndian<qint32>( value )
: qFromLittleEndian<qint32>( value );
for (int i=0; i<32; i=i+8) {
array->append(value >> i);
}
}
uint16_t crc16(char *buf, int len)
{
uint16_t crc = 0x0000;
for (int pos = 0; pos < len; pos++) {
crc ^= (uint16_t)buf[pos] & 0xff;
for (int i = 8; i != 0; i--) { // Each bit
if ((crc & 0x0001) != 0) { // LSB set
crc >>= 1; // Shift right
crc ^= 0xA001; // XOR 0xA001
}
else
crc >>= 1; // Shift right
}
}
return crc;
}
void write_header(QByteArray *array, quint32 data_size) {
quint8 header_size = 14;
quint8 protocol_version = 16;
quint16 profile_version = 1320; // always littleEndian
write_int8(array, header_size);
write_int8(array, protocol_version);
write_int16(array, profile_version, false);
write_int32(array, data_size, false);
array->append(".FIT");
uint16_t header_crc = crc16(array->data(), array->length());
write_int16(array, header_crc, false);
}
void write_message_definition(QByteArray *array, int global_msg_num, int local_msg_typ, int num_fields) {
// Definition ------
write_int8(array, DEFINITION_MSG_HEADER + local_msg_typ); // definition_header
write_int8(array, 0); // reserved
write_int8(array, 1); // is_big_endian
write_int16(array, global_msg_num, true);
write_int8(array, num_fields);
}
void write_field_definition(QByteArray *array, int field_num, int field_size, int base_type) {
write_int8(array, field_num);
write_int8(array, field_size);
write_int8(array, base_type);
}
void write_file_id(QByteArray *array, const RideFile *ride) {
// 0 type
// 1 manufacturer
// 2 product/garmin_product
// 3 serial_number
// 4 time_created
// 5 number
// 8 product_name
write_message_definition(array, FILE_ID_MSG_NUM, 0, 6); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 0, 1, 0); // 1. type (0)
write_field_definition(array, 1, 2, 132); // 2. manufacturer (1)
write_field_definition(array, 2, 2, 132); // 3. product (2)
write_field_definition(array, 4, 4, 134); // 4. time_created (4)
write_field_definition(array, 3, 4, 134); // 5. serial_number (3)
write_field_definition(array, 5, 2, 132); // 6. number (5)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. type
int value = 4; //file:activity
write_int8(array, value);
// 2. manufacturer
value = 0;
write_int16(array, value, true);
// 3. product
value = 0;
write_int16(array, value, true);
// 4. time_created
value = ride->startTime().toTime_t() - qbase_time.toTime_t();
write_int32(array, value, true);
// 5. serial
value = 0;
write_int32(array, value, true);
// 6. number
value = 65535; //NA
write_int16(array, value, true);
}
void write_file_creator(QByteArray *array) {
// 0 software_version uint16
// 1 hardware_version uint8
write_message_definition(array, FILE_CREATOR_MSG_NUM, 0, 2); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 0, 2, 132); // 1. software_version (0)
write_field_definition(array, 1, 1, 2); // 1. hardware_version (0)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. software_version
int value = 100;
write_int16(array, value, true);
// 1. hardware_version
value = 255; // NA
write_int8(array, value);
}
void write_session(QByteArray *array, const RideFile *ride, QHash<QString,RideMetricPtr> computed) {
write_message_definition(array, SESSION_MSG_NUM, 0, 10); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 253, 4, 134); // timestamp (253)
write_field_definition(array, 254, 2, 132); // message_index (254)
write_field_definition(array, 0, 1, 2); // event (0)
write_field_definition(array, 1, 1, 2); // event_type (1)
write_field_definition(array, 2, 4, 134); // start_time (2)
write_field_definition(array, 5, 1, 0); // sport (5)
write_field_definition(array, 6, 1, 2); // subsport (6)
write_field_definition(array, 7, 4, 134); // total_elapsed_time (7)
write_field_definition(array, 9, 4, 134); // total_distance (9)
write_field_definition(array, 28, 1, 0); // trigger (28)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. timestamp (253)
int value = ride->startTime().toTime_t() - qbase_time.toTime_t();;
if (ride->dataPoints().count() > 0) {
value += ride->dataPoints().last()->secs+ride->recIntSecs();
}
write_int32(array, value, true);
// 2. message_index (254)
write_int16(array, 0, true);
// 3. event (0)
value = 9; // lap (8=session)
write_int8(array, value);
// 4. event_type (1)
value = 1; // stop (4=stop_all)
write_int8(array, value);
// 5. start_time (4)
value = ride->startTime().toTime_t() - qbase_time.toTime_t();
write_int32(array, value, true);
// 6. sport
write_int8(array, 2);
// 7. sub sport
write_int8(array, 0);
// 8. total_elapsed_time (7)
value = computed.value("workout_time")->value(true) * 1000;
write_int32(array, value, true);
// 9. total_distance (9)
value = computed.value("total_distance")->value(true) * 100000;
write_int32(array, value, true);
// 10. trigger
write_int8(array, 0); // activity end
}
void write_lap(QByteArray *array, const RideFile *ride) {
write_message_definition(array, LAP_MSG_NUM, 0, 6); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 253, 4, 134); // timestamp (253)
write_field_definition(array, 254, 2, 132); // message_index (254)
write_field_definition(array, 0, 1, 2); // event (0)
write_field_definition(array, 1, 1, 2); // event_type (1)
write_field_definition(array, 2, 4, 134); // start_time (2)
write_field_definition(array, 24, 1, 2); // trigger (24)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. timestamp
int value = ride->startTime().toTime_t() - qbase_time.toTime_t();;
if (ride->dataPoints().count() > 0) {
value += ride->dataPoints().last()->secs+ride->recIntSecs();
}
write_int32(array, value, true);
// 2. message_index (254)
write_int16(array, 0, true);
// 3. event
value = 9; // session=8, lap=9
write_int8(array, value);
// 4. event_type
value = 1; // stop_all=9, stop=1
write_int8(array, value);
// 5. start_time
value = ride->startTime().toTime_t() - qbase_time.toTime_t();;
write_int32(array, value, true);
// 6. trigger
value = 7; // session_end
write_int8(array, value);
}
void write_start_event(QByteArray *array, const RideFile *ride) {
write_message_definition(array, EVENT_MSG_NUM, 0, 5); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 253, 4, 134); // timestamp (253)
write_field_definition(array, 0, 1, 2); // event (0)
write_field_definition(array, 1, 1, 2); // event_type (1)
write_field_definition(array, 3, 4, 134); // data (3)
write_field_definition(array, 4, 1, 2); // event_group (4)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. timestamp
int value = ride->startTime().toTime_t() - qbase_time.toTime_t();
write_int32(array, value, true);
// 2. event
value = 0;
write_int8(array, value);
// 3. event_type
value = 0;
write_int8(array, value);
// 4. data
write_int32(array, 0, true);
// 5. event_group
write_int8(array, 0);
}
void write_stop_event(QByteArray *array, const RideFile *ride) {
write_message_definition(array, EVENT_MSG_NUM, 0, 5); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 253, 4, 134); // timestamp (253)
write_field_definition(array, 0, 1, 2); // event (0)
write_field_definition(array, 1, 1, 2); // event_type (1)
write_field_definition(array, 3, 4, 134); // data (3)
write_field_definition(array, 4, 1, 2); // event_group (4)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. timestamp
int value = ride->startTime().toTime_t() + 2 - qbase_time.toTime_t();
write_int32(array, value, true);
// 2. event
value = 8; // timer=0
write_int8(array, value);
// 3. event_type
value = 9; // stop_all=4
write_int8(array, value);
// 4. data
write_int32(array, 1, true);
// 5. event_group
write_int8(array, 1);
}
void write_activity(QByteArray *array, const RideFile *ride, QHash<QString,RideMetricPtr> computed) {
write_message_definition(array, ACTIVITY_MSG_NUM, 0, 6); // global_msg_num, local_msg_type, num_fields
write_field_definition(array, 253, 4, 134); // timestamp (253)
write_field_definition(array, 0, 4, 134); // total_timer_time (0)
write_field_definition(array, 1, 2, 132); // num_sessions (1)
write_field_definition(array, 2, 1, 2); // type (2)
write_field_definition(array, 3, 1, 2); // event (3)
write_field_definition(array, 4, 1, 2); // event_type (4)
// Record ------
int record_header = 0;
write_int8(array, record_header);
// 1. timestamp
int value = ride->startTime().toTime_t() - qbase_time.toTime_t();
if (ride->dataPoints().count() > 0) {
value += ride->dataPoints().last()->secs+ride->recIntSecs();
}
write_int32(array, value, true);
// 2. total_timer_time
value = computed.value("workout_time")->value(true) * 1000;
write_int32(array, value, true);
// 3. num_sessions
value = 1;
write_int16(array, value, true);
// 4. type
value = 0; // manual
write_int8(array, value);
// 5. event
value = 26; // activity
write_int8(array, value);
// 6. event_type
value = 1; // stop
write_int8(array, value);
}
void write_record_definition(QByteArray *array, const RideFile *ride, QMap<int, int> *local_msg_type_for_record_type, bool withAlt, bool withWatts, bool withHr, bool withCad, int type ) {
int num_fields = 1;
QByteArray *fields = new QByteArray();
write_field_definition(fields, 253, 4, 134); // timestamp (253)
if ( ride->areDataPresent()->lat ) {
num_fields ++;
write_field_definition(fields, 0, 4, 133); // position_lat (0)
num_fields ++;
write_field_definition(fields, 1, 4, 133); // position_long (2)
}
if ( withAlt && ride->areDataPresent()->alt ) {
num_fields ++;
write_field_definition(fields, 2, 2, 132); // altitude (2) 4 0x84
}
if ( withHr && ride->areDataPresent()->hr ) {
num_fields ++;
write_field_definition(fields, 3, 1, 2); // heart_rate (3)
}
if ( withCad && ride->areDataPresent()->cad ) {
num_fields ++;
write_field_definition(fields, 4, 1, 2); // cadence (4)
}
if ( ride->areDataPresent()->km ) {
num_fields ++;
write_field_definition(fields, 5, 4, 134); // distance (5)
}
if ( ride->areDataPresent()->kph ) {
num_fields ++;
write_field_definition(fields, 6, 2, 132); // speed (6)
}
if ( withWatts && ride->areDataPresent()->watts ) {
num_fields ++;
write_field_definition(fields, 7, 2, 132); // power (7)
}
// can be NA...
if ( (type&1)==1 ) {
num_fields ++;
write_field_definition(fields, 13, 1, 2); // temperature (13)
}
if ( (type&2)==1 ) {
num_fields ++;
write_field_definition(fields, 30, 1, 2); // left_right_balance (30)
}
int local_msg_type = local_msg_type_for_record_type->values().count()+1;
write_message_definition(array, RECORD_MSG_NUM, local_msg_type, num_fields); // global_msg_num, local_msg_type, num_fields
array->append(fields->data(), fields->size());
local_msg_type_for_record_type->insert(type, local_msg_type);
}
void write_record(QByteArray *array, const RideFile *ride, bool withAlt, bool withWatts, bool withHr, bool withCad ) {
QMap<int, int> *local_msg_type_for_record_type = new QMap<int, int>();
// Record ------
foreach (const RideFilePoint *point, ride->dataPoints()) {
int type = 0;
// Temperature and lrbalance can be NA
if ( ride->areDataPresent()->temp && point->temp != RideFile::NA) {
type += 1;
}
if ( ride->areDataPresent()->lrbalance && point->lrbalance != RideFile::NA) {
type += 2;
}
// Add record definition for this type of record
if (local_msg_type_for_record_type->value(type, -1)==-1)
write_record_definition(array, ride, local_msg_type_for_record_type, withAlt, withWatts, withHr, withCad, type);
int record_header = local_msg_type_for_record_type->value(type, 1);
// RidePoint
QByteArray *ridePoint = new QByteArray();
write_int8(ridePoint, record_header);
int value = point->secs + ride->startTime().toTime_t() - qbase_time.toTime_t();
write_int32(ridePoint, value, true);
if ( ride->areDataPresent()->lat ) {
write_int32(ridePoint, point->lat * 0x7fffffff / 180, true);
write_int32(ridePoint, point->lon * 0x7fffffff / 180, true);
}
if ( withAlt && ride->areDataPresent()->alt ) {
write_int16(ridePoint, (point->alt+500) * 5, true);
}
if ( withHr && ride->areDataPresent()->hr ) {
write_int8(ridePoint, point->hr);
}
if ( withCad && ride->areDataPresent()->cad ) {
write_int8(ridePoint, point->cad);
}
if ( ride->areDataPresent()->km ) {
write_int32(ridePoint, point->km * 100000, true);
}
if ( ride->areDataPresent()->kph ) {
write_int16(ridePoint, point->kph / 3.6 * 1000, true);
}
if ( withWatts && ride->areDataPresent()->watts ) {
write_int16(ridePoint, point->watts, true);
}
// temp and lrbalance can be NA... Not present for a point even if present in RideFile
if ( (type&1)==1) {
write_int8(ridePoint, point->temp);
}
if ( (type&2)==1 ) {
write_int8(ridePoint, point->lrbalance);
}
array->append(ridePoint->data(), ridePoint->size());
}
}
QByteArray
FitFileReader::toByteArray(Context *context, const RideFile *ride, bool withAlt, bool withWatts, bool withHr, bool withCad) const
{
const char *metrics[] = {
"total_distance",
"workout_time",
"total_work",
"average_hr",
"max_heartrate",
"average_cad",
"max_cadence",
"average_power",
"max_power",
"max_speed",
"average_speed",
NULL
};
QStringList worklist = QStringList();
for (int i=0; metrics[i];i++) worklist << metrics[i];
QHash<QString,RideMetricPtr> computed;
if (context) { // can't do this standalone
RideItem *tempItem = new RideItem(const_cast<RideFile*>(ride), context);
computed = RideMetric::computeMetrics(tempItem, Specification(), worklist);
}
QByteArray array;
QByteArray data;
// An activity file shall contain file_id, activity, session, and lap messages.
// The file may also contain record, event, length and/or hrv messages.
// All data messages in an activity file (other than hrv) are related by a timestamp.
write_file_id(&data, ride); // file_id 0
write_file_creator(&data); // file_creator 49
write_start_event(&data, ride); // event 21 (x15)
write_record(&data, ride, withAlt, withWatts, withHr, withCad); // record 20 (x14)
write_lap(&data, ride); // lap 19 (x11)
write_stop_event(&data, ride); // event 21 (x15)
write_session(&data, ride, computed); // session 18 (x12)
write_activity(&data, ride, computed); // activity 34 (x22)
write_header(&array, data.size());
array += data;
uint16_t array_crc = crc16(array.data(), array.length());
write_int16(&array, array_crc, false);
return array;
}
bool
FitFileReader::writeRideFile(Context *context, const RideFile *ride, QFile &file) const
{
QByteArray content = toByteArray(context, ride, true, true, true, true);
if (!file.open(QIODevice::WriteOnly)) return(false);
file.resize(0);
QDataStream out(&file);
out.setByteOrder(QDataStream::LittleEndian);
file.write(content);
file.close();
return(true);
}