Mac: implement BLE discovery

This commit is contained in:
Christopher Willis-Ford 2022-05-13 19:44:34 -07:00
parent 3a4cd5c964
commit 2d99bafec1
4 changed files with 191 additions and 12 deletions

View file

@ -7,6 +7,7 @@ namespace ScratchLink;
using ScratchLink.JsonRpc;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
/// <summary>
@ -376,4 +377,28 @@ internal abstract class BLESession<TUUID> : Session
return maskedPrefix.SequenceEqual(this.DataPrefix);
}
}
/// <summary>
/// JSON-ready class to use when reporting that a peripheral was discovered.
/// </summary>
protected class BLEPeripheralDiscovered
{
/// <summary>
/// Gets or sets the advertised name of the peripheral.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
/// <summary>
/// Gets or sets the ID which can be used for connecting to this peripheral.
/// </summary>
[JsonPropertyName("peripheralId")]
public string PeripheralId { get; set; }
/// <summary>
/// Gets or sets the relative signal strength of the advertisement.
/// </summary>
[JsonPropertyName("rssi")]
public int RSSI { get; set; }
}
}

View file

@ -30,6 +30,8 @@ internal class MacBLESession : BLESession<CBUUID>
private readonly CBCentralManager cbManager;
private readonly Dictionary<NSUuid, CBPeripheral> discoveredPeripherals = new ();
private readonly SemaphoreSlim filterLock = new (1);
private List<BLEScanFilter> filters;
private HashSet<CBUUID> allowedServices;
@ -45,9 +47,8 @@ internal class MacBLESession : BLESession<CBUUID>
: base(context)
{
this.cbManager = new ();
this.cbManager.UpdatedState += this.CbManager_UpdatedState;
this.cbManager.DiscoveredPeripheral += this.CbManager_DiscoveredPeripheral;
this.cbManager.ConnectionEventDidOccur += this.CbManager_ConnectionEventDidOccur;
this.cbManager.UpdatedState += this.WrapEventHandler(this.CbManager_UpdatedState);
this.cbManager.DiscoveredPeripheral += this.WrapEventHandler<CBDiscoveredPeripheralEventArgs>(this.CbManager_DiscoveredPeripheral);
this.CancellationToken.Register(() =>
{
@ -86,7 +87,7 @@ internal class MacBLESession : BLESession<CBUUID>
protected override async Task<object> DoDiscover(List<BLEScanFilter> filters, HashSet<CBUUID> optionalServices)
{
var allowedServices = filters.Aggregate(
optionalServices.ToHashSet(), // start with a clone of the optional services list
optionalServices.OrEmpty().ToHashSet(), // start with a clone of the optional services list
(result, filter) =>
{
result.UnionWith(filter.RequiredServices);
@ -215,7 +216,7 @@ internal class MacBLESession : BLESession<CBUUID>
}
}
private async void CbManager_DiscoveredPeripheral(object sender, CBDiscoveredPeripheralEventArgs e)
private async Task CbManager_DiscoveredPeripheral(object sender, CBDiscoveredPeripheralEventArgs e)
{
var rssi = e.RSSI;
if (rssi.CompareTo(MinimumSignalStrength) < 0)
@ -233,9 +234,14 @@ internal class MacBLESession : BLESession<CBUUID>
var advertisementData = e.AdvertisementData;
var advertisedServices = advertisementData["kCBAdvDataServiceUUIDs"] as NSArray<CBUUID>;
var peripheralServices = peripheral.Services.Select(service => service.UUID);
var allServices = advertisedServices.Union(peripheralServices);
var allServices = new HashSet<CBUUID>();
allServices.UnionWith(peripheral.Services.OrEmpty().Select(service => service.UUID));
if (advertisementData.TryGetValue<NSArray>(CBAdvertisement.DataServiceUUIDsKey, out var advertisedServices))
{
allServices.UnionWith(advertisedServices.EnumerateAs<CBUUID>());
}
var manufacturerData = new Dictionary<int, IEnumerable<byte>>();
if (advertisementData[CBAdvertisement.DataManufacturerDataKey] is NSData advertisedManufacturerData)
@ -261,10 +267,17 @@ internal class MacBLESession : BLESession<CBUUID>
{
this.filterLock.Release();
}
}
private void CbManager_ConnectionEventDidOccur(object sender, CBPeripheralConnectionEventEventArgs e)
{
throw new NotImplementedException();
// the device must have passed the filter!
this.discoveredPeripherals[peripheral.Identifier] = peripheral;
await this.SendNotification(
"didDiscoverPeripheral",
new BLEPeripheralDiscovered()
{
Name = peripheral.Name,
PeripheralId = peripheral.Identifier.ToString(),
RSSI = rssi.Int32Value,
},
this.CancellationToken);
}
}

View file

@ -0,0 +1,47 @@
// <copyright file="NSExtensions.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink.Platforms.MacCatalyst;
using Foundation;
using ObjCRuntime;
/// <summary>
/// Extensions for NS data types.
/// </summary>
public static class NSExtensions
{
/// <summary>
/// Attempt to retrieve a value of a specific type from the dictionary.
/// The value will be cast using <c>(<typeparamref name="T"/>)<paramref name="value"/></c> which may throw.
/// </summary>
/// <typeparam name="T">The type of value to retrieve.</typeparam>
/// <param name="dict">The dictionary from which to retrieve a value.</param>
/// <param name="key">The key to look up within the dictionary.</param>
/// <param name="value">If successful, the value will be placed here.</param>
/// <returns>True if successful, false otherwise.</returns>
public static bool TryGetValue<T>(this NSDictionary dict, NSObject key, out T value)
where T : NSObject
{
var returnValue = dict.TryGetValue(key, out var baseValue);
value = (T)baseValue;
return returnValue;
}
/// <summary>
/// Enumerate the array, assuming that each item is of the specified type.
/// Items will be retrieved using <see cref="NSArray.GetItem{T}(nuint)"/>.
/// </summary>
/// <typeparam name="T">The type of the items in the array.</typeparam>
/// <param name="array">The array to enumerate.</param>
/// <returns>An enumerable for the items in the array.</returns>
public static IEnumerable<T> EnumerateAs<T>(this NSArray array)
where T : class, INativeObject
{
for (nuint i = 0; i < array.Count; ++i)
{
yield return array.GetItem<T>(i);
}
}
}

View file

@ -108,6 +108,93 @@ internal class Session : IDisposable
this.cancellationTokenSource.Dispose();
}
/// <summary>
/// In DEBUG builds, wrap an event handler with a try/catch which reports the exception using SendErrorNotifiation.
/// Otherwise, return the original event handler without modification.
/// </summary>
/// <remarks>
/// This could theoretically leak sensitive information. Use only for debugging.
/// </remarks>
/// <param name="original">The original event handler, to be wrapped.</param>
/// <returns>The wrapped event handler.</returns>
protected EventHandler WrapEventHandler(EventHandler original)
{
#if DEBUG
return (object sender, EventArgs args) =>
{
try
{
original(sender, args);
}
catch (Exception e)
{
this.SendEventExceptionNotification(e);
}
};
#else
return original;
#endif
}
/// <summary>
/// In DEBUG builds, wrap an event handler with a try/catch which reports the exception using SendErrorNotifiation.
/// Otherwise, return the original event handler without modification.
/// </summary>
/// <remarks>
/// This could theoretically leak sensitive information. Use only for debugging.
/// </remarks>
/// <typeparam name="T">The type of event args associated with the event handler.</typeparam>
/// <param name="original">The original event handler, to be wrapped.</param>
/// <returns>The wrapped event handler.</returns>
protected EventHandler<T> WrapEventHandler<T>(EventHandler<T> original)
{
#if DEBUG
return (object sender, T args) =>
{
try
{
original(sender, args);
}
catch (Exception e)
{
this.SendEventExceptionNotification(e);
}
};
#else
return original;
#endif
}
/// <summary>
/// In DEBUG builds, wrap an event handler with a try/catch which reports the exception using SendErrorNotifiation.
/// Otherwise, return the original event handler without modification.
/// This version wraps an async handler. Make sure any async handler returns <see cref="Task"/>.
/// </summary>
/// <remarks>
/// This could theoretically leak sensitive information. Use only for debugging.
/// </remarks>
/// <typeparam name="T">The type of event args associated with the event handler.</typeparam>
/// <param name="original">The original event handler, to be wrapped.</param>
/// <returns>The wrapped event handler.</returns>
protected EventHandler<T> WrapEventHandler<T>(Func<object, T, Task> original)
{
#if DEBUG
return async (object sender, T args) =>
{
try
{
await original(sender, args);
}
catch (Exception e)
{
this.SendEventExceptionNotification(e);
}
};
#else
return (object sender, T args) => { original(sender, args); };
#endif
}
/// <summary>
/// Handle a "getVersion" request.
/// </summary>
@ -235,6 +322,13 @@ internal class Session : IDisposable
}
}
#if DEBUG
private void SendEventExceptionNotification(Exception e)
{
_ = this.SendErrorNotification(JsonRpc2Error.InternalError(e.ToString()), this.CancellationToken);
}
#endif
private Task<object> HandleUnrecognizedMethod(string methodName, JsonElement? args)
{
throw new JsonRpc2Exception(JsonRpc2Error.MethodNotFound(methodName));