Usage
Install SimpleSharpBLE and use SimpleBLE from C# and .NET.
SimpleSharpBLE exposes SimpleBLE's Bluetooth client and GATT hosting APIs to C# and .NET 10 on Windows, macOS, Linux, Android, iOS/iPadOS, and Mac Catalyst. One NuGet package carries the native libraries for every supported platform.
Install from NuGet
Add SimpleSharpBLE to a .NET 10 project:
dotnet add package SimpleSharpBLEThe package targets net10.0 and ships XML documentation for IntelliSense.
Development builds from main are published as prereleases; add --prerelease
to try one.
Platform support
| Platform | Runtimes | Requirements |
|---|---|---|
| Windows 10 or newer | win-x64, win-arm64 | Bluetooth enabled in system settings |
| Linux with glibc | linux-x64, linux-arm64 | BlueZ and D-Bus |
| macOS 15.0 or newer | osx-arm64 | Bluetooth permission |
| Android 12 or newer | android-arm64, android-x64 | Nearby devices permission |
| iOS/iPadOS 15.8 or newer | ios-arm64, iossimulator-arm64 | NSBluetoothAlwaysUsageDescription |
| Mac Catalyst 16.0 or newer | maccatalyst-arm64 | NSBluetoothAlwaysUsageDescription |
Intel Macs and 32-bit platforms are not included in the package.
See Platform notes for per-OS setup.
First scan
using SimpleSharpBLE;
var adapters = Adapter.GetAdapters();
try
{
if (adapters.Count == 0)
{
Console.WriteLine("No Bluetooth adapters found.");
return;
}
var adapter = adapters[0];
adapter.ScanFound += (_, e) =>
{
using var peripheral = e.Peripheral;
Console.WriteLine($"{peripheral.Identifier} [{peripheral.Address}] {peripheral.Rssi} dBm");
};
adapter.ScanFor(TimeSpan.FromSeconds(5));
}
finally
{
foreach (var adapter in adapters) adapter.Dispose();
}Ownership and disposal
Adapters, backends, peripherals, and local GATT objects wrap native handles.
Dispose every object a method returns, including list entries you do not use and
the Peripheral delivered with each ScanFound or ScanUpdated event. Each
wrapper owns its handle independently: a peripheral stays usable after its
adapter is disposed, and an adapter after its backend.
Disposing a wrapper releases it and its event registrations. It does not stop a
scan, disconnect, unsubscribe, or stop a local host; call ScanStop,
Disconnect, Unsubscribe, or Stop first. A call that is already running
in native code finishes before the handle is released; later calls throw
ObjectDisposedException.
Choose an adapter
Adapter.GetAdapters() returns the adapters of every active backend.
To choose a backend, enumerate backends and use their adapters:
var backends = Backend.GetBackends();
try
{
foreach (var backend in backends)
{
Console.WriteLine($"{backend.Identifier}: Bluetooth enabled = {backend.IsBluetoothEnabled}");
foreach (var adapter in backend.Adapters)
{
using (adapter) Console.WriteLine($" {adapter.Identifier} [{adapter.Address}]");
}
}
}
finally
{
foreach (var backend in backends) backend.Dispose();
}Backend identifiers are WinRT, CoreBluetooth, SimpleBluez, Android, and
Dongl. Adapter.IsBluetoothEnabled reports whether any active backend has
Bluetooth enabled. PowerOn and PowerOff request power changes where the
platform allows it, and IsPowered, PoweredOn, and PoweredOff report the
adapter's state.
Dongl
The Dongl USB adapter has its own backend on
Windows, macOS, and Linux. From SimpleSharpBLE 1.2.1 it's on by default; with
1.2.0, enable it before the first enumeration. Backend.GetDongl() returns it:
using var dongl = Backend.GetDongl();
var adapters = dongl.Adapters;
// Use and dispose the Dongl adapters like any other adapter.With the backend enabled, Adapter.GetAdapters() also includes Dongl adapters.
For pairing through a Dongl, register Advanced.Dongl.SetPasskeyRequestCallback,
SetPasskeyDisplayCallback, or SetNumericComparisonCallback on the peripheral
before connecting. These callbacks run synchronously on a native pairing worker;
return null or false to reject pairing.
Scan
ScanFor(TimeSpan) scans synchronously and ScanForAsync runs the same scan on
a worker. ScanStart and ScanStop let your application control timing, and
ScanIsActive reports the state. ScanStarted and ScanStopped follow the
scan; ScanFound reports the first advertisement from each device and
ScanUpdated the later ones.
After a scan, ScanGetResults() returns the peripherals seen.
GetPairedPeripherals() and GetConnectedPeripherals() list devices known to
the system, where the backend supports it.
A peripheral exposes Identifier (the advertised name), Address,
AddressType, Rssi, TxPower (short.MinValue when not advertised),
IsConnectable, ManufacturerData keyed by company identifier, and Services.
Before connection, Services holds advertised service UUIDs and service data.
On macOS and iOS, Address is a CoreBluetooth identifier rather than the
Bluetooth address.
Connect, read, and write
const string battery = "180f";
const string level = "2a19";
peripheral.Connect();
try
{
Console.WriteLine($"MTU {peripheral.Mtu}");
foreach (var service in peripheral.Services)
{
foreach (var characteristic in service.Characteristics)
Console.WriteLine($"{service.Uuid} {characteristic.Uuid} {string.Join(",", characteristic.Capabilities)}");
}
byte[] value = peripheral.Read(battery, level);
Console.WriteLine($"Battery: {value[0]}%");
}
finally
{
peripheral.Disconnect();
}Connect blocks until the link is up and services are discovered. After
connection, Services returns discovered services with their characteristics,
capabilities, and descriptors as managed snapshots that remain valid after the
peripheral is disposed. UUID arguments accept 16-bit and 32-bit short forms or
full 128-bit UUIDs, in either case.
Use Read, WriteRequest (acknowledged), and WriteCommand (unacknowledged)
for characteristics, and the Read and Write overloads that take a descriptor
UUID for descriptors. IsConnected, IsPaired, Unpair, and the Connected
and Disconnected events cover connection state. Disconnected is also raised
when the link drops unexpectedly.
Notifications and indications
peripheral.Notify("180f", "2a19", data => Console.WriteLine($"Battery: {data[0]}%"));
// ...
peripheral.Unsubscribe("180f", "2a19");Indicate works the same way for indications. A new subscription replaces the
previous callback for that characteristic. Each callback receives its own copy of
the payload.
Callbacks and threading
Events and subscription callbacks run on the .NET thread pool after the native
callback has returned. Callbacks for one object run one at a time, in the order
the native library reported them, so a handler can call other SimpleSharpBLE
methods, including blocking ones and Unsubscribe or Dispose. A slow handler
delays later events for the same object.
An exception thrown by a handler does not reach native code. It is raised
through CallbackErrors.Unhandled, or written to Trace when that event has no
handlers. In UI apps, marshal updates to the UI thread, for example with
MainThread.BeginInvokeOnMainThread in MAUI.
The exceptions to this model are Local.Characteristic.ReadHandler and the Dongl
pairing callbacks, which run synchronously on a native thread and must return
promptly.
Async methods
Blocking operations have Async counterparts: ScanForAsync, ConnectAsync,
DisconnectAsync, ReadAsync, WriteRequestAsync, WriteCommandAsync,
WriteAsync, NotifyAsync, IndicateAsync, and UnsubscribeAsync.
await peripheral.ConnectAsync();
byte[] value = await peripheral.ReadAsync("180f", "2a19");
await peripheral.DisconnectAsync();These methods run the blocking call on a worker thread. A cancellation token prevents queued work from starting; once the native call starts, it runs to completion. Keep a write buffer unchanged until its task completes.
Errors
Native failures throw BleException. Its Code property is a BleErrorCode,
such as PeripheralNotConnected, GattServiceNotFound,
GattCharacteristicNotFound, OperationNotSupported, or OperationFailed,
and its message carries the native details. Invalid arguments throw the usual
ArgumentException types before reaching native code, and platform-specific
APIs called on another platform throw PlatformNotSupportedException.
try
{
byte[] value = peripheral.Read("180f", "2a19");
}
catch (BleException error) when (error.Code == BleErrorCode.PeripheralNotConnected)
{
// Reconnect and retry.
}Host a GATT service
Adapter.CreateLocalPeripheral() creates a local GATT host. Configure services,
characteristics, and advertised UUIDs before calling Start:
const string serviceUuid = "12345678-1234-5678-1234-56789abcdef0";
using var host = adapter.CreateLocalPeripheral();
using var service = host.AddService(serviceUuid);
using var characteristic = service.AddCharacteristic("12345678-1234-5678-1234-56789abcdef1",
SimpleSharpBLE.Local.CharacteristicCapability.Read |
SimpleSharpBLE.Local.CharacteristicCapability.WriteRequest |
SimpleSharpBLE.Local.CharacteristicCapability.Notify);
characteristic.Value = "hello"u8.ToArray();
characteristic.Written += (_, e) => characteristic.Value = e.Value; // Echo to subscribers.
host.AddAdvertisedService(serviceUuid);
host.Start();
// ...
host.Stop();Setting Value stores the bytes and publishes them to notify or indicate
subscribers on a best-effort basis. Written runs after a client write has
updated Value. Subscribed reports the first subscriber and Unsubscribed
the last. ClientConnected and ClientDisconnected report clients by address;
CoreBluetooth does not report connections, so on macOS and iOS these events
follow a client's first and last subscription instead.
Set ReadHandler to compute read values on demand. It runs synchronously on the
native thread, so return quickly and do not call SimpleSharpBLE from it. Set it
to null to serve the stored Value again.
SetAdvertisedManufacturerData(companyId, data) adds manufacturer specific data
to the advertisement, and SetConnectable(false) makes it non-connectable, for
example to advertise a beacon.
Configure both before Start.
IsStarted and IsAdvertising report the host state. After Stop,
RemoveAllServices clears the layout so you can rebuild it. Disposing a service
or characteristic wrapper does not remove it from the host. On Linux,
Advanced.Linux.SetAdvertisementLocalName sets the advertised name; macOS and iOS
have equivalents under Advanced.MacOS and Advanced.IOS.
Configuration and logging
Config holds process-wide backend options. Set them before the first
enumeration. For example, Config.SimpleBluez.ConnectionTimeout changes the
Linux connection timeout, Config.Android.ConnectionPriority requests a
connection priority after each Android connection, and Config.ResetAll()
restores the defaults.
Logging.Level sets the native log level. Logging.SetCallback forwards log
entries to your code on a worker thread, Logging.LogDefaultStdout() writes
them to standard output, and Logging.LogDefaultFile(path) appends them to a
file.
Logging.Level = LogLevel.Debug;
Logging.SetCallback(entry => Console.WriteLine($"[{entry.Level}] {entry.Module}: {entry.Message}"));Trimming, NativeAOT, and MAUI
The binding is trimming and NativeAOT compatible and produces no trim or AOT warnings. A NativeAOT app needs no extra configuration; publish it as usual:
dotnet publish -c Release -r linux-x64 -p:PublishAot=trueThe native libraries are copied next to the executable. Self-contained and trimmed publishing work the same way.
In MAUI, reference the package from the app project and follow the platform setup below for each target. The package selects the native libraries for Android, iOS, and Mac Catalyst automatically.
Platform notes
Windows
Windows needs no manifest entries for Bluetooth, but Bluetooth must be enabled in system settings. Pairing requests show a system prompt.
Linux
SimpleSharpBLE uses BlueZ over D-Bus. On Ubuntu, install the runtime packages:
sudo apt-get install bluez libdbus-1-3The user needs access to the system Bluetooth service, which most desktop
distributions grant by default. Config.SimpleBluez.ConnectionTimeout and
DisconnectionTimeout adjust how long BlueZ operations wait.
macOS
A console app run from a terminal uses the terminal's Bluetooth permission;
grant it under System Settings > Privacy & Security > Bluetooth. An app
bundle needs NSBluetoothAlwaysUsageDescription in its Info.plist. Without
it, macOS terminates the process on the first Bluetooth access. See
Bluetooth Permissions.
Android
Target Android 12 or newer (SupportedOSPlatformVersion 31.0 or higher) with
the android-arm64 and/or android-x64 runtimes. The package adds its Java
bridge classes and ProGuard rules to the app. Declare and request
BLUETOOTH_SCAN and BLUETOOTH_CONNECT (and BLUETOOTH_ADVERTISE to host
services), enable Bluetooth, then initialize once on the application thread:
Java.Lang.JavaSystem.LoadLibrary("simpleble");
SimpleSharpBLE.Advanced.Android.Initialize(Java.Interop.JniEnvironment.Runtime.InvocationPointer,
Android.App.Application.Context.Handle);Run blocking operations off the UI thread. The Android scan example includes the manifest and runtime permission handling.
iOS, iPadOS, and Mac Catalyst
Set SupportedOSPlatformVersion to 15.8 or higher for iOS and 16.0 or higher
for Mac Catalyst; the build reports an error for lower values. Supported runtimes
are ios-arm64, iossimulator-arm64, and maccatalyst-arm64. Add
NSBluetoothAlwaysUsageDescription to Info.plist, and give sandboxed Catalyst
apps the com.apple.security.device.bluetooth entitlement. The native libraries
link statically and automatically. Run blocking BLE operations off the UI thread.
Advanced.IOS applies to both iOS and Mac Catalyst, and Utils.OperatingSystem
reports IOS on both.
See Examples for complete programs.
