mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
.. 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
3579 lines
135 KiB
C++
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);
|
|
}
|
|
|
|
|
|
|
|
|