aidlab_sdk 2.3.0 copy "aidlab_sdk: ^2.3.0" to clipboard
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) {}
}
4
likes
160
points
812
downloads

Documentation

API reference

Publisher

verified publisheraidlab.com

Weekly Downloads

Aidlab Flutter SDK. For more information please visit https://www.aidlab.com/developer

Homepage

License

MIT (license)

Dependencies

ffi, flutter, flutter_reactive_ble

More

Packages that depend on aidlab_sdk

Packages that implement aidlab_sdk