Repository navigation
Expand file tree
/
Copy pathhid_remote.cpp
More file actions
322 lines (284 loc) · 10.5 KB
/
Copy pathhid_remote.cpp
File metadata and controls
322 lines (284 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
// HidRemote 実装(ステップ4)。
// NimBLE-Arduino v2 系の複合 HID(キーボード + マウス)。
// v1 系とは API が異なる: setReportMap/getInputReport/advertiseOnDisconnect、
// コールバックは NimBLEConnInfo& を取るシグネチャになっている。
// (v2.5.0 時点では NimBLEDevice::getDeviceName() や
// NimBLECharacteristic::getSubscribedCount() は存在しない)
#include "hid_remote.hpp"
#include <M5Unified.h>
namespace {
// 複合 Report Map: キーボード(Report ID 1)+ マウス(Report ID 2)。
// マウスは将来の IMU 空中マウス用にマップ定義のみ(送信は sendMouseClick)。
constexpr uint8_t REPORT_MAP[] = {
// ---- キーボード (Report ID 1) ----
0x05,
0x01, // Usage Page (Generic Desktop)
0x09,
0x06, // Usage (Keyboard)
0xA1,
0x01, // Collection (Application)
0x85,
0x01, // Report ID (1)
0x05,
0x07, // Usage Page (Keyboard/Keypad)
0x19,
0xE0, // Usage Minimum (Left Control)
0x29,
0xE7, // Usage Maximum (Right GUI)
0x15,
0x00, // Logical Minimum (0)
0x25,
0x01, // Logical Maximum (1)
0x75,
0x01, // Report Size (1)
0x95,
0x08, // Report Count (8)
0x81,
0x02, // Input (Data, Var, Abs) : 修飾キー 8bit
0x95,
0x01, // Report Count (1)
0x75,
0x08, // Report Size (8)
0x81,
0x01, // Input (Const) : 予約 1byte
0x95,
0x05, // Report Count (5)
0x75,
0x01, // Report Size (1)
0x05,
0x08, // Usage Page (LEDs)
0x19,
0x01, // Usage Minimum (Num Lock)
0x29,
0x05, // Usage Maximum (Kana)
0x91,
0x02, // Output (Data, Var, Abs) : LED 5bit
0x95,
0x01, // Report Count (1)
0x75,
0x03, // Report Size (3)
0x91,
0x01, // Output (Const) : パディング 3bit
0x95,
0x06, // Report Count (6)
0x75,
0x08, // Report Size (8)
0x15,
0x00, // Logical Minimum (0)
0x25,
0x65, // Logical Maximum (0x65)
0x05,
0x07, // Usage Page (Keyboard/Keypad)
0x19,
0x00, // Usage Minimum (0)
0x29,
0x65, // Usage Maximum (0x65)
0x81,
0x00, // Input (Data, Array) : 同時押し 6 キー
0xC0, // End Collection
// ---- マウス (Report ID 2) ----
0x05,
0x01, // Usage Page (Generic Desktop)
0x09,
0x02, // Usage (Mouse)
0xA1,
0x01, // Collection (Application)
0x85,
0x02, // Report ID (2)
0x09,
0x01, // Usage (Pointer)
0xA1,
0x00, // Collection (Physical)
0x05,
0x09, // Usage Page (Buttons)
0x19,
0x01, // Usage Minimum (1)
0x29,
0x03, // Usage Maximum (3)
0x15,
0x00, // Logical Minimum (0)
0x25,
0x01, // Logical Maximum (1)
0x95,
0x03, // Report Count (3)
0x75,
0x01, // Report Size (1)
0x81,
0x02, // Input (Data, Var, Abs) : ボタン 3bit
0x95,
0x01, // Report Count (1)
0x75,
0x05, // Report Size (5)
0x81,
0x03, // Input (Const) : パディング 5bit
0x05,
0x01, // Usage Page (Generic Desktop)
0x09,
0x30, // Usage (X)
0x09,
0x31, // Usage (Y)
0x09,
0x38, // Usage (Wheel)
0x15,
0x81, // Logical Minimum (-127)
0x25,
0x7F, // Logical Maximum (127)
0x75,
0x08, // Report Size (8)
0x95,
0x03, // Report Count (3)
0x81,
0x06, // Input (Data, Var, Rel) : X / Y / ホイール
0xC0, // End Collection
0xC0, // End Collection
};
// キーボードレポート: [修飾キー, 予約, キー x6]
constexpr size_t KEYBOARD_REPORT_SIZE = 8;
// マウスレポート: [ボタン, X, Y, ホイール]
constexpr size_t MOUSE_REPORT_SIZE = 4;
// GAP Appearance: 961 = HID Keyboard(ホスト側でキーボードとして認識させる)
constexpr uint16_t APPEARANCE_HID_KEYBOARD = 0x03C1;
} // namespace
// 接続/切断コールバック。切断時はアドバタイジングを再開して再接続を待つ。
class HidRemote::ServerCallbacks : public NimBLEServerCallbacks {
public:
void onConnect(NimBLEServer* server, NimBLEConnInfo& connInfo) override {
M5_LOGI("HidRemote: connected (%s)",
connInfo.getAddress().toString().c_str());
}
void onDisconnect(NimBLEServer* server, NimBLEConnInfo& connInfo,
int reason) override {
M5_LOGI("HidRemote: disconnected (reason=%d), restart advertising",
reason);
NimBLEDevice::startAdvertising();
}
void onAuthenticationComplete(NimBLEConnInfo& connInfo) override {
M5_LOGI("HidRemote: auth complete (encrypted=%d bonded=%d)",
connInfo.isEncrypted(), connInfo.isBonded());
}
};
void HidRemote::begin(const char* deviceName) {
this->_deviceName = deviceName;
NimBLEDevice::init(deviceName);
// ボンディング有効(プレゼン本番で毎回ペアリングは避けたい)。
// 画面はあるが入力手段としては使わないので Just Works(MITM なし)。
NimBLEDevice::setSecurityAuth(/*bonding=*/true, /*mitm=*/false,
/*sc=*/true);
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
this->_server = NimBLEDevice::createServer();
this->_serverCallbacks = new ServerCallbacks();
this->_server->setCallbacks(this->_serverCallbacks);
this->_server->advertiseOnDisconnect(true);
this->_hid = new NimBLEHIDDevice(this->_server);
this->_hid->setManufacturer("M5Stack");
// sig=0x02 (USB Implementer's Forum), VID/PID は Espressif のものを流用
this->_hid->setPnp(0x02, 0x303A, 0x0001, 0x0100);
// country=0, flags=0x02 (NormallyConnectable)
this->_hid->setHidInfo(0x00, 0x02);
this->_hid->setReportMap(const_cast<uint8_t*>(REPORT_MAP),
sizeof(REPORT_MAP));
this->_keyboardInput = this->_hid->getInputReport(REPORT_ID_KEYBOARD);
this->_mouseInput = this->_hid->getInputReport(REPORT_ID_MOUSE);
this->_hid->getOutputReport(REPORT_ID_KEYBOARD); // LED 出力の受け皿
// ホストは購読直後に現在値を読むことがあるため、正しいサイズの初期値を入れる
const uint8_t zeroKeyboard[KEYBOARD_REPORT_SIZE] = {0};
this->_keyboardInput->setValue(zeroKeyboard, sizeof(zeroKeyboard));
const uint8_t zeroMouse[MOUSE_REPORT_SIZE] = {0};
this->_mouseInput->setValue(zeroMouse, sizeof(zeroMouse));
this->_hid->setBatteryLevel(100);
// v2 では startServices() は非推奨: サーバーの start()
// が全サービスを開始する
this->_server->start();
this->startAdvertising();
M5_LOGI("HidRemote: advertising as \"%s\"", deviceName);
}
void HidRemote::update() {
// 保留中の release レポートを期限が来たら送る(非ブロッキング)
if (this->_releasePending &&
(int32_t)(millis() - this->_releaseAtMs) >= 0) {
this->sendKeyRelease();
}
}
bool HidRemote::isConnected() const {
return this->_server != nullptr && this->_server->getConnectedCount() > 0;
}
void HidRemote::setKeys(uint8_t nextUsage, uint8_t prevUsage) {
this->_nextUsage = nextUsage;
this->_prevUsage = prevUsage;
}
void HidRemote::sendNext() {
this->sendKeyTap(this->_nextUsage);
}
void HidRemote::sendPrev() {
this->sendKeyTap(this->_prevUsage);
}
void HidRemote::sendKeyTap(uint8_t usage) {
if (!this->isConnected() || this->_keyboardInput == nullptr) {
return;
}
// 前のタップの release がまだなら先に送って確定させる
if (this->_releasePending) {
this->sendKeyRelease();
}
uint8_t report[KEYBOARD_REPORT_SIZE] = {0};
report[2] = usage;
this->_keyboardInput->setValue(report, sizeof(report));
this->_keyboardInput->notify();
// 実機検証で判明: release を press 直後(<1ms)に送ると両通知が同一
// コネクションイベントに乗り、macOS がキー入力として認識しない
// (接続直後の1回だけ認識される、という紛らわしい振る舞いになる)。
// release は update() が KEY_RELEASE_DELAY_MS 経過後に送る
this->_releasePending = true;
this->_releaseAtMs = millis() + KEY_RELEASE_DELAY_MS;
}
void HidRemote::sendKeyRelease() {
this->_releasePending = false;
if (!this->isConnected() || this->_keyboardInput == nullptr) {
return;
}
uint8_t report[KEYBOARD_REPORT_SIZE] = {0};
this->_keyboardInput->setValue(report, sizeof(report));
this->_keyboardInput->notify();
}
void HidRemote::sendMouseClick(uint8_t buttonMask) {
if (!this->isConnected() || this->_mouseInput == nullptr) {
return;
}
// press レポート → release レポートの順で送る(移動量ゼロ)。
// キーボードと同じ理由で press/release の間隔を空ける。
// 将来 IMU 空中マウスを実装するときは非ブロッキング化を検討すること
uint8_t report[MOUSE_REPORT_SIZE] = {0};
report[0] = buttonMask;
this->_mouseInput->setValue(report, sizeof(report));
this->_mouseInput->notify();
delay(KEY_RELEASE_DELAY_MS);
memset(report, 0, sizeof(report));
this->_mouseInput->setValue(report, sizeof(report));
this->_mouseInput->notify();
}
void HidRemote::setBatteryLevel(uint8_t percent) {
if (this->_hid == nullptr) {
return;
}
if (percent > 100) {
percent = 100;
}
this->_hid->setBatteryLevel(percent, this->isConnected());
}
void HidRemote::startAdvertising() {
NimBLEAdvertising* advertising = NimBLEDevice::getAdvertising();
// アドバタイジングパケットは 31 バイト上限。appearance + 16bit UUID +
// 名前(デバイス名 + ヘッダ 2 バイト)を 1 パケットに
// 同居させると上限を超えて名前が載らず、スキャナ側で「Bluetooth デバイス」
// 等の無名表示になる(実機で確認)。名前はスキャンレスポンス側に分離する。
NimBLEAdvertisementData advData;
advData.setFlags(BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP);
advData.setAppearance(APPEARANCE_HID_KEYBOARD);
advData.addServiceUUID(this->_hid->getHidService()->getUUID());
NimBLEAdvertisementData scanData;
scanData.setName(this->_deviceName);
advertising->enableScanResponse(true);
advertising->setAdvertisementData(advData);
advertising->setScanResponseData(scanData);
advertising->start();
}