本文旨在補充《超圖解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蓝牙控制器项目。