feat: implement Haptika PPG and Watch V1
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user