From 17019ff00ad5d3a2e54f1a4e47746c0ecc4d5e0e Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Date: Fri, 26 Jun 2015 21:01:25 -0300 Subject: [PATCH] Add support for Lap Swimming FIT files The length message is used to generate 1 sample per second with speed, cadence and distance data. One test file is included. --- src/FitRideFile.cpp | 152 ++++++++++++++++++++-- test/rides/AleV_Swim_2015-02-09-16-15.fit | Bin 0 -> 8311 bytes 2 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 test/rides/AleV_Swim_2015-02-09-16-15.fit diff --git a/src/FitRideFile.cpp b/src/FitRideFile.cpp index e044f9e31..8f26718d7 100644 --- a/src/FitRideFile.cpp +++ b/src/FitRideFile.cpp @@ -18,6 +18,7 @@ #include "FitRideFile.h" #include "Settings.h" +#include "Units.h" #include #include #include @@ -28,7 +29,7 @@ #include #include #include - +#include #define FIT_DEBUG false // debug traces #define RECORD_TYPE 20 @@ -487,7 +488,6 @@ struct FitFileReaderState 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)); } @@ -648,7 +648,7 @@ struct FitFileReaderState unknown_record_fields.insert(field.num); } } - if (time == last_time) + 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. @@ -683,7 +683,6 @@ struct FitFileReaderState // 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; @@ -787,11 +786,146 @@ struct FitFileReaderState leftTopDeathCenter, rightTopDeathCenter, leftBottomDeathCenter, rightBottomDeathCenter, leftTopPeakPowerPhase, rightTopPeakPowerPhase, leftBottomPeakPowerPhase, rightBottomPeakPowerPhase, smO2, tHb, rvert, rcad, rcontact, 0.0, interval); - last_time = time; last_distance = km; } + void decodeLength(const FitDefinition &def, int time_offset, const std::vector values) { + time_t time = 0; + if (time_offset > 0) + time = last_time + time_offset; + double cad = 0, km = 0, kph = 0; + + bool length_type = false; + 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) qDebug() << " event:" << value; + break; + case 1: // event type + if (FIT_DEBUG) 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; + break; + case 3: // total elapsed time + length_duration = value / 1000; + break; + case 4: // total timer time + if (FIT_DEBUG) qDebug() << " total_timer_time:" << value; + break; + case 5: // total strokes + if (FIT_DEBUG) qDebug() << " total_strokes:" << value; + break; + case 6: // avg speed + kph = value * 3.6 / 1000.0; + break; + case 7: // swim stroke + if (FIT_DEBUG) qDebug() << " swim_stroke:" << value; + break; + case 9: // cadence + cad = value; + break; + case 11: // total_calories + if (FIT_DEBUG) qDebug() << " total_calories:" << value; + break; + case 12: // length type: 0-rest, 1-strokes + length_type = value; + break; + case 254: // message_index + if (FIT_DEBUG) qDebug() << " message_index:" << value; + break; + default: + unknown_record_fields.insert(field.num); + } + } + + // 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); + } + + double secs = time - start_time; + + // Normalize distance for the most common pool lengths, + // this is a hack to avoid the need for a double pass since + // pool_length comes in Session message at the end of the file. + double pool_length = kph*length_duration/3600; + if (fabs(pool_length - 0.050) < 0.004) pool_length = 0.050; + 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; + + km = last_distance + pool_length; + if ((secs > last_time + 1) && (isGarminSmartRecording.toInt() != 0) && (secs - last_time < 10*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::NoTemp, + 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.0, 0); + } + last_time += deltaSecs; + } + double deltaSecs = length_duration; + double deltaDist = km - last_distance; + + // only fill 10x the maximal smart recording gap defined + // in preferences - we don't want to crash / stall on bad + // or corrupt files + if ((isGarminSmartRecording.toInt() != 0) && deltaSecs > 0 && deltaSecs < 10*GarminHWM.toInt()) { + + for (int i = 1; i <= deltaSecs; i++) { + double weight = i /deltaSecs; + rideFile->appendPoint( + last_time + (deltaSecs * weight), cad, 0.0, + last_distance + (deltaDist * weight), + kph, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, RideFile::NoTemp, + 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.0, 0); + } + last_time += deltaSecs; + last_distance += deltaDist; + } + } + int read_record(bool &stop, QStringList &errors) { stop = false; int count = 0; @@ -935,16 +1069,14 @@ struct FitFileReaderState 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 101: decodeLength(def, time_offset, values); break; /* lap swimming */ case 2: /* DEVICE_SETTINGS */ case 3: /* USER_PROFILE */ case 7: /* ZONES_TARGET12 */ case 8: /* HR_ZONE */ case 9: /* POWER_ZONE */ + case 12: /* SPORT */ case 13: /* unknown */ case 22: /* undocumented */ case 72: /* undocumented - new for garmin 800*/ @@ -955,6 +1087,8 @@ struct FitFileReaderState case 113: /* unknown */ case 125: /* unknown */ case 128: /* unknown */ + case 140: /* unknown */ + case 141: /* unknown */ case 147: /* unknown */ break; diff --git a/test/rides/AleV_Swim_2015-02-09-16-15.fit b/test/rides/AleV_Swim_2015-02-09-16-15.fit new file mode 100644 index 0000000000000000000000000000000000000000..a40ee9120304c1247aac15810451387c2d08efe4 GIT binary patch literal 8311 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 literal 0 HcmV?d00001