NimBLE Keyboard Sample - palette-system/az-macro GitHub Wiki

NimBLEになってからセキュリティの設定の仕方が変わったらしく下記が必要だったのと

NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND);


それだけではダメだったので おそらく NimBLEHIDDevice クラスと NimBLESecurity クラスの中の動きが少し変わったのではないかという結論です。

こちらのソースを参考に、ESP32-NimBLE-KeyboardのhidReportDescriptorで動かせるようにコードを変更しました。

参考にしたソース: https://gist.github.com/jins-tkomoda/dbc3d02198780a8b2ee2e2f04ba3c92b


こちらがiOSでもペアリング出来て、10秒おきに a を押すだけのサンプルソースです。
ただゴリゴリBLEのコードベタ書きなので参考程度にご参照下さい。

/**
 * NimBLE-Arduino HID Mouse
 */
// 参考 : https://gist.github.com/jins-tkomoda/dbc3d02198780a8b2ee2e2f04ba3c92b
#include <NimBLEDevice.h>
#include "HIDKeyboardTypes.h"
#include "HIDTypes.h"

// Appearance
#define GENERIC_HID		0x03C0
#define HID_KEYBOARD	   0x03C1
#define HID_MOUSE		  0x03C2
#define HID_JOYSTICK	   0x03C3
#define HID_GAMEPAD		0x03C4
#define HID_TABLET		 0x03C5
#define HID_CARD_READER	0x03C6
#define HID_DIGITAL_PEN	0x03C7
#define HID_BARCODE		0x03C8

/*
 * Keyboard
 */
#define KEYBOARD_ID 0x01
#define MEDIA_KEYS_ID 0x02

const uint8_t reportMap[] = {
  USAGE_PAGE(1),      0x01,          // USAGE_PAGE (Generic Desktop Ctrls)
  USAGE(1),           0x06,          // USAGE (Keyboard)
  COLLECTION(1),      0x01,          // COLLECTION (Application)
  // ------------------------------------------------- Keyboard
  REPORT_ID(1),       KEYBOARD_ID,   //   REPORT_ID (1)
  USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  USAGE_MINIMUM(1),   0xE0,          //   USAGE_MINIMUM (0xE0)
  USAGE_MAXIMUM(1),   0xE7,          //   USAGE_MAXIMUM (0xE7)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   Logical Maximum (1)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  REPORT_COUNT(1),    0x08,          //   REPORT_COUNT (8)
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 1 byte (Reserved)
  REPORT_SIZE(1),     0x08,          //   REPORT_SIZE (8)
  HIDINPUT(1),        0x01,          //   INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  REPORT_COUNT(1),    0x05,          //   REPORT_COUNT (5) ; 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  USAGE_PAGE(1),      0x08,          //   USAGE_PAGE (LEDs)
  USAGE_MINIMUM(1),   0x01,          //   USAGE_MINIMUM (0x01) ; Num Lock
  USAGE_MAXIMUM(1),   0x05,          //   USAGE_MAXIMUM (0x05) ; Kana
  HIDOUTPUT(1),       0x02,          //   OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 3 bits (Padding)
  REPORT_SIZE(1),     0x03,          //   REPORT_SIZE (3)
  HIDOUTPUT(1),       0x01,          //   OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  REPORT_COUNT(1),    0x06,          //   REPORT_COUNT (6) ; 6 bytes (Keys)
  REPORT_SIZE(1),     0x08,          //   REPORT_SIZE(8)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM(0)
  LOGICAL_MAXIMUM(1), 0x65,          //   LOGICAL_MAXIMUM(0x65) ; 101 keys
  USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  USAGE_MINIMUM(1),   0x00,          //   USAGE_MINIMUM (0)
  USAGE_MAXIMUM(1),   0x65,          //   USAGE_MAXIMUM (0x65)
  HIDINPUT(1),        0x00,          //   INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  END_COLLECTION(0),                 // END_COLLECTION
  // ------------------------------------------------- Media Keys
  USAGE_PAGE(1),      0x0C,          // USAGE_PAGE (Consumer)
  USAGE(1),           0x01,          // USAGE (Consumer Control)
  COLLECTION(1),      0x01,          // COLLECTION (Application)
  REPORT_ID(1),       MEDIA_KEYS_ID, //   REPORT_ID (3)
  USAGE_PAGE(1),      0x0C,          //   USAGE_PAGE (Consumer)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   LOGICAL_MAXIMUM (1)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  REPORT_COUNT(1),    0x10,          //   REPORT_COUNT (16)
  USAGE(1),           0xB5,          //   USAGE (Scan Next Track)     ; bit 0: 1
  USAGE(1),           0xB6,          //   USAGE (Scan Previous Track) ; bit 1: 2
  USAGE(1),           0xB7,          //   USAGE (Stop)                ; bit 2: 4
  USAGE(1),           0xCD,          //   USAGE (Play/Pause)          ; bit 3: 8
  USAGE(1),           0xE2,          //   USAGE (Mute)                ; bit 4: 16
  USAGE(1),           0xE9,          //   USAGE (Volume Increment)    ; bit 5: 32
  USAGE(1),           0xEA,          //   USAGE (Volume Decrement)    ; bit 6: 64
  USAGE(2),           0x23, 0x02,    //   Usage (WWW Home)            ; bit 7: 128
  USAGE(2),           0x94, 0x01,    //   Usage (My Computer) ; bit 0: 1
  USAGE(2),           0x92, 0x01,    //   Usage (Calculator)  ; bit 1: 2
  USAGE(2),           0x2A, 0x02,    //   Usage (WWW fav)     ; bit 2: 4
  USAGE(2),           0x21, 0x02,    //   Usage (WWW search)  ; bit 3: 8
  USAGE(2),           0x26, 0x02,    //   Usage (WWW stop)    ; bit 4: 16
  USAGE(2),           0x24, 0x02,    //   Usage (WWW back)    ; bit 5: 32
  USAGE(2),           0x83, 0x01,    //   Usage (Media sel)   ; bit 6: 64
  USAGE(2),           0x8A, 0x01,    //   Usage (Mail)        ; bit 7: 128
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  END_COLLECTION(0)                  // END_COLLECTION
};

static NimBLEServer* pServer;

/**  None of these are required as they will be handled by the library with defaults. **
 **                       Remove as you see fit for your needs                        */  
class ServerCallbacks: public NimBLEServerCallbacks {
    void onConnect(NimBLEServer* pServer) {
        Serial.println("Client connected");
        Serial.println("Multi-connect support: start advertising");
        NimBLEDevice::startAdvertising();
    };
    /** Alternative onConnect() method to extract details of the connection. 
     *  See: src/ble_gap.h for the details of the ble_gap_conn_desc struct.
     */  
    void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) {
        Serial.print("Client address: ");
        Serial.println(NimBLEAddress(desc->peer_ota_addr).toString().c_str());
        /** We can use the connection handle here to ask for different connection parameters.
         *  Args: connection handle, min connection interval, max connection interval
         *  latency, supervision timeout.
         *  Units; Min/Max Intervals: 1.25 millisecond increments.
         *  Latency: number of intervals allowed to skip.
         *  Timeout: 10 millisecond increments, try for 5x interval time for best results.  
         */
        pServer->updateConnParams(desc->conn_handle, 0x10, 0x20, 0, 600);
    };
    void onDisconnect(NimBLEServer* pServer) {
        Serial.println("Client disconnected - start advertising");
        NimBLEDevice::startAdvertising();
    };
    
/********************* Security handled here **********************
****** Note: these are the same return values as defaults ********/
    uint32_t onPassKeyRequest(){
        Serial.println("Server Passkey Request");
        /** This should return a random 6 digit number for security 
         *  or make your own static passkey as done here.
         */
        return 123456; 
    };

    bool onConfirmPIN(uint32_t pass_key){
        Serial.print("The passkey YES/NO number: ");Serial.println(pass_key);
        /** Return false if passkeys don't match. */
        return true; 
    };

    void onAuthenticationComplete(ble_gap_conn_desc* desc){
        /** Check that encryption was successful, if not we disconnect the client */  
        if(!desc->sec_state.encrypted) {
            NimBLEDevice::getServer()->disconnect(desc->conn_handle);
            Serial.println("Encrypt connection failed - disconnecting client");
            return;
        }
        Serial.println("Starting BLE work!");
    };
};

/** Handler class for characteristic actions */
class CharacteristicCallbacks: public NimBLECharacteristicCallbacks {
    void onRead(NimBLECharacteristic* pCharacteristic){
        Serial.print(pCharacteristic->getUUID().toString().c_str());
        Serial.print(": onRead(), value: ");
        Serial.println(pCharacteristic->getValue().c_str());
    };

    void onWrite(NimBLECharacteristic* pCharacteristic) {
        Serial.print(pCharacteristic->getUUID().toString().c_str());
        Serial.print(": onWrite(), value: ");
        Serial.println(pCharacteristic->getValue().c_str());
    };
    /** Called before notification or indication is sent, 
     *  the value can be changed here before sending if desired.
     */
    void onNotify(NimBLECharacteristic* pCharacteristic) {
        Serial.println("Sending notification to clients");
    };
    
    /** The status returned in status is defined in NimBLECharacteristic.h.
     *  The value returned in code is the NimBLE host return code.
     */
    void onStatus(NimBLECharacteristic* pCharacteristic, Status status, int code) {
        String str = ("Notf/Ind stscode: ");
        str += status;
        str += ", retcode: ";
        str += code;
        str += ", "; 
        str += NimBLEUtils::returnCodeToString(code);
        //Serial.print(str);
    };

    void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) {
        String str = "Client ID: ";
        str += desc->conn_handle;
        str += " Address: ";
        str += std::string(NimBLEAddress(desc->peer_ota_addr)).c_str();
        if(subValue == 0) {
            str += " Unsubscribed to ";
        }else if(subValue == 1) {
            str += " Subscribed to notfications for ";
        } else if(subValue == 2) {
            str += " Subscribed to indications for ";
        } else if(subValue == 3) {
            str += " Subscribed to notifications and indications for ";
        }
        str += std::string(pCharacteristic->getUUID()).c_str();

        Serial.println(str);
    };
};
    
/** Handler class for descriptor actions */    
class DescriptorCallbacks : public NimBLEDescriptorCallbacks {
    void onWrite(NimBLEDescriptor* pDescriptor) {
        std::string dscVal((char*)pDescriptor->getValue(), pDescriptor->getLength());
        Serial.print("Descriptor witten value:");
        Serial.println(dscVal.c_str());
    };

    void onRead(NimBLEDescriptor* pDescriptor) {
        Serial.print(pDescriptor->getUUID().toString().c_str());
        Serial.println(" Descriptor read");
    };
};



/** Define callback instances globally to use for multiple Charateristics \ Descriptors */ 
static DescriptorCallbacks dscCallbacks;
static CharacteristicCallbacks chrCallbacks;

NimBLECharacteristic* pInputCharacteristic;

void setup() {
    Serial.begin(115200);
    Serial.println("Starting NimBLE HID Server");

    /** sets device name */
    NimBLEDevice::init("NimBLE-Hid");
    NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
    NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND);

    pServer = NimBLEDevice::createServer();
    pServer->setCallbacks(new ServerCallbacks());

    //DeviceInfoService
    NimBLEService* pDeviceInfoService = pServer->createService("180A"); // <-デバイスインフォのUUID
    NimBLECharacteristic* pPnpCharacteristic = pDeviceInfoService->createCharacteristic("2A50", NIMBLE_PROPERTY::READ);
    uint8_t sig = 0x02;
    uint16_t vid = 0xe502;
    uint16_t pid = 0xa111;
    uint16_t version = 0x0210; 
    uint8_t pnp[] = { sig, (uint8_t) (vid >> 8), (uint8_t) vid, (uint8_t) (pid >> 8), (uint8_t) pid, (uint8_t) (version >> 8), (uint8_t) version };
    pPnpCharacteristic->setValue(pnp, sizeof(pnp));
    pPnpCharacteristic->setCallbacks(&chrCallbacks);

    //DeviceInfoService-Manufacturer
    NimBLECharacteristic* pManufacturerCharacteristic = pDeviceInfoService->createCharacteristic("2A29", NIMBLE_PROPERTY::READ); // 0x2a29 = メーカー名
    pManufacturerCharacteristic->setValue("NimBLE Community");
    pManufacturerCharacteristic->setCallbacks(&chrCallbacks);

    //HidService
    NimBLEService* pHidService = pServer->createService(NimBLEUUID("1812"), 40);

    //HidService-hidInfo
    NimBLECharacteristic* pHidInfoCharacteristic = pHidService->createCharacteristic("2A4A", NIMBLE_PROPERTY::READ);// HID Information 会社名?とか?あと何かのフラグ
    uint8_t country = 0x00;
    uint8_t flags = 0x01;
    uint8_t info[] = { 0x11, 0x1, country, flags };                                              
    pHidInfoCharacteristic->setValue(info, sizeof(info));
    pHidInfoCharacteristic->setCallbacks(&chrCallbacks);

    //HidService-reportMap
    NimBLECharacteristic* pReportMapCharacteristic = pHidService->createCharacteristic("2A4B",NIMBLE_PROPERTY::READ); // HID Report Map (ここでHIDのいろいろ設定
    pReportMapCharacteristic->setValue((uint8_t*)reportMap, sizeof(reportMap));
    pReportMapCharacteristic->setCallbacks(&chrCallbacks);

    //HidService-HidControl
    NimBLECharacteristic* pHidControlCharacteristic = pHidService->createCharacteristic("2A4C", NIMBLE_PROPERTY::WRITE_NR);// HID Control Point
    pHidControlCharacteristic->setCallbacks(&chrCallbacks);

    //HidService-protocolMode
    NimBLECharacteristic* pProtocolModeCharacteristic = pHidService->createCharacteristic("2A4E",NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ); // Protocol Mode
    const uint8_t pMode[] = { 0x01 }; // 0: Boot Protocol 1: Rport Protocol
    pProtocolModeCharacteristic->setValue((uint8_t*) pMode, 1);
    pProtocolModeCharacteristic->setCallbacks(&chrCallbacks);

    //HidService-input
    pInputCharacteristic = pHidService->createCharacteristic("2A4D", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC); // Report
    NimBLEDescriptor* pC01Ddsc = pInputCharacteristic->createDescriptor( "2908", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20); // Report Reference
    uint8_t desc1_val[] = { 0x01, 0x01 }; // Report ID 1 を Input に設定
    pC01Ddsc->setValue((uint8_t*) desc1_val, 2);
    pC01Ddsc->setCallbacks(&dscCallbacks);

    //HidService-input2 media port
    NimBLECharacteristic* pInputCharacteristic2 = pHidService->createCharacteristic("2A4D", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC); // Report
    NimBLEDescriptor* pC01Ddsc2 = pInputCharacteristic2->createDescriptor("2908", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
    uint8_t desc1_val2[] = { 0x02, 0x01 }; // Report ID 2 を Input に設定
    pC01Ddsc2->setValue((uint8_t*) desc1_val2, 2);
    pC01Ddsc2->setCallbacks(&dscCallbacks);

    // HidService-output
    NimBLECharacteristic* pOutputCharacteristic = pHidService->createCharacteristic("2A4D", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);
    pOutputCharacteristic->setCallbacks(&chrCallbacks);
    NimBLEDescriptor* pDesc3 = pOutputCharacteristic->createDescriptor("2908", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
    uint8_t desc1_val3[] = { 0x01, 0x02}; // Report ID 1 を Output に設定
    pDesc3->setValue((uint8_t*) desc1_val3, 2);

    //BatteryService
    NimBLEService* pBatteryService = pServer->createService("180F");
    NimBLECharacteristic* pBatteryLevelCharacteristic = pBatteryService->createCharacteristic("2A19", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
    NimBLE2904* pBatteryLevelDescriptor = (NimBLE2904*)pBatteryLevelCharacteristic->createDescriptor("2904"); 
    pBatteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UTF8);
    pBatteryLevelDescriptor->setNamespace(1);
    pBatteryLevelDescriptor->setUnit(0x27ad);
    pBatteryLevelDescriptor->setCallbacks(&dscCallbacks);

    /** Start the services when finished creating all Characteristics and Descriptors */
    pDeviceInfoService->start();
    pHidService->start();
    pBatteryService->start();

    NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
    pAdvertising->setAppearance(HID_KEYBOARD); //HID_KEYBOARD / HID_MOUSE
    pAdvertising->addServiceUUID(pHidService->getUUID());

    pAdvertising->setScanResponse(true);
    pAdvertising->start();

    Serial.println("Advertising Started");
}

void loop() {
  /** Do your thing here, this just spams notifications to all connected clients */
    if(pServer->getConnectedCount()) {
                    uint8_t a[] = {0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00};
                    pInputCharacteristic->setValue(a, sizeof(a));
                    pInputCharacteristic->notify();
                    delay(200);
                    a[2] = 0x00;
                    pInputCharacteristic->setValue(a, sizeof(a));
                    pInputCharacteristic->notify();
                    delay(10000);
    }
    delay(2000);
    
}
⚠️ **GitHub.com Fallback** ⚠️