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
becomes0000180F-0000-1000-8000-00805F9B34FB
. - When using ESP-IDF, you can often use predefined macros like
ESP_GATT_UUID_BATTERY_SERVICE
which resolves to0x180F
.
- 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-567890ABCDEF
In 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 inesp_bt_uuid_t
whenlen
isESP_UUID_LEN_128
).
- 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:
// 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” (UUID0x2900
) 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
, possiblyNOTIFY
orINDICATE
if it changes frequently. - Device configuration setting: Likely
READ
andWRITE
. - Control point (e.g., turn LED on/off): Likely
WRITE
orWRITE_NR
. - Status that changes infrequently:
READ
. - Status that changes frequently and needs to be pushed:
NOTIFY
(for speed) orINDICATE
(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, or0x0002
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 theESP_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.
- Characteristic Extended Properties (UUID
%%{ 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
value0x1234
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
value2550
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
inesp_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 values0x00
(false) and0x01
(true). - The Presentation Format descriptor can specify a boolean format.
- Often represented as
- 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]
.
- Example: Accelerometer (X, Y, Z as
- Consider alignment and padding if
memcpy
ing 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).
- Use
- 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
- UUID:
- 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
(orESP_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.
- Permissions:
- 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
.
- Value: Format (
- CCCD (UUID
- UUID:
- 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
(orESP_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
.
- Value: Format (
- Characteristic User Description (UUID
- UUID:
%%{ 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 toNOTIFY
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
- UUID:
- 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 User Description (UUID
- UUID:
- 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).
- UUID:
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
- 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 UUID0x2A19
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.
- 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.
- 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.
- Service UUID:
- 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.
- Consider the following (poorly designed) custom service for a weather sensor:
- 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
andesp_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.