###### tags: `ESP32` `MicroPython` ###### tags: `ESP32` `MicroPython` `HID` `BLE` # ESP32 MicroPython BLE HID 實作鍵盤與多媒體控制器 要實作藍牙人機界面裝置 (BLE HID), 例如藍牙鍵盤、滑鼠等, 牽涉到兩個部分: - **HID 所需要的 HID Report Discriptor**:人機界面裝置在人的操作後需要傳送對應的資料給主端 (host, 也就是手機/電腦等), 這個資料稱為**報告**, HID Report Descriptor 就是用來描述這個裝置的報告格式, 這樣主端才知道如何解譯收到的資料。 - **BLE GATT Service**:建立可讓電腦/手機連線的 BLE 服務, 並且依照前述的報告格式傳送控制資訊給電腦/手機。 ## HID Report Discriptor HID Report Ddescriptor (也稱為 Report Map) 是用來說明人機界面裝置傳送的資料格式, 以及這些資料如何解譯, 這樣主端才知道收到這些資料時該怎麼反應。對於鍵盤、滑鼠這類人機界面裝置來說, 已經有現成的規範要依循, 以下我們會以鍵盤為例, 說明如何撰寫 HID Report Descriptor。 ### 資料項 HID Report Descriptor 是由一筆筆的資料項 (item) 構成, 每一筆資料項則是由類型與資料組成, 然後再由各種不同類型的資料項依照規定的結構組成完整的 HID Report Descriptor。 #### Usage Page HID Report Descriptor 的開頭必須要有 Usage Page 與 Usage 兩個資料項, 分別說明這一個 HID Report Descriptor 描述的裝置所屬的大分類與子分類, 大分類可[參考官方文件](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=16), 鍵盤、滑鼠等裝置都歸屬在編號 0x01 的 Generic Desktop Page。 在大分類下, 還會區分為個別的子分類, 像是 Generic Desktop Page 的子分類, 也可以參考[官方文件](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=31), 鍵盤就是編號 0x06 的 keyboard。 實際撰寫 HID Report Descriptor 時, 資料項的類型是用代碼來表示, [0x05 是 Usage Page](https://usb.org/sites/default/files/hid1_11.pdf#page=45), [0x09 是 Usage](https://usb.org/sites/default/files/hid1_11.pdf#page=50), 所以鍵盤裝置的 HID Report Descriptor 前兩個資料項如下 (以 Python 的 bytes 表示): ```python= HID_REPORT_MAP = bytes([ # report map for keyboard 0x05, 0x01, # Usage Page (Generic Desktop) 0x09, 0x06, # Usage (Keyboard) ``` 實際上資料類型代碼的最低兩個位元代表後面實際資料的長度, 像是剛剛的 0x05 和 0x09 最低的兩個位元是 01, 所以跟隨的資料長度就是一個位元組。 #### Collection 標示完裝置的分類後, 接著就是正式的內容了。由於單一實體裝置可能具備多個裝置的功能, 例如具有軌跡球的鍵盤就同時具備滑鼠與鍵盤的功能, 每一種功能都會有自己的獨立報告需要描述, 必須使用 Collection 與 End Collection 包起來。實際的寫法像是這樣: ```python= HID_REPORT_MAP = bytes([ # report map for keyboard 0x05, 0x01, # Usage Page (Generic Desktop) 0x09, 0x06, # Usage (Keyboard) 0xA1, 0x01, # Collection (Application) ... 0xC0, # End Collection ``` Collection 資料項的資料是用來說明這份報告的性質, 可參考[官方文件](https://usb.org/sites/default/files/hid1_11.pdf#page=38), 鍵盤就標示 0x01 的 Application。 #### Report ID 如同剛剛所說, 同一裝置可能會回報多種報告, 因此必須為每一種報告編號, 這樣主端才不會弄錯, 做出錯誤的回應。要為報告編號, 就使用 Report ID 資料項, 跟隨的資料就是序號, 將每一種報告依序編號, 例如我們的範例程式會製作一個同時可當鍵盤也可當多媒體控制器 (也就是可以控制播放、暫停、大小聲) 的裝置, 所以就會有兩種報告, 寫法如下: ```python= HID_REPORT_MAP = bytes([ # report map for keyboard 0x05, 0x01, # Usage Page (Generic Desktop) 0x09, 0x06, # Usage (Keyboard) 0xA1, 0x01, # Collection (Application) 0x85, 0x01, # Report ID (1) ... 0xC0, # End Collection # report map for Cosumer Control (media keys) 0x05, 0x0C, # Usage Page (CONSUMER PAGE) 0x09, 0x01, # Usage (Consumer Control) 0xA1, 0x01, # Collection (Application) 0x85, 0x02, # here Report ID (2) ... 0xC0, # End Collection ]) ``` 第 6 行幫第一種報告編號為 1, 而第 13 行幫第二種報告編號為 2。注意到如同前述, 每一種報告的描述就用 Collection 和 End Collection 資料項包起來。 #### Report Size 和 Report Count 接著就是描述真正要傳送的資料格式了, 描述的時候是以資料群組為對象。Report Size 是用來描述這個資料群組是以幾個位元為單位, 而 Report Count 則是這個資料群組共包含幾個資料單元。舉例來說, 鍵盤上有 ALT、CTRL、Shift、GUI(就是有印 Windows 圖示的那個) 等[稱為 modifier 的按鍵](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=88), 在標準鍵盤上左右都有一個, 總共 8 個按鍵, 如果我們想用 8 個位元來表示這些按鍵是否有被按住, 就可以這樣表示: ```python= 0x75, 0x01, # Report Size (1) 0x95, 0x08, # Report Count (8) ``` 由於個別按鍵是以個別的位元來表示, 因此 Report Size 是 1, 表示是 1 個位元, 但是這 8 個按鍵是一整組的資料, 所以用 Report Count 8 表示, 所以實際上要傳送報告時, 表示這會送出 1 個位元組, 其中個別位元就代表個別的按鍵。 定義好一組資料後, 必須再用 Usage Page 標示這組資料的意義, 這時候一樣可參考[官方文件](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=16), 因為這一組資料是代表按鍵, 所以加上這樣的描述: ```python= 0x05, 0x07, # Usage Page (Key Codes) ``` #### Logical Minimum/Maximum 和 Usage Minimum/Maximum 再來我們要針對這一組資料說明傳送的報告裡面可能的值, 以及對應到主端時的實際值。 ```python= 0x15, 0x00, # Logical Minimum (0) 0x25, 0x01, # Logical Maximum (1) ``` Logical 開頭的是這個資料群組裡單一單元的可能值, 由於這組資料是以位元為單元, 以 0 表示沒按下、1 表示按下, 所以 Logical Minimum 是 0、Logical Maximum 是 1。這是裝置傳送報告時真正傳送的資料。 Usage 開頭的則是主端收到報告後, 要轉換成的值, 如果你看[官方文件](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=88), 這裡記載了這些按鍵對應的 Usage 編碼, 分別從 0xE0 到 0xE7, 因此我們加上如下的敘述: ```python= 0x19, 0xE0, # Usage Minimum (224) 0x29, 0xE7, # Usage Maximum (231) ``` 這代表裝置送過來 1 個位元組給主端, 主端必須依據這個位元組的內容, 轉換成 0xE0~0xE7 之間的值, 才能判斷到底使用者按了哪些按鍵。 #### Input 剛剛的資料項說明了報告中傳遞的值, 以及要轉換的目的範圍, 但是沒有說明要怎麼轉換, 這就是 Input 資料項的作用了。 Input 項的資料說明的就是轉換的規則, 在[官方文件](https://usb.org/sites/default/files/hid1_11.pdf#page=38)中可以看到, 它基本上是 1 個位元組, 並以個別位元表示轉換的規則, 其中主要的是: |位元|說明| |----|----| |0|是可變的資料 (0, Data) 還是固定不變的常數 (1, Constant)| |1|轉換時是數值對應數值 (0, Array) 還是位元對應數值 (1, Variable)| |2|是絕對數值 (0, Absolute) 還是相對數值 (1, Relative)| 以我們剛剛的例子來說, 因為傳送的值會隨按鍵情況而異, 所以是 Data;另外我們是用單一位元表示單一按鍵, 所以轉換時要從位元對應到數值, 因此是 Variabl;最後, 這裡傳遞的是絕對的按鍵值, 不是像滑鼠移動時是傳遞相對於目前位置的移動量, 所以設為 Absolute。因此, 我們加上了這一項資料: ```python= 0x81, 0x02, # Input (Data, Variable, Absolute); Modifier byte ``` 0x02 表示 00000010, 所以是 Data/Variable/Absolute。到這裡, 就完成了鍵盤的報告中有關 modifier key 的描述: ```python= HID_REPORT_MAP = bytes([ # report map for keyboard 0x05, 0x01, # Usage Page (Generic Desktop) 0x09, 0x06, # Usage (Keyboard) 0xA1, 0x01, # Collection (Application) 0x85, 0x01, # Report ID (1) 0x75, 0x01, # Report Size (1) 0x95, 0x08, # Report Count (8) 0x05, 0x07, # Usage Page (Key Codes) 0x19, 0xE0, # Usage Minimum (224) 0x29, 0xE7, # Usage Maximum (231) 0x15, 0x00, # Logical Minimum (0) 0x25, 0x01, # Logical Maximum (1) 0x81, 0x02, # Input (Data, Variable, Absolute); Modifier byte ``` 這樣的轉換方式可以用單一位元組表示各種可能的組合鍵, 主端即可輕易判斷。 #### 保留給 OEM 廠商客製按鍵的欄位 在 USB 鍵盤的規範中, 要加上一組保留給鍵盤廠商的位元組, 在 HID Report Descriptor 中的寫法如下: ```python= 0x95, 0x01, # Report Count (1) 0x75, 0x08, # Report Size (8) 0x81, 0x01, # Input (Constant); Reserved byte ``` 表示這一組資料的單位是 8 個位元, 但只有 1 個單元, 並且是固定內容的 Constant/Array/Absolute。 #### 其他按鍵值 接著就是鍵盤上輸入文字的那些按鍵了, 我們直接看怎麼描述: ```python= 0x95, 0x06, # Report Count (6) 0x75, 0x08, # Report Size (8) 0x15, 0x00, # Logical Minimum (0) 0x25, 0x65, # Logical Maximum (101) 0x05, 0x07, # Usage Page (Key Codes) 0x19, 0x00, # Usage Minimum (0) 0x29, 0x65, # Usage Maximum (101) 0x81, 0x00, # Input (Data, Array); Key array (6 bytes) ``` 這一組資料的單位是 8 個位元, 也就是 1 個位元組, 代表 1 個按下的按鍵, 總共有 6 個, 也就是單一次報告可以傳送同時按下的 6 個按鍵。每個位元組傳送時的可能值是 0~101, 主端會轉換成同樣範圍的值, 這 0~101 就是 [USB 鍵盤規範的個別按鍵](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=83)。 #### 接收資訊 除了裝置可以傳送報告給主端, 主端也可以依據我們描述的格式傳送報告裝置, 例如鍵盤上的指示燈 (Caps lock 等), USB 鍵盤規範有 5 個, 是可以由主端控制的, 描述如下: ```python= 0x95, 0x05, # Report Count (5) 0x75, 0x01, # Report Size (1) 0x05, 0x08, # Usage Page (LEDs) 0x19, 0x01, # Usage Minimum (1) 0x29, 0x05, # Usage Maximum (5) 0x91, 0x02, # Output (Data, Variable, Absolute); LED report 0x95, 0x01, # Report Count (1) 0x75, 0x03, # Report Size (3) 0x91, 0x01, # Output (Constant); LED report padding ``` 它分成兩組資料, 第 1 組是從 5 個位元對應到 [1~5 的燈號](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=91), 第 2 組是為了填滿 1 個位元組額外加上的 3 個位元。 注意到這兩組資料都是用 Output 項而不是 Input 項。這裡的輸出或是輸入都是以主端的觀點, 不是裝置的觀點。 #### 完整內容 到這裡就把鍵盤的 HID Report Descriptor 寫完了, 根據上述, 每一次報告時資料總共有 8 個位元組, 依據為: ``` modified 按鍵, 保留欄位, 按鍵 1, 按鍵 2, 按鍵 3, 按鍵 4, 按鍵 5, 按鍵 6 ``` ### 綜合演練:撰寫多媒體控制器的 HID Report Descriptor 利用以上所學, 在我們的實作範例中加上了多媒體控制器的 HID Report Descriptor: ```python= # report map for Cosumer Control (media keys) 0x05, 0x0C, # Usage Page (CONSUMER PAGE) 0x09, 0x01, # Usage (Consumer Control) 0xA1, 0x01, # Collection (Application) 0x85, 0x02, # here Report ID (2) 0x75, 0x10, # Report Size (16) 0x95, 0x01, # Report Count (1) 0x15, 0x01, # Logical Minimum (1) 0x26, 0x8C, 0x02, # Logical Maximum (0x028C) 0x19, 0x01, # Usage Minimum (1) 0x2A, 0x8C, 0x02, # Usage Maximum (0x028C) 0x81, 0x00, # Input (Data, Array, Absolute); Modifier byte 0xC0, # End Collection ``` 這裡比較特別的是, 根據[官方文件](https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=118), 多媒體控制器的按鍵 Uasge 有可能是 2 個位元組, 因此在 9 行雖然是 Logical Maximum, 但它的代碼是 0x26, 而不是我們之前一直在用的 0x25, 這就是因為最後 2 個位元是 10 而不是 01, 表示這一項資料有 2 個位元組, 才能容納 0x028C 這樣的數值;同樣的作法也出現在第 11 行的 Usage Maximum 中。 ### 參考文件: - Usage Page 及 Usage ID 請查[Universal Serial Bus HID Usage Tables](https://www.usb.org/document-library/hid-usage-tables-122)。 - Usage Table 請查[HID Usage Tables FOR Universal Serial Bus (USB)](https://usb.org/sites/default/files/hut1_2.pdf)。 - 有關 HID Report Descriptor 的格式, 可參考[Device Class Definition for HID](https://www.usb.org/document-library/device-class-definition-hid-111)。 - 可以用[HID Descriptor Tool](https://www.usb.org/document-library/hid-descriptor-tool) 工具協助撰寫 HID Report Descriptor。 ### 參考實作 - CircuitPython 的 [hid_device](https://github.com/hathach/tinyusb/blob/ab4d30fd6bca02c73eb9b4ff82db0b2b0f403344/src/class/hid/hid_device.h) - [Adafruit CircuitPython HID](https://github.com/adafruit/Adafruit_CircuitPython_HID/tree/master/adafruit_hid) 程式庫 ## BLE 要實作 BLE HID 設備, 主要參考的文件就是 [HID over GATT Profile](https://www.bluetooth.com/specifications/specs/hid-over-gatt-profile-1-0/) 必須提供以下 3 種服務: 1. [Battery service](https://www.bluetooth.com/specifications/specs/battery-service-1-0/):提供電量資訊給主端, 在這個服務中, 一定要具備的特徵是 Battery Level。 2. [HID sevice](https://www.bluetooth.com/specifications/specs/hid-service-1-0/):這個服務比較複雜, 稍後詳細說明。 3. [device information service](https://www.bluetooth.com/specifications/specs/device-information-service-1-1/):提供有關裝置的製造商資訊, 一定要具備 PnP ID 特徵, 傳遞以下格式的資訊給主端: |名稱|位元組數|說明| |---|------|---| |廠商代號來源|1|1:[Bluetooth 會員](https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/)<br/>2:[USB 廠商代號](https://www.usb.org/sites/default/files/vendor_ids033021.pdf)| |廠商代號|2|參考上述來源, 例如 0x02E5 是 Espreeif| |產品編號|2|自取| |產品版本|2|自取| ### HID Service HID Services 要求要建立一個 HID Service Server, 它的 UUID 是 [0x1812](https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf#page=21), 這個 Service 必須有以下的特徵: 1. [Protocol Mode](https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf#page=13):用來說明這個裝置是使用 Boot Mode(0) 還是 Report Mode(1)。 2. 兩個 [Report](https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf#page=13): - Read/Notify 的 Input Report:用來讓 HID 裝置傳送 report 給主端, 例如按鍵資訊或是滑鼠動作。 - Read/Write/Write Without Response 的 Output Report:讓主端送出控制指令給 HID 裝置, 像是從主機透過軟體設定鍵盤的燈號。 每個 Report 特徵需要有一個 Report Reference 描述器說明這個 Report 負責 Report Map 哪一個 Report ID 的 Report, 以及是 Input(1) 還是 Output(2) Report。 Input Report 還需要一個 Client Characteristic Configuration 描述器, 用來說明 Input Report 特徵的值中, 哪一段才是真正的 Report。 1. Report Map:用來傳遞 HID Report Descriptor: 3. HID Information:用來說明裝置屬性, 包含: |欄位|長度|說明| |---|---|---| |實作版本|2 位元組|這是指[Device Class Definition for HID](https://www.usb.org/document-library/device-class-definition-hid-111) 的版本, 像是 1.11 的話, 就是 b'\x11\x01'。| |國別碼|1 位元組|若是本地化版本的裝置, 就要設置國別碼, 否則填 0 即可。| |旗標|1 位元組|位元 0 表示可 (1)否 (0) 送訊號喚醒主端;位元 1 表示在已細節但未連接狀態下是 (1) 否 (0) 還要廣播| 像是範例程式中就使用以下的數值: 4. HID Control Point:用來讓主端通知裝置進入省電的 [Suspend state](https://www.itread01.com/content/1550043933.html)。 我們的範例程式中, 就建立了以下的服務: ```python=86 # 建立伺服器 hid_service = ( # 服務 UUID(0x1812), ( # Human Interface Device (UUID(0x2A4A), F_READ), # HID information (UUID(0x2A4B), F_READ), # HID report map (UUID(0x2A4C), F_WRITE), # HID control point (UUID(0x2A4D), F_READ_NOTIFY, ( # Report(input) (UUID(0x2908), ATT_F_READ), # Report reference # Client Characteristic Configuration (UUID(0x2902), ATT_F_READ), )), (UUID(0x2A4D), F_READ_WRITE_NORESPONSE, ( # Report(output) (UUID(0x2908), ATT_F_READ), # Report reference )), (UUID(0x2A4D), F_READ_NOTIFY, ( # Report(comsumer control) (UUID(0x2908), ATT_F_READ), # Report reference )), (UUID(0x2A4E), F_READ_WRITE), # HID protocol mode ), ) devinfo_service = ( UUID(0x180a), ( # device info service (UUID(0x2a50), F_READ), # PnP ID (UUID(0x2a29), F_READ), # Manufacturer Name String ), ) bat_service = ( UUID(0x180f), ( # battery Service (UUID(0x2A19), F_READ_NOTIFY, ( # Battery Level # Client Characteristic Configuration (UUID(0x2902), ATT_F_READ_WRITE), # for notify # Characteristic Presentation Format (UUID(0x2904), ATT_F_READ), )), ), ) # register services 註冊服務 ble.config(gap_name="MP-keyboard") handles = ble.gatts_register_services(( hid_service, devinfo_service, bat_service, )) print(handles) # 依序對應到服務中定義的特徵與描述器 h_info, h_map, _, h_rep, h_d1, _, _, h_d2, h_com, h_d3, h_proto = handles[0] h_pnp, h_manu = handles[1] h_bat, _, h_fmt = handles[2] # set initial data # HID info: ver=1.11, country=0, flags=normal ble.gatts_write(h_info, b"\x11\x01\x00\x00") ble.gatts_write(h_map, HID_REPORT_MAP) # HID report map ble.gatts_write(h_d1, struct.pack("<BB", 1, 1)) # report: id=1, type=input ble.gatts_write(h_d2, struct.pack("<BB", 1, 2)) # report: id=1, type=output ble.gatts_write(h_d3, struct.pack("<BB", 2, 1)) # report: id=2, type=input ble.gatts_write(h_proto, b"\x01") # protocol mode: report (0 for boot mode) # format:UINT8 ble.gatts_write(h_fmt, b'\x04\x00\xad\x27\x01\x00\x00') ble.gatts_write(h_bat, b"\x64") # battery level, always 100% # vendor id source 0x01 -> Bluetooth company identify # vendor id 0x02E5 -> Expressif # Product id 0xA111 -> the ESP32BleKyboard lib's prod id # product version 0x0210 -> V2.1.0 ble.gatts_write(h_pnp, b'\x01\xe5\x02\x11\xa1\x10\x02') ble.gatts_write(h_manu, b'Espressif') ``` ### Advertising 在廣播的時候, BLE HID 必須使用 UUID 廣播類型, 並使用 BLE HID 的 UUID x01812。此外, 廣播封包中還必須傳送 Local Name 與 Appearance, 像是範例程式終究傳送這樣的廣播內容: ```python=160 adv = ( b"\x02\x01\x06" # flag: 0x0110, b"\x03\x03\x12\x18" # BLE HID UUID:0x1812 b"\x03\x19\xc1\x03" # appearance:keyboard b"\x0c\x09MP-keyboard" # local name(要與上面的gap_name一樣) ) conn_handle = None ble.gap_advertise(100_000, adv) ``` :::warning 根據規格書以及[這裡的說明](https://docs.silabs.com/bluetooth/latest/code-examples/applications/ble-hid-keyboard#security), BLE HID 裝置端必須採用 bond 以及 LE Security Mode 1, 但是 MicroPython 的實作中, ubluetooth 的[設定](http://docs.micropython.org/en/latest/library/ubluetooth.html#configuration)到了 1.15 版才支援這兩項功能, 可惜的是 [ESP32 的版本並不支援](https://forum.micropython.org/viewtopic.php?f=18&t=9524#p53538), 僅支援 STM32。 因此, 儘管[在這裡有 MicroPython BLE Keyboard/mouse 的範例](https://github.com/dpgeorge/micropython/tree/examples-bluetooth-ble-hid/examples/bluetooth), 這些範例只能配合 Windows/Linux/Android 運作, macOS/iOS 似乎都嚴守規範, 無法配合運作。 ::: ## Arduino Core for ESP32 實作參考 - Arduino Core for ESP32 中的 [BLEHIDDevice](https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/src/BLEHIDDevice.cpp) - [ESP32 BLE Keyboard 程式庫](https://github.com/T-vK/ESP32-BLE-Keyboard) ## BLE 規格書 - BLE 個別[規範](https://www.bluetooth.com/specifications/specs/)參考。 - [BLE 廣播資料](https://www.bluetooth.com/specifications/specs/core-specification-supplement-9/) ## 實作範例 請參考[Github 上的範例](https://github.com/codemee/esp32_mp_ble_tests/blob/master/BLE_HID_Combo.py)