Compare commits

...

43 Commits

Author SHA1 Message Date
Roberto Viola
4b86f77a22 gpx imported correctly!
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-14 09:24:43 +02:00
Roberto Viola
e43276e52b gpx opened and analyzed. i have to put in the train program table, easy
step i guess

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-14 09:03:21 +02:00
Roberto Viola
5e7a8d938b fan speed supported
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-13 08:38:50 +02:00
cagnulein
a0c1e1b645 Merge pull request #21 from cagnulein/ui
Ui and much more!
2020-10-13 08:00:12 +02:00
cagnulein
aa53956a35 Merge branch 'master' into ui 2020-10-13 07:59:56 +02:00
Roberto Viola
cd1c10a090 -no-log added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 16:06:07 +02:00
Roberto Viola
c21e337bdd train program example added 2020-10-12 15:32:49 +02:00
Roberto Viola
1d23ac4b81 fix typo
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 15:13:42 +02:00
Roberto Viola
bbeaa5ec95 calories added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 15:12:34 +02:00
Roberto Viola
9f6a4de4ac ui screenshot added 2020-10-12 15:06:20 +02:00
Roberto Viola
ee1c3e0118 connectivity icon on UI added
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 15:00:02 +02:00
Roberto Viola
81ac8909c8 elevationGain added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 12:23:27 +02:00
Roberto Viola
3a45935617 UI layout fixed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 12:12:20 +02:00
Roberto Viola
c89c381177 odometer added on UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 12:09:40 +02:00
Roberto Viola
ea57069f33 train program total distance added to UI
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 11:51:05 +02:00
Roberto Viola
bf40c460a5 added currentRowElapsedTime, totalElapsedTime and Duration of the train
program on UI

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 11:39:58 +02:00
Roberto Viola
338b19f664 added the ability to enable/disable the train program; start button on
the console starts the program also is valid and enabled

Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 11:03:42 +02:00
Roberto Viola
9f9000427f auto start tape on connect disabled
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 10:44:56 +02:00
Roberto Viola
92cd9baea3 updated installation from source steps 2020-10-12 10:25:57 +02:00
Roberto Viola
e21ad70ea9 getting kcal and distance from the treadmill (tests need)
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 08:55:26 +02:00
cagnulein
d7ac459a3d fan speed buttons managed
Signed-off-by: Roberto Viola <roberto.viola@systemceramics.com>
2020-10-12 08:24:19 +02:00
Roberto Viola
d58db4100f fix typo on requestSpeed check 2020-10-11 16:15:08 +02:00
Roberto Viola
3c7cb254e6 fixed changing speed and inclination at the same time 2020-10-11 15:59:40 +02:00
Roberto Viola
259b53e8e0 change speed and inclination only if needed 2020-10-11 15:48:56 +02:00
Roberto Viola
b73092bd8f fix treadmill slots 2020-10-11 15:28:02 +02:00
Roberto Viola
787b9aa2c2 trainprogram debug lines added 2020-10-11 14:44:41 +02:00
Roberto Viola
73a0bd7c65 fix seg fault without a train program 2020-10-11 14:39:06 +02:00
Roberto Viola
d068526e55 save program xml issue fixed 2020-10-11 14:27:56 +02:00
Roberto Viola
8945063f30 support for future arguments to the executable 2020-10-11 14:27:35 +02:00
Roberto Viola
4625bccad3 no bluetooth dongle support added 2020-10-11 14:26:56 +02:00
Roberto Viola
5c723375d7 trainprogram start added 2020-10-11 10:34:59 +02:00
Roberto Viola
1212bc83f8 restart bt connection to domyos treadmill 2020-10-11 09:45:17 +02:00
Roberto Viola
efc9788c89 fixed build issues 2020-10-10 13:47:28 +02:00
Roberto Viola
42c43158e6 Merge remote-tracking branch 'origin/startup_sequence_fix' into ui 2020-10-10 13:27:53 +02:00
cagnulein
1d6c46a32b Merge branch 'cleaning_code' into ui 2020-10-09 11:25:35 +02:00
Roberto Viola
3ba0219ce4 added watt visualization 2020-10-09 10:36:27 +02:00
cagnulein
c2c5b7746f treadmill class created deleting all the external variables 2020-10-08 17:21:16 +02:00
cagnulein
4944f6d48d scheduler added 2020-10-08 16:35:36 +02:00
cagnulein
33a478b1ae load, save and reset added and tested 2020-10-08 15:19:38 +02:00
cagnulein
4712d5780a adding ability to save and load training program (not finished yet) 2020-10-08 14:19:09 +02:00
cagnulein
76d3139d79 added spaces on debug log 2020-10-08 08:16:21 +02:00
cagnulein
8fb8ed6f44 Merge pull request #14 from cagnulein/startup_sequence_fix
trying to fix startup sequence
2020-10-08 08:13:35 +02:00
cagnulein
5774e725a7 started adding a gui 2020-10-02 16:38:07 +02:00
23 changed files with 1850 additions and 132 deletions

View File

@@ -3,17 +3,30 @@ Zwift bridge for Domyos treadmills
<a href="https://www.buymeacoffee.com/cagnulein" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
![UI](docs/ui.png)
### Features
1. Domyos compatible
2. Zwift compatible
3. Create, load and save train programs
4. Measure distance, elevation gain and watts
![First Success](docs/first_success.jpg)
### Installation
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
### Installation from source
$ sudo apt upgrade && sudo apt update # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt install libqt5bluetooth5
$ sudo apt install git libqt5bluetooth5 libqt5widgets5
$ sudo hciconfig hci0 leadv 0
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd src
$ qmake
$ make -j4
$ sudo ./qdomyos-zwift
@@ -21,6 +34,12 @@ $ sudo ./qdomyos-zwift
Raspberry PI 0W and Domyos Intense Run
### No gui version
run as
$ sudo ./qdomyos-zwift -no-gui
### Reference
https://github.com/ProH4Ck/treadmill-bridge

BIN
docs/ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,8 +1,10 @@
#include "domyostreadmill.h"
#include "trainprogram.h"
#include "virtualtreadmill.h"
#include <QFile>
#include <QDateTime>
#include <QMetaEnum>
#include <QBluetoothLocalDevice>
// set speed and incline to 0
uint8_t initData1[] = { 0xf0, 0xc8, 0x01, 0xb9 };
@@ -13,39 +15,6 @@ uint8_t noOpData[] = { 0xf0, 0xac, 0x9c };
// stop tape
uint8_t initDataF0C800B8[] = { 0xf0, 0xc8, 0x00, 0xb8 };
#if 0
uint8_t initDataStart[] = { 0xf0, 0xc8, 0x00, 0xb8 };
uint8_t initDataStart2[] = { 0xf0, 0xcb, 0x01, 0x00, 0x00, 0x02, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00 };
uint8_t initDataStart3[] = { 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xb6 };
uint8_t initDataStart4[] = { 0xf0, 0xc8, 0x00, 0xb8 };
uint8_t initDataStart5[] = { 0xf0, 0xc8, 0x01, 0xb9 };
uint8_t initDataStart6[] =
{
0xf0, 0xad, 0xff, 0xff, 0x00, 0x0a, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart7[] = { 0xff, 0xff, 0x95 };
uint8_t initDataStart8[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart9[] = { 0xff, 0xff, 0x8b };
uint8_t initDataStart10[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x03, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart11[] = { 0xff, 0xff, 0x8a };
uint8_t initDataStart12[] =
{
0xf0, 0xad, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x04, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff
};
uint8_t initDataStart13[] = { 0xff, 0xff, 0x9e };
#endif
// main startup sequence
uint8_t initDataStart[] = { 0xf0, 0xa3, 0x93 };
uint8_t initDataStart2[] = { 0xf0, 0xa4, 0x94 };
@@ -81,42 +50,45 @@ QBluetoothUuid _gattCommunicationChannelServiceId((QString)"49535343-fe7d-4ae5-8
QBluetoothUuid _gattWriteCharacteristicId((QString)"49535343-8841-43f4-a8d4-ecbe34729bb3");
QBluetoothUuid _gattNotifyCharacteristicId((QString)"49535343-1e4d-4bd9-ba61-23c647249616");
QBluetoothDeviceInfo treadmill;
QBluetoothDeviceInfo bttreadmill;
QLowEnergyController* m_control = 0;
QLowEnergyService* gattCommunicationChannelService = 0;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotifyCharacteristic;
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
bool restart = false;
bool initDone = false;
bool initRequest = false;
extern volatile double currentSpeed;
extern volatile double currentIncline;
extern volatile uint8_t currentHeart;
extern volatile double requestSpeed;
extern volatile double requestIncline;
extern volatile int8_t requestStart;
extern volatile int8_t requestStop;
QFile* debugCommsLog = 0;
QFile* debugCommsLog;
domyostreadmill::domyostreadmill()
domyostreadmill::domyostreadmill(bool logs)
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
QTimer* refresh = new QTimer(this);
debugCommsLog = new QFile("debug-" + QDateTime::currentDateTime().toString() + ".log");
debugCommsLog->open(QIODevice::WriteOnly | QIODevice::Unbuffered);
refresh = new QTimer(this);
if(logs)
{
debugCommsLog = new QFile("debug-" + QDateTime::currentDateTime().toString() + ".log");
debugCommsLog->open(QIODevice::WriteOnly | QIODevice::Unbuffered);
}
initDone = false;
// Create a discovery agent and connect to its signals
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
this, SLOT(deviceDiscovered(QBluetoothDeviceInfo)));
if(!QBluetoothLocalDevice::allDevices().count())
{
debug("no bluetooth dongle found!");
}
else
{
// Create a discovery agent and connect to its signals
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
this, SLOT(deviceDiscovered(QBluetoothDeviceInfo)));
// Start a discovery
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
// Start a discovery
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
}
connect(refresh, SIGNAL(timeout()), this, SLOT(update()));
refresh->start(200);
@@ -125,8 +97,11 @@ domyostreadmill::domyostreadmill()
void domyostreadmill::debug(QString text)
{
QString debug = QDateTime::currentDateTime().toString() + text + '\n';
debugCommsLog->write(debug.toLocal8Bit());
qDebug() << debug;
if(debugCommsLog)
{
debugCommsLog->write(debug.toLocal8Bit());
qDebug() << debug;
}
}
void domyostreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log)
@@ -152,7 +127,7 @@ void domyostreadmill::updateDisplay(uint16_t elapsed)
writeIncline[3] = (elapsed >> 8) & 0xFF; // high byte for elapsed time (in seconds)
writeIncline[4] = (elapsed & 0xFF); // low byte for elasped time (in seconds)
writeIncline[12] = currentHeart;
writeIncline[12] = currentHeart();
for(uint8_t i=0; i<sizeof(writeIncline)-1; i++)
{
@@ -163,14 +138,14 @@ void domyostreadmill::updateDisplay(uint16_t elapsed)
//qDebug() << "writeIncline crc" << QString::number(writeIncline[26], 16);
writeCharacteristic(writeIncline, 20, "updateDisplay speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline) + " elapsed=" + QString::number(elapsed) );
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "updateDisplay speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline) + " elapsed=" + QString::number(elapsed) );
writeCharacteristic(writeIncline, 20, "updateDisplay elapsed=" + QString::number(elapsed) );
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "updateDisplay elapsed=" + QString::number(elapsed) );
}
void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestIncline)
{
uint8_t writeIncline[] = {0xf0, 0xad, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00};
writeIncline[4] = ((uint16_t)(requestSpeed*10) >> 8) & 0xFF;
@@ -192,50 +167,91 @@ void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestInc
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
}
bool domyostreadmill::changeFanSpeed(uint8_t speed)
{
uint8_t fanSpeed[] = {0xf0, 0xca, 0x00, 0x00};
if(speed > 5) return false;
fanSpeed[2] = speed;
for(uint8_t i=0; i<sizeof(fanSpeed)-1; i++)
{
fanSpeed[3] += fanSpeed[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(fanSpeed, 4, "changeFanSpeed speed=" + QString::number(speed));
return true;
}
void domyostreadmill::update()
{
static uint32_t counter = 0;
{
//qDebug() << treadmill.isValid() << m_control->state() << gattCommunicationChannelService << gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
if(initRequest)
{
initRequest = false;
btinit();
btinit(false);
}
else if(treadmill.isValid() &&
else if(restart)
{
startDiscover();
}
else if(bttreadmill.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotifyCharacteristic.isValid() &&
initDone)
{
if(currentSpeed > 0.0)
counter++;
if(currentSpeed() > 0.0)
{
elapsed += ((double)refresh->interval() / 1000.0);
if(trainProgram)
trainProgram->scheduler(refresh->interval());
}
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
// byte 3 - 4 = elapsed time
// byte 17 = inclination
if(requestSpeed != -1)
{
debug("writing speed " + QString::number(requestSpeed));
forceSpeedOrIncline(requestSpeed, currentIncline);
if(requestSpeed != currentSpeed())
{
debug("writing speed " + QString::number(requestSpeed));
double inc = Inclination;
if(requestInclination != -1)
{
inc = requestInclination;
requestInclination = -1;
}
forceSpeedOrIncline(requestSpeed, inc);
}
requestSpeed = -1;
}
if(requestIncline != -1)
if(requestInclination != -1)
{
debug("writing incline " + QString::number(requestIncline));
forceSpeedOrIncline(currentSpeed, requestIncline);
requestIncline = -1;
if(requestInclination != currentInclination())
{
debug("writing incline " + QString::number(requestInclination));
double speed = currentSpeed();
if(requestSpeed != -1)
{
speed = requestSpeed;
requestSpeed = -1;
}
forceSpeedOrIncline(speed, requestInclination);
}
requestInclination = -1;
}
if(requestStart != -1)
{
debug("starting...");
btinit();
btinit(true);
requestStart = -1;
emit tapeStarted();
}
if(requestStop != -1)
{
@@ -243,6 +259,20 @@ void domyostreadmill::update()
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}
if(requestIncreaseFan != -1)
{
debug("increasing fan speed...");
changeFanSpeed(FanSpeed + 1);
requestIncreaseFan = -1;
}
else if(requestDecreaseFan != -1)
{
debug("decreasing fan speed...");
changeFanSpeed(FanSpeed - 1);
requestDecreaseFan = -1;
}
elevationAcc += (currentSpeed() / 3600.0) * 1000 * (currentInclination() / 100) * (refresh->interval() / 1000);
}
}
@@ -276,24 +306,41 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
debug("stop button pressed!");
requestStop = 1;
}
else if (newValue.at(22) == 0x0b)
{
debug("increase speed fan pressed!");
requestIncreaseFan = 1;
}
else if (newValue.at(22) == 0x0a)
{
debug("decrease speed fan pressed!");
requestDecreaseFan = 1;
}
/*if ((uint8_t)newValue.at(1) != 0xbc && newValue.at(2) != 0x04) // intense run, these are the bytes for the inclination and speed status
return;*/
double speed = GetSpeedFromPacket(newValue);
double incline = GetInclinationFromPacket(newValue);
double kcal = GetKcalFromPacket(newValue);
double distance = GetDistanceFromPacket(newValue);
currentHeart = newValue.at(18);
Heart = newValue.at(18);
FanSpeed = newValue.at(23);
debug("Current speed: " + QString::number(speed));
debug("Current incline: " + QString::number(incline));
debug("Current heart: " + QString::number(currentHeart));
debug("Current heart: " + QString::number(Heart));
debug("Current KCal: " + QString::number(kcal));
debug("Current Distance: " + QString::number(distance));
if(m_control->error() != QLowEnergyController::NoError)
qDebug() << "QLowEnergyController ERROR!!" << m_control->errorString();
currentSpeed = speed;
currentIncline = incline;
Speed = speed;
Inclination = incline;
KCal = kcal;
Distance = distance;
}
double domyostreadmill::GetSpeedFromPacket(QByteArray packet)
@@ -303,6 +350,19 @@ double domyostreadmill::GetSpeedFromPacket(QByteArray packet)
return data;
}
double domyostreadmill::GetKcalFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(10) << 8) | packet.at(11);
return (double)convertedData;
}
double domyostreadmill::GetDistanceFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(12) << 8) | packet.at(13);
double data = ((double)convertedData) / 10.0f;
return data;
}
double domyostreadmill::GetInclinationFromPacket(QByteArray packet)
{
uint16_t convertedData = (packet.at(2) << 8) | packet.at(3);
@@ -311,7 +371,7 @@ double domyostreadmill::GetInclinationFromPacket(QByteArray packet)
return data;
}
void domyostreadmill::btinit()
void domyostreadmill::btinit(bool startTape)
{
writeCharacteristic(initData1, sizeof(initData1), "init");
writeCharacteristic(initData2, sizeof(initData2), "init");
@@ -325,9 +385,12 @@ void domyostreadmill::btinit()
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init");
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init");
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init");
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init");
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init");
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init");
if(startTape)
{
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init");
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init");
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init");
}
initDone = true;
}
@@ -363,7 +426,7 @@ void domyostreadmill::stateChanged(QLowEnergyService::ServiceState state)
if(!first)
{
debug("creating virtual treadmill interface...");
v = new virtualtreadmill();
v = new virtualtreadmill(this);
}
first = 1;
// ********************************************************************************************************
@@ -415,8 +478,8 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
if(device.name().startsWith("Domyos"))
{
discoveryAgent->stop();
treadmill = device;
m_control = QLowEnergyController::createCentral(treadmill, this);
bttreadmill = device;
m_control = QLowEnergyController::createCentral(bttreadmill, this);
connect(m_control, SIGNAL(serviceDiscovered(const QBluetoothUuid &)),
this, SLOT(serviceDiscovered(const QBluetoothUuid &)));
connect(m_control, SIGNAL(discoveryFinished()),
@@ -429,7 +492,7 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
Q_UNUSED(error);
Q_UNUSED(this);
debug("Cannot connect to remote device.");
exit(1);
restart = true;
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
@@ -439,7 +502,7 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
debug("LowEnergy controller disconnected");
exit(2);
restart = true;
});
// Connect
@@ -447,3 +510,22 @@ void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
return;
}
}
void domyostreadmill::startDiscover()
{
initDone = false;
initRequest = false;
if(m_control)
delete m_control;
if(gattCommunicationChannelService)
delete gattCommunicationChannelService;
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
restart = false;
}
bool domyostreadmill::connected()
{
if(!m_control)
return false;
return m_control->state() == QLowEnergyController::DiscoveredState;
}

View File

@@ -25,20 +25,31 @@
#include <QObject>
class domyostreadmill : QObject
#include "virtualtreadmill.h"
#include "treadmill.h"
class domyostreadmill : public treadmill
{
Q_OBJECT
public:
domyostreadmill();
domyostreadmill(bool logs = true);
virtualtreadmill* virtualTreadMill = 0;
bool connected();
bool changeFanSpeed(uint8_t speed);
private:
double GetSpeedFromPacket(QByteArray packet);
double GetInclinationFromPacket(QByteArray packet);
double GetKcalFromPacket(QByteArray packet);
double GetDistanceFromPacket(QByteArray packet);
void forceSpeedOrIncline(double requestSpeed, double requestIncline);
void updateDisplay(uint16_t elapsed);
void btinit();
void btinit(bool startTape);
void writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log=false);
void debug(QString text);
void startDiscover();
QTimer* refresh;
private slots:

49
src/gpx.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "gpx.h"
#include <QDomDocument>
#include <QDebug>
#include "math.h"
gpx::gpx(QObject *parent) : QObject(parent)
{
}
QList<gpx_altitude_point_for_treadmill> gpx::open(QString gpx)
{
QFile input(gpx);
input.open(QIODevice::ReadOnly);
QDomDocument doc;
doc.setContent(&input);
QDomNodeList points = doc.elementsByTagName("trkpt");
for (int i = 0; i < points.size(); i++)
{
QDomNode point = points.item(i);
QDomNamedNodeMap att = point.attributes();
QString lat = att.namedItem("lat").nodeValue();
QString lon = att.namedItem("lon").nodeValue();
QDomElement ele = point.firstChildElement("ele");
QDomElement time = point.firstChildElement("time");
gpx_point g;
//2020-10-10T10:54:45
g.time = QDateTime::fromString(time.text(), Qt::ISODate);
g.p.setAltitude(ele.text().toFloat());
g.p.setLatitude(lat.toFloat());
g.p.setLongitude(lon.toFloat());
this->points.append(g);
}
const uint8_t secondsInclination = 60;
QList<gpx_altitude_point_for_treadmill> inclinationList;
for(int32_t i=secondsInclination; i<this->points.count(); i+=secondsInclination)
{
double distance = this->points[i].p.distanceTo(this->points[i-secondsInclination].p);
double elevation = this->points[i].p.altitude() - this->points[i-secondsInclination].p.altitude();
gpx_altitude_point_for_treadmill g;
g.seconds = secondsInclination;
g.speed = (distance / 1000.0) * (3600 / secondsInclination);
g.inclination = (elevation / distance) * 100;
inclinationList.append(g);
}
return inclinationList;
}

38
src/gpx.h Normal file
View File

@@ -0,0 +1,38 @@
#ifndef GPX_H
#define GPX_H
#include <QObject>
#include <QFile>
#include <QTime>
#include <QGeoCoordinate>
class gpx_altitude_point_for_treadmill
{
public:
uint32_t seconds;
float inclination;
float speed;
};
class gpx_point
{
public:
QDateTime time;
QGeoCoordinate p;
};
class gpx : public QObject
{
Q_OBJECT
public:
explicit gpx(QObject *parent = nullptr);
QList<gpx_altitude_point_for_treadmill> open(QString gpx);
private:
QList<gpx_point> points;
signals:
};
#endif // GPX_H

7
src/icons.qrc Normal file
View File

@@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/icons">
<file>icons/bluetooth-icon.png</file>
<file>icons/zwift-on.png</file>
<file>icons/fan.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
src/icons/fan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/icons/zwift-on.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,21 +1,44 @@
#include <QtCore/qcoreapplication.h>
#include <QApplication>
#include "virtualtreadmill.h"
#include "domyostreadmill.h"
#include "mainwindow.h"
bool nologs = false;
QCoreApplication* createApplication(int &argc, char *argv[])
{
bool nogui = false;
for (int i = 1; i < argc; ++i) {
if (!qstrcmp(argv[i], "-no-gui"))
nogui = true;
if (!qstrcmp(argv[i], "-no-log"))
nologs = true;
}
if(nogui)
return new QCoreApplication(argc, argv);
else
return new QApplication(argc, argv);
}
int main(int argc, char *argv[])
{
//QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
#ifndef Q_OS_ANDROID
QCoreApplication app(argc, argv);
#else
QGuiApplication app(argc, argv);
#endif
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
//virtualtreadmill* V = new virtualtreadmill();
domyostreadmill* D = new domyostreadmill();
domyostreadmill* D = new domyostreadmill(!nologs);
if (qobject_cast<QApplication *>(app.data())) {
// start GUI version...
MainWindow* W = new MainWindow(D);
W->show();
} else {
// start non-GUI version...
}
//Q_UNUSED(V);
Q_UNUSED(D);
return app.exec();
return app->exec();
}

305
src/mainwindow.cpp Normal file
View File

@@ -0,0 +1,305 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include "gpx.h"
MainWindow::MainWindow(domyostreadmill* treadmill) :
QDialog(nullptr),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->treadmill = treadmill;
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MainWindow::update);
timer->start(1000);
update();
}
void MainWindow::update()
{
ui->speed->setText(QString::number(treadmill->currentSpeed()));
ui->inclination->setText(QString::number(treadmill->currentInclination()));
ui->heartrate->setText(QString::number(treadmill->currentHeart()));
if(treadmill->virtualTreadMill)
ui->watt->setText(QString::number(treadmill->virtualTreadMill->watts(ui->weight->text().toFloat())));
else
ui->watt->setText("0");
if(treadmill)
{
ui->odometer->setText(QString::number(treadmill->odometer()));
ui->elevationGain->setText(QString::number(treadmill->elevationGain()));
ui->calories->setText(QString::number(treadmill->calories()));
ui->fanBar->setValue(treadmill->fanSpeed());
if(treadmill->trainProgram)
{
ui->trainProgramElapsedTime->setText(treadmill->trainProgram->totalElapsedTime().toString("hh:mm:ss"));
ui->trainProgramCurrentRowElapsedTime->setText(treadmill->trainProgram->currentRowElapsedTime().toString("hh:mm:ss"));
ui->trainProgramDuration->setText(treadmill->trainProgram->duration().toString("hh:mm:ss"));
double distance = treadmill->trainProgram->totalDistance();
if(distance > 0)
{
ui->trainProgramTotalDistance->setText(QString::number(distance));
}
else
ui->trainProgramTotalDistance->setText("N/A");
}
if(treadmill->connected())
{
ui->connectionToTreadmill->setEnabled(true);
if(treadmill->virtualTreadMill)
{
if(treadmill->virtualTreadMill->connected())
ui->connectionToZwift->setEnabled(true);
else
ui->connectionToZwift->setEnabled(false);
}
else
ui->connectionToZwift->setEnabled(false);
}
else
ui->connectionToTreadmill->setEnabled(false);
}
else
{
ui->connectionToTreadmill->setEnabled(false);
ui->connectionToZwift->setEnabled(false);
}
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::addEmptyRow()
{
int row = ui->tableWidget->rowCount();
editing = true;
ui->tableWidget->insertRow(row);
ui->tableWidget->setItem(row, 0, new QTableWidgetItem("00:00:00"));
ui->tableWidget->setItem(row, 1, new QTableWidgetItem("10"));
ui->tableWidget->setItem(row, 2, new QTableWidgetItem("0"));
ui->tableWidget->setItem(row, 3, new QTableWidgetItem(""));
ui->tableWidget->item(row, 0)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidget->item(row, 1)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidget->item(row, 2)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
ui->tableWidget->item(row, 3)->setCheckState(Qt::CheckState::Checked);
editing = false;
}
void MainWindow::on_tableWidget_cellChanged(int row, int column)
{
if(editing) return;
if(column == 0)
{
switch(ui->tableWidget->currentItem()->text().length())
{
case 4:
ui->tableWidget->currentItem()->setText("00:0" + ui->tableWidget->currentItem()->text());
break;
case 5:
ui->tableWidget->currentItem()->setText("00:" + ui->tableWidget->currentItem()->text());
break;
case 7:
ui->tableWidget->currentItem()->setText("0" + ui->tableWidget->currentItem()->text());
break;
}
QString fmt = "hh:mm:ss";
QTime dt = QTime::fromString(ui->tableWidget->currentItem()->text());
QString timeStr = dt.toString("hh:mm:ss");
ui->tableWidget->currentItem()->setText(timeStr);
}
if(row + 1 == ui->tableWidget->rowCount() && ui->tableWidget->currentItem()->text().length() )
addEmptyRow();
QList<trainrow> rows;
for(int i = 0; i < ui->tableWidget->rowCount(); i++)
{
if(!ui->tableWidget->item(i, 0)->text().contains("00:00:00"))
{
trainrow t;
t.duration = QTime::fromString(ui->tableWidget->item(i, 0)->text(), "hh:mm:ss");
t.speed = ui->tableWidget->item(i, 1)->text().toFloat();
t.inclination = ui->tableWidget->item(i, 2)->text().toFloat();
t.forcespeed = ui->tableWidget->item(i, 3)->checkState() == Qt::CheckState::Checked;
rows.append(t);
}
else
{
break;
}
createTrainProgram(rows);
}
}
void MainWindow::trainProgramSignals()
{
connect(treadmill->trainProgram, SIGNAL(start()), treadmill, SLOT(start()));
connect(treadmill->trainProgram, SIGNAL(stop()), treadmill, SLOT(stop()));
connect(treadmill->trainProgram, SIGNAL(changeSpeed(double)), treadmill, SLOT(changeSpeed(double)));
connect(treadmill->trainProgram, SIGNAL(changeInclination(double)), treadmill, SLOT(changeInclination(double)));
connect(treadmill->trainProgram, SIGNAL(changeSpeedAndInclination(double, double)), treadmill, SLOT(changeSpeedAndInclination(double, double)));
connect(treadmill, SIGNAL(tapeStarted()), treadmill->trainProgram, SLOT(onTapeStarted()));
}
void MainWindow::createTrainProgram(QList<trainrow> rows)
{
if(treadmill->trainProgram) delete treadmill->trainProgram;
treadmill->trainProgram = new trainprogram(rows);
if(rows.length() == 0)
addEmptyRow();
trainProgramSignals();
}
void MainWindow::on_tableWidget_currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous)
{
Q_UNUSED(current);
Q_UNUSED(previous);
}
void MainWindow::on_save_clicked()
{
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
"train.xml",
tr("Train Program (*.xml)"));
if(!fileName.isEmpty() && treadmill->trainProgram)
treadmill->trainProgram->save(fileName);
}
void MainWindow::on_load_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
"train.xml",
tr("Train Program (*.xml *.gpx)"));
if(!fileName.isEmpty())
{
if(fileName.endsWith("xml"))
{
if(treadmill->trainProgram)
delete treadmill->trainProgram;
treadmill->trainProgram = trainprogram::load(fileName);
}
else if(fileName.endsWith("gpx"))
{
if(treadmill->trainProgram)
delete treadmill->trainProgram;
gpx g;
QList<trainrow> list;
foreach(gpx_altitude_point_for_treadmill p, g.open(fileName))
{
trainrow r;
r.speed = p.speed;
r.duration = QTime(0,0,0,0);
r.duration = r.duration.addSecs(p.seconds);
r.inclination = p.inclination;
r.forcespeed = true;
list.append(r);
}
treadmill->trainProgram = new trainprogram(list);
}
else
{
return;
}
int countRow = 0;
foreach(trainrow row, treadmill->trainProgram->rows)
{
if(ui->tableWidget->rowCount() <= countRow)
addEmptyRow();
QTableWidgetItem* i;
editing = true;
i = ui->tableWidget->takeItem(countRow, 0);
i->setText(row.duration.toString("hh:mm:ss"));
ui->tableWidget->setItem(countRow, 0, i);
i = ui->tableWidget->takeItem(countRow, 1);
i->setText(QString::number(row.speed));
ui->tableWidget->setItem(countRow, 1, i);
i = ui->tableWidget->takeItem(countRow, 2);
i->setText(QString::number(row.inclination));
ui->tableWidget->setItem(countRow, 2, i);
i = ui->tableWidget->takeItem(countRow, 3);
i->setCheckState(row.forcespeed?Qt::CheckState::Checked:Qt::CheckState::Unchecked);
ui->tableWidget->setItem(countRow, 3, i);
editing = false;
countRow++;
}
trainProgramSignals();
}
}
void MainWindow::on_reset_clicked()
{
if(treadmill->currentSpeed() > 0) return;
int countRow = 0;
foreach(trainrow row, treadmill->trainProgram->rows)
{
QTableWidgetItem* i;
editing = true;
i = ui->tableWidget->takeItem(countRow, 0);
i->setText("00:00:00");
ui->tableWidget->setItem(countRow, 0, i);
i = ui->tableWidget->takeItem(countRow, 1);
i->setText("0");
ui->tableWidget->setItem(countRow, 1, i);
i = ui->tableWidget->takeItem(countRow, 2);
i->setText("0");
ui->tableWidget->setItem(countRow, 2, i);
i = ui->tableWidget->takeItem(countRow, 3);
i->setCheckState(row.forcespeed?Qt::CheckState::Checked:Qt::CheckState::Unchecked);
ui->tableWidget->setItem(countRow, 3, i);
editing = false;
countRow++;
}
createTrainProgram(QList<trainrow>());
}
void MainWindow::on_stop_clicked()
{
treadmill->stop();
}
void MainWindow::on_start_clicked()
{
treadmill->trainProgram->restart();
treadmill->start();
}
void MainWindow::on_groupBox_2_clicked()
{
if(!treadmill->trainProgram)
createTrainProgram(QList<trainrow>());
treadmill->trainProgram->enabled = ui->groupBox_2->isChecked();
}
void MainWindow::on_fanSpeedMinus_clicked()
{
if(treadmill)
treadmill->changeFanSpeed(treadmill->fanSpeed() - 1);
}
void MainWindow::on_fanSpeedPlus_clicked()
{
if(treadmill)
treadmill->changeFanSpeed(treadmill->fanSpeed() + 1);
}

49
src/mainwindow.h Normal file
View File

@@ -0,0 +1,49 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QDialog>
#include <QTimer>
#include <QTime>
#include <QDebug>
#include <QTableWidgetItem>
#include <trainprogram.h>
#include "domyostreadmill.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QDialog
{
Q_OBJECT
public:
explicit MainWindow(domyostreadmill* treadmill);
~MainWindow();
private:
void addEmptyRow();
void createTrainProgram(QList<trainrow> rows);
void trainProgramSignals();
bool editing = false;
Ui::MainWindow *ui;
QTimer *timer;
domyostreadmill* treadmill;
private slots:
void update();
void on_tableWidget_cellChanged(int row, int column);
void on_tableWidget_currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous);
void on_save_clicked();
void on_load_clicked();
void on_reset_clicked();
void on_stop_clicked();
void on_start_clicked();
void on_groupBox_2_clicked();
void on_fanSpeedMinus_clicked();
void on_fanSpeedPlus_clicked();
};
#endif // MAINWINDOW_H

734
src/mainwindow.ui Normal file
View File

@@ -0,0 +1,734 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QDialog" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>669</width>
<height>647</height>
</rect>
</property>
<property name="windowTitle">
<string>qDoymos-Zwift</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBoxConnectionStatus">
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<property name="title">
<string>Connection Status</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_16">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QPushButton" name="connectionToTreadmill">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Treadmill Connection Status</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/icons/bluetooth-icon.png</normaloff>:/icons/icons/bluetooth-icon.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="connectionToZwift">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Zwift Connection Status</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/icons/zwift-on.png</normaloff>:/icons/icons/zwift-on.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>180</height>
</size>
</property>
<property name="title">
<string>Treadmill Status</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Speed (Km/h)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="speed">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Inclination (degrees)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="inclination">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Heart rate (bpm)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="heartrate">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_4">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Watt</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="watt">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_22">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>Odometer (km):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="odometer">
<property name="text">
<string>0.0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Elevation Gain (meters):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="elevationGain">
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>Calories (kcal):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="calories">
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<property name="spacing">
<number>6</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="fanSpeedMinus">
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QPushButton" name="fanButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>52</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/icons/fan.png</normalon>
</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="fanBar">
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="fanSpeedPlus">
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="title">
<string>Train me!</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>130</number>
</attribute>
<column>
<property name="text">
<string>Durantion (s)</string>
</property>
</column>
<column>
<property name="text">
<string>Speed (km/h)</string>
</property>
</column>
<column>
<property name="text">
<string>Inclination (degrees)</string>
</property>
</column>
<column>
<property name="text">
<string>Force Speed</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Total Elapsed Time: </string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="trainProgramElapsedTime">
<property name="text">
<string>00:00:00</string>
</property>
<property name="maxLength">
<number>8</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Current Row Elapsed Time:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="trainProgramCurrentRowElapsedTime">
<property name="text">
<string>00:00:00</string>
</property>
<property name="maxLength">
<number>8</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Program Duration:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="trainProgramDuration">
<property name="text">
<string>00:00:00</string>
</property>
<property name="maxLength">
<number>8</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Total Distance (km):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="trainProgramTotalDistance">
<property name="text">
<string>0.0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Player Weight (kg):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="weight">
<property name="text">
<string>70.0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="reset">
<property name="text">
<string>&amp;Reset</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="load">
<property name="text">
<string>&amp;Load</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="save">
<property name="text">
<string>&amp;Save</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QPushButton" name="start">
<property name="font">
<font>
<pointsize>9</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stop">
<property name="font">
<font>
<pointsize>9</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>S&amp;top</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="icons.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -1,5 +1,4 @@
QT -= gui
QT += bluetooth
QT += bluetooth widgets xml positioning
CONFIG += c++11 console
CONFIG -= app_bundle
@@ -17,7 +16,11 @@ DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
domyostreadmill.cpp \
gpx.cpp \
main.cpp \
treadmill.cpp \
mainwindow.cpp \
trainprogram.cpp \
virtualtreadmill.cpp
# Default rules for deployment.
@@ -27,4 +30,14 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin
HEADERS += \
domyostreadmill.h \
gpx.h \
treadmill.h \
mainwindow.h \
trainprogram.h \
virtualtreadmill.h
FORMS += \
mainwindow.ui
RESOURCES += \
icons.qrc

165
src/trainprogram.cpp Normal file
View File

@@ -0,0 +1,165 @@
#include "trainprogram.h"
#include <QFile>
#include <QtXml/QtXml>
trainprogram::trainprogram(QList<trainrow> rows)
{
this->rows = rows;
}
void trainprogram::scheduler(int tick)
{
Q_ASSERT(tick);
ticks++;
elapsed = ticks / (1000 / tick);
ticksCurrentRow++;
elapsedCurrentRow = ticksCurrentRow / (1000 / tick);
if(rows.count() == 0 || started == false || enabled == false) return;
// entry point
if(ticks == 1 && currentStep == 0)
{
if(rows[0].forcespeed && rows[0].speed)
{
qDebug() << "trainprogram change speed" + QString::number(rows[0].speed);
emit changeSpeedAndInclination(rows[0].speed, rows[0].inclination);
}
else
{
qDebug() << "trainprogram change inclination" + QString::number(rows[0].inclination);
emit changeInclination(rows[0].inclination);
}
}
uint32_t currentRowLen = rows[currentStep].duration.second() +
(rows[currentStep].duration.minute() * 60) +
(rows[currentStep].duration.hour() * 3600);
uint32_t nextRowLen = 0;
if(rows.count() > currentStep + 1)
nextRowLen = rows[currentStep + 1].duration.second() +
(rows[currentStep + 1].duration.minute() * 60) +
(rows[currentStep + 1].duration.hour() * 3600);
qDebug() << "trainprogram elapsed current row" + QString::number(elapsedCurrentRow) + "current row len" + QString::number(currentRowLen);
if(elapsedCurrentRow >= currentRowLen && currentRowLen)
{
if(nextRowLen)
{
currentStep++;
ticksCurrentRow = 0;
elapsedCurrentRow = 0;
if(rows[currentStep].forcespeed && rows[currentStep].speed)
{
qDebug() << "trainprogram change speed" + QString::number(rows[currentStep].speed);
emit changeSpeedAndInclination(rows[currentStep].speed, rows[currentStep].inclination);
}
qDebug() << "trainprogram change inclination" + QString::number(rows[currentStep].inclination);
emit changeInclination(rows[currentStep].inclination);
}
else
{
qDebug() << "trainprogram ends!";
started = false;
emit stop();
}
}
}
void trainprogram::onTapeStarted()
{
started = true;
}
void trainprogram::restart()
{
ticks = 0;
ticksCurrentRow = 0;
elapsed = 0;
elapsedCurrentRow = 0;
currentStep = 0;
started = true;
}
void trainprogram::save(QString filename)
{
QFile output(filename);
output.open(QIODevice::WriteOnly);
QXmlStreamWriter stream(&output);
stream.setAutoFormatting(true);
stream.writeStartDocument();
stream.writeStartElement("rows");
foreach (trainrow row, rows) {
stream.writeStartElement("row");
stream.writeAttribute("duration", row.duration.toString());
stream.writeAttribute("speed", QString::number(row.speed));
stream.writeAttribute("inclination", QString::number(row.inclination));
stream.writeAttribute("forcespeed", row.forcespeed?"1":"0");
stream.writeEndElement();
}
stream.writeEndElement();
stream.writeEndDocument();
}
trainprogram* trainprogram::load(QString filename)
{
QList<trainrow> list;
QFile input(filename);
input.open(QIODevice::ReadOnly);
QXmlStreamReader stream(&input);
while(!stream.atEnd())
{
stream.readNext();
trainrow row;
QXmlStreamAttributes atts = stream.attributes();
if(atts.length())
{
row.duration = QTime::fromString(atts.value("duration").toString(), "hh:mm:ss");
row.speed = atts.value("speed").toDouble();
row.inclination = atts.value("inclination").toDouble();
row.forcespeed = atts.value("forcespeed").toInt()?true:false ;
list.append(row);
}
}
trainprogram *tr = new trainprogram(list);
return tr;
}
QTime trainprogram::totalElapsedTime()
{
return QTime(0,0,elapsed);
}
QTime trainprogram::currentRowElapsedTime()
{
return QTime(0,0,elapsedCurrentRow);
}
QTime trainprogram::duration()
{
QTime total(0,0,0,0);
foreach (trainrow row, rows) {
total = total.addSecs((row.duration.hour() * 3600) + (row.duration.minute() * 60) + row.duration.second());
}
return total;
}
double trainprogram::totalDistance()
{
double distance = 0;
foreach (trainrow row, rows) {
if(row.duration.hour() || row.duration.minute() || row.duration.second())
{
if(!row.forcespeed)
{
return -1;
}
distance += ((row.duration.hour() * 3600) + (row.duration.minute() * 60) + row.duration.second()) * (row.speed / 3600);
}
}
return distance;
}

53
src/trainprogram.h Normal file
View File

@@ -0,0 +1,53 @@
#ifndef TRAINPROGRAM_H
#define TRAINPROGRAM_H
#include <QTime>
#include <QObject>
class trainrow
{
public:
QTime duration;
double speed;
double inclination;
bool forcespeed;
};
class trainprogram: public QObject
{
Q_OBJECT
public:
trainprogram(QList<trainrow>);
void save(QString filename);
static trainprogram* load(QString filename);
QTime totalElapsedTime();
QTime currentRowElapsedTime();
QTime duration();
double totalDistance();
QList<trainrow> rows;
uint32_t elapsed = 0;
bool enabled = true;
void restart();
void scheduler(int tick);
public slots:
void onTapeStarted();
signals:
void start();
void stop();
void changeSpeed(double speed);
void changeInclination(double inclination);
void changeSpeedAndInclination(double speed, double inclination);
private:
bool started = false;
uint32_t ticks = 0;
uint16_t currentStep = 0;
uint32_t ticksCurrentRow = 0;
uint32_t elapsedCurrentRow = 0;
};
#endif // TRAINPROGRAM_H

19
src/treadmill.cpp Normal file
View File

@@ -0,0 +1,19 @@
#include "treadmill.h"
treadmill::treadmill()
{
}
void treadmill::start(){ requestStart = 1; }
void treadmill::stop(){ requestStop = 1; }
void treadmill::changeSpeed(double speed){ requestSpeed = speed;}
void treadmill::changeInclination(double inclination){ requestInclination = inclination; }
void treadmill::changeSpeedAndInclination(double speed, double inclination){ requestSpeed = speed; requestInclination = inclination;}
unsigned char treadmill::currentHeart(){ return Heart; }
double treadmill::currentSpeed(){ return Speed; }
double treadmill::currentInclination(){ return Inclination; }
double treadmill::odometer(){ return Distance; }
double treadmill::elevationGain(){ return elevationAcc; }
double treadmill::calories(){ return KCal; }
uint8_t treadmill::fanSpeed() { return FanSpeed; };

48
src/treadmill.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef TREADMILL_H
#define TREADMILL_H
#include <QObject>
#include "trainprogram.h"
class treadmill:public QObject
{
Q_OBJECT
public:
treadmill();
virtual unsigned char currentHeart();
virtual double currentSpeed();
virtual double currentInclination();
trainprogram* trainProgram = 0;
virtual double odometer();
virtual double elevationGain();
virtual double calories();
virtual uint8_t fanSpeed();
public slots:
virtual void start();
virtual void stop();
virtual void changeSpeed(double speed);
virtual void changeInclination(double inclination);
virtual void changeSpeedAndInclination(double speed, double inclination);
signals:
void tapeStarted();
protected:
double elevationAcc = 0;
double elapsed = 0;
double Speed = 0;
double Inclination = 0;
double KCal = 0;
double Distance = 0;
uint8_t FanSpeed = 0;
uint8_t Heart = 0;
double requestSpeed = -1;
double requestInclination = -1;
int8_t requestStart = -1;
int8_t requestStop = -1;
int8_t requestIncreaseFan = -1;
int8_t requestDecreaseFan = -1;
};
#endif // TREADMILL_H

View File

@@ -1,16 +1,10 @@
#include "virtualtreadmill.h"
#include <QtMath>
volatile double currentSpeed = 0;
volatile double currentIncline = 0;
volatile uint8_t currentHeart = 0;
volatile double requestSpeed = -1;
volatile double requestIncline = -1;
volatile int8_t requestStart = -1;
volatile int8_t requestStop = -1;
virtualtreadmill::virtualtreadmill()
virtualtreadmill::virtualtreadmill(treadmill* t)
{
treadMill = t;
//! [Advertising Data]
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
@@ -109,7 +103,8 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
b = newValue.at(2);
uint16_t uspeed = a + (((uint16_t)b) << 8);
requestSpeed = (double)uspeed / 100.0;
double requestSpeed = (double)uspeed / 100.0;
treadMill->changeSpeed(requestSpeed);
qDebug() << "new requested speed" << requestSpeed;
}
else if ((char)newValue.at(0)== 0x03) // Set Target Inclination
@@ -118,19 +113,20 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
b = newValue.at(2);
int16_t sincline = a + (((int16_t)b) << 8);
requestIncline = (double)sincline / 10.0;
double requestIncline = (double)sincline / 10.0;
if(requestIncline < 0)
requestIncline = 0;
treadMill->changeInclination(requestIncline);
qDebug() << "new requested incline" << requestIncline;
}
else if ((char)newValue.at(0)== 0x07) // Start request
{
requestStart = 1;
treadMill->start();
qDebug() << "request to start";
}
else if ((char)newValue.at(0)== 0x08) // Stop request
{
requestStop = 1;
treadMill->stop();
qDebug() << "request to stop";
}
break;
@@ -151,19 +147,19 @@ void virtualtreadmill::treadmillProvider()
value.append(0x08); // Inclination avaiable
value.append((char)0x00);
uint16_t normalizeSpeed = (uint16_t)qRound(currentSpeed * 100);
uint16_t normalizeSpeed = (uint16_t)qRound(treadMill->currentSpeed() * 100);
char a = (normalizeSpeed >> 8) & 0XFF;
char b = normalizeSpeed & 0XFF;
QByteArray speedBytes;
speedBytes.append(b);
speedBytes.append(a);
uint16_t normalizeIncline = (uint32_t)qRound(currentIncline * 10);
uint16_t normalizeIncline = (uint32_t)qRound(treadMill->currentInclination() * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
QByteArray inclineBytes;
inclineBytes.append(b);
inclineBytes.append(a);
double ramp = qRadiansToDegrees(qAtan(currentIncline/100));
double ramp = qRadiansToDegrees(qAtan(treadMill->currentInclination()/100));
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
b = normalizeRamp & 0XFF;
@@ -189,27 +185,33 @@ void virtualtreadmill::treadmillProvider()
QByteArray valueHR;
valueHR.append(char(0)); // Flags that specify the format of the value.
valueHR.append(char(currentHeart)); // Actual value.
valueHR.append(char(treadMill->currentHeart())); // Actual value.
QLowEnergyCharacteristic characteristicHR
= serviceHR->characteristic(QBluetoothUuid::HeartRateMeasurement);
Q_ASSERT(characteristicHR.isValid());
serviceHR->writeCharacteristic(characteristicHR, valueHR); // Potentially causes notification.
}
uint16_t virtualtreadmill::watts()
uint16_t virtualtreadmill::watts(double weight)
{
// calc Watts ref. https://alancouzens.com/blog/Run_Power.html
uint16_t watts=0;
if(currentSpeed > 0)
if(treadMill->currentSpeed() > 0)
{
double weight=75.0; // TODO: config need
double pace=60/currentSpeed;
double pace=60/treadMill->currentSpeed();
double VO2R=210.0/pace;
double VO2A=(VO2R*weight)/1000.0;
double hwatts=75*VO2A;
double vwatts=((9.8*weight) * (currentIncline/100));
double vwatts=((9.8*weight) * (treadMill->currentInclination()/100));
watts=hwatts+vwatts;
}
return watts;
}
bool virtualtreadmill::connected()
{
if(!leController)
return false;
return leController->state() == QLowEnergyController::ConnectedState;
}

View File

@@ -22,11 +22,15 @@
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include "treadmill.h"
class virtualtreadmill: QObject
{
Q_OBJECT
public:
virtualtreadmill();
virtualtreadmill(treadmill* t);
uint16_t watts(double weight=75.0);
bool connected();
private:
QLowEnergyController* leController;
@@ -34,8 +38,8 @@ private:
QLowEnergyService* serviceHR;
QLowEnergyAdvertisingData advertisingData;
QLowEnergyServiceData serviceData;
QTimer treadmillTimer;
uint16_t watts();
QTimer treadmillTimer;
treadmill* treadMill;
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<rows>
<row duration="00:02:00" speed="5" inclination="0" forcespeed="1"/>
<row duration="00:03:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:05:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7.7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7.7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7.7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:01:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:02:00" speed="7" inclination="1" forcespeed="1"/>
<row duration="00:06:00" speed="6" inclination="0" forcespeed="1"/>
<row duration="00:01:00" speed="5" inclination="0" forcespeed="1"/>
</rows>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<rows>
<row duration="00:01:00" speed="9.85447" inclination="-1.82658" forcespeed="1"/>
<row duration="00:01:00" speed="10.2993" inclination="-2.91281" forcespeed="1"/>
<row duration="00:01:00" speed="10.2958" inclination="-2.15623" forcespeed="1"/>
<row duration="00:01:00" speed="10.2988" inclination="-0.174779" forcespeed="1"/>
<row duration="00:01:00" speed="10.3375" inclination="-2.49576" forcespeed="1"/>
<row duration="00:01:00" speed="11.0169" inclination="-1.41601" forcespeed="1"/>
<row duration="00:01:00" speed="11.8215" inclination="-2.0302" forcespeed="1"/>
<row duration="00:01:00" speed="11.0147" inclination="-1.19839" forcespeed="1"/>
<row duration="00:01:00" speed="3.66404" inclination="3.11131" forcespeed="1"/>
<row duration="00:01:00" speed="9.80634" inclination="5.75139" forcespeed="1"/>
<row duration="00:01:00" speed="8.71754" inclination="9.91104" forcespeed="1"/>
<row duration="00:01:00" speed="8.99724" inclination="9.40288" forcespeed="1"/>
<row duration="00:01:00" speed="10.11" inclination="4.51039" forcespeed="1"/>
<row duration="00:01:00" speed="10.1726" inclination="4.01078" forcespeed="1"/>
<row duration="00:01:00" speed="9.96776" inclination="6.01941" forcespeed="1"/>
<row duration="00:01:00" speed="9.21979" inclination="9.24099" forcespeed="1"/>
<row duration="00:01:00" speed="9.00303" inclination="9.39684" forcespeed="1"/>
<row duration="00:01:00" speed="7.59236" inclination="7.6656" forcespeed="1"/>
<row duration="00:01:00" speed="9.24165" inclination="5.12895" forcespeed="1"/>
<row duration="00:01:00" speed="6.95364" inclination="9.23258" forcespeed="1"/>
<row duration="00:01:00" speed="10.4115" inclination="6.45437" forcespeed="1"/>
<row duration="00:01:00" speed="9.95344" inclination="5.9678" forcespeed="1"/>
<row duration="00:01:00" speed="10.3448" inclination="2.72599" forcespeed="1"/>
<row duration="00:01:00" speed="10.485" inclination="0.286123" forcespeed="1"/>
<row duration="00:01:00" speed="9.42228" inclination="-5.4127" forcespeed="1"/>
<row duration="00:01:00" speed="11.5566" inclination="-6.5417" forcespeed="1"/>
<row duration="00:01:00" speed="10.3381" inclination="-3.48228" forcespeed="1"/>
<row duration="00:01:00" speed="12.2822" inclination="-2.34486" forcespeed="1"/>
<row duration="00:01:00" speed="10.4139" inclination="4.72445" forcespeed="1"/>
<row duration="00:01:00" speed="8.69461" inclination="-5.38266" forcespeed="1"/>
<row duration="00:01:00" speed="10.1889" inclination="-3.06215" forcespeed="1"/>
<row duration="00:01:00" speed="9.98059" inclination="2.46477" forcespeed="1"/>
<row duration="00:01:00" speed="10.9802" inclination="3.87972" forcespeed="1"/>
<row duration="00:01:00" speed="9.05151" inclination="-0.0662914" forcespeed="1"/>
<row duration="00:01:00" speed="3.99165" inclination="-19.9917" forcespeed="1"/>
<row duration="00:01:00" speed="8.7631" inclination="-12.0505" forcespeed="1"/>
<row duration="00:01:00" speed="6.64707" inclination="-16.2478" forcespeed="1"/>
<row duration="00:01:00" speed="6.51384" inclination="-14.8299" forcespeed="1"/>
<row duration="00:01:00" speed="8.57592" inclination="-10.8443" forcespeed="1"/>
<row duration="00:01:00" speed="8.401" inclination="-8.92751" forcespeed="1"/>
<row duration="00:01:00" speed="11.2526" inclination="-2.45276" forcespeed="1"/>
<row duration="00:01:00" speed="11.6882" inclination="-1.12934" forcespeed="1"/>
<row duration="00:01:00" speed="13.9318" inclination="-0.387599" forcespeed="1"/>
<row duration="00:01:00" speed="10.6752" inclination="-0.112417" forcespeed="1"/>
<row duration="00:01:00" speed="10.2653" inclination="-2.33797" forcespeed="1"/>
<row duration="00:01:00" speed="10.7095" inclination="-2.18497" forcespeed="1"/>
<row duration="00:01:00" speed="9.3889" inclination="2.10888" forcespeed="1"/>
<row duration="00:01:00" speed="11.1298" inclination="-3.01893" forcespeed="1"/>
<row duration="00:01:00" speed="11.9722" inclination="-1.90441" forcespeed="1"/>
<row duration="00:01:00" speed="12.0696" inclination="-1.73991" forcespeed="1"/>
<row duration="00:01:00" speed="12.9631" inclination="-1.80512" forcespeed="1"/>
<row duration="00:01:00" speed="12.336" inclination="-1.89688" forcespeed="1"/>
<row duration="00:01:00" speed="11.5617" inclination="-0.467066" forcespeed="1"/>
<row duration="00:01:00" speed="11.3423" inclination="-1.63987" forcespeed="1"/>
<row duration="00:01:00" speed="11.5684" inclination="0" forcespeed="1"/>
<row duration="00:01:00" speed="10.971" inclination="0.656273" forcespeed="1"/>
<row duration="00:01:00" speed="11.2002" inclination="-0.642841" forcespeed="1"/>
<row duration="00:01:00" speed="11.1976" inclination="-0.910918" forcespeed="1"/>
<row duration="00:01:00" speed="10.4373" inclination="-0.977264" forcespeed="1"/>
<row duration="00:01:00" speed="10.2949" inclination="-0.582811" forcespeed="1"/>
<row duration="00:01:00" speed="10.9175" inclination="0.0549612" forcespeed="1"/>
<row duration="00:01:00" speed="9.11082" inclination="2.30495" forcespeed="1"/>
<row duration="00:01:00" speed="10.4257" inclination="1.20855" forcespeed="1"/>
<row duration="00:01:00" speed="10.9692" inclination="1.42217" forcespeed="1"/>
<row duration="00:01:00" speed="10.5789" inclination="1.24777" forcespeed="1"/>
<row duration="00:01:00" speed="10.5046" inclination="0.971002" forcespeed="1"/>
<row duration="00:01:00" speed="10.5973" inclination="0.339712" forcespeed="1"/>
<row duration="00:01:00" speed="10.4905" inclination="1.14389" forcespeed="1"/>
<row duration="00:01:00" speed="11.7968" inclination="1.5767" forcespeed="1"/>
<row duration="00:01:00" speed="10.9239" inclination="1.70268" forcespeed="1"/>
<row duration="00:01:00" speed="12.6751" inclination="2.46152" forcespeed="1"/>
</rows>