61 lines
2.0 KiB
Swift
61 lines
2.0 KiB
Swift
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
|
|
}
|
|
}
|