Notice how the application layer is right above the GATT which in turn is built upon the ATT. The ATT is based on a Client <–> Server relationship. The server holds information like sensor values, the state of a light switch, position data, etc. This information is organized in a table, referred to as an attribute table. Each attribute in the table is a value or piece of information with some associated properties. So when a client wants the sensor data it refers to e.g. row 11 in the table. If it wants the state of the light switch it might refer to row 16, and so on. Here is an excerpt from the Bluetooth Core Specification V4.2 (Hereafter referred to as BCS):
Vol 3:, Part F, Ch. 2, “Protocol Overview”:
The attribute protocol defines tworoles; a server role and a clientrole. It allows a server to expose aset of attributes to a client that areaccessible using the attributeprotocol. An attribute is a discretevalue that has the following threeproperties associated with it: (1) an attribute type, defined by a UUID, (2) an attribute handle, (3) a set of permissions that are defined by eachhigher layer specification thatutilizes the attribute; thesepermissions cannot be accessed usingthe Attribute Protocol.
The attributetype specifies what the attributerepresents. Bluetooth SIG definedattribute types are defined in theBluetooth SIG assigned numbers page,and used by an associated higher layerspecification. Non-Bluetooth SIGattribute types may also be defined.
Let us relate this to a typical application, namely the Heart Rate Profile. In Table 1 each and every row is an attribute, and each attribute has a handle, a type, a set of permissions, and a value.
Table 1:
Now you might wonder why we have read/write permissions for an attribute and read/write properties for the characteristic value. Shouldn’t they always be the same? And that is a legitimate question. The properties for the characteristic value are actually only guidelines for the client, used in the GATT and application layers. They are just clues, if you will, of what the client can expect from the Characteristic Value Declaration attribute. The permissions for the attribute (on the ATT layer) will always overrule the characteristic value properties (on the GATT layer). Now you might ask again “but why do we need both permissions and properties?”. And the simple, but disappointing, answer is: “Because the Bluetooth Core Specification says so”. It is confusing, but has implications for how we will set up our characteristic later so it needs to be said.
In the case of the Heart Rate Measurement Characteristic in Table 1 the Characteristic Value Declaration is followed by a Descriptor Declaration. The descriptor is an attribute with additional information about the characteristic. There are several kinds of descriptors, but in this tutorial we will only deal with the Client Characteristic Configuration Descriptor (CCCD). More about this later.
Figure 1:
In Figure 1 above I have marked the Service Declaration attribute. As you can see inside the red square the UUID of this attribute is 0x2800, the handle is 0x000C and the value is 0x180D (Least Significant Byte first (LSB) in the value field). This is exactly what we would expect after going through the attribute table in Table 1. The UUID shows that the attribute is a service declaration and the value shows that it is a Heart Rate Service.
Figure 2:
In Figure 2 I have marked the first Characteristic Declaration attribute. As expected, the UUID is 0x2803 showing us that this is indeed a characteristic declaration. The value here is interesting. Although the meaning of the values is written in plain text to the right let's decode the value ourselves anyway. The value shown in the 'Value:' field is a number of bytes shown LSB first. To make it more readable we will reorder them to Most Significant Byte (MSB) first:2A37-000E-10. You might already have noticed that 2A37 is the Heart Rate Measurement characteristic UUID and 000E is the handle identifying where this characteristic value declaration can be found in the attribute table. 0x10 is defining the properties. Comparing the value 0x10 to the bit field in Table 2 we can see that this represents Notification. So everything is still consistent with the attribute table in Table 1.
Figure 3:
In Figure 3 you can see that the characteristic value declaration's UUID is indeed 0x2A37 and the handle value is 0x000E, as specified in the characteristic declaration in Figure 2.
Figure 4:
Finally, in Figure 4 you can see the values of the CCCD attribute. You can see the predefined Descriptor UUID (0x2902) and that the value is 0x0000 This means that notification is currently switched off. More on this later.
The important files in this tutorial are our_service.c, our_service.h, and of course main.c. In these files you will find various comments beginning with:
// FROM_SERVICE_TUTORIAL: // ALREADY_DONE_FOR_YOU: // OUR_JOB: Step X.X
FROM_SERVICE_TUTORIAL means that this is code we looked at in the previous tutorial. A few places you will see ALREADY_DONE_FOR_YOU. This marks very basic code that I have already prepared for you so that we don't have to go through the less important stuff in detail. Finally, OUR_JOB: Step X.X indicates what and when things need to be done in the tutorial.
This is what our attribute table should include to achieve all this:
Table: 3
The highlighted line is the Service Declaration declaring our service. As you can see the attribute type is 0x2800, the attribute handle is 0x000C, and the attribute value is our 128-bit custom UUID.
sd_ble_gatts_characteristic_add()
is our first goal. This function will add both the Characteristic Declaration and the Characteristic Value Declaration to our attribute table. However, in order to get there we have to do a few things first.
sd_ble_gatts_characteristic_add()
takes four parameters:
@param[in] uint16_t service_handle. @param[in] ble_gatts_char_md_t const * p_char_md @param[in] ble_gatts_attr_t const * p_attr_char_value @param[out] ble_gatts_char_handles_t * p_handles
As you can see the function has three input parameters and one output parameter. The three input parameters need to be populated with details that will define the characteristic attributes. These parameters define the properties, read/write permissions, descriptors, characteristic values, etc. In fact, you have more than 70 properties at your disposal when you are defining your characteristic. It sounds daunting, but don't give up and throw away your dev kit just yet. Most of the parameters are optional and almost all of them work just fine if you initialize them to zero using memset(¶meter, 0, sizeof(parameter));
. Pretty neat!
We will start by populating only the essential parameters we need to get started. In fact, all we need to do is to choose where in memory to store the characteristic attributes and define a characteristic value type using our custom UUID. In the function our_char_add()
I have already declared all the necessary variables for you and initialized them to zero. The order of which I have declared them might be a little confusing, but since the necessary parameters are nested into structures, and these structures might be nested into other structures, this is unfortunately the way it has to be. I have also selected names that might seem a little cryptic, but these are naming conventions used throughout the SDKs and I chose to go for consistency.
The three most important variables are:
ble_gatts_attr_md_t attr_md
, The Attribute Metadata: This is a structure holding permissions and authorization levels required by characteristic value attributes. It also holds information on whether or not the characteristic value is of variable length and where in memory it is stored. ble_gatts_char_md_t char_md
, The Characteristic Metadata: This is a structure holding the value properties of the characteristic value. It also holds metadata of the CCCD and possibly other kinds of descriptors. ble_gatts_attr_t attr_char_value
, The Characteristic Value Attribute: This structure holds the actual value of the characteristic (like the temperature value). It also holds the maximum length of the value (it might e.g. be four bytes long) and it's UUID.So let's start populating the parameters. To make things a little easier I have tried to assign each step a number:
our_char_add()
function:
ble_uuid_t char_uuid; ble_uuid128_t base_uuid = BLE_UUID_OUR_BASE_UUID; char_uuid.uuid = BLE_UUID_OUR_CHARACTERISTC_UUID; sd_ble_uuid_vs_add(&base_uuid, &char_uuid.type); APP_ERROR_CHECK(err_code);
This will use the same base UUID as the service, but add a different 16-bit UUID for the characteristic. The UUID is defined as 0xBEEF in our_service.h. Note that this base UUID was added to the vendor specific table in the previous tutorial when we created the custom service. All subsequent calls with the same base UUID will return a reference to the same ID in the table. This way we will save some memory by not needing to store a large array of 128-bit long IDs.
BLE_GATTS_VLOC_STACK
. The only other valid option is to use
BLE_GATTS_VLOC_USER
to store the attributes in the user controlled part of memory.
ble_gatts_attr_md_t attr_md; memset(&attr_md, 0, sizeof(attr_md)); attr_md.vloc = BLE_GATTS_VLOC_STACK;
In the attribute metadata structure ble_gatts_attr_md_t
you also have the option to define the permissions with associated authorization requirements. For example if you need Man In The Middle protection (MITM) or a passkey to access your attribute. We will circle back to this.
ble_gatts_attr_t attr_char_value; memset(&attr_char_value, 0, sizeof(attr_char_value)); attr_char_value.p_uuid = &char_uuid; attr_char_value.p_attr_md = &attr_md;
ble_os_t
definition in our_service.h and add the line shown below:
typedef struct { uint16_t conn_handle; uint16_t service_handle; // OUR_JOB: Step 2.D, Add handles for our characteristic ble_gatts_char_handles_t char_handles; }ble_os_t;
As you can see, the ble_os_t
struct already has a field for the service declaration handle. The connection handle, conn_handle
, is there to keep track of the current connection and don't really have anything to do with the attribute table handles. If you go to the definition of ble_gatts_char_handles_t
you can see that our new variable can hold 16-bit handles for the characteristic value, user descriptor, its CCCD, and also something called Server Characteristic Configuration Descriptor (SCCD) which is not within the scope of this tutorial.
err_code = sd_ble_gatts_characteristic_add(p_our_service->service_handle, &char_md, &attr_char_value, &p_our_service->char_handles); APP_ERROR_CHECK(err_code);
As you can see, we are giving the SoftDevice information about what service the characteristic belongs to (the service_handle), the Characteristic Metadata, and the Characteristic Value Attributes. The stack then processes the parameters and initiates the characteristic. Then it stores the handle values of our characteristic into our p_our_serice structure.
Now compile and download your application to the kit. Make sure that you have remembered to program the SoftDevice to the kit as well. Open MCP, connect to your kit, and do a service discovery. You should see something like this:
As you can see there is a new characteristic in our service, but it doesn't do anything useful at all. It has no value and you can neither read from it nor write to it. You can try for yourself by selecting the characteristic value line, type in some hexadecimal numbers in the "Value:" field, and hit the "Write" or "Read" button. MCP will send a write request or attempt a read, but since we have not defined any read or write permissions yet your kit will deny any read or write requests by default. This is what happens when we initialize the read/write attribute permissions to zero. The log window in MCP should show something like this:
our_char_add()
and add the following lines:
ble_gatts_char_md_t char_md; memset(&char_md, 0, sizeof(char_md)); char_md.char_props.read = 1; char_md.char_props.write = 1;
This will populate the characteristic metadata with read and write properties. Do another service discovery in MCP and note that the "Properties" field in the Characteristic Declaration has changed to "Read, Write":
Now try to read and write some values to the characteristic again.
But, wait, what is this?? You are still getting READ and WRITE_NOT_PERMITTED errors?
You should, if you have followed the tutorial to the letter so far. This is because what we just did was just setting the properties in the characteristic declaration and as discussed earlier they are just guidelines. So even though these properties are exposed when your MCP does a service discovery we have yet to set the required permissions for read and write operations. Before we have done this the SoftDevice doesn't know what to allow and simply denies any reads and writes of the characteristic.
BLE_GAP_CONN_SEC_MODE_SET_OPEN()
. Go to Step 2.G in
our_char_add()
and add the following two lines:
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.write_perm);
Now do yet another service discovery and note how the word "Value: " is added to the characteristic value line in MCP:
When you try to read from the characteristic now then this should pop up in the log window in MCP:
Great success! We have just read 0, zero(!), bytes from our new characteristic because we have not yet assigned a value or a value length to it.
Next, try to write e.g. the value 0x12 to the characteristic. You should get the following error because the value attribute length is initialized to zero:
attr_char_value.max_len = 4; attr_char_value.init_len = 4; uint8_t value[4] = {0x12,0x34,0x56,0x78}; attr_char_value.p_value = value;
This will set the initial length, and also the maximum allowed length, of the characteristic value to 4 bytes. And just to be a little thorough we will also set the initial value to 12-34-56-78. Once again, try to read from your characteristic and then write a couple of bytes. You should see that the value is updated. Even if you disconnect and reconnect the new value should be retained.
Challenge 1:
What happens? Why are you not allowed to write more than four bytes to your characteristic and how can you fix it?
Challenge 2:
Try to experiment with the attr_md
permissions and the following permission macros:
Try to do some reads and writes. See what happens and watch out for error messages in the MCP log window. Hint on number 3: If you click "Bond" in MCP and bond with your kit you will automatically add encryption (ENC) to your BLE link. However, you are not required to use Man In The Middle (MITM) protection when bonding.
The BCS defines two ways of "pushing" data:
Vol 3: Part G, Ch. 4.10 & 4.11:
Indication - This sub-procedure is used when aserver is configured to indicate aCharacteristic Value to a client andexpects an Attribute Protocol layeracknowledgement that the indicationwas successfully received.
Notification - This sub-procedure is used when aserver is configured to notify aCharacteristic Value to a clientwithout expecting any AttributeProtocol layer acknowledgment that thenotification was successfullyreceived.
The subtle, but important difference here is that by using indication your kit (the server) will require an application level acknowledgment in return after every transmission of updated data. By using notification, on the other hand, our kit will just "mindlessly" transmit data and not care about whether it is acknowledged by the application in the other end or not. So sticking to the "keep it simple"-philosophy of this tutorial we will use the latter. To add notification functionality we need to add a descriptor to our attribute table, namely the Client Characteristic Configuration Descriptor (CCCD). The BLE Core Specifications has the following to say about the CCCD:
Vol 3: Part G, Ch 3.3.3.3:
The Client CharacteristicConfiguration declaration is anoptional characteristic descriptorthat defines how the characteristicmay be configured by a specificclient [...]
What is written in between these cryptic lines is that the CCCD is a writable descriptor that allows the client, i.e. your MCP or phone, to enable or disable notification or indication, on your kit.
ble_gatts_attr_md_t cccd_md; memset(&cccd_md, 0, sizeof(cccd_md)); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm); cccd_md.vloc = BLE_GATTS_VLOC_STACK; char_md.p_cccd_md = &cccd_md; char_md.char_props.notify = 1;
Compile, download, run, and connect to your kit. Now you should be able to see the CCCD added to your characteristic:
Notice also that "Notify" is added to the Characteristic Declaration's properties field. The UUID value of the CCCD is 0x2902 as we have seen earlier and the value is 0x0000 meaning that notification and indication is currently switched off. If you want you can enable notification right away, but since we have not yet set up any mechanisms updating the values nothing fancy will happen. Anyway, try to click the "Enable services" button. You should see that the value field of the CCCD is updated to 0x0001, meaning that notification is indeed enabled. You can also use the "Write" button and write to the CCCD manually.
Our service structure, the ble_os_t
, has a field called "conn_handle". This shall hold the handle of the current connection as provided by the BLE stack and we have to initialize it during system startup. Naturally, since we are not in a connection at system startup, the handle should be initialized to some "invalid" value. In the SDKs this value is defined as BLE_CONN_HANDLE_INVALID
(with value 0xFFFF). So head over to the our_service_init()
function in our_service.c and type in the following.
p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID;
ble_evt_dispatch()
. This function is called by the SoftDevice when a BLE stack event has occurs. To keep our service up to date with the latest connection events we will call
ble_our_service_on_ble_evt()
from the dispatch function:
ble_our_service_on_ble_evt(&m_our_service, p_ble_evt);
ble_our_service_on_ble_evt()
. Inside we will make a short switch case statement. In our example the only thing the statement will do is to update the connection handle stored in the service structure. On a connection event we will store the current connection handle as provided by the BLE stack. On a disconnect event we will set the handle back to "invalid".
switch (p_ble_evt->header.evt_id) { case BLE_GAP_EVT_CONNECTED: p_our_service->conn_handle = p_ble_evt->evt.gap_evt.conn_handle; break; case BLE_GAP_EVT_DISCONNECTED: p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID; default: // No implementation needed. break; }
our_termperature_characteristic_update()
. This is where we will implement the notification. First implement an if-statement like this:
if (p_our_service->conn_handle != BLE_CONN_HANDLE_INVALID) { }
Here we check whether or not we are in a valid connection, and this is where the housekeeping comes in handy. If you try to send out notifications when not in a connection the SoftDevice will get grumpy and give you errors. That is why we need this if-statement and why we implemented the housekeeping to keep the connection handle updated. Now, inside the if-statement type in the following:
uint16_t len = 4; ble_gatts_hvx_params_t hvx_params; memset(&hvx_params, 0, sizeof(hvx_params)); hvx_params.handle = p_our_service->char_handles.value_handle; hvx_params.type = BLE_GATT_HVX_NOTIFICATION; hvx_params.offset = 0; hvx_params.p_len = &len; hvx_params.p_data = (uint8_t*)characteristic_value; sd_ble_gatts_hvx(p_our_service->conn_handle, &hvx_params);
First you might wonder what hvx stands for? It is not very intuitive, but it stands for Handle Value X, where X symbolize either notification or indication as the struct and function can be used for both. So to do a notification we declare a variable, hvx_params
, of type ble_gatts_hvx_params_t
. This will hold the necessary parameters to do a notification and provide them to the sd_ble_gatts_hvx()
function. Here is what we will store in the variable:
p_our_service->char_handles.value_handle
.BLE_GATT_HVX_NOTIFICATION
. The other option would be BLE_GATT_HVX_INDICATION
.Finally we pass this structure into the sd_ble_gatts_hvx()
. We also provide the function with the relevant connection handle. In some applications you might work with several concurrent connections and this is why the function also needs to know what handle to use. Now we have set up everything that has to do with the characteristic and notification.
timer_timeout_handler()
in main.c and type in the following:
int32_t temperature = 0; sd_temp_get(&temperature); our_termperature_characteristic_update(&m_our_service, &temperature); nrf_gpio_pin_toggle(LED_4);
APP_TIMER_DEF(m_our_char_timer_id); #define OUR_CHAR_TIMER_INTERVAL APP_TIMER_TICKS(1000, APP_TIMER_PRESCALER) // 1000 ms intervals
This will instantiate a timer ID variable and define a timer interval of 1000 ms.
app_timer_create(&m_our_char_timer_id, APP_TIMER_MODE_REPEATED, timer_timeout_handler);
We pass:
APP_TIMER_MODE_REPEATED
. This tells the timer library to set up a timer that triggers at regular intervals. The other option is APP_TIMER_MODE_SINGLE_SHOT
which sets up a timer that triggers only once. Most often in Nordic's libraries the fact that a module is initiated does not mean that it is started, so we have to call one last function to reach our goal:
app_timer_start(m_our_char_timer_id, OUR_CHAR_TIMER_INTERVAL, NULL);
We pass:
NULL
.Compile, download, connect, and discover services. Now to the moment of truth: Click "Enable services". If we have done everything right the characteristic value should update every second. You should see LED 4 and the value line in MCP blink green every second. You should also see values ticking in in the Log window in MCP like this:
If you now put your finger on the nRF51 chip you should also see that the values are changing.
Challenge 1:
Try to alter our_termperature_characteristic_update()
so that you only send a notification when the temperature has changed. Remember that BLE is all about saving energy so why spend resources on transmitting the same value over and over again?Hint: Use a variable to store the current temperature value and compare it to the new one on the next measurement.
Challenge 2:
Try to modify the timer so that the temperature value is only measured when you are in a connection. I.e. start the timer on a connection event and stop it on a disconnect event. Why spend energy on measurements if you don't use them, right? Hint: Look for a timer start and stop function in the app_timer library. Then see if you can do some magic in the on_ble_evt()
function in main.c.
Once again I urge you to post any questions you have in the Questions-section on the forum, not here. You will most likely get faster response that way. Positive or negative critique though is very welcome in the comment section below.
转自https://devzone.nordicsemi.com/tutorials/17/