Sunday, June 8, 2025

A quick guide to using Bluetooth serial ports from C#



Reading from classic Bluetooth serial ports isn't hard but does involve understanding 5 different classes and connecting them together. In this post, I'm show all the classes you need to make the code work, how to create or use them, and how they fit together. This walk-through will be in reverse order: I'll start with actually reading data and then work backwards through the classes. The recapitulation at the end will list the objects in forward order.

I use this code to connect to a small portable GPS device that sends National Maritime Electronics Industry (NMEA) formatted GPS information over a Bluetooth serial port device and translate and display the resulting messages in an easy-to-use app.

Starting with the DataReader class you need to read from your device, and ending up with the generic Device interfaces, here's the classes and objects you need to use your Bluetooth serial-port connected device.

Use a DataReader to read the data. 

Documentation DataReader:

Hints for using DataReader: call LoadAsync(count) to read from the serial port into the DataReader's internal memory and then ReadString() to get a string of the data. Be sure to set the InputStreamOptions to Partial. Partial means that whenever there's data the load will return. This is important because the serial port sends just a little data at a time.

Constructing the DataReader: construct the DataReader with the constructor that takes an IInputStream as a parameter

The IInputStream is from a connected StreamSocket. 

Documentation IInputStream:

An IInputStream is an interface for the concept of "getting stream of data (with no framing) from a specific source like a Bluetooth device or a network connection. You won't be calling any methods on it.

Getting the IInputStream: your IInputStream will be from a connected StreamSocket's InputStream property.

The StreamSocket is connected using data from an RfcommDeviceService objects

Documentation StreamSocket

Hints for the StreamSocket: StreamSocket is a single class that can handle multiple different type of connections. Common uses are for networking (where it's a classic TCP/IP network connection) and for Bluetooth classic. You'll just connect it and then use its InputStream property.

Constructing the StreamSocket: construct the StreamSocket with the default constructor with no parameters and then connect it using the RfcommDeviceService's ConnectionHostName and ConnectionServiceName. The hostname will look like "(00:19:01:48:4E:F5)" and the servicename will look like "Bluetooth#Bluetooth2c:0d:a7:c8:53:33-00:19:01:48:4e:f5#RFCOMM:00000000:{00001101-0000-1000-8000-00805f9b34fb}"

The RfcommDeviceService is from a BluetoothDevice object

Documentation RfcommDeviceService:

Hints for the RfcommDeviceService: The RfcommDeviceService is a little confusing because there's so much apparent overlap between a BluetoothDevice which supports an RfcommDeviceService. A good way to think of it is that my GPS Bluetooth serial device is the "BluetoothDevice" as seen by Windows. And the RfcommDeviceService is the serial port that could theoretically be connected to anything.

Getting the RfcommDeviceService: The RfcommDeviceService is gotten from the BluetoothDevice. There are two ways to do this: 

  1. Call the GetRfcommServicesForIdAsync()  method to get just the specific serial service you want. This lets you ask for a serial port and not, for example, an ObexFileTransfer. I know ahead of time that I want the SerialPort service, so I use this method to get just the right matching services (although I  think there will always be just the one)
  2. Call the GetRfcommServicesAsync() method get a list of all possible serial services and then pick your preferred service from it.

It's important to know that Device might support both the generic raw serial port as RfcommServiceId.SerialPort and might also support the more structured ObexFileTransfer. For my specific GPS device, it supports both the SerialPort and also the MFi/iAP(2) protocol. When I connect to the device, I see the NMEA messages, so it must be the right one.

BluetoothDevice object is initialized from a DeviceInformation Id

Documentation DeviceInformation:

The BluetoothDevice object that's used to get the RfcommDeviceService is used in the program to track device events like the connection being lost, and to ask for permission to access the device.

Constructing the BluetoothDevice: call the static BluetoothDevice.FromIdAync(deviceId) method, passing in the select DeviceInformation's Id property.

The DeviceInformation object is found with DeviceInformation.FindAllAsync(query)

The DeviceInformation object is used by the Windows device system to return information about devices in a Windows computer. A device can be an adapter, like a USB port or Bluetooth port, or a device like a video card or monitor, or an input device like a keyboard or mouse. 

Getting a DeviceInformation object. Devices are found using a query string in the Advanced Query Syntax (AQS) format. This format has no obvious documentation at learn.microsoft.com. The good news is that you can get pre-created strings for devices, so it's no great loss. Get a pre-created string from the static GetDeviceSelector() method on each device class. The methods often do not require any parameters; sometime they take in some kind of specialty parameter. For my program, I eventually need a BluetoothDevice, and I know it must be paired, so I use an AQS query string from BluetoothDevice.

Use the AQS string in the DeviceInformation.FindAllAsync() method. This returns a list of DeviceInformation objects. You'll have to look at each DeviceInformation object to decide which one to use. I often do a match using the Name property.

Hints for using the DeviceInformation: Most of code for reading and writing a Bluetooth serial port is very similar and can often be shared between your different Bluetooth projects. But matching a device is often unique to the device and will have to be changed for every program. It's best to not try to wrap the matching code into something "handier" or "easier to understand".

Recap: critical steps

Let's recapitulate the steps, but this time in the order you'll do it in your code. In these steps, names in UpperCase are classes and lowerCase are objects. 

The steps to reading from a Bluetooth serial device are:

  1. Get a DeviceInformation object by using an AQS string from a static device GetDeviceSelector() method and passing that AQS string to the static DeviceInformation.FindAllAsync() method
  2. Create a BluetoothDevice from the deviceInformation.Id property with the static BluetoothDevice.FromIdAsync() method
  3. Find the correct RfcommDeviceService from the bluetoothDevice.GetRfcommServicesForIdAsync() method.
  4. Construct a StreamSocket and then connect it with the rfcommDeviceService's ConnectedHostName and ConnectedHostService
  5. Get an IInputStream from the streamSocket.InputStream property
  6. Create a DataReader object with the DataReader constructor that takes an IInputStream as a parameter. Set the dataReader option to Partial
  7. Call dataReader.LoadAsync() to get some of the serial data from the device, and call dataReader.ReadString() to get the data as a string.


Thanks for reading, and good luck!