import Foundation import WatchKit @MainActor public final class WatchHapticService: ObservableObject { @Published public private(set) var isRunning = false @Published public private(set) var elapsedSeconds: TimeInterval = 0 private let arbiter: WatchResourceArbiter private var task: Task? private var activeRunID: UUID? public init(arbiter: WatchResourceArbiter = .shared) { self.arbiter = arbiter } public func start(protocol regulation: WatchRegulationProtocol) async throws { task?.cancel() task = nil activeRunID = nil await arbiter.forceRelease() try await arbiter.acquire(.hapticRegulation) let runID = UUID() activeRunID = runID isRunning = true elapsedSeconds = 0 WKInterfaceDevice.current().play(.start) task = Task { @MainActor [weak self] in guard let self else { return } defer { Task { @MainActor in await self.finishRun(runID) } } if regulation.id == "sosStress" { for _ in 0..<3 { guard !Task.isCancelled else { return } WKInterfaceDevice.current().play(.click) try? await Task.sleep(for: .milliseconds(600)) } } var previousPhaseIndex = -1 var previousCycleIndex = -1 let startedAt = Date() while !Task.isCancelled { let elapsed = Date().timeIntervalSince(startedAt) self.elapsedSeconds = min(regulation.durationSeconds, elapsed) if elapsed >= regulation.durationSeconds { WKInterfaceDevice.current().play(.success) return } if regulation.id == "sosStress" { let second = Int(elapsed.rounded(.down)) if second > 0 && second.isMultiple(of: 10) { WKInterfaceDevice.current().play(.click) try? await Task.sleep(for: .seconds(1)) continue } } else if let position = RegulationPhaseEngine.position(in: regulation, elapsedSeconds: elapsed), position.phaseIndex != previousPhaseIndex || position.cycleIndex != previousCycleIndex { if previousPhaseIndex >= 0 { WKInterfaceDevice.current().play(.click) } previousPhaseIndex = position.phaseIndex previousCycleIndex = position.cycleIndex } try? await Task.sleep(for: .milliseconds(100)) } } } public func startCardiacMirror( initialBPM: Double, durationSeconds: TimeInterval = 120, decelerationBPMPerMinute: Double = 1.5 ) async throws { task?.cancel() task = nil activeRunID = nil await arbiter.forceRelease() try await arbiter.acquire(.hapticRegulation) let runID = UUID() activeRunID = runID isRunning = true elapsedSeconds = 0 WKInterfaceDevice.current().play(.start) task = Task { @MainActor [weak self] in guard let self else { return } defer { Task { @MainActor in await self.finishRun(runID) } } let startedAt = Date() while !Task.isCancelled { let elapsed = Date().timeIntervalSince(startedAt) self.elapsedSeconds = min(durationSeconds, elapsed) guard elapsed < durationSeconds else { WKInterfaceDevice.current().play(.success) return } let targetBPM = max(35, initialBPM - decelerationBPMPerMinute * elapsed / 60) WKInterfaceDevice.current().play(.click) try? await Task.sleep(for: .seconds(60 / targetBPM)) } } } public func cancel(playStop: Bool = true) { guard task != nil || isRunning else { return } task?.cancel() task = nil activeRunID = nil isRunning = false if playStop { WKInterfaceDevice.current().play(.stop) } Task { @MainActor [weak self] in guard let self, self.activeRunID == nil else { return } await self.arbiter.forceRelease() } } private func finishRun(_ runID: UUID) async { guard activeRunID == runID else { return } activeRunID = nil task = nil isRunning = false try? await arbiter.release(.hapticRegulation) } }