Wearing Optimization
Only available for 287-2 bracelet, from Firmware 7.0
The wearing optimization is a process on the bracelet that calibrates our PPG algorithms to the user's skin. The LED power adapts to the user's skin type based on this calibration. Bracelet readings like SpO₂ depend on it — it should be performed at least once after pairing.
The process takes ~2 minutes if the patient stays still, up to ~4 minutes if movement is detected. The patient should be seated and still, or lying down and still.
Integration: WearingOptimizationView
Since the latest SDK release the entire workflow (preparation → instructions → running → retry → success/failure) is bundled as a self-contained SwiftUI view. Host apps present the view, hand it a result callback, and don't touch the bracelet directly.
See the sample app for the live integration:
Shared/UI/Settings/WearingOptimization.swift— sheet trigger + result handling.
The previous manual integration (MetricCalibrationViewModel.startSpecialModeCustom(value: 4) / getSpecialModeStatus() / specialModeStatus(...) delegate) is no longer the supported integration path and has been removed from the sample app.
Quickstart
The minimal integration is a button that toggles a sheet presenting WearingOptimizationView:
import SwiftUI
import CorsanoSDK
struct ContentView: View {
@State private var showWO = false
@State private var lastResult: WearingOptimizationResult?
var body: some View {
VStack {
Button("Run Wearing Optimization") { showWO = true }
if let lastResult { Text(describe(lastResult)) }
}
.sheet(isPresented: $showWO) {
WearingOptimizationView { result in
lastResult = result
showWO = false
}
}
}
private func describe(_ result: WearingOptimizationResult) -> String {
switch result {
case .success(let q, let pos):
return "Success — quality \(q)% (\(pos == .seated ? "seated" : "supine"))"
case .cancelledByUser:
return "Cancelled"
case .bracelectDisconnected:
return "Bracelet disconnected"
case .failedAfterRetries(let n, _):
return "Failed after \(n) attempt(s)"
}
}
}
That's the entire host-side wiring. Every screen, the bracelet driving (startSpecialModeCustom etc.), the 5-second status polling, the retry budget, and the preparation substeps all live inside the SDK.
Public initializer
public init(
config: WearingOptimizationConfig = .default,
delegate: WearingOptimizationDelegate? = nil,
onCompleted: @escaping (WearingOptimizationResult) -> Void
)
config— branding (colors / logo), retry budget, and the toggle for the multi-step preparation phase. Defaults to a neutral blue accent andmaxAttempts = 3,includePreparation = true.delegate— optional telemetry hooks (start, per-step funnel, per-attempt result, terminal). All methods have no-op default implementations, so you can implement only the ones you care about.onCompleted— terminal callback. Required. Carries the same value the delegate'swearingOptimizationDidFinish(_:)receives.
The view automatically dismisses control back to the host once the terminal screen is acknowledged — your onCompleted closure is the right place to close the presenting sheet / cover / navigation destination.
Configuration
public struct WearingOptimizationConfig: Sendable {
public var branding: Branding
public var maxAttempts: Int // default 3
public var includePreparation: Bool // default true
public struct Branding: Sendable {
public var accentColor: Color // buttons, progress, dot-orbit tint
public var successColor: Color // default neptune green
public var errorColor: Color // default reference orange
public var logo: Image? // optional mark above the title
}
}
Branding example:
let config = WearingOptimizationConfig(
branding: .init(
accentColor: Color("BrandBlue"),
logo: Image("BrandLogo")
),
maxAttempts: 3,
includePreparation: true
)
WearingOptimizationView(config: config) { result in … }
Notes:
- Strings are not host-overridable. The SDK ships its own
WearingOptimization.stringstable with the canonical patient copy in (currently) English; the other 16 CardioMood locales are on the roadmap. - Setting
maxAttempts = 1disables the retry-prompt UX entirely — first miss is terminal. - Setting
includePreparation = falseskips the multi-step preparation phase (strap tightness, bracelet placement, wrist / skin / hair-density pickers) and opens directly on the instructions screen. Useful when the host app already gathers that information elsewhere — but if you do skip it, you must callUserProfileApi.sendUserProfileToBracelet(force: true)yourself before opening WO, because that BLE side-effect normally fires from the bundled preparation flow.
Result handling
public enum WearingOptimizationResult {
case success(spo2Quality: Int, position: BraceletPosition)
case cancelledByUser
case bracelectDisconnected
case failedAfterRetries(attemptCount: Int, lastReason: FailureReason)
}
public enum BraceletPosition { case seated, supine }
public enum FailureReason {
case ppgSignalQualityTooLow
case bracelectNotWornCorrectly
case bleTimeout
case unknown(String)
}
- A session is successful when the bracelet reports
isSuccess == 1AND quality ≥ 50. Thespo2Qualityvalue carried in.successis the raw 0–100 reading at the moment success was observed. positionis inferred from the bracelet's accelerometer signal at success time in the bundled patient flow..failedAfterRetries(.lastReason:)distinguishes between recoverable failure modes (ppgSignalQualityTooLow,bracelectNotWornCorrectly) and harder ones (bleTimeout). Theunknown(String)case is the forward-compatibility escape hatch — host apps compiled against an older SDK header keep working when new failure categories are added..bracelectDisconnectedis terminal and separate from a per-attempt failure: it means the BLE link itself dropped during the flow, not that an individual attempt produced a bad signal.
Optional telemetry — WearingOptimizationDelegate
If you want funnel attribution, implement this protocol on a class-typed observer and pass it as the delegate argument:
@MainActor
public protocol WearingOptimizationDelegate: AnyObject {
func wearingOptimizationDidStart()
func wearingOptimization(didEnterStep step: WearingOptimizationStep)
func wearingOptimization(didFinishAttempt attemptIndex: Int,
succeeded: Bool,
reason: FailureReason?)
func wearingOptimizationDidFinish(_ result: WearingOptimizationResult)
}
All four hooks have default no-op implementations — implement just the ones you need. attemptIndex is 1-based; reason is non-nil only when succeeded == false.
Step values:
public enum WearingOptimizationStep {
case preparation // multi-step prep phase
case armedPreparing // instructions screen ("position your arm…")
case running // bracelet computing PPG quality
case retryPrompt // failure CTA between attempts
case finalSuccess // terminal success screen
case finalFailure // terminal failure screen
}
The terminal wearingOptimizationDidFinish(_:) callback receives the same WearingOptimizationResult 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:
- Preparation (skipped if
includePreparation == false) — three substeps in order: skin tone + hair density + wrist; strap tightness; bracelet placement. The strap-tightness step is whereUserProfileApi.sendUserProfileToBracelet(force: true)fires, so the bracelet picks up any skin / hair / wrist edits made in the preceding picker. - Instructions — "Remain still for 1 minute" with the Sitting / Lying reference cards. User taps Start to fire
startSpecialModeCustom(value: 4)on the bracelet. - Running — animated orbit + live progress %. The view polls the bracelet every 5 seconds via
getSpecialModeStatus(typeOfParam: 4). - Retry prompt — shown if an attempt produced an unacceptable signal AND retries are available. Includes a Seated / Supine position picker that drives a 2-step (supine) or 3-step (seated, with an extra hand-flat-on-desk step) instructional modal before the retry runs.
- Success — terminal screen with quality + duration.
- Failure — terminal screen after the retry budget is exhausted.
Requirements
- iOS 15+ (the running view uses
TimelineView/Canvas; the preparation animations are gated@available(iOS 15.0, *)). - Bracelet must be paired and connected before presenting the view. Standard SDK pairing applies — see the pairing section in the overview.
- Bracelet plan should be set (
setPlanDirectly) and the user profile populated (height / weight / wrist / gender / metric unit / birthdate) before running the optimization. If the bundled preparation phase is in use it will also pushsetProfileto the bracelet at the strap-tightness substep.
Listening for the terminal result via the delegate
final class WOTelemetry: NSObject, WearingOptimizationDelegate {
func wearingOptimizationDidStart() {
Analytics.track("wo_start")
}
func wearingOptimization(didEnterStep step: WearingOptimizationStep) {
Analytics.track("wo_step", properties: ["step": "\(step)"])
}
func wearingOptimization(didFinishAttempt n: Int, succeeded: Bool, reason: FailureReason?) {
Analytics.track("wo_attempt", properties: [
"n": n, "succeeded": succeeded, "reason": "\(reason ?? .unknown(""))"
])
}
func wearingOptimizationDidFinish(_ result: WearingOptimizationResult) {
Analytics.track("wo_finish", properties: ["result": "\(result)"])
}
}
// Hold the observer for the lifetime of the screen:
@StateObject private var telemetry = WOTelemetry()
WearingOptimizationView(config: .default, delegate: telemetry) { result in … }