niedziela, 27 marca 2022

[12] ESP32 - Arduino - Klawiatura Bluetooth

W tym poście chciałbym opisać sposób wykonania klawiatury na bluetooth z modułem ESP32.

Znalezione obrazy dla zapytania arduino esp32
[Źródło: http://paulobrien.co.nz/2017/03/16/esp32-programming-with-arduino-on-windows/]

Klawiatura bluetooth:


Biblioteka dostępna jest w serwisie GitHub pod tym linkiem (https://github.com/T-vK/ESP32-BLE-Keyboard)

Deskryptor wygląda następująco:

  1. static const uint8_t _hidReportDescriptor[] = {
  2.   USAGE_PAGE(1),      0x01,          // USAGE_PAGE (Generic Desktop Ctrls)
  3.   USAGE(1),           0x06,          // USAGE (Keyboard)
  4.   COLLECTION(1),      0x01,          // COLLECTION (Application)
  5.   // ------------------------------------------------- Keyboard
  6.   REPORT_ID(1),       KEYBOARD_ID,   //   REPORT_ID (1)
  7.   USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  8.   USAGE_MINIMUM(1),   0xE0,          //   USAGE_MINIMUM (0xE0)
  9.   USAGE_MAXIMUM(1),   0xE7,          //   USAGE_MAXIMUM (0xE7)
  10.   LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  11.   LOGICAL_MAXIMUM(1), 0x01,          //   Logical Maximum (1)
  12.   REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  13.   REPORT_COUNT(1),    0x08,          //   REPORT_COUNT (8)
  14.   HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  15.   REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 1 byte (Reserved)
  16.   REPORT_SIZE(1),     0x08,          //   REPORT_SIZE (8)
  17.   HIDINPUT(1),        0x01,          //   INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  18.   REPORT_COUNT(1),    0x05,          //   REPORT_COUNT (5) ; 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
  19.   REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  20.   USAGE_PAGE(1),      0x08,          //   USAGE_PAGE (LEDs)
  21.   USAGE_MINIMUM(1),   0x01,          //   USAGE_MINIMUM (0x01) ; Num Lock
  22.   USAGE_MAXIMUM(1),   0x05,          //   USAGE_MAXIMUM (0x05) ; Kana
  23.   HIDOUTPUT(1),       0x02,          //   OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  24.   REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 3 bits (Padding)
  25.   REPORT_SIZE(1),     0x03,          //   REPORT_SIZE (3)
  26.   HIDOUTPUT(1),       0x01,          //   OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  27.   REPORT_COUNT(1),    0x06,          //   REPORT_COUNT (6) ; 6 bytes (Keys)
  28.   REPORT_SIZE(1),     0x08,          //   REPORT_SIZE(8)
  29.   LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM(0)
  30.   LOGICAL_MAXIMUM(1), 0x65,          //   LOGICAL_MAXIMUM(0x65) ; 101 keys
  31.   USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  32.   USAGE_MINIMUM(1),   0x00,          //   USAGE_MINIMUM (0)
  33.   USAGE_MAXIMUM(1),   0x65,          //   USAGE_MAXIMUM (0x65)
  34.   HIDINPUT(1),        0x00,          //   INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  35.   END_COLLECTION(0),                 // END_COLLECTION
  36.   // ------------------------------------------------- Media Keys
  37.   USAGE_PAGE(1),      0x0C,          // USAGE_PAGE (Consumer)
  38.   USAGE(1),           0x01,          // USAGE (Consumer Control)
  39.   COLLECTION(1),      0x01,          // COLLECTION (Application)
  40.   REPORT_ID(1),       MEDIA_KEYS_ID, //   REPORT_ID (3)
  41.   USAGE_PAGE(1),      0x0C,          //   USAGE_PAGE (Consumer)
  42.   LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  43.   LOGICAL_MAXIMUM(1), 0x01,          //   LOGICAL_MAXIMUM (1)
  44.   REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  45.   REPORT_COUNT(1),    0x10,          //   REPORT_COUNT (16)
  46.   USAGE(1),           0xB5,          //   USAGE (Scan Next Track)     ; bit 0: 1
  47.   USAGE(1),           0xB6,          //   USAGE (Scan Previous Track) ; bit 1: 2
  48.   USAGE(1),           0xB7,          //   USAGE (Stop)                ; bit 2: 4
  49.   USAGE(1),           0xCD,          //   USAGE (Play/Pause)          ; bit 3: 8
  50.   USAGE(1),           0xE2,          //   USAGE (Mute)                ; bit 4: 16
  51.   USAGE(1),           0xE9,          //   USAGE (Volume Increment)    ; bit 5: 32
  52.   USAGE(1),           0xEA,          //   USAGE (Volume Decrement)    ; bit 6: 64
  53.   USAGE(2),           0x23, 0x02,    //   Usage (WWW Home)            ; bit 7: 128
  54.   USAGE(2),           0x94, 0x01,    //   Usage (My Computer) ; bit 0: 1
  55.   USAGE(2),           0x92, 0x01,    //   Usage (Calculator)  ; bit 1: 2
  56.   USAGE(2),           0x2A, 0x02,    //   Usage (WWW fav)     ; bit 2: 4
  57.   USAGE(2),           0x21, 0x02,    //   Usage (WWW search)  ; bit 3: 8
  58.   USAGE(2),           0x26, 0x02,    //   Usage (WWW stop)    ; bit 4: 16
  59.   USAGE(2),           0x24, 0x02,    //   Usage (WWW back)    ; bit 5: 32
  60.   USAGE(2),           0x83, 0x01,    //   Usage (Media sel)   ; bit 6: 64
  61.   USAGE(2),           0x8A, 0x01,    //   Usage (Mail)        ; bit 7: 128
  62.   HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  63.   END_COLLECTION(0)                  // END_COLLECTION
  64. };

Dokładniejszy opis można znaleźć np. tutaj

Wspomniana biblioteka pozwala na obsługę ESP32 jako klawiatura podłączona po Bluetooth. Oprócz przesyłania standardowych klawiszy ASCII oraz pozostałych klawiszy standardowo dostępnych na klawiaturze. Jest możliwość przesłania zestawu klawiszy specjalnych:

  1. typedef uint8_t MediaKeyReport[2];
  2.  
  3. const MediaKeyReport KEY_MEDIA_NEXT_TRACK = {1, 0};
  4. const MediaKeyReport KEY_MEDIA_PREVIOUS_TRACK = {2, 0};
  5. const MediaKeyReport KEY_MEDIA_STOP = {4, 0};
  6. const MediaKeyReport KEY_MEDIA_PLAY_PAUSE = {8, 0};
  7. const MediaKeyReport KEY_MEDIA_MUTE = {16, 0};
  8. const MediaKeyReport KEY_MEDIA_VOLUME_UP = {32, 0};
  9. const MediaKeyReport KEY_MEDIA_VOLUME_DOWN = {64, 0};
  10. const MediaKeyReport KEY_MEDIA_WWW_HOME = {128, 0};
  11. const MediaKeyReport KEY_MEDIA_LOCAL_MACHINE_BROWSER = {0, 1}; // Opens "My Computer" on Windows
  12. const MediaKeyReport KEY_MEDIA_CALCULATOR = {0, 2};
  13. const MediaKeyReport KEY_MEDIA_WWW_BOOKMARKS = {0, 4};
  14. const MediaKeyReport KEY_MEDIA_WWW_SEARCH = {0, 8};
  15. const MediaKeyReport KEY_MEDIA_WWW_STOP = {0, 16};
  16. const MediaKeyReport KEY_MEDIA_WWW_BACK = {0, 32};
  17. const MediaKeyReport KEY_MEDIA_CONSUMER_CONTROL_CONFIGURATION = {0, 64}; // Media Selection
  18. const MediaKeyReport KEY_MEDIA_EMAIL_READER = {0, 128};

Przesłanie klawisza wykonuje się przez 

  1. //Wciśnięcie klawisza
  2. size_t press(uint8_t k);
  3. size_t press(const MediaKeyReport k);
  4. //Puszczenie klawisza
  5. size_t release(uint8_t k);
  6. size_t release(const MediaKeyReport k);
  7. void releaseAll(void);
  8. //Wpisanie
  9. size_t write(uint8_t c);
  10. size_t write(const MediaKeyReport c);
  11. size_t write(const uint8_t *buffer, size_t size);

Funkcja press() dodaje wybrany klawisz do raportu po czym wysyła go przez bluetooth. 

Biblioteka oferuje też przekierowanie funkcji print(), tak aby wiadomość została wysłana do urządzenia przez USB. Jej użycie można znaleźć w przykładzie dołączonym do biblioteki.

Wciśnięcie każdego przycisku musi być zakończone jego puszczeniem. Inaczej przycisk będzie interpretowany jako ciągle uruchomiony.

Połączenie Bluetooth:

Na samym początku musimy stworzyć obiekt klasy BleKeyboard:

  1. BleKeyboard::BleKeyboard(std::string deviceName, std::string deviceManufacturer, uint8_t batteryLevel)
  2.     : hid(0)
  3.     , deviceName(std::string(deviceName).substr(0, 15))
  4.     , deviceManufacturer(std::string(deviceManufacturer).substr(0,15))
  5.     , batteryLevel(batteryLevel) {}

Może ona być utworzona bez żadnych argumentów, w tym przypadku zostaną do niej przypisane standardowe informacje. W przypadku podawania danych do dyspozycji jest nazwa urządzenia oraz nazwa producenta (obie informacje zostały ograniczone do 15 znaków). Ostatnim elementem jest poziom naładowania baterii w urządzeniu (0 - 100).

Rozpoczęcie pracy modułu bluetooth rozpoczynamy od wywołania funkcji begin():

  1. bleKeyboard.begin();
  2. bleKeyboard.begin();

W pętli sprawdzany jest stan flagi connected, która informuje o nawiązaniu połączenia z modułem bluetooth:

  1. bool BleKeyboard::isConnected(void) {
  2.   return this->connected;
  3. }
  4.  
  5. void loop() {
  6.   if(bleKeyboard.isConnected()) {
  7.   }
  8. }

Program:


W poniższym projekcie chciałbym wykonać klawiaturę pozwalającą na obsługę następujących klawiszy multimedialnych:

Do tego celu na szybko wykorzystam standardową klawiaturę membranową 4x4 (przykładowy link). 

Obsługa klawiatury działa poprzez bibliotekę Keypad:

  1. #include <BleKeyboard.h>
  2. #include <Keypad.h>
  3. //-------------------------------------------------
  4. #define NORMAL_KEY_TO_SEND
  5. //-------------------------------------------------
  6. #define ROW_NUM     4 // four rows
  7. #define COLUMN_NUM  4 // four columns
  8.  
  9. char keys[ROW_NUM][COLUMN_NUM] = {
  10.   {'1', '2', '3', 'A'},
  11.   {'4', '5', '6', 'B'},
  12.   {'7', '8', '9', 'C'},
  13.   {'*', '0', '#', 'D'}
  14. };
  15.  
  16. byte pin_rows[ROW_NUM]      = {23, 22, 21, 19};
  17. byte pin_column[COLUMN_NUM] = {18, 5, 4, 15};
  18.  
  19. Keypad keypad = Keypad( makeKeymap(keys), pin_rows, pin_column, ROW_NUM, COLUMN_NUM );
  20. BleKeyboard bleKeyboard("BLE KEYBOARD V1.0", "Bluetooth Device Manufacturer", 100);
  21.  
  22. void setup() {
  23.   Serial.begin(115200);
  24.   Serial.println("Device Enabled");
  25.   bleKeyboard.begin();
  26. }
  27.  
  28. void loop() {
  29.   char key = keypad.getKey();
  30.  
  31.   if(bleKeyboard.isConnected()) {
  32.     if(key){
  33.       #if defined(NORMAL_KEY_TO_SEND)
  34.       bleKeyboard.write(key);
  35.       #else
  36.       SendResponseToKey(key);
  37.       #endif
  38.     }
  39.   }
  40. }

Wysłanie innego klawisza niż ten zwrócony przez bibliotekę wygląda następująco:

  1. void SendResponseToKey(char clickedKey) {
  2.   if(clickedKey == '1') {
  3.     bleKeyboard.write(KEY_MEDIA_STOP);
  4.   }
  5.   else if(clickedKey == '2') {
  6.     bleKeyboard.write(KEY_MEDIA_PLAY_PAUSE);
  7.   }
  8.   else if(clickedKey == '3') {
  9.     bleKeyboard.write(KEY_MEDIA_NEXT_TRACK);
  10.   }
  11.   else if(clickedKey == '4') {
  12.     bleKeyboard.write(KEY_MEDIA_PREVIOUS_TRACK);    
  13.   }
  14.   else if(clickedKey == '5') {
  15.     bleKeyboard.write(KEY_MEDIA_STOP);
  16.   }
  17.   else if(clickedKey == '6') {
  18.      bleKeyboard.write(KEY_MEDIA_WWW_HOME);  
  19.   }
  20.   else if(clickedKey == '7') {
  21.     bleKeyboard.write(KEY_MEDIA_LOCAL_MACHINE_BROWSER);    
  22.   }
  23.   else if(clickedKey == '8') {
  24.     bleKeyboard.write(KEY_MEDIA_CALCULATOR);    
  25.   }
  26.   else if(clickedKey == '9') {
  27.      bleKeyboard.write(KEY_MEDIA_WWW_SEARCH);  
  28.   }
  29.   else if(clickedKey == '0') {
  30.      bleKeyboard.write(KEY_MEDIA_PLAY_PAUSE);  
  31.   }
  32.   else if(clickedKey == '*') {
  33.     bleKeyboard.write(KEY_MEDIA_CONSUMER_CONTROL_CONFIGURATION);
  34.   }
  35.   else if(clickedKey == '#') {
  36.     bleKeyboard.write(KEY_MEDIA_EMAIL_READER);    
  37.   }
  38.   else if(clickedKey == 'A') {
  39.     bleKeyboard.write(KEY_MEDIA_VOLUME_UP);
  40.   }
  41.   else if(clickedKey == 'B') {
  42.     bleKeyboard.write(KEY_MEDIA_VOLUME_DOWN);
  43.   }
  44.   else if(clickedKey == 'C') {
  45.     bleKeyboard.write(KEY_MEDIA_MUTE);
  46.   }
  47.   else if(clickedKey == 'D') {
  48.     bleKeyboard.write(KEY_MEDIA_WWW_STOP);
  49.   }
  50. }

W instrukcjach warunkowych wybieramy kliknięcie klawisza multimedialnego w zależności od klawisza z klawiatury fizycznej.

Można też wykonać taką klawiaturę pozwalającą na obsługę skrótów w różnych programach. Poniżej przykład dla programu Altium Designer. Klawisze skrótów są wykonywane przez kliknięcie kilku przycisków w odpowiedniej kolejności.

  1. /*
  2.  * 1 -> //SCH pn //(PLACE -> NET LABEL)
  3.  * 2 -> //SCH pw //(PLACE -> WIRE)
  4.  * 3 -> //SCH pr //(PLACE -> PORT)
  5.  * 4 -> //SCH pt //(PLACE -> TEXT STRING)
  6.  * 5 -> //SCH po //(PLACE -> POWER PORT)
  7.  * 6 -> //SCH
  8.  * 7 -> //SCH
  9.  * 8 -> //SCH
  10.  * 9 -> //PCB pv //(PLACE -> VIA)
  11.  * * -> //PCB pp //(PLACE -> PAD)
  12.  * 0 -> //PCB pt //(PLACE -> INTERACTIVE ROUTING)
  13.  * # -> //PCB ps //(PLACE -> STRING)
  14.  * A -> //PCB pg //(PLACE -> POLYGON POUR)
  15.  * B -> //PCB tga //(TOOLS -> POLYGON POURS -> REBUILD ALL)
  16.  * C -> //PCB
  17.  * D -> //PCB
  18.  */

Informacje do komputera można przesłać na dwa sposoby:

  1. //#1 -  bleKeyboard.print("pn");
  2. //#2 -  bleKeyboard.write('p');
  3. //      bleKeyboard.write('w');

W pierwszym przypadku wykorzystuję funkcje print, w drugim funkcje write, gdzie wysyłam dane za pomocą wywołania funkcji osobno dla każdego znaku. 

Cała zmodyfikowana funkcja wygląda następująco:

  1. void SendResponseToKey(char clickedKey) {
  2.   if(clickedKey == '1') {    
  3.     bleKeyboard.print("pn");
  4.   }
  5.   else if(clickedKey == '2') {    
  6.     bleKeyboard.print("pw");
  7.   }
  8.   else if(clickedKey == '3') {
  9.     bleKeyboard.print("pr");
  10.   }
  11.   else if(clickedKey == '4') {
  12.     bleKeyboard.print("pt");
  13.   }
  14.   else if(clickedKey == '5') {
  15.     bleKeyboard.print("po");
  16.   }
  17.   else if(clickedKey == '6') {  
  18.   }
  19.   else if(clickedKey == '7') {  
  20.   }
  21.   else if(clickedKey == '8') {  
  22.   }
  23.   else if(clickedKey == '9') {
  24.     bleKeyboard.print("pv");
  25.   }
  26.   else if(clickedKey == '0') {
  27.     bleKeyboard.print("pp");
  28.   }
  29.   else if(clickedKey == '*') {
  30.     bleKeyboard.print("pt");
  31.   }
  32.   else if(clickedKey == '#') {
  33.     bleKeyboard.print("ps");  
  34.   }
  35.   else if(clickedKey == 'A') {
  36.     bleKeyboard.print("pg");
  37.   }
  38.   else if(clickedKey == 'B') {
  39.     bleKeyboard.print("tga");
  40.   }
  41.   else if(clickedKey == 'C') {
  42.   }
  43.   else if(clickedKey == 'D') {
  44.   }
  45. }

Aby zwiększyć czytelność można skorzystać np. z define.

Poniżej jeszcze prosty projekt pozwalający na obsługę klawiatury multimedialnej z pozycji joysticku sterowanego przez odczyt danych z dwóch osi ADC:


Dane odczytane są wartościami maksymalnymi. Częściowe przejechani kontrolerem lub obrócenie go o 45 stopni spowoduje odczytanie wyników z pomiędzy podanego zakresu. W tym przypadku natomiast program ma za zadanie obsługę prostych wiadomości multimedialnych. Pin oznaczony jako SW obsługuje przycisk aktywowany po kliknięciu joystick'a.

  1. #include <BleKeyboard.h>
  2.  
  3. #define Analog_12_VRx    12
  4. #define Analog_14_VRy    14
  5.  
  6. int AN_Pot_VRx_Result = 0;
  7. int AN_Pot_VRy_Result = 0;
  8.  
  9. uint8_t flag_UP = 0x00;
  10. uint8_t flag_DOWN = 0x00;
  11. uint8_t flag_RIGHT = 0x00;
  12. uint8_t flag_LEFT = 0x00;
  13.  
  14. BleKeyboard bleKeyboard("BLE KEYBOARD V1.0", "Bluetooth Device Manufacturer", 100);
  15.  
  16. void setup()
  17. {
  18.    Serial.begin(115200);
  19.    Serial.println("Device Enabled");
  20.    bleKeyboard.begin();
  21. }
  22.  
  23. void loop()
  24. {
  25.   if(bleKeyboard.isConnected()) {
  26.     SendResponseToKey();
  27.   }
  28.   delay(100);
  29. }
  30.  
  31. void SendResponseToKey(void) {
  32.   AN_Pot_VRx_Result = analogRead(Analog_12_VRx);
  33.   AN_Pot_VRy_Result = analogRead(Analog_14_VRy);
  34.  
  35.   if((AN_Pot_VRx_Result == 0) && (flag_UP == 0))
  36.   {
  37.     bleKeyboard.write(KEY_MEDIA_MUTE);
  38.     flag_UP = 1;
  39.   }
  40.   else
  41.   {
  42.     flag_UP = 0;
  43.   }
  44.  
  45.   if((AN_Pot_VRx_Result == 4095) && (flag_DOWN == 0))
  46.   {
  47.     bleKeyboard.write(KEY_MEDIA_PLAY_PAUSE);
  48.     flag_DOWN = 1;
  49.   }
  50.   else
  51.   {
  52.     flag_DOWN = 0;
  53.   }
  54.  
  55.   if((AN_Pot_VRy_Result == 0) && (flag_RIGHT == 0))
  56.   {
  57.     bleKeyboard.write(KEY_MEDIA_VOLUME_DOWN);
  58.     flag_RIGHT = 1;
  59.   }
  60.   else
  61.   {
  62.     flag_RIGHT = 0;
  63.   }
  64.  
  65.   if((AN_Pot_VRy_Result == 4095) && (flag_LEFT == 0))
  66.   {
  67.     bleKeyboard.write(KEY_MEDIA_VOLUME_UP);
  68.     flag_LEFT = 1;
  69.   }
  70.   else
  71.   {
  72.     flag_LEFT = 0;
  73.   }
  74. }

Oczywiście w projekcie który miałby być wykorzystywany do codziennych zadań należałoby wykorzystać znacznie lepszej jakości przyciski. Ta klawiatura jest raczej niskiej jakości i nie sprawia żadnej przyjemności z jej używania. Podłączone do modułu ESP według własnego uznania.