Skip to main content

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 and maxAttempts = 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's wearingOptimizationDidFinish(_:) 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.strings table with the canonical patient copy in (currently) English; the other 16 CardioMood locales are on the roadmap.
  • Setting maxAttempts = 1 disables the retry-prompt UX entirely — first miss is terminal.
  • Setting includePreparation = false skips 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 call UserProfileApi.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 == 1 AND quality ≥ 50. The spo2Quality value carried in .success is the raw 0–100 reading at the moment success was observed.
  • position is 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). The unknown(String) case is the forward-compatibility escape hatch — host apps compiled against an older SDK header keep working when new failure categories are added.
  • .bracelectDisconnected is 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:

  1. Preparation (skipped if includePreparation == false) — three substeps in order: skin tone + hair density + wrist; strap tightness; bracelet placement. The strap-tightness step is where UserProfileApi.sendUserProfileToBracelet(force: true) fires, so the bracelet picks up any skin / hair / wrist edits made in the preceding picker.
  2. Instructions — "Remain still for 1 minute" with the Sitting / Lying reference cards. User taps Start to fire startSpecialModeCustom(value: 4) on the bracelet.
  3. Running — animated orbit + live progress %. The view polls the bracelet every 5 seconds via getSpecialModeStatus(typeOfParam: 4).
  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.
  5. Success — terminal screen with quality + duration.
  6. 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 push setProfile to 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 … }