microsoft / react-native-winrt

Windows Runtime projection for React Native for Windows
MIT License
85 stars 11 forks source link

Closing a connection to a Bluetooth Low Energy Device throws exception and doesn't work #280

Closed jdschin closed 1 year ago

jdschin commented 1 year ago

I have a react-native application that runs on windows.

Im connecting a BLE Device like this (after a search with the BluetoothLEAdvertisementWatcher)

this.connectedBleDevice = await BluetoothLEDevice.fromBluetoothAddressAsync(bluetoothAddress);

Im having a function that loads a specific Gatt Service

  private loadGattService = async (serviceUuid: string): Promise<GattDeviceService> => {
    if (this.connectedBleDevice) {
      const serviceResult: GattDeviceServicesResult | null = await this.connectedBleDevice.getGattServicesForUuidAsync(
        serviceUuid,
        BluetoothCacheMode.uncached,
      );
      if (serviceResult?.status === GattCommunicationStatusSuccess) {
        if (serviceResult.services.length > 0) {
          return serviceResult.services[0];
        }
      }
    }
    throw new Error('Service could not be loaded');
  };

...

this.gattService = await this.loadGattService(SERVICE_UUID);

With the service Im connecting on characteristics that send notifications. All that works perfectly fine.

When Im trying to disconnect the BLE Device Im getting an exception with no context and the connection is still established.

  disconnect = async (): Promise<void> => {
    try {
      this.statusCharacteristic?.removeEventListener('valuechanged', {}); // doesnt throw
      this.responseCharacteristic?.removeEventListener('valuechanged', {}); // doesnt throw
      this.gattService?.close(); // throws
      this.gattService = undefined
    } catch (e) {
      console.log(e); // =>  [TypeError: Object expected]
      console.log(JSON.stringify(e)); // => {}
    }
  };

After calling disconnect() the BLE device is still connected.

From what I've read in the C# UWP documentation for the native implementation, this.gattService?.close() (I assume that this is the equivalent to gattService.Dispose() in C#) should do the trick and disconnect the device.

I've also tried

this.gattService?.session.close();
this.gattService?.device.close();

Version combination I've tried:

react-native-windows: 0.72.8 react-native-winrt: 0.72.0 react-native: 0.72.4

And

react-native-windows: 0.72.0 react-native-winrt: 0.72.0 react-native: 0.72.0

dunhor commented 1 year ago

While I'm personally unfamiliar with the bluetooth APIs, I can help suggest some debugging steps. First, you say that:

When Im trying to disconnect the BLE Device Im getting an exception with no context and the connection is still established.

I see that logging the exception object gave you [TypeError: Object expected] and trying to convert to JSON string gave you {}. The first error is interesting, however I'm unsure if it's expected or not. The second output seems possibly reasonable to me given how we've had issues in the past with JSI not giving us the proper facilities to ensure things like properties being enumerable. If you change to something like the following, do you have better success?

console.log(`ERROR: ${e.message} (${e.number})`);

Does that give you better output? If not, the next thing I'd try doing is running the application under a debugger like Visual Studio (either launch under the debugger or attach after launch) and then enable the "break on exception" feature. This will allow you to see where the exception is actually originating from. The callstack should hopefully give you enough context as to what's going on.

From what I've read in the C# UWP documentation for the native implementation, this.gattService?.close() (I assume that this is the equivalent to gattService.Dispose() in C#) should do the trick and disconnect the device.

This is kind of interesting because the documentaton, even when scoped down to only C++/WinRT, shows both Close and Dispose being functions available on the GattDeviceService class. By default I'd assume that this is a docs bug, however the description text for the two is different and I don't see mention of Dispose being .NET specific. I'll try and dig into that a bit more

dunhor commented 1 year ago

Okay, so looking at the WinMD I don't see any indication that the type has a Dispose function and it implements IClosable, which is where the Close method comes from and is projected in .NET as IDisposable. So I agree, close is the correct function to call and the inconsistency seems to be an unfortunate docs bug.

jdschin commented 1 year ago

Hey @dunhor thanks for the fast reply! When trying

console.log(`ERROR: ${e.message} (${e.number})`);

I get: ERROR: Object expected (-2146823281)

I also tried to use VisualStudio for debugging (InstanceSettings.UseDirectDebugger = true) but the exception never reaches VisualStudio it always appears in the React Native window. image

When debugging in Chrome with Pause On Caught Exceptions enabled, the debugger also never pauses.

Do you have any ideas how to access the "real" stack trace?

dunhor commented 1 year ago

Hmm, okay, so this one is a little interesting. -2146823281 corresponds to 0x800A138F, which, from what I can see in source, corresponds to ERROR_RESOURCE_NOT_FOUND (although if you're familiar with the way HRESULTs work, you'll notice that this is NOT a Win32 error encoded as an HRESULT, which would start with 0x8007XXXX). So I don't think that this error is originating from the call. Instead, this error seems to be coming from Chakra as all the info I've been able to find suggests that this is a JS specific error. Two things I'd suggest trying next:

  1. console.log(typeof this.gattService)
  2. console.log(typeof this.gattService.close)

Do you have any ideas how to access the "real" stack trace?

Sorry, I should have been more specific. When I suggested attaching the Visual Studio debugger, I was implying that you'll need a native debugger. The Chrome debugger is not a native debugger. That said, if this error is coming from Chakra, I'm unsure why the JS debugger never breaks in. A native debugger might break in, but I'm not personally familiar enough with Chakra to really know.

jdschin commented 1 year ago

Ok, logging this brought a bit more clarity:

console.log('Close: ', this.connectedBleDevice?.close); // => Close: undefined

the function is somehow not there. With that ERROR_RESOURCE_NOT_FOUND makes sense. BluetoothLEDevice.close seems to be missing.

jdschin commented 1 year ago

Same goes for GattDeviceService.close

dunhor commented 1 year ago

Okay, I think I have an idea. What does your RnWinRTParameters entry look like in your ExperimentalFeatures.props file? E.g. it should look something like this

    <RnWinRTParameters>
      -include Windows.Devices.Bluetooth.GenericAttributeProfile
      ...
    </RnWinRTParameters>

If it doesn't already, please try adding -include Windows.Foundation to the list of included namespaces as that's the namespace where IClosable is defined.

jdschin commented 1 year ago

I just had those Parameters set. I thought having -include Windows.Devices.Bluetooth would be enough.

But having the config like this fixes the undefined problem.

    <RnWinRTParameters>
        -include Windows.Devices.Bluetooth
        -include Windows.Devices.Bluetooth.Advertisement
        -include Windows.Storage.Streams
        -include Windows.Devices.Radios
        -include Windows.Devices.Bluetooth.GenericAttributeProfile
        -include Windows.Foundation
    </RnWinRTParameters>

Calling .close() still doesn't disconnect my Bluetooth LE device. I'm also setting all of the references I have in TypeScript to null or undefined. Any idea what else I could try?

jdschin commented 1 year ago

FYI: The react native base project is a C# project. Not sure if this changes anything since react-native-winrt generates JSI wrappers for the C++ library.

dunhor commented 1 year ago

But having the config like this fixes the undefined problem

Awesome, great to hear!

I thought having -include Windows.Devices.Bluetooth would be enough.

It's a balancing act. If we recursively include namespaces that are referenced by the included namespaces, we'd end up including a lot of types that aren't needed which would produce a binary that's both large and time consuming to build.

Calling .close() still doesn't disconnect my Bluetooth LE device. I'm also setting all of the references I have in TypeScript to null or undefined. Any idea what else I could try?

Unfortunately, I'm not familiar with the Bluetooth APIs and have never tried to use them. I'd suggest following an existing sample (which it sounds like you might be doing?). You might find something useful in the Windows-universal-samples repo

FYI: The react native base project is a C# project. Not sure if this changes anything since react-native-winrt generates JSI wrappers for the C++ library.

No, that shouldn't pose an issue. WinRT describes and ABI, so it doesn't matter which language is being used, so long as the ABI is being respected.