Skip to content

Commit c39f672

Browse files
committed
opus run
1 parent 07846d3 commit c39f672

7 files changed

Lines changed: 119 additions & 91 deletions

File tree

src/devices/fakerower/fakerower.cpp

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -156,34 +156,3 @@ void fakerower::changeInclinationRequested(double grade, double percentage) {
156156
}
157157

158158
bool fakerower::connected() { return true; }
159-
160-
void fakerower::proxyVirtualRowerCommand(const QByteArray &value) {
161-
auto *virtualRower = dynamic_cast<virtualrower *>(VirtualDevice());
162-
if (!virtualRower || value.size() < 2 || static_cast<uint8_t>(value.at(0)) != 0xf0) {
163-
return;
164-
}
165-
166-
const uint8_t command = static_cast<uint8_t>(value.at(1));
167-
if (command == 0xA1) {
168-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
169-
QByteArray::fromHex("f0a106000701290704d3"));
170-
} else if (command == 0xA3) {
171-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
172-
QByteArray::fromHex("f0a3022001b6"));
173-
} else if (command == 0xA4) {
174-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
175-
QByteArray::fromHex("f0a4010095"));
176-
} else if (command == 0xA5) {
177-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
178-
QByteArray::fromHex("f0a5010ea4"));
179-
} else if (command == 0xB0 && value.size() > 3 && static_cast<uint8_t>(value.at(3)) == 0x00) {
180-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
181-
QByteArray::fromHex("f0d00100c1"));
182-
} else if (command == 0xB0) {
183-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66")),
184-
QByteArray::fromHex("f0d00101c2"));
185-
} else if (command == 0xA0) {
186-
virtualRower->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
187-
value);
188-
}
189-
}

src/devices/fakerower/fakerower.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ class fakerower : public rower {
3737
public:
3838
fakerower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice);
3939
bool connected() override;
40-
void proxyVirtualRowerCommand(const QByteArray &value);
4140

4241
private:
4342
QTimer *refresh;

src/devices/faketreadmill/faketreadmill.cpp

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -137,34 +137,3 @@ void faketreadmill::changeInclinationRequested(double grade, double percentage)
137137
}
138138

139139
bool faketreadmill::connected() { return true; }
140-
141-
void faketreadmill::proxyVirtualTreadmillCommand(const QByteArray &value) {
142-
auto *virtualTreadmill = dynamic_cast<virtualtreadmill *>(VirtualDevice());
143-
if (!virtualTreadmill || value.size() < 2 || static_cast<uint8_t>(value.at(0)) != 0xf0) {
144-
return;
145-
}
146-
147-
const uint8_t command = static_cast<uint8_t>(value.at(1));
148-
if (command == 0xA1) {
149-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
150-
QByteArray::fromHex("f0a106000701290704d3"));
151-
} else if (command == 0xA3) {
152-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
153-
QByteArray::fromHex("f0a3022001b6"));
154-
} else if (command == 0xA4) {
155-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
156-
QByteArray::fromHex("f0a4010095"));
157-
} else if (command == 0xA5) {
158-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
159-
QByteArray::fromHex("f0a5010ea4"));
160-
} else if (command == 0xB0 && value.size() > 3 && static_cast<uint8_t>(value.at(3)) == 0x00) {
161-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
162-
QByteArray::fromHex("f0d00100c1"));
163-
} else if (command == 0xB0) {
164-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66")),
165-
QByteArray::fromHex("f0d00101c2"));
166-
} else if (command == 0xA0) {
167-
virtualTreadmill->relayEchelonPacket(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")),
168-
value);
169-
}
170-
}

src/devices/faketreadmill/faketreadmill.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ class faketreadmill : public treadmill {
4141
bool connected() override;
4242
double minStepSpeed() override { return 0.1; }
4343
double minStepInclination() override { return 0.1; }
44-
void proxyVirtualTreadmillCommand(const QByteArray &value);
4544

4645
private:
4746
QTimer *refresh;

src/virtualdevices/virtualrower.cpp

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#include "virtualdevices/virtualrower.h"
22
#include "devices/echelonrower/echelonrower.h"
3-
#include "devices/fakerower/fakerower.h"
43
#include "qsettings.h"
54
#include "qzsettings.h"
65
#include "rower.h"
@@ -13,6 +12,21 @@
1312
#include <chrono>
1413
#include <QThread>
1514

15+
namespace {
16+
QByteArray echelonPacket(std::initializer_list<uint8_t> bytes) {
17+
QByteArray value;
18+
uint8_t checksum = 0;
19+
20+
for (uint8_t byte : bytes) {
21+
value.append(static_cast<char>(byte));
22+
checksum = static_cast<uint8_t>(checksum + byte);
23+
}
24+
25+
value.append(static_cast<char>(checksum));
26+
return value;
27+
}
28+
}
29+
1630
// PM5 Concept2 BLE UUIDs
1731
// Base UUID: CE06XXXX-43E5-11E4-916C-0800200C9A66
1832

@@ -446,15 +460,15 @@ void virtualrower::characteristicChanged(const QLowEnergyCharacteristic &charact
446460

447461
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66")) &&
448462
serviceEchelon && newValue.length() > 3 && leController->state() == QLowEnergyController::ConnectedState) {
463+
// When QZ is backed by a real Echelon rower, forward the app payload to the rower and let the
464+
// rower's own notifications be mirrored back to the client (firmware unlock passthrough).
449465
if (auto *realEchelon = dynamic_cast<echelonrower *>(Rower); realEchelon && realEchelon->connected()) {
450466
realEchelon->proxyVirtualRowerCommand(newValue);
451467
return;
452468
}
453-
if (auto *fakeEchelon = dynamic_cast<fakerower *>(Rower); fakeEchelon && fakeEchelon->connected()) {
454-
fakeEchelon->proxyVirtualRowerCommand(newValue);
455-
return;
456-
}
457469

470+
// Otherwise (fake rower / no real Echelon hardware) answer the handshake synthetically, exactly
471+
// like the real Echelon rower does in the HCI snoop traces.
458472
QLowEnergyCharacteristic notify1 =
459473
serviceEchelon->characteristic(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")));
460474
QLowEnergyCharacteristic notify2 =
@@ -463,18 +477,23 @@ void virtualrower::characteristicChanged(const QLowEnergyCharacteristic &charact
463477
if (notify1.isValid()) {
464478
const uint8_t command = static_cast<uint8_t>(newValue.at(1));
465479
if (command == 0xA1) {
466-
writeCharacteristic(serviceEchelon, notify1, QByteArray::fromHex("f0a106000701290704d3"));
480+
// Echelon rower model/info reply (from snoop)
481+
writeCharacteristic(serviceEchelon, notify1, QByteArray::fromHex("f0a106101e0050060520"));
482+
echelonInitDone = true;
467483
} else if (command == 0xA3) {
468484
writeCharacteristic(serviceEchelon, notify1, QByteArray::fromHex("f0a3022001b6"));
469-
} else if (command == 0xA4) {
470-
writeCharacteristic(serviceEchelon, notify1, QByteArray::fromHex("f0a4010095"));
471-
} else if (command == 0xA5) {
472-
writeCharacteristic(serviceEchelon, notify1, QByteArray::fromHex("f0a5010ea4"));
473485
} else if (command == 0xB0 && static_cast<uint8_t>(newValue.at(3)) == 0x00) {
474-
writeCharacteristic(serviceEchelon, notify1, QByteArray::fromHex("f0d00100c1"));
486+
if (auto *rowerDevice = qobject_cast<rower *>(Rower)) {
487+
rowerDevice->stop(false);
488+
}
489+
writeCharacteristic(serviceEchelon, notify2, QByteArray::fromHex("f0d00100c1"));
475490
} else if (command == 0xB0 && notify2.isValid()) {
491+
if (auto *rowerDevice = qobject_cast<rower *>(Rower)) {
492+
rowerDevice->start();
493+
}
476494
writeCharacteristic(serviceEchelon, notify2, QByteArray::fromHex("f0d00101c2"));
477495
} else if (command == 0xA0) {
496+
// keep-alive echo
478497
writeCharacteristic(serviceEchelon, notify1, newValue);
479498
}
480499
}
@@ -505,6 +524,78 @@ void virtualrower::relayEchelonPacket(const QBluetoothUuid &sourceUuid, const QB
505524
writeCharacteristic(serviceEchelon, targetCharacteristic, value);
506525
}
507526

527+
void virtualrower::echelonWriteStatus() {
528+
if (!serviceEchelon) {
529+
return;
530+
}
531+
532+
QLowEnergyCharacteristic characteristic =
533+
serviceEchelon->characteristic(QBluetoothUuid(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66")));
534+
if (!characteristic.isValid()) {
535+
return;
536+
}
537+
538+
const QTime elapsed = Rower->elapsedTime();
539+
const uint16_t elapsedSeconds =
540+
static_cast<uint16_t>((elapsed.hour() * 3600) + (elapsed.minute() * 60) + elapsed.second());
541+
542+
const uint8_t cadence = static_cast<uint8_t>(qRound(Rower->currentCadence().value())); // strokes per minute
543+
const uint32_t strokeCount = static_cast<uint32_t>(qRound(static_cast<rower *>(Rower)->currentStrokesCount().value()));
544+
const uint32_t distanceMeters = static_cast<uint32_t>(qRound(Rower->odometer() * 1000.0));
545+
546+
// The parser reconstructs the speed as (60 / pace) * 30, so encode pace = 1800 / speed[km/h]
547+
const double speed = Rower->currentSpeed().value();
548+
uint16_t pace = 0;
549+
if (speed > 0.0) {
550+
pace = static_cast<uint16_t>(qBound(1.0, qRound(1800.0 / speed) * 1.0, 65535.0));
551+
}
552+
553+
writeCharacteristic(serviceEchelon, characteristic,
554+
echelonPacket({
555+
0xf0,
556+
0xd1,
557+
0x11,
558+
static_cast<uint8_t>(elapsedSeconds >> 8),
559+
static_cast<uint8_t>(elapsedSeconds),
560+
0x00,
561+
0x00,
562+
0x00,
563+
0x00,
564+
static_cast<uint8_t>(strokeCount),
565+
0x00,
566+
cadence,
567+
0x00,
568+
static_cast<uint8_t>(pace >> 8),
569+
static_cast<uint8_t>(pace),
570+
static_cast<uint8_t>(distanceMeters >> 16),
571+
static_cast<uint8_t>(distanceMeters >> 8),
572+
static_cast<uint8_t>(distanceMeters),
573+
0x00,
574+
0x00,
575+
}));
576+
}
577+
578+
void virtualrower::echelonWriteResistance() {
579+
if (!serviceEchelon) {
580+
return;
581+
}
582+
583+
const int resistance = qRound(Rower->currentResistance().value());
584+
if (echelonLastResistance == resistance) {
585+
return;
586+
}
587+
588+
QLowEnergyCharacteristic characteristic =
589+
serviceEchelon->characteristic(QBluetoothUuid(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66")));
590+
if (!characteristic.isValid()) {
591+
return;
592+
}
593+
594+
writeCharacteristic(serviceEchelon, characteristic,
595+
echelonPacket({0xf0, 0xd2, 0x01, static_cast<uint8_t>(qBound(0, resistance, 255))}));
596+
echelonLastResistance = resistance;
597+
}
598+
508599
void virtualrower::writeCharacteristic(QLowEnergyService *service, const QLowEnergyCharacteristic &characteristic,
509600
const QByteArray &value) {
510601
try {
@@ -812,6 +903,11 @@ void virtualrower::rowerProvider() {
812903
writeCharacteristic(serviceFIT, characteristic, value);
813904
}
814905
}
906+
907+
if (echelon && echelonInitDone && serviceEchelon) {
908+
echelonWriteResistance();
909+
echelonWriteStatus();
910+
}
815911
// characteristic
816912
// = service->characteristic((QBluetoothUuid::CharacteristicType)0x2AD9); // Fitness Machine Control Point
817913
// Q_ASSERT(characteristic.isValid());

src/virtualdevices/virtualrower.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,13 @@ class virtualrower : public virtualdevice {
6565

6666
bool noHeartService = false;
6767

68+
bool echelonInitDone = false;
69+
int echelonLastResistance = -1;
70+
6871
void writeCharacteristic(QLowEnergyService *service, const QLowEnergyCharacteristic &characteristic,
6972
const QByteArray &value);
73+
void echelonWriteStatus();
74+
void echelonWriteResistance();
7075
void setupPM5Services();
7176
QByteArray buildPM5GeneralStatus();
7277
QByteArray buildPM5AdditionalStatus();

src/virtualdevices/virtualtreadmill.cpp

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#include "virtualdevices/virtualtreadmill.h"
22
#include "devices/echelonstride/echelonstride.h"
3-
#include "devices/faketreadmill/faketreadmill.h"
43
#include <QThread>
54
#include <QSettings>
65
#include <QtMath>
@@ -510,15 +509,15 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
510509

511510
if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66")) &&
512511
serviceEchelon && newValue.length() > 3) {
512+
// When QZ is backed by a real Echelon treadmill, forward the app payload to the treadmill and let
513+
// the treadmill's own notifications be mirrored back to the client (firmware unlock passthrough).
513514
if (auto *realEchelon = dynamic_cast<echelonstride *>(treadMill); realEchelon && realEchelon->connected()) {
514515
realEchelon->proxyVirtualTreadmillCommand(newValue);
515516
return;
516517
}
517-
if (auto *fakeEchelon = dynamic_cast<faketreadmill *>(treadMill); fakeEchelon && fakeEchelon->connected()) {
518-
fakeEchelon->proxyVirtualTreadmillCommand(newValue);
519-
return;
520-
}
521518

519+
// Otherwise (fake treadmill / no real Echelon hardware) answer the handshake synthetically, exactly
520+
// like the real Echelon Stride does in the HCI snoop traces.
522521
QLowEnergyCharacteristic notify1 =
523522
serviceEchelon->characteristic(QBluetoothUuid(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66")));
524523
QLowEnergyCharacteristic notify2 =
@@ -550,19 +549,11 @@ void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
550549
writeEchelonCharacteristic(notify2, QByteArray::fromHex("f0d00111d2"));
551550
echelonWriteRunningState();
552551
} else if (command == 0xA0) {
552+
// keep-alive echo
553553
writeEchelonCharacteristic(notify1, newValue);
554-
} else if (command == 0xD2 && newValue.length() >= 5) {
555-
if (auto *treadmillDevice = qobject_cast<treadmill *>(treadMill)) {
556-
const double inclination = static_cast<uint8_t>(newValue.at(3));
557-
treadmillDevice->changeInclination(inclination, inclination);
558-
}
559-
} else if (command == 0xD3 && newValue.length() >= 6) {
560-
if (auto *treadmillDevice = qobject_cast<treadmill *>(treadMill)) {
561-
const uint16_t rawSpeed =
562-
(static_cast<uint8_t>(newValue.at(3)) << 8) | static_cast<uint8_t>(newValue.at(4));
563-
treadmillDevice->changeSpeed(static_cast<double>(rawSpeed) / 1000.0);
564-
}
565554
}
555+
// Note: incoming 0xD1/0xD2/0xD3/0xD5 writes are the app's acknowledgement echoes of our own
556+
// telemetry notifications (as seen in the snoop), not control commands, so they are ignored.
566557
}
567558
}
568559
}

0 commit comments

Comments
 (0)