Kurs Nordic nRF z BT (6). Bluetooth LE - urządzenie centralne

Kurs Nordic nRF z BT (6). Bluetooth LE - urządzenie centralne

Tym razem nasza płytka wystąpi w roli urządzenia centralnego (central), które będzie zarządzać połączeniami z innymi urządzeniami BLE. Użyjemy wbudowanych w Zephyr komend shellowych, aby dowiedzieć się więcej o stosie BLE.

Do tej pory nasza płytka deweloperska nRF5340DK działała jako urządzenie typu peripheral, czyli serwer w architekturze Bluetooth Low Energy. Po włączeniu moduł rozpoczynał rozgłaszanie (advertising) i oczekiwał na połączenie z telefonem lub innym klientem (central). Po nawiązaniu połączenia możliwe było sterowanie diodą LED za pomocą aplikacji nRF Connect, z użyciem charakterystyk zdefiniowanych w oprogramowaniu płytki.

O ile wygodniej byłoby jednak, gdybyśmy mogli włączać i wyłączać diodę za pomocą dedykowanego pilota BLE, bez potrzeby korzystania ze smartfona. Taki pilot pozwoliłby na szybkie i wygodne sterowanie urządzeniem, szczególnie w sytuacjach, gdy sięganie po telefon jest niepraktyczne. Przykład takiego urządzenia pokazuje fotografia 1.

Fotografia 1. Pilot BLE

Piloty zdalnego sterowania oparte na BLE można znaleźć na portalach aukcyjnych i w sklepach z elektroniką. Często są oferowane pod nazwą „Bluetooth Media Button” lub „przycisk Bluetooth” i domyślnie są przeznaczone do sterowania funkcjami multimedialnymi, takimi jak regulacja głośności czy odtwarzanie utworów na smartfonach. Aby taki pilot współpracował z naszą płytką, niezbędne jest, aby działał w standardzie Bluetooth Low Energy (BLE), ponieważ zestaw nRF5340DK obsługuje jedynie tę wersję protokołu Bluetooth.

Warto pamiętać, że wielu producentów nie podaje jednoznacznie, czy ich urządzenie wspiera standard BLE, a klasyczny Bluetooth nie jest kompatybilny z BLE. Dlatego – dla pewności – najlepiej wybrać pilot przynajmniej w wersji Bluetooth 4.2, który niemal zawsze zawiera BLE. Kupno odpowiedniego modelu pozwoli na integrację z płytką nRF5340DK i umożliwi wykorzystanie pilota jako urządzenia HID (Human Interface Device), co otwiera możliwość użycia funkcji BLE do komunikacji i sterowania. Możemy też skorzystać z klawiatury pracującej w standardzie BLE.

Co to jest HID?

HID to standard komunikacji określający zasady wymiany danych między urządzeniami typu Device (np. pilotem lub klawiaturą) a urządzeniami typu Host (takimi jak smartfon, komputer czy, w omawianym przypadku, nasza płytka deweloperska). Standard ten pierwotnie powstał na potrzeby urządzeń USB, aby mogły one pracować na jednolitych zasadach, bez konieczności instalowania dodatkowych sterowników.

W technologii Bluetooth rozróżniamy dwa jego warianty:

  1. Classic Bluetooth HID – standard działający w ramach tradycyjnego protokołu Bluetooth, stosowany tam, gdzie wymagana jest większa przepustowość i szybkość transmisji danych.
  2. Bluetooth Low Energy HID – zoptymalizowany pod kątem urządzeń o niskim poborze mocy, takich jak piloty, klawiatury czy myszki. Często nazywany jest też HID over GATT (HoG).

My przyjrzymy się drugiemu wariantowi.

W kontekście BLE, HID to profil Bluetooth – czyli specyfikacja opisująca konkretny przypadek użycia komunikacji. Profil można uznać za zbiór scenariuszy użycia określających, jak serwisy i charakterystyki współpracują ze sobą w celu realizacji konkretnej funkcjonalności. Aby lepiej zobrazować zagadnienie, posłużymy się przykładem: profil baterii opisuje nam, jak i kiedy informacja o stanie baterii zostanie przesłana przez cały stos Bluetooth.

Konfiguracja płytki

Zaczniemy od rekonfiguracji projektu w pliku prj.conf (listing 1).

CONFIG_BT_CENTRAL=y
CONFIG_BT_GATT_CLIENT=y
CONFIG_BT_SHELL=y

Listing 1. Plik prj.conf – dodatkowe opcje

Ponieważ pilot działa jako urządzenie peryferyjne (peripheral), zadaniem naszej płytki będzie połączenie się z nim jako urządzenie centralne (central). Pierwsza z nowych opcji konfiguracyjnych włącza tę rolę na poziomie warstwy GAP. Kolejna opcja pozwala płytce działać jako klient na poziomie warstwy GATT.

Zephyr umożliwia kontrolę nad Bluetooth za pomocą poleceń dostępnych w shellu, co pozwala na skonfigurowanie połączenia bez potrzeby pisania dodatkowego kodu. Trzecia opcja dodaje komendy związane z BLE do konsoli.

Aby uniknąć konfliktów, wyłączmy dotychczasowe użycie Bluetooth, które mogłoby przeszkadzać w eksperymentach. Wystarczy zakomentować wywołanie funkcji bt_control_init() w pliku main.c z poprzedniej części kursu. To właśnie zaleta modułowego kodu – sterowanie rozbudowaną funkcjonalnością można realizować za pomocą pojedynczych linii.

Teraz wystarczy skompilować projekt, wgrać go na płytkę i można zaczynać zabawę.

Zabawa shellem

W trzecim odcinku naszego kursu dość powierzchownie zapoznaliśmy się z shellem Zephyra. W tej części będziemy go intensywnie używać. Aby lepiej go poznać i sprawdzić, czy komendy Bluetooth zostały poprawnie dodane, wpiszmy polecenie help w terminalu z logami i shellem. Port tego terminalu jest wyświetlony we wtyczce nRF Connect w sekcji CONNECTED DEVICES (rysunek 1).

Rysunek 1. Port COM skojarzony z shellem

Poprawny wynik działania polecenia prezentuje listing 2.

uart:~$ help
Please press the <Tab> button to see all available commands.
You can also use the <Tab> button to prompt or auto-complete all commands or its subcommands.
You can try to call commands with <-h> or <--help> parameter for more information.

Shell supports following meta-keys:
Ctrl + (a key from: abcdefklnpuw)
Alt + (a key from: bf)
Please refer to shell documentation for more details.

Available commands:
bt : Bluetooth shell commands
clear : Clear screen.
device : Device commands
devmem : Read/write physical memory
Usage:
Read memory at address with optional width:
devmem address [width]
Write memory at address with mandatory width and value:
devmem address <width> <value>
gatt : Bluetooth GATT shell commands
gpio : GPIO commands
help : Prints the help message.
history : Command history.
kernel : Kernel commands
led : LED
rem : Ignore lines beginning with ‘rem ‘
resize : Console gets terminal screen size or assumes default in case the
readout fails. It must be executed after each terminal width change
to ensure correct text display.
retval : Print return value of most recent command
shell : Useful, not Unix-like shell commands.
uart:~$

Listing 2. Wynik polecenia help

Warto zauważyć, że shell w Zephyrze wspiera autouzupełnianie poleceń za pomocą klawisza <Tab>. Możemy także wygodnie przeglądać i wywoływać wcześniej wprowadzone komendy, korzystając z klawiszy strzałek w górę i w dół. Na liście wyświetlonych komend możemy zauważyć dodaną już przez nas komendę led – jednak to, co nas teraz interesuje, to komendy bt i gatt.

Ponieważ wyłączyliśmy automatyczny proces inicjalizacji Bluetooth, teraz musimy uruchomić go ręcznie za pomocą komendy bt init. Więcej informacji o komendach shell dla Bluetooth można znaleźć pod linkiem [2]. Następnie, zgodnie z działaniem prawdziwego projektu, rozpoczniemy skanowanie – jak przystało na każde porządne urządzenie typu central.

Włączenie trybu szczegółowego (verbose) skanowania powoduje wyświetlanie dodatkowych informacji o wykrytych urządzeniach Bluetooth. Dzięki temu uzyskamy szczegółowe dane, takie jak moc sygnału (RSSI), typy urządzeń czy treść wysyłanych danych rozgłoszeniowych.

Kolejno wpisujemy polecenia w konsoli:

  • bt init
  • bt scan-verbose-output on
  • bt scan on

W zależności od liczby urządzeń BLE w pobliżu, konsola może zostać zalana ogromną ilością informacji. Aby zatrzymać tę lawinę, wpisujemy w konsoli bt scan off. Co ważne, także podczas ciągłego wyświetlania tekstu w terminalu możemy wprowadzać komendy – nawet jeśli ich wpisywanie nie jest widoczne na ekranie. Przykładowy rezultat skanowania (a właściwie jego początek) prezentuje listing 3.

uart:~$ bt init
Bluetooth initialized
[00:00:04.274,139] <inf> bt_hci_core: HW Platform: Nordic Semiconductor (0x0002)
[00:00:04.274,200] <inf> bt_hci_core: HW Variant: nRF53x (0x0003)
[00:00:04.274,230] <inf> bt_hci_core: Firmware: Standard Bluetooth controller (0x00) Version 54.58864 Build 1214809870
[00:00:04.276,184] <inf> bt_hci_core: Identity: F5:20:E7:1F:38:EF (random)
[00:00:04.276,214] <inf> bt_hci_core: HCI: version 5.4 (0x0d) revision 0x218f, manufacturer 0x0059
[00:00:04.276,245] <inf> bt_hci_core: LMP: version 5.4 (0x0d) subver 0x218f
uart:~$ bt scan-verbose-output on
uart:~$ bt scan on
Bluetooth active scan enabled
[DEVICE]: 43:DB:16:D5:5F:B8 (random), AD evt type 2, RSSI -85 C:0 S:1 D:0 SR:0 E:0 Prim: LE 1M, Secn: No packets, Interval: 0x0000 (0 us), SID: 0xff
[SCAN DATA START – ADV_SCAN_IND]
Type 0xff: 0x2d, 0x01, 0x02, 0x00, 0x01, 0x10, 0xba, 0x5f, 0x07, 0xea, 0xcf, 0xc6, 0x44, 0x0f, 0xbc,0x30, 0xb5, 0xa5, 0x28, 0x82, 0xd1, 0x90, 0x4f, 0x78, 0x72, 0xc5, 0x92, 0x7f
[SCAN DATA END]
[DEVICE]: 43:DB:16:D5:5F:B8 (random), AD evt type 4, RSSI -85 C:0 S:1 D:0 SR:1 E:0 Prim: LE 1M, Secn: No packets, Interval: 0x0000 (0 us), SID: 0xff
[SCAN DATA START – SCAN_RSP]
[SCAN DATA END]
[DEVICE]: AC:12:2F:8A:40:82 (public), AD evt type 0, RSSI -58 C:1 S:1 D:0 SR:0 E:0 Prim: LE 1M, Secn: No packets, Interval: 0x0000 (0 us), SID: 0xff
[SCAN DATA START – ADV_IND]
Type 0x01: 0x06
Type 0xff: 0xac, 0x12, 0x2f, 0x8a, 0x40, 0x82, 0x00, 0x00
Type 0x05: 0xdaf51c01
[SCAN DATA END]

Listing 3. Rozpoczęcie skanowania w konsoli

Przeglądając wyniki skanowania, możemy zauważyć różne typy adresów MAC rozgłaszających się urządzeń. W Bluetooth wyróżniamy dwa główne typy adresów:

  • Public Address (adres publiczny) – stały adres nadawany fabrycznie, który nie zmienia się w trakcie pracy urządzenia. Adres ten musi być zarejestrowany w IEEE (Institute of Electrical and Electronics Engineers), podobnie jak adresy MAC w urządzeniach Wi-Fi czy Ethernet.
  • Random Address (adres losowy) – nie wymaga rejestracji w IEEE i dzieli się na dwa dodatkowe podtypy:
    • Static Address (adres statyczny) – działa jako zamiennik adresu publicznego; pozostaje niezmienny przez czas działania urządzenia lub do jego ponownego uruchomienia.
    • Private Address (adres prywatny) – zapewnia dodatkowy poziom prywatności. Może być generowany całkowicie losowo bądź za pomocą klucza IRK (Identity Resolving Key) i regularnie zmieniany, aby zapobiegać śledzeniu przez obce urządzenia.

Aby zmniejszyć liczbę wyświetlanych urządzeń, włączymy filtrowanie na podstawie poziomu sygnału. Ustawiając wysoki próg filtrowania (np. –20 dBm), wykluczymy z wyników skanowania najprawdopodobniej wszystkie wpisy. Potem, z włączonym skanowaniem, będziemy stopniowo obniżać próg (do –25 dBm, potem –30 dBm itd.). Przykładowo komenda bt scan-filter-set rssi -20 ustawi filtr na podstawie wartości RSSI (Received Signal Strength Indicator), a na konsoli będą wyświetlane tylko te urządzenia, które mają siłę sygnału równą lub większą niż –20 dBm.

Wprowadzamy wartość filtra i rozpoczynamy skanowanie. Teraz czas wybudzić pilot. Urządzenia zasilane bateryjnie zazwyczaj przechodzą w tryb uśpienia, gdy tylko jest to możliwe. Naciśnięcie dowolnego przycisku niesparowanego pilota powinno rozpocząć proces rozgłaszania, który często jest sygnalizowany np. miganiem podświetlenia. Proces ten można prześledzić na listingu 4.

uart:~$ bt scan-filter-set rssi -40
RSSI cutoff set at -40 dB
uart:~$ bt scan on
Bluetooth active scan enabled
uart:~$ bt scan-filter-set rssi -41
RSSI cutoff set at -41 dB
uart:~$ bt scan-filter-set rssi -42
RSSI cutoff set at -42 dB
uart:~$ bt scan-filter-set rssi -45
RSSI cutoff set at -45 dB
[DEVICE]: B3:F2:CF:65:C6:47 (public), AD evt type 0, RSSI -44 Smart Remote C:1 S:1 D:0 SR:0 E:0 Prim: LE 1M, Secn: No packets, Interval: 0x0000 (0 us), SID: 0xff
[SCAN DATA START – ADV_IND]
Type 0x01: 0x06
Type 0x03: 0x1812
Type 0x19: 0xc1, 0x03
Type 0x09: Smart Remote
[SCAN DATA END]
[DEVICE]: B3:F2:CF:65:C6:47 (public), AD evt type 4, RSSI -44 C:0 S:1 D:0 SR:1 E:0 Prim: LE 1M, Secn: No packets, Interval: 0x0000 (0 us), SID: 0xff
[SCAN DATA START – SCAN_RSP]
Type 0xff: 0xd6, 0x05, 0x08, 0x00, 0x4a, 0x4c, 0x41, 0x49, 0x53, 0x44, 0x4b
[SCAN DATA END]

Listing 4. Wykrycie pilota podczas skanowania

Warto upewnić się, że pilot znajduje się blisko płytki, aby jego sygnał był odpowiednio silny i został wykryty podczas skanowania. Skanowanie kończymy, ponownie wpisując w konsoli bt scan off zaraz po tym, jak wykryjemy nasze urządzenie.

Jak widać na listingu 4, siła sygnału pilota wynosi –44 dBm, dlatego musieliśmy obniżyć nasz próg do –45 dBm, aby urządzenie zostało wykryte. Najważniejsze informacje, które możemy odczytać z konsoli, to:

  • Adres MAC urządzenia: B3:F2:CF:65:C6:47 (public)
  • Usługa udostępniana przez urządzenie: UUID 0x1812, czyli Human Interface Device Service. Możemy to zweryfikować w specyfikacji Bluetooth dostępnej pod adresem [1].
  • Nazwa urządzenia: Smart Remote

Znając adres pilota, możemy się z nim połączyć (bt connect B3:F2:CF:65:C6:47 (public)). Należy podać adres MAC wraz z jego typem. Po nawiązaniu połączenia warto podnieść poziom bezpieczeństwa, szczególnie w przypadku urządzeń HID. Nie chcemy przecież, aby ktoś mógł np. podsłuchiwać transmisję z klawiatury podczas wpisywania przez nas hasła. Do tego celu służy nam komenda bt security 2. O poziomach zabezpieczenia mówiliśmy w poprzedniej części kursu. Przykładowy przebieg tych operacji pokazaliśmy na listingu 5.

uart:~$ bt scan off
Scan successfully stopped
uart:~$ bt connect B3:F2:CF:65:C6:47 (public)
Connection pending
Connected: B3:F2:CF:65:C6:47 (public)
uart:~$ bt security 2
Security changed: B3:F2:CF:65:C6:47 (public) level 2
Bonded with B3:F2:CF:65:C6:47 (public)
uart:~$

Listing 5. Łączenie z pilotem

Pierwsze dwa kroki procesu za nami. Zanim klient zacznie komunikować się z serwerem, nie ma wiedzy o danych i funkcjonalnościach przechowywanych na tym serwerze. Dlatego najpierw przeprowadza tzw. odkrywanie usług (Service Discovery), podczas którego pyta serwer o dostępne atrybuty. Zapytamy pilot (serwer) o wszystkie jego podstawowe serwisy komendą gatt discover-primary. Wynikiem będą wszystkie serwisy wystawione przez serwer GATT (listing 6).

uart:~$ gatt discover-primary
Discover pending
Service 1800 found: start handle 1, end_handle 7
Service 1801 found: start handle 8, end_handle b
Service 180a found: start handle c, end_handle 1e
Service 180f found: start handle 1f, end_handle 22
Service 1812 found: start handle 23, end_handle 49
Service ae40 found: start handle 4a, end_handle 4f
Service ae00 found: start handle 80, end_handle 85
Discover complete
uart:~$

Listing 6. Serwisy pilota

Możemy zauważyć, że nawet stosunkowo proste urządzenie ma rozbudowaną tablicę atrybutów ATT – ostatni element ma uchwyt o wartości 0x85, co oznacza, że łączna liczba atrybutów to aż 133.

Ostatnie dwie usługi to niestandardowe dane klienta, jednak pozostałe mają UUID określone przez organizację Bluetooth SIG i można je zidentyfikować na podstawie specyfikacji [1].

Dla ułatwienia analizy możemy skorzystać z aplikacji nRF Connect, która oferuje przyjazny graficznie interfejs i wykonuje ten sam proces znacznie szybciej.

Zanim przejdziemy do głównej funkcji pilota – obsługi przycisków – skorzystamy z prostej usługi monitorowania stanu baterii, aby lepiej zrozumieć, jak wygląda przepływ danych w BLE.

Battery Service

Wyobraźmy sobie, że jesteśmy teraz sparowanym klientem serwera (pilota) i interesuje nas stan jego baterii. Usługa monitorowania baterii ma UUID o wartości 0x180F. Z danych z listingu 6 wiemy, że definicja tej usługi znajduje się w tablicy atrybutów i obejmuje wpisy o uchwytach od 0x1F do 0x22.

Zobrazujemy sobie teraz zawartość tablicy atrybutów dotyczącej serwisu baterii. Najpierw pobierzemy listę wpisów w tablicy atrybutów wraz z ich typami i uchwytami komendą gatt discover. Wynik, okrojony do interesującego nas zakresu, prezentuje listing 7.

uart:~$ gatt discover
Discover pending
Descriptor 2800 found: handle 1
...
Descriptor 2800 found: handle 1f
Descriptor 2803 found: handle 20
Descriptor 2a19 found: handle 21
Descriptor 2902 found: handle 22
...
Descriptor 2902 found: handle 85
Discover complete
uart:~$

Listing 7. Lista atrybutów GATT pilota

Następnie odczytamy wartości poszczególnych atrybutów za pomocą polecenia gatt read <numer atrybutu>. Rezultat znajduje się na listingu 8.

uart:~$ gatt read 1f
Read pending
Read complete: err 0x00 length 2
00000000: 0f 18 |.. |
Read complete: err 0x00 length 0
uart:~$ gatt read 20
Read pending
Read complete: err 0x00 length 5
00000000: 12 21 00 19 2a |.!..* |
Read complete: err 0x00 length 0
uart:~$ gatt read 21
Read pending
Read complete: err 0x00 length 1
00000000: 00 |. |
Read complete: err 0x00 length 0
uart:~$ gatt read 22
Read pending
Read complete: err 0x00 length 2
00000000: 00 00 |.. |
Read complete: err 0x00 length 0
uart:~$

Listing 8. Wartości atrybutów usługi baterii

Wyniki możemy zebrać w zgrabnej tabelce pokazanej na rysunku 2.

Rysunek 2. Tablica atrybutów Battery Service

Definicja każdego serwisu na poziomie warstwy GATT zaczyna się od wpisu o typie 0x2800 (Primary Service). Dzięki temu łatwo można przeanalizować strukturę usług udostępnianych przez serwer – wystarczy wybrać wpisy o tym typie. Tak właśnie przeprowadziliśmy Service Discovery po połączeniu z pilotem.

Nasz serwis ma standardowy, 16-bitowy identyfikator Serwisu Baterii (0x180F). Może dziwić, że UUID znajduje się w części Value atrybutu, ale tak właśnie działa standard Bluetooth – wpisy typu 0x2800 zawierają UUID usługi jako swoją wartość. W ten sposób można łatwo odróżnić poszczególne usługi w tabeli ATT.

Następny wpis (handle 0x0020) deklaruje charakterystykę wraz z jej parametrami.

Pierwszy bajt definiuje właściwości charakterystyki: ustawione dwa bity oznaczają możliwość odczytu charakterystyki (Read) oraz subskrypcji powiadomień (Notify).

Dalej mamy handle, który wskazuje, gdzie znajduje się wartość tej charakterystyki (w tym przypadku 0x0021).

Na końcu deklaracji znajduje się UUID charakterystyki – w naszym przypadku 0x2A19, które identyfikuje ją jako Battery Level zgodnie ze standardem.

Wpis o handle 0x0021 przechowuje faktyczną wartość charakterystyki. Zgodnie ze specyfikacją [3] jest to zmienna reprezentująca poziom baterii w procentach (wartości od 0 do 100).

Ostatni wpis (o handle 0x0022) to tzw. Client Characteristic Configuration (CCC). Jest to specjalny deskryptor (dodatkowy atrybut charakterystyki), który służy do zarządzania powiadomieniami i wskazaniami dla danej charakterystyki. Wartość deskryptora CCC decyduje o tym, czy powiadomienia są włączone (0x0001), czy też wyłączone (0x0000). Aby włączyć powiadomienia dotyczące poziomu baterii, klient (np. nasze urządzenie centralne) musi zapisać odpowiednią wartość do deskryptora. Każda charakterystyka umożliwiająca notyfikację lub indykację ma taki deskryptor, czyli atrybut o typie 0x2902, w swojej definicji.

Podsumowując: po analizie tych atrybutów wiemy, że aby odczytać stan baterii, należy sprawdzić wartość spod uchwytu 0x21. Z kolei chęć odbierania powiadomień o zmianach stanu naładowania należy zgłosić, wpisując odpowiednią wartość do uchwytu 0x22.

Tak oto wysokopoziomowa logika monitorowania stanu baterii sprowadza się do interakcji z niewielką liczbą atrybutów w tabeli ATT. To pokazuje, jak efektywnie warstwa GATT pozwala na dostęp do danych urządzenia w sposób prosty i standardowy.

Shell Zephyra ma dostępne polecenie subskrypcji w formacie gatt subscribe <handle CCC> <handle wartości>.

Na listingu 9 mamy przeprowadzoną operację subskrypcji i rezygnacji z niej, wraz ze sprawdzeniem wartości CCC. Warto zauważyć, że zaraz po włączeniu subskrypcji serwer proponuje zmianę parametrów połączenia, takich jak interwał komunikacji (interval) oraz opóźnienie (latency). Taka zmiana może wynikać z optymalizacji połączenia pod kątem oszczędności energii lub poprawy wydajności.

uart:~$ gatt subscribe 22 21
Subscribed
LE conn param req: int (0x0006, 0x0009) lat 100 to 300
LE conn param updated: int 0x0009 lat 100 to 300
uart:~$ gatt read 22
Read pending
Read complete: err 0x00 length 2
00000000: 01 00 |.. |
Read complete: err 0x00 length 0
uart:~$ gatt unsubscribe
Unsubscribe success
Unsubscribed
uart:~$ gatt read 22
Read pending
Read complete: err 0x00 length 2
00000000: 00 00 |.. |
Read complete: err 0x00 length 0
uart:~$

Listing 9. Subskrybowanie stanu baterii

W przypadku tego pilota wartość poziomu baterii cały czas wynosi 0. Producent akcesorium uznał najwyraźniej, że nie jest to istotna funkcjonalność do implementacji.

BLE HID

Serwis HID ma standardowy UUID o wartości 0x1812. Jak wskazuje listing 6, ów serwis składa się z 38 atrybutów.

Odczytanie klawiszy nie będzie jednak tak skomplikowane, jak mogłoby się wydawać – informacje o naciśnięciach przycisków znajdują się w raportach stanu klawiszy (tzw. HID Input Reports). Raporty te są niczym innym, jak tylko standardowymi charakterystykami Serwisu HID o UUID 0x2A4D.

Na poziomie GATT proces obsługi HID Input Reports sprowadza się do:

  1. Odnalezienia odpowiednich charakterystyk w ramach usługi HID (w przypadku tego urządzenia to atrybuty od 0x23 do 0x49).
  2. Zlokalizowania atrybutów zawierających wartości raportów klawiszy (atrybuty typu 0x2A4D).
  3. Ustawienia subskrypcji tych atrybutów, aby automatycznie odbierać powiadomienia o ich zmianach (atrybuty o typie 0x2902).

Możemy przeanalizować całą listę atrybutów, którą uzyskaliśmy na listingu 7, ale sprytniej będzie skorzystać z komendy gatt discover z odpowiednimi parametrami filtrującymi.

  • Komenda gatt discover 2a4d wyświetli listę wszystkich atrybutów typu HID Input Report w urządzeniu.
  • Polecenie gatt discover 2902 23 49 wyświetli listę wszystkich deskryptorów CCC (Client Characteristic Configuration) w zakresie uchwytów od 0x23 do 0x49, czyli w obrębie serwisu HID – nie zobaczymy więc w wyniku np. deskryptora Serwisu Baterii.

Dzięki takiemu rozwiązaniu możemy szybko zidentyfikować interesujące nas atrybuty bez konieczności przeglądania całej tabeli. Wynik stosownej operacji prezentuje listing 10.

uart:~$ gatt discover 2a4d
Discover pending
Descriptor 2a4d found: handle 27
Descriptor 2a4d found: handle 2b
Descriptor 2a4d found: handle 2f
Descriptor 2a4d found: handle 32
Descriptor 2a4d found: handle 36
Descriptor 2a4d found: handle 3a
Descriptor 2a4d found: handle 3e
Discover complete
uart:~$ gatt discover 2902 23 49
Discover pending
Descriptor 2902 found: handle 28
Descriptor 2902 found: handle 2c
Descriptor 2902 found: handle 33
Descriptor 2902 found: handle 37
Descriptor 2902 found: handle 3b
Descriptor 2902 found: handle 3f
Descriptor 2902 found: handle 45
Discover complete
uart:~$

Listing 10. Analiza serwisu HID

Urządzenie HID może mieć wiele raportów HID Input Report (0x2A4D), ponieważ każdy z nich reprezentuje inny zestaw danych wejściowych. Na przykład jeden raport może obsługiwać standardowe klawisze a inny – odpowiadać za przyciski multimedialne. Każdy raport ma dedykowany atrybut oraz odpowiadający mu deskryptor CCC, co pozwala na niezależne zarządzanie subskrypcjami.

Shell Zephyra umożliwia subskrybowanie jednocześnie tylko jednego atrybutu. W związku z tym musimy „po omacku” wybrać odpowiedni uchwyt, który odpowiada za raport stanu klawiszy. Nasze zadanie sprowadza się do:

Użycia komendy gatt subscribe z odpowiednią parą uchwytów (deskryptora CCC i raportu).

Sprawdzenia reakcji na naciśnięcie przycisku.

Anulowania subskrypcji i przetestowania kolejnego uchwytu, jeśli nie zaobserwujemy żadnej reakcji (gatt unsubscribe).

Proces ten pozwala – metodą prób i błędów – zidentyfikować właściwy raport odpowiedzialny za obsługę klawiszy. Na listingu 11 można zauważyć, że atrybut o uchwycie 0x27 jest aktywnym raportem w tym pilocie i po jego wybraniu zaczynamy otrzymywać powiadomienia o wciśnięciach.

uart:~$ gatt subscribe 3f 3e
Subscribed
uart:~$ gatt unsubscribe
Unsubscribe success
Unsubscribed
uart:~$ gatt subscribe 28 27
Subscribed
Notification: value_handle 39, length 2
00000000: 01 00 |.. |
Notification: value_handle 39, length 2
00000000: 00 00 |.. |
Notification: value_handle 39, length 2
00000000: 02 00 |.. |
Notification: value_handle 39, length 2
00000000: 00 00 |.. |
Notification: value_handle 39, length 2
00000000: 04 00 |.. |
Notification: value_handle 39, length 2
00000000: 00 00 |.. |
Notification: value_handle 39, length 2
00000000: 08 00 |.. |
Notification: value_handle 39, length 2
00000000: 00 00 |.. |
Notification: value_handle 39, length 2
00000000: 10 00 |.. |
Notification: value_handle 39, length 2
00000000: 00 00 |.. |
uart:~$

Listing 11. Subskrybowanie raportów HID

Pilot, którego używamy, przypisuje jeden bit dla każdego z pięciu przycisków. Jednak format raportu urządzenia HID zależy od jego implementacji i jest opisany w mapie klawiszy HID (HID Report Map).

Taką mapę możemy z pilota odczytać – jest kolejną charakterystyką serwisu HID. Zgodnie ze standardem [1] UUID Report Map to 0x2A4B. Na listingu 12 pokazujemy, jak odnaleźć handle z tą wartością i odczytać mapę.

uart:~$ gatt discover 2a4b
Discover pending
Descriptor 2a4b found: handle 42
Discover complete

uart:~$ gatt read 42
Read pending
Read complete: err 0x00 length 22
00000000: 05 0c 09 01 a1 01 85 01 09 e9 09 ea 09 b5 09 b6 |.....|
00000010: 09 cd 09 40 09 30 |...@.|
Read complete: err 0x00 length 22
00000000: 0a 23 02 15 00 25 01 75 01 95 08 81 02 75 01 95 |.#...|
00000010: 01 05 0b 09 21 81 |....!|
Read complete: err 0x00 length 8
00000000: 02 75 01 95 07 81 03 c0 |.u...|
Read complete: err 0x00 length 0
uart:~$

Listing 12. Odczyt Memory Map

Taki zestaw danych niewiele mówi zwykłemu śmiertelnikowi, ale można go zdekodować jednym z dostępnych narzędzi. My użyjemy parsera online [5], gdyż jest to aplikacja prosta w obsłudze. Bardziej „ludzki” już opis raportu HID prezentuje rysunek 3.

Rysunek 3. Opis raportu HID

Można na nim zauważyć, że za stan klawiszy odpowiadają wartości w zakresie od 0 do 1 (Logical Minimum: 0 i Logical Maximum: 1), a także który bit raportu jest przypisany do jakiej funkcji klawisza. Warto jednak zauważyć, że mapa raportu zawiera osiem bitów, podczas gdy na pilocie dostępnych jest jedynie pięć klawiszy.

Okazuje się, że pilot wspiera także kombinacje klawiszy; na przykład wciśnięcie na dłużej obu klawiszy zmiany utworu spowoduje wysłanie raportu z wartością 0x80.

Podsumowanie

W tym odcinku symulowaliśmy działanie algorytmu typowego urządzenia centralnego, które komunikuje się z urządzeniem peryferyjnym. Wykonaliśmy kolejno wszystkie niezbędne operacje: skanowanie, łączenie, odkrywanie usług, odczytywanie oraz subskrybowanie charakterystyk...

Zahaczyliśmy nawet o profil HID. I choć nasz „algorytm” nie w pełni realizował założenia profilu HID, to udało nam się skutecznie uzyskać informacje o wciśniętych przyciskach.

Dzięki poleceniom w shellu Zephyra poznaliśmy strukturę tablicy atrybutów, odnaleźliśmy charakterystyki odpowiedzialne za raporty stanu klawiszy oraz zrealizowaliśmy subskrypcję powiadomień.

W niemal identyczny sposób można eksperymentować z klawiaturą BLE – sprawdziliśmy to!

W następnym odcinku dołączymy obsługę urządzenia HID do naszego projektu.

Krzysztof Kierys
Paweł Jachimowski

Odnośniki w tekście
[1] https://www.bluetooth.com/specifications/assigned-numbers/
[2] https://tiny.pl/hmxxcfc1
[3] https://www.bluetooth.com/specifications/specs/bas-1-1/
[4] https://tiny.pl/md8mnw-6
[5] https://eleccelerator.com/usbdescreqparser/

Artykuł ukazał się w
Elektronika Praktyczna
grudzień 2024
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik marzec 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec - kwiecień 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna marzec 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich kwiecień 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów