From a0c5669166cb1f319224c8ccb682f7137bc45656 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Wed, 28 Apr 2010 21:34:06 +0100 Subject: [PATCH] Support for Polar SRD file format A ridefile reader for Polar .srd format files. The code is largely based upon code from the "s710" project. Since "s710" is dependant upon GD and a number of deployment tools and the fact that the code hasn't changed since May 2007 the workout code has been included directly into the SrdRideFile.h and SrdRideFile.cpp source files. 2 sample SRD files have been included in the test/rides directory which were kindly supplied by Ian Charles. --- src/SrdRideFile.cpp | 1130 ++++++++++++++++++++++ src/SrdRideFile.h | 670 +++++++++++++ src/src.pro | 2 + src/test/rides/20090506T164017.05256.srd | Bin 0 -> 5256 bytes src/test/rides/20090512T163932.03592.srd | Bin 0 -> 3592 bytes src/test/rides/descriptions.txt | 10 + 6 files changed, 1812 insertions(+) create mode 100644 src/SrdRideFile.cpp create mode 100644 src/SrdRideFile.h create mode 100644 src/test/rides/20090506T164017.05256.srd create mode 100644 src/test/rides/20090512T163932.03592.srd diff --git a/src/SrdRideFile.cpp b/src/SrdRideFile.cpp new file mode 100644 index 000000000..2b3d5d8cc --- /dev/null +++ b/src/SrdRideFile.cpp @@ -0,0 +1,1130 @@ +/* + * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com) + * + * 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 "SrdRideFile.h" + +// System includes +#include +#include +#include +#include +#include +#include +#include +#include + +static int srdFileReaderRegistered = + RideFileFactory::instance().registerReader( + "srd", "Polar SRD files", new SrdFileReader()); + +RideFile *SrdFileReader::openRideFile(QFile &file, QStringList &errorStrings) const +{ + workout_t * w; + S710_Filter filter = S710_FILTER_OFF; + + // use s710 lib to read in file and construct its workout_t structure + w = read_workout((char*)QFileInfo(file).absoluteFilePath().toLatin1().constData(),filter,S710_HRM_AUTO); + + if (w) { + // Success! + + // ok, so lets build up a new RideFile based upon the workout returned + RideFile *result = new RideFile; + + // + // first class variables + // + result->setRecIntSecs(w->recording_interval); + result->setStartTime(QDateTime(QDate(w->date.tm_year+1900, w->date.tm_mon+1, w->date.tm_mday), + QTime(w->date.tm_hour, w->date.tm_min, w->date.tm_sec, 0))); // (tm) S710_Date w->date + + switch (w->type) { + case S710_HRM_S610: result->setDeviceType("Polar S610"); break; + case S710_HRM_S710: result->setDeviceType("Polar S710"); break; + case S710_HRM_S810: result->setDeviceType("Polar S810"); break; + case S710_HRM_S625X: result->setDeviceType("Polar S625X"); break; + case S710_HRM_AUTO: + case S710_HRM_UNKNOWN: + default: + result->setDeviceType("Polar HRM"); + break; + } + + // + // metadata variables + // + result->setTag("Workout Code", w->exercise_label); + // Not available in current file format + // result->setTag("Device Info", ); + // result->setTag("Sport", ); + // result->setTag("Objective", ); + // result->setTag("Weight", ); + + // + // dataPoints + // + double time = 0; + for (int i = 0; i < w->samples; i++ ) { + double cad=0, hr=0, km=0, kph=0, nm=0, watts=0, alt=0, lon=0, lat=0, wind=0; + + // get values + hr = w->hr_data[i]; + if (S710_HAS_SPEED(w->mode)) kph = w->speed_data[i] * 10.0 / 160.0; + if (S710_HAS_CADENCE(w->mode)) cad = w->cad_data[i]; + if (S710_HAS_ALTITUDE(w->mode)) alt = w->alt_data[i]; + if (S710_HAS_POWER(w->mode)) watts = w->power_data[i].power; // note: pedal_index and lr balance not extracted + km = w->dist_data[i]; + + // add to ride + result->appendPoint(time, cad, hr, km, kph, nm, watts, alt, lon, lat, wind, 0); + + // keep a track of time + time += w->recording_interval; + } + + // + // intervals last so we can check against the data points + // + for (int i=0; ilaps; i++) { + + RideFileInterval add; + + // name is ok, but start stop need to be in time rather than datapoint no. + add.name = QString("Interval #%1").arg(i+1); + add.start = w->lap_data[i].split.seconds + + (60 * (w->lap_data[i].split.minutes)) + + (3600 * w->lap_data[i].split.hours); + + add.stop = w->lap_data[i].cumulative.seconds + + (60 * (w->lap_data[i].cumulative.minutes)) + + (3600 * w->lap_data[i].cumulative.hours); + + result->addInterval(add.start, add.stop, add.name); + } + + // free up s710 resources + free_workout(w); + + return result; + + } else { + // Failed to read + errorStrings << "Could not read file"; + } + return NULL; +} + + +//=================================================================================== +// +// ******************** +// +// FROM HERE TO BOTTOM OF SOURCE FILE ARE THE SRD WORKOUT ROUTINES +// INCLUDED FROM THE "S710" LINUX PROJECT see: http://code.google.com/p/s710 +// +// To avoid dependency on the s710 source (which has other dependencies) +// the code has been incorporated into this source file as an extract and +// converted to static functions within this source file (the original code +// is part of a library). The last modification date for this source was +// May 9th 2007, it is now largely a dormant project). +// +// The code has been /slightly/ modified in porting from C to C++ +// +// ******************** +// +// The original source is published under the GPL v2 License +// +// Original Author: +// Dave Bailey daveb net> +// +// Numerous bugfixes, protocol/file format information and feature enhancements +// have been contributed by (in alphabetical order by last name): +// Bjorn Andersson iki fi> +// Jani Averbach jaa iki fi> +// Markus Braun Braun krawel de> +// Robert Estes transmeta com> +// Michael Karbach physik uni-wuppertal de> +// Stefan Kleditzsch iwr uni-heidelberg de> +// Max Matveev sgi com> +// Berend Ozceri alumni cmu edu> +// Matti Tahvonen tahvonen com> +// Frank Vercruesse vercruesse de> +// Aldy Hernandez redhat com> +// +//=================================================================================== + +// modified from #defined straight string constants +static char S710_SPEED_KPH[] = "kph"; +static char S710_SPEED_MPH[] = "mph"; +static char S710_DISTANCE_KM[] = "km"; +static char S710_DISTANCE_MI[] = "mi"; +static char S710_ALTITUDE_M[] = "m"; +static char S710_ALTITUDE_FT[] = "ft"; +static char S710_TEMPERATURE_C[] = "C"; +static char S710_TEMPERATURE_F[] = "F"; + +static void diff_s710_time ( S710_Time *t1, S710_Time *t2, S710_Time *diff ) +{ + int t_t1; + int t_t2; + int t_diff; + int negative = 0; + + /* first compute t1 and t2 in tenths of a second */ + + t_t1 = ((t1->hours * 60 + t1->minutes) * 60 + t1->seconds) * 10 + t1->tenths; + t_t2 = ((t2->hours * 60 + t2->minutes) * 60 + t2->seconds) * 10 + t2->tenths; + + t_diff = t_t2 - t_t1; + if ( t_diff < 0 ) { + negative = 1; + t_diff = -t_diff; + } + + diff->tenths = t_diff % 10; + t_diff = (t_diff - diff->tenths) / 10; + + /* now t_diff is in seconds */ + + diff->seconds = t_diff % 60; + t_diff = (t_diff - diff->seconds) / 60; + + /* now t_diff is in minutes */ + + diff->minutes = t_diff % 60; + t_diff = (t_diff - diff->minutes) / 60; + + /* the rest is the hours */ + + diff->hours = t_diff; + + /* if we got a negative time, switch the sign of everything. */ + + if ( negative ) { + diff->hours = -diff->hours; + diff->minutes = -diff->minutes; + diff->seconds = -diff->seconds; + diff->tenths = -diff->tenths; + } +} + +//---------------------------------------------------------------------- +// +// SRD FILE READING FUNCTIONS +// +//---------------------------------------------------------------------- +static void read_preamble ( workout_t * w, unsigned char * buf ) +{ + /* number of bytes in the buffer (including these two) */ + + w->bytes = buf[0] + (buf[1]<<8); + + w->exercise_number = buf[2]; + if ( w->exercise_number > 0 && w->exercise_number <= 5 ) { + extract_label(&buf[3],&w->exercise_label,7); + } else { + strcpy(w->exercise_label,""); + } +} + + +static void read_date ( workout_t * w, unsigned char * buf ) +{ + /* date of workout */ + + w->date.tm_sec = BCD(buf[0]); + w->date.tm_min = BCD(buf[1]); + w->date.tm_hour = BCD(buf[2] & 0x7f); + + /* PATCH for AM/PM mode detection from Berend Ozceri */ + + w->ampm = S710_AM_PM_MODE_UNSET; + if ( buf[3] & 0x80 ) w->ampm |= S710_AM_PM_MODE_SET; + if ( buf[2] & 0x80 ) w->ampm |= S710_AM_PM_MODE_PM; + + w->date.tm_hour += (buf[3] & 0x80) ? /* am/pm mode? */ + ((buf[2] & 0x80) ? ((w->date.tm_hour < 12) ? 12 : 0) : /* yes, pm set */ + ((w->date.tm_hour >= 12) ? -12 : 0)) : /* yes, pm unset */ + 0; /* no */ + + w->date.tm_mon = LNIB(buf[5]) - 1; + w->date.tm_mday = BCD(buf[3] & 0x7f); + w->date.tm_year = 100 + BCD(buf[4]); + w->date.tm_isdst = -1; /* Daylight savings time not known yet? */ + w->unixtime = mktime(&w->date); +} + + +static void read_duration ( workout_t * w, unsigned char * buf ) +{ + w->duration.tenths = UNIB(buf[0]); + w->duration.seconds = BCD(buf[1]); + w->duration.minutes = BCD(buf[2]); + w->duration.hours = BCD(buf[3]); +} + + +static void read_units ( workout_t * w, unsigned char b ) +{ + if ( b & 0x02 ) { /* english */ + + w->units.system = S710_UNITS_ENGLISH; + w->units.altitude = S710_ALTITUDE_FT; + w->units.speed = S710_SPEED_MPH; + w->units.distance = S710_DISTANCE_MI; + w->units.temperature = S710_TEMPERATURE_F; + + } else { /* metric */ + + w->units.system = S710_UNITS_METRIC; + w->units.altitude = S710_ALTITUDE_M; + w->units.speed = S710_SPEED_KPH; + w->units.distance = S710_DISTANCE_KM; + w->units.temperature = S710_TEMPERATURE_C; + } +} + +static int get_recording_interval ( unsigned char b ) +{ + int ri = 0; + + switch ( b ) { + case 0: ri = 5; break; + case 1: ri = 15; break; + case 2: ri = 60; break; + default: break; + } + + return ri; +} + + +static void read_recording_interval ( workout_t * w, unsigned char * buf ) +{ + w->recording_interval = get_recording_interval(buf[0]); +} + + +static void read_hr_limits ( workout_t * w, unsigned char * buf ) +{ + /* HR limits */ + + w->hr_limit[0].lower = buf[0]; + w->hr_limit[0].upper = buf[1]; + w->hr_limit[1].lower = buf[2]; + w->hr_limit[1].upper = buf[3]; + w->hr_limit[2].lower = buf[4]; + w->hr_limit[2].upper = buf[5]; + + /* time below, within, above hr limits 1 */ + + w->hr_zone[0][0].tenths = 0; + w->hr_zone[0][0].seconds = BCD(buf[9]); /* Below zone 1, seconds */ + w->hr_zone[0][0].minutes = BCD(buf[10]); /* Below zone 1, minutes */ + w->hr_zone[0][0].hours = BCD(buf[11]); /* Below zone 1, hours */ + w->hr_zone[0][1].seconds = BCD(buf[12]); /* Within zone 1, seconds */ + w->hr_zone[0][1].minutes = BCD(buf[13]); /* Within zone 1, minutes */ + w->hr_zone[0][1].hours = BCD(buf[14]); /* Within zone 1, hours */ + w->hr_zone[0][2].seconds = BCD(buf[15]); /* Above zone 1, seconds */ + w->hr_zone[0][2].minutes = BCD(buf[16]); /* Above zone 1, minutes */ + w->hr_zone[0][2].hours = BCD(buf[17]); /* Above zone 1, hours */ + + /* time below, within, above hr limits 2 */ + + w->hr_zone[1][0].tenths = 0; + w->hr_zone[1][0].seconds = BCD(buf[18]); /* Below zone 2, seconds */ + w->hr_zone[1][0].minutes = BCD(buf[19]); /* Below zone 2, minutes */ + w->hr_zone[1][0].hours = BCD(buf[20]); /* Below zone 2, hours */ + w->hr_zone[1][1].seconds = BCD(buf[21]); /* Within zone 2, seconds */ + w->hr_zone[1][1].minutes = BCD(buf[22]); /* Within zone 2, minutes */ + w->hr_zone[1][1].hours = BCD(buf[23]); /* Within zone 2, hours */ + w->hr_zone[1][2].seconds = BCD(buf[24]); /* Above zone 2, seconds */ + w->hr_zone[1][2].minutes = BCD(buf[25]); /* Above zone 2, minutes */ + w->hr_zone[1][2].hours = BCD(buf[26]); /* Above zone 2, hours */ + + /* time below, within, above hr limits 3 */ + + w->hr_zone[2][0].tenths = 0; + w->hr_zone[2][0].seconds = BCD(buf[27]); /* Below zone 3, seconds */ + w->hr_zone[2][0].minutes = BCD(buf[28]); /* Below zone 3, minutes */ + w->hr_zone[2][0].hours = BCD(buf[29]); /* Below zone 3, hours */ + w->hr_zone[2][1].seconds = BCD(buf[30]); /* Within zone 3, seconds */ + w->hr_zone[2][1].minutes = BCD(buf[31]); /* Within zone 3, minutes */ + w->hr_zone[2][1].hours = BCD(buf[32]); /* Within zone 3, hours */ + w->hr_zone[2][2].seconds = BCD(buf[33]); /* Above zone 3, seconds */ + w->hr_zone[2][2].minutes = BCD(buf[34]); /* Above zone 3, minutes */ + w->hr_zone[2][2].hours = BCD(buf[35]); /* Above zone 3, hours */ +} + + +static void read_bestlap_split ( workout_t * w, unsigned char * buf ) +{ + w->bestlap_split.tenths = UNIB(buf[0]); + w->bestlap_split.seconds = BCD(buf[1]); + w->bestlap_split.minutes = BCD(buf[2]); + w->bestlap_split.hours = BCD(buf[3]); +} + + +static void read_energy ( workout_t * w, unsigned char * buf ) +{ + w->energy = BCD(buf[0])/10 + 10 * BCD(buf[1]) + 1000 * BCD(buf[2]); + w->total_energy = BCD(buf[3]) + 100 * BCD(buf[4]) + 10000 * BCD(buf[5]); +} + + +static void read_cumulative_exercise ( workout_t * w, unsigned char * buf ) +{ + w->cumulative_exercise.tenths = 0; + w->cumulative_exercise.seconds = 0; + w->cumulative_exercise.minutes = BCD(buf[2]); + w->cumulative_exercise.hours = BCD(buf[0]) + BCD(buf[1]) * 100; +} + + +static void read_ride_info( workout_t * w, unsigned char * buf ) +{ + w->cumulative_ride.tenths = 0; + w->cumulative_ride.seconds = 0; + w->cumulative_ride.minutes = BCD(buf[2]); + w->cumulative_ride.hours = BCD(buf[0]) + BCD(buf[1]) * 100; + + w->odometer = 10000 * BCD(buf[5]) + 100 * BCD(buf[4]) + BCD(buf[3]); + + /* exercise distance */ + + w->exercise_distance = buf[6] + (buf[7]<<8); + + /* avg and max speed */ + + w->avg_speed = buf[8] | ((buf[9] & 0xfUL) << 8); + w->max_speed = (buf[10] << 4) | (buf[9] >> 4); + + /* avg, max cadence */ + + w->avg_cad = buf[11]; + w->max_cad = buf[12]; + + /* min, avg, max temperature */ + + if ( w->units.system == S710_UNITS_ENGLISH ) { + + /* English units */ + + w->min_temp = buf[19]; + w->avg_temp = buf[20]; + w->max_temp = buf[21]; + + } else { + + /* Metric units */ + + w->min_temp = buf[19] & 0x7f; + w->min_temp = ( buf[19] & 0x80 ) ? w->min_temp : - w->min_temp; + + w->avg_temp = buf[20] & 0x7f; + w->avg_temp = ( buf[20] & 0x80 ) ? w->avg_temp : - w->avg_temp; + + w->max_temp = buf[21] & 0x7f; + w->max_temp = ( buf[21] & 0x80 ) ? w->max_temp : - w->max_temp; + } + + /* altitude, ascent */ + + w->min_alt = buf[13] + ((buf[14] & 0x7f)<<8); + w->min_alt = ( buf[14] & 0x80 ) ? w->min_alt : - w->min_alt; + + w->avg_alt = buf[15] + ((buf[16] & 0x7f)<<8); + w->avg_alt = ( buf[16] & 0x80 ) ? w->avg_alt : - w->avg_alt; + + w->max_alt = buf[17] + ((buf[18] & 0x7f)<<8); + w->max_alt = ( buf[18] & 0x80 ) ? w->max_alt : - w->max_alt; + + w->ascent = (buf[23] << 8) + buf[22]; + + /* avg, max power data */ + + w->avg_power.power = buf[24] + (LNIB(buf[25]) << 8); + w->max_power.power = UNIB(buf[25]) + (buf[26] << 4); + w->avg_power.pedal_index = buf[27]; + w->max_power.pedal_index = buf[28]; + w->avg_power.lr_balance = buf[29]; + /* there is no max_power LR balance */ + w->max_power.lr_balance = 0; +} + + +/* Extract the lap data. */ + +static void read_laps ( workout_t * w, unsigned char * buf ) +{ + S710_Distance prev_lap_dist; + S710_Altitude prev_lap_ascent; + int offset; + int lap_size; + int hdr_size; + lap_data_t * l; + int i; + + prev_lap_ascent = 0; + prev_lap_dist = 0; + lap_size = bytes_per_lap(w->type,w->mode,w->interval_mode); + hdr_size = header_size(w); + w->lap_data = (lap_data_t*)calloc(w->laps,sizeof(lap_data_t)); + + for ( i = 0; i < w->laps; i++ ) { + + /* position to the start of the lap */ + + offset = hdr_size + i * lap_size; + l = &w->lap_data[i]; + + /* timestamp (split) */ + + l->cumulative.hours = buf[offset+2]; + l->cumulative.minutes = buf[offset+1] & 0x3f; + l->cumulative.seconds = buf[offset] & 0x3f; + l->cumulative.tenths = ((buf[offset+1] & 0xc0)>>4) | + ((buf[offset] & 0xc0)>>6); + + if ( i == 0 ) + memcpy(&l->split,&l->cumulative,sizeof(S710_Time)); + else + diff_s710_time(&w->lap_data[i-1].cumulative,&l->cumulative,&l->split); + + /* heart rate data */ + + l->lap_hr = buf[offset+3]; + l->avg_hr = buf[offset+4]; + l->max_hr = buf[offset+5]; + offset += 6; + + /* altitude data */ + + if ( S710_HAS_ALTITUDE(w->mode) ) { + l->alt = buf[offset] + (buf[offset+1]<<8) - 512; + /* This ascent data is cumulative from start of the exercise */ + l->cumul_ascent = buf[offset+2] + (buf[offset+3]<<8); + l->ascent = l->cumul_ascent - prev_lap_ascent; + prev_lap_ascent = l->cumul_ascent; + + if ( w->units.system == S710_UNITS_ENGLISH ) { /* English units */ + l->temp = buf[offset+4] + 14; + l->alt *= 5; + } else { + l->temp = buf[offset+4] - 10; + } + + offset += 5; + } + + /* bike data */ + + if ( S710_HAS_SPEED(w->mode) ) { + + /* cadence data */ + + if ( S710_HAS_CADENCE(w->mode) ) { + l->cad = buf[offset]; + offset += 1; + } + + /* next 4 bytes are power data */ + + if ( S710_HAS_POWER(w->mode) ) { + l->power.power = buf[offset] + (buf[offset+1]<<8); + l->power.lr_balance = buf[offset+3]; /* ??? switched ??? */ + l->power.pedal_index = buf[offset+2]; /* ??? with this ??? */ + offset += 4; + } + + /* + next 4 bytes are distance/speed data + this is cumulative distance from start (at least with S720i) + */ + + l->cumul_distance = buf[offset] + (buf[offset+1]<<8); + l->distance = l->cumul_distance - prev_lap_dist; + prev_lap_dist = l->cumul_distance; + + /* + The offset + 2 appears to be 4b.4b, where the upper 4b is the + integer portion of the speed and the lower 4b is the fractional + portion in sixteenths. I'm not sure where the most significant + bits come from, but it might be the high order nibble of the next + byte- lets try that. + */ + + l->speed = buf[offset+2] + ((buf[offset+3] & 0xf0) << 4); + + /* fprintf(stderr, "distance/speed: %02x %02x %02x %02x\n", + buf[offset], buf[offset+1], buf[offset+2], buf[offset+3]); */ + /* fprintf(stderr, "SPEED: %X\n", l->speed); */ + + } + } +} + + +static int read_samples ( workout_t * w, unsigned char * buf ) +{ + int offset; + int lap_size; + int sample_size; + unsigned long accum; + int ok = 1; + int i; + int s; + int x; + + lap_size = bytes_per_lap(w->type,w->mode,w->interval_mode); + sample_size = bytes_per_sample(w->mode); + offset = header_size(w); + + if ( offset != 0 ) { + + /* now add the offset due to laps */ + + offset += w->laps * lap_size; + + /* number of samples */ + + w->samples = (w->bytes - offset)/sample_size; + + /* allocate memory */ + + ok = allocate_sample_space(w); + + /* if we succeeded in allocating the buffers, ok will not be 0 here. */ + + if ( ok ) { + + /* At last, we can extract the samples. They are in reverse order. */ + + for ( i = 0; i < w->samples; i++ ) { + s = offset + i * sample_size; + x = w->samples - 1 - i; + w->hr_data[x] = buf[s]; + s++; + + if ( S710_HAS_ALTITUDE(w->mode) ) { + w->alt_data[x] = buf[s] + ((buf[s+1] & 0x1f)<<8) - 512; + if ( w->units.system == S710_UNITS_ENGLISH ) { + w->alt_data[x] *= 5; + } + s += 2; + } + + if ( S710_HAS_SPEED(w->mode) ) { + if ( S710_HAS_ALTITUDE(w->mode) ) s -= 1; + w->speed_data[x] = ((buf[s] & 0xe0) << 3) + buf[s+1]; + s += 2; + if ( S710_HAS_POWER(w->mode) ) { + w->power_data[x].power = buf[s] + (buf[s+1]<<8); + w->power_data[x].lr_balance = buf[s+2]; + w->power_data[x].pedal_index = buf[s+3]; + s += 4; + } + if ( S710_HAS_CADENCE(w->mode) ) w->cad_data[x] = buf[s]; + } + } + + if ( S710_HAS_SPEED(w->mode) ) { + accum = 0; + for ( i = 0; i < w->samples; i++ ) { + w->dist_data[i] = accum / 57600.0; + accum += w->speed_data[i] * w->recording_interval; + } + } + + } + } else { + /* we don't know what kind of HRM this is */ + + ok = 0; + } + + return ok; +} + + +static void compute_speed_info ( workout_t * w ) +{ + int i; + int j; + float avg; + + /* compute median speed and highest sampled speed */ + + if ( S710_HAS_SPEED(w->mode) ) { + avg = 0; + w->highest_speed = 0; + j = 0; + for ( i = 0; i < w->samples; i++ ) { + if ( w->speed_data[i] > w->highest_speed ) + w->highest_speed = w->speed_data[i]; + if ( w->speed_data[i] > 0 ) { + avg = (float)(avg * j + w->speed_data[i])/(j+1); + j++; + } + } + w->median_speed = avg; + } +} + + +/* don't call this unless you're sure it's going to be ok. */ + +static workout_t * extract_workout ( unsigned char * buf, + S710_Filter filter, + S710_HRM_Type type ) +{ + workout_t * w = NULL; + int ok = 1; + + if ( (w = (workout_t*)calloc(1,sizeof(workout_t))) == NULL ) { + fprintf(stderr,"extract_workout: calloc(%ld): %s\n", + (long)sizeof(workout_t),strerror(errno)); + return NULL; + } + + /* Define the type of the HRM */ + + w->type = type; + + /* Now extract the header data */ + + read_preamble(w,buf); + read_date(w,buf+10); + read_duration(w,buf+15); + + w->avg_hr = buf[19]; + w->max_hr = buf[20]; + w->laps = BCD(buf[21]); + w->manual_laps = BCD(buf[22]); + w->interval_mode = (S710_Interval_Mode)buf[23]; + w->user_id = BCD(buf[24]); + + read_units(w,buf[25]); + + /* recording mode and interval */ + + if ( w->type == S710_HRM_S610 ) { + w->mode = 0; + read_recording_interval (w,buf+26); + read_hr_limits (w,buf+28); + read_bestlap_split (w,buf+65); + read_energy (w,buf+69); + read_cumulative_exercise (w,buf+75); + } else { + w->mode = buf[26]; + read_recording_interval (w,buf+27); + read_hr_limits (w,buf+29); + read_bestlap_split (w,buf+66); + read_energy (w,buf+70); + read_cumulative_exercise (w,buf+76); + read_ride_info (w,buf+79); + } + + read_laps(w,buf); + ok = read_samples(w,buf); + + /* never let a partially allocated workout get through. */ + + if ( !ok ) { + free_workout(w); + return NULL; + } + + /* if filtering was requested, filter the HR data and recompute + avg/max HR values. */ + + if ( filter == S710_FILTER_ON ) { + filter_workout(w); + } + + compute_speed_info(w); + + return w; +} + + +/* + Attempt to auto-detect the HRM type based on some information in the file. + This may not always succeed, but it seems to work relatively well. +*/ + +static S710_HRM_Type detect_hrm_type ( unsigned char * buf, unsigned int bytes ) +{ + S710_HRM_Type type = S710_HRM_UNKNOWN; + int duration = 0; + int samples; + int laps; + int header = 0; + int bps = 0; + int bpl = 0; + int ri; + + if ( buf[34] == 0 && buf[36] == 251 ) { + + /* this is a s610 HRM */ + + type = S710_HRM_S610; + + } else if ( (buf[35] == 0 || buf[35] == 48) && buf[37] == 251 ) { + + /* this is either an s710 or s625x or...? */ + + if ( (ri = get_recording_interval(buf[27])) != 0 ) { + + /* compute the number of bytes per sample and per lap */ + + bps = bytes_per_sample(buf[26]); + bpl = bytes_per_lap(S710_HRM_UNKNOWN,buf[26],buf[23]); + + /* obtain the number of laps and samples in the file */ + + duration = BCD(buf[16])+60*(BCD(buf[17])+60*BCD(buf[18])); + samples = duration / ri + 1; + laps = BCD(buf[21]); + + /* now compute the size of the file header */ + + header = bytes - samples * bps - laps * bpl; + + /* based on the header size, we can make a guess at + the HRM type. note that this will NOT work if the + file was truncated due to the watch memory filling + up before recording was stopped. */ + + /* assume it's an S710 unless the header size matches + the S625x header size. */ + + if ( header == S710_HEADER_SIZE_S625X ) { + type = S710_HRM_S625X; + } else { + type = S710_HRM_S710; + } + } + } + + return type; +} + + +static workout_t * read_workout ( char *filename, S710_Filter filter, S710_HRM_Type type ) +{ + int fd; + struct stat sb; + workout_t * w = NULL; + unsigned char buf[65536]; + + if ( stat(filename,&sb) != -1 ) { + if ( (fd = open(filename,O_RDONLY)) != -1 ) { + if ( sb.st_size < (int)sizeof(buf) ) { + if ( read(fd,buf,sb.st_size) == sb.st_size ) { + if ( buf[0] + (buf[1]<<8) == sb.st_size ) { + + /* + if type == S710_HRM_AUTO, try to guess the "real" type. + we do this using some heuristics that may or may not be + reliable. + */ + + if ( type == S710_HRM_AUTO ) { + type = detect_hrm_type(buf,sb.st_size); + } + + /* we're good to go */ + + if ( type != S710_HRM_UNKNOWN ) { + w = extract_workout(buf,filter,type); + } else { + fprintf(stderr,"%s: unable to auto-detect HRM type\n", + filename); + } + } else { + fprintf(stderr,"%s: invalid data [%d] [%d] (size %ld)\n", + filename,buf[0],buf[1],(long)sb.st_size); + } + } else { + fprintf(stderr,"%s: read(%ld): %s\n", + filename,(long)sb.st_size,strerror(errno)); + } + } else { + fprintf(stderr,"%s: file size of %ld bytes is too big!\n", + filename,(long)sb.st_size); + } + close(fd); + } else { + fprintf(stderr,"open(%s): %s\n",filename,strerror(errno)); + } + } else { + fprintf(stderr,"stat(%s): %s\n",filename,strerror(errno)); + } + + return w; +} + +static int header_size ( workout_t * w ) +{ + int size = 0; + + switch ( w->type ) { + case S710_HRM_S610: size = S710_HEADER_SIZE_S610; break; + case S710_HRM_S625X: size = S710_HEADER_SIZE_S625X; break; + case S710_HRM_S710: size = S710_HEADER_SIZE_S710; break; + default: break; + } + + return size; +} + + +static int bytes_per_lap ( S710_HRM_Type type, unsigned char bt, unsigned char bi ) +{ + int lap_size = 6; + + /* Compute the number of bytes per lap. */ + + if ( S710_HAS_ALTITUDE(bt) ) lap_size += 5; + if ( S710_HAS_SPEED(bt) ) { + if ( S710_HAS_CADENCE(bt) ) lap_size += 1; + if ( S710_HAS_POWER(bt) ) lap_size += 4; + lap_size += 4; + } + + /* + This is Matti Tahvonen's fix for handling laps with interval mode. + Applies to the S625X and the S710/S720i. + */ + + if ( type != S710_HRM_S610 && bi != 0 ) { + lap_size += 5; + } + + return lap_size; +} + + +static int bytes_per_sample ( unsigned char bt ) +{ + int recsiz = 1; + + if ( S710_HAS_ALTITUDE(bt) ) recsiz += 2; + if ( S710_HAS_SPEED(bt) ) { + if ( S710_HAS_ALTITUDE(bt) ) recsiz -= 1; + recsiz += 2; + if ( S710_HAS_POWER(bt) ) recsiz += 4; + if ( S710_HAS_CADENCE(bt) ) recsiz += 1; + } + + return recsiz; +} + + +static int allocate_sample_space ( workout_t * w ) +{ + int ok = 1; + +#define MAKEBUF(a,b) \ + if ( (w->a = (b *)calloc(w->samples,sizeof(b))) == NULL ) { \ + fprintf(stderr,"%s: calloc(%d,%ld): %s\n", \ + #a,w->samples,(long)sizeof(b),strerror(errno)); \ + ok = 0; \ + } + + MAKEBUF(hr_data,S710_Heart_Rate); + if ( S710_HAS_ALTITUDE(w->mode) ) MAKEBUF(alt_data, S710_Altitude); + if ( S710_HAS_SPEED(w->mode) ) { + MAKEBUF(speed_data, S710_Speed); + MAKEBUF(dist_data, S710_Distance); + if ( S710_HAS_POWER(w->mode) ) MAKEBUF(power_data, S710_Power); + if ( S710_HAS_CADENCE(w->mode) ) MAKEBUF(cad_data, S710_Cadence); + } + +#undef MAKEBUF + + return ok; +} + +static void free_workout ( workout_t *w ) +{ + if ( w != NULL ) { + if ( w->lap_data ) free(w->lap_data); + if ( w->alt_data ) free(w->alt_data); + if ( w->speed_data ) free(w->speed_data); + if ( w->dist_data ) free(w->dist_data); + if ( w->cad_data ) free(w->cad_data); + if ( w->power_data ) free(w->power_data); + free(w); + } +} + +static char alpha_map ( unsigned char c ) +{ + char a = '?'; + + switch ( c ) { + case 0: case 1: case 2: case 3: case 4: + case 5: case 6: case 7: case 8: case 9: + a = '0' + c; + break; + case 10: + a = ' '; + break; + case 11: case 12: case 13: case 14: case 15: + case 16: case 17: case 18: case 19: case 20: + case 21: case 22: case 23: case 24: case 25: + case 26: case 27: case 28: case 29: case 30: + case 31: case 32: case 33: case 34: case 35: + case 36: + a = 'A' + c - 11; + break; + case 37: case 38: case 39: case 40: case 41: + case 42: case 43: case 44: case 45: case 46: + case 47: case 48: case 49: case 50: case 51: + case 52: case 53: case 54: case 55: case 56: + case 57: case 58: case 59: case 60: case 61: + case 62: + a = 'a' + c - 37; + break; + case 63: a = '-'; break; + case 64: a = '%'; break; + case 65: a = '/'; break; + case 66: a = '('; break; + case 67: a = ')'; break; + case 68: a = '*'; break; + case 69: a = '+'; break; + case 70: a = '.'; break; + case 71: a = ':'; break; + case 72: a = '?'; break; + default: break; + } + + return a; +} + +static void extract_label ( unsigned char *buf, S710_Label *label, int bytes ) +{ + int i; + char *p = (char *)label; + + for ( i = 0; i < bytes && i < (int)sizeof(S710_Label)-1; i++ ) { + *(p+i) = alpha_map(buf[i]); + } + + *(p+i) = 0; +} + + +/* + This function filters out bad HR data from a workout and recomputes + the average and max HR from the filtered data. +*/ + +static void filter_workout ( workout_t *w ) +{ + int v; + int lv; + int i; + int j; + float f_interp; + int remax = 0; + float avg; + + /* clean up the sample data */ + + if ( w->hr_data != NULL ) { + v = 1; + lv = 0; + for ( i = 0; i < w->samples; i++ ) { + if ( !v && w->hr_data[i] <= S710_MAX_VALID_HR && w->hr_data[i] != 0 ) { + if ( lv >= 0 ) { + for ( j = lv; j < i; j++ ) { + f_interp = (float)w->hr_data[lv] + + (w->hr_data[i]-w->hr_data[lv])*(j-lv)/(i-lv); + w->hr_data[j] = (int) f_interp; + } + } + v = 1; + remax = 1; + } else if ( v && + (w->hr_data[i] > S710_MAX_VALID_HR || w->hr_data[i] == 0) ) { + v = 0; + lv = i - 1; + } + } + + /* recompute max and avg HR if we have to */ + + if ( remax != 0 ) { + w->max_hr = 0; + avg = 0; + j = 0; + for ( i = 0; i < w->samples; i++ ) { + if ( w->hr_data[i] > w->max_hr ) w->max_hr = w->hr_data[i]; + if ( w->hr_data[i] > 0 ) { + avg = (float)(avg * j + w->hr_data[i])/(j+1); + j++; + } + } + w->avg_hr = (int)avg; + } + } + + if ( w->cad_data != NULL ) { + v = 1; + lv = 0; + for ( i = 0; i < w->samples; i++ ) { + if ( !v && w->cad_data[i] <= S710_MAX_VALID_CAD ) { + if ( lv >= 0 ) { + for ( j = lv; j < i; j++ ) { + f_interp = (float)w->cad_data[lv] + + (w->cad_data[i]-w->cad_data[lv])*(j-lv)/(i-lv); + w->cad_data[j] = (int) f_interp; + } + } + v = 1; + remax = 1; + } else if ( v && + ( w->cad_data[i] > S710_MAX_VALID_CAD ) ) { + v = 0; + lv = i - 1; + } + } + + /* recompute max and avg cadence if we have to */ + + if ( remax != 0 ) { + w->max_cad = 0; + avg = 0; + j = 0; + for ( i = 0; i < w->samples; i++ ) { + if ( w->cad_data[i] > w->max_cad ) w->max_cad = w->cad_data[i]; + if ( w->cad_data[i] > 0 ) { + avg = (float)(avg * j + w->cad_data[i])/(j+1); + j++; + } + } + w->avg_cad = (int)avg; + } + } + + w->filtered = 1; +} diff --git a/src/SrdRideFile.h b/src/SrdRideFile.h new file mode 100644 index 000000000..a62718377 --- /dev/null +++ b/src/SrdRideFile.h @@ -0,0 +1,670 @@ +/* + * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _SrdRideFile_h +#define _SrdRideFile_h + +#include "RideFile.h" + +struct SrdFileReader : public RideFileReader { + virtual RideFile *openRideFile(QFile &file, QStringList &errors) const; +}; + +//=================================================================================== +// +// ******************** +// +// FROM HERE TO BOTTOM OF SOURCE FILE ARE THE SRD WORKOUT ROUTINES +// INCLUDED FROM THE "S710" LINUX PROJECT see: http://code.google.com/p/s710 +// +// To avoid dependency on the s710 source (which has other dependencies) +// the code has been incorporated into this source file as an extract and +// converted to static functions within this source file (the original code +// is part of a library). The last modification date for this source was +// May 9th 2007, it is now largely a dormant project). +// +// ******************** +// +// The original source is published under the GPL v2 License +// +// Original Author: +// Dave Bailey daveb net> +// +// Numerous bugfixes, protocol/file format information and feature enhancements +// have been contributed by (in alphabetical order by last name): +// Bjorn Andersson iki fi> +// Jani Averbach jaa iki fi> +// Markus Braun Braun krawel de> +// Robert Estes transmeta com> +// Michael Karbach physik uni-wuppertal de> +// Stefan Kleditzsch iwr uni-heidelberg de> +// Max Matveev sgi com> +// Berend Ozceri alumni cmu edu> +// Matti Tahvonen tahvonen com> +// Frank Vercruesse vercruesse de> +// Aldy Hernandez redhat com> +// +//=================================================================================== + +#include +#include + +/* constants */ + +#define S710_REQUEST 0xa3 +#define S710_RESPONSE 0x5c + +#define S710_USB_VENDOR_ID 0x0da4 +#define S710_USB_PRODUCT_ID 1 +#define S710_USB_INTERFACE 0 + +#define S710_MODE_HEART_RATE 0 +#define S710_MODE_ALTITUDE 2 +#define S710_MODE_CADENCE 4 +#define S710_MODE_POWER 8 +#define S710_MODE_BIKE1 16 +#define S710_MODE_BIKE2 32 +#define S710_MODE_SPEED (S710_MODE_BIKE1 | S710_MODE_BIKE2) + +#define S710_AM_PM_MODE_UNSET 0 +#define S710_AM_PM_MODE_SET 1 +#define S710_AM_PM_MODE_PM 2 + +#define S710_WORKOUT_HEADER 1 +#define S710_WORKOUT_LAPS 2 +#define S710_WORKOUT_SAMPLES 4 +#define S710_WORKOUT_FULL 7 + +#define S710_TIC_PLAIN 0 +#define S710_TIC_LINES 1 +#define S710_TIC_SHADE 2 +#define S710_TIC_SHADE_RED (S710_TIC_SHADE | 4) +#define S710_TIC_SHADE_GREEN (S710_TIC_SHADE | 8) +#define S710_TIC_SHADE_BLUE (S710_TIC_SHADE | 16) + +#define S710_Y_AXIS_LEFT -1 +#define S710_Y_AXIS_RIGHT 1 + +#define S710_X_MARGIN 15 +#define S710_Y_MARGIN 15 + +#define S710_BLANK_SAMPLE_LIMIT 2880 /* 4 hours at 5 second intervals. */ + +#define S710_HEADER_SIZE_S625X 130 +#define S710_HEADER_SIZE_S710 109 +#define S710_HEADER_SIZE_S610 78 + +#define S710_MAX_VALID_HR 206 +#define S710_MAX_VALID_CAD 170 +#define S710_HRM_REST_HR 45 +#define S710_HRM_MAX_HR 200 + +typedef enum { + S710_DRIVER_SERIAL, + S710_DRIVER_IR, + S710_DRIVER_USB +} S710_Driver_Type; + +typedef enum { + S710_X_AXIS_DISTANCE, + S710_X_AXIS_TIME +} S710_X_Axis; + +typedef enum { + S710_Y_AXIS_HEART_RATE, + S710_Y_AXIS_ALTITUDE, + S710_Y_AXIS_SPEED, + S710_Y_AXIS_CADENCE, + S710_Y_AXIS_POWER +} S710_Y_Axis; + +typedef enum { + S710_FILTER_OFF, + S710_FILTER_ON +} S710_Filter; + +typedef enum { + S710_MODE_RDWR, + S710_MODE_RDONLY +} S710_Mode; + +typedef enum { + S710_GET_OVERVIEW, + S710_GET_USER, + S710_GET_WATCH, + S710_GET_LOGO, + S710_GET_BIKE, + S710_GET_EXERCISE_1, + S710_GET_EXERCISE_2, + S710_GET_EXERCISE_3, + S710_GET_EXERCISE_4, + S710_GET_EXERCISE_5, + S710_GET_REMINDER_1, + S710_GET_REMINDER_2, + S710_GET_REMINDER_3, + S710_GET_REMINDER_4, + S710_GET_REMINDER_5, + S710_GET_REMINDER_6, + S710_GET_REMINDER_7, + S710_GET_FILES, + S710_CONTINUE_TRANSFER, + S710_CLOSE_CONNECTION, + S710_SET_USER, + S710_SET_WATCH, + S710_SET_LOGO, + S710_SET_BIKE, + S710_SET_EXERCISE_1, + S710_SET_EXERCISE_2, + S710_SET_EXERCISE_3, + S710_SET_EXERCISE_4, + S710_SET_EXERCISE_5, + S710_SET_REMINDER_1, + S710_SET_REMINDER_2, + S710_SET_REMINDER_3, + S710_SET_REMINDER_4, + S710_SET_REMINDER_5, + S710_SET_REMINDER_6, + S710_SET_REMINDER_7, + S710_HARD_RESET +} S710_Packet_Index; + +typedef enum { + S710_OFF, + S710_ON +} S710_On_Off; + +typedef enum { + S710_HOURS_24, + S710_HOURS_12 +} S710_Hours; + +typedef enum { + S710_BIKE_NONE, + S710_BIKE_1, + S710_BIKE_2 +} S710_Bike; + +typedef enum { + S710_EXE_NONE, + S710_EXE_BASIC_USE, + S710_EXE_SET_1, + S710_EXE_SET_2, + S710_EXE_SET_3, + S710_EXE_SET_4, + S710_EXE_SET_5 +} S710_Exercise; + +typedef enum { + S710_REPEAT_OFF, + S710_REPEAT_HOURLY, + S710_REPEAT_DAILY, + S710_REPEAT_WEEKLY, + S710_REPEAT_MONTHLY, + S710_REPEAT_YEARLY +} S710_Repeat; + +typedef enum { + S710_UNITS_METRIC, + S710_UNITS_ENGLISH +} S710_Units; + +typedef enum { + S710_HT_SHOW_LIMITS, + S710_HT_STORE_LAP, + S710_HT_SWITCH_DISP +} S710_Heart_Touch; + +typedef enum { + S710_RECORD_INT_05, + S710_RECORD_INT_15, + S710_RECORD_INT_60 +} S710_Recording_Interval; + +typedef enum { + S710_INTERVAL_MODE_OFF = 0, + S710_INTERVAL_MODE_ON = 1 +} S710_Interval_Mode; + +typedef enum { + S710_ACTIVITY_LOW, + S710_ACTIVITY_MEDIUM, + S710_ACTIVITY_HIGH, + S710_ACTIVITY_TOP +} S710_Activity_Level; + +typedef enum { + S710_GENDER_MALE, + S710_GENDER_FEMALE +} S710_Gender; + +typedef enum { + S710_ATTRIBUTE_TYPE_INTEGER, + S710_ATTRIBUTE_TYPE_STRING, + S710_ATTRIBUTE_TYPE_BYTE, + S710_ATTRIBUTE_TYPE_BOOLEAN, + S710_ATTRIBUTE_TYPE_ENUM_INTEGER, + S710_ATTRIBUTE_TYPE_ENUM_STRING +} S710_Attribute_Type; + +typedef enum { + S710_MAP_TYPE_USER, + S710_MAP_TYPE_WATCH, + S710_MAP_TYPE_LOGO, + S710_MAP_TYPE_BIKE, + S710_MAP_TYPE_EXERCISE, + S710_MAP_TYPE_REMINDER, + S710_MAP_TYPE_COUNT +} S710_Map_Type; + +typedef enum { + S710_HRM_AUTO = 0, + S710_HRM_S610 = 11, /* same as in hrm files */ + S710_HRM_S710 = 12, /* same as in hrm files */ + S710_HRM_S810 = 13, /* same as in hrm files */ + S710_HRM_S625X = 22, /* same as in hrm files */ + S710_HRM_UNKNOWN = 255 +} S710_HRM_Type; + +typedef enum { + S710_MERGE_TRUE, + S710_MERGE_CONCAT +} S710_Merge_Type; + +/* helpful macros */ + +#define UNIB(x) ((x)>>4) +#define LNIB(x) ((x)&0x0f) +#define BINU(x) ((x)<<4) +#define BINL(x) ((x)&0x0f) +#define BCD(x) (UNIB(x)*10 + LNIB(x)) +#define HEX(x) ((((x)/10)<<4) + ((x)%10)) +#define DIGITS01(x) ((x)%100) +#define DIGITS23(x) (((x)/100)%100) +#define DIGITS45(x) (((x)/10000)%100) + +#define CLAMP(a,b,c) if((a)<(b))(a)=(b);if((a)>(c))(a)=(c) + + +/* structure and type definitions */ + +typedef struct S710_Driver { + S710_Driver_Type type; + S710_Mode mode; + char path[PATH_MAX]; + void *data; +} S710_Driver; + + +typedef char S710_Label[8]; +typedef struct tm S710_Date; + +typedef struct S710_Time { + int hours; + int minutes; + int seconds; + int tenths; +} S710_Time; + +/* basic types */ + +typedef unsigned char S710_Heart_Rate; +typedef unsigned char S710_Cadence; +typedef short S710_Altitude; +typedef unsigned short S710_Speed; /* 1/16ths of a km/hr */ +typedef float S710_Distance; + +typedef struct S710_Power { + unsigned short power; + unsigned char lr_balance; + unsigned char pedal_index; +} S710_Power; + +typedef char S710_Temperature; + +typedef struct S710_HR_Limit { + S710_Heart_Rate lower; + S710_Heart_Rate upper; +} S710_HR_Limit; + +/* basic communication packet */ + +typedef char *PacketName; +typedef unsigned char PacketType; +typedef unsigned char PacketID; +typedef unsigned short PacketLength; +typedef unsigned short PacketChecksum; +typedef unsigned char PacketData[1]; + +typedef struct packet_t { + PacketName name; + PacketType type; + PacketID id; + PacketLength length; + PacketChecksum checksum; + PacketData data; +} packet_t; + +/* overview data */ + +typedef struct overview_t { + int files; + int bytes; +} overview_t; + +/* user data */ + +typedef struct user_t { + S710_On_Off altimeter; + S710_On_Off fitness_test; + S710_On_Off predict_hr_max; + S710_On_Off energy_expenditure; + S710_On_Off options_lock; + S710_On_Off help; + S710_On_Off activity_button_sound; + S710_Units units; + S710_Heart_Touch heart_touch; + S710_Recording_Interval recording_interval; + S710_Activity_Level activity_level; + S710_Gender gender; + unsigned int weight; + unsigned int height; + unsigned int vo2max; + S710_Heart_Rate max_hr; + unsigned int user_id; + S710_Label name; + S710_Date birth_date; + unsigned char unknown[2]; +} user_t; + +/* watch data */ + +typedef struct watch_t { + S710_Date time1; + S710_Time time2; + S710_Time alarm; + S710_On_Off alarm_on; + S710_Hours time1_hours; + S710_Hours time2_hours; + int time_zone; +} watch_t; + +/* logo data */ + +typedef struct logo_t { + unsigned char column[47]; +} logo_t; + +/* bike data */ + +typedef struct bike_data_t { + unsigned int wheel_size; + S710_Label label; + S710_On_Off power_sensor; + S710_On_Off cadence_sensor; + unsigned int chain_weight; + unsigned int chain_length; + unsigned int span_length; +} bike_data_t; + +typedef struct bike_t { + S710_Bike in_use; + bike_data_t bike[2]; +} bike_t; + +/* exercise data */ + +typedef struct exercise_t { + unsigned int which; + S710_Label label; + S710_Time timer[3]; + S710_HR_Limit hr_limit[3]; + S710_Time recovery_time; + S710_Heart_Rate recovery_hr; +} exercise_t; + +/* reminder data */ + +typedef struct reminder_t { + unsigned int which; + S710_Label label; + S710_Date date; + S710_On_Off on; + S710_Exercise exercise; + S710_Repeat repeat; +} reminder_t; + +/* workout data */ + +typedef struct files_t { + unsigned short bytes; + unsigned short cursor; + unsigned char data[32768]; +} files_t; + + +/* lap data */ + +typedef struct lap_data_t { + S710_Time split; + S710_Time cumulative; + S710_Heart_Rate lap_hr; + S710_Heart_Rate avg_hr; + S710_Heart_Rate max_hr; + S710_Altitude alt; + S710_Altitude ascent; + S710_Altitude cumul_ascent; + S710_Temperature temp; + S710_Cadence cad; + int distance; + int cumul_distance; + S710_Speed speed; + S710_Power power; +} lap_data_t; + + +/* units info */ + +typedef struct units_data_t { + S710_Units system; /* S710_UNITS_METRIC or S710_UNITS_ENGLISH */ + char *altitude; /* "m" or "ft" */ + char *speed; /* "kph" or "mph" */ + char *distance; /* "km" or "mi" */ + char *temperature; /* "C" or "F" */ +} units_data_t; + + +/* a single workout */ + +typedef struct workout_t { + S710_HRM_Type type; + S710_Date date; + int ampm; + time_t unixtime; + int user_id; + S710_Interval_Mode interval_mode; + int exercise_number; + S710_Label exercise_label; + S710_Time duration; + S710_Heart_Rate avg_hr; + S710_Heart_Rate max_hr; + int bytes; + int laps; + int manual_laps; + int samples; + units_data_t units; + int mode; + int recording_interval; + int filtered; + S710_HR_Limit hr_limit[3]; + S710_Time hr_zone[3][3]; + S710_Time bestlap_split; + S710_Time cumulative_exercise; + S710_Time cumulative_ride; + int odometer; + int exercise_distance; + S710_Cadence avg_cad; + S710_Cadence max_cad; + S710_Altitude min_alt; + S710_Altitude avg_alt; + S710_Altitude max_alt; + S710_Temperature min_temp; + S710_Temperature avg_temp; + S710_Temperature max_temp; + S710_Altitude ascent; + S710_Power avg_power; + S710_Power max_power; + S710_Speed avg_speed; + S710_Speed max_speed; + S710_Speed median_speed; + S710_Speed highest_speed; + int energy; + int total_energy; + lap_data_t *lap_data; + S710_Heart_Rate *hr_data; + S710_Altitude *alt_data; + S710_Speed *speed_data; + S710_Distance *dist_data; /* computed from speed_data */ + S710_Cadence *cad_data; + S710_Power *power_data; +} workout_t; + + +/* A few macros that are useful */ + +#define S710_HAS_FIELD(x,y) (((x) & S710_MODE_##y) != 0) + +#define S710_HAS_CADENCE(x) S710_HAS_FIELD(x,CADENCE) +#define S710_HAS_POWER(x) S710_HAS_FIELD(x,POWER) +#define S710_HAS_SPEED(x) S710_HAS_FIELD(x,SPEED) +#define S710_HAS_ALTITUDE(x) S710_HAS_FIELD(x,ALTITUDE) +#define S710_HAS_BIKE1(x) S710_HAS_FIELD(x,BIKE1) + + +/* structs used in rendering images using GD */ + +typedef struct byte_zone_t { + int min; + int max; + struct { + int red; + int green; + int blue; + int pixel; + } color; + int seconds; /* spent in this zone (HR or cadence) */ +} byte_zone_t; + +typedef struct byte_histogram_t { + int zones; + byte_zone_t *zone; + int hist[0x10000]; /* histogram of seconds at each byte value */ +} byte_histogram_t; + +/* structs and unions used when setting watch properties */ + +typedef union attribute_value_t { + int int_value; + S710_Label string_value; + unsigned char byte_value; + S710_On_Off bool_value; + struct { + int eval; + int ival; + } enum_int_value; + struct { + char *sval; + int ival; + } enum_string_value; +} attribute_value_t; + +/* + if the type is S710_ATTRIBUTE_TYPE_INTEGER, then values is a three-element + array with the first element being the lower bound and the second element + the upper bound for the value, and the third element the offset (amount to + add to the value before storing it). + + if the type is S710_ATTRIBUTE_TYPE_STRING, then values is a single-element + array with the only element being the (integer) maximum string length. + + if the type is S710_ATTRIBUTE_TYPE_BOOLEAN or S710_ATTRIBUTE_TYPE_BYTE, + then values is NULL. + + if the type is S710_ATTRIBUTE_TYPE_ENUM_INTEGER, then values is a + negative-terminated list of allowed (non-negative) integer values. + + if the type is S710_ATTRIBUTE_TYPE_ENUM_STRING, then values is a + NULL-terminated list of pairs of allowed string values and the integer + values they correspond to. +*/ + +typedef struct attribute_pair_t { + char *name; + S710_Attribute_Type type; + void *ptr; + attribute_value_t *values; + int vcount; + attribute_value_t value; + int oosync; + struct attribute_pair_t *next; +} attribute_pair_t; + +typedef struct attribute_map_t { + union { + user_t user; + watch_t watch; + bike_t bike; + logo_t logo; + exercise_t exercise; + reminder_t reminder; + } data; + S710_Map_Type type; + attribute_pair_t *pairs; + int oosync; +} attribute_map_t; + + +// s701 library function prototypes +static void diff_s710_time ( S710_Time *t1, S710_Time *t2, S710_Time *diff ); +static workout_t * read_workout ( char *filename, S710_Filter filter, S710_HRM_Type type ); +static void read_preamble ( workout_t * w, unsigned char * buf ); +static void read_date ( workout_t * w, unsigned char * buf ); +static void read_duration ( workout_t * w, unsigned char * buf ); +static void read_units ( workout_t * w, unsigned char b ); +static int get_recording_interval ( unsigned char b ); +static void read_recording_interval ( workout_t * w, unsigned char * buf ); +static void read_hr_limits ( workout_t * w, unsigned char * buf ); +static void read_bestlap_split ( workout_t * w, unsigned char * buf ); +static void read_energy ( workout_t * w, unsigned char * buf ); +static void read_cumulative_exercise ( workout_t * w, unsigned char * buf ); +static void read_ride_info( workout_t * w, unsigned char * buf ); +static void read_laps ( workout_t * w, unsigned char * buf ); +static int read_samples ( workout_t * w, unsigned char * buf ); +static void compute_speed_info ( workout_t * w ); +static workout_t * extract_workout ( unsigned char * buf, S710_Filter filter, S710_HRM_Type type ); +static S710_HRM_Type detect_hrm_type ( unsigned char * buf, unsigned int bytes ); +static int header_size ( workout_t * w ); +static int bytes_per_lap ( S710_HRM_Type type, unsigned char bt, unsigned char bi ); +static int bytes_per_sample ( unsigned char bt ); +static int allocate_sample_space ( workout_t * w ); +static void free_workout ( workout_t *w ); +static char alpha_map ( unsigned char c ); +static void extract_label ( unsigned char *buf, S710_Label *label, int bytes ); +static void filter_workout ( workout_t *w ); + + +#endif diff --git a/src/src.pro b/src/src.pro index 9b6b107f8..80d3dc408 100644 --- a/src/src.pro +++ b/src/src.pro @@ -135,6 +135,7 @@ HEADERS += \ SimpleNetworkClient.h \ SpecialFields.h \ SplitRideDialog.h \ + SrdRideFile.h \ SrmRideFile.h \ StressCalculator.h \ SummaryMetrics.h \ @@ -232,6 +233,7 @@ SOURCES += \ SimpleNetworkClient.cpp \ SpecialFields.cpp \ SplitRideDialog.cpp \ + SrdRideFile.cpp \ SrmRideFile.cpp \ StressCalculator.cpp \ TcxParser.cpp \ diff --git a/src/test/rides/20090506T164017.05256.srd b/src/test/rides/20090506T164017.05256.srd new file mode 100644 index 0000000000000000000000000000000000000000..843caa2446263759a4ed882d4051483503116dc2 GIT binary patch literal 5256 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 literal 0 HcmV?d00001 diff --git a/src/test/rides/descriptions.txt b/src/test/rides/descriptions.txt index e5b31adf0..aef9b5db6 100644 --- a/src/test/rides/descriptions.txt +++ b/src/test/rides/descriptions.txt @@ -26,3 +26,13 @@ (all zeros in this file), power, cadence, speed (all zeros), heart rate, altitude, temperature, and interval number. Note the interval numbering convention. + +20090506T164017.05256.srd + A polar srd file which includes altitude, speed and heartrate. It + does not contain interval or power data, although there is a single + interval at the end of the workout which is 0 seconds long. + +20090512T163932.03592.srd + A polar srd file which includes altitude, speed and heartrate. It + does not contain interval or power data, although there is a single + interval at the end of the workout which is 0 seconds long.