/* * 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 #include #include #include #include #include #include #include #include #include // 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 #include #include #import #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; 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; iheader()->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; iheader()->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; iheader()->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->setSelectionMode(QAbstractItemView::ExtendedSelection); 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); trainSplitter->addWidget(deviceItem); workoutItem->addWidget(workoutTree); 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); 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); 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); session_time = QTime(); session_elapsed_msec = 0; lap_time = QTime(); lap_elapsed_msec = 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())); 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 0 if (event->type() == QEvent::KeyPress) { // we care about cmd / ctrl Qt::KeyboardModifiers kmod = static_cast(event)->modifiers(); bool ctrl = (kmod & Qt::ControlModifier) != 0; Q_UNUSED(ctrl); // what was pressed int key =static_cast(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) { //XXX TODO default: break; } return true; // we listen to 'em all } #endif } 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()); #ifdef GC_HAVE_VLC // RLV currently only support for VLC videosyncTree->setStyleSheet(GCColor::stylesheet()); #endif #endif workoutTree->setStyleSheet(GCColor::stylesheet()); deviceTree->setStyleSheet(GCColor::stylesheet()); // DEVICES // Disconnect any running telemetry before manipulating device list Disconnect(); // zap whats there QList devices = deviceTree->invisibleRootItem()->takeChildren(); for (int i=0; ivalue(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; iinvisibleRootItem(), 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(false)->whichRange(QDate::currentDate()); if (range != -1) { FTP = context->athlete->zones(false)->getCP(range); WPRIME = context->athlete->zones(false)->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 TrainSidebar::devices() { QList 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(ergFileQueryAdapter.getErgFile()); 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(); } 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; } /* * 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;istartLUW(); 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) context->notifyMediaSelected(""); // CTRL+Click to clear selection else 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) { qDebug() << "start..."; #ifdef WIN32 // disable the screen saver on Windows SetThreadExecutionState(ES_DISPLAY_REQUIRED | ES_CONTINUOUS); #endif // 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) { QDateTime now = QDateTime::currentDateTime(); // setup file QString filename = now.toString(QString("yyyy_MM_dd_hh_mm_ss")) + 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); //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(); // 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; } if(deviceStatus == DEVICE_ERROR) { recordFile->remove(); } else { // add to the view - using basename ONLY QString name; name = recordFile->fileName(); QList list; list.append(name); RideImportWizard *dialog = new RideImportWizard (list, context); dialog->process(); // do it! } // cancel recording status &= ~RT_RECORDING; } 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->setWindResistance(bicycle.WindResistance()); 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() { // 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) { // For classic rlv with no location data: // 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.setSlope(slope); 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 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 { 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); 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; case CALIBRATION_TYPE_FORTIUS: switch (calibrationState) { case CALIBRATION_STATE_IDLE: case CALIBRATION_STATE_PENDING: break; case CALIBRATION_STATE_REQUESTED: status = QString(tr("Give the pedal a kick to start calibration...\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; } 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(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(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(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; QListlist = 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; QListlist = 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; imodel()->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; imodel()->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; imodel()->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) 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) { 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); }