Bluetooth jest standardem z wieloletnią historią, który próbowano przystosować do wszystkiego. W rezultacie Bluetooth jest jak USB. Dla użytkownika jest bardzo prosty, lecz projektantów może przyprawić o ból głowy. Nie będziemy tu studiować dokładnie wszystkich możliwości, profili, charakterystyk i całej masy różnych skrótowców, jakie są związane z tym tematem. Zamiast tego zapoznamy się z dwoma praktycznymi przykładami.
Pierwszym będzie prosty kod, którego zadaniem jest utworzyć coś w rodzaju terminala pomiędzy telefonem a modułem ESP32. Zobaczymy w jaki sposób można przesyłać krótkie wiadomości tekstowe pomiędzy tymi urządzeniami. Na telefonie należy zainstalować aplikację obsługującą terminal, na przykład Serial Bluetooth Terminal. Działa na Androidzie i można ją bezpłatnie pobrać ze Sklepu Play.
W drugiej części zobaczymy, w jaki sposób można zrobić skaner. Napiszemy prosty kod, który będzie przez zadany czas szukał urządzeń w zasięgu i wyświetlał informacje o nich na konsoli MicroPythona.
Oba przykłady można uruchomić także na Raspberry Pi Pico W oraz 2W bez konieczności żadnych zmian w kodzie.
Terminal
Analizę programu zaczniemy od linii 21, gdzie znajduje kod, który wykonuje się jako pierwszy. W tym miejscu tworzymy instancję klasy BLE z modułu bluetooth i zapisujemy ją do zmiennej ble, którą będziemy wielokrotnie wykorzystywać w wielu innych miejscach naszego programu. Zwyczajem MicroPythona jest, że obiekty związane z komunikacją trzeba aktywować i robimy to, wywołując metodę active z argumentem True w linii 22.
Cała obsługa Bluetooth na ESP32 dokonuje się w przerwaniach. W linii 23 podajemy nazwę funkcji, która ma zostać wywołana, kiedy nastąpi jakiekolwiek zdarzenie związane z obsługą Bluetooth. Podajemy funkcję bluetooth_interrupt, którą omówimy w dalszej części artykułu.
Kolejnym krokiem jest skonfigurowanie serwisów, profili i charakterystyk. Jest to dość mocno zagmatwane i mało intuicyjne. Ogólnie rzecz biorąc, Bluetooth działa podobnie jak MQTT, gdzie subskrybujemy kanały, które chcemy odczytywać i publikujemy wiadomości w tych samych lub innych kanałach. W przypadku Bluetooth, możemy wykorzystywać wiele różnych kanałów do odczytu i zapisu wiadomości. Robimy to wywołując metodę gatts_register_services, do której przez argument podajemy krotkę SERVICES.
Przejdźmy teraz do linii 8, gdzie definiujemy, co to w ogóle jest SERVICES – jest to krotka, w której wymienione są wszystkie serwisy. W naszym przypadku będziemy wykorzystywać tylko jeden serwis, ale gdybyśmy chcieli więcej, wystarczy je tu wypisać po przecinku.
Uwaga – jeżeli krotka ma mieć tylko jeden element to po jego nazwie należy postawić przecinek! Podajemy tutaj UART_SERVICE i przeskakujemy do linii 7, gdzie jest to zdefiniowane.
Każdy serwis musi być utworzony jako krotka składająca się z dokładnie dwóch elementów (linia 7). Pierwszym jest UUID (Universally Unique Identifier) serwisu. W naszym przypadku jest to UART_UUID, który tworzymy w linii 4. Dokładniej rzecz biorąc, w MicroPythonie identyfikator UUID tworzymy jako obiekt klasy UUID. Tworzymy go, podając do konstruktora odpowiedni kod poprzez argument. Drugim elementem krotki serwisu jest kolejna krotka, w której wyszczególnione są charakterystyki.
W naszym przypadku charakterystykami są UART_TX oraz UART_RX, które tworzymy w liniach 5 i 6 jako kolejne… krotki. Ich pierwszym elementem jest UUID charakterystyki, tworzone w taki sam sposób jak omawiane wcześniej UUID serwisu. Drugim elementem są flagi, takie jak FLAG_READ, FLAG_WRITE, FLAG_NOTIFY, FLAG_INDICATE i FLAG_WRITE_NO_RESPONSE. Jeżeli charakterystyka ma mieć kilka flag jednocześnie, to należy oddzielić je operatorem |.
Wydaje się to strasznie zagmatwane, tym bardziej, że w roli UUID musimy podać długie szesnastkowe numery. Na potrzeby naszego przykładu, wziąłem kody UUID z usługi Transparent UART, jaka jest dostępna w modułach firmy Microchip, na przykład RN4870. Te charakterystyki są domyślnie wgrane do programu Serial Bluetooth Terminal. Teoretycznie numery UUID możemy sobie wpisać dowolne (teoretycznie!), ale wtedy musimy także skonfigurować je po stronie terminala w telefonie czy jakimkolwiek innym urządzeniu, które ma się łączyć z naszym programem na ESP32.
Wróćmy do linii 24, gdzie wywołujemy metodę gatts_register_services, przekazując jej te wszystkie kroki z krotkami. Metoda zwraca… krotkę z krotkami. Znajdują się w niej handles do poszczególnych charakterystyk. W naszym przykładzie, metoda zwraca dokładnie ((16, 19),) czyli krotkę, zawierającą krotkę, w której są liczby 16 i 19. Pierwsza z nich to handle do charakterystyki TX, a druga do RX. Te informacje będą nam potrzebne w przyszłości, więc proponuję zapisać je do nieco bardziej ludzko brzmiących zmiennych jak rx_handle i tx_handle.
Potrzebujemy jeszcze jeden handle do połączenia, czyli conn_handle. Póki co połączenia jeszcze żadnego nie mamy, więc do tej zmiennej wpisujemy wartość None.
W linii 25 wyświetlamy adres MAC naszego ESP32. Może się on przydać podczas skanowania urządzeń. Każde urządzenie w standardzie Bluetooth musi mieć unikalny adres, a nazwy nie musi mieć. Stosujemy tutaj funkcję print, która ma wyświetlić f-string, a wewnątrz niego wywołujemy funkcję get_my_mac.
Tę funkcję definiujemy w linii 17 i kilku kolejnych. Pobieramy adres MAC przy pomocy metody config z argumentem „mac”. W ten sposób dostajemy krotkę, której drugim argumentem jest obiekt typu bytes, składający się z sześciu bajtów tworzących adres MAC. W Pythonie wygląda to niezbyt przyjemnie, np. b’\xdc\xda\x0c\x1eN\xe2’. Kolejne linie mają na celu przekonwertowanie adresu na tradycyjny zapis, gdzie poszczególne bajty wyświetlane są w formacie szesnastkowym i oddzielane są dwukropkami.
W linii 26 włączamy advertising. Jest to usługa polegająca na tym, że urządzenie Bluetooth co pewien czas wysyła komunikaty informujące o swoim istnieniu. Dzięki temu inne urządzenia mogą skanować otoczenie i wykrywać urządzenia z włączonym advertisingiem. Istnieje możliwość, aby w pakiecie wysyłanym w ramach advertisingu była zawarta nazwa urządzenia.
Przeskoczmy do linii 13, gdzie utworzymy funkcję advertiser. Przyjmuje ona jeden argument i jest on nazwą urządzenia, jaka ma pojawić się podczas wyszukiwania na telefonie, tablecie, komputerze itp. W linii 14 sprawdzamy czy podany argument jest typu str i jeżeli tak, to przekształcamy go na bytes. Przypomnijmy, że str to napis, który może zawierać różne znaki diakrytyczne i jeżeli nie podamy jawnie kodowania, to domyślnie stosowane jest UTF-8. W linii 15 przygotowujemy dane, jakie mają być cyklicznie rozsyłane i zawieramy w nich nazwę naszego urządzenia. W linii 16 wywołujemy metodę gap_advertise. Pierwszym argumentem jest okres rozsyłania danych, podany w mikrosekundach, a drugim jest utworzona wcześniej tablica bajtów.
Przejdźmy teraz do linii 9, gdzie zaczyna się funkcja bluetooth_interrupt. Ta funkcja jest wywoływana automatycznie za każdym razem, kiedy wystąpi jakieś zdarzenie związane z interfacem Bluetooth. Przyjmuje dwa argumenty. Argument event to kod zdarzenia, a data to krotka, w której znajdują się różne dodatkowe informacje powiązane ze zdarzeniem. Kody zdarzeń są liczbami kolejnymi, ale żeby kod programy był bardziej czytelny, można sobie je zdefiniować przy pomocy stałych const, co robimy w linii 2 i kolejnych (dziwne, że takie rzeczy nie są zdefiniowane przez autorów MicroPythona w module bluetooth). Przypomnijmy, że const to coś, co odróżnia MicroPythona od Pythona znanego ze zwykłych komputerów. Stała jest zapisana w pamięci Flash i nie zajmuje RAMu.
Zdarzeń jest bardzo dużo i są opisane w dokumentacji modułu bluetooth, ale nam wystarczy obsługa tylko trzech z nich. W linii 10 sprawdzamy, czy otrzymaliśmy zdarzenie _IRQ_CENTRAL_CONNECT, co oznacza, że jakieś urządzenie nawiązało połączenie z ESP32. W takiej sytuacji jedyne co musimy zrobić, to odczytać handle połączenia, który jest zerowym elementem kroki data, a następnie zapisać go do zmiennej globalnej conn_handle.
W linii 11 mamy zdarzenie _IRQ_CENTRAL_DISCONNECT związane z zakończeniem połączenia. W takiej sytuacji musimy zresetować zmienną conn_handle wpisując do niej wartość None. Ważne jest, aby po zakończeniu połączenia ponownie uruchomić advertiser, bo inaczej ESP32 nie będzie mógł zostać wykryty przez inne urządzenia.
Ostatnim zdarzeniem, opisanym w linii 12, jest _IRQ_GATTS_WRITE, czyli zapisanie jakichś informacji do charakterystyki przez centralę, a mówiąc po ludzku, przesłanie danych z telefonu do ESP32. Handle do tych danych mamy w krotce data na pozycji 1. Możemy go od razu przekazać do funkcji gatts_read, która zwraca dane w postaci bytes. Jeżeli taka forma nas zadowala to możemy ją od razu przetworzyć (np. zapisać do jakiejś kolejki). W naszym przykładzie przekonwertujemy bytes na string w kodowaniu UTF-8, a następnie obetniemy wszelkie spacje i enetery na końcu przy pomocy metody strip. Finalnie, wyświetlamy odebrane dane na konsoli przy pomocy funkcji print.
Funkcja, która służy do przesyłania danych z ESP32 do telefonu to ble_send. Jej jedynym argumentem są dane do wysłania. Na początku sprawdzamy, czy Bluetooth w ogóle jest podłączony przy pomocy zmiennej conn_handle. Jeżeli jest ona równa None to znaczy, że nie mamy aktywnego połączenia, więc natychmiast wychodzimy z funkcji.