aidlab_sdk 2.2.2 copy "aidlab_sdk: ^2.2.2" to clipboard
aidlab_sdk: ^2.2.2 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';
import 'package:shared_preferences/shared_preferences.dart';

AidlabManager? aidlabManager;

final class LastConnectedDeviceStore {
  static const String _key = 'aidlab_example_last_connected_address';

  static Future<String?> load() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String? raw = prefs.getString(_key);
    return (raw == null || raw.isEmpty) ? null : raw;
  }

  static Future<void> save(String address) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(_key, address);
  }
}

final class _BackgroundReconnectDelegate implements DeviceDelegate {
  _BackgroundReconnectDelegate(this._address);

  final String _address;

  @override
  void didConnect(Device device) {
    AutoPairService.registerConnectedDevice(device);
  }

  @override
  void didDisconnect(Device device, DisconnectReason reason) {
    AutoPairService.unregisterConnectedDevice(device);
    AutoPairService.setConnecting(device.address, false);
  }

  @override
  void didReceiveError(String error) {
    AutoPairService.setConnecting(_address, false);
  }

  @override
  void didReceiveECG(Device device, int timestamp, double value) {}

  @override
  void didReceiveRespiration(Device device, int timestamp, double value) {}

  @override
  void didReceiveBatteryLevel(Device device, int stateOfCharge) {}

  @override
  void didReceiveSteps(Device device, int timestamp, int value) {}

  @override
  void didReceiveSkinTemperature(Device device, int timestamp, double value) {}

  @override
  void didReceiveAccelerometer(
      Device device, int timestamp, double ax, double ay, double az) {}

  @override
  void didReceiveGyroscope(
      Device device, int timestamp, double qx, double qy, double qz) {}

  @override
  void didReceiveMagnetometer(
      Device device, int timestamp, double mx, double my, double mz) {}

  @override
  void didReceiveQuaternion(Device device, int timestamp, double qw, double qx,
      double qy, double qz) {}

  @override
  void didReceiveOrientation(
      Device device, int timestamp, double roll, double pitch, double yaw) {}

  @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 didReceiveBodyPosition(
      Device device, int timestamp, BodyPosition bodyPosition) {}

  @override
  void didReceiveHeartRate(Device device, int timestamp, int heartRate) {}

  @override
  void didReceiveRr(Device device, int timestamp, int rr) {}

  @override
  void didReceiveRespirationRate(Device device, int timestamp, int value) {}

  @override
  void didReceiveSoundVolume(Device device, int timestamp, int soundVolume) {}

  @override
  void didDetectExercise(Device device, Exercise exercise) {}

  @override
  void didReceiveActivity(
      Device device, int timestamp, ActivityType activity) {}

  @override
  void wearStateDidChange(Device device, WearState state) {}

  @override
  void didReceivePayload(Device device, String process, Uint8List payload) {}

  @override
  void didDetectUserEvent(Device device, int timestamp) {}

  @override
  void didReceiveSignalQuality(Device device, int timestamp, int value) {}

  @override
  void syncStateDidChange(Device device, SyncState state) {}

  @override
  void didReceivePastECG(Device device, int timestamp, double value) {}

  @override
  void didReceivePastRespiration(Device device, int timestamp, double value) {}

  @override
  void didReceivePastSkinTemperature(
      Device device, int timestamp, double value) {}

  @override
  void didReceivePastHeartRate(Device device, int timestamp, int heartRate) {}

  @override
  void didReceivePastRr(Device device, int timestamp, int rr) {}

  @override
  void didReceiveUnsynchronizedSize(
      Device device, int unsynchronizedSize, double syncBytesPerSecond) {}

  @override
  void didReceivePastRespirationRate(Device device, int timestamp, int value) {}

  @override
  void didReceivePastActivity(
      Device device, int timestamp, ActivityType activity) {}

  @override
  void didReceivePastSteps(Device device, int timestamp, int value) {}

  @override
  void didReceivePastSoundVolume(
      Device device, int timestamp, int soundVolume) {}

  @override
  void didReceivePastAccelerometer(
      Device device, int timestamp, double ax, double ay, double az) {}

  @override
  void didReceivePastGyroscope(
      Device device, int timestamp, double qx, double qy, double qz) {}

  @override
  void didReceivePastMagnetometer(
      Device device, int timestamp, double mx, double my, double mz) {}

  @override
  void didReceivePastQuaternion(Device device, int timestamp, double qw,
      double qx, double qy, double qz) {}

  @override
  void didReceivePastOrientation(
      Device device, int timestamp, double roll, double pitch, double yaw) {}

  @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 didReceivePastBodyPosition(
      Device device, int timestamp, BodyPosition bodyPosition) {}

  @override
  void didReceivePastPressure(Device device, int timestamp, int value) {}

  @override
  void didReceivePastSignalQuality(Device device, int timestamp, int value) {}

  @override
  void didReceivePressure(Device device, int timestamp, int value) {}

  @override
  void didReceivePressureWearState(Device device, WearState wearState) {}

  @override
  void didReceivePastUserEvent(Device device, int timestamp) {}
}

// In-memory connection state and device names (autopair removed)
class AutoPairService {
  static final Map<String, String> _deviceNames = {};
  static final Set<String> _connectedDevices = {};
  static final Set<String> _connectingDevices = {};
  static final Map<String, Device> _connectedDeviceRefs = {};

  static String getDeviceName(String address) {
    return _deviceNames[address] ?? address;
  }

  static void setDeviceName(String address, String name) {
    _deviceNames[address] = name;
  }

  static bool isConnected(String address) {
    return _connectedDevices.contains(address);
  }

  static bool isConnecting(String address) {
    return _connectingDevices.contains(address);
  }

  static Device? connectedDevice(String address) {
    return _connectedDeviceRefs[address];
  }

  static void registerConnectedDevice(Device device) {
    _connectedDeviceRefs[device.address] = device;
    _connectedDevices.add(device.address);
    _connectingDevices.remove(device.address);
  }

  static void unregisterConnectedDevice(Device device) {
    _connectedDeviceRefs.remove(device.address);
    _connectedDevices.remove(device.address);
    _connectingDevices.remove(device.address);
  }

  static void setConnected(String address, bool connected) {
    if (connected) {
      _connectedDevices.add(address);
      _connectingDevices.remove(address);
    } else {
      _connectedDeviceRefs.remove(address);
      _connectedDevices.remove(address);
      _connectingDevices.remove(address);
    }
  }

  static void setConnecting(String address, bool connecting) {
    if (connecting) {
      _connectingDevices.add(address);
    } else {
      _connectingDevices.remove(address);
    }
  }

  static bool canConnect(String address) {
    return !isConnected(address) && !isConnecting(address);
  }
}

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, Device> _discoveredDevices = {};
  final Map<String, int> _deviceRssi = {};
  bool _isScanning = false;
  bool _permissionsReady = false;
  bool _appIsInBackground = false;
  String? _lastConnectedAddress;

  static const Set<String> _allowedNames = <String>{
    'Aidlab',
    'Aidlab 2',
    'Aidmed One',
  };

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    unawaited(_ensurePermissions());
    aidlabManager = AidlabManager(this);
    unawaited(_loadLastConnectedDevice());
  }

  Future<void> _loadLastConnectedDevice() async {
    final String? last = await LastConnectedDeviceStore.load();
    if (!mounted) {
      return;
    }
    setState(() {
      _lastConnectedAddress = last;
    });
  }

  /// Ask for runtime permissions required for BLE scanning.
  /// Android 12+ requires bluetoothScan and bluetoothConnect; Android 6–11 requires Location and enabled location services.
  Future<void> _ensurePermissions() async {
    // iOS: request Bluetooth permission only. Location is not required for CoreBluetooth scanning.
    if (Platform.isIOS) {
      // Request the iOS Bluetooth permission. This triggers the native prompt.
      final bluetoothStatus = await Permission.bluetooth.request();
      setState(() {
        _permissionsReady = bluetoothStatus.isGranted;
      });
      return;
    }

    // Android: request runtime permissions depending on OS version.
    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) {
      // On Android pre-12, users must enable Location Services for BLE scanning.
      await openAppSettings();
    }

    setState(() {
      _permissionsReady = hasBle12Plus || hasBlePre12;
    });
  }

  @override
  void dispose() {
    unawaited(aidlabManager?.dispose());
    aidlabManager = null;
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused ||
        state == AppLifecycleState.inactive) {
      _appIsInBackground = true;
      if (_lastConnectedAddress != null) {
        unawaited(_startBackgroundScan());
      }
    } else if (state == AppLifecycleState.resumed) {
      _appIsInBackground = false;
    }
  }

  Future<void> _startBackgroundScan() async {
    final AidlabManager? manager = aidlabManager;
    if (manager == null) {
      return;
    }
    await manager.scan(ScanMode.lowPower);
    if (!mounted) {
      return;
    }
    setState(() {
      _isScanning = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Devices'),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _devices.length,
              itemBuilder: (context, index) {
                final device = _devices[index];
                final deviceName =
                    AutoPairService.getDeviceName(device.address);
                final isConnected = AutoPairService.isConnected(device.address);
                final isConnecting =
                    AutoPairService.isConnecting(device.address);
                final connectedDevice =
                    AutoPairService.connectedDevice(device.address);
                final rssi = _deviceRssi[device.address];

                return Card(
                  margin:
                      const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  color: isConnected
                      ? Colors.green.shade50
                      : isConnecting
                          ? Colors.orange.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 (isConnected)
                          const Text('Connected',
                              style: TextStyle(
                                  color: Colors.green,
                                  fontWeight: FontWeight.bold)),
                        if (isConnecting)
                          const Text('Connecting...',
                              style: TextStyle(
                                  color: Colors.orange,
                                  fontWeight: FontWeight.bold)),
                      ],
                    ),
                    trailing: const Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [],
                    ),
                    onTap: () {
                      if (AutoPairService.canConnect(device.address)) {
                        unawaited(_connectToDevice(device));
                      } else if (isConnected) {
                        final Device detailDevice = connectedDevice ?? device;
                        // If already connected, still allow to go to detail screen
                        unawaited(
                          Navigator.push(
                            context,
                            MaterialPageRoute<void>(
                              builder: (context) => DeviceDetailScreen(
                                device: detailDevice,
                                rememberAsLastConnected: false,
                                initiallyConnected: connectedDevice != null,
                              ),
                            ),
                          ),
                        );
                      }
                    },
                  ),
                );
              },
            ),
          ),
          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 {
                      // iOS: allow starting scan even if permission isn't yet granted;
                      // the system prompt appears upon first CoreBluetooth use.
                      if (Platform.isIOS) {
                        if (!_permissionsReady) {
                          await Permission.bluetooth.request();
                        }
                        await startScan();
                        return;
                      }

                      // Android: gate scanning on runtime permissions.
                      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 {
    // Use aggressive scan mode so we continue to receive duplicate advertisements
    // and the RSSI values keep refreshing while the device list is visible.
    final manager = aidlabManager;
    if (manager == null) {
      return;
    }

    await manager.scan(ScanMode.aggressive);

    if (!mounted) {
      return;
    }

    setState(() {
      _isScanning = true;
    });
  }

  Future<void> stopScan() async {
    final manager = aidlabManager;
    if (manager != null) {
      await manager.stopScan();
    }

    if (!mounted) {
      return;
    }

    setState(() {
      _isScanning = false;
      _devices.clear();
    });
  }

  @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;
    }

    // Store discovered devices
    _discoveredDevices[device.address] = device;
    _deviceRssi[device.address] = rssi;

    // Save device name if not already saved
    if (!AutoPairService._deviceNames.containsKey(device.address)) {
      AutoPairService.setDeviceName(
          device.address, device.name ?? device.address);
    }

    setState(() {
      if (!_devices.any((d) => d.address == device.address)) {
        _devices.add(device);
      } else {
        _devices.removeWhere((d) => d.address == device.address);
        _devices.add(device);
      }
    });

    // Background auto-reconnect (only after user connected intentionally at least once).
    if (_appIsInBackground &&
        last != null &&
        device.address == last &&
        AutoPairService.canConnect(device.address)) {
      AutoPairService.setConnecting(device.address, true);
      unawaited(device.connect(_BackgroundReconnectDelegate(device.address)));
    }
  }

  @override
  void onBluetoothStarted() {
    ScaffoldMessenger.of(context)
        .showSnackBar(const SnackBar(content: Text("Bluetooth started")));
  }

  @override
  void onDeviceScanStarted() {}

  @override
  void onDeviceScanStopped() {}

  @override
  void onScanFailed(int errorCode) {
    /// Show toast
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text("Scan failed: $errorCode")));
  }

  // Autopair monitoring removed

  Future<void> _connectToDevice(Device device) async {
    // Prevent multiple simultaneous connections to same device
    if (AutoPairService.isConnecting(device.address) ||
        AutoPairService.isConnected(device.address)) {
      return;
    }

    // Open detail screen which handles connection lifecycle
    if (_isScanning) {
      await stopScan();
    }
    if (!mounted) {
      return;
    }
    unawaited(LastConnectedDeviceStore.save(device.address));
    setState(() {
      _lastConnectedAddress = device.address;
    });
    unawaited(
      Navigator.push(
        context,
        MaterialPageRoute<void>(
          builder: (context) => DeviceDetailScreen(
            device: device,
            rememberAsLastConnected: true,
            initiallyConnected: false,
          ),
        ),
      ),
    );
  }
}

class DeviceDetailScreen extends StatefulWidget {
  final Device device;
  final bool rememberAsLastConnected;
  final bool initiallyConnected;

  const DeviceDetailScreen({
    super.key,
    required this.device,
    required this.rememberAsLastConnected,
    this.initiallyConnected = false,
  });

  @override
  State<DeviceDetailScreen> createState() => _DeviceDetailScreenState();
}

class _DeviceDetailScreenState extends State<DeviceDetailScreen>
    implements DeviceDelegate {
  /// Device information
  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 _acceptConnection = true;

  final List<double> _ecg = [];
  final List<double> _respiration = [];
  static const List<DataType> _liveDataTypes = <DataType>[
    DataType.ECG,
    DataType.RESPIRATION,
    DataType.SKIN_TEMPERATURE,
    DataType.ACTIVITY,
    DataType.STEPS,
    DataType.HEART_RATE,
    DataType.RR,
    DataType.RESPIRATION_RATE,
    DataType.BODY_POSITION,
    DataType.PRESSURE,
  ];

  // Ensures UI leaves "Connecting..." as soon as we have proof of live data.
  void _ensureConnectedUi() {
    if (!_isConnected) {
      AutoPairService.setConnected(widget.device.address, true);
      AutoPairService.setConnecting(widget.device.address, false);
      setState(() {
        _isConnected = true;
      });
    }
  }

  /// Connect to device when the screen is created
  @override
  void initState() {
    super.initState();

    if (widget.initiallyConnected) {
      unawaited(_attachToExistingConnection());
    } else {
      AutoPairService.setConnecting(widget.device.address, true);
      unawaited(_connectToDeviceLifecycle());
    }
  }

  Future<void> _attachToExistingConnection() async {
    widget.device.delegate = this;
    AutoPairService.setConnected(widget.device.address, true);
    AutoPairService.setConnecting(widget.device.address, false);

    setState(() {
      _isConnected = true;
      _firmwareRevision = widget.device.firmwareRevision ?? "Unknown";
      _hardwareRevision = widget.device.hardwareRevision ?? "Unknown";
      _serialNumber = widget.device.serialNumber ?? "Unknown";
    });

    try {
      await widget.device.collect(_liveDataTypes, <DataType>[]);
    } on Object {
      AutoPairService.setConnecting(widget.device.address, true);
      await _connectToDeviceLifecycle();
    }
  }

  Future<void> _connectToDeviceLifecycle() async {
    try {
      await widget.device.connect(this);
    } catch (_) {
      AutoPairService.setConnecting(widget.device.address, false);
      if (!_acceptConnection || !mounted) {
        return;
      }
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Connection failed')),
      );
      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);
        unawaited(
          _cancelPendingConnectionIfNeeded().whenComplete(() {
            if (mounted && navigator.mounted) {
              navigator.pop();
            }
          }),
        );
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(_isConnected ? 'Connected Device' : 'Connecting...'),
          backgroundColor: _isConnected ? Colors.green : Colors.orange,
        ),
        body: 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)),
                    ),
                  ],
                ),
              ),
            ),
            // Always visible disconnect button at the bottom
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(16),
              color: Colors.red,
              child: ElevatedButton(
                onPressed: _isConnected ? disconnect : 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) {
      _acceptConnection = false;
      AutoPairService.setConnecting(widget.device.address, false);
      AutoPairService.unregisterConnectedDevice(widget.device);
      unawaited(widget.device.disconnect());
    }
    super.dispose();
  }

  Future<void> _cancelPendingConnectionIfNeeded() async {
    if (_isConnected) {
      return;
    }
    _acceptConnection = false;
    AutoPairService.setConnecting(widget.device.address, false);
    AutoPairService.unregisterConnectedDevice(widget.device);
    await widget.device.disconnect();
  }

  void disconnect() {
    unawaited(widget.device.disconnect());

    // Immediately update local state
    AutoPairService.unregisterConnectedDevice(widget.device);
    setState(() {
      _isConnected = false;
    });
  }

  @override
  void didConnect(Device device) {
    if (!_acceptConnection || !mounted) {
      unawaited(device.disconnect());
      return;
    }

    /// Update device information and connection state
    AutoPairService.registerConnectedDevice(device);
    // Clear connecting state once we are connected to avoid stale UI status
    AutoPairService.setConnecting(device.address, false);

    if (widget.rememberAsLastConnected) {
      unawaited(LastConnectedDeviceStore.save(device.address));
    }

    setState(() {
      _isConnected = true;
      _firmwareRevision = device.firmwareRevision ?? "Unknown";
      _hardwareRevision = device.hardwareRevision ?? "Unknown";
      _serialNumber = device.serialNumber ?? "Unknown";
    });
    unawaited(device.collect(_liveDataTypes, []));
  }

  @override
  void didDetectUserEvent(Device device, int timestamp) {}

  @override
  void didDisconnect(Device? device, DisconnectReason reason) {
    // Update connection state
    if (device != null) {
      AutoPairService.unregisterConnectedDevice(device);
      // Ensure connecting flag is cleared on any disconnect
      AutoPairService.setConnecting(device.address, false);
    }

    if (!mounted) {
      return;
    }

    setState(() {
      _isConnected = false;
    });

    // Show disconnect reason
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
          content: Text('Disconnected: ${reason.toString().split('.').last}')),
    );

    // Automatically go back to scanning screen
    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) {
    final message = String.fromCharCodes(payload);
    debugPrint('[$process] $message');
  }

  @override
  void didReceiveECG(Device device, int timestamp, double value) {
    _ensureConnectedUi();
    setState(() {
      _ecg.add(value);
      while (_ecg.length > 500) {
        _ecg.removeAt(0);
      }
    });
  }

  @override
  void didReceiveError(String error) {
    // If connection error, clear connecting state
    if (error.toLowerCase().contains('connect') ||
        error.toLowerCase().contains('failed') ||
        error.toLowerCase().contains('timeout')) {
      AutoPairService.setConnecting(widget.device.address, false);
      setState(() {
        _isConnected = false;
      });
    }

    /// Show toast
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
  }

  @override
  void didReceiveGyroscope(
      Device device, int timestamp, double qx, double qy, double qz) {}

  @override
  void didReceiveHeartRate(Device device, int timestamp, int heartRate) {
    _ensureConnectedUi();
    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) {
    _ensureConnectedUi();
    setState(() {
      _respiration.add(value);
      while (_respiration.length > 500) {
        _respiration.removeAt(0);
      }
    });
  }

  @override
  void didReceiveRespirationRate(Device device, int timestamp, int value) {
    _ensureConnectedUi();
    setState(() {
      _respirationRate = "$value";
    });
  }

  @override
  void didReceiveRr(Device device, int timestamp, int rr) {
    setState(() {
      _rr = "$rr";
    });
  }

  @override
  void didReceiveSignalQuality(Device device, int timestamp, int value) {
    _ensureConnectedUi();
    setState(() {
      _signalQuality = "$value";
    });
  }

  @override
  void didReceiveSkinTemperature(Device device, int timestamp, double value) {
    _ensureConnectedUi();
    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 didReceivePressureWearState(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 qx, double qy, double qz) {}

  @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 didReceivePastUserEvent(Device device, int timestamp) {}
}
4
likes
0
points
736
downloads

Publisher

verified publisheraidlab.com

Weekly Downloads

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

Homepage

License

unknown (license)

Dependencies

ffi, flutter, flutter_reactive_ble

More

Packages that depend on aidlab_sdk

Packages that implement aidlab_sdk