From 56612aec060a2ff70326b3392de3c43a6979bcc6 Mon Sep 17 00:00:00 2001 From: profanum429 Date: Tue, 2 Sep 2014 16:15:30 -0500 Subject: [PATCH 01/14] Update for fw 1.0.12 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 46a7d02..38774f1 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ v800_downloader =============== +Update: Release 3 still works with the new 1.0.12 firmware but right after the update you won't see any sessions since when the firmware updates all the sessions on the watch have their full set of data removed and leave only the statistics information behind. Any new activities will be downloadable :) + V800 Downloader is a tool that is used to download sessions from the Polar V800 watch. In normal usage there is no first party way to export data from the V800, but with V800 Downloader + Bipolar (https://www.github.com/pcolby/bipolar) data can be exported from the V800 without using any Polar software. From e0b8736e4073dcf27c5843a0ea25328721f65736 Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Thu, 4 Sep 2014 21:44:00 -0600 Subject: [PATCH 02/14] Added multisport support, forgot about it earlier whoops. --- src/export/v800export.cpp | 80 +++++++++++++++++++++++---------------- src/usb/v800usb.cpp | 50 +++++++++++++++--------- src/usb/v800usb.h | 2 +- 3 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/export/v800export.cpp b/src/export/v800export.cpp index 62df775..256eafd 100644 --- a/src/export/v800export.cpp +++ b/src/export/v800export.cpp @@ -37,45 +37,59 @@ void V800export::export_sessions(QList sessions, unsigned char mode) QSettings settings; QString default_dir = settings.value(tr("default_dir")).toString(); - for(int session_iter = 0; session_iter < sessions.length(); session_iter++) + for(int sessions_iter = 0; sessions_iter < sessions.length(); sessions_iter++) { - if(!make_bipolar_names(sessions[session_iter])) - { - emit export_session_error(sessions[session_iter], RENAME_ERROR); - continue; - } - - QString session_info(QString(tr("%1/%2/v2-users-0000000-training-sessions-%3")).arg(default_dir).arg(sessions[session_iter]).arg(sessions[session_iter])); - polar::v2::TrainingSession parser(session_info); - - if(!parser.parse()) - emit export_session_error(sessions[session_iter], PARSE_ERROR); - - if(mode & TCX_EXPORT) - { - QString tcx(QString(tr("%1/%2.tcx")).arg(default_dir).arg(sessions[session_iter])); - if(!parser.writeTCX(tcx)) - emit export_session_error(sessions[session_iter], TCX_ERROR); - } + QStringList filters; + filters << QString(tr("%1*")).arg(sessions[sessions_iter]); - if(mode & HRM_EXPORT) - { - QString hrm(QString(tr("%1/%2")).arg(default_dir).arg(sessions[session_iter])); - QStringList hrm_out = parser.writeHRM(hrm); - if(hrm_out.length() < 1) - emit export_session_error(sessions[session_iter], HRM_ERROR); - } + QDir filter_dir(default_dir); + filter_dir.setNameFilters(filters); + filter_dir.setFilter(QDir::Dirs); - if(mode & GPX_EXPORT) + int multi_sessions_iter; + QStringList multi_sessions = filter_dir.entryList(); + for(multi_sessions_iter = 0; multi_sessions_iter < multi_sessions.length(); multi_sessions_iter++) { - QString gpx(QString(tr("%1/%2.gpx")).arg(default_dir).arg(sessions[session_iter])); - if(!parser.writeGPX(gpx)) - emit export_session_error(sessions[session_iter], GPX_ERROR); + if(!make_bipolar_names(multi_sessions[multi_sessions_iter])) + { + emit export_session_error(sessions[sessions_iter], RENAME_ERROR); + continue; + } + + QString session_info(QString(tr("%1/%2/v2-users-0000000-training-sessions-%3")).arg(default_dir).arg(multi_sessions[multi_sessions_iter]).arg(multi_sessions[multi_sessions_iter])); + polar::v2::TrainingSession parser(session_info); + + qDebug("Parser: %s", session_info.toUtf8().constData()); + + if(!parser.parse()) + emit export_session_error(sessions[sessions_iter], PARSE_ERROR); + + if(mode & TCX_EXPORT) + { + QString tcx(QString(tr("%1/%2.tcx")).arg(default_dir).arg(multi_sessions[multi_sessions_iter])); + if(!parser.writeTCX(tcx)) + emit export_session_error(sessions[sessions_iter], TCX_ERROR); + } + + if(mode & HRM_EXPORT) + { + QString hrm(QString(tr("%1/%2")).arg(default_dir).arg(multi_sessions[multi_sessions_iter])); + QStringList hrm_out = parser.writeHRM(hrm); + if(hrm_out.length() < 1) + emit export_session_error(sessions[sessions_iter], HRM_ERROR); + } + + if(mode & GPX_EXPORT) + { + QString gpx(QString(tr("%1/%2.gpx")).arg(default_dir).arg(multi_sessions[multi_sessions_iter])); + if(!parser.writeGPX(gpx)) + emit export_session_error(sessions[sessions_iter], GPX_ERROR); + } + + QDir(QString(tr("%1/%2")).arg(default_dir).arg(multi_sessions[multi_sessions_iter])).removeRecursively(); } - QDir(QString(tr("%1/%2")).arg(default_dir).arg(sessions[session_iter])).removeRecursively(); - - emit export_session_done(session_iter, sessions.length()); + emit export_session_done(sessions_iter, sessions.length()); } emit export_sessions_done(); diff --git a/src/usb/v800usb.cpp b/src/usb/v800usb.cpp index d1de711..ad17e97 100755 --- a/src/usb/v800usb.cpp +++ b/src/usb/v800usb.cpp @@ -62,9 +62,9 @@ void V800usb::get_sessions(QList sessions) { QString session; QStringList session_split; - QList files, temp_session_files, temp_files; + QList multi_sports, files, temp_session_files, temp_files; QDateTime session_time; - int session_iter, files_iter; + int session_iter, files_iter, multi_sports_iter, multi_sport_cnt; for(session_iter = 0; session_iter < sessions.length(); session_iter++) { @@ -74,22 +74,36 @@ void V800usb::get_sessions(QList sessions) if(session_split.length() == 2) { - files.clear(); - files = get_v800_data(QString(tr("%1/%2/E/%3/00/")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1])); + multi_sports.clear(); + multi_sports = get_v800_data(QString(tr("%1/%2/E/%3/")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1])); - for(files_iter = 0; files_iter < files.length(); files_iter++) + multi_sport_cnt = 0; + + for(multi_sports_iter = 0; multi_sports_iter < multi_sports.length(); multi_sports_iter++) { - temp_files = get_v800_data(QString(tr("%1/%2/E/%3/00/%4")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1]).arg(files[files_iter])); - if(temp_files.length() == 1) - temp_session_files.append(temp_files[0]); - } + if(multi_sports[multi_sports_iter].contains(tr("/"))) + { + files.clear(); + files = get_v800_data(QString(tr("%1/%2/E/%3/%4/")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1]).arg(multi_sports[multi_sports_iter])); + + for(files_iter = 0; files_iter < files.length(); files_iter++) + { + temp_files = get_v800_data(QString(tr("%1/%2/E/%3/%4/%5")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1]).arg(multi_sports[multi_sports_iter]).arg(files[files_iter]), multi_sport_cnt); + if(temp_files.length() == 1) + temp_session_files.append(temp_files[0]); + } + + temp_files = get_v800_data(QString(tr("%1/%2/E/%3/TSESS.BPB")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1]), multi_sport_cnt); + if(temp_files.length() == 1) + temp_session_files.append(temp_files[0]); + + temp_files = get_v800_data(QString(tr("%1/%2/E/%3/PHYSDATA.BPB")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1]), multi_sport_cnt); + if(temp_files.length() == 1) + temp_session_files.append(temp_files[0]); - temp_files = get_v800_data(QString(tr("%1/%2/E/%3/TSESS.BPB")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1])); - if(temp_files.length() == 1) - temp_session_files.append(temp_files[0]); - temp_files = get_v800_data(QString(tr("%1/%2/E/%3/PHYSDATA.BPB")).arg(tr(V800_ROOT_DIR)).arg(session_split[0]).arg(session_split[1])); - if(temp_files.length() == 1) - temp_session_files.append(temp_files[0]); + multi_sport_cnt++; + } + } QString tag = QDateTime(QDate::fromString(session_split[0], tr("yyyyMMdd")), QTime::fromString(session_split[1], tr("HHmmss"))).toString(tr("yyyyMMddhhmmss")); emit session_done(tag, session_iter, sessions.length()); @@ -159,7 +173,7 @@ void V800usb::get_all_sessions() emit all_sessions(sessions); } -QList V800usb::get_v800_data(QString request, bool debug) +QList V800usb::get_v800_data(QString request, int multi_sport, bool debug) { QList data; QByteArray packet, full; @@ -242,12 +256,12 @@ QList V800usb::get_v800_data(QString request, bool debug) QSettings settings; QString default_dir = settings.value(tr("default_dir")).toString(); - QString raw_dir = (QString(tr("%1/%2")).arg(default_dir).arg(tag)); + QString raw_dir = (QString(tr("%1/%2%3")).arg(default_dir).arg(tag).arg(multi_sport)); QDir(raw_dir).mkpath(raw_dir); QString raw_dest = (QString(tr("%1/%2")).arg(raw_dir).arg(file)); - //qDebug("Path: %s", raw_dest.toUtf8().constData()); + qDebug("Path: %s", raw_dest.toUtf8().constData()); QFile *raw_file; raw_file = new QFile(raw_dest); diff --git a/src/usb/v800usb.h b/src/usb/v800usb.h index 0ff97cd..aa35e47 100755 --- a/src/usb/v800usb.h +++ b/src/usb/v800usb.h @@ -57,7 +57,7 @@ public slots: int is_end(QByteArray packet); QByteArray add_to_full(QByteArray packet, QByteArray full, bool initial_packet, bool final_packet); - QList get_v800_data(QString request, bool debug=false); + QList get_v800_data(QString request, int multi_sport = 0, bool debug=false); void get_all_sessions(); From 7525596548839daffbd53fbcf9659c3e8055c355 Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Fri, 5 Sep 2014 21:11:49 -0600 Subject: [PATCH 03/14] Updated to latest Bipolar build (0.3.1) --- src/bipolar/polar/v2/trainingsession.cpp | 178 ++++++++++++++++------- src/bipolar/polar/v2/trainingsession.h | 38 +++++ src/export/v800export.cpp | 2 + 3 files changed, 166 insertions(+), 52 deletions(-) diff --git a/src/bipolar/polar/v2/trainingsession.cpp b/src/bipolar/polar/v2/trainingsession.cpp index c733247..d1facd7 100644 --- a/src/bipolar/polar/v2/trainingsession.cpp +++ b/src/bipolar/polar/v2/trainingsession.cpp @@ -22,9 +22,7 @@ #include "message.h" #include "types.h" -/* -#include "os/versioninfo.h" -*/ +//#include "os/versioninfo.h" #include #include @@ -52,11 +50,17 @@ namespace polar { namespace v2 { -TrainingSession::TrainingSession(const QString &baseName) : baseName(baseName) +TrainingSession::TrainingSession(const QString &baseName) + : baseName(baseName), hrmOptions(LapNames) { } +int TrainingSession::exerciseCount() const +{ + return (isValid()) ? parsedExercises.count() : -1; +} + /// @see https://github.com/pcolby/bipolar/wiki/Polar-Sport-Types QString TrainingSession::getTcxSport(const quint64 &polarSportValue) { @@ -137,7 +141,7 @@ QString TrainingSession::getTcxSport(const quint64 &polarSportValue) } QMap::ConstIterator iter = map.constFind(polarSportValue); if (iter == map.constEnd()) { - qWarning() << "unknown polar sport value" << polarSportValue; + qWarning() << "Unknown polar sport value" << polarSportValue; } return (iter == map.constEnd()) ? TCX_OTHER : iter.value(); } @@ -311,7 +315,7 @@ QVariantMap TrainingSession::parseCreateExercise(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open exercise-create file" << fileName; + qWarning() << "Failed to open exercise-create file" << fileName; return QVariantMap(); } return parseCreateExercise(file); @@ -408,7 +412,7 @@ QVariantMap TrainingSession::parseCreateSession(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open session-create file" << fileName; + qWarning() << "Failed to open session-create file" << fileName; return QVariantMap(); } return parseCreateSession(file); @@ -458,7 +462,7 @@ QVariantMap TrainingSession::parseLaps(QIODevice &data) const ADD_FIELD_INFO("2/1/1", "hours", Uint32); ADD_FIELD_INFO("2/1/2", "minutes", Uint32); ADD_FIELD_INFO("2/1/3", "seconds", Uint32); - ADD_FIELD_INFO("2/1.4", "milliseconds", Uint32); + ADD_FIELD_INFO("2/1/4", "milliseconds", Uint32); ADD_FIELD_INFO("2/2", "average-duration", EmbeddedMessage); ADD_FIELD_INFO("2/2/1", "hours", Uint32); ADD_FIELD_INFO("2/2/2", "minutes", Uint32); @@ -478,7 +482,7 @@ QVariantMap TrainingSession::parseLaps(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open laps file" << fileName; + qWarning() << "Failed to open laps file" << fileName; return QVariantMap(); } return parseLaps(file); @@ -651,7 +655,7 @@ QVariantMap TrainingSession::parsePhysicalInformation(const QString &fileName) c { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open physical information file" << fileName; + qWarning() << "Failed to open physical information file" << fileName; return QVariantMap(); } return parsePhysicalInformation(file); @@ -689,7 +693,7 @@ QVariantMap TrainingSession::parseRoute(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open route file" << fileName; + qWarning() << "Failed to open route file" << fileName; return QVariantMap(); } return parseRoute(file); @@ -713,7 +717,7 @@ QVariantMap TrainingSession::parseRRSamples(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open rrsamples file" << fileName; + qWarning() << "Failed to open rrsamples file" << fileName; return QVariantMap(); } return parseRRSamples(file); @@ -784,7 +788,7 @@ QVariantMap TrainingSession::parseSamples(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open samples file" << fileName; + qWarning() << "Failed to open samples file" << fileName; return QVariantMap(); } return parseSamples(file); @@ -802,14 +806,14 @@ QVariantMap TrainingSession::parseStatistics(QIODevice &data) const ADD_FIELD_INFO("2/2", "maximum", Float); ADD_FIELD_INFO("3", "cadence", EmbeddedMessage); ADD_FIELD_INFO("3/1", "average", Uint32); - ADD_FIELD_INFO("3/1", "maximum", Uint32); + ADD_FIELD_INFO("3/2", "maximum", Uint32); ADD_FIELD_INFO("4", "altitude", EmbeddedMessage); ADD_FIELD_INFO("4/1", "minimum", Float); ADD_FIELD_INFO("4/2", "average", Float); ADD_FIELD_INFO("4/3", "maximum", Float); ADD_FIELD_INFO("5", "power", EmbeddedMessage); ADD_FIELD_INFO("5/1", "average", Uint32); - ADD_FIELD_INFO("5/1", "maximum", Uint32); + ADD_FIELD_INFO("5/2", "maximum", Uint32); ADD_FIELD_INFO("6", "lr_balance", EmbeddedMessage); ADD_FIELD_INFO("6/1", "average", Float); ADD_FIELD_INFO("7", "temperature", EmbeddedMessage); @@ -820,13 +824,13 @@ QVariantMap TrainingSession::parseStatistics(QIODevice &data) const ADD_FIELD_INFO("8/1", "average", Float); ADD_FIELD_INFO("9", "stride", EmbeddedMessage); ADD_FIELD_INFO("9/1", "average", Uint32); - ADD_FIELD_INFO("9/1", "maximum", Uint32); + ADD_FIELD_INFO("9/2", "maximum", Uint32); ADD_FIELD_INFO("10", "include", EmbeddedMessage); ADD_FIELD_INFO("10/1", "average", Float); - ADD_FIELD_INFO("10/1", "maximum", Float); + ADD_FIELD_INFO("10/2", "maximum", Float); ADD_FIELD_INFO("11", "declince", EmbeddedMessage); ADD_FIELD_INFO("11/1", "average", Float); - ADD_FIELD_INFO("11/1", "maximum", Float); + ADD_FIELD_INFO("11/2", "maximum", Float); ProtoBuf::Message parser(fieldInfo); if (isGzipped(data)) { @@ -841,7 +845,7 @@ QVariantMap TrainingSession::parseStatistics(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open stats file" << fileName; + qWarning() << "Failed to open stats file" << fileName; return QVariantMap(); } return parseStatistics(file); @@ -905,12 +909,54 @@ QVariantMap TrainingSession::parseZones(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "failed to open zones file" << fileName; + qWarning() << "Failed to open zones file" << fileName; return QVariantMap(); } return parseZones(file); } +void TrainingSession::setGpxOption(const GpxOption option, const bool enabled) +{ + if (enabled) { + gpxOptions |= option; + } else { + gpxOptions &= ~option; + } +} + +void TrainingSession::setGpxOptions(const GpxOptions options) +{ + gpxOptions = options; +} + +void TrainingSession::setHrmOption(const HrmOption option, const bool enabled) +{ + if (enabled) { + hrmOptions |= option; + } else { + hrmOptions &= ~option; + } +} + +void TrainingSession::setHrmOptions(const HrmOptions options) +{ + hrmOptions = options; +} + +void TrainingSession::setTcxOption(const TcxOption option, const bool enabled) +{ + if (enabled) { + tcxOptions |= option; + } else { + tcxOptions &= ~option; + } +} + +void TrainingSession::setTcxOptions(const TcxOptions options) +{ + tcxOptions = options; +} + /** * @brief Fetch the first item from a list contained within a QVariant. * @@ -1007,7 +1053,7 @@ bool sensorOffline(const QVariantList &list, const int index) const QVariant endIndex = first(map.value(QLatin1String("start-index"))); if ((!startIndex.canConvert(QMetaType::Int)) || (!endIndex.canConvert(QMetaType::Int))) { - qWarning() << "ignoring invalid 'offline' entry" << entry; + qWarning() << "Ignoring invalid 'offline' entry" << entry; continue; } if ((startIndex.toInt() <= index) && (index <= endIndex.toInt())) { @@ -1039,7 +1085,7 @@ QString TrainingSession::getOutputBaseFileName(const QString &format) if (format.contains(QLatin1String("$userId" )) || format.contains(QLatin1String("$sessionId"))) { if (!inputFileNameParts.exactMatch(inputBaseNameInfo.fileName())) { - qWarning() << "baseName does not match format" << baseName; + qWarning() << "Base name does not match format" << baseName; return QString(); } } @@ -1061,21 +1107,33 @@ QString TrainingSession::getOutputBaseFileName(const QString &format) fileName.replace(QLatin1String("$baseName"), inputBaseNameInfo.fileName()); - if (format.contains(QLatin1String("$date" )) || - format.contains(QLatin1String("$dateUTC")) || - format.contains(QLatin1String("$time" )) || - format.contains(QLatin1String("$timeUTC"))) + if (format.contains(QLatin1String("$date" )) || + format.contains(QLatin1String("$dateUTC" )) || + format.contains(QLatin1String("$dateExt" )) || + format.contains(QLatin1String("$dateExtUTC")) || + format.contains(QLatin1String("$time" )) || + format.contains(QLatin1String("$timeUTC" )) || + format.contains(QLatin1String("$timeExt" )) || + format.contains(QLatin1String("$timeExtUTC"))) { const QDateTime startTime = getDateTime(firstMap(parsedSession.value(QLatin1String("start")))); - fileName.replace(QLatin1String("$dateUTC"), + fileName.replace(QLatin1String("$dateExtUTC"), startTime.toUTC().toString(QLatin1String("yyyy-MM-dd"))); - fileName.replace(QLatin1String("$date"), + fileName.replace(QLatin1String("$dateExt"), startTime.toString(QLatin1String("yyyy-MM-dd"))); - fileName.replace(QLatin1String("$timeUTC"), + fileName.replace(QLatin1String("$dateUTC"), + startTime.toUTC().toString(QLatin1String("yyyyMMdd"))); + fileName.replace(QLatin1String("$date"), + startTime.toString(QLatin1String("yyyyMMdd"))); + fileName.replace(QLatin1String("$timeExtUTC"), startTime.toUTC().toString(QLatin1String("HH:mm:ss"))); - fileName.replace(QLatin1String("$time"), + fileName.replace(QLatin1String("$timeExt"), startTime.toString(QLatin1String("HH:mm:ss"))); + fileName.replace(QLatin1String("$timeUTC"), + startTime.toUTC().toString(QLatin1String("HHmmss"))); + fileName.replace(QLatin1String("$time"), + startTime.toString(QLatin1String("HHmmss"))); } if (format.contains(QLatin1String("$userId"))) { @@ -1128,13 +1186,17 @@ QStringList TrainingSession::getOutputFileNames(const QString &fileNameFormat, fileInfo.fileName() + QLatin1String("-exercises-*-create"))).count(); if (exerciseCount == 1) { fileNames.append(baseName + QLatin1String(".hrm")); - fileNames.append(baseName + QLatin1String(".rr.hrm")); + if (hrmOptions.testFlag(RrFiles)) { + fileNames.append(baseName + QLatin1String(".rr.hrm")); + } } else { for (int index = 0; index < exerciseCount; ++index) { fileNames.append(QString::fromLatin1("%1.%2.hrm") .arg(baseName).arg(index)); - fileNames.append(QString::fromLatin1("%1.%2.rr.hrm") - .arg(baseName).arg(index)); + if (hrmOptions.testFlag(RrFiles)) { + fileNames.append(QString::fromLatin1("%1.%2.rr.hrm") + .arg(baseName).arg(index)); + } } } } @@ -1212,7 +1274,7 @@ QDomDocument TrainingSession::toGPX(const QDateTime &creationTime) const (duration.size() != latitude.size()) || (duration.size() != longitude.size()) || (duration.size() != satellites.size())) { - qWarning() << "lists not all equal sizes:" << duration.size() + qWarning() << "Sample lists not all equal sizes:" << duration.size() << altitude.size() << latitude.size() << longitude.size() << satellites.size(); } @@ -1500,7 +1562,7 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const } // [LapNames] This HRM section is undocumented, but supported by PPT5. - if (!laps.isEmpty()) { + if ((hrmOptions.testFlag(LapNames)) && (!laps.isEmpty())) { stream << "\r\n[LapNames]\r\n"; const QStringList keys = laps.keys(); for (int index = 0; index < keys.length(); ++index) { @@ -1654,16 +1716,19 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const QDomElement multiSportSession; if ((parsedExercises.size() > 1) && (!parsedSession.isEmpty())) { multiSportSession = doc.createElement(QLatin1String("MultiSportSession")); + QDateTime id = getDateTime(firstMap(parsedSession.value(QLatin1String("start")))); + if (tcxOptions.testFlag(ForceTcxUTC)) { + id = id.toUTC(); + } multiSportSession.appendChild(doc.createElement(QLatin1String("Id"))) - .appendChild(doc.createTextNode(getDateTime(firstMap(parsedSession - .value(QLatin1String("start")))).toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate))); + .appendChild(doc.createTextNode(id.toString(Qt::ISODate))); activities.appendChild(multiSportSession); } foreach (const QVariant &exercise, parsedExercises) { const QVariantMap map = exercise.toMap(); if (!map.contains(CREATE)) { - qWarning() << "skipping exercise with no 'create' request data"; + qWarning() << "Skipping exercise with no 'create' request data"; continue; } const QVariantMap create = map.value(CREATE).toMap(); @@ -1720,9 +1785,12 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const .value(QLatin1String("value"))).toULongLong())); // Get the starting time. - const QDateTime startTime = getDateTime(firstMap(create.value(QLatin1String("start")))); + QDateTime startTime = getDateTime(firstMap(create.value(QLatin1String("start")))); + if (tcxOptions.testFlag(ForceTcxUTC)) { + startTime = startTime.toUTC(); + } activity.appendChild(doc.createElement(QLatin1String("Id"))) - .appendChild(doc.createTextNode(startTime.toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate))); + .appendChild(doc.createTextNode(startTime.toString(Qt::ISODate))); // Build a map of lap split times to lap data. QVariantList laps = map.value(LAPS).toMap().value(QLatin1String("laps")).toList(); @@ -1780,15 +1848,18 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const // Create the Lap element, and set its StartTime attribute. #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) - const QDateTime lapStartTime = startTime.addMSecs(index * recordInterval); + QDateTime lapStartTime = startTime.addMSecs(index * recordInterval); #else /// @todo Remove this hack when Qt 5.2+ is available on Travis CI. QDateTime lapStartTime = startTime.toUTC() .addMSecs(index * recordInterval).addSecs(startTime.utcOffset()); lapStartTime.setUtcOffset(startTime.utcOffset()); #endif + if (tcxOptions.testFlag(ForceTcxUTC)) { + lapStartTime = lapStartTime.toUTC(); + } lap = doc.createElement(QLatin1String("Lap")); lap.setAttribute(QLatin1String("StartTime"), - lapStartTime.toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate)); + lapStartTime.toString(Qt::ISODate)); activity.appendChild(lap); // Add the per-lap (or per-exercise) statistics. @@ -1840,8 +1911,11 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const .addMSecs(index * recordInterval).addSecs(startTime.utcOffset()); trackPointTime.setUtcOffset(startTime.utcOffset()); #endif + if (tcxOptions.testFlag(ForceTcxUTC)) { + trackPointTime = trackPointTime.toUTC(); + } trackPoint.insertBefore(doc.createElement(QLatin1String("Time")), QDomNode()) - .appendChild(doc.createTextNode(trackPointTime.toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate))); + .appendChild(doc.createTextNode(trackPointTime.toString(Qt::ISODate))); track.appendChild(trackPoint); } } @@ -1859,7 +1933,7 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const .appendChild(doc.createTextNode(QLatin1String("Bipolar"))); tcx.appendChild(author); -/* + /* { QDomElement build = doc.createElement(QLatin1String("Build")); author.appendChild(build); @@ -1898,7 +1972,7 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const #undef BIPOLAR_STRINGIFY #endif } -*/ + */ /// @todo Make this dynamic if/when app is localized. author.appendChild(doc.createElement(QLatin1String("LangID"))) @@ -2029,7 +2103,7 @@ bool TrainingSession::writeGPX(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::WriteOnly|QIODevice::Truncate)) { - qWarning() << "failed to open" << QDir::toNativeSeparators(fileName); + qWarning() << "Failed to open" << QDir::toNativeSeparators(fileName); return false; } return writeGPX(file); @@ -2039,7 +2113,7 @@ bool TrainingSession::writeGPX(QIODevice &device) const { QDomDocument gpx = toGPX(); if (gpx.isNull()) { - qWarning() << "failed to convert to GPX" << baseName; + qWarning() << "Failed to convert to GPX" << baseName; return false; } device.write(gpx.toByteArray()); @@ -2059,10 +2133,10 @@ QStringList TrainingSession::writeHRM(const QString &fileNameFormat, QStringList TrainingSession::writeHRM(const QString &baseName) const { QStringList fileNames; - for (int rrDataOnly = 0; rrDataOnly <= 1; ++rrDataOnly) { + for (int rrDataOnly = 0; rrDataOnly <= (hrmOptions.testFlag(RrFiles) ? 1 : 0); ++rrDataOnly) { QStringList hrm = toHRM(rrDataOnly); if (hrm.isEmpty()) { - qWarning() << "failed to convert to HRM" << baseName; + qWarning() << "Failed to convert to HRM" << baseName; return QStringList(); } @@ -2073,7 +2147,7 @@ QStringList TrainingSession::writeHRM(const QString &baseName) const : QString::fromLatin1("%1.%2.%3").arg(baseName).arg(index).arg(extension); QFile file(fileName); if (!file.open(QIODevice::WriteOnly|QIODevice::Truncate)) { - qWarning() << "failed to open" << QDir::toNativeSeparators(fileName); + qWarning() << "Failed to open" << QDir::toNativeSeparators(fileName); } else if (file.write(hrm.at(index).toLatin1())) { fileNames.append(fileName); } @@ -2098,7 +2172,7 @@ bool TrainingSession::writeTCX(const QString &fileName) const { QFile file(fileName); if (!file.open(QIODevice::WriteOnly|QIODevice::Truncate)) { - qWarning() << "failed to open" << QDir::toNativeSeparators(fileName); + qWarning() << "Failed to open" << QDir::toNativeSeparators(fileName); return false; } return writeTCX(file); @@ -2108,7 +2182,7 @@ bool TrainingSession::writeTCX(QIODevice &device) const { QDomDocument tcx = toTCX(); if (tcx.isNull()) { - qWarning() << "failed to convert to TCX" << baseName; + qWarning() << "Failed to convert to TCX" << baseName; return false; } device.write(tcx.toByteArray()); diff --git a/src/bipolar/polar/v2/trainingsession.h b/src/bipolar/polar/v2/trainingsession.h index 7f9261c..7ffd439 100644 --- a/src/bipolar/polar/v2/trainingsession.h +++ b/src/bipolar/polar/v2/trainingsession.h @@ -40,6 +40,7 @@ namespace v2 { */ class TrainingSession : public QObject { Q_OBJECT + Q_PROPERTY(int exerciseCount READ exerciseCount) public: enum OutputFormat { @@ -50,8 +51,29 @@ class TrainingSession : public QObject { }; Q_DECLARE_FLAGS(OutputFormats, OutputFormat) + enum GpxOption { + CluetrustGpxExtension = 0x1001, + GarminTrackPointExtension = 0x1002, + }; + Q_DECLARE_FLAGS(GpxOptions, GpxOption) + + enum HrmOption { + RrFiles = 0x0001, + LapNames = 0x0002, + }; + Q_DECLARE_FLAGS(HrmOptions, HrmOption) + + enum TcxOption { + ForceTcxUTC = 0x0001, + GarminActivityExtension = 0x1001, + GarminCourceExtension = 0x1002, + }; + Q_DECLARE_FLAGS(TcxOptions, TcxOption) + TrainingSession(const QString &baseName); + int exerciseCount() const; + QStringList getOutputFileNames(const QString &fileNameFormat, const OutputFormats outputFormats, QString outputDirName = QString()); @@ -60,6 +82,13 @@ class TrainingSession : public QObject { bool parse(); + void setGpxOption(const GpxOption option, const bool enabled = true); + void setHrmOption(const HrmOption option, const bool enabled = true); + void setTcxOption(const TcxOption option, const bool enabled = true); + void setGpxOptions(const GpxOptions options); + void setHrmOptions(const HrmOptions options); + void setTcxOptions(const TcxOptions options); + QString writeGPX(const QString &fileNameFormat, QString outputDirName); bool writeGPX(const QString &fileName) const; bool writeGPX(QIODevice &device) const; @@ -77,6 +106,10 @@ class TrainingSession : public QObject { QVariantMap parsedPhysicalInformation; QVariantMap parsedSession; + GpxOptions gpxOptions; + HrmOptions hrmOptions; + TcxOptions tcxOptions; + static QString getTcxSport(const quint64 &polarSportValue); QString getOutputBaseFileName(const QString &format); @@ -121,6 +154,11 @@ class TrainingSession : public QObject { }; +Q_DECLARE_OPERATORS_FOR_FLAGS(TrainingSession::OutputFormats); +Q_DECLARE_OPERATORS_FOR_FLAGS(TrainingSession::GpxOptions); +Q_DECLARE_OPERATORS_FOR_FLAGS(TrainingSession::HrmOptions); +Q_DECLARE_OPERATORS_FOR_FLAGS(TrainingSession::TcxOptions); + }} #endif // __POLAR_V2_TRAINING_SESSION_H__ diff --git a/src/export/v800export.cpp b/src/export/v800export.cpp index 256eafd..29c732c 100644 --- a/src/export/v800export.cpp +++ b/src/export/v800export.cpp @@ -61,6 +61,8 @@ void V800export::export_sessions(QList sessions, unsigned char mode) qDebug("Parser: %s", session_info.toUtf8().constData()); + parser.setTcxOption(polar::v2::TrainingSession::ForceTcxUTC, true); + if(!parser.parse()) emit export_session_error(sessions[sessions_iter], PARSE_ERROR); From e740c01e5acac3752013355093dc6c09dd38c608 Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Sat, 6 Sep 2014 19:37:50 -0600 Subject: [PATCH 04/14] Updated the multisport handling - files are _(SPORT NUMBER), i.e. _0.tcx, _1.tcx, etc. --- src/export/v800export.cpp | 2 +- src/usb/v800usb.cpp | 2 +- src/widgets/v800main.cpp | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/export/v800export.cpp b/src/export/v800export.cpp index 29c732c..01a4e46 100644 --- a/src/export/v800export.cpp +++ b/src/export/v800export.cpp @@ -40,7 +40,7 @@ void V800export::export_sessions(QList sessions, unsigned char mode) for(int sessions_iter = 0; sessions_iter < sessions.length(); sessions_iter++) { QStringList filters; - filters << QString(tr("%1*")).arg(sessions[sessions_iter]); + filters << QString(tr("%1_*")).arg(sessions[sessions_iter]); QDir filter_dir(default_dir); filter_dir.setNameFilters(filters); diff --git a/src/usb/v800usb.cpp b/src/usb/v800usb.cpp index ad17e97..e2291a7 100755 --- a/src/usb/v800usb.cpp +++ b/src/usb/v800usb.cpp @@ -256,7 +256,7 @@ QList V800usb::get_v800_data(QString request, int multi_sport, bool deb QSettings settings; QString default_dir = settings.value(tr("default_dir")).toString(); - QString raw_dir = (QString(tr("%1/%2%3")).arg(default_dir).arg(tag).arg(multi_sport)); + QString raw_dir = (QString(tr("%1/%2_%3")).arg(default_dir).arg(tag).arg(multi_sport)); QDir(raw_dir).mkpath(raw_dir); QString raw_dest = (QString(tr("%1/%2")).arg(raw_dir).arg(file)); diff --git a/src/widgets/v800main.cpp b/src/widgets/v800main.cpp index 7f39f67..40f683b 100755 --- a/src/widgets/v800main.cpp +++ b/src/widgets/v800main.cpp @@ -154,6 +154,7 @@ void V800Main::handle_sessions_done() { download_progress->setValue(1); download_progress->setLabelText(tr("Exporting 1/%2...").arg(sessions_to_export.length())); + download_progress->show(); unsigned char export_mask = (ui->tcxBox->isChecked() ? V800export::TCX_EXPORT : 0x00) | (ui->hrmBox->isChecked() ? V800export::HRM_EXPORT : 0x00) | From 4d7043e29eb7702b58ad4ae1d8b1f1e39bfb405b Mon Sep 17 00:00:00 2001 From: profanum429 Date: Sat, 6 Sep 2014 19:59:12 -0600 Subject: [PATCH 05/14] Updated README for Release4 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 38774f1..56d61bf 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ v800_downloader =============== +Update: Release 4 now properly supports multisport sessions. Exported files will have a _(NUMBER) appended to them before the extension. This number is the order of the sport in the multisport session. Single sport sessions now have a _0 appended before the extension to represent that this is sport 1 of the session. + Update: Release 3 still works with the new 1.0.12 firmware but right after the update you won't see any sessions since when the firmware updates all the sessions on the watch have their full set of data removed and leave only the statistics information behind. Any new activities will be downloadable :) V800 Downloader is a tool that is used to download sessions from the Polar V800 watch. In normal usage there is no first party way From 215c9b1c7c6ef6a6bb1bb741f9af5e3495642ed2 Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Sat, 11 Oct 2014 10:40:36 -0500 Subject: [PATCH 06/14] Fixed bug where we'd get invalid directories and files after a factory reset. Also updated to latest Bipolar conversion code. --- .gitignore | 3 + src/bipolar/polar/v2/trainingsession.cpp | 104 +++++++++++++++++++++-- src/bipolar/polar/v2/trainingsession.h | 15 ++-- src/bipolar/protobuf/message.cpp | 10 +-- src/usb/v800usb.cpp | 8 +- src/widgets/v800main.cpp | 6 +- 6 files changed, 122 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index daf24e8..c14b899 100755 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ release/tmp/moc_message.cpp release/tmp/moc_trainingsession.cpp release/tmp/moc_v800export.cpp release/v800_downloader.app/Contents/Resources/v800_downloader.icns +debug/tmp/moc_message.cpp +debug/tmp/moc_trainingsession.cpp +debug/tmp/moc_v800export.cpp diff --git a/src/bipolar/polar/v2/trainingsession.cpp b/src/bipolar/polar/v2/trainingsession.cpp index d1facd7..200d195 100644 --- a/src/bipolar/polar/v2/trainingsession.cpp +++ b/src/bipolar/polar/v2/trainingsession.cpp @@ -61,12 +61,24 @@ int TrainingSession::exerciseCount() const return (isValid()) ? parsedExercises.count() : -1; } +#define TCX_RUNNING QLatin1String("Running") +#define TCX_BIKING QLatin1String("Biking") +#define TCX_OTHER QLatin1String("Other") + +QString TrainingSession::getTcxCadenceSensor(const quint64 &polarSportValue) +{ + const QString tcxSport = getTcxSport(polarSportValue); + if (tcxSport == TCX_BIKING) { + return QLatin1String("Bike"); + } else if (tcxSport == TCX_RUNNING) { + return QLatin1String("Footpod"); + } + return QString(); +} + /// @see https://github.com/pcolby/bipolar/wiki/Polar-Sport-Types QString TrainingSession::getTcxSport(const quint64 &polarSportValue) { - #define TCX_RUNNING QLatin1String("Running") - #define TCX_BIKING QLatin1String("Biking") - #define TCX_OTHER QLatin1String("Other") static QMap map; if (map.isEmpty()) { map.insert( 1, TCX_RUNNING); // Running @@ -1709,6 +1721,10 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const tcx.setAttribute(QLatin1String("xsi:schemaLocation"), QLatin1String("http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 " "http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd")); + if (tcxOptions.testFlag(GarminActivityExtension)) { + tcx.setAttribute(QLatin1String("xmlns:ax2"), + QLatin1String("http://www.garmin.com/xmlschemas/ActivityExtension/v2")); + } doc.appendChild(tcx); QDomElement activities = doc.createElement(QLatin1String("Activities")); @@ -1868,6 +1884,53 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const track = doc.createElement(QLatin1String("Track")); lap.appendChild(track); + + // Add any enabled extensions. + if (tcxOptions.testFlag(GarminActivityExtension)) { + QDomElement extensions = doc.createElement(QLatin1String("Extensions")); + lap.appendChild(extensions); + + // Add the Garmin Activity Extension. + if (tcxOptions.testFlag(GarminActivityExtension)) { + QDomElement lx = doc.createElement(QLatin1String("LX")); + lx.setAttribute(QLatin1String("xmlns"), + QLatin1String("http://www.garmin.com/xmlschemas/ActivityExtension/v2")); + extensions.appendChild(lx); + + if (stats.contains(QLatin1String("speed"))) { + lx.appendChild(doc.createElement(QLatin1String("AvgSpeed"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(firstMap(stats.value(QLatin1String("speed"))) + .value(QLatin1String("average"))).toDouble()))); + } + + if (stats.contains(QLatin1String("cadence"))) { + const QVariantMap cadence = firstMap(stats.value(QLatin1String("cadence"))); + + const QString sensor = getTcxCadenceSensor( + first(firstMap(create.value(QLatin1String("sport"))) + .value(QLatin1String("value"))).toULongLong()); + + if (sensor != QLatin1String("Footpod")) { + lx.appendChild(doc.createElement(QLatin1String("MaxBikeCadence"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(cadence.value(QLatin1String("maximum"))).toUInt()))); + } + + lx.appendChild(doc.createElement(QLatin1String("AvgRunCadence"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(cadence.value(QLatin1String("average"))).toUInt()))); + + if (sensor == QLatin1String("Footpod")) { + lx.appendChild(doc.createElement(QLatin1String("MaxRunCadence"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(cadence.value(QLatin1String("maximum"))).toUInt()))); + } + + /// @todo AvgWatts and MaxWatts when power data is available. + } + } + } } QDomElement trackPoint = doc.createElement(QLatin1String("Trackpoint")); @@ -1903,6 +1966,34 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const .appendChild(doc.createTextNode(cadence.at(index).toString())); } + if (tcxOptions.testFlag(GarminActivityExtension)) { + QDomElement tpx = doc.createElement(QLatin1String("TPX")); + tpx.setAttribute(QLatin1String("xmlns"), + QLatin1String("http://www.garmin.com/xmlschemas/ActivityExtension/v2")); + trackPoint.appendChild(doc.createElement(QLatin1String("Extensions"))) + .appendChild(tpx); + + if ((index < cadence.length()) && (cadence.at(index).toInt() >= 0) && + (!sensorOffline(samples.value(QLatin1String("speed-offline")).toList(), index))) { + tpx.appendChild(doc.createElement(QLatin1String("Speed"))) + .appendChild(doc.createTextNode(speed.at(index).toString())); + } + + if ((index < cadence.length()) && (cadence.at(index).toInt() >= 0) && + (!sensorOffline(samples.value(QLatin1String("cadence-offline")).toList(), index))) { + const QString sensor = getTcxCadenceSensor( + first(firstMap(create.value(QLatin1String("sport"))) + .value(QLatin1String("value"))).toULongLong()); + if (!sensor.isEmpty()) { + tpx.setAttribute(QLatin1String("CadenceSensor"), sensor); + } + if (sensor == QLatin1String("Footpod")) { + tpx.appendChild(doc.createElement(QLatin1String("RunCadence"))) + .appendChild(doc.createTextNode(cadence.at(index).toString())); + } + } + } + if (trackPoint.hasChildNodes()) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) QDateTime trackPointTime = startTime.addMSecs(index * recordInterval); @@ -1932,8 +2023,7 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const author.appendChild(doc.createElement(QLatin1String("Name"))) .appendChild(doc.createTextNode(QLatin1String("Bipolar"))); tcx.appendChild(author); - - /* +/* { QDomElement build = doc.createElement(QLatin1String("Build")); author.appendChild(build); @@ -1972,8 +2062,7 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const #undef BIPOLAR_STRINGIFY #endif } - */ - +*/ /// @todo Make this dynamic if/when app is localized. author.appendChild(doc.createElement(QLatin1String("LangID"))) .appendChild(doc.createTextNode(QLatin1String("EN"))); @@ -2023,7 +2112,6 @@ void TrainingSession::addLapStats(QDomDocument &doc, QDomElement &lap, lap.appendChild(doc.createElement(QLatin1String("Intensity"))) .appendChild(doc.createTextNode(QString::fromLatin1("Active"))); - // Cadence is only available per exercise, not per lap. if (stats.contains(QLatin1String("cadence"))) { lap.appendChild(doc.createElement(QLatin1String("Cadence"))) .appendChild(doc.createTextNode(QString::fromLatin1("%1") diff --git a/src/bipolar/polar/v2/trainingsession.h b/src/bipolar/polar/v2/trainingsession.h index 7ffd439..d2e7fc3 100644 --- a/src/bipolar/polar/v2/trainingsession.h +++ b/src/bipolar/polar/v2/trainingsession.h @@ -44,16 +44,16 @@ class TrainingSession : public QObject { public: enum OutputFormat { - GpxOutput = 0x01, - HrmOutput = 0x02, - TcxOutput = 0x04, + GpxOutput = 0x0001, + HrmOutput = 0x0002, + TcxOutput = 0x0004, AllOutputs = GpxOutput|HrmOutput|TcxOutput }; Q_DECLARE_FLAGS(OutputFormats, OutputFormat) enum GpxOption { - CluetrustGpxExtension = 0x1001, - GarminTrackPointExtension = 0x1002, + CluetrustGpxExtension = 0x0100, + GarminTrackPointExtension = 0x0200, }; Q_DECLARE_FLAGS(GpxOptions, GpxOption) @@ -65,8 +65,8 @@ class TrainingSession : public QObject { enum TcxOption { ForceTcxUTC = 0x0001, - GarminActivityExtension = 0x1001, - GarminCourceExtension = 0x1002, + GarminActivityExtension = 0x0100, + //GarminCourseExtension = 0x0200, //< Needs power support. }; Q_DECLARE_FLAGS(TcxOptions, TcxOption) @@ -110,6 +110,7 @@ class TrainingSession : public QObject { HrmOptions hrmOptions; TcxOptions tcxOptions; + static QString getTcxCadenceSensor(const quint64 &polarSportValue); static QString getTcxSport(const quint64 &polarSportValue); QString getOutputBaseFileName(const QString &format); diff --git a/src/bipolar/protobuf/message.cpp b/src/bipolar/protobuf/message.cpp index fc34471..0cf90e3 100644 --- a/src/bipolar/protobuf/message.cpp +++ b/src/bipolar/protobuf/message.cpp @@ -47,7 +47,7 @@ QVariantMap Message::parse(QIODevice &data, const QString &tagPathPrefix) const // Fetch the next field's tag index and wire type. QPair tagAndType = parseTagAndType(data); if (tagAndType.first == 0) { - qWarning() << "invalid tag:" << tagAndType.first; + qWarning() << "Invalid tag:" << tagAndType.first; return QVariantMap(); } @@ -99,7 +99,7 @@ QVariant Message::parseValue(QIODevice &data, const quint8 wireType, (wireType != Types::getWireType(scalarType))) { qWarning() << tagPath << "wire type" << wireType << "does not match " "expected wire type" << Types::getWireType(scalarType) << "for " - "scalar type" << scalarType; + "scalar type" << scalarType << '.'; } switch (wireType) { @@ -139,7 +139,7 @@ QVariant Message::parseValue(QIODevice &data, const quint8 wireType, } break; } - qWarning() << "invalid wireType:" << wireType << "(tagPath:" << tagPath << ')'; + qWarning() << "Invalid wireType:" << wireType << "(tagPath:" << tagPath << ')'; return QVariant(); } @@ -149,7 +149,7 @@ QVariant Message::parseLengthDelimitedValue(QIODevice &data, { const QVariant value = readLengthDelimitedValue(data); if (!value.isValid()) { - qWarning() << "failed to read prefix-delimited value"; + qWarning() << "Failed to read prefix-delimited value."; return QVariant(); } @@ -193,7 +193,7 @@ QVariant Message::readLengthDelimitedValue(QIODevice &data) const // I haven't found any Protocl Buffers documentation to support / dispute this. const QVariant length = parseUnsignedVarint(data); if (!length.isValid()) { - qWarning() << "failed to read prefix-delimited length"; + qWarning() << "Failed to read prefix-delimited length."; return QVariant(); } const QByteArray value = data.read(length.toULongLong()); diff --git a/src/usb/v800usb.cpp b/src/usb/v800usb.cpp index e2291a7..8fcc108 100755 --- a/src/usb/v800usb.cpp +++ b/src/usb/v800usb.cpp @@ -333,7 +333,13 @@ QList V800usb::extract_dir_and_files(QByteArray full) full_state = 3; loc++; break; - case 3: /* now get the full string */ + case 3: /* we need a 0x10 after the string */ + if(full.at(loc+size) == 0x10) + full_state = 4; + else + full_state = 0; + break; + case 4: /* now get the full string */ QString name(tr(QByteArray(full.constData()+loc, size))); dir_and_files.append(name); diff --git a/src/widgets/v800main.cpp b/src/widgets/v800main.cpp index 40f683b..1f80549 100755 --- a/src/widgets/v800main.cpp +++ b/src/widgets/v800main.cpp @@ -268,13 +268,13 @@ void V800Main::on_downloadBtn_clicked() QString tag = QDateTime(QDate::fromString(session_split[0], tr("yyyyMMdd")), QTime::fromString(session_split[1], tr("HHmmss"))).toString(tr("yyyyMMddhhmmss")); if(export_mask & V800export::TCX_EXPORT) - if(QFile::exists(QString(tr("%1/%2.tcx")).arg(default_dir).arg(tag))) + if(QFile::exists(QString(tr("%1/%2_0.tcx")).arg(default_dir).arg(tag))) export_exists |= V800export::TCX_EXPORT; if(export_mask & V800export::HRM_EXPORT) - if(QFile::exists(QString(tr("%1/%2.hrm")).arg(default_dir).arg(tag))) + if(QFile::exists(QString(tr("%1/%2_0.hrm")).arg(default_dir).arg(tag))) export_exists |= V800export::HRM_EXPORT; if(export_mask & V800export::GPX_EXPORT) - if(QFile::exists(QString(tr("%1/%2.gpx")).arg(default_dir).arg(tag))) + if(QFile::exists(QString(tr("%1/%2_0.gpx")).arg(default_dir).arg(tag))) export_exists |= V800export::GPX_EXPORT; if(export_mask != export_exists) From 82c6264a8b0f9d0ab24ae47abc526ee1b5b7ec64 Mon Sep 17 00:00:00 2001 From: profanum429 Date: Sat, 11 Oct 2014 10:58:57 -0500 Subject: [PATCH 07/14] Info for Release5 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 56d61bf..63d254b 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ v800_downloader =============== +Update: Release 5 fixes a bug where some garbage data would result in thinking files and directories existed where they did not and had obviously faulty names. This has been fixed. I only saw this after I did a factory reset, so it might not occur in normal usage but it shouldn't be an issue now. + Update: Release 4 now properly supports multisport sessions. Exported files will have a _(NUMBER) appended to them before the extension. This number is the order of the sport in the multisport session. Single sport sessions now have a _0 appended before the extension to represent that this is sport 1 of the session. Update: Release 3 still works with the new 1.0.12 firmware but right after the update you won't see any sessions since when the firmware updates all the sessions on the watch have their full set of data removed and leave only the statistics information behind. Any new activities will be downloadable :) From c1380f3f67a9b5aca5e04408b93338665625fc4c Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Thu, 30 Oct 2014 15:38:02 -0500 Subject: [PATCH 08/14] Debug code to parse planned route files - looking into it. --- src/bipolar/polar/v2/trainingsession.cpp | 48 ++++++++++++++++++++++++ src/bipolar/polar/v2/trainingsession.h | 2 + src/widgets/v800main.cpp | 8 +++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/bipolar/polar/v2/trainingsession.cpp b/src/bipolar/polar/v2/trainingsession.cpp index 200d195..3a49f68 100644 --- a/src/bipolar/polar/v2/trainingsession.cpp +++ b/src/bipolar/polar/v2/trainingsession.cpp @@ -239,6 +239,54 @@ bool TrainingSession::parse(const QString &exerciseId, const QMap V800Main::V800Main(QWidget *parent) : @@ -81,7 +83,6 @@ V800Main::V800Main(QWidget *parent) : settings.setValue(tr("file_dir"), file_dir); } - ui->setupUi(this); ui->verticalLayout->setAlignment(Qt::AlignTop); ui->verticalLayout->setSpacing(20); @@ -118,7 +119,10 @@ void V800Main::handle_not_ready() failure.setIcon(QMessageBox::Critical); failure.exec(); - exit(-1); + polar::v2::TrainingSession parser(tr("0000000")); + parser.parsePRoute(); + + //exit(-1); } void V800Main::handle_ready() From 79f99246681f5d7cfc7e8c7401dd9ca396d8958e Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Sat, 1 Nov 2014 10:22:14 -0500 Subject: [PATCH 09/14] Revert "Debug code to parse planned route files - looking into it." This reverts commit c1380f3f67a9b5aca5e04408b93338665625fc4c. --- src/bipolar/polar/v2/trainingsession.cpp | 48 ------------------------ src/bipolar/polar/v2/trainingsession.h | 2 - src/widgets/v800main.cpp | 8 +--- 3 files changed, 2 insertions(+), 56 deletions(-) diff --git a/src/bipolar/polar/v2/trainingsession.cpp b/src/bipolar/polar/v2/trainingsession.cpp index 3a49f68..200d195 100644 --- a/src/bipolar/polar/v2/trainingsession.cpp +++ b/src/bipolar/polar/v2/trainingsession.cpp @@ -239,54 +239,6 @@ bool TrainingSession::parse(const QString &exerciseId, const QMap V800Main::V800Main(QWidget *parent) : @@ -83,6 +81,7 @@ V800Main::V800Main(QWidget *parent) : settings.setValue(tr("file_dir"), file_dir); } + ui->setupUi(this); ui->verticalLayout->setAlignment(Qt::AlignTop); ui->verticalLayout->setSpacing(20); @@ -119,10 +118,7 @@ void V800Main::handle_not_ready() failure.setIcon(QMessageBox::Critical); failure.exec(); - polar::v2::TrainingSession parser(tr("0000000")); - parser.parsePRoute(); - - //exit(-1); + exit(-1); } void V800Main::handle_ready() From d5ffd96c05c4c0a16b29e4340dece8d83a0d6da1 Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Sat, 1 Nov 2014 10:23:10 -0500 Subject: [PATCH 10/14] Ignored some stuff... --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c14b899..83e1bcf 100755 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ release/v800_downloader.app/Contents/Resources/v800_downloader.icns debug/tmp/moc_message.cpp debug/tmp/moc_trainingsession.cpp debug/tmp/moc_v800export.cpp +debug/v800_downloader.app/Contents/Resources/v800_downloader.icns From b5619b5d29ac231a05a6bc06358d68380b936e1a Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Tue, 4 Nov 2014 16:28:14 -0600 Subject: [PATCH 11/14] Added popup box support for the M400, still need to get the PID for it to have possible support if the comms are the same. Also added in hook to handle uploading routes, nothing in the form of actually doing it yet though...this might change --- src/ui/v800main.ui | 7 +++++ src/usb/v800usb.cpp | 19 ++++++++++-- src/usb/v800usb.h | 11 ++++++- src/widgets/v800main.cpp | 67 ++++++++++++++++++++++++++++++---------- src/widgets/v800main.h | 3 ++ 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/ui/v800main.ui b/src/ui/v800main.ui index aa90eab..6cd5345 100755 --- a/src/ui/v800main.ui +++ b/src/ui/v800main.ui @@ -98,6 +98,13 @@ V800... + + + + Upload Route + + + diff --git a/src/usb/v800usb.cpp b/src/usb/v800usb.cpp index 8fcc108..fcee29b 100755 --- a/src/usb/v800usb.cpp +++ b/src/usb/v800usb.cpp @@ -29,10 +29,11 @@ #include "native_usb.h" #include "trainingsession.h" -V800usb::V800usb(QObject *parent) : +V800usb::V800usb(int device, QObject *parent) : QObject(parent) { usb = NULL; + this->device = device; } V800usb::~V800usb() @@ -46,8 +47,15 @@ V800usb::~V800usb() void V800usb::start() { + int ret = -1; usb = new native_usb(); - if(usb->open_usb(0x0da4, 0x0008) != -1) + + if(device == V800) + ret = usb->open_usb(0x0da4, 0x0008); + else if(device == M400) + ret = usb->open_usb(0x0da4, 0x0008); + + if(ret != -1) { get_all_sessions(); emit ready(); @@ -123,10 +131,15 @@ void V800usb::get_all_objects(QString path) void V800usb::get_file(QString path) { - get_v800_data(path, true); + get_v800_data(path, 0, true); emit file_done(); } +void V800usb::upload_route(QString route) +{ + qDebug("Route file: %s", route.toLatin1().constData()); +} + void V800usb::get_all_sessions() { QList dates, times, files, sessions; diff --git a/src/usb/v800usb.h b/src/usb/v800usb.h index aa35e47..b758926 100755 --- a/src/usb/v800usb.h +++ b/src/usb/v800usb.h @@ -24,13 +24,18 @@ #define V800_ROOT_DIR "/U/0" +enum { + V800 = 0, + M400 +}; + class native_usb; class V800usb : public QObject { Q_OBJECT public: - explicit V800usb(QObject *parent = 0); + explicit V800usb(int device, QObject *parent = 0); ~V800usb(); signals: @@ -50,6 +55,8 @@ public slots: void get_all_objects(QString path); void get_file(QString path); + void upload_route(QString route); + private: QList extract_dir_and_files(QByteArray full); QByteArray generate_request(QString request); @@ -62,6 +69,8 @@ public slots: void get_all_sessions(); native_usb *usb; + + int device; }; #endif // V800USB_H diff --git a/src/widgets/v800main.cpp b/src/widgets/v800main.cpp index 1f80549..01c72f6 100755 --- a/src/widgets/v800main.cpp +++ b/src/widgets/v800main.cpp @@ -33,32 +33,18 @@ V800Main::V800Main(QWidget *parent) : v800_ready = false; - QThread *usb_thread = new QThread; - usb = new V800usb(); - usb->moveToThread(usb_thread); - QThread *export_data_thread = new QThread; export_data = new V800export(); export_data->moveToThread(export_data_thread); - connect(usb, SIGNAL(all_sessions(QList)), this, SLOT(handle_all_sessions(QList))); - connect(usb, SIGNAL(sessions_done()), this, SLOT(handle_sessions_done())); - connect(usb, SIGNAL(session_done(QString, int, int)), this, SLOT(handle_session_done(QString, int, int))); - connect(usb, SIGNAL(ready()), this, SLOT(handle_ready())); - connect(usb, SIGNAL(not_ready()), this, SLOT(handle_not_ready())); - connect(export_data, SIGNAL(export_session_done(int,int)), this, SLOT(handle_export_session_done(int,int))); connect(export_data, SIGNAL(export_sessions_done()), this, SLOT(handle_export_sessions_done())); connect(export_data, SIGNAL(export_session_error(QString,int)), this, SLOT(handle_export_session_error(QString,int))); - connect(this, SIGNAL(get_sessions(QList)), usb, SLOT(get_sessions(QList))); connect(this, SIGNAL(export_sessions(QList,unsigned char)), export_data, SLOT(export_sessions(QList,unsigned char))); new QShortcut(QKeySequence(Qt::SHIFT + Qt::Key_A), this, SLOT(handle_advanced_shortcut())); - connect(usb_thread, SIGNAL(started()), usb, SLOT(start())); - usb_thread->start(); - connect(export_data_thread, SIGNAL(started()), export_data, SLOT(start())); export_data_thread->start(); @@ -81,7 +67,6 @@ V800Main::V800Main(QWidget *parent) : settings.setValue(tr("file_dir"), file_dir); } - ui->setupUi(this); ui->verticalLayout->setAlignment(Qt::AlignTop); ui->verticalLayout->setSpacing(20); @@ -89,15 +74,47 @@ V800Main::V800Main(QWidget *parent) : ui->exerciseTree->setHeaderLabel(tr("Session")); ui->fsBtn->setVisible(false); + ui->uploadBtn->setVisible(false); ui->tcxBox->setChecked(true); disable_all(); + + QStringList devices; + devices.append(tr("V800")); + devices.append(tr("M400")); + + bool ok; + QString selected_device; + selected_device = QInputDialog::getItem(this, tr("Select Device"), tr("Device:"), devices, 0, false, &ok); + + if(!ok || selected_device.isEmpty()) + exit(-1); + + QThread *usb_thread = new QThread; + if(selected_device == tr("V800")) + usb = new V800usb(V800); + else if(selected_device == tr("M400")) + usb = new V800usb(M400); + usb->moveToThread(usb_thread); + + connect(usb, SIGNAL(all_sessions(QList)), this, SLOT(handle_all_sessions(QList))); + connect(usb, SIGNAL(sessions_done()), this, SLOT(handle_sessions_done())); + connect(usb, SIGNAL(session_done(QString, int, int)), this, SLOT(handle_session_done(QString, int, int))); + connect(usb, SIGNAL(ready()), this, SLOT(handle_ready())); + connect(usb, SIGNAL(not_ready()), this, SLOT(handle_not_ready())); + + connect(this, SIGNAL(get_sessions(QList)), usb, SLOT(get_sessions(QList))); + connect(this, SIGNAL(upload_route(QString)), usb, SLOT(upload_route(QString))); + + connect(usb_thread, SIGNAL(started()), usb, SLOT(start())); + usb_thread->start(); + this->show(); start_in_progress = new QMessageBox(this); start_in_progress->setWindowTitle(tr("V800 Downloader")); - start_in_progress->setText(tr("Downloading session list from V800...")); + start_in_progress->setText(tr("Downloading session list from device...")); start_in_progress->setIcon(QMessageBox::Information); start_in_progress->setStandardButtons(0); start_in_progress->setWindowModality(Qt::WindowModal); @@ -114,7 +131,7 @@ void V800Main::handle_not_ready() { QMessageBox failure; failure.setWindowTitle(tr("V800 Downloader")); - failure.setText(tr("Failed to open V800!")); + failure.setText(tr("Failed to open device!")); failure.setIcon(QMessageBox::Critical); failure.exec(); @@ -212,9 +229,15 @@ void V800Main::handle_export_session_error(QString session, int error) void V800Main::handle_advanced_shortcut() { if(ui->fsBtn->isVisible()) + { ui->fsBtn->setVisible(false); + ui->uploadBtn->setVisible(false); + } else + { ui->fsBtn->setVisible(true); + ui->uploadBtn->setVisible(true); + } } void V800Main::enable_all() @@ -223,6 +246,7 @@ void V800Main::enable_all() ui->checkBtn->setEnabled(true); ui->uncheckBtn->setEnabled(true); ui->fsBtn->setEnabled(true); + ui->uploadBtn->setEnabled(true); ui->dirSelectBtn->setEnabled(true); ui->tcxBox->setEnabled(true); ui->hrmBox->setEnabled(true); @@ -235,6 +259,7 @@ void V800Main::disable_all() ui->checkBtn->setEnabled(false); ui->uncheckBtn->setEnabled(false); ui->fsBtn->setEnabled(false); + ui->uploadBtn->setEnabled(false); ui->dirSelectBtn->setEnabled(false); ui->tcxBox->setEnabled(false); ui->hrmBox->setEnabled(false); @@ -331,6 +356,14 @@ void V800Main::on_fsBtn_clicked() fs->show(); } +void V800Main::on_uploadBtn_clicked() +{ + QString route = QFileDialog::getOpenFileName(this, tr("Open Route"), QDir::homePath(), tr("Route Files (*.bpb)")); + + if(!route.isEmpty()) + emit upload_route(route); +} + void V800Main::on_dirSelectBtn_clicked() { QSettings settings; diff --git a/src/widgets/v800main.h b/src/widgets/v800main.h index 902d927..0506cf8 100755 --- a/src/widgets/v800main.h +++ b/src/widgets/v800main.h @@ -42,6 +42,7 @@ class V800Main : public QWidget signals: void get_sessions(QList session); void export_sessions(QList sessions, unsigned char mode); + void upload_route(QString route); private slots: void handle_ready(); @@ -63,6 +64,8 @@ private slots: void on_dirSelectBtn_clicked(); + void on_uploadBtn_clicked(); + private: void disable_all(); void enable_all(); From c647ad2d52f92b380197a9cbabfd09387c81cade Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Sat, 8 Nov 2014 19:53:58 -0600 Subject: [PATCH 12/14] Added code to upload stuff to the V800; partially working, in process of debugging it. --- src/usb/v800usb.cpp | 265 ++++++++++++++++++++++++++++++++++++++- src/usb/v800usb.h | 8 ++ src/widgets/v800main.cpp | 5 +- 3 files changed, 276 insertions(+), 2 deletions(-) diff --git a/src/usb/v800usb.cpp b/src/usb/v800usb.cpp index fcee29b..555774a 100755 --- a/src/usb/v800usb.cpp +++ b/src/usb/v800usb.cpp @@ -138,6 +138,76 @@ void V800usb::get_file(QString path) void V800usb::upload_route(QString route) { qDebug("Route file: %s", route.toLatin1().constData()); + + if(!QFile(QString(tr("%1/PROUTE.BPB")).arg(route)).exists()) + { + qDebug("No PROUTE.BPB"); + return; + } + + if(!QFile(QString(tr("%1/ID.BPB")).arg(route)).exists()) + { + qDebug("No ID.BPB"); + return; + } + + if(!QFile(QString(tr("%1/TST.BPB")).arg(route)).exists()) + { + qDebug("No TST.BPB"); + return; + } + + QList favs, routes; + int max_fav_num = 0, max_route_num = 0, new_route_num; + + favs = get_v800_data(tr("/U/0/FAV/")); + routes = get_v800_data(tr("/U/0/ROUTES/")); + + for(int cnt = 0; cnt < favs.length(); cnt++) + { + if(max_fav_num < favs[cnt].remove(tr("/")).toInt()) + max_fav_num = favs[cnt].remove(tr("/")).toInt(); + } + + for(int cnt = 0; cnt < routes.length(); cnt++) + { + if(max_route_num < routes[cnt].remove(tr("/")).toInt()) + max_route_num = routes[cnt].remove(tr("/")).toInt(); + } + + if(favs.length() == 0) + max_fav_num = -1; + if(routes.length() == 0) + max_route_num = -1; + + if(max_fav_num != max_route_num) + { + qDebug("Unbalanced route and favorites, can't add route :("); +// return; + } + + new_route_num = max_route_num+1; + qDebug("New route and fav: %d", new_route_num); + + if(new_route_num > 99) + { + qDebug("More than 99 routes, can't add another"); + return; + } + + new_route_num = 0; + + /* + put_v800_dir(tr("/U/0/ROUTES/")); + put_v800_dir(QString(tr("/U/0/ROUTES/%1/")).arg(new_route_num)); + put_v800_data(QString(tr("%1/PROUTE.BPB")).arg(route), QString(tr("/U/0/ROUTES/%1/PROUTE.BPB")).arg(new_route_num)); + */ + + remove_v800_dir(tr("/U/0/FAV")); + put_v800_dir(tr("/U/0/FAV/")); + put_v800_dir(QString(tr("/U/0/FAV/%1/")).arg(new_route_num, 2, 10, QChar(0x30))); + put_v800_data(QString(tr("%1/ID.BPB")).arg(route), QString(tr("/U/0/FAV/%1/ID.BPB")).arg(new_route_num, 2, 10, QChar(0x30))); + put_v800_data(QString(tr("%1/TST.BPB")).arg(route), QString(tr("/U/0/FAV/%1/TST.BPB")).arg(new_route_num, 2, 10, QChar(0x30))); } void V800usb::get_all_sessions() @@ -161,7 +231,7 @@ void V800usb::get_all_sessions() for(files_iter = 0; files_iter < files.length(); files_iter++) { - if(QString(files[files_iter]).compare(tr("ROUTE.GZB")) == 0) + if(QString(files[files_iter]).compare(tr("SAMPLES.GZB")) == 0) { session_exists = true; break; @@ -433,3 +503,196 @@ QByteArray V800usb::add_to_full(QByteArray packet, QByteArray full, bool initial return new_full; } + +void V800usb::remove_v800_dir(QString dest) +{ + qDebug("Dir: %s", dest.toLatin1().constData()); + + QByteArray packet; + int cont = 1, usb_state = 0; + + while(cont) + { + // usb state machine for writing + switch(usb_state) + { + case 0: // send the initial packet to the watch + packet.clear(); + packet = generate_directory_command(dest.toUtf8(), true); + usb->write_usb(packet); + + usb_state = 1; + + break; + case 1: // see what we got from the v800 + packet.clear(); + packet = usb->read_usb(); + + qDebug("Done creating directory"); + cont = 0; + + break; + } + } +} + +void V800usb::put_v800_dir(QString dest) +{ + qDebug("Dir: %s", dest.toLatin1().constData()); + + QByteArray packet; + int cont = 1, usb_state = 0; + + while(cont) + { + // usb state machine for writing + switch(usb_state) + { + case 0: // send the initial packet to the watch + packet.clear(); + packet = generate_directory_command(dest.toUtf8(), false); + usb->write_usb(packet); + + usb_state = 1; + + break; + case 1: // see what we got from the v800 + packet.clear(); + packet = usb->read_usb(); + + qDebug("Done creating directory"); + cont = 0; + + break; + } + } +} + +void V800usb::put_v800_data(QString src, QString dest) +{ + qDebug("Src: %s\nDest: %s", src.toLatin1().constData(), dest.toLatin1().constData()); + + QFile src_file(src); + QByteArray packet, full, data; + int cont = 1, usb_state = 0, packet_num, data_loc; + + src_file.open(QIODevice::ReadOnly); + full = src_file.readAll(); + + while(cont) + { + // usb state machine for writing + switch(usb_state) + { + case 0: // send the initial packet to the watch + data = full.mid(0, 55-dest.length()); + + packet.clear(); + packet = generate_initial_command(dest.toUtf8(), data); + usb->write_usb(packet); + + data_loc = 55-dest.length(); + packet_num = 1; + usb_state = 1; + + break; + case 1: // see what we got from the v800 + packet.clear(); + packet = usb->read_usb(); + + if(!is_command_end(packet)) + usb_state = 2; + else + usb_state = 3; + + break; + case 2: + data = full.mid(data_loc, 61); + + packet.clear(); + packet = generate_next_command(data, packet_num); + usb->write_usb(packet); + + data_loc += 61; + if(packet_num == 0xff) + packet_num = 0x00; + else + packet_num++; + + usb_state = 1; + + break; + case 3: + qDebug("Done with file"); + + cont = 0; + break; + } + } +} + +QByteArray V800usb::generate_directory_command(QByteArray dest, bool remove) +{ + QByteArray packet; + + packet[0] = 01; + packet[1] = (unsigned char)((dest.length()+8) << 2); + packet[2] = 0x00; + packet[3] = dest.length()+4; + packet[4] = 0x00; + packet[5] = 0x08; + if(remove) + packet[6] = 0x03; + else + packet[6] = 0x01; + packet[7] = 0x12; + packet[8] = dest.length(); + packet.append(dest); + + return packet; +} + +QByteArray V800usb::generate_initial_command(QByteArray dest, QByteArray data) +{ + QByteArray packet; + + packet[0] = 01; + packet[1] = (unsigned char)((dest.length()+data.length()+7) << 2); + packet[2] = 0x00; + packet[3] = dest.length()+4; + packet[4] = 0x00; + packet[5] = 0x08; + packet[6] = 0x01; + packet[7] = 0x12; + packet[8] = dest.length(); + packet.append(dest); + packet.append(data); + + if(packet.length() == 64) + packet[1] = packet[1] | 0x01; + + return packet; +} + +QByteArray V800usb::generate_next_command(QByteArray data, int packet_num) +{ + QByteArray packet; + + packet[0] = 01; + packet[1] = (unsigned char)((data.length()+1) << 2); + packet[2] = packet_num; + packet.append(data); + + if(packet.length() == 64) + packet[1] = packet[1] | 0x01; + + return packet; +} + +int V800usb::is_command_end(QByteArray packet) +{ + if(packet.at(1) == 0x10) + return 1; + else + return 0; +} diff --git a/src/usb/v800usb.h b/src/usb/v800usb.h index b758926..4f4db9f 100755 --- a/src/usb/v800usb.h +++ b/src/usb/v800usb.h @@ -66,6 +66,14 @@ public slots: QList get_v800_data(QString request, int multi_sport = 0, bool debug=false); + void remove_v800_dir(QString dest); + void put_v800_dir(QString dest); + void put_v800_data(QString src, QString dest); + QByteArray generate_directory_command(QByteArray dest, bool remove); + QByteArray generate_initial_command(QByteArray dest, QByteArray data); + QByteArray generate_next_command(QByteArray data, int packet_num); + int is_command_end(QByteArray packet); + void get_all_sessions(); native_usb *usb; diff --git a/src/widgets/v800main.cpp b/src/widgets/v800main.cpp index 01c72f6..67478be 100755 --- a/src/widgets/v800main.cpp +++ b/src/widgets/v800main.cpp @@ -86,10 +86,13 @@ V800Main::V800Main(QWidget *parent) : bool ok; QString selected_device; + /* selected_device = QInputDialog::getItem(this, tr("Select Device"), tr("Device:"), devices, 0, false, &ok); if(!ok || selected_device.isEmpty()) exit(-1); + */ + selected_device = tr("V800"); QThread *usb_thread = new QThread; if(selected_device == tr("V800")) @@ -358,7 +361,7 @@ void V800Main::on_fsBtn_clicked() void V800Main::on_uploadBtn_clicked() { - QString route = QFileDialog::getOpenFileName(this, tr("Open Route"), QDir::homePath(), tr("Route Files (*.bpb)")); + QString route = QFileDialog::getExistingDirectory(this, tr("Open Route"), QDir::homePath(), QFileDialog::ShowDirsOnly); if(!route.isEmpty()) emit upload_route(route); From d17258ad52874349088d98a06e677f20d2a7aa5b Mon Sep 17 00:00:00 2001 From: Christian Weber Date: Sat, 10 Jan 2015 15:52:47 -0600 Subject: [PATCH 13/14] Updated to Bipolar 0.4; disabled broken route upload. --- src/bipolar/polar/v2/trainingsession.cpp | 283 ++++++++++++++++++++--- src/bipolar/polar/v2/trainingsession.h | 9 +- src/bipolar/protobuf/fixnum.cpp | 2 +- src/bipolar/protobuf/fixnum.h | 2 +- src/bipolar/protobuf/message.cpp | 2 +- src/bipolar/protobuf/message.h | 2 +- src/bipolar/protobuf/types.cpp | 2 +- src/bipolar/protobuf/types.h | 2 +- src/bipolar/protobuf/varint.cpp | 2 +- src/bipolar/protobuf/varint.h | 2 +- src/usb/v800usb.cpp | 2 - src/widgets/v800main.cpp | 2 + 12 files changed, 269 insertions(+), 43 deletions(-) diff --git a/src/bipolar/polar/v2/trainingsession.cpp b/src/bipolar/polar/v2/trainingsession.cpp index 200d195..da05eb9 100644 --- a/src/bipolar/polar/v2/trainingsession.cpp +++ b/src/bipolar/polar/v2/trainingsession.cpp @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. @@ -21,9 +21,9 @@ #include "message.h" #include "types.h" - -//#include "os/versioninfo.h" - +/* +#include "os/versioninfo.h" +*/ #include #include #include @@ -786,6 +786,45 @@ QVariantMap TrainingSession::parseSamples(QIODevice &data) const ADD_FIELD_INFO("20", "fwd-acceleration-offline", EmbeddedMessage); ADD_FIELD_INFO("20/1", "start-index", Uint32); ADD_FIELD_INFO("20/2", "stop-index", Uint32); + ADD_FIELD_INFO("21", "moving-type-offline", EmbeddedMessage); + ADD_FIELD_INFO("21/1", "start-index", Uint32); + ADD_FIELD_INFO("21/2", "stop-index", Uint32); + ADD_FIELD_INFO("22", "left-pedal-power", EmbeddedMessage); + ADD_FIELD_INFO("22/1", "current-power", Int32); + ADD_FIELD_INFO("22/2", "cumulative-revolutions", Uint32); + ADD_FIELD_INFO("22/3", "cumulative-timestamp", Uint32); + ADD_FIELD_INFO("22/4", "min-force", Sint32); + ADD_FIELD_INFO("22/5", "max-force", Uint32); + ADD_FIELD_INFO("22/6", "min-force-angle", Uint32); + ADD_FIELD_INFO("22/7", "max-force-angle", Uint32); + ADD_FIELD_INFO("22/8", "bottom-dead-spot", Uint32); + ADD_FIELD_INFO("22/9", "top-dead-spot", Uint32); + ADD_FIELD_INFO("23", "left-pedal-power-offline", EmbeddedMessage); + ADD_FIELD_INFO("23/1", "start-index", Uint32); + ADD_FIELD_INFO("23/2", "stop-index", Uint32); + ADD_FIELD_INFO("24", "right-pedal-power", EmbeddedMessage); + ADD_FIELD_INFO("24/1", "current-power", Int32); + ADD_FIELD_INFO("24/2", "cumulative-revolutions", Uint32); + ADD_FIELD_INFO("24/3", "cumulative-timestamp", Uint32); + ADD_FIELD_INFO("24/4", "min-force", Sint32); + ADD_FIELD_INFO("24/5", "max-force", Uint32); + ADD_FIELD_INFO("24/6", "min-force-angle", Uint32); + ADD_FIELD_INFO("24/7", "max-force-angle", Uint32); + ADD_FIELD_INFO("24/8", "bottom-dead-spot", Uint32); + ADD_FIELD_INFO("24/9", "top-dead-spot", Uint32); + ADD_FIELD_INFO("25", "right-pedal-power-offline",EmbeddedMessage); + ADD_FIELD_INFO("25/1", "start-index", Uint32); + ADD_FIELD_INFO("25/2", "stop-index", Uint32); + ADD_FIELD_INFO("26", "left-power-calibration", EmbeddedMessage); + ADD_FIELD_INFO("26/1", "start-index", Uint32); + ADD_FIELD_INFO("26/2", "value", Float); + ADD_FIELD_INFO("26/3", "operation", Enumerator); + ADD_FIELD_INFO("26/4", "cause", Enumerator); + ADD_FIELD_INFO("27", "right-power-calibration", EmbeddedMessage); + ADD_FIELD_INFO("27/1", "start-index", Uint32); + ADD_FIELD_INFO("27/2", "value", Float); + ADD_FIELD_INFO("27/3", "operation", Enumerator); + ADD_FIELD_INFO("27/4", "cause", Enumerator); ProtoBuf::Message parser(fieldInfo); if (isGzipped(data)) { @@ -1240,6 +1279,18 @@ QDomDocument TrainingSession::toGPX(const QDateTime &creationTime) const gpx.setAttribute(QLatin1String("xsi:schemaLocation"), QLatin1String("http://www.topografix.com/GPX/1/1 " "http://www.topografix.com/GPX/1/1/gpx.xsd")); + if (gpxOptions.testFlag(CluetrustGpxDataExtension)) { + gpx.setAttribute(QLatin1String("xmlns:gpxdata"), + QLatin1String("http://www.cluetrust.com/XML/GPXDATA/1/0")); + } + if (gpxOptions.testFlag(GarminAccelerationExtension)) { + gpx.setAttribute(QLatin1String("xmlns:gpxax"), + QLatin1String("http://www.garmin.com/xmlschemas/AccelerationExtension/v1")); + } + if (gpxOptions.testFlag(GarminTrackPointExtension)) { + gpx.setAttribute(QLatin1String("xmlns:gpxtpx"), + QLatin1String("http://www.garmin.com/xmlschemas/TrackPointExtension/v1")); + } doc.appendChild(gpx); QDomElement metaData = doc.createElement(QLatin1String("metadata")); @@ -1276,7 +1327,15 @@ QDomDocument TrainingSession::toGPX(const QDateTime &creationTime) const const QDateTime startTime = getDateTime(firstMap( route.value(QLatin1String("timestamp")))); - // Get the number of samples. + // Get the "samples" samples. + const QVariantMap samples = map.value(SAMPLES).toMap(); + const QVariantList cadence = samples.value(QLatin1String("cadence")).toList(); + const QVariantList distance = samples.value(QLatin1String("distance")).toList(); + const QVariantList forwardAcceleration = samples.value(QLatin1String("fwd-acceleration")).toList(); + const QVariantList heartrate = samples.value(QLatin1String("heartrate")).toList(); + const QVariantList temperature = samples.value(QLatin1String("temperature")).toList(); + + // Get the "route" samples. const QVariantList altitude = route.value(QLatin1String("altitude")).toList(); const QVariantList duration = route.value(QLatin1String("duration")).toList(); const QVariantList latitude = route.value(QLatin1String("latitude")).toList(); @@ -1328,6 +1387,93 @@ QDomDocument TrainingSession::toGPX(const QDateTime &creationTime) const timeOffset).toString(Qt::ISODate))); trkpt.appendChild(doc.createElement(QLatin1String("sat"))) .appendChild(doc.createTextNode(satellites.at(index).toString())); + + if (gpxOptions.testFlag(CluetrustGpxDataExtension) || + gpxOptions.testFlag(GarminAccelerationExtension) || + gpxOptions.testFlag(GarminTrackPointExtension)) + { + QDomElement extensions = doc.createElement(QLatin1String("extensions")); + + if (gpxOptions.testFlag(CluetrustGpxDataExtension)) { + if ((index < heartrate.length()) && + (!sensorOffline(samples.value(QLatin1String("heartrate-offline")).toList(), index))) { + extensions.appendChild(doc.createElement(QLatin1String("gpxdata:hr"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(heartrate.at(index).toUInt()))); + } + + if ((index < cadence.length()) && + (!sensorOffline(samples.value(QLatin1String("altitude-offline")).toList(), index))) { + extensions.appendChild(doc.createElement(QLatin1String("gpxdata:cadence"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(cadence.at(index).toUInt()))); + } + + if (index < temperature.length()) { + extensions.appendChild(doc.createElement(QLatin1String("gpxdata:temp"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(temperature.at(index).toFloat()))); + } + + if ((index < distance.length()) && + (!sensorOffline(samples.value(QLatin1String("distance-offline")).toList(), index))) { + /// @todo Include optional gpxdata:sensor="wheel|pedometer" attribute. + extensions.appendChild(doc.createElement(QLatin1String("gpxdata:distance"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(distance.at(index).toUInt()))); + } + } + + if (gpxOptions.testFlag(GarminAccelerationExtension)) { + QDomElement accelerationExtension = doc.createElement( + QLatin1String("gpxax:AccelerationExtension")); + + if ((index < forwardAcceleration.length()) && + (!sensorOffline(samples.value(QLatin1String("fwd-acceleration-offline")).toList(), index))) { + QDomElement accel = doc.createElement(QLatin1String("gpxax:accel")); + accel.setAttribute(QLatin1String("x"), QString::fromLatin1("%1") + .arg(forwardAcceleration.at(index).toFloat())); + accel.setAttribute(QLatin1String("y"), QLatin1String("0")); + accel.setAttribute(QLatin1String("z"), QLatin1String("0")); + accelerationExtension.appendChild(accel); + } + + extensions.appendChild(accelerationExtension); + } + + if (gpxOptions.testFlag(GarminTrackPointExtension)) { + QDomElement trackPointExtension = doc.createElement( + QLatin1String("gpxtpx:TrackPointExtension")); + + if (index < temperature.length()) { + trackPointExtension.appendChild(doc.createElement(QLatin1String("gpxtpx:atemp"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(temperature.at(index).toFloat()))); + } + + if ((index < heartrate.length()) && + (!sensorOffline(samples.value(QLatin1String("heartrate-offline")).toList(), index))) { + const uint hr = heartrate.at(index).toUInt(); + if ((hr >= 1) && (hr <= 255)) { // Schema enforced. + trackPointExtension.appendChild(doc.createElement(QLatin1String("gpxtpx:hr"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1").arg(hr))); + } + } + + if ((index < cadence.length()) && + (!sensorOffline(samples.value(QLatin1String("altitude-offline")).toList(), index))) { + const uint cad = cadence.at(index).toUInt(); + if (cad <= 254) { // Schema enforced. + trackPointExtension.appendChild(doc.createElement(QLatin1String("gpxtpx:cad"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1").arg(cad))); + } + } + + extensions.appendChild(trackPointExtension); + } + + trkpt.appendChild(extensions); + } trkseg.appendChild(trkpt); } } @@ -1351,9 +1497,13 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const const QVariantList rrsamples = map.value(RRSAMPLES).toMap().value(QLatin1String("value")).toList(); - const bool haveAltitude = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("speed")))); - const bool haveCadence = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("cadence")))); - const bool haveSpeed = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("altitude")))); + const bool haveAltitude = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("speed")))); + const bool haveCadence = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("cadence")))); + const bool havePowerLeft = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("left-pedal-power")))); + const bool havePowerRight = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("right-pedal-power")))); + const bool havePower = (havePowerLeft || havePowerRight); + const bool havePowerBalance = (havePowerLeft && havePowerRight); + const bool haveSpeed = ((!rrDataOnly) && (haveAnySamples(samples, QLatin1String("altitude")))); QString hrmData; QTextStream stream(&hrmData); @@ -1364,13 +1514,13 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const "Version=106\r\n" "Monitor=1\r\n" "SMode="; - stream << (haveSpeed ? '1' : '0'); // a) Speed - stream << (haveCadence ? '1' : '0'); // b) Cadence - stream << (haveAltitude ? '1' : '0'); // c) Altitude + stream << (haveSpeed ? '1' : '0'); // a) Speed + stream << (haveCadence ? '1' : '0'); // b) Cadence + stream << (haveAltitude ? '1' : '0'); // c) Altitude + stream << (havePower ? '1' : '0'); // d) Power + stream << (havePowerBalance ? '1' : '0'); // e) Power Left Right Balance stream << - "0" // d) Power (not supported by V800 yet). - "0" // e) Power Left Right Ballance (not supported by V800 yet). - "0" // f) Power Pedalling Index (not supported by V800 yet). + "0" // f) Power Pedalling Index (does not appear to be supported by FlowSync). "0" // g) HR/CC data (available only with Polar XTrainer Plus). "0" // h) US / Euro unit (always metric). "0" // i) Air pressure (not available). @@ -1506,7 +1656,7 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const stream << '\t' << first(hrStats.value(QLatin1String("maximum"))).toUInt(); stream << "\r\n"; // Row 2 - stream << "28"; + stream << "28"; // All three "extra data" fields present (on row 3). stream << "\t0"; // Recovery time (seconds); data not available. stream << "\t0"; // Recovery HR (bpm); data not available. stream << "\t" << qRound(first(firstMap(stats.value(QLatin1String("speed"))) @@ -1515,8 +1665,14 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const .value(QLatin1String("maximum"))).toUInt(); stream << "\t0"; // Momentary altitude; not available per lap. stream << "\r\n"; - // Row 3 - stream << qRound(first(header.value(QLatin1String("descent"))).toFloat() * 10.0); + // Row 3: HRM allows up to three "extra data" fields. Here we + // choose to leave out descent if power is available. + if (havePower) { + stream << first(firstMap(header.value(QLatin1String("power"))) + .value(QLatin1String("average"))).toUInt() * 10; + } else { + stream << qRound(first(header.value(QLatin1String("descent"))).toFloat() * 10.0); + } stream << '\t' << (first(firstMap(stats.value(QLatin1String("pedaling"))) .value(QLatin1String("average"))).toUInt() * 10); stream << '\t' << qRound(first(firstMap(stats.value(QLatin1String("incline"))) @@ -1568,7 +1724,11 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const // all multiplied by 10, to extra data can hold 0..300 units. if (!laps.isEmpty()) { stream << "\r\n[ExtraData]\r\n"; - stream << "Descent\r\nMeters\t1000\t0\r\n"; + if (havePower) { + stream << "Power\r\nW\t3000\t0\r\n"; + } else { + stream << "Descent\r\nMeters\t1000\t0\r\n"; + } stream << "Pedaling Index\r\n%\t100\t0\r\n"; stream << "Max Incline\r\nDegrees\t90\t0\r\n"; } @@ -1663,15 +1823,17 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const stream << sample.toUInt() << "\r\n"; } } else { - const QVariantList altitude = samples.value(QLatin1String("altitude")).toList(); - const QVariantList cadence = samples.value(QLatin1String("cadence")).toList(); - const QVariantList speed = samples.value(QLatin1String("speed")).toList(); + const QVariantList altitude = samples.value(QLatin1String("altitude")).toList(); + const QVariantList cadence = samples.value(QLatin1String("cadence")).toList(); + const QVariantList speed = samples.value(QLatin1String("speed")).toList(); + const QVariantList powerLeft = samples.value(QLatin1String("left-pedal-power")).toList(); + const QVariantList powerRight = samples.value(QLatin1String("right-pedal-power")).toList(); for (int index = 0; index < heartrate.length(); ++index) { stream << ((index < heartrate.length()) ? heartrate.at(index).toUInt() : (uint)0); if (haveSpeed) { stream << '\t' << ((index < speed.length()) - ? qRound(speed.at(index).toFloat() * 10.0) : ( int)0); + ? qRound(speed.at(index).toFloat() * 10.0) : (int)0); } if (haveCadence) { stream << '\t' << ((index < cadence.length()) @@ -1679,10 +1841,26 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const } if (haveAltitude) { stream << '\t' << ((index < altitude.length()) - ? qRound(altitude.at(index).toFloat()) : ( int)0); + ? qRound(altitude.at(index).toFloat()) : (int)0); + } + if (havePower) { + const int currentPowerLeft = (index < powerLeft.length()) ? + first(powerLeft.at(index).toMap().value(QLatin1String("current-power"))).toInt() : 0; + const int currentPowerRight = (index < powerRight.length()) ? + first(powerRight.at(index).toMap().value(QLatin1String("current-power"))).toInt() : 0; + const int currentPower = currentPowerLeft + currentPowerRight; + stream << '\t' << currentPower; + if (havePowerBalance) { + // Convert the left and right powers into a left-right balance percentage. + const int leftBalance = (currentPower == 0) ? 0 : + qRound(100.0 * (float)currentPowerLeft / (float)currentPower); + if (leftBalance > 100) { + qWarning() << "leftBalance of " << leftBalance << "% is greater than 100%"; + } + /// @todo Include Pedalling Index here, if/when available. + stream << '\t' << qMax(qMin(leftBalance, 255), 0); + } } - // Power (Watts) - not yet supported by Polar. - // Power Balance and Pedalling Index - not yet supported by Polar. // Air pressure - not available in protobuf data. stream << "\r\n"; } @@ -1707,8 +1885,6 @@ QStringList TrainingSession::toHRM(const bool rrDataOnly) const */ QDomDocument TrainingSession::toTCX(const QString &buildTime) const { - Q_UNUSED(buildTime); - QDomDocument doc; doc.appendChild(doc.createProcessingInstruction(QLatin1String("xml"), QLatin1String("version='1.0' encoding='utf-8'"))); @@ -1725,6 +1901,10 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const tcx.setAttribute(QLatin1String("xmlns:ax2"), QLatin1String("http://www.garmin.com/xmlschemas/ActivityExtension/v2")); } + if (tcxOptions.testFlag(GarminCourseExtension)) { + tcx.setAttribute(QLatin1String("xmlns:cx1"), + QLatin1String("http://www.garmin.com/xmlschemas/CourseExtension/v1")); + } doc.appendChild(tcx); QDomElement activities = doc.createElement(QLatin1String("Activities")); @@ -1756,6 +1936,8 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const // Get the "samples" samples. const QVariantList altitude = samples.value(QLatin1String("altitude")).toList(); const QVariantList cadence = samples.value(QLatin1String("cadence")).toList(); + const QVariantList powerLeft = samples.value(QLatin1String("left-pedal-power")).toList(); + const QVariantList powerRight = samples.value(QLatin1String("right-pedal-power")).toList(); const QVariantList distance = samples.value(QLatin1String("distance")).toList(); const QVariantList heartrate = samples.value(QLatin1String("heartrate")).toList(); const QVariantList speed = samples.value(QLatin1String("speed")).toList(); @@ -1886,7 +2068,9 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const lap.appendChild(track); // Add any enabled extensions. - if (tcxOptions.testFlag(GarminActivityExtension)) { + if (tcxOptions.testFlag(GarminActivityExtension) || + tcxOptions.testFlag(GarminCourseExtension)) + { QDomElement extensions = doc.createElement(QLatin1String("Extensions")); lap.appendChild(extensions); @@ -1927,7 +2111,37 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const .arg(first(cadence.value(QLatin1String("maximum"))).toUInt()))); } - /// @todo AvgWatts and MaxWatts when power data is available. + /// @todo Steps + + // Note, AvgWatts is defined by both the Garmin Activity + // Extension and the Garmin Course Extension schemas. + const QVariantMap power = firstMap(base.value(QLatin1String("power"))); + if (power.contains(QLatin1String("average"))) { + lx.appendChild(doc.createElement(QLatin1String("AvgWatts"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(power.value(QLatin1String("average"))).toUInt()))); + } + if (power.contains(QLatin1String("maximum"))) { + lx.appendChild(doc.createElement(QLatin1String("MaxWatts"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(power.value(QLatin1String("maximum"))).toUInt()))); + } + } + + if (tcxOptions.testFlag(GarminCourseExtension)) { + QDomElement cx = doc.createElement(QLatin1String("CX")); + cx.setAttribute(QLatin1String("xmlns"), + QLatin1String("http://www.garmin.com/xmlschemas/CourseExtension/v1")); + extensions.appendChild(cx); + + // Note, AvgWatts is defined by both the Garmin Activity + // Extension and the Garmin Course Extension schemas. + const QVariantMap power = firstMap(base.value(QLatin1String("power"))); + if (power.contains(QLatin1String("average"))) { + cx.appendChild(doc.createElement(QLatin1String("AvgWatts"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(power.value(QLatin1String("average"))).toUInt()))); + } } } } @@ -1992,6 +2206,17 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const .appendChild(doc.createTextNode(cadence.at(index).toString())); } } + + const int currentPowerLeft = (index < powerLeft.length()) ? + first(powerLeft.at(index).toMap().value(QLatin1String("current-power"))).toInt() : 0; + const int currentPowerRight = (index < powerRight.length()) ? + first(powerRight.at(index).toMap().value(QLatin1String("current-power"))).toInt() : 0; + const int currentPower = currentPowerLeft + currentPowerRight; + if (currentPower != 0) { + tpx.appendChild(doc.createElement(QLatin1String("Watts"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(currentPower))); + } } if (trackPoint.hasChildNodes()) { diff --git a/src/bipolar/polar/v2/trainingsession.h b/src/bipolar/polar/v2/trainingsession.h index d2e7fc3..e8c274f 100644 --- a/src/bipolar/polar/v2/trainingsession.h +++ b/src/bipolar/polar/v2/trainingsession.h @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. @@ -52,8 +52,9 @@ class TrainingSession : public QObject { Q_DECLARE_FLAGS(OutputFormats, OutputFormat) enum GpxOption { - CluetrustGpxExtension = 0x0100, - GarminTrackPointExtension = 0x0200, + CluetrustGpxDataExtension = 0x0100, + GarminAccelerationExtension = 0x0200, + GarminTrackPointExtension = 0x0400, }; Q_DECLARE_FLAGS(GpxOptions, GpxOption) @@ -66,7 +67,7 @@ class TrainingSession : public QObject { enum TcxOption { ForceTcxUTC = 0x0001, GarminActivityExtension = 0x0100, - //GarminCourseExtension = 0x0200, //< Needs power support. + GarminCourseExtension = 0x0200, }; Q_DECLARE_FLAGS(TcxOptions, TcxOption) diff --git a/src/bipolar/protobuf/fixnum.cpp b/src/bipolar/protobuf/fixnum.cpp index 1bb9981..92f9276 100644 --- a/src/bipolar/protobuf/fixnum.cpp +++ b/src/bipolar/protobuf/fixnum.cpp @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/fixnum.h b/src/bipolar/protobuf/fixnum.h index 9ebf8a8..c0edb00 100644 --- a/src/bipolar/protobuf/fixnum.h +++ b/src/bipolar/protobuf/fixnum.h @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/message.cpp b/src/bipolar/protobuf/message.cpp index 0cf90e3..da4deab 100644 --- a/src/bipolar/protobuf/message.cpp +++ b/src/bipolar/protobuf/message.cpp @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/message.h b/src/bipolar/protobuf/message.h index 4dec50e..4dbd0c1 100644 --- a/src/bipolar/protobuf/message.h +++ b/src/bipolar/protobuf/message.h @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/types.cpp b/src/bipolar/protobuf/types.cpp index 9cf538a..8ab7668 100644 --- a/src/bipolar/protobuf/types.cpp +++ b/src/bipolar/protobuf/types.cpp @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/types.h b/src/bipolar/protobuf/types.h index 893b01c..473bc0d 100644 --- a/src/bipolar/protobuf/types.h +++ b/src/bipolar/protobuf/types.h @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/varint.cpp b/src/bipolar/protobuf/varint.cpp index ea543ee..b23e392 100644 --- a/src/bipolar/protobuf/varint.cpp +++ b/src/bipolar/protobuf/varint.cpp @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/bipolar/protobuf/varint.h b/src/bipolar/protobuf/varint.h index 9ec3c49..a30f5ae 100644 --- a/src/bipolar/protobuf/varint.h +++ b/src/bipolar/protobuf/varint.h @@ -1,5 +1,5 @@ /* - Copyright 2014 Paul Colby + Copyright 2014-2015 Paul Colby This file is part of Bipolar. diff --git a/src/usb/v800usb.cpp b/src/usb/v800usb.cpp index 555774a..eeaefc0 100755 --- a/src/usb/v800usb.cpp +++ b/src/usb/v800usb.cpp @@ -197,11 +197,9 @@ void V800usb::upload_route(QString route) new_route_num = 0; - /* put_v800_dir(tr("/U/0/ROUTES/")); put_v800_dir(QString(tr("/U/0/ROUTES/%1/")).arg(new_route_num)); put_v800_data(QString(tr("%1/PROUTE.BPB")).arg(route), QString(tr("/U/0/ROUTES/%1/PROUTE.BPB")).arg(new_route_num)); - */ remove_v800_dir(tr("/U/0/FAV")); put_v800_dir(tr("/U/0/FAV/")); diff --git a/src/widgets/v800main.cpp b/src/widgets/v800main.cpp index 67478be..062587a 100755 --- a/src/widgets/v800main.cpp +++ b/src/widgets/v800main.cpp @@ -361,10 +361,12 @@ void V800Main::on_fsBtn_clicked() void V800Main::on_uploadBtn_clicked() { + /* QString route = QFileDialog::getExistingDirectory(this, tr("Open Route"), QDir::homePath(), QFileDialog::ShowDirsOnly); if(!route.isEmpty()) emit upload_route(route); + */ } void V800Main::on_dirSelectBtn_clicked() From 600a59ccf92cab2ed57795a3f22ea4bff378e855 Mon Sep 17 00:00:00 2001 From: profanum429 Date: Wed, 18 Apr 2018 19:09:15 -0500 Subject: [PATCH 14/14] Update README.md Been gone forever, no longer have any Polar products, just wanted to close this out. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 63d254b..f2ce87b 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ v800_downloader =============== +Update: I haven't followed this in roughly 4 years; I kinda forgot it existed, sorry guys! I haven't owned a V800 for probably 3 years now so I can't do any testing or changes for stuff. Probably already have but consider this pretty much abandoned / no more work on it (not changing much from what it's been at though, lol). Feel free to fork and modify to heart's content. + Update: Release 5 fixes a bug where some garbage data would result in thinking files and directories existed where they did not and had obviously faulty names. This has been fixed. I only saw this after I did a factory reset, so it might not occur in normal usage but it shouldn't be an issue now. Update: Release 4 now properly supports multisport sessions. Exported files will have a _(NUMBER) appended to them before the extension. This number is the order of the sport in the multisport session. Single sport sessions now have a _0 appended before the extension to represent that this is sport 1 of the session.