The maximum transmission unit (MTU) is the largest amount of data that can be exchanged in a single GATT operation. Any read or write operation that fits within this limit can be accomplished in a single operation. However, if the data to be read or written is larger than MTU then a so-called long read/write operation must be used. This article demonstrates how to implement this.
According to the Bluetooth specification, the maximum size of any attribute is 512 bytes. The attached application handles reading and writing a characteristic of 512 bytes.
When reading a user characteristic longer than MTU, multiple gatt_server_user_read_request events will be generated on the server side, each containing the offset from the beginning of the characteristic. The application code must use the offset parameter to send the correct chunk of data.
Characteristics can be written by calling gecko_cmd_gatt_write_characteristic_value. If the characteristic data fits within MTU – 3 bytes then a single operation used, otherwise the ‘write long ‘ procedure is used. The write long procedure consists of a prepare write request operation and an execute write request operation. A maximum of MTU – 5 bytes can be sent in a single prepare_value_write operation. The application can also access these operations directly by calling gecko_cmd_gatt_prepare_characteristic_value_write() and gecko_cmd_gatt_execute_characteristic_value_write(). This is a useful approach if the size of the characteristic is greater than 255 bytes.
Notifications and Indications are limited to MTU – 3 bytes. Since all notifications and indications must fit within a single GATT operation, the application does not demonstrate them.
The attached application can operate in either slave or master mode. The application starts in slave mode, to switch to master mode, press PB0 on the WSTK.
As soon as the device is switched into master mode, it begins scanning for a device advertising a service with the following UUID cdb5433c-d716-4b02-87f5-c49263182377. When a device advertising this service is found a connection is formed. The gecko_evt_gatt_mtu_exchanged event saves the MTU size for the connection, this will be needed for writing the long characteristic later.
The master now discovers service and characteristic handles. Once the long_data characteristic is found, the master performs a read of this characteristic by calling gecko_cmd_gatt_read_characteristic_value(). The size of this characteristic is 512 bytes so the “read long” procedure will always be used.
The master can also write data to this characteristic. Pressing PB1 on the WSTK triggers a write of an array of test data to this long characteristic. This action is handled by a helper function called write_characteristic() which in turn uses a helper function called queueCharacteristicChunk. This function can handle any data size up to 512 bytes. Writing the characteristic data is handled by queueing data with as many calls to gecko_cmd_gatt_prepare_characteristic_value_write() as necessary, once all of the data has been queued up, it is written with a call to gecko_cmd_gatt_execute_characteristic_value_write(). Since only one GATT operation can take place at a time for a given connection, the gatt_procedure_completed event is used to drive the process of queueing writes. To get the process started queueCharacteristicChunk() is called directly from write_characteristic(), after that queueCharacteristicChunk() is called from the gatt_procedure_completed event. This ensures that the previous procedure is finished before attempting to start another.
Upon startup the slave begins advertising the service mentioned above. This service contains a single user-type characteristic of 512 bytes. The gecko_evt_gatt_server_user_read_request event handler takes care of read requests from the master. Since the characteristic is known to be larger than an MTU, this event handler uses the connection mtu size and offset parameters passed in to the event to send the correct portion of the array to the master. This event will be generated as many times as necessary to allow reading the entire characteristic. A gatt_server_attribute_value event is generated for each queued write performed by the master and a gatt_server_execute_write_completed event is generated when all of the queued writes have been completed. The result parameter indicates whether an error has occurred. A user_write_request response must be sent by the application for each queued write, this is handled in the user_write_request event handler.
Get started by creating an soc-empty application for your chosen device with the Appbuilder in SimplicityStudio. Once that’s complete, you can extract the zip file attached to this article and copy it to your project folder. Open hal-config.h and change the definition of from HAL_VCOM_ENABLE 0 to 1.
Open the .isc file for your project and import the gatt.xml file included in the zip by clicking the import icon on the right side of the .isc file, when the file has imported click generate and then build the project. As mentioned previously, this application can act as either a master or a slave. Flash the application onto to two evaluation boards and open a terminal window such as Teraterm or SimplicityStudio console for each. Choose one of the kits to be a master and press PB0. Now you’ll see a message indicating that the device has entered master mode and once it has found the slave, indicates that it has connected and has read the characteristic in three steps since the MTU size is 247 bytes. Once this process has completed you’ll see a message indicating that the read has finished and to press PB1 to write a block of test data to the slave. The master displays messages indicating how many bytes are written to the slave in each operation and the message “exec_result = 0x00” when complete.
Just thought I'd let you know that there is a bug in the example code queueCharacteristicChunk.
Replace the *(data+offset) with just data in 2 places
And another bug:
write_offset should start out as 0
write_offset = write_characteristic(test_data_to_write,sizeof(test_data_to_write),connection_handle,characteristic_handle,write_offset);
OK, I'll look into that. jm, do you need to support every possible MTU size from 23 up to the maximum? I'm trying to understand your use case so that I can help you.
No, it was just on observation from my code based on this example. I expected the default value of 23 to work (just slower), but I kept getting 1033 errors with the large packet size. Then as I reduced the packet size I kept getting 0x182 errors. Increasing the MTU size fixed these issues.
Nobody ever did tell me how many packets could be queued before a 1033 error would be triggered.
Thank you, very useful article!
I think that line 425 is incorrect or at least in my use-case it is (reading the same characteristic multiple times in a row with updated data).
bytes_left_to_send = 0; //reset for next time
Should be like this:
bytes_left_to_send = sizeof(long_data_buffer); //reset for next time
As resetting would mean setting the bytes_left_to_send to the buffer size not zero :)