blob: 0e34078158f19511fdc11e4a70550e35d42ce361 [file] [log] [blame]
Characteristic Access
=====================
.. contents::
:local:
:depth: 2
Review
^^^^^^
A characteristic's access callback implements its behavior. Recall that
services and characteristics are registered with NimBLE via attribute
tables. Each characteristic definition in an attribute table contains an
*access_cb* field. The *access_cb* field is an application callback
that gets executed whenever a peer device attempts to read or write the
characteristic.
Earlier in this tutorial, we looked at how *bleprph* implements the GAP
service. Let's take another look at how *bleprph* specifies the first
few characteristics in this service.
.. code:: c
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
{
/*** Service: GAP. */
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid128 = BLE_UUID16(BLE_GAP_SVC_UUID16),
.characteristics = (struct ble_gatt_chr_def[]) { {
/*** Characteristic: Device Name. */
.uuid128 = BLE_UUID16(BLE_GAP_CHR_UUID16_DEVICE_NAME),
.access_cb = gatt_svr_chr_access_gap,
.flags = BLE_GATT_CHR_F_READ,
}, {
/*** Characteristic: Appearance. */
.uuid128 = BLE_UUID16(BLE_GAP_CHR_UUID16_APPEARANCE),
.access_cb = gatt_svr_chr_access_gap,
.flags = BLE_GATT_CHR_F_READ,
}, {
// [...]
As you can see, *bleprph* uses the same *access_cb* function for all
the GAP service characteristics, but the developer could have
implemented separate functions for each characteristic if they
preferred. Here is the *access_cb* function that the GAP service
characteristics use:
.. code:: c
static int
gatt_svr_chr_access_gap(uint16_t conn_handle, uint16_t attr_handle, uint8_t op,
union ble_gatt_access_ctxt *ctxt, void *arg)
{
uint16_t uuid16;
uuid16 = ble_uuid_128_to_16(ctxt->chr_access.chr->uuid128);
assert(uuid16 != 0);
switch (uuid16) {
case BLE_GAP_CHR_UUID16_DEVICE_NAME:
assert(op == BLE_GATT_ACCESS_OP_READ_CHR);
ctxt->chr_access.data = (void *)bleprph_device_name;
ctxt->chr_access.len = strlen(bleprph_device_name);
break;
case BLE_GAP_CHR_UUID16_APPEARANCE:
assert(op == BLE_GATT_ACCESS_OP_READ_CHR);
ctxt->chr_access.data = (void *)&bleprph_appearance;
ctxt->chr_access.len = sizeof bleprph_appearance;
break;
case BLE_GAP_CHR_UUID16_PERIPH_PRIV_FLAG:
assert(op == BLE_GATT_ACCESS_OP_READ_CHR);
ctxt->chr_access.data = (void *)&bleprph_privacy_flag;
ctxt->chr_access.len = sizeof bleprph_privacy_flag;
break;
case BLE_GAP_CHR_UUID16_RECONNECT_ADDR:
assert(op == BLE_GATT_ACCESS_OP_WRITE_CHR);
if (ctxt->chr_access.len != sizeof bleprph_reconnect_addr) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
memcpy(bleprph_reconnect_addr, ctxt->chr_access.data,
sizeof bleprph_reconnect_addr);
break;
case BLE_GAP_CHR_UUID16_PERIPH_PREF_CONN_PARAMS:
assert(op == BLE_GATT_ACCESS_OP_READ_CHR);
ctxt->chr_access.data = (void *)&bleprph_pref_conn_params;
ctxt->chr_access.len = sizeof bleprph_pref_conn_params;
break;
default:
assert(0);
break;
}
return 0;
}
After you've taken a moment to examine the structure of this function,
let's explore some details.
Function signature
^^^^^^^^^^^^^^^^^^
.. code:: c
static int
gatt_svr_chr_access_gap(uint16_t conn_handle, uint16_t attr_handle, uint8_t op,
union ble_gatt_access_ctxt *ctxt, void *arg)
A characteristic access function always takes this same set of
parameters and always returns an int. The parameters to this function
type are documented below.
+----------------+--------------+------------+
| **Parameter** | **Purpose** | **Notes** |
+================+==============+============+
| conn\_handle | Indicates | Use this |
| | which | value to |
| | connection | determine |
| | the | which peer |
| | characterist | is |
| | ic | accessing |
| | access was | the |
| | sent over. | characteri |
| | | stic. |
+----------------+--------------+------------+
| attr\_handle | The | Can be |
| | low-level | used to |
| | ATT handle | determine |
| | of the | which |
| | characterist | characteri |
| | ic | stic |
| | value | is being |
| | attribute. | accessed |
| | | if you |
| | | don't want |
| | | to perform |
| | | a UUID |
| | | lookup. |
+----------------+--------------+------------+
| op | Indicates | Valid |
| | whether this | values |
| | is a read or | are:\ *BLE |
| | write | \_GATT\_AC |
| | operation | CESS\_OP\_ |
| | | READ\_CHR* |
| | | \ \ *BLE\_ |
| | | GATT\_ACCE |
| | | SS\_OP\_WR |
| | | ITE\_CHR* |
+----------------+--------------+------------+
| ctxt | Contains the | For |
| | characterist | characteri |
| | ic | stic |
| | value | accesses, |
| | pointer that | use the |
| | the | *ctxt->chr |
| | application | \_access* |
| | needs to | member; |
| | access. | for |
| | | descriptor |
| | | accesses, |
| | | use the |
| | | *ctxt->dsc |
| | | \_access* |
| | | member. |
+----------------+--------------+------------+
The return value of the access function tells the NimBLE stack how to
respond to the peer performing the operation. A value of 0 indicates
success. For failures, the function returns the specific ATT error code
that the NimBLE stack should respond with. The ATT error codes are
defined in
`net/nimble/host/include/host/ble\_att.h <https://github.com/apache/incubator-mynewt-core/blob/master/net/nimble/host/include/host/ble_att.h>`__.
Determine characteristic being accessed
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code:: c
{
uint16_t uuid16;
uuid16 = ble_uuid_128_to_16(ctxt->chr_access.chr->uuid128);
assert(uuid16 != 0);
switch (uuid16) {
// [...]
This function uses the UUID to determine which characteristic is being
accessed. There are two alternative methods *bleprph* could have used to
accomplish this task:
- Map characteristics to ATT handles during service registration; use
the *attr\_handle* parameter as a key into this table during
characteristic access.
- Implement a dedicated function for each characteristic; each function
inherently knows which characteristic it corresponds to.
All the GAP service characteristics have 16-bit UUIDs, so this function
uses the *ble\_uuid\_128\_to\_16()* function to convert the 128-bit UUID
to its corresponding 16-bit UUID. This conversion function returns the
corresponding 16-bit UUID on success, or 0 on failure. Success is
asserted here to ensure the NimBLE stack is doing its job properly; the
stack should only call this function for accesses to characteristics
that it is registered with, and all GAP service characteristics have
valid 16-bit UUIDs.
Read access
^^^^^^^^^^^
.. code:: c
case BLE_GAP_CHR_UUID16_DEVICE_NAME:
assert(op == BLE_GATT_ACCESS_OP_READ_CHR);
ctxt->chr_access.data = (void *)bleprph_device_name;
ctxt->chr_access.len = strlen(bleprph_device_name);
break;
This code excerpt handles read accesses to the device name
characteristic. The *assert()* here is another case of making sure the
NimBLE stack is doing its job; this characteristic was registered as
read-only, so the stack should have prevented write accesses.
To fulfill a characteristic read request, the application needs to
assign the *ctxt->chr\_access.data* field to point to the attribute data
to respond with, and fill the *ctxt->chr\_access.len* field with the
length of the attribute data. *bleprph* stores the device name in
read-only memory as follows:
.. code:: c
const char *bleprph_device_name = "nimble-bleprph";
The cast to pointer-to-void is a necessary annoyance to remove the
*const* qualifier from the device name variable. You will need to "cast
away const" whenever you respond to read requests with read-only data.
It is not shown in the above snippet, but this function ultimately
returns 0. By returning 0, *bleprph* indicates that the characteristic
data in *ctxt->chr\_access* is valid and that NimBLE should include it
in its response to the peer.
**A word of warning:** The attribute data that *ctxt->chr\_access.data*
points to must remain valid after the access function returns, as the
NimBLE stack needs to use it to form a GATT read response. In other
words, you must not allocate the characteristic value data on the stack
of the access function. Two characteristic accesses never occur at the
same time, so it is OK to use the same memory for repeated accesses.
Write access
^^^^^^^^^^^^
.. code:: c
case BLE_GAP_CHR_UUID16_RECONNECT_ADDR:
assert(op == BLE_GATT_ACCESS_OP_WRITE_CHR);
if (ctxt->chr_access.len != sizeof bleprph_reconnect_addr) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
memcpy(bleprph_reconnect_addr, ctxt->chr_access.data,
sizeof bleprph_reconnect_addr);
break;
This code excerpt handles writes to the reconnect address
characteristic. This characteristic was registered as write-only, so the
*assert()* here is just a safety precaution to ensure the NimBLE stack
is doing its job.
For writes, the roles of the *ctxt->chr\_access.data* and
*ctxt->chr\_access.len* fields are the reverse of the read case. The
NimBLE stack uses these fields to indicate the data written by the peer.
Many characteristics have strict length requirements for write
operations. This characteristic has such a restriction; if the written
data is not a 48-bit BR address, the application tells NimBLE to respond
with an invalid attribute value length error.
For writes, the *ctxt->chr\_access.data* pointer is only valid for the
duration of the access function. If the application needs to save the
written data, it should store it elsewhere before the function returns.
In this case, *bleprph* stores the specified address in a global
variable called *bleprph\_reconnect\_addr*.