This example shows how to implement transparent serial data connection between two BGM111 modules. The example is implemented using BGScript and full source code is provided at the end of this article.
The goal of this example is to provide a simple template for users that want to use SPP-like communication in their projects. To keep the code as short and simple as possible the features are quite minimal. Users are expected to customize the code as needed to match their project requirements.
To run this example you need:
The client part of this example (spp_client) can be also used as a starting point for any generic BLE central implementation that needs to scan for devices and automatically connect to those devices that advertise some specific service UUID. The client performs service discovery after connection establishment and this process works similarly for any GATT based services. With small modifications the spp_client code can be converted into e.g. a “heart rate client” or “thermometer client”.
There is no standard SPP service in Bluetooth Smart and therefore we need to implement this as a custom service. The service is defined in GATT xml as follows:
<service uuid="4880c12c-fdcb-4077-8920-a450d7f9b907" advertise="true"> <description>SPP Service</description> <characteristic uuid="fec26ec4-6d71-4442-9f81-55bc21d658d6" id="xgatt_spp_data"> <description>SPP Data</description> <properties write_no_response="true" notify="true" /> <value variable_length="true" length="20" type="hex"></value> </characteristic> </service>
This is a custom service and the 128-bit UUID values have been generated using the online tool available at www.guidgenerator.com
The custom service is about as minimal as possible. There is only one characteristic that is used for both incoming and outgoing data.
For incoming data (data sent by the SPP client and written to UART in SPP server) we use unacknowledged write transfers (write_no_response). This will give better performance than normal acknowledged writes because several write operations can be fitted into one connection interval.
For outgoing data (data received from UART and sent to SPP client) we use notifications. Notifications are unacknowledged and this will again allow several notifications to be fitted into one connection interval.
Note that the data transfers are unacknowledged at GATT level. This means that at application level there is no acknowledgements. However, at the lower protocol layers each packet is still acknowledged and retransmissions are used when needed to ensure that all packets are delivered.
At boot event the server is put into advertisement mode. This example uses advertising packets that are automatically filled by the stack. In our custom SPP service definition (see above) we have set advertise=true, meaning that the stack will automatically add the 128-bit UUID in advertisement packets. The SPP client will use this information to recognize the SPP server among other BLE peripherals.
The core of this SPP example implementation is a 256-byte FIFO buffer that is used to manage outgoing data. Data is received from UART and it is pushed to the SPP client using notifications. By using a large FIFO buffer the receiving and sending of data are effectively decoupled from each other.
When data is received from UART the endpoint_data event is raised. The number of bytes is variable and it depends on how fast the external host is writing to the UART.
The endpoint_data handler is really simple: it just copies the received bytes to the FIFO buffer. The FIFO buffer is implemented using a large buffer and three additional variables for bookkeeping (write pointer, read pointer, number of bytes in FIFO).
There is some simple overflow checking included. If the number of bytes exceeds 256 then FIFO overflow happens. In this case the code simply writes string “OVERFLOW!” to the UART. This is just a placeholder for some real error handling code. Depending on application the overflow situation may be handled differently. For some applications it could be best to just drop the bytes that do not fit in buffer. For other applications it may be better to immediately stop all data transfer to avoid any further damage.
The FIFO buffer content needs to be pushed to connected SPP client using notifications. The target is to do this as fast as possible to get best throughput and to avoid FIFO overflow. In this example, the FIFO offloading is implemented using a soft timer that runs with 10ms interval.
When the 10 ms soft timer event is raised, the code checks the FIFO fill level. Data is sent out using as large packets as possible. In this case, the maximum size is 20 bytes per notification. A while loop is used to send as much data as possible per each timer event. The while loop keeps sending notifications until the stack returns an error message (no more buffer space available) or until the FIFO becomes empty.
To understand how the FIFO offloading works, imagine that someone types one character to the UART each second. The character is not immediately sent. Instead, there is a worst-case delay of 10ms until the timer event is raised. In this case, there would be only one byte in the FIFO and the application would send a single notification with just one byte.
Another example would be that there is some external host program that is writing to the UART at high speed. In this case, when the timer event is raised there can be 20 or more bytes in the buffer and the notifications that are sent use maximum data length. This way the size of the notifications is automatically adjusted to the rate at which data is received from UART. For small amounts of data (< 20 bytes) there is latency that is <= 10 ms (which is the soft timer interval).
Handling of incoming data (data received from the client over BLE) is simpler than outgoing data. When client writes to the spp_data characteristic the gatt_server_attribute_value event is raised. The script will simply copy this data to UART by calling endpoint_send. There is no flow control used and it is assumed that the external host connected to the UART can accept all data that is sent by the script.
To maximize throughput the server script will try to set connection interval to the lowest possible value (7.5ms) immediately after connection is established. This is done in event le_connection_opened by calling function le_connection_set_parameters.
In terms of incoming/outgoing UART data, the SPP client works the same way as the SPP server. A similar 256-byte FIFO buffer is used as in the SPP server script. The only differences are that:
The client script is a bit more complex than the server because the client needs to be able to detect the SPP server by looking at the advertisement packets. Additionally, the client needs to do service discovery after connecting to the client to get the needed information about the remote GATT database.
To be able to use the SPP service the client needs to know the characteristic handle values. The handles are discovered dynamically so that there is no need to use any hard-coded values. This is essential if the SPP server needs to be ported to some other BLE module and the handle values are not known in advance.
At startup the client starts discovery by calling le_gap_discover. For each received advertisement packet the stack will raise event le_gap_scan_response. In order to recognize the SPP server the script scans through each advertisement packet and searches for the 128-bit service UUID that we assigned for our custom SPP service.
Scanning of advertisement packets is done in procedure decode_adv_data. The advertising packets include one or more advertising data elements that are encoded as defined in the BT specification. Each advertising element begins with a length byte that indicates the length of that field. This makes it easy to scan through all the elements in a while-loop.
The second byte in advertising element is the AD type. The predefined types are listed in https://www.bluetooth.com/specifications/assigned-numbers/Generic-Access-Profile
In this case, we are interested in types 0x06 and 0x07 that indicate incomplete/complete list of 128-bit service values. If such AD type is found, then the AD payload is compared against the known 128-bit UUID of our SPP service to see if there is a match.
After finding a match in the advertising data the client script will open a connection by calling le_gap_open. When connection is opened, the next task is to discover services and figure out what is the handle value that corresponds to the SPP_data characteristic (see XML definition of our custom service above).
The service discovery is implemented as a simple state machine and the sequence of operations after connection is opened is summarized below:
1) Call gatt_discover_primary_services_by_uuid to start service discovery
2) Call gatt_discover_characteristics to find the characteristics in the SPP service
3) Call gatt_set_characteristic_notification to enable notifications for spp_data characteristic
Note that in step 1) above we want to discover only services that match the specific UUID we are looking for. Another option would be to call cmd_gatt_discover_primary_services that will return list of all services in the remote GATT database.
After each procedure in the above sequence is completed, the stack will raise event gatt_procedure_completed. The script uses a variable main_state that is used to keep track what is the current state. The gatt_procedure_completed event will trigger the state machine to move on to the next logical state.
When notifications have been enabled the application is in transparent SPP mode. This is indicated by writing string “DATA” to UART. After this point, any data that is received from UART is sent to server using non-acknowledged write transfer. Similarly, all data received via notifications (event gatt_characteristic_value) is copied to the local UART.
Note that on the server application there is also "DATA" string that is printed to console. On server side this is done when the remote client has enabled notifications for the spp_data characteristic.
The data is handled transparently, meaning that the script does not care at all what values are transmitted. It can be either ASCII strings or binary data, or mixture of these. Note that there is also no way for the external host to for example close the connection. If this kind of functionality is needed then it could be implemented with for example using some dedicated GPIO pin.
To run the demo, simply compile and flash the spp_client and spp_server example applications to two BGM111 development kits. The onboard USB-to-UART converter is used to allow opening a terminal connection to both sides. The screenshots shown below illustrate how to test the example with two TeraTerm terminals running on a PC.
The above screenshot illustrates what happens when the client and server boards are powered up. Client is on the left side and server on the right. Quickly after power up the client will find the server and open a connection automatically. When the connection is set up properly and notifications are enabled then both applications will output string “DATA”. This indicates that the transparent serial connection is open. From this point on, any data you type into the client terminal will appear on the server terminal and vice versa.
You can try pressing reset button on either of the boards and see what happens. The connection should be restored automatically when both units are back online.
With TeraTerm it is also possible to test file transfer over the transparent serial connection. TeraTerm has built-in support for XMODEM protocol. Below is a series of screenshots that illustrate how to set up an XMODEM file transfer. You can test file transfers in both directions. Both the client and the server can be put into receive or send mode.
With XMODEM protocol you should be able to get about 900-1000 Bytes/sec sustained throughput between two BGM111 kits. Note that this is the application level throughput that includes all the overhead caused by XMODEM acknowledgements and possible retransmissions.
In unidirectional data transfer tests this example should go up to ~80 kbps. This can be tested for example by using TeraTerm macros or some dedicated host program written in C, Python or similar. Note that as the example does not use any flow control it is quite easy to cause the FIFO buffer or the internal UART buffers of the stack to overflow if the host just keeps transmitting data at full speed.
As stated in the beginning, this example is intended to be used as a template and it is not a complete SPP solution. Users are advised to modify the example as needed and add new features to adapt the code to meet their specific application needs.
Some possible improvements that could be made to the code:
* power saving: disable any soft timers when they are not needed to allow the modules enter sleep when there is no data to transfer
* custom commands to change communication parameters (connection interval, slave latency etc)
* flow control, based on for example FIFO fill level
* additional GPIO control to allow interrupting the transparent data mode
* security: allow transparent data mode to be entered only if devices are bonded