mirror of
https://github.com/scratchfoundation/scratch-link.git
synced 2025-08-28 22:39:42 -04:00
share BTSession's address privacy implementation with BLESession
This commit is contained in:
parent
0b0b8802a8
commit
7ec18de421
8 changed files with 152 additions and 119 deletions
|
@ -10,7 +10,9 @@ using System.Linq;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using CoreBluetooth;
|
||||
using Fleck;
|
||||
using Foundation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ScratchLink.Extensions;
|
||||
using ScratchLink.JsonRpc;
|
||||
|
@ -18,12 +20,15 @@ using ScratchLink.JsonRpc;
|
|||
/// <summary>
|
||||
/// Implements the cross-platform portions of a BLE session.
|
||||
/// </summary>
|
||||
/// <typeparam name="TUUID">The platform-specific type which represents UUIDs (like Guid or CBUUID).</typeparam>
|
||||
internal abstract class BLESession<TUUID> : Session
|
||||
/// <typeparam name="TPeripheral">The platform-specific type for a BLE peripheral device.</typeparam>
|
||||
/// <typeparam name="TPeripheralAddress">The platform-specific type for a BLE peripheral device's address.</typeparam>
|
||||
/// <typeparam name="TUUID">The platform-specific type which represents BLE UUIDs (like Guid or CBUUID).</typeparam>
|
||||
internal abstract class BLESession<TPeripheral, TPeripheralAddress, TUUID> : PeripheralSession<TPeripheral, TPeripheralAddress>
|
||||
where TPeripheral : class
|
||||
where TUUID : IEquatable<TUUID>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BLESession{TUUID}"/> class.
|
||||
/// Initializes a new instance of the <see cref="BLESession{TDevice, TPeripheralAddress, TUUID}"/> class.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="Session.Session(IWebSocketConnection)"/>
|
||||
public BLESession(IWebSocketConnection webSocket)
|
||||
|
@ -32,7 +37,6 @@ internal abstract class BLESession<TUUID> : Session
|
|||
this.GattHelpers = ScratchLinkApp.Current.Services.GetService<GattHelpers<TUUID>>();
|
||||
this.AllowedServices = new ();
|
||||
this.Handlers["discover"] = this.HandleDiscover;
|
||||
this.Handlers["connect"] = this.HandleConnect;
|
||||
this.Handlers["write"] = this.HandleWrite;
|
||||
this.Handlers["read"] = this.HandleRead;
|
||||
this.Handlers["startNotifications"] = this.HandleStartNotifications;
|
||||
|
@ -147,6 +151,7 @@ internal abstract class BLESession<TUUID> : Session
|
|||
this.AllowedServices.UnionWith(filter.RequiredServices.OrEmpty());
|
||||
}
|
||||
|
||||
this.ClearPeripherals();
|
||||
return await this.DoDiscover(filters);
|
||||
}
|
||||
|
||||
|
@ -158,31 +163,26 @@ internal abstract class BLESession<TUUID> : Session
|
|||
protected abstract Task<object> DoDiscover(List<BLEScanFilter> filters);
|
||||
|
||||
/// <summary>
|
||||
/// Implement the JSON-RPC "connect" request to connect to a particular peripheral.
|
||||
/// Valid in the discovery state; transitions to connected state on success.
|
||||
/// Track a discovered peripheral device and report it to the client.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The name of the method being called ("connect").</param>
|
||||
/// <param name="args">
|
||||
/// A JSON object containing the UUID of a peripheral found by the most recent discovery request.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected async Task<object> HandleConnect(string methodName, JsonElement? args)
|
||||
/// <param name="peripheral">The platform-specific device reference or record.</param>
|
||||
/// <param name="peripheralAddress">The internal system address of this device.</param>
|
||||
/// <param name="displayName">A user-friendly name, if possible.</param>
|
||||
/// <param name="rssi">A relative signal strength indicator.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
||||
protected async Task OnPeripheralDiscovered(TPeripheral peripheral, TPeripheralAddress peripheralAddress, string displayName, int rssi)
|
||||
{
|
||||
if (args?.TryGetProperty("peripheralId", out var jsonPeripheralId) != true)
|
||||
var peripheralId = this.RegisterPeripheral(peripheral, peripheralAddress);
|
||||
|
||||
var message = new BLEPeripheralDiscovered()
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("connect request must include peripheralId"));
|
||||
}
|
||||
|
||||
return await this.DoConnect(jsonPeripheralId);
|
||||
Name = displayName,
|
||||
PeripheralId = peripheralId,
|
||||
RSSI = rssi,
|
||||
};
|
||||
await this.SendNotification("didDiscoverPeripheral", message, this.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform-specific implementation for connecting to a peripheral device.
|
||||
/// </summary>
|
||||
/// <param name="jsonPeripheralId">A JSON element representing a platform-specific peripheral ID.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
|
||||
protected abstract Task<object> DoConnect(JsonElement jsonPeripheralId);
|
||||
|
||||
/// <summary>
|
||||
/// Implement the JSON-RPC "write" request to write a value to a particular service characteristic.
|
||||
/// </summary>
|
||||
|
|
|
@ -16,18 +16,15 @@ using ScratchLink.JsonRpc;
|
|||
/// <summary>
|
||||
/// Implements the cross-platform portions of a Bluetooth Classic (RFCOMM) session.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDevice">Platform-specific device reference. Used to make a device connection.</typeparam>
|
||||
/// <typeparam name="TDeviceId">Platform-specific device address. Used for tracking device discovery records.</typeparam>
|
||||
internal abstract class BTSession<TDevice, TDeviceId> : Session
|
||||
/// <inheritdoc cref="PeripheralSession{TPeripheral, TPeripheralAddress}"/>
|
||||
internal abstract class BTSession<TPeripheral, TPeripheralAddress> : PeripheralSession<TPeripheral, TPeripheralAddress>
|
||||
where TPeripheral : class
|
||||
{
|
||||
/// <summary>
|
||||
/// PIN code for auto-pairing.
|
||||
/// </summary>
|
||||
protected const string AutoPairingCode = "0000";
|
||||
|
||||
private readonly Dictionary<TDeviceId, string> deviceIdToString = new ();
|
||||
private readonly Dictionary<string, TDevice> availableDevices = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BTSession{TDevice, TDeviceId}"/> class.
|
||||
/// </summary>
|
||||
|
@ -59,7 +56,7 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("majorDeviceClass and minorDeviceClass required"));
|
||||
}
|
||||
|
||||
this.availableDevices.Clear();
|
||||
this.ClearPeripherals();
|
||||
return this.DoDiscover((byte)majorDeviceClass, (byte)minorDeviceClass);
|
||||
}
|
||||
|
||||
|
@ -71,39 +68,6 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected abstract Task<object> DoDiscover(byte majorDeviceClass, byte minorDeviceClass);
|
||||
|
||||
/// <summary>
|
||||
/// Implement the JSON-RPC "connect" request to connect to a particular peripheral.
|
||||
/// Valid in the discovery state; transitions to connected state on success.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The name of the method being called ("connect").</param>
|
||||
/// <param name="args">
|
||||
/// A JSON object containing the ID of a peripheral found by the most recent discovery request.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected Task<object> HandleConnect(string methodName, JsonElement? args)
|
||||
{
|
||||
var peripheralId = args?.TryGetProperty("peripheralId")?.GetString();
|
||||
|
||||
if (peripheralId == null)
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("peripheralId required"));
|
||||
}
|
||||
|
||||
if (!this.availableDevices.TryGetValue(peripheralId, out var device))
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidRequest(string.Format("Device {0} not available for connection", peripheralId)));
|
||||
}
|
||||
|
||||
return this.DoConnect(device);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform-specific implementation for connecting to a peripheral device.
|
||||
/// </summary>
|
||||
/// <param name="device">The requested device.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
|
||||
protected abstract Task<object> DoConnect(TDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Implement the JSON-RPC "send" request to send data to the connected peripheral.
|
||||
/// </summary>
|
||||
|
@ -148,15 +112,14 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
/// <summary>
|
||||
/// Track a discovered device and report it to the client.
|
||||
/// </summary>
|
||||
/// <param name="device">The platform-specific device reference or record.</param>
|
||||
/// <param name="deviceId">The internal system address of this device.</param>
|
||||
/// <param name="peripheral">The platform-specific device reference or record.</param>
|
||||
/// <param name="peripheralAddress">The internal system address of this device.</param>
|
||||
/// <param name="displayName">A user-friendly name, if possible.</param>
|
||||
/// <param name="rssi">A relative signal strength indicator.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
||||
protected async Task OnDeviceFound(TDevice device, TDeviceId deviceId, string displayName, int rssi)
|
||||
protected async Task OnPeripheralDiscovered(TPeripheral peripheral, TPeripheralAddress peripheralAddress, string displayName, int rssi)
|
||||
{
|
||||
var peripheralId = this.GetPeripheralId(deviceId);
|
||||
this.availableDevices[peripheralId] = device;
|
||||
var peripheralId = this.RegisterPeripheral(peripheral, peripheralAddress);
|
||||
|
||||
var message = new BTPeripheralDiscovered
|
||||
{
|
||||
|
@ -167,17 +130,6 @@ internal abstract class BTSession<TDevice, TDeviceId> : Session
|
|||
await this.SendRequest("didDiscoverPeripheral", message, this.CancellationToken);
|
||||
}
|
||||
|
||||
private string GetPeripheralId(TDeviceId deviceId)
|
||||
{
|
||||
if (!this.deviceIdToString.TryGetValue(deviceId, out var peripheralId))
|
||||
{
|
||||
peripheralId = Guid.NewGuid().ToString();
|
||||
this.deviceIdToString[deviceId] = peripheralId;
|
||||
}
|
||||
|
||||
return peripheralId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON-ready class to use when reporting that a peripheral was discovered.
|
||||
/// </summary>
|
||||
|
|
|
@ -9,7 +9,7 @@ using System.Text.Json.Serialization;
|
|||
/// <summary>
|
||||
/// Data class representing a JSON-RPC 2.0 Error object.
|
||||
/// </summary>
|
||||
internal class JsonRpc2Error
|
||||
public class JsonRpc2Error
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric error code for this error.
|
||||
|
|
111
scratch-link-common/PeripheralSession.cs
Normal file
111
scratch-link-common/PeripheralSession.cs
Normal file
|
@ -0,0 +1,111 @@
|
|||
// <copyright file="PeripheralSession.cs" company="Scratch Foundation">
|
||||
// Copyright (c) Scratch Foundation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace ScratchLink;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Fleck;
|
||||
using ScratchLink.Extensions;
|
||||
using ScratchLink.JsonRpc;
|
||||
|
||||
/// <summary>
|
||||
/// A kind of session which discovers and connects to peripheral devices by some sort of address.
|
||||
/// One session can search for, connect to, and interact with one peripheral device.
|
||||
/// Handles address privacy.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPeripheral">The type of peripheral device handled by this session.</typeparam>
|
||||
/// <typeparam name="TPeripheralAddress">The type of address (UUID, path, etc.) used by this session to identify a peripheral device.</typeparam>
|
||||
public abstract class PeripheralSession<TPeripheral, TPeripheralAddress> : Session
|
||||
where TPeripheral : class
|
||||
{
|
||||
private readonly Dictionary<TPeripheralAddress, string> peripheralAddressToId = new ();
|
||||
private readonly Dictionary<string, TPeripheral> discoveredPeripherals = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeripheralSession{TPeripheral, TPeripheralAddress}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The WebSocket which this session will use for communication.</param>
|
||||
public PeripheralSession(IWebSocketConnection webSocket)
|
||||
: base(webSocket)
|
||||
{
|
||||
this.Handlers["connect"] = this.HandleConnect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement the JSON-RPC "connect" request to connect to a particular peripheral device.
|
||||
/// Valid in the discovery state; transitions to connected state on success.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The name of the method being called ("connect").</param>
|
||||
/// <param name="args">
|
||||
/// A JSON object containing the ID of a peripheral found by the most recent discovery request.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected async Task<object> HandleConnect(string methodName, JsonElement? args)
|
||||
{
|
||||
var peripheralId = args?.TryGetProperty("peripheralId")?.GetString();
|
||||
|
||||
if (peripheralId == null)
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("connect request must include peripheralId"));
|
||||
}
|
||||
|
||||
var peripheral = this.GetPeripheral(peripheralId);
|
||||
|
||||
if (peripheral == null)
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidRequest(string.Format("peripheral {0} not available for connection", peripheralId)));
|
||||
}
|
||||
|
||||
return await this.DoConnect(peripheral);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform-specific implementation for connecting to a peripheral device.
|
||||
/// </summary>
|
||||
/// <param name="peripheral">The requested peripheral device.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
|
||||
protected abstract Task<object> DoConnect(TPeripheral peripheral);
|
||||
|
||||
/// <summary>
|
||||
/// Store the peripheral in the "discovered peripherals" list using a session-specific peripheral ID.
|
||||
/// Storing a peripheral with the same address several times during the same session will result in the same ID each time.
|
||||
/// </summary>
|
||||
/// <param name="peripheral">The peripheral being registered.</param>
|
||||
/// <param name="peripheralAddress">The peripheral device's address.</param>
|
||||
/// <returns>An anonymized, session-specific peripheral ID.</returns>
|
||||
protected string RegisterPeripheral(TPeripheral peripheral, TPeripheralAddress peripheralAddress)
|
||||
{
|
||||
if (!this.peripheralAddressToId.TryGetValue(peripheralAddress, out var peripheralId))
|
||||
{
|
||||
peripheralId = Guid.NewGuid().ToString();
|
||||
this.peripheralAddressToId[peripheralAddress] = peripheralId;
|
||||
}
|
||||
|
||||
this.discoveredPeripherals[peripheralId] = peripheral;
|
||||
|
||||
return peripheralId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a registered peripheral.
|
||||
/// </summary>
|
||||
/// <param name="peripheralId">The anonymized peripheral ID.</param>
|
||||
/// <returns>The peripheral if found, otherwise null.</returns>
|
||||
protected TPeripheral GetPeripheral(string peripheralId)
|
||||
{
|
||||
return this.discoveredPeripherals.GetValueOrDefault(peripheralId, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all registered peripherals.
|
||||
/// The mapping of peripheral address to ID will not be cleared. To clear that, start a new session.
|
||||
/// </summary>
|
||||
protected void ClearPeripherals()
|
||||
{
|
||||
this.discoveredPeripherals.Clear();
|
||||
}
|
||||
}
|
|
@ -26,9 +26,9 @@ using JsonRpcMethodHandler = System.Func<
|
|||
using RequestId = System.UInt32;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for Scratch Link sessions. One session can search for, connect to, and interact with one peripheral device.
|
||||
/// Base class for Scratch Link sessions.
|
||||
/// </summary>
|
||||
internal class Session : IDisposable
|
||||
public class Session : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the Scratch Link network protocol version. Note that this is not the application version.
|
||||
|
|
|
@ -29,5 +29,6 @@
|
|||
<Compile Include="$(MSBuildThisFileDirectory)SessionManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)WebSocketListener.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SemaphoreSlimExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)PeripheralSession.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -22,7 +22,7 @@ using ScratchLink.Mac.Extensions;
|
|||
/// <summary>
|
||||
/// Implements a BLE session on MacOS.
|
||||
/// </summary>
|
||||
internal class MacBLESession : BLESession<CBUUID>
|
||||
internal class MacBLESession : BLESession<CBPeripheral, NSUuid, CBUUID>
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum value for RSSI during discovery: peripherals with a weaker signal will be ignored.
|
||||
|
@ -142,25 +142,8 @@ internal class MacBLESession : BLESession<CBUUID>
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<object> DoConnect(JsonElement jsonPeripheralId)
|
||||
protected override async Task<object> DoConnect(CBPeripheral peripheral)
|
||||
{
|
||||
NSUuid peripheralId = null;
|
||||
|
||||
try
|
||||
{
|
||||
var peripheralIdString = jsonPeripheralId.GetString();
|
||||
peripheralId = new NSUuid(peripheralIdString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore any exceptions: just check below for a valid peripheralId
|
||||
}
|
||||
|
||||
if (peripheralId == null)
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("malformed peripheralId"));
|
||||
}
|
||||
|
||||
using (await this.filterLock.WaitDisposableAsync(this.CancellationToken))
|
||||
{
|
||||
if (this.connectedPeripheral != null)
|
||||
|
@ -168,13 +151,8 @@ internal class MacBLESession : BLESession<CBUUID>
|
|||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidRequest("already connected or connecting"));
|
||||
}
|
||||
|
||||
if (!this.discoveredPeripherals.TryGetValue(peripheralId, out var discoveredPeripheral))
|
||||
{
|
||||
throw new JsonRpc2Exception(JsonRpc2Error.InvalidParams("invalid peripheralId: " + peripheralId));
|
||||
}
|
||||
|
||||
this.cbManager.StopScan();
|
||||
this.connectedPeripheral = discoveredPeripheral;
|
||||
this.connectedPeripheral = peripheral;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
@ -417,16 +395,7 @@ internal class MacBLESession : BLESession<CBUUID>
|
|||
}
|
||||
|
||||
// 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);
|
||||
await this.OnPeripheralDiscovered(peripheral, peripheral.Identifier, peripheral.Name, rssi.Int32Value);
|
||||
}
|
||||
|
||||
private void CbManager_DisconnectedPeripheral(object sender, CBPeripheralErrorEventArgs e)
|
||||
|
|
|
@ -177,6 +177,6 @@ internal class MacBTSession : BTSession<BluetoothDevice, BluetoothDeviceAddress>
|
|||
return;
|
||||
}
|
||||
|
||||
await this.OnDeviceFound(e.Device, e.Device.Address, e.Device.NameOrAddress, e.Device.Rssi);
|
||||
await this.OnPeripheralDiscovered(e.Device, e.Device.Address, e.Device.NameOrAddress, e.Device.Rssi);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue