mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
To enable workout preview in the more specific layout, provided the current perspective allows switching, to avoid unwanted swithing when using the Workout Editor. Reorder perspectives in default layout.
3287 lines
115 KiB
C++
3287 lines
115 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 "TrainSidebar.h"
|
|
#include "MainWindow.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "Settings.h"
|
|
#include "Colors.h"
|
|
#include "Units.h"
|
|
#include "DeviceTypes.h"
|
|
#include "DeviceConfiguration.h"
|
|
#include "RideImportWizard.h"
|
|
#include "HelpWhatsThis.h"
|
|
#include <QApplication>
|
|
#include <QtGui>
|
|
#include <QRegExp>
|
|
#include <QStyle>
|
|
#include <QStyleFactory>
|
|
#include <QScrollBar>
|
|
|
|
#include <QEvent>
|
|
#include <QInputEvent>
|
|
#include <QKeyEvent>
|
|
#include <QMutexLocker>
|
|
|
|
#include <QSound>
|
|
|
|
// Three current realtime device types supported are:
|
|
#include "RealtimeController.h"
|
|
#include "ComputrainerController.h"
|
|
#include "MonarkController.h"
|
|
#include "KettlerController.h"
|
|
#include "KettlerRacerController.h"
|
|
#include "ErgofitController.h"
|
|
#include "DaumController.h"
|
|
#include "ANTlocalController.h"
|
|
#include "NullController.h"
|
|
#ifdef QT_BLUETOOTH_LIB
|
|
#include "BT40Controller.h"
|
|
#endif
|
|
#ifdef GC_HAVE_LIBUSB
|
|
#include "FortiusController.h"
|
|
#include "ImagicController.h"
|
|
#endif
|
|
|
|
// Media selection helper
|
|
#if defined(GC_VIDEO_AV) || defined(GC_VIDEO_QUICKTIME)
|
|
#include "QtMacVideoWindow.h"
|
|
#else
|
|
#include "VideoWindow.h"
|
|
#endif
|
|
#ifdef Q_OS_MAC
|
|
#include <CoreServices/CoreServices.h>
|
|
#include <QStyle>
|
|
#include <QStyleFactory>
|
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
|
#endif
|
|
|
|
#ifdef WIN32
|
|
#include "windows.h"
|
|
#endif
|
|
|
|
#include "TrainDB.h"
|
|
#include "Library.h"
|
|
|
|
TrainSidebar::TrainSidebar(Context *context) : GcWindow(context), context(context),
|
|
bicycle(context)
|
|
{
|
|
QWidget *c = new QWidget;
|
|
//c->setContentsMargins(0,0,0,0); // bit of space is useful
|
|
QVBoxLayout *cl = new QVBoxLayout(c);
|
|
setControls(c);
|
|
autoConnect = false;
|
|
trainView=NULL;
|
|
useSimulatedSpeed = false;
|
|
|
|
cl->setSpacing(0);
|
|
cl->setContentsMargins(0,0,0,0);
|
|
|
|
// don't set the source for telemetry
|
|
lastAppliedIntensity = 100;
|
|
bpmTelemetry = wattsTelemetry = kphTelemetry = rpmTelemetry = -1;
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
videoModel = new QSqlTableModel(this, trainDB->connection());
|
|
videoModel->setTable("videos");
|
|
videoModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
|
|
videoModel->select();
|
|
while (videoModel->canFetchMore(QModelIndex())) videoModel->fetchMore(QModelIndex());
|
|
|
|
vsortModel = new QSortFilterProxyModel(this);
|
|
vsortModel->setSourceModel(videoModel);
|
|
vsortModel->setDynamicSortFilter(true);
|
|
vsortModel->sort(1, Qt::AscendingOrder); //filename order
|
|
|
|
mediaTree = new QTreeView;
|
|
mediaTree->setModel(vsortModel);
|
|
|
|
// hide unwanted columns and header
|
|
for(int i=0; i<mediaTree->header()->count(); i++)
|
|
mediaTree->setColumnHidden(i, true);
|
|
mediaTree->setColumnHidden(1, false); // show filename
|
|
mediaTree->header()->hide();
|
|
|
|
mediaTree->setSortingEnabled(false);
|
|
mediaTree->setAlternatingRowColors(false);
|
|
mediaTree->setEditTriggers(QAbstractItemView::NoEditTriggers); // read-only
|
|
mediaTree->expandAll();
|
|
mediaTree->header()->setCascadingSectionResizes(true); // easier to resize this way
|
|
mediaTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
mediaTree->header()->setStretchLastSection(true);
|
|
mediaTree->header()->setMinimumSectionSize(0);
|
|
mediaTree->header()->setFocusPolicy(Qt::NoFocus);
|
|
mediaTree->setFrameStyle(QFrame::NoFrame);
|
|
#ifdef Q_OS_MAC
|
|
mediaTree->header()->setSortIndicatorShown(false); // blue looks nasty
|
|
mediaTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
QStyle *cde = QStyleFactory::create(OS_STYLE);
|
|
mediaTree->verticalScrollBar()->setStyle(cde);
|
|
#endif
|
|
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncModel = new QSqlTableModel(this, trainDB->connection());
|
|
videosyncModel->setTable("videosyncs");
|
|
videosyncModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
|
|
videosyncModel->select();
|
|
while (videosyncModel->canFetchMore(QModelIndex())) videosyncModel->fetchMore(QModelIndex());
|
|
|
|
vssortModel = new QSortFilterProxyModel(this);
|
|
vssortModel->setSourceModel(videosyncModel);
|
|
vssortModel->setDynamicSortFilter(true);
|
|
vssortModel->sort(1, Qt::AscendingOrder); //filename order
|
|
|
|
videosyncTree = new QTreeView;
|
|
videosyncTree->setModel(vssortModel);
|
|
|
|
// hide unwanted columns and header
|
|
for(int i=0; i<videosyncTree->header()->count(); i++)
|
|
videosyncTree->setColumnHidden(i, true);
|
|
videosyncTree->setColumnHidden(1, false); // show filename
|
|
videosyncTree->header()->hide();
|
|
|
|
videosyncTree->setSortingEnabled(false);
|
|
videosyncTree->setAlternatingRowColors(false);
|
|
videosyncTree->setEditTriggers(QAbstractItemView::NoEditTriggers); // read-only
|
|
videosyncTree->expandAll();
|
|
videosyncTree->header()->setCascadingSectionResizes(true); // easier to resize this way
|
|
videosyncTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
videosyncTree->header()->setStretchLastSection(true);
|
|
videosyncTree->header()->setMinimumSectionSize(0);
|
|
videosyncTree->header()->setFocusPolicy(Qt::NoFocus);
|
|
videosyncTree->setFrameStyle(QFrame::NoFrame);
|
|
#ifdef Q_OS_MAC
|
|
videosyncTree->header()->setSortIndicatorShown(false); // blue looks nasty
|
|
videosyncTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
QStyle *cdevideosync = QStyleFactory::create(OS_STYLE);
|
|
videosyncTree->verticalScrollBar()->setStyle(cdevideosync);
|
|
#endif
|
|
|
|
#endif //GC_HAVE_VLC
|
|
|
|
#endif //GC_VIDEO_NONE
|
|
|
|
deviceTree = new DeviceTreeView;
|
|
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);
|
|
deviceTree->expandItem(deviceTree->invisibleRootItem());
|
|
deviceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
#ifdef Q_OS_WIN
|
|
QStyle *xde = QStyleFactory::create(OS_STYLE);
|
|
deviceTree->verticalScrollBar()->setStyle(xde);
|
|
#endif
|
|
|
|
workoutModel = new QSqlTableModel(this, trainDB->connection());
|
|
workoutModel->setTable("workouts");
|
|
workoutModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
|
|
workoutModel->select();
|
|
while (workoutModel->canFetchMore(QModelIndex())) workoutModel->fetchMore(QModelIndex());
|
|
|
|
sortModel = new QSortFilterProxyModel(this);
|
|
sortModel->setSourceModel(workoutModel);
|
|
sortModel->setDynamicSortFilter(true);
|
|
sortModel->sort(1, Qt::AscendingOrder); //filename order
|
|
|
|
workoutTree = new QTreeView;
|
|
workoutTree->setModel(sortModel);
|
|
|
|
// hide unwanted columns and header
|
|
for(int i=0; i<workoutTree->header()->count(); i++)
|
|
workoutTree->setColumnHidden(i, true);
|
|
workoutTree->setColumnHidden(1, false); // show filename
|
|
workoutTree->header()->hide();
|
|
workoutTree->setFrameStyle(QFrame::NoFrame);
|
|
workoutTree->setAlternatingRowColors(false);
|
|
workoutTree->setEditTriggers(QAbstractItemView::NoEditTriggers); // read-only
|
|
workoutTree->expandAll();
|
|
workoutTree->header()->setCascadingSectionResizes(true); // easier to resize this way
|
|
workoutTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
workoutTree->header()->setStretchLastSection(true);
|
|
workoutTree->header()->setMinimumSectionSize(0);
|
|
workoutTree->header()->setFocusPolicy(Qt::NoFocus);
|
|
workoutTree->setFrameStyle(QFrame::NoFrame);
|
|
#ifdef Q_OS_MAC
|
|
workoutTree->header()->setSortIndicatorShown(false); // blue looks nasty
|
|
workoutTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
#ifdef Q_OS_WIN
|
|
xde = QStyleFactory::create(OS_STYLE);
|
|
workoutTree->verticalScrollBar()->setStyle(xde);
|
|
#endif
|
|
|
|
connect(context, SIGNAL(newLap()), this, SLOT(resetLapTimer()));
|
|
connect(context, SIGNAL(viewChanged(int)), this, SLOT(viewChanged(int)));
|
|
|
|
// not used but kept in case re-instated in the future
|
|
recordSelector = new QCheckBox(this);
|
|
recordSelector->setText(tr("Save workout data"));
|
|
recordSelector->setChecked(true);
|
|
recordSelector->hide(); // we don't let users change this for now
|
|
|
|
trainSplitter = new GcSplitter(Qt::Vertical);
|
|
trainSplitter->setContentsMargins(0,0,0,0);
|
|
deviceItem = new GcSplitterItem(tr("Devices"), iconFromPNG(":images/sidebar/power.png"), this);
|
|
|
|
// devices splitter actions
|
|
QAction *moreDeviceAct = new QAction(iconFromPNG(":images/sidebar/extra.png"), tr("Menu"), this);
|
|
deviceItem->addAction(moreDeviceAct);
|
|
connect(moreDeviceAct, SIGNAL(triggered(void)), this, SLOT(devicePopup(void)));
|
|
|
|
workoutItem = new GcSplitterItem(tr("Workouts"), iconFromPNG(":images/sidebar/folder.png"), this);
|
|
QAction *moreWorkoutAct = new QAction(iconFromPNG(":images/sidebar/extra.png"), tr("Menu"), this);
|
|
workoutItem->addAction(moreWorkoutAct);
|
|
connect(moreWorkoutAct, SIGNAL(triggered(void)), this, SLOT(workoutPopup(void)));
|
|
|
|
deviceItem->addWidget(deviceTree);
|
|
HelpWhatsThis *helpDeviceTree = new HelpWhatsThis(deviceTree);
|
|
deviceTree->setWhatsThis(helpDeviceTree->getWhatsThisText(HelpWhatsThis::SideBarTrainView_Devices));
|
|
trainSplitter->addWidget(deviceItem);
|
|
|
|
workoutItem->addWidget(workoutTree);
|
|
HelpWhatsThis *helpWorkoutTree = new HelpWhatsThis(workoutTree);
|
|
workoutTree->setWhatsThis(helpWorkoutTree->getWhatsThisText(HelpWhatsThis::SideBarTrainView_Workouts));
|
|
trainSplitter->addWidget(workoutItem);
|
|
|
|
cl->addWidget(trainSplitter);
|
|
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaItem = new GcSplitterItem(tr("Media"), iconFromPNG(":images/sidebar/movie.png"), this);
|
|
QAction *moreMediaAct = new QAction(iconFromPNG(":images/sidebar/extra.png"), tr("Menu"), this);
|
|
mediaItem->addAction(moreMediaAct);
|
|
connect(moreMediaAct, SIGNAL(triggered(void)), this, SLOT(mediaPopup(void)));
|
|
mediaItem->addWidget(mediaTree);
|
|
HelpWhatsThis *helpMediaTree = new HelpWhatsThis(mediaTree);
|
|
mediaTree->setWhatsThis(helpMediaTree->getWhatsThisText(HelpWhatsThis::SideBarTrainView_Media));
|
|
trainSplitter->addWidget(mediaItem);
|
|
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncItem = new GcSplitterItem(tr("VideoSync"), iconFromPNG(":images/sidebar/sync.png"), this);
|
|
QAction *moreVideoSyncAct = new QAction(iconFromPNG(":images/sidebar/extra.png"), tr("Menu"), this);
|
|
videosyncItem->addAction(moreVideoSyncAct);
|
|
connect(moreVideoSyncAct, SIGNAL(triggered(void)), this, SLOT(videosyncPopup(void)));
|
|
videosyncItem->addWidget(videosyncTree);
|
|
HelpWhatsThis *helpVideosyncTree = new HelpWhatsThis(videosyncTree);
|
|
videosyncTree->setWhatsThis(helpVideosyncTree->getWhatsThisText(HelpWhatsThis::SideBarTrainView_VideoSync));
|
|
trainSplitter->addWidget(videosyncItem);
|
|
#endif //GC_HAVE_VLC
|
|
#endif //GC_VIDEO_NONE
|
|
trainSplitter->prepare(context->athlete->cyclist, "train");
|
|
|
|
#ifdef Q_OS_MAC
|
|
// get rid of annoying focus rectangle for sidebar components
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#if defined GC_HAVE_VLC // not on OSX at present
|
|
videosyncTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
#endif
|
|
workoutTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
deviceTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
|
|
// handle config changes
|
|
connect(deviceTree,SIGNAL(itemSelectionChanged()), this, SLOT(deviceTreeWidgetSelectionChanged()));
|
|
connect(deviceTree,SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(deviceTreeMenuPopup(const QPoint &)));
|
|
connect(deviceTree,SIGNAL(itemMoved(int, int)), this, SLOT(moveDevices(int, int)));
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
connect(mediaTree->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
|
|
this, SLOT(mediaTreeWidgetSelectionChanged()));
|
|
connect(context, SIGNAL(selectMedia(QString)), this, SLOT(selectVideo(QString)));
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
connect(videosyncTree->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
|
|
this, SLOT(videosyncTreeWidgetSelectionChanged()));
|
|
connect(context, SIGNAL(selectVideoSync(QString)), this, SLOT(selectVideoSync(QString)));
|
|
#endif //GC_HAVE_VLC
|
|
#endif //GC_VIDEO_NONE
|
|
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
connect(context, SIGNAL(selectWorkout(QString)), this, SLOT(selectWorkout(QString)));
|
|
connect(trainDB, SIGNAL(dataChanged()), this, SLOT(refresh()));
|
|
|
|
connect(workoutTree->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(workoutTreeWidgetSelectionChanged()));
|
|
|
|
videosyncFile = NULL;
|
|
calibrating = false;
|
|
|
|
// Remote control support
|
|
remote = new RemoteControl;
|
|
|
|
// now the GUI is setup lets sort our control variables
|
|
gui_timer = new QTimer(this);
|
|
disk_timer = new QTimer(this);
|
|
load_timer = new QTimer(this);
|
|
start_timer = new QTimer(this);
|
|
start_timer->setSingleShot(true);
|
|
|
|
session_time = QTime();
|
|
session_elapsed_msec = 0;
|
|
lap_time = QTime();
|
|
lap_elapsed_msec = 0;
|
|
secs_to_start = 0;
|
|
|
|
rrFile = recordFile = vo2File = NULL;
|
|
lastRecordSecs = 0;
|
|
status = 0;
|
|
setStatusFlags(RT_MODE_ERGO); // ergo mode by default
|
|
mode = ERG;
|
|
pendingConfigChange = false;
|
|
|
|
displayWorkoutLap = 0;
|
|
pwrcount = 0;
|
|
cadcount = 0;
|
|
hrcount = 0;
|
|
spdcount = 0;
|
|
lodcount = 0;
|
|
wbalr = wbal = 0;
|
|
load_msecs = total_msecs = lap_msecs = 0;
|
|
displayWorkoutDistance = displayDistance = displayPower = displayHeartRate =
|
|
displaySpeed = displayCadence = slope = load = 0;
|
|
displaySMO2 = displayTHB = displayO2HB = displayHHB = 0;
|
|
displayLRBalance = displayLTE = displayRTE = displayLPS = displayRPS = 0;
|
|
displayLatitude = displayLongitude = displayAltitude = 0.0;
|
|
|
|
connect(gui_timer, SIGNAL(timeout()), this, SLOT(guiUpdate()));
|
|
connect(disk_timer, SIGNAL(timeout()), this, SLOT(diskUpdate()));
|
|
connect(load_timer, SIGNAL(timeout()), this, SLOT(loadUpdate()));
|
|
connect(start_timer, SIGNAL(timeout()), this, SLOT(Start()));
|
|
|
|
configChanged(CONFIG_APPEARANCE | CONFIG_DEVICES | CONFIG_ZONES); // will reset the workout tree
|
|
setLabels();
|
|
|
|
// capture keyboard events so we can control during
|
|
// a workout using basic keyboard controls
|
|
context->mainWindow->installEventFilter(this);
|
|
|
|
#ifndef Q_OS_MAC
|
|
//toolbarButtons->hide();
|
|
#endif
|
|
|
|
}
|
|
|
|
void
|
|
TrainSidebar::refresh()
|
|
{
|
|
int row;
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
// remember selection
|
|
row = mediaTree->currentIndex().row();
|
|
QString videoPath = mediaTree->model()->data(mediaTree->model()->index(row,0)).toString();
|
|
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
// refresh data
|
|
videoModel->select();
|
|
while (videoModel->canFetchMore(QModelIndex())) videoModel->fetchMore(QModelIndex());
|
|
#endif
|
|
|
|
// restore selection
|
|
selectVideo(videoPath);
|
|
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
// remember selection
|
|
row = videosyncTree->currentIndex().row();
|
|
QString videosyncPath = videosyncTree->model()->data(videosyncTree->model()->index(row,0)).toString();
|
|
|
|
// refresh data
|
|
videosyncModel->select();
|
|
while (videosyncModel->canFetchMore(QModelIndex())) videosyncModel->fetchMore(QModelIndex());
|
|
|
|
// restore selection
|
|
selectVideoSync(videosyncPath);
|
|
#endif // GC_HAVE_VLC
|
|
|
|
#endif // GC_VIDEO_NONE
|
|
|
|
row = workoutTree->currentIndex().row();
|
|
QString workoutPath = workoutTree->model()->data(workoutTree->model()->index(row,0)).toString();
|
|
|
|
workoutModel->select();
|
|
while (workoutModel->canFetchMore(QModelIndex())) workoutModel->fetchMore(QModelIndex());
|
|
|
|
// restore selection
|
|
selectWorkout(workoutPath);
|
|
}
|
|
|
|
void
|
|
TrainSidebar::workoutPopup()
|
|
{
|
|
// OK - we are working with a specific event..
|
|
QMenu menu(workoutTree);
|
|
QAction *import = new QAction(tr("Import Workout from File"), workoutTree);
|
|
QAction *download = new QAction(tr("Get Workouts from ErgDB"), workoutTree);
|
|
QAction *dlTodaysPlan = new QAction(tr("Get Workouts from Today's Plan"), workoutTree);
|
|
QAction *wizard = new QAction(tr("Create Workout via Wizard"), workoutTree);
|
|
QAction *scan = new QAction(tr("Scan for Workouts"), workoutTree);
|
|
|
|
menu.addAction(import);
|
|
menu.addAction(download);
|
|
menu.addAction(dlTodaysPlan);
|
|
menu.addAction(wizard);
|
|
menu.addAction(scan);
|
|
|
|
|
|
// we can delete too
|
|
int delNumber = 0;
|
|
|
|
QModelIndexList list = workoutTree->selectionModel()->selectedRows();
|
|
foreach (QModelIndex index, list)
|
|
{
|
|
QModelIndex target = sortModel->mapToSource(index);
|
|
|
|
QString filename = workoutModel->data(workoutModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
if (QFileInfo(filename).exists()) {
|
|
delNumber++;
|
|
}
|
|
}
|
|
|
|
if (delNumber > 0) {
|
|
QAction *del = new QAction(tr("Delete selected Workout"), workoutTree);
|
|
|
|
if (delNumber > 1) {
|
|
del->setText(QString(tr("Delete %1 selected Workouts")).arg(delNumber));
|
|
}
|
|
menu.addAction(del);
|
|
connect(del, SIGNAL(triggered(void)), this, SLOT(deleteWorkouts(void)));
|
|
}
|
|
|
|
// connect menu to functions
|
|
connect(import, SIGNAL(triggered(void)), context->mainWindow, SLOT(importWorkout(void)));
|
|
connect(wizard, SIGNAL(triggered(void)), context->mainWindow, SLOT(showWorkoutWizard(void)));
|
|
connect(download, SIGNAL(triggered(void)), context->mainWindow, SLOT(downloadErgDB(void)));
|
|
connect(dlTodaysPlan, SIGNAL(triggered(void)), context->mainWindow, SLOT(downloadTodaysPlanWorkouts(void)));
|
|
connect(scan, SIGNAL(triggered(void)), context->mainWindow, SLOT(manageLibrary(void)));
|
|
|
|
// execute the menu
|
|
menu.exec(trainSplitter->mapToGlobal(QPoint(workoutItem->pos().x()+workoutItem->width()-20,
|
|
workoutItem->pos().y())));
|
|
}
|
|
|
|
bool
|
|
TrainSidebar::eventFilter(QObject *, QEvent *event)
|
|
{
|
|
// do not allow to close the Window when active
|
|
if (event->type() == QEvent::Close) {
|
|
if (status & RT_RUNNING) {
|
|
QMessageBox::warning(this, tr("Train mode active"), tr("Please stop the train mode before closing the window or application."));
|
|
event->ignore();
|
|
return true;
|
|
} else if (gui_timer->isActive()) {
|
|
// we just disconnecting before allowing the window to close
|
|
Disconnect();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// only when we are recording !
|
|
if (status & RT_RECORDING) {
|
|
if (event->type() == QEvent::KeyPress) {
|
|
|
|
// we care about cmd / ctrl
|
|
Qt::KeyboardModifiers kmod = static_cast<QInputEvent*>(event)->modifiers();
|
|
bool ctrl = (kmod & Qt::ControlModifier) != 0;
|
|
Q_UNUSED(ctrl);
|
|
|
|
// what was pressed
|
|
int key =static_cast<QKeyEvent*>(event)->key();
|
|
|
|
//
|
|
// We can process the keyboard event here
|
|
// and call any training method
|
|
//
|
|
// KEY FUNCTION
|
|
// Start() - will pause/unpause if running
|
|
// Stop() - will end workout
|
|
// Pause() - pause only
|
|
// UnPause() - unpause
|
|
// FFwd() - nip forward
|
|
// Rewind() - nip back
|
|
// newLap() - new lap marker
|
|
//
|
|
switch(key) {
|
|
|
|
case Qt::Key_Space:
|
|
Start();
|
|
break;
|
|
|
|
case Qt::Key_Escape:
|
|
Stop();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
|
|
}
|
|
return true; // we listen to 'em all
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
TrainSidebar::mediaPopup()
|
|
{
|
|
// OK - we are working with a specific event..
|
|
QMenu menu(mediaTree);
|
|
QAction *import = new QAction(tr("Import Video from File"), mediaTree);
|
|
QAction *scan = new QAction(tr("Scan for Videos"), mediaTree);
|
|
|
|
menu.addAction(import);
|
|
menu.addAction(scan);
|
|
|
|
// connect menu to functions
|
|
connect(import, SIGNAL(triggered(void)), context->mainWindow, SLOT(importWorkout(void)));
|
|
connect(scan, SIGNAL(triggered(void)), context->mainWindow, SLOT(manageLibrary(void)));
|
|
|
|
QModelIndex current = mediaTree->currentIndex();
|
|
QModelIndex target = vsortModel->mapToSource(current);
|
|
QString filename = videoModel->data(videoModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
if (QFileInfo(filename).exists()) {
|
|
QAction *del = new QAction(tr("Remove reference to selected video"), workoutTree);
|
|
menu.addAction(del);
|
|
connect(del, SIGNAL(triggered(void)), this, SLOT(deleteVideos(void)));
|
|
}
|
|
|
|
// execute the menu
|
|
menu.exec(trainSplitter->mapToGlobal(QPoint(mediaItem->pos().x()+mediaItem->width()-20,
|
|
mediaItem->pos().y())));
|
|
}
|
|
|
|
void
|
|
TrainSidebar::videosyncPopup()
|
|
{
|
|
// OK - we are working with a specific event..
|
|
QMenu menu(videosyncTree);
|
|
QAction *import = new QAction(tr("Import VideoSync from File"), videosyncTree);
|
|
QAction *scan = new QAction(tr("Scan for VideoSyncs"), videosyncTree);
|
|
|
|
menu.addAction(import);
|
|
menu.addAction(scan);
|
|
|
|
// connect menu to functions
|
|
connect(import, SIGNAL(triggered(void)), context->mainWindow, SLOT(importWorkout(void)));
|
|
connect(scan, SIGNAL(triggered(void)), context->mainWindow, SLOT(manageLibrary(void)));
|
|
|
|
QModelIndex current = videosyncTree->currentIndex();
|
|
QModelIndex target = vssortModel->mapToSource(current);
|
|
QString filename = videosyncModel->data(videosyncModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
if (QFileInfo(filename).exists()) {
|
|
QAction *del = new QAction(tr("Delete selected VideoSync"), workoutTree);
|
|
menu.addAction(del);
|
|
connect(del, SIGNAL(triggered(void)), this, SLOT(deleteVideoSyncs(void)));
|
|
}
|
|
|
|
// execute the menu
|
|
menu.exec(trainSplitter->mapToGlobal(QPoint(videosyncItem->pos().x()+videosyncItem->width()-20,
|
|
videosyncItem->pos().y())));
|
|
}
|
|
|
|
void
|
|
TrainSidebar::configChanged(qint32)
|
|
{
|
|
// do not refresh if workout running, defer to end of workout
|
|
if (status&RT_RUNNING) {
|
|
pendingConfigChange = true;
|
|
return;
|
|
}
|
|
|
|
// auto connect is off by default
|
|
autoConnect = appsettings->value(this, TRAIN_AUTOCONNECT, false).toBool();
|
|
|
|
// lap sounds are off by default
|
|
lapAudioEnabled = appsettings->value(this, TRAIN_LAPALERT, false).toBool();
|
|
|
|
useSimulatedSpeed = appsettings->value(this, TRAIN_USESIMULATEDSPEED, false).toBool();
|
|
|
|
setProperty("color", GColor(CTRAINPLOTBACKGROUND));
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setStyleSheet(GCColor::stylesheet(true));
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setStyleSheet(GCColor::stylesheet(true));
|
|
#endif
|
|
#endif
|
|
workoutTree->setStyleSheet(GCColor::stylesheet(true));
|
|
deviceTree->setStyleSheet(GCColor::stylesheet(true));
|
|
|
|
// DEVICES
|
|
|
|
// Disconnect any running telemetry before manipulating device list
|
|
Disconnect();
|
|
|
|
// zap whats there
|
|
QList<QTreeWidgetItem *> devices = deviceTree->invisibleRootItem()->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++) {
|
|
|
|
// add to the selection tree
|
|
QTreeWidgetItem *device = new QTreeWidgetItem(deviceTree->invisibleRootItem(), 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_CT) {
|
|
Devices[i].controller = new ComputrainerController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_MONARK) {
|
|
Devices[i].controller = new MonarkController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_KETTLER) {
|
|
Devices[i].controller = new KettlerController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_KETTLER_RACER) {
|
|
Devices[i].controller = new KettlerRacerController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_ERGOFIT) {
|
|
Devices[i].controller = new ErgofitController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_DAUM) {
|
|
Devices[i].controller = new DaumController(this, &Devices[i]);
|
|
#ifdef GC_HAVE_LIBUSB
|
|
} else if (Devices.at(i).type == DEV_FORTIUS) {
|
|
Devices[i].controller = new FortiusController(this, &Devices[i]);
|
|
} else if (Devices.at(i).type == DEV_IMAGIC) {
|
|
Devices[i].controller = new ImagicController(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]);
|
|
// connect slot for receiving remote control commands
|
|
connect(Devices[i].controller, SIGNAL(remoteControl(uint16_t)), this, SLOT(remoteControl(uint16_t)));
|
|
// connect slot for receiving rrData
|
|
connect(Devices[i].controller, SIGNAL(rrData(uint16_t,uint8_t,uint8_t)), this, SLOT(rrData(uint16_t,uint8_t,uint8_t)));
|
|
#ifdef QT_BLUETOOTH_LIB
|
|
} else if (Devices.at(i).type == DEV_BT40) {
|
|
Devices[i].controller = new BT40Controller(this, &Devices[i]);
|
|
connect(Devices[i].controller, SIGNAL(vo2Data(double,double,double,double,double,double)),
|
|
this, SLOT(vo2Data(double,double,double,double,double,double)));
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// select the first device
|
|
if (Devices.count()) {
|
|
deviceTree->setCurrentItem(deviceTree->invisibleRootItem()->child(0));
|
|
}
|
|
// And select default workout to Ergo
|
|
QModelIndex firstWorkout = sortModel->index(0, 0, QModelIndex());
|
|
workoutTree->setCurrentIndex(firstWorkout);
|
|
|
|
// Re-read ANT remote control command mappings
|
|
remote->readConfig();
|
|
|
|
// Athlete
|
|
FTP=285; // default to 285 if zones are not set
|
|
WPRIME = 20000;
|
|
|
|
int range = context->athlete->zones("Bike") ? context->athlete->zones("Bike")->whichRange(QDate::currentDate()) : -1;
|
|
if (range != -1) {
|
|
FTP = context->athlete->zones("Bike")->getCP(range);
|
|
WPRIME = context->athlete->zones("Bike")->getWprime(range);
|
|
}
|
|
|
|
// Reinit Bicycle
|
|
bicycle.Reset(context);
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Device Selected
|
|
*--------------------------------------------------------------------*/
|
|
void
|
|
TrainSidebar::deviceTreeWidgetSelectionChanged()
|
|
{
|
|
//qDebug() << "TrainSidebar::deviceTreeWidgetSelectionChanged()";
|
|
|
|
bpmTelemetry = wattsTelemetry = kphTelemetry = rpmTelemetry = -1;
|
|
deviceSelected();
|
|
|
|
if (status&RT_CONNECTED) Disconnect(); // disconnect first
|
|
if (autoConnect) Connect(); // re-connect
|
|
}
|
|
|
|
int
|
|
TrainSidebar::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>
|
|
TrainSidebar::devices()
|
|
{
|
|
QList<int> returning;
|
|
foreach(QTreeWidgetItem *item, deviceTree->selectedItems())
|
|
if (item->type() != HEAD_TYPE)
|
|
returning << item->type();
|
|
|
|
return returning;
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Workout Selected
|
|
*--------------------------------------------------------------------*/
|
|
void
|
|
TrainSidebar::workoutTreeWidgetSelectionChanged()
|
|
{
|
|
QModelIndex current = workoutTree->currentIndex();
|
|
QModelIndex target = sortModel->mapToSource(current);
|
|
QString filename = workoutModel->data(workoutModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
|
|
// wipe away the current selected workout once we've told everyone
|
|
// since they might be editing it and want to save changes first (!!)
|
|
ErgFile *prior = const_cast<ErgFile*>(ergFileQueryAdapter.getErgFile());
|
|
workoutfile = filename;
|
|
|
|
if (filename == "") {
|
|
|
|
// an empty workout
|
|
context->notifyErgFileSelected(NULL);
|
|
ergFileQueryAdapter.setErgFile(NULL);
|
|
|
|
// clean last
|
|
if (prior) delete prior;
|
|
|
|
return;
|
|
}
|
|
|
|
// is it the auto mode?
|
|
int index = target.row();
|
|
if (index == 0) {
|
|
// ergo mode
|
|
context->notifyErgFileSelected(NULL);
|
|
ergFileQueryAdapter.setErgFile(NULL);
|
|
mode = ERG;
|
|
setLabels();
|
|
clearStatusFlags(RT_WORKOUT);
|
|
//ergPlot->setVisible(false);
|
|
} else if (index == 1) {
|
|
// slope mode
|
|
context->notifyErgFileSelected(NULL);
|
|
ergFileQueryAdapter.setErgFile(NULL);
|
|
mode = CRS;
|
|
setLabels();
|
|
clearStatusFlags(RT_WORKOUT);
|
|
//ergPlot->setVisible(false);
|
|
} else {
|
|
// workout mode
|
|
ErgFile* ergFile = new ErgFile(filename, mode, context);
|
|
mode = ergFile->mode;
|
|
|
|
if (ergFile->isValid()) {
|
|
|
|
setStatusFlags(RT_WORKOUT);
|
|
|
|
// success! we have a load file
|
|
// setup the course profile in the
|
|
// display!
|
|
context->notifyErgFileSelected(ergFile);
|
|
ergFileQueryAdapter.setErgFile(ergFile);
|
|
adjustIntensity(100);
|
|
setLabels();
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
// Try to select matching media and videosync files
|
|
QString workoutName = QFileInfo(filename).baseName();
|
|
mediaTree->setFocus();
|
|
mediaTree->keyboardSearch(workoutName);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setFocus();
|
|
videosyncTree->keyboardSearch(workoutName);
|
|
#endif
|
|
workoutTree->setFocus();
|
|
#endif
|
|
} else {
|
|
|
|
// couldn't parse fall back to ERG mode
|
|
delete ergFile;
|
|
ergFile = NULL;
|
|
context->notifyErgFileSelected(NULL);
|
|
ergFileQueryAdapter.setErgFile(NULL);
|
|
removeInvalidWorkout();
|
|
mode = ERG;
|
|
clearStatusFlags(RT_WORKOUT);
|
|
setLabels();
|
|
}
|
|
}
|
|
|
|
// set the device to the right mode
|
|
if (mode == ERG || mode == MRC) {
|
|
setStatusFlags(RT_MODE_ERGO);
|
|
clearStatusFlags(RT_MODE_SPIN);
|
|
|
|
// update every active device
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setMode(RT_MODE_ERGO);
|
|
|
|
} else { // SLOPE MODE
|
|
setStatusFlags(RT_MODE_SPIN);
|
|
clearStatusFlags(RT_MODE_ERGO);
|
|
|
|
// update every active device
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setMode(RT_MODE_SPIN);
|
|
}
|
|
|
|
maintainLapDistanceState();
|
|
|
|
ergFileQueryAdapter.resetQueryState();
|
|
|
|
// clean last
|
|
if (prior) delete prior;
|
|
|
|
// lets SWITCH PERSPECTIVE for the selected file, but only
|
|
// if everything has been initialised properly (aka lazy load)
|
|
if (trainView && trainView->page()) {
|
|
|
|
Perspective::switchenum want=Perspective::None;
|
|
if (mediafile != "") want=Perspective::Video; // if media file selected
|
|
else want = (mode == ERG || mode == MRC) ? Perspective::Erg : Perspective::Slope; // mode always known
|
|
if (want == Perspective::Slope && ergFileQueryAdapter.hasGPS()) want=Perspective::Map; // Map without Video
|
|
|
|
// if the current perspective allows automatic switching,
|
|
// we want a view type and the current page isn't what
|
|
// we want then lets go find one to switch to and switch
|
|
// to the first one that matches
|
|
if (trainView->page()->trainSwitch() != Perspective::None && want != Perspective::None && trainView->page()->trainSwitch() != want) {
|
|
|
|
for(int i=0; i<trainView->perspectives_.count(); i++) {
|
|
if (trainView->perspectives_[i]->trainSwitch() == want) {
|
|
context->mainWindow->switchPerspective(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Calculate Lap State with respect to distance.
|
|
* Doesn't apply to time-based workouts.
|
|
* Calculates the current lap distance.
|
|
* Calculates the lap distance remaining in the current lap.
|
|
* Route span is used if workout has no laps.
|
|
*/
|
|
void
|
|
TrainSidebar::maintainLapDistanceState()
|
|
{
|
|
// lapDistance, lapDistanceRemaining are only relevant when
|
|
// running in slope mode.
|
|
if (!ergFileQueryAdapter.getErgFile() || !(status&RT_MODE_SLOPE)) {
|
|
displayLapDistance = 0;
|
|
displayLapDistanceRemaining = -1;
|
|
return;
|
|
}
|
|
|
|
double currentpositionM = displayWorkoutDistance * 1000.;
|
|
double lapmarkerM = ergFileQueryAdapter.currentLap(currentpositionM);
|
|
|
|
// If no current lap then handle route as lap.
|
|
if (lapmarkerM < 0.) {
|
|
displayLapDistance = displayWorkoutDistance;
|
|
displayLapDistanceRemaining = (ergFileQueryAdapter.Duration() / 1000.) - displayWorkoutDistance;
|
|
return;
|
|
}
|
|
|
|
displayLapDistance = (currentpositionM - lapmarkerM) / 1000.;
|
|
double nextlapmarkerM = ergFileQueryAdapter.nextLap(currentpositionM);
|
|
|
|
// If no next lap then use distance to end of route.
|
|
if (nextlapmarkerM < 0.) {
|
|
displayLapDistanceRemaining = (ergFileQueryAdapter.Duration() / 1000.) - displayWorkoutDistance;
|
|
return;
|
|
}
|
|
|
|
displayLapDistanceRemaining = (nextlapmarkerM - currentpositionM) / 1000.;
|
|
}
|
|
|
|
QStringList
|
|
TrainSidebar::listWorkoutFiles(const QDir &dir) const
|
|
{
|
|
QStringList filters;
|
|
filters << "*.erg";
|
|
filters << "*.mrc";
|
|
filters << "*.crs";
|
|
filters << "*.pgmf";
|
|
|
|
return dir.entryList(filters, QDir::Files, QDir::Name);
|
|
}
|
|
|
|
void
|
|
TrainSidebar::deleteVideos()
|
|
{
|
|
QModelIndex current = mediaTree->currentIndex();
|
|
QModelIndex target = vsortModel->mapToSource(current);
|
|
QString filename = videoModel->data(videoModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
|
|
if (QFileInfo(filename).exists()) {
|
|
// are you sure?
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("Are you sure you want to remove the reference to this video?"));
|
|
msgBox.setInformativeText(filename);
|
|
QPushButton *deleteButton = msgBox.addButton(tr("Remove"),QMessageBox::YesRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(QMessageBox::Cancel);
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.exec();
|
|
|
|
if(msgBox.clickedButton() != deleteButton) return;
|
|
|
|
// delete from disk
|
|
//XXX QFile(filename).remove(); // lets not for now..
|
|
|
|
// remove any reference (from drag and drop)
|
|
Library *l = Library::findLibrary("Media Library");
|
|
if (l) l->removeRef(context, filename);
|
|
|
|
// delete from DB
|
|
trainDB->startLUW();
|
|
trainDB->deleteVideo(filename);
|
|
trainDB->endLUW();
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
TrainSidebar::deleteVideoSyncs()
|
|
{
|
|
QModelIndex current = videosyncTree->currentIndex();
|
|
QModelIndex target = vssortModel->mapToSource(current);
|
|
QString filename = videosyncModel->data(videosyncModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
|
|
if (QFileInfo(filename).exists()) {
|
|
// are you sure?
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("Are you sure you want to delete this VideoSync?"));
|
|
msgBox.setInformativeText(filename);
|
|
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;
|
|
|
|
// delete from disk
|
|
QFile(filename).remove();
|
|
|
|
// delete from DB
|
|
trainDB->startLUW();
|
|
trainDB->deleteVideoSync(filename);
|
|
trainDB->endLUW();
|
|
}
|
|
}
|
|
|
|
void
|
|
TrainSidebar::removeInvalidVideoSync()
|
|
{
|
|
QModelIndex current = videosyncTree->currentIndex();
|
|
QModelIndex target = vssortModel->mapToSource(current);
|
|
QString filename = videosyncModel->data(videosyncModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("The VideoSync file is either not valid or not existing and will be removed from the library."));
|
|
msgBox.setInformativeText(filename);
|
|
QPushButton *removeButton = msgBox.addButton(tr("Remove"),QMessageBox::YesRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(removeButton);
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.exec();
|
|
|
|
if(msgBox.clickedButton() != removeButton) return;
|
|
|
|
// delete from DB
|
|
trainDB->startLUW();
|
|
trainDB->deleteVideoSync(filename);
|
|
trainDB->endLUW();
|
|
|
|
}
|
|
|
|
void
|
|
TrainSidebar::deleteWorkouts()
|
|
{
|
|
QStringList nameList;
|
|
QModelIndexList list = workoutTree->selectionModel()->selectedRows();
|
|
foreach (QModelIndex index, list) {
|
|
QModelIndex target = sortModel->mapToSource(index);
|
|
QString filename = workoutModel->data(workoutModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
if (QFileInfo(filename).exists()) {
|
|
nameList.append(filename);
|
|
}
|
|
}
|
|
|
|
if (nameList.count()>0) {
|
|
// are you sure?
|
|
QMessageBox msgBox;
|
|
if (nameList.count()==1) {
|
|
msgBox.setText(tr("Are you sure you want to delete this Workout?"));
|
|
}
|
|
else {
|
|
msgBox.setText(QString(tr("Are you sure you want to delete this %1 workouts?")).arg(nameList.count()));
|
|
}
|
|
QString info;
|
|
for (int i=0;i<nameList.count() && i<21;i++) {
|
|
info += nameList.at(i)+"\n";
|
|
if (i == 20) {
|
|
info += "...\n";
|
|
}
|
|
}
|
|
msgBox.setInformativeText(info);
|
|
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;
|
|
|
|
foreach (QString filename, nameList) {
|
|
if (QFileInfo(filename).exists()) {
|
|
// delete from disk
|
|
QFile(filename).remove();
|
|
// delete from DB
|
|
trainDB->startLUW();
|
|
trainDB->deleteWorkout(filename);
|
|
trainDB->endLUW();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
TrainSidebar::removeInvalidWorkout()
|
|
{
|
|
QModelIndex current = workoutTree->currentIndex();
|
|
QModelIndex target = sortModel->mapToSource(current);
|
|
QString filename = workoutModel->data(workoutModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("The Workout file is either not valid or not existing and will be removed from the library."));
|
|
msgBox.setInformativeText(filename);
|
|
QPushButton *removeButton = msgBox.addButton(tr("Remove"),QMessageBox::YesRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(removeButton);
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.exec();
|
|
|
|
if(msgBox.clickedButton() != removeButton) return;
|
|
|
|
// delete from DB
|
|
trainDB->startLUW();
|
|
trainDB->deleteWorkout(filename);
|
|
trainDB->endLUW();
|
|
|
|
}
|
|
|
|
void
|
|
TrainSidebar::mediaTreeWidgetSelectionChanged()
|
|
{
|
|
|
|
QModelIndex current = mediaTree->currentIndex();
|
|
QModelIndex target = vsortModel->mapToSource(current);
|
|
QString filename = videoModel->data(videoModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
if (filename == context->videoFilename) {
|
|
mediafile = "";
|
|
context->notifyMediaSelected(""); // CTRL+Click to clear selection
|
|
} else {
|
|
mediafile = filename;
|
|
context->notifyMediaSelected(filename);
|
|
}
|
|
}
|
|
|
|
void
|
|
TrainSidebar::videosyncTreeWidgetSelectionChanged()
|
|
{
|
|
|
|
QModelIndex current = videosyncTree->currentIndex();
|
|
QModelIndex target = vssortModel->mapToSource(current);
|
|
QString filename = videosyncModel->data(videosyncModel->index(target.row(), 0), Qt::DisplayRole).toString();
|
|
|
|
// wip away the current selected videosync
|
|
if (videosyncFile) {
|
|
delete videosyncFile;
|
|
videosyncFile = NULL;
|
|
}
|
|
|
|
if (filename == "") {
|
|
context->notifyVideoSyncFileSelected(NULL);
|
|
return;
|
|
}
|
|
|
|
// is "None" selected?
|
|
int index = target.row();
|
|
if (index == 0) {
|
|
// None menu entry
|
|
context->notifyVideoSyncFileSelected(NULL);
|
|
} else {
|
|
videosyncFile = new VideoSyncFile(filename, mode, context);
|
|
if (videosyncFile->isValid()) {
|
|
context->notifyVideoSyncFileSelected(videosyncFile);
|
|
} else {
|
|
delete videosyncFile;
|
|
videosyncFile = NULL;
|
|
context->notifyVideoSyncFileSelected(NULL);
|
|
removeInvalidVideoSync();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------------
|
|
* Was realtime window, now local and manages controller and chart updates etc
|
|
*------------------------------------------------------------------------------*/
|
|
|
|
void TrainSidebar::Start() // when start button is pressed
|
|
{
|
|
if (status&RT_PAUSED) {
|
|
|
|
qDebug() << "unpause...";
|
|
|
|
// UN PAUSE!
|
|
session_time.start();
|
|
lap_time.start();
|
|
clearStatusFlags(RT_PAUSED);
|
|
|
|
// Reset speed simulation timer.
|
|
bicycle.resettimer();
|
|
|
|
maintainLapDistanceState();
|
|
|
|
//foreach(int dev, activeDevices) Devices[dev].controller->restart();
|
|
//gui_timer->start(REFRESHRATE);
|
|
if (status & RT_RECORDING) disk_timer->start(SAMPLERATE);
|
|
load_period.restart();
|
|
load_timer->start(LOADRATE);
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setEnabled(false);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setEnabled(false);
|
|
#endif
|
|
#endif
|
|
|
|
// tell the world
|
|
context->notifyUnPause();
|
|
|
|
emit setNotification(tr("Resuming.."), 2);
|
|
|
|
} else if (status&RT_RUNNING) {
|
|
|
|
qDebug() << "pause...";
|
|
|
|
// Pause!
|
|
session_elapsed_msec += session_time.elapsed();
|
|
lap_elapsed_msec += lap_time.elapsed();
|
|
setStatusFlags(RT_PAUSED);
|
|
//foreach(int dev, activeDevices) Devices[dev].controller->pause();
|
|
//gui_timer->stop();
|
|
if (status & RT_RECORDING) disk_timer->stop();
|
|
load_timer->stop();
|
|
load_msecs += load_period.restart();
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
// enable media tree so we can change movie - mid workout
|
|
mediaTree->setEnabled(true);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setEnabled(true);
|
|
#endif
|
|
#endif
|
|
|
|
// tell the world
|
|
context->notifyPause();
|
|
|
|
emit setNotification(tr("Paused.."), 2);
|
|
|
|
} else if (status&RT_CONNECTED) {
|
|
|
|
// Delayed start handling
|
|
if (secs_to_start == 0) {
|
|
secs_to_start = appsettings->value(this, TRAIN_STARTDELAY, 0).toUInt();
|
|
} else {
|
|
secs_to_start--;
|
|
}
|
|
if (secs_to_start > 0) {
|
|
emit setNotification(tr("Starting in %1").arg(secs_to_start), 1);
|
|
start_timer->start(1000);
|
|
return;
|
|
}
|
|
|
|
qDebug() << "start...";
|
|
|
|
#ifdef WIN32
|
|
// disable the screen saver on Windows
|
|
SetThreadExecutionState(ES_DISPLAY_REQUIRED | ES_CONTINUOUS);
|
|
#endif
|
|
|
|
context->mainWindow->showSidebar(false);
|
|
if (appsettings->value(this, TRAIN_AUTOHIDE, false).toBool()) context->mainWindow->showLowbar(false);
|
|
|
|
// Stop users from selecting different devices
|
|
// media or workouts whilst a workout is in progress
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setEnabled(false);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setEnabled(false);
|
|
#endif
|
|
#endif
|
|
workoutTree->setEnabled(false);
|
|
deviceTree->setEnabled(false);
|
|
|
|
// START!
|
|
load = 100;
|
|
slope = 0.0;
|
|
|
|
// Reset Speed Simulation
|
|
bicycle.clear();
|
|
|
|
maintainLapDistanceState();
|
|
|
|
if (mode == ERG || mode == MRC) {
|
|
setStatusFlags(RT_MODE_ERGO);
|
|
clearStatusFlags(RT_MODE_SPIN);
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setMode(RT_MODE_ERGO);
|
|
} else { // SLOPE MODE
|
|
setStatusFlags(RT_MODE_SPIN);
|
|
clearStatusFlags(RT_MODE_ERGO);
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setMode(RT_MODE_SPIN);
|
|
}
|
|
|
|
// tell the world
|
|
context->notifyStart();
|
|
|
|
// we're away!
|
|
setStatusFlags(RT_RUNNING);
|
|
|
|
// tell the world
|
|
context->notifyStart();
|
|
|
|
load_period.restart();
|
|
session_time.start();
|
|
session_elapsed_msec = 0;
|
|
lap_time.start();
|
|
lap_elapsed_msec = 0;
|
|
wbalr = 0;
|
|
wbal = WPRIME;
|
|
|
|
resetTextAudioEmitTracking();
|
|
|
|
//reset all calibration data
|
|
calibrating = startCalibration = restartCalibration = finishCalibration = false;
|
|
calibrationSpindownTime = calibrationZeroOffset = calibrationSlope = calibrationTargetSpeed = 0;
|
|
calibrationCadence = calibrationCurrentSpeed = calibrationTorque = 0;
|
|
calibrationState = CALIBRATION_STATE_IDLE;
|
|
calibrationType = CALIBRATION_TYPE_NOT_SUPPORTED;
|
|
calibrationDeviceIndex = -1;
|
|
clearStatusFlags(RT_CALIBRATING);
|
|
//foreach(int dev, activeDevices) { // Do for selected device only
|
|
// Devices[dev].controller->resetCalibrationState();
|
|
//}
|
|
|
|
load_timer->start(LOADRATE); // start recording
|
|
|
|
if (recordSelector->isChecked()) {
|
|
setStatusFlags(RT_RECORDING);
|
|
}
|
|
|
|
if (status & RT_RECORDING) {
|
|
|
|
QString workoutName;
|
|
if (context->currentErgFile()) {
|
|
workoutName = QFileInfo(context->currentErgFile()->filename).baseName();
|
|
}
|
|
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
|
|
// setup file
|
|
QString filename = now.toString(QString("yyyy_MM_dd_hh_mm_ss")) + "_" + workoutName + QString(".csv");
|
|
|
|
if (!context->athlete->home->records().exists())
|
|
context->athlete->home->createAllSubdirs();
|
|
|
|
QString fulltarget = context->athlete->home->records().canonicalPath() + "/" + filename;
|
|
|
|
if (recordFile) delete recordFile;
|
|
recordFile = new QFile(fulltarget);
|
|
lastRecordSecs = 0;
|
|
if (!recordFile->open(QFile::WriteOnly | QFile::Truncate)) {
|
|
clearStatusFlags(RT_RECORDING);
|
|
} else {
|
|
|
|
// CSV File header
|
|
|
|
QTextStream recordFileStream(recordFile);
|
|
recordFileStream << "secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, slope, temp, interval, lrbalance, lte, rte, lps, rps, smo2, thb, o2hb, hhb, target\n";
|
|
|
|
disk_timer->start(SAMPLERATE); // start screen
|
|
}
|
|
}
|
|
gui_timer->start(REFRESHRATE); // start recording
|
|
|
|
emit setNotification(tr("Starting.."), 2);
|
|
}
|
|
}
|
|
|
|
void TrainSidebar::Pause() // pause capture to recalibrate
|
|
{
|
|
// Not convinced this function is ever reached, these are handled in Start()
|
|
|
|
// we're not running fool!
|
|
if ((status&RT_RUNNING) == 0) return;
|
|
|
|
if (status&RT_PAUSED) {
|
|
|
|
session_time.start();
|
|
lap_time.start();
|
|
clearStatusFlags(RT_PAUSED);
|
|
foreach(int dev, activeDevices) Devices[dev].controller->restart();
|
|
gui_timer->start(REFRESHRATE);
|
|
if (status & RT_RECORDING) disk_timer->start(SAMPLERATE);
|
|
load_period.restart();
|
|
load_timer->start(LOADRATE);
|
|
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setEnabled(false);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setEnabled(false);
|
|
#endif
|
|
#endif
|
|
|
|
// tell the world
|
|
context->notifyUnPause();
|
|
} else {
|
|
|
|
session_elapsed_msec += session_time.elapsed();
|
|
lap_elapsed_msec += lap_time.elapsed();
|
|
foreach(int dev, activeDevices) Devices[dev].controller->pause();
|
|
setStatusFlags(RT_PAUSED);
|
|
gui_timer->stop();
|
|
if (status & RT_RECORDING) disk_timer->stop();
|
|
load_timer->stop();
|
|
load_msecs += load_period.restart();
|
|
|
|
// enable media tree so we can change movie
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setEnabled(true);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setEnabled(true);
|
|
#endif
|
|
#endif
|
|
|
|
// tell the world
|
|
context->notifyPause();
|
|
}
|
|
}
|
|
|
|
void TrainSidebar::Stop(int deviceStatus) // when stop button is pressed
|
|
{
|
|
if ((status&RT_RUNNING) == 0) return;
|
|
|
|
// re-enable the screen saver on Windows
|
|
#ifdef WIN32
|
|
SetThreadExecutionState(ES_CONTINUOUS);
|
|
#endif
|
|
|
|
clearStatusFlags(RT_RUNNING|RT_PAUSED);
|
|
|
|
// Stop users from selecting different devices
|
|
// media or workouts whilst a workout is in progress
|
|
#if !defined GC_VIDEO_NONE
|
|
mediaTree->setEnabled(true);
|
|
#ifdef GC_HAVE_VLC // RLV currently only support for VLC
|
|
videosyncTree->setEnabled(true);
|
|
#endif
|
|
#endif
|
|
workoutTree->setEnabled(true);
|
|
deviceTree->setEnabled(true);
|
|
|
|
context->mainWindow->showSidebar(true);
|
|
if (appsettings->value(this, TRAIN_AUTOHIDE, false).toBool()) context->mainWindow->showLowbar(true);
|
|
|
|
//reset all calibration data
|
|
calibrating = startCalibration = restartCalibration = finishCalibration = false;
|
|
calibrationSpindownTime = calibrationZeroOffset = calibrationSlope = calibrationTargetSpeed = 0;
|
|
calibrationCadence = calibrationCurrentSpeed = calibrationTorque = 0;
|
|
calibrationState = CALIBRATION_STATE_IDLE;
|
|
calibrationType = CALIBRATION_TYPE_NOT_SUPPORTED;
|
|
calibrationDeviceIndex = -1;
|
|
clearStatusFlags(RT_CALIBRATING);
|
|
//foreach(int dev, activeDevices) { // Do for selected device only
|
|
// Devices[dev].controller->resetCalibrationState();
|
|
//}
|
|
|
|
load = 0;
|
|
slope = 0.0;
|
|
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
|
|
if (status & RT_RECORDING) {
|
|
disk_timer->stop();
|
|
|
|
// close and reset File
|
|
recordFile->close();
|
|
|
|
// Request mutual exclusion with ANT+/BTLE threads to change status and close rr/vo2 files
|
|
rrMutex.lock();
|
|
vo2Mutex.lock();
|
|
|
|
// cancel recording
|
|
status &= ~RT_RECORDING;
|
|
|
|
// close rrFile
|
|
if (rrFile) {
|
|
//fprintf(stderr, "Closing r-r file\n"); fflush(stderr);
|
|
rrFile->close();
|
|
delete rrFile;
|
|
rrFile=NULL;
|
|
}
|
|
|
|
// close vo2File
|
|
if (vo2File) {
|
|
fprintf(stderr, "Closing vo2 file\n"); fflush(stderr);
|
|
vo2File->close();
|
|
delete vo2File;
|
|
vo2File=NULL;
|
|
}
|
|
|
|
// Release mutual exclusion with ANT+/BTLE threads before to open import dialog to avoid deadlocks
|
|
rrMutex.unlock();
|
|
vo2Mutex.unlock();
|
|
|
|
if(deviceStatus == DEVICE_ERROR)
|
|
{
|
|
recordFile->remove();
|
|
}
|
|
else {
|
|
// add to the view - using basename ONLY
|
|
QString name;
|
|
name = recordFile->fileName();
|
|
|
|
QList<QString> list;
|
|
list.append(name);
|
|
|
|
RideImportWizard *dialog = new RideImportWizard (list, context);
|
|
dialog->process(); // do it!
|
|
}
|
|
}
|
|
|
|
load_timer->stop();
|
|
load_msecs = 0;
|
|
|
|
// get back to normal after it may have been adusted by the user
|
|
//lastAppliedIntensity=100;
|
|
adjustIntensity(100);
|
|
if (context->currentErgFile()) context->currentErgFile()->reload();
|
|
context->notifySetNow(load_msecs);
|
|
|
|
|
|
// tell the world
|
|
context->notifyStop();
|
|
|
|
// if a config change was requested while workout was running, action it now
|
|
if (pendingConfigChange) {
|
|
pendingConfigChange = false;
|
|
configChanged(CONFIG_APPEARANCE | CONFIG_DEVICES | CONFIG_ZONES);
|
|
}
|
|
|
|
// Re-enable gui elements
|
|
// reset counters etc
|
|
pwrcount = 0;
|
|
cadcount = 0;
|
|
hrcount = 0;
|
|
spdcount = 0;
|
|
lodcount = 0;
|
|
displayWorkoutLap = 0;
|
|
wbalr = 0;
|
|
wbal = WPRIME;
|
|
session_elapsed_msec = 0;
|
|
session_time.restart();
|
|
lap_elapsed_msec = 0;
|
|
lap_time.restart();
|
|
displayWorkoutDistance = displayDistance = 0;
|
|
displayLapDistance = 0;
|
|
displayLapDistanceRemaining = -1;
|
|
displayAltitude = 0;
|
|
|
|
ergFileQueryAdapter.resetQueryState();
|
|
guiUpdate();
|
|
|
|
emit setNotification(tr("Stopped.."), 2);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
// Called by push devices (e.g. ANT+)
|
|
void TrainSidebar::updateData(RealtimeData &rtData)
|
|
{
|
|
displayPower = rtData.getWatts();
|
|
displayCadence = rtData.getCadence();
|
|
displayHeartRate = rtData.getHr();
|
|
displaySpeed = rtData.getSpeed();
|
|
load = rtData.getLoad();
|
|
displayLRBalance = rtData.getLRBalance();
|
|
displayLTE = rtData.getLTE();
|
|
displayRTE = rtData.getRTE();
|
|
displayLPS = rtData.getLPS();
|
|
displayRPS = rtData.getRPS();
|
|
displaySMO2 = rtData.getSmO2();
|
|
displayTHB = rtData.gettHb();
|
|
displayO2HB = rtData.getO2Hb();
|
|
displayHHB = rtData.getHHb();
|
|
displayLatitude = rtData.getLatitude();
|
|
displayLongitude = rtData.getLongitude();
|
|
displayAltitude = rtData.getAltitude();
|
|
// Gradient not supported
|
|
return;
|
|
}
|
|
|
|
void TrainSidebar::toggleConnect()
|
|
{
|
|
if (status&RT_CONNECTED)
|
|
Disconnect();
|
|
else
|
|
Connect();
|
|
}
|
|
|
|
void TrainSidebar::Connect()
|
|
{
|
|
//qDebug() << "current tab:" << context->tab->currentView();
|
|
|
|
// only try and connect if we are in train view..
|
|
// fixme: these values are hard-coded throughout
|
|
if (!(context->tab->currentView() == 3)) return;
|
|
|
|
if (status&RT_CONNECTED) return; // already connected
|
|
|
|
qDebug() << "connecting..";
|
|
|
|
// if we have selected multiple devices lets
|
|
// configure the series we collect from each one
|
|
if (deviceTree->selectedItems().count() > 1) {
|
|
MultiDeviceDialog *multisetup = new MultiDeviceDialog(context, this);
|
|
if (multisetup->exec() == false) {
|
|
return;
|
|
}
|
|
} else if (deviceTree->selectedItems().count() == 1) {
|
|
bpmTelemetry = wattsTelemetry = kphTelemetry = rpmTelemetry =
|
|
deviceTree->selectedItems().first()->type();
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
activeDevices = devices();
|
|
|
|
foreach(int dev, activeDevices) {
|
|
Devices[dev].controller->setWheelCircumference(Devices[dev].wheelSize);
|
|
Devices[dev].controller->setRollingResistance(bicycle.RollingResistance());
|
|
Devices[dev].controller->setWeight(bicycle.MassKG());
|
|
Devices[dev].controller->setWindSpeed(0); // Move to loadUpdate when wind simulation is added
|
|
|
|
Devices[dev].controller->start();
|
|
Devices[dev].controller->resetCalibrationState();
|
|
connect(Devices[dev].controller, &RealtimeController::setNotification, this, &TrainSidebar::setNotification);
|
|
}
|
|
setStatusFlags(RT_CONNECTED);
|
|
gui_timer->start(REFRESHRATE);
|
|
|
|
emit setNotification(tr("Connected.."), 2);
|
|
}
|
|
|
|
void TrainSidebar::Disconnect()
|
|
{
|
|
// cancel any pending start
|
|
if (secs_to_start > 0) {
|
|
secs_to_start = 0;
|
|
start_timer->stop();
|
|
}
|
|
|
|
// don't try to disconnect if running or not connected
|
|
if ((status&RT_RUNNING) || ((status&RT_CONNECTED) == 0)) return;
|
|
|
|
static QIcon connectedIcon(":images/oxygen/power-on.png");
|
|
static QIcon disconnectedIcon(":images/oxygen/power-off.png");
|
|
|
|
qDebug() << "disconnecting..";
|
|
|
|
foreach(int dev, activeDevices) {
|
|
disconnect(Devices[dev].controller, &RealtimeController::setNotification, this, &TrainSidebar::setNotification);
|
|
Devices[dev].controller->stop();
|
|
}
|
|
clearStatusFlags(RT_CONNECTED);
|
|
|
|
gui_timer->stop();
|
|
|
|
emit setNotification(tr("Disconnected.."), 2);
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// SCREEN UPDATE FUNCTIONS
|
|
//----------------------------------------------------------------------
|
|
|
|
void TrainSidebar::guiUpdate() // refreshes the telemetry
|
|
{
|
|
RealtimeData rtData;
|
|
rtData.setLap(displayWorkoutLap);
|
|
rtData.mode = mode;
|
|
|
|
// get latest telemetry from devices
|
|
if ((status&RT_RUNNING) || (status&RT_CONNECTED)) {
|
|
|
|
#ifdef Q_OS_MAC
|
|
// 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.
|
|
|
|
CFStringRef reasonForActivity = CFSTR("TrainSidebar::guiUpdate");
|
|
IOPMAssertionID assertionID;
|
|
IOReturn suspendSreensaverSuccess = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionLevelOn, reasonForActivity, &assertionID);
|
|
|
|
#elif defined(WIN32)
|
|
// Multimedia applications, such as video players and presentation applications, must use ES_DISPLAY_REQUIRED
|
|
// when they display video for long periods of time without user input.
|
|
SetThreadExecutionState(ES_DISPLAY_REQUIRED);
|
|
#endif
|
|
|
|
if(calibrating) {
|
|
foreach(int dev, activeDevices) { // Do for selected device only
|
|
RealtimeData local = rtData;
|
|
|
|
if (calibrationDeviceIndex == dev) {
|
|
// need telemetry for calibration dialog updates
|
|
// (and F3 button press for Computrainer)
|
|
|
|
Devices[dev].controller->getRealtimeData(local);
|
|
calibrationCurrentSpeed = local.getSpeed();
|
|
calibrationTorque = local.getTorque();
|
|
calibrationCadence = local.getCadence();
|
|
|
|
calibrationTargetSpeed = Devices[dev].controller->getCalibrationTargetSpeed();
|
|
|
|
calibrationState = Devices[dev].controller->getCalibrationState();
|
|
|
|
calibrationSpindownTime = Devices[dev].controller->getCalibrationSpindownTime();
|
|
calibrationZeroOffset = Devices[dev].controller->getCalibrationZeroOffset();
|
|
calibrationSlope = Devices[dev].controller->getCalibrationSlope();
|
|
|
|
// if calibration is moving out of pending state..
|
|
if ((calibrationState == CALIBRATION_STATE_PENDING) && startCalibration) {
|
|
startCalibration = false;
|
|
qDebug() << "Sending calibration request..";
|
|
Devices[dev].controller->setCalibrationState(CALIBRATION_STATE_REQUESTED);
|
|
}
|
|
|
|
// if calibration was already requested, but not receiving updates, then try again..
|
|
if ((calibrationState == CALIBRATION_STATE_STARTING) && restartCalibration) {
|
|
restartCalibration = false;
|
|
qDebug() << "No response to our calibration request, re-requesting..";
|
|
Devices[dev].controller->setCalibrationState(CALIBRATION_STATE_REQUESTED);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// calibration has completed (or failed), toggle out of calibration state
|
|
// - do this outside of dev loop in case of no valid/supported device
|
|
if (finishCalibration) {
|
|
finishCalibration = false;
|
|
Calibrate();
|
|
}
|
|
|
|
// Update the calibration dialog if necessary
|
|
updateCalibration();
|
|
|
|
// and exit. Nothing else to do until we finish calibrating
|
|
return;
|
|
} else {
|
|
rtData.setLoad(load); // always set load..
|
|
rtData.setSlope(slope); // always set load..
|
|
rtData.setAltitude(displayAltitude); // always set display altitude
|
|
|
|
double distanceTick = 0;
|
|
|
|
// fetch the right data from each device...
|
|
foreach(int dev, activeDevices) {
|
|
|
|
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
|
|
rtData.setSlope(local.getSlope()); // and get slope in case it was adjusted
|
|
// to within defined limits
|
|
}
|
|
|
|
if (Devices[dev].type == DEV_FORTIUS || Devices[dev].type == DEV_IMAGIC) {
|
|
rtData.setLoad(local.getLoad()); // and get load in case it was adjusted
|
|
rtData.setSlope(local.getSlope()); // and get slope in case it was adjusted
|
|
// to within defined limits
|
|
}
|
|
|
|
if (Devices[dev].type == DEV_ANTLOCAL || Devices[dev].type == DEV_NULL) {
|
|
rtData.setHb(local.getSmO2(), local.gettHb()); //only moxy data from ant and robot devices right now
|
|
}
|
|
|
|
if (Devices[dev].type == DEV_NULL || Devices[dev].type == DEV_BT40) {
|
|
// Only robot and BT40 devices provides VO2 metrics
|
|
rtData.setRf(local.getRf());
|
|
rtData.setRMV(local.getRMV());
|
|
rtData.setVO2_VCO2(local.getVO2(), local.getVCO2());
|
|
rtData.setTv(local.getTv());
|
|
rtData.setFeO2(local.getFeO2());
|
|
}
|
|
|
|
// 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());
|
|
rtData.setRouteDistance(local.getRouteDistance());
|
|
rtData.setDistanceRemaining(local.getDistanceRemaining());
|
|
rtData.setLapDistance(local.getLapDistance());
|
|
rtData.setLapDistanceRemaining(local.getLapDistanceRemaining());
|
|
}
|
|
if (dev == wattsTelemetry) {
|
|
rtData.setWatts(local.getWatts());
|
|
rtData.setAltWatts(local.getAltWatts());
|
|
rtData.setLRBalance(local.getLRBalance());
|
|
rtData.setLTE(local.getLTE());
|
|
rtData.setRTE(local.getRTE());
|
|
rtData.setLPS(local.getLPS());
|
|
rtData.setRPS(local.getRPS());
|
|
}
|
|
if (local.getTrainerStatusAvailable())
|
|
{
|
|
rtData.setTrainerStatusAvailable(true);
|
|
rtData.setTrainerReady(local.getTrainerReady());
|
|
rtData.setTrainerRunning(local.getTrainerRunning());
|
|
rtData.setTrainerCalibRequired(local.getTrainerCalibRequired());
|
|
rtData.setTrainerConfigRequired(local.getTrainerConfigRequired());
|
|
rtData.setTrainerBrakeFault(local.getTrainerBrakeFault());
|
|
}
|
|
}
|
|
|
|
// If simulated speed is *not* checked then you get speed reported by
|
|
// trainer which in ergo mode will be dictated by your gear and cadence,
|
|
// and in slope mode is whatever the trainer happens to implement.
|
|
if (useSimulatedSpeed) {
|
|
BicycleSimState newState(rtData);
|
|
SpeedDistance ret = bicycle.SampleSpeed(newState);
|
|
|
|
rtData.setSpeed(ret.v);
|
|
|
|
displaySpeed = ret.v;
|
|
distanceTick = ret.d;
|
|
} else {
|
|
distanceTick = displaySpeed / (5 * 3600); // assumes 200ms refreshrate
|
|
}
|
|
|
|
// only update time & distance if actively running (not just connected, and not running but paused)
|
|
if ((status&RT_RUNNING) && ((status&RT_PAUSED) == 0)) {
|
|
|
|
displayDistance += distanceTick;
|
|
displayLapDistance += distanceTick;
|
|
displayLapDistanceRemaining -= distanceTick;
|
|
displayWorkoutDistance += distanceTick;
|
|
|
|
if (!(status&RT_MODE_ERGO) && (context->currentVideoSyncFile()))
|
|
{
|
|
// If we reached the end of the RLV then stop
|
|
if (displayWorkoutDistance >= context->currentVideoSyncFile()->Distance) {
|
|
Stop(DEVICE_OK);
|
|
return;
|
|
}
|
|
// TODO : graphs to be shown at seek position
|
|
}
|
|
|
|
// If we just tripped over the end of the lap, we need to look at base data
|
|
// to find distance to next lap. This is primarily due to lap display updates
|
|
// -0.999 is chosen as a number that is less than 0, but greater than -1
|
|
if (displayLapDistanceRemaining < 0 && displayLapDistanceRemaining > -0.999) {
|
|
maintainLapDistanceState();
|
|
}
|
|
|
|
rtData.setDistance(displayDistance);
|
|
rtData.setRouteDistance(displayWorkoutDistance);
|
|
rtData.setLapDistance(displayLapDistance);
|
|
rtData.setLapDistanceRemaining(displayLapDistanceRemaining);
|
|
|
|
const ErgFile* ergFile = ergFileQueryAdapter.getErgFile();
|
|
if (ergFile) {
|
|
|
|
// update DistanceRemaining
|
|
if (ergFile->Duration / 1000.0 > displayWorkoutDistance)
|
|
rtData.setDistanceRemaining(ergFile->Duration / 1000.0 - displayWorkoutDistance);
|
|
else
|
|
rtData.setDistanceRemaining(0.0);
|
|
|
|
// If ergfile has no gradient then there is no location, or altitude (or slope.)
|
|
if (ergFile->hasGradient()) {
|
|
bool fAltitudeSet = false;
|
|
if (!ergFile->StrictGradient) {
|
|
// Attempt to obtain location and derived slope from altitude in ergfile.
|
|
geolocation geoloc;
|
|
if (ergFileQueryAdapter.locationAt(displayWorkoutDistance * 1000, displayWorkoutLap, geoloc, slope)) {
|
|
displayLatitude = geoloc.Lat();
|
|
displayLongitude = geoloc.Long();
|
|
displayAltitude = geoloc.Alt();
|
|
|
|
if (displayLatitude && displayLongitude) {
|
|
rtData.setLatitude(displayLatitude);
|
|
rtData.setLongitude(displayLongitude);
|
|
}
|
|
fAltitudeSet = true;
|
|
}
|
|
}
|
|
|
|
if (ergFile->StrictGradient || !fAltitudeSet) {
|
|
slope = ergFileQueryAdapter.gradientAt(displayWorkoutDistance * 1000, displayWorkoutLap);
|
|
}
|
|
|
|
if (!fAltitudeSet) {
|
|
// Since we have gradient, we also have altitude
|
|
displayAltitude = ergFileQueryAdapter.altitudeAt(displayWorkoutDistance * 1000, displayWorkoutLap);
|
|
}
|
|
|
|
rtData.setSlope(slope);
|
|
rtData.setAltitude(displayAltitude);
|
|
}
|
|
}
|
|
else if (!(status & RT_MODE_ERGO)) {
|
|
// For manual slope mode, estimate vertical change based upon time passed and slope.
|
|
// Note this isn't exactly right but is very close - we should use the previous slope for the time passed.
|
|
double altitudeDeltaMeters = slope * (10 * distanceTick); // ((slope / 100) * distanceTick) * 1000
|
|
displayAltitude += altitudeDeltaMeters;
|
|
rtData.setAltitude(displayAltitude);
|
|
}
|
|
|
|
// 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);
|
|
|
|
long lapTimeRemaining;
|
|
if (ergFile) lapTimeRemaining = ergFile->nextLap(load_msecs) - load_msecs;
|
|
else lapTimeRemaining = 0;
|
|
|
|
long ergTimeRemaining;
|
|
if (ergFile) ergTimeRemaining = ergFileQueryAdapter.currentTime() - load_msecs;
|
|
else ergTimeRemaining = 0;
|
|
|
|
double lapPosition = status & RT_MODE_ERGO ? load_msecs : displayWorkoutDistance * 1000;
|
|
|
|
// alert when approaching end of lap
|
|
if (lapAudioEnabled && lapAudioThisLap) {
|
|
|
|
// alert when 3 seconds from end of ERG lap, or 20 meters from end of CRS lap
|
|
bool fPlayAudio = false;
|
|
if (status & RT_MODE_ERGO && lapTimeRemaining > 0 && lapTimeRemaining < 3000) {
|
|
fPlayAudio = true;
|
|
} else {
|
|
double lapmarker = ergFileQueryAdapter.nextLap(lapPosition);
|
|
if (status&RT_MODE_SLOPE && (lapmarker >= 0.) && lapmarker - lapPosition < 20) {
|
|
fPlayAudio = true;
|
|
}
|
|
}
|
|
|
|
if (fPlayAudio) {
|
|
lapAudioThisLap = false;
|
|
QSound::play(":audio/lap.wav");
|
|
}
|
|
}
|
|
|
|
// Text Cues
|
|
if (lapPosition > textPositionEmitted) {
|
|
double searchRange = (status & RT_MODE_ERGO) ? 1000 : 10;
|
|
int rangeStart, rangeEnd;
|
|
if (ergFileQueryAdapter.textsInRange(lapPosition, searchRange, rangeStart, rangeEnd)) {
|
|
for (int idx = rangeStart; idx <= rangeEnd; idx++) {
|
|
ErgFileText cue = ergFile->Texts.at(idx);
|
|
emit setNotification(cue.text, cue.duration);
|
|
}
|
|
}
|
|
textPositionEmitted = lapPosition + searchRange;
|
|
}
|
|
|
|
// Maintain time in ERGO mode
|
|
if (status& RT_MODE_ERGO) {
|
|
if (lapTimeRemaining < 0) {
|
|
if (ergFile) lapTimeRemaining = ergFile->Duration - load_msecs;
|
|
if (lapTimeRemaining < 0)
|
|
lapTimeRemaining = 0;
|
|
}
|
|
rtData.setLapMsecsRemaining(lapTimeRemaining);
|
|
|
|
if (ergTimeRemaining < 0) {
|
|
if (ergFile) ergTimeRemaining = ergFile->Duration - load_msecs;
|
|
if (ergTimeRemaining < 0)
|
|
ergTimeRemaining = 0;
|
|
}
|
|
rtData.setErgMsecsRemaining(ergTimeRemaining);
|
|
}
|
|
} else {
|
|
rtData.setDistance(displayDistance);
|
|
rtData.setRouteDistance(displayWorkoutDistance);
|
|
rtData.setLapDistance(displayLapDistance);
|
|
rtData.setLapDistanceRemaining(displayLapDistanceRemaining);
|
|
rtData.setMsecs(session_elapsed_msec);
|
|
rtData.setLapMsecs(lap_elapsed_msec);
|
|
}
|
|
|
|
// local stuff ...
|
|
displayPower = rtData.getWatts();
|
|
displayCadence = rtData.getCadence();
|
|
displayHeartRate = rtData.getHr();
|
|
displaySpeed = rtData.getSpeed();
|
|
load = rtData.getLoad();
|
|
slope = rtData.getSlope();
|
|
displayLRBalance = rtData.getLRBalance();
|
|
displayLTE = rtData.getLTE();
|
|
displayRTE = rtData.getRTE();
|
|
displayLPS = rtData.getLPS();
|
|
displayRPS = rtData.getRPS();
|
|
displaySMO2 = rtData.getSmO2();
|
|
displayTHB = rtData.gettHb();
|
|
displayO2HB = rtData.getO2Hb();
|
|
displayHHB = rtData.getHHb();
|
|
displayLatitude = rtData.getLatitude();
|
|
displayLongitude = rtData.getLongitude();
|
|
displayAltitude = rtData.getAltitude();
|
|
|
|
double weightKG = context->athlete->getWeight(QDate::currentDate()) + 10; // 10kg bike
|
|
double vs = computeInstantSpeed(weightKG, rtData.getSlope(), rtData.getAltitude(), rtData.getWatts());
|
|
|
|
rtData.setVirtualSpeed(vs);
|
|
|
|
// W'bal on the fly
|
|
// using Dave Waterworth's reformulation
|
|
double TAU = appsettings->cvalue(context->athlete->cyclist, GC_WBALTAU, 300).toInt();
|
|
|
|
// any watts expended in last 200msec?
|
|
double JOULES = double(rtData.getWatts() - FTP) / 5.00f;
|
|
if (JOULES < 0) JOULES = 0;
|
|
|
|
// running total of replenishment
|
|
wbalr += JOULES * exp((total_msecs/1000.00f) / TAU);
|
|
wbal = WPRIME - (wbalr * exp((-total_msecs/1000.00f) / TAU));
|
|
|
|
rtData.setWbal(wbal);
|
|
|
|
// go update the displays...
|
|
context->notifyTelemetryUpdate(rtData); // signal everyone to update telemetry
|
|
}
|
|
|
|
#ifdef Q_OS_MAC
|
|
if (suspendSreensaverSuccess == kIOReturnSuccess)
|
|
{
|
|
// Re-enable screen saver
|
|
suspendSreensaverSuccess = IOPMAssertionRelease(assertionID);
|
|
//The system will be able to sleep again.
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// can be called from the controller - when user presses "Lap" button
|
|
void TrainSidebar::newLap()
|
|
{
|
|
qDebug() << "running:" << (status&RT_RUNNING) << "paused:" << (status&RT_PAUSED);
|
|
|
|
if ((status&RT_RUNNING) && ((status&RT_PAUSED) == 0)) {
|
|
|
|
pwrcount = 0;
|
|
cadcount = 0;
|
|
hrcount = 0;
|
|
spdcount = 0;
|
|
|
|
ergFileQueryAdapter.addNewLap(displayWorkoutDistance * 1000.);
|
|
|
|
resetTextAudioEmitTracking();
|
|
maintainLapDistanceState();
|
|
|
|
context->notifyNewLap();
|
|
|
|
emit setNotification(tr("New lap.."), 2);
|
|
}
|
|
}
|
|
|
|
void TrainSidebar::resetLapTimer()
|
|
{
|
|
lap_time.restart();
|
|
lap_elapsed_msec = 0;
|
|
displayLapDistance = 0;
|
|
this->resetTextAudioEmitTracking();
|
|
this->maintainLapDistanceState();
|
|
}
|
|
|
|
void TrainSidebar::resetTextAudioEmitTracking()
|
|
{
|
|
lapAudioThisLap = true;
|
|
textPositionEmitted = -1;
|
|
}
|
|
|
|
// Can be called from the controller - when user steers to scroll display
|
|
void TrainSidebar::steerScroll(int scrollAmount)
|
|
{
|
|
if (scrollAmount == 0)
|
|
emit setNotification(tr("Recalibrating steering.."), 10);
|
|
else
|
|
context->notifySteerScroll(scrollAmount);
|
|
}
|
|
|
|
void TrainSidebar::warnnoConfig()
|
|
{
|
|
QMessageBox::warning(this, tr("No Devices Configured"), tr("Please configure a device in Preferences."));
|
|
}
|
|
|
|
template<class T, typename T_GetMethod, typename T_SetMethod, typename T_arg>
|
|
struct ScopedOp
|
|
{
|
|
T* m_pt;
|
|
T_SetMethod m_setMethod;
|
|
T_arg m_argSave;
|
|
|
|
ScopedOp(T* pt, T_GetMethod getMethod, T_SetMethod setMethod, T_arg argNew) : m_pt(pt), m_setMethod(setMethod) {
|
|
m_argSave = (m_pt->*getMethod)();
|
|
(m_pt->*m_setMethod)(argNew);
|
|
}
|
|
|
|
virtual ~ScopedOp() { (m_pt->*m_setMethod)(m_argSave); }
|
|
};
|
|
|
|
struct ScopedPrecision : ScopedOp<QTextStream, int (QTextStream::*)() const, void (QTextStream::*)(int), int> {
|
|
ScopedPrecision(QTextStream* pc, int tempPrecision) : ScopedOp::ScopedOp(pc, &QTextStream::realNumberPrecision, &QTextStream::setRealNumberPrecision, tempPrecision) {}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// DISK UPDATE FUNCTIONS
|
|
//----------------------------------------------------------------------
|
|
void TrainSidebar::diskUpdate()
|
|
{
|
|
int secs;
|
|
|
|
long torq = 0;
|
|
QTextStream recordFileStream(recordFile);
|
|
|
|
if (calibrating) return;
|
|
|
|
// convert from milliseconds to secondes
|
|
total_msecs = session_elapsed_msec + session_time.elapsed();
|
|
secs = round(total_msecs / 1000.0);
|
|
|
|
if (secs <= lastRecordSecs) return; // Avoid duplicates
|
|
lastRecordSecs = secs;
|
|
|
|
// GoldenCheetah CVS Format "secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, slope, temp, interval, lrbalance, lte, rte, lps, rps, smo2, thb, o2hb, hhb\n";
|
|
|
|
recordFileStream << secs
|
|
<< "," << displayCadence
|
|
<< "," << displayHeartRate
|
|
<< "," << displayDistance
|
|
<< "," << displaySpeed
|
|
<< "," << torq
|
|
<< "," << displayPower;
|
|
|
|
// QTextStream default precision is 6, location data needs much more than that.
|
|
// Avoid extra precision for other fields since it isn't needed and would grow
|
|
// the intermediate file for no reason.
|
|
{
|
|
ScopedPrecision tempPrecision(&recordFileStream, 20);
|
|
|
|
recordFileStream << "," << displayAltitude
|
|
<< "," << displayLongitude
|
|
<< "," << displayLatitude;
|
|
}
|
|
|
|
recordFileStream << "," // headwind
|
|
<< "," // slope
|
|
<< "," // temp
|
|
<< "," << displayWorkoutLap
|
|
<< "," << displayLRBalance
|
|
<< "," << displayLTE
|
|
<< "," << displayRTE
|
|
<< "," << displayLPS
|
|
<< "," << displayRPS
|
|
<< "," << displaySMO2
|
|
<< "," << displayTHB
|
|
<< "," << displayO2HB
|
|
<< "," << displayHHB
|
|
<< "," << load
|
|
<< "," << "\n";
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// WORKOUT MODE
|
|
//----------------------------------------------------------------------
|
|
|
|
void TrainSidebar::loadUpdate()
|
|
{
|
|
int curLap = 0;
|
|
|
|
// 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) {
|
|
if (context->currentErgFile()) {
|
|
load = ergFileQueryAdapter.wattsAt(load_msecs, curLap);
|
|
|
|
if (displayWorkoutLap != curLap)
|
|
{
|
|
context->notifyNewLap();
|
|
maintainLapDistanceState();
|
|
}
|
|
displayWorkoutLap = curLap;
|
|
}
|
|
|
|
// we got to the end!
|
|
if (load == -100) {
|
|
Stop(DEVICE_OK);
|
|
} else {
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setLoad(load);
|
|
context->notifySetNow(load_msecs);
|
|
}
|
|
} else {
|
|
if (context->currentErgFile()) {
|
|
// Call gradientAt to obtain current lap num.
|
|
ergFileQueryAdapter.gradientAt(displayWorkoutDistance * 1000., curLap);
|
|
|
|
if (displayWorkoutLap != curLap) {
|
|
context->notifyNewLap();
|
|
maintainLapDistanceState();
|
|
}
|
|
|
|
displayWorkoutLap = curLap;
|
|
}
|
|
|
|
// we got to the end!
|
|
if (slope == -100) {
|
|
Stop(DEVICE_OK);
|
|
} else {
|
|
foreach(int dev, activeDevices) {
|
|
Devices[dev].controller->setGradient(slope);
|
|
Devices[dev].controller->setWindResistance(bicycle.WindResistance(displayAltitude));
|
|
}
|
|
context->notifySetNow(displayWorkoutDistance * 1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrainSidebar::Calibrate()
|
|
{
|
|
// Check we're running (and not paused) before attempting
|
|
// calibration, buttons should be disabled to prevent this,
|
|
// but could be triggered by remote control..
|
|
if ((status & RT_RUNNING) && ((status&RT_PAUSED) == 0)) {
|
|
toggleCalibration();
|
|
updateCalibration();
|
|
}
|
|
}
|
|
|
|
void TrainSidebar::toggleCalibration()
|
|
{
|
|
if (calibrating) {
|
|
|
|
// exiting calibration - restart gui etc
|
|
session_time.start();
|
|
lap_time.start();
|
|
load_period.restart();
|
|
|
|
clearStatusFlags(RT_CALIBRATING);
|
|
load_timer->start(LOADRATE);
|
|
if (status & RT_RECORDING) disk_timer->start(SAMPLERATE);
|
|
context->notifyUnPause(); // get video started again, amongst other things
|
|
|
|
// back to ergo/slope mode and restore load/gradient
|
|
if (status&RT_MODE_ERGO) {
|
|
|
|
foreach(int dev, activeDevices) {
|
|
if (calibrationDeviceIndex == dev) {
|
|
Devices[dev].controller->setCalibrationState(CALIBRATION_STATE_IDLE);
|
|
Devices[dev].controller->setMode(RT_MODE_ERGO);
|
|
Devices[dev].controller->setLoad(load);
|
|
}
|
|
}
|
|
} else {
|
|
|
|
foreach(int dev, activeDevices) {
|
|
if (calibrationDeviceIndex == dev) {
|
|
Devices[dev].controller->setCalibrationState(CALIBRATION_STATE_IDLE);
|
|
Devices[dev].controller->setMode(RT_MODE_SPIN);
|
|
Devices[dev].controller->setGradient(slope);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
// entering calibration - pause gui/load, streaming and recording
|
|
// but keep the gui ticking so we get realtime telemetry for calibration
|
|
session_elapsed_msec += session_time.elapsed();
|
|
lap_elapsed_msec += lap_time.elapsed();
|
|
|
|
setStatusFlags(RT_CALIBRATING);
|
|
if (status & RT_RECORDING) disk_timer->stop();
|
|
load_timer->stop();
|
|
load_msecs += load_period.restart();
|
|
|
|
context->notifyPause(); // get video started again, amongst other things
|
|
|
|
calibrationDeviceIndex = getCalibrationIndex();
|
|
|
|
// only do this for the selected device
|
|
foreach(int dev, devices()) {
|
|
if (calibrationDeviceIndex == dev) {
|
|
calibrationType = Devices[dev].controller->getCalibrationType();
|
|
|
|
// trainer (tacx vortex smart) doesn't appear to reduce resistance automatically when entering calibration mode
|
|
if (status&RT_MODE_ERGO)
|
|
Devices[dev].controller->setLoad(0);
|
|
else
|
|
Devices[dev].controller->setGradient(0);
|
|
|
|
|
|
Devices[dev].controller->setMode(RT_MODE_CALIBRATE);
|
|
Devices[dev].controller->setCalibrationState(CALIBRATION_STATE_PENDING);
|
|
}
|
|
}
|
|
|
|
if (calibrationDeviceIndex == -1)
|
|
qDebug() << "No device(s) found with calibration support";
|
|
else
|
|
qDebug() << "Device" << calibrationDeviceIndex << "being used for calibration";
|
|
}
|
|
|
|
startCalibration = restartCalibration = finishCalibration = false;
|
|
calibrating = !calibrating; // toggle calibration
|
|
}
|
|
|
|
void TrainSidebar::updateCalibration()
|
|
{
|
|
static QString status;
|
|
static uint8_t lastState, stateCount;
|
|
|
|
//qDebug() << "TrainSidebar::updateCalibration()" << calibrating << calibrationState << stateCount;
|
|
|
|
if (!calibrating) {
|
|
|
|
stateCount = 1;
|
|
|
|
// leaving calibration, clear any notification text
|
|
status = QString(tr("Exiting calibration.."));
|
|
emit setNotification(status,3);
|
|
|
|
} else {
|
|
|
|
// Track how long we've been in the same state
|
|
if (calibrationState == lastState)
|
|
stateCount++;
|
|
else
|
|
stateCount = 1;
|
|
|
|
// update message depending on calibration type and state
|
|
switch (calibrationType) {
|
|
|
|
case CALIBRATION_TYPE_NOT_SUPPORTED:
|
|
status = QString(tr("Calibration not supported for this device."));
|
|
if ((stateCount % 10) == 0) {
|
|
finishCalibration = true;
|
|
}
|
|
break;
|
|
|
|
case CALIBRATION_TYPE_COMPUTRAINER:
|
|
status = QString(tr("Calibrating...\nPress F3 on Controller when done."));
|
|
break;
|
|
|
|
case CALIBRATION_TYPE_SPINDOWN:
|
|
|
|
switch (calibrationState) {
|
|
|
|
case CALIBRATION_STATE_IDLE:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_PENDING:
|
|
// Can go straight into spindown calibration
|
|
startCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_REQUESTED:
|
|
status = QString(tr("Requesting calibration.."));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTING:
|
|
status = QString(tr("Requesting calibration.."));
|
|
// if just spinning here, the device has not responded to calibration request
|
|
if ((stateCount % 5) == 0)
|
|
restartCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTED:
|
|
status = QString(tr("Calibrating..."));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_POWER:
|
|
status = QString(tr("Calibrating...\nCurrent speed %1 kph\nIncrease speed to %2 kph")).arg(QString::number(calibrationCurrentSpeed, 'f', 1), QString::number(calibrationTargetSpeed, 'f', 1));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_COAST:
|
|
status = QString(tr("Calibrating...\nStop pedalling until speed drops to 0"));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_SUCCESS:
|
|
// display zero offset and spindown stats
|
|
status = QString(tr("Calibration completed successfully!\nSpindown %1 ms\nZero Offset %2")).arg(QString::number(calibrationSpindownTime), QString::number(calibrationZeroOffset));;
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_FAILURE:
|
|
status = QString(tr("Calibration failed!"));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_FAILURE_SPINDOWN_TOO_FAST:
|
|
status = QString(tr("Calibration Failed: Loosen Roller"));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_FAILURE_SPINDOWN_TOO_SLOW:
|
|
status = QString(tr("Calibration Failed: Tighten Roller"));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case CALIBRATION_TYPE_ZERO_OFFSET:
|
|
|
|
switch (calibrationState) {
|
|
|
|
case CALIBRATION_STATE_IDLE:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_PENDING:
|
|
// Wait for cadence to be zero before requesting zero offset calibration
|
|
status = QString(tr("Unclip or stop pedalling to begin calibration.."));
|
|
if (calibrationCadence == 0)
|
|
startCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_REQUESTED:
|
|
status = QString(tr("Requesting calibration.."));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTING:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTED:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_POWER:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_COAST:
|
|
status = QString(tr("Calibrating...\nUnclip or stop pedalling until process is completed..\nTorque %1")).arg(QString::number(calibrationTorque, 'f', 3));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_SUCCESS:
|
|
// yuk, zero offset for FE-C devices is unsigned, but for power meters is signed..
|
|
status = QString(tr("Calibration completed successfully!\nZero Offset %1\nTorque %2")).arg(QString::number((int16_t)calibrationZeroOffset), QString::number(calibrationTorque, 'f', 3));;
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_FAILURE:
|
|
status = QString(tr("Calibration failed!"));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
}
|
|
break;
|
|
|
|
case CALIBRATION_TYPE_ZERO_OFFSET_SRM:
|
|
|
|
switch (calibrationState) {
|
|
|
|
case CALIBRATION_STATE_IDLE:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_PENDING:
|
|
// Wait for cadence to be zero before requesting zero offset calibration
|
|
status = QString(tr("Unclip or stop pedalling to begin calibration.."));
|
|
if (calibrationCadence == 0)
|
|
startCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_REQUESTED:
|
|
status = QString(tr("Requesting calibration.."));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTING:
|
|
status = QString(tr("Requesting calibration.."));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTED:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_POWER:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_COAST:
|
|
status = QString(tr("Calibrating...\nUnclip or stop pedalling until process is completed..\nZero Offset %1")).arg(QString::number((int16_t)calibrationZeroOffset));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_SUCCESS:
|
|
// yuk, zero offset for FE-C devices is unsigned, but for power meters is signed..
|
|
status = QString(tr("Calibration completed successfully!\nZero Offset %1\nSlope %2")).arg(QString::number((int16_t)calibrationZeroOffset), QString::number(calibrationSlope));;
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
case CALIBRATION_STATE_FAILURE:
|
|
status = QString(tr("Calibration failed!"));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
|
|
}
|
|
break;
|
|
|
|
#ifdef GC_HAVE_LIBUSB
|
|
case CALIBRATION_TYPE_FORTIUS:
|
|
|
|
switch (calibrationState) {
|
|
|
|
case CALIBRATION_STATE_IDLE:
|
|
case CALIBRATION_STATE_PENDING:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_REQUESTED:
|
|
if (calibrationZeroOffset == 0)
|
|
status = QString(tr("Give the pedal a kick to start calibration...\nThe motor will run until calibration is complete."));
|
|
else
|
|
status = QString(tr("Allow wheel speed to settle, DO NOT PEDAL...\nThe motor will run until calibration is complete."));
|
|
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTING:
|
|
status = QString(tr("Calibrating... DO NOT PEDAL, remain seated...\nGathering enough samples to calculate average: %1"))
|
|
.arg(QString::number((int16_t)calibrationZeroOffset));
|
|
break;
|
|
|
|
case CALIBRATION_STATE_STARTED:
|
|
{
|
|
const double calibrationPower_W = Fortius::rawForce_to_N(calibrationZeroOffset) * Fortius::kph_to_ms(calibrationTargetSpeed);
|
|
status = QString(tr("Calibrating... DO NOT PEDAL, remain seated...\nWaiting for value to stabilize (max %1s): %2 (%3W @ %4kph)"))
|
|
.arg(QString::number((int16_t)FortiusController::calibrationDurationLimit_s),
|
|
QString::number((int16_t)calibrationZeroOffset),
|
|
QString::number((int16_t)calibrationPower_W),
|
|
QString::number((int16_t)calibrationTargetSpeed));
|
|
}
|
|
break;
|
|
|
|
case CALIBRATION_STATE_POWER:
|
|
case CALIBRATION_STATE_COAST:
|
|
break;
|
|
|
|
case CALIBRATION_STATE_SUCCESS:
|
|
{
|
|
const double calibrationPower_W = Fortius::rawForce_to_N(calibrationZeroOffset) * Fortius::kph_to_ms(calibrationTargetSpeed);
|
|
status = QString(tr("Calibration completed successfully!\nFinal calibration value %1 (%2W @ %3kph)"))
|
|
.arg(QString::number((int16_t)calibrationZeroOffset),
|
|
QString::number((int16_t)calibrationPower_W),
|
|
QString::number((int16_t)calibrationTargetSpeed));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
}
|
|
break;
|
|
|
|
case CALIBRATION_STATE_FAILURE:
|
|
status = QString(tr("Calibration failed! Do not pedal while calibration is taking place.\nAllow wheel to run freely."));
|
|
|
|
// No further ANT messages to set state, so must move ourselves on..
|
|
if ((stateCount % 25) == 0)
|
|
finishCalibration = true;
|
|
break;
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
}
|
|
|
|
lastState = calibrationState;
|
|
|
|
// set notification text, no timeout
|
|
emit setNotification(status, 0);
|
|
}
|
|
}
|
|
|
|
void TrainSidebar::FFwd()
|
|
{
|
|
if (((status&RT_RUNNING) == 0) || (status&RT_PAUSED)) return;
|
|
|
|
if (status&RT_MODE_ERGO) {
|
|
// In ergo mode seek is of time.
|
|
load_msecs += 10000; // jump forward 10 seconds
|
|
context->notifySeek(load_msecs);
|
|
}
|
|
else {
|
|
// Otherwise Seek is of Distance.
|
|
double stepSize = 1.; // jump forward a kilometer in the workout
|
|
if (context->currentVideoSyncFile()) {
|
|
// If step would take us past the end then step to end.
|
|
double videoDistance = context->currentVideoSyncFile()->Distance;
|
|
if ((displayWorkoutDistance + stepSize) > videoDistance) {
|
|
stepSize = videoDistance - displayWorkoutDistance;
|
|
}
|
|
context->notifySeek(stepSize); // in case of video with RLV file synchronisation just ask to go forward
|
|
}
|
|
displayWorkoutDistance += stepSize;
|
|
}
|
|
|
|
resetTextAudioEmitTracking();
|
|
|
|
maintainLapDistanceState();
|
|
|
|
emit setNotification(tr("Fast forward.."), 2);
|
|
}
|
|
|
|
void TrainSidebar::Rewind()
|
|
{
|
|
if (((status&RT_RUNNING) == 0) || (status&RT_PAUSED)) return;
|
|
|
|
if (status&RT_MODE_ERGO) {
|
|
// In ergo mode seek is of time.
|
|
load_msecs -=10000; // jump back 10 seconds
|
|
if (load_msecs < 0) load_msecs = 0;
|
|
context->notifySeek(load_msecs);
|
|
}
|
|
else {
|
|
// Otherwise Seek is of distance.
|
|
double stepSize = -1.; // jump back a kilometer
|
|
|
|
// If step would take us before the start then step to the start.
|
|
if ((displayWorkoutDistance + stepSize) < 0) {
|
|
stepSize = -displayWorkoutDistance;
|
|
}
|
|
|
|
if (context->currentVideoSyncFile()) {
|
|
context->notifySeek(stepSize);
|
|
}
|
|
|
|
displayWorkoutDistance += stepSize;
|
|
}
|
|
|
|
resetTextAudioEmitTracking();
|
|
|
|
maintainLapDistanceState();
|
|
|
|
emit setNotification(tr("Rewind.."), 2);
|
|
}
|
|
|
|
|
|
// jump to next Lap marker (if there is one?)
|
|
void TrainSidebar::FFwdLap()
|
|
{
|
|
if (((status&RT_RUNNING) == 0) || (status&RT_PAUSED)) return;
|
|
|
|
double lapmarker;
|
|
|
|
if (status&RT_MODE_ERGO) {
|
|
lapmarker = ergFileQueryAdapter.nextLap(load_msecs);
|
|
if (lapmarker >= 0.) load_msecs = lapmarker; // jump forward to lapmarker
|
|
context->notifySeek(load_msecs);
|
|
} else {
|
|
static const double s_BeforeOffset = 10.1;
|
|
lapmarker = ergFileQueryAdapter.nextLap((displayWorkoutDistance*1000) + s_BeforeOffset);
|
|
if (lapmarker >= 0) {
|
|
// Go to slightly before lap marker so the lap transition message will be displayed.
|
|
lapmarker = std::max(0., lapmarker - s_BeforeOffset);
|
|
|
|
displayWorkoutDistance = lapmarker / 1000; // jump forward to lapmarker
|
|
}
|
|
}
|
|
|
|
resetTextAudioEmitTracking();
|
|
|
|
maintainLapDistanceState();
|
|
|
|
if (lapmarker >= 0) emit setNotification(tr("Next Lap.."), 2);
|
|
}
|
|
|
|
// jump to next Lap marker (if there is one?)
|
|
void TrainSidebar::RewindLap()
|
|
{
|
|
if (((status & RT_RUNNING) == 0) || (status & RT_PAUSED)) return;
|
|
|
|
double lapmarker;
|
|
|
|
if (status & RT_MODE_ERGO) {
|
|
// Search for lap prior to 1 second ago.
|
|
long target = std::max<long>(0, load_msecs - 1000);
|
|
lapmarker = ergFileQueryAdapter.prevLap(target);
|
|
if (lapmarker >= 0.) load_msecs = lapmarker; // jump to lapmarker
|
|
context->notifySeek(load_msecs);
|
|
}
|
|
else {
|
|
// Search for lap prior to 50 meters ago.
|
|
double target = std::max(0., (displayWorkoutDistance * 1000) - 50.);
|
|
|
|
lapmarker = ergFileQueryAdapter.prevLap(target);
|
|
|
|
// Go to slightly before lap marker so the lap transition message will be displayed.
|
|
lapmarker = std::max(0., lapmarker - 10.1);
|
|
|
|
if (lapmarker >= 0.) displayWorkoutDistance = lapmarker / 1000; // jump to lapmarker
|
|
}
|
|
|
|
resetTextAudioEmitTracking();
|
|
|
|
maintainLapDistanceState();
|
|
|
|
if (lapmarker >= 0) emit setNotification(tr("Back Lap.."), 2);
|
|
}
|
|
|
|
|
|
// higher load/gradient
|
|
void TrainSidebar::Higher()
|
|
{
|
|
if ((status&RT_CONNECTED) == 0) return;
|
|
|
|
if (context->currentErgFile()) {
|
|
// adjust the workout IF
|
|
adjustIntensity(lastAppliedIntensity+5);
|
|
|
|
} else {
|
|
if (status&RT_MODE_ERGO) load += 5;
|
|
else slope += 0.1;
|
|
|
|
if (load >1500) load = 1500;
|
|
if (slope >40) slope = 40;
|
|
|
|
if (status&RT_MODE_ERGO)
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setLoad(load);
|
|
else
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setGradient(slope);
|
|
}
|
|
|
|
emit setNotification(tr("Increasing intensity.."), 2);
|
|
}
|
|
|
|
// lower load/gradient
|
|
void TrainSidebar::Lower()
|
|
{
|
|
if ((status&RT_CONNECTED) == 0) return;
|
|
|
|
if (context->currentErgFile()) {
|
|
// adjust the workout IF
|
|
adjustIntensity(std::max<int>(5, lastAppliedIntensity - 5));
|
|
|
|
} else {
|
|
|
|
if (status&RT_MODE_ERGO) load -= 5;
|
|
else slope -= 0.1;
|
|
|
|
if (load <0) load = 0;
|
|
if (slope <-40) slope = -40;
|
|
|
|
if (status&RT_MODE_ERGO)
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setLoad(load);
|
|
else
|
|
foreach(int dev, activeDevices) Devices[dev].controller->setGradient(slope);
|
|
}
|
|
|
|
emit setNotification(tr("Decreasing intensity.."), 2);
|
|
}
|
|
|
|
void TrainSidebar::setLabels()
|
|
{
|
|
/* should this be kept, or removed? Currently these are always hidden.
|
|
if (context->currentErgFile()) {
|
|
|
|
if (context->currentErgFile()->format == CRS) {
|
|
|
|
stress->setText(QString("Elevation %1").arg(context->currentErgFile()->ELE, 0, 'f', 0));
|
|
intensity->setText(QString("Grade %1 %").arg(context->currentErgFile()->GRADE, 0, 'f', 1));
|
|
|
|
} else {
|
|
|
|
stress->setText(QString("BikeStress %1").arg(context->currentErgFile()->BikeStress, 0, 'f', 0));
|
|
intensity->setText(QString("IF %1").arg(context->currentErgFile()->IF, 0, 'f', 3));
|
|
}
|
|
|
|
} else {
|
|
stress->setText("");
|
|
intensity->setText("");
|
|
}
|
|
*/
|
|
}
|
|
|
|
void TrainSidebar::adjustIntensity(int value)
|
|
{
|
|
if (value == lastAppliedIntensity) {
|
|
return;
|
|
}
|
|
|
|
ErgFile* ergFile = const_cast<ErgFile*>(ergFileQueryAdapter.getErgFile());
|
|
if (!ergFile) return; // no workout selected
|
|
|
|
// block signals temporarily
|
|
context->mainWindow->blockSignals(true);
|
|
|
|
// work through the ergFile from NOW
|
|
// adjusting back from last setting
|
|
// and increasing to new intensity setting
|
|
|
|
double from = double(lastAppliedIntensity) / 100.00;
|
|
double to = double(value) / 100.00;
|
|
|
|
lastAppliedIntensity = value;
|
|
|
|
long starttime = context->getNow();
|
|
|
|
bool insertedNow = context->getNow() ? false : true; // don't add if at start
|
|
|
|
// what about gradient courses?
|
|
ErgFilePoint last;
|
|
for(int i = 0; i < ergFile->Points.count(); i++) {
|
|
|
|
if (ergFile->Points.at(i).x >= starttime) {
|
|
|
|
if (insertedNow == false) {
|
|
|
|
if (i) {
|
|
// add a point to adjust from
|
|
|
|
// This pass simply modifies load or gradient.
|
|
// Start with copy of 'last', then overwrite only the part we wish to change,
|
|
// this is necessary so crs point will start with an intact location and not
|
|
// zeros.
|
|
ErgFilePoint add = last;
|
|
add.x = context->getNow();
|
|
add.val = last.val / from * to;
|
|
|
|
// recalibrate altitude if gradient changing
|
|
if (ergFile->format == CRS) add.y = last.y + ((add.x-last.x) * (add.val/100));
|
|
else add.y = add.val;
|
|
|
|
ergFile->Points.insert(i, add);
|
|
|
|
last = add;
|
|
i++; // move on to next point (i.e. where we were!)
|
|
}
|
|
insertedNow = true;
|
|
}
|
|
|
|
ErgFilePoint *p = &ergFile->Points[i];
|
|
|
|
// recalibrate altitude if in CRS mode
|
|
p->val = p->val / from * to;
|
|
if (ergFile->format == CRS) {
|
|
if (i) p->y = last.y + ((p->x-last.x) * (last.val/100));
|
|
}
|
|
else p->y = p->val;
|
|
}
|
|
|
|
// remember last
|
|
last = ergFile->Points.at(i);
|
|
}
|
|
|
|
// recalculate metrics
|
|
ergFile->calculateMetrics();
|
|
setLabels();
|
|
|
|
// Ergfile points have been edited so reset interpolation and
|
|
// query state.
|
|
ergFileQueryAdapter.resetQueryState();
|
|
|
|
// unblock signals now we are done
|
|
context->mainWindow->blockSignals(false);
|
|
|
|
// force replot
|
|
context->notifySetNow(context->getNow());
|
|
|
|
emit intensityChanged(lastAppliedIntensity);
|
|
}
|
|
|
|
MultiDeviceDialog::MultiDeviceDialog(Context *, TrainSidebar *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(tr("Cancel"), this);
|
|
buttons->addWidget(cancelButton);
|
|
|
|
applyButton = new QPushButton(tr("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
|
|
TrainSidebar::devicePopup()
|
|
{
|
|
// OK - we are working with a specific event..
|
|
QMenu menu(deviceTree);
|
|
|
|
QAction *addDevice = new QAction(tr("Add Device"), deviceTree);
|
|
connect(addDevice, SIGNAL(triggered(void)), context->mainWindow, 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);
|
|
}
|
|
|
|
// execute the menu
|
|
menu.exec(trainSplitter->mapToGlobal(QPoint(deviceItem->pos().x()+deviceItem->width()-20,
|
|
deviceItem->pos().y())));
|
|
}
|
|
void
|
|
TrainSidebar::deviceTreeMenuPopup(const QPoint &pos)
|
|
{
|
|
QMenu menu(deviceTree);
|
|
QAction *addDevice = new QAction(tr("Add Device"), deviceTree);
|
|
connect(addDevice, SIGNAL(triggered(void)), context->mainWindow, 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
|
|
TrainSidebar::deleteDevice()
|
|
{
|
|
// get the configuration
|
|
DeviceConfigurations all;
|
|
QList<DeviceConfiguration>list = all.getList();
|
|
|
|
// Delete the selected device
|
|
QTreeWidgetItem *selected = deviceTree->selectedItems().first();
|
|
int index = deviceTree->invisibleRootItem()->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
|
|
context->notifyConfigChanged(CONFIG_DEVICES);
|
|
}
|
|
|
|
void
|
|
TrainSidebar::moveDevices(int oldposition, int newposition)
|
|
{
|
|
// get the configuration
|
|
DeviceConfigurations all;
|
|
QList<DeviceConfiguration>list = all.getList();
|
|
|
|
// move the devices
|
|
list.move(oldposition, newposition);
|
|
all.writeConfig(list);
|
|
|
|
// tell everyone
|
|
context->notifyConfigChanged(CONFIG_DEVICES);
|
|
}
|
|
|
|
// we have been told to select this video (usually because
|
|
// the user just dragndropped it in)
|
|
void
|
|
TrainSidebar::selectVideo(QString fullpath)
|
|
{
|
|
// look at each entry in the top workoutTree
|
|
for (int i=0; i<mediaTree->model()->rowCount(); i++) {
|
|
QString path = mediaTree->model()->data(mediaTree->model()->index(i,0)).toString();
|
|
if (path == fullpath) {
|
|
mediaTree->setCurrentIndex(mediaTree->model()->index(i,0));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
TrainSidebar::selectVideoSync(QString fullpath)
|
|
{
|
|
// look at each entry in the top videosyncTree
|
|
for (int i=0; i<videosyncTree->model()->rowCount(); i++) {
|
|
QString path = videosyncTree->model()->data(videosyncTree->model()->index(i,0)).toString();
|
|
if (path == fullpath) {
|
|
videosyncTree->setCurrentIndex(videosyncTree->model()->index(i,0));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// we have been told to select this workout (usually because
|
|
// the user just dragndropped it in)
|
|
void
|
|
TrainSidebar::selectWorkout(QString fullpath)
|
|
{
|
|
// look at each entry in the top workoutTree
|
|
for (int i=0; i<workoutTree->model()->rowCount(); i++) {
|
|
QString path = workoutTree->model()->data(workoutTree->model()->index(i,0)).toString();
|
|
if (path == fullpath) {
|
|
workoutTree->setCurrentIndex(workoutTree->model()->index(i,0));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// got a remote control command
|
|
void
|
|
TrainSidebar::remoteControl(uint16_t command)
|
|
{
|
|
//qDebug() << "TrainSidebar::remoteControl()" << command;
|
|
|
|
switch(command){
|
|
|
|
case GC_REMOTE_CMD_START:
|
|
this->Start();
|
|
break;
|
|
|
|
case GC_REMOTE_CMD_STOP:
|
|
this->Stop();
|
|
break;
|
|
|
|
case GC_REMOTE_CMD_LAP:
|
|
this->newLap();
|
|
break;
|
|
|
|
case GC_REMOTE_CMD_HIGHER:
|
|
this->Higher();
|
|
break;
|
|
|
|
case GC_REMOTE_CMD_LOWER:
|
|
this->Lower();
|
|
break;
|
|
|
|
case GC_REMOTE_CMD_CALIBRATE:
|
|
this->Calibrate();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// HRV R-R data received
|
|
void TrainSidebar::rrData(uint16_t rrtime, uint8_t count, uint8_t bpm)
|
|
{
|
|
Q_UNUSED(count)
|
|
|
|
QMutexLocker locker(&rrMutex);
|
|
|
|
if (status&RT_RECORDING && rrFile == NULL && recordFile != NULL) {
|
|
QString rrfile = recordFile->fileName().replace("csv", "rr");
|
|
//fprintf(stderr, "First r-r, need to open file %s\n", rrfile.toStdString().c_str()); fflush(stderr);
|
|
|
|
// setup the rr file
|
|
rrFile = new QFile(rrfile);
|
|
if (!rrFile->open(QFile::WriteOnly | QFile::Truncate)) {
|
|
delete rrFile;
|
|
rrFile=NULL;
|
|
} else {
|
|
|
|
// CSV File header
|
|
QTextStream recordFileStream(rrFile);
|
|
recordFileStream << "secs, hr, msecs\n";
|
|
}
|
|
}
|
|
|
|
// output a line if recording and file ready
|
|
if (status&RT_RECORDING && rrFile) {
|
|
QTextStream recordFileStream(rrFile);
|
|
|
|
// convert from milliseconds to secondes
|
|
double secs = double(session_elapsed_msec + session_time.elapsed()) / 1000.00;
|
|
|
|
// output a line
|
|
recordFileStream << secs << ", " << bpm << ", " << rrtime << "\n";
|
|
}
|
|
//fprintf(stderr, "R-R: %d ms, HR=%d, count=%d\n", rrtime, bpm, count); fflush(stderr);
|
|
}
|
|
|
|
// VO2 Measurement data received
|
|
void TrainSidebar::vo2Data(double rf, double rmv, double vo2, double vco2, double tv, double feo2)
|
|
{
|
|
QMutexLocker locker(&vo2Mutex);
|
|
|
|
if (status&RT_RECORDING && vo2File == NULL && recordFile != NULL) {
|
|
QString vo2filename = recordFile->fileName().replace("csv", "vo2");
|
|
|
|
// setup the rr file
|
|
vo2File = new QFile(vo2filename);
|
|
if (!vo2File->open(QFile::WriteOnly | QFile::Truncate)) {
|
|
delete vo2File;
|
|
vo2File=NULL;
|
|
} else {
|
|
|
|
// CSV File header
|
|
QTextStream recordFileStream(vo2File);
|
|
recordFileStream << "secs, rf, rmv, vo2, vco2, tv, feo2\n";
|
|
}
|
|
}
|
|
|
|
// output a line if recording and file ready
|
|
if (status&RT_RECORDING && vo2File) {
|
|
QTextStream recordFileStream(vo2File);
|
|
|
|
// convert from milliseconds to secondes
|
|
double secs = double(session_elapsed_msec + session_time.elapsed()) / 1000.00;
|
|
|
|
// output a line
|
|
recordFileStream << secs << ", " << rf << ", " << rmv << ", " << vo2 << ", " << vco2 << ", " << tv << ", " << feo2 << "\n";
|
|
}
|
|
}
|
|
|
|
// connect/disconnect automatically when view changes
|
|
void
|
|
TrainSidebar::viewChanged(int index)
|
|
{
|
|
//qDebug() << "view has changed to:" << index;
|
|
|
|
// ensure buttons reflect current state
|
|
setStatusFlags(0);
|
|
|
|
if (!autoConnect) return;
|
|
|
|
// fixme: value hard-coded throughout
|
|
if (index == 3) {
|
|
//train view
|
|
if ((status&RT_CONNECTED) == 0) {
|
|
Connect();
|
|
}
|
|
} else {
|
|
// other view
|
|
if ((status&RT_CONNECTED) && (status&RT_RUNNING) == 0) {
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void TrainSidebar::setStatusFlags(int flags)
|
|
{
|
|
status |= flags;
|
|
context->isRunning = (status&RT_RUNNING);
|
|
context->isPaused = (status&RT_PAUSED);
|
|
|
|
emit statusChanged(status);
|
|
}
|
|
|
|
void TrainSidebar::clearStatusFlags(int flags)
|
|
{
|
|
status &=~flags;
|
|
context->isRunning = (status&RT_RUNNING);
|
|
context->isPaused = (status&RT_PAUSED);
|
|
|
|
emit statusChanged(status);
|
|
}
|
|
|
|
int TrainSidebar::getCalibrationIndex()
|
|
{
|
|
// select the least recently calibrated GC device that reports calibration capabilities.
|
|
//
|
|
// note that for for ANT devices, the calibration class handles the case of multiple sensors
|
|
// with support for calibration, and will select a suitable device/channel for calibration each time.
|
|
|
|
int index = -1;
|
|
QTime lastCal = QTime::currentTime();
|
|
|
|
foreach(int dev, devices()) {
|
|
if (Devices[dev].controller->getCalibrationType()) {
|
|
// device supports calibration
|
|
//qDebug() << "Device" << dev << "supports calibration, last cal attempt timestamp is" << Devices[dev].controller->getCalibrationTimestamp();
|
|
if (Devices[dev].controller->getCalibrationTimestamp() < lastCal) {
|
|
// older (or no) calibration timestamp, select this device
|
|
lastCal = Devices[dev].controller->getCalibrationTimestamp();
|
|
index = dev;
|
|
}
|
|
}
|
|
}
|
|
if (index != -1)
|
|
Devices[index].controller->setCalibrationTimestamp();
|
|
|
|
return index;
|
|
}
|
|
|
|
DeviceTreeView::DeviceTreeView()
|
|
{
|
|
setDragDropMode(QAbstractItemView::InternalMove);
|
|
setDragDropOverwriteMode(true);
|
|
}
|
|
|
|
void
|
|
DeviceTreeView::dropEvent(QDropEvent* event)
|
|
{
|
|
// get the list of the items that are about to be dropped
|
|
QTreeWidgetItem* item = selectedItems()[0];
|
|
|
|
// row we started on
|
|
int idx1 = indexFromItem(item).row();
|
|
|
|
// the default implementation takes care of the actual move inside the tree
|
|
QTreeWidget::dropEvent(event);
|
|
|
|
// moved to !
|
|
int idx2 = indexFromItem(item).row();
|
|
|
|
// notify subscribers in some useful way
|
|
Q_EMIT itemMoved(idx1, idx2);
|
|
}
|