Compare commits

...

14 Commits

Author SHA1 Message Date
Roberto Viola
6ba867a1b5 Mobi Rower 2026-01-30 21:31:29 +01:00
Roberto Viola
72f57053a7 Update project.pbxproj 2026-01-30 09:37:13 +01:00
Roberto Viola
13ea5313b1 Add D500V2 workaround for FTMS start simulation command
Implements a workaround for D500V2 bikes that require a START_RESUME (0x07) command before accepting simulation parameters (0x11). The code now tracks the command sequence and injects the necessary command if missing, ensuring compatibility with D500V2 models. Also adds detection and flag for D500V2 during device discovery.
2026-01-30 09:23:31 +01:00
Roberto Viola
7f694733b2 Update project.pbxproj 2026-01-29 16:34:57 +01:00
Roberto Viola
b1755c004a Detect iPadOS multi-window mode and add padding for window control buttons (#4239)
* Detect iPadOS multi-window mode and add padding for window control buttons

When running on iPadOS in multi-window mode (Stage Manager, Split View,
Slide Over), the window control buttons (red/yellow/green) at the top-left
overlap with the hamburger menu button. This adds:

- Native iOS detection via UIWindowScene API to check if window is smaller
  than screen (indicating multi-window mode)
- QML-side window width check for reactive updates when user enters/exits
  multi-window mode
- 70px left padding on toolbar when in multi-window mode on iPadOS

Fixes #4238

https://claude.ai/code/session_01VPuuPcJnU1GEtGy1vosET9

* Update homeform.h

* Use top padding instead of left padding for iPadOS multi-window mode

Move the toolbar down instead of to the right when in multi-window mode,
which feels more natural and preserves the horizontal layout.

https://claude.ai/code/session_01VPuuPcJnU1GEtGy1vosET9

* Update main.qml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 16:28:58 +01:00
Roberto Viola
360ab66431 Update project.pbxproj 2026-01-29 15:17:25 +01:00
Roberto Viola
04b659a91f Add WT treadmill device support (e.g. WT703) (#4241)
Adds support for WT treadmill devices with names matching pattern "WT"
followed by 3 digits (max 5 characters). The forceSpeed is scaled to
miles when miles_unit setting is enabled, similar to TM4800/TM6500/T3G_ELITE.

https://claude.ai/code/session_01GJXMLrS4sA9LFxSkASSwuU

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 15:15:05 +01:00
Roberto Viola
9487fa3cb4 Add BGYM Bluetooth device support as CSC bike (#4240)
Add support for BGYM devices with 8-byte name length to be recognized
as CSC bikes, enabling compatibility with this fitness equipment brand.

https://claude.ai/code/session_01BDEuRWwTUYM12x5KM7Rd3s

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 14:39:19 +01:00
Roberto Viola
1ac2149424 Add Domyos TS100 treadmill support with fixed 15° inclination (#4234) 2026-01-29 13:36:11 +01:00
Roberto Viola
28558697b2 Fix indentation for Bowflex Treadmill options
Corrected the indentation of the Bowflex Treadmill AccordionElement and its contents in settings.qml to ensure proper UI structure and readability.
2026-01-29 12:09:59 +01:00
Roberto Viola
6918fb9eba Skip problematic services for Zwift RunPod
Added logic to detect Zwift RunPod devices and skip the fff0 and ffc0 services during service scan to avoid discovery issues. This prevents connection problems specific to Zwift RunPod devices.
2026-01-29 12:05:50 +01:00
Roberto Viola
365abbb7cb Add jump rope FIT session metrics: total_cycles, avg_cadence, max_cadence (#4232) 2026-01-28 21:27:12 +01:00
Roberto Viola
d3f52682cc Watt Gain also affects Zwift Power Offset in workout (ERG) mode #4205 (#4222) 2026-01-28 15:58:44 +01:00
Roberto Viola
da4f360f63 Fix data conversion and logging in domyosrower.cpp
Corrected type casting for packet data extraction in GetSpeedFromPacket and GetDistanceFromPacket to ensure proper uint16_t conversion. Updated debug logging in characteristicChanged to improve output formatting.
2026-01-28 08:38:05 +01:00
20 changed files with 661 additions and 50 deletions

View File

@@ -4573,7 +4573,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4774,7 +4774,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
@@ -5011,7 +5011,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -5107,7 +5107,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5199,7 +5199,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5315,7 +5315,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5425,7 +5425,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -5516,7 +5516,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1270;
CURRENT_PROJECT_VERSION = 1274;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;

View File

@@ -968,7 +968,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(nordictrackifitadbRower);
} else if (((csc_as_bike && b.name().startsWith(cscName)) ||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) &&
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-")) ||
(b.name().toUpper().startsWith(QStringLiteral("BGYM")) && b.name().length() == 8)) &&
!cscBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -1568,7 +1569,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith(QStringLiteral("TC-"))) || // FTMS (Focus Fitness Jet 7 iPlus)
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("BODYCRAFT_")) // Bodycraft T850 Treadmill
b.name().toUpper().startsWith(QStringLiteral("BODYCRAFT_")) || // Bodycraft T850 Treadmill
(b.name().toUpper().startsWith(QStringLiteral("WT")) && b.name().length() == 5 && b.name().midRef(2).toInt() > 0) // WT treadmill (e.g. WT703)
) &&
!horizonTreadmill && filter) {
this->setLastBluetoothDevice(b);
@@ -2092,6 +2094,15 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(lifespanTreadmill, &lifespantreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
lifespanTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(lifespanTreadmill);
} else if (b.name().startsWith(QStringLiteral("AT-R")) && !mobiRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
mobiRower = new mobirower(noWriteResistance, noHeartService);
emit deviceConnected(b);
connect(mobiRower, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
mobiRower->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(mobiRower);
} else if ((b.name().toUpper().startsWith(QStringLiteral("ECH-ROW")) ||
b.name().toUpper().startsWith(QStringLiteral("ROWSPORT")) ||
b.name().toUpper().startsWith(QStringLiteral("ROW-S"))) &&
@@ -3603,6 +3614,11 @@ void bluetooth::restart() {
delete echelonRower;
echelonRower = nullptr;
}
if (mobiRower) {
delete mobiRower;
mobiRower = nullptr;
}
if (echelonStride) {
delete echelonStride;
@@ -4038,6 +4054,8 @@ bluetoothdevice *bluetooth::device() {
return echelonConnectSport;
} else if (echelonRower) {
return echelonRower;
} else if (mobiRower) {
return mobiRower;
} else if (echelonStride) {
return echelonStride;
} else if (echelonStairclimber) {

View File

@@ -154,6 +154,7 @@
#include "zwift_play/zwiftPlayDevice.h"
#include "zwift_play/zwiftclickremote.h"
#include "devices/mobirower/mobirower.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
@@ -269,6 +270,7 @@ class bluetooth : public QObject, public SignalHandler {
echelonrower *echelonRower = nullptr;
ftmsrower *ftmsRower = nullptr;
smartrowrower *smartrowRower = nullptr;
mobirower *mobiRower = nullptr;
echelonstride *echelonStride = nullptr;
echelonstairclimber *echelonStairclimber = nullptr;
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;

View File

@@ -301,7 +301,7 @@ void domyosrower::serviceDiscovered(const QBluetoothUuid &gatt) {
void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QDateTime now = QDateTime::currentDateTime();
qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
qDebug() << QStringLiteral(" << ") + QString::number(newValue.length()) + QStringLiteral(" ") + newValue.toHex(' ');
Q_UNUSED(characteristic);
QSettings settings;
QString heartRateBeltName =
@@ -643,7 +643,7 @@ void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characte
double domyosrower::GetSpeedFromPacket(const QByteArray &packet) {
uint16_t convertedData = (packet.at(6) << 8) | packet.at(7);
uint16_t convertedData = (packet.at(6) << 8) | ((uint8_t)packet.at(7));
if (convertedData > 65000 || convertedData == 0 || currentCadence().value() == 0)
return 0;
return (60.0 / (double)(convertedData)) * 30.0;
@@ -657,7 +657,7 @@ double domyosrower::GetKcalFromPacket(const QByteArray &packet) {
double domyosrower::GetDistanceFromPacket(const QByteArray &packet) {
uint16_t convertedData = (packet.at(12) << 8) | packet.at(13);
uint16_t convertedData = (packet.at(12) << 8) | ((uint8_t)packet.at(13));
double data = ((double)convertedData) / 10.0f;
return data;
}

View File

@@ -1477,6 +1477,29 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
if (gattWriteCharControlPointId.isValid()) {
qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' ');
// D500V2 workaround: track request control (0x00) and start simulation (0x07) commands
// If we receive simulation params (0x11) without start simulation, inject it first
if (D500V2 && b.length() > 0) {
uint8_t commandCode = (uint8_t)b.at(0);
if (commandCode == FTMS_REQUEST_CONTROL) {
// Command 0x00: Request Control - expect start simulation next
awaiting_start_simulation_after_request_control = true;
qDebug() << "D500V2 workaround: received REQUEST_CONTROL (0x00), now awaiting START_RESUME (0x07)";
} else if (commandCode == FTMS_START_RESUME) {
// Command 0x07: Start Resume - no longer awaiting
awaiting_start_simulation_after_request_control = false;
qDebug() << "D500V2 workaround: received START_RESUME (0x07), ready for simulation params";
} else if (commandCode == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && D500V2 && awaiting_start_simulation_after_request_control) {
// Command 0x11: Set Simulation Params - but we're still awaiting start simulation
// For D500V2, inject the start simulation command (0x07) first
qDebug() << "D500V2 workaround: received SET_INDOOR_BIKE_SIMULATION_PARAMS (0x11) without START_RESUME, injecting 0x07 first";
uint8_t startSimulation[] = {FTMS_START_RESUME};
writeCharacteristic(startSimulation, sizeof(startSimulation), "injectWrite [D500V2 workaround: start simulation 0x07]", false, true);
awaiting_start_simulation_after_request_control = false;
}
}
// handling gears
if (b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && (zwiftPlayService == nullptr || !gears_zwift_ratio)) {
double min_inclination = settings.value(QZSettings::min_inclination, QZSettings::default_min_inclination).toDouble();
@@ -1551,11 +1574,12 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
// handling watt gain and offset for erg mode
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
double bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toDouble();
if (watt_gain != 1.0 || watt_offset != 0) {
if (watt_gain != 1.0 || watt_offset != 0 || bike_power_offset != 0) {
uint16_t powerRequested = (((uint8_t)b.at(1)) + (b.at(2) << 8));
qDebug() << "applying watt_gain/watt_offset from" << powerRequested;
powerRequested = ((powerRequested / watt_gain) - watt_offset);
powerRequested = ((powerRequested / watt_gain) - watt_offset + bike_power_offset);
qDebug() << "to" << powerRequested;
b[1] = powerRequested & 0xFF;
@@ -1683,6 +1707,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
ergModeSupported = false;
max_resistance = 32;
DOMYOS = true;
} else if (bluetoothDevice.name().toUpper().startsWith("D500V2")) {
qDebug() << QStringLiteral("D500V2 found - enabling workaround for start simulation command");
D500V2 = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
qDebug() << QStringLiteral("_3G_Cardio_RB found");
_3G_Cardio_RB = true;

View File

@@ -135,9 +135,13 @@ class ftmsbike : public bike {
bool resistance_received = false;
inclinationResistanceTable _inclinationResistanceTable;
// D500V2 workaround: track if we're awaiting start simulation command after request control
bool awaiting_start_simulation_after_request_control = false;
bool DU30_bike = false;
bool ICSE = false;
bool DOMYOS = false;
bool D500V2 = false;
bool _3G_Cardio_RB = false;
bool SCH_190U = false;
bool SCH_290R = false;

View File

@@ -1261,7 +1261,7 @@ void horizontreadmill::forceSpeed(double requestSpeed) {
if(BOWFLEX_T9) {
requestSpeed *= miles_conversion; // this treadmill wants the speed in miles, at least seems so!!
}
if(TM4800 || TM6500 || T3G_ELITE) {
if(TM4800 || TM6500 || T3G_ELITE || WT_TREADMILL) {
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
if(miles) {
requestSpeed *= miles_conversion; // these treadmills want the speed in miles when miles_unit is enabled
@@ -1918,7 +1918,10 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
if (Flags.inclination) {
if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill && !T01)
if(domyos_treadmill_ts100) {
// Domyos TS100 has a fixed 15° inclination
Inclination = 15;
} else if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill && !T01)
parseInclination(treadmillInclinationOverride((double)(
(int16_t)(
((int16_t)(int8_t)newValue.at(index + 1) << 8) |
@@ -1965,6 +1968,10 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
}
index += 4; // the ramo value is useless
emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value()));
} else if(domyos_treadmill_ts100) {
// Domyos TS100 has a fixed 15° inclination (no inclination flag in 2ACD)
Inclination = 15;
emit debug(QStringLiteral("Current Inclination (TS100 fixed): ") + QString::number(Inclination.value()));
}
if (Flags.elevation) {
@@ -2656,6 +2663,10 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((device.name().toUpper().startsWith("DOMYOS"))) {
qDebug() << QStringLiteral("DOMYOS found");
DOMYOS = true;
domyos_treadmill_ts100 = settings.value(QZSettings::domyos_treadmill_ts100, QZSettings::default_domyos_treadmill_ts100).toBool();
if(domyos_treadmill_ts100) {
qDebug() << QStringLiteral("Domyos TS100 mode ON - Fixed 15° inclination");
}
} else if ((device.name().toUpper().startsWith(QStringLiteral("BFX_T9_")))) {
qDebug() << QStringLiteral("BOWFLEX T9 found");
BOWFLEX_T9 = true;
@@ -2687,6 +2698,9 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
qDebug() << QStringLiteral("TM6500 treadmill found");
TM6500 = true;
minInclination = -3.0;
} else if (device.name().toUpper().startsWith(QStringLiteral("WT")) && device.name().length() == 5) {
qDebug() << QStringLiteral("WT treadmill found");
WT_TREADMILL = true;
}
if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) {

View File

@@ -106,6 +106,7 @@ class horizontreadmill : public treadmill {
bool ICONCEPT_FTMS_treadmill = false;
bool iconcept_ftms_treadmill_inclination_table = false;
bool DOMYOS = false;
bool domyos_treadmill_ts100 = false;
bool SW_TREADMILL = false;
bool BOWFLEX_T9 = false;
bool YPOO_MINI_PRO = false;
@@ -118,6 +119,7 @@ class horizontreadmill : public treadmill {
bool T01 = false;
bool TM4800 = false;
bool TM6500 = false;
bool WT_TREADMILL = false;
void testProfileCRC();
void updateProfileCRC();

View File

@@ -0,0 +1,353 @@
#include "mobirower.h"
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#endif
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualrower.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <chrono>
#include <math.h>
using namespace std::chrono_literals;
#ifdef Q_OS_IOS
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
#endif
mobirower::mobirower(bool noWriteResistance, bool noHeartService) {
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
initDone = false;
connect(refresh, &QTimer::timeout, this, &mobirower::update);
refresh->start(200ms);
}
void mobirower::update() {
if (m_control == nullptr)
return;
if (m_control->state() == QLowEnergyController::UnconnectedState) {
emit disconnected();
return;
}
if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService && gattNotifyCharacteristic.isValid() && initDone) {
update_metrics(true, watts());
if (requestStart != -1) {
qDebug() << QStringLiteral("starting...");
requestStart = -1;
emit bikeStarted();
}
if (requestStop != -1) {
qDebug() << QStringLiteral("stopping...");
requestStop = -1;
}
}
}
void mobirower::serviceDiscovered(const QBluetoothUuid &gatt) {
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
}
void mobirower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
Q_UNUSED(characteristic);
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
// Validate packet: 13 bytes, starts with 0xab 0x04
if (newValue.length() < 13 ||
(uint8_t)newValue.at(0) != 0xab ||
(uint8_t)newValue.at(1) != 0x04) {
qDebug() << QStringLiteral("Invalid packet format");
return;
}
// Parse power from bytes 9-10 (big-endian uint16)
uint16_t power = ((uint8_t)newValue.at(9) << 8) | (uint8_t)newValue.at(10);
// Parse stroke count from bytes 11-12 (big-endian uint16)
uint16_t strokeCount = ((uint8_t)newValue.at(11) << 8) | (uint8_t)newValue.at(12);
// Calculate cadence from stroke delta
double timeDelta = lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime());
if (timeDelta > 0 && strokeCount >= lastStrokeCount) {
uint16_t strokeDelta = strokeCount - lastStrokeCount;
// Convert to strokes per minute (SPM)
double cadence = (strokeDelta / (timeDelta / 60000.0));
if (cadence < 200) { // sanity check
Cadence = cadence;
}
}
lastStrokeCount = strokeCount;
m_watt = power;
StrokesCount = strokeCount;
// Calculate speed from strokes (standard rower formula)
// Using a simplified formula: speed in km/h derived from cadence
if (Cadence.value() > 0) {
// Typical rower: ~10m per stroke at normal pace
// Speed = (cadence * meters_per_stroke * 60) / 1000 for km/h
double metersPerStroke = 8.0; // approximate
Speed = (Cadence.value() * metersPerStroke * 60.0) / 1000.0;
} else {
Speed = 0;
}
StrokesLength =
((Speed.value() / 60.0) * 1000.0) /
Cadence.value(); // this is just to fill the tile
if (watts())
KCal +=
((((0.048 * ((double)watts()) + 1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
QDateTime::currentDateTime()))));
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
Heart = (uint8_t)KeepAwakeHelper::heart();
else
#endif
{
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
}
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
if (ios_peloton_workaround && cadence && !virtual_device_rower && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
qDebug() << QStringLiteral("Current Power: ") + QString::number(m_watt.value());
qDebug() << QStringLiteral("Current Stroke Count: ") + QString::number(StrokesCount.value());
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value());
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts());
if (m_control->error() != QLowEnergyController::NoError) {
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
}
}
void mobirower::stateChanged(QLowEnergyService::ServiceState state) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
if (state == QLowEnergyService::ServiceDiscovered) {
// Find the notify characteristic (0xffe4)
QBluetoothUuid notifyCharUuid((quint16)0xffe4);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(notifyCharUuid);
if (!gattNotifyCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattNotifyCharacteristic not valid, trying to find by properties");
auto characteristics_list = gattCommunicationChannelService->characteristics();
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
qDebug() << QStringLiteral("c -> ") << c.uuid() << c.properties();
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
gattNotifyCharacteristic = c;
break;
}
}
}
if (!gattNotifyCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattNotifyCharacteristic still not valid");
return;
}
// establish hook into notifications
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
&mobirower::characteristicChanged);
connect(gattCommunicationChannelService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &mobirower::errorService);
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
&mobirower::descriptorWritten);
// ******************************************* virtual bike/rower init *************************************
if (!firstStateChanged && !this->hasVirtualDevice()
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
) {
QSettings settings;
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && !virtual_device_rower) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
#endif
#endif
if (virtual_device_enabled) {
if (!virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
firstStateChanged = 1;
// ********************************************************************************************************
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(
gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
void mobirower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
qDebug() << QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ');
initDone = true;
emit connectedAndDiscovered();
}
void mobirower::serviceScanDone(void) {
qDebug() << QStringLiteral("serviceScanDone");
// Service UUID 0xffe0
QBluetoothUuid serviceUuid((quint16)0xffe0);
gattCommunicationChannelService = m_control->createServiceObject(serviceUuid);
if (!gattCommunicationChannelService) {
qDebug() << "service 0xffe0 not found, trying to find any service";
auto services = m_control->services();
for (const QBluetoothUuid &s : qAsConst(services)) {
qDebug() << QStringLiteral("service ") << s.toString();
}
if (!services.isEmpty()) {
gattCommunicationChannelService = m_control->createServiceObject(services.first());
}
}
if (!gattCommunicationChannelService) {
qDebug() << "no service found";
return;
}
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &mobirower::stateChanged);
gattCommunicationChannelService->discoverDetails();
}
void mobirower::errorService(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
qDebug() << QStringLiteral("mobirower::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString();
}
void mobirower::error(QLowEnergyController::Error err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
qDebug() << "mobirower::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString();
}
void mobirower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
qDebug() << "Found new device: " + device.name() + " (" + device.address().toString() + ')';
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &mobirower::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &mobirower::serviceScanDone);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, &mobirower::error);
connect(m_control, &QLowEnergyController::stateChanged, this, &mobirower::controllerStateChanged);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
qDebug() << QStringLiteral("Cannot connect to remote device.");
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
qDebug() << QStringLiteral("Controller connected. Search services...");
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
qDebug() << QStringLiteral("LowEnergy controller disconnected");
emit disconnected();
});
// Connect
m_control->connectToDevice();
return;
}
bool mobirower::connected() {
if (!m_control) {
return false;
}
return m_control->state() == QLowEnergyController::DiscoveredState;
}
uint16_t mobirower::watts() {
return m_watt.value();
}
void mobirower::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;
if (state == QLowEnergyController::UnconnectedState && m_control) {
qDebug() << QStringLiteral("trying to connect back again...");
initDone = false;
m_control->connectToDevice();
}
}

View File

@@ -0,0 +1,82 @@
#ifndef MOBIROWER_H
#define MOBIROWER_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QDateTime>
#include <QObject>
#include <QString>
#include "rower.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class mobirower : public rower {
Q_OBJECT
public:
mobirower(bool noWriteResistance, bool noHeartService);
bool connected() override;
private:
void startDiscover();
uint16_t watts() override;
QTimer *refresh;
QLowEnergyService *gattCommunicationChannelService = nullptr;
QLowEnergyCharacteristic gattNotifyCharacteristic;
uint8_t firstStateChanged = 0;
uint16_t lastStrokeCount = 0;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
bool initDone = false;
bool noWriteResistance = false;
bool noHeartService = false;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif
Q_SIGNALS:
void disconnected();
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // MOBIROWER_H

View File

@@ -654,7 +654,16 @@ void strydrunpowersensor::serviceScanDone(void) {
emit debug(QStringLiteral("serviceScanDone"));
auto services_list = m_control->services();
bool isZwiftPod = bluetoothDevice.name().contains(QStringLiteral("Zwift RunPod"), Qt::CaseInsensitive);
for (const QBluetoothUuid &s : qAsConst(services_list)) {
// For Zwift RunPod, skip both fff0 and ffc0 services that cause discovery issues
if (isZwiftPod && (s.toString() == QStringLiteral("{0000fff0-0000-1000-8000-00805f9b34fb}") ||
s.toString() == QStringLiteral("{f000ffc0-0451-4000-b000-000000000000}"))) {
qDebug() << QStringLiteral("Skipping problematic services for Zwift RunPod:") << s.toString();
continue;
}
gattCommunicationChannelService.append(m_control->createServiceObject(s));
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
&strydrunpowersensor::stateChanged);

View File

@@ -203,6 +203,7 @@ class homeform : public QObject {
Q_PROPERTY(QString previewWorkoutDescription READ previewWorkoutDescription NOTIFY previewWorkoutDescriptionChanged)
Q_PROPERTY(QString previewWorkoutTags READ previewWorkoutTags NOTIFY previewWorkoutTagsChanged)
Q_PROPERTY(bool miles_unit READ miles_unit)
Q_PROPERTY(bool iPadMultiWindowMode READ iPadMultiWindowMode)
Q_PROPERTY(bool currentCoordinateValid READ currentCoordinateValid)
Q_PROPERTY(bool trainProgramLoadedWithVideo READ trainProgramLoadedWithVideo)
@@ -705,6 +706,18 @@ class homeform : public QObject {
return settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
}
bool iPadMultiWindowMode() {
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
return lockscreen::isInMultiWindowMode();
#else
return false;
#endif
#else
return false;
#endif
}
bool currentCoordinateValid() {
if (bluetoothManager && bluetoothManager->device()) {
return bluetoothManager->device()->currentCordinate().isValid();

View File

@@ -111,6 +111,8 @@ class lockscreen {
static void set_action_profile(const char* profile);
static const char* get_action_profile();
// multi-window detection for iPadOS
static bool isInMultiWindowMode();
};
#endif // LOCKSCREEN_H

View File

@@ -616,13 +616,43 @@ void lockscreen::zwiftClickRemote(const char* Name, const char* UUID, void* devi
void lockscreen::zwiftClickRemote_WriteCharacteristic(unsigned char* qdata, unsigned char length, void* deviceClass) {
if (ios_zwiftClickRemotes == nil) return;
// Get the specific remote for this device
NSValue *key = [NSValue valueWithPointer:deviceClass];
ios_zwiftclickremote *remote = [ios_zwiftClickRemotes objectForKey:key];
if(remote) {
[remote writeCharacteristic:qdata length:length];
}
}
bool lockscreen::isInMultiWindowMode() {
// Check if we're on iPad and in multi-window mode (Stage Manager, Split View, Slide Over)
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) {
return false;
}
if (@available(iOS 13.0, *)) {
// Get the foreground active scene
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive &&
[scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
// Get the window bounds and screen bounds
CGRect windowBounds = windowScene.coordinateSpace.bounds;
CGRect screenBounds = windowScene.screen.bounds;
// If window is smaller than screen in either dimension, we're in multi-window mode
// Add a small tolerance for floating point comparison
if (windowBounds.size.width < screenBounds.size.width - 1 ||
windowBounds.size.height < screenBounds.size.height - 1) {
return true;
}
}
}
}
return false;
}
#endif

View File

@@ -31,20 +31,30 @@ ApplicationWindow {
// Helper functions for cleaner padding calculations
function getTopPadding() {
// Add padding for iPadOS multi-window mode (Stage Manager, Split View, Slide Over)
// to avoid overlap with window control buttons (red/yellow/green)
// Check both the native detection and window size comparison for reactivity
if (Qt.platform.os === "ios") {
var isMultiWindow = (typeof rootItem !== "undefined" && rootItem && rootItem.iPadMultiWindowMode) ||
(window.width < Screen.width - 10); // Window smaller than screen = multi-window
if (isMultiWindow) {
return 15; // Space for window control buttons
}
}
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
AndroidStatusBar.height : AndroidStatusBar.leftInset;
}
function getBottomPadding() {
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
AndroidStatusBar.navigationBarHeight : AndroidStatusBar.rightInset;
}
function getLeftPadding() {
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
return (Screen.orientation === Qt.LandscapeOrientation || Screen.orientation === Qt.InvertedLandscapeOrientation) ?
return (Screen.orientation === Qt.LandscapeOrientation || Screen.orientation === Qt.InvertedLandscapeOrientation) ?
AndroidStatusBar.leftInset : 0;
}

View File

@@ -101,6 +101,7 @@ SOURCES += \
$$PWD/devices/pitpatbike/pitpatbike.cpp \
$$PWD/devices/speraxtreadmill/speraxtreadmill.cpp \
$$PWD/devices/sportsplusrower/sportsplusrower.cpp \
$$PWD/devices/mobirower/mobirower.cpp \
$$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \
$$PWD/devices/sramAXSController/sramAXSController.cpp \
$$PWD/devices/stairclimber.cpp \
@@ -378,6 +379,7 @@ HEADERS += \
$$PWD/devices/pitpatbike/pitpatbike.h \
$$PWD/devices/speraxtreadmill/speraxtreadmill.h \
$$PWD/devices/sportsplusrower/sportsplusrower.h \
$$PWD/devices/mobirower/mobirower.h \
$$PWD/devices/sportstechelliptical/sportstechelliptical.h \
$$PWD/devices/sramAXSController/sramAXSController.h \
$$PWD/devices/stairclimber.h \

View File

@@ -162,6 +162,11 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
double watt_sum = 0;
int watt_count = 0;
// Variables for jump rope cadence
double cadence_sum = 0;
int cadence_count = 0;
uint8_t max_cadence = 0;
for (int i = firstRealIndex; i < session.length(); i++) {
if (session.at(i).coordinate.isValid()) {
gps_data = true;
@@ -204,6 +209,15 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
watt_sum += session.at(i).watt;
watt_count++;
}
// Collect cadence data for jump rope
if (type == JUMPROPE && session.at(i).cadence > 0) {
cadence_sum += session.at(i).cadence;
cadence_count++;
if (session.at(i).cadence > max_cadence) {
max_cadence = session.at(i).cadence;
}
}
}
if (speed_count > 0) {
@@ -448,6 +462,15 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
sessionMesg.SetSubSport(FIT_SUB_SPORT_GENERIC);
if (session.last().stepCount)
sessionMesg.SetJumpCount(session.last().stepCount);
// Total cycles
if (session.last().stepCount)
sessionMesg.SetTotalCycles(session.last().stepCount);
// Avg cadence (jump rate)
if (cadence_count > 0)
sessionMesg.SetAvgCadence((uint8_t)(cadence_sum / cadence_count));
// Max cadence (max jump rate)
if (max_cadence > 0)
sessionMesg.SetMaxCadence(max_cadence);
} else {
sessionMesg.SetSport(FIT_SPORT_CYCLING);

View File

@@ -769,6 +769,7 @@ const QString QZSettings::domyos_treadmill_button_16kmh = QStringLiteral("domyos
const QString QZSettings::domyos_treadmill_button_22kmh = QStringLiteral("domyos_treadmill_button_22kmh");
const QString QZSettings::proform_treadmill_sport_8_5 = QStringLiteral("proform_treadmill_sport_8_5");
const QString QZSettings::domyos_treadmill_t900a = QStringLiteral("domyos_treadmill_t900a");
const QString QZSettings::domyos_treadmill_ts100 = QStringLiteral("domyos_treadmill_ts100");
const QString QZSettings::domyos_treadmill_sync_start = QStringLiteral("domyos_treadmill_sync_start");
const QString QZSettings::enerfit_SPX_9500 = QStringLiteral("enerfit_SPX_9500");
const QString QZSettings::proform_treadmill_505_cst = QStringLiteral("proform_treadmill_505_cst");
@@ -1049,7 +1050,7 @@ const QString QZSettings::trainprogram_auto_lap_on_segment = QStringLiteral("tra
const QString QZSettings::kingsmith_r2_enable_hw_buttons = QStringLiteral("kingsmith_r2_enable_hw_buttons");
const uint32_t allSettingsCount = 855;
const uint32_t allSettingsCount = 856;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1688,6 +1689,7 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::domyos_treadmill_button_22kmh, QZSettings::default_domyos_treadmill_button_22kmh},
{QZSettings::proform_treadmill_sport_8_5, QZSettings::default_proform_treadmill_sport_8_5},
{QZSettings::domyos_treadmill_t900a, QZSettings::default_domyos_treadmill_t900a},
{QZSettings::domyos_treadmill_ts100, QZSettings::default_domyos_treadmill_ts100},
{QZSettings::domyos_treadmill_sync_start, QZSettings::default_domyos_treadmill_sync_start},
{QZSettings::enerfit_SPX_9500, QZSettings::default_enerfit_SPX_9500},
{QZSettings::proform_treadmill_505_cst, QZSettings::default_proform_treadmill_505_cst},

View File

@@ -2122,6 +2122,9 @@ class QZSettings {
static const QString domyos_treadmill_t900a;
static constexpr bool default_domyos_treadmill_t900a = false;
static const QString domyos_treadmill_ts100;
static constexpr bool default_domyos_treadmill_ts100 = false;
static const QString domyos_treadmill_sync_start;
static constexpr bool default_domyos_treadmill_sync_start = false;

View File

@@ -1272,6 +1272,7 @@ import Qt.labs.platform 1.1
property bool technogym_bike: false
property bool kingsmith_r2_enable_hw_buttons: false
property bool treadmill_direct_distance: false
property bool domyos_treadmill_ts100: false
}
@@ -9067,6 +9068,20 @@ import Qt.labs.platform 1.1
onClicked: settings.domyos_treadmill_t900a = checked
}
IndicatorOnlySwitch {
text: qsTr("TS100 (Fixed 15° Inclination)")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.domyos_treadmill_ts100
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.domyos_treadmill_ts100 = checked
}
IndicatorOnlySwitch {
text: qsTr("Sync Start (Old Behavior)")
spacing: 0
@@ -9720,33 +9735,33 @@ import Qt.labs.platform 1.1
}
}
}
}
}
AccordionElement {
id: bowflexTreadmillAccordion
title: qsTr("Bowflex Treadmill Options")
indicatRectColor: Material.color(Material.Grey)
textColor: Material.color(Material.Yellow)
color: Material.backgroundColor
accordionContent: ColumnLayout {
spacing: 0
IndicatorOnlySwitch {
id: bowflexT9MilesDelegate
text: qsTr("T9 mi/h speed")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.fitshow_treadmill_miles
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.fitshow_treadmill_miles = checked
}
AccordionElement {
id: bowflexTreadmillAccordion
title: qsTr("Bowflex Treadmill Options")
indicatRectColor: Material.color(Material.Grey)
textColor: Material.color(Material.Yellow)
color: Material.backgroundColor
accordionContent: ColumnLayout {
spacing: 0
IndicatorOnlySwitch {
id: bowflexT9MilesDelegate
text: qsTr("T9 mi/h speed")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.fitshow_treadmill_miles
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.fitshow_treadmill_miles = checked
}
}
}
}
}
}
AccordionElement {
id: toorxTreadmillAccordion