Exemple #1
0
        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");
            }
        }
Exemple #2
0
        /// <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}");
            }
        }
Exemple #3
0
        /// <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;
        }
Exemple #4
0
        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"
            }
        }
Exemple #6
0
        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");
            }
        }
Exemple #7
0
        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);
            }
        }
Exemple #8
0
        // 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");
            }
        }
Exemple #9
0
        /// <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}");
            }
        }
Exemple #10
0
        // 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");
            }
        }
Exemple #11
0
        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);
            }
        }
Exemple #12
0
        /// <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);
        }
Exemple #13
0
        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);
            }
        }
Exemple #14
0
        /// <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);
        }