Esempio n. 1
0
        /// <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}");
        }
Esempio n. 2
0
        /// <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);
        }
Esempio n. 3
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");
            }
        }
Esempio n. 4
0
        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})");
            }
        }
Esempio n. 5
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;
        }
Esempio n. 6
0
        /// <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();
        }
Esempio n. 7
0
        /// <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}");
            }
        }
Esempio n. 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");
            }
        }
Esempio n. 9
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");
            }
        }
Esempio n. 10
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);
        }
Esempio n. 11
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);
        }