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

I’m sure you’re aware that implementing Bluetooth Low Energy (BLE) for devices that interact with smartphones is one of the best ways to achieve a great user experience for your IoT device.

However, designing BLE devices can be a daunting process!

You’re probably thinking:

“Where do I even start??”

One thing I wish I had when I started developing for BLE is more blog posts 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 (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 implementation example using the nRF52 platform.

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

Before we dig into the steps of designing our custom GATT, we’ll need to go over a few core concepts.

Understanding the Attribute Protocol (ATT)

Attribute Protocol (ATT) is the basis for all data exchanges between BLE devices and GATT, so it’s important that we understand it first. It defines generic elements that are used as the basis for GATT called Attributes.

ATT defines two roles: Server and Client.

A Server hosts the data organized into elements called Attributes.
A Client requests reads, writes, notifications and indications for the different attributes’ values.

Each Attribute consists of the following:

  • An Attribute Type defined by a Universally Unique Identifier (UUID) – 128 bits, although standard adopted attributes will have a shortened UUID of 16 bits that is used instead. This defines what the Attribute value really means. For example, a UUID of 0x180A indicates the Device Information Service, and a UUID of 0x180F indicates the Battery Service.
  • An identifier assigned to it called the Attribute Handle which is unique within a specific server (16 bits == 2 bytes) (0x0001-0xFFFF) (0x0000 is reserved for future use).
  • Permissions: only apply to the Attribute’s value, and define whether the value has certain restrictions (readable, writeable).
  • Value: the value of the Attribute. This can be anything and is described by the Attribute Type (UUID).

Think of Handles and Values as Key-Value pairs.

For example, a Client is able to discover the Server’s Attributes. When it wants to read the value of an Attribute or write to it, it uses the Handle to reference the Attribute it’s interested in. Some examples of Attributes include sensor values (read-only) and commands (write-only).

There are two main types of Attributes: Bluetooth SIG-defined attributes (Standard) and user-defined Attributes (Custom). You can find the list of standard Bluetooth SIG Attributes here. In this tutorial and examples, we will use both types.

What is Bluetooth GATT (Generic Attribute Profile)?

GATT defines the hierarchical view of the data that the Server holds. GATT is built on top of the Attribute Protocol (ATT). GATT is specific (and mandatory) for Bluetooth Low Energy implementations. Its role within the Server is to define an exposed data structure (including format) which the Client can use to understand and reference for data access purposes.

GATT consists of the following:

  • Profile: this defines a specific use case and consists of a group of Services that satisfy the use case.
  • Service: a collection of data and associated behaviors to accomplish a specific feature or functionality.
  • Characteristic: contains a single value and optional information about the value.

From the Bluetooth 5.0 specification document (Vol. 3, Part G, Figure 2.5):

In the following sections, we’ll dig deeper into each of the elements in the diagram.


A Profile is actually a “virtual” grouping of one or more Services. Profiles are not visible to Clients when discovering the GATT of a Server nor do they exist on the actual Server. Standard Profiles (Bluetooth SIG adopted) are defined to ensure interoperability between applications of different devices.
For example, if you use the Heart Rate Profile (defined here under the GATT-Based Specifications section), then you can be sure that other devices that support the Heart Rate Profile can interface with your device with no compatibility issues.


Services are collections of data and associated behaviors to accomplish a specific feature or functionality. They contain a collection of characteristics (at least one) and their (optional) descriptors.

There are two types of Services: Primary and Secondary. A primary is one that provides the primary functionality of a device, whereas a secondary is one that adds auxiliary functionality to the device and is referenced from at least one primary Service. In practice, secondary services are rarely used.

Services are assigned unique identifiers called UUIDs. Official Bluetooth adopted Services have 16-bit UUIDs assigned, whereas user-defined get assigned 128-bit UUIDs. For example, the Heart Rate Service has a UUID of 180D, whereas an example of a user-defined Service would have a UUID of 195AE58A-437A-489B-B0CD-B7C9C394BAE4.


Characteristics contain a single value and optional information about the value. Examples of optional information include configuration information about how the value is accessed and information about how the value is displayed or represented (such as units, ranges…etc). A characteristic also contains properties that define what operations are allowed or can be configured for the value.

Most common ones are:

  • Read: If enabled, it will allow a Client to read the value.
  • Write: If enabled, it will allow a Client to change the value.
  • Indicate: If enabled, the Client will be notified if the value changes and a confirmation from the Client to the Server is expected.
  • Notify: If enabled, the Client will be notified if the value changes, a confirmation is NOT expected.

Indications and Notifications provide a more efficient way for Clients to know when a value has changed (instead of having to manually poll and read the characteristic value to know when it actually changes).

Example GATT

One of the best examples that I’ve found useful for visualizing and learning GATT is the gatt.xml framework that Silicon Labs uses for their BLE development. Following is an example of a GATT.xml (from the dkble112 example provided with their BLE stack):

As you can see from this example, everything is laid out neatly and the hierarchy of services, characteristics and their components are pretty clear. You’ll see that there are four services defined:

  • Generic Access Profile Service (UUID = 0x1800)
  • Device Information Service (UUID = 0x180A)
  • Health Thermometer Service (UUID = 0x1809)
  • Battery Service (UUID = 0x180F)

Some of the things you’ll notice:

  • Each Service has one or more Characteristics as part of its definition.
  • Each Characteristic also has a UUID assigned to it.
  • All the Services and Characteristics defined in this GATT are standard/adopted ones (hence the 16-bit UUIDs instead of 128-bit UUIDs).
  • Each Characteristic has a value defined within it.
  • All Characteristics have a property assigned as well (e.g. read, indicate…etc).
  • Most values have a const value whereas one of them (in the Battery Service) is defined as a user type. In the case of the user-defined Characteristic, the user application is responsible for defining the value and providing it to the stack when a Client requests a read. This also means that the value can be dynamically updated during runtime.

You can learn more about Silicon Labs’ GATT.xml framework here.

How to Design your Bluetooth GATT [step-by-step]

1. Design your system at a high-level

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:

  • Define the data elements that the server needs to expose to the Client.
  • Define whether each of these elements will be available for: Read, Write, and Notifications of value changes back to the Client.
  • Think 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.
  • Group 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.

2. Match your data elements to any standard Services and 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 with in 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.

3. Assign Custom UUIDs to the custom Services and Characteristics

Use an online such as GUID Generator to generate custom UUIDs for the different non-standard Services and Characteristics in your GATT. For a step-by-step guide on how to do this refer to my blog post: How do I choose a UUID for my custom services and characteristics?

4. 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 Preview Development Kit (A MIDI device use-case)

In this example, we will follow the GATT spec for a MIDI BLE device according to the BLE MIDI Specification (PDF here). The focus is on the GATT implementation only (Services and Characteristics). Upon a successful implementation, you will be able to scan for the device, connect it, discover its Services and Characteristics, and verify that they match the MIDI spec. (Full source code is available for download at the bottom of the post)

The steps will be as follows:

  1. Use the ble_app_template example provided by the nRF52 SDK.
  2. Modify the example to add the MIDI Service and Characteristic (according to the spec). Add the Battery Service to the GATT as an example of implementing a Bluetooth SIG standard Service.
  3. Compile, and flash the nRF52840 Preview Development Kit with the new image.
  4. Scan for the device via a Client Emulator App such as the Nordic nRF Connect App (iOS or Android).
  5. Connect to the device and discover the Services and Characteristics.
  6. Verify that the GATT matches the MIDI spec.
  7. Verify that the Battery Service is showing up correctly.

Let’s get started!

Step 1: Setup

Follow my previous tutorial on nRF52 Development and Debugging on a Mac. 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 one exception is that we will be using the ble_app_template example instead of ble_app_hrs.

Step 2: Source code modifications

Once you have the ble_app_template NetBeans Project set up, you will want to modify the source code as follows:

In main.c, add:

Modify the following:

Add the following:

Delete the following:







Comment out the lines:

In Makefile under pca_10056/s140/armgcc/Makefile, add the following below line #37:

In sdk_config.h under pca_10056/s140/config/sdk_config.h, modify the following line to enable the Battery Service:

Add the following files to the root ble_app_template/ example folder:



Step 3: Build and flash the updated project

Compile and flash the updated project. Refer to the previous nRF52 Development and Debugging on a Mac tutorial for step-by-step instructions on how to do this.


Steps 4 & 5:

Run the nRF Connect App on your smartphone:

nRF Connect iOS app main screen

Start a Scan:
nRF Connect iOS app scan results

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

nRF Connect iOS app Connected screen

Click on the Unknown Service (which matches the MIDI Service), and verify that the Characteristic matches the following:

nRF Connect iOS App MIDI Service

Navigate back and click on the Battery Service, then verify that the Characteristic matches the following:

nRF Connect iOS App Battery Service

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

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.

Please comment below if you have any questions or run into any issues.

And be sure to enter your email in the form below to be notified when future Novel Bits blog posts are published!


If you would like to download the code used in this post, please 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 9-page 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!


  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

    midi_service.c line 6

    • 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

    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

    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


    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?


    • 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. */

        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);

        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.


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

        // Enter main loop.
        for (;;)

        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


      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?


  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 […]

Leave a Comment