/* * Copyright (c) 2007-2008 Sean C. Rhea (srhea@srhea.net) * * 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 #include #include #include #include #include #include #include #include #include #define FIT_DEBUG false // debug traces #define RECORD_TYPE 20 static int fitFileReaderRegistered = RideFileFactory::instance().registerReader( "fit", "Garmin FIT", new FitFileReader()); static const QDateTime qbase_time(QDate(1989, 12, 31), QTime(0, 0, 0), Qt::UTC); struct FitField { int num; int type; // FIT base_type int size; // in bytes }; struct FitDefinition { int global_msg_num; bool is_big_endian; std::vector fields; }; /* 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; enum fitValueType { SingleValue, DoubleValue, StringValue }; typedef enum fitValueType FitValueType; struct FitValue { FitValueType type; fit_value_t v; fit_value_t v2; fit_string_value s; }; struct FitFileReaderState { QFile &file; QStringList &errors; RideFile *rideFile; time_t start_time; time_t last_time; double last_distance; QMap local_msg_types; QSet unknown_record_fields, unknown_global_msg_nums, unknown_base_type; int interval; int calibration; int devices; bool stopped; int last_event_type; int last_event; int last_msg_type; QVariant isGarminSmartRecording; QVariant GarminHWM; 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), last_event_type(-1), last_event(-1), last_msg_type(-1) { } struct TruncatedRead {}; void read_unknown( int size, int *count = NULL ){ char c[size+1]; if (file.read(c, size ) != 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; } 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; default: ; // do nothing } } if (manu == 1) { // 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: rideFile->setDeviceType("Garmin FR301"); break; case 717: case 987: rideFile->setDeviceType("Garmin FR405"); break; case 782: rideFile->setDeviceType("Garmin FR50"); break; case 988: rideFile->setDeviceType("Garmin FR60"); break; case 1018: rideFile->setDeviceType("Garmin FR310XT"); break; case 1036: case 1199: case 1213: case 1387: rideFile->setDeviceType("Garmin Edge 500"); break; case 1124: case 1274: rideFile->setDeviceType("Garmin FR110"); break; case 1169: case 1333: case 1334: case 1386: rideFile->setDeviceType("Garmin Edge 800"); break; case 1325: rideFile->setDeviceType("Garmin Edge 200"); break; case 1328: rideFile->setDeviceType("Garmin FR910XT"); break; case 1345: case 1410: rideFile->setDeviceType("Garmin FR610"); break; case 1360: rideFile->setDeviceType("Garmin FR210"); break; case 1436: rideFile->setDeviceType("Garmin FR70"); break; case 1446: rideFile->setDeviceType("Garmin FR310XT 4T"); break; case 1482: case 1688: rideFile->setDeviceType("Garmin FR10"); break; case 1499: rideFile->setDeviceType("Garmin Swim"); break; case 1551: rideFile->setDeviceType("Garmin Fenix"); break; case 1561: case 1742: case 1821: rideFile->setDeviceType("Garmin Edge 510"); break; case 1567: rideFile->setDeviceType("Garmin Edge 810"); break; case 1623: rideFile->setDeviceType("Garmin FR620"); break; case 1765: case 2130: case 2131: case 2132: rideFile->setDeviceType("Garmin FR920XT"); break; case 1836: case 2052: case 2053: case 2070: case 2100: rideFile->setDeviceType("Garmin Edge 1000"); break; case 1903: rideFile->setDeviceType("Garmin FR15"); break; case 1967: rideFile->setDeviceType("Garmin Fenix2"); break; case 2050: case 2188: case 2189: rideFile->setDeviceType("Garmin Fenix3"); break; case 2067: rideFile->setDeviceType("Garmin Edge 520"); break; case 2147: rideFile->setDeviceType("Garmin Edge 25"); break; case 2153: rideFile->setDeviceType("Garmin FR225"); break; case 2238: rideFile->setDeviceType("Garmin Edge 20"); break; case 20119: rideFile->setDeviceType("Garmin Training Center"); break; case 65532: rideFile->setDeviceType("Android ANT+ Plugin"); break; case 65534: rideFile->setDeviceType("Garmin Connect Website"); break; default: rideFile->setDeviceType(QString("Garmin %1").arg(prod)); } } else if (manu == 6 ) { // SRM // powercontrol now uses FIT files from PC8 switch (prod) { case 6: rideFile->setDeviceType("SRM PC6");break; case 7: rideFile->setDeviceType("SRM PC7");break; case 8: rideFile->setDeviceType("SRM PC8");break; default: rideFile->setDeviceType("SRM Powercontrol");break; } } else if (manu == 9 ) { // Powertap switch (prod) { case 14: rideFile->setDeviceType("Joule 2.0");break; case 18: rideFile->setDeviceType("Joule");break; case 19: rideFile->setDeviceType("Joule GPS");break; case 22: rideFile->setDeviceType("Joule GPS+");break; default: rideFile->setDeviceType(QString("Powertap Device %1").arg(prod));break; } } else if (manu == 38) { // o_synce switch (prod) { case 1: rideFile->setDeviceType("o_synce navi2coach"); break; default: rideFile->setDeviceType(QString("o_synce %1").arg(prod)); } } else if (manu == 70) { // does not set product at this point rideFile->setDeviceType("Sigmasport ROX"); } else if (manu == 76) { // Moxy rideFile->setDeviceType("Moxy Monitor"); } else if (manu == 260) { // Zwift! rideFile->setDeviceType("Zwift"); } else { rideFile->setDeviceType(QString("Unknown FIT Device %1:%2").arg(manu).arg(prod)); } rideFile->setFileFormat("FIT (*.fit)"); } void decodeSession(const FitDefinition &def, int, const std::vector values) { 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 5: // sport field switch (value) { case 1: // running: rideFile->setTag("Sport","Run"); break; default: // if we can't work it out, assume bike case 2: // cycling rideFile->setTag("Sport","Bike"); break; case 5: // swimming rideFile->setTag("Sport","Swim"); break; } break; default: ; // do nothing } if (FIT_DEBUG) { printf("decodeSession field %d: %d bytes, num %d, type %d\n", i, field.size, field.num, field.type ); } } } void decodeDeviceInfo(const FitDefinition &def, int, const std::vector values) { int i = 0; foreach(const FitField &field, def.fields) { fit_value_t value = values[i++].v; if( value == NA_VALUE ) continue; if (FIT_DEBUG) { printf("decodeDeviceInfo field %d: %d bytes, num %d, type %d\n", i, field.size, field.num, field.type ); } } } void decodeEvent(const FitDefinition &def, int, const std::vector values) { int time = -1; int event = -1; int event_type = -1; qint16 data16 = -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; default: ; // do nothing } } if (event == 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); } } else if (event == 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; } } if (FIT_DEBUG) { printf("event type %d\n", event_type); } last_event = event; last_event_type = event_type; } 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; ++interval; foreach(const FitField &field, def.fields) { fit_value_t value = values[i++].v; if( value == NA_VALUE ) continue; switch (field.num) { case 253: time = value + qbase_time.toTime_t(); break; case 2: this_start_time = value + qbase_time.toTime_t(); 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 (rideFile->dataPoints().count()) // no samples means no laps.. rideFile->addInterval(RideFileInterval::DEVICE, this_start_time - start_time, time - start_time, QString(QObject::tr("Lap %1")).arg(interval)); } void decodeRecord(const FitDefinition &def, int time_offset, const std::vector values) { 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 = 0; double kph = 0, temperature = RideFile::NoTemp, watts = 0, slope = 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; fit_value_t lati = NA_VALUE, lngi = NA_VALUE; int i = 0; foreach(const FitField &field, def.fields) { fit_value_t value = values[i].v; fit_value_t value2 = values[i++].v2; if( value == NA_VALUE ) continue; switch (field.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; break; case 0: // POSITION_LAT lati = value; break; case 1: // POSITION_LONG lngi = value; break; case 2: // ALTITUDE 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 rvert = value / 100.0f; break; //case 40: // ACTIVITY_TYPE // // TODO We should know/test value for run // run = true; // break; case 41: // GROUND CONTACT TIME rcontact = value / 10.0f; 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; break; case 53: // RUNNING CADENCE FRACTIONAL VALUE break; case 54: // tHb tHb= value/100.0f; break; case 57: // SMO2 smO2= value/10.0f; break; case 61: // ? GPS Altitude ? 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(value * 360.0/256); leftBottomDeathCenter = round(value2 * 360.0/256); break; case 70: // ? Left Peak Phase ? leftTopPeakPowerPhase = round(value * 360.0/256); leftBottomPeakPowerPhase = round(value2 * 360.0/256); break; case 71: // ? Right Power Phase ? rightTopDeathCenter = round(value * 360.0/256); rightBottomDeathCenter = round(value2 * 360.0/256); break; case 72: // ? Right Peak Phase ? rightTopPeakPowerPhase = round(value * 360.0/256); rightBottomPeakPowerPhase = round(value2 * 360.0/256); break; default: unknown_record_fields.insert(field.num); } } if (time == last_time) return; // Sketchy, but some FIT files do this. 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; double headwind = 0.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), prevPoint->headwind + (deltaHeadwind * weight), 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->appendPoint(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); last_time = time; last_distance = km; } 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; 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) { 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; if (FIT_DEBUG) { 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)) { 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) { 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: value.type = SingleValue; value.v = read_uint8(&count); size = 1; break; case 1: value.type = SingleValue; value.v = read_int8(&count); size = 1; break; case 2: value.type = SingleValue; value.v = read_uint8(&count); size = 1; // Multi-values ? if (field.size>size) { value.type = DoubleValue; value.v2 = read_uint8(&count); size = 2; } break; case 3: value.type = SingleValue; value.v = read_int16(def.is_big_endian, &count); size = 2; break; case 4: value.type = SingleValue; value.v = read_uint16(def.is_big_endian, &count); size = 2; // Multi-values ? if (field.size>size) { value.type = DoubleValue; value.v2 = read_uint16(def.is_big_endian, &count); size = 4; } break; case 5: value.type = SingleValue; value.v = read_int32(def.is_big_endian, &count); size = 4; break; case 6: value.type = SingleValue; value.v = read_uint32(def.is_big_endian, &count); size = 4; break; case 7: value.type = StringValue; value.s = read_text(field.size, &count); size = field.size; break; //case 8: // FLOAT32 //case 9: // FLOAT64 case 10: value.type = SingleValue; value.v = read_uint8z(&count); size = 1; 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 // we may need to add support for float, string + byte base types here default: read_unknown( field.size, &count ); value.type = SingleValue; value.v = NA_VALUE; unknown_base_type.insert(field.num); size = field.size; } // Quick fix : we need to support multivalues if (size < field.size) { if (FIT_DEBUG) { printf( " warning : size=%d for size=%d (num=%d)\n", field.size, field.type, field.num); } read_unknown( field.size-size, &count ); } values.push_back(value); if (FIT_DEBUG) { printf( " field: type=%d num=%d ", field.type, field.num); if (value.type == SingleValue) printf( "value=%lld\n", value.v ); else if (value.type == DoubleValue) printf( "value=%lld value2=%lld\n", value.v, value.v2 ); else if (value.type == StringValue) printf( "salue=%s\n",value.s.c_str() ); } } // 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 0: decodeFileId(def, time_offset, values); break; case 19: decodeLap(def, time_offset, values); break; case RECORD_TYPE: decodeRecord(def, time_offset, values); break; case 21: decodeEvent(def, time_offset, values); break; case 23: //decodeDeviceInfo(def, time_offset, values); break; /* device info */ case 18: decodeSession(def, time_offset, values); break; /* session */ case 101: /* lap swimming length */ errors << "Unsupported Lap Swimming FIT File - Use .tcx or .pwx formats"; stop = true; break; case 2: /* DEVICE_SETTINGS */ case 3: /* USER_PROFILE */ case 7: /* ZONES_TARGET12 */ case 8: /* HR_ZONE */ case 9: /* POWER_ZONE */ case 13: /* unknown */ case 22: /* undocumented */ case 72: /* undocumented - new for garmin 800*/ case 34: /* activity */ case 49: /* file creator */ case 79: /* unknown */ case 104: /* battery */ case 113: /* unknown */ case 125: /* unknown */ case 128: /* unknown */ case 147: /* unknown */ 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->setRecIntSecs(1.0); // this is a terrible assumption! if (!file.open(QIODevice::ReadOnly)) { delete rideFile; return NULL; } int data_size = 0; 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); file.close(); delete rideFile; return NULL; } 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; // 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"; file.close(); delete rideFile; return NULL; } fit_str[4] = '\0'; if (strcmp(fit_str, ".FIT") != 0) { errors << QString("bad header, expected \".FIT\" but got \"%1\"").arg(fit_str); file.close(); delete rideFile; return NULL; } // read the rest of the header if (header_size == 14) read_uint16(false); } catch (TruncatedRead &e) { errors << "truncated file body"; return NULL; } int bytes_read = 0; bool stop = false; bool truncated = false; 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; } } 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); file.close(); return rideFile; } } }; RideFile *FitFileReader::openRideFile(QFile &file, QStringList &errors, QList*) const { QSharedPointer state(new FitFileReaderState(file, errors)); return state->run(); } // vi:expandtab tabstop=4 shiftwidth=4