From 549b39235f3c4e97576e4cca05c45e28ae74502f Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Mon, 29 Dec 2025 23:39:03 +0100 Subject: [PATCH 1/5] PoC - Try handle random failures of EnableNotificationAsync [android] --- .../BluetoothLE/BluetoothLEDevice.cs | 102 +++++++++++------- .../BrickController2/Helpers/AsyncLock.cs | 7 ++ 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs index 77645e23..d5b2832a 100644 --- a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs +++ b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs @@ -8,6 +8,7 @@ using Android.OS; using Android.Runtime; using Java.Util; +using BrickController2.Helpers; using BrickController2.PlatformServices.BluetoothLE; namespace BrickController2.Droid.PlatformServices.BluetoothLE @@ -18,7 +19,7 @@ public class BluetoothLEDevice : BluetoothGattCallback, IBluetoothLEDevice private readonly Context _context; private readonly BluetoothAdapter _bluetoothAdapter; - private readonly object _lock = new object(); + private readonly AsyncLock _lock = new(); private BluetoothDevice? _bluetoothDevice = null; private BluetoothGatt? _bluetoothGatt = null; @@ -49,14 +50,14 @@ public BluetoothLEDevice(Context context, BluetoothAdapter bluetoothAdapter, str { using (token.Register(() => { - lock (_lock) + using (_lock.Lock()) { Disconnect(); _connectCompletionSource?.TrySetResult(null); } })) { - lock (_lock) + using (_lock.Lock(token)) { if (State != BluetoothLEDeviceState.Disconnected) { @@ -97,7 +98,7 @@ public BluetoothLEDevice(Context context, BluetoothAdapter bluetoothAdapter, str var result = await _connectCompletionSource.Task; - lock (_lock) + using (_lock.Lock(token)) { _connectCompletionSource = null; return result; @@ -125,27 +126,25 @@ internal void Disconnect() State = BluetoothLEDeviceState.Disconnected; } - public Task DisconnectAsync() + public async Task DisconnectAsync() { - lock (_lock) + using (await _lock.LockAsync()) { Disconnect(); } - - return Task.CompletedTask; } public async Task EnableNotificationAsync(IGattCharacteristic characteristic, CancellationToken token) { using (token.Register(() => { - lock (_lock) + using (_lock.Lock(token)) { _descriptorWriteCompletionSource?.TrySetResult(false); } })) - lock (_lock) + using (await _lock.LockAsync(token)) { if (_bluetoothGatt == null || State != BluetoothLEDeviceState.Connected) { @@ -158,33 +157,54 @@ public async Task EnableNotificationAsync(IGattCharacteristic characterist return false; } + // wait slightly for local internal state to settle + await Task.Delay(400, token); + var descriptor = nativeCharacteristic.GetDescriptor(ClientCharacteristicConfigurationUUID); if (descriptor == null) { return false; } -#pragma warning disable CA1422 // Validate platform compatibility - if (!(descriptor?.SetValue(BluetoothGattDescriptor.EnableNotificationValue!.ToArray()) ?? false)) + _descriptorWriteCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var valueToSet = BluetoothGattDescriptor.EnableNotificationValue!.ToArray(); + bool writeInitiated = false; + + if (Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu) // API 33+ { - return false; + // New API: Pass value and write type explicitly +#pragma warning disable CA1416 // Validate platform compatibility + var resultCode = _bluetoothGatt.WriteDescriptor(descriptor, valueToSet); +#pragma warning restore CA1416 // Validate platform compatibility + writeInitiated = resultCode == 0; } + else + { +#pragma warning disable CA1422 // Validate platform compatibility + // Old API: Set local value, then write + if (!descriptor.SetValue(valueToSet)) + { + _descriptorWriteCompletionSource = null; + return false; + } + writeInitiated = _bluetoothGatt.WriteDescriptor(descriptor); #pragma warning restore CA1422 // Validate platform compatibility + } - _descriptorWriteCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - -#pragma warning disable CA1422 // Validate platform compatibility - if (!(_bluetoothGatt?.WriteDescriptor(descriptor) ?? false)) + if (!writeInitiated) { _descriptorWriteCompletionSource = null; - return false; - } -#pragma warning restore CA1422 // Validate platform compatibility + return false; // BLE stack was busy or request failed immediately } + } - var result = await _descriptorWriteCompletionSource.Task.ConfigureAwait(false); + // restrict wait time to avoid hanging till target device disconnects + var result = await _descriptorWriteCompletionSource.Task + .WaitAsync(TimeSpan.FromSeconds(10), token) + .ConfigureAwait(false); - lock (_lock) + using (await _lock.LockAsync(token)) { _descriptorWriteCompletionSource = null; return result; @@ -195,13 +215,13 @@ public async Task EnableNotificationAsync(IGattCharacteristic characterist { using (token.Register(() => { - lock (_lock) + using (_lock.Lock()) { _readCompletionSource?.TrySetResult(null); } })) { - lock (_lock) + using (await _lock.LockAsync(token)) { var nativeCharacteristic = ((GattCharacteristic)characteristic).BluetoothGattCharacteristic; @@ -216,7 +236,7 @@ public async Task EnableNotificationAsync(IGattCharacteristic characterist var result = await _readCompletionSource.Task; - lock (_lock) + using (await _lock.LockAsync(token)) { _readCompletionSource = null; return result; @@ -228,13 +248,13 @@ public async Task WriteAsync(IGattCharacteristic characteristic, byte[] da { using (token.Register(() => { - lock (_lock) + using (_lock.Lock()) { _writeCompletionSource?.TrySetResult(false); } })) { - lock (_lock) + using (await _lock.LockAsync(token)) { if (_bluetoothGatt == null || State != BluetoothLEDeviceState.Connected) { @@ -264,7 +284,7 @@ public async Task WriteAsync(IGattCharacteristic characteristic, byte[] da var result = await _writeCompletionSource.Task.ConfigureAwait(false); - lock (_lock) + using (await _lock.LockAsync(token)) { _writeCompletionSource = null; return result; @@ -272,13 +292,13 @@ public async Task WriteAsync(IGattCharacteristic characteristic, byte[] da } } - public Task WriteNoResponseAsync(IGattCharacteristic characteristic, byte[] data, CancellationToken token) + public async Task WriteNoResponseAsync(IGattCharacteristic characteristic, byte[] data, CancellationToken token) { - lock (_lock) + using (await _lock.LockAsync(token)) { if (_bluetoothGatt == null || State != BluetoothLEDeviceState.Connected) { - return Task.FromResult(false); + return false; } var nativeCharacteristic = ((GattCharacteristic)characteristic).BluetoothGattCharacteristic; @@ -287,14 +307,14 @@ public Task WriteNoResponseAsync(IGattCharacteristic characteristic, byte[ #pragma warning disable CA1422 // Validate platform compatibility if (!(nativeCharacteristic?.SetValue(data) ?? false)) { - return Task.FromResult(false); + return false; } #pragma warning restore CA1422 // Validate platform compatibility #pragma warning disable CA1422 // Validate platform compatibility var result = _bluetoothGatt?.WriteCharacteristic(nativeCharacteristic) ?? false; #pragma warning restore CA1422 // Validate platform compatibility - return Task.FromResult(result); + return result; } } @@ -306,7 +326,7 @@ public override void OnConnectionStateChange(BluetoothGatt? gatt, [GeneratedEnum break; case ProfileState.Connected: - lock (_lock) + using (_lock.Lock()) { if (State == BluetoothLEDeviceState.Connecting && status == GattStatus.Success) { @@ -314,7 +334,7 @@ public override void OnConnectionStateChange(BluetoothGatt? gatt, [GeneratedEnum Task.Run(async () => { await Task.Delay(750); - lock (_lock) + using (await _lock.LockAsync()) { if (State == BluetoothLEDeviceState.Discovering && _bluetoothGatt != null) { @@ -340,7 +360,7 @@ public override void OnConnectionStateChange(BluetoothGatt? gatt, [GeneratedEnum break; case ProfileState.Disconnected: - lock (_lock) + using (_lock.Lock()) { switch (State) { @@ -372,7 +392,7 @@ public override void OnConnectionStateChange(BluetoothGatt? gatt, [GeneratedEnum public override void OnServicesDiscovered(BluetoothGatt? gatt, [GeneratedEnum] GattStatus status) { - lock (_lock) + using (_lock.Lock()) { if (status == GattStatus.Success && State == BluetoothLEDeviceState.Discovering) { @@ -407,7 +427,7 @@ public override void OnServicesDiscovered(BluetoothGatt? gatt, [GeneratedEnum] G public override void OnCharacteristicRead(BluetoothGatt? gatt, BluetoothGattCharacteristic? characteristic, [GeneratedEnum] GattStatus status) { - lock (_lock) + using (_lock.Lock()) { #pragma warning disable CA1422 // Validate platform compatibility _readCompletionSource?.TrySetResult(characteristic?.GetValue()); @@ -417,7 +437,7 @@ public override void OnCharacteristicRead(BluetoothGatt? gatt, BluetoothGattChar public override void OnCharacteristicWrite(BluetoothGatt? gatt, BluetoothGattCharacteristic? characteristic, [GeneratedEnum] GattStatus status) { - lock (_lock) + using (_lock.Lock()) { _writeCompletionSource?.TrySetResult(status == GattStatus.Success); } @@ -425,7 +445,7 @@ public override void OnCharacteristicWrite(BluetoothGatt? gatt, BluetoothGattCha public override void OnDescriptorWrite(BluetoothGatt? gatt, BluetoothGattDescriptor? descriptor, [GeneratedEnum] GattStatus status) { - lock (_lock) + using (_lock.Lock()) { _descriptorWriteCompletionSource?.TrySetResult(status == GattStatus.Success); } @@ -433,7 +453,7 @@ public override void OnDescriptorWrite(BluetoothGatt? gatt, BluetoothGattDescrip public override void OnCharacteristicChanged(BluetoothGatt? gatt, BluetoothGattCharacteristic? characteristic) { - lock (_lock) + using (_lock.Lock()) { var guid = characteristic?.Uuid?.ToGuid(); #pragma warning disable CA1422 // Validate platform compatibility diff --git a/BrickController2/BrickController2/Helpers/AsyncLock.cs b/BrickController2/BrickController2/Helpers/AsyncLock.cs index 14429f6d..b9ed7c8a 100644 --- a/BrickController2/BrickController2/Helpers/AsyncLock.cs +++ b/BrickController2/BrickController2/Helpers/AsyncLock.cs @@ -19,6 +19,13 @@ public async Task LockAsync(CancellationToken token) return new Releaser(_semaphore); } + + public IDisposable Lock(CancellationToken token = default) + { + _semaphore.Wait(token); + return new Releaser(_semaphore); + } + private struct Releaser : IDisposable { private SemaphoreSlim? _semaphore; From 684926793bbac87693b702c4017210722b9691d9 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Wed, 31 Dec 2025 09:56:29 +0100 Subject: [PATCH 2/5] Stop using auto connect for Android --- .../PlatformServices/BluetoothLE/BluetoothLEDevice.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs index d5b2832a..5f07273b 100644 --- a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs +++ b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs @@ -78,11 +78,11 @@ public BluetoothLEDevice(Context context, BluetoothAdapter bluetoothAdapter, str if (Build.VERSION.SdkInt >= BuildVersionCodes.M) { - _bluetoothGatt = _bluetoothDevice.ConnectGatt(_context, autoConnect, this, BluetoothTransports.Le); + _bluetoothGatt = _bluetoothDevice.ConnectGatt(_context, autoConnect: false, this, BluetoothTransports.Le); } else { - _bluetoothGatt = _bluetoothDevice.ConnectGatt(_context, autoConnect, this); + _bluetoothGatt = _bluetoothDevice.ConnectGatt(_context, autoConnect: false, this); } if (_bluetoothGatt is null) From 06916f47aa9f62bcb4ede920036cd2816343ecb5 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Wed, 31 Dec 2025 09:56:59 +0100 Subject: [PATCH 3/5] Try to apply delay before starting notification enablement --- .../DeviceManagement/TechnicMoveDevice.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/BrickController2/BrickController2/DeviceManagement/TechnicMoveDevice.cs b/BrickController2/BrickController2/DeviceManagement/TechnicMoveDevice.cs index bd3e28b6..b6195f37 100644 --- a/BrickController2/BrickController2/DeviceManagement/TechnicMoveDevice.cs +++ b/BrickController2/BrickController2/DeviceManagement/TechnicMoveDevice.cs @@ -133,6 +133,20 @@ protected override byte[] GetServoCommand(int channel, int servoValue, int servo return base.GetServoCommand(channel, servoValue, servoSpeed); } + protected override async Task ValidateServicesAsync(IEnumerable? services, CancellationToken token) + { + // better handle this type of device + if (services?.Any(s => s.Uuid == ServiceUuid && s.Characteristics.Any(c => c.Uuid == CharacteristicUuid)) == true) + { + // give some additional wait time for the device to be ready + await Task.Delay(TimeSpan.FromSeconds(1), token); + + return await base.ValidateServicesAsync(services, token); + } + + return false; + } + protected override async Task AfterConnectSetupAsync(bool requestDeviceInformation, CancellationToken token) { if (await base.AfterConnectSetupAsync(requestDeviceInformation, token)) From 686b136fc333ad1ac035c70351576ab06ce5dcd0 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Thu, 1 Jan 2026 01:12:26 +0100 Subject: [PATCH 4/5] Update BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../PlatformServices/BluetoothLE/BluetoothLEDevice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs index 5f07273b..67f1fc32 100644 --- a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs +++ b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs @@ -138,7 +138,7 @@ public async Task EnableNotificationAsync(IGattCharacteristic characterist { using (token.Register(() => { - using (_lock.Lock(token)) + using (_lock.Lock()) { _descriptorWriteCompletionSource?.TrySetResult(false); } From 43f3c674ec9e73a5c7cf82720beeffd4f1d70572 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Fri, 23 Jan 2026 18:46:10 +0100 Subject: [PATCH 5/5] try reset refs --- .../BluetoothLE/BluetoothLEService.cs | 2 +- .../DeviceManagement/BluetoothDevice.cs | 14 ++++++++++++-- .../DeviceManagement/ControlPlusDevice.cs | 8 ++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEService.cs b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEService.cs index 39929c6d..ae7e5225 100644 --- a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEService.cs +++ b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEService.cs @@ -73,7 +73,7 @@ private async Task ScanAsync(Action ConnectAsync( bool requestDeviceInformation, CancellationToken token) { - using (await _asyncLock.LockAsync()) + using (await _asyncLock.LockAsync(token)) { if (_bleDevice != null || DeviceState != DeviceState.Disconnected) { @@ -110,15 +110,23 @@ protected virtual Task AfterConnectSetupAsync(bool requestDeviceInformatio return Task.FromResult(true); } + protected virtual void OnDeviceDisconnecting() + { + } + private async Task DisconnectInternalAsync() { if (_bleDevice != null) { await StopOutputTaskAsync(); DeviceState = DeviceState.Disconnecting; + // notify disconnection + OnDeviceDisconnecting(); + // execute native device disconnection + cleanup await _bleDevice.DisconnectAsync(); _bleDevice = null; } + _onDeviceDisconnected = null; DeviceState = DeviceState.Disconnected; } @@ -129,8 +137,10 @@ private void OnDeviceDisconnected(IBluetoothLEDevice bluetoothLEDevice) { using (await _asyncLock.LockAsync()) { + var disconnectedCallback = _onDeviceDisconnected; await DisconnectInternalAsync(); - _onDeviceDisconnected?.Invoke(this); + // notify + disconnectedCallback?.Invoke(this); } }); } diff --git a/BrickController2/BrickController2/DeviceManagement/ControlPlusDevice.cs b/BrickController2/BrickController2/DeviceManagement/ControlPlusDevice.cs index 99e2e035..827ce6eb 100644 --- a/BrickController2/BrickController2/DeviceManagement/ControlPlusDevice.cs +++ b/BrickController2/BrickController2/DeviceManagement/ControlPlusDevice.cs @@ -233,6 +233,14 @@ protected virtual byte[] GetServoCommand(int channel, int servoValue, int servoS return _servoSendBuffer; } + protected override void OnDeviceDisconnecting() + { + base.OnDeviceDisconnecting(); + + // reset any stored characteristic reference + _characteristic = null; + } + protected override void OnCharacteristicChanged(Guid characteristicGuid, byte[] data) { if (characteristicGuid != CharacteristicUuid || data.Length < 4)