本文旨在補充《超圖解ESP32深度實作》第16章「BLE藍牙人機輸入裝置應用實作」單元,說明HID裝置的Vender ID(廠商識別碼,縮寫VID)以及Product ID(產品識別碼,縮寫PID),以及ESP32的BLEHIDDevice類別的一些方法,並且編寫一個藍牙BLE遊戲控制器程式庫。
ESP32-BLE-Gamepad程式庫
“lemmingDev”編寫了一個具備下列功能的“ESP32-BLE-Gamepad”程式庫:
- 支援Windows, Android, Linux和macOS,不支援iOS。
- 高達128個按鍵
- 6軸類比搖桿(x, y, z, rZ, rX, rY)、16位元解析度
- 2個滑桿、16位元解析度
- 4組HAT(一個十字鍵和3個帽子開關)
- 模擬器控制裝置(方向舵、方向盤、油門、煞車…)
- 可設置的HID描述器(descriptor)
- 可客製化的藍牙裝置名稱和製造商名稱
在Arduino IDE的程式庫管理員搜尋“esp32 ble gamepad”,即可找到並安裝這個程式庫:
底下是ESP32 BLE Gamepad專案原始碼頁面展示的範例程式碼,筆者把其中訊息和註解翻譯成中文:
#include <BleGamepad.h> BleGamepad bleGamepad; void setup() { Serial.begin(115200); Serial.println("BLE低功耗藍牙開工了~"); bleGamepad.begin(); // 上面的bleGamepad.begin() 敘述等同 // bleGamepad.begin(16, 1, true, true, true, true, true, true, true, true, false, false, false, false, false); // 代表此裝置有16個按鍵、1個十字鍵、啟用x, y, z, rZ, rX, rY, 滑桿1, 滑桿2、沒有方向舵、推進器、加速器、煞車和方向盤。 // 預設啟用「自動傳送HID報告」 // 執行bleGamepad.setAutoReport(false); 可停止自動傳送報告 // 然後透過bleGamepad.sendReport(); 在需要時傳送報告 } void loop() { if(bleGamepad.isConnected()) { Serial.println("按下按鍵5和16,所有搖桿軸都推到最大值,十字鍵(hat 1)按著「右下」。"); bleGamepad.press(BUTTON_5); bleGamepad.press(BUTTON_16); bleGamepad.setAxes(32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, DPAD_DOWN_RIGHT); // 所有搖桿軸、滑桿、帽子開關…等,都能單獨設置,請參閱IndividualAxes.ino範例檔。 delay(500); Serial.println("放開按鍵5,所有搖桿軸都推向最小值,十字鍵(hat 1)回到中間(未按下)狀態"); bleGamepad.release(BUTTON_5); bleGamepad.setAxes(-32767, -32767, -32767, -32767, -32767, -32767, -32767, -32767, DPAD_CENTERED); delay(500); } }
此程式庫的原始碼說明頁面底下有提到,這個程式庫乃基於T-vK 編寫的ESP32-BLE-Mouse 以及chegewara編寫的這個ESP32 Windows 10無線藍牙滑鼠程式碼。
開啟 “ESP32-BLE-Gamepad”程式庫的資料夾(預設安裝路徑:“文件\ Arduino\libraries\ESP32-BLE-Gamepad”),可以看到它有4個程式檔:
其中的BleConnectionStatus.cpp(BLE連線狀態)檔源自ESP32-BLE-Mouse程式庫,這個程式檔定義了兩個方法,都需要傳入BLEServer類型物件:
- onConnect():藍牙連線之後啟動通知。
- onDisconnect():藍牙斷線之後停止通知。
底下是onDisconnect()方法的原始碼,關於0x2902特徵UUID的說明,請參閱《超圖解ESP32深度實作》第15-29和15-34頁說明。
void BleConnectionStatus::onDisconnect(BLEServer* pServer) { this->connected = false; BLE2902* desc = (BLE2902*)this->inputGamepad->getDescriptorByUUID(BLEUUID((uint16_t)0x2902)); desc->setNotifications(false); }
自製ESP32藍牙BLE遊戲手把
筆者基於“lemmingDev”編寫的“ESP32-BLE-Gamepad”程式庫,以及Jason Harley編寫的Leonardo-Switch-Controller程式庫,改寫了一個可用於電腦和Android手機的ESP32藍牙遊戲手把程式庫,可點擊此連結下載原始檔。這個程式庫的方法名稱沿用Leonardo-Switch-Controller程式庫的定義(參閱「自製Switch Pro相容遊戲控制器(三):Joystick程式庫的類別方法說明」)。
HID報告描述器的內容也沿用任天堂Switch Pro控制器的設置,相關說明請參閱「Gamepad手把的HID Report Descriptor(報告描述器)格式說明」。
這個例子的遊戲控制器採用現成的Wii經典手把,你可以改用按鍵開關和類比搖桿模組自行組裝遊戲手把。Wii經典手把採用I2C通訊,連接ESP32的預設I2C接腳和3.3V,麵包板示範接線:
採用自製的ESP32藍牙BLE遊戲控制器程式庫的Arduino範例程式,NintendoExtensionCtrl.h程式庫需要額外安裝,請參閱第一篇文章說明。
#include <esp32blegamepad.h> // 自製的ESP32藍牙BLE遊戲控制器程式庫 #include <nintendoextensionctrl.h> // 讀取Wii控制器的程式庫 BleGamepad bleGamepad; ClassicController classic; void setup() { Serial.begin(115200); bleGamepad.begin(false); classic.begin(); while (!classic.connect()) { Serial.println("連上Wii經典控制器,開始啟動藍牙…"); delay(1000); } } // 左右類比搖桿的數值範圍:0~255 int rightStick(uint8_t input) { if (input > 24) return 255; else if (input > 20) return 127; else if (input < 8) return 0; else if (input < 12) return 64; return 127; } int leftStick(uint8_t input) { if (input > 45) return 255; else if (input > 35) return 191; else if (input < 15) return 0; else if (input < 25) return 64; return 127; } void loop() { boolean success = classic.update(); // 讀取Wii經典手把的資料 if (!success) { Serial.println("經典控制器斷線了~重新連線…"); classic.reconnect(); delay(100); } else { classic.printDebug(); // 顯示經典手把的狀態 // 讀取Wii經典手把的所有按鍵的狀態 bleGamepad.setButton(2, classic.buttonA()); bleGamepad.setButton(1, classic.buttonB()); bleGamepad.setButton(3, classic.buttonX()); bleGamepad.setButton(0, classic.buttonY()); bleGamepad.setButton(4, classic.buttonL()); bleGamepad.setButton(5, classic.buttonR()); bleGamepad.setButton(6, classic.buttonZL()); bleGamepad.setButton(7, classic.buttonZR()); bleGamepad.setButton(9, classic.buttonPlus()); bleGamepad.setButton(8, classic.buttonMinus()); bleGamepad.setButton(12, classic.buttonHome()); // 讀取左右類比軸 bleGamepad.setXAxis(leftStick(classic.leftJoyX())); bleGamepad.setYAxis(255-leftStick(classic.leftJoyY())); // 反相Y軸的值 bleGamepad.setZAxis(rightStick(classic.rightJoyX())); bleGamepad.setZAxisRotation(255-rightStick(classic.rightJoyY())); // 讀取十字鍵 if (classic.dpadUp()) { if (classic.dpadLeft()) bleGamepad.setHatSwitch(7); else if (classic.dpadRight()) bleGamepad.setHatSwitch(1); else bleGamepad.setHatSwitch(0); } else if (classic.dpadDown()) { if (classic.dpadLeft()) bleGamepad.setHatSwitch(5); else if (classic.dpadRight()) bleGamepad.setHatSwitch(3); else bleGamepad.setHatSwitch(4); } else if (classic.dpadRight()) bleGamepad.setHatSwitch(2); else if (classic.dpadLeft()) bleGamepad.setHatSwitch(6); else bleGamepad.setHatSwitch(-1); } // 送出經典手把的HID報告 bleGamepad.sendState(); delay(1); }
編譯程式後上傳到連接Wii經典控制器的ESP32開發板,從電腦系統的控制台搜尋並連結到ESP32 BLE Gamepad裝置:
接著開啟瀏覽器連結到Gamepad Tester(遊戲控制器測試網頁),即可測試遊戲控制器的各個按鍵和搖桿:
ESP32藍牙BLE遊戲控制器程式庫的原始碼
底下是ESP32BleGamepad.cpp的原始碼,程式透過一個FreeRTOS多工任務建立並廣播HID裝置資訊:
#include <ESP32BleGamepad.h> #include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> #include <BLEHIDDevice.h> #include <HIDTypes.h> #define GAMEPAD_REPORT_ID 0x03 #define GAMEPAD_STATE_SIZE 7 // 報告描述器資料 static const uint8_t _hidReportDescriptor[] = { // 遊戲控制器(Gamepad) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x05, // USAGE (Gamepad) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x03, // REPORT_ID (3) // 14個按鍵 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (範圍最小值:1) 0x29, 0x0E, // USAGE_MAXIMUM (範圍最大值:14) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x0E, // REPORT_COUNT (14個按鍵) 0x81, 0x02, // INPUT (Data,Var,Abs) // 補上兩個空白 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x02, // REPORT_SIZE (2個位元) 0x81, 0x01, // INPUT (保留) // 8方向十字鍵(HAT) 0x05, 0x01, // USAGE_PAGE (General Desktop),通用桌上型 0x09, 0x39, // USAGE (Hat Switch),帽子開關(十字鍵) 0x15, 0x00, // LOGICAL_MINIMUM (0),邏輯最小值 0x25, 0x07, // LOGICAL_MAXIMUM (7),邏輯最大值 0x35, 0x00, // PHYSICAL_MINIMUM (0),實體最小值 0x46, 0x3B, 0x01, // PHYSICAL_MAXIMUM (315) ,實體最大值 0x65, 0x14, // UNIT (Eng Rot:Angular Pos),單位:英制角度 0x75, 0x04, // REPORT_SIZE (4),佔4個位元 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs),絕對可變資料 // 填補4個空白位元 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x04, // REPORT_SIZE (4),共4個位元 0x81, 0x01, // INPUT // X, Y和Z軸 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8) 0x09, 0x01, // USAGE (Pointer),游標 0xA1, 0x00, // COLLECTION (Physical),游標的座標資料集合 0x09, 0x30, // USAGE (x) 0x09, 0x31, // USAGE (y) 0x09, 0x32, // USAGE (z) 0x09, 0x35, // USAGE (rz) 0x95, 0x04, // REPORT_COUNT (4) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION 0xc0 // END_COLLECTION }; BleGamepad::BleGamepad(std::string deviceName, std::string deviceManufacturer, uint8_t batteryLevel) { // 初始化類比搖桿、十字鍵和按鍵的狀態 xAxis = 0; yAxis = 0; zAxis = 0; zAxisRotation = 0; buttons = 0; hatSwitch = -1; this->deviceName = deviceName; // 裝置名稱字串 this->deviceManufacturer = deviceManufacturer; // 製造商名稱字串 this->batteryLevel = batteryLevel; // 電量準位 this->connectionStatus = new BleConnectionStatus(); // 連線狀態物件 } void BleGamepad::begin(bool initAutoSendState) { autoSendState = initAutoSendState; // 建立FreeRTOS任務 xTaskCreate(this->taskServer, "server", 20000, (void *)this, 5, NULL); } // FreeRTOS任務函式 void BleGamepad::taskServer(void *pvParameter) { // 從任務參數取得遊戲控制器物件(BleGamepadInstance) BleGamepad *BleGamepadInstance = (BleGamepad *)pvParameter; // 設定裝置名稱 BLEDevice::init(BleGamepadInstance->deviceName); // 建立藍牙裝置伺服器 BLEServer *pServer = BLEDevice::createServer(); // 設定連線狀態回呼 pServer->setCallbacks(BleGamepadInstance->connectionStatus); // 建立HID(人機介面裝置)物件 BleGamepadInstance->hid = new BLEHIDDevice(pServer); // 設定「輸入」類型裝置的報告ID BleGamepadInstance->inputGamepad = BleGamepadInstance->hid->inputReport(GAMEPAD_REPORT_ID); // 替連線狀態設定偵聽的對象 BleGamepadInstance->connectionStatus->inputGamepad = BleGamepadInstance->inputGamepad; // 設定製造商資訊(UTF-8編碼字串的自訂廠商名稱) BleGamepadInstance->hid->manufacturer()->setValue(BleGamepadInstance->deviceManufacturer); // 設定裝置的PID和VID,參閱下文說明。 BleGamepadInstance->hid->pnp(0x02, 0x7e05, 0x0920, 0x0110); BleGamepadInstance->hid->hidInfo(0x00, 0x01); // 設定連線認證模式,“ESP_LE_AUTH_BOND”代表「綁定」, // 也就是儲存長期密鑰,日後連線不必再配對, // 相關說明請參閱《超圖解ESP32深度實作》15-17頁。 BLESecurity *pSecurity = new BLESecurity(); pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND); // 設定HID報告描述器 BleGamepadInstance->hid->reportMap((uint8_t *)_hidReportDescriptor, sizeof(_hidReportDescriptor)); // 啟動服務 BleGamepadInstance->hid->startServices(); // 設定啟動服務的回呼處理函式 BleGamepadInstance->onStarted(pServer); // 設定廣播訊息(廣告封包) BLEAdvertising *pAdvertising = pServer->getAdvertising(); // 設定HID裝置的圖示 pAdvertising->setAppearance(HID_GAMEPAD); pAdvertising->addServiceUUID(BleGamepadInstance->hid->hidService()->getUUID()); // 開始廣播 pAdvertising->start(); // 設定電量準位 BleGamepadInstance->hid->setBatteryLevel(BleGamepadInstance->batteryLevel); // 無限期等待 vTaskDelay(portMAX_DELAY); } void BleGamepad::end() { } // 設定按鍵 void BleGamepad::setButton(uint8_t button, uint8_t value) { if (value == 0) { releaseButton(button); } else { pressButton(button); } } // 按下按鍵 void BleGamepad::pressButton(uint8_t button) { bitSet(buttons, button); if (autoSendState) sendState(); } // 放開按鍵 void BleGamepad::releaseButton(uint8_t button) { bitClear(buttons, button); if (autoSendState) sendState(); } // 設定類比搖桿X軸(左搖桿X) void BleGamepad::setXAxis(uint8_t value) { xAxis = value; if (autoSendState) sendState(); } // 設定類比搖桿Y軸(左搖桿Y) void BleGamepad::setYAxis(uint8_t value) { yAxis = value; if (autoSendState) sendState(); } // 右搖桿X void BleGamepad::setZAxis(uint8_t value) { zAxis = value; if (autoSendState) sendState(); } // 右搖桿Y void BleGamepad::setZAxisRotation(uint8_t value) { zAxisRotation = value; if (autoSendState) sendState(); } // 設定十字鍵 void BleGamepad::setHatSwitch(int16_t value) { hatSwitch = value; if (autoSendState) sendState(); } // 傳送HID報告 void BleGamepad::sendState() { if (this->isConnected()) { uint8_t data[GAMEPAD_STATE_SIZE]; uint32_t buttonTmp = buttons; // 把16位元拆分成2位元組 data[0] = buttonTmp & 0xFF; buttonTmp >>= 8; data[1] = buttonTmp & 0xFF; if (hatSwitch < 0) hatSwitch = 8; // 把十字鍵按鍵狀態包裝成一個位元組 data[2] = (B00001111 & hatSwitch); data[3] = xAxis; data[4] = yAxis; data[5] = zAxis; data[6] = zAxisRotation; this->inputGamepad->setValue(data, GAMEPAD_STATE_SIZE); this->inputGamepad->notify(); } } // 確認藍牙的連線狀態 bool BleGamepad::isConnected() { return this->connectionStatus->connected; } // 設定電量準位 void BleGamepad::setBatteryLevel(uint8_t level) { this->batteryLevel = level; if (hid != 0) this->hid->setBatteryLevel(this->batteryLevel); }
FreeRTOS的taskServer任務函式,和ESP32-BLE-Mouse的同名任務函式相比,主要的差別在人機介面裝置的Report ID(報告ID)不同,滑鼠是0x02,遊戲控制器(Gamepad)是0x03。
USB裝置的VID, PID和DID
電腦系統透過HID裝置的Vender ID(廠商識別碼,縮寫VID)以及Product ID(產品識別碼,縮寫PID)來辨識裝置,進而安裝適合的驅動程式。在Windows 10系統的「裝置管理員」的「人性化介面裝置」分類,列舉所有連接這台電腦的HID裝置。以Xbox One控制器為例,除了列在「人性化介面裝置」,還會單獨列舉在「Xbox週邊設備」裡面;在任一HID裝置名稱上按滑鼠右鍵,選擇「內容」:
切換到「詳細資料」分頁,從「屬性」選單選擇「硬體識別碼」,即可見到該裝置的VID和PID碼,以這個Xbox One控制器為例,VID是0x045E(代表微軟公司)、PID是0x02EA(代表Xbox One S控制器)。
VID由USB機構統籌編制,根據USB機構的“Getting a Vendor ID”頁面指出,取得VID的方式有兩種:支付年費美金$5,000加入會員,或者繳納美金$3,500元取得為期兩年的非USB開發者論壇(USB Implementers Forum,縮寫為USB-IF)會員商標授權。
實驗產品或者自己DIY的東西當然不用取得VID,可採用現有廠商的識別碼。devicehunt.com的這個網頁以及linux-usb.org的這個文件,列舉了所有USB製造商的VID以及產品的ID;這些網頁用Device ID(裝置ID,縮寫DID)來稱呼PID,但「裝置ID」通常是指VID和PID組成的裝置唯一識別碼。
ESP32 BLEHIDDevice類別的方法
ESP32 BLE人機介面裝置透過BLEHIDDevice類別設定裝置的資訊,底下列舉本文用到的一些方法:
- BLECharacteristic* inputReport(uint8_t reportID):設定「輸入報告」的報告ID,此例為3。
- void reportMap(uint8_t* map, uint16_t):設定HID報告描述器。
- void startServices():啟動服務
- void manufacturer(std::string name):設定製造商名稱字串
- void setBatteryLevel(uint8_t level):設定電量準位。
- void pnp(uint8_t sig, uint16_t vid, uint16_t pid, uint16_t version):設定裝置的VID和PID。
- void hidInfo(uint8_t country, uint8_t flags):設定此裝置的地區以及是否可喚醒主機等資訊。
其中,hidInfo()方法的第1個參數是地區碼,但HID裝置通常跟使用地區無關,所以第1個參數設成0;第2個參數的第1個位元指出裝置是否具備喚醒主機的功能,第2個位元則指出裝置是否為可直接連線(normally connectable),也就是通電之後,無需人為操作即可與主機連線。
設定裝置VID和PID的pnp()方法有4個參數:
第1個參數的可能值為1或2:
- 1:代表由藍牙技術聯盟(Bluetooth Special Interest Group,縮寫為Bluetooth SIG)核發的公司ID。
- 2:代表由USB開發者論壇(USB-IF)核發的製造商ID(VID)。
第2個參數是VID(製造商ID),由上述兩個單位核發。
第3個參數是PID(產品ID),由廠商自行指定。
第4個參數是版本編號,由廠商自行指定。
本文的藍牙BLE遊戲手把程式庫的hidinfo()和pnp()方法敘述沿用“ESP32-BLE-Gamepad”程式庫的設定;“BleGamepadInstance”是BleGamepad類別物件,hid則是BLEHIDDevice類別物件:
BleGamepadInstance->hid->pnp(0x02, 0x7e05, 0x0920, 0x0110); BleGamepadInstance->hid->hidInfo(0x00, 0x01);
使用 ESP32-BLE-Gamepad 可连接 Mac, 使用 Gamepad Tester 可测试识别, 使用博主修改后的代码连接 Switch 后无法被识别, 亦无法连接 Switch。是否有缺失环节?
对,主因是Switch控制器采用自家定义的,简称”NWCP”的通讯协议,相关说明请参阅switchbrew.org的joycon维基网页。
另外,也请参阅这个拆解Switch控制器的反向工程文档。
以及Switch适用的ESP32 GameCube蓝牙控制器项目。