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"
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+
508599void 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());
0 commit comments