From 17705c1eaeccb359f496baef9a4e172b42e81957 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Tue, 3 Jan 2012 14:49:41 +0000 Subject: [PATCH] More configurable realtime plot Allow data smoothing, which is particularly useful during bike fit sessions to watch trends rather than micro changes. Also allow the user to select which data series to plot. This allows you to open multiple plots for each series. --- src/RealtimePlot.cpp | 70 +++++++++++- src/RealtimePlot.h | 15 +++ src/RealtimePlotWindow.cpp | 221 +++++++++++++++++++++++++++++++++++-- src/RealtimePlotWindow.h | 57 ++++++++++ 4 files changed, 352 insertions(+), 11 deletions(-) diff --git a/src/RealtimePlot.cpp b/src/RealtimePlot.cpp index fea7b474c..d87f4ceb7 100644 --- a/src/RealtimePlot.cpp +++ b/src/RealtimePlot.cpp @@ -78,7 +78,15 @@ void RealtimeHrData::addData(double v) { hrData[hrCur++] = v; if (hrCur==MAXSAMP //void RealtimeLodData::addData(double v) { lodData[lodCur++] = v; if (lodCur==50) lodCur=0; } -RealtimePlot::RealtimePlot() : pwrCurve(NULL) +RealtimePlot::RealtimePlot() : + pwrCurve(NULL), + showPowerState(Qt::Checked), + showPow30sState(Qt::Checked), + showHrState(Qt::Checked), + showSpeedState(Qt::Checked), + showCadState(Qt::Checked), + showAltState(Qt::Checked), + smooth(0) { setInstanceName("Realtime Plot"); @@ -233,3 +241,63 @@ RealtimePlot::configChanged() spdpen.setWidth(width); spdCurve->setPen(spdpen); } + +void +RealtimePlot::showPower(int state) +{ + showPowerState = state; + pwrCurve->setVisible(state == Qt::Checked); + enableAxis(yLeft, showAltState == Qt::Checked || showPowerState == Qt::Checked || showPow30sState == Qt::Checked); + replot(); +} + +void +RealtimePlot::showPow30s(int state) +{ + showPow30sState = state; + pwr30Curve->setVisible(state == Qt::Checked); + enableAxis(yLeft, showAltState == Qt::Checked || showPowerState == Qt::Checked || showPow30sState == Qt::Checked); + replot(); +} + +void +RealtimePlot::showHr(int state) +{ + showHrState = state; + hrCurve->setVisible(state == Qt::Checked); + enableAxis(yRight, showCadState == Qt::Checked || showHrState == Qt::Checked); + replot(); +} + +void +RealtimePlot::showSpeed(int state) +{ + showSpeedState = state; + spdCurve->setVisible(state == Qt::Checked); + enableAxis(yRight2, state == Qt::Checked); + replot(); +} + +void +RealtimePlot::showCad(int state) +{ + showCadState = state; + cadCurve->setVisible(state == Qt::Checked); + enableAxis(yRight, showCadState == Qt::Checked || showHrState == Qt::Checked); + replot(); +} + +void +RealtimePlot::showAlt(int state) +{ + showAltState = state; + altPwrCurve->setVisible(state == Qt::Checked); + enableAxis(yLeft, showAltState == Qt::Checked || showPowerState == Qt::Checked || showPow30sState == Qt::Checked); + replot(); +} + +void +RealtimePlot::setSmoothing(int value) +{ + smooth = value; +} diff --git a/src/RealtimePlot.h b/src/RealtimePlot.h index 8e4bcefde..81d79dd30 100644 --- a/src/RealtimePlot.h +++ b/src/RealtimePlot.h @@ -167,6 +167,13 @@ class RealtimePlot : public QwtPlot QwtPlotCurve *hrCurve; //QwtPlotCurve *lodCurve; + int showPowerState; + int showPow30sState; + int showHrState; + int showSpeedState; + int showCadState; + int showAltState; + #if 0 // power stores last 30 seconds for 30 second rolling avg, all else // just the last 30 seconds @@ -183,9 +190,17 @@ class RealtimePlot : public QwtPlot //RealtimeLodData lodData; RealtimePlot(); + int smooth; public slots: void configChanged(); + void showPower(int state); + void showPow30s(int state); + void showHr(int state); + void showSpeed(int state); + void showCad(int state); + void showAlt(int state); + void setSmoothing(int value); }; diff --git a/src/RealtimePlotWindow.cpp b/src/RealtimePlotWindow.cpp index 8db042597..c6f2531b8 100644 --- a/src/RealtimePlotWindow.cpp +++ b/src/RealtimePlotWindow.cpp @@ -20,20 +20,82 @@ #include "RealtimePlotWindow.h" RealtimePlotWindow::RealtimePlotWindow(MainWindow *mainWindow) : - GcWindow(mainWindow), mainWindow(mainWindow) + GcWindow(mainWindow), mainWindow(mainWindow), active(false) { setContentsMargins(0,0,0,0); setInstanceName("RT Plot"); - setControls(NULL); setProperty("color", GColor(CRIDEPLOTBACKGROUND)); + QWidget *c = new QWidget; + QVBoxLayout *cl = new QVBoxLayout(c); + setControls(c); + + // setup the controls + QLabel *showLabel = new QLabel(tr("Show"), c); + cl->addWidget(showLabel); + + showHr = new QCheckBox(tr("Heart Rate"), this); + showHr->setCheckState(Qt::Checked); + cl->addWidget(showHr); + + showSpeed = new QCheckBox(tr("Speed"), this); + showSpeed->setCheckState(Qt::Checked); + cl->addWidget(showSpeed); + + showCad = new QCheckBox(tr("Cadence"), this); + showCad->setCheckState(Qt::Checked); + cl->addWidget(showCad); + + showPower = new QCheckBox(tr("Power"), this); + showPower->setCheckState(Qt::Checked); + cl->addWidget(showPower); + + showAlt = new QCheckBox(tr("Alternate Power"), this); + showAlt->setCheckState(Qt::Checked); + cl->addWidget(showAlt); + + showPow30s = new QCheckBox(tr("30s Power"), this); + showPow30s->setCheckState(Qt::Checked); + cl->addWidget(showPow30s); + + QLabel *smoothLabel = new QLabel(tr("Smoothing (5Hz Samples)"), this); + smoothLineEdit = new QLineEdit(this); + smoothLineEdit->setFixedWidth(40); + + cl->addWidget(smoothLabel); + cl->addWidget(smoothLineEdit); + smoothSlider = new QSlider(Qt::Horizontal); + smoothSlider->setTickPosition(QSlider::TicksBelow); + smoothSlider->setTickInterval(10); + smoothSlider->setMinimum(1); + smoothSlider->setMaximum(150); + smoothLineEdit->setValidator(new QIntValidator(smoothSlider->minimum(), + smoothSlider->maximum(), + smoothLineEdit)); + cl->addWidget(smoothSlider); + cl->addStretch(); + QVBoxLayout *layout = new QVBoxLayout(this); rtPlot = new RealtimePlot(); layout->addWidget(rtPlot); + // common controls + connect(showPower, SIGNAL(stateChanged(int)), this, SLOT(setShowPower(int))); + connect(showPow30s, SIGNAL(stateChanged(int)), this, SLOT(setShowPow30s(int))); + connect(showHr, SIGNAL(stateChanged(int)), this, SLOT(setShowHr(int))); + connect(showSpeed, SIGNAL(stateChanged(int)), this, SLOT(setShowSpeed(int))); + connect(showCad, SIGNAL(stateChanged(int)), this, SLOT(setShowCad(int))); + connect(showAlt, SIGNAL(stateChanged(int)), this, SLOT(setShowAlt(int))); + connect(smoothSlider, SIGNAL(valueChanged(int)), this, SLOT(setSmoothingFromSlider())); + connect(smoothLineEdit, SIGNAL(editingFinished()), this, SLOT(setSmoothingFromLineEdit())); + // get updates.. connect(mainWindow, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData))); + // lets initialise all the smoothing variables + hrtot = hrindex = cadtot = cadindex = spdtot = spdindex = alttot = altindex = powtot = powindex = 0; + for(int i=0; i<150; i++) powHist[i] = altHist[i] = spdHist[i] = cadHist[i] = hrHist[i] = 0; + // set to zero telemetryUpdate(RealtimeData()); } @@ -41,13 +103,17 @@ RealtimePlotWindow::RealtimePlotWindow(MainWindow *mainWindow) : void RealtimePlotWindow::start() { - //resetValues(); + // lets initialise all the smoothing variables + hrtot = hrindex = cadtot = cadindex = spdtot = spdindex = alttot = altindex = powtot = powindex = 0; + for(int i=0; i<150; i++) powHist[i] = altHist[i] = spdHist[i] = cadHist[i] = hrHist[i] = 0; } void RealtimePlotWindow::stop() { - //resetValues(); + // lets initialise all the smoothing variables + hrtot = hrindex = cadtot = cadindex = spdtot = spdindex = alttot = altindex = powtot = powindex = 0; + for(int i=0; i<150; i++) powHist[i] = altHist[i] = spdHist[i] = cadHist[i] = hrHist[i] = 0; } void @@ -58,11 +124,146 @@ RealtimePlotWindow::pause() void RealtimePlotWindow::telemetryUpdate(RealtimeData rtData) { - rtPlot->pwrData.addData(rtData.value(RealtimeData::Watts)); - rtPlot->altPwrData.addData(rtData.value(RealtimeData::AltWatts)); - rtPlot->pwr30Data.addData(rtData.value(RealtimeData::Watts)); - rtPlot->cadData.addData(rtData.value(RealtimeData::Cadence)); - rtPlot->spdData.addData(rtData.value(RealtimeData::Speed)); - rtPlot->hrData.addData(rtData.value(RealtimeData::HeartRate)); + // lets apply smoothing if we have to + if (rtPlot->smooth) { + + // Heartrate + double hr = rtData.value(RealtimeData::HeartRate); + hrtot += hr; + hrtot -= hrHist[hrindex]; + hrHist[hrindex] = hr; + hrindex++; + if (hrindex >= rtPlot->smooth) hrindex = 0; + hr = hrtot / rtPlot->smooth; + rtPlot->hrData.addData(hr); + + // Speed + double spd= rtData.value(RealtimeData::Speed); + spdtot += spd; spdtot -= spdHist[spdindex]; spdHist[spdindex] = spd; + spdindex++; if (spdindex >= rtPlot->smooth) spdindex = 0; + spd = spdtot / rtPlot->smooth; + rtPlot->spdData.addData(spd); + + // Power + double pow = rtData.value(RealtimeData::Watts); + powtot += pow; powtot -= powHist[powindex]; powHist[powindex] = pow; + powindex++; if (powindex >= rtPlot->smooth) powindex = 0; + pow = powtot / rtPlot->smooth; + rtPlot->pwrData.addData(pow); + + // Alternate Power + double alt = rtData.value(RealtimeData::AltWatts); + alttot += alt; alttot -= altHist[altindex]; altHist[altindex] = alt; + altindex++; if (altindex >= rtPlot->smooth) altindex = 0; + alt = alttot / rtPlot->smooth; + rtPlot->altPwrData.addData(alt); + + // Cadence + double cad = rtData.value(RealtimeData::Cadence); + cadtot += cad; cadtot -= cadHist[cadindex]; cadHist[cadindex] = cad; + cadindex++; if (cadindex >= rtPlot->smooth) cadindex = 0; + cad = cadtot / rtPlot->smooth; + rtPlot->cadData.addData(cad); + + // its smoothed to 30s anyway + rtPlot->pwr30Data.addData(rtData.value(RealtimeData::Watts)); + + } else { + rtPlot->pwrData.addData(rtData.value(RealtimeData::Watts)); + rtPlot->altPwrData.addData(rtData.value(RealtimeData::AltWatts)); + rtPlot->pwr30Data.addData(rtData.value(RealtimeData::Watts)); + rtPlot->cadData.addData(rtData.value(RealtimeData::Cadence)); + rtPlot->spdData.addData(rtData.value(RealtimeData::Speed)); + rtPlot->hrData.addData(rtData.value(RealtimeData::HeartRate)); + } rtPlot->replot(); // redraw } + +void +RealtimePlotWindow::setSmoothingFromSlider() +{ + // active tells us we have been triggered by + // the setSmoothingFromLineEdit which will also + // recalculates smoothing, lets not double up... + if (active) return; + else active = true; + + if (rtPlot->smooth != smoothSlider->value()) { + setSmoothing(smoothSlider->value()); + smoothLineEdit->setText(QString("%1").arg(rtPlot->smooth)); + } + active = false; +} + +void +RealtimePlotWindow::setSmoothingFromLineEdit() +{ + // active tells us we have been triggered by + // the setSmoothingFromSlider which will also + // recalculates smoothing, lets not double up... + if (active) return; + else active = true; + + int value = smoothLineEdit->text().toInt(); + if (value != rtPlot->smooth) { + smoothSlider->setValue(value); + setSmoothing(value); + } + active = false; +} + +void +RealtimePlotWindow::setShowPow30s(int value) +{ + showPow30s->setChecked(value); + rtPlot->showPow30s(value); + rtPlot->replot(); +} + +void +RealtimePlotWindow::setShowPower(int value) +{ + showPower->setChecked(value); + rtPlot->showPower(value); + rtPlot->replot(); +} + +void +RealtimePlotWindow::setShowHr(int value) +{ + showHr->setChecked(value); + rtPlot->showHr(value); + rtPlot->replot(); +} + +void +RealtimePlotWindow::setShowSpeed(int value) +{ + showSpeed->setChecked(value); + rtPlot->showSpeed(value); + rtPlot->replot(); +} + +void +RealtimePlotWindow::setShowCad(int value) +{ + showCad->setChecked(value); + rtPlot->showCad(value); + rtPlot->replot(); +} + +void +RealtimePlotWindow::setShowAlt(int value) +{ + showAlt->setChecked(value); + rtPlot->showAlt(value); + rtPlot->replot(); +} + +void +RealtimePlotWindow::setSmoothing(int value) +{ + hrtot = hrindex = cadtot = cadindex = spdtot = spdindex = alttot = altindex = powtot = powindex = 0; + smoothSlider->setValue(value); + rtPlot->setSmoothing(value); +} diff --git a/src/RealtimePlotWindow.h b/src/RealtimePlotWindow.h index e02d7f9f6..d49bf7610 100644 --- a/src/RealtimePlotWindow.h +++ b/src/RealtimePlotWindow.h @@ -37,10 +37,27 @@ class RealtimePlotWindow : public GcWindow Q_OBJECT G_OBJECT + Q_PROPERTY(int showHr READ isShowHr WRITE setShowHr USER true) + Q_PROPERTY(int showSpeed READ isShowSpeed WRITE setShowSpeed USER true) + Q_PROPERTY(int showCad READ isShowCad WRITE setShowCad USER true) + Q_PROPERTY(int showAlt READ isShowAlt WRITE setShowAlt USER true) + Q_PROPERTY(int showPower READ isShowPower WRITE setShowPower USER true) + Q_PROPERTY(int showPow30s READ isShowPow30s WRITE setShowPow30s USER true) + Q_PROPERTY(int smoothing READ smoothing WRITE setSmoothing USER true) + public: RealtimePlotWindow(MainWindow *mainWindow); + // get properties - the setters are below + int isShowHr() const { return showHr->checkState(); } + int isShowSpeed() const { return showSpeed->checkState(); } + int isShowCad() const { return showCad->checkState(); } + int isShowAlt() const { return showAlt->checkState(); } + int isShowPower() const { return showPower->checkState(); } + int isShowPow30s() const { return showPow30s->checkState(); } + int smoothing() const { return smoothSlider->value(); } + public slots: // trap signals @@ -49,10 +66,50 @@ class RealtimePlotWindow : public GcWindow void stop(); void pause(); + // set properties + void setSmoothingFromSlider(); + void setSmoothingFromLineEdit(); + void setShowPower(int state); + void setShowPow30s(int state); + void setShowHr(int state); + void setShowSpeed(int state); + void setShowCad(int state); + void setShowAlt(int state); + void setSmoothing(int value); + private: MainWindow *mainWindow; RealtimePlot *rtPlot; + bool active; + + // Common controls + QGridLayout *controlsLayout; + QCheckBox *showHr; + QCheckBox *showSpeed; + QCheckBox *showCad; + QCheckBox *showAlt; + QCheckBox *showPower; + QCheckBox *showPow30s; + QSlider *smoothSlider; + QLineEdit *smoothLineEdit; + + // for smoothing charts + double powHist[150]; + double powtot; + int powindex; + double altHist[150]; + double alttot; + int altindex; + double spdHist[150]; + double spdtot; + int spdindex; + double cadHist[150]; + double cadtot; + int cadindex; + double hrHist[150]; + double hrtot; + int hrindex; }; #endif // _GC_RealtimePlotWindow_h