From 383f641ac220d3bb3cf18feb160f4f8b5a5e8abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB?= Date: Sun, 9 Aug 2026 18:24:55 +0200 Subject: [PATCH] feat(watch): improve lap detection with CMDeviceMotion and low-pass filtering --- targets/watch/WorkoutManager.swift | 37 +++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/targets/watch/WorkoutManager.swift b/targets/watch/WorkoutManager.swift index 445403a..59cec16 100644 --- a/targets/watch/WorkoutManager.swift +++ b/targets/watch/WorkoutManager.swift @@ -393,32 +393,44 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate { healthStore.execute(query) } - // MARK: - Lap Detection (Accelerometer) + // MARK: - Lap Detection (CMDeviceMotion) + + private var filteredUserAccel: Double = 0 + private var filteredRotationX: Double = 0 + private let filterAlpha: Double = 0.3 private func startLapDetection() { - guard motionManager.isAccelerometerAvailable else { return } - motionManager.accelerometerUpdateInterval = 0.1 // 10 Hz - motionManager.startAccelerometerUpdates() + guard motionManager.isDeviceMotionAvailable else { return } + motionManager.deviceMotionUpdateInterval = 0.1 // 10 Hz + motionManager.startDeviceMotionUpdates(using: .xMagneticNorthZVertical) lapDetectionTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in - self?.processAccelerometerData() + self?.processDeviceMotion() } } private func stopLapDetection() { lapDetectionTimer?.invalidate() lapDetectionTimer = nil - motionManager.stopAccelerometerUpdates() + motionManager.stopDeviceMotionUpdates() } - private func processAccelerometerData() { - guard let data = motionManager.accelerometerData else { return } + private func processDeviceMotion() { + guard let motion = motionManager.deviceMotion else { return } - let magnitude = sqrt(data.acceleration.x * data.acceleration.x + - data.acceleration.y * data.acceleration.y + - data.acceleration.z * data.acceleration.z) + // userAcceleration has gravity removed — clean movement signal + let rawUserAccel = sqrt(motion.userAcceleration.x * motion.userAcceleration.x + + motion.userAcceleration.y * motion.userAcceleration.y + + motion.userAcceleration.z * motion.userAcceleration.z) - let isStationary = magnitude < 1.1 // Near rest (~1g from gravity) + // Low-pass filter for smoothing + filteredUserAccel = filterAlpha * rawUserAccel + (1 - filterAlpha) * filteredUserAccel + + // Rotation rate on x-axis detects the flip/turn at the wall + let rawRotationX = abs(motion.rotationRate.x) + filteredRotationX = filterAlpha * rawRotationX + (1 - filterAlpha) * filteredRotationX + + let isStationary = filteredUserAccel < 0.05 let now = Date() if isStationary { @@ -439,7 +451,6 @@ class WorkoutManager: NSObject, ObservableObject, HKLiveWorkoutBuilderDelegate { self.lapDetected = true self.lastLapDetectionTime = now } - // Reset after detection DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.lapDetected = false }