mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa53956a35 | ||
|
|
cd1c10a090 | ||
|
|
c21e337bdd | ||
|
|
1d23ac4b81 | ||
|
|
bbeaa5ec95 | ||
|
|
9f6a4de4ac | ||
|
|
ee1c3e0118 | ||
|
|
81ac8909c8 | ||
|
|
3a45935617 | ||
|
|
c89c381177 | ||
|
|
ea57069f33 | ||
|
|
bf40c460a5 | ||
|
|
338b19f664 | ||
|
|
9f9000427f | ||
|
|
92cd9baea3 | ||
|
|
e21ad70ea9 | ||
|
|
d7ac459a3d | ||
|
|
d58db4100f | ||
|
|
3c7cb254e6 | ||
|
|
259b53e8e0 | ||
|
|
b73092bd8f | ||
|
|
787b9aa2c2 | ||
|
|
73a0bd7c65 | ||
|
|
d068526e55 | ||
|
|
8945063f30 | ||
|
|
4625bccad3 | ||
|
|
5c723375d7 | ||
|
|
1212bc83f8 | ||
|
|
efc9788c89 | ||
|
|
42c43158e6 | ||
|
|
ff354fd20d | ||
|
|
d21f92727e | ||
|
|
632991e58e | ||
|
|
1d6c46a32b | ||
|
|
3ba0219ce4 | ||
|
|
21ec2a890e | ||
|
|
b434d1f1e6 | ||
|
|
373eb3fbf9 | ||
|
|
acfdff7b5c | ||
|
|
38a41451f3 | ||
|
|
c2c5b7746f | ||
|
|
4944f6d48d | ||
|
|
33a478b1ae | ||
|
|
99fd62c8ec | ||
|
|
4712d5780a | ||
|
|
eff5d1d2f3 | ||
|
|
22b6de14f1 | ||
|
|
76d3139d79 | ||
|
|
8fb8ed6f44 | ||
|
|
483cc45643 | ||
|
|
3d152b903a | ||
|
|
1974dd26ee | ||
|
|
78f81261d9 | ||
|
|
d542aa819b | ||
|
|
5774e725a7 |
29
README.md
29
README.md
@@ -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>
|
||||
|
||||

|
||||
|
||||
### Features
|
||||
|
||||
1. Domyos compatible
|
||||
2. Zwift compatible
|
||||
3. Create, load and save train programs
|
||||
4. Measure distance, elevation gain and watts
|
||||
|
||||

|
||||
|
||||
### 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
BIN
docs/ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -1,6 +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 };
|
||||
@@ -11,17 +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, 0xcb, 0x03, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x02,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00 };
|
||||
uint8_t initDataStart6[] = { 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc1 };
|
||||
#endif
|
||||
|
||||
// main startup sequence
|
||||
uint8_t initDataStart[] = { 0xf0, 0xa3, 0x93 };
|
||||
uint8_t initDataStart2[] = { 0xf0, 0xa4, 0x94 };
|
||||
@@ -57,53 +50,84 @@ 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;
|
||||
|
||||
domyostreadmill::domyostreadmill()
|
||||
domyostreadmill::domyostreadmill(bool logs)
|
||||
{
|
||||
QTimer* refresh = new QTimer(this);
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
|
||||
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);
|
||||
}
|
||||
|
||||
void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestIncline, uint16_t elapsed)
|
||||
void domyostreadmill::debug(QString text)
|
||||
{
|
||||
QString debug = QDateTime::currentDateTime().toString() + text + '\n';
|
||||
if(debugCommsLog)
|
||||
{
|
||||
debugCommsLog->write(debug.toLocal8Bit());
|
||||
qDebug() << debug;
|
||||
}
|
||||
}
|
||||
|
||||
void domyostreadmill::writeCharacteristic(uint8_t* data, uint8_t data_len, QString info, bool disable_log)
|
||||
{
|
||||
QEventLoop loop;
|
||||
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic,QByteArray)),
|
||||
&loop, SLOT(quit()));
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)data, data_len));
|
||||
|
||||
if(!disable_log)
|
||||
debug(" >> " + QByteArray((const char*)data, data_len).toHex(' ') + " // " + info);
|
||||
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
void domyostreadmill::updateDisplay(uint16_t elapsed)
|
||||
{
|
||||
uint8_t writeIncline[] = {0xf0, 0xcb, 0x03, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x02,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00,
|
||||
(uint8_t)(requestSpeed * 10), 0x01, 0xff, 0xff, 0xff, 0xff, 0x00};
|
||||
0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x00};
|
||||
|
||||
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[16] = (uint8_t)(requestIncline * 10);
|
||||
writeIncline[12] = currentHeart();
|
||||
|
||||
for(uint8_t i=0; i<sizeof(writeIncline)-1; i++)
|
||||
{
|
||||
@@ -113,65 +137,128 @@ void domyostreadmill::forceSpeedOrIncline(double requestSpeed, double requestInc
|
||||
|
||||
//qDebug() << "writeIncline crc" << QString::number(writeIncline[26], 16);
|
||||
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)writeIncline, sizeof(writeIncline)));
|
||||
|
||||
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, 0x00};
|
||||
|
||||
writeIncline[4] = ((uint16_t)(requestSpeed*10) >> 8) & 0xFF;
|
||||
writeIncline[5] = ((uint16_t)(requestSpeed*10) & 0xFF);
|
||||
|
||||
writeIncline[13] = ((uint16_t)(requestIncline*10) >> 8) & 0xFF;
|
||||
writeIncline[14] = ((uint16_t)(requestIncline*10) & 0xFF);
|
||||
|
||||
for(uint8_t i=0; i<sizeof(writeIncline)-1; i++)
|
||||
{
|
||||
//qDebug() << QString::number(writeIncline[i], 16);
|
||||
writeIncline[22] += writeIncline[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
//qDebug() << "writeIncline crc" << QString::number(writeIncline[26], 16);
|
||||
|
||||
|
||||
writeCharacteristic(writeIncline, 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
|
||||
writeCharacteristic(&writeIncline[20], sizeof (writeIncline) - 20, "forceSpeedOrIncline speed=" + QString::number(requestSpeed) + " incline=" + QString::number(requestIncline));
|
||||
}
|
||||
|
||||
|
||||
void domyostreadmill::update()
|
||||
{
|
||||
static uint8_t first = 0;
|
||||
static virtualtreadmill* v;
|
||||
static uint32_t counter = 0;
|
||||
Q_UNUSED(v);
|
||||
//qDebug() << treadmill.isValid() << m_control->state() << gattCommunicationChannelService << gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone;
|
||||
if(treadmill.isValid() &&
|
||||
(m_control->state() == QLowEnergyController::ConnectedState || m_control->state() == QLowEnergyController::DiscoveredState) &&
|
||||
|
||||
if(initRequest)
|
||||
{
|
||||
initRequest = false;
|
||||
btinit(false);
|
||||
}
|
||||
else if(restart)
|
||||
{
|
||||
startDiscover();
|
||||
}
|
||||
else if(bttreadmill.isValid() &&
|
||||
m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService &&
|
||||
gattWriteCharacteristic.isValid() &&
|
||||
gattNotifyCharacteristic.isValid() &&
|
||||
initDone)
|
||||
{
|
||||
counter++;
|
||||
if(!first)
|
||||
if(currentSpeed() > 0.0)
|
||||
{
|
||||
qDebug() << "creating virtual treadmill interface...";
|
||||
v = new virtualtreadmill();
|
||||
elapsed += ((double)refresh->interval() / 1000.0);
|
||||
if(trainProgram)
|
||||
trainProgram->scheduler(refresh->interval());
|
||||
}
|
||||
first = 1;
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)noOpData, sizeof(noOpData)));
|
||||
writeCharacteristic(noOpData, sizeof(noOpData), "noOp", true);
|
||||
|
||||
// byte 3 - 4 = elapsed time
|
||||
// byte 17 = inclination
|
||||
|
||||
if(requestSpeed != -1)
|
||||
{
|
||||
qDebug() << "writing speed" << requestSpeed;
|
||||
forceSpeedOrIncline(requestSpeed, currentIncline, counter/5);
|
||||
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)
|
||||
{
|
||||
qDebug() << "writing incline" << requestIncline;
|
||||
forceSpeedOrIncline(currentSpeed, requestIncline, counter/5);
|
||||
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)
|
||||
{
|
||||
qDebug() << "starting...";
|
||||
btinit();
|
||||
debug("starting...");
|
||||
btinit(true);
|
||||
requestStart = -1;
|
||||
emit tapeStarted();
|
||||
}
|
||||
if(requestStop != -1)
|
||||
{
|
||||
qDebug() << "stopping...";
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataF0C800B8, sizeof(initDataF0C800B8)));
|
||||
debug("stopping...");
|
||||
writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
|
||||
requestStop = -1;
|
||||
}
|
||||
if(requestIncreaseFan != -1)
|
||||
{
|
||||
debug("increasing fan speed TODO...");
|
||||
requestIncreaseFan = -1;
|
||||
}
|
||||
else if(requestDecreaseFan != -1)
|
||||
{
|
||||
debug("decreasing fan speed TODO...");
|
||||
requestDecreaseFan = -1;
|
||||
}
|
||||
|
||||
elevationAcc += (currentSpeed() / 3600.0) * 1000 * (currentInclination() / 100) * (refresh->interval() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
void domyostreadmill::serviceDiscovered(const QBluetoothUuid &gatt)
|
||||
{
|
||||
qDebug() << "serviceDiscovered" << gatt;
|
||||
debug("serviceDiscovered " + gatt.toString());
|
||||
}
|
||||
|
||||
static QByteArray lastPacket;
|
||||
@@ -180,6 +267,8 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
|
||||
//qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
|
||||
Q_UNUSED(characteristic);
|
||||
|
||||
debug(" << " + newValue.toHex(' '));
|
||||
|
||||
if (lastPacket.length() && lastPacket == newValue)
|
||||
return;
|
||||
|
||||
@@ -189,35 +278,48 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
|
||||
|
||||
if (newValue.at(22) == 0x06)
|
||||
{
|
||||
qDebug() << "START PRESSED!";
|
||||
debug("start button pressed!");
|
||||
requestStart = 1;
|
||||
}
|
||||
else if (newValue.at(22) == 0x07)
|
||||
{
|
||||
qDebug() << "STOP PRESSED!";
|
||||
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);
|
||||
//var isStartPressed = GetIsStartPressedFromPacket(currentPacket);
|
||||
//var isStopPressed = GetIsStopPressedFromPacket(currentPacket);
|
||||
double kcal = GetKcalFromPacket(newValue);
|
||||
double distance = GetDistanceFromPacket(newValue);
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine(args.CharacteristicValue.ToArray().HexDump());
|
||||
#endif
|
||||
Heart = newValue.at(18);
|
||||
|
||||
currentHeart = newValue.at(18);
|
||||
debug("Current speed: " + QString::number(speed));
|
||||
debug("Current incline: " + QString::number(incline));
|
||||
debug("Current heart: " + QString::number(Heart));
|
||||
debug("Current KCal: " + QString::number(kcal));
|
||||
debug("Current Distance: " + QString::number(distance));
|
||||
|
||||
qDebug() << "Current speed: " << speed;
|
||||
qDebug() << "Current incline: " << incline;
|
||||
qDebug() << "Current heart:" << currentHeart;
|
||||
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)
|
||||
@@ -227,99 +329,159 @@ 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);
|
||||
qDebug() << convertedData;
|
||||
double data = ((double)convertedData - 1000.0f) / 10.0f;
|
||||
if (data < 0) return 0;
|
||||
return data;
|
||||
}
|
||||
|
||||
void domyostreadmill::btinit()
|
||||
void domyostreadmill::btinit(bool startTape)
|
||||
{
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initData1, sizeof(initData1)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initData2, sizeof(initData2)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart, sizeof(initDataStart)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart2, sizeof(initDataStart2)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart3, sizeof(initDataStart3)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart4, sizeof(initDataStart4)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart5, sizeof(initDataStart5)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart6, sizeof(initDataStart6)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart7, sizeof(initDataStart7)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart8, sizeof(initDataStart8)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart9, sizeof(initDataStart9)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart10, sizeof(initDataStart10)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart11, sizeof(initDataStart11)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart12, sizeof(initDataStart12)));
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray::fromRawData((const char*)initDataStart13, sizeof(initDataStart13)));
|
||||
writeCharacteristic(initData1, sizeof(initData1), "init");
|
||||
writeCharacteristic(initData2, sizeof(initData2), "init");
|
||||
writeCharacteristic(initDataStart, sizeof(initDataStart), "init");
|
||||
writeCharacteristic(initDataStart2, sizeof(initDataStart2), "init");
|
||||
writeCharacteristic(initDataStart3, sizeof(initDataStart3), "init");
|
||||
writeCharacteristic(initDataStart4, sizeof(initDataStart4), "init");
|
||||
writeCharacteristic(initDataStart5, sizeof(initDataStart5), "init");
|
||||
writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init");
|
||||
writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init");
|
||||
writeCharacteristic(initDataStart8, sizeof(initDataStart8), "init");
|
||||
writeCharacteristic(initDataStart9, sizeof(initDataStart9), "init");
|
||||
writeCharacteristic(initDataStart10, sizeof(initDataStart10), "init");
|
||||
if(startTape)
|
||||
{
|
||||
writeCharacteristic(initDataStart11, sizeof(initDataStart11), "init");
|
||||
writeCharacteristic(initDataStart12, sizeof(initDataStart12), "init");
|
||||
writeCharacteristic(initDataStart13, sizeof(initDataStart13), "init");
|
||||
}
|
||||
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
void domyostreadmill::stateChanged(QLowEnergyService::ServiceState state)
|
||||
{
|
||||
qDebug() << "stateChanged" << state;
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
debug("BTLE stateChanged " + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
|
||||
if(state == QLowEnergyService::ServiceDiscovered)
|
||||
{
|
||||
//qDebug() << gattCommunicationChannelService->characteristics();
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotifyCharacteristic.isValid());
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
|
||||
this, SLOT(characteristicChanged(QLowEnergyCharacteristic,QByteArray)));
|
||||
connect(gattCommunicationChannelService, SIGNAL(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)),
|
||||
this, SLOT(characteristicWritten(const QLowEnergyCharacteristic, const QByteArray)));
|
||||
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)),
|
||||
this, SLOT(errorService(QLowEnergyService::ServiceError)));
|
||||
connect(gattCommunicationChannelService, SIGNAL(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)), this,
|
||||
SLOT(descriptorWritten(const QLowEnergyDescriptor, const QByteArray)));
|
||||
|
||||
// ******************************************* virtual treadmill init *************************************
|
||||
static uint8_t first = 0;
|
||||
static virtualtreadmill* v;
|
||||
Q_UNUSED(v);
|
||||
if(!first)
|
||||
{
|
||||
debug("creating virtual treadmill interface...");
|
||||
v = new virtualtreadmill(this);
|
||||
}
|
||||
first = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
// await _gattNotifyCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
|
||||
btinit();
|
||||
|
||||
initDone = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void domyostreadmill::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
|
||||
{
|
||||
debug("descriptorWritten " + descriptor.name() + " " + newValue.toHex(' '));
|
||||
|
||||
initRequest = true;
|
||||
}
|
||||
|
||||
void domyostreadmill::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
|
||||
{
|
||||
Q_UNUSED(characteristic);
|
||||
debug("characteristicWritten " + newValue.toHex(' '));
|
||||
}
|
||||
|
||||
void domyostreadmill::serviceScanDone(void)
|
||||
{
|
||||
qDebug() << "serviceScanDone";
|
||||
debug("serviceScanDone");
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
connect(gattCommunicationChannelService, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(stateChanged(QLowEnergyService::ServiceState)));
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void domyostreadmill::errorService(QLowEnergyService::ServiceError err)
|
||||
{
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
debug("domyostreadmill::errorService" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
|
||||
}
|
||||
|
||||
void domyostreadmill::error(QLowEnergyController::Error err)
|
||||
{
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
debug("domyostreadmill::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString());
|
||||
}
|
||||
|
||||
void domyostreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
|
||||
{
|
||||
qDebug() << "Found new device:" << device.name() << '(' << device.address().toString() << ')';
|
||||
debug("Found new device: " + device.name() + " (" + device.address().toString() + ')');
|
||||
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()),
|
||||
this, SLOT(serviceScanDone()));
|
||||
connect(m_control, SIGNAL(error(QLowEnergyController::Error)),
|
||||
this, SLOT(error(QLowEnergyController::Error)));
|
||||
|
||||
connect(m_control, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
qDebug() << "Cannot connect to remote device.";
|
||||
exit(1);
|
||||
debug("Cannot connect to remote device.");
|
||||
restart = true;
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << "Controller connected. Search services...";
|
||||
debug("Controller connected. Search services...");
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << "LowEnergy controller disconnected";
|
||||
exit(2);
|
||||
debug("LowEnergy controller disconnected");
|
||||
restart = true;
|
||||
});
|
||||
|
||||
// Connect
|
||||
@@ -327,3 +489,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;
|
||||
}
|
||||
|
||||
@@ -25,27 +25,44 @@
|
||||
|
||||
#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();
|
||||
|
||||
private:
|
||||
double GetSpeedFromPacket(QByteArray packet);
|
||||
double GetInclinationFromPacket(QByteArray packet);
|
||||
void forceSpeedOrIncline(double requestSpeed, double requestIncline, uint16_t elapsed);
|
||||
void btinit();
|
||||
double GetKcalFromPacket(QByteArray packet);
|
||||
double GetDistanceFromPacket(QByteArray packet);
|
||||
void forceSpeedOrIncline(double requestSpeed, double requestIncline);
|
||||
void updateDisplay(uint16_t elapsed);
|
||||
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:
|
||||
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // DOMYOSTREADMILL_H
|
||||
|
||||
6
src/icons.qrc
Normal file
6
src/icons.qrc
Normal file
@@ -0,0 +1,6 @@
|
||||
<RCC>
|
||||
<qresource prefix="/icons">
|
||||
<file>icons/bluetooth-icon.png</file>
|
||||
<file>icons/zwift-on.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
BIN
src/icons/bluetooth-icon.png
Normal file
BIN
src/icons/bluetooth-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
src/icons/zwift-on.png
Normal file
BIN
src/icons/zwift-on.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
41
src/main.cpp
41
src/main.cpp
@@ -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();
|
||||
}
|
||||
|
||||
266
src/mainwindow.cpp
Normal file
266
src/mainwindow.cpp
Normal file
@@ -0,0 +1,266 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include <QFileDialog>
|
||||
|
||||
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()));
|
||||
|
||||
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)"));
|
||||
if(!fileName.isEmpty())
|
||||
{
|
||||
if(treadmill->trainProgram)
|
||||
delete treadmill->trainProgram;
|
||||
treadmill->trainProgram = trainprogram::load(fileName);
|
||||
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();
|
||||
}
|
||||
47
src/mainwindow.h
Normal file
47
src/mainwindow.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#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();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
660
src/mainwindow.ui
Normal file
660
src/mainwindow.ui
Normal file
@@ -0,0 +1,660 @@
|
||||
<?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>663</width>
|
||||
<height>509</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>0</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>
|
||||
</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>&Reset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="load">
|
||||
<property name="text">
|
||||
<string>&Load</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="save">
|
||||
<property name="text">
|
||||
<string>&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&top</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="icons.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,5 +1,4 @@
|
||||
QT -= gui
|
||||
QT += bluetooth
|
||||
QT += bluetooth widgets
|
||||
|
||||
CONFIG += c++11 console
|
||||
CONFIG -= app_bundle
|
||||
@@ -18,6 +17,9 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
||||
SOURCES += \
|
||||
domyostreadmill.cpp \
|
||||
main.cpp \
|
||||
treadmill.cpp \
|
||||
mainwindow.cpp \
|
||||
trainprogram.cpp \
|
||||
virtualtreadmill.cpp
|
||||
|
||||
# Default rules for deployment.
|
||||
@@ -27,4 +29,13 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
|
||||
HEADERS += \
|
||||
domyostreadmill.h \
|
||||
treadmill.h \
|
||||
mainwindow.h \
|
||||
trainprogram.h \
|
||||
virtualtreadmill.h
|
||||
|
||||
FORMS += \
|
||||
mainwindow.ui
|
||||
|
||||
RESOURCES += \
|
||||
icons.qrc
|
||||
|
||||
165
src/trainprogram.cpp
Normal file
165
src/trainprogram.cpp
Normal 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
53
src/trainprogram.h
Normal 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
|
||||
18
src/treadmill.cpp
Normal file
18
src/treadmill.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#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; }
|
||||
46
src/treadmill.h
Normal file
46
src/treadmill.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#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();
|
||||
|
||||
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 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
|
||||
@@ -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);
|
||||
@@ -75,6 +69,7 @@ virtualtreadmill::virtualtreadmill()
|
||||
|
||||
//! [Start Advertising]
|
||||
leController = QLowEnergyController::createPeripheral();
|
||||
Q_ASSERT(leController);
|
||||
service = leController->addService(serviceData);
|
||||
serviceHR = leController->addService(serviceDataHR);
|
||||
|
||||
@@ -108,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
|
||||
@@ -117,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;
|
||||
@@ -150,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;
|
||||
@@ -188,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
23
train-programs-examples/calorie-barbara.xml
Normal file
23
train-programs-examples/calorie-barbara.xml
Normal 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>
|
||||
Reference in New Issue
Block a user