diff --git a/Darp.Ble.slnx b/Darp.Ble.slnx
index e801ad38..1bc916e1 100644
--- a/Darp.Ble.slnx
+++ b/Darp.Ble.slnx
@@ -5,11 +5,14 @@
+
+
+
diff --git a/docs/telemetry.md b/docs/telemetry.md
index 459a9755..07fda4b8 100644
--- a/docs/telemetry.md
+++ b/docs/telemetry.md
@@ -11,7 +11,7 @@ metrics.AddMeter(BleTelemetry.MeterName); // "Darp.Ble"
`BleManagerBuilder.ConfigureDiagnostics(options => options.IncludeAddresses = false)` replaces peer addresses with `` in logs and spans. Addresses are never metric dimensions.
-The backends add their own sources and event ids when they are ported to v2 (HciHost below; WinRT and Android follow); their spans nest inside the core's.
+The backends add their own sources and event ids when they are ported to v2 (HciHost and WinRT below; Android follows); their spans nest inside the core's.
## Spans (source `Darp.Ble`)
@@ -58,6 +58,7 @@ Span names are message templates; values are only in tags. A cancelled operation
| 1200 | AdvertisingStarted | Debug | Advertising set {Address} started |
| 1201 | AdvertisingStopped | Debug | Advertising set {Address} stopped |
| 1202 | AdvertisingRestartFailed | Warning | Advertising set {Address} could not start again after its central disconnected |
+| 1203 | AdvertisingFailed | Warning | Advertising set {Address} stopped because of an error |
| 1300 | Connecting | Debug | Connecting to {Address} |
| 1301 | Connected | Debug | Connected to {Address} |
| 1302 | ConnectFailed | Warning | Connecting to {Address} failed: {Kind} |
@@ -91,3 +92,13 @@ The HCI layer logs under categories that start with `Darp.Ble.Hci` and traces on
| 2007 | AttPacketTransmission | Trace | {Source} to {Destination}: [0x{ConnectionHandle:X3}]: {PacketName} |
| 2008 | ReceivedUnknownAttPacket | Warning | {Source} to {Destination}: [0x{ConnectionHandle:X3}]: {OpCode} |
| 2009 | AttPacketTransmissionWarning | Warning | {Source} to {Destination}: [0x{ConnectionHandle:X3}]: {PacketName} {Message} |
+
+## WinRT backend (`Darp.Ble.WinRT`)
+
+The WinRT backend logs under categories that start with `Darp.Ble.WinRT` and has no spans of its own.
+
+| Id | Name | Level | Message |
+| --- | --- | --- | --- |
+| 3000 | NotOnWindows | Warning | The WinRT backend has no adapter: the app does not run on Windows 10 2004 or later, or was built without its Windows target |
+| 3001 | ReservedSectionDropped | Warning | Windows sets advertising data section {AdType} itself; the app's section is left out |
+| 3002 | HostedRequestFailed | Warning | A central's request to a hosted service could not be answered |
diff --git a/src/Darp.Ble.HciHost/Connections/ConnectAttempt.cs b/src/Darp.Ble.HciHost/Connections/ConnectAttempt.cs
index ec7522c5..6a851323 100644
--- a/src/Darp.Ble.HciHost/Connections/ConnectAttempt.cs
+++ b/src/Darp.Ble.HciHost/Connections/ConnectAttempt.cs
@@ -50,8 +50,9 @@ public void Dispose()
[MessageSink]
private void OnConnectionComplete(HciLeEnhancedConnectionCompleteV1Event hciEvent)
{
- if (hciEvent.Role is not HciLeConnectionRole.Central || _result.Task.IsCompleted)
+ if (_result.Task.IsCompleted)
return;
+ // Of a failed event only the status is valid; it is this attempt's, as the controller runs one at a time
if (hciEvent.Status is HciCommandStatus.UnknownConnectionIdentifier)
{
// The attempt was cancelled
@@ -69,8 +70,11 @@ private void OnConnectionComplete(HciLeEnhancedConnectionCompleteV1Event hciEven
);
return;
}
- // A link to another peer is not this attempt's
- if (hciEvent.PeerAddress.ToUInt64() != _peer.Value.ToUInt64())
+ // A link a central made to our peripheral role, or one to another peer, is not this attempt's
+ if (
+ hciEvent.Role is not HciLeConnectionRole.Central
+ || hciEvent.PeerAddress.ToUInt64() != _peer.Value.ToUInt64()
+ )
return;
if (!_device.TryGetConnection(hciEvent.ConnectionHandle, out AclConnection? connection))
return;
diff --git a/src/Darp.Ble.HciHost/Connections/HciHostCentralDriver.cs b/src/Darp.Ble.HciHost/Connections/HciHostCentralDriver.cs
index 1e31b535..e7941092 100644
--- a/src/Darp.Ble.HciHost/Connections/HciHostCentralDriver.cs
+++ b/src/Darp.Ble.HciHost/Connections/HciHostCentralDriver.cs
@@ -6,6 +6,7 @@
using Darp.Ble.Hci.Payload;
using Darp.Ble.Hci.Payload.Command;
using Darp.Ble.Hci.Payload.Result;
+using Darp.Ble.HciHost.Gap;
namespace Darp.Ble.HciHost.Connections;
@@ -15,11 +16,12 @@ namespace Darp.Ble.HciHost.Connections;
"CA1001:Types that own disposable fields should be disposable",
Justification = "The semaphore never creates its wait handle"
)]
-internal sealed class HciHostCentralDriver(HciDevice device) : ICentralDriver
+internal sealed class HciHostCentralDriver(HciDevice device, HciHostObserverDriver observer) : ICentralDriver
{
private static readonly TimeSpan MinSupervisionTimeout = TimeSpan.FromMilliseconds(720);
private readonly HciDevice _device = device;
+ private readonly HciHostObserverDriver _observer = observer;
// The controller initiates one connection at a time; further attempts wait for their turn.
// Without its wait handle the semaphore holds nothing to dispose
@@ -34,6 +36,19 @@ public async Task ConnectAsync(
CancellationToken cancellationToken
)
{
+ // The controller needs the address type; an address without it, e.g. parsed from text, takes the type it was heard with
+ if (address.Type is BleAddressType.NotAvailable)
+ {
+ address = _observer.TypeOf(address.Value) is { } type
+ ? address with
+ {
+ Type = type,
+ }
+ : throw new BleConnectException(
+ BleConnectErrorKind.Unreachable,
+ $"The address type of {address} is unknown; scan until the device is heard, then connect"
+ );
+ }
ushort interval = HciTiming.ToConnectionIntervalUnits(
parameters.ConnectionInterval,
nameof(parameters.ConnectionInterval)
diff --git a/src/Darp.Ble.HciHost/Gap/AdvertisingReportAssembler.cs b/src/Darp.Ble.HciHost/Gap/AdvertisingReportAssembler.cs
new file mode 100644
index 00000000..a1edc3e9
--- /dev/null
+++ b/src/Darp.Ble.HciHost/Gap/AdvertisingReportAssembler.cs
@@ -0,0 +1,65 @@
+using Darp.Ble.Hci.Payload.Event;
+
+namespace Darp.Ble.HciHost.Gap;
+
+///
+/// Joins the reports of one advertisement. A controller splits data that does not fit one report: every report but
+/// the last says more data is to come. Reports of other advertisers and scan responses may come in between
+///
+/// Only the transport's read loop uses it
+internal sealed class AdvertisingReportAssembler
+{
+ // The most data an advertisement can carry
+ private const int MaxDataLength = 1650;
+ private const int ScanResponse = 0b01000;
+ private const int MoreDataToCome = 0b01;
+ private const sbyte TxPowerNotAvailable = 0x7F;
+
+ private readonly Dictionary _pending = [];
+
+ /// The data of the whole advertisement, once a report ends it
+ /// The next report
+ /// The data of the advertisement this report ends
+ /// Its TX power: later reports may leave out the one an earlier report gave
+ /// False while more data is to come, or when an advertisement is longer than any can be
+ public bool TryEnd(HciLeExtendedAdvertisingReport report, out ReadOnlyMemory data, out sbyte txPower)
+ {
+ // Bits 5 and 6 of the event type: complete, more data to come, or truncated
+ bool moreToCome = ((report.EventType >> 5) & 0b11) == MoreDataToCome;
+ var sender = new Sender(
+ report.AddressType,
+ report.Address.ToUInt64(),
+ report.AdvertisingSId,
+ (report.EventType & ScanResponse) != 0
+ );
+ bool continues = _pending.Remove(sender, out Pending earlier);
+ txPower = continues && report.TxPower == TxPowerNotAvailable ? earlier.TxPower : report.TxPower;
+ data = default;
+ if (!continues && !moreToCome)
+ {
+ data = report.Data;
+ return true;
+ }
+ // An advertisement already too long is dropped up to its last report
+ byte[]? joined = continues && earlier.Data is null ? null : [.. earlier.Data ?? [], .. report.Data.Span];
+ if (joined?.Length > MaxDataLength)
+ joined = null;
+ if (moreToCome)
+ {
+ _pending[sender] = new Pending(joined, txPower);
+ return false;
+ }
+ if (joined is null)
+ return false;
+ data = joined;
+ return true;
+ }
+
+ /// A new scan starts without the parts of the last one
+ public void Clear() => _pending.Clear();
+
+ private readonly record struct Sender(byte AddressType, ulong Address, byte AdvertisingSId, bool IsScanResponse);
+
+ /// The data so far, or null for an advertisement too long to deliver
+ private readonly record struct Pending(byte[]? Data, sbyte TxPower);
+}
diff --git a/src/Darp.Ble.HciHost/Gap/HciHostObserverDriver.cs b/src/Darp.Ble.HciHost/Gap/HciHostObserverDriver.cs
index 854a39d4..ad8011e1 100644
--- a/src/Darp.Ble.HciHost/Gap/HciHostObserverDriver.cs
+++ b/src/Darp.Ble.HciHost/Gap/HciHostObserverDriver.cs
@@ -19,6 +19,15 @@ internal sealed partial class HciHostObserverDriver : IObserverDriver, IDisposab
private IScanSink? _sink;
private bool _transportFailed;
+ // The type each address was last heard with, so an address known only by its value can be connected.
+ // Bounded: a long scan among devices with changing private addresses hears ever new ones
+ private const int MaxKnownAddresses = 1024;
+ private readonly Dictionary _addressTypes = [];
+
+ // Used on the read loop only; a new scan's sink starts it afresh
+ private readonly AdvertisingReportAssembler _assembler = new();
+ private IScanSink? _assembledFor;
+
public HciHostObserverDriver(HciDevice device)
{
_device = device;
@@ -85,33 +94,64 @@ await _device
.ConfigureAwait(false);
}
+ /// The type the address was last heard with, if it was heard
+ /// The value of the address
+ /// The address type, or null
+ public BleAddressType? TypeOf(Data.UInt48 address)
+ {
+ lock (_lock)
+ return _addressTypes.TryGetValue((ulong)address, out BleAddressType type) ? type : null;
+ }
+
+ private void Remember(ulong address, BleAddressType type)
+ {
+ // Anonymous advertisements have no address to connect to
+ if (type is BleAddressType.NotAvailable)
+ return;
+ if (_addressTypes.Count >= MaxKnownAddresses && !_addressTypes.ContainsKey(address))
+ _addressTypes.Clear();
+ _addressTypes[address] = type;
+ }
+
[MessageSink]
private void OnAdvertisingReport(HciLeExtendedAdvertisingReportEvent reportEvent)
{
IScanSink? sink;
lock (_lock)
+ {
sink = _sink;
+ foreach (HciLeExtendedAdvertisingReport report in reportEvent.Reports)
+ Remember(report.Address.ToUInt64(), (BleAddressType)report.AddressType);
+ }
if (sink is null)
return;
+ if (!ReferenceEquals(sink, _assembledFor))
+ {
+ _assembler.Clear();
+ _assembledFor = sink;
+ }
DateTimeOffset timestamp = _device.TimeProvider.GetUtcNow();
foreach (HciLeExtendedAdvertisingReport report in reportEvent.Reports)
{
+ if (!_assembler.TryEnd(report, out ReadOnlyMemory data, out sbyte txPower))
+ continue;
sink.OnReport(
- GapAdvertisement.FromExtendedAdvertisingReport(
+ GapAdvertisement.Create(
timestamp,
- (BleEventType)report.EventType,
+ // Without the data status in bits 5 and 6: the advertisement is whole
+ (BleEventType)(report.EventType & 0b11111),
new BleAddress((BleAddressType)report.AddressType, (Data.UInt48)report.Address.ToUInt64()),
(Physical)report.PrimaryPhy,
(Physical)report.SecondaryPhy,
(AdvertisingSId)report.AdvertisingSId,
- (TxPowerLevel)report.TxPower,
+ (TxPowerLevel)txPower,
(Rssi)report.Rssi,
(PeriodicAdvertisingInterval)report.PeriodicAdvertisingInterval,
new BleAddress(
(BleAddressType)report.DirectAddressType,
(Data.UInt48)report.DirectAddress.ToUInt64()
),
- AdvertisingData.From(report.Data)
+ AdvertisingData.From(data)
)
);
}
diff --git a/src/Darp.Ble.HciHost/HciHostAdapterDriver.cs b/src/Darp.Ble.HciHost/HciHostAdapterDriver.cs
index ffc34b3d..c8eb3cb5 100644
--- a/src/Darp.Ble.HciHost/HciHostAdapterDriver.cs
+++ b/src/Darp.Ble.HciHost/HciHostAdapterDriver.cs
@@ -28,7 +28,7 @@ int numberOfAdvertisingSets
_device = device;
_observer = new HciHostObserverDriver(device);
_peripheral = new HciHostPeripheralDriver(device, includeGattService);
- _central = new HciHostCentralDriver(device);
+ _central = new HciHostCentralDriver(device, _observer);
_broadcaster = new HciHostBroadcasterDriver(
device,
maxAdvertisingDataLength,
diff --git a/src/Darp.Ble.Mock/Gap/MockObserverDriver.cs b/src/Darp.Ble.Mock/Gap/MockObserverDriver.cs
index eae27f47..da9c607b 100644
--- a/src/Darp.Ble.Mock/Gap/MockObserverDriver.cs
+++ b/src/Darp.Ble.Mock/Gap/MockObserverDriver.cs
@@ -60,7 +60,7 @@ Rssi rssi
{
AdvertisingParameters parameters = advertising.Parameters;
bool isLegacy = eventType.HasFlag(BleEventType.Legacy);
- return GapAdvertisement.FromExtendedAdvertisingReport(
+ return GapAdvertisement.Create(
timestamp,
eventType,
advertising.Address,
diff --git a/src/Darp.Ble.Mock/MockAir.cs b/src/Darp.Ble.Mock/MockAir.cs
index 795e5727..cfe3787e 100644
--- a/src/Darp.Ble.Mock/MockAir.cs
+++ b/src/Darp.Ble.Mock/MockAir.cs
@@ -53,7 +53,11 @@ public void StopAdvertising(MockAdvertisingSetDriver set)
set =>
!ReferenceEquals(set.Adapter, central)
&& set.Adapter.IsInRange
- && set.RandomAddress == address
+ // An address without its type, e.g. parsed from text, is looked up by its value
+ && (
+ set.RandomAddress == address
+ || (address.Type is BleAddressType.NotAvailable && set.RandomAddress.Value == address.Value)
+ )
&& set.Type.HasFlag(BleEventType.Connectable)
);
}
diff --git a/src/Darp.Ble.WinRT/BleHelper.cs b/src/Darp.Ble.WinRT/BleHelper.cs
deleted file mode 100644
index 6f5e9289..00000000
--- a/src/Darp.Ble.WinRT/BleHelper.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Darp.Ble.Data;
-using Windows.Devices.Bluetooth;
-
-namespace Darp.Ble.WinRT;
-
-internal static class BleHelper
-{
- public static BleAddress GetBleAddress(ulong winAddress, BluetoothAddressType winAddressType)
- {
- BleAddressType addressType = winAddressType switch
- {
- BluetoothAddressType.Public => BleAddressType.Public,
- BluetoothAddressType.Random => BleAddressType.Random,
- _ => BleAddressType.NotAvailable,
- };
- return new BleAddress(addressType, (UInt48)winAddress);
- }
-}
diff --git a/src/Darp.Ble.WinRT/Connections/AttributeWrappers.cs b/src/Darp.Ble.WinRT/Connections/AttributeWrappers.cs
new file mode 100644
index 00000000..1fba55b7
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Connections/AttributeWrappers.cs
@@ -0,0 +1,68 @@
+namespace Darp.Ble.WinRT.Connections;
+
+///
+/// A connection's wrappers of one kind of native GATT object, one per attribute handle, so every discovery returns
+/// the same ones. A rediscovered attribute keeps its wrapper and native object: Windows may return the object the
+/// wrapper holds, so releasing the new one could close the wrapped one. Once the connection ended it wraps nothing
+/// more: its end hands every wrapper to disposal, and a discovery that completes later releases its native objects instead.
+/// Generic over the native and wrapper types so the bookkeeping builds and is tested without Windows
+///
+internal sealed class AttributeWrappers
+ where TWrapper : class
+{
+ private readonly Func _handleOf;
+ private readonly Func _wrap;
+ private readonly Action _release;
+ private readonly Lock _lock = new();
+ private readonly Dictionary _byHandle = [];
+ private bool _ended;
+
+ /// Starts without wrappers
+ /// The attribute handle of a native object
+ /// Wraps a native object the first time its handle shows up
+ /// Releases a native object of a discovery that completed after the end
+ public AttributeWrappers(Func handleOf, Func wrap, Action release)
+ {
+ _handleOf = handleOf;
+ _wrap = wrap;
+ _release = release;
+ }
+
+ /// The wrappers of what a discovery returned; null once the connection ended
+ public IReadOnlyList? Wrap(IReadOnlyList natives)
+ {
+ // Under the lock, so a wrapper is either part of the end or never created
+ lock (_lock)
+ {
+ if (_ended)
+ {
+ foreach (TNative native in natives)
+ _release(native);
+ return null;
+ }
+ var wrappers = new TWrapper[natives.Count];
+ for (var i = 0; i < natives.Count; i++)
+ {
+ TNative native = natives[i];
+ if (_byHandle.TryGetValue(_handleOf(native), out TWrapper? known))
+ {
+ wrappers[i] = known;
+ continue;
+ }
+ wrappers[i] = _wrap(native);
+ _byHandle.Add(_handleOf(native), wrappers[i]);
+ }
+ return wrappers;
+ }
+ }
+
+ /// The connection ended; returns every wrapper handed out, for the caller to release
+ public IReadOnlyList End()
+ {
+ lock (_lock)
+ {
+ _ended = true;
+ return [.. _byHandle.Values];
+ }
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Connections/CentralSessions.cs b/src/Darp.Ble.WinRT/Connections/CentralSessions.cs
new file mode 100644
index 00000000..3339f609
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Connections/CentralSessions.cs
@@ -0,0 +1,78 @@
+using System.Runtime.CompilerServices;
+
+namespace Darp.Ble.WinRT.Connections;
+
+///
+/// One central per device for as long as its connection lasts, across the GATT sessions Windows hands out. A connection
+/// ends when its last session closed or this app ended it; the device's next activity is a new connection, and late
+/// callbacks of the old sessions are ignored. Not thread-safe: its owner calls it under its own lock.
+/// Generic over the session and central types so the bookkeeping builds and is tested without Windows
+///
+internal sealed class CentralSessions
+ where TSession : class
+ where TCentral : class
+{
+ private readonly Dictionary _byDevice = new(StringComparer.Ordinal);
+ private readonly Dictionary _bySession = new(ReferenceEqualityComparer.Instance);
+
+ // Closed sessions, so their late callbacks do not bring an ended connection back; weak, so they can go away
+ private readonly ConditionalWeakTable _closed = [];
+
+ ///
+ /// The central of the session's device; null for a session that closed. Says whether the central and the session
+ /// are new, so each session can be watched for closing and each central announced once
+ ///
+ public (TCentral? Central, bool IsNewCentral, bool IsNewSession) Resolve(
+ string deviceId,
+ TSession session,
+ Func create
+ )
+ {
+ if (_closed.TryGetValue(session, out _))
+ return (null, false, false);
+ bool isNewCentral = !_byDevice.TryGetValue(deviceId, out Connection? connection);
+ if (connection is null)
+ {
+ connection = new Connection(deviceId, create());
+ _byDevice[deviceId] = connection;
+ }
+ bool isNewSession = connection.Sessions.Add(session);
+ _bySession[session] = connection;
+ return (connection.Central, isNewCentral, isNewSession);
+ }
+
+ /// A session closed; returns the central when that ended its connection
+ public TCentral? Close(TSession session)
+ {
+ _closed.AddOrUpdate(session, session);
+ if (!_bySession.Remove(session, out Connection? connection))
+ return null;
+ connection.Sessions.Remove(session);
+ if (connection.Sessions.Count > 0)
+ return null;
+ _byDevice.Remove(connection.DeviceId);
+ return connection.Central;
+ }
+
+ /// This app ended the central's connection; returns false when it had ended already
+ public bool End(TCentral central)
+ {
+ Connection? connection = _byDevice.Values.FirstOrDefault(c => ReferenceEquals(c.Central, central));
+ if (connection is null)
+ return false;
+ _byDevice.Remove(connection.DeviceId);
+ foreach (TSession session in connection.Sessions)
+ {
+ _bySession.Remove(session);
+ _closed.AddOrUpdate(session, session);
+ }
+ return true;
+ }
+
+ private sealed class Connection(string deviceId, TCentral central)
+ {
+ public string DeviceId { get; } = deviceId;
+ public TCentral Central { get; } = central;
+ public HashSet Sessions { get; } = new(ReferenceEqualityComparer.Instance);
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Darp.Ble.WinRT.csproj b/src/Darp.Ble.WinRT/Darp.Ble.WinRT.csproj
index 04b8218c..8dc58ad7 100644
--- a/src/Darp.Ble.WinRT/Darp.Ble.WinRT.csproj
+++ b/src/Darp.Ble.WinRT/Darp.Ble.WinRT.csproj
@@ -1,24 +1,18 @@
-
+
net10.0;net10.0-windows10.0.19041.0
- true
+ 13
true
-
+
-
-
-
-
-
+
-
- <_Parameter1>TargetFramework
- <_Parameter2>$(TargetFramework)
-
+
+
diff --git a/src/Darp.Ble.WinRT/Gap/PublisherSections.cs b/src/Darp.Ble.WinRT/Gap/PublisherSections.cs
new file mode 100644
index 00000000..619f547a
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Gap/PublisherSections.cs
@@ -0,0 +1,62 @@
+using Darp.Ble.Data.AssignedNumbers;
+using Darp.Ble.Gap;
+
+namespace Darp.Ble.WinRT.Gap;
+
+/// Windows' advertisement publisher sets some data sections itself and refuses them from the app
+///
+internal static class PublisherSections
+{
+ private static readonly HashSet Reserved =
+ [
+ AdTypes.Flags,
+ AdTypes.IncompleteListOf16BitServiceOrServiceClassUuids,
+ AdTypes.CompleteListOf16BitServiceOrServiceClassUuids,
+ AdTypes.IncompleteListOf32BitServiceOrServiceClassUuids,
+ AdTypes.CompleteListOf32BitServiceOrServiceClassUuids,
+ AdTypes.IncompleteListOf128BitServiceOrServiceClassUuids,
+ AdTypes.CompleteListOf128BitServiceOrServiceClassUuids,
+ AdTypes.ShortenedLocalName,
+ AdTypes.CompleteLocalName,
+ AdTypes.TxPowerLevel,
+ AdTypes.ClassOfDevice,
+ AdTypes.SimplePairingHashC192,
+ AdTypes.SimplePairingRandomizerR192,
+ AdTypes.SecurityManagerTkValue,
+ AdTypes.SecurityManagerOutOfBandFlags,
+ AdTypes.PeripheralConnectionIntervalRange,
+ AdTypes.ListOf16BitServiceSolicitationUuids,
+ AdTypes.ListOf32BitServiceSolicitationUuids,
+ AdTypes.ListOf128BitServiceSolicitationUuids,
+ AdTypes.ServiceData16BitUuid,
+ AdTypes.ServiceData32BitUuid,
+ AdTypes.ServiceData128BitUuid,
+ AdTypes.PublicTargetAddress,
+ AdTypes.RandomTargetAddress,
+ AdTypes.Appearance,
+ AdTypes.AdvertisingInterval,
+ AdTypes.LeBluetoothDeviceAddress,
+ AdTypes.LeRole,
+ AdTypes.SimplePairingHashC256,
+ AdTypes.SimplePairingRandomizerR256,
+ AdTypes.ThreeDInformationData,
+ ];
+
+ /// The sections the publisher takes, and the types it would refuse
+ public static (
+ IReadOnlyList<(AdTypes Type, ReadOnlyMemory Bytes)> Kept,
+ IReadOnlyList Dropped
+ ) Split(AdvertisingData data)
+ {
+ List<(AdTypes, ReadOnlyMemory)> kept = [];
+ List dropped = [];
+ foreach ((AdTypes type, ReadOnlyMemory bytes) in data)
+ {
+ if (Reserved.Contains(type))
+ dropped.Add(type);
+ else
+ kept.Add((type, bytes));
+ }
+ return (kept, dropped);
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerCharacteristic.cs b/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerCharacteristic.cs
deleted file mode 100644
index ba306879..00000000
--- a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerCharacteristic.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System.Reactive.Linq;
-using System.Runtime.InteropServices.WindowsRuntime;
-using Darp.Ble.Data;
-using Darp.Ble.Exceptions;
-using Darp.Ble.Gatt.Server;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-using Windows.Foundation;
-
-namespace Darp.Ble.WinRT.Gatt.Server;
-
-internal sealed class WinGattServerCharacteristic(
- GattServerService service,
- GattCharacteristic gattCharacteristic,
- ILogger logger
-)
- : GattServerCharacteristic(
- service,
- gattCharacteristic.AttributeHandle,
- BleUuid.FromGuid(gattCharacteristic.Uuid, inferType: true),
- (GattProperty)gattCharacteristic.CharacteristicProperties,
- logger
- )
-{
- private readonly GattCharacteristic _gattCharacteristic = gattCharacteristic;
-
- ///
- protected override IObservable DiscoverDescriptorsCore()
- {
- return Observable.Create(observer =>
- {
- return Observable
- .FromAsync(token => _gattCharacteristic.GetDescriptorsAsync().AsTask(token))
- .Subscribe(
- result =>
- {
- if (result.Status is not GattCommunicationStatus.Success)
- {
- observer.OnError(
- new Exception(
- $"Could not query new descriptor for characteristic - got result {result.Status} ({result.ProtocolError})"
- )
- );
- return;
- }
- foreach (GattDescriptor descriptor in result.Descriptors)
- {
- observer.OnNext(
- new WinGattServerDescriptor(
- this,
- descriptor,
- ServiceProvider.GetLogger()
- )
- );
- }
- },
- observer.OnError,
- observer.OnCompleted
- );
- });
- }
-
- ///
- protected override async Task WriteAsyncCore(byte[] bytes, CancellationToken cancellationToken)
- {
- GattWriteResult result = await _gattCharacteristic
- .WriteValueWithResultAsync(bytes.AsBuffer(), GattWriteOption.WriteWithResponse)
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (result.Status is GattCommunicationStatus.Success)
- return;
- if (result.Status is GattCommunicationStatus.ProtocolError)
- throw new GattCharacteristicException(
- this,
- $"Could not write because of protocol error {result.ProtocolError}"
- );
- throw new GattCharacteristicException(this, $"Could not write because of {result.Status}");
- }
-
- protected override void WriteWithoutResponseCore(byte[] bytes)
- {
- _ = Task.Run(() => _gattCharacteristic.WriteValueAsync(bytes.AsBuffer(), GattWriteOption.WriteWithoutResponse));
- }
-
- protected override async Task ReadAsyncCore(CancellationToken cancellationToken)
- {
- GattReadResult result = await _gattCharacteristic
- .ReadValueAsync(BluetoothCacheMode.Uncached)
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (result.Status is not GattCommunicationStatus.Success)
- throw new Exception($"Could not read value because of {result.Status} ({result.ProtocolError})");
- return result.Value.ToArray();
- }
-
- protected override async Task EnableNotificationsAsync(
- TState state,
- Action onNotify,
- CancellationToken cancellationToken
- )
- {
- if (!_gattCharacteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify))
- {
- throw new GattCharacteristicException(this, "Characteristic does not support notification");
- }
- GattCommunicationStatus res = await _gattCharacteristic
- .WriteClientCharacteristicConfigurationDescriptorAsync(
- GattClientCharacteristicConfigurationDescriptorValue.Notify
- )
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (res is not GattCommunicationStatus.Success)
- {
- throw new GattCharacteristicException(this, "Could not write notification status to cccd");
- }
-
- return Observable
- .FromEventPattern<
- TypedEventHandler,
- GattCharacteristic,
- GattValueChangedEventArgs
- >(
- addHandler => _gattCharacteristic.ValueChanged += addHandler,
- removeHandler => _gattCharacteristic.ValueChanged -= removeHandler
- )
- .Select(x => x.EventArgs.CharacteristicValue.ToArray())
- .Where(x => x is not null)
- .Subscribe(bytes => onNotify(state, bytes));
- }
-
- protected override async Task DisableNotificationsAsync()
- {
- await _gattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
- GattClientCharacteristicConfigurationDescriptorValue.None
- );
- }
-}
diff --git a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerDescriptor.cs b/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerDescriptor.cs
deleted file mode 100644
index 9c64fbd6..00000000
--- a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerDescriptor.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Runtime.InteropServices.WindowsRuntime;
-using Darp.Ble.Data;
-using Darp.Ble.Gatt.Server;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-
-namespace Darp.Ble.WinRT.Gatt.Server;
-
-internal sealed class WinGattServerDescriptor(
- GattServerCharacteristic characteristic,
- GattDescriptor winDescriptor,
- ILogger logger
-) : GattServerDescriptor(characteristic, BleUuid.FromGuid(winDescriptor.Uuid, inferType: true), logger)
-{
- private readonly GattDescriptor _winDescriptor = winDescriptor;
-
- public override async Task ReadAsync(CancellationToken cancellationToken = default)
- {
- GattReadResult result = await _winDescriptor.ReadValueAsync().AsTask(cancellationToken).ConfigureAwait(false);
- if (result.Status is not GattCommunicationStatus.Success)
- throw new Exception($"Could not read value because of {result.Status} ({result.ProtocolError})");
- return result.Value.ToArray();
- }
-
- public override async Task WriteAsync(byte[] bytes, CancellationToken cancellationToken = default)
- {
- GattWriteResult result = await _winDescriptor
- .WriteValueWithResultAsync(bytes.AsBuffer())
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (result.Status is GattCommunicationStatus.Success)
- return true;
- if (result.Status is GattCommunicationStatus.ProtocolError)
- throw new Exception($"Could not write because of protocol error {result.ProtocolError}");
- throw new Exception($"Could not write because of {result.Status}");
- }
-}
diff --git a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerPeer.cs b/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerPeer.cs
deleted file mode 100644
index fdb75e2f..00000000
--- a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerPeer.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
-using Darp.Ble.Data;
-using Darp.Ble.Gatt;
-using Darp.Ble.Gatt.Server;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-using Windows.Devices.Enumeration;
-using Windows.Foundation;
-
-namespace Darp.Ble.WinRT.Gatt.Server;
-
-internal sealed class WinGattServerPeer : GattServerPeer
-{
- private readonly BluetoothLEDevice _winDev;
-
- internal WinGattServerPeer(WinBleCentral central, BluetoothLEDevice winDev, ILogger logger)
- : base(central, BleHelper.GetBleAddress(winDev.BluetoothAddress, winDev.BluetoothAddressType), logger)
- {
- _winDev = winDev;
- Observable
- .FromEventPattern, BluetoothLEDevice, object>(
- addHandler => winDev.ConnectionStatusChanged += addHandler,
- removeHandler => winDev.ConnectionStatusChanged -= removeHandler
- )
- .Where(x => x.Sender is not null)
- .Select(x =>
- x.Sender!.ConnectionStatus is BluetoothConnectionStatus.Connected
- ? ConnectionStatus.Connected
- : ConnectionStatus.Disconnected
- )
- .Subscribe(ConnectionSubject);
- }
-
- private IObservable DiscoverService(Func> getServices)
- {
- return Observable.Create(
- async (observer, cancellationToken) =>
- {
- DeviceAccessStatus accessStatus = await _winDev
- .RequestAccessAsync()
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (accessStatus is not DeviceAccessStatus.Allowed)
- {
- observer.OnError(new Exception($"Access request disallowed: {accessStatus}..."));
- return Disposable.Empty;
- }
- return Observable
- .FromAsync(token => getServices().AsTask(token))
- .Subscribe(
- result =>
- {
- if (result.Status is not GattCommunicationStatus.Success)
- {
- observer.OnError(
- new Exception(
- $"Could not query new services for device - got result {result.Status} ({result.ProtocolError})"
- )
- );
- return;
- }
-
- foreach (GattDeviceService gattDeviceService in result.Services)
- {
- observer.OnNext(
- new WinGattServerService(
- this,
- gattDeviceService,
- ServiceProvider.GetLogger()
- )
- );
- }
- },
- observer.OnError,
- observer.OnCompleted
- );
- }
- );
- }
-
- ///
- protected override IObservable DiscoverServicesCore()
- {
- return DiscoverService(() => _winDev.GetGattServicesAsync(BluetoothCacheMode.Uncached));
- }
-
- ///
- protected override IObservable DiscoverServiceCore(BleUuid uuid)
- {
- return DiscoverService(() => _winDev.GetGattServicesForUuidAsync(uuid.Value, BluetoothCacheMode.Uncached));
- }
-
- /// Disposes of all Services and the Device. Completes when a disconnection event was received
- protected override async ValueTask DisposeAsyncCore()
- {
- await base.DisposeAsyncCore().ConfigureAwait(false);
- foreach (WinGattServerService service in Services.OfType())
- service.Dispose();
- _winDev.Dispose();
- ConnectionSubject.OnNext(ConnectionStatus.Disconnected);
- }
-}
diff --git a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerService.cs b/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerService.cs
deleted file mode 100644
index 0c5fec75..00000000
--- a/src/Darp.Ble.WinRT/Gatt/Server/WinGattServerService.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
-using Darp.Ble.Data;
-using Darp.Ble.Gatt;
-using Darp.Ble.Gatt.Server;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-using Windows.Devices.Enumeration;
-using Windows.Foundation;
-
-namespace Darp.Ble.WinRT.Gatt.Server;
-
-internal sealed class WinGattServerService(
- GattServerPeer peer,
- GattDeviceService winService,
- ILogger logger
-) : GattServerService(peer, BleUuid.FromGuid(winService.Uuid, inferType: true), GattServiceType.Undefined, logger)
-{
- private readonly GattDeviceService _winService = winService;
-
- private IObservable DiscoverCharacteristic(
- Func> getServices
- )
- {
- return Observable.Create(
- async (observer, cancellationToken) =>
- {
- DeviceAccessStatus accessStatus = await _winService
- .RequestAccessAsync()
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (accessStatus is not DeviceAccessStatus.Allowed)
- {
- observer.OnError(new Exception($"Access request disallowed: {accessStatus}..."));
- return Disposable.Empty;
- }
- return Observable
- .FromAsync(token => getServices().AsTask(token))
- .Subscribe(
- result =>
- {
- if (result.Status is not GattCommunicationStatus.Success)
- {
- observer.OnError(
- new Exception(
- $"Could not query new services for device - got result {result.Status} ({result.ProtocolError})"
- )
- );
- return;
- }
-
- foreach (GattCharacteristic gattCharacteristic in result.Characteristics)
- {
- observer.OnNext(
- new WinGattServerCharacteristic(
- this,
- gattCharacteristic,
- ServiceProvider.GetLogger()
- )
- );
- }
- },
- observer.OnError,
- observer.OnCompleted
- );
- }
- );
- }
-
- ///
- protected override IObservable DiscoverCharacteristicsCore()
- {
- return DiscoverCharacteristic(() => _winService.GetCharacteristicsAsync());
- }
-
- ///
- protected override IObservable DiscoverCharacteristicsCore(BleUuid uuid)
- {
- return DiscoverCharacteristic(() => _winService.GetCharacteristicsForUuidAsync(uuid.Value));
- }
-
- /// Dispose the underlying Windows service. Required for successful disconnection!
- internal void Dispose() => _winService.Dispose();
-}
diff --git a/src/Darp.Ble.WinRT/Gatt/SubscriptionSnapshots.cs b/src/Darp.Ble.WinRT/Gatt/SubscriptionSnapshots.cs
new file mode 100644
index 00000000..47df07ed
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Gatt/SubscriptionSnapshots.cs
@@ -0,0 +1,46 @@
+namespace Darp.Ble.WinRT.Gatt;
+
+///
+/// Windows reports the subscribed clients of a characteristic as a whole list on every change, also for raw CCCD writes.
+/// This turns those lists into who subscribed and who left, per characteristic
+///
+internal sealed class SubscriptionSnapshots
+ where TCharacteristic : notnull
+ where TClient : notnull
+{
+ private readonly Lock _lock = new();
+ private readonly Dictionary> _subscribed = [];
+
+ /// Takes the characteristic's current list; repeating it reports nothing
+ public (IReadOnlyList Added, IReadOnlyList Removed) Update(
+ TCharacteristic characteristic,
+ IEnumerable subscribed
+ )
+ {
+ HashSet current = [.. subscribed];
+ lock (_lock)
+ {
+ HashSet previous = _subscribed.GetValueOrDefault(characteristic) ?? [];
+ TClient[] added = [.. current.Where(client => !previous.Contains(client))];
+ TClient[] removed = [.. previous.Where(client => !current.Contains(client))];
+ _subscribed[characteristic] = current;
+ return (added, removed);
+ }
+ }
+
+ public bool IsSubscribed(TCharacteristic characteristic, TClient client)
+ {
+ lock (_lock)
+ return _subscribed.GetValueOrDefault(characteristic)?.Contains(client) is true;
+ }
+
+ /// The client's connection ended: it is subscribed nowhere, and a later list with it counts as new
+ public void Forget(TClient client)
+ {
+ lock (_lock)
+ {
+ foreach (HashSet clients in _subscribed.Values)
+ clients.Remove(client);
+ }
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Gatt/WinGattClientCharacteristic.cs b/src/Darp.Ble.WinRT/Gatt/WinGattClientCharacteristic.cs
deleted file mode 100644
index aa6a423b..00000000
--- a/src/Darp.Ble.WinRT/Gatt/WinGattClientCharacteristic.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System.Runtime.InteropServices.WindowsRuntime;
-using Darp.Ble.Data;
-using Darp.Ble.Gatt.Att;
-using Darp.Ble.Gatt.Client;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-using Windows.Foundation;
-using Windows.Storage.Streams;
-
-namespace Darp.Ble.WinRT.Gatt;
-
-internal sealed class WinGattClientCharacteristic : GattClientCharacteristic
-{
- public new WinGattClientService Service { get; }
- private readonly GattLocalCharacteristic _winCharacteristic;
-
- public WinGattClientCharacteristic(
- WinGattClientService winService,
- GattLocalCharacteristic winCharacteristic,
- IGattCharacteristicValue value,
- ILogger logger
- )
- : base(winService, (GattProperty)winCharacteristic.CharacteristicProperties, value, logger)
- {
- Service = winService;
- _winCharacteristic = winCharacteristic;
- winCharacteristic.ReadRequested += async (_, args) =>
- {
- using Deferral deferral = args.GetDeferral();
- GattReadRequest? request = await args.GetRequestAsync().AsTask().ConfigureAwait(false);
- try
- {
- IGattClientPeer peerClient = Service.Peripheral.GetOrRegisterSession(args.Session);
- byte[] readValue = await Value.ReadValueAsync(peerClient).ConfigureAwait(false);
- request.RespondWithValue(readValue.AsBuffer());
- }
- catch
- {
- request.RespondWithProtocolError(123);
- }
- };
- winCharacteristic.WriteRequested += async (_, args) =>
- {
- using Deferral deferral = args.GetDeferral();
- GattWriteRequest request = await args.GetRequestAsync().AsTask().ConfigureAwait(false);
- try
- {
- IGattClientPeer peerClient = Service.Peripheral.GetOrRegisterSession(args.Session);
- DataReader reader = DataReader.FromBuffer(request.Value);
- byte[] bytes = reader.DetachBuffer().ToArray();
- GattProtocolStatus status = await Value.WriteValueAsync(peerClient, bytes).ConfigureAwait(false);
- if (request.Option == GattWriteOption.WriteWithResponse)
- {
- if (status is GattProtocolStatus.Success)
- request.Respond();
- else
- request.RespondWithProtocolError((byte)status);
- }
- }
- catch
- {
- // ignored
- }
- };
- }
-
- protected override void OnDescriptorAdded(IGattCharacteristicValue value)
- {
- BleUuid uuid = value.AttributeType;
- GattLocalDescriptorResult result = _winCharacteristic
- .CreateDescriptorAsync(uuid.Value, new GattLocalDescriptorParameters())
- .GetResults();
- if (result.Error is not BluetoothError.Success)
- throw new Exception("Could not add descriptor to windows");
- var winDescriptor = result.Descriptor;
- winDescriptor.ReadRequested += async (_, args) =>
- {
- using Deferral deferral = args.GetDeferral();
- GattReadRequest? request = await args.GetRequestAsync().AsTask().ConfigureAwait(false);
- try
- {
- IGattClientPeer peerClient = Service.Peripheral.GetOrRegisterSession(args.Session);
- byte[] readValue = await Value.ReadValueAsync(peerClient).ConfigureAwait(false);
- request.RespondWithValue(readValue.AsBuffer());
- }
- catch
- {
- request.RespondWithProtocolError(123);
- }
- };
- winDescriptor.WriteRequested += async (_, args) =>
- {
- using Deferral deferral = args.GetDeferral();
- GattWriteRequest request = await args.GetRequestAsync().AsTask().ConfigureAwait(false);
- try
- {
- IGattClientPeer peerClient = Service.Peripheral.GetOrRegisterSession(args.Session);
- DataReader reader = DataReader.FromBuffer(request.Value);
- byte[] bytes = reader.DetachBuffer().ToArray();
- GattProtocolStatus status = await Value.WriteValueAsync(peerClient, bytes).ConfigureAwait(false);
- if (request.Option == GattWriteOption.WriteWithResponse)
- {
- if (status is GattProtocolStatus.Success)
- request.Respond();
- else
- request.RespondWithProtocolError((byte)status);
- }
- }
- catch
- {
- // ignored
- }
- };
- }
-
- protected override async ValueTask NotifyAsyncCore(IGattClientPeer clientPeer, byte[] value)
- {
- GattSubscribedClient? subscribedClient = _winCharacteristic.SubscribedClients.FirstOrDefault(x =>
- {
- BleAddress address = BleAddress.Parse(x.Session.DeviceId.Id[^17..], provider: null);
- return address == clientPeer.Address;
- });
- if (subscribedClient is null)
- return;
-
- await _winCharacteristic
- .NotifyValueAsync(value.AsBuffer(), subscribedClient)
- .AsTask(CancellationToken.None)
- .ConfigureAwait(false);
- }
-
- protected override async Task IndicateAsyncCore(
- IGattClientPeer clientPeer,
- byte[] value,
- CancellationToken cancellationToken
- )
- {
- GattSubscribedClient? subscribedClient = _winCharacteristic.SubscribedClients.FirstOrDefault(x =>
- {
- BleAddress address = BleAddress.Parse(x.Session.DeviceId.Id[^17..], provider: null);
- return address == clientPeer.Address;
- });
- if (subscribedClient is null)
- return;
-
- GattClientNotificationResult result = await _winCharacteristic
- .NotifyValueAsync(value.AsBuffer(), subscribedClient)
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (result.Status is not GattCommunicationStatus.Success)
- throw new Exception("Hey thats not good :(");
- }
-}
diff --git a/src/Darp.Ble.WinRT/Gatt/WinGattClientPeer.cs b/src/Darp.Ble.WinRT/Gatt/WinGattClientPeer.cs
deleted file mode 100644
index e24317c8..00000000
--- a/src/Darp.Ble.WinRT/Gatt/WinGattClientPeer.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Reactive;
-using System.Reactive.Linq;
-using Darp.Ble.Data;
-using Darp.Ble.Gatt.Client;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-using Windows.Foundation;
-
-namespace Darp.Ble.WinRT.Gatt;
-
-internal sealed class WinGattClientPeer(
- WinBlePeripheral peripheral,
- GattSession session,
- BleAddress address,
- ILogger logger
-) : GattClientPeer(peripheral, address, logger)
-{
- private readonly GattSession _session = session;
-
- public override bool IsConnected => _session.SessionStatus is GattSessionStatus.Active;
-
- public override IObservable WhenDisconnected =>
- Observable
- .FromEventPattern<
- TypedEventHandler,
- GattSession,
- GattSessionStatusChangedEventArgs
- >(
- addHandler => _session.SessionStatusChanged += addHandler,
- removeHandler => _session.SessionStatusChanged -= removeHandler
- )
- .Where(x => x.EventArgs.Status is GattSessionStatus.Closed)
- .Select(_ => Unit.Default);
-}
diff --git a/src/Darp.Ble.WinRT/Gatt/WinGattClientService.cs b/src/Darp.Ble.WinRT/Gatt/WinGattClientService.cs
deleted file mode 100644
index 8e0b93a6..00000000
--- a/src/Darp.Ble.WinRT/Gatt/WinGattClientService.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Runtime.InteropServices.WindowsRuntime;
-using Darp.Ble.Data;
-using Darp.Ble.Data.AssignedNumbers;
-using Darp.Ble.Gap;
-using Darp.Ble.Gatt;
-using Darp.Ble.Gatt.Att;
-using Darp.Ble.Gatt.Client;
-using Darp.Ble.Gatt.Server;
-using Darp.Ble.Utils;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-
-namespace Darp.Ble.WinRT.Gatt;
-
-internal sealed class WinGattClientService(
- WinBlePeripheral peripheral,
- GattServiceProvider provider,
- ILogger logger
-)
- : GattClientService(
- peripheral,
- BleUuid.FromGuid(provider.Service.Uuid, inferType: true),
- GattServiceType.Undefined,
- logger
- )
-{
- private readonly GattServiceProvider _serviceProvider = provider;
- private readonly GattLocalService _winService = provider.Service;
- public new WinBlePeripheral Peripheral { get; } = peripheral;
-
- protected override GattClientCharacteristic CreateCharacteristicCore(
- GattProperty properties,
- IGattCharacteristicValue value
- )
- {
- GattLocalCharacteristicResult result = _winService
- .CreateCharacteristicAsync(
- value.AttributeType.Value,
- new GattLocalCharacteristicParameters
- {
- CharacteristicProperties = (GattCharacteristicProperties)properties,
- ReadProtectionLevel = GattProtectionLevel.EncryptionAndAuthenticationRequired,
- }
- )
- .GetResults();
- if (result.Error is not BluetoothError.Success)
- throw new Exception("Nopiii");
- result.Characteristic.SubscribedClientsChanged += (sender, _) =>
- {
- foreach (GattSubscribedClient senderSubscribedClient in sender.SubscribedClients)
- {
- Peripheral.GetOrRegisterSession(senderSubscribedClient.Session);
- }
- };
- return new WinGattClientCharacteristic(
- this,
- result.Characteristic,
- value,
- ServiceProvider.GetLogger()
- );
- }
-
- public IAsyncDisposable Advertise(IAdvertisingSet advertisingSet)
- {
- AdvertisingParameters parameters = advertisingSet.Parameters;
- var winParameters = new GattServiceProviderAdvertisingParameters();
- if (parameters.Type.HasFlag(BleEventType.Connectable))
- winParameters.IsConnectable = true;
- winParameters.IsDiscoverable = true;
- if (advertisingSet.Data.TryGetFirstType(AdTypes.ServiceData16BitUuid, out ReadOnlyMemory memory))
- {
- winParameters.ServiceData = memory.ToArray().AsBuffer();
- }
- _serviceProvider.StartAdvertising(winParameters);
- return AsyncDisposable.Create(
- _serviceProvider,
- provider =>
- {
- provider.StopAdvertising();
- return ValueTask.CompletedTask;
- }
- );
- }
-}
diff --git a/src/Darp.Ble.WinRT/InternalHelpers.cs b/src/Darp.Ble.WinRT/InternalHelpers.cs
deleted file mode 100644
index e17af8dd..00000000
--- a/src/Darp.Ble.WinRT/InternalHelpers.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-
-namespace Darp.Ble.WinRT;
-
-internal static class InternalHelpers
-{
- public static ILogger GetLogger(this IServiceProvider serviceProvider)
- {
- return serviceProvider.GetService>() ?? NullLogger.Instance;
- }
-}
diff --git a/src/Darp.Ble.WinRT/Logging.cs b/src/Darp.Ble.WinRT/Logging.cs
index 4587ade3..c1d05cb5 100644
--- a/src/Darp.Ble.WinRT/Logging.cs
+++ b/src/Darp.Ble.WinRT/Logging.cs
@@ -1,22 +1,30 @@
-using Darp.Ble.Data.AssignedNumbers;
using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.Advertisement;
namespace Darp.Ble.WinRT;
internal static partial class Logging
{
[LoggerMessage(
+ EventId = 3000,
+ EventName = "NotOnWindows",
Level = LogLevel.Warning,
- Message = "Ignoring data section {AdType}. This type is reserved by Windows"
+ Message = "The WinRT backend has no adapter: the app does not run on Windows 10 2004 or later, or was built without its Windows target"
)]
- public static partial void LogIgnoreDataSectionReservedType(this ILogger logger, AdTypes adType);
+ public static partial void LogNotOnWindows(this ILogger logger);
- [LoggerMessage(Level = LogLevel.Warning, Message = "Publisher status changed to {Status} with {Error}")]
- public static partial void LogPublisherChangedToError(
- this ILogger logger,
- BluetoothLEAdvertisementPublisherStatus status,
- BluetoothError error
- );
+ [LoggerMessage(
+ EventId = 3001,
+ EventName = "ReservedSectionDropped",
+ Level = LogLevel.Warning,
+ Message = "Windows sets advertising data section {AdType} itself; the app's section is left out"
+ )]
+ public static partial void LogReservedSectionDropped(this ILogger logger, byte adType);
+
+ [LoggerMessage(
+ EventId = 3002,
+ EventName = "HostedRequestFailed",
+ Level = LogLevel.Warning,
+ Message = "A central's request to a hosted service could not be answered"
+ )]
+ public static partial void LogHostedRequestFailed(this ILogger logger, Exception exception);
}
diff --git a/src/Darp.Ble.WinRT/PublicAPI.Shipped.txt b/src/Darp.Ble.WinRT/PublicAPI.Shipped.txt
new file mode 100644
index 00000000..7dc5c581
--- /dev/null
+++ b/src/Darp.Ble.WinRT/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Darp.Ble.WinRT/PublicAPI.Unshipped.txt b/src/Darp.Ble.WinRT/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000..ec8dc36f
--- /dev/null
+++ b/src/Darp.Ble.WinRT/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Darp.Ble.WinRT.WinRTBleManagerBuilderExtensions
+static Darp.Ble.WinRT.WinRTBleManagerBuilderExtensions.AddWinRT(this Darp.Ble.BleManagerBuilder! builder) -> Darp.Ble.BleManagerBuilder!
diff --git a/src/Darp.Ble.WinRT/WinAdvertisingSet.cs b/src/Darp.Ble.WinRT/WinAdvertisingSet.cs
deleted file mode 100644
index e747e6c2..00000000
--- a/src/Darp.Ble.WinRT/WinAdvertisingSet.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Darp.Ble.Data;
-using Darp.Ble.Gap;
-using Darp.Ble.Implementation;
-
-namespace Darp.Ble.WinRT;
-
-/// The windows implementation of the advertising set
-///
-public sealed class WinAdvertisingSet(BleBroadcaster broadcaster) : AdvertisingSet(broadcaster)
-{
- ///
- public override Task SetRandomAddressAsync(BleAddress randomAddress, CancellationToken cancellationToken = default)
- {
- throw new NotSupportedException();
- }
-
- ///
- public override Task SetScanResponseDataAsync(
- AdvertisingData scanResponseData,
- CancellationToken cancellationToken = default
- )
- {
- throw new NotSupportedException();
- }
-}
diff --git a/src/Darp.Ble.WinRT/WinBleBroadcaster.cs b/src/Darp.Ble.WinRT/WinBleBroadcaster.cs
deleted file mode 100644
index d517f2b6..00000000
--- a/src/Darp.Ble.WinRT/WinBleBroadcaster.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using System.Collections.Concurrent;
-using System.Runtime.InteropServices.WindowsRuntime;
-using Darp.Ble.Data;
-using Darp.Ble.Data.AssignedNumbers;
-using Darp.Ble.Gap;
-using Darp.Ble.Gatt.Server;
-using Darp.Ble.Implementation;
-using Darp.Ble.Utils;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.Advertisement;
-
-namespace Darp.Ble.WinRT;
-
-internal sealed class WinBleBroadcaster(WinBleDevice winBleDevice, ILogger logger)
- : BleBroadcaster(winBleDevice, logger)
-{
- private readonly WinBleDevice _winBleDevice = winBleDevice;
- private readonly ConcurrentDictionary _publishers = new();
-
- public override bool IsAdvertising => _publishers.All(x => x.Key.IsAdvertising);
-
- protected override Task CreateAdvertisingSetAsyncCore(
- AdvertisingParameters? parameters,
- AdvertisingData? data,
- AdvertisingData? scanResponseData,
- CancellationToken cancellationToken
- )
- {
- return Task.FromResult(new WinAdvertisingSet(this));
- }
-
- protected override Task StartAdvertisingCoreAsync(
- IReadOnlyCollection<(IAdvertisingSet AdvertisingSet, TimeSpan Duration, byte NumberOfEvents)> advertisingSets,
- CancellationToken cancellationToken
- )
- {
- List disposables = [];
- foreach ((IAdvertisingSet advertisingSet, TimeSpan duration, int numberOfEvents) in advertisingSets)
- {
- if (_publishers.ContainsKey(advertisingSet))
- continue;
- if (_winBleDevice.Capabilities.HasFlag(Capabilities.Peripheral))
- {
- var peripheral = (WinBlePeripheral)_winBleDevice.Peripheral;
- if (peripheral.Services.Count > 0)
- {
- IAsyncDisposable d = peripheral.AdvertiseServices(advertisingSet);
- if (duration > TimeSpan.Zero)
- {
- var source = new CancellationTokenSource(duration);
- source.Token.Register(async () => await d.DisposeAsync());
- }
- if (numberOfEvents > 0)
- {
- ArgumentOutOfRangeException.ThrowIfGreaterThan(numberOfEvents, 0);
- }
- disposables.Add(d);
- continue;
- }
- }
- var publisher = new BluetoothLEAdvertisementPublisher();
-
- foreach ((AdTypes type, ReadOnlyMemory bytes) in advertisingSet.Data)
- {
- // Reserved types: https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher?view=winrt-22621
- if (
- type
- is AdTypes.Flags
- or AdTypes.IncompleteListOf16BitServiceOrServiceClassUuids
- or AdTypes.CompleteListOf16BitServiceOrServiceClassUuids
- or AdTypes.IncompleteListOf32BitServiceOrServiceClassUuids
- or AdTypes.CompleteListOf32BitServiceOrServiceClassUuids
- or AdTypes.IncompleteListOf128BitServiceOrServiceClassUuids
- or AdTypes.CompleteListOf128BitServiceOrServiceClassUuids
- or AdTypes.ShortenedLocalName
- or AdTypes.CompleteLocalName
- or AdTypes.TxPowerLevel
- or AdTypes.ClassOfDevice
- or AdTypes.SimplePairingHashC192
- or AdTypes.SimplePairingRandomizerR192
- or AdTypes.SecurityManagerTkValue
- or AdTypes.SecurityManagerOutOfBandFlags
- or AdTypes.PeripheralConnectionIntervalRange
- or AdTypes.ListOf16BitServiceSolicitationUuids
- or AdTypes.ListOf32BitServiceSolicitationUuids
- or AdTypes.ListOf128BitServiceSolicitationUuids
- or AdTypes.ServiceData16BitUuid
- or AdTypes.ServiceData32BitUuid
- or AdTypes.ServiceData128BitUuid
- or AdTypes.PublicTargetAddress
- or AdTypes.RandomTargetAddress
- or AdTypes.Appearance
- or AdTypes.AdvertisingInterval
- or AdTypes.LeBluetoothDeviceAddress
- or AdTypes.LeRole
- or AdTypes.SimplePairingHashC256
- or AdTypes.SimplePairingRandomizerR256
- or AdTypes.ThreeDInformationData
- )
- {
- Logger?.LogIgnoreDataSectionReservedType(type);
- continue;
- }
- publisher.Advertisement.DataSections.Add(
- new BluetoothLEAdvertisementDataSection((byte)type, bytes.ToArray().AsBuffer())
- );
- }
-
- if (!advertisingSet.Parameters.Type.HasFlag(BleEventType.Legacy))
- {
- publisher.UseExtendedAdvertisement = true;
- }
- if (advertisingSet.Parameters.AdvertisingTxPower is not TxPowerLevel.NotAvailable)
- {
- publisher.IncludeTransmitPowerLevel = true;
- publisher.PreferredTransmitPowerLevelInDBm = (short)advertisingSet.Parameters.AdvertisingTxPower;
- }
-
- IAsyncDisposable disposable = AsyncDisposable.Create(publisher, state => state.Stop());
- // //publisher.UseExtendedAdvertisement = true;
- // //publisher.IncludeTransmitPowerLevel = true;
- publisher.StatusChanged += async (_, args) =>
- {
- if (args.Error is BluetoothError.Success)
- return;
- Logger?.LogPublisherChangedToError(args.Status, args.Error);
- await disposable.DisposeAsync().ConfigureAwait(false);
- };
- if (_publishers.TryAdd(advertisingSet, publisher))
- {
- publisher.Start();
- disposables.Add(disposable);
- }
- }
- IAsyncDisposable combinedDisposable = AsyncDisposable.Create(
- disposables,
- async x =>
- {
- foreach (IAsyncDisposable asyncDisposable in x)
- {
- await asyncDisposable.DisposeAsync().ConfigureAwait(false);
- }
- }
- );
- return Task.FromResult(combinedDisposable);
- }
-
- protected override Task StopAdvertisingCoreAsync(
- IReadOnlyCollection advertisingSets,
- CancellationToken cancellationToken
- )
- {
- foreach (IAdvertisingSet advertisingSet in advertisingSets)
- {
- if (_publishers.TryGetValue(advertisingSet, out BluetoothLEAdvertisementPublisher? publisher))
- {
- publisher.Stop();
- }
- }
- return Task.FromResult(true);
- }
-}
diff --git a/src/Darp.Ble.WinRT/WinBleCentral.cs b/src/Darp.Ble.WinRT/WinBleCentral.cs
deleted file mode 100644
index bc831dba..00000000
--- a/src/Darp.Ble.WinRT/WinBleCentral.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Reactive.Linq;
-using Darp.Ble.Data;
-using Darp.Ble.Gatt.Server;
-using Darp.Ble.Implementation;
-using Darp.Ble.WinRT.Gatt.Server;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-
-namespace Darp.Ble.WinRT;
-
-///
-internal sealed class WinBleCentral(BleDevice device, ILogger logger) : BleCentral(device, logger)
-{
- ///
- protected override IObservable ConnectToPeripheralCore(
- BleAddress address,
- BleConnectionParameters connectionParameters,
- BleObservationParameters observationParameters
- )
- {
- return Observable.Create(
- async (observer, cancellationToken) =>
- {
- BluetoothLEDevice? winDev = await BluetoothLEDevice
- .FromBluetoothAddressAsync(
- address.Value,
- address.Type switch
- {
- BleAddressType.Public or BleAddressType.PublicIdentity => BluetoothAddressType.Public,
- BleAddressType.NotAvailable => BluetoothAddressType.Unspecified,
- _ => BluetoothAddressType.Random,
- }
- )
- .AsTask(cancellationToken)
- .ConfigureAwait(false);
- if (winDev is null)
- {
- observer.OnError(new Exception("PeripheralConnection: Failed!"));
- return;
- }
- observer.OnNext(new WinGattServerPeer(this, winDev, ServiceProvider.GetLogger()));
- }
- );
- }
-}
diff --git a/src/Darp.Ble.WinRT/WinBleDevice.cs b/src/Darp.Ble.WinRT/WinBleDevice.cs
deleted file mode 100644
index 9b390b9d..00000000
--- a/src/Darp.Ble.WinRT/WinBleDevice.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Darp.Ble.Data;
-using Darp.Ble.Data.AssignedNumbers;
-using Darp.Ble.Implementation;
-using Windows.Devices.Bluetooth;
-
-namespace Darp.Ble.WinRT;
-
-/// Provides windows specific implementation of a ble device
-internal sealed class WinBleDevice(IServiceProvider serviceProvider, string name)
- : BleDevice(serviceProvider, serviceProvider.GetLogger())
-{
- private BluetoothAdapter? _adapter;
- public override BleAddress RandomAddress =>
- _adapter?.BluetoothAddress is null
- ? BleAddress.NotAvailable
- : new BleAddress(BleAddressType.Random, (UInt48)_adapter.BluetoothAddress);
-
- protected override Task SetRandomAddressAsyncCore(BleAddress randomAddress, CancellationToken cancellationToken)
- {
- throw new NotSupportedException();
- }
-
- ///
- protected override async Task InitializeAsyncCore(CancellationToken cancellationToken)
- {
- _adapter = await BluetoothAdapter.GetDefaultAsync();
- if (!_adapter.IsLowEnergySupported)
- return InitializeResult.DeviceVersionUnsupported;
- Observer = new WinBleObserver(this, ServiceProvider.GetLogger());
- if (_adapter.IsCentralRoleSupported)
- Central = new WinBleCentral(this, ServiceProvider.GetLogger());
- if (_adapter.IsAdvertisementOffloadSupported)
- Broadcaster = new WinBleBroadcaster(this, ServiceProvider.GetLogger());
- if (_adapter.IsPeripheralRoleSupported)
- Peripheral = new WinBlePeripheral(this, ServiceProvider.GetLogger());
- return InitializeResult.Success;
- }
-
- ///
- public override string? Name { get; set; } = name;
-
- ///
- public override AppearanceValues Appearance { get; set; } = AppearanceValues.Computer;
-
- ///
- public override string Identifier => BleDeviceIdentifiers.WinRT;
-}
diff --git a/src/Darp.Ble.WinRT/WinBleFactory.cs b/src/Darp.Ble.WinRT/WinBleFactory.cs
deleted file mode 100644
index cfc06d53..00000000
--- a/src/Darp.Ble.WinRT/WinBleFactory.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System.Runtime.Versioning;
-#if !WINDOWS
-using System.Reflection;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-#endif
-
-namespace Darp.Ble.WinRT;
-
-/// Search for the default windows ble device
-/// If used in an environment which is not build for windows, this becomes a NoOp
-[SupportedOSPlatform("windows")]
-public sealed partial class WinBleFactory : IBleFactory
-{
- /// The name of the resulting device
- public string Name { get; set; } = "Windows";
-
- ///
- public IEnumerable EnumerateDevices(IServiceProvider serviceProvider)
- {
-#if WINDOWS
- yield return new WinBleDevice(serviceProvider, Name);
-#else
- var logger = serviceProvider.GetService>();
- if (logger is not null)
- LogNoWindowsUnintentionalUse(logger, nameof(WinBleFactory), GetTargetFrameworkMoniker());
- yield break;
-#endif
- }
-
-#if !WINDOWS
- [LoggerMessage(
- Level = LogLevel.Warning,
- Message = "Using the {FactoryName} while not compiled for Windows is probably unintended! You compiled for {TargetFrameworkName}"
- )]
- private static partial void LogNoWindowsUnintentionalUse(
- ILogger logger,
- string factoryName,
- string? targetFrameworkName
- );
-
- private static string? GetTargetFrameworkMoniker()
- {
- Assembly assembly = typeof(WinBleFactory).Assembly;
- AssemblyMetadataAttribute? meta = assembly
- .GetCustomAttributes()
- .FirstOrDefault(a => a.Key.Equals("TargetFramework", StringComparison.Ordinal));
- return meta?.Value;
- }
-#endif
-}
diff --git a/src/Darp.Ble.WinRT/WinBleFactoryExtensions.cs b/src/Darp.Ble.WinRT/WinBleFactoryExtensions.cs
deleted file mode 100644
index ab28b172..00000000
--- a/src/Darp.Ble.WinRT/WinBleFactoryExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Runtime.Versioning;
-
-namespace Darp.Ble.WinRT;
-
-/// Extension methods for easier addition of a
-public static class WinBleFactoryExtensions
-{
- /// Add a new and configure it.
- /// An optional callback to configure the factory
- /// The callback to configure the factory
- /// The
- /// If used in an environment which is not build for windows, this becomes a NoOp
- [SupportedOSPlatform("windows")]
- public static BleManagerBuilder AddWinRT(this BleManagerBuilder builder, Action? configure = null)
- {
- ArgumentNullException.ThrowIfNull(builder);
- return builder.Add(configure);
- }
-}
diff --git a/src/Darp.Ble.WinRT/WinBleObserver.cs b/src/Darp.Ble.WinRT/WinBleObserver.cs
deleted file mode 100644
index e7351e71..00000000
--- a/src/Darp.Ble.WinRT/WinBleObserver.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Reactive;
-using System.Reactive.Linq;
-using System.Runtime.InteropServices.WindowsRuntime;
-using Darp.Ble.Data;
-using Darp.Ble.Data.AssignedNumbers;
-using Darp.Ble.Exceptions;
-using Darp.Ble.Gap;
-using Darp.Ble.Implementation;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.Advertisement;
-using Windows.Foundation;
-
-namespace Darp.Ble.WinRT;
-
-///
-internal sealed class WinBleObserver(BleDevice device, ILogger logger) : BleObserver(device, logger)
-{
- private BluetoothLEAdvertisementWatcher? _watcher;
- private IDisposable? _observableSubscription;
-
- [MemberNotNull(nameof(_watcher))]
- private void CreateScanners(ScanType mode)
- {
- _watcher = new BluetoothLEAdvertisementWatcher
- {
- ScanningMode = mode switch
- {
- ScanType.Passive => BluetoothLEScanningMode.Passive,
- ScanType.Active => BluetoothLEScanningMode.Active,
- _ => throw new ArgumentOutOfRangeException(nameof(mode)),
- },
- AllowExtendedAdvertisements = true,
- };
- // Subscription needed for the watcher to start (observable will only subscribe later)
- _watcher.Received += (_, _) => { };
- _watcher.Stopped += (_, _) => { };
- }
-
- protected override Task StartObservingAsyncCore(CancellationToken cancellationToken)
- {
- CreateScanners(Parameters.ScanType);
- _watcher.Start();
- if (_watcher.Status is BluetoothLEAdvertisementWatcherStatus.Aborted)
- {
- throw new BleObservationStartException(
- this,
- $"Watcher status is '{_watcher.Status}' but should be 'Created'.\nTry restarting the bluetooth adapter!"
- );
- }
- _watcher.Stopped += async (_, args) =>
- {
- if (args.Error is BluetoothError.Success)
- return;
- _observableSubscription?.Dispose();
- _watcher = null;
- await OnErrorAsync(
- new BleObservationException(this, $"Watcher stopped with error {args.Error}", innerException: null)
- )
- .ConfigureAwait(false);
- };
- _observableSubscription = Observable
- .FromEventPattern<
- TypedEventHandler,
- BluetoothLEAdvertisementWatcher,
- BluetoothLEAdvertisementReceivedEventArgs
- >(addHandler => _watcher.Received += addHandler, removeHandler => _watcher.Received -= removeHandler)
- .Select(adv => OnAdvertisementReport(this, adv))
- .Subscribe(adv => OnNext(adv));
- return Task.CompletedTask;
- }
-
- protected override Task StopObservingAsyncCore()
- {
- _observableSubscription?.Dispose();
- _watcher?.Stop();
- _watcher = null;
- return Task.CompletedTask;
- }
-
- protected override async ValueTask DisposeAsyncCore()
- {
- _observableSubscription?.Dispose();
- _watcher?.Stop();
- await base.DisposeAsyncCore().ConfigureAwait(false);
- }
-
- private static GapAdvertisement OnAdvertisementReport(
- BleObserver bleObserver,
- EventPattern gapEvt
- )
- {
- BluetoothLEAdvertisementReceivedEventArgs eventArgs = gapEvt.EventArgs;
-
- BleEventType advertisementType = eventArgs.AdvertisementType switch
- {
- BluetoothLEAdvertisementType.ConnectableUndirected => BleEventType.AdvInd,
- BluetoothLEAdvertisementType.ConnectableDirected => BleEventType.AdvDirectInd,
- BluetoothLEAdvertisementType.ScannableUndirected => BleEventType.AdvScanInd,
- BluetoothLEAdvertisementType.NonConnectableUndirected => BleEventType.AdvNonConnInd,
- // A legacy scan response answers ADV_IND (connectable) or ADV_SCAN_IND, both scannable
- BluetoothLEAdvertisementType.ScanResponse => BleEventType.Legacy
- | BleEventType.Scannable
- | BleEventType.ScanResponse
- | (eventArgs.IsConnectable ? BleEventType.Connectable : BleEventType.None),
- _ => GetExtendedEventType(eventArgs),
- };
-
- (AdTypes, ReadOnlyMemory)[] pduData = eventArgs
- .Advertisement.DataSections.Select(section =>
- ((AdTypes)section.DataType, (ReadOnlyMemory)section.Data.ToArray())
- )
- .ToArray();
-
- GapAdvertisement advertisement = GapAdvertisement.FromExtendedAdvertisingReport(
- bleObserver,
- eventArgs.Timestamp,
- advertisementType,
- BleHelper.GetBleAddress(eventArgs.BluetoothAddress, eventArgs.BluetoothAddressType),
- Physical.NotAvailable,
- Physical.NotAvailable,
- AdvertisingSId.NoAdIProvided,
- (TxPowerLevel?)eventArgs.TransmitPowerLevelInDBm ?? TxPowerLevel.NotAvailable,
- (Rssi)eventArgs.RawSignalStrengthInDBm,
- PeriodicAdvertisingInterval.NoPeriodicAdvertising,
- new BleAddress(BleAddressType.NotAvailable, (UInt48)0x000000000000),
- AdvertisingData.From(pduData)
- );
-
- return advertisement;
- }
-
- /// Extended advertisements have no single PDU type, Windows reports their properties separately
- private static BleEventType GetExtendedEventType(BluetoothLEAdvertisementReceivedEventArgs eventArgs)
- {
- var eventType = BleEventType.None;
- if (eventArgs.IsConnectable)
- eventType |= BleEventType.Connectable;
- if (eventArgs.IsScannable)
- eventType |= BleEventType.Scannable;
- if (eventArgs.IsDirected)
- eventType |= BleEventType.Directed;
- if (eventArgs.IsScanResponse)
- eventType |= BleEventType.ScanResponse;
- return eventType;
- }
-}
diff --git a/src/Darp.Ble.WinRT/WinBlePeripheral.cs b/src/Darp.Ble.WinRT/WinBlePeripheral.cs
deleted file mode 100644
index 763139a1..00000000
--- a/src/Darp.Ble.WinRT/WinBlePeripheral.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Darp.Ble.Data;
-using Darp.Ble.Gatt.Client;
-using Darp.Ble.Gatt.Server;
-using Darp.Ble.Implementation;
-using Darp.Ble.Utils;
-using Darp.Ble.WinRT.Gatt;
-using Microsoft.Extensions.Logging;
-using Windows.Devices.Bluetooth;
-using Windows.Devices.Bluetooth.GenericAttributeProfile;
-
-namespace Darp.Ble.WinRT;
-
-internal sealed class WinBlePeripheral(WinBleDevice device, ILogger logger)
- : BlePeripheral(device, logger)
-{
- protected override GattClientService AddServiceCore(BleUuid uuid, bool isPrimary)
- {
- GattServiceProviderResult result = GattServiceProvider.CreateAsync(uuid.Value).GetResults();
- if (result.Error is not BluetoothError.Success)
- throw new Exception("Nope");
- GattServiceProvider provider = result.ServiceProvider;
- return new WinGattClientService(this, provider, ServiceProvider.GetLogger());
- }
-
- internal IGattClientPeer GetOrRegisterSession(GattSession gattSession)
- {
- BleAddress address = BleAddress.Parse(gattSession.DeviceId.Id[^17..], provider: null);
- if (PeerDevices.TryGetValue(address, out IGattClientPeer? clientPeer) && clientPeer.IsConnected)
- {
- return clientPeer;
- }
- clientPeer = new WinGattClientPeer(this, gattSession, address, ServiceProvider.GetLogger());
- OnConnectedCentral(clientPeer);
- return clientPeer;
- }
-
- public IAsyncDisposable AdvertiseServices(IAdvertisingSet advertisingSet)
- {
- List disposables = [];
- foreach (IGattClientService value in Services)
- {
- if (value is WinGattClientService service)
- disposables.Add(service.Advertise(advertisingSet));
- }
- return AsyncDisposable.Create(
- disposables,
- async list =>
- {
- foreach (IAsyncDisposable disposable in list)
- {
- await disposable.DisposeAsync().ConfigureAwait(false);
- }
- }
- );
- }
-}
diff --git a/src/Darp.Ble.WinRT/WinRTBackend.cs b/src/Darp.Ble.WinRT/WinRTBackend.cs
new file mode 100644
index 00000000..dc3588e2
--- /dev/null
+++ b/src/Darp.Ble.WinRT/WinRTBackend.cs
@@ -0,0 +1,24 @@
+using Darp.Ble.Backends;
+using Microsoft.Extensions.Logging;
+
+namespace Darp.Ble.WinRT;
+
+/// Windows' Bluetooth stack: one adapter, the system's default
+internal sealed class WinRTBackend : IBleBackend
+{
+ public string Name => BleBackends.WinRT;
+
+ public Task> EnumerateAdaptersAsync(
+ BleBackendContext context,
+ CancellationToken cancellationToken
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+#if WINDOWS
+ if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
+ return Task.FromResult>([new WinRTAdapterSource()]);
+#endif
+ context.LoggerFactory.CreateLogger().LogNotOnWindows();
+ return Task.FromResult>([]);
+ }
+}
diff --git a/src/Darp.Ble.WinRT/WinRTBleManagerBuilderExtensions.cs b/src/Darp.Ble.WinRT/WinRTBleManagerBuilderExtensions.cs
new file mode 100644
index 00000000..fd175edd
--- /dev/null
+++ b/src/Darp.Ble.WinRT/WinRTBleManagerBuilderExtensions.cs
@@ -0,0 +1,14 @@
+namespace Darp.Ble.WinRT;
+
+/// Registers the Windows backend
+public static class WinRTBleManagerBuilderExtensions
+{
+ /// Adds Windows' default Bluetooth adapter. On other platforms, or a build without the Windows target, it adds no adapter
+ /// The builder
+ /// The builder
+ public static BleManagerBuilder AddWinRT(this BleManagerBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ return builder.AddBackend(new WinRTBackend());
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Connections/WinRTCentralDriver.cs b/src/Darp.Ble.WinRT/Windows/Connections/WinRTCentralDriver.cs
new file mode 100644
index 00000000..96b265c0
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Connections/WinRTCentralDriver.cs
@@ -0,0 +1,67 @@
+using Darp.Ble.Backends;
+using Darp.Ble.Connections;
+using Darp.Ble.Data;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace Darp.Ble.WinRT.Connections;
+
+///
+/// Connects through a GATT session that keeps the connection up. Windows chooses the connection parameters;
+/// the connection exists once the session is active
+///
+internal sealed class WinRTCentralDriver : ICentralDriver
+{
+ public async Task ConnectAsync(
+ BleAddress address,
+ BleConnectionParameters parameters,
+ IConnectionSink sink,
+ CancellationToken cancellationToken
+ )
+ {
+ BluetoothLEDevice? device = await BluetoothLEDevice
+ .FromBluetoothAddressAsync(address.Value, WinRTMapping.ToWindows(address.Type))
+ .AsTask(cancellationToken)
+ .ConfigureAwait(false);
+ if (device is null)
+ throw new BleConnectException(BleConnectErrorKind.Unreachable, $"Windows does not know a device {address}");
+ GattSession? session = null;
+ try
+ {
+ session = await GattSession
+ .FromDeviceIdAsync(device.BluetoothDeviceId)
+ .AsTask(cancellationToken)
+ .ConfigureAwait(false);
+ var active = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ void OnStatus(GattSession sender, GattSessionStatusChangedEventArgs args)
+ {
+ if (args.Status is GattSessionStatus.Active)
+ active.TrySetResult();
+ }
+ session.SessionStatusChanged += OnStatus;
+ try
+ {
+ session.MaintainConnection = true;
+ if (session.SessionStatus is GattSessionStatus.Active)
+ active.TrySetResult();
+ // Windows keeps trying; the caller decides how long
+ await active.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ session.SessionStatusChanged -= OnStatus;
+ }
+ return new WinRTConnectionDriver(device, session, sink);
+ }
+ catch
+ {
+ if (session is not null)
+ {
+ session.MaintainConnection = false;
+ session.Dispose();
+ }
+ device.Dispose();
+ throw;
+ }
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Connections/WinRTCentralLink.cs b/src/Darp.Ble.WinRT/Windows/Connections/WinRTCentralLink.cs
new file mode 100644
index 00000000..da8f694b
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Connections/WinRTCentralLink.cs
@@ -0,0 +1,13 @@
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+
+namespace Darp.Ble.WinRT.Connections;
+
+/// A central's connection to this app's services: one per device for as long as its sessions last
+internal sealed class WinRTCentralLink(string deviceId, BleAddress address) : ICentralConnectionDriver
+{
+ /// Windows' id of the device; opaque
+ public string DeviceId { get; } = deviceId;
+
+ public BleAddress Address { get; } = address;
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Connections/WinRTConnectionDriver.cs b/src/Darp.Ble.WinRT/Windows/Connections/WinRTConnectionDriver.cs
new file mode 100644
index 00000000..c30c1d90
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Connections/WinRTConnectionDriver.cs
@@ -0,0 +1,190 @@
+using Darp.Ble.Backends;
+using Darp.Ble.Connections;
+using Darp.Ble.Gatt;
+using Darp.Ble.WinRT.Gatt;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+using Windows.Devices.Enumeration;
+
+namespace Darp.Ble.WinRT.Connections;
+
+///
+/// This app's session with a peripheral. Ending it releases the session; Windows keeps the physical link while
+/// another app uses the device
+///
+internal sealed class WinRTConnectionDriver : IConnectionDriver
+{
+ private const int DefaultMtu = 23;
+
+ // Twice the longest delay measured from the session going active to Windows reporting a larger MTU
+ private static readonly TimeSpan MtuWait = TimeSpan.FromMilliseconds(500);
+
+ private readonly BluetoothLEDevice _device;
+ private readonly GattSession _session;
+ private readonly IConnectionSink _sink;
+
+ // One object per attribute handle, so every discovery returns the same ones and notifications find them
+ private readonly AttributeWrappers _services;
+ private readonly AttributeWrappers _characteristics;
+ private readonly Dictionary _descriptors = [];
+ private bool _accessGranted;
+ private int _ended;
+
+ public WinRTConnectionDriver(BluetoothLEDevice device, GattSession session, IConnectionSink sink)
+ {
+ _device = device;
+ _session = session;
+ _sink = sink;
+ // A service that arrives after the end is released; a characteristic holds nothing to release
+ _services = new(s => s.AttributeHandle, s => new WinRTRemoteService(this, s), s => s.Dispose());
+ _characteristics = new(c => c.AttributeHandle, c => new WinRTRemoteCharacteristic(this, c), _ => { });
+ _device.ConnectionStatusChanged += OnConnectionStatusChanged;
+ _session.SessionStatusChanged += OnSessionStatusChanged;
+ _session.MaxPduSizeChanged += OnMaxPduSizeChanged;
+ // The link may have ended before the handlers were attached
+ if (
+ _device.ConnectionStatus is BluetoothConnectionStatus.Disconnected
+ || _session.SessionStatus is GattSessionStatus.Closed
+ )
+ LinkDown(DisconnectReason.RemoteRequest);
+ }
+
+ public IConnectionSink Sink => _sink;
+
+ ///
+ /// Waits briefly for the MTU Windows exchanges itself, about 0.2 s after the session is active, so the connection
+ /// starts with it. Windows reports only a larger MTU: a peer that keeps the default costs the whole wait, and an
+ /// MTU that grows after the wait still reaches the sink
+ ///
+ public async Task ExchangeMtuAsync(int clientMtu, GattRequest request)
+ {
+ var changed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ void OnChanged(GattSession sender, object args) => changed.TrySetResult();
+ _session.MaxPduSizeChanged += OnChanged;
+ try
+ {
+ // Nothing is sent, so the wait ends with the request
+ if (_session.MaxPduSize <= DefaultMtu)
+ await changed.Task.WaitAsync(MtuWait, request.HandOffToken).ConfigureAwait(false);
+ }
+ catch (TimeoutException)
+ {
+ // The peer keeps the default MTU, or Windows exchanges later
+ }
+ finally
+ {
+ _session.MaxPduSizeChanged -= OnChanged;
+ }
+ return _session.MaxPduSize;
+ }
+
+ public async Task> DiscoverServicesAsync(GattRequest request)
+ {
+ await EnsureAccessAsync(request).ConfigureAwait(false);
+ GattDeviceServicesResult result = await NativeCall
+ .RunAsync(request, () => _device.GetGattServicesAsync(BluetoothCacheMode.Uncached))
+ .ConfigureAwait(false);
+ ThrowIfFailed(result.Status, result.ProtocolError, "discover services");
+ return _services.Wrap(result.Services) ?? throw Ended();
+ }
+
+ /// Windows asks the user once whether the app may use the device
+ private async Task EnsureAccessAsync(GattRequest request)
+ {
+ if (_accessGranted)
+ return;
+ // Nothing is sent yet, so the wait for the user's answer ends with the request
+ DeviceAccessStatus status = await _device
+ .RequestAccessAsync()
+ .AsTask(request.HandOffToken)
+ .ConfigureAwait(false);
+ if (status is not DeviceAccessStatus.Allowed)
+ throw new BleException($"Windows does not allow access to the device: {status}");
+ _accessGranted = true;
+ }
+
+ /// Wraps discovered characteristics; their wrappers receive the notifications
+ public IReadOnlyList WrapCharacteristics(
+ IReadOnlyList characteristics
+ ) => _characteristics.Wrap(characteristics) ?? throw Ended();
+
+ /// A discovery Windows answered after this connection ended; the core reports the end instead
+ private static BleException Ended() => new("The connection ended before Windows answered");
+
+ /// Only discovery, one request at a time, asks for descriptors; disposal does not need them
+ public WinRTRemoteDescriptor GetDescriptor(GattDescriptor descriptor)
+ {
+ if (_descriptors.TryGetValue(descriptor.AttributeHandle, out WinRTRemoteDescriptor? known))
+ return known;
+ var created = new WinRTRemoteDescriptor(this, descriptor);
+ _descriptors.Add(descriptor.AttributeHandle, created);
+ return created;
+ }
+
+ private void OnConnectionStatusChanged(BluetoothLEDevice sender, object args)
+ {
+ if (sender.ConnectionStatus is BluetoothConnectionStatus.Disconnected)
+ LinkDown(DisconnectReason.RemoteRequest);
+ }
+
+ private void OnMaxPduSizeChanged(GattSession sender, object args) => _sink.OnMtuChanged(sender.MaxPduSize);
+
+ private void OnSessionStatusChanged(GattSession sender, GattSessionStatusChangedEventArgs args)
+ {
+ if (args.Status is GattSessionStatus.Closed)
+ LinkDown(DisconnectReason.RemoteRequest);
+ }
+
+ ///
+ /// Fails with the peer's ATT error, the lost link or Windows' refusal. A device Windows cannot reach ends the
+ /// connection: it is reported first, so the core fails the request as a disconnect
+ ///
+ public void ThrowIfFailed(GattCommunicationStatus status, byte? protocolError, string action)
+ {
+ switch (status)
+ {
+ case GattCommunicationStatus.Success:
+ return;
+ case GattCommunicationStatus.ProtocolError:
+ throw new GattException(
+ (GattProtocolStatus)(protocolError ?? (byte)GattProtocolStatus.UnlikelyError),
+ $"The peer answered {action} with {protocolError}"
+ );
+ case GattCommunicationStatus.Unreachable:
+ LinkDown(DisconnectReason.ConnectionTimeout);
+ throw new BleException($"Windows could not reach the device to {action}");
+ default:
+ // AccessDenied: Windows does not let this app use the device. No lost link, so the connection stays
+ throw new BleException(
+ $"Windows refused to {action}: {status}",
+ isTransient: false,
+ nativeCode: (int)status,
+ innerException: null
+ );
+ }
+ }
+
+ /// Reports the end once. Windows' status events tell no reason; the peer or the link ended the connection
+ private void LinkDown(DisconnectReason reason)
+ {
+ if (Interlocked.Exchange(ref _ended, 1) == 0)
+ _sink.OnDisconnected(new DisconnectInfo(reason));
+ }
+
+ /// Releases this app's part without waiting for the physical link
+ public ValueTask DisposeAsync()
+ {
+ Interlocked.Exchange(ref _ended, 1);
+ _device.ConnectionStatusChanged -= OnConnectionStatusChanged;
+ _session.SessionStatusChanged -= OnSessionStatusChanged;
+ _session.MaxPduSizeChanged -= OnMaxPduSizeChanged;
+ foreach (WinRTRemoteCharacteristic characteristic in _characteristics.End())
+ characteristic.Detach();
+ foreach (WinRTRemoteService service in _services.End())
+ service.Dispose();
+ _session.MaintainConnection = false;
+ _session.Dispose();
+ _device.Dispose();
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Connections/WinRTPeripheralDriver.cs b/src/Darp.Ble.WinRT/Windows/Connections/WinRTPeripheralDriver.cs
new file mode 100644
index 00000000..af9a3861
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Connections/WinRTPeripheralDriver.cs
@@ -0,0 +1,316 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+using Darp.Ble.Backends;
+using Darp.Ble.Connections;
+using Darp.Ble.Data;
+using Darp.Ble.Data.AssignedNumbers;
+using Darp.Ble.WinRT.Gatt;
+using Microsoft.Extensions.Logging;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+using Windows.Devices.Radios;
+
+namespace Darp.Ble.WinRT.Connections;
+
+///
+/// Hosting on Windows. Windows owns the GAP, GATT, Device Information and Scan Parameters services, so adding
+/// those fails. Windows serves a service only while it advertises it, so added services are live while a connectable
+/// advertising set runs. A central becomes known through its first request or subscription; Windows has no event for a
+/// connection alone, and this app cannot take a central's link down, only end its own part. When the radio turns off and
+/// on again, Windows advertises the services again by itself
+///
+internal sealed class WinRTPeripheralDriver(Radio radio, ILogger logger) : IPeripheralDriver, IDisposable
+{
+ private static readonly HashSet Reserved =
+ [
+ BleUuid.FromUInt16(0x1800),
+ BleUuid.FromUInt16(0x1801),
+ BleUuid.FromUInt16(0x180A),
+ BleUuid.FromUInt16(0x1813),
+ ];
+
+ private readonly Radio _radio = radio;
+ private readonly Lock _lock = new();
+
+ // Resolving and announcing a central, and ending it, happen under this lock: no callback sees a central the core
+ // does not know yet or no longer knows
+ private readonly Lock _announcing = new();
+
+ // Services are added and advertised one change at a time, through their whole native calls
+ private readonly SemaphoreSlim _adding = new(1, 1);
+
+ // Subscription lists are applied one at a time, so an older list cannot win over a newer one
+ private readonly SemaphoreSlim _subscribing = new(1, 1);
+ private readonly List _services = [];
+ private readonly CentralSessions _sessions = new();
+ private readonly SubscriptionSnapshots _subscriptions = new();
+ private readonly Dictionary _addresses = new(StringComparer.Ordinal);
+ private IGattServerSink? _sink;
+ private int _connectableSets;
+
+ public ILogger Logger { get; } = logger;
+
+ public IGattServerSink Sink =>
+ _sink ?? throw new InvalidOperationException("The core did not attach the peripheral");
+
+ public void Attach(IGattServerSink sink) => _sink = sink;
+
+ public async Task AddServiceAsync(GattServiceDefinition service, CancellationToken cancellationToken)
+ {
+ if (Reserved.Contains(service.Uuid))
+ throw new InvalidOperationException($"Windows serves the service {service.Uuid} itself");
+ if (!service.IsPrimary)
+ throw new NotSupportedException("Windows hosts primary services only");
+ await _adding.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ // An advertised service starts advertising at once, which waits silently while the radio is off
+ if (_connectableSets > 0)
+ WinRTRadio.ThrowIfOff(_radio);
+ WinRTHostedService? hosted = await WinRTHostedService
+ .CreateAsync(this, service, cancellationToken)
+ .ConfigureAwait(false);
+ try
+ {
+ if (_connectableSets > 0)
+ await hosted.StartAdvertisingAsync(cancellationToken).ConfigureAwait(false);
+ lock (_lock)
+ _services.Add(hosted);
+ hosted = null;
+ }
+ finally
+ {
+ // Not added: nothing of it stays
+ hosted?.Dispose();
+ }
+ }
+ finally
+ {
+ _adding.Release();
+ }
+ }
+
+ /// Whether any service is added; connectable advertising needs one
+ public bool HasServices
+ {
+ get
+ {
+ lock (_lock)
+ return _services.Count > 0;
+ }
+ }
+
+ ///
+ /// A connectable advertising set started or stopped: the services are advertised while one runs. The count changes
+ /// only when starting succeeded; a failure stops the services it started
+ ///
+ public async Task SetAdvertisedAsync(bool advertised, CancellationToken cancellationToken)
+ {
+ await _adding.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (advertised)
+ WinRTRadio.ThrowIfOff(_radio);
+ int sets = Math.Max(0, _connectableSets + (advertised ? 1 : -1));
+ WinRTHostedService[] services;
+ lock (_lock)
+ services = [.. _services];
+ if (sets > 0 && _connectableSets == 0)
+ await StartAdvertisingAsync(services, cancellationToken).ConfigureAwait(false);
+ else if (sets == 0)
+ {
+ foreach (WinRTHostedService service in services)
+ service.StopAdvertising();
+ }
+ _connectableSets = sets;
+ }
+ finally
+ {
+ _adding.Release();
+ }
+ }
+
+ private static async Task StartAdvertisingAsync(WinRTHostedService[] services, CancellationToken cancellationToken)
+ {
+ List started = [];
+ try
+ {
+ foreach (WinRTHostedService service in services)
+ {
+ await service.StartAdvertisingAsync(cancellationToken).ConfigureAwait(false);
+ started.Add(service);
+ }
+ }
+ catch
+ {
+ foreach (WinRTHostedService service in started)
+ service.StopAdvertising();
+ throw;
+ }
+ }
+
+ public async ValueTask NotifyAsync(
+ ICentralConnectionDriver central,
+ GattCharacteristicDefinition characteristic,
+ ReadOnlyMemory value,
+ CancellationToken cancellationToken
+ )
+ {
+ var link = (WinRTCentralLink)central;
+ cancellationToken.ThrowIfCancellationRequested();
+ GattLocalCharacteristic? native;
+ lock (_lock)
+ native = _services
+ .Select(service => service.Find(characteristic))
+ .FirstOrDefault(found => found is not null);
+ if (native is null || !_subscriptions.IsSubscribed(characteristic, link))
+ return;
+ // The client of this central right now; a session that closed is no longer listed
+ GattSubscribedClient? client = native.SubscribedClients.FirstOrDefault(c =>
+ string.Equals(c.Session.DeviceId.Id, link.DeviceId, StringComparison.Ordinal)
+ );
+ if (client is null)
+ return;
+ // Longer values are cut to what the client takes, as ATT does
+ int length = Math.Min(value.Length, client.MaxNotificationSize);
+ GattClientNotificationResult result = await native
+ .NotifyValueAsync(value[..length].ToArray().AsBuffer(), client)
+ .AsTask()
+ .ConfigureAwait(false);
+ if (result.Status is not GattCommunicationStatus.Success)
+ throw new BleException($"Windows could not notify the central: {result.Status}");
+ }
+
+ public void SetGapIdentity(string deviceName, AppearanceValues appearance) =>
+ throw new NotSupportedException("Windows serves the GAP service itself");
+
+ /// Ends this app's connection to the central; Windows may keep the link for other apps
+ public Task DisconnectAsync(ICentralConnectionDriver central, CancellationToken cancellationToken)
+ {
+ var link = (WinRTCentralLink)central;
+ lock (_announcing)
+ {
+ if (_sessions.End(link))
+ End(link, new DisconnectInfo(DisconnectReason.LocalRequest));
+ }
+ return Task.CompletedTask;
+ }
+
+ /// The central of a session; announced to the core before its first request. Null for a session that closed
+ public async Task ResolveAsync(GattSession session)
+ {
+ string deviceId = session.DeviceId.Id;
+ BleAddress address = await AddressOfAsync(deviceId).ConfigureAwait(false);
+ lock (_announcing)
+ {
+ (WinRTCentralLink? link, bool isNewCentral, bool isNewSession) = _sessions.Resolve(
+ deviceId,
+ session,
+ () => new WinRTCentralLink(deviceId, address)
+ );
+ if (link is null)
+ return null;
+ if (isNewCentral)
+ Sink.OnCentralConnected(link);
+ if (isNewSession)
+ {
+ session.SessionStatusChanged += OnSessionStatusChanged;
+ // It may have closed before the handler was attached
+ if (session.SessionStatus is GattSessionStatus.Closed)
+ {
+ Close(session);
+ return null;
+ }
+ }
+ return link;
+ }
+ }
+
+ /// Windows reports a new list of subscribed clients; the core learns the changes against the list now
+ public async Task OnSubscribedClientsChangedAsync(
+ GattCharacteristicDefinition characteristic,
+ GattLocalCharacteristic native
+ )
+ {
+ try
+ {
+ await _subscribing.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ List links = [];
+ foreach (GattSubscribedClient client in native.SubscribedClients)
+ {
+ if (await ResolveAsync(client.Session).ConfigureAwait(false) is { } link)
+ links.Add(link);
+ }
+ (IReadOnlyList added, IReadOnlyList removed) =
+ _subscriptions.Update(characteristic, links);
+ foreach (WinRTCentralLink link in added)
+ Sink.OnSubscriptionChanged(link, characteristic, enabled: true);
+ foreach (WinRTCentralLink link in removed)
+ Sink.OnSubscriptionChanged(link, characteristic, enabled: false);
+ }
+ finally
+ {
+ _subscribing.Release();
+ }
+ }
+ catch (Exception e)
+ {
+ // An event handler must not throw into Windows
+ Logger.LogHostedRequestFailed(e);
+ }
+ }
+
+ private void OnSessionStatusChanged(GattSession sender, GattSessionStatusChangedEventArgs args)
+ {
+ if (args.Status is GattSessionStatus.Closed)
+ Close(sender);
+ }
+
+ /// A session closed; ends its central when it was the last
+ private void Close(GattSession session)
+ {
+ session.SessionStatusChanged -= OnSessionStatusChanged;
+ lock (_announcing)
+ {
+ if (_sessions.Close(session) is { } link)
+ End(link, new DisconnectInfo(DisconnectReason.RemoteRequest));
+ }
+ }
+
+ private void End(WinRTCentralLink link, DisconnectInfo info)
+ {
+ _subscriptions.Forget(link);
+ Sink.OnCentralDisconnected(link, info);
+ }
+
+ private async Task AddressOfAsync(string deviceId)
+ {
+ lock (_lock)
+ {
+ if (_addresses.TryGetValue(deviceId, out BleAddress? known))
+ return known;
+ }
+ using BluetoothLEDevice? device = await BluetoothLEDevice.FromIdAsync(deviceId).AsTask().ConfigureAwait(false);
+ BleAddress address = device is null
+ ? BleAddress.NotAvailable
+ : WinRTMapping.ToAddress(device.BluetoothAddress, device.BluetoothAddressType);
+ lock (_lock)
+ _addresses[deviceId] = address;
+ return address;
+ }
+
+ public void Dispose()
+ {
+ WinRTHostedService[] services;
+ lock (_lock)
+ {
+ services = [.. _services];
+ _services.Clear();
+ }
+ foreach (WinRTHostedService service in services)
+ service.Dispose();
+ _adding.Dispose();
+ _subscribing.Dispose();
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gap/WinRTAdvertisingSetDriver.cs b/src/Darp.Ble.WinRT/Windows/Gap/WinRTAdvertisingSetDriver.cs
new file mode 100644
index 00000000..48193243
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gap/WinRTAdvertisingSetDriver.cs
@@ -0,0 +1,278 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.Data.AssignedNumbers;
+using Darp.Ble.Gap;
+using Darp.Ble.WinRT.Connections;
+using Microsoft.Extensions.Logging;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.Advertisement;
+
+namespace Darp.Ble.WinRT.Gap;
+
+///
+/// One advertising set; the core serializes its calls and changes parameters only while it is stopped. Windows aborts a
+/// running publisher for good when the radio turns off; the set reports that to the core
+///
+internal sealed class WinRTAdvertisingSetDriver(
+ WinRTPeripheralDriver? peripheral,
+ AdvertisingParameters parameters,
+ AdvertisingData data,
+ IAdvertisingSetSink sink,
+ ILogger logger
+) : IAdvertisingSetDriver
+{
+#pragma warning disable CA2213 // The adapter owns the peripheral
+ private readonly WinRTPeripheralDriver? _peripheral = peripheral;
+#pragma warning restore CA2213
+ private readonly IAdvertisingSetSink _sink = sink;
+ private readonly ILogger _logger = logger;
+
+ // The core serializes the calls, so the fields below need no lock
+ private AdvertisingParameters _parameters = parameters;
+ private AdvertisingData _data = data;
+ private BluetoothLEAdvertisementPublisher? _publisher;
+ private bool _servicesAdvertised;
+
+ /// Windows chooses the address
+ public BleAddress RandomAddress => BleAddress.NotAvailable;
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ if (_parameters.Type.HasFlag(BleEventType.Connectable))
+ {
+ if (_peripheral is not { HasServices: true } peripheral)
+ throw new NotSupportedException(
+ "Windows advertises connectable only the added services; add one first"
+ );
+ if (_data.Count > 0)
+ throw new NotSupportedException(
+ "Windows advertises connectable only the added services, without data of the app's own"
+ );
+ await peripheral.SetAdvertisedAsync(true, cancellationToken).ConfigureAwait(false);
+ _servicesAdvertised = true;
+ return;
+ }
+ // The core starts only a stopped set, so a publisher still here was aborted and reported; a late duplicate of
+ // that report must not reach the new run
+ _publisher = null;
+ Publish(await StartPublisherAsync(cancellationToken).ConfigureAwait(false));
+ }
+
+ /// A publisher with the current data, once Windows advertises it; a failed start leaves nothing running
+ private async Task StartPublisherAsync(CancellationToken cancellationToken)
+ {
+ BluetoothLEAdvertisementPublisher publisher = CreatePublisher();
+ Task started = WaitForAsync(
+ publisher,
+ BluetoothLEAdvertisementPublisherStatus.Started,
+ cancellationToken
+ );
+ publisher.Start();
+ try
+ {
+ BluetoothLEAdvertisementPublisherStatusChangedEventArgs args = await started.ConfigureAwait(false);
+ if (args.Status is BluetoothLEAdvertisementPublisherStatus.Aborted)
+ throw Stopped(args.Error);
+ publisher.StatusChanged += OnRunningStatusChanged;
+ return publisher;
+ }
+ catch
+ {
+ // Windows may still start it after a cancel
+ if (
+ publisher.Status
+ is BluetoothLEAdvertisementPublisherStatus.Waiting
+ or BluetoothLEAdvertisementPublisherStatus.Started
+ )
+ publisher.Stop();
+ throw;
+ }
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ if (_servicesAdvertised && _peripheral is { } peripheral)
+ {
+ // Still this set's share until the stop went through, so a cancelled stop can be repeated
+ await peripheral.SetAdvertisedAsync(false, cancellationToken).ConfigureAwait(false);
+ _servicesAdvertised = false;
+ }
+ if (_publisher is { } publisher)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ _publisher = null;
+ await StopPublisherAsync(publisher).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Once asked to stop, Windows stops, so the wait for it is not cancellable: the set never looks advertising when it
+ /// is not. An abort meanwhile, e.g. by the radio turning off, ends it as well
+ ///
+ private async Task StopPublisherAsync(BluetoothLEAdvertisementPublisher publisher)
+ {
+ publisher.StatusChanged -= OnRunningStatusChanged;
+ var stopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ void OnChanged(
+ BluetoothLEAdvertisementPublisher sender,
+ BluetoothLEAdvertisementPublisherStatusChangedEventArgs args
+ )
+ {
+ if (
+ args.Status
+ is BluetoothLEAdvertisementPublisherStatus.Stopped
+ or BluetoothLEAdvertisementPublisherStatus.Aborted
+ )
+ stopped.TrySetResult();
+ }
+ // Before the status is read, so an abort in between is not missed
+ publisher.StatusChanged += OnChanged;
+ try
+ {
+ if (
+ publisher.Status
+ is not BluetoothLEAdvertisementPublisherStatus.Started
+ and not BluetoothLEAdvertisementPublisherStatus.Waiting
+ )
+ return;
+ publisher.Stop();
+ await stopped.Task.ConfigureAwait(false);
+ }
+ finally
+ {
+ publisher.StatusChanged -= OnChanged;
+ }
+ }
+
+ public Task SetParametersAsync(AdvertisingParameters parameters, CancellationToken cancellationToken)
+ {
+ _parameters = parameters;
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// A running publisher takes no new data, so a new one starts before the old one stops: when starting fails, the
+ /// set keeps advertising its old data
+ ///
+ public async Task SetDataAsync(AdvertisingData data, CancellationToken cancellationToken)
+ {
+ if (_parameters.Type.HasFlag(BleEventType.Connectable) && data.Count > 0)
+ throw new NotSupportedException(
+ "Windows advertises connectable only the added services, without data of the app's own"
+ );
+ AdvertisingData previous = _data;
+ _data = data;
+ // An aborted publisher was reported to the core as stopped; the data apply at the next start
+ if (_publisher is not { Status: not BluetoothLEAdvertisementPublisherStatus.Aborted } running)
+ return;
+ BluetoothLEAdvertisementPublisher started;
+ try
+ {
+ started = await StartPublisherAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ _data = previous;
+ throw;
+ }
+ Publish(started);
+ // The new data apply already; the old publisher only has to go
+ await StopPublisherAsync(running).ConfigureAwait(false);
+ }
+
+ public Task SetScanResponseDataAsync(AdvertisingData data, CancellationToken cancellationToken) =>
+ throw new NotSupportedException("Windows sends no scan response data of the app's own");
+
+ public Task SetRandomAddressAsync(BleAddress address, CancellationToken cancellationToken) =>
+ throw new NotSupportedException("Windows chooses the advertising address itself");
+
+ public ValueTask DisposeAsync() => new(StopAsync(CancellationToken.None));
+
+ private BluetoothLEAdvertisementPublisher CreatePublisher()
+ {
+ var publisher = new BluetoothLEAdvertisementPublisher
+ {
+ UseExtendedAdvertisement = !_parameters.Type.HasFlag(BleEventType.Legacy),
+ };
+ if (_parameters.AdvertisingTxPower is not TxPowerLevel.NotAvailable)
+ {
+ publisher.IncludeTransmitPowerLevel = true;
+ publisher.PreferredTransmitPowerLevelInDBm = (short)_parameters.AdvertisingTxPower;
+ }
+ (IReadOnlyList<(AdTypes Type, ReadOnlyMemory Bytes)> kept, IReadOnlyList dropped) =
+ PublisherSections.Split(_data);
+ foreach (AdTypes type in dropped)
+ _logger.LogReservedSectionDropped((byte)type);
+ foreach ((AdTypes type, ReadOnlyMemory bytes) in kept)
+ publisher.Advertisement.DataSections.Add(
+ new BluetoothLEAdvertisementDataSection((byte)type, bytes.ToArray().AsBuffer())
+ );
+ return publisher;
+ }
+
+ /// Makes a started publisher the set's; an abort it ignored while it was not the set's yet is reported now
+ private void Publish(BluetoothLEAdvertisementPublisher publisher)
+ {
+ _publisher = publisher;
+ if (publisher.Status is BluetoothLEAdvertisementPublisherStatus.Aborted)
+ ReportAborted(publisher, null);
+ }
+
+ /// Only a started publisher has this handler, and a stop the core asks for removes it first
+ private void OnRunningStatusChanged(
+ BluetoothLEAdvertisementPublisher sender,
+ BluetoothLEAdvertisementPublisherStatusChangedEventArgs args
+ )
+ {
+ if (args.Status is BluetoothLEAdvertisementPublisherStatus.Aborted)
+ ReportAborted(sender, args.Error);
+ }
+
+ /// A publisher this set replaced, or has not taken yet, reports nothing for the current one
+ private void ReportAborted(BluetoothLEAdvertisementPublisher publisher, BluetoothError? error)
+ {
+ if (!ReferenceEquals(publisher, _publisher))
+ return;
+ publisher.StatusChanged -= OnRunningStatusChanged;
+ _sink.OnFailed(Stopped(error));
+ }
+
+ /// Without an error, Windows aborted the publisher before the set took it; Windows reports no reason then
+ private static BleException Stopped(BluetoothError? error) =>
+ new(
+ error is null ? "Windows stopped advertising" : $"Windows stopped advertising: {error}",
+ isTransient: error is null or BluetoothError.RadioNotAvailable,
+ nativeCode: null,
+ innerException: null
+ );
+
+ /// Waits until the publisher reaches the status or Windows aborts it, and returns which, with Windows' error
+ private static async Task WaitForAsync(
+ BluetoothLEAdvertisementPublisher publisher,
+ BluetoothLEAdvertisementPublisherStatus status,
+ CancellationToken cancellationToken
+ )
+ {
+ var reached = new TaskCompletionSource(
+ TaskCreationOptions.RunContinuationsAsynchronously
+ );
+ void OnChanged(
+ BluetoothLEAdvertisementPublisher sender,
+ BluetoothLEAdvertisementPublisherStatusChangedEventArgs args
+ )
+ {
+ if (args.Status == status || args.Status is BluetoothLEAdvertisementPublisherStatus.Aborted)
+ reached.TrySetResult(args);
+ }
+ publisher.StatusChanged += OnChanged;
+ try
+ {
+ return await reached.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ publisher.StatusChanged -= OnChanged;
+ }
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gap/WinRTBroadcasterDriver.cs b/src/Darp.Ble.WinRT/Windows/Gap/WinRTBroadcasterDriver.cs
new file mode 100644
index 00000000..fd800db6
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gap/WinRTBroadcasterDriver.cs
@@ -0,0 +1,35 @@
+using Darp.Ble.Backends;
+using Darp.Ble.Gap;
+using Darp.Ble.WinRT.Connections;
+using Microsoft.Extensions.Logging;
+
+namespace Darp.Ble.WinRT.Gap;
+
+///
+/// Advertising on Windows. A non-connectable set is an advertisement publisher with the app's data; a connectable
+/// set advertises the added services, as Windows connects centrals only to those. Windows chooses intervals and
+/// addresses, sends no scan response data of the app's own, and keeps advertising when a central connects, so
+/// sets are never stopped by a connection. When the radio turns off, a non-connectable set stops; connectable sets go on
+/// once it is back, as Windows advertises the services again by itself
+///
+internal sealed class WinRTBroadcasterDriver(WinRTPeripheralDriver? peripheral, ILogger logger) : IBroadcasterDriver
+{
+ private readonly WinRTPeripheralDriver? _peripheral = peripheral;
+ private readonly ILogger _logger = logger;
+
+ public Task CreateAdvertisingSetAsync(
+ AdvertisingParameters parameters,
+ AdvertisingData data,
+ AdvertisingData? scanResponseData,
+ IAdvertisingSetSink sink,
+ CancellationToken cancellationToken
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (scanResponseData is not null)
+ throw new NotSupportedException("Windows sends no scan response data of the app's own");
+ return Task.FromResult(
+ new WinRTAdvertisingSetDriver(_peripheral, parameters, data, sink, _logger)
+ );
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gap/WinRTObserverDriver.cs b/src/Darp.Ble.WinRT/Windows/Gap/WinRTObserverDriver.cs
new file mode 100644
index 00000000..8bcb4032
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gap/WinRTObserverDriver.cs
@@ -0,0 +1,150 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.Data.AssignedNumbers;
+using Darp.Ble.Gap;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.Advertisement;
+using Windows.Devices.Radios;
+
+namespace Darp.Ble.WinRT.Gap;
+
+///
+/// One watcher per radio scan. Windows chooses scan interval and window itself; only the scan type applies.
+/// Each scan's callbacks belong to its own watcher, so a stopped scan never reports into the next one.
+/// When the radio turns off, Windows stops the watcher for good, so the scan fails
+///
+internal sealed class WinRTObserverDriver(Radio radio, TimeProvider timeProvider) : IObserverDriver, IAsyncDisposable
+{
+ private readonly Radio _radio = radio;
+ private readonly TimeProvider _timeProvider = timeProvider;
+ private Run? _run;
+
+ public Task StartScanAsync(BleScanParameters parameters, IScanSink sink, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ WinRTRadio.ThrowIfOff(_radio);
+ var watcher = new BluetoothLEAdvertisementWatcher
+ {
+ ScanningMode =
+ parameters.ScanType is ScanType.Active
+ ? BluetoothLEScanningMode.Active
+ : BluetoothLEScanningMode.Passive,
+ AllowExtendedAdvertisements = true,
+ };
+ var run = new Run(watcher, sink, _timeProvider);
+ _run = run;
+ // Both handlers before Start, so no report or stop of this run is missed
+ watcher.Received += run.OnReceived;
+ watcher.Stopped += run.OnStopped;
+ watcher.Start();
+ if (watcher.Status is BluetoothLEAdvertisementWatcherStatus.Aborted)
+ {
+ run.Detach();
+ _run = null;
+ throw new BleException("Windows could not start scanning; the Bluetooth adapter may need a restart");
+ }
+ return Task.CompletedTask;
+ }
+
+ public async Task StopScanAsync(CancellationToken cancellationToken)
+ {
+ Run? run = _run;
+ _run = null;
+ if (run is not null)
+ await run.StopAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public ValueTask DisposeAsync() => new(StopScanAsync(CancellationToken.None));
+
+ /// One radio scan with its watcher and the sink it reports to
+ private sealed class Run(BluetoothLEAdvertisementWatcher watcher, IScanSink sink, TimeProvider timeProvider)
+ {
+ private readonly TaskCompletionSource _stopped = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private volatile bool _detached;
+
+ public void OnReceived(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs report)
+ {
+ if (_detached)
+ return;
+ sink.OnReport(ToAdvertisement(report, timeProvider.GetUtcNow()));
+ }
+
+ public void OnStopped(
+ BluetoothLEAdvertisementWatcher sender,
+ BluetoothLEAdvertisementWatcherStoppedEventArgs args
+ )
+ {
+ _stopped.TrySetResult();
+ // A stop this driver asked for detached the run first; any other stop ends the scan
+ if (_detached)
+ return;
+ _detached = true;
+ sink.OnFailed(
+ args.Error is BluetoothError.Success
+ // Windows reports no error when the radio turns off
+ ? new BleException(
+ "Windows stopped scanning; the Bluetooth radio may be off",
+ isTransient: true,
+ nativeCode: null,
+ innerException: null
+ )
+ : new BleException(
+ $"Windows stopped scanning: {args.Error}",
+ isTransient: args.Error is BluetoothError.RadioNotAvailable,
+ nativeCode: null,
+ innerException: null
+ )
+ );
+ }
+
+ /// Stop only asks the watcher to stop; it has stopped once it reports so
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ _detached = true;
+ if (
+ watcher.Status
+ is BluetoothLEAdvertisementWatcherStatus.Started
+ or BluetoothLEAdvertisementWatcherStatus.Stopping
+ )
+ {
+ watcher.Stop();
+ await _stopped.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
+ }
+ Detach();
+ }
+
+ public void Detach()
+ {
+ _detached = true;
+ watcher.Received -= OnReceived;
+ watcher.Stopped -= OnStopped;
+ }
+ }
+
+ private static GapAdvertisement ToAdvertisement(
+ BluetoothLEAdvertisementReceivedEventArgs report,
+ DateTimeOffset timestamp
+ )
+ {
+ (AdTypes Section, ReadOnlyMemory Bytes)[] sections =
+ [
+ .. report.Advertisement.DataSections.Select(section =>
+ ((AdTypes)section.DataType, (ReadOnlyMemory)section.Data.ToArray())
+ ),
+ ];
+ return GapAdvertisement.Create(
+ timestamp,
+ WinRTMapping.ToEventType(report),
+ WinRTMapping.ToAddress(report.BluetoothAddress, report.BluetoothAddressType),
+ Physical.NotAvailable,
+ Physical.NotAvailable,
+ AdvertisingSId.NoAdIProvided,
+ report.TransmitPowerLevelInDBm is { } power ? (TxPowerLevel)power : TxPowerLevel.NotAvailable,
+ (Rssi)report.RawSignalStrengthInDBm,
+ PeriodicAdvertisingInterval.NoPeriodicAdvertising,
+ BleAddress.NotAvailable,
+ AdvertisingData.From(sections)
+ );
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gatt/WinRTHostedService.cs b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTHostedService.cs
new file mode 100644
index 00000000..e4fa8a74
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTHostedService.cs
@@ -0,0 +1,281 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+using System.Text;
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.Gatt;
+using Darp.Ble.WinRT.Connections;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+using Windows.Foundation;
+
+namespace Darp.Ble.WinRT.Gatt;
+
+///
+/// One added service: Windows' service provider with the characteristics and descriptors of the definition.
+/// Windows creates the CCCDs itself and takes a user description as characteristic metadata
+///
+internal sealed class WinRTHostedService : IDisposable
+{
+ private const byte UnlikelyError = 0x0E;
+ private const byte InvalidOffset = 0x07;
+ private static readonly BleUuid UserDescription = BleUuid.FromUInt16(0x2901);
+ private static readonly BleUuid Cccd = BleUuid.FromUInt16(0x2902);
+
+ private readonly WinRTPeripheralDriver _peripheral;
+ private readonly GattServiceProvider _provider;
+ private readonly Dictionary _characteristics = [];
+ private bool _advertising;
+
+ private WinRTHostedService(WinRTPeripheralDriver peripheral, GattServiceProvider provider)
+ {
+ _peripheral = peripheral;
+ _provider = provider;
+ }
+
+ /// Creates the service with every characteristic and descriptor; a failure removes what was created
+ public static async Task CreateAsync(
+ WinRTPeripheralDriver peripheral,
+ GattServiceDefinition definition,
+ CancellationToken cancellationToken
+ )
+ {
+ GattServiceProviderResult result = await GattServiceProvider
+ .CreateAsync(definition.Uuid.Value)
+ .AsTask(cancellationToken)
+ .ConfigureAwait(false);
+ if (result.Error is not BluetoothError.Success)
+ throw new BleException(
+ $"Windows could not create the service {definition.Uuid}: {result.Error}",
+ isTransient: result.Error is BluetoothError.RadioNotAvailable,
+ nativeCode: null,
+ innerException: null
+ );
+ var service = new WinRTHostedService(peripheral, result.ServiceProvider);
+ try
+ {
+ foreach (GattCharacteristicDefinition characteristic in definition.Characteristics)
+ await service.AddAsync(characteristic, cancellationToken).ConfigureAwait(false);
+ return service;
+ }
+ catch
+ {
+ service.Dispose();
+ throw;
+ }
+ }
+
+ /// The native characteristic of an added definition, or null
+ public GattLocalCharacteristic? Find(GattCharacteristicDefinition characteristic) =>
+ _characteristics.GetValueOrDefault(characteristic);
+
+ private async Task AddAsync(GattCharacteristicDefinition definition, CancellationToken cancellationToken)
+ {
+ var parameters = new GattLocalCharacteristicParameters
+ {
+ CharacteristicProperties = WinRTMapping.ToWindows(definition.Properties),
+ ReadProtectionLevel = GattProtectionLevel.Plain,
+ WriteProtectionLevel = GattProtectionLevel.Plain,
+ };
+ GattDescriptorDefinition? description = definition.Descriptors.FirstOrDefault(d => d.Uuid == UserDescription);
+ if (description?.StaticValue is { } text)
+ parameters.UserDescription = Encoding.UTF8.GetString(text.Span);
+ GattLocalCharacteristicResult result = await _provider
+ .Service.CreateCharacteristicAsync(definition.Uuid.Value, parameters)
+ .AsTask(cancellationToken)
+ .ConfigureAwait(false);
+ if (result.Error is not BluetoothError.Success)
+ throw new BleException($"Windows could not create the characteristic {definition.Uuid}: {result.Error}");
+ GattLocalCharacteristic characteristic = result.Characteristic;
+ _characteristics[definition] = characteristic;
+ characteristic.ReadRequested += (sender, args) =>
+ _ = ReadAsync(args.Session, definition, args.GetDeferral(), args.GetRequestAsync());
+ characteristic.WriteRequested += (sender, args) =>
+ _ = WriteAsync(args.Session, definition, args.GetDeferral(), args.GetRequestAsync());
+ characteristic.SubscribedClientsChanged += (sender, args) =>
+ _ = _peripheral.OnSubscribedClientsChangedAsync(definition, sender);
+ foreach (GattDescriptorDefinition descriptor in definition.Descriptors)
+ {
+ // Windows generates both itself
+ if (descriptor.Uuid == Cccd || (descriptor.Uuid == UserDescription && descriptor.StaticValue is not null))
+ continue;
+ GattLocalDescriptorResult created = await characteristic
+ .CreateDescriptorAsync(
+ descriptor.Uuid.Value,
+ new GattLocalDescriptorParameters
+ {
+ ReadProtectionLevel = GattProtectionLevel.Plain,
+ WriteProtectionLevel = GattProtectionLevel.Plain,
+ }
+ )
+ .AsTask(cancellationToken)
+ .ConfigureAwait(false);
+ if (created.Error is not BluetoothError.Success)
+ throw new BleException($"Windows could not create the descriptor {descriptor.Uuid}: {created.Error}");
+ created.Descriptor.ReadRequested += (sender, args) =>
+ _ = ReadAsync(args.Session, descriptor, args.GetDeferral(), args.GetRequestAsync());
+ created.Descriptor.WriteRequested += (sender, args) =>
+ _ = WriteAsync(args.Session, descriptor, args.GetDeferral(), args.GetRequestAsync());
+ }
+ }
+
+ ///
+ /// Advertises the service; Windows serves a service to centrals only this way. Windows reports Aborted without an
+ /// error on the way to Started, also when it starts again after a stop
+ ///
+ public async Task StartAdvertisingAsync(CancellationToken cancellationToken)
+ {
+ if (_advertising)
+ return;
+ var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ void OnChanged(GattServiceProvider sender, GattServiceProviderAdvertisementStatusChangedEventArgs args)
+ {
+ if (IsStarted(args.Status))
+ started.TrySetResult();
+ else if (
+ args.Status is GattServiceProviderAdvertisementStatus.Aborted
+ && args.Error is not BluetoothError.Success
+ )
+ started.TrySetException(new BleException($"Windows could not add the service: {args.Error}"));
+ }
+ _provider.AdvertisementStatusChanged += OnChanged;
+ try
+ {
+ // After a stop the status still says Started, so only the events of this start count then
+ bool stale = IsStarted(_provider.AdvertisementStatus);
+ _provider.StartAdvertising(
+ new GattServiceProviderAdvertisingParameters { IsConnectable = true, IsDiscoverable = true }
+ );
+ _advertising = true;
+ if (!stale && IsStarted(_provider.AdvertisementStatus))
+ started.TrySetResult();
+ await started.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ StopAdvertising();
+ throw;
+ }
+ finally
+ {
+ _provider.AdvertisementStatusChanged -= OnChanged;
+ }
+ }
+
+ /// Stops advertising the service; Windows does not report this in the provider's status
+ public void StopAdvertising()
+ {
+ if (!_advertising)
+ return;
+ _advertising = false;
+ _provider.StopAdvertising();
+ }
+
+ private static bool IsStarted(GattServiceProviderAdvertisementStatus status) =>
+ status
+ is GattServiceProviderAdvertisementStatus.Started
+ or GattServiceProviderAdvertisementStatus.StartedWithoutAllAdvertisementData;
+
+ private async Task ReadAsync(
+ GattSession session,
+ GattAttributeDefinition attribute,
+ Deferral deferral,
+ IAsyncOperation requested
+ )
+ {
+ using (deferral)
+ {
+ try
+ {
+ WinRTCentralLink? central = await _peripheral.ResolveAsync(session).ConfigureAwait(false);
+ GattReadRequest? request = await requested.AsTask().ConfigureAwait(false);
+ // A session that closed, or a request Windows withdrew
+ if (central is null || request is null)
+ return;
+ byte[] value;
+ try
+ {
+ value = await _peripheral.Sink.OnReadRequest(central, attribute).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Fail(request.RespondWithProtocolError, e);
+ return;
+ }
+ if (request.Offset > (uint)value.Length)
+ {
+ request.RespondWithProtocolError(InvalidOffset);
+ return;
+ }
+ request.RespondWithValue(value.AsBuffer((int)request.Offset, value.Length - (int)request.Offset));
+ }
+ catch (Exception e)
+ {
+ // An event handler must not throw into Windows; the central's request times out
+ _peripheral.Logger.LogHostedRequestFailed(e);
+ }
+ }
+ }
+
+ private async Task WriteAsync(
+ GattSession session,
+ GattAttributeDefinition attribute,
+ Deferral deferral,
+ IAsyncOperation requested
+ )
+ {
+ using (deferral)
+ {
+ try
+ {
+ WinRTCentralLink? central = await _peripheral.ResolveAsync(session).ConfigureAwait(false);
+ GattWriteRequest? request = await requested.AsTask().ConfigureAwait(false);
+ if (central is null || request is null)
+ return;
+ byte[] value = request.Value.ToArray();
+ if (request.Option is GattWriteOption.WriteWithoutResponse)
+ {
+ _peripheral.Sink.OnWriteCommand(central, attribute, value);
+ return;
+ }
+ if (request.Offset != 0)
+ {
+ request.RespondWithProtocolError(InvalidOffset);
+ return;
+ }
+ try
+ {
+ await _peripheral.Sink.OnWriteRequest(central, attribute, value).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Fail(request.RespondWithProtocolError, e);
+ return;
+ }
+ request.Respond();
+ }
+ catch (Exception e)
+ {
+ // An event handler must not throw into Windows; the central's request times out
+ _peripheral.Logger.LogHostedRequestFailed(e);
+ }
+ }
+ }
+
+ /// The core's failure as an ATT error; a connection that ends gets no answer
+ private static void Fail(Action respondWithError, Exception exception)
+ {
+ switch (exception)
+ {
+ case OperationCanceledException:
+ return;
+ case GattException { AttStatus: { } status }:
+ respondWithError((byte)status);
+ return;
+ default:
+ respondWithError(UnlikelyError);
+ return;
+ }
+ }
+
+ public void Dispose() => StopAdvertising();
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteCharacteristic.cs b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteCharacteristic.cs
new file mode 100644
index 00000000..1983ca9c
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteCharacteristic.cs
@@ -0,0 +1,101 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.Gatt;
+using Darp.Ble.WinRT.Connections;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace Darp.Ble.WinRT.Gatt;
+
+internal sealed class WinRTRemoteCharacteristic : IRemoteCharacteristicDriver
+{
+ private readonly WinRTConnectionDriver _connection;
+ private readonly GattCharacteristic _characteristic;
+
+ public WinRTRemoteCharacteristic(WinRTConnectionDriver connection, GattCharacteristic characteristic)
+ {
+ _connection = connection;
+ _characteristic = characteristic;
+ Uuid = BleUuid.FromGuid(characteristic.Uuid, inferType: true);
+ Properties = WinRTMapping.ToProperties(characteristic.CharacteristicProperties);
+ // Before any CCCD write, so a value the peer sends right after enabling is not lost
+ _characteristic.ValueChanged += OnValueChanged;
+ }
+
+ public BleUuid Uuid { get; }
+
+ public GattProperty Properties { get; }
+
+ public async Task ReadAsync(GattRequest request)
+ {
+ GattReadResult result = await NativeCall
+ .RunAsync(request, () => _characteristic.ReadValueAsync(BluetoothCacheMode.Uncached))
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "read");
+ return result.Value.ToArray();
+ }
+
+ public async Task WriteAsync(ReadOnlyMemory value, GattRequest request)
+ {
+ GattWriteResult result = await NativeCall
+ .RunAsync(
+ request,
+ () =>
+ _characteristic.WriteValueWithResultAsync(
+ value.ToArray().AsBuffer(),
+ GattWriteOption.WriteWithResponse
+ )
+ )
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "write");
+ }
+
+ ///
+ /// Windows has no signal for buffer space: the write is handed off at once, and only waiting in the core's queue
+ /// can be cancelled. The task completes with Windows' result; the bytes are copied before the call
+ ///
+ public async ValueTask WriteWithoutResponseAsync(
+ ReadOnlyMemory value,
+ CancellationToken cancellationToken,
+ CancellationToken connectionClosed
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ GattWriteResult result = await _characteristic
+ .WriteValueWithResultAsync(value.ToArray().AsBuffer(), GattWriteOption.WriteWithoutResponse)
+ .AsTask()
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "write without response");
+ }
+
+ public async Task> DiscoverDescriptorsAsync(GattRequest request)
+ {
+ GattDescriptorsResult result = await NativeCall
+ .RunAsync(request, () => _characteristic.GetDescriptorsAsync(BluetoothCacheMode.Uncached))
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "discover descriptors");
+ return [.. result.Descriptors.Select(_connection.GetDescriptor)];
+ }
+
+ public async Task SetNotificationsAsync(bool enabled, GattRequest request)
+ {
+ GattWriteResult result = await NativeCall
+ .RunAsync(
+ request,
+ () =>
+ _characteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(
+ enabled
+ ? GattClientCharacteristicConfigurationDescriptorValue.Notify
+ : GattClientCharacteristicConfigurationDescriptorValue.None
+ )
+ )
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "write the CCCD");
+ }
+
+ private void OnValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) =>
+ _connection.Sink.OnNotification(this, args.CharacteristicValue.ToArray());
+
+ public void Detach() => _characteristic.ValueChanged -= OnValueChanged;
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteDescriptor.cs b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteDescriptor.cs
new file mode 100644
index 00000000..c6ded68d
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteDescriptor.cs
@@ -0,0 +1,34 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.WinRT.Connections;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace Darp.Ble.WinRT.Gatt;
+
+internal sealed class WinRTRemoteDescriptor(WinRTConnectionDriver connection, GattDescriptor descriptor)
+ : IRemoteDescriptorDriver
+{
+ private readonly WinRTConnectionDriver _connection = connection;
+ private readonly GattDescriptor _descriptor = descriptor;
+
+ public BleUuid Uuid { get; } = BleUuid.FromGuid(descriptor.Uuid, inferType: true);
+
+ public async Task ReadAsync(GattRequest request)
+ {
+ GattReadResult result = await NativeCall
+ .RunAsync(request, () => _descriptor.ReadValueAsync(BluetoothCacheMode.Uncached))
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "read the descriptor");
+ return result.Value.ToArray();
+ }
+
+ public async Task WriteAsync(ReadOnlyMemory value, GattRequest request)
+ {
+ GattWriteResult result = await NativeCall
+ .RunAsync(request, () => _descriptor.WriteValueWithResultAsync(value.ToArray().AsBuffer()))
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "write the descriptor");
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteService.cs b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteService.cs
new file mode 100644
index 00000000..9cf067f3
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/Gatt/WinRTRemoteService.cs
@@ -0,0 +1,31 @@
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.WinRT.Connections;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace Darp.Ble.WinRT.Gatt;
+
+internal sealed class WinRTRemoteService(WinRTConnectionDriver connection, GattDeviceService service)
+ : IRemoteServiceDriver,
+ IDisposable
+{
+#pragma warning disable CA2213 // The connection owns its services, not the other way round
+ private readonly WinRTConnectionDriver _connection = connection;
+#pragma warning restore CA2213
+ private readonly GattDeviceService _service = service;
+
+ public BleUuid Uuid { get; } = BleUuid.FromGuid(service.Uuid, inferType: true);
+
+ /// One Windows call; Windows reads the pages itself, so an abandoned request cannot stop between them
+ public async Task> DiscoverCharacteristicsAsync(GattRequest request)
+ {
+ GattCharacteristicsResult result = await NativeCall
+ .RunAsync(request, () => _service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached))
+ .ConfigureAwait(false);
+ _connection.ThrowIfFailed(result.Status, result.ProtocolError, "discover characteristics");
+ return _connection.WrapCharacteristics(result.Characteristics);
+ }
+
+ public void Dispose() => _service.Dispose();
+}
diff --git a/src/Darp.Ble.WinRT/Windows/NativeCall.cs b/src/Darp.Ble.WinRT/Windows/NativeCall.cs
new file mode 100644
index 00000000..e462dd95
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/NativeCall.cs
@@ -0,0 +1,27 @@
+using Darp.Ble.Backends;
+using Windows.Foundation;
+
+namespace Darp.Ble.WinRT;
+
+///
+/// One GATT request as a Windows call. Windows hides its own queue and the transactions inside a call, so the
+/// request counts as sent once the call started, and the ATT timeout includes Windows' queueing. A started call is
+/// never cancelled natively: its effect on a sent request is undocumented
+///
+internal static class NativeCall
+{
+ public static async Task RunAsync(GattRequest request, Func> start)
+ {
+ request.HandOffToken.ThrowIfCancellationRequested();
+ IAsyncOperation operation = start();
+ request.OnTransactionSent();
+ try
+ {
+ return await operation.AsTask().ConfigureAwait(false);
+ }
+ finally
+ {
+ request.OnTransactionAnswered();
+ }
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/WinRTAdapterDriver.cs b/src/Darp.Ble.WinRT/Windows/WinRTAdapterDriver.cs
new file mode 100644
index 00000000..c9601db8
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/WinRTAdapterDriver.cs
@@ -0,0 +1,55 @@
+using Darp.Ble.Backends;
+using Darp.Ble.Data;
+using Darp.Ble.WinRT.Connections;
+using Darp.Ble.WinRT.Gap;
+using Microsoft.Extensions.Logging;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Radios;
+
+namespace Darp.Ble.WinRT;
+
+/// The opened default adapter; its roles follow what Windows says the adapter can do
+internal sealed class WinRTAdapterDriver : IBleAdapterDriver
+{
+ private readonly WinRTObserverDriver _observer;
+ private readonly WinRTCentralDriver? _central;
+ private readonly WinRTPeripheralDriver? _peripheral;
+ private readonly WinRTBroadcasterDriver? _broadcaster;
+
+ public WinRTAdapterDriver(BluetoothAdapter adapter, Radio radio, BleBackendContext context)
+ {
+ _observer = new WinRTObserverDriver(radio, context.TimeProvider);
+ RandomAddress = new BleAddress(BleAddressType.Public, (UInt48)adapter.BluetoothAddress);
+ if (adapter.IsCentralRoleSupported)
+ _central = new WinRTCentralDriver();
+ ILogger logger = context.LoggerFactory.CreateLogger();
+ if (adapter.IsPeripheralRoleSupported)
+ _peripheral = new WinRTPeripheralDriver(radio, logger);
+ // The peripheral role includes the broadcaster; without it, advertising needs offloading
+ if (adapter.IsPeripheralRoleSupported || adapter.IsAdvertisementOffloadSupported)
+ _broadcaster = new WinRTBroadcasterDriver(_peripheral, logger);
+ }
+
+ /// Windows sets the address and serves the GAP service itself
+ public Capabilities Features => Capabilities.None;
+
+ public IObserverDriver? Observer => _observer;
+
+ public IBroadcasterDriver? Broadcaster => _broadcaster;
+
+ public ICentralDriver? Central => _central;
+
+ public IPeripheralDriver? Peripheral => _peripheral;
+
+ /// The adapter's own address; Windows chooses the addresses it advertises and connects with
+ public BleAddress RandomAddress { get; }
+
+ public Task SetRandomAddressAsync(BleAddress address, CancellationToken cancellationToken) =>
+ throw new NotSupportedException("Windows chooses the adapter's addresses itself");
+
+ public async ValueTask DisposeAsync()
+ {
+ await _observer.DisposeAsync().ConfigureAwait(false);
+ _peripheral?.Dispose();
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/WinRTAdapterSource.cs b/src/Darp.Ble.WinRT/Windows/WinRTAdapterSource.cs
new file mode 100644
index 00000000..d6412dab
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/WinRTAdapterSource.cs
@@ -0,0 +1,36 @@
+using Darp.Ble.Backends;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Radios;
+
+namespace Darp.Ble.WinRT;
+
+/// The system's default Bluetooth adapter
+internal sealed class WinRTAdapterSource : IBleAdapterSource
+{
+ public string? Name => "Windows";
+
+ public async Task OpenAsync(BleBackendContext context, CancellationToken cancellationToken)
+ {
+ BluetoothAdapter? adapter = await BluetoothAdapter
+ .GetDefaultAsync()
+ .AsTask(cancellationToken)
+ .ConfigureAwait(false);
+ if (adapter is null)
+ throw new BleOpenException(BleOpenErrorKind.NotAvailable, "Windows has no Bluetooth adapter");
+ if (!adapter.IsLowEnergySupported)
+ throw new BleOpenException(
+ BleOpenErrorKind.NotAvailable,
+ "The Bluetooth adapter does not support Low Energy"
+ );
+ Radio? radio = await adapter.GetRadioAsync().AsTask(cancellationToken).ConfigureAwait(false);
+ if (radio is not { State: RadioState.On })
+ {
+ throw new BleOpenException(
+ BleOpenErrorKind.Disabled,
+ $"The Bluetooth radio is {radio?.State.ToString() ?? "unavailable"}",
+ isTransient: true
+ );
+ }
+ return new WinRTAdapterDriver(adapter, radio, context);
+ }
+}
diff --git a/src/Darp.Ble.WinRT/Windows/WinRTMapping.cs b/src/Darp.Ble.WinRT/Windows/WinRTMapping.cs
new file mode 100644
index 00000000..89b3f756
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/WinRTMapping.cs
@@ -0,0 +1,55 @@
+using Darp.Ble.Data;
+using Darp.Ble.Gap;
+using Darp.Ble.Gatt;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.Advertisement;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace Darp.Ble.WinRT;
+
+/// Conversions between Windows' Bluetooth types and the library's
+internal static class WinRTMapping
+{
+ public static BleAddress ToAddress(ulong address, BluetoothAddressType type) =>
+ new(
+ type switch
+ {
+ BluetoothAddressType.Public => BleAddressType.Public,
+ BluetoothAddressType.Random => BleAddressType.Random,
+ _ => BleAddressType.NotAvailable,
+ },
+ (UInt48)address
+ );
+
+ public static BluetoothAddressType ToWindows(BleAddressType type) =>
+ type switch
+ {
+ BleAddressType.Public or BleAddressType.PublicIdentity => BluetoothAddressType.Public,
+ BleAddressType.NotAvailable => BluetoothAddressType.Unspecified,
+ _ => BluetoothAddressType.Random,
+ };
+
+ public static BleEventType ToEventType(BluetoothLEAdvertisementReceivedEventArgs report) =>
+ report.AdvertisementType switch
+ {
+ BluetoothLEAdvertisementType.ConnectableUndirected => BleEventType.AdvInd,
+ BluetoothLEAdvertisementType.ConnectableDirected => BleEventType.AdvDirectInd,
+ BluetoothLEAdvertisementType.ScannableUndirected => BleEventType.AdvScanInd,
+ BluetoothLEAdvertisementType.NonConnectableUndirected => BleEventType.AdvNonConnInd,
+ // A legacy scan response answers ADV_IND (connectable) or ADV_SCAN_IND, both scannable
+ BluetoothLEAdvertisementType.ScanResponse => BleEventType.Legacy
+ | BleEventType.Scannable
+ | BleEventType.ScanResponse
+ | (report.IsConnectable ? BleEventType.Connectable : BleEventType.None),
+ _ => (report.IsConnectable ? BleEventType.Connectable : BleEventType.None)
+ | (report.IsScannable ? BleEventType.Scannable : BleEventType.None)
+ | (report.IsDirected ? BleEventType.Directed : BleEventType.None)
+ | (report.IsScanResponse ? BleEventType.ScanResponse : BleEventType.None),
+ };
+
+ public static GattCharacteristicProperties ToWindows(GattProperty properties) =>
+ (GattCharacteristicProperties)(uint)properties;
+
+ public static GattProperty ToProperties(GattCharacteristicProperties properties) =>
+ (GattProperty)((uint)properties & 0xFF);
+}
diff --git a/src/Darp.Ble.WinRT/Windows/WinRTRadio.cs b/src/Darp.Ble.WinRT/Windows/WinRTRadio.cs
new file mode 100644
index 00000000..c7951c5d
--- /dev/null
+++ b/src/Darp.Ble.WinRT/Windows/WinRTRadio.cs
@@ -0,0 +1,21 @@
+using Windows.Devices.Radios;
+
+namespace Darp.Ble.WinRT;
+
+///
+/// Windows' Bluetooth radio. A scan or a service started while the radio is off waits silently until it is on, so the
+/// backend refuses to start one then
+///
+internal static class WinRTRadio
+{
+ public static void ThrowIfOff(Radio radio)
+ {
+ if (radio.State is not RadioState.On)
+ throw new BleException(
+ $"The Bluetooth radio is {radio.State}",
+ isTransient: true,
+ nativeCode: null,
+ innerException: null
+ );
+ }
+}
diff --git a/src/Darp.Ble/Backends/BleBackendContext.cs b/src/Darp.Ble/Backends/BleBackendContext.cs
index 128de594..d2a70a45 100644
--- a/src/Darp.Ble/Backends/BleBackendContext.cs
+++ b/src/Darp.Ble/Backends/BleBackendContext.cs
@@ -1,9 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Diagnostics;
using Microsoft.Extensions.Logging;
namespace Darp.Ble.Backends;
/// The services a gives to its backends and their drivers
+[Experimental(Experiments.Backends)]
public sealed class BleBackendContext
{
internal BleBackendContext(ILoggerFactory loggerFactory, TimeProvider timeProvider, DiagnosticsSettings diagnostics)
diff --git a/src/Darp.Ble/Backends/Connections/ICentralConnectionDriver.cs b/src/Darp.Ble/Backends/Connections/ICentralConnectionDriver.cs
index c4d911ef..7d588368 100644
--- a/src/Darp.Ble/Backends/Connections/ICentralConnectionDriver.cs
+++ b/src/Darp.Ble/Backends/Connections/ICentralConnectionDriver.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// A central connected to the adapter's peripheral role. Belongs to its peripheral driver, which reports it through
+[Experimental(Experiments.Backends)]
public interface ICentralConnectionDriver
{
/// The address of the central
diff --git a/src/Darp.Ble/Backends/Connections/ICentralDriver.cs b/src/Darp.Ble/Backends/Connections/ICentralDriver.cs
index c2179890..b100a61b 100644
--- a/src/Darp.Ble/Backends/Connections/ICentralDriver.cs
+++ b/src/Darp.Ble/Backends/Connections/ICentralDriver.cs
@@ -1,9 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Connections;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// The central role of an adapter: connects to peripherals
+[Experimental(Experiments.Backends)]
public interface ICentralDriver
{
/// Connects to a peripheral
diff --git a/src/Darp.Ble/Backends/Connections/IConnectionDriver.cs b/src/Darp.Ble/Backends/Connections/IConnectionDriver.cs
index 1e285fe3..9a74d46a 100644
--- a/src/Darp.Ble/Backends/Connections/IConnectionDriver.cs
+++ b/src/Darp.Ble/Backends/Connections/IConnectionDriver.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Connections;
using Darp.Ble.Gatt;
@@ -16,6 +17,7 @@ namespace Darp.Ble.Backends;
///
/// Disposing it ends this app's connection and must end every task still running
///
+[Experimental(Experiments.Backends)]
public interface IConnectionDriver : IAsyncDisposable
{
/// Agrees on the ATT MTU with the peer
diff --git a/src/Darp.Ble/Backends/Connections/IConnectionSink.cs b/src/Darp.Ble/Backends/Connections/IConnectionSink.cs
index 7ba5e9c5..be3e66a5 100644
--- a/src/Darp.Ble/Backends/Connections/IConnectionSink.cs
+++ b/src/Darp.Ble/Backends/Connections/IConnectionSink.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Connections;
namespace Darp.Ble.Backends;
/// Receives the events of one connection. Calls must stay cheap; the core does its work elsewhere
+[Experimental(Experiments.Backends)]
public interface IConnectionSink
{
/// Reports that the link ended without this app asking for it
@@ -13,4 +15,8 @@ public interface IConnectionSink
/// The characteristic, as the connection returned it from discovery
/// The value; only valid during the call, the core copies it
void OnNotification(IRemoteCharacteristicDriver characteristic, ReadOnlySpan value);
+
+ /// The MTU grew after the exchange, e.g. Windows exchanges it itself once the link is up
+ /// The new MTU; a smaller one than the connection has is ignored
+ void OnMtuChanged(int mtu);
}
diff --git a/src/Darp.Ble/Backends/Connections/IPeripheralDriver.cs b/src/Darp.Ble/Backends/Connections/IPeripheralDriver.cs
index 3ad7c102..a640c9ff 100644
--- a/src/Darp.Ble/Backends/Connections/IPeripheralDriver.cs
+++ b/src/Darp.Ble/Backends/Connections/IPeripheralDriver.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data.AssignedNumbers;
namespace Darp.Ble.Backends;
/// The peripheral role of an adapter: accepts connections from centrals
+[Experimental(Experiments.Backends)]
public interface IPeripheralDriver
{
/// Binds the sink that receives connections. The core calls it once, when the adapter opens, before anything else
diff --git a/src/Darp.Ble/Backends/Experiments.cs b/src/Darp.Ble/Backends/Experiments.cs
new file mode 100644
index 00000000..3f3b3ac9
--- /dev/null
+++ b/src/Darp.Ble/Backends/Experiments.cs
@@ -0,0 +1,8 @@
+namespace Darp.Ble.Backends;
+
+/// Diagnostic ids of the API that may change in a minor release
+internal static class Experiments
+{
+ /// The driver layer, for backends: written for this library's backends and changed with them
+ public const string Backends = "DARPBLE0001";
+}
diff --git a/src/Darp.Ble/Backends/Gap/IAdvertisingSetDriver.cs b/src/Darp.Ble/Backends/Gap/IAdvertisingSetDriver.cs
index 4999c68c..af759e10 100644
--- a/src/Darp.Ble/Backends/Gap/IAdvertisingSetDriver.cs
+++ b/src/Darp.Ble/Backends/Gap/IAdvertisingSetDriver.cs
@@ -1,9 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
using Darp.Ble.Gap;
namespace Darp.Ble.Backends;
/// One advertising set. The core serializes the calls of a set and only changes parameters while it is stopped
+[Experimental(Experiments.Backends)]
public interface IAdvertisingSetDriver : IAsyncDisposable
{
/// The address the set advertises with
diff --git a/src/Darp.Ble/Backends/Gap/IAdvertisingSetSink.cs b/src/Darp.Ble/Backends/Gap/IAdvertisingSetSink.cs
index 4edecbbb..a9d1d6ba 100644
--- a/src/Darp.Ble/Backends/Gap/IAdvertisingSetSink.cs
+++ b/src/Darp.Ble/Backends/Gap/IAdvertisingSetSink.cs
@@ -1,10 +1,18 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Darp.Ble.Backends;
/// Receives what happens to an advertising set without the core asking. Called on the driver's thread; the core returns at once
+[Experimental(Experiments.Backends)]
public interface IAdvertisingSetSink
{
/// A central connected through the set, so the set stopped advertising
/// Reported for the run the set advertises in, before a later or completes
/// The connection, as reported to the peripheral role's before this call
void OnStoppedByConnection(ICentralConnectionDriver central);
+
+ /// The backend stopped the set because of an error, e.g. the radio turned off; it stays stopped until it is started again
+ /// Reported for the run the set advertises in, before a later or completes
+ /// Why the set stopped
+ void OnFailed(BleException error);
}
diff --git a/src/Darp.Ble/Backends/Gap/IBroadcasterDriver.cs b/src/Darp.Ble/Backends/Gap/IBroadcasterDriver.cs
index 9259200d..24d04d29 100644
--- a/src/Darp.Ble/Backends/Gap/IBroadcasterDriver.cs
+++ b/src/Darp.Ble/Backends/Gap/IBroadcasterDriver.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Gap;
namespace Darp.Ble.Backends;
/// The advertising of an adapter
+[Experimental(Experiments.Backends)]
public interface IBroadcasterDriver
{
/// Creates a stopped advertising set
diff --git a/src/Darp.Ble/Backends/Gap/IObserverDriver.cs b/src/Darp.Ble/Backends/Gap/IObserverDriver.cs
index e2791f59..422be67f 100644
--- a/src/Darp.Ble/Backends/Gap/IObserverDriver.cs
+++ b/src/Darp.Ble/Backends/Gap/IObserverDriver.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Gap;
namespace Darp.Ble.Backends;
/// The scanning of an adapter. The core runs at most one radio scan at a time and shares it between all scans of the app
+[Experimental(Experiments.Backends)]
public interface IObserverDriver
{
/// Starts scanning and reports every advertisement to the sink until stopped
diff --git a/src/Darp.Ble/Backends/Gap/IScanSink.cs b/src/Darp.Ble/Backends/Gap/IScanSink.cs
index 584f29b3..fab007fb 100644
--- a/src/Darp.Ble/Backends/Gap/IScanSink.cs
+++ b/src/Darp.Ble/Backends/Gap/IScanSink.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Gap;
namespace Darp.Ble.Backends;
/// Receives the reports of one radio scan. The core keeps every call cheap: it never blocks and never runs app code
+[Experimental(Experiments.Backends)]
public interface IScanSink
{
/// Reports an advertisement; may be called on the driver's read loop
diff --git a/src/Darp.Ble/Backends/Gatt/GattAttributeDefinition.cs b/src/Darp.Ble/Backends/Gatt/GattAttributeDefinition.cs
index f6f56a70..6ee10ab0 100644
--- a/src/Darp.Ble/Backends/Gatt/GattAttributeDefinition.cs
+++ b/src/Darp.Ble/Backends/Gatt/GattAttributeDefinition.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// A hosted attribute centrals read or write: a characteristic or a descriptor. The object itself identifies it
+[Experimental(Experiments.Backends)]
public abstract class GattAttributeDefinition
{
private protected GattAttributeDefinition(BleUuid uuid)
diff --git a/src/Darp.Ble/Backends/Gatt/GattCharacteristicDefinition.cs b/src/Darp.Ble/Backends/Gatt/GattCharacteristicDefinition.cs
index e5630595..1da40d92 100644
--- a/src/Darp.Ble/Backends/Gatt/GattCharacteristicDefinition.cs
+++ b/src/Darp.Ble/Backends/Gatt/GattCharacteristicDefinition.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
using Darp.Ble.Gatt;
@@ -5,6 +6,7 @@ namespace Darp.Ble.Backends;
/// A characteristic of an added service. The object itself identifies it, also when two have the same UUID
/// The backend adds the client characteristic configuration descriptor of a notifying characteristic itself
+[Experimental(Experiments.Backends)]
public sealed class GattCharacteristicDefinition : GattAttributeDefinition
{
internal GattCharacteristicDefinition(BleUuid uuid, GattProperty properties)
diff --git a/src/Darp.Ble/Backends/Gatt/GattDescriptorDefinition.cs b/src/Darp.Ble/Backends/Gatt/GattDescriptorDefinition.cs
index 230e6c1d..ccdb2320 100644
--- a/src/Darp.Ble/Backends/Gatt/GattDescriptorDefinition.cs
+++ b/src/Darp.Ble/Backends/Gatt/GattDescriptorDefinition.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// A descriptor of an added characteristic. The object itself identifies it
+[Experimental(Experiments.Backends)]
public sealed class GattDescriptorDefinition : GattAttributeDefinition
{
internal GattDescriptorDefinition(BleUuid uuid, ReadOnlyMemory? staticValue)
diff --git a/src/Darp.Ble/Backends/Gatt/GattRequest.cs b/src/Darp.Ble/Backends/Gatt/GattRequest.cs
index c42f6220..b47cdd82 100644
--- a/src/Darp.Ble/Backends/Gatt/GattRequest.cs
+++ b/src/Darp.Ble/Backends/Gatt/GattRequest.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Connections;
namespace Darp.Ble.Backends;
@@ -7,6 +8,7 @@ namespace Darp.Ble.Backends;
/// A request takes one or more ATT transactions, e.g. discovery page by page, and reports each one, so the core can time out
/// a peer that stops answering. A write without response has no transaction and gets its tokens directly
///
+[Experimental(Experiments.Backends)]
public sealed class GattRequest
{
private readonly RequestQueue _queue;
diff --git a/src/Darp.Ble/Backends/Gatt/GattServiceDefinition.cs b/src/Darp.Ble/Backends/Gatt/GattServiceDefinition.cs
index df940caa..e1b92aaa 100644
--- a/src/Darp.Ble/Backends/Gatt/GattServiceDefinition.cs
+++ b/src/Darp.Ble/Backends/Gatt/GattServiceDefinition.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// A complete service to add; it does not change afterwards
+[Experimental(Experiments.Backends)]
public sealed class GattServiceDefinition
{
internal GattServiceDefinition(
diff --git a/src/Darp.Ble/Backends/Gatt/IGattServerSink.cs b/src/Darp.Ble/Backends/Gatt/IGattServerSink.cs
index 891e51e7..25aa8f30 100644
--- a/src/Darp.Ble/Backends/Gatt/IGattServerSink.cs
+++ b/src/Darp.Ble/Backends/Gatt/IGattServerSink.cs
@@ -1,9 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Connections;
using Darp.Ble.Gatt;
namespace Darp.Ble.Backends;
/// Receives what centrals do with the adapter's peripheral role. Called on the driver's thread; the core returns at once
+[Experimental(Experiments.Backends)]
public interface IGattServerSink
{
/// A central connected
diff --git a/src/Darp.Ble/Backends/Gatt/IRemoteCharacteristicDriver.cs b/src/Darp.Ble/Backends/Gatt/IRemoteCharacteristicDriver.cs
index dc03ebd3..3c4d0d3b 100644
--- a/src/Darp.Ble/Backends/Gatt/IRemoteCharacteristicDriver.cs
+++ b/src/Darp.Ble/Backends/Gatt/IRemoteCharacteristicDriver.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
using Darp.Ble.Gatt;
@@ -5,6 +6,7 @@ namespace Darp.Ble.Backends;
/// A characteristic discovered on a peripheral. Belongs to its connection, which returns the same object for the same characteristic every time
/// Its operations fail as describes
+[Experimental(Experiments.Backends)]
public interface IRemoteCharacteristicDriver
{
/// The UUID of the characteristic
diff --git a/src/Darp.Ble/Backends/Gatt/IRemoteDescriptorDriver.cs b/src/Darp.Ble/Backends/Gatt/IRemoteDescriptorDriver.cs
index ede3d846..280508a5 100644
--- a/src/Darp.Ble/Backends/Gatt/IRemoteDescriptorDriver.cs
+++ b/src/Darp.Ble/Backends/Gatt/IRemoteDescriptorDriver.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
using Darp.Ble.Gatt;
@@ -5,6 +6,7 @@ namespace Darp.Ble.Backends;
/// A descriptor discovered on a peripheral. Belongs to its connection
/// Its operations fail as describes
+[Experimental(Experiments.Backends)]
public interface IRemoteDescriptorDriver
{
/// The UUID of the descriptor
diff --git a/src/Darp.Ble/Backends/Gatt/IRemoteServiceDriver.cs b/src/Darp.Ble/Backends/Gatt/IRemoteServiceDriver.cs
index 43bedba3..e228f744 100644
--- a/src/Darp.Ble/Backends/Gatt/IRemoteServiceDriver.cs
+++ b/src/Darp.Ble/Backends/Gatt/IRemoteServiceDriver.cs
@@ -1,9 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// A service discovered on a peripheral. Belongs to its connection; the core queues its requests with the connection's
/// Its operations fail as describes
+[Experimental(Experiments.Backends)]
public interface IRemoteServiceDriver
{
/// The UUID of the service
diff --git a/src/Darp.Ble/Backends/IBleAdapterDriver.cs b/src/Darp.Ble/Backends/IBleAdapterDriver.cs
index 594ef14d..3a0fe4f2 100644
--- a/src/Darp.Ble/Backends/IBleAdapterDriver.cs
+++ b/src/Darp.Ble/Backends/IBleAdapterDriver.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Data;
namespace Darp.Ble.Backends;
/// The driver of an opened adapter. The core wraps it in a and disposes it once
+[Experimental(Experiments.Backends)]
public interface IBleAdapterDriver : IAsyncDisposable
{
/// The features beyond the roles, e.g. ; the core adds the roles of the role drivers
diff --git a/src/Darp.Ble/Backends/IBleAdapterSource.cs b/src/Darp.Ble/Backends/IBleAdapterSource.cs
index fd072b51..6f1d8c28 100644
--- a/src/Darp.Ble/Backends/IBleAdapterSource.cs
+++ b/src/Darp.Ble/Backends/IBleAdapterSource.cs
@@ -1,6 +1,9 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Darp.Ble.Backends;
/// One adapter a backend enumerated; the core shows it to apps as
+[Experimental(Experiments.Backends)]
public interface IBleAdapterSource
{
/// The name of the adapter, if the backend knows one
diff --git a/src/Darp.Ble/Backends/IBleBackend.cs b/src/Darp.Ble/Backends/IBleBackend.cs
index 9dabed55..22efdf0d 100644
--- a/src/Darp.Ble/Backends/IBleBackend.cs
+++ b/src/Darp.Ble/Backends/IBleBackend.cs
@@ -1,7 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Darp.Ble.Backends;
/// A backend that connects the library to a platform or a radio, e.g. the mock, HCI or WinRT backend
/// Implemented only by the backends of this library; apps register backends through their builder extensions
+[Experimental(Experiments.Backends)]
public interface IBleBackend
{
/// The name apps see as , e.g.
diff --git a/src/Darp.Ble/BleBackends.cs b/src/Darp.Ble/BleBackends.cs
index 2addf4f5..df546107 100644
--- a/src/Darp.Ble/BleBackends.cs
+++ b/src/Darp.Ble/BleBackends.cs
@@ -8,4 +8,7 @@ public static class BleBackends
/// The HCI backend from the Darp.Ble.HciHost package, which drives a controller over a serial port
public const string HciHost = "Darp.Ble.HciHost";
+
+ /// The Windows backend from the Darp.Ble.WinRT package, on Windows' own Bluetooth stack
+ public const string WinRT = "Darp.Ble.WinRT";
}
diff --git a/src/Darp.Ble/BleManagerBuilder.cs b/src/Darp.Ble/BleManagerBuilder.cs
index 20b2ace6..27725a85 100644
--- a/src/Darp.Ble/BleManagerBuilder.cs
+++ b/src/Darp.Ble/BleManagerBuilder.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Darp.Ble.Backends;
using Darp.Ble.Diagnostics;
@@ -15,6 +16,7 @@ internal BleManagerBuilder() { }
/// Adds a backend. Backend packages call this from their extensions, e.g. AddMock()
/// The backend to add; adapters are enumerated in the order backends were added
/// This builder
+ [Experimental(Experiments.Backends)]
public BleManagerBuilder AddBackend(IBleBackend backend)
{
ArgumentNullException.ThrowIfNull(backend);
diff --git a/src/Darp.Ble/Connections/BleCentral.cs b/src/Darp.Ble/Connections/BleCentral.cs
index 7cba69a3..0d7777c2 100644
--- a/src/Darp.Ble/Connections/BleCentral.cs
+++ b/src/Darp.Ble/Connections/BleCentral.cs
@@ -68,7 +68,7 @@ public IReadOnlyList Connections
/// so pass one that times out. The Mock fails at once for an address nobody advertises connectable.
/// The adapter's scans pause while the link comes up and continue afterwards
///
- /// The address of the peripheral
+ /// The address of the peripheral; without its type (, e.g. parsed from text) the backend uses the type the device was heard with, so scan for it first
/// How the connection runs; the defaults when null
/// Cancels connecting; the only limit on how long it takes
/// The connection once the link is up. The caller owns it; disposing the adapter ends it too
diff --git a/src/Darp.Ble/Connections/ConnectedPeripheral.cs b/src/Darp.Ble/Connections/ConnectedPeripheral.cs
index 79514f72..75cd9e81 100644
--- a/src/Darp.Ble/Connections/ConnectedPeripheral.cs
+++ b/src/Darp.Ble/Connections/ConnectedPeripheral.cs
@@ -118,7 +118,7 @@ public async Task DiscoverServiceAsync(
?? throw new GattException(GattErrorKind.NotFound, $"The peripheral has no service {uuid}");
}
- /// The longest value a write can carry, from the MTU agreed when connecting
+ /// The longest value a write can carry, from the MTU agreed when connecting; it may grow later, e.g. on Windows
/// Longer writes are rejected on every backend; a notification longer than this is cut to it
public int MaxWritePayloadLength => _maxWritePayloadLength;
@@ -272,7 +272,17 @@ internal async Task ExchangeMtuAsync(CancellationToken cancellationToken)
cancellationToken
)
.ConfigureAwait(false);
- _maxWritePayloadLength = mtu - WriteHeaderLength;
+ GrowMtu(mtu);
+ }
+
+ /// The backend reports a larger MTU than the exchange returned
+ internal void OnMtuChanged(int mtu) => GrowMtu(mtu);
+
+ /// Keeps the larger MTU: an MTU never shrinks, and the exchange and a change may arrive in either order
+ private void GrowMtu(int mtu)
+ {
+ lock (_lock)
+ _maxWritePayloadLength = Math.Max(_maxWritePayloadLength, mtu - WriteHeaderLength);
}
/// Ends the connection exactly once; the first reason wins
diff --git a/src/Darp.Ble/Connections/ConnectionEvents.cs b/src/Darp.Ble/Connections/ConnectionEvents.cs
index f16a8d52..512847f4 100644
--- a/src/Darp.Ble/Connections/ConnectionEvents.cs
+++ b/src/Darp.Ble/Connections/ConnectionEvents.cs
@@ -30,6 +30,15 @@ public void OnNotification(IRemoteCharacteristicDriver characteristic, ReadOnlyS
connection?.OnNotification(characteristic, value);
}
+ /// A change before the connection object exists is part of the exchange that follows
+ public void OnMtuChanged(int mtu)
+ {
+ ConnectedPeripheral? connection;
+ lock (_lock)
+ connection = _connection;
+ connection?.OnMtuChanged(mtu);
+ }
+
public void Attach(ConnectedPeripheral connection)
{
DisconnectInfo? earlyDisconnect;
diff --git a/src/Darp.Ble/Data/BleAddress.cs b/src/Darp.Ble/Data/BleAddress.cs
index 8bb41761..36ea7371 100644
--- a/src/Darp.Ble/Data/BleAddress.cs
+++ b/src/Darp.Ble/Data/BleAddress.cs
@@ -5,7 +5,7 @@
namespace Darp.Ble.Data;
/// The ble address
-public sealed record BleAddress : ISpanParsable, IEquatable, IEquatable
+public sealed record BleAddress : ISpanParsable, IFormattable, IEquatable, IEquatable
{
/// Initializes a new ble address
/// The 48bit address
@@ -71,22 +71,20 @@ public static implicit operator UInt48(BleAddress bleAddress)
)]
public static explicit operator BleAddress(UInt48 value) => new(value);
- /// Parses a string of suitable format and returns a ble address
- /// The string to parse.
- /// An object that provides culture-specific formatting information about . Will be ignored
- /// AA:BB:CC:DD:EE:FF
- /// The parsed BleAddress with type
+ /// Parses an address as writes it
+ /// The address: AA:BB:CC:DD:EE:FF, or with its type as P:AA:BB:CC:DD:EE:FF (public) or R:AA:BB:CC:DD:EE:FF (random)
+ /// Ignored
+ /// The address; without a type in the text, of type
/// Thrown if the format does not comply.
public static BleAddress Parse(ReadOnlySpan s, IFormatProvider? provider) =>
TryParse(s, provider, out BleAddress? address)
? address
: throw new FormatException($"Given input '{s}' could not be parsed");
- /// Parses a string of suitable format and returns a ble address
- /// The string to parse.
- /// An object that provides culture-specific formatting information about . Will be ignored
- /// The parsed BleAddress with type or default
- /// AA:BB:CC:DD:EE:FF
+ /// Parses an address as writes it
+ /// The address: AA:BB:CC:DD:EE:FF, or with its type as P:AA:BB:CC:DD:EE:FF (public) or R:AA:BB:CC:DD:EE:FF (random)
+ /// Ignored
+ /// The address; without a type in the text, of type
/// True if the parsing was successful
public static bool TryParse(
ReadOnlySpan s,
@@ -94,63 +92,42 @@ public static bool TryParse(
[NotNullWhen(true)] out BleAddress? result
)
{
- if (s.Length < 17)
+ result = null;
+ BleAddressType type = BleAddressType.NotAvailable;
+ if (s.Length == 19 && s[1] == ':')
{
- result = null;
- return false;
- }
- if (s[2] != ':' || s[5] != ':' || s[8] != ':' || s[11] != ':' || s[14] != ':')
- {
- result = null;
- return false;
+ type = s[0] switch
+ {
+ 'P' => BleAddressType.Public,
+ 'R' => BleAddressType.Random,
+ _ => BleAddressType.NotAvailable,
+ };
+ if (type is BleAddressType.NotAvailable)
+ return false;
+ s = s[2..];
}
- if (
- !TryGetHexVal(s, out byte b0)
- || !TryGetHexVal(s[3..], out byte b1)
- || !TryGetHexVal(s[6..], out byte b2)
- || !TryGetHexVal(s[9..], out byte b3)
- || !TryGetHexVal(s[12..], out byte b4)
- || !TryGetHexVal(s[15..], out byte b5)
- )
- {
- result = null;
+ if (s.Length != 17)
return false;
- }
- var value = new UInt48(b5, b4, b3, b2, b1, b0);
- result = new BleAddress(BleAddressType.NotAvailable, value);
- return true;
- }
-
- private static bool TryGetHexVal(in ReadOnlySpan s, out byte res)
- {
- // The calling method already checked that the span is at least 2 characters long
- int lower = GetHexVal(s[1]);
- int upper = GetHexVal(s[0]);
- if (lower > 15 || upper > 15)
+ ulong value = 0;
+ for (var i = 0; i < 6; i++)
{
- res = default;
- return false;
+ if (i > 0 && s[(3 * i) - 1] != ':')
+ return false;
+ if (
+ !byte.TryParse(
+ s.Slice(3 * i, 2),
+ NumberStyles.AllowHexSpecifier,
+ CultureInfo.InvariantCulture,
+ out byte b
+ )
+ )
+ return false;
+ value = (value << 8) | b;
}
- res = (byte)((upper << 4) + lower);
+ result = new BleAddress(type, (UInt48)value);
return true;
}
- private static int GetHexVal(char hex)
- {
- int val = hex;
- //For uppercase A-F letters:
- //return val - (val < 58 ? 48 : 55);
- //For lowercase a-f letters:
- //return val - (val < 58 ? 48 : 87);
- //Or the two combined, but a bit slower:
- return val
- - (
- val < 58 ? 48
- : val < 97 ? 55
- : 87
- );
- }
-
///
public static BleAddress Parse(string s, IFormatProvider? provider) => Parse((ReadOnlySpan)s, provider);
@@ -171,11 +148,30 @@ public static bool TryParse(
public static BleAddress NotAvailable { get; } = new(BleAddressType.NotAvailable, UInt48.Zero);
/// The address as six hex bytes separated by colons, most significant first, e.g. AA:BB:CC:DD:EE:FF
- /// reads it back without the type
+ /// reads it back without the type; the format "T" keeps it
/// The formatted address
public override string ToString() =>
string.Join(':', Value.ToString("X12", CultureInfo.InvariantCulture).Chunk(2).Select(pair => new string(pair)));
+ /// The address in a format: null or empty as ; "T" with its type in front, P: for public and R: for random addresses
+ /// Only public and random addresses get a type; reads both forms back
+ /// Null, empty or "T"
+ /// Ignored
+ /// The formatted address
+ /// Thrown for any other format
+ public string ToString(string? format, IFormatProvider? formatProvider) =>
+ format switch
+ {
+ null or "" => ToString(),
+ "T" => Type switch
+ {
+ BleAddressType.Public => $"P:{this}",
+ BleAddressType.Random => $"R:{this}",
+ _ => ToString(),
+ },
+ _ => throw new FormatException($"The format '{format}' is not supported; use \"T\" or none"),
+ };
+
///
public bool Equals(BleAddress? other)
{
diff --git a/src/Darp.Ble/Gap/AdvertisementStreamExtensions.cs b/src/Darp.Ble/Gap/AdvertisementStreamExtensions.cs
index 873f0664..e9a36fb2 100644
--- a/src/Darp.Ble/Gap/AdvertisementStreamExtensions.cs
+++ b/src/Darp.Ble/Gap/AdvertisementStreamExtensions.cs
@@ -8,14 +8,19 @@ public static class AdvertisementStreamExtensions
{
/// Only advertisements from this address; all of them when the address is null
/// The advertisements
- /// The address to keep
+ /// The address to keep; without its type (), of any type
/// The type of the advertisements
/// The filtered advertisements
public static IAsyncEnumerable WhereAddress(this IAsyncEnumerable source, BleAddress? address)
where TAdv : IGapAdvertisement
{
ArgumentNullException.ThrowIfNull(source);
- return address is null ? source : source.Where(adv => adv.Address.Equals(address));
+ if (address is null)
+ return source;
+ // An address without its type, e.g. parsed from text, matches its value of any type
+ return address.Type is BleAddressType.NotAvailable
+ ? source.Where(adv => adv.Address.Value == address.Value)
+ : source.Where(adv => adv.Address.Equals(address));
}
/// Only advertisements from this address value; all of them when the address is null
diff --git a/src/Darp.Ble/Gap/AdvertisingSet.cs b/src/Darp.Ble/Gap/AdvertisingSet.cs
index 20b44dfd..57ce7b24 100644
--- a/src/Darp.Ble/Gap/AdvertisingSet.cs
+++ b/src/Darp.Ble/Gap/AdvertisingSet.cs
@@ -9,7 +9,8 @@ namespace Darp.Ble.Gap;
/// An advertising set: what an adapter advertises, and how
///
/// Its operations run one at a time. Its state changes only after the backend accepted a change.
-/// A central that connects through the set stops it; with autoRestart the set starts again once that central disconnected
+/// A central that connects through the set stops it; with autoRestart the set starts again once that central disconnected.
+/// A set the backend stopped because of an error, e.g. the radio turned off, stays stopped until it is started again
///
[SuppressMessage(
"Usage",
@@ -95,14 +96,7 @@ public Task StartAsync(CancellationToken cancellationToken = default) =>
/// Cancels stopping
/// A task that completes once the set stopped
public Task StopAsync(CancellationToken cancellationToken = default) =>
- RunAsync(
- () =>
- {
- CancelPendingRestart();
- return StopCoreAsync(cancellationToken);
- },
- cancellationToken
- );
+ RunAsync(() => StopCoreAsync(cancellationToken), cancellationToken);
/// Changes how the set advertises. Controllers reject this while advertising
/// The new parameters
@@ -214,7 +208,8 @@ private async Task DisposeCoreAsync()
{
lock (_lock)
_isDisposed = true;
- CancelPendingRestart();
+ // Closed first: a central connecting now admits no restart that disposal would wait for
+ CloseAdmission();
await StopCoreAsync(CancellationToken.None).ConfigureAwait(false);
}
finally
@@ -261,6 +256,18 @@ internal void OnStoppedByConnection(ICentralConnectionDriver central)
_ = RestartAfterDisconnectAsync(central, restart);
}
+ /// The backend stopped the set because of an error. Called on the driver's thread
+ internal void OnFailed(BleException error)
+ {
+ lock (_lock)
+ {
+ if (!_isAdvertising)
+ return;
+ _isAdvertising = false;
+ }
+ _logger.LogAdvertisingFailed(RandomAddress, error);
+ }
+
/// Starts the set again once the central disconnected, unless a command or disposal cancelled the restart
private async Task RestartAfterDisconnectAsync(ICentralConnectionDriver central, CancellationTokenSource restart)
{
@@ -355,8 +362,11 @@ private async Task StartCoreAsync(CancellationToken cancellationToken)
private async Task StopCoreAsync(CancellationToken cancellationToken)
{
+ // In one step with the connection callback's check: a central connecting now admits no restart
lock (_lock)
{
+ _pendingRestart?.Cancel();
+ _pendingRestart = null;
if (!_isAdvertising)
return;
_isStopping = true;
diff --git a/src/Darp.Ble/Gap/AdvertisingSetEvents.cs b/src/Darp.Ble/Gap/AdvertisingSetEvents.cs
index 0d6057e3..2facab4d 100644
--- a/src/Darp.Ble/Gap/AdvertisingSetEvents.cs
+++ b/src/Darp.Ble/Gap/AdvertisingSetEvents.cs
@@ -11,4 +11,6 @@ internal sealed class AdvertisingSetEvents : IAdvertisingSetSink
public void OnStoppedByConnection(ICentralConnectionDriver central) =>
Volatile.Read(ref _set)?.OnStoppedByConnection(central);
+
+ public void OnFailed(BleException error) => Volatile.Read(ref _set)?.OnFailed(error);
}
diff --git a/src/Darp.Ble/Gap/GapAdvertisement.cs b/src/Darp.Ble/Gap/GapAdvertisement.cs
index 88b414a5..1f9c047e 100644
--- a/src/Darp.Ble/Gap/GapAdvertisement.cs
+++ b/src/Darp.Ble/Gap/GapAdvertisement.cs
@@ -1,31 +1,37 @@
-using System.Buffers.Binary;
+using System.Diagnostics.CodeAnalysis;
+using Darp.Ble.Backends;
using Darp.Ble.Data;
namespace Darp.Ble.Gap;
-/// An advertising report. Immutable: it owns one copy of the report's bytes, which every scan shares
+/// A received advertisement; immutable, so every scan shares it
public sealed class GapAdvertisement : IGapAdvertisement
{
- private const int HeaderLength = 24;
-
- private readonly ReadOnlyMemory _bytes;
-
- private GapAdvertisement(DateTimeOffset timestamp, ReadOnlyMemory bytes)
+ private GapAdvertisement(
+ DateTimeOffset timestamp,
+ BleEventType eventType,
+ BleAddress address,
+ Physical primaryPhy,
+ Physical secondaryPhy,
+ AdvertisingSId advertisingSId,
+ TxPowerLevel txPower,
+ Rssi rssi,
+ PeriodicAdvertisingInterval periodicAdvertisingInterval,
+ BleAddress directAddress,
+ AdvertisingData data
+ )
{
- ReadOnlySpan span = bytes.Span;
Timestamp = timestamp;
- _bytes = bytes;
- EventType = (BleEventType)BinaryPrimitives.ReadUInt16LittleEndian(span);
- Address = new BleAddress((BleAddressType)span[2], UInt48.ReadLittleEndian(span[3..]));
- PrimaryPhy = (Physical)span[9];
- SecondaryPhy = (Physical)span[10];
- AdvertisingSId = (AdvertisingSId)span[11];
- TxPower = (TxPowerLevel)span[12];
- Rssi = (Rssi)span[13];
- PeriodicAdvertisingInterval = (PeriodicAdvertisingInterval)BinaryPrimitives.ReadUInt16LittleEndian(span[14..]);
- DirectAddress = new BleAddress((BleAddressType)span[16], UInt48.ReadLittleEndian(span[17..]));
- byte dataLength = span[23];
- Data = AdvertisingData.FromOwned(bytes[HeaderLength..(HeaderLength + dataLength)]);
+ EventType = eventType;
+ Address = address;
+ PrimaryPhy = primaryPhy;
+ SecondaryPhy = secondaryPhy;
+ AdvertisingSId = advertisingSId;
+ TxPower = txPower;
+ Rssi = rssi;
+ PeriodicAdvertisingInterval = periodicAdvertisingInterval;
+ DirectAddress = directAddress;
+ Data = data;
}
///
@@ -61,19 +67,8 @@ private GapAdvertisement(DateTimeOffset timestamp, ReadOnlyMemory bytes)
///
public AdvertisingData Data { get; }
- ///
- public ReadOnlyMemory AsReadOnlyMemory() => _bytes;
-
- /// Create an advertisement from the bytes of an extended advertising report. The bytes are copied
- /// BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 4, Part E, 7.7.65.13 LE Extended Advertising Report event
- /// The time the report was received
- /// The bytes of a single extended advertising report
- /// The advertisement
- public static GapAdvertisement FromExtendedAdvertisingReport(DateTimeOffset timestamp, ReadOnlySpan report) =>
- new(timestamp, report.ToArray());
-
- /// Create an advertisement from its values
- /// The time the report was received
+ /// Create an advertisement; backends report what they received with it
+ /// The time the advertisement was received
/// The type of the advertising event
/// The address of the advertising device
/// The primary physical the advertising device used
@@ -83,9 +78,10 @@ public static GapAdvertisement FromExtendedAdvertisingReport(DateTimeOffset time
/// The Rssi of the advertising device
/// The interval of periodic advertisements
/// The address of the device the advertisement is directed to
- /// The data sections
+ /// The data sections of the whole advertisement
/// The advertisement
- public static GapAdvertisement FromExtendedAdvertisingReport(
+ [Experimental(Experiments.Backends)]
+ public static GapAdvertisement Create(
DateTimeOffset timestamp,
BleEventType eventType,
BleAddress address,
@@ -96,33 +92,39 @@ public static GapAdvertisement FromExtendedAdvertisingReport(
Rssi rssi,
PeriodicAdvertisingInterval periodicAdvertisingInterval,
BleAddress directAddress,
- AdvertisingData advertisingData
+ AdvertisingData data
)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(directAddress);
- ArgumentNullException.ThrowIfNull(advertisingData);
- ReadOnlySpan dataSpan = advertisingData.AsReadOnlyMemory().Span;
- var bytes = new byte[HeaderLength + dataSpan.Length];
- Span buffer = bytes;
- BinaryPrimitives.WriteUInt16LittleEndian(buffer, (ushort)eventType);
- buffer[2] = (byte)address.Type;
- UInt48.WriteLittleEndian(buffer[3..], address.Value);
- buffer[9] = (byte)primaryPhy;
- buffer[10] = (byte)secondaryPhy;
- buffer[11] = (byte)advertisingSId;
- buffer[12] = (byte)txPower;
- buffer[13] = (byte)rssi;
- BinaryPrimitives.WriteUInt16LittleEndian(buffer[14..], (ushort)periodicAdvertisingInterval);
- buffer[16] = (byte)directAddress.Type;
- UInt48.WriteLittleEndian(buffer[17..], directAddress.Value);
- buffer[23] = (byte)dataSpan.Length;
- dataSpan.CopyTo(buffer[HeaderLength..]);
- return new GapAdvertisement(timestamp, bytes);
+ ArgumentNullException.ThrowIfNull(data);
+ return new GapAdvertisement(
+ timestamp,
+ eventType,
+ address,
+ primaryPhy,
+ secondaryPhy,
+ advertisingSId,
+ txPower,
+ rssi,
+ periodicAdvertisingInterval,
+ directAddress,
+ data
+ );
}
private bool Equals(GapAdvertisement other) =>
- Timestamp.Equals(other.Timestamp) && _bytes.Span.SequenceEqual(other._bytes.Span);
+ Timestamp.Equals(other.Timestamp)
+ && EventType == other.EventType
+ && Address.Equals(other.Address)
+ && PrimaryPhy == other.PrimaryPhy
+ && SecondaryPhy == other.SecondaryPhy
+ && AdvertisingSId == other.AdvertisingSId
+ && TxPower == other.TxPower
+ && Rssi == other.Rssi
+ && PeriodicAdvertisingInterval == other.PeriodicAdvertisingInterval
+ && DirectAddress.Equals(other.DirectAddress)
+ && Data.AsReadOnlyMemory().Span.SequenceEqual(other.Data.AsReadOnlyMemory().Span);
///
public bool Equals(IGapAdvertisement? other) =>
@@ -135,10 +137,11 @@ public override bool Equals(object? obj) =>
///
public override int GetHashCode()
{
- // The same values as Equals: the timestamp and the bytes of the report
+ // A subset of what Equals compares: the timestamp, the sender and the data
var hash = new HashCode();
hash.Add(Timestamp);
- hash.AddBytes(_bytes.Span);
+ hash.Add(Address);
+ hash.AddBytes(Data.AsReadOnlyMemory().Span);
return hash.ToHashCode();
}
}
@@ -191,9 +194,6 @@ public GapAdvertisement(IGapAdvertisement advertisement, TUserData userData)
///
public AdvertisingData Data => _advertisement.Data;
- ///
- public ReadOnlyMemory AsReadOnlyMemory() => _advertisement.AsReadOnlyMemory();
-
///
public TUserData UserData { get; }
object? IGapAdvertisementWithUserData.UserData => UserData;
diff --git a/src/Darp.Ble/Gap/IGapAdvertisement.cs b/src/Darp.Ble/Gap/IGapAdvertisement.cs
index fe469597..90da8512 100644
--- a/src/Darp.Ble/Gap/IGapAdvertisement.cs
+++ b/src/Darp.Ble/Gap/IGapAdvertisement.cs
@@ -37,20 +37,8 @@ public interface IGapAdvertisement : IEquatable
/// The address of the device the advertisement is directed to
BleAddress DirectAddress { get; }
- /// The data sections
+ /// The data sections of the whole advertisement; an HCI controller may report it in several parts, which the backend joins
AdvertisingData Data { get; }
-
- /// The advertising report in the layout of a single report of the HCI LE Extended Advertising Report event
- ///
- /// The bytes start at Event_Type and do not include the event header. They are read-only; every scan shares them, and the call does not copy.
- /// Only the HCI host receives these bytes from the controller.
- /// Other backends, e.g. WinRT or Android, build them from the values the platform reports.
- /// A value the platform does not report holds its "not available" value,
- /// e.g. (0) for both PHYs on Windows, which a controller never reports as primary PHY.
- /// Treat the bytes as a received HCI report only if the advertisement came from the HCI host.
- ///
- /// The bytes of the advertising report
- ReadOnlyMemory AsReadOnlyMemory();
}
/// An advertisement with attached user data
diff --git a/src/Darp.Ble/Gatt/Server/AesCmac.cs b/src/Darp.Ble/Gatt/Server/AesCmac.cs
index 9833240b..bf539ddd 100644
--- a/src/Darp.Ble/Gatt/Server/AesCmac.cs
+++ b/src/Darp.Ble/Gatt/Server/AesCmac.cs
@@ -1,8 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
+using Darp.Ble.Backends;
namespace Darp.Ble.Gatt;
/// The AES-CMAC encryption algorithm
+[Experimental(Experiments.Backends)]
public sealed class AesCmac : IDisposable
{
private readonly Aes _aes;
diff --git a/src/Darp.Ble/Logging.cs b/src/Darp.Ble/Logging.cs
index a4100ab4..f131ce5d 100644
--- a/src/Darp.Ble/Logging.cs
+++ b/src/Darp.Ble/Logging.cs
@@ -102,6 +102,14 @@ public static partial void LogAdvertisingRestartFailed(
Exception exception
);
+ [LoggerMessage(
+ EventId = 1203,
+ EventName = "AdvertisingFailed",
+ Level = LogLevel.Warning,
+ Message = "Advertising set {Address} stopped because of an error"
+ )]
+ public static partial void LogAdvertisingFailed(this ILogger logger, BleAddress address, Exception exception);
+
[LoggerMessage(
EventId = 1300,
EventName = "Connecting",
diff --git a/src/Darp.Ble/PublicAPI.Unshipped.txt b/src/Darp.Ble/PublicAPI.Unshipped.txt
index 2a202efc..39763a75 100644
--- a/src/Darp.Ble/PublicAPI.Unshipped.txt
+++ b/src/Darp.Ble/PublicAPI.Unshipped.txt
@@ -1,94 +1,4 @@
#nullable enable
-Darp.Ble.Backends.BleBackendContext
-Darp.Ble.Backends.BleBackendContext.IncludeAddresses.get -> bool
-Darp.Ble.Backends.BleBackendContext.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory!
-Darp.Ble.Backends.BleBackendContext.TimeProvider.get -> System.TimeProvider!
-Darp.Ble.Backends.GattAttributeDefinition
-Darp.Ble.Backends.GattAttributeDefinition.Uuid.get -> Darp.Ble.Data.BleUuid!
-Darp.Ble.Backends.GattCharacteristicDefinition
-Darp.Ble.Backends.GattCharacteristicDefinition.Descriptors.get -> System.Collections.Generic.IReadOnlyList!
-Darp.Ble.Backends.GattCharacteristicDefinition.Properties.get -> Darp.Ble.Gatt.GattProperty
-Darp.Ble.Backends.GattDescriptorDefinition
-Darp.Ble.Backends.GattDescriptorDefinition.StaticValue.get -> System.ReadOnlyMemory?
-Darp.Ble.Backends.GattRequest
-Darp.Ble.Backends.GattRequest.HandOffToken.get -> System.Threading.CancellationToken
-Darp.Ble.Backends.GattRequest.IsAbandoned.get -> bool
-Darp.Ble.Backends.GattRequest.OnTransactionAnswered() -> void
-Darp.Ble.Backends.GattRequest.OnTransactionSent() -> void
-Darp.Ble.Backends.GattServiceDefinition
-Darp.Ble.Backends.GattServiceDefinition.Characteristics.get -> System.Collections.Generic.IReadOnlyList!
-Darp.Ble.Backends.GattServiceDefinition.IsPrimary.get -> bool
-Darp.Ble.Backends.GattServiceDefinition.Uuid.get -> Darp.Ble.Data.BleUuid!
-Darp.Ble.Backends.IAdvertisingSetDriver
-Darp.Ble.Backends.IAdvertisingSetDriver.RandomAddress.get -> Darp.Ble.Data.BleAddress!
-Darp.Ble.Backends.IAdvertisingSetDriver.SetDataAsync(Darp.Ble.Gap.AdvertisingData! data, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IAdvertisingSetDriver.SetParametersAsync(Darp.Ble.Gap.AdvertisingParameters! parameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IAdvertisingSetDriver.SetRandomAddressAsync(Darp.Ble.Data.BleAddress! address, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IAdvertisingSetDriver.SetScanResponseDataAsync(Darp.Ble.Gap.AdvertisingData! data, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IAdvertisingSetDriver.StartAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IAdvertisingSetDriver.StopAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IAdvertisingSetSink
-Darp.Ble.Backends.IAdvertisingSetSink.OnStoppedByConnection(Darp.Ble.Backends.ICentralConnectionDriver! central) -> void
-Darp.Ble.Backends.IBleAdapterDriver
-Darp.Ble.Backends.IBleAdapterDriver.Broadcaster.get -> Darp.Ble.Backends.IBroadcasterDriver?
-Darp.Ble.Backends.IBleAdapterDriver.Central.get -> Darp.Ble.Backends.ICentralDriver?
-Darp.Ble.Backends.IBleAdapterDriver.Features.get -> Darp.Ble.Capabilities
-Darp.Ble.Backends.IBleAdapterDriver.Observer.get -> Darp.Ble.Backends.IObserverDriver?
-Darp.Ble.Backends.IBleAdapterDriver.Peripheral.get -> Darp.Ble.Backends.IPeripheralDriver?
-Darp.Ble.Backends.IBleAdapterDriver.RandomAddress.get -> Darp.Ble.Data.BleAddress!
-Darp.Ble.Backends.IBleAdapterDriver.SetRandomAddressAsync(Darp.Ble.Data.BleAddress! address, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IBleAdapterSource
-Darp.Ble.Backends.IBleAdapterSource.Name.get -> string?
-Darp.Ble.Backends.IBleAdapterSource.OpenAsync(Darp.Ble.Backends.BleBackendContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IBleBackend
-Darp.Ble.Backends.IBleBackend.EnumerateAdaptersAsync(Darp.Ble.Backends.BleBackendContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>!
-Darp.Ble.Backends.IBleBackend.Name.get -> string!
-Darp.Ble.Backends.IBroadcasterDriver
-Darp.Ble.Backends.IBroadcasterDriver.CreateAdvertisingSetAsync(Darp.Ble.Gap.AdvertisingParameters! parameters, Darp.Ble.Gap.AdvertisingData! data, Darp.Ble.Gap.AdvertisingData? scanResponseData, Darp.Ble.Backends.IAdvertisingSetSink! sink, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.ICentralConnectionDriver
-Darp.Ble.Backends.ICentralConnectionDriver.Address.get -> Darp.Ble.Data.BleAddress!
-Darp.Ble.Backends.ICentralDriver
-Darp.Ble.Backends.ICentralDriver.ConnectAsync(Darp.Ble.Data.BleAddress! address, Darp.Ble.Connections.BleConnectionParameters! parameters, Darp.Ble.Backends.IConnectionSink! sink, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IConnectionDriver
-Darp.Ble.Backends.IConnectionDriver.DiscoverServicesAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!>!
-Darp.Ble.Backends.IConnectionDriver.ExchangeMtuAsync(int clientMtu, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IConnectionSink
-Darp.Ble.Backends.IConnectionSink.OnDisconnected(Darp.Ble.Connections.DisconnectInfo! info) -> void
-Darp.Ble.Backends.IConnectionSink.OnNotification(Darp.Ble.Backends.IRemoteCharacteristicDriver! characteristic, System.ReadOnlySpan value) -> void
-Darp.Ble.Backends.IGattServerSink
-Darp.Ble.Backends.IGattServerSink.OnCentralConnected(Darp.Ble.Backends.ICentralConnectionDriver! central) -> void
-Darp.Ble.Backends.IGattServerSink.OnCentralDisconnected(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Connections.DisconnectInfo! info) -> void
-Darp.Ble.Backends.IGattServerSink.OnReadRequest(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattAttributeDefinition! attribute) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IGattServerSink.OnSubscriptionChanged(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattCharacteristicDefinition! characteristic, bool enabled) -> void
-Darp.Ble.Backends.IGattServerSink.OnWriteCommand(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattAttributeDefinition! attribute, System.ReadOnlyMemory value) -> void
-Darp.Ble.Backends.IGattServerSink.OnWriteRequest(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattAttributeDefinition! attribute, System.ReadOnlyMemory value) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IObserverDriver
-Darp.Ble.Backends.IObserverDriver.StartScanAsync(Darp.Ble.Gap.BleScanParameters! parameters, Darp.Ble.Backends.IScanSink! sink, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IObserverDriver.StopScanAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IPeripheralDriver
-Darp.Ble.Backends.IPeripheralDriver.AddServiceAsync(Darp.Ble.Backends.GattServiceDefinition! service, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IPeripheralDriver.Attach(Darp.Ble.Backends.IGattServerSink! sink) -> void
-Darp.Ble.Backends.IPeripheralDriver.DisconnectAsync(Darp.Ble.Backends.ICentralConnectionDriver! central, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IPeripheralDriver.NotifyAsync(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattCharacteristicDefinition! characteristic, System.ReadOnlyMemory value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
-Darp.Ble.Backends.IPeripheralDriver.SetGapIdentity(string! deviceName, Darp.Ble.Data.AssignedNumbers.AppearanceValues appearance) -> void
-Darp.Ble.Backends.IRemoteCharacteristicDriver
-Darp.Ble.Backends.IRemoteCharacteristicDriver.DiscoverDescriptorsAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!>!
-Darp.Ble.Backends.IRemoteCharacteristicDriver.Properties.get -> Darp.Ble.Gatt.GattProperty
-Darp.Ble.Backends.IRemoteCharacteristicDriver.ReadAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IRemoteCharacteristicDriver.SetNotificationsAsync(bool enabled, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IRemoteCharacteristicDriver.Uuid.get -> Darp.Ble.Data.BleUuid!
-Darp.Ble.Backends.IRemoteCharacteristicDriver.WriteAsync(System.ReadOnlyMemory value, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IRemoteCharacteristicDriver.WriteWithoutResponseAsync(System.ReadOnlyMemory value, System.Threading.CancellationToken cancellationToken, System.Threading.CancellationToken connectionClosed) -> System.Threading.Tasks.ValueTask
-Darp.Ble.Backends.IRemoteDescriptorDriver
-Darp.Ble.Backends.IRemoteDescriptorDriver.ReadAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IRemoteDescriptorDriver.Uuid.get -> Darp.Ble.Data.BleUuid!
-Darp.Ble.Backends.IRemoteDescriptorDriver.WriteAsync(System.ReadOnlyMemory value, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
-Darp.Ble.Backends.IRemoteServiceDriver
-Darp.Ble.Backends.IRemoteServiceDriver.DiscoverCharacteristicsAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!>!
-Darp.Ble.Backends.IRemoteServiceDriver.Uuid.get -> Darp.Ble.Data.BleUuid!
-Darp.Ble.Backends.IScanSink
-Darp.Ble.Backends.IScanSink.OnFailed(Darp.Ble.BleException! error) -> void
-Darp.Ble.Backends.IScanSink.OnReport(Darp.Ble.Gap.IGapAdvertisement! advertisement) -> void
Darp.Ble.BleAdapter
Darp.Ble.BleAdapter.Backend.get -> string!
Darp.Ble.BleAdapter.Broadcaster.get -> Darp.Ble.Gap.BleBroadcaster!
@@ -116,7 +26,6 @@ Darp.Ble.BleManager
Darp.Ble.BleManager.BleManager(System.Action! configure, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory = null, System.TimeProvider? timeProvider = null) -> void
Darp.Ble.BleManager.EnumerateAdaptersAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
Darp.Ble.BleManagerBuilder
-Darp.Ble.BleManagerBuilder.AddBackend(Darp.Ble.Backends.IBleBackend! backend) -> Darp.Ble.BleManagerBuilder!
Darp.Ble.BleManagerBuilder.ConfigureDiagnostics(System.Action! configure) -> Darp.Ble.BleManagerBuilder!
Darp.Ble.BleManagerBuilder.ConfigureQueues(System.Action! configure) -> Darp.Ble.BleManagerBuilder!
Darp.Ble.BleManagerServiceCollectionExtensions
@@ -3914,6 +3823,7 @@ Darp.Ble.Data.BleAddress.Equals(Darp.Ble.Data.BleAddress? other) -> bool
Darp.Ble.Data.BleAddress.Equals(Darp.Ble.Data.UInt48 other) -> bool
Darp.Ble.Data.BleAddress.Equals(ulong other) -> bool
Darp.Ble.Data.BleAddress.RandomKind.get -> Darp.Ble.Data.BleRandomAddressKind?
+Darp.Ble.Data.BleAddress.ToString(string? format, System.IFormatProvider? formatProvider) -> string!
Darp.Ble.Data.BleAddress.Type.get -> Darp.Ble.Data.BleAddressType
Darp.Ble.Data.BleAddress.Type.init -> void
Darp.Ble.Data.BleAddress.Value.get -> Darp.Ble.Data.UInt48
@@ -4069,7 +3979,6 @@ Darp.Ble.Gap.BleScanParameters.ScanWindow.init -> void
Darp.Ble.Gap.GapAdvertisement
Darp.Ble.Gap.GapAdvertisement.Address.get -> Darp.Ble.Data.BleAddress!
Darp.Ble.Gap.GapAdvertisement.AdvertisingSId.get -> Darp.Ble.Gap.AdvertisingSId
-Darp.Ble.Gap.GapAdvertisement.AsReadOnlyMemory() -> System.ReadOnlyMemory
Darp.Ble.Gap.GapAdvertisement.Data.get -> Darp.Ble.Gap.AdvertisingData!
Darp.Ble.Gap.GapAdvertisement.DirectAddress.get -> Darp.Ble.Data.BleAddress!
Darp.Ble.Gap.GapAdvertisement.Equals(Darp.Ble.Gap.IGapAdvertisement? other) -> bool
@@ -4083,7 +3992,6 @@ Darp.Ble.Gap.GapAdvertisement.TxPower.get -> Darp.Ble.Data.TxPowerLevel
Darp.Ble.Gap.GapAdvertisement
Darp.Ble.Gap.GapAdvertisement.Address.get -> Darp.Ble.Data.BleAddress!
Darp.Ble.Gap.GapAdvertisement.AdvertisingSId.get -> Darp.Ble.Gap.AdvertisingSId
-Darp.Ble.Gap.GapAdvertisement.AsReadOnlyMemory() -> System.ReadOnlyMemory
Darp.Ble.Gap.GapAdvertisement.Data.get -> Darp.Ble.Gap.AdvertisingData!
Darp.Ble.Gap.GapAdvertisement.DirectAddress.get -> Darp.Ble.Data.BleAddress!
Darp.Ble.Gap.GapAdvertisement.Equals(Darp.Ble.Gap.IGapAdvertisement? other) -> bool
@@ -4099,7 +4007,6 @@ Darp.Ble.Gap.GapAdvertisement.UserData.get -> TUserData
Darp.Ble.Gap.IGapAdvertisement
Darp.Ble.Gap.IGapAdvertisement.Address.get -> Darp.Ble.Data.BleAddress!
Darp.Ble.Gap.IGapAdvertisement.AdvertisingSId.get -> Darp.Ble.Gap.AdvertisingSId
-Darp.Ble.Gap.IGapAdvertisement.AsReadOnlyMemory() -> System.ReadOnlyMemory
Darp.Ble.Gap.IGapAdvertisement.Data.get -> Darp.Ble.Gap.AdvertisingData!
Darp.Ble.Gap.IGapAdvertisement.DirectAddress.get -> Darp.Ble.Data.BleAddress!
Darp.Ble.Gap.IGapAdvertisement.EventType.get -> Darp.Ble.Gap.BleEventType
@@ -4118,10 +4025,6 @@ Darp.Ble.Gap.PeriodicAdvertisingInterval.NoPeriodicAdvertising = 0 -> Darp.Ble.G
Darp.Ble.Gap.ScanType
Darp.Ble.Gap.ScanType.Active = 1 -> Darp.Ble.Gap.ScanType
Darp.Ble.Gap.ScanType.Passive = 0 -> Darp.Ble.Gap.ScanType
-Darp.Ble.Gatt.AesCmac
-Darp.Ble.Gatt.AesCmac.AesCmac(System.ReadOnlySpan key) -> void
-Darp.Ble.Gatt.AesCmac.Dispose() -> void
-Darp.Ble.Gatt.AesCmac.Encrypt(System.ReadOnlySpan message) -> byte[]!
Darp.Ble.Gatt.CharacteristicDeclaration
Darp.Ble.Gatt.CharacteristicDeclaration
Darp.Ble.Gatt.CharacteristicDeclaration.CharacteristicDeclaration(Darp.Ble.Data.BleUuid! uuid) -> void
@@ -4134,9 +4037,9 @@ Darp.Ble.Gatt.DescriptorDeclaration.DescriptorDeclaration(Darp.Ble.Data.BleUuid!
Darp.Ble.Gatt.DescriptorDeclaration.Uuid.get -> Darp.Ble.Data.BleUuid!
Darp.Ble.Gatt.GattErrorKind
Darp.Ble.Gatt.GattErrorKind.AttError = 0 -> Darp.Ble.Gatt.GattErrorKind
+Darp.Ble.Gatt.GattErrorKind.InvalidValue = 3 -> Darp.Ble.Gatt.GattErrorKind
Darp.Ble.Gatt.GattErrorKind.NotFound = 1 -> Darp.Ble.Gatt.GattErrorKind
Darp.Ble.Gatt.GattErrorKind.PropertyMismatch = 2 -> Darp.Ble.Gatt.GattErrorKind
-Darp.Ble.Gatt.GattErrorKind.InvalidValue = 3 -> Darp.Ble.Gatt.GattErrorKind
Darp.Ble.Gatt.GattException
Darp.Ble.Gatt.GattException.AttStatus.get -> Darp.Ble.Gatt.GattProtocolStatus?
Darp.Ble.Gatt.GattException.GattException(Darp.Ble.Gatt.GattErrorKind kind, string! message) -> void
@@ -4472,8 +4375,107 @@ Darp.Ble.QueueOptions
Darp.Ble.QueueOptions.Capacity.get -> int?
Darp.Ble.QueueOptions.FullMode.get -> Darp.Ble.QueueFullMode
Darp.Ble.QueueOptions.QueueOptions(int capacity, Darp.Ble.QueueFullMode fullMode) -> void
+[DARPBLE0001]Darp.Ble.Backends.BleBackendContext
+[DARPBLE0001]Darp.Ble.Backends.BleBackendContext.IncludeAddresses.get -> bool
+[DARPBLE0001]Darp.Ble.Backends.BleBackendContext.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory!
+[DARPBLE0001]Darp.Ble.Backends.BleBackendContext.TimeProvider.get -> System.TimeProvider!
+[DARPBLE0001]Darp.Ble.Backends.GattAttributeDefinition
+[DARPBLE0001]Darp.Ble.Backends.GattAttributeDefinition.Uuid.get -> Darp.Ble.Data.BleUuid!
+[DARPBLE0001]Darp.Ble.Backends.GattCharacteristicDefinition
+[DARPBLE0001]Darp.Ble.Backends.GattCharacteristicDefinition.Descriptors.get -> System.Collections.Generic.IReadOnlyList!
+[DARPBLE0001]Darp.Ble.Backends.GattCharacteristicDefinition.Properties.get -> Darp.Ble.Gatt.GattProperty
+[DARPBLE0001]Darp.Ble.Backends.GattDescriptorDefinition
+[DARPBLE0001]Darp.Ble.Backends.GattDescriptorDefinition.StaticValue.get -> System.ReadOnlyMemory?
+[DARPBLE0001]Darp.Ble.Backends.GattRequest
+[DARPBLE0001]Darp.Ble.Backends.GattRequest.HandOffToken.get -> System.Threading.CancellationToken
+[DARPBLE0001]Darp.Ble.Backends.GattRequest.IsAbandoned.get -> bool
+[DARPBLE0001]Darp.Ble.Backends.GattRequest.OnTransactionAnswered() -> void
+[DARPBLE0001]Darp.Ble.Backends.GattRequest.OnTransactionSent() -> void
+[DARPBLE0001]Darp.Ble.Backends.GattServiceDefinition
+[DARPBLE0001]Darp.Ble.Backends.GattServiceDefinition.Characteristics.get -> System.Collections.Generic.IReadOnlyList!
+[DARPBLE0001]Darp.Ble.Backends.GattServiceDefinition.IsPrimary.get -> bool
+[DARPBLE0001]Darp.Ble.Backends.GattServiceDefinition.Uuid.get -> Darp.Ble.Data.BleUuid!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.RandomAddress.get -> Darp.Ble.Data.BleAddress!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.SetDataAsync(Darp.Ble.Gap.AdvertisingData! data, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.SetParametersAsync(Darp.Ble.Gap.AdvertisingParameters! parameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.SetRandomAddressAsync(Darp.Ble.Data.BleAddress! address, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.SetScanResponseDataAsync(Darp.Ble.Gap.AdvertisingData! data, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.StartAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetDriver.StopAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetSink
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetSink.OnFailed(Darp.Ble.BleException! error) -> void
+[DARPBLE0001]Darp.Ble.Backends.IAdvertisingSetSink.OnStoppedByConnection(Darp.Ble.Backends.ICentralConnectionDriver! central) -> void
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.Broadcaster.get -> Darp.Ble.Backends.IBroadcasterDriver?
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.Central.get -> Darp.Ble.Backends.ICentralDriver?
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.Features.get -> Darp.Ble.Capabilities
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.Observer.get -> Darp.Ble.Backends.IObserverDriver?
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.Peripheral.get -> Darp.Ble.Backends.IPeripheralDriver?
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.RandomAddress.get -> Darp.Ble.Data.BleAddress!
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterDriver.SetRandomAddressAsync(Darp.Ble.Data.BleAddress! address, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterSource
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterSource.Name.get -> string?
+[DARPBLE0001]Darp.Ble.Backends.IBleAdapterSource.OpenAsync(Darp.Ble.Backends.BleBackendContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IBleBackend
+[DARPBLE0001]Darp.Ble.Backends.IBleBackend.EnumerateAdaptersAsync(Darp.Ble.Backends.BleBackendContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>!
+[DARPBLE0001]Darp.Ble.Backends.IBleBackend.Name.get -> string!
+[DARPBLE0001]Darp.Ble.Backends.IBroadcasterDriver
+[DARPBLE0001]Darp.Ble.Backends.IBroadcasterDriver.CreateAdvertisingSetAsync(Darp.Ble.Gap.AdvertisingParameters! parameters, Darp.Ble.Gap.AdvertisingData! data, Darp.Ble.Gap.AdvertisingData? scanResponseData, Darp.Ble.Backends.IAdvertisingSetSink! sink, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.ICentralConnectionDriver
+[DARPBLE0001]Darp.Ble.Backends.ICentralConnectionDriver.Address.get -> Darp.Ble.Data.BleAddress!
+[DARPBLE0001]Darp.Ble.Backends.ICentralDriver
+[DARPBLE0001]Darp.Ble.Backends.ICentralDriver.ConnectAsync(Darp.Ble.Data.BleAddress! address, Darp.Ble.Connections.BleConnectionParameters! parameters, Darp.Ble.Backends.IConnectionSink! sink, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IConnectionDriver
+[DARPBLE0001]Darp.Ble.Backends.IConnectionDriver.DiscoverServicesAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!>!
+[DARPBLE0001]Darp.Ble.Backends.IConnectionDriver.ExchangeMtuAsync(int clientMtu, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IConnectionSink
+[DARPBLE0001]Darp.Ble.Backends.IConnectionSink.OnDisconnected(Darp.Ble.Connections.DisconnectInfo! info) -> void
+[DARPBLE0001]Darp.Ble.Backends.IConnectionSink.OnNotification(Darp.Ble.Backends.IRemoteCharacteristicDriver! characteristic, System.ReadOnlySpan value) -> void
+[DARPBLE0001]Darp.Ble.Backends.IConnectionSink.OnMtuChanged(int mtu) -> void
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink.OnCentralConnected(Darp.Ble.Backends.ICentralConnectionDriver! central) -> void
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink.OnCentralDisconnected(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Connections.DisconnectInfo! info) -> void
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink.OnReadRequest(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattAttributeDefinition! attribute) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink.OnSubscriptionChanged(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattCharacteristicDefinition! characteristic, bool enabled) -> void
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink.OnWriteCommand(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattAttributeDefinition! attribute, System.ReadOnlyMemory value) -> void
+[DARPBLE0001]Darp.Ble.Backends.IGattServerSink.OnWriteRequest(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattAttributeDefinition! attribute, System.ReadOnlyMemory value) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IObserverDriver
+[DARPBLE0001]Darp.Ble.Backends.IObserverDriver.StartScanAsync(Darp.Ble.Gap.BleScanParameters! parameters, Darp.Ble.Backends.IScanSink! sink, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IObserverDriver.StopScanAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IPeripheralDriver
+[DARPBLE0001]Darp.Ble.Backends.IPeripheralDriver.AddServiceAsync(Darp.Ble.Backends.GattServiceDefinition! service, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IPeripheralDriver.Attach(Darp.Ble.Backends.IGattServerSink! sink) -> void
+[DARPBLE0001]Darp.Ble.Backends.IPeripheralDriver.DisconnectAsync(Darp.Ble.Backends.ICentralConnectionDriver! central, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IPeripheralDriver.NotifyAsync(Darp.Ble.Backends.ICentralConnectionDriver! central, Darp.Ble.Backends.GattCharacteristicDefinition! characteristic, System.ReadOnlyMemory value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
+[DARPBLE0001]Darp.Ble.Backends.IPeripheralDriver.SetGapIdentity(string! deviceName, Darp.Ble.Data.AssignedNumbers.AppearanceValues appearance) -> void
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.DiscoverDescriptorsAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!>!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.Properties.get -> Darp.Ble.Gatt.GattProperty
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.ReadAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.SetNotificationsAsync(bool enabled, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.Uuid.get -> Darp.Ble.Data.BleUuid!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.WriteAsync(System.ReadOnlyMemory value, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteCharacteristicDriver.WriteWithoutResponseAsync(System.ReadOnlyMemory value, System.Threading.CancellationToken cancellationToken, System.Threading.CancellationToken connectionClosed) -> System.Threading.Tasks.ValueTask
+[DARPBLE0001]Darp.Ble.Backends.IRemoteDescriptorDriver
+[DARPBLE0001]Darp.Ble.Backends.IRemoteDescriptorDriver.ReadAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteDescriptorDriver.Uuid.get -> Darp.Ble.Data.BleUuid!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteDescriptorDriver.WriteAsync(System.ReadOnlyMemory value, Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteServiceDriver
+[DARPBLE0001]Darp.Ble.Backends.IRemoteServiceDriver.DiscoverCharacteristicsAsync(Darp.Ble.Backends.GattRequest! request) -> System.Threading.Tasks.Task!>!
+[DARPBLE0001]Darp.Ble.Backends.IRemoteServiceDriver.Uuid.get -> Darp.Ble.Data.BleUuid!
+[DARPBLE0001]Darp.Ble.Backends.IScanSink
+[DARPBLE0001]Darp.Ble.Backends.IScanSink.OnFailed(Darp.Ble.BleException! error) -> void
+[DARPBLE0001]Darp.Ble.Backends.IScanSink.OnReport(Darp.Ble.Gap.IGapAdvertisement! advertisement) -> void
+[DARPBLE0001]Darp.Ble.BleManagerBuilder.AddBackend(Darp.Ble.Backends.IBleBackend! backend) -> Darp.Ble.BleManagerBuilder!
+[DARPBLE0001]Darp.Ble.Gatt.AesCmac
+[DARPBLE0001]Darp.Ble.Gatt.AesCmac.AesCmac(System.ReadOnlySpan key) -> void
+[DARPBLE0001]Darp.Ble.Gatt.AesCmac.Dispose() -> void
+[DARPBLE0001]Darp.Ble.Gatt.AesCmac.Encrypt(System.ReadOnlySpan message) -> byte[]!
+[DARPBLE0001]static Darp.Ble.Gap.GapAdvertisement.Create(System.DateTimeOffset timestamp, Darp.Ble.Gap.BleEventType eventType, Darp.Ble.Data.BleAddress! address, Darp.Ble.Data.Physical primaryPhy, Darp.Ble.Data.Physical secondaryPhy, Darp.Ble.Gap.AdvertisingSId advertisingSId, Darp.Ble.Data.TxPowerLevel txPower, Darp.Ble.Data.Rssi rssi, Darp.Ble.Gap.PeriodicAdvertisingInterval periodicAdvertisingInterval, Darp.Ble.Data.BleAddress! directAddress, Darp.Ble.Gap.AdvertisingData! data) -> Darp.Ble.Gap.GapAdvertisement!
const Darp.Ble.BleBackends.HciHost = "Darp.Ble.HciHost" -> string!
const Darp.Ble.BleBackends.Mock = "Darp.Ble.Mock" -> string!
+const Darp.Ble.BleBackends.WinRT = "Darp.Ble.WinRT" -> string!
const Darp.Ble.Diagnostics.BleTelemetry.MeterName = "Darp.Ble" -> string!
const Darp.Ble.Diagnostics.BleTelemetry.SourceName = "Darp.Ble" -> string!
override Darp.Ble.Connections.BleConnectionParameters.Equals(object? obj) -> bool
@@ -4616,8 +4618,6 @@ static Darp.Ble.Gap.AdvertisingParameters.operator !=(Darp.Ble.Gap.AdvertisingPa
static Darp.Ble.Gap.AdvertisingParameters.operator ==(Darp.Ble.Gap.AdvertisingParameters? left, Darp.Ble.Gap.AdvertisingParameters? right) -> bool
static Darp.Ble.Gap.BleScanParameters.operator !=(Darp.Ble.Gap.BleScanParameters? left, Darp.Ble.Gap.BleScanParameters? right) -> bool
static Darp.Ble.Gap.BleScanParameters.operator ==(Darp.Ble.Gap.BleScanParameters? left, Darp.Ble.Gap.BleScanParameters? right) -> bool
-static Darp.Ble.Gap.GapAdvertisement.FromExtendedAdvertisingReport(System.DateTimeOffset timestamp, Darp.Ble.Gap.BleEventType eventType, Darp.Ble.Data.BleAddress! address, Darp.Ble.Data.Physical primaryPhy, Darp.Ble.Data.Physical secondaryPhy, Darp.Ble.Gap.AdvertisingSId advertisingSId, Darp.Ble.Data.TxPowerLevel txPower, Darp.Ble.Data.Rssi rssi, Darp.Ble.Gap.PeriodicAdvertisingInterval periodicAdvertisingInterval, Darp.Ble.Data.BleAddress! directAddress, Darp.Ble.Gap.AdvertisingData! advertisingData) -> Darp.Ble.Gap.GapAdvertisement!
-static Darp.Ble.Gap.GapAdvertisement.FromExtendedAdvertisingReport(System.DateTimeOffset timestamp, System.ReadOnlySpan