@@ -249,7 +249,6 @@ double echelonconnectsport::bikeResistanceToPeloton(double resistance) {
249249void echelonconnectsport::characteristicChanged (const QLowEnergyCharacteristic &characteristic,
250250 const QByteArray &newValue) {
251251 // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
252- Q_UNUSED (characteristic);
253252 QSettings settings;
254253 QString heartRateBeltName =
255254 settings.value (QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString ();
@@ -261,6 +260,17 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
261260
262261 qDebug () << " << " + newValue.toHex (' ' );
263262
263+ maybePromptToEnableVirtualEchelon (newValue);
264+
265+ if (newValue == QByteArray::fromHex (" f0a5010ea4" )) {
266+ unlockResponseReceived = true ;
267+ maybePromptForClassicBridge ();
268+ }
269+
270+ if (auto *virtualBike = VirtualBike ()) {
271+ virtualBike->relayEchelonPacket (characteristic.uuid (), newValue);
272+ }
273+
264274 lastPacket = newValue;
265275
266276 // resistance value is in another frame
@@ -372,6 +382,8 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
372382 qDebug () << QStringLiteral (" Last CrankEventTime: " ) + QString::number (LastCrankEventTime);
373383 qDebug () << QStringLiteral (" Current Watt: " ) + QString::number (watts ());
374384
385+ maybePromptForClassicBridge ();
386+
375387 if (!useNativeIOS && m_control && m_control->error () != QLowEnergyController::NoError) {
376388 qDebug () << QStringLiteral (" QLowEnergyController ERROR!!" ) << m_control->errorString ();
377389 }
@@ -491,12 +503,7 @@ void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) {
491503 // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
492504 this ->setVirtualDevice (virtualRower, VIRTUAL_DEVICE_MODE ::ALTERNATIVE );
493505 } else {
494- qDebug () << QStringLiteral (" creating virtual bike interface..." );
495- auto virtualBike =
496- new virtualbike (this , noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
497- // connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
498- connect (virtualBike, &virtualbike::changeInclination, this , &echelonconnectsport::changeInclination);
499- this ->setVirtualDevice (virtualBike, VIRTUAL_DEVICE_MODE ::PRIMARY );
506+ createVirtualBike ();
500507 }
501508 }
502509 }
@@ -565,6 +572,112 @@ void echelonconnectsport::error(QLowEnergyController::Error err) {
565572 m_control->errorString ();
566573}
567574
575+ void echelonconnectsport::maybePromptForClassicBridge () {
576+ QSettings settings;
577+ const bool virtualEchelonEnabled =
578+ settings.value (QZSettings::virtual_device_echelon, QZSettings::default_virtual_device_echelon).toBool ();
579+
580+ if (!virtualEchelonEnabled || classicVirtualBridgeActive || classicBridgePromptShown || !unlockResponseReceived ||
581+ Cadence.value () <= 0 ) {
582+ return ;
583+ }
584+
585+ requestClassicBridgePrompt ();
586+ }
587+
588+ void echelonconnectsport::requestClassicBridgePrompt () {
589+ if (!homeform::singleton ()) {
590+ return ;
591+ }
592+
593+ classicBridgePromptShown = true ;
594+ homeform::singleton ()->setEchelonBridgeSwitchPromptRequested (true );
595+ }
596+
597+ void echelonconnectsport::maybePromptToEnableVirtualEchelon (const QByteArray &newValue) {
598+ QSettings settings;
599+ const bool virtualEchelonEnabled =
600+ settings.value (QZSettings::virtual_device_echelon, QZSettings::default_virtual_device_echelon).toBool ();
601+ const bool isLockedFrame = newValue.size () == 8 && static_cast <uint8_t >(newValue.at (0 )) == 0xf0 &&
602+ static_cast <uint8_t >(newValue.at (1 )) == 0xe0 ;
603+
604+ if (virtualEchelonEnabled || lockedBikePromptShown || !isLockedFrame || !homeform::singleton ()) {
605+ return ;
606+ }
607+
608+ lockedBikePromptShown = true ;
609+ homeform::singleton ()->setEchelonEnablePromptRequested (true );
610+ }
611+
612+ void echelonconnectsport::createVirtualBike (bool forceClassicMode, DirconManager *existingDirconManager) {
613+ qDebug () << QStringLiteral (" creating virtual bike interface..." )
614+ << (forceClassicMode ? QStringLiteral (" (classic bridge)" ) : QStringLiteral (" (echelon bridge)" ));
615+ auto virtualBike =
616+ new virtualbike (this , noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain,
617+ forceClassicMode, existingDirconManager);
618+ connect (virtualBike, &virtualbike::changeInclination, this , &echelonconnectsport::changeInclination);
619+ this ->setVirtualDevice (virtualBike, VIRTUAL_DEVICE_MODE ::PRIMARY );
620+ }
621+
622+ void echelonconnectsport::switchToClassicVirtualBikeBridge () {
623+ if (classicVirtualBridgeActive) {
624+ return ;
625+ }
626+
627+ classicVirtualBridgeActive = true ;
628+ if (homeform::singleton ()) {
629+ homeform::singleton ()->setEchelonBridgeSwitchPromptRequested (false );
630+ homeform::singleton ()->setToastRequested (QStringLiteral (" Switching to classic Bluetooth bridge" ));
631+ }
632+
633+ #ifdef Q_OS_IOS
634+ #ifndef IO_UNDER_QT
635+ QSettings settings;
636+ const bool cadence =
637+ settings.value (QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool ();
638+ const bool ios_peloton_workaround =
639+ settings.value (QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool ();
640+ if (ios_peloton_workaround && cadence) {
641+ this ->setVirtualDevice (nullptr , VIRTUAL_DEVICE_MODE ::NONE );
642+ if (!h) {
643+ qDebug () << " ios_peloton_workaround activated!" ;
644+ h = new lockscreen ();
645+ }
646+ h->virtualbike_ios ();
647+ return ;
648+ }
649+ #endif
650+ #endif
651+
652+ DirconManager *existingDirconManager = nullptr ;
653+ if (auto *virtualBike = dynamic_cast <virtualbike *>(VirtualBike ())) {
654+ existingDirconManager = virtualBike->detachDirconManager ();
655+ }
656+
657+ createVirtualBike (true , existingDirconManager);
658+ }
659+
660+ void echelonconnectsport::enableVirtualEchelonBridge () {
661+ QSettings settings;
662+ settings.setValue (QZSettings::virtual_device_echelon, true );
663+
664+ classicVirtualBridgeActive = false ;
665+ classicBridgePromptShown = false ;
666+ lockedBikePromptShown = true ;
667+
668+ if (homeform::singleton ()) {
669+ homeform::singleton ()->setEchelonEnablePromptRequested (false );
670+ homeform::singleton ()->setToastRequested (QStringLiteral (" Virtual Echelon enabled for this bike" ));
671+ }
672+
673+ DirconManager *existingDirconManager = nullptr ;
674+ if (auto *virtualBike = dynamic_cast <virtualbike *>(VirtualBike ())) {
675+ existingDirconManager = virtualBike->detachDirconManager ();
676+ }
677+
678+ createVirtualBike (false , existingDirconManager);
679+ }
680+
568681void echelonconnectsport::deviceDiscovered (const QBluetoothDeviceInfo &device) {
569682 qDebug () << QStringLiteral (" Found new device: " ) + device.name () + QStringLiteral (" (" ) +
570683 device.address ().toString () + ' )' ;
@@ -619,6 +732,26 @@ void echelonconnectsport::deviceDiscovered(const QBluetoothDeviceInfo &device) {
619732 }
620733}
621734
735+ void echelonconnectsport::proxyVirtualBikeCommand (const QByteArray &value) {
736+ if (!gattCommunicationChannelService || !gattWriteCharacteristic.isValid () || !m_control ||
737+ m_control->state () == QLowEnergyController::UnconnectedState) {
738+ qDebug () << QStringLiteral (" proxyVirtualBikeCommand ignored because the Echelon bike is not ready" );
739+ return ;
740+ }
741+
742+ // On the real-bike passthrough path the app's unlock handshake is proxied to the bike, but the
743+ // corresponding A5 reply is not guaranteed to surface back as a bike notification. Mark the
744+ // unlock flow as seen so the popup can still appear once cadence starts flowing.
745+ if (!unlockResponseReceived && value.size () > 1 && static_cast <uint8_t >(value.at (0 )) == 0xf0 &&
746+ static_cast <uint8_t >(value.at (1 )) != 0xa0 ) {
747+ unlockResponseReceived = true ;
748+ maybePromptForClassicBridge ();
749+ }
750+
751+ writeCharacteristic (reinterpret_cast <uint8_t *>(const_cast <char *>(value.constData ())), value.size (),
752+ QStringLiteral (" virtual echelon proxy" ));
753+ }
754+
622755bool echelonconnectsport::connected () {
623756 QSettings settings;
624757 bool useNativeIOS = false ;
0 commit comments