scratch-link/scratch-link-common/PeripheralSession.cs
2023-06-01 14:52:19 -07:00

124 lines
5.3 KiB
C#

// <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="TDiscoveredPeripheral">The type used to track discovered peripheral devices. Passed to <c>DoConnect</c>.</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<TDiscoveredPeripheral, TPeripheralAddress> : Session
where TDiscoveredPeripheral : class
{
private readonly Dictionary<TPeripheralAddress, string> peripheralAddressToId = new ();
private readonly Dictionary<string, TDiscoveredPeripheral> 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>
/// Gets a value indicating whether this session is connected to a peripheral device.
/// </summary>
protected abstract bool IsConnected { get; }
/// <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)
{
if (this.IsConnected)
{
throw JsonRpc2Error.InvalidRequest("cannot connect when already connected").ToException();
}
var peripheralId = args?.TryGetProperty("peripheralId")?.GetString();
if (peripheralId == null)
{
throw JsonRpc2Error.InvalidParams("connect request must include peripheralId").ToException();
}
var discoveredPeripheral = this.GetDiscoveredPeripheral(peripheralId);
if (discoveredPeripheral == null)
{
throw JsonRpc2Error.InvalidRequest(string.Format("peripheral {0} not available for connection", peripheralId)).ToException();
}
return await this.DoConnect(discoveredPeripheral, args);
}
/// <summary>
/// Platform-specific implementation for connecting to a peripheral device.
/// </summary>
/// <param name="discoveredPeripheral">The requested peripheral device.</param>
/// <param name="args">
/// A JSON object containing the args passed by the client, in case the platform-specific implementation needs them.
/// </param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
protected abstract Task<object> DoConnect(TDiscoveredPeripheral discoveredPeripheral, JsonElement? args);
/// <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="discoveredPeripheral">The peripheral information being registered.</param>
/// <param name="peripheralAddress">The peripheral device's address.</param>
/// <returns>An anonymized, session-specific peripheral ID.</returns>
protected string RegisterPeripheral(TDiscoveredPeripheral discoveredPeripheral, TPeripheralAddress peripheralAddress)
{
if (!this.peripheralAddressToId.TryGetValue(peripheralAddress, out var peripheralId))
{
peripheralId = Guid.NewGuid().ToString();
this.peripheralAddressToId[peripheralAddress] = peripheralId;
}
this.discoveredPeripherals[peripheralId] = discoveredPeripheral;
return peripheralId;
}
/// <summary>
/// Retrieve a peripheral registered during discovery.
/// </summary>
/// <param name="peripheralId">The anonymized peripheral ID.</param>
/// <returns>The peripheral if found, otherwise null.</returns>
protected TDiscoveredPeripheral GetDiscoveredPeripheral(string peripheralId)
{
return this.discoveredPeripherals.GetValueOrDefault(peripheralId, null);
}
/// <summary>
/// Clear all peripherals registered during discovery.
/// The mapping of peripheral address to ID will not be cleared. To clear that, start a new session.
/// </summary>
protected void ClearDiscoveredPeripherals()
{
this.discoveredPeripherals.Clear();
}
}