Files
GoldenCheetah/src/TrainTool.cpp
Mark Liversedge fb626e683f Add support for Tacx/RLV Workout (.pgmf)
The Tacx ergometer has a workout file format
(.pgmf) that is similar in concept to the
Racermate .CRS/.ERG/.MRC file format.

This patch adds support for this kind of
workout file.

Further work is required to support the .rlv
file format in order to support video playback
to match the user's speed (keeping the video
in sync as you ride).

In addition, for Turbos that do not support
variable load using gradient/slope we will need
to add an algorithm to approximate speed from
power/weight/slope.

Fixes #382
2011-11-12 18:30:56 +00:00

1210 lines
37 KiB
C++

/*
* Copyright (c) 2009 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 "TrainTool.h"
#include "MainWindow.h"
#include "Settings.h"
#include "Colors.h"
#include "Units.h"
#include "DeviceTypes.h"
#include "DeviceConfiguration.h"
#include <assert.h>
#include <QApplication>
#include <QtGui>
#include <QRegExp>
// Three current realtime device types supported are:
#include "RealtimeController.h"
#include "ComputrainerController.h"
#include "ANTplusController.h"
#include "ANTlocalController.h"
#include "NullController.h"
#ifdef GC_HAVE_VLC
// Media selection helper
#include "VideoWindow.h"
#endif
#ifdef Q_OS_MAC
#include "QtMacVideoWindow.h"
#include <CoreServices/CoreServices.h>
#endif
TrainTool::TrainTool(MainWindow *parent, const QDir &home) : GcWindow(parent), home(home), main(parent)
{
setInstanceName("Train Controls");
QWidget *c = new QWidget;
//c->setContentsMargins(0,0,0,0); // bit of space is useful
QVBoxLayout *cl = new QVBoxLayout(c);
setControls(c);
cl->setSpacing(0);
cl->setContentsMargins(0,0,0,0);
#if 0 // not in this release .. or for a while TBH
serverTree = new QTreeWidget;
serverTree->setFrameStyle(QFrame::NoFrame);
serverTree->setColumnCount(1);
serverTree->setSelectionMode(QAbstractItemView::SingleSelection);
serverTree->header()->hide();
serverTree->setAlternatingRowColors (false);
serverTree->setIndentation(5);
allServers = new QTreeWidgetItem(serverTree, HEAD_TYPE);
allServers->setText(0, tr("Race Servers"));
serverTree->expandItem(allServers);
#endif
#if defined Q_OS_MAC || defined GC_HAVE_VLC
mediaTree = new QTreeWidget;
mediaTree->setFrameStyle(QFrame::NoFrame);
mediaTree->setColumnCount(1);
mediaTree->setSelectionMode(QAbstractItemView::SingleSelection);
mediaTree->header()->hide();
mediaTree->setAlternatingRowColors (false);
mediaTree->setIndentation(5);
allMedia = new QTreeWidgetItem(mediaTree, HEAD_TYPE);
allMedia->setText(0, tr("Video / Media"));
mediaTree->expandItem(allMedia);
#endif
deviceTree = new QTreeWidget;
deviceTree->setFrameStyle(QFrame::NoFrame);
deviceTree->setSelectionMode(QAbstractItemView::MultiSelection);
deviceTree->setColumnCount(1);
deviceTree->header()->hide();
deviceTree->setAlternatingRowColors (false);
deviceTree->setIndentation(5);
allDevices = new QTreeWidgetItem(deviceTree, HEAD_TYPE);
allDevices->setText(0, tr("Devices"));
deviceTree->expandItem(allDevices);
workoutTree = new QTreeWidget;
workoutTree->setFrameStyle(QFrame::NoFrame);
workoutTree->setColumnCount(1);
workoutTree->setSelectionMode(QAbstractItemView::SingleSelection);
workoutTree->header()->hide();
workoutTree->setAlternatingRowColors (false);
workoutTree->setIndentation(5);
allWorkouts = new QTreeWidgetItem(workoutTree, HEAD_TYPE);
allWorkouts->setText(0, tr("Workout Library"));
workoutTree->expandItem(allWorkouts);
// TOOLBAR BUTTONS ETC
QHBoxLayout *toolbuttons=new QHBoxLayout;
toolbuttons->setSpacing(0);
toolbuttons->setContentsMargins(0,0,0,0);
QIcon rewIcon(":images/oxygen/rewind.png");
QPushButton *rewind = new QPushButton(rewIcon, "", this);
rewind->setFocusPolicy(Qt::NoFocus);
rewind->setIconSize(QSize(24,24));
rewind->setAutoFillBackground(false);
rewind->setAutoDefault(false);
rewind->setFlat(true);
rewind->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
toolbuttons->addWidget(rewind);
QIcon stopIcon(":images/oxygen/stop.png");
QPushButton *stop = new QPushButton(stopIcon, "", this);
stop->setFocusPolicy(Qt::NoFocus);
stop->setIconSize(QSize(24,24));
stop->setAutoFillBackground(false);
stop->setAutoDefault(false);
stop->setFlat(true);
stop->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
toolbuttons->addWidget(stop);
QIcon playIcon(":images/oxygen/play.png");
play = new QPushButton(playIcon, "", this);
play->setFocusPolicy(Qt::NoFocus);
play->setIconSize(QSize(24,24));
play->setAutoFillBackground(false);
play->setAutoDefault(false);
play->setFlat(true);
play->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
toolbuttons->addWidget(play);
QIcon fwdIcon(":images/oxygen/ffwd.png");
QPushButton *forward = new QPushButton(fwdIcon, "", this);
forward->setFocusPolicy(Qt::NoFocus);
forward->setIconSize(QSize(24,24));
forward->setAutoFillBackground(false);
forward->setAutoDefault(false);
forward->setFlat(true);
forward->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
toolbuttons->addWidget(forward);
intensitySlider = new QSlider(Qt::Horizontal, this);
intensitySlider->setAutoFillBackground(false);
intensitySlider->setFocusPolicy(Qt::NoFocus);
intensitySlider->setMinimum(50);
intensitySlider->setMaximum(150);
intensitySlider->setValue(100);
toolbuttons->addWidget(intensitySlider);
QPalette pal;
stress = new QLabel(this);
stress->setAutoFillBackground(false);
stress->setFixedWidth(100);
stress->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);
pal.setColor(stress->foregroundRole(), Qt::white);
stress->setPalette(pal);
intensity = new QLabel(this);
intensity->setAutoFillBackground(false);
intensity->setFixedWidth(100);
intensity->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);
pal.setColor(intensity->foregroundRole(), Qt::white);
intensity->setPalette(pal);
toolbuttons->addWidget(stress, Qt::AlignVCenter|Qt::AlignCenter);
toolbuttons->addWidget(intensity, Qt::AlignVCenter|Qt::AlignCenter);
toolbuttons->addStretch();
toolbarButtons = new QWidget(this);
toolbarButtons->setContentsMargins(0,0,0,0);
toolbarButtons->setFocusPolicy(Qt::NoFocus);
toolbarButtons->setAutoFillBackground(false);
toolbarButtons->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
toolbarButtons->setLayout(toolbuttons);
toolbarButtons->hide();
connect(play, SIGNAL(clicked()), this, SLOT(Start()));
connect(stop, SIGNAL(clicked()), this, SLOT(Stop()));
connect(forward, SIGNAL(clicked()), this, SLOT(FFwd()));
connect(rewind, SIGNAL(clicked()), this, SLOT(Rewind()));
connect(intensitySlider, SIGNAL(valueChanged(int)), this, SLOT(adjustIntensity()));
// not used but kept in case re-instated in the future
recordSelector = new QCheckBox(this);
recordSelector->setText(tr("Save workout data"));
recordSelector->setChecked(Qt::Checked);
recordSelector->hide(); // we don't let users change this for now
trainSplitter = new QSplitter;
trainSplitter->setHandleWidth(1);
trainSplitter->setFrameStyle(QFrame::NoFrame);
trainSplitter->setOrientation(Qt::Vertical);
trainSplitter->setContentsMargins(0,0,0,0);
trainSplitter->setLineWidth(0);
trainSplitter->setMidLineWidth(0);
cl->addWidget(trainSplitter);
trainSplitter->addWidget(deviceTree);
//trainSplitter->addWidget(serverTree);
trainSplitter->addWidget(workoutTree);
#if defined Q_OS_MAC || defined GC_HAVE_VLC
trainSplitter->addWidget(mediaTree);
#endif
// handle config changes
//connect(serverTree,SIGNAL(itemSelectionChanged()), this, SLOT(serverTreeWidgetSelectionChanged()));
connect(deviceTree,SIGNAL(itemSelectionChanged()), this, SLOT(deviceTreeWidgetSelectionChanged()));
connect(workoutTree,SIGNAL(itemSelectionChanged()), this, SLOT(workoutTreeWidgetSelectionChanged()));
#if defined Q_OS_MAC || defined GC_HAVE_VLC
connect(mediaTree,SIGNAL(itemSelectionChanged()), this, SLOT(mediaTreeWidgetSelectionChanged()));
#endif
connect(main, SIGNAL(configChanged()), this, SLOT(configChanged()));
// add a watch on all directories
QVariant workoutDir = appsettings->value(NULL, GC_WORKOUTDIR);
watcher = boost::shared_ptr<QFileSystemWatcher>(new QFileSystemWatcher());
watcher->addPaths(workoutDir.toStringList());
connect(&*watcher,SIGNAL(directoryChanged(QString)),this,SLOT(configChanged()));
connect(&*watcher,SIGNAL(fileChanged(QString)),this,SLOT(configChanged()));
// set home
main = parent;
deviceController = NULL;
streamController = NULL;
ergFile = NULL;
// metric or imperial?
QVariant unit = appsettings->value(this, GC_UNIT);
useMetricUnits = (unit.toString() == "Metric");
// now the GUI is setup lets sort our control variables
gui_timer = new QTimer(this);
disk_timer = new QTimer(this);
stream_timer = new QTimer(this);
load_timer = new QTimer(this);
session_time = QTime();
session_elapsed_msec = 0;
lap_time = QTime();
lap_elapsed_msec = 0;
recordFile = NULL;
status = 0;
status |= RT_MODE_ERGO; // ergo mode by default
displayWorkoutLap = displayLap = 0;
pwrcount = 0;
cadcount = 0;
hrcount = 0;
spdcount = 0;
lodcount = 0;
load_msecs = total_msecs = lap_msecs = 0;
displayWorkoutDistance = displayDistance = displayPower = displayHeartRate =
displaySpeed = displayCadence = displayGradient = displayLoad = 0;
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()));
configChanged(); // will reset the workout tree
setLabels();
}
void
TrainTool::configChanged()
{
setProperty("color", GColor(CRIDEPLOTBACKGROUND));
// DEVICES
// zap whats there
//QList<QTreeWidgetItem *> servers = allServers->takeChildren();
//for (int i=0; i<servers.count(); i++) delete servers.at(i);
QList<QTreeWidgetItem *> devices = allDevices->takeChildren();
for (int i=0; i<devices.count(); i++) delete devices.at(i);
DeviceConfigurations all;
Devices.clear();
Devices = all.getList();
for (int i=0; i<Devices.count(); i++) {
if (Devices.at(i).type == DEV_GSERVER) {
//QTreeWidgetItem *server = new QTreeWidgetItem(allServers, i);
//server->setText(0, Devices.at(i).name);
} else {
QTreeWidgetItem *device = new QTreeWidgetItem(allDevices, i);
device->setText(0, Devices.at(i).name);
}
}
// select the first device
if (Devices.count()) {
deviceTree->setCurrentItem(allDevices->child(0));
}
// WORKOUTS
// zap whats there
QList<QTreeWidgetItem *> workouts = allWorkouts->takeChildren();
for (int i=0; i<workouts.count(); i++) delete workouts.at(i);
// standard workouts - ergo and slope
QTreeWidgetItem *ergomode = new QTreeWidgetItem(allWorkouts, WORKOUT_TYPE);
ergomode->setText(0, tr("Manual Ergo Mode"));
QTreeWidgetItem *slopemode = new QTreeWidgetItem(allWorkouts, WORKOUT_TYPE);
slopemode->setText(0, tr("Manual Slope Mode"));
// add all the workouts in the library
QVariant workoutDir = appsettings->value(this, GC_WORKOUTDIR);
QStringListIterator w(listWorkoutFiles(workoutDir.toString()));
while (w.hasNext()) {
QString name = w.next();
QTreeWidgetItem *work = new QTreeWidgetItem(allWorkouts, WORKOUT_TYPE);
work->setText(0, name);
}
#if defined Q_OS_MAC || defined GC_HAVE_VLC
// MEDIA
QList<QTreeWidgetItem *> media = allMedia->takeChildren();
for (int i=0; i<media.count(); i++) delete media.at(i);
#ifndef Q_OS_MAC
// add dvd playback via VLC
QTreeWidgetItem *dvd = new QTreeWidgetItem(allMedia, WORKOUT_TYPE);
dvd->setText(0, "DVD");
#endif
MediaHelper mediaHelper;
foreach(QString video, mediaHelper.listMedia(QDir(workoutDir.toString()))) {
// add a media line for the video (it might be a song though...)
QTreeWidgetItem *media = new QTreeWidgetItem(allMedia, WORKOUT_TYPE);
media->setText(0, video);
}
#endif
// Athlete
FTP=285; // default to 285 if zones are not set
int range = main->zones()->whichRange(QDate::currentDate());
if (range != -1) FTP = main->zones()->getCP(range);
// metric or imperial changed?
QVariant unit = appsettings->value(this, GC_UNIT);
useMetricUnits = (unit.toString() == "Metric");
}
/*----------------------------------------------------------------------
* Race Server Selected
*----------------------------------------------------------------------*/
void
TrainTool::serverTreeWidgetSelectionChanged()
{
serverSelected();
}
int
TrainTool::selectedServerNumber()
{
if (serverTree->selectedItems().isEmpty()) return -1;
QTreeWidgetItem *selected = serverTree->selectedItems().first();
if (selected->type() == HEAD_TYPE) return -1;
else return selected->type();
}
/*----------------------------------------------------------------------
* Device Selected
*--------------------------------------------------------------------*/
void
TrainTool::deviceTreeWidgetSelectionChanged()
{
deviceSelected();
}
int
TrainTool::selectedDeviceNumber()
{
if (deviceTree->selectedItems().isEmpty()) return -1;
QTreeWidgetItem *selected = deviceTree->selectedItems().first();
if (selected->type() == HEAD_TYPE) return -1;
else return selected->type();
}
/*----------------------------------------------------------------------
* Workout Selected
*--------------------------------------------------------------------*/
void
TrainTool::workoutTreeWidgetSelectionChanged()
{
assert(workoutTree->selectedItems().size() <= 1);
if (workoutTree->selectedItems().isEmpty())
workout = NULL;
else {
QTreeWidgetItem *which = workoutTree->selectedItems().first();
if (which->type() != WORKOUT_TYPE)
workout = NULL;
else
workout = which;
}
int mode;
// wip away the current selected workout
if (ergFile) {
delete ergFile;
ergFile = NULL;
}
// which one is selected?
if (currentWorkout() == NULL || currentWorkout()->type() != WORKOUT_TYPE) {
main->notifyErgFileSelected(NULL);
setLabels();
return;
}
// is it the auto mode?
int index = workoutItems()->indexOfChild((QTreeWidgetItem *)currentWorkout());
if (index == 0) {
// ergo mode
main->notifyErgFileSelected(NULL);
mode = ERG;
setLabels();
status &= ~RT_WORKOUT;
//ergPlot->setVisible(false);
} else if (index == 1) {
// slope mode
main->notifyErgFileSelected(NULL);
mode = CRS;
setLabels();
status &= ~RT_WORKOUT;
//ergPlot->setVisible(false);
} else {
// workout mode
QVariant workoutDir = appsettings->value(this, GC_WORKOUTDIR);
QString fileName = workoutDir.toString() + "/" + currentWorkout()->text(0); // filename
ergFile = new ErgFile(fileName, mode, FTP, main);
if (ergFile->isValid()) {
status |= RT_WORKOUT;
// success! we have a load file
// setup the course profile in the
// display!
main->notifyErgFileSelected(ergFile);
intensitySlider->setValue(100);
lastAppliedIntensity = 100;
setLabels();
} else {
// couldn't parse fall back to ERG mode
delete ergFile;
ergFile = NULL;
main->notifyErgFileSelected(NULL);
mode = ERG;
status &= ~RT_WORKOUT;
setLabels();
}
}
// set the device to the right mode
if (mode == ERG || mode == MRC) {
status |= RT_MODE_ERGO;
status &= ~RT_MODE_SPIN;
if (deviceController != NULL) deviceController->setMode(RT_MODE_ERGO);
} else { // SLOPE MODE
status |= RT_MODE_SPIN;
status &= ~RT_MODE_ERGO;
if (deviceController != NULL) deviceController->setMode(RT_MODE_SPIN);
}
}
QStringList
TrainTool::listWorkoutFiles(const QDir &dir) const
{
QStringList filters;
filters << "*.erg";
filters << "*.mrc";
filters << "*.crs";
filters << "*.pgmf";
return dir.entryList(filters, QDir::Files, QDir::Name);
}
void
TrainTool::mediaTreeWidgetSelectionChanged()
{
assert(mediaTree->selectedItems().size() <= 1);
if (mediaTree->selectedItems().isEmpty())
media = NULL;
else {
QTreeWidgetItem *which = mediaTree->selectedItems().first();
if (which->type() != WORKOUT_TYPE)
media = NULL;
else
media = which;
}
// which one is selected?
if (currentMedia() == NULL || currentMedia()->type() != WORKOUT_TYPE) {
main->notifyMediaSelected("");
return;
}
QVariant workoutDir = appsettings->value(this, GC_WORKOUTDIR);
QString fileName = workoutDir.toString() + "/" + currentMedia()->text(0); // filename
main->notifyMediaSelected(fileName);
}
/*--------------------------------------------------------------------------------
* Was realtime window, now local and manages controller and chart updates etc
*------------------------------------------------------------------------------*/
void TrainTool::setDeviceController()
{
int deviceno = selectedDeviceNumber();
if (deviceno == -1) // not selected, maybe they are spectating
return;
// zap the current one
if (deviceController != NULL) {
delete deviceController;
deviceController = NULL;
}
if (Devices.count() > 0) {
DeviceConfiguration temp = Devices.at(deviceno);
if (Devices.at(deviceno).type == DEV_ANTPLUS) {
deviceController = new ANTplusController(this, &temp);
} else if (Devices.at(deviceno).type == DEV_CT) {
deviceController = new ComputrainerController(this, &temp);
} else if (Devices.at(deviceno).type == DEV_NULL) {
deviceController = new NullController(this, &temp);
} else if (Devices.at(deviceno).type == DEV_ANTLOCAL) {
deviceController = new ANTlocalController(this, &temp);
}
}
}
// open a connection to the GoldenServer via a GoldenClient
void TrainTool::setStreamController()
{
int deviceno = selectedServerNumber();
if (deviceno == -1) return;
// zap the current one
if (streamController != NULL) {
delete streamController;
streamController = NULL;
}
if (Devices.count() > 0) {
DeviceConfiguration config = Devices.at(deviceno);
streamController = new GoldenClient;
// connect
QStringList speclist = config.portSpec.split(":", QString::SkipEmptyParts);
bool rc = streamController->connect(speclist[0], // host
speclist[1].toInt(), // port
"9cf638294030cea7b1590a4ca32e7f58", // raceid
appsettings->cvalue(main->cyclist, GC_NICKNAME).toString(), // name
FTP, // CP60
appsettings->cvalue(main->cyclist, GC_WEIGHT).toDouble()); // weight
// no connection
if (rc == false) {
streamController->closeAndExit();
streamController = NULL;
status &= ~RT_STREAMING;
QMessageBox msgBox;
msgBox.setText(QString(tr("Cannot Connect to Server %1 on port %2").arg(speclist[0]).arg(speclist[1])));
msgBox.setIcon(QMessageBox::Critical);
msgBox.exec();
}
}
}
void TrainTool::Start() // when start button is pressed
{
static QIcon playIcon(":images/oxygen/play.png");
static QIcon pauseIcon(":images/oxygen/pause.png");
if (status&RT_PAUSED && deviceController != NULL) {
// UN PAUSE!
play->setIcon(playIcon);
session_time.start();
lap_time.start();
status &=~RT_PAUSED;
deviceController->restart();
gui_timer->start(REFRESHRATE);
if (status & RT_STREAMING) stream_timer->start(STREAMRATE);
if (status & RT_RECORDING) disk_timer->start(SAMPLERATE);
load_period.restart();
if (status & RT_WORKOUT) load_timer->start(LOADRATE);
// tell the world
main->notifyUnPause();
} else if (status&RT_RUNNING && deviceController != NULL) {
// Pause!
play->setIcon(playIcon);
session_elapsed_msec += session_time.elapsed();
lap_elapsed_msec += lap_time.elapsed();
deviceController->pause();
status |=RT_PAUSED;
gui_timer->stop();
if (status & RT_STREAMING) stream_timer->stop();
if (status & RT_RECORDING) disk_timer->stop();
if (status & RT_WORKOUT) load_timer->stop();
load_msecs += load_period.restart();
// tell the world
main->notifyPause();
} else {
// START!
play->setIcon(pauseIcon);
// open the controller if it is selected
setDeviceController();
if (deviceController == NULL) return;
else deviceController->start(); // start device
// tell the world
main->notifyStart();
// we're away!
status |=RT_RUNNING;
// should we be streaming too?
//setStreamController();
if (streamController != NULL) status |= RT_STREAMING;
load_period.restart();
session_time.start();
session_elapsed_msec = 0;
lap_time.start();
lap_elapsed_msec = 0;
if (status & RT_WORKOUT) {
load_timer->start(LOADRATE); // start recording
}
if (recordSelector->isChecked()) {
status |= RT_RECORDING;
}
if (status & RT_RECORDING) {
QDateTime now = QDateTime::currentDateTime();
// setup file
QString filename = now.toString(QString("yyyy_MM_dd_hh_mm_ss")) + QString(".csv");
QString fulltarget = home.absolutePath() + "/" + filename;
if (recordFile) delete recordFile;
recordFile = new QFile(fulltarget);
if (!recordFile->open(QFile::WriteOnly | QFile::Truncate)) {
status &= ~RT_RECORDING;
} else {
// CSV File header
QTextStream recordFileStream(recordFile);
recordFileStream << "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)\n";
disk_timer->start(SAMPLERATE); // start screen
}
}
// stream
if (status & RT_STREAMING) {
stream_timer->start(STREAMRATE);
}
gui_timer->start(REFRESHRATE); // start recording
}
}
void TrainTool::Pause() // pause capture to recalibrate
{
if (deviceController == NULL) return;
// we're not running fool!
if ((status&RT_RUNNING) == 0) return;
if (status&RT_PAUSED) {
session_time.start();
lap_time.start();
status &=~RT_PAUSED;
deviceController->restart();
gui_timer->start(REFRESHRATE);
if (status & RT_STREAMING) stream_timer->start(STREAMRATE);
if (status & RT_RECORDING) disk_timer->start(SAMPLERATE);
load_period.restart();
if (status & RT_WORKOUT) load_timer->start(LOADRATE);
// tell the world
main->notifyUnPause();
} else {
session_elapsed_msec += session_time.elapsed();
lap_elapsed_msec += lap_time.elapsed();
deviceController->pause();
status |=RT_PAUSED;
gui_timer->stop();
if (status & RT_STREAMING) stream_timer->stop();
if (status & RT_RECORDING) disk_timer->stop();
if (status & RT_WORKOUT) load_timer->stop();
load_msecs += load_period.restart();
// tell the world
main->notifyPause();
}
}
void TrainTool::Stop(int deviceStatus) // when stop button is pressed
{
if (deviceController == NULL) return;
if ((status&RT_RUNNING) == 0) return;
status &= ~RT_RUNNING;
// wipe connection
deviceController->stop();
delete deviceController;
deviceController = NULL;
gui_timer->stop();
QDateTime now = QDateTime::currentDateTime();
if (status & RT_RECORDING) {
disk_timer->stop();
// close and reset File
recordFile->close();
if(deviceStatus == DEVICE_ERROR)
{
recordFile->remove();
}
else {
// add to the view - using basename ONLY
QString name;
name = recordFile->fileName();
main->addRide(QFileInfo(name).fileName(), true);
}
}
if (status & RT_STREAMING) {
stream_timer->stop();
streamController->closeAndExit();
delete streamController;
streamController = NULL;
}
if (status & RT_WORKOUT) {
load_timer->stop();
load_msecs = 0;
}
// get back to normal after it may have been adusted by the user
lastAppliedIntensity=100;
intensitySlider->setValue(100);
if (main->currentErgFile()) main->currentErgFile()->reload();
main->notifySetNow(load_msecs);
// reset the play button
QIcon playIcon(":images/oxygen/play.png");
play->setIcon(playIcon);
// tell the world
main->notifyStop();
// Re-enable gui elements
//recordSelector->setEnabled(true);
// reset counters etc
pwrcount = 0;
cadcount = 0;
hrcount = 0;
spdcount = 0;
lodcount = 0;
displayWorkoutLap = displayLap =0;
session_elapsed_msec = 0;
session_time.restart();
lap_elapsed_msec = 0;
lap_time.restart();
displayWorkoutDistance = displayDistance = 0;
guiUpdate();
return;
}
// Called by push devices (e.g. ANT+)
void TrainTool::updateData(RealtimeData &rtData)
{
displayPower = rtData.getWatts();
displayCadence = rtData.getCadence();
displayHeartRate = rtData.getHr();
displaySpeed = rtData.getSpeed();
displayLoad = rtData.getLoad();
// Gradient not supported
return;
}
//----------------------------------------------------------------------
// SCREEN UPDATE FUNCTIONS
//----------------------------------------------------------------------
void TrainTool::guiUpdate() // refreshes the telemetry
{
RealtimeData rtData;
rtData.setLap(displayLap + displayWorkoutLap); // user laps + predefined workout lap
if (deviceController == NULL) return;
// On a Mac prevent the screensaver from kicking in
// this is apparently the 'supported' mechanism for
// disabling the screen saver on a Mac instead of
// temporarily adjusting/disabling the user preferences
// for screen saving and power management. Makes sense.
#ifdef Q_OS_MAC
UpdateSystemActivity(OverallAct);
#endif
// get latest telemetry from device (if it is a pull device e.g. Computrainer //
if (status&RT_RUNNING && deviceController->doesPull() == true) {
deviceController->getRealtimeData(rtData);
// Distance assumes current speed for the last second. from km/h to km/sec
displayDistance += displaySpeed / (5 * 3600); // XXX assumes 200ms refreshrate
displayWorkoutDistance += displaySpeed / (5 * 3600); // XXX assumes 200ms refreshrate
rtData.setDistance(displayDistance);
// time
total_msecs = session_elapsed_msec + session_time.elapsed();
lap_msecs = lap_elapsed_msec + lap_time.elapsed();
rtData.setMsecs(total_msecs);
rtData.setLapMsecs(lap_msecs);
// local stuff ...
displayPower = rtData.getWatts();
displayCadence = rtData.getCadence();
displayHeartRate = rtData.getHr();
displaySpeed = rtData.getSpeed();
displayLoad = rtData.getLoad();
// go update the displays...
main->notifyTelemetryUpdate(rtData); // signal everyone to update telemetry
// set now to current time when not using a workout
// but limit to almost every second (account for
// slight timing errors of 100ms or so)
if (!(status&RT_WORKOUT) && rtData.getMsecs()%1000 < 100) {
main->notifySetNow(rtData.getMsecs());
}
}
}
// can be called from the controller - when user presses "Lap" button
void TrainTool::newLap()
{
displayLap++;
pwrcount = 0;
cadcount = 0;
hrcount = 0;
spdcount = 0;
lap_time.restart();
lap_elapsed_msec = 0;
main->notifyNewLap();
}
// can be called from the controller
void TrainTool::nextDisplayMode()
{
}
void TrainTool::warnnoConfig()
{
QMessageBox::warning(this, tr("No Devices Configured"), "Please configure a device in Preferences.");
}
//----------------------------------------------------------------------
// STREAMING FUNCTION
//----------------------------------------------------------------------
#if 0
TrainTool::SelectStream(int index)
{
if (index > 0) {
status |= RT_STREAMING;
setStreamController();
} else {
status &= ~RT_STREAMING;
}
}
#endif
void
TrainTool::streamUpdate()
{
// send over the wire...
if (streamController) {
// send my data
streamController->sendTelemetry(displayPower,
displayCadence,
displayDistance,
displayHeartRate,
displaySpeed);
// get standings for everyone else
RaceStatus current = streamController->getStandings();
// send out to all the widgets...
notifyRaceStandings(current);
// has the race finished?
if (current.race_finished == true) {
Stop(0); // all over dude
QMessageBox msgBox;
msgBox.setText(tr("Race Over!"));
msgBox.setIcon(QMessageBox::Information);
msgBox.exec();
}
}
}
//----------------------------------------------------------------------
// DISK UPDATE FUNCTIONS
//----------------------------------------------------------------------
void TrainTool::diskUpdate()
{
double Minutes;
long Torq = 0, Altitude = 0;
QTextStream recordFileStream(recordFile);
// convert from milliseconds to minutes
total_msecs = session_elapsed_msec + session_time.elapsed();
Minutes = total_msecs;
Minutes /= 1000.00;
Minutes *= (1.0/60);
// PowerAgent Format "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)"
recordFileStream << Minutes
<< "," << Torq
<< "," << displaySpeed
<< "," << displayPower
<< "," << displayDistance
<< "," << displayCadence
<< "," << displayHeartRate
<< "," << (displayLap + displayWorkoutLap)
<< "," << Altitude
<< "," << "\n";
}
//----------------------------------------------------------------------
// WORKOUT MODE
//----------------------------------------------------------------------
void TrainTool::loadUpdate()
{
int curLap;
long load;
double gradient;
// the period between loadUpdate calls is not constant, and not exactly LOADRATE,
// therefore, use a QTime timer to measure the load period
load_msecs += load_period.restart();
if (deviceController == NULL) return;
if (status&RT_MODE_ERGO) {
load = ergFile->wattsAt(load_msecs, curLap);
displayWorkoutLap = curLap;
// we got to the end!
if (load == -100) {
Stop(DEVICE_OK);
} else {
displayLoad = load;
deviceController->setLoad(displayLoad);
main->notifySetNow(load_msecs);
}
} else {
gradient = ergFile->gradientAt(displayWorkoutDistance*1000, curLap);
displayWorkoutLap = curLap;
// we got to the end!
if (gradient == -100) {
Stop(DEVICE_OK);
} else {
displayGradient = gradient;
deviceController->setGradient(displayGradient);
main->notifySetNow(displayWorkoutDistance * 1000);
}
}
}
void TrainTool::FFwd()
{
if ((status&RT_RUNNING) == 0) return;
if (status&RT_MODE_ERGO) load_msecs += 10000; // jump forward 10 seconds
else displayWorkoutDistance += 1; // jump forward a kilometer in the workout
}
void TrainTool::Rewind()
{
if ((status&RT_RUNNING) == 0) return;
if (status&RT_MODE_ERGO) {
load_msecs -=10000; // jump back 10 seconds
if (load_msecs < 0) load_msecs = 0;
} else {
displayWorkoutDistance -=1; // jump back a kilometer
if (displayWorkoutDistance < 0) displayWorkoutDistance = 0;
}
}
// jump to next Lap marker (if there is one?)
void TrainTool::FFwdLap()
{
if ((status&RT_RUNNING) == 0) return;
double lapmarker;
if (status&RT_MODE_ERGO) {
lapmarker = ergFile->nextLap(load_msecs);
if (lapmarker != -1) load_msecs = lapmarker; // jump forward to lapmarker
} else {
lapmarker = ergFile->nextLap(displayWorkoutDistance*1000);
if (lapmarker != -1) displayWorkoutDistance = lapmarker/1000; // jump forward to lapmarker
}
}
// higher load/gradient
void TrainTool::Higher()
{
if ((status&RT_RUNNING) == 0) return;
if (deviceController == NULL) return;
if (main->currentErgFile()) {
// adjust the workout IF
intensitySlider->setValue(intensitySlider->value()+5);
} else {
if (status&RT_MODE_ERGO) displayLoad += 5;
else displayGradient += 0.1;
if (displayLoad >1500) displayLoad = 1500;
if (displayGradient >15) displayGradient = 15;
if (status&RT_MODE_ERGO) deviceController->setLoad(displayLoad);
else deviceController->setGradient(displayGradient);
}
}
// higher load/gradient
void TrainTool::Lower()
{
if ((status&RT_RUNNING) == 0) return;
if (deviceController == NULL) return;
if (main->currentErgFile()) {
// adjust the workout IF
intensitySlider->setValue(intensitySlider->value()-5);
} else {
if (status&RT_MODE_ERGO) displayLoad -= 5;
else displayGradient -= 0.1;
if (displayLoad <0) displayLoad = 0;
if (displayGradient <-10) displayGradient = -10;
if (status&RT_MODE_ERGO) deviceController->setLoad(displayLoad);
else deviceController->setGradient(displayGradient);
}
}
void TrainTool::setLabels()
{
if (main->currentErgFile()) {
intensitySlider->show();
if (main->currentErgFile()->format == CRS) {
stress->setText(QString("Elevation %1").arg(main->currentErgFile()->ELE, 0, 'f', 0));
intensity->setText(QString("Grade %1 %").arg(main->currentErgFile()->GRADE, 0, 'f', 1));
} else {
stress->setText(QString("TSS %1").arg(main->currentErgFile()->TSS, 0, 'f', 0));
intensity->setText(QString("IF %1").arg(main->currentErgFile()->IF, 0, 'f', 3));
}
} else {
intensitySlider->hide();
stress->setText("");
intensity->setText("");
}
}
void TrainTool::adjustIntensity()
{
if (!main->currentErgFile()) return; // no workout selected
// block signals temporarily
main->blockSignals(true);
// work through the ergFile from NOW
// adjusting back from last intensity setting
// and increasing to new intensity setting
double from = double(lastAppliedIntensity) / 100.00;
double to = double(intensitySlider->value()) / 100.00;
lastAppliedIntensity = intensitySlider->value();
long starttime = main->getNow();
bool insertedNow = main->getNow() ? false : true; // don't add if at start
//XXX what about gradient courses?
ErgFilePoint last;
for(int i = 0; i < main->currentErgFile()->Points.count(); i++) {
if (main->currentErgFile()->Points.at(i).x >= starttime) {
if (insertedNow == false) {
if (i) {
// add a point to adjust from
ErgFilePoint add;
add.x = main->getNow();
add.val = last.val / from * to;
// recalibrate altitude if gradient changing
if (main->currentErgFile()->format == CRS) add.y = last.y + ((add.x-last.x) * (add.val/100));
else add.y = add.val;
main->currentErgFile()->Points.insert(i, add);
last = add;
i++; // move on to next point (i.e. where we were!)
}
insertedNow = true;
}
ErgFilePoint *p = &main->currentErgFile()->Points[i];
// recalibrate altitude if in CRS mode
p->val = p->val / from * to;
if (main->currentErgFile()->format == CRS) {
if (i) p->y = last.y + ((p->x-last.x) * (last.val/100));
}
else p->y = p->val;
}
// remember last
last = main->currentErgFile()->Points.at(i);
}
// recalculate metrics
main->currentErgFile()->calculateMetrics();
setLabels();
// unblock signals now we are done
main->blockSignals(false);
// force replot
main->notifySetNow(main->getNow());
}