mirror of
https://github.com/scratchfoundation/scratch-link.git
synced 2025-08-20 18:39:51 -04:00
Mac: implement BLE discovery
This commit is contained in:
parent
3a4cd5c964
commit
2d99bafec1
4 changed files with 191 additions and 12 deletions
scratch-link
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
47
scratch-link/Platforms/MacCatalyst/NSExtensions.cs
Normal file
47
scratch-link/Platforms/MacCatalyst/NSExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue