https://github.com/synheart-ai/synheart-wear-flutter
Flutter SDK for wearable signals — HealthKit, Health Connect, WHOOP, Garmin, Oura, Fitbit.
https://github.com/synheart-ai/synheart-wear-flutter
biosignals dart fitbit flutter garmin health-connect healthkit hrv oura sdk wearable whoop
Last synced: 1 day ago
JSON representation
Flutter SDK for wearable signals — HealthKit, Health Connect, WHOOP, Garmin, Oura, Fitbit.
- Host: GitHub
- URL: https://github.com/synheart-ai/synheart-wear-flutter
- Owner: synheart-ai
- License: other
- Created: 2025-11-10T19:11:42.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-06-03T23:10:36.000Z (12 days ago)
- Last Synced: 2026-06-04T02:07:16.947Z (12 days ago)
- Topics: biosignals, dart, fitbit, flutter, garmin, health-connect, healthkit, hrv, oura, sdk, wearable, whoop
- Language: Dart
- Homepage: https://pub.dev/packages/synheart_wear
- Size: 404 KB
- Stars: 1
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
README
🌐 [synheart.ai](https://synheart.ai) — Human State Interface (HSI) infrastructure for developers and AI systems.
# Synheart Wear
[](https://pub.dev/packages/synheart_wear)
[](https://flutter.dev)
[](LICENSE)
> **Source-available.** This repository is open for reading, auditing, and
> filing issues. We do **not** accept pull requests — see
> [CONTRIBUTING.md](CONTRIBUTING.md) for the rationale and how to contribute
> via issues. Security reports go through [SECURITY.md](SECURITY.md).
> **Unified wearable SDK** for Flutter — stream HR, HRV, steps, calories, and distance from Apple Health (HealthKit on iOS / Health Connect on Android) and Garmin (real-time, license-gated). Cloud-based vendors (Whoop, Fitbit, Oura) ship as separate `*Provider` classes — see the [Multi-device support](#multi-device-support) table for status.
## Logging
This plugin does **not** install its own host log callback. Logging is
owned exclusively by `synheart_core` via
`CoreRuntimeBridge.initRuntimeLogging` / `shutdownRuntimeLogging`; the
wear plugin reads through the same callback the host registered. To
filter for wear-specific log lines, scope your existing log filter to
the `synheart_wear_flutter` target.
## Features
| Feature | Description |
| ---------------------- | ------------------------------------------- |
| 📱 **Cross-Platform** | iOS & Android support |
| ⌚ **Multi-device** | Apple Health, Garmin (RTS, license required), plus cloud providers (Whoop, Fitbit, Oura) |
| 🔄 **Real-Time** | Live HR and HRV streaming |
| 📊 **Unified Schema** | Consistent data format across all devices |
| 🔒 **Privacy-First** | Consent-based access with encryption |
| 💾 **Offline Support** | Encrypted local data persistence |
## 🚀 Quick Start
### Installation
```yaml
dependencies:
synheart_wear: ^0.4.1
```
```bash
flutter pub get
```
### Basic Usage
**Recommended Pattern (Explicit Permission Control):**
```dart
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.platformHealth, // Uses Apple Health on iOS and 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
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
// 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();
```
## 📊 Data Schema
`WearMetrics.toJson()` produces this shape:
```json
{
"timestamp": "2025-10-20T18:30:00Z",
"device_id": "applewatch_1234",
"source": "apple_healthkit",
"metrics": {
"hr": 72,
"hrv_rmssd": 45,
"hrv_sdnn": 62,
"steps": 1045,
"calories": 120.4,
"distance": 2.5
},
"meta": {
"battery": 0.82
}
}
```
`timestamp`, `device_id`, `source`, `metrics`, and `meta` are required.
`metrics` keys are scoped by the `MetricType` enum (`hr`, `hrv_rmssd`,
`hrv_sdnn`, `steps`, `calories`, `distance`, `stress`); adapters set
the keys they actually have. `meta` is a free-form `Map`
— `meta.battery` (`double` in `[0, 1]`) is the only key the SDK reads
back via `WearMetrics.batteryLevel`; everything else there is
adapter-defined.
**Access in code:**
```dart
final metrics = await synheart.readMetrics();
print(metrics.getMetric(MetricType.hr)); // 72
print(metrics.getMetric(MetricType.steps)); // 1045
print(metrics.getMetric(MetricType.distance)); // 2.5
print(metrics.batteryLevel); // 0.82
```
📚 **[Full API Documentation](https://synheart-ai.github.io/synheart_wear/)** | **[Data Schema Details](#-data-schema)**
## ⌚ Supported Devices
| Device | Platform | Status |
| -------------- | ----------- | ----------------- |
| Apple Watch | iOS | ✅ Ready |
| Health Connect | Android | ✅ Ready |
| Whoop (cloud, OAuth) | iOS/Android | ✅ Available via `WhoopProvider` (separate from the unified adapter API) |
| Fitbit (cloud, OAuth) | iOS/Android | ✅ Available via `FitbitProvider` (separate from the unified adapter API) |
| Oura (cloud, OAuth) | iOS/Android | ✅ Available via `OuraProvider` (separate from the unified adapter API) |
| Garmin (RTS) | iOS/Android | ✅ Ready (license required — see [`doc/GARMIN_SETUP.md`](doc/GARMIN_SETUP.md)) |
| Samsung Health | Android | 📋 Planned (no native adapter yet; SDK only detects whether the Samsung Health app is installed) |
> **Garmin RTS** ships behind a license. The open-source build includes a stub `GarminHealth` facade; the real native implementation (Companion SDK 4.7.0 on iOS + Android) is overlaid from the private `synheart-wear-garmin-companion` repo via `make build-with-garmin`. See [`doc/GARMIN_SETUP.md`](doc/GARMIN_SETUP.md) for the full workflow.
## ⚙️ Platform Configuration
### Android
Add to `android/app/src/main/AndroidManifest.xml`:
```xml
```
**Note:** `MainActivity` must extend `FlutterFragmentActivity` (not `FlutterActivity`) for Android 14+.
### iOS
Add to `ios/Runner/Info.plist`:
```xml
NSHealthShareUsageDescription
This app needs access to your health data to provide insights.
NSHealthUpdateUsageDescription
This app needs permission to update your health data.
```
## ⚠️ Platform Limitations
| Platform | Limitation | SDK Behavior |
| ----------- | ------------------------------- | ------------------------------------ |
| **Android** | HRV: Only `HRV_RMSSD` supported | Automatically maps to supported type |
| **Android** | Distance: Uses `DISTANCE_DELTA` | Automatically uses correct type |
| **iOS** | Full support for all metrics | No limitations |
## 🔒 Privacy & Security
- ✅ Consent-first design
- ✅ AES-256-CBC encryption
- ✅ Automatic key management
- ✅ Anonymized UUIDs
- ✅ Right to forget (revoke & delete)
## 🧠 Wear in HSI compute
This package **does not generate HSI** and **does not bundle native compute binaries**.
HSI is generated by the Synheart runtime and is typically orchestrated/validated in **Synheart Core**.
Read normalized metrics from this SDK (`SynheartWear.readMetrics()` /
`streamHR()` / `streamHRV()`) and pass them to the runtime through whatever
ingestion path your Synheart Core deployment exposes.
## 📖 Additional Resources
- **[Full API Documentation](https://docs.synheart.ai/synheart-wear/flutter)** — Complete API reference
- **[GitHub Issues](https://github.com/synheart-ai/synheart-wear-flutter/issues)** — Report bugs or request features
- **[pub.dev Package](https://pub.dev/packages/synheart_wear)** — Package details
---
## 📋 Detailed Sections
Initialization Flow & Best Practices
### Recommended Initialization Pattern
The SDK supports two initialization patterns:
**1. Explicit Permission Control (Recommended):**
```dart
// Step 1: Create SDK instance
final synheart = SynheartWear(
config: SynheartWearConfig.withAdapters({DeviceAdapter.platformHealth}),
);
// Step 2: Request permissions with reason
final result = await synheart.requestPermissions(
permissions: {PermissionType.heartRate, PermissionType.steps},
reason: 'This app needs access to your health data.',
);
// Step 3: Initialize (validates permissions and data)
if (result.values.any((s) => s == ConsentStatus.granted)) {
await synheart.initialize();
}
```
**2. Automatic Permission Handling:**
```dart
// Let initialize() handle permissions automatically
final synheart = SynheartWear(
config: SynheartWearConfig.withAdapters({DeviceAdapter.platformHealth}),
);
await synheart.initialize(); // Requests permissions internally if needed
```
### What `initialize()` Does
The `initialize()` method:
1. Requests permissions (if not already granted)
2. Initializes all enabled adapters
3. Validates that wearable data is available
4. Checks that data is not stale (>24 hours old)
**Important:** `initialize()` will throw `SynheartWearError` if:
- No wearable data is available (`NO_WEARABLE_DATA`)
- Latest data is older than 24 hours (`STALE_DATA`)
- Permissions are denied (`PERMISSION_DENIED`)
### Permission Request Behavior
- Calling `requestPermissions()` before `initialize()` allows you to provide a custom reason
- `initialize()` will also request permissions internally if not already granted
- If permissions are already granted, `initialize()` will skip the permission request
Data Schema Details
### Field Descriptions
| Field | Type | Description | Example |
| ------------------- | ------------------- | ------------------------ | ------------------------------------------ |
| `timestamp` | `string` (ISO 8601) | When data was recorded | `"2025-10-20T18:30:00Z"` |
| `device_id` | `string` | Unique device identifier | `"applewatch_1234"` |
| `source` | `string` | Data source adapter | `"apple_healthkit"`, `"fitbit"`, `"whoop"` |
| `metrics.hr` | `number` | Heart rate (bpm) | `72` |
| `metrics.hrv_rmssd` | `number` | HRV RMSSD (ms) | `45` |
| `metrics.hrv_sdnn` | `number` | HRV SDNN (ms) | `62` |
| `metrics.steps` | `number` | Step count | `1045` |
| `metrics.calories` | `number` | Calories (kcal) | `120.4` |
| `metrics.distance` | `number` | Distance (km) | `2.5` |
| `meta.battery` | `number` | Battery level (0.0-1.0) | `0.82` (82%) |
| `meta.synced` | `boolean` | Sync status | `true` |
**Notes:**
- Optional fields may be `null` if unavailable
- Platform limitations may affect metric availability
- `meta` object may contain device-specific fields
Platform-Specific Permission Handling
```dart
// Determine platform-specific permissions
Set permissions;
if (Platform.isAndroid) {
// Android Health Connect limitations:
// - HRV: Only RMSSD supported (SDNN not available)
// - Distance: Not directly supported (would need DISTANCE_DELTA)
permissions = {
PermissionType.heartRate,
PermissionType.heartRateVariability, // Maps to RMSSD on Android
PermissionType.steps,
PermissionType.calories,
// Note: Distance is not included as Health Connect doesn't support it
};
} else {
// iOS HealthKit supports all metrics
permissions = {
PermissionType.heartRate,
PermissionType.heartRateVariability, // Supports both RMSSD and SDNN
PermissionType.steps,
PermissionType.calories,
PermissionType.distance,
};
}
final result = await synheart.requestPermissions(
permissions: permissions,
reason: 'This app needs access to your health data.',
);
// Check if permissions were granted before initializing
if (result.values.any((s) => s == ConsentStatus.granted)) {
await synheart.initialize();
} else {
// Handle permission denial
print('Permissions were not granted');
}
```
Usage Examples
### Complete Health Monitoring App
```dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:synheart_wear/synheart_wear.dart';
class HealthMonitor extends StatefulWidget {
@override
_HealthMonitorState createState() => _HealthMonitorState();
}
class _HealthMonitorState extends State {
late SynheartWear _sdk;
StreamSubscription? _hrSubscription;
WearMetrics? _latestMetrics;
bool _isConnected = false;
@override
void initState() {
super.initState();
_sdk = SynheartWear(
config: SynheartWearConfig.withAdapters({DeviceAdapter.platformHealth}),
);
}
Future _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);
});
}
@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(
children: [
if (_latestMetrics != null) ...[
Text('HR: ${_latestMetrics!.getMetric(MetricType.hr)} bpm'),
Text('Steps: ${_latestMetrics!.getMetric(MetricType.steps)}'),
],
ElevatedButton(
onPressed: _startStreaming,
child: Text('Start Streaming'),
),
],
)
: Center(
child: ElevatedButton(
onPressed: _connect,
child: Text('Connect to Health'),
),
),
);
}
}
```
### Error Handling
```dart
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');
// User denied permissions - show message or retry
} on DeviceUnavailableError catch (e) {
print('Device unavailable: $e');
// 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');
}
} 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
Architecture
```text
┌─────────────────────────┐
│ synheart_wear SDK │
├─────────────────────────┤
│ Device Adapters Layer │
│ (Apple, Fitbit, etc.) │
├─────────────────────────┤
│ Normalization Engine │
│ (standard output schema)│
├─────────────────────────┤
│ Local Cache & Storage │
│ (encrypted, offline) │
└─────────────────────────┘
```
Roadmap
| Version | Goal | Status |
| ------- | ----------------------- | -------------- |
| v0.1 | Core SDK | ✅ Complete |
| v0.2 | Real-time streaming | ✅ Complete |
| v0.3 | Extended device support | ✅ Complete |
| v0.4 | Additional vendor adapters | 📋 Planned |
---
## ⌚ Real-Time Watch Data
Due to HealthKit (iOS) and Health Connect (Android) API limitations, real-time biometric streaming (HR, HRV, accelerometer) requires an active workout/exercise session on the watch. For real-time session-based data, use the Synheart watch companion apps alongside the [Synheart Session SDK](https://github.com/synheart-ai/synheart-session):
- [synheart-edge-watch-ios](https://github.com/synheart-ai/synheart-edge-watch-ios) — watchOS companion (HKWorkoutSession)
- [synheart-edge-watch-android](https://github.com/synheart-ai/synheart-edge-watch-android) — Wear OS companion (Health Services)
This SDK handles non-realtime and historical data (daily HR, HRV, steps, sleep, etc.) which does not require a workout session.
## Contributing
This is a source-available repository. Issues and feature requests are
welcome; pull requests are not accepted at this time. See
[CONTRIBUTING.md](CONTRIBUTING.md) for the rationale and the supported
contribution path. Security reports go through [SECURITY.md](SECURITY.md).
## License
Apache 2.0 License
---
## Patent Pending Notice
This project is provided under an open-source license. Certain underlying systems, methods, and architectures described or implemented herein may be covered by one or more pending patent applications.
Nothing in this repository grants any license, express or implied, to any patents or patent applications, except as provided by the applicable open-source license.
## Not a Medical Device
This SDK is intended for wellness and research use only. It is not a medical device, is not intended to diagnose, treat, cure, or prevent any disease or condition, and has not been evaluated by the FDA or any other regulatory body.