feat: implement Haptika PPG and Watch V1
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import Foundation
|
||||
|
||||
public enum PPGCoherencePolicy {
|
||||
public static let spotMaximumDurationSeconds = 120.0
|
||||
public static let coherenceMaximumDurationSeconds = 300.0
|
||||
public static let coherenceBufferDurationSeconds = 75.0
|
||||
|
||||
public static func maximumDurationSeconds(isCoherence: Bool) -> Double {
|
||||
isCoherence ? coherenceMaximumDurationSeconds : spotMaximumDurationSeconds
|
||||
}
|
||||
}
|
||||
|
||||
public struct PPGLiveAcceptedBeat: Equatable, Sendable {
|
||||
public let sequence: Int
|
||||
public let timestampSeconds: Double
|
||||
public let intervalMs: Double
|
||||
public let confidence: Double
|
||||
}
|
||||
|
||||
/// Deduplicates accepted intervals emitted by overlapping live-analysis windows.
|
||||
public struct PPGLiveBeatTracker: Sendable {
|
||||
private var lastPublishedTimestamp: Double?
|
||||
private var sequence = 0
|
||||
|
||||
public init() {}
|
||||
|
||||
public mutating func ingest(_ intervals: [PulseInterval]) -> [PPGLiveAcceptedBeat] {
|
||||
let sorted = intervals
|
||||
.filter {
|
||||
$0.isAccepted && $0.endSeconds.isFinite && $0.durationMilliseconds.isFinite
|
||||
&& (300...2_000).contains($0.durationMilliseconds)
|
||||
}
|
||||
.sorted { $0.endSeconds < $1.endSeconds }
|
||||
let previous = lastPublishedTimestamp
|
||||
let newIntervals = sorted.filter { interval in
|
||||
guard let previous else { return true }
|
||||
// Physiological intervals are at least 300 ms. A 250 ms boundary treats
|
||||
// small peak refinements as the same beat without swallowing a real one.
|
||||
return interval.endSeconds > previous + 0.25
|
||||
}
|
||||
let beats = newIntervals.map { interval -> PPGLiveAcceptedBeat in
|
||||
sequence += 1
|
||||
return PPGLiveAcceptedBeat(
|
||||
sequence: sequence,
|
||||
timestampSeconds: interval.endSeconds,
|
||||
intervalMs: interval.durationMilliseconds,
|
||||
confidence: min(max(interval.qualityScore, 0), 1)
|
||||
)
|
||||
}
|
||||
if let latest = sorted.last?.endSeconds {
|
||||
lastPublishedTimestamp = max(lastPublishedTimestamp ?? latest, latest)
|
||||
}
|
||||
return beats
|
||||
}
|
||||
|
||||
public mutating func reset() {
|
||||
lastPublishedTimestamp = nil
|
||||
sequence = 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user