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 handleAttribute UUIDAttribute valueAttribute permissions
0x0001Set the on/off status1/0Writable but not readable
0x0002Read the on/off status1/0Readable 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:

  1. 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.

  2. 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.

  3. 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 to esp-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.