// <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(); } }