Files
haptika/packages/react-native-haptika-ppg/ios/Signal/PPGCoherencePolicy.swift
2026-07-31 16:20:10 +02:00

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
}
}