shinyorg / shiny

.NET Framework for Backgrounding & Device Hardware Services (iOS, Android, & Catalyst)
https://shinylib.net
MIT License
1.43k stars 227 forks source link

[Bug]: Bug with NotifyCharacteristic Subscription #1405

Closed sleushunou closed 6 months ago

sleushunou commented 6 months ago

Component/Nuget

BluetoothLE Client (Shiny.BluetoothLE)

What operating system(s) are effected?

Version(s) of Operation Systems

tested on iPhone 12 (iOS16)

Hosting Model

Steps To Reproduce

get IPeripheral from .Scan() subscribe to characteristic (call NotifyCharacteristic method from IPeripheral) call Connect() call CancelConnection() call Connect() again try to read characteristic after connection complete (call ReadCharacteristic method from IPeripheral)

Expected Behavior

characteristic readed immediately or readed with a small delay

Actual Behavior

characteristic readed with 5seconds+ delay

Exception or Log output

instead of logs. Why it happens? It happens because on method below handler of this.WhenConnected() subscription calls several times (5 and more ) at the same time, so this.Native.SetNotifyValue(false, native) calls also several times at the same time. Each call of this.Native.SetNotifyValue(false, native) is write operation to descriptor under the hood (I can see it in logs from BLE peripheral device). This write-to-descriptor operations occurs at the system level, and is performed bypassing the operation queue that you added to shiny library. It looks like there is a system queue of BLE operations in iOS, and our read operation is executed only after all five or more write operations to the descriptor have been completed

` public IObservable NotifyCharacteristic(string serviceUuid, string characteristicUuid, bool useIndicateIfAvailable = true) { this.AssertConnnection(); var key = $"{serviceUuid}-{characteristicUuid}";

    if (!this.notifiers.ContainsKey(key))
    {
        var obs = Observable
            .Create<BleCharacteristicResult>(ob =>
            {
                this.logger.LogInformation($"Initial Notification Hook: {serviceUuid} / {characteristicUuid}");

                CBCharacteristic native = null!;
                this.WhenConnected()
                    .Select(_ =>
                    {
                        this.logger.LogDebug($"Connection Detected - Attempting to hook: {serviceUuid} / {characteristicUuid}");
                        return this.GetNativeCharacteristic(serviceUuid, characteristicUuid);
                    })
                    .Switch()
                    .Select(ch =>
                    {
                        this.FromNative(ch).AssertNotify();

                        native = ch;
                        this.logger.CharacteristicInfo("Hooking Notification Characteristic", serviceUuid, characteristicUuid);
                        this.Native.SetNotifyValue(true, ch);

                        return this.charUpdateSubj
                            .Where(x => x.Char.Equals(ch))
                            .Select(x => this.ToResult(x.Char, BleCharacteristicEvent.Notification));
                    })
                    .Switch()
                    .Subscribe(
                        ob.OnNext,
                        ob.OnError
                    );

                return () =>
                {
                    if (native == null)
                        return;

                    try
                    {
                        this.Native.SetNotifyValue(false, native);
                    }
                    catch (Exception ex)
                    {
                        this.logger.DisableNotificationError(ex, serviceUuid, characteristicUuid);
                    }
                };
            })
            .Publish()
            .RefCount();

        this.notifiers.Add(key, obs);
    }

    return this.notifiers[key];
}`

Code Sample

`

    private IDisposable _subscription;

    private async Task Test(IPeripheral peripheral, BleCharacteristicInfo bleCharacteristicInfo)
    {
        _subscription = peripheral
            .NotifyCharacteristic("serviceid", "charid")
            .Subscribe((BleCharacteristicResult obj) =>
        {
        });

        peripheral.Connect(new ConnectionConfig(false));
        await Task.Delay(2000).ConfigureAwait(false);
        peripheral.CancelConnection();
        await Task.Delay(2000).ConfigureAwait(false);
        peripheral.Connect(new ConnectionConfig(false));
        peripheral.WhenStatusChanged().Distinct().Where(x => x == ConnectionState.Connected).Subscribe((obj) =>
        {
            peripheral.ReadCharacteristic(bleCharacteristicInfo);
        });
    }`

Code of Conduct

aritchie commented 6 months ago

I don't see the connection event firing so many times, but the fact is that this is another abnormal use-case you softeq guys have given me. The read operation is happening, so there isn't really a bug here.

You really need to await the connection event before disconnecting in rapid succession like this to have any hope of waiting out events.