mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 23:41:50 +01:00
Compare commits
14 Commits
build-1270
...
Mobi-Rower
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba867a1b5 | ||
|
|
72f57053a7 | ||
|
|
13ea5313b1 | ||
|
|
7f694733b2 | ||
|
|
b1755c004a | ||
|
|
360ab66431 | ||
|
|
04b659a91f | ||
|
|
9487fa3cb4 | ||
|
|
1ac2149424 | ||
|
|
28558697b2 | ||
|
|
6918fb9eba | ||
|
|
365abbb7cb | ||
|
|
d3f52682cc | ||
|
|
da4f360f63 |
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"))) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
353
src/devices/mobirower/mobirower.cpp
Normal file
353
src/devices/mobirower/mobirower.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
82
src/devices/mobirower/mobirower.h
Normal file
82
src/devices/mobirower/mobirower.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
20
src/main.qml
20
src/main.qml
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
23
src/qfit.cpp
23
src/qfit.cpp
@@ -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);
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user