Compare commits

...

4 Commits

Author SHA1 Message Date
Roberto Viola
06a179288b Merge branch 'JRNY-virtual-treadmill-implementation' of https://github.com/cagnulein/qdomyos-zwift into JRNY-virtual-treadmill-implementation 2025-08-20 14:17:16 +02:00
Roberto Viola
d81bbad9a0 Force heart rate service outside FTMS for JRNY
On Android, the virtual treadmill now forces the heart rate service to be outside FTMS for JRNY compatibility, with a debug message indicating this behavior.
2025-08-20 14:17:08 +02:00
Roberto Viola
c914d147f0 Update virtualtreadmill.cpp 2025-08-19 20:01:35 +02:00
Roberto Viola
0c30ac0a74 JRNY virtual treadmill implementation 2025-08-19 16:25:51 +02:00
4 changed files with 161 additions and 37 deletions

View File

@@ -974,9 +974,10 @@ const QString QZSettings::tile_auto_virtual_shifting_climb_enabled = QStringLite
const QString QZSettings::tile_auto_virtual_shifting_climb_order = QStringLiteral("tile_auto_virtual_shifting_climb_order");
const QString QZSettings::tile_auto_virtual_shifting_sprint_enabled = QStringLiteral("tile_auto_virtual_shifting_sprint_enabled");
const QString QZSettings::tile_auto_virtual_shifting_sprint_order = QStringLiteral("tile_auto_virtual_shifting_sprint_order");
const QString QZSettings::jrny_virtual_treadmill = QStringLiteral("jrny_virtual_treadmill");
const uint32_t allSettingsCount = 798;
const uint32_t allSettingsCount = 800;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1795,6 +1796,7 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::tile_auto_virtual_shifting_climb_order, QZSettings::default_tile_auto_virtual_shifting_climb_order},
{QZSettings::tile_auto_virtual_shifting_sprint_enabled, QZSettings::default_tile_auto_virtual_shifting_sprint_enabled},
{QZSettings::tile_auto_virtual_shifting_sprint_order, QZSettings::default_tile_auto_virtual_shifting_sprint_order},
{QZSettings::jrny_virtual_treadmill, QZSettings::default_jrny_virtual_treadmill},
{QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike},
};

View File

@@ -2660,6 +2660,12 @@ class QZSettings {
static const QString tile_auto_virtual_shifting_sprint_order;
static constexpr int default_tile_auto_virtual_shifting_sprint_order = 57;
/**
* @brief Enable JRNY virtual treadmill (Android only)
*/
static const QString jrny_virtual_treadmill;
static constexpr bool default_jrny_virtual_treadmill = false;
/**
* @brief Write the QSettings values using the constants from this namespace.
* @param showDefaults Optionally indicates if the default should be shown with the key.

View File

@@ -1198,6 +1198,7 @@ import Qt.labs.platform 1.1
property int tile_auto_virtual_shifting_sprint_order: 57
property string proform_rower_ip: ""
property string ftms_elliptical: "Disabled"
property bool jrny_virtual_treadmill: false
}
@@ -11838,6 +11839,34 @@ import Qt.labs.platform 1.1
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: jrnyVirtualTreadmillDelegate
text: qsTr("JRNY Virtual Treadmill")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.jrny_virtual_treadmill
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.jrny_virtual_treadmill = checked; window.settings_restart_to_apply = true; }
}
Label {
text: qsTr("Enable JRNY virtual treadmill for Android. Advertises as JOHNSON T202-5 device. Default is off.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: virtualBikeForceResistanceDelegate
text: qsTr("Zwift Force Resistance")
@@ -12018,7 +12047,7 @@ import Qt.labs.platform 1.1
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.dircon_server_base_port = dirconServerPortTextField.text; toast.show("Setting saved!"); }
}
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
#include "virtualdevices/virtualtreadmill.h"
#include "qzsettings.h"
#include <QThread>
#include <QSettings>
#include <QtMath>
@@ -15,6 +16,20 @@ using namespace std::chrono_literals;
virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) {
QSettings settings;
treadMill = t;
bool jrnyVirtualTreadmill = settings.value(QZSettings::jrny_virtual_treadmill, QZSettings::default_jrny_virtual_treadmill).toBool();
// JRNY Virtual Treadmill is only supported on Android
if (jrnyVirtualTreadmill) {
#ifndef Q_OS_ANDROID
qDebug() << "JRNY Virtual Treadmill is only supported on Android";
jrnyVirtualTreadmill = false; // Disable on non-Android platforms
#else
// Force heart rate service outside FTMS for JRNY compatibility
noHeartService = true;
qDebug() << "JRNY Virtual Treadmill: forcing heart rate service outside FTMS";
#endif
}
int bikeResistanceOffset =
settings.value(QZSettings::bike_resistance_offset, QZSettings::default_bike_resistance_offset).toInt();
@@ -57,11 +72,21 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) {
//! [Advertising Data]
advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
advertisingData.setIncludePowerLevel(true);
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
advertisingData.setLocalName(QStringLiteral("KICKR RUN"));
#else
advertisingData.setLocalName(QStringLiteral("DomyosBridge"));
if (jrnyVirtualTreadmill) {
#ifdef Q_OS_ANDROID
advertisingData.setLocalName(QStringLiteral("T202-5"));
#else
qDebug() << "JRNY Virtual Treadmill is only supported on Android";
return; // Exit early on non-Android platforms
#endif
} else {
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
advertisingData.setLocalName(QStringLiteral("KICKR RUN"));
#else
advertisingData.setLocalName(QStringLiteral("DomyosBridge"));
#endif
}
QList<QBluetoothUuid> services;
// Add Wahoo Run Service UUID
@@ -87,20 +112,68 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) {
//! [Advertising Data]
// Add Device Information Service
QLowEnergyCharacteristicData manufacturerNameChar;
manufacturerNameChar.setUuid(QBluetoothUuid::CharacteristicType::ManufacturerNameString);
manufacturerNameChar.setProperties(QLowEnergyCharacteristic::Read);
manufacturerNameChar.setValue(QByteArray("Wahoo Fitness")); // Changed to Wahoo Fitness
if (jrnyVirtualTreadmill) {
manufacturerNameChar.setValue(QByteArray("JOHNSON"));
} else {
manufacturerNameChar.setValue(QByteArray("Wahoo Fitness")); // Changed to Wahoo Fitness
}
QLowEnergyCharacteristicData modelNumberChar;
modelNumberChar.setUuid(QBluetoothUuid::CharacteristicType::ModelNumberString);
modelNumberChar.setProperties(QLowEnergyCharacteristic::Read);
if (jrnyVirtualTreadmill) {
modelNumberChar.setValue(QByteArray("T202-5"));
}
QLowEnergyCharacteristicData firmwareRevChar;
firmwareRevChar.setUuid(QBluetoothUuid::CharacteristicType::FirmwareRevisionString);
firmwareRevChar.setProperties(QLowEnergyCharacteristic::Read);
firmwareRevChar.setValue(QByteArray("1.0.11"));
if (jrnyVirtualTreadmill) {
firmwareRevChar.setValue(QByteArray("V100.0.0"));
} else {
firmwareRevChar.setValue(QByteArray("1.0.11"));
}
QLowEnergyCharacteristicData softwareRevChar;
softwareRevChar.setUuid(QBluetoothUuid::CharacteristicType::SoftwareRevisionString);
softwareRevChar.setProperties(QLowEnergyCharacteristic::Read);
if (jrnyVirtualTreadmill) {
softwareRevChar.setValue(QByteArray("V100.0.1"));
}
QLowEnergyCharacteristicData hardwareRevChar;
hardwareRevChar.setUuid(QBluetoothUuid::CharacteristicType::HardwareRevisionString);
hardwareRevChar.setProperties(QLowEnergyCharacteristic::Read);
hardwareRevChar.setValue(QByteArray("1"));
if (jrnyVirtualTreadmill) {
hardwareRevChar.setValue(QByteArray("WLT8"));
} else {
hardwareRevChar.setValue(QByteArray("1"));
}
QLowEnergyCharacteristicData serialNumberChar;
serialNumberChar.setUuid(QBluetoothUuid::CharacteristicType::SerialNumberString);
serialNumberChar.setProperties(QLowEnergyCharacteristic::Read);
if (jrnyVirtualTreadmill) {
serialNumberChar.setValue(QByteArray("V100.0.2"));
}
QLowEnergyCharacteristicData systemIdChar;
systemIdChar.setUuid(QBluetoothUuid::CharacteristicType::SystemID);
systemIdChar.setProperties(QLowEnergyCharacteristic::Read);
if (jrnyVirtualTreadmill) {
QByteArray systemIdData;
systemIdData.append(char(0xFA));
systemIdData.append(char(0xE4));
systemIdData.append(char(0xE3));
systemIdData.append(char(0x4B));
systemIdData.append(char(0x3D));
systemIdData.append(char(0x09));
systemIdChar.setValue(systemIdData);
}
// Create Device Information Service
serviceDataDIS.setType(QLowEnergyServiceData::ServiceTypePrimary);
@@ -108,32 +181,41 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) {
serviceDataDIS.addCharacteristic(manufacturerNameChar);
serviceDataDIS.addCharacteristic(firmwareRevChar);
serviceDataDIS.addCharacteristic(hardwareRevChar);
if (jrnyVirtualTreadmill) {
serviceDataDIS.addCharacteristic(modelNumberChar);
serviceDataDIS.addCharacteristic(softwareRevChar);
serviceDataDIS.addCharacteristic(serialNumberChar);
serviceDataDIS.addCharacteristic(systemIdChar);
}
// Create Wahoo Run Service
serviceDataWahoo.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceDataWahoo.setUuid(QBluetoothUuid(QString("A026EE0E-0A7D-4AB3-97FA-F1500F9FEB8B")));
// Create Wahoo Run Service (only when not in JRNY mode)
if (!jrnyVirtualTreadmill) {
serviceDataWahoo.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceDataWahoo.setUuid(QBluetoothUuid(QString("A026EE0E-0A7D-4AB3-97FA-F1500F9FEB8B")));
// Add Wahoo Run Notify Characteristic
QLowEnergyCharacteristicData wahooNotifyChar;
wahooNotifyChar.setUuid(QBluetoothUuid(QString("A026E03D-0A7D-4AB3-97FA-F1500F9FEB8B")));
wahooNotifyChar.setProperties(QLowEnergyCharacteristic::Read |
QLowEnergyCharacteristic::WriteNoResponse |
QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData wahooNotifyConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
wahooNotifyChar.addDescriptor(wahooNotifyConfig);
// Add Wahoo Run Notify Characteristic
QLowEnergyCharacteristicData wahooNotifyChar;
wahooNotifyChar.setUuid(QBluetoothUuid(QString("A026E03D-0A7D-4AB3-97FA-F1500F9FEB8B")));
wahooNotifyChar.setProperties(QLowEnergyCharacteristic::Read |
QLowEnergyCharacteristic::WriteNoResponse |
QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData wahooNotifyConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
wahooNotifyChar.addDescriptor(wahooNotifyConfig);
// Add Wahoo Run Write Characteristic
QLowEnergyCharacteristicData wahooWriteChar;
wahooWriteChar.setUuid(QBluetoothUuid(QString("A026E03E-0A7D-4AB3-97FA-F1500F9FEB8B")));
wahooWriteChar.setProperties(QLowEnergyCharacteristic::WriteNoResponse |
QLowEnergyCharacteristic::Indicate);
const QLowEnergyDescriptorData wahooWriteConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
wahooWriteChar.addDescriptor(wahooWriteConfig);
// Add Wahoo Run Write Characteristic
QLowEnergyCharacteristicData wahooWriteChar;
wahooWriteChar.setUuid(QBluetoothUuid(QString("A026E03E-0A7D-4AB3-97FA-F1500F9FEB8B")));
wahooWriteChar.setProperties(QLowEnergyCharacteristic::WriteNoResponse |
QLowEnergyCharacteristic::Indicate);
const QLowEnergyDescriptorData wahooWriteConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
wahooWriteChar.addDescriptor(wahooWriteConfig);
serviceDataWahoo.addCharacteristic(wahooNotifyChar);
serviceDataWahoo.addCharacteristic(wahooWriteChar);
serviceDataWahoo.addCharacteristic(wahooNotifyChar);
serviceDataWahoo.addCharacteristic(wahooWriteChar);
}
//! [Service Data]
if (ftmsServiceEnable()) {
@@ -352,8 +434,10 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) {
serviceDIS = leController->addService(serviceDataDIS);
QThread::msleep(100);
serviceWahoo = leController->addService(serviceDataWahoo);
QThread::msleep(100);
if (!jrnyVirtualTreadmill) {
serviceWahoo = leController->addService(serviceDataWahoo);
QThread::msleep(100);
}
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
genericAccessServer = leController->addService(genericAccessServerData);
@@ -368,7 +452,7 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) {
QObject::connect(serviceFTMS, &QLowEnergyService::characteristicChanged, this,
&virtualtreadmill::characteristicChanged);
if (serviceWahoo)
if (!jrnyVirtualTreadmill && serviceWahoo)
QObject::connect(serviceWahoo, &QLowEnergyService::characteristicChanged, this,
&virtualtreadmill::wahooCharacteristicChanged);
@@ -467,6 +551,7 @@ void virtualtreadmill::reconnect() {
QSettings settings;
bool bluetooth_relaxed =
settings.value(QZSettings::bluetooth_relaxed, QZSettings::default_bluetooth_relaxed).toBool();
bool jrnyVirtualTreadmill = settings.value(QZSettings::jrny_virtual_treadmill, QZSettings::default_jrny_virtual_treadmill).toBool();
if (bluetooth_relaxed) {
return;
@@ -485,8 +570,10 @@ void virtualtreadmill::reconnect() {
serviceDIS = leController->addService(serviceDataDIS);
QThread::msleep(100);
serviceWahoo = leController->addService(serviceDataWahoo);
QThread::msleep(100);
if (!jrnyVirtualTreadmill) {
serviceWahoo = leController->addService(serviceDataWahoo);
QThread::msleep(100);
}
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
genericAccessServer = leController->addService(genericAccessServerData);
@@ -500,7 +587,7 @@ void virtualtreadmill::reconnect() {
QLowEnergyAdvertisingParameters pars;
pars.setInterval(100, 100);
if (serviceFTMS || serviceRSC || serviceWahoo) {
if (serviceFTMS || serviceRSC || (!jrnyVirtualTreadmill && serviceWahoo) || (jrnyVirtualTreadmill && serviceDIS)) {
#ifdef Q_OS_ANDROID
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/BleAdvertiser",
"startAdvertisingTreadmill",