Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disconnect hangs sometimes on Windows #788

Open
melihercan opened this issue Dec 21, 2023 · 18 comments
Open

Disconnect hangs sometimes on Windows #788

melihercan opened this issue Dec 21, 2023 · 18 comments

Comments

@melihercan
Copy link

To help us fix your issue, please provide the information in the below template. If something causes a crash, provide as much information as you can gather.
Just imagine: we do not know what you are doing!

Note: There is often little we can do without a minimal reproducible sample of the issue, so please provide that in a standalone git repository and link it here.

Steps to reproduce

  1. Get main branch

  2. Customize the program.cs so that it scans passively 3 times and then calls ShowNumberOfServices on WGX-iBeacon device:

using BLE.Client.WinConsole;
using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using System;

Console.WriteLine("Hello, BLE World!");
using (var ct = new ConsoleTracer())
{
    ////Plugin.BLE.Abstractions.Trace.TraceImplementation = ct.GetPrefixedTrace("Plugin.BLE");
    var demo = new BleDemo(ct.GetPrefixedTrace("      DEMO"));

    for (int i=0; i<3; i++)
        await demo.DoTheScanning(ScanMode.Passive, 5000);
    await demo.ShowNumberOfServices("EB408B203E81");    // iBeacon
}

Also change the DoTheScanning debug messages a bit:

            ////Write("Bluetooth is on");
            Write("Scanning now for " + time_ms + " ms..." + " with mode:" + scanMode);            

Expected behavior

  1. It should scan and report the found BLE devices
  2. Connect to WGX-iBeacon device
  3. Get services and display them
  4. Repeat 2 and 3

Actual behavior

  1. Scan is OK
  2. Connection to the device takes a long time, ~30sec (please see the log below)!!!
  3. Calls dev.GetServicesAsync and it hangs!!!

I have tried with different BLE devices and the issue is reproducible

Crashlog

No crash just hanging. Here is the log:

Hello, BLE World!
16:31:40.687       DEMO - Scanning now for 5000 ms... with mode:Passive
16:31:41.226       DEMO - DeviceDiscovered: 5C4076784064 with Name = Bluetooth 5c:40:76:78:40:64
16:31:41.276       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
16:31:43.243       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
16:31:45.730       DEMO - Scanning now for 5000 ms... with mode:Passive
16:31:46.489       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
16:31:46.489       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
16:31:46.513       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
16:31:46.513       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
16:31:46.538       DEMO - DeviceDiscovered: 5C4076784064 with Name = Bluetooth 5c:40:76:78:40:64
16:31:46.538       DEMO - DeviceDiscovered: 5C4076784064 with Name = Bluetooth 5c:40:76:78:40:64
16:31:47.199       DEMO - DeviceDiscovered: E0FD80FCB5B7 with Name = Bluetooth e0:fd:80:fc:b5:b7
16:31:47.199       DEMO - DeviceDiscovered: E0FD80FCB5B7 with Name = Bluetooth e0:fd:80:fc:b5:b7
16:31:50.755       DEMO - Scanning now for 5000 ms... with mode:Passive
16:31:51.047       DEMO - DeviceDiscovered: 7F0B22C537DB with Name = Bluetooth 7f:0b:22:c5:37:db
16:31:51.047       DEMO - DeviceDiscovered: 7F0B22C537DB with Name = Bluetooth 7f:0b:22:c5:37:db
16:31:51.047       DEMO - DeviceDiscovered: 7F0B22C537DB with Name = Bluetooth 7f:0b:22:c5:37:db
16:31:51.521       DEMO - DeviceDiscovered: 5C4076784064 with Name = Bluetooth 5c:40:76:78:40:64
16:31:51.521       DEMO - DeviceDiscovered: 5C4076784064 with Name = Bluetooth 5c:40:76:78:40:64
16:31:51.521       DEMO - DeviceDiscovered: 5C4076784064 with Name = Bluetooth 5c:40:76:78:40:64
16:31:52.463       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
16:31:52.463       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
16:31:52.463       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
16:31:53.395       DEMO - DeviceDiscovered: EB408B203E81 with Name = WGX_iBeacon
16:31:53.395       DEMO - DeviceDiscovered: EB408B203E81 with Name = WGX_iBeacon
16:31:53.395       DEMO - DeviceDiscovered: EB408B203E81 with Name = WGX_iBeacon
16:31:54.588       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
16:31:54.588       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
16:31:54.588       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
16:31:55.787       DEMO - Connecting to device with address = EB408B203E81
16:32:33.591       DEMO - Connected to WGX_iBeacon EB408B203E81 Connected
16:32:33.591       DEMO - Calling dev.GetServicesAsync()...

Configuration

Version of the Plugin: Repo main brach

Platform: Windows 11

Device: WGX-iBeacon

@smsissuechecker
Copy link

Hi @melihercan,

I'm the friendly issue checker.
Thanks for using the issue template 🌟
I appreciate it very much. I'm sure, the maintainers of this repository will answer, soon.

@melihercan
Copy link
Author

On the following run it works ok:

Hello, BLE World!
17:17:56.999       DEMO - Scanning now for 5000 ms... with mode:Passive
17:18:02.039       DEMO - Scanning now for 5000 ms... with mode:Passive
17:18:07.058       DEMO - Scanning now for 5000 ms... with mode:Passive
17:18:08.691       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:18:08.691       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:18:08.691       DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:18:12.122       DEMO - Connecting to device with address = EB408B203E81
17:18:12.232       DEMO - Connected to WGX_iBeacon EB408B203E81 Connected
17:18:12.232       DEMO - Calling dev.GetServicesAsync()...
17:18:12.276       DEMO - Found 7 services
17:18:13.288       DEMO - Disconnecting from WGX_iBeacon EB408B203E81
17:18:14.300       DEMO - ReConnecting to device WGX_iBeacon EB408B203E81...
17:18:14.300       DEMO - Connect Done.
17:18:15.300       DEMO - Calling dev.GetServicesAsync()...
17:18:15.301       DEMO - Found 7 services
Console Tracer is Finished.

@melihercan
Copy link
Author

On the third run, it hung on the Disconnect call: It looks like there is a race condition somewhere on the native Windows path so that the native calls do not complete...
``
17:21:43.681 DEMO - Scanning now for 5000 ms... with mode:Passive
17:21:45.405 DEMO - DeviceDiscovered: EB408B203E81 with Name = WGX_iBeacon
17:21:45.874 DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:21:48.577 DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
17:21:48.726 DEMO - Scanning now for 5000 ms... with mode:Passive
17:21:48.897 DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
17:21:48.897 DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
17:21:51.025 DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:21:51.025 DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:21:52.210 DEMO - DeviceDiscovered: 4A344A6746ED with Name = Bluetooth 4a:34:4a:67:46:ed
17:21:52.210 DEMO - DeviceDiscovered: 4A344A6746ED with Name = Bluetooth 4a:34:4a:67:46:ed
17:21:53.757 DEMO - Scanning now for 5000 ms... with mode:Passive
17:21:54.275 DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:21:54.275 DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:21:54.275 DEMO - DeviceDiscovered: 43B1DA015144 with Name = Bluetooth 43:b1:da:01:51:44
17:21:55.703 DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
17:21:55.703 DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
17:21:55.703 DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
17:21:56.771 DEMO - DeviceDiscovered: 4246CE57C1BC with Name = Bluetooth 42:46:ce:57:c1:bc
17:21:56.771 DEMO - DeviceDiscovered: 4246CE57C1BC with Name = Bluetooth 42:46:ce:57:c1:bc
17:21:56.771 DEMO - DeviceDiscovered: 4246CE57C1BC with Name = Bluetooth 42:46:ce:57:c1:bc
17:21:58.791 DEMO - Connecting to device with address = EB408B203E81
17:22:01.311 DEMO - Connected to WGX_iBeacon EB408B203E81 Connected
17:22:01.311 DEMO - Calling dev.GetServicesAsync()...
17:22:02.567 DEMO - Found 7 services
17:22:03.582 DEMO - Disconnecting from WGX_iBeacon EB408B203E81`

@janusw
Copy link
Member

janusw commented Dec 27, 2023

I have tried with different BLE devices and the issue is reproducible

[..]

Platform: Windows 11

Device: WGX-iBeacon

So you are saying the same problem also occurs with other devices? Or is it specific to this WGX-iBeacon device?

@janusw
Copy link
Member

janusw commented Dec 27, 2023

@AskBojesen You're the one who contributed the WinConsole demo app (and refactored the Windows BLE implementation). Can you actually reproduce this problem?

@melihercan
Copy link
Author

melihercan commented Dec 27, 2023

I have tried with different BLE devices and the issue is reproducible
[..]
Platform: Windows 11
Device: WGX-iBeacon

So you are saying the same problem also occurs with other devices? Or is it specific to this WGX-iBeacon device?

Yes, the same issue occurs on other devices. For example, I used SimCPR

GetServicesAsync hanging:

Hello, BLE World!
14:55:51.696       DEMO - Scanning now for 5000 ms... with mode:Passive
14:55:52.848       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
14:55:54.058       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
14:55:56.736       DEMO - Scanning now for 5000 ms... with mode:Passive
14:55:57.258       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
14:55:57.258       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
14:56:00.682       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
14:56:00.682       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
14:56:01.757       DEMO - Scanning now for 5000 ms... with mode:Passive
14:56:02.807       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
14:56:02.807       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
14:56:02.807       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
14:56:03.584       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
14:56:03.584       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
14:56:03.584       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
14:56:06.780       DEMO - Connecting to device with address = e0189f0a7401
14:56:08.225       DEMO - Connected to SimCPR sensor E0189F0A7401 Connected
14:56:08.225       DEMO - Calling dev.GetServicesAsync()...

Disconnect hanging:

Hello, BLE World!
15:00:18.658       DEMO - Scanning now for 5000 ms... with mode:Passive
15:00:22.907       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
15:00:23.697       DEMO - Scanning now for 5000 ms... with mode:Passive
15:00:23.862       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
15:00:23.862       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
15:00:26.763       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
15:00:26.763       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
15:00:28.719       DEMO - Scanning now for 5000 ms... with mode:Passive
15:00:29.379       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
15:00:29.379       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
15:00:29.379       DEMO - DeviceDiscovered: E0189F0AEAC0 with Name = SimCPR sensor
15:00:32.196       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
15:00:32.196       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
15:00:32.196       DEMO - DeviceDiscovered: E0189F0A7401 with Name = SimCPR sensor
15:00:33.752       DEMO - Connecting to device with address = e0189f0a7401
15:00:35.401       DEMO - Connected to SimCPR sensor E0189F0A7401 Connected
15:00:35.401       DEMO - Calling dev.GetServicesAsync()...
15:00:37.216       DEMO - Found 6 services
15:00:38.228       DEMO - Disconnecting from SimCPR sensor E0189F0A7401

@AskBojesen
Copy link
Contributor

@AskBojesen You're the one who contributed the WinConsole demo app (and refactored the Windows BLE implementation). Can you actually reproduce this problem?

I will try to reproduce and fix now

@AskBojesen
Copy link
Contributor

I cannot reproduce - @melihercan : Will you give PR #808 a test ?

@melihercan
Copy link
Author

@AskBojesen Sure, I will try it tomorrow and let you know.

@melihercan
Copy link
Author

@AskBojesen Unfortunately, I wasn't able to test it today due to an interruption from another project. I will check again tomorrow.

@melihercan
Copy link
Author

Hi @AskBojesen First I added an extra debug message to mark the end of ShowNumberOfServicesWithDisconnectConnect call.

        public async Task ShowNumberOfServicesWithDisconnectConnect()
        {
            string bleaddress = BleAddressSelector.GetBleAddress();
            Write("Connecting to device with address = {0}", bleaddress);
            IDevice dev = await Adapter.ConnectToKnownDeviceAsync(bleaddress.ToBleDeviceGuid()) ?? throw new Exception("null");
            string name = dev.Name;
            Write($"Connected to {name} {dev.Id.ToHexBleAddress()} {dev.State}");
            Write("Calling dev.GetServicesAsync()...");
            var services = await dev.GetServicesAsync();
            Write("Found {0} services", services.Count);
            await Task.Delay(1000);
            Write("Disconnecting from {0} {1}", name, dev.Id.ToHexBleAddress());
            await Adapter.DisconnectDeviceAsync(dev);
            await Task.Delay(1000);
            Write("ReConnecting to device {0} {1}...", name, dev.Id.ToHexBleAddress());
            await Adapter.ConnectToDeviceAsync(dev);
            Write("Connect Done.");
            await Task.Delay(1000);
            Write("Calling dev.GetServicesAsync()...");
            services = await dev.GetServicesAsync();
            Write("Found {0} services", services.Count);
            await Adapter.DisconnectDeviceAsync(dev);
            await Task.Delay(100);
            Write("------------ END ShowNumberOfServicesWithDisconnectConnect");
        }

then I ran the code 3 times:
Run1: Disconnect hangs
Run2: Connect takes 21 sec! and Disconnect hangs
Run3: Disconnect hangs
Logs808.txt

I attach the log file with 3 runs.

@melihercan
Copy link
Author

@AskBojesen FYI, I quickly replaced the Window API layer with the following code (implemented only the APIs that I am calling - quick and dirty), and then each time it connects, gets services, and disconnects successfully. No delays, no hanging.

namespace Plugin.BLE.Abstractions
{
    public class ScanFilterOptions
    {
    }
    public struct ConnectParameters
    {
    }

}

namespace Plugin.BLE.Abstractions.Contracts
{
    public interface IAdapter
    {
        int ScanTimeout { get; set; }
        ScanMode ScanMode { get; set; }

        event EventHandler<DeviceEventArgs> DeviceAdvertised;

        event EventHandler<DeviceEventArgs> DeviceDiscovered;
        event EventHandler<DeviceEventArgs> DeviceConnected;
        event EventHandler<DeviceEventArgs> DeviceDisconnected;
        event EventHandler<DeviceErrorEventArgs> DeviceConnectionLost;
        event EventHandler<DeviceErrorEventArgs> DeviceConnectionError;
        Task StartScanningForDevicesAsync(ScanFilterOptions scanFilterOptions = null, Func<IDevice, bool> deviceFilter = null, bool allowDuplicatesKey = false, CancellationToken cancellationToken = default);

        Task StopScanningForDevicesAsync();

        Task<IDevice> ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default);
        Task DisconnectDeviceAsync(IDevice device);

    }

    public interface IDevice
    {
        Guid Id { get; }
        string Name { get; }
        int Rssi { get; set; }

        Task<IReadOnlyList<IService>> GetServicesAsync(CancellationToken cancellationToken = default);
        Task<IService> GetServiceAsync(Guid id, CancellationToken cancellationToken = default);

    }

    public interface IService
    {
        Guid Id { get; }
        string Name { get; }

        Task<IReadOnlyList<ICharacteristic>> GetCharacteristicsAsync();
        Task<ICharacteristic> GetCharacteristicAsync(Guid id);

    }

    public interface IDescriptor
    {
    }

    public interface ICharacteristic
    {
        Guid Id { get; }
        string Name { get; }

        byte[] Value { get; }

        event EventHandler<CharacteristicUpdatedEventArgs> ValueUpdated;
        Task<(byte[] data, int resultCode)> ReadAsync(CancellationToken cancellationToken = default);
        Task<int> WriteAsync(byte[] data, CancellationToken cancellationToken = default);
        Task StartUpdatesAsync(CancellationToken cancellationToken = default);
        Task StopUpdatesAsync(CancellationToken cancellationToken = default);
        Task<IReadOnlyList<IDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default);

    }

    public interface IBluetoothLE
    {
        IAdapter Adapter { get; }
    }

    public enum ScanMode
    {
        Passive,
        LowPower,
        Balanced,
        LowLatency
    }

}


namespace Plugin.BLE
{

    internal class Adapter : IAdapter
    {
        private const string SimCprDeviceName = "SimCPR sensor";
        private BluetoothLEAdvertisementWatcher _bleWatcher;
        private ConcurrentDictionary<ulong, Device> _devices = new();
        private bool _isScanningEnabled;

        public Adapter()
        {
            _bleWatcher = new BluetoothLEAdvertisementWatcher();
            _bleWatcher.ScanningMode = BluetoothLEScanningMode/*.Active;*/.Passive;
            _bleWatcher.Received += BleWatcher_Received;
            _bleWatcher.Stopped += BleWatcher_Stopped;
            _bleWatcher.Start();
        }

        private void BleWatcher_Stopped(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
        {
            //throw new NotImplementedException();
        }

        private async void BleWatcher_Received(BluetoothLEAdvertisementWatcher sender, 
            BluetoothLEAdvertisementReceivedEventArgs args)
        {
            if (args.Advertisement.LocalName == SimCprDeviceName)
            {
                Debug.WriteLine($"-------- {DateTime.Now:HH:mm:ss:fff} Received: " +
                    $"{args.BluetoothAddress}, " +
                    $"{args.Advertisement.LocalName}");

                Device device = null;
                if (_devices.ContainsKey(args.BluetoothAddress))
                {
                    device = _devices[args.BluetoothAddress];
                    device.Rssi = args.RawSignalStrengthInDBm;
                }
                else
                {
                    BluetoothLEDevice bluetoothLeDevice =
                        await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
                    Debug.WriteLine($"###################### {DateTime.Now:HH:mm:ss:fff} " +
                        $"Found: " +
                        $"{bluetoothLeDevice.BluetoothAddress}, " +
                        $"{bluetoothLeDevice.DeviceInformation.Properties["System.Devices.Aep.ContainerId"]}");
                    device = new Device(this, bluetoothLeDevice, args.RawSignalStrengthInDBm);
                    _devices.TryAdd(args.BluetoothAddress, device);
                }

                if (_isScanningEnabled)
                {
                    DeviceDiscovered?.Invoke(this, new DeviceEventArgs
                    {
                        Device = device,
                    });
                }
            }
        }

        public int ScanTimeout { get; set ; }
        public ScanMode ScanMode { get ; set ; }

        public event EventHandler<DeviceEventArgs> DeviceAdvertised;
        public event EventHandler<DeviceEventArgs> DeviceDiscovered;
        public event EventHandler<DeviceEventArgs> DeviceConnected;
        public event EventHandler<DeviceEventArgs> DeviceDisconnected;
        public event EventHandler<DeviceErrorEventArgs> DeviceConnectionLost;
        public event EventHandler<DeviceErrorEventArgs> DeviceConnectionError;

        public async Task<IDevice> ConnectToKnownDeviceAsync(Guid deviceGuid, 
            Abstractions.ConnectParameters connectParameters = default, CancellationToken cancellationToken = default)
        {
            var device = _devices.Values.First(d => d.Id == deviceGuid);
            await device.ConnectAsync();
            return device;
        }

        public async Task DisconnectDeviceAsync(IDevice device)
        {
            Device castedDevice = (Device)device;

            await castedDevice.DisconnectAsync();

            castedDevice.BluetoothLeDevice.Dispose();
            _devices.TryRemove(castedDevice.BluetoothLeDevice.BluetoothAddress, out castedDevice);
        }

        public Task StartScanningForDevicesAsync(Abstractions.ScanFilterOptions scanFilterOptions = null, 
            Func<IDevice, bool> deviceFilter = null, bool allowDuplicatesKey = false, 
            CancellationToken cancellationToken = default)
        {
            _isScanningEnabled = true;
            return Task.CompletedTask;
        }

        public Task StopScanningForDevicesAsync()
        {
            _isScanningEnabled = false;
            return Task.CompletedTask;
        }

        internal void OnDeviceConnected(DeviceEventArgs args) => 
            DeviceConnected?.Invoke(this, args);

        internal void OnDeviceDisconnected(DeviceEventArgs args) =>
            DeviceDisconnected?.Invoke(this, args);

        internal void OnDeviceConnectionError(DeviceErrorEventArgs args) =>
            DeviceConnectionError?.Invoke(this, args);

        internal void OnDeviceConnectionLost(DeviceErrorEventArgs args) =>
            DeviceConnectionLost(this, args);
    }

    internal class Device : IDevice
    {
        private Adapter _adapter;
        public BluetoothLEDevice BluetoothLeDevice { get; set; }
        private int _rssi;
        private bool _connected;

        private ConcurrentDictionary<Guid, Service> _services = new();

        public Device(Adapter adapter, BluetoothLEDevice bluetoothLeDevice, int rssi)
        {
            _adapter = adapter;
            BluetoothLeDevice = bluetoothLeDevice;
            _rssi = rssi;

            BluetoothLeDevice.ConnectionStatusChanged += BluetoothLeDevice_ConnectionStatusChanged;
        }

        private void BluetoothLeDevice_ConnectionStatusChanged(BluetoothLEDevice sender, object args)
        {
            if (_connected && BluetoothLeDevice.ConnectionStatus == BluetoothConnectionStatus.Disconnected)
            {
                _connected = false;
                _adapter.OnDeviceConnectionLost(new() 
                { 
                    Device = this, 
                    ErrorMessage = "Connection lost" 
                });
            }
        }

        public Guid Id => (Guid)BluetoothLeDevice.DeviceInformation.Properties["System.Devices.Aep.ContainerId"];

        public string Name => BluetoothLeDevice.DeviceInformation.Name;

        public int Rssi
        {
            get => _rssi;
            set => _rssi = value;
        }

        public Task<IReadOnlyList<IService>> GetServicesAsync(CancellationToken cancellationToken = default)
        {
            return Task.FromResult((IReadOnlyList<IService>)_services.Values);
        }

        public Task<IService> GetServiceAsync(Guid id, CancellationToken cancellationToken = default)
        {
            return Task.FromResult((IService)_services[id]);
        }


        internal async Task ConnectAsync()
        {
            Debug.WriteLine($"+++++++++++++++++++++++++++++++++++++++++++++ {DateTime.Now:HH:mm:ss:fff} {BluetoothLeDevice.ConnectionStatus} BEFORE GetGattServicesAsync");
            GattDeviceServicesResult result = await BluetoothLeDevice
                .GetGattServicesAsync(BluetoothCacheMode.Uncached);
            Debug.WriteLine($"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {DateTime.Now:HH:mm:ss:fff} AFTER GetGattServicesAsync");
            if (result.Status == GattCommunicationStatus.Success)
            {
                foreach(var service in result.Services)
                {
                    if (!_services.ContainsKey(service.Uuid))
                    {
                        _services.TryAdd(service.Uuid, new Service(this, service));
                    }
                }
                _connected = true;
                _adapter.OnDeviceConnected(new()
                {
                    Device = this
                });
            }
            else
            {
                _adapter.OnDeviceConnectionError(new()
                {
                    Device = this,
                    ErrorMessage = "Cannot connected"
                });
            }

        }

        internal async Task DisconnectAsync()
        {
            _connected = false;
            _adapter.OnDeviceDisconnected(new() 
            { 
                Device = this 
            });
        }
    }

    internal class Service : IService
    {
        private Device _device;
        private GattDeviceService _service;

        private ConcurrentDictionary<Guid, Characteristic> _characteristics = new();

        public Service(Device device, GattDeviceService service)
        {
            _device = device;
            _service = service;
        }

        public Guid Id => _service.Uuid;

        public string Name => _service.Uuid.ToString();

        public async Task<IReadOnlyList<ICharacteristic>> GetCharacteristicsAsync()
        {
            GattCharacteristicsResult result = await _service.GetCharacteristicsAsync();
            if (result.Status == GattCommunicationStatus.Success)
            {
                foreach(var characteristic in result.Characteristics)
                {
                    if (!_characteristics.ContainsKey(characteristic.Uuid))
                    {
                        _characteristics.TryAdd(characteristic.Uuid, new Characteristic(this, characteristic));
                    }
                }
            }

            return (IReadOnlyList<ICharacteristic>)_characteristics.Values;
        }

        public Task<ICharacteristic> GetCharacteristicAsync(Guid id)
        {
            return Task.FromResult((ICharacteristic)_characteristics[id]);
        }

    }

    internal class Characteristic : ICharacteristic
    {
        private Service _service;
        private GattCharacteristic _characteristic;

        public Characteristic(Service service, GattCharacteristic characteristic)
        {
            _service = service;
            _characteristic = characteristic;
        }

        public Guid Id => _characteristic.Uuid;

        public string Name => _characteristic.UserDescription;

        private byte[] _value;
        public byte[] Value => _value;

        public event EventHandler<CharacteristicUpdatedEventArgs> ValueUpdated;

        public Task<IReadOnlyList<IDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }

        public async Task<(byte[] data, int resultCode)> ReadAsync(CancellationToken cancellationToken = default)
        {
            GattReadResult result = await _characteristic.ReadValueAsync();
            if (result.Status == GattCommunicationStatus.Success)
            {
                var value = result.Value.ToArray();
                //_value = value;
                return (value, 0);
            }
            else
            {
                return (null, -1);
            }
        }

        public async Task<int> WriteAsync(byte[] data, CancellationToken cancellationToken = default)
        {
            GattCommunicationStatus result = await _characteristic.WriteValueAsync(data.AsBuffer());
            if (result == GattCommunicationStatus.Success)
            {
                return 0;
            }
            else
            {
                return -1;
            }
        }

        public async Task StartUpdatesAsync(CancellationToken cancellationToken = default)
        {
            GattCommunicationStatus status = await _characteristic
                .WriteClientCharacteristicConfigurationDescriptorAsync(
                    GattClientCharacteristicConfigurationDescriptorValue.Notify);
            if (status == GattCommunicationStatus.Success)
            {
                _characteristic.ValueChanged += Characteristic_ValueChanged;
            }
            else
                throw new Exception(status.ToString());
        }


        public async Task StopUpdatesAsync(CancellationToken cancellationToken = default)
        {
            _characteristic.ValueChanged -= Characteristic_ValueChanged;
            await _characteristic
                .WriteClientCharacteristicConfigurationDescriptorAsync(
                    GattClientCharacteristicConfigurationDescriptorValue.None);
        }

        private void Characteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
        {
            var value = args.CharacteristicValue.ToArray();
            _value = value;
            ValueUpdated?.Invoke(this, new CharacteristicUpdatedEventArgs(this));
        }

    }

    public class BleImplementation : IBluetoothLE
    {
        Adapter _adapter;

        public IAdapter Adapter => _adapter;

        public void Initialize()
        {
            _adapter = new Adapter();
        }
    }

    public static class CrossBluetoothLE
    {
        static readonly Lazy<IBluetoothLE> Implementation = new Lazy<IBluetoothLE>(CreateImplementation, System.Threading.LazyThreadSafetyMode.PublicationOnly);

        public static IBluetoothLE Current
        {
            get
            {
                var ret = Implementation.Value;
                if (ret == null)
                {
                    throw NotImplementedInReferenceAssembly();
                }
                return ret;
            }
        }

        static IBluetoothLE CreateImplementation()
        {
#if NETSTANDARD
            return null;
#else
            var implementation = new BleImplementation();
            implementation.Initialize();
            return implementation;
#endif
        }

        internal static Exception NotImplementedInReferenceAssembly()
        {
            return new NotImplementedException("This functionality is not implemented in the portable version of " +
                "this assembly.  You should reference the NuGet package from your main application project " +
                "in order to reference the platform-specific implementation.");
        }
    }
}

namespace Plugin.BLE.Abstractions.EventArgs
{
    public class DeviceEventArgs : System.EventArgs
    {
        public IDevice Device;
    }

    public class DeviceErrorEventArgs : DeviceEventArgs
    {
        public string ErrorMessage;
    }

    public class CharacteristicUpdatedEventArgs : System.EventArgs
    {
        public ICharacteristic Characteristic { get; set; }
        public CharacteristicUpdatedEventArgs(ICharacteristic characteristic)
        {
            Characteristic = characteristic;
        }
    }


}

namespace Plugin.BLE.Extensions
{

}

namespace Plugin.BLE.Abstractions.Extensions
{

}

namespace Plugin.BLE.Windows
{

}

@AskBojesen
Copy link
Contributor

AskBojesen commented Feb 7, 2024

@melihercan : But above is not related to the WinConsole client itself.
When you write that disconnect hangs - have you then been patient ? I have seen it takes 5 secs to disconnect in windows

Do you have a better implementation ? then we need that in a new branch!

In the WinConsole program I have also added some tests not using the plugin - to troubleshoot errors

@melihercan
Copy link
Author

melihercan commented Feb 7, 2024

@melihercan : But above is not related to the WinConsole client itself.
The issue is not coming from the WinConsole app, but the BLE library windows portion. The code I provided replaces the BLE library windows portion. Then it is working fine. I have seen the same hanging issue on MAUI app too.

When you write that disconnect hangs - have you then been patient ? I have seen it takes 5 secs to disconnect in windows
Yes I did (minutes), but it is not completing.

Do you have a better implementation ? then we need that in a new branch!
No I don't. I was not able to use the Windows portion of the BLE library due to the hanging issue. Hence I had the quick and dirty replacement and my app is running fine now.

In the WinConsole program I have also added some tests not using the plugin - to troubleshoot errors

@AskBojesen
Copy link
Contributor

My idea is to make "demos' in the WinConsole project to reproduce errors, and if I suspect that it is the implementation in the plugin which causes errors, then I make a Pure Windows demo to investigate in parallel.

I have just found another error, which I can reproduce every time... I will make a fix for that first.
The error is: Connect -> ConnecttionLost -> Connect

@AskBojesen
Copy link
Contributor

I have just reproduced with the latest'n'greatest from master including my last merged PR
We should rather rename this issue to: Disconnect hangs sometimes on Windows

@AskBojesen
Copy link
Contributor

@melihercan : Will you rename this issue to: "Disconnect hangs sometimes on Windows"
Just for sake of good order...

@melihercan melihercan changed the title BLE.Client.WinConsole demo app hangs on GetServicesAsync call Disconnect hangs sometimes on Windows Feb 12, 2024
@melihercan
Copy link
Author

Renamed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants