Skip to content

Commit

Permalink
Add auto reconnection
Browse files Browse the repository at this point in the history
  • Loading branch information
ChadNedzlek committed Feb 10, 2024
1 parent 734395b commit 72b4728
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 44 deletions.
67 changes: 67 additions & 0 deletions VaettirNet.PixelsDice.Net/AsyncAutoResetEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace VaettirNet.PixelsDice.Net;

internal sealed class AsyncAutoResetEvent
{
private readonly Queue<TaskCompletionSource> _queue = new();

private bool _signaled;

public AsyncAutoResetEvent(bool signaled)
{
_signaled = signaled;
}

public Task WaitAsync(CancellationToken cancellationToken = default)
{
lock (_queue)
{
if (_signaled)
{
_signaled = false;
return Task.CompletedTask;
}
else
{
var tcs = new TaskCompletionSource();
if (cancellationToken.CanBeCanceled)
{
// If the token is cancelled, cancel the waiter.
var registration = cancellationToken.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false);

// If the waiter completes or faults, unregister our interest in cancellation.
tcs.Task.ContinueWith(
_ => registration.Unregister(),
cancellationToken,
TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.NotOnFaulted,
TaskScheduler.Default);
}
_queue.Enqueue(tcs);
return tcs.Task;
}
}
}

public void Set()
{
TaskCompletionSource toRelease = null;

lock (_queue)
{
if (_queue.Count > 0)
{
toRelease = _queue.Dequeue();
}
else if (!_signaled)
{
_signaled = true;
}
}

// It's possible that the TCS has already been cancelled.
toRelease?.TrySetResult();
}
}
202 changes: 161 additions & 41 deletions VaettirNet.PixelsDice.Net/Ble/BlePeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,42 +15,43 @@ internal sealed class BlePeripheral : IDisposable, IAsyncDisposable
private readonly SafePeripheralHandle _handle;
private readonly Dispatcher _dispatcher;

private BlePeripheral(SafePeripheralHandle handle, string id, string address, Dispatcher dispatcher)
{
Id = id;
Address = address;
_handle = handle;
_dispatcher = dispatcher;
}
private NotifyCallback _notifyCallback;
private OnNotifyCallback _receiveCallback;

public void Dispose()
{
DisposeAsync().GetAwaiter().GetResult();
}
private readonly object _reconnectLock = new();
private ConnectionCallback _disconnectedCallback;
private ConnectionCallback _connectedCallback;
private Task _reconnectTask;
private CancellationTokenSource _reconnectCancellation;

public async ValueTask DisposeAsync()
{
if (Interlocked.Exchange(ref _notifyCallback, null) != null)
{
await DisconnectAsync();
}
_handle.Dispose();
}
public ConnectionState ConnectionState;
private AsyncAutoResetEvent _disconnectedEvent;

private NotifyCallback _notifyCallback;
private GCHandle _callbackHandle;
private OnNotifyCallback _receiveCallback;
public bool IsConnected => ConnectionState == ConnectionState.Connected;

public bool IsConnected => _notifyCallback != null;
public event Action<BlePeripheral, ConnectionState> ConnectionStateChanged;

public Task ConnectAsync(OnNotifyCallback receiveCallback)
{
if (_notifyCallback != null)
throw new InvalidOperationException();


if (_disconnectedCallback == null)
{
lock (_reconnectLock)
{
if (_disconnectedCallback == null)
{
_disconnectedCallback = OnDisconnected;
_connectedCallback = OnConnected;
NativeMethods.OnDisconnected(_handle, _disconnectedCallback, IntPtr.Zero).CheckSuccess();
NativeMethods.OnConnected(_handle, _connectedCallback, IntPtr.Zero).CheckSuccess();
}
}
}

_notifyCallback = OnNotify;
_receiveCallback = receiveCallback;
_callbackHandle = GCHandle.Alloc(_notifyCallback);

return _dispatcher.Execute(Dispatched);

Expand All @@ -63,6 +64,76 @@ void Dispatched()
_notifyCallback,
IntPtr.Zero)
.CheckSuccess();
ConnectionState = ConnectionState.Connected;
}
}

private void SetConnectionState(ConnectionState connectionState)
{
ConnectionState = connectionState;
ConnectionStateChanged?.Invoke(this, connectionState);
}

private void OnConnected(IntPtr peripheral, IntPtr userdata)
{
SetConnectionState(ConnectionState.Connected);
}

private void OnDisconnected(IntPtr peripheral, IntPtr userdata)
{
Logger.Instance.Log(PixelsLogLevel.Info, "Connection to device lost... reconnecting...");
SetConnectionState(ConnectionState.Reconnecting);
lock (_reconnectLock)
{
if (_reconnectTask == null)
{
_reconnectCancellation = new CancellationTokenSource();
_disconnectedEvent = new AsyncAutoResetEvent(false);
_reconnectTask = Task.Factory.StartNew(() => ReconnectCallback(_reconnectCancellation.Token),
_reconnectCancellation.Token);
}
else
{
_disconnectedEvent.Set();
}
}
}

private async Task ReconnectCallback(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var result = NativeMethods.IsConnectable(_handle, out var connectable);

if (result == CallResult.Failure || connectable == false)
{
// Die is not connectable, just wait for a bit
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
continue;
}

result = await _dispatcher.Execute(() => NativeMethods.ConnectPeripheral(_handle));
switch (result)
{
case CallResult.Success:
// We are reconnected, wait until we get disconnected again
result = NativeMethods.IsConnected(_handle, out var connected);
if (result == CallResult.Failure || connected == false)
{
// We didn't really connect, try again
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
continue;
}

Logger.Instance.Log(PixelsLogLevel.Info, "Device reconnected");

await _disconnectedEvent.WaitAsync(cancellationToken);
break;
case CallResult.Failure:
// That didn't work, just chill for a bit
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
break;
}
}
}

Expand All @@ -78,27 +149,51 @@ private void OnNotify(BleUuid service, BleUuid characteristic, IntPtr data, UInt

public void SendMessage<T>(T data) where T : struct
{
var buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref data, 1));
Span<byte> buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref data, 1));
NativeMethods.WriteCommand(
_handle,
PixelsId.PixelsServiceUuid,
PixelsId.WriteCharacteristicUuid,
ref buffer[0],
(UIntPtr)buffer.Length
).CheckSuccess();
_handle,
PixelsId.PixelsServiceUuid,
PixelsId.WriteCharacteristicUuid,
ref buffer[0],
(UIntPtr)buffer.Length
)
.CheckSuccess();
}

private async Task StopReconnectionAsync()
{
if (_disconnectedCallback == null)
return;

Task runningTask;

lock (_reconnectLock)
{
if (_disconnectedCallback == null)
return;

runningTask = Interlocked.Exchange(ref _reconnectTask, null);
_reconnectCancellation.Cancel();
}

if (runningTask != null)
{
await _reconnectTask.IgnoreCancellation(_reconnectCancellation.Token);
}
}

public async Task DisconnectAsync()
{
await StopReconnectionAsync();
await _dispatcher.Execute(() => { NativeMethods.DisconnectPeripheral(_handle).CheckSuccess(); }).ConfigureAwait(false);
_notifyCallback = null;
_callbackHandle.Free();
ConnectionState = ConnectionState.Disconnected;
}

public static BlePeripheral Create(SafePeripheralHandle handle, Dispatcher dispatcher)
{
using var id = NativeMethods.GetPeripheralIdentifier(handle);
using var addy = NativeMethods.GetPeripheralAddress(handle);
using StringHandle id = NativeMethods.GetPeripheralIdentifier(handle);
using StringHandle addy = NativeMethods.GetPeripheralAddress(handle);
return new BlePeripheral(handle, id.Value, addy.Value, dispatcher);
}

Expand All @@ -109,15 +204,15 @@ public string GetPersistentId()

public static string GetPersistentId(SafePeripheralHandle bleHandle)
{
using var id = NativeMethods.GetPeripheralIdentifier(bleHandle);
using var addr = NativeMethods.GetPeripheralAddress(bleHandle);
using StringHandle id = NativeMethods.GetPeripheralIdentifier(bleHandle);
using StringHandle addr = NativeMethods.GetPeripheralAddress(bleHandle);
return GetPersistentId(id.Value, addr.Value);
}

private static string GetPersistentId(string id, string address)
{
using MemoryStream stream = new MemoryStream();
using (BinaryWriter writer = new BinaryWriter(stream, Encoding.ASCII, true))
using var stream = new MemoryStream();
using (var writer = new BinaryWriter(stream, Encoding.ASCII, true))
{
writer.Write(id);
writer.Write(address);
Expand All @@ -128,11 +223,11 @@ private static string GetPersistentId(string id, string address)

public byte[][] GetManufacturerData()
{
var cnt = NativeMethods.GetManufacturerDataCount(_handle);
nuint cnt = NativeMethods.GetManufacturerDataCount(_handle);
if (cnt == 0)
return Array.Empty<byte[]>();
var ret = new byte[cnt][];
BleManufacturerData data = new BleManufacturerData();
var data = new BleManufacturerData();
for (nuint i = 0; i < cnt; i++)
{
NativeMethods.GetManufacturerData(_handle, i, ref data);
Expand All @@ -142,4 +237,29 @@ public byte[][] GetManufacturerData()

return ret;
}

private BlePeripheral(SafePeripheralHandle handle, string id, string address, Dispatcher dispatcher)
{
Id = id;
Address = address;
_handle = handle;
_dispatcher = dispatcher;
}

public void Dispose()
{
DisposeAsync().AsTask().GetAwaiter().GetResult();
}

public async ValueTask DisposeAsync()
{
await StopReconnectionAsync();

if (Interlocked.Exchange(ref _notifyCallback, null) != null)
{
await DisconnectAsync();
}

_handle.Dispose();
}
}
8 changes: 8 additions & 0 deletions VaettirNet.PixelsDice.Net/ConnectionState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace VaettirNet.PixelsDice.Net;

public enum ConnectionState
{
Disconnected,
Connected,
Reconnecting,
}
12 changes: 12 additions & 0 deletions VaettirNet.PixelsDice.Net/Interop/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ internal static partial class NativeMethods

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_adapter_set_callback_on_scan_found")]
internal static partial CallResult OnScanFound(SafeAdapterHandle adapter, ScanCallback callback, IntPtr data);

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_peripheral_set_callback_on_connected")]
internal static partial CallResult OnConnected(SafePeripheralHandle peripheral, ConnectionCallback callback, IntPtr data);

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_peripheral_set_callback_on_disconnected")]
internal static partial CallResult OnDisconnected(SafePeripheralHandle peripheral, ConnectionCallback callback, IntPtr data);

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_peripheral_services_count")]
internal static partial nuint GetServiceCount(SafePeripheralHandle peripheral);
Expand Down Expand Up @@ -95,4 +101,10 @@ internal static extern CallResult Unsubscribe(SafePeripheralHandle peripheral,

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_logging_set_callback")]
internal static partial void SetLogCallback(LogCallback callback);

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_peripheral_is_connected")]
internal static partial CallResult IsConnected(SafePeripheralHandle peripheral, [MarshalAs(UnmanagedType.Bool)] out bool connected);

[LibraryImport(SimpleBleLibraryName, EntryPoint = "simpleble_peripheral_is_connectable")]
internal static partial CallResult IsConnectable(SafePeripheralHandle peripheral, [MarshalAs(UnmanagedType.Bool)] out bool connected);
}
4 changes: 3 additions & 1 deletion VaettirNet.PixelsDice.Net/Interop/ScanCallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

namespace VaettirNet.PixelsDice.Net.Interop;

internal delegate void ScanCallback(IntPtr adapter, IntPtr peripheral, IntPtr data);
internal delegate void ScanCallback(IntPtr adapter, IntPtr peripheral, IntPtr data);

internal delegate void ConnectionCallback(IntPtr peripheral, IntPtr userData);
Loading

0 comments on commit 72b4728

Please sign in to comment.