Skip to main content

Bluetooth Usage with Seeed Studio XIAO MG24

Seeed Studio XIAO MG24Seeed Studio XIAO MG24 Sense

The Seeed Studio XIAO MG24 is a robust development board that supports Bluetooth LE 5.3 and Bluetooth mesh, making it an ideal choice for a wide array of IoT applications requiring wireless connectivity. Leveraging its exceptional RF performance, the XIAO MG24 delivers reliable, high-speed wireless communication over various distances, making it a versatile solution for both short-range and long-range applications. In this tutorial, we will explore the fundamental features of the XIAO MG24's Bluetooth capabilities, including how to scan for nearby Bluetooth devices, establish a Bluetooth connection, and transmit and receive data over that connection.

Method for switching antennas

The Seeed Studio XIAO MG24 has two antenna options: an internal antenna and an external antenna. For convenience, you can choose to use the internal antenna, and to enhance signal strength, you can opt for the external antenna. Below are the method for switching between the two antennas.

PB04 is used to select between using the built-in antenna or an external antenna. Before that, you need to set PB05 high level to turn on this function. If PB04 is set low level, it uses the built-in antenna; if it set to high level, it uses the external antenna. Default is low level. If you want to set it high, you can refer the code below.

#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4

void setup() {
// turn on this antenna function
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);

delay(100);

// HIGH -> Use external antenna / LOW -> Use built-in antenna
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, HIGH);

Bluetooth Low Energy (BLE) Usage

Bluetooth Low Energy, BLE for short, is a power-conserving variant of Bluetooth. BLE’s primary application is short distance transmission of small amounts of data (low bandwidth). Unlike Bluetooth that is always on, BLE remains in sleep mode constantly except for when a connection is initiated.

Due to its properties, BLE is suitable for applications that need to exchange small amounts of data periodically running on a coin cell. For example, BLE is of great use in healthcare, fitness, tracking, beacons, security, and home automation industries.

This makes it consume very low power. BLE consumes approximately 100x less power than Bluetooth (depending on the use case).

About the BLE part of XIAO MG24, we will introduce its use in the following sections.

  • Some fundamental concepts -- We will first get to know some concepts that may be used frequently in BLE in order to help us understand the execution process and thinking of BLE programs.
  • BLE Scanner -- This section will explain how to search for nearby Bluetooth devices and print them out in the serial monitor.
  • BLE server/client -- This section will explain how to use XIAO MG24 as Server and Client to send and receive specified data messages. It will also use to receive or send messages from the phone to XIAO.

Some fundamental concepts

Server and Client

With Bluetooth Low Energy, there are two types of devices: the server and the client. The XIAO MG24 can act either as a client or as a server.

The server advertises its existence, so it can be found by other devices, and contains the data that the client can read. The client scans the nearby devices, and when it finds the server it is looking for, it establishes a connection and listens for incoming data. This is called point-to-point communication.

Attribute

Attribute is actually a piece of data. Each Bluetooth device is used to provide a service, and the service is a collection of data, the collection can be called a database, each entry in the database is an Attribute, so here I translate Attribute into data entries. You can imagine a Bluetooth device as a table, each row inside the table is an Attribute.

GATT

When two Bluetooth devices establish a connection, they need a protocol to determine how to communicate. GATT (Generic Attribute Profile) is such a protocol that defines how data is transmitted between Bluetooth devices.

In the GATT protocol, the functions and properties of a device are organized into structures called services, characteristics, and descriptors. A service represents a set of related functions and features provided by a device. Each service can include multiple characteristics, which define a certain property or behavior of the service, such as sensor data or control commands. Each characteristic has a unique identifier and a value, which can be read or written to communicate. Descriptors are used to describe metadata of characteristics, such as format and access permission of characteristic values.

By using the GATT protocol, Bluetooth devices can communicate in different application scenarios, such as transmitting sensor data or controlling remote devices.

BLE Characteristic

ATT, full name attribute protocol. In the end, ATT is composed of a group of ATT commands, that is, request and response commands, ATT is also the uppermost layer of the Bluetooth null packet, that is, ATT is where we analyze the Bluetooth packet the most.

ATT command, formally known as ATT PDU (Protocol Data Unit). It includes 4 categories: read, write, notify and indicate. These commands can be divided into two types: if it requires a response, then it will be followed by a request; on the contrary, if it only requires an ACK but not a response, then it will not be followed by a request.

Service and Characteristic are defined in the GATT layer. The Service side provides the Service, the Service is the data, and the data is the attribute, and the Service and Characteristic are the logical presentation of the data, or the data that the user can see are eventually transformed into the Service and Characteristic.

Let's take a look at what the service and characteristic look like from a mobile perspective. nRF Connect is an application that shows us very visually how each packet should look like.

As you can see, in the Bluetooth specification, each specific Bluetooth application is composed of multiple Services, and each Service is composed of multiple Characteristics. A Characteristic consists of a UUID, Properties, and a Value.

Properties are used to describe the types and permissions of operations on a characteristic, such as whether it supports read, write, notify, and so on. This is similar to the four categories included in an ATT PDU.

UUID

Each service, characteristic and descriptor have an UUID (Universally Unique Identifier). An UUID is a unique 128-bit (16 bytes) number. For example:

ea094cbd-3695-4205-b32d-70c1dea93c35

There are shortened UUIDs for all types, services, and profiles specified in the SIG (Bluetooth Special Interest Group). But if your application needs its own UUID, you can generate it using this UUID generator website.

BLE Scanner

Creating a XIAO MG24 BLE scanner is simple. The following is a sample program to create a scanner.

/*
BLE scan example

The example scans for other BLE devices and prints out the address, RSSI, channel and name for each found device.

Find out more on the Silabs BLE API usage at: https://docs.silabs.com/bluetooth/latest/bluetooth-stack-api/

This example only works with the 'BLE (Silabs)' protocol stack variant.

Compatible boards:
- Arduino Nano Matter
- SparkFun Thing Plus MGM240P
- xG27 DevKit
- xG24 Explorer Kit
- xG24 Dev Kit
- BGM220 Explorer Kit
- Ezurio Lyra 24P 20dBm Dev Kit
- Seeed Studio XIAO MG24 (Sense)

Author: Tamas Jozsi (Silicon Labs)
*/
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4

void setup() {
Serial.begin(115200);
}

void loop() {

}

static String get_complete_local_name_from_ble_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response);

/**************************************************************************/ /**
* Bluetooth stack event handler
* Called when an event happens on BLE the stack
*
* @param[in] evt Event coming from the Bluetooth stack
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t* evt) {
static uint32_t scan_report_num = 0u;
sl_status_t sc;

switch (SL_BT_MSG_ID(evt->header)) {
// This event is received when the BLE device has successfully booted
case sl_bt_evt_system_boot_id:
// Print a welcome message
Serial.begin(115200);

// turn on this antenna function
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);

delay(100);
// HIGH -> Use external antenna / LOW -> Use built-in antenna
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, HIGH);

Serial.println();
Serial.println("Silicon Labs BLE scan example");
Serial.println("BLE stack booted");
// Start scanning for other BLE devices
sc = sl_bt_scanner_set_parameters(sl_bt_scanner_scan_mode_active, // mode
16, // interval (value * 0.625 ms)
16); // window (value * 0.625 ms)
app_assert_status(sc);
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("Started scanning...");
break;

// This event is received when we scan the advertisement of another BLE device
case sl_bt_evt_scanner_legacy_advertisement_report_id:
scan_report_num++;
Serial.print(" -> #");
Serial.print(scan_report_num);
Serial.print(" | Address: ");
for (int i = 5; i >= 0; i--) {
Serial.printf("%02x", evt->data.evt_scanner_legacy_advertisement_report.address.addr[i]);
if (i > 0) {
Serial.print(":");
}
}
Serial.print(" | RSSI: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.rssi);
Serial.print(" dBm");
Serial.print(" | Channel: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.channel);
Serial.print(" | Name: ");
Serial.println(get_complete_local_name_from_ble_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report)));
break;

// Default event handler
default:
Serial.print("BLE event: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}

/**************************************************************************/ /**
* Finds the complete local name in BLE advertisements
*
* @param[in] response BLE response event received from scanning
*
* @return The complete local name if found, "N/A" otherwise
*****************************************************************************/
static String get_complete_local_name_from_ble_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response) {
int i = 0;
// Go through the response data
while (i < (response->data.len - 1)) {
uint8_t advertisement_length = response->data.data[i];
uint8_t advertisement_type = response->data.data[i + 1];

// If the length exceeds the maximum possible device name length
if (advertisement_length > 29) {
continue;
}

// Type 0x09 = Complete Local Name, 0x08 Shortened Name
// If the field type matches the Complete Local Name
if (advertisement_type == 0x09) {
// Copy the device name
char device_name[advertisement_length + 1];
memcpy(device_name, response->data.data + i + 2, advertisement_length);
device_name[advertisement_length] = '\0';
return String(device_name);
}
// Jump to next advertisement record
i = i + advertisement_length + 1;
}
return "N/A";
}

#ifndef BLE_STACK_SILABS
#error "This example is only compatible with the Silicon Labs BLE stack. Please select 'BLE (Silabs)' in 'Tools > Protocol stack'."
#endif
tip

It should be noted that 'BLE (Silabs)' in 'Tools > Protocol stack' needs to be selected before compilation.

Now you can select XIAO MG24 motherboard and upload the program. If the program runs smoothly, open the serial monitor and set the baud rate to 115200, you can see the following result.

This program prints out the name, MAC address, Channel and signal of the scanned Bluetooth device.

Program annotation

This example demonstrates how to scan for nearby Bluetooth Low Energy (BLE) devices using the Silicon Labs BLE stack, printing the address, RSSI (Received Signal Strength Indicator), channel, and name of each discovered device.

The code begins by defining an event handler functionsl_bt_on_event, which processes various Bluetooth Low Energy (BLE) events generated by the BLE stack. This function uses a switch statement to differentiate between event types, such as when the BLE device boots up and when it receives advertisement reports from nearby devices. Upon receiving the boot event, it initializes the serial communication, configures GPIO pins for antenna control, and starts scanning for BLE devices with specified parameters.

When the scanning process detects an advertisement report from a BLE device, the sl_bt_evt_scanner_legacy_advertisement_report_id case is triggered. In this case, the function increments a counter for each detected device and extracts key information, including the device's address, RSSI, channel, and local name. It utilizes the helper function get_complete_local_name_from_ble_advertisement to retrieve the complete name of the device from the advertisement data, which is then printed to the serial output.

The helper functionget_complete_local_name_from_ble_advertisement, iterates through the advertisement data to locate the complete local name field. It checks each advertisement record for the type that corresponds to the complete local name and returns it as a string. If the complete name is not found, the function returns "N/A." This systematic approach allows the application to effectively discover and identify nearby BLE devices, providing valuable information during the scanning process.

BLE server/client

As previously mentioned, XIAO MG24 can act as both a server and a client. Let's take a look at the program as a server and how to use it. After uploading the following program to XIAO, it will act as a server and send a "Hello World" message to all Bluetooth devices connected to XIAO.

//Server Code
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4

bool notification_enabled = false;

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.begin(115200);
Serial.println("Silicon Labs BLE send hello world example");

// turn on the antenna function
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);

delay(100);

// HIGH -> Use external antenna / LOW -> Use built-in antenna
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}

void loop() {
if (notification_enabled) {
// Send a notification every two seconds with the message 'hello world'
send_helloworld_notification();
}
delay(2000);
}

static void ble_initialize_gatt_db();
static void ble_start_advertising();

static const uint8_t advertised_name[] = "XIAO_MG24 Server"; // Name of your BLE device
static uint16_t gattdb_session_id;
static uint16_t generic_access_service_handle;
static uint16_t name_characteristic_handle;
static uint16_t my_service_handle;
static uint16_t led_control_characteristic_handle;
static uint16_t notify_characteristic_handle;

/**************************************************************************/ /**
* Bluetooth stack event handler
* Called when an event happens on BLE the stack
*
* @param[in] evt Event coming from the Bluetooth stack
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt) {
switch (SL_BT_MSG_ID(evt->header)) {
// -------------------------------
// This event indicates the device has started and the radio is ready.
// Do not call any stack command before receiving this boot event!
case sl_bt_evt_system_boot_id:
{
Serial.println("BLE stack booted");

// Initialize the application specific GATT table
ble_initialize_gatt_db();

// Start advertising
ble_start_advertising();
Serial.println("BLE advertisement started");
}
break;

// -------------------------------
// This event indicates that a new connection was opened
case sl_bt_evt_connection_opened_id:
Serial.println("BLE connection opened");
break;

// -------------------------------
// This event indicates that a connection was closed
case sl_bt_evt_connection_closed_id:
Serial.println("BLE connection closed");
// Restart the advertisement
ble_start_advertising();
Serial.println("BLE advertisement restarted");
break;

// -------------------------------
// This event indicates that the value of an attribute in the local GATT
// database was changed by a remote GATT client
case sl_bt_evt_gatt_server_attribute_value_id:
// Check if the changed characteristic is the LED control
if (led_control_characteristic_handle == evt->data.evt_gatt_server_attribute_value.attribute) {
Serial.println("LED control characteristic data received");
// Check the length of the received data
if (evt->data.evt_gatt_server_attribute_value.value.len == 0) {
break;
}
// Get the received byte
uint8_t received_data = evt->data.evt_gatt_server_attribute_value.value.data[0];
// Turn the LED on/off according to the received data
// If we receive a '0' - turn the LED off
// If we receive a '1' - turn the LED on
if (received_data == 0x00) {
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.println("LED off");
} else if (received_data == 0x01) {
Serial.println("LED on");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
}
}
break;

// -------------------------------
// This event is received when a GATT characteristic status changes
case sl_bt_evt_gatt_server_characteristic_status_id:
// If the 'Notify' characteristic has been changed
if (evt->data.evt_gatt_server_characteristic_status.characteristic == notify_characteristic_handle) {
// The client just enabled the notification - send notification of the current state
if (evt->data.evt_gatt_server_characteristic_status.client_config_flags & sl_bt_gatt_notification) {
Serial.println("change notification enabled");
notification_enabled = true;
} else {
Serial.println("change notification disabled");
notification_enabled = false;
}
}
break;

// -------------------------------
// Default event handler
default:
break;
}
}

/**************************************************************************/ /**
* Sends a BLE notification the the client if notifications are enabled
*****************************************************************************/
static void send_helloworld_notification() {
uint8_t str[12] = "Hello World";
sl_status_t sc = sl_bt_gatt_server_notify_all(notify_characteristic_handle,
sizeof(str),
(const uint8_t *)&str);
if (sc == SL_STATUS_OK) {
Serial.println("Send notification!");
}
}

/**************************************************************************/ /**
* Starts BLE advertisement
* Initializes advertising if it's called for the first time
*****************************************************************************/
static void ble_start_advertising() {
static uint8_t advertising_set_handle = 0xff;
static bool init = true;
sl_status_t sc;

if (init) {
// Create an advertising set
sc = sl_bt_advertiser_create_set(&advertising_set_handle);
app_assert_status(sc);

// Set advertising interval to 100ms
sc = sl_bt_advertiser_set_timing(
advertising_set_handle,
160, // minimum advertisement interval (milliseconds * 1.6)
160, // maximum advertisement interval (milliseconds * 1.6)
0, // advertisement duration
0); // maximum number of advertisement events
app_assert_status(sc);

init = false;
}

// Generate data for advertising
sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle, sl_bt_advertiser_general_discoverable);
app_assert_status(sc);

// Start advertising and enable connections
sc = sl_bt_legacy_advertiser_start(advertising_set_handle, sl_bt_advertiser_connectable_scannable);
app_assert_status(sc);
}

/**************************************************************************/ /**
* Initializes the GATT database
* Creates a new GATT session and adds certain services and characteristics
*****************************************************************************/
static void ble_initialize_gatt_db() {
sl_status_t sc;
// Create a new GATT database
sc = sl_bt_gattdb_new_session(&gattdb_session_id);
app_assert_status(sc);

// Add the Generic Access service to the GATT DB
const uint8_t generic_access_service_uuid[] = { 0x00, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(generic_access_service_uuid),
generic_access_service_uuid,
&generic_access_service_handle);
app_assert_status(sc);

// Add the Device Name characteristic to the Generic Access service
// The value of the Device Name characteristic will be advertised
const sl_bt_uuid_16_t device_name_characteristic_uuid = { .data = { 0x00, 0x2A } };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
generic_access_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
device_name_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
sizeof(advertised_name) - 1,
sizeof(advertised_name) - 1,
advertised_name,
&name_characteristic_handle);
app_assert_status(sc);

// Start the Generic Access service
sc = sl_bt_gattdb_start_service(gattdb_session_id, generic_access_service_handle);
app_assert_status(sc);

// Add my BLE service to the GATT DB
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 my_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(my_service_uuid),
my_service_uuid.data,
&my_service_handle);
app_assert_status(sc);

// Add the 'LED Control' characteristic to the Blinky service
// UUID: 5b026510-4088-c297-46d8-be6c736a087a
const uuid_128 led_control_characteristic_uuid = {
.data = { 0x7a, 0x08, 0x6a, 0x73, 0x6c, 0xbe, 0xd8, 0x46, 0x97, 0xc2, 0x88, 0x40, 0x10, 0x65, 0x02, 0x5b }
};
uint8_t led_char_init_value = 0;
sc = sl_bt_gattdb_add_uuid128_characteristic(gattdb_session_id,
my_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ | SL_BT_GATTDB_CHARACTERISTIC_WRITE,
0x00,
0x00,
led_control_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1, // max length
sizeof(led_char_init_value), // initial value length
&led_char_init_value, // initial value
&led_control_characteristic_handle);

// Start the Blinky service
sc = sl_bt_gattdb_start_service(gattdb_session_id, my_service_handle);
app_assert_status(sc);

// Add the 'Notify' characteristic to my BLE service
// UUID: 61a885a4-41c3-60d0-9a53-6d652a70d29c
const uuid_128 btn_report_characteristic_uuid = {
.data = { 0x9c, 0xd2, 0x70, 0x2a, 0x65, 0x6d, 0x53, 0x9a, 0xd0, 0x60, 0xc3, 0x41, 0xa4, 0x85, 0xa8, 0x61 }
};
uint8_t notify_char_init_value = 0;
sc = sl_bt_gattdb_add_uuid128_characteristic(gattdb_session_id,
my_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ | SL_BT_GATTDB_CHARACTERISTIC_NOTIFY,
0x00,
0x00,
btn_report_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1, // max length
sizeof(notify_char_init_value), // initial value length
&notify_char_init_value, // initial value
&notify_characteristic_handle);

// Start my BLE service
sc = sl_bt_gattdb_start_service(gattdb_session_id, my_service_handle);
app_assert_status(sc);

// Commit the GATT DB changes
sc = sl_bt_gattdb_commit(gattdb_session_id);
app_assert_status(sc);
}

#ifndef BLE_STACK_SILABS
#error "This example is only compatible with the Silicon Labs BLE stack. Please select 'BLE (Silabs)' in 'Tools > Protocol stack'."
#endif

Meanwhile, you can search and download the nRF Connect app in major mobile app stores, which allows your phone to search for and connect to Bluetooth devices.

After downloading the software, follow the steps shown below to search for and connect XIAO_MG24, and you will see the advertised "Hello World".

If you want to use another XIAO MG24 as a client to receive messages from the server, then you can use the following procedure for the client XIAO.

// Client Code
#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4

// Connection states
enum conn_state_t {
ST_BOOT,
ST_SCAN,
ST_CONNECT,
ST_SERVICE_DISCOVER,
ST_CHAR_DISCOVER,
ST_READY
};

conn_state_t connection_state = ST_BOOT;
uint8_t connection_handle = __UINT8_MAX__;
uint32_t blinky_service_handle = __UINT32_MAX__;
uint16_t led_control_char_handle = __UINT16_MAX__;
bool gatt_procedure_in_progress = false;

// If there's no built-in button set a pin where a button is connected
#ifndef BTN_BUILTIN
#define BTN_BUILTIN D0
#endif

void setup() {
// Set the built-in LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Turn the built-in LED off
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
// Set the built-in button as input
pinMode(BTN_BUILTIN, INPUT);
// Start Serial
Serial.begin(115200);

// turn on the antenna function
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);

delay(100);

// HIGH -> Use external antenna / LOW -> Use built-in antenna
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}

void loop() {
// Static variable for remembering the previous state of the button
static uint8_t btn_state_prev = LOW;
// If the connection is fully established and we don't have any ongoing GATT procedures
if (connection_state == ST_READY && !gatt_procedure_in_progress) {
// Read the current state of the button
uint8_t btn_state = digitalRead(BTN_BUILTIN);
// If the current state is different than the previous state
if (btn_state_prev != btn_state) {
// Update the previous state
btn_state_prev = btn_state;
// Invert the state (the SL board buttons give a 0 when pressed and 1 when released)
uint8_t btn_state_inv = !btn_state;
// Log the state change
Serial.print("Sending button state: ");
Serial.println(btn_state_inv);
// Send the new state over BLE by writing the other device's LED Control characteristic
sl_status_t sc = sl_bt_gatt_write_characteristic_value(connection_handle, led_control_char_handle, sizeof(uint8_t), &btn_state_inv);
app_assert_status(sc);
gatt_procedure_in_progress = true;
}
}
}

// Blinky service
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 blinky_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};

// LED Control characteristic
// UUID: 5b026510-4088-c297-46d8-be6c736a087a
const uuid_128 led_control_characteristic_uuid = {
.data = { 0x7a, 0x08, 0x6a, 0x73, 0x6c, 0xbe, 0xd8, 0x46, 0x97, 0xc2, 0x88, 0x40, 0x10, 0x65, 0x02, 0x5b }
};
const uint8_t advertised_name[] = "XIAO_MG24 Server";

static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response);

/**************************************************************************/ /**
* Bluetooth stack event handler
* Called when an event happens on BLE the stack
*
* @param[in] evt Event coming from the Bluetooth stack
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t* evt) {
static uint32_t scan_report_num = 0u;
sl_status_t sc;

switch (SL_BT_MSG_ID(evt->header)) {
// This event is received when the BLE device has successfully booted
case sl_bt_evt_system_boot_id:
// Print a welcome message
Serial.println();
Serial.println("Silicon Labs BLE light switch client example");
Serial.println("BLE stack booted");
// Start scanning for other BLE devices
sc = sl_bt_scanner_set_parameters(sl_bt_scanner_scan_mode_active, 16, 16);
app_assert_status(sc);
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("Started scanning...");
connection_state = ST_SCAN;
break;

// This event is received when we scan the advertisement of another BLE device
case sl_bt_evt_scanner_legacy_advertisement_report_id:
scan_report_num++;
Serial.print(" -> #");
Serial.print(scan_report_num);
Serial.print(" | Address: ");
for (int i = 5; i >= 0; i--) {
Serial.printf("%02x", evt->data.evt_scanner_legacy_advertisement_report.address.addr[i]);
if (i > 0) {
Serial.print(":");
}
}
Serial.print(" | RSSI: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.rssi);
Serial.print(" dBm");
Serial.print(" | Channel: ");
Serial.print(evt->data.evt_scanner_legacy_advertisement_report.channel);
Serial.print(" | Name: ");
Serial.println(find_complete_local_name_in_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report)));

// If we find the other devices's name
if (find_complete_local_name_in_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report))) {
Serial.println("Target device found!");
Serial.print("Forming a connection to ");
for (int i = 5; i >= 0; i--) {
Serial.printf("%02x", evt->data.evt_scanner_legacy_advertisement_report.address.addr[i]);
if (i > 0) {
Serial.print(":");
}
}
Serial.println(" ");

// Stop scanning
sc = sl_bt_scanner_stop();
app_assert_status(sc);
// Connect to the device
sc = sl_bt_connection_open(evt->data.evt_scanner_legacy_advertisement_report.address,
evt->data.evt_scanner_legacy_advertisement_report.address_type,
sl_bt_gap_phy_1m,
NULL);
// app_assert_status(sc);
connection_state = ST_CONNECT;

Serial.println("We are now connected to the BLE Server");
}
break;

// This event is received when a BLE connection has been opened
case sl_bt_evt_connection_opened_id:
Serial.println("Connection opened");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
connection_handle = evt->data.evt_connection_opened.connection;
// Discover Health Thermometer service on the connected device
sc = sl_bt_gatt_discover_primary_services_by_uuid(connection_handle,
sizeof(blinky_service_uuid),
blinky_service_uuid.data);
app_assert_status(sc);
gatt_procedure_in_progress = true;
connection_state = ST_SERVICE_DISCOVER;
break;

// This event is received when a BLE connection has been closed
case sl_bt_evt_connection_closed_id:
Serial.println("Connection closed");
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
connection_handle = __UINT8_MAX__;
// Restart scanning
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("Restarted scanning...");
connection_state = ST_SCAN;
break;

// This event is generated when a new service is discovered
case sl_bt_evt_gatt_service_id:
Serial.println("GATT service discovered");
// Store the handle of the discovered Thermometer Service
blinky_service_handle = evt->data.evt_gatt_service.service;
break;

// This event is generated when a new characteristic is discovered
case sl_bt_evt_gatt_characteristic_id:
Serial.println("GATT charactersitic discovered");
// Store the handle of the discovered Temperature Measurement characteristic
led_control_char_handle = evt->data.evt_gatt_characteristic.characteristic;
break;

// This event is received when a GATT procedure completes
case sl_bt_evt_gatt_procedure_completed_id:
Serial.println("GATT procedure completed");
gatt_procedure_in_progress = false;

if (connection_state == ST_SERVICE_DISCOVER) {
Serial.println("GATT service discovery finished");
// Discover thermometer characteristic on the connected device
sc = sl_bt_gatt_discover_characteristics_by_uuid(evt->data.evt_gatt_procedure_completed.connection,
blinky_service_handle,
sizeof(led_control_characteristic_uuid.data),
led_control_characteristic_uuid.data);
app_assert_status(sc);
gatt_procedure_in_progress = true;
connection_state = ST_CHAR_DISCOVER;
break;
}

if (connection_state == ST_CHAR_DISCOVER) {
Serial.println("GATT characteristic discovery finished");
connection_state = ST_READY;
break;
}
break;

// Default event handler
default:
Serial.print("BLE event: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}

/**************************************************************************/ /**
* Finds a configured name in BLE advertisements
*
* @param[in] response BLE response event received from scanning
*
* @return true if found, false otherwise
*****************************************************************************/
static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t* response) {
int i = 0;
bool found = false;

// Go through the response data
while (i < (response->data.len - 1)) {
uint8_t advertisement_length = response->data.data[i];
uint8_t advertisement_type = response->data.data[i + 1];

// Type 0x09 = Complete Local Name, 0x08 Shortened Name
// If the field type matches the Complete Local Name
if (advertisement_type == 0x09) {
// Check if device name matches
if (memcmp(response->data.data + i + 2, advertised_name, strlen((const char*)advertised_name)) == 0) {
found = true;
break;
}
}
// Jump to next advertisement record
i = i + advertisement_length + 1;
}
return found;
}

#ifndef BLE_STACK_SILABS
#error "This example is only compatible with the Silicon Labs BLE stack. Please select 'BLE (Silabs)' in 'Tools > Protocol stack'."
#endif

The above program will turn XIAO into a client and search for nearby Bluetooth devices. When the UUID of the Bluetooth device matches the UUID you provided, it will connect to the device and obtain its characteristic value.

Program annotation

Let’s take a quick look at how the BLE server example code works. It starts by importing the necessary libraries for the BLE capabilities. Then, you need to define a UUID for the Service and Characteristic.

// Add my BLE service to the GATT DB
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 my_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};

// Add the 'Notify' characteristic to my BLE service
// UUID: 61a885a4-41c3-60d0-9a53-6d652a70d29c
const uuid_128 btn_report_characteristic_uuid = {
.data = { 0x9c, 0xd2, 0x70, 0x2a, 0x65, 0x6d, 0x53, 0x9a, 0xd0, 0x60, 0xc3, 0x41, 0xa4, 0x85, 0xa8, 0x61 }
};

You can leave the default UUIDs, or you can go to uuidgenerator.net to create random UUIDs for your services and characteristics.

Then, you create a BLE device called “XIAO_MG24 Server”. You can change this name to whatever you like. In the following line, you set the BLE device as a server. After that, you create a service for the BLE server with the UUID defined earlier.

sl_status_t sc;
// Create a new GATT database
sc = sl_bt_gattdb_new_session(&gattdb_session_id);
app_assert_status(sc);

// Add the Generic Access service to the GATT DB
const uint8_t generic_access_service_uuid[] = { 0x00, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(generic_access_service_uuid),
generic_access_service_uuid,
&generic_access_service_handle);
app_assert_status(sc);

// Add the Device Name characteristic to the Generic Access service
// The value of the Device Name characteristic will be advertised
const sl_bt_uuid_16_t device_name_characteristic_uuid = { .data = { 0x00, 0x2A } };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
generic_access_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
device_name_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
sizeof(advertised_name) - 1,
sizeof(advertised_name) - 1,
advertised_name,
&name_characteristic_handle);
app_assert_status(sc);

// Start the Generic Access service
sc = sl_bt_gattdb_start_service(gattdb_session_id, generic_access_service_handle);
app_assert_status(sc);

// Add my BLE service to the GATT DB
// UUID: de8a5aac-a99b-c315-0c80-60d4cbb51224
const uuid_128 my_service_uuid = {
.data = { 0x24, 0x12, 0xb5, 0xcb, 0xd4, 0x60, 0x80, 0x0c, 0x15, 0xc3, 0x9b, 0xa9, 0xac, 0x5a, 0x8a, 0xde }
};
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(my_service_uuid),
my_service_uuid.data,
&my_service_handle);
app_assert_status(sc);

Then, you set the characteristic for that service. As you can see, you also use the UUID defined earlier, and you need to pass as arguments the characteristic’s properties. In this case, it’s: READ and NOTIFY.

// Add the 'Notify' characteristic to my BLE service
// UUID: 61a885a4-41c3-60d0-9a53-6d652a70d29c
const uuid_128 btn_report_characteristic_uuid = {
.data = { 0x9c, 0xd2, 0x70, 0x2a, 0x65, 0x6d, 0x53, 0x9a, 0xd0, 0x60, 0xc3, 0x41, 0xa4, 0x85, 0xa8, 0x61 }
};
uint8_t notify_char_init_value = 0;
sc = sl_bt_gattdb_add_uuid128_characteristic(gattdb_session_id,
my_service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ | SL_BT_GATTDB_CHARACTERISTIC_NOTIFY,
0x00,
0x00,
btn_report_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1, // max length
sizeof(notify_char_init_value), // initial value length
&notify_char_init_value, // initial value
&notify_characteristic_handle);

// Start my BLE service
sc = sl_bt_gattdb_start_service(gattdb_session_id, my_service_handle);
app_assert_status(sc);

// Commit the GATT DB changes
sc = sl_bt_gattdb_commit(gattdb_session_id);
app_assert_status(sc);

After creating the characteristic, you can set its value with the sl_bt_gatt_server_notify_all() method. In this case we’re setting the value to the text “Hello World”. You can change this text to whatever your like. In future projects, this text can be a sensor reading, or the state of a lamp, for example.

Finally, you can start the service, and the advertising, so other BLE devices can scan and find this BLE device.

// Start advertising
ble_start_advertising();

This is just a simple example on how to create a BLE server. The function of this program is to send notifications every two seconds, with the content being "Hello World".

BLE Sensor Data Exchange

Next, we'll come to the real world to complete a case. In this case, we will use the function getCPUTemp() of XIAO MG24 to measure the temperature of the current MCU, and then send the temperature value of the MCU to another XIAO MG24 via Bluetooth to simulate a health thermometer.

We need to prepare two XIAO, one as server and one as client. Here is the sample program as a server. XIAO as a server has the following main tasks.

  • First, use the function getCPUTemp() to get the current temperature of MCU;
  • Second, create the Bluetooth server;
  • Third, advertise the temperature values through Bluetooth;
  • Fourth, show the real-time temperature.
// server

/*
BLE health thermometer example

The example implements a minimal BLE Health Thermometer profile to provide temperature measurements over BLE

On startup the sketch will start a BLE advertisement with the configured name, then
it will accept any incoming connection. When a device is connected and enables indications for the
health thermometer characteristic, then the device will send it's CPU temperature readings as thermometer data.
With the EFR Connect app you can test this functionality by going to the "Demo" tab and selecting "Health Thermometer".
Alternatively, you can test this example by flashing an other BLE board with the 'ble_health_thermometer_client' demo
and have the two boards exchange the temperature measurements over BLE.

Find out more on the API usage at: https://docs.silabs.com/bluetooth/latest/bluetooth-stack-api/

This example only works with the 'BLE (Silabs)' protocol stack variant.

You can test the thermometer device with the EFR Connect app:
- https://play.google.com/store/apps/details?id=com.siliconlabs.bledemo
- https://apps.apple.com/us/app/efr-connect-ble-mobile-app/id1030932759

Compatible boards:
- Arduino Nano Matter
- SparkFun Thing Plus MGM240P
- xG27 DevKit
- xG24 Explorer Kit
- xG24 Dev Kit
- BGM220 Explorer Kit
- Ezurio Lyra 24P 20dBm Dev Kit
- Seeed Studio XIAO MG24 (Sense)

Author: Tamas Jozsi (Silicon Labs)
*/

#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4

static void handle_temperature_indication();
static void ble_initialize_gatt_db();
static void ble_start_advertising();

const uint8_t advertised_name[] = "XIAOMG24_BLE";
uint8_t connection_handle = 0u;
uint16_t temp_measurement_characteristic_handle = 0u;
bool indication_enabled = false;

void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.begin(115200);

// turn on this antenna function
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);

delay(100);

// HIGH -> Use external antenna / LOW -> Use built-in antenna
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}

void loop()
{
handle_temperature_indication();
}

/**************************************************************************//**
* Sends a BLE indication with the current temperature to the connected device
* if enabled, then waits for a second
*****************************************************************************/
static void handle_temperature_indication()
{
// Return immediately if indications are not enabled
if (!indication_enabled) {
return;
}

// Get the current CPU temperature
float temperature = getCPUTemp();

// Convert the temperature to an IEEE 11073 float value
int32_t millicelsius = (int32_t)(temperature * 1000);
uint8_t buffer[5];
uint32_t tmp_value = ((uint32_t)millicelsius & 0x00ffffffu) | ((uint32_t)(-3) << 24);
buffer[0] = 0;
buffer[1] = tmp_value & 0xff;
buffer[2] = (tmp_value >> 8) & 0xff;
buffer[3] = (tmp_value >> 16) & 0xff;
buffer[4] = (tmp_value >> 24) & 0xff;

// Send the indication
sl_bt_gatt_server_send_indication(connection_handle, temp_measurement_characteristic_handle, sizeof(buffer), buffer);

// Log the temperature
Serial.print("Temperature indication sent - current temperature: ");
Serial.print(temperature);
Serial.println(" C");

// Wait for a second
delay(1000);
}

/**************************************************************************//**
* Bluetooth stack event handler
* Called when an event happens on BLE the stack
*
* @param[in] evt Event coming from the Bluetooth stack
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
switch (SL_BT_MSG_ID(evt->header)) {
// This event is received when the BLE device has successfully booted
case sl_bt_evt_system_boot_id:
{
// Print a welcome message
Serial.begin(115200);
Serial.println();
Serial.println("Silicon Labs BLE health thermometer example");
Serial.println("BLE stack booted");
// Initialize the application specific GATT DB
ble_initialize_gatt_db();
// Start advertising
ble_start_advertising();
}
break;

// This event is received when a BLE connection has been opened
case sl_bt_evt_connection_opened_id:
// Store the connection handle which will be needed for sending indications
connection_handle = evt->data.evt_connection_opened.connection;
Serial.println("Connection opened");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
break;

// This event is received when a BLE connection has been closed
case sl_bt_evt_connection_closed_id:
// Reset stored values
connection_handle = 0u;
indication_enabled = false;
Serial.println("Connection closed");
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
// Restart the advertisement
ble_start_advertising();
break;

// This event is received when a GATT characteristic status changes
case sl_bt_evt_gatt_server_characteristic_status_id:
{
// If the temperature measurement characteristic has been changed
if (evt->data.evt_gatt_server_characteristic_status.characteristic == temp_measurement_characteristic_handle) {
uint16_t client_config_flags = evt->data.evt_gatt_server_characteristic_status.client_config_flags;
uint8_t status_flags = evt->data.evt_gatt_server_characteristic_status.status_flags;
if ((client_config_flags == 0x02) && (status_flags == 0x01)) {
// If indication was enabled (0x02) in the client config flags, and the status flag shows that it's a change
Serial.println("Temperature indication enabled");
indication_enabled = true;
} else if ((client_config_flags == 0x00) && (status_flags == 0x01)) {
// If indication was disabled (0x00) in the client config flags, and the status flag shows that it's a change
Serial.println("Temperature indication disabled");
indication_enabled = false;
}
}
}
break;

// Default event handler
default:
Serial.print("BLE event: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}

/**************************************************************************//**
* Starts BLE advertisement
* Initializes advertising if it's called for the first time
*****************************************************************************/
static void ble_start_advertising()
{
static uint8_t advertising_set_handle = 0xff;
static bool init = true;
sl_status_t sc;

if (init) {
// Create an advertising set
sc = sl_bt_advertiser_create_set(&advertising_set_handle);
app_assert_status(sc);

// Set advertising interval to 100ms
sc = sl_bt_advertiser_set_timing(
advertising_set_handle,
160, // minimum advertisement interval (milliseconds * 1.6)
160, // maximum advertisement interval (milliseconds * 1.6)
0, // advertisement duration
0); // maximum number of advertisement events
app_assert_status(sc);

init = false;
}

// Generate data for advertising
sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle, sl_bt_advertiser_general_discoverable);
app_assert_status(sc);

// Start advertising and enable connections
sc = sl_bt_legacy_advertiser_start(advertising_set_handle, sl_bt_advertiser_connectable_scannable);
app_assert_status(sc);

Serial.print("Started advertising as '");
Serial.print((const char*)advertised_name);
Serial.println("'...");
}

/**************************************************************************//**
* Initializes the GATT database
* Creates a new GATT session and adds certain services and characteristics
*****************************************************************************/
static void ble_initialize_gatt_db()
{
sl_status_t sc;
uint16_t gattdb_session_id;
uint16_t service_handle;
uint16_t device_name_characteristic_handle;
uint16_t temp_type_characteristic_handle;

// Create a new GATT database
sc = sl_bt_gattdb_new_session(&gattdb_session_id);
app_assert_status(sc);

// Generic Access service
const uint8_t generic_access_service_uuid[] = { 0x00, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(generic_access_service_uuid),
generic_access_service_uuid,
&service_handle);
app_assert_status(sc);

// Device Name characteristic
const sl_bt_uuid_16_t device_name_characteristic_uuid = { .data = { 0x00, 0x2A } };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
device_name_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
sizeof(advertised_name) - 1,
sizeof(advertised_name) - 1,
advertised_name,
&device_name_characteristic_handle);
app_assert_status(sc);

sc = sl_bt_gattdb_start_service(gattdb_session_id, service_handle);
app_assert_status(sc);

// Health Thermometer service
const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(thermometer_service_uuid),
thermometer_service_uuid,
&service_handle);
app_assert_status(sc);

// Temperature Measurement characteristic
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
uint8_t temp_initial_value[5] = { 0, 0, 0, 0, 0 };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_INDICATE,
0x00,
0x00,
temp_measurement_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
5,
5,
temp_initial_value,
&temp_measurement_characteristic_handle);
app_assert_status(sc);

// Temperature Type characteristic
const sl_bt_uuid_16_t temp_type_characteristic_uuid = { .data = { 0x1D, 0x2A } };
// Temperature type: body (2)
uint8_t temp_type_initial_value = 2;
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_READ,
0x00,
0x00,
temp_type_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
1,
1,
&temp_type_initial_value,
&temp_type_characteristic_handle);
app_assert_status(sc);

// Start the Health Thermometer service
sc = sl_bt_gattdb_start_service(gattdb_session_id, service_handle);
app_assert_status(sc);

// Commit the GATT DB changes
sc = sl_bt_gattdb_commit(gattdb_session_id);
app_assert_status(sc);
}

#ifndef BLE_STACK_SILABS
#error "This example is only compatible with the Silicon Labs BLE stack. Please select 'BLE (Silabs)' in 'Tools > Protocol stack'."
#endif

After uploading the program for one of the XIAO, if the program runs smoothly, then you can take out your phone and use the nRF Connect APP to search for the Bluetooth device named XIAOMG24_BLE, connect it, and click the button shown below, you will receive the temperature data information.

Next, we need to take out our other XIAO, which acts as a client to collect and display our data.

// client

/*
BLE health thermometer client example

The example connects to another board running the 'BLE Health Thermometer' example and reads the temperature through BLE

On startup the sketch will start a scanning for the other board running the 'ble_health_thermometer' example and
advertising as "Thermometer Example". Once the other board is found, it establishes a connection,
discovers it's services and characteristics, then subscribes to the temperature measurements.
After the subscription the example starts receiving the temperature data from the other board periodically,
and prints it to Serial.

Find out more on the API usage at: https://docs.silabs.com/bluetooth/latest/bluetooth-stack-api/

This example only works with the 'BLE (Silabs)' protocol stack variant.

Compatible boards:
- Arduino Nano Matter
- SparkFun Thing Plus MGM240P
- xG27 DevKit
- xG24 Explorer Kit
- xG24 Dev Kit
- BGM220 Explorer Kit
- Ezurio Lyra 24P 20dBm Dev Kit
- Seeed Studio XIAO MG24 (Sense)

Author: Tamas Jozsi (Silicon Labs)
*/

#define RF_SW_PW_PIN PB5
#define RF_SW_PIN PB4

void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
Serial.begin(115200);

// turn on this antenna function
pinMode(RF_SW_PW_PIN, OUTPUT);
digitalWrite(RF_SW_PW_PIN, HIGH);

delay(100);

// HIGH -> Use external antenna / LOW -> Use built-in antenna
pinMode(RF_SW_PIN, OUTPUT);
digitalWrite(RF_SW_PIN, LOW);
}

void loop()
{
}

// Connection states
enum conn_state_t {
ST_BOOT,
ST_SCAN,
ST_CONNECT,
ST_SERVICE_DISCOVER,
ST_CHAR_DISCOVER,
ST_REQUEST_INDICATION,
ST_RECEIVE_DATA
};

// IEEE 11073 float structure
typedef struct {
uint8_t mantissa_l;
uint8_t mantissa_m;
int8_t mantissa_h;
int8_t exponent;
} IEEE_11073_float;

static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t *response);
static float translate_IEEE_11073_temperature_to_float(IEEE_11073_float const *IEEE_11073_value);

const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
const uint8_t advertised_name[] = "XIAOMG24_BLE";

uint32_t thermometer_service_handle = __UINT32_MAX__;
uint16_t temp_measurement_char_handle = __UINT16_MAX__;
conn_state_t connection_state = ST_BOOT;

/**************************************************************************//**
* Bluetooth stack event handler
* Called when an event happens on BLE the stack
*
* @param[in] evt Event coming from the Bluetooth stack
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
sl_status_t sc;

switch (SL_BT_MSG_ID(evt->header)) {
// This event is received when the BLE device has successfully booted
case sl_bt_evt_system_boot_id:
// Print a welcome message
Serial.println();
Serial.println("Silicon Labs BLE health thermometer client example");
Serial.println("BLE stack booted");
// Start scanning for other BLE devices
sc = sl_bt_scanner_set_parameters(sl_bt_scanner_scan_mode_active, 16, 16);
app_assert_status(sc);
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("Started scanning...");
connection_state = ST_SCAN;
break;

// This event is received when we scan the advertisement of another BLE device
case sl_bt_evt_scanner_legacy_advertisement_report_id:
Serial.println("BLE scan report received");
// If we find the other devices's name
if (find_complete_local_name_in_advertisement(&(evt->data.evt_scanner_legacy_advertisement_report))) {
Serial.println("Target device found");
// Stop scanning
sc = sl_bt_scanner_stop();
app_assert_status(sc);
// Connect to the device
sc = sl_bt_connection_open(evt->data.evt_scanner_legacy_advertisement_report.address,
evt->data.evt_scanner_legacy_advertisement_report.address_type,
sl_bt_gap_phy_1m,
NULL);
app_assert_status(sc);
connection_state = ST_CONNECT;
}
break;

// This event is received when a BLE connection has been opened
case sl_bt_evt_connection_opened_id:
Serial.println("Connection opened");
digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE);
// Discover Health Thermometer service on the connected device
sc = sl_bt_gatt_discover_primary_services_by_uuid(evt->data.evt_connection_opened.connection,
sizeof(thermometer_service_uuid),
thermometer_service_uuid);
app_assert_status(sc);
connection_state = ST_SERVICE_DISCOVER;
break;

// This event is received when a BLE connection has been closed
case sl_bt_evt_connection_closed_id:
Serial.println("Connection closed");
digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE);
// Restart scanning
sc = sl_bt_scanner_start(sl_bt_scanner_scan_phy_1m,
sl_bt_scanner_discover_generic);
app_assert_status(sc);
Serial.println("Restarted scanning...");
connection_state = ST_SCAN;
break;

// This event is generated when a new service is discovered
case sl_bt_evt_gatt_service_id:
Serial.println("GATT service discovered");
// Store the handle of the discovered Thermometer Service
thermometer_service_handle = evt->data.evt_gatt_service.service;
break;

// This event is generated when a new characteristic is discovered
case sl_bt_evt_gatt_characteristic_id:
Serial.println("GATT charactersitic discovered");
// Store the handle of the discovered Temperature Measurement characteristic
temp_measurement_char_handle = evt->data.evt_gatt_characteristic.characteristic;
break;

// This event is received when a GATT procedure completes
case sl_bt_evt_gatt_procedure_completed_id:
Serial.println("GATT procedure completed");

if (connection_state == ST_SERVICE_DISCOVER) {
Serial.println("GATT service discovery finished");
// Discover thermometer characteristic on the connected device
sc = sl_bt_gatt_discover_characteristics_by_uuid(evt->data.evt_gatt_procedure_completed.connection,
thermometer_service_handle,
sizeof(temp_measurement_characteristic_uuid.data),
temp_measurement_characteristic_uuid.data);
app_assert_status(sc);
connection_state = ST_CHAR_DISCOVER;
break;
}

if (connection_state == ST_CHAR_DISCOVER) {
Serial.println("GATT characteristic discovery finished");
// Enable temperature measurement indications
sc = sl_bt_gatt_set_characteristic_notification(evt->data.evt_gatt_procedure_completed.connection,
temp_measurement_char_handle,
sl_bt_gatt_indication);
app_assert_status(sc);
connection_state = ST_REQUEST_INDICATION;
break;
}

if (connection_state == ST_REQUEST_INDICATION) {
Serial.println("Temperature measurement indications enabled");
connection_state = ST_RECEIVE_DATA;
}
break;

// This event is received when a characteristic value was received (like an indication)
case sl_bt_evt_gatt_characteristic_value_id:
{
Serial.println("GATT data received");
// Get the received data from the event
uint8_t* char_value = &(evt->data.evt_gatt_characteristic_value.value.data[0]);
// Convert it back to float
float temperature = translate_IEEE_11073_temperature_to_float((IEEE_11073_float *)(char_value + 1));
// Print to Serial
Serial.print("Received temperature: ");
Serial.print(temperature);
Serial.println(" C");

sc = sl_bt_gatt_send_characteristic_confirmation(evt->data.evt_gatt_characteristic_value.connection);
app_assert_status(sc);
}
break;

// Default event handler
default:
Serial.print("BLE event: 0x");
Serial.println(SL_BT_MSG_ID(evt->header), HEX);
break;
}
}

/**************************************************************************//**
* Finds a configured name in BLE advertisements
*
* @param[in] response BLE response event received from scanning
*
* @return true if found, false otherwise
*****************************************************************************/
static bool find_complete_local_name_in_advertisement(sl_bt_evt_scanner_legacy_advertisement_report_t *response)
{
int i = 0;
bool found = false;

// Go through the response data
while (i < (response->data.len - 1)) {
uint8_t advertisement_length = response->data.data[i];
uint8_t advertisement_type = response->data.data[i + 1];

// Type 0x09 = Complete Local Name, 0x08 Shortened Name
// If the field type matches the Complete Local Name
if (advertisement_type == 0x09) {
// Check if device name matches
if (memcmp(response->data.data + i + 2, advertised_name, strlen((const char*)advertised_name)) == 0) {
found = true;
break;
}
}
// Jump to next advertisement record
i = i + advertisement_length + 1;
}
return found;
}

/**************************************************************************//**
* Translates a IEEE-11073 temperature value to float
*
* @param[in] IEEE_11073_value the IEEE 11073 float value to convert
*
* @return the converted value in float, NAN on failure
*****************************************************************************/
static float translate_IEEE_11073_temperature_to_float(IEEE_11073_float const *IEEE_11073_value)
{
int32_t mantissa = 0;
uint8_t mantissa_l;
uint8_t mantissa_m;
int8_t mantissa_h;
int8_t exponent;

// Wrong Argument: NULL pointer is passed
if (!IEEE_11073_value) {
return NAN;
}

// Caching Fields
mantissa_l = IEEE_11073_value->mantissa_l;
mantissa_m = IEEE_11073_value->mantissa_m;
mantissa_h = IEEE_11073_value->mantissa_h;
exponent = IEEE_11073_value->exponent;

// IEEE-11073 Standard NaN Value Passed
if ((mantissa_l == 0xFF) && (mantissa_m == 0xFF) && (mantissa_h == 0x7F) && (exponent == 0x00)) {
return NAN;
}

// Converting a 24bit Signed Value to a 32bit Signed Value
mantissa |= mantissa_h;
mantissa <<= 8;
mantissa |= mantissa_m;
mantissa <<= 8;
mantissa |= mantissa_l;
mantissa <<= 8;
mantissa >>= 8;

return ((float)mantissa) * pow(10.0f, (float)exponent);
}

#ifndef BLE_STACK_SILABS
#error "This example is only compatible with the Silicon Labs BLE stack. Please select 'BLE (Silabs)' in 'Tools > Protocol stack'."
#endif

Finally, if the Server and Client programs run smoothly, you can see the following information printed by the Client through the serial port.

Program annotation

For the above programs, we will pick the more important parts to explain. We'll start with the server program.

At the beginning of the program, we define the name of the Bluetooth server, this name can be the name you set, but you need to remember it because you need to rely on this name to search for this Bluetooth device.

const uint8_t advertised_name[] = "XIAOMG24_BLE";

In the previous sections of the tutorial, we have talked about that under the server there will be Characteristic, and under Characteristic there will be the values and the rest of the content. So we need to follow this one principle when we create ads.

// Health Thermometer service
const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
sc = sl_bt_gattdb_add_service(gattdb_session_id,
sl_bt_gattdb_primary_service,
SL_BT_GATTDB_ADVERTISED_SERVICE,
sizeof(thermometer_service_uuid),
thermometer_service_uuid,
&service_handle);
app_assert_status(sc);

// Temperature Measurement characteristic
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
uint8_t temp_initial_value[5] = { 0, 0, 0, 0, 0 };
sc = sl_bt_gattdb_add_uuid16_characteristic(gattdb_session_id,
service_handle,
SL_BT_GATTDB_CHARACTERISTIC_INDICATE,
0x00,
0x00,
temp_measurement_characteristic_uuid,
sl_bt_gattdb_fixed_length_value,
5,
5,
temp_initial_value,
&temp_measurement_characteristic_handle);
app_assert_status(sc);

In the above program, you can see that sl_bt_gattdb_add_service() is used to create a server. The parameter is a specific UUID: 0x1809. In the rules of GATT, 0x1809 indicates the thermometer type data, and the UUID of the same Characteristic: 0x2A1C also has a special meaning. In GATT, it indicates the temperature measurement. This fits the case of our temperature values, so here I am defining it as such. You can read here what some of the specific UUIDs that GATT has prepared for us mean.

Of course, you can also set the UUIDs without following the GATT standard, you just need to make sure that these two values are unique and will not affect your client's ability to find the values by recognizing these UUIDs. You can go to uuidgenerator.net to create random UUIDs for your services and characteristics.

Finally, we measure and advertise the temperature value of the MCU once per second in the loop.

The next step is the Client program, which will seem much more complicated.

At the beginning of the program, it's still very familiar content. You need to make sure that this content is consistent with what you have configured on the server side.

const uint8_t thermometer_service_uuid[] = { 0x09, 0x18 };
const sl_bt_uuid_16_t temp_measurement_characteristic_uuid = { .data = { 0x1C, 0x2A } };
const uint8_t advertised_name[] = "XIAOMG24_BLE";

Next, we will write a Bluetooth stack event handler function, which mainly handles callback tasks triggered by various Bluetooth events, including initialization of Bluetooth devices, connection and disconnection of Bluetooth, and searching for nearby Bluetooth devices.

/**************************************************************************//**
* Bluetooth stack event handler
* Called when an event happens on BLE the stack
*
* @param[in] evt Event coming from the Bluetooth stack
*****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)

The following process is the key to finding temperature values in the server. Firstly, after we successfully locate our server UUID and find the characteristic UUID under the server, we will process the obtained data, as shown in the following code snippet. Finally, print out the processed data through the serial port. This parsing method is a one-to-one correspondence with the data structure of Bluetooth.

void sl_bt_on_event(sl_bt_msg_t *evt)
{
sl_status_t sc;

switch (SL_BT_MSG_ID(evt->header)) {

...

// This event is received when a characteristic value was received (like an indication)
case sl_bt_evt_gatt_characteristic_value_id:
{
Serial.println("GATT data received");
// Get the received data from the event
uint8_t* char_value = &(evt->data.evt_gatt_characteristic_value.value.data[0]);
// Convert it back to float
float temperature = translate_IEEE_11073_temperature_to_float((IEEE_11073_float *)(char_value + 1));
// Print to Serial
Serial.print("Received temperature: ");
Serial.print(temperature);
Serial.println(" C");

sc = sl_bt_gatt_send_characteristic_confirmation(evt->data.evt_gatt_characteristic_value.connection);
app_assert_status(sc);
}
break;

...

}
}
tip

The above example gives the simplest example of a single value for a single sensor, sourced from Silicon Labs. If you would like to gain a deeper understanding of the usage of the SiliconLabs BLE API, we recommend reading the tutorial here.

Tech Support & Product Discussion

Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.

Loading Comments...