Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Classic Bluetooth | Getting Started

Classic Bluetooth (BR/EDR) for the devices Bluetooth LE never replaced: SPP serial adapters, receipt and label printers, barcode scanners, OBD dongles and a long tail of industrial and medical equipment. One IBluetoothManager is both the client and the server. It finds devices, pairs with them, and opens or accepts RFCOMM connections that you read and write as an ordinary System.IO.Stream.

GitHub GitHub stars for shinyorg/shiny
Downloads NuGet downloads for Shiny.Bluetooth
Frameworks
.NET MAUI
.NET
Operating Systems
Android
Windows
macOS
Linux
Operation Android Windows macOS Linux
Paired devices ✅ ✅ ✅ ✅
Discovery (inquiry) ✅ ✅ ✅ ✅
Pair ✅ ✅ ✅ ✅ (needs a BlueZ agent for PIN / confirmation)
Connect (RFCOMM client) ✅ ✅ ✅ ✅
Listen (RFCOMM server) ✅ ✅ ✅ ✅
Make discoverable ✅ (system dialog) ❌ ❌ ✅

IBluetoothManager.Capabilities reports the same thing at runtime. An operation the platform does not support throws BluetoothNotSupportedException.

claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View Skills Repository
Terminal window
dotnet add package Shiny.Bluetooth

On Linux, add the BlueZ-backed package as well:

Terminal window
dotnet add package Shiny.Bluetooth.Linux
using Shiny;
builder.Services.AddBluetooth(); // Android, Windows, macOS
services.AddLinuxBluetooth(); // Linux

A MAUI app that also targets iOS has to leave the package out of the iOS and Mac Catalyst builds:

<PackageReference Include="Shiny.Bluetooth"
Condition="!$(TargetFramework.Contains('-ios')) AND !$(TargetFramework.Contains('maccatalyst'))" />
#if ANDROID || WINDOWS
builder.Services.AddBluetooth();
#endif

Then ask for access. This requests the Android permissions or raises the macOS privacy prompt. It returns Disabled when access is granted but the radio is off:

var access = await bluetooth.RequestAccess();
if (access != AccessState.Available)
return;

See Platform Setup for the manifest entries and entitlements.

var devices = new List<IBluetoothDevice>(await bluetooth.GetPairedDevices());
await foreach (var result in bluetooth.Discover(ct))
{
Console.WriteLine($"{result.Device.Name} {result.Device.Address} {result.Rssi} dBm");
if (!devices.Contains(result.Device))
devices.Add(result.Device);
}

Discover runs an inquiry for about 12 seconds and yields each address once. On Android and macOS that is the OS’s own inquiry cycle. Windows and Linux would otherwise scan until stopped, so they get the same window (IBluetoothManager.InquiryDuration). Cancel the token or break out of the loop to end it sooner. Finish the inquiry before connecting: an inquiry in progress slows connections on the same radio to a crawl.

Each BluetoothDiscoveryResult holds the Device and the Rssi (signal strength in dBm) it was heard at. The signal strength describes that sighting, not the device, so it lives on the result. Devices from GetPairedDevices() have none.

An IBluetoothDevice carries:

Property
Address Always AA:BB:CC:DD:EE:FF. GetDevice also accepts aa-bb-…, AABBCC… and the other platform forms. Two devices are equal when their addresses are.
Name The friendly name, or null if the OS has not resolved it yet
IsPaired Whether this machine is bonded with it, as read and kept current by Pair()
ClassOfDevice / MajorDeviceClass The raw Class of Device, and its major class decoded (Phone, Imaging, AudioVideo, …)
Pair() / Connect(serviceUuid) See below and Connections

You don’t need to discover a device you already know the address of:

var device = await bluetooth.GetDevice("00:1A:7D:DA:71:13");

GetDevice fills in the name, pairing state and class from whatever the OS already knows about the address. On Linux, BlueZ must already know the device (paired, or seen by Discover) before you can connect.

if (!device.IsPaired && !await device.Pair(ct))
return; // declined, wrong PIN, or timed out

The OS shows its own confirmation or PIN prompt where the pairing method needs one. Pair returns true when the device ends up paired, including when it already was. A secure Connect pairs on the way in anyway, so call Pair when you want the prompt to come at a predictable point in your UI.

On Linux, a device that asks for a PIN or a confirmation needs a BlueZ agent to answer it. A desktop session already has one. On a headless device, pair once with bluetoothctl (agent on, default-agent, pair <address>). Shiny marks a device it paired as trusted, so it can connect to your server later without BlueZ asking an agent to authorize it.

  • Connections: connect to a service, host one, and work with the stream
  • Platform Setup: permissions, entitlements and platform limits