Compare commits

...

7 Commits

Author SHA1 Message Date
Roberto Viola
4d0c5a5cb8 Update project.pbxproj 2025-07-04 12:03:24 +02:00
Roberto Viola
f4d161867b Add inclination support to lockscreen and Bluetooth device
Introduces setInclination method to the lockscreen class and updates bluetoothdevice to set inclination from external sources. This enhances the data synchronization by including inclination values alongside speed, power, cadence, and steps.
2025-07-04 11:59:08 +02:00
Roberto Viola
a3654095f7 Refactor speed and inclination variables for TCP data
Introduces tcpSpeed and tcpInclination static variables in WatchKitConnection to distinguish TCP-received values from local ones. Updates references in AppDelegate and Connection to use the new variables for TCP data handling.
2025-07-04 11:43:37 +02:00
Roberto Viola
f2bfb0e7b6 Add device UUID to treadmill data sync protocol
Introduces a unique device UUID to messages exchanged between devices to prevent processing echoed treadmill data. Updates both C++ and Swift code to include and check the UUID in message payloads, ensuring that treadmill speed and inclination updates are only processed if the message originates from a different device.
2025-07-04 11:19:49 +02:00
Roberto Viola
dbb4b57ee9 Update faketreadmill.cpp 2025-07-04 11:10:33 +02:00
Roberto Viola
05e64940c3 Add TCP speed and inclination integration for iOS
Introduces methods to access TCP speed and inclination data from iOS via lockscreen and WatchKitConnection. Updates faketreadmill to use these values if available, allowing for dynamic updates from external sources.
2025-07-04 11:03:43 +02:00
Roberto Viola
c4c380bc4a Add treadmill inclination sync between iOS and desktop
Introduces support for sending and receiving treadmill inclination data between the iOS app and the desktop application. Adds an 'inclination' property to WatchKitConnection, updates message formats to include inclination, and refactors TCP message handling to process and apply inclination updates for fake treadmill devices. Also adds code style rules to documentation.
2025-07-04 10:24:28 +02:00
11 changed files with 176 additions and 31 deletions

View File

@@ -371,4 +371,9 @@ The ProForm 995i implementation serves as the reference example:
## Additional Memories
- When adding a new setting in QML (setting-tiles.qml), you must:
* Add the property at the END of the properties list
* Add the property at the END of the properties list
## Code Style Rules
- All comments MUST be written in English only
- Avoid code duplication - unify repeated logic into shared functions or sections when possible

View File

@@ -4381,7 +4381,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1117;
CURRENT_PROJECT_VERSION = 1118;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4575,7 +4575,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1117;
CURRENT_PROJECT_VERSION = 1118;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4805,7 +4805,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1117;
CURRENT_PROJECT_VERSION = 1118;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4901,7 +4901,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1117;
CURRENT_PROJECT_VERSION = 1118;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -4993,7 +4993,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1117;
CURRENT_PROJECT_VERSION = 1118;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5109,7 +5109,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1117;
CURRENT_PROJECT_VERSION = 1118;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

View File

@@ -260,6 +260,7 @@ void bluetoothdevice::update_hr_from_external() {
h.setPower(m_watt.value());
h.setCadence(Cadence.value());
h.setSteps(StepCount.value());
h.setInclination(Inclination.value());
Heart = appleWatchHeartRate;
qDebug() << "Current Heart from Apple Watch: " << QString::number(appleWatchHeartRate);
#endif

View File

@@ -11,6 +11,9 @@
#include "keepawakehelper.h"
#include <QLowEnergyConnectionParameters>
#endif
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
#include <chrono>
@@ -85,6 +88,15 @@ void faketreadmill::update() {
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
}
}
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
// Initialize lockscreen for iOS TCP data access
if (!h) {
h = new lockscreen();
h->virtualtreadmill_zwift_ios(false);
}
#endif
#endif
if (!firstStateChanged)
emit connectedAndDiscovered();
firstStateChanged = 1;
@@ -112,6 +124,19 @@ void faketreadmill::update() {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
// Update from TCP data if available
if (h && firstStateChanged) {
double tcpSpeed = h->getTcpSpeed();
double tcpInclination = h->getTcpInclination();
if (tcpSpeed > -100) {
Speed = tcpSpeed;
}
if (tcpInclination > -100) {
Inclination = tcpInclination;
}
}
#endif
#endif
}

View File

@@ -38,6 +38,7 @@
#include <QStandardPaths>
#include <QTime>
#include <QUrlQuery>
#include <QUuid>
#include <chrono>
homeform *homeform::m_singleton = 0;
@@ -682,6 +683,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
}
#ifndef Q_OS_IOS
deviceUUID = QUuid::createUuid().toString();
iphone_browser = new QMdnsEngine::Browser(&iphone_server, "_qz_iphone._tcp.local.", &iphone_cache);
QObject::connect(iphone_browser, &QMdnsEngine::Browser::serviceAdded, [](const QMdnsEngine::Service &service) {
@@ -705,16 +707,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
[]() { qDebug() << "iphone_socket connected!"; });
QObject::connect(homeform::singleton()->iphone_socket, &QTcpSocket::readyRead, []() {
QString rec = homeform::singleton()->iphone_socket->readAll();
qDebug() << "iphone_socket received << " << rec;
QStringList fields = rec.split("#");
foreach (QString f, fields) {
if (f.contains("HR")) {
QStringList values = f.split("=");
if (values.length() > 1) {
emit homeform::singleton()->heartRate(values[1].toDouble());
}
}
}
homeform::singleton()->processTcpMessage(rec);
});
homeform::singleton()->iphone_address = address;
@@ -745,16 +738,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
[]() { qDebug() << "iphone_socket connected!"; });
QObject::connect(homeform::singleton()->iphone_socket, &QTcpSocket::readyRead, []() {
QString rec = homeform::singleton()->iphone_socket->readAll();
qDebug() << "iphone_socket received << " << rec;
QStringList fields = rec.split("#");
foreach (QString f, fields) {
if (f.contains("HR")) {
QStringList values = f.split("=");
if (values.length() > 1) {
emit homeform::singleton()->heartRate(values[1].toDouble());
}
}
}
homeform::singleton()->processTcpMessage(rec);
});
homeform::singleton()->iphone_address = address;
homeform::singleton()->iphone_socket->connectToHost(
@@ -6967,13 +6951,24 @@ void homeform::update() {
#ifndef Q_OS_IOS
if (iphone_socket && iphone_socket->state() == QAbstractSocket::ConnectedState) {
QString toSend =
"SENDER=PAD#HR=" + QString::number(bluetoothManager->device()->currentHeart().value()) +
"SENDER=PAD#UUID=" + deviceUUID +
"#HR=" + QString::number(bluetoothManager->device()->currentHeart().value()) +
"#KCAL=" + QString::number(bluetoothManager->device()->calories().value()) +
"#BCAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) +
"#SPD=" + QString::number(bluetoothManager->device()->currentSpeed().value()) +
"#PWR=" + QString::number(bluetoothManager->device()->wattsMetric().value()) +
"#CAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) +
"#ODO=" + QString::number(bluetoothManager->device()->odometer()) + "#";
"#ODO=" + QString::number(bluetoothManager->device()->odometer());
// Aggiungi inclinazione se il device connesso è un treadmill
if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) {
treadmill* t = qobject_cast<treadmill*>(bluetoothManager->device());
if (t) {
toSend += "#INCL=" + QString::number(t->currentInclination().value());
}
}
toSend += "#";
int write = iphone_socket->write(toSend.toLocal8Bit(), toSend.length());
qDebug() << "iphone_socket send " << write << toSend;
}
@@ -6998,6 +6993,59 @@ bool homeform::getDevice() {
return this->bluetoothManager->device()->connected();
}
#ifndef Q_OS_IOS
void homeform::processTcpMessage(const QString& message) {
qDebug() << "iphone_socket received << " << message;
QStringList fields = message.split("#");
bool hasTreadmillData = false;
double speed = 0.0;
double incline = 0.0;
QString remoteUUID;
foreach (QString f, fields) {
if (f.contains("HR")) {
QStringList values = f.split("=");
if (values.length() > 1) {
emit heartRate(values[1].toDouble());
}
} else if (f.contains("UUID=")) {
QStringList values = f.split("=");
if (values.length() > 1) {
remoteUUID = values[1];
}
} else if (f.contains("SPD=")) {
QStringList values = f.split("=");
if (values.length() > 1) {
speed = values[1].toDouble();
}
} else if (f.contains("INCL=")) {
QStringList values = f.split("=");
if (values.length() > 1) {
incline = values[1].toDouble();
hasTreadmillData = true;
}
}
}
// Process treadmill data only if UUID is different (avoid echo)
if (hasTreadmillData && !remoteUUID.isEmpty() && remoteUUID != deviceUUID && bluetoothManager) {
bluetooth* bt = bluetoothManager;
if (bt->device()) {
faketreadmill* ft = qobject_cast<faketreadmill*>(bt->device());
if (ft) {
// Update speed and incline of fake treadmill
if (speed > -100) {
ft->changeSpeed(speed);
}
if (incline > -100) {
ft->changeInclination(incline, incline);
}
}
}
}
}
#endif
bool homeform::getLap() {
if (!this->bluetoothManager->device()) {

View File

@@ -823,6 +823,8 @@ class homeform : public QObject {
QTcpSocket *iphone_socket = nullptr;
QMdnsEngine::Service iphone_service;
QHostAddress iphone_address;
void processTcpMessage(const QString& message);
QString deviceUUID;
#endif
public slots:

View File

@@ -55,6 +55,16 @@ var pedometer = CMPedometer()
{
return WatchKitConnection.stepCadence;
}
@objc public func getTcpSpeed() -> Double
{
return WatchKitConnection.tcpSpeed;
}
@objc public func getTcpInclination() -> Double
{
return WatchKitConnection.tcpInclination;
}
@objc public func setSteps(steps: Int) -> Void
{
@@ -128,8 +138,20 @@ var pedometer = CMPedometer()
Server.server?.send(createString(sender: sender))
}
@objc public func setInclination(inclination: Double) -> Void
{
var sender: String
if UIDevice.current.userInterfaceIdiom == .pad {
sender = "PAD"
} else {
sender = "PHONE"
}
WatchKitConnection.inclination = inclination;
Server.server?.send(createString(sender: sender))
}
func createString(sender: String) -> String {
return "SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(WatchKitConnection.kcal)#BCAD=\(WatchKitConnection.cadence)#SPD=\(WatchKitConnection.speed)#PWR=\(WatchKitConnection.power)#CAD=\(WatchKitConnection.stepCadence)#ODO=\(WatchKitConnection.distance)#";
return "SENDER=\(sender)#UUID=\(WatchKitConnection.deviceUUID)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(WatchKitConnection.kcal)#BCAD=\(WatchKitConnection.cadence)#SPD=\(WatchKitConnection.speed)#PWR=\(WatchKitConnection.power)#CAD=\(WatchKitConnection.stepCadence)#ODO=\(WatchKitConnection.distance)#INCL=\(WatchKitConnection.inclination)#";
}
@objc func updateHeartRate() {

View File

@@ -112,6 +112,27 @@ class Connection {
if sender?.contains("PAD") ?? false && message.contains("PWR=") {
let pwr : String = message.slice(from: "PWR=", to: "#") ?? ""
WatchKitConnection.power = (Double(pwr) ?? WatchKitConnection.power)
}
if sender?.contains("PAD") ?? false && message.contains("INCL=") {
let incl : String = message.slice(from: "INCL=", to: "#") ?? ""
WatchKitConnection.inclination = (Double(incl) ?? WatchKitConnection.inclination)
}
// Handle treadmill data from any sender except self (avoid echo)
if message.contains("UUID=") {
let remoteUUID = message.slice(from: "UUID=", to: "#") ?? ""
if remoteUUID != WatchKitConnection.deviceUUID {
// Process speed data from remote device
if message.contains("SPD=") {
let spd : String = message.slice(from: "SPD=", to: "#") ?? ""
WatchKitConnection.tcpSpeed = (Double(spd) ?? WatchKitConnection.tcpSpeed)
}
// Process inclination data from remote device
if message.contains("INCL=") {
let incl : String = message.slice(from: "INCL=", to: "#") ?? ""
WatchKitConnection.tcpInclination = (Double(incl) ?? WatchKitConnection.tcpInclination)
}
}
}
}
}

View File

@@ -26,10 +26,14 @@ class WatchKitConnection: NSObject {
static var distance = 0.0
static var stepCadence = 0
static var kcal = 0.0
static var speed = 0.0
static var speed = -100.0
static var power = 0.0
static var cadence = 0.0
static var steps = 0
static var inclination = -100.0
static var deviceUUID = UUID().uuidString
static var tcpSpeed = -100.0
static var tcpInclination = -100.0
private override init() {
super.init()

View File

@@ -15,6 +15,7 @@ class lockscreen {
void setSpeed(double speed);
void setPower(double power);
void setCadence(double cadence);
void setInclination(double inclination);
// virtualbike
void virtualbike_ios();
@@ -97,6 +98,10 @@ class lockscreen {
// Zwift Hub Protobuf
static QByteArray zwift_hub_inclinationCommand(double inclination);
// TCP Data Access
double getTcpSpeed();
double getTcpInclination();
static QByteArray zwift_hub_setGearsCommand(unsigned int gears);
static uint32_t zwift_hub_getPowerFromBuffer(const QByteArray& buffer);
static uint32_t zwift_hub_getCadenceFromBuffer(const QByteArray& buffer);

View File

@@ -89,6 +89,10 @@ void lockscreen::setSpeed(double speed)
{
[h setSpeedWithSpeed:speed];
}
void lockscreen::setInclination(double inclination)
{
[h setInclinationWithInclination:inclination];
}
void lockscreen::virtualbike_ios()
@@ -385,6 +389,14 @@ QByteArray lockscreen::zwift_hub_inclinationCommand(double inclination) {
}
}
double lockscreen::getTcpSpeed() {
return [h getTcpSpeed];
}
double lockscreen::getTcpInclination() {
return [h getTcpInclination];
}
QByteArray lockscreen::zwift_hub_setGearsCommand(unsigned int gears) {
NSError *error = nil;
NSData *command = [ZwiftHubBike setGearCommandWithGears:gears error:&error];