aidlab_sdk 2.3.0
aidlab_sdk: ^2.3.0 copied to clipboard
Aidlab Flutter SDK. For more information please visit https://www.aidlab.com/developer
example/lib/main.dart
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:aidlab_sdk/aidlab_sdk.dart';
import 'package:aidlab_sdk_example/line_chart.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
AidlabManager? aidlabManager;
String? lastConnectedAddress;
bool disableAidlabExampleBleBootstrap = false;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aidlab Flutter SDK Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const DeviceListScreen(),
);
}
}
class DeviceListScreen extends StatefulWidget {
const DeviceListScreen({super.key});
@override
State<DeviceListScreen> createState() => _DeviceListScreenState();
}
class _DeviceListScreenState extends State<DeviceListScreen>
with WidgetsBindingObserver
implements AidlabManagerDelegate {
final List<Device> _devices = [];
final Map<String, int> _deviceRssi = {};
bool _isScanning = false;
bool _permissionsReady = false;
String? _lastConnectedAddress;
bool _deviceFlowActive = false;
static const Set<String> _allowedNames = <String>{
'Aidlab',
'Aidlab 2',
'Aidmed One',
};
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (disableAidlabExampleBleBootstrap) {
return;
}
aidlabManager = AidlabManager(this);
_startInitialLoad();
}
void _startInitialLoad() {
scheduleMicrotask(() async {
try {
await _ensurePermissions();
_loadLastConnectedAddress();
} catch (error) {
debugPrint('Initial device list load failed: $error');
}
});
}
void _loadLastConnectedAddress() {
setState(() {
_lastConnectedAddress = lastConnectedAddress;
});
}
Future<void> _ensurePermissions() async {
if (Platform.isIOS) {
final bluetoothStatus = await Permission.bluetooth.request();
setState(() {
_permissionsReady = bluetoothStatus.isGranted;
});
return;
}
final scanStatus = await Permission.bluetoothScan.request();
final connectStatus = await Permission.bluetoothConnect.request();
final locationStatus = await Permission.locationWhenInUse.request();
final serviceEnabled = await Permission.location.serviceStatus.isEnabled;
final hasBle12Plus = scanStatus.isGranted && connectStatus.isGranted;
final hasBlePre12 = locationStatus.isGranted && serviceEnabled;
if (!serviceEnabled && !hasBle12Plus) {
await openAppSettings();
}
setState(() {
_permissionsReady = hasBle12Plus || hasBlePre12;
});
}
@override
void dispose() {
final AidlabManager? manager = aidlabManager;
if (manager != null) {
scheduleMicrotask(() async {
try {
await manager.dispose();
} catch (error) {
debugPrint('AidlabManager.dispose failed: $error');
}
});
}
aidlabManager = null;
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (_lastConnectedAddress == null || _deviceFlowActive || !_isScanning) {
return;
}
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive) {
scheduleMicrotask(() async {
try {
await _startReconnectScan(ScanMode.lowPower);
} catch (error) {
debugPrint('Background reconnect scan failed: $error');
}
});
}
}
Future<void> _startReconnectScan(ScanMode mode) async {
final AidlabManager? manager = aidlabManager;
if (manager == null) {
return;
}
await manager.scan(mode);
if (!mounted) {
return;
}
setState(() {
_isScanning = true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Devices'),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _devices.length,
itemBuilder: (context, index) {
final device = _devices[index];
final deviceName = device.name ?? device.address;
final isLastConnected = _lastConnectedAddress != null &&
device.address == _lastConnectedAddress;
final rssi = _deviceRssi[device.address];
return Card(
margin:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: isLastConnected ? Colors.green.shade50 : null,
child: ListTile(
title: Text(deviceName),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(device.address),
if (rssi != null) Text('RSSI: $rssi dBm'),
if (isLastConnected)
const Text('Last connected',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold)),
],
),
trailing: const Row(
mainAxisSize: MainAxisSize.min,
children: [],
),
onTap: () async {
await _connectToDevice(device);
},
),
);
},
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: _isScanning
? ElevatedButton(
onPressed: () async {
await stopScan();
},
style:
ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Stop scan'),
)
: ElevatedButton(
onPressed: () async {
if (Platform.isIOS) {
if (!_permissionsReady) {
await Permission.bluetooth.request();
}
await startScan();
return;
}
await _ensurePermissions();
if (!context.mounted) return;
if (_permissionsReady) {
await startScan();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Grant Bluetooth permission when prompted'),
),
);
}
},
child: const Text('Start scan'),
),
),
],
),
),
);
}
Future<void> startScan() async {
await _startReconnectScan(ScanMode.aggressive);
}
Future<void> stopScan() async {
final manager = aidlabManager;
if (manager != null) {
await manager.stopScan();
}
if (!mounted) {
return;
}
setState(() {
_isScanning = false;
_devices.clear();
_lastConnectedAddress = null;
});
lastConnectedAddress = null;
}
@override
void didDiscover(Device device, int rssi) {
final String? last = _lastConnectedAddress;
final bool isKnown = _allowedNames.contains(device.name) ||
(last != null && device.address == last);
if (!isKnown) {
return;
}
_deviceRssi[device.address] = rssi;
setState(() {
if (!_devices.any((d) => d.address == device.address)) {
_devices.add(device);
} else {
_devices.removeWhere((d) => d.address == device.address);
_devices.add(device);
}
});
if (last != null && device.address == last) {
scheduleMicrotask(() async {
try {
await _connectToDevice(device);
} catch (error) {
debugPrint('Auto reconnect failed: $error');
}
});
}
}
@override
void onBluetoothStarted() {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Bluetooth started")));
}
@override
void onDeviceScanStarted() {}
@override
void onDeviceScanStopped() {}
@override
void onScanFailed(int errorCode) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Scan failed: $errorCode")));
}
Future<void> _connectToDevice(Device device) async {
if (_deviceFlowActive) {
return;
}
_deviceFlowActive = true;
if (_isScanning) {
await stopScan();
}
if (!mounted) {
_deviceFlowActive = false;
return;
}
lastConnectedAddress = device.address;
setState(() {
_lastConnectedAddress = device.address;
});
try {
await Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => DeviceDetailScreen(
device: device,
rememberAsLastConnected: true,
),
),
);
} finally {
_deviceFlowActive = false;
}
if (!mounted || _lastConnectedAddress == null) {
return;
}
await _startReconnectScan(ScanMode.aggressive);
}
}
class DeviceDetailScreen extends StatefulWidget {
final Device device;
final bool rememberAsLastConnected;
const DeviceDetailScreen({
super.key,
required this.device,
required this.rememberAsLastConnected,
});
@override
State<DeviceDetailScreen> createState() => _DeviceDetailScreenState();
}
class _DeviceDetailScreenState extends State<DeviceDetailScreen>
implements DeviceDelegate {
String _firmwareRevision = "Unknown";
String _hardwareRevision = "Unknown";
String _serialNumber = "Unknown";
String _heartRate = "Unknown";
String _skinTemperature = "Unknown";
String _respirationRate = "Unknown";
String _rr = "Unknown";
String _activity = "Unknown";
int _steps = 0;
String _soundVolume = "Unknown";
String _batteryLevel = "Unknown";
String _signalQuality = "Unknown";
String _bodyPosition = "Unknown";
String _wearState = "Unknown";
String _syncState = "Unknown";
String _exercise = "Unknown";
bool _isConnected = false;
bool _isClosing = false;
final List<double> _ecg = [];
final List<double> _respiration = [];
static const List<DataType> _liveDataTypes = <DataType>[
DataType.ecg,
DataType.respiration,
DataType.skinTemperature,
DataType.activity,
DataType.steps,
DataType.heartRate,
DataType.rr,
DataType.respirationRate,
DataType.bodyPosition,
DataType.pressure,
];
/// Connect to device when the screen is created
@override
void initState() {
super.initState();
scheduleMicrotask(() async {
try {
await _connectToDeviceLifecycle();
} catch (error) {
debugPrint('Initial device detail flow failed: $error');
}
});
}
Future<void> _connectToDeviceLifecycle() async {
try {
await widget.device.connect(this);
if (_isClosing || !mounted) {
await widget.device.disconnect();
return;
}
} catch (error) {
debugPrint('device.connect failed: $error');
if (_isClosing || !mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Connection failed: $error')),
);
setState(() {
_isConnected = false;
});
}
}
@override
Widget build(BuildContext context) {
return PopScope<void>(
canPop: _isConnected,
onPopInvokedWithResult: (bool didPop, void _) {
if (didPop) {
return;
}
final NavigatorState navigator = Navigator.of(context);
scheduleMicrotask(() async {
try {
if (!_isConnected) {
_isClosing = true;
await widget.device.disconnect();
}
} catch (error) {
debugPrint('Cancel pending connection failed: $error');
} finally {
if (mounted && navigator.mounted) {
navigator.pop();
}
}
});
},
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_isConnected ? 'Connected Device' : 'Connecting...'),
backgroundColor: _isConnected ? Colors.green : Colors.orange,
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
ListTile(
title: const Text('Device Address'),
trailing: Text(widget.device.address),
),
ListTile(
title: const Text('Device Name'),
trailing: Text(widget.device.name ?? "Unknown"),
),
ListTile(
title: const Text('Firmware revision'),
trailing: Text(_firmwareRevision),
),
ListTile(
title: const Text('Hardware revision'),
trailing: Text(_hardwareRevision),
),
ListTile(
title: const Text('Serial number'),
trailing: Text(_serialNumber),
),
ListTile(
title: const Text('Battery Level'),
trailing: Text(_batteryLevel),
),
ListTile(
title: const Text('Signal Quality'),
trailing: Text(_signalQuality),
),
ListTile(
title: const Text('Heart Rate'),
trailing: Text(_heartRate),
),
ListTile(
title: const Text('Skin Temperature'),
trailing: Text(_skinTemperature),
),
ListTile(
title: const Text('Respiration Rate'),
trailing: Text(_respirationRate),
),
ListTile(
title: const Text('RR'),
trailing: Text(_rr),
),
ListTile(
title: const Text('Activity'),
trailing: Text(_activity),
),
ListTile(
title: const Text('Steps'),
trailing: Text(_steps.toString()),
),
ListTile(
title: const Text('Sound Volume'),
trailing: Text(_soundVolume),
),
ListTile(
title: const Text('Body Position'),
trailing: Text(_bodyPosition),
),
ListTile(
title: const Text('Wear State'),
trailing: Text(_wearState),
),
ListTile(
title: const Text('Sync State'),
trailing: Text(_syncState),
),
ListTile(
title: const Text('Exercise'),
trailing: Text(_exercise),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: 100,
child: LineChart(_ecg)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: 100,
child: LineChart(_respiration)),
),
],
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.red,
child: ElevatedButton(
onPressed: _isConnected
? () async {
try {
await widget.device.disconnect();
} catch (error) {
debugPrint('Manual disconnect failed: $error');
}
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text(
_isConnected ? 'DISCONNECT' : 'CONNECTING...',
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
@override
void dispose() {
if (!_isConnected) {
_isClosing = true;
scheduleMicrotask(() async {
try {
await widget.device.disconnect();
} catch (error) {
debugPrint('Dispose disconnect failed: $error');
}
});
}
super.dispose();
}
@override
Future<void> didConnect(Device device) async {
if (_isClosing || !mounted) {
return;
}
if (widget.rememberAsLastConnected) {
lastConnectedAddress = device.address;
}
setState(() {
_isConnected = true;
_firmwareRevision = device.firmwareRevision ?? "Unknown";
_hardwareRevision = device.hardwareRevision ?? "Unknown";
_serialNumber = device.serialNumber ?? "Unknown";
});
try {
await device.collect(_liveDataTypes, []);
} catch (error) {
debugPrint('Start live collection failed: $error');
}
}
@override
void didDetectUserEvent(Device device, int timestamp) {}
@override
void didDisconnect(Device device, DisconnectReason reason) {
if (!mounted) {
return;
}
setState(() {
_isConnected = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Disconnected: ${reason.toString().split('.').last}')),
);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}
@override
void didDetectExercise(Device device, Exercise exercise) {
setState(() {
_exercise = exercise.toString().split('.').last;
});
}
@override
void didReceiveAccelerometer(
Device device, int timestamp, double ax, double ay, double az) {}
@override
void didReceiveActivity(Device device, int timestamp, ActivityType activity) {
setState(() {
_activity = activity.toString().split('.').last;
});
}
@override
void didReceiveBatteryLevel(Device device, int stateOfCharge) {
setState(() {
_batteryLevel = "$stateOfCharge%";
});
}
@override
void didReceiveBodyPosition(
Device device, int timestamp, BodyPosition bodyPosition) {
setState(() {
_bodyPosition = bodyPosition.toString().split('.').last;
});
}
@override
void didReceivePayload(Device device, String process, Uint8List payload) {
}
@override
void didReceiveECG(Device device, int timestamp, double value) {
setState(() {
_ecg.add(value);
while (_ecg.length > 500) {
_ecg.removeAt(0);
}
});
}
@override
void didReceiveError(Device device, String error) {
debugPrint('Device error (${device.address}): $error');
if (error.toLowerCase().contains('connect') ||
error.toLowerCase().contains('failed') ||
error.toLowerCase().contains('timeout')) {
setState(() {
_isConnected = false;
});
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
}
@override
void didReceiveGyroscope(
Device device, int timestamp, double gx, double gy, double gz) {}
@override
void didReceiveHeartRate(Device device, int timestamp, int heartRate) {
setState(() {
_heartRate = "$heartRate";
});
}
@override
void didReceiveMagnetometer(
Device device, int timestamp, double mx, double my, double mz) {}
@override
void didReceiveOrientation(
Device device, int timestamp, double roll, double pitch, double yaw) {}
@override
void didReceiveQuaternion(Device device, int timestamp, double qw, double qx,
double qy, double qz) {}
@override
void didReceiveRespiration(Device device, int timestamp, double value) {
setState(() {
_respiration.add(value);
while (_respiration.length > 500) {
_respiration.removeAt(0);
}
});
}
@override
void didReceiveRespirationRate(Device device, int timestamp, int value) {
setState(() {
_respirationRate = "$value";
});
}
@override
void didReceiveRr(Device device, int timestamp, int rr) {
setState(() {
_rr = "$rr";
});
}
@override
void didReceiveSignalQuality(Device device, int timestamp, int value) {
setState(() {
_signalQuality = "$value";
});
}
@override
void didReceiveSkinTemperature(Device device, int timestamp, double value) {
setState(() {
_skinTemperature = "${value.toStringAsFixed(1)} °C";
});
}
@override
void didReceiveEDA(Device device, int timestamp, double conductance) {}
@override
void didReceiveGPS(
Device device,
int timestamp,
double latitude,
double longitude,
double altitude,
double speed,
double heading,
double hdop) {}
@override
void didReceiveSoundVolume(Device device, int timestamp, int value) {
setState(() {
_soundVolume = "$value dB";
});
}
@override
void didReceiveSteps(Device device, int timestamp, int steps) {
setState(() {
_steps += steps;
});
}
@override
void didReceivePressure(Device device, int timestamp, int value) {}
@override
void pressureWearStateDidChange(Device device, WearState wearState) {}
@override
void didReceiveUnsynchronizedSize(
Device device, int unsynchronizedSize, double syncBytesPerSecond) {}
@override
void syncStateDidChange(Device device, SyncState state) {
setState(() {
_syncState = state.toString().split('.').last;
});
}
@override
void wearStateDidChange(Device device, WearState wearState) {
setState(() {
_wearState = wearState.toString().split('.').last;
});
}
@override
void didReceivePastAccelerometer(
Device device, int timestamp, double ax, double ay, double az) {}
@override
void didReceivePastActivity(
Device device, int timestamp, ActivityType activity) {}
@override
void didReceivePastBodyPosition(
Device device, int timestamp, BodyPosition bodyPosition) {}
@override
void didReceivePastECG(Device device, int timestamp, double value) {}
@override
void didReceivePastGyroscope(
Device device, int timestamp, double gx, double gy, double gz) {}
@override
void didReceivePastHeartRate(Device device, int timestamp, int heartRate) {}
@override
void didReceivePastMagnetometer(
Device device, int timestamp, double mx, double my, double mz) {}
@override
void didReceivePastOrientation(
Device device, int timestamp, double roll, double pitch, double yaw) {}
@override
void didReceivePastPressure(Device device, int timestamp, int values) {}
@override
void didReceivePastQuaternion(Device device, int timestamp, double qw,
double qx, double qy, double qz) {}
@override
void didReceivePastRespiration(Device device, int timestamp, double value) {}
@override
void didReceivePastRespirationRate(Device device, int timestamp, int value) {}
@override
void didReceivePastRr(Device device, int timestamp, int rr) {}
@override
void didReceivePastSignalQuality(Device device, int timestamp, int value) {}
@override
void didReceivePastSkinTemperature(
Device device, int timestamp, double value) {}
@override
void didReceivePastEDA(Device device, int timestamp, double conductance) {}
@override
void didReceivePastGPS(
Device device,
int timestamp,
double latitude,
double longitude,
double altitude,
double speed,
double heading,
double hdop) {}
@override
void didReceivePastSoundVolume(
Device device, int timestamp, int soundVolume) {}
@override
void didReceivePastSteps(Device device, int timestamp, int value) {}
@override
void didDetectPastUserEvent(Device device, int timestamp) {}
}