From 08a23c6faaeecd00a2c84ae1d29dbe4267ce1391 Mon Sep 17 00:00:00 2001 From: Mark Rages Date: Sat, 5 Sep 2009 17:17:25 -0500 Subject: [PATCH] Add support for Quarq ANT+ log import. The Quarq ANT+ log contains a hex dump of raw ANT+ messages. This importer uses the closed-source program "qollector_interpret" to convert the ANT+ log file into an XML format, then parses that into a RideFile. qollector_interpret binaries for several platforms may be downloaded from http://opensource.quarq.us/qollector_interpret If the qollector_interpret program is not available, the menu selection for importing Quarq ANT+ will not appear, nor will .qla files be imported. --- src/MainWindow.cpp | 61 +++++++++++++++++ src/MainWindow.h | 1 + src/QuarqParser.cpp | 128 +++++++++++++++++++++++++++++++++++ src/QuarqParser.h | 68 +++++++++++++++++++ src/QuarqRideFile.cpp | 153 ++++++++++++++++++++++++++++++++++++++++++ src/QuarqRideFile.h | 32 +++++++++ src/src.pro | 4 ++ 7 files changed, 447 insertions(+) create mode 100644 src/QuarqParser.cpp create mode 100644 src/QuarqParser.h create mode 100644 src/QuarqRideFile.cpp create mode 100644 src/QuarqRideFile.h diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 5df1f59d9..39931b6fd 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -28,6 +28,7 @@ #include "PowerHist.h" #include "RideItem.h" #include "RideFile.h" +#include "QuarqRideFile.h" #include "RideMetric.h" #include "Settings.h" #include "TimeUtils.h" @@ -606,6 +607,9 @@ MainWindow::MainWindow(const QDir &home) : SLOT (importPolar())); rideMenu->addAction(tr("&Import from WKO..."), this, SLOT (importWKO())); + if (quarqInterpreterInstalled()) + rideMenu->addAction(tr("&Import from Quarq ANT+ log..."), this, + SLOT (importQuarq())); rideMenu->addAction(tr("Find &best intervals..."), this, SLOT(findBestIntervals()), tr ("Ctrl+B")); rideMenu->addAction(tr("Split &ride..."), this, @@ -1137,6 +1141,63 @@ MainWindow::importPolar() } } +void +MainWindow::importQuarq() +{ + QVariant lastDirVar = settings->value(GC_SETTINGS_LAST_IMPORT_PATH); + QString lastDir = (lastDirVar != QVariant()) + ? lastDirVar.toString() : QDir::homePath(); + QStringList fileNames = QFileDialog::getOpenFileNames( + this, tr("Import Ant"), lastDir, + tr("Quarq ANT+ Format (*.qla)")); + if (!fileNames.isEmpty()) { + lastDir = QFileInfo(fileNames.front()).absolutePath(); + settings->setValue(GC_SETTINGS_LAST_IMPORT_PATH, lastDir); + } + QStringList fileNamesCopy = fileNames; + QStringListIterator i(fileNames); + while (i.hasNext()) { + QString fileName = i.next(); + QFile file(fileName); + QStringList errors; + + boost::scoped_ptr ride( + RideFileFactory::instance().openRideFile(file, errors)); + + if (!ride || !errors.empty()) { + QString all = (ride + ? tr("Non-fatal problem(s) opening %1:") + : tr("Fatal problem(s) opening %1:")).arg(fileName); + QStringListIterator i(errors); + while (i.hasNext()) + all += "\n" + i.next(); + if (ride) + QMessageBox::warning(this, tr("Open Warning"), all); + else { + QMessageBox::critical(this, tr("Open Error"), all); + return; + } + } + + QChar zero = QLatin1Char('0'); + QString name = QString("%1_%2_%3_%4_%5_%6.qla") + .arg(ride->startTime().date().year(), 4, 10, zero) + .arg(ride->startTime().date().month(), 2, 10, zero) + .arg(ride->startTime().date().day(), 2, 10, zero) + .arg(ride->startTime().time().hour(), 2, 10, zero) + .arg(ride->startTime().time().minute(), 2, 10, zero) + .arg(ride->startTime().time().second(), 2, 10, zero); + + if (!file.copy(home.absolutePath() + "/" + name)) { + QMessageBox::critical(this, tr("Copy Error"), + tr("Couldn't copy %1").arg(fileName+" to "+home.absolutePath()+"/"+name)); + return; + } + + addRide(name); + } +} + void MainWindow::findBestIntervals() { diff --git a/src/MainWindow.h b/src/MainWindow.h index 46dfd234f..6bac5a564 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -68,6 +68,7 @@ class MainWindow : public QMainWindow void importTCX(); void importWKO(); void importPolar(); + void importQuarq(); void findBestIntervals(); void splitRide(); void deleteRide(); diff --git a/src/QuarqParser.cpp b/src/QuarqParser.cpp new file mode 100644 index 000000000..28faa6da5 --- /dev/null +++ b/src/QuarqParser.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2009 Mark Rages (mark@quarq.us) + * + * 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 +#include +#include +#include "QuarqParser.h" + +QuarqParser::QuarqParser (RideFile* rideFile) + : rideFile(rideFile), + version(""), + km(0), + watts(0), + cad(0), + hr(0), + initial_seconds(-1.0), + seconds_from_start(0), + kph(0), + nm(0) +{ + ; +} + +// implement sample-and-hold resampling + +#define SAMPLE_INTERVAL 1.0 // seconds + +void +QuarqParser::incrementTime( const double new_time ) +{ + if (initial_seconds < 0.0) { + initial_seconds = new_time; + } + + float time_diff = new_time - initial_seconds; + + while (time_diff > seconds_from_start) { + + rideFile->appendPoint(seconds_from_start, cad, hr, km, + kph, nm, watts, 0, 9, 0); + + seconds_from_start += SAMPLE_INTERVAL; + } + time.setTime_t(new_time); +} + +bool +QuarqParser::startElement( const QString&, const QString&, + const QString& qName, + const QXmlAttributes& qAttributes) +{ + buf.clear(); + + if (qName == "Qollector") { + version = qAttributes.value("version"); + + // reset the timer for a new tag + seconds_from_start = 0.0; + initial_seconds = -1; + + return TRUE; + } + +#define CheckQuarqXml(name,unit,dest) do { \ + if (qName== #name) { \ + QString name = qAttributes.value( #unit ); \ + QString timestamp = qAttributes.value("timestamp"); \ + \ + if ((! name.isEmpty()) && (!timestamp.isEmpty()) && \ + ( name.toLower() != "nan")) { \ + dest = name.toDouble(); \ + incrementTime(timestamp.toDouble()); \ + } \ + return TRUE; \ + } \ + } while (0); + + CheckQuarqXml(Cadence, RPM, cad ); + CheckQuarqXml(Power, Watts, watts ); + CheckQuarqXml(HeartRate, BPM, hr ); + // clearly bogus, equating RPM to kph. + // Unless you have an 18 foot wheel, by chance + CheckQuarqXml(Speed, RPM, kph ); + +#undef CheckQuarqXml + + // default case + + // only print the first time and unknown happens + if (!unknown_keys[qName]++) + std::cerr << "Unknown Element " << qPrintable(qName) << std::endl; + return TRUE; +} + +bool +QuarqParser::endElement( const QString&, const QString&, const QString& qName) +{ + + // flush one last data point + if (qName == "Qollector") { + rideFile->appendPoint(seconds_from_start, cad, hr, km, + kph, nm, watts, 0, 0, 0); + } + + return TRUE; +} + +bool +QuarqParser::characters( const QString& str ) +{ + buf += str; + return TRUE; +} diff --git a/src/QuarqParser.h b/src/QuarqParser.h new file mode 100644 index 000000000..212fbcaf9 --- /dev/null +++ b/src/QuarqParser.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2009 Mark Rages (mark@quarq.us) + * + * 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 _QuarqParser_h +#define _QuarqParser_h + +#include "RideFile.h" +#include +#include +#include +#include +#include + +class QuarqParser : public QXmlDefaultHandler +{ +public: + QuarqParser(RideFile* rideFile); + + bool startElement( const QString&, const QString&, const QString&, + const QXmlAttributes& ); + bool endElement( const QString&, const QString&, const QString& ); + + bool characters( const QString& ); + +private: + + void incrementTime( const double new_time ) ; + + RideFile* rideFile; + + QString buf; + + QString version; + + QDateTime time; + double km; + + double watts; + double cad; + double hr; + + double initial_seconds; + double seconds_from_start; + + double kph; + double nm; + + QHash unknown_keys; + +}; + +#endif // _QuarqParser_h + diff --git a/src/QuarqRideFile.cpp b/src/QuarqRideFile.cpp new file mode 100644 index 000000000..a59a20617 --- /dev/null +++ b/src/QuarqRideFile.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2009 Mark Rages (mark@quarq.us) + * + * 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 "QuarqRideFile.h" +#include "QuarqParser.h" +#include +#include + +static QString installed_path = ""; + + +QProcess *getInterpreterProcess( QString path ) { + + QProcess *antProcess; + + antProcess = new QProcess( ); + antProcess->start( path ); + + if (!antProcess->waitForStarted()) { + delete antProcess; + antProcess = NULL; + } + + return antProcess; +} + +/* + Thanks to ANT+ nondisclosure agreements, the Quarq ANT+ + interpretation code lives in a closed source binary. It takes the + log on stdin and writes XML to stdout. + + If the binary is not available, no Quarq ANT+ capability is shown + in the menu, nor are any ANT+ files opened. + + QProcess note: + + It turns out that the interpreter must actually be opened and + executed before we can be sure it's there. Checking the return + value of start() isn't sufficient. On my Linux system, start() + returns true upon opening the OS X build of qollector_interpret. + + */ + + +bool quarqInterpreterInstalled( void ) { + + static bool checkedInstallation = false; + static bool installed; + + if (!checkedInstallation) { + + QString interpreterPath="/usr/local/bin/qollector_interpret/build-"; + QStringList executables; + executables << "linux-i386/qollector_interpret"; + executables << "osx-ppc-i386/qollector_interpret"; + executables << "win32/qollector_interpret.exe"; + + for ( QStringList::Iterator ex = executables.begin(); ex != executables.end(); ++ex ) { + + QProcess *antProcess = getInterpreterProcess( interpreterPath + *ex ); + + if (NULL == antProcess) { + installed = false; + } else { + + antProcess->closeWriteChannel(); + antProcess->waitForFinished(-1); + + installed=((QProcess::NormalExit == antProcess->exitStatus()) && + (0 == antProcess->exitCode())); + + delete antProcess; + + if (installed) { + installed_path = interpreterPath + *ex; + break; + } + } + } + + if (!installed) + std::cerr << "Cannot open qollector_interpret program, available from http://opensource.quarq.us/qollector_interpret." << std::endl; + + checkedInstallation = true; + } + + return installed; +} + +static int antFileReaderRegistered = + quarqInterpreterInstalled() ? RideFileFactory::instance().registerReader("qla", new QuarqFileReader()) : 0; + +RideFile *QuarqFileReader::openRideFile(QFile &file, QStringList &errors) const +{ + (void) errors; + RideFile *rideFile = new RideFile(); + rideFile->setDeviceType("Quarq Qollector"); + + QuarqParser handler(rideFile); + + QProcess *antProcess = getInterpreterProcess( installed_path ); + + assert(antProcess); + + QXmlInputSource source (antProcess); + QXmlSimpleReader reader; + reader.setContentHandler (&handler); + + // this could done be a loop to "save memory." + file.open(QIODevice::ReadOnly); + antProcess->write(file.readAll()); + antProcess->closeWriteChannel(); + antProcess->waitForFinished(-1); + + assert(QProcess::NormalExit == antProcess->exitStatus()); + assert(0 == antProcess->exitCode()); + + reader.parse(source); + + reader.parseContinue(); + + QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_" + "(\\d\\d)_(\\d\\d)_(\\d\\d)\\.qla$"); + if (rideTime.indexIn(file.fileName()) >= 0) { + QDateTime datetime(QDate(rideTime.cap(1).toInt(), + rideTime.cap(2).toInt(), + rideTime.cap(3).toInt()), + QTime(rideTime.cap(4).toInt(), + rideTime.cap(5).toInt(), + rideTime.cap(6).toInt())); + rideFile->setStartTime(datetime); + } + + delete antProcess; + + return rideFile; +} + diff --git a/src/QuarqRideFile.h b/src/QuarqRideFile.h new file mode 100644 index 000000000..700e8f90c --- /dev/null +++ b/src/QuarqRideFile.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2009 Mark Rages (mark@quarq.us) + * + * 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 _QuarqRideFile_h +#define _QuarqRideFile_h + +#include "RideFile.h" +#include + +bool quarqInterpreterInstalled( void ); + +struct QuarqFileReader : public RideFileReader { + virtual RideFile *openRideFile(QFile &file, QStringList &errors) const; +}; + +#endif // _QuarqRideFile_h + diff --git a/src/src.pro b/src/src.pro index 117eaacee..1ba0674ac 100644 --- a/src/src.pro +++ b/src/src.pro @@ -59,6 +59,8 @@ HEADERS += \ SrmRideFile.h \ TcxParser.h \ TcxRideFile.h \ + QuarqParser.h \ + QuarqRideFile.h \ TimeUtils.h \ ConfigDialog.h \ DatePickerDialog.h \ @@ -104,6 +106,8 @@ SOURCES += \ SrmRideFile.cpp \ TcxParser.cpp \ TcxRideFile.cpp \ + QuarqParser.cpp \ + QuarqRideFile.cpp \ TimeUtils.cpp \ BasicRideMetrics.cpp \ BikeScore.cpp \