DSoft.System.BluetoothLe
4.1.2610.102
dotnet add package DSoft.System.BluetoothLe --version 4.1.2610.102
NuGet\Install-Package DSoft.System.BluetoothLe -Version 4.1.2610.102
<PackageReference Include="DSoft.System.BluetoothLe" Version="4.1.2610.102" />
<PackageVersion Include="DSoft.System.BluetoothLe" Version="4.1.2610.102" />
<PackageReference Include="DSoft.System.BluetoothLe" />
paket add DSoft.System.BluetoothLe --version 4.1.2610.102
#r "nuget: DSoft.System.BluetoothLe, 4.1.2610.102"
#:package DSoft.System.BluetoothLe@4.1.2610.102
#addin nuget:?package=DSoft.System.BluetoothLe&version=4.1.2610.102
#tool nuget:?package=DSoft.System.BluetoothLe&version=4.1.2610.102
DSoft.System.BluetoothLe
DSoft.System.BluetoothLe is a cross-platform Bluetooth Low Energy library for modern .NET. It provides one API for scanning, connecting, discovering GATT services/characteristics, reading, writing, and receiving characteristic updates across mobile and desktop targets.
The library started as a fork/repackage of Plugin.BLE and has been migrated from Xamarin targets to .NET platform targets.
Supported Targets
| Target | Minimum OS |
|---|---|
net10.0-android |
Android API 21 |
net10.0-ios |
iOS 15.0 |
net10.0-maccatalyst |
Mac Catalyst 15.0 |
net10.0-macos |
macOS 15.0 |
net10.0-tvos |
tvOS 15.0 |
net10.0-windows10.0.19041.0 |
Windows 10 1809 |
net481 |
Windows 10 1809 |
net10.0 / netstandard2.0 |
API surface only; platform Bluetooth calls throw on unsupported platforms |
Install
Reference the package from your app project:
<PackageReference Include="DSoft.System.BluetoothLe" Version="4.1.*" />
When working from source, reference the project:
<ProjectReference Include="..\DSoft.System.BluetoothLe\DSoft.System.BluetoothLe.csproj" />
Use the library from the System.BluetoothLe namespace:
using System.BluetoothLe;
using System.BluetoothLe.EventArgs;
Platform Setup
Your app must request the operating-system permissions needed for Bluetooth. The library does not replace runtime permission prompts or app manifest entries.
Android
Add Bluetooth permissions to your Android manifest. For Android 12/API 31 and later, apps normally need BLUETOOTH_SCAN and BLUETOOTH_CONNECT. Older Android versions commonly require BLUETOOTH, BLUETOOTH_ADMIN, and location permission for scanning.
Example manifest entries:
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
Request dangerous permissions at runtime before scanning or connecting.
iOS, macOS, Mac Catalyst, tvOS
Add Bluetooth usage descriptions to your app's Info.plist where required by the platform:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to nearby BLE devices.</string>
For iOS background BLE scenarios, also configure the appropriate background modes in your app.
Windows
Windows support uses the Windows Runtime Bluetooth APIs. The .NET for Windows and .NET Framework implementations share the same implementation and require Windows 10 1809 or later with a Bluetooth LE-capable adapter.
Packaged Windows apps should declare the Bluetooth capability in the app manifest. Desktop WPF apps should still handle unavailable Bluetooth hardware/radio states at runtime.
Quick Start
Get the current platform implementation:
var bluetooth = BluetoothLE.Current;
// Await readiness rather than testing IsOn. A radio that has only just been brought up has not settled and
// reports BluetoothState.Unknown, so an immediate IsOn check fails against a perfectly good adapter.
var state = await bluetooth.WaitForStateAsync(BluetoothState.On, cancellationToken);
var adapter = bluetooth.Adapter;
If you would rather report the real state to the user than wait for a particular one, await
WaitForAvailabilityAsync(), which completes as soon as the radio settles into any determinate state -
including Off and Unauthorized.
Scan for devices:
var adapter = BluetoothLE.Current.Adapter;
adapter.DeviceDiscovered += (sender, args) =>
{
Console.WriteLine($"Found {args.Device.NameOrId} ({args.Device.Id}) RSSI {args.Device.Rssi}");
};
adapter.ScanTimeout = 10000; // milliseconds
adapter.ScanMode = ScanMode.LowLatency;
await adapter.StartScanningForDevicesAsync(cancellationToken: cancellationToken);
Starting a scan while one is already running throws InvalidOperationException, and a scan the platform
refuses to start (radio off, permission denied, unsupported) throws AdapterScanException carrying a
ScanFailureReason rather than completing empty after the full timeout.
Scan for devices that advertise a service:
var heartRateService = Guid.Parse("0000180d-0000-1000-8000-00805f9b34fb");
await adapter.StartScanningForDevicesAsync(
serviceUuids: new[] { heartRateService },
deviceFilter: device => !string.IsNullOrWhiteSpace(device.Name),
allowDuplicatesKey: false,
cancellationToken: cancellationToken);
Connect to a discovered device:
var device = adapter.DiscoveredDevices.FirstOrDefault();
if (device == null)
{
throw new InvalidOperationException("No BLE device was discovered.");
}
await adapter.ConnectToDeviceAsync(device);
Connect to a known device by id:
var knownDeviceId = Guid.Parse("00000000-0000-0000-0000-000000000000");
var device = await adapter.ConnectToKnownDeviceAsync(knownDeviceId);
Discover services and characteristics:
var services = await device.GetServicesAsync();
foreach (var service in services)
{
Console.WriteLine($"{service.Name}: {service.Id}");
var characteristics = await service.GetCharacteristicsAsync();
foreach (var characteristic in characteristics)
{
Console.WriteLine($" {characteristic.Name}: {characteristic.Id} ({characteristic.Properties})");
}
}
Read and write a characteristic:
var serviceId = Guid.Parse("0000180d-0000-1000-8000-00805f9b34fb");
var characteristicId = Guid.Parse("00002a37-0000-1000-8000-00805f9b34fb");
var service = await device.GetServiceAsync(serviceId);
var characteristic = await service.GetCharacteristicAsync(characteristicId);
if (characteristic.CanRead)
{
byte[] value = await characteristic.ReadAsync();
}
if (characteristic.CanWrite)
{
await characteristic.WriteAsync(new byte[] { 0x01, 0x02, 0x03 });
}
Subscribe to characteristic updates:
characteristic.ValueUpdated += (sender, args) =>
{
var value = args.Characteristic.Value;
Console.WriteLine(BitConverter.ToString(value));
};
await characteristic.StartUpdatesAsync();
// Later:
await characteristic.StopUpdatesAsync();
Disconnect:
await adapter.DisconnectDeviceAsync(device);
Useful API Surface
BluetoothLE.Current: singleton entry point for the current platform.BluetoothLE.State,IsAvailable,IsOn: current Bluetooth state.BluetoothLE.StateChanged: Bluetooth state notifications.Adapter.StartScanningForDevicesAsync: scan for BLE devices.Adapter.DeviceDiscovered: raised the first time a device is discovered during a scan.Adapter.DeviceAdvertised: raised for matching advertisements.Adapter.ConnectToDeviceAsync: connect to a discovered device.Adapter.ConnectToKnownDeviceAsync: connect directly by known platform device id.Device.GetServicesAsync: discover GATT services.Service.GetCharacteristicsAsync: discover GATT characteristics.Characteristic.ReadAsync,WriteAsync,StartUpdatesAsync,StopUpdatesAsync: interact with characteristic values.Descriptor.ReadAsync,WriteAsync: interact with descriptors.Device.BondState: the device's bond (pairing) state, as the platform reports it.Adapter.DeviceBondStateChanged: raised on Android when a device's bond state changes.IGattStatusException.GattStatus,GattFailure: the GATT status on a failed read or write.Device.CreateBondAsync,RemoveBondAsync: request or remove a bond on Android.
Bonding
A peripheral that requires an encrypted link only answers a central it is bonded (paired) with. What an app can see of that bond depends on the platform.
| Platform | Device.BondState |
Adapter.DeviceBondStateChanged |
|---|---|---|
| Android | NotBonded, Bonding or Bonded, read from the system on every call |
Raised on the main thread for every bond change the system broadcasts, including changes made in the system Bluetooth settings |
| iOS, macOS, Mac Catalyst, tvOS | Always Unknown |
Never raised |
| Windows | Bonded or NotBonded, from the device's pairing information |
Never raised |
net10.0 / netstandard2.0 |
Always Unknown |
Never raised |
Apple platforms. CoreBluetooth gives an app no way to read or request a bond. The operating system pairs on demand: the first time the app reads, writes or subscribes to a characteristic the peripheral protects, iOS shows the pairing prompt itself, and once the user accepts the link is encrypted for that and later operations. If the user declines, or the peripheral has lost its keys (for example after a factory reset), the operation fails, and the user has to remove the device in Settings > Bluetooth before pairing again. Design the app around attempting the protected operation and handling its failure, not around checking the bond first.
Android. On Android 12 and later, reading the bond state and receiving bond changes both need the BLUETOOTH_CONNECT permission. Without it BondState reports Unknown and the event is not raised. Android, like Apple platforms, normally starts bonding by itself the first time a protected characteristic is accessed; DeviceBondStateChanged reports the outcome.
Requesting and removing a bond (Android). Device.CreateBondAsync starts bonding, showing the system pairing prompt where the peripheral asks for one, and completes with true once the device is bonded or false if bonding fails or is refused. Use it for an explicit "Pair" action instead of waiting for the first protected operation. Device.RemoveBondAsync forgets a stale bond, for example after the peripheral has been factory reset. Android has no public API for removal, so it is best effort: it calls a hidden platform method that newer Android versions may refuse, and returns false when it cannot remove the bond. In that case, send the user to the system Bluetooth settings. Both methods return false on Apple platforms and Windows. A cancellation token stops the wait, but not bonding the system has already started.
if (device.BondState != DeviceBondState.Bonded && !await device.CreateBondAsync(cancellationToken))
{
// Not bonded: tell the user to pair, or to forget the device in Settings and try again.
}
Recognising a security failure. A peripheral that requires a bonded, encrypted link refuses an unbonded central with ATT error Insufficient Authentication (0x05) or Insufficient Encryption (0x0F). The read and write exceptions (CharacteristicReadException, CharacteristicWriteException, DescriptorReadException and DescriptorWriteException) carry that status in GattStatus, classified in GattFailure, on Android, Apple platforms and Windows. All four implement IGattStatusException, so one catch covers them:
try
{
await characteristic.WriteAsync(data, cancellationToken);
}
catch (BleException ex) when (ex is IGattStatusException { GattFailure: GattFailure.InsufficientAuthentication or GattFailure.InsufficientEncryption })
{
// Ask the user to pair, or to forget the device in Settings and pair again.
}
Subscribing with StartUpdatesAsync fails as a DescriptorWriteException on Android, where it writes the configuration descriptor itself, and as a CharacteristicReadException on Apple platforms. GattStatus is null, and GattFailure is None, when no GATT status was reported: the operation could not start, the device disconnected, or on Apple platforms the error was not an ATT error. NativeStatus and the exception messages are unchanged.
BondState never throws: it reports Unknown wherever the state cannot be read, including for a stand-in device and after the device has been disposed.
Notes
- BLE device identifiers are platform-specific. Persist known device ids only for the same platform/device context.
- Scanning and connecting require OS permissions and Bluetooth hardware. Always handle
BluetoothState.UnavailableandBluetoothState.Off. net10.0andnetstandard2.0builds keep the shared API available, but platform Bluetooth operations require a supported platform target.- Some Android APIs used by the implementation are marked obsolete by newer SDK analyzers (CA1422). Migrating to the API 33 GATT overloads rewrites the notification delivery path, so it is deliberately deferred until it can be tested against real hardware; the behaviour is correct today.
- Every event this library raises is raised on a native callback thread - the Android GATT callback thread or the CoreBluetooth delegate queue - and is not marshalled to your UI thread. Marshal in your handler.
Trace.TraceImplementationreceives every diagnostic message the library emits. It writes toSystem.Diagnostics.Traceby default; assign your own delegate to bridge it to your logging framework, or set it tonullto silence the library.DeviceandServicecan be derived outside the library to build a stand-in peripheral where no radio is present, for a device picker or for unit tests of code that takes aDevice. Derive fromDeviceand callprotected Device(Guid id, string name, int rssi = 0); derive fromServiceand callprotected Service(Device device). A stand-in has no adapter or native peripheral, so members that reach the platform fail as they do on a target without Bluetooth support. In 4.0 this constructor replaces the parameterlessprotected Device()that 3.x exposed, andDevice.Idis no longer assignable from outside the library - pass the identity to the constructor instead.- Upgrading from 3.x:
AdvertisementRecordType.UuidsIncomple16Bitwas renamed toUuidsIncomplete16Bitin 4.0, a spelling correction that breaks compilation of any code that read that record type. Rename the reference; the value (0x02) is unchanged.
Building From Source
Restore and build the active solution with the .NET 10 SDK:
dotnet restore DSoft.System.BluetoothLe.slnx
dotnet build DSoft.System.BluetoothLe.slnx --no-restore
net10.0-windows10.0.19041.0 and net481 are only built when the host is Windows; on macOS and Linux the
remaining seven target frameworks build and the two Windows ones are skipped.
Continuous integration
GitHub Actions (.github/workflows/):
ci.yml: runs on pull requests tomainanddevelopment. Builds Release for every target framework. Publishes nothing.release.yml: runs on every push tomain(changes only to Markdown or workflow files are skipped; run it by hand to test a workflow change). Builds Release as4.1.yyMM.<run number + 100>plusRELEASE_SUFFIX(empty for a stable version; set it to-prereleasein the workflow to publish a prerelease), uploads the package as thedropartifact and pushes it to nuget.org with Trusted Publishing (OIDC,NUGET_USERsecret,nugetenvironment), then tags the commitv<version>and creates a GitHub release with the package attached.AssemblyVersionis not stamped by CI: it stays pinned in the project file (see "Strong naming").
Strong naming
The assembly is strong-named with DSoft.snk, which is committed to this repository on purpose. A strong
name is an identity, not a signature: it lets the runtime tell this assembly apart from another of the same
name, and it is what net481 binding needs. It is not a guarantee of origin, and publishing the key does not
weaken anything that was ever guaranteed. Verify the package through NuGet, not through the strong name.
Relationship To Plugin.BLE
This project keeps the broad shape of Plugin.BLE but uses:
- Namespace:
System.BluetoothLe - Entry point:
BluetoothLE.Current - Multi-targeted partial classes instead of the original base-class layout
Existing Plugin.BLE concepts map closely to this library, but code should be updated to the names above.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-android36.0 is compatible. net10.0-browser was computed. net10.0-ios was computed. net10.0-ios26.0 is compatible. net10.0-maccatalyst was computed. net10.0-maccatalyst26.0 is compatible. net10.0-macos was computed. net10.0-macos26.0 is compatible. net10.0-tvos was computed. net10.0-tvos26.0 is compatible. net10.0-windows was computed. net10.0-windows10.0.19041 is compatible. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.8.1
- Microsoft.Windows.SDK.Contracts (>= 10.0.19041.2)
-
.NETStandard 2.0
- No dependencies.
-
net10.0
- No dependencies.
-
net10.0-android36.0
- No dependencies.
-
net10.0-ios26.0
- No dependencies.
-
net10.0-maccatalyst26.0
- No dependencies.
-
net10.0-macos26.0
- No dependencies.
-
net10.0-tvos26.0
- No dependencies.
-
net10.0-windows10.0.19041
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.1.2610.102 | 0 | 10/18/2026 |
| 4.0.2610.101 | 68 | 10/14/2026 |
| 4.0.2610.91 | 109 | 10/9/2026 |
| 4.0.2609.21 | 146 | 9/2/2026 |
| 3.0.2607.92 | 439 | 7/9/2026 |
| 2.0.2110.291 | 2,906 | 10/30/2021 |
| 2.0.2109.301 | 605 | 9/30/2021 |
| 2.0.2108.171 | 614 | 8/17/2021 |
| 2.0.2107.141 | 633 | 7/14/2021 |
| 2.0.2107.132 | 614 | 7/13/2021 |
| 2.0.2107.131 | 598 | 7/13/2021 |
| 2.0.2106.104 | 723 | 6/15/2021 |
| 2.0.2010.51-prerelease | 665 | 10/5/2020 |
| 2.0.2010.21-prerelease | 629 | 10/2/2020 |
| 2.0.2009.301-prerelease | 556 | 9/30/2020 |
| 2.0.2006.222-prerelease | 590 | 6/22/2020 |
| 2.0.2006.191-prerelease | 636 | 6/19/2020 |
| 2.0.2006.181-prerelease | 613 | 6/18/2020 |
| 2.0.2006.152-prerelease | 648 | 6/15/2020 |
4.1 - adds bonding and the GATT status on failures. Device.BondState reports a device's bond (pairing) state, and on Android
Adapter.DeviceBondStateChanged is raised when it changes. DeviceBondState gains Unknown, declared last
so existing values are unchanged, which Apple platforms always report because CoreBluetooth does not
expose bonding. BondStatusBroadcastReceiver is now registered by the Android adapter.
Device.CreateBondAsync and Device.RemoveBondAsync request and remove a bond on Android; removal is
best effort, through a hidden platform method, and reports false where Android refuses it. Both
return false on Apple platforms and Windows.
CharacteristicReadException, CharacteristicWriteException, DescriptorReadException and
DescriptorWriteException carry the GATT status in GattStatus, classified as GattFailure, so an app can
recognise Insufficient Authentication (0x05) and Insufficient Encryption (0x0F) on Android, Apple
platforms and Windows. All four implement IGattStatusException. NativeStatus and messages are unchanged.
4.0.1 - restores the stand-in seam that 4.0 removed. Device and Service can again be derived outside
the library: protected Device(Guid id, string name, int rssi = 0) replaces the parameterless
protected Device() that 3.x exposed, and Service(Device) is protected again. Device.Id stays
settable only by the library; a stand-in passes its identity to the constructor instead.
Also recorded from 4.0, where the release notes omitted it: AdvertisementRecordType.UuidsIncomple16Bit
was renamed to UuidsIncomplete16Bit. A deliberate spelling correction, but a compile-breaking rename
for a 3.x consumer that read that record type.
Also on Windows: an advertisement is resolved to a device with the address type the advertisement
carries, and a device glyph that cannot be loaded - or a host with no WPF Application - no longer
raises an unobserved exception from inside the scan callback.
4.0 - modernisation release. Contains breaking changes.
This release corrects defects carried over from the Xamarin port and reshapes the public API.
Fixed: a failed scan start no longer wedges the adapter into a permanent "already scanning" state;
connection failures are no longer lost as unhandled async void exceptions; CancellationToken is now
honoured throughout - service discovery, characteristic and descriptor reads and writes, notifications,
MTU and RSSI - where it was previously accepted and discarded; on Android, a pending read or write is
now cancelled when the link drops for any reason, a characteristic read is no longer completed by an
unrelated notification, and a BluetoothGattCallback is no longer allocated for every advertisement
received; malformed advertisements no longer crash the process from inside a native scan callback;
built-in tracing, which had never been wired up on any platform, now works.
Changed deliberately: starting a scan while one is running throws InvalidOperationException instead of
silently doing nothing, and StopScanningForDevicesAsync now genuinely waits for the scan to stop; a scan
that cannot start (adapter off, permission denied, unsupported) faults with AdapterScanException and
raises ScanFailed instead of completing empty after the full timeout; devices arriving in the tail of a
stopping scan are no longer reported; exceptions thrown by consumer event handlers are logged and
swallowed instead of terminating the app; task continuations now resume on the thread pool rather than
inline on the CoreBluetooth or Android GATT callback thread; Characteristic.WriteAsync returns Task and
throws CharacteristicWriteException instead of returning an unexplained false on two platforms and
throwing on the third; ConnectToKnownDeviceAsync reuses an already-discovered device instead of minting
a duplicate (on Android, a duplicate BluetoothGatt); Device.Dispose() no longer starts an unawaited
disconnect - disconnect explicitly first; on Android, 32-bit service-data advertisement records are no
longer byte-reversed and incomplete 32-bit UUID lists now are, and a denied BLUETOOTH_SCAN /
BLUETOOTH_CONNECT permission is reported as BluetoothState.Unauthorized; Device.State on iOS, macOS,
Mac Catalyst and tvOS reports the new DeviceState.Disconnecting while a disconnect is in progress,
where it previously reported Disconnected - Android and Windows still move straight to Disconnected,
so test for Connected rather than for Disconnected if you need portable behaviour.
Removed: BluetoothLE.UseOldFindCharacteristicMode, Adapter.ConnectedDeviceRegistry (use ConnectedDevices
/ TryGetConnectedDevice), the fork's PlatformNotSupportedException (the BCL type is thrown instead),
BleCommandQueue, LEStream, the unreachable Android API-18 scan path, and several unused parameterless
constructors.
Added: BluetoothLE.WaitForStateAsync - await Bluetooth readiness instead of polling IsOn / IsAvailable,
which on Apple can bail out on a working adapter that has not yet settled. Every exception the library
raises deliberately now derives from BleException, so one catch clause covers them all.
The Windows (net10.0-windows10.0.19041.0) and net481 targets compile cleanly, but were not run: the
work was done on macOS, where those two targets build only by overriding TargetFrameworks with
EnableWindowsTargeting. Their behaviour should be smoke-tested on a Windows agent before release.