1443浏览
查看: 1443|回复: 0

[ESP8266/ESP32] FireBeetle 读取蓝牙键盘输入

[复制链接]
Arduino 可以使用键盘作为输入设备,最常见的是下面2种接口的键盘:
1.     PS2 接口的键盘。缺点是这种接口键盘市面上非常少见
FireBeetle 读取蓝牙键盘输入图1
2.     USB 键盘。这种键盘非常常见,为了在 Arduino 上使用,可以使用 USB Host Shield,缺点是占用SPI接口和多个GPIO;或者使用 CH9325 这种USB转串口的芯片,这个方案的缺点是可能存在兼容性问题。
这次介绍的是ESP32Arduino 直接读取蓝牙键盘的输入。特别需要注意的是蓝牙键盘有两种,Classical BLE。我测试过罗技的 K480 Classical蓝牙键盘:
FireBeetle 读取蓝牙键盘输入图2
还有苹果的A2449键盘,同样也是Classical键盘。
FireBeetle 读取蓝牙键盘输入图3

IDF 提供了一个读取 Classical 键盘输入的示例,但是经过我的测试该代码无法正常工作。
这次介绍的代码只适用于 BLE 键盘,我入手的是雷柏 x220t 键鼠套装。这个键盘支持三种模式:2.4GClassical蓝牙和 BLE 蓝牙。
FireBeetle 读取蓝牙键盘输入图4

  1. /** NimBLE_Server Demo:
  2. *
  3. *  Demonstrates many of the available features of the NimBLE client library.
  4. *
  5. *  Created: on March 24 2020
  6. *      Author: H2zero
  7. *
  8. */
  9. /*
  10. * This program is based on https://github.com/h2zero/NimBLE-Arduino/tree/master/examples/NimBLE_Client.
  11. * My changes are covered by the MIT license.
  12. */
  13. /*
  14. * MIT License
  15. *
  16. * Copyright (c) 2022 esp32beans@gmail.com
  17. *
  18. * Permission is hereby granted, free of charge, to any person obtaining a copy
  19. * of this software and associated documentation files (the "Software"), to deal
  20. * in the Software without restriction, including without limitation the rights
  21. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  22. * copies of the Software, and to permit persons to whom the Software is
  23. * furnished to do so, subject to the following conditions:
  24. *
  25. * The above copyright notice and this permission notice shall be included in all
  26. * copies or substantial portions of the Software.
  27. *
  28. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  29. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  30. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  31. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  32. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  33. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  34. * SOFTWARE.
  35. */
  36. // Install NimBLE-Arduino by h2zero using the IDE library manager.
  37. #include <NimBLEDevice.h>
  38. const char HID_SERVICE[] = "1812";
  39. const char HID_INFORMATION[] = "2A4A";
  40. const char HID_REPORT_MAP[] = "2A4B";
  41. const char HID_CONTROL_POINT[] = "2A4C";
  42. const char HID_REPORT_DATA[] = "2A4D";
  43. void scanEndedCB(NimBLEScanResults results);
  44. static NimBLEAdvertisedDevice* advDevice;
  45. static bool doConnect = false;
  46. static uint32_t scanTime = 0; /** 0 = scan forever */
  47. /**  None of these are required as they will be handled by the library with defaults. **
  48. **                       Remove as you see fit for your needs                        */
  49. class ClientCallbacks : public NimBLEClientCallbacks {
  50.   void onConnect(NimBLEClient* pClient) {
  51.     Serial.println("Connected");
  52.     /** After connection we should change the parameters if we don't need fast response times.
  53.      *  These settings are 150ms interval, 0 latency, 450ms timout.
  54.      *  Timeout should be a multiple of the interval, minimum is 100ms.
  55.      *  I find a multiple of 3-5 * the interval works best for quick response/reconnect.
  56.      *  Min interval: 120 * 1.25ms = 150, Max interval: 120 * 1.25ms = 150, 0 latency, 60 * 10ms = 600ms timeout
  57.      */
  58.     pClient->updateConnParams(120,120,0,60);
  59.   };
  60.   void onDisconnect(NimBLEClient* pClient) {
  61.     Serial.print(pClient->getPeerAddress().toString().c_str());
  62.     Serial.println(" Disconnected - Starting scan");
  63.     NimBLEDevice::getScan()->start(scanTime, scanEndedCB);
  64.   };
  65.   /** Called when the peripheral requests a change to the connection parameters.
  66.    *  Return true to accept and apply them or false to reject and keep
  67.    *  the currently used parameters. Default will return true.
  68.    */
  69.   bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) {
  70.     // Failing to accepts parameters may result in the remote device
  71.     // disconnecting.
  72.     return true;
  73.   };
  74.   /********************* Security handled here **********************
  75.    ****** Note: these are the same return values as defaults ********/
  76.   uint32_t onPassKeyRequest(){
  77.     Serial.println("Client Passkey Request");
  78.     /** return the passkey to send to the server */
  79.     return 123456;
  80.   };
  81.   bool onConfirmPIN(uint32_t pass_key){
  82.     Serial.print("The passkey YES/NO number: ");
  83.     Serial.println(pass_key);
  84.     /** Return false if passkeys don't match. */
  85.     return true;
  86.   };
  87.   /** Pairing process complete, we can check the results in ble_gap_conn_desc */
  88.   void onAuthenticationComplete(ble_gap_conn_desc* desc){
  89.     if(!desc->sec_state.encrypted) {
  90.       Serial.println("Encrypt connection failed - disconnecting");
  91.       /** Find the client with the connection handle provided in desc */
  92.       NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
  93.       return;
  94.     }
  95.   };
  96. };
  97. /** Define a class to handle the callbacks when advertisments are received */
  98. class AdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {
  99.   void onResult(NimBLEAdvertisedDevice* advertisedDevice) {
  100.     if ((advertisedDevice->getAdvType() == BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_HD)
  101.         || (advertisedDevice->getAdvType() == BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_LD)
  102.         || (advertisedDevice->haveServiceUUID() && advertisedDevice->isAdvertisingService(NimBLEUUID(HID_SERVICE))))
  103.     {
  104.       Serial.print("Advertised HID Device found: ");
  105.       Serial.println(advertisedDevice->toString().c_str());
  106.       /** stop scan before connecting */
  107.       NimBLEDevice::getScan()->stop();
  108.       /** Save the device reference in a global for the client to use*/
  109.       advDevice = advertisedDevice;
  110.       /** Ready to connect now */
  111.       doConnect = true;
  112.     }
  113.   };
  114. };
  115. /** Notification / Indication receiving handler callback */
  116. // Notification from 4c:75:25:xx:yy:zz: Service = 0x1812, Characteristic = 0x2a4d, Value = 1,0,0,0,0,
  117. void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){
  118.   std::string str = (isNotify == true) ? "Notification" : "Indication";
  119.   str += " from ";
  120.   /** NimBLEAddress and NimBLEUUID have std::string operators */
  121.   str += std::string(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress());
  122.   str += ": Service = " + std::string(pRemoteCharacteristic->getRemoteService()->getUUID());
  123.   str += ", Characteristic = " + std::string(pRemoteCharacteristic->getUUID());
  124.   str += ", Value = ";
  125.   Serial.print(str.c_str());
  126.   for (size_t i = 0; i < length; i++) {
  127.     Serial.print(pData[i], HEX);
  128.     Serial.print(',');
  129.   }
  130.   Serial.print(' ');
  131.   if (length == 6) {
  132.     // BLE Trackball Mouse from Amazon returns 6 bytes per HID report
  133.     Serial.printf("buttons: %02x, x: %d, y: %d, wheel: %d",
  134.         pData[0], *(int16_t *)&pData[1], *(int16_t *)&pData[3], (int8_t)pData[5]);
  135.   }
  136.   else if (length == 5) {
  137.     // https://github.com/wakwak-koba/ESP32-NimBLE-Mouse
  138.     // returns 5 bytes per HID report
  139.     Serial.printf("buttons: %02x, x: %d, y: %d, wheel: %d hwheel: %d",
  140.         pData[0], (int8_t)pData[1], (int8_t)pData[2], (int8_t)pData[3], (int8_t)pData[4]);
  141.   }
  142.   Serial.println();
  143. }
  144. /** Callback to process the results of the last scan or restart it */
  145. void scanEndedCB(NimBLEScanResults results){
  146.   Serial.println("Scan Ended");
  147. }
  148. /** Create a single global instance of the callback class to be used by all clients */
  149. static ClientCallbacks clientCB;
  150. /** Handles the provisioning of clients and connects / interfaces with the server */
  151. bool connectToServer()
  152. {
  153.   NimBLEClient* pClient = nullptr;
  154.   /** Check if we have a client we should reuse first **/
  155.   if(NimBLEDevice::getClientListSize()) {
  156.     /** Special case when we already know this device, we send false as the
  157.      *  second argument in connect() to prevent refreshing the service database.
  158.      *  This saves considerable time and power.
  159.      */
  160.     pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
  161.     if(pClient){
  162.       if(!pClient->connect(advDevice, false)) {
  163.         Serial.println("Reconnect failed");
  164.         return false;
  165.       }
  166.       Serial.println("Reconnected client");
  167.     }
  168.     /** We don't already have a client that knows this device,
  169.      *  we will check for a client that is disconnected that we can use.
  170.      */
  171.     else {
  172.       pClient = NimBLEDevice::getDisconnectedClient();
  173.     }
  174.   }
  175.   /** No client to reuse? Create a new one. */
  176.   if(!pClient) {
  177.     if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
  178.       Serial.println("Max clients reached - no more connections available");
  179.       return false;
  180.     }
  181.     pClient = NimBLEDevice::createClient();
  182.     Serial.println("New client created");
  183.     pClient->setClientCallbacks(&clientCB, false);
  184.     /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout.
  185.      *  These settings are safe for 3 clients to connect reliably, can go faster if you have less
  186.      *  connections. Timeout should be a multiple of the interval, minimum is 100ms.
  187.      *  Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout
  188.      */
  189.     pClient->setConnectionParams(12,12,0,51);
  190.     /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */
  191.     pClient->setConnectTimeout(5);
  192.     if (!pClient->connect(advDevice)) {
  193.       /** Created a client but failed to connect, don't need to keep it as it has no data */
  194.       NimBLEDevice::deleteClient(pClient);
  195.       Serial.println("Failed to connect, deleted client");
  196.       return false;
  197.     }
  198.   }
  199.   if(!pClient->isConnected()) {
  200.     if (!pClient->connect(advDevice)) {
  201.       Serial.println("Failed to connect");
  202.       return false;
  203.     }
  204.   }
  205.   Serial.print("Connected to: ");
  206.   Serial.println(pClient->getPeerAddress().toString().c_str());
  207.   Serial.print("RSSI: ");
  208.   Serial.println(pClient->getRssi());
  209.   /** Now we can read/write/subscribe the charateristics of the services we are interested in */
  210.   NimBLERemoteService* pSvc = nullptr;
  211.   NimBLERemoteCharacteristic* pChr = nullptr;
  212.   NimBLERemoteDescriptor* pDsc = nullptr;
  213.   pSvc = pClient->getService(HID_SERVICE);
  214.   if(pSvc) {     /** make sure it's not null */
  215.     // This returns the HID report descriptor like this
  216.     // HID_REPORT_MAP 0x2a4b Value: 5,1,9,2,A1,1,9,1,A1,0,5,9,19,1,29,5,15,0,25,1,75,1,
  217.     // Copy and paste the value digits to http://eleccelerator.com/usbdescreqparser/
  218.     // to see the decoded report descriptor.
  219.     pChr = pSvc->getCharacteristic(HID_REPORT_MAP);
  220.     if(pChr) {     /** make sure it's not null */
  221.       Serial.print("HID_REPORT_MAP ");
  222.       if(pChr->canRead()) {
  223.         std::string value = pChr->readValue();
  224.         Serial.print(pChr->getUUID().toString().c_str());
  225.         Serial.print(" Value: ");
  226.         uint8_t *p = (uint8_t *)value.data();
  227.         for (size_t i = 0; i < value.length(); i++) {
  228.           Serial.print(p[i], HEX);
  229.           Serial.print(',');
  230.         }
  231.         Serial.println();
  232.       }
  233.     }
  234.     else {
  235.       Serial.println("HID REPORT MAP char not found.");
  236.     }
  237.     // Subscribe to characteristics HID_REPORT_DATA.
  238.     // One real device reports 2 with the same UUID but
  239.     // different handles. Using getCharacteristic() results
  240.     // in subscribing to only one.
  241.     std::vector<NimBLERemoteCharacteristic*>*charvector;
  242.     charvector = pSvc->getCharacteristics(true);
  243.     for (auto &it: *charvector) {
  244.       if (it->getUUID() == NimBLEUUID(HID_REPORT_DATA)) {
  245.         Serial.println(it->toString().c_str());
  246.         if (it->canNotify()) {
  247.           if(!it->subscribe(true, notifyCB)) {
  248.             /** Disconnect if subscribe failed */
  249.             Serial.println("subscribe notification failed");
  250.             pClient->disconnect();
  251.             return false;
  252.           }
  253.         }
  254.       }
  255.     }
  256.   }
  257.   Serial.println("Done with this device!");
  258.   return true;
  259. }
  260. void setup ()
  261. {
  262.   Serial.begin(115200);
  263.   Serial.println("Starting NimBLE HID Client");
  264.   /** Initialize NimBLE, no device name spcified as we are not advertising */
  265.   NimBLEDevice::init("");
  266.   /** Set the IO capabilities of the device, each option will trigger a different pairing method.
  267.    *  BLE_HS_IO_KEYBOARD_ONLY    - Passkey pairing
  268.    *  BLE_HS_IO_DISPLAY_YESNO   - Numeric comparison pairing
  269.    *  BLE_HS_IO_NO_INPUT_OUTPUT - DEFAULT setting - just works pairing
  270.    */
  271.   //NimBLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); // use passkey
  272.   //NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_YESNO); //use numeric comparison
  273.   /** 2 different ways to set security - both calls achieve the same result.
  274.    *  no bonding, no man in the middle protection, secure connections.
  275.    *
  276.    *  These are the default values, only shown here for demonstration.
  277.    */
  278.   NimBLEDevice::setSecurityAuth(true, false, true);
  279.   //NimBLEDevice::setSecurityAuth(/*BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM |*/ BLE_SM_PAIR_AUTHREQ_SC);
  280.   /** Optional: set the transmit power, default is 3db */
  281.   NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
  282.   /** Optional: set any devices you don't want to get advertisments from */
  283.   // NimBLEDevice::addIgnored(NimBLEAddress ("aa:bb:cc:dd:ee:ff"));
  284.   /** create new scan */
  285.   NimBLEScan* pScan = NimBLEDevice::getScan();
  286.   /** create a callback that gets called when advertisers are found */
  287.   pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());
  288.   /** Set scan interval (how often) and window (how long) in milliseconds */
  289.   pScan->setInterval(45);
  290.   pScan->setWindow(15);
  291.   /** Active scan will gather scan response data from advertisers
  292.    *  but will use more energy from both devices
  293.    */
  294.   pScan->setActiveScan(true);
  295.   /** Start scanning for advertisers for the scan time specified (in seconds) 0 = forever
  296.    *  Optional callback for when scanning stops.
  297.    */
  298.   pScan->start(scanTime, scanEndedCB);
  299. }
  300. void loop ()
  301. {
  302.   /** Loop here until we find a device we want to connect to */
  303.   if (!doConnect) return;
  304.   doConnect = false;
  305.   /** Found a device we want to connect to, do it now */
  306.   if(connectToServer()) {
  307.     Serial.println("Success! we should now be getting notifications!");
  308.   } else {
  309.     Serial.println("Failed to connect, starting scan");
  310.     NimBLEDevice::getScan()->start(scanTime,scanEndedCB);
  311.   }
  312. }
复制代码
烧录完成后,要切换键盘到蓝牙模式,然后程序会配对,之后就可以读取到按键信息了:

  1. Starting NimBLE HID Client
  2. Advertised HID Device found: Name: RAPOO BT4.0 KB, Address: b4:ee:25:f3:86:99, appearance: 961, serviceUUID: 0x1812
  3. Scan Ended
  4. New client created
  5. Connected
  6. Connected to: b4:ee:25:f3:86:99
  7. RSSI: -64
  8. HID_REPORT_MAP 0x2a4b Value:
  9. Done with this device!
  10. Success! we should now be getting notifications!
  11. b4:ee:25:f3:86:99 Disconnected - Starting scan
  12. Advertised HID Device found: Name: RAPOO BT4.0 KB, Address: b4:ee:25:f3:86:99, appearance: 961, serviceUUID: 0x1812
  13. Scan Ended
  14. Connected
  15. Reconnected client
  16. Connected to: b4:ee:25:f3:86:99
  17. RSSI: -43
  18. HID_REPORT_MAP 0x2a4b Value:
  19. Done with this device!
  20. Success! we should now be getting notifications!
  21. b4:ee:25:f3:86:99 Disconnected - Starting scan
  22. Advertised HID Device found: Name: RAPOO BT4.0 KB, Address: b4:ee:25:f3:86:99, appearance: 961, serviceUUID: 0x1812
  23. Scan Ended
  24. Connected
  25. Reconnected client
  26. Connected to: b4:ee:25:f3:86:99
  27. RSSI: -42
  28. HID_REPORT_MAP 0x2a4b Value: 5,1,9,6,A1,1,85,1,5,7,19,E0,29,E7,15,0,25,1,75,1,95,8,81,2,95,1,75,8,81,3,95,5,75,1,5,8,19,1,29,5,91,2,95,1,75,3,91,3,95,6,75,8,15,0,26,FF,0,5,7,19,0,29,FF,81,0,C0,5,C,9,1,A1,1,85,2,15,0,25,1,75,1,95,1E,A,24,2,A,25,2,A,26,2,A,27,2,A,21,2,A,2A,2,A,23,2,A,8A,1,9,E2,9,EA,9,E9,9,CD,9,B7,9,B6,9,B5,A,83,1,A,94,1,A,92,1,A,9,2,9,B2,9,B3,9,B4,9,8D,9,4,9,30,A,7,3,A,A,3,A,B,3,A,B1,1,9,B8,81,2,95,1,75,2,81,3,C0,
  29. Characteristic: uuid: 0x2a4d, handle: 27 0x001b, props:  0x1a
  30. Characteristic: uuid: 0x2a4d, handle: 31 0x001f, props:  0x1a
  31. Characteristic: uuid: 0x2a4d, handle: 35 0x0023, props:  0x0e
  32. Done with this device!
  33. Success! we should now be getting notifications!
  34. Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,14,2B,0,0,0,0,
  35. Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,2B,0,0,0,0,0,
  36. Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,0,0,0,0,0,0,
  37. Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,14,0,0,0,0,0,
  38. Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,0,0,0,0,0,0,
  39. Notification from b4:ee:25:f3:86:99: Service = 0x1812, Characteristic = 0x2a4d, Value = 0,0,2C,0,0,0,0,0,
复制代码
FireBeetle 读取蓝牙键盘输入图5

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail