Chapter 58: BLE GATT Services and Characteristics

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the principles of effective BLE GATT service and characteristic design.
  • Differentiate between standard SIG-adopted and custom services/characteristics.
  • Correctly choose and utilize 16-bit and 128-bit UUIDs.
  • Select appropriate properties (Read, Write, Notify, Indicate, etc.) for characteristics based on use cases.
  • Define suitable permissions for attribute access.
  • Effectively use standard descriptors like CCCD, User Description, and Presentation Format.
  • Design strategies for representing various data types within characteristic values.
  • Apply best practices for creating efficient, maintainable, and interoperable GATT structures.

Introduction

In the previous chapter, we learned how to implement a GATT server on the ESP32, bringing our BLE peripheral to life by creating services and characteristics. However, what services and characteristics to create, and how to define them, is a critical design step that significantly impacts your application’s functionality, efficiency, and usability.

Poor GATT design can lead to inefficient communication, difficulty for client applications to understand and use your device, and even interoperability issues. Conversely, a well-thought-out GATT structure makes your BLE device intuitive, power-efficient, and easy to integrate with other systems.

This chapter focuses on the art and science of designing BLE GATT services and characteristics. We will explore how to model your device’s data and functionality, choose appropriate UUIDs, define properties and permissions, and utilize descriptors to create a robust and meaningful interface for BLE clients. Understanding these design principles is paramount before writing any GATT server code.

Theory

At its heart, designing a GATT structure is about defining a contract. This contract specifies what data your peripheral offers and how clients can interact with it.

Recap: Services, Characteristics, and Descriptors

Let’s briefly revisit the core components from Chapter 56 and 57:

  • Service: A collection of related characteristics that encapsulate a specific function or feature of the device. For example, a “Heart Rate Service” groups all heart-rate-related data.
  • Characteristic: The fundamental data container in GATT. It represents a piece of information (like a temperature reading or a device name) or a control point (like a switch). It has a value, properties defining how it can be accessed, and permissions.
  • Descriptor: Provides additional metadata or configuration options for a characteristic. For example, a human-readable description or a setting to enable notifications.

1. Standard vs. Custom Services and Characteristics

When designing your GATT structure, the first decision is often whether to use pre-defined standard entities or create custom ones.

  • Bluetooth SIG-Adopted (Standard) Services/Characteristics:
    • Defined by the Bluetooth Special Interest Group (SIG) for common use cases (e.g., Battery Service, Device Information Service, Heart Rate Service).
    • They use 16-bit UUIDs (e.g., 0x180F for Battery Service, 0x2A19 for Battery Level characteristic).
    • Advantages: Promotes interoperability. Standard client applications can often understand and display data from these services without specific knowledge of your device.
    • When to use: If your device implements a feature covered by a standard profile, using the SIG-adopted service/characteristic is highly recommended.
    • The list of adopted services and characteristics can be found on the Bluetooth SIG website under “Assigned Numbers.”
  • Custom Services/Characteristics:
    • Defined by you for application-specific data or functionality not covered by standard profiles.
    • They must use 128-bit UUIDs to avoid collisions with SIG-adopted UUIDs or other custom UUIDs.
    • Advantages: Flexibility to model any data or control point specific to your device.
    • When to use: For proprietary features or when no suitable standard service/characteristic exists.
Feature Bluetooth SIG-Adopted (Standard) Custom
Definition Defined by Bluetooth SIG for common use cases. Defined by developers for application-specific data/functionality.
UUID Type 16-bit (e.g., 0x180F for Battery Service) 128-bit (e.g., A1B2C3D4-…-ABCDEF)
Example Characteristic Battery Level (0x2A19) “Device Specific Sensor Value”
Advantages Promotes interoperability; understood by standard clients. Flexibility to model any specific data or control point.
When to Use If device feature is covered by a standard profile. Highly recommended. For proprietary features or when no suitable standard entity exists.
Warning N/A Never use a 16-bit UUID for custom entities to avoid conflicts.

Warning: Never use a 16-bit UUID for a custom service or characteristic. This can lead to conflicts if the Bluetooth SIG later adopts that UUID for a standard entity.

2. Universally Unique Identifiers (UUIDs)

UUIDs are the unique identifiers for services, characteristics, and descriptors.

UUID Type Description Formation / Usage Example ESP-IDF Representation (Conceptual)
16-bit UUID Short alias for SIG-adopted entities. Combined with Bluetooth Base UUID (0000xxxx-0000-1000-8000-00805F9B34FB) where ‘xxxx’ is the 16-bit value. 0x180F (Battery Service) Macros like ESP_GATT_UUID_BATTERY_SERVICE. esp_bt_uuid_t.uuid.uuid16
128-bit UUID Globally unique identifier required for custom entities. Typically a Version 4 (random) UUID. Generated using online tools or libraries. A1B2C3D4-E5F6-7890-1234-567890ABCDEF Byte array (e.g., uint8_t my_uuid[16]). esp_bt_uuid_t.uuid.uuid128
  • 16-bit UUIDs:
    • Reserved for Bluetooth SIG-adopted entities.
    • These are short aliases for a full 128-bit Bluetooth Base UUID combined with the 16-bit value. The Bluetooth Base UUID is 0000xxxx-0000-1000-8000-00805F9B34FB. For example, 0x180F becomes 0000180F-0000-1000-8000-00805F9B34FB.
    • When using ESP-IDF, you can often use predefined macros like ESP_GATT_UUID_BATTERY_SERVICE which resolves to 0x180F.
  • 128-bit UUIDs:
    • Required for all custom services, characteristics, and descriptors.These are randomly generated 128-bit numbers. You can use online UUID generators (ensure you get a Version 4 random UUID).Example: A1B2C3D4-E5F6-7890-1234-567890ABCDEFIn ESP-IDF, you’ll represent these as a byte array, typically in little-endian or big-endian format depending on the API (ESP-IDF usually expects little-endian for direct array representation in esp_bt_uuid_t when len is ESP_UUID_LEN_128).
C
// Example of a custom 128-bit UUID in ESP-IDF
static uint8_t my_custom_service_uuid[ESP_UUID_LEN_128] = {
    // LSB ... MSB (Example: A1B2C3D4-E5F6-7890-1234-567890ABCDEF)
    0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12,
    0x90, 0x78, 0xF6, 0xE5, 0xD4, 0xC3, 0xB2, 0xA1
};
// When setting esp_bt_uuid_t:
// esp_bt_uuid_t service_uuid;
// service_uuid.len = ESP_UUID_LEN_128;
// memcpy(service_uuid.uuid.uuid128, my_custom_service_uuid, ESP_UUID_LEN_128);

3. Characteristic Properties

Properties define how a characteristic’s value can be accessed by a client. You combine these using bitwise OR.

  • ESP_GATT_CHAR_PROP_BIT_BROADCAST (0x01): Allows the characteristic value to be included in advertising packets if the server supports broadcasting. Less common.
  • ESP_GATT_CHAR_PROP_BIT_READ (0x02): Allows the client to read the characteristic’s value.
  • ESP_GATT_CHAR_PROP_BIT_WRITE_NR (0x04): “Write Without Response.” Allows the client to write the characteristic’s value without an acknowledgment from the server. Faster but less reliable.
  • ESP_GATT_CHAR_PROP_BIT_WRITE (0x08): Allows the client to write the characteristic’s value, and the server will acknowledge the write. More reliable.
  • ESP_GATT_CHAR_PROP_BIT_NOTIFY (0x10): Allows the server to send the characteristic’s value to the client without the client explicitly requesting it (polling). The client must first subscribe to notifications by writing to the CCCD. No acknowledgment from the client.
  • ESP_GATT_CHAR_PROP_BIT_INDICATE (0x20): Similar to Notify, but the client must acknowledge receipt of the indication. More reliable than Notify, but slower due to the acknowledgment.
  • ESP_GATT_CHAR_PROP_BIT_AUTH (0x40): “Authenticated Signed Writes.” Allows signed writes to the characteristic, requiring an authenticated link.
  • ESP_GATT_CHAR_PROP_BIT_EXT_PROP (0x80): “Extended Properties.” Indicates that an “Extended Properties Descriptor” (UUID 0x2900) is present to define further properties.

As a summary Table:

Property Name ESP-IDF Macro Hex Value Description Common Use Case
Broadcast ESP_GATT_CHAR_PROP_BIT_BROADCAST 0x01 Allows characteristic value to be in advertising packets. Server status broadcast (less common for GATT interaction).
Read ESP_GATT_CHAR_PROP_BIT_READ 0x02 Client can read the characteristic value. Sensor readings, device information, status flags.
Write Without Response ESP_GATT_CHAR_PROP_BIT_WRITE_NR 0x04 Client can write value without server acknowledgment. Faster, less reliable. High-frequency control commands where occasional loss is okay.
Write ESP_GATT_CHAR_PROP_BIT_WRITE 0x08 Client can write value; server acknowledges. More reliable. Configuration settings, critical control commands.
Notify ESP_GATT_CHAR_PROP_BIT_NOTIFY 0x10 Server sends value to subscribed client without explicit request. No client ack. Frequently changing sensor data, real-time updates.
Indicate ESP_GATT_CHAR_PROP_BIT_INDICATE 0x20 Server sends value to subscribed client; client must acknowledge. More reliable than Notify. Critical alerts or updates requiring confirmed receipt.
Authenticated Signed Writes ESP_GATT_CHAR_PROP_BIT_AUTH 0x40 Allows signed writes, requiring an authenticated link. Secure commands requiring data integrity and authentication.
Extended Properties ESP_GATT_CHAR_PROP_BIT_EXT_PROP 0x80 Indicates an Extended Properties Descriptor (UUID 0x2900) is present for more properties. Advanced scenarios like “Reliable Write”.

Choosing Properties:

  • Sensor reading: Likely READ, possibly NOTIFY or INDICATE if it changes frequently.
  • Device configuration setting: Likely READ and WRITE.
  • Control point (e.g., turn LED on/off): Likely WRITE or WRITE_NR.
  • Status that changes infrequently: READ.
  • Status that changes frequently and needs to be pushed: NOTIFY (for speed) or INDICATE (for reliability).

4. Attribute Permissions

Permissions define the security conditions required to access an attribute’s value (characteristic value or descriptor value). These are combined using bitwise OR.

  • ESP_GATT_PERM_READ (0x01): Read access is allowed.
  • ESP_GATT_PERM_READ_ENCRYPTED (0x02): Read access is allowed only over an encrypted link.
  • ESP_GATT_PERM_READ_ENC_MITM (0x04): Read access is allowed only over an encrypted link with Man-In-The-Middle (MITM) protection (authenticated pairing).
  • ESP_GATT_PERM_WRITE (0x10): Write access is allowed.
  • ESP_GATT_PERM_WRITE_ENCRYPTED (0x20): Write access is allowed only over an encrypted link.
  • ESP_GATT_PERM_WRITE_ENC_MITM (0x40): Write access is allowed only over an encrypted link with MITM protection.
  • ESP_GATT_PERM_WRITE_SIGNED (0x80): (Not directly used in basic permissions, related to Authenticated Signed Writes property).
  • ESP_GATT_PERM_WRITE_SIGNED_MITM (0x100): (Not directly used in basic permissions).

As a Summary Table:

Permission Name ESP-IDF Macro Hex Value Description Typical Use
Read ESP_GATT_PERM_READ 0x01 Read access is allowed (no security). Publicly readable data (e.g., Manufacturer Name, User Description).
Read Encrypted ESP_GATT_PERM_READ_ENCRYPTED 0x02 Read access allowed only over an encrypted link. Moderately sensitive data that needs confidentiality.
Read Encrypted MITM ESP_GATT_PERM_READ_ENC_MITM 0x04 Read access allowed only over an encrypted link with Man-In-The-Middle (MITM) protection (authenticated pairing). Highly sensitive data requiring authenticated access.
Write ESP_GATT_PERM_WRITE 0x10 Write access is allowed (no security). Non-sensitive, writable configurations; CCCD for non-sensitive characteristics.
Write Encrypted ESP_GATT_PERM_WRITE_ENCRYPTED 0x20 Write access allowed only over an encrypted link. Control points or configurations needing confidentiality.
Write Encrypted MITM ESP_GATT_PERM_WRITE_ENC_MITM 0x40 Write access allowed only over an encrypted link with MITM protection. Critical control points or highly sensitive configurations.
Write Signed ESP_GATT_PERM_WRITE_SIGNED 0x80 Related to Authenticated Signed Writes property. Requires data signing. Secure commands where data integrity from an authenticated source is key.
Write Signed MITM ESP_GATT_PERM_WRITE_SIGNED_MITM 0x100 Similar to Write Signed, but also requires MITM protection. Highest security for write operations.

Choosing Permissions:

  • Publicly readable data (e.g., Manufacturer Name): ESP_GATT_PERM_READ.
  • Writable configuration that is not sensitive: ESP_GATT_PERM_WRITE.
  • Sensitive data or critical control points: Use _ENCRYPTED or _ENC_MITM permissions. This forces the BLE link to be secure before access is granted.
  • CCCD: Typically ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE (or encrypted versions if the characteristic it configures is sensitive).

Note: Enforcing encrypted/authenticated permissions requires the BLE link to undergo pairing and bonding, which involves the Security Manager Protocol (SMP), covered in Chapter 60.

5. Descriptors

Descriptors provide context or configuration for characteristics.

  • Client Characteristic Configuration Descriptor (CCCD – UUID 0x2902):
    • Purpose: Essential for characteristics that support Notifications or Indications.
    • Value: A 2-byte bitfield.
      • Bit 0: Notifications enabled (1) / disabled (0).
      • Bit 1: Indications enabled (1) / disabled (0).
    • Properties: Must be Readable and Writable by the client.
    • Permissions: Typically ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE.
    • How it works: The client writes 0x0001 to this descriptor to enable notifications, or 0x0002 to enable indications for the parent characteristic. The server stores this state (per client connection) and starts sending updates.
  • Characteristic User Description (CUD – UUID 0x2901):
    • Purpose: Provides a human-readable string describing the characteristic (e.g., “Room Temperature Sensor”).
    • Value: A UTF-8 string.
    • Properties: Must be Readable.
    • Permissions: Typically ESP_GATT_PERM_READ.
    • Usefulness: Helps developers understand the purpose of characteristics when using generic BLE explorer tools.
  • Characteristic Presentation Format (CPF – UUID 0x2904):
    • Purpose: Defines the format, unit, and exponent of the characteristic’s value.
    • Value: A 7-byte structure:
      • Format (1 byte): e.g., boolean, uint8, sint16, float32 (see Bluetooth SIG Assigned Numbers for format types).
      • Exponent (1 byte, signed): e.g., -2 for value * 10^-2.
      • Unit (2 bytes): UUID for the unit (e.g., 0x272F for Celsius, 0x2703 for percentage).
      • Namespace (1 byte): Identifies the organization that defined the unit UUID.
      • Description (2 bytes): Namespace-specific enumeration.
    • Properties: Must be Readable.
    • Permissions: Typically ESP_GATT_PERM_READ.
    • Usefulness: Allows clients to correctly interpret and display numerical data.
  • Other Descriptors:
    • Characteristic Extended Properties (UUID 0x2900): If the ESP_GATT_CHAR_PROP_BIT_EXT_PROP property is set on the characteristic. Defines additional properties like “Reliable Write” or “Writable Auxiliaries.”
    • Valid Range (UUID 0x2906): Defines the valid range for a numerical characteristic value.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
sequenceDiagram
    participant Client as GATT Client
    participant Server as ESP32 GATT Server

    activate Client
    Client->>Server: 1. Connect Request
    activate Server
    Server-->>Client: 2. Connection Established
    deactivate Server

    Note over Client,Server: Client wants to receive temperature updates from <br>"TempMeasurement" Characteristic

    Client->>Server: 3. Write Request to CCCD of "TempMeasurement" Char.<br>(Handle: Temp_CCCD_Handle, Value: 0x0001 - Enable Notifications)
    activate Server
    Note right of Server: Server receives write to CCCD Handle.<br>Validates value (0x0001).<br>Stores [Client_Conn_ID, Temp_Char_Handle, Notify_Enabled = true]
    Server-->>Client: 4. Write Response (Status: OK)
    deactivate Server
    deactivate Client

    loop Temperature Changes on Server
        Note over Server: Temperature changes (e.g., from 25.0°C to 25.5°C)
        activate Server
        Server->>Client: 5. Send Notification for "TempMeasurement" Char.<br>(Handle: Temp_Char_Handle, Value: [New Temp Data])
        deactivate Server
    end


Key GATT Descriptors Table:

Descriptor Name UUID Purpose Value Structure / Example Typical Properties & Permissions
Client Characteristic Configuration Descriptor (CCCD) 0x2902 Enables/disables Notifications or Indications for its parent characteristic. 2-byte bitfield:
Bit 0: Notify (1=on, 0=off)
Bit 1: Indicate (1=on, 0=off)
E.g., 0x0001 (Notify On), 0x0002 (Indicate On)
Properties: Read, Write
Permissions: ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE (or encrypted versions)
Characteristic User Description (CUD) 0x2901 Provides a human-readable string describing the characteristic. UTF-8 string.
E.g., “Ambient Temperature Sensor”
Properties: Read
Permissions: ESP_GATT_PERM_READ (or encrypted)
Characteristic Presentation Format (CPF) 0x2904 Defines format, unit, and exponent of the characteristic’s value. 7-byte structure: Format, Exponent, Unit (UUID), Namespace, Description.
E.g., Format: sint16, Exp: -2, Unit: 0x272F (Celsius)
Properties: Read
Permissions: ESP_GATT_PERM_READ (or encrypted)
Characteristic Extended Properties 0x2900 Defines additional characteristic properties if ESP_GATT_CHAR_PROP_BIT_EXT_PROP is set on the characteristic. 2-byte bitfield (e.g., Reliable Write, Writable Auxiliaries). Properties: Read
Permissions: ESP_GATT_PERM_READ (or encrypted)
Valid Range 0x2906 Defines the valid range for a numerical characteristic value. Two fields, each of the same format as the characteristic value (e.g., lower bound, upper bound). Properties: Read
Permissions: ESP_GATT_PERM_READ (or encrypted)

6. Data Types and Formatting in Characteristic Values

Characteristics can hold various types of data. You need to decide how to represent this data as a byte array.

  • Integers (uint8_t, int16_t, uint32_t, etc.):
    • Choose the smallest appropriate size.
    • Endianness: BLE standard is little-endian. If your ESP32 (which is little-endian) is communicating with another little-endian system, direct memcpy works. If communicating with a big-endian system, byte swapping is needed on one side. ESP-IDF typically handles this correctly for its internal operations, but be mindful when constructing/parsing multi-byte values.
    • Example: A uint16_t value 0x1234 is [0x34, 0x12] in little-endian.
  • Floating-Point Numbers (float, double):
    • BLE GATT doesn’t have a native “float” characteristic type in the same way it has for integers via the Presentation Format descriptor.
    • Option 1 (Recommended for standard interpretation): Use an integer type and the Characteristic Presentation Format descriptor to specify the exponent. E.g., temperature 25.50°C could be sent as int16_t value 2550 with an exponent of -2 in the CPF.
    • Option 2 (Custom interpretation): Send the raw IEEE 754 byte representation of the float.
      // float temp = 25.5f;
      // uint8_t char_value[sizeof(float)];
      // memcpy(char_value, &temp, sizeof(float)); // // Client needs to know to interpret these 4 bytes as a float
  • Strings:
    • Typically UTF-8 encoded.
    • The length can vary. The client might read the length first or the server might send it as part of a notification.
    • Consider attr_max_len in esp_attr_value_t carefully.
    • Null termination: Standard C strings are null-terminated. If the client expects this, ensure it. However, the length field in ATT packets is explicit, so null termination isn’t strictly required by BLE itself if the length is known.
  • Booleans:
    • Often represented as uint8_t with values 0x00 (false) and 0x01 (true).
    • The Presentation Format descriptor can specify a boolean format.
  • Custom Structures / Multiple Values:
    • Option 1: Separate Characteristics: Each field of the structure gets its own characteristic. Simpler for clients to understand individual fields but more overhead (more attributes, potentially more reads).
    • Option 2: Single Characteristic with Byte Array: Pack the structure into a byte array. More efficient for transferring multiple related values at once but requires the client to know the byte layout to parse it.
      • Example: Accelerometer (X, Y, Z as int16_t each): [X_LSB, X_MSB, Y_LSB, Y_MSB, Z_LSB, Z_MSB].
    • Consider alignment and padding if memcpying structs directly.

Summary:

Data Type Representation in Characteristic Value (Byte Array) Key Considerations Example
Integers (uint8_t, int16_t, uint32_t) Direct binary representation. Smallest appropriate size. Endianness (BLE standard is Little-Endian). uint16_t val = 0x1234; -> [0x34, 0x12]
Floating-Point (float, double) 1. Scaled integer + CPF descriptor (recommended).
2. Raw IEEE 754 byte representation.
Interpretation by client. CPF aids standard interpretation. Raw bytes require custom client logic. 1. Temp 25.50°C -> int16_t: 2550, CPF exponent: -2.
2. float f=25.5; memcpy(bytes, &f, 4);
Strings UTF-8 encoded byte array. Variable length. attr_max_len. Null termination (optional if length is explicit). “Hello” -> [0x48, 0x65, 0x6C, 0x6C, 0x6F]
Booleans Typically uint8_t: 0x00 (false), 0x01 (true). CPF can specify boolean format. true -> [0x01]
Custom Structures / Multiple Values 1. Separate characteristics for each field.
2. Single characteristic with packed byte array.
Option 1: Simpler client parsing, more overhead.
Option 2: Efficient transfer, client needs to know byte layout. Alignment/padding for structs.
Struct {X,Y,Z as int16_t} -> [X_LSB, X_MSB, Y_LSB, Y_MSB, Z_LSB, Z_MSB]
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph LR
    subgraph Conceptual_Data [Conceptual Data Structure]
        direction LR
        Struct["<b>struct SensorData {</b><br>  int id;<br>  float value;<br>  char name[10];<br><b>};</b>"]
    end

    subgraph OptionA [Option A: Separate Characteristics]
        direction LR
        ServiceA["Sensor Service (Custom UUID)"]
        CharA1["<b>ID_Char</b> (int)<br>UUID: Custom1<br>Props: Read"]
        CharA2["<b>Value_Char</b> (scaled int + CPF)<br>UUID: Custom2<br>Props: Read, Notify<br>Desc: CPF (format=sint32, exp=-2), CUD"]
        CharA3["<b>Name_Char</b> (string)<br>UUID: Custom3<br>Props: Read<br>Desc: CUD"]
        ServiceA --> CharA1
        ServiceA --> CharA2
        ServiceA --> CharA3
    end

    subgraph OptionB ["Option_B:_Single_Characteristic_(Packed Data)"]
        direction LR
        ServiceB["Sensor Service (Custom UUID)"]
        CharB1["<b>SensorData_Char</b> (byte array)<br>UUID: Custom4<br>Props: Read, Notify<br>Desc: CUD ('Packed ID, Value, Name')"]
        NoteCharB1["Note: Client must parse byte array:<br>[id_bytes][value_bytes][name_bytes]"]
        ServiceB --> CharB1
        CharB1 --> NoteCharB1
    end

    Conceptual_Data -.-> OptionA
    Conceptual_Data -.-> OptionB

    classDef serviceNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef charNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef noteNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E,fontSize:10px;
    classDef structNode fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3;

    class Struct structNode;
    class ServiceA,ServiceB serviceNode;
    class CharA1,CharA2,CharA3,CharB1 charNode;
    class NoteCharB1 noteNode;

7. Designing for Efficiency and Usability

  • Minimize Data Size: Send only necessary data. Smaller packets mean less radio time and lower power consumption.
  • MTU (Maximum Transmission Unit):
    • The default ATT MTU is 23 bytes, allowing a payload of 20 bytes for characteristic values (1 byte for opcode, 2 for handle).
    • BLE 4.2+ allows MTU negotiation up to (typically) 247 bytes (or even larger with BLE 5 extended data length). The ESP-IDF default local MTU can be configured (e.g., via esp_ble_gatt_set_local_mtu).
    • If sending more than (MTU - 3) bytes in a notification/read, it will be fragmented or require long attribute procedures. Design characteristic sizes with MTU in mind.
  • Notifications vs. Indications:
    • Use NOTIFY for frequent updates where occasional loss is acceptable (e.g., continuous sensor stream for display).
    • Use INDICATE for critical updates that require acknowledgment (e.g., an alarm condition).
  • Polling vs. Pushing: Favor NOTIFY/INDICATE over client polling (repeated reads) for data that changes, as it’s much more power-efficient for the peripheral.
  • Grouping: Group related characteristics under a single service.
  • Clarity: Use User Description Descriptors where helpful. Choose meaningful (though custom) UUIDs if possible (e.g., embedding a company ID or product ID in parts of the 128-bit UUID).
  • Versioning: If you anticipate changes to your GATT structure, plan for versioning.
    • Option 1: Add a “Service Version” or “Profile Version” characteristic (e.g., read-only string or integer).
    • Option 2: Use different service UUIDs for major incompatible versions.
    • Avoid changing UUIDs or properties of existing characteristics in a way that breaks backward compatibility if possible. Adding new characteristics to a service is usually safer.

Best Practices for GATT Design:

Best Practice Description Benefit
Minimize Data Size Send only necessary data in characteristic values. Use smallest appropriate data types. Lower power consumption (less radio time), faster transfers.
Be MTU Aware Understand ATT_MTU limits (default 23 bytes). Design characteristic sizes accordingly or plan for MTU negotiation / long attributes. Efficient data transfer, avoids truncation or excessive fragmentation.
Choose Notifications/Indications Wisely Use NOTIFY for frequent, non-critical updates. Use INDICATE for critical updates requiring client acknowledgment. Balances reliability with speed and power efficiency.
Favor Pushing over Polling Use NOTIFY/INDICATE for data that changes, instead of requiring clients to repeatedly read (poll). Significantly more power-efficient for the peripheral (server).
Logical Grouping Group related characteristics under a single, well-defined service. Improved organization, easier for clients to understand device functionality.
Clarity and Documentation Use Characteristic User Description (CUD) descriptors. Document custom UUIDs and data formats. Easier for developers to integrate with your device using generic BLE tools.
Use Standard Entities When Possible If a SIG-adopted service/characteristic fits your use case, use it. Promotes interoperability with standard client applications.
Plan for Versioning If GATT structure might evolve, include a version characteristic or use different service UUIDs for incompatible changes. Allows for future updates while maintaining compatibility with older clients if possible.
Correct UUID Usage Use 16-bit UUIDs only for SIG-adopted entities. Use 128-bit UUIDs for all custom entities. Avoids UUID collisions and ensures global uniqueness for custom designs.
Align Properties and Permissions Ensure characteristic properties (e.g., READ, WRITE) match the defined attribute permissions (e.g., ESP_GATT_PERM_READ). Prevents unexpected access errors for clients.

Practical Examples (Design Focus)

These examples focus on the design choices rather than full ESP-IDF implementation code, which builds upon Chapter 57.

Example 1: Custom Environment Sensing Service

Goal: Create a service to report temperature and humidity. Temperature should be notifiable.

  • Service: “Custom Environment Service”
    • UUID: 128-bit Custom UUID (e.g., F0000001-0451-4000-B000-000000000000)
    • Type: Primary
  • Characteristic 1: “Temperature Measurement”
    • UUID: 128-bit Custom UUID (e.g., F0000002-0451-4000-B000-000000000000)
    • Value: Temperature data (e.g., int16_t representing °C * 100 for two decimal places).
    • Properties:READ | NOTIFY
      • READ: Client can poll for temperature.
      • NOTIFY: Server can push updates when temperature changes significantly.
    • Permissions (Value): ESP_GATT_PERM_READ (or ESP_GATT_PERM_READ_ENCRYPTED if sensitive).
    • Descriptors:
      • CCCD (UUID 0x2902):
        • Permissions: ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE.
        • Purpose: To allow client to enable/disable notifications.
      • Characteristic User Description (UUID 0x2901):
        • Value: “Measures ambient temperature.”
        • Permissions: ESP_GATT_PERM_READ.
      • Characteristic Presentation Format (UUID 0x2904):
        • Value: Format (sint16), Exponent (-2), Unit (0x272F for Celsius).
        • Permissions: ESP_GATT_PERM_READ.
  • Characteristic 2: “Humidity Measurement”
    • UUID: 128-bit Custom UUID (e.g., F0000003-0451-4000-B000-000000000000)
    • Value: Humidity data (e.g., uint8_t representing % RH).
    • Properties: READ
    • Permissions (Value): ESP_GATT_PERM_READ (or ESP_GATT_PERM_READ_ENCRYPTED).
    • Descriptors:
      • Characteristic User Description (UUID 0x2901):
        • Value: “Measures relative humidity.”
        • Permissions: ESP_GATT_PERM_READ.
      • Characteristic Presentation Format (UUID 0x2904):
        • Value: Format (uint8), Exponent (0), Unit (0x2703 for percentage).
        • Permissions: ESP_GATT_PERM_READ.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph LR
    Service["<b>Service: Custom Environment Service</b><br>UUID: F0000001-... (128-bit)<br>Type: Primary"]

    subgraph Temperature_Characteristic ["Characteristic: Temperature Measurement"]
        direction LR
        Temp_UUID["UUID: F0000002-... (128-bit)"]
        Temp_Value["Value: int16 (Temp °C * 100)"]
        Temp_Props["Properties: READ | NOTIFY"]
        Temp_Perms["Permissions (Value): ESP_GATT_PERM_READ"]
        
        subgraph Temp_Descriptors ["Descriptors"]
            direction LR
            Temp_CCCD["<b>CCCD (0x2902)</b><br>Perms: R/W"]
            Temp_CUD["<b>User Description (0x2901)</b><br>Value: <b>Measures ambient temperature.</b><br>Perms: R"]
            Temp_CPF["<b>Presentation Format (0x2904)</b><br>Value: format=sint16, exp=-2, unit=°C (0x272F)<br>Perms: R"]
        end
    end

    subgraph Humidity_Characteristic ["Characteristic: Humidity Measurement"]
        direction LR
        Hum_UUID["UUID: F0000003-... (128-bit)"]
        Hum_Value["Value: uint8 (% RH)"]
        Hum_Props["Properties: READ"]
        Hum_Perms["Permissions (Value): ESP_GATT_PERM_READ"]

        subgraph Hum_Descriptors ["Descriptors"]
            direction LR
            Hum_CUD["<b>User Description (0x2901)</b><br>Value: <b>Measures relative humidity.</b><br>Perms: R"]
            Hum_CPF["<b>Presentation Format (0x2904)</b><br>Value: format=uint8, exp=0, unit=% (0x2703)<br>Perms: R"]
        end
    end

    Service --> Temperature_Characteristic
    Service --> Humidity_Characteristic

    classDef serviceNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef charNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef descNode fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;
    classDef detailNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E,fontSize:11px;

    class Service serviceNode;
    class Temperature_Characteristic charNode;
    class Humidity_Characteristic charNode;
    class Temp_UUID detailNode;
    class Temp_Value detailNode;
    class Temp_Props detailNode;
    class Temp_Perms detailNode;
    class Hum_UUID detailNode;
    class Hum_Value detailNode;
    class Hum_Props detailNode;
    class Hum_Perms detailNode;
    class Temp_Descriptors descNode;
    class Hum_Descriptors descNode;
    class Temp_CCCD detailNode;
    class Temp_CUD detailNode;
    class Temp_CPF detailNode;
    class Hum_CUD detailNode;
    class Hum_CPF detailNode;

Design Rationale:

  • Temperature is notifiable because it might change dynamically, and pushing updates is efficient.
  • Humidity might change less frequently or might be polled on demand, so only READ is provided initially (can be extended to NOTIFY if needed).
  • CPF descriptors help clients interpret the numerical data correctly.
  • Custom 128-bit UUIDs are used because this is not a standard SIG service.

Example 2: Smart Light Control Service

Goal: A service to control a smart light (on/off) and read its current status.

  • Service: “Smart Light Control Service”
    • UUID: 128-bit Custom UUID (e.g., A0000001-...)
    • Type: Primary
  • Characteristic 1: “Light Switch State”
    • UUID: 128-bit Custom UUID (e.g., A0000002-...)
    • Value: uint8_t: 0x00 for OFF, 0x01 for ON.
    • Properties:READ | WRITE
      • READ: Client can check the current state.
      • WRITE: Client can change the state.
    • Permissions (Value): ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE (or encrypted versions if control is sensitive).
    • Descriptors:
      • Characteristic User Description (UUID 0x2901):
        • Value: “Controls the light (0=OFF, 1=ON).”
        • Permissions: ESP_GATT_PERM_READ.
  • Characteristic 2 (Optional): “Light Intensity”
    • UUID: 128-bit Custom UUID (e.g., A0000003-...)
    • Value: uint8_t: 0-100 for percentage intensity.
    • Properties: READ | WRITE | NOTIFY (if intensity can change by other means and client needs updates).
    • Permissions (Value): ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE.
    • Descriptors: CCCD (if NOTIFY), User Description, Presentation Format (Unit: percentage).

Design Rationale:

  • Clear separation of control (write) and status (read).
  • Simple uint8_t for on/off state is efficient.
  • Intensity characteristic adds finer control and demonstrates how a service can evolve.

Variant Notes

  • ESP32-S2: As consistently noted, the ESP32-S2 lacks Bluetooth hardware and thus cannot implement any BLE GATT services or characteristics.
  • ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2: All these variants fully support the design and implementation of BLE GATT services and characteristics as described. The ESP-IDF APIs for GATT are consistent.
  • Impact of BLE 5.x Features:
    • Larger ATT_MTU: Newer chips (ESP32 ECO3+, S3, C3, C6, H2) supporting BLE 5.0 can negotiate larger ATT_MTU sizes (e.g., up to 247 bytes or more with Data Length Extension). This means a single read, write, notification, or indication can carry more data. When designing characteristics, you can potentially use larger values if you know the client and server will negotiate a larger MTU. However, always consider fallback for clients that only support the default 23-byte MTU.
    • LE 2M PHY / Coded PHY: These PHYs affect throughput and range but don’t directly change how you define services or characteristics. However, higher throughput might make sending larger characteristic values more feasible.
    • Advertising Extensions (BLE 5.0): Allows more data in advertising packets. While not directly part of GATT service definition, you might advertise service UUIDs or even characteristic data. This is covered in Chapter 62.

The fundamental GATT design principles (UUIDs, properties, permissions, descriptors) remain the same regardless of the specific ESP32 variant (that supports BLE) or BLE version features. The newer features primarily offer more flexibility in how much data can be exchanged efficiently.

Common Mistakes & Troubleshooting Tips

Mistake / Issue (Design Focus) Symptom(s) Fix / Best Practice
Using 16-bit UUIDs for Custom Entities Potential UUID collisions with standard entities; interoperability problems; misinterpretation by generic clients. Always generate and use full 128-bit UUIDs for any custom service, characteristic, or descriptor.
Forgetting CCCD for Notifiable/Indicatable Characteristics Clients cannot enable/disable notifications or indications as there’s no CCCD to write to. Server won’t know to send updates. If a characteristic has NOTIFY or INDICATE property, always add a CCCD (UUID 0x2902) with R/W permissions.
Incorrect Characteristic Properties Clients cannot perform expected operations (e.g., read a sensor, write a control) or can perform unintended/insecure operations. Carefully choose properties (READ, WRITE, NOTIFY, etc.) to match the characteristic’s purpose and intended data flow.
Inconsistent Data Formatting/Interpretation Client displays garbage/incorrect values or crashes. Endianness issues with multi-byte values. Clearly document data format (size, type, endianness). Use CPF descriptor for numerical data. Ensure client/server agree on byte array interpretation.
Overly Large Values without MTU Awareness Data truncation, failed operations, inefficient communication due to excessive fragmentation. Keep characteristic values concise. Consider MTU negotiation for larger data, or break data into smaller characteristics. Default ATT_MTU payload is 20 bytes.
Missing User Descriptions for Custom Characteristics Developers using generic BLE tools struggle to understand the purpose of custom characteristics. Add Characteristic User Description (CUD, UUID 0x2901) with a clear, human-readable string for custom characteristics.
Inadequate Permissions for Sensitive Data/Controls Sensitive data readable without encryption, or critical controls writable without security. Use encrypted or authenticated permissions (e.g., ESP_GATT_PERM_READ_ENCRYPTED) for sensitive data or critical control points.

Exercises

  1. Design a Smart Lock Service:
    • Define a GATT service for a smart door lock.
    • Include characteristics for:
      • Lock State (Readable, Notifiable – e.g., “Locked”, “Unlocked”, “Jammed”).
      • Unlock/Lock Command (Writable – e.g., write a specific byte to unlock, another to lock).
      • Battery Level (Readable, Notifiable – using the standard Battery Service UUID 0x180F and Battery Level characteristic UUID 0x2A19 if possible, or a custom one if integrated into your service).
    • Specify UUIDs (custom 128-bit where needed), properties, permissions, and any necessary descriptors (CCCD, User Description). Justify your design choices.
  2. Design a Simple Button Service:
    • Define a GATT service for a device with a single physical button.
    • The primary purpose is to notify a client when the button is pressed (and perhaps when released, or if it’s a long press).
    • Specify UUIDs, properties (likely NOTIFY), permissions, and descriptors for a “Button Press” characteristic.
  3. Critique and Improve GATT Design:
    • Consider the following (poorly designed) custom service for a weather sensor:
      • Service UUID: 0xABCD (Problematic!)
      • Characteristic 1: “WeatherData” (UUID 0x1234)
        • Value: A comma-separated string like “25.5,60.1,1012.5” (Temp,Humidity,Pressure).
        • Properties: READ | WRITE (Why write?)
        • No descriptors.
    • Identify at least 3 design flaws.
    • Propose an improved GATT structure for this weather sensor, following best practices. Specify UUID types, properties, permissions, and useful descriptors.
  4. Research Standard Services:
    • Go to the Bluetooth SIG website (“Assigned Numbers” or service/profile specifications).
    • Find and list three standard (SIG-adopted) services not extensively covered in this chapter (e.g., “Current Time Service,” “Next DST Change Service,” “Glucose Service”).
    • For each service, list its 16-bit UUID and the names/UUIDs of at least two of its primary characteristics. Briefly describe the purpose of these characteristics.

Summary

  • Effective GATT design is crucial for creating functional, efficient, and interoperable BLE devices.
  • Use standard SIG-adopted services and characteristics (16-bit UUIDs) for common functionalities to ensure interoperability.
  • Employ custom services and characteristics (128-bit UUIDs) for application-specific data and control.
  • Carefully select characteristic properties (Read, Write, Notify, Indicate, etc.) to match the intended data flow and interaction model.
  • Define appropriate attribute permissions (Read/Write, Encrypted, Authenticated) to secure data access.
  • Utilize descriptors like CCCD (for notifications/indications), User Description (for clarity), and Presentation Format (for data interpretation).
  • Plan data representation within characteristic values carefully, considering data types, endianness, and potential MTU limitations.
  • Design for efficiency by minimizing data size and favoring server-pushed updates (Notify/Indicate) over client polling.

Further Reading

  • Bluetooth SIG – Service Discovery and GATT Specifications: https://www.bluetooth.com/specifications/specs/
  • Bluetooth SIG – Assigned Numbers Document: https://www.bluetooth.com/specifications/assigned-numbers/
  • ESP-IDF API Reference – esp_gatts_api.h and esp_gatt_defs.h: For ESP32-specific definitions of properties, permissions, and UUID macros.
  • Application Bluetooth Low Energy with Bluetooth Core Specification Version 5.0 (or later): A good book or online resource explaining BLE concepts in depth.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top