Marche 6 minutes, et la vraie recuperation cardiaque

Deux ajouts lies par le meme constat : ce qu'on peut mesurer honnetement.

MARCHE 6 MINUTES — troisieme protocole de la vue de test. Sous-maximal, sans
risque, c'est la reference en cardiologie et le seul des trois qui ne demande
aucun effort maximal. Type d'activite HealthKit .walking (la distance et la
vitesse ne vivent pas sur le meme quantity type que la course).

RECUPERATION CARDIAQUE — le pont collecte desormais
heartRateRecoveryOneMinute, la valeur calculee par la Watch elle-meme.

Pourquoi ne pas la deriver de la serie de FC post-seance, deja en base :
mesure faite sur les 19 seances exploitables de la prod, la baisse mediane a
1 minute ressort a 2 bpm, avec 7 valeurs nulles ou negatives (min -10, max 15).
Ce n'est pas physiologique : la fin de seance HealthKit ne coincide pas avec
l'arret de l'effort. Une courbe batie la-dessus monterait et descendrait sans
rien mesurer.

⚠️ Le sample n'est PAS rattache au workout : predicateForObjects(from:) ne le
rendrait jamais. Apple l'ecrit comme un echantillon independant horodate juste
apres la seance, d'ou une requete par fenetre temporelle.

⚠️ NON COMPILE. La courbe de tendance cote web attend ce build : sans lui, le
champ reste absent et il n'y a rien a tracer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-11 18:58:24 +00:00
parent a61ba25590
commit c74faeaad5
2 changed files with 66 additions and 7 deletions

View File

@@ -21,7 +21,10 @@ import WatchKit
struct VmaTestView: View {
enum Protocole: String, CaseIterable {
case stages = "Paliers"
case sixMin = "6 minutes"
case sixMin = "6 min course"
// Test de marche 6 minutes : sous-maximal, référence en cardiologie.
// Le seul des trois qui ne demande aucun effort maximal.
case walk6 = "6 min marche"
}
@StateObject private var workout = WorkoutManager.shared
@@ -80,9 +83,7 @@ struct VmaTestView: View {
.labelsHidden()
.frame(height: 60)
Text(proto == .stages
? "Paliers d'une minute, de 8 km/h par pas de 0,5. La montre vibre à chaque changement. Arrête-toi quand tu ne tiens plus l'allure."
: "Six minutes à allure maximale régulière, sur terrain plat. La distance parcourue donne la VMA.")
Text(protocolHelp)
.font(.caption2)
.foregroundStyle(.secondary)
@@ -94,6 +95,17 @@ struct VmaTestView: View {
}
}
private var protocolHelp: String {
switch proto {
case .stages:
return "Paliers d'une minute, de 8 km/h par pas de 0,5. La montre vibre à chaque changement. Arrête-toi quand tu ne tiens plus l'allure."
case .sixMin:
return "Six minutes à allure maximale régulière, sur terrain plat. La distance parcourue donne la VMA."
case .walk6:
return "Marche six minutes à ton pas le plus rapide, sans courir. Sous-maximal : c'est le test de référence en cardiologie."
}
}
// MARK: - Pendant le test
private var runningView: some View {
@@ -162,7 +174,8 @@ struct VmaTestView: View {
.font(.caption2).foregroundStyle(.secondary)
} else {
Text(String(format: "%.0f m", workout.distanceMeters)).font(.title3)
Text("en 6 minutes").font(.caption2).foregroundStyle(.secondary)
Text(proto == .walk6 ? "en 6 minutes de marche" : "en 6 minutes")
.font(.caption2).foregroundStyle(.secondary)
}
if maxHr > 0 {
@@ -196,7 +209,12 @@ struct VmaTestView: View {
finished = false
started = true
// startWorkout est async (@MainActor) : même appel que ContentView.
Task { await workout.startWorkout(activityType: .running, locationType: .outdoor) }
// Marche et course sont deux types HealthKit distincts : la distance
// et la vitesse ne sont pas collectées sur le même quantity type.
Task {
await workout.startWorkout(activityType: proto == .walk6 ? .walking : .running,
locationType: .outdoor)
}
WKInterfaceDevice.current().play(.start)
}
@@ -228,7 +246,7 @@ struct VmaTestView: View {
sendError = nil
var payload: [String: Any] = [
"vmaTest": true,
"protocol": proto == .stages ? "stages" : "cooper6",
"protocol": protocolKey,
"maxHr": Int(maxHr.rounded()),
]
if proto == .stages {
@@ -248,6 +266,14 @@ struct VmaTestView: View {
}
}
private var protocolKey: String {
switch proto {
case .stages: return "stages"
case .sixMin: return "cooper6"
case .walk6: return "walk6"
}
}
private func timeString(_ s: Double) -> String {
let t = Int(s.rounded())
return String(format: "%d:%02d", t / 60, t % 60)