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,30 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "HaptikaPPG"
s.version = package["version"]
s.summary = package["description"]
s.homepage = "https://haptika.local"
s.license = { :type => "Proprietary" }
s.authors = { "Haptika" => "engineering@haptika.local" }
s.platforms = { :ios => "16.0" }
s.source = { :path => "." }
# Nitro Modules 0.36.1 is not yet clean under Swift 6 strict concurrency.
s.swift_version = "5.0"
s.source_files = [
"ios/**/*.{swift,m,mm,cpp,h,hpp}",
"cpp/**/*.{cpp,h,hpp}"
]
load "nitrogen/generated/ios/HaptikaPPG+autolinking.rb"
add_nitrogen_files(s)
s.frameworks = "AVFoundation", "Accelerate", "CoreVideo", "HealthKit"
s.dependency "VisionCamera"
s.dependency "React-jsi"
s.dependency "React-callinvoker"
install_modules_dependencies(s)
end
@@ -0,0 +1,47 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "HaptikaPPGNativeTests",
platforms: [
.iOS(.v16),
.macOS(.v13),
],
targets: [
.target(
name: "HaptikaFrameMetrics",
path: "ios/FrameProcessor",
exclude: [
"HybridHaptikaPPGFrameProcessor.swift",
"PPGFingerContactDetector.swift",
],
sources: ["PPGFrameMetricsExtractor.swift"]
),
.target(
name: "HaptikaSignal",
path: "ios/Signal"
),
.target(
name: "HaptikaSafety",
path: "ios/Safety",
exclude: ["PPGThermalMonitor.swift"],
sources: ["PPGThermalSafetyPolicy.swift"]
),
.testTarget(
name: "HaptikaFrameMetricsTests",
dependencies: ["HaptikaFrameMetrics"],
path: "Tests/HaptikaFrameMetricsTests"
),
.testTarget(
name: "HaptikaSignalTests",
dependencies: ["HaptikaSignal"],
path: "Tests/HaptikaSignalTests"
),
.testTarget(
name: "HaptikaSafetyTests",
dependencies: ["HaptikaSafety"],
path: "Tests/HaptikaSafetyTests"
),
]
)
@@ -0,0 +1,341 @@
import CoreMedia
import CoreVideo
import XCTest
@testable import HaptikaFrameMetrics
final class PPGFrameMetricsExtractorTests: XCTestCase {
func testFullRangeYUVExtractionUsesCentralROIAndUnlocksBuffer() throws {
let pixelBuffer = try makePixelBuffer(
width: 64,
height: 64,
pixelFormat: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
)
try fillBiPlanarYUV(pixelBuffer, y: 100, cb: 128, cr: 128)
let metrics = try PPGFrameMetricsExtractor(gridSize: 16, roiFraction: 0.5)
.extract(from: makeSampleBuffer(pixelBuffer: pixelBuffer, timestampSeconds: 4.25))
XCTAssertEqual(metrics.timestampSeconds, 4.25, accuracy: 0.000_001)
XCTAssertEqual(metrics.redMean, 100, accuracy: 0.000_001)
XCTAssertEqual(metrics.greenMean, 100, accuracy: 0.000_001)
XCTAssertEqual(metrics.blueMean, 100, accuracy: 0.000_001)
XCTAssertEqual(metrics.luminanceMean, 100, accuracy: 0.000_001)
try assertCanRelock(pixelBuffer)
}
func testVideoRangeYUVExtractionAppliesVideoRangeConversion() throws {
let pixelBuffer = try makePixelBuffer(
width: 48,
height: 48,
pixelFormat: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
)
try fillBiPlanarYUV(pixelBuffer, y: 126, cb: 128, cr: 128)
let metrics = try PPGFrameMetricsExtractor(gridSize: 12, roiFraction: 0.5)
.extract(from: makeSampleBuffer(pixelBuffer: pixelBuffer, timestampSeconds: 2))
let expected = 1.164_383 * 110
XCTAssertEqual(metrics.redMean, expected, accuracy: 0.001)
XCTAssertEqual(metrics.greenMean, expected, accuracy: 0.001)
XCTAssertEqual(metrics.blueMean, expected, accuracy: 0.001)
try assertCanRelock(pixelBuffer)
}
func testBGRAExtractionSamplesOnlyCentralROI() throws {
let pixelBuffer = try makePixelBuffer(
width: 64,
height: 64,
pixelFormat: kCVPixelFormatType_32BGRA
)
try fillBGRA(pixelBuffer) { x, y in
if (16..<48).contains(x), (16..<48).contains(y) {
return (blue: 20, green: 80, red: 200, alpha: 255)
}
return (blue: 240, green: 10, red: 5, alpha: 255)
}
let sampleBuffer = try makeSampleBuffer(pixelBuffer: pixelBuffer, timestampSeconds: 12.5)
let metrics = try PPGFrameMetricsExtractor(gridSize: 16, roiFraction: 0.5)
.extract(from: sampleBuffer)
XCTAssertEqual(metrics.timestampSeconds, 12.5, accuracy: 0.000_001)
XCTAssertEqual(metrics.redMean, 200, accuracy: 0.000_001)
XCTAssertEqual(metrics.greenMean, 80, accuracy: 0.000_001)
XCTAssertEqual(metrics.blueMean, 20, accuracy: 0.000_001)
XCTAssertEqual(metrics.redVariance, 0, accuracy: 0.000_001)
XCTAssertEqual(metrics.greenVariance, 0, accuracy: 0.000_001)
XCTAssertEqual(
metrics.luminanceMean,
0.2126 * 200 + 0.7152 * 80 + 0.0722 * 20,
accuracy: 0.000_001
)
XCTAssertEqual(metrics.saturationRatio, 0, accuracy: 0.000_001)
try assertCanRelock(pixelBuffer)
}
func testBGRARedOnlyClippingKeepsUsableGreenChannel() throws {
let pixelBuffer = try makePixelBuffer(
width: 48,
height: 48,
pixelFormat: kCVPixelFormatType_32BGRA
)
try fillBGRA(pixelBuffer) { x, y in
let isCentral = (12..<36).contains(x) && (12..<36).contains(y)
return isCentral
? (blue: 40, green: 100, red: 255, alpha: 255)
: (blue: 0, green: 0, red: 0, alpha: 255)
}
let sampleBuffer = try makeSampleBuffer(pixelBuffer: pixelBuffer, timestampSeconds: 1)
let metrics = try PPGFrameMetricsExtractor(gridSize: 12, roiFraction: 0.5)
.extract(from: sampleBuffer)
XCTAssertEqual(metrics.redMean, 255, accuracy: 0.000_001)
XCTAssertEqual(metrics.saturationRatio, 0, accuracy: 0.000_001)
try assertCanRelock(pixelBuffer)
}
func testBGRASaturationRequiresBothUsefulChannelsToClip() throws {
let pixelBuffer = try makePixelBuffer(
width: 48,
height: 48,
pixelFormat: kCVPixelFormatType_32BGRA
)
try fillBGRA(pixelBuffer) { _, _ in
(blue: 80, green: 255, red: 255, alpha: 255)
}
let sampleBuffer = try makeSampleBuffer(pixelBuffer: pixelBuffer, timestampSeconds: 1)
let metrics = try PPGFrameMetricsExtractor(gridSize: 12, roiFraction: 0.5)
.extract(from: sampleBuffer)
XCTAssertEqual(metrics.saturationRatio, 1, accuracy: 0.000_001)
try assertCanRelock(pixelBuffer)
}
func testUnsupportedPixelFormatUnlocksBufferOnThrow() throws {
let pixelBuffer = try makePixelBuffer(
width: 32,
height: 32,
pixelFormat: kCVPixelFormatType_OneComponent8
)
let sampleBuffer = try makeSampleBuffer(pixelBuffer: pixelBuffer, timestampSeconds: 2)
XCTAssertThrowsError(try PPGFrameMetricsExtractor().extract(from: sampleBuffer)) { error in
guard case PPGFrameMetricsExtractorError.unsupportedPixelFormat(let format) = error else {
return XCTFail("Expected unsupportedPixelFormat, got \(error)")
}
XCTAssertEqual(format, kCVPixelFormatType_OneComponent8)
}
try assertCanRelock(pixelBuffer)
}
func testRepeatedExtractionDoesNotLeavePixelBuffersLocked() throws {
let extractor = PPGFrameMetricsExtractor(gridSize: 8, roiFraction: 0.5)
for index in 0..<100 {
autoreleasepool {
do {
let pixelBuffer = try makePixelBuffer(
width: 32,
height: 32,
pixelFormat: kCVPixelFormatType_32BGRA
)
try fillBGRA(pixelBuffer) { _, _ in (20, 90, 180, 255) }
_ = try extractor.extract(
from: makeSampleBuffer(
pixelBuffer: pixelBuffer,
timestampSeconds: Double(index) / 30
)
)
try assertCanRelock(pixelBuffer)
} catch {
XCTFail("Repeated extraction failed at \(index): \(error)")
}
}
}
}
private func makePixelBuffer(
width: Int,
height: Int,
pixelFormat: OSType
) throws -> CVPixelBuffer {
if pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
|| pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
return try makeBiPlanarPixelBuffer(width: width, height: height, pixelFormat: pixelFormat)
}
var pixelBuffer: CVPixelBuffer?
let status = CVPixelBufferCreate(
kCFAllocatorDefault,
width,
height,
pixelFormat,
nil,
&pixelBuffer
)
guard status == kCVReturnSuccess, let pixelBuffer else {
throw TestBufferError.pixelBufferCreation(status)
}
return pixelBuffer
}
private func makeBiPlanarPixelBuffer(
width: Int,
height: Int,
pixelFormat: OSType
) throws -> CVPixelBuffer {
let yBytesPerRow = width
let uvBytesPerRow = width
let ySize = yBytesPerRow * height
let uvHeight = height / 2
let totalSize = ySize + uvBytesPerRow * uvHeight
guard let storage = malloc(totalSize) else {
throw TestBufferError.missingBaseAddress
}
var planeAddresses: [UnsafeMutableRawPointer?] = [
storage,
storage.advanced(by: ySize),
]
var planeWidths = [width, width / 2]
var planeHeights = [height, uvHeight]
var planeBytesPerRow = [yBytesPerRow, uvBytesPerRow]
var pixelBuffer: CVPixelBuffer?
let status = CVPixelBufferCreateWithPlanarBytes(
kCFAllocatorDefault,
width,
height,
pixelFormat,
storage,
totalSize,
2,
&planeAddresses,
&planeWidths,
&planeHeights,
&planeBytesPerRow,
{ _, dataPointer, _, _, _ in
free(UnsafeMutableRawPointer(mutating: dataPointer))
},
nil,
nil,
&pixelBuffer
)
guard status == kCVReturnSuccess, let pixelBuffer else {
free(storage)
throw TestBufferError.pixelBufferCreation(status)
}
return pixelBuffer
}
private func fillBiPlanarYUV(
_ pixelBuffer: CVPixelBuffer,
y: UInt8,
cb: UInt8,
cr: UInt8
) throws {
let status = CVPixelBufferLockBaseAddress(pixelBuffer, [])
guard status == kCVReturnSuccess else {
throw TestBufferError.pixelBufferLock(status)
}
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) }
guard CVPixelBufferGetPlaneCount(pixelBuffer) >= 2,
let yBase = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0),
let uvBase = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)
else {
throw TestBufferError.missingBaseAddress
}
let yHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0)
let yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
let yBytes = yBase.assumingMemoryBound(to: UInt8.self)
for row in 0..<yHeight {
yBytes.advanced(by: row * yStride).initialize(repeating: y, count: yStride)
}
let uvWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1)
let uvHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1)
let uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
let uvBytes = uvBase.assumingMemoryBound(to: UInt8.self)
for row in 0..<uvHeight {
for column in 0..<uvWidth {
let offset = row * uvStride + column * 2
uvBytes[offset] = cb
uvBytes[offset + 1] = cr
}
}
}
private func fillBGRA(
_ pixelBuffer: CVPixelBuffer,
pixel: (_ x: Int, _ y: Int) -> (blue: UInt8, green: UInt8, red: UInt8, alpha: UInt8)
) throws {
let lockStatus = CVPixelBufferLockBaseAddress(pixelBuffer, [])
guard lockStatus == kCVReturnSuccess else {
throw TestBufferError.pixelBufferLock(lockStatus)
}
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) }
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
throw TestBufferError.missingBaseAddress
}
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let bytes = baseAddress.assumingMemoryBound(to: UInt8.self)
for y in 0..<height {
for x in 0..<width {
let color = pixel(x, y)
let offset = y * bytesPerRow + x * 4
bytes[offset] = color.blue
bytes[offset + 1] = color.green
bytes[offset + 2] = color.red
bytes[offset + 3] = color.alpha
}
}
}
private func makeSampleBuffer(
pixelBuffer: CVPixelBuffer,
timestampSeconds: Double
) throws -> CMSampleBuffer {
var formatDescription: CMVideoFormatDescription?
let formatStatus = CMVideoFormatDescriptionCreateForImageBuffer(
allocator: kCFAllocatorDefault,
imageBuffer: pixelBuffer,
formatDescriptionOut: &formatDescription
)
guard formatStatus == noErr, let formatDescription else {
throw TestBufferError.formatDescriptionCreation(formatStatus)
}
var timing = CMSampleTimingInfo(
duration: .invalid,
presentationTimeStamp: CMTime(seconds: timestampSeconds, preferredTimescale: 60_000),
decodeTimeStamp: .invalid
)
var sampleBuffer: CMSampleBuffer?
let sampleStatus = CMSampleBufferCreateReadyWithImageBuffer(
allocator: kCFAllocatorDefault,
imageBuffer: pixelBuffer,
formatDescription: formatDescription,
sampleTiming: &timing,
sampleBufferOut: &sampleBuffer
)
guard sampleStatus == noErr, let sampleBuffer else {
throw TestBufferError.sampleBufferCreation(sampleStatus)
}
return sampleBuffer
}
private func assertCanRelock(_ pixelBuffer: CVPixelBuffer) throws {
let status = CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
guard status == kCVReturnSuccess else {
throw TestBufferError.pixelBufferLock(status)
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
}
}
private enum TestBufferError: Error {
case pixelBufferCreation(CVReturn)
case pixelBufferLock(CVReturn)
case missingBaseAddress
case formatDescriptionCreation(OSStatus)
case sampleBufferCreation(OSStatus)
}
@@ -0,0 +1,16 @@
import XCTest
@testable import HaptikaSafety
final class PPGThermalSafetyPolicyTests: XCTestCase {
func testSeriousAndCriticalStatesStopMeasurement() {
XCTAssertTrue(PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: "serious"))
XCTAssertTrue(PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: "critical"))
}
func testNominalFairAndUnknownStatesRemainAvailable() {
XCTAssertFalse(PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: "nominal"))
XCTAssertFalse(PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: "fair"))
XCTAssertFalse(PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: "unknown"))
XCTAssertFalse(PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: "future-state"))
}
}
@@ -0,0 +1,52 @@
import XCTest
@testable import HaptikaSignal
final class PPGCoherencePolicyTests: XCTestCase {
func testSessionDurationLimitsRemainPurposeSpecific() {
XCTAssertEqual(PPGCoherencePolicy.maximumDurationSeconds(isCoherence: false), 120)
XCTAssertEqual(PPGCoherencePolicy.maximumDurationSeconds(isCoherence: true), 300)
XCTAssertEqual(PPGCoherencePolicy.coherenceBufferDurationSeconds, 75)
}
func testLiveTrackerPublishesEveryNewBeatOnlyOnce() {
var tracker = PPGLiveBeatTracker()
let first = tracker.ingest([
interval(end: 1, duration: 800), interval(end: 1.8, duration: 800),
])
XCTAssertEqual(first.map(\.sequence), [1, 2])
let overlapping = tracker.ingest([
interval(end: 1.82, duration: 800), interval(end: 2.6, duration: 780),
])
XCTAssertEqual(overlapping.map(\.sequence), [3])
XCTAssertEqual(overlapping.first?.timestampSeconds, 2.6)
}
func testTrackerDoesNotMoveBackwardAndResetsCleanly() {
var tracker = PPGLiveBeatTracker()
_ = tracker.ingest([interval(end: 10, duration: 700)])
XCTAssertTrue(tracker.ingest([interval(end: 9.9, duration: 700)]).isEmpty)
tracker.reset()
XCTAssertEqual(tracker.ingest([interval(end: 1, duration: 700)]).first?.sequence, 1)
}
func testTrackerRejectsInvalidAndRejectedIntervals() {
var tracker = PPGLiveBeatTracker()
let rejected = PulseInterval(
sourceIndex: 0, startSeconds: 0, endSeconds: 1,
durationMilliseconds: 800, rejectionReason: .localOutlier, qualityScore: 1
)
XCTAssertTrue(tracker.ingest([
rejected, interval(end: .nan, duration: 800), interval(end: 2, duration: 2_500),
]).isEmpty)
}
private func interval(end: Double, duration: Double) -> PulseInterval {
PulseInterval(
sourceIndex: 0,
startSeconds: end - duration / 1_000,
endSeconds: end,
durationMilliseconds: duration,
qualityScore: 0.9
)
}
}
@@ -0,0 +1,247 @@
import XCTest
@testable import HaptikaSignal
final class PPGSignalPipelineTests: XCTestCase {
func testPRVCalculatorDerivesIntervalMetrics() throws {
let intervals = [700.0, 720, 760, 710, 800, 730, 750, 705]
let metrics = try PPGPRVCalculator.calculate(
intervalsMilliseconds: intervals,
configuration: PPGProcessingConfiguration(minimumIntervalsForPRV: 5)
)
XCTAssertEqual(metrics.meanIpiMs ?? 0, 734.375, accuracy: 0.001)
XCTAssertEqual(metrics.medianIpiMs ?? 0, 725, accuracy: 0.001)
XCTAssertEqual(metrics.pnn50Percent ?? 0, 200.0 / 7.0, accuracy: 0.001)
XCTAssertEqual(metrics.rangeIpiMs ?? 0, 100, accuracy: 0.001)
XCTAssertEqual(metrics.modeIpiMs ?? 0, 713, accuracy: 0.001)
XCTAssertEqual(metrics.amo50Percent ?? 0, 62.5, accuracy: 0.001)
XCTAssertEqual((metrics.coefficientOfVariation ?? 0) * 100, 4.68, accuracy: 0.1)
}
func testBPMEstimatorCorrectsSecondHarmonic() throws {
let sampleRate = 30.0
let duration = 30.0
let sampleCount = Int(sampleRate * duration)
let timestamps = (0..<sampleCount).map { Double($0) / sampleRate }
// The spectral method sees a strong 144 BPM harmonic while the clean peak
// intervals correctly describe a 72 BPM pulse.
let signal = timestamps.map { sin(2 * Double.pi * 2.4 * $0) }
let peakInterval = 60.0 / 72.0
let peaks = (0..<35).map { index in
DetectedPeak(
sampleIndex: Int((Double(index) * peakInterval * sampleRate).rounded()),
timestampSeconds: Double(index) * peakInterval,
amplitude: 1,
prominence: 1
)
}
let intervals = zip(peaks, peaks.dropFirst()).enumerated().map { index, pair in
PulseInterval(
sourceIndex: index,
startSeconds: pair.0.timestampSeconds,
endSeconds: pair.1.timestampSeconds,
durationMilliseconds: peakInterval * 1_000
)
}
let detection = PeakDetectionResult(
peaks: peaks,
intervals: intervals,
selectedPolarity: 1,
confidence: 0.9
)
let estimate = try PPGBPMEstimator.estimate(
signal: signal,
timestampsSeconds: timestamps,
validityMask: Array(repeating: true, count: sampleCount),
peakDetection: detection,
selectedChannel: .greenRedRatio,
configuration: .production
)
XCTAssertNotNil(estimate.bpm)
XCTAssertEqual(estimate.bpm ?? 0, 72, accuracy: 1.5)
XCTAssertEqual(estimate.frequencyBpm ?? 0, 72, accuracy: 1.5)
}
func testProcessorProducesHeartRateFromCleanContactPPG() throws {
let sampleRate = 30.0
let duration = 55.0
let sampleCount = Int(sampleRate * duration) + 1
let samples = (0..<sampleCount).map { index -> PPGFrameSample in
let time = Double(index) / sampleRate
let pulse = sin(2 * Double.pi * 1.2 * time)
+ 0.18 * sin(2 * Double.pi * 2.4 * time)
let slowLightDrift = 1 + 0.08 * sin(2 * Double.pi * 0.08 * time)
return PPGFrameSample(
timestampSeconds: time,
red: 185 * slowLightDrift + 2.5 * pulse,
green: 82 * slowLightDrift + 5.5 * pulse,
luminance: 112 * slowLightDrift + 4 * pulse,
contactConfidence: 0.9,
saturationRatio: 0,
motionArtifactScore: 0.02
)
}
let configuration = PPGProcessingConfiguration(
minimumAnalysisDurationSeconds: 45,
filterTapCount: 61
)
let result = try PPGSignalProcessor(configuration: configuration)
.analyze(samples: samples)
XCTAssertEqual(result.status, .valid)
XCTAssertNotNil(result.bpm.bpm)
XCTAssertEqual(result.bpm.bpm ?? 0, 72, accuracy: 3)
XCTAssertGreaterThan(result.quality.score, 0.65)
}
func testResamplerKeepsUniformGridAndMasksLongGap() throws {
let input = [
TimestampedValue(timestampSeconds: 0.00, value: 0),
TimestampedValue(timestampSeconds: 0.09, value: 1),
TimestampedValue(timestampSeconds: 0.21, value: 2),
TimestampedValue(timestampSeconds: 0.80, value: 3),
TimestampedValue(timestampSeconds: 0.91, value: 4),
]
let result = try PPGSignalResampler.resample(
input,
targetSampleRateHz: 10,
maximumInterpolationGapSeconds: 0.20
)
XCTAssertEqual(result.timestampsSeconds.count, result.values.count)
XCTAssertEqual(result.timestampsSeconds[1] - result.timestampsSeconds[0], 0.1, accuracy: 1e-9)
XCTAssertFalse(result.validityMask[4], "A long camera gap must remain unusable")
XCTAssertGreaterThan(result.diagnostics.droppedFrameRatio, 0)
}
func testPRVLocalFilterRejectsArtifactWithoutRejectingGradualChange() throws {
let intervals = [700.0, 710, 720, 730, 740, 400, 760, 770, 780, 790, 800]
let result = try PPGPRVCalculator.calculate(
intervals: makeIntervals(intervals),
configuration: PPGProcessingConfiguration(
minimumIntervalsForPRV: 5,
maximumRejectedIntervalRatioForPRV: 0.25
)
)
XCTAssertEqual(result.intervals[5].rejectionReason, .localOutlier)
XCTAssertTrue(result.intervals[10].isAccepted, "A gradual HR change must remain usable")
XCTAssertEqual(result.metrics.validIntervalCount, intervals.count - 1)
XCTAssertTrue(result.metrics.reliable)
}
func testProcessorCanRecoverPulseCarriedByBlueChrominance() throws {
let sampleRate = 30.0
let duration = 55.0
let samples = (0...Int(sampleRate * duration)).map { index -> PPGFrameSample in
let time = Double(index) / sampleRate
let pulse = sin(2 * Double.pi * 1.2 * time)
+ 0.20 * sin(2 * Double.pi * 2.4 * time)
let red = 150.0
let green = 100.0
let blue = 80 + 9 * pulse
let luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
return PPGFrameSample(
timestampSeconds: time,
red: red,
green: green,
luminance: luminance,
contactConfidence: 0.95,
saturationRatio: 0,
motionArtifactScore: 0
)
}
let result = try PPGSignalProcessor(
configuration: PPGProcessingConfiguration(
minimumAnalysisDurationSeconds: 45,
filterTapCount: 61
)
).analyze(samples: samples)
XCTAssertEqual(result.status, .valid)
XCTAssertTrue(
[.blue, .greenRedRatio, .blueRedRatio, .normalizedBlend]
.contains(result.bpm.selectedChannel)
)
XCTAssertEqual(result.bpm.bpm ?? 0, 72, accuracy: 3)
}
func testCoachingContactScoreDoesNotDiscardARealPeriodicSignal() throws {
let sampleRate = 30.0
let duration = 55.0
let samples = (0...Int(sampleRate * duration)).map { index -> PPGFrameSample in
let time = Double(index) / sampleRate
let pulse = sin(2 * Double.pi * 1.25 * time)
+ 0.16 * sin(2 * Double.pi * 2.5 * time)
return PPGFrameSample(
timestampSeconds: time,
red: 142 + 3.2 * pulse,
green: 104 + 5.8 * pulse,
blue: 76 + 1.4 * pulse,
luminance: 111 + 4.9 * pulse,
contactConfidence: 0.10,
saturationRatio: 0,
motionArtifactScore: 0.02
)
}
let result = try PPGSignalProcessor(
configuration: PPGProcessingConfiguration(
minimumAnalysisDurationSeconds: 45,
filterTapCount: 61
)
).analyze(samples: samples)
XCTAssertEqual(result.status, .valid)
XCTAssertEqual(result.bpm.bpm ?? 0, 75, accuracy: 3)
XCTAssertTrue(result.quality.issues.contains(.poorFingerContact))
}
func testBeatMorphologyScoresDistortedCycleWithoutLosingCleanBeats() throws {
let sampleRate = 30.0
let periodSamples = 25
let sampleCount = periodSamples * 14 + 1
var signal = (0..<sampleCount).map { index in
cos(2 * Double.pi * Double(index % periodSamples) / Double(periodSamples))
}
// Preserve the two bounding pulse peaks but flatten one complete cycle.
for index in (periodSamples * 6 + 1)..<(periodSamples * 7) {
signal[index] = -0.15
}
let timestamps = (0..<sampleCount).map { Double($0) / sampleRate }
let result = try PPGPeakDetector.detect(
signal: signal,
timestampsSeconds: timestamps,
validityMask: Array(repeating: true, count: sampleCount),
configuration: PPGProcessingConfiguration(
minimumAnalysisDurationSeconds: 1,
minimumPeakProminence: 0.10,
minimumPeakAmplitude: 0.05
)
)
let scores = result.intervals.map(\.qualityScore)
XCTAssertLessThan(
scores.min() ?? 1,
SignalStatistics.median(scores) - 0.05,
"The flattened pulse must receive a distinctly lower technical quality"
)
XCTAssertGreaterThan(result.acceptedIntervals.count, 8)
}
private func makeIntervals(_ durations: [Double]) -> [PulseInterval] {
var elapsed = 0.0
return durations.enumerated().map { index, duration in
defer { elapsed += duration / 1_000 }
return PulseInterval(
sourceIndex: index,
startSeconds: elapsed,
endSeconds: elapsed + duration / 1_000,
durationMilliseconds: duration
)
}
}
}
@@ -0,0 +1,202 @@
import XCTest
@testable import HaptikaSignal
final class PPGSyntheticSignalTests: XCTestCase {
private let processorConfiguration = PPGProcessingConfiguration(
minimumAnalysisDurationSeconds: 45,
filterTapCount: 61
)
func testPublicationThresholdsRemainOrderedByScientificDemand() {
XCTAssertEqual(PPGPublicationThresholds.bpmQualityScore, 0.45)
XCTAssertEqual(PPGPublicationThresholds.potentialIndexQualityScore, 0.65)
XCTAssertEqual(PPGPublicationThresholds.prvQualityScore, 0.80)
XCTAssertLessThan(
PPGPublicationThresholds.bpmQualityScore,
PPGPublicationThresholds.potentialIndexQualityScore
)
XCTAssertLessThan(
PPGPublicationThresholds.potentialIndexQualityScore,
PPGPublicationThresholds.prvQualityScore
)
}
func testGeneratorIsDeterministicAndAppliesAllConfiguredArtifacts() throws {
let configuration = PPGSyntheticSignalConfiguration(
durationSeconds: 10,
startingBPM: 72,
intervalVariationMilliseconds: [-20, 20],
noiseAmplitude: 1.5,
driftAmplitude: 0.05,
movementWindows: [.init(startSeconds: 2, endSeconds: 3, intensity: 0.8)],
saturationWindows: [.init(startSeconds: 5, endSeconds: 6)],
dropEveryNthFrame: 10,
missingBeatIndices: [3],
falsePeakTimesSeconds: [7.25],
seed: 42
)
let first = try PPGSyntheticSignalGenerator.generate(configuration: configuration)
let second = try PPGSyntheticSignalGenerator.generate(configuration: configuration)
XCTAssertEqual(first, second)
XCTAssertLessThan(first.samples.count, 301)
XCTAssertTrue(first.samples.contains { $0.motionArtifactScore >= 0.8 })
XCTAssertTrue(first.samples.contains { $0.saturationRatio == 1 })
XCTAssertEqual(first.nominalIntervalsMilliseconds[0], 60_000 / 72 - 20, accuracy: 0.001)
XCTAssertEqual(first.nominalIntervalsMilliseconds[1], 60_000 / 72 + 20, accuracy: 0.001)
}
func testEndToEndCleanSignalsAt60_75And120BPM() throws {
let processor = try PPGSignalProcessor(configuration: processorConfiguration)
for expectedBPM in [60.0, 75.0, 120.0] {
let fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(
durationSeconds: 60,
startingBPM: expectedBPM,
noiseAmplitude: 0.25,
driftAmplitude: 0.04,
seed: UInt64(expectedBPM)
)
)
let result = try processor.analyze(samples: fixture.samples)
XCTAssertEqual(result.status, .valid, "Expected a valid \(expectedBPM) BPM fixture")
XCTAssertEqual(result.bpm.bpm ?? 0, expectedBPM, accuracy: 3)
XCTAssertFalse(result.quality.issues.contains(.nonFiniteInput))
}
}
func testModerateNoiseDriftAndDroppedFramesRemainAnalyzable() throws {
let fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(
durationSeconds: 60,
startingBPM: 78,
noiseAmplitude: 1.2,
driftAmplitude: 0.10,
dropEveryNthFrame: 20,
seed: 77
)
)
let result = try PPGSignalProcessor(configuration: processorConfiguration)
.analyze(samples: fixture.samples)
XCTAssertEqual(result.bpm.bpm ?? 0, 78, accuracy: 4)
XCTAssertGreaterThan(result.timebase.droppedFrameRatio, 0)
}
func testMovementAndSaturationAreReportedInsteadOfProducingSilentPRV() throws {
let fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(
durationSeconds: 60,
startingBPM: 72,
noiseAmplitude: 0.5,
movementWindows: [.init(startSeconds: 10, endSeconds: 45, intensity: 1)],
saturationWindows: [.init(startSeconds: 8, endSeconds: 52, intensity: 1)],
seed: 91
)
)
let result = try PPGSignalProcessor(configuration: processorConfiguration)
.analyze(samples: fixture.samples)
XCTAssertTrue(result.quality.issues.contains(.motionArtifacts))
XCTAssertTrue(result.quality.issues.contains(.excessiveSaturation))
XCTAssertFalse(result.prv.reliable)
}
func testMissingAndFalsePeaksDoNotCreateNonFiniteMetrics() throws {
let fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(
durationSeconds: 60,
startingBPM: 75,
noiseAmplitude: 0.4,
missingBeatIndices: [8, 22, 41],
falsePeakTimesSeconds: [12.30, 31.15, 46.40],
seed: 121
)
)
let result = try PPGSignalProcessor(configuration: processorConfiguration)
.analyze(samples: fixture.samples)
XCTAssertTrue(result.bpm.confidence.isFinite)
XCTAssertTrue(result.quality.score.isFinite)
XCTAssertTrue(result.intervals.allSatisfy { $0.durationMilliseconds.isFinite })
XCTAssertGreaterThan(result.bpm.rejectedPeakCount + result.prv.rejectedIntervalCount, 0)
}
func testProgressiveBPMChangeStaysInsideGeneratedRange() throws {
let fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(
durationSeconds: 60,
startingBPM: 60,
endingBPM: 90,
noiseAmplitude: 0.2,
seed: 3
)
)
let result = try PPGSignalProcessor(configuration: processorConfiguration)
.analyze(samples: fixture.samples)
XCTAssertNotNil(result.bpm.bpm)
XCTAssertTrue((60...90).contains(result.bpm.bpm ?? 0))
XCTAssertGreaterThan(fixture.nominalIntervalsMilliseconds.first ?? 0, 950)
XCTAssertLessThan(fixture.nominalIntervalsMilliseconds.last ?? 1_000, 700)
}
func testKnownIntervalVariabilityProducesExpectedRMSSD() throws {
let offsets = [-30.0, 30.0]
let fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(
durationSeconds: 60,
startingBPM: 75,
intervalVariationMilliseconds: offsets
)
)
let metrics = try PPGPRVCalculator.calculate(
intervalsMilliseconds: Array(fixture.nominalIntervalsMilliseconds.prefix(60)),
configuration: PPGProcessingConfiguration(minimumIntervalsForPRV: 20)
)
XCTAssertEqual(metrics.estimatedRmssdMs ?? 0, 60, accuracy: 0.001)
XCTAssertTrue(metrics.reliable)
}
func testNaNAndInfinityFramesAreDiscardedExhaustively() throws {
var fixture = try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(durationSeconds: 60, startingBPM: 72)
)
fixture.samples.insert(
PPGFrameSample(timestampSeconds: .nan, red: 1, green: 1, luminance: 1),
at: 20
)
fixture.samples.insert(
PPGFrameSample(timestampSeconds: 2, red: .infinity, green: 1, luminance: 1),
at: 40
)
fixture.samples.insert(
PPGFrameSample(timestampSeconds: 3, red: 1, green: -.infinity, luminance: 1),
at: 60
)
fixture.samples.insert(
PPGFrameSample(timestampSeconds: 4, red: 1, green: 1, luminance: .nan),
at: 80
)
let result = try PPGSignalProcessor(configuration: processorConfiguration)
.analyze(samples: fixture.samples)
XCTAssertEqual(result.timebase.discardedSampleCount, 4)
XCTAssertTrue(result.quality.issues.contains(.nonFiniteInput))
XCTAssertEqual(result.bpm.bpm ?? 0, 72, accuracy: 3)
}
func testGeneratorRejectsUnsafeConfiguration() {
XCTAssertThrowsError(
try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(sampleRateHz: .nan)
)
)
XCTAssertThrowsError(
try PPGSyntheticSignalGenerator.generate(
configuration: PPGSyntheticSignalConfiguration(dropEveryNthFrame: 1)
)
)
}
}
@@ -0,0 +1,887 @@
import CoreMedia
import Foundation
import NitroModules
import VisionCamera
/// Nitro entry point for camera PPG processing.
///
/// A `HybridFrameSpec` and its `CMSampleBuffer` exist only as local values during
/// `processFrame(frame:)`. Persistent state contains compact numeric metrics only.
final class HybridHaptikaPPGFrameProcessor: HybridHaptikaPPGFrameProcessorSpec {
private enum SessionPhase {
case idle
case active
case thermalStopped
case finishing
}
private struct MeasurementConfiguration: Sendable {
let sessionPurpose: PPGSessionPurpose
let targetSampleRate: Double
let minimumFrequencyHz: Double
let maximumFrequencyHz: Double
let stabilizationSeconds: Double
let measurementSeconds: Double
let emitEveryFrames: Int
let minimumContactConfidence: Double
let maximumInterpolationGapSeconds: Double
let longGapLimitSeconds: Double
let maximumBufferedSampleCount: Int
init(native: NativePPGConfiguration) throws {
guard native.targetSampleRate.isFinite,
native.targetSampleRate >= 20,
native.targetSampleRate <= 120
else {
throw HaptikaPPGFrameProcessorError.invalidConfiguration(
"targetSampleRate must be between 20 and 120 Hz"
)
}
guard native.minimumFrequencyHz.isFinite,
native.maximumFrequencyHz.isFinite,
native.minimumFrequencyHz > 0,
native.maximumFrequencyHz > native.minimumFrequencyHz,
native.maximumFrequencyHz < native.targetSampleRate / 2
else {
throw HaptikaPPGFrameProcessorError.invalidConfiguration(
"the physiological band must be ordered and below Nyquist"
)
}
guard native.stabilizationSeconds.isFinite,
native.stabilizationSeconds >= 0,
native.stabilizationSeconds <= 30
else {
throw HaptikaPPGFrameProcessorError.invalidConfiguration(
"stabilizationSeconds must be between 0 and 30"
)
}
let maximumDuration = PPGCoherencePolicy.maximumDurationSeconds(
isCoherence: native.sessionPurpose == .coherence
)
guard native.measurementSeconds.isFinite,
native.measurementSeconds >= 20,
native.measurementSeconds <= maximumDuration
else {
throw HaptikaPPGFrameProcessorError.invalidConfiguration(
"measurementSeconds exceeds the limit for this session purpose"
)
}
guard native.emitEveryFrames.isFinite,
native.emitEveryFrames >= 1,
native.emitEveryFrames.rounded() == native.emitEveryFrames,
native.emitEveryFrames <= 10_000
else {
throw HaptikaPPGFrameProcessorError.invalidConfiguration(
"emitEveryFrames must be a positive integer"
)
}
guard native.minimumContactConfidence.isFinite,
(0...1).contains(native.minimumContactConfidence)
else {
throw HaptikaPPGFrameProcessorError.invalidConfiguration(
"minimumContactConfidence must be between 0 and 1"
)
}
sessionPurpose = native.sessionPurpose
targetSampleRate = native.targetSampleRate
minimumFrequencyHz = native.minimumFrequencyHz
maximumFrequencyHz = native.maximumFrequencyHz
stabilizationSeconds = native.stabilizationSeconds
measurementSeconds = native.measurementSeconds
emitEveryFrames = Int(native.emitEveryFrames)
minimumContactConfidence = native.minimumContactConfidence
maximumInterpolationGapSeconds = 0.25
longGapLimitSeconds = 0.80
// This bound permits a full measurement plus recovery time after contact interruptions.
let maximumBufferedDuration = sessionPurpose == .coherence
? PPGCoherencePolicy.coherenceBufferDurationSeconds
: stabilizationSeconds + measurementSeconds + 60
maximumBufferedSampleCount = max(
Int((targetSampleRate * maximumBufferedDuration).rounded(.up)),
256
)
}
var coreConfiguration: PPGProcessingConfiguration {
PPGProcessingConfiguration(
targetSampleRateHz: targetSampleRate,
minimumFrequencyHz: minimumFrequencyHz,
maximumFrequencyHz: maximumFrequencyHz,
// The UI may finish as soon as 50 clean beats have been collected.
// Keep a 45 s floor for HRV while allowing completion before the 90 s
// safety limit on a clean signal.
minimumAnalysisDurationSeconds: min(max(30, measurementSeconds * 0.75), 45),
maximumInterpolationGapSeconds: maximumInterpolationGapSeconds,
longGapLimitSeconds: longGapLimitSeconds,
minimumContactConfidence: minimumContactConfidence,
maximumSaturationRatio: 0.95,
maximumMotionArtifactScore: 0.95,
minimumPeakProminence: 0.35,
minimumPeakAmplitude: 0.10,
refractoryPeriodSeconds: 0.25,
maximumMethodDisagreementBPM: 8,
minimumQualityScore: 0.55,
minimumIntervalsForPRV: 35,
maximumRejectedIntervalRatioForPRV: 0.45
)
}
}
private struct SessionSnapshot: Sendable {
let configuration: MeasurementConfiguration
let samples: [PPGFrameSample]
let startedAtMonotonicSeconds: Double
let endedAtMonotonicSeconds: Double
let processedFrameCount: Int
let droppedFrameCount: Int
let acquisitionDroppedFrameCount: Int
let usableDurationSeconds: Double
let maximumAcquisitionGapSeconds: Double
let bufferLimitReached: Bool
let terminalReason: String?
}
private let stateLock = NSLock()
private let metricsExtractor = PPGFrameMetricsExtractor()
private let thermalMonitor = PPGThermalMonitor()
private var phase: SessionPhase = .idle
private var configuration: MeasurementConfiguration?
private var contactDetector: PPGFingerContactDetector?
private var terminalThermalState: ThermalState?
private var numericSamples: [PPGFrameSample] = []
private var startedAtMonotonicSeconds = 0.0
private var terminalReason: String?
private var bufferLimitReached = false
private var processedFrameCount = 0
private var droppedFrameCount = 0
private var acquisitionDroppedFrameCount = 0
private var lastObservedTimestamp: Double?
private var recentFrameIntervals: [Double] = []
private var maximumAcquisitionGapSeconds = 0.0
private var validContactStartedAt: Double?
private var acquisitionStartedAt: Double?
private var lastUsableTimestamp: Double?
private var usableDurationSeconds = 0.0
private var lastMetrics: PPGFrameColorMetrics?
private var lastContactEvaluation: PPGContactEvaluation?
private var liveBpm: Double?
private var liveIpiMs: Double?
private var liveBeatSequence = 0
private var lastLiveAnalysisFrame = 0
private var liveBeatTracker = PPGLiveBeatTracker()
private var pendingLiveBeats: [NativeAcceptedBeat] = []
private var liveWindowQuality: Double?
private var totalCollectedSampleCount = 0
override init() {
recentFrameIntervals.reserveCapacity(90)
super.init()
}
func startMeasurement(configuration nativeConfiguration: NativePPGConfiguration) throws {
let parsedConfiguration = try MeasurementConfiguration(native: nativeConfiguration)
let thermalState = thermalMonitor.currentState()
guard !Self.isUnsafe(thermalState) else {
throw HaptikaPPGFrameProcessorError.unsafeThermalState(thermalState.stringValue)
}
stateLock.lock()
defer { stateLock.unlock() }
resetLocked()
configuration = parsedConfiguration
contactDetector = PPGFingerContactDetector(
minimumConfidence: parsedConfiguration.minimumContactConfidence
)
numericSamples.reserveCapacity(
min(
parsedConfiguration.maximumBufferedSampleCount,
Int((parsedConfiguration.targetSampleRate * parsedConfiguration.measurementSeconds).rounded(.up))
)
)
startedAtMonotonicSeconds = ProcessInfo.processInfo.systemUptime
phase = .active
}
func processFrame(frame: any HybridFrameSpec) throws -> PPGFrameUpdate? {
stateLock.lock()
defer { stateLock.unlock() }
let thermalState = thermalMonitor.currentState()
if phase == .thermalStopped {
// Keep notifying the worklet until React Native acknowledges the terminal state and tears
// the capture session down. A single callback could be lost while the JS runtime is busy.
return makeFrameUpdate(
timestampSeconds: Self.finiteTimestamp(frame.timestamp, fallback: lastObservedTimestamp),
thermalState: terminalThermalState ?? thermalState,
metrics: lastMetrics,
evaluation: lastContactEvaluation
)
}
guard phase == .active, let configuration, let contactDetector else {
// Camera teardown and Nitro calls can overlap by a frame; ignoring it is safe and avoids
// retaining an already-finished frame.
return nil
}
if Self.isUnsafe(thermalState) {
phase = .thermalStopped
terminalThermalState = thermalState
terminalReason = "thermal_\(thermalState.stringValue)"
processedFrameCount += 1
return makeFrameUpdate(
timestampSeconds: Self.finiteTimestamp(frame.timestamp, fallback: lastObservedTimestamp),
thermalState: thermalState,
metrics: lastMetrics,
evaluation: lastContactEvaluation
)
}
guard frame.isValid,
let nativeFrame = frame as? any NativeFrame,
let sampleBuffer = nativeFrame.sampleBuffer
else {
throw HaptikaPPGFrameProcessorError.frameUnavailable
}
// The sample buffer is intentionally scoped to this call. The extractor returns value-only
// metrics and never stores the CMSampleBuffer or CVPixelBuffer.
let metrics = try metricsExtractor.extract(from: sampleBuffer)
processedFrameCount += 1
let shouldEmit = processedFrameCount.isMultiple(of: configuration.emitEveryFrames)
guard recordFrameTimingLocked(
timestampSeconds: metrics.timestampSeconds,
configuration: configuration
) else {
if shouldEmit {
return makeFrameUpdate(
timestampSeconds: lastObservedTimestamp ?? metrics.timestampSeconds,
thermalState: thermalState,
metrics: lastMetrics,
evaluation: lastContactEvaluation
)
}
return nil
}
let evaluation = contactDetector.evaluate(metrics)
lastMetrics = metrics
lastContactEvaluation = evaluation
updateAcquisitionLocked(
metrics: metrics,
evaluation: evaluation,
configuration: configuration
)
updateLiveEstimateLocked(configuration: configuration)
guard shouldEmit else { return nil }
return makeFrameUpdate(
timestampSeconds: metrics.timestampSeconds,
thermalState: thermalState,
metrics: metrics,
evaluation: evaluation
)
}
func finishMeasurement() throws -> NativePPGProcessingResult {
let snapshot: SessionSnapshot
stateLock.lock()
switch phase {
case .active, .thermalStopped:
guard let configuration else {
stateLock.unlock()
throw HaptikaPPGFrameProcessorError.measurementNotStarted
}
phase = .finishing
snapshot = SessionSnapshot(
configuration: configuration,
samples: numericSamples,
startedAtMonotonicSeconds: startedAtMonotonicSeconds,
endedAtMonotonicSeconds: ProcessInfo.processInfo.systemUptime,
processedFrameCount: processedFrameCount,
droppedFrameCount: droppedFrameCount,
acquisitionDroppedFrameCount: acquisitionDroppedFrameCount,
usableDurationSeconds: usableDurationSeconds,
maximumAcquisitionGapSeconds: maximumAcquisitionGapSeconds,
bufferLimitReached: bufferLimitReached,
terminalReason: terminalReason
)
stateLock.unlock()
case .idle, .finishing:
stateLock.unlock()
throw HaptikaPPGFrameProcessorError.measurementNotStarted
}
defer { resetAfterFinishing() }
if let terminalReason = snapshot.terminalReason {
return makeInvalidResult(
snapshot: snapshot,
reason: terminalReason,
issue: "durationInsufficient"
)
}
if snapshot.bufferLimitReached {
return makeInvalidResult(
snapshot: snapshot,
reason: "The numeric measurement buffer reached its safety limit.",
issue: "nonFiniteProcessingResult"
)
}
do {
let processor = try PPGSignalProcessor(
configuration: snapshot.configuration.coreConfiguration
)
let analysis = try processor.analyze(samples: snapshot.samples)
return makeNativeResult(analysis: analysis, snapshot: snapshot)
} catch {
let mapped = Self.mapProcessingError(error)
return makeInvalidResult(
snapshot: snapshot,
reason: mapped.reason,
issue: mapped.issue
)
}
}
func cancelMeasurement() throws {
stateLock.lock()
resetLocked()
stateLock.unlock()
}
func getThermalState() throws -> ThermalState {
thermalMonitor.currentState()
}
private func recordFrameTimingLocked(
timestampSeconds: Double,
configuration: MeasurementConfiguration
) -> Bool {
guard timestampSeconds.isFinite, timestampSeconds >= 0 else {
droppedFrameCount += 1
if acquisitionStartedAt != nil { acquisitionDroppedFrameCount += 1 }
return false
}
guard let previousTimestamp = lastObservedTimestamp else {
lastObservedTimestamp = timestampSeconds
return true
}
let interval = timestampSeconds - previousTimestamp
guard interval > 0 else {
droppedFrameCount += 1
if acquisitionStartedAt != nil { acquisitionDroppedFrameCount += 1 }
return false
}
if acquisitionStartedAt != nil {
maximumAcquisitionGapSeconds = max(maximumAcquisitionGapSeconds, interval)
}
if recentFrameIntervals.count >= 5 {
let expectedInterval = SignalStatistics.median(recentFrameIntervals)
if expectedInterval > 0, interval > expectedInterval * 1.5 {
let estimatedMissing = min(
max((interval / expectedInterval).rounded() - 1, 0),
100_000
)
let missingCount = Int(estimatedMissing)
droppedFrameCount += missingCount
if acquisitionStartedAt != nil { acquisitionDroppedFrameCount += missingCount }
}
}
if interval <= max(configuration.maximumInterpolationGapSeconds, 0.25) {
recentFrameIntervals.append(interval)
if recentFrameIntervals.count > 90 {
recentFrameIntervals.removeFirst(recentFrameIntervals.count - 90)
}
}
lastObservedTimestamp = timestampSeconds
return true
}
private func updateAcquisitionLocked(
metrics: PPGFrameColorMetrics,
evaluation: PPGContactEvaluation,
configuration: MeasurementConfiguration
) {
// Acquisition is intentionally clock-based rather than contact-gated. A
// contact detector is useful feedback, but making it a hard gate can leave
// a complete minute with zero samples on cameras whose color response is
// different. The signal processor still rejects low-SNR, saturated and
// motion-corrupted portions at the end.
// Store compact numeric samples from the first camera frame. The final
// processor decides which portions are usable. Do not derive a camera
// frame count from the 100 Hz resampling target, and do not throw away a
// good signal merely because the contact heuristic is conservative.
if acquisitionStartedAt == nil {
acquisitionStartedAt = metrics.timestampSeconds
lastUsableTimestamp = nil
}
guard acquisitionStartedAt != nil else { return }
let sample = PPGFrameSample(
timestampSeconds: metrics.timestampSeconds,
red: metrics.redMean,
green: metrics.greenMean,
blue: metrics.blueMean,
luminance: metrics.luminanceMean,
contactConfidence: evaluation.confidence,
saturationRatio: metrics.saturationRatio,
motionArtifactScore: evaluation.motionArtifactScore
)
totalCollectedSampleCount += 1
if configuration.sessionPurpose == .coherence {
numericSamples.append(sample)
if numericSamples.count > configuration.maximumBufferedSampleCount {
numericSamples.removeFirst(numericSamples.count - configuration.maximumBufferedSampleCount)
}
} else if numericSamples.count < configuration.maximumBufferedSampleCount {
numericSamples.append(sample)
} else {
bufferLimitReached = true
}
let opticallyUsable = evaluation.confidence >= 0.35
&& metrics.saturationRatio <= 0.95
&& evaluation.motionArtifactScore <= 0.90
if opticallyUsable {
if let previousTimestamp = lastUsableTimestamp {
let interval = metrics.timestampSeconds - previousTimestamp
if interval > 0, interval <= configuration.maximumInterpolationGapSeconds {
usableDurationSeconds += interval
}
}
lastUsableTimestamp = metrics.timestampSeconds
} else {
// Never bridge quality time across a finger removal or a large movement.
lastUsableTimestamp = nil
}
}
private func makeFrameUpdate(
timestampSeconds: Double,
thermalState: ThermalState,
metrics: PPGFrameColorMetrics?,
evaluation: PPGContactEvaluation?
) -> PPGFrameUpdate {
let signalLevel = min(max((evaluation?.pulsatileAmplitude ?? 0) / 0.03, 0), 1)
let waveformValue: Double
if let metrics {
waveformValue = metrics.greenMean / max(metrics.redMean + metrics.greenMean + metrics.blueMean, 1)
} else {
waveformValue = 0
}
return PPGFrameUpdate(
timestampSeconds: timestampSeconds,
contactState: evaluation?.state ?? .absent,
contactConfidence: min(max(evaluation?.confidence ?? 0, 0), 1),
signalLevel: signalLevel,
saturationRatio: min(max(metrics?.saturationRatio ?? 0, 0), 1),
motionArtifactScore: min(max(evaluation?.motionArtifactScore ?? 0, 0), 1),
collectedSampleCount: Double(totalCollectedSampleCount),
usableDurationSeconds: usableDurationSeconds,
droppedFrameCount: Double(droppedFrameCount),
thermalState: thermalState,
waveformValue: waveformValue,
liveBpm: liveBpm,
liveIpiMs: liveIpiMs,
beatSequence: Double(liveBeatSequence),
acceptedBeats: takePendingLiveBeatsLocked(),
liveWindowQuality: liveWindowQuality
)
}
private func takePendingLiveBeatsLocked() -> [NativeAcceptedBeat] {
let beats = pendingLiveBeats
pendingLiveBeats.removeAll(keepingCapacity: true)
return beats
}
private func updateLiveEstimateLocked(configuration: MeasurementConfiguration) {
// Camera input is constrained to 30 fps. The final 100 Hz configuration
// is a resampling timebase and must not delay live BPM warm-up to 23 s.
let liveInputSampleRate = 30.0
guard numericSamples.count >= Int(liveInputSampleRate * 7),
processedFrameCount - lastLiveAnalysisFrame >= 15
else { return }
lastLiveAnalysisFrame = processedFrameCount
let maximumWindowCount = Int(liveInputSampleRate * 15)
let window = Array(numericSamples.suffix(maximumWindowCount))
let liveConfiguration = PPGProcessingConfiguration(
// A lightweight 30 Hz pass is enough for the live display. The final
// analysis still uses the regular 100 Hz timebase.
targetSampleRateHz: min(configuration.targetSampleRate, 30),
minimumFrequencyHz: configuration.minimumFrequencyHz,
maximumFrequencyHz: configuration.maximumFrequencyHz,
minimumAnalysisDurationSeconds: 6,
maximumInterpolationGapSeconds: configuration.maximumInterpolationGapSeconds,
longGapLimitSeconds: configuration.longGapLimitSeconds,
minimumContactConfidence: min(configuration.minimumContactConfidence, 0.45),
maximumSaturationRatio: 0.55,
maximumMotionArtifactScore: 0.65,
minimumPeakProminence: 0.28,
minimumPeakAmplitude: 0.08,
refractoryPeriodSeconds: 0.30,
maximumMethodDisagreementBPM: 12,
minimumQualityScore: 0.35,
minimumIntervalsForPRV: 4,
maximumRejectedIntervalRatioForPRV: 0.45,
filterTapCount: 31
)
guard let analysis = try? PPGSignalProcessor(configuration: liveConfiguration)
.analyze(samples: window)
else { return }
liveWindowQuality = analysis.quality.score
if let bpm = analysis.bpm.bpm ?? analysis.bpm.temporalBpm,
bpm.isFinite, (35...220).contains(bpm) {
liveBpm = bpm
}
let acceptedIntervals = analysis.intervals.filter(\.isAccepted)
if let interval = acceptedIntervals.last,
interval.durationMilliseconds.isFinite {
liveIpiMs = interval.durationMilliseconds
}
// The live analysis uses an overlapping window. Count every newly seen
// accepted interval instead of incrementing only once per analysis pass;
// otherwise a 90 BPM pulse sampled every 0.5 s loses real beats.
let newlyAccepted = liveBeatTracker.ingest(acceptedIntervals)
for beat in newlyAccepted {
liveBeatSequence = beat.sequence
pendingLiveBeats.append(
NativeAcceptedBeat(
sequence: Double(beat.sequence),
timestampSeconds: beat.timestampSeconds,
intervalMs: beat.intervalMs,
confidence: beat.confidence
)
)
}
}
private func makeNativeResult(
analysis: PPGAnalysisResult,
snapshot: SessionSnapshot
) -> NativePPGProcessingResult {
var issues = analysis.quality.issues.map(Self.nativeIssue)
if analysis.bpm.rejectedPeakCount > max(3, analysis.bpm.validPeakCount / 4) {
Self.appendUnique("tooManyRejectedPeaks", to: &issues)
}
let observedDroppedRatio = snapshot.samples.isEmpty
? 0
: Double(snapshot.acquisitionDroppedFrameCount)
/ Double(snapshot.samples.count + snapshot.acquisitionDroppedFrameCount)
let droppedFrameRatio = min(
max(max(analysis.quality.droppedFrameRatio, observedDroppedRatio), 0),
1
)
if droppedFrameRatio > 0.12 {
Self.appendUnique("droppedFrames", to: &issues)
}
let timestampJitterRatio = Self.timestampJitterRatio(analysis.timebase)
let longestGapSeconds = max(
analysis.timebase.maximumGapSeconds,
snapshot.maximumAcquisitionGapSeconds
)
let isValid = analysis.status == .valid
let invalidReason = isValid ? nil : Self.invalidReason(for: issues)
let resampledSampleCount = max(
Int((analysis.timebase.durationSeconds * snapshot.configuration.targetSampleRate).rounded(.down)) + 1,
0
)
return NativePPGProcessingResult(
valid: isValid,
invalidReason: invalidReason,
bpm: NativeBPMEstimate(
bpm: analysis.bpm.bpm,
temporalBpm: analysis.bpm.temporalBpm,
frequencyBpm: analysis.bpm.frequencyBpm,
methodAgreementBpm: analysis.bpm.methodAgreementBpm,
validPeakCount: Double(analysis.bpm.validPeakCount),
rejectedPeakCount: Double(analysis.bpm.rejectedPeakCount),
confidence: analysis.bpm.confidence,
selectedChannel: analysis.bpm.selectedChannel.rawValue
),
prv: NativePRVMetrics(
estimatedRmssdMs: analysis.prv.estimatedRmssdMs,
estimatedSdnnMs: analysis.prv.estimatedSdnnMs,
meanIpiMs: analysis.prv.meanIpiMs,
medianIpiMs: analysis.prv.medianIpiMs,
pnn50Percent: analysis.prv.pnn50Percent,
rangeIpiMs: analysis.prv.rangeIpiMs,
modeIpiMs: analysis.prv.modeIpiMs,
amo50Percent: analysis.prv.amo50Percent,
coefficientOfVariation: analysis.prv.coefficientOfVariation,
validIntervalCount: Double(analysis.prv.validIntervalCount),
rejectedIntervalCount: Double(analysis.prv.rejectedIntervalCount),
rejectedIntervalRatio: analysis.prv.rejectedIntervalRatio,
reliable: analysis.prv.reliable
),
quality: NativeSignalQualityResult(
score: analysis.quality.score,
level: analysis.quality.level.rawValue,
usableDurationSeconds: analysis.quality.usableDurationSeconds,
droppedFrameRatio: droppedFrameRatio,
saturationRatio: analysis.quality.saturationRatio,
rejectedIntervalRatio: analysis.quality.rejectedIntervalRatio,
signalToNoiseRatio: analysis.quality.signalToNoiseRatio,
motionArtifactRatio: analysis.quality.motionArtifactRatio,
methodAgreementScore: analysis.quality.methodAgreementScore,
timestampJitterRatio: timestampJitterRatio,
longestGapSeconds: longestGapSeconds,
issues: issues
),
startedAtMonotonicSeconds: snapshot.startedAtMonotonicSeconds,
endedAtMonotonicSeconds: snapshot.endedAtMonotonicSeconds,
rawSampleCount: Double(snapshot.samples.count),
resampledSampleCount: Double(resampledSampleCount)
)
}
private func makeInvalidResult(
snapshot: SessionSnapshot,
reason: String,
issue: String
) -> NativePPGProcessingResult {
let intervals = zip(snapshot.samples, snapshot.samples.dropFirst()).compactMap { pair -> Double? in
let interval = pair.1.timestampSeconds - pair.0.timestampSeconds
return interval.isFinite && interval > 0 ? interval : nil
}
let estimatedRate = intervals.isEmpty ? 0 : 1 / max(SignalStatistics.median(intervals), 1e-9)
let intervalMAD = intervals.isEmpty ? 0 : SignalStatistics.mad(intervals)
let jitterRatio = estimatedRate > 0 ? intervalMAD * estimatedRate : 0
let saturation = snapshot.samples.isEmpty
? 0
: SignalStatistics.mean(snapshot.samples.map(\.saturationRatio))
let motion = snapshot.samples.isEmpty
? 0
: SignalStatistics.mean(snapshot.samples.map(\.motionArtifactScore))
let droppedRatio = snapshot.samples.isEmpty
? 0
: Double(snapshot.acquisitionDroppedFrameCount)
/ Double(snapshot.samples.count + snapshot.acquisitionDroppedFrameCount)
return NativePPGProcessingResult(
valid: false,
invalidReason: reason,
bpm: NativeBPMEstimate(
bpm: nil,
temporalBpm: nil,
frequencyBpm: nil,
methodAgreementBpm: nil,
validPeakCount: 0,
rejectedPeakCount: 0,
confidence: 0,
selectedChannel: "none"
),
prv: NativePRVMetrics(
estimatedRmssdMs: nil,
estimatedSdnnMs: nil,
meanIpiMs: nil,
medianIpiMs: nil,
pnn50Percent: nil,
rangeIpiMs: nil,
modeIpiMs: nil,
amo50Percent: nil,
coefficientOfVariation: nil,
validIntervalCount: 0,
rejectedIntervalCount: 0,
rejectedIntervalRatio: snapshot.samples.isEmpty ? 1 : 0,
reliable: false
),
quality: NativeSignalQualityResult(
score: 0,
level: "invalid",
usableDurationSeconds: snapshot.usableDurationSeconds,
droppedFrameRatio: min(max(droppedRatio, 0), 1),
saturationRatio: min(max(saturation, 0), 1),
rejectedIntervalRatio: snapshot.samples.isEmpty ? 1 : 0,
signalToNoiseRatio: nil,
motionArtifactRatio: min(max(motion, 0), 1),
methodAgreementScore: 0,
timestampJitterRatio: max(jitterRatio, 0),
longestGapSeconds: snapshot.maximumAcquisitionGapSeconds,
issues: [issue]
),
startedAtMonotonicSeconds: snapshot.startedAtMonotonicSeconds,
endedAtMonotonicSeconds: snapshot.endedAtMonotonicSeconds,
rawSampleCount: Double(snapshot.samples.count),
resampledSampleCount: 0
)
}
private func resetAfterFinishing() {
stateLock.lock()
resetLocked()
stateLock.unlock()
}
private func resetLocked() {
contactDetector?.reset()
contactDetector = nil
configuration = nil
terminalThermalState = nil
numericSamples.removeAll(keepingCapacity: false)
phase = .idle
startedAtMonotonicSeconds = 0
terminalReason = nil
bufferLimitReached = false
processedFrameCount = 0
droppedFrameCount = 0
acquisitionDroppedFrameCount = 0
lastObservedTimestamp = nil
recentFrameIntervals.removeAll(keepingCapacity: true)
maximumAcquisitionGapSeconds = 0
validContactStartedAt = nil
acquisitionStartedAt = nil
lastUsableTimestamp = nil
usableDurationSeconds = 0
lastMetrics = nil
lastContactEvaluation = nil
liveBpm = nil
liveIpiMs = nil
liveBeatSequence = 0
liveBeatTracker.reset()
lastLiveAnalysisFrame = 0
pendingLiveBeats.removeAll(keepingCapacity: false)
liveWindowQuality = nil
totalCollectedSampleCount = 0
}
private static func isUnsafe(_ state: ThermalState) -> Bool {
PPGThermalSafetyPolicy.shouldStopMeasurement(forStateName: state.stringValue)
}
private static func finiteTimestamp(_ timestamp: Double, fallback: Double?) -> Double {
if timestamp.isFinite, timestamp >= 0 { return timestamp }
return fallback ?? 0
}
private static func timestampJitterRatio(_ diagnostics: TimebaseDiagnostics) -> Double {
guard diagnostics.estimatedSampleRateHz > 0 else { return 0 }
return max(diagnostics.frameIntervalMADSeconds * diagnostics.estimatedSampleRateHz, 0)
}
private static func nativeIssue(_ issue: SignalQualityIssue) -> String {
switch issue {
case .insufficientDuration:
return "durationInsufficient"
case .invalidTimestamps, .irregularTimestamps:
return "timestampJitter"
case .longInterruption:
return "longGap"
case .tooManyDroppedFrames:
return "droppedFrames"
case .poorFingerContact:
return "contactInsufficient"
case .excessiveSaturation:
return "saturation"
case .motionArtifacts:
return "motionArtifacts"
case .lowPulsatileAmplitude:
return "lowPulsatileAmplitude"
case .lowSignalToNoise:
return "lowSignalToNoiseRatio"
case .tooFewPeaks:
return "tooFewPeaks"
case .inconsistentBPMMethods:
return "methodDisagreement"
case .tooManyRejectedIntervals:
return "tooManyRejectedIntervals"
case .unstableChannel:
return "channelInstability"
case .nonFiniteInput:
return "nonFiniteProcessingResult"
}
}
private static func appendUnique(_ issue: String, to issues: inout [String]) {
if !issues.contains(issue) { issues.append(issue) }
}
private static func invalidReason(for issues: [String]) -> String {
guard let issue = issues.first else {
return "La qualité du signal est insuffisante."
}
switch issue {
case "contactInsufficient", "contactInterrupted":
return "Le contact du doigt a été insuffisant ou interrompu."
case "durationInsufficient":
return "La durée de signal exploitable est insuffisante."
case "droppedFrames", "timestampJitter", "longGap":
return "La capture comporte trop d’interruptions ou d’images manquantes."
case "saturation":
return "Le signal caméra est trop saturé."
case "motionArtifacts":
return "Trop de mouvements ont perturbé la mesure."
case "lowPulsatileAmplitude", "lowSignalToNoiseRatio":
return "Le signal pulsatile est trop faible ou trop bruité."
case "tooFewPeaks", "tooManyRejectedPeaks", "tooManyRejectedIntervals":
return "Trop peu de pulsations fiables ont été détectées."
case "methodDisagreement":
return "Les estimations temporelle et fréquentielle ne concordent pas."
case "channelInstability":
return "Aucun canal couleur n’est suffisamment stable."
default:
return "Le traitement numérique du signal a produit un résultat invalide."
}
}
private static func mapProcessingError(_ error: Error) -> (reason: String, issue: String) {
guard let signalError = error as? PPGSignalError else {
return ("Le traitement numérique du signal a échoué.", "nonFiniteProcessingResult")
}
switch signalError {
case .insufficientSamples, .insufficientDuration:
return ("La durée de signal exploitable est insuffisante.", "durationInsufficient")
case .invalidTimestamps:
return ("Les horodatages caméra sont invalides.", "timestampJitter")
case .noUsableChannel:
return ("Aucun canal couleur n’est exploitable.", "channelInstability")
case .invalidConfiguration:
return ("La configuration du traitement du signal est invalide.", "nonFiniteProcessingResult")
}
}
}
private enum HaptikaPPGFrameProcessorError: LocalizedError {
case invalidConfiguration(String)
case unsafeThermalState(String)
case frameUnavailable
case measurementNotStarted
var errorDescription: String? {
switch self {
case .invalidConfiguration(let reason):
return "Invalid PPG configuration: \(reason)."
case .unsafeThermalState(let state):
return "PPG measurement refused because the iPhone thermal state is \(state)."
case .frameUnavailable:
return "The VisionCamera frame is invalid or does not expose a CMSampleBuffer."
case .measurementNotStarted:
return "No PPG measurement is currently active."
}
}
}
@@ -0,0 +1,132 @@
import Foundation
struct PPGContactEvaluation: Sendable {
let state: FingerContactState
let confidence: Double
let pulsatileAmplitude: Double
let motionArtifactScore: Double
}
/// Camera-agnostic coverage detector.
///
/// Contact is deliberately not a prerequisite for collecting frames. This
/// class only coaches the user and supplies soft quality metadata. The final
/// decision is made from the periodic optical signal, never from skin colour.
final class PPGFingerContactDetector {
private let requiredStableFrames = 6
private let requiredReleaseFrames = 12
private let historyLimit = 90
private var history: [PPGFrameColorMetrics] = []
private var state: FingerContactState = .adjusting
private var stableFrames = 0
private var releaseFrames = 0
init(minimumConfidence _: Double) {
history.reserveCapacity(historyLimit)
}
func reset() {
history.removeAll(keepingCapacity: true)
state = .adjusting
stableFrames = 0
releaseFrames = 0
}
func evaluate(_ metrics: PPGFrameColorMetrics) -> PPGContactEvaluation {
let previous = history.last
history.append(metrics)
if history.count > historyLimit {
history.removeFirst(history.count - historyLimit)
}
let brightness = metrics.luminanceMean / 255
let red = metrics.redMean / 255
let green = metrics.greenMean / 255
let blue = metrics.blueMean / 255
let chroma = max(red, green, blue) - min(red, green, blue)
let spatialDeviation = sqrt(max(metrics.redVariance + metrics.greenVariance, 0) / 2)
let uniformity = clamp01(1 - spatialDeviation / 82)
let exposure = smoothWindow(brightness, lower: 0.025, upper: 0.985, feather: 0.10)
let warmth = clamp01(0.5 + (red - blue) * 1.4)
let coverage = clamp01(0.45 * uniformity + 0.30 * exposure + 0.15 * warmth + 0.10 * min(chroma * 3, 1))
let motion = motionScore(current: metrics, previous: previous)
let amplitude = pulsatileAmplitude()
let temporalStability = clamp01(1 - motion / 0.55)
let confidence = clamp01(0.72 * coverage + 0.28 * temporalStability)
let candidate: FingerContactState
if brightness < 0.015 || (uniformity < 0.12 && warmth < 0.25) {
candidate = .absent
} else if brightness > 0.992 || metrics.saturationRatio > 0.92 {
candidate = .overexposed
} else if history.count >= 15 && motion > 0.68 {
candidate = .unstable
} else if confidence >= 0.48 {
candidate = .valid
} else {
candidate = .adjusting
}
applyHysteresis(candidate)
return PPGContactEvaluation(
state: state,
confidence: confidence,
pulsatileAmplitude: amplitude,
motionArtifactScore: motion
)
}
private func applyHysteresis(_ candidate: FingerContactState) {
if candidate == .valid {
stableFrames += 1
releaseFrames = 0
if stableFrames >= requiredStableFrames { state = .valid }
return
}
stableFrames = 0
releaseFrames += 1
// Brief changes are feedback, not session resets. Holding the last valid
// state avoids a flashing UI while automatic exposure settles.
if state == .valid && releaseFrames < requiredReleaseFrames { return }
state = candidate
}
private func pulsatileAmplitude() -> Double {
let window = history.suffix(60)
guard window.count >= 20 else { return 0 }
let normalized = window.map {
$0.greenMean / max($0.redMean + $0.greenMean + $0.blueMean, 1)
}
return max((normalized.max() ?? 0) - (normalized.min() ?? 0), 0)
}
private func motionScore(
current: PPGFrameColorMetrics,
previous: PPGFrameColorMetrics?
) -> Double {
guard let previous else { return 0 }
let lightDelta = abs(current.luminanceMean - previous.luminanceMean) / 255
let redDelta = abs(current.redMean - previous.redMean) / 255
let greenDelta = abs(current.greenMean - previous.greenMean) / 255
// Normal pulse modulation is small. Only large frame-to-frame steps are
// considered motion, regardless of the absolute colour of the fingertip.
return clamp01(max(lightDelta / 0.16, (redDelta + greenDelta) / 0.28))
}
private func smoothWindow(
_ value: Double,
lower: Double,
upper: Double,
feather: Double
) -> Double {
if value <= lower || value >= upper { return 0 }
if value < lower + feather { return clamp01((value - lower) / feather) }
if value > upper - feather { return clamp01((upper - value) / feather) }
return 1
}
private func clamp01(_ value: Double) -> Double {
min(max(value.isFinite ? value : 0, 0), 1)
}
}
@@ -0,0 +1,237 @@
import CoreMedia
import CoreVideo
import Foundation
struct PPGFrameColorMetrics: Sendable {
let timestampSeconds: Double
let redMean: Double
let greenMean: Double
let blueMean: Double
let redVariance: Double
let greenVariance: Double
let luminanceMean: Double
let saturationRatio: Double
}
enum PPGFrameMetricsExtractorError: Error {
case invalidTimestamp
case unsupportedPixelFormat(OSType)
case pixelBufferLockFailed(CVReturn)
case missingPlane
case missingBaseAddress
case emptyRegionOfInterest
}
/// Extracts compact color statistics from a central ROI without retaining or copying a frame.
final class PPGFrameMetricsExtractor {
private let gridSize: Int
private let roiFraction: Double
init(gridSize: Int = 32, roiFraction: Double = 0.55) {
self.gridSize = max(8, gridSize)
self.roiFraction = min(max(roiFraction, 0.2), 0.9)
}
func extract(from sampleBuffer: CMSampleBuffer) throws -> PPGFrameColorMetrics {
let timestamp = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
guard timestamp.isFinite, timestamp >= 0 else {
throw PPGFrameMetricsExtractorError.invalidTimestamp
}
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
throw PPGFrameMetricsExtractorError.missingBaseAddress
}
let lockFlags = CVPixelBufferLockFlags.readOnly
let lockStatus = CVPixelBufferLockBaseAddress(pixelBuffer, lockFlags)
guard lockStatus == kCVReturnSuccess else {
throw PPGFrameMetricsExtractorError.pixelBufferLockFailed(lockStatus)
}
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, lockFlags) }
let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
switch pixelFormat {
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
return try extractBiPlanarYUV(
from: pixelBuffer,
timestamp: timestamp,
isFullRange: true
)
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
return try extractBiPlanarYUV(
from: pixelBuffer,
timestamp: timestamp,
isFullRange: false
)
case kCVPixelFormatType_32BGRA:
return try extractBGRA(from: pixelBuffer, timestamp: timestamp)
default:
throw PPGFrameMetricsExtractorError.unsupportedPixelFormat(pixelFormat)
}
}
private func extractBiPlanarYUV(
from pixelBuffer: CVPixelBuffer,
timestamp: Double,
isFullRange: Bool
) throws -> PPGFrameColorMetrics {
guard CVPixelBufferGetPlaneCount(pixelBuffer) >= 2 else {
throw PPGFrameMetricsExtractorError.missingPlane
}
guard let yBase = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0),
let uvBase = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)
else {
throw PPGFrameMetricsExtractorError.missingBaseAddress
}
let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0)
let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0)
let yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
let uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
let yBytes = yBase.assumingMemoryBound(to: UInt8.self)
let uvBytes = uvBase.assumingMemoryBound(to: UInt8.self)
let roi = regionOfInterest(width: width, height: height)
let stepX = max(roi.width / gridSize, 1)
let stepY = max(roi.height / gridSize, 1)
var accumulator = ColorAccumulator()
for y in stride(from: roi.minY, to: roi.maxY, by: stepY) {
for x in stride(from: roi.minX, to: roi.maxX, by: stepX) {
let yValue = Double(yBytes[y * yStride + x])
let uvOffset = (y / 2) * uvStride + (x / 2) * 2
let cb = Double(uvBytes[uvOffset])
let cr = Double(uvBytes[uvOffset + 1])
let rgb = convertYCbCrToRGB(y: yValue, cb: cb, cr: cr, isFullRange: isFullRange)
accumulator.add(red: rgb.red, green: rgb.green, blue: rgb.blue, luminance: yValue)
}
}
return try accumulator.result(timestamp: timestamp)
}
private func extractBGRA(
from pixelBuffer: CVPixelBuffer,
timestamp: Double
) throws -> PPGFrameColorMetrics {
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
throw PPGFrameMetricsExtractorError.missingBaseAddress
}
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let bytes = baseAddress.assumingMemoryBound(to: UInt8.self)
let roi = regionOfInterest(width: width, height: height)
let stepX = max(roi.width / gridSize, 1)
let stepY = max(roi.height / gridSize, 1)
var accumulator = ColorAccumulator()
for y in stride(from: roi.minY, to: roi.maxY, by: stepY) {
for x in stride(from: roi.minX, to: roi.maxX, by: stepX) {
let offset = y * bytesPerRow + x * 4
let blue = Double(bytes[offset])
let green = Double(bytes[offset + 1])
let red = Double(bytes[offset + 2])
let luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
accumulator.add(red: red, green: green, blue: blue, luminance: luminance)
}
}
return try accumulator.result(timestamp: timestamp)
}
private func regionOfInterest(width: Int, height: Int) -> (
minX: Int,
maxX: Int,
minY: Int,
maxY: Int,
width: Int,
height: Int
) {
let roiWidth = max(Int(Double(width) * roiFraction), 1)
let roiHeight = max(Int(Double(height) * roiFraction), 1)
let minX = max((width - roiWidth) / 2, 0)
let minY = max((height - roiHeight) / 2, 0)
return (minX, minX + roiWidth, minY, minY + roiHeight, roiWidth, roiHeight)
}
private func convertYCbCrToRGB(
y: Double,
cb: Double,
cr: Double,
isFullRange: Bool
) -> (red: Double, green: Double, blue: Double) {
let red: Double
let green: Double
let blue: Double
if isFullRange {
let shiftedCb = cb - 128
let shiftedCr = cr - 128
red = y + 1.402 * shiftedCr
green = y - 0.344_136 * shiftedCb - 0.714_136 * shiftedCr
blue = y + 1.772 * shiftedCb
} else {
let shiftedY = max(y - 16, 0)
let shiftedCb = cb - 128
let shiftedCr = cr - 128
red = 1.164_383 * shiftedY + 1.596_027 * shiftedCr
green = 1.164_383 * shiftedY - 0.391_762 * shiftedCb - 0.812_968 * shiftedCr
blue = 1.164_383 * shiftedY + 2.017_232 * shiftedCb
}
return (clampByte(red), clampByte(green), clampByte(blue))
}
private func clampByte(_ value: Double) -> Double {
min(max(value, 0), 255)
}
}
private struct ColorAccumulator {
private(set) var count = 0
private var redMean = 0.0
private var greenMean = 0.0
private var blueMean = 0.0
private var luminanceMean = 0.0
private var redM2 = 0.0
private var greenM2 = 0.0
private var saturatedCount = 0
mutating func add(red: Double, green: Double, blue: Double, luminance: Double) {
count += 1
let n = Double(count)
let redDelta = red - redMean
redMean += redDelta / n
redM2 += redDelta * (red - redMean)
let greenDelta = green - greenMean
greenMean += greenDelta / n
greenM2 += greenDelta * (green - greenMean)
blueMean += (blue - blueMean) / n
luminanceMean += (luminance - luminanceMean) / n
// A fingertip lit by the torch commonly clips only the red channel while
// green still contains a clean pulsatile waveform. Count true optical
// clipping only when both useful color channels (or luminance) are clipped.
if (red >= 250 && green >= 250) || luminance >= 250 {
saturatedCount += 1
}
}
func result(timestamp: Double) throws -> PPGFrameColorMetrics {
guard count > 0 else {
throw PPGFrameMetricsExtractorError.emptyRegionOfInterest
}
let divisor = Double(max(count - 1, 1))
return PPGFrameColorMetrics(
timestampSeconds: timestamp,
redMean: redMean,
greenMean: greenMean,
blueMean: blueMean,
redVariance: redM2 / divisor,
greenVariance: greenM2 / divisor,
luminanceMean: luminanceMean,
saturationRatio: Double(saturatedCount) / Double(count)
)
}
}
@@ -0,0 +1,303 @@
import CoreHaptics
import Foundation
import NitroModules
/// HAPTIKA.CARDIAC_MIRROR_BEAT v1.0.0
///
/// This pattern is deliberately isolated from generic notification haptics. A
/// short transient with a controlled sharpness gives the user a repeatable
/// “heartbeat” texture without pretending to reproduce a clinical waveform.
final class HybridHaptikaCardiacMirror: HybridHaptikaCardiacMirrorSpec {
private static let patternID = "HAPTIKA.CARDIAC_MIRROR_BEAT"
private static let patternVersion = "1.0.0"
// Kept at zero for Nitro ABI compatibility. Public Watch workout APIs expose
// cadence updates, not exact beat events, so Haptika advertises no latency
// SLA between a biological beat and the reconstructed iPhone pulse.
private static let latencyBudgetMs = 0.0
private let lock = NSLock()
private var engine: CHHapticEngine?
private var patternPlayer: CHHapticPatternPlayer?
private var regulationPlayer: CHHapticAdvancedPatternPlayer?
private var supported: Bool
override init() {
self.supported = CHHapticEngine.capabilitiesForHardware().supportsHaptics
super.init()
guard supported else { return }
configureEngine()
}
func isSupported() throws -> Bool {
return supported
}
func patternInfo() throws -> NativeCardiacMirrorPatternInfo {
return NativeCardiacMirrorPatternInfo(
id: Self.patternID,
version: Self.patternVersion,
latencyBudgetMs: Self.latencyBudgetMs
)
}
func start(targetBpm: Double) throws {
guard supported else { return }
lock.lock()
defer { lock.unlock() }
do {
try engine?.start()
if patternPlayer == nil {
patternPlayer = try makePatternPlayer()
}
} catch {
// Haptics are optional and must never block the visual experience.
patternPlayer = nil
}
}
func pulse() throws {
guard supported else { return }
lock.lock()
defer { lock.unlock() }
do {
try engine?.start()
if patternPlayer == nil {
patternPlayer = try makePatternPlayer()
}
try patternPlayer?.start(atTime: CHHapticTimeImmediate)
} catch {
patternPlayer = nil
}
}
func stop() throws {
lock.lock()
defer { lock.unlock() }
patternPlayer = nil
regulationPlayer = nil
engine?.stop(completionHandler: nil)
}
func playRegulationProtocol(kind: String) throws -> Double {
guard supported else { return 0 }
lock.lock()
defer { lock.unlock() }
do {
try engine?.start()
try regulationPlayer?.stop(atTime: CHHapticTimeImmediate)
let configuration = try makeRegulationPattern(kind: kind)
guard let engine else { return 0 }
let player = try engine.makeAdvancedPlayer(with: configuration.pattern)
player.loopEnabled = configuration.loops
regulationPlayer = player
try player.start(atTime: CHHapticTimeImmediate)
return configuration.durationSeconds
} catch {
regulationPlayer = nil
return 0
}
}
func stopRegulationProtocol() throws {
lock.lock()
defer { lock.unlock() }
try? regulationPlayer?.stop(atTime: CHHapticTimeImmediate)
regulationPlayer = nil
}
func startWatchSession() throws {
HaptikaWatchConnectivityService.shared.activate()
}
func watchReachable() throws -> Bool {
return HaptikaWatchConnectivityService.shared.state().reachable
}
func latestHeartbeat() throws -> NativeCardiacMirrorHeartbeat? {
guard let sample = HaptikaWatchConnectivityService.shared.latestHeartRate() else {
return nil
}
return NativeCardiacMirrorHeartbeat(
timestampSeconds: sample.timestampSeconds,
bpm: sample.bpm
)
}
private func configureEngine() {
do {
let hapticEngine = try CHHapticEngine()
hapticEngine.resetHandler = { [weak self] in
self?.lock.lock()
defer { self?.lock.unlock() }
self?.patternPlayer = nil
}
hapticEngine.stoppedHandler = { [weak self] _ in
self?.lock.lock()
defer { self?.lock.unlock() }
self?.patternPlayer = nil
}
engine = hapticEngine
} catch {
supported = false
}
}
private func makePatternPlayer() throws -> CHHapticPatternPlayer {
guard let engine else {
throw NSError(domain: "HaptikaCardiacMirror", code: 1)
}
let intensity = CHHapticEventParameter(
parameterID: .hapticIntensity,
value: 0.78
)
let sharpness = CHHapticEventParameter(
parameterID: .hapticSharpness,
value: 0.32
)
let event = CHHapticEvent(
eventType: .hapticTransient,
parameters: [intensity, sharpness],
relativeTime: 0
)
let pattern = try CHHapticPattern(events: [event], parameters: [])
return try engine.makePlayer(with: pattern)
}
private func makeRegulationPattern(
kind: String
) throws -> (pattern: CHHapticPattern, durationSeconds: Double, loops: Bool) {
switch kind {
case "coherence55":
return (try makeBreathingPattern(inhale: 5, exhale: 5), 10, true)
case "relax46":
return (try makeBreathingPattern(inhale: 4, exhale: 6), 10, true)
case "box4444":
return (try makePhasedBreathingPattern(inhale: 4, hold: 4, exhale: 4, emptyHold: 4), 16, true)
case "breathing478":
return (try makePhasedBreathingPattern(inhale: 4, hold: 7, exhale: 8, emptyHold: 0), 19, true)
case "sosStress":
return (try makeSettlingPattern(intensity: 0.25, duration: 60), 62.2, false)
case "microAnchor":
return (try makeSettlingPattern(intensity: 0.15, duration: 35), 37, false)
default:
throw NSError(domain: "HaptikaRegulation", code: 2)
}
}
private func makeBreathingPattern(
inhale: TimeInterval,
exhale: TimeInterval
) throws -> CHHapticPattern {
let inhaleEvent = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.55),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.08)
],
relativeTime: 0,
duration: inhale
)
let transition = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.65)
],
relativeTime: inhale
)
let exhaleEvent = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.24),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.04)
],
relativeTime: inhale + 0.08,
duration: exhale - 0.08
)
return try CHHapticPattern(
events: [inhaleEvent, transition, exhaleEvent],
parameters: []
)
}
private func makePhasedBreathingPattern(
inhale: TimeInterval,
hold: TimeInterval,
exhale: TimeInterval,
emptyHold: TimeInterval
) throws -> CHHapticPattern {
let inhaleEvent = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.48),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.08)
],
relativeTime: 0,
duration: inhale
)
let fullHoldMarker = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.42),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.52)
],
relativeTime: inhale
)
let exhaleEvent = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.20),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.03)
],
relativeTime: inhale + hold,
duration: exhale
)
var events = [inhaleEvent, fullHoldMarker, exhaleEvent]
if emptyHold > 0 {
events.append(
CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.25)
],
relativeTime: inhale + hold + exhale + emptyHold - 0.02
)
)
}
return try CHHapticPattern(events: events, parameters: [])
}
private func makeSettlingPattern(
intensity: Float,
duration: TimeInterval
) throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
for index in 0..<3 {
events.append(
CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.62),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.42)
],
relativeTime: TimeInterval(index) * 0.6
)
)
}
events.append(
CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.04)
],
relativeTime: 2,
duration: duration
)
)
return try CHHapticPattern(events: events, parameters: [])
}
}
@@ -0,0 +1,451 @@
import Foundation
import HealthKit
import NitroModules
/// Read-only HealthKit access used as dated physiological context.
///
/// This module deliberately uses sample queries rather than observer/live queries. It never
/// creates heartbeat series and never treats historical HealthKit values as a current reading.
final class HybridHaptikaHealthKit: HybridHaptikaHealthKitSpec {
private static let defaultLookbackDays = 30.0
private static let minimumLookbackDays = 1.0
private static let maximumLookbackDays = 365.0
private static let maximumRecentHeartRateSamples = 100
private let healthStore: HKHealthStore
override init() {
self.healthStore = HKHealthStore()
super.init()
}
func isAvailable() throws -> Bool {
return HKHealthStore.isHealthDataAvailable()
}
func requestReadAuthorization() throws -> Promise<Bool> {
guard HKHealthStore.isHealthDataAvailable() else {
return Promise.resolved(withResult: false)
}
let healthStore = self.healthStore
let readTypes = Self.readTypes()
return Promise<Bool>.async {
do {
try await healthStore.requestAuthorization(
toShare: Set<HKSampleType>(),
read: readTypes
)
// HealthKit intentionally does not reveal whether read access was granted. A successful
// authorization request only means that the sheet completed without an operational error.
return true
} catch {
// HealthKit is optional; cancellation, a missing entitlement, or a locked/unavailable
// store must not block the camera measurement flow.
return false
}
}
}
func fetchContext(lookbackDays: Double) throws -> Promise<NativeHealthKitContext> {
let fetchedAt = Date()
guard HKHealthStore.isHealthDataAvailable() else {
return Promise.resolved(
withResult: Self.emptyContext(
available: false,
authorizationRequestSettled: false,
fetchedAt: fetchedAt
)
)
}
let healthStore = self.healthStore
let boundedLookbackDays = Self.boundedLookbackDays(lookbackDays)
return Promise<NativeHealthKitContext>.async {
let authorizationRequestSettled = await Self.authorizationRequestIsSettled(
healthStore: healthStore
)
// Do not query before the optional permission flow has been presented. When access has been
// denied, HealthKit protects the decision by returning no samples, just as it does when the
// store genuinely has no samples.
guard authorizationRequestSettled else {
return Self.emptyContext(
available: true,
authorizationRequestSettled: false,
fetchedAt: fetchedAt
)
}
let startDate = fetchedAt.addingTimeInterval(-boundedLookbackDays * 86_400.0)
async let restingSamples = Self.quantitySamplesSafely(
healthStore: healthStore,
type: Self.quantityType(.restingHeartRate),
startDate: startDate,
endDate: fetchedAt,
limit: 1
)
async let hrvSamples = Self.quantitySamplesSafely(
healthStore: healthStore,
type: Self.quantityType(.heartRateVariabilitySDNN),
startDate: startDate,
endDate: fetchedAt,
limit: 1
)
async let heartRateSamples = Self.quantitySamplesSafely(
healthStore: healthStore,
type: Self.quantityType(.heartRate),
startDate: startDate,
endDate: fetchedAt,
limit: Self.maximumRecentHeartRateSamples
)
let (resting, hrv, heartRates) = await (
restingSamples,
hrvSamples,
heartRateSamples
)
let latestResting = resting.first
let latestHrv = hrv.first
let nativeResting = latestResting.flatMap {
Self.makeNativeQuantitySample(
$0,
metricType: .restingheartrate,
unit: Self.beatsPerMinuteUnit,
unitLabel: "count/min"
)
}
let nativeHrv = latestHrv.flatMap {
Self.makeNativeQuantitySample(
$0,
metricType: .hrvsdnn,
unit: Self.millisecondUnit,
unitLabel: "ms"
)
}
let nativeHeartRates = heartRates.compactMap {
Self.makeNativeQuantitySample(
$0,
metricType: .heartrate,
unit: Self.beatsPerMinuteUnit,
unitLabel: "count/min"
)
}
return NativeHealthKitContext(
available: true,
// This means the authorization request no longer needs presenting. Apple deliberately
// makes an explicit read denial indistinguishable from an authorized store with no data.
authorized: authorizationRequestSettled,
latestRestingHeartRate: nativeResting,
latestHrvSdnn: nativeHrv,
recentHeartRateSamples: nativeHeartRates,
contextConfidence: Self.contextConfidence(
latestResting: latestResting,
latestHrv: latestHrv,
heartRates: heartRates,
fetchedAt: fetchedAt,
lookbackDays: boundedLookbackDays
),
fetchedAt: Self.iso8601String(from: fetchedAt)
)
}
}
func fetchLatestHeartbeatSeries() throws -> Promise<NativeHeartbeatSeriesSummary?> {
guard HKHealthStore.isHealthDataAvailable() else {
return Promise.resolved(withResult: nil)
}
let healthStore = self.healthStore
return Promise<NativeHeartbeatSeriesSummary?>.async {
guard await Self.authorizationRequestIsSettled(healthStore: healthStore) else {
return nil
}
do {
guard let sample = try await Self.latestHeartbeatSeriesSample(
healthStore: healthStore
) else {
return nil
}
var heartbeatTimes: [Double] = []
var precededByGapIndexes: [Double] = []
let descriptor = HKHeartbeatSeriesQueryDescriptor(sample)
for try await heartbeat in descriptor.results(for: healthStore) {
let time = heartbeat.timeIntervalSinceStart
guard time.isFinite, time >= 0 else {
continue
}
heartbeatTimes.append(time)
if heartbeat.precededByGap {
precededByGapIndexes.append(Double(heartbeatTimes.count - 1))
}
}
guard !heartbeatTimes.isEmpty else {
return nil
}
let source = sample.sourceRevision.source
return NativeHeartbeatSeriesSummary(
startDate: Self.iso8601String(from: sample.startDate),
endDate: Self.iso8601String(from: sample.endDate),
sourceName: Self.nonEmpty(source.name),
sourceBundleIdentifier: Self.nonEmpty(source.bundleIdentifier),
heartbeatTimesSinceSeriesStartSeconds: heartbeatTimes,
precededByGapIndexes: precededByGapIndexes
)
} catch {
// A read denial, no accessible series, protected-data lock, or query failure all degrade to
// an absent optional context. None of them should affect the camera PPG measurement.
return nil
}
}
}
private static var beatsPerMinuteUnit: HKUnit {
return HKUnit.count().unitDivided(by: .minute())
}
private static var millisecondUnit: HKUnit {
return HKUnit.secondUnit(with: .milli)
}
private static func quantityType(
_ identifier: HKQuantityTypeIdentifier
) -> HKQuantityType? {
return HKObjectType.quantityType(forIdentifier: identifier)
}
private static func readTypes() -> Set<HKObjectType> {
var types = Set<HKObjectType>()
if let heartRate = quantityType(.heartRate) {
types.insert(heartRate)
}
if let restingHeartRate = quantityType(.restingHeartRate) {
types.insert(restingHeartRate)
}
if let hrvSdnn = quantityType(.heartRateVariabilitySDNN) {
types.insert(hrvSdnn)
}
// Read-only access to series already present in HealthKit. No builder or write type is used.
types.insert(HKSeriesType.heartbeat())
return types
}
private static func authorizationRequestIsSettled(
healthStore: HKHealthStore
) async -> Bool {
do {
let status = try await healthStore.statusForAuthorizationRequest(
toShare: Set<HKSampleType>(),
read: readTypes()
)
return status == .unnecessary
} catch {
return false
}
}
private static func quantitySamplesSafely(
healthStore: HKHealthStore,
type: HKQuantityType?,
startDate: Date,
endDate: Date,
limit: Int
) async -> [HKQuantitySample] {
guard let type else {
return []
}
do {
return try await quantitySamples(
healthStore: healthStore,
type: type,
startDate: startDate,
endDate: endDate,
limit: limit
)
} catch {
return []
}
}
private static func quantitySamples(
healthStore: HKHealthStore,
type: HKQuantityType,
startDate: Date,
endDate: Date,
limit: Int
) async throws -> [HKQuantitySample] {
let predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: [.strictEndDate]
)
let sort = NSSortDescriptor(
key: HKSampleSortIdentifierEndDate,
ascending: false
)
return try await withCheckedThrowingContinuation { continuation in
let query = HKSampleQuery(
sampleType: type,
predicate: predicate,
limit: limit,
sortDescriptors: [sort]
) { _, samples, error in
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(
returning: samples?.compactMap { $0 as? HKQuantitySample } ?? []
)
}
healthStore.execute(query)
}
}
private static func latestHeartbeatSeriesSample(
healthStore: HKHealthStore
) async throws -> HKHeartbeatSeriesSample? {
let sort = NSSortDescriptor(
key: HKSampleSortIdentifierEndDate,
ascending: false
)
return try await withCheckedThrowingContinuation { continuation in
let query = HKSampleQuery(
sampleType: HKSeriesType.heartbeat(),
predicate: nil,
limit: 1,
sortDescriptors: [sort]
) { _, samples, error in
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(
returning: samples?.first as? HKHeartbeatSeriesSample
)
}
healthStore.execute(query)
}
}
private static func makeNativeQuantitySample(
_ sample: HKQuantitySample,
metricType: HealthMetricType,
unit: HKUnit,
unitLabel: String
) -> NativeHealthMetricSample? {
let value = sample.quantity.doubleValue(for: unit)
guard value.isFinite else {
return nil
}
let source = sample.sourceRevision.source
let deviceName = nonEmpty(sample.device?.name) ?? nonEmpty(sample.device?.model)
return NativeHealthMetricSample(
type: metricType,
value: value,
unit: unitLabel,
startDate: iso8601String(from: sample.startDate),
endDate: iso8601String(from: sample.endDate),
sourceName: nonEmpty(source.name),
sourceBundleIdentifier: nonEmpty(source.bundleIdentifier),
deviceName: deviceName
)
}
/// A conservative availability-and-recency score for optional context only. It is not a signal
/// quality score and must not be interpreted as confidence in a stress or medical conclusion.
private static func contextConfidence(
latestResting: HKQuantitySample?,
latestHrv: HKQuantitySample?,
heartRates: [HKQuantitySample],
fetchedAt: Date,
lookbackDays: Double
) -> Double {
let horizon = max(lookbackDays * 86_400.0, 1.0)
func freshness(of sample: HKSample) -> Double {
let age = max(fetchedAt.timeIntervalSince(sample.endDate), 0.0)
return min(max(1.0 - age / horizon, 0.0), 1.0)
}
var score = 0.0
if let latestResting {
score += 0.35 * freshness(of: latestResting)
}
if let latestHrv {
score += 0.35 * freshness(of: latestHrv)
}
if let latestHeartRate = heartRates.first {
let sampleCoverage = min(Double(heartRates.count) / 5.0, 1.0)
score += 0.20 * freshness(of: latestHeartRate)
score += 0.10 * sampleCoverage
}
return min(max(score, 0.0), 1.0)
}
private static func emptyContext(
available: Bool,
authorizationRequestSettled: Bool,
fetchedAt: Date
) -> NativeHealthKitContext {
return NativeHealthKitContext(
available: available,
authorized: authorizationRequestSettled,
latestRestingHeartRate: nil,
latestHrvSdnn: nil,
recentHeartRateSamples: [],
contextConfidence: 0,
fetchedAt: iso8601String(from: fetchedAt)
)
}
private static func boundedLookbackDays(_ value: Double) -> Double {
guard value.isFinite else {
return defaultLookbackDays
}
return min(max(value, minimumLookbackDays), maximumLookbackDays)
}
private static func iso8601String(from date: Date) -> String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter.string(from: date)
}
private static func nonEmpty(_ value: String?) -> String? {
guard let value else {
return nil
}
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
@@ -0,0 +1,18 @@
import Foundation
struct PPGThermalMonitor {
func currentState() -> ThermalState {
switch ProcessInfo.processInfo.thermalState {
case .nominal:
return .nominal
case .fair:
return .fair
case .serious:
return .serious
case .critical:
return .critical
@unknown default:
return .unknown
}
}
}
@@ -0,0 +1,17 @@
import Foundation
/// Pure, testable safety gate used before and during camera acquisition.
/// Serious and critical states stop the measurement; unknown is conservative
/// only in the sense that no torch escalation is attempted elsewhere.
public enum PPGThermalSafetyPolicy {
public static func shouldStopMeasurement(forStateName stateName: String) -> Bool {
switch stateName.lowercased() {
case "serious", "critical":
return true
case "nominal", "fair", "unknown":
return false
default:
return false
}
}
}
@@ -0,0 +1,196 @@
import Foundation
public struct FrequencyBPMEstimate: Codable, Equatable, Sendable {
public var bpm: Double?
public var autocorrelation: Double
public var confidence: Double
public init(bpm: Double?, autocorrelation: Double, confidence: Double) {
self.bpm = bpm
self.autocorrelation = autocorrelation.isFinite ? autocorrelation : 0
self.confidence = SignalStatistics.clamp01(confidence)
}
}
public enum PPGBPMEstimator {
public static func estimate(
signal: [Double],
timestampsSeconds: [Double],
validityMask: [Bool],
peakDetection: PeakDetectionResult,
selectedChannel: PPGSignalChannel,
configuration: PPGProcessingConfiguration
) throws -> BPMEstimate {
let configuration = try configuration.validated()
guard signal.count == timestampsSeconds.count,
signal.count == validityMask.count,
signal.count >= 5
else {
throw PPGSignalError.insufficientSamples(required: 5, actual: signal.count)
}
let acceptedIntervals = peakDetection.acceptedIntervals.map(\.durationMilliseconds)
let temporalBPM: Double? = acceptedIntervals.count >= 4
? 60_000 / SignalStatistics.median(acceptedIntervals)
: nil
let frequency = frequencyEstimate(
signal: signal,
timestampsSeconds: timestampsSeconds,
validityMask: validityMask,
minimumFrequencyHz: configuration.minimumFrequencyHz,
maximumFrequencyHz: configuration.maximumFrequencyHz
)
// Autocorrelation can lock onto the second harmonic (2×) or a sub-harmonic
// (0.5×). Prefer the equivalent candidate closest to the clean beat-to-beat
// estimate before judging the two methods as inconsistent.
let adjustedFrequencyBPM = temporalBPM.flatMap { temporal -> Double? in
guard let raw = frequency.bpm else { return nil }
return [raw, raw / 2, raw * 2]
.filter {
$0 >= configuration.minimumFrequencyHz * 60
&& $0 <= configuration.maximumFrequencyHz * 60
}
.min(by: { abs($0 - temporal) < abs($1 - temporal) })
} ?? frequency.bpm
let agreement = temporalBPM.flatMap { temporal in
adjustedFrequencyBPM.map { abs(temporal - $0) }
}
let agreementScore = agreement.map {
SignalStatistics.clamp01(1 - $0 / max(configuration.maximumMethodDisagreementBPM, 1e-9))
} ?? 0
let finalBPM: Double?
if let temporalBPM,
let frequencyBPM = adjustedFrequencyBPM,
let agreement,
agreement <= configuration.maximumMethodDisagreementBPM {
finalBPM = 0.65 * temporalBPM + 0.35 * frequencyBPM
} else if let temporalBPM,
acceptedIntervals.count >= 8,
peakDetection.confidence >= 0.55 {
// A long, regular sequence of detected beats is sufficient for mean HR.
// Keep PRV subject to the stricter downstream interval-quality checks.
finalBPM = temporalBPM
} else if let frequencyBPM = adjustedFrequencyBPM,
frequency.confidence >= 0.55 {
finalBPM = frequencyBPM
} else {
finalBPM = nil
}
let countScore = min(Double(acceptedIntervals.count) / 10, 1)
let confidence = 0.40 * peakDetection.confidence
+ 0.25 * frequency.confidence
+ 0.25 * agreementScore
+ 0.10 * countScore
return BPMEstimate(
bpm: finalBPM,
temporalBpm: temporalBPM,
frequencyBpm: adjustedFrequencyBPM,
methodAgreementBpm: agreement,
validPeakCount: peakDetection.peaks.count,
rejectedPeakCount: peakDetection.rejectedIntervalCount,
confidence: confidence,
selectedChannel: selectedChannel,
selectedPolarity: peakDetection.selectedPolarity
)
}
public static func frequencyEstimate(
signal: [Double],
timestampsSeconds: [Double],
validityMask: [Bool],
minimumFrequencyHz: Double,
maximumFrequencyHz: Double
) -> FrequencyBPMEstimate {
guard signal.count == timestampsSeconds.count,
signal.count == validityMask.count,
signal.count >= 12,
minimumFrequencyHz > 0,
maximumFrequencyHz > minimumFrequencyHz
else {
return FrequencyBPMEstimate(bpm: nil, autocorrelation: 0, confidence: 0)
}
let intervals = zip(timestampsSeconds, timestampsSeconds.dropFirst())
.map { $1 - $0 }
.filter { $0.isFinite && $0 > 0 }
let sampleInterval = SignalStatistics.median(intervals)
guard sampleInterval > 0 else {
return FrequencyBPMEstimate(bpm: nil, autocorrelation: 0, confidence: 0)
}
let sampleRate = 1 / sampleInterval
let minimumLag = max(Int((sampleRate / maximumFrequencyHz).rounded(.down)), 2)
let maximumLag = min(Int((sampleRate / minimumFrequencyHz).rounded(.up)), signal.count / 2)
guard maximumLag > minimumLag else {
return FrequencyBPMEstimate(bpm: nil, autocorrelation: 0, confidence: 0)
}
let validValues = signal.indices.compactMap {
validityMask[$0] && signal[$0].isFinite ? signal[$0] : nil
}
let center = SignalStatistics.mean(validValues)
var correlations: [Int: Double] = [:]
for lag in minimumLag...maximumLag {
var cross = 0.0
var leftPower = 0.0
var rightPower = 0.0
var pairCount = 0
for index in 0..<(signal.count - lag) {
guard validityMask[index], validityMask[index + lag] else { continue }
let left = signal[index] - center
let right = signal[index + lag] - center
guard left.isFinite, right.isFinite else { continue }
cross += left * right
leftPower += left * left
rightPower += right * right
pairCount += 1
}
guard pairCount >= 10, leftPower > 1e-12, rightPower > 1e-12 else { continue }
correlations[lag] = cross / (leftPower * rightPower).squareRoot()
}
guard !correlations.isEmpty,
let strongest = correlations.max(by: { $0.value < $1.value }),
strongest.value > 0.10
else {
return FrequencyBPMEstimate(bpm: nil, autocorrelation: 0, confidence: 0)
}
let localPeaks = correlations.keys.sorted().filter { lag in
let value = correlations[lag] ?? -.infinity
let left = correlations[lag - 1] ?? -.infinity
let right = correlations[lag + 1] ?? -.infinity
return value >= left && value >= right
}
let strongThreshold = max(strongest.value * 0.92, 0.10)
let chosenLag = localPeaks.first(where: { (correlations[$0] ?? 0) >= strongThreshold })
?? strongest.key
let centerCorrelation = correlations[chosenLag] ?? strongest.value
let leftCorrelation = correlations[chosenLag - 1] ?? centerCorrelation
let rightCorrelation = correlations[chosenLag + 1] ?? centerCorrelation
let denominator = leftCorrelation - 2 * centerCorrelation + rightCorrelation
let fractionalOffset: Double
if abs(denominator) > 1e-12 {
fractionalOffset = min(
max(0.5 * (leftCorrelation - rightCorrelation) / denominator, -0.5),
0.5
)
} else {
fractionalOffset = 0
}
let refinedLag = Double(chosenLag) + fractionalOffset
guard refinedLag > 0 else {
return FrequencyBPMEstimate(bpm: nil, autocorrelation: centerCorrelation, confidence: 0)
}
let bpm = 60 * sampleRate / refinedLag
let confidence = SignalStatistics.clamp01((centerCorrelation - 0.10) / 0.75)
return FrequencyBPMEstimate(
bpm: bpm.isFinite ? bpm : nil,
autocorrelation: centerCorrelation,
confidence: confidence
)
}
}
@@ -0,0 +1,172 @@
import Foundation
public enum PPGSignalPreprocessor {
/// Robustly clips isolated outliers, subtracts a centered local baseline, and scales by MAD.
public static func detrendAndNormalize(
_ input: [Double],
sampleRateHz: Double,
baselineWindowSeconds: Double = 1.6
) -> [Double] {
guard !input.isEmpty, sampleRateHz.isFinite, sampleRateHz > 0 else { return [] }
let finite = sanitize(input)
let center = SignalStatistics.median(finite)
let rawMAD = SignalStatistics.mad(finite, around: center)
let fallbackScale = max(SignalStatistics.standardDeviation(finite), 1e-9)
let clipScale = max(1.4826 * rawMAD, fallbackScale * 0.25, 1e-9)
let clipped = finite.map { min(max($0, center - 8 * clipScale), center + 8 * clipScale) }
var window = max(Int((baselineWindowSeconds * sampleRateHz).rounded()), 3)
if window.isMultiple(of: 2) { window += 1 }
let baseline = centeredMovingAverage(clipped, window: window)
let detrended = zip(clipped, baseline).map { $0.0 - $0.1 }
let detrendedCenter = SignalStatistics.median(detrended)
let detrendedMAD = SignalStatistics.mad(detrended, around: detrendedCenter)
let detrendedSTD = SignalStatistics.standardDeviation(detrended)
let scale = max(1.4826 * detrendedMAD, detrendedSTD * 0.20, 1e-9)
return detrended.map { value in
min(max((value - detrendedCenter) / scale, -12), 12)
}
}
public static func centeredMovingAverage(_ input: [Double], window: Int) -> [Double] {
guard !input.isEmpty else { return [] }
let normalizedWindow = max(window, 1)
let half = normalizedWindow / 2
return input.indices.map { index in
var sum = 0.0
for offset in -half...half {
sum += input[SignalStatistics.reflectedIndex(index + offset, count: input.count)]
}
return sum / Double(2 * half + 1)
}
}
public static func dilateInvalidRegions(_ mask: [Bool], radius: Int) -> [Bool] {
guard radius > 0, !mask.isEmpty else { return mask }
var difference = Array(repeating: 0, count: mask.count + 1)
for index in mask.indices where !mask[index] {
let lower = max(0, index - radius)
let upper = min(mask.count - 1, index + radius)
difference[lower] += 1
difference[upper + 1] -= 1
}
var active = 0
return mask.indices.map { index in
active += difference[index]
return active == 0
}
}
private static func sanitize(_ input: [Double]) -> [Double] {
var lastFinite = input.first(where: \.isFinite) ?? 0
return input.map { value in
if value.isFinite { lastFinite = value }
return lastFinite
}
}
}
/// Symmetric, windowed-sinc FIR band-pass filter. Coefficients are regenerated from the actual
/// resampling frequency, so a 30 Hz and a 60 Hz capture use the same physiological pass band.
public struct PPGBandPassFilter: Equatable, Sendable {
public let sampleRateHz: Double
public let lowCutoffHz: Double
public let highCutoffHz: Double
public let coefficients: [Double]
public init(
sampleRateHz: Double,
lowCutoffHz: Double,
highCutoffHz: Double,
tapCount requestedTapCount: Int? = nil
) throws {
guard sampleRateHz.isFinite,
lowCutoffHz.isFinite,
highCutoffHz.isFinite,
sampleRateHz >= 10,
lowCutoffHz > 0,
highCutoffHz > lowCutoffHz,
highCutoffHz < sampleRateHz / 2
else {
throw PPGSignalError.invalidConfiguration("band-pass frequencies must lie inside Nyquist")
}
var tapCount = requestedTapCount
?? min(max(Int((sampleRateHz * 4).rounded()), 31), 301)
tapCount = max(tapCount, 5)
if tapCount.isMultiple(of: 2) { tapCount += 1 }
self.sampleRateHz = sampleRateHz
self.lowCutoffHz = lowCutoffHz
self.highCutoffHz = highCutoffHz
self.coefficients = Self.makeCoefficients(
sampleRateHz: sampleRateHz,
lowCutoffHz: lowCutoffHz,
highCutoffHz: highCutoffHz,
tapCount: tapCount
)
}
public var invalidRegionGuardRadius: Int { coefficients.count / 2 }
/// Applies centered convolution. The symmetric impulse response introduces no peak-time shift.
public func apply(to input: [Double]) -> [Double] {
guard !input.isEmpty else { return [] }
let clean = input.map { $0.isFinite ? $0 : 0 }
let half = coefficients.count / 2
var output = Array(repeating: 0.0, count: clean.count)
for outputIndex in clean.indices {
var accumulator = 0.0
for coefficientIndex in coefficients.indices {
let sourceIndex = SignalStatistics.reflectedIndex(
outputIndex + coefficientIndex - half,
count: clean.count
)
accumulator += coefficients[coefficientIndex] * clean[sourceIndex]
}
output[outputIndex] = accumulator.isFinite ? accumulator : 0
}
return output
}
private static func makeCoefficients(
sampleRateHz: Double,
lowCutoffHz: Double,
highCutoffHz: Double,
tapCount: Int
) -> [Double] {
let midpoint = tapCount / 2
let low = lowCutoffHz / sampleRateHz
let high = highCutoffHz / sampleRateHz
var taps = (0..<tapCount).map { index -> Double in
let offset = Double(index - midpoint)
let idealHigh = 2 * high * sinc(2 * high * offset)
let idealLow = 2 * low * sinc(2 * low * offset)
let hamming = 0.54 - 0.46 * cos(2 * Double.pi * Double(index) / Double(tapCount - 1))
return (idealHigh - idealLow) * hamming
}
let centerFrequency = (lowCutoffHz * highCutoffHz).squareRoot()
var real = 0.0
var imaginary = 0.0
for index in taps.indices {
let offset = Double(index - midpoint)
let angle = -2 * Double.pi * centerFrequency * offset / sampleRateHz
real += taps[index] * cos(angle)
imaginary += taps[index] * sin(angle)
}
let gain = (real * real + imaginary * imaginary).squareRoot()
if gain > 1e-12 {
taps = taps.map { $0 / gain }
}
return taps
}
private static func sinc(_ value: Double) -> Double {
guard abs(value) > 1e-12 else { return 1 }
return sin(Double.pi * value) / (Double.pi * value)
}
}
@@ -0,0 +1,523 @@
import Foundation
public enum PPGSignalError: Error, Equatable, Sendable {
case invalidConfiguration(String)
case insufficientSamples(required: Int, actual: Int)
case insufficientDuration(requiredSeconds: Double, actualSeconds: Double)
case invalidTimestamps
case noUsableChannel
}
/// Compact numeric output expected from the camera frame extractor.
/// No image data is retained by the signal-processing core.
public struct PPGFrameSample: Codable, Equatable, Sendable {
public var timestampSeconds: Double
public var red: Double
public var green: Double
public var blue: Double
public var luminance: Double
public var contactConfidence: Double
public var saturationRatio: Double
public var motionArtifactScore: Double
public init(
timestampSeconds: Double,
red: Double,
green: Double,
blue: Double? = nil,
luminance: Double,
contactConfidence: Double = 1,
saturationRatio: Double = 0,
motionArtifactScore: Double = 0
) {
self.timestampSeconds = timestampSeconds
self.red = red
self.green = green
let reconstructedBlue = (luminance - 0.2126 * red - 0.7152 * green) / 0.0722
self.blue = blue ?? min(max(reconstructedBlue.isFinite ? reconstructedBlue : luminance, 0), 255)
self.luminance = luminance
self.contactConfidence = Self.clamp01(contactConfidence)
self.saturationRatio = Self.clamp01(saturationRatio)
self.motionArtifactScore = Self.clamp01(motionArtifactScore)
}
public var hasFiniteSignalValues: Bool {
timestampSeconds.isFinite && red.isFinite && green.isFinite
&& blue.isFinite && luminance.isFinite
}
private static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return min(max(value, 0), 1)
}
}
public enum PPGSignalChannel: String, CaseIterable, Codable, Equatable, Sendable {
case red
case green
case blue
case luminance
case greenRedRatio
case blueRedRatio
case normalizedBlend
}
/// Publication gates for the three product decisions made from one camera capture.
///
/// These thresholds deliberately do not mean the same thing:
/// - mean BPM can be published from a technically usable periodic signal;
/// - beat-to-beat PRV needs substantially better timing quality;
/// - the potential-activation index is gated again by the product layer and a
/// personalized baseline. It must never be presented as a medical stress measure.
///
/// Keeping the values named here prevents a permissive live-preview threshold from
/// accidentally becoming the PRV or product-index threshold.
public enum PPGPublicationThresholds {
/// Minimum final technical score for a definitive mean BPM.
public static let bpmQualityScore = 0.45
/// Minimum temporal/frequency estimator confidence for a definitive mean BPM.
public static let bpmConfidence = 0.40
/// Minimum final technical score for beat-to-beat PRV to be marked reliable.
public static let prvQualityScore = 0.80
/// Minimum estimator confidence for beat-to-beat PRV to be marked reliable.
public static let prvBpmConfidence = 0.70
/// Product-layer signal gate used before calculating the potential index.
public static let potentialIndexQualityScore = 0.65
/// Product-layer minimum number of accepted pulse intervals.
public static let potentialIndexMinimumIntervals = 20
}
public struct PPGProcessingConfiguration: Codable, Equatable, Sendable {
public var targetSampleRateHz: Double
public var minimumFrequencyHz: Double
public var maximumFrequencyHz: Double
public var minimumAnalysisDurationSeconds: Double
public var maximumInterpolationGapSeconds: Double
public var longGapLimitSeconds: Double
public var minimumContactConfidence: Double
public var maximumSaturationRatio: Double
public var maximumMotionArtifactScore: Double
public var minimumPeakProminence: Double
public var minimumPeakAmplitude: Double
public var refractoryPeriodSeconds: Double
public var maximumMethodDisagreementBPM: Double
public var minimumQualityScore: Double
public var minimumIntervalsForPRV: Int
public var maximumRejectedIntervalRatioForPRV: Double
public var filterTapCount: Int?
public init(
targetSampleRateHz: Double = 30,
minimumFrequencyHz: Double = 0.7,
maximumFrequencyHz: Double = 3.5,
minimumAnalysisDurationSeconds: Double = 45,
maximumInterpolationGapSeconds: Double = 0.20,
longGapLimitSeconds: Double = 0.80,
minimumContactConfidence: Double = 0.55,
maximumSaturationRatio: Double = 0.70,
maximumMotionArtifactScore: Double = 0.65,
minimumPeakProminence: Double = 0.35,
minimumPeakAmplitude: Double = 0.10,
refractoryPeriodSeconds: Double = 0.25,
maximumMethodDisagreementBPM: Double = 8,
minimumQualityScore: Double = PPGPublicationThresholds.potentialIndexQualityScore,
minimumIntervalsForPRV: Int = 20,
maximumRejectedIntervalRatioForPRV: Double = 0.20,
filterTapCount: Int? = nil
) {
self.targetSampleRateHz = targetSampleRateHz
self.minimumFrequencyHz = minimumFrequencyHz
self.maximumFrequencyHz = maximumFrequencyHz
self.minimumAnalysisDurationSeconds = minimumAnalysisDurationSeconds
self.maximumInterpolationGapSeconds = maximumInterpolationGapSeconds
self.longGapLimitSeconds = longGapLimitSeconds
self.minimumContactConfidence = Self.clamp01(minimumContactConfidence)
self.maximumSaturationRatio = Self.clamp01(maximumSaturationRatio)
self.maximumMotionArtifactScore = Self.clamp01(maximumMotionArtifactScore)
self.minimumPeakProminence = max(minimumPeakProminence, 0)
self.minimumPeakAmplitude = max(minimumPeakAmplitude, 0)
self.refractoryPeriodSeconds = max(refractoryPeriodSeconds, 0)
self.maximumMethodDisagreementBPM = max(maximumMethodDisagreementBPM, 0)
self.minimumQualityScore = Self.clamp01(minimumQualityScore)
self.minimumIntervalsForPRV = max(minimumIntervalsForPRV, 2)
self.maximumRejectedIntervalRatioForPRV = Self.clamp01(maximumRejectedIntervalRatioForPRV)
self.filterTapCount = filterTapCount
}
public static let production = PPGProcessingConfiguration()
public func validated() throws -> Self {
guard targetSampleRateHz.isFinite, targetSampleRateHz >= 10 else {
throw PPGSignalError.invalidConfiguration("targetSampleRateHz must be at least 10 Hz")
}
guard minimumFrequencyHz.isFinite,
maximumFrequencyHz.isFinite,
minimumFrequencyHz > 0,
maximumFrequencyHz > minimumFrequencyHz,
maximumFrequencyHz < targetSampleRateHz / 2
else {
throw PPGSignalError.invalidConfiguration("frequency band must be inside (0, Nyquist)")
}
guard minimumAnalysisDurationSeconds.isFinite, minimumAnalysisDurationSeconds > 0 else {
throw PPGSignalError.invalidConfiguration("minimumAnalysisDurationSeconds must be positive")
}
guard maximumInterpolationGapSeconds.isFinite,
maximumInterpolationGapSeconds > 0,
longGapLimitSeconds.isFinite,
longGapLimitSeconds >= maximumInterpolationGapSeconds
else {
throw PPGSignalError.invalidConfiguration("gap limits are inconsistent")
}
if let filterTapCount, filterTapCount < 5 {
throw PPGSignalError.invalidConfiguration("filterTapCount must be at least 5")
}
return self
}
private static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return min(max(value, 0), 1)
}
}
public struct TimestampedValue: Equatable, Sendable {
public var timestampSeconds: Double
public var value: Double
public init(timestampSeconds: Double, value: Double) {
self.timestampSeconds = timestampSeconds
self.value = value
}
}
public struct TimebaseDiagnostics: Codable, Equatable, Sendable {
public var inputSampleCount: Int
public var usableInputSampleCount: Int
public var discardedSampleCount: Int
public var duplicateTimestampCount: Int
public var outOfOrderTimestampCount: Int
public var durationSeconds: Double
public var usableDurationSeconds: Double
public var estimatedSampleRateHz: Double
public var frameIntervalMADSeconds: Double
public var droppedFrameRatio: Double
public var maximumGapSeconds: Double
public var longGapDurationSeconds: Double
public init(
inputSampleCount: Int,
usableInputSampleCount: Int,
discardedSampleCount: Int,
duplicateTimestampCount: Int,
outOfOrderTimestampCount: Int,
durationSeconds: Double,
usableDurationSeconds: Double,
estimatedSampleRateHz: Double,
frameIntervalMADSeconds: Double,
droppedFrameRatio: Double,
maximumGapSeconds: Double,
longGapDurationSeconds: Double
) {
self.inputSampleCount = inputSampleCount
self.usableInputSampleCount = usableInputSampleCount
self.discardedSampleCount = discardedSampleCount
self.duplicateTimestampCount = duplicateTimestampCount
self.outOfOrderTimestampCount = outOfOrderTimestampCount
self.durationSeconds = max(durationSeconds, 0)
self.usableDurationSeconds = max(usableDurationSeconds, 0)
self.estimatedSampleRateHz = max(estimatedSampleRateHz, 0)
self.frameIntervalMADSeconds = max(frameIntervalMADSeconds, 0)
self.droppedFrameRatio = min(max(droppedFrameRatio, 0), 1)
self.maximumGapSeconds = max(maximumGapSeconds, 0)
self.longGapDurationSeconds = max(longGapDurationSeconds, 0)
}
}
public struct ResampledSignal: Equatable, Sendable {
public var timestampsSeconds: [Double]
public var values: [Double]
public var validityMask: [Bool]
public var diagnostics: TimebaseDiagnostics
public init(
timestampsSeconds: [Double],
values: [Double],
validityMask: [Bool],
diagnostics: TimebaseDiagnostics
) {
precondition(timestampsSeconds.count == values.count)
precondition(values.count == validityMask.count)
self.timestampsSeconds = timestampsSeconds
self.values = values
self.validityMask = validityMask
self.diagnostics = diagnostics
}
}
public struct DetectedPeak: Codable, Equatable, Sendable {
public var sampleIndex: Int
public var timestampSeconds: Double
public var amplitude: Double
public var prominence: Double
public init(sampleIndex: Int, timestampSeconds: Double, amplitude: Double, prominence: Double) {
self.sampleIndex = sampleIndex
self.timestampSeconds = timestampSeconds
self.amplitude = amplitude
self.prominence = max(prominence, 0)
}
}
public enum PulseIntervalRejectionReason: String, Codable, Equatable, Sendable {
case outsidePhysiologicalRange
case localOutlier
case invalidSignalRegion
case lowMorphologyQuality
}
public struct PulseInterval: Codable, Equatable, Sendable {
public var sourceIndex: Int
public var startSeconds: Double
public var endSeconds: Double
public var durationMilliseconds: Double
public var rejectionReason: PulseIntervalRejectionReason?
/// Per-beat technical quality. This is not a medical confidence score.
public var qualityScore: Double
public init(
sourceIndex: Int,
startSeconds: Double,
endSeconds: Double,
durationMilliseconds: Double,
rejectionReason: PulseIntervalRejectionReason? = nil,
qualityScore: Double = 1
) {
self.sourceIndex = sourceIndex
self.startSeconds = startSeconds
self.endSeconds = endSeconds
self.durationMilliseconds = durationMilliseconds
self.rejectionReason = rejectionReason
self.qualityScore = min(max(qualityScore.isFinite ? qualityScore : 0, 0), 1)
}
public var isAccepted: Bool { rejectionReason == nil }
}
public struct PeakDetectionResult: Equatable, Sendable {
public var peaks: [DetectedPeak]
public var intervals: [PulseInterval]
public var selectedPolarity: Int
public var confidence: Double
public init(
peaks: [DetectedPeak],
intervals: [PulseInterval],
selectedPolarity: Int,
confidence: Double
) {
self.peaks = peaks
self.intervals = intervals
self.selectedPolarity = selectedPolarity >= 0 ? 1 : -1
self.confidence = Self.clamp01(confidence)
}
public var acceptedIntervals: [PulseInterval] { intervals.filter(\.isAccepted) }
public var rejectedIntervalCount: Int { intervals.count - acceptedIntervals.count }
private static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return min(max(value, 0), 1)
}
}
public struct BPMEstimate: Codable, Equatable, Sendable {
/// Final BPM. It is nil when the independent temporal and frequency estimates disagree.
public var bpm: Double?
public var temporalBpm: Double?
public var frequencyBpm: Double?
public var methodAgreementBpm: Double?
public var validPeakCount: Int
public var rejectedPeakCount: Int
public var confidence: Double
public var selectedChannel: PPGSignalChannel
public var selectedPolarity: Int
public init(
bpm: Double?,
temporalBpm: Double?,
frequencyBpm: Double?,
methodAgreementBpm: Double?,
validPeakCount: Int,
rejectedPeakCount: Int,
confidence: Double,
selectedChannel: PPGSignalChannel,
selectedPolarity: Int
) {
self.bpm = bpm
self.temporalBpm = temporalBpm
self.frequencyBpm = frequencyBpm
self.methodAgreementBpm = methodAgreementBpm
self.validPeakCount = max(validPeakCount, 0)
self.rejectedPeakCount = max(rejectedPeakCount, 0)
self.confidence = Self.clamp01(confidence)
self.selectedChannel = selectedChannel
self.selectedPolarity = selectedPolarity >= 0 ? 1 : -1
}
private static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return min(max(value, 0), 1)
}
}
public struct PRVMetrics: Codable, Equatable, Sendable {
public var estimatedRmssdMs: Double?
public var estimatedSdnnMs: Double?
public var meanIpiMs: Double?
public var medianIpiMs: Double?
public var pnn50Percent: Double?
public var rangeIpiMs: Double?
public var modeIpiMs: Double?
public var amo50Percent: Double?
public var coefficientOfVariation: Double?
public var validIntervalCount: Int
public var rejectedIntervalCount: Int
public var rejectedIntervalRatio: Double
public var reliable: Bool
public init(
estimatedRmssdMs: Double?,
estimatedSdnnMs: Double?,
meanIpiMs: Double?,
medianIpiMs: Double?,
pnn50Percent: Double?,
rangeIpiMs: Double?,
modeIpiMs: Double?,
amo50Percent: Double?,
coefficientOfVariation: Double?,
validIntervalCount: Int,
rejectedIntervalCount: Int,
rejectedIntervalRatio: Double,
reliable: Bool
) {
self.estimatedRmssdMs = estimatedRmssdMs
self.estimatedSdnnMs = estimatedSdnnMs
self.meanIpiMs = meanIpiMs
self.medianIpiMs = medianIpiMs
self.pnn50Percent = pnn50Percent
self.rangeIpiMs = rangeIpiMs
self.modeIpiMs = modeIpiMs
self.amo50Percent = amo50Percent
self.coefficientOfVariation = coefficientOfVariation
self.validIntervalCount = max(validIntervalCount, 0)
self.rejectedIntervalCount = max(rejectedIntervalCount, 0)
self.rejectedIntervalRatio = Self.clamp01(rejectedIntervalRatio)
self.reliable = reliable
}
private static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 1 }
return min(max(value, 0), 1)
}
}
public enum SignalQualityLevel: String, Codable, Equatable, Sendable {
case good
case medium
case poor
case invalid
}
public enum SignalQualityIssue: String, Codable, Equatable, Hashable, Sendable {
case insufficientDuration
case invalidTimestamps
case longInterruption
case tooManyDroppedFrames
case irregularTimestamps
case poorFingerContact
case excessiveSaturation
case motionArtifacts
case lowPulsatileAmplitude
case lowSignalToNoise
case tooFewPeaks
case inconsistentBPMMethods
case tooManyRejectedIntervals
case unstableChannel
case nonFiniteInput
}
public struct SignalQualityResult: Codable, Equatable, Sendable {
public var score: Double
public var level: SignalQualityLevel
public var usableDurationSeconds: Double
public var droppedFrameRatio: Double
public var saturationRatio: Double
public var rejectedIntervalRatio: Double
/// Signal-to-residual ratio in decibels around the selected pulse frequency.
public var signalToNoiseRatio: Double?
public var motionArtifactRatio: Double
public var methodAgreementScore: Double
public var issues: [SignalQualityIssue]
public init(
score: Double,
level: SignalQualityLevel,
usableDurationSeconds: Double,
droppedFrameRatio: Double,
saturationRatio: Double,
rejectedIntervalRatio: Double,
signalToNoiseRatio: Double?,
motionArtifactRatio: Double,
methodAgreementScore: Double,
issues: [SignalQualityIssue]
) {
self.score = Self.clamp01(score)
self.level = level
self.usableDurationSeconds = max(usableDurationSeconds, 0)
self.droppedFrameRatio = Self.clamp01(droppedFrameRatio)
self.saturationRatio = Self.clamp01(saturationRatio)
self.rejectedIntervalRatio = Self.clamp01(rejectedIntervalRatio)
self.signalToNoiseRatio = signalToNoiseRatio?.isFinite == true ? signalToNoiseRatio : nil
self.motionArtifactRatio = Self.clamp01(motionArtifactRatio)
self.methodAgreementScore = Self.clamp01(methodAgreementScore)
self.issues = Array(Set(issues)).sorted { $0.rawValue < $1.rawValue }
}
private static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return min(max(value, 0), 1)
}
}
public enum PPGAnalysisStatus: String, Codable, Equatable, Sendable {
case valid
case invalid
}
public struct PPGAnalysisResult: Codable, Equatable, Sendable {
public var status: PPGAnalysisStatus
public var bpm: BPMEstimate
public var prv: PRVMetrics
public var quality: SignalQualityResult
public var peaks: [DetectedPeak]
public var intervals: [PulseInterval]
public var timebase: TimebaseDiagnostics
public init(
status: PPGAnalysisStatus,
bpm: BPMEstimate,
prv: PRVMetrics,
quality: SignalQualityResult,
peaks: [DetectedPeak],
intervals: [PulseInterval],
timebase: TimebaseDiagnostics
) {
self.status = status
self.bpm = bpm
self.prv = prv
self.quality = quality
self.peaks = peaks
self.intervals = intervals
self.timebase = timebase
}
}
@@ -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
}
}
@@ -0,0 +1,242 @@
import Foundation
/// A deterministic artifact window used by ``PPGSyntheticSignalConfiguration``.
public struct PPGSyntheticArtifactWindow: Codable, Equatable, Sendable {
public var startSeconds: Double
public var endSeconds: Double
public var intensity: Double
public init(startSeconds: Double, endSeconds: Double, intensity: Double = 1) {
self.startSeconds = max(startSeconds.isFinite ? startSeconds : 0, 0)
self.endSeconds = max(endSeconds.isFinite ? endSeconds : 0, self.startSeconds)
self.intensity = min(max(intensity.isFinite ? intensity : 0, 0), 1)
}
func intensity(at time: Double) -> Double {
(startSeconds...endSeconds).contains(time) ? intensity : 0
}
}
/// Configuration for a numeric PPG camera fixture.
///
/// The generator creates the same compact RGB/luminance/time values consumed by
/// production signal processing. It never creates or retains images. All random
/// noise is seeded, so a failing test can be reproduced byte for byte.
public struct PPGSyntheticSignalConfiguration: Codable, Equatable, Sendable {
public var sampleRateHz: Double
public var durationSeconds: Double
public var startingBPM: Double
public var endingBPM: Double
/// Repeating beat-to-beat offsets, in milliseconds, added to the nominal IPI.
public var intervalVariationMilliseconds: [Double]
public var noiseAmplitude: Double
public var driftAmplitude: Double
public var driftFrequencyHz: Double
public var movementWindows: [PPGSyntheticArtifactWindow]
public var saturationWindows: [PPGSyntheticArtifactWindow]
/// Every nth nominal camera frame is omitted. Nil keeps every frame.
public var dropEveryNthFrame: Int?
/// Suppresses the pulse cycle beginning at each listed zero-based beat index.
public var missingBeatIndices: Set<Int>
/// Adds a narrow artificial pulse at each supplied timestamp.
public var falsePeakTimesSeconds: [Double]
public var seed: UInt64
public init(
sampleRateHz: Double = 30,
durationSeconds: Double = 60,
startingBPM: Double = 75,
endingBPM: Double? = nil,
intervalVariationMilliseconds: [Double] = [],
noiseAmplitude: Double = 0,
driftAmplitude: Double = 0,
driftFrequencyHz: Double = 0.05,
movementWindows: [PPGSyntheticArtifactWindow] = [],
saturationWindows: [PPGSyntheticArtifactWindow] = [],
dropEveryNthFrame: Int? = nil,
missingBeatIndices: Set<Int> = [],
falsePeakTimesSeconds: [Double] = [],
seed: UInt64 = 0x4841_5054_494B_4101
) {
self.sampleRateHz = sampleRateHz
self.durationSeconds = durationSeconds
self.startingBPM = startingBPM
self.endingBPM = endingBPM ?? startingBPM
self.intervalVariationMilliseconds = intervalVariationMilliseconds
self.noiseAmplitude = max(noiseAmplitude, 0)
self.driftAmplitude = max(driftAmplitude, 0)
self.driftFrequencyHz = max(driftFrequencyHz, 0)
self.movementWindows = movementWindows
self.saturationWindows = saturationWindows
self.dropEveryNthFrame = dropEveryNthFrame
self.missingBeatIndices = missingBeatIndices
self.falsePeakTimesSeconds = falsePeakTimesSeconds
self.seed = seed
}
public func validated() throws -> Self {
guard sampleRateHz.isFinite, sampleRateHz >= 10, sampleRateHz <= 240 else {
throw PPGSignalError.invalidConfiguration("synthetic sampleRateHz must be in 10...240")
}
guard durationSeconds.isFinite, durationSeconds > 0 else {
throw PPGSignalError.invalidConfiguration("synthetic durationSeconds must be positive")
}
guard startingBPM.isFinite, endingBPM.isFinite,
(30...240).contains(startingBPM), (30...240).contains(endingBPM)
else {
throw PPGSignalError.invalidConfiguration("synthetic BPM must be in 30...240")
}
guard intervalVariationMilliseconds.allSatisfy({ $0.isFinite && abs($0) <= 300 }) else {
throw PPGSignalError.invalidConfiguration("synthetic interval variation must be finite and <= 300 ms")
}
if let dropEveryNthFrame, dropEveryNthFrame < 2 {
throw PPGSignalError.invalidConfiguration("dropEveryNthFrame must be at least 2")
}
return self
}
}
public struct PPGSyntheticSignal: Equatable, Sendable {
public var samples: [PPGFrameSample]
public var beatTimesSeconds: [Double]
public var nominalIntervalsMilliseconds: [Double]
public init(
samples: [PPGFrameSample],
beatTimesSeconds: [Double],
nominalIntervalsMilliseconds: [Double]
) {
self.samples = samples
self.beatTimesSeconds = beatTimesSeconds
self.nominalIntervalsMilliseconds = nominalIntervalsMilliseconds
}
}
public enum PPGSyntheticSignalGenerator {
public static func generate(
configuration input: PPGSyntheticSignalConfiguration
) throws -> PPGSyntheticSignal {
let configuration = try input.validated()
let beatSchedule = makeBeatSchedule(configuration)
let frameCount = Int((configuration.durationSeconds * configuration.sampleRateHz).rounded(.down)) + 1
var random = SeededRandomNumberGenerator(seed: configuration.seed)
var samples: [PPGFrameSample] = []
samples.reserveCapacity(frameCount)
var beatIndex = 0
for frameIndex in 0..<frameCount {
if let nth = configuration.dropEveryNthFrame,
frameIndex > 0, frameIndex.isMultiple(of: nth) {
continue
}
let time = Double(frameIndex) / configuration.sampleRateHz
while beatIndex + 1 < beatSchedule.times.count,
beatSchedule.times[beatIndex + 1] <= time {
beatIndex += 1
}
let currentBeat = beatSchedule.times[min(beatIndex, beatSchedule.times.count - 1)]
let nextBeat = beatIndex + 1 < beatSchedule.times.count
? beatSchedule.times[beatIndex + 1]
: currentBeat + 60 / configuration.endingBPM
let phase = min(max((time - currentBeat) / max(nextBeat - currentBeat, 1e-6), 0), 1)
var pulse = cos(2 * Double.pi * phase) + 0.18 * cos(4 * Double.pi * phase)
if configuration.missingBeatIndices.contains(beatIndex) { pulse = 0 }
for falsePeakTime in configuration.falsePeakTimesSeconds where falsePeakTime.isFinite {
let distance = (time - falsePeakTime) / 0.055
pulse += 1.25 * exp(-0.5 * distance * distance)
}
let drift = configuration.driftAmplitude
* sin(2 * Double.pi * configuration.driftFrequencyHz * time)
let movement = configuration.movementWindows.map { $0.intensity(at: time) }.max() ?? 0
let saturation = configuration.saturationWindows.map { $0.intensity(at: time) }.max() ?? 0
let baseNoise = random.nextGaussian() * configuration.noiseAmplitude
let movementNoise = random.nextGaussian() * movement * 16
let commonLight = 1 + drift + movementNoise * 0.006
var red = 176 * commonLight + 2.8 * pulse + 0.55 * baseNoise + movementNoise
var green = 92 * commonLight + 6.2 * pulse + baseNoise - 0.35 * movementNoise
var blue = 68 * commonLight + 1.8 * pulse + 0.45 * baseNoise + 0.20 * movementNoise
if saturation > 0 {
red = red * (1 - saturation) + 255 * saturation
green = green * (1 - saturation) + 255 * saturation
blue = blue * (1 - saturation) + 230 * saturation
}
red = min(max(red, 0), 255)
green = min(max(green, 0), 255)
blue = min(max(blue, 0), 255)
let luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
samples.append(
PPGFrameSample(
timestampSeconds: time,
red: red,
green: green,
blue: blue,
luminance: luminance,
contactConfidence: max(0.95 - 0.25 * movement, 0),
saturationRatio: saturation,
motionArtifactScore: movement
)
)
}
return PPGSyntheticSignal(
samples: samples,
beatTimesSeconds: beatSchedule.times,
nominalIntervalsMilliseconds: beatSchedule.intervals
)
}
private static func makeBeatSchedule(
_ configuration: PPGSyntheticSignalConfiguration
) -> (times: [Double], intervals: [Double]) {
var times = [0.0]
var intervals: [Double] = []
var beatIndex = 0
while let last = times.last, last < configuration.durationSeconds {
let progress = min(max(last / configuration.durationSeconds, 0), 1)
let bpm = configuration.startingBPM
+ (configuration.endingBPM - configuration.startingBPM) * progress
let variation = configuration.intervalVariationMilliseconds.isEmpty
? 0
: configuration.intervalVariationMilliseconds[
beatIndex % configuration.intervalVariationMilliseconds.count
]
let intervalMs = max(60_000 / bpm + variation, 250)
intervals.append(intervalMs)
times.append(last + intervalMs / 1_000)
beatIndex += 1
}
return (times, intervals)
}
}
private struct SeededRandomNumberGenerator: RandomNumberGenerator {
private var state: UInt64
private var spareGaussian: Double?
init(seed: UInt64) {
state = seed == 0 ? 0x9E37_79B9_7F4A_7C15 : seed
}
mutating func next() -> UInt64 {
state &+= 0x9E37_79B9_7F4A_7C15
var value = state
value = (value ^ (value >> 30)) &* 0xBF58_476D_1CE4_E5B9
value = (value ^ (value >> 27)) &* 0x94D0_49BB_1331_11EB
return value ^ (value >> 31)
}
mutating func nextGaussian() -> Double {
if let spareGaussian {
self.spareGaussian = nil
return spareGaussian
}
let u1 = max(Double(next()) / Double(UInt64.max), 1e-12)
let u2 = Double(next()) / Double(UInt64.max)
let radius = sqrt(-2 * log(u1))
let angle = 2 * Double.pi * u2
spareGaussian = radius * sin(angle)
return radius * cos(angle)
}
}
@@ -0,0 +1,143 @@
import Foundation
public struct PRVCalculationResult: Equatable, Sendable {
public var metrics: PRVMetrics
public var intervals: [PulseInterval]
public init(metrics: PRVMetrics, intervals: [PulseInterval]) {
self.metrics = metrics
self.intervals = intervals
}
}
public enum PPGPRVCalculator {
public static func calculate(
intervals inputIntervals: [PulseInterval],
configuration: PPGProcessingConfiguration
) throws -> PRVCalculationResult {
let configuration = try configuration.validated()
var intervals = inputIntervals
let minimumInterval = 1_000 / configuration.maximumFrequencyHz
let maximumInterval = 1_000 / configuration.minimumFrequencyHz
for index in intervals.indices where intervals[index].rejectionReason == nil {
let value = intervals[index].durationMilliseconds
if !value.isFinite || value < minimumInterval || value > maximumInterval {
intervals[index].rejectionReason = .outsidePhysiologicalRange
}
}
// Filter against a moving neighbourhood (±13 intervals), not one global
// median. This follows gradual physiological changes while isolating a
// missed/double peak or a short movement artifact. Decisions use a frozen
// snapshot so one rejected beat cannot cascade into later rejections.
let initiallyAccepted = Set(intervals.indices.filter { intervals[$0].isAccepted })
if initiallyAccepted.count >= 3 {
for index in intervals.indices where initiallyAccepted.contains(index) {
let lower = max(0, index - 13)
let upper = min(intervals.count - 1, index + 13)
let localValues = (lower...upper).compactMap { neighbour -> Double? in
guard neighbour != index, initiallyAccepted.contains(neighbour) else { return nil }
return intervals[neighbour].durationMilliseconds
}
guard localValues.count >= 2 else { continue }
let center = SignalStatistics.median(localValues)
let robustDeviation = 1.4826 * SignalStatistics.mad(localValues, around: center)
let allowedDeviation = max(center * 0.20, 3.5 * robustDeviation, 70)
let value = intervals[index].durationMilliseconds
let resemblesMissedBeat = abs(value - 2 * center) <= max(0.18 * center, 70)
let resemblesDoublePeak = abs(2 * value - center) <= max(0.18 * center, 70)
if abs(value - center) > allowedDeviation
|| resemblesMissedBeat
|| resemblesDoublePeak {
intervals[index].rejectionReason = .localOutlier
}
}
}
let accepted = intervals.filter(\.isAccepted)
let values = accepted.map(\.durationMilliseconds)
let rejectedCount = intervals.count - accepted.count
let rejectedRatio = intervals.isEmpty
? 1
: Double(rejectedCount) / Double(intervals.count)
let median = values.isEmpty ? nil : SignalStatistics.median(values)
let mean = values.isEmpty ? nil : SignalStatistics.mean(values)
let sdnn = values.count >= 2 ? SignalStatistics.standardDeviation(values) : nil
let coefficientOfVariation: Double?
if let median, let sdnn, median > 0 {
coefficientOfVariation = sdnn / median
} else {
coefficientOfVariation = nil
}
var successiveDifferences: [Double] = []
for pair in zip(accepted, accepted.dropFirst()) {
// Do not bridge over a rejected or missing beat: it would create a fictitious PRV difference.
guard pair.1.sourceIndex == pair.0.sourceIndex + 1 else { continue }
let difference = pair.1.durationMilliseconds - pair.0.durationMilliseconds
successiveDifferences.append(difference)
}
let squaredSuccessiveDifferences = successiveDifferences.map { $0 * $0 }
let rmssd = squaredSuccessiveDifferences.isEmpty
? nil
: SignalStatistics.mean(squaredSuccessiveDifferences).squareRoot()
let pnn50Percent = successiveDifferences.isEmpty
? nil
: 100 * Double(successiveDifferences.filter { abs($0) > 50 }.count)
/ Double(successiveDifferences.count)
let rangeIpiMs: Double? = values.count >= 2
? (values.max() ?? 0) - (values.min() ?? 0)
: nil
let histogram = Dictionary(grouping: values) { Int(floor($0 / 50)) }
let modalBin = histogram.max { lhs, rhs in
if lhs.value.count == rhs.value.count { return lhs.key > rhs.key }
return lhs.value.count < rhs.value.count
}
let modeIpiMs = modalBin.map { SignalStatistics.mean($0.value) }
let amo50Percent = modalBin.map {
100 * Double($0.value.count) / Double(values.count)
}
let reliable = values.count >= configuration.minimumIntervalsForPRV
&& rejectedRatio <= configuration.maximumRejectedIntervalRatioForPRV
&& rmssd != nil
&& sdnn != nil
let metrics = PRVMetrics(
estimatedRmssdMs: rmssd,
estimatedSdnnMs: sdnn,
meanIpiMs: mean,
medianIpiMs: median,
pnn50Percent: pnn50Percent,
rangeIpiMs: rangeIpiMs,
modeIpiMs: modeIpiMs,
amo50Percent: amo50Percent,
coefficientOfVariation: coefficientOfVariation,
validIntervalCount: values.count,
rejectedIntervalCount: rejectedCount,
rejectedIntervalRatio: rejectedRatio,
reliable: reliable
)
return PRVCalculationResult(metrics: metrics, intervals: intervals)
}
public static func calculate(
intervalsMilliseconds: [Double],
configuration: PPGProcessingConfiguration
) throws -> PRVMetrics {
var elapsed = 0.0
let intervals = intervalsMilliseconds.enumerated().map { index, duration in
let start = elapsed
elapsed += duration / 1_000
return PulseInterval(
sourceIndex: index,
startSeconds: start,
endSeconds: elapsed,
durationMilliseconds: duration
)
}
return try calculate(intervals: intervals, configuration: configuration).metrics
}
}
@@ -0,0 +1,276 @@
import Foundation
public enum PPGPeakDetector {
public static func detect(
signal: [Double],
timestampsSeconds: [Double],
validityMask: [Bool],
configuration: PPGProcessingConfiguration
) throws -> PeakDetectionResult {
let configuration = try configuration.validated()
guard signal.count == timestampsSeconds.count,
signal.count == validityMask.count,
signal.count >= 5
else {
throw PPGSignalError.insufficientSamples(required: 5, actual: signal.count)
}
let positive = detectPolarity(
signal: signal,
timestampsSeconds: timestampsSeconds,
validityMask: validityMask,
polarity: 1,
configuration: configuration
)
let negative = detectPolarity(
signal: signal,
timestampsSeconds: timestampsSeconds,
validityMask: validityMask,
polarity: -1,
configuration: configuration
)
if negative.confidence > positive.confidence + 1e-12 {
return negative
}
if abs(negative.confidence - positive.confidence) <= 1e-12,
negative.acceptedIntervals.count > positive.acceptedIntervals.count {
return negative
}
return positive
}
private struct Candidate {
var index: Int
var amplitude: Double
var prominence: Double
}
private static func detectPolarity(
signal: [Double],
timestampsSeconds: [Double],
validityMask: [Bool],
polarity: Int,
configuration: PPGProcessingConfiguration
) -> PeakDetectionResult {
let values = signal.map { ($0.isFinite ? $0 : 0) * Double(polarity) }
let timestampIntervals = zip(timestampsSeconds, timestampsSeconds.dropFirst())
.map { $1 - $0 }
.filter { $0.isFinite && $0 > 0 }
let sampleInterval = SignalStatistics.median(timestampIntervals)
guard sampleInterval > 0 else {
return PeakDetectionResult(peaks: [], intervals: [], selectedPolarity: polarity, confidence: 0)
}
let sampleRate = 1 / sampleInterval
let validValues = values.indices.compactMap { validityMask[$0] ? values[$0] : nil }
let center = SignalStatistics.median(validValues)
let robustScale = max(1.4826 * SignalStatistics.mad(validValues, around: center), 1e-6)
let amplitudeThreshold = center + max(configuration.minimumPeakAmplitude, robustScale * 0.18)
let prominenceRadius = max(Int((0.45 * sampleRate).rounded()), 2)
var candidates: [Candidate] = []
candidates.reserveCapacity(max(signal.count / 15, 8))
for index in 1..<(values.count - 1) {
guard validityMask[index],
validityMask[index - 1],
validityMask[index + 1],
values[index] >= amplitudeThreshold,
values[index] > values[index - 1],
values[index] >= values[index + 1]
else { continue }
let leftStart = max(0, index - prominenceRadius)
let rightEnd = min(values.count - 1, index + prominenceRadius)
let leftMinimum = values[leftStart..<index].min() ?? values[index]
let rightMinimum = values[(index + 1)...rightEnd].min() ?? values[index]
let prominence = min(values[index] - leftMinimum, values[index] - rightMinimum)
guard prominence >= configuration.minimumPeakProminence else { continue }
candidates.append(Candidate(index: index, amplitude: values[index], prominence: prominence))
}
let minimumDistance = max(
Int((configuration.refractoryPeriodSeconds * sampleRate).rounded()),
2
)
var selected: [Candidate] = []
for candidate in candidates {
if let last = selected.last, candidate.index - last.index < minimumDistance {
let candidateStrength = candidate.prominence + 0.25 * candidate.amplitude
let lastStrength = last.prominence + 0.25 * last.amplitude
if candidateStrength > lastStrength {
selected[selected.count - 1] = candidate
}
} else {
selected.append(candidate)
}
}
let peaks = selected.map { candidate -> DetectedPeak in
let index = candidate.index
let left = values[index - 1]
let middle = values[index]
let right = values[index + 1]
let denominator = left - 2 * middle + right
let fractionalOffset: Double
if abs(denominator) > 1e-12 {
fractionalOffset = min(max(0.5 * (left - right) / denominator, -0.5), 0.5)
} else {
fractionalOffset = 0
}
let refinedTimestamp = timestampsSeconds[index] + fractionalOffset * sampleInterval
return DetectedPeak(
sampleIndex: index,
timestampSeconds: refinedTimestamp,
amplitude: candidate.amplitude,
prominence: candidate.prominence
)
}
var intervals = zip(peaks, peaks.dropFirst()).enumerated().map { sourceIndex, pair in
let durationMilliseconds = (pair.1.timestampSeconds - pair.0.timestampSeconds) * 1_000
let lowerIndex = min(pair.0.sampleIndex, pair.1.sampleIndex)
let upperIndex = max(pair.0.sampleIndex, pair.1.sampleIndex)
let crossesInvalidRegion = validityMask[lowerIndex...upperIndex].contains(false)
return PulseInterval(
sourceIndex: sourceIndex,
startSeconds: pair.0.timestampSeconds,
endSeconds: pair.1.timestampSeconds,
durationMilliseconds: durationMilliseconds,
rejectionReason: crossesInvalidRegion ? .invalidSignalRegion : nil
)
}
let minimumInterval = 1_000 / configuration.maximumFrequencyHz
let maximumInterval = 1_000 / configuration.minimumFrequencyHz
for index in intervals.indices where intervals[index].rejectionReason == nil {
let duration = intervals[index].durationMilliseconds
if !duration.isFinite || duration < minimumInterval || duration > maximumInterval {
intervals[index].rejectionReason = .outsidePhysiologicalRange
}
}
// Score every pulse independently. A short movement or pressure change can
// therefore reject only the affected beats instead of invalidating the
// complete recording. Cycles are normalized to 50 points before their
// shape is compared with neighbouring cycles, following the published
// morphology-based quality approach used for camera PPG.
let cycleShapes = intervals.indices.map { index in
normalizedCycle(
values: values,
lowerIndex: peaks[index].sampleIndex,
upperIndex: peaks[index + 1].sampleIndex,
outputCount: 50
)
}
let medianProminence = max(SignalStatistics.median(peaks.map(\.prominence)), 1e-9)
for index in intervals.indices {
let lowerPeakQuality = SignalStatistics.clamp01(peaks[index].prominence / medianProminence)
let upperPeakQuality = SignalStatistics.clamp01(peaks[index + 1].prominence / medianProminence)
let prominenceQuality = min(lowerPeakQuality, upperPeakQuality)
let neighbourhood = max(0, index - 3)...min(intervals.count - 1, index + 3)
let correlations = neighbourhood.compactMap { neighbour -> Double? in
guard neighbour != index,
let shape = cycleShapes[index],
let other = cycleShapes[neighbour]
else { return nil }
return shapeCorrelation(shape, other)
}
let morphologyQuality: Double
if correlations.isEmpty {
morphologyQuality = intervals.count < 3 ? 0.75 : 0
} else {
// Use the best two local matches so one adjacent corrupted cycle does
// not drag a clean beat below the acceptance threshold.
let strongest = correlations.sorted(by: >).prefix(2)
morphologyQuality = SignalStatistics.clamp01(
(SignalStatistics.mean(Array(strongest)) + 1) / 2
)
}
let technicalQuality = SignalStatistics.clamp01(
0.65 * morphologyQuality + 0.35 * prominenceQuality
)
intervals[index].qualityScore = technicalQuality
if intervals[index].rejectionReason == nil,
intervals.count >= 3,
technicalQuality < 0.42 {
intervals[index].rejectionReason = .lowMorphologyQuality
}
}
let physiologicallyPlausible = intervals
.filter(\.isAccepted)
.map(\.durationMilliseconds)
if physiologicallyPlausible.count >= 3 {
let median = SignalStatistics.median(physiologicallyPlausible)
let robustDeviation = 1.4826 * SignalStatistics.mad(physiologicallyPlausible, around: median)
let allowedDeviation = max(0.28 * median, 3.5 * robustDeviation, 90)
for index in intervals.indices where intervals[index].rejectionReason == nil {
if abs(intervals[index].durationMilliseconds - median) > allowedDeviation {
intervals[index].rejectionReason = .localOutlier
}
}
}
let accepted = intervals.filter(\.isAccepted)
let acceptedDurations = accepted.map(\.durationMilliseconds)
let intervalAcceptance = intervals.isEmpty
? 0
: Double(accepted.count) / Double(intervals.count)
let countScore = min(Double(accepted.count) / 8, 1)
let regularity: Double
if acceptedDurations.count >= 2 {
let median = max(SignalStatistics.median(acceptedDurations), 1)
let robustCV = 1.4826 * SignalStatistics.mad(acceptedDurations, around: median) / median
regularity = SignalStatistics.clamp01(1 - robustCV / 0.20)
} else {
regularity = 0
}
let prominenceScore = SignalStatistics.clamp01(medianProminence / 1.5)
let validCoverage = Double(validityMask.filter { $0 }.count) / Double(max(validityMask.count, 1))
let confidence = 0.27 * countScore
+ 0.23 * intervalAcceptance
+ 0.20 * regularity
+ 0.20 * prominenceScore
+ 0.10 * validCoverage
return PeakDetectionResult(
peaks: peaks,
intervals: intervals,
selectedPolarity: polarity,
confidence: confidence
)
}
private static func normalizedCycle(
values: [Double],
lowerIndex: Int,
upperIndex: Int,
outputCount: Int
) -> [Double]? {
guard lowerIndex >= 0,
upperIndex < values.count,
upperIndex - lowerIndex >= 3,
outputCount >= 3
else { return nil }
let span = Double(upperIndex - lowerIndex)
let sampled = (0..<outputCount).map { outputIndex -> Double in
let position = Double(lowerIndex)
+ Double(outputIndex) * span / Double(outputCount - 1)
let left = min(max(Int(position.rounded(.down)), lowerIndex), upperIndex)
let right = min(left + 1, upperIndex)
let fraction = position - Double(left)
return values[left] + fraction * (values[right] - values[left])
}
let center = SignalStatistics.mean(sampled)
let scale = SignalStatistics.standardDeviation(sampled)
guard scale.isFinite, scale > 1e-8 else { return nil }
return sampled.map { ($0 - center) / scale }
}
private static func shapeCorrelation(_ lhs: [Double], _ rhs: [Double]) -> Double? {
guard lhs.count == rhs.count, lhs.count >= 3 else { return nil }
let value = zip(lhs, rhs).reduce(0.0) { $0 + $1.0 * $1.1 } / Double(lhs.count)
return value.isFinite ? min(max(value, -1), 1) : nil
}
}
@@ -0,0 +1,135 @@
import Foundation
public enum PPGSignalResampler {
/// Resamples irregular samples onto a uniform time grid using the real frame timestamps.
/// Values spanning a long gap are numerically interpolated to keep downstream filters finite,
/// but the corresponding validity mask remains false so no beat can be accepted there.
public static func resample(
_ samples: [TimestampedValue],
targetSampleRateHz: Double,
maximumInterpolationGapSeconds: Double
) throws -> ResampledSignal {
guard targetSampleRateHz.isFinite, targetSampleRateHz > 0 else {
throw PPGSignalError.invalidConfiguration("targetSampleRateHz must be positive")
}
guard maximumInterpolationGapSeconds.isFinite, maximumInterpolationGapSeconds > 0 else {
throw PPGSignalError.invalidConfiguration("maximumInterpolationGapSeconds must be positive")
}
let finite = samples.enumerated().compactMap { index, sample -> (Int, TimestampedValue)? in
guard sample.timestampSeconds.isFinite, sample.value.isFinite else { return nil }
return (index, sample)
}
guard finite.count >= 3 else {
throw PPGSignalError.insufficientSamples(required: 3, actual: finite.count)
}
let outOfOrderCount = zip(finite, finite.dropFirst()).reduce(0) { count, pair in
count + (pair.1.1.timestampSeconds < pair.0.1.timestampSeconds ? 1 : 0)
}
let sorted = finite.sorted {
if $0.1.timestampSeconds == $1.1.timestampSeconds { return $0.0 < $1.0 }
return $0.1.timestampSeconds < $1.1.timestampSeconds
}
var unique: [TimestampedValue] = []
unique.reserveCapacity(sorted.count)
var duplicateTimestampCount = 0
for (_, sample) in sorted {
if let last = unique.last, sample.timestampSeconds - last.timestampSeconds <= 1e-9 {
duplicateTimestampCount += 1
continue
}
unique.append(sample)
}
guard unique.count >= 3 else {
throw PPGSignalError.invalidTimestamps
}
let intervals = zip(unique, unique.dropFirst()).map {
$1.timestampSeconds - $0.timestampSeconds
}
guard intervals.allSatisfy({ $0.isFinite && $0 > 0 }) else {
throw PPGSignalError.invalidTimestamps
}
let medianInterval = SignalStatistics.median(intervals)
guard medianInterval > 0 else { throw PPGSignalError.invalidTimestamps }
let intervalMAD = SignalStatistics.mad(intervals, around: medianInterval)
let maximumGap = intervals.max() ?? 0
let duration = unique.last!.timestampSeconds - unique.first!.timestampSeconds
var inferredMissingFrames = 0
var longGapDuration = 0.0
for interval in intervals {
if interval > medianInterval * 1.5 {
inferredMissingFrames += max(Int((interval / medianInterval).rounded()) - 1, 0)
}
if interval > maximumInterpolationGapSeconds {
longGapDuration += max(interval - medianInterval, 0)
}
}
let inferredTotalFrames = unique.count + inferredMissingFrames
let droppedFrameRatio = inferredTotalFrames > 0
? Double(inferredMissingFrames) / Double(inferredTotalFrames)
: 0
let usableDuration = max(duration - longGapDuration, 0)
let step = 1 / targetSampleRateHz
let outputCount = max(Int((duration * targetSampleRateHz).rounded(.down)) + 1, 1)
var timestamps: [Double] = []
var values: [Double] = []
var validityMask: [Bool] = []
timestamps.reserveCapacity(outputCount)
values.reserveCapacity(outputCount)
validityMask.reserveCapacity(outputCount)
var upperIndex = 1
let firstTimestamp = unique[0].timestampSeconds
for outputIndex in 0..<outputCount {
let timestamp = firstTimestamp + Double(outputIndex) * step
while upperIndex < unique.count - 1,
unique[upperIndex].timestampSeconds < timestamp {
upperIndex += 1
}
let upper = unique[upperIndex]
let lower = unique[upperIndex - 1]
let gap = upper.timestampSeconds - lower.timestampSeconds
let fraction = gap > 0 ? (timestamp - lower.timestampSeconds) / gap : 0
let clampedFraction = min(max(fraction, 0), 1)
let value = lower.value + clampedFraction * (upper.value - lower.value)
let liesInsideSourceRange = timestamp >= lower.timestampSeconds - 1e-9
&& timestamp <= upper.timestampSeconds + 1e-9
let isExactSourceSample = abs(timestamp - lower.timestampSeconds) <= step * 1e-6
|| abs(timestamp - upper.timestampSeconds) <= step * 1e-6
let isValid = liesInsideSourceRange
&& (isExactSourceSample || gap <= maximumInterpolationGapSeconds)
timestamps.append(timestamp)
values.append(value.isFinite ? value : 0)
validityMask.append(isValid)
}
let diagnostics = TimebaseDiagnostics(
inputSampleCount: samples.count,
usableInputSampleCount: unique.count,
discardedSampleCount: samples.count - finite.count,
duplicateTimestampCount: duplicateTimestampCount,
outOfOrderTimestampCount: outOfOrderCount,
durationSeconds: duration,
usableDurationSeconds: usableDuration,
estimatedSampleRateHz: 1 / medianInterval,
frameIntervalMADSeconds: intervalMAD,
droppedFrameRatio: droppedFrameRatio,
maximumGapSeconds: maximumGap,
longGapDurationSeconds: longGapDuration
)
return ResampledSignal(
timestampsSeconds: timestamps,
values: values,
validityMask: validityMask,
diagnostics: diagnostics
)
}
}
@@ -0,0 +1,331 @@
import Foundation
public struct PPGSignalProcessor: Sendable {
public let configuration: PPGProcessingConfiguration
public init(configuration: PPGProcessingConfiguration = .production) throws {
self.configuration = try configuration.validated()
}
public func analyze(samples inputSamples: [PPGFrameSample]) throws -> PPGAnalysisResult {
let finiteSamples = inputSamples.filter(\.hasFiniteSignalValues)
let discardedInputCount = inputSamples.count - finiteSamples.count
guard finiteSamples.count >= 3 else {
throw PPGSignalError.insufficientSamples(required: 3, actual: finiteSamples.count)
}
let sortedTimestamps = finiteSamples.map(\.timestampSeconds).sorted()
guard let firstTimestamp = sortedTimestamps.first,
let lastTimestamp = sortedTimestamps.last,
lastTimestamp > firstTimestamp
else {
throw PPGSignalError.invalidTimestamps
}
let duration = lastTimestamp - firstTimestamp
guard duration >= configuration.minimumAnalysisDurationSeconds else {
throw PPGSignalError.insufficientDuration(
requiredSeconds: configuration.minimumAnalysisDurationSeconds,
actualSeconds: duration
)
}
let red = try resample(finiteSamples, value: \.red)
let green = try resample(finiteSamples, value: \.green)
let blue = try resample(finiteSamples, value: \.blue)
let luminance = try resample(finiteSamples, value: \.luminance)
let contact = try resample(finiteSamples, value: \.contactConfidence)
let saturation = try resample(finiteSamples, value: \.saturationRatio)
let motion = try resample(finiteSamples, value: \.motionArtifactScore)
let greenRedRatio = try PPGSignalResampler.resample(
finiteSamples.map {
let total = max($0.red + $0.green + $0.blue, 1)
return TimestampedValue(
timestampSeconds: $0.timestampSeconds,
value: $0.green / total
)
},
targetSampleRateHz: configuration.targetSampleRateHz,
maximumInterpolationGapSeconds: configuration.maximumInterpolationGapSeconds
)
let blueRedRatio = try PPGSignalResampler.resample(
finiteSamples.map {
let total = max($0.red + $0.green + $0.blue, 1)
return TimestampedValue(
timestampSeconds: $0.timestampSeconds,
value: $0.blue / total
)
},
targetSampleRateHz: configuration.targetSampleRateHz,
maximumInterpolationGapSeconds: configuration.maximumInterpolationGapSeconds
)
guard red.values.count == green.values.count,
red.values.count == luminance.values.count,
red.values.count == contact.values.count,
red.values.count == saturation.values.count,
red.values.count == motion.values.count
else {
throw PPGSignalError.invalidTimestamps
}
let blendValues = normalizedBlend(
red: red.values,
green: green.values,
blue: blue.values,
luminance: luminance.values
)
let blendMask = zip(red.validityMask, zip(green.validityMask, luminance.validityMask)).map {
$0.0 && $0.1.0 && $0.1.1
}
let blend = ResampledSignal(
timestampsSeconds: red.timestampsSeconds,
values: blendValues,
validityMask: blendMask,
diagnostics: red.diagnostics
)
let filter = try PPGBandPassFilter(
sampleRateHz: configuration.targetSampleRateHz,
lowCutoffHz: configuration.minimumFrequencyHz,
highCutoffHz: configuration.maximumFrequencyHz,
tapCount: configuration.filterTapCount
)
let commonMetricMask = zip(contact.validityMask, zip(saturation.validityMask, motion.validityMask))
.map { $0.0 && $0.1.0 && $0.1.1 }
// The coaching contact score must never decide whether a frame contains a
// pulse. Camera colour varies too much between front sensors and exposure
// modes. Only timestamp continuity and extreme optical corruption create
// invalid regions; periodicity and beat consistency decide quality later.
let physiologicalMask = contact.values.indices.map { index in
commonMetricMask[index]
&& saturation.values[index] <= configuration.maximumSaturationRatio
&& motion.values[index] <= configuration.maximumMotionArtifactScore
}
let guardedPhysiologicalMask = PPGSignalPreprocessor.dilateInvalidRegions(
physiologicalMask,
radius: max(Int((configuration.targetSampleRateHz * 0.20).rounded()), 1)
)
let sourceCandidates: [(PPGSignalChannel, ResampledSignal)] = [
(.red, red),
(.green, green),
(.blue, blue),
(.luminance, luminance),
(.greenRedRatio, greenRedRatio),
(.blueRedRatio, blueRedRatio),
(.normalizedBlend, blend),
]
let candidates = try sourceCandidates.map { channel, resampled in
try analyzeChannel(
channel: channel,
resampled: resampled,
guardedPhysiologicalMask: guardedPhysiologicalMask,
filter: filter
)
}
guard var selected = candidates.max(by: channelCandidateIsWorse) else {
throw PPGSignalError.noUsableChannel
}
selected.timebase.inputSampleCount = inputSamples.count
selected.timebase.discardedSampleCount += discardedInputCount
let prvCalculation = try PPGPRVCalculator.calculate(
intervals: selected.peakDetection.intervals,
configuration: configuration
)
let validCoverage = Double(selected.validityMask.filter { $0 }.count)
/ Double(max(selected.validityMask.count, 1))
var qualityTimebase = selected.timebase
qualityTimebase.usableDurationSeconds = min(
qualityTimebase.usableDurationSeconds,
qualityTimebase.durationSeconds * validCoverage
)
let quality = PPGSignalQualityEvaluator.evaluate(
SignalQualityInput(
timebase: qualityTimebase,
contactConfidence: contact.values,
saturationRatio: saturation.values,
motionArtifactScore: motion.values,
filteredSignal: selected.filteredSignal,
validityMask: selected.validityMask,
bpmEstimate: selected.bpm,
intervals: prvCalculation.intervals,
signalToNoiseRatioDB: selected.snrDB,
channelStabilityScore: selected.selectionScore
),
configuration: configuration
)
// A camera PPG can provide a useful mean heart rate before it provides
// beat-to-beat timing good enough for PRV. Publish that conservative BPM
// when the evaluator did not mark the signal structurally invalid; keep
// PRV reliability gated below by the stricter quality/confidence checks.
// This mirrors how practical camera HR apps remain useful on borderline
// captures without turning them into HRV measurements.
let isValid = quality.level != .invalid
&& quality.score >= PPGPublicationThresholds.bpmQualityScore
&& selected.bpm.confidence >= PPGPublicationThresholds.bpmConfidence
&& selected.bpm.bpm != nil
var publishedBPM = selected.bpm
var publishedPRV = prvCalculation.metrics
if !isValid {
// Keep independent estimates for diagnostics, but do not publish a definitive BPM.
publishedBPM.bpm = nil
publishedPRV.reliable = false
} else if quality.score < PPGPublicationThresholds.prvQualityScore
|| selected.bpm.confidence < PPGPublicationThresholds.prvBpmConfidence {
// Beat-to-beat variability requires a stricter signal than mean pulse rate.
publishedPRV.reliable = false
}
return PPGAnalysisResult(
status: isValid ? .valid : .invalid,
bpm: publishedBPM,
prv: publishedPRV,
quality: quality,
peaks: selected.peakDetection.peaks,
intervals: prvCalculation.intervals,
timebase: selected.timebase
)
}
private struct ChannelCandidate {
var channel: PPGSignalChannel
var filteredSignal: [Double]
var validityMask: [Bool]
var timebase: TimebaseDiagnostics
var peakDetection: PeakDetectionResult
var bpm: BPMEstimate
var snrDB: Double?
var selectionScore: Double
}
private func analyzeChannel(
channel: PPGSignalChannel,
resampled: ResampledSignal,
guardedPhysiologicalMask: [Bool],
filter: PPGBandPassFilter
) throws -> ChannelCandidate {
let prepared = PPGSignalPreprocessor.detrendAndNormalize(
resampled.values,
sampleRateHz: configuration.targetSampleRateHz
)
let filtered = filter.apply(to: prepared)
let gapGuardedMask = PPGSignalPreprocessor.dilateInvalidRegions(
resampled.validityMask,
radius: filter.invalidRegionGuardRadius
)
var validityMask = zip(gapGuardedMask, guardedPhysiologicalMask).map { $0.0 && $0.1 }
let edgeGuard = min(filter.invalidRegionGuardRadius, validityMask.count / 4)
if edgeGuard > 0 {
for index in 0..<edgeGuard { validityMask[index] = false }
for index in (validityMask.count - edgeGuard)..<validityMask.count {
validityMask[index] = false
}
}
let peakDetection = try PPGPeakDetector.detect(
signal: filtered,
timestampsSeconds: resampled.timestampsSeconds,
validityMask: validityMask,
configuration: configuration
)
let bpm = try PPGBPMEstimator.estimate(
signal: filtered,
timestampsSeconds: resampled.timestampsSeconds,
validityMask: validityMask,
peakDetection: peakDetection,
selectedChannel: channel,
configuration: configuration
)
let snrDB = bpm.frequencyBpm.flatMap { frequencyBPM in
SignalStatistics.periodicBandSNRDB(
values: filtered,
timestamps: resampled.timestampsSeconds,
validityMask: validityMask,
fundamentalFrequencyHz: frequencyBPM / 60,
minimumFrequencyHz: configuration.minimumFrequencyHz,
maximumFrequencyHz: configuration.maximumFrequencyHz
)
}
let snrScore = snrDB.map { SignalStatistics.clamp01(($0 + 3) / 15) } ?? 0
let coverage = Double(validityMask.filter { $0 }.count) / Double(max(validityMask.count, 1))
let acceptedIntervals = peakDetection.acceptedIntervals
let intervalAcceptance = peakDetection.intervals.isEmpty
? 0
: Double(acceptedIntervals.count) / Double(peakDetection.intervals.count)
let beatQuality = acceptedIntervals.isEmpty
? 0
: SignalStatistics.mean(acceptedIntervals.map(\.qualityScore))
let selectionScore = SignalStatistics.clamp01(
0.42 * bpm.confidence
+ 0.23 * snrScore
+ 0.13 * coverage
+ 0.12 * intervalAcceptance
+ 0.10 * beatQuality
)
return ChannelCandidate(
channel: channel,
filteredSignal: filtered,
validityMask: validityMask,
timebase: resampled.diagnostics,
peakDetection: peakDetection,
bpm: bpm,
snrDB: snrDB,
selectionScore: selectionScore
)
}
private func resample(
_ samples: [PPGFrameSample],
value keyPath: KeyPath<PPGFrameSample, Double>
) throws -> ResampledSignal {
try PPGSignalResampler.resample(
samples.map {
TimestampedValue(timestampSeconds: $0.timestampSeconds, value: $0[keyPath: keyPath])
},
targetSampleRateHz: configuration.targetSampleRateHz,
maximumInterpolationGapSeconds: configuration.maximumInterpolationGapSeconds
)
}
private func normalizedBlend(
red: [Double],
green: [Double],
blue: [Double],
luminance: [Double]
) -> [Double] {
let standardizedRed = robustStandardize(red)
let standardizedGreen = robustStandardize(green)
let standardizedBlue = robustStandardize(blue)
let standardizedLuminance = robustStandardize(luminance)
return standardizedRed.indices.map { index in
// A compact chrominance projection suppresses common illumination and
// automatic-exposure changes while preserving blood-volume modulation.
0.70 * standardizedGreen[index]
- 0.40 * standardizedRed[index]
- 0.30 * standardizedBlue[index]
+ 0.10 * standardizedLuminance[index]
}
}
private func robustStandardize(_ values: [Double]) -> [Double] {
let center = SignalStatistics.median(values)
let scale = max(
1.4826 * SignalStatistics.mad(values, around: center),
SignalStatistics.standardDeviation(values) * 0.20,
1e-9
)
return values.map { min(max(($0 - center) / scale, -12), 12) }
}
private func channelCandidateIsWorse(_ lhs: ChannelCandidate, _ rhs: ChannelCandidate) -> Bool {
if abs(lhs.selectionScore - rhs.selectionScore) > 1e-12 {
return lhs.selectionScore < rhs.selectionScore
}
let lhsIndex = PPGSignalChannel.allCases.firstIndex(of: lhs.channel) ?? 0
let rhsIndex = PPGSignalChannel.allCases.firstIndex(of: rhs.channel) ?? 0
// Prefer the earlier, simpler channel when scores are exactly tied.
return lhsIndex > rhsIndex
}
}
@@ -0,0 +1,176 @@
import Foundation
public struct SignalQualityInput: Equatable, Sendable {
public var timebase: TimebaseDiagnostics
public var contactConfidence: [Double]
public var saturationRatio: [Double]
public var motionArtifactScore: [Double]
public var filteredSignal: [Double]
public var validityMask: [Bool]
public var bpmEstimate: BPMEstimate
public var intervals: [PulseInterval]
public var signalToNoiseRatioDB: Double?
public var channelStabilityScore: Double
public init(
timebase: TimebaseDiagnostics,
contactConfidence: [Double],
saturationRatio: [Double],
motionArtifactScore: [Double],
filteredSignal: [Double],
validityMask: [Bool],
bpmEstimate: BPMEstimate,
intervals: [PulseInterval],
signalToNoiseRatioDB: Double?,
channelStabilityScore: Double
) {
self.timebase = timebase
self.contactConfidence = contactConfidence
self.saturationRatio = saturationRatio
self.motionArtifactScore = motionArtifactScore
self.filteredSignal = filteredSignal
self.validityMask = validityMask
self.bpmEstimate = bpmEstimate
self.intervals = intervals
self.signalToNoiseRatioDB = signalToNoiseRatioDB
self.channelStabilityScore = SignalStatistics.clamp01(channelStabilityScore)
}
}
public enum PPGSignalQualityEvaluator {
public static func evaluate(
_ input: SignalQualityInput,
configuration: PPGProcessingConfiguration
) -> SignalQualityResult {
let totalCount = max(input.validityMask.count, 1)
let validCoverage = Double(input.validityMask.filter { $0 }.count) / Double(totalCount)
let contactMean = boundedMean(input.contactConfidence)
let saturationMean = boundedMean(input.saturationRatio)
let motionMean = boundedMean(input.motionArtifactScore)
let motionFrameRatio = input.motionArtifactScore.isEmpty
? 0
: Double(input.motionArtifactScore.filter { $0 >= 0.50 }.count)
/ Double(input.motionArtifactScore.count)
let motionArtifactRatio = max(motionMean, motionFrameRatio)
let validSignal = input.filteredSignal.indices.compactMap { index -> Double? in
guard index < input.validityMask.count,
input.validityMask[index],
input.filteredSignal[index].isFinite
else { return nil }
return input.filteredSignal[index]
}
let pulsatileAmplitude = validSignal.isEmpty
? 0
: SignalStatistics.percentile(validSignal, 0.95)
- SignalStatistics.percentile(validSignal, 0.05)
let amplitudeScore = SignalStatistics.clamp01(pulsatileAmplitude / 2.0)
let snrScore = input.signalToNoiseRatioDB.map {
SignalStatistics.clamp01(($0 + 3) / 15)
} ?? 0
let sampleInterval = input.timebase.estimatedSampleRateHz > 0
? 1 / input.timebase.estimatedSampleRateHz
: 0
let jitterRatio = sampleInterval > 0
? input.timebase.frameIntervalMADSeconds / sampleInterval
: 1
let droppedFrameScore = SignalStatistics.clamp01(1 - input.timebase.droppedFrameRatio / 0.15)
let jitterScore = SignalStatistics.clamp01(1 - jitterRatio / 0.15)
let gapScore = input.timebase.maximumGapSeconds <= configuration.maximumInterpolationGapSeconds
? 1.0
: SignalStatistics.clamp01(
1 - input.timebase.longGapDurationSeconds / max(input.timebase.durationSeconds, 1e-9)
)
let timebaseScore = 0.45 * droppedFrameScore + 0.35 * jitterScore + 0.20 * gapScore
let durationScore = SignalStatistics.clamp01(
input.timebase.usableDurationSeconds / configuration.minimumAnalysisDurationSeconds
)
let contactScore = SignalStatistics.clamp01(contactMean)
let saturationScore = SignalStatistics.clamp01(1 - saturationMean / 0.70)
let motionScore = SignalStatistics.clamp01(1 - motionArtifactRatio / 0.75)
let peakScore = input.bpmEstimate.confidence
let agreementScore = input.bpmEstimate.methodAgreementBpm.map {
SignalStatistics.clamp01(1 - $0 / max(configuration.maximumMethodDisagreementBPM, 1e-9))
} ?? 0
let rejectedIntervalRatio = input.intervals.isEmpty
? 1
: Double(input.intervals.filter { !$0.isAccepted }.count) / Double(input.intervals.count)
let intervalScore = SignalStatistics.clamp01(1 - rejectedIntervalRatio / 0.35)
let score = 0.03 * contactScore
+ 0.05 * saturationScore
+ 0.06 * motionScore
+ 0.14 * timebaseScore
+ 0.12 * durationScore
+ 0.08 * amplitudeScore
+ 0.20 * snrScore
+ 0.14 * peakScore
+ 0.09 * agreementScore
+ 0.05 * intervalScore
+ 0.04 * input.channelStabilityScore
var issues: [SignalQualityIssue] = []
if input.timebase.usableDurationSeconds < configuration.minimumAnalysisDurationSeconds {
issues.append(.insufficientDuration)
}
if input.timebase.outOfOrderTimestampCount > 0 || input.timebase.duplicateTimestampCount > 0 {
issues.append(.invalidTimestamps)
}
if input.timebase.maximumGapSeconds > configuration.longGapLimitSeconds {
issues.append(.longInterruption)
}
if input.timebase.droppedFrameRatio > 0.12 { issues.append(.tooManyDroppedFrames) }
if jitterRatio > 0.12 { issues.append(.irregularTimestamps) }
if contactMean < 0.35 || validCoverage < 0.75 { issues.append(.poorFingerContact) }
if saturationMean > 0.60 { issues.append(.excessiveSaturation) }
if motionArtifactRatio > 0.55 { issues.append(.motionArtifacts) }
if pulsatileAmplitude < 0.75 { issues.append(.lowPulsatileAmplitude) }
if (input.signalToNoiseRatioDB ?? -.infinity) < 3 { issues.append(.lowSignalToNoise) }
if input.bpmEstimate.validPeakCount < 8 { issues.append(.tooFewPeaks) }
if input.bpmEstimate.bpm == nil { issues.append(.inconsistentBPMMethods) }
if rejectedIntervalRatio > 0.25 { issues.append(.tooManyRejectedIntervals) }
if input.channelStabilityScore < 0.45 { issues.append(.unstableChannel) }
if input.timebase.discardedSampleCount > 0 { issues.append(.nonFiniteInput) }
let hardInvalid = input.bpmEstimate.bpm == nil
|| input.signalToNoiseRatioDB == nil
|| (input.signalToNoiseRatioDB ?? -.infinity) < 0
|| input.bpmEstimate.confidence < PPGPublicationThresholds.bpmConfidence
|| input.timebase.maximumGapSeconds > configuration.longGapLimitSeconds
|| validCoverage < 0.55
|| saturationMean > 0.92
|| motionArtifactRatio > 0.90
|| input.timebase.usableDurationSeconds < configuration.minimumAnalysisDurationSeconds * 0.80
let clampedScore = SignalStatistics.clamp01(score)
let level: SignalQualityLevel
if hardInvalid || clampedScore < PPGPublicationThresholds.bpmQualityScore {
level = .invalid
} else if clampedScore < configuration.minimumQualityScore {
level = .poor
} else if clampedScore < PPGPublicationThresholds.prvQualityScore {
level = .medium
} else {
level = .good
}
return SignalQualityResult(
score: clampedScore,
level: level,
usableDurationSeconds: input.timebase.usableDurationSeconds,
droppedFrameRatio: input.timebase.droppedFrameRatio,
saturationRatio: saturationMean,
rejectedIntervalRatio: rejectedIntervalRatio,
signalToNoiseRatio: input.signalToNoiseRatioDB,
motionArtifactRatio: motionArtifactRatio,
methodAgreementScore: agreementScore,
issues: issues
)
}
private static func boundedMean(_ values: [Double]) -> Double {
let finite = values.filter(\.isFinite).map { min(max($0, 0), 1) }
return finite.isEmpty ? 0 : SignalStatistics.mean(finite)
}
}
@@ -0,0 +1,196 @@
import Foundation
enum SignalStatistics {
static func mean(_ values: [Double]) -> Double {
guard !values.isEmpty else { return 0 }
return values.reduce(0, +) / Double(values.count)
}
static func median(_ values: [Double]) -> Double {
guard !values.isEmpty else { return 0 }
let sorted = values.sorted()
let middle = sorted.count / 2
if sorted.count.isMultiple(of: 2) {
return (sorted[middle - 1] + sorted[middle]) / 2
}
return sorted[middle]
}
static func mad(_ values: [Double], around center: Double? = nil) -> Double {
guard !values.isEmpty else { return 0 }
let center = center ?? median(values)
return median(values.map { abs($0 - center) })
}
static func standardDeviation(_ values: [Double], sample: Bool = true) -> Double {
guard values.count > (sample ? 1 : 0) else { return 0 }
let average = mean(values)
let sumSquares = values.reduce(0) { partial, value in
let delta = value - average
return partial + delta * delta
}
let divisor = Double(values.count - (sample ? 1 : 0))
return (sumSquares / divisor).squareRoot()
}
static func rootMeanSquare(_ values: [Double]) -> Double {
guard !values.isEmpty else { return 0 }
let squareMean = values.reduce(0) { $0 + $1 * $1 } / Double(values.count)
return squareMean.squareRoot()
}
static func percentile(_ values: [Double], _ probability: Double) -> Double {
guard !values.isEmpty else { return 0 }
let sorted = values.sorted()
let p = min(max(probability, 0), 1)
let position = p * Double(sorted.count - 1)
let lower = Int(position.rounded(.down))
let upper = Int(position.rounded(.up))
guard lower != upper else { return sorted[lower] }
let fraction = position - Double(lower)
return sorted[lower] + fraction * (sorted[upper] - sorted[lower])
}
static func clamp01(_ value: Double) -> Double {
guard value.isFinite else { return 0 }
return min(max(value, 0), 1)
}
static func reflectedIndex(_ index: Int, count: Int) -> Int {
guard count > 1 else { return 0 }
var candidate = index
while candidate < 0 || candidate >= count {
if candidate < 0 {
candidate = -candidate - 1
}
if candidate >= count {
candidate = 2 * count - candidate - 1
}
}
return candidate
}
/// Fits a sinusoid at `frequencyHz` and reports its power relative to residual power in dB.
static func sinusoidalSNRDB(
values: [Double],
timestamps: [Double],
validityMask: [Bool],
frequencyHz: Double
) -> Double? {
guard values.count == timestamps.count,
values.count == validityMask.count,
frequencyHz.isFinite,
frequencyHz > 0
else { return nil }
let validIndices = values.indices.filter {
validityMask[$0] && values[$0].isFinite && timestamps[$0].isFinite
}
guard validIndices.count >= 12 else { return nil }
let average = validIndices.reduce(0) { $0 + values[$1] } / Double(validIndices.count)
var sinSin = 0.0
var cosCos = 0.0
var sinCos = 0.0
var valueSin = 0.0
var valueCos = 0.0
let origin = timestamps[validIndices[0]]
for index in validIndices {
let angle = 2 * Double.pi * frequencyHz * (timestamps[index] - origin)
let sine = sin(angle)
let cosine = cos(angle)
let value = values[index] - average
sinSin += sine * sine
cosCos += cosine * cosine
sinCos += sine * cosine
valueSin += value * sine
valueCos += value * cosine
}
let determinant = sinSin * cosCos - sinCos * sinCos
guard abs(determinant) > 1e-12 else { return nil }
let sineCoefficient = (valueSin * cosCos - valueCos * sinCos) / determinant
let cosineCoefficient = (valueCos * sinSin - valueSin * sinCos) / determinant
var modeledPower = 0.0
var residualPower = 0.0
for index in validIndices {
let angle = 2 * Double.pi * frequencyHz * (timestamps[index] - origin)
let modeled = sineCoefficient * sin(angle) + cosineCoefficient * cos(angle)
let value = values[index] - average
modeledPower += modeled * modeled
let residual = value - modeled
residualPower += residual * residual
}
guard modeledPower > 1e-12 else { return -60 }
let ratio = modeledPower / max(residualPower, 1e-12)
return 10 * log10(ratio)
}
/// Compares average spectral power around the pulse fundamental (and its second harmonic)
/// with the remaining physiological band. The finite bandwidth tolerates slow BPM changes.
static func periodicBandSNRDB(
values: [Double],
timestamps: [Double],
validityMask: [Bool],
fundamentalFrequencyHz: Double,
minimumFrequencyHz: Double,
maximumFrequencyHz: Double
) -> Double? {
guard values.count == timestamps.count,
values.count == validityMask.count,
values.count >= 30,
fundamentalFrequencyHz.isFinite,
fundamentalFrequencyHz > 0,
minimumFrequencyHz > 0,
maximumFrequencyHz > minimumFrequencyHz
else { return nil }
let validIndices = values.indices.filter {
validityMask[$0] && values[$0].isFinite && timestamps[$0].isFinite
}
guard validIndices.count >= 30 else { return nil }
let origin = timestamps[validIndices[0]]
let duration = timestamps[validIndices.last!] - origin
guard duration > 2 else { return nil }
let average = validIndices.reduce(0) { $0 + values[$1] } / Double(validIndices.count)
let frequencyStep = max(1 / duration, 0.02)
let fundamentalHalfWidth = max(0.12, min(0.28, fundamentalFrequencyHz * 0.18))
let secondHarmonic = 2 * fundamentalFrequencyHz
let harmonicHalfWidth = min(0.36, 1.5 * fundamentalHalfWidth)
var signalPowers: [Double] = []
var noisePowers: [Double] = []
var frequency = minimumFrequencyHz
while frequency <= maximumFrequencyHz + frequencyStep * 0.5 {
var real = 0.0
var imaginary = 0.0
for index in validIndices {
let relativeTime = timestamps[index] - origin
let window = 0.5 - 0.5 * cos(2 * Double.pi * relativeTime / duration)
let centeredValue = (values[index] - average) * window
let angle = -2 * Double.pi * frequency * relativeTime
real += centeredValue * cos(angle)
imaginary += centeredValue * sin(angle)
}
let power = real * real + imaginary * imaginary
let nearFundamental = abs(frequency - fundamentalFrequencyHz) <= fundamentalHalfWidth
let nearSecondHarmonic = secondHarmonic <= maximumFrequencyHz
&& abs(frequency - secondHarmonic) <= harmonicHalfWidth
if nearFundamental || nearSecondHarmonic {
signalPowers.append(power)
} else {
noisePowers.append(power)
}
frequency += frequencyStep
}
guard !signalPowers.isEmpty, !noisePowers.isEmpty else { return nil }
let signalPower = mean(signalPowers)
let noisePower = mean(noisePowers)
guard signalPower > 1e-12 else { return -60 }
return 10 * log10(signalPower / max(noisePower, 1e-12))
}
}
@@ -0,0 +1,65 @@
import Foundation
import React
/// Thin React Native facade over the central WCSession owner. The PPG and
/// Core Haptics objects remain Nitro modules; connectivity is intentionally a
/// process-wide service and therefore must not be instantiated per HybridObject.
@objc(HaptikaWatchConnectivity)
final class HaptikaWatchConnectivityModule: NSObject {
@objc static func requiresMainQueueSetup() -> Bool { false }
@objc func start() {
HaptikaWatchConnectivityService.shared.activate()
}
@objc func getConnectionState(
_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
let state = HaptikaWatchConnectivityService.shared.state()
resolve([
"supported": state.supported,
"paired": state.paired,
"watchAppInstalled": state.installed,
"reachable": state.reachable,
"activationState": state.activationState,
"latestEventId": state.latestEventId ?? NSNull(),
"latestEventAt": state.latestEventAt ?? NSNull()
])
}
@objc func drainEvents(
_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
resolve(HaptikaWatchConnectivityService.shared.drainEvents())
}
@objc func acknowledgeEvents(_ eventIds: [String]) {
HaptikaWatchConnectivityService.shared.acknowledge(eventIds: eventIds)
}
@objc func sendPreferences(
_ json: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
resolve(HaptikaWatchConnectivityService.shared.sendPreferences(json: json))
}
@objc func sendCommand(
_ json: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
resolve(HaptikaWatchConnectivityService.shared.sendCommand(json: json))
}
@objc func transferEvent(
_ json: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
resolve(HaptikaWatchConnectivityService.shared.transferEvent(json: json))
}
}
@@ -0,0 +1,21 @@
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(HaptikaWatchConnectivity, NSObject)
RCT_EXTERN_METHOD(start)
RCT_EXTERN_METHOD(getConnectionState:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(drainEvents:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(acknowledgeEvents:(NSArray<NSString *> *)eventIds)
RCT_EXTERN_METHOD(sendPreferences:(NSString *)json
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sendCommand:(NSString *)json
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(transferEvent:(NSString *)json
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@end
@@ -0,0 +1,490 @@
import Foundation
import WatchConnectivity
/// The only owner of `WCSession` in the iPhone process.
///
/// Durable Watch envelopes are persisted before being exposed to JavaScript.
/// JavaScript explicitly acknowledges imported event IDs, after which the
/// service removes them and sends an acknowledgement to the paired Watch.
final class HaptikaWatchConnectivityService: NSObject, WCSessionDelegate {
static let shared = HaptikaWatchConnectivityService()
struct ConnectionState {
let supported: Bool
let paired: Bool
let installed: Bool
let reachable: Bool
let activationState: String
let latestEventId: String?
let latestEventAt: String?
}
struct LiveHeartRate {
let bpm: Double
let timestampSeconds: Double
}
private static let inboxKey = "HaptikaWatchConnectivity.inbox.v1"
private let lock = NSLock()
private let defaults = UserDefaults.standard
private var memoryLiveEvents: [String] = []
private var latestEventIdValue: String?
private var latestEventAtValue: String?
private var latestHeartRateValue: LiveHeartRate?
private override init() {
super.init()
}
func activate() {
guard WCSession.isSupported() else { return }
let session = WCSession.default
if session.delegate !== self {
session.delegate = self
}
if session.activationState != .activated {
session.activate()
}
}
func state() -> ConnectionState {
guard WCSession.isSupported() else {
return ConnectionState(supported: false, paired: false, installed: false, reachable: false, activationState: "notActivated", latestEventId: nil, latestEventAt: nil)
}
let session = WCSession.default
lock.lock()
let eventId = latestEventIdValue
let eventAt = latestEventAtValue
lock.unlock()
let activation: String
switch session.activationState {
case .activated: activation = "activated"
case .inactive: activation = "inactive"
case .notActivated: activation = "notActivated"
@unknown default: activation = "notActivated"
}
return ConnectionState(
supported: true,
paired: session.isPaired,
installed: session.isWatchAppInstalled,
reachable: session.isReachable,
activationState: activation,
latestEventId: eventId,
latestEventAt: eventAt
)
}
func latestHeartRate() -> LiveHeartRate? {
lock.lock()
defer { lock.unlock() }
return latestHeartRateValue
}
func drainEvents() -> [String] {
lock.lock()
defer { lock.unlock() }
let durable = defaults.stringArray(forKey: Self.inboxKey) ?? []
return durable + memoryLiveEvents
}
func acknowledge(eventIds: [String]) {
guard !eventIds.isEmpty else { return }
let identifiers = Set(eventIds)
lock.lock()
let durable = defaults.stringArray(forKey: Self.inboxKey) ?? []
defaults.set(durable.filter { json in
guard let eventId = Self.eventId(in: json) else { return false }
return !identifiers.contains(eventId)
}, forKey: Self.inboxKey)
memoryLiveEvents.removeAll { json in
guard let eventId = Self.eventId(in: json) else { return true }
return identifiers.contains(eventId)
}
lock.unlock()
sendAcknowledgement(eventIds: eventIds)
}
func sendPreferences(json: String) -> Bool {
guard let dictionary = Self.dictionary(from: json), WCSession.isSupported() else { return false }
let rawPayload = (dictionary["payload"] as? [String: Any]) ?? dictionary
let save = (rawPayload["saveMindAndBodyWorkouts"] as? Bool) ?? true
let catalogVersion = (rawPayload["protocolCatalogVersion"] as? String) ?? "1.0.0"
let preferences: [String: Any] = [
"schemaVersion": 1,
"saveMindAndBodyWorkouts": save,
"protocolCatalogVersion": catalogVersion,
"updatedAt": (rawPayload["updatedAt"] as? String) ?? Self.iso8601.string(from: Date())
]
guard let data = try? JSONSerialization.data(withJSONObject: preferences, options: [.sortedKeys]) else { return false }
activate()
do {
try WCSession.default.updateApplicationContext(["preferences": data])
return true
} catch {
return false
}
}
func sendCommand(json: String) -> Bool {
guard let dictionary = Self.dictionary(from: json), WCSession.isSupported() else { return false }
activate()
guard WCSession.default.isReachable else { return false }
let payload = (dictionary["payload"] as? [String: Any]) ?? dictionary
guard let envelope = Self.watchWireEnvelope(kind: "mirrorCommand", payload: payload),
let encoded = try? JSONSerialization.data(withJSONObject: envelope, options: [.sortedKeys]) else { return false }
WCSession.default.sendMessageData(encoded, replyHandler: nil, errorHandler: nil)
return true
}
func transferEvent(json: String) -> Bool {
guard let dictionary = Self.dictionary(from: json), WCSession.isSupported(),
let kind = dictionary["kind"] as? String,
let payloadObject = dictionary["payload"],
JSONSerialization.isValidJSONObject(payloadObject),
let envelope = Self.watchWireEnvelope(kind: kind, payload: payloadObject),
let encoded = try? JSONSerialization.data(withJSONObject: envelope, options: [.sortedKeys]) else { return false }
activate()
WCSession.default.transferUserInfo(["envelope": encoded])
return true
}
private func receive(_ dictionary: [String: Any], durable: Bool) {
guard let normalized = normalize(dictionary),
let data = try? JSONSerialization.data(withJSONObject: normalized, options: [.sortedKeys]),
let json = String(data: data, encoding: .utf8) else { return }
let kind = normalized["kind"] as? String
let eventId = normalized["eventId"] as? String
let sentAt = normalized["sentAt"] as? String
lock.lock()
latestEventIdValue = eventId
latestEventAtValue = sentAt
if kind == "liveHeartRate", let payload = normalized["payload"] as? [String: Any],
let bpm = Self.double(payload["bpm"]), bpm >= 35, bpm <= 220,
let timestamp = Self.double(payload["timestampSeconds"]), timestamp.isFinite {
latestHeartRateValue = LiveHeartRate(bpm: bpm, timestampSeconds: timestamp)
}
if durable {
var inbox = defaults.stringArray(forKey: Self.inboxKey) ?? []
if eventId == nil || !inbox.contains(where: { Self.eventId(in: $0) == eventId }) {
inbox.append(json)
defaults.set(Array(inbox.suffix(500)), forKey: Self.inboxKey)
}
} else {
memoryLiveEvents.append(json)
if memoryLiveEvents.count > 60 { memoryLiveEvents.removeFirst(memoryLiveEvents.count - 60) }
}
lock.unlock()
}
private func normalize(_ dictionary: [String: Any]) -> [String: Any]? {
if dictionary["kind"] != nil,
dictionary["eventId"] != nil || dictionary["eventID"] != nil {
return normalizeEnvelopeDictionary(dictionary)
}
// Compatibility with the early prototype. It remains a live HR sample and
// is never treated as a biological heartbeat.
guard let bpm = Self.double(dictionary["bpm"]), bpm >= 35, bpm <= 220,
let timestamp = Self.double(dictionary["timestampSeconds"]) else { return nil }
return [
"schemaVersion": 1,
"eventId": "legacy-live-\(UUID().uuidString)",
"deviceId": "legacy-watch",
"sequence": 0,
"sentAt": Self.iso8601.string(from: Date()),
"kind": "liveHeartRate",
"payload": ["bpm": bpm, "timestampSeconds": timestamp]
]
}
private func normalizeEnvelopeDictionary(_ dictionary: [String: Any]) -> [String: Any]? {
let eventId = (dictionary["eventId"] ?? dictionary["eventID"]) as? String
let deviceId = (dictionary["deviceId"] ?? dictionary["deviceID"]) as? String
guard let eventId, let deviceId, let kindValue = dictionary["kind"] as? String else { return nil }
let kind = kindValue == "acknowledgement" ? "ack" : kindValue
let sentAt = (dictionary["sentAt"] as? String) ?? Self.iso8601.string(from: Date())
let sequence = Self.double(dictionary["sequence"]) ?? 0
let payloadObject: Any
if let payload = dictionary["payload"] as? [String: Any] {
payloadObject = payload
} else if let base64 = dictionary["payload"] as? String,
let data = Data(base64Encoded: base64),
let decoded = try? JSONSerialization.jsonObject(with: data) {
payloadObject = decoded
} else if let data = dictionary["payload"] as? Data,
let decoded = try? JSONSerialization.jsonObject(with: data) {
payloadObject = decoded
} else {
payloadObject = NSNull()
}
let normalizedPayload = normalizePayload(payloadObject, kind: kind, deviceId: deviceId, sentAt: sentAt)
return [
"schemaVersion": 1,
"eventId": eventId,
"deviceId": deviceId,
"sequence": sequence,
"sentAt": sentAt,
"kind": kind,
"payload": normalizedPayload
]
}
private func normalizePayload(_ payload: Any, kind: String, deviceId: String, sentAt: String) -> Any {
guard let value = payload as? [String: Any] else { return payload }
// Current watchOS builds encode the exact cross-platform V1 DTO. Do not
// reinterpret canonical fields such as `coverage`, `currentBpm` or the
// compact sample offsets.
if Self.double(value["schemaVersion"]) == 1 {
if kind == "measurementUpsert" {
var canonical = value
for key in ["currentBpm", "averageBpm", "minimumBpm", "maximumBpm", "invalidReason", "feeling"] where canonical[key] == nil {
canonical[key] = NSNull()
}
if var device = canonical["device"] as? [String: Any] {
if device["model"] == nil { device["model"] = NSNull() }
if device["systemVersion"] == nil { device["systemVersion"] = NSNull() }
canonical["device"] = device
}
if var context = canonical["context"] as? [String: Any] {
if context["posture"] == nil { context["posture"] = NSNull() }
for key in ["recentExercise", "recentCaffeine", "recentMeal"] where context[key] == nil {
context[key] = NSNull()
}
canonical["context"] = context
}
return canonical
}
if kind == "regulationSessionUpsert" {
var canonical = value
for key in ["beforeMeasurementId", "afterMeasurementId", "endedAt", "workoutUUID", "feelingBefore", "feelingAfter"] where canonical[key] == nil {
canonical[key] = NSNull()
}
return canonical
}
if ["tombstone", "preferences"].contains(kind) { return value }
}
switch kind {
case "measurementUpsert":
guard let id = value["id"] as? String,
let startedAt = value["startedAt"] as? String,
let endedAt = value["endedAt"] as? String else { return value }
let updatedAt = (value["updatedAt"] as? String) ?? sentAt
let startedSeconds = Self.timestamp(startedAt) ?? 0
let duration = max(1, (Self.timestamp(endedAt) ?? startedSeconds + 60) - startedSeconds)
let coverageSeconds = Self.double(value["coverageSeconds"]) ?? 0
let rawSamples = value["samples"] as? [[String: Any]] ?? []
let samples: [[String: Any]] = rawSamples.compactMap { sample in
guard let bpm = Self.double(sample["bpm"]),
let date = sample["date"] as? String,
let sampleTimestamp = Self.timestamp(date) else { return nil }
return ["offsetSeconds": max(0, sampleTimestamp - startedSeconds), "bpm": bpm]
}
let context = value["context"] as? [String: Any] ?? [:]
let postureValue = context["posture"] as? String
let posture: Any = ["seated", "lying", "standing"].contains(postureValue ?? "")
? Self.jsonString(postureValue)
: NSNull()
let reasons = value["invalidReasons"] as? [String] ?? []
let feeling: Any
if let score = Self.double(value["feeling"]), (1 ... 5).contains(score) {
feeling = ["score": Int(score), "recordedAt": updatedAt]
} else {
feeling = NSNull()
}
let devicePayload: [String: Any] = [
"id": deviceId,
"name": (value["deviceName"] as? String) ?? "Apple Watch",
"model": Self.jsonString(value["deviceModel"]),
"systemVersion": NSNull()
]
let contextPayload: [String: Any] = [
"atRest": (context["isAtRest"] as? Bool) ?? true,
"posture": posture,
"recentExercise": (context["recentExercise"] as? Bool) ?? false,
"recentCaffeine": (context["recentCaffeine"] as? Bool) ?? false,
"recentMeal": (context["recentMeal"] as? Bool) ?? false
]
let normalizedCoverage = min(1.0, max(0.0, coverageSeconds / duration))
let normalizedQuality = min(1.0, max(0.0, Self.double(value["quality"]) ?? 0.0))
let invalidReason: Any = reasons.isEmpty ? NSNull() : reasons.joined(separator: ",")
var normalized: [String: Any] = [:]
normalized["schemaVersion"] = 1
normalized["id"] = id
normalized["updatedAt"] = updatedAt
normalized["startedAt"] = startedAt
normalized["endedAt"] = endedAt
normalized["source"] = "appleWatchOptical"
normalized["device"] = devicePayload
normalized["currentBpm"] = Self.jsonNumber(value["currentBPM"])
normalized["averageBpm"] = Self.jsonNumber(value["averageBPM"])
normalized["minimumBpm"] = Self.jsonNumber(value["minimumBPM"])
normalized["maximumBpm"] = Self.jsonNumber(value["maximumBPM"])
normalized["samples"] = samples
normalized["coverage"] = normalizedCoverage
normalized["quality"] = normalizedQuality
normalized["valid"] = (value["isValid"] as? Bool) ?? false
normalized["invalidReason"] = invalidReason
normalized["context"] = contextPayload
normalized["feeling"] = feeling
return normalized
case "regulationSessionUpsert":
let sourceState = value["state"] as? String
let normalizedState = sourceState == "interrupted" ? "cancelled" : (sourceState ?? "cancelled")
var normalized: [String: Any] = [:]
normalized["schemaVersion"] = 1
normalized["id"] = value["id"] as? String ?? ""
normalized["updatedAt"] = value["updatedAt"] as? String ?? sentAt
normalized["protocolId"] = value["protocolID"] as? String ?? ""
normalized["beforeMeasurementId"] = value["beforeMeasurementID"] ?? NSNull()
normalized["afterMeasurementId"] = value["afterMeasurementID"] ?? NSNull()
normalized["startedAt"] = value["startedAt"] as? String ?? sentAt
normalized["endedAt"] = value["endedAt"] ?? NSNull()
normalized["durationSeconds"] = Self.double(value["durationSeconds"]) ?? 0.0
normalized["state"] = normalizedState
normalized["workoutUUID"] = value["healthKitWorkoutID"] ?? NSNull()
normalized["feelingBefore"] = value["feelingBefore"] ?? NSNull()
normalized["feelingAfter"] = value["feelingAfter"] ?? NSNull()
return normalized
case "tombstone":
let deletedAt = value["deletedAt"] as? String ?? sentAt
return [
"schemaVersion": 1,
"entityId": (value["entityId"] ?? value["entityID"]) as? String ?? "",
"entityKind": value["entityKind"] as? String ?? "measurement",
"deletedAt": deletedAt,
"updatedAt": deletedAt
]
case "liveHeartRate":
let sampledAt = value["sampledAt"] as? String
return [
"bpm": Self.double(value["bpm"]) ?? 0,
"timestampSeconds": sampledAt.flatMap(Self.timestamp) ?? Date().timeIntervalSince1970,
"measurementId": value["measurementID"] ?? NSNull()
]
default:
return value
}
}
private func sendAcknowledgement(eventIds: [String]) {
guard WCSession.isSupported() else { return }
activate()
let validEventIds = eventIds.compactMap(UUID.init(uuidString:)).map(\.uuidString)
guard !validEventIds.isEmpty else { return }
let payload: [String: Any] = ["eventIDs": validEventIds, "receivedAt": Self.iso8601.string(from: Date())]
guard let envelope = Self.watchWireEnvelope(kind: "ack", payload: payload),
let envelopeData = try? JSONSerialization.data(withJSONObject: envelope, options: [.sortedKeys]) else { return }
if WCSession.default.isReachable {
WCSession.default.sendMessageData(envelopeData, replyHandler: nil) { _ in
WCSession.default.transferUserInfo(["envelope": envelopeData])
}
} else {
WCSession.default.transferUserInfo(["envelope": envelopeData])
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) { session.activate() }
func sessionReachabilityDidChange(_ session: WCSession) {}
func sessionWatchStateDidChange(_ session: WCSession) {}
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
receive(message, durable: (message["kind"] as? String) != "liveHeartRate")
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
receiveEnvelopeData(messageData, durable: false)
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data, replyHandler: @escaping (Data) -> Void) {
receiveEnvelopeData(messageData, durable: false)
replyHandler(Data())
}
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
receive(message, durable: (message["kind"] as? String) != "liveHeartRate")
replyHandler(["accepted": true])
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
if let envelope = userInfo["envelope"] as? Data {
receiveEnvelopeData(envelope, durable: true)
} else {
receive(userInfo, durable: true)
}
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
receive(applicationContext, durable: true)
}
private static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
private static func dictionary(from json: String) -> [String: Any]? {
guard let data = json.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data),
let dictionary = object as? [String: Any],
JSONSerialization.isValidJSONObject(dictionary) else { return nil }
return dictionary
}
private static func eventId(in json: String) -> String? {
return dictionary(from: json)?["eventId"] as? String
}
private static func double(_ value: Any?) -> Double? {
if let number = value as? NSNumber { return number.doubleValue }
return value as? Double
}
private func receiveEnvelopeData(_ data: Data, durable: Bool) {
guard let object = try? JSONSerialization.jsonObject(with: data),
let dictionary = object as? [String: Any] else { return }
receive(dictionary, durable: durable || (dictionary["kind"] as? String) != "liveHeartRate")
}
private static func timestamp(_ value: String) -> TimeInterval? {
if let date = iso8601.date(from: value) { return date.timeIntervalSince1970 }
let fallback = ISO8601DateFormatter()
fallback.formatOptions = [.withInternetDateTime]
return fallback.date(from: value)?.timeIntervalSince1970
}
private static func jsonNumber(_ value: Any?) -> Any {
return double(value).map { NSNumber(value: $0) } ?? NSNull()
}
private static func jsonString(_ value: Any?) -> Any {
return (value as? String).map { $0 as Any } ?? NSNull()
}
private static func watchWireEnvelope(kind: String, payload: Any) -> [String: Any]? {
guard JSONSerialization.isValidJSONObject(payload) else { return nil }
return [
"schemaVersion": 1,
"eventId": UUID().uuidString,
"deviceId": UIDeviceIdentifier.current,
"sequence": 0,
"sentAt": iso8601.string(from: Date()),
"kind": kind,
"payload": payload
]
}
}
private enum UIDeviceIdentifier {
static let current: String = {
let key = "HaptikaWatchConnectivity.deviceId.v1"
if let existing = UserDefaults.standard.string(forKey: key) { return existing }
let generated = "iphone-\(UUID().uuidString)"
UserDefaults.standard.set(generated, forKey: key)
return generated
}()
}
@@ -0,0 +1,26 @@
{
"$schema": "https://nitro.margelo.com/nitro.schema.json",
"cxxNamespace": ["haptika", "ppg"],
"ios": {
"iosModuleName": "HaptikaPPG"
},
"android": {
"androidNamespace": ["haptika", "ppg"],
"androidCxxLibName": "HaptikaPPG"
},
"autolinking": {
"HaptikaPPGFrameProcessor": {
"ios": {
"language": "swift",
"implementationClassName": "HybridHaptikaPPGFrameProcessor"
}
},
"HaptikaHealthKit": {
"ios": {
"language": "swift",
"implementationClassName": "HybridHaptikaHealthKit"
}
}
},
"ignorePaths": ["**/node_modules"]
}
@@ -0,0 +1 @@
** linguist-generated=true
@@ -0,0 +1,62 @@
#
# HaptikaPPG+autolinking.rb
# This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
# https://github.com/mrousavy/nitro
# Copyright © Marc Rousavy @ Margelo
#
# This is a Ruby script that adds all files generated by Nitrogen
# to the given podspec.
#
# To use it, add this to your .podspec:
# ```ruby
# Pod::Spec.new do |spec|
# # ...
#
# # Add all files generated by Nitrogen
# load 'nitrogen/generated/ios/HaptikaPPG+autolinking.rb'
# add_nitrogen_files(spec)
# end
# ```
def add_nitrogen_files(spec)
Pod::UI.puts "[NitroModules] 🔥 HaptikaPPG is boosted by nitro!"
spec.dependency "NitroModules"
current_source_files = Array(spec.attributes_hash['source_files'])
spec.source_files = current_source_files + [
# Generated cross-platform specs
"nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}",
# Generated bridges for the cross-platform specs
"nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}",
]
current_public_header_files = Array(spec.attributes_hash['public_header_files'])
spec.public_header_files = current_public_header_files + [
# Generated specs
"nitrogen/generated/shared/**/*.{h,hpp}",
# Swift to C++ bridging helpers
"nitrogen/generated/ios/HaptikaPPG-Swift-Cxx-Bridge.hpp"
]
current_private_header_files = Array(spec.attributes_hash['private_header_files'])
spec.private_header_files = current_private_header_files + [
# iOS specific specs
"nitrogen/generated/ios/c++/**/*.{h,hpp}",
# Views are framework-specific and should be private
"nitrogen/generated/shared/**/views/**/*"
]
current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {}
spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({
# Use C++ 20
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
# Enables C++ <-> Swift interop (by default it's only ObjC)
"SWIFT_OBJC_INTEROP_MODE" => "objcxx",
# Enables stricter modular headers
"DEFINES_MODULE" => "YES",
# Disable auto-generated ObjC header for Swift (Static linkage on Xcode 26.4 breaks here)
"SWIFT_INSTALL_OBJC_HEADER" => "NO",
})
end
@@ -0,0 +1,110 @@
///
/// HaptikaPPG-Swift-Cxx-Bridge.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HaptikaPPG-Swift-Cxx-Bridge.hpp"
// Include C++ implementation defined types
#include "HaptikaPPG-Swift-Cxx-Umbrella.hpp"
#include "HybridHaptikaCardiacMirrorSpecSwift.hpp"
#include "HybridHaptikaHealthKitSpecSwift.hpp"
#include "HybridHaptikaPPGFrameProcessorSpecSwift.hpp"
#include <NitroModules/NitroDefines.hpp>
#include <VisionCamera/VisionCamera-Swift-Cxx-Bridge.hpp>
namespace margelo::nitro::haptika::ppg::bridge::swift {
// pragma MARK: std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>
std::shared_ptr<margelo::nitro::camera::HybridFrameSpec> create_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(void* NON_NULL swiftUnsafePointer) noexcept {
// Implemented in VisionCamera
return margelo::nitro::camera::bridge::swift::create_std__shared_ptr_HybridFrameSpec_(swiftUnsafePointer);
}
void* NON_NULL get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_ cppType) {
// Implemented in VisionCamera
return margelo::nitro::camera::bridge::swift::get_std__shared_ptr_HybridFrameSpec_(cppType);
}
// pragma MARK: std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec>
std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec> create_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(void* NON_NULL swiftUnsafePointer) noexcept {
HaptikaPPG::HybridHaptikaPPGFrameProcessorSpec_cxx swiftPart = HaptikaPPG::HybridHaptikaPPGFrameProcessorSpec_cxx::fromUnsafe(swiftUnsafePointer);
return std::make_shared<margelo::nitro::haptika::ppg::HybridHaptikaPPGFrameProcessorSpecSwift>(swiftPart);
}
void* NON_NULL get_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_ cppType) {
std::shared_ptr<margelo::nitro::haptika::ppg::HybridHaptikaPPGFrameProcessorSpecSwift> swiftWrapper = std::dynamic_pointer_cast<margelo::nitro::haptika::ppg::HybridHaptikaPPGFrameProcessorSpecSwift>(cppType);
#ifdef NITRO_DEBUG
if (swiftWrapper == nullptr) [[unlikely]] {
throw std::runtime_error("Class \"HybridHaptikaPPGFrameProcessorSpec\" is not implemented in Swift!");
}
#endif
HaptikaPPG::HybridHaptikaPPGFrameProcessorSpec_cxx& swiftPart = swiftWrapper->getSwiftPart();
return swiftPart.toUnsafe();
}
// pragma MARK: std::function<void(bool /* result */)>
Func_void_bool create_Func_void_bool(void* NON_NULL swiftClosureWrapper) noexcept {
auto swiftClosure = HaptikaPPG::Func_void_bool::fromUnsafe(swiftClosureWrapper);
return [swiftClosure = std::move(swiftClosure)](bool result) mutable -> void {
swiftClosure.call(result);
};
}
// pragma MARK: std::function<void(const std::exception_ptr& /* error */)>
Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept {
auto swiftClosure = HaptikaPPG::Func_void_std__exception_ptr::fromUnsafe(swiftClosureWrapper);
return [swiftClosure = std::move(swiftClosure)](const std::exception_ptr& error) mutable -> void {
swiftClosure.call(error);
};
}
// pragma MARK: std::function<void(const NativeHealthKitContext& /* result */)>
Func_void_NativeHealthKitContext create_Func_void_NativeHealthKitContext(void* NON_NULL swiftClosureWrapper) noexcept {
auto swiftClosure = HaptikaPPG::Func_void_NativeHealthKitContext::fromUnsafe(swiftClosureWrapper);
return [swiftClosure = std::move(swiftClosure)](const NativeHealthKitContext& result) mutable -> void {
swiftClosure.call(result);
};
}
// pragma MARK: std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& /* result */)>
Func_void_std__optional_NativeHeartbeatSeriesSummary_ create_Func_void_std__optional_NativeHeartbeatSeriesSummary_(void* NON_NULL swiftClosureWrapper) noexcept {
auto swiftClosure = HaptikaPPG::Func_void_std__optional_NativeHeartbeatSeriesSummary_::fromUnsafe(swiftClosureWrapper);
return [swiftClosure = std::move(swiftClosure)](const std::optional<NativeHeartbeatSeriesSummary>& result) mutable -> void {
swiftClosure.call(result);
};
}
// pragma MARK: std::shared_ptr<HybridHaptikaHealthKitSpec>
std::shared_ptr<HybridHaptikaHealthKitSpec> create_std__shared_ptr_HybridHaptikaHealthKitSpec_(void* NON_NULL swiftUnsafePointer) noexcept {
HaptikaPPG::HybridHaptikaHealthKitSpec_cxx swiftPart = HaptikaPPG::HybridHaptikaHealthKitSpec_cxx::fromUnsafe(swiftUnsafePointer);
return std::make_shared<margelo::nitro::haptika::ppg::HybridHaptikaHealthKitSpecSwift>(swiftPart);
}
void* NON_NULL get_std__shared_ptr_HybridHaptikaHealthKitSpec_(std__shared_ptr_HybridHaptikaHealthKitSpec_ cppType) {
std::shared_ptr<margelo::nitro::haptika::ppg::HybridHaptikaHealthKitSpecSwift> swiftWrapper = std::dynamic_pointer_cast<margelo::nitro::haptika::ppg::HybridHaptikaHealthKitSpecSwift>(cppType);
#ifdef NITRO_DEBUG
if (swiftWrapper == nullptr) [[unlikely]] {
throw std::runtime_error("Class \"HybridHaptikaHealthKitSpec\" is not implemented in Swift!");
}
#endif
HaptikaPPG::HybridHaptikaHealthKitSpec_cxx& swiftPart = swiftWrapper->getSwiftPart();
return swiftPart.toUnsafe();
}
// pragma MARK: std::shared_ptr<HybridHaptikaCardiacMirrorSpec>
std::shared_ptr<HybridHaptikaCardiacMirrorSpec> create_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(void* NON_NULL swiftUnsafePointer) noexcept {
HaptikaPPG::HybridHaptikaCardiacMirrorSpec_cxx swiftPart = HaptikaPPG::HybridHaptikaCardiacMirrorSpec_cxx::fromUnsafe(swiftUnsafePointer);
return std::make_shared<margelo::nitro::haptika::ppg::HybridHaptikaCardiacMirrorSpecSwift>(swiftPart);
}
void* NON_NULL get_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(std__shared_ptr_HybridHaptikaCardiacMirrorSpec_ cppType) {
std::shared_ptr<margelo::nitro::haptika::ppg::HybridHaptikaCardiacMirrorSpecSwift> swiftWrapper = std::dynamic_pointer_cast<margelo::nitro::haptika::ppg::HybridHaptikaCardiacMirrorSpecSwift>(cppType);
#ifdef NITRO_DEBUG
if (swiftWrapper == nullptr) [[unlikely]] {
throw std::runtime_error("Class \"HybridHaptikaCardiacMirrorSpec\" is not implemented in Swift!");
}
#endif
HaptikaPPG::HybridHaptikaCardiacMirrorSpec_cxx& swiftPart = swiftWrapper->getSwiftPart();
return swiftPart.toUnsafe();
}
} // namespace margelo::nitro::haptika::ppg::bridge::swift
@@ -0,0 +1,489 @@
///
/// HaptikaPPG-Swift-Cxx-Bridge.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
// Forward declarations of C++ defined types
// Forward declaration of `FingerContactState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class FingerContactState; }
// Forward declaration of `HealthMetricType` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class HealthMetricType; }
// Forward declaration of `HybridFrameSpec` to properly resolve imports.
namespace margelo::nitro::camera { class HybridFrameSpec; }
// Forward declaration of `HybridHaptikaCardiacMirrorSpec` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { class HybridHaptikaCardiacMirrorSpec; }
// Forward declaration of `HybridHaptikaHealthKitSpec` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { class HybridHaptikaHealthKitSpec; }
// Forward declaration of `HybridHaptikaPPGFrameProcessorSpec` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { class HybridHaptikaPPGFrameProcessorSpec; }
// Forward declaration of `NativeAcceptedBeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeAcceptedBeat; }
// Forward declaration of `NativeCardiacMirrorHeartbeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorHeartbeat; }
// Forward declaration of `NativeCardiacMirrorPatternInfo` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorPatternInfo; }
// Forward declaration of `NativeHealthKitContext` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthKitContext; }
// Forward declaration of `NativeHealthMetricSample` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthMetricSample; }
// Forward declaration of `NativeHeartbeatSeriesSummary` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHeartbeatSeriesSummary; }
// Forward declaration of `NativePPGProcessingResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGProcessingResult; }
// Forward declaration of `PPGFrameUpdate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct PPGFrameUpdate; }
// Forward declaration of `ThermalState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class ThermalState; }
// Forward declarations of Swift defined types
// Forward declaration of `HybridFrameSpec_cxx` to properly resolve imports.
namespace VisionCamera { class HybridFrameSpec_cxx; }
// Forward declaration of `HybridHaptikaCardiacMirrorSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaCardiacMirrorSpec_cxx; }
// Forward declaration of `HybridHaptikaHealthKitSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaHealthKitSpec_cxx; }
// Forward declaration of `HybridHaptikaPPGFrameProcessorSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaPPGFrameProcessorSpec_cxx; }
// Include C++ defined types
#include "FingerContactState.hpp"
#include "HealthMetricType.hpp"
#include "HybridHaptikaCardiacMirrorSpec.hpp"
#include "HybridHaptikaHealthKitSpec.hpp"
#include "HybridHaptikaPPGFrameProcessorSpec.hpp"
#include "NativeAcceptedBeat.hpp"
#include "NativeCardiacMirrorHeartbeat.hpp"
#include "NativeCardiacMirrorPatternInfo.hpp"
#include "NativeHealthKitContext.hpp"
#include "NativeHealthMetricSample.hpp"
#include "NativeHeartbeatSeriesSummary.hpp"
#include "NativePPGProcessingResult.hpp"
#include "PPGFrameUpdate.hpp"
#include "ThermalState.hpp"
#include <NitroModules/Promise.hpp>
#include <NitroModules/PromiseHolder.hpp>
#include <NitroModules/Result.hpp>
#include <VisionCamera/HybridFrameSpec.hpp>
#include <exception>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
/**
* Contains specialized versions of C++ templated types so they can be accessed from Swift,
* as well as helper functions to interact with those C++ types from Swift.
*/
namespace margelo::nitro::haptika::ppg::bridge::swift {
// pragma MARK: std::optional<double>
/**
* Specialized version of `std::optional<double>`.
*/
using std__optional_double_ = std::optional<double>;
inline std::optional<double> create_std__optional_double_(const double& value) noexcept {
return std::optional<double>(value);
}
inline bool has_value_std__optional_double_(const std::optional<double>& optional) noexcept {
return optional.has_value();
}
inline double get_std__optional_double_(const std::optional<double>& optional) noexcept {
return optional.value();
}
// pragma MARK: std::vector<NativeAcceptedBeat>
/**
* Specialized version of `std::vector<NativeAcceptedBeat>`.
*/
using std__vector_NativeAcceptedBeat_ = std::vector<NativeAcceptedBeat>;
inline std::vector<NativeAcceptedBeat> create_std__vector_NativeAcceptedBeat_(size_t size) noexcept {
std::vector<NativeAcceptedBeat> vector;
vector.reserve(size);
return vector;
}
// pragma MARK: std::optional<PPGFrameUpdate>
/**
* Specialized version of `std::optional<PPGFrameUpdate>`.
*/
using std__optional_PPGFrameUpdate_ = std::optional<PPGFrameUpdate>;
inline std::optional<PPGFrameUpdate> create_std__optional_PPGFrameUpdate_(const PPGFrameUpdate& value) noexcept {
return std::optional<PPGFrameUpdate>(value);
}
inline bool has_value_std__optional_PPGFrameUpdate_(const std::optional<PPGFrameUpdate>& optional) noexcept {
return optional.has_value();
}
inline PPGFrameUpdate get_std__optional_PPGFrameUpdate_(const std::optional<PPGFrameUpdate>& optional) noexcept {
return optional.value();
}
// pragma MARK: std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>
/**
* Specialized version of `std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>`.
*/
using std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_ = std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>;
std::shared_ptr<margelo::nitro::camera::HybridFrameSpec> create_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(void* NON_NULL swiftUnsafePointer) noexcept;
void* NON_NULL get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_ cppType);
// pragma MARK: std::weak_ptr<margelo::nitro::camera::HybridFrameSpec>
using std__weak_ptr_margelo__nitro__camera__HybridFrameSpec_ = std::weak_ptr<margelo::nitro::camera::HybridFrameSpec>;
inline std__weak_ptr_margelo__nitro__camera__HybridFrameSpec_ weakify_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& strong) noexcept { return strong; }
// pragma MARK: std::optional<std::string>
/**
* Specialized version of `std::optional<std::string>`.
*/
using std__optional_std__string_ = std::optional<std::string>;
inline std::optional<std::string> create_std__optional_std__string_(const std::string& value) noexcept {
return std::optional<std::string>(value);
}
inline bool has_value_std__optional_std__string_(const std::optional<std::string>& optional) noexcept {
return optional.has_value();
}
inline std::string get_std__optional_std__string_(const std::optional<std::string>& optional) noexcept {
return optional.value();
}
// pragma MARK: std::vector<std::string>
/**
* Specialized version of `std::vector<std::string>`.
*/
using std__vector_std__string_ = std::vector<std::string>;
inline std::vector<std::string> create_std__vector_std__string_(size_t size) noexcept {
std::vector<std::string> vector;
vector.reserve(size);
return vector;
}
// pragma MARK: std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec>
/**
* Specialized version of `std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec>`.
*/
using std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_ = std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec>;
std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec> create_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(void* NON_NULL swiftUnsafePointer) noexcept;
void* NON_NULL get_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_ cppType);
// pragma MARK: std::weak_ptr<HybridHaptikaPPGFrameProcessorSpec>
using std__weak_ptr_HybridHaptikaPPGFrameProcessorSpec_ = std::weak_ptr<HybridHaptikaPPGFrameProcessorSpec>;
inline std__weak_ptr_HybridHaptikaPPGFrameProcessorSpec_ weakify_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(const std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec>& strong) noexcept { return strong; }
// pragma MARK: Result<void>
using Result_void_ = Result<void>;
inline Result_void_ create_Result_void_() noexcept {
return Result<void>::withValue();
}
inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept {
return Result<void>::withError(error);
}
// pragma MARK: Result<std::optional<PPGFrameUpdate>>
using Result_std__optional_PPGFrameUpdate__ = Result<std::optional<PPGFrameUpdate>>;
inline Result_std__optional_PPGFrameUpdate__ create_Result_std__optional_PPGFrameUpdate__(const std::optional<PPGFrameUpdate>& value) noexcept {
return Result<std::optional<PPGFrameUpdate>>::withValue(value);
}
inline Result_std__optional_PPGFrameUpdate__ create_Result_std__optional_PPGFrameUpdate__(const std::exception_ptr& error) noexcept {
return Result<std::optional<PPGFrameUpdate>>::withError(error);
}
// pragma MARK: Result<NativePPGProcessingResult>
using Result_NativePPGProcessingResult_ = Result<NativePPGProcessingResult>;
inline Result_NativePPGProcessingResult_ create_Result_NativePPGProcessingResult_(const NativePPGProcessingResult& value) noexcept {
return Result<NativePPGProcessingResult>::withValue(value);
}
inline Result_NativePPGProcessingResult_ create_Result_NativePPGProcessingResult_(const std::exception_ptr& error) noexcept {
return Result<NativePPGProcessingResult>::withError(error);
}
// pragma MARK: Result<ThermalState>
using Result_ThermalState_ = Result<ThermalState>;
inline Result_ThermalState_ create_Result_ThermalState_(ThermalState value) noexcept {
return Result<ThermalState>::withValue(std::move(value));
}
inline Result_ThermalState_ create_Result_ThermalState_(const std::exception_ptr& error) noexcept {
return Result<ThermalState>::withError(error);
}
// pragma MARK: std::shared_ptr<Promise<bool>>
/**
* Specialized version of `std::shared_ptr<Promise<bool>>`.
*/
using std__shared_ptr_Promise_bool__ = std::shared_ptr<Promise<bool>>;
inline std::shared_ptr<Promise<bool>> create_std__shared_ptr_Promise_bool__() noexcept {
return Promise<bool>::create();
}
inline PromiseHolder<bool> wrap_std__shared_ptr_Promise_bool__(std::shared_ptr<Promise<bool>> promise) noexcept {
return PromiseHolder<bool>(std::move(promise));
}
// pragma MARK: std::function<void(bool /* result */)>
/**
* Specialized version of `std::function<void(bool)>`.
*/
using Func_void_bool = std::function<void(bool /* result */)>;
/**
* Wrapper class for a `std::function<void(bool / * result * /)>`, this can be used from Swift.
*/
class Func_void_bool_Wrapper final {
public:
explicit Func_void_bool_Wrapper(std::function<void(bool /* result */)>&& func): _function(std::make_unique<std::function<void(bool /* result */)>>(std::move(func))) {}
inline void call(bool result) const noexcept {
_function->operator()(result);
}
private:
std::unique_ptr<std::function<void(bool /* result */)>> _function;
} SWIFT_NONCOPYABLE;
Func_void_bool create_Func_void_bool(void* NON_NULL swiftClosureWrapper) noexcept;
inline Func_void_bool_Wrapper wrap_Func_void_bool(Func_void_bool value) noexcept {
return Func_void_bool_Wrapper(std::move(value));
}
// pragma MARK: std::function<void(const std::exception_ptr& /* error */)>
/**
* Specialized version of `std::function<void(const std::exception_ptr&)>`.
*/
using Func_void_std__exception_ptr = std::function<void(const std::exception_ptr& /* error */)>;
/**
* Wrapper class for a `std::function<void(const std::exception_ptr& / * error * /)>`, this can be used from Swift.
*/
class Func_void_std__exception_ptr_Wrapper final {
public:
explicit Func_void_std__exception_ptr_Wrapper(std::function<void(const std::exception_ptr& /* error */)>&& func): _function(std::make_unique<std::function<void(const std::exception_ptr& /* error */)>>(std::move(func))) {}
inline void call(std::exception_ptr error) const noexcept {
_function->operator()(error);
}
private:
std::unique_ptr<std::function<void(const std::exception_ptr& /* error */)>> _function;
} SWIFT_NONCOPYABLE;
Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept;
inline Func_void_std__exception_ptr_Wrapper wrap_Func_void_std__exception_ptr(Func_void_std__exception_ptr value) noexcept {
return Func_void_std__exception_ptr_Wrapper(std::move(value));
}
// pragma MARK: std::optional<NativeHealthMetricSample>
/**
* Specialized version of `std::optional<NativeHealthMetricSample>`.
*/
using std__optional_NativeHealthMetricSample_ = std::optional<NativeHealthMetricSample>;
inline std::optional<NativeHealthMetricSample> create_std__optional_NativeHealthMetricSample_(const NativeHealthMetricSample& value) noexcept {
return std::optional<NativeHealthMetricSample>(value);
}
inline bool has_value_std__optional_NativeHealthMetricSample_(const std::optional<NativeHealthMetricSample>& optional) noexcept {
return optional.has_value();
}
inline NativeHealthMetricSample get_std__optional_NativeHealthMetricSample_(const std::optional<NativeHealthMetricSample>& optional) noexcept {
return optional.value();
}
// pragma MARK: std::vector<NativeHealthMetricSample>
/**
* Specialized version of `std::vector<NativeHealthMetricSample>`.
*/
using std__vector_NativeHealthMetricSample_ = std::vector<NativeHealthMetricSample>;
inline std::vector<NativeHealthMetricSample> create_std__vector_NativeHealthMetricSample_(size_t size) noexcept {
std::vector<NativeHealthMetricSample> vector;
vector.reserve(size);
return vector;
}
// pragma MARK: std::shared_ptr<Promise<NativeHealthKitContext>>
/**
* Specialized version of `std::shared_ptr<Promise<NativeHealthKitContext>>`.
*/
using std__shared_ptr_Promise_NativeHealthKitContext__ = std::shared_ptr<Promise<NativeHealthKitContext>>;
inline std::shared_ptr<Promise<NativeHealthKitContext>> create_std__shared_ptr_Promise_NativeHealthKitContext__() noexcept {
return Promise<NativeHealthKitContext>::create();
}
inline PromiseHolder<NativeHealthKitContext> wrap_std__shared_ptr_Promise_NativeHealthKitContext__(std::shared_ptr<Promise<NativeHealthKitContext>> promise) noexcept {
return PromiseHolder<NativeHealthKitContext>(std::move(promise));
}
// pragma MARK: std::function<void(const NativeHealthKitContext& /* result */)>
/**
* Specialized version of `std::function<void(const NativeHealthKitContext&)>`.
*/
using Func_void_NativeHealthKitContext = std::function<void(const NativeHealthKitContext& /* result */)>;
/**
* Wrapper class for a `std::function<void(const NativeHealthKitContext& / * result * /)>`, this can be used from Swift.
*/
class Func_void_NativeHealthKitContext_Wrapper final {
public:
explicit Func_void_NativeHealthKitContext_Wrapper(std::function<void(const NativeHealthKitContext& /* result */)>&& func): _function(std::make_unique<std::function<void(const NativeHealthKitContext& /* result */)>>(std::move(func))) {}
inline void call(NativeHealthKitContext result) const noexcept {
_function->operator()(result);
}
private:
std::unique_ptr<std::function<void(const NativeHealthKitContext& /* result */)>> _function;
} SWIFT_NONCOPYABLE;
Func_void_NativeHealthKitContext create_Func_void_NativeHealthKitContext(void* NON_NULL swiftClosureWrapper) noexcept;
inline Func_void_NativeHealthKitContext_Wrapper wrap_Func_void_NativeHealthKitContext(Func_void_NativeHealthKitContext value) noexcept {
return Func_void_NativeHealthKitContext_Wrapper(std::move(value));
}
// pragma MARK: std::vector<double>
/**
* Specialized version of `std::vector<double>`.
*/
using std__vector_double_ = std::vector<double>;
inline std::vector<double> create_std__vector_double_(size_t size) noexcept {
std::vector<double> vector;
vector.reserve(size);
return vector;
}
// pragma MARK: std::optional<NativeHeartbeatSeriesSummary>
/**
* Specialized version of `std::optional<NativeHeartbeatSeriesSummary>`.
*/
using std__optional_NativeHeartbeatSeriesSummary_ = std::optional<NativeHeartbeatSeriesSummary>;
inline std::optional<NativeHeartbeatSeriesSummary> create_std__optional_NativeHeartbeatSeriesSummary_(const NativeHeartbeatSeriesSummary& value) noexcept {
return std::optional<NativeHeartbeatSeriesSummary>(value);
}
inline bool has_value_std__optional_NativeHeartbeatSeriesSummary_(const std::optional<NativeHeartbeatSeriesSummary>& optional) noexcept {
return optional.has_value();
}
inline NativeHeartbeatSeriesSummary get_std__optional_NativeHeartbeatSeriesSummary_(const std::optional<NativeHeartbeatSeriesSummary>& optional) noexcept {
return optional.value();
}
// pragma MARK: std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>
/**
* Specialized version of `std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>`.
*/
using std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary___ = std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>;
inline std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>> create_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary___() noexcept {
return Promise<std::optional<NativeHeartbeatSeriesSummary>>::create();
}
inline PromiseHolder<std::optional<NativeHeartbeatSeriesSummary>> wrap_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary___(std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>> promise) noexcept {
return PromiseHolder<std::optional<NativeHeartbeatSeriesSummary>>(std::move(promise));
}
// pragma MARK: std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& /* result */)>
/**
* Specialized version of `std::function<void(const std::optional<NativeHeartbeatSeriesSummary>&)>`.
*/
using Func_void_std__optional_NativeHeartbeatSeriesSummary_ = std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& /* result */)>;
/**
* Wrapper class for a `std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& / * result * /)>`, this can be used from Swift.
*/
class Func_void_std__optional_NativeHeartbeatSeriesSummary__Wrapper final {
public:
explicit Func_void_std__optional_NativeHeartbeatSeriesSummary__Wrapper(std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& /* result */)>&& func): _function(std::make_unique<std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& /* result */)>>(std::move(func))) {}
inline void call(std::optional<NativeHeartbeatSeriesSummary> result) const noexcept {
_function->operator()(result);
}
private:
std::unique_ptr<std::function<void(const std::optional<NativeHeartbeatSeriesSummary>& /* result */)>> _function;
} SWIFT_NONCOPYABLE;
Func_void_std__optional_NativeHeartbeatSeriesSummary_ create_Func_void_std__optional_NativeHeartbeatSeriesSummary_(void* NON_NULL swiftClosureWrapper) noexcept;
inline Func_void_std__optional_NativeHeartbeatSeriesSummary__Wrapper wrap_Func_void_std__optional_NativeHeartbeatSeriesSummary_(Func_void_std__optional_NativeHeartbeatSeriesSummary_ value) noexcept {
return Func_void_std__optional_NativeHeartbeatSeriesSummary__Wrapper(std::move(value));
}
// pragma MARK: std::shared_ptr<HybridHaptikaHealthKitSpec>
/**
* Specialized version of `std::shared_ptr<HybridHaptikaHealthKitSpec>`.
*/
using std__shared_ptr_HybridHaptikaHealthKitSpec_ = std::shared_ptr<HybridHaptikaHealthKitSpec>;
std::shared_ptr<HybridHaptikaHealthKitSpec> create_std__shared_ptr_HybridHaptikaHealthKitSpec_(void* NON_NULL swiftUnsafePointer) noexcept;
void* NON_NULL get_std__shared_ptr_HybridHaptikaHealthKitSpec_(std__shared_ptr_HybridHaptikaHealthKitSpec_ cppType);
// pragma MARK: std::weak_ptr<HybridHaptikaHealthKitSpec>
using std__weak_ptr_HybridHaptikaHealthKitSpec_ = std::weak_ptr<HybridHaptikaHealthKitSpec>;
inline std__weak_ptr_HybridHaptikaHealthKitSpec_ weakify_std__shared_ptr_HybridHaptikaHealthKitSpec_(const std::shared_ptr<HybridHaptikaHealthKitSpec>& strong) noexcept { return strong; }
// pragma MARK: Result<bool>
using Result_bool_ = Result<bool>;
inline Result_bool_ create_Result_bool_(bool value) noexcept {
return Result<bool>::withValue(std::move(value));
}
inline Result_bool_ create_Result_bool_(const std::exception_ptr& error) noexcept {
return Result<bool>::withError(error);
}
// pragma MARK: Result<std::shared_ptr<Promise<bool>>>
using Result_std__shared_ptr_Promise_bool___ = Result<std::shared_ptr<Promise<bool>>>;
inline Result_std__shared_ptr_Promise_bool___ create_Result_std__shared_ptr_Promise_bool___(const std::shared_ptr<Promise<bool>>& value) noexcept {
return Result<std::shared_ptr<Promise<bool>>>::withValue(value);
}
inline Result_std__shared_ptr_Promise_bool___ create_Result_std__shared_ptr_Promise_bool___(const std::exception_ptr& error) noexcept {
return Result<std::shared_ptr<Promise<bool>>>::withError(error);
}
// pragma MARK: Result<std::shared_ptr<Promise<NativeHealthKitContext>>>
using Result_std__shared_ptr_Promise_NativeHealthKitContext___ = Result<std::shared_ptr<Promise<NativeHealthKitContext>>>;
inline Result_std__shared_ptr_Promise_NativeHealthKitContext___ create_Result_std__shared_ptr_Promise_NativeHealthKitContext___(const std::shared_ptr<Promise<NativeHealthKitContext>>& value) noexcept {
return Result<std::shared_ptr<Promise<NativeHealthKitContext>>>::withValue(value);
}
inline Result_std__shared_ptr_Promise_NativeHealthKitContext___ create_Result_std__shared_ptr_Promise_NativeHealthKitContext___(const std::exception_ptr& error) noexcept {
return Result<std::shared_ptr<Promise<NativeHealthKitContext>>>::withError(error);
}
// pragma MARK: Result<std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>>
using Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____ = Result<std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>>;
inline Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____ create_Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____(const std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>& value) noexcept {
return Result<std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>>::withValue(value);
}
inline Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____ create_Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____(const std::exception_ptr& error) noexcept {
return Result<std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>>>::withError(error);
}
// pragma MARK: std::optional<NativeCardiacMirrorHeartbeat>
/**
* Specialized version of `std::optional<NativeCardiacMirrorHeartbeat>`.
*/
using std__optional_NativeCardiacMirrorHeartbeat_ = std::optional<NativeCardiacMirrorHeartbeat>;
inline std::optional<NativeCardiacMirrorHeartbeat> create_std__optional_NativeCardiacMirrorHeartbeat_(const NativeCardiacMirrorHeartbeat& value) noexcept {
return std::optional<NativeCardiacMirrorHeartbeat>(value);
}
inline bool has_value_std__optional_NativeCardiacMirrorHeartbeat_(const std::optional<NativeCardiacMirrorHeartbeat>& optional) noexcept {
return optional.has_value();
}
inline NativeCardiacMirrorHeartbeat get_std__optional_NativeCardiacMirrorHeartbeat_(const std::optional<NativeCardiacMirrorHeartbeat>& optional) noexcept {
return optional.value();
}
// pragma MARK: std::shared_ptr<HybridHaptikaCardiacMirrorSpec>
/**
* Specialized version of `std::shared_ptr<HybridHaptikaCardiacMirrorSpec>`.
*/
using std__shared_ptr_HybridHaptikaCardiacMirrorSpec_ = std::shared_ptr<HybridHaptikaCardiacMirrorSpec>;
std::shared_ptr<HybridHaptikaCardiacMirrorSpec> create_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(void* NON_NULL swiftUnsafePointer) noexcept;
void* NON_NULL get_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(std__shared_ptr_HybridHaptikaCardiacMirrorSpec_ cppType);
// pragma MARK: std::weak_ptr<HybridHaptikaCardiacMirrorSpec>
using std__weak_ptr_HybridHaptikaCardiacMirrorSpec_ = std::weak_ptr<HybridHaptikaCardiacMirrorSpec>;
inline std__weak_ptr_HybridHaptikaCardiacMirrorSpec_ weakify_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(const std::shared_ptr<HybridHaptikaCardiacMirrorSpec>& strong) noexcept { return strong; }
// pragma MARK: Result<NativeCardiacMirrorPatternInfo>
using Result_NativeCardiacMirrorPatternInfo_ = Result<NativeCardiacMirrorPatternInfo>;
inline Result_NativeCardiacMirrorPatternInfo_ create_Result_NativeCardiacMirrorPatternInfo_(const NativeCardiacMirrorPatternInfo& value) noexcept {
return Result<NativeCardiacMirrorPatternInfo>::withValue(value);
}
inline Result_NativeCardiacMirrorPatternInfo_ create_Result_NativeCardiacMirrorPatternInfo_(const std::exception_ptr& error) noexcept {
return Result<NativeCardiacMirrorPatternInfo>::withError(error);
}
// pragma MARK: Result<std::optional<NativeCardiacMirrorHeartbeat>>
using Result_std__optional_NativeCardiacMirrorHeartbeat__ = Result<std::optional<NativeCardiacMirrorHeartbeat>>;
inline Result_std__optional_NativeCardiacMirrorHeartbeat__ create_Result_std__optional_NativeCardiacMirrorHeartbeat__(const std::optional<NativeCardiacMirrorHeartbeat>& value) noexcept {
return Result<std::optional<NativeCardiacMirrorHeartbeat>>::withValue(value);
}
inline Result_std__optional_NativeCardiacMirrorHeartbeat__ create_Result_std__optional_NativeCardiacMirrorHeartbeat__(const std::exception_ptr& error) noexcept {
return Result<std::optional<NativeCardiacMirrorHeartbeat>>::withError(error);
}
// pragma MARK: Result<double>
using Result_double_ = Result<double>;
inline Result_double_ create_Result_double_(double value) noexcept {
return Result<double>::withValue(std::move(value));
}
inline Result_double_ create_Result_double_(const std::exception_ptr& error) noexcept {
return Result<double>::withError(error);
}
} // namespace margelo::nitro::haptika::ppg::bridge::swift
@@ -0,0 +1,110 @@
///
/// HaptikaPPG-Swift-Cxx-Umbrella.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
// Forward declarations of C++ defined types
// Forward declaration of `FingerContactState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class FingerContactState; }
// Forward declaration of `HealthMetricType` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class HealthMetricType; }
// Forward declaration of `HybridFrameSpec` to properly resolve imports.
namespace margelo::nitro::camera { class HybridFrameSpec; }
// Forward declaration of `HybridHaptikaCardiacMirrorSpec` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { class HybridHaptikaCardiacMirrorSpec; }
// Forward declaration of `HybridHaptikaHealthKitSpec` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { class HybridHaptikaHealthKitSpec; }
// Forward declaration of `HybridHaptikaPPGFrameProcessorSpec` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { class HybridHaptikaPPGFrameProcessorSpec; }
// Forward declaration of `NativeAcceptedBeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeAcceptedBeat; }
// Forward declaration of `NativeBPMEstimate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeBPMEstimate; }
// Forward declaration of `NativeCardiacMirrorHeartbeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorHeartbeat; }
// Forward declaration of `NativeCardiacMirrorPatternInfo` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorPatternInfo; }
// Forward declaration of `NativeHealthKitContext` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthKitContext; }
// Forward declaration of `NativeHealthMetricSample` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthMetricSample; }
// Forward declaration of `NativeHeartbeatSeriesSummary` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHeartbeatSeriesSummary; }
// Forward declaration of `NativePPGConfiguration` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGConfiguration; }
// Forward declaration of `NativePPGProcessingResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGProcessingResult; }
// Forward declaration of `NativePRVMetrics` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePRVMetrics; }
// Forward declaration of `NativeSignalQualityResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeSignalQualityResult; }
// Forward declaration of `PPGFrameUpdate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct PPGFrameUpdate; }
// Forward declaration of `PPGSessionPurpose` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class PPGSessionPurpose; }
// Forward declaration of `ThermalState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class ThermalState; }
// Include C++ defined types
#include "FingerContactState.hpp"
#include "HealthMetricType.hpp"
#include "HybridHaptikaCardiacMirrorSpec.hpp"
#include "HybridHaptikaHealthKitSpec.hpp"
#include "HybridHaptikaPPGFrameProcessorSpec.hpp"
#include "NativeAcceptedBeat.hpp"
#include "NativeBPMEstimate.hpp"
#include "NativeCardiacMirrorHeartbeat.hpp"
#include "NativeCardiacMirrorPatternInfo.hpp"
#include "NativeHealthKitContext.hpp"
#include "NativeHealthMetricSample.hpp"
#include "NativeHeartbeatSeriesSummary.hpp"
#include "NativePPGConfiguration.hpp"
#include "NativePPGProcessingResult.hpp"
#include "NativePRVMetrics.hpp"
#include "NativeSignalQualityResult.hpp"
#include "PPGFrameUpdate.hpp"
#include "PPGSessionPurpose.hpp"
#include "ThermalState.hpp"
#include <NitroModules/Promise.hpp>
#include <NitroModules/Result.hpp>
#include <VisionCamera/HybridFrameSpec.hpp>
#include <exception>
#include <memory>
#include <optional>
#include <string>
#include <vector>
// C++ helpers for Swift
#include "HaptikaPPG-Swift-Cxx-Bridge.hpp"
// Common C++ types used in Swift
#include <NitroModules/ArrayBufferHolder.hpp>
#include <NitroModules/AnyMapUtils.hpp>
#include <NitroModules/RuntimeError.hpp>
#include <NitroModules/DateToChronoDate.hpp>
// Forward declarations of Swift defined types
// Forward declaration of `HybridFrameSpec_cxx` to properly resolve imports.
namespace VisionCamera { class HybridFrameSpec_cxx; }
// Forward declaration of `HybridHaptikaCardiacMirrorSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaCardiacMirrorSpec_cxx; }
// Forward declaration of `HybridHaptikaHealthKitSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaHealthKitSpec_cxx; }
// Forward declaration of `HybridHaptikaPPGFrameProcessorSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaPPGFrameProcessorSpec_cxx; }
// Include Swift defined types
#if __has_include("HaptikaPPG-Swift.h")
// This header is generated by Xcode/Swift on every app build.
// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "HaptikaPPG".
#include "HaptikaPPG-Swift.h"
// Same as above, but used when building with frameworks (`use_frameworks`)
#elif __has_include(<HaptikaPPG/HaptikaPPG-Swift.h>)
#include <HaptikaPPG/HaptikaPPG-Swift.h>
#else
#error HaptikaPPG's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "HaptikaPPG", and try building the app first.
#endif
@@ -0,0 +1,49 @@
///
/// HaptikaPPGAutolinking.mm
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#import <Foundation/Foundation.h>
#import <NitroModules/HybridObjectRegistry.hpp>
#import "HaptikaPPG-Swift-Cxx-Umbrella.hpp"
#import <type_traits>
#include "HybridHaptikaPPGFrameProcessorSpecSwift.hpp"
#include "HybridHaptikaHealthKitSpecSwift.hpp"
#include "HybridHaptikaCardiacMirrorSpecSwift.hpp"
@interface HaptikaPPGAutolinking : NSObject
@end
@implementation HaptikaPPGAutolinking
+ (void) load {
using namespace margelo::nitro;
using namespace margelo::nitro::haptika::ppg;
HybridObjectRegistry::registerHybridObjectConstructor(
"HaptikaPPGFrameProcessor",
[]() -> std::shared_ptr<HybridObject> {
std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec> hybridObject = HaptikaPPG::HaptikaPPGAutolinking::createHaptikaPPGFrameProcessor();
return hybridObject;
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"HaptikaHealthKit",
[]() -> std::shared_ptr<HybridObject> {
std::shared_ptr<HybridHaptikaHealthKitSpec> hybridObject = HaptikaPPG::HaptikaPPGAutolinking::createHaptikaHealthKit();
return hybridObject;
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"HaptikaCardiacMirror",
[]() -> std::shared_ptr<HybridObject> {
std::shared_ptr<HybridHaptikaCardiacMirrorSpec> hybridObject = HaptikaPPG::HaptikaPPGAutolinking::createHaptikaCardiacMirror();
return hybridObject;
}
);
}
@end
@@ -0,0 +1,50 @@
///
/// HaptikaPPGAutolinking.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
// TODO: Use empty enums once Swift supports exporting them as namespaces
// See: https://github.com/swiftlang/swift/pull/83616
public final class HaptikaPPGAutolinking {
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
public static func createHaptikaPPGFrameProcessor() -> bridge.std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_ {
let hybridObject = HybridHaptikaPPGFrameProcessor()
return { () -> bridge.std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_ in
let __cxxWrapped = hybridObject.getCxxWrapper()
return __cxxWrapped.getCxxPart()
}()
}
public static func isHaptikaPPGFrameProcessorRecyclable() -> Bool {
return HybridHaptikaPPGFrameProcessor.self is any RecyclableView.Type
}
public static func createHaptikaHealthKit() -> bridge.std__shared_ptr_HybridHaptikaHealthKitSpec_ {
let hybridObject = HybridHaptikaHealthKit()
return { () -> bridge.std__shared_ptr_HybridHaptikaHealthKitSpec_ in
let __cxxWrapped = hybridObject.getCxxWrapper()
return __cxxWrapped.getCxxPart()
}()
}
public static func isHaptikaHealthKitRecyclable() -> Bool {
return HybridHaptikaHealthKit.self is any RecyclableView.Type
}
public static func createHaptikaCardiacMirror() -> bridge.std__shared_ptr_HybridHaptikaCardiacMirrorSpec_ {
let hybridObject = HybridHaptikaCardiacMirror()
return { () -> bridge.std__shared_ptr_HybridHaptikaCardiacMirrorSpec_ in
let __cxxWrapped = hybridObject.getCxxWrapper()
return __cxxWrapped.getCxxPart()
}()
}
public static func isHaptikaCardiacMirrorRecyclable() -> Bool {
return HybridHaptikaCardiacMirror.self is any RecyclableView.Type
}
}
@@ -0,0 +1,11 @@
///
/// HybridHaptikaCardiacMirrorSpecSwift.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HybridHaptikaCardiacMirrorSpecSwift.hpp"
namespace margelo::nitro::haptika::ppg {
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,150 @@
///
/// HybridHaptikaCardiacMirrorSpecSwift.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#include "HybridHaptikaCardiacMirrorSpec.hpp"
// Forward declaration of `HybridHaptikaCardiacMirrorSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaCardiacMirrorSpec_cxx; }
// Forward declaration of `NativeCardiacMirrorPatternInfo` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorPatternInfo; }
// Forward declaration of `NativeCardiacMirrorHeartbeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorHeartbeat; }
#include "NativeCardiacMirrorPatternInfo.hpp"
#include <string>
#include "NativeCardiacMirrorHeartbeat.hpp"
#include <optional>
#include "HaptikaPPG-Swift-Cxx-Umbrella.hpp"
namespace margelo::nitro::haptika::ppg {
/**
* The C++ part of HybridHaptikaCardiacMirrorSpec_cxx.swift.
*
* HybridHaptikaCardiacMirrorSpecSwift (C++) accesses HybridHaptikaCardiacMirrorSpec_cxx (Swift), and might
* contain some additional bridging code for C++ <> Swift interop.
*
* Since this obviously introduces an overhead, I hope at some point in
* the future, HybridHaptikaCardiacMirrorSpec_cxx can directly inherit from the C++ class HybridHaptikaCardiacMirrorSpec
* to simplify the whole structure and memory management.
*/
class HybridHaptikaCardiacMirrorSpecSwift: public virtual HybridHaptikaCardiacMirrorSpec {
public:
// Constructor from a Swift instance
explicit HybridHaptikaCardiacMirrorSpecSwift(const HaptikaPPG::HybridHaptikaCardiacMirrorSpec_cxx& swiftPart):
HybridObject(HybridHaptikaCardiacMirrorSpec::TAG),
_swiftPart(swiftPart) { }
public:
// Get the Swift part
inline HaptikaPPG::HybridHaptikaCardiacMirrorSpec_cxx& getSwiftPart() noexcept {
return _swiftPart;
}
public:
inline size_t getExternalMemorySize() noexcept override {
return _swiftPart.getMemorySize();
}
bool equals(const std::shared_ptr<HybridObject>& other) override {
if (auto otherCast = std::dynamic_pointer_cast<HybridHaptikaCardiacMirrorSpecSwift>(other)) {
return _swiftPart.equals(otherCast->_swiftPart);
}
return false;
}
void dispose() noexcept override {
_swiftPart.dispose();
}
std::string toString() override {
return _swiftPart.toString();
}
public:
// Properties
public:
// Methods
inline bool isSupported() override {
auto __result = _swiftPart.isSupported();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline NativeCardiacMirrorPatternInfo patternInfo() override {
auto __result = _swiftPart.patternInfo();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline void start(double targetBpm) override {
auto __result = _swiftPart.start(std::forward<decltype(targetBpm)>(targetBpm));
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
inline void pulse() override {
auto __result = _swiftPart.pulse();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
inline void stop() override {
auto __result = _swiftPart.stop();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
inline void startWatchSession() override {
auto __result = _swiftPart.startWatchSession();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
inline bool watchReachable() override {
auto __result = _swiftPart.watchReachable();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline std::optional<NativeCardiacMirrorHeartbeat> latestHeartbeat() override {
auto __result = _swiftPart.latestHeartbeat();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline double playRegulationProtocol(const std::string& kind) override {
auto __result = _swiftPart.playRegulationProtocol(kind);
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline void stopRegulationProtocol() override {
auto __result = _swiftPart.stopRegulationProtocol();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
private:
HaptikaPPG::HybridHaptikaCardiacMirrorSpec_cxx _swiftPart;
};
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,11 @@
///
/// HybridHaptikaHealthKitSpecSwift.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HybridHaptikaHealthKitSpecSwift.hpp"
namespace margelo::nitro::haptika::ppg {
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,120 @@
///
/// HybridHaptikaHealthKitSpecSwift.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#include "HybridHaptikaHealthKitSpec.hpp"
// Forward declaration of `HybridHaptikaHealthKitSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaHealthKitSpec_cxx; }
// Forward declaration of `NativeHealthKitContext` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthKitContext; }
// Forward declaration of `NativeHealthMetricSample` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthMetricSample; }
// Forward declaration of `HealthMetricType` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class HealthMetricType; }
// Forward declaration of `NativeHeartbeatSeriesSummary` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHeartbeatSeriesSummary; }
#include <NitroModules/Promise.hpp>
#include "NativeHealthKitContext.hpp"
#include "NativeHealthMetricSample.hpp"
#include <optional>
#include "HealthMetricType.hpp"
#include <string>
#include <vector>
#include "NativeHeartbeatSeriesSummary.hpp"
#include "HaptikaPPG-Swift-Cxx-Umbrella.hpp"
namespace margelo::nitro::haptika::ppg {
/**
* The C++ part of HybridHaptikaHealthKitSpec_cxx.swift.
*
* HybridHaptikaHealthKitSpecSwift (C++) accesses HybridHaptikaHealthKitSpec_cxx (Swift), and might
* contain some additional bridging code for C++ <> Swift interop.
*
* Since this obviously introduces an overhead, I hope at some point in
* the future, HybridHaptikaHealthKitSpec_cxx can directly inherit from the C++ class HybridHaptikaHealthKitSpec
* to simplify the whole structure and memory management.
*/
class HybridHaptikaHealthKitSpecSwift: public virtual HybridHaptikaHealthKitSpec {
public:
// Constructor from a Swift instance
explicit HybridHaptikaHealthKitSpecSwift(const HaptikaPPG::HybridHaptikaHealthKitSpec_cxx& swiftPart):
HybridObject(HybridHaptikaHealthKitSpec::TAG),
_swiftPart(swiftPart) { }
public:
// Get the Swift part
inline HaptikaPPG::HybridHaptikaHealthKitSpec_cxx& getSwiftPart() noexcept {
return _swiftPart;
}
public:
inline size_t getExternalMemorySize() noexcept override {
return _swiftPart.getMemorySize();
}
bool equals(const std::shared_ptr<HybridObject>& other) override {
if (auto otherCast = std::dynamic_pointer_cast<HybridHaptikaHealthKitSpecSwift>(other)) {
return _swiftPart.equals(otherCast->_swiftPart);
}
return false;
}
void dispose() noexcept override {
_swiftPart.dispose();
}
std::string toString() override {
return _swiftPart.toString();
}
public:
// Properties
public:
// Methods
inline bool isAvailable() override {
auto __result = _swiftPart.isAvailable();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline std::shared_ptr<Promise<bool>> requestReadAuthorization() override {
auto __result = _swiftPart.requestReadAuthorization();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline std::shared_ptr<Promise<NativeHealthKitContext>> fetchContext(double lookbackDays) override {
auto __result = _swiftPart.fetchContext(std::forward<decltype(lookbackDays)>(lookbackDays));
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>> fetchLatestHeartbeatSeries() override {
auto __result = _swiftPart.fetchLatestHeartbeatSeries();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
private:
HaptikaPPG::HybridHaptikaHealthKitSpec_cxx _swiftPart;
};
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,11 @@
///
/// HybridHaptikaPPGFrameProcessorSpecSwift.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HybridHaptikaPPGFrameProcessorSpecSwift.hpp"
namespace margelo::nitro::haptika::ppg {
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,145 @@
///
/// HybridHaptikaPPGFrameProcessorSpecSwift.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#include "HybridHaptikaPPGFrameProcessorSpec.hpp"
// Forward declaration of `HybridHaptikaPPGFrameProcessorSpec_cxx` to properly resolve imports.
namespace HaptikaPPG { class HybridHaptikaPPGFrameProcessorSpec_cxx; }
// Forward declaration of `NativePPGConfiguration` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGConfiguration; }
// Forward declaration of `PPGSessionPurpose` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class PPGSessionPurpose; }
// Forward declaration of `PPGFrameUpdate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct PPGFrameUpdate; }
// Forward declaration of `FingerContactState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class FingerContactState; }
// Forward declaration of `ThermalState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class ThermalState; }
// Forward declaration of `NativeAcceptedBeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeAcceptedBeat; }
// Forward declaration of `HybridFrameSpec` to properly resolve imports.
namespace margelo::nitro::camera { class HybridFrameSpec; }
// Forward declaration of `NativePPGProcessingResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGProcessingResult; }
// Forward declaration of `NativeBPMEstimate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeBPMEstimate; }
// Forward declaration of `NativePRVMetrics` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePRVMetrics; }
// Forward declaration of `NativeSignalQualityResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeSignalQualityResult; }
#include "NativePPGConfiguration.hpp"
#include "PPGSessionPurpose.hpp"
#include "PPGFrameUpdate.hpp"
#include <optional>
#include "FingerContactState.hpp"
#include "ThermalState.hpp"
#include "NativeAcceptedBeat.hpp"
#include <vector>
#include <memory>
#include <VisionCamera/HybridFrameSpec.hpp>
#include "NativePPGProcessingResult.hpp"
#include <string>
#include "NativeBPMEstimate.hpp"
#include "NativePRVMetrics.hpp"
#include "NativeSignalQualityResult.hpp"
#include "HaptikaPPG-Swift-Cxx-Umbrella.hpp"
namespace margelo::nitro::haptika::ppg {
/**
* The C++ part of HybridHaptikaPPGFrameProcessorSpec_cxx.swift.
*
* HybridHaptikaPPGFrameProcessorSpecSwift (C++) accesses HybridHaptikaPPGFrameProcessorSpec_cxx (Swift), and might
* contain some additional bridging code for C++ <> Swift interop.
*
* Since this obviously introduces an overhead, I hope at some point in
* the future, HybridHaptikaPPGFrameProcessorSpec_cxx can directly inherit from the C++ class HybridHaptikaPPGFrameProcessorSpec
* to simplify the whole structure and memory management.
*/
class HybridHaptikaPPGFrameProcessorSpecSwift: public virtual HybridHaptikaPPGFrameProcessorSpec {
public:
// Constructor from a Swift instance
explicit HybridHaptikaPPGFrameProcessorSpecSwift(const HaptikaPPG::HybridHaptikaPPGFrameProcessorSpec_cxx& swiftPart):
HybridObject(HybridHaptikaPPGFrameProcessorSpec::TAG),
_swiftPart(swiftPart) { }
public:
// Get the Swift part
inline HaptikaPPG::HybridHaptikaPPGFrameProcessorSpec_cxx& getSwiftPart() noexcept {
return _swiftPart;
}
public:
inline size_t getExternalMemorySize() noexcept override {
return _swiftPart.getMemorySize();
}
bool equals(const std::shared_ptr<HybridObject>& other) override {
if (auto otherCast = std::dynamic_pointer_cast<HybridHaptikaPPGFrameProcessorSpecSwift>(other)) {
return _swiftPart.equals(otherCast->_swiftPart);
}
return false;
}
void dispose() noexcept override {
_swiftPart.dispose();
}
std::string toString() override {
return _swiftPart.toString();
}
public:
// Properties
public:
// Methods
inline void startMeasurement(const NativePPGConfiguration& configuration) override {
auto __result = _swiftPart.startMeasurement(std::forward<decltype(configuration)>(configuration));
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
inline std::optional<PPGFrameUpdate> processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
auto __result = _swiftPart.processFrame(frame);
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline NativePPGProcessingResult finishMeasurement() override {
auto __result = _swiftPart.finishMeasurement();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
inline void cancelMeasurement() override {
auto __result = _swiftPart.cancelMeasurement();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}
inline ThermalState getThermalState() override {
auto __result = _swiftPart.getThermalState();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
auto __value = std::move(__result.value());
return __value;
}
private:
HaptikaPPG::HybridHaptikaPPGFrameProcessorSpec_cxx _swiftPart;
};
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,56 @@
///
/// FingerContactState.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
/**
* Represents the JS union `FingerContactState`, backed by a C++ enum.
*/
public typealias FingerContactState = margelo.nitro.haptika.ppg.FingerContactState
public extension FingerContactState {
/**
* Get a FingerContactState for the given String value, or
* return `nil` if the given value was invalid/unknown.
*/
init?(fromString string: String) {
switch string {
case "absent":
self = .absent
case "adjusting":
self = .adjusting
case "valid":
self = .valid
case "tooMuchPressure":
self = .toomuchpressure
case "overexposed":
self = .overexposed
case "unstable":
self = .unstable
default:
return nil
}
}
/**
* Get the String value this FingerContactState represents.
*/
var stringValue: String {
switch self {
case .absent:
return "absent"
case .adjusting:
return "adjusting"
case .valid:
return "valid"
case .toomuchpressure:
return "tooMuchPressure"
case .overexposed:
return "overexposed"
case .unstable:
return "unstable"
}
}
}
@@ -0,0 +1,46 @@
///
/// Func_void_NativeHealthKitContext.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Wraps a Swift `(_ value: NativeHealthKitContext) -> Void` as a class.
* This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
*/
public final class Func_void_NativeHealthKitContext {
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
private let closure: (_ value: NativeHealthKitContext) -> Void
public init(_ closure: @escaping (_ value: NativeHealthKitContext) -> Void) {
self.closure = closure
}
@inline(__always)
public func call(value: NativeHealthKitContext) -> Void {
self.closure(value)
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
@inline(__always)
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `Func_void_NativeHealthKitContext`.
* The pointer has to be a retained opaque `Unmanaged<Func_void_NativeHealthKitContext>`.
* This removes one strong reference from the object!
*/
@inline(__always)
public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_NativeHealthKitContext {
return Unmanaged<Func_void_NativeHealthKitContext>.fromOpaque(pointer).takeRetainedValue()
}
}
@@ -0,0 +1,46 @@
///
/// Func_void_bool.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Wraps a Swift `(_ value: Bool) -> Void` as a class.
* This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
*/
public final class Func_void_bool {
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
private let closure: (_ value: Bool) -> Void
public init(_ closure: @escaping (_ value: Bool) -> Void) {
self.closure = closure
}
@inline(__always)
public func call(value: Bool) -> Void {
self.closure(value)
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
@inline(__always)
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `Func_void_bool`.
* The pointer has to be a retained opaque `Unmanaged<Func_void_bool>`.
* This removes one strong reference from the object!
*/
@inline(__always)
public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_bool {
return Unmanaged<Func_void_bool>.fromOpaque(pointer).takeRetainedValue()
}
}
@@ -0,0 +1,46 @@
///
/// Func_void_std__exception_ptr.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Wraps a Swift `(_ error: Error) -> Void` as a class.
* This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
*/
public final class Func_void_std__exception_ptr {
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
private let closure: (_ error: Error) -> Void
public init(_ closure: @escaping (_ error: Error) -> Void) {
self.closure = closure
}
@inline(__always)
public func call(error: std.exception_ptr) -> Void {
self.closure(RuntimeError.from(cppError: error))
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
@inline(__always)
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `Func_void_std__exception_ptr`.
* The pointer has to be a retained opaque `Unmanaged<Func_void_std__exception_ptr>`.
* This removes one strong reference from the object!
*/
@inline(__always)
public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__exception_ptr {
return Unmanaged<Func_void_std__exception_ptr>.fromOpaque(pointer).takeRetainedValue()
}
}
@@ -0,0 +1,46 @@
///
/// Func_void_std__optional_NativeHeartbeatSeriesSummary_.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Wraps a Swift `(_ value: NativeHeartbeatSeriesSummary?) -> Void` as a class.
* This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
*/
public final class Func_void_std__optional_NativeHeartbeatSeriesSummary_ {
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
private let closure: (_ value: NativeHeartbeatSeriesSummary?) -> Void
public init(_ closure: @escaping (_ value: NativeHeartbeatSeriesSummary?) -> Void) {
self.closure = closure
}
@inline(__always)
public func call(value: bridge.std__optional_NativeHeartbeatSeriesSummary_) -> Void {
self.closure(value.value)
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
@inline(__always)
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `Func_void_std__optional_NativeHeartbeatSeriesSummary_`.
* The pointer has to be a retained opaque `Unmanaged<Func_void_std__optional_NativeHeartbeatSeriesSummary_>`.
* This removes one strong reference from the object!
*/
@inline(__always)
public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__optional_NativeHeartbeatSeriesSummary_ {
return Unmanaged<Func_void_std__optional_NativeHeartbeatSeriesSummary_>.fromOpaque(pointer).takeRetainedValue()
}
}
@@ -0,0 +1,44 @@
///
/// HealthMetricType.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
/**
* Represents the JS union `HealthMetricType`, backed by a C++ enum.
*/
public typealias HealthMetricType = margelo.nitro.haptika.ppg.HealthMetricType
public extension HealthMetricType {
/**
* Get a HealthMetricType for the given String value, or
* return `nil` if the given value was invalid/unknown.
*/
init?(fromString string: String) {
switch string {
case "heartRate":
self = .heartrate
case "restingHeartRate":
self = .restingheartrate
case "hrvSdnn":
self = .hrvsdnn
default:
return nil
}
}
/**
* Get the String value this HealthMetricType represents.
*/
var stringValue: String {
switch self {
case .heartrate:
return "heartRate"
case .restingheartrate:
return "restingHeartRate"
case .hrvsdnn:
return "hrvSdnn"
}
}
}
@@ -0,0 +1,64 @@
///
/// HybridHaptikaCardiacMirrorSpec.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/// See ``HybridHaptikaCardiacMirrorSpec``
public protocol HybridHaptikaCardiacMirrorSpec_protocol: HybridObject {
// Properties
// Methods
func isSupported() throws -> Bool
func patternInfo() throws -> NativeCardiacMirrorPatternInfo
func start(targetBpm: Double) throws -> Void
func pulse() throws -> Void
func stop() throws -> Void
func startWatchSession() throws -> Void
func watchReachable() throws -> Bool
func latestHeartbeat() throws -> NativeCardiacMirrorHeartbeat?
func playRegulationProtocol(kind: String) throws -> Double
func stopRegulationProtocol() throws -> Void
}
public extension HybridHaptikaCardiacMirrorSpec_protocol {
/// Default implementation of ``HybridObject.toString``
func toString() -> String {
return "[HybridObject HaptikaCardiacMirror]"
}
}
/// See ``HybridHaptikaCardiacMirrorSpec``
open class HybridHaptikaCardiacMirrorSpec_base {
private weak var cxxWrapper: HybridHaptikaCardiacMirrorSpec_cxx? = nil
public init() { }
public func getCxxWrapper() -> HybridHaptikaCardiacMirrorSpec_cxx {
#if DEBUG
guard self is any HybridHaptikaCardiacMirrorSpec else {
fatalError("`self` is not a `HybridHaptikaCardiacMirrorSpec`! Did you accidentally inherit from `HybridHaptikaCardiacMirrorSpec_base` instead of `HybridHaptikaCardiacMirrorSpec`?")
}
#endif
if let cxxWrapper = self.cxxWrapper {
return cxxWrapper
} else {
let cxxWrapper = HybridHaptikaCardiacMirrorSpec_cxx(self as! any HybridHaptikaCardiacMirrorSpec)
self.cxxWrapper = cxxWrapper
return cxxWrapper
}
}
}
/**
* A Swift base-protocol representing the HaptikaCardiacMirror HybridObject.
* Implement this protocol to create Swift-based instances of HaptikaCardiacMirror.
* ```swift
* class HybridHaptikaCardiacMirror : HybridHaptikaCardiacMirrorSpec {
* // ...
* }
* ```
*/
public typealias HybridHaptikaCardiacMirrorSpec = HybridHaptikaCardiacMirrorSpec_protocol & HybridHaptikaCardiacMirrorSpec_base
@@ -0,0 +1,247 @@
///
/// HybridHaptikaCardiacMirrorSpec_cxx.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* A class implementation that bridges HybridHaptikaCardiacMirrorSpec over to C++.
* In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined.
*
* Also, some Swift types need to be bridged with special handling:
* - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330)
* - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper
* - Throwing methods need to be wrapped with a Result<T, Error> type, as exceptions cannot be propagated to C++
*/
open class HybridHaptikaCardiacMirrorSpec_cxx {
/**
* The Swift <> C++ bridge's namespace (`margelo::nitro::haptika::ppg::bridge::swift`)
* from `HaptikaPPG-Swift-Cxx-Bridge.hpp`.
* This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift.
*/
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Holds an instance of the `HybridHaptikaCardiacMirrorSpec` Swift protocol.
*/
private var __implementation: any HybridHaptikaCardiacMirrorSpec
/**
* Holds a weak pointer to the C++ class that wraps the Swift class.
*/
private var __cxxPart: bridge.std__weak_ptr_HybridHaptikaCardiacMirrorSpec_
/**
* Create a new `HybridHaptikaCardiacMirrorSpec_cxx` that wraps the given `HybridHaptikaCardiacMirrorSpec`.
* All properties and methods bridge to C++ types.
*/
public init(_ implementation: any HybridHaptikaCardiacMirrorSpec) {
self.__implementation = implementation
self.__cxxPart = .init()
/* no base class */
}
/**
* Get the actual `HybridHaptikaCardiacMirrorSpec` instance this class wraps.
*/
@inline(__always)
public func getHybridHaptikaCardiacMirrorSpec() -> any HybridHaptikaCardiacMirrorSpec {
return __implementation
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `HybridHaptikaCardiacMirrorSpec_cxx`.
* The pointer has to be a retained opaque `Unmanaged<HybridHaptikaCardiacMirrorSpec_cxx>`.
* This removes one strong reference from the object!
*/
public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridHaptikaCardiacMirrorSpec_cxx {
return Unmanaged<HybridHaptikaCardiacMirrorSpec_cxx>.fromOpaque(pointer).takeRetainedValue()
}
/**
* Gets (or creates) the C++ part of this Hybrid Object.
* The C++ part is a `std::shared_ptr<HybridHaptikaCardiacMirrorSpec>`.
*/
public func getCxxPart() -> bridge.std__shared_ptr_HybridHaptikaCardiacMirrorSpec_ {
let cachedCxxPart = self.__cxxPart.lock()
if Bool(fromCxx: cachedCxxPart) {
return cachedCxxPart
} else {
let newCxxPart = bridge.create_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(self.toUnsafe())
__cxxPart = bridge.weakify_std__shared_ptr_HybridHaptikaCardiacMirrorSpec_(newCxxPart)
return newCxxPart
}
}
/**
* Get the memory size of the Swift class (plus size of any other allocations)
* so the JS VM can properly track it and garbage-collect the JS object if needed.
*/
@inline(__always)
public var memorySize: Int {
return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize
}
/**
* Compares this object with the given [other] object for reference equality.
*/
@inline(__always)
public func equals(other: HybridHaptikaCardiacMirrorSpec_cxx) -> Bool {
return self.__implementation === other.__implementation
}
/**
* Call dispose() on the Swift class.
* This _may_ be called manually from JS.
*/
@inline(__always)
public func dispose() {
self.__implementation.dispose()
}
/**
* Call toString() on the Swift class.
*/
@inline(__always)
public func toString() -> String {
return self.__implementation.toString()
}
// Properties
// Methods
@inline(__always)
public final func isSupported() -> bridge.Result_bool_ {
do {
let __result = try self.__implementation.isSupported()
let __resultCpp = __result
return bridge.create_Result_bool_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_bool_(__exceptionPtr)
}
}
@inline(__always)
public final func patternInfo() -> bridge.Result_NativeCardiacMirrorPatternInfo_ {
do {
let __result = try self.__implementation.patternInfo()
let __resultCpp = __result
return bridge.create_Result_NativeCardiacMirrorPatternInfo_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_NativeCardiacMirrorPatternInfo_(__exceptionPtr)
}
}
@inline(__always)
public final func start(targetBpm: Double) -> bridge.Result_void_ {
do {
try self.__implementation.start(targetBpm: targetBpm)
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
@inline(__always)
public final func pulse() -> bridge.Result_void_ {
do {
try self.__implementation.pulse()
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
@inline(__always)
public final func stop() -> bridge.Result_void_ {
do {
try self.__implementation.stop()
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
@inline(__always)
public final func startWatchSession() -> bridge.Result_void_ {
do {
try self.__implementation.startWatchSession()
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
@inline(__always)
public final func watchReachable() -> bridge.Result_bool_ {
do {
let __result = try self.__implementation.watchReachable()
let __resultCpp = __result
return bridge.create_Result_bool_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_bool_(__exceptionPtr)
}
}
@inline(__always)
public final func latestHeartbeat() -> bridge.Result_std__optional_NativeCardiacMirrorHeartbeat__ {
do {
let __result = try self.__implementation.latestHeartbeat()
let __resultCpp = { () -> bridge.std__optional_NativeCardiacMirrorHeartbeat_ in
if let __unwrappedValue = __result {
return bridge.create_std__optional_NativeCardiacMirrorHeartbeat_(__unwrappedValue)
} else {
return .init()
}
}()
return bridge.create_Result_std__optional_NativeCardiacMirrorHeartbeat__(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_std__optional_NativeCardiacMirrorHeartbeat__(__exceptionPtr)
}
}
@inline(__always)
public final func playRegulationProtocol(kind: std.string) -> bridge.Result_double_ {
do {
let __result = try self.__implementation.playRegulationProtocol(kind: String(kind))
let __resultCpp = __result
return bridge.create_Result_double_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_double_(__exceptionPtr)
}
}
@inline(__always)
public final func stopRegulationProtocol() -> bridge.Result_void_ {
do {
try self.__implementation.stopRegulationProtocol()
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
}
@@ -0,0 +1,58 @@
///
/// HybridHaptikaHealthKitSpec.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/// See ``HybridHaptikaHealthKitSpec``
public protocol HybridHaptikaHealthKitSpec_protocol: HybridObject {
// Properties
// Methods
func isAvailable() throws -> Bool
func requestReadAuthorization() throws -> Promise<Bool>
func fetchContext(lookbackDays: Double) throws -> Promise<NativeHealthKitContext>
func fetchLatestHeartbeatSeries() throws -> Promise<NativeHeartbeatSeriesSummary?>
}
public extension HybridHaptikaHealthKitSpec_protocol {
/// Default implementation of ``HybridObject.toString``
func toString() -> String {
return "[HybridObject HaptikaHealthKit]"
}
}
/// See ``HybridHaptikaHealthKitSpec``
open class HybridHaptikaHealthKitSpec_base {
private weak var cxxWrapper: HybridHaptikaHealthKitSpec_cxx? = nil
public init() { }
public func getCxxWrapper() -> HybridHaptikaHealthKitSpec_cxx {
#if DEBUG
guard self is any HybridHaptikaHealthKitSpec else {
fatalError("`self` is not a `HybridHaptikaHealthKitSpec`! Did you accidentally inherit from `HybridHaptikaHealthKitSpec_base` instead of `HybridHaptikaHealthKitSpec`?")
}
#endif
if let cxxWrapper = self.cxxWrapper {
return cxxWrapper
} else {
let cxxWrapper = HybridHaptikaHealthKitSpec_cxx(self as! any HybridHaptikaHealthKitSpec)
self.cxxWrapper = cxxWrapper
return cxxWrapper
}
}
}
/**
* A Swift base-protocol representing the HaptikaHealthKit HybridObject.
* Implement this protocol to create Swift-based instances of HaptikaHealthKit.
* ```swift
* class HybridHaptikaHealthKit : HybridHaptikaHealthKitSpec {
* // ...
* }
* ```
*/
public typealias HybridHaptikaHealthKitSpec = HybridHaptikaHealthKitSpec_protocol & HybridHaptikaHealthKitSpec_base
@@ -0,0 +1,201 @@
///
/// HybridHaptikaHealthKitSpec_cxx.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* A class implementation that bridges HybridHaptikaHealthKitSpec over to C++.
* In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined.
*
* Also, some Swift types need to be bridged with special handling:
* - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330)
* - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper
* - Throwing methods need to be wrapped with a Result<T, Error> type, as exceptions cannot be propagated to C++
*/
open class HybridHaptikaHealthKitSpec_cxx {
/**
* The Swift <> C++ bridge's namespace (`margelo::nitro::haptika::ppg::bridge::swift`)
* from `HaptikaPPG-Swift-Cxx-Bridge.hpp`.
* This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift.
*/
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Holds an instance of the `HybridHaptikaHealthKitSpec` Swift protocol.
*/
private var __implementation: any HybridHaptikaHealthKitSpec
/**
* Holds a weak pointer to the C++ class that wraps the Swift class.
*/
private var __cxxPart: bridge.std__weak_ptr_HybridHaptikaHealthKitSpec_
/**
* Create a new `HybridHaptikaHealthKitSpec_cxx` that wraps the given `HybridHaptikaHealthKitSpec`.
* All properties and methods bridge to C++ types.
*/
public init(_ implementation: any HybridHaptikaHealthKitSpec) {
self.__implementation = implementation
self.__cxxPart = .init()
/* no base class */
}
/**
* Get the actual `HybridHaptikaHealthKitSpec` instance this class wraps.
*/
@inline(__always)
public func getHybridHaptikaHealthKitSpec() -> any HybridHaptikaHealthKitSpec {
return __implementation
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `HybridHaptikaHealthKitSpec_cxx`.
* The pointer has to be a retained opaque `Unmanaged<HybridHaptikaHealthKitSpec_cxx>`.
* This removes one strong reference from the object!
*/
public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridHaptikaHealthKitSpec_cxx {
return Unmanaged<HybridHaptikaHealthKitSpec_cxx>.fromOpaque(pointer).takeRetainedValue()
}
/**
* Gets (or creates) the C++ part of this Hybrid Object.
* The C++ part is a `std::shared_ptr<HybridHaptikaHealthKitSpec>`.
*/
public func getCxxPart() -> bridge.std__shared_ptr_HybridHaptikaHealthKitSpec_ {
let cachedCxxPart = self.__cxxPart.lock()
if Bool(fromCxx: cachedCxxPart) {
return cachedCxxPart
} else {
let newCxxPart = bridge.create_std__shared_ptr_HybridHaptikaHealthKitSpec_(self.toUnsafe())
__cxxPart = bridge.weakify_std__shared_ptr_HybridHaptikaHealthKitSpec_(newCxxPart)
return newCxxPart
}
}
/**
* Get the memory size of the Swift class (plus size of any other allocations)
* so the JS VM can properly track it and garbage-collect the JS object if needed.
*/
@inline(__always)
public var memorySize: Int {
return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize
}
/**
* Compares this object with the given [other] object for reference equality.
*/
@inline(__always)
public func equals(other: HybridHaptikaHealthKitSpec_cxx) -> Bool {
return self.__implementation === other.__implementation
}
/**
* Call dispose() on the Swift class.
* This _may_ be called manually from JS.
*/
@inline(__always)
public func dispose() {
self.__implementation.dispose()
}
/**
* Call toString() on the Swift class.
*/
@inline(__always)
public func toString() -> String {
return self.__implementation.toString()
}
// Properties
// Methods
@inline(__always)
public final func isAvailable() -> bridge.Result_bool_ {
do {
let __result = try self.__implementation.isAvailable()
let __resultCpp = __result
return bridge.create_Result_bool_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_bool_(__exceptionPtr)
}
}
@inline(__always)
public final func requestReadAuthorization() -> bridge.Result_std__shared_ptr_Promise_bool___ {
do {
let __result = try self.__implementation.requestReadAuthorization()
let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in
let __promise = bridge.create_std__shared_ptr_Promise_bool__()
let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise)
__result
.then({ __result in __promiseHolder.resolve(__result) })
.catch({ __error in __promiseHolder.reject(__error.toCpp()) })
return __promise
}()
return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr)
}
}
@inline(__always)
public final func fetchContext(lookbackDays: Double) -> bridge.Result_std__shared_ptr_Promise_NativeHealthKitContext___ {
do {
let __result = try self.__implementation.fetchContext(lookbackDays: lookbackDays)
let __resultCpp = { () -> bridge.std__shared_ptr_Promise_NativeHealthKitContext__ in
let __promise = bridge.create_std__shared_ptr_Promise_NativeHealthKitContext__()
let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_NativeHealthKitContext__(__promise)
__result
.then({ __result in __promiseHolder.resolve(__result) })
.catch({ __error in __promiseHolder.reject(__error.toCpp()) })
return __promise
}()
return bridge.create_Result_std__shared_ptr_Promise_NativeHealthKitContext___(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_std__shared_ptr_Promise_NativeHealthKitContext___(__exceptionPtr)
}
}
@inline(__always)
public final func fetchLatestHeartbeatSeries() -> bridge.Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____ {
do {
let __result = try self.__implementation.fetchLatestHeartbeatSeries()
let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary___ in
let __promise = bridge.create_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary___()
let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary___(__promise)
__result
.then({ __result in __promiseHolder.resolve({ () -> bridge.std__optional_NativeHeartbeatSeriesSummary_ in
if let __unwrappedValue = __result {
return bridge.create_std__optional_NativeHeartbeatSeriesSummary_(__unwrappedValue)
} else {
return .init()
}
}()) })
.catch({ __error in __promiseHolder.reject(__error.toCpp()) })
return __promise
}()
return bridge.create_Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_std__shared_ptr_Promise_std__optional_NativeHeartbeatSeriesSummary____(__exceptionPtr)
}
}
}
@@ -0,0 +1,60 @@
///
/// HybridHaptikaPPGFrameProcessorSpec.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import VisionCamera
import NitroModules
/// See ``HybridHaptikaPPGFrameProcessorSpec``
public protocol HybridHaptikaPPGFrameProcessorSpec_protocol: HybridObject {
// Properties
// Methods
func startMeasurement(configuration: NativePPGConfiguration) throws -> Void
func processFrame(frame: (any HybridFrameSpec)) throws -> PPGFrameUpdate?
func finishMeasurement() throws -> NativePPGProcessingResult
func cancelMeasurement() throws -> Void
func getThermalState() throws -> ThermalState
}
public extension HybridHaptikaPPGFrameProcessorSpec_protocol {
/// Default implementation of ``HybridObject.toString``
func toString() -> String {
return "[HybridObject HaptikaPPGFrameProcessor]"
}
}
/// See ``HybridHaptikaPPGFrameProcessorSpec``
open class HybridHaptikaPPGFrameProcessorSpec_base {
private weak var cxxWrapper: HybridHaptikaPPGFrameProcessorSpec_cxx? = nil
public init() { }
public func getCxxWrapper() -> HybridHaptikaPPGFrameProcessorSpec_cxx {
#if DEBUG
guard self is any HybridHaptikaPPGFrameProcessorSpec else {
fatalError("`self` is not a `HybridHaptikaPPGFrameProcessorSpec`! Did you accidentally inherit from `HybridHaptikaPPGFrameProcessorSpec_base` instead of `HybridHaptikaPPGFrameProcessorSpec`?")
}
#endif
if let cxxWrapper = self.cxxWrapper {
return cxxWrapper
} else {
let cxxWrapper = HybridHaptikaPPGFrameProcessorSpec_cxx(self as! any HybridHaptikaPPGFrameProcessorSpec)
self.cxxWrapper = cxxWrapper
return cxxWrapper
}
}
}
/**
* A Swift base-protocol representing the HaptikaPPGFrameProcessor HybridObject.
* Implement this protocol to create Swift-based instances of HaptikaPPGFrameProcessor.
* ```swift
* class HybridHaptikaPPGFrameProcessor : HybridHaptikaPPGFrameProcessorSpec {
* // ...
* }
* ```
*/
public typealias HybridHaptikaPPGFrameProcessorSpec = HybridHaptikaPPGFrameProcessorSpec_protocol & HybridHaptikaPPGFrameProcessorSpec_base
@@ -0,0 +1,195 @@
///
/// HybridHaptikaPPGFrameProcessorSpec_cxx.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
import VisionCamera
/**
* A class implementation that bridges HybridHaptikaPPGFrameProcessorSpec over to C++.
* In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined.
*
* Also, some Swift types need to be bridged with special handling:
* - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330)
* - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper
* - Throwing methods need to be wrapped with a Result<T, Error> type, as exceptions cannot be propagated to C++
*/
open class HybridHaptikaPPGFrameProcessorSpec_cxx {
/**
* The Swift <> C++ bridge's namespace (`margelo::nitro::haptika::ppg::bridge::swift`)
* from `HaptikaPPG-Swift-Cxx-Bridge.hpp`.
* This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift.
*/
public typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Holds an instance of the `HybridHaptikaPPGFrameProcessorSpec` Swift protocol.
*/
private var __implementation: any HybridHaptikaPPGFrameProcessorSpec
/**
* Holds a weak pointer to the C++ class that wraps the Swift class.
*/
private var __cxxPart: bridge.std__weak_ptr_HybridHaptikaPPGFrameProcessorSpec_
/**
* Create a new `HybridHaptikaPPGFrameProcessorSpec_cxx` that wraps the given `HybridHaptikaPPGFrameProcessorSpec`.
* All properties and methods bridge to C++ types.
*/
public init(_ implementation: any HybridHaptikaPPGFrameProcessorSpec) {
self.__implementation = implementation
self.__cxxPart = .init()
/* no base class */
}
/**
* Get the actual `HybridHaptikaPPGFrameProcessorSpec` instance this class wraps.
*/
@inline(__always)
public func getHybridHaptikaPPGFrameProcessorSpec() -> any HybridHaptikaPPGFrameProcessorSpec {
return __implementation
}
/**
* Casts this instance to a retained unsafe raw pointer.
* This acquires one additional strong reference on the object!
*/
public func toUnsafe() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(self).toOpaque()
}
/**
* Casts an unsafe pointer to a `HybridHaptikaPPGFrameProcessorSpec_cxx`.
* The pointer has to be a retained opaque `Unmanaged<HybridHaptikaPPGFrameProcessorSpec_cxx>`.
* This removes one strong reference from the object!
*/
public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridHaptikaPPGFrameProcessorSpec_cxx {
return Unmanaged<HybridHaptikaPPGFrameProcessorSpec_cxx>.fromOpaque(pointer).takeRetainedValue()
}
/**
* Gets (or creates) the C++ part of this Hybrid Object.
* The C++ part is a `std::shared_ptr<HybridHaptikaPPGFrameProcessorSpec>`.
*/
public func getCxxPart() -> bridge.std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_ {
let cachedCxxPart = self.__cxxPart.lock()
if Bool(fromCxx: cachedCxxPart) {
return cachedCxxPart
} else {
let newCxxPart = bridge.create_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(self.toUnsafe())
__cxxPart = bridge.weakify_std__shared_ptr_HybridHaptikaPPGFrameProcessorSpec_(newCxxPart)
return newCxxPart
}
}
/**
* Get the memory size of the Swift class (plus size of any other allocations)
* so the JS VM can properly track it and garbage-collect the JS object if needed.
*/
@inline(__always)
public var memorySize: Int {
return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize
}
/**
* Compares this object with the given [other] object for reference equality.
*/
@inline(__always)
public func equals(other: HybridHaptikaPPGFrameProcessorSpec_cxx) -> Bool {
return self.__implementation === other.__implementation
}
/**
* Call dispose() on the Swift class.
* This _may_ be called manually from JS.
*/
@inline(__always)
public func dispose() {
self.__implementation.dispose()
}
/**
* Call toString() on the Swift class.
*/
@inline(__always)
public func toString() -> String {
return self.__implementation.toString()
}
// Properties
// Methods
@inline(__always)
public final func startMeasurement(configuration: NativePPGConfiguration) -> bridge.Result_void_ {
do {
try self.__implementation.startMeasurement(configuration: configuration)
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
@inline(__always)
public final func processFrame(frame: bridge.std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_) -> bridge.Result_std__optional_PPGFrameUpdate__ {
do {
let __result = try self.__implementation.processFrame(frame: { () -> any HybridFrameSpec in
let __unsafePointer = bridge.get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(frame)
let __instance = HybridFrameSpec_cxx.fromUnsafe(__unsafePointer)
return __instance.getHybridFrameSpec()
}())
let __resultCpp = { () -> bridge.std__optional_PPGFrameUpdate_ in
if let __unwrappedValue = __result {
return bridge.create_std__optional_PPGFrameUpdate_(__unwrappedValue)
} else {
return .init()
}
}()
return bridge.create_Result_std__optional_PPGFrameUpdate__(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_std__optional_PPGFrameUpdate__(__exceptionPtr)
}
}
@inline(__always)
public final func finishMeasurement() -> bridge.Result_NativePPGProcessingResult_ {
do {
let __result = try self.__implementation.finishMeasurement()
let __resultCpp = __result
return bridge.create_Result_NativePPGProcessingResult_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_NativePPGProcessingResult_(__exceptionPtr)
}
}
@inline(__always)
public final func cancelMeasurement() -> bridge.Result_void_ {
do {
try self.__implementation.cancelMeasurement()
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}
@inline(__always)
public final func getThermalState() -> bridge.Result_ThermalState_ {
do {
let __result = try self.__implementation.getThermalState()
let __resultCpp = __result
return bridge.create_Result_ThermalState_(__resultCpp)
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_ThermalState_(__exceptionPtr)
}
}
}
@@ -0,0 +1,44 @@
///
/// NativeAcceptedBeat.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeAcceptedBeat`, backed by a C++ struct.
*/
public typealias NativeAcceptedBeat = margelo.nitro.haptika.ppg.NativeAcceptedBeat
public extension NativeAcceptedBeat {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeAcceptedBeat`.
*/
init(sequence: Double, timestampSeconds: Double, intervalMs: Double, confidence: Double) {
self.init(sequence, timestampSeconds, intervalMs, confidence)
}
@inline(__always)
var sequence: Double {
return self.__sequence
}
@inline(__always)
var timestampSeconds: Double {
return self.__timestampSeconds
}
@inline(__always)
var intervalMs: Double {
return self.__intervalMs
}
@inline(__always)
var confidence: Double {
return self.__confidence
}
}
@@ -0,0 +1,116 @@
///
/// NativeBPMEstimate.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeBPMEstimate`, backed by a C++ struct.
*/
public typealias NativeBPMEstimate = margelo.nitro.haptika.ppg.NativeBPMEstimate
public extension NativeBPMEstimate {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeBPMEstimate`.
*/
init(bpm: Double?, temporalBpm: Double?, frequencyBpm: Double?, methodAgreementBpm: Double?, validPeakCount: Double, rejectedPeakCount: Double, confidence: Double, selectedChannel: String) {
self.init({ () -> bridge.std__optional_double_ in
if let __unwrappedValue = bpm {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = temporalBpm {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = frequencyBpm {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = methodAgreementBpm {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), validPeakCount, rejectedPeakCount, confidence, std.string(selectedChannel))
}
@inline(__always)
var bpm: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__bpm) {
let __unwrapped = bridge.get_std__optional_double_(self.__bpm)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var temporalBpm: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__temporalBpm) {
let __unwrapped = bridge.get_std__optional_double_(self.__temporalBpm)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var frequencyBpm: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__frequencyBpm) {
let __unwrapped = bridge.get_std__optional_double_(self.__frequencyBpm)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var methodAgreementBpm: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__methodAgreementBpm) {
let __unwrapped = bridge.get_std__optional_double_(self.__methodAgreementBpm)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var validPeakCount: Double {
return self.__validPeakCount
}
@inline(__always)
var rejectedPeakCount: Double {
return self.__rejectedPeakCount
}
@inline(__always)
var confidence: Double {
return self.__confidence
}
@inline(__always)
var selectedChannel: String {
return String(self.__selectedChannel)
}
}
@@ -0,0 +1,34 @@
///
/// NativeCardiacMirrorHeartbeat.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeCardiacMirrorHeartbeat`, backed by a C++ struct.
*/
public typealias NativeCardiacMirrorHeartbeat = margelo.nitro.haptika.ppg.NativeCardiacMirrorHeartbeat
public extension NativeCardiacMirrorHeartbeat {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeCardiacMirrorHeartbeat`.
*/
init(timestampSeconds: Double, bpm: Double) {
self.init(timestampSeconds, bpm)
}
@inline(__always)
var timestampSeconds: Double {
return self.__timestampSeconds
}
@inline(__always)
var bpm: Double {
return self.__bpm
}
}
@@ -0,0 +1,39 @@
///
/// NativeCardiacMirrorPatternInfo.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeCardiacMirrorPatternInfo`, backed by a C++ struct.
*/
public typealias NativeCardiacMirrorPatternInfo = margelo.nitro.haptika.ppg.NativeCardiacMirrorPatternInfo
public extension NativeCardiacMirrorPatternInfo {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeCardiacMirrorPatternInfo`.
*/
init(id: String, version: String, latencyBudgetMs: Double) {
self.init(std.string(id), std.string(version), latencyBudgetMs)
}
@inline(__always)
var id: String {
return String(self.__id)
}
@inline(__always)
var version: String {
return String(self.__version)
}
@inline(__always)
var latencyBudgetMs: Double {
return self.__latencyBudgetMs
}
}
@@ -0,0 +1,77 @@
///
/// NativeHealthKitContext.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeHealthKitContext`, backed by a C++ struct.
*/
public typealias NativeHealthKitContext = margelo.nitro.haptika.ppg.NativeHealthKitContext
public extension NativeHealthKitContext {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeHealthKitContext`.
*/
init(available: Bool, authorized: Bool, latestRestingHeartRate: NativeHealthMetricSample?, latestHrvSdnn: NativeHealthMetricSample?, recentHeartRateSamples: [NativeHealthMetricSample], contextConfidence: Double, fetchedAt: String) {
self.init(available, authorized, { () -> bridge.std__optional_NativeHealthMetricSample_ in
if let __unwrappedValue = latestRestingHeartRate {
return bridge.create_std__optional_NativeHealthMetricSample_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_NativeHealthMetricSample_ in
if let __unwrappedValue = latestHrvSdnn {
return bridge.create_std__optional_NativeHealthMetricSample_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__vector_NativeHealthMetricSample_ in
var __vector = bridge.create_std__vector_NativeHealthMetricSample_(recentHeartRateSamples.count)
for __item in recentHeartRateSamples {
__vector.push_back(__item)
}
return __vector
}(), contextConfidence, std.string(fetchedAt))
}
@inline(__always)
var available: Bool {
return self.__available
}
@inline(__always)
var authorized: Bool {
return self.__authorized
}
@inline(__always)
var latestRestingHeartRate: NativeHealthMetricSample? {
return self.__latestRestingHeartRate.value
}
@inline(__always)
var latestHrvSdnn: NativeHealthMetricSample? {
return self.__latestHrvSdnn.value
}
@inline(__always)
var recentHeartRateSamples: [NativeHealthMetricSample] {
return self.__recentHeartRateSamples.map({ __item in __item })
}
@inline(__always)
var contextConfidence: Double {
return self.__contextConfidence
}
@inline(__always)
var fetchedAt: String {
return String(self.__fetchedAt)
}
}
@@ -0,0 +1,103 @@
///
/// NativeHealthMetricSample.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeHealthMetricSample`, backed by a C++ struct.
*/
public typealias NativeHealthMetricSample = margelo.nitro.haptika.ppg.NativeHealthMetricSample
public extension NativeHealthMetricSample {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeHealthMetricSample`.
*/
init(type: HealthMetricType, value: Double, unit: String, startDate: String, endDate: String, sourceName: String?, sourceBundleIdentifier: String?, deviceName: String?) {
self.init(type, value, std.string(unit), std.string(startDate), std.string(endDate), { () -> bridge.std__optional_std__string_ in
if let __unwrappedValue = sourceName {
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
} else {
return .init()
}
}(), { () -> bridge.std__optional_std__string_ in
if let __unwrappedValue = sourceBundleIdentifier {
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
} else {
return .init()
}
}(), { () -> bridge.std__optional_std__string_ in
if let __unwrappedValue = deviceName {
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
} else {
return .init()
}
}())
}
@inline(__always)
var type: HealthMetricType {
return self.__type
}
@inline(__always)
var value: Double {
return self.__value
}
@inline(__always)
var unit: String {
return String(self.__unit)
}
@inline(__always)
var startDate: String {
return String(self.__startDate)
}
@inline(__always)
var endDate: String {
return String(self.__endDate)
}
@inline(__always)
var sourceName: String? {
return { () -> String? in
if bridge.has_value_std__optional_std__string_(self.__sourceName) {
let __unwrapped = bridge.get_std__optional_std__string_(self.__sourceName)
return String(__unwrapped)
} else {
return nil
}
}()
}
@inline(__always)
var sourceBundleIdentifier: String? {
return { () -> String? in
if bridge.has_value_std__optional_std__string_(self.__sourceBundleIdentifier) {
let __unwrapped = bridge.get_std__optional_std__string_(self.__sourceBundleIdentifier)
return String(__unwrapped)
} else {
return nil
}
}()
}
@inline(__always)
var deviceName: String? {
return { () -> String? in
if bridge.has_value_std__optional_std__string_(self.__deviceName) {
let __unwrapped = bridge.get_std__optional_std__string_(self.__deviceName)
return String(__unwrapped)
} else {
return nil
}
}()
}
}
@@ -0,0 +1,92 @@
///
/// NativeHeartbeatSeriesSummary.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeHeartbeatSeriesSummary`, backed by a C++ struct.
*/
public typealias NativeHeartbeatSeriesSummary = margelo.nitro.haptika.ppg.NativeHeartbeatSeriesSummary
public extension NativeHeartbeatSeriesSummary {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeHeartbeatSeriesSummary`.
*/
init(startDate: String, endDate: String, sourceName: String?, sourceBundleIdentifier: String?, heartbeatTimesSinceSeriesStartSeconds: [Double], precededByGapIndexes: [Double]) {
self.init(std.string(startDate), std.string(endDate), { () -> bridge.std__optional_std__string_ in
if let __unwrappedValue = sourceName {
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
} else {
return .init()
}
}(), { () -> bridge.std__optional_std__string_ in
if let __unwrappedValue = sourceBundleIdentifier {
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
} else {
return .init()
}
}(), { () -> bridge.std__vector_double_ in
var __vector = bridge.create_std__vector_double_(heartbeatTimesSinceSeriesStartSeconds.count)
for __item in heartbeatTimesSinceSeriesStartSeconds {
__vector.push_back(__item)
}
return __vector
}(), { () -> bridge.std__vector_double_ in
var __vector = bridge.create_std__vector_double_(precededByGapIndexes.count)
for __item in precededByGapIndexes {
__vector.push_back(__item)
}
return __vector
}())
}
@inline(__always)
var startDate: String {
return String(self.__startDate)
}
@inline(__always)
var endDate: String {
return String(self.__endDate)
}
@inline(__always)
var sourceName: String? {
return { () -> String? in
if bridge.has_value_std__optional_std__string_(self.__sourceName) {
let __unwrapped = bridge.get_std__optional_std__string_(self.__sourceName)
return String(__unwrapped)
} else {
return nil
}
}()
}
@inline(__always)
var sourceBundleIdentifier: String? {
return { () -> String? in
if bridge.has_value_std__optional_std__string_(self.__sourceBundleIdentifier) {
let __unwrapped = bridge.get_std__optional_std__string_(self.__sourceBundleIdentifier)
return String(__unwrapped)
} else {
return nil
}
}()
}
@inline(__always)
var heartbeatTimesSinceSeriesStartSeconds: [Double] {
return self.__heartbeatTimesSinceSeriesStartSeconds.map({ __item in __item })
}
@inline(__always)
var precededByGapIndexes: [Double] {
return self.__precededByGapIndexes.map({ __item in __item })
}
}
@@ -0,0 +1,64 @@
///
/// NativePPGConfiguration.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativePPGConfiguration`, backed by a C++ struct.
*/
public typealias NativePPGConfiguration = margelo.nitro.haptika.ppg.NativePPGConfiguration
public extension NativePPGConfiguration {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativePPGConfiguration`.
*/
init(sessionPurpose: PPGSessionPurpose, targetSampleRate: Double, minimumFrequencyHz: Double, maximumFrequencyHz: Double, stabilizationSeconds: Double, measurementSeconds: Double, emitEveryFrames: Double, minimumContactConfidence: Double) {
self.init(sessionPurpose, targetSampleRate, minimumFrequencyHz, maximumFrequencyHz, stabilizationSeconds, measurementSeconds, emitEveryFrames, minimumContactConfidence)
}
@inline(__always)
var sessionPurpose: PPGSessionPurpose {
return self.__sessionPurpose
}
@inline(__always)
var targetSampleRate: Double {
return self.__targetSampleRate
}
@inline(__always)
var minimumFrequencyHz: Double {
return self.__minimumFrequencyHz
}
@inline(__always)
var maximumFrequencyHz: Double {
return self.__maximumFrequencyHz
}
@inline(__always)
var stabilizationSeconds: Double {
return self.__stabilizationSeconds
}
@inline(__always)
var measurementSeconds: Double {
return self.__measurementSeconds
}
@inline(__always)
var emitEveryFrames: Double {
return self.__emitEveryFrames
}
@inline(__always)
var minimumContactConfidence: Double {
return self.__minimumContactConfidence
}
}
@@ -0,0 +1,82 @@
///
/// NativePPGProcessingResult.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativePPGProcessingResult`, backed by a C++ struct.
*/
public typealias NativePPGProcessingResult = margelo.nitro.haptika.ppg.NativePPGProcessingResult
public extension NativePPGProcessingResult {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativePPGProcessingResult`.
*/
init(valid: Bool, invalidReason: String?, bpm: NativeBPMEstimate, prv: NativePRVMetrics, quality: NativeSignalQualityResult, startedAtMonotonicSeconds: Double, endedAtMonotonicSeconds: Double, rawSampleCount: Double, resampledSampleCount: Double) {
self.init(valid, { () -> bridge.std__optional_std__string_ in
if let __unwrappedValue = invalidReason {
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
} else {
return .init()
}
}(), bpm, prv, quality, startedAtMonotonicSeconds, endedAtMonotonicSeconds, rawSampleCount, resampledSampleCount)
}
@inline(__always)
var valid: Bool {
return self.__valid
}
@inline(__always)
var invalidReason: String? {
return { () -> String? in
if bridge.has_value_std__optional_std__string_(self.__invalidReason) {
let __unwrapped = bridge.get_std__optional_std__string_(self.__invalidReason)
return String(__unwrapped)
} else {
return nil
}
}()
}
@inline(__always)
var bpm: NativeBPMEstimate {
return self.__bpm
}
@inline(__always)
var prv: NativePRVMetrics {
return self.__prv
}
@inline(__always)
var quality: NativeSignalQualityResult {
return self.__quality
}
@inline(__always)
var startedAtMonotonicSeconds: Double {
return self.__startedAtMonotonicSeconds
}
@inline(__always)
var endedAtMonotonicSeconds: Double {
return self.__endedAtMonotonicSeconds
}
@inline(__always)
var rawSampleCount: Double {
return self.__rawSampleCount
}
@inline(__always)
var resampledSampleCount: Double {
return self.__resampledSampleCount
}
}
@@ -0,0 +1,206 @@
///
/// NativePRVMetrics.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativePRVMetrics`, backed by a C++ struct.
*/
public typealias NativePRVMetrics = margelo.nitro.haptika.ppg.NativePRVMetrics
public extension NativePRVMetrics {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativePRVMetrics`.
*/
init(estimatedRmssdMs: Double?, estimatedSdnnMs: Double?, meanIpiMs: Double?, medianIpiMs: Double?, pnn50Percent: Double?, rangeIpiMs: Double?, modeIpiMs: Double?, amo50Percent: Double?, coefficientOfVariation: Double?, validIntervalCount: Double, rejectedIntervalCount: Double, rejectedIntervalRatio: Double, reliable: Bool) {
self.init({ () -> bridge.std__optional_double_ in
if let __unwrappedValue = estimatedRmssdMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = estimatedSdnnMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = meanIpiMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = medianIpiMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = pnn50Percent {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = rangeIpiMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = modeIpiMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = amo50Percent {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = coefficientOfVariation {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), validIntervalCount, rejectedIntervalCount, rejectedIntervalRatio, reliable)
}
@inline(__always)
var estimatedRmssdMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__estimatedRmssdMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__estimatedRmssdMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var estimatedSdnnMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__estimatedSdnnMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__estimatedSdnnMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var meanIpiMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__meanIpiMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__meanIpiMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var medianIpiMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__medianIpiMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__medianIpiMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var pnn50Percent: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__pnn50Percent) {
let __unwrapped = bridge.get_std__optional_double_(self.__pnn50Percent)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var rangeIpiMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__rangeIpiMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__rangeIpiMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var modeIpiMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__modeIpiMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__modeIpiMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var amo50Percent: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__amo50Percent) {
let __unwrapped = bridge.get_std__optional_double_(self.__amo50Percent)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var coefficientOfVariation: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__coefficientOfVariation) {
let __unwrapped = bridge.get_std__optional_double_(self.__coefficientOfVariation)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var validIntervalCount: Double {
return self.__validIntervalCount
}
@inline(__always)
var rejectedIntervalCount: Double {
return self.__rejectedIntervalCount
}
@inline(__always)
var rejectedIntervalRatio: Double {
return self.__rejectedIntervalRatio
}
@inline(__always)
var reliable: Bool {
return self.__reliable
}
}
@@ -0,0 +1,103 @@
///
/// NativeSignalQualityResult.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `NativeSignalQualityResult`, backed by a C++ struct.
*/
public typealias NativeSignalQualityResult = margelo.nitro.haptika.ppg.NativeSignalQualityResult
public extension NativeSignalQualityResult {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `NativeSignalQualityResult`.
*/
init(score: Double, level: String, usableDurationSeconds: Double, droppedFrameRatio: Double, saturationRatio: Double, rejectedIntervalRatio: Double, signalToNoiseRatio: Double?, motionArtifactRatio: Double, methodAgreementScore: Double, timestampJitterRatio: Double, longestGapSeconds: Double, issues: [String]) {
self.init(score, std.string(level), usableDurationSeconds, droppedFrameRatio, saturationRatio, rejectedIntervalRatio, { () -> bridge.std__optional_double_ in
if let __unwrappedValue = signalToNoiseRatio {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), motionArtifactRatio, methodAgreementScore, timestampJitterRatio, longestGapSeconds, { () -> bridge.std__vector_std__string_ in
var __vector = bridge.create_std__vector_std__string_(issues.count)
for __item in issues {
__vector.push_back(std.string(__item))
}
return __vector
}())
}
@inline(__always)
var score: Double {
return self.__score
}
@inline(__always)
var level: String {
return String(self.__level)
}
@inline(__always)
var usableDurationSeconds: Double {
return self.__usableDurationSeconds
}
@inline(__always)
var droppedFrameRatio: Double {
return self.__droppedFrameRatio
}
@inline(__always)
var saturationRatio: Double {
return self.__saturationRatio
}
@inline(__always)
var rejectedIntervalRatio: Double {
return self.__rejectedIntervalRatio
}
@inline(__always)
var signalToNoiseRatio: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__signalToNoiseRatio) {
let __unwrapped = bridge.get_std__optional_double_(self.__signalToNoiseRatio)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var motionArtifactRatio: Double {
return self.__motionArtifactRatio
}
@inline(__always)
var methodAgreementScore: Double {
return self.__methodAgreementScore
}
@inline(__always)
var timestampJitterRatio: Double {
return self.__timestampJitterRatio
}
@inline(__always)
var longestGapSeconds: Double {
return self.__longestGapSeconds
}
@inline(__always)
var issues: [String] {
return self.__issues.map({ __item in String(__item) })
}
}
@@ -0,0 +1,149 @@
///
/// PPGFrameUpdate.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
/**
* Represents an instance of `PPGFrameUpdate`, backed by a C++ struct.
*/
public typealias PPGFrameUpdate = margelo.nitro.haptika.ppg.PPGFrameUpdate
public extension PPGFrameUpdate {
private typealias bridge = margelo.nitro.haptika.ppg.bridge.swift
/**
* Create a new instance of `PPGFrameUpdate`.
*/
init(timestampSeconds: Double, contactState: FingerContactState, contactConfidence: Double, signalLevel: Double, saturationRatio: Double, motionArtifactScore: Double, collectedSampleCount: Double, usableDurationSeconds: Double, droppedFrameCount: Double, thermalState: ThermalState, waveformValue: Double, liveBpm: Double?, liveIpiMs: Double?, beatSequence: Double, acceptedBeats: [NativeAcceptedBeat], liveWindowQuality: Double?) {
self.init(timestampSeconds, contactState, contactConfidence, signalLevel, saturationRatio, motionArtifactScore, collectedSampleCount, usableDurationSeconds, droppedFrameCount, thermalState, waveformValue, { () -> bridge.std__optional_double_ in
if let __unwrappedValue = liveBpm {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = liveIpiMs {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}(), beatSequence, { () -> bridge.std__vector_NativeAcceptedBeat_ in
var __vector = bridge.create_std__vector_NativeAcceptedBeat_(acceptedBeats.count)
for __item in acceptedBeats {
__vector.push_back(__item)
}
return __vector
}(), { () -> bridge.std__optional_double_ in
if let __unwrappedValue = liveWindowQuality {
return bridge.create_std__optional_double_(__unwrappedValue)
} else {
return .init()
}
}())
}
@inline(__always)
var timestampSeconds: Double {
return self.__timestampSeconds
}
@inline(__always)
var contactState: FingerContactState {
return self.__contactState
}
@inline(__always)
var contactConfidence: Double {
return self.__contactConfidence
}
@inline(__always)
var signalLevel: Double {
return self.__signalLevel
}
@inline(__always)
var saturationRatio: Double {
return self.__saturationRatio
}
@inline(__always)
var motionArtifactScore: Double {
return self.__motionArtifactScore
}
@inline(__always)
var collectedSampleCount: Double {
return self.__collectedSampleCount
}
@inline(__always)
var usableDurationSeconds: Double {
return self.__usableDurationSeconds
}
@inline(__always)
var droppedFrameCount: Double {
return self.__droppedFrameCount
}
@inline(__always)
var thermalState: ThermalState {
return self.__thermalState
}
@inline(__always)
var waveformValue: Double {
return self.__waveformValue
}
@inline(__always)
var liveBpm: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__liveBpm) {
let __unwrapped = bridge.get_std__optional_double_(self.__liveBpm)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var liveIpiMs: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__liveIpiMs) {
let __unwrapped = bridge.get_std__optional_double_(self.__liveIpiMs)
return __unwrapped
} else {
return nil
}
}()
}
@inline(__always)
var beatSequence: Double {
return self.__beatSequence
}
@inline(__always)
var acceptedBeats: [NativeAcceptedBeat] {
return self.__acceptedBeats.map({ __item in __item })
}
@inline(__always)
var liveWindowQuality: Double? {
return { () -> Double? in
if bridge.has_value_std__optional_double_(self.__liveWindowQuality) {
let __unwrapped = bridge.get_std__optional_double_(self.__liveWindowQuality)
return __unwrapped
} else {
return nil
}
}()
}
}
@@ -0,0 +1,40 @@
///
/// PPGSessionPurpose.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
/**
* Represents the JS union `PPGSessionPurpose`, backed by a C++ enum.
*/
public typealias PPGSessionPurpose = margelo.nitro.haptika.ppg.PPGSessionPurpose
public extension PPGSessionPurpose {
/**
* Get a PPGSessionPurpose for the given String value, or
* return `nil` if the given value was invalid/unknown.
*/
init?(fromString string: String) {
switch string {
case "spotMeasurement":
self = .spotmeasurement
case "coherence":
self = .coherence
default:
return nil
}
}
/**
* Get the String value this PPGSessionPurpose represents.
*/
var stringValue: String {
switch self {
case .spotmeasurement:
return "spotMeasurement"
case .coherence:
return "coherence"
}
}
}
@@ -0,0 +1,52 @@
///
/// ThermalState.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
/**
* Represents the JS union `ThermalState`, backed by a C++ enum.
*/
public typealias ThermalState = margelo.nitro.haptika.ppg.ThermalState
public extension ThermalState {
/**
* Get a ThermalState for the given String value, or
* return `nil` if the given value was invalid/unknown.
*/
init?(fromString string: String) {
switch string {
case "nominal":
self = .nominal
case "fair":
self = .fair
case "serious":
self = .serious
case "critical":
self = .critical
case "unknown":
self = .unknown
default:
return nil
}
}
/**
* Get the String value this ThermalState represents.
*/
var stringValue: String {
switch self {
case .nominal:
return "nominal"
case .fair:
return "fair"
case .serious:
return "serious"
case .critical:
return "critical"
case .unknown:
return "unknown"
}
}
}
@@ -0,0 +1,92 @@
///
/// FingerContactState.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/NitroHash.hpp>)
#include <NitroModules/NitroHash.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
namespace margelo::nitro::haptika::ppg {
/**
* An enum which can be represented as a JavaScript union (FingerContactState).
*/
enum class FingerContactState {
ABSENT SWIFT_NAME(absent) = 0,
ADJUSTING SWIFT_NAME(adjusting) = 1,
VALID SWIFT_NAME(valid) = 2,
TOOMUCHPRESSURE SWIFT_NAME(toomuchpressure) = 3,
OVEREXPOSED SWIFT_NAME(overexposed) = 4,
UNSTABLE SWIFT_NAME(unstable) = 5,
} CLOSED_ENUM;
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ FingerContactState <> JS FingerContactState (union)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::FingerContactState> final {
static inline margelo::nitro::haptika::ppg::FingerContactState fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("absent"): return margelo::nitro::haptika::ppg::FingerContactState::ABSENT;
case hashString("adjusting"): return margelo::nitro::haptika::ppg::FingerContactState::ADJUSTING;
case hashString("valid"): return margelo::nitro::haptika::ppg::FingerContactState::VALID;
case hashString("tooMuchPressure"): return margelo::nitro::haptika::ppg::FingerContactState::TOOMUCHPRESSURE;
case hashString("overexposed"): return margelo::nitro::haptika::ppg::FingerContactState::OVEREXPOSED;
case hashString("unstable"): return margelo::nitro::haptika::ppg::FingerContactState::UNSTABLE;
default: [[unlikely]]
throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum FingerContactState - invalid value!");
}
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::haptika::ppg::FingerContactState arg) {
switch (arg) {
case margelo::nitro::haptika::ppg::FingerContactState::ABSENT: return JSIConverter<std::string>::toJSI(runtime, "absent");
case margelo::nitro::haptika::ppg::FingerContactState::ADJUSTING: return JSIConverter<std::string>::toJSI(runtime, "adjusting");
case margelo::nitro::haptika::ppg::FingerContactState::VALID: return JSIConverter<std::string>::toJSI(runtime, "valid");
case margelo::nitro::haptika::ppg::FingerContactState::TOOMUCHPRESSURE: return JSIConverter<std::string>::toJSI(runtime, "tooMuchPressure");
case margelo::nitro::haptika::ppg::FingerContactState::OVEREXPOSED: return JSIConverter<std::string>::toJSI(runtime, "overexposed");
case margelo::nitro::haptika::ppg::FingerContactState::UNSTABLE: return JSIConverter<std::string>::toJSI(runtime, "unstable");
default: [[unlikely]]
throw std::invalid_argument("Cannot convert FingerContactState to JS - invalid value: "
+ std::to_string(static_cast<int>(arg)) + "!");
}
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isString()) {
return false;
}
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("absent"):
case hashString("adjusting"):
case hashString("valid"):
case hashString("tooMuchPressure"):
case hashString("overexposed"):
case hashString("unstable"):
return true;
default:
return false;
}
}
};
} // namespace margelo::nitro
@@ -0,0 +1,80 @@
///
/// HealthMetricType.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/NitroHash.hpp>)
#include <NitroModules/NitroHash.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
namespace margelo::nitro::haptika::ppg {
/**
* An enum which can be represented as a JavaScript union (HealthMetricType).
*/
enum class HealthMetricType {
HEARTRATE SWIFT_NAME(heartrate) = 0,
RESTINGHEARTRATE SWIFT_NAME(restingheartrate) = 1,
HRVSDNN SWIFT_NAME(hrvsdnn) = 2,
} CLOSED_ENUM;
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ HealthMetricType <> JS HealthMetricType (union)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::HealthMetricType> final {
static inline margelo::nitro::haptika::ppg::HealthMetricType fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("heartRate"): return margelo::nitro::haptika::ppg::HealthMetricType::HEARTRATE;
case hashString("restingHeartRate"): return margelo::nitro::haptika::ppg::HealthMetricType::RESTINGHEARTRATE;
case hashString("hrvSdnn"): return margelo::nitro::haptika::ppg::HealthMetricType::HRVSDNN;
default: [[unlikely]]
throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum HealthMetricType - invalid value!");
}
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::haptika::ppg::HealthMetricType arg) {
switch (arg) {
case margelo::nitro::haptika::ppg::HealthMetricType::HEARTRATE: return JSIConverter<std::string>::toJSI(runtime, "heartRate");
case margelo::nitro::haptika::ppg::HealthMetricType::RESTINGHEARTRATE: return JSIConverter<std::string>::toJSI(runtime, "restingHeartRate");
case margelo::nitro::haptika::ppg::HealthMetricType::HRVSDNN: return JSIConverter<std::string>::toJSI(runtime, "hrvSdnn");
default: [[unlikely]]
throw std::invalid_argument("Cannot convert HealthMetricType to JS - invalid value: "
+ std::to_string(static_cast<int>(arg)) + "!");
}
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isString()) {
return false;
}
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("heartRate"):
case hashString("restingHeartRate"):
case hashString("hrvSdnn"):
return true;
default:
return false;
}
}
};
} // namespace margelo::nitro
@@ -0,0 +1,30 @@
///
/// HybridHaptikaCardiacMirrorSpec.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HybridHaptikaCardiacMirrorSpec.hpp"
namespace margelo::nitro::haptika::ppg {
void HybridHaptikaCardiacMirrorSpec::loadHybridMethods() {
// load base methods/properties
HybridObject::loadHybridMethods();
// load custom methods/properties
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("isSupported", &HybridHaptikaCardiacMirrorSpec::isSupported);
prototype.registerHybridMethod("patternInfo", &HybridHaptikaCardiacMirrorSpec::patternInfo);
prototype.registerHybridMethod("start", &HybridHaptikaCardiacMirrorSpec::start);
prototype.registerHybridMethod("pulse", &HybridHaptikaCardiacMirrorSpec::pulse);
prototype.registerHybridMethod("stop", &HybridHaptikaCardiacMirrorSpec::stop);
prototype.registerHybridMethod("startWatchSession", &HybridHaptikaCardiacMirrorSpec::startWatchSession);
prototype.registerHybridMethod("watchReachable", &HybridHaptikaCardiacMirrorSpec::watchReachable);
prototype.registerHybridMethod("latestHeartbeat", &HybridHaptikaCardiacMirrorSpec::latestHeartbeat);
prototype.registerHybridMethod("playRegulationProtocol", &HybridHaptikaCardiacMirrorSpec::playRegulationProtocol);
prototype.registerHybridMethod("stopRegulationProtocol", &HybridHaptikaCardiacMirrorSpec::stopRegulationProtocol);
});
}
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,77 @@
///
/// HybridHaptikaCardiacMirrorSpec.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/HybridObject.hpp>)
#include <NitroModules/HybridObject.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `NativeCardiacMirrorPatternInfo` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorPatternInfo; }
// Forward declaration of `NativeCardiacMirrorHeartbeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeCardiacMirrorHeartbeat; }
#include "NativeCardiacMirrorPatternInfo.hpp"
#include "NativeCardiacMirrorHeartbeat.hpp"
#include <optional>
#include <string>
namespace margelo::nitro::haptika::ppg {
using namespace margelo::nitro;
/**
* An abstract base class for `HaptikaCardiacMirror`
* Inherit this class to create instances of `HybridHaptikaCardiacMirrorSpec` in C++.
* You must explicitly call `HybridObject`'s constructor yourself, because it is virtual.
* @example
* ```cpp
* class HybridHaptikaCardiacMirror: public HybridHaptikaCardiacMirrorSpec {
* public:
* HybridHaptikaCardiacMirror(...): HybridObject(TAG) { ... }
* // ...
* };
* ```
*/
class HybridHaptikaCardiacMirrorSpec: public virtual HybridObject {
public:
// Constructor
explicit HybridHaptikaCardiacMirrorSpec(): HybridObject(TAG) { }
// Destructor
~HybridHaptikaCardiacMirrorSpec() override = default;
public:
// Properties
public:
// Methods
virtual bool isSupported() = 0;
virtual NativeCardiacMirrorPatternInfo patternInfo() = 0;
virtual void start(double targetBpm) = 0;
virtual void pulse() = 0;
virtual void stop() = 0;
virtual void startWatchSession() = 0;
virtual bool watchReachable() = 0;
virtual std::optional<NativeCardiacMirrorHeartbeat> latestHeartbeat() = 0;
virtual double playRegulationProtocol(const std::string& kind) = 0;
virtual void stopRegulationProtocol() = 0;
protected:
// Hybrid Setup
void loadHybridMethods() override;
protected:
// Tag for logging
static constexpr auto TAG = "HaptikaCardiacMirror";
};
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,24 @@
///
/// HybridHaptikaHealthKitSpec.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HybridHaptikaHealthKitSpec.hpp"
namespace margelo::nitro::haptika::ppg {
void HybridHaptikaHealthKitSpec::loadHybridMethods() {
// load base methods/properties
HybridObject::loadHybridMethods();
// load custom methods/properties
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("isAvailable", &HybridHaptikaHealthKitSpec::isAvailable);
prototype.registerHybridMethod("requestReadAuthorization", &HybridHaptikaHealthKitSpec::requestReadAuthorization);
prototype.registerHybridMethod("fetchContext", &HybridHaptikaHealthKitSpec::fetchContext);
prototype.registerHybridMethod("fetchLatestHeartbeatSeries", &HybridHaptikaHealthKitSpec::fetchLatestHeartbeatSeries);
});
}
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,71 @@
///
/// HybridHaptikaHealthKitSpec.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/HybridObject.hpp>)
#include <NitroModules/HybridObject.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `NativeHealthKitContext` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthKitContext; }
// Forward declaration of `NativeHeartbeatSeriesSummary` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHeartbeatSeriesSummary; }
#include <NitroModules/Promise.hpp>
#include "NativeHealthKitContext.hpp"
#include "NativeHeartbeatSeriesSummary.hpp"
#include <optional>
namespace margelo::nitro::haptika::ppg {
using namespace margelo::nitro;
/**
* An abstract base class for `HaptikaHealthKit`
* Inherit this class to create instances of `HybridHaptikaHealthKitSpec` in C++.
* You must explicitly call `HybridObject`'s constructor yourself, because it is virtual.
* @example
* ```cpp
* class HybridHaptikaHealthKit: public HybridHaptikaHealthKitSpec {
* public:
* HybridHaptikaHealthKit(...): HybridObject(TAG) { ... }
* // ...
* };
* ```
*/
class HybridHaptikaHealthKitSpec: public virtual HybridObject {
public:
// Constructor
explicit HybridHaptikaHealthKitSpec(): HybridObject(TAG) { }
// Destructor
~HybridHaptikaHealthKitSpec() override = default;
public:
// Properties
public:
// Methods
virtual bool isAvailable() = 0;
virtual std::shared_ptr<Promise<bool>> requestReadAuthorization() = 0;
virtual std::shared_ptr<Promise<NativeHealthKitContext>> fetchContext(double lookbackDays) = 0;
virtual std::shared_ptr<Promise<std::optional<NativeHeartbeatSeriesSummary>>> fetchLatestHeartbeatSeries() = 0;
protected:
// Hybrid Setup
void loadHybridMethods() override;
protected:
// Tag for logging
static constexpr auto TAG = "HaptikaHealthKit";
};
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,25 @@
///
/// HybridHaptikaPPGFrameProcessorSpec.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "HybridHaptikaPPGFrameProcessorSpec.hpp"
namespace margelo::nitro::haptika::ppg {
void HybridHaptikaPPGFrameProcessorSpec::loadHybridMethods() {
// load base methods/properties
HybridObject::loadHybridMethods();
// load custom methods/properties
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("startMeasurement", &HybridHaptikaPPGFrameProcessorSpec::startMeasurement);
prototype.registerHybridMethod("processFrame", &HybridHaptikaPPGFrameProcessorSpec::processFrame);
prototype.registerHybridMethod("finishMeasurement", &HybridHaptikaPPGFrameProcessorSpec::finishMeasurement);
prototype.registerHybridMethod("cancelMeasurement", &HybridHaptikaPPGFrameProcessorSpec::cancelMeasurement);
prototype.registerHybridMethod("getThermalState", &HybridHaptikaPPGFrameProcessorSpec::getThermalState);
});
}
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,81 @@
///
/// HybridHaptikaPPGFrameProcessorSpec.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/HybridObject.hpp>)
#include <NitroModules/HybridObject.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `NativePPGConfiguration` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGConfiguration; }
// Forward declaration of `PPGFrameUpdate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct PPGFrameUpdate; }
// Forward declaration of `HybridFrameSpec` to properly resolve imports.
namespace margelo::nitro::camera { class HybridFrameSpec; }
// Forward declaration of `NativePPGProcessingResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePPGProcessingResult; }
// Forward declaration of `ThermalState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class ThermalState; }
#include "NativePPGConfiguration.hpp"
#include "PPGFrameUpdate.hpp"
#include <optional>
#include <memory>
#include <VisionCamera/HybridFrameSpec.hpp>
#include "NativePPGProcessingResult.hpp"
#include "ThermalState.hpp"
namespace margelo::nitro::haptika::ppg {
using namespace margelo::nitro;
/**
* An abstract base class for `HaptikaPPGFrameProcessor`
* Inherit this class to create instances of `HybridHaptikaPPGFrameProcessorSpec` in C++.
* You must explicitly call `HybridObject`'s constructor yourself, because it is virtual.
* @example
* ```cpp
* class HybridHaptikaPPGFrameProcessor: public HybridHaptikaPPGFrameProcessorSpec {
* public:
* HybridHaptikaPPGFrameProcessor(...): HybridObject(TAG) { ... }
* // ...
* };
* ```
*/
class HybridHaptikaPPGFrameProcessorSpec: public virtual HybridObject {
public:
// Constructor
explicit HybridHaptikaPPGFrameProcessorSpec(): HybridObject(TAG) { }
// Destructor
~HybridHaptikaPPGFrameProcessorSpec() override = default;
public:
// Properties
public:
// Methods
virtual void startMeasurement(const NativePPGConfiguration& configuration) = 0;
virtual std::optional<PPGFrameUpdate> processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
virtual NativePPGProcessingResult finishMeasurement() = 0;
virtual void cancelMeasurement() = 0;
virtual ThermalState getThermalState() = 0;
protected:
// Hybrid Setup
void loadHybridMethods() override;
protected:
// Tag for logging
static constexpr auto TAG = "HaptikaPPGFrameProcessor";
};
} // namespace margelo::nitro::haptika::ppg
@@ -0,0 +1,95 @@
///
/// NativeAcceptedBeat.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeAcceptedBeat).
*/
struct NativeAcceptedBeat final {
public:
double sequence SWIFT_PRIVATE;
double timestampSeconds SWIFT_PRIVATE;
double intervalMs SWIFT_PRIVATE;
double confidence SWIFT_PRIVATE;
public:
NativeAcceptedBeat() = default;
explicit NativeAcceptedBeat(double sequence, double timestampSeconds, double intervalMs, double confidence): sequence(sequence), timestampSeconds(timestampSeconds), intervalMs(intervalMs), confidence(confidence) {}
public:
friend bool operator==(const NativeAcceptedBeat& lhs, const NativeAcceptedBeat& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeAcceptedBeat <> JS NativeAcceptedBeat (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeAcceptedBeat> final {
static inline margelo::nitro::haptika::ppg::NativeAcceptedBeat fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeAcceptedBeat(
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sequence"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "intervalMs"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "confidence")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeAcceptedBeat& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "sequence"), JSIConverter<double>::toJSI(runtime, arg.sequence));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds"), JSIConverter<double>::toJSI(runtime, arg.timestampSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "intervalMs"), JSIConverter<double>::toJSI(runtime, arg.intervalMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "confidence"), JSIConverter<double>::toJSI(runtime, arg.confidence));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sequence")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "intervalMs")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "confidence")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,112 @@
///
/// NativeBPMEstimate.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <optional>
#include <string>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeBPMEstimate).
*/
struct NativeBPMEstimate final {
public:
std::optional<double> bpm SWIFT_PRIVATE;
std::optional<double> temporalBpm SWIFT_PRIVATE;
std::optional<double> frequencyBpm SWIFT_PRIVATE;
std::optional<double> methodAgreementBpm SWIFT_PRIVATE;
double validPeakCount SWIFT_PRIVATE;
double rejectedPeakCount SWIFT_PRIVATE;
double confidence SWIFT_PRIVATE;
std::string selectedChannel SWIFT_PRIVATE;
public:
NativeBPMEstimate() = default;
explicit NativeBPMEstimate(std::optional<double> bpm, std::optional<double> temporalBpm, std::optional<double> frequencyBpm, std::optional<double> methodAgreementBpm, double validPeakCount, double rejectedPeakCount, double confidence, std::string selectedChannel): bpm(bpm), temporalBpm(temporalBpm), frequencyBpm(frequencyBpm), methodAgreementBpm(methodAgreementBpm), validPeakCount(validPeakCount), rejectedPeakCount(rejectedPeakCount), confidence(confidence), selectedChannel(selectedChannel) {}
public:
friend bool operator==(const NativeBPMEstimate& lhs, const NativeBPMEstimate& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeBPMEstimate <> JS NativeBPMEstimate (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeBPMEstimate> final {
static inline margelo::nitro::haptika::ppg::NativeBPMEstimate fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeBPMEstimate(
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bpm"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "temporalBpm"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "frequencyBpm"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "methodAgreementBpm"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "validPeakCount"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedPeakCount"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "confidence"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selectedChannel")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeBPMEstimate& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "bpm"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.bpm));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "temporalBpm"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.temporalBpm));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "frequencyBpm"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.frequencyBpm));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "methodAgreementBpm"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.methodAgreementBpm));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "validPeakCount"), JSIConverter<double>::toJSI(runtime, arg.validPeakCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "rejectedPeakCount"), JSIConverter<double>::toJSI(runtime, arg.rejectedPeakCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "confidence"), JSIConverter<double>::toJSI(runtime, arg.confidence));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "selectedChannel"), JSIConverter<std::string>::toJSI(runtime, arg.selectedChannel));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bpm")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "temporalBpm")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "frequencyBpm")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "methodAgreementBpm")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "validPeakCount")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedPeakCount")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "confidence")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selectedChannel")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,87 @@
///
/// NativeCardiacMirrorHeartbeat.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeCardiacMirrorHeartbeat).
*/
struct NativeCardiacMirrorHeartbeat final {
public:
double timestampSeconds SWIFT_PRIVATE;
double bpm SWIFT_PRIVATE;
public:
NativeCardiacMirrorHeartbeat() = default;
explicit NativeCardiacMirrorHeartbeat(double timestampSeconds, double bpm): timestampSeconds(timestampSeconds), bpm(bpm) {}
public:
friend bool operator==(const NativeCardiacMirrorHeartbeat& lhs, const NativeCardiacMirrorHeartbeat& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeCardiacMirrorHeartbeat <> JS NativeCardiacMirrorHeartbeat (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeCardiacMirrorHeartbeat> final {
static inline margelo::nitro::haptika::ppg::NativeCardiacMirrorHeartbeat fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeCardiacMirrorHeartbeat(
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bpm")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeCardiacMirrorHeartbeat& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds"), JSIConverter<double>::toJSI(runtime, arg.timestampSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "bpm"), JSIConverter<double>::toJSI(runtime, arg.bpm));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bpm")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,91 @@
///
/// NativeCardiacMirrorPatternInfo.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeCardiacMirrorPatternInfo).
*/
struct NativeCardiacMirrorPatternInfo final {
public:
std::string id SWIFT_PRIVATE;
std::string version SWIFT_PRIVATE;
double latencyBudgetMs SWIFT_PRIVATE;
public:
NativeCardiacMirrorPatternInfo() = default;
explicit NativeCardiacMirrorPatternInfo(std::string id, std::string version, double latencyBudgetMs): id(id), version(version), latencyBudgetMs(latencyBudgetMs) {}
public:
friend bool operator==(const NativeCardiacMirrorPatternInfo& lhs, const NativeCardiacMirrorPatternInfo& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeCardiacMirrorPatternInfo <> JS NativeCardiacMirrorPatternInfo (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeCardiacMirrorPatternInfo> final {
static inline margelo::nitro::haptika::ppg::NativeCardiacMirrorPatternInfo fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeCardiacMirrorPatternInfo(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "version"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "latencyBudgetMs")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeCardiacMirrorPatternInfo& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter<std::string>::toJSI(runtime, arg.id));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "version"), JSIConverter<std::string>::toJSI(runtime, arg.version));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "latencyBudgetMs"), JSIConverter<double>::toJSI(runtime, arg.latencyBudgetMs));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "version")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "latencyBudgetMs")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,111 @@
///
/// NativeHealthKitContext.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `NativeHealthMetricSample` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeHealthMetricSample; }
#include "NativeHealthMetricSample.hpp"
#include <optional>
#include <vector>
#include <string>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeHealthKitContext).
*/
struct NativeHealthKitContext final {
public:
bool available SWIFT_PRIVATE;
bool authorized SWIFT_PRIVATE;
std::optional<NativeHealthMetricSample> latestRestingHeartRate SWIFT_PRIVATE;
std::optional<NativeHealthMetricSample> latestHrvSdnn SWIFT_PRIVATE;
std::vector<NativeHealthMetricSample> recentHeartRateSamples SWIFT_PRIVATE;
double contextConfidence SWIFT_PRIVATE;
std::string fetchedAt SWIFT_PRIVATE;
public:
NativeHealthKitContext() = default;
explicit NativeHealthKitContext(bool available, bool authorized, std::optional<NativeHealthMetricSample> latestRestingHeartRate, std::optional<NativeHealthMetricSample> latestHrvSdnn, std::vector<NativeHealthMetricSample> recentHeartRateSamples, double contextConfidence, std::string fetchedAt): available(available), authorized(authorized), latestRestingHeartRate(latestRestingHeartRate), latestHrvSdnn(latestHrvSdnn), recentHeartRateSamples(recentHeartRateSamples), contextConfidence(contextConfidence), fetchedAt(fetchedAt) {}
public:
friend bool operator==(const NativeHealthKitContext& lhs, const NativeHealthKitContext& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeHealthKitContext <> JS NativeHealthKitContext (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeHealthKitContext> final {
static inline margelo::nitro::haptika::ppg::NativeHealthKitContext fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeHealthKitContext(
JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "available"))),
JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "authorized"))),
JSIConverter<std::optional<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "latestRestingHeartRate"))),
JSIConverter<std::optional<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "latestHrvSdnn"))),
JSIConverter<std::vector<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "recentHeartRateSamples"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "contextConfidence"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "fetchedAt")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeHealthKitContext& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "available"), JSIConverter<bool>::toJSI(runtime, arg.available));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "authorized"), JSIConverter<bool>::toJSI(runtime, arg.authorized));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "latestRestingHeartRate"), JSIConverter<std::optional<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::toJSI(runtime, arg.latestRestingHeartRate));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "latestHrvSdnn"), JSIConverter<std::optional<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::toJSI(runtime, arg.latestHrvSdnn));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "recentHeartRateSamples"), JSIConverter<std::vector<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::toJSI(runtime, arg.recentHeartRateSamples));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "contextConfidence"), JSIConverter<double>::toJSI(runtime, arg.contextConfidence));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "fetchedAt"), JSIConverter<std::string>::toJSI(runtime, arg.fetchedAt));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "available")))) return false;
if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "authorized")))) return false;
if (!JSIConverter<std::optional<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "latestRestingHeartRate")))) return false;
if (!JSIConverter<std::optional<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "latestHrvSdnn")))) return false;
if (!JSIConverter<std::vector<margelo::nitro::haptika::ppg::NativeHealthMetricSample>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "recentHeartRateSamples")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "contextConfidence")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "fetchedAt")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,114 @@
///
/// NativeHealthMetricSample.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `HealthMetricType` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class HealthMetricType; }
#include "HealthMetricType.hpp"
#include <string>
#include <optional>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeHealthMetricSample).
*/
struct NativeHealthMetricSample final {
public:
HealthMetricType type SWIFT_PRIVATE;
double value SWIFT_PRIVATE;
std::string unit SWIFT_PRIVATE;
std::string startDate SWIFT_PRIVATE;
std::string endDate SWIFT_PRIVATE;
std::optional<std::string> sourceName SWIFT_PRIVATE;
std::optional<std::string> sourceBundleIdentifier SWIFT_PRIVATE;
std::optional<std::string> deviceName SWIFT_PRIVATE;
public:
NativeHealthMetricSample() = default;
explicit NativeHealthMetricSample(HealthMetricType type, double value, std::string unit, std::string startDate, std::string endDate, std::optional<std::string> sourceName, std::optional<std::string> sourceBundleIdentifier, std::optional<std::string> deviceName): type(type), value(value), unit(unit), startDate(startDate), endDate(endDate), sourceName(sourceName), sourceBundleIdentifier(sourceBundleIdentifier), deviceName(deviceName) {}
public:
friend bool operator==(const NativeHealthMetricSample& lhs, const NativeHealthMetricSample& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeHealthMetricSample <> JS NativeHealthMetricSample (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeHealthMetricSample> final {
static inline margelo::nitro::haptika::ppg::NativeHealthMetricSample fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeHealthMetricSample(
JSIConverter<margelo::nitro::haptika::ppg::HealthMetricType>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "type"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "value"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "unit"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startDate"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "endDate"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceName"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceBundleIdentifier"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "deviceName")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeHealthMetricSample& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "type"), JSIConverter<margelo::nitro::haptika::ppg::HealthMetricType>::toJSI(runtime, arg.type));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "value"), JSIConverter<double>::toJSI(runtime, arg.value));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "unit"), JSIConverter<std::string>::toJSI(runtime, arg.unit));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "startDate"), JSIConverter<std::string>::toJSI(runtime, arg.startDate));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "endDate"), JSIConverter<std::string>::toJSI(runtime, arg.endDate));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "sourceName"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.sourceName));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "sourceBundleIdentifier"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.sourceBundleIdentifier));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "deviceName"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.deviceName));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<margelo::nitro::haptika::ppg::HealthMetricType>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "type")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "value")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "unit")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startDate")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "endDate")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceName")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceBundleIdentifier")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "deviceName")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,105 @@
///
/// NativeHeartbeatSeriesSummary.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
#include <optional>
#include <vector>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeHeartbeatSeriesSummary).
*/
struct NativeHeartbeatSeriesSummary final {
public:
std::string startDate SWIFT_PRIVATE;
std::string endDate SWIFT_PRIVATE;
std::optional<std::string> sourceName SWIFT_PRIVATE;
std::optional<std::string> sourceBundleIdentifier SWIFT_PRIVATE;
std::vector<double> heartbeatTimesSinceSeriesStartSeconds SWIFT_PRIVATE;
std::vector<double> precededByGapIndexes SWIFT_PRIVATE;
public:
NativeHeartbeatSeriesSummary() = default;
explicit NativeHeartbeatSeriesSummary(std::string startDate, std::string endDate, std::optional<std::string> sourceName, std::optional<std::string> sourceBundleIdentifier, std::vector<double> heartbeatTimesSinceSeriesStartSeconds, std::vector<double> precededByGapIndexes): startDate(startDate), endDate(endDate), sourceName(sourceName), sourceBundleIdentifier(sourceBundleIdentifier), heartbeatTimesSinceSeriesStartSeconds(heartbeatTimesSinceSeriesStartSeconds), precededByGapIndexes(precededByGapIndexes) {}
public:
friend bool operator==(const NativeHeartbeatSeriesSummary& lhs, const NativeHeartbeatSeriesSummary& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeHeartbeatSeriesSummary <> JS NativeHeartbeatSeriesSummary (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeHeartbeatSeriesSummary> final {
static inline margelo::nitro::haptika::ppg::NativeHeartbeatSeriesSummary fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeHeartbeatSeriesSummary(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startDate"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "endDate"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceName"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceBundleIdentifier"))),
JSIConverter<std::vector<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "heartbeatTimesSinceSeriesStartSeconds"))),
JSIConverter<std::vector<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "precededByGapIndexes")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeHeartbeatSeriesSummary& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "startDate"), JSIConverter<std::string>::toJSI(runtime, arg.startDate));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "endDate"), JSIConverter<std::string>::toJSI(runtime, arg.endDate));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "sourceName"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.sourceName));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "sourceBundleIdentifier"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.sourceBundleIdentifier));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "heartbeatTimesSinceSeriesStartSeconds"), JSIConverter<std::vector<double>>::toJSI(runtime, arg.heartbeatTimesSinceSeriesStartSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "precededByGapIndexes"), JSIConverter<std::vector<double>>::toJSI(runtime, arg.precededByGapIndexes));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startDate")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "endDate")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceName")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceBundleIdentifier")))) return false;
if (!JSIConverter<std::vector<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "heartbeatTimesSinceSeriesStartSeconds")))) return false;
if (!JSIConverter<std::vector<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "precededByGapIndexes")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,112 @@
///
/// NativePPGConfiguration.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `PPGSessionPurpose` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class PPGSessionPurpose; }
#include "PPGSessionPurpose.hpp"
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativePPGConfiguration).
*/
struct NativePPGConfiguration final {
public:
PPGSessionPurpose sessionPurpose SWIFT_PRIVATE;
double targetSampleRate SWIFT_PRIVATE;
double minimumFrequencyHz SWIFT_PRIVATE;
double maximumFrequencyHz SWIFT_PRIVATE;
double stabilizationSeconds SWIFT_PRIVATE;
double measurementSeconds SWIFT_PRIVATE;
double emitEveryFrames SWIFT_PRIVATE;
double minimumContactConfidence SWIFT_PRIVATE;
public:
NativePPGConfiguration() = default;
explicit NativePPGConfiguration(PPGSessionPurpose sessionPurpose, double targetSampleRate, double minimumFrequencyHz, double maximumFrequencyHz, double stabilizationSeconds, double measurementSeconds, double emitEveryFrames, double minimumContactConfidence): sessionPurpose(sessionPurpose), targetSampleRate(targetSampleRate), minimumFrequencyHz(minimumFrequencyHz), maximumFrequencyHz(maximumFrequencyHz), stabilizationSeconds(stabilizationSeconds), measurementSeconds(measurementSeconds), emitEveryFrames(emitEveryFrames), minimumContactConfidence(minimumContactConfidence) {}
public:
friend bool operator==(const NativePPGConfiguration& lhs, const NativePPGConfiguration& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativePPGConfiguration <> JS NativePPGConfiguration (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativePPGConfiguration> final {
static inline margelo::nitro::haptika::ppg::NativePPGConfiguration fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativePPGConfiguration(
JSIConverter<margelo::nitro::haptika::ppg::PPGSessionPurpose>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sessionPurpose"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "targetSampleRate"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "minimumFrequencyHz"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maximumFrequencyHz"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stabilizationSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "measurementSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "emitEveryFrames"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "minimumContactConfidence")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativePPGConfiguration& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "sessionPurpose"), JSIConverter<margelo::nitro::haptika::ppg::PPGSessionPurpose>::toJSI(runtime, arg.sessionPurpose));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "targetSampleRate"), JSIConverter<double>::toJSI(runtime, arg.targetSampleRate));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "minimumFrequencyHz"), JSIConverter<double>::toJSI(runtime, arg.minimumFrequencyHz));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "maximumFrequencyHz"), JSIConverter<double>::toJSI(runtime, arg.maximumFrequencyHz));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "stabilizationSeconds"), JSIConverter<double>::toJSI(runtime, arg.stabilizationSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "measurementSeconds"), JSIConverter<double>::toJSI(runtime, arg.measurementSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "emitEveryFrames"), JSIConverter<double>::toJSI(runtime, arg.emitEveryFrames));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "minimumContactConfidence"), JSIConverter<double>::toJSI(runtime, arg.minimumContactConfidence));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<margelo::nitro::haptika::ppg::PPGSessionPurpose>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sessionPurpose")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "targetSampleRate")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "minimumFrequencyHz")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maximumFrequencyHz")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stabilizationSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "measurementSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "emitEveryFrames")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "minimumContactConfidence")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,124 @@
///
/// NativePPGProcessingResult.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `NativeBPMEstimate` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeBPMEstimate; }
// Forward declaration of `NativePRVMetrics` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativePRVMetrics; }
// Forward declaration of `NativeSignalQualityResult` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeSignalQualityResult; }
#include <string>
#include <optional>
#include "NativeBPMEstimate.hpp"
#include "NativePRVMetrics.hpp"
#include "NativeSignalQualityResult.hpp"
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativePPGProcessingResult).
*/
struct NativePPGProcessingResult final {
public:
bool valid SWIFT_PRIVATE;
std::optional<std::string> invalidReason SWIFT_PRIVATE;
NativeBPMEstimate bpm SWIFT_PRIVATE;
NativePRVMetrics prv SWIFT_PRIVATE;
NativeSignalQualityResult quality SWIFT_PRIVATE;
double startedAtMonotonicSeconds SWIFT_PRIVATE;
double endedAtMonotonicSeconds SWIFT_PRIVATE;
double rawSampleCount SWIFT_PRIVATE;
double resampledSampleCount SWIFT_PRIVATE;
public:
NativePPGProcessingResult() = default;
explicit NativePPGProcessingResult(bool valid, std::optional<std::string> invalidReason, NativeBPMEstimate bpm, NativePRVMetrics prv, NativeSignalQualityResult quality, double startedAtMonotonicSeconds, double endedAtMonotonicSeconds, double rawSampleCount, double resampledSampleCount): valid(valid), invalidReason(invalidReason), bpm(bpm), prv(prv), quality(quality), startedAtMonotonicSeconds(startedAtMonotonicSeconds), endedAtMonotonicSeconds(endedAtMonotonicSeconds), rawSampleCount(rawSampleCount), resampledSampleCount(resampledSampleCount) {}
public:
friend bool operator==(const NativePPGProcessingResult& lhs, const NativePPGProcessingResult& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativePPGProcessingResult <> JS NativePPGProcessingResult (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativePPGProcessingResult> final {
static inline margelo::nitro::haptika::ppg::NativePPGProcessingResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativePPGProcessingResult(
JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "valid"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "invalidReason"))),
JSIConverter<margelo::nitro::haptika::ppg::NativeBPMEstimate>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bpm"))),
JSIConverter<margelo::nitro::haptika::ppg::NativePRVMetrics>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "prv"))),
JSIConverter<margelo::nitro::haptika::ppg::NativeSignalQualityResult>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "quality"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startedAtMonotonicSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "endedAtMonotonicSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rawSampleCount"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "resampledSampleCount")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativePPGProcessingResult& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "valid"), JSIConverter<bool>::toJSI(runtime, arg.valid));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "invalidReason"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.invalidReason));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "bpm"), JSIConverter<margelo::nitro::haptika::ppg::NativeBPMEstimate>::toJSI(runtime, arg.bpm));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "prv"), JSIConverter<margelo::nitro::haptika::ppg::NativePRVMetrics>::toJSI(runtime, arg.prv));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "quality"), JSIConverter<margelo::nitro::haptika::ppg::NativeSignalQualityResult>::toJSI(runtime, arg.quality));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "startedAtMonotonicSeconds"), JSIConverter<double>::toJSI(runtime, arg.startedAtMonotonicSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "endedAtMonotonicSeconds"), JSIConverter<double>::toJSI(runtime, arg.endedAtMonotonicSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "rawSampleCount"), JSIConverter<double>::toJSI(runtime, arg.rawSampleCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "resampledSampleCount"), JSIConverter<double>::toJSI(runtime, arg.resampledSampleCount));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "valid")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "invalidReason")))) return false;
if (!JSIConverter<margelo::nitro::haptika::ppg::NativeBPMEstimate>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bpm")))) return false;
if (!JSIConverter<margelo::nitro::haptika::ppg::NativePRVMetrics>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "prv")))) return false;
if (!JSIConverter<margelo::nitro::haptika::ppg::NativeSignalQualityResult>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "quality")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startedAtMonotonicSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "endedAtMonotonicSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rawSampleCount")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "resampledSampleCount")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,131 @@
///
/// NativePRVMetrics.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <optional>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativePRVMetrics).
*/
struct NativePRVMetrics final {
public:
std::optional<double> estimatedRmssdMs SWIFT_PRIVATE;
std::optional<double> estimatedSdnnMs SWIFT_PRIVATE;
std::optional<double> meanIpiMs SWIFT_PRIVATE;
std::optional<double> medianIpiMs SWIFT_PRIVATE;
std::optional<double> pnn50Percent SWIFT_PRIVATE;
std::optional<double> rangeIpiMs SWIFT_PRIVATE;
std::optional<double> modeIpiMs SWIFT_PRIVATE;
std::optional<double> amo50Percent SWIFT_PRIVATE;
std::optional<double> coefficientOfVariation SWIFT_PRIVATE;
double validIntervalCount SWIFT_PRIVATE;
double rejectedIntervalCount SWIFT_PRIVATE;
double rejectedIntervalRatio SWIFT_PRIVATE;
bool reliable SWIFT_PRIVATE;
public:
NativePRVMetrics() = default;
explicit NativePRVMetrics(std::optional<double> estimatedRmssdMs, std::optional<double> estimatedSdnnMs, std::optional<double> meanIpiMs, std::optional<double> medianIpiMs, std::optional<double> pnn50Percent, std::optional<double> rangeIpiMs, std::optional<double> modeIpiMs, std::optional<double> amo50Percent, std::optional<double> coefficientOfVariation, double validIntervalCount, double rejectedIntervalCount, double rejectedIntervalRatio, bool reliable): estimatedRmssdMs(estimatedRmssdMs), estimatedSdnnMs(estimatedSdnnMs), meanIpiMs(meanIpiMs), medianIpiMs(medianIpiMs), pnn50Percent(pnn50Percent), rangeIpiMs(rangeIpiMs), modeIpiMs(modeIpiMs), amo50Percent(amo50Percent), coefficientOfVariation(coefficientOfVariation), validIntervalCount(validIntervalCount), rejectedIntervalCount(rejectedIntervalCount), rejectedIntervalRatio(rejectedIntervalRatio), reliable(reliable) {}
public:
friend bool operator==(const NativePRVMetrics& lhs, const NativePRVMetrics& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativePRVMetrics <> JS NativePRVMetrics (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativePRVMetrics> final {
static inline margelo::nitro::haptika::ppg::NativePRVMetrics fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativePRVMetrics(
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "estimatedRmssdMs"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "estimatedSdnnMs"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "meanIpiMs"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "medianIpiMs"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "pnn50Percent"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rangeIpiMs"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "modeIpiMs"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "amo50Percent"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "coefficientOfVariation"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "validIntervalCount"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalCount"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalRatio"))),
JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "reliable")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativePRVMetrics& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "estimatedRmssdMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.estimatedRmssdMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "estimatedSdnnMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.estimatedSdnnMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "meanIpiMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.meanIpiMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "medianIpiMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.medianIpiMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "pnn50Percent"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.pnn50Percent));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "rangeIpiMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.rangeIpiMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "modeIpiMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.modeIpiMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "amo50Percent"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.amo50Percent));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "coefficientOfVariation"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.coefficientOfVariation));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "validIntervalCount"), JSIConverter<double>::toJSI(runtime, arg.validIntervalCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalCount"), JSIConverter<double>::toJSI(runtime, arg.rejectedIntervalCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalRatio"), JSIConverter<double>::toJSI(runtime, arg.rejectedIntervalRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "reliable"), JSIConverter<bool>::toJSI(runtime, arg.reliable));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "estimatedRmssdMs")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "estimatedSdnnMs")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "meanIpiMs")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "medianIpiMs")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "pnn50Percent")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rangeIpiMs")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "modeIpiMs")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "amo50Percent")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "coefficientOfVariation")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "validIntervalCount")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalCount")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalRatio")))) return false;
if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "reliable")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,129 @@
///
/// NativeSignalQualityResult.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
#include <optional>
#include <vector>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (NativeSignalQualityResult).
*/
struct NativeSignalQualityResult final {
public:
double score SWIFT_PRIVATE;
std::string level SWIFT_PRIVATE;
double usableDurationSeconds SWIFT_PRIVATE;
double droppedFrameRatio SWIFT_PRIVATE;
double saturationRatio SWIFT_PRIVATE;
double rejectedIntervalRatio SWIFT_PRIVATE;
std::optional<double> signalToNoiseRatio SWIFT_PRIVATE;
double motionArtifactRatio SWIFT_PRIVATE;
double methodAgreementScore SWIFT_PRIVATE;
double timestampJitterRatio SWIFT_PRIVATE;
double longestGapSeconds SWIFT_PRIVATE;
std::vector<std::string> issues SWIFT_PRIVATE;
public:
NativeSignalQualityResult() = default;
explicit NativeSignalQualityResult(double score, std::string level, double usableDurationSeconds, double droppedFrameRatio, double saturationRatio, double rejectedIntervalRatio, std::optional<double> signalToNoiseRatio, double motionArtifactRatio, double methodAgreementScore, double timestampJitterRatio, double longestGapSeconds, std::vector<std::string> issues): score(score), level(level), usableDurationSeconds(usableDurationSeconds), droppedFrameRatio(droppedFrameRatio), saturationRatio(saturationRatio), rejectedIntervalRatio(rejectedIntervalRatio), signalToNoiseRatio(signalToNoiseRatio), motionArtifactRatio(motionArtifactRatio), methodAgreementScore(methodAgreementScore), timestampJitterRatio(timestampJitterRatio), longestGapSeconds(longestGapSeconds), issues(issues) {}
public:
friend bool operator==(const NativeSignalQualityResult& lhs, const NativeSignalQualityResult& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ NativeSignalQualityResult <> JS NativeSignalQualityResult (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::NativeSignalQualityResult> final {
static inline margelo::nitro::haptika::ppg::NativeSignalQualityResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::NativeSignalQualityResult(
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "score"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "level"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "usableDurationSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "droppedFrameRatio"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "saturationRatio"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalRatio"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "signalToNoiseRatio"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "motionArtifactRatio"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "methodAgreementScore"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampJitterRatio"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "longestGapSeconds"))),
JSIConverter<std::vector<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "issues")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::NativeSignalQualityResult& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "score"), JSIConverter<double>::toJSI(runtime, arg.score));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "level"), JSIConverter<std::string>::toJSI(runtime, arg.level));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "usableDurationSeconds"), JSIConverter<double>::toJSI(runtime, arg.usableDurationSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "droppedFrameRatio"), JSIConverter<double>::toJSI(runtime, arg.droppedFrameRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "saturationRatio"), JSIConverter<double>::toJSI(runtime, arg.saturationRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalRatio"), JSIConverter<double>::toJSI(runtime, arg.rejectedIntervalRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "signalToNoiseRatio"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.signalToNoiseRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "motionArtifactRatio"), JSIConverter<double>::toJSI(runtime, arg.motionArtifactRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "methodAgreementScore"), JSIConverter<double>::toJSI(runtime, arg.methodAgreementScore));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "timestampJitterRatio"), JSIConverter<double>::toJSI(runtime, arg.timestampJitterRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "longestGapSeconds"), JSIConverter<double>::toJSI(runtime, arg.longestGapSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "issues"), JSIConverter<std::vector<std::string>>::toJSI(runtime, arg.issues));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "score")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "level")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "usableDurationSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "droppedFrameRatio")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "saturationRatio")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rejectedIntervalRatio")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "signalToNoiseRatio")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "motionArtifactRatio")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "methodAgreementScore")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampJitterRatio")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "longestGapSeconds")))) return false;
if (!JSIConverter<std::vector<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "issues")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,152 @@
///
/// PPGFrameUpdate.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
// Forward declaration of `FingerContactState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class FingerContactState; }
// Forward declaration of `ThermalState` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { enum class ThermalState; }
// Forward declaration of `NativeAcceptedBeat` to properly resolve imports.
namespace margelo::nitro::haptika::ppg { struct NativeAcceptedBeat; }
#include "FingerContactState.hpp"
#include "ThermalState.hpp"
#include <optional>
#include "NativeAcceptedBeat.hpp"
#include <vector>
namespace margelo::nitro::haptika::ppg {
/**
* A struct which can be represented as a JavaScript object (PPGFrameUpdate).
*/
struct PPGFrameUpdate final {
public:
double timestampSeconds SWIFT_PRIVATE;
FingerContactState contactState SWIFT_PRIVATE;
double contactConfidence SWIFT_PRIVATE;
double signalLevel SWIFT_PRIVATE;
double saturationRatio SWIFT_PRIVATE;
double motionArtifactScore SWIFT_PRIVATE;
double collectedSampleCount SWIFT_PRIVATE;
double usableDurationSeconds SWIFT_PRIVATE;
double droppedFrameCount SWIFT_PRIVATE;
ThermalState thermalState SWIFT_PRIVATE;
double waveformValue SWIFT_PRIVATE;
std::optional<double> liveBpm SWIFT_PRIVATE;
std::optional<double> liveIpiMs SWIFT_PRIVATE;
double beatSequence SWIFT_PRIVATE;
std::vector<NativeAcceptedBeat> acceptedBeats SWIFT_PRIVATE;
std::optional<double> liveWindowQuality SWIFT_PRIVATE;
public:
PPGFrameUpdate() = default;
explicit PPGFrameUpdate(double timestampSeconds, FingerContactState contactState, double contactConfidence, double signalLevel, double saturationRatio, double motionArtifactScore, double collectedSampleCount, double usableDurationSeconds, double droppedFrameCount, ThermalState thermalState, double waveformValue, std::optional<double> liveBpm, std::optional<double> liveIpiMs, double beatSequence, std::vector<NativeAcceptedBeat> acceptedBeats, std::optional<double> liveWindowQuality): timestampSeconds(timestampSeconds), contactState(contactState), contactConfidence(contactConfidence), signalLevel(signalLevel), saturationRatio(saturationRatio), motionArtifactScore(motionArtifactScore), collectedSampleCount(collectedSampleCount), usableDurationSeconds(usableDurationSeconds), droppedFrameCount(droppedFrameCount), thermalState(thermalState), waveformValue(waveformValue), liveBpm(liveBpm), liveIpiMs(liveIpiMs), beatSequence(beatSequence), acceptedBeats(acceptedBeats), liveWindowQuality(liveWindowQuality) {}
public:
friend bool operator==(const PPGFrameUpdate& lhs, const PPGFrameUpdate& rhs) = default;
};
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ PPGFrameUpdate <> JS PPGFrameUpdate (object)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::PPGFrameUpdate> final {
static inline margelo::nitro::haptika::ppg::PPGFrameUpdate fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::haptika::ppg::PPGFrameUpdate(
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds"))),
JSIConverter<margelo::nitro::haptika::ppg::FingerContactState>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "contactState"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "contactConfidence"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "signalLevel"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "saturationRatio"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "motionArtifactScore"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "collectedSampleCount"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "usableDurationSeconds"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "droppedFrameCount"))),
JSIConverter<margelo::nitro::haptika::ppg::ThermalState>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "thermalState"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "waveformValue"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "liveBpm"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "liveIpiMs"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "beatSequence"))),
JSIConverter<std::vector<margelo::nitro::haptika::ppg::NativeAcceptedBeat>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "acceptedBeats"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "liveWindowQuality")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::haptika::ppg::PPGFrameUpdate& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds"), JSIConverter<double>::toJSI(runtime, arg.timestampSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "contactState"), JSIConverter<margelo::nitro::haptika::ppg::FingerContactState>::toJSI(runtime, arg.contactState));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "contactConfidence"), JSIConverter<double>::toJSI(runtime, arg.contactConfidence));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "signalLevel"), JSIConverter<double>::toJSI(runtime, arg.signalLevel));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "saturationRatio"), JSIConverter<double>::toJSI(runtime, arg.saturationRatio));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "motionArtifactScore"), JSIConverter<double>::toJSI(runtime, arg.motionArtifactScore));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "collectedSampleCount"), JSIConverter<double>::toJSI(runtime, arg.collectedSampleCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "usableDurationSeconds"), JSIConverter<double>::toJSI(runtime, arg.usableDurationSeconds));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "droppedFrameCount"), JSIConverter<double>::toJSI(runtime, arg.droppedFrameCount));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "thermalState"), JSIConverter<margelo::nitro::haptika::ppg::ThermalState>::toJSI(runtime, arg.thermalState));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "waveformValue"), JSIConverter<double>::toJSI(runtime, arg.waveformValue));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "liveBpm"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.liveBpm));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "liveIpiMs"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.liveIpiMs));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "beatSequence"), JSIConverter<double>::toJSI(runtime, arg.beatSequence));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "acceptedBeats"), JSIConverter<std::vector<margelo::nitro::haptika::ppg::NativeAcceptedBeat>>::toJSI(runtime, arg.acceptedBeats));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "liveWindowQuality"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.liveWindowQuality));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timestampSeconds")))) return false;
if (!JSIConverter<margelo::nitro::haptika::ppg::FingerContactState>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "contactState")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "contactConfidence")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "signalLevel")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "saturationRatio")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "motionArtifactScore")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "collectedSampleCount")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "usableDurationSeconds")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "droppedFrameCount")))) return false;
if (!JSIConverter<margelo::nitro::haptika::ppg::ThermalState>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "thermalState")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "waveformValue")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "liveBpm")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "liveIpiMs")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "beatSequence")))) return false;
if (!JSIConverter<std::vector<margelo::nitro::haptika::ppg::NativeAcceptedBeat>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "acceptedBeats")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "liveWindowQuality")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,76 @@
///
/// PPGSessionPurpose.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/NitroHash.hpp>)
#include <NitroModules/NitroHash.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
namespace margelo::nitro::haptika::ppg {
/**
* An enum which can be represented as a JavaScript union (PPGSessionPurpose).
*/
enum class PPGSessionPurpose {
SPOTMEASUREMENT SWIFT_NAME(spotmeasurement) = 0,
COHERENCE SWIFT_NAME(coherence) = 1,
} CLOSED_ENUM;
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ PPGSessionPurpose <> JS PPGSessionPurpose (union)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::PPGSessionPurpose> final {
static inline margelo::nitro::haptika::ppg::PPGSessionPurpose fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("spotMeasurement"): return margelo::nitro::haptika::ppg::PPGSessionPurpose::SPOTMEASUREMENT;
case hashString("coherence"): return margelo::nitro::haptika::ppg::PPGSessionPurpose::COHERENCE;
default: [[unlikely]]
throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum PPGSessionPurpose - invalid value!");
}
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::haptika::ppg::PPGSessionPurpose arg) {
switch (arg) {
case margelo::nitro::haptika::ppg::PPGSessionPurpose::SPOTMEASUREMENT: return JSIConverter<std::string>::toJSI(runtime, "spotMeasurement");
case margelo::nitro::haptika::ppg::PPGSessionPurpose::COHERENCE: return JSIConverter<std::string>::toJSI(runtime, "coherence");
default: [[unlikely]]
throw std::invalid_argument("Cannot convert PPGSessionPurpose to JS - invalid value: "
+ std::to_string(static_cast<int>(arg)) + "!");
}
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isString()) {
return false;
}
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("spotMeasurement"):
case hashString("coherence"):
return true;
default:
return false;
}
}
};
} // namespace margelo::nitro
@@ -0,0 +1,88 @@
///
/// ThermalState.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/NitroHash.hpp>)
#include <NitroModules/NitroHash.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
namespace margelo::nitro::haptika::ppg {
/**
* An enum which can be represented as a JavaScript union (ThermalState).
*/
enum class ThermalState {
NOMINAL SWIFT_NAME(nominal) = 0,
FAIR SWIFT_NAME(fair) = 1,
SERIOUS SWIFT_NAME(serious) = 2,
CRITICAL SWIFT_NAME(critical) = 3,
UNKNOWN SWIFT_NAME(unknown) = 4,
} CLOSED_ENUM;
} // namespace margelo::nitro::haptika::ppg
namespace margelo::nitro {
// C++ ThermalState <> JS ThermalState (union)
template <>
struct JSIConverter<margelo::nitro::haptika::ppg::ThermalState> final {
static inline margelo::nitro::haptika::ppg::ThermalState fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("nominal"): return margelo::nitro::haptika::ppg::ThermalState::NOMINAL;
case hashString("fair"): return margelo::nitro::haptika::ppg::ThermalState::FAIR;
case hashString("serious"): return margelo::nitro::haptika::ppg::ThermalState::SERIOUS;
case hashString("critical"): return margelo::nitro::haptika::ppg::ThermalState::CRITICAL;
case hashString("unknown"): return margelo::nitro::haptika::ppg::ThermalState::UNKNOWN;
default: [[unlikely]]
throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum ThermalState - invalid value!");
}
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::haptika::ppg::ThermalState arg) {
switch (arg) {
case margelo::nitro::haptika::ppg::ThermalState::NOMINAL: return JSIConverter<std::string>::toJSI(runtime, "nominal");
case margelo::nitro::haptika::ppg::ThermalState::FAIR: return JSIConverter<std::string>::toJSI(runtime, "fair");
case margelo::nitro::haptika::ppg::ThermalState::SERIOUS: return JSIConverter<std::string>::toJSI(runtime, "serious");
case margelo::nitro::haptika::ppg::ThermalState::CRITICAL: return JSIConverter<std::string>::toJSI(runtime, "critical");
case margelo::nitro::haptika::ppg::ThermalState::UNKNOWN: return JSIConverter<std::string>::toJSI(runtime, "unknown");
default: [[unlikely]]
throw std::invalid_argument("Cannot convert ThermalState to JS - invalid value: "
+ std::to_string(static_cast<int>(arg)) + "!");
}
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isString()) {
return false;
}
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("nominal"):
case hashString("fair"):
case hashString("serious"):
case hashString("critical"):
case hashString("unknown"):
return true;
default:
return false;
}
}
};
} // namespace margelo::nitro
@@ -0,0 +1,36 @@
{
"name": "react-native-haptika-ppg",
"version": "0.1.0",
"description": "iPhone-only native PPG, thermal-state, and read-only HealthKit integration for Haptika",
"main": "src/index.ts",
"types": "src/index.ts",
"react-native": "src/index.ts",
"source": "src/index.ts",
"files": [
"src",
"ios",
"nitrogen",
"nitro.json",
"react-native.config.js",
"HaptikaPPG.podspec"
],
"scripts": {
"specs": "nitrogen",
"typecheck": "tsc --noEmit"
},
"license": "UNLICENSED",
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-nitro-modules": "*",
"react-native-vision-camera": "*"
},
"devDependencies": {
"nitrogen": "0.36.1",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-nitro-modules": "0.36.1",
"react-native-vision-camera": "5.1.0",
"typescript": "^5.8.3"
}
}
@@ -0,0 +1,8 @@
module.exports = {
dependency: {
platforms: {
ios: {},
android: null,
},
},
};
@@ -0,0 +1,20 @@
import {NitroModules} from 'react-native-nitro-modules';
import type {
HaptikaHealthKit,
HaptikaPPGFrameProcessor,
HaptikaCardiacMirror,
} from './specs/HaptikaPPG.nitro';
export * from './specs/HaptikaPPG.nitro';
export const haptikaPPG =
NitroModules.createHybridObject<HaptikaPPGFrameProcessor>(
'HaptikaPPGFrameProcessor',
);
export const haptikaHealthKit =
NitroModules.createHybridObject<HaptikaHealthKit>('HaptikaHealthKit');
export const haptikaCardiacMirror =
NitroModules.createHybridObject<HaptikaCardiacMirror>('HaptikaCardiacMirror');
@@ -0,0 +1,187 @@
import type {HybridObject} from 'react-native-nitro-modules';
import type {Frame} from 'react-native-vision-camera';
export type ThermalState =
| 'nominal'
| 'fair'
| 'serious'
| 'critical'
| 'unknown';
export type FingerContactState =
| 'absent'
| 'adjusting'
| 'valid'
| 'tooMuchPressure'
| 'overexposed'
| 'unstable';
export type PPGSessionPurpose = 'spotMeasurement' | 'coherence';
export interface NativePPGConfiguration {
sessionPurpose: PPGSessionPurpose;
targetSampleRate: number;
minimumFrequencyHz: number;
maximumFrequencyHz: number;
stabilizationSeconds: number;
measurementSeconds: number;
emitEveryFrames: number;
minimumContactConfidence: number;
}
export interface NativeAcceptedBeat {
sequence: number;
timestampSeconds: number;
intervalMs: number;
confidence: number;
}
export interface PPGFrameUpdate {
timestampSeconds: number;
contactState: FingerContactState;
contactConfidence: number;
signalLevel: number;
saturationRatio: number;
motionArtifactScore: number;
collectedSampleCount: number;
usableDurationSeconds: number;
droppedFrameCount: number;
thermalState: ThermalState;
waveformValue: number;
liveBpm?: number;
liveIpiMs?: number;
beatSequence: number;
acceptedBeats: NativeAcceptedBeat[];
liveWindowQuality?: number;
}
export interface NativeBPMEstimate {
bpm?: number;
temporalBpm?: number;
frequencyBpm?: number;
methodAgreementBpm?: number;
validPeakCount: number;
rejectedPeakCount: number;
confidence: number;
selectedChannel: string;
}
export interface NativePRVMetrics {
estimatedRmssdMs?: number;
estimatedSdnnMs?: number;
meanIpiMs?: number;
medianIpiMs?: number;
pnn50Percent?: number;
rangeIpiMs?: number;
modeIpiMs?: number;
amo50Percent?: number;
coefficientOfVariation?: number;
validIntervalCount: number;
rejectedIntervalCount: number;
rejectedIntervalRatio: number;
reliable: boolean;
}
export interface NativeSignalQualityResult {
score: number;
level: string;
usableDurationSeconds: number;
droppedFrameRatio: number;
saturationRatio: number;
rejectedIntervalRatio: number;
signalToNoiseRatio?: number;
motionArtifactRatio: number;
methodAgreementScore: number;
timestampJitterRatio: number;
longestGapSeconds: number;
issues: string[];
}
export interface NativePPGProcessingResult {
valid: boolean;
invalidReason?: string;
bpm: NativeBPMEstimate;
prv: NativePRVMetrics;
quality: NativeSignalQualityResult;
startedAtMonotonicSeconds: number;
endedAtMonotonicSeconds: number;
rawSampleCount: number;
resampledSampleCount: number;
}
export interface HaptikaPPGFrameProcessor
extends HybridObject<{ios: 'swift'}> {
startMeasurement(configuration: NativePPGConfiguration): void;
processFrame(frame: Frame): PPGFrameUpdate | undefined;
finishMeasurement(): NativePPGProcessingResult;
cancelMeasurement(): void;
getThermalState(): ThermalState;
}
export type HealthMetricType = 'heartRate' | 'restingHeartRate' | 'hrvSdnn';
export interface NativeHealthMetricSample {
type: HealthMetricType;
value: number;
unit: string;
startDate: string;
endDate: string;
sourceName?: string;
sourceBundleIdentifier?: string;
deviceName?: string;
}
export interface NativeHealthKitContext {
available: boolean;
authorized: boolean;
latestRestingHeartRate?: NativeHealthMetricSample;
latestHrvSdnn?: NativeHealthMetricSample;
recentHeartRateSamples: NativeHealthMetricSample[];
contextConfidence: number;
fetchedAt: string;
}
export interface NativeHeartbeatSeriesSummary {
startDate: string;
endDate: string;
sourceName?: string;
sourceBundleIdentifier?: string;
heartbeatTimesSinceSeriesStartSeconds: number[];
precededByGapIndexes: number[];
}
export interface HaptikaHealthKit extends HybridObject<{ios: 'swift'}> {
isAvailable(): boolean;
requestReadAuthorization(): Promise<boolean>;
fetchContext(lookbackDays: number): Promise<NativeHealthKitContext>;
fetchLatestHeartbeatSeries(): Promise<NativeHeartbeatSeriesSummary | undefined>;
}
export interface NativeCardiacMirrorPatternInfo {
id: string;
version: string;
latencyBudgetMs: number;
}
export interface NativeCardiacMirrorHeartbeat {
timestampSeconds: number;
bpm: number;
}
/**
* Versioned native haptic surface for Module 7 and the single iPhone owner of
* WCSession. Watch heart-rate messages are cadence samples, not biological
* beat events; the UI reconstructs a target rhythm from them.
*/
export interface HaptikaCardiacMirror extends HybridObject<{ios: 'swift'}> {
isSupported(): boolean;
patternInfo(): NativeCardiacMirrorPatternInfo;
start(targetBpm: number): void;
pulse(): void;
stop(): void;
startWatchSession(): void;
watchReachable(): boolean;
latestHeartbeat(): NativeCardiacMirrorHeartbeat | undefined;
playRegulationProtocol(kind: string): number;
stopRegulationProtocol(): void;
}
@@ -0,0 +1,23 @@
{
"include": ["src"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"verbatimModuleSyntax": true
}
}