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