From 1e8b6edb62f1b21de81ebb26ce056a45a006f110 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Fri, 4 Nov 2011 16:28:36 +0000 Subject: [PATCH] Show kJoules, TSS/BikeScore et al on Train View The refactoring of the realtime display last year removed the display of metrics such as BikeScore and kJoules. This patch adds more metrics that can be displayed; * Averages for; power, hr, cadence, speed * KJoules of work * Coggan Metrics; NP, TSS, IF, VI * Skiba Metrics; xPower, BikeScore, RI, Skiba VI Also included is an updated default layout to include some of these metrics. Fixes #231 --- src/DialWindow.cpp | 217 ++++++++++++++++++++++++++++++++++++++- src/DialWindow.h | 28 ++++- src/RealtimeData.cpp | 78 +++++++------- src/RealtimeData.h | 9 +- src/TrainTool.cpp | 33 ------ src/TrainTool.h | 9 +- src/xml/train-layout.xml | 146 ++++++++++++++++++-------- 7 files changed, 391 insertions(+), 129 deletions(-) diff --git a/src/DialWindow.cpp b/src/DialWindow.cpp index e14ae5613..274e45dd4 100644 --- a/src/DialWindow.cpp +++ b/src/DialWindow.cpp @@ -22,6 +22,8 @@ DialWindow::DialWindow(MainWindow *mainWindow) : GcWindow(mainWindow), mainWindow(mainWindow) { + rolling.resize(150); // enough for 30 seconds at 5hz + setContentsMargins(0,0,0,0); setInstanceName("Dial"); @@ -125,6 +127,207 @@ DialWindow::telemetryUpdate(const RealtimeData &rtData) valueLabel->setText(QString("%1").arg(value, 0, 'f', 3)); break; + case RealtimeData::AvgWatts: + sum += rtData.value(RealtimeData::Watts); + count++; + value = sum / count; + valueLabel->setText(QString("%1").arg(round(value))); + break; + + case RealtimeData::AvgSpeed: + sum += rtData.value(RealtimeData::Speed); + count++; + value = sum / count; + if (!mainWindow->useMetricUnits) value *= MILES_PER_KM; + valueLabel->setText(QString("%1").arg(value, 0, 'f', 1)); + break; + + case RealtimeData::AvgCadence: + sum += rtData.value(RealtimeData::Cadence); + count++; + value = sum / count; + valueLabel->setText(QString("%1").arg(round(value))); + break; + + case RealtimeData::AvgHeartRate: + sum += rtData.value(RealtimeData::HeartRate); + count++; + value = sum / count; + valueLabel->setText(QString("%1").arg(round(value))); + break; + + // ENERGY + case RealtimeData::Joules: + sum += rtData.value(RealtimeData::Watts) / 5; // joules + valueLabel->setText(QString("%1").arg(round(sum/1000))); // kJoules + break; + + // COGGAN Metrics + case RealtimeData::NP: + case RealtimeData::IF: + case RealtimeData::TSS: + case RealtimeData::VI: + { + + // Update sum of watts for last 30 seconds + sum += rtData.value(RealtimeData::Watts); + sum -= rolling[index]; + rolling[index] = rtData.value(RealtimeData::Watts); + + // raise average to the 4th power + rollingSum += pow(sum/150,4); // raise rolling average to 4th power + count ++; + + // move index on/round + index = (index >= 149) ? 0 : index+1; + + // calculate NP + double np = pow(rollingSum / (count), 0.25); + + if (series == RealtimeData::NP) { + // We only wanted NP so thats it + valueLabel->setText(QString("%1").arg(round(np))); + + } else { + + double rif, cp; + // carry on and calculate IF + if (mainWindow->zones()) { + + // get cp for today + int zonerange = mainWindow->zones()->whichRange(QDateTime::currentDateTime().date()); + if (zonerange >= 0) cp = mainWindow->zones()->getCP(zonerange); + else cp = 0; + + } else { + cp = 0; + } + + if (cp) rif = np / cp; + else rif = 0; + + if (series == RealtimeData::IF) { + + // we wanted IF so thats it + valueLabel->setText(QString("%1").arg(rif, 0, 'f', 3)); + + } else { + + double normWork = np * (rtData.value(RealtimeData::Time) / 1000); // msecs + double rawTSS = normWork * rif; + double workInAnHourAtCP = cp * 3600; + double tss = rawTSS / workInAnHourAtCP * 100.0; + + if (series == RealtimeData::TSS) { + + valueLabel->setText(QString("%1").arg(tss, 0, 'f', 1)); + + } else { + + // track average power for VI + apsum += rtData.value(RealtimeData::Watts); + apcount++; + + double ap = apsum ? apsum / apcount : 0; + + // VI is all that is left! + valueLabel->setText(QString("%1").arg(ap ? np / ap : 0, 0, 'f', 3)); + + } + + } + + } + + } + break; + + // SKIBA Metrics + case RealtimeData::XPower: + case RealtimeData::RI: + case RealtimeData::BikeScore: + case RealtimeData::SkibaVI: + { + + static const double exp = 2.0f / ((25.0f / 0.2f) + 1.0f); + static const double rem = 1.0f - exp; + + count++; + + if (count < 125) { + + // get up to speed + rsum += rtData.value(RealtimeData::Watts); + ewma = rsum / count; + + } else { + + // we're up to speed + ewma = (rtData.value(RealtimeData::Watts) * exp) + (ewma * rem); + } + + sum += pow(ewma, 4.0f); + double xpower = pow(sum / count, 0.25f); + + if (series == RealtimeData::XPower) { + + // We wanted XPower! + valueLabel->setText(QString("%1").arg(round(xpower))); + + } else { + + double rif, cp; + // carry on and calculate IF + if (mainWindow->zones()) { + + // get cp for today + int zonerange = mainWindow->zones()->whichRange(QDateTime::currentDateTime().date()); + if (zonerange >= 0) cp = mainWindow->zones()->getCP(zonerange); + else cp = 0; + + } else { + cp = 0; + } + + if (cp) rif = xpower / cp; + else rif = 0; + + if (series == RealtimeData::RI) { + + // we wanted IF so thats it + valueLabel->setText(QString("%1").arg(rif, 0, 'f', 3)); + + } else { + + double normWork = xpower * (rtData.value(RealtimeData::Time) / 1000); // msecs + double rawTSS = normWork * rif; + double workInAnHourAtCP = cp * 3600; + double tss = rawTSS / workInAnHourAtCP * 100.0; + + if (series == RealtimeData::BikeScore) { + + valueLabel->setText(QString("%1").arg(tss, 0, 'f', 1)); + + } else { + + // track average power for Relative Intensity + apsum += rtData.value(RealtimeData::Watts); + apcount++; + + double ap = apsum ? apsum / apcount : 0; + + // RI is all that is left! + valueLabel->setText(QString("%1").arg(ap ? xpower / ap : 0, 0, 'f', 3)); + + } + + } + + } + + } + break; + default: valueLabel->setText(QString("%1").arg(round(value))); break; @@ -135,7 +338,7 @@ DialWindow::telemetryUpdate(const RealtimeData &rtData) void DialWindow::resizeEvent(QResizeEvent * ) { // set point size - int size = geometry().height()-15; + int size = geometry().height()-24; if (size <= 0) size = 4; if (size >= 64) size = 64; @@ -158,30 +361,40 @@ void DialWindow::seriesChanged() case RealtimeData::LapTime: case RealtimeData::Distance: case RealtimeData::Lap: + case RealtimeData::RI: + case RealtimeData::IF: + case RealtimeData::VI: + case RealtimeData::SkibaVI: case RealtimeData::None: foreground = GColor(CPLOTMARKER); break; case RealtimeData::Load: + case RealtimeData::BikeScore: + case RealtimeData::TSS: foreground = Qt::blue; break; case RealtimeData::XPower: + case RealtimeData::NP: case RealtimeData::Joules: - case RealtimeData::BikeScore: case RealtimeData::Watts: + case RealtimeData::AvgWatts: foreground = GColor(CPOWER); break; case RealtimeData::Speed: + case RealtimeData::AvgSpeed: foreground = GColor(CSPEED); break; case RealtimeData::Cadence: + case RealtimeData::AvgCadence: foreground = GColor(CCADENCE); break; case RealtimeData::HeartRate: + case RealtimeData::AvgHeartRate: foreground = GColor(CHEARTRATE); break; } diff --git a/src/DialWindow.h b/src/DialWindow.h index 47bb66d3c..82d1731e8 100644 --- a/src/DialWindow.h +++ b/src/DialWindow.h @@ -25,6 +25,7 @@ #include "MainWindow.h" +#include "Zones.h" // for data series types #include "RideFile.h" // for data series types #include "RealtimeData.h" // for realtimedata structure @@ -99,8 +100,33 @@ class DialWindow : public GcWindow double instantValue; double avg30, avgLap, avgTotal; double lapNumber; + + // for calculating averages int count; - void resetValues() { instantValue = avg30 = avgLap = avgTotal = lapNumber = 0; telemetryUpdate(RealtimeData()); } + double sum; + + // for keeping track of rolling averages (max 30s at 5hz) + // used by NP and XPower + QVector rolling; + double rollingSum; + int index; // index into rolling (circular buffer) + + // VI/RI makes us track AP too + int apcount; + int apsum; + + // used by XPower algorithm + double rsum, ewma; + + void resetValues() { + + rolling.fill(0.00); + rsum = ewma = 0.0f; + rollingSum = index = 0; + apcount = count = sum = instantValue = avg30 = + apsum = avgLap = avgTotal = lapNumber = 0; + telemetryUpdate(RealtimeData()); + } // controls QComboBox *seriesSelector; diff --git a/src/RealtimeData.cpp b/src/RealtimeData.cpp index 297f78fc2..89666c5b3 100644 --- a/src/RealtimeData.cpp +++ b/src/RealtimeData.cpp @@ -25,7 +25,7 @@ RealtimeData::RealtimeData() { name[0] = '\0'; lap = watts = hr = speed = wheelRpm = cadence = load = 0; - msecs = lapMsecs = bikeScore = joules = 0; + msecs = lapMsecs = /* bikeScore = joules =*/ 0; } void RealtimeData::setName(char *name) @@ -68,19 +68,6 @@ void RealtimeData::setDistance(double x) { this->distance = x; } -void RealtimeData::setJoules(long x) -{ - this->joules = x; -} -void RealtimeData::setBikeScore(long x) -{ - this->bikeScore = x; -} -void RealtimeData::setXPower(long x) -{ - this->xPower = x; -} - const char * RealtimeData::getName() const { @@ -122,18 +109,6 @@ double RealtimeData::getDistance() const { return distance; } -long RealtimeData::getJoules() const -{ - return joules; -} -long RealtimeData::getBikeScore() const -{ - return bikeScore; -} -long RealtimeData::getXPower() const -{ - return xPower; -} double RealtimeData::value(DataSeries series) const { @@ -151,15 +126,6 @@ double RealtimeData::value(DataSeries series) const case Distance: return distance; break; - case Joules: return joules; - break; - - case BikeScore: return bikeScore; - break; - - case XPower: return xPower; - break; - case Watts: return watts; break; @@ -202,8 +168,18 @@ const QList &RealtimeData::listDataSeries() seriesList << HeartRate; seriesList << Load; seriesList << BikeScore; + seriesList << SkibaVI; + seriesList << TSS; seriesList << XPower; + seriesList << NP; + seriesList << RI; + seriesList << IF; + seriesList << VI; seriesList << Joules; + seriesList << AvgWatts; + seriesList << AvgSpeed; + seriesList << AvgCadence; + seriesList << AvgHeartRate; } return seriesList; } @@ -225,15 +201,33 @@ QString RealtimeData::seriesName(DataSeries series) case LapTime: return tr("Lap Time"); break; + case TSS: return tr("TSS"); + break; + case BikeScore: return tr("BikeScore"); break; - case Joules: return tr("Joules"); + case Joules: return tr("kJoules"); break; case XPower: return tr("XPower"); break; + case NP: return tr("Normalized Power"); + break; + + case IF: return tr("Intensity Factor"); + break; + + case RI: return tr("Relative Intensity"); + break; + + case SkibaVI: return tr("Skiba Variability Index"); + break; + + case VI: return tr("Variability Index"); + break; + case Distance: return tr("Distance"); break; @@ -251,6 +245,18 @@ QString RealtimeData::seriesName(DataSeries series) case Load: return tr("Target Power"); break; + + case AvgWatts: return tr("Average Power"); + break; + + case AvgSpeed: return tr("Average Speed"); + break; + + case AvgHeartRate: return tr("Average Heartrate"); + break; + + case AvgCadence: return tr("Average Cadence"); + break; } } diff --git a/src/RealtimeData.h b/src/RealtimeData.h index 1cea34f47..5b239db5b 100644 --- a/src/RealtimeData.h +++ b/src/RealtimeData.h @@ -32,7 +32,9 @@ public: // abstract to dataseries enum dataseries { None=0, Time, LapTime, Distance, Lap, Watts, Speed, Cadence, HeartRate, Load, - XPower, BikeScore, Joules }; + XPower, BikeScore, RI, Joules, SkibaVI, + NP, TSS, IF, VI, + AvgWatts, AvgSpeed, AvgCadence, AvgHeartRate }; typedef enum dataseries DataSeries; double value(DataSeries) const; @@ -69,9 +71,6 @@ public: long getMsecs() const; long getLapMsecs() const; double getDistance() const; - long getBikeScore() const; - long getJoules() const; - long getXPower() const; long getLap() const; @@ -87,8 +86,6 @@ private: long lap; long msecs; long lapMsecs; - long bikeScore, joules, xPower; }; - #endif diff --git a/src/TrainTool.cpp b/src/TrainTool.cpp index 871c557e9..60bc238d8 100644 --- a/src/TrainTool.cpp +++ b/src/TrainTool.cpp @@ -201,7 +201,6 @@ TrainTool::TrainTool(MainWindow *parent, const QDir &home) : GcWindow(parent), h disk_timer = new QTimer(this); stream_timer = new QTimer(this); load_timer = new QTimer(this); - metrics_timer = new QTimer(this); session_time = QTime(); session_elapsed_msec = 0; @@ -222,13 +221,10 @@ TrainTool::TrainTool(MainWindow *parent, const QDir &home) : GcWindow(parent), h displaySpeed = displayCadence = displayGradient = displayLoad = 0; manualOverride = false; - rideFile = boost::shared_ptr(new RideFile(QDateTime::currentDateTime(),1)); - connect(gui_timer, SIGNAL(timeout()), this, SLOT(guiUpdate())); connect(disk_timer, SIGNAL(timeout()), this, SLOT(diskUpdate())); connect(stream_timer, SIGNAL(timeout()), this, SLOT(streamUpdate())); connect(load_timer, SIGNAL(timeout()), this, SLOT(loadUpdate())); - connect(metrics_timer, SIGNAL(timeout()), this, SLOT(metricsUpdate())); configChanged(); // will reset the workout tree @@ -603,9 +599,6 @@ void TrainTool::Start() // when start button is pressed } } - // create a new rideFile - rideFile = boost::shared_ptr(new RideFile(QDateTime::currentDateTime(),1)); - // stream if (status & RT_STREAMING) { @@ -613,7 +606,6 @@ void TrainTool::Start() // when start button is pressed } gui_timer->start(REFRESHRATE); // start recording - metrics_timer->start(METRICSRATE); } } @@ -633,7 +625,6 @@ void TrainTool::Pause() // pause capture to recalibrate deviceController->restart(); setPauseText(tr("Pause")); gui_timer->start(REFRESHRATE); - metrics_timer->start(METRICSRATE); if (status & RT_STREAMING) stream_timer->start(STREAMRATE); if (status & RT_RECORDING) disk_timer->start(SAMPLERATE); load_period.restart(); @@ -650,7 +641,6 @@ void TrainTool::Pause() // pause capture to recalibrate setPauseText(tr("Un-Pause")); status |=RT_PAUSED; gui_timer->stop(); - metrics_timer->stop(); if (status & RT_STREAMING) stream_timer->stop(); if (status & RT_RECORDING) disk_timer->stop(); if (status & RT_WORKOUT) load_timer->stop(); @@ -676,7 +666,6 @@ void TrainTool::Stop(int deviceStatus) // when stop button is pressed deviceController = NULL; gui_timer->stop(); - metrics_timer->stop(); QDateTime now = QDateTime::currentDateTime(); @@ -777,11 +766,6 @@ void TrainTool::guiUpdate() // refreshes the telemetry rtData.setMsecs(total_msecs); rtData.setLapMsecs(lap_msecs); - // metrics - rtData.setJoules(kjoules); - rtData.setBikeScore(bikescore); - rtData.setXPower(xpower); - // local stuff ... displayPower = rtData.getWatts(); displayCadence = rtData.getCadence(); @@ -900,23 +884,6 @@ void TrainTool::diskUpdate() << "," << Altitude << "," << "\n"; - rideFile->appendPoint(total_msecs/1000,displayCadence,displayHeartRate,displayDistance,displaySpeed,0, - displayPower,Altitude,0,0,0,displayLap + displayWorkoutLap); -} - -void TrainTool::metricsUpdate() -{ - // calculate bike score, xpower - const RideMetricFactory &factory = RideMetricFactory::instance(); - const RideMetric *rm = factory.rideMetric("skiba_xpower"); - - QStringList metrics; - metrics.append("skiba_bike_score"); - metrics.append("skiba_xpower"); - QHash results = rm->computeMetrics( - this->main,&*rideFile,this->main->zones(),this->main->hrZones(),metrics); - bikescore = results["skiba_bike_score"]->value(true); - xpower = results["skiba_xpower"]->value(true); } //---------------------------------------------------------------------- diff --git a/src/TrainTool.h b/src/TrainTool.h index 2c802e034..328d57067 100644 --- a/src/TrainTool.h +++ b/src/TrainTool.h @@ -50,7 +50,6 @@ #define STREAMRATE 200 // rate at which we stream updates to remote peer #define SAMPLERATE 1000 // disk update in milliseconds #define LOADRATE 1000 // rate at which load is adjusted -#define METRICSRATE 1000 // rate the metrics are updated // device treeview node types #define HEAD_TYPE 6666 @@ -140,7 +139,6 @@ class TrainTool : public GcWindow void diskUpdate(); // writes to CSV file void streamUpdate(); // writes to remote Peer void loadUpdate(); // sets Load on CT like devices - void metricsUpdate(); // calculates the metrics // When no config has been setup void warnnoConfig(); @@ -183,9 +181,6 @@ class TrainTool : public GcWindow double displayDistance, displayWorkoutDistance; int displayLap; // user increment for Lap int displayWorkoutLap; // which Lap in the workout are we at? - double kjoules; - double bikescore; - double xpower; bool manualOverride; // during an erg woprkout, if the Higher/Lower // signals are called, it switches to manual // until the next lap/interval @@ -197,7 +192,6 @@ class TrainTool : public GcWindow QFile *recordFile; // where we record! ErgFile *ergFile; // workout file - boost::shared_ptr rideFile; // keeps track of the workout to figure out BikeScore long total_msecs, lap_msecs, @@ -210,8 +204,7 @@ class TrainTool : public GcWindow QTimer *gui_timer, // refresh the gui *stream_timer, // send telemetry to server *load_timer, // change the load on the device - *disk_timer, // write to .CSV file - *metrics_timer; // computational intense metrics + *disk_timer; // write to .CSV file public: // everyone else wants this diff --git a/src/xml/train-layout.xml b/src/xml/train-layout.xml index 678ae7e56..35b46f069 100644 --- a/src/xml/train-layout.xml +++ b/src/xml/train-layout.xml @@ -4,7 +4,7 @@ - + @@ -16,107 +16,167 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - + - + - - + + - + - - - - - - - - - + - + - - + + - - - + + + + + + + + + + + - - + + + + + + + + + + + + + +