From 82ac0f5e1f1ee58ab6de40afa394109c7a67d9f7 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Mon, 23 Nov 2009 10:42:48 -0500 Subject: [PATCH] add realtime mode Joint work between Mark L, Justin, and Steve Gribble. --- src/ANTplusController.cpp | 91 +++ src/ANTplusController.h | 52 ++ src/Computrainer.cpp | 982 ++++++++++++++++++++++++++++ src/Computrainer.h | 207 ++++++ src/ComputrainerController.cpp | 159 +++++ src/ComputrainerController.h | 53 ++ src/ConfigDialog.cpp | 140 ++++ src/ConfigDialog.h | 10 + src/DeviceConfiguration.cpp | 143 ++++ src/DeviceConfiguration.h | 58 ++ src/DeviceTypes.cpp | 61 ++ src/DeviceTypes.h | 61 ++ src/ErgFile.cpp | 361 ++++++++++ src/ErgFile.h | 88 +++ src/ErgFilePlot.cpp | 121 ++++ src/ErgFilePlot.h | 83 +++ src/MainWindow.cpp | 7 +- src/MainWindow.h | 10 +- src/Pages.cpp | 340 +++++++++- src/Pages.h | 80 +++ src/QuarqdClient.cpp | 413 ++++++++++++ src/QuarqdClient.h | 83 +++ src/RealtimeController.cpp | 36 + src/RealtimeController.h | 58 ++ src/RealtimeData.cpp | 77 +++ src/RealtimeData.h | 51 ++ src/RealtimePlot.cpp | 174 +++++ src/RealtimePlot.h | 129 ++++ src/RealtimeWindow.cpp | 863 ++++++++++++++++++++++++ src/RealtimeWindow.h | 187 ++++++ src/Settings.h | 11 + src/SimpleNetworkClient.cpp | 257 ++++++++ src/SimpleNetworkClient.h | 112 ++++ src/SimpleNetworkController.cpp | 107 +++ src/SimpleNetworkController.h | 109 +++ src/application.qrc | 1 + src/images/arduino.png | Bin 0 -> 16355 bytes src/main.cpp | 9 +- src/simpleserver.py | 219 +++++++ src/src.pro | 30 +- src/test/workouts/Hausach48km.crs | 355 ++++++++++ src/test/workouts/HourofPowerV1.erg | 388 +++++++++++ src/test/workouts/IsoL4hour.mrc | 20 + src/test/workouts/Kilo.crs | 18 + src/test/workouts/L345Pyramid.erg | 93 +++ src/test/workouts/RampTest.erg | 24 + src/test/workouts/RoadTempo2hrs.erg | 310 +++++++++ 47 files changed, 7233 insertions(+), 8 deletions(-) create mode 100644 src/ANTplusController.cpp create mode 100644 src/ANTplusController.h create mode 100644 src/Computrainer.cpp create mode 100644 src/Computrainer.h create mode 100644 src/ComputrainerController.cpp create mode 100644 src/ComputrainerController.h create mode 100644 src/DeviceConfiguration.cpp create mode 100644 src/DeviceConfiguration.h create mode 100644 src/DeviceTypes.cpp create mode 100644 src/DeviceTypes.h create mode 100644 src/ErgFile.cpp create mode 100644 src/ErgFile.h create mode 100644 src/ErgFilePlot.cpp create mode 100644 src/ErgFilePlot.h create mode 100644 src/QuarqdClient.cpp create mode 100644 src/QuarqdClient.h create mode 100644 src/RealtimeController.cpp create mode 100644 src/RealtimeController.h create mode 100644 src/RealtimeData.cpp create mode 100644 src/RealtimeData.h create mode 100644 src/RealtimePlot.cpp create mode 100644 src/RealtimePlot.h create mode 100644 src/RealtimeWindow.cpp create mode 100644 src/RealtimeWindow.h create mode 100644 src/SimpleNetworkClient.cpp create mode 100644 src/SimpleNetworkClient.h create mode 100644 src/SimpleNetworkController.cpp create mode 100644 src/SimpleNetworkController.h create mode 100644 src/images/arduino.png create mode 100644 src/simpleserver.py create mode 100644 src/test/workouts/Hausach48km.crs create mode 100644 src/test/workouts/HourofPowerV1.erg create mode 100644 src/test/workouts/IsoL4hour.mrc create mode 100644 src/test/workouts/Kilo.crs create mode 100644 src/test/workouts/L345Pyramid.erg create mode 100644 src/test/workouts/RampTest.erg create mode 100644 src/test/workouts/RoadTempo2hrs.erg diff --git a/src/ANTplusController.cpp b/src/ANTplusController.cpp new file mode 100644 index 000000000..86c13b72d --- /dev/null +++ b/src/ANTplusController.cpp @@ -0,0 +1,91 @@ +/* + * 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 +#include "ANTplusController.h" +#include "QuarqdClient.h" +#include "RealtimeData.h" + +ANTplusController::ANTplusController(RealtimeWindow *parent, DeviceConfiguration *dc) : RealtimeController(parent) +{ + myANTplus = new QuarqdClient (parent, dc); +} + + +int +ANTplusController::start() +{ + myANTplus->start(); + return 0; +} + + +int +ANTplusController::restart() +{ + return myANTplus->restart(); +} + + +int +ANTplusController::pause() +{ + return myANTplus->pause(); +} + + +int +ANTplusController::stop() +{ + return myANTplus->stop(); +} + + +bool +ANTplusController::discover(DeviceConfiguration *dc, QProgressDialog *progress) +{ + return myANTplus->discover(dc, progress); +} + + +bool ANTplusController::doesPush() { return false; } +bool ANTplusController::doesPull() { return true; } +bool ANTplusController::doesLoad() { return false; } + +/* + * gets called from the GUI to get updated telemetry. + * so whilst we are at it we check button status too and + * act accordingly. + * + */ +void +ANTplusController::getRealtimeData(RealtimeData &rtData) +{ + if(!myANTplus->isRunning()) + { + QMessageBox msgBox; + msgBox.setText("Cannot Connect to Quarqd"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.exec(); + parent->Stop(); + } + // get latest telemetry + rtData = myANTplus->getRealtimeData(); +} + +void ANTplusController::pushRealtimeData(RealtimeData &) { } // update realtime data with current values diff --git a/src/ANTplusController.h b/src/ANTplusController.h new file mode 100644 index 000000000..20fa67292 --- /dev/null +++ b/src/ANTplusController.h @@ -0,0 +1,52 @@ +/* + * 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 "RealtimeController.h" +#include "DeviceConfiguration.h" +#include "QuarqdClient.h" +#include "ConfigDialog.h" + +// Abstract base class for Realtime device controllers + + +#ifndef _GC_ANTplusController_h +#define _GC_ANTplusController_h 1 + +class ANTplusController : public RealtimeController +{ + +public: + ANTplusController (RealtimeWindow *parent =0, DeviceConfiguration *dc =0); + + QuarqdClient *myANTplus; // the device itself + + int start(); + int restart(); // restart after paused + int pause(); // pauses data collection, inbound telemetry is discarded + int stop(); // stops data collection thread + bool discover(DeviceConfiguration *dc =0, QProgressDialog *progress = 0); // tell if a device is present at the address/port passed + // port is specified as ipname:port e.g. 192.168.2.1:8168 + // telemetry push pull + bool doesPush(), doesPull(), doesLoad(); + void getRealtimeData(RealtimeData &rtData); + void pushRealtimeData(RealtimeData &rtData); + void setLoad(double) { return; } +}; + +#endif // _GC_ANTplusController_h + diff --git a/src/Computrainer.cpp b/src/Computrainer.cpp new file mode 100644 index 000000000..0599ac291 --- /dev/null +++ b/src/Computrainer.cpp @@ -0,0 +1,982 @@ +/* + * Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net), + * 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 + */ + +// ISSUES +// ====== +// 1. SpinScan decoding is not implemented (but erg and slope mode are) +// 2. Some C-style casts used for expediency + +#include "Computrainer.h" + +const static uint8_t ergo_command[56] = { +// Ergo various +// crc - - mode cmd val bits +// ---- ---- ---- ---- ---- ---- ---- + 0x6D, 0x00, 0x00, 0x0A, 0x08, 0x00, 0xE0, + 0x65, 0x00, 0x00, 0x0A, 0x10, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x0A, 0x18, 0x5D, 0xC1, + 0x33, 0x00, 0x00, 0x0A, 0x24, 0x1E, 0xE0, + 0x6A, 0x00, 0x00, 0x0A, 0x2C, 0x5F, 0xE0, + 0x41, 0x00, 0x00, 0x0A, 0x34, 0x00, 0xE0, + 0x2D, 0x00, 0x00, 0x0A, 0x38, 0x10, 0xC2, + 0x03, 0x00, 0x00, 0x0A, 0x40, 0x32, 0xE0 // set LOAD command +}; + +const static uint8_t ss_command[56] = { +// Spinscan various +// crc - - mode cmd val bits +// ---- ---- ---- ---- ---- ---- ---- + 0x61, 0x00, 0x00, 0x16, 0x08, 0x00, 0xE0, // set GRADIENT command + 0x59, 0x00, 0x00, 0x16, 0x10, 0x00, 0xE0, // set WINDSPEED (?) + 0x74, 0x00, 0x00, 0x16, 0x18, 0x5D, 0xC1, + 0x27, 0x00, 0x00, 0x16, 0x24, 0x1E, 0xE0, + 0x5E, 0x00, 0x00, 0x16, 0x2C, 0x5F, 0xE0, + 0x35, 0x00, 0x00, 0x16, 0x34, 0x00, 0xE0, + 0x21, 0x00, 0x00, 0x16, 0x38, 0x10, 0xC2, + 0x29, 0x00, 0x00, 0x16, 0x40, 0x00, 0xE0 +}; + +/* ---------------------------------------------------------------------- + * CONSTRUCTOR/DESRTUCTOR + * ---------------------------------------------------------------------- */ +Computrainer::Computrainer(QObject *parent, QString devname) : QThread(parent) +{ + + devicePower = deviceHeartRate = deviceCadence = deviceSpeed = deviceRRC = 0.00; + for (int i=0; i<24; spinData[i++] = 0.00) ; + mode = DEFAULT_MODE; + load = DEFAULT_LOAD; + gradient = DEFAULT_GRADIENT; + deviceCalibrated = false; + deviceHRConnected = false; + deviceCADConnected = false; + setDevice(devname); + deviceStatus=0; + this->parent = parent; + + /* 56 byte control sequence, composed of 8 command packets + * where the last packet sets the load. The first byte + * is a CRC for the value being issued (e.g. Load in WATTS) + * + * these members are modified as load / gradient are set + */ + memcpy(ERGO_Command, ergo_command, 56); + memcpy(SS_Command, ss_command, 56); +} + +Computrainer::~Computrainer() +{ +} + +/* ---------------------------------------------------------------------- + * SET + * ---------------------------------------------------------------------- */ +void Computrainer::setDevice(QString devname) +{ + // if not null, replace existing if set, otherwise set + deviceFilename = devname; +} + +void Computrainer::setMode(int mode, double load, double gradient) +{ + pvars.lock(); + this->mode = mode; + this->load = load; + this->gradient = gradient; + pvars.unlock(); +} + +void Computrainer::setLoad(double load) +{ + pvars.lock(); + if (load > 1500) load = 1500; + if (load < 50) load = 50; + this->load = load; + pvars.unlock(); +} + +void Computrainer::setGradient(double gradient) +{ + pvars.lock(); + this->gradient = gradient; + pvars.unlock(); +} + + +/* ---------------------------------------------------------------------- + * GET + * ---------------------------------------------------------------------- */ +bool Computrainer::isHRConnected() +{ + bool tmp; + pvars.lock(); + tmp = deviceHRConnected; + pvars.unlock(); + + return tmp; +} + +bool Computrainer::isCADConnected() +{ + bool tmp; + pvars.lock(); + tmp = deviceCADConnected; + pvars.unlock(); + + return tmp; +} + +bool Computrainer::isCalibrated() +{ + bool tmp; + pvars.lock(); + tmp = deviceCalibrated; + pvars.unlock(); + + return tmp; +} + +void Computrainer::getTelemetry(double &power, double &heartrate, double &cadence, double &speed, + double &RRC, bool &calibration, int &buttons, int &status) +{ + + pvars.lock(); + power = devicePower; + heartrate = deviceHeartRate; + cadence = deviceCadence; + speed = deviceSpeed; + RRC = deviceRRC; + calibration = deviceCalibrated; + buttons = deviceButtons; + status = deviceStatus; + pvars.unlock(); +} + +void Computrainer::getSpinScan(double spinData[]) +{ + pvars.lock(); + for (int i=0; i<24; spinData[i] = this->spinData[i]) ; + pvars.unlock(); +} + +int Computrainer::getMode() +{ + int tmp; + pvars.lock(); + tmp = mode; + pvars.unlock(); + return tmp; +} + +double Computrainer::getLoad() +{ + double tmp; + pvars.lock(); + tmp = load; + pvars.unlock(); + return tmp; +} + +double Computrainer::getGradient() +{ + double tmp; + pvars.lock(); + tmp = gradient; + pvars.unlock(); + return tmp; +} + +/*---------------------------------------------------------------------- + * COMPUTRAINER PROTOCOL DECODE/ENCODE ROUTINES + * (The clever stuff) + * + * Many thanks to Stephan Mantler for sharing some of his initial + * findings and source code. + * + * -- NOTE -- + * + * ALTHOUGH THESE ROUTINES READ AND WRITE FROM/TO THE LOW LEVEL + * IO BUFFERS THEY *DO NOT* WRITE TO THE CLASS MEMBERS DIRECTLY + * THIS IS TO MINIMISE RACE CONDITIONS. THE EXECUTIVE FUNCTIONS + * ESPECIALLY run() CONTROL ACCESS TO THESE AND CACHE VALUES IN + * LOCAL VARS TO MINIMISE THIS. + * + * ** IF YOU MODIFY THIS CODE PLEASE BEAR THIS IN MIND ** + * + * The protocol definition and reference code has been put up at + * https://www.sourceforge.net/projects/ctmac + * + * void prepareCommand() - sets up the command packet according to current settings + * int calcCRC(double value) - calculates the checksum for the current command + * int unpackTelemetry() - unpacks from CT protocol layout to byte layout + * returns non-zero if unknown message type + * + *---------------------------------------------------------------------- */ + +int +Computrainer::start() +{ + QThread::start(); + return 0; +} + +void Computrainer::prepareCommand(int mode, double value) +{ + // prepare the control message according to the current mode and gradient/load + int load, gradient, crc; + + switch (mode) { + + case CT_ERGOMODE : + + load = (int)value; + crc = calcCRC(load); + + // BYTE 0 - 49 is b0, 53 is b4, 54 is b5, 55 is b6 + ERGO_Command[49] = crc >> 1; // set byte 0 + + // BYTE 4 - command and highbyte + ERGO_Command[53] = 0x40; // set command + ERGO_Command[53] |= (load&(2048+1024+512)) >> 9; + + // BYTE 5 - low 7 + ERGO_Command[54] = 0; + ERGO_Command[54] |= (load&(128+64+32+16+8+4+2)) >> 1; + + // BYTE 6 - sync + z set + ERGO_Command[55] = 128+64; + + // low bit of supplement in bit 6 (32) + ERGO_Command[55] |= crc & 1 ? 32 : 0; + // Bit 2 (0x02) is low bit of high byte in load (bit 9 0x256) + ERGO_Command[55] |= (load&256) ? 2 : 0; + // Bit 1 (0x01) is low bit of low byte in load (but 1 0x01) + ERGO_Command[55] |= load&1; + + break; + + case CT_SSMODE : + + if (value < -9.9) gradient = -99; + else if (value > 15) gradient = 150; + else gradient = value *10; // gradient is passed as an integer + + // negative gradients use two's complement with bit 2048 set + if (gradient < 0) { + gradient *= -1; // make it positive + gradient &= 1024+512+256+128+64+32+16+8+4+2+1; // the number + gradient = ~gradient; // two's complement + } + + crc = calcCRC(gradient); + + // BYTE 0 - 49 is b0, 53 is b4, 54 is b5, 55 is b6 + SS_Command[0] = crc >> 1; // set byte 0 + + // BYTE 4 - command and highbyte + SS_Command[4] = 0x08; // set command - slope set + SS_Command[4] |= (gradient&(2048+1024+512)) >> 9; + + // BYTE 5 - low 7 + SS_Command[5] = 0; + SS_Command[5] |= (gradient&(128+64+32+16+8+4+2)) >> 1; + + // BYTE 6 - sync + z set + SS_Command[6] = 128+64; + + // low bit of supplement in bit 6 (32) + SS_Command[6] |= crc & 1 ? 32 : 0; + // Bit 2 (0x02) is low bit of high byte in load (bit 9 0x256) + SS_Command[6] |= (gradient&256) ? 2 : 0; + // Bit 1 (0x01) is low bit of low byte in load (but 1 0x01) + SS_Command[6] |= gradient&1; + break; + + } +} + +// thanks to Sean Rhea for working this one out! +int Computrainer::calcCRC(int value) +{ + return (0xff & (107 - (value & 0xff) - (value >> 8))); +} + + +// funny, just a few lines of code. oh the pain to get this working :-) +void Computrainer::unpackTelemetry(int &ss1, int &ss2, int &ss3, int &buttons, int &type, int &value8, int &value12) +{ + /* ---- looking at spinscan data -- commented out for release + static int ss[24]; + static int pos=0; + ----- */ + + // inbound data is in the 7 byte array Computrainer::buf[] + // for code clarity they hjave been put into these holdiing + // variables. the overhead is minimal and makes the code a + // lot easier to decipher! :-) + + short s1 = buf[0]; // ss data + short s2 = buf[1]; // ss data + short s3 = buf[2]; // ss data + short bt = buf[3]; // button data + short b1 = buf[4]; // message and value + short b2 = buf[5]; // value + short b3 = buf[6]; // the dregs (sync, z and lsb for all the others) + + // ss vars + ss1 = s1<<1 | (b3&32)>>5; + ss2 = s2<<1 | (b3&16)>>4; + ss3 = s3<<1 | (b3&8)>>3; + + + // buttons + buttons = bt<<1 | (b3&4)>>2; + + // 4-bit message type + type = (b1&120)>>3; + + // 8 bit value + value8 = (b2&~128)<<1 | (b3&1); // 8 bit values + + // 12 bit value + value12 = value8 | (b1&7)<<9 | (b3&2)<<7; + + /* ------- Looking at spinscan data ? -- commented out for release + if (buttons&64) { + for (pos=0; pos<24; pos++) fprintf(stderr, "%d, ", ss[pos]); + pos=0; + fprintf(stderr, "\n"); + } + if (ss1 || ss2 || ss3) { + ss[pos++] = ss1; + ss[pos++] = ss2; + ss[pos++] = ss3; + } + ------- */ +} + + +/* ---------------------------------------------------------------------- + * EXECUTIVE FUNCTIONS + * + * start() - start/re-start reading telemetry in a thread + * stop() - stop reading telemetry and terminates thread + * pause() - discards inbound telemetry (ignores it) + * + * + * THE MEAT OF THE CODE IS IN RUN() IT IS A WHILE LOOP CONSTANTLY + * READING TELEMETRY AND ISSUING CONTROL COMMANDS WHILST UPDATING + * MEMBER VARIABLES AS TELEMETRY CHANGES ARE FOUND. + * + * run() - bg thread continuosly reading/writing the device port + * it is kicked off by start and then examines status to check + * when it is time to pause or stop altogether. + * ---------------------------------------------------------------------- */ +int Computrainer::restart() +{ + int status; + + // get current status + pvars.lock(); + status = this->deviceStatus; + pvars.unlock(); + // what state are we in anyway? + if (status&CT_RUNNING && status&CT_PAUSED) { + status &= ~CT_PAUSED; + pvars.lock(); + this->deviceStatus = status; + pvars.unlock(); + return 0; // ok its running again! + } + return 2; +} + +int Computrainer::stop() +{ + int status; + + // get current status + pvars.lock(); + status = this->deviceStatus; + pvars.unlock(); + + // what state are we in anyway? + pvars.lock(); + deviceStatus = 0; // Terminate it! + pvars.unlock(); + return 0; +} + +int Computrainer::pause() +{ + int status; + + // get current status + pvars.lock(); + status = this->deviceStatus; + pvars.unlock(); + + if (status&CT_PAUSED) return 2; // already paused you muppet! + else if (!(status&CT_RUNNING)) return 4; // not running anyway, fool! + else { + + // ok we're running and not paused so lets pause + status |= CT_PAUSED; + pvars.lock(); + this->deviceStatus = status; + pvars.unlock(); + + return 0; + } +} + +// used by thread to set variables and emit event if needed +// on unexpected exit +int Computrainer::quit(int code) +{ + // event code goes here! + exit(code); + return 0; // never gets here obviously but shuts up the compiler! +} + +/*---------------------------------------------------------------------- + * THREADED CODE - READS TELEMETRY AND SENDS COMMANDS TO KEEP CT ALIVE + *----------------------------------------------------------------------*/ +void Computrainer::run() +{ + + // locally cached settings - only update main class variables + // when they change + + int cmds=0; // count loops with no command sent + + // holders for unpacked telemetry + int ss1,ss2,ss3, buttons, type, value8, value12; + + // newly read values - compared against cached values + int changed; + int newmode; + double newload, newgradient; + double newspeed, newRRC; + bool newcalibrated, newhrconnected, newcadconnected; + bool isDeviceOpen = false; + + // Cached current values + // when new values are received from the device + // if they differ from current values we update + // otherwise do nothing + int curmode, curstatus; + double curload, curgradient; + double curPower; // current output power in Watts + double curHeartRate; // current heartrate in BPM + double curCadence; // current cadence in RPM + double curSpeed; // current speef in KPH + double curRRC; // calibrated Rolling Resistance + bool curcalibrated; // is it calibrated? + bool curhrconnected; // is HR sensor connected? + bool curcadconnected; // is CAD sensor connected? + double curspinData[24]; // SS values only in SS_MODE + int curButtons; // Button status + int curStatus; // Device status running, paused, disconnected + + + // initialise local cache & main vars + pvars.lock(); + this->deviceStatus = CT_RUNNING; + curmode = this->mode; + curload = this->load; + curgradient = this->gradient; + curPower = this->devicePower = 0; + curHeartRate = this->deviceHeartRate = 0; + curCadence = this->deviceCadence = 0; + curSpeed = this->deviceSpeed = 0; + curButtons = this->deviceButtons; + curRRC = this->deviceRRC = 0; + curcalibrated = false; + this->deviceCalibrated = false; + curhrconnected = false; + this->deviceHRConnected = false; + curcadconnected = false; + this->deviceCADConnected = false; + curButtons = 0; + this->deviceButtons = 0; + curStatus = this->deviceStatus; + for (int i=0; i<24; i++) curspinData[i] = this->spinData[i] = 0; + pvars.unlock(); + + + // open the device + if (openPort()) { + quit(2); + return; // open failed! + } else { + isDeviceOpen = true; + } + + // send first command to get computrainer ready + prepareCommand(curmode, curmode == CT_ERGOMODE ? curload : curgradient); + if (sendCommand(curmode) == -1) { + // send failed - ouch! + closePort(); // need to release that file handle!! + quit(4); + return; // couldn't write to the device + } + + + while(1) { + + if (isDeviceOpen == true) { + + if (readMessage() > 0) { + + //---------------------------------------------------------------- + // UPDATE BASIC TELEMETRY (HR, CAD, SPD et al) + //---------------------------------------------------------------- + + changed = 0; + unpackTelemetry(ss1, ss2, ss3, buttons, type, value8, value12); + + switch (type) { + case CT_HEARTRATE : + if (value8 != curHeartRate) { + curHeartRate = value8; + pvars.lock(); + this->deviceHeartRate = curHeartRate; + pvars.unlock(); + + changed=1; + } + break; + + case CT_POWER : + if (value12 != curPower) { + curPower = value12; + pvars.lock(); + this->devicePower = curPower; + pvars.unlock(); + + changed=1; + } + break; + + case CT_CADENCE : + if (value8 != curCadence) { + curCadence = value8; + pvars.lock(); + this->deviceCadence = curCadence; + pvars.unlock(); + + changed=1; + } + break; + + case CT_SPEED : + value12 *=36; // convert from mps to kph + value12 *=9; + value12 /=10; // it seems that compcs takes off 10% ???? + newspeed = value12; + newspeed /= 1000; + if (newspeed != curSpeed) { + pvars.lock(); + this->deviceSpeed = curSpeed = newspeed; + pvars.unlock(); + + changed=1; + } + break; + + case CT_RRC : + newcalibrated = value12&2048 ? true : false; + newRRC = value12&~2048; // only use 11bits + newRRC /= 256; + + if (newRRC != curRRC) { + pvars.lock(); + this->deviceRRC = curRRC = newRRC; + pvars.unlock(); + + changed=1; + } + break; + + case CT_SENSOR : + newcadconnected = value12&2048 ? true : false; + newhrconnected = value12&1024 ? true : false; + + if (newhrconnected != curhrconnected || newcadconnected != curcadconnected) { + pvars.lock(); + this->deviceHRConnected=curhrconnected=newhrconnected; + this->deviceCADConnected=curcadconnected=newcadconnected; + pvars.unlock(); + + changed=1; + } + break; + + default : + break; + } + + //---------------------------------------------------------------- + // UPDATE BUTTONS + //---------------------------------------------------------------- + if (buttons != curButtons) { + // let the gui workout what the deal is with silly button values! + pvars.lock(); + this->deviceButtons = curButtons = buttons; + pvars.unlock(); + } + + //---------------------------------------------------------------- + // UPDATE SSCAN + //---------------------------------------------------------------- + /* not yet implemented */ + + } else { + // no data + // how long to sleep for ... mmm save CPU cycles vs + // data overflow ? + CTsleeper::msleep (100); // lets try a tenth of a second + } + + } + + //---------------------------------------------------------------- + // LISTEN TO GUI CONTROL COMMANDS + //---------------------------------------------------------------- + pvars.lock(); + curstatus = this->deviceStatus; + newmode = this->mode; + newload = this->load; + curgradient = newgradient = this->gradient; + pvars.unlock(); + + /* time to shut up shop */ + if (!(curstatus&CT_RUNNING)) { + // time to stop! + closePort(); // need to release that file handle!! + quit(0); + return; + } + + if ((curstatus&CT_PAUSED) && isDeviceOpen == true) { + closePort(); + isDeviceOpen = false; + + } else if (!(curstatus&CT_PAUSED) && (curstatus&CT_RUNNING) && isDeviceOpen == false) { + + if (openPort()) { + quit(2); + return; // open failed! + } + isDeviceOpen = true; + + // send first command to get computrainer ready + prepareCommand(curmode, curmode == CT_ERGOMODE ? curload : curgradient); + if (sendCommand(curmode) == -1) { + // send failed - ouch! + closePort(); // need to release that file handle!! + quit(4); + return; // couldn't write to the device + } + } + + //---------------------------------------------------------------- + // KEEP THE COMPUTRAINER CONTROL ALIVE + //---------------------------------------------------------------- + if (isDeviceOpen == true && !(cmds%10)) { + cmds=1; + curmode = newmode; + curload = newload; + curgradient = newgradient; + + prepareCommand(curmode, curmode == CT_ERGOMODE ? curload : curgradient); + if (sendCommand(curmode) == -1) { + // send failed - ouch! + closePort(); // need to release that file handle!! + quit(4); + cmds=20; + return; // couldn't write to the device + } + } else { + cmds++; + } + } +} + +void CTsleeper::msleep(unsigned long msecs) +{ + QThread::msleep(msecs); +} + + +/* ---------------------------------------------------------------------- + * LOW LEVEL DEVICE IO ROUTINES - PORT TO QIODEVICE REQUIRED BEFORE COMMIT + * + * + * HIGH LEVEL IO + * int sendCommand() - writes a command to the device + * int readMessage() - reads an inbound message + * + * LOW LEVEL IO + * openPort() - opens serial device and configures it + * closePort() - closes serial device and releases resources + * rawRead() - non-blocking read of inbound data + * rawWrite() - non-blocking write of outbound data + * discover() - check if a ct is attached to the port specified + * ---------------------------------------------------------------------- */ +int Computrainer::sendCommand(int mode) // writes a command to the device +{ + switch (mode) { + + case CT_ERGOMODE : + return rawWrite(ERGO_Command, 56); + break; + + case CT_SSMODE : + return rawWrite(SS_Command, 56); + break; + + default : + return -1; + break; + } +} + +int Computrainer::readMessage() +{ + int rc; + + if ((rc = rawRead(buf, 7)) > 0 && (buf[6]&128) == 0) { + + // we got something but need to sync + while ((buf[6]&128) == 0 && rc > 0) { + rc = rawRead(&buf[6], 1); + } + + // at this point we are synced, we may have a dodgy + // record but that is fair enough if we were out of + // sync anyway - the alternative is to keep going + // until we get a good message and that will + // lead to bigger issues (plus we may have a hw + // problem anyway). + // + // From experience, the need to sync is quite rare + // on a normally configured and working system + + } + return rc; +} + +int Computrainer::closePort() +{ +#ifdef WIN32 + return (int)!CloseHandle(devicePort); +#else + tcflush(devicePort, TCIOFLUSH); // clear out the garbage + return close(devicePort); +#endif +} + +int Computrainer::openPort() +{ +#ifndef WIN32 + + // LINUX AND MAC USES TERMIO / IOCTL / STDIO + +#if defined(Q_OS_MACX) + int ldisc=TTYDISC; +#else + int ldisc=N_TTY; // LINUX +#endif + + if ((devicePort=open(deviceFilename.toAscii(),O_RDWR | O_NOCTTY | O_NONBLOCK)) == -1) return errno; + + tcflush(devicePort, TCIOFLUSH); // clear out the garbage + + if (ioctl(devicePort, TIOCSETD, &ldisc) == -1) return errno; + + // get current settings for the port + tcgetattr(devicePort, &deviceSettings); + + // set raw mode i.e. ignbrk, brkint, parmrk, istrip, inlcr, igncr, icrnl, ixon + // noopost, cs8, noecho, noechonl, noicanon, noisig, noiexn + cfmakeraw(&deviceSettings); + cfsetspeed(&deviceSettings, B2400); + + // further attributes + deviceSettings.c_iflag= IGNPAR; + deviceSettings.c_oflag=0; + deviceSettings.c_cflag &= (~CSIZE & ~CSTOPB); +#if defined(Q_OS_MACX) + deviceSettings.c_cflag |= (CS8 | CREAD | HUPCL | CCTS_OFLOW | CRTS_IFLOW); +#else + deviceSettings.c_cflag |= (CS8 | CREAD | HUPCL | CRTSCTS); +#endif + deviceSettings.c_lflag=0; + deviceSettings.c_cc[VMIN]=0; + deviceSettings.c_cc[VTIME]=0; + + // set those attributes + if(tcsetattr(devicePort, TCSANOW, &deviceSettings) == -1) return errno; + tcgetattr(devicePort, &deviceSettings); + +#else + // WINDOWS USES SET/GETCOMMSTATE AND READ/WRITEFILE + + COMMTIMEOUTS timeouts; // timeout settings on serial ports + + wchar_t deviceFilenameW[32]; // COM1 needs 4 characters, 32 should be enough? + MultiByteToWideChar(CP_ACP, 0, deviceFilename.toAscii(), -1, (LPWSTR)deviceFilenameW, + sizeof(deviceFilenameW)); + + // win32 commport API + devicePort = CreateFile (deviceFilenameW, GENERIC_READ|GENERIC_WRITE, + FILE_SHARE_DELETE|FILE_SHARE_WRITE|FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + + if (devicePort == INVALID_HANDLE_VALUE) return -1; + + if (GetCommState (devicePort, &deviceSettings) == false) return -1; + + // so we've opened the comm port lets set it up for + deviceSettings.BaudRate = CBR_2400; + deviceSettings.fParity = NOPARITY; + deviceSettings.ByteSize = 8; + deviceSettings.StopBits = ONESTOPBIT; + deviceSettings.EofChar = 0x0; + deviceSettings.ErrorChar = 0x0; + deviceSettings.EvtChar = 0x0; + deviceSettings.fBinary = true; + deviceSettings.fRtsControl = RTS_CONTROL_HANDSHAKE; + deviceSettings.fOutxCtsFlow = TRUE; + + + if (SetCommState(devicePort, &deviceSettings) == false) { + CloseHandle(devicePort); + return -1; + } + + timeouts.ReadIntervalTimeout = 0; + timeouts.ReadTotalTimeoutConstant = 1000; + timeouts.ReadTotalTimeoutMultiplier = 50; + timeouts.WriteTotalTimeoutConstant = 2000; + timeouts.WriteTotalTimeoutMultiplier = 0; + SetCommTimeouts(devicePort, &timeouts); + +#endif + + // success + return 0; +} + +int Computrainer::rawWrite(uint8_t *bytes, int size) // unix!! +{ + int rc=0,ibytes; + +#ifdef WIN32 + DWORD cBytes; + rc = WriteFile(devicePort, bytes, size, &cBytes, NULL); + if (!rc) return -1; + return rc; + +#else + + ioctl(devicePort, FIONREAD, &ibytes); + + // timeouts are less critical for writing, since vols are low + rc= write(devicePort, bytes, size); + + // but it is good to avoid buffer overflow since the + // computrainer microcontroller has almost no RAM + if (rc != -1) tcdrain(devicePort); // wait till its gone. + + ioctl(devicePort, FIONREAD, &ibytes); +#endif + + return rc; + +} + +int Computrainer::rawRead(uint8_t bytes[], int size) +{ + int rc=0; + +#ifdef WIN32 + + // Readfile deals with timeouts and readyread issues + DWORD cBytes; + rc = ReadFile(devicePort, bytes, 7, &cBytes, NULL); + if (rc) return (int)cBytes; + else return (-1); + +#else + + int timeout=0, i=0; + uint8_t byte; + + // read one byte at a time sleeping when no data ready + // until we timeout waiting then return error + for (i=0; i= CT_READTIMEOUT) return -1; // we timed out! + } + + return i; + +#endif +} + + +// check to see of there is a port at the device specified +// returns true if the device exists and false if not +bool Computrainer::discover(QString filename) +{ + uint8_t *greeting = (uint8_t *)"Racermate"; + uint8_t handshake[7]; + + if (filename.isEmpty()) return false; // no null filenames thanks + + // lets set the port + setDevice(filename); + + // lets open it + openPort(); + + // send a probe + if (rawWrite(greeting, 9) == -1) return false; + + // did we get something back from the device? + if (rawRead(handshake, 6) != 6) return false; + + handshake[6] = '\0'; + + if (strcmp((char *)handshake, "LinkUp")) return false; + else return true; +} diff --git a/src/Computrainer.h b/src/Computrainer.h new file mode 100644 index 000000000..4e8ef90af --- /dev/null +++ b/src/Computrainer.h @@ -0,0 +1,207 @@ +/* + * 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 + */ + + +// I have consciously avoided putting things like data logging, lap marking, +// intervals or any load management functions in this class. It is restricted +// to controlling an reading telemetry from the device +// +// I expect higher order classes to implement such functions whilst +// other devices (e.g. ANT+ devices) may be implemented with the same basic +// interface +// +// I have avoided a base abstract class at this stage since I am uncertain +// what core methods would be required by say, ANT+ or Tacx devices + + +#ifndef _GC_Computrainer_h +#define _GC_Computrainer_h 1 + +#include +#include +#include +#include +#include +#include "RealtimeController.h" + +#ifdef WIN32 +#include +#include +#else +#include // unix!! +#include // unix!! +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + + +/* Some CT Microcontroller / Protocol Constants */ + +/* read timeouts in microseconds */ +#define CT_READTIMEOUT 1000 +#define CT_WRITETIMEOUT 2000 + +// message type +#define CT_SPEED 0x01 +#define CT_POWER 0x02 +#define CT_HEARTRATE 0x03 +#define CT_CADENCE 0x06 +#define CT_RRC 0x09 +#define CT_SENSOR 0x0b + +// buttons +#define CT_RESET 0x01 +#define CT_F1 0x02 +#define CT_F3 0x04 +#define CT_PLUS 0x08 +#define CT_F2 0x10 +#define CT_MINUS 0x20 +#define CT_SSS 0x40 // spinscan sync is not a button! +#define CT_NONE 0x80 + +/* Device operation mode */ +#define CT_ERGOMODE 0x01 +#define CT_SSMODE 0x02 + +/* UI operation mode */ +#define UI_MANUAL 0x01 // using +/- keys to adjust +#define UI_ERG 0x02 // running an erg file! + +/* Control status */ +#define CT_RUNNING 0x01 +#define CT_PAUSED 0x02 + +/* default operation mode */ +#define DEFAULT_MODE CT_ERGOMODE +#define DEFAULT_LOAD 100.00 +#define DEFAULT_GRADIENT 2.00 + + +class Computrainer : public QThread +{ + +public: + Computrainer(QObject *parent=0, QString deviceFilename=0); // pass device + ~Computrainer(); + + QObject *parent; + + // HIGH-LEVEL FUNCTIONS + int start(); // Calls QThread to start + int restart(); // restart after paused + int pause(); // pauses data collection, inbound telemetry is discarded + int stop(); // stops data collection thread + int quit(int error); // called by thread before exiting + bool discover(QString deviceFilename); // confirm CT is attached to device + + // SET + void setDevice(QString deviceFilename); // setup the device filename + void setLoad(double load); // set the load to generate in ERGOMODE + void setGradient(double gradient); // set the load to generate in SSMODE + void setMode(int mode, + double load=DEFAULT_LOAD, // set mode to CT_ERGOMODE or CT_SSMODE + double gradient=DEFAULT_GRADIENT); + + + // GET TELEMETRY AND STATUS + // direct access to class variables is not allowed because we need to use wait conditions + // to sync data read/writes between the run() thread and the main gui thread + bool isCalibrated(); + bool isHRConnected(); + bool isCADConnected(); + void getTelemetry(double &Power, double &HeartRate, double &Cadence, double &Speed, + double &RRC, bool &calibration, int &Buttons, int &Status); + void getSpinScan(double spinData[]); + int getMode(); + double getGradient(); + double getLoad(); + +private: + void run(); // called by start to kick off the CT comtrol thread + + // 56 bytes comprise of 8 7byte command messages, where + // the last is the set load / gradient respectively + uint8_t ERGO_Command[56], + SS_Command[56]; + + // Utility and BG Thread functions + int openPort(); + int closePort(); + + // Protocol encoding + void prepareCommand(int mode, double value); // sets up the command packet according to current settings + int sendCommand(int mode); // writes a command to the device + int calcCRC(int value); // calculates the checksum for the current command + + // Protocol decoding + int readMessage(); + void unpackTelemetry(int &b1, int &b2, int &b3, int &buttons, int &type, int &value8, int &value12); + + // Mutex for controlling accessing private data + QMutex pvars; + + // INBOUND TELEMETRY - all volatile since it is updated by the run() thread + volatile double devicePower; // current output power in Watts + volatile double deviceHeartRate; // current heartrate in BPM + volatile double deviceCadence; // current cadence in RPM + volatile double deviceSpeed; // current speef in KPH + volatile double deviceRRC; // calibrated Rolling Resistance + volatile bool deviceCalibrated; // is it calibrated? + volatile double spinData[24]; // SS values only in SS_MODE + volatile int deviceButtons; // Button status + volatile bool deviceHRConnected; // HR jack is connected + volatile bool deviceCADConnected; // Cadence jack is connected + volatile int deviceStatus; // Device status running, paused, disconnected + + // OUTBOUND COMMANDS - all volatile since it is updated by the GUI thread + volatile int mode; + volatile double load; + volatile double gradient; + + // i/o message holder + uint8_t buf[7]; + + // device port + QString deviceFilename; +#ifdef WIN32 + HANDLE devicePort; // file descriptor for reading from com3 + DCB deviceSettings; // serial port settings baud rate et al +#else + int devicePort; // unix!! + struct termios deviceSettings; // unix!! +#endif + // raw device utils + int rawWrite(uint8_t *bytes, int size); // unix!! + int rawRead(uint8_t *bytes, int size); // unix!! +}; + +class CTsleeper : public QThread +{ + public: + static void msleep(unsigned long msecs); // inherited from QThread +}; + +#endif // _GC_Computrainer_h + diff --git a/src/ComputrainerController.cpp b/src/ComputrainerController.cpp new file mode 100644 index 000000000..c11242fb5 --- /dev/null +++ b/src/ComputrainerController.cpp @@ -0,0 +1,159 @@ +/* + * 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 "ComputrainerController.h" +#include "Computrainer.h" +#include "RealtimeData.h" + +ComputrainerController::ComputrainerController(RealtimeWindow *parent, DeviceConfiguration *dc) : RealtimeController(parent) +{ + myComputrainer = new Computrainer (parent, dc->portSpec); +} + + +int +ComputrainerController::start() +{ + return myComputrainer->start(); +} + + +int +ComputrainerController::restart() +{ + return myComputrainer->restart(); +} + + +int +ComputrainerController::pause() +{ + return myComputrainer->pause(); +} + + +int +ComputrainerController::stop() +{ + return myComputrainer->stop(); +} + + +bool +ComputrainerController::discover(DeviceConfiguration *) {return false; } // NOT IMPLEMENTED YET + + +bool ComputrainerController::doesPush() { return false; } +bool ComputrainerController::doesPull() { return true; } +bool ComputrainerController::doesLoad() { return true; } + +/* + * gets called from the GUI to get updated telemetry. + * so whilst we are at it we check button status too and + * act accordingly. + * + */ +void +ComputrainerController::getRealtimeData(RealtimeData &rtData) +{ + int Buttons, Status; + bool calibration; + double Power, HeartRate, Cadence, Speed, RRC, Load; + + if(!myComputrainer->isRunning()) + { + QMessageBox msgBox; + msgBox.setText("Cannot Connect to Computrainer"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.exec(); + parent->Stop(); + } + // get latest telemetry + myComputrainer->getTelemetry(Power, HeartRate, Cadence, Speed, + RRC, calibration, Buttons, Status); + + // + // PASS BACK TELEMETRY + // + rtData.setWatts(Power); + rtData.setHr(HeartRate); + rtData.setRPM(Cadence); + rtData.setSpeed(Speed); + + // + // BUTTONS + // + + // ADJUST LOAD + Load = myComputrainer->getLoad(); + if ((Buttons&CT_PLUS) && !(Buttons&CT_F3)) { + parent->Higher(); + } + if ((Buttons&CT_MINUS) && !(Buttons&CT_F3)) { + parent->Lower(); + } + rtData.setLoad(Load); + + // FFWD/REWIND + if ((Buttons&CT_PLUS) && (Buttons&CT_F3)) { + parent->FFwd(); + } + if ((Buttons&CT_MINUS) && (Buttons&CT_F3)) { + parent->Rewind(); + } + + + // LAP/INTERVAL + if (Buttons&CT_F1 && !(Buttons&CT_F3)) { + parent->newLap(); + } + if ((Buttons&CT_F1) && (Buttons&CT_F3)) { + parent->FFwdLap(); + } + + // if Buttons == 0 we just pressed stop! + if (Buttons&CT_RESET) { + parent->Stop(); + } + + // displaymode + if (Buttons&CT_F2) { + parent->nextDisplayMode(); + } +} + +void ComputrainerController::pushRealtimeData(RealtimeData &) { } // update realtime data with current values + +void +ComputrainerController::setLoad(double load) +{ + myComputrainer->setLoad(load); +} + +void +ComputrainerController::setGradient(double grade) +{ + myComputrainer->setGradient(grade); +} +void +ComputrainerController::setMode(int mode) +{ + if (mode == RT_MODE_ERGO) mode = CT_ERGOMODE; + if (mode == RT_MODE_SPIN) mode = CT_SSMODE; + myComputrainer->setMode(mode); +} diff --git a/src/ComputrainerController.h b/src/ComputrainerController.h new file mode 100644 index 000000000..a2714b196 --- /dev/null +++ b/src/ComputrainerController.h @@ -0,0 +1,53 @@ +/* + * 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 "RealtimeController.h" +#include "Computrainer.h" + +// Abstract base class for Realtime device controllers + + +#ifndef _GC_ComputrainerController_h +#define _GC_ComputrainerController_h 1 + +class ComputrainerController : public RealtimeController +{ + +public: + ComputrainerController (RealtimeWindow *, DeviceConfiguration *); + + Computrainer *myComputrainer; // the device itself + + int start(); + int restart(); // restart after paused + int pause(); // pauses data collection, inbound telemetry is discarded + int stop(); // stops data collection thread + bool discover(DeviceConfiguration *); // tell if a device is present at port passed + + + // telemetry push pull + bool doesPush(), doesPull(), doesLoad(); + void getRealtimeData(RealtimeData &rtData); + void pushRealtimeData(RealtimeData &rtData); + void setLoad(double); + void setGradient(double); + void setMode(int); +}; + +#endif // _GC_ComputrainerController_h + diff --git a/src/ConfigDialog.cpp b/src/ConfigDialog.cpp index d7c95ddcc..76b654dc8 100644 --- a/src/ConfigDialog.cpp +++ b/src/ConfigDialog.cpp @@ -4,6 +4,7 @@ #include "MainWindow.h" #include "ConfigDialog.h" +#include "RealtimeWindow.h" #include "Pages.h" #include "Settings.h" #include "Zones.h" @@ -43,9 +44,12 @@ ConfigDialog::ConfigDialog(QDir _home, Zones **_zones) configPage = new ConfigurationPage(); + devicePage = new DevicePage(this); + pagesWidget = new QStackedWidget; pagesWidget->addWidget(configPage); pagesWidget->addWidget(cyclistPage); + pagesWidget->addWidget(devicePage); closeButton = new QPushButton(tr("Close")); saveButton = new QPushButton(tr("Save")); @@ -61,6 +65,12 @@ ConfigDialog::ConfigDialog(QDir _home, Zones **_zones) connect(cyclistPage->btnDelete, SIGNAL(clicked()), this, SLOT(delete_Clicked())); connect(cyclistPage->calendar, SIGNAL(selectionChanged()), this, SLOT(calendarDateChanged())); + // connect the pieces... + connect(devicePage->typeSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(changedType(int))); + connect(devicePage->addButton, SIGNAL(clicked()), this, SLOT(devaddClicked())); + connect(devicePage->delButton, SIGNAL(clicked()), this, SLOT(devdelClicked())); + connect(devicePage->pairButton, SIGNAL(clicked()), this, SLOT(devpairClicked())); + horizontalLayout = new QHBoxLayout; horizontalLayout->addWidget(contentsWidget); horizontalLayout->addWidget(pagesWidget, 1); @@ -85,6 +95,7 @@ ConfigDialog::~ConfigDialog() delete cyclistPage; delete contentsWidget; delete configPage; + delete devicePage; delete pagesWidget; delete closeButton; delete horizontalLayout; @@ -108,6 +119,11 @@ void ConfigDialog::createIcons() cyclistButton->setTextAlignment(Qt::AlignHCenter); cyclistButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + QListWidgetItem *realtimeButton = new QListWidgetItem(contentsWidget); + realtimeButton->setIcon(QIcon(":images/arduino.png")); + realtimeButton->setText(tr("Devices")); + realtimeButton->setTextAlignment(Qt::AlignHCenter); + realtimeButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); connect(contentsWidget, SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem *)), @@ -186,6 +202,14 @@ void ConfigDialog::save_Clicked() cyclistPage->checkboxNew->setEnabled(true); (*zones)->write(home); + + // Save the device configuration... + DeviceConfigurations all; + all.writeConfig(devicePage->deviceListModel->Configuration); + + // update widgets to let them know config has changed + // only realtime sorted thus far; + mainwindow->realtimeWindow->configUpdate(); } void ConfigDialog::moveCalendarToCurrentRange() { @@ -251,3 +275,119 @@ void ConfigDialog::calendarDateChanged() { assert(range >= 0); cyclistPage->setCurrentRange(range); } + +// +// DEVICE CONFIG STUFF +// + +void +ConfigDialog::changedType(int) +{ +// THIS CODE IS DISABLED FOR THIS RELEASE XXX +// // disable/enable default checkboxes +// if (devicePage->devices.at(index).download == false) { +// devicePage->isDefaultDownload->setEnabled(false); +// devicePage->isDefaultDownload->setCheckState(Qt::Unchecked); +// } else { +// devicePage->isDefaultDownload->setEnabled(true); +// } +// if (devicePage->devices.at(index).realtime == false) { +// devicePage->isDefaultRealtime->setEnabled(false); +// devicePage->isDefaultRealtime->setCheckState(Qt::Unchecked); +// } else { +// devicePage->isDefaultRealtime->setEnabled(true); +// } + devicePage->setConfigPane(); +} + +void +ConfigDialog::devaddClicked() +{ + DeviceConfiguration add; + DeviceTypes Supported; + + // get values from the gui elements + add.name = devicePage->deviceName->displayText(); + add.type = devicePage->typeSelector->itemData(devicePage->typeSelector->currentIndex()).toInt(); + add.portSpec = devicePage->deviceSpecifier->displayText(); + add.deviceProfile = devicePage->deviceProfile->displayText(); + + // NOT IMPLEMENTED IN THIS RELEASE XXX + //add.isDefaultDownload = devicePage->isDefaultDownload->isChecked() ? true : false; + //add.isDefaultRealtime = devicePage->isDefaultDownload->isChecked() ? true : false; + + // Validate the name + QRegExp nameSpec(".+"); + if (nameSpec.exactMatch(add.name) == false) { + QMessageBox::critical(0, "Invalid Device Name", + QString("Device Name should be non-blank")); + return ; + } + + // Validate the portSpec + QRegExp antSpec("[^:]*:[^:]*"); // ip:port same as TCP ... for now... + QRegExp tcpSpec("[^:]*:[^:]*"); // ip:port +#ifdef WIN32 + QRegExp serialSpec("COM[0-9]*"); // COMx for WIN32, /dev/* for others +#else + QRegExp serialSpec("/dev/.*"); // COMx for WIN32, /dev/* for others +#endif + + // check the portSpec is valid, based upon the connection type + switch (Supported.getType(add.type).connector) { + case DEV_ANT : + if (antSpec.exactMatch(add.portSpec) == false) { + QMessageBox::critical(0, "Invalid Port Specification", + QString("For ANT devices the specifier must be ") + + "hostname:portnumber"); + return ; + } + break; + case DEV_SERIAL : + if (serialSpec.exactMatch(add.portSpec) == false) { + QMessageBox::critical(0, "Invalid Port Specification", + QString("For Serial devices the specifier must be ") + +#ifdef WIN32 + "COMn" +#else + "/dev/xxxxx" +#endif + ); + return ; + } + break; + case DEV_TCP : + if (tcpSpec.exactMatch(add.portSpec) == false) { + QMessageBox::critical(0, "Invalid Port Specification", + QString("For TCP streaming devices the specifier must be ") + + "hostname:portnumber"); + return ; + } + break; + } + + devicePage->deviceListModel->add(add); +} + +void +ConfigDialog::devdelClicked() +{ + devicePage->deviceListModel->del(); +} + +void +ConfigDialog::devpairClicked() +{ + DeviceConfiguration add; + + // get values from the gui elements + add.name = devicePage->deviceName->displayText(); + add.type = devicePage->typeSelector->itemData(devicePage->typeSelector->currentIndex()).toInt(); + add.portSpec = devicePage->deviceSpecifier->displayText(); + add.deviceProfile = devicePage->deviceProfile->displayText(); + + QProgressDialog *progress = new QProgressDialog("Looking for Devices...", "Abort Scan", 0, 200, this); + progress->setWindowModality(Qt::WindowModal); + + devicePage->pairClicked(&add, progress); +} diff --git a/src/ConfigDialog.h b/src/ConfigDialog.h index fa52d15fb..efe9169bb 100644 --- a/src/ConfigDialog.h +++ b/src/ConfigDialog.h @@ -26,6 +26,12 @@ class ConfigDialog : public QDialog void delete_Clicked(); void calendarDateChanged(); + // device config slots + void changedType(int); + void devaddClicked(); + void devpairClicked(); + void devdelClicked(); + private: void createIcons(); void calculateZones(); @@ -34,6 +40,7 @@ class ConfigDialog : public QDialog ConfigurationPage *configPage; CyclistPage *cyclistPage; + DevicePage *devicePage; QPushButton *saveButton; QStackedWidget *pagesWidget; QPushButton *closeButton; @@ -45,6 +52,9 @@ class ConfigDialog : public QDialog QSettings *settings; QDir home; Zones **zones; + + // used by device config + QList twiNames, twiSpecs, twiTypes, twiDefaults; }; #endif diff --git a/src/DeviceConfiguration.cpp b/src/DeviceConfiguration.cpp new file mode 100644 index 000000000..8c1e7658b --- /dev/null +++ b/src/DeviceConfiguration.cpp @@ -0,0 +1,143 @@ +/* + * 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 // used by settings.h +#include +#include +#include "Settings.h" +#include "DeviceTypes.h" +#include "DeviceConfiguration.h" + +// +// Model for the device list, populates tie deviceList QTableView +// + +DeviceConfiguration::DeviceConfiguration() +{ + // just set all to empty! + type=0; + isDefaultDownload=false; + isDefaultRealtime=false; +} + + +DeviceConfigurations::DeviceConfigurations() +{ + // read in the config - it updates the local Entries QList + readConfig(); +} + +DeviceConfigurations::~DeviceConfigurations() +{ +} + + +QList +DeviceConfigurations::getList() +{ + return Entries; +} + +// serialise/deserialise config +QList +DeviceConfigurations::readConfig() +{ + int count; + + boost::shared_ptr settings = GetApplicationSettings(); + + // get count of devices + QVariant configVal = settings->value(GC_DEV_COUNT); + if (configVal.isNull()) { + count=0; + } else { + count = configVal.toInt(); + } + + // for each device + for (int i=0; i< count; i++) { + + DeviceConfiguration Entry; + + QString configStr = QString("%1%2").arg(GC_DEV_NAME).arg(i+1); + configVal = settings->value(configStr); + Entry.name = configVal.toString(); + + configStr = QString("%1%2").arg(GC_DEV_SPEC).arg(i+1); + configVal = settings->value(configStr); + Entry.portSpec = configVal.toString(); + + configStr = QString("%1%2").arg(GC_DEV_TYPE).arg(i+1); + configVal = settings->value(configStr); + Entry.type = configVal.toInt(); + + configStr = QString("%1%2").arg(GC_DEV_PROF).arg(i+1); + configVal = settings->value(configStr); + Entry.deviceProfile = configVal.toString(); + + configStr = QString("%1%2").arg(GC_DEV_DEFI).arg(i+1); + configVal = settings->value(configStr); + Entry.isDefaultDownload = configVal.toInt(); + + configStr = QString("%1%2").arg(GC_DEV_DEFR).arg(i+1); + configVal = settings->value(configStr); + Entry.isDefaultRealtime = configVal.toInt(); + + Entries.append(Entry); + } + return Entries; +} + +void +DeviceConfigurations::writeConfig(QList Configuration) +{ + // loop through the entries in the Configuration QList + // writing to the GC settings + + int i=0; + boost::shared_ptr settings = GetApplicationSettings(); + + settings->setValue(GC_DEV_COUNT, Configuration.count()); + for (i=0; i < Configuration.count(); i++) { + + // name + QString configStr = QString("%1%2").arg(GC_DEV_NAME).arg(i+1); + settings->setValue(configStr, Configuration.at(i).name); + + // type + configStr = QString("%1%2").arg(GC_DEV_TYPE).arg(i+1); + settings->setValue(configStr, Configuration.at(i).type); + + // portSpec + configStr = QString("%1%2").arg(GC_DEV_SPEC).arg(i+1); + settings->setValue(configStr, Configuration.at(i).portSpec); + + // deviceProfile + configStr = QString("%1%2").arg(GC_DEV_PROF).arg(i+1); + settings->setValue(configStr, Configuration.at(i).deviceProfile); + + // isDefaultDownload + configStr = QString("%1%2").arg(GC_DEV_DEFI).arg(i+1); + settings->setValue(configStr, Configuration.at(i).isDefaultDownload); + + // isDefaultRealtime + configStr = QString("%1%2").arg(GC_DEV_DEFR).arg(i+1); + settings->setValue(configStr, Configuration.at(i).isDefaultRealtime); + } + +} diff --git a/src/DeviceConfiguration.h b/src/DeviceConfiguration.h new file mode 100644 index 000000000..c5617561f --- /dev/null +++ b/src/DeviceConfiguration.h @@ -0,0 +1,58 @@ +/* + * 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 + */ + + +// Lists all the devices configured in preferences + +#ifndef _GC_DeviceConfiguration_h +#define _GC_DeviceConfiguration_h + +class DeviceConfiguration +{ + public: // direct update to class vars from deviceModel! + DeviceConfiguration(); + + int type; + QString name, + portSpec, + deviceProfile; // device specific data + // used by ANT to store ANTIDs + // available for use by all devices + + bool isDefaultDownload, // not implemented yet + isDefaultRealtime; // not implemented yet +}; + +class DeviceConfigurations +{ + public: + DeviceConfigurations(); + ~DeviceConfigurations(); + + QList getList(); + + // serialise/deserialise config + QList readConfig(); + void writeConfig(QList); + + private: + QList Entries; // all the Configurations + +}; + +#endif diff --git a/src/DeviceTypes.cpp b/src/DeviceTypes.cpp new file mode 100644 index 000000000..3f1fc2418 --- /dev/null +++ b/src/DeviceTypes.cpp @@ -0,0 +1,61 @@ +/* + * 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 +#include "DeviceTypes.h" + +// NOTE: +// Device Types not fully supported in this release (functionally) have +// been commented out. As new features or existing features are updated to +// use this new class then the lines will be uncommented +// +// As a result, only Realtime uses this feature at present (and the associated +// configuration DeviceConfiguration class and preferences pane + +static DeviceType SupportedDevices[] = +{ + { DEV_CT, DEV_SERIAL, (char *) "Computrainer", true, false }, + { DEV_ANTPLUS, DEV_ANT, (char *) "ANT+ via Quarqd", true, false }, +// { DEV_GSERVER, DEV_TCP, (char *) "Golden Cheetah Server", false, false }, +// { DEV_PT, DEV_SERIAL, (char *) "Powertap Head Unit", false, true }, +// { DEV_SRM, DEV_SERIAL, (char *) "SRM PowerControl V/VI", false, true }, +// { DEV_GCLIENT, DEV_TCP, (char *) "Golden Cheetah Client", false, false }, + { 0, 0, NULL, 0, 0 } +}; + +DeviceTypes::DeviceTypes() +{ + for (int i=0; SupportedDevices[i].type;i++) + Supported.append(SupportedDevices[i]); +} + +DeviceTypes::~DeviceTypes() +{} + +QList DeviceTypes::getList() +{ + return Supported; +} + +DeviceType DeviceTypes::getType(int type) +{ + for (int i=0; i< Supported.count(); i++) { + if (Supported.at(i).type == type) + return Supported.at(i); + } + return Supported.at(0); // yuck.whatever +} diff --git a/src/DeviceTypes.h b/src/DeviceTypes.h new file mode 100644 index 000000000..c89eb01d8 --- /dev/null +++ b/src/DeviceTypes.h @@ -0,0 +1,61 @@ +/* + * 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 + */ + + +// Lists all the device types supported + +#ifndef _GC_DeviceTypes_h +#define _GC_DeviceTypes_h 1 + +#include + +#define DEV_PT 0x0001 +#define DEV_SRM 0x0002 +#define DEV_CT 0x0010 +#define DEV_ANTPLUS 0x0020 +#define DEV_GSERVER 0x0100 // NOT IMPLEMENTED IN THIS RELEASE XXX +#define DEV_GCLIENT 0x0200 // NOT IMPLEMENTED IN THIS RELEASE XXX + +#define DEV_ANT 0x01 // ants use id:hostname:port +#define DEV_SERIAL 0x02 // use filename COMx or /dev/cuxxxx +#define DEV_TCP 0x03 // tcp port is hostname:port NOT IMPLEMENTED IN THIS RELEASE + +class DeviceType +{ + public: + int type; // type specifier - not sure if neccessary + int connector; // is it a serial or tcp device? + char *name; // narrative name + bool realtime; // can it do realtime + bool download; // can it do download? +}; + +class DeviceTypes +{ + public: + DeviceTypes(); + ~DeviceTypes(); + + QList Supported; // all the supported types in a list + QList getList(); // returns a list of the supported device types + + DeviceType getType(int); // return all details for type x +}; + +#endif // _GC_DeviceTypes_h + diff --git a/src/ErgFile.cpp b/src/ErgFile.cpp new file mode 100644 index 000000000..de792da18 --- /dev/null +++ b/src/ErgFile.cpp @@ -0,0 +1,361 @@ +/* + * 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 "ErgFile.h" + + +ErgFile::ErgFile(QString filename, int &mode) +{ + QFile ergFile(filename); + int section = NOMANSLAND; // section 0=init, 1=header data, 2=course data + double Cp=285; // default to 285 if zones are not set + leftPoint=rightPoint=0; + MaxWatts = Ftp = 0; + int lapcounter = 0; + int format = ERG; // either ERG or MRC + + // running totals for CRS file format + long rdist = 0; // running total for distance + long ralt = 200; // always start at 200 meters just to prettify the graph + + // Get users CP for relative watts calculations + if (mainwindow->zones) { + QDate today = QDate::currentDate(); + int range = mainwindow->zones->whichRange(today); + if (range != -1) Cp = mainwindow->zones->getCP(range); + } + + // open the file + if (ergFile.open(QIODevice::ReadOnly | QIODevice::Text) == false) { + valid = false; + return; + } + + // ok. opened ok lets parse. + QTextStream inputStream(&ergFile); + while (!inputStream.atEnd()) { + + // Code plagiarised from CsvRideFile. + // the readLine() method doesn't handle old Macintosh CR line endings + // this workaround will load the the entire file if it has CR endings + // then split and loop through each line + // otherwise, there will be nothing to split and it will read each line as expected. + QString linesIn = ergFile.readLine(); + QStringList lines = linesIn.split('\r'); + + // workaround for empty lines + if(lines.isEmpty()) { + continue; + } + + for (int li = 0; li < lines.size(); ++li) { + QString line = lines[li]; + + // Section markers + QRegExp startHeader("^.*\\[COURSE HEADER\\].*$", Qt::CaseInsensitive); + QRegExp endHeader("^.*\\[END COURSE HEADER\\].*$", Qt::CaseInsensitive); + QRegExp startData("^.*\\[COURSE DATA\\].*$", Qt::CaseInsensitive); + QRegExp endData("^.*\\[END COURSE DATA\\].*$", Qt::CaseInsensitive); + // ignore whitespace and support for ';' comments (a GC extension) + QRegExp ignore("^(;.*|[ \\t\\n]*)$", Qt::CaseInsensitive); + // workout settings + QRegExp settings("^([^=]*)=[ \\t]*([^=\\n\\r\\t]*).*$", Qt::CaseInsensitive); + + // format setting for ergformat + QRegExp ergformat("^[;]*(MINUTES[ \\t]+WATTS).*$", Qt::CaseInsensitive); + QRegExp mrcformat("^[;]*(MINUTES[ \\t]+PERCENT).*$", Qt::CaseInsensitive); + QRegExp crsformat("^[;]*(DISTANCE[ \\t]+GRADE[ \\t]+WIND).*$", Qt::CaseInsensitive); + + // time watts records + QRegExp absoluteWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)[ \\t\\n]*$", Qt::CaseInsensitive); + QRegExp relativeWatts("^[ \\t]*([0-9\\.]+)[ \\t]*([0-9\\.]+)%[ \\t\\n]*$", Qt::CaseInsensitive); + + // distance slope wind records + QRegExp absoluteSlope("^[ \\t]*([0-9\\.]+)[ \\t]*([-0-9\\.]+)[ \\t\\n]([-0-9\\.]+)[ \\t\\n]*$", + Qt::CaseInsensitive); + + // Lap marker in an ERG/MRC file + QRegExp lapmarker("^[ \\t]*([0-9\\.]+)[ \\t]*LAP[ \\t\\n]*$", Qt::CaseInsensitive); + QRegExp crslapmarker("^[ \\t]*LAP[ \\t\\n]*$", Qt::CaseInsensitive); + + // so what we go then? + if (startHeader.exactMatch(line)) { + section = SETTINGS; + } else if (endHeader.exactMatch(line)) { + section = NOMANSLAND; + } else if (startData.exactMatch(line)) { + section = DATA; + } else if (endData.exactMatch(line)) { + section = END; + } else if (ergformat.exactMatch(line)) { + // save away the format + mode = format = ERG; + } else if (mrcformat.exactMatch(line)) { + // save away the format + mode = format = ERG; + } else if (crsformat.exactMatch(line)) { + // save away the format + mode = format = CRS; + } else if (lapmarker.exactMatch(line)) { + // lap marker found + ErgFileLap add; + + add.x = lapmarker.cap(1).toDouble() * 60000; // from mins to 1000ths of a second + add.LapNum = ++lapcounter; + Laps.append(add); + + } else if (crslapmarker.exactMatch(line)) { + // new distance lapmarker + ErgFileLap add; + + add.x = rdist; + add.LapNum = ++lapcounter; + Laps.append(add); + + } else if (settings.exactMatch(line)) { + // we have name = value setting + QRegExp pversion("^VERSION *", Qt::CaseInsensitive); + if (pversion.exactMatch(settings.cap(1))) Version = settings.cap(2); + + QRegExp pfilename("^FILE NAME *", Qt::CaseInsensitive); + if (pfilename.exactMatch(settings.cap(1))) Filename = settings.cap(2); + + QRegExp pname("^DESCRIPTION *", Qt::CaseInsensitive); + if (pname.exactMatch(settings.cap(1))) Name = settings.cap(2); + + QRegExp punit("^UNITS *", Qt::CaseInsensitive); + if (punit.exactMatch(settings.cap(1))) { + Units = settings.cap(2); + // UNITS can be ENGLISH or METRIC (miles/km) + // XXX FIXME XXX + } + + QRegExp pftp("^FTP *", Qt::CaseInsensitive); + if (pftp.exactMatch(settings.cap(1))) Ftp = settings.cap(2).toInt(); + + } else if (absoluteWatts.exactMatch(line)) { + // we have mins watts line + ErgFilePoint add; + + add.x = absoluteWatts.cap(1).toDouble() * 60000; // from mins to 1000ths of a second + add.val = add.y = absoluteWatts.cap(2).toInt(); // plain watts + + switch (format) { + + case ERG: // its an absolute wattage + if (Ftp) { // adjust if target FTP is set. + // if ftp is set then convert to the users CP + + double watts = add.y; + double ftp = Ftp; + watts *= Cp/ftp; + add.y = add.val = watts; + } + break; + case MRC: // its a percent relative to CP (mrc file) + add.y *= Cp; + add.y /= 100.00; + add.val = add.y; + break; + } + Points.append(add); + if (add.y > MaxWatts) MaxWatts=add.y; + + } else if (relativeWatts.exactMatch(line)) { + + // we have a relative watts match + ErgFilePoint add; + add.x = relativeWatts.cap(1).toDouble() * 60000; // from mins to 1000ths of a second + add.val = add.y = (relativeWatts.cap(2).toDouble() /100.00) * Cp; + Points.append(add); + if (add.y > MaxWatts) MaxWatts=add.y; + + } else if (absoluteSlope.exactMatch(line)) { + // dist, grade, wind strength + ErgFilePoint add; + + // distance guff + add.x = rdist; + int distance = absoluteSlope.cap(1).toDouble() * 1000; // convert to meters + rdist += distance; + + // gradient and altitude + add.val = absoluteSlope.cap(2).toDouble(); + add.y = ralt; + ralt += distance * add.val /100.00 ; /* paused */ + + Points.append(add); + if (add.y > MaxWatts) MaxWatts=add.y; + + + } else if (ignore.exactMatch(line)) { + // do nothing for this line + } else { + // ignore bad lines for now. just bark. + qDebug()<<"huh?" << line; + } + + } + + } + + // done. + ergFile.close(); + + if (Points.count() > 0) { + valid = true; + + // add the last point for a crs file + if (mode == CRS) { + ErgFilePoint add; + add.x = rdist; + add.val = 0.0; + add.y = ralt; + Points.append(add); + if (add.y > MaxWatts) MaxWatts=add.y; + } + + // add a start point if it doesn't exist + if (Points.at(0).x > 0) { + ErgFilePoint add; + add.x = 0; + add.y = Points.at(0).y; + add.val = Points.at(0).val; + Points.insert(0, add); + } + + // set ErgFile duration + Duration = Points.last().x; // last is the end point in msecs + + leftPoint = 0; + rightPoint = 1; + + } else { + valid = false; + } +} + +ErgFile::~ErgFile() +{ + Points.clear(); +} + +bool +ErgFile::isValid() +{ + return valid; +} + +int +ErgFile::wattsAt(long x, int &lapnum) +{ + // workout what wattage load should be set for any given + // point in time in msecs. + + // is it in bounds? + if (x < 0 || x > Duration) return -100; // out of bounds!!! + + // do we need to return the Lap marker? + if (Laps.count() > 0) { + int lap=0; + for (int i=0; i=Laps.at(i).x) lap += 1; + } + lapnum = lap; + + } + + // find right section of the file + while (x < Points.at(leftPoint).x || x > Points.at(rightPoint).x) { + if (x < Points.at(leftPoint).x) { + leftPoint--; + rightPoint--; + } else if (x > Points.at(rightPoint).x) { + leftPoint++; + rightPoint++; + } + } + + // two different points in time but the same watts + // at both, it doesn't really matter which value + // we use + if (Points.at(leftPoint).val == Points.at(rightPoint).val) + return Points.at(rightPoint).val; + + // the erg file will list the point in time twice + // to show a jump from one wattage to another + // at this point in ime (i.e x=100 watts=100 followed + // by x=100 watts=200) + if (Points.at(leftPoint).x == Points.at(rightPoint).x) + return Points.at(rightPoint).val; + + // so this point in time between two points and + // we are ramping from one point and another + // the steps in the calculation have been explicitly + // listed for code clarity + double deltaW = Points.at(rightPoint).val - Points.at(leftPoint).val; + double deltaT = Points.at(rightPoint).x - Points.at(leftPoint).x; + double offT = x - Points.at(leftPoint).x; + double factor = offT / deltaT; + + double nowW = Points.at(leftPoint).val + (deltaW * factor); + + return nowW; +} + +double +ErgFile::gradientAt(long x, int &lapnum) +{ + // workout what wattage load should be set for any given + // point in time in msecs. + + // is it in bounds? + if (x < 0 || x > Duration) return -100; // out of bounds!!! (-10 through +15 are valid return vals) + + // do we need to return the Lap marker? + if (Laps.count() > 0) { + int lap=0; + for (int i=0; i=Laps.at(i).x) lap += 1; + } + lapnum = lap; + } + + // find right section of the file + while (x < Points.at(leftPoint).x || x > Points.at(rightPoint).x) { + if (x < Points.at(leftPoint).x) { + leftPoint--; + rightPoint--; + } else if (x > Points.at(rightPoint).x) { + leftPoint++; + rightPoint++; + } + } + return Points.at(leftPoint).val; +} + +int ErgFile::nextLap(long x) +{ + // do we need to return the Lap marker? + if (Laps.count() > 0) { + for (int i=0; i +#include +#include +#include +#include +#include +#include +#include +#include +#include "Zones.h" // For zones ... see below vvvv +#include "MainWindow.h" // gets access to mainwindow to read zones info + +// which section of the file are we in? +#define NOMANSLAND 0 +#define SETTINGS 1 +#define DATA 2 +#define END 3 + +// is this in .erg or .mrc format? +#define ERG 1 +#define MRC 2 +#define CRS 3 + +class ErgFilePoint +{ + public: + long x; // x axis - time in msecs or distance in meters + long y; // y axis - load in watts or altitude + + double val; // the value to send to the device (watts/gradient) +}; + +class ErgFileLap +{ + public: + long x; // when does this LAP marker occur? (time in msecs or distance in meters + int LapNum; // from 1 - n +}; + +class ErgFile +{ + public: + ErgFile(QString, int&); // constructor uses filename + ~ErgFile(); // delete the contents + + bool isValid(); // is the file valid or not? + int wattsAt(long, int&); // return the watts value for the passed msec + double gradientAt(long, int&); // return the gradient value for the passed meter + int nextLap(long); // return the msecs value for the next Lap marker + + QString Version, // version number / identifer + Units, // units used + Filename, // filename from inside file + Name, // workout name + Format; // only ever seen MINUTES WATTS + long Duration; // Duration of this workout in msecs + int Ftp; // FTP this file was targetted at + int MaxWatts; // maxWatts in this ergfile (scaling) + bool valid; // did it parse ok? + + int leftPoint, rightPoint; // current points we are between + + QList Points; // points in workout + QList Laps; // interval markers in the file +}; + +#endif diff --git a/src/ErgFilePlot.cpp b/src/ErgFilePlot.cpp new file mode 100644 index 000000000..6fd52cac7 --- /dev/null +++ b/src/ErgFilePlot.cpp @@ -0,0 +1,121 @@ +/* + * 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 "ErgFilePlot.h" + +// static locals for now +static ErgFile *ergFile; +static QList *courseData; +static long Now; +static int MaxWatts; + +// Load history +double ErgFileData::x(size_t i) const { return courseData ? courseData->at(i).x : 0; } +double ErgFileData::y(size_t i) const { return courseData ? courseData->at(i).y : 0; } +size_t ErgFileData::size() const { return courseData ? courseData->count() : 0; } +QwtData *ErgFileData::copy() const { return new ErgFileData(); } +//void ErgFileData::init() { } + +// Now bar +double NowData::x(size_t) const { return Now; } +double NowData::y(size_t i) const { return (i ? MaxWatts : 0); } +size_t NowData::size() const { return 2; } +QwtData *NowData::copy() const { return new NowData(); } + +ErgFilePlot::ErgFilePlot(QList *data) +{ + //insertLegend(new QwtLegend(), QwtPlot::BottomLegend); + setCanvasBackground(Qt::white); + courseData = data; // what we plot + Now = 0; // where we are + + // Setup the axis (of evil :-) + setAxisTitle(yLeft, "Watts"); + //setAxisScale(yLeft, 0, 500); // watts -- Autoscale please! + enableAxis(yLeft, false); + enableAxis(xBottom, false); // very little value and some cpu overhead + enableAxis(yRight, false); + + // Load Curve + LodCurve = new QwtPlotCurve("Course Load"); + QPen Lodpen = QPen(Qt::blue, 1.0); + LodCurve->setPen(Lodpen); + LodCurve->setData(lodData); + LodCurve->attach(this); + LodCurve->setYAxis(QwtPlot::yLeft); + QColor brush_color = QColor(124, 91, 31); + brush_color.setAlpha(64); + LodCurve->setBrush(brush_color); // fill below the line + + // Now pointer + NowCurve = new QwtPlotCurve("Now"); + QPen Nowpen = QPen(Qt::red, 2.0); + NowCurve->setPen(Nowpen); + NowCurve->setData(nowData); + NowCurve->attach(this); + NowCurve->setYAxis(QwtPlot::yLeft); + +} + +void +ErgFilePlot::setData(ErgFile *ergfile) +{ + if (ergfile) { + + // set up again + ergFile = ergfile; + setTitle(ergFile->Name); + courseData = &ergfile->Points; + MaxWatts = ergfile->MaxWatts; + + // clear the previous marks (if any) + for(int i=0; idetach(); + delete Marks.at(i); + } + Marks.clear(); + + for(int i=0; i < ergFile->Laps.count(); i++) { + + // Show Lap Number + QwtText text(QString::number(ergFile->Laps.at(i).LapNum)); + text.setFont(QFont("Helvetica", 10, QFont::Bold)); + text.setColor(Qt::black); + + // vertical line + QwtPlotMarker *add = new QwtPlotMarker(); + add->setLineStyle(QwtPlotMarker::VLine); + add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop); + add->setLinePen(QPen(Qt::black, 0, Qt::DashDotLine)); + add->setValue(ergFile->Laps.at(i).x, 0.0); + add->setLabel(text); + add->attach(this); + + Marks.append(add); + } + } +} + +void +ErgFilePlot::setNow(long msecs) +{ + Now = msecs; + replot(); // and update +} + diff --git a/src/ErgFilePlot.h b/src/ErgFilePlot.h new file mode 100644 index 000000000..1fa2dc301 --- /dev/null +++ b/src/ErgFilePlot.h @@ -0,0 +1,83 @@ +/* + * 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 + */ + +#ifndef _GC_ErgFilePlot_h +#define _GC_ErgFilePlot_h 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ErgFile.h" + + +class ErgFileData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; +}; + +class NowData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; +}; + +class ErgFilePlot : public QwtPlot +{ + Q_OBJECT + + public: + + ErgFilePlot(QList * = 0); + QList Marks; + + void setData(ErgFile *); // set the course + void setNow(long); // set point we're add for progress pointer + //void plot(); + + private: + + QwtPlotGrid *grid; + QwtPlotCurve *LodCurve; + QwtPlotCurve *NowCurve; + + ErgFileData lodData; + NowData nowData; + + ErgFilePlot(); + +}; + + + +#endif + diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 20f50fed0..20951091e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -20,12 +20,14 @@ #include "AllPlotWindow.h" #include "BestIntervalDialog.h" #include "ChooseCyclistDialog.h" +#include "Computrainer.h" #include "ConfigDialog.h" #include "CriticalPowerWindow.h" #include "PfPvWindow.h" #include "DownloadRideDialog.h" #include "ManualRideDialog.h" #include "HistogramWindow.h" +#include "RealtimeWindow.h" #include "RideItem.h" #include "RideFile.h" #include "RideImportWizard.h" @@ -225,6 +227,10 @@ MainWindow::MainWindow(const QDir &home) : performanceManagerWindow = new PerformanceManagerWindow(); tabWidget->addTab(performanceManagerWindow, "Performance Manager"); + //////////////////////// Realtime //////////////////////// + + realtimeWindow = new RealtimeWindow(this, home); + tabWidget->addTab(realtimeWindow, tr("Realtime")); ////////////////////////////// Signals ////////////////////////////// @@ -934,4 +940,3 @@ void MainWindow::dateChanged(const QDate &date) } } - diff --git a/src/MainWindow.h b/src/MainWindow.h index 09cb8a28d..fa91f4b5c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -24,6 +24,7 @@ #include #include #include "RideItem.h" +#include "QuarqdClient.h" #include class AllPlotWindow; @@ -33,6 +34,7 @@ class PfPvWindow; class QwtPlotPanner; class QwtPlotPicker; class QwtPlotZoomer; +class RealtimeWindow; class RideFile; class WeeklySummaryWindow; class Zones; @@ -52,6 +54,10 @@ class MainWindow : public QMainWindow QDir home; void setCriticalPower(int cp); + RealtimeWindow *realtimeWindow; // public so config dialog can notify it of changes config + + Zones *zones; // used in ErgFile to get access to power.zones without having to re-read + protected: virtual void resizeEvent(QResizeEvent*); virtual void moveEvent(QMoveEvent*); @@ -108,7 +114,6 @@ class MainWindow : public QMainWindow QwtPlotCurve *weeklyRICurve; PerformanceManagerWindow *performanceManagerWindow; - Zones *zones; // pedal force/pedal velocity scatter plot widgets PfPvWindow *pfPvWindow; @@ -123,7 +128,10 @@ class MainWindow : public QMainWindow float timebsfactor; float distancebsfactor; + QuarqdClient *client; }; +extern MainWindow *mainwindow; // so ConfigDialog can update widgets when config changes + #endif // _GC_MainWindow_h diff --git a/src/Pages.cpp b/src/Pages.cpp index 325bf8971..63b5141bc 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -3,7 +3,9 @@ #include #include "Pages.h" #include "Settings.h" - +#include "DeviceTypes.h" +#include "DeviceConfiguration.h" +#include "ANTplusController.h" ConfigurationPage::~ConfigurationPage() { @@ -388,3 +390,339 @@ bool CyclistPage::isNewMode() { return (checkboxNew->checkState() == Qt::Checked); } + +DevicePage::DevicePage(QWidget *parent) : QWidget(parent) +{ + DeviceTypes all; + devices = all.getList(); + + nameLabel = new QLabel(tr("Device Name"),this); + deviceName = new QLineEdit(tr(""), this); + + typeLabel = new QLabel(tr("Device Type"),this); + typeSelector = new QComboBox(this); + + for (int i=0; i< devices.count(); i++) { + DeviceType cur = devices.at(i); + + // WARNING: cur.type is what is stored in configuration + // do not change this!! + typeSelector->addItem(cur.name, cur.type); + } + + specLabel = new QLabel(tr("Device Port"),this); + specHint = new QLabel(); + profHint = new QLabel(); + deviceSpecifier= new QLineEdit(tr(""), this); + + profLabel = new QLabel(tr("Device Profile"),this); + deviceProfile = new QLineEdit(tr(""), this); + +// THIS CODE IS DISABLED FOR THIS RELEASE XXX +// isDefaultDownload = new QCheckBox(tr("Default download device"), this); +// isDefaultRealtime = new QCheckBox(tr("Default realtime device"), this); + + addButton = new QPushButton(tr("Add"),this); + delButton = new QPushButton(tr("Delete"),this); + pairButton = new QPushButton(tr("Pair"),this); + + deviceList = new QTableView(this); + deviceListModel = new deviceModel(this); + + // replace standard model with ours + QItemSelectionModel *stdmodel = deviceList->selectionModel(); + deviceList->setModel(deviceListModel); + delete stdmodel; + + deviceList->setSortingEnabled(false); + deviceList->setSelectionBehavior(QAbstractItemView::SelectRows); + deviceList->horizontalHeader()->setStretchLastSection(true); + deviceList->verticalHeader()->hide(); + deviceList->setEditTriggers(QAbstractItemView::NoEditTriggers); + deviceList->setSelectionMode(QAbstractItemView::SingleSelection); + + leftLayout = new QGridLayout(); + rightLayout = new QVBoxLayout(); + inLayout = new QGridLayout(); + deviceGroup = new QGroupBox(tr("Device Configuration"), this); + + leftLayout->addWidget(nameLabel, 0,0); + leftLayout->addWidget(deviceName, 0,2); + leftLayout->setRowMinimumHeight(1,10); + leftLayout->addWidget(typeLabel, 2,0); + leftLayout->addWidget(typeSelector, 2,2); + leftLayout->setRowMinimumHeight(3,10); + leftLayout->addWidget(specHint, 4,2); + leftLayout->addWidget(specLabel, 5,0); + leftLayout->addWidget(deviceSpecifier, 5,2); + leftLayout->setRowMinimumHeight(6,10); + leftLayout->addWidget(profHint, 7,2); + leftLayout->addWidget(profLabel, 8,0); + leftLayout->addWidget(deviceProfile, 8,2); + + leftLayout->setColumnMinimumWidth(1,10); + +// THIS CODE IS DISABLED FOR THIS RELEASE XXX +// leftLayout->addWidget(isDefaultDownload, 6,1); +// leftLayout->addWidget(isDefaultRealtime, 8,1); + +// leftLayout->setRowStretch(0, 2); +// leftLayout->setRowStretch(1, 1); +// leftLayout->setRowStretch(2, 2); +// leftLayout->setRowStretch(3, 1); +// leftLayout->setRowStretch(4, 2); +// leftLayout->setRowStretch(5, 1); +// leftLayout->setRowStretch(6, 2); +// leftLayout->setRowStretch(7, 1); +// leftLayout->setRowStretch(8, 2); + + rightLayout->addWidget(addButton); + rightLayout->addSpacing(10); + rightLayout->addWidget(delButton); + rightLayout->addStretch(); + rightLayout->addWidget(pairButton); + + inLayout->addItem(leftLayout, 0,0); + inLayout->addItem(rightLayout, 0,1); + inLayout->addWidget(deviceList,1,0,1,2); + + deviceGroup->setLayout(inLayout); + + mainLayout = new QVBoxLayout; + mainLayout->addWidget(deviceGroup); + mainLayout->addStretch(1); + setLayout(mainLayout); + + // to make sure the default checkboxes have been set appropiately... + // THIS CODE IS DISABLED IN THIS RELEASE XXX + // isDefaultRealtime->setEnabled(false); + + setConfigPane(); +} + +void +DevicePage::setConfigPane() +{ + // depending upon the type of device selected + // the spec hint tells the user the format they should use + DeviceTypes Supported; + + // sorry... ;-) obfuscated c++ contest winner 2009 + switch (Supported.getType(typeSelector->itemData(typeSelector->currentIndex()).toInt()).connector) { + + case DEV_ANT: + specHint->setText("hostname:port"); + profHint->setText("antid 1, antid 2 ..."); + profHint->show(); + pairButton->show(); + profLabel->show(); + deviceProfile->show(); + break; + case DEV_SERIAL: +#ifdef WIN32 + specHint->setText("COMx"); +#else + specHint->setText("/dev/xxxx"); +#endif + pairButton->hide(); + profHint->hide(); + profLabel->hide(); + deviceProfile->hide(); + break; + case DEV_TCP: + specHint->setText("hostname:port"); + pairButton->hide(); + profHint->hide(); + profLabel->hide(); + deviceProfile->hide(); + break; + } + //specHint->setTextFormat(Qt::Italic); // mmm need to read the docos +} + + +// add a new configuration +void +deviceModel::add(DeviceConfiguration &newone) +{ + insertRows(0,1, QModelIndex()); + + // insert name + QModelIndex index = deviceModel::index(0,0, QModelIndex()); + setData(index, newone.name, Qt::EditRole); + + // insert type + index = deviceModel::index(0,1, QModelIndex()); + setData(index, newone.type, Qt::EditRole); + + // insert portSpec + index = deviceModel::index(0,2, QModelIndex()); + setData(index, newone.portSpec, Qt::EditRole); + + // insert Profile + index = deviceModel::index(0,3, QModelIndex()); + setData(index, newone.deviceProfile, Qt::EditRole); +} + +// delete an existing configuration +void +deviceModel::del() +{ + // which row is selected in the table? + DevicePage *temp = static_cast(parent); + QItemSelectionModel *selectionModel = temp->deviceList->selectionModel(); + + QModelIndexList indexes = selectionModel->selectedRows(); + QModelIndex index; + + foreach (index, indexes) { + //int row = this->mapToSource(index).row(); + removeRows(index.row(), 1, QModelIndex()); + } +} + +void +DevicePage::pairClicked(DeviceConfiguration *dc, QProgressDialog *progress) +{ + ANTplusController ANTplus(0, dc); + ANTplus.discover(dc, progress); + deviceProfile->setText(dc->deviceProfile); +} + +DevicePage::~DevicePage() +{ +} + +deviceModel::deviceModel(QObject *parent) : QAbstractTableModel(parent) +{ + this->parent = parent; + + // get current configuration + DeviceConfigurations all; + Configuration = all.getList(); +} + +int +deviceModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return Configuration.size(); +} + +int +deviceModel::columnCount(const QModelIndex &) const +{ + return 4; +} + + +// setup the headings! +QVariant deviceModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) return QVariant(); // no display, no game! + + if (orientation == Qt::Horizontal) { + switch (section) { + case 0: + return tr("Device Name"); + case 1: + return tr("Device Type"); + case 2: + return tr("Port Spec"); + case 3: + return tr("Profile"); + default: + return QVariant(); + } + } + return QVariant(); + } + +// return data item for row/col specified in index +QVariant deviceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) return QVariant(); + if (index.row() >= Configuration.size() || index.row() < 0) return QVariant(); + + if (role == Qt::DisplayRole) { + DeviceConfiguration Entry = Configuration.at(index.row()); + + switch(index.column()) { + case 0 : return Entry.name; + break; + case 1 : + { + DeviceTypes all; + DeviceType lookupType = all.getType (Entry.type); + return lookupType.name; + } + break; + case 2 : + return Entry.portSpec; + break; + case 3 : + return Entry.deviceProfile; + } + } + + // how did we get here!? + return QVariant(); +} + +// update the model with new data +bool deviceModel::insertRows(int position, int rows, const QModelIndex &index) +{ + Q_UNUSED(index); + beginInsertRows(QModelIndex(), position, position+rows-1); + + for (int row=0; row < rows; row++) { + DeviceConfiguration emptyEntry; + Configuration.insert(position, emptyEntry); + } + endInsertRows(); + return true; +} + +// delete a row! +bool deviceModel::removeRows(int position, int rows, const QModelIndex &index) +{ + Q_UNUSED(index); + beginRemoveRows(QModelIndex(), position, position+rows-1); + + for (int row=0; row < rows; ++row) { + Configuration.removeAt(position); + } + + endRemoveRows(); + return true; +} + +bool deviceModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.isValid() && role == Qt::EditRole) { + int row = index.row(); + + DeviceConfiguration p = Configuration.value(row); + + switch (index.column()) { + case 0 : //name + p.name = value.toString(); + break; + case 1 : //type + p.type = value.toInt(); + break; + case 2 : // spec + p.portSpec = value.toString(); + break; + case 3 : // Profile + p.deviceProfile = value.toString(); + break; + } + Configuration.replace(row,p); + emit(dataChanged(index, index)); + + return true; + } + + return false; +} diff --git a/src/Pages.h b/src/Pages.h index d3ab04f3f..7d406b555 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include #include #include "Zones.h" @@ -14,6 +17,9 @@ #include #include #include +#include +#include "DeviceTypes.h" +#include "DeviceConfiguration.h" class QGroupBox; class QHBoxLayout; @@ -105,4 +111,78 @@ class CyclistPage : public QWidget QIntValidator *perfManSTSavgValidator; QIntValidator *perfManLTSavgValidator; }; + +class deviceModel : public QAbstractTableModel +{ + + public: + deviceModel(QObject *parent=0); + QObject *parent; + + // sets up the headers + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + + // how much data do we have? + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + + // manipulate the data - data() gets and setData() sets (set/get might be better?) + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + // insert/remove and update + void add(DeviceConfiguration &); // add a new DeviceConfiguration + void del(); // add a new DeviceConfiguration + bool insertRows(int position, int rows, const QModelIndex &index=QModelIndex()); + bool removeRows(int position, int rows, const QModelIndex &index=QModelIndex()); + + + QList Configuration; // the actual data + }; + +class DevicePage : public QWidget +{ + public: + ~DevicePage(); + DevicePage(QWidget *parent = 0); + void setConfigPane(); + void pairClicked(DeviceConfiguration *, QProgressDialog *); + + QList devices; + + // GUI Elements + QGroupBox *deviceGroup; + QLabel *nameLabel; + QLineEdit *deviceName; + + QLabel *typeLabel; + QComboBox *typeSelector; + + QLabel *specLabel; + QLabel *specHint; // hints at the format for a port spec + QLabel *profHint; // hints at the format for profile info + QLineEdit *deviceSpecifier; + + QLabel *profLabel; + QLineEdit *deviceProfile; + + QCheckBox *isDefaultDownload; + QCheckBox *isDefaultRealtime; + + QTableView *deviceList; + + QPushButton *addButton; + QPushButton *delButton; + QPushButton *pairButton; + + QGridLayout *leftLayout; + QVBoxLayout *rightLayout; + + QGridLayout *inLayout; + QVBoxLayout *mainLayout; + + deviceModel *deviceListModel; +}; + + #endif diff --git a/src/QuarqdClient.cpp b/src/QuarqdClient.cpp new file mode 100644 index 000000000..51ab69e48 --- /dev/null +++ b/src/QuarqdClient.cpp @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca) + * + * 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 +#include +#include +#include +#include "QuarqdClient.h" +#include "RealtimeData.h" + +/* Control status */ +#define ANT_RUNNING 0x01 +#define ANT_PAUSED 0x02 + +QuarqdClient::QuarqdClient(QObject *parent, DeviceConfiguration *config) : QThread(parent) +{ + Status=0; + + // server hostname and TCP port# + deviceHostname = config->portSpec.section(':',0,0).toAscii(); // after the colon + devicePort = (int)QString(config->portSpec).section(':',1,1).toInt(); // after the colon + antIDs = config->deviceProfile.split(","); + lastReadWatts = 0; + lastReadRPM = 0; + +} + +QuarqdClient::~QuarqdClient() +{ + //free(deviceHostname); +} + +void QuarqdClient::run() +{ + // int counter=0; + int status; // control commands from controller + bool isPortOpen = false; + + Status = ANT_RUNNING; + QString strBuf; + + openPort(); + isPortOpen = true; + initChannel(); + elapsedTime.start(); + + while(1) + { + if(tcpSocket->isValid()) + { + if(!tcpSocket->waitForReadyRead(-1)) + { + return; + } + QByteArray array = tcpSocket->readAll(); + strBuf = array; + parseElement(strBuf); // updates local telemetry + } + + + //---------------------------------------------------------------------- + // LISTEN TO CONTROLLER FOR COMMANDS + //---------------------------------------------------------------------- + pvars.lock(); + status = this->Status; + pvars.unlock(); + + /* time to shut up shop */ + if (!(status&ANT_RUNNING)) { + // time to stop! + quit(0); + return; + } + + if ((status&ANT_PAUSED) && isPortOpen == true) { + closePort(); + isPortOpen = false; + + } else if (!(status&ANT_PAUSED) && (status&ANT_RUNNING) && isPortOpen == false) { + closePort(); + if (openPort()) { + quit(2); + return; // open failed! + } + isPortOpen = true; + //init the channel + initChannel(); + + } + } + + +} + +void +QuarqdClient::parseElement(QString &strBuf) // updates QuarqdClient::telemetry +{ + QStringList qList = strBuf.split("\n"); + + //Loop for all the elements. + for(int i=0; i 5000) + telemetry.setRPM(0); + + if(elapsedTime.elapsed() - lastReadWatts > 5000) + telemetry.setWatts(0); + + } + + return; +} + +long QuarqdClient::getTimeStamp(QString &str) +{ + //Get the timestamp + int start = str.indexOf("timestamp='"); + start += 11; + int end = str.indexOf(".", start); + qDebug() << str.mid(start, end - start); + return str.mid(start, end - start).toLong(); +} + +int +QuarqdClient::openPort() +{ + tcpSocket = new QTcpSocket(); + tcpSocket->connectToHost(deviceHostname, devicePort); + return 0; +} + +int +QuarqdClient::closePort() +{ + tcpSocket->close(); + delete tcpSocket; + return 0; +} + +// +// All stuff below here is to support startup / shutdown commands and +// interaction with the ANTplusController class between the QuarqdClient +// and the GUI +// +int +QuarqdClient::start() +{ + QThread::start(); + return 0; +} + +int +QuarqdClient::restart() +{ + int status; + + // get current status + pvars.lock(); + status = this->Status; + pvars.unlock(); + // what state are we in anyway? + if (status&ANT_RUNNING && status&ANT_PAUSED) { + status &= ~ANT_PAUSED; + pvars.lock(); + this->Status = status; + pvars.unlock(); + return 0; // ok its running again! + } + return 2; +} + +int +QuarqdClient::pause() +{ + int status; + + // get current status + pvars.lock(); + status = this->Status; + pvars.unlock(); + + if (status&ANT_PAUSED) return 2; // already paused you muppet! + else if (!(status&ANT_RUNNING)) return 4; // not running anyway, fool! + else { + + // ok we're running and not paused so lets pause + status |= ANT_PAUSED; + pvars.lock(); + this->Status = status; + pvars.unlock(); + + return 0; + } +} + +int +QuarqdClient::stop() +{ + int status; + + // get current status + pvars.lock(); + status = this->Status; + pvars.unlock(); + + closePort(); + + // what state are we in anyway? + pvars.lock(); + Status = 0; // Terminate it! + pvars.unlock(); + return 0; +} + + +int +QuarqdClient::quit(int code) +{ +qDebug()<<"QUIT CODE" << code; + // event code goes here! + closePort(); + exit(code); + return 0; // never gets here obviously but shuts up the compiler! +} + +bool +QuarqdClient::discover(DeviceConfiguration *config, QProgressDialog *progress) +{ + QString strBuf; + QStringList strList; + sentDual = false; + sentSpeed = false; + sentHR = false; + sentCad = false; + sentPWR = false; + + openPort(); + + QByteArray strPwr("X-set-channel: 0p"); //Power + QByteArray strHR("X-set-channel: 0h"); //Heart Rate + QByteArray strSpeed("X-set-channel: 0s"); //Speed + QByteArray strCad("X-set-channel: 0c"); //Cadence + QByteArray strDual("X-set-channel: 0d"); //Dual (Speed/Cadence) + + // tell quarqd to start scanning.... + if (tcpSocket->isOpen()) { + tcpSocket->write(strPwr); //Power + + } + + QTime start; + start.start(); + progress->setMaximum(50000); + + while(start.elapsed() <= 50000) //Scan for 50 seconds. + { + if (progress->wasCanceled()) + { + tcpSocket->close(); + return false; + } + + progress->setValue(start.elapsed()); + + if(start.elapsed() >= 40000 && sentDual == false) + { + sentDual = true; + tcpSocket->write(strDual); //Dual + } else if(start.elapsed() >= 30000 && sentSpeed == false) + { + sentSpeed = true; + tcpSocket->write(strSpeed); //Speed + } else if(start.elapsed() >= 20000 && sentHR == false) + { + sentHR = true; + tcpSocket->write(strHR); //HR + } else if(start.elapsed() >= 10000 && sentCad == false) + { + sentCad = true; + tcpSocket->write(strCad); //Cadence + } else if(start.elapsed() >= 0 && sentPWR == false) + { + sentPWR = true; + tcpSocket->write(strPwr); + } + + if (tcpSocket->bytesAvailable() > 0) { + QByteArray array = tcpSocket->readAll(); + strBuf = array; + qDebug() << strBuf; + QStringList qList = strBuf.split("\n"); + + //Loop for all the elements. + for(int i=0; isetValue(start.elapsed()); + QString str = qList.at(i); + qDebug() << str; + if(str.contains("id")) + { + int start = str.indexOf("id"); + start += 4; + int end = str.indexOf("'", start); + QString id = str.mid(start, end - start); + if(!strList.contains(id)) + { + if(id != "0p" && id != "0h" && id != "0s" && id != "0c" && id != "0d") + strList.append(id); + } + } + } + } + } + progress->setValue(40000);//We are done. + //Now return a comma delimited string. + for(int i=0; i < strList.size(); i++) + { + config->deviceProfile.append(strList.at(i)); + if(i < strList.size() -1) + config->deviceProfile.append(','); + } + + return config; +} + +RealtimeData +QuarqdClient::getRealtimeData() +{ + return telemetry; +} + +void +QuarqdClient::initChannel() +{ + + if(!tcpSocket->isValid()) + return; + + QByteArray setChannel("X-set-channel: "); + QByteArray channel; + + for(int i=0; i < antIDs.size(); i++) + { + if(tcpSocket->isValid()) + { + channel.clear(); + channel = setChannel; + channel.append(antIDs.at(i)); + tcpSocket->write(channel); + qDebug() << channel; + sleep(2); + + } + } + + QByteArray setBlanking = "X-set-timeout: blanking=1800"; + QByteArray setDrop = "X-set-timeout: drop=3600"; + QByteArray setLost = "X-set-timeout: lost=7200"; + + if(!tcpSocket->isValid()) + { + tcpSocket->write(setChannel); + tcpSocket->write(setDrop); + tcpSocket->write(setLost); + } + +} diff --git a/src/QuarqdClient.h b/src/QuarqdClient.h new file mode 100644 index 000000000..a50fd2d0a --- /dev/null +++ b/src/QuarqdClient.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca) + * + * 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 + */ + + +#ifndef CLIENT_H +#define CLIENT_H + +#include +#include +#include +#include +#include +#include +#include +#include "RealtimeData.h" +#include "DeviceConfiguration.h" + +class QuarqdClient : public QThread +{ + + Q_OBJECT + +public: + QuarqdClient(QObject *parent = 0, DeviceConfiguration *dc=0); + ~QuarqdClient(); + + int start(); // Calls QThread to start + int restart(); // restart after paused + int pause(); // pauses data collection, inbound telemetry is discarded + int stop(); // stops data collection thread + int quit(int error); // called by thread before exiting + bool discover(DeviceConfiguration *, QProgressDialog *); // confirm Server available at portSpec + + RealtimeData getRealtimeData(); // return current realtime data + // SET + void setDevice(DeviceConfiguration); // setup the device filename + QString getAntID(); + +private: + QTcpSocket *tcpSocket; // IMPORTANT MUST BE CREATED IN THREAD + QMutex pvars; // lock/unlock access to telemetry data between thread and controller + int Status; // what status is the client in? + + void run(); + + int openPort(), closePort(); // open and close socket + void initChannel(); + + void parseElement(QString &str); // reads input string and updates current telemetry values + long getTimeStamp(QString &str); + bool parsePortSpec(char *, char &, int &); // parse a port spec from string to ip, portnum + + // Current (Last read) Realtime Data + RealtimeData telemetry; + + // server hostname and TCP port# + QString deviceHostname; + int devicePort; + QStringList antIDs; + long lastReadWatts; + long lastReadRPM; + QTime elapsedTime; + bool sentDual, sentSpeed, sentHR, sentCad, sentPWR; + +}; + + +#endif diff --git a/src/RealtimeController.cpp b/src/RealtimeController.cpp new file mode 100644 index 000000000..76fde9e62 --- /dev/null +++ b/src/RealtimeController.cpp @@ -0,0 +1,36 @@ +/* + * 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 "RealtimeController.h" +#include "RealtimeWindow.h" +#include "RealtimeData.h" + +// Abstract base class for Realtime device controllers + +RealtimeController::RealtimeController(RealtimeWindow *parent) { this->parent = parent; } +int RealtimeController::start() { return 0; } +int RealtimeController::restart() { return 0; } +int RealtimeController::pause() { return 0; } +int RealtimeController::stop() { return 0; } +bool RealtimeController::discover(char *) { return false; } +bool RealtimeController::doesPull() { return false; } +bool RealtimeController::doesPush() { return false; } +bool RealtimeController::doesLoad() { return false; } +void RealtimeController::getRealtimeData(RealtimeData &) { } +void RealtimeController::pushRealtimeData(RealtimeData &) { } // update realtime data with current values + diff --git a/src/RealtimeController.h b/src/RealtimeController.h new file mode 100644 index 000000000..87fe96273 --- /dev/null +++ b/src/RealtimeController.h @@ -0,0 +1,58 @@ +/* + * 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 + */ + + +// Abstract base class for Realtime device controllers +#include "RealtimeData.h" +#include "RealtimeWindow.h" + +#ifndef _GC_RealtimeController_h +#define _GC_RealtimeController_h 1 + + +class RealtimeController +{ + +public: + RealtimeWindow *parent; // for push devices + + RealtimeController (RealtimeWindow *parent); + + virtual int start(); + virtual int restart(); // restart after paused + virtual int pause(); // pauses data collection, inbound telemetry is discarded + virtual int stop(); // stops data collection thread + virtual bool discover(char *pathname); // tell if a device is present at port passed + + // push or pull telemetry + virtual bool doesPush(); // this device is a push device (e.g. Quarq) + virtual bool doesPull(); // this device is a pull device (e.g. CT) + virtual bool doesLoad(); // this device can generate Load + + // will update the realtime data with current data (only called for doesPull devices) + virtual void getRealtimeData(RealtimeData &rtData); // update realtime data with current values + virtual void pushRealtimeData(RealtimeData &rtData); // update realtime data with current values + + // only relevant for Computrainer like devices + virtual void setLoad(double) { return; } + virtual void setGradient(double) { return; } + virtual void setMode(int) { return; } +}; + +#endif // _GC_RealtimeController_h + diff --git a/src/RealtimeData.cpp b/src/RealtimeData.cpp new file mode 100644 index 000000000..97f340530 --- /dev/null +++ b/src/RealtimeData.cpp @@ -0,0 +1,77 @@ + +/* + * Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca) + * + * 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 "RealtimeData.h" + +RealtimeData::RealtimeData() +{ + watts = hr = speed = rpm = load = 0; + time = 0; +} + +void RealtimeData::setWatts(double watts) +{ + this->watts = watts; +} +void RealtimeData::setHr(double hr) +{ + this->hr = hr; +} +void RealtimeData::setTime(long time) +{ + this->time = time; +} +void RealtimeData::setSpeed(double speed) +{ + this->speed = speed; +} +void RealtimeData::setRPM(double rpm) +{ + this->rpm = rpm; +} +void RealtimeData::setLoad(double load) +{ + this->load = load; +} + +double RealtimeData::getWatts() +{ + return watts; +} +double RealtimeData::getHr() +{ + return hr; +} +long RealtimeData::getTime() +{ + return time; +} + +double RealtimeData::getSpeed() +{ + return speed; +} +double RealtimeData::getRPM() +{ + return rpm; +} +double RealtimeData::getLoad() +{ + return load; +} diff --git a/src/RealtimeData.h b/src/RealtimeData.h new file mode 100644 index 000000000..d1b338acf --- /dev/null +++ b/src/RealtimeData.h @@ -0,0 +1,51 @@ +/* + * 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 + */ + + +#ifndef _GC_RealtimeData_h +#define _GC_RealtimeData_h 1 + +class RealtimeData +{ + + +public: + + RealtimeData(); + void reset(); // set all values to zero + void setWatts(double watts); + void setHr(double hr); + void setTime(long time); + void setSpeed(double speed); + void setRPM(double rpm); + void setLoad(double load); + double getWatts(); + double getHr(); + long getTime(); + double getSpeed(); + double getRPM(); + double getLoad(); + +private: + double hr, watts, rpm, speed, load; + unsigned long time; + +}; + + +#endif diff --git a/src/RealtimePlot.cpp b/src/RealtimePlot.cpp new file mode 100644 index 000000000..849a1b438 --- /dev/null +++ b/src/RealtimePlot.cpp @@ -0,0 +1,174 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include "RealtimePlot.h" + +// infuriating qwtdata api... +// power stores last 30 seconds for 30 second rolling avg, all else +// just the last 30 seconds +double pwrData[150], cadData[50], hrData[50], spdData[50], lodData[50]; +double pwr30; +int pwrCur, cadCur, hrCur, spdCur, lodCur; + + +// Power history +double RealtimePwrData::x(size_t i) const { return (double)50-i; } +double RealtimePwrData::y(size_t i) const { return pwrData[(pwrCur+100+i) <150 ? (pwrCur+100+i) : (pwrCur+100+i-150)]; } +size_t RealtimePwrData::size() const { return 50; } +QwtData *RealtimePwrData::copy() const { return new RealtimePwrData(); } +void RealtimePwrData::init() { pwrCur=0; for (int i=0; i<150; i++) pwrData[i]=0; } +void RealtimePwrData::addData(double v) { pwrData[pwrCur++] = v; if (pwrCur==150) pwrCur=0; } + +// 30 second Power rolling avg +double Realtime30PwrData::x(size_t i) const { return (double)50-i; } + +double Realtime30PwrData::y(size_t i) const { if (i==1) { pwr30=0; for (int x=0; x<150; x++) { pwr30+=pwrData[x]; } pwr30 /= 150; } return pwr30; } +size_t Realtime30PwrData::size() const { return 50; } +QwtData *Realtime30PwrData::copy() const { return new Realtime30PwrData(); } +void Realtime30PwrData::init() { pwrCur=0; for (int i=0; i<150; i++) pwrData[i]=0; } +void Realtime30PwrData::addData(double v) { pwrData[pwrCur++] = v; if (pwrCur==150) pwrCur=0; } + +// Cadence history +double RealtimeCadData::x(size_t i) const { return (double)50-i; } +double RealtimeCadData::y(size_t i) const { return cadData[(cadCur+i) < 50 ? (cadCur+i) : (cadCur+i-50)]; } +size_t RealtimeCadData::size() const { return 50; } +QwtData *RealtimeCadData::copy() const { return new RealtimeCadData(); } +void RealtimeCadData::init() { cadCur=0; for (int i=0; i<50; i++) cadData[i]=0; } +void RealtimeCadData::addData(double v) { cadData[cadCur++] = v; if (cadCur==50) cadCur=0; } + +// Speed history +double RealtimeSpdData::x(size_t i) const { return (double)50-i; } +double RealtimeSpdData::y(size_t i) const { return spdData[(spdCur+i) < 50 ? (spdCur+i) : (spdCur+i-50)]; } +size_t RealtimeSpdData::size() const { return 50; } +QwtData *RealtimeSpdData::copy() const { return new RealtimeSpdData(); } +void RealtimeSpdData::init() { spdCur=0; for (int i=0; i<50; i++) spdData[i]=0; } +void RealtimeSpdData::addData(double v) { spdData[spdCur++] = v; if (spdCur==50) spdCur=0; } + +// HR history +double RealtimeHrData::x(size_t i) const { return (double)50-i; } +double RealtimeHrData::y(size_t i) const { return hrData[(hrCur+i) < 50 ? (hrCur+i) : (hrCur+i-50)]; } +size_t RealtimeHrData::size() const { return 50; } +QwtData *RealtimeHrData::copy() const { return new RealtimeHrData(); } +void RealtimeHrData::init() { hrCur=0; for (int i=0; i<50; i++) hrData[i]=0; } +void RealtimeHrData::addData(double v) { hrData[hrCur++] = v; if (hrCur==50) hrCur=0; } + +// Load history +//double RealtimeLodData::x(size_t i) const { return (double)50-i; } +//double RealtimeLodData::y(size_t i) const { return lodData[(lodCur+i) < 50 ? (lodCur+i) : (lodCur+i-50)]; } +//size_t RealtimeLodData::size() const { return 50; } +//QwtData *RealtimeLodData::copy() const { return new RealtimeLodData(); } +//void RealtimeLodData::init() { lodCur=0; for (int i=0; i<50; i++) lodData[i]=0; } +//void RealtimeLodData::addData(double v) { lodData[lodCur++] = v; if (lodCur==50) lodCur=0; } + + +RealtimePlot::RealtimePlot() : pwrCurve(NULL) +{ + //insertLegend(new QwtLegend(), QwtPlot::BottomLegend); + setCanvasBackground(Qt::white); + pwrData.init(); + cadData.init(); + spdData.init(); + hrData.init(); + + // Setup the axis (of evil :-) + setAxisTitle(yLeft, "Watts"); + setAxisTitle(yRight, "Cadence / HR"); + setAxisTitle(yRight2, "Speed"); + setAxisTitle(xBottom, "Seconds Ago"); + + setAxisScale(yLeft, 0, 500); // watts + setAxisScale(yRight, 0, 230); // cadence / hr + setAxisScale(xBottom, 50, 0, 15); // time ago + setAxisScale(yRight2, 0, 60); // speed km/h - 60kmh on a turbo is good going! + + setAxisLabelRotation(yRight2,90); + setAxisLabelAlignment(yRight2,Qt::AlignVCenter); + enableAxis(xBottom, false); // very little value and some cpu overhead + enableAxis(yLeft, true); + enableAxis(yRight, true); + enableAxis(yRight2, true); + + // 30s Power curve + pwr30Curve = new QwtPlotCurve("30s Power"); + pwr30Curve->setRenderHint(QwtPlotItem::RenderAntialiased); // too cpu intensive + QPen pwr30pen = QPen(Qt::red, 2.0, Qt::DashLine); + pwr30Curve->setPen(pwr30pen); + pwr30Curve->setData(pwr30Data); + pwr30Curve->attach(this); + pwr30Curve->setYAxis(QwtPlot::yLeft); + + // Power curve + pwrCurve = new QwtPlotCurve("Power"); + //pwrCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + QPen pwrpen = QPen(Qt::red); + pwrpen.setWidth(2.0); + pwrCurve->setPen(pwrpen); + pwrCurve->setData(pwrData); + pwrCurve->attach(this); + pwr30Curve->setYAxis(QwtPlot::yLeft); + + // HR + hrCurve = new QwtPlotCurve("HeartRate"); + //hrCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + QPen hrpen = QPen(Qt::blue); + hrpen.setWidth(2.0); + hrCurve->setPen(hrpen); + hrCurve->setData(hrData); + hrCurve->attach(this); + hrCurve->setYAxis(QwtPlot::yRight); + + // Cadence + cadCurve = new QwtPlotCurve("Cadence"); + //cadCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + QPen cadpen = QPen(QColor(0,204,204)); + cadpen.setWidth(2.0); + cadCurve->setPen(cadpen); + cadCurve->setData(cadData); + cadCurve->attach(this); + cadCurve->setYAxis(QwtPlot::yRight); + + // Speed + spdCurve = new QwtPlotCurve("Speed"); + //spdCurve->setRenderHint(QwtPlotItem::RenderAntialiased); + QPen spdpen = QPen(QColor(0,204,0)); + spdpen.setWidth(2.0); + spdCurve->setPen(spdpen); + spdCurve->setData(spdData); + spdCurve->attach(this); + spdCurve->setYAxis(QwtPlot::yRight2); + + // Load +// lodCurve = new QwtPlotCurve("Load"); +// //lodCurve->setRenderHint(QwtPlotItem::RenderAntialiased); +// QPen lodpen = QPen(QColor(128,128,128)); +// lodpen.setWidth(2.0); +// lodCurve->setPen(lodpen); +// QColor brush_color = QColor(124, 91, 31); +// brush_color.setAlpha(64); +// lodCurve->setBrush(brush_color); // fill below the line +// lodCurve->setData(lodData); +// lodCurve->attach(this); +// lodCurve->setYAxis(QwtPlot::yLeft); +} diff --git a/src/RealtimePlot.h b/src/RealtimePlot.h new file mode 100644 index 000000000..cf168fd27 --- /dev/null +++ b/src/RealtimePlot.h @@ -0,0 +1,129 @@ +/* + * 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 + */ + +#ifndef _GC_RealtimePlot_h +#define _GC_RealtimePlot_h 1 + +#include +#include +#include +#include +#include +#include +#include +#include "Settings.h" + + +class Realtime30PwrData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + void addData(double v) ; +}; + +class RealtimePwrData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + void addData(double v) ; +}; + +class RealtimeLodData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + void addData(double v) ; +}; + +// tedious virtual data interface for QWT +class RealtimeCadData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + void addData(double v) ; +}; +// tedious virtual data interface for QWT +class RealtimeSpdData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + void addData(double v) ; +}; +// tedious virtual data interface for QWT +class RealtimeHrData : public QwtData +{ + public: + double x(size_t i) const ; + double y(size_t i) const ; + size_t size() const ; + virtual QwtData *copy() const ; + void init() ; + void addData(double v) ; +}; + +class RealtimePlot : public QwtPlot +{ + Q_OBJECT + + private: + + QwtPlotGrid *grid; + QwtPlotCurve *pwrCurve; + QwtPlotCurve *pwr30Curve; + QwtPlotCurve *cadCurve; + QwtPlotCurve *spdCurve; + QwtPlotCurve *hrCurve; + //QwtPlotCurve *lodCurve; + + + public: + Realtime30PwrData pwr30Data; + RealtimePwrData pwrData; + RealtimeSpdData spdData; + RealtimeHrData hrData; + RealtimeCadData cadData; + //RealtimeLodData lodData; + + RealtimePlot(); + +}; + + + +#endif // _GC_RealtimePlot_h + diff --git a/src/RealtimeWindow.cpp b/src/RealtimeWindow.cpp new file mode 100644 index 000000000..a94b1ad6d --- /dev/null +++ b/src/RealtimeWindow.cpp @@ -0,0 +1,863 @@ +/* + * 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 "MainWindow.h" +#include "RealtimeWindow.h" +#include "RealtimeData.h" +#include "RealtimePlot.h" +#include "math.h" // for round() + +// Three current realtime device types supported are: +#include "ComputrainerController.h" +#include "ANTplusController.h" +#include "SimpleNetworkController.h" +#include "ErgFile.h" + +void +RealtimeWindow::configUpdate() +{ + + // config has been updated! so re-read + // wipe out all the current entries + deviceSelector->clear(); + streamSelector->clear(); + + // get configured devices + DeviceConfigurations all; + + Devices.clear(); + Devices = all.getList(); + + streamSelector->addItem("No streaming", -1); + for (int i=0; iaddItem(Devices.at(i).name, i); + + // add data sources + deviceSelector->addItem(Devices.at(i).name, i); + } + + deviceSelector->setCurrentIndex(0); + streamSelector->setCurrentIndex(0); + + // reconnect - otherwise after no config they still get an error + if (Devices.count() > 0) { + disconnect(startButton, SIGNAL(clicked()), this, SLOT(warnnoConfig())); + disconnect(pauseButton, SIGNAL(clicked()), this, SLOT(warnnoConfig())); + disconnect(stopButton, SIGNAL(clicked()), this, SLOT(warnnoConfig())); + connect(startButton, SIGNAL(clicked()), this, SLOT(Start())); + connect(pauseButton, SIGNAL(clicked()), this, SLOT(Pause())); + connect(stopButton, SIGNAL(clicked()), this, SLOT(Stop())); + } +} + +RealtimeWindow::RealtimeWindow(MainWindow *parent, const QDir &home) : QWidget(parent) +{ + + // set home + this->home = home; + deviceController = NULL; + streamController = NULL; + ergFile = NULL; + + // main layout for the window + main_layout = new QVBoxLayout(this); + timer_layout = new QGridLayout(); + + // BUTTONS AND LCDS + button_layout = new QHBoxLayout(); + option_layout = new QHBoxLayout(); + controls_layout = new QVBoxLayout(); + + deviceSelector = new QComboBox(this); + streamSelector = new QComboBox(this); + workoutSelector = new QComboBox(this); + + workoutSelector->addItem("Manual Mode", 0); + workoutSelector->addItem("Workout Mode", 1); + workoutSelector->addItem("Slope Mode", 2); + + // get configured devices + DeviceConfigurations all; + Devices = all.getList(); + + streamSelector->addItem("No streaming", -1); + for (int i=0; iaddItem(Devices.at(i).name, i); + + // add data sources + deviceSelector->addItem(Devices.at(i).name, i); + } + + deviceSelector->setCurrentIndex(0); + streamSelector->setCurrentIndex(0); + + recordSelector = new QCheckBox(this); + recordSelector->setText("Save"); + recordSelector->setChecked(Qt::Checked); + + startButton = new QPushButton(tr("Start"), this); + startButton->setMaximumHeight(100); + pauseButton = new QPushButton(tr("Pause"), this); + pauseButton->setMaximumHeight(100); + stopButton = new QPushButton(tr("Stop"), this); + stopButton->setMaximumHeight(100); + + button_layout->addWidget(startButton); + button_layout->addWidget(pauseButton); + button_layout->addWidget(stopButton); + option_layout->addWidget(deviceSelector); + option_layout->addSpacing(10); + option_layout->addWidget(recordSelector); + option_layout->addSpacing(10); + option_layout->addWidget(streamSelector); + + // XXX NETWORK STREAMING DISABLED IN THIS RELEASE SO HIDE THE COMBO + streamSelector->hide(); + + option_layout->addSpacing(10); + option_layout->addWidget(workoutSelector); + + controls_layout->addItem(option_layout); + controls_layout->addItem(button_layout); + + connect(deviceSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(SelectDevice(int))); + connect(recordSelector, SIGNAL(clicked()), this, SLOT(SelectRecord())); + connect(streamSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(SelectStream(int))); + connect(workoutSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(SelectWorkout(int))); + + if (Devices.count() > 0) { + connect(startButton, SIGNAL(clicked()), this, SLOT(Start())); + connect(pauseButton, SIGNAL(clicked()), this, SLOT(Pause())); + connect(stopButton, SIGNAL(clicked()), this, SLOT(Stop())); + } else { + connect(startButton, SIGNAL(clicked()), this, SLOT(warnnoConfig())); + connect(pauseButton, SIGNAL(clicked()), this, SLOT(warnnoConfig())); + connect(stopButton, SIGNAL(clicked()), this, SLOT(warnnoConfig())); + } + powerLabel = new QLabel(tr("WATTS"), this); + powerLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + heartrateLabel = new QLabel(tr("BPM"), this); + heartrateLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + speedLabel = new QLabel(tr("KPH"), this); + speedLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + cadenceLabel = new QLabel(tr("RPM"), this); + cadenceLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + lapLabel = new QLabel(tr("Lap/Interval"), this); + lapLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + loadLabel = new QLabel(tr("Load WATTS"), this); + loadLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + distanceLabel = new QLabel(tr("Distance (KM)"), this); + distanceLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + + avgpowerLabel = new QLabel(tr("Avg WATTS"), this); + avgpowerLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + avgheartrateLabel = new QLabel(tr("Avg BPM"), this); + avgheartrateLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + avgspeedLabel = new QLabel(tr("Avg KPH"), this); + avgspeedLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + avgcadenceLabel = new QLabel(tr("Avg RPM"), this); + avgcadenceLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + avgloadLabel = new QLabel(tr("Avg Load WATTS"), this); + avgloadLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + + laptimeLabel = new QLabel(tr("LAP TIME"), this); + laptimeLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + timeLabel = new QLabel(tr("TIME"), this); + timeLabel->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + + powerLCD = new QLCDNumber(this); powerLCD->setSegmentStyle(QLCDNumber::Filled); + heartrateLCD = new QLCDNumber(this); heartrateLCD->setSegmentStyle(QLCDNumber::Filled); + speedLCD = new QLCDNumber(this); speedLCD->setSegmentStyle(QLCDNumber::Filled); + cadenceLCD = new QLCDNumber(this); cadenceLCD->setSegmentStyle(QLCDNumber::Filled); + lapLCD = new QLCDNumber(this); lapLCD->setSegmentStyle(QLCDNumber::Filled); + loadLCD = new QLCDNumber(this); loadLCD->setSegmentStyle(QLCDNumber::Filled); + distanceLCD = new QLCDNumber(this); distanceLCD->setSegmentStyle(QLCDNumber::Filled); + + avgpowerLCD = new QLCDNumber(this); avgpowerLCD->setSegmentStyle(QLCDNumber::Filled); + avgheartrateLCD = new QLCDNumber(this); avgheartrateLCD->setSegmentStyle(QLCDNumber::Filled); + avgspeedLCD = new QLCDNumber(this); avgspeedLCD->setSegmentStyle(QLCDNumber::Filled); + avgcadenceLCD = new QLCDNumber(this); avgcadenceLCD->setSegmentStyle(QLCDNumber::Filled); + avgloadLCD = new QLCDNumber(this); avgloadLCD->setSegmentStyle(QLCDNumber::Filled); + + laptimeLCD = new QLCDNumber(this); laptimeLCD->setSegmentStyle(QLCDNumber::Filled); + laptimeLCD->setNumDigits(9); + timeLCD = new QLCDNumber(this); timeLCD->setSegmentStyle(QLCDNumber::Filled); + timeLCD->setNumDigits(9); + + gridLayout = new QGridLayout(); + gridLayout->addWidget(powerLabel, 1, 0); + gridLayout->addWidget(cadenceLabel, 1, 1); + gridLayout->addWidget(heartrateLabel, 1, 2); + gridLayout->addWidget(speedLabel, 1, 3); + gridLayout->addWidget(distanceLabel, 1, 4); + gridLayout->addWidget(lapLabel, 1, 5); + gridLayout->addWidget(powerLCD, 2, 0); + gridLayout->addWidget(cadenceLCD, 2, 1); + gridLayout->addWidget(heartrateLCD, 2, 2); + gridLayout->addWidget(speedLCD, 2, 3); + gridLayout->addWidget(distanceLCD, 2, 4); + gridLayout->addWidget(lapLCD, 2, 5); + gridLayout->addWidget(avgpowerLabel, 3, 0); + gridLayout->addWidget(avgcadenceLabel, 3, 1); + gridLayout->addWidget(avgheartrateLabel, 3, 2); + gridLayout->addWidget(avgspeedLabel, 3, 3); + gridLayout->addWidget(avgloadLabel, 3, 4); + gridLayout->addWidget(loadLabel, 3, 5); + gridLayout->addWidget(loadLCD, 4, 5); + gridLayout->addWidget(avgpowerLCD, 4, 0); + gridLayout->addWidget(avgcadenceLCD, 4, 1); + gridLayout->addWidget(avgheartrateLCD, 4, 2); + gridLayout->addWidget(avgspeedLCD, 4, 3); + gridLayout->addWidget(avgloadLCD, 4, 4); + gridLayout->setRowStretch(2, 4); + gridLayout->setRowStretch(4, 3); + + // timers etc + timer_layout->addWidget(timeLabel, 0, 0); + timer_layout->addWidget(laptimeLabel, 0, 3); + timer_layout->addWidget(timeLCD, 1, 0); + timer_layout->addItem(controls_layout, 1,1); + timer_layout->addWidget(laptimeLCD, 1, 3); + timer_layout->setRowStretch(0, 1); + timer_layout->setRowStretch(1, 4); + + // REALTIME PLOT + rtPlot = new RealtimePlot(); + + // COURSE PLOT + ergPlot = new ErgFilePlot(0); + ergPlot->setVisible(false); + + // LAYOUT + + main_layout->addWidget(ergPlot); + main_layout->addItem(timer_layout); + main_layout->addItem(gridLayout); + main_layout->addWidget(rtPlot); + + displaymode=2; + main_layout->setStretch(0,1); + main_layout->setStretch(1,1); + main_layout->setStretch(2,2); + main_layout->setStretch(3, displaymode); + + + // now the GUI is setup lets sort our control variables + gui_timer = new QTimer(this); + disk_timer = new QTimer(this); + stream_timer = new QTimer(this); + load_timer = new QTimer(this); + + recordFile = NULL; + status = 0; + status |= RT_RECORDING; // recording is on by default! - add others here + status |= RT_MODE_ERGO; // ergo mode by default + displayWorkoutLap = displayLap = 0; + pwrcount = 0; + cadcount = 0; + hrcount = 0; + spdcount = 0; + lodcount = 0; + load_msecs = total_msecs = lap_msecs = 0; + displayWorkoutDistance = displayDistance = displayPower = displayHeartRate = + displaySpeed = displayCadence = displayGradient = displayLoad = 0; + avgPower= avgHeartRate= avgSpeed= avgCadence= avgLoad= 0; + main = parent; + + connect(gui_timer, SIGNAL(timeout()), this, SLOT(guiUpdate())); + connect(disk_timer, SIGNAL(timeout()), this, SLOT(diskUpdate())); + connect(stream_timer, SIGNAL(timeout()), this, SLOT(streamUpdate())); + connect(load_timer, SIGNAL(timeout()), this, SLOT(loadUpdate())); + + // setup the controller based upon currently selected device + setDeviceController(); + rtPlot->replot(); +} + +void RealtimeWindow::setDeviceController() +{ + int deviceno = this->deviceSelector->currentIndex(); + + // zap the current one + if (deviceController != NULL) { + delete deviceController; + deviceController = NULL; + } + + if (Devices.count() > 0) { + DeviceConfiguration temp = Devices.at(deviceno); + if (Devices.at(deviceno).type == DEV_ANTPLUS) { + deviceController = new ANTplusController(this, &temp); + } else if (Devices.at(deviceno).type == DEV_CT) { + deviceController = new ComputrainerController(this, &temp); + } else if (Devices.at(deviceno).type == DEV_GSERVER) { + deviceController = new SimpleNetworkController(this, &temp); + } + } +} + +void RealtimeWindow::setStreamController() +{ + int deviceno = streamSelector->itemData(streamSelector->currentIndex()).toInt(); + + // zap the current one + if (streamController != NULL) { + delete streamController; + streamController = NULL; + } + + if (Devices.count() > 0) { + DeviceConfiguration temp = Devices.at(deviceno); + if (Devices.at(deviceno).type == DEV_ANTPLUS) { + streamController = new ANTplusController(this, &temp); + } else if (Devices.at(deviceno).type == DEV_CT) { + streamController = new ComputrainerController(this, &temp); + } else if (Devices.at(deviceno).type == DEV_GSERVER) { + streamController = new SimpleNetworkController(this, &temp); + } + } +} + +void RealtimeWindow::SelectDevice(int index) +{ + if (index != -1) + setDeviceController(); +} + +void RealtimeWindow::Start() // when start button is pressed +{ + if (status&RT_RUNNING) { + newLap(); + } else { + status |=RT_RUNNING; + deviceController->start(); // start device + startButton->setText(tr("Lap/Interval")); + recordSelector->setEnabled(false); + streamSelector->setEnabled(false); + deviceSelector->setEnabled(false); + workoutSelector->setEnabled(false); + + if (status & RT_WORKOUT) { + load_timer->start(LOADRATE); // start recording + } + if (status & RT_RECORDING) { + + // setup file + QDate date = QDate().currentDate(); + QTime time = QTime().currentTime(); + QDateTime now(date, time); + QChar zero = QLatin1Char ( '0' ); + QString filename = QString ( "%1_%2_%3_%4_%5_%6.csv" ) + .arg ( now.date().year(), 4, 10, zero ) + .arg ( now.date().month(), 2, 10, zero ) + .arg ( now.date().day(), 2, 10, zero ) + .arg ( now.time().hour(), 2, 10, zero ) + .arg ( now.time().minute(), 2, 10, zero ) + .arg ( now.time().second(), 2, 10, zero ); + + QString fulltarget = home.absolutePath() + "/" + filename; + if (recordFile) delete recordFile; + recordFile = new QFile(fulltarget); + if (!recordFile->open(QFile::WriteOnly | QFile::Truncate)) { + status &= ~RT_RECORDING; + } else { + + // CSV File header + + QTextStream recordFileStream(recordFile); + recordFileStream << "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)\n"; + disk_timer->start(SAMPLERATE); // start screen + } + } + + // stream + if (status & RT_STREAMING) { + streamController->start(); + stream_timer->start(STREAMRATE); + } + + gui_timer->start(REFRESHRATE); // start recording + + } +} + +void RealtimeWindow::Pause() // pause capture to recalibrate +{ + // we're not running fool! + if ((status&RT_RUNNING) == 0) return; + + if (status&RT_PAUSED) { + status &=~RT_PAUSED; + deviceController->restart(); + pauseButton->setText(tr("Pause")); + gui_timer->start(REFRESHRATE); + if (status & RT_STREAMING) stream_timer->start(STREAMRATE); + if (status & RT_RECORDING) disk_timer->start(SAMPLERATE); + if (status & RT_WORKOUT) load_timer->start(LOADRATE); + } else { + deviceController->pause(); + pauseButton->setText(tr("Un-Pause")); + status |=RT_PAUSED; + gui_timer->stop(); + if (status & RT_STREAMING) stream_timer->stop(); + if (status & RT_RECORDING) disk_timer->stop(); + if (status & RT_WORKOUT) load_timer->stop(); + } +} + +void RealtimeWindow::Stop() // when stop button is pressed +{ + if ((status&RT_RUNNING) == 0) return; + + status &= ~RT_RUNNING; + startButton->setText(tr("Start")); + deviceController->stop(); + gui_timer->stop(); + + if (status & RT_RECORDING) { + disk_timer->stop(); + + // close and reset File + recordFile->close(); + + // add to the view - using basename ONLY + QString name; + name = recordFile->fileName(); + main->addRide(QFileInfo(name).fileName(), true); + } + + if (status & RT_STREAMING) { + stream_timer->stop(); + streamController->stop(); + } + + if (status & RT_WORKOUT) { + load_timer->stop(); + load_msecs = 0; + ergPlot->setNow(load_msecs); + ergPlot->replot(); + } + + // Re-enable gui elements + recordSelector->setEnabled(true); + streamSelector->setEnabled(true); + deviceSelector->setEnabled(true); + workoutSelector->setEnabled(true); + + // reset counters etc + pwrcount = 0; + cadcount = 0; + hrcount = 0; + spdcount = 0; + lodcount = 0; + displayWorkoutLap = displayLap =0; + lap_msecs = 0; + total_msecs = 0; + avgPower= avgHeartRate= avgSpeed= avgCadence= avgLoad= 0; + displayWorkoutDistance = displayDistance = 0; + guiUpdate(); + + return; +} + + +// Called by push devices (e.g. ANT+) +void RealtimeWindow::updateData(RealtimeData &rtData) +{ + displayPower = rtData.getWatts(); + displayCadence = rtData.getRPM(); + displayHeartRate = rtData.getHr(); + displaySpeed = rtData.getSpeed(); + displayLoad = rtData.getLoad(); + // Gradient not supported + return; +} + +//---------------------------------------------------------------------- +// SCREEN UPDATE FUNCTIONS +//---------------------------------------------------------------------- + +void RealtimeWindow::guiUpdate() // refreshes the telemetry +{ + RealtimeData rtData; + + // get latest telemetry from device (if it is a pull device e.g. Computrainer // + if (status&RT_RUNNING && deviceController->doesPull() == true) { + deviceController->getRealtimeData(rtData); + displayPower = rtData.getWatts(); + displayCadence = rtData.getRPM(); + displayHeartRate = rtData.getHr(); + displaySpeed = rtData.getSpeed(); + displayLoad = rtData.getLoad(); + } + + // Distance assumes current speed for the last second. from km/h to km/sec + displayDistance += displaySpeed / (5 * 3600); // XXX assumes 200ms refreshrate + displayWorkoutDistance += displaySpeed / (5 * 3600); // XXX assumes 200ms refreshrate + + // update those LCDs! + timeLCD->display(QString("%1:%2:%3.%4").arg(total_msecs/3600000) + .arg((total_msecs%3600000)/60000,2,10,QLatin1Char('0')) + .arg((total_msecs%60000)/1000,2,10,QLatin1Char('0')) + .arg((total_msecs%1000)/100)); + + laptimeLCD->display(QString("%1:%2:%3.%4").arg(lap_msecs/3600000,2) + .arg((lap_msecs%3600000)/60000,2,10,QLatin1Char('0')) + .arg((lap_msecs%60000)/1000,2,10,QLatin1Char('0')) + .arg((lap_msecs%1000)/100)); + + // Cadence, HR and Power needs to be rounded to 0 decimal places + powerLCD->display(round(displayPower)); + speedLCD->display(round(displaySpeed*10.00)/10.00); + cadenceLCD->display(round(displayCadence)); + heartrateLCD->display(round(displayHeartRate)); + lapLCD->display(displayWorkoutLap+displayLap); + + // load or gradient depending on mode we are running + if (status&RT_MODE_ERGO) loadLCD->display(displayLoad); + else loadLCD->display(round(displayGradient*10)/10.00); + + // distance + distanceLCD->display(round(displayDistance*10.00)/10.00); + + // NZ Averages..... + if (displayPower) { //NZAP is bogus - make it configurable!!! + pwrcount++; if (pwrcount ==1) avgPower = displayPower; + avgPower = ((avgPower * (double)pwrcount) + displayPower) /(double) (pwrcount+1); + } + if (displayCadence) { + cadcount++; if (cadcount ==1) avgCadence = displayCadence; + avgCadence = ((avgCadence * (double)cadcount) + displayCadence) /(double) (cadcount+1); + } + if (displayHeartRate) { + hrcount++; if (hrcount ==1) avgHeartRate = displayHeartRate; + avgHeartRate = ((avgHeartRate * (double)hrcount) + displayHeartRate) /(double) (hrcount+1); + } + if (displaySpeed) { + spdcount++; if (spdcount ==1) avgSpeed = displaySpeed; + avgSpeed = ((avgSpeed * (double)spdcount) + displaySpeed) /(double) (spdcount+1); + } + if (displayLoad && status&RT_MODE_ERGO) { + lodcount++; if (lodcount ==1) avgLoad = displayLoad; + avgLoad = ((avgLoad * (double)lodcount) + displayLoad) /(double) (lodcount+1); + avgloadLCD->display((int)avgLoad); + } + if (status&RT_MODE_SPIN) { + grdcount++; if (grdcount ==1) avgGradient = displayGradient; + avgGradient = ((avgGradient * (double)grdcount) + displayGradient) /(double) (grdcount+1); + avgloadLCD->display((int)avgGradient); + } + + avgpowerLCD->display((int)avgPower); + avgspeedLCD->display(round(avgSpeed*10.00)/10.00); + avgcadenceLCD->display((int)avgCadence); + avgheartrateLCD->display((int)avgHeartRate); + + + // now that plot.... + rtPlot->pwrData.addData(displayPower); // add new data point + rtPlot->cadData.addData(displayCadence); // add new data point + rtPlot->spdData.addData(displaySpeed); // add new data point + rtPlot->hrData.addData(displayHeartRate); // add new data point + //rtPlot->lodData.addData(displayLoad); // add new Load point + rtPlot->replot(); // redraw + + this->update(); + + // add time + total_msecs += REFRESHRATE; + lap_msecs += REFRESHRATE; + +} + +// can be called from the controller - when user presses "Lap" button +void RealtimeWindow::newLap() +{ + displayLap++; + + lap_msecs = 0; + pwrcount = 0; + cadcount = 0; + hrcount = 0; + spdcount = 0; + + // set avg to current values to ensure averages represent from now onwards + // and not from beginning of workout + avgPower = displayPower; + avgCadence = displayCadence; + avgHeartRate = displayHeartRate; + avgSpeed = displaySpeed; + avgLoad = displayLoad; +} + +// can be called from the controller +void RealtimeWindow::nextDisplayMode() +{ + if (displaymode == 8) displaymode=2; + else displaymode += 1; + main_layout->setStretch(3,displaymode); +} + +void RealtimeWindow::warnnoConfig() +{ + QMessageBox::warning(this, tr("No Devices Configured"), "Please configure a device in Preferences and then restart Golden Cheetah for the changes to take affect."); +} + +//---------------------------------------------------------------------- +// STREAMING FUNCTION +//---------------------------------------------------------------------- +void +RealtimeWindow::SelectStream(int index) +{ + + if (index > 0) { + status |= RT_STREAMING; + setStreamController(); + } else { + status &= ~RT_STREAMING; + } +} + +void +RealtimeWindow::streamUpdate() +{ + RealtimeData rtData; + + // get current telemetry... + rtData.setWatts(displayPower); + rtData.setRPM(displayCadence); + rtData.setHr(displayHeartRate); + rtData.setSpeed(displaySpeed); + rtData.setLoad(displayLoad); + rtData.setTime(0); + + // send over the wire... + streamController->pushRealtimeData(rtData); +} + +//---------------------------------------------------------------------- +// DISK UPDATE FUNCTIONS +//---------------------------------------------------------------------- +void +RealtimeWindow::SelectRecord() +{ + if (recordSelector->isChecked()) { + status |= RT_RECORDING; + } else { + status &= ~RT_RECORDING; + } +} + +void RealtimeWindow::diskUpdate() +{ + double Minutes; + + long Torq = 0, Altitude = 0; + QTextStream recordFileStream(recordFile); + + // convert from milliseconds to minutes + Minutes = total_msecs; + Minutes /= 1000.00; + Minutes *= (1.0/60); + + // PowerAgent Format "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)" + recordFileStream << Minutes + << "," << Torq + << "," << displaySpeed + << "," << displayPower + << "," << displayDistance + << "," << displayCadence + << "," << displayHeartRate + << "," << (displayLap + displayWorkoutLap) + << "," << Altitude + << "," << "\n"; +} + +//---------------------------------------------------------------------- +// WORKOUT MODE +//---------------------------------------------------------------------- + +void +RealtimeWindow::SelectWorkout(int index) +{ + int mode; + + if (ergFile) { + delete ergFile; + ergFile = NULL; + } + + if (index == 1) { + status |= RT_WORKOUT; + + // choose a file and then parse it! + QString filename = QFileDialog::getOpenFileName(this, + tr("Open Workout File"), home.dirName(), tr("Workout Files (*.erg *.mrc *.crs)")); + + if (!filename.isEmpty()) { + ergFile = new ErgFile(filename, mode); + if (ergFile->isValid()) { + + // success! we have a load file + // setup the course profile in the + // display! + ergPlot->setData(ergFile); + ergPlot->setVisible(true); + ergPlot->replot(); + + // set the device to the right mode + if (mode == ERG) { + status |= RT_MODE_ERGO; + status &= ~RT_MODE_SPIN; + deviceController->setMode(RT_MODE_ERGO); + // set the labels on the gui + loadLabel->setText("Load WATTS"); + avgloadLabel->setText("Avg Load WATTS"); + } else { // SLOPE MODE + status |= RT_MODE_SPIN; + status &= ~RT_MODE_ERGO; + deviceController->setMode(RT_MODE_SPIN); + // set the labels on the gui + loadLabel->setText("Gradient PERCENT"); + avgloadLabel->setText("Avg Gradient PERCENT"); + } + return; + } + } + + // oops didn't parse or no file selected + workoutSelector->setCurrentIndex(0); // will drop back here and delet/unset! + + } else if (index == 2) { + + // spinscan mode + ergPlot->setVisible(false); + status |= RT_MODE_SPIN; + status &= ~RT_MODE_ERGO; + status &= ~RT_WORKOUT; // temp + + deviceController->setMode(RT_MODE_SPIN); + loadLabel->setText("Gradient PERCENT"); + avgloadLabel->setText("Avg Gradient PERCENT"); + + } else { + + status |= RT_MODE_ERGO; + status &= ~RT_MODE_SPIN; + loadLabel->setText("Load WATTS"); + avgloadLabel->setText("Avg Load WATTS"); + deviceController->setMode(RT_MODE_ERGO); + ergPlot->setVisible(false); + status &= ~RT_WORKOUT; + } +} + +void RealtimeWindow::loadUpdate() +{ + long load; + double gradient; + load_msecs += LOADRATE; + + if (status&RT_MODE_ERGO) { + load = ergFile->wattsAt(load_msecs, displayWorkoutLap); + + // we got to the end! + if (load == -100) { + Stop(); + } else { + displayLoad = load; + deviceController->setLoad(displayLoad); + ergPlot->setNow(load_msecs); + } + } else { + gradient = ergFile->gradientAt(displayWorkoutDistance*1000, displayWorkoutLap); + + // we got to the end! + if (gradient == -100) { + Stop(); + } else { + displayGradient = gradient; + deviceController->setGradient(displayGradient); + ergPlot->setNow(displayWorkoutDistance * 1000); // now is in meters we keep it in kilometers + } + } +} + +void RealtimeWindow::FFwd() +{ + if (status&RT_MODE_ERGO) load_msecs += 10000; // jump forward 10 seconds + else displayWorkoutDistance += 1; // jump forward a kilometer in the workout +} + +void RealtimeWindow::Rewind() +{ + if (status&RT_MODE_ERGO) { + load_msecs -=10000; // jump back 10 seconds + if (load_msecs < 0) load_msecs = 0; + } else { + displayWorkoutDistance -=1; // jump back a kilometer + if (displayWorkoutDistance < 0) displayWorkoutDistance = 0; + } +} + + +// jump to next Lap marker (if there is one?) +void RealtimeWindow::FFwdLap() +{ + double lapmarker; + + if (status&RT_MODE_ERGO) { + lapmarker = ergFile->nextLap(load_msecs); + if (lapmarker != -1) load_msecs = lapmarker; // jump forward to lapmarker + } else { + lapmarker = ergFile->nextLap(displayWorkoutDistance*1000); + if (lapmarker != -1) displayWorkoutDistance = lapmarker/1000; // jump forward to lapmarker + } +} + +// higher load/gradient +void RealtimeWindow::Higher() +{ + if (status&RT_MODE_ERGO) displayLoad += 5; + else displayGradient += 0.1; + + if (displayLoad >1500) displayLoad = 1500; + if (displayGradient >15) displayGradient = 15; + + if (status&RT_MODE_ERGO) deviceController->setLoad(displayLoad); + else deviceController->setGradient(displayGradient); +} + +// higher load/gradient +void RealtimeWindow::Lower() +{ + if (status&RT_MODE_ERGO) displayLoad -= 5; + else displayGradient -= 0.1; + + if (displayLoad <0) displayLoad = 0; + if (displayGradient <-10) displayGradient = -10; + + if (status&RT_MODE_ERGO) deviceController->setLoad(displayLoad); + else deviceController->setGradient(displayGradient); +} diff --git a/src/RealtimeWindow.h b/src/RealtimeWindow.h new file mode 100644 index 000000000..b3706c547 --- /dev/null +++ b/src/RealtimeWindow.h @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net) + * + * 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 + */ + +#ifndef _GC_RealtimeWindow_h +#define _GC_RealtimeWindow_h 1 + +#include +#include +#include "MainWindow.h" +#include "DeviceConfiguration.h" +#include "DeviceTypes.h" +#include "ErgFile.h" +#include "ErgFilePlot.h" + +// Status settings +#define RT_MODE_ERGO 0x0001 // load generation modes +#define RT_MODE_SPIN 0x0002 // spinscan like modes +#define RT_RUNNING 0x0100 // is running now +#define RT_PAUSED 0x0200 // is paused +#define RT_RECORDING 0x0400 // is recording to disk +#define RT_WORKOUT 0x0800 // is running a workout +#define RT_STREAMING 0x1000 // is streaming to a remote peer + +#define REFRESHRATE 200 // screen refresh in milliseconds +#define STREAMRATE 200 // rate at which we stream updates to remote peer +#define SAMPLERATE 1000 // disk update in milliseconds +#define LOADRATE 1000 // rate at which load is adjusted + + +class RealtimeController; +class RealtimePlot; +class RealtimeData; + +class RealtimeWindow : public QWidget +{ + Q_OBJECT + + public: + + RealtimeController *deviceController; // read from + RealtimeController *streamController; // send out to + + RealtimeWindow(MainWindow *, const QDir &); + + void updateData(RealtimeData &); // to update telemetry by push devices + void newLap(); // start new Lap! + void nextDisplayMode(); // show next display mode + void setDeviceController(); // based upon selected device + void setStreamController(); // based upon selected device + void configUpdate(); // called when config changes + + public slots: + + void Start(); // when start button is pressed + void Pause(); // when Paude is pressed + void Stop(); // when stop button is pressed + + void FFwd(); // jump forward when in a workout + void Rewind(); // jump backwards when in a workout + void FFwdLap(); // jump forward to next Lap marker + void Higher(); // set load/gradient higher + void Lower(); // set load/gradient higher + + void SelectDevice(int); // when combobox chooses device + void SelectRecord(); // when checkbox chooses record mode + void SelectStream(int); // when remote server to stream to is selected + void SelectWorkout(int); // to select a Workout to use + + // Timed actions + void guiUpdate(); // refreshes the telemetry + void diskUpdate(); // writes to CSV file + void streamUpdate(); // writes to remote Peer + void loadUpdate(); // sets Load on CT like devices + + // When no config has been setup + void warnnoConfig(); + + protected: + + + // passed from MainWindow + QDir home; + MainWindow *main; + + QList Devices; + + // updated with a RealtimeData object either from + // update() - from a push device (quarqd ANT+) + // Device->getRealtimeData() - from a pull device (Computrainer) + double displayPower, displayHeartRate, displayCadence, + displayLoad, displayGradient, displaySpeed; + double displayDistance, displayWorkoutDistance; + int displayLap; // user increment for Lap + int displayWorkoutLap; // which Lap in the workout are we at? + + // for non-zero average calcs + int pwrcount, cadcount, hrcount, spdcount, lodcount, grdcount; // for NZ average calc + int status; + int displaymode; + + QFile *recordFile; // where we record! + ErgFile *ergFile; // workout file + + long total_msecs, + lap_msecs, + load_msecs; + + // GUI WIDGETS + // layout + RealtimePlot *rtPlot; + ErgFilePlot *ergPlot; + + QVBoxLayout *main_layout; + + // labels + QLabel *powerLabel, + *heartrateLabel, + *speedLabel, + *cadenceLabel, + *loadLabel, + *lapLabel, + *laptimeLabel, + *timeLabel, + *distanceLabel; + + double avgPower, avgHeartRate, avgSpeed, avgCadence, avgLoad, avgGradient; + QLabel *avgpowerLabel, + *avgheartrateLabel, + *avgspeedLabel, + *avgcadenceLabel, + *avgloadLabel; + + QHBoxLayout *button_layout, + *option_layout; + QGridLayout *timer_layout; + QVBoxLayout *controls_layout; + QCheckBox *recordSelector; + QComboBox *deviceSelector, + *streamSelector, + *workoutSelector; + QPushButton *startButton, + *pauseButton, + *stopButton; + + QGridLayout *gridLayout; + + // the LCDs + QLCDNumber *powerLCD, + *heartrateLCD, + *speedLCD, + *cadenceLCD, + *loadLCD, + *lapLCD, + *laptimeLCD, + *timeLCD, + *distanceLCD; + + QLCDNumber *avgpowerLCD, + *avgheartrateLCD, + *avgspeedLCD, + *avgcadenceLCD, + *avgloadLCD; + + QTimer *gui_timer, // refresh the gui + *stream_timer, // send telemetry to server + *load_timer, // change the load on the device + *disk_timer; // write to .CSV file + +}; + +#endif // _GC_RealtimeWindow_h + diff --git a/src/Settings.h b/src/Settings.h index 60327a1be..80b7fe6ca 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -47,6 +47,17 @@ #define GC_SB_NAME "SBname" #define GC_SB_ACRONYM "SB" +// device Configurations NAME/SPEC/TYPE/DEFI/DEFR all get a number appended +// to them to specify which configured device i.e. devices1 ... devicesn where +// n is defined in GC_DEV_COUNT +#define GC_DEV_COUNT "devices" +#define GC_DEV_NAME "devicename" +#define GC_DEV_SPEC "devicespec" +#define GC_DEV_PROF "deviceprof" +#define GC_DEV_TYPE "devicetype" +#define GC_DEV_DEFI "devicedefi" +#define GC_DEV_DEFR "devicedefr" + #include #include diff --git a/src/SimpleNetworkClient.cpp b/src/SimpleNetworkClient.cpp new file mode 100644 index 000000000..468a4e0d3 --- /dev/null +++ b/src/SimpleNetworkClient.cpp @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2009 Steve Gribble (gribble [at] cs.washington.edu) and + * 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 "SimpleNetworkClient.h" +#include "DeviceTypes.h" +#include "DeviceConfiguration.h" + +#include +#include + +#include + +SimpleNetworkClient::SimpleNetworkClient(QObject *parent, + DeviceConfiguration *dc) + : parent (parent), running(false), connected(false), kill_signal(false) +{ + server_hostname = dc->portSpec.section(':',0,0).toAscii(); // after the colon + server_port = dc->portSpec.section(':',1,1).toInt(); // after the colon +qDebug()<<"client constructed for" << server_hostname << server_port; +} + +SimpleNetworkClient::~SimpleNetworkClient() { + closeAndExit(); +} + +bool SimpleNetworkClient::start() { + QMutexLocker locker(&client_lock); + + if (running) { + // already running; fail. + return false; + } + + // Fire up socket connector and reader thread + QThread::start(); + + // Wait for child to indicate that it connected to the server + client_cond.wait(&client_lock); + if (!connected) { + QThread::wait(); // second of two ways a thread can exit + return false; + } + + printf("Connected to network...\n"); + return true; +} + +void SimpleNetworkClient::closeAndExit() { + QMutexLocker locker(&client_lock); + + if (!running) + return; + + kill_signal = true; + client_cond.wait(&client_lock); + kill_signal = false; + QThread::wait(); // second of two ways the thread can exit +} + +bool SimpleNetworkClient::getRealtimeData(RealtimeData &rtData) { + QMutexLocker locker(&client_lock); + + if (!connected) { + return false; + } + rtData = read_data_cache; + return true; +} + +bool SimpleNetworkClient::pushRealtimeData(RealtimeData &rtData) { + QMutexLocker locker(&client_lock); + + if (!connected) { +qDebug() << "SimpleNetworkClient Not connected!"; + return false; + } + +qDebug() << "SimpleNetworkClient is about to push..."; + + // need second pair of eyes here: can somebody confirm this pushes a + // copy of rtData into the queue, not the reference? (I think + // it pushes a copy because the type of write_queue is + // queue, not queue. ) + + // push the realtime data into a queue to be written by the child + write_queue.enqueue(rtData); + + return true; +} + + +#define MAX_BYTES_PER_LINE 128 + +void SimpleNetworkClient::run() { + QTcpSocket server; + QMutexLocker locker(&client_lock); + + // signal that I'm running + running = true; + + /////////////// try to connect to the remote host + server.connectToHost(server_hostname, server_port); + // wait up to 5 seconds to connect + if (server.waitForConnected(5000)) { + connected = true; + } + // let start() invoker know the outcome of connection attempt + client_cond.wakeOne(); + if (connected == false) { + server.close(); + running = connected = false; + while (!write_queue.empty()) write_queue.dequeue(); + return; + } + + /////////// Loop, reading lines in from the network and writing from the queue. + while(1) { + char network_data[MAX_BYTES_PER_LINE]; + + // Try up to 1 second to read next line from network. + if (!read_next_line(locker, server, read_data_cache, network_data)) { + // read failed, close up shop. + server.close(); + running = connected = false; + while (!write_queue.empty()) write_queue.dequeue(); + if (kill_signal) + client_cond.wakeOne(); + return; + } + + // If anything in our write queue, push it into the network. + while(!write_queue.empty()) { + if (!write_next_line(locker, + server, + write_queue.dequeue(), + network_data)) { + // write failed, close up shop. + server.close(); + running = connected = false; + while (!write_queue.empty()) write_queue.dequeue(); + if (kill_signal) + client_cond.wakeOne(); + return; + } + } + } +} + +bool SimpleNetworkClient::read_next_line(QMutexLocker &locker, + QTcpSocket &server, + RealtimeData &read_into_me, + char *next_line) { + qint64 read_result; + qint64 num_read_so_far; + bool done; + + next_line[0] = '\0'; + next_line[MAX_BYTES_PER_LINE-1] = '\0'; // to be safe + num_read_so_far = 0; + done = false; + + while(!done) { + // wait up to a second for socket to be ready to read + locker.unlock(); + server.waitForReadyRead(100); + locker.relock(); + + // re-entered lock; make sure I'm not told to kill myself. + if (kill_signal) { + printf("Got kill signal\n"); + return false; + } + + read_result = + server.readLine(next_line + num_read_so_far, + MAX_BYTES_PER_LINE - num_read_so_far - 1); + if (read_result == -1) { + return false; + } + + if ((read_result == 0) && (num_read_so_far == 0)) { + // didn't get anything yet, but need to give writing a + // chance. so-- return to main loop. + return true; + } + + num_read_so_far += read_result; + if ((num_read_so_far == MAX_BYTES_PER_LINE - 1) || + ((num_read_so_far > 0) && + next_line[strlen(next_line)-1] == '\n')) + done = true; + } + + // Make sure there is a trailing newline. + if (next_line[strlen(next_line) - 1] != '\n') { + // nope; quit out. + return false; + } + + // Yup; try to parse it. + { + float watts, hr, speed, rpm, load; + long time; + + if (sscanf(next_line, "%f %f %ld %f %f %f\n", + &watts, &hr, &time, &speed, &rpm, &load) != 6) { + // couldn't parse, so quit. + return false; + } + read_into_me.setWatts(watts); + read_into_me.setHr(hr); + read_into_me.setTime(time); + read_into_me.setSpeed(speed); + read_into_me.setRPM(rpm); + read_into_me.setLoad(load); + + printf("Read from network: %f %f %ld %f %f %f\n", + read_into_me.getWatts(), read_into_me.getHr(), + read_into_me.getTime(), read_into_me.getSpeed(), + read_into_me.getRPM(), read_into_me.getLoad()); + } + return true; +} + +bool SimpleNetworkClient::write_next_line(QMutexLocker &locker, + QTcpSocket &server, + RealtimeData record, + char *buffer) { + int num_written; + snprintf(buffer, MAX_BYTES_PER_LINE-1, + "%.2f %.2f %ld %.2f %.2f %.3f\n", + record.getWatts(), record.getHr(), record.getTime(), + record.getSpeed(), record.getRPM(), record.getLoad()); + locker.unlock(); + num_written = server.write(buffer, strlen(buffer)); + locker.relock(); + if (num_written != (int) strlen(buffer)) { + return false; + } + return true; +} diff --git a/src/SimpleNetworkClient.h b/src/SimpleNetworkClient.h new file mode 100644 index 000000000..7d1328eb7 --- /dev/null +++ b/src/SimpleNetworkClient.h @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2009 Steve Gribble (gribble [at] cs.washington.edu) and + * 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 + */ + + +#ifndef _GC_SimpleNetworkClient_h +#define _GC_SimpleNetworkClient_h 1 + +#include +#include +#include +#include +#include +#include +#include + +#include "DeviceTypes.h" +#include "DeviceConfiguration.h" +#include "RealtimeController.h" +#include "RealtimeData.h" + +// This class is used by SimpleNetworkController to actually +// connect to the remote server, and do the reads/writes and +// marshalling/unmarshalling of data over the network. +// The class spawns a thread to do reads, and steals the +// caller's thread to do writes. + +class SimpleNetworkClient : public QThread +{ + public: + + QObject *parent; + + // hostname and port are the hostname/port of the server to which + // this SimpleNetworkClient should connect. + SimpleNetworkClient(QObject *parent, DeviceConfiguration *dc); + ~SimpleNetworkClient(); + + // Forge the connection to the remote host, and start the + // thread running. Returns false on failure (in which case + // the thread will not be running). + bool start(); + + // If the TCP connection is open, closes it. If the thread + // is running, terminates it and waits for it to exit. + void closeAndExit(); + + // When called, this method will fill in rtData with the latest + // realtime data from the remote peer. + // + // Returns true on success, false if the remote peer has been + // disconnected. + bool getRealtimeData(RealtimeData &rtData); + + // When called, this method will push the realtime data in + // rtData to the server. + // + // Returns true on success, false if the remote peer has been + // disconnected. + bool pushRealtimeData(RealtimeData &rtData); + + private: + // When SimpleNetworkClient.start() is called, the new thread + // will begin executing here. + void run(); + + // The thread uses this to pull from the network. + bool read_next_line(QMutexLocker &locker, + QTcpSocket &server, + RealtimeData &read_into_me, + char *next_line); + + // The thread uses this to push into the network. + bool write_next_line(QMutexLocker &locker, + QTcpSocket &server, + RealtimeData record, + char *buffer); + + // Hostname and port number we connect to. + QString server_hostname; + quint16 server_port; + + // For coordinating between GUI and the thread. + QMutex client_lock; + QWaitCondition client_cond; + bool running; // controlled by child thread + bool connected; // controlled by child thread + bool kill_signal; // controlled by caller; child thread will signal + + // The latest data read from the network. + RealtimeData read_data_cache; + + // A Queue of data to write to the network. + QQueue write_queue; +}; + +#endif // _GC_SimpleNetworkClient_h diff --git a/src/SimpleNetworkController.cpp b/src/SimpleNetworkController.cpp new file mode 100644 index 000000000..ac3577f95 --- /dev/null +++ b/src/SimpleNetworkController.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2009 Steve Gribble (gribble [at] cs.washington.edu) and + * 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 + +#include "SimpleNetworkController.h" +#include "RealtimeData.h" + +SimpleNetworkController::SimpleNetworkController(RealtimeWindow *parent, + DeviceConfiguration *dc) + : RealtimeController(parent), parent(parent), client(parent, dc), state(DISCONNECTED) +{ +} + +int SimpleNetworkController::start() { + if (state != DISCONNECTED) { + // can't connect if I'm already connected, return failure + return -1; + } + + if (client.start()) { + state = RUNNING; + return 0; + } else { +qDebug()<<"Client didn't start!"; + } + return -1; +} + +int SimpleNetworkController::stop() { + if (state == DISCONNECTED) { + // can't disconnected if I'm disconnected, return failure + return -1; + } + client.closeAndExit(); + state = DISCONNECTED; + return 0; +} + +int SimpleNetworkController::pause() { + if (state != RUNNING) { + // can't pause if I'm not running + return -1; + } + state = PAUSED; + return 0; +} + +int SimpleNetworkController::restart() { + if (state != PAUSED) { + // can't resume if I'm not paused + return -1; + } + state = RUNNING; + return 0; +} + +void SimpleNetworkController::getRealtimeData(RealtimeData &rtData) { + if (state == RUNNING) { + + // did the thread die? + if(!client.isRunning()) + { + QMessageBox msgBox; + msgBox.setText("Cannot Connect to peer"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.exec(); + parent->Stop(); + } + + if (!client.getRealtimeData(data_cache)) { + // client has disconnected since the last poll + printf("getRealtimeData invoked, and client failed\n"); + this->stop(); + } + } + rtData = data_cache; +} + +void SimpleNetworkController::pushRealtimeData(RealtimeData &rtData) { + if (state == RUNNING) { + if (!client.pushRealtimeData(rtData)) { + // client has disconnected since the last push + printf("pushRealtimeData invoked, and client failed\n"); + this->stop(); + return; + } + } else { + qDebug()<<"Pushed but not running!"; + } +} diff --git a/src/SimpleNetworkController.h b/src/SimpleNetworkController.h new file mode 100644 index 000000000..1b8981974 --- /dev/null +++ b/src/SimpleNetworkController.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2009 Steve Gribble (gribble [at] cs.washington.edu) and + * 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 + */ + + +#ifndef _GC_SimpleNetworkController_h +#define _GC_SimpleNetworkController_h 1 + +#include +#include + +#include "RealtimeController.h" +#include "RealtimeData.h" +#include "SimpleNetworkClient.h" +#include "DeviceTypes.h" +#include "DeviceConfiguration.h" + +// This class serves as our first simple cut as a realtime +// network device controller. This class opens up a TCP connection +// to a server, and then starts pulling telemetry updates from a single +// peer via the server. + +// This class currently uses a pull model. As well, GC should push +// realtime data drawn from a local bike into this class, causing that +// data to be relayed to the server (and from there to the peer). + +class SimpleNetworkController : public RealtimeController +{ + public: + + RealtimeWindow *parent; + + // hostname and port are the hostname/port of the server to which + // this SimpleNetworkControlller should connect. + SimpleNetworkController(RealtimeWindow *parent, + DeviceConfiguration *dc); + ~SimpleNetworkController() { } + + // Connect to the server; blocks until connection finishes or fails. + // + // Returns 0 on success, non-zero on failure. + int start(); + + // Disconnect from the server; blocks until disconnect finishes or + // fails. + // + // Returns 0 on successful disconnection, non-zero if the controller + // wasn't disconnected to begin with. + int stop(); + + // If the controller is connected to the server and running, this + // method causes the controller to "pause", i.e., to ignore updates + // flowing from the server locally. + // + // Returns 0 if the pause took effect, non-zero if the pause isn't + // meaningful (i.e., the controller isn't connected, or it's already + // paused). + int pause(); + + // If the controller is connected and paused, this method causes the + // controller to unpause and resume processing updates from the server. + // + // Returns 0 if the restart succeeds, non-zero otherwise. + int restart(); + + // XXX -- NOT SURE WHAT THIS METHOD IS FOR. I'VE CURRENTLY STUBBED + // IT OUT TO RETURN TRUE. + bool discover(char *) { return true; } + + // The SimpleNetworkController is currently a pull mode controller. + bool doesPush() { return false; } + bool doesPull() { return true; } + bool doesLoad() { return false; } + void setLoad(double) { return; } + + // When called, this method will fill in rtData with the latest + // realtime data from the remote peer. + // + // XXX -- should probably have a return value so that the caller + // can learn when the data source has unexpectedly disconnected. + void getRealtimeData(RealtimeData &rtData); + + // When called, this method will push the realtime data in + // rtData to the server. + void pushRealtimeData(RealtimeData &rtData); + + private: + SimpleNetworkClient client; + enum {DISCONNECTED, RUNNING, PAUSED} state; + RealtimeData data_cache; +}; + + +#endif // _GC_SimpleNetworkController_h diff --git a/src/application.qrc b/src/application.qrc index 9faed51f6..668bfb43d 100644 --- a/src/application.qrc +++ b/src/application.qrc @@ -2,6 +2,7 @@ images/cyclist.png images/power.png + images/arduino.png images/query.png images/update.png images/gc.png diff --git a/src/images/arduino.png b/src/images/arduino.png new file mode 100644 index 0000000000000000000000000000000000000000..39d68b76d718b0d95c49f9dd9080396afcdebe2b GIT binary patch literal 16355 zcmV<9KODe`P) zjm7A~^h?Lj4|yX?_g3pG_g?6aUl<*nUpTv0D8F%U@t|IH9qUWaKQ|r<(WJYU-+gU$ z!B{|2|msalQOO45?BlmAF8$qA<(WAvumRQ+1lNtHd$(i~`*PVmy&GlQO7v^rP z-eLkQHSG8O?9T14eeG*=Q$w2EXp~AX|NIpt6d-6uZppD&ywmAW(eSYoV^3Uo%p(O{ zK^&VOWh1Ho@HgMPQ7oiRKDM}eFQ&MvsrpUxPPLq`H-&0la4136%f&Joi(Gy9Kx3G_ z?fuH~%2XmQS6V}<{=K!GKY8QE!XsxnZmUm>zWbwBH-G%|PyNpCxg_s!p7$45e)z_F zY%ttSW!kjBQq0ZehtkvpOS)IDFE$94b10W4YLyaU+7V7@7fNT&oO$h?x9R=({LG{g zh^9D6Bi)ADw&hkfUsRUdNG!&X^}^1^{ms?I$gh1tkUWCO8Rcr&C7o>c`Om)K6}+bF zymRXo!;)iT{SzYtx!ujdXn%r~6oQC&J%rjS$}%@Fpt@v3mtU<_zxNk^by#Z9wmZxU zjM91WGtYN}!JjPNaQla5W@ZlZg)l=GTUK8tzI5a2!B2k~^(2SM5Wg#r@M&#$f$Orx zw(*M(?;W2S7A8hF$|Z>rGV}9dm)Lr+6x`fZ>a|vfWy1+p3f{hPya^3ev%l;9(e3RnDC~?GTP6i1q!0 z`!}!0gFe>Kj6z}S{yma#5BK-aoID*0_`;Gv5hUAX6_Wkg^~IJasK>%CK?F2C9SDvL z5Al+xf9}Z9W2eH5w~)<`O^w~%*{V{^z|@S|*1|L|Z||CglHatC4vif@^~ludSUQ&6 zxwkwWOPxM_Mo4G8(P&{e*J{XhQ*Dt{>eTtYmUfUW*K75F#2YlZ+R)QyE?nE$UwOFk zsq>Fy{C-z9NP^nfJ0RTdshNePJ9i9;4*5JG!#R@fyYt%Xv|(n8hmO;wFP=X!)Ze$W zzIo~KCu8w64sa!Rh^%bM)w!ur9Fdbp=QlRi1%@AprAH>_D2gRmU#np=KJn*^hx?l2 zcmqbUtX;YJ;>65vUbtvhO6^*)&nqRm_UZAFOOKot1?iou*CW9&m2c!ex--agFI_y( zw3_LFmuKi!qcS-$GS-*MZautw^P{WTLr*j{*gJf z=WN}ipLp!lg|p`lcK6peHWp4C?Yg$rZhiFO`>DRDD3T&YR!c=y)f(06(V6*ZG~DHx z#ijeU%X%WA|L>J|+l&wy?2FUvKY!%W?)pQ$m|eW~UPo?J5B58|doMikxR<6_j`PN2 z@4xvrU9SD(7e1HasIwCzafx<2a?m5Xx-yYTJ$mwJUo0R5J%_rPJ=pvDlTSZ$>LlZu zeS?GH{?tGs-pv;V{hpCTcyMBLsg!$|%V~<$$mJe8ef-iyTJZ+&K6oJc0(h8fcW#K$ z@W{Y$CLC$Co0)KwuE|IY_$r!MAteMW51q}f*9_zQezEmcb^uAduk4;a7BeB|kuDrQ5 zb>ZCB*25PbJ>M4zmhWzliGiP-Ya!9&VW2f`y^M<5HkM1Y-6coZXcZS!Hu!*td^ zT&z{vmhn5k_j?9G;K)b=F~YJDz=QnZADlV%m#@Dwa`E}s>czi&?T7ByzE<8l@Wf*E zL_njdiKi}Zy!M7NH`g`wKs4CtQe}?ZV+fb_x{gT+ykwb9p^$s&i!YjvOj35MS#w>B z>=GwWoPOfTrw4{dWUbA37=ttjwPx52FW=qYSu-rzATUg@?>oHdz_VWjKZ+Yvrc-O4w_QI)f-BRDVbNklf zGAl}!Pg>c0AY10d^Utdk(PYRh>D+7AnYl^-iK87}P+f}AU1oDXSEy7~OB1>rK`=Ds zb`6W9xZTw~?XXp;)eMp?=`}0J6F%OfX^mpz)Z|n=-FMh7o2f7rmh#2zc)PZ_ywrbu zGT-RD_1^VhIJUF?kQaShJH@K5-6*WT`sU@SaQf8P=(XiV!T)KQB<^i*4|tPmO{Peu z(rmA-EapvrKihCKCQ+k`8$ciRgA@u`5}XYAO`;&S4cE z5_#QnY?7Tmax(wHyCz979*;>mgsY=v`n5ww z=&DY6cO~xi{r&aDO~u(P=8FcgGHCVG^ z+PXzuy?$@_xr>LJ+m0&xBwwwtANET;&vRa1#3m0lX|HiL>vtB4L&31EP;mG>Zjf1VNNX)~! z_U`6}({;IEz!3P`yIZ;-xCW=U?cq#6K`}(YQ{Bn-^HEQiNCbScAjpdD_`UBMwbWy0 zIEq%=rYMF6ySz`e)q3qUrD_Zh%R#9c2yv`%D9eLG18b}GGAEopb5Y-_veL_n#ozv<(5_XKhN39PPoD}$)1k>Di)}}XPdloh71U7D zM{3<{F5eYBEa6=F)?d#){amA1%ig?^xpa|7qP_nRHWRadnsK65fzU;;G;Mqd%26j+3OZ{;}Wx&2t(hwoSrv zD8}Jhn>nIC;_zap)+7j?0PrA~J{0hHjHaU&WvhhalI(EQ@ApRJcICvSr(XW)4-}H` z5NyqGv}VVy*QQU+e)_4Wy@W?=nQcwaA8cxtiaw;|4xf4a(R4bwxVTuUHSXVCG1AjR zqoZp}4+)1nI)B0?Ou{R5MULWFW?~FyQ#aJXlV?i%hx)-j!SR=#dJa9p(e&rO{6(_w zU;aB+*JBJz+m>mn)mrJq!cku$T4jkVw{L}|z-a&A_WA~+n-h_^#4z}EenFbg43=&8 zzudncJAF>!Jo1AjA|eKl&NXi>N+aXy$_AmxW9LtA{rDCCxl6KP0uMwYv0+IR z*WWCbvy_(`8=cVW#^%Zv12fEsYG0rIYDOsqFJe zTN|78jz#(VWQU<7erSHu;^~g5KYaiF_}r}R;YgMZdVQQjgcx!0_AMN@M?dxal~-P4 zS#IRye6d-jnXmjtXlO*Cc*`3Kj!m21=;BV!ip1K1pwKsv-7hpb>Exy73_f5ne%l)} z12KgY_bQDVMKhx#u0Key65lrv=u6X%!}}v5$qo*UM*B0ZM*h_E&!~ox$@C>NsgP-Z zeq!!dPbY&?`06{aoj)@BTVH-@WN>u(&YhIc_xr#7DEJGNtXEYr|L;8cg&T3llZbMolG zd+)s!9T{!OZ67B@RAVlBGN2Pdt2rKEX1)9vK~&_tWYs5v(IK=4r!y0hO0D*6Iw286 zwtwW%?yeO}%R4K_=Z>EL-%kO;Y?KO~q%SxWyK?JYr=gbD*MmIk@B&YZgRFNT5T1JB zQ}5iktF9uv_BGM1kn*#!7KG;GA+~W92~eb z8}j>xW~TEGmTHC~CXyXPa?+-p2*Hhcw4U1xsW~%k_iBre2o9jfmnieF6+G;06(IE+>Ln`7S39HenA3t&W*7dvf z&1ysnwbkXe(mB$a`%&U)V40Wh+Owa)s33s7p#nSiPfg9HY^W$x%wpG~0yh zGKE4RJ~%=U=ElxiaCGYF$DguU>b>QU7EYh4FR#>=m;cQdpC1n;8JYR<>u>L=swd!0 zCPRno%LBsE7eD)g>R2D%zS$_0resF&& z-d<~F50r9s_uR(W;F-=FHzUB(}f;=@VFcRCSy@$B)<>(@j@{==_-m6gjG ziHwu>=P#U_2!$$ZyVG-h@8=HpI;~2nxxct7L{r7Ry?P@{%MD}YVT|QJTD_;LhFWeI zn*LyWz1pZW+CX;=qpk1l?CO?fXlia_+sPf|I+gY1#m35VAJsjQ z2qsxJLV9gg|HbRCNR-p9*91*@;n54nh9i@H{`}O)2XFn-PrE^eEUvAd9Ul2#zWUPn z!PMhNCYh(Ee3goR?$qh=@$oLzr9|)5`|CgW<+aA)?&QSS1HnGr$@;?nfIs4ul02b^ zVymcEWV#*jNVeVdbQPncQZ)L4Ygp_?X4}|$N%T8`vb{zOd2>SmIf03PIfcE0YT`u-+lF?w@g9wpp}e8 zJjMJr+4i5E8M12G@BYnSi!`4}#F;NWdD2jgm@jfSeDARQ-OD!>xn2u~S4nk{IS-swD8UFvFjUpT~?getcPKPNJD#OLvGEK3rxkI|o(qEF7vEoBdH zRm$E_*rnZQD8MV7&ckhDds|M0ijL^zIF=0d4`xW->L|L8K|vyw(tfAi?h*vaF-=2F zCE|+KAT5S;y4iZs$MDr{n#^|A!=BKsYd6jvKf`iDsnVj8eSiGp*I!=VsK_!iG|-<( z*ABCEmv1N(>l$Qb>D*vcSy_)@x;Qeqxv`o|CsfOrJbFwD#IIhz+Y)?BU58B#YAR9O z&xw(+R>_7)Ro_?|RJ4h3#8eF1vVc=5o*@LXWpqfAM7i`aKDp6kc$7tVY_RVhxQef|V3;5E{KmX~;G0gB6T-= zxr1h>#k!VbS>e8Z-pftTj8`i)mK4`_^A9SuI?dnDmKSs7>$_PpmYzIuez(>n{Xt!~ zg)TWYJ(Wx)eLQU+>^1J)9m^z7&P-194^9jY4x!p+63I+_u)l9;XdshJNSrt{Fc=Pn zu*L#K85G^-q+Gp5+NM@2%WE4hmZNKvt~ow6fKIfxvhiSTU2WGq9Jl^p?cKM2iJgpu z!+X2Cs-k#&UNj*Tg@>yTZe06t^Wl2P>tBEPkol`OZkeVT3CI5H&5gLvyR?PL)ZaAbEmf~o9G=6ilJ(wCJLQZIC1^~U)X$5F1>Ji(7b;HnC*S6HQ zvA4LA85&Y;yHPLuk|_>UQK?F(D4#mTkeu5US*B&^oX7$T8n)r$(HI)w5y;gBo={Z5 zD-KHDc1L&WGAGb+hN>Uz5Uy#_WJ}i~p|DT#RAUXlKUgU@C4b~=U;U=>O%*l&-o1NQ zu3Tv;op3BObL2#|QI~k3m@jlK8$;lq{m1`@8J>T{u*^!mL3x~tW0tcenPLw14hc%6 zX(mB)1ER`)igR3g|nxWBl}4-Iz-%3OUwgu|FtETu_!xTr5cDzdIxM<-^Z zL4T!H+bR|KP>|AfqgrDf!ZLME;247t7#tA^kkB?Qo(Dc)tChMC;PUyhO?G(}^PVq% zn2ilg4h*NR-MV5n>jaK?NA1#>IK9Zkx>7o|u<)7BeqM_Dc|nLK`%p6jK3}#}8=jb9 zFSAbov?}ro!$aj)e!6@0okt#9I6FTVb6J7pRGM%Z(sr=VjODnBqOl~8a)WtX*L9j? zN)7GJn-BCZ`ITRNv0gm*{-6Gd5b*1k9SC^8^64jc9zNtbf2Gz6XVRP`s-2Dx#kt*a z9EW9DMb{*+NAyS@k4NBmilVxXYwHGbnVF1*ux7bx32$cxGY&~^6{@Z*2Zu-U%WD%y z=g&^2o|ybx(=>K(+=zg+ZPc|!bAEbSr)ZmXG~3_}U9MIO2m6nlee~+`eKMX-4Gt_# zjDhm4R4Q6Llq+QGxh!IGT^lLgbtuC&%`QQbOt<$S2-|kLHqpfiby>W+7}9Kv_`@S3 zR+kux$Jef32G656s)R4#6{QsQ|RWhW8*N5B8OS8v@R zB|%DrXwyL6NIoA;l307vG+f=}yk0aG04qF%+jX%CWS4XumlqhvasYr!rFwd5lxDat zVS$st0mg`lF-UYNOo+pS>2TVpw_6cgA4tZ%UN7QAYTy-LmSuTF;BowV7vF!f3RnVs z!@Tq3m(N}}FM7N#jx0rRQly&CW)Bb8p{dF9=eIZ4{`^1x86j|Dx^Lj<=}xVBB%K@= zsO=j!j}AZc!to>TG_#q6w=K*6(?9#>yKjHME46>~KmYf~e(S%>5pHIB!Jp3LwU%ON zC`EWC49S?TiQXCIK%_CQix`0Z9171yuo%X>R=r$fc!(A(II4|&c58ckVZ(K+%|@3bD#e1_s@ax-WFZKw8xjZ0OM(K#_jm_UfAS8#$6hl_4_7$w z!-AwcR+pwylQXEFQZ(GMtQMiTAR~@WQG~5%R#r2@VAr%pKARJHA`|s@_P5DmVZX40 zdAB|{QSKCrxvgD+()`@S*)v70n`>1F(@^S7wcA6Q{U%sMxB5fx_v=deGP{tRAvK;PEj86!3cmj>Bywj=UvX}`9U%_qP(dHpu$vl)`~cna;-(Ddv_JG+L+)^S>RFJ?#~ z6y{B{g*As%1<8V@fl|Qv`k9`Bf&FH%u`~`Z&J^X6Ouf;<-a4k^@%T!uW>r(t3*+x% zhbcm78vzm=9|MHfML;ZX>ITn?I1*0R;n2P~AO;jrpc;onGP%7Z#u3J9VvTUHadMdN zt?eTNuqviwhGHRpeRsbigA8bOtd41Meuj|SM5W$g4~Sj45s4fKdUS9kzq0XHKYIPG zfVWWE^9{xcj=7etnH{j5rL9uM6G;k!_u$G3;qeI*e#zr&31Szzq~!n`0B2#@HB;;M zp2Fb3i6a`QATDx}@Ib_)Ab2oxEvH68_{|TuH)|+YSQ`Q-z`+j_M3)i-tQKZOO*cEb zMS_2{Pz`%ojUP9`yJ{xg3l#sOdQ2#T!r#S0FzML^%qn5O&$10&*JzJ7=8wx3#ZYWS z5G_(8gF#o~6273>4RlSL2zs|Q+dnzCdFM`{CC6iN)8TjbidPTb`NHSEyjN&m-#VaA z%~fkH$?L;LS#5a7$)_OQIJ7%a~@T@*V+g=BXu&0v^Mil6OTU1*C46p4+` z5cpFttft1u`7GtSrQ!k5oTZs6e#g`lx_?)1wH~Y!;QM)W{D#~mlu}EQyq3k5+MTE* z0>Yd;^T_h`8-)kkC!T+Q|v zgC79~BqE{Hr%%51=H<5D;X!~Loy!-iD1NA9Bjd9ztG1>n-P5xa%W$Yia__imw#Bf7 zM41#+4vUt%z^|6a3wh9`QDzLCrg37}PFq3PA}69~DT<1s)k}U;M>j z=xosItru!;ICe#DzPq>#;DSRJkH*V;8-%VJJ}5Je=@Ld&CT!Kt#OP#lvr-3jL=+qA zJ9#eCr!s=hiUdbdG^%w?+1f4XmUsR%O>;yv+}z(v4)v!}Ly^S5#^&0&3nxxrI=_-D zEk9Vzq$8hw@$)}@{jF;+{~#U;|I^=lzNnKsmoFgh+V=)HWL%Ve z={}X_-hJ=G0PBmT2IP(!>K|ZZNkb?5`zI}vh()3pZwcFML){u3J_{}8$T2n$Y$10j z#>`d;MHey6i9E#%L^xskV>V64ct)!?u(IW5tqd+cAZSi$znMKmaSr?Z64|I%tF^X2 z6!rLgZQWYhK2Ug0@$inWolEq+`_oql5?FyiehRDz20xKZcPw4*$mvuvnMfjb#DkWL zP!SstRWEj&SOyv6B4F_N_$HsvQ{&Mv*=m^8iqWdyzj-6Sweh{$sCdO& zi;X)=(RgbA<|0IAN;gV(mvHRT$5wk4g=AWte6!N6w+J4SPRi-DQN6;M6w}JLg8UP$HijUFqVeMxue& ziP~D$qKLer4<#~S<UDqveWP+zuI`nzD z7EA4yYr$|_m|xi8xxK?y$56=hm`$;F4?9Y$$@Nd>_qTEY{JDI#)eeFNpe?7Z$d=6W z0ssZ#?d>pjx|iM9OdDP8lU+r?*i7s#B7|g%Pyx~L4iX!G^vTlnein0g7FVfH_W%GN ziAh93RI-l)L=|qC7SwLu9(b8mO4UdZ`%k z`n;MG&H@I-%E%BFZvCz1p;LPSpa51(Cd}v z-qs@|t3Axc7AF|6$!gB2x-7W4w zB6L$xk#aI2aUdWZ?(hEP-+lYT%hx7Hro6t`Bty%(Nw(T99*kr9I;t<0FjcK}`;L*T zOt6ffp`ZWsXTu3UoIZ7f`227GzQRfk(+!G(-NB?PqIqB}(@i~2vTCIvkiu^Mzzz8T zk|=KgRwWUm49~%u1fi&RWYLj&BVP{~cR-RthLl7!bDE^Qk_S1A@UWkF#rGgX+U@oy zp<nQ4;z#uA{-<&b$8|7ts9r$ef1}m zd}S<^Dwb;mTpO56>2S4D0$<4l!XkuwLR+gBycEm3WJ$5#Xf#LXj-0*pWT{*yR0Rk* zIX-dl?v=;RoPYAkr#@O<+^Fx4&y1E=w+O~!RQ=hLN8VYv&&8tSW0QM@EcnBr!9kDL z$CD(SD0q4#B+Jk}Xyuw10}u&d0lE~*Hg*+3A$#$PWX2~yNn?D2B=04{$0ZH5S&}3= zcJi@$v&ut)U|Ae=nxf(R;INpsrWFoPC6fWmT>9vShhcYCH#5l$8k}k z<8>X(|6)MQm9nT_JVzg$owJTwYu>!f~zE_-7Z+eEG=ayWjo$&!0Q; z?2%YV$1_S7SBk^IBo{br$5THV_AL=!H@mI?)9Z{LwybfZS=YFn-YFtei1 zL=m>^SUf7X>Hr^!6m%8eC#Uz5R}^z(IL<8=0;}-J(M8x;HLM83SRJQRW8iqSiOc1LgXlozYdGhNK3n5QX@ z!I;@;HwXF?z5r8h999)mQ|cTsjC#H>5LxQ1|@1M0aXUJ#&;@U4v zN8vPCX~}df1;B?L*RRpFlIwsipsL}M$wH`P%j*V-CWt* z=i;fdYBpP{pj%=B8kbO)Yo&HWBDio6_{)>8*I`QlD`gw@AN-5|@wE@%+0Sp0fCdkFI* zOCwGb2N92ped{$R8nf=lTmrDy$F&z*`bl{BhQwU`aWO-X_H3oE@Pc?r6QrJ$xJ3!EbZ%{i$)7Si8|$TZi;o4wWK2`AHeEItLbf=$!}!SP)0Zx-xfT!w zP8V8%i$T*tqJbe~Y|ONV&TxRq95{4|^YJ`W?Q}>VD++PbN|0OBCTDQ&A>qleOhK>hn~{TK6acWGk>=ACr^1b!GDqOB?fFUF%$kEvop zgZign7BC2_Ya{qTFj`Wy7k=X#4axk`d)IRfv)N_RfzWQgsz-#?sv4g;vAO=B@o+-` zXUKp+@f3UyEMGC-aWje3;_8N?wu6Idu-FG%J65&EOFkpt>Q!2_CU6@BCT9mg8c}_+ zdw3@7Avg?%#psVdBy_Z! z9!YAq+Kcy>)^>KG5CZ4)^237g)@rLw-PR$qn7UOiRC!9&H3J^Bj@m(PL9lTn;k8cd z^3L8bZY~vdmI)*~81BIg!I;YPrDm;dYf<=z=g0YUG&nY5`9hm}d5h%eNCJH^5DB_4 zYy*1%c&i%IRwe8t_%IkX;IahRSKBcP=r9q!iEbR2oRjzUdp4~_2?|{JH1-K^baI+ zrQE={_rjTDCm2Zx`Bx6}HT1omT_PM6yuQ-zHUSweoiXxZsH!0g^eH8M!M4DE}@+AwfZyq^;kn$t-*q&$`WDWcV2 zEY_`6`ZANynH0yU7RrQDiw_Tg400_s-k;3pV7(_=n%1ne!~Kbl2L`q#m@$Jx(ves` z3R&DsKhUn0Zs;&UBW7L{K*Hh__sSLa8Wa-PeiXcLL~1MTuYcp~M~@#hZS&~D{O;l2 z?W@<4lM^C1N=<`lPfDb~n>)NTd+Mwv`8pIcdHl358gFU(=*(PTaIi(uV<%44ZKrQ? zHd~kXa6#;jK&O9ZxJqG zS40Pfph3Enq3HD*5?5*@iM z_=0&*!qBpLPN$hrX3&D3tahf)oEyD#No_RR>l=a?%Fw}#oxY9%oQpDB|Hd$o=n^M~Cwt*ULM*~rWFj{|Ud zzqgqyjUSymfAX9vE0g`BkiVhGp#sBA4n%~&u${fuMRkEfArhij^?DXE!R>yGL9sJ4 z{b2ZFp>Ql3Nyeg<4yw#Ka%8?(t(sl3sc6l^Lj^n%{!Xxv6xCJ~pWly`mn#)wsd%GV z)0F0~z4YQ>EIJ%dcxk5HY_u?XT84st%`=c?NK{WzVnnH@Z2`{ue9?3!)ZYg^MGA%h zYyjF>FHiGmdx~AJPo+=3g*bMn_jW3WkAtyHV{u zLkILe^fT0EFnEDr004o5v6n*;6hQ#^yq=)n7YYR#!?e$yJv)D77LLYrG>Uh(HKJW*}7yJS2QHMb56bm!=3pRvE&zVY6#eCkupsFymo z@U8FvNbRa@JiN5BCiV{)N(UzgbZ4)|V{Gr~9S+^oOk6e+V?auG8mU-N5CNx*ARXZ= zt?6Jm+~2>sx?FF{-dOCgS{q8H4tBP&Uja{`U285C?(h;aT$MbcMca^HSc39-yaL0@ ztp-jZFH%@uILV+}^-xET4hD+>;L9-I_{aZbbZ}^DWT;iE!lUab>Ubuzw4d*4N~=`t zpP0?T_h^_@G(?b)p*dn8od|jx&2ryJ|H+AA$Wo4pbvU72MRWG_S{8GB4Bn=?krpLO5SXx>=F1zwSvFCUMi{EJHq@?9km*! zlyuZO-0s-Mnb{-5iDZ)GeO{^I2`Z-h^UIelLoenJy(E?KFr-gpqv4GI#O0+;*Au|- zrT_rI7l)WZ<>FZs9nK#Pq`YMY91NQ^I*8BZj0hgm`B@wHGJu){qLo@IMynCeZ%_yR&C z+8s4KKCZW$5M86ORJOK`G%yUSmjIGiA03sTYeoX15T#%$F!YH;6r0f6J;+rT+l~7C z{OoXlpWJNJ>Xq5)sil?Ww|?@IW9J?j_Z&@ge8%HBkefQB5X}71xLd6feM3YW)Ic{8 z@V9f>D0n)#Ju}eP7Yhe?x}g>u^~&;XBfq`TtX4rIhN2O0j~6aoghza9XV2_(Xvu4v zMvu%x5S)kddfiGj)IVBov``Lp2O1XKg`VDajTiOghJ5U#y<-6QhPKYQu%^JmgQf7euo`qPT4 z!h`bYlb4>}&Hdd!{P2h0`N`eb~h-~ zc>L^{Kp+6GNLw>Ur{@08fBVO;y?OZ`zWe>bk>S%5)3>&_eWvTDxooiv?v{{SX%8PH z-Hnf~UOIjRhT(_z@4$zD{KU~qpL!zGmxg@{m-zsR_x5(6*B|Ecuf6u#t8czFJvj?3 zRqnjRkuPfQ=1n>+v6&w-9bXFka1Je!;0M7-bQX*JtmvrH3q zi2wt7vexQxHKA|-O}uUEwkO-O-+A-x=bm`#*o8+f z|MIQHjfZgN%rDFZL*kwN%5Z|VTELE>jm0Hcc#bd3`=qaa=l}g{Ml)uH$4iyE+mMOX zHDPwvLv>4A8}PPFXToAv-QL(39Uc9ZuYPT4Z1myQgWZp=gH;(G7{nHSaODak^1)E# zv!D6gxzp#r`q3zQ4P-aAXdm-F`l6h+Nq+ zBaDCenGDgelNt*5X-h|jasv@zPovQZ5er3 z+}Qu|&were*i$*PTcf}wptPwh6qM}VXf)X87w>-b;l$MRzxrSQ?b_}EOraDar@=v_pbc(mB{QY4n;H?A08goAr-1BGd7sH35fIl&g(LE=8%(GB(jG4@%f)7s$CW;g^>{r4u~6Um z$nf;k2OqrG<%z^d=GKG7GtYnF-iJ5kd_hXaJ+59~y*G3I?7_pW{%GhAe($$`{<9x% z=F1ZNV*VhQqC_+{KRP)+JOWJ!&D1aO@MGP&e)C}az(a5WRsdCqCyRJ|OorWAzB`c4 zJTgD?_kZ*4$%%a`;IOg!JV2o zJoX1o_3mEj`gV~{jz1{KFah9hB7UD*Dw?HSm?fK=YoLLBK7TYC!%e|RFp`YN-5!pi z+GyiY@CgSxHh`z9!(OX2TdhK+G(0pkJvOzwd+=aw6&}C+7G`N9m`p0wD)b^)m8WN> zSYHQCz;ibnW`Jo!z~Sy+g2Z5Q-^Z$aF|vlA0RMICbOZb$Vo^Z*^;T zvshg(Huq(-MQ|+>@+%MKFXHog=`IZBKANZ=?!h?HpXnRO^xcXE2>AARvlsYvMV;-YC<^+rtw_X1g8Qw-<*IerB|ESz(QJSsmHl1D(qoA@oMS>D zaQ@sQM@B~O-M;gSU%dA2<#%Gq1TTtk5CYXQG&J6>Y4ijX#N5X0gQtS;)Gj9E2cn*fUd8Uat={%KiIGrDBnz*};K+BsT7M;1$y+ zP13b$_1@k457*Y9X5tYc{>VyaW^U%r^6KrCwNABxt9GoWDtlXmV@ymi`&%02SYU>+L&t?zB6K$K%Jbfol{ahTSC_nu&Q| zk(<+#@cyxv9)E0PWF!=gE-fwN+6@TQlgE$E&rHuO9GRUQUtPXWtFR~Nmca^T_;x5P zx{3aTE2oyO2q0b6j#;k}9hm?hK@(Hs69fJIFhL>uGpA2$iVhJn2&qNHoXq8T>K;x!<16fKsLf__iXcmA=*j~_dJ{hfDj zeRQKxDGMG?Dw6?+fr0>?fg)+SDc35sTeoiSZ0*80oJgj6A_>FdhGbFhz`CNeo1ipD z$A(|SozK-}-84SBbz6ejkK-cYAjeRkytX$VUisiM1Je{H7?KuTy9@l>vw`ruLsO0d z=D9+Ei-K?6b)XNiexKjx!@yOmHE?aJkjn$LEi5cty?Pbj;JW+qqsJgv0TK-i476nh zHeQsmljqNP{odW(-EnxN<4Lws1vf#|YT%I>hR1-;i9#eARyz2{7Tl+wcoJ6y@2xE7 ziv{p4=t;mvot6w<7ULx9Ik;BX>D_ZOwpf z9bQ+|9ef}b4!XEsHa66(78!!`GQJ3ET?k3*mFffunF-EeN#YtcTylee1m+$rg6qW& z!W4*UmgQkasMTt`C>fT$yMJ);;>CyS>z6OTf9~8ll4jn&aurt{@O)QqTx+%E%wQh^ zlUQD@R`M%rJqlHaQNYC$!+kn&>_{jSXf?ors`qZ+g*_9*a;7y~8Yd$2+leoK!P`~<>FFp47#jk$t*Z=g-za?v}F3ml9 z>I|-NEk9U#@%d+8{mGA>e&$J97&!u47q|o13N+LNwFA#7Skg|ZK*%5~+wczdMoBm> z0>NY&_xwX3Fh0C~z21O=t7Art$6`>lP8>fTjzm0OZ!i=D<2gP#0iIOV)v@V`j;4}M z&p5iXwT(GXYqg-y!-@qp`I{S1RKZ<05*WB*903&ungPuB@)c;16nZO;yQNpzHyZeiG?&ix?uz5p6gCJSwj?4jLW1(@B0$v{+D@W$$ ztF;*Kr0L7=SC?)ifQXa=Ff^}KCzj*t$uE4Xz!tK(mK-kO~a!YU4y$p5#)=j#` z22gKZngNH|yQ*f}BD5Wr!oPiCLt%jZ6b%O+(4IYz!cKZ%%%g~oq8=RXx0)^Z_R;9? z-o4Xo$#@4`WFG2nwTAnhckkUVmWtsR{*4WkC$25GZ+`S4{(1r=I~>edL5%{U?Cek`&6D(7Z@g}Fv~)a4T8`4FKX&Hy(~n+gmr4hl8w~DLL1;(a$E|># zhyi%w5pnN_g~z>TQe-)>iXUr@C;(W?{?QS*jC-~>s8BqMz^11s2Zu7ZZrmOp8=0A% zT3ozsnA(L4k7Tp?>o-2cvx1((!Oj==QPf!u)f58)Wg89;j=J4B+}&$8KtThlpvzE5 z`<9{<3x%cSWzaOJ#<;KpuJz{4TVSm~S$*g4zm3~B3&$6rMs^xaO3@zNy3G(ylJ^FC z91ant+4HkU3e{$1Yg^aNXP$a`G@9gX``I(+w0xcxhNptz5Sl224u2>DQ9T?8AV?5} zz0+%%Fvg(Rf#-z*2&oH5gvJ2#*W|=_CY=V{0Vl{Z%+&ZeF4G&D7K=qt+mJ5NFz&(h zL{euIFroc_Rxw^J??|#hh^$YC7d-v8KfJE5Z+ui4B z4$>8Lhjy)&+uqsRSYN%nnBUzdiRqJ^&u=!G1k8DGiq@+fd@WkrLhfk_p|)wTY^DJo z23d#}0)`bHV4Nw83Q*WtmWFRt!qqcQ2#14vdk4@^(T8EN!8>4v36jFSg;1g&S6Qo- zeX~=?dEi}&c0?1-1(0h@_g8=8%QLwBtD@s@;cyg70}mGZ3T!CVS_S!qe@?~JwRWq8 z!h%cKEtFXJE;)8}Wo_~19lPi2;7=Z(^Wxex0s7i^Z}p*OcKZfLny?afB>=@`MaGna+=4iSwJsM+sOm7RATOX@LJ#3R5KKCmOa_kxCA3w?CA}?V3Z}GN zE{kmt{eDclI9#c|6doQ1`dP)MkSu6#N<_Y*~%^FU&FCZ04`JS_oN~R$<^}MOz zdNDJgsl%d(E2tpc(34aR1SbN%{{tE(>v#1%n}YxV002ovPDHLkV1igffQ$eD literal 0 HcmV?d00001 diff --git a/src/main.cpp b/src/main.cpp index f155b3ab5..b12ae3248 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ // BLECK - homedir passing via global becuase ridefile is pure virtual and // cannot pass with current definition -- Sean can advise!! extern QString WKO_HOMEDIR; +MainWindow *mainwindow; int main(int argc, char *argv[]) @@ -102,8 +103,8 @@ main(int argc, char *argv[]) if (home.cd(cyclist)) { // used by WkoRideFileReader to store notes WKO_HOMEDIR = home.absolutePath(); - MainWindow *main = new MainWindow(home); - main->show(); + mainwindow = new MainWindow(home); + mainwindow->show(); home.cdUp(); anyOpened = true; } @@ -119,8 +120,8 @@ main(int argc, char *argv[]) assert(false); // used by WkoRideFileReader to store notes WKO_HOMEDIR = home.absolutePath(); - MainWindow *main = new MainWindow(home); - main->show(); + mainwindow = new MainWindow(home); + mainwindow->show(); } return app.exec(); } diff --git a/src/simpleserver.py b/src/simpleserver.py new file mode 100644 index 000000000..c6d0c1bb4 --- /dev/null +++ b/src/simpleserver.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python + +# simpleserver.py +# +# Acts as a simple server for coupling two golden cheetah peers together +# usage: ./simpleserver.py listen_port +# +# Copyright (c) 2009 Steve Gribble (gribble [at] cs.washington.edu) and +# 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 + +import socket +import threading +import SocketServer +import sys + +# this class keeps track of the state of a connected peer, including +# the socket used to talk to it, it's name, and a queue of records waiting +# to be written into it. +class PeerTracker(): + def __init__(self): + self.writequeue = [ ] # list of records to be sent TO peer + +# The main "threaded web server" class. Has a condition variable and +# a list of connected peers. +class CheetahServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + def init_cheetah(self): + self.cond = threading.Condition() # used to coordinate client lists + self.peers = [ ] # a list of connected peers + +# A new ThreadedCheetahHandler is instantiated for each incoming connection, +# and a new thread is dispatched to handle() to service that connection. +class ThreadedCheetahHandler(SocketServer.BaseRequestHandler): + # if something goes wrong with this connection, removepeer is called + # to clean up out of the server list. + def removepeer(self): + self.server.cond.acquire() + print "(" + self.me_peer.peername + ")", \ + "bogus or broken input, dropping.\n" + self.server.peers.remove(self.me_peer) + self.server.cond.release() + + # a line of data was received; this method is invoked to parse it + # and route it to the appropriate peer queue. if parsing fails, the + # peer is killed off. if parsing succeeds but only one peer is + # connected, the record is dropped but the peer stays up. + def processinput(self, data): + # make sure data has 6 fields. if not, drop peer. + components = data.split() + if (len(components) != 6): + self.removepeer() + return True + # try to parse the fields. if fail, drop peer. + try: + valdict = { } + valdict['watts'] = float(components[0]) + valdict['hr'] = float(components[1]) + valdict['time'] = int(components[2]) + valdict['speed'] = float(components[3]) + valdict['rpm'] = float(components[4]) + valdict['load'] = float(components[5]) + + # parse succeeded, so add to peer's input queue + print "(" + self.me_peer.peername + ")", \ + "well-formed record arrived..." + print " ... watts:%.2f hr:%.2f time:%ld speed:%.2f rpm:%.2f load:%.3f" % \ + (valdict['watts'], valdict['hr'], valdict['time'], \ + valdict['speed'], valdict['rpm'], valdict['load']) + self.server.cond.acquire() + if (len(self.server.peers) == 2): + if (self.server.peers[0] == self.me_peer): + otherpeer = self.server.peers[1] + else: + otherpeer = self.server.peers[0] + otherpeer.writequeue.append(valdict) + + # notify writing thread that there is work to do + self.server.cond.notify() + print " ...queued for", otherpeer.peername + "\n" + else: + print " ...but other peer not connected,", \ + "so dropped it.\n" + self.server.cond.release() + except ValueError: + # couldn't parse line, so drop connection + self.removepeer() + return True + + # finished with record, but not done with getting input + # records from peer, so return and tell caller we're not + # done yet + return False + + # this method gets invoked by the web server when a new peer + # connects to it. we check to see if we have room. if so, + # we add the peer, then start processing input. + def handle(self): + # New connection arrived. + print "(server) new incoming connection..." + self.server.cond.acquire() + # If the server is full, bonk out, else add. + if (len(self.server.peers) < 2): + # We have room. Create record for new peer. + self.me_peer = PeerTracker() + self.me_peer.socket = self.request + if (len(self.server.peers) == 0): + self.me_peer.peername = "Peer A" + elif (self.server.peers[0].peername == "Peer A"): + self.me_peer.peername = "Peer B" + else: + self.me_peer.peername = "Peer A" + self.server.peers.append(self.me_peer) + print " ...added peer #", str(len(self.server.peers)), \ + "named \"" + self.me_peer.peername + "\"\n" + done = False + else: + # Server is full, so bonk out. + print " ...but server already has 2 peers, so dropping.\n" + done = True + self.server.cond.release() + + # while this peer is connected, process requests + socketfile = self.request.makefile() + while not done: + try: + data = socketfile.readline() + done = self.processinput(data) + except socket.error, msg: + self.removepeer() + break; + +# this utility method is used by thread_write to actually write a +# record into a peer's socket. if the write fails, we ignore for now, +# and rely on the read on the socket to fail and clean up later. +def write_to_peer(item, peer): + s = "%.2f %.2f %d %.2f %.2f %.3f\n" % \ + (item['watts'], item['hr'], item['time'], \ + item['speed'], item['rpm'], item['load']) + try: + peer.socket.sendall(s) + except socket.error, msg: + pass + +# we spawn a thread to service writes -- when woken up by a signal on +# the server condition variable, the writer thread checks the peers' +# queues for work, and if it finds it, writes into the peer's socket. +# the writer thread finishes writing before it relinquishes the lock +# by waiting. this thread is a daemon, so we don't have to worry +# about making it exit on error. +def thread_write(server): + print "(writer) awake and waiting for business\n" + server.cond.acquire() + + # spin forever waiting for work + while True: + server.cond.wait() + # see if the first peer has some work + for peer in server.peers: + while len(peer.writequeue) > 0: + next = peer.writequeue.pop(0) + write_to_peer(next, peer) + server.cond.release() + +# main() invokes this to spawn the web server and the writer thread. +def run_server(port): + # initialize the web server + server = CheetahServer(("", port), ThreadedCheetahHandler) + server.allow_reuse_address = True + ip, port = server.server_address + print "(server) running in thread:", \ + threading.currentThread().getName() + "\n" + server.init_cheetah() + + # fire up the writer thread + writer_thread = threading.Thread(target=thread_write, args=(server,)) + writer_thread.daemon = True + writer_thread.start() + + # open for business and get incoming connections + server.serve_forever() + + +############################### +# everything below here is roughly equivalent to "main()" from C +############################### + +def usage(): + print "usage: ./simpleserver.py listen_port" + sys.exit(1) # failure + +def main(argv): + # validate arguments + if (len(argv) != 1): + usage() + try: + port = int(argv[0]) + except ValueError: + usage() + if ((port < 1) or (port > 65535)): + usage() + + # great! + run_server(port) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/src/src.pro b/src/src.pro index d967ae263..697d61300 100644 --- a/src/src.pro +++ b/src/src.pro @@ -7,7 +7,7 @@ TARGET = GoldenCheetah DEPENDPATH += . !isEmpty( BOOST_INCLUDE ) { INCLUDEPATH += $${BOOST_INCLUDE} } INCLUDEPATH += ../qwt/src -QT += xml sql +QT += xml sql network LIBS += ../qwt/lib/libqwt.a LIBS += -lm @@ -45,9 +45,11 @@ win32 { HEADERS += \ AllPlot.h \ AllPlotWindow.h \ + ANTplusController.h \ BestIntervalDialog.h \ ChooseCyclistDialog.h \ CommPort.h \ + Computrainer.h \ Computrainer3dpFile.h \ ConfigDialog.h \ CpintPlot.h \ @@ -57,7 +59,11 @@ HEADERS += \ DatePickerDialog.h \ DaysScaleDraw.h \ Device.h \ + DeviceTypes.h \ + DeviceConfiguration.h \ DownloadRideDialog.h \ + ErgFile.h \ + ErgFilePlot.h \ HistogramWindow.h \ LogTimeScaleDraw.h \ LogTimeScaleEngine.h \ @@ -74,9 +80,15 @@ HEADERS += \ PowerHist.h \ PowerTapDevice.h \ PowerTapUtil.h \ + QuarqdClient.h \ QuarqParser.h \ QuarqRideFile.h \ RawRideFile.h \ + RealtimeData.h \ + RealtimeWindow.h \ + RealtimeController.h \ + ComputrainerController.h \ + RealtimePlot.h \ RideCalendar.h \ RideFile.h \ RideImportWizard.h \ @@ -85,6 +97,8 @@ HEADERS += \ Season.h \ SeasonParser.h \ Settings.h \ + SimpleNetworkController.h \ + SimpleNetworkClient.h \ SplitRideDialog.h \ SrmRideFile.h \ StressCalculator.h \ @@ -102,11 +116,13 @@ SOURCES += \ AerobicDecoupling.cpp \ AllPlot.cpp \ AllPlotWindow.cpp \ + ANTplusController.cpp \ BasicRideMetrics.cpp \ BestIntervalDialog.cpp \ BikeScore.cpp \ ChooseCyclistDialog.cpp \ CommPort.cpp \ + Computrainer.cpp \ Computrainer3dpFile.cpp \ ConfigDialog.cpp \ CpintPlot.cpp \ @@ -115,7 +131,11 @@ SOURCES += \ DBAccess.cpp \ DatePickerDialog.cpp \ Device.cpp \ + DeviceTypes.cpp \ + DeviceConfiguration.cpp \ DownloadRideDialog.cpp \ + ErgFile.cpp \ + ErgFilePlot.cpp \ HistogramWindow.cpp \ LogTimeScaleDraw.cpp \ LogTimeScaleEngine.cpp \ @@ -132,9 +152,15 @@ SOURCES += \ PowerHist.cpp \ PowerTapDevice.cpp \ PowerTapUtil.cpp \ + QuarqdClient.cpp \ QuarqParser.cpp \ QuarqRideFile.cpp \ RawRideFile.cpp \ + RealtimeData.cpp \ + RealtimeController.cpp \ + ComputrainerController.cpp \ + RealtimeWindow.cpp \ + RealtimePlot.cpp \ RideCalendar.cpp \ RideFile.cpp \ RideImportWizard.cpp \ @@ -142,6 +168,8 @@ SOURCES += \ RideMetric.cpp \ Season.cpp \ SeasonParser.cpp \ + SimpleNetworkController.cpp \ + SimpleNetworkClient.cpp \ SplitRideDialog.cpp \ SrmRideFile.cpp \ StressCalculator.cpp \ diff --git a/src/test/workouts/Hausach48km.crs b/src/test/workouts/Hausach48km.crs new file mode 100644 index 000000000..3ddbb6131 --- /dev/null +++ b/src/test/workouts/Hausach48km.crs @@ -0,0 +1,355 @@ +[COURSE HEADER] +UNITS = METRIC +DESCRIPTION = Hausach 48KM +FILE NAME = Hausach_48Km.crs +;DISTANCE GRADE WIND +; COMMENTS +[END COURSE HEADER] + +[COURSE DATA] +0.02 0.00 0 +0.19 0.00 0 +0.22 -0.52 0 +0.22 0.00 0 +0.25 -0.46 0 +0.15 0.76 0 +0.15 0.00 0 +0.2 -0.86 0 +0.22 0.00 0 +0.19 0.60 0 +0.19 0.60 0 +0.19 0.60 0 +0.17 2.36 0 +0.16 0.72 0 +0.17 0.00 0 +0.16 2.15 0 +0.16 1.43 0 +0.16 2.15 0 +0.14 0.82 0 +0.16 0.72 0 +0.06 1.91 0 +0.11 2.08 0 +0.13 2.65 0 +0.12 2.39 0 +0.14 2.46 0 +0.15 0.00 0 +0.15 1.91 0 +0.15 0.76 0 +0.13 4.41 0 +0.12 1.91 0 +0.13 0.88 0 +0.13 3.97 0 +0.13 1.32 0 +0.14 1.64 0 +0.13 0.88 0 +0.13 2.65 0 +0.12 4.30 0 +0.1 3.44 0 +0.11 4.17 0 +0.12 0.00 0 +0.14 1.64 0 +0.11 1.04 0 +0.11 3.65 0 +0.11 2.61 0 +0.11 3.65 0 +0.1 1.72 0 +0.1 5.74 0 +0.1 2.29 0 +0.1 4.01 0 +0.09 5.74 0 +0.09 3.82 0 +0.11 3.65 0 +0.1 1.15 0 +0.09 5.74 0 +0.08 4.30 0 +0.08 2.87 0 +0.07 8.21 0 +0.08 5.74 0 +0.06 5.74 0 +0.06 5.74 0 +0.05 2.29 0 +0.07 4.92 0 +0.07 6.56 0 +0.07 3.28 0 +0.07 4.92 0 +0.07 8.21 0 +0.06 3.82 0 +0.06 5.74 0 +0.07 4.92 0 +0.06 9.59 0 +0.06 4.78 0 +0.06 6.70 0 +0.06 7.66 0 +0.05 8.05 0 +0.05 8.05 0 +0.05 9.21 0 +0.05 4.59 0 +0.05 6.89 0 +0.05 6.89 0 +0.04 7.18 0 +0.05 9.21 0 +0.04 8.63 0 +0.05 6.89 0 +0.03 7.66 0 +0.05 6.89 0 +0.05 8.05 0 +0.05 6.89 0 +0.04 11.54 0 +0.05 6.89 0 +0.04 5.74 0 +0.05 8.05 0 +0.04 10.08 0 +0.05 4.59 0 +0.04 7.18 0 +0.05 6.89 0 +0.06 5.74 0 +0.07 4.92 0 +0.08 2.87 0 +0.07 4.92 0 +0.08 2.87 0 +0.07 3.28 0 +0.08 3.58 0 +0.07 4.92 0 +0.07 4.10 0 +0.07 3.28 0 +0.07 4.92 0 +0.07 3.28 0 +0.07 1.64 0 +0.07 4.92 0 +0.1 2.29 0 +0.1 1.15 0 +0.1 -3.44 0 +0.01 -11.54 0 +0.01 0.00 0 +0.01 0.00 0 +0.04 8.63 0 +0.06 5.74 0 +0.05 6.89 0 +0.03 7.66 0 +0.03 3.82 0 +0.06 9.59 0 +0.05 6.89 0 +0.05 9.21 0 +0.05 4.59 0 +0.01 0.00 0 +0.05 9.21 0 +0.06 5.74 0 +0.05 6.89 0 +0.06 7.66 0 +0.06 5.74 0 +0.08 5.74 0 +0.09 3.18 0 +0.13 0.00 0 +0.07 6.56 0 +0.06 8.63 0 +0.06 5.74 0 +0.06 5.74 0 +0.06 5.74 0 +0.06 7.66 0 +0.07 3.28 0 +0.07 3.28 0 +0.05 0.00 0 +0.04 0.00 0 +LAP +0.01 0.00 0 +0.06 0.00 0 +0.14 -6.56 0 +0.15 -6.12 0 +0.2 -6.03 0 +0.22 -1.82 0 +0.21 -6.29 0 +0.19 -6.65 0 +0.18 -7.98 0 +0.18 -7.34 0 +0.27 -3.18 0 +0.29 -1.38 0 +0.29 -0.99 0 +0.26 0.00 0 +0.26 -1.32 0 +0.32 -2.87 0 +0.34 -3.20 0 +0.24 -5.02 0 +0.34 -4.55 0 +0.36 -6.54 0 +0.4 -5.74 0 +0.36 -6.38 0 +0.31 -6.85 0 +0.44 -3.65 0 +0.42 -5.60 0 +0.41 -5.32 0 +0.33 -2.78 0 +0.33 -2.61 0 +0.37 -3.72 0 +0.3 -2.48 0 +0.27 -2.33 0 +0.23 -1.99 0 +0.13 -1.76 0 +0.28 -2.25 0 +0.28 -0.41 0 +0.28 -0.41 0 +0.28 -0.82 0 +0.29 -0.79 0 +0.29 -0.40 0 +0.28 -0.41 0 +0.29 -1.58 0 +0.23 0.25 0 +0.01 0.00 0 +0.09 0.00 0 +0.28 -1.02 0 +0.27 -0.64 0 +0.25 -0.46 0 +0.27 -0.85 0 +0.28 -1.02 0 +0.26 -0.44 0 +0.27 -0.85 0 +0.27 0.00 0 +0.27 -0.42 0 +0.27 -1.27 0 +0.28 -0.41 0 +0.25 0.00 0 +0.24 -1.19 0 +0.1 0.00 0 +0.02 0.00 0 +0.17 -0.67 0 +0.18 0.64 0 +0.18 0.64 0 +0.11 1.04 0 +0.14 0.82 0 +0.17 0.00 0 +0.17 0.67 0 +0.16 1.43 0 +0.17 0.00 0 +0.1 1.15 0 +0.1 2.29 0 +0.03 0.00 0 +0.09 -1.27 0 +0.08 1.43 0 +0.17 0.00 0 +LAP +0.17 0.67 0 +0.17 0.67 0 +0.18 0.64 0 +0.17 0.67 0 +0.1 1.72 0 +0.09 1.27 0 +0.1 4.59 0 +0.09 6.38 0 +0.07 8.21 0 +0.08 4.30 0 +0.1 1.15 0 +0.1 2.29 0 +0.1 2.29 0 +0.09 5.74 0 +0.09 2.55 0 +0.09 3.82 0 +0.08 5.74 0 +0.07 4.92 0 +0.07 4.92 0 +0.08 2.87 0 +0.09 5.10 0 +0.08 2.87 0 +0.08 5.02 0 +0.07 5.74 0 +0.08 2.87 0 +0.08 5.02 0 +0.1 1.15 0 +0.1 2.29 0 +0.08 5.74 0 +0.06 8.63 0 +0.08 3.58 0 +0.07 4.92 0 +0.07 4.92 0 +0.08 2.87 0 +0.14 6.15 0 +0.08 3.58 0 +0.08 2.87 0 +0.06 4.78 0 +0.07 4.92 0 +0.08 2.87 0 +0.06 9.59 0 +0.06 5.74 0 +0.05 4.59 0 +0.05 8.05 0 +0.05 8.05 0 +0.06 5.74 0 +0.06 7.66 0 +0.07 1.64 0 +0.07 3.28 0 +0.07 4.92 0 +0.06 5.74 0 +0.06 5.74 0 +0.01 11.54 0 +0.02 0.00 0 +0.08 2.87 0 +0.08 5.02 0 +0.08 4.30 0 +0.07 3.28 0 +0.08 2.87 0 +0.08 5.02 0 +0.08 2.87 0 +0.09 2.55 0 +0.09 3.18 0 +0.09 2.55 0 +0.09 1.91 0 +0.09 2.55 0 +0.08 4.30 0 +0.07 6.56 0 +0.07 5.74 0 +0.06 6.70 0 +0.07 4.92 0 +0.07 4.92 0 +0.06 5.74 0 +0.07 3.28 0 +0.07 4.92 0 +0.08 2.87 0 +0.07 4.92 0 +0.09 2.55 0 +0.12 2.87 0 +0.1 2.87 0 +LAP +0.2 -3.15 0 +0.19 -5.74 0 +0.3 -3.82 0 +0.37 -4.65 0 +0.32 -5.02 0 +0.3 -5.74 0 +0.31 -4.07 0 +0.45 -6.00 0 +0.41 -4.76 0 +0.4 -3.58 0 +0.4 -4.30 0 +0.39 -3.23 0 +0.36 -2.87 0 +0.37 -2.79 0 +0.37 -2.63 0 +0.31 -1.29 0 +0.29 -1.19 0 +0.33 -2.61 0 +0.35 -1.80 0 +0.25 -0.46 0 +0.26 -3.53 0 +0.35 -0.82 0 +0.3 -1.15 0 +0.27 -0.64 0 +0.25 -0.23 0 +0.2 0.00 0 +0.22 0.52 0 +0.19 0.00 0 +0.22 0.00 0 +0.23 0.00 0 +0.21 0.00 0 +0.22 0.52 0 +0.21 0.55 0 +0.21 0.00 0 +0.19 0.00 0 +0.08 1.43 0 +0.18 0.00 0 +0.18 -0.64 0 +0.2 0.57 0 +0.22 0.00 0 +0.22 0.00 0 +0.21 1.36 0 +0.19 -0.60 0 +0.19 0.60 0 +0.12 0.00 0 +[END COURSE DATA] diff --git a/src/test/workouts/HourofPowerV1.erg b/src/test/workouts/HourofPowerV1.erg new file mode 100644 index 000000000..3580913c6 --- /dev/null +++ b/src/test/workouts/HourofPowerV1.erg @@ -0,0 +1,388 @@ +[COURSE HEADER] +VERSION = 2 +UNITS = ENGLISH +DESCRIPTION = Bill Black's Hour of Power +FILE NAME = HourofPowerV1.erg +FTP = 275 +MINUTES WATTS +[END COURSE HEADER] + +[COURSE DATA] +0.0 124 +2.0 124 +2.0 155 +6.0 155 +6.0 186 +10.0 186 +10.0 248 +12.5 248 +12.5 170 +15.0 170 +15.25 272 +15.5 276 +15.75 271 +16.0 273 +16.25 269 +16.5 267 +16.75 276 +17.0 278 +17.25 269 +17.5 269 +17.75 264 +18.0 264 +18.06 311 +18.12 333 +18.18 345 +18.24 350 +18.3 353 +18.36 354 +18.42 356 +18.42 264 +18.42 264 +18.67 269 +18.92 268 +19.17 266 +19.42 270 +19.67 268 +19.92 276 +20.17 278 +20.42 264 +20.67 267 +20.92 273 +21.17 275 +21.42 266 +21.48 311 +21.54 334 +21.6 345 +21.65 350 +21.71 353 +21.77 354 +21.83 356 +21.83 264 +21.83 264 +22.08 270 +22.33 274 +22.58 270 +22.83 268 +23.08 270 +23.33 278 +23.58 271 +23.83 271 +24.08 275 +24.33 265 +24.58 273 +24.83 273 +24.89 315 +24.95 336 +25.01 346 +25.07 351 +25.13 353 +25.19 354 +25.25 356 +25.25 264 +25.25 264 +25.5 278 +25.75 272 +26.0 267 +26.25 273 +26.5 265 +26.75 272 +27.0 272 +27.25 271 +27.5 275 +27.75 264 +28.0 264 +28.25 273 +28.31 315 +28.37 336 +28.43 346 +28.49 351 +28.55 353 +28.61 354 +28.67 356 +28.67 264 +28.67 264 +28.92 271 +29.17 271 +29.42 278 +29.67 264 +29.92 274 +30.17 266 +30.42 273 +30.67 270 +30.92 275 +31.17 278 +31.42 270 +31.67 273 +31.73 315 +31.79 336 +31.85 346 +31.9 351 +31.96 353 +32.02 354 +32.08 356 +32.08 264 +32.08 264 +32.33 271 +32.58 274 +32.83 271 +33.08 266 +33.33 267 +33.58 275 +33.83 264 +34.08 271 +34.33 278 +34.58 273 +34.83 275 +35.08 267 +35.14 312 +35.2 334 +35.26 345 +35.32 350 +35.38 353 +35.44 354 +35.5 356 +35.5 264 +35.5 264 +35.75 269 +36.0 277 +36.25 264 +36.5 266 +36.75 266 +37.0 270 +37.25 275 +37.5 268 +37.75 271 +38.0 272 +38.25 274 +38.5 278 +38.56 318 +38.62 337 +38.68 346 +38.74 351 +38.8 353 +38.86 354 +38.92 356 +38.92 264 +38.92 264 +39.17 272 +39.42 267 +39.67 264 +39.92 270 +40.17 274 +40.42 271 +40.67 266 +40.92 272 +41.17 276 +41.42 265 +41.67 273 +41.92 271 +41.98 314 +42.04 335 +42.1 346 +42.15 351 +42.21 353 +42.27 354 +42.33 356 +42.33 264 +42.33 264 +42.58 277 +42.83 275 +43.08 275 +43.33 271 +43.58 276 +43.83 264 +44.08 275 +44.33 267 +44.58 271 +44.83 271 +45.08 265 +45.33 278 +45.39 318 +45.45 337 +45.51 346 +45.57 351 +45.63 353 +45.69 354 +45.75 356 +45.75 264 +45.75 264 +46.0 265 +46.25 265 +46.5 275 +46.75 276 +47.0 268 +47.25 265 +47.5 267 +47.75 271 +48.0 276 +48.25 264 +48.5 278 +48.75 271 +48.81 314 +48.87 335 +48.93 346 +48.99 351 +49.05 353 +49.11 354 +49.17 356 +49.17 264 +49.17 264 +49.42 276 +49.67 278 +49.92 268 +50.17 273 +50.42 269 +50.67 273 +50.92 266 +51.17 268 +51.42 274 +51.67 265 +51.92 270 +52.17 272 +52.23 315 +52.29 335 +52.35 346 +52.4 351 +52.46 353 +52.52 354 +52.58 356 +52.58 264 +52.58 264 +52.83 274 +53.08 264 +53.33 272 +53.58 267 +53.83 278 +54.08 274 +54.33 268 +54.58 271 +54.83 276 +55.08 264 +55.33 269 +55.58 271 +55.64 314 +55.7 335 +55.76 346 +55.82 351 +55.88 353 +55.94 354 +56.0 356 +56.0 264 +56.0 264 +56.25 270 +56.5 277 +56.75 264 +57.0 264 +57.25 278 +57.5 267 +57.75 267 +58.0 268 +58.25 274 +58.5 265 +58.75 276 +59.0 272 +59.06 314 +59.12 335 +59.18 346 +59.24 351 +59.3 353 +59.36 354 +59.42 356 +59.42 264 +59.42 264 +59.67 267 +59.92 277 +60.17 268 +60.42 272 +60.67 272 +60.92 268 +61.17 269 +61.42 271 +61.67 276 +61.92 278 +62.17 275 +62.42 271 +62.48 314 +62.54 335 +62.6 346 +62.65 351 +62.71 353 +62.77 354 +62.83 356 +62.83 264 +62.83 264 +63.08 270 +63.33 267 +63.58 268 +63.83 277 +64.08 265 +64.33 269 +64.58 275 +64.83 275 +65.08 267 +65.33 272 +65.58 274 +65.83 264 +65.89 311 +65.95 334 +66.01 345 +66.07 350 +66.13 353 +66.19 354 +66.25 356 +66.25 264 +66.25 264 +66.5 266 +66.75 265 +67.0 276 +67.25 274 +67.5 266 +67.75 277 +68.0 274 +68.25 272 +68.5 265 +68.75 273 +69.0 265 +69.25 265 +69.31 311 +69.37 334 +69.43 345 +69.49 350 +69.55 353 +69.61 354 +69.67 356 +69.67 264 +69.67 264 +69.92 272 +70.17 265 +70.42 269 +70.67 278 +70.92 275 +71.17 269 +71.42 273 +71.67 277 +71.92 275 +72.17 270 +72.42 269 +72.67 274 +72.73 316 +72.79 336 +72.85 346 +72.9 351 +72.96 353 +73.02 354 +73.08 356 +73.08 264 +73.08 264 +73.33 273 +73.58 275 +73.83 269 +74.08 265 +74.33 272 +74.58 277 +74.83 269 +75.08 272 +77.0 124 +80.0 124 +[END COURSE DATA] diff --git a/src/test/workouts/IsoL4hour.mrc b/src/test/workouts/IsoL4hour.mrc new file mode 100644 index 000000000..baa9dc791 --- /dev/null +++ b/src/test/workouts/IsoL4hour.mrc @@ -0,0 +1,20 @@ +[COURSE HEADER] +VERSION = 2 +UNITS = ENGLISH +DESCRIPTION = An hour at FTP +FILE NAME = IsoL4hour.mrc +MINUTES PERCENT +[END COURSE HEADER] + +[COURSE DATA] +0 55 +5 55 +5 75 +15 75 +15 50 +20 50 +20 100 +80 100 +80 55 +95 55 +[END COURSE DATA] diff --git a/src/test/workouts/Kilo.crs b/src/test/workouts/Kilo.crs new file mode 100644 index 000000000..39918761d --- /dev/null +++ b/src/test/workouts/Kilo.crs @@ -0,0 +1,18 @@ +[COURSE HEADER] +UNITS = METRIC +DESCRIPTION = Golden Cheetah Velodrome Kilo TT +FILE NAME = kilo.crs +;DISTANCE GRADE WIND +;COMMENTS +[END COURSE HEADER] + +[COURSE DATA] +0.25 0.0 0 +LAP +0.25 0.0 0 +LAP +0.25 0.0 0 +LAP +0.25 0.0 0 +LAP +[END COURSE DATA] diff --git a/src/test/workouts/L345Pyramid.erg b/src/test/workouts/L345Pyramid.erg new file mode 100644 index 000000000..3953d7fe5 --- /dev/null +++ b/src/test/workouts/L345Pyramid.erg @@ -0,0 +1,93 @@ +[COURSE HEADER] +VERSION = 2 +UNITS = ENGLISH +DESCRIPTION = 3 sets of L345 Pyramid +FILE NAME = L345Pyramid.erg +MINUTES WATTS +[END COURSE HEADER] + +[COURSE DATA] +; +; All relative to users current 60 min CP +; +; Warm up at L1 through to L2 +0 55% +5 55% +10 75% +15 75% +20 50% +25 50% + +; +; Set one +; +25 LAP +25 75% +30 75% +30 90% +35 90% +35 100% +40 100% +40 110% +45 110% +45 120% +50 120% +50 110% +55 110% +55 100% +60 100% +60 90% +65 90% +65 75% +70 75% + +; +; Set two +; +70 LAP +70 75% +75 75% +75 90% +80 90% +80 100% +85 100% +85 110% +90 110% +90 120% +95 120% +95 110% +100 110% +100 100% +105 100% +105 90% +110 90% +110 75% +115 75% +; +; Set three +; +115 LAP +120 75% +120 90% +125 90% +125 100% +130 100% +130 110% +135 110% +135 120% +140 120% +140 110% +145 110% +145 100% +150 100% +150 90% +155 90% +155 75% +160 75% +; +; And cool down +; +160 LAP +160 60% +180 60% +[END COURSE DATA] diff --git a/src/test/workouts/RampTest.erg b/src/test/workouts/RampTest.erg new file mode 100644 index 000000000..b00dc3d97 --- /dev/null +++ b/src/test/workouts/RampTest.erg @@ -0,0 +1,24 @@ +[COURSE HEADER] +VERSION = 2 +UNITS = ENGLISH +DESCRIPTION = Ramp Test - 10w every 15 seconds +FILE NAME = Ramp.erg +MINUTES WATTS +[END COURSE HEADER] + +[COURSE DATA] +0 100 +1 140 +2 180 +3 220 +4 260 +4 LAP +5 300 +6 340 +6 LAP +7 380 +8 420 +8 LAP +9 460 +10 500 +[END COURSE DATA] diff --git a/src/test/workouts/RoadTempo2hrs.erg b/src/test/workouts/RoadTempo2hrs.erg new file mode 100644 index 000000000..9a18506d3 --- /dev/null +++ b/src/test/workouts/RoadTempo2hrs.erg @@ -0,0 +1,310 @@ +[COURSE HEADER] +VERSION = 2 +UNITS = ENGLISH +DESCRIPTION = Brisk Tempo Hilly Ride (2hrs) +FILE NAME = 260WFTPRider_Road110MinsTempoHilly.erg +FTP = 260 +MINUTES WATTS +[END COURSE HEADER] + +[COURSE DATA] +0.0 104 +2.0 104 +2.0 130 +5.0 130 +5.0 156 +7.5 156 +7.5 156 +10.0 156 +10.33 153 +10.67 177 +11.0 416 +11.33 281 +11.67 62 +12.0 125 +12.33 161 +12.67 104 +13.0 133 +13.33 169 +13.67 278 +14.0 265 +14.33 185 +14.67 237 +15.0 239 +15.33 208 +15.67 68 +16.0 114 +16.33 151 +16.67 299 +17.0 187 +17.33 296 +17.67 104 +18.0 195 +18.33 221 +18.67 338 +19.0 138 +19.33 239 +19.67 216 +20.0 161 +20.33 283 +20.67 182 +21.0 268 +21.33 104 +21.67 190 +22.0 151 +22.33 91 +22.67 133 +23.0 94 +23.33 130 +23.67 104 +24.0 190 +24.33 159 +24.67 200 +25.0 174 +25.33 190 +25.67 179 +26.0 179 +26.33 177 +26.67 169 +27.0 164 +27.33 130 +27.67 112 +28.0 177 +28.33 234 +28.67 185 +29.0 190 +29.33 164 +29.67 172 +30.0 169 +30.33 174 +30.67 166 +31.0 182 +31.33 182 +31.67 203 +32.0 234 +32.33 203 +32.67 203 +33.0 159 +33.33 96 +33.67 226 +34.0 221 +34.33 273 +34.67 304 +35.0 250 +35.33 99 +35.67 239 +36.0 190 +36.33 203 +36.67 177 +37.0 151 +37.33 99 +37.67 226 +38.0 151 +38.33 65 +38.67 172 +39.0 130 +39.33 205 +39.67 192 +40.0 224 +40.33 224 +40.67 218 +41.0 276 +41.33 195 +41.67 224 +42.0 221 +42.33 190 +42.67 226 +43.0 218 +43.33 213 +43.67 299 +44.0 250 +44.33 268 +44.67 278 +45.0 268 +45.33 294 +45.67 211 +46.0 177 +46.33 172 +46.67 242 +47.0 273 +47.33 169 +47.67 107 +48.0 65 +48.33 250 +48.67 34 +49.0 101 +49.33 161 +49.67 229 +50.0 304 +50.33 216 +50.67 159 +51.0 304 +51.33 159 +51.67 291 +52.0 185 +52.33 101 +52.67 104 +53.0 94 +53.33 109 +53.67 234 +54.0 237 +54.33 151 +54.67 138 +55.0 437 +55.33 135 +55.67 216 +56.0 179 +56.33 96 +56.67 148 +57.0 130 +57.33 364 +57.67 159 +58.0 96 +58.33 437 +58.67 68 +59.0 151 +59.33 39 +59.67 81 +60.0 133 +60.33 187 +60.67 122 +61.0 179 +61.33 330 +61.67 244 +62.0 255 +62.33 208 +62.67 250 +63.0 247 +63.33 125 +63.67 109 +64.0 68 +64.33 341 +64.67 211 +65.0 317 +65.33 133 +65.67 148 +66.0 224 +66.33 437 +66.67 130 +67.0 268 +67.33 283 +67.67 289 +68.0 302 +68.33 382 +68.67 52 +69.0 185 +69.33 140 +69.67 70 +70.0 276 +70.33 120 +70.67 169 +71.0 120 +71.33 146 +71.67 198 +72.0 203 +72.33 161 +72.67 130 +73.0 218 +73.33 164 +73.67 205 +74.0 203 +74.33 190 +74.67 151 +75.0 159 +75.33 187 +75.67 213 +76.0 226 +76.33 213 +76.67 164 +77.0 169 +77.33 208 +77.67 234 +78.0 229 +78.33 221 +78.67 190 +79.0 208 +79.33 221 +79.67 229 +80.0 198 +80.33 133 +80.67 218 +81.0 242 +81.33 361 +81.67 205 +82.0 221 +82.33 237 +82.67 218 +83.0 224 +83.33 229 +83.67 179 +84.0 109 +84.33 224 +84.67 205 +85.0 70 +85.33 146 +85.67 195 +86.0 177 +86.33 112 +86.67 226 +87.0 231 +87.33 226 +87.67 278 +88.0 224 +88.33 257 +88.67 221 +89.0 231 +89.33 247 +89.67 257 +90.0 244 +90.33 270 +90.67 320 +91.0 322 +91.33 281 +91.67 330 +92.0 237 +92.33 237 +92.67 237 +93.0 351 +93.33 247 +93.67 55 +94.0 174 +94.33 143 +94.67 130 +95.0 114 +95.33 114 +95.67 437 +96.0 226 +96.33 169 +96.67 328 +97.0 192 +97.33 296 +97.67 159 +98.0 122 +98.33 96 +98.67 75 +99.0 135 +99.33 159 +99.67 179 +100.0 203 +100.33 140 +100.67 419 +101.0 112 +101.33 195 +101.67 169 +102.0 62 +102.33 182 +102.67 117 +103.0 356 +103.33 109 +103.67 101 +104.0 398 +104.33 78 +104.67 159 +105.0 86 +105.33 49 +105.67 374 +106.0 213 +106.32 104 +106.32 104 +110.0 104 +[END COURSE DATA]