feat: implement Haptika PPG and Watch V1

This commit is contained in:
Sacha
2026-07-31 16:19:42 +02:00
parent 6b7d4e0728
commit b9930b7378
249 changed files with 46475 additions and 122 deletions
@@ -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
}
}