Files
GoldenCheetah/src/Bin2RideFile.cpp

660 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (c) 2012 Damien Grauser (Damien.Grauser@pev-geneve.ch)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "Bin2RideFile.h"
#include <math.h>
#define START 0x210
#define UNIT_VERSION 0x2000
#define SYSTEM_INFO 0x2003
#define BIN2_DEBUG false // debug traces
static int bin2FileReaderRegistered =
RideFileFactory::instance().registerReader(
"bin2", "Joule GPS File", new Bin2FileReader());
struct Bin2FileReaderState
{
QFile &file;
QStringList &errors;
RideFile *rideFile;
int data_version;
double secs, km;
int interval;
double last_interval_secs;
bool jouleGPS;
bool stopped;
QString deviceInfo;
Bin2FileReaderState(QFile &file, QStringList &errors) :
file(file), errors(errors), rideFile(NULL), secs(0), km(0),
interval(0), last_interval_secs(0.0), jouleGPS(true), stopped(true)
{
}
struct TruncatedRead {};
int bcd2Int(char c)
{
return (0xff & c) - (((0xff & c)/16)*6);
}
double read_bytes(int len, int *count = NULL, int *sum = NULL)
{
char c;
double res = 0;
for (int i = 0; i < len; ++i) {
if (file.read(&c, 1) != 1)
throw TruncatedRead();
if (sum)
*sum += (0xff & c);
if (count)
*count += 1;
res += pow(256,i) * (0xff & (unsigned) c) ;
}
return res;
}
QString read_text(int len, int *count = NULL, int *sum = NULL)
{
char c;
QString res = "";
for (int i = 0; i < len; ++i) {
if (file.read(&c, 1) != 1)
throw TruncatedRead();
if (sum)
*sum += (0xff & c);
if (count)
*count += 1;
res += c;
}
return res;
}
QDateTime read_date(int *bytes_read = NULL, int *sum = NULL)
{
int sec = bcd2Int(read_bytes(1, bytes_read, sum));
int min = bcd2Int(read_bytes(1, bytes_read, sum));
int hour = bcd2Int(read_bytes(1, bytes_read, sum));
int day = bcd2Int(read_bytes(1, bytes_read, sum));
int month = bcd2Int(read_bytes(1, bytes_read, sum));
int year = bcd2Int(read_bytes(1, bytes_read, sum));
return QDateTime(QDate(2000+year,month,day), QTime(hour,min,sec));
}
QDateTime read_RTC_mark(double *secs, int *bytes_read = NULL, int *sum = NULL)
{
QDateTime date = read_date(bytes_read, sum);
if (jouleGPS) {
read_bytes(1, bytes_read, sum); // dummy
read_bytes(4, bytes_read, sum); // time_moving
// time
double newsecs = double(read_bytes(4, bytes_read, sum));
if (BIN2_DEBUG)
qDebug() << "read_RTC_mark new ridetime" << newsecs << "secs (old" << *secs <<"secs)";
*secs = newsecs;
read_bytes(16, bytes_read, sum); // dummy
} else {
read_bytes(13, bytes_read, sum); // dummy
}
return date;
}
int read_interval_mark(double *secs, int *bytes_read = NULL, int *sum = NULL)
{
int intervalNumber = read_bytes(1, bytes_read, sum);
read_bytes(2, bytes_read, sum); // dummy
double rideTime = double(read_bytes(4, bytes_read, sum));
if (BIN2_DEBUG)
qDebug() << "read_interval_mark" << rideTime << "at" << *secs <<"secs";
if (jouleGPS)
read_bytes(24, bytes_read, sum); // dummy
else
read_bytes(12, bytes_read, sum); // dummy
return intervalNumber;
}
void read_detail_record(double *secs, int *bytes_read = NULL, int *sum = NULL)
{
int cad = 0;
int lrbal = 0;
int hr = 0;
int watts = 0;
int nm = 0;
double kph = 0.0;
int alt = 0;
double temp = RideFile::NoTemp;
double lat = 0.0;
double lng = 0.0;
double km = 0.0;
if (jouleGPS) {
cad = read_bytes(1, bytes_read, sum);
read_bytes(1, bytes_read, sum); // pedal_smoothness
lrbal = read_bytes(1, bytes_read, sum);
hr = read_bytes(1, bytes_read, sum);
read_bytes(1, bytes_read, sum); // dummy
watts = read_bytes(2, bytes_read, sum);
nm = read_bytes(2, bytes_read, sum);
kph = read_bytes(2, bytes_read, sum);
alt = read_bytes(2, bytes_read, sum); // todo this value is signed
temp = read_bytes(2, bytes_read, sum); // °C × 10 todo this value is signed
lat = read_bytes(4, bytes_read, sum); // todo this value is signed
lng = read_bytes(4, bytes_read, sum); // todo this value is signed
km = read_bytes(8, bytes_read, sum)/1000.0/1000.0;
} else {
read_bytes(1, bytes_read, sum); // dropout flag
cad = read_bytes(1, bytes_read, sum);
hr = read_bytes(1, bytes_read, sum);
kph = read_bytes(2, bytes_read, sum);
watts = read_bytes(2, bytes_read, sum);
nm = read_bytes(2, bytes_read, sum);
alt = read_bytes(2, bytes_read, sum); // todo this value is signed
temp = read_bytes(2, bytes_read, sum); // °C × 10 todo this value is signed
read_bytes(2, bytes_read, sum); // dummy
km = read_bytes(4, bytes_read, sum)/1000.0/1000.0;
}
// Validations
if (lrbal == 0xFF)
lrbal = 0;
else if ((lrbal & 0x200) == 0x200)
lrbal = 100-(lrbal & 0x3F);
else
lrbal = lrbal & 0x3F;
if (cad == 0xFF)
cad = 0;
if (hr == 0xFF)
hr = 0;
if (watts == 0xFFFF) // 65535
watts = 0;
if (kph == 0xFFFF) // 65535
kph = 0;
else
kph = kph/10.0;
if (temp == 0x8000) //0x8000 = invalid
temp = RideFile::NoTemp;
else if (temp > 0x7FFF) // Negative
temp = (temp-0xFFFF)/10.0;
else
temp = temp/10.0;
if (alt == 0x8000)
alt = 0;
else if (alt > 0x7FFF) // Negative
alt = (alt-0xFFFF);
if (lat == 0x80000000) //0x80000000 = invalid
lat = 0;
else if (lat > 0x7FFFFFFF) // Negative
lat = (lat-0xFFFFFFFF)/10000000.0;
else
lat = lat/10000000.0;
if (lng == 0x80000000) //0x80000000 = invalid
lng = 0;
else if (lng > 0x7FFFFFFF) // Negative
lng = (lng-0xFFFFFFFF)/10000000.0;
else
lng = lng/10000000.0;
// 0.0 values at end are for garmin vector torque efficiency/pedal smoothness which are not available
rideFile->appendPoint(*secs, cad, hr, km, kph, nm, watts, alt, lng, lat, 0.0, 0, temp, lrbal, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, interval);
(*secs)++;
}
void read_header(uint16_t &header, uint16_t &command, uint16_t &length, int *bytes_read = NULL, int *sum = NULL)
{
header = read_bytes(2, bytes_read, sum);
command = read_bytes(2, bytes_read, sum);
length = read_bytes(2, bytes_read, sum);
}
void read_ride_summary(int *bytes_read = NULL, int *sum = NULL)
{
data_version = read_bytes(1, bytes_read, sum); // data_version
read_bytes(1, bytes_read, sum); // firmware_minor_version
QDateTime t = read_date(bytes_read, sum);
rideFile->setStartTime(t);
if (jouleGPS)
{
read_bytes(148, bytes_read, sum);
if (data_version >= 4)
read_bytes(8, bytes_read, sum);
if (data_version >= 6)
read_bytes(8, bytes_read, sum);
} else
{
read_bytes(84, bytes_read, sum);
}
}
void read_interval_summary(int *bytes_read = NULL, int *sum = NULL)
{
int interval_summary_size = 32;
if (data_version>=4)
interval_summary_size = 36;
read_bytes(interval_summary_size*100, bytes_read, sum);
}
void read_username(int *bytes_read = NULL, int *sum = NULL)
{
QString name = read_text(20, bytes_read, sum);
//deviceInfo += QString("User : %1\n").arg(name);
}
void read_user_info_record(int *bytes_read = NULL, int *sum = NULL)
{
read_bytes(64, bytes_read, sum);
int smartbelt_A = read_bytes(2, bytes_read, sum);
int smartbelt_B = read_bytes(2, bytes_read, sum);
int smartbelt_C = read_bytes(2, bytes_read, sum);
deviceInfo += QString("Smartbelt %1-%2-%3\n").arg(smartbelt_A).arg(smartbelt_B).arg(smartbelt_C);
read_bytes(42, bytes_read, sum);
if (data_version>=6)
read_bytes(4, bytes_read, sum);
if (data_version>=8)
read_bytes(20, bytes_read, sum);
}
void read_ant_info_record(int *bytes_read = NULL, int *sum = NULL)
{
for (int i = 0; i < 6; i++) {
int device_type = read_bytes(1, bytes_read, sum);
if (device_type < 255) {
QString text = read_text(20, bytes_read, sum);
for(int i=0; i < text.length(); i++)
{
if (text.at(i) == QChar(0))
text = text.left(i);
}
//while (text.endsWith( QChar(0) )) text.chop(1);
read_bytes(1, bytes_read, sum); // flag
uint16_t id = read_bytes(2, bytes_read, sum);
read_bytes(2, bytes_read, sum);
read_bytes(2, bytes_read, sum);
QString device_type_str;
if (device_type == 11)
device_type_str = "Primary Power Id";
else if (device_type == 120)
device_type_str = "Chest strap Id";
else
device_type_str = QString("ANT %1 Id").arg(device_type);
deviceInfo += QString("%1 %2 %3\n").arg(device_type_str).arg(id).arg(text);
}
}
}
// pages
int read_summary_page()
{
int sum = 0;
int bytes_read = 0;
char header1 = read_bytes(1, &bytes_read, &sum); // Always 0x10
char header2 = read_bytes(1, &bytes_read, &sum); // Always 0x02
uint16_t command = read_bytes(2, &bytes_read, &sum);
if (header1 == 0x10 && header2 == 0x02 && command == 0x2022)
{
uint16_t length = read_bytes(2, &bytes_read, &sum);
uint16_t page_number = read_bytes(2, &bytes_read, &sum); // Page #
if (page_number == 0) {
// Page #0
if (jouleGPS) {
read_ride_summary(&bytes_read, &sum);
read_interval_summary(&bytes_read, &sum);
read_username(&bytes_read, &sum);
read_user_info_record(&bytes_read, &sum);
read_ant_info_record(&bytes_read, &sum);
} else {
read_ride_summary(&bytes_read, &sum);
// page still contains 47 interval_summary
}
int finish = length+6-bytes_read;
for (int i = 0; i < finish; i++) {
read_bytes(1, &bytes_read, &sum); // to finish
}
read_bytes(1, &bytes_read, &sum); // checksum
} else {
if (page_number == 1 && !jouleGPS) {
//page still contains 48 interval_summary
int finish = length+6-bytes_read;
for (int i = 0; i < finish; i++) {
read_bytes(1, &bytes_read, &sum); // to finish
}
read_bytes(1, &bytes_read, &sum); // checksum
}
// not a summary page !
}
}
return bytes_read;
}
int read_detail_page()
{
int sum = 0;
int bytes_read = 0;
char header1 = read_bytes(1, &bytes_read, &sum); // Always 0x10
char header2 = read_bytes(1, &bytes_read, &sum); // Always 0x02
uint16_t command = read_bytes(2, &bytes_read, &sum);
if (header1 == 0x10 && header2 == 0x02 && command == 0x2022)
{
read_bytes(2, &bytes_read, &sum); // length
uint16_t page_number = read_bytes(2, &bytes_read, &sum); // Page #
if (page_number == 2 && !jouleGPS) {
//page still contains 5 interval_summary in 2 blocks of 256k
read_bytes(512, &bytes_read, &sum);
}
if (page_number > 0) {
if (BIN2_DEBUG)
qDebug() << "page_number" << page_number;
int nb_record = 128;
// Page # >0
// Joule GPS : 128 x record (32k)
if (!jouleGPS) {
// Page # = 2
// Joule : 168 x record (20k)
// Page # > 1
// Joule : 192 x record (20k)
if (page_number == 2)
nb_record = 168;
else
nb_record = 192;
}
QDateTime t;
for (int i = 0; i < nb_record; i++) {
int flag = read_bytes(1, &bytes_read, &sum);
//b0..b1: "00" Detail Record
//b0..b1: "01" RTC Mark Record
//b0..b1: "10" Session Mark (Joule GPS)
//b0..b1: "10" Interval Record (Joule 1.0)
//b0..b1: "11" Interval Record (Joule GPS)
//b0..b1: "11" Invalid ( 0xFF)
//b3: Power was calculated
//b4: HR packet missing
//b5: CAD packet missing
//b6: PWR packet missing
//b7: Power data = old (No new power calculated)
if (BIN2_DEBUG && (flag & 0x03) != 0) {
qDebug() << "flag B0..B1" << (flag & 0x03) << "flag" << flag;
if (flag == 0xff) {
qDebug() << "Invalid record";
}
else {
if ((flag & 0x08) == 0x08) {
qDebug() << "Power was calculated";
}
if ((flag & 0x10) == 0x10) {
qDebug() << "HPR packet missing";
}
if ((flag & 0x20) == 0x20) {
qDebug() << "CAD packet missing";
}
if ((flag & 0x40) == 0x40) {
qDebug() << "PWR packet missing";
}
if ((flag & 0x80) == 0x80) {
qDebug() << "Power data = old";
}
}
}
if (flag == 0xff) {
// means invalid entry
if (BIN2_DEBUG)
qDebug() << "invalid at" << secs << "secs";
if (jouleGPS)
read_bytes(31, &bytes_read, &sum); // dummy
else
read_bytes(19, &bytes_read, &sum); // dummy
//increase seconds
(secs)++;
}
else if ((flag & 0x03) == 0x01){
// The RTC Mark is written when there is a gap in the ride data,
// or when the internal Real Time Clock is adjusted.
t= read_RTC_mark(&secs, &bytes_read, &sum);
}
else if ((jouleGPS && (flag & 0x03) == 0x03) || (!jouleGPS && (flag & 0x03) == 0x02)) {
// The Interval Mark immediately precedes a Detail Record at the same RTC time.
int t = read_interval_mark(&secs, &bytes_read, &sum);
interval = t;
}
else if ((flag & 0x03) == 0x00 ){
// The detail record contains current telemetry data.
read_detail_record(&secs, &bytes_read, &sum);
} else {
if (BIN2_DEBUG)
qDebug() << "UNKNOW: flag" << flag << "at" << secs << "secs";
if (jouleGPS)
read_bytes(31, &bytes_read, &sum); // dummy
else
read_bytes(19, &bytes_read, &sum); // dummy
}
if (!jouleGPS && (i+1)%12 == 0)
read_bytes(16, &bytes_read, &sum); // unused
}
read_bytes(1, &bytes_read, &sum); // checksum
}
}
return bytes_read;
}
int read_version()
{
int sum = 0;
int bytes_read = 0;
uint16_t header = read_bytes(2, &bytes_read, &sum); // Always 0x210 (0x10-0x02)
uint16_t command = read_bytes(2, &bytes_read, &sum);
if (header == START && command == UNIT_VERSION)
{
read_bytes(2, &bytes_read, &sum); // length
int major_version = read_bytes(1, &bytes_read, &sum);
int minor_version = read_bytes(2, &bytes_read, &sum);
int data_version = 1;
if (major_version == 18)
{
jouleGPS = false;
} else
{
jouleGPS = true;
}
if (jouleGPS)
data_version = read_bytes(2, &bytes_read, &sum);
QString version = QString(minor_version<100?"%1.0%2 (%3)":"%1.%2 (%3)").arg(major_version).arg(minor_version).arg(data_version);
if (jouleGPS)
deviceInfo += "Joule GPS";
else
deviceInfo += "Joule";
deviceInfo += QString(" Version %1\n").arg(version);
read_bytes(1, &bytes_read, &sum); // checksum
}
return bytes_read;
}
int read_system_info()
{
int sum = 0;
int bytes_read = 0;
uint16_t header = read_bytes(2, &bytes_read, &sum); // Always (0x10-0x02)
uint16_t command = read_bytes(2, &bytes_read, &sum);
if (header == START && command == SYSTEM_INFO)
{
read_bytes(2, &bytes_read, &sum); // length
if (jouleGPS)
read_bytes(52, &bytes_read, &sum);
else
read_bytes(50, &bytes_read, &sum);
uint16_t odometer = read_bytes(8, &bytes_read, &sum);
deviceInfo += QString("Odometer %1km\n").arg(odometer/1000.0);
if (!jouleGPS)
read_bytes(34, &bytes_read, &sum);
read_bytes(1, &bytes_read, &sum); // checksum
}
return bytes_read;
}
RideFile * run() {
errors.clear();
rideFile = new RideFile;
rideFile->setFileFormat("CycleOps Joule (bin2)");
rideFile->setRecIntSecs(1);
if (!file.open(QIODevice::ReadOnly)) {
delete rideFile;
return NULL;
}
bool stop = false;
int data_size = file.size();
int bytes_read = 0;
bytes_read += read_version();
if (jouleGPS)
rideFile->setDeviceType("Joule GPS");
else
rideFile->setDeviceType("Joule");
bytes_read += read_system_info();
bytes_read += read_summary_page();
if (!jouleGPS)
bytes_read += read_summary_page();
while (!stop && (bytes_read < data_size)) {
bytes_read += read_detail_page(); // read_page(stop, errors);
}
rideFile->setTag("Device Info", deviceInfo);
if (stop) {
file.close();
delete rideFile;
return NULL;
} else {
file.close();
return rideFile;
}
}
};
RideFile *Bin2FileReader::openRideFile(QFile &file, QStringList &errors, QList<RideFile*>*) const
{
QSharedPointer<Bin2FileReaderState> state(new Bin2FileReaderState(file, errors));
return state->run();
}