yanshouwang / bluetooth_low_energy

A Flutter plugin for controlling the bluetooth low energy.
https://pub.dev/packages/bluetooth_low_energy
MIT License
50 stars 16 forks source link

Cannot access value of empty optional (Windows) #63

Closed ddomnik closed 5 months ago

ddomnik commented 7 months ago

Hey, I am developing on windows, and notice a crash when a BLE device disconnects unexpectedly.

How to reproduce:

  1. Connect to BLE device.
  2. Perform hardware reset on BLE device / turn off BLE device.

image

[ERROR:flutter/shell/common/shell.cc(1038)] The 'dev.flutter.pigeon.bluetooth_low_energy_windows.MyCentralManagerFlutterApi.onConnectionStateChanged' channel sent a message from native to Flutter on a non-platform thread. Platform channel messages must be sent on the platform thread. Failure to do so may result in data loss or crashes, and must be fixed in the plugin or application code creating that channel.
See https://docs.flutter.dev/platform-integration/platform-channels#channels-and-platform-threading for more information.
flutter: connectionStateChangedSubscription
Lost connection to device.

Exited.

my code:


class BledSign {

  late final ValueNotifier<BluetoothLowEnergyState> state;
  late final ValueNotifier<bool> discovering;
  late final ValueNotifier<List<DiscoveredEventArgs>> discoveredEventArgs;
  late final StreamSubscription stateChangedSubscription;
  late final StreamSubscription discoveredSubscription;

  late final ValueNotifier<Peripheral?> connectedDevice;
  late final ValueNotifier<String> connectionDeviceName;
  late final ValueNotifier<bool> connectionState;
  late final DiscoveredEventArgs eventArgs;
  late final ValueNotifier<List<GattService>> services;
  late final ValueNotifier<List<GattCharacteristic>> characteristics;
  late final ValueNotifier<GattService?> service;
  late final ValueNotifier<GattCharacteristic?> characteristic;
  late final ValueNotifier<GattCharacteristicWriteType> writeType;
  late final TextEditingController writeController;
  late final StreamSubscription connectionStateChangedSubscription;
  late final StreamSubscription characteristicNotifiedSubscription;

  BledSign._private() {
    state = ValueNotifier(BluetoothLowEnergyState.unknown);
    discovering = ValueNotifier(false);
    discoveredEventArgs = ValueNotifier([]);

    connectedDevice = ValueNotifier(null);
    connectionDeviceName = ValueNotifier("");
    connectionState = ValueNotifier(false);
    services = ValueNotifier([]);
    characteristics = ValueNotifier([]);
    service = ValueNotifier(null);
    characteristic = ValueNotifier(null);
    writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
    writeController = TextEditingController();

    stateChangedSubscription = CentralManager.instance.stateChanged.listen(
      (eventArgs) {
        print("stateChangedSubscription");

        state.value = eventArgs.state;
        print('Change in stateChangedSubscription: $state.value');
      },
      onError: (error) {
        // Handle errors
        print('Error in stateChangedSubscription: $error');
      },
      onDone: () {
        // Handle stream closure
        print('stateChangedSubscription stream closed');
      },
    );

    discoveredSubscription = CentralManager.instance.discovered.listen(
      (eventArgs) {
        print("discoveredSubscription");

        final items = discoveredEventArgs.value;

        bool isNewEntry = true;
        int indexToUpdate = -1;

        // Check if the device is already in the list
        for (int i = 0; i < items.length; i++) {
          if (items[i].peripheral == eventArgs.peripheral) {
            isNewEntry = false;
            indexToUpdate = i;
            break;
          }
        }

        if (isNewEntry) {
          // Add a new entry if the device is not in the list
          List<DiscoveredEventArgs> updatedItems = List.from(items);
          updatedItems.add(eventArgs);
          discoveredEventArgs.value = updatedItems;

          print("UPDATE");
        } else {
          // Update the existing entry if the device is already in the list
          List<DiscoveredEventArgs> updatedItems = List.from(items);
          updatedItems[indexToUpdate] = eventArgs;
          discoveredEventArgs.value = updatedItems;
        }
      },
    );

    connectionStateChangedSubscription = CentralManager.instance.connectionStateChanged.listen(
      (eventArgs) async {
        print("connectionStateChangedSubscription");

        services.value = await CentralManager.instance.discoverGATT(eventArgs.peripheral);

        final connectionState = eventArgs.connectionState;
        this.connectionState.value = connectionState;
        if (!connectionState) {
          connectionDeviceName.value = "";
          services.value = [];
          characteristics.value = [];
          service.value = null;
          characteristic.value = null;
          connectedDevice.value = null;
          print("connectedDevice null");
        } else {
          discoveredEventArgs.value.forEach((device) {
            if (device.peripheral.uuid == eventArgs.peripheral.uuid) {
              connectionDeviceName.value = device.advertisement.name!;
            }
          });
        }
      },
    );

    characteristicNotifiedSubscription = CentralManager.instance.characteristicNotified.listen(
      (eventArgs) {},
    );

    print("BLED SIGN INSTANCE CREATED");
  }

  // Static private instance variable
  static BledSign? _instance;

  // Static method to access the instance
  static BledSign get instance {
    // Initialize instance if null
    _instance ??= BledSign._private();
    return _instance!;
  }

  init() async {
    hierarchicalLoggingEnabled = true;
    CentralManager.instance.logLevel = Level.ALL;

    await CentralManager.instance.setUp();

    state.value = await CentralManager.instance.getState();

    if (kDebugMode) {
      print('BLED SIGN INSTANCE Initialized ${state.value}');
    }
  }

  Future<void> startDiscovery() async {
    print("START DISCOVERY");
    //discoveredEventArgs.value = [];
    await CentralManager.instance.stopDiscovery();
    await CentralManager.instance.startDiscovery();
    discovering.value = true;

    Future.delayed(Duration(seconds: 5), stopDiscovery);
  }

  Future<void> stopDiscovery() async {
    print("STOP DISCOVERY");
    await CentralManager.instance.stopDiscovery();
    discovering.value = false;
  }

  Future<bool> connect(Peripheral peripheral) async {
    print("Connect to: ${peripheral.uuid}");
    try {
      await CentralManager.instance.connect(peripheral);
      connectedDevice.value = peripheral;
      return true;
    } on Exception catch (_err) {
      print('Connect failed: $_err');
      return false;
    }
  }

  Future<bool> disconnect(Peripheral? peripheral) async {
    connectedDevice.value = null;
    if (peripheral != null) {
      print("Disconnect");
      try {
        await CentralManager.instance.disconnect(peripheral);
        return true;
      } on Exception catch (_err) {
        print('Disconnect failed: $_err');
        return false;
      }
    }
    return false;
  }
  cleanup() {
    stateChangedSubscription.cancel();
    discoveredSubscription.cancel();
    state.dispose();
    discovering.dispose();
    discoveredEventArgs.dispose();

    connectionStateChangedSubscription.cancel();
    characteristicNotifiedSubscription.cancel();
    connectionState.dispose();
    services.dispose();
    characteristics.dispose();
    service.dispose();
    characteristic.dispose();
    writeType.dispose();
    writeController.dispose();
  }
}
yanshouwang commented 7 months ago

Thanks for your report, I'll look into this issue a few days later.

yanshouwang commented 6 months ago

Can't reproduce this issue with my device, is this issue always happen with your device? Need more information to resolve it.

ddomnik commented 6 months ago

Yes. If you can guide me where this issue rises in the source code and how to debug your lib I can try to resolve it.

yanshouwang commented 6 months ago

Yes. If you can guide me where this issue rises in the source code and how to debug your lib I can try to resolve it.

Here is the guide of how to develope windows plugin in the step2f section. You need Visual Studio 2022 with C++ environment to debug this plugin.

After run the flutter app on windows, open the windows sln, then attach the app process to Visual Studio to debug the native codes.

ddomnik commented 6 months ago

Thanks.

Also this is the code I use for testing, where I can reproduce the error:

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class BledDev {
  late bool connecting = false;

  late final ValueNotifier<BluetoothLowEnergyState> state;
  late final ValueNotifier<bool> discovering;
  late final ValueNotifier<List<DiscoveredEventArgs>> discoveredEventArgs;
  late final StreamSubscription stateChangedSubscription;
  late final StreamSubscription discoveredSubscription;

  late final ValueNotifier<Peripheral?> connectedDevice;
  late final ValueNotifier<String> connectionDeviceName;
  late final ValueNotifier<bool> connectionState;
  late final DiscoveredEventArgs eventArgs;
  late final ValueNotifier<List<GattService>> services;
  late final ValueNotifier<List<GattCharacteristic>> characteristics;
  late final ValueNotifier<GattService?> service;
  late final ValueNotifier<GattCharacteristic?> characteristic;
  late final ValueNotifier<GattCharacteristicWriteType> writeType;
  late final TextEditingController writeController;
  late final StreamSubscription connectionStateChangedSubscription;
  late final StreamSubscription characteristicNotifiedSubscription;

  BledDev._private() {
    state = ValueNotifier(BluetoothLowEnergyState.unknown);
    discovering = ValueNotifier(false);
    discoveredEventArgs = ValueNotifier([]);

    connectedDevice = ValueNotifier(null);
    connectionDeviceName = ValueNotifier("");
    connectionState = ValueNotifier(false);
    services = ValueNotifier([]);
    characteristics = ValueNotifier([]);
    service = ValueNotifier(null);
    characteristic = ValueNotifier(null);
    writeType = ValueNotifier(GattCharacteristicWriteType.withResponse);
    writeController = TextEditingController();

    stateChangedSubscription = CentralManager.instance.stateChanged.listen(
      (eventArgs) {
        print("stateChangedSubscription");

        state.value = eventArgs.state;
        print('Change in stateChangedSubscription: $state.value');
      },
      onError: (error) {
        // Handle errors
        print('Error in stateChangedSubscription: $error');
      },
      onDone: () {
        // Handle stream closure
        print('stateChangedSubscription stream closed');
      },
    );

    discoveredSubscription = CentralManager.instance.discovered.listen(
      (eventArgs) {
        print("discoveredSubscription");

        final items = discoveredEventArgs.value;

        bool isNewEntry = true;
        int indexToUpdate = -1;

        // Check if the device is already in the list
        for (int i = 0; i < items.length; i++) {
          if (items[i].peripheral == eventArgs.peripheral) {
            isNewEntry = false;
            indexToUpdate = i;
            break;
          }
        }

        if (isNewEntry) {
          // Add a new entry if the device is not in the list
          List<DiscoveredEventArgs> updatedItems = List.from(items);
          updatedItems.add(eventArgs);
          discoveredEventArgs.value = updatedItems;

          updatedItems.forEach((element) {
            print("name: '${element.advertisement.name}' uudi: '${element.peripheral.uuid}'");

            if (element.advertisement.name == "BLED DEV") {
              connect(element.peripheral);
            }
          });

          print("UPDATE");
        } else {
          // Update the existing entry if the device is already in the list
          List<DiscoveredEventArgs> updatedItems = List.from(items);
          updatedItems[indexToUpdate] = eventArgs;
          discoveredEventArgs.value = updatedItems;
        }
      },
    );

    connectionStateChangedSubscription = CentralManager.instance.connectionStateChanged.listen(
      (eventArgs) async {
        print("connectionStateChangedSubscription");

        services.value = await CentralManager.instance.discoverGATT(eventArgs.peripheral);

        final connectionState = eventArgs.connectionState;
        this.connectionState.value = connectionState;
        if (!connectionState) {
          connectionDeviceName.value = "";
          services.value = [];
          characteristics.value = [];
          service.value = null;
          characteristic.value = null;
          connectedDevice.value = null;
          print("connectedDevice null");
        } else {
          discoveredEventArgs.value.forEach((device) {
            if (device.peripheral.uuid == eventArgs.peripheral.uuid) {
              connectionDeviceName.value = device.advertisement.name!;
            }
          });
        }
      },
    );

    characteristicNotifiedSubscription = CentralManager.instance.characteristicNotified.listen(
      (eventArgs) {},
    );

    print("BLED DEV INSTANCE CREATED");
  }

  // Static private instance variable
  static BledDev? _instance;

  // Static method to access the instance
  static BledDev get instance {
    // Initialize instance if null
    _instance ??= BledDev._private();
    return _instance!;
  }

  init() async {
    hierarchicalLoggingEnabled = true;
    CentralManager.instance.logLevel = Level.ALL;

    await CentralManager.instance.setUp();

    state.value = await CentralManager.instance.getState();

    if (kDebugMode) {
      print('BLED DEV INSTANCE Initialized ${state.value}');
    }
  }

  Future<void> startDiscovery() async {
    print("START DISCOVERY");
    discovering.value = true;
    //discoveredEventArgs.value = [];
    await CentralManager.instance.stopDiscovery();
    await CentralManager.instance.startDiscovery();
    Future.delayed(Duration(seconds: 5), stopDiscovery);
  }

  Future<void> stopDiscovery() async {
    print("STOP DISCOVERY");
    await CentralManager.instance.stopDiscovery();
    discovering.value = false;
  }

  Future<bool> connect(Peripheral peripheral) async {
    if (connecting) {
      print("Already connecting ...");
      return false;
    }

    connecting = true;
    print("Connect to: ${peripheral.uuid}");
    try {
      await CentralManager.instance.connect(peripheral);
      connectedDevice.value = peripheral;

      connecting = false;
      return true;
    } on Exception catch (_err) {
      print('Connect failed: $_err');
      connecting = false;
      return false;
    }
  }

  Future<bool> disconnect(Peripheral? peripheral) async {
    connectedDevice.value = null;
    if (peripheral != null) {
      print("Disconnect");
      try {
        await CentralManager.instance.disconnect(peripheral);

        return true;
      } on Exception catch (_err) {
        print('Disconnect failed: $_err');
        return false;
      }
    }
    return false;
  }

  cleanup() {
    stateChangedSubscription.cancel();
    discoveredSubscription.cancel();
    state.dispose();
    discovering.dispose();
    discoveredEventArgs.dispose();

    connectionStateChangedSubscription.cancel();
    characteristicNotifiedSubscription.cancel();
    connectionState.dispose();
    services.dispose();
    characteristics.dispose();
    service.dispose();
    characteristic.dispose();
    writeType.dispose();
    writeController.dispose();
  }
}

Modify NewProject Example this way:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  final BledDev _bledDev = BledDev.instance;

  @override
  void initState() {
    super.initState();
    print("home");

    _bledDev.init();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      _bledDev.startDiscovery();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
yanshouwang commented 6 months ago

services.value = await CentralManager.instance.discoverGATT(eventArgs.peripheral);

This method should only be called when connected

====== EDIT

I saw this crash when method called with wrong status. The null pointer exception can not be caught, will fix this crash in the next version. For now please don't call methods with wrong status to avoid this crash.

yanshouwang commented 6 months ago

The 6.0.0-dev.0 has released, this issue should be resolved.

yanshouwang commented 5 months ago

Fixed in 6.0.0