> ## 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 - Flutter SDK

> Flutter SDK with stream-based session API for HR metrics and behavioral signals

## Overview

The Synheart Session Dart SDK provides a stream-based API for running timed biometric sessions. Sessions emit session frames with HR metrics at configurable intervals and deliver a final summary when complete. Frames can optionally include behavioral signal data (typing, scrolling, taps, app switches, idle gaps, stability/fragmentation indices) when a `BehaviorProvider` is configured. A built-in mock engine lets you develop and test without hardware.

**Key Features:**

* Stream-based session lifecycle (`SessionStarted` -> `SessionFrame*` / `BiosignalFrame*` -> `SessionSummary`)
* Optional behavioral signal fusion via `BehaviorProvider` (mock or native)
* Built-in mock engine with deterministic output
* Apple Watch connectivity with transparent local fallback
* Configurable session modes, duration, and compute profile

## Installation

Add to your `pubspec.yaml`:

```yaml theme={null}
dependencies:
  synheart_session: ^0.2.0
```

Install dependencies:

```bash theme={null}
flutter pub get
```

### Requirements

* Dart SDK `>=3.8.0 <4.0.0`
* Flutter `>=3.22.0`

## Quick Start

### Mock Mode (Development)

```dart theme={null}
import 'package:synheart_session/synheart_session.dart';

final session = SynheartSession.mock(seed: 42);

final config = SessionConfig(
  mode: SessionMode.focus,
  durationSec: 30,
  profile: const ComputeProfile(windowSec: 10, emitIntervalSec: 3),
);

session.startSession(config).listen((event) {
  switch (event) {
    case SessionStarted():
      print('Session started');
    case SessionFrame():
      print('HR: ${event.metrics['hr_mean_bpm']} bpm');
      print('RMSSD: ${event.metrics['rmssd_ms']} ms');
    case SessionSummary():
      print('Done: ${event.durationActualSec}s');
    case SessionError():
      print('Error [${event.code.value}]: ${event.message}');
  }
});
```

### Native Mode (Production)

```dart theme={null}
final session = SynheartSession();

final config = SessionConfig(
  mode: SessionMode.breathing,
  durationSec: 60,
);

final subscription = session.startSession(config).listen(
  (event) {
    // Handle events
  },
  onDone: () {
    print('Stream closed');
  },
);

// Stop early
await session.stopSession(config.sessionId);

// Clean up
session.dispose();
```

In native mode, the SDK automatically routes sessions to Apple Watch when connected. If the watch is not reachable, it falls back to the local engine transparently.

## Apple Watch Connectivity

Query watch connectivity status from your Flutter app:

```dart theme={null}
final session = SynheartSession();
final status = await session.getWatchStatus();

if (status != null) {
  print('Supported: ${status.supported}');
  print('Paired: ${status.paired}');
  print('Installed: ${status.installed}');
  print('Reachable: ${status.reachable}');
}
```

`WatchStatus` fields:

| Field       | Type   | Description                                   |
| ----------- | ------ | --------------------------------------------- |
| `supported` | `bool` | Whether the device supports WatchConnectivity |
| `paired`    | `bool` | Whether an Apple Watch is paired              |
| `installed` | `bool` | Whether the companion watch app is installed  |
| `reachable` | `bool` | Whether the watch app is currently reachable  |

<Note>
  `getWatchStatus()` returns `null` in mock mode and on Android. The watch must be paired, the companion app installed, and the watch app in the foreground for `reachable` to be `true`.
</Note>

## Session Configuration

```dart theme={null}
final config = SessionConfig(
  mode: SessionMode.focus,          // or SessionMode.breathing
  durationSec: 60,                  // session duration in seconds
  profile: const ComputeProfile(
    windowSec: 10,                  // sliding window size
    emitIntervalSec: 3,             // session frame interval
  ),
  windowLabel: 'morning-check',     // optional label
);

// Auto-generated UUID accessible via config.sessionId
```

### SessionConfig

| Parameter           | Type             | Required | Default                 | Description                           |
| ------------------- | ---------------- | -------- | ----------------------- | ------------------------------------- |
| `mode`              | `SessionMode`    | Yes      | -                       | Session mode (`focus` or `breathing`) |
| `durationSec`       | `int`            | Yes      | -                       | Session duration in seconds           |
| `profile`           | `ComputeProfile` | No       | 60s window, 5s interval | Session frame computation timing      |
| `windowLabel`       | `String?`        | No       | `null`                  | Optional label for the session window |
| `includeRawSamples` | `bool`           | No       | `false`                 | Enable raw biosignal frame streaming  |
| `sessionId`         | `String?`        | No       | Auto-generated UUID     | Custom session ID                     |

### ComputeProfile

| Parameter            | Type   | Default | Description                                                            |
| -------------------- | ------ | ------- | ---------------------------------------------------------------------- |
| `windowSec`          | `int`  | 60      | Sliding window size in seconds                                         |
| `emitIntervalSec`    | `int`  | 5       | How often to emit session frames in seconds                            |
| `rawEmitIntervalSec` | `int?` | `null`  | How often to emit raw biosignal frames (defaults to `emitIntervalSec`) |

## Session Events

All events extend the sealed `SessionEvent` class and include a `sessionId`.

### SessionStarted

Emitted when the engine begins capturing data.

```dart theme={null}
case SessionStarted():
  print('Started at ${event.startedAtMs}');
```

| Field         | Type     | Description                    |
| ------------- | -------- | ------------------------------ |
| `sessionId`   | `String` | Session identifier             |
| `startedAtMs` | `int`    | Unix timestamp in milliseconds |

### SessionFrame

Emitted at each `emitIntervalSec` with computed session metrics.

```dart theme={null}
case SessionFrame():
  final hr = event.metrics['hr_mean_bpm'] as double?;
  final rmssd = event.metrics['rmssd_ms'] as double?;
  print('HR: $hr bpm, RMSSD: $rmssd ms');

  // Optional motion metrics (present when watch has accelerometer)
  final motionRms = event.metrics['motion_rms_g'] as double?;
  if (motionRms != null) {
    print('Motion RMS: $motionRms g');
  }

  // Optional behavioral signals
  if (event.behavior != null) {
    print('Stability: ${event.behavior!['stability_index']}');
  }
```

| Field         | Type                    | Description                                                            |
| ------------- | ----------------------- | ---------------------------------------------------------------------- |
| `sessionId`   | `String`                | Session identifier                                                     |
| `seq`         | `int`                   | Frame sequence number (1-based)                                        |
| `emittedAtMs` | `int`                   | Unix timestamp in milliseconds                                         |
| `metrics`     | `Map<String, dynamic>`  | Flat metrics dictionary (hr\_mean\_bpm, hr\_sdnn\_ms, rmssd\_ms, etc.) |
| `behavior`    | `Map<String, dynamic>?` | Optional behavioral signals (when BehaviorProvider configured)         |

### BiosignalFrame

Emitted at each `rawEmitIntervalSec` when `includeRawSamples` is `true`. Contains raw biosignal samples for live display.

```dart theme={null}
case BiosignalFrame():
  for (final sample in event.samples) {
    print('BPM: ${sample.bpm}, RR: ${sample.rrIntervalMs} ms');
    if (sample.accelerometer case final accel?) {
      print('Accel: (${accel.x}, ${accel.y}, ${accel.z})');
    }
  }
```

| Field         | Type                    | Description                     |
| ------------- | ----------------------- | ------------------------------- |
| `sessionId`   | `String`                | Session identifier              |
| `seq`         | `int`                   | Frame sequence number (1-based) |
| `emittedAtMs` | `int`                   | Unix timestamp in milliseconds  |
| `samples`     | `List<BiosignalSample>` | Raw biosignal samples           |

**BiosignalSample fields:**

| Field           | Type                                | Description                         |
| --------------- | ----------------------------------- | ----------------------------------- |
| `timestampMs`   | `int`                               | Unix timestamp in milliseconds      |
| `bpm`           | `double`                            | Instantaneous heart rate            |
| `rrIntervalMs`  | `double?`                           | Inter-beat interval in milliseconds |
| `accelerometer` | `({double x, double y, double z})?` | 3-axis accelerometer                |
| `spo2`          | `double?`                           | SpO2 percentage (0-100)             |

### SessionSummary

Emitted when the session completes (duration elapsed or stopped manually). Contains aggregate metrics for the full session.

```dart theme={null}
case SessionSummary():
  print('Duration: ${event.durationActualSec}s');
  print('Avg HR: ${event.metrics['hr_mean_bpm']} bpm');
```

| Field               | Type                    | Description                            |
| ------------------- | ----------------------- | -------------------------------------- |
| `sessionId`         | `String`                | Session identifier                     |
| `durationActualSec` | `int`                   | Actual session duration in seconds     |
| `metrics`           | `Map<String, dynamic>`  | Aggregate metrics for the full session |
| `behavior`          | `Map<String, dynamic>?` | Optional aggregate behavioral signals  |

### SessionError

Emitted when the session encounters an error. The stream closes after this event.

```dart theme={null}
case SessionError():
  print('[${event.code.value}]: ${event.message}');
```

| Field       | Type               | Description                  |
| ----------- | ------------------ | ---------------------------- |
| `sessionId` | `String`           | Session identifier           |
| `code`      | `SessionErrorCode` | Error code                   |
| `message`   | `String`           | Human-readable error message |

**Error codes:**

| Code                 | Description                                   |
| -------------------- | --------------------------------------------- |
| `permission_denied`  | Health data permissions not granted           |
| `sensor_unavailable` | Heart rate sensor not available               |
| `low_battery`        | Device battery too low                        |
| `os_terminated`      | Operating system terminated the session       |
| `invalid_state`      | Invalid session state (e.g., duplicate start) |

## Behavioral Signals

The SDK supports optional behavioral signal fusion. In mock mode, pass a `BehaviorProvider` to `SynheartSession.mock()`. In production mode, behavioral data flows automatically from the native `SessionEngine` when a `BehaviorProvider` is configured on the native side.

### Mock Mode

```dart theme={null}
import 'package:synheart_session/synheart_session.dart';

final session = SynheartSession.mock(
  seed: 42,
  behaviorProvider: MockBehaviorProvider(),
);

final config = SessionConfig(
  mode: SessionMode.focus,
  durationSec: 30,
  profile: const ComputeProfile(windowSec: 10, emitIntervalSec: 3),
);

session.startSession(config).listen((event) {
  switch (event) {
    case SessionFrame():
      print('HR: ${event.metrics['hr_mean_bpm']}');
      if (event.behavior != null) {
        print('Stability: ${event.behavior!['stability_index']}');
        print('Fragmentation: ${event.behavior!['fragmentation_index']}');
      }
    case SessionSummary():
      print('Done: ${event.durationActualSec}s');
      if (event.behavior != null) {
        print('Final stability: ${event.behavior!['stability_index']}');
      }
    default:
      break;
  }
});
```

### Behavioral Fields

When present, the `behavior` map on `SessionFrame` and `SessionSummary` includes:

| Field                     | Type      | Description                        |
| ------------------------- | --------- | ---------------------------------- |
| `typing_cadence`          | `double?` | Keys per second                    |
| `inter_key_latency`       | `double?` | Milliseconds between keystrokes    |
| `burst_length`            | `int?`    | Keys in current burst              |
| `scroll_velocity`         | `double?` | Pixels per second                  |
| `scroll_acceleration`     | `double?` | Pixels per second squared          |
| `scroll_jitter`           | `double?` | Variance in scroll speed           |
| `tap_rate`                | `double?` | Taps per second                    |
| `app_switches_per_minute` | `int`     | App switches per minute            |
| `foreground_duration`     | `double?` | Seconds in foreground              |
| `idle_gap_seconds`        | `double?` | Seconds since last interaction     |
| `stability_index`         | `double?` | 0.0 to 1.0                         |
| `fragmentation_index`     | `double?` | 0.0 to 1.0                         |
| `timestamp`               | `int`     | Capture timestamp (ms since epoch) |

<Note>
  The `behavior` key is only present when a `BehaviorProvider` is configured and returns data. Null optional fields are omitted from the map.
</Note>

## Complete Example

```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:synheart_session/synheart_session.dart';

class SessionDemo extends StatefulWidget {
  const SessionDemo({super.key});

  @override
  State<SessionDemo> createState() => _SessionDemoState();
}

class _SessionDemoState extends State<SessionDemo> {
  final _session = SynheartSession.mock(seed: 42);
  StreamSubscription<SessionEvent>? _subscription;
  SessionConfig? _activeConfig;
  double _hr = 0;
  bool _running = false;

  void _start() {
    final config = SessionConfig(
      mode: SessionMode.focus,
      durationSec: 30,
      profile: const ComputeProfile(windowSec: 10, emitIntervalSec: 3),
    );

    setState(() {
      _activeConfig = config;
      _running = true;
    });

    _subscription = _session.startSession(config).listen(
      (event) {
        switch (event) {
          case SessionFrame():
            setState(() {
              _hr = (event.metrics['hr_mean_bpm'] as num?)?.toDouble() ?? 0;
            });
          case SessionSummary():
            setState(() => _running = false);
          default:
            break;
        }
      },
      onDone: () => setState(() => _running = false),
    );
  }

  Future<void> _stop() async {
    if (_activeConfig != null) {
      await _session.stopSession(_activeConfig!.sessionId);
    }
  }

  @override
  void dispose() {
    _subscription?.cancel();
    _session.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('HR: ${_hr.toStringAsFixed(1)} bpm',
                style: const TextStyle(fontSize: 32)),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _running ? _stop : _start,
              child: Text(_running ? 'Stop' : 'Start'),
            ),
          ],
        ),
      ),
    );
  }
}
```

## API Reference

### SynheartSession

| Constructor                                                             | Description                                                         |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `SynheartSession()`                                                     | Production mode using platform channels                             |
| `SynheartSession.mock({int? seed, BehaviorProvider? behaviorProvider})` | Mock mode with deterministic output and optional behavioral signals |

| Method                          | Returns                  | Description                         |
| ------------------------------- | ------------------------ | ----------------------------------- |
| `startSession(SessionConfig)`   | `Stream<SessionEvent>`   | Start a session and receive events  |
| `stopSession(String sessionId)` | `Future<void>`           | Stop a running session              |
| `getStatus()`                   | `Future<SessionStatus?>` | Get current session status          |
| `getWatchStatus()`              | `Future<WatchStatus?>`   | Get Apple Watch connectivity status |
| `dispose()`                     | `void`                   | Release all resources               |

## Resources

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