Chapter 3 Tutorials
Following step-by-step tutorials show the basic usage of core tools and concepts of the SDK.
3.1 Hello World
In this tutorial, we are going to create a device which is advertising its name, “Hello, 世界”.
Start ingWizard
from start menu and select menu item Project
-> New Project ...
.
This brings up the project wizard. This first page shown by the wizard is
Development Tool
(see Figure 3.1).
3.1.1 Development Tool
Page
On this page (Figure 3.1):
- Choose IDE/Toolchain
- Choose a project name
- Choose where to store your project
ingWizard
provides below handy functionality:
If
Git
is used for software configuration management, selectSetup .gitignore
;If
Visual Studio Code
is the preferred code editor, selectSetup Visual Studio Code
.
Then press Next
to proceed to the next page, Choose Chip Series
.
3.1.2 Choose Chip Series
Page
On this page (Figure 3.2), choose the target chip series of the project.
Then press Next
to proceed to the next page, Choose Project Type
.
3.1.3 Choose Project Type
Page
On this page (Figure 3.3), select Typical
.
Then press Next
to proceed to the next page, Role of Your Device
.
3.1.4 Role of Your Device
Page
On this page (Figure 3.4), just select Peripheral, and press Next
to proceed to the next page, Peripheral Setup
.
3.1.5 Peripheral Setup
Page
On this page (Figure 3.5), select Legacy
advertising.
Phones that support BLE 5.x extended advertising are still rare at
present (r Sys.Date()
) even if BLE 5.0 is declared as
“supported”, so we use legacy advertising for better compatibility.
Furthermore, legacy advertising can be changed to BLE 5.x extended
advertising by toggling a single bit later.
Click Setup Advertising Data
button,
which will bring up the advertising data editor (Figure 3.6).
In the editor, type name
to quickly search for the GAP advertising item 09 - «Complete Local Name»
,
and click Add
to add it into our device’s advertising data.
Click the newly added 09 - «Complete Local Name»
item, then fill in
“Hello, 世界” in the data editor shown below and press Enter. Data Preview
will
be updated and the whole advertising data is shown in raw bytes with a few
comments on each item. Obviously, Chinese characters are encoded in UTF-8
properly.
Now, click OK
to go back to project wizard, and press Next
to proceed
to the next page Security & Privacy
.
3.1.6 Security & Privacy
Page
Leave all options as default (Figure 3.7), and press Next
to proceed to the next page Firmare Over-The-Air
.
3.1.7 Firmare Over-The-Air
Page
Leave all options as default (Figure 3.8), and press Next
to proceed to the last page Common Functions
.
3.1.8 Common Functions
Page
On this page (Figure 3.9), we also accept the default settings and press Create
.
Now your project is created (Figure 3.10), and ready for building and downloading.
3.1.9 Build your project
Back to the main window of ingWizard
(Figure 3.10),
click on your project to open it. Build your project in IDE.
3.1.10 Download
To download your project, back to ingWizard
(Figure 3.10),
right click on your project, and select Download to Flash
from
the popup menu to start the downloader (Figure 3.11).
All settings in the downloader are ready except the UART port number.
In the downloader, configure the correct UART port and then click Start
.
Once downloaded, check if you can find a device named “Hello, 世界” by LightBlue,
INGdemo
(Figure 3.12) or other apps. Note that, this device may
not be listed in the Bluetooth menu of system settings at present.
3.2 iBeacon
In this tutorial, let’s make an iBeacon. iBeacon is a protocol developed by Apple11 and introduced at the Apple Worldwide Developers Conference in 2013. Beacons are a class of Bluetooth low energy (BLE) devices that broadcast their identifier to nearby portable electronic devices. This technology enables smartphones, tablets and other devices to perform actions when in close proximity to an iBeacon device.
Firstly, get a iBeacon scanning app from App Store. We will use an app called Locate
in this
tutorial. Locate
has a list of preconfigured proximity UUIDs, which includes an all 0s
Null UUID. We will use this Null UUID12.
3.2.1 Setup Advertising Data
There are two items in iBeacon advertising packet.
Flags
Value is fixed to 0x06, i.e. two bits are set, LE General Discoverable Mode & BR/EDR Not Supported.
Manufacturer Specific Data
The contents of this item is shown in Table 3.1
Size in Bytes | Name | Value | Notes |
---|---|---|---|
2 | Company ID | 0x004C | Company ID of Apple, Inc |
2 | Beacon Type | 0x1502 | Value defined by Apple |
16 | Proximity UUID | User defined value | |
2 | Major | Group ID | |
2 | Minor | ID within a group | |
1 | Measured Power | in dBm | Measured by an iPhone 5s at a 1 meter distance |
In order to make an iBeacon device, we can just follow the same steps as in the Hello World example, with only on exception that we need to configure the advertising package according to the specification.
In the advertising data editor, add 0x01 - «Flags»
and 0xFF - «Manufacturer Specific Data»
.
Click 0x01 - «Flags»
, check LE General Discoverable Mode
and BR/EDR Not Supported
.
Click 0xFF - «Manufacturer Specific Data»
, then the Edit as
button, a menu pops up and select
iBeacon ...
(Figure 3.13) to open iBeacon manufacturer specific data
editor (Figure 3.14).
Signal power can be set to any reasonable value (such as -50dBm), and we will calibrate it later with
the help of the Locate
app.
3.2.2 Try It
Let’s select GNU Arm Embedded Toolchain as our development environment on Choose Project Type
page,
and the wizard will make everything ready
(Figure 3.15).
Click on the project to open a console, type make
13 to build it.
Back to ingWizard
, follow the same steps to download it. Now we are able to find our newly
created iBeacon in Locate
. (Figure 3.16)
Tap on our device then we can calibrate signal power or check distance in real-time as shown in Figure 3.17.
Once signal power is calibrated, we can right click on our project in ingWizard
, and select Edit Data
-> Advertising
menu item to edit its advertising data with same editor that we are getting familiar with.
After advertising data is updated, rebuild the project and check if the distance is more accurate.
According to the specification, proximity beacons must use a non connectable undirected advertising PDU, using a fixed 100ms advertising interval. In this tutorial, we are not going to touch the code, so advertising parameters are not touched, either. To make these parameters fully meet the specification, please refer to the corresponding host GAP APIs.
3.3 Thermometer
In this tutorial, we are going to make a serious BLE device, a thermometer.
Bluetooth SIG has already defined a GATT service called Health
Thermometer14. This SDK contains a reference app called INGdemo
, which can be
deployed to an Android
or iOS
device. Using INGdemo
, we can check Bluetooth devices’ advertising
data, and if health thermometer service is found in a device, INGdemo
can connect to it and
read temperature.
In this tutorial, you will learn how to:
- Broadcast supported services
- Configure a GATT profile
- Respond to the read request of a GATT characteristic
3.3.1 Setup Advertising Data
Again, we follow the same steps as in the Hello World example,
and on the Peripheral Setup
page, we declare the thermometer service and create a
GATT profile. Add following three items into the advertising data:
Flags
Value is fixed to 0x06, i.e. two bits are set, LE General Discoverable Mode & BR/EDR Not Supported.
Complete List of 16-bit Service Class UUIDs
Add one service
0x1809 - Health Thermometer
as shown in Figure 3.18.Complete Local Name
Let’s name our device as “AccurateOne”.
3.3.2 Setup GATT Profile
Back to the Peripheral Setup
page and click Setup ATT database ...
to open the GATT profile
editor. Add two service, General Access (0x1800) and Health Thermometer (0x1809).
Delete all non-mandatory characteristics of General Access service. For Health Thermometer
service, keep two characteristics, i.e. temperature measurement and temperature type, and delete
the other two.
Next, edit each characteristic’s value:
Device Name of General Access:
Right click on the characteristic, select
Edit String Value ...
menu, and set the value to “AccurateOne”.Appearance of General Access:
Right click on the characteristic, select
Help
and the editor will open the corresponding document on Bluetooth SIG website. Find the value for general thermometer (0x0300), then click theEdit
button and input0x00, 0x03
into the data field.Temperature Measurement of Health Thermometer
Check the document on Bluetooth SIG website. click the
Edit
button and input five 0s (0, 0, 0, 0, 0
) into the data field. Here the first byte contains the flags showing that the following measurement is aFLOAT
value in units of Celsius. Checkread
anddynamic
properties (Figure 3.19).FLOAT
type is IEEE-11073 32-bit float. Basically, it has a 24-bit mantissa, and an 8-bit exponent (the most significant byte) in base 10.Temperature Type of Health Thermometer
Check the document on Bluetooth SIG website. Set it to any valid value by click the
Edit
button.
3.3.3 Write the Code
After project is created, open profile.c
in IDE, and the temperature measurement
characteristic handling function att_read_callback
is automatically generated by ingWizard
.
static uint16_t att_read_callback(hci_con_handle_t connection_handle,
uint16_t att_handle, uint16_t offset,
uint8_t * buffer, uint16_t buffer_size)
{
switch (att_handle)
{
case HANDLE_TEMPERATURE_MEASUREMENT:
if (buffer)
{
// add your code
return buffer_size;
}
else
return 1; // TODO: return required buffer size
default:
return 0;
}
}
att_read_callback
will be called twice or more when app reads a characteristic that has dynamic
property: one for querying required buffer size, and one for reading data. If data is large,
att_read_callback
might be called more times, each reading a part of data specified by offset
.
As discussed above, define a temperature measurement type:
typedef __packed struct gatt_temperature_meas
{
;
uint8 flags:24;
sint32 mantissa:8;
sint32 exponent} gatt_temperature_meas_t;
static gatt_temperature_meas_t temperature_meas = {0};
Now, we can complete the above case HANDLE_TEMPERATURE_MEASUREMENT
clause:
case HANDLE_TEMPERATURE_MEASUREMENT:
if (buffer)
{
// simulate an "accurate" thermometer
.mantissa = rand() % 100;
temperature_meas// output data
(buffer, ((uint8 *)&temperature_meas) + offset, buffer_size);
memcpyreturn buffer_size;
}
else
return sizeof(gatt_temperature_meas_t);
Build & download project, then connect to “AccurateOne” device in INGdemo
app. Check if
temperature changes randomly each time Refresh
button is pressed (Figure 3.20).
A thermometer (a server) can use notification or indication procedure to notify (without acknowledge) or indicate (with acknowledge) a characteristic value, see [Thermometer with Notification]. In this example, “AccurateOne” does not use these two procedures, and sends its measurement passively.
3.4 Thermometer with FOTA
In this tutorial, we are going to add Firmware Over-The-Air update feature into our thermometer.
This SDK provides a FOTA reference design that is workable out-of-the-box. To make FOTA work,
at least three parties are involved, a device, an app, and an HTTP server. The INGdemo
app
is already there, so in this tutorial, we will focus on the device and HTTP server.
3.4.1 Device with FOTA
Follow the same steps as in the previous Thermometer example to create a new project, say “ota”.
When editing advertising data, we can import data created in previous example by clicking
Open File...
button of the editor. Advertising data is stored in
$(ProjectPath)/data/advertising.adv
. Let’s change device’s name to “Clickety Click”.
When editing GATT profile database, we can import data created in previous example by clicking
Open File...
button of the editor. GATT profile data is stored in
$(ProjectPath)/data/gatt.profile
. Select INGChips Service
from the drop-down menu of
Add Service
button, and add “INGChips FOTA Service”. At present, we are not going to consider
security issues, so delete the “FOTA Public Key” characteristics.
Next, edit characteristics value of this service:
FOTA Version:
This identifies the full version number of our project. As shown in flash downloader, a whole project is composed by two binaries, one is from SDK bundle, called platform binary, and the other one is built from our project, called the app binary. FOTA version contains two sub-versions, one for each binary. Each sub-version contains three fields:
Major: A 16-bit field.
Minor: A 8-bit field.
Patch: Another 8-bit field.
Each bundle has its own version (so as the platform binary), using the same numbering scheme, which can be found on
SDK
page ofEnvironment Options
dialog (use menu itemTools
->Environment Options
to open this dialog). Suppose platform version is 1.0.115, and we would like our app’s version to be 1.0.0, then we set this characteristic’s value to (Fig 3.21):0x0001, 0, 1 // platform version 0x0001, 0, 0, // app version
FOTA Control
This is control point during update. Set its value to
0
(i.e.OTA_STATUS_DISABLED
), which is the initial status of FOTA.
Click OK
to close GATT profile editor. (Note: do not click Save
, unless you want to change
the file $(ProjectPath)/data/gatt.profile
that is opened in editor.)
Back to project wizard, press Next
to proceed to the next page Firmare Over-The-Air
. On this
page, let’s check FOTA
. Note that characteristics handles related to FOTA is
generated automatically by inspecting the GATT profile.
Then finish remaining steps on project wizard.
Open our brand-new project “ota”, copy the code from previous example to make
our thermometer respond to Refresh
in INGdemo
app.
Next, let’s make a new version.
3.4.2 Make a New Version
New version of our “ota” will have a new name “Barba Trick”, and app version number is upgraded to
2.0.0
. These data are saved in advertising and profile data respectively, so right click on
the project and use editors to update it. After data is updated, use Save As ...
to save data
to another file in the same directory, for example, update advertising data and save it to
$(ProjectPath)/data/advertising_2.adv
, and updated profile to
$(ProjectPath)/data/gatt_2.profile
.
Use macro V2
to control the actual advertising and profile data:
const static uint8_t adv_data[] = {
#ifndef V2
#include "../data/advertising.adv"
#else
#include "../data/advertising_2.adv"
#endif
};
......
const static uint8_t profile_data[] = {
#ifndef V2
#include "../data/gatt.profile"
#else
#include "../data/gatt_2.profile"
#endif
};
Rebuild the project with macro V2
defined, copy ota.bin
and platform.bin
(in
SDK_DIR/sdk/bundles/typical
) to an empty directory, say ota_app_v2
.
Create a file named manifest.json
in ota_app_v2
, with follow data in it:
{"platform": {
"version": [1,0,1],
"name": "platform.bin",
"address": 16384
,
}"app": {
"version": [2,0,0],
"name": "ota.bin",
"address": 163840
,
}"entry": 16384,
"bins":[]
}
Those addresses can be found in Environment Options
. entry
value is fixed to 0x4000
,
i.e. 16384
. Note that json
do not accept the popular
0xabcd
hexadecimal literals. INGdemo
can download additional binaries specified by bins
to device. In this case, we don’t have such binaries, so this field is left as an empty array.
Then create a readme
file for this update with some information about this update in it.
Now the FOTA package is ready. Make a ota_app_v2.zip
ZIP archive of the whole ota_app_v2
directory. Note that ota_app_v2
should not be made into a sub-directory in ota_app_v2.zip
.
Table 3.2 summarize the files in the ZIP archive.
File Name | Notes |
---|---|
readme | Some information about this update |
manifest.json | Meta information |
platform.bin | Platform binary |
ota.bin | App binary |
Back to IDE, rebuild the project leaving macro V2
undefined, then download the project.
3.4.3 FOTA Server
INGdemo
app needs a FOTA server URL, defined in class Thermometer.FOTA_SERVER
.
Move ota_app_v2.zip
to HTTP server’s document directory, and create a latest.json
file,
which contains information about latest version. Its content is:
{"app": [2,0,0],
"platform": [1,0,1],
"package": "ota_app_v2.zip"
}
Make sure that these two files can be accessed through URL (FOTA_SERVER + latest.json)
and
(FOTA_SERVER + ota_app_v2.zip)
.
3.4.4 Try It
Connect to “Clickety Click” in INGdemo
, click Update
(Figure 3.22).
Since platform.bin
is up-to-date,
only app.bin
need to be updated, the whole update completes in a short time.
Return to the main page, scan again and check if our new version works, a device named
“Barba Trick” appearing. Connect to “Barba Trick”, firmware is up-to-date now.
This tutorial gives an example on FOTA implementation. Users are free to design a new FOTA solution, from version definition to FOTA service and characteristics. It also possible to develop a dedicated secondary app for FOTA.
Security must be considered.
3.5 iBeacon Scanner
We already know to how to make iBeacon devices. In this tutorial, we are going to create an iBeacon scanner.
A scanner plays a central role in Bluetooth pico network. As always, we create a new project
named “iscanner” in ingWizard
(Fig 3.23).
On Role of Your Device
page, select Central. A central
device almost always scans for something then performs other actions, and our new project
wizard automatically adds codes to start scanning.
Open this new project in IDE, and navigate to function user_packet_handler
. We can see
there is an event called HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT
:
case HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT:
{
const le_ext_adv_report_t *report = decode_hci_le_meta_event(packet,
)->reports;
le_meta_event_ext_adv_report_t// ...
}
break;
Each time this event is received, we can check if the advertising report contains
0xFF - «Manufacturer Specific Data»
, and if it is an iBeacon packet. With the knowlegde
of making an iBeacon device, it is straight forward to define an iBeacon packet type
in C
.
typedef __packed struct ibeacon_adv
{
uint16_t apple_id;
uint16_t id;
uint8_t uuid[16];
uint16_t major;
uint16_t minor;
int8_t ref_power;
} ibeacon_adv_t;
#define APPLE_COMPANY_ID 0x004C
#define IBEACON_ID 0x1502
__packed
is an extended keyword to specify a data alignment of 1 for a data type. Fortunately, it is supported by both ARM and IAR compilers. Alternatively, one can use #pragma pack
directive:
#pragma pack (push, 1)
typedef struct ibeacon_adv
{
...
} ibeacon_adv_t;
#pragma pack (pop)
Before proceeding, let’s create a helper function that converts an UUID to a string.
const char *format_uuid(char *buffer, uint8_t *uuid)
{
(buffer, "{%02X%02X%02X%02X-%02X%02X-%02X%02X-"
sprintf"%02X%02X-%02X%02X%02X%02X%02X%02X}",
[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9],
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]);
uuidreturn buffer;
}
3.5.1 Distance Estimation
The received signal strength indication (RSSI) is reported together with advertising data. Generally, the intensity of electromagnetic waves radiating from a point source is inversely proportional to the square of the distance from the source. The well known equation for free space loss is:
\[ Loss = 32.45 + 20log(d) + 20log(f) \]
Where \(d\) is in km, \(f\) in MHz and \(Loss\) in dB. By comparing RSSI and
measured power at a distance of 1 meter (ref_power
),
we can grossly estimate the distance between the scanner and beacon using the free space
loss equation:
double estimate_distance(int8_t ref_power, int8_t rssi)
{
return pow(10, (ref_power - rssi) / 20.0);
}
Now, we are able to make a fully functional iBeancon scanner in less than twenty lines:
uint8_t length;
*p_ibeacon;
ibeacon_adv_t char str_buffer[80];
const le_ext_adv_report_t *report;
......
case HCI_SUBEVENT_LE_EXTENDED_ADVERTISING_REPORT:
= decode_hci_le_meta_event(packet,
report )->reports;
le_meta_event_ext_adv_report_t= (ibeacon_adv_t *)ad_data_from_type(report->data_len,
p_ibeacon (uint8_t *)report->data, 0xff, &length);
if ((length != sizeof(ibeacon_adv_t))
|| (p_ibeacon->apple_id != APPLE_COMPANY_ID)
|| (p_ibeacon->id != IBEACON_ID))
break;
("%s %04X,%04X, %.1fm\n",
printf(str_buffer, p_ibeacon->uuid),
format_uuid->major, p_ibeacon->minor,
p_ibeacon(p_ibeacon->ref_power, report->rssi));
estimate_distancebreak;
Use the Locate
app to transmit iBeacon signal, and check if our device can found it (Figure 3.24). Finaly, since RSSI value fluctuates,
one can add a low pass filter on RSSI to make
the estimation more stable.
Note that the size of this app’s binary increases dramatically. This is mainly because that Cortex-M3 don’t have a hardware floating-point unit and floating-point operations are all performed by library functions. Think twice before using floating-point operations.
3.5.2 Concurrent Advertising & Scanning
As an exercise, we can merge iBeacon project with this one, and check if our device can send iBeacon signals while keeps scanning for other iBeacon devices.
Bluetooth radio uses TDD (Time Division Duplex) topology in which data transmission occur in one direction at one time and data reception occur at another time, and it’s impossible to receive its own iBeacon signal.
3.6 Notification & Indication
A server can use notification or indication procedure to notify (without acknowledge) or indicate (with acknowledge) a characteristic’s value. Now, let’s add notification and indication features to our thermometer we have created in a previous tutorial.
To notify or indicate a characteristic’s value, we use att_server_notify
and att_server_indicate
respectively. These APIs must be called within the Bluetooth stack (Host
) task.
Unsolicited notifications and indication may be triggered by a timer or interrupts, i.e. by sources outside of Bluetooth stack task. To call these Bluetooth stack APIs, inter-task communication mechanism based on RTOS messages is provided.
3.6.1 Inter-task Communication
btstack_push_user_msg
can be used to send a message into Bluetooth stack stack:
uint32_t btstack_push_user_msg(uint32_t msg_id, void *data, const uint16_t len);
This message will be passed to your user_packet_handler
under event ID BTSTACK_EVENT_USER_MSG
:
static void user_packet_handler(uint8_t packet_type, uint16_t channel,
uint8_t *packet, uint16_t size)
{
uint8_t event = hci_event_packet_get_type(packet);
*p_user_msg;
btstack_user_msg_t if (packet_type != HCI_EVENT_PACKET) return;
switch (event)
{
// ......
case BTSTACK_EVENT_USER_MSG:
= hci_event_packet_get_user_msg(packet);
p_user_msg (p_user_msg->msg_id, p_user_msg->data,
user_msg_handler->len);
p_user_msgbreak;
// ......
}
}
Here, we delegate the handling of the user message to another function user_msg_handler
. Note that
user_msg_handler
is running in the context of Bluetooth stack task, and we are allowed to
call those Bluetooth stack APIs now.
Event BTSTACK_EVENT_USER_MSG
is broadcasted to all HCI
event callback functions.
3.6.2 Timer
Now let’s make our thermometer “AccurateOne” to update its value once per second. Firstly, create a
timer in initialization, such as in app_main
or setup_profile
.
= 0;
TimerHandle_t app_timer
uint32_t setup_profile(void *data, void *user_data)
{
= xTimerCreate("app",
app_timer (1000),
pdMS_TO_TICKS,
pdTRUE,
NULL);
app_timer_callback// ...
}
Timer callback function is defined as:
#define USER_MSG_ID_REQUEST_SEND 1
static void app_timer_callback(TimerHandle_t xTimer)
{
if (temperture_notify_enable | temperture_indicate_enable)
(USER_MSG_ID_REQUEST_SEND, NULL, 0);
btstack_push_user_msg}
This timer is started when we get HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE
in HCI_EVENT_LE_META
, and
stopped when we get HCI_EVENT_DISCONNECTION_COMPLETE
.
Here temperture_notify_enable
and temperture_indicate_enable
are two flags initialized as 0s
and set to 1
in att_write_callback
:
static int att_write_callback(hci_con_handle_t connection_handle,
uint16_t att_handle, uint16_t transaction_mode,
uint16_t offset, uint8_t *buffer, uint16_t buffer_size)
{
switch (att_handle)
{
case HANDLE_TEMPERATURE_MEASUREMENT + 1:
= connection_handle;
handle_send switch (*(uint16_t *)buffer)
{
case GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_INDICATION:
= 1;
temperture_indicate_enable break;
case GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION:
= 1;
temperture_notify_enable break;
}
return 0;
// ...
}
}
Here we store connection_handle
to a global variable handle_send
which will be used later.
The last piece of code is to handle message USER_MSG_ID_REQUEST_SEND
in user_msg_handler
:
static void user_msg_handler(uint32_t msg_id, void *data, uint16_t size)
{
switch (msg_id)
{
case USER_MSG_ID_REQUEST_SEND:
(handle_send);
att_server_request_can_send_now_eventbreak;
}
}
And report temperature in ATT_EVENT_CAN_SEND_NOW
:
...
case ATT_EVENT_CAN_SEND_NOW:
.mantissa = rand() % 100;
temperature_measif (temperture_notify_enable)
{
(handle_send,
att_server_notify,
HANDLE_TEMPERATURE_MEASUREMENT(uint8_t*)&temperature_meas,
sizeof(temperature_meas));
}
if (temperture_indicate_enable)
{
(handle_send,
att_server_indicate,
HANDLE_TEMPERATURE_MEASUREMENT(uint8_t*)&temperature_meas,
sizeof(temperature_meas));
}
break;
...
Try to rebuild and download the project, and check if the temperature value shown in INGdemo
changes once
per second.
There is a fully functional thermometer example, a.k.a
thermo_ota
, supporting FOTA, notification and
indication.
3.7 Throughput
BLE 5.0 introduces a new uncoded PHY with a sampling rate at 2M.
3.7.1 Theoretical Peak Throughput
Maximum payload length is 251 bytes for a Data Physical Channel PDU. Using 2M PHY, it takes 1048 μs to transmit. And an empty Data Physical Channel PDU takes 44 μs to transmit.
To achieve maximum throughput on one direction, length of all PDUs on this direction should be 251 bytes, while on the other direction, all PDUs should be empty. So, the transmission of 251 bytes takes a total duration of
\[ 1048 + 44 + 150 * 2 = 1392 (\mu s) \]
Therefore, the theoretical peak throughput provided by link layer is
\[ 251 * 8 / 1392 * 1000000 \approx 1442.528 (kbps) \]
For an app working above GATT, I2CAP and ATT all have their own overhead. Typically, GATT has a maximum effective payload of (251 - 7 =) 244 bytes. So, GATT could provide a theoretical peak throughput of
\[ 244 * 8 / 1392 * 1000000 \approx 1402.298 (kbps) \]
3.7.2 Test Throughput
There are a pair of examples in SDK for throughput testing (Figure \(\ref{fig:ch1-tpt}\)).
3.7.2.1 Test against INGdemo
Download peripheral_throughput
. Use INGdemo
to connect to ING Tpt
, and open throughput testing
page. On this page, we can test throughput from master to slave, from slave to master, or
on both directions simultaneously.
Figure \(\ref{fig:ch1-tpt-app}\) shows that using a common low end Android phone with 2M PHY support, we can achieve a 1M+ bps throughput over the air.
3.7.2.2 Test against Our Own App
Example central_throughput
demonstrates the typical procedure for a BLE central device:
- Scan and connect to a device that has throughput service declared in its advertising
- Discover throughput service;
- Discover characteristics of the service;
- Discover descriptors of characteristics.
INGChips Throughput Service
has two characteristics.
Generic Output
By this characteristic, peripheral device send data to central device.
This characteristic has a
Client Characteristic Configuration
descriptor.Generic Input
By this characteristic, central device send data to peripheral device.
Download central_throughput
to another board. This app has a UART command line interface to
host computer. Connect to a host computer, type “?” to check supported commands.
This app connects to peripheral_throughput
automatically. Input command start s->m
or start m->s
to
start testing throughput from peripheral to central, or from central to peripheral, receptively.
Figure \(\ref{fig:ch1-board-tpt}\) shows that using two boards, we have achieved a stable throughout at 1.2M+ bps over the air.
This throughput is tested over the air, a little bit lower than theoretical peak value, but much more practical.
3.8 Dual Role & BLE Gateway
In this tutorial, we are going to create a BLE gateway, which collects data from several peripheral devices and reports data to a central device. When collecting data, this gateway is a central device, while reporting data, it is a peripheral device, i.e., our app has two roles.
More specifically, our gateway only supports to collect data from thermometers. Let call it a
smart_meter
.
smart_meter
uses a generic string based output service for report data to a central device, such as
the INGdemo
running on a smart phone. It also has a UART control interface connecting to a host
computer.
Checkout the example peripheral_console
for how to do
string input & output.
Full functional smart_meter
app is also provided as an
example. Take this example as an reference while creating your own.
Now, let’s create this BLE gateway.
3.8.1 Use ingWizard
to create a peripheral app
Use GUI editor to edit advertising data, naming our app as “ING Smart Meter”.
Use GUI editor to edit GATT Profile. Add INGChips Console Service
into
GATT Profile (Figure \(\ref{fig:ch1-smart-meter-gatt}\)).
3.8.2 Define Thermometer Data
A thermometer is identified by its device address and id
. Each thermometer
uses its own connection identified by conn_handle
.
typedef struct slave_info
{
uint8_t id;
;
bd_addr_t addruint16_t conn_handle;
;
gatt_client_service_t service_thermo;
gatt_client_characteristic_t temp_char;
gatt_client_characteristic_descriptor_t temp_desc;
gatt_client_notification_t temp_notify} slave_info_t;
Define four thermometers.
3.8.3 Scan for Thermometers
Call two GAP APIs to start scanning. Once a device is found, check whether its device address
is one of the thermometers. If so, stop scanning and call
gap_ext_create_connection
to connect.
After connection established, if there is any thermometer not connected, then start scanning again.
3.8.4 Discover Services
After connection established, call gatt_client
APIs to discover its services.
These APIs follow a similar logic like Android, iOS.
3.8.5 Data Handling
Subscribe to thermometer’s Temperature Measurement
characteristic. When a new measurement is
received, convert the value into a string and report it to a host computer. If our app is already
connected to a central device, forward this information to it through GATT characteristic.
3.8.6 Robustness
To make our app more robust:
If disconnected from a thermometer, then start scanning;
If disconnected from a central device, then start advertising.
3.8.7 Prepare Thermometers
We can use example thermo_ota
as thermometers. But we need to configure
different address for each one.
We can write a simple script for downloader to generate these addresses automatically:
procedure OnStartBin(const BatchCounter, BinIndex: Integer;
var Data: TBytes; var Abort: Boolean);
begin
if BinIndex <> 6 then Exit;
0] := BatchCounter;
Data[end;
For further information on downloader scripting, see Scripting & Mass Production.
3.9 Hello, Nim
To use Nim
to develop apps, nim
and Gnu Toolchain
are both required. Nim
compiler
translates Nim
source code into C
source code, then Gnu Toolchain
is invoked to
compile and link the translated C
source code together with SDK, as shown in
Figure \(\ref{fig:ch1-nim-build}\).
Visual Studio Code
is recommended for Nim
code editing and building. Let’s make a simple app using Nim
.
3.9.1 Create a Nim
Project
On the Develpment Tool
page, Select Nim + Gnu Toolchain
. Select By Code
for both
advertising & ATT database generation (Figure \(\ref{fig:ch1-nim-by-code}\)).
ingWizard
also support create these data for Nim
apps. In this tutorial, we are going to
show that it is easy to create these data with
meta-programming16 in Nim
.
3.9.2 Create Advertising Data
Using Nim
module btdatabuilder
, we can create advertising and GATT profile easily.
Example 1: Create a device named “Hello, Nim”
let = ToArray([Flags({LEGeneralDiscoverableMode, BR_EDR_NotSupported}), advData LocalName("Hello, Nim")])
Example 2: Create an iBeacon
let = ToArray([Flags({LEGeneralDiscoverableMode, BR_EDR_NotSupported}), advData iBeacon("{E9052F1E-9D67-4A6E-B2D7-459D132D6A94}", 0, 0, -50)])
3.9.3 Create Profile Data
defineProfile([Service(SIG_UUID_SERVICE_GENERIC_ACCESS),
Characteristic(SIG_UUID_CHARACT_GAP_DEVICE_NAME, ATT_PROPERTY_READ,
"Hello, Nim"),
Characteristic(SIG_UUID_CHARACT_GAP_APPEARANCE, ATT_PROPERTY_READ,
[0u8, 0]),
Service(SIG_UUID_SERVICE_BATTERY_SERVICE),
Characteristic(SIG_UUID_CHARACT_BATTERY_LEVEL, ATT_PROPERTY_READ,
[20u8], "HANDLE_BATTERY_LEVEL")],
"profileData")
Once above code got compiled, ATT database is stored in profileData
, handle of battery level
characteristic is identified by a const HANDLE_BATTERY_LEVEL
, and the offset (in byte)
of battery level value in ATT base (i.e. profileData
) is identified by a const HANDLE_BATTERY_LEVEL_OFFSET
.
We can use these variables and constants generated by macro defineProfile
just as normal
ones. For example, let’s create a task that updates battery level pseudo randomly:
proc updateBatteryLevel(unused: pointer) {.noconv.} =
while true:
vTaskDelay(pdMS_TO_TICKS(1000))
[HANDLE_BATTERY_LEVEL_OFFSET] = rand_level()
profileData...
discard xTaskCreate(updateBatteryLevel, "b",
, nil,
configMINIMAL_STACK_SIZE- 1, nil) configMAX_PRIORITIES
There are at least three ways to generate pseudo random number in Nim
, use PRNG provided
by C
’s stdlib, use PRNG provided by provided by Nim
, or create our own PRNG.
- Use
C
’s PRNG
# It's easy to import C functions and use them
proc rand(): cint {. importc: "rand", header: "stdlib.h".}
proc rand_level(): uint8 = cast[uint8](rand() mod 101)
- Use
Nim
’s PRNG
import random
proc rand_level(): uint8 = cast[uint8](rand(0..100))
- Create a simple PRNG
proc rand_level(): uint8=
var last {.global.} = 0u16
= (last * 173 + 31) and 0x7fffu16
last return cast[uint8](last mod 101)
As we see, all three ways are easy in Nim
.
platform_hrng
can be used to initialized PRNG.
3.9.4 Benefits of Adopting Nim
Nim
is as powerful as C
because SDK provides bindings of all C
APIs for Nim
.
There are many benefits of adopting Nim
, such as it supports meta-programming and it is
strongly typed.
Meta-programming
With metaprogramming, we can create advertising and ATT database at compile time, which has
0
overhead in runtime obviously.Strongly Typed
Nim
is more strongly typed thanC
, which can help to make code safer.
Note that UUID is not allowed to be all 0s in final products.↩︎
Makefile
follows the syntax of GNUmake
.↩︎https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.health_thermometer.xml↩︎
Apps can report a different version in FOTA. It is not reqired to be same as in
Environment Options
.↩︎