Skip to content

Commit ae5af86

Browse files
committed
Merge branch 'master' into codex/fix-resistance-value-for-fs-yk-bike
2 parents d06d8dd + 77f2059 commit ae5af86

28 files changed

Lines changed: 841 additions & 104 deletions

build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@
375375
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */; };
376376
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */; };
377377
877350F72D1C08E60070CBD8 /* SmartControl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877350F62D1C08E50070CBD8 /* SmartControl.cpp */; };
378+
8774865C2F92432600A02AFD /* moc_cycplusbc2controller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8774865B2F92432600A02AFD /* moc_cycplusbc2controller.cpp */; };
379+
8774865F2F92434B00A02AFD /* cycplusbc2controller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8774865E2F92434B00A02AFD /* cycplusbc2controller.cpp */; };
378380
8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */; };
379381
8775008529E87713008E48B7 /* moc_iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */; };
380382
877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877758B22C98627300BB1697 /* moc_sportstechelliptical.cpp */; };
@@ -1398,6 +1400,9 @@
13981400
8772B7F92CB5603A004AB8E9 /* deerruntreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = deerruntreadmill.h; path = ../src/devices/deeruntreadmill/deerruntreadmill.h; sourceTree = SOURCE_ROOT; };
13991401
877350F52D1C08E50070CBD8 /* SmartControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SmartControl.h; path = ../src/devices/kineticinroadbike/SmartControl.h; sourceTree = SOURCE_ROOT; };
14001402
877350F62D1C08E50070CBD8 /* SmartControl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = SmartControl.cpp; path = ../src/devices/kineticinroadbike/SmartControl.cpp; sourceTree = SOURCE_ROOT; };
1403+
8774865B2F92432600A02AFD /* moc_cycplusbc2controller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_cycplusbc2controller.cpp; sourceTree = "<group>"; };
1404+
8774865D2F92434B00A02AFD /* cycplusbc2controller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = cycplusbc2controller.h; path = ../src/devices/cycplusbc2controller/cycplusbc2controller.h; sourceTree = SOURCE_ROOT; };
1405+
8774865E2F92434B00A02AFD /* cycplusbc2controller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = cycplusbc2controller.cpp; path = ../src/devices/cycplusbc2controller/cycplusbc2controller.cpp; sourceTree = SOURCE_ROOT; };
14011406
8775008129E876F7008E48B7 /* iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = iconceptelliptical.cpp; path = ../src/devices/iconceptelliptical/iconceptelliptical.cpp; sourceTree = "<group>"; };
14021407
8775008229E876F7008E48B7 /* iconceptelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iconceptelliptical.h; path = ../src/devices/iconceptelliptical/iconceptelliptical.h; sourceTree = "<group>"; };
14031408
8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_iconceptelliptical.cpp; sourceTree = "<group>"; };
@@ -2360,6 +2365,9 @@
23602365
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
23612366
isa = PBXGroup;
23622367
children = (
2368+
8774865D2F92434B00A02AFD /* cycplusbc2controller.h */,
2369+
8774865E2F92434B00A02AFD /* cycplusbc2controller.cpp */,
2370+
8774865B2F92432600A02AFD /* moc_cycplusbc2controller.cpp */,
23632371
8798FFA62F8577B3003E969D /* lifespanbike.h */,
23642372
8798FFA72F8577B3003E969D /* lifespanbike.cpp */,
23652373
8798FFA42F857787003E969D /* moc_lifespanbike.cpp */,
@@ -3749,6 +3757,7 @@
37493757
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
37503758
87C5F0BB26285E5F0067A1B5 /* mimetext.cpp in Compile Sources */,
37513759
8732C17F27353464006DF424 /* iconceptbike.cpp in Compile Sources */,
3760+
8774865F2F92434B00A02AFD /* cycplusbc2controller.cpp in Compile Sources */,
37523761
87CF5169293C879800A7CABC /* characteristicwriteprocessore005.cpp in Compile Sources */,
37533762
873824C027E64707004F1B46 /* moc_dirconmanager.cpp in Compile Sources */,
37543763
2F0E70F826316F3600E11F3A /* virtualbike_zwift.swift in Compile Sources */,
@@ -3968,6 +3977,7 @@
39683977
872089142CE65451008C2C17 /* qmqtttype.cpp in Compile Sources */,
39693978
872089152CE65451008C2C17 /* qmqttcontrolpacket.cpp in Compile Sources */,
39703979
872089162CE65451008C2C17 /* qmqttmessage.cpp in Compile Sources */,
3980+
8774865C2F92432600A02AFD /* moc_cycplusbc2controller.cpp in Compile Sources */,
39713981
872089172CE65451008C2C17 /* qmqtttopicname.cpp in Compile Sources */,
39723982
872089182CE65451008C2C17 /* qmqttpublishproperties.cpp in Compile Sources */,
39733983
872089192CE65451008C2C17 /* qmqttauthenticationproperties.cpp in Compile Sources */,

src/android/src/BleAdvertiser.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,78 @@
3737

3838
public class BleAdvertiser {
3939
private static final UUID SERVICE_UUID = UUID.fromString("00001826-0000-1000-8000-00805f9b34fb");
40+
private static final UUID ECHELON_SERVICE_UUID = UUID.fromString("0bf669f0-45f2-11e7-9598-0800200c9a66");
41+
private static final String ECHELON_DEVICE_NAME = "ECHEX-5s-160880";
4042
// PM5 Concept2 UUIDs
4143
private static final UUID PM5_DISCOVERY_SERVICE_UUID = UUID.fromString("CE060000-43E5-11E4-916C-0800200C9A66");
4244
private static final UUID PM5_ROWING_SERVICE_UUID = UUID.fromString("CE060030-43E5-11E4-916C-0800200C9A66");
4345
private static final byte[] SERVICE_DATA_ROWER = {0x01, 0x10, 0x00};
4446
private static final byte[] SERVICE_DATA_TREADMILL = {0x01, 0x01, 0x00};
4547

48+
public static void startAdvertisingEchelon(Context context, String deviceName) {
49+
try {
50+
if (context == null) {
51+
QLog.e("BleAdvertiser", "Context is null for Echelon advertising");
52+
return;
53+
}
54+
55+
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
56+
if (bluetoothManager != null) {
57+
android.bluetooth.BluetoothAdapter adapter = bluetoothManager.getAdapter();
58+
if (adapter == null) {
59+
QLog.e("BleAdvertiser", "Bluetooth adapter is null for Echelon advertising");
60+
return;
61+
}
62+
63+
android.bluetooth.le.BluetoothLeAdvertiser advertiser = adapter.getBluetoothLeAdvertiser();
64+
65+
AdvertiseSettings settings = new AdvertiseSettings.Builder()
66+
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
67+
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
68+
.setConnectable(true)
69+
.build();
70+
71+
AdvertiseData advertiseData = new AdvertiseData.Builder()
72+
.addServiceUuid(new ParcelUuid(ECHELON_SERVICE_UUID))
73+
.build();
74+
75+
if (advertiser != null) {
76+
try {
77+
adapter.setName(deviceName != null && !deviceName.isEmpty() ? deviceName : ECHELON_DEVICE_NAME);
78+
} catch (SecurityException | IllegalArgumentException e) {
79+
QLog.e("BleAdvertiser", "Unable to set Echelon device name: " + e.getMessage());
80+
}
81+
advertiser.startAdvertising(settings, advertiseData, advertiseCallback);
82+
} else {
83+
QLog.e("BleAdvertiser", "BluetoothLeAdvertiser is null for Echelon advertising");
84+
}
85+
}
86+
} catch (Throwable t) {
87+
QLog.e("BleAdvertiser", "Echelon advertising crash: " + t.toString());
88+
}
89+
}
90+
91+
public static void stopAdvertising(Context context) {
92+
try {
93+
if (context == null) {
94+
return;
95+
}
96+
97+
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
98+
if (bluetoothManager == null || bluetoothManager.getAdapter() == null) {
99+
return;
100+
}
101+
102+
android.bluetooth.le.BluetoothLeAdvertiser advertiser =
103+
bluetoothManager.getAdapter().getBluetoothLeAdvertiser();
104+
if (advertiser != null) {
105+
advertiser.stopAdvertising(advertiseCallback);
106+
}
107+
} catch (Throwable t) {
108+
QLog.e("BleAdvertiser", "stopAdvertising crash: " + t.toString());
109+
}
110+
}
111+
46112
public static void startAdvertisingRower(Context context) {
47113
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
48114
if (bluetoothManager != null) {

src/devices/bluetooth.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,6 +2837,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
28372837
emit searchingStop();
28382838
this->signalBluetoothDeviceConnected(focusTreadmill);
28392839
} else if (((b.name().startsWith(QStringLiteral("FS-")) && !horizonTreadmill && !snode_bike && !fitplus_bike && !ftmsBike && !iconsole_elliptical) ||
2840+
(b.name().toUpper().startsWith(QStringLiteral("TR510-T"))) ||
28402841
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
28412842
(b.name().startsWith(QStringLiteral("SW")) && b.name().length() == 14 &&
28422843
!b.name().contains('(') && !b.name().contains(')') && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||

src/devices/echelonconnectsport/echelonconnectsport.cpp

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ double echelonconnectsport::bikeResistanceToPeloton(double resistance) {
249249
void 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+
568681
void 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+
622755
bool echelonconnectsport::connected() {
623756
QSettings settings;
624757
bool useNativeIOS = false;

src/devices/echelonconnectsport/echelonconnectsport.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class echelonconnectsport : public bike {
4343
resistance_t maxResistance() override { return max_resistance; }
4444
resistance_t resistanceFromPowerRequest(uint16_t power) override;
4545
bool connected() override;
46+
void proxyVirtualBikeCommand(const QByteArray &value);
47+
void switchToClassicVirtualBikeBridge();
48+
void enableVirtualEchelonBridge();
4649

4750
private:
4851
const resistance_t max_resistance = 32;
@@ -76,6 +79,10 @@ class echelonconnectsport : public bike {
7679

7780
bool initDone = false;
7881
bool initRequest = false;
82+
bool unlockResponseReceived = false;
83+
bool classicBridgePromptShown = false;
84+
bool classicVirtualBridgeActive = false;
85+
bool lockedBikePromptShown = false;
7986

8087
bool noWriteResistance = false;
8188
bool noHeartService = false;
@@ -104,6 +111,11 @@ class echelonconnectsport : public bike {
104111
void update();
105112
void error(QLowEnergyController::Error err);
106113
void errorService(QLowEnergyService::ServiceError);
114+
115+
void maybePromptForClassicBridge();
116+
void maybePromptToEnableVirtualEchelon(const QByteArray &newValue);
117+
void requestClassicBridgePrompt();
118+
void createVirtualBike(bool forceClassicMode = false, DirconManager *existingDirconManager = nullptr);
107119
};
108120

109121
#endif // ECHELONCONNECTSPORT_H

0 commit comments

Comments
 (0)