Bluetooth Low Energy is a powerful technology, but not always the easiest to understand and use effectively. It is not like classic Bluetooth where you have a predefined set of official profiles to choose from; although there are predefined (a.k.a. "adopted") profiles specified by the Bluetooth SIG, these are just the tip of the iceberg, a small subset of the kind of functionality you can achieve with BLE.
In many (or even most) cases, the best option is to create your own custom profile(s) for your application. This gives you ultimate flexibility, and it doesn't even cost anything. In fact, it can be even easier than using one of the adopted profiles, since you get to define exactly how everything works together rather than conforming your application into something that is already defined. Also, since there is no official generic "serial port profile" in the BLE world like SPP in classic Bluetooth, sometimes a custom implementation is the only option to do what you need.
In order to do this effectively, it is important to understand how a BLE connection works, what roles are played by the devices involved, and how exactly data is transferred from one device to the other over the air. Many terms are used, and they are usually not interchangeable: master, slave, central, peripheral, client, server, advertise, scan, read, write, notify, and indicate all mean different things. Knowing the difference will make it easier to describe and build your BLE application.
Here's what this article covers:
One important concept in BLE connectivity is the difference between a master device and a slave device. What do each of these terms imply? First, realize that they are not interchangeable with client/server, which will be explained below. Actually, in the BLE world, the master/slave difference is very easy to define and recognize:
These pairs of terms are the only ones in the list above which are actually interchangeable. References to a "master" or "central" device are describing the same thing, and references to a "slave" or "peripheral" device are also each describing the same thing. In the CoreBluetooth APIs provided by Apple for iOS development, the "Central" and "Peripheral" nomenclature is used; you will not generally see references to master or slave devices in this context.
One important distinction between the master and slave device in a BLE network is that a slave may only be connected to a single master, but a master may be connected to multiple slaves. The BLE specification does not limit the number of slaves a master may connect to, but there is always a practical limitation, especially on small embedded modules. Our v1.1 BLE stack can support up to 8 simultaneous connections as a master device, when properly configured.
The connection role--whether a device is a master or slave--is defined the moment the connection is made. Our stack is capable of acting either as a master or as a slave device. If a device is operating as a slave, it needs to advertise (accomplished in our BLE stack with the gap_set_mode command); if it is operating as a master, it will optionally scan for devices (accomplished in our stack with gap_discover) and initiate a connection request to another device (accomplished in our stack with gap_connect_direct).
Another important concept in a BLE design is the difference between a GATT server and a GATT client. These roles are not mutually exclusive, though typically your device will only be one or the other. Which role(s) your device takes depends on how you need it to work. Here is a basic summary of each kind of functionality:
Unlike the master/slave distinction defined previously, it is easy to see that one device might actually be both of these things at the same time, based on how your application defines the data structure and flow for each side of the connection. While it is most common for the slave (peripheral) device to be the GATT server and the master (center) device to be the GATT client, this is not required. The GATT functionality of a device is logically separate from the master/slave role. The master/slave roles control how the BLE radio connection is managed, and the client/server roles are dictated by the storage and flow of data.
Most of our example projects in the SDK archive and online implement slave (peripheral) devices designed to be GATT servers. These are easy to test with many of the freely available scanning apps for the iOS platform, and upcoming apps for the Android platform now that official BLE APIs have been implemented in Android 4.3. However, we do have a few examples which implement the master (central) end of the connection, designed to be GATT clients. These include the "thermometer-demo" project in the /src folder of the SDK archive, and this BGScript-based Health Thermometer Collector.
There are also some 3rd-party unofficial BLE master examples available for Python and Visual C# .NET designed to control our modules externally using the BGAPI protocol (see this KB article for more info).
Now that we have established the difference between a master and slave device, and a GATT client and GATT server, how do we actually define the GATT structure, and how to we use it to move data from Device A to Device B, whether that means server-to-client or client-to-server? Both directions are easily possible, of course.
In BLE projects built using our SDK, the GATT structure is defined in the "gatt.xml" file that is part of your project's source files. The structure of this file is documented in the Profile Development Kit Developer Guide, available on the BLE112 and BLE113 pages of our Tech Forum. The structure and flow of data is always defined on the GATT server, and the client simply gets to make use of whatever is exposed by the server.
A GATT database implements one or more profiles, and each profile is made up of one or more services, and each service is made up of one or more characteristics. For example, in outline form:
You can implement as many profiles, services, and characteristics as you need. These may be entirely customized, in which case they would use your own 128-bit UUIDs generated at a site such as guidgenerator.com. Or, you can create a project which implements adopted specifications by referencing the Bluetooth SIG's online definitions of profiles, services, and characteristics. (If you do this, it usually makes the most sense to start at the profile level and drill down from there, since the full list of characteristics includes many things that will undoubtedly be irrelevant to your design.) You can combine these two as well, using some adopted profiles/services and some of your own proprietary ones.
Every BLE device acting as a GATT server must implement the official Generic Access service. This includes two mandatory characteristics: Device Name and Appearance.These are similar to the Friendly Name and Class of Device values used by classic Bluetooth. Here is an example definition of an absolutely minimal GATT definition using our gatt.xml format and providing the Generic Access service entries:
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <!-- 1800: org.bluetooth.service.generic_access --> <service uuid="1800" id="generic_access"> <description>Generic Access</description> <!-- 2A00: org.bluetooth.characteristic.gap.device_name --> <characteristic uuid="2A00" id="c_device_name"> <description>Device Name</description> <properties read="true" const="true" /> <value>My Peripheral BLE Device</value> </characteristic> <!-- 2A01: org.bluetooth.characteristic.gap.appearance --> <characteristic uuid="2A01" id="c_appearance"> <description>Appearance</description> <properties read="true" const="true" /> <value type="hex">0100</value> </characteristic> </service> </configuration>
This alone is enough to work with, but it won't let you do anything useful other than tell the GATT client who and what you are. We still need to explore basic data transfer methods that we can use. First though, one minor clarification between attributes and characteristics.
You may occasionally hear or see the terms "attribute" and "characteristic" used interchangeably, and while this isn't entirely wrong, it isn't totally accurate and it can be confusing. Remember that a service is made up of one or more characteristics. However, one single characteristic--generally the most specific level down to which we define our GATT structure--may be comprised of many different attributes.
Each attribute is given a unique numerical handle which the GATT client may use to reference, access, and modify it. Every characteristic has one main attribute which allows access to the actual value stored in the database for that characteristic. When you read about "writing a value to this characteristic" or "reading that characteristic's value," the read/write operations are really being done to the main data attribute.
Some other related attributes are read-only (such as a Characteristic User Description attribute), some control the behavior of the characteristic (such as the Client Characteristic Configuration attribute which is used to enable notify or indicate operations). Our BLE stack and SDK tools generate these as necessary based on the contents of your project's gatt.xml file.
Every attribute has a UUID. These may be either 16 bits (e.g. "180a") or 128 bits (e.g. "e7add780-b042-4876-aae1-112855353cc1"). All 16-bit UUIDs are defined by the Bluetooth SIG and are known as adopted UUIDs. All 128-bit UUIDs are custom and may be used for any purpose without approval from the Bluetooth SIG. Two very common 16-bit UUIDs that you will see are 2901, the Characteristic User Description attribute (defined in gatt.xml with the <description> tag), and 2902, the Client Characteristic Configuration attribute (created by our SDK when either "notify" or "indicate" is enabled on a characteristic).
One important note is that some attribute UUIDs do not technically need to be unique. Their handles are always unique, but the UUIDs occasionally overlap. For example, every Client Characteristic Configuration attribute has a UUID of 0x2902, even though there may be a dozen of them in a single GATT database. You should always give your own custom characteristics fully unique UUIDs, but don't be alarmed if you are testing out your prototype with BLEGUI, you perform a Descriptor Discovery operation, and suddenly see multiple instances of the same UUID for certain things. This is normal.
There are four basic operations for moving data in BLE: read, write, notify, and indicate. The BLE protocol specification requires that the maximum data payload size for these operations is 20 bytes, or in the case of read operations, 22 bytes. BLE is built for low power consumption, for infrequent short-burst data transmissions. Sending lots of data is possible, but usually ends up being less efficient than classic Bluetooth when trying to achieve maximum throughput.
Here are a few general guidelines about what kind of data transfer you will need to use:
One quick note about how the various BGAPI functions are named: if it starts with "attclient_", then it relates to an operation performed on or by the GATT client. If it stars with "attributes_", then it relates to an operation performed on or by the GATT server.
The above four BLE data transfer operations are described here. Commands which you must send are shown separately from the events which the stack generates:
attclient_read_by_handlecommand - Reads a remote attribute's value with the given handle. This can be used to read attributes up to 22 bytes in length. Read operations requested by the client are the only ones which can request this much data in one packet; other operations (write/notify/indicate) are limited to 20 bytes total. This is the most common method used by a GATT client to read individual attribute values.
attclient_read_by_typecommand - Reads the value of each attribute of a given type (UUID) and in a given attribute handle range. For example, you can use this to read all the characteristic declarations (UUID = 0x2803) of a given service after discovering the start/end handle boundaries with the attclient_find_by_group_type command. If you are building a GATT client device for a specific kind of application, you may never need this.
attclient_read_longcommand - Starts a procedure where the client first sends normal read request to the server, and if the server returns an attribute value with a length equal to the BLE MTU (22 bytes), then the client continues to send "read long" requests until rest of the attribute is read. This only applies if you are reading attributes that are longer than 22 bytes. It is often simpler to construct your GATT server such that there are no long attributes, for simplicity. Note that the BLE protocol still requires that data is packetized into max. 22-byte chunks, so using "read long" does not save transmission time.
attclient_read_multiplecommand - Reads multiple attributes from a GATT server. This takes a list of attribute handles for input and requests all of the resulting values back at once in a single response packet. Values are sent back in the order they are requested, with no delineation between different values. Any data beyond the maximum read packet size (22 bytes) will be silently dropped. This also requires that all requested attributes (except optionally the last one) have a known size, since no value delineation is used, and that all values combined fit within 22 bytes or less.
attclient_read_multiple_responseevent - Generated when the above attclient_read_multiple command is used. This event will occur exactly once for the entire "read multiple" request, delivering a maximum of 22 bytes.
attributes_user_read_requestevent - Generated on the GATT server when a client requests reading data from a characteristic whose "type" value is set to "user" in gatt.xml. Normally, this operation is handled by the stack automatically, and the characteristic value stored in RAM right on the module is sent back to the client. But with "user" characteristics, this data storage and retrieval is entirely up to you. The stack simply lets you know when it has been requested, and then you must get it (or generate it), then send it back (or not) in whatever format is desired. This can be useful for immediate I/O status reads, for example, so that instead of maintaining a value in memory or polling all the time so it is kept "fresh," you can instead just wait for the request and read the I/O status on the spot when it comes in. (ONLY applies if type="user" is set on the characteristic's <value> tag in gatt.xml)
attributes_user_read_responsecommand - Used to send back the desired data when an attributes_user_read_request is generated by a client's request on a "user" characteristic. This command takes a result code and the actual value, if any, to send back. (ONLY applies if type="user" is set on the characteristic's <value> tag in gatt.xml)
attclient_attribute_writecommand - Writes a remote attribute's value on a GATT server, up to 20 bytes in length. This performs an acknowledged write operation, so the server will respond when the value has been successfully written. This is the most common method used by a GATT client to write individual attribute values.
attclient_procedure_completedevent - Triggered on the GATT client when the server acknowledges a standard (acknowledged) write operation, performed with the attclient_attribute_write command directly above. This event does not occur when using the unacknowledged attclient_write_command command directly below. This event is useful for controlling program flow based on whether a critical write operation has finished yet or not.
attclient_write_commandcommand - Writes a remote attribute's value on a GATT server, up to 20 bytes in length. This is exactly the same as the above command except that it is unacknowledged. There will be no response upon completion. Like the "notify" operation, this means that it is faster (multiple unacknowledged writes may be performed within a single connection interval), but less reliable than acknowledged writes.
attclient_prepare_writecommand - Queue a particular write operation for execution later as part of a group of writes. It is effectively the same as a single write operation, but not executed immediately. You can queue only up to 18 bytes at a time to be written, and you specify the offset as well as the data. The attclient_procedure_completed event occurs when the preparation is done. This only necessary when you need to write more than 20 bytes to a single attribute, since it gives you the ability to write at a certain offset. You have to break up the write operation into 18-byte (or smaller) chunks.
attclient_prepare_write(0, 10, 0, 18, "abcdefghijklmnopqr")
attclient_prepare_write(0, 10, 18, 12, "stuvwxyz1234")
attclient_execute_writecommand - Executes (argument=1) or cancels (argument=0) any pending write operations queued with attclient_prepare_write. The stack handles sending the queued writes in multiple packets automatically when you run this command, and it will take as many connection intervals as necessary. The "attclient_procedure_completed" event occurs when the execution is done. On the server side, this looks like a rapid series of write operations, and is handled the same way (acknowledging each one).
attributes_valueevent - Triggered on the GATT server when the client writes a new value to a particular characteristic. This is the event you can use to tell when an characteristic has been updated, and then respond accordingly (parse the new value for command data, set I/O pins based on the stored data, close the connection, begin reading ADC measurements, etc.). Note that if type="user" is set on the characteristic's <value> tag in gatt.xml, then the new written value from the client will be provided as one of the parameters of this event, but the value itself will not be stored in RAM automatically by the stack. It is up to you to store or otherwise process the value, and acknowledge as necessary.
attributes_user_write_responsecommand - Used to send back an appropriate acknowledgement result code when an attributes_value event is generated by a client's write operation on a "user" characteristic. If this manual acknowledgement is necessary, then you should typically call this command in the same code as the event handler for the related attributes_value event. (ONLY applies if type="user" is set on the characteristic's <value> tag in gatt.xml)
attributes_writecommand - Writes a local attribute's value on the GATT server, up to 255 bytes in length (BGAPI internal parser limits this to 64 bytes at a time, though offsets may be used). This command alone only affects the data stored locally, but if the GATT client has subscribed to notifications, then this also triggers pushing the data to the remote GATT client. This is the command you will use to push data once notifications (or indications, see below) have been enabled by the client.
attributes_statusevent - Triggered on the GATT server when the remote GATT client changes the notification (or indication) subscription status. If the updated status equals 1, then notifications are enabled. If it equals 2, then indications are enabled. If it equals 0, then neither are enabled. It is technically possible to use a value of 3 to enable both, but this is not typically useful because GATT servers generally only enable one or the other. (iOS devices will only use notifications if both are available.)
attclient_attribute_valueevent - Triggered on the GATT client when the remote GATT server pushes a new value via notification (or indication). This event is how the client knows it has received an updated value. The pushed data will be made available as part of the event's parameters. Remember that for notifications and indications, only up to 20 bytes may be pushed. It is not possible to push more than 20 bytes.
attributes_writecommand - Writes a local attribute's value on the GATT server, up to 255 bytes in length (BGAPI internal parser limits this to 64 bytes at a time, though offsets may be used). This alone only affects the data stored locally, but if the GATT client has subscribed to indications, then this also triggers pushing the data to the remote GATT client. This is the command you will use to push data once indications (or notifications, see above) have been enabled by the client. Remember that indications are acknowledged, so you may do this only once within a given connection interval. It will be followed up by the attclient_indicated event when the acknowledgement comes back from the GATT client.
attributes_statusevent - Triggered on the GATT server when the remote GATT client changes the indication (or notification) subscription status. If the updated status equals 1, then notifications are enabled. If it equals 2, then indications are enabled. If it equals 0, then neither are enabled. It is technically possible to use a value of 3 to enable both, but this is not typically useful because GATT servers generally only enable one or the other. (iOS devices will only use notifications if both are available.)
attclient_attribute_valueevent - Triggered on the GATT client when the remote GATT server pushes a new value via indication (or notification). This event is how the client knows it has received an updated value. The pushed data will be made available as part of the event's parameters. Remember that for notifications and indications, only up to 20 bytes may be pushed. It is not possible to push more than 20 bytes.
attclient_indicate_confirmcommand - Sends an indication confirmation from the GATT client back to the remote GATT server, after an indicated value has arrived at the client (with an attclient_attribute_value event). Normally, this is handled automatically by the BLE stack running on the module. However, it is possible to configure this to require a manual command, using the <manual_confirm> tag in your project's config.xml file. Why? Sometimes there are specific data flow control triggers you need to deal with very carefully, such as in the "cable_replacement" example project in our BLE SDK archive. Controlling precisely when the confirmation occurs and what happens before and after it may be valuable.
attclient_indicatedevent - Triggered on the GATT server when the remote GATT client confirms having received the data previously pushed via an indication. Very often, this event is ignored, but as mentioned in the previous item, sometimes it is valuable to know precisely when this occurs for data flow control purposes (as in the "cable_replacement" example project).
Typically, the GATT server functionality is provided by one side and the client functionality is entirely on the other side, but it could be that both sides provide both kinds of functionality. This does not usually pose any advantages for a well-designed and efficient BLE project—it usually complicates the implementation needlessly—so we will not go into it here.
Let us consider the very typical example case of the Bluegiga BLE112 module being used as a peripheral device, meant to be controlled from a BLE-enabled smartphone such as an iPhone 5 or Android 4.3+ device. Further, we will say that the following is true about the application:
This gives us enough information to plan out and then build the GATT structure:
Now that we have this basic plan, we should consider the easiest way to accomplish it. Can we reuse anything that has already been defined by the Bluetooth SIG? There isn't any "General IO Pin" service, or "General Mode Config" service, so those will need to be custom. However, there is an official Battery service already, which we can (and should) use. This service has a UUID of 0x180F. It has a single 1-byte characteristic which must be readable (for polling the battery level) and may optionally support notifications for pushing battery updates. This will be perfect. (There is an example project in the latest BLE SDK found in the "/example/battery" folder which implements exactly this.)
We will also need to include at least the universal Generic Access service to include the device name and appearance, as well as a custom service for the missing pieces (points #2 and #3 above). For the custom part, we will need a total of three 128-bit UUIDs: one for the service, and two for the two characteristics the service will contain. We can use these UUIDs, freshly minted from guidgenerator.com:
Also, we'll want to make our custom service UUID advertised automatically by the BLE stack. This will make it easy to filter while scanning on the smartphone so that only peripheral devices which include that unique ID will appear in the scan results. This is done by adding ' advertise="true" ' to the <service> tag in our GATT definition.
Our final gatt.xml content to include all of these things will look like this:
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <!-- 1800: org.bluetooth.service.generic_access --> <service uuid="1800" id="s_generic_access"> <description>Generic Access</description> <!-- 2A00: org.bluetooth.characteristic.gap.device_name --> <characteristic uuid="2A00" id="c_device_name"> <description>Device Name</description> <properties read="true" const="true" /> <value>My BLE Device</value> </characteristic> <!-- 2A01: org.bluetooth.characteristic.gap.appearance --> <characteristic uuid="2A01" id="c_appearance"> <description>Appearance</description> <properties read="true" const="true" /> <!-- Generic device, Generic category --> <value type="hex">0000</value> </characteristic> </service> <!-- 180F: org.bluetooth.service.battery_service --> <service uuid="180f" advertise="true"> <description>Battery Service</description> <characteristic uuid="2a19" id="c_battery_status"> <properties read="true" notify="true" /> <value length="1" /> </characteristic> </service> <!-- custom service --> <service uuid="624e957f-cb42-4cd6-bacc-84aeb898f69b" advertise="true"> <description>Custom Device Service</description> <!-- custom write-only characteristic for setting mode --> <characteristic uuid="e4c937b3-7f6d-41f9-b997-40c561f4453b" id="c_mode_config"> <description>Mode Configuration</description> <properties write="true" /> <value length="4" /> </characteristic> <!-- custom read/write characteristic for getting/setting I/O port status --> <characteristic uuid="df342b03-53f9-43b4-acb6-62a63ca0615a" id="c_port_status"> <description>IO Port Status</description> <properties read="true" write="true" /> <value length="3" type="user" /> </characteristic> </service> </configuration>
For a detailed explanation of exactly what all of the different tags here mean, please read through the Profile Developer Toolkit Guide available on the Tech Forum. In this document, you will find definitions of everything used here.