This webpage documents this GATT client demonstration, which issues requests to the GATT server discussed on a separate webpage. As discussed on that webpage, a GATT client has no prior knowledge of the information stored on the server to which it connects, but it does have an understanding of the overall structure and layout of that server. This shared understanding of the organization of information in a GATT database (into services, characteristics, etc.) allows for the client to ask questions that the server understands. By asking a series of questions ("which services do you offer?", "which characteristics are included in this service?", etc.) the client can build a local model of the server with which it's communicating.
This webpage describes the implementation of a client, demonstrated in the video below, that performs this querying process and that generates a command-line representation of the server's database using ASCII escape sequences. The client allows for the user to enable/disable notifications on characteristics, to issue write requests to characteristics that offer write permissions, and to issue read requests to the characteristics with read permissions. All of this is implemented using BTstack, the Bluetooth stack implementation used by the Pi Pico W. The webpage first discusses the characteristic discovery process, and then the client user interface.
These materials have been assembled for students in ECE 4760, though I hope that they might be useful to hobbyists building a Bluetooth interface among Pi Pico W devices.
Before you read this webpage, please read the webpage on the GATT server. The server webpage provides a brief overview of the Bluetooth stack and describes the structure of a GATT database in detail. This document assumes that the reader already has an understanding of these topics, to the depth that the server webpage describes them.
Our GATT client builds a local model of the GATT server by asking it a series of questions. This "series of questions" takes the form of a state machine. Before getting into the details of this state machine, let us have a brief conceptual discussion about the nature of the questions that the client will ask the server. Hopefully, this provides a conceptual handle that we can grab onto as we wade into the weeds of implementation.
The client receives GAP advertisements from peripheral devices, and searches those advertisements for the presence of the service that it's looking for. Once it finds it, it connects to the peripheral and builds its local model of the server. This process is summarized below. Each of the items in this enumerated list will be elaborated upon later in this document.
You can think about each item in this list as filling in gaps in the local version of the attribute database that the client is building. As discussed on the GATT server page, these attribute databases are well reprsented as tables. The particular attribute database that this GATT client will reconstruct is copied below from the GATT server page. As we move through each step above, I will include a version of this table that only includes the information that the client has been able to fill in to that point.
The database that our client will discover:
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x0A | 0x02 (Read-only) | 0x04 | 0x2800 (UUID for primary service) | 0x1801 (UUID for GATT service) |
0x0D | 0x02 (Read-only) | 0x05 | 0x2803 (UUID for characteristic declaration) | 0x2B2A (UUID for database hash) |
0x18 | 0x02 (Read-only) | 0x06 | 0x2B2A (UUID for database hash) | [128-bit hash value computed from database structure] |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | [No value, dynamic characteristic!] |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0A | 0x2902 (UUID for client characteristic configuration) | 0x0000 (notifications/indications off) |
0x08 | 0x10a (Read, write, dynamic) | 0x0B | 0x2901 (UUID for characteristic user description) | [No value, dynamic characteristic!] |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | [No value, dynamic characteristic!] |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0E | 0x2902 (UUID for client characteristic configuration) | 0x0000 (notifications/indications off) |
0x08 | 0x10a (Read, write, dynamic) | 0x0F | 0x2901 (UUID for characteristic user description) | [No value, dynamic characteristic!] |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | [No value, dynamic characteristic!] |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x12 | 0x2902 (UUID for client characteristic configuration) | 0x0000 (notifications/indications off) |
0x08 | 0x10a (Read, write, dynamic) | 0x13 | 0x2901 (UUID for characteristic user description) | [No value, dynamic characteristic!] |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | [No value, dynamic characteristic!] |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x16 | 0x2902 (UUID for client characteristic configuration) | 0x0000 (notifications/indications off) |
0x08 | 0x10a (Read, write, dynamic) | 0x17 | 0x2901 (UUID for characteristic user description) | [No value, dynamic characteristic!] |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | [No value, dynamic characteristic!] |
0x08 | 0x10a (Read, write, dynamic) | 0x1A | 0x2901 (UUID for characteristic user description) | [No value, dynamic characteristic!] |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) | [No value, dynamic characteristic!] |
0x08 | 0x10a (Read, write, dynamic) | 0x1D | 0x2901 (UUID for characteristic user description) | [No value, dynamic characteristic!] |
There may be many peripheral devices sending GAP advertisements that the client will receive. The client chooses a peripheral for connection based on the contents of its advertising packet. In particular, it looks for an advertisement packet that contains the AD Type associated with a complete list of 16-bit service class UUID's, and that has the 16-bit UUID 0xFF10 associated with it. See here for a description of the advertising packet construction on the server side, and see below for the code which we use to parse the received packets for the custom service.
This function takes, as its arguments, a 16-bit value which represents the UUID for which it is searching and a pointer to the character array that is the received advertising packet. It parses the packet's AD Types, sizes, and data, and it returns true in the event that it finds the AD Type and data that we're looking for.
static bool advertisement_report_contains_service(uint16_t service, uint8_t *advertisement_report){
// get advertisement from report event
const uint8_t * adv_data = gap_event_advertising_report_get_data(advertisement_report);
uint8_t adv_len = gap_event_advertising_report_get_data_length(advertisement_report);
// iterate over advertisement data
ad_context_t context;
for (ad_iterator_init(&context, adv_len, adv_data) ; ad_iterator_has_more(&context) ; ad_iterator_next(&context)){
uint8_t data_type = ad_iterator_get_data_type(&context);
uint8_t data_size = ad_iterator_get_data_len(&context);
const uint8_t * data = ad_iterator_get_data(&context);
switch (data_type){
case BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS:
for (int i = 0; i < data_size; i += 2) {
uint16_t type = little_endian_read_16(data, i);
if (type == service) return true;
}
default:
break;
}
}
return false;
}
The context from which this function gets called is the HCI packet handler. This is the packet handler which manages events being emitted from the HCI layer of the Bluetooth stack, and this is where our discussion of the GATT client state machine begins. This packet handler manages events which occur before connection with a peripheral device. After the system boots up, a BTSTACK_EVENT_STATE
event will be emitted and received by the packet handler. Upon receipt, the packet handler starts the client (initializes state
to TC_W4_SCAN_RESULT
and starts the GAP scanner). This packet handler then manages events associated with GAP advertising reports. As you can see, it confirms that the system is in the correct state, then calls the helper function above to parse the received advertisement for the custom service UUID. If it finds it, then it gathers the address of the sender, performs a state transition to TC_W4_CONNECT
, stops the GAP scanner, and instantiates the connection process with the peripheral.
The subsequent state waits for the connection process to complete before retrieving the HCI connection handle, performing a state transition, and asking the first question to our server. The client calls gatt_client_discover_primary_services_by_uuid128(handle_gatt_client_event, connection_handle, service_name);
. This is a request to the server to confirm that it offers the primary service specified by the 128-bit custom UUID that we pass in as the third argument. We will parse the server's response in our GATT packet handler, where the state machine continues. We also perform a state transition to TC_W4_SERVICE_RESULT
.
(Please note that the final case in the packet handler below manages disconnection events).
// Callback function for HCI events
static void hci_event_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(size);
UNUSED(channel);
bd_addr_t local_addr;
if (packet_type != HCI_EVENT_PACKET) return;
// Retrieve event type
uint8_t event_type = hci_event_packet_get_type(packet);
switch(event_type){
// Starting up
case BTSTACK_EVENT_STATE:
if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) {
gap_local_bd_addr(local_addr);
printf("BTstack up and running on %s.\n", bd_addr_to_str(local_addr));
client_start();
} else {
state = TC_OFF;
}
break;
// We've received an advertising report!
case GAP_EVENT_ADVERTISING_REPORT:
// We're placed into this state by client_start(), above
if (state != TC_W4_SCAN_RESULT) return;
// Confirm that this advertising report includes our custom service UUID
if (!advertisement_report_contains_service(CUSTOM_SERVICE, packet)) return;
// Store the address of the sender, and the type of the server
gap_event_advertising_report_get_address(packet, server_addr);
server_addr_type = gap_event_advertising_report_get_address_type(packet);
// Perform a state transition
state = TC_W4_CONNECT;
// Stop scanning
gap_stop_scan();
// Print a little message
printf("Connecting to device with addr %s.\n", bd_addr_to_str(server_addr));
// Connect to the server that sent the advertising report
gap_connect(server_addr, server_addr_type);
break;
case HCI_EVENT_LE_META:
// Wait for connection complete
switch (hci_event_le_meta_get_subevent_code(packet)) {
case HCI_SUBEVENT_LE_CONNECTION_COMPLETE:
// Confirm that we are in the proper state
if (state != TC_W4_CONNECT) return;
// Retrieve connection handle from packet
connection_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
// initialize gatt client context with handle, and add it to the list of active clients
// query primary services
// Search for the custom service which was advertised
DEBUG_LOG("Search for custom service.\n");
// Perform state transition (ATT state machine above)
state = TC_W4_SERVICE_RESULT;
// Search for our custom service by its 128-bit UUID
gatt_client_discover_primary_services_by_uuid128(handle_gatt_client_event, connection_handle, service_name);
break;
default:
break;
}
break;
case HCI_EVENT_DISCONNECTION_COMPLETE:
// unregister listener
connection_handle = HCI_CON_HANDLE_INVALID;
if (listener_registered){
listener_registered = false;
gatt_client_stop_listening_for_characteristic_value_updates(¬ification_listener);
}
printf("Disconnected %s\n", bd_addr_to_str(server_addr));
// Turn off the LED to indicate disconnection to server
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
// Initialize relevant variables
k = 0 ;
k2 = 0 ;
num_characteristics = 0 ;
if (state == TC_OFF) break;
// Start looking for another server
client_start();
break;
default:
break;
}
}
After we send a primary service discovery request to the server, the server will respond with the start/end handles and UUID of the service that we've asked about. This response comes in the form of a packet, which gets emitted to our GATT client event packet handler. This packet uses the BTstack API function hci_event_packet_get_type(packet) ;
to extract the type of the packet, and then it implements a state machine based on that packet type.
The full state machine is linked here. In order to avoid confusing this discussion with massive blocks of code, I'll only include the relevant pieces of this state machine inline in this document. In the snippet below, you'll note that the GATT client event packet handler is called by BTstack with the HCI channel identifier, a pointer to the packet (a character array), and the size of the packet. Before entering the state machine, the packet handler extracts the packet type from the packet. Then, it enters a state machine and takes an action based on its present state. Recall that the HCI packet handler placed us into state TC_W4_SERVICE_RESULT
.
The GATT server will actually respond to our our service discovery request with two packets. The first is of type GATT_EVENT_SERVICE_QUERY_RESULT
, and the packet payload contains the information of relevance about the primary service. We store this information in an object of type gatt_client_service_t
, a BTstack structure with fields for the start/end handles of the service and its UUID. The second packet that we receive will be of type GATT_EVENT_QUERY_COMPLETE
, which indicates that the server has finished the previous request. In this case, we confirm that we haven't had any ATT errors, turn on an LED, initialize some variables, and perform a state transition to TC_W4_CHARACTERISTIC_RESULT
. Importantly, we also issue a second request to the server. This one is gatt_client_discover_characteristics_for_service(handle_gatt_client_event, connection_handle, &server_service);
, which translates to "please tell me all the characteristics that the primary service contains.
// Callback function which manages GATT events. Implements a state machine.
static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(packet_type);
UNUSED(channel);
UNUSED(size);
// Local variables for storing ATT status and type of packet
uint8_t att_status;
uint8_t type_of_packet ;
// What type of packet did we just receive?
type_of_packet = hci_event_packet_get_type(packet) ;
.
.
.
// Packet was not a notification, enter state machine
switch(state){
// We've been placed into this state by the HCI callback machine, which
// has connected to a GATT server and has queried for our custom service UUID
case TC_W4_SERVICE_RESULT:
switch(type_of_packet) {
// This packet contains the result of the service query. Store the service.
case GATT_EVENT_SERVICE_QUERY_RESULT:
// store service (we expect only one)
DEBUG_LOG("Storing service\n");
gatt_event_service_query_result_get_service(packet, &server_service);
break;
// Finished with query, look for our custom characteristic
case GATT_EVENT_QUERY_COMPLETE:
// Check ATT status to make sure we don't have any errors
att_status = gatt_event_query_complete_get_att_status(packet);
if (att_status != ATT_ERROR_SUCCESS){
printf("SERVICE_QUERY_RESULT, ATT Error 0x%02x.\n", att_status);
gap_disconnect(connection_handle);
break;
}
// Turn on the LED to indicate connection to server
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
// Clear all notification information
memset(notifications_enabled, -1, MAX_CHARACTERISTICS) ;
// Transition to next state
state = TC_W4_CHARACTERISTIC_RESULT;
DEBUG_LOG("Search for counting characteristic.\n");
// Discover all characteristics contained within custom service.
gatt_client_discover_characteristics_for_service(handle_gatt_client_event, connection_handle, &server_service);
break;
default:
break;
}
break;
.
.
.
}
.
.
.
}
}
As we transition out of this state, after having made a connection to the server and issuing the primary service discovery request, our understanding of the contents of the remote database looks like that which is shown below. Via connection, we've discovered the GAP service and the device name characteristic contained therein. After our primary service discovery, we also know the handle for our custom primary service of interest. We haven't discovered the GATT service or database hash characteristic, so I've omitted them from the table below.
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
The server responds to our primary service characteristic discovery request with a sequence of packets, each of type GATT_EVENT_CHARACTERISTIC_QUERY_RESULT
. Each of these packets contains the start/end handles and UUID's of one of the characteristics contained in the primary service. The final packet that we receive will again be of type GATT_EVENT_QUERY_COMPLETE
to indicate that the server is finished responding to our discovery request. As we receive each characteristic's start/end handles and UUID's, we store that information in an object of type gatt_client_characteristic_t
. This BTstack object contains fields for the start/end handles of each of our characteristics, each characteristic's value handle, its properties (does the characteristic allow for notification/indication?), and its UUID.
The relevant state from the state machine is included below. We log each characteristic that we discover in an array of gatt_client_characteristic_t
objects called server_characteristic
. Once we receive the GATT_EVENT_QUERY_COMPLETE
packet, we store the number of characteristics that we discovered, zero a couple variables, confirm that we haven't had any ATT errors, and then issue a characteristic descriptor discovery request for the first characteristic in our list. Finally, we transition state to TC_W4_CHARACTERISTIC_DESCRIPTOR
.
// We've found the custom service, and we've just sent a query for all characteristics.
// We don't know how many characteristics this service contains, we need to discover them.
case TC_W4_CHARACTERISTIC_RESULT:
switch(type_of_packet) {
// This packet contains information about a characteristic in the custom service. We'll
// keep receiving these packets until we've gotten one for each service. Then we'll get
// a GATT_EVENT_QUERY_COMPLETE packet.
case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
// Store the characteristic in a gatt_client_characteristic_t object, and increment to the next
// element in an array of such objects.
DEBUG_LOG("Storing characteristic\n");
gatt_event_characteristic_query_result_get_characteristic(packet, &server_characteristic[k++]);
break;
// We've received all characteristics!
case GATT_EVENT_QUERY_COMPLETE:
// How many characteristics did we just discover?
num_characteristics = k ;
// Reset our two incrementers
k = 0 ;
k2 = 0 ;
// Check the ATT status for errors
att_status = gatt_event_query_complete_get_att_status(packet);
if (att_status != ATT_ERROR_SUCCESS){
printf("CHARACTERISTIC_QUERY_RESULT, ATT Error 0x%02x.\n", att_status);
gap_disconnect(connection_handle);
break;
}
// Clear the terminal screen
printf("\033[2J") ;
// Discover all characteristic descriptors for all characteristics. starting with number "k" (which
// has been reset to 0 above).
gatt_client_discover_characteristic_descriptors(handle_gatt_client_event, connection_handle, &server_characteristic[k]) ;
// State transition
state = TC_W4_CHARACTERISTIC_DESCRIPTOR ;
break;
default:
break;
}
break;
Upon leaving this state, we have discovered the handles for each of our characteristics and each of our characteristic values, but we've not yet learned the descriptors for those characteristics, or their value. At this point, our understanding of the remote database looks like the following. We know the handles and access permissions for our characteristics, but we don't yet know their values or descriptors.
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) |
For each characteristic descriptor discovery request that we issue, the server responds to our primary service characteristic discovery request with a sequence of packets, each of type GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT. Each of these packets contains the handles and UUID's of one of the characteristic descriptors contained in the characteristic in question. The final packet that we receive will again be of type GATT_EVENT_QUERY_COMPLETE to indicate that the server is finished responding to our discovery request. As we receive each characteristic descriptor's handle and UUID, we store that information in an object of type gatt_client_characteristic_descriptor_t. This BTstack object contains fields for the handle of each of our characteristic descriptors and its UUID.
We will issue one of these requests for each of the characteristics that we discovered in the previous state. The relevant piece of the state machine is included below. After we have learned the descriptors for every characteristic, we go back to the first one and issue a gatt_client_read_characteristic_descriptor
request to the descriptor with the UUID associated with the characteristic user description. We also state transition to TC_W4_CHARACTERISTIC_DESCRIPTOR_PRINT
.
// We're querying each characteristic for its descriptors.
case TC_W4_CHARACTERISTIC_DESCRIPTOR:
switch(type_of_packet) {
// We've just received another descriptor for characteristic k. Store it in an array of gatt_characteristic_descriptor objects, and then
// increment to the next element in an array of such objects.
case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT:
DEBUG_LOG("Storing characteristic descriptor\n") ;
gatt_event_all_characteristic_descriptors_query_result_get_characteristic_descriptor(packet, &server_characteristic_descriptor[k][k2++]) ;
break ;
// We've received all descriptors for characteristic k.
case GATT_EVENT_QUERY_COMPLETE:
DEBUG_LOG("Transitioning to Descriptor Print\n") ;
// Increment k to the next characteristic
k++ ;
// Reset k2 (used to count characteristic descriptors) back to 0.
k2 = 0 ;
// If we haven't yet queried all of the characteristics that we discovered, query the next characteristic for all its descriptors.
// We stay in this state to receive these next descriptors.
if (k < num_characteristics) {
gatt_client_discover_characteristic_descriptors(handle_gatt_client_event, connection_handle, &server_characteristic[k]) ;
}
// If we have received all of the descriptors for all of our characteristics . . .
else {
// Reset our characteristic counter to 0.
k = 0 ;
// For characteristic k (reset to 0), query the value of the descriptor with UUID CHARACTERISTIC_USER_DESCRIPTION.
// This descriptor contains the plaintext user description of this characteristic. We're not sure if this is the first or the second
// descriptor for the characteristic, so check the UUID of each and read the correct one.
if (server_characteristic_descriptor[k][0].uuid16 == CHARACTERISTIC_USER_DESCRIPTION) {
gatt_client_read_characteristic_descriptor(handle_gatt_client_event, connection_handle, &server_characteristic_descriptor[k][0]) ;
}
else {
gatt_client_read_characteristic_descriptor(handle_gatt_client_event, connection_handle, &server_characteristic_descriptor[k][1]) ;
}
// Move to next state.
state = TC_W4_CHARACTERISTIC_DESCRIPTOR_PRINT ;
}
break ;
default:
break ;
}
break ;
Upon leaving this state, we have learned the UUID's and handles associated with all characteristic descriptors. Our understanding of the remote database now looks like the following. We know the handles of all the characteristics and their descriptors, but we've yet to learn the values of any of these attributes.
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0A | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x0B | 0x2901 (UUID for characteristic user description) | |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0E | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x0F | 0x2901 (UUID for characteristic user description) | |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x12 | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x13 | 0x2901 (UUID for characteristic user description) | |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x16 | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x17 | 0x2901 (UUID for characteristic user description) | |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | |
0x08 | 0x10a (Read, write, dynamic) | 0x1A | 0x2901 (UUID for characteristic user description) | |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) | |
0x08 | 0x10a (Read, write, dynamic) | 0x1D | 0x2901 (UUID for characteristic user description) |
The server responds to each gatt_client_read_characteristic_descriptor
request that we issue with two packets. The first is of type GATT_EVENT_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT
and contains the user description (a plaintext character array description of the characteristic), and the second is of type GATT_EVENT_QUERY_COMPLETE
to indicate that the server has finished that request. The client issues one of these requests for each of the characteristics, thereby learning the value of each characteristic user description attribute.
After getting through all characteristics, the state machine resets its iterating variables to return to the first characteristic in our list. It changes the state to TC_W4_CHARACTERISTIC_VALUE_PRINT
, and issues the request gatt_client_read_value_of_characteristic
for the first characteristic.
// We'd like to locally store the user descriptions for each characteristic. We'll do so in this state.
case TC_W4_CHARACTERISTIC_DESCRIPTOR_PRINT:
switch(type_of_packet) {
// The result of the characteristic descriptor query for the CHARACTERISTIC_USER_DESCRIPTION. Parse this packet,
// and store the contents in a global array of descriptors.
case GATT_EVENT_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT:
DEBUG_LOG("Storing characteristic descriptor\n") ;
// The user description is a character array. How long is it?
descriptor_length = gatt_event_characteristic_descriptor_query_result_get_descriptor_length(packet);
// What is the address of the start of this array?
const uint8_t *descriptor = gatt_event_characteristic_descriptor_query_result_get_descriptor(packet);
// Store the user description in a global array of characteristic descriptions.
memcpy(server_characteristic_user_description[k], descriptor, descriptor_length) ;
// Null-terminate the character array.
server_characteristic_user_description[k][descriptor_length] = 0 ;
break ;
// Finished with read for this particular characteristic, do we need to read more?
case GATT_EVENT_QUERY_COMPLETE:
// Increment k to the next characteristic
k++ ;
// Are there still characteristics remaining? If so . . .
if (k < num_characteristics) {
// Read the user description for the next characteristic, just like before.
if (server_characteristic_descriptor[k][0].uuid16 == CHARACTERISTIC_USER_DESCRIPTION) {
gatt_client_read_characteristic_descriptor(handle_gatt_client_event, connection_handle, &server_characteristic_descriptor[k][0]) ;
}
else {
gatt_client_read_characteristic_descriptor(handle_gatt_client_event, connection_handle, &server_characteristic_descriptor[k][1]) ;
}
}
// If not . . .
else {
// Reset both the characteristic and characteristic descriptor iterators
k = 0 ;
k2 = 0 ;
// Read the value of characteristic k (reset to 0)
gatt_client_read_value_of_characteristic(handle_gatt_client_event, connection_handle, &server_characteristic[k]) ;
// Transition state
state = TC_W4_CHARACTERISTIC_VALUE_PRINT ;
}
break ;
default:
break ;
}
break ;
Upon leaving this state, we've learned the values for each of our characteristic descriptor attributes:
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0A | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x0B | 0x2901 (UUID for characteristic user description) | "Read-only counter" |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0E | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x0F | 0x2901 (UUID for characteristic user description) | "DDS Frequency" |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x12 | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x13 | 0x2901 (UUID for characteristic user description) | "String from Pico" |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x16 | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x17 | 0x2901 (UUID for characteristic user description) | "String to Pico" |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | |
0x08 | 0x10a (Read, write, dynamic) | 0x1A | 0x2901 (UUID for characteristic user description) | "LED Status and Control" |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) | |
0x08 | 0x10a (Read, write, dynamic) | 0x1D | 0x2901 (UUID for characteristic user description) | "Color Selection" |
The server responds to each gatt_client_read_value_of_characteristic request that we issue with two packets. The first is of type GATT_EVENT_CHARACTERISTIC_VALUE_QUERY_RESULT and contains the characteristic value, and the second is of type GATT_EVENT_QUERY_COMPLETE to indicate that the server has finished that request. The client issues one of these requests for each of the characteristics, thereby learning the value of each characteristic.
After getting through all characteristics, the state machine resets its iterating variables to return to the first characteristic in our list. It searches the characteristic descriptors for occurrences of the characteristic user configuration descriptor and, when it finds it, it issues a read request to that characteristic and transition state to TC_W4_CHARACTERISTIC_NOTIFICATIONS. If it never finds such a characteristic descriptor, it transitions to state TC_W4_READY, where it remains.
// We'd like to read the value of each characteristic. We'll do so in this state.
case TC_W4_CHARACTERISTIC_VALUE_PRINT:
switch(type_of_packet) {
// We've just received the result of a characteristic value query. This packet contains that value
case GATT_EVENT_CHARACTERISTIC_VALUE_QUERY_RESULT:
DEBUG_LOG("Storing characteristic value\n") ;
// The characteristic value is a character array. How long is it?
descriptor_length = gatt_event_characteristic_value_query_result_get_value_length(packet);
// What is the address to the start of this array?
const uint8_t *descriptor = gatt_event_characteristic_value_query_result_get_value(packet);
// Store the characteristic value in a global array of characteristic values
memcpy(server_characteristic_values[k], descriptor, descriptor_length) ;
// Null-terminate the character array.
server_characteristic_values[k][descriptor_length] = 0 ;
break ;
// We've finished receiving the value for a particular characteristic
case GATT_EVENT_QUERY_COMPLETE:
// Move to the next characteristic
k++ ;
// Do we still have more characteristics to query for value? If so . . .
if (k < num_characteristics) {
// Read the next one!
gatt_client_read_value_of_characteristic(handle_gatt_client_event, connection_handle, &server_characteristic[k]) ;
}
// If not . . .
else {
k = 0 ;
k2 = 0 ;
// Find the next characteristic with a descriptor.
while((k<num_characteristics) && (server_characteristic_descriptor[k][0].uuid16!=CHARACTERISTIC_CONFIGURATION)) {
k++ ;
}
// Have we gone thru all characteristics? If not, read the next configuration
if (k<num_characteristics) {
DEBUG_LOG("QUERYING FOR CONFIGURATION: %d\n", k) ;
gatt_client_read_characteristic_descriptor(handle_gatt_client_event, connection_handle, &server_characteristic_descriptor[k][0]) ;
// Transition state
state = TC_W4_CHARACTERISTIC_NOTIFICATIONS ;
}
// If so, move to next state
else {
// Signal thread that all characteristics have been acquired
PT_SEM_SAFE_SIGNAL(pt, &characteristics_discovered) ;
// Transition to an idle state
state = TC_W4_READY ;
// Turn on notifications
// Register handler for notifications
listener_registered = true;
gatt_client_listen_for_characteristic_value_updates(¬ification_listener, handle_gatt_client_event, connection_handle, NULL);
}
}
break ;
default:
break ;
}
break ;
Upon leaving this state, the client's understanding of the remote server looks like the following:
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | "1" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0A | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x0B | 0x2901 (UUID for characteristic user description) | "Read-only counter" |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | "400" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0E | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x0F | 0x2901 (UUID for characteristic user description) | "DDS Frequency" |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | "" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x12 | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x13 | 0x2901 (UUID for characteristic user description) | "String from Pico" |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | "" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x16 | 0x2902 (UUID for client characteristic configuration) | |
0x08 | 0x10a (Read, write, dynamic) | 0x17 | 0x2901 (UUID for characteristic user description) | "String to Pico" |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | "OFF" |
0x08 | 0x10a (Read, write, dynamic) | 0x1A | 0x2901 (UUID for characteristic user description) | "LED Status and Control" |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) | "15" |
0x08 | 0x10a (Read, write, dynamic) | 0x1D | 0x2901 (UUID for characteristic user description) | "Color Selection" |
The client continues to search characteristic descriptors for the UUID associated with characteristic configuration. It moves through the characteristics in order, issuing a read request to that descriptor each time it finds one. The server responds to these reqests with two packets. The first is of type GATT_EVENT_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT and contains the value of the descriptor (0 for notifications disabled, 1 for notifications enabled). The second packet is of type GATT_EVENT_QUERY_COMPLETE and indicates that the server has finished with the latest request.
After getting through all characteristics, the client has fully populated its local version of the server database. It signals a Protothread via a semaphore that all characteristics have been discovered, which starts the user interface.
// Obtain the notification status of each characteristic
case TC_W4_CHARACTERISTIC_NOTIFICATIONS:
switch(type_of_packet) {
// Received the result of a notification query
case GATT_EVENT_CHARACTERISTIC_DESCRIPTOR_QUERY_RESULT:
DEBUG_LOG("Storing characteristic notification status\n") ;
// The characteristic value is a character array. How long is it?
descriptor_length = gatt_event_characteristic_value_query_result_get_value_length(packet);
// What is the address to the start of this array?
const uint8_t *descriptor = gatt_event_characteristic_value_query_result_get_value(packet);
// Store the characteristic configuration
server_characteristic_configurations[k] = little_endian_read_16(descriptor, 0) ;
// Set the notifications as enabled or disabled
if ((char)server_characteristic_configurations[k]) {
notifications_enabled[k] = 1 ;
}
else {
notifications_enabled[k] = 0 ;
}
break ;
// Finished with a query. Do we need to do another?
case GATT_EVENT_QUERY_COMPLETE:
// Increment k one time
k++ ;
// Find the next characteristic with a descriptor.
while((k<num_characteristics) && (server_characteristic_descriptor[k][0].uuid16!=CHARACTERISTIC_CONFIGURATION)) {
k++ ;
}
// Have we gone thru all characteristics? If not, read the next configuration
if (k<num_characteristics) {
gatt_client_read_characteristic_descriptor(handle_gatt_client_event, connection_handle, &server_characteristic_descriptor[k][0]) ;
}
// If so, move to next state
else {
// Signal thread that all characteristics have been acquired
PT_SEM_SAFE_SIGNAL(pt, &characteristics_discovered) ;
// Transition to an idle state
state = TC_W4_READY ;
// Turn on notifications
// Register handler for notifications
listener_registered = true;
gatt_client_listen_for_characteristic_value_updates(¬ification_listener, handle_gatt_client_event, connection_handle, NULL);
}
break ;
default:
break ;
}
break ;
The client has complete knowledge of the server's attribute database upon leaving this state:
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | "1" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0A | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x0B | 0x2901 (UUID for characteristic user description) | "Read-only counter" |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | "400" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0E | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x0F | 0x2901 (UUID for characteristic user description) | "DDS Frequency" |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | "" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x12 | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x13 | 0x2901 (UUID for characteristic user description) | "String from Pico" |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | "" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x16 | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x17 | 0x2901 (UUID for characteristic user description) | "String to Pico" |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | "OFF" |
0x08 | 0x10a (Read, write, dynamic) | 0x1A | 0x2901 (UUID for characteristic user description) | "LED Status and Control" |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) | "15" |
0x08 | 0x10a (Read, write, dynamic) | 0x1D | 0x2901 (UUID for characteristic user description) | "Color Selection" |
In the event that notifications are enabled for a particular characteristic, the server will autonomously send updated characteristic values to the client. Our client must receive those notifications, parse them for the characteristic handle for which they apply, and update the local database accordingly. The code snippet below accomplishes this, and lives outside the rest of the state machine. That is to say, the code below runs every time the client receives a packet of type GATT_EVENT_NOTIFICATION, regardless of the value of the state.
// Did we just receive a notification? If so, update the associated characteristic value
// and signal the protothread which refreshes values in the terminal UI.
if (type_of_packet == GATT_EVENT_NOTIFICATION) {
// How many bytes in the payload?
uint32_t value_length = gatt_event_notification_get_value_length(packet);
// What is the value handle for the notification we've just received
uint16_t notification_handle = gatt_event_notification_get_value_handle(packet) ;
// What is the address of the start of the data for this packet (a byte array)
const uint8_t *value = gatt_event_notification_get_value(packet);
// Initialize our characteristic identifier to -1 (invalid)
int which_characteristic = -1 ;
// Loop thru each characteristic, compare that characteristic's value handle to the value
// handle of the notification we've received. A match indicates that this is a notification
// for the associated characteristic.
for (int i=0; i<num_characteristics; i++) {
if (notification_handle==server_characteristic[i].value_handle) {
which_characteristic = i ;
}
}
// Did we find a value handle match?
if (which_characteristic>=0) {
// If so, copy the received data to the value buffer of the associated characteristic
memcpy(server_characteristic_values[which_characteristic], value, value_length) ;
// Null-terminate the character array
server_characteristic_values[which_characteristic][value_length] = 0 ;
// Make sure notifications are labeled as on for this characteristic
// notifications_enabled[which_characteristic] = 1 ;
// Semaphore-signal the protothread which will refresh values in the UI
PT_SEM_SAFE_SIGNAL(pt, &characteristics_discovered) ;
}
// Exit the callback
return;
}
As an example, suppose that characteristics have been enabled for characteristic A (the "Read-only counter," and suppose that the value of this characteristic has been updated from 1 to 2. On the client, our local database will look like the following after having received and parsed the notification from the server.
Length (bytes) | Access permissions | Handle | Type | Value |
---|---|---|---|---|
0x0A | 0x02 (Read-only) | 0x01 | 0x2800 (UUID for primary service) | 0x1800 (UUID for GAP service) |
0x0D | 0x02 (Read-only) | 0x02 | 0x2803 (UUID for characteristic declaration) | 0x2A00 (UUID for device name) |
0x13 | 0x02 (Read-only) | 0x03 | 0x2A00 (UUID for device name) | "PICO_SERVER" |
0x18 | 0x02 (Read-only) | 0x07 | 0x2800 (UUID for primary service) | 0x0000FF1000001000800000805F9B34FB (custom service UUID) |
0x1B | 0x02 (Read-only) | 0x08 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic A |
0x16 | 0x302 (Read-only, dynamic, long UUID) | 0x09 | 0x0000FF1100001000800000805F9B34FB (custom characteristic UUID) | "2" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0A | 0x2902 (UUID for client characteristic configuration) | 1 |
0x08 | 0x10a (Read, write, dynamic) | 0x0B | 0x2901 (UUID for characteristic user description) | "Read-only counter" |
0x1B | 0x02 (Read-only) | 0x0C | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic B |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x0D | 0x0000FF1200001000800000805F9B34FB (custom characteristic UUID) | "400" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x0E | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x0F | 0x2901 (UUID for characteristic user description) | "DDS Frequency" |
0x1B | 0x02 (Read-only) | 0x10 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic C |
0x16 | 0x302 (Read, dynamic, long UUID) | 0x11 | 0x0000FF1300001000800000805F9B34FB (custom characteristic UUID) | "" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x12 | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x13 | 0x2901 (UUID for characteristic user description) | "String from Pico" |
0x1B | 0x02 (Read-only) | 0x14 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic D |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x15 | 0x0000FF1400001000800000805F9B34FB (custom characteristic UUID) | "" |
0x0A | 0x10e (Read, write without response, write, dynamic) | 0x16 | 0x2902 (UUID for client characteristic configuration) | 0 |
0x08 | 0x10a (Read, write, dynamic) | 0x17 | 0x2901 (UUID for characteristic user description) | "String to Pico" |
0x1B | 0x02 (Read-only) | 0x18 | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic E |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x19 | 0x0000FF1500001000800000805F9B34FB (custom characteristic UUID) | "OFF" |
0x08 | 0x10a (Read, write, dynamic) | 0x1A | 0x2901 (UUID for characteristic user description) | "LED Status and Control" |
0x1B | 0x02 (Read-only) | 0x1B | 0x2803 (UUID for characteristic declaration) | Access permissions, handle, and 128-bit UUID of custom characteristic F |
0x16 | 0x306 (Read, write without response, dynamic, long UUID) | 0x1C | 0x0000FF1600001000800000805F9B34FB (custom characteristic UUID) | "15" |
0x08 | 0x10a (Read, write, dynamic) | 0x1D | 0x2901 (UUID for characteristic user description) | "Color Selection" |
The GATT client demonstration includes a command-line user application that allows for the user to visualize the contents of the database, enable/disable notifications for characteristics, issue read requests for particular characteristics, and issue write requests for particular characteristics. This application is implemented by means of two Protothreads. One of the protothreads writes all characteristic values to the screen whenever it is semaphore-signalled to do so, and the other implements a command-line user interface.
In the final state of the server discovery state machine discussed above, a semaphore is used to trigger the thread below to run once. This thread uses ASCII escape sequences to clear the terminal screen and write all the characteristic handles, user descriptions, and values. After having done so, it waits until it is semaphore-signalled again to repeat the process. This will only occur if the user interface thread resets the server discovery state machine, or if the client receives a notification from the server.
// Protothread that prints all characteristic values
static PT_THREAD (protothread_client(struct pt *pt))
{
PT_BEGIN(pt) ;
// For incrementing through characteristics
static int counter = 0 ;
while(1) {
// Wait to be signalled by the GATT state machine
PT_SEM_SAFE_WAIT(pt, &characteristics_discovered) ;
// Save cursor position
printf("\033[s") ;
// Move cursor to 10th row
printf("\033[10;0H") ;
// Make cursor invisible
printf("\033[?25l") ;
// Print all characteristic values
printf("Discovered characteristics:\n\n") ;
for (counter = 0; counter < num_characteristics; counter++) {
printf("Characteristic ID.:\033[K\t %c\n", (counter+97)) ;
printf("User description:\033[K\t %s\n", server_characteristic_user_description[counter]) ;
// printf("Config:\033[K\t\t\t %04x\n", notifications_enabled[counter]) ;
printf("Access permissions:\033[K\t ") ;
// Only print notification status if characteristic allows for notifications
parsePermissions(server_characteristic[counter].properties) ;
if ((notifications_enabled[counter]>=0) && (notifications_enabled[counter]<2)) {
if (notifications_enabled[counter]) printf("\033[1mNotify:\033[K\t\t\t ON\033[22m\n") ;
else printf("Notify:\033[K\t\t\t OFF\n") ;
}
printf("Value:\033[K\t\t\t %s\n\n", server_characteristic_values[counter]) ;
}
// Erase down
printf("\033[J") ;
// Restore cursor position
printf("\033[s") ;
}
PT_END(pt) ;
}
The second protothread implements a command-line user interface that allows for the user to interact with the GATT client and server. In particular, the user may request to read the values of all characteristics on the server (which will then be updated in the terminal window). The user may also enable/disable notifications for characteristics which allow notifications (if they attempt to do so on a characteristic that does not allow notifications, they receive an error message). And the user may use the serial terminal to update the values of characteristics with write permisssions.
In order to enable/disable notifications, the thread resets the server discovery state machine state to TC_W4_ENABLE_NOTIFICATIONS_COMPLETE, and then uses the BTstack function gatt_client_write_client_characteristic_configuration
to turn notifications on or off for a particular characteristic. In order to write to a characteristic, the thread uses gatt_client_write_value_of_characteristic_without_response
. It then resets the state to TC_W4_CHARACTERISTIC_VALUE_PRINT and re-reads all characteristic values.
// Protothread which implements a user interface
static PT_THREAD (protothread_ui(struct pt *pt))
{
PT_BEGIN(pt) ;
// For holding user input
static char temp_var ;
static char temp_var2 ;
while(1) {
// Move cursor to line above data
printf("\033[10;0H") ;
// Erase to top
printf("\033[1J") ;
// Move cursor to top
printf("\033[H") ;
// Print user instructions
sprintf(pt_serial_out_buffer, "Type 0 to refresh characteristic values.\n\r") ;
serial_write ;
sprintf(pt_serial_out_buffer, "Type the ID of a characteristic to write.\n\r") ;
serial_write ;
sprintf(pt_serial_out_buffer, "Type the ID of a characteristic (UPPERCASE) to toggle notifications on/off.\n\r") ;
serial_write ;
// Wait for user input
serial_read ;
// Parse user input
temp_var = pt_serial_in_buffer[0]-97 ;
temp_var2 = pt_serial_in_buffer[0]-65 ;
// Did the user specify a valid (lowercase) characteristic ID?
if ((temp_var>=0) && (temp_var<num_characteristics)) {
// Does this characteristic include write permissions?
if (server_characteristic[temp_var].properties & (1u<<2)) {
// Move cursor to row 4
printf("\033[4;0H") ;
// Prompt for user input
sprintf(pt_serial_out_buffer, "Please type a value for characteristic %c.\n\r\033[K", temp_var+97) ;
serial_write ;
serial_read ;
// Send user input to server
int status = gatt_client_write_value_of_characteristic_without_response(connection_handle, server_characteristic[temp_var].value_handle, strlen(pt_serial_in_buffer), pt_serial_in_buffer);
}
// If characteristic does not include write permissions, tell the user.
else {
printf("\033[4;0H") ;
sprintf(pt_serial_out_buffer, "\033[1mNo write permission for that characteristic.\n\r\033[22m") ;
serial_write ;
PT_YIELD_usec(500000) ;
}
}
// Did the user alternatively specify a valid (UPPERCASE) characteristic ID?
else if ((temp_var2>=0) && (temp_var2<num_characteristics)) {
// Does this characteristic include notifications?
if (server_characteristic[temp_var2].properties & (1u<<4)) {
// Wait until our state machine is ready for a new instruction
PT_YIELD_UNTIL(pt, state==TC_W4_READY) ;
// If notifications are presently enabled, disable them
if (notifications_enabled[temp_var2]) {
state = TC_W4_ENABLE_NOTIFICATIONS_COMPLETE;
gatt_client_write_client_characteristic_configuration(handle_gatt_client_event, connection_handle,
&server_characteristic[temp_var2], GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NONE);
}
// If notifications are presently disabled, enable them
else {
state = TC_W4_ENABLE_NOTIFICATIONS_COMPLETE;
gatt_client_write_client_characteristic_configuration(handle_gatt_client_event, connection_handle,
&server_characteristic[temp_var2], GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);
}
}
// If not, tell the user
else {
printf("\033[4;0H") ;
sprintf(pt_serial_out_buffer, "\033[1mNo notification permission for that characteristic.\n\r\033[22m") ;
serial_write ;
PT_YIELD_usec(500000) ;
}
}
// Wait until the state machine is ready to receive a new instruction
PT_YIELD_UNTIL(pt, state==TC_W4_READY) ;
// Reset characteristic counters
k = 0 ;
k2 = 0 ;
// Refresh all characteristic values
state = TC_W4_CHARACTERISTIC_VALUE_PRINT ;
gatt_client_read_value_of_characteristic(handle_gatt_client_event, connection_handle, &server_characteristic[k]) ;
}
PT_END(pt) ;
}
To start our GATT client, we initialize stdio, initialize our semaphore, and start the CYW43. We then initialize the L2CAP protocol, the security manager, and initialize an att server on our client (we could actually eliminate this for communicating with another Pico-based peripheral). We initialize the GATT client, which gives us access to the BTstack GATT client API, and then register our HCI packet handler. Finally, we turn on the Bluetooth device and schedule our application threads.
int main() {
// Initialize stdio
stdio_init_all();
// Make cursor invisible
printf("\033[?25l") ;
// Initialize the semaphore
PT_SEM_SAFE_INIT(&characteristics_discovered, 0) ;
// initialize CYW43 driver architecture (will enable BT if/because CYW43_ENABLE_BLUETOOTH == 1)
if (cyw43_arch_init()) {
printf("failed to initialise cyw43_arch\n");
return -1;
}
// Initialize L2CAP and Security Manager
l2cap_init();
sm_init();
sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
// setup empty ATT server - only needed if LE Peripheral does ATT queries on its own, e.g. Android and iOS
att_server_init(NULL, NULL, NULL);
// Initialize GATT client
gatt_client_init();
// Register the HCI event callback function
hci_event_callback_registration.callback = &hci_event_handler;
hci_add_event_handler(&hci_event_callback_registration);
// Turn on!
hci_power_control(HCI_POWER_ON);
// Add and schedule threads
pt_add_thread(protothread_client) ;
pt_add_thread(protothread_ui) ;
pt_sched_method = SCHED_ROUND_ROBIN ;
pt_schedule_start ;
}
The flowchart below summarizes the content above.