Developing applications which use Bluetooth can be tricky. Commonly, on one side of the communication you have some hardware or maybe some IoT device, while on other, there is some mobile app or similar. When doing geenfield projects, normally you don’t have everything in place on the device side, but you need to develop Bluetooth application stack in parallel with device development/production.
In this blog post I will present one way how to handle this situation. I am going to present Bluetooth communication between two Bluetooth endpoints.
I will show “server-pheripheral side” of Bluetooth communication, side which is waiting for connection. In the next blog post I will present also client side. This will be presented by Xamarin.Forms mobile app (Android and iOS).
Before digging into code, I will present some basic stuff.
Bluetooth.
Bluetooth is a wireless technology standard for exchanging data between fixed and mobile devices over short distances. It’s widely used technology, especially for connecting telephones, speakers, tablets, media players, robotics systems, laptops, and console gaming equipment as well as some high definition headsets, modems and even watches. Bluetooth is really broad topic, but If someone wants to go deeper here is the start point : www.bluetooth.com.
Universal Windows Platform – UWP
I will use Universal Windows Platform (UWP) for server side app development. UWP has solid support for Bluetooth. There are two different Bluetooth technologies that you can choose to implement in your app.
In my case I will use Bluetooth Low Energy (LE). This specification defines protocols for discovery and communication between power-efficient devices. Discovery of devices is done through the Generic Access Profile (GAP) protocol. After discovery, device-to-device communication is done through the Generic Attribute (GATT) protocol. For more about Bluetooth LE, you can check Bluetooth Core Specification version 4.0, where Bluetooth LE was introduced.
Application development stack for developing Bluetooth application are under:
What is GATT?
Generic Attribute Profile (GATT) is built on top of the Attribute Protocol (ATT) and establishes common operations and a framework for the data transported and stored by the Attribute Protocol.
GATT is an acronym for the Generic Attribute Profile, and it defines the way that two Bluetooth Low Energy devices exchange data. Basic concepts here are called Services and Characteristics. Attribute Protocol (ATT) store Services, Characteristics and related data in a simple lookup table using 16-bit IDs (UUIDs or to Windows developers more familiar (and very similar) entities called GUIDs).
GATT comes into play when devices are connected, meaning that you have already gone through the advertising process called GAP.
Very important thing here is that GATT connection is exclusive. On short, a BLE peripheral can only be connected to one central device (a mobile phone, etc.) at a time! As soon as a peripheral is connected to a central device, it will stop advertising and other devices will no longer see it or connect to this peripheral device.
Structure of GATT protocol: Profile, Service, Characteristics, Characteristic descriptors.
GATT is (again) very broad topic, but you can find a lot of material about this topic on the internet or at official Bluetooth site.
Creating GATT server on top of UWP platform.
Universal Windows Platform (UWP) has good support for Bluetooth, so I decided to try with this framework.
Show me the code!
I will host my GATT server inside (UWP) Console application and UWP GUI app.
For the purpose of this example I introduced my GATT server interface. It’s simple, minimalistic definition. I will basically just exchange simple data over Bluetooth connection, so for that purpose this interface definition will provide all I need:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public interface IGattServer { event GattChararteristicHandler OnChararteristicWrite; Task Initialize(); Task<bool> AddReadCharacteristicAsync(Guid characteristicId, string characteristicValue, string userDescription); Task<bool> AddWriteCharacteristicAsync(Guid characteristicId, string userDescription); Task<bool> AddReadWriteCharacteristicAsync(Guid characteristicId, string userDescription); void Start(); void Stop(); } |
My GATT server will be able to initialize read, write and read-write characteristics inside GATT service. Not a rocket science, but enough to get it going. Simple cases will be covered, which can be quickly extended on more complex scenarios.
Just to summarize what is going on in code below:
- “Root” service is defined via
GattServiceProvider.CreateAsync(_serviceId)
. ServiceProvider
is then extracted from service.- Characteristics can be added on top of root Service.
- On
Start()
service is advertised (available to external devices).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
public class GattServer : IGattServer { private GattServiceProvider _gattServiceProvider; private readonly ILogger _logger; private readonly Guid _serviceId; public delegate void GattChararteristicHandler(object myObject, CharacteristicEventArgs myArgs); public event GattChararteristicHandler OnChararteristicWrite; public GattServer(Guid serviceId, ILogger logger) { _logger = logger; _serviceId = serviceId; } public async Task Initialize() { var cellaGatService = await GattServiceProvider.CreateAsync(_serviceId); if (cellaGatService.Error == BluetoothError.RadioNotAvailable) { throw new Exception("BLE not enabled"); }; if (cellaGatService.Error == BluetoothError.Success) { _gattServiceProvider = cellaGatService.ServiceProvider; } _gattServiceProvider.AdvertisementStatusChanged += async (sender, args) => { await _logger.LogMessageAsync( sender.AdvertisementStatus == GattServiceProviderAdvertisementStatus.Started ? "GATT server started." : "GATT server stopped."); }; } public async Task<bool> AddReadCharacteristicAsync(Guid characteristicId, string characteristicValue, string userDescription = "N/A") { await _logger.LogMessageAsync($"Adding read characteristic to gatt service: description: {userDescription}, guid: {characteristicId}, value: {characteristicValue}."); var charactericticParameters = new GattLocalCharacteristicParameters { CharacteristicProperties = GattCharacteristicProperties.Read, StaticValue = Encoding.UTF8.GetBytes(characteristicValue).AsBuffer(), ReadProtectionLevel = GattProtectionLevel.Plain, UserDescription = userDescription }; var characteristicResult = await _gattServiceProvider.Service.CreateCharacteristicAsync(characteristicId, charactericticParameters); var readCharacteristic = characteristicResult.Characteristic; readCharacteristic.ReadRequested += async (a, b) => { await _logger.LogMessageAsync("read requested.."); }; // Warning, dont remove this... return characteristicResult.Error == BluetoothError.Success; } public async Task<bool> AddWriteCharacteristicAsync(Guid characteristicId, string userDescription = "N/A") { await _logger.LogMessageAsync($"Adding write characteristic to service: description: {userDescription}, guid: {characteristicId}"); var charactericticParameters = new GattLocalCharacteristicParameters { CharacteristicProperties = GattCharacteristicProperties.WriteWithoutResponse, WriteProtectionLevel = GattProtectionLevel.Plain, UserDescription = userDescription }; var characteristicResult = await _gattServiceProvider.Service.CreateCharacteristicAsync(characteristicId, charactericticParameters); if (characteristicResult.Error != BluetoothError.Success) { await _logger.LogMessageAsync("Adding write characteristic failed"); return false; } var characteristic = characteristicResult.Characteristic; characteristic.WriteRequested += async (sender, args) => { using (args.GetDeferral()) { // For this you need UWP bluetooth manifest enabled on app !!!!!!!! var request = await args.GetRequestAsync(); if (request == null) { return; } using (var dataReader = DataReader.FromBuffer(request.Value)) { var characteristicValue = dataReader.ReadString(request.Value.Length); if (OnChararteristicWrite != null) OnChararteristicWrite(null, new CharacteristicEventArgs(characteristic.Uuid, characteristicValue)); } if (request.Option == GattWriteOption.WriteWithResponse) { request.Respond(); } } }; return true; } public async Task<bool> AddReadWriteCharacteristicAsync(Guid characteristicId, string userDescription = "N/A") { await _logger.LogMessageAsync($"Adding write characteristic to cella service: description: {userDescription}, guid: {characteristicId}"); var charactericticParameters = new GattLocalCharacteristicParameters { CharacteristicProperties = GattCharacteristicProperties.WriteWithoutResponse | GattCharacteristicProperties.Read, WriteProtectionLevel = GattProtectionLevel.Plain, ReadProtectionLevel = GattProtectionLevel.Plain, UserDescription = userDescription }; var characteristicResult = await _gattServiceProvider.Service.CreateCharacteristicAsync(characteristicId, charactericticParameters); if (characteristicResult.Error != BluetoothError.Success) { await _logger.LogMessageAsync("Adding write characteristic failed"); return false; } var characteristic = characteristicResult.Characteristic; characteristic.WriteRequested += async (sender, args) => { using (args.GetDeferral()) { // For this you need UWP bluetooth manifest enabled on app !!!!!!!! var request = await args.GetRequestAsync(); if (request == null) { return; } using (var dataReader = DataReader.FromBuffer(request.Value)) { var characteristicValue = dataReader.ReadString(request.Value.Length); if (OnChararteristicWrite != null) OnChararteristicWrite(this, new CharacteristicEventArgs(characteristic.Uuid, characteristicValue)); } if (request.Option == GattWriteOption.WriteWithResponse) { request.Respond(); } } }; characteristic.ReadRequested += async (sender, args) => { var deferral = args.GetDeferral(); var request = await args.GetRequestAsync(); var writer = new DataWriter(); request.RespondWithValue(writer.DetachBuffer()); deferral.Complete(); }; return true; } public void Start() { if (_gattServiceProvider.AdvertisementStatus == GattServiceProviderAdvertisementStatus.Created || _gattServiceProvider.AdvertisementStatus == GattServiceProviderAdvertisementStatus.Stopped) { var advParameters = new GattServiceProviderAdvertisingParameters { IsDiscoverable = true, IsConnectable = true }; _gattServiceProvider.StartAdvertising(advParameters); } } public void Stop() { _gattServiceProvider.StopAdvertising(); // Warning: This does not change AdvertisementStatus status, fails on second click... } |
To host my GATT server I created two examples: console and UWP GUI version. Let’s check the first one:
UWP Console host.
In general, UWP programs are desktop GUI applications, but with small tricks, console application can be developed also. You can find all the information how to do it here: https://blog.pieeatingninjas.be/2018/04/05/creating-a-uwp-console-app-in-c/.
To summarize, In my console host I created new instance of my Gatt server, make some initialization and start broadcasting my service on my BLE Windows 10 Bluetooth infrastructure. The code is here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
using Jenx.Bluetooth.GattServer.Common; using System.Threading.Tasks; namespace Jenx.Bluetooth.GattServer.Console { internal class Program { static private ILogger _logger; static private IGattServer _gattServer; private static async Task Main(string[] args) { InitializeLogger(); InitializeGattServer(); await StartGattServer(); await StartLooping(); } #region Private private static void InitializeLogger() { _logger = new ConsoleLogger(); } private static void InitializeGattServer() { _gattServer = new Common.GattServer(GattCharacteristicIdentifiers.ServiceId, _logger); _gattServer.OnChararteristicWrite += GattServerOnChararteristicWrite; } private static async Task StartGattServer() { try { await _logger.LogMessageAsync("Starting Initializong Jenx.si Bluetooth Gatt service."); await _gattServer.Initialize(); await _logger.LogMessageAsync("Jenx.si Bluetooth Gatt service initialized."); } catch { await _logger.LogMessageAsync("Error starting Jenx.si Bluetooth Gatt service."); throw; } await _gattServer.AddReadWriteCharacteristicAsync(GattCharacteristicIdentifiers.DataExchange, "Data exchange"); await _gattServer.AddReadCharacteristicAsync(GattCharacteristicIdentifiers.FirmwareVersion, "1.0.0.1", "Firmware Version"); await _gattServer.AddWriteCharacteristicAsync(GattCharacteristicIdentifiers.InitData, "Init info"); await _gattServer.AddReadCharacteristicAsync(GattCharacteristicIdentifiers.ManufacturerName, "Jenx.si", "Manufacturer"); _gattServer.Start(); await _logger.LogMessageAsync("Jenx.si Bluetooth Gatt service started."); } private static async Task StartLooping() { System.ConsoleKeyInfo cki; System.Console.CancelKeyPress += new System.ConsoleCancelEventHandler(KeyPressHandler); while (true) { await _logger.LogMessageAsync("Press any key, or 'X' to quit, or "); await _logger.LogMessageAsync("CTRL+C to interrupt the read operation:"); cki = System.Console.ReadKey(true); await _logger.LogMessageAsync($" Key pressed: {cki.Key}\n"); // Exit if the user pressed the 'X' key. if (cki.Key == System.ConsoleKey.X) break; } } private static async void KeyPressHandler(object sender, System.ConsoleCancelEventArgs args) { await _logger.LogMessageAsync("\nThe read operation has been interrupted."); await _logger.LogMessageAsync($" Key pressed: {args.SpecialKey}"); await _logger.LogMessageAsync($" Cancel property: {args.Cancel}"); await _logger.LogMessageAsync("Setting the Cancel property to true..."); args.Cancel = true; await _logger.LogMessageAsync($" Cancel property: {args.Cancel}"); await _logger.LogMessageAsync("The read operation will resume...\n"); } private static async void GattServerOnChararteristicWrite(object myObject, CharacteristicEventArgs myArgs) { await _logger.LogMessageAsync($"Characteristic with Guid: {myArgs.Characteristic.ToString()} changed: {myArgs.Value.ToString()}"); } private static void StopGattServer() { _gattServer.Stop(); } #endregion Private } } |
When I compiled everything and run the application, my output was:
The most important (but easy to forget) thing is that you must enable Bluetooth Permission in UWP Package manifest. If this is not enabled, write characteristics will not work as expected.
UWP GUI host.
I like “big button” projects for my proof-of-concept experiments: In the next section, I will quickly present UWP GUI (big button) application which hosts my GATT server. Nothing fancy, just two buttons and one text output control, e.g.:
You can spot similarity with console version. The core application flow is identical.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public sealed partial class MainPage : Page { private ILogger _logger; private IGattServer _gattServer; public MainPage() { InitializeComponent(); InitializeLogger(); InitializeGattServer(); } private void InitializeLogger() { _logger = new ControlLogger(LogTextBox); } private void InitializeGattServer() { _gattServer = new Common.GattServer(GattCharacteristicIdentifiers.ServiceId, _logger); _gattServer.OnChararteristicWrite += _gattServer_OnChararteristicWrite; ; } private async void _gattServer_OnChararteristicWrite(object myObject, CharacteristicEventArgs myArgs) { await _logger.LogMessageAsync($"Characteristic with Guid: {myArgs.Characteristic.ToString()} changed: {myArgs.Value.ToString()}"); } private async void StartGattServer_Click(object sender, RoutedEventArgs e) { try { await _gattServer.Initialize(); } catch { return; } await _gattServer.AddReadWriteCharacteristicAsync(GattCharacteristicIdentifiers.DataExchange, "Data exchange"); await _gattServer.AddReadCharacteristicAsync(GattCharacteristicIdentifiers.FirmwareVersion, "1.0.0.1", "Firmware Version"); await _gattServer.AddWriteCharacteristicAsync(GattCharacteristicIdentifiers.InitData, "Init info"); await _gattServer.AddReadCharacteristicAsync(GattCharacteristicIdentifiers.ManufacturerName, "Jenx.si", "Manufacturer"); _gattServer.Start(); } private void StopGattServer_Click(object sender, RoutedEventArgs e) { _gattServer.Stop(); } } |
Testing my GATT server.
In order to test my Bluetooth GATT server I used one of many mobile BLE client applications, in my case nRF Connect.
I scanned my network, I got my device list and connect to my “device”:
And then I play around with my application by changing my GATT characteristics.
And my GATT server just logger my data transferred via BLE.
Conclusion.
To conclude: in this blog post you can find all details howto create GATT server in order to test or experiment with Bluetooth LE connection. Example presented here is just basic scenario, but this example can easily be extended in more realistic (complex) cases.
In the next blog post I will talk about how to connect and to exchange data with BLE with Xamarin.Forms Android and iOS mobile apps.
Hope you enjoyed & stay tuned.
And yes, you can download code for this experiment here: https://github.com/josipx/Jenx.Bluetooth.GattServer
4 thoughts on “Bluetooth GATT server.”
This gatt server project is really helpfull for me !!
I have followed up your example and been able to create my own gatt server and mobile application, thank you !!
The only point, your gatt server is UWP dependant. That’s mean it cannot run on some windows machine (for instance Windows 10 LTSB or LTSC)
Do you have any idea of how to create a gatt server without using UWP librairies ?
I can’t seem to get the code to build. Any idea why?