mbed
BLE

mbed-OSでBLEのHIDキーボードを実装してみた


はじめに

mbedを用いて無線通信をしたい。もっというとキーボードやマウスとして接続したい。

そう思いたってmbedをHIDとして実装する方法を調べてみました。mbed×BLEの前例記事がそもそも少ないので、HIDの実装となると文献がほぼなく苦労しましたが、なんとか動くモノを実装できたのでまとめてみます。

今回の実装に関しては佐藤氏のブログ記事 氾濫原|ErgoDoxではないナニか。オープンソースかつBluetooth接続のキーボード を大変参考にさせていただきました。


実装環境


動作確認済み


  • macOS High Sierra 10.13.3

  • iOS 11.2.2


作ったキーボード(?)

IMG_5526.JPG

赤いキーでAが入力でき、黒いキーで削除ができます。


回路

mbed_hid.png

キーとなるスイッチをp0とp1にそれぞれつなぎ、BLEの接続確認をするためのLEDをLED2ピンに繋ぎました。なおLEDは抵抗入りのものを利用したので省略しています。

動作確認だけのため電源は有線で得ています。本当に無線化したい場合はバッテリを使ってください。


実装


1.mbed-OSのプロジェクト立ち上げ

ここからImport into Mbed IDEでMbed Compilerにインポートすると楽です。

最新のmbed-OSでの動作は未確認なので、状況に応じて古いバージョンにリビジョンしてください。


2.有志のライブラリを拝借

jpbrucker氏のgithubレポジトリのBLE_HIDディレクトリから

- HIDServiceBase.h

- HIDServiceBase.cpp

- KeyboardService.h

- Keyboard_types.h

- USBHID_Types.h

をダウンロードしてプロジェクトのルートディレクトリにインポートします。


3.main.cppを実装


ソースコード


main.cpp

#include "mbed.h"

#include "BLE.h"
#include "KeyboardService.h"
#include "BatteryService.h"
#include "DeviceInformationService.h"

#if 0
Serial pc(USBTX, USBRX);
#define DEBUG(...) { pc.printf(__VA_ARGS__); }
#else

#define DEBUG(...)
#endif

InterruptIn btnA(p1);
InterruptIn btnD(p0);
DigitalOut led(LED2);

BLE ble;
KeyboardService *kbdService;

static const char DEVICE_NAME[] = "MyKeyboard";
static const char SHORT_DEVICE_NAME[] = "MK";

static const uint16_t UUID_SERVICES[] = {
GattService::UUID_HUMAN_INTERFACE_DEVICE_SERVICE,
GattService::UUID_DEVICE_INFORMATION_SERVICE,
GattService::UUID_BATTERY_SERVICE};

static Gap::ConnectionParams_t fast;

static void connectionCallback(const Gap::ConnectionCallbackParams_t *params) {
DEBUG("Connected!\r\n");
led = 1;
ble.gap().stopAdvertising();
ble.updateConnectionParams(params->handle, &fast);
}

static void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params) {
DEBUG("Disconnected!\r\n");
led = 0;
ble.gap().startAdvertising();
}

static void securitySetupInitiatedCallback(Gap::Handle_t, bool allowBonding, bool requireMITM, SecurityManager::SecurityIOCapabilities_t iocaps) {
DEBUG("Security setup initiated\r\n");
}

static void passkeyDisplayCallback(Gap::Handle_t handle, const SecurityManager::Passkey_t passkey) {
DEBUG("Input passKey: ");
for (int i=0; i<Gap::ADDR_LEN; i++) {
DEBUG("%c", passkey[i]);
}
DEBUG("\r\n");
}

static void securitySetupCompletedCallback(Gap::Handle_t handle, SecurityManager::SecurityCompletionStatus_t status) {
if (status == SecurityManager::SEC_STATUS_SUCCESS) {
DEBUG("Security success %d\r\n", status);
} else {
DEBUG("Security failed %d\r\n", status);
}
}

void send_A() {
if (!kbdService)
return;
if (!kbdService->isConnected()) {
DEBUG("Error: not connected");
} else {
DEBUG("Send");
kbdService->_putc(65);
}
}

void send_D() {
if (!kbdService)
return;
if (!kbdService->isConnected()) {
DEBUG("Error: not connected");
} else {
DEBUG("Send");
kbdService->_putc(8);
}
}

int main(void) {
btnA.mode(PullUp);
btnD.mode(PullUp);
btnA.rise(send_A);
btnD.rise(send_D);
DEBUG("Start\r\n");

DEBUG("Initialise\r\n");
ble.init();

DEBUG("Setup the event handlers\r\n");
ble.gap().onConnection(connectionCallback);
ble.gap().onDisconnection(disconnectionCallback);

DEBUG("Setup latency\r\n");
ble.getPreferredConnectionParams(&fast);
fast.minConnectionInterval = 7.5;
fast.maxConnectionInterval = 10;
fast.slaveLatency = 0;
ble.setPreferredConnectionParams(&fast);

DEBUG("Initialize security\r\n");
ble.securityManager().onSecuritySetupInitiated(securitySetupInitiatedCallback);
ble.securityManager().onPasskeyDisplay(passkeyDisplayCallback);
ble.securityManager().onSecuritySetupCompleted(securitySetupCompletedCallback);
ble.securityManager().init(true, false, SecurityManager::IO_CAPS_NONE);

DEBUG("Adding device info and battery service\r\n");
KeyboardService kbdService_(ble);
kbdService = &kbdService_;
DeviceInformationService deviceInfo(ble, "ARM", "m1", "abc", "def", "ghi", "jkl");
BatteryService batteryInfo(ble, 80);

DEBUG("Advertising payload\r\n");
ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::KEYBOARD);
ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (const uint8_t *)DEVICE_NAME, strlen(DEVICE_NAME));
ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::SHORTENED_LOCAL_NAME, (const uint8_t *)SHORT_DEVICE_NAME, strlen(SHORT_DEVICE_NAME));
ble.gap().setDeviceName((const uint8_t *)DEVICE_NAME);

ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)UUID_SERVICES, sizeof(UUID_SERVICES));
ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
ble.gap().setAdvertisingInterval(160); //100ms; in multiples of 0.625ms.
ble.gap().startAdvertising();

DEBUG("Add service\r\n");
ble.gattServer().addService(customService);

while (true) {
ble.waitForEvent();
}
}



カスタマイズする時編集するところ


  • DEVICE_NAME

  • SHORT_DEVICE_NAME

  • deviceInfo

  • batteryInfo

  • ボタン(スイッチ)関連


ちょっと解説

kbdService->_putc(数字);のところで入力するキーを指定しています。

 数字はASCIIコードの10進数番号です(たぶん)。

・スイッチはDigitalInに抵抗を挟まず繋いでますが、mbedのプルアップモードを使うとちゃんと動きます。

 また、riseでキーが押された時の関数を指定してあげれば割り込み処理ができます。


Macと接続したい時

ここにある通り、BatteryService.hDeviceInformationService.hをコピーして中身を書きかえて、requireSecurity(SecurityManager::SECURITY_MODE_ENCRYPTION_NO_MITM)にすればOKです。

具体的には佐藤氏のmbedレポジトリを見てください。


注意

文字入力がうまくいかない時は、一度完全に端末との接続を解除してもう一度繋いでみてください。


おわりに

先駆者様がいたおかげで短時間にてHIDキーボードの実装ができました。

この分野はマニアックなのか、BLEが難しいからか非常に前例文献が少ないので、なるべく分かったことは記事に残していきたいですね。