Compare commits

...

21 Commits

Author SHA1 Message Date
Roberto Viola
30efd418fe Update project.pbxproj 2025-08-27 11:37:21 +02:00
Roberto Viola
1492924cef Merge branch 'master' into active_calories 2025-08-27 11:36:14 +02:00
Roberto Viola
e18ab1a938 Update project.pbxproj 2025-08-26 14:59:31 +02:00
Roberto Viola
6bc67387e2 Merge branch 'master' into active_calories 2025-08-26 14:57:36 +02:00
Roberto Viola
2366ab25bb Update project.pbxproj 2025-08-26 08:26:43 +02:00
Roberto Viola
676024acb1 Merge branch 'master' into active_calories 2025-08-26 08:22:32 +02:00
Roberto Viola
acf8e13877 build 1149 2025-08-25 10:08:44 +02:00
Roberto Viola
8c74150b8b Add option to calculate calories from heart rate
Introduces a new setting to calculate calories based on heart rate data instead of power. Updates the bluetoothdevice logic to support HR-based calorie calculation, adds a new metric for HR calories, and exposes the option in the settings UI. Also updates QZSettings to include the new configuration key and default.
2025-08-25 09:30:19 +02:00
Roberto Viola
9e14cc9912 Update project.pbxproj 2025-08-24 07:01:31 +02:00
Roberto Viola
d510d44657 Merge branch 'master' into active_calories 2025-08-24 06:43:37 +02:00
Roberto Viola
c8a76ab9c7 build 1145 2025-08-23 08:45:13 +02:00
Roberto Viola
aa5241e3d1 fixing 2025-08-22 07:58:08 +02:00
Roberto Viola
db22ee0d9b removing basal 2025-08-20 16:23:18 +02:00
Roberto Viola
2321838f46 Update project.pbxproj 2025-08-20 15:52:56 +02:00
Roberto Viola
37aab5ab8d adding something for debug 2025-08-20 15:46:59 +02:00
Roberto Viola
fdc83d812d Merge branch 'active_calories' of https://github.com/cagnulein/qdomyos-zwift into active_calories 2025-08-20 15:46:17 +02:00
Roberto Viola
4284dcb6b4 apex bike cadence updated 2025-08-20 15:46:08 +02:00
Roberto Viola
542245696d Merge branch 'active_calories' of https://github.com/cagnulein/qdomyos-zwift into active_calories 2025-08-20 15:40:19 +02:00
Roberto Viola
97bd9f03e5 watchkit 2025-08-20 15:40:15 +02:00
Roberto Viola
9e3b307d01 Update AppDelegate.swift 2025-08-20 14:48:04 +02:00
Roberto Viola
a1b313081e first commit 2025-08-20 14:31:28 +02:00
17 changed files with 365 additions and 48 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
src/qdomyos-zwift.pro.user
.idea/
src/Makefile

View File

@@ -4445,7 +4445,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1147;
CURRENT_PROJECT_VERSION = 1152;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4641,7 +4641,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1147;
CURRENT_PROJECT_VERSION = 1152;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4873,7 +4873,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1147;
CURRENT_PROJECT_VERSION = 1152;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4969,7 +4969,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1147;
CURRENT_PROJECT_VERSION = 1152;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5061,7 +5061,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1147;
CURRENT_PROJECT_VERSION = 1152;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5177,7 +5177,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1147;
CURRENT_PROJECT_VERSION = 1152;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

View File

@@ -23,6 +23,7 @@ class WatchKitConnection: NSObject {
static let shared = WatchKitConnection()
public static var distance = 0.0
public static var kcal = 0.0
public static var totalKcal = 0.0
public static var stepCadence = 0
public static var speed = 0.0
public static var cadence = 0.0
@@ -70,6 +71,9 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.distance = dDistance
let dKcal = Double(result["kcal"] as! Double)
WatchKitConnection.kcal = dKcal
if let totalKcalDouble = result["totalKcal"] as? Double {
WatchKitConnection.totalKcal = totalKcalDouble
}
let dSpeed = Double(result["speed"] as! Double)
WatchKitConnection.speed = dSpeed

View File

@@ -28,6 +28,7 @@ class WorkoutTracking: NSObject {
static let shared = WorkoutTracking()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
public static var cadenceLastSteps = Double()
public static var cadenceSteps = 0
@@ -166,6 +167,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
@@ -185,6 +187,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.workoutType()
])
}
@@ -223,25 +226,30 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
workoutSession.stopActivity(with: Date())
workoutSession.end()
guard let quantityType = HKQuantityType.quantityType(
// Write active calories
guard let activeQuantityType = HKQuantityType.quantityType(
forIdentifier: .activeEnergyBurned) else {
return
}
let unit = HKUnit.kilocalorie()
let totalEnergyBurned = WorkoutTracking.kcal
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let activeEnergyBurned = WorkoutTracking.kcal
let activeQuantity = HKQuantity(unit: unit,
doubleValue: activeEnergyBurned)
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: startDate,
end: Date())
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
workoutBuilder.add([sample]) {(success, error) in}
workoutBuilder.add([activeSample]) {(success, error) in
if let error = error {
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
}
}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance
let quantityMiles = HKQuantity(unit: unitDistance,
@@ -273,6 +281,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}
@@ -334,6 +346,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}
@@ -399,6 +415,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}

View File

@@ -109,7 +109,52 @@ QTime bluetoothdevice::maxPace() {
double bluetoothdevice::odometerFromStartup() { return Distance.valueRaw(); }
double bluetoothdevice::odometer() { return Distance.value(); }
double bluetoothdevice::lapOdometer() { return Distance.lapValue(); }
metric bluetoothdevice::calories() { return KCal; }
metric bluetoothdevice::calories() {
QSettings settings;
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
if (fromHR && Heart.value() > 0) {
// Calculate calories based on heart rate
double totalHRKCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value());
hrKCal.setValue(totalHRKCal);
if (activeOnly) {
activeKCal.setValue(metric::calculateActiveKCal(hrKCal.value(), elapsed.value()));
return activeKCal;
} else {
return hrKCal;
}
} else {
// Power-based calculation (current behavior)
if (activeOnly) {
activeKCal.setValue(metric::calculateActiveKCal(KCal.value(), elapsed.value()));
return activeKCal;
} else {
return KCal;
}
}
}
metric bluetoothdevice::totalCalories() {
QSettings settings;
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
if (fromHR && Heart.value() > 0) {
return hrKCal; // Return HR-based total calories
} else {
return KCal; // Return power-based total calories
}
}
metric bluetoothdevice::activeCalories() {
return activeKCal;
}
metric bluetoothdevice::hrCalories() {
return hrKCal;
}
metric bluetoothdevice::jouls() { return m_jouls; }
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
@@ -254,7 +299,17 @@ void bluetoothdevice::update_hr_from_external() {
#ifndef IO_UNDER_QT
lockscreen h;
long appleWatchHeartRate = h.heartRate();
h.setKcal(KCal.value());
QSettings settings;
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
if (activeOnly) {
// When active calories setting is enabled, send both total and active calories
h.setKcal(calories().value()); // This will be active calories
h.setTotalKcal(totalCalories().value()); // This will be total calories
} else {
// When disabled, send total calories as before
h.setKcal(calories().value()); // This will be total calories
}
h.setDistance(Distance.value());
h.setSpeed(Speed.value());
h.setPower(m_watt.value());
@@ -277,7 +332,7 @@ void bluetoothdevice::update_hr_from_external() {
double kcal = calories().value();
if(kcal < 0)
kcal = 0;
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0);
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value());
#endif
#endif
}
@@ -288,6 +343,8 @@ void bluetoothdevice::clearStats() {
moving.clear(true);
Speed.clear(false);
KCal.clear(true);
hrKCal.clear(true);
activeKCal.clear(true);
Distance.clear(true);
Distance1s.clear(true);
Heart.clear(false);
@@ -313,6 +370,8 @@ void bluetoothdevice::setPaused(bool p) {
elapsed.setPaused(p);
Speed.setPaused(p);
KCal.setPaused(p);
hrKCal.setPaused(p);
activeKCal.setPaused(p);
Distance.setPaused(p);
Distance1s.setPaused(p);
Heart.setPaused(p);
@@ -336,6 +395,8 @@ void bluetoothdevice::setLap() {
elapsed.setLap(true);
Speed.setLap(false);
KCal.setLap(true);
hrKCal.setLap(true);
activeKCal.setLap(true);
Distance.setLap(true);
Distance1s.setLap(true);
Heart.setLap(false);

View File

@@ -108,11 +108,19 @@ class bluetoothdevice : public QObject {
/**
* @brief calories Gets a metric object to get and set the amount of energy expended.
* Default implementation returns the protected KCal property. Units: kcal
* Default implementation returns the protected KCal property, potentially adjusted for active calories. Units: kcal
* Other implementations could have different units.
* @return
*/
virtual metric calories();
virtual metric activeCalories();
virtual metric hrCalories();
/**
* @brief totalCalories Gets total calories (including BMR) regardless of active calories setting.
* @return Total calories metric
*/
virtual metric totalCalories();
/**
* @brief jouls Gets a metric object to get and set the number of joules expended. Units: joules
@@ -548,6 +556,8 @@ class bluetoothdevice : public QObject {
* @brief KCal The number of kilocalories expended in the session. Units: kcal
*/
metric KCal;
metric activeKCal;
metric hrKCal;
/**
* @brief Speed The simulated speed of the device. Units: km/h

View File

@@ -5216,8 +5216,9 @@ void homeform::update() {
QString::number((bluetoothManager->device())->currentSpeed().max() * unit_conversion, 'f', 1));
heart->setValue(QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0));
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
calories->setValue(QString::number(bluetoothManager->device()->calories().value(), 'f', 0));
calories->setSecondLine(QString::number(bluetoothManager->device()->calories().rate1s() * 60.0, 'f', 1) +
calories->setSecondLine(QString::number((activeOnly ? bluetoothManager->device()->activeCalories().rate1s() : bluetoothManager->device()->calories().rate1s()) * 60.0, 'f', 1) +
" /min");
if (!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool())
fan->setValue(QString::number(bluetoothManager->device()->fanSpeed()));
@@ -7260,9 +7261,13 @@ void homeform::update() {
#ifndef Q_OS_IOS
if (iphone_socket && iphone_socket->state() == QAbstractSocket::ConnectedState) {
QSettings mdns_settings;
bool activeOnly = mdns_settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
QString toSend =
"SENDER=PAD#HR=" + QString::number(bluetoothManager->device()->currentHeart().value()) +
"#KCAL=" + QString::number(bluetoothManager->device()->calories().value()) +
(activeOnly ? "#TOTALKCAL=" + QString::number(bluetoothManager->device()->totalCalories().value()) : "") +
"#BCAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) +
"#SPD=" + QString::number(bluetoothManager->device()->currentSpeed().value()) +
"#PWR=" + QString::number(bluetoothManager->device()->wattsMetric().value()) +

View File

@@ -92,6 +92,18 @@ var pedometer = CMPedometer()
Server.server?.send(createString(sender: sender))
}
@objc public func setTotalKcal(totalKcal: Double) -> Void
{
var sender: String
if UIDevice.current.userInterfaceIdiom == .pad {
sender = "PAD"
} else {
sender = "PHONE"
}
WatchKitConnection.totalKcal = totalKcal;
Server.server?.send(createString(sender: sender))
}
@objc public func setCadence(cadence: Double) -> Void
{
var sender: String
@@ -129,7 +141,7 @@ var pedometer = CMPedometer()
}
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)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(WatchKitConnection.kcal)#TOTALKCAL=\(WatchKitConnection.totalKcal)#BCAD=\(WatchKitConnection.cadence)#SPD=\(WatchKitConnection.speed)#PWR=\(WatchKitConnection.power)#CAD=\(WatchKitConnection.stepCadence)#ODO=\(WatchKitConnection.distance)#";
}
@objc func updateHeartRate() {

View File

@@ -26,6 +26,7 @@ class WatchKitConnection: NSObject {
static var distance = 0.0
static var stepCadence = 0
static var kcal = 0.0
static var totalKcal = 0.0
static var speed = 0.0
static var power = 0.0
static var cadence = 0.0
@@ -55,6 +56,11 @@ class WatchKitConnection: NSObject {
WatchKitConnection.kcal = Kcal;
}
public func setTotalKCal(TotalKcal: Double) -> Void
{
WatchKitConnection.totalKcal = TotalKcal;
}
public func setDistance(Distance: Double) -> Void
{
WatchKitConnection.distance = Distance;

View File

@@ -31,6 +31,7 @@ protocol WorkoutTrackingProtocol {
public static var lastDateMetric = Date()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var steps = Double()
var sport: Int = 0
let healthStore = HKHealthStore()
@@ -100,6 +101,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
@@ -119,6 +121,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.workoutType()
])
}
@@ -166,22 +169,27 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
return
}
guard let quantityType = HKQuantityType.quantityType(
// Write active calories
guard let activeQuantityType = HKQuantityType.quantityType(
forIdentifier: .activeEnergyBurned) else {
return
}
let unit = HKUnit.kilocalorie()
let totalEnergyBurned = WorkoutTracking.kcal
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let activeEnergyBurned = WorkoutTracking.kcal
let activeQuantity = HKQuantity(unit: unit,
doubleValue: activeEnergyBurned)
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: startDate,
end: Date())
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
workoutBuilder.add([sample]) {(success, error) in}
workoutBuilder.add([activeSample]) {(success, error) in
if let error = error {
SwiftDebug.qtDebug("WorkoutTracking active calories: " + error.localizedDescription)
}
}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance * 0.000621371
@@ -215,6 +223,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
SwiftDebug.qtDebug("WorkoutTracking: " + error.localizedDescription)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
} else {
@@ -270,14 +282,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
return
}
// No need to manually set values - the builder has added the samples
// and the workout now has steps and distance metrics built in
// You can access the data if needed:
if let workout = workout {
// Here you can use the workout as needed
// The steps and distance are now part of the workout's statistics
}
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
}
}
}
@@ -286,7 +294,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
workoutInProgress = false;
}
@objc func addMetrics(power: Double, cadence: Double, speed: Double, kcal: Double, steps: Double, deviceType: UInt8, distance: Double) {
@objc func addMetrics(power: Double, cadence: Double, speed: Double, kcal: Double, steps: Double, deviceType: UInt8, distance: Double, totalKcal: Double) {
SwiftDebug.qtDebug("WorkoutTracking: GET DATA: \(Date())")
if(workoutInProgress == false && power > 0) {
@@ -298,6 +306,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let Speed = speed / 100;
WorkoutTracking.kcal = kcal
WorkoutTracking.totalKcal = totalKcal
WorkoutTracking.steps = steps
WorkoutTracking.distance = distance

View File

@@ -10,6 +10,7 @@ class lockscreen {
long heartRate();
long stepCadence();
void setKcal(double kcal);
void setTotalKcal(double totalKcal);
void setDistance(double distance);
void setSteps(double steps);
void setSpeed(double speed);
@@ -17,7 +18,7 @@ class lockscreen {
void setCadence(double cadence);
void startWorkout(unsigned short deviceType);
void stopWorkout();
void workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance);
void workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance, double totalKcal);
bool appleWatchAppInstalled();
// virtualbike

View File

@@ -129,6 +129,11 @@ void lockscreen::setKcal(double kcal)
[h setKcalWithKcal:kcal];
}
void lockscreen::setTotalKcal(double totalKcal)
{
[h setTotalKcalWithTotalKcal:totalKcal];
}
void lockscreen::setDistance(double distance)
{
[h setDistanceWithDistance:distance * 0.621371];
@@ -172,9 +177,9 @@ void lockscreen::virtualbike_setCadence(unsigned short crankRevolutions, unsigne
[_virtualbike updateCadenceWithCrankRevolutions:crankRevolutions LastCrankEventTime:lastCrankEventTime];
}
void lockscreen::workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance) {
void lockscreen::workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories, unsigned long long currentSteps, unsigned char deviceType, double currentDistance, double totalKcal) {
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:watt cadence:cadence*2 speed:speed * 100 kcal:currentCalories steps:currentSteps deviceType:deviceType distance:currentDistance];
[workoutTracking addMetricsWithPower:watt cadence:cadence*2 speed:speed * 100 kcal:currentCalories steps:currentSteps deviceType:deviceType distance:currentDistance totalKcal:totalKcal];
}
void lockscreen::virtualbike_zwift_ios(bool disable_hr, bool garmin_bluetooth_compatibility, bool zwift_play_emulator, bool watt_bike_emulator)
@@ -226,7 +231,7 @@ double lockscreen::virtualbike_getPowerRequested()
bool lockscreen::virtualbike_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 CrankRevolutions, UInt16 LastCrankEventTime, signed short Gears, UInt16 currentCalories, UInt32 Distance)
{
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::BIKE distance:Distance];
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::BIKE distance:Distance totalKcal:0];
if(_virtualbike_zwift != nil)
return [_virtualbike_zwift updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt CrankRevolutions:CrankRevolutions LastCrankEventTime:LastCrankEventTime Gears:Gears];
@@ -236,7 +241,7 @@ bool lockscreen::virtualbike_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResi
bool lockscreen::virtualrower_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 CrankRevolutions, UInt16 LastCrankEventTime, UInt16 StrokesCount, UInt32 Distance, UInt16 KCal, UInt16 Pace)
{
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:KCal steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::ROWING distance:Distance];
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:KCal steps:0 deviceType: bluetoothdevice::BLUETOOTH_TYPE::ROWING distance:Distance totalKcal:0];
if(_virtualrower != nil)
return [_virtualrower updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt CrankRevolutions:CrankRevolutions LastCrankEventTime:LastCrankEventTime StrokesCount:StrokesCount Distance:Distance KCal:KCal Pace:Pace];
@@ -301,7 +306,7 @@ double lockscreen::virtualtreadmill_getRequestedSpeed()
bool lockscreen::virtualtreadmill_updateFTMS(UInt16 normalizeSpeed, UInt8 currentResistance, UInt16 currentCadence, UInt16 currentWatt, UInt16 currentInclination, UInt64 currentDistance, unsigned short currentCalories, qint32 currentSteps, unsigned short elapsedSeconds)
{
if(workoutTracking != nil && !appleWatchAppInstalled())
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:currentSteps deviceType:bluetoothdevice::BLUETOOTH_TYPE::TREADMILL distance:currentDistance];
[workoutTracking addMetricsWithPower:currentWatt cadence:currentCadence speed:normalizeSpeed kcal:currentCalories steps:currentSteps deviceType:bluetoothdevice::BLUETOOTH_TYPE::TREADMILL distance:currentDistance totalKcal:0];
if(_virtualtreadmill_zwift != nil)
return [_virtualtreadmill_zwift updateFTMSWithNormalizeSpeed:normalizeSpeed currentCadence:currentCadence currentResistance:currentResistance currentWatt:currentWatt currentInclination:currentInclination currentDistance:currentDistance elapsedTimeSeconds:elapsedSeconds];

View File

@@ -423,3 +423,44 @@ Women:
return (T * ((0.6309 * H) + (0.1988 * W) + (0.2017 * A) - 55.0969) / 4.184);
}
}
double metric::calculateBMR() {
// Calculate Basal Metabolic Rate using Mifflin-St Jeor equation
// BMR (kcal/day) for males: 10 * weight(kg) + 6.25 * height(cm) - 5 * age + 5
// BMR (kcal/day) for females: 10 * weight(kg) + 6.25 * height(cm) - 5 * age - 161
QSettings settings;
QString sex = settings.value(QZSettings::sex, QZSettings::default_sex).toString();
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
double age = settings.value(QZSettings::age, QZSettings::default_age).toDouble();
double height = settings.value(QZSettings::height, QZSettings::default_height).toDouble();
// Full Mifflin-St Jeor equation with height
if (sex.toLower().contains("female")) {
return (10 * weight) + (6.25 * height) - (5 * age) - 161;
} else {
return (10 * weight) + (6.25 * height) - (5 * age) + 5;
}
}
double metric::calculateActiveKCal(double totalKCal, double elapsed) {
QSettings settings;
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
if (!activeOnly) {
return totalKCal; // Return total calories if active-only mode is disabled
}
// Calculate BMR in calories per second
double bmrPerDay = calculateBMR();
double bmrPerSecond = bmrPerDay / (24.0 * 60.0 * 60.0); // Convert from calories/day to calories/second
// Calculate BMR calories for the elapsed time
double bmrForElapsed = bmrPerSecond * elapsed;
// Active calories = Total calories - BMR calories for the elapsed time
double activeKCal = totalKCal - bmrForElapsed;
// Ensure we don't return negative calories
return activeKCal > 0 ? activeKCal : 0;
}

View File

@@ -52,6 +52,8 @@ class metric {
static double calculateWeightLoss(double kcal);
static double calculateVO2Max(QList<SessionLine> *session);
static double calculateKCalfromHR(double HR_AVG, double elapsed);
static double calculateBMR();
static double calculateActiveKCal(double totalKCal, double elapsed);
static double powerPeak(QList<SessionLine> *session, int seconds);

View File

@@ -974,9 +974,12 @@ const QString QZSettings::tile_auto_virtual_shifting_climb_enabled = QStringLite
const QString QZSettings::tile_auto_virtual_shifting_climb_order = QStringLiteral("tile_auto_virtual_shifting_climb_order");
const QString QZSettings::tile_auto_virtual_shifting_sprint_enabled = QStringLiteral("tile_auto_virtual_shifting_sprint_enabled");
const QString QZSettings::tile_auto_virtual_shifting_sprint_order = QStringLiteral("tile_auto_virtual_shifting_sprint_order");
const QString QZSettings::calories_active_only = QStringLiteral("calories_active_only");
const QString QZSettings::calories_from_hr = QStringLiteral("calories_from_hr");
const QString QZSettings::height = QStringLiteral("height");
const uint32_t allSettingsCount = 798;
const uint32_t allSettingsCount = 801;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1796,6 +1799,9 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::tile_auto_virtual_shifting_sprint_enabled, QZSettings::default_tile_auto_virtual_shifting_sprint_enabled},
{QZSettings::tile_auto_virtual_shifting_sprint_order, QZSettings::default_tile_auto_virtual_shifting_sprint_order},
{QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike},
{QZSettings::calories_active_only, QZSettings::default_calories_active_only},
{QZSettings::calories_from_hr, QZSettings::default_calories_from_hr},
{QZSettings::height, QZSettings::default_height},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -2660,6 +2660,24 @@ class QZSettings {
static const QString tile_auto_virtual_shifting_sprint_order;
static constexpr int default_tile_auto_virtual_shifting_sprint_order = 57;
/**
* @brief Calculate only active calories (exclude basal metabolic rate)
*/
static const QString calories_active_only;
static constexpr bool default_calories_active_only = false;
/**
* @brief Calculate calories from heart rate instead of power
*/
static const QString calories_from_hr;
static constexpr bool default_calories_from_hr = false;
/**
* @brief User height in centimeters for BMR calculation
*/
static const QString height;
static constexpr double default_height = 175.0;
/**
* @brief Write the QSettings values using the constants from this namespace.
* @param showDefaults Optionally indicates if the default should be shown with the key.

View File

@@ -1198,6 +1198,9 @@ import Qt.labs.platform 1.1
property int tile_auto_virtual_shifting_sprint_order: 57
property string proform_rower_ip: ""
property string ftms_elliptical: "Disabled"
property bool calories_active_only: false
property real height: 175.0
property bool calories_from_hr: false
}
@@ -1317,6 +1320,62 @@ import Qt.labs.platform 1.1
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
id: labelHeight
text: qsTr("Player Height") + "(" + (settings.miles_unit?"ft/in":"cm") + ")"
Layout.fillWidth: true
}
TextField {
id: heightTextField
text: settings.miles_unit ? Math.floor(settings.height / 30.48) + "'" + Math.round((settings.height % 30.48) / 2.54) + '"' : settings.height
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
//inputMethodHints: Qt.ImhFormattedNumbersOnly
onAccepted: {
if (settings.miles_unit) {
var parts = text.match(/(\d+)'(\d+)"/);
if (parts) {
settings.height = parseInt(parts[1]) * 30.48 + parseInt(parts[2]) * 2.54;
}
} else {
settings.height = text;
}
}
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
id: okHeightButton
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: {
if (settings.miles_unit) {
var parts = heightTextField.text.match(/(\d+)'(\d+)"/);
if (parts) {
settings.height = parseInt(parts[1]) * 30.48 + parseInt(parts[2]) * 2.54;
}
} else {
settings.height = heightTextField.text;
}
toast.show("Setting saved!");
}
}
}
Label {
text: qsTr("Enter your height for more accurate BMR and active calories calculation. Use centimeters for metric or feet'inches\" format (e.g., 5'10\") for imperial units.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
@@ -1721,7 +1780,63 @@ import Qt.labs.platform 1.1
}
Label {
text: qsTr("This prevents your bike or treadmill from sending its calories-burned calculation to QZ and defaults to QZs more accurate calculation.")
text: qsTr("This prevents your bike or treadmill from sending its calories-burned calculation to QZ and defaults to QZ's more accurate calculation.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: switchActiveCaloriesOnlyDelegate
text: qsTr("Calculate Active Calories Only")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.calories_active_only
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.calories_active_only = checked
}
Label {
text: qsTr("Enable to calculate only active calories (excluding basal metabolic rate) similar to Apple Watch. When disabled, total calories including BMR are calculated. This affects both display and Apple Health integration.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: switchCaloriesFromHRDelegate
text: qsTr("Calculate Calories from Heart Rate")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.calories_from_hr
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.calories_from_hr = checked
}
Label {
text: qsTr("Enable to calculate calories based on heart rate data instead of power. Requires heart rate sensor connection for accurate calorie estimation.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2