mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
Implementing a workout library that keeps track of media and workouts that can be used in train view. This first part implements; - library.xml to record search paths - search dialog to find media/workouts Part 2 and 3 will bring: - Sqllite libraryDB to store found details - Update traintool to use libraryDB and display icons, duration/distance, IF and TSS in list - import and drag-n-drop of new media/workouts
1625 lines
52 KiB
C++
1625 lines
52 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_LIBUSB
|
|
#include "FortiusController.h"
|
|
#endif
|
|
|
|
#ifdef GC_HAVE_VLC
|
|
// Media selection helper
|
|
#include "VideoWindow.h"
|
|
#endif
|
|
|
|
#ifdef Q_OS_MAC
|
|
#include "QtMacVideoWindow.h"
|
|
#include <CoreServices/CoreServices.h>
|
|
#endif
|
|
|
|
#include <math.h> // isnan and isinf
|
|
|
|
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);
|
|
|
|
// don't set the source for telemetry
|
|
bpmTelemetry = wattsTelemetry = kphTelemetry = rpmTelemetry = -1;
|
|
|
|
#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);
|
|
if (appsettings->value(this, TRAIN_MULTI, false).toBool() == true)
|
|
deviceTree->setSelectionMode(QAbstractItemView::MultiSelection);
|
|
else
|
|
deviceTree->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
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);
|
|
deviceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
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 *toolallbuttons=new QHBoxLayout; // on toolbar
|
|
toolallbuttons->setSpacing(0);
|
|
toolallbuttons->setContentsMargins(0,0,0,0);
|
|
|
|
QHBoxLayout *toolbuttons = new QHBoxLayout;
|
|
toolallbuttons->addLayout(toolbuttons);
|
|
toolbuttons->setSpacing(0);
|
|
toolbuttons->setContentsMargins(0,0,0,0);
|
|
toolbuttons->addStretch();
|
|
|
|
QIcon rewIcon(":images/oxygen/rewind.png");
|
|
QPushButton *rewind = new QPushButton(rewIcon, "", this);
|
|
rewind->setFocusPolicy(Qt::NoFocus);
|
|
rewind->setIconSize(QSize(20,20));
|
|
rewind->setAutoFillBackground(false);
|
|
rewind->setAutoDefault(false);
|
|
rewind->setFlat(true);
|
|
rewind->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
|
|
rewind->setAutoRepeat(true);
|
|
rewind->setAutoRepeatDelay(200);
|
|
toolbuttons->addWidget(rewind);
|
|
|
|
QIcon stopIcon(":images/oxygen/stop.png");
|
|
QPushButton *stop = new QPushButton(stopIcon, "", this);
|
|
stop->setFocusPolicy(Qt::NoFocus);
|
|
stop->setIconSize(QSize(20,20));
|
|
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(20,20));
|
|
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(20,20));
|
|
forward->setAutoFillBackground(false);
|
|
forward->setAutoDefault(false);
|
|
forward->setFlat(true);
|
|
forward->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
|
|
forward->setAutoRepeat(true);
|
|
forward->setAutoRepeatDelay(200);
|
|
toolbuttons->addWidget(forward);
|
|
|
|
QIcon lapIcon(":images/oxygen/lap.png");
|
|
QPushButton *lap = new QPushButton(lapIcon, "", this);
|
|
lap->setFocusPolicy(Qt::NoFocus);
|
|
lap->setIconSize(QSize(20,20));
|
|
lap->setAutoFillBackground(false);
|
|
lap->setAutoDefault(false);
|
|
lap->setFlat(true);
|
|
lap->setStyleSheet("background-color: rgba( 255, 255, 255, 0% ); border: 0px;");
|
|
toolbuttons->addWidget(lap);
|
|
toolbuttons->addStretch();
|
|
|
|
QHBoxLayout *slideLayout = new QHBoxLayout;
|
|
slideLayout->setSpacing(0);
|
|
slideLayout->setContentsMargins(0,0,0,0);
|
|
toolallbuttons->addLayout(slideLayout);
|
|
|
|
intensitySlider = new QSlider(Qt::Horizontal, this);
|
|
intensitySlider->setAutoFillBackground(false);
|
|
intensitySlider->setFocusPolicy(Qt::NoFocus);
|
|
intensitySlider->setMinimum(75);
|
|
intensitySlider->setMaximum(125);
|
|
intensitySlider->setValue(100);
|
|
slideLayout->addStretch();
|
|
slideLayout->addWidget(intensitySlider);
|
|
|
|
#ifdef Q_OS_MAC
|
|
QWindowsStyle *macstyler = new QWindowsStyle();
|
|
play->setStyle(macstyler);
|
|
stop->setStyle(macstyler);
|
|
rewind->setStyle(macstyler);
|
|
forward->setStyle(macstyler);
|
|
lap->setStyle(macstyler);
|
|
#endif
|
|
|
|
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);
|
|
|
|
slideLayout->addWidget(stress, Qt::AlignVCenter|Qt::AlignCenter);
|
|
slideLayout->addWidget(intensity, Qt::AlignVCenter|Qt::AlignCenter);
|
|
slideLayout->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(toolallbuttons);
|
|
|
|
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(lap, SIGNAL(clicked()), this, SLOT(newLap()));
|
|
connect(main, SIGNAL(newLap()), this, SLOT(resetLapTimer()));
|
|
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
|
|
|
|
#ifdef Q_OS_MAC
|
|
// get rid of annoying focus rectangle for sidebar components
|
|
mediaTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
workoutTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
deviceTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
|
|
// handle config changes
|
|
//connect(serverTree,SIGNAL(itemSelectionChanged()), this, SLOT(serverTreeWidgetSelectionChanged()));
|
|
connect(deviceTree,SIGNAL(itemSelectionChanged()), this, SLOT(deviceTreeWidgetSelectionChanged()));
|
|
connect(deviceTree,SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(deviceTreeMenuPopup(const QPoint &)));
|
|
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);
|
|
#if 0 //XXX Performance issues with this
|
|
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()));
|
|
#endif
|
|
|
|
// set home
|
|
main = parent;
|
|
streamController = NULL;
|
|
ergFile = NULL;
|
|
calibrating = false;
|
|
|
|
// metric or imperial?
|
|
useMetricUnits = main->useMetricUnits;
|
|
|
|
// 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 = slope = load = 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();
|
|
|
|
#ifndef Q_OS_MAC
|
|
toolbarButtons->hide();
|
|
#endif
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
if (appsettings->value(this, TRAIN_MULTI, false).toBool() == true)
|
|
deviceTree->setSelectionMode(QAbstractItemView::MultiSelection);
|
|
else
|
|
deviceTree->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
|
|
// wipe whatever is there
|
|
foreach(DeviceConfiguration x, Devices) delete x.controller;
|
|
Devices.clear();
|
|
|
|
DeviceConfigurations all;
|
|
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 {
|
|
|
|
// add to the selection tree
|
|
QTreeWidgetItem *device = new QTreeWidgetItem(allDevices, i);
|
|
device->setText(0, Devices.at(i).name);
|
|
|
|
// Create the controllers for each device
|
|
// we can call upon each of these when we need
|
|
// to interact with the device
|
|
if (Devices.at(i).type == DEV_ANTPLUS) {
|
|
Devices[i].controller = new ANTplusController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_CT) {
|
|
Devices[i].controller = new ComputrainerController(this, &Devices[i]);
|
|
#ifdef GC_HAVE_LIBUSB
|
|
} else if (Devices.at(i).type == DEV_FORTIUS) {
|
|
Devices[i].controller = new FortiusController(this, &Devices[i]);
|
|
#endif
|
|
} else if (Devices.at(i).type == DEV_NULL) {
|
|
Devices[i].controller = new NullController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_ANTLOCAL) {
|
|
Devices[i].controller = new ANTlocalController(this, &Devices[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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?
|
|
useMetricUnits = main->useMetricUnits;
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* 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()
|
|
{
|
|
bpmTelemetry = wattsTelemetry = kphTelemetry = rpmTelemetry = -1;
|
|
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();
|
|
}
|
|
|
|
QList<int>
|
|
TrainTool::devices()
|
|
{
|
|
QList<int> returning;
|
|
foreach(QTreeWidgetItem *item, deviceTree->selectedItems())
|
|
if (item->type() != HEAD_TYPE)
|
|
returning << item->type();
|
|
|
|
return returning;
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* 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;
|
|
|
|
// update every active device
|
|
foreach(int dev, devices()) Devices[dev].controller->setMode(RT_MODE_ERGO);
|
|
|
|
} else { // SLOPE MODE
|
|
status |= RT_MODE_SPIN;
|
|
status &= ~RT_MODE_ERGO;
|
|
|
|
// update every active device
|
|
foreach(int dev, devices()) Devices[dev].controller->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
|
|
*------------------------------------------------------------------------------*/
|
|
|
|
// 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) {
|
|
|
|
// UN PAUSE!
|
|
play->setIcon(pauseIcon);
|
|
|
|
session_time.start();
|
|
lap_time.start();
|
|
status &=~RT_PAUSED;
|
|
foreach(int dev, devices()) Devices[dev].controller->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);
|
|
|
|
mediaTree->setEnabled(false);
|
|
|
|
// tell the world
|
|
main->notifyUnPause();
|
|
|
|
} else if (status&RT_RUNNING) {
|
|
|
|
// Pause!
|
|
play->setIcon(playIcon);
|
|
|
|
session_elapsed_msec += session_time.elapsed();
|
|
lap_elapsed_msec += lap_time.elapsed();
|
|
foreach(int dev, devices()) Devices[dev].controller->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();
|
|
|
|
#if defined Q_OS_MAC || defined GC_HAVE_VLC
|
|
// enable media tree so we can change movie
|
|
mediaTree->setEnabled(true);
|
|
#endif
|
|
|
|
// tell the world
|
|
main->notifyPause();
|
|
|
|
} else {
|
|
|
|
// Stop users from selecting different devices
|
|
// media or workouts whilst a workout is in progress
|
|
// lets reset libusb to clear buffers
|
|
// and reset connection to device
|
|
// this appeara to help with ANT USB2 sticks
|
|
#ifdef GC_HAVE_LIBUSB
|
|
//usb_init(); //XXX lets not its a clusterfuck
|
|
#endif
|
|
|
|
// if we have selected multiple devices lets
|
|
// configure the series we collect from each one
|
|
if (deviceTree->selectedItems().count() > 1) {
|
|
MultiDeviceDialog *multisetup = new MultiDeviceDialog(main, this);
|
|
if (multisetup->exec() == false) {
|
|
return;
|
|
}
|
|
} else if (deviceTree->selectedItems().count() == 1) {
|
|
bpmTelemetry = wattsTelemetry = kphTelemetry = rpmTelemetry =
|
|
deviceTree->selectedItems().first()->type();
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
#if defined Q_OS_MAC || defined GC_HAVE_VLC
|
|
mediaTree->setEnabled(false);
|
|
#endif
|
|
workoutTree->setEnabled(false);
|
|
deviceTree->setEnabled(false);
|
|
|
|
// START!
|
|
play->setIcon(pauseIcon);
|
|
|
|
load = 0;
|
|
slope = 0.0;
|
|
|
|
if (mode == ERG || mode == MRC) {
|
|
status |= RT_MODE_ERGO;
|
|
status &= ~RT_MODE_SPIN;
|
|
foreach(int dev, devices()) Devices[dev].controller->setMode(RT_MODE_ERGO);
|
|
} else { // SLOPE MODE
|
|
status |= RT_MODE_SPIN;
|
|
status &= ~RT_MODE_ERGO;
|
|
foreach(int dev, devices()) Devices[dev].controller->setMode(RT_MODE_SPIN);
|
|
}
|
|
|
|
foreach(int dev, devices()) Devices[dev].controller->start();
|
|
|
|
// 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;
|
|
calibrating = false;
|
|
|
|
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
|
|
{
|
|
// we're not running fool!
|
|
if ((status&RT_RUNNING) == 0) return;
|
|
|
|
if (status&RT_PAUSED) {
|
|
|
|
session_time.start();
|
|
lap_time.start();
|
|
status &=~RT_PAUSED;
|
|
foreach(int dev, devices()) Devices[dev].controller->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);
|
|
|
|
#if defined Q_OS_MAC || defined GC_HAVE_VLC
|
|
mediaTree->setEnabled(false);
|
|
#endif
|
|
|
|
// tell the world
|
|
main->notifyUnPause();
|
|
|
|
} else {
|
|
|
|
session_elapsed_msec += session_time.elapsed();
|
|
lap_elapsed_msec += lap_time.elapsed();
|
|
foreach(int dev, devices()) Devices[dev].controller->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();
|
|
|
|
// enable media tree so we can change movie
|
|
mediaTree->setEnabled(true);
|
|
|
|
// tell the world
|
|
main->notifyPause();
|
|
}
|
|
}
|
|
|
|
void TrainTool::Stop(int deviceStatus) // when stop button is pressed
|
|
{
|
|
|
|
if ((status&RT_RUNNING) == 0) return;
|
|
|
|
status &= ~RT_RUNNING;
|
|
|
|
// Stop users from selecting different devices
|
|
// media or workouts whilst a workout is in progress
|
|
#if defined Q_OS_MAC || defined GC_HAVE_VLC
|
|
mediaTree->setEnabled(true);
|
|
#endif
|
|
workoutTree->setEnabled(true);
|
|
deviceTree->setEnabled(true);
|
|
|
|
// wipe connection
|
|
foreach(int dev, devices()) Devices[dev].controller->stop();
|
|
|
|
gui_timer->stop();
|
|
calibrating = false;
|
|
|
|
load = 0;
|
|
slope = 0.0;
|
|
|
|
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();
|
|
load = 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
|
|
rtData.mode = mode;
|
|
|
|
// 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 devices
|
|
if (status&RT_RUNNING) {
|
|
|
|
rtData.setLoad(load); // always set load..
|
|
rtData.setSlope(slope); // always set load..
|
|
|
|
// fetch the right data from each device...
|
|
foreach(int dev, devices()) {
|
|
|
|
RealtimeData local = rtData;
|
|
Devices[dev].controller->getRealtimeData(local);
|
|
|
|
// get spinscan data from a computrainer?
|
|
if (Devices[dev].type == DEV_CT) {
|
|
memcpy((uint8_t*)rtData.spinScan, (uint8_t*)local.spinScan, 24);
|
|
rtData.setLoad(local.getLoad()); // and get load in case it was adjusted
|
|
// to within defined limits
|
|
}
|
|
|
|
// what are we getting from this one?
|
|
if (dev == bpmTelemetry) rtData.setHr(local.getHr());
|
|
if (dev == rpmTelemetry) rtData.setCadence(local.getCadence());
|
|
if (dev == kphTelemetry) {
|
|
rtData.setSpeed(local.getSpeed());
|
|
rtData.setDistance(local.getDistance());
|
|
}
|
|
if (dev == wattsTelemetry) {
|
|
rtData.setWatts(local.getWatts());
|
|
rtData.setAltWatts(local.getAltWatts());
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if (!calibrating) { // freeze time whilst calibrating
|
|
total_msecs = session_elapsed_msec + session_time.elapsed();
|
|
lap_msecs = lap_elapsed_msec + lap_time.elapsed();
|
|
|
|
rtData.setMsecs(total_msecs);
|
|
rtData.setLapMsecs(lap_msecs);
|
|
|
|
long lapTimeRemaining;
|
|
if (ergFile) lapTimeRemaining = ergFile->nextLap(load_msecs) - load_msecs;
|
|
else lapTimeRemaining = 0;
|
|
|
|
if(lapTimeRemaining < 0)
|
|
{
|
|
if (ergFile) lapTimeRemaining = ergFile->Duration - load_msecs;
|
|
if(lapTimeRemaining < 0)
|
|
lapTimeRemaining = 0;
|
|
}
|
|
rtData.setLapMsecsRemaining(lapTimeRemaining);
|
|
}
|
|
|
|
// local stuff ...
|
|
displayPower = rtData.getWatts();
|
|
displayCadence = rtData.getCadence();
|
|
displayHeartRate = rtData.getHr();
|
|
displaySpeed = rtData.getSpeed();
|
|
load = rtData.getLoad();
|
|
|
|
// virtual speed
|
|
double crr = 0.004f; // typical for asphalt surfaces
|
|
double g = 9.81; // g constant 9.81 m/s
|
|
double weight = appsettings->cvalue(main->cyclist, GC_WEIGHT, 0.0).toDouble();
|
|
double m = weight ? weight + 8 : 83; // default to 75kg weight, plus 8kg bike
|
|
double sl = slope / 100; // 10% = 0.1
|
|
double ad = 1.226f; // default air density at sea level
|
|
double cdA = 0.5f; // typical
|
|
double pw = rtData.getWatts();
|
|
|
|
// algorithm supplied by Tom Compton
|
|
// from www.AnalyticCycling.com
|
|
// 3.6 * ... converts from meters per second to kph
|
|
double vs = 3.6f * (
|
|
(-2*pow(2,0.3333333333333333)*(crr*m + g*m*sl)) /
|
|
pow(54*pow(ad,2)*pow(cdA,2)*pw +
|
|
sqrt(2916*pow(ad,4)*pow(cdA,4)*pow(pw,2) +
|
|
864*pow(ad,3)*pow(cdA,3)*pow(crr*m +
|
|
g*m*sl,3)),0.3333333333333333) +
|
|
pow(54*pow(ad,2)*pow(cdA,2)*pw +
|
|
sqrt(2916*pow(ad,4)*pow(cdA,4)*pow(pw,2) +
|
|
864*pow(ad,3)*pow(cdA,3)*pow(crr*m +
|
|
g*m*sl,3)),0.3333333333333333)/
|
|
(3.*pow(2,0.3333333333333333)*ad*cdA));
|
|
|
|
// just in case...
|
|
if (isnan(vs) || isinf(vs)) vs = 0.00f;
|
|
|
|
rtData.setVirtualSpeed(vs);
|
|
|
|
|
|
// 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()
|
|
{
|
|
if ((status&RT_RUNNING) == RT_RUNNING) {
|
|
displayLap++;
|
|
|
|
pwrcount = 0;
|
|
cadcount = 0;
|
|
hrcount = 0;
|
|
spdcount = 0;
|
|
|
|
main->notifyNewLap();
|
|
}
|
|
}
|
|
|
|
void TrainTool::resetLapTimer()
|
|
{
|
|
lap_time.restart();
|
|
lap_elapsed_msec = 0;
|
|
}
|
|
|
|
// 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);
|
|
|
|
if (calibrating) return;
|
|
|
|
// 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;
|
|
|
|
// we hold our horses whilst calibration is taking place...
|
|
if (calibrating) return;
|
|
|
|
// 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 (status&RT_MODE_ERGO) {
|
|
load = ergFile->wattsAt(load_msecs, curLap);
|
|
|
|
if(displayWorkoutLap != curLap)
|
|
{
|
|
main->notifyNewLap();
|
|
}
|
|
displayWorkoutLap = curLap;
|
|
|
|
// we got to the end!
|
|
if (load == -100) {
|
|
Stop(DEVICE_OK);
|
|
} else {
|
|
foreach(int dev, devices()) Devices[dev].controller->setLoad(load);
|
|
main->notifySetNow(load_msecs);
|
|
}
|
|
} else {
|
|
slope = ergFile->gradientAt(displayWorkoutDistance*1000, curLap);
|
|
|
|
if(displayWorkoutLap != curLap)
|
|
{
|
|
main->notifyNewLap();
|
|
}
|
|
displayWorkoutLap = curLap;
|
|
|
|
// we got to the end!
|
|
if (slope == -100) {
|
|
Stop(DEVICE_OK);
|
|
} else {
|
|
foreach(int dev, devices()) Devices[dev].controller->setGradient(slope);
|
|
main->notifySetNow(displayWorkoutDistance * 1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrainTool::Calibrate()
|
|
{
|
|
static QProgressDialog *bar=NULL;
|
|
|
|
// toggle calibration
|
|
if (calibrating) {
|
|
bar->reset(); // will hide...
|
|
|
|
// restart gui etc
|
|
session_time.start();
|
|
lap_time.start();
|
|
load_period.restart();
|
|
if (status & RT_WORKOUT) load_timer->start(LOADRATE);
|
|
main->notifyUnPause(); // get video started again, amongst other things
|
|
|
|
// back to ergo/slope mode
|
|
if (status&RT_MODE_ERGO)
|
|
foreach(int dev, devices()) Devices[dev].controller->setMode(RT_MODE_ERGO);
|
|
else
|
|
foreach(int dev, devices()) Devices[dev].controller->setMode(RT_MODE_SPIN);
|
|
|
|
} else {
|
|
|
|
if (bar == NULL) {
|
|
QString title = tr("Calibrating...\nPress F3 on Controller when done.");
|
|
bar = new QProgressDialog(title, tr("Done"), 0, 0, this);
|
|
bar->setWindowModality(Qt::WindowModal);
|
|
bar->setMinimumDuration(0);
|
|
bar->setAutoClose(true); // hide when reset
|
|
connect(bar, SIGNAL(canceled()), this, SLOT(Calibrate()));
|
|
}
|
|
bar->show();
|
|
|
|
// pause gui/load but keep recording!
|
|
session_elapsed_msec += session_time.elapsed();
|
|
lap_elapsed_msec += lap_time.elapsed();
|
|
if (status & RT_WORKOUT) load_timer->stop();
|
|
load_msecs += load_period.restart();
|
|
main->notifyPause(); // get video started again, amongst other things
|
|
|
|
// only do this for computrainers!
|
|
foreach(int dev, devices())
|
|
if (Devices[dev].type == DEV_CT)
|
|
Devices[dev].controller->setMode(RT_MODE_CALIBRATE);
|
|
}
|
|
calibrating = !calibrating;
|
|
}
|
|
|
|
void TrainTool::FFwd()
|
|
{
|
|
if ((status&RT_RUNNING) == 0) return;
|
|
|
|
if (status&RT_MODE_ERGO) {
|
|
load_msecs += 10000; // jump forward 10 seconds
|
|
main->notifySeek(load_msecs);
|
|
}
|
|
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;
|
|
main->notifySeek(load_msecs);
|
|
} 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
|
|
main->notifySeek(load_msecs);
|
|
} 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 (main->currentErgFile()) {
|
|
// adjust the workout IF
|
|
intensitySlider->setValue(intensitySlider->value()+5);
|
|
|
|
} else {
|
|
if (status&RT_MODE_ERGO) load += 5;
|
|
else slope += 0.1;
|
|
|
|
if (load >1500) load = 1500;
|
|
if (slope >15) slope = 15;
|
|
|
|
if (status&RT_MODE_ERGO)
|
|
foreach(int dev, devices()) Devices[dev].controller->setLoad(load);
|
|
else
|
|
foreach(int dev, devices()) Devices[dev].controller->setGradient(slope);
|
|
}
|
|
}
|
|
|
|
// higher load/gradient
|
|
void TrainTool::Lower()
|
|
{
|
|
if ((status&RT_RUNNING) == 0) return;
|
|
|
|
if (main->currentErgFile()) {
|
|
// adjust the workout IF
|
|
intensitySlider->setValue(intensitySlider->value()-5);
|
|
|
|
} else {
|
|
|
|
if (status&RT_MODE_ERGO) load -= 5;
|
|
else slope -= 0.1;
|
|
|
|
if (load <0) load = 0;
|
|
if (slope <-10) slope = -10;
|
|
|
|
if (status&RT_MODE_ERGO)
|
|
foreach(int dev, devices()) Devices[dev].controller->setLoad(load);
|
|
else
|
|
foreach(int dev, devices()) Devices[dev].controller->setGradient(slope);
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
MultiDeviceDialog::MultiDeviceDialog(MainWindow *, TrainTool *traintool) : traintool(traintool)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
|
|
setWindowTitle(tr("Multiple Device Configuration"));
|
|
|
|
QVBoxLayout *main = new QVBoxLayout(this);
|
|
QFormLayout *mainLayout = new QFormLayout;
|
|
main->addLayout(mainLayout);
|
|
|
|
bpmSelect = new QComboBox(this);
|
|
mainLayout->addRow(new QLabel("Heartrate", this), bpmSelect);
|
|
|
|
wattsSelect = new QComboBox(this);
|
|
mainLayout->addRow(new QLabel("Power", this), wattsSelect);
|
|
|
|
rpmSelect = new QComboBox(this);
|
|
mainLayout->addRow(new QLabel("Cadence", this), rpmSelect);
|
|
|
|
kphSelect = new QComboBox(this);
|
|
mainLayout->addRow(new QLabel("Speed", this), kphSelect);
|
|
|
|
// update the device selections for the drop downs
|
|
foreach(QTreeWidgetItem *selected, traintool->deviceTree->selectedItems()) {
|
|
if (selected->type() == HEAD_TYPE) continue;
|
|
|
|
bpmSelect->addItem(selected->text(0), selected->type());
|
|
wattsSelect->addItem(selected->text(0), selected->type());
|
|
rpmSelect->addItem(selected->text(0), selected->type());
|
|
kphSelect->addItem(selected->text(0), selected->type());
|
|
}
|
|
|
|
bpmSelect->addItem("None", -1);
|
|
wattsSelect->addItem("None", -1);
|
|
rpmSelect->addItem("None", -1);
|
|
kphSelect->addItem("None", -1);
|
|
|
|
// set to the current values (if set)
|
|
if (traintool->bpmTelemetry != -1) {
|
|
int index = bpmSelect->findData(traintool->bpmTelemetry);
|
|
if (index >=0) bpmSelect->setCurrentIndex(index);
|
|
}
|
|
if (traintool->wattsTelemetry != -1) {
|
|
int index = wattsSelect->findData(traintool->wattsTelemetry);
|
|
if (index >=0) wattsSelect->setCurrentIndex(index);
|
|
}
|
|
if (traintool->rpmTelemetry != -1) {
|
|
int index = rpmSelect->findData(traintool->rpmTelemetry);
|
|
if (index >=0) rpmSelect->setCurrentIndex(index);
|
|
}
|
|
if (traintool->kphTelemetry != -1) {
|
|
int index = kphSelect->findData(traintool->kphTelemetry);
|
|
if (index >=0) kphSelect->setCurrentIndex(index);
|
|
}
|
|
|
|
QHBoxLayout *buttons = new QHBoxLayout;
|
|
buttons->addStretch();
|
|
main->addLayout(buttons);
|
|
|
|
cancelButton = new QPushButton("Cancel", this);
|
|
buttons->addWidget(cancelButton);
|
|
|
|
applyButton = new QPushButton("Apply", this);
|
|
buttons->addWidget(applyButton);
|
|
|
|
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
|
|
connect(applyButton, SIGNAL(clicked()), this, SLOT(applyClicked()));
|
|
}
|
|
|
|
void
|
|
MultiDeviceDialog::applyClicked()
|
|
{
|
|
traintool->rpmTelemetry = rpmSelect->itemData(rpmSelect->currentIndex()).toInt();
|
|
traintool->bpmTelemetry = bpmSelect->itemData(bpmSelect->currentIndex()).toInt();
|
|
traintool->wattsTelemetry = wattsSelect->itemData(wattsSelect->currentIndex()).toInt();
|
|
traintool->kphTelemetry = kphSelect->itemData(kphSelect->currentIndex()).toInt();
|
|
accept();
|
|
}
|
|
|
|
void
|
|
MultiDeviceDialog::cancelClicked()
|
|
{
|
|
reject();
|
|
}
|
|
|
|
void
|
|
TrainTool::deviceTreeMenuPopup(const QPoint &pos)
|
|
{
|
|
QMenu menu(deviceTree);
|
|
QAction *addDevice = new QAction(tr("Add Device"), deviceTree);
|
|
connect(addDevice, SIGNAL(triggered(void)), main, SLOT(addDevice()));
|
|
menu.addAction(addDevice);
|
|
|
|
if (deviceTree->selectedItems().size() == 1) {
|
|
QAction *delDevice = new QAction(tr("Delete Device"), deviceTree);
|
|
connect(delDevice, SIGNAL(triggered(void)), this, SLOT(deleteDevice()));
|
|
menu.addAction(delDevice);
|
|
}
|
|
|
|
menu.exec(deviceTree->mapToGlobal(pos));
|
|
}
|
|
|
|
void
|
|
TrainTool::deleteDevice()
|
|
{
|
|
// get the configuration
|
|
DeviceConfigurations all;
|
|
QList<DeviceConfiguration>list = all.getList();
|
|
|
|
// Delete the selected device
|
|
QTreeWidgetItem *selected = deviceTree->selectedItems().first();
|
|
int index = allDevices->indexOfChild(selected);
|
|
|
|
if (index < 0 || index > list.size()) return;
|
|
|
|
// make sure they really mean this!
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("Are you sure you want to delete this device?"));
|
|
msgBox.setInformativeText(list[index].name);
|
|
QPushButton *deleteButton = msgBox.addButton(tr("Delete"),QMessageBox::YesRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(QMessageBox::Cancel);
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.exec();
|
|
|
|
if(msgBox.clickedButton() != deleteButton) return;
|
|
|
|
// find this one and delete it
|
|
list.removeAt(index);
|
|
all.writeConfig(list);
|
|
|
|
// tell everyone
|
|
main->notifyConfigChanged();
|
|
}
|