Jak już wspominaliśmy w czwartej części tego kursu, standard Bluetooth pozwala na jednoczesną pracę w rolach Central i Peripheral. Telefon jako Central nawiązuje połączenie z płytką deweloperską nRF5340-DK, która wystawia serwis sterowania diodą jako Peripheral. Pilot (którego przykład pokazano na fotografii 1) bądź klawiatura BLE również pracuje w trybie Peripheral, więc to nasza płytka będzie miała za zadanie nawiązać połączenie i subskrybować odpowiednie charakterystyki.
Ponieważ w poprzednim odcinku dość szczegółowo omówiliśmy urządzenia HID oraz cały proces komunikacji z nimi, teraz skupimy się na zastosowaniu tej wiedzy w praktycznej implementacji.
Konfiguracja płytki
Zaczniemy od rekonfiguracji projektu w pliku prj.conf (listing 1).
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_LOG=y
CONFIG_LOG_PROCESS_THREAD_SLEEP_MS=100
CONFIG_USE_SEGGER_RTT=y
CONFIG_LOG_BACKEND_RTT=y
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_GPIO_SHELL=y
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME=”Led control”
CONFIG_BT_SMP=y
CONFIG_BT_PRIVACY=y
CONFIG_BT_FIXED_PASSKEY=y
CONFIG_BT_SHELL=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_GATT_CLIENT=y
CONFIG_BT_MAX_CONN=4
CONFIG_BT_MAX_PAIRED=4
CONFIG_BT_GATT_DM=y
CONFIG_BT_GATT_DM_MAX_ATTRS=128
CONFIG_BT_SCAN=y
CONFIG_BT_SCAN_FILTER_ENABLE=y
CONFIG_BT_SCAN_UUID_CNT=1
CONFIG_BT_HOGP=y
CONFIG_BT_L2CAP_TX_BUF_COUNT=12
Listing 1. Plik prj.conf - docelowa konfiguracja
Dodane opcje konfiguracyjne CONFIG_BT_MAX_CONN i CONFIG_BT_MAX_PAIRED zwiększą liczbę możliwych połączeń. Co prawda przewidujemy obsługę tylko jednego urządzenia HID, ale będziemy mogli sparować się z więcej niż jednym telefonem.
Następnie włączamy moduł GATT Discovery Manager, który upraszcza proces odkrywania serwisów i charakterystyk GATT oraz zwiększamy domyślną liczbę atrybutów, które powinien obsłużyć, do 128.
Dalej włączamy obsługę modułu skanowania BLE, który umożliwia detekcję urządzeń w otoczeniu wraz z możliwością zastosowania filtrów. Będziemy wykrywać urządzenia HID na podstawie rozgłaszanego przez nie identyfikatora serwisu HID.
Włączenie modułu HOGP (HID over GATT Profile) zautomatyzuje proces subskrybowania charakterystyk urządzenia HID.
Ostatnia opcja zwiększa liczbę kanałów komunikacyjnych z niższymi warstwami stosu BLE do wartości zalecanej w przypadku urządzeń HID.
Jak już wiemy, mikrokontroler nRF5340 ma dwa rdzenie Arm® Cortex®-M33: jeden aplikacyjny i jeden sieciowy (Bluetooth). Musimy jeszcze poinformować część sieciową o większej liczbie możliwych połączeń. W głównym katalogu projektu dodajemy katalog „child_image”, a w nim plik „hci_ipc.conf” zawierający tylko jedną linijkę:
Teraz buildsystem automatycznie uwzględni ten plik przy budowie obrazu dla procesora sieciowego.
Aplikacja
Urządzenie Bluetooth LE nie może jednocześnie skanować i rozgłaszać, ponieważ obie te funkcje korzystają z tego samego interfejsu radiowego. Dlatego po włączeniu naszej płytki procesor rozpocznie skanowanie i – jeśli znajdzie urządzenie HID – nawiąże z nim połączenie. Po upływie pięciu sekund skanowanie zostanie zakończone, a płytka zacznie rozgłaszać, aby umożliwić połączenie ze smartfonem.
Główny przepływ części Bluetooth naszego programu będzie wyglądał następująco:
- włączenie i zainicjowanie stosu BLE – funkcja bt_control_init(),
- inicjalizacja i rozpoczęcie skanowania w poszukiwaniu urządzenia HID – funkcja bt_central_scan_start().
- wyłączenie skanowania po upływie pięciu sekund – bt_central_scan_stop(),
- rozpoczęcie rozgłaszania (advertising) i oczekiwanie na połączenie z telefonem – funkcja bt_le_adv_start.
W tym celu wprowadzamy tylko dwie główne zmiany w pliku bt_control.c (listing 2):
- dodajemy wywołania bt_central_scan_start() i bt_central_scan_stop() w ciele funkcji bt_ready_cb(),
- dodajemy wywołanie bt_central_connected() w ciele funkcji connected_cb(), ale tylko wtedy, jeśli nasza płytka łączy się poprzez Bluetooth w roli Central.
#include <zephyr/bluetooth/gatt.h>
#include „bt_control.h”
#include „bt_central.h”
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(bt_control, LOG_LEVEL_DBG);
// advertisement data (AdvData)
static const struct bt_data adv_data[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, (sizeof(CONFIG_BT_DEVICE_NAME)-1)),
};
static void connected_cb(struct bt_conn *conn, uint8_t err) {
if (err) {
LOG_ERR(„Connection failed. error:%u”, err);
return;
}
struct bt_conn_info info;
int ret = bt_conn_get_info(conn, &info);
if (ret == 0) {
char addr_dst[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(info.le.dst, addr_dst, sizeof(addr_dst));
LOG_INF(„Connected to: %s”, addr_dst);
} else {
LOG_ERR(„MAC read failed”);
}
if (info.role == BT_CONN_ROLE_CENTRAL) {
LOG_INF(„Connected as CENTRAL”);
bt_central_connected(conn);
}
if (info.role == BT_CONN_ROLE_PERIPHERAL) {
LOG_INF(„Connected as PERIPHERAL”);
}
}
static void disconnected_cb(struct bt_conn *conn, uint8_t reason) {
LOG_INF(„Disconnected. Reason: %u”, reason);
}
static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
{
if (!err) {
LOG_INF(„Security changed to BT_SECURITY_L%d”, level);
} else {
LOG_ERR(„Security change failed: %d”, err);
}
}
static struct bt_conn_cb conn_callbacks = {
.connected = connected_cb,
.disconnected = disconnected_cb,
.security_changed = security_changed_cb
};
static void pairing_complete_cb(struct bt_conn *conn, bool bonded) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF(„Pairing completed: %s, bonded: %d”, addr, bonded);
}
static void pairing_failed_cb(struct bt_conn *conn, enum bt_security_err reason) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_ERR(„Pairing failed conn: %s, reason %d”, addr, reason);
}
static struct bt_conn_auth_info_cb conn_auth_info_callbacks = {
.pairing_complete = pairing_complete_cb,
.pairing_failed = pairing_failed_cb,
};
static void auth_passkey_display_cb(struct bt_conn *conn, unsigned int passkey) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF(„Passkey for %s: %06u”, addr, passkey);
}
static void auth_cancel_cb(struct bt_conn *conn) {
LOG_INF(„Pairing cancelled.”);
}
static struct bt_conn_auth_cb conn_auth_callbacks = {
.passkey_display = auth_passkey_display_cb,
.passkey_entry = NULL,
.cancel = auth_cancel_cb
};
void bt_ready_cb(int status) {
int err=0;
//MAC
bt_addr_le_t addrs[CONFIG_BT_ID_MAX];
size_t count = CONFIG_BT_ID_MAX;
bt_id_get(addrs, &count);
//print MAC
if (count > 0) {
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&addrs[0], addr_str, sizeof(addr_str));
LOG_DBG(„BLE MAC: %s”, addr_str);
} else {
LOG_ERR(„Error while getting MAC”);
}
err = bt_central_scan_start();
if (err) {
LOG_ERR(„Failed to connect”);
}
k_msleep(5000);
bt_central_scan_stop();
// Start advertising
err = bt_le_adv_start(BT_LE_ADV_CONN, adv_data, ARRAY_SIZE(adv_data), NULL, 0);
if (err) {
LOG_ERR(„BLE advertisement error: %d”, err);
} else {
LOG_DBG(„BLE advertisement started”);
}
}
int bt_control_init(void) {
LOG_DBG(„BLE init...”);
#ifdef CONFIG_BT_FIXED_PASSKEY
int err = bt_passkey_set(777777);
if (err) {
LOG_ERR(„Unable to set passkey (err: %d)”, err);
}
#endif
//Register connected/disconnected callbacks
bt_conn_cb_register(&conn_callbacks);
//Register pairing callbacks
err = bt_conn_auth_cb_register(&conn_auth_callbacks);
if (err) {
LOG_ERR(„Failed to register authorization callbacks.”);
}
//Register pairing info callbacks
err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
if (err) {
LOG_ERR(„Failed to register authorization info callbacks.”);
}
int ret = bt_enable(bt_ready_cb);
if (ret) {
LOG_ERR(„BLE init error: %d”, ret);
return ret;
}
return ret;
}
Listing 2. Plik bt_control.c
Algorytm pracy jako Central
Przypomnijmy, że w poprzednim odcinku odczyt klawiszy pilota osiągnęliśmy, używając następujących komend w konsoli:
- bt scan on,
- bt connect,
- bt security 2,
- gatt discover,
- gatt subscribe <handle CCC> <handle wartości>.
Według tego samego schematu działa nowa część naszego programu. Całość zawiera się w nowych plikach: bt_central.c (listing 3) oraz bt_central.h (listing 4).
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <bluetooth/scan.h>
#include <bluetooth/services/hogp.h>
#include „led_control.h”
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(bt_central, LOG_LEVEL_DBG);
static struct bt_hogp hogp;
static void button_callback(const uint8_t id, const uint8_t *data,
const uint8_t size) {
if ((id == 1) && (size == 2)) {
uint8_t button_val = data[0];
if (button_val == 0x01) {
led_send((led_msg){ LED_STATE, 0 });
} else if (button_val == 0x02) {
led_send((led_msg){ LED_STATE, 1 });
} else if (button_val == 0x04) {
led_send((led_msg){ LED_BLINK, 500 });
} else if (button_val == 0x08) {
led_send((led_msg){ LED_BLINK, 100 });
}
}
}
static uint8_t hogp_notify_cb(struct bt_hogp *hogp,
struct bt_hogp_rep_info *rep,
uint8_t err, const uint8_t *data) {
if (!data) {
LOG_DBG(„No data”);
return BT_GATT_ITER_STOP;
}
uint8_t id = bt_hogp_rep_id(rep);
uint8_t size = bt_hogp_rep_size(rep);
struct bt_conn_info info;
bt_conn_get_info(hogp->conn, &info);
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(info.le.dst, addr, sizeof(addr));
LOG_INF(„Notification from %s: id:%u, size:%u”, addr, id, size);
LOG_HEXDUMP_INF( data, size, „Notification data:”);
button_callback(id, data, size);
return BT_GATT_ITER_CONTINUE;
}
static void hids_on_ready(void) {
struct bt_hogp_rep_info *rep = NULL;
LOG_INF(„HIDS ready”);
while (NULL != (rep = bt_hogp_rep_next(&hogp, rep))) {
enum bt_hids_report_type type = bt_hogp_rep_type(rep);
int id = bt_hogp_rep_id(rep);
if (type == BT_HIDS_REPORT_TYPE_INPUT) {
LOG_INF(„Subscribe to report id: %u”, id);
int err = bt_hogp_rep_subscribe(&hogp, rep, hogp_notify_cb);
if (err) {
LOG_ERR(„Subscribe error”);
}
}
}
}
static void scan_filter_match(struct bt_scan_device_info *device_info,
struct bt_scan_filter_match *filter_match,
bool connectable) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(device_info->recv_info->addr, addr, sizeof(addr));
LOG_INF(„Filter match on device %s”, addr);
}
static void scan_connecting_error(struct bt_scan_device_info *device_info) {
LOG_ERR(„Connecting failed”);
}
static void scan_connecting(struct bt_scan_device_info *device_info,
struct bt_conn *conn) {
LOG_INF(„Connecting”);
}
static void scan_filter_no_match(struct bt_scan_device_info *device_info,
bool connectable) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(device_info->recv_info->addr, addr, sizeof(addr));
LOG_INF(„no filter match on device %s”, addr);
}
BT_SCAN_CB_INIT(scan_cb, scan_filter_match, scan_filter_no_match,
scan_connecting_error, scan_connecting);
void bt_central_scan_stop(void) {
bt_scan_stop();
LOG_INF(„Scanning stopped”);
}
static void hogp_ready_cb(struct bt_hogp *hogp) {
LOG_DBG(„HOGP ready”);
hids_on_ready();
}
static void hogp_prep_fail_cb(struct bt_hogp *hogp, int err)
{
LOG_ERR(„ERROR: HIDS client preparation failed!”);
}
static void hogp_pm_update_cb(struct bt_hogp *hogp)
{
LOG_INF(„Protocol mode updated”);
}
static const struct bt_hogp_init_params hogp_init_params = {
.ready_cb = hogp_ready_cb,
.prep_error_cb = hogp_prep_fail_cb,
.pm_update_cb = hogp_pm_update_cb
};
static void discovery_completed_cb(struct bt_gatt_dm *dm,
void *context) {
int err;
LOG_INF(„Discovery ok”);
bt_gatt_dm_data_print(dm);
err = bt_hogp_handles_assign(dm, &hogp);
if (err) {
LOG_ERR(„Handles assign failed - error: %d”, err);
}
err = bt_gatt_dm_data_release(dm);
if (err) {
LOG_ERR(„Release iscovery data failed, error: %d”, err);
}
}
static void discovery_service_not_found_cb(struct bt_conn *conn,
void *context) {
LOG_ERR(„Discovery service not found”);
}
static void discovery_error_found_cb(struct bt_conn *conn,
int err,
void *context) {
LOG_ERR(„Discovery failed - error: %d”, err);
}
static const struct bt_gatt_dm_cb discovery_cb = {
.completed = discovery_completed_cb,
.service_not_found = discovery_service_not_found_cb,
.error_found = discovery_error_found_cb,
};
static void gatt_discover(struct bt_conn *conn) {
LOG_DBG(„Discovery starting”);
int err = bt_gatt_dm_start(conn, BT_UUID_HIDS, &discovery_cb, NULL);
if (err) {
LOG_ERR(„Discovery starting error: %d”, err);
}
}
void bt_central_connected(struct bt_conn *conn) {
int ret = bt_conn_set_security(conn, BT_SECURITY_L2);
if (ret == 0) {
LOG_INF(„Started changing security level”);
} else {
LOG_ERR(„Security level change failed - err: %d”, ret);
}
bt_hogp_init(&hogp, &hogp_init_params);
gatt_discover(conn);
}
int bt_central_scan_start(void) {
int err;
struct bt_scan_init_param scan_init = {
.connect_if_match = 1,
.scan_param = NULL,
.conn_param = BT_LE_CONN_PARAM_DEFAULT
};
bt_scan_init(&scan_init);
bt_scan_cb_register(&scan_cb);
err = bt_scan_filter_add(BT_SCAN_FILTER_TYPE_UUID, BT_UUID_HIDS);
if (err) {
LOG_ERR(„Scanning filters cannot be set - err: %d)”, err);
return err;
}
err = bt_scan_filter_enable(BT_SCAN_UUID_FILTER, true);
if (err) {
LOG_ERR(„Filters cannot be turned on - err %d”, err);
return err;
}
err = bt_scan_start(BT_SCAN_TYPE_SCAN_ACTIVE);
if (err) {
LOG_ERR(„Scanning failed to start - err: %d”, err);
return err;
}
LOG_DBG(„Scanning started”);
return err;
}
Listing 3. Plik bt_central.c
int bt_central_scan_start(void);
void bt_central_scan_stop(void);
void bt_central_connected(struct bt_conn *conn);
Listing 4. Plik bt_central.h
Logika algorytmu opiera się na wywoływaniu kolejnych funkcji zwrotnych (tzw. callbacków).
Wywołanie bt_central_scan_start() rozpocznie skanowanie z włączonym filtrem. Zwróćmy uwagę na ustawiony tutaj parametr connect_if_match = 1. Powoduje on, że w przypadku znalezienia urządzenia spełniającego warunki filtra (czyli rozgłaszającego serwis HID) nastąpi automatyczne nawiązanie połączenia.
Nawiązanie połączenia powoduje wywołanie callbacka connected_cb(), który z kolei spowoduje wykonanie kodu zawartego w bt_central_connected().
Funkcja bt_central_connected() podnosi poziom bezpieczeństwa połączenia do L2 (szyfrowanie i uwierzytelnianie), inicjalizuje obsługę HID (m.in. ustawiając callbacki modułu HOGP) oraz rozpoczyna odkrywanie usług na podłączonym urządzeniu pod kątem serwisu HID (poprzez parametr BT_UUID_HIDS).
Zakończenie procesu odkrywania usług powinno wywołać kolejnego callbacka: discovery_completed_cb(). W tym momencie warto bliżej przyjrzeć się funkcji bt_gatt_dm_data_print(). Przy obecnej konfiguracji projektu nic ona nie robi, ale jeśli dodamy opcję CONFIG_BT_GATT_DM_DATA_PRINT=y do pliku prj.conf, to wspomniany fragment kodu wyświetli nam zestaw informacji o serwisie HID.
Następnie funkcja bt_hogp_handles_assign() wyszukuje raporty HID i przypisuje ich uchwyty do struktury hogp. W poprzednim odcinku kursu zrobiliśmy to „na piechotę”, sprawdzając, jak atrybuty powiązane są z uchwytami. Na końcu zwalniamy pamięć zajętą podczas odkrywania usług funkcją bt_gatt_dm_data_release().
Gdy uchwyty serwisu HID zostaną poprawnie przypisane, wywołana będzie funkcja hogp_ready_cb(), która z kolei uruchomi hids_on_ready(). W opisywanej procedurze spinamy interesujące nas raporty wejściowe urządzenia HID z finalnym callbackiem hogp_notify_cb(). Moduł HOGP automatycznie subskrybuje odpowiednie charakterystyki. Na koniec, już po wysłaniu notyfikacji przez pilota o wciśniętym przycisku, w logach zobaczymy informację o numerze raportu, jego rozmiarze oraz wartościach (co widać na końcu listingu 5).
[00:00:15.062,744] <inf> bt_hci_core: HW Platform: Nordic Semiconductor (0x0002)
[00:00:15.062,774] <inf> bt_hci_core: HW Variant: nRF53x (0x0003)
[00:00:15.062,805] <inf> bt_hci_core: Firmware: Standard Bluetooth controller (0x00) Version 54.58864 Build 1214809870
[00:00:15.065,093] <inf> bt_hci_core: Identity: F5:20:E7:1F:38:EF (random)
[00:00:15.065,124] <inf> bt_hci_core: HCI: version 5.4 (0x0d) revision 0x218f, manufacturer 0x0059
[00:00:15.065,155] <inf> bt_hci_core: LMP: version 5.4 (0x0d) subver 0x218f
[00:00:15.065,246] <dbg> bt_control: bt_ready_cb: BLE MAC: F5:20:E7:1F:38:EF (random)
[00:00:15.068,328] <dbg> bt_central: bt_central_scan_start: Scanning started
[00:00:15.256,866] <inf> bt_central: no filter match on device 7B:1F:47:61:8D:9A (random)
[00:00:15.560,150] <inf> bt_central: no filter match on device 42:8B:8D:03:60:EF (random)
[00:00:15.560,974] <inf> bt_central: Filter match on device B3:F2:CF:65:C6:47 (public)
[00:00:15.562,133] <inf> bt_central: Connecting
[00:00:16.064,544] <inf> bt_control: Connected to: B3:F2:CF:65:C6:47 (public)
[00:00:16.064,575] <inf> bt_control: Connected as CENTRAL
[00:00:16.066,284] <inf> bt_central: Started changing security level
[00:00:16.066,314] <dbg> bt_central: gatt_discover: Discovery starting
[00:00:16.766,082] <inf> bt_control: Security changed to BT_SECURITY_L2
[00:00:17.066,284] <inf> bt_control: Pairing completed: B3:F2:CF:65:C6:47 (public), bonded: 1
[00:00:17.866,088] <inf> bt_central: Discovery ok
[00:00:18.766,082] <dbg> bt_central: hogp_ready_cb: HOGP ready
[00:00:18.766,113] <inf> bt_central: HIDS ready
[00:00:18.766,113] <inf> bt_central: Subscribe to report id: 1
[00:00:18.766,204] <inf> bt_central: Subscribe to report id: 2
[00:00:18.766,265] <inf> bt_central: Subscribe to report id: 3
[00:00:18.766,296] <inf> bt_central: Subscribe to report id: 4
[00:00:18.766,357] <inf> bt_central: Subscribe to report id: 5
[00:00:18.766,387] <inf> bt_central: Subscribe to report id: 6
[00:00:20.068,420] <inf> bt_central: Scanning stopped
[00:00:20.072,479] <dbg> bt_control: bt_ready_cb: BLE advertisement started
[00:00:24.418,609] <inf> bt_central: Notification from B3:F2:CF:65:C6:47 (public): id:1, size:2
[00:00:24.418,640] <inf> bt_central: Notification data:
01 00
[00:00:24.418,762] <inf> led_control: LED_STATE 0 received
[00:00:24.419,372] <inf> bt_central: Notification from B3:F2:CF:65:C6:47 (public): id:1, size:2
[00:00:24.419,403] <inf> bt_central: Notification data:
00 00
[00:00:25.802,368] <inf> bt_central: Notification from B3:F2:CF:65:C6:47 (public): id:1, size:2
[00:00:25.802,398] <inf> bt_central: Notification data:
02 00
[00:00:25.802,490] <inf> led_control: LED_STATE 1 received
[00:00:25.803,131] <inf> bt_central: Notification from B3:F2:CF:65:C6:47 (public): id:1, size:2
[00:00:25.803,161] <inf> bt_central: Notification data:
00 00
uart:~$
Listing 5. Log działania aplikacji
Teraz pozostaje jedynie przekazanie danych do funkcji dekodującej zmianę stanu diody LED, co robimy w funkcji button_callback() niemal identycznie, jak przy obsłudze przycisków na płytce.
Uruchomienie
Ponieważ wszystko ma się wydarzyć automatycznie, uruchomienie aplikacji okaże się proste. Urządzenie BLE ustawiamy w tryb parowania, a następnie włączamy lub resetujemy naszą płytkę. Na listingu 5 pokazano przykładowy log z testów aplikacji, w którym kolejne naciśnięcia przycisków pilota powodują włączanie i wyłączanie diody.
Podsumowanie
To był już ostatni odcinek naszego kursu. Choć zaprezentowany materiał był jedynie wprowadzeniem w temat Zephyra i Bluetooth LE, to i tak udało nam się omówić szeroki zakres zagadnień – od konfiguracji projektu, przez implementację algorytmu urządzenia centralnego, aż po testowanie współpracy z urządzeniami HID. System Zephyr oraz nRF Connect SDK znacząco ułatwiły realizację tych zadań, oferując gotowe moduły i narzędzia, które znacznie przyspieszają rozwój aplikacji. Teorii było sporo, ale efekt końcowy – choć skromny – pokazuje, że już na tym etapie można stworzyć funkcjonalne rozwiązanie BLE. Dziękujemy za udział w kursie i zachęcamy do dalszego eksplorowania możliwości Zephyra!
Krzysztof Kierys
Paweł Jachimowski