/// <summary> /// Handle the client's request to write a value to a particular service characteristic. /// </summary> /// <param name="parameters"> /// The IDs of the service & characteristic along with the message and optionally the message encoding. /// </param> /// <returns>The number of decoded bytes written</returns> private async Task <JToken> Write(JObject parameters) { var buffer = EncodingHelpers.DecodeBuffer(parameters); var endpoint = await GetEndpoint("write request", parameters, GattHelpers.BlockListStatus.ExcludeWrites); Console.Write(endpoint.AttributeHandle); Console.Write(" "); foreach (byte bt in buffer) { Console.Write(bt + " "); } Console.WriteLine(""); var withResponse = (parameters["withResponse"]?.ToObject <bool>() ?? false) || !endpoint.CharacteristicProperties.HasFlag(GattCharacteristicProperties.WriteWithoutResponse); var result = await endpoint.WriteValueWithResultAsync(buffer.AsBuffer(), withResponse?GattWriteOption.WriteWithResponse : GattWriteOption.WriteWithoutResponse); switch (result.Status) { case GattCommunicationStatus.Success: return(buffer.Length); case GattCommunicationStatus.ProtocolError: throw JsonRpcException.ApplicationError($"Error while attempting to write: {result.Status} {result.ProtocolError}"); // "ProtocolError 3" default: throw JsonRpcException.ApplicationError($"Error while attempting to write: {result.Status}"); // "Unreachable" } }
private void Discover(JObject parameters) { var major = parameters["majorDeviceClass"]?.ToObject <BluetoothMajorClass>(); var minor = parameters["minorDeviceClass"]?.ToObject <BluetoothMinorClass>(); if (major == null || minor == null) { throw JsonRpcException.InvalidParams("majorDeviceClass and minorDeviceClass required"); } var deviceClass = BluetoothClassOfDevice.FromParts(major.Value, minor.Value, BluetoothServiceCapabilities.None); var selector = BluetoothDevice.GetDeviceSelectorFromClassOfDevice(deviceClass); try { _watcher = DeviceInformation.CreateWatcher(selector, new List <String> { SignalStrengthPropertyName, IsPresentPropertyName, BluetoothAddressPropertyName }); _watcher.Added += PeripheralDiscovered; _watcher.EnumerationCompleted += EnumerationCompleted; _watcher.Updated += PeripheralUpdated; _watcher.Stopped += EnumerationStopped; _watcher.Start(); } catch (ArgumentException) { throw JsonRpcException.ApplicationError("Failed to create device watcher"); } }
static SerialPort sp = null; //串口对象 protected override async Task DidReceiveCall(string method, JObject parameters, Func <JToken, JsonRpcException, Task> completion) { switch (method) { case "connect": await completion(await Connect(parameters), null); break; case "disconnect": await completion(await DisConnect(parameters), null); break; case "send": await completion(await SendMessage(parameters), null); break;; case "read": await completion(await ReadMessage(parameters), null); break; default: throw JsonRpcException.MethodNotFound(method); } }
/// <summary> /// Handle a client request /// </summary> /// <param name="method">The name of the method called by the client</param> /// <param name="parameters">The parameters passed by the client</param> /// <param name="completion">The completion handler to be called with the result</param> protected override async Task DidReceiveCall(string method, JObject parameters, Func <JToken, JsonRpcException, Task> completion) { switch (method) { case "discover": if (m_peripheralData != null) { SendRemoteRequest("didDiscoverPeripheral", m_peripheralData); } else { Discover(parameters); } await completion(null, null); break; case "connect": await Connect(parameters); await completion(null, null); break; case "write": await completion(await Write(parameters), null); break; case "read": await completion(await Read(parameters), null); break; case "startNotifications": await StartNotifications(parameters); await completion(null, null); break; case "stopNotifications": await StopNotifications(parameters); await completion(null, null); break; case "pingMe": await completion("willPing", null); SendRemoteRequest("ping", null, (result, error) => { Debug.Print($"Got result from ping: {result}"); return(Task.CompletedTask); }); break; default: throw JsonRpcException.MethodNotFound(method); } }
protected async Task SendErrorNotification(JsonRpcException error) { try { var message = MakeResponse(null, null, error); var messageText = JsonConvert.SerializeObject(message); _socketLock.Wait(); try { await _webSocket.Send(messageText); } finally { _socketLock.Release(); } } catch (Exception e) { // This probably means the WebSocket is closed, causing Send to throw. Since SendErrorNotification // is often called in response to an exception -- possibly one that caused the WebSocket to close -- // reporting this secondary exception won't help anyone and may itself cause a crash. Instead, print // this secondary exception and move on... Debug.Print($"Error serializing or sending error notification: {e}"); Debug.Print($"Original error notification was: {error}"); } }
private async Task <JToken> SendMessage(JObject parameters) { string key = ""; foreach (string item in s_writerMap.Keys) { key = item; } DataWriter _socketWriter = s_writerMap[key]; if (_socketWriter == null) { throw JsonRpcException.InvalidRequest("Not connected to peripheral"); } var data = EncodingHelpers.DecodeBuffer(parameters); try { _socketWriter.WriteBytes(data); await _socketWriter.StoreAsync(); } catch (ObjectDisposedException) { throw JsonRpcException.InvalidRequest("Not connected to peripheral"); } return(data.Length); }
protected override async Task DidReceiveCall(string method, JObject parameters, Func <JToken, JsonRpcException, Task> completion) { switch (method) { case "discover": Discover(parameters); await completion(null, null); break; case "connect": if (_watcher != null && _watcher.Status == DeviceWatcherStatus.Started) { _watcher.Stop(); } await Connect(parameters); await completion(null, null); break; case "send": await completion(await SendMessage(parameters), null); break; default: throw JsonRpcException.MethodNotFound(method); } }
private async Task DidReceiveRequest(JObject request, Func <JToken, Task> sendResult) { var method = request["method"]?.ToObject <string>(); if (string.IsNullOrWhiteSpace(method)) { throw JsonRpcException.InvalidRequest("method value missing or not a string"); } // optional: dictionary of parameters by name var parameters = request["params"]?.ToObject <JObject>() ?? new JObject(); async Task resultHandler(JToken result, JsonRpcException error) { if (error != null) { throw error; } await sendResult(result); }; _sessionLock.Wait(); try { await DidReceiveCall(method, parameters, resultHandler); } finally { _sessionLock.Release(); } }
/// <summary> /// Handle the client's request to read the value of a particular service characteristic. /// </summary> /// <param name="parameters"> /// The IDs of the service & characteristic, an optional encoding to be used in the response, and an optional /// flag to request notification of future changes to this characteristic's value. /// </param> /// <returns> /// The current value as a JSON object with a "message" property and optional "encoding" property /// </returns> private async Task <JToken> Read(JObject parameters) { var endpoint = await GetEndpoint("read request", parameters, GattHelpers.BlockListStatus.ExcludeReads); //Console.Write("-->" +endpoint.AttributeHandle); var encoding = parameters.TryGetValue("encoding", out var encodingToken) ? encodingToken?.ToObject <string>() // possibly null and that's OK : "base64"; var startNotifications = parameters["startNotifications"]?.ToObject <bool>() ?? false; var readResult = await endpoint.ReadValueAsync(BluetoothCacheMode.Uncached); if (startNotifications) { await StartNotifications(endpoint, encoding); } switch (readResult.Status) { case GattCommunicationStatus.Success: // Calling ToArray() on a buffer of length 0 throws an ArgumentException var resultBytes = readResult.Value.Length > 0 ? readResult.Value.ToArray() : new byte[0]; return(EncodingHelpers.EncodeBuffer(resultBytes, encoding)); case GattCommunicationStatus.Unreachable: throw JsonRpcException.ApplicationError("destination unreachable"); default: throw JsonRpcException.ApplicationError($"unknown result from read: {readResult.Status}"); } }
internal BLEDataFilter(JToken dataFilter) { var filterObject = (JObject)dataFilter; JToken token; if (filterObject.TryGetValue("dataPrefix", out token)) { dataPrefix = token.ToObject <List <byte> >(); } else { dataPrefix = new List <byte>(); } if (filterObject.TryGetValue("mask", out token)) { mask = token.ToObject <List <byte> >(); } else { mask = Enumerable.Repeat <byte>(0xFF, dataPrefix.Count).ToList(); } if (dataPrefix.Count != mask.Count) { throw JsonRpcException.InvalidParams( $"length of data prefix ({dataPrefix.Count}) does not match length of mask ({mask.Count})"); } }
/// <summary> /// Handle the client's request to connect to a particular peripheral. /// Valid in the discovery state; transitions to connected state on success. /// </summary> /// <param name="parameters"> /// A JSON object containing the UUID of a peripheral found by the most recent discovery request /// </param> private async Task Connect(JObject parameters) { if (_services != null) { throw JsonRpcException.InvalidRequest("already connected to peripheral"); } var peripheralId = parameters["peripheralId"].ToObject <ulong>(); if (!_reportedPeripherals.Contains(peripheralId)) { // the client may only connect to devices that were returned by the current discovery request throw JsonRpcException.InvalidParams($"invalid peripheral ID: {peripheralId}"); } _peripheral = await BluetoothLEDevice.FromBluetoothAddressAsync(peripheralId); var servicesResult = await _peripheral.GetGattServicesAsync(BluetoothCacheMode.Uncached); if (servicesResult.Status != GattCommunicationStatus.Success) { throw JsonRpcException.ApplicationError($"failed to enumerate GATT services: {servicesResult.Status}"); } _peripheral.ConnectionStatusChanged += OnPeripheralStatusChanged; _services = servicesResult.Services; // cache all characteristics in all services foreach (var service in _services) { var characteristicsResult = await service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached); if (characteristicsResult.Status != GattCommunicationStatus.Success) { continue; } foreach (var characteristic in characteristicsResult.Characteristics) { _cachedCharacteristics.Add(characteristic.Uuid, characteristic); } } // collect optional services plus all services from all filters // Note: this modifies _optionalServices for convenience since we know it'll go away soon. _allowedServices = _optionalServices ?? new HashSet <Guid>(); _allowedServices = _filters .Where(filter => filter.RequiredServices?.Count > 0) .Aggregate(_allowedServices, (result, filter) => { result.UnionWith(filter.RequiredServices); return(result); }); // clean up resources used by discovery _watcher.Stop(); _watcher = null; _reportedPeripherals.Clear(); _optionalServices = null; }
private static async void ListenForMessages(string id) { try { DataReader _socketReader = s_readerMap[id]; DataWriter _socketWriter = s_writerMap[id]; while (true) { await _socketReader.LoadAsync(sizeof(UInt16)); var messageSize = _socketReader.ReadUInt16(); var headerBytes = BitConverter.GetBytes(messageSize); var messageBytes = new byte[messageSize]; await _socketReader.LoadAsync(messageSize); _socketReader.ReadBytes(messageBytes); var totalBytes = new byte[headerBytes.Length + messageSize]; Array.Copy(headerBytes, totalBytes, headerBytes.Length); Array.Copy(messageBytes, 0, totalBytes, headerBytes.Length, messageSize); var parameters = EncodingHelpers.EncodeBuffer(totalBytes, "base64"); s_currentSession.SendRemoteRequest("didReceiveMessage", parameters); } } catch (Exception e) { await s_currentSession.SendErrorNotification(JsonRpcException.ApplicationError("Peripheral connection closed")); Debug.Print($"Closing connection to peripheral: {e.Message}"); //Dispose(); } }
/// <summary> /// Resolve a Web Bluetooth GATT "name" to a canonical UUID, using an assigned numbers table if necessary. /// See <a href="https://webbluetoothcg.github.io/web-bluetooth/#resolveuuidname">here</a> for more info. /// </summary> /// <param name="nameToken">A short UUID in integer form, a full UUID, or the name of an assigned number</param> /// <param name="assignedNumbersTable">The table of assigned numbers to resolve integer names</param> /// <returns>The UUID associated with the token. Throws if not possible.</returns> public static Guid ResolveUuidName(JToken nameToken, IReadOnlyDictionary <string, short> assignedNumbersTable) { if (nameToken.Type == JTokenType.Integer) { return(CanonicalUuid(nameToken.ToObject <int>())); } var name = nameToken.ToObject <string>(); // Web Bluetooth demands an exact match to this regex but the .NET Guid constructor is more permissive. // See https://webbluetoothcg.github.io/web-bluetooth/#valid-uuid var validGuidRegex = new Regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); if (validGuidRegex.IsMatch(name)) { return(new Guid(name)); } // TODO: does Windows / .NET really have no built-in call for this? if (assignedNumbersTable.TryGetValue(name, out var id)) { return(CanonicalUuid(id)); } throw JsonRpcException.InvalidParams($"unknown or invalid GATT name: {nameToken}"); }
/// <summary> /// Encode `data` using `encoding`, either into `destination` or a new JSON object. /// </summary> /// <param name="data">The data to encode</param> /// <param name="encoding">The type of encoding to use, or null to "encode" as a Unicode string</param> /// <param name="destination"> /// The optional object to encode into. /// If not null, the "message" and "encoding" properties will be adjusted as necessary. /// If null, a new object will be created with "message" and (possibly) "encoding" properties. /// </param> /// <returns>The object to which the encoded message was written, regardless of source</returns> public static JObject EncodeBuffer(byte[] data, string encoding, JObject destination = null) { if (destination == null) { destination = new JObject(); } switch (encoding) { case "base64": destination["encoding"] = encoding; destination["message"] = Convert.ToBase64String(data); break; case null: destination.Remove("encoding"); destination["message"] = Encoding.UTF8.GetString(data); break; default: throw JsonRpcException.InvalidParams($"unsupported encoding: {encoding}"); } return(destination); }
private JObject MakeResponse(JToken responseId, JToken result, JsonRpcException error) { return(new JObject( new JProperty("jsonrpc", "2.0"), new JProperty("id", responseId), error == null ? new JProperty("result", result) : new JProperty("error", JObject.FromObject(error)) )); }
private async Task Connect(JObject parameters) { //if (_connectedSocket?.Information.RemoteHostName != null) //{ // throw JsonRpcException.InvalidRequest("Already connected"); //} var id = parameters["peripheralId"]?.ToObject <string>(); var address = Convert.ToUInt64(id, 16); var bluetoothDevice = await BluetoothDevice.FromBluetoothAddressAsync(address); if (!bluetoothDevice.DeviceInformation.Pairing.IsPaired) { if (parameters.TryGetValue("pin", out var pin)) { _pairingCode = (string)pin; } var pairingResult = await Pair(bluetoothDevice); if (pairingResult != DevicePairingResultStatus.Paired && pairingResult != DevicePairingResultStatus.AlreadyPaired) { throw JsonRpcException.ApplicationError("Could not automatically pair with peripheral"); } } s_currentSession = this; if (s_connectMap.ContainsKey(id)) { // ListenForMessages(id); return; } var services = await bluetoothDevice.GetRfcommServicesForIdAsync(RfcommServiceId.SerialPort, BluetoothCacheMode.Uncached); if (services.Services.Count > 0) { StreamSocket _connectedSocket = new StreamSocket(); await _connectedSocket.ConnectAsync(services.Services[0].ConnectionHostName, services.Services[0].ConnectionServiceName); DataWriter _socketWriter = new DataWriter(_connectedSocket.OutputStream); DataReader _socketReader = new DataReader(_connectedSocket.InputStream) { ByteOrder = ByteOrder.LittleEndian }; s_readerMap.Add(id, _socketReader); s_writerMap.Add(id, _socketWriter); s_connectMap.Add(id, _connectedSocket); ListenForMessages(id); } else { throw JsonRpcException.ApplicationError("Cannot read services from peripheral"); } }
/// <summary> /// Search for peripherals which match the filter information provided in the parameters. /// Valid in the initial state; transitions to discovery state on success. /// </summary> /// <param name="parameters"> /// JSON object containing at least one filter, and optionally an "optionalServices" list. See /// <a href="https://webbluetoothcg.github.io/web-bluetooth/#dictdef-requestdeviceoptions">here</a> for more /// information, but note that the "acceptAllDevices" property is ignored. /// </param> private void Discover(JObject parameters) { if (_services != null) { throw JsonRpcException.InvalidRequest("cannot discover when connected"); } var jsonFilters = parameters["filters"]?.ToObject <JArray>(); if (jsonFilters == null || jsonFilters.Count < 1) { throw JsonRpcException.InvalidParams("discovery request must include filters"); } var newFilters = jsonFilters.Select(filter => new BLEScanFilter(filter)).ToList(); if (newFilters.Any(filter => filter.IsEmpty)) { throw JsonRpcException.InvalidParams("discovery request includes empty filter"); } HashSet <Guid> newOptionalServices = null; if (parameters.TryGetValue("optionalServices", out var optionalServicesToken)) { var optionalServicesArray = (JArray)optionalServicesToken; newOptionalServices = new HashSet <Guid>(optionalServicesArray.Select(GattHelpers.GetServiceUuid)); } if (_watcher?.Status == BluetoothLEAdvertisementWatcherStatus.Started) { _watcher.Received -= OnAdvertisementReceived; _watcher.Stop(); } _watcher = new BluetoothLEAdvertisementWatcher() { SignalStrengthFilter = { InRangeThresholdInDBm = MinimumSignalStrength, OutOfRangeThresholdInDBm = MinimumSignalStrength - SignalStrengthMargin, OutOfRangeTimeout = TimeSpan.FromMilliseconds(OutOfRangeTimeout) } }; _reportedPeripherals.Clear(); _filters = newFilters; _optionalServices = newOptionalServices; _watcher.Received += OnAdvertisementReceived; _watcher.ScanningMode = BluetoothLEScanningMode.Active; _watcher.Start(); }
/// <summary> /// Decode the "message" property of `jsonBuffer` into bytes. /// If the buffer has an `encoding` property, use that method. Otherwise, assume the message is Unicode text. /// </summary> /// <param name="jsonBuffer"> /// A JSON object containing a "message" property and optionally an "encoding" property. /// </param> /// <returns>An array of bytes containing the decoded data</returns> public static byte[] DecodeBuffer(JObject jsonBuffer) { var message = jsonBuffer["message"].ToObject <string>(); var encoding = jsonBuffer["encoding"]?.ToObject <string>(); switch (encoding) { case "base64": // "message" is encoded with Base64 return(Convert.FromBase64String(message)); case null: // "message" is a Unicode string with no additional encoding return(Encoding.UTF8.GetBytes(message)); default: throw JsonRpcException.InvalidParams($"unsupported encoding: {encoding}"); } }
private async Task StartNotifications(GattCharacteristic endpoint, string encoding) { if (!_notifyCharacteristics.Contains(endpoint)) { endpoint.ValueChanged += OnValueChanged; var notificationRequestResult = await endpoint.WriteClientCharacteristicConfigurationDescriptorAsync( GattClientCharacteristicConfigurationDescriptorValue.Notify); if (notificationRequestResult != GattCommunicationStatus.Success) { endpoint.ValueChanged -= OnValueChanged; throw JsonRpcException.ApplicationError( $"could not start notifications: {notificationRequestResult}"); } _notifyCharacteristics.Add(endpoint); } }
// See https://webbluetoothcg.github.io/web-bluetooth/#bluetoothlescanfilterinit-canonicalizing internal BLEScanFilter(JToken filter) { var filterObject = (JObject)filter; JToken token; if (filterObject.TryGetValue("name", out token)) { Name = token.ToString(); } if (filterObject.TryGetValue("namePrefix", out token)) { NamePrefix = token.ToString(); } if (filterObject.TryGetValue("services", out token)) { var serviceArray = (JArray)token; RequiredServices = new HashSet <Guid>(serviceArray.Select(GattHelpers.GetServiceUuid)); if (RequiredServices.Count < 1) { throw JsonRpcException.InvalidParams($"filter contains empty or invalid services list: {filter}"); } } if (filterObject.TryGetValue("manufacturerData", out token)) { ManufacturerData = new Dictionary <int, BLEDataFilter>(); var manufacturerData = (JObject)token; foreach (var kv in manufacturerData) { var manufacturerId = int.Parse(kv.Key); var dataFilter = new BLEDataFilter(kv.Value); ManufacturerData.Add(manufacturerId, dataFilter); } } if (filterObject.TryGetValue("serviceData", out token)) { throw JsonRpcException.ApplicationError("filtering on serviceData is not currently supported"); } }
private async Task <JToken> SendMessage(JObject parameters) { if (_socketWriter == null) { throw JsonRpcException.InvalidRequest("Not connected to peripheral"); } var data = EncodingHelpers.DecodeBuffer(parameters); try { _socketWriter.WriteBytes(data); await _socketWriter.StoreAsync(); } catch (ObjectDisposedException) { throw JsonRpcException.InvalidRequest("Not connected to peripheral"); } return(data.Length); }
/// <summary> /// Handle the client's request to write a value to a particular service characteristic. /// </summary> /// <param name="parameters"> /// The IDs of the service & characteristic along with the message and optionally the message encoding. /// </param> /// <returns>The number of decoded bytes written</returns> private async Task <JToken> Write(JObject parameters) { var buffer = EncodingHelpers.DecodeBuffer(parameters); var endpoint = await GetEndpoint("write request", parameters, GattHelpers.BlockListStatus.ExcludeWrites); var withResponse = (parameters["withResponse"]?.ToObject <bool>() ?? false) || !endpoint.CharacteristicProperties.HasFlag(GattCharacteristicProperties.WriteWithoutResponse); var result = await endpoint.WriteValueAsync(buffer.AsBuffer(), withResponse?GattWriteOption.WriteWithResponse : GattWriteOption.WriteWithoutResponse); switch (result) { case GattCommunicationStatus.Success: return(buffer.Length); case GattCommunicationStatus.Unreachable: throw JsonRpcException.ApplicationError("destination unreachable"); default: throw JsonRpcException.ApplicationError($"unknown result from write: {result}"); } }
private async Task DidReceiveResponse(JObject response) { var requestId = response["id"]?.ToObject <RequestId?>(); if (!requestId.HasValue) { throw JsonRpcException.InvalidRequest("response ID value missing or wrong type"); } if (!_completionHandlers.TryGetValue(requestId.Value, out var completionHandler)) { throw JsonRpcException.InvalidRequest("response ID does not correspond to any open request"); } var error = response["error"]?.ToObject <JsonRpcException>(); try { if (error != null) { await completionHandler(null, error); } else { var result = response["result"]; await completionHandler(result, null); } } catch (Exception e) { var remoteMessage = $"exception encountered while handling response {requestId}"; Debug.Print(remoteMessage); Debug.Print($"The exception was: {e}"); throw JsonRpcException.ApplicationError(remoteMessage); } }
// See https://webbluetoothcg.github.io/web-bluetooth/#bluetoothlescanfilterinit-canonicalizing internal BLEScanFilter(JToken filter) { var filterObject = (JObject)filter; JToken token; if (filterObject.TryGetValue("name", out token)) { Name = token.ToString(); } if (filterObject.TryGetValue("namePrefix", out token)) { NamePrefix = token.ToString(); } if (filterObject.TryGetValue("services", out token)) { var serviceArray = (JArray)token; RequiredServices = new HashSet <Guid>(serviceArray.Select(GattHelpers.GetServiceUuid)); if (RequiredServices.Count < 1) { throw JsonRpcException.InvalidParams($"filter contains empty or invalid services list: {filter}"); } } if (filterObject.TryGetValue("manufacturerData", out token)) { throw JsonRpcException.ApplicationError("filtering on manufacturerData is not currently supported"); } if (filterObject.TryGetValue("serviceData", out token)) { throw JsonRpcException.ApplicationError("filtering on serviceData is not currently supported"); } }
// Override this to handle received RPC requests & notifications. // Call this method with `await super.DidReceiveCall(...)` to implement default calls like `getVersion`. // Call the completion handler when done with a request: // - pass your call's "return value" (or null) as `result` on success // - pass an instance of `JsonRpcException` for `error` on failure // You may also throw a `JsonRpcException` (or any other `Exception`) to signal failure. // Exceptions are caught even when thrown in an `async` method after `await`: // http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions protected virtual async Task DidReceiveCall(string method, JObject parameters, CompletionHandler completion) { switch (method) { case "pingMe": await completion("willPing", null); SendRemoteRequest("ping", null, (result, error) => { Debug.Print($"Got result from ping: {result}"); return(Task.CompletedTask); }); break; case "getVersion": await completion(GetVersion(), null); break; default: // unrecognized method throw JsonRpcException.MethodNotFound(method); } }
/// <summary> /// Fetch the characteristic referred to in the endpointInfo object and perform access verification. /// </summary> /// <param name="errorContext"> /// A string to include in error reporting, if an error is encountered /// </param> /// <param name="endpointInfo"> /// A JSON object which may contain a 'serviceId' property and a 'characteristicId' property /// </param> /// <param name="checkFlag"> /// Check if this flag is set for this service or characteristic in the block list. If so, throw. /// </param> /// <returns> /// The specified GATT service characteristic, if it can be resolved and all checks pass. /// Otherwise, a JSON-RPC exception is thrown indicating what went wrong. /// </returns> private async Task <GattCharacteristic> GetEndpoint(string errorContext, JObject endpointInfo, GattHelpers.BlockListStatus checkFlag) { GattDeviceService service; Guid?serviceId; if (_peripheral.ConnectionStatus != BluetoothConnectionStatus.Connected) { throw JsonRpcException.ApplicationError($"Peripheral is not connected for {errorContext}"); } if (endpointInfo.TryGetValue("serviceId", out var serviceToken)) { serviceId = GattHelpers.GetServiceUuid(serviceToken); service = _services?.FirstOrDefault(s => s.Uuid == serviceId); } else { service = _services?.FirstOrDefault(); // could in theory be null serviceId = service?.Uuid; } if (!serviceId.HasValue) { throw JsonRpcException.InvalidParams($"Could not determine service UUID for {errorContext}"); } //if (_allowedServices?.Contains(serviceId.Value) != true) //{ // throw JsonRpcException.InvalidParams($"attempt to access unexpected service: {serviceId}"); //} var blockStatus = GattHelpers.GetBlockListStatus(serviceId.Value); if (blockStatus.HasFlag(checkFlag)) { throw JsonRpcException.InvalidParams($"service is block-listed with {blockStatus}: {serviceId}"); } if (service == null) { throw JsonRpcException.InvalidParams($"could not find service {serviceId}"); } GattCharacteristic characteristic; Guid?characteristicId; if (endpointInfo.TryGetValue("characteristicId", out var characteristicToken)) { characteristic = null; // we will attempt to collect this below characteristicId = GattHelpers.GetCharacteristicUuid(characteristicToken); } else { if (!_cachedServiceCharacteristics.TryGetValue(service.Uuid, out var characteristics)) { var characteristicsResult = await service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached); if (characteristicsResult.Status != GattCommunicationStatus.Success) { throw JsonRpcException.ApplicationError( $"failed to collect characteristics from service: {characteristicsResult.Status}"); } characteristics = characteristicsResult.Characteristics; _cachedServiceCharacteristics.Add(service.Uuid, characteristics); } characteristic = characteristics.FirstOrDefault(); // could in theory be null characteristicId = characteristic?.Uuid; } if (!characteristicId.HasValue) { throw JsonRpcException.InvalidParams($"Could not determine characteristic UUID for {errorContext}"); } blockStatus = GattHelpers.GetBlockListStatus(characteristicId.Value); if (blockStatus.HasFlag(checkFlag)) { throw JsonRpcException.InvalidParams( $"characteristic is block-listed with {blockStatus}: {characteristicId}"); } // collect the characteristic if we didn't do so above if (characteristic == null && !_cachedCharacteristics.TryGetValue(characteristicId.Value, out characteristic)) { var characteristicsResult = await service.GetCharacteristicsForUuidAsync(characteristicId.Value, BluetoothCacheMode.Uncached); if (characteristicsResult.Status != GattCommunicationStatus.Success) { throw JsonRpcException.ApplicationError( $"failed to collect characteristics from service: {characteristicsResult.Status}"); } if (characteristicsResult.Characteristics.Count < 1) { throw JsonRpcException.InvalidParams( $"could not find characteristic {characteristicId} on service {serviceId}"); } // TODO: why is this a list? characteristic = characteristicsResult.Characteristics[0]; _cachedCharacteristics.Add(characteristicId.Value, characteristic); } try { // Unfortunately there's no direct way to test if the peripheral object has been disposed. The // `connectionState` property still indicates that the peripheral is connected in some cases, for // example when Bluetooth is turned off in Bluetooth settings / Control Panel. However, trying to // access the `Service` property of the `Characteristic` will throw an `ObjectDisposedException` in // this case, so that's the hack being used here to check for a disposed peripheral. var tempDisposalProbe = characteristic.Service; } catch (ObjectDisposedException e) { // This could mean that Bluetooth was turned off or the computer resumed from sleep throw JsonRpcException.ApplicationError($"Peripheral is disposed for {errorContext}"); } return(characteristic); }
private async Task DidReceiveMessage(string message, Func <string, Task> sendResponseText) { var encoding = Encoding.UTF8; JToken responseId = null; async Task SendResponseInternal(JToken result, JsonRpcException error) { var response = MakeResponse(responseId, result, error); var responseText = JsonConvert.SerializeObject(response); Console.WriteLine("REP:" + responseText); await sendResponseText(responseText); } async Task SendResponse(JToken result, JsonRpcException error) { try { await SendResponseInternal(result, error); } catch (Exception firstError) { try { Debug.Print($"Could not encode response: {firstError}"); await SendResponseInternal(null, JsonRpcException.ApplicationError("Could not encode response")); } catch (Exception secondError) { Debug.Print($"Could not report response encoding failure: {secondError}"); } } } try { Console.WriteLine("Rec:" + message); var json = JObject.Parse(message); // do this as early as possible so that error responses can include it. responseId = json["id"]; // property "jsonrpc" must be exactly "2.0" if ((string)json["jsonrpc"] != "2.0") { throw JsonRpcException.InvalidRequest("unrecognized JSON-RPC version string"); } if (json["method"] != null) { await DidReceiveRequest(json, async result => await SendResponse(result, null)); } else if (json["result"] != null || json["error"] != null) { await DidReceiveResponse(json); } else { throw JsonRpcException.InvalidRequest("message is neither request nor response"); } } catch (JsonRpcException jsonRpcException) { await SendResponse(null, jsonRpcException); } catch (Exception e) { var jsonRpcException = JsonRpcException.ApplicationError($"Unhandled error encountered during call: {e}"); await SendResponse(null, jsonRpcException); } }
/// <summary> /// Fetch the characteristic referred to in the endpointInfo object and perform access verification. /// </summary> /// <param name="errorContext"> /// A string to include in error reporting, if an error is encountered /// </param> /// <param name="endpointInfo"> /// A JSON object which may contain a 'serviceId' property and a 'characteristicId' property /// </param> /// <param name="checkFlag"> /// Check if this flag is set for this service or characteristic in the block list. If so, throw. /// </param> /// <returns> /// The specified GATT service characteristic, if it can be resolved and all checks pass. /// Otherwise, a JSON-RPC exception is thrown indicating what went wrong. /// </returns> private async Task <GattCharacteristic> GetEndpoint(string errorContext, JObject endpointInfo, GattHelpers.BlockListStatus checkFlag) { GattDeviceService service; Guid?serviceId; if (endpointInfo.TryGetValue("serviceId", out var serviceToken)) { serviceId = GattHelpers.GetServiceUuid(serviceToken); service = _services?.FirstOrDefault(s => s.Uuid == serviceId); } else { service = _services?.FirstOrDefault(); // could in theory be null serviceId = service?.Uuid; } if (!serviceId.HasValue) { throw JsonRpcException.InvalidParams($"Could not determine service UUID for {errorContext}"); } if (_allowedServices?.Contains(serviceId.Value) != true) { throw JsonRpcException.InvalidParams($"attempt to access unexpected service: {serviceId}"); } var blockStatus = GattHelpers.GetBlockListStatus(serviceId.Value); if (blockStatus.HasFlag(checkFlag)) { throw JsonRpcException.InvalidParams($"service is block-listed with {blockStatus}: {serviceId}"); } if (service == null) { throw JsonRpcException.InvalidParams($"could not find service {serviceId}"); } GattCharacteristic characteristic; Guid?characteristicId; if (endpointInfo.TryGetValue("characteristicId", out var characteristicToken)) { characteristic = null; // we will attempt to collect this below characteristicId = GattHelpers.GetCharacteristicUuid(characteristicToken); } else { if (!_cachedServiceCharacteristics.TryGetValue(service.Uuid, out var characteristics)) { var characteristicsResult = await service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached); if (characteristicsResult.Status != GattCommunicationStatus.Success) { throw JsonRpcException.ApplicationError( $"failed to collect characteristics from service: {characteristicsResult.Status}"); } characteristics = characteristicsResult.Characteristics; _cachedServiceCharacteristics.Add(service.Uuid, characteristics); } characteristic = characteristics.FirstOrDefault(); // could in theory be null characteristicId = characteristic?.Uuid; } if (!characteristicId.HasValue) { throw JsonRpcException.InvalidParams($"Could not determine characteristic UUID for {errorContext}"); } blockStatus = GattHelpers.GetBlockListStatus(characteristicId.Value); if (blockStatus.HasFlag(checkFlag)) { throw JsonRpcException.InvalidParams( $"characteristic is block-listed with {blockStatus}: {characteristicId}"); } // collect the characteristic if we didn't do so above if (characteristic == null && !_cachedCharacteristics.TryGetValue(characteristicId.Value, out characteristic)) { var characteristicsResult = await service.GetCharacteristicsForUuidAsync(characteristicId.Value, BluetoothCacheMode.Uncached); if (characteristicsResult.Status != GattCommunicationStatus.Success) { throw JsonRpcException.ApplicationError( $"failed to collect characteristics from service: {characteristicsResult.Status}"); } if (characteristicsResult.Characteristics.Count < 1) { throw JsonRpcException.InvalidParams( $"could not find characteristic {characteristicId} on service {serviceId}"); } // TODO: why is this a list? characteristic = characteristicsResult.Characteristics[0]; _cachedCharacteristics.Add(characteristicId.Value, characteristic); } return(characteristic); }
/// <summary> /// Handle a client request /// </summary> /// <param name="method">The name of the method called by the client</param> /// <param name="parameters">The parameters passed by the client</param> /// <param name="completion">The completion handler to be called with the result</param> protected override async Task DidReceiveCall(string method, JObject parameters, Func <JToken, JsonRpcException, Task> completion) { switch (method) { case "discover": if (m_peripheralData != null) { SendRemoteRequest("didDiscoverPeripheral", m_peripheralData); firstconnect = false; } else { Discover(parameters); firstconnect = true; } await completion(null, null); break; case "connect": if (_services != null) { foreach (var service in _services) { try { service.Dispose(); } catch { // ignore: probably the peripheral is gone } } _services = null; _peripheral.Dispose(); } _cachedCharacteristics.Clear(); //if (firstconnect) JObject ret = null; try { await Connect(parameters); ret = new JObject { new JProperty("status", new JValue("OK")) }; } catch (Exception e) { ret = new JObject { new JProperty("status", new JValue("Error")), new JProperty("message", new JValue(e.Message)) }; } SendRemoteRequest("connect", ret); await completion(null, null); break; case "write": await completion(await Write(parameters), null); break; case "read": await completion(await Read(parameters), null); break; case "startNotifications": //if (firstconnect) await StartNotifications(parameters); await completion(null, null); break; case "stopNotifications": await StopNotifications(parameters); await completion(null, null); break; case "pingMe": await completion("willPing", null); SendRemoteRequest("ping", null, (result, error) => { Debug.Print($"Got result from ping: {result}"); return(Task.CompletedTask); }); break; default: throw JsonRpcException.MethodNotFound(method); } }