Compare commits

...

8 Commits

Author SHA1 Message Date
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
19 changed files with 352 additions and 79 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 = 1142;
CURRENT_PROJECT_VERSION = 1143;
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 = 1142;
CURRENT_PROJECT_VERSION = 1143;
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 = 1142;
CURRENT_PROJECT_VERSION = 1143;
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 = 1142;
CURRENT_PROJECT_VERSION = 1143;
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 = 1142;
CURRENT_PROJECT_VERSION = 1143;
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 = 1142;
CURRENT_PROJECT_VERSION = 1143;
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,24 +226,53 @@ 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)")
}
}
// Write total calories if available (when active calories setting is enabled)
if WorkoutTracking.totalKcal > 0 {
guard let basalQuantityType = HKQuantityType.quantityType(
forIdentifier: .basalEnergyBurned) else {
return
}
// Calculate basal calories as difference between total and active
let basalEnergyBurned = WorkoutTracking.totalKcal - activeEnergyBurned
let basalQuantity = HKQuantity(unit: unit,
doubleValue: basalEnergyBurned)
let basalSample = HKCumulativeQuantitySeriesSample(type: basalQuantityType,
quantity: basalQuantity,
start: startDate,
end: Date())
workoutBuilder.add([basalSample]) {(success, error) in
if let error = error {
print("WatchWorkoutTracking basal calories: \(error.localizedDescription)")
}
}
}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance
@@ -273,6 +305,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 +370,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 +439,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

@@ -152,7 +152,15 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
emit resistanceRead(Resistance.value());
m_pelotonResistance = Resistance.value();
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value());
// Parse cadence from 5th byte (index 4) and multiply by 2
uint8_t rawCadence = newValue.at(4);
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = rawCadence * 2;
}
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value()) + QStringLiteral(", Raw cadence: ") + QString::number(rawCadence) + QStringLiteral(", Final cadence: ") + QString::number(Cadence.value());
}
if (newValue.length() != 10 || newValue.at(2) != 0x31) {
@@ -162,41 +170,13 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
uint16_t distanceData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
double distance = ((double)distanceData);
if(distance != lastDistance) {
if(lastDistance != 0) {
double deltaDistance = distance - lastDistance;
double deltaTime = fabs(now.msecsTo(lastTS));
double timeHours = deltaTime / (1000.0 * 60.0 * 60.0);
double k = 0.005333;
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = (deltaDistance *k) / timeHours; // Speed in distance units per hour
} else {
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = Speed.value() / 0.37497622;
}
}
lastDistance = distance;
lastTS = now;
qDebug() << "lastDistance" << lastDistance << "lastTS" << lastTS;
// Calculate speed using the same method as echelon bike
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = 0.37497622 * ((double)Cadence.value());
} else {
// Check if speed and cadence should be reset due to timeout (2 seconds)
if (lastTS.msecsTo(now) > 2000) {
if (Speed.value() > 0) {
Speed = 0;
qDebug() << "Speed reset to 0 due to timeout";
}
if (Cadence.value() > 0) {
Cadence = 0;
qDebug() << "Cadence reset to 0 due to timeout";
}
lastTS = now;
}
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
if (watts())

View File

@@ -109,7 +109,24 @@ 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();
if (activeOnly) {
// Return active calories (total minus BMR)
metric activeKCal = KCal;
activeKCal.setValue(metric::calculateActiveKCal(KCal.value(), elapsed.value()));
return activeKCal;
} else {
// Return total calories
return KCal;
}
}
metric bluetoothdevice::totalCalories() {
return KCal;
}
metric bluetoothdevice::jouls() { return m_jouls; }
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
@@ -254,7 +271,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());

View File

@@ -108,12 +108,18 @@ 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();
/**
* @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
*/

View File

@@ -61,6 +61,17 @@ void faketreadmill::update() {
Distance += ((Speed.value() / (double)3600.0) /
((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(now))));
KCal +=
((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in
// kg * 3.5) / 200 ) / 60
lastRefreshCharacteristicChanged = now;
// ******************************************* virtual treadmill init *************************************
@@ -116,12 +127,6 @@ void faketreadmill::update() {
#endif
}
if (Heart.value()) {
static double lastKcal = 0;
if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator
lastKcal = abs(KCal.value());
KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal;
}
}
void faketreadmill::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic,

View File

@@ -7207,9 +7207,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,51 @@ 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)
}
}
// Write total calories if available (when active calories setting is enabled)
if WorkoutTracking.totalKcal > 0 {
guard let basalQuantityType = HKQuantityType.quantityType(
forIdentifier: .basalEnergyBurned) else {
return
}
// Calculate basal calories as difference between total and active
let basalEnergyBurned = WorkoutTracking.totalKcal - activeEnergyBurned
let basalQuantity = HKQuantity(unit: unit,
doubleValue: basalEnergyBurned)
let basalSample = HKCumulativeQuantitySeriesSample(type: basalQuantityType,
quantity: basalQuantity,
start: startDate,
end: Date())
workoutBuilder.add([basalSample]) {(success, error) in
if let error = error {
SwiftDebug.qtDebug("WorkoutTracking basal calories: " + error.localizedDescription)
}
}
}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance * 0.000621371
@@ -215,6 +247,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 +306,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")
}
}
}

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);

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];

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,11 @@ 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::height = QStringLiteral("height");
const uint32_t allSettingsCount = 798;
const uint32_t allSettingsCount = 800;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1796,6 +1798,8 @@ 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::height, QZSettings::default_height},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -2660,6 +2660,18 @@ 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 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,8 @@ 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
}
@@ -1317,6 +1319,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 +1779,35 @@ 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