mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 08:08:42 +00:00
read Garmin FIT files
There is still the mystery of what global message type #22 is, but other than that concern, this code seems to work pretty well now.
This commit is contained in:
398
src/FitRideFile.cpp
Normal file
398
src/FitRideFile.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* 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 <qsharedpointer>
|
||||
#include <qmap>
|
||||
#include <qset>
|
||||
#include <stdio.h>
|
||||
|
||||
static int fitFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader(
|
||||
"fit", "Garmin FIT", new FitFileReader());
|
||||
|
||||
/*static const char *base_type_names[] = {
|
||||
"enum", // 0
|
||||
"int8", // 1
|
||||
"uint8", // 2
|
||||
"int16", // 3
|
||||
"uint16", // 4
|
||||
"int32", // 5
|
||||
"uint32", // 6
|
||||
"string", // 7
|
||||
"float32", // 8
|
||||
"float64", // 9
|
||||
"uint8z", // 10
|
||||
"uint16z", // 11
|
||||
"uint32z", // 12
|
||||
"byte" // 13
|
||||
}; */
|
||||
static const int base_type_names_size(14);
|
||||
|
||||
static const QDateTime qbase_time(QDate(1989, 12, 31), QTime(0, 0, 0), Qt::UTC);
|
||||
|
||||
struct FitField {
|
||||
int num;
|
||||
int type;
|
||||
int size; // in bytes
|
||||
};
|
||||
|
||||
struct FitDefinition {
|
||||
int global_msg_num;
|
||||
std::vector<FitField> fields;
|
||||
};
|
||||
|
||||
struct FitFileReaderState
|
||||
{
|
||||
static QMap<int,QString> global_msg_names;
|
||||
|
||||
QFile &file;
|
||||
QStringList &errors;
|
||||
RideFile *rideFile;
|
||||
time_t start_time;
|
||||
unsigned last_time;
|
||||
QMap<int, FitDefinition> local_msg_types;
|
||||
QSet<int> unknown_record_fields, unknown_global_msg_nums;
|
||||
int interval;
|
||||
int devices;
|
||||
|
||||
FitFileReaderState(QFile &file, QStringList &errors) :
|
||||
file(file), errors(errors), rideFile(NULL), start_time(0),
|
||||
last_time(0), interval(0), devices(0)
|
||||
{
|
||||
if (global_msg_names.isEmpty()) {
|
||||
global_msg_names.insert(0, "file_id");
|
||||
global_msg_names.insert(18, "session");
|
||||
global_msg_names.insert(19, "lap");
|
||||
global_msg_names.insert(20, "record");
|
||||
global_msg_names.insert(21, "event");
|
||||
global_msg_names.insert(22, "undocumented");
|
||||
global_msg_names.insert(23, "device_info");
|
||||
global_msg_names.insert(34, "activity");
|
||||
global_msg_names.insert(49, "file_creator");
|
||||
}
|
||||
}
|
||||
|
||||
struct TruncatedRead {};
|
||||
|
||||
int read_byte(int *count = NULL) {
|
||||
char c;
|
||||
if (file.read(&c, 1) != 1)
|
||||
throw TruncatedRead();
|
||||
if (count)
|
||||
(*count) += 1;
|
||||
return 0xff & c;
|
||||
}
|
||||
|
||||
int read_short(int *count = NULL) {
|
||||
uint16_t s;
|
||||
if (file.read(reinterpret_cast<char*>(&s), 2) != 2)
|
||||
throw TruncatedRead();
|
||||
if (count)
|
||||
(*count) += 2;
|
||||
return 0xffff & s;
|
||||
}
|
||||
|
||||
int read_long(int *count = NULL) {
|
||||
uint32_t l;
|
||||
if (file.read(reinterpret_cast<char*>(&l), 4) != 4)
|
||||
throw TruncatedRead();
|
||||
if (count)
|
||||
(*count) += 4;
|
||||
return l;
|
||||
}
|
||||
|
||||
void decodeFileId(const FitDefinition &def, int, const std::vector<int> values) {
|
||||
int i = 0;
|
||||
int manu = -1, prod = -1;
|
||||
foreach(const FitField &field, def.fields) {
|
||||
int value = values[i++];
|
||||
switch (field.num) {
|
||||
case 1: manu = value; break;
|
||||
case 2: prod = value; break;
|
||||
default: ; // do nothing
|
||||
}
|
||||
}
|
||||
if (manu == 1) {
|
||||
switch (prod) {
|
||||
case 717: 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: rideFile->setDeviceType("Garmin Edge 500"); break;
|
||||
default: rideFile->setDeviceType(QString("Unknown Garmin Device %1").arg(prod));
|
||||
}
|
||||
}
|
||||
else {
|
||||
rideFile->setDeviceType(QString("Unknown FIT Device %1:%2").arg(manu).arg(prod));
|
||||
}
|
||||
}
|
||||
|
||||
void decodeLap(const FitDefinition &def, int time_offset, const std::vector<int> values) {
|
||||
time_t time = 0;
|
||||
if (time_offset > 0)
|
||||
time = last_time + time_offset;
|
||||
int i = 0;
|
||||
time_t this_start_time = 0;
|
||||
++interval;
|
||||
foreach(const FitField &field, def.fields) {
|
||||
int value = values[i++];
|
||||
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)
|
||||
errors << QString("lap %1 has no start time").arg(interval);
|
||||
else {
|
||||
rideFile->addInterval(this_start_time - start_time, time - start_time,
|
||||
QString("%1").arg(interval));
|
||||
}
|
||||
last_time = time;
|
||||
}
|
||||
|
||||
void decodeRecord(const FitDefinition &def, int time_offset, const std::vector<int> values) {
|
||||
time_t time = 0;
|
||||
if (time_offset > 0)
|
||||
time = last_time + time_offset;
|
||||
double alt = 0, cad = 0, km = 0, grade = 0, hr = 0, lat = 0, lng = 0;
|
||||
double resistance = 0, kph = 0, temperature = 0, time_from_course = 0, watts = 0;
|
||||
int lati = 0x7fffffff, lngi = 0x7fffffff;
|
||||
int i = 0;
|
||||
foreach(const FitField &field, def.fields) {
|
||||
int value = values[i++];
|
||||
switch (field.num) {
|
||||
case 253: time = value + qbase_time.toTime_t(); break;
|
||||
case 0: lati = value; break;
|
||||
case 1: lngi = value; break;
|
||||
case 2: alt = (value == 0xffff) ? 0 : (value / 5.0 - 500.0); break;
|
||||
case 3: hr = value; break;
|
||||
case 4: cad = (value == 0xff) ? 0 : value; break;
|
||||
case 5: km = ((uint32_t) value == 0xffffffff) ? 0 : value / 100000.0; break;
|
||||
case 6: kph = (value == 0xffff) ? 0 : value * 3.6 / 1000.0; break;
|
||||
case 7: watts = (value == 0xffff) ? 0 : value; break;
|
||||
case 9: grade = (value == 0x7fff) ? 0 : value / 100.0; break;
|
||||
case 10: resistance = (value == 0xff) ? 0 : value; break;
|
||||
case 11: time_from_course = (value == 0x7fffffff) ? 0 : value / 1000.0; break;
|
||||
case 13: temperature = (value == 0x7f) ? 0 : value; break;
|
||||
default: unknown_record_fields.insert(field.num);
|
||||
}
|
||||
}
|
||||
if (lati != 0x7fffffff && lngi != 0x7fffffff) {
|
||||
lat = lati * 180.0 / 0x7fffffff;
|
||||
lng = lngi * 180.0 / 0x7fffffff;
|
||||
}
|
||||
if (start_time == 0) {
|
||||
start_time = time - 1; // XXX: recording interval?
|
||||
QDateTime t;
|
||||
t.setTime_t(start_time);
|
||||
rideFile->setStartTime(t);
|
||||
}
|
||||
last_time = time;
|
||||
double secs = time - start_time;
|
||||
double nm = 0.0; // XXX
|
||||
double headwind = 0.0;
|
||||
int interval = 0; // XXX
|
||||
rideFile->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, lng, lat, headwind, interval);
|
||||
}
|
||||
|
||||
int read_record(bool &stop, QStringList &errors) {
|
||||
stop = false;
|
||||
int count = 0;
|
||||
int header_byte = read_byte(&count);
|
||||
if (!(header_byte & 0x80) && (header_byte & 0x40)) {
|
||||
// Definition record
|
||||
int local_msg_type = header_byte & 0xf;
|
||||
int reserved = read_byte(&count); (void) reserved; // unused
|
||||
int architecture = read_byte(&count);
|
||||
int global_msg_num = read_short(&count);
|
||||
if (!global_msg_names.contains(global_msg_num)) {
|
||||
errors << QString("unknown global_msg_num %1").arg(global_msg_num);
|
||||
stop = true;
|
||||
return count;
|
||||
}
|
||||
/*printf("definition: local type %d is %s\n",
|
||||
local_msg_type,
|
||||
global_msg_names[global_msg_num].toAscii().constData());*/
|
||||
if (architecture != 0) {
|
||||
errors << QString("unsupported architecture %1").arg(architecture);
|
||||
stop = true;
|
||||
return count;
|
||||
}
|
||||
int num_fields = read_byte(&count);
|
||||
// printf(", %d num_fields:\n", num_fields);
|
||||
local_msg_types.insert(local_msg_type, FitDefinition());
|
||||
FitDefinition &def = local_msg_types[local_msg_type];
|
||||
def.global_msg_num = global_msg_num;
|
||||
for (int i = 0; i < num_fields; ++i) {
|
||||
def.fields.push_back(FitField());
|
||||
FitField &field = def.fields.back();
|
||||
int field_def_num = read_byte(&count);
|
||||
int field_size = read_byte(&count);
|
||||
field.size = field_size;
|
||||
int base_type = read_byte(&count);
|
||||
int base_type_num = base_type & 0x1f;
|
||||
if (base_type_num >= base_type_names_size) {
|
||||
errors << QString("unknown base type %1").arg(base_type_num);
|
||||
stop = true;
|
||||
return count;
|
||||
}
|
||||
field.type = base_type_num;
|
||||
field.num = field_def_num;
|
||||
/*printf(" field %d: %d bytes, num %d, type %s, %s endianness\n",
|
||||
i, field_size, field_def_num, base_type_names[base_type_num],
|
||||
(base_type & 0x80) ? "with" : "without");*/
|
||||
}
|
||||
}
|
||||
else {
|
||||
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("unrecognized local type %1").arg(local_msg_type);
|
||||
stop = true;
|
||||
return count;
|
||||
}
|
||||
const FitDefinition &def = local_msg_types[local_msg_type];
|
||||
std::vector<int> values;
|
||||
foreach(const FitField &field, def.fields) {
|
||||
int v;
|
||||
switch (field.size) {
|
||||
case 1: v = read_byte(&count); break;
|
||||
case 2: v = read_short(&count); break;
|
||||
case 4: v = read_long(&count); break;
|
||||
default:
|
||||
errors << QString("unsupported field size %1").arg(field.size);
|
||||
stop = true;
|
||||
return count;
|
||||
}
|
||||
values.push_back(v);
|
||||
}
|
||||
// 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 20: decodeRecord(def, time_offset, values); break;
|
||||
case 23: /* device info */
|
||||
case 18: /* session */
|
||||
case 21: /* event */
|
||||
case 22: /* undocumented */
|
||||
case 34: /* activity */
|
||||
case 49: /* file creator */
|
||||
break;
|
||||
default:
|
||||
/*
|
||||
int i = 0;
|
||||
printf("----------------\n");
|
||||
foreach(const FitField &field, def.fields) {
|
||||
int value = values[i++];
|
||||
printf("msg %d: field %d = %d\n", def.global_msg_num, field.num, value);
|
||||
}
|
||||
*/
|
||||
unknown_global_msg_nums.insert(def.global_msg_num);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
RideFile * run() {
|
||||
rideFile = new RideFile;
|
||||
rideFile->setDeviceType("Garmin FIT"); // XXX: read from device msg?
|
||||
rideFile->setRecIntSecs(1.0); // XXX: always?
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
}
|
||||
int header_size = read_byte();
|
||||
if (header_size != 12) {
|
||||
errors << QString("bad header size: %1").arg(header_size);
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
}
|
||||
int protocol_version = read_byte();
|
||||
(void) protocol_version;
|
||||
int profile_version = read_short(); // not sure what to do with this
|
||||
(void) profile_version;
|
||||
int data_size = read_long(); // always little endian
|
||||
char fit_str[5];
|
||||
if (file.read(fit_str, 4) != 4) {
|
||||
errors << "truncated header";
|
||||
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);
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
}
|
||||
int bytes_read = 0;
|
||||
bool stop = false;
|
||||
try {
|
||||
while (!stop && (bytes_read < data_size))
|
||||
bytes_read += read_record(stop, errors);
|
||||
}
|
||||
catch (TruncatedRead &e) {
|
||||
errors << "truncated file body";
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
}
|
||||
if (stop) {
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
}
|
||||
else {
|
||||
int crc = read_short();
|
||||
(void) crc;
|
||||
foreach(int num, unknown_global_msg_nums) {
|
||||
if (global_msg_names.contains(num)) {
|
||||
errors << ("unsupported global message \"" + global_msg_names[num] +
|
||||
"\" (%1); ignoring it").arg(num);
|
||||
}
|
||||
else {
|
||||
errors << QString("unsupported global message number %1; ignoring it").arg(num);
|
||||
}
|
||||
}
|
||||
foreach(int num, unknown_record_fields)
|
||||
errors << QString("unknown record field %1; ignoring it").arg(num);
|
||||
return rideFile;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QMap<int,QString> FitFileReaderState::global_msg_names;
|
||||
|
||||
RideFile *FitFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
QSharedPointer<FitFileReaderState> state(new FitFileReaderState(file, errors));
|
||||
return state->run();
|
||||
}
|
||||
|
||||
29
src/FitRideFile.h
Normal file
29
src/FitRideFile.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010 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
|
||||
*/
|
||||
|
||||
#ifndef _FitRideFile_h
|
||||
#define _FitRideFile_h
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
struct FitFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
};
|
||||
|
||||
#endif // _FitRideFile_h
|
||||
|
||||
@@ -73,6 +73,7 @@ HEADERS += \
|
||||
DownloadRideDialog.h \
|
||||
ErgFile.h \
|
||||
ErgFilePlot.h \
|
||||
FitRideFile.h \
|
||||
GcRideFile.h \
|
||||
GoogleMapControl.h \
|
||||
HistogramWindow.h \
|
||||
@@ -165,6 +166,7 @@ SOURCES += \
|
||||
DownloadRideDialog.cpp \
|
||||
ErgFile.cpp \
|
||||
ErgFilePlot.cpp \
|
||||
FitRideFile.cpp \
|
||||
GcRideFile.cpp \
|
||||
GoogleMapControl.cpp \
|
||||
HistogramWindow.cpp \
|
||||
|
||||
Reference in New Issue
Block a user