ceezblog.com

I like cat, isn't it obvious?

Writing windows app to communicate with UART Transparent BLE module – C# winform

This post is about how to write a C# winform app to connect to your project using BLE module. This post is from point of view of an electronic engineer instead of software engineer and just focus on the BLE communication programming part only, especially for those UART transparent BLE modules, like RN4871. There is no complete sample in this post but just some hints and code snippet here and there to help with programming.

Introduction

Imagine, you write a windows app and you need to collect data from your microcontroller project, then pretty much you need to find a way to link them together, yeah? There are obvious choices:

  • USB to UART dongle and wire it up directly to UART port on your microcontroller
  • Implement wifi and IoT (internet of thing) software stack on your project and communicate with your host app via TCP-IP or so, like ESP8266 or ESP32
  • Use some transceiver modules and convert UART into RF, like RF24L01, CC1101, or just simply bit-bang data over a pair of 315 MHz transceiver modules
  • Use Bluetooth (known as BT classic) over virtual COM port, by using some module like HC-05
  • Or just use Bluetooth LE (BLE or bluetooth low energy)

Below is the pictures that I stole from the internet. And yes, I have tried them all.

If we wound back to 20-30 years ago, the obvious choice is BT classic and virtual COM port for a commercial product. Unless your project needs to dump a huge chunk of data without any loss, then you should choose TCP-IP over ethernet cable or wifi instead. But now, BLE is so popular these days, why not choosing BLE instead, yeah?

Before I wrote SVI-Toolset app, I really struggled with researching BLE communication programming on windows PC: there was almost no windows app, nor there was a solid sample code for windows PC to connect to a BLE module. I am talking about off-the-shelf module like RN4871, CC2541… The only really working sample I could find was BLE console. It was around Feb 2023 when I started experiment with BLE and untill today, Aug 2025, I still couldn’t find a working sample from M$.

Of course, there are some paid framework to work with BLE, which framework also compatible across muliple platforms, like Windows, MacOS, Linux… But for some small fry like me, it’s not possible to invest in a large sum of money and time for that.

Okay, enough ranting, let’s dig into the good stuff!

Prepare visual studio project

You might need newest visual studio. At this time of writing, VS Community 2022 is free. Make sure you install WinUI application development (or windows ux for older visual studio).

Just start a new winform project as usual, nothing special here. The trick is to add reference to windows ux library that provides headers for Bluetooth or Bluetooth Low Energy. Go to project list panel, add reference and browse to the header file:

C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.22000.0\Windows.winmd

If you can’t find this folder, then just download and install Windows SDK or choose a newer version that you have. I still use old version just fine, which version was for windows 10, instead of using newest version 10.0.26100

You actually can use VS2013 and install Windows SDK for the same header instead of using VS2022.

If you double-click on the the assembly name you will see all of the supported classes that this header provides. Among tons of classes this windows ux supports, we are interested in:

So, just add a winform dialog maybe, and a couple of labels, buttons… Then <view code> of your “form1.cs” and add those classes to your code.

Sorry if I am mixing terms from C++ and C#, I code embedded C more often than winform C#.

C#
using Windows.Devices.Bluetooth;using Windows.Devices.Bluetooth.Advertisement;using Windows.Devices.Bluetooth.GenericAttributeProfile;using Windows.Devices.Enumeration;using Windows.Devices.Radios;

How BLE works? View from a different angle

I’ll spare you the boring detail about BLE, GATT profile, GATT service… whatever. You can read it here https://www.bluetooth.com/. However, I am sure after you read/watch a sh!t load of those documents and videos about BLE, still you couldn’t write program to talk to a BLE module. But of course I have to include a tiny bit of info about this, just enough for you to write code.

Okay, before I go on, I advice you to forget everything you know about BT classic and all definition of server/client or host/client you understand so far. I could have sworn those BLE definitions were made to brainf~ck with us. Just try not to compare BLE with everything you know, okay?

Every communication should be 2-way, yeah? Unless your project is about just sending out data, like temperature sensor or an SOS beacon. So that you need send and receive which is 2-way communication between 2 devices. BLE stuff doesn’t provide a direct definition of send and receive like a conventional communication like UART or SPI. but for BLE, you have multiple GATT services (or GATT profiles) on a single device, but most of the time you have only 1 or 2 services. Each service may be for different purposes.

You should only use your BLE device as BLE server and run the GATT services that your PC app can send request to. Below is what a BLE device should be like

So each BLE differenciates to another by their name and their MAC address while GATT services and Characteristics are distinguish by their UUIDs. Those UUIDs above were mocked up, btw. Basically, it is just the same as company tag, division tag, and individual worker tag. You can define your own tags to BLE device as you like. Some of off-the-shelf BLE modules do allow you to change those tags, others don’t.

Each characteristic could have multiple properties: read, write, notify… You don’t have to understand those properties. I can say each characteristic is a basket to hold the message so it can be 2-way or one-way. Normally we set UART-TX on one characteristic and UART-RX on another characteristic to elimate confusion. Datasheet of the UART transparent BLE module will tell you exactly which characteristic is to send or receive of UART.

For of RN4871 (page 65 RN4871 user’s guide)

  • Service UUID: 49535343-FE7D-4AE5-8FA9-9FAFD205E455
  • Characteristic UUID for UART-TX: 49535343-1E4D-4BD9-BA61-23C647249616
  • Characteristic UUID for UART-TX: 49535343-8841-43F4-A8D4-ECBE34729BB3

For WCH CH9141 (page 6 WCH BLE-TPT.pdf)

  • Transparent UART service UUID: 0xFFF0
  • Characteristic UUID for UART-TX: 0xFFF1
  • Characteristic UUID for UART-TX: 0xFFF2

Code from PC side

The work flow of communication to BLE device from PC app

You have to declare in your code a few objects to deal with the hierarchy structure of BLE.

C#
BluetoothLEDevice bleObj;GattDeviceService gattSer;GattCharacteristic gattRX;GattCharacteristic gattTX;

Your PC app must manage the BLE devices on its own. It seems more work, but it’s actually better for the user. Think about the old way, user has to find the correct COM port to connect to. It’s more trouble if user plugs in many devices that pop up as COM port, such as Arduino Leonardo, CP2102, CH910F. Arduino Leonardo driver is the most troublesome, as it pops up as different COM port when plugged in different USB port. Additionally, most users are not tech savvy who can open device manager to read the COM port number.

To simplify, You need to

  • Scan for nearby BLE
  • Select the correct BLE – store it’s Mac address for future use
  • Assign that BLE device to bleObj
  • Assign correct Gatt Service to gattSer
  • Assign correct Gatt Characteristics to corresponding gattRX and gattTX
  • Add event listener that monitors ValueChanged of gattRX
  • Use gattRX object to receive data
  • Use gattTX object to send data

Easy peasy, lemon squezzy!

Scan for BLE devices

There are two class you can use for scanning nearby BLE devices: BluetoothLEAdvertisementWatcher and DeviceWatcher

Ok, so BLE is totally completely different beast to BT classic. You DO NOT go to Bluetooth and devices to add a new BLE device. In fact, you don’t have to pair with BLE device using system. This is true for Windows and Android. I don’t know about iOS or linux though.

The ble “connected” status is just a virtual concept, as ble device only wake up, send data, then go back to sleep. Mostly. There is no need for pair or sync. Of course, BLE is not for transmitting data securely, so keep in mind that you should obscure your data before sending over BLE, by encoding or encrypting the payload.

You have to manage BLE device inside your code: scan, connect and disconnect! Yup, disconnect just makes killing the “virtual link” between PC app and BLE device quicker. There is a timeout before the BLE module decides to accept “new link” and broadcast its advertisement again, for RN4871 this is about 5s.

Okay, both BluetoothLEAdvertisementWatcher and DeviceWatcher can be used for scanning BLE beacons to detect nearby BLE devices, they are practically doing the same thing.

The only different is BluetoothLEAdvertisementWatcher only listen for BLE beacon or BLE advertisement messages. This can be super useful to filter out non BLE device nearby. While DeviceWatcher will monitor all nearby devices.

Using BluetoothLEAdvertisementWatcher is super easy

C#
// short versionBluetoothLEAdvertisementWatcher ble_watcher;ble_watcher = new BluetoothLEAdvertisementWatcher();ble_watcher.Received += (BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) => { /* add new device to the list here */ };// run the watcher when you need to scan nearby ble devicesble_watcher.Start();

Longer version of

C#
// a class to hold string data, similar to struct of C++class My_BLE_Device {	public string name;  public ulong address;  public short RSSI;  public My_BLE_Device(string device_name, ulong device_address, short my_RSSI){  	name = device_name;    address = device_address;    RSSI = my_RSSI;  }}List<My_BLE_Device> _ble_dev_list = new List<My_BLE_Device>();void scan() {	BluetoothLEAdvertisementWatcher ble_watcher;	ble_watcher = new BluetoothLEAdvertisementWatcher();	ble_watcher.Received += BLE_watcher_receive;		// run the watcher when you need to scan nearby ble devices	ble_watcher.Start();}// callback function when a new BLE device pops up in the scannerprivate void BLE_watcher_receive(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) {  if (args.Advertisement.LocalName.Contains("SV") || args.Advertisement.LocalName.Contains("BLE")) { // found device  var found_ble_dev = new My_BLE_Device(args.Advertisement.LocalName, args.BluetoothAddress, args.RawSignalStrengthInDBm);  // check in our list if not exist  if (_ble_dev_list.Count == 0 || !_ble_dev_list.Exists(x => x.address.Equals(found_ble_dev.address))) {  	_ble_dev_list.Add(found_ble_dev);    }  }}

Just have to add event listenner to run a callback function when receive an advertisement beacon. You get RSSI (received signal strength indicator) coming along with ble advertisement beacon.

As sample code above, I only choose to add ble devices which name contains “SV” or “BLE”.

In the other hand, using DeviceWatcher is a little bit of dark magic involved

Below is some code I took from BLE Console app

C#
List<DeviceInformation> _deviceList = new List<DeviceInformation>();string _aqsAllBLEDevices = "(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")";string[] _requestedBLEProperties = { "System.Devices.Aep.DeviceAddress", "System.Devices.Aep.Bluetooth.Le.IsConnectable", };deviceWatcher = DeviceInformation.CreateWatcher(_aqsAllBLEDevices, _requestedBLEProperties, DeviceInformationKind.AssociationEndpoint);deviceWatcher.Updated += (_, __) => { }; // add an empty inline function for this event listenerdeviceWatcher.Added += (DeviceWatcher sender, DeviceInformation devInfo) => { if (_deviceList.FirstOrDefault(d => d.Id.Equals(devInfo.Id) || d.Name.Equals(devInfo.Name)) == null) _deviceList.Add(devInfo); };deviceWatcher.Start();

There are a few voodoo stuffs to put into the initial constructor there, alright. You will get more detail about the device this way but you don’t have RSSI information. For my need, RSSI is more important than extra detail about a BLE device.

Just a note: ESP32’s BLE stack does not play nice with BluetoothLEAdvertisementWatcher. Somehow ESP32 doesn’t advertise it’s ble name, it’s just blank! So, I suggest to use both if you are connect to ESP32 to fix this. If you plan to use ESP32 as a transparent UART passthrough for your project, I advise you not to go into this rabbit hole. Although ESP32 allows you to freely program ESP32 to do whatever you want it to do, but the lack of DMA stuffs of arduino framework, make it very difficult to do it correctly.

Connect, Send and Receive data from BLE module

So, you have a list of BLE candidates, you choose one to connect to and then you should check if the BLE device is the correct one.

One way to do that is to match the UUIDs of the target BLE device with the UUIDs from the datasheet. The code blow is to check if the BLE device has the same UUIDs of TX and RX for RN4871 BLE module.

C#
async Task Connect_BLE(ulong dev_address){    // Try assign a BLE device using its address to bleObj    bleObj = await BluetoothLEDevice.FromBluetoothAddressAsync(dev_address).AsTask().TimeoutAfter(10000);    // Go through all of its available services    var result = await bleObj.GetGattServicesAsync(BluetoothCacheMode.Uncached);    if (result.Status == 0) { // status = 0 = no problem        bool found = false;        foreach (GattDeviceService ser in result.Services) { //search through services to get our target services            if (ser.Uuid.ToString().Equals("49535343-fe7d-4ae5-8fa9-9fafd205e455")) { // found our Gatt service for RN4871                gattSer = ser;                found = true;            }        }        if (!found) {    //if not found the correct gatt service            bleObj.Dispose(); //we got squat, so dispose of this object            return;        }				// we have alread found correct characteristic with the same unique id        var result2 = await gattSer.GetCharacteristicsAsync();        if (result2.Status == GattCommunicationStatus.Success && result2.Characteristics.Count>1) {            gattRX = result2.Characteristics[0];    // first characteristic should be RX            gattTX = result2.Characteristics[1];    // second characteristic should be TX            // check if UUIDs are match            if (!gattRX.Uuid.ToString().Equals("49535343-1e4d-4bd9-ba61-23c647249616") || !gattTX.Uuid.ToString().Equals("49535343-8841-43f4-a8d4-ecbe34729bb3")) {                bleObj.Dispose(); // no match --> dispose                return;            }        }        else {            bleObj.Dispose();            return;        }        // looking good, we got a solid connection        // Subcribe to value_changed on RX characteristic => callback Characteristic_ValueChanged()        var status = await gattRX.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);        if (status == GattCommunicationStatus.Success) {            gattRX.ValueChanged += Characteristic_ValueChanged;            Callback_DeviceConnected(); // run some routine after have a solid link to BLE        }        return; // Connect successfully     }        // handle error    tb_Stat.AppendText("\r\nTimeout - Connect fail.");}

Once UUIDs are verified, you have a solid target to read and write data to. So just use gattRX object and gattTX object to send and receive data.

To “connect” or establish a “link” to BLE module, you just write something to BLE device and wait for response. Like the sample below, I write a change of configuration and set it to notify on the receiving GATT characteristic.

C#
var status = await gattRX.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);if (status == GattCommunicationStatus.Success){    gattRX.ValueChanged += Characteristic_ValueChanged; // monitor if value of this characteristic changed    isConnect = true;    Callback_DeviceConnected();}

If success, then the app knows that BLE module is ready to reply to request of the app. Again, there is no definition of “connected” concept for BLE, I just make it up for more intuitive usage.

On PC side, you will have send function like this

C#
// global declareGattCharacteristic gattTX;async Task SendData_BLE(byte[] data){    if (!isConnect) return;    var writer = new DataWriter();    writer.WriteBytes(data);    // WriteByte used for simplicity    await gattTX.WriteValueAsync(writer.DetachBuffer());}// in data preparation functionbyte[] data = new byte[5];data[0] = (byte)_BLE_MSG_ID.BM_REQUEST_BATTERY_VOLTAGE;data[1] = (byte)'0';data[2] = (byte)'0';data[3] = (byte)_BLE_MSG_ID.BM_SEPARATOR;_ = SendData_BLE(data); // assign an empty holder for this task

So, you write some data to a GATT service, that assosiates with TX line of UART, and magically on the other end of BLE module, it spits out the same data on its UART port.

Receive function is like this

C#
// global declareGattCharacteristic gattRX;// add event listener for when the value of that GATT service has changedgattRX.ValueChanged -= Characteristic_ValueChanged;// Callback funtion for event listenervoid Characteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args){    byte[] data;    CryptographicBuffer.CopyToByteArray(args.CharacteristicValue, out data);    ProcessData(data);}

You add an event listener to GATT characteristic object and then when data poured in, the callback function will be invoked with the message from UART port.

The problem is, one BLE message contains multiple bytes (characters) in one payload while each payload on UART line is just a single byte. A broastcasting interval must be introduced to send a bunch of bytes after some time, like 100ms for CH9141 and about 50ms for RN4871. Basically, after 50ms, BLE module just dumps all the data it currently holds to PC app.

The maximum size of payload (or MTU, Maximum Transmission Unit) is dictated by the firmware of that BLE module. RN4871 fw1.1.8 only does 20 chars as payload while RN4871 fw1.3.0 can do 50 chars as payload. So that your message will be cut into 2 ble messages instead of 1.

On the PC app, if you receive each payload and process each payload individually, you might have corrupted data.

To overcome this, you can build yourself a custom protocol to recognize your data package with unique identifiers, like

Message #1Message #2
AAAAhello_to_my_friendsZZZZ

AAAA marks the beginning of your data and ZZZZ marks the end of data. So that you just collect multiple messages continuously but only process those messages when you see both AAAA and ZZZZ in the data you collected.

You can implement a fix-frame format like below for each BLE payload, assuming each data field is a 16bit number

C#
// 4 data fields separated by comma in a single ble payload[data_field_1],[data_field_2],[data_field_3],[data_field_4]// expand it into individual byte[byte1][byte2][,][byte4][byte5][,][byte7][byte8]...

You can use your own creativity to make a suitable frame format for you.

For ESP32, it is possible to change the MTU from default 23 bytes to a higher number such as 500 bytes. Which indeed will give you more flexibility to frame your data.

Anyhow, I had a few bad experience with ESP32, both hardware and software. It still leaves bad taste in my mouth after about 5 years already. So that I don’t recommend using ESP32 for something that needs to be reliable and long lasting.

Additional security stuff on the BLE device side

Most of the UART transparent BLE modules don’t have a bluetooth profile, which auto “connects” to last paired device and does not allow new device to pair with, like bluetooth HID or bluetooth a2dp… Any client (your pc app from different PC) can connect to it at will without any pin code or so.

Basically, if only you have the app and only you have the BLE device, then there is not a possibility someone tamper your device over BLE connection. But if you use this on a commercial product, this could be very bad. Imagine that you can freely pair with your neighbour BT speaker and you play heavy rock music at 2AM in the morning. Yeah, that problem.

The PIN code paring of BLE stack is quite finicky and doesn’t work. So that you should implement software password check for yourself:

  • The device still allows connection but for a few seconds, just like you have 60s to disable house alarm after you unlock front door
  • Sustain a connection only after client sends correct pin code
  • Disconnect if the client doesn’t send correct pin code after a few tries
  • Reject connection after a delay, sending a disconnect notification. The time delay would deter brute force attack

You should manage these code in your application microcontroller instead, which microcontroller that BLE module wires to via UART port.

You can reject connection by reset the BLE module (pull RST pin to ground), disrupt power to the BLE module or even “enter programming” and issue a disconnect command manually.

If your product is used or is going to be used by a large number users you should take security seriously.

In summarise

This post is not a tutorial for you to write C# code to connect to your BLE. It’s just some pointers and a few code snipet here and there.

  1. Choose a suitable BLE module as transparent UART passthrough.
  2. Read datasheet for its UUIDs for connecting to.
  3. Prepare Visual Studio project with reference to Windows UX package that provides Bluetooth relate classes
  4. Write your winform app that can
    • Manage the list of available BLE devices
    • Check if the select device is the correct target
    • Send BLE messages
    • Receive BLE messages
  5. Write pin code feature in your application microcontroller

And that’s that!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *