Skip to main content

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:
flutter pub get

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:
FieldTypeDescription
supportedboolWhether the device supports WatchConnectivity
pairedboolWhether an Apple Watch is paired
installedboolWhether the companion watch app is installed
reachableboolWhether 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

ParameterTypeRequiredDefaultDescription
modeSessionModeYes-Session mode (focus or breathing)
durationSecintYes-Session duration in seconds
profileComputeProfileNo60s window, 5s intervalSession frame computation timing
windowLabelString?NonullOptional label for the session window
includeRawSamplesboolNofalseEnable raw biosignal frame streaming
sessionIdString?NoAuto-generated UUIDCustom session ID

ComputeProfile

ParameterTypeDefaultDescription
windowSecint60Sliding window size in seconds
emitIntervalSecint5How often to emit session frames in seconds
rawEmitIntervalSecint?nullHow 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}');
FieldTypeDescription
sessionIdStringSession identifier
startedAtMsintUnix 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']}');
  }
FieldTypeDescription
sessionIdStringSession identifier
seqintFrame sequence number (1-based)
emittedAtMsintUnix timestamp in milliseconds
metricsMap<String, dynamic>Flat metrics dictionary (hr_mean_bpm, hr_sdnn_ms, rmssd_ms, etc.)
behaviorMap<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})');
    }
  }
FieldTypeDescription
sessionIdStringSession identifier
seqintFrame sequence number (1-based)
emittedAtMsintUnix timestamp in milliseconds
samplesList<BiosignalSample>Raw biosignal samples
BiosignalSample fields:
FieldTypeDescription
timestampMsintUnix timestamp in milliseconds
bpmdoubleInstantaneous heart rate
rrIntervalMsdouble?Inter-beat interval in milliseconds
accelerometer({double x, double y, double z})?3-axis accelerometer
spo2double?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');
FieldTypeDescription
sessionIdStringSession identifier
durationActualSecintActual session duration in seconds
metricsMap<String, dynamic>Aggregate metrics for the full session
behaviorMap<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}');
FieldTypeDescription
sessionIdStringSession identifier
codeSessionErrorCodeError code
messageStringHuman-readable error message
Error codes:
CodeDescription
permission_deniedHealth data permissions not granted
sensor_unavailableHeart rate sensor not available
low_batteryDevice battery too low
os_terminatedOperating system terminated the session
invalid_stateInvalid 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:
FieldTypeDescription
typing_cadencedouble?Keys per second
inter_key_latencydouble?Milliseconds between keystrokes
burst_lengthint?Keys in current burst
scroll_velocitydouble?Pixels per second
scroll_accelerationdouble?Pixels per second squared
scroll_jitterdouble?Variance in scroll speed
tap_ratedouble?Taps per second
app_switches_per_minuteintApp switches per minute
foreground_durationdouble?Seconds in foreground
idle_gap_secondsdouble?Seconds since last interaction
stability_indexdouble?0.0 to 1.0
fragmentation_indexdouble?0.0 to 1.0
timestampintCapture 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

ConstructorDescription
SynheartSession()Production mode using platform channels
SynheartSession.mock({int? seed, BehaviorProvider? behaviorProvider})Mock mode with deterministic output and optional behavioral signals
MethodReturnsDescription
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()voidRelease all resources

Resources


Author: Israel Goytom