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

> Cross-platform wearable SDK for Flutter apps (iOS + Android)

## Overview

The Synheart Wear Flutter SDK provides a unified API for streaming biometric data from Apple Watch, Fitbit, Garmin, Whoop, and Samsung devices in Flutter applications.

**Key Features:**

* Cross-platform support (iOS + Android)
* Real-time HR and HRV streaming
* Unified data schema across all devices
* Encrypted local storage
* Consent-based permissions

## Installation

Add to your `pubspec.yaml`:

```yaml theme={null}
dependencies:
  synheart_wear: ^0.3.1
```

Install dependencies:

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

## Platform Configuration

### iOS Configuration

Add to `ios/Runner/Info.plist`:

```xml theme={null}
<key>NSHealthShareUsageDescription</key>
<string>This app needs access to your health data to provide insights.</string>

<key>NSHealthUpdateUsageDescription</key>
<string>This app needs permission to update your health data.</string>
```

### Android Configuration

Add to `android/app/src/main/AndroidManifest.xml`:

```xml theme={null}
<!-- Health Connect Permissions -->
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
<uses-permission android:name="android.permission.health.READ_STEPS"/>
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
<uses-permission android:name="android.permission.health.READ_DISTANCE"/>

<!-- Health Connect Package Query -->
<queries>
    <package android:name="com.google.android.apps.healthdata" />
    <intent>
        <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
    </intent>
</queries>

<application>
    <!-- Required: Privacy Policy Activity Alias -->
    <activity-alias
        android:name="ViewPermissionUsageActivity"
        android:exported="true"
        android:targetActivity=".MainActivity"
        android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
        <intent-filter>
            <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
            <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
        </intent-filter>
    </activity-alias>
</application>
```

**Important:** Your `MainActivity` must extend `FlutterFragmentActivity` (not `FlutterActivity`) for Android 14+.

## Basic Usage

**Recommended Pattern (Explicit Permission Control):**

```dart theme={null}
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:synheart_wear/synheart_wear.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Step 1: Create SDK instance
  final adapters = <DeviceAdapter>{
    DeviceAdapter.platformHealth, // Apple HealthKit on iOS, Health Connect on Android
  };

  // Use withAdapters() to explicitly specify which adapters to enable
  // Note: Default constructor includes fitbit by default, so use withAdapters() for clarity
  final synheart = SynheartWear(
    config: SynheartWearConfig.withAdapters(adapters),
  );

  // Step 2: Request permissions (with reason for better UX)
  final result = await synheart.requestPermissions(
    permissions: {
      PermissionType.heartRate,
      PermissionType.steps,
      PermissionType.calories,
    },
    reason: 'This app needs access to your health data.',
  );

  // Step 3: Initialize SDK (validates permissions and data availability)
  if (result.values.any((s) => s == ConsentStatus.granted)) {
    try {
      await synheart.initialize();
      
      // Step 4: Read metrics
      final metrics = await synheart.readMetrics();
      print('HR: ${metrics.getMetric(MetricType.hr)} bpm');
      print('Steps: ${metrics.getMetric(MetricType.steps)}');
    } on SynheartWearError catch (e) {
      print('Initialization failed: $e');
      // Handle errors: NO_WEARABLE_DATA, STALE_DATA, etc.
    }
  }
}
```

**Alternative Pattern (Simplified):**

If you don't need to provide a custom reason, you can let `initialize()` handle permissions automatically:

```dart theme={null}
final synheart = SynheartWear(
  config: SynheartWearConfig.withAdapters({DeviceAdapter.platformHealth}),
);

// Initialize will request permissions internally if needed
await synheart.initialize();
final metrics = await synheart.readMetrics();
```

**Note:** `initialize()` validates that wearable data is available and not stale (>24 hours old). If no data is available or data is too old, it will throw a `SynheartWearError` with codes `NO_WEARABLE_DATA` or `STALE_DATA`.

## Real-Time Streaming

```dart theme={null}
// Stream heart rate every 5 seconds
// Note: Streams are created lazily when first listener subscribes
// Multiple calls to streamHR() return the same stream controller
final hrSubscription = synheart.streamHR(interval: Duration(seconds: 5))
  .listen((metrics) {
    final hr = metrics.getMetric(MetricType.hr);
    if (hr != null) print('Current HR: $hr bpm');
  }, onError: (error) {
    print('Stream error: $error');
  });

// Stream HRV in 5-second windows
final hrvSubscription = synheart.streamHRV(windowSize: Duration(seconds: 5))
  .listen((metrics) {
    final hrv = metrics.getMetric(MetricType.hrvRmssd);
    if (hrv != null) print('HRV RMSSD: $hrv ms');
  }, onError: (error) {
    print('HRV stream error: $error');
  });

// Don't forget to cancel subscriptions when done
// hrSubscription.cancel();
// hrvSubscription.cancel();
```

## Complete Example

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

class HealthMonitor extends StatefulWidget {
  @override
  _HealthMonitorState createState() => _HealthMonitorState();
}

class _HealthMonitorState extends State<HealthMonitor> {
  late SynheartWear _sdk;
  StreamSubscription<WearMetrics>? _hrSubscription;
  WearMetrics? _latestMetrics;
  bool _isConnected = false;

  @override
  void initState() {
    super.initState();
    _sdk = SynheartWear(
      config: SynheartWearConfig.withAdapters({
        DeviceAdapter.platformHealth,
      }),
    );
  }

  Future<void> _connect() async {
    try {
      // Step 1: Request permissions
      final result = await _sdk.requestPermissions(
        permissions: {
          PermissionType.heartRate,
          PermissionType.steps,
          PermissionType.calories,
        },
        reason: 'This app needs access to your health data.',
      );

      // Step 2: Initialize if permissions granted
      if (result.values.any((s) => s == ConsentStatus.granted)) {
        await _sdk.initialize();
        
        // Step 3: Read initial metrics
        final metrics = await _sdk.readMetrics();
        setState(() {
          _isConnected = true;
          _latestMetrics = metrics;
        });
      } else {
        setState(() {
          _isConnected = false;
          // Show error: permissions denied
        });
      }
    } on SynheartWearError catch (e) {
      // Handle SDK-specific errors (NO_WEARABLE_DATA, STALE_DATA, etc.)
      print('SDK Error: $e');
      setState(() {
        _isConnected = false;
        // Show error message
      });
    } catch (e) {
      // Handle other errors
      print('Error: $e');
      setState(() {
        _isConnected = false;
      });
    }
  }

  void _startStreaming() {
    _hrSubscription = _sdk
        .streamHR(interval: Duration(seconds: 3))
        .listen(
      (metrics) {
        setState(() => _latestMetrics = metrics);
      },
      onError: (error) {
        print('Stream error: $error');
        // Handle stream errors
      },
    );
  }

  void _stopStreaming() {
    _hrSubscription?.cancel();
    _hrSubscription = null;
  }

  @override
  void dispose() {
    _hrSubscription?.cancel();
    _sdk.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Health Monitor')),
      body: _isConnected
          ? Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                if (_latestMetrics != null) ...[
                  Text(
                    'HR: ${_latestMetrics!.getMetric(MetricType.hr) ?? "--"} bpm',
                    style: TextStyle(fontSize: 32),
                  ),
                  SizedBox(height: 16),
                  Text('Steps: ${_latestMetrics!.getMetric(MetricType.steps) ?? "--"}'),
                  Text('Calories: ${_latestMetrics!.getMetric(MetricType.calories) ?? "--"} kcal'),
                  SizedBox(height: 24),
                ],
                ElevatedButton(
                  onPressed: _hrSubscription == null ? _startStreaming : _stopStreaming,
                  child: Text(_hrSubscription == null ? 'Start Streaming' : 'Stop Streaming'),
                ),
              ],
            )
          : Center(
              child: ElevatedButton(
                onPressed: _connect,
                child: Text('Connect to Health'),
              ),
            ),
    );
  }
}
```

## Data Schema

Access metrics using the `WearMetrics` object:

```dart theme={null}
final metrics = await synheart.readMetrics();

// Basic metrics
final hr = metrics.getMetric(MetricType.hr);           // Heart rate (bpm)
final steps = metrics.getMetric(MetricType.steps);     // Steps
final calories = metrics.getMetric(MetricType.calories); // Calories (kcal)
final distance = metrics.getMetric(MetricType.distance); // Distance (km)

// HRV metrics
final hrvRmssd = metrics.getMetric(MetricType.hrvRmssd); // HRV RMSSD (ms)
final hrvSdnn = metrics.getMetric(MetricType.hrvSdnn);   // HRV SDNN (ms)

// RR intervals (nullable; non-null only when the adapter exposes RR samples)
final rrIntervals = metrics.rrIntervalsMs; // List<double>?

// Metadata
final batteryLevel = metrics.batteryLevel;     // 0.0-1.0
final deviceId = metrics.deviceId;             // String
final source = metrics.source;                 // String
final timestamp = metrics.timestamp;           // DateTime
```

## Error Handling

```dart theme={null}
try {
  // Request permissions
  final result = await synheart.requestPermissions(
    permissions: {PermissionType.heartRate, PermissionType.steps},
    reason: 'This app needs access to your health data.',
  );

  if (result.values.any((s) => s == ConsentStatus.granted)) {
    // Initialize (may throw if no data or stale data)
    await synheart.initialize();
    
    // Read metrics
    final metrics = await synheart.readMetrics();
    if (metrics.hasValidData) {
      print('Data available');
    }
  }
} on PermissionDeniedError catch (e) {
  print('Permission denied: ${e.message}');
  // User denied permissions - show message or retry
} on DeviceUnavailableError catch (e) {
  print('Device unavailable: ${e.message}');
  // Health data source not available - check device connection
} on SynheartWearError catch (e) {
  // Handle SDK-specific errors
  if (e.code == 'NO_WEARABLE_DATA') {
    print('No wearable data available. Please check device connection.');
  } else if (e.code == 'STALE_DATA') {
    print('Data is stale. Please sync your wearable device.');
  } else {
    print('SDK error: ${e.message}');
  }
} catch (e) {
  print('Unexpected error: $e');
}
```

**Common Error Codes:**

* `NO_WEARABLE_DATA`: No health data available from connected devices
* `STALE_DATA`: Latest data is older than 24 hours
* `PERMISSION_DENIED`: User denied required permissions
* `DEVICE_UNAVAILABLE`: Health data source is not available

## BLE Heart Rate Monitor

Connect directly to any standard Bluetooth LE heart rate monitor (WHOOP Broadcast, Polar, Wahoo, Garmin HRM straps) for real-time HR streaming. No cloud backend needed.

### Platform Setup

**Android** — Add to `AndroidManifest.xml`:

```xml theme={null}
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

**iOS** — Add to `Info.plist`:

```xml theme={null}
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to heart rate monitors.</string>
```

### Usage

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

// Create the BLE HRM bridge
final bleHrm = BleHrmProvider();

// Scan for nearby HR monitors (optionally filter by name prefix)
final devices = await bleHrm.scan(timeoutMs: 10000, namePrefix: 'WHOOP');

// Connect to a device
await bleHrm.connect(
  deviceId: devices.first.deviceId,
  sessionId: 'my-session',
);

// Listen to heart rate samples
bleHrm.onHeartRate.listen((sample) {
  print('BPM: ${sample.bpm}');
  if (sample.rrIntervalsMs != null) {
    print('RR intervals: ${sample.rrIntervalsMs}');
  }
});

// Check connection status
final connected = await bleHrm.isConnected();

// Disconnect when done
await bleHrm.disconnect();
```

### HeartRateSample

Each sample from the stream contains:

| Field           | Type           | Description                                                        |
| --------------- | -------------- | ------------------------------------------------------------------ |
| `tsMs`          | `int`          | Phone receipt timestamp (ms since epoch)                           |
| `bpm`           | `double`       | Heart rate in beats per minute                                     |
| `source`        | `String`       | Always `"ble_hrm"`                                                 |
| `deviceId`      | `String`       | BLE device UUID                                                    |
| `deviceName`    | `String?`      | Device advertised name                                             |
| `sessionId`     | `String?`      | Optional session tag                                               |
| `rrIntervalsMs` | `List<double>` | RR intervals in milliseconds (empty when device doesn't expose RR) |

### Error Handling

BLE HRM uses structured error codes returned as `PlatformException`:

| Code                | Meaning                                       |
| ------------------- | --------------------------------------------- |
| `PERMISSION_DENIED` | Bluetooth permission not granted              |
| `BLUETOOTH_OFF`     | Bluetooth adapter is disabled                 |
| `DEVICE_NOT_FOUND`  | Device not found or connection timed out      |
| `SUBSCRIBE_FAILED`  | Failed to subscribe to HR notifications       |
| `DISCONNECTED`      | Device disconnected (emitted on event stream) |

### Notes

* WHOOP devices require "Broadcast Heart Rate" enabled in the WHOOP app settings
* RR intervals are available on some devices (e.g. Polar chest straps) but not all
* Reconnection is automatic: 3 retries with exponential backoff (1s, 2s, 4s)
* Only one BLE HRM device can be connected at a time

## Cloud Providers

### WhoopProvider

```dart theme={null}
final whoopProvider = WhoopProvider(
  appId: 'your-app-id',
  userId: 'user-123',
);

// Historical data methods
final recovery = await whoopProvider.fetchRecovery(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final sleep = await whoopProvider.fetchSleep(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final workouts = await whoopProvider.fetchWorkouts(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final cycles = await whoopProvider.fetchCycles(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
```

### GarminProvider (Cloud)

Full Garmin Connect integration via OAuth PKCE. Supports 12 summary types through the Garmin Health API backfill endpoint: **dailies**, **epochs**, **sleeps**, **stressDetails**, **hrv**, **userMetrics**, **bodyComps**, **pulseox**, **respiration**, **healthSnapshot**, **bloodPressures**, and **skinTemp**.

```dart theme={null}
final garminProvider = GarminProvider(
  appId: 'your-app-id',
  userId: 'user-123',
);

// Backfill API — fetch any of the 12 summary types
final dailies = await garminProvider.fetchDailies(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final sleeps = await garminProvider.fetchSleeps(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final hrv = await garminProvider.fetchHRV(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);
final stress = await garminProvider.fetchStressDetails(
  start: DateTime.now().subtract(Duration(days: 7)),
  end: DateTime.now(),
);

// Additional methods: fetchEpochs(), fetchUserMetrics(), fetchBodyComps(),
// fetchPulseOx(), fetchRespiration(), fetchHealthSnapshot(),
// fetchBloodPressures(), fetchSkinTemp()
```

### GarminHealth (Native RTS)

The `GarminHealth` facade provides native Garmin device integration for scanning, pairing, and real-time streaming. All method signatures use generic SDK-owned types (`ScannedDevice`, `PairedDevice`, `WearMetrics`), never Garmin-specific types.

> **Important:** The Garmin Health SDK Real-Time Streaming (RTS) capability requires a separate license from Garmin. This facade is available on demand for licensed integrations. The underlying Garmin Health SDK code is proprietary to Garmin and is not distributed as open source.

```dart theme={null}
final garmin = GarminHealth(licenseKey: 'your-garmin-sdk-key');
await garmin.initialize();

// Scan for devices
garmin.scannedDevicesStream.listen((devices) {
  print('Found ${devices.length} devices');
});
await garmin.startScanning();

// Pair with a device
final paired = await garmin.pairDevice(scannedDevice);

// Stream real-time metrics
garmin.realTimeStream.listen((metrics) {
  final hr = metrics.getMetric(MetricType.hr);
  print('HR: $hr bpm');
});
await garmin.startStreaming(device: paired);

// Read unified metrics
final metrics = await garmin.readMetrics();
```

## Platform-Specific Notes

### Android

* HRV: Only `HRV_RMSSD` is supported by Health Connect
* Distance: Uses `DISTANCE_DELTA` type
* Requires Health Connect app installed
* MainActivity must extend `FlutterFragmentActivity`

### iOS

* Full support for all metrics via HealthKit
* Requires HealthKit capability enabled in Xcode
* Background delivery available for real-time updates

## API Reference

### `SynheartWear`

Main SDK class for interacting with wearable devices.

**Constructor:**

```dart theme={null}
SynheartWear({required SynheartWearConfig config})
```

**Methods:**

| Method                 | Description                     | Returns                                      |
| ---------------------- | ------------------------------- | -------------------------------------------- |
| `requestPermissions()` | Request health data permissions | `Future<Map<PermissionType, ConsentStatus>>` |
| `initialize()`         | Initialize the SDK              | `Future<void>`                               |
| `readMetrics()`        | Read latest metrics             | `Future<WearMetrics>`                        |
| `streamHR()`           | Stream heart rate               | `Stream<WearMetrics>`                        |
| `streamHRV()`          | Stream HRV                      | `Stream<WearMetrics>`                        |
| `dispose()`            | Cleanup resources               | `void`                                       |

### `SynheartWearConfig`

Configuration for the SDK.

**Constructor:**

```dart theme={null}
SynheartWearConfig.withAdapters(
  Set<DeviceAdapter> adapters, {
  bool enableLocalCaching = true,
  bool enableEncryption = true,
})
```

### `WearMetrics`

Biometric data container.

**Methods:**

| Method                       | Description            | Returns   |
| ---------------------------- | ---------------------- | --------- |
| `getMetric(MetricType type)` | Get specific metric    | `double?` |
| `hasValidData`               | Check if data is valid | `bool`    |

**Properties:**

| Property        | Type            | Description                                                    |
| --------------- | --------------- | -------------------------------------------------------------- |
| `timestamp`     | `DateTime`      | When data was recorded                                         |
| `deviceId`      | `String`        | Device identifier                                              |
| `source`        | `String`        | Data source                                                    |
| `rrIntervalsMs` | `List<double>?` | RR intervals (ms); non-null only when the adapter exposes them |
| `batteryLevel`  | `double?`       | Battery level (0.0-1.0); pulled from `meta.battery`            |

## Resources

* **Repository**: [synheart-wear-flutter](https://github.com/synheart-ai/synheart-wear-flutter)
* **pub.dev**: [synheart\_wear](https://pub.dev/packages/synheart_wear)
* **API Docs**: [API Reference](https://pub.dev/documentation/synheart_wear/latest/)
* **Issues**: [GitHub Issues](https://github.com/synheart-ai/synheart-wear-flutter/issues)
