Blood Pressure measurement
Only available for 287-2 bracelet
The Corsano bracelet computes a cuffless (NIBP) blood-pressure estimate. Before those readings are trustworthy, the bracelet's NIBP model has to be calibrated against a reference cuff — the user takes two (sometimes three) measurements with a real BP cuff, and the SDK feeds those readings to the bracelet so the firmware can fit its model.
The whole calibration workflow (instructions → measure → record → retry → success/failure) is bundled as a self-contained SwiftUI view, BPInitializationView, exactly like WearingOptimizationView. Host apps present the view, hand it a source of cuff readings, and don't touch the bracelet's NIBP plan directly.
Integration: BPInitializationView
The SDK owns the bracelet side of calibration: switching the bracelet into its NIBP-calibration plan, feeding each cuff reading, reading the firmware's accept/reject verdict, and restoring the normal monitoring plan when the flow ends. What the SDK does not own is the BP cuff itself — the cuff's BLE link belongs to the host app. You supply readings through a cuffProvider closure and (optionally) report pairing state through cuffIsPaired.
See the sample app for the live integration (the Settings screen presents the sheet and handles the result).
Quickstart
The minimal integration is a button that toggles a sheet presenting BPInitializationView, plus a cuffProvider that returns readings from your cuff integration:
import SwiftUI
import CorsanoSDK
struct ContentView: View {
@State private var showBPInit = false
@State private var lastResult: BPInitializationResult?
var body: some View {
VStack {
Button("Calibrate NIBP") { showBPInit = true }
if let lastResult { Text(describe(lastResult)) }
}
.sheet(isPresented: $showBPInit) {
BPInitializationView(
cuffProvider: {
// Take one measurement with the host-owned BP cuff.
// Return `nil` if no reading could be obtained.
await myCuffController.takeMeasurement()
},
onCompleted: { result in
lastResult = result
showBPInit = false
}
)
}
}
private func describe(_ result: BPInitializationResult) -> String {
switch result {
case .success(let n): return "Calibrated (\(n) measurements)"
case .cancelledByUser: return "Cancelled"
case .braceletDisconnected: return "Bracelet disconnected"
case .cuffError(let e): return "Cuff error: \(e)"
case .failedAfterRetries(let n): return "Not calibrated after \(n) attempt(s)"
}
}
}
That's the entire host-side wiring. Every screen, the bracelet driving (plan switch, feeding readings, reading the verdict, plan restore), the two/three-measurement rule, the retry budget, and the preparation substeps all live inside the SDK.
Public initializer
public init(
config: BPInitializationConfig = .default,
delegate: BPInitializationDelegate? = nil,
cuffProvider: @escaping @MainActor () async -> CuffReading?,
cuffIsPaired: @escaping @MainActor () -> Bool = { true },
onCuffPairingRequested: (@MainActor () -> Void)? = nil,
onCompleted: @escaping (BPInitializationResult) -> Void
)
config— branding (colors / logo), the deviation threshold, retry budget, and the preparation toggle. Defaults to a neutral blue accent,deviationThresholdMmHg = 10,maxAttempts = 3,includePreparation = true.delegate— optional telemetry hooks (start, per-step funnel, per-measurement, per-attempt, terminal). All methods have no-op default implementations, so you implement only the ones you care about.cuffProvider— required. The host-supplied source of cuff readings. Called once per measurement; it's anasyncclosure so you can drive a real cuff BLE exchange. Returnnilwhen no reading could be obtained — the flow then ends in.cuffError.cuffIsPaired— host check for whether a BP cuff is paired. Defaults to{ true }. When it returnsfalse, the flow opens on the needs-cuff-pairing screen instead of the instructions.onCuffPairingRequested— optional hook fired when the user taps "Pair a cuff" on the needs-cuff-pairing screen (present your own cuff-pairing UI here).onCompleted— required terminal callback. Carries the same value the delegate'sbpInitializationDidFinish(_:)receives.
The view hands control back to the host once the terminal screen is acknowledged — your onCompleted closure is the right place to dismiss the presenting sheet.
Configuration
public struct BPInitializationConfig: Sendable {
public var branding: Branding
public var deviationThresholdMmHg: Int // default 10
public var maxAttempts: Int // default 3
public var includePreparation: Bool // default true
public struct Branding: Sendable {
public var accentColor: Color // buttons, links, progress
public var successColor: Color // success seal + success CTA (default green)
public var errorColor: Color // warning icon + error CTA (default orange)
public var logo: Image? // optional mark above the title
}
}
Branding example:
let config = BPInitializationConfig(
branding: .init(
accentColor: Color("BrandBlue"),
logo: Image("BrandLogo")
),
deviationThresholdMmHg: 10,
maxAttempts: 3,
includePreparation: true
)
BPInitializationView(config: config, cuffProvider: { … }) { result in … }
Notes:
- The two-measurement rule. A calibration attempt always takes 2 cuff measurements. A 3rd is taken only when the first two readings differ by more than
deviationThresholdMmHgon systolic or diastolic. The default10matches the Corsano patient app. maxAttempts = 1disables the retry-prompt UX entirely — a first unaccepted session is terminal.includePreparation = falseskips the preparation checklist (cuff fit, arm position, sit-and-rest) and opens directly on the instructions screen. Useful when the host already gathers that guidance elsewhere.- Strings are not host-overridable. The SDK ships its own fixed string table (currently English); the other CardioMood locales are on the roadmap. Mirrors the Wearing-Optimization module.
Result handling
public enum BPInitializationResult {
case success(measurementCount: Int)
case cancelledByUser
case braceletDisconnected
case cuffError(CuffError)
case failedAfterRetries(attemptCount: Int)
}
.success(measurementCount:)— the firmware accepted the calibration.measurementCountis how many cuff measurements were taken in the successful session (2 or 3)..cancelledByUser— the user dismissed the flow (Cancel button or swipe-to-dismiss) before it concluded..braceletDisconnected— the BLE link to the bracelet dropped mid-flow. Terminal, and distinct from a per-attempt failure..cuffError(CuffError)— the hostcuffProviderreturnednil(couldn't deliver a reading)..failedAfterRetries(attemptCount:)— every attempt in themaxAttemptsbudget ran and none was accepted by the firmware.
The cuff reading you supply
cuffProvider returns a CuffReading (or nil):
public struct CuffReading: Sendable {
public var systolic: Int
public var diastolic: Int
public var pulse: Int
/// Cuff-reported MAP. When nil, the SDK derives it as
/// `diastolic + (systolic - diastolic) / 3`.
public var meanArterialPressure: Int?
public init(systolic: Int, diastolic: Int, pulse: Int,
meanArterialPressure: Int? = nil)
}
public enum CuffError {
case noReading
case cuffDisconnected
case unknown(String) // forward-compatibility escape hatch
}
The SDK owns neither the cuff's BLE link nor its readings — it only feeds the values you return into the bracelet and consumes the firmware's quality verdict. The unknown(String) case lets hosts compiled against an older SDK header keep working when new categories are added.
Optional telemetry — BPInitializationDelegate
If you want funnel attribution, implement this protocol on a class-typed observer and pass it as the delegate argument:
@MainActor
public protocol BPInitializationDelegate: AnyObject {
func bpInitializationDidStart()
func bpInitialization(didEnterStep step: BPInitializationStep)
func bpInitialization(didFinishMeasurement index: Int, braceletAccepted: Bool)
func bpInitialization(didFinishAttempt attemptIndex: Int, succeeded: Bool)
func bpInitializationDidFinish(_ result: BPInitializationResult)
}
All five hooks have default no-op implementations — implement just the ones you need. index (per measurement, within a session) and attemptIndex (per calibration session) are both 1-based.
Step values:
public enum BPInitializationStep {
case needsCuffPairing // no cuff paired (entry only; never returned to)
case instructions // how to position the cuff, what to expect
case measuring // requesting cuff readings + feeding the bracelet
case measurementRecorded // per-measurement sys/dia/pulse, auto-advances
case retryPrompt // session not accepted; retry CTA (budget remaining)
case finalSuccess // terminal success screen
case finalFailure // terminal failure screen (budget exhausted)
}
The terminal bpInitializationDidFinish(_:) callback receives the same BPInitializationResult value as the onCompleted closure — listen on whichever channel fits your code best.
Flow reference
The bundled flow walks through the same screens regardless of host:
- Needs cuff pairing (only if
cuffIsPaired()returnsfalse) — prompts the user to pair a cuff; "Pair a cuff" firesonCuffPairingRequested. Entry step only; the flow never returns here. - Instructions — how to fit the cuff and what to expect. With
includePreparation == truethis includes the preparation checklist (cuff fit, arm position, sit and rest). The user taps Start to begin the first calibration session. - Measuring — "Press START on the BP cuff monitor". For each measurement the SDK calls your
cuffProvider, feeds the returned reading to the bracelet, and reads the firmware verdict. Two measurements per session; a third only if the first two deviate by more thandeviationThresholdMmHg. - Measurement recorded — shows the just-captured sys/dia/pulse and auto-advances to the next measurement (or the terminal screen). A firmware-rejected reading is shown here exactly like an accepted one — no mid-sequence warning, matching the patient app.
- Retry prompt — shown when a session was not accepted and the attempt budget is not yet exhausted.
- Calibration complete — terminal success screen; the bracelet's NIBP model is calibrated and its normal monitoring plan restored. Lists each measurement with a per-measurement quality dot.
- Calibration summary — terminal screen after the retry budget is exhausted; shows the same summary (never an alarming warning) and simply ends the flow.
Requirements
- iOS 15+.
- Bracelet must be paired and connected before presenting the view — standard SDK pairing applies (see the pairing section in the overview).
- The user profile should be populated (height / weight / wrist / gender / metric unit / birthdate) and the bracelet plan set before calibration; the SDK switches to the NIBP-calibration plan for the duration of the flow and restores the previous plan when it ends.
- A BP cuff integration on the host side. The SDK does not connect to the cuff — you provide readings through
cuffProvider, and (optionally) surface pairing state throughcuffIsPaired/onCuffPairingRequested.
Listening for the terminal result via the delegate
final class BPInitTelemetry: NSObject, BPInitializationDelegate {
func bpInitializationDidStart() {
Analytics.track("bpinit_start")
}
func bpInitialization(didEnterStep step: BPInitializationStep) {
Analytics.track("bpinit_step", properties: ["step": "\(step)"])
}
func bpInitialization(didFinishMeasurement index: Int, braceletAccepted: Bool) {
Analytics.track("bpinit_measurement",
properties: ["index": index, "accepted": braceletAccepted])
}
func bpInitialization(didFinishAttempt attemptIndex: Int, succeeded: Bool) {
Analytics.track("bpinit_attempt",
properties: ["n": attemptIndex, "succeeded": succeeded])
}
func bpInitializationDidFinish(_ result: BPInitializationResult) {
Analytics.track("bpinit_finish", properties: ["result": "\(result)"])
}
}
// Hold the observer for the lifetime of the screen:
@StateObject private var telemetry = BPInitTelemetry()
BPInitializationView(config: .default,
delegate: telemetry,
cuffProvider: { … }) { result in … }