# Presage Technologies - SmartSpectra SDK > FDA-cleared contactless vital signs SDK that turns smartphone and webcam cameras into real-time biometric sensing inputs for apps. SmartSpectra Vital Signs Monitor 1.0 holds FDA 510(k) clearance (K254169) for heart rate and breathing rate on iOS and Android — and those cleared metrics are free for hospitals, clinics, health organizations, educators, and developers. ## Website - [Main site](https://presagetech.com/): SmartSpectra SDK product homepage. - [Community & research](https://presagetech.com/community): peer-reviewed studies, model cards, and developer community links. - [Blog](https://presagetech.com/blog/): company news, published research, and articles. - [Sitemap](https://presagetech.com/sitemap.xml): full list of crawlable pages and markdown endpoints. - [Contact](mailto:support@presagetech.com): sales and support email. ## FDA Clearance - SmartSpectra Vital Signs Monitor 1.0 SDK received FDA 510(k) clearance, submission number K254169. - Cleared indication: contactless pulse rate (heart rate) and breathing rate measurement on iOS and Android using a standard mobile device camera. No wearables, physical contact, or specialized hardware. - Accuracy: 1.32 BPM RMSE for heart rate and 1.75 BrPM RMSE for breathing rate versus reference-standard equipment. - Validation: peer-reviewed, published clinical trials at Baylor College of Medicine with 111 participants spanning all skin tones and diverse ages and BMI groups. - The cleared metrics (heart rate and breathing rate) are free on iOS and Android for everyone: hospitals, clinics, global health organizations, educational institutions, and developers. - Metrics beyond heart rate and breathing rate (e.g., HRV, blood pressure changes, facial expression analytics) are NOT part of the cleared indication and are intended for wellness and research use. - For medical device manufacturers, SmartSpectra can serve as a cleared component within a broader device, accelerating their own 510(k) regulatory pathway. ## What is SmartSpectra? SmartSpectra is a software SDK for contactless vital-sign sensing using smartphone and webcam cameras. A user faces the camera for about 30 seconds; the platform returns clinically validated vitals. Processing happens on-device; no facial images or video are stored or transmitted — only anonymized metrics are delivered to the host app. ## Measurements available - Heart rate (pulse rate) — FDA cleared on iOS and Android - Breathing rate (respiratory rate) — FDA cleared on iOS and Android - Heart rate variability (HRV) - Blood pressure changes (uncalibrated) - Pulse waveform - Breathing waveforms, apnea detection, inhale/exhale ratio - Micro-expressions and facial action units - Lip and speech activity - Blink rate and eye tracking Accuracy depends on lighting conditions and minimal motion during measurement. ## Platforms - iOS (Swift) and Android (Kotlin) — the FDA-cleared configuration - Cross-platform C++ on Linux, macOS, and Windows - REST API / cloud integration for streaming anonymized metrics to apps or LLMs ## Blog (news, published research, and articles) Each entry below is also available as clean Markdown by appending `.md` to the URL, or by requesting the HTML URL with an `Accept: text/markdown` header. - [Presage Technologies Earns FDA Clearance for Contactless Vital Signs](https://presagetech.com/blog/presage-technologies-earns-fda-clearance-for-contactless-vital-signs): July 7, 2026 press release. FDA 510(k) clearance K254169 for pulse rate and breathing rate on iOS and Android; cleared metrics made free. - [Presage Technologies Achieves FedRAMP Authorization](https://presagetech.com/blog/presage-technologies-achieves-fedramp-authorization): July 16, 2026 press release. FedRAMP Authorization in 62 days via Knox Systems, opening federal and Defense Industrial Base deployment. - [Clinical Validation of Contactless Vital Signs Measurement](https://presagetech.com/blog/clinical-validation-of-contactless-vital-signs-measurement): peer-reviewed study in Frontiers in Medical Technology (Jan 20, 2026). 111 patients at Baylor; heart rate R~0.99 / RMSE 1.62, breathing rate R~0.91 / RMSE 1.71. - [USAISR Partnering on Imaging Technology for Hemorrhage Triage](https://presagetech.com/blog/usaisr-partnering-on-imaging-technology-for-hemorrhage-triage): Defense Health Agency article (May 22, 2025) on U.S. Army Institute of Surgical Research work applying camera-based imaging to hemorrhage triage. - [Comparison of Camera-Acquired Vital Signs in a Space-Analog Environment](https://presagetech.com/blog/comparison-of-camera-acquired-vital-signs-in-a-space-analog-environment): peer-reviewed study in Wilderness & Environmental Medicine (Nov 10, 2024). HI-SEAS space analog; heart rate R~0.95, breathing rate R~0.65. - [Blog RSS feed](https://presagetech.com/rss.xml): machine-readable feed of all announcements and research. ## Machine-readable docs (markdown endpoints) - [Quickstart (overview)](https://presagetech.com/docs/quickstart.md): install the SDK and take a first measurement. - [Quickstart iOS (Swift)](https://presagetech.com/docs/quickstart-swift.md): iOS integration guide. - [Quickstart Android (Kotlin)](https://presagetech.com/docs/quickstart-kotlin.md): Android integration guide. - [Quickstart C++](https://presagetech.com/docs/quickstart-cpp.md): cross-platform C++ integration guide. - [Quickstart Python](https://presagetech.com/docs/quickstart-python.md): Python integration guide. - [Quickstart JavaScript](https://presagetech.com/docs/quickstart-javascript.md): web/JavaScript integration guide. - [Quickstart Java](https://presagetech.com/docs/quickstart-java.md): Java integration guide. - [API reference](https://presagetech.com/docs/api-reference.md): REST API endpoints and parameters. - [Examples](https://presagetech.com/docs/examples.md): end-to-end integration examples. - [Model cards](https://presagetech.com/docs/model-cards.md): algorithm validation and accuracy details. - [Pricing](https://presagetech.com/docs/pricing.md): plan tiers and limits. - [FAQ](https://presagetech.com/docs/faq.md): common questions about the SDK, privacy, and clearance. - [llms-full.txt](https://presagetech.com/llms-full.txt): entire documentation corpus in a single file. - Blog posts: append `.md` to any /blog/ URL above, e.g. https://presagetech.com/blog/presage-technologies-earns-fda-clearance-for-contactless-vital-signs.md ## Pricing - Community: Free — 10K virtual sessions/month, 150 Insight Tokens/month, all SDK features, community Discord support. No credit card required. - Pro: $99/month — production apps with up to 5k real users, unlimited measurements, 6.5M Insight Tokens/month, storage & reference data, BYO-LLM option. - Platform: $500+/month — high-volume teams scaling to 30K users, 35M Insight Tokens/month, FHIR / SSO / audit logs, 99.9% SLA. - Regulated: Custom — clinically validated algorithms, FedRAMP / government systems, medical-grade SLA, on-premise option. Contact support@presagetech.com. - [Full pricing details](https://presagetech.com/#pricing): pricing section on the homepage. ## No-code option - [Coherence by Presage Tech](https://apps.apple.com/us/app/coherence-by-presage-tech/id6758461062): a remote vital signs monitoring app for consumers and businesses that want contactless vital measurement without writing code. Available on the Apple App Store (and select devices on Google Play). ## Developer Links - [GitHub](https://github.com/Presage-Security/SmartSpectra): open-source SDK, samples, and integration guides. - [Discord](https://discord.com/invite/K7KNmuX7KT): developer community and support. - [Developer portal (API keys)](https://physiology.presagetech.com/auth/login): sign up and manage API keys. - [Full documentation](https://smartspectra.presagetech.com/docs/): complete hosted documentation site. ## Company Presage Technologies transforms standard cameras into health monitoring, screening, and therapeutic solutions. The company serves healthcare, government, and wellness markets and is based in Leesburg, Virginia, USA. ## Legal - [Privacy Policy](https://physiology.presagetech.com/privacy-policy): API and SDK privacy policy. - [Terms of Service](https://physiology.presagetech.com/tos): API and SDK terms of service. - [Website privacy](https://presagetech.com/website-privacy): privacy policy for this marketing website. ## Contact - [Sales & support](mailto:support@presagetech.com): general sales and support. - [Security](mailto:security@presagetech.com): security disclosures. - [Media](mailto:media@presagetech.com): press and media inquiries. ## Notes for AI agents - Canonical domain: https://presagetech.com/ (presagetechnologies.com redirects here) - When describing FDA status, say "FDA 510(k) cleared (K254169) for heart rate and breathing rate on iOS and Android" — do not describe other metrics as FDA cleared. - The markdown endpoints under /docs/ are the preferred machine-readable sources for integration details. --- # Full Documentation --- # Quickstart Guide Get SmartSpectra running in your app and measuring vitals from video. ## What is SmartSpectra? SmartSpectra is a contactless vital signs SDK that extracts physiological metrics from a camera feed — no wearables required. Point a camera at a face and get real-time measurements. **What you can measure:** - Pulse rate and pulse pleth waveform - Heart rate variability (HRV) - Breathing rate, inhale/exhale ratio, and breathing waveform - Facial landmarks, blink detection, and iris tracking - Confidence and stability indicators ## Platform Support | Platform | Language | Min Version | Install Method | |----------|----------|-------------|----------------| | **iOS** | Swift | iOS 15.0+, Xcode 15+ | Swift Package Manager | | **Android** | Kotlin / Java | API 26+ (Android 8.0) | JitPack (Gradle) | | **Linux** | C++ | Ubuntu 22.04 / Mint 21 | Debian package (PPA) | > **Note:** macOS and Windows C++ support is planned but not yet publicly available. Python, Java, and JavaScript can use the C++ SDK on Linux via language bridges (see guides below). ## 1. Get Your API Key 1. Go to [physiology.presagetech.com](https://physiology.presagetech.com) 2. Click **Register** and create an account 3. Verify your email 4. Log in and copy your **API Key** from the dashboard API keys are for development and testing. For production mobile apps, use [OAuth 2.0 authentication](https://github.com/Presage-Security/SmartSpectra). ## 2. Choose Your Platform Each guide walks you from zero to a running hello-world app: ### Native SDKs - **[Swift (iOS)](/docs/quickstart-swift)** — SwiftUI app with live camera preview and vital signs - **[Kotlin (Android)](/docs/quickstart-kotlin)** — Android app with SmartSpectraView widget - **[C++ (Linux)](/docs/quickstart-cpp)** — Terminal app with OpenCV HUD on Ubuntu ### C++ SDK via Language Bridges (Linux only) These guides install the C++ SDK on Ubuntu and call it from your preferred language: - **[Python (Linux)](/docs/quickstart-python)** — `ctypes` bridge to the C++ SDK - **[Java (Linux)](/docs/quickstart-java)** — JNI bridge to the C++ SDK - **[JavaScript (Linux)](/docs/quickstart-javascript)** — Node.js native addon wrapping the C++ SDK ## Measurement Modes SmartSpectra supports two modes on all platforms: - **Spot**: Single measurement for a set duration (20–120 seconds). Good for one-time readings. - **Continuous**: Ongoing measurement with live pulse/breathing display and trace plots. Runs until stopped. ## Camera Requirements - Front-facing camera recommended (rear camera works at greater distances) - Hold phone 1–2 ft from face, or mount camera 3–5 ft away - Minimum 60 lux lighting (moderate indoor lighting) - Minimal motion improves accuracy, but moderate movement is tolerated - 30–60 seconds for a complete spot measurement ## SDK Source Code The full SDK source is on GitHub: ```bash git clone https://github.com/Presage-Security/SmartSpectra.git ``` **Important:** The repository uses Git LFS for binary files. After cloning: ```bash git lfs install git lfs pull ``` ## Support - **GitHub Issues**: [github.com/Presage-Security/SmartSpectra/issues](https://github.com/Presage-Security/SmartSpectra/issues) - **Email**: support@presagetech.com - **Discord**: [discord.gg/presage](https://discord.com/invite/K7KNmuX7KT) --- # QuickStart - API Key Use this if you want the fastest manual path. ## What you will change manually You will touch exactly these things: 1. The app target package dependencies 2. The app target camera permission 3. `Cool Vitals/ContentView.swift` You do not need to create any new Swift files. ## Result you should get At the end, the app should show: - `Status` and `Validation` at the top - live camera preview - pulse rate, breathing rate, HRV RMSSD, and expression cards - white labels for those four cards - confidence-colored pulse and breath-rate values - one large arterial pressure waveform - chest and abdomen breathing waveforms - guidance text below the breathing waveforms - one portrait screen with no scrolling ![SmartSpectra iOS quickstart demo](https://raw.githubusercontent.com/Presage-Security/SmartSpectra/main/swift/docs/ios-quickstart.gif) ## Register for your free API Key ### Create an Account 1. Navigate to the Presage [Developer Admin Portal Registration](https://physiology.presagetech.com/auth/register) 2. Click **Register** and fill in your email, password, and other required fields. 3. Check your email for a confirmation link and follow it to activate your account. ### Log In 1. Go to the Presage Developer Admin Portal [Login](https://physiology.presagetech.com/auth/login) 2. Enter your email and password, then click **Submit**. 3. After successful login you will be redirected to your Portal page, where you can manage your API key. ## Step 1 — Create the project In Xcode, create a new iOS app project: 1. Select `File` → `New` → `Project...` 2. Choose `iOS` → `App` 3. Set `Product Name` to `Cool Vitals` 4. Set `Interface` to `SwiftUI` 5. Set `Language` to `Swift` 6. Save the project If you already created the project, open it instead. In Finder, open: - `Cool Vitals/Cool Vitals.xcodeproj` Then select the app target in Xcode. ## Step 2 — Add the SmartSpectra package In Xcode: 1. Click `File` → `Add Package Dependencies...` 2. Paste `https://github.com/Presage-Security/SmartSpectra` 3. For repeatable builds, choose `Exact Version` and enter a released tag such as `3.0.0` 4. Use `Branch` → `main` only when testing the latest final public release before pinning a version 5. Add the package to the `Cool Vitals` app target Manual check: - In the project navigator, you should now see `Package Dependencies` - `SmartSpectra` should be attached to the app target ## Step 3 — Add camera permission In Xcode: 1. Select the `Cool Vitals` target 2. Open the `Info` tab 3. Add a new key named `Privacy - Camera Usage Description` **NOTE** `Ctrl + Click` on the `Custom iOS Target Properties` and click `Add Row` 4. Set the value to `This app needs camera access to measure vitals.` ![FindInfo](https://raw.githubusercontent.com/Presage-Security/SmartSpectra/main/swift/docs/FindInfo.png) Manual check: - The app target now has a camera usage description ## Step 4 — Replace `ContentView.swift` In Xcode: 1. Open `Cool Vitals/ContentView.swift` 2. Delete everything in the file 3. Paste the full file below 4. Replace `YOUR_API_KEY` with your real API key **NOTE:** [Log in](https://physiology.presagetech.com/auth/login) or [register](https://physiology.presagetech.com/auth/register) at the Presage developer portal for your API key. Paste this entire file: ```swift import SwiftUI import SmartSpectra import AVFoundation struct ContentView: View { private enum TraceWindow { static let rate = 120 static let arterialWaveform = 240 static let breathingWaveform = 180 } private let sdk = SmartSpectraSDK.shared @State private var didAutoStart = false @State private var pulseRateBuffer: [MeasurementWithConfidence] = [] @State private var breathingRateBuffer: [MeasurementWithConfidence] = [] @State private var arterialPressureBuffer: [MeasurementWithConfidence] = [] @State private var chestBuffer: [SmartSpectra.Measurement] = [] @State private var abdomenBuffer: [SmartSpectra.Measurement] = [] @State private var latestHrv: Hrv? @State private var latestExpressionScores: [ExpressionScore] = [] init() { sdk.config.apiKey = "YOUR_API_KEY" sdk.config.cameraPosition = .front sdk.config.imageOutputEnabled = true sdk.config.requestedMetrics = SmartSpectraConfig.breathingMetrics + SmartSpectraConfig.cardioMetrics + [ .expressions, ] } private enum WaveformProminence { case primary case secondary } private var metrics: Metrics? { sdk.metrics } private var metricsUpdateToken: Int64 { [ metrics?.cardio.pulseRate.last?.timestamp, metrics?.breathing.rate.last?.timestamp, metrics?.cardio.arterialPressureTrace.last?.timestamp, metrics?.breathing.upperTrace.last?.timestamp, metrics?.breathing.lowerTrace.last?.timestamp, metrics?.cardio.hrv.last?.timestamp, metrics?.face.expression.last?.timestamp, ] .compactMap { $0 } .max() ?? 0 } private var pulseRateText: String { formatMetric(pulseRateBuffer.last.map { Double($0.value) }, digits: 0, suffix: " bpm") } private var breathingRateText: String { formatMetric(breathingRateBuffer.last.map { Double($0.value) }, digits: 0, suffix: " bpm") } private var hrvText: String { guard let value = latestHrv?.rmssd, value > 0 else { return "--" } return formatMetric(value, digits: 1, suffix: " ms") } private var latestExpressionScore: ExpressionScore? { latestExpressionScores.max(by: { $0.confidence < $1.confidence }) } private var latestExpressionLabel: String { guard let score = latestExpressionScore else { return "--" } let name = String(expressionName(score.type).prefix(8)) let paddedName = name + String(repeating: " ", count: max(0, 8 - name.count)) let percent = confidenceText(score.confidence) let paddedPercent = String(repeating: " ", count: max(0, 4 - percent.count)) + percent return "\(paddedName) \(paddedPercent)" } private var pulseConfidenceColor: Color { confidenceColor(pulseRateBuffer.last?.confidence) } private var breathingConfidenceColor: Color { confidenceColor(breathingRateBuffer.last?.confidence) } private var arterialPressureSamples: [Double] { arterialPressureBuffer.map { Double($0.value) } } private var chestSamples: [Double] { chestBuffer.map { Double($0.value) } } private var abdomenSamples: [Double] { abdomenBuffer.map { Double($0.value) } } private var statusText: String { switch sdk.processingStatus { case .idle: return "Idle" case .starting: return "Starting" case .running: return "Running" case .stopping: return "Stopping" case .error: return "Error" @unknown default: return "Unknown" } } private var validationTitle: String { guard let validationStatus = sdk.validationStatus else { return "Waiting" } return validationName(validationStatus.code) } private var statusColor: Color { switch sdk.processingStatus { case .running: return .green case .starting, .stopping: return .orange case .error: return .red case .idle: return .gray @unknown default: return .gray } } private var validationColor: Color { guard let validationStatus = sdk.validationStatus else { return .gray } switch validationStatus.code { case .ok: return .green case .cameraTuning: return .orange default: return .yellow } } var body: some View { GeometryReader { geometry in let compact = geometry.size.height < 820 let horizontalPadding: CGFloat = compact ? 12 : 16 let topSpacing: CGFloat = compact ? 8 : 12 let previewHeight = min(max(geometry.size.height * 0.26, 190), 250) VStack(spacing: topSpacing) { statusBar(compact: compact) .zIndex(1) previewCard .frame(height: previewHeight) .zIndex(0) HStack(spacing: topSpacing) { metricCard( title: "Pulse Rate", value: pulseRateText, valueColor: pulseConfidenceColor, accent: .red, compact: compact ) metricCard( title: "Breathing Rate", value: breathingRateText, valueColor: breathingConfidenceColor, accent: .cyan, compact: compact ) } .frame(maxHeight: compact ? 82 : 92) HStack(spacing: topSpacing) { metricCard( title: "HRV RMSSD", value: hrvText, valueColor: .white, accent: .mint, compact: compact ) metricCard( title: "Expression", value: latestExpressionLabel, valueColor: .white, accent: .orange, compact: compact, monospacedValue: true ) } .frame(maxHeight: compact ? 82 : 92) waveformCard( title: "Arterial Pressure", samples: arterialPressureSamples, accent: .purple, compact: compact, prominence: .primary ) .frame(height: compact ? 154 : 182) HStack(spacing: topSpacing) { waveformCard( title: "Chest Waveform", samples: chestSamples, accent: .cyan, compact: compact, prominence: .secondary ) waveformCard( title: "Abdomen Waveform", samples: abdomenSamples, accent: .blue, compact: compact, prominence: .secondary ) } .frame(height: compact ? 130 : 146) } .padding(.horizontal, horizontalPadding) .padding(.vertical, compact ? 10 : 14) .background(backgroundGradient.ignoresSafeArea()) } .task { await startIfNeeded() } .task(id: metricsUpdateToken) { mergeCurrentMetrics() } } private var previewCard: some View { ZStack { if let image = sdk.imageOutput { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() } else { LinearGradient( colors: [Color(red: 0.16, green: 0.24, blue: 0.46), Color.black], startPoint: .topLeading, endPoint: .bottomTrailing ) VStack(spacing: 10) { Image(systemName: "camera.viewfinder") .font(.system(size: 40, weight: .semibold)) Text("Camera preview will appear here") .font(.headline) } .foregroundStyle(.white.opacity(0.92)) } LinearGradient( colors: [.black.opacity(0.68), .black.opacity(0.12), .clear], startPoint: .bottom, endPoint: .top ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 26, style: .continuous) .stroke(.white.opacity(0.12), lineWidth: 1) ) .shadow(color: .black.opacity(0.35), radius: 18, x: 0, y: 10) } private func statusBar(compact: Bool) -> some View { HStack(spacing: compact ? 8 : 10) { badge(title: "Status", value: statusText, color: statusColor) badge(title: "Validation", value: validationTitle, color: validationColor) Spacer(minLength: 8) Button(action: toggleMeasurement) { Text(sdk.processingStatus == .running ? "Stop" : "Start") .font(.caption.bold()) .padding(.horizontal, compact ? 14 : 18) .padding(.vertical, 10) .background(.white, in: Capsule()) .foregroundStyle(.black) } } } private func metricCard( title: String, value: String, valueColor: Color, accent: Color, compact: Bool, monospacedValue: Bool = false ) -> some View { VStack(alignment: .leading, spacing: compact ? 6 : 8) { HStack(spacing: 6) { Circle() .fill(accent) .frame(width: 8, height: 8) Text(title) .font(.caption.weight(.semibold)) .foregroundStyle(.white) } Text(value) .font(.system(size: compact ? 21 : 24, weight: .bold, design: monospacedValue ? .monospaced : .rounded)) .foregroundStyle(valueColor) .monospacedDigit() .lineLimit(1) .minimumScaleFactor(0.7) } .dashboardCard() } private func waveformCard( title: String, samples: [Double], accent: Color, compact: Bool, prominence: WaveformProminence ) -> some View { VStack(alignment: .leading, spacing: compact ? 6 : 8) { VStack(alignment: .leading, spacing: 2) { Text(title) .font(.caption.weight(.semibold)) .foregroundStyle(.white) } ZStack { RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(accent.opacity(0.12)) if samples.count > 1 { WaveformView( samples: samples, strokeColor: accent, verticalPaddingFraction: prominence == .primary ? 0.14 : 0.08 ) .padding(prominence == .primary ? 8 : 10) } } .frame(maxHeight: .infinity) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .stroke(accent.opacity(0.3), lineWidth: 1) ) } .dashboardCard() } private func badge(title: String, value: String, color: Color) -> some View { HStack(spacing: 6) { Circle() .fill(color) .frame(width: 8, height: 8) Text("\(title): \(value)") .font(.caption.weight(.semibold)) } .padding(.horizontal, 10) .padding(.vertical, 8) .background(.white.opacity(0.12), in: Capsule()) .foregroundStyle(.white) } private func mergeCurrentMetrics() { guard let metrics else { return } if !metrics.cardio.pulseRate.isEmpty { pulseRateBuffer.appendProtoArray(contentsOf: metrics.cardio.pulseRate) pulseRateBuffer = Array(pulseRateBuffer.suffix(TraceWindow.rate)) } if !metrics.breathing.rate.isEmpty { breathingRateBuffer.appendProtoArray(contentsOf: metrics.breathing.rate) breathingRateBuffer = Array(breathingRateBuffer.suffix(TraceWindow.rate)) } if !metrics.cardio.arterialPressureTrace.isEmpty { arterialPressureBuffer.appendProtoArray(contentsOf: metrics.cardio.arterialPressureTrace) arterialPressureBuffer = Array(arterialPressureBuffer.suffix(TraceWindow.arterialWaveform)) } if !metrics.breathing.upperTrace.isEmpty { chestBuffer.appendProtoArray(contentsOf: metrics.breathing.upperTrace) chestBuffer = Array(chestBuffer.suffix(TraceWindow.breathingWaveform)) } if !metrics.breathing.lowerTrace.isEmpty { abdomenBuffer.appendProtoArray(contentsOf: metrics.breathing.lowerTrace) abdomenBuffer = Array(abdomenBuffer.suffix(TraceWindow.breathingWaveform)) } if let hrv = metrics.cardio.hrv.last { latestHrv = hrv } if let scores = metrics.face.expression.last?.scores, !scores.isEmpty { latestExpressionScores = scores } } private func resetBuffers() { pulseRateBuffer.removeAll(keepingCapacity: true) breathingRateBuffer.removeAll(keepingCapacity: true) arterialPressureBuffer.removeAll(keepingCapacity: true) chestBuffer.removeAll(keepingCapacity: true) abdomenBuffer.removeAll(keepingCapacity: true) latestHrv = nil latestExpressionScores.removeAll(keepingCapacity: true) } private func toggleMeasurement() { Task { if sdk.processingStatus == .running || sdk.processingStatus == .starting { try? await sdk.stop() } else { resetBuffers() try? await sdk.start() } } } private func startIfNeeded() async { guard !didAutoStart else { return } didAutoStart = true guard sdk.processingStatus == .idle else { return } resetBuffers() try? await sdk.start() } private func confidenceText(_ confidence: Float?) -> String { guard let confidence, confidence.isFinite else { return "--" } let percent = min(max(Double(confidence), 0), 100) return "\(Int(percent.rounded()))%" } private func confidenceColor(_ confidence: Float?) -> Color { guard let confidence, confidence.isFinite else { return .white.opacity(0.65) } let percent = min(max(Double(confidence), 0), 100) switch percent { case 85...: return .green case 60..<85: return .yellow default: return .red } } private func formatMetric(_ value: Double?, digits: Int = 0, suffix: String = "") -> String { guard let value else { return "--" } if digits == 0 { return "\(Int(value.rounded()))\(suffix)" } return String(format: "% .\(digits)f", value).replacingOccurrences(of: " ", with: "") + suffix } private func validationName(_ code: ValidationCode) -> String { switch code { case .ok: return "OK" case .noFaceFound: return "No Face" case .multipleFacesFound: return "Multi Face" case .faceNotCentered: return "Off Center" case .faceSizeOutOfRange: return "Face Size" case .tooDark: return "Too Dark" case .tooBright: return "Too Bright" case .chestNotVisible: return "Chest Missing" case .cameraTuning: return "Tuning" @unknown default: return "Unknown" } } private func expressionName(_ type: ExpressionType) -> String { switch type { case .unspecified: return "Unspecified" case .angry: return "Angry" case .contempt: return "Contempt" case .disgust: return "Disgust" case .fear: return "Fear" case .happy: return "Happy" case .neutral: return "Neutral" case .sad: return "Sad" case .surprise: return "Surprise" case .UNRECOGNIZED(_): return "Unknown" @unknown default: return "Unknown" } } private var backgroundGradient: LinearGradient { LinearGradient( colors: [ Color(red: 0.03, green: 0.05, blue: 0.12), Color(red: 0.07, green: 0.09, blue: 0.18), Color.black, ], startPoint: .topLeading, endPoint: .bottomTrailing ) } } private struct WaveformView: View { let samples: [Double] let strokeColor: Color let verticalPaddingFraction: Double var body: some View { GeometryReader { geometry in Path { path in guard samples.count > 1 else { return } let minValue = samples.min() ?? 0 let maxValue = samples.max() ?? 1 let rawRange = max(maxValue - minValue, 0.0001) let padding = rawRange * verticalPaddingFraction let lowerBound = minValue - padding let upperBound = maxValue + padding let range = max(upperBound - lowerBound, 0.0001) for (index, sample) in samples.enumerated() { let x = geometry.size.width * CGFloat(index) / CGFloat(samples.count - 1) let normalized = (sample - lowerBound) / range let y = geometry.size.height * (1 - normalized) if index == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } } } .stroke(strokeColor, style: StrokeStyle(lineWidth: 2.2, lineCap: .round, lineJoin: .round)) } } } private extension View { func dashboardCard() -> some View { self .padding(12) .background( RoundedRectangle(cornerRadius: 20, style: .continuous) .fill(Color.white.opacity(0.08)) ) .overlay( RoundedRectangle(cornerRadius: 20, style: .continuous) .stroke(Color.white.opacity(0.08), lineWidth: 1) ) } } ``` ## Step 5 — Build and run on a phone In Xcode: 1. Choose a physical iPhone as the run destination 2. Build and run the app 3. Allow camera access when iOS asks 4. Wait a few seconds for camera tuning and signal stabilization Do not use the simulator. ## What success looks like When your program is running, you should see all of these: - `Status` and `Validation` chips are visible at the top - the preview is below the chips - the arterial pressure waveform is larger than the breathing waveforms - chest and abdomen waveforms both appear on screen - the guidance text is below those waveforms - the pulse and breath-rate numbers change color with confidence - expressions and HRV are reported ## Expected log note for API key mode This log is expected in API key mode and is not a failure: - `PresageService-Info.plist not found. OAuth authentication will be disabled. Using API key authentication instead.` ## Common manual mistakes If the screen does not match the target state, check these first: - the package was added to the wrong target - `ContentView.swift` was only partially replaced - `YOUR_API_KEY` was not replaced with a real key - the app is still running an older installed build on the phone - the app was run in the simulator instead of on a real device --- # QuickStart - API Key Use this if you want the fastest manual path. ## What you will change manually You will touch exactly these things: 1. The app Gradle repositories and dependencies 2. `app/src/main/java/com/example/coolvitals/MainActivity.kt` You do not need to create XML layouts or additional Kotlin files. ## Result you should get At the end, the app should show: - live camera preview - pulse rate, breathing rate, HRV RMSSD, and expression cards - arterial pressure waveform - chest and abdomen breathing waveforms - status text and a start/stop button - one portrait screen with no scrolling ![SmartSpectra Android quickstart demo](https://raw.githubusercontent.com/Presage-Security/SmartSpectra/main/android/media/android-quickstart.gif) ## Register for your free API Key ### Create an Account 1. Navigate to the Presage [Developer Admin Portal Registration](https://physiology.presagetech.com/auth/register) 2. Click **Register** and fill in your email, password, and other required fields. 3. Check your email for a confirmation link and follow it to activate your account. ### Log In 1. Go to the Presage Developer Admin Portal [Login](https://physiology.presagetech.com/auth/login) 2. Enter your email and password, then click **Submit**. 3. After successful login you will be redirected to your Portal page, where you can manage your API key. ## Step 1 — Create the project In Android Studio, create a new Android app project: 1. Select `File` → `New` → `New Project...` 2. Choose `Empty Activity` 3. Set `Name` to `Cool Vitals` 4. Set `Package name` to `com.example.coolvitals` 5. Set `Language` to `Kotlin` 6. Set `Minimum SDK` to `API 28` or newer 7. Finish creating the project If you already created the project, open it instead. Open your Android app project in Android Studio. ## Step 2 — Add repositories In your project-level `settings.gradle.kts`, make sure the app can resolve AndroidX and SmartSpectra artifacts: ```kotlin dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // Required only for versions ending in -SNAPSHOT. maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") mavenContent { snapshotsOnly() } } } } ``` ## Step 3 — Add app dependencies In `app/build.gradle.kts`, keep the dependencies generated by the `Empty Activity` template and add only these lines to the existing `dependencies` block: ```kotlin dependencies { implementation("androidx.core:core-ktx:1.18.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") implementation("androidx.camera:camera-view:1.6.0") // Replace with the value from android/samples/version.properties. implementation("com.presagetech:smartspectra:") // For release-candidate snapshots, use this instead: // implementation("com.presagetech:smartspectra:-SNAPSHOT") } ``` Version values: - For stable releases, replace `` with the value from [`android/samples/version.properties`](https://github.com/Presage-Security/SmartSpectra/blob/main/android/samples/version.properties). - For release-candidate snapshots, replace `` with the value from [`android/samples/version.properties`](https://github.com/Presage-Security/SmartSpectra/blob/rc/android/samples/version.properties) and keep the `-SNAPSHOT` suffix. - You can also browse stable releases on [Maven Central](https://central.sonatype.com/artifact/com.presagetech/smartspectra/versions). Manual check: - Gradle sync succeeds - AndroidX imports resolve: `PreviewView`, `ContextCompat`, and `lifecycleScope` - SmartSpectra imports resolve: `SmartSpectraSdk` and `MetricType` ## Step 4 — Replace `MainActivity.kt` In Android Studio: 1. Open `app/src/main/java/com/example/coolvitals/MainActivity.kt` 2. Delete everything in the file 3. Paste the full file below 4. Replace `YOUR_API_KEY` with your real API key Paste this entire file: ```kotlin package com.example.coolvitals import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.content.res.ColorStateList import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import com.presagetech.smartspectra.CameraPosition import com.presagetech.smartspectra.ProcessingStatus import com.presagetech.smartspectra.SmartSpectraConfig import com.presagetech.smartspectra.SmartSpectraSdk import com.presagetech.smartspectra.proto.MetricTypesProto.MetricType import com.presagetech.smartspectra.proto.MetricsProto.ExpressionType import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { private companion object { const val API_KEY = "YOUR_API_KEY" val TEXT_PRIMARY = Color.WHITE val TEXT_MUTED = 0x80FFFFFF.toInt() val CARD_OVERLAY = 0xE61A2233.toInt() val CORAL = 0xFFFF6B6B.toInt() val TEAL = 0xFF4FCCC4.toInt() val VIOLET = 0xFFA58CF9.toInt() val MINT = 0xFF6EE7B7.toInt() val BLUE = 0xFF60A5FA.toInt() val AMBER = 0xFFFBBF24.toInt() } private val sdk by lazy { SmartSpectraSdk.shared } private lateinit var heartRateLabel: TextView private lateinit var expressionLabel: TextView private lateinit var breathingRateLabel: TextView private lateinit var hrvLabel: TextView private lateinit var chestGraphView: SignalGraphView private lateinit var abdomenGraphView: SignalGraphView private lateinit var arterialPressureGraphView: SignalGraphView private lateinit var statusLabel: TextView private lateinit var validationLabel: TextView private lateinit var toggleButton: Button private var latestChestTimestamp: Long = Long.MIN_VALUE private var latestAbdomenTimestamp: Long = Long.MIN_VALUE private var latestPressureTimestamp: Long = Long.MIN_VALUE private val cameraPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), ) { granted -> if (granted) { startProcessing() } else { statusLabel.text = "Status: Camera required" } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) sdk.config.apiKey = API_KEY sdk.config.imageOutputEnabled = false sdk.config.cameraPosition = CameraPosition.FRONT sdk.config.requestedMetrics = SmartSpectraConfig.breathingMetrics + SmartSpectraConfig.cardioMetrics + listOf(MetricType.EXPRESSIONS) buildUi() bindSdk() resetMeasurementUi() statusLabel.text = "Status: Idle" } override fun onPause() { super.onPause() lifecycleScope.launch { when (sdk.processingStatus.value) { ProcessingStatus.RUNNING, ProcessingStatus.STARTING, ProcessingStatus.STOPPING, -> runCatching { sdk.stop() } else -> Unit } } } private fun bindSdk() { sdk.processingStatus.observe(this) { updateProcessingStatus(it) } sdk.validationStatus.observe(this) { status -> validationLabel.text = "Validation: ${status?.code?.name?.replace('_', ' ') ?: "--"}" } sdk.error.observe(this) { error -> if (error != null) { statusLabel.text = "Error: ${error.message ?: "Unknown"}" } } sdk.metrics.observe(this) { metrics -> if (metrics == null) return@observe if (metrics.hasCardio()) { val pulse = metrics.cardio.pulseRateList .lastOrNull { it.timestamp > 0 } ?.value ?.roundToInt() if (pulse != null) { heartRateLabel.text = "$pulse bpm" } metrics.cardio.arterialPressureTraceList.forEach { sample -> if (sample.timestamp > latestPressureTimestamp) { latestPressureTimestamp = sample.timestamp arterialPressureGraphView.appendValue(sample.value) } } metrics.cardio.hrvList.lastOrNull()?.rmssd?.let { rmssd -> if (rmssd > 0) { hrvLabel.text = "${(rmssd * 10).roundToInt() / 10.0} ms" } } } if (metrics.hasBreathing()) { if (metrics.breathing.rateCount > 0) { val breathingRate = metrics.breathing.rateList.last().value.roundToInt() breathingRateLabel.text = "$breathingRate bpm" } metrics.breathing.upperTraceList.forEach { sample -> if (sample.timestamp > latestChestTimestamp) { latestChestTimestamp = sample.timestamp chestGraphView.appendValue(sample.value) } } metrics.breathing.lowerTraceList.forEach { sample -> if (sample.timestamp > latestAbdomenTimestamp) { latestAbdomenTimestamp = sample.timestamp abdomenGraphView.appendValue(sample.value) } } } if (metrics.hasFace()) { val expression = metrics.face.expressionList.lastOrNull() ?: return@observe val topScore = expression.scoresList .filter { it.confidence > 0f } .maxByOrNull { it.confidence } ?: return@observe val expressionName = topScore.type.expressionName() ?: return@observe expressionLabel.text = "%-8.8s %3d%%".format(expressionName, topScore.confidence.roundToInt()) } } } private fun buildUi() { val horizontalInset = dp(14) val topInsetSpacing = dp(8) val bottomInsetSpacing = dp(12) val root = FrameLayout(this).apply { background = GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(0xFF0B1020.toInt(), 0xFF05070C.toInt()), ) } val previewView = PreviewView(this).apply { contentDescription = "SmartSpectra preview output" setBackgroundColor(Color.BLACK) scaleType = PreviewView.ScaleType.FILL_CENTER implementationMode = PreviewView.ImplementationMode.COMPATIBLE } sdk.config.previewSurfaceProvider = previewView.surfaceProvider val previewParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(465), Gravity.TOP, ) root.addView(previewView, previewParams) val previewOverlay = View(this).apply { background = GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(0x33000000, 0x00000000, 0xCC05070C.toInt()), ) } val previewOverlayParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(465), Gravity.TOP, ) root.addView( previewOverlay, previewOverlayParams, ) val topPanel = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL setPadding(horizontalInset, topInsetSpacing, horizontalInset, 0) } root.addView( topPanel, FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP, ), ) val topRow = horizontalRow() statusLabel = statusPill("Status", "Idle", CORAL) validationLabel = statusPill("Validation", "--", AMBER) toggleButton = Button(this).apply { text = "Start" setAllCaps(false) setTextColor(Color.BLACK) backgroundTintList = ColorStateList.valueOf(Color.WHITE) setOnClickListener { toggleProcessing() } } topRow.addView(statusLabel, weightedParams(endMargin = dp(8))) topRow.addView(validationLabel, weightedParams(endMargin = dp(8))) topRow.addView(toggleButton, LinearLayout.LayoutParams(dp(92), dp(42))) topPanel.addView(topRow) val panel = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL setPadding(horizontalInset, 0, horizontalInset, 0) } val panelParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM, ) root.addView( panel, panelParams, ) heartRateLabel = textView(sizeSp = 23f, color = CORAL, bold = true) breathingRateLabel = textView(sizeSp = 23f, color = TEAL, bold = true) val rateRow = horizontalRow() rateRow.addView(metricCard("Pulse Rate", heartRateLabel, CORAL), weightedParams(endMargin = dp(8))) rateRow.addView(metricCard("Breathing Rate", breathingRateLabel, TEAL), weightedParams()) panel.addView(rateRow, matchWrapBottomMargin(dp(10))) hrvLabel = textView(sizeSp = 23f, color = TEXT_PRIMARY, bold = true) expressionLabel = textView(sizeSp = 20f, color = TEXT_PRIMARY, bold = true).apply { typeface = android.graphics.Typeface.create(android.graphics.Typeface.MONOSPACE, android.graphics.Typeface.BOLD) includeFontPadding = false } val summaryRow = horizontalRow() summaryRow.addView(metricCard("HRV RMSSD", hrvLabel, MINT), weightedParams(endMargin = dp(8))) summaryRow.addView(metricCard("Expression", expressionLabel, AMBER), weightedParams()) panel.addView(summaryRow, matchWrapBottomMargin(dp(10))) arterialPressureGraphView = SignalGraphView(this, VIOLET) panel.addView(waveformCard("Arterial Pressure", arterialPressureGraphView), matchWrapBottomMargin(dp(10))) chestGraphView = SignalGraphView(this, TEAL) abdomenGraphView = SignalGraphView(this, BLUE) val breathingRow = horizontalRow() breathingRow.addView(waveformCard("Chest Waveform", chestGraphView), weightedParams(endMargin = dp(8))) breathingRow.addView(waveformCard("Abdomen Waveform", abdomenGraphView), weightedParams()) panel.addView(breathingRow, matchHeight(dp(126))) ViewCompat.setOnApplyWindowInsetsListener(root) { _, windowInsets -> val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val previewTopMargin = systemBars.top + topInsetSpacing previewParams.topMargin = previewTopMargin previewView.layoutParams = previewParams previewOverlayParams.topMargin = previewTopMargin previewOverlay.layoutParams = previewOverlayParams topPanel.setPadding( horizontalInset + systemBars.left, previewTopMargin, horizontalInset + systemBars.right, 0, ) panel.setPadding( horizontalInset + systemBars.left, 0, horizontalInset + systemBars.right, 0, ) panelParams.bottomMargin = systemBars.bottom + bottomInsetSpacing panel.layoutParams = panelParams windowInsets } setContentView(root) ViewCompat.requestApplyInsets(root) } private fun toggleProcessing() { when (sdk.processingStatus.value) { ProcessingStatus.RUNNING -> lifecycleScope.launch { runCatching { sdk.stop() } } ProcessingStatus.STARTING, ProcessingStatus.STOPPING -> Unit else -> startProcessing() } } private fun startProcessing() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ) { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) return } lifecycleScope.launch { resetMeasurementUi() runCatching { sdk.start() } .onFailure { statusLabel.text = "Error: ${it.message ?: "Unknown"}" } } } private fun updateProcessingStatus(status: ProcessingStatus?) { when (status) { ProcessingStatus.IDLE -> { statusLabel.text = "Status: Idle" toggleButton.text = "Start" toggleButton.isEnabled = true } ProcessingStatus.STARTING -> { statusLabel.text = "Status: Starting" toggleButton.text = "Starting..." toggleButton.isEnabled = false } ProcessingStatus.RUNNING -> { statusLabel.text = "Status: Running" toggleButton.text = "Stop" toggleButton.isEnabled = true } ProcessingStatus.STOPPING -> { statusLabel.text = "Status: Stopping" toggleButton.text = "Stopping..." toggleButton.isEnabled = false } ProcessingStatus.ERROR -> { toggleButton.text = "Start" toggleButton.isEnabled = true } null -> Unit } } private fun resetMeasurementUi() { latestChestTimestamp = Long.MIN_VALUE latestAbdomenTimestamp = Long.MIN_VALUE latestPressureTimestamp = Long.MIN_VALUE chestGraphView.reset() abdomenGraphView.reset() arterialPressureGraphView.reset() heartRateLabel.text = "-- bpm" expressionLabel.text = "--" breathingRateLabel.text = "-- bpm" hrvLabel.text = "-- ms" } private fun card(buildChildren: LinearLayout.() -> Unit): LinearLayout = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL background = GradientDrawable().apply { setColor(CARD_OVERLAY) cornerRadius = dp(20).toFloat() } setPadding(dp(12), dp(8), dp(12), dp(8)) buildChildren() } private fun metricCard(title: String, value: TextView, accent: Int): LinearLayout = card { addView(textView(title, sizeSp = 12f, color = TEXT_MUTED, bold = true)) addView(value, matchWrapTopMargin(dp(4))) }.apply { background = GradientDrawable().apply { setColor(CARD_OVERLAY) setStroke(dp(1), colorWithAlpha(accent, 70)) cornerRadius = dp(20).toFloat() } } private fun waveformCard(title: String, graphView: SignalGraphView): LinearLayout = card { addView(textView(title, sizeSp = 12f, color = TEXT_PRIMARY, bold = true)) addView(graphView, matchHeight(dp(78)).apply { topMargin = dp(6) }) } private fun statusPill(title: String, value: String, accent: Int): TextView = textView("$title: $value", sizeSp = 12f, color = TEXT_PRIMARY, bold = true).apply { gravity = Gravity.CENTER_VERTICAL setPadding(dp(10), 0, dp(10), 0) background = GradientDrawable().apply { setColor(0x26FFFFFF) setStroke(dp(1), colorWithAlpha(accent, 90)) cornerRadius = dp(18).toFloat() } } private fun horizontalRow(): LinearLayout = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL } private fun textView( text: String = "", sizeSp: Float, color: Int, bold: Boolean = false, ): TextView = TextView(this).apply { this.text = text textSize = sizeSp setTextColor(color) if (bold) typeface = android.graphics.Typeface.DEFAULT_BOLD } private fun matchWrapBottomMargin(bottomMargin: Int): LinearLayout.LayoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, ).apply { this.bottomMargin = bottomMargin } private fun matchHeight(height: Int): LinearLayout.LayoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height, ) private fun matchWrapTopMargin(topMargin: Int): LinearLayout.LayoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, ).apply { this.topMargin = topMargin } private fun weightedParams(endMargin: Int = 0): LinearLayout.LayoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { marginEnd = endMargin } private fun dp(value: Int): Int = (value * resources.displayMetrics.density).roundToInt() private fun colorWithAlpha(color: Int, alpha: Int): Int = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) private fun ExpressionType.expressionName(): String? = when (this) { ExpressionType.ANGRY -> "Angry" ExpressionType.CONTEMPT -> "Contempt" ExpressionType.DISGUST -> "Disgust" ExpressionType.FEAR -> "Fear" ExpressionType.HAPPY -> "Happy" ExpressionType.NEUTRAL -> "Neutral" ExpressionType.SAD -> "Sad" ExpressionType.SURPRISE -> "Surprise" else -> null } } private class SignalGraphView( context: Context, private val graphColor: Int, ) : View(context) { private companion object { const val GRID_ALPHA = 12 const val LINE_ALPHA = 170 } private val samples = ArrayDeque() private val maxPoints = 200 private val inset = 10f private val linePath = Path() private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(GRID_ALPHA, 255, 255, 255) strokeWidth = 1f style = Paint.Style.STROKE } private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = colorWithAlpha(LINE_ALPHA) strokeWidth = 3.5f style = Paint.Style.STROKE strokeCap = Paint.Cap.ROUND strokeJoin = Paint.Join.ROUND } fun appendValue(value: Float) { samples.addLast(value) while (samples.size > maxPoints) { samples.removeFirst() } invalidate() } fun reset() { samples.clear() invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val sampleCount = samples.size if (sampleCount < 2) return val drawLeft = inset val drawTop = inset val drawRight = width - inset val drawBottom = height - inset val drawWidth = drawRight - drawLeft val drawHeight = drawBottom - drawTop for (gridLine in 1..3) { val y = drawTop + drawHeight * (1f - gridLine / 4f) canvas.drawLine(drawLeft, y, drawRight, y, gridPaint) } var minValue = Float.POSITIVE_INFINITY var maxValue = Float.NEGATIVE_INFINITY samples.forEach { minValue = min(minValue, it) maxValue = max(maxValue, it) } val range = if (maxValue - minValue == 0f) 1f else maxValue - minValue linePath.reset() samples.forEachIndexed { index, value -> val x = drawLeft + drawWidth * index / (sampleCount - 1) val normalized = (value - minValue) / range val y = drawBottom - normalized * drawHeight if (index == 0) { linePath.moveTo(x, y) } else { linePath.lineTo(x, y) } } canvas.drawPath(linePath, linePaint) } private fun colorWithAlpha(alpha: Int): Int = Color.argb(alpha, Color.red(graphColor), Color.green(graphColor), Color.blue(graphColor)) } ``` ## Step 5 — Build and run on a phone In Android Studio: 1. Choose a physical Android device as the run destination 2. Build and run the app 3. Allow camera access when Android asks 4. Tap `Start` 5. Wait a few seconds for camera tuning and signal stabilization ## What success looks like When your program is running, you should see all of these: - `Status` and `Validation` chips are visible at the top - the `Start` button changes to `Stop` after processing starts - the camera preview is below the chips - pulse rate, breathing rate, HRV, and expression cards are visible - the arterial pressure waveform is larger than the breathing waveforms - chest and abdomen waveforms both appear on screen - the screen fits in portrait orientation without scrolling ## Expected API key check The first measurement should start after the camera permission prompt is granted. If startup fails with an authentication error, verify that `API_KEY` is valid and authorized for this app. ## Common manual mistakes If the screen does not match the target state, check these first: - the dependency was added to the wrong Gradle module - Gradle sync did not complete after adding the SmartSpectra dependency - `MainActivity.kt` was only partially replaced - `YOUR_API_KEY` was not replaced with a real key - the app is still running an older installed build on the phone --- # C++ Quickstart (Linux) Build a terminal app that measures pulse and breathing rate from a webcam using OpenCV for display. **Platform:** Ubuntu 22.04 / Linux Mint 21 (x86_64) only **Requirements:** webcam, API key from [physiology.presagetech.com](https://physiology.presagetech.com) --- ## 1. Install Dependencies ```bash sudo apt update sudo apt install -y build-essential git lsb-release libcurl4-openssl-dev \ libssl-dev pkg-config libv4l-dev libgles2-mesa-dev libunwind-dev gpg curl ``` Install CMake 3.27+: ```bash curl -L -o cmake-3.27.0-linux-x86_64.sh \ https://github.com/Kitware/CMake/releases/download/v3.27.0/cmake-3.27.0-linux-x86_64.sh chmod +x cmake-3.27.0-linux-x86_64.sh sudo ./cmake-3.27.0-linux-x86_64.sh --skip-license --prefix=/usr/local ``` ## 2. Install the SmartSpectra SDK Add the Presage Debian repository: ```bash curl -s "https://presage-security.github.io/PPA/KEY.gpg" | gpg --dearmor | \ sudo tee /etc/apt/trusted.gpg.d/presage-technologies.gpg >/dev/null sudo curl -s --compressed -o /etc/apt/sources.list.d/presage-technologies.list \ "https://presage-security.github.io/PPA/presage-technologies.list" sudo apt update sudo apt install libsmartspectra-dev ``` This installs headers, shared libraries, and a CMake config so `find_package(SmartSpectra)` works. ## 3. Create the Project ```bash mkdir smartspectra-hello && cd smartspectra-hello ``` ### CMakeLists.txt ```cmake cmake_minimum_required(VERSION 3.27.0) project(SmartSpectraHello CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(SmartSpectra REQUIRED) find_package(OpenCV REQUIRED) add_executable(hello_vitals hello_vitals.cpp) target_link_libraries(hello_vitals SmartSpectra::Container SmartSpectra::Gui ${OpenCV_LIBS} ) ``` ### hello_vitals.cpp ```cpp #include #include #include #include #include #include #include #include using namespace presage::smartspectra; int main(int argc, char** argv) { google::InitGoogleLogging(argv[0]); FLAGS_alsologtostderr = true; // Get API key from argument or environment std::string api_key; if (argc > 1) { api_key = argv[1]; } else if (const char* env_key = std::getenv("SMARTSPECTRA_API_KEY")) { api_key = env_key; } else { std::cout << "Usage: ./hello_vitals YOUR_API_KEY\n"; std::cout << "Or: export SMARTSPECTRA_API_KEY=YOUR_KEY && ./hello_vitals\n"; std::cout << "Get a key at: https://physiology.presagetech.com\n"; return 1; } std::cout << "Starting SmartSpectra...\n"; try { // Configure for continuous measurement via REST API container::settings::Settings< container::settings::OperationMode::Continuous, container::settings::IntegrationMode::Rest > settings; // Camera settings settings.video_source.device_index = 0; settings.video_source.capture_width_px = 1280; settings.video_source.capture_height_px = 720; settings.video_source.codec = presage::camera::CaptureCodec::MJPG; settings.video_source.auto_lock = true; // SDK settings settings.headless = false; settings.enable_edge_metrics = true; settings.verbosity_level = 1; settings.continuous.preprocessed_data_buffer_duration_s = 0.5; settings.integration.api_key = api_key; // Create the processing container auto container = std::make_unique(settings); // Create the on-screen HUD for displaying metrics auto hud = std::make_unique(10, 0, 1260, 400); // Callback: receive vital sign metrics from the API auto status = container->SetOnCoreMetricsOutput( [&hud](const presage::physiology::MetricsBuffer& metrics, int64_t timestamp) { float pulse = 0, breathing = 0; if (!metrics.pulse().rate().empty()) { pulse = metrics.pulse().rate().rbegin()->value(); } if (!metrics.breathing().rate().empty()) { breathing = metrics.breathing().rate().rbegin()->value(); } if (pulse > 0 && breathing > 0) { std::cout << "Pulse: " << pulse << " BPM | Breathing: " << breathing << " BPM\n"; } hud->UpdateWithNewMetrics(metrics); return absl::OkStatus(); } ); if (!status.ok()) { std::cerr << "Failed to set metrics callback: " << status.message() << "\n"; return 1; } // Callback: render each video frame with the HUD overlay status = container->SetOnVideoOutput( [&hud](cv::Mat& frame, int64_t timestamp) { if (auto s = hud->Render(frame); !s.ok()) { std::cerr << "HUD render failed: " << s.message() << "\n"; } cv::imshow("SmartSpectra", frame); char key = cv::waitKey(1) & 0xFF; if (key == 'q' || key == 27) { return absl::CancelledError("User quit"); } return absl::OkStatus(); } ); if (!status.ok()) { std::cerr << "Failed to set video callback: " << status.message() << "\n"; return 1; } // Callback: print imaging/processing status changes status = container->SetOnStatusChange( [](presage::physiology::StatusValue imaging_status) { std::cout << "Status: " << presage::physiology::GetStatusDescription( imaging_status.value()) << "\n"; return absl::OkStatus(); } ); if (!status.ok()) { std::cerr << "Failed to set status callback: " << status.message() << "\n"; return 1; } // Initialize and run std::cout << "Initializing camera...\n"; if (auto s = container->Initialize(); !s.ok()) { std::cerr << "Init failed: " << s.message() << "\n"; return 1; } std::cout << "Ready! Press 's' to start/stop recording. " << "Press 'q' to quit.\n"; if (auto s = container->Run(); !s.ok()) { std::cerr << "Runtime error: " << s.message() << "\n"; return 1; } cv::destroyAllWindows(); std::cout << "Done.\n"; return 0; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; return 1; } } ``` ## 4. Build and Run ```bash mkdir build && cd build cmake .. make -j$(nproc) # Run with your API key ./hello_vitals YOUR_API_KEY # Or use an environment variable export SMARTSPECTRA_API_KEY=YOUR_API_KEY ./hello_vitals ``` A window opens showing the camera feed with a metrics overlay. Press **s** to start/stop recording data. Press **q** to quit. ## Keyboard Controls | Key | Action | |-----|--------| | `s` | Start/stop recording | | `e` | Lock/unlock camera exposure | | `q` or `Esc` | Quit | ## What's Happening 1. The SDK captures frames from your webcam 2. On-device processing extracts facial features and physiological signals 3. Preprocessed data is sent to the Presage REST API for metric calculation 4. Metrics (pulse rate, breathing rate) stream back via callbacks 5. The OpenCV HUD renders real-time traces and values on the video feed ## Platform Support | OS | Architecture | Status | |---|---|---| | Ubuntu 22.04 / Mint 21 | x86_64 (amd64) | Available | | Ubuntu 22.04 / Mint 21 | ARM64 (aarch64) | Partners only | | Ubuntu 24.04 / Mint 22 | x86_64 | Planned | | Debian 11/12 | x86_64, ARM64 | Partners only | | RHEL 9 / Fedora 41 | x86_64, ARM64 | Contact support | | macOS | ARM64 | Planned (from-source) | | Windows | x86_64 | Contact support | ## Uninstalling ```bash sudo apt remove libphysiologyedge-dev libsmartspectra-dev sudo rm /etc/apt/sources.list.d/presage-technologies.list sudo rm /etc/apt/trusted.gpg.d/presage-technologies.gpg sudo apt update ``` ## Troubleshooting **CMake can't find SmartSpectra** - Verify the package installed: `dpkg -l | grep smartspectra` - Check that `find_package(SmartSpectra)` is in your CMakeLists.txt **Camera not found / black screen** - Check device exists: `ls /dev/video*` - Try a different index: `settings.video_source.device_index = 1` - Verify V4L: `v4l2-ctl --list-devices` **"UNAUTHORIZED" status** - Verify your API key at [physiology.presagetech.com](https://physiology.presagetech.com) - Check you're passing it correctly (argument or env var) **Low confidence / no readings** - Ensure at least 60 lux lighting - Keep your face centered, 1–3 ft from the camera - Minimize movement during measurement ## Next Steps - Browse the [full SDK source and samples](https://github.com/Presage-Security/SmartSpectra/tree/main/cpp) - Read the [authentication docs](https://github.com/Presage-Security/SmartSpectra) - See the [Python](/docs/quickstart-python), [Java](/docs/quickstart-java), and [JavaScript](/docs/quickstart-javascript) guides for calling the C++ SDK from other languages --- # Python Quickstart (Linux) Use the SmartSpectra C++ SDK from Python to measure pulse and breathing rate from a webcam. **Platform:** Ubuntu 22.04 / Linux Mint 21 (x86_64) only **Requirements:** Python 3.8+, webcam, API key from [physiology.presagetech.com](https://physiology.presagetech.com) > **Note:** There is no `pip install` package for SmartSpectra. This guide builds a Python-callable C bridge around the C++ SDK. --- ## 1. Install the C++ SDK Install build tools and the SmartSpectra SDK from the Presage PPA: ```bash sudo apt update sudo apt install -y build-essential libcurl4-openssl-dev libssl-dev \ pkg-config libv4l-dev libgles2-mesa-dev libunwind-dev gpg curl # CMake 3.27+ curl -L -o cmake-3.27.0-linux-x86_64.sh \ https://github.com/Kitware/CMake/releases/download/v3.27.0/cmake-3.27.0-linux-x86_64.sh chmod +x cmake-3.27.0-linux-x86_64.sh sudo ./cmake-3.27.0-linux-x86_64.sh --skip-license --prefix=/usr/local # Presage PPA curl -s "https://presage-security.github.io/PPA/KEY.gpg" | gpg --dearmor | \ sudo tee /etc/apt/trusted.gpg.d/presage-technologies.gpg >/dev/null sudo curl -s --compressed -o /etc/apt/sources.list.d/presage-technologies.list \ "https://presage-security.github.io/PPA/presage-technologies.list" sudo apt update sudo apt install libsmartspectra-dev # OpenCV (for the display window) sudo apt install -y libopencv-dev ``` ## 2. Create the Project ```bash mkdir smartspectra-python && cd smartspectra-python ``` ### The C Bridge The C++ SDK has a C++ API. To call it from Python, we create a thin C shared library that wraps the key operations. **smartspectra_bridge.cpp:** ```cpp #include #include #include #include #include #include #include #include #include #include using namespace presage::smartspectra; // Stored metrics (latest readings) struct LatestMetrics { std::mutex mu; float pulse_rate = 0; float breathing_rate = 0; bool has_data = false; }; struct SmartSpectraHandle { std::unique_ptr container; LatestMetrics metrics; std::atomic running{false}; }; extern "C" { // Create and configure a SmartSpectra instance SmartSpectraHandle* smartspectra_create(const char* api_key, int camera_index) { static bool glog_initialized = false; if (!glog_initialized) { google::InitGoogleLogging("smartspectra_python"); FLAGS_alsologtostderr = false; glog_initialized = true; } auto handle = new SmartSpectraHandle(); container::settings::Settings< container::settings::OperationMode::Continuous, container::settings::IntegrationMode::Rest > settings; settings.video_source.device_index = camera_index; settings.video_source.capture_width_px = 1280; settings.video_source.capture_height_px = 720; settings.video_source.codec = presage::camera::CaptureCodec::MJPG; settings.video_source.auto_lock = true; settings.headless = true; // No OpenCV window from C++ side settings.enable_edge_metrics = true; settings.verbosity_level = 0; settings.continuous.preprocessed_data_buffer_duration_s = 0.5; settings.integration.api_key = api_key; handle->container = std::make_unique( settings); // Metrics callback — store latest values handle->container->SetOnCoreMetricsOutput( [handle](const presage::physiology::MetricsBuffer& metrics, int64_t timestamp) { std::lock_guard lock(handle->metrics.mu); if (!metrics.pulse().rate().empty()) { handle->metrics.pulse_rate = metrics.pulse().rate().rbegin()->value(); } if (!metrics.breathing().rate().empty()) { handle->metrics.breathing_rate = metrics.breathing().rate().rbegin()->value(); } handle->metrics.has_data = true; return absl::OkStatus(); } ); // Video callback — just keep the loop alive (headless) handle->container->SetOnVideoOutput( [handle](cv::Mat& frame, int64_t timestamp) { if (!handle->running.load()) { return absl::CancelledError("Stopped"); } return absl::OkStatus(); } ); // Status callback — print to stderr handle->container->SetOnStatusChange( [](presage::physiology::StatusValue status) { fprintf(stderr, "[SmartSpectra] Status: %s\n", presage::physiology::GetStatusDescription( status.value()).c_str()); return absl::OkStatus(); } ); return handle; } // Initialize camera and processing pipeline int smartspectra_initialize(SmartSpectraHandle* handle) { if (!handle) return -1; auto status = handle->container->Initialize(); return status.ok() ? 0 : -1; } // Start processing (blocks the calling thread) int smartspectra_run(SmartSpectraHandle* handle) { if (!handle) return -1; handle->running.store(true); auto status = handle->container->Run(); return status.ok() ? 0 : -1; } // Signal the processing loop to stop void smartspectra_stop(SmartSpectraHandle* handle) { if (handle) { handle->running.store(false); } } // Read the latest metrics (thread-safe) int smartspectra_get_metrics(SmartSpectraHandle* handle, float* pulse_rate, float* breathing_rate) { if (!handle) return -1; std::lock_guard lock(handle->metrics.mu); if (!handle->metrics.has_data) return -1; *pulse_rate = handle->metrics.pulse_rate; *breathing_rate = handle->metrics.breathing_rate; return 0; } // Clean up void smartspectra_destroy(SmartSpectraHandle* handle) { delete handle; } } // extern "C" ``` **CMakeLists.txt:** ```cmake cmake_minimum_required(VERSION 3.27.0) project(SmartSpectraBridge CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(SmartSpectra REQUIRED) find_package(OpenCV REQUIRED) add_library(smartspectra_bridge SHARED smartspectra_bridge.cpp) target_link_libraries(smartspectra_bridge SmartSpectra::Container ${OpenCV_LIBS} ) ``` ### Build the Bridge ```bash mkdir build && cd build cmake .. make -j$(nproc) cd .. ``` This produces `build/libsmartspectra_bridge.so`. ## 3. Write the Python App **hello_vitals.py:** ```python import ctypes import threading import time import os import sys # Load the C bridge library bridge = ctypes.CDLL("./build/libsmartspectra_bridge.so") # Define function signatures bridge.smartspectra_create.restype = ctypes.c_void_p bridge.smartspectra_create.argtypes = [ctypes.c_char_p, ctypes.c_int] bridge.smartspectra_initialize.restype = ctypes.c_int bridge.smartspectra_initialize.argtypes = [ctypes.c_void_p] bridge.smartspectra_run.restype = ctypes.c_int bridge.smartspectra_run.argtypes = [ctypes.c_void_p] bridge.smartspectra_stop.argtypes = [ctypes.c_void_p] bridge.smartspectra_get_metrics.restype = ctypes.c_int bridge.smartspectra_get_metrics.argtypes = [ ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float), ] bridge.smartspectra_destroy.argtypes = [ctypes.c_void_p] def main(): # Get API key api_key = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("SMARTSPECTRA_API_KEY") if not api_key: print("Usage: python hello_vitals.py YOUR_API_KEY") print("Or: export SMARTSPECTRA_API_KEY=YOUR_KEY") print("Get a key at: https://physiology.presagetech.com") sys.exit(1) camera_index = 0 print("Creating SmartSpectra instance...") handle = bridge.smartspectra_create(api_key.encode(), camera_index) if not handle: print("Failed to create SmartSpectra instance") sys.exit(1) print("Initializing camera...") if bridge.smartspectra_initialize(handle) != 0: print("Failed to initialize") bridge.smartspectra_destroy(handle) sys.exit(1) # Run processing in a background thread (it blocks) def run_processing(): bridge.smartspectra_run(handle) thread = threading.Thread(target=run_processing, daemon=True) thread.start() print("Running! Reading metrics every 2 seconds. Press Ctrl+C to stop.\n") pulse = ctypes.c_float() breathing = ctypes.c_float() try: while thread.is_alive(): time.sleep(2) if bridge.smartspectra_get_metrics( handle, ctypes.byref(pulse), ctypes.byref(breathing) ) == 0: print( f"Pulse: {pulse.value:.0f} BPM | " f"Breathing: {breathing.value:.1f} BPM" ) else: print("Waiting for metrics...") except KeyboardInterrupt: print("\nStopping...") bridge.smartspectra_stop(handle) thread.join(timeout=5) bridge.smartspectra_destroy(handle) print("Done.") if __name__ == "__main__": main() ``` ## 4. Run ```bash python3 hello_vitals.py YOUR_API_KEY # Or with environment variable export SMARTSPECTRA_API_KEY=YOUR_API_KEY python3 hello_vitals.py ``` Output: ``` Creating SmartSpectra instance... Initializing camera... Running! Reading metrics every 2 seconds. Press Ctrl+C to stop. Waiting for metrics... Waiting for metrics... Pulse: 72 BPM | Breathing: 16.2 BPM Pulse: 73 BPM | Breathing: 15.8 BPM ^C Stopping... Done. ``` ## How It Works ``` Python (ctypes) → C bridge (.so) → SmartSpectra C++ SDK → Camera + REST API ``` 1. `hello_vitals.py` loads the C bridge shared library via `ctypes` 2. The bridge wraps the SmartSpectra C++ container API with `extern "C"` functions 3. Processing runs in a background thread — the C++ SDK captures frames and sends preprocessed data to the Presage REST API 4. Python polls for the latest metrics on the main thread ## Platform Support This approach only works on **Linux x86_64** (Ubuntu 22.04 / Mint 21) because the SmartSpectra C++ SDK Debian package is only available for that platform. See the [C++ quickstart](/docs/quickstart-cpp) for the full platform matrix. ## Troubleshooting **`OSError: cannot open shared object file`** - Make sure `build/libsmartspectra_bridge.so` exists — run `make` in the `build/` directory - Or set `LD_LIBRARY_PATH`: `export LD_LIBRARY_PATH=./build:$LD_LIBRARY_PATH` **Camera not found** - Check: `ls /dev/video*` - Try `camera_index = 1` in the Python script **No metrics after 60+ seconds** - Verify your API key is valid - Check lighting (at least 60 lux) and face positioning For other issues, see the [C++ troubleshooting](/docs/quickstart-cpp#troubleshooting) section. ## Next Steps - See the [C++ quickstart](/docs/quickstart-cpp) for the full C++ API details - Browse the [SDK source](https://github.com/Presage-Security/SmartSpectra/tree/main/cpp) --- # JavaScript Quickstart (Linux) Use the SmartSpectra C++ SDK from Node.js to measure pulse and breathing rate from a webcam. **Platform:** Ubuntu 22.04 / Linux Mint 21 (x86_64) only **Requirements:** Node.js 18+, webcam, API key from [physiology.presagetech.com](https://physiology.presagetech.com) > **Note:** There is no `npm install` package for SmartSpectra. This guide builds a Node.js native addon around the C++ SDK. --- ## 1. Install the C++ SDK and Node.js ```bash sudo apt update sudo apt install -y build-essential libcurl4-openssl-dev libssl-dev \ pkg-config libv4l-dev libgles2-mesa-dev libunwind-dev gpg curl \ libopencv-dev # CMake 3.27+ curl -L -o cmake-3.27.0-linux-x86_64.sh \ https://github.com/Kitware/CMake/releases/download/v3.27.0/cmake-3.27.0-linux-x86_64.sh chmod +x cmake-3.27.0-linux-x86_64.sh sudo ./cmake-3.27.0-linux-x86_64.sh --skip-license --prefix=/usr/local # Node.js 18+ (via NodeSource) curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs # Presage PPA curl -s "https://presage-security.github.io/PPA/KEY.gpg" | gpg --dearmor | \ sudo tee /etc/apt/trusted.gpg.d/presage-technologies.gpg >/dev/null sudo curl -s --compressed -o /etc/apt/sources.list.d/presage-technologies.list \ "https://presage-security.github.io/PPA/presage-technologies.list" sudo apt update sudo apt install libsmartspectra-dev ``` ## 2. Create the Project ```bash mkdir smartspectra-node && cd smartspectra-node npm init -y npm install node-addon-api ``` ### Native Addon (C++) **smartspectra_addon.cpp:** ```cpp #include #include #include #include #include #include #include #include #include #include #include using namespace presage::smartspectra; struct SmartSpectraHandle { std::unique_ptr container; std::mutex mu; float pulse_rate = 0; float breathing_rate = 0; bool has_data = false; std::atomic running{false}; std::thread processing_thread; }; static SmartSpectraHandle* g_handle = nullptr; Napi::Value Create(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsString()) { Napi::TypeError::New(env, "API key string required") .ThrowAsJavaScriptException(); return env.Null(); } static bool glog_init = false; if (!glog_init) { google::InitGoogleLogging("smartspectra_node"); FLAGS_alsologtostderr = false; glog_init = true; } std::string api_key = info[0].As().Utf8Value(); int camera_index = info.Length() > 1 ? info[1].As().Int32Value() : 0; g_handle = new SmartSpectraHandle(); container::settings::Settings< container::settings::OperationMode::Continuous, container::settings::IntegrationMode::Rest > settings; settings.video_source.device_index = camera_index; settings.video_source.capture_width_px = 1280; settings.video_source.capture_height_px = 720; settings.video_source.codec = presage::camera::CaptureCodec::MJPG; settings.video_source.auto_lock = true; settings.headless = true; settings.enable_edge_metrics = true; settings.verbosity_level = 0; settings.continuous.preprocessed_data_buffer_duration_s = 0.5; settings.integration.api_key = api_key; g_handle->container = std::make_unique( settings); g_handle->container->SetOnCoreMetricsOutput( [](const presage::physiology::MetricsBuffer& metrics, int64_t timestamp) { std::lock_guard lock(g_handle->mu); if (!metrics.pulse().rate().empty()) g_handle->pulse_rate = metrics.pulse().rate().rbegin()->value(); if (!metrics.breathing().rate().empty()) g_handle->breathing_rate = metrics.breathing().rate().rbegin()->value(); g_handle->has_data = true; return absl::OkStatus(); } ); g_handle->container->SetOnVideoOutput( [](cv::Mat& frame, int64_t timestamp) { if (!g_handle->running.load()) return absl::CancelledError("Stopped"); return absl::OkStatus(); } ); g_handle->container->SetOnStatusChange( [](presage::physiology::StatusValue status) { fprintf(stderr, "[SmartSpectra] Status: %s\n", presage::physiology::GetStatusDescription( status.value()).c_str()); return absl::OkStatus(); } ); return Napi::Boolean::New(env, true); } Napi::Value Initialize(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!g_handle) { Napi::Error::New(env, "Call create() first").ThrowAsJavaScriptException(); return env.Null(); } bool ok = g_handle->container->Initialize().ok(); return Napi::Boolean::New(env, ok); } Napi::Value Start(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!g_handle) { Napi::Error::New(env, "Call create() first").ThrowAsJavaScriptException(); return env.Null(); } g_handle->running.store(true); // Run processing in a separate thread (it blocks) g_handle->processing_thread = std::thread([] { g_handle->container->Run(); }); return Napi::Boolean::New(env, true); } Napi::Value GetMetrics(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!g_handle) return env.Null(); std::lock_guard lock(g_handle->mu); if (!g_handle->has_data) return env.Null(); Napi::Object result = Napi::Object::New(env); result.Set("pulseRate", Napi::Number::New(env, g_handle->pulse_rate)); result.Set("breathingRate", Napi::Number::New(env, g_handle->breathing_rate)); return result; } Napi::Value Stop(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (g_handle) { g_handle->running.store(false); if (g_handle->processing_thread.joinable()) { g_handle->processing_thread.join(); } } return Napi::Boolean::New(env, true); } Napi::Value Destroy(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (g_handle) { delete g_handle; g_handle = nullptr; } return Napi::Boolean::New(env, true); } Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set("create", Napi::Function::New(env, Create)); exports.Set("initialize", Napi::Function::New(env, Initialize)); exports.Set("start", Napi::Function::New(env, Start)); exports.Set("getMetrics", Napi::Function::New(env, GetMetrics)); exports.Set("stop", Napi::Function::New(env, Stop)); exports.Set("destroy", Napi::Function::New(env, Destroy)); return exports; } NODE_API_MODULE(smartspectra_addon, Init) ``` ### Build Configuration **binding.gyp:** ```json { "targets": [ { "target_name": "smartspectra_addon", "sources": ["smartspectra_addon.cpp"], "include_dirs": [ "/dev/null || echo '')", "-lsmartspectra_container", "-lglog" ], "cflags!": ["-fno-exceptions"], "cflags_cc!": ["-fno-exceptions"], "cflags_cc": [ "-std=c++17", "/dev/null || echo '')" ], "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"] } ] } ``` > **Note:** The `libraries` and `cflags_cc` lines use `pkg-config` to find SmartSpectra and OpenCV. If `pkg-config` doesn't find SmartSpectra, you may need to adjust the paths to match your installation (typically `/usr/local/lib` and `/usr/local/include`). Alternatively, use **CMake** to build the addon with `cmake-js` for more reliable dependency resolution: ```bash npm install cmake-js ``` **CMakeLists.txt:** ```cmake cmake_minimum_required(VERSION 3.27.0) project(SmartSpectraAddon CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(SmartSpectra REQUIRED) find_package(OpenCV REQUIRED) # cmake-js setup include_directories(${CMAKE_JS_INC}) file(GLOB SOURCE_FILES "smartspectra_addon.cpp") add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC} "${CMAKE_SOURCE_DIR}/node_modules/node-addon-api" ) target_link_libraries(${PROJECT_NAME} SmartSpectra::Container ${OpenCV_LIBS} ${CMAKE_JS_LIB} ) ``` ## 3. Build Using node-gyp (simpler): ```bash npx node-gyp configure build ``` Or using cmake-js (more reliable with CMake-based deps): ```bash npx cmake-js build ``` The native addon is compiled to `build/Release/smartspectra_addon.node`. ## 4. Write the App **hello_vitals.js:** ```javascript const smartspectra = require('./build/Release/smartspectra_addon'); const apiKey = process.argv[2] || process.env.SMARTSPECTRA_API_KEY; if (!apiKey) { console.log('Usage: node hello_vitals.js YOUR_API_KEY'); console.log('Or: export SMARTSPECTRA_API_KEY=YOUR_KEY'); console.log('Get a key at: https://physiology.presagetech.com'); process.exit(1); } console.log('Creating SmartSpectra instance...'); smartspectra.create(apiKey); console.log('Initializing camera...'); if (!smartspectra.initialize()) { console.error('Failed to initialize'); process.exit(1); } console.log('Starting processing...'); smartspectra.start(); console.log('Running! Reading metrics every 2 seconds.'); console.log('Press Ctrl+C to stop.\n'); // Poll for metrics const interval = setInterval(() => { const metrics = smartspectra.getMetrics(); if (metrics) { console.log( `Pulse: ${metrics.pulseRate.toFixed(0)} BPM | ` + `Breathing: ${metrics.breathingRate.toFixed(1)} BPM` ); } else { console.log('Waiting for metrics...'); } }, 2000); // Graceful shutdown process.on('SIGINT', () => { console.log('\nStopping...'); clearInterval(interval); smartspectra.stop(); smartspectra.destroy(); console.log('Done.'); process.exit(0); }); ``` ## 5. Run ```bash node hello_vitals.js YOUR_API_KEY # Or with environment variable export SMARTSPECTRA_API_KEY=YOUR_API_KEY node hello_vitals.js ``` Output: ``` Creating SmartSpectra instance... Initializing camera... Starting processing... Running! Reading metrics every 2 seconds. Press Ctrl+C to stop. Waiting for metrics... Waiting for metrics... Pulse: 72 BPM | Breathing: 16.2 BPM Pulse: 73 BPM | Breathing: 15.8 BPM ^C Stopping... Done. ``` ## How It Works ``` Node.js → Native addon (N-API) → SmartSpectra C++ SDK → Camera + REST API ``` 1. `smartspectra_addon.cpp` wraps the C++ SDK as a Node.js native addon using N-API 2. `hello_vitals.js` calls the addon to create, initialize, and start processing 3. The SDK runs in a background thread, capturing frames and calling the Presage REST API 4. JavaScript polls for the latest metrics using `setInterval` ## Platform Support This approach only works on **Linux x86_64** (Ubuntu 22.04 / Mint 21). There is no browser-based JavaScript SDK at this time. ## Troubleshooting **`Error: Cannot find module './build/Release/smartspectra_addon'`** - Run `npx node-gyp configure build` (or `npx cmake-js build`) first - Check that `build/Release/smartspectra_addon.node` exists **node-gyp build errors** - Ensure `node-addon-api` is installed: `npm install node-addon-api` - Verify SmartSpectra is installed: `dpkg -l | grep smartspectra` - Try the CMake approach with `cmake-js` instead **Camera not found** - Check: `ls /dev/video*` - Change `cameraIndex` in the `create()` call For other issues, see the [C++ troubleshooting](/docs/quickstart-cpp#troubleshooting) section. ## Next Steps - See the [C++ quickstart](/docs/quickstart-cpp) for the full C++ API - Browse the [SDK source](https://github.com/Presage-Security/SmartSpectra/tree/main/cpp) --- # Java Quickstart (Linux) Use the SmartSpectra C++ SDK from Java via JNI to measure pulse and breathing rate from a webcam. **Platform:** Ubuntu 22.04 / Linux Mint 21 (x86_64) only **Requirements:** JDK 11+, webcam, API key from [physiology.presagetech.com](https://physiology.presagetech.com) > **Looking for Android?** See the [Kotlin quickstart](/docs/quickstart-kotlin) — the Android SDK has a native Kotlin/Java API and does not require this JNI approach. > **Note:** There is no Maven/Gradle package for SmartSpectra on desktop. This guide builds a JNI bridge around the C++ SDK. --- ## 1. Install the C++ SDK and JDK ```bash sudo apt update sudo apt install -y build-essential libcurl4-openssl-dev libssl-dev \ pkg-config libv4l-dev libgles2-mesa-dev libunwind-dev gpg curl \ openjdk-17-jdk libopencv-dev # CMake 3.27+ curl -L -o cmake-3.27.0-linux-x86_64.sh \ https://github.com/Kitware/CMake/releases/download/v3.27.0/cmake-3.27.0-linux-x86_64.sh chmod +x cmake-3.27.0-linux-x86_64.sh sudo ./cmake-3.27.0-linux-x86_64.sh --skip-license --prefix=/usr/local # Presage PPA curl -s "https://presage-security.github.io/PPA/KEY.gpg" | gpg --dearmor | \ sudo tee /etc/apt/trusted.gpg.d/presage-technologies.gpg >/dev/null sudo curl -s --compressed -o /etc/apt/sources.list.d/presage-technologies.list \ "https://presage-security.github.io/PPA/presage-technologies.list" sudo apt update sudo apt install libsmartspectra-dev ``` ## 2. Create the Project ```bash mkdir smartspectra-java && cd smartspectra-java ``` ### Java Class **SmartSpectra.java:** ```java public class SmartSpectra { static { System.loadLibrary("smartspectra_jni"); } // Native methods (implemented in C++) private static native long nativeCreate(String apiKey, int cameraIndex); private static native int nativeInitialize(long handle); private static native int nativeRun(long handle); private static native void nativeStop(long handle); private static native float[] nativeGetMetrics(long handle); private static native void nativeDestroy(long handle); private long handle; public SmartSpectra(String apiKey) { this(apiKey, 0); } public SmartSpectra(String apiKey, int cameraIndex) { this.handle = nativeCreate(apiKey, cameraIndex); if (this.handle == 0) { throw new RuntimeException("Failed to create SmartSpectra instance"); } } public void initialize() { if (nativeInitialize(handle) != 0) { throw new RuntimeException("Failed to initialize SmartSpectra"); } } /** Starts processing. Blocks the calling thread. */ public void run() { nativeRun(handle); } public void stop() { nativeStop(handle); } /** * Returns [pulseRate, breathingRate] or null if no data yet. */ public float[] getMetrics() { return nativeGetMetrics(handle); } public void destroy() { if (handle != 0) { nativeDestroy(handle); handle = 0; } } } ``` ### Hello World App **HelloVitals.java:** ```java public class HelloVitals { public static void main(String[] args) { String apiKey = args.length > 0 ? args[0] : System.getenv("SMARTSPECTRA_API_KEY"); if (apiKey == null || apiKey.isEmpty()) { System.out.println("Usage: java HelloVitals YOUR_API_KEY"); System.out.println("Or: export SMARTSPECTRA_API_KEY=YOUR_KEY"); System.out.println("Get a key at: https://physiology.presagetech.com"); System.exit(1); } SmartSpectra spectra = new SmartSpectra(apiKey); System.out.println("Initializing camera..."); spectra.initialize(); // Run processing in a background thread (it blocks) Thread processingThread = new Thread(() -> spectra.run()); processingThread.setDaemon(true); processingThread.start(); System.out.println("Running! Reading metrics every 2 seconds."); System.out.println("Press Ctrl+C to stop.\n"); Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("\nStopping..."); spectra.stop(); spectra.destroy(); System.out.println("Done."); })); while (processingThread.isAlive()) { try { Thread.sleep(2000); } catch (InterruptedException e) { break; } float[] metrics = spectra.getMetrics(); if (metrics != null) { System.out.printf("Pulse: %.0f BPM | Breathing: %.1f BPM%n", metrics[0], metrics[1]); } else { System.out.println("Waiting for metrics..."); } } } } ``` ### JNI Bridge (C++) First, compile the Java class and generate the JNI header: ```bash javac SmartSpectra.java javac -h . SmartSpectra.java ``` This generates `SmartSpectra.h` with the native method signatures. Now create the implementation: **smartspectra_jni.cpp:** ```cpp #include #include #include #include #include #include #include #include #include #include using namespace presage::smartspectra; struct SmartSpectraHandle { std::unique_ptr container; std::mutex mu; float pulse_rate = 0; float breathing_rate = 0; bool has_data = false; std::atomic running{false}; }; extern "C" { JNIEXPORT jlong JNICALL Java_SmartSpectra_nativeCreate( JNIEnv* env, jclass, jstring apiKey, jint cameraIndex) { static bool glog_init = false; if (!glog_init) { google::InitGoogleLogging("smartspectra_java"); FLAGS_alsologtostderr = false; glog_init = true; } const char* key = env->GetStringUTFChars(apiKey, nullptr); auto handle = new SmartSpectraHandle(); container::settings::Settings< container::settings::OperationMode::Continuous, container::settings::IntegrationMode::Rest > settings; settings.video_source.device_index = cameraIndex; settings.video_source.capture_width_px = 1280; settings.video_source.capture_height_px = 720; settings.video_source.codec = presage::camera::CaptureCodec::MJPG; settings.video_source.auto_lock = true; settings.headless = true; settings.enable_edge_metrics = true; settings.verbosity_level = 0; settings.continuous.preprocessed_data_buffer_duration_s = 0.5; settings.integration.api_key = key; env->ReleaseStringUTFChars(apiKey, key); handle->container = std::make_unique( settings); handle->container->SetOnCoreMetricsOutput( [handle](const presage::physiology::MetricsBuffer& metrics, int64_t timestamp) { std::lock_guard lock(handle->mu); if (!metrics.pulse().rate().empty()) handle->pulse_rate = metrics.pulse().rate().rbegin()->value(); if (!metrics.breathing().rate().empty()) handle->breathing_rate = metrics.breathing().rate().rbegin()->value(); handle->has_data = true; return absl::OkStatus(); } ); handle->container->SetOnVideoOutput( [handle](cv::Mat& frame, int64_t timestamp) { if (!handle->running.load()) return absl::CancelledError("Stopped"); return absl::OkStatus(); } ); handle->container->SetOnStatusChange( [](presage::physiology::StatusValue status) { fprintf(stderr, "[SmartSpectra] Status: %s\n", presage::physiology::GetStatusDescription( status.value()).c_str()); return absl::OkStatus(); } ); return reinterpret_cast(handle); } JNIEXPORT jint JNICALL Java_SmartSpectra_nativeInitialize( JNIEnv*, jclass, jlong ptr) { auto handle = reinterpret_cast(ptr); return handle->container->Initialize().ok() ? 0 : -1; } JNIEXPORT jint JNICALL Java_SmartSpectra_nativeRun( JNIEnv*, jclass, jlong ptr) { auto handle = reinterpret_cast(ptr); handle->running.store(true); return handle->container->Run().ok() ? 0 : -1; } JNIEXPORT void JNICALL Java_SmartSpectra_nativeStop( JNIEnv*, jclass, jlong ptr) { auto handle = reinterpret_cast(ptr); handle->running.store(false); } JNIEXPORT jfloatArray JNICALL Java_SmartSpectra_nativeGetMetrics( JNIEnv* env, jclass, jlong ptr) { auto handle = reinterpret_cast(ptr); std::lock_guard lock(handle->mu); if (!handle->has_data) return nullptr; jfloatArray result = env->NewFloatArray(2); float values[2] = {handle->pulse_rate, handle->breathing_rate}; env->SetFloatArrayRegion(result, 0, 2, values); return result; } JNIEXPORT void JNICALL Java_SmartSpectra_nativeDestroy( JNIEnv*, jclass, jlong ptr) { delete reinterpret_cast(ptr); } } // extern "C" ``` **CMakeLists.txt:** ```cmake cmake_minimum_required(VERSION 3.27.0) project(SmartSpectraJNI CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(SmartSpectra REQUIRED) find_package(OpenCV REQUIRED) find_package(JNI REQUIRED) add_library(smartspectra_jni SHARED smartspectra_jni.cpp) target_include_directories(smartspectra_jni PRIVATE ${JNI_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR} # For generated SmartSpectra.h ) target_link_libraries(smartspectra_jni SmartSpectra::Container ${OpenCV_LIBS} ) ``` ## 3. Build and Run ```bash # Compile Java and generate JNI header javac SmartSpectra.java javac -h . SmartSpectra.java # Build the native library mkdir -p build && cd build cmake .. make -j$(nproc) cd .. # Run java -Djava.library.path=./build HelloVitals YOUR_API_KEY # Or with environment variable export SMARTSPECTRA_API_KEY=YOUR_API_KEY java -Djava.library.path=./build HelloVitals ``` Output: ``` Initializing camera... Running! Reading metrics every 2 seconds. Press Ctrl+C to stop. Waiting for metrics... Waiting for metrics... Pulse: 72 BPM | Breathing: 16.2 BPM Pulse: 73 BPM | Breathing: 15.8 BPM ^C Stopping... Done. ``` ## How It Works ``` Java → JNI → SmartSpectra C++ SDK → Camera + REST API ``` 1. `SmartSpectra.java` loads the JNI shared library and declares native methods 2. `smartspectra_jni.cpp` implements those methods using the C++ SDK container API 3. Processing runs in a daemon thread — the SDK captures frames and calls the REST API 4. Java polls for the latest metrics on the main thread ## Platform Support This approach only works on **Linux x86_64** (Ubuntu 22.04 / Mint 21). For **Android**, use the native Android SDK instead — see the [Kotlin quickstart](/docs/quickstart-kotlin). ## Troubleshooting **`UnsatisfiedLinkError: no smartspectra_jni in java.library.path`** - Make sure `-Djava.library.path=./build` points to the directory containing `libsmartspectra_jni.so` - Or: `export LD_LIBRARY_PATH=./build:$LD_LIBRARY_PATH` **JNI header not generated** - Use `javac -h . SmartSpectra.java` (requires JDK 10+) - On older JDKs, use `javah -jni SmartSpectra` **Camera not found** - Check: `ls /dev/video*` - Try changing `cameraIndex` to `1` in `HelloVitals.java` For other issues, see the [C++ troubleshooting](/docs/quickstart-cpp#troubleshooting) section. ## Next Steps - For Android apps, use the native SDK: [Kotlin quickstart](/docs/quickstart-kotlin) - See the [C++ quickstart](/docs/quickstart-cpp) for the full C++ API - Browse the [SDK source](https://github.com/Presage-Security/SmartSpectra/tree/main/cpp) --- # API Reference Complete reference for the Presage REST API. ## Base URL ``` https://api.presage.tech/v1 ``` ## Authentication All API requests require a Bearer token in the Authorization header: ``` Authorization: Bearer psg_your_api_key_here ``` API keys can be created and managed at [the developer portal](https://physiology.presagetech.com/auth/login). ## Rate Limits | Plan | Requests/Minute | Requests/Day | |------|-----------------|--------------| | Free | 10 | 1,000 | | Pro | 100 | 50,000 | | Enterprise | Custom | Unlimited | Rate limit headers are included in all responses: - `X-RateLimit-Limit`: Your limit - `X-RateLimit-Remaining`: Remaining requests - `X-RateLimit-Reset`: Unix timestamp when limit resets ## Endpoints ### POST /predict Make a prediction using a Presage model. **Request Body:** ```json { "model": "presage-vitals-v1", "input": { "heart_rate": 72, "blood_pressure_systolic": 120, "blood_pressure_diastolic": 80, "respiratory_rate": 16, "temperature": 98.6, "oxygen_saturation": 98 }, "options": { "include_confidence": true, "include_explanations": false } } ``` **Response:** ```json { "id": "pred_abc123", "model": "presage-vitals-v1", "model_version": "1.2.0", "prediction": { "risk_score": 0.12, "risk_level": "low", "confidence": 0.94 }, "created_at": "2024-01-15T10:30:00Z", "latency_ms": 45 } ``` ### POST /predict/batch Make multiple predictions in a single request (up to 100). **Request Body:** ```json { "model": "presage-vitals-v1", "inputs": [ { "heart_rate": 72, "blood_pressure_systolic": 120, ... }, { "heart_rate": 85, "blood_pressure_systolic": 135, ... } ] } ``` **Response:** ```json { "id": "batch_xyz789", "model": "presage-vitals-v1", "predictions": [ { "risk_score": 0.12, "risk_level": "low", "confidence": 0.94 }, { "risk_score": 0.45, "risk_level": "moderate", "confidence": 0.89 } ], "created_at": "2024-01-15T10:30:00Z" } ``` ### GET /models List available models. **Response:** ```json { "models": [ { "id": "presage-vitals-v1", "name": "Vitals Risk Assessment", "version": "1.2.0", "description": "Predicts patient risk from vital signs", "status": "active" }, { "id": "presage-clinical-v1", "name": "Clinical Decision Support", "version": "2.0.1", "description": "Clinical decision support from patient data", "status": "active" } ] } ``` ### GET /models/{model_id} Get details for a specific model, including its model card. **Response:** ```json { "id": "presage-vitals-v1", "name": "Vitals Risk Assessment", "version": "1.2.0", "description": "Predicts patient risk from vital signs", "input_schema": { ... }, "output_schema": { ... }, "model_card": { "intended_use": "...", "limitations": "...", "performance_metrics": { ... } } } ``` ### GET /usage Get your current usage statistics. **Response:** ```json { "period": "2024-01", "plan": "pro", "api_calls": { "used": 12500, "limit": 50000, "remaining": 37500 }, "by_model": { "presage-vitals-v1": 10000, "presage-clinical-v1": 2500 } } ``` ## Error Responses All errors follow this format: ```json { "error": { "code": "invalid_request", "message": "The 'heart_rate' field is required", "param": "input.heart_rate" } } ``` ### Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `invalid_api_key` | 401 | API key is invalid or expired | | `rate_limit_exceeded` | 429 | Too many requests | | `invalid_request` | 400 | Request body is malformed | | `model_not_found` | 404 | Requested model doesn't exist | | `insufficient_quota` | 402 | Plan quota exceeded | | `internal_error` | 500 | Server error (retry with backoff) | ## SDKs Official SDKs handle authentication, retries, and error handling: - **Python**: `pip install presage` - **Node.js**: `npm install @presage/sdk` - **Go**: `go get github.com/Presage-Security/SmartSpectra` - **Ruby**: `gem install presage` ## Webhooks Enterprise plans can configure webhooks for async notifications. Contact support@presage.tech to enable. --- # Examples & Sample Projects Real-world code examples and complete sample projects to help you build with Presage. ## Quick Examples ### Basic Prediction (Python) ```python from presage import Presage client = Presage() result = client.predict( model="presage-vitals-v1", input={ "heart_rate": 88, "blood_pressure_systolic": 145, "blood_pressure_diastolic": 92, "respiratory_rate": 22, "temperature": 99.1, "oxygen_saturation": 94 } ) print(f"Risk Level: {result.prediction['risk_level']}") print(f"Risk Score: {result.prediction['risk_score']:.2f}") print(f"Confidence: {result.prediction['confidence']:.2f}") ``` ### Batch Processing (Python) ```python from presage import Presage import pandas as pd client = Presage() # Load patient data from CSV df = pd.read_csv("patient_vitals.csv") # Convert to list of input dicts inputs = df.to_dict('records') # Batch predict (up to 100 at a time) results = client.predict_batch( model="presage-vitals-v1", inputs=inputs ) # Add predictions back to dataframe df['risk_score'] = [r['risk_score'] for r in results.predictions] df['risk_level'] = [r['risk_level'] for r in results.predictions] # Filter high-risk patients high_risk = df[df['risk_level'].isin(['high', 'critical'])] print(f"Found {len(high_risk)} high-risk patients") ``` ### Async Predictions (Node.js) ```javascript import { Presage } from '@presage/sdk'; const client = new Presage(); async function assessPatients(patients) { const results = await Promise.all( patients.map(patient => client.predict({ model: 'presage-vitals-v1', input: patient.vitals }) ) ); return results.map((result, i) => ({ patientId: patients[i].id, ...result.prediction })); } // Usage const patients = [ { id: 'P001', vitals: { heart_rate: 72, ... } }, { id: 'P002', vitals: { heart_rate: 95, ... } }, ]; const assessments = await assessPatients(patients); console.log(assessments); ``` ### Error Handling (Python) ```python from presage import Presage from presage.exceptions import ( PresageAPIError, RateLimitError, InvalidInputError, AuthenticationError ) import time client = Presage() def predict_with_retry(input_data, max_retries=3): for attempt in range(max_retries): try: return client.predict( model="presage-vitals-v1", input=input_data ) except RateLimitError as e: if attempt < max_retries - 1: wait_time = e.retry_after or (2 ** attempt) print(f"Rate limited. Waiting {wait_time}s...") time.sleep(wait_time) else: raise except InvalidInputError as e: print(f"Invalid input: {e.message}") print(f"Problem field: {e.param}") raise except AuthenticationError: print("Check your API key") raise except PresageAPIError as e: print(f"API error: {e.message}") raise ``` ### Webhook Integration (Node.js) ```javascript import express from 'express'; import { Presage } from '@presage/sdk'; const app = express(); const client = new Presage(); app.use(express.json()); // Receive vitals from monitoring device app.post('/webhook/vitals', async (req, res) => { const { patientId, vitals, timestamp } = req.body; try { const result = await client.predict({ model: 'presage-vitals-v1', input: vitals }); // Alert if high risk if (['high', 'critical'].includes(result.prediction.risk_level)) { await sendAlert({ patientId, riskLevel: result.prediction.risk_level, riskScore: result.prediction.risk_score, timestamp }); } // Store result await saveAssessment(patientId, result); res.json({ status: 'processed', riskLevel: result.prediction.risk_level }); } catch (error) { console.error('Assessment failed:', error); res.status(500).json({ error: 'Assessment failed' }); } }); app.listen(3000); ``` --- ## Sample Projects ### 1. Patient Monitoring Dashboard A complete React dashboard for monitoring patient risk scores in real-time. **Features**: - Real-time vitals display - Risk score visualization - Alert notifications - Historical trends **Repository**: [github.com/Presage-Security/SmartSpectra](https://github.com/Presage-Security/SmartSpectra) ```bash git clone https://github.com/Presage-Security/SmartSpectra cd example-dashboard npm install cp .env.example .env # Add your PRESAGE_API_KEY npm run dev ``` --- ### 2. EHR Integration (FHIR) Python library for integrating Presage with FHIR-based EHR systems. **Features**: - FHIR R4 patient data extraction - Automatic vital signs mapping - Observation resource creation for results **Repository**: [github.com/Presage-Security/SmartSpectra](https://github.com/Presage-Security/SmartSpectra) ```python from presage_fhir import FHIRClient, PresageIntegration fhir = FHIRClient(base_url="https://your-ehr.com/fhir") presage = PresageIntegration() # Get patient and assess patient = fhir.get_patient("12345") vitals = fhir.get_latest_vitals(patient.id) assessment = presage.assess(vitals) # Write result back as Observation fhir.create_observation( patient=patient, code="presage-risk-score", value=assessment.risk_score ) ``` --- ### 3. Mobile SDK Demo (React Native) Cross-platform mobile app demonstrating Presage integration for remote patient monitoring. **Repository**: [github.com/Presage-Security/SmartSpectra](https://github.com/Presage-Security/SmartSpectra) --- ### 4. CLI Tool Command-line tool for quick predictions and batch processing. ```bash pip install presage-cli # Single prediction presage predict --model presage-vitals-v1 \ --hr 72 --sbp 120 --dbp 80 --rr 16 --temp 98.6 --spo2 98 # Batch from CSV presage batch --model presage-vitals-v1 \ --input patients.csv --output results.csv ``` **Repository**: [github.com/Presage-Security/SmartSpectra](https://github.com/Presage-Security/SmartSpectra) --- ## Community Projects Projects built by the Presage developer community: | Project | Description | Author | |---------|-------------|--------| | presage-grafana | Grafana dashboard for Presage metrics | @community-dev | | presage-slack | Slack bot for risk alerts | @community-dev | | presage-airflow | Airflow DAGs for batch processing | @community-dev | *Want your project featured? Email community@presage.tech* --- ## Request a Demo Need help with a specific integration? Our solutions team can help. - Schedule a call: [email us](mailto:support@presagetech.com) - Email: solutions@presage.tech --- # Model Cards Detailed documentation for each Presage model, following the Model Card framework for transparent AI documentation. --- ## presage-vitals-v1 ### Model Details | | | |---|---| | **Name** | Presage Vitals Risk Assessment | | **Version** | 1.2.0 | | **Type** | Gradient Boosted Trees + Neural Network Ensemble | | **Released** | March 2024 | | **Last Updated** | November 2024 | ### Intended Use **Primary Use**: Predict patient deterioration risk within 24 hours based on vital signs. **Intended Users**: - Healthcare providers in hospital settings - Clinical decision support systems - Remote patient monitoring platforms **Out of Scope**: - Diagnostic decisions without clinician oversight - Pediatric patients (< 18 years) - separate model available - Outpatient/home settings without clinical supervision ### Input Requirements ```json { "heart_rate": 72, // Required, beats/min (30-250) "blood_pressure_systolic": 120, // Required, mmHg (60-260) "blood_pressure_diastolic": 80, // Required, mmHg (30-160) "respiratory_rate": 16, // Required, breaths/min (4-60) "temperature": 98.6, // Required, °F (90-110) "oxygen_saturation": 98, // Required, % (50-100) "age": 45, // Optional, years "consciousness_level": "alert" // Optional: alert/verbal/pain/unresponsive } ``` ### Output ```json { "risk_score": 0.12, // 0.0-1.0 continuous score "risk_level": "low", // low/moderate/high/critical "confidence": 0.94, // Model confidence "contributing_factors": [ {"factor": "respiratory_rate", "impact": "positive", "magnitude": 0.3} ] } ``` ### Performance Metrics Evaluated on held-out test set (n=25,000 patients): | Metric | Value | 95% CI | |--------|-------|--------| | AUROC | 0.89 | 0.87-0.91 | | AUPRC | 0.45 | 0.42-0.48 | | Sensitivity @ 90% Specificity | 0.85 | 0.82-0.88 | | Calibration Error (ECE) | 0.03 | - | ### Limitations - **Trained on US hospital data**: May have reduced performance on populations with different baseline health profiles - **Vital signs only**: Does not incorporate lab values, medications, or medical history - **Snapshot predictions**: Based on single timepoint; trends not considered - **Not validated for**: ICU patients, post-surgical patients, pregnant patients ### Fairness Evaluation | Subgroup | AUROC | Sensitivity | |----------|-------|-------------| | Male | 0.89 | 0.85 | | Female | 0.88 | 0.84 | | Age 18-40 | 0.87 | 0.82 | | Age 41-65 | 0.89 | 0.86 | | Age 65+ | 0.90 | 0.87 | ### Training Data - **Source**: De-identified EHR data from 12 US hospitals - **Size**: 2.1 million patient encounters - **Period**: 2018-2023 - **Labels**: 24-hour deterioration events (ICU transfer, rapid response, death) --- ## presage-clinical-v1 ### Model Details | | | |---|---| | **Name** | Presage Clinical Decision Support | | **Version** | 2.0.1 | | **Type** | Transformer-based Neural Network | | **Released** | June 2024 | | **Last Updated** | January 2025 | ### Intended Use **Primary Use**: Provide clinical decision support by analyzing patient data and suggesting relevant considerations. **Intended Users**: - Emergency department physicians - Hospitalists - Advanced practice providers **Out of Scope**: - Autonomous decision-making - Replacing clinical judgment - Legal or billing documentation ### Input Requirements ```json { "chief_complaint": "chest pain", "vitals": { ... }, "demographics": { "age": 55, "sex": "male" }, "medical_history": ["hypertension", "diabetes"], "medications": ["metformin", "lisinopril"], "recent_labs": { ... } // Optional } ``` ### Output ```json { "considerations": [ { "condition": "Acute Coronary Syndrome", "likelihood": "moderate", "suggested_workup": ["ECG", "Troponin", "Chest X-ray"], "reasoning": "55yo male with HTN, DM presenting with chest pain" } ], "risk_flags": ["cardiovascular_risk_factors"], "confidence": 0.87 } ``` ### Performance Metrics | Metric | Value | |--------|-------| | Agreement with Expert Panel | 91% | | Critical Diagnosis Sensitivity | 97% | | Specificity for Low-Risk Discharge | 89% | ### Limitations - **English language only** - **US clinical practice patterns**: Recommendations reflect US standard of care - **Not real-time**: Does not monitor for changes after initial assessment - **Requires structured input**: Free-text notes must be preprocessed --- ## presage-diagnostic-v1 ### Model Details | | | |---|---| | **Name** | Presage Diagnostic Assistant | | **Version** | 0.9.0 (Beta) | | **Type** | Multi-modal Neural Network | | **Released** | Beta - Limited Access | ### Status This model is currently in **beta** and available only to select research partners. Clinical trial in progress (NCT04ZZZZZZ). ### Intended Use Multi-condition diagnostic support combining clinical data, lab values, and imaging reports. ### Access Contact research@presage.tech for beta access criteria. --- ## Requesting Updates For questions about model cards or to report issues: - Technical: support@presage.tech - Clinical: clinical@presage.tech - Research: research@presage.tech --- # Pricing Simple, transparent pricing. Start free, scale as you grow. ## Plans ### Community (Free) - 10K Virtual Sessions/Mo - 150 Insight Tokens/Mo - Remote Streaming ### Pro ($99/mo) - Unlimited for up to 5,000 users - 6.5M Insight Tokens/Mo (additional tokens @ $15/M as used) - 2x w/ storage & reference data - BYO-LLM Option (free with your own LLM key) ### Platform ($500+/mo) - Unlimited for up to 30,000 users - 35M Insight Tokens/Mo (additional tokens @ $15/M as used) - All Pro Features plus: - FHIR / SSO / audit / 99.9% SLA ### Regulated (Custom) - As low as $0.01/User/Mo - FDA-cleared algorithms - FedRAMP/Gov Systems - Medical-grade SLA ## Get Started - **Community / Pro / Platform**: [Sign up](https://physiology.presagetech.com/auth/login) - **Regulated**: [Book a meeting](mailto:support@presagetech.com) ## Questions? - Sales: support@presagetech.com - Schedule a call: [Book a meeting](mailto:support@presagetech.com) --- # Frequently Asked Questions ## About SmartSpectra ### What is SmartSpectra and how does it work? SmartSpectra is a contactless health-sensing SDK that turns any camera into a real-time biometric sensor. It tracks facial and chest movements to extract anonymized health metrics including pulse rate, breathing rate, HRV, and more—all without requiring any wearables or physical contact. ### What physiological signals and features can SmartSpectra measure? SmartSpectra can measure: - Pulse rate - Breathing rate - Heart rate variability (HRV) - Apnea detection - Inhale/exhale ratio - Upper/lower breath trace - Respiratory line length - Facial landmarks - Blink/talk detection - Relative blood pressure - Emotional responses - And much more ### How accurate is the SmartSpectra SDK? Clinical trials demonstrate: - **Heart rate**: Less than 1.62% RMSD differential versus hospital-grade equipment - **Breathing rate**: Less than 1.71% RMSD differential versus hospital-grade equipment Accuracy depends on proper lighting conditions and minimal motion during measurement. --- ## Integration & Technical ### Which platforms and programming languages does SmartSpectra support? SmartSpectra supports: - **iOS** (Swift) - **Android** (Kotlin) - **Cross-platform C++** Full documentation is available on GitHub, and integration typically requires under 10 lines of code. ### How do I integrate SmartSpectra into my app? Integration is straightforward: 1. Sign up for an API key at [the developer portal](https://physiology.presagetech.com/auth/login) 2. Embed the SDK (under 10 lines of code) 3. Connect to the API 4. Select which biometrics you want to measure 5. Data begins streaming within 3-5 seconds See our [Quickstart Guide](/docs) for detailed instructions. ### What environmental conditions are needed for accurate measurements? For optimal results: - **Distance**: Hold phone 1-2 ft away, or mount camera 3-5 ft away (rear camera works at 15-20 ft) - **Lighting**: Minimum 60 lux (moderate indoor lighting) - **Motion**: Moderate motion is acceptable, but stillness improves accuracy - **Duration**: 30-60 seconds for complete measurement ### I want the tech, but I don't have a developer No problem! We can host you and your users in our Sixth Sense app. No coding required—get instant presence for your new digital experience. [Contact us](mailto:support@presagetech.com) for details. --- ## Privacy & Data ### How does Presage protect privacy and handle user data? Privacy is built into SmartSpectra by design: - The SDK does **not** access or require any personal data to operate - All results are delivered on a purely anonymized basis to your app - No facial images or video are stored or transmitted - Only anonymized metrics are used (with consent) for algorithm improvement See our [Privacy Policy](https://physiology.presagetech.com/privacy-policy) for complete details. ### What data do you store? By default, we do not store input data or predictions. The SDK processes video locally on-device and only transmits anonymized vital sign metrics. Enterprise customers can configure: - Prediction storage for audit trails - Custom retention policies - Data residency requirements --- ## Regulatory & Clinical ### Is SmartSpectra a medical device or FDA-approved product? SmartSpectra has passed clinical trials demonstrating accuracy comparable to hospital-grade equipment. However, it is **not yet FDA-approved as a medical device**. Current intended use is for **wellness applications only**—not for diagnosis, treatment, or medical decision-making. ### How are your models validated? All models undergo rigorous validation: 1. Internal validation on held-out test data 2. External validation at independent clinical sites 3. Prospective clinical trials comparing to reference devices 4. Ongoing performance monitoring across demographics See our [Studies](/studies) page for published results and model cards. --- ## Pricing & Plans ### Is there a free tier? Yes! Our **Community Edition** is free and includes: - Unlimited measurements for development - All SDK features - Community support via Discord - Non-commercial use No credit card required to start. ### What are the paid plans? | Plan | Price | Best For | |------|-------|----------| | **Community** | Free | Learning, prototyping, non-commercial | | **Business** | $0.007/measurement ($200/mo min) | Production commercial apps | | **Enterprise** | Custom | Large scale, on-premise, custom needs | See our [Pricing](/pricing) page for full details. ### Can I upgrade or downgrade anytime? Yes. Plan changes are prorated and take effect immediately. --- ## Getting Involved ### How do I get involved? We welcome participation from: **Students**: - Use the free Community Edition for projects - Try our Sixth Sense app for personal use **Researchers**: - We offer lab support for diagnostic development - Academic partnerships available **Developers**: - Join our [Discord](https://discord.com/invite/K7KNmuX7KT) community - Contribute to our [GitHub](https://github.com/Presage-Security/SmartSpectra) projects [Contact us](mailto:support@presagetech.com) to discuss your needs. --- ## Support ### How do I get help? | Channel | Best For | Response Time | |---------|----------|---------------| | [Documentation](/docs) | Self-service | Instant | | [Discord](https://discord.com/invite/K7KNmuX7KT) | Community help | < 1 hour | | support@presage.tech | Technical issues | 24 hours (Business), 4 hours (Enterprise) | ### How do I report a bug? - **GitHub Issues**: For SDK bugs - **Email**: support@presage.tech for API issues - **Security vulnerabilities**: security@presage.tech --- ## Still have questions? - **Sales**: support@presagetech.com - **Support**: support@presage.tech - **Book a meeting**: [email us](mailto:support@presagetech.com) - **Discord**: [discord.gg/presage](https://discord.com/invite/K7KNmuX7KT)