From 3ce8ca9e287c0c72743accca766f3819cc83b60c Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 25 May 2026 20:21:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(watchos):=20mode=20Test=20(FC=20simul?= =?UTF-8?q?=C3=A9e)=20=E2=80=94=20valide=20le=20flux=20sans=20HealthKit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bouton "Test (FC simulée)" qui envoie des samples synthétiques (FC ~110-150, calories, distance, durée) via WCSession toutes les 2s, sans démarrer de séance HealthKit. Permet de valider watch -> iPhone -> /live indépendamment de l'autorisation Santé (bloquée par un sous-système Santé coincé sur la Watch). Co-Authored-By: Claude Opus 4.7 (1M context) --- ios/App/CoachWatch/ContentView.swift | 10 +++++- ios/App/CoachWatch/WorkoutManager.swift | 43 ++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/ios/App/CoachWatch/ContentView.swift b/ios/App/CoachWatch/ContentView.swift index d0a1d5a..b3088d3 100644 --- a/ios/App/CoachWatch/ContentView.swift +++ b/ios/App/CoachWatch/ContentView.swift @@ -45,10 +45,18 @@ struct ActivityPickerView: View { .frame(maxWidth: .infinity, alignment: .leading) } } + Button { + workout.startSimulation() + } label: { + Label("Test (FC simulée)", systemImage: "waveform.path.ecg") + .frame(maxWidth: .infinity, alignment: .leading) + } + .tint(.gray) + if let message = workout.statusMessage { Text(message) .font(.footnote) - .foregroundStyle(.red) + .foregroundStyle(.secondary) } } .padding(.horizontal, 4) diff --git a/ios/App/CoachWatch/WorkoutManager.swift b/ios/App/CoachWatch/WorkoutManager.swift index ce95691..d8dd147 100644 --- a/ios/App/CoachWatch/WorkoutManager.swift +++ b/ios/App/CoachWatch/WorkoutManager.swift @@ -24,6 +24,7 @@ final class WorkoutManager: NSObject, ObservableObject { private var lastSentAt = Date.distantPast private let sendInterval: TimeInterval = 2 + private var simTask: Task? private override init() { super.init() } @@ -86,7 +87,47 @@ final class WorkoutManager: NSObject, ObservableObject { func pause() { session?.pause() } func resume() { session?.resume() } - func end() { session?.end() } + + func end() { + if let simTask { + simTask.cancel() + self.simTask = nil + isRunning = false + statusMessage = nil + } else { + session?.end() + } + } + + /// Mode test : FC simulée, sans HealthKit, pour valider le flux watch -> iPhone -> /live. + func startSimulation() { + guard !isRunning else { return } + isRunning = true + statusMessage = "Mode test — FC simulée" + let startedAt = Date() + simTask = Task { [weak self] in + while !Task.isCancelled { + guard let self else { return } + let elapsed = Date().timeIntervalSince(startedAt) + let hr = 110 + 40 * abs(sin(elapsed / 30)) + let kcal = elapsed / 6 + let dist = elapsed * 2.6 + self.heartRate = hr + self.activeEnergyKcal = kcal + self.distanceMeters = dist + self.elapsedSec = elapsed + ConnectivityManager.shared.sendSample([ + "heartRate": hr, + "activeEnergyKcal": kcal, + "distanceMeters": dist, + "elapsedSec": elapsed, + "ts": Date().timeIntervalSince1970, + "sim": true + ]) + try? await Task.sleep(nanoseconds: 2_000_000_000) + } + } + } /// Appelé sur le main depuis le callback de collecte (hors-main). private func ingest(hr: Double?, kcal: Double?, dist: Double?, elapsed: TimeInterval) {