BLE MIDI Device - GATT Example

Bluetooth GATT: How to Design Custom Services & Characteristics [MIDI device use case]

You’re probably aware that adding Bluetooth Low Energy (BLE) to your device is one of the best ways to achieve a great user experience for your IoT device.

Why is that, you may ask?

Well, because adding BLE allows your device to be connectable from a smartphone app. And we can all agree that smartphone apps have become very familiar to end-users and are (usually) very easy to use.

However, designing BLE devices can be a confusing process!

You’re probably thinking:

“Where do I even start?”

The one thing I wish I had when I started developing for BLE is more resources that walk you step-by-step on how to tackle the different phases of the system design and implementation.

The Bluetooth GATT (Generic Attribute Profile) is the foundation for the design of any BLE system and defines the way a smartphone application (or any central device) interacts with the end-device (the peripheral device).

Keep in mind that GATT is used exclusively after a connection has been established between the two devices.

The Bluetooth SIG defines quite a few standard Profiles, Services, and Characteristics.

However, many times you will find that none of these satisfies the use case you’re designing.

That’s where custom profiles, services, and characteristics come in.

Reader João Neves sent in a question asking about implementing a BLE MIDI controller:

I’m trying to be able to send MIDI over Bluetooth… I’m trying to advertise that device as a BLE MIDI controller…

There’s not a lot of info on internet for this subject and I can’t find any for the nRF52 DK.

In today’s tutorial, I’ll be covering a detailed step-by-step guide on how to design your custom GATT to satisfy your product’s requirements and go through a complete design and simplistic implementation of a MIDI device using the nRF52 platform.

Bonus: Download my free report on the Essential Bluetooth Low Energy Tools which help you develop for BLE in the most efficient manner.

Before explaining what services and characteristics are, we first need to cover two very important concepts: the Generic Attribute Profile (GATT) and the Attribute Protocol (ATT).

GATT stands for Generic Attribute Profile. To understand what GATT is, we first need to understand the underlying framework for GATT: the Attribute Protocol (ATT). The GATT only comes into play after a connection has been established between two BLE devices.

Attribute Protocol (ATT)

ATT defines how a server exposes its data to a client and how this data is structured. There are two roles within the ATT:

  • Server:
    This is the device that exposes the data it controls or contains, and possibly some other aspects of server behavior that other devices may be able to control. It is the device that accepts incoming commands from a peer device and sends responses, notifications, and indications.For example, a thermometer device will behave as a server when it exposes the temperature of its surrounding environment, the unit of measurement, its battery level, and possibly the time intervals at which the thermometer reads and records the temperature. It can also notify the client (defined later) when a temperature reading has changed rather than have the client poll for the data waiting for a change to occur.
  • Client:
    This is the device that interfaces with the server with the purpose of reading the server’s exposed data and/or controlling the server’s behavior. It is the device that sends commands and requests and accepts incoming notifications and indications.
 In the previous example, a mobile device that connects to the thermometer and reads its temperature value is acting in the Client role.

The data that the server exposes is structured as attributes. An attribute is the generic term for any type of data exposed by the server and defines the structure of this data. For example, services and characteristics (both described later) are types of attributes. Attributes are made up of the following:

  • Attribute type (Universally Unique Identifier or UUID)
    This is a 16-bit number (in the case of Bluetooth SIG-Adopted Attributes), or 128-bit number (in the case of custom attribute types defined by the developer, also sometimes referred to as vendor-specific UUIDs). For example, the UUID for a SIG-adopted temperature measurement value is 0x2A1C SIG-adopted attribute types (UUIDs) share all but 16 bits of a special 128-bit base UUID:00000000-0000-1000-8000-00805F9B34FBThe published 16-bit UUID value replaces the 2 bytes in bold in the base UUID. A custom UUID, on the other hand, can be any 128-bit number that does not use the SIG-adopted base UUID. For example, a developer can define their own attribute type (UUID) for a temperature reading as: F5A1287E-227D-4C9E-AD2C-11D0FD6ED640.
    One benefit of using a SIG-adopted UUID is the reduced packet size since it can be transmitted as the 16-bit representation instead of the full 128-bit value.
  • Attribute Handle
    This is a 16-bit value that the server assigns to each of its attributes — think of it as an address. This value is used by the client to reference a specific attribute and is guaranteed by the server to uniquely identify the attribute during the life of the connection between two devices. The range of handles is 0x0001-0xFFFF, where the value of 0x0000 is reserved.
  • Attribute Permissions
    Permissions determine whether an attribute can be read or written to, whether it can be notified or indicated, and what security levels are required for each of these operations. These permissions are not defined or discovered via the Attribute Protocol (ATT) but rather defined at a higher layer (GATT layer or Application layer).

The following figure shows a logical representation of an Attribute:

Attribute structure

Figure 1: Attribute structure

The Generic Attribute Profile (GATT)

Now that we’ve covered the concept of attributes, we’ll go over three important concepts in BLE that you will come across very often:

  • Services
  • Characteristics
  • Profiles

These concepts are used specifically to allow hierarchy in the structuring of the data exposed by the Server. Services and characteristics are types of attributes that serve a specific purpose. Characteristics are the lowest level attribute within a database of attributes. Profiles are a bit different and are not discovered on a server — we will explain them later in this chapter.

The GATT defines the format of services and their characteristics, and the procedures that are used to interface with these attributes such as service discovery, characteristic reads, characteristic writes, notifications, and indications.

GATT takes on the same roles as the Attribute Protocol (ATT). The roles are not set per device — rather they are determined per transaction (such as request ⟷ response, indication ⟷ confirmation, notification).

So, in this sense, a device can act as a server serving up data for clients, and at the same time act as a client reading data served up by other servers (all during the same connection).

Services and Characteristics

Services

A service is a grouping of one or more attributes, some of which are characteristics. It’s meant to group together related attributes that satisfy a specific functionality on the server. For example, the SIG-adopted Battery Service contains one characteristic called the Battery Level.

A service also contains other attributes (non-characteristics) that help structure the data within a service (such as service declarations, characteristic declarations, and others).

Here’s what a service looks like:

GATT Profile, Services, Characteristics Hierarchy

Figure 2: Profiles, Services, and Characteristics
(Source: Bluetooth 5 specification document)

From the figure, we can see the different attributes that a service is made up of:

  • One or more include services
  • One or more characteristics
    • Characteristic properties
    • A characteristic value
    • Zero or more characteristic descriptors

An include service allows a service to refer to other services for purposes such as extending the included service. There are two types of services:

  • Primary Service: represents the primary functionality of a device.
  • Secondary Service: provides the auxiliary functionality of a device and is referenced (included) by at least one other primary service on the device (it is rarely used and won’t be discussed here).

Characteristics

A characteristic is always part of a service and it represents a piece of information/data that a server wants to expose to a client. For example, the battery level characteristic represents the remaining power level of a battery in a device which can be read by a client. The characteristic contains other attributes that help define the value it holds:

  • Properties: represented by a number of bits and which defines how a characteristic value can be used. Some examples include: read, write, write without response, notify, indicate.
  • Descriptors: used to contain related information about the characteristic Value. Some examples include: extended properties, user description, fields used for subscribing to notifications and indications, and a field that defines the presentation of the value such as the format and the unit of the value.

Understanding these concepts is important, however, as an application developer you’ll probably interface with APIs provided by the chipset or mobile operating system SDK that abstract out many of these concepts.

For example, you may have an API for enabling notifications on a certain characteristic that you can simply call (you don’t necessarily need to know that the stack ends up writing a value of 0x0001 to the characteristic’s Client Characteristic Configuration Descriptor (CCCD) on a server to enable notifications).

It’s important to keep in mind that while there are no restrictions or limitations on the characteristics contained within a service, services are meant to group together related characteristics that define a specific functionality within a device.

For example, even though it’s technically possible — it does not make sense to create a service called the humidity service that includes both a humidity characteristic and a temperature characteristic. Instead, it would make more sense to have two separate services specific to each of these two distinct functionalities (temperature reading, and humidity reading).

It’s worth mentioning that the Bluetooth SIG has adopted quite a few services and characteristics that satisfy a good number of common use cases. For these adopted services, specification documents exist to help developers implement them along with ensuring conformance and interoperability with this service.

If a device claims conformance to a service, it must be implemented according to the service specification published by the Bluetooth SIG. This is essential if you want to develop a device that is guaranteed to be connectable with third-party devices from other vendors. The Bluetooth SIG-adopted services make the connection specification “pre-negotiated” between different vendors.

You can find the list of adopted services here and their respective specifications here. Adopted characteristics can be found here.

Profiles

Profiles are much broader in definition than services. They are concerned with defining the behavior of both the client and server when it comes to services, characteristics and even connections and security requirements. Services and their specifications, on the other hand, deal with the implementation of these services and characteristics on the server side only.

Just like in the case of services, there are also SIG-adopted profiles that have published specifications. In a profile specification, you will generally find the following:

  • Definition of roles and the relationship between the GATT server and client.
  • Required Services.
  • Service requirements.
  • How the required services and characteristics are used.
  • Details of connection establishment requirements including advertising and connection parameters.
  • Security considerations.

Following is an example of a diagram taken from the Blood Pressure Profile specification document. It shows the relationship between the roles (server, client), services, and characteristics within the profile.

Blood Pressure Profile

Figure 3: Blood Pressure Profile
(Source: Blood Pressure Profile Specification)

The roles are represented by the yellow boxes, whereas the services are represented by the orange boxes. You can find the list of SIG-adopted profiles here.

Example GATT

Let’s look at an example of a GATT implementation. For this example, we’ll look at an example GATT.xml file that’s used by the Silicon Labs Bluetooth Low Energy development framework (BGLib).

GATT xml example

Figure 4: GATT XML example

In this XML, you’ll notice the following:

  • There are two services defined:
    • Generic Access Profile (GAP) service with UUID: 0x1800 (SIG-adopted service).
    • Cable Replacement service with UUID: 0bd51666-e7cb-469b-8e4d-2742f1ba77cc (a custom or vendor-specific service).
  • The Generic Access Profile service is mandatory per the spec, and it includes the following mandatory characteristics:
    • Name with UUID 0x2a00 and value: Bluegiga CR Demo.
    • Appearance with UUID 0x2a01 and value 0x4142.
      Appearance value definitions can be found here.
      Note: the creation and inclusion of this Service are usually handled by the chipset’s SDK, and usually APIs are provided to simply set the Name and Appearance values.
  • The Cable Replacement service has one characteristic named data
    • The data characteristic has a UUID: e7add780-b042-4876-aae1-112855353cc1
    • It has both writes and indications enabled.

GATT Design Guidelines

While GATT is a pretty flexible framework, there are a few general guidelines to follow when designing it and creating the services and characteristics within it. Following are some recommendations:

  • Make sure to implement the following mandatory service and its characteristics:
    • Generic Access Profile (GAP) service.
    • Name and Appearance characteristics within the GAP service.
  • One thing to keep in mind is that vendor SDKs usually do not require you to explicitly implement this service, but rather they provide APIs for setting the name and appearance. The SDK then handles creating the GAP service and setting the characteristics according to the user-provided values.
  • Utilize the Bluetooth SIG-adopted profiles, services, and characteristics in your design whenever possible. This has the following benefits:
    • You get the benefit of reducing the size of data packets involving UUIDs for services and characteristics (including advertisement packets, discovery procedures, and others) — since 16-bit UUID values are used instead of 128-bit values.
    • Bluetooth chipset and module vendors usually provide implementations of these profiles, services, and characteristics in their SDKs — reducing development time considerably.
    • Interoperability with other third-party devices and applications, allowing more devices to interface with your device and provide a richer user experience.
  • Group characteristics that serve related functionality within a single service.
  • Avoid having services with too many characteristics. A good separation of services makes it faster to discover certain characteristics and leads to a better GATT design that’s modular and user-friendly.

Step 1: Document the different user scenarios and data points

Even though the GATT is usually more focused on the peripheral role (since a peripheral is usually the server exposing the data), the central can still act as the server in some cases for specific data points it needs to expose. Also, if you’re designing both ends (central and peripheral), it helps to think in terms of what needs to happen from each side since this could affect some aspects of the system and GATT design.

In this step, you’ll think about your system from a high-level in terms of data elements that need to be exposed on the Server device. It helps to not think too much about the technical aspects of BLE.

Instead, focus on the following:

  • Defining the data elements that the server needs to expose to the Client.
  • Defining whether each of these elements will be available for: Read, Write, and Notifications of value changes back to the Client.
  • Thinking about eliminating any redundant data elements. This is important for two reasons:
    • It reduces the amount of data being transferred, which in turn reduces power consumption.
    • It makes the design simpler and easier to understand and update later on.
  • Grouping the data elements into a meaningful number of groups based on related functionality. This encourages clarity in design and also helps others (within your team or outside) understand your design. It also helps in making maintenance and future updates easier.

Step 2: Define the services, characteristics, and access permissions

The next step is to group the characteristics into meaningful groups (services) based on their functionalities and define the access permissions for each of these characteristics.

Step 3: Re-use Bluetooth SIG-adopted services & characteristics

Take a look at the data elements and data groups you brainstormed in the previous step. Now refer to the standard Services and Characteristics and see which ones match the data elements you came up within the design. This is not mandatory per the Bluetooth spec since users are given the freedom to create custom Services and Characteristics. However, there are two benefits to this approach:

  • Allowing your device to be interoperable with other devices.
  • Vendors usually provide many examples that utilize the standard Services and Characteristics, which makes your development work easier.

Step 4: Assign UUIDs to Custom Services and Characteristics

For any custom services and characteristics within the GATT, we can use an online tool to generate UUIDs such as the Online GUID Generator.

A common practice is to choose a base UUID for the custom service and then increment the 3rd and 4th Most Significant Bytes (MSB) within the UUID of each included characteristic.

For example, we could choose the UUID:

00000001-1000-2000-3000-111122223333

for a specific service and then

0000000[N]-1000-2000-3000-111122223333, (where N > 1) for each of its characteristics.

So, in this case, the service and its characteristics’ UUIDs would look something like this:

Service A: 00000001-1000-2000-3000-111122223333

|—- Characteristic 1: 00000002-1000-2000-3000-111122223333

|—- Characteristic 2: 00000003-1000-2000-3000-111122223333

|….

|—- Characteristic n: 0000000[n-1]-1000-2000-3000-111122223333

The only restriction for choosing UUIDs for custom services and characteristics is that they must not collide with the Bluetooth SIG base UUID:

XXXXXXXX-0000-1000-8000-00805F9B34FB

You could also randomly choose a unique UUID for each element (including services and characteristics). However, following the previously mentioned common practice makes it a bit easier to relate services and their characteristics to one another.

For a step-by-step guide on how to do to choose a single UUID, refer to my blog post: How do I choose a UUID for my custom services and characteristics?

Step 5: Implement your GATT using the framework and APIs provided by the BLE solution vendor

The solutions provided by the different vendors vary widely, so this step will be specific to the BLE module and development framework that you end up choosing. I’ll be using the nRF52 APIs in the next section which goes over a full design and implementation example.

Example using nRF52 Development Kit [MIDI device use case]

Full source code is available for download at the bottom of the post. Click here

Let’s take a look at the definitions for services and characteristics within the MIDI BLE Specification (found here: BLE MIDI Specification):

BLE MIDI Services and Characteristics

Figure 5: BLE MIDI Services and Characteristics

We notice that there is one service and one characteristic defined:

  • MIDI Service (UUID: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700)
    • MIDI Data I/O Characteristic (UUID: 7772E5DB-3868-4112-A1A9-F2669D106BF3)
      • write (encryption recommended, write without response is required)
      • read (encryption recommended, respond with no payload)
      • notify (encryption recommended)

Since the services and characteristics for a standardized MIDI device are already defined, we can simply skip all the steps except for step #5 where we implement it for the nRF52 development kit.

Then, upon a successful implementation, you will be able to scan for the device, connect it, discover its Services and Characteristics, and verify that the GATT structure matches the MIDI spec.

Before you follow along with the implementation, make sure you have:

  • A nRF52840 development kit (the nRF52832 could be used instead, but the companion source code will have to be modified to be compatible with this chipset. Alternatively, you could follow the steps and modify the project example for the nRF52832 chipset).
  • The development environment set up and ready to interface with the development kit.

Important Note: Make sure you are using SDK version 15.0.0 to run this example.
You can download it from here: https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/

To get the environment set up, follow my tutorial titled The complete cross-platform nRF development tutorial.

The steps we will go through:

  1. Use the ble_app_template example provided in the nRF52 SDK (version 15.0.0 in our case).
  2. Open the project in Segger Embedded Studio and add the necessary files to the project.
  3. Modify the example to add the MIDI Service and Characteristic (according to the spec).
  4. Compile the code and flash it to the nRF52840 development kit.
  5. Scan for the device via a Client emulator app such as the Nordic nRF Connect App (iOS, Android, or desktop).
  6. Connect to the device and discover the Services and Characteristics.
  7. Verify that the GATT matches the MIDI spec.

Let’s get started!

Step 1: Setup

If you haven’t already done so, you will want to follow the tutorial step-by-step to get your development and debugging environment set up:

The complete cross-platform nRF development tutorial

Note: Make sure you use SDK version 15.0.0 to run this example (available to download from here: https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/).

Step 2: Project Setup

Once you have SES (Segger Embedded Studio) installed, open the example project located at  <nRF SDK>/examples/ble_peripheral/ble_app_template.

Template Project

Figure 6: BLE Peripheral Template Example

Now that you have the project open, we’ll take the following steps:

  • Enable Debug messages to show up in the debug terminal (using the Segger RTT backend).
  • Modify the vendor-specific UUID count to 2 (one for the MIDI service, and one for the MIDI Data I/O characteristic).
  • Add the midi_service.h and midi_service.c files to the project under the Application folder.
  • Make any necessary changes to the section placement macros in the project.

Let’s go through each of these steps.

  • Enable Debug messages to show up in the debug terminal (using the Segger RTT backend).
    First, right-click on the sdk_config.h file under the Applications folder, and click on CMSIS Configuration Wizard.

    CMSIS Configuration Wizard

    Figure 7: CMSIS Configuration Wizard

    Navigate to nRF_Log –> NRF_LOG_BACKEND_RTT_ENABLED and make sure it is checked [✓].

    NRF LOG RTT BACKEND

    Figure 8: Configuring RTT Backend for nRF Logging

    Make sure you hit the save button (File icon).

  • Modify the vendor-specific UUID count to 2 (one for the MIDI service, and one for the MIDI Data I/O characteristic).
    Right-click on the sdk_config.h file under the Applications folder, and click on CMSIS Configuration Wizard.
    Navigate to nRF_SoftDevice –> NRF_SDH_BLE_ENABLED –> BLE Stack configuration –> NRF_SDH_BLE_VS_UUID_COUNT, and make sure it is set to 2.
    Make sure you hit the save button (File icon).
    Vendor Specific UUID CountFigure 9: Setting the Vendor-Specific UUID Count value
  • Add the midi_service.h and midi_service.c files to the project under the Application folder.
    Right-click on the Applications folder, then click “Add New File…

    Adding a New File in SES

    Figure 10: Adding a New File

    Choose C File (.c) and type midi_service in the Name field. Also, make sure the location is appropriate (I suggest putting it in the root folder of the project).
    Repeat the same steps for midi_service.h (but choosing Header File (.h) instead).

  • Make any necessary changes to the section placement macros in the project.
    To make sure the section placement macros values are set correctly, compile and flash the project (in Debug mode) to the development kit. If the macros need adjusting, a debug message will be displayed prompting you to use different values:

    RAM Location and Size

    Figure 11: Debug message showing correct RAM location and size

    If you get the debug message, copy the values for RAM start location and Maximum RAM size and adjust the macros accordingly.
    You can do so by right-clicking on the Project (not Solution), then click on Options:

    Project Options

    Figure 12: Project Options

    Then choose Common from the configuration drop-down menu

    Common Configuration

    Figure 13: Common Project Configuration

    Next, choose Linker under Code:

    Linker Configuration

    Figure 14: Linker Configuration

    On the right-hand side, double-click on Section Placement Macros

    Section Placement Macros

    Figure 15: Section Placement Macros

    Now, modify the macros RAM_START and RAM_SIZE to match the values displayed in the debug message.

Step 3: Source Code Modifications

Now that we have made all the project modifications necessary, we’re ready to write the code for the MIDI service (and its characteristic) and the code needed to initialize them in main.c.

midi_service.h

The header file will define the data structures and function prototypes needed to implement the MIDI service.

Let’s go through implementing it line by line.

  • Add the conditional macro and the necessary #include files:
  •  Add the macro used for instantiating and defining the service object:
  • Now, we add the macros that define the UUIDs for the service and characteristic. These values are defined in the BLE MIDI spec, so we don’t have to come up with our own values. Following the spec, we define the following:
  • Define the different MIDI event types and a data structure to represent a MIDI event.
  • Forward-declare the MIDI service data structure:
  • Define the MIDI Data I/O write handler function:
  • Define the init data structure that is used to configure a callback function that can be called back to the main application:
  • Define the main MIDI Service data structure that contains the service handle, any characteristic handles, the UUID type, connection handle, and more:
  • Define the function prototype for the initializer function. This function gets called from the main application to initialize the service:
  • Define the BLE event handler for the MIDI service. This will get called by the stack whenever a BLE event is reported and may need to be processed by the MIDI service:
  • Close the conditional macro for the header file:

midi_service.c

The source .c file will handle the implementation of the service including initialization, the creation of services and characteristics, as well as event handling.

Let’s go through implementing it line-by-line.

  • Add the necessary #include for header files:
  • Implement the function that handles the connection event. The function simply sets and stores the connection handle:
  • Implement the function that handles the disconnection event. The function simply resets the connection handle associated with the service:
  • Implement the write handler function that processes the write event and verifies that it’s valid. The implementation/processing is left to the developer.
    It also handles the case where notifications are enabled/disabled and passes this event back up to the main application (via the event handler that was set in the initialization function).
  • Implement the BLE event handler function.
    This function gets called automatically by the SoftDevice and is set up by the BLE_MIDI_DEF macro we defined at the top of the  midi_service.h file.
    It processes the BLE event and hands it off to the appropriate function to be processed. It handles the connection event, the disconnection event, and the write event:
  • Now, we’ll implement the function for creating and adding the data I/O characteristic as part of the MIDI service.
    • Define the function prototype:
    • Define variables for the error code (err_code), GATT characteristic metadata (char_md), GATT client characteristic configuration descriptor (CCCD) metadata (cccd_md), attribute metadata (att_md), characteristic value attribute (attr_char_value), and UUID (ble_uuid):
    • Configure the CCCD needed for enabling notifications and indications (only notifications are needed in our case, according to the BLE MIDI spec).
      We make sure reads and writes are enabled.
      Also, we define the location of the attribute to be put on the stack RAM instead of the user RAM.
    • Next, we configure the characteristic metadata.
      This includes characteristic properties that are exposed to the Central device when discovering the characteristic.
      We first clear the variable, then enable writes with no response, reads, and notifications.
      Finally, we clear any unnecessary properties and we make sure we assign the CCCD data pointer to our local CCCD metadata variable (cccd_md) that we set up in the previous step
    • Now it’s time to add the UUID of the characteristic to the database of UUIDs in the stack.
      Using the Nordic nRF5 SDK, the way you define a UUID is by defining a base UUID (128 bits) and an “offset” UUID which replaces the 3rd and 4th most significant bytes within the base UUID. When defining the base UUID, make sure you clear/zero-out the 3rd and 4th Most Significant Bytes (MSB). You can learn more about this here.
      For our MIDI Data I/O characteristic, we already have the UUID defined per the BLE MIDI spec, so we know the base and the offset. Below is the code to add this characteristic.
      Notice that before we store the UUID in the ble_uuid data structure, we add it as a vendor-specific UUID to the stack’s UUID database using the sd_ble_uuid_vs_add() API.
      After that’s done, we have a type that’s assigned to us by the stack (stored in p_midi_service->uuid_type).
    • Now we can configure the characteristic value’s metadata (attr_md).
      We configure the permissions to match the characteristic properties we defined previously (both reads and writes enabled).
      Enabling notifications and indications is specified by the CCCD which we already set up previously.
    •  Before we actually add the characteristic using the API, we have to configure the characteristic value (which is what holds the value we read from the peer BLE device).
      Here we assign the UUID, the attribute metadata, the size and maximum length of the value.
      Note: To implement according to the MIDI spec correctly, you’ll probably have to change the max length of the characteristic value. For simplicity, we’ll make the value 1 byte in our example.
    • We can now add the characteristic to the GATT database managed by the stack.
      We do so by calling the following API. The API takes four parameters: the service handle, characteristic metadata, characteristic value, and a pointer to the structure where the assigned handles will be stored.
  • Next, we will write the service initializer function: ble_midi_service_init().

    • We define the prototype for the function to take two parameters: a pointer to the MIDI service data structure, and a pointer to the MIDI initializer data structure (which holds parameters that the main application can pass to configure the service — only a callback function for data I/O writes in our case).
      We also define an error code variable and a UUID variable needed for creating the UUID for the service.
    • Now we initialize some parameters: the connection handle and the application’s data I/O write handler function:
    • Next, we define the service’s UUID. This is identical to the process for the data I/O characteristic we went through previously.
    • Now that we’ve defined the UUID, we can add the service to the GATT database. We define it as a Primary Service, pass its assigned UUID, and a pointer to a data structure to hold the assigned handle for the service.
    • Now that we’ve added the service, we can call our function to add the characteristic:
    • Finally, we return NRF_SUCCESS and close the function:
  • The last function we want to implement is one to handle updating the data I/O sending notifications to the client whenever the data I/O characteristic value is updated.

    •  First, we check the passed-in MIDI service pointer:
    • Next, we set up a GATT server value variable which is needed to pass to the GATT database to store the characteristic value.
      Note again that for simplicity, we set up our value to be 1 byte in length. The length here has to match the length that we set up in the function to add the characteristic to the database.
    •  Now that we’ve created and set the value variable, we can store its updated value:
    • Finally, we want to check if the client has enabled notifications and if so, then send the updated value. This is done with the following code:

main.c

So far, we’ve written the code to implement the MIDI service (both header (.h) and source (.c) files), but we haven’t modified the main application to initialize the service. We also need to make some other modifications to the main.c.

Let’s go through each of them.

  • We first need to #include the header file for the MIDI service.
  • Next, we want to modify the advertised Device Name to indicate this is the MIDI example.
  • We instantiate the MIDI Service via the macro we defined in midi_service.h (highlighted line below):
  • We can delete the following block of code:
  • Instead, we replace it with a function to handle MIDI events in our main application:
  • Next, let’s modify the services_init() function to initialize the MIDI service:
  • The final change to main.c is to change the debug output for the name of the example (in the main() function):

Step 4: Build and flash the updated project

Now that we’ve made all the source code changes necessary, it’s time to build the project and flash it to the development board.

Compile and flash the updated project.

Refer to the previous The complete cross-platform nRF development tutorial for step-by-step instructions on how to do this.

Note 1: Sometimes you may want to clean and rebuild the project from scratch to make sure you don’t have any stale code. You can do so by right-clicking the Project and then selecting the Clean option.

Note 2: If you run into issues and your application does not run. you may want to perform an Erase All to the development board.
You can do this by navigating to Target –> Connect J-Link.

Connect J-Link

Figure 16: Connect J-Link option

Once it’s connected, click on Target –> Erase All.

Erase All

Figure 17: Erase All option

Now, flash the project to the board.

 

Testing

Steps 5 & 6:

Run the nRF Connect or LightBlue app on your smartphone and start a Scan:
nRF Connect Scanning

 

Locate and connect to the device named Nordic_MIDI, and verify that the Services listed match the following:

MIDI Service

Click on the Unknown Service (which matches the MIDI Service), and verify that the Characteristic matches the following.
Also, notice the properties for the characteristic and that they match what we defined in the nRF application: read, write without response, and notify.

MIDI Data I/O Characteristic

That’s it! You have just implemented a custom device with a custom GATT that matches the MIDI spec!

Summary & Conclusion

In this tutorial we covered the following:

  • Attribute Protocol (ATT) and the Generic Attribute Profile (GATT).
  • Profiles, Services, and Characteristics.
  • How to Design your custom GATT (step-by-step).
  • Implementation of a custom GATT using a real-life example of a MIDI device (including full source code).
  • Testing and verification using the nRF Connect mobile application.

I hope you’ve enjoyed this tutorial and found it useful.

Other useful tutorials/posts

Finally, if you’re interested in learning more about BLE, Bluetooth 5, and how to implement a complete home automation system that utilizes BLE, then check out my recently released e-book:

Bluetooth 5 & Bluetooth Low Energy: A Developer’s Guide

Bluetooth 5 & Bluetooth Low Energy: A Developer's Guide cover

Download Full Source Code

If you would like to download the code used in this post, simply enter your email address in the form below. You’ll get a .zip containing all the source code, and I will also send you a FREE Report on the Essential Bluetooth Developer Tools. In addition, you will receive exclusive content, tips, and tricks that I don’t post to the blog!

 

 


19 Comments

  1. Peter on July 30, 2017 at 6:27 am

    great post -very helpful – THANKS!
    an unimportatnt detail: the compiler reported “‘NULL’ undeclared” and “implicit declaration of function ‘memset'”
    So I think the following includes got recognized as HTML tags
    midi_service.h line 17
    #include

    midi_service.c line 6
    #include

    • Mohammad Afaneh Mohammad Afaneh on September 4, 2017 at 9:01 am

      Peter, sorry for the late response – missed your message somehow!

      Yes, thanks for pointing those out. I will fix/delete them.

  2. David Elvig on February 16, 2018 at 12:08 pm

    Mohammad,
    Thanks for this.
    I reread and followed it after getting the nRF Dev Kit and the latest Segger Embedded Studio under my belt.
    I’m getting an error described in this Nordic post

    https://devzone.nordicsemi.com/f/nordic-q-a/29347/big-picture-view-of-using-nrf52-as-a-ble-midi-add-on

    In your hands, does this example work with the correct Dev Kit (and with Segger)?

  3. ABDELMALEK OMAR on February 27, 2018 at 10:33 pm

    soft140 has change many things can you update the tutorial

    • Mohammad Afaneh Mohammad Afaneh on February 27, 2018 at 11:15 pm

      More blog posts and updates will be coming soon. At the moment, I’m 100% focusing on my upcoming e-book. After the release (March 28th, 2018), there will be a lot of updates coming to the website including updates to the blog posts. Thanks.

  4. Adnan Jouned on March 13, 2018 at 3:44 pm

    Thanks for this article,

    Here you’ve talked about GATT Generic Access Profile Service (UUID = 0x1800), may I ask if this is the same as GAP? and how could we usually determine GAP UUID ?

    • Mohammad Afaneh Mohammad Afaneh on March 13, 2018 at 9:51 pm

      Thanks, Adnan.

      GAP (the Generic Access Profile), by definition, describes the interactions between two BLE devices in terms of discovery, connections, and security. The GAP service you’re referring to is simply a service that is implemented and exposed by the device as part of its GATT structure. Per the Bluetooth specification, implementation of the GAP service is mandatory. So, basically, there are two main distinct concepts here: GAP (the profile itself), and a service that’s simply named the GAP service.

      Now, for any service, a UUID is used to identify this service, and this is either a 16-bit value (in the case of Bluetooth SIG-adopted UUIDs) or a 128-bit value (in the case of custom services). The value 0x1800 is assigned by the Bluetooth SIG to the GAP service so that when a device discovers this service (that’s exposed by another device), it already knows how to interact with it and interpret the information tied to it.

      Hopefully, this makes it a little easier to understand and more clear.

  5. Vinitha on March 19, 2018 at 8:39 am

    Hi Mohammad,
    I am using Ble 4.0(BT43). Through AT commands i am able to discover and connect to other BLEs. Now I am trying to scan and connect the cycling speed and cadence sensor(CSC) through my BLE 4.0. The problem is I am able to discover the CSC MAC address whereas I am not able to pair the device. The commands that I used to pair are
    1)AT+AB LeConnect [bd address]
    2) AT+AB LeGetChar [handle] Where [handle] is the GATT layer character handle, 1 byte in ascii coded hex format: hh
    but am not getiing any response…. What should I do to pair my BLE with that of the cycling speed and cadence sensor.
    Can you please help me with this.???

  6. JK2940 on April 25, 2018 at 3:02 am

    Very helpful article. Thanks.
    Any way you could add a few code bits on how to transmit and receive the GATT MIDI data?
    That would show the whole data flow from end to end.

    • Mohammad Afaneh Mohammad Afaneh on April 27, 2018 at 2:27 pm

      Thanks, JK2940.

      Unfortunately, I am not an expert (or even have any experience with MIDI). However, I know someone who is experienced with this, and I will reach out to him to get more information.

  7. phlb on April 27, 2018 at 2:21 pm

    hello,

    Thanks for this article. Very helpful for me.
    I port your code on my own hardware with nrf52832 with last sdk v 15.0.0 and s132 v 6.0.0. It works but needs some little modification.
    My device is detected with mac osx hardware tool and nrfConnect. But with Audio MIDI Setup application found in Applications/Utilities on MAC OS X, the device is not visible. have you ever tried with this application? specific data in advertisements?

    Thanks

    • Mohammad Afaneh Mohammad Afaneh on April 27, 2018 at 2:28 pm

      Thanks, phlb.

      The code was written for an older SDK, so I am not surprised that it didn’t work out of the box. Thanks for reporting this.

      I will give it a try with the application you mentioned and report back.

      • phlb on April 28, 2018 at 6:55 am

        it works now:
        #define BLE_UUID_MIDI_SERVICE_UUID 0x0E5A

        static ble_uuid_t m_adv_uuids[] = /**< Universally unique service identifiers. */
        {
        {BLE_UUID_MIDI_SERVICE_UUID, BLE_UUID_TYPE_VENDOR_BEGIN}
        };

        static void advertising_init(void)
        {
        ret_code_t err_code;
        ble_advertising_init_t init;

        memset(&init, 0, sizeof(init));

        init.advdata.name_type = BLE_ADVDATA_FULL_NAME;
        init.advdata.include_appearance = true;
        init.advdata.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
        //init.advdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
        //init.advdata.uuids_complete.p_uuids = m_adv_uuids;

        //scan response data
        init.srdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
        init.srdata.uuids_complete.p_uuids = m_adv_uuids;

        init.config.ble_adv_fast_enabled = true;
        init.config.ble_adv_fast_interval = APP_ADV_INTERVAL;
        init.config.ble_adv_fast_timeout = APP_ADV_DURATION;

        init.evt_handler = on_adv_evt;

        err_code = ble_advertising_init(&m_advertising, &init);
        APP_ERROR_CHECK(err_code);

        ble_advertising_conn_cfg_tag_set(&m_advertising, APP_BLE_CONN_CFG_TAG);
        }

        warning in main() function services_init(); should be placed before advertising_init();

        int main(void)
        {
        bool erase_bonds;

        // Initialize.
        log_init();
        timers_init();
        buttons_leds_init(&erase_bonds);
        power_management_init();
        ble_stack_init();
        gap_params_init();
        gatt_init();
        services_init();
        advertising_init();
        sensor_simulator_init();
        conn_params_init();
        peer_manager_init();

        //
        nuodio_ble_system_init(&g_nuodio_ble_system);
        nuodio_ble_midi_core_init(&g_nuodio_ble_system);

        // Start execution.
        NRF_LOG_INFO("Heart Rate Sensor example started.");
        application_timers_start();
        advertising_start(erase_bonds);

        // Enter main loop.
        for (;;)
        {
        idle_state_handle();
        }
        }

        In advertising_init() I use scan response data to place BLE_UUID_MIDI_SERVICE_UUID and avoid DEVICE_NAME truncation in advertisement.
        the BLE_UUID_MIDI_SERVICE_UUID should be place in advertisement data to be display in Audio MIDI Setup application with full device name.

    • Yanislav Donchev on May 3, 2018 at 6:30 pm

      Hello!

      I have successfully ported this code on SDK 10 and nrf51822. Now, I’m upgrading to the new SDK 15 and nrf52832 with s132 6.0.0. I followed the tutorial step by step, but I couldn’t manage to get the SDK 15 working. When, I flash the code the chip is not detected in the nRF Connect app. I have tried the chip with a few of the examples, to see that it is not faulty. I have also tried to implement this code in the template app that comes with the SDK but with no success. Can you share the changes that you made to make this code compatible?

      Thanks.

  8. JK2940 on April 29, 2018 at 1:24 am

    Very cool. Thanks.
    Now I can see my nRF52 DK test device with my iOS test app.
    I am also using the latest Nordic SDK.
    Not able to connect yet though…

  9. […] contoh xml yang mengambarkan hubungan antar ketiganya( diambil dari situs ini […]

  10. Frank Vieren on November 23, 2018 at 8:52 am

    Hi Mohammad Afaneh,

    Indeed great post, very helpful. Thanks,
    Frank

    • Mohammad Afaneh Mohammad Afaneh on November 23, 2018 at 3:04 pm

      Thanks, Frank!

Leave a Comment