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:
dependencies:
synheart_session: ^0.2.0
Install dependencies:
Requirements
- Dart SDK
>=3.8.0 <4.0.0
- Flutter
>=3.22.0
Quick Start
Mock Mode (Development)
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)
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:
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 |
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.
Session Configuration
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.
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.
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.
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.
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.
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
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) |
The behavior key is only present when a BehaviorProvider is configured and returns data. Null optional fields are omitted from the map.
Complete Example
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
Author: Israel Goytom