/* * 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 #include #include #include #include #include #include #include #include #include #include #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::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 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 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 local_msg_types; QMap local_deve_fields; // All developer fields QMap record_extra_fields; QMap record_deve_fields; // Developer fields in DEVELOPER XDATA or STANDARD DATA QMap record_deve_native_fields; // Developer fields with native values QSet record_native_fields; QSet 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 deviceInfos; QList 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( &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( &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( &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(&i), 2) != 2) throw TruncatedRead(); if (count) (*count) += 2; i = is_big_endian ? qFromBigEndian( i ) : qFromLittleEndian( 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(&i), 2) != 2) throw TruncatedRead(); if (count) (*count) += 2; i = is_big_endian ? qFromBigEndian( i ) : qFromLittleEndian( 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(&i), 2) != 2) throw TruncatedRead(); if (count) (*count) += 2; i = is_big_endian ? qFromBigEndian( i ) : qFromLittleEndian( 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(&i), 4) != 4) throw TruncatedRead(); if (count) (*count) += 4; i = is_big_endian ? qFromBigEndian( i ) : qFromLittleEndian( 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(&i), 4) != 4) throw TruncatedRead(); if (count) (*count) += 4; i = is_big_endian ? qFromBigEndian( i ) : qFromLittleEndian( 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(&i), 4) != 4) throw TruncatedRead(); if (count) (*count) += 4; i = is_big_endian ? qFromBigEndian( i ) : qFromLittleEndian( i ); return i == 0x00000000 ? NA_VALUE : i; } fit_float_value read_float32(int *count = NULL) { float f; if (file.read(reinterpret_cast(&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& 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& 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& 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& 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& 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& 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; jsecs = hrv_time; p->number[0] = rrvalue; hrvXdata->datapoints.append(p); } } } } void decodeLap(const FitDefinition &def, int time_offset, const std::vector& 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& 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 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"<-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& 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& 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& values) { time_t time = 0; if (time_offset > 0) { time = last_time + time_offset; } QList timestamps; QList 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> 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;i0) { 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& 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& 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& 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 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;i1) { // 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;ivalue(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*) const { QSharedPointer 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( value ) : qFromLittleEndian( 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( value ) : qFromLittleEndian( 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 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 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 *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 *local_msg_type_for_record_type = new QMap(); // 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 computed; if (context) { // can't do this standalone RideItem *tempItem = new RideItem(const_cast(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); }