This example shows how to implement transparent serial data connection between two radio boards or modules.
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.
The associated sample code is a single application that implements both the server and client roles (in their own C-files). The role is selected dynamically at power-up using pushbuttons, details are found later in the section Running the demo.
The client part of this example (spp_client_main) 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_main code can be converted into e.g. a “heart rate client” or “thermometer client”.
Project setup
This section guides you through setting up an SPP example application on your kit.
To run this example you need:
Two Wireless Starter Kits (WSTK)
Two radio boards or modules
Two terminal windows (such as TeraTerm or PuTTY) running on your PC
1. Create a new SoC - Empty application project with Bluetooth SDK using version 2.12 or newer.
The example sits on top of the basic SoC - Empty application. We will replace the app.c files with the files provided with this article.
2. Click on the .isc file in your project tree, select the Custom BLE GATT field on the right side of the configurator and finally select Import GATT from .bgproj file from the icons next to the field (the bottommost icon)
3. Select the provided gatt.xml, click Save and finally press Generate. You should now have a new SPP service and within it one characteristic.
4. Copy the following files to your project:
- app.c
- spp_utils.h
- spp_utils.c
- spp_client_main.c
- spp_server_main.c
5. Enable printing to console by setting DEBUG_LEVEL from 0 to 1 in app.h
That's it! When you program the application to your development kit, you should see this print at the serial output:
* SPP server mode *
Keeping button PB0 or PB1 pressed during reset will select client mode, in that case the print will be:
* SPP client mode *
SPP server
There is no standard SPP service in Bluetooth Low Energy and therefore we need to implement this as a custom service. The custom service is about as minimal as possible. There is only one characteristic that is used for both incoming and outgoing data. The service is defined in the gatt.xml file associated with this article and shown below.
<!-- Our custom service is declared here -->
<!-- UUID values generated with https://www.guidgenerator.com/ -->
<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>
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.
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 with the command gatt_server_send_characteristic_notification. 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 are 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.
The core of this SPP example implementation is a 256-byte FIFO buffer (data[] in send_spp_data function) that is used to manage outgoing data. Data is received from UART and it is pushed to the SPP client using notifications. Incoming data from client raises the gatt_server_attribute_value event. The received data will then be copied to UART.
Some simple overflow checking can be optionally included. If the number of bytes exceeds 256 then FIFO overflow happens. 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. See comments in spp_utils.h for more information.
SPP client
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. The only differences are that:
Data is received over the air by notifications (event gatt_characteristic_value).
Data is sent by calling gatt_write_characteristic_value_without_response.
The client 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_start_discovery. For each received advertisement packet the stack will raise event le_gap_scan_response. In order to recognize the SPP server the client 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 function process_scan_response. 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 Bluetooth SIG website
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 will open a connection by calling le_gap_connect. 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 attached).
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 (in FIND_SERVICE state)
3) Call gatt_set_characteristic_notification to enable notifications for spp_data characteristic (in FIND_CHAR state)
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 client 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 "SPP Mode ON" 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 "SPP Mode ON" 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 program does not care at all what values are transmitted. It can be either ASCII strings or binary data, or mixture of these. The connection can be closed by pressing either of the pushbuttons on the client.
Running the demo
Simplest way to run the demo is to use two WSTKs with the same radio board (for example two BGM13S radio boards). In this case you can use the same binary for both boards.
When the application boots, it checks the state of pushbuttons PB0, PB1. If buttons are not pressed, the application starts in SPP server role.
By keeping either PB0 or PB1 pressed during reboot the application starts in SPP client mode.
In server mode, the device advertises the custom SPP service and waits for incoming connections.
In client mode, the device starts scanning and searches for the custom SPP UUID in the scan responses. If match is found, the client connects to the target, discovers the SPP service and characteristics and then enables notifications for the SPP_data characteristic. At this point, any data that is input in the client side is sent over the air to the server and printed on the remote UART. Similarly, any data input to the server UART is transmitted back to the client.
To connect to the kit using terminal program use the following UART settings: baud rate 115200, 8N1, no flow control.
NOTE: Make sure that you are using the same baud rate and flow control settings in your starter kit and radio board or module firmware as well as your terminal program. For WSTK, this can be checked in
The above screencapture 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 "SPP Mode ON". 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. Pressing either of the buttons on the client board will close the connection and print the stats for the last session to the terminal.
Power management
USART peripheral is not accessible in EM2 sleep mode. For this reason, both the client and the server applications disable sleeping (EM2 mode) temporarily when the SPP mode is active. SPP mode in this context means that the client and server are connected and that the client has enabled notifications for the SPP_data characteristics.
When SPP mode is entered, the code calls SLEEP_SleepBlockBegin(sleepEM2) to temporarily disable sleeping. When connection is closed (or client disables notifications) then SLEEP_SleepBlockEnd(sleepEM2) is called to re-enable sleeping.
For more details on the power management details, see article here.
Known issues
This example implementation does not guarantee 100% reliable transfer. The implementation uses retargetserial driver for reading data from UART. The driver is found in <project_root>/hardware/kit/common/drivers/ . For incoming data, the driver uses a FIFO buffer whose size is defined using symbol RXBUFSIZE (default value 8).
To get more reliable operation, it is recommended to increase the RXBUFSIZE value. However, even with a large FIFO buffer, there is a chance that some data is lost if the data rate is very high. If the FIFO buffer in RAM becomes full, the driver will simply drop the bytes that do not fit in. More discussion about this topic is found in following forum thread: SPP-over-BLE C example for UART receive data is lost
Conclusion
This example is a simple C-based SPP implementation for BGMxxx / EFR32BG products. It is not optimized for performance or low power. The code has been kept as simple as possible.
UART input/output is handled using stdio retarget functions (see this article for details.)
This is not the most efficient way to handle UART input/output but the benefit is that it is portable and allows the sample code to be used easily on several radio boards without any modifications.
Why there is no SPP in Visual GATT for EFR32BG1 with EVAL Startup Board PCB4100A Rev 3 ?
What needed to make SPP work for this EFR32BG1 device?, I do not like package from BGMxxx series.
0
This SPP implementation is a custom service I made up just for demo purposes. It is not included in the Visual GATT editor at least for now.
The example discussed in this article should work also with PCB4100A without any changes. You first need to create the Soc Empty for this specific board and then add the SPP source files as instructed above.
0
I have written this code into EFR32BG1 via EVAL board PCB4001 (which should be similar to BTExxx device).
I cannot see VCOM appearing in device list in Window Laptop not my android phone.
I like to make connection over SPP in bluetooth between Window Laptop or Andoid Phone and this device. What need to be done and how?
0
You should see device like JLink CDC Uart Driver in Device Manager (COMx), and this is the correct COM device to conect in this example. But this is GATT service not real SPP profile so you will not see any extra COM ports.
0
Hi
I found C_BASED_SPP_EXAMPLE very useful. I'd like to know which pheripheral must be enabled (RTC must be enabled for example?)because in the zip file there is no InitDevice.c file.
Best Regards
Giovanni
0
@Gio63 if you create the Soc Empty example for your target radio board in Studio you will get the InitDevice.c file that is suitable for that board.
0
Thank you JaakkoV. Just another question......I need to send data received from SPP client through UART but I need to send binary data as well so I think printf can't work. I thought to modify file spp_server_main.c (inside C_BASED_SPP_EXAMPLE) because my application behave only as a server in this way:
for (i = 0; i < nbytes; i++) { RETARGET_WriteChar(*buffer++); } return nbytes; }
Do you think could it work? Thank you
Giovanni
0
@Gio63 the solution that you proposed looks good to me. You don't necessarily need to use memcpy to first copy the bytes into another buffer. You could simply just have one function call:
Thank you Jaakkov. In the file ssp_server_main.c I don't find any call to API Function "gecko_cmd_le_gap_set_adv_parameters" There is any reason ?
Thank you
Best Regards
Giovanni
0
The example uses default advertising parameters. You can add a call to gecko_cmd_le_gap_set_adv_parameters before starting advertisements if you want to use some other settings.
KBA_BT_0903: SPP-over-BLE example
Introduction
This example shows how to implement transparent serial data connection between two radio boards or modules.
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.
The associated sample code is a single application that implements both the server and client roles (in their own C-files). The role is selected dynamically at power-up using pushbuttons, details are found later in the section Running the demo.
The client part of this example (spp_client_main) 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_main code can be converted into e.g. a “heart rate client” or “thermometer client”.
Project setup
This section guides you through setting up an SPP example application on your kit.
To run this example you need:
1. Create a new SoC - Empty application project with Bluetooth SDK using version 2.12 or newer.
The example sits on top of the basic SoC - Empty application. We will replace the app.c files with the files provided with this article.
2. Click on the .isc file in your project tree, select the Custom BLE GATT field on the right side of the configurator and finally select Import GATT from .bgproj file from the icons next to the field (the bottommost icon)
3. Select the provided gatt.xml, click Save and finally press Generate. You should now have a new SPP service and within it one characteristic.
4. Copy the following files to your project:
- app.c
- spp_utils.h
- spp_utils.c
- spp_client_main.c
- spp_server_main.c
5. Enable printing to console by setting DEBUG_LEVEL from 0 to 1 in app.h
That's it! When you program the application to your development kit, you should see this print at the serial output:
* SPP server mode *
Keeping button PB0 or PB1 pressed during reset will select client mode, in that case the print will be:
* SPP client mode *
SPP server
There is no standard SPP service in Bluetooth Low Energy and therefore we need to implement this as a custom service. The custom service is about as minimal as possible. There is only one characteristic that is used for both incoming and outgoing data. The service is defined in the gatt.xml file associated with this article and shown below.
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.
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 with the command gatt_server_send_characteristic_notification. 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 are 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.
The core of this SPP example implementation is a 256-byte FIFO buffer (data[] in send_spp_data function) that is used to manage outgoing data. Data is received from UART and it is pushed to the SPP client using notifications. Incoming data from client raises the gatt_server_attribute_value event. The received data will then be copied to UART.
Some simple overflow checking can be optionally included. If the number of bytes exceeds 256 then FIFO overflow happens. 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. See comments in spp_utils.h for more information.
SPP client
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. The only differences are that:
Data is received over the air by notifications (event gatt_characteristic_value).
Data is sent by calling gatt_write_characteristic_value_without_response.
The client 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_start_discovery. For each received advertisement packet the stack will raise event le_gap_scan_response. In order to recognize the SPP server the client 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 function process_scan_response. 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 Bluetooth SIG website
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 will open a connection by calling le_gap_connect. 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 attached).
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 (in FIND_SERVICE state)
3) Call gatt_set_characteristic_notification to enable notifications for spp_data characteristic (in FIND_CHAR state)
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 client 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 "SPP Mode ON" 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 "SPP Mode ON" 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 program does not care at all what values are transmitted. It can be either ASCII strings or binary data, or mixture of these. The connection can be closed by pressing either of the pushbuttons on the client.
Running the demo
Simplest way to run the demo is to use two WSTKs with the same radio board (for example two BGM13S radio boards). In this case you can use the same binary for both boards.
When the application boots, it checks the state of pushbuttons PB0, PB1. If buttons are not pressed, the application starts in SPP server role.
By keeping either PB0 or PB1 pressed during reboot the application starts in SPP client mode.
In server mode, the device advertises the custom SPP service and waits for incoming connections.
In client mode, the device starts scanning and searches for the custom SPP UUID in the scan responses. If match is found, the client connects to the target, discovers the SPP service and characteristics and then enables notifications for the SPP_data characteristic. At this point, any data that is input in the client side is sent over the air to the server and printed on the remote UART. Similarly, any data input to the server UART is transmitted back to the client.
To connect to the kit using terminal program use the following UART settings: baud rate 115200, 8N1, no flow control.
NOTE: Make sure that you are using the same baud rate and flow control settings in your starter kit and radio board or module firmware as well as your terminal program. For WSTK, this can be checked in
The above screencapture 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 "SPP Mode ON". 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. Pressing either of the buttons on the client board will close the connection and print the stats for the last session to the terminal.
Power management
USART peripheral is not accessible in EM2 sleep mode. For this reason, both the client and the server applications disable sleeping (EM2 mode) temporarily when the SPP mode is active. SPP mode in this context means that the client and server are connected and that the client has enabled notifications for the SPP_data characteristics.
When SPP mode is entered, the code calls SLEEP_SleepBlockBegin(sleepEM2) to temporarily disable sleeping. When connection is closed (or client disables notifications) then SLEEP_SleepBlockEnd(sleepEM2) is called to re-enable sleeping.
For more details on the power management details, see article here.
Known issues
This example implementation does not guarantee 100% reliable transfer. The implementation uses retargetserial driver for reading data from UART. The driver is found in <project_root>/hardware/kit/common/drivers/ . For incoming data, the driver uses a FIFO buffer whose size is defined using symbol RXBUFSIZE (default value 8).
To get more reliable operation, it is recommended to increase the RXBUFSIZE value. However, even with a large FIFO buffer, there is a chance that some data is lost if the data rate is very high. If the FIFO buffer in RAM becomes full, the driver will simply drop the bytes that do not fit in. More discussion about this topic is found in following forum thread: SPP-over-BLE C example for UART receive data is lost
Conclusion
This example is a simple C-based SPP implementation for BGMxxx / EFR32BG products. It is not optimized for performance or low power. The code has been kept as simple as possible.
UART input/output is handled using stdio retarget functions (see this article for details.)
This is not the most efficient way to handle UART input/output but the benefit is that it is portable and allows the sample code to be used easily on several radio boards without any modifications.
Hi
This is what I'm looking for SPP implementation.
Why there is no SPP in Visual GATT for EFR32BG1 with EVAL Startup Board PCB4100A Rev 3 ?
What needed to make SPP work for this EFR32BG1 device?, I do not like package from BGMxxx series.
This SPP implementation is a custom service I made up just for demo purposes. It is not included in the Visual GATT editor at least for now.
The example discussed in this article should work also with PCB4100A without any changes. You first need to create the Soc Empty for this specific board and then add the SPP source files as instructed above.
I have written this code into EFR32BG1 via EVAL board PCB4001 (which should be similar to BTExxx device).
I cannot see VCOM appearing in device list in Window Laptop not my android phone.
I like to make connection over SPP in bluetooth between Window Laptop or Andoid Phone and this device. What need to be done and how?
You should see device like JLink CDC Uart Driver in Device Manager (COMx), and this is the correct COM device to conect in this example. But this is GATT service not real SPP profile so you will not see any extra COM ports.
Hi
I found C_BASED_SPP_EXAMPLE very useful. I'd like to know which pheripheral must be enabled (RTC must be enabled for example?)because in the zip file there is no InitDevice.c file.
Best Regards
Giovanni
@Gio63 if you create the Soc Empty example for your target radio board in Studio you will get the InitDevice.c file that is suitable for that board.
Thank you JaakkoV. Just another question......I need to send data received from SPP client through UART but I need to send binary data as well so I think printf can't work. I thought to modify file spp_server_main.c (inside C_BASED_SPP_EXAMPLE) because my application behave only as a server in this way:
switch (BGLIB_MSG_ID(evt->header)) {
.....
........
......
case gecko_evt_gatt_server_attribute_value_id:
{
memcpy(printbuf, evt->data.evt_gatt_server_attribute_value.value.data, evt->data.evt_gatt_server_attribute_value.value.len);
TxBuf(printfbuf, evt->data.evt_gatt_server_attribute_value.value.len);
}
......
}
int TxBuf(uint8_t *buffer, int nbytes)
{
int i;
for (i = 0; i < nbytes; i++)
{
RETARGET_WriteChar(*buffer++);
}
return nbytes;
}
Do you think could it work? Thank you
Giovanni
@Gio63 the solution that you proposed looks good to me. You don't necessarily need to use memcpy to first copy the bytes into another buffer. You could simply just have one function call:
Thank you Jaakkov. In the file ssp_server_main.c I don't find any call to API Function "gecko_cmd_le_gap_set_adv_parameters" There is any reason ?
Thank you
Best Regards
Giovanni
The example uses default advertising parameters. You can add a call to gecko_cmd_le_gap_set_adv_parameters before starting advertisements if you want to use some other settings.