自製Switch Pro相容遊戲控制器(四):ESP32 BLE藍牙低功耗遊戲手把

本文旨在補充《超圖解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個程式檔:

“ESP32-BLE-Gamepad”程式庫的資料夾

其中的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程式庫的類別方法說明」)。

自製ESP32藍牙BLE遊戲手把的資料夾

HID報告描述器的內容也沿用任天堂Switch Pro控制器的設置,相關說明請參閱「Gamepad手把的HID Report Descriptor(報告描述器)格式說明」。

這個例子的遊戲控制器採用現成的Wii經典手把,你可以改用按鍵開關和類比搖桿模組自行組裝遊戲手把。Wii經典手把採用I2C通訊,連接ESP32的預設I2C接腳和3.3V,麵包板示範接線:

ESP32 BLE遊戲手把示範接線

採用自製的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裝置:

Windows 10藍芽

接著開啟瀏覽器連結到Gamepad Tester(遊戲控制器測試網頁),即可測試遊戲控制器的各個按鍵和搖桿:

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裝置名稱上按滑鼠右鍵,選擇「內容」:

indows 10的裝置管理員

切換到「詳細資料」分頁,從「屬性」選單選擇「硬體識別碼」,即可見到該裝置的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組成的裝置唯一識別碼。

devicehunt.com

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:

第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);
Posts created 470

2 thoughts on “自製Switch Pro相容遊戲控制器(四):ESP32 BLE藍牙低功耗遊戲手把

  1. 使用 ESP32-BLE-Gamepad 可连接 Mac, 使用 Gamepad Tester 可测试识别, 使用博主修改后的代码连接 Switch 后无法被识别, 亦无法连接 Switch。是否有缺失环节?

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top