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