Bluetooth Protocol
1. Introduction to Bluetooth protocol
Chapter 7 introduces the protocol and architecture of Bluetooth. The Bluetooth protocol defines message formats and process for completing specific functions, such as link control, security services, service information exchange and data transmission. This section only introduces the attribute protocol (ATT) of the Bluetooth protocol specification. Bluetooth data exists in the form of attributes, and each attribute consists of four elements.
Attribute handle
Just as memory addresses are used to find contents in memory, attribute handles can also help find the corresponding attribute. For example, the first attribute handle is 0x0001, the second attribute handle is 0x0002, and so on, up to a maximum of 0xFFFF.
Attribute UUID
Each data represents specific property. For example, a smart light has two basic attributes, one for setting the on/off status, and the other for reading the on/off status.
Attribute value
Attribute value is the information that each attribute carries, while the other three elements are to enable the peer to obtain the attribute value much easier. For example, for a smart light, the attribute value for setting the on/off status can be set to "1" to turn on the light, or to "0" to turn off the light; the attribute value for reading the on/off status can be "1" for the "on" status or "0" for the "off" status.
Attribute permissions
Each attribute has corresponding access restrictions for its own attribute values, such as some attributes are readable, some are writable, and some are readable and writable. The party that owns the data can control the attribute permissions of local data through attribute permissions. For example, the switch attribute permission of the smart light can be set as writable but not readable, and the attribute permission for reading the switch status of the smart light can be set as read-only and not writable.
Table 8.2 lists the Bluetooth attributes for the basic functions of a smart light.
Table 8.2. Bluetooth attributes for basic functions of smart light
Attribute handle | Attribute UUID | Attribute value | Attribute permissions |
---|---|---|---|
0x0001 | Set the on/off status | 1/0 | Writable but not readable |
0x0002 | Read the on/off status | 1/0 | Readable but not writable |
The device that stores the data (i.e., attributes) is usually called the server, and the device that receives data from other devices is called the client. For a smart light and a smartphone, the smart light is server, and the smartphone is the client. The following are common operations between a server and a client:
-
The client sends data to the server.
Data is transmitted by writing data to the server. There are two types of write operations: one is write request, and the other is write command. The main difference between the two is that the former requires a response (write response) from the peer, while the latter does not. For a smart light, the command to turn on/off the light sent by the smartphone is a write operation, and this is a write request which requires the smart light to respond. Such response is not a simple ACK response. The result of the action of turning on/off the light needs to be returned to the smartphone to inform it of the current status of the smart light.
-
The server sends data to the client.
The updated data is sent from the server to the client mainly in the form of server indication or notification. Similar to write operations, the main difference between indication and notification is that the former requires the other device to respond (confirm) after receiving the data indication. For a smart light, if it is turned on/off through a physical switch button, its status needs to be reported to the smartphone through indications or notifications, and the smartphone will display the latest status.
-
The client reads data from the server actively.
Generally, the client obtains values of corresponding attributes from the server through read operation. In the case mentioned above where a smart light is turned on/off through a physical switch button, except for waiting to be notified by the server, the smartphone can also obtain the real-time status through read operations.
Then let's consider which way is better to get the status of the smart light. Active reading takes time whenever the smartphone initiates a read operation, while indication or notification saves the time for repetitive data transmission. It seems that the latter option is faster but if the smart light is not connected to the phone when sending the notification, its status will not be updated. This can be fixed by updating the status as soon as the phone becomes connected to the smart light; otherwise, it is recommended to use the read operation.
2. Creating a Bluetooth server using ESP-IDF component
📝 Source code
For the source code of Bluetooth, please refer to
esp-idf/examples/provisioning/legacy/ble_prov
. For the example code of customised configuration, please refer toesp-idf/examples/provisioning/legacy/custom_config
.
The following example uses the protocomm
component to implement the smart light server, and the customised configuration uses the custom-proto protocol. As mentioned earlier, to implement the on/off control and status query of the smart light, two attributes need to be defined. The code is as below:
static esp_err_t wifi_prov_config_set_light_handler(uint32_t session_id, const uint8_t *inbuf,
ssize_t inlen, uint8_t **outbuf,
ssize_t *outlen, void *priv_data)
{
CustomConfigRequest *req;
CustomConfigResponse resp;
req = custom_config_request_unpack(NULL, inlen, inbuf);
if (!req) {
ESP_LOGE(TAG, "Unable to unpack config data");
return ESP_ERR_INVALID_ARG;
}
custom_config_response_init(&resp);
resp.status = CUSTOM_CONFIG_STATUS_ConfigFail;
if (req->open_light) {//Turn on the smart light
//Pull up GPIO level according to the status
ESP_LOGI(TAG, "Open the light");
} else {
//Pull down GPIO level according to the status
ESP_LOGI(TAG, "Close the light");
}
//Set response status and smart light status according to the light's execution result
resp.status = CUSTOM_CONFIG_STATUS_ConfigSuccess;
custom_config_request_free_unpacked(req, NULL);
resp.light_status = 1; //Respond according to the light status
*outlen = custom_config_response_get_packed_size(&resp);
if (*outlen <= 0) {
ESP_LOGE(TAG, "Invalid encoding for response");
return ESP_FAIL;
}
*outbuf = (uint8_t *) malloc(*outlen);
if (*outbuf == NULL) {
ESP_LOGE(TAG, "System out of memory");
return ESP_ERR_NO_MEM;
}
custom_config_response_pack(&resp, *outbuf);
return ESP_OK;
}
static int wifi_prov_config_get_light_handler(uint32_t session_id, const uint8_t *inbuf,
ssize_t inlen, uint8_t **outbuf,
ssize_t *outlen, void *priv_data)
{
CustomConfigResponse resp;
custom_config_response_init(&resp);
resp.status = CUSTOM_CONFIG_STATUS_ConfigSuccess;
resp.light_status = 1; //Respond according to the light status
*outlen = custom_config_response_get_packed_size(&resp);
if (*outlen <= 0) {
ESP_LOGE(TAG, "Invalid encoding for response");
return ESP_FAIL;
}
*outbuf = (uint8_t *) malloc(*outlen);
if (*outbuf == NULL) {
ESP_LOGE(TAG, "System out of memory");
return ESP_ERR_NO_MEM;
}
custom_config_response_pack(&resp, *outbuf);
return ESP_OK;`
}
static esp_err_t app_prov_start_service(void)
{
//Create protocomm
g_prov->pc = protocomm_new();
if (g_prov->pc == NULL) {
ESP_LOGE(TAG, "Failed to create new protocomm instance");
return ESP_FAIL;
}
//Attribute value
protocomm_ble_name_uuid_t nu_lookup_table[] = {
{"prov-session", 0x0001},
{"prov-config", 0x0002},
{"proto-ver", 0x0003},
{"set-light", 0x0004}, //Set the state of the smart light
{"get-light", 0x0005}, //Get the status of the smart light
};
//Bluetooth configuration
protocomm_ble_config_t config = {
.service_uuid = {
/* LSB <---------------------------------------
* ---------------------------------------> MSB */
0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf,
0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02,
},
.nu_lookup_count=sizeof(nu_lookup_table)/sizeof(nu_lookup_table[0]),
.nu_lookup = nu_lookup_table
};
uint8_t eth_mac[6];
esp_wifi_get_mac(WIFI_IF_STA, eth_mac);
snprintf(config.device_name, sizeof(config.device_name), "%s%02X%02X%02X",
ssid_prefix, eth_mac[3], eth_mac[4], eth_mac[5]);
//Release BT memory as only Bluetooth LE protocol stack is used.
esp_err_t err = esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
if (err) {
ESP_LOGE(TAG, "bt_controller_mem_release failed %d", err);
if (err ! = ESP_ERR_INVALID_STATE) {
return err;
}
}
//Start protocomm Bluetooth LE protocol stack
if (protocomm_ble_start(g_prov->pc, &config) ! = ESP_OK) {
ESP_LOGE(TAG, "Failed to start BLE provisioning");
return ESP_FAIL;
}
//Set protocomm version verification endpoint for the protocol
protocomm_set_version(g_prov->pc, "proto-ver", "V0.1");
//Set protocomm security type for the endpoint
if (g_prov->security == 0) {
protocomm_set_security(g_prov->pc, "prov-session",
&protocomm_security0, NULL);
} else if (g_prov->security == 1) {
protocomm_set_security(g_prov->pc, "prov-session",
&protocomm_security1, g_prov->pop);
}
//Add an endpoint for Wi-Fi configuration
if(protocomm_add_endpoint(g_prov->pc, "prov-config",
wifi_prov_config_data_handler,
(void *) &wifi_prov_handlers) ! =ESP_OK){
ESP_LOGE(TAG, "Failed to set provisioning endpoint");
protocomm_ble_stop(g_prov->pc);
return ESP_FAIL;
}
//Add an endpoint for setting smart light status
if (protocomm_add_endpoint(g_prov->pc, "set-light",
wifi_prov_config_set_light_handler,
NULL) ! = ESP_OK) {
ESP_LOGE(TAG, "Failed to set set-light endpoint");
protocomm_ble_stop(g_prov->pc);
return ESP_FAIL;
}
//Add an endpoint for getting smart light status
if (protocomm_add_endpoint(g_prov->pc, "get-light",
wifi_prov_config_get_light_handler,
NULL) ! = ESP_OK) {
ESP_LOGE(TAG, "Failed to set get-light endpoint");
protocomm_ble_stop(g_prov->pc);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Provisioning started with BLE devname : '%s'", config.device_name);
return ESP_OK;
}
The above example provides two attributes: set-light
and get-light
,
and the corresponding attribute handles are 0x0004 and 0x0005,
respectively. When the smartphone sends a command to set the light, the
wifi_prov_config_set_light_handler()
callback function will be
executed to handle the on/off action and inform the smartphone of the
current status of the smart light. When the smartphone sends a read
command, the wifi_prov_config_get_light_handler()
callback function
will be executed to inform the smartphone of the current status of the
smart light. You can use the Bluetooth debugging assistant of the
smartphone to scan the devices connected to Bluetooth, and understand
the function of each service more intuitively through the services
provided by the Bluetooth device.
The above example implements local control via Bluetooth based on the
protocomm
component, and the data structure is relatively complex. If
you are an experienced developer, you can try to use the ideas of the
above example to implement local control. In addition, this book
provides the most basic server example based on Bluetooth for beginners.
You can refer to the example code in subsection 8.5.3 to understand the
process of local control using Bluetooth.