> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Synheart Session - Swift SDK

> Native Swift SDK for iOS and watchOS with on-device SessionEngine, pluggable providers, and behavioral signal fusion

## Overview

The Synheart Session Swift SDK provides a native SessionEngine that captures heart rate data, computes session metrics on-device, and optionally fuses behavioral signals alongside HR data. It runs on iOS, macOS, watchOS, and tvOS. The engine is timer-driven, emitting session frames at configurable intervals and delivering a session summary when complete. The SDK supports pluggable `BiosignalProvider` (for HR sources) and `BehaviorProvider` (for behavioral signals).

<Note>
  The SDK computes mean HR locally from sampled biosignals. HRV (`rmssd_ms`, `hr_sdnn_ms`, `pnn50`) and the HSI 1.3 envelope are produced upstream by the Synheart runtime and ingested into the session via `ingestHsiMetrics()`. Without that ingest call, HRV fields will be `0.0`.
</Note>

## Installation

### Swift Package Manager

Add to your `Package.swift`:

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/synheart-ai/synheart-session-swift.git", from: "0.1.0")
]
```

Or in Xcode:

1. File > Add Packages...
2. Enter: `https://github.com/synheart-ai/synheart-session-swift.git`

### Requirements

* iOS 13.0+ / macOS 10.15+ / watchOS 6.0+ / tvOS 13.0+
* Swift 5.9+
* Xcode 15.0+

## Quick Start

```swift theme={null}
import SynheartSession

let engine = SessionEngine()

let config = SessionConfig(
    sessionId: UUID().uuidString,
    mode: .focus,
    durationSec: 30,
    profile: ComputeProfile(windowSec: 10, emitIntervalSec: 3)
)

try engine.start(config: config) { event in
    let type = event["type"] as? String ?? ""

    switch type {
    case "session_started":
        print("Session started")

    case "session_frame":
        let seq = event["seq"] as? Int ?? 0
        if let metrics = event["metrics"] as? [String: Any] {
            let hr = metrics["hr_mean_bpm"] as? Double ?? 0
            print("#\(seq) HR: \(hr) bpm")
        }

    case "session_summary":
        let duration = event["duration_actual_sec"] as? Int ?? 0
        print("Session complete: \(duration)s")

    default:
        break
    }
}

// Stop early
try engine.stop(sessionId: config.sessionId)
```

## Types

### SessionConfig

```swift theme={null}
let config = SessionConfig(
    sessionId: UUID().uuidString,      // unique session identifier
    mode: .focus,                       // .focus or .breathing
    durationSec: 60,                   // session duration in seconds
    profile: ComputeProfile(
        windowSec: 10,                 // sliding window size
        emitIntervalSec: 3             // frame emission interval
    ),
    windowLabel: "morning-check"       // optional label
)
```

Set `includeRawSamples: true` to enable raw biosignal streaming alongside session frames:

```swift theme={null}
let config = SessionConfig(
    sessionId: UUID().uuidString,
    mode: .focus,
    durationSec: 60,
    profile: ComputeProfile(windowSec: 10, emitIntervalSec: 3, rawEmitIntervalSec: 1),
    includeRawSamples: true
)
```

You can also create a `SessionConfig` from a dictionary, which is how the WatchConnectivity relay deserializes commands from the phone:

```swift theme={null}
let config = try SessionConfig(from: [
    "session_id": "abc-123",
    "mode": "focus",
    "duration_sec": 30,
    "profile": ["window_sec": 10, "emit_interval_sec": 3]
])
```

### SessionMode

| Value        | Raw           | Description                     |
| ------------ | ------------- | ------------------------------- |
| `.focus`     | `"focus"`     | Steady-state measurement        |
| `.breathing` | `"breathing"` | Guided breathing with HRV focus |

### ComputeProfile

| Parameter            | Type   | Default | Description                                                  |
| -------------------- | ------ | ------- | ------------------------------------------------------------ |
| `windowSec`          | `Int`  | 60      | Sliding window size in seconds                               |
| `emitIntervalSec`    | `Int`  | 5       | Frame emission interval in seconds                           |
| `rawEmitIntervalSec` | `Int?` | `nil`   | Raw biosignal frame interval (defaults to `emitIntervalSec`) |

### SessionError

| Case                 | Raw                    | Description                         |
| -------------------- | ---------------------- | ----------------------------------- |
| `.permissionDenied`  | `"permission_denied"`  | Health data permissions not granted |
| `.sensorUnavailable` | `"sensor_unavailable"` | Sensor not available                |
| `.invalidState`      | `"invalid_state"`      | Invalid engine state                |
| `.lowBattery`        | `"low_battery"`        | Device battery too low              |
| `.osTerminated`      | `"os_terminated"`      | OS terminated the session           |

## Event Dictionary Format

The `SessionEngine` callback receives `[String: Any]` dictionaries with a `"type"` key:

### session\_started

```swift theme={null}
["type": "session_started", "session_id": "...", "started_at_ms": 1700000000000]
```

### session\_frame

Contains flat metrics computed over the sliding window:

```swift theme={null}
["type": "session_frame", "session_id": "...", "seq": 1,
 "emitted_at_ms": 1700000003000,
 "metrics": [
    "hr_mean_bpm": 72.3,
    "hr_sdnn_ms": 45.12,
    "rmssd_ms": 36.10,
    "sample_count": 60,
    "start_ms": 1700000000000,
    "end_ms": 1700000060000,
    "motion_rms_g": 1.02,        // optional, when accelerometer available
    "motion_sample_count": 125,   // optional
    "active_energy_kcal": 12.5    // optional, iOS only
 ]]
```

To extract metrics:

```swift theme={null}
if let metrics = event["metrics"] as? [String: Any] {
    let hr = metrics["hr_mean_bpm"] as? Double ?? 0
    let rmssd = metrics["rmssd_ms"] as? Double ?? 0
    let motionRms = metrics["motion_rms_g"] as? Double  // nil if no accelerometer
}
```

### biosignal\_frame

Emitted when `includeRawSamples` is `true`. Contains raw biosignal samples for live display:

```swift theme={null}
["type": "biosignal_frame", "session_id": "...", "seq": 1,
 "emitted_at_ms": 1700000003000,
 "samples": [
    ["timestamp_ms": 1700000002000, "bpm": 72.3,
     "rr_interval_ms": 829.9,
     "accelerometer": ["x": 0.01, "y": -0.02, "z": 1.0]],
    ...
 ]]
```

To extract the latest BPM:

```swift theme={null}
case "biosignal_frame":
    if let samples = event["samples"] as? [[String: Any]],
       let latest = samples.last {
        let bpm = latest["bpm"] as? Double ?? 0
        print("Live BPM: \(bpm)")
    }
```

### session\_summary

```swift theme={null}
["type": "session_summary", "session_id": "...",
 "duration_actual_sec": 30, "metrics": ["hr_mean_bpm": 72.3, ...]]
```

## WatchOS Integration

The Swift SDK is designed to run on Apple Watch. A typical setup uses `PhoneSessionRelay` on the watch to receive commands from the phone and run `SessionEngine` locally.

### Watch-Side Relay

```swift theme={null}
import WatchConnectivity
import SynheartSession

class PhoneSessionRelay: NSObject, WCSessionDelegate, ObservableObject {
    private let engine = SessionEngine()

    @Published var isRemoteRunning = false
    @Published var lastHr: Double = 0

    override init() {
        super.init()
        guard WCSession.isSupported() else { return }
        WCSession.default.delegate = self
        WCSession.default.activate()
    }

    func session(_ session: WCSession,
                 didReceiveMessage message: [String: Any]) {
        guard let command = message["command"] as? String else { return }

        switch command {
        case "start_session":
            do {
                let config = try SessionConfig(from: message)
                try engine.start(config: config) { [weak self] event in
                    self?.sendToPhone(event)
                    // Update @Published state for local UI
                }
            } catch {
                // Send error back to phone
            }

        case "stop_session":
            if let sessionId = message["session_id"] as? String {
                try? engine.stop(sessionId: sessionId)
            }

        default: break
        }
    }

    private func sendToPhone(_ event: [String: Any]) {
        guard WCSession.default.isReachable else { return }
        WCSession.default.sendMessage(event, replyHandler: nil)
    }

    // Required delegate methods
    func session(_ session: WCSession,
                 activationDidCompleteWith state: WCSessionActivationState,
                 error: Error?) {}
}
```

### Watch App Entry Point

```swift theme={null}
import SwiftUI
import WatchKit

class AppDelegate: NSObject, WKApplicationDelegate {
    let phoneRelay = PhoneSessionRelay()
    func applicationDidFinishLaunching() {}
}

@main
struct SynheartWatchApp: App {
    @WKApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            SessionView()
                .environmentObject(delegate.phoneRelay)
        }
    }
}
```

### Phone-Side Relay (iOS Plugin)

The phone-side `WatchSessionRelay` sends commands to the watch and receives events back. It is used inside the Flutter iOS plugin to bridge between Dart and watchOS:

```swift theme={null}
import WatchConnectivity

class WatchSessionRelay: NSObject, WCSessionDelegate {
    var isReachable: Bool {
        guard WCSession.isSupported() else { return false }
        return WCSession.default.isReachable
    }

    func startSession(config: SessionConfig,
                      callback: @escaping ([String: Any]) -> Void,
                      onSendFailed: @escaping () -> Void) {
        // Send command via WCSession.sendMessage
        // Store callback to forward events from watch
        // Call onSendFailed if sendMessage fails (triggers local fallback)
    }
}
```

### XcodeGen Configuration

If you use XcodeGen for your watchOS project, add the SDK as a local package and link WatchConnectivity:

```yaml theme={null}
name: SynheartWatchDemo
options:
  deploymentTarget:
    watchOS: "10.0"

packages:
  SynheartSession:
    path: path/to/synheart-session-swift

targets:
  SynheartWatchApp:
    type: application
    platform: watchOS
    dependencies:
      - package: SynheartSession
      - sdk: WatchConnectivity.framework
```

## Behavioral Signals

The `SessionEngine` accepts an optional `BehaviorProvider` that fuses behavioral data into session frames.

### Mock Behavior (Development)

```swift theme={null}
import SynheartSession

let engine = SessionEngine(behaviorProvider: MockBehaviorProvider())

try engine.start(config: config) { event in
    if let behavior = event["behavior"] as? [String: Any] {
        print("Stability: \(behavior["stability_index"] ?? "")")
    }
}
```

### Production (via synheart-behavior)

```swift theme={null}
import SynheartSession
import SynheartSessionBehavior
import SynheartBehavior

let behaviorSdk = SynheartBehavior()
try behaviorSdk.initialize()

let engine = SessionEngine(
    provider: myBiosignalProvider,
    behaviorProvider: BehaviorSdkProvider(sdk: behaviorSdk)
)
```

The `"behavior"` key is only present in frames when a `BehaviorProvider` is configured and returns data. See the [synheart-session-swift](https://github.com/synheart-ai/synheart-session-swift) repository README for the full list of behavioral fields.

## API Reference

### SessionEngine

| Constructor                                 | Description                                  |
| ------------------------------------------- | -------------------------------------------- |
| `SessionEngine()`                           | Default mock biosignal provider, no behavior |
| `SessionEngine(provider:)`                  | Custom biosignal provider                    |
| `SessionEngine(provider:behaviorProvider:)` | Custom biosignal + behavioral providers      |
| `SessionEngine(behaviorProvider:)`          | Default mock biosignal + behavioral provider |

| Method                    | Description                                                                                                                    |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `start(config:callback:)` | Start a session. Throws if already running.                                                                                    |
| `stop(sessionId:)`        | Stop a running session. Throws if no session or ID mismatch.                                                                   |
| `ingestHsiMetrics(_:)`    | Feed runtime-computed HRV/HSI metrics into the active session. The session merges these into the next emitted `session_frame`. |
| `getStatus()`             | Returns status dict or `nil` if no active session.                                                                             |

## Resources

* **Repository**: [synheart-session-swift](https://github.com/synheart-ai/synheart-session-swift)
* **Issues**: [GitHub Issues](https://github.com/synheart-ai/synheart-session-swift/issues)
