128 lines
4.0 KiB
Swift
128 lines
4.0 KiB
Swift
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<Void, Never>?
|
|
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)
|
|
}
|
|
}
|