ESP32とminiUHSを使ってUSBキーボードをBLE HIDキーボードにしてみる(再度更新)

概要

  • USBの日本語(JIS)キーボードをmini USB Host Shield (miniUHS)を使ってESP32に接続する。
  • Arduino core for ESP32を使ってBLE HIDキーボードを構成し、PCやスマホで使ってみる。

ちょっと前に(SparkFun) Pro Micro でやったことをESP32でやってみて、ついでにBLEライブラリを使ってみた、というお話。残念ながら、Bluetoothキーボードとしての実用レベルには達していない。

※ BLEセキュリティ関係の小改造を行ってみましたが、やはり初回接続時のみしかうまくいきませんでした。

※※ その後、リスタート時も再接続できるようになりました。

構成

DOIT DevKit V1を使う

今回は、しばらく前にaliexpress で $5.00くらいで購入した端子の少ないESP32開発ボードを使うことにした。サイトでの商品名が “ESP-32 ESP-32S ESP-WROOM-32 ESP32-S Development Board WiFi Bluetooth Ultra-Low Power 云々“となっていて呼び方に迷うところだが、どうやら深センにあるDOIT社の”DOIT ESP32 DevKit V1“という製品のコピー品らしい。pdf版の回路図はこちら

doit esp32
DOIT DevKit. 端子の少ないESP32開発ボード

あまりきれいな仕上がりではないが、まあ安いので。載っているデバイスを見ると、ESP-WROOM-32、3.3V用の定電圧レギュレータ(AMS1117)、USBシリアル変換デバイス( CP2102 ) など今まで使ってきた秋月電子の開発ボード(DevKitC)と変わらないようだ。

ESP32開発ボード2種

左側がDOIT DevKit V1、右側が秋月電子のESP32-DevKITCで38ピンある。DOITの方には、およそ使わないだろうWROOM-32内蔵フラッシュ用の6端子(GPIO6~GPIO11)がなく、GPIO0とGND(が1つ)省略されているので30ピンで収まっている。

ただ、PCBアンテナがボードからはみ出していないから、ボード自体の大きさはあまり変わらない。DOIT側のWROOM-32のアンテナ部分には、ペイントによるマーキングがあるのだけど、これは何を意味しているのか気になるところ。まあ、いずれもテレックマークが付いているから日本国内でも電波を出せる。

1枚目のDOIT DEVKIT V1の写真を拡大してみると、WROOM-32の左側に並んだチップ抵抗の上下にLEDがあることに気が付いた。回路図を見ると、片方は電源インジケータでもう片方は抵抗を介してGPIO2に接続されているようだ。DevKITC互換ボードをブレッドボードに載せるとき、GPIO2にLEDをつないで使うことがあったのでちょうどいい。
GPIO2はブートモードを左右するストラッピングピンの1つで、ESP32内でプルダウンされている。ダウンロードブート時にはLに保つ必要があるが、その後は自由に使うことができる。

回路図

MAX3421Eのリセットや、USB-Aコネクタ用のVBUSの配線を変更したmini UHSボードと、ESP32ボードを以下のように接続した。miniUHSボードの改造内容についてはこちらを参照。

ESP32_miniUHS_1

いずれも+3.3V動作なので、SPI用の配線はESP32のVSPI用の端子にそのまま接続する。miniUHSのINT端子は、ESP32のGPIO17に接続した。

端子間の接続については、UHSライブラリ(USB Host Shield Library V2.0 ) の、avrpins.h 内にある、Pinout for ESP32 dev module にしたがった。このライブラリは、ESP32での利用も考慮されている。

しょうしょう急いで作ったこともあって、ブレッドボードに載せるとこんな具合にぐちゃぐちゃになってしまった。

ESP32 + mini UHS (コンデンサ入ってなかった)

スケッチ1

まずはESP32でのminiUHSとUHSライブラリの具合を見るため、キーボードが上げてくるコードをシリアルモニタに表示させてみた。

miniUHSのコントロールには、ちょっと前に Pro Micro用に導入したCircuits@Home が公開しているライブラリを使う。導入方法や概要についてはこちらを参照。今回は、このライブラリに付属のスケッチ例(USBHIDBootKbd.ino)をちょっと手直しして使った。とりたててライブラリに手を入れることなくビルドも通り、意図通りに動いた。

// ESP32_HIDBootKbd_Test1,ino
// based on USBHIDBootKbd.ino in USB Host Shield Libraty 2.0.

#include <hidboot.h>
#include <usbhub.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

static const uint8_t LED = 2;

uint8_t modifier_keys = 0;

class KbdRptParser : public KeyboardReportParser {
  protected:
    void OnControlKeysChanged(uint8_t before, uint8_t after);
    void OnKeyDown	(uint8_t mod, uint8_t key);
    void OnKeyUp	(uint8_t mod, uint8_t key);
    void OnKeyPressed(uint8_t key) {}
};

void show_event(uint8_t k, uint8_t mod, const char* evt) {
  char tmp[40];
  sprintf(tmp, "%4s : k = %02x, mod = %02x\n", evt, k, mod);
  Serial.print(tmp);
}

#define key_down(a, b) show_event(a, b, "DOWN" )
#define key_up(a, b) show_event(a, b, "UP" )

int key_pressed = 0;
void KbdRptParser::OnKeyDown(uint8_t mod, uint8_t key) {
  key_down(key, mod);
  key_pressed++;
  if (key_pressed > 0)
    digitalWrite(LED, 1);
}

void KbdRptParser::OnControlKeysChanged(uint8_t before, uint8_t after) {
  uint8_t change = before ^ after;
  if (change) {
    modifier_keys = after;
    if (change & after) 
      key_down(0, after); 
    else
      key_up(0, after);
  }
}

void KbdRptParser::OnKeyUp(uint8_t mod, uint8_t key) {
  key_up(key, mod);
  key_pressed--;
  if (key_pressed < 1)
    digitalWrite(LED, 0);
}

USB     Usb;
HIDBoot<USB_HID_PROTOCOL_KEYBOARD>    HidKeyboard(&Usb);
KbdRptParser Prs;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin( 115200 );
#if !defined(__MIPSEL__)
  while (!Serial); // Wait for serial port to connect - used on Leonardo, Teensy and other boards with built-in USB CDC serial connection
#endif
  Serial.println("ESP32_HIDBootKbd_Test1 Start");
  if (Usb.Init() == -1)
    Serial.println("OSC did not start.");
  delay( 200 );
  HidKeyboard.SetReportParser(0, &Prs);
}

void loop() {
  Usb.Task();
}

KbdRptParser を介して通知されるキーの押下時(OnKeyDown())とリリース(OnKeyUp())時に、キーコード(HID Usage ID)と修飾キーの内容をシリアルモニタに表示する。修飾キー単独の押下/リリースについては OnControlKeysChanged() に通知されるので、キーコードを0として同様に表示するようにした。

修飾キー以外のキーが押下されたときにGPIO2に接続された青いLEDを点灯させ、すべてのキーがリリースされたとき消灯するようにした。

シリアルモニタにはキーボード操作のたびにコードが表示されるのだが、特に画面をとるほどでもないだろう。

DOIT DEVKIT V1への書き込みについて

ビルド結果をマイコンボードに書き込むとき、Arduino IDEで書き込みを開始してもダウンロードが始まらない。そのため、ダウンロードが開始するまでBOOTボタンを押したままにしておき、開始したことが画面で確認できてからBOOTボタンを離すことで、ようやく書き込みできた。
ときによっては、その操作を何度か繰り返さないとダウンロードできないこともあった。回路図を見るとそういう操作無しでもうまくいきそうに見える。改善できるとは思うが、ボタン押せばいいのでとりあえず今回は見送り。

Bluetoothキーボードにしてみる

せっかくESP32を使うので、Arduino Coreに用意されているBLEライブラリを使ってワイヤレスキーボードにできないものかと思って書いてみた。 https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLETests/SampleHIDKeyboard.cpp をArduino風に書き替え、USBキーボードから上がってくるコードを渡せるようにした。

メインスケッチ (ESP32_BLE_Kbd_Test2.ino)

// 
// ESP32_BLE_Kbd_Test2,ino

// UHS2.0
#include <hidboot.h>
#include <usbhub.h>
// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

#include "BleHidKeyboard.h"

static const uint8_t LED = 2;
static const uint8_t  SW = 0;

BleHidKeyboard* ble = 0;

class KbdRptParser : public KeyboardReportParser {
protected:
	void OnControlKeysChanged(uint8_t before, uint8_t after);
	void OnKeyDown	(uint8_t mod, uint8_t key);
	void OnKeyUp	(uint8_t mod, uint8_t key);
	void OnKeyPressed(uint8_t key) {}
};

void show_event(uint8_t k, uint8_t mod, const char* evt) {
	char tmp[40];
	sprintf(tmp, "%4s : k = %02x, mod = %02x\n", evt, k, mod);
	Serial.print(tmp);
}

#define key_down(a, b) show_event(a, b, "DOWN" )
#define key_up(a, b) show_event(a, b, "UP" )

void KbdRptParser::OnKeyDown(uint8_t mod, uint8_t key) {
	key_down(key, mod);
	ble->report_press(key, mod);
}

void KbdRptParser::OnControlKeysChanged(uint8_t before, uint8_t after) {
	ble->modifier_change(before, after);
}

void KbdRptParser::OnKeyUp(uint8_t mod, uint8_t key) {
	key_up(key, mod);
	ble->report_release(key, mod);
}

USB     Usb;
HIDBoot<USB_HID_PROTOCOL_KEYBOARD>    HidKeyboard(&Usb);
KbdRptParser Prs;

void show_error(int period) {
	digitalWrite(LED, 1);
	delay(period);
	digitalWrite(LED, 0);
	delay(period);
}

void setup() {
	pinMode(LED, OUTPUT);
	Serial.begin( 115200 );
	delay(100);
	Serial.println("ESP32_BLE_KBD_Test2 Start...");
	if (Usb.Init() == -1) {
		Serial.println("OSC did not start.");
		show_error(250);
	}
	ble = BleHidKeyboard::getInstance();
	ble->set_led_pin(LED);
	delay( 200 );
	HidKeyboard.SetReportParser(0, &Prs);
	ble->init();
	while(!ble->is_connected())
		delay(200);  
}

void loop() {
	Usb.Task();
	delay(1);
}

メインスケッチでは、UHSライブラリからあがってくるキーの押下/リリース情報を、BLEキーボード用のクラス(BleHidKeyboard)に渡す。

USBでホストデバイスに接続するときにはArduinoのHIDライブラリを利用するクラスに渡してインプットレポートとしていたが、今回はBLEを利用するクラスに渡す。今のところ、HID-USBとHID-BLEのインタフェースを共通化していない。NICOLA(親指シフト)化するなら共通化したいところだが、この実装ではやる気が起きないというか…。

BLE HIDキーボード用のクラス BleHidKeyboard

HIDキーボードとしてホストデバイスから認識され、物理キーボードから上がってくるキーコード(HID Usage ID)を送信できるようにするためのクラス。(BleHidKeyboard.h)

// BLE Libraries.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <BLEHIDDevice.h>

// HID input report data.
#define REPORT_KEYS 6
typedef struct {
	uint8_t modifiers;
	uint8_t reserved;
	uint8_t keys[REPORT_KEYS];
} key_report_t;

// HID report desc (keyboard).
static const uint8_t reportMap[] = {
	0x05, 0x01, // USAGE_PAGE (Generic Desktop)
	0x09, 0x06, // USAGE (Keyboard)
	0xa1, 0x01, // COLLECTION (Application)
	0x85, 0x01, //  REPORT_ID (1)

	0x05, 0x07, //   USAGE_PAGE (usage = keyboard page)
	// モデファイヤキー(修飾キー)
	0x19, 0xe0, //     USAGE_MINIMUM (左CTRLが0xe0)
	0x29, 0xe7, //     USAGE_MAXIMUM (右GUIが0xe7)
	0x15, 0x00, //   LOGICAL_MINIMUM (0)
	0x25, 0x01, //   LOGICAL_MAXIMUM (1)
	0x95, 0x08, //   REPORT_COUNT (8) 全部で8つ(左右4つずつ)。
	0x75, 0x01, //   REPORT_SIZE (1) 各修飾キーにつき1ビット
	0x81, 0x02, //   INPUT (Data,Var,Abs) 8ビット長のInputフィールド(変数)が1つ。
	// 予約フィールド
	0x95, 0x01, //   REPORT_COUNT (1)
	0x75, 0x08, //   REPORT_SIZE (8) 1ビットが8つ。
	0x81, 0x01, //   INPUT (Cnst,Var,Abs)
	// LED状態のアウトプット
	0x95, 0x05, //   REPORT_COUNT (5) 全部で5つ。
	0x75, 0x01, //   REPORT_SIZE (1)  各LEDにつき1ビット
	0x05, 0x08, //   USAGE_PAGE (LEDs)
	0x19, 0x01, //     USAGE_MINIMUM (1) (NumLock LEDが1)
	0x29, 0x05, //     USAGE_MAXIMUM (5) (KANA LEDが5)
	0x91, 0x02, //   OUTPUT (Data,Var,Abs) // LED report
	// LEDレポートのパディング
	0x95, 0x01, //   REPORT_COUNT (1)
	0x75, 0x03, //   REPORT_SIZE (3)  残りの3ビットを埋める。
	0x91, 0x01, //   OUTPUT (Cnst,Var,Abs) // padding 
	// 押下情報のインプット
	0x95, 0x06, //   REPORT_COUNT (6) 全部で6つ。
	0x75, 0x08, //   REPORT_SIZE (8) おのおの8ビットで表現
	0x15, 0x00, //   LOGICAL_MINIMUM (0) キーコードの範囲は、
	0x25, 0xdd, //   LOGICAL_MAXIMUM (221) 0~221(0xdd)まで

	0x05, 0x07, //   USAGE_PAGE (Keyboard)
	0x19, 0x00, //     USAGE_MINIMUM (0はキーコードではない)
	0x29, 0xdd, //     USAGE_MAXIMUM (Keypad Hexadecimalまで)
	0x81, 0x00, //   INPUT (Data,Ary,Abs)
	0xc0    // END_COLLECTION
};

class BleHidKeyboard;
static BleHidKeyboard* instance = 0;
class BleHidKeyboard {
  class ServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
       BleHidKeyboard::getInstance()->set_connected(true);
      Serial.println("ServerCallbacks::onConnect()");
    };
  
    void onDisconnect(BLEServer* pServer) {
      BleHidKeyboard::getInstance()->set_connected(false);
      Serial.println("ServerCallbacks::onDisconnect()");
    }
  };
  
  class BleHidOutputReport : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* me){
      uint8_t* value = (uint8_t*)(me->getValue().c_str());
      if (value) {
        uint8_t led = *value;
        BleHidKeyboard::getInstance()->set_led_state(led);
        Serial.printf("BleHidOutputReport: %02x\n", led);
      }
    }
  };

	BleHidKeyboard() {
		connected = false;
		memset(&report, 0, sizeof(key_report_t));
		modifier_keys = 0;
		led_state = 0;  // set by hid output report.
		led_pin = 0;
	}
	bool connected;
	BLEHIDDevice* hid;
	BLEServer* pServer;
  BLECharacteristic* input;
  BLECharacteristic* output;

	key_report_t report;
	uint8_t modifier_keys;
	uint8_t led_state;
	uint8_t led_pin;
public:
  static BleHidKeyboard* getInstance() {
    if (!instance)
      instance = new BleHidKeyboard();
    return instance;    
  };
	void init() {
		BLEDevice::init("ESP32_BLE_KBD1");
		pServer = BLEDevice::createServer();
		pServer->setCallbacks(new ServerCallbacks());
	//	BLEDevice::setMTU(23);
	 	hid = new BLEHIDDevice(pServer);
		input = hid->inputReport(1);
		output = hid->outputReport(1);
		output->setCallbacks(new BleHidOutputReport());
		std::string name = "okiraku-camera";
		hid->manufacturer()->setValue(name);
		hid->pnp(0x02, 0xe502, 0xa111, 0x0210);
		hid->hidInfo(0x00,0x01);  // country == 0, flags == 1 ( providing wake-up signal to a HID host)
		hid->reportMap((uint8_t*)reportMap, sizeof(reportMap));
		hid->startServices();
		BLEAdvertising *pAdvertising = pServer->getAdvertising();
		pAdvertising->setAppearance(HID_KEYBOARD);
		pAdvertising->addServiceUUID(hid->hidService()->getUUID());
		pAdvertising->start();
		Serial.println("Start Advertising.");

		BLESecurity *pSecurity = new BLESecurity();
		pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);	
	}

	void set_led_state(uint8_t led) { led_state = led; }
	bool is_connected() const { return connected; }
	void set_connected(bool f) { 
		connected = f; 
		if (led_pin != 0)
			digitalWrite(led_pin, f ? 1 : 0);
	}
	void sendReport() {
		if (is_connected()) {
			input->setValue((uint8_t*)&report, sizeof(key_report_t));
			input->notify();
		}
	}
// keyboard interface.	
	void report_press(uint8_t key, uint8_t mod) {
		modifier_keys = mod;
		if (key != 0) {
			bool already = false;
			int empty_slot = -1;
			for(int i = 0; i < REPORT_KEYS; i++) {
				if (report.keys[i] == key)
					already = true;
				if (report.keys[i] == 0 && empty_slot < 0)
					empty_slot = i;
			}
			if (empty_slot < 0)	// error condition.
				return;			// should report it ? 
			if (!already)
				report.keys[empty_slot] = key;
		}
		report.modifiers = modifier_keys;
		sendReport();  
	}
	void report_release(uint8_t key, uint8_t mod) {
		if (key != 0) {
			for(int i = 0; i < REPORT_KEYS; i++) {
				if (report.keys[i] == key) {
					report.keys[i] = 0;
					break;
				}
			}
		}
		report.modifiers = modifier_keys;
		sendReport();  
	};
	void modifier_change(uint8_t before, uint8_t after) {
		uint8_t change = before ^ after;
		modifier_keys = after;
		if (change & after)
			report_press(0, after);
		else
			report_release(0, after);
	}
	void stroke(uint8_t key, uint8_t mod) {
		report_press(key, mod);
		report_release(key, mod);
	}
	void set_led_pin(uint8_t led) { led_pin = led; }
};

BLE関係の初期化やコールバックがあるので、若干長くなっているが、基本的にはPro Micro用のHIDキーボードの中身と同じになっている。

reportMap (HID Report Description)

HID Usage Page 7 のキーボードとして動作させるため、Pro Micro用に用意したものをコピーしてきた。
ArduinoのHIDライブラリでは、キーボードとマウスのコンポジットデバイスになっているので、レポートID == 2としていたが、こちらはキーボード単独なので1としている。

class BleHidKeyboard

https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLETests/SampleHIDKeyboard.cpp の中身を Arduino Core for the ESP32を組み込んだArduino IDEでビルドできるようなクラスにした。そして、物理キーボード側(UHSライブラリ経由)から上がってくるコードを、HIDインプットレポートとして送信するためのメソッドを用意した。

BLE HIDキーボードとするための処理はほとんど init() 内に収めてあるが、これはNeil Kolbanさんのサンプルのまんまである。

もとのサンプルコードは、あらかじめプログラム内に書かれた文字列を繰り返して送信(インプットレポートとして input->setValue())するようになっているが、このクラスではUHSライブラリから上がってくるコードを渡すように変更した( sendReport() )。
また、ホストデバイスと接続しました通知( BLEServerCallbacks::onConnect() )がきたとき、DOIT DEVKIT V1のLED(GPIO2に接続)を点灯するようにした。青色LEDなのでそれらしく見える。BLEServerCallbacks::onDisconnect() がきたら消灯する。

BLECharacteristicCallbacksを派生したBleHidOutputReportは、アウトプットレポートを受信したときに呼ばれる。アウトプットレポートではキーボードLEDの状態がくるのだが、現在使っているキーボードにはLEDがないので、物理キーボードに反映する処理は省略した。なお、hoboNicolaライブラリでは、UHSライブラリのKeyboardReportParserのParse() を上書きすることで、LEDを点灯可能とした。

1本のヘッダファイルにすべて収めたかったので、コールバックを処理するクラスはインナークラスとした。

スマホとの接続

Android 8.0.0 (HUAWEI NOVA LITE)に接続し、ちょっと入力したところを動画にしてみた。

他にBluetoothキーボードを持っていないので比較できないのだけど、デバイスを認識さえしてくれれば、後は早い。

何もせずにいてスマホの画面が消えてしまっても、キーボードを叩くと画面が復活し、空白キーを叩いてPINの入力もできた。

シリアルモニタを見ていると、

ServerCallbacks::onDisconnect()
ServerCallbacks::onConnect()
ServerCallbacks::onDisconnect()
ServerCallbacks::onConnect()
DOWN : k = 2c, mod = 00
  UP : k = 2c, mod = 00
ServerCallbacks::onDisconnect()
ServerCallbacks::onConnect()

けっこう、切れたりつながったりしているようだった。(k = 2c は空白キー)。

使用感は悪い

スマホに接続して「メモを作成」などをすると、ちゃんと文字が入力できるし、半角/全角キーでかなと英数の切り替えもできる。キーボードとして認識されているようである。

ただ、Shift + 英数キーを押してもCaps Lockが掛からないようだった。シリアルモニタには、英数キーの押下に伴ってCAPS LOCK LEDを点灯しろ、というアウトプットレポートが届いているのだが。

DOWN : k = 39, mod = 00
  UP : k = 39, mod = 00
BleHidOutputReport: 02
DOWN : k = 39, mod = 00
  UP : k = 39, mod = 00
BleHidOutputReport: 00
DOWN : k = 39, mod = 00
  UP : k = 39, mod = 00
BleHidOutputReport: 02
DOWN : k = 39, mod = 00
  UP : k = 39, mod = 00
BleHidOutputReport: 00
DOWN : k = 39, mod = 02
  UP : k = 39, mod = 02
BleHidOutputReport: 02
DOWN : k = 39, mod = 02
  UP : k = 39, mod = 00
BleHidOutputReport: 00

k = 39が英数 ( CapsLock )キーを表している。英数キー単独でも、Shiftキーを押しながら( mod = 02 )でもメモ帳には大文字が表示されなかった。Androidのアプリの問題なのかもしれないが。

全般的には、調子よく入力できるときと、そうでないときとがある。具合が悪いときは、なかなかキー入力が入らなかったり、キーを1回押しただけなのに、aaaaaaaaaaとリピートされたりする。BackSpace が余計にリピートされると打った文字が消えてしまって腹立たしいことになる。

でかい

ビルドすると、Arduino IDEには以下のように表示された。

最大2097152バイトのフラッシュメモリのうち、スケッチが1163861バイト(55%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が58744バイト(17%)を使っていて、ローカル変数で268936バイト使うことができます。

スケッチが1163861バイト 使っている。なんで?

何かが足りない

問題は、ESP32をリセットしたようなとき、いったんホストデバイス側のBluetoothをオフにしてからオンにしないと、キーボード入力ができなくなること。

一度キーボードとして利用できる状況になっていれば、スマホ側から切断された場合でもキーボードは自動的に再接続しその後の入力も支障なく行える。だがESP32をリスタートしてしまうと、自動接続は行われるのだがキー入力の送信が行えない状態になってしまう。

Arduino IDEの、ツール/Core Debug Level をVerboseにしてビルドし直してみると、キーボードからのイベントに対して以下のようなデバッグメッセージが出力されていた。

DOWN : k = 52, mod = 00
[D][BLECharacteristic.cpp:664] setValue(): >> setValue: length=8, data=0000520000000000, characteristic UUID=00002a4d-0000-1000-8000-00805f9b34fb
[D][BLECharacteristic.cpp:671] setValue(): << setValue
[D][BLECharacteristic.cpp:524] notify(): >> notify: length: 8
[D][BLECharacteristic.cpp:543] notify(): << notifications disabled; ignoring
  UP : k = 52, mod = 00
[D][BLECharacteristic.cpp:664] setValue(): >> setValue: length=8, data=0000000000000000, characteristic UUID=00002a4d-0000-1000-8000-00805f9b34fb
[D][BLECharacteristic.cpp:671] setValue(): << setValue
[D][BLECharacteristic.cpp:524] notify(): >> notify: length: 8
[D][BLECharacteristic.cpp:543] notify(): << notifications disabled; ignoring

つまり、インプットレポートのsetValue() が門前払いされている。なお、k = 52は上矢印キーである。

うまくいくときはこんな具合になっている。

DOWN : k = 52, mod = 00
[D][BLECharacteristic.cpp:664] setValue(): >> setValue: length=8, data=0000520000000000, characteristic UUID=00002a4d-0000-1000-8000-00805f9b34fb
[D][BLECharacteristic.cpp:671] setValue(): << setValue
[D][BLECharacteristic.cpp:524] notify(): >> notify: length: 8
[D][BLEDevice.cpp:96] gattServerEventHandler(): gattServerEventHandler [esp_gatt_if: 4] ... ESP_GATTS_CONF_EVT
...

この問題はまだ解決できていない。たぶん、何かが足りないのだろう。ESP IDFを使うとうまくいくんだろうか?

きょうのまとめ

ESP32を使うのはしばらくぶりだったので、いろいろ思い出すためにArduino IDEの更新やArduino core for the ESP32 の導入から始めた。今回のスケッチを動かす前に、GPIO2 に接続されているオンボードLEDをチカチカさせたりもしたが、そこは省略。

ESP32のWiFiでキーボード入力を飛ばすとなると、udpでコントロールするリモコン的なものも考えられる。今回はUSBキーボードをつないだが、UHSライブラリがサポートするHIDデバイスならば、ゲームコントローラでもマウスでも構わないわけで。
もっとも、そういう用途ならESP-WROOM-02で十分だろう。

BLEデバイスとしたときのESP32の消費電流がどの程度なのだろう。世の中で安く売っているBluetoothキーボードは、単3や単4の乾電池2本でけっこうな期間使える。それに対して、ESP32で実装した場合はどうなんだろう、という疑問がある。自分のお気に入りのキーボード(ちょっと頑張ればPS/2キーボード)をワイヤレス化できるのだから、モバイルバッテリ必須でもしかたないのかなぁ、とか思ったりするのだが。とりあえず、BLEデバイスのお勉強用に取り組むという感じか。

BluetoothドングルをPCに付けて、PCのキーボードとしても試していたのだけどリスタート問題は同じだった。このドングル、数日使った時点でPCから認識されなくなってしまった(CSRチップの入った国内周辺機器大手B社の製品)。1000円くらいで買って1年間放置してあったので保証もきかないから燃えないゴミにするか。
その後、サンワサプライの、やはり1200円くらいのドングルを購入して使うようになった。こちらは今のところ安定している。

追記(問題解決)

たびたび思いつきを書いていて、何やってるのか分からなくなってきたので整理して書き直しました。

うまくいく場合
  • まず、PCとのペアリングを外してデバイスを削除する。
  • その状態でESP32をリスタートし、新しいデバイスとしてPCとペアリングする。
  • そのまま使い続ける場合、上記のログのようにキーボードからの入力がPCやスマホに表示される。
うまくいかない場合
  • うまく入力できているときに、ESP32をリスタートする。
  • 接続完了のイベントも来るし、PC側からキーボードLED状態の通知(Output Report)も届く。
  • ただキーボードを打つと、上記ログのように notifications disabled; ignoring  となって入力できない。
セキュリティ関係の問題か?

当初は、ホストデバイスとESP32 BLE間の認証がうまくいってないのかと疑った。ただ接続は完了しているし、少なくともOutput Report は正しく届いている。

BLESecurity *pSecurity = new BLESecurity(); pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);

の部分を以下のように変更したが同じだった。

BLESecurity *pSecurity = new BLESecurity();
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);
pSecurity->setCapability(ESP_IO_CAP_NONE);
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);

ESP_IO_CAP_NONEを、ESP_IO_CAP_OUT や ESP_IO_CAP_IOに変えてやると、BLESecurityCallbacksonConfirmPIN() に交換されているキーが表示され、PC側のペアリング画面にも同じ6桁の数字が表示されたりした。ふつう、キーボードはPINを表示できないので、PC側で常にOK することになるが。

また、ペアリングと接続に成功したホストデバイスのアドレスなどの情報は、ESP32のnvsに維持されるホワイトリストに追加されることも分かった。この動作はきー入力できない場合も同様で、どうも認証に失敗しているわけではなさそう。

ホワイトリストに保存されている内容は、

esptool.exe --port COM3 --baud 921600 read_flash 0x9000 0x5000 nvs.bin

によってフラッシュメモリからnvsエリアを読み出してやれば確認できる。いろいろと書かれている。(–port COM3 はうちのPCにESP32を接続した場合のCOMポートの指定)。

ログの比較

うまくいくときといかないときのverbose なログを比較してみたところ、うまくいく場合にのみ、ハンドル 0x42に対してESP_GATTS_WRITE_EVTが起きて、先頭が0x01、 次が0x00 というデータを書いていることが分かった。

[D][BLEDevice.cpp:96] gattServerEventHandler(): gattServerEventHandler [esp_gatt_if: 4] ... ESP_GATTS_WRITE_EVT
[D][BLEUtils.cpp:1647] dumpGattServerEvent(): GATT ServerEvent: ESP_GATTS_WRITE_EVT
[D][BLEUtils.cpp:1832] dumpGattServerEvent(): [conn_id: 0, trans_id: 21, bda: 00:09:dd:40:xx:xx, handle: 0x42, offset: 0, need_rsp: 1, is_prep: 0, len: 2]
[D][BLEUtils.cpp:1834] dumpGattServerEvent(): [Data: 0100]

ハンドル0x42というのは、以下のように、BLEHIDDeviceに追加されたデスクリプタ BLE2902(  00002902-0000-1000-8000-00805f9b34fb )の内部ハンドルのようだった。

[D][BLEDevice.cpp:96] gattServerEventHandler(): gattServerEventHandler [esp_gatt_if: 4] ... ESP_GATTS_ADD_CHAR_DESCR_EVT
[D][BLEUtils.cpp:1647] dumpGattServerEvent(): GATT ServerEvent: ESP_GATTS_ADD_CHAR_DESCR_EVT
[D][BLEUtils.cpp:1657] dumpGattServerEvent(): [status: ESP_GATT_OK, attr_handle: 66 0x42, service_handle: 55 0x37, char_uuid: 00002902-0000-1000-8000-00805f9b34fb]
[D][BLEServer.cpp:177] handleGATTServerEvent(): >> handleGATTServerEvent: ESP_GATTS_ADD_CHAR_DESCR_EVT

(リスタートから開始するので、生成されるハンドルの値はいつもだいたい一緒)。

ログを見ていると、このuuid をもつデスクリプタがいつも2つ作られているようで、もう一方はハンドル0x62となっている。そして、以下のログのように0x62に対しても0x01, 0x00 が書かれている。

[D][BLEDevice.cpp:96] gattServerEventHandler(): gattServerEventHandler [esp_gatt_if: 4] ... ESP_GATTS_WRITE_EVT
[D][BLEUtils.cpp:1647] dumpGattServerEvent(): GATT ServerEvent: ESP_GATTS_WRITE_EVT
[D][BLEUtils.cpp:1832] dumpGattServerEvent(): [conn_id: 0, trans_id: 20, bda: 00:09:dd:40:xx:xx, handle: 0x62, offset: 0, need_rsp: 1, is_prep: 0, len: 2]
[D][BLEUtils.cpp:1834] dumpGattServerEvent(): [Data: 0100]

このあたりは、もう少し追ってみたいところではあるが、当面の問題はキー入力なのでペンディングにした。

input Reportできるようにする

notification disabled; ignore  というログを出しているのは、BLECharacteristic.cppの541行目付近で以下のようになっている。

BLE2902 *p2902 = (BLE2902*)getDescriptorByUUID((uint16_t)0x2902);
if (p2902 != nullptr && !p2902->getNotifications()) {
   ESP_LOGD(LOG_TAG, "<< notifications disabled; ignoring");
   return;
}

そして、BLE2092.cppの getNotifications() は、以下のようになっている。

bool BLE2902::getNotifications() {
  return (getValue()[0] & (1 << 0)) != 0;
}

つまり、BLE2902のもつデータの1バイト目のビット0がセットされていれば、この関数はtrue を返し送信が行われる。ログの比較にあった [0100]というデータの書き込みの有無がこのデータに影響していると考えたので、以下のようなコードを用意し、BLESecurityCallbacks の onAuthenticationComplete() で auth_cmpl.success == trueのときに呼び出すようにしてみたら、ESP32をリスタートしてもキー入力が送信できるようになった。

if (input) {
  BLE2902 *p2902 = (BLE2902*)(input->getDescriptorByUUID((uint16_t)0x2902));
  if (p2902)
    p2902->setNotifications(true);
}

うまくいくときのログを見ると、onAuthenticationComplete() よりも後ろで 0x42や0x62に対する ESP_GATTS_WRITE_EVT が起きているのだが、適当なコールバック関数がなかったのでここで実行することにした。その後正しい値が書かれても問題ないわけで。

ホストデバイス側から通知が来ないことが問題なのではなくて、ペアリングして信頼関係ができた時点での通知データは、その後同じホストと接続する場合、デバイス側で復元すべきものなのかもしれない。正直、ややこしくてよく分からないことが多い。

ようやく安定してキー入力できるようになったので、ちゃんとしたクラスにしてBluetooth版のほぼNICOLAキーボードにする予定です。