The first step in building a Bluetooth Low Energy (BLE) app is to understand the basics of how Bluetooth provides data to clients. We need to understand some basic terminology and concepts to interpret the specifications. With that knowledge, it will be easier to build an application that can interpret the data for a workout. This post won’t go too deep into the underlying implementation details, but it will provide some the concepts you need to understand to build a BLE app.
Most languages provide a way to interact with Bluetooth devices. Under the covers, these libraries rely on the operating system for the actual communication. For example, on Apple devices, you use the Core Bluetooth framework to interact with BLE devices. On Android, you use the Android Bluetooth API. Windows provides the Windows.Devices.Bluetooth namespace for interacting with BLE devices. These frameworks provide a way to interact with the Bluetooth hardware without having to understand the underlying implementation. Ever wondered what’s really happenening?
Mind the GAP (and other terminology)
Bluetooth has specifications covering every aspect of its implementation. The first component to understand is the Generic Access Profile (GAP). It’s responsible for Device Disovery. Devices (called “Bluetooth Peripherals”) periodically emit small amounts of data to announce their presence. This data is called an advertisement. The advertisement contains information about the device, such as its name, services, and other data. Other devices (called “Bluetooth Central”) can listen for these announcements to discover and connect to broadcasting devices. You’ll see the word “Profile” frequently. It’s simply a standard for connecting devices. GAP is responsible to advertising an unconnected interactions
Each Peripheral also contains a number of attributes that describe the device and its services, defined by the Generic Attribute (GATT) profile. GATT is responsible for data transfer. This profile requires devices to provides a way to understand the features it provides. The various attributes are defined with a 128-bit identifier (UUID). As we dive in, you may notice that some standardized attributes have a 16-bit identifier. These are just a shorthand. The values are added to a known base identifier to create the final 128-bit UUID.
There are logical containers for providing data to interesting clients called Services. Each service groups together related information. Clients can filter for devices that provide specific services as part of the process of connecting to a device. For example, the treadmill device I’m using exposes training data throught the Fitness Machine Service. Each service contains Characteristics that provide the actual data. Characteristics provide internal state, sensor data, or other information that can be read or written. For example, the treadmill’s Fitness Machine Service contains a Characteristic for the current speed of the treadmill. Similar to services, an interested device can filter for specific characteristics.
The details of what a Characteristic provides are called Properties. Properties define what operations are permitted for the Charactiertic’s Value (such as Read and Write). An important operation to know is “Notify”. When a client subscribes to a Characteristic’s notifications, the peripheral will send updates to the client whenever the value of the Characteristic changes. This is useful for real-time data, such as the current speed of the treadmill. For programmers, it’s helpful to think of values as being arbitrary data. That is, the values may represent single values (such as a battery level) or multiple values at once (such as the speed, inclination, and running time of a treadmill).
Characteristics can also include Descriptors that provide additional information about the Characteristic, such as the format and unit of the data (or the status of connections). For now, it’s enough to know they exist.
I know that’s a lot of information to take in. The important thing to know is that you can filter on services and chacteristics. Once you’ve identified devices that provide the right services, you can connect to them and interact with characteristics to read and write data.
Data Types
The values of Characteristics can be primitives or serialized objects that contain multiple primitives. Unfortunately, most platforms leave part of the process of interpreting the data up to the developer. As a result, you have to understand these details in a bit more depth. For the application I’m building, the key characteristic I’ll need is Treadmill Data that’s provided by the Fitness Machine Service (FTMS). The Characteristic is a structure, detailed in the GATT Specification Supplement v7. The first few fields look something like this:
Field | Data Type | Size (octets) | Description |
---|---|---|---|
Flags | boolean[16] | 2 | Indicates which fields are present |
Instantaneous Speed | uint16 | 0 or 2 | Speed of the treadmill belt in units of 1/100 kilometer per hour. Present if bit 0 of Flags field set to zero. Represented values: M = 1, d = -2, b = 0. |
Average Speed | uint16 | 0 or 2 | Aberage speed since the start of the training session in units of 1/100 kilometer per hour. Present if bit 0 of Flags field set to one. Represented values: M = 1, d = -2, b = 0. |
Total Distance | uint24 | 0 or 3 | Total distance covered since the start of the training session in meteres. Present if bit 2 of Flags field is set to 1. |
Inclination | sint16 | 0 or 2 | The current inclination of the device, with a positive value making the user feel like they are going uphill. Unit is 1/10 of a percent Present if bit 3 of Flags field is set to 1. Represented values: M = 1, d = -1, b = 0. |
… | … | … | … |
Let’s break this down.
The Treadmill Data will be a structure that contains multiple bytes of data. The first two bytes (octets) contain 16 boolean flags indicating which fields will be present in the structure. This is a common technique with Bluetooth values. It allows the device to send only the data that’s needed, saving bandwidth. Following that, the structure may contain the Instantaneous Speed, Average Speed, and Total Distance fields. Whether or not these fields are present is defined by the state of specific flags.
The instant speed is a 16-bit unsigned integer (uint16). If it is present, it will be two bytes in little Endian format. That means the least significant byte (LSB) is sent first. For example, 0x1234
would be serialized as 0x34 0x12
. For Swift, this is equivalent to using a Uint16
. The inclination field is similar. It uses a signed, 16-bit integer (Int16
). So far, nothing too complicated. Total distance uses an unsigned 24-bit integer. Similar to the other types, the LSB is sent first. That means 0x123456 would be serialized as 0x56 0x34 0x12
. Unfortuantely, Swift does not have a native type for this. That will be one of the first challenges to overcome.
That leaves one more important piece – the “represented values”. This detail is important for interpreting the raw data. The values represent a multipler (M), decimal exponent (d), and binary exponent (b). When these are provided, the serialized value requires a mathematical operation to convert it to the actual value. The formula is:
As an example, let’s look at Instaneous Speed (measured in km/h). We’ll assume the serialized value is 0xaf 0x01
. This is 0x01af
in little endian format, or decimal 431. The represented values are M = 1
, d = -2
, and b = 0
. Plugging these into the formula, we get:
So, the final value is 4.31
kilometers per hour.
That’s enough theory for now. Next, we will put the knowledge into practice and start to build the application.