Skip to content

Commit e7abaeb

Browse files
authored
[codex] Propagate total kcal to Apple Watch (#4724)
* Propagate total kcal to Apple Watch * Show watch heart rate in Live Activity * Adjust Domyos elliptical inclination control * Fix Live Activity heart rate signature * Accumulate TRX treadmill calories as raw metric * Sync watch calories before saving workout * Revert "Adjust Domyos elliptical inclination control" This reverts commit 406f770. * kcal per minutes clipped to 0 * debug and baseline kcal * Reset totalKcal on the watch at workout start Prevents a stale totalKcal from a previous workout from leaking into the basalEnergyBurned calculation in stopWorkOut().
1 parent e6ba3b5 commit e7abaeb

11 files changed

Lines changed: 131 additions & 60 deletions

File tree

build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/MainController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ extension MainController: WorkoutTrackingDelegate {
104104
"\(heartRate)" as AnyObject])
105105
WorkoutTracking.distance = WatchKitConnection.distance
106106
WorkoutTracking.kcal = WatchKitConnection.kcal
107+
WorkoutTracking.totalKcal = WatchKitConnection.totalKcal
107108
WorkoutTracking.speed = WatchKitConnection.speed
108109
WorkoutTracking.power = WatchKitConnection.power
109110
WorkoutTracking.cadence = WatchKitConnection.cadence

build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchKitConnection.swift

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ class WatchKitConnection: NSObject {
3535
private override init() {
3636
super.init()
3737
}
38+
39+
private static func doubleValue(_ value: Any?) -> Double? {
40+
if let value = value as? Double {
41+
return value
42+
}
43+
if let value = value as? NSNumber {
44+
return value.doubleValue
45+
}
46+
if let value = value as? String {
47+
return Double(value)
48+
}
49+
return nil
50+
}
3851

3952
private let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil
4053

@@ -68,36 +81,53 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
6881
{
6982
validReachableSession?.sendMessage(message, replyHandler: { (result) in
7083
print(result)
71-
let dDistance = Double(result["distance"] as! Double)
72-
WatchKitConnection.distance = dDistance
73-
let dKcal = Double(result["kcal"] as! Double)
74-
WatchKitConnection.kcal = dKcal
75-
if let totalKcalDouble = result["totalKcal"] as? Double {
84+
if let dDistance = WatchKitConnection.doubleValue(result["distance"]) {
85+
WatchKitConnection.distance = dDistance
86+
}
87+
if let dKcal = WatchKitConnection.doubleValue(result["kcal"]) {
88+
WatchKitConnection.kcal = dKcal
89+
WorkoutTracking.kcal = dKcal
90+
} else {
91+
WatchKitConnection.shared.sendDebug("reply missing kcal raw=\(result)")
92+
}
93+
if let totalKcalDouble = WatchKitConnection.doubleValue(result["totalKcal"]) {
7694
WatchKitConnection.totalKcal = totalKcalDouble
95+
WorkoutTracking.totalKcal = totalKcalDouble
96+
} else {
97+
WatchKitConnection.shared.sendDebug("reply missing totalKcal raw=\(result) localKcal=\(WatchKitConnection.kcal) localTotalKcal=\(WatchKitConnection.totalKcal)")
98+
}
99+
if let dSpeed = WatchKitConnection.doubleValue(result["speed"]) {
100+
WatchKitConnection.speed = dSpeed
101+
}
102+
if let dPower = WatchKitConnection.doubleValue(result["power"]) {
103+
WatchKitConnection.power = dPower
104+
}
105+
if let dCadence = WatchKitConnection.doubleValue(result["cadence"]) {
106+
WatchKitConnection.cadence = dCadence
77107
}
78-
79-
let dSpeed = Double(result["speed"] as! Double)
80-
WatchKitConnection.speed = dSpeed
81-
let dPower = Double(result["power"] as! Double)
82-
WatchKitConnection.power = dPower
83-
let dCadence = Double(result["cadence"] as! Double)
84-
WatchKitConnection.cadence = dCadence
85-
if let stepsDouble = result["steps"] as? Double {
108+
if let stepsDouble = WatchKitConnection.doubleValue(result["steps"]) {
86109
let iSteps = Int(stepsDouble)
87110
WatchKitConnection.steps = iSteps
88111
}
89-
if let elevationGainDouble = result["elevationGain"] as? Double {
112+
if let elevationGainDouble = WatchKitConnection.doubleValue(result["elevationGain"]) {
90113
WatchKitConnection.elevationGain = elevationGainDouble
91114
// Calculate flights climbed and update WorkoutTracking
92115
let flightsClimbed = elevationGainDouble / 3.048 // One flight = 10 feet = 3.048 meters
93116
WorkoutTracking.flightsClimbed = flightsClimbed
94117
WorkoutTracking.elevationGain = elevationGainDouble
95118
print("WatchKitConnection: Received elevation gain: \(elevationGainDouble)m, flights: \(flightsClimbed)")
96119
}
120+
WatchKitConnection.shared.sendDebug("reply parsed local distance=\(WatchKitConnection.distance) kcal=\(WatchKitConnection.kcal) totalKcal=\(WatchKitConnection.totalKcal) speed=\(WatchKitConnection.speed) power=\(WatchKitConnection.power) cadence=\(WatchKitConnection.cadence) steps=\(WatchKitConnection.steps) elevationGain=\(WatchKitConnection.elevationGain)")
97121
}, errorHandler: { (error) in
98122
print(error)
99123
})
100124
}
125+
126+
func sendDebug(_ message: String) {
127+
validReachableSession?.sendMessage(["watchDebug": message as AnyObject], replyHandler: nil, errorHandler: { (error) in
128+
print("Watch debug send error: \(error)")
129+
})
130+
}
101131
}
102132

103133
extension WatchKitConnection: WCSessionDelegate {

build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchWorkoutTracking.swift

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
235235
// Reset flights climbed and elevation gain for new workout
236236
WorkoutTracking.flightsClimbed = 0
237237
WorkoutTracking.elevationGain = 0
238+
WorkoutTracking.totalKcal = 0
239+
WatchKitConnection.totalKcal = 0
238240
print("Start workout")
239241
configWorkout()
240242
workoutSession.startActivity(with: Date())
@@ -251,33 +253,53 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
251253
}
252254

253255
func stopWorkOut() {
254-
print("Stop workout")
255-
workoutSession.stopActivity(with: Date())
256-
workoutSession.end()
256+
print("Stop workout")
257+
WorkoutTracking.kcal = WatchKitConnection.kcal
258+
WorkoutTracking.totalKcal = WatchKitConnection.totalKcal
259+
WatchKitConnection.shared.sendDebug("stop pressed local watchKcal=\(WatchKitConnection.kcal) watchTotalKcal=\(WatchKitConnection.totalKcal) trackingKcal=\(WorkoutTracking.kcal) trackingTotalKcal=\(WorkoutTracking.totalKcal)")
260+
workoutSession.stopActivity(with: Date())
261+
workoutSession.end()
257262

258-
// Write active calories
259-
guard let activeQuantityType = HKQuantityType.quantityType(
260-
forIdentifier: .activeEnergyBurned) else {
261-
return
262-
}
263-
264-
let unit = HKUnit.kilocalorie()
265-
let activeEnergyBurned = WorkoutTracking.kcal
266-
let activeQuantity = HKQuantity(unit: unit,
267-
doubleValue: activeEnergyBurned)
268-
269-
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
270-
271-
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
272-
quantity: activeQuantity,
273-
start: startDate,
274-
end: Date())
275-
276-
workoutBuilder.add([activeSample]) {(success, error) in
277-
if let error = error {
278-
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
279-
}
280-
}
263+
// Write active and basal calories
264+
guard let activeQuantityType = HKQuantityType.quantityType(
265+
forIdentifier: .activeEnergyBurned) else {
266+
return
267+
}
268+
269+
let unit = HKUnit.kilocalorie()
270+
let activeEnergyBurned = WorkoutTracking.kcal
271+
let activeQuantity = HKQuantity(unit: unit,
272+
doubleValue: activeEnergyBurned)
273+
let basalEnergyBurned = max(WorkoutTracking.totalKcal - activeEnergyBurned, 0)
274+
WatchKitConnection.shared.sendDebug("stop energy samples active=\(activeEnergyBurned) total=\(WorkoutTracking.totalKcal) basal=\(basalEnergyBurned)")
275+
276+
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
277+
278+
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
279+
quantity: activeQuantity,
280+
start: startDate,
281+
end: Date())
282+
var energySamples = [activeSample]
283+
284+
if basalEnergyBurned > 0,
285+
let basalQuantityType = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned) {
286+
let basalQuantity = HKQuantity(unit: unit,
287+
doubleValue: basalEnergyBurned)
288+
let basalSample = HKCumulativeQuantitySeriesSample(type: basalQuantityType,
289+
quantity: basalQuantity,
290+
start: startDate,
291+
end: Date())
292+
energySamples.append(basalSample)
293+
}
294+
295+
workoutBuilder.add(energySamples) {(success, error) in
296+
if let error = error {
297+
print("WatchWorkoutTracking energy calories: \(error.localizedDescription)")
298+
WatchKitConnection.shared.sendDebug("stop energy add error=\(error.localizedDescription) active=\(activeEnergyBurned) total=\(WorkoutTracking.totalKcal) basal=\(basalEnergyBurned)")
299+
} else {
300+
WatchKitConnection.shared.sendDebug("stop energy add success=\(success) active=\(activeEnergyBurned) total=\(WorkoutTracking.totalKcal) basal=\(basalEnergyBurned)")
301+
}
302+
}
281303

282304
let unitDistance = HKUnit.mile()
283305
let miles = WorkoutTracking.distance

src/devices/bluetoothdevice.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,11 @@ void bluetoothdevice::update_ios_live_activity() {
405405
.toString();
406406
QByteArray compactLeadingMetricUtf8 = compactLeadingMetric.toUtf8();
407407
QByteArray compactTrailingMetricUtf8 = compactTrailingMetric.toUtf8();
408-
const uint8_t workoutHeartRate = heartRateFromHealthKit ? 0 : (uint8_t)Heart.value();
408+
const uint8_t liveActivityHeartRate = (uint8_t)Heart.value();
409+
const uint8_t workoutHeartRate = heartRateFromHealthKit ? 0 : liveActivityHeartRate;
409410
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(),
410411
deviceType(), odometer() * 1000.0, totalCalories().value(), useMiles,
411-
workoutHeartRate, compactLeadingMetricUtf8.constData(),
412+
workoutHeartRate, liveActivityHeartRate, compactLeadingMetricUtf8.constData(),
412413
metricValueForSetting(compactLeadingMetric), compactTrailingMetricUtf8.constData(),
413414
metricValueForSetting(compactTrailingMetric));
414415
heartRateFromHealthKit = false;

src/devices/m3ibike/m3ibike.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,11 +754,12 @@ void m3ibike::processAdvertising(const QByteArray &data) {
754754
.toString();
755755
QByteArray compactLeadingMetricUtf8 = compactLeadingMetric.toUtf8();
756756
QByteArray compactTrailingMetricUtf8 = compactTrailingMetric.toUtf8();
757-
const uint8_t workoutHeartRate = heartRateFromHealthKit ? 0 : (uint8_t)Heart.value();
757+
const uint8_t liveActivityHeartRate = (uint8_t)Heart.value();
758+
const uint8_t workoutHeartRate = heartRateFromHealthKit ? 0 : liveActivityHeartRate;
758759

759760
h->workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), calories().value(),
760761
StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value(),
761-
useMiles, workoutHeartRate, compactLeadingMetricUtf8.constData(),
762+
useMiles, workoutHeartRate, liveActivityHeartRate, compactLeadingMetricUtf8.constData(),
762763
metricValueForSetting(compactLeadingMetric), compactTrailingMetricUtf8.constData(),
763764
metricValueForSetting(compactTrailingMetric));
764765
heartRateFromHealthKit = false;

src/devices/trxappgateusbtreadmill/trxappgateusbtreadmill.cpp

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -279,15 +279,13 @@ void trxappgateusbtreadmill::characteristicChanged(const QLowEnergyCharacteristi
279279
if (!firstCharChanged) {
280280
Distance += ((speed / 3600.0) / (1000.0 / (lastTimeCharChanged.msecsTo(now))));
281281
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()))
282-
kcal =
283-
KCal.value() + ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) *
284-
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
285-
200.0) /
286-
(60000.0 / ((double)lastTimeCharChanged.msecsTo(
287-
now)))); //(( (0.048* Output in watts +1.19) * body
288-
// weight in kg * 3.5) / 200 ) / 60
289-
else
290-
kcal = KCal.value();
282+
KCal += ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) *
283+
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
284+
200.0) /
285+
(60000.0 / ((double)lastTimeCharChanged.msecsTo(
286+
now)))); //(( (0.048* Output in watts +1.19) * body
287+
// weight in kg * 3.5) / 200 ) / 60
288+
kcal = KCal.value();
291289
}
292290
lastTimeCharChanged = now;
293291

@@ -308,7 +306,8 @@ void trxappgateusbtreadmill::characteristicChanged(const QLowEnergyCharacteristi
308306

309307
Speed = speed;
310308
Inclination = incline;
311-
KCal = kcal;
309+
if (firstCharChanged)
310+
KCal = kcal;
312311

313312
firstCharChanged = false;
314313

src/homeform.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6249,8 +6249,13 @@ void homeform::update() {
62496249

62506250
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
62516251
calories->setValue(QString::number(bluetoothManager->device()->calories().value(), 'f', 0));
6252-
calories->setSecondLine(QString::number((activeOnly ? bluetoothManager->device()->activeCalories().rate1s() : bluetoothManager->device()->calories().rate1s()) * 60.0, 'f', 1) +
6253-
" /min");
6252+
double caloriesPerMinute =
6253+
(activeOnly ? bluetoothManager->device()->activeCalories().rate1s()
6254+
: bluetoothManager->device()->calories().rate1s()) *
6255+
60.0;
6256+
if (caloriesPerMinute < 0)
6257+
caloriesPerMinute = 0;
6258+
calories->setSecondLine(QString::number(caloriesPerMinute, 'f', 1) + " /min");
62546259
if (!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool())
62556260
fan->setValue(QString::number(bluetoothManager->device()->fanSpeed()));
62566261
else

src/ios/AppleWatchToIpad/Connection.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ class Connection {
9797
let kcal : String = message.slice(from: "KCAL=", to: "#") ?? ""
9898
WatchKitConnection.kcal = (Double(kcal) ?? WatchKitConnection.kcal)
9999
}
100+
if sender?.contains("PAD") ?? false && message.contains("TOTALKCAL=") {
101+
let totalKcal : String = message.slice(from: "TOTALKCAL=", to: "#") ?? ""
102+
WatchKitConnection.totalKcal = (Double(totalKcal) ?? WatchKitConnection.totalKcal)
103+
}
100104
if sender?.contains("PAD") ?? false && message.contains("ODO=") {
101105
let odo : String = message.slice(from: "ODO=", to: "#") ?? ""
102106
WatchKitConnection.distance = (Double(odo) ?? WatchKitConnection.distance)

src/ios/WatchKitConnection.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ extension WatchKitConnection: WCSessionDelegate {
123123
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
124124
print("didReceiveMessage")
125125
print(message)
126+
if let watchDebug = message["watchDebug"] as? String {
127+
SwiftDebug.qtDebug("Watch debug: \(watchDebug)")
128+
}
126129
}
127130

128131
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
@@ -132,6 +135,10 @@ extension WatchKitConnection: WCSessionDelegate {
132135
print(message)
133136

134137
SwiftDebug.qtDebug("WatchKitConnection received payload: \(message)")
138+
139+
if let watchDebug = message["watchDebug"] as? String {
140+
SwiftDebug.qtDebug("Watch debug: \(watchDebug)")
141+
}
135142

136143
if(message.keys.first?.description == "heartRate") {
137144
guard let heartReate = message.values.first as? String else {
@@ -150,6 +157,7 @@ extension WatchKitConnection: WCSessionDelegate {
150157

151158
replyValues["distance"] = WatchKitConnection.distance
152159
replyValues["kcal"] = WatchKitConnection.kcal
160+
replyValues["totalKcal"] = WatchKitConnection.totalKcal
153161
replyValues["cadence"] = WatchKitConnection.cadence
154162
replyValues["power"] = WatchKitConnection.power
155163
replyValues["speed"] = WatchKitConnection.speed

src/ios/lockscreen.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class lockscreen {
2323
void workoutTrackingUpdate(double speed, unsigned short cadence, unsigned short watt, unsigned short currentCalories,
2424
unsigned long long currentSteps, unsigned char deviceType, double currentDistance,
2525
double totalKcal, bool useMiles, unsigned char heartRate,
26-
const char *compactLeadingMetric, int compactLeadingValue,
26+
int liveActivityHeartRate, const char *compactLeadingMetric, int compactLeadingValue,
2727
const char *compactTrailingMetric, int compactTrailingValue);
2828
bool appleWatchAppInstalled();
2929

0 commit comments

Comments
 (0)