root / hw / bt-hid.c @ 109819e0
History | View | Annotate | Download (15.8 kB)
1 |
/*
|
---|---|
2 |
* QEMU Bluetooth HID Profile wrapper for USB HID.
|
3 |
*
|
4 |
* Copyright (C) 2007-2008 OpenMoko, Inc.
|
5 |
* Written by Andrzej Zaborowski <andrew@openedhand.com>
|
6 |
*
|
7 |
* This program is free software; you can redistribute it and/or
|
8 |
* modify it under the terms of the GNU General Public License as
|
9 |
* published by the Free Software Foundation; either version 2 or
|
10 |
* (at your option) version 3 of the License.
|
11 |
*
|
12 |
* This program is distributed in the hope that it will be useful,
|
13 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15 |
* GNU General Public License for more details.
|
16 |
*
|
17 |
* You should have received a copy of the GNU General Public License along
|
18 |
* with this program; if not, if not, see <http://www.gnu.org/licenses/>.
|
19 |
*/
|
20 |
|
21 |
#include "qemu-common.h" |
22 |
#include "usb.h" |
23 |
#include "bt.h" |
24 |
|
25 |
enum hid_transaction_req {
|
26 |
BT_HANDSHAKE = 0x0,
|
27 |
BT_HID_CONTROL = 0x1,
|
28 |
BT_GET_REPORT = 0x4,
|
29 |
BT_SET_REPORT = 0x5,
|
30 |
BT_GET_PROTOCOL = 0x6,
|
31 |
BT_SET_PROTOCOL = 0x7,
|
32 |
BT_GET_IDLE = 0x8,
|
33 |
BT_SET_IDLE = 0x9,
|
34 |
BT_DATA = 0xa,
|
35 |
BT_DATC = 0xb,
|
36 |
}; |
37 |
|
38 |
enum hid_transaction_handshake {
|
39 |
BT_HS_SUCCESSFUL = 0x0,
|
40 |
BT_HS_NOT_READY = 0x1,
|
41 |
BT_HS_ERR_INVALID_REPORT_ID = 0x2,
|
42 |
BT_HS_ERR_UNSUPPORTED_REQUEST = 0x3,
|
43 |
BT_HS_ERR_INVALID_PARAMETER = 0x4,
|
44 |
BT_HS_ERR_UNKNOWN = 0xe,
|
45 |
BT_HS_ERR_FATAL = 0xf,
|
46 |
}; |
47 |
|
48 |
enum hid_transaction_control {
|
49 |
BT_HC_NOP = 0x0,
|
50 |
BT_HC_HARD_RESET = 0x1,
|
51 |
BT_HC_SOFT_RESET = 0x2,
|
52 |
BT_HC_SUSPEND = 0x3,
|
53 |
BT_HC_EXIT_SUSPEND = 0x4,
|
54 |
BT_HC_VIRTUAL_CABLE_UNPLUG = 0x5,
|
55 |
}; |
56 |
|
57 |
enum hid_protocol {
|
58 |
BT_HID_PROTO_BOOT = 0,
|
59 |
BT_HID_PROTO_REPORT = 1,
|
60 |
}; |
61 |
|
62 |
enum hid_boot_reportid {
|
63 |
BT_HID_BOOT_INVALID = 0,
|
64 |
BT_HID_BOOT_KEYBOARD, |
65 |
BT_HID_BOOT_MOUSE, |
66 |
}; |
67 |
|
68 |
enum hid_data_pkt {
|
69 |
BT_DATA_OTHER = 0,
|
70 |
BT_DATA_INPUT, |
71 |
BT_DATA_OUTPUT, |
72 |
BT_DATA_FEATURE, |
73 |
}; |
74 |
|
75 |
#define BT_HID_MTU 48 |
76 |
|
77 |
/* HID interface requests */
|
78 |
#define GET_REPORT 0xa101 |
79 |
#define GET_IDLE 0xa102 |
80 |
#define GET_PROTOCOL 0xa103 |
81 |
#define SET_REPORT 0x2109 |
82 |
#define SET_IDLE 0x210a |
83 |
#define SET_PROTOCOL 0x210b |
84 |
|
85 |
struct bt_hid_device_s {
|
86 |
struct bt_l2cap_device_s btdev;
|
87 |
struct bt_l2cap_conn_params_s *control;
|
88 |
struct bt_l2cap_conn_params_s *interrupt;
|
89 |
USBDevice *usbdev; |
90 |
|
91 |
int proto;
|
92 |
int connected;
|
93 |
int data_type;
|
94 |
int intr_state;
|
95 |
struct {
|
96 |
int len;
|
97 |
uint8_t buffer[1024];
|
98 |
} dataother, datain, dataout, feature, intrdataout; |
99 |
enum {
|
100 |
bt_state_ready, |
101 |
bt_state_transaction, |
102 |
bt_state_suspend, |
103 |
} state; |
104 |
}; |
105 |
|
106 |
static void bt_hid_reset(struct bt_hid_device_s *s) |
107 |
{ |
108 |
struct bt_scatternet_s *net = s->btdev.device.net;
|
109 |
|
110 |
/* Go as far as... */
|
111 |
bt_l2cap_device_done(&s->btdev); |
112 |
bt_l2cap_device_init(&s->btdev, net); |
113 |
|
114 |
s->usbdev->handle_reset(s->usbdev); |
115 |
s->proto = BT_HID_PROTO_REPORT; |
116 |
s->state = bt_state_ready; |
117 |
s->dataother.len = 0;
|
118 |
s->datain.len = 0;
|
119 |
s->dataout.len = 0;
|
120 |
s->feature.len = 0;
|
121 |
s->intrdataout.len = 0;
|
122 |
s->intr_state = 0;
|
123 |
} |
124 |
|
125 |
static int bt_hid_out(struct bt_hid_device_s *s) |
126 |
{ |
127 |
USBPacket p; |
128 |
|
129 |
if (s->data_type == BT_DATA_OUTPUT) {
|
130 |
p.pid = USB_TOKEN_OUT; |
131 |
p.devep = 1;
|
132 |
p.data = s->dataout.buffer; |
133 |
p.len = s->dataout.len; |
134 |
s->dataout.len = s->usbdev->handle_data(s->usbdev, &p); |
135 |
|
136 |
return s->dataout.len;
|
137 |
} |
138 |
|
139 |
if (s->data_type == BT_DATA_FEATURE) {
|
140 |
/* XXX:
|
141 |
* does this send a USB_REQ_CLEAR_FEATURE/USB_REQ_SET_FEATURE
|
142 |
* or a SET_REPORT? */
|
143 |
p.devep = 0;
|
144 |
} |
145 |
|
146 |
return -1; |
147 |
} |
148 |
|
149 |
static int bt_hid_in(struct bt_hid_device_s *s) |
150 |
{ |
151 |
USBPacket p; |
152 |
|
153 |
p.pid = USB_TOKEN_IN; |
154 |
p.devep = 1;
|
155 |
p.data = s->datain.buffer; |
156 |
p.len = sizeof(s->datain.buffer);
|
157 |
s->datain.len = s->usbdev->handle_data(s->usbdev, &p); |
158 |
|
159 |
return s->datain.len;
|
160 |
} |
161 |
|
162 |
static void bt_hid_send_handshake(struct bt_hid_device_s *s, int result) |
163 |
{ |
164 |
*s->control->sdu_out(s->control, 1) =
|
165 |
(BT_HANDSHAKE << 4) | result;
|
166 |
s->control->sdu_submit(s->control); |
167 |
} |
168 |
|
169 |
static void bt_hid_send_control(struct bt_hid_device_s *s, int operation) |
170 |
{ |
171 |
*s->control->sdu_out(s->control, 1) =
|
172 |
(BT_HID_CONTROL << 4) | operation;
|
173 |
s->control->sdu_submit(s->control); |
174 |
} |
175 |
|
176 |
static void bt_hid_disconnect(struct bt_hid_device_s *s) |
177 |
{ |
178 |
/* Disconnect s->control and s->interrupt */
|
179 |
} |
180 |
|
181 |
static void bt_hid_send_data(struct bt_l2cap_conn_params_s *ch, int type, |
182 |
const uint8_t *data, int len) |
183 |
{ |
184 |
uint8_t *pkt, hdr = (BT_DATA << 4) | type;
|
185 |
int plen;
|
186 |
|
187 |
do {
|
188 |
plen = MIN(len, ch->remote_mtu - 1);
|
189 |
pkt = ch->sdu_out(ch, plen + 1);
|
190 |
|
191 |
pkt[0] = hdr;
|
192 |
if (plen)
|
193 |
memcpy(pkt + 1, data, plen);
|
194 |
ch->sdu_submit(ch); |
195 |
|
196 |
len -= plen; |
197 |
data += plen; |
198 |
hdr = (BT_DATC << 4) | type;
|
199 |
} while (plen == ch->remote_mtu - 1); |
200 |
} |
201 |
|
202 |
static void bt_hid_control_transaction(struct bt_hid_device_s *s, |
203 |
const uint8_t *data, int len) |
204 |
{ |
205 |
uint8_t type, parameter; |
206 |
int rlen, ret = -1; |
207 |
if (len < 1) |
208 |
return;
|
209 |
|
210 |
type = data[0] >> 4; |
211 |
parameter = data[0] & 0xf; |
212 |
|
213 |
switch (type) {
|
214 |
case BT_HANDSHAKE:
|
215 |
case BT_DATA:
|
216 |
switch (parameter) {
|
217 |
default:
|
218 |
/* These are not expected to be sent this direction. */
|
219 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
220 |
} |
221 |
break;
|
222 |
|
223 |
case BT_HID_CONTROL:
|
224 |
if (len != 1 || (parameter != BT_HC_VIRTUAL_CABLE_UNPLUG && |
225 |
s->state == bt_state_transaction)) { |
226 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
227 |
break;
|
228 |
} |
229 |
switch (parameter) {
|
230 |
case BT_HC_NOP:
|
231 |
break;
|
232 |
case BT_HC_HARD_RESET:
|
233 |
case BT_HC_SOFT_RESET:
|
234 |
bt_hid_reset(s); |
235 |
break;
|
236 |
case BT_HC_SUSPEND:
|
237 |
if (s->state == bt_state_ready)
|
238 |
s->state = bt_state_suspend; |
239 |
else
|
240 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
241 |
break;
|
242 |
case BT_HC_EXIT_SUSPEND:
|
243 |
if (s->state == bt_state_suspend)
|
244 |
s->state = bt_state_ready; |
245 |
else
|
246 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
247 |
break;
|
248 |
case BT_HC_VIRTUAL_CABLE_UNPLUG:
|
249 |
bt_hid_disconnect(s); |
250 |
break;
|
251 |
default:
|
252 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
253 |
} |
254 |
break;
|
255 |
|
256 |
case BT_GET_REPORT:
|
257 |
/* No ReportIDs declared. */
|
258 |
if (((parameter & 8) && len != 3) || |
259 |
(!(parameter & 8) && len != 1) || |
260 |
s->state != bt_state_ready) { |
261 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
262 |
break;
|
263 |
} |
264 |
if (parameter & 8) |
265 |
rlen = data[2] | (data[3] << 8); |
266 |
else
|
267 |
rlen = INT_MAX; |
268 |
switch (parameter & 3) { |
269 |
case BT_DATA_OTHER:
|
270 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
271 |
break;
|
272 |
case BT_DATA_INPUT:
|
273 |
/* Here we can as well poll s->usbdev */
|
274 |
bt_hid_send_data(s->control, BT_DATA_INPUT, |
275 |
s->datain.buffer, MIN(rlen, s->datain.len)); |
276 |
break;
|
277 |
case BT_DATA_OUTPUT:
|
278 |
bt_hid_send_data(s->control, BT_DATA_OUTPUT, |
279 |
s->dataout.buffer, MIN(rlen, s->dataout.len)); |
280 |
break;
|
281 |
case BT_DATA_FEATURE:
|
282 |
bt_hid_send_data(s->control, BT_DATA_FEATURE, |
283 |
s->feature.buffer, MIN(rlen, s->feature.len)); |
284 |
break;
|
285 |
} |
286 |
break;
|
287 |
|
288 |
case BT_SET_REPORT:
|
289 |
if (len < 2 || len > BT_HID_MTU || s->state != bt_state_ready || |
290 |
(parameter & 3) == BT_DATA_OTHER ||
|
291 |
(parameter & 3) == BT_DATA_INPUT) {
|
292 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
293 |
break;
|
294 |
} |
295 |
s->data_type = parameter & 3;
|
296 |
if (s->data_type == BT_DATA_OUTPUT) {
|
297 |
s->dataout.len = len - 1;
|
298 |
memcpy(s->dataout.buffer, data + 1, s->dataout.len);
|
299 |
} else {
|
300 |
s->feature.len = len - 1;
|
301 |
memcpy(s->feature.buffer, data + 1, s->feature.len);
|
302 |
} |
303 |
if (len == BT_HID_MTU)
|
304 |
s->state = bt_state_transaction; |
305 |
else
|
306 |
bt_hid_out(s); |
307 |
break;
|
308 |
|
309 |
case BT_GET_PROTOCOL:
|
310 |
if (len != 1 || s->state == bt_state_transaction) { |
311 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
312 |
break;
|
313 |
} |
314 |
*s->control->sdu_out(s->control, 1) = s->proto;
|
315 |
s->control->sdu_submit(s->control); |
316 |
break;
|
317 |
|
318 |
case BT_SET_PROTOCOL:
|
319 |
if (len != 1 || s->state == bt_state_transaction || |
320 |
(parameter != BT_HID_PROTO_BOOT && |
321 |
parameter != BT_HID_PROTO_REPORT)) { |
322 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
323 |
break;
|
324 |
} |
325 |
s->proto = parameter; |
326 |
s->usbdev->handle_control(s->usbdev, SET_PROTOCOL, s->proto, 0, 0, |
327 |
NULL);
|
328 |
ret = BT_HS_SUCCESSFUL; |
329 |
break;
|
330 |
|
331 |
case BT_GET_IDLE:
|
332 |
if (len != 1 || s->state == bt_state_transaction) { |
333 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
334 |
break;
|
335 |
} |
336 |
s->usbdev->handle_control(s->usbdev, GET_IDLE, 0, 0, 1, |
337 |
s->control->sdu_out(s->control, 1));
|
338 |
s->control->sdu_submit(s->control); |
339 |
break;
|
340 |
|
341 |
case BT_SET_IDLE:
|
342 |
if (len != 2 || s->state == bt_state_transaction) { |
343 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
344 |
break;
|
345 |
} |
346 |
|
347 |
/* We don't need to know about the Idle Rate here really,
|
348 |
* so just pass it on to the device. */
|
349 |
ret = s->usbdev->handle_control(s->usbdev, |
350 |
SET_IDLE, data[1], 0, 0, NULL) ? |
351 |
BT_HS_SUCCESSFUL : BT_HS_ERR_INVALID_PARAMETER; |
352 |
/* XXX: Does this generate a handshake? */
|
353 |
break;
|
354 |
|
355 |
case BT_DATC:
|
356 |
if (len > BT_HID_MTU || s->state != bt_state_transaction) {
|
357 |
ret = BT_HS_ERR_INVALID_PARAMETER; |
358 |
break;
|
359 |
} |
360 |
if (s->data_type == BT_DATA_OUTPUT) {
|
361 |
memcpy(s->dataout.buffer + s->dataout.len, data + 1, len - 1); |
362 |
s->dataout.len += len - 1;
|
363 |
} else {
|
364 |
memcpy(s->feature.buffer + s->feature.len, data + 1, len - 1); |
365 |
s->feature.len += len - 1;
|
366 |
} |
367 |
if (len < BT_HID_MTU) {
|
368 |
bt_hid_out(s); |
369 |
s->state = bt_state_ready; |
370 |
} |
371 |
break;
|
372 |
|
373 |
default:
|
374 |
ret = BT_HS_ERR_UNSUPPORTED_REQUEST; |
375 |
} |
376 |
|
377 |
if (ret != -1) |
378 |
bt_hid_send_handshake(s, ret); |
379 |
} |
380 |
|
381 |
static void bt_hid_control_sdu(void *opaque, const uint8_t *data, int len) |
382 |
{ |
383 |
struct bt_hid_device_s *hid = opaque;
|
384 |
|
385 |
bt_hid_control_transaction(hid, data, len); |
386 |
} |
387 |
|
388 |
static void bt_hid_datain(void *opaque) |
389 |
{ |
390 |
struct bt_hid_device_s *hid = opaque;
|
391 |
|
392 |
/* If suspended, wake-up and send a wake-up event first. We might
|
393 |
* want to also inspect the input report and ignore event like
|
394 |
* mouse movements until a button event occurs. */
|
395 |
if (hid->state == bt_state_suspend) {
|
396 |
hid->state = bt_state_ready; |
397 |
} |
398 |
|
399 |
if (bt_hid_in(hid) > 0) |
400 |
/* TODO: when in boot-mode precede any Input reports with the ReportID
|
401 |
* byte, here and in GetReport/SetReport on the Control channel. */
|
402 |
bt_hid_send_data(hid->interrupt, BT_DATA_INPUT, |
403 |
hid->datain.buffer, hid->datain.len); |
404 |
} |
405 |
|
406 |
static void bt_hid_interrupt_sdu(void *opaque, const uint8_t *data, int len) |
407 |
{ |
408 |
struct bt_hid_device_s *hid = opaque;
|
409 |
|
410 |
if (len > BT_HID_MTU || len < 1) |
411 |
goto bad;
|
412 |
if ((data[0] & 3) != BT_DATA_OUTPUT) |
413 |
goto bad;
|
414 |
if ((data[0] >> 4) == BT_DATA) { |
415 |
if (hid->intr_state)
|
416 |
goto bad;
|
417 |
|
418 |
hid->data_type = BT_DATA_OUTPUT; |
419 |
hid->intrdataout.len = 0;
|
420 |
} else if ((data[0] >> 4) == BT_DATC) { |
421 |
if (!hid->intr_state)
|
422 |
goto bad;
|
423 |
} else
|
424 |
goto bad;
|
425 |
|
426 |
memcpy(hid->intrdataout.buffer + hid->intrdataout.len, data + 1, len - 1); |
427 |
hid->intrdataout.len += len - 1;
|
428 |
hid->intr_state = (len == BT_HID_MTU); |
429 |
if (!hid->intr_state) {
|
430 |
memcpy(hid->dataout.buffer, hid->intrdataout.buffer, |
431 |
hid->dataout.len = hid->intrdataout.len); |
432 |
bt_hid_out(hid); |
433 |
} |
434 |
|
435 |
return;
|
436 |
bad:
|
437 |
fprintf(stderr, "%s: bad transaction on Interrupt channel.\n",
|
438 |
__FUNCTION__); |
439 |
} |
440 |
|
441 |
/* "Virtual cable" plug/unplug event. */
|
442 |
static void bt_hid_connected_update(struct bt_hid_device_s *hid) |
443 |
{ |
444 |
int prev = hid->connected;
|
445 |
|
446 |
hid->connected = hid->control && hid->interrupt; |
447 |
|
448 |
/* Stop page-/inquiry-scanning when a host is connected. */
|
449 |
hid->btdev.device.page_scan = !hid->connected; |
450 |
hid->btdev.device.inquiry_scan = !hid->connected; |
451 |
|
452 |
if (hid->connected && !prev) {
|
453 |
hid->usbdev->handle_reset(hid->usbdev); |
454 |
hid->proto = BT_HID_PROTO_REPORT; |
455 |
} |
456 |
|
457 |
/* Should set HIDVirtualCable in SDP (possibly need to check that SDP
|
458 |
* isn't destroyed yet, in case we're being called from handle_destroy) */
|
459 |
} |
460 |
|
461 |
static void bt_hid_close_control(void *opaque) |
462 |
{ |
463 |
struct bt_hid_device_s *hid = opaque;
|
464 |
|
465 |
hid->control = NULL;
|
466 |
bt_hid_connected_update(hid); |
467 |
} |
468 |
|
469 |
static void bt_hid_close_interrupt(void *opaque) |
470 |
{ |
471 |
struct bt_hid_device_s *hid = opaque;
|
472 |
|
473 |
hid->interrupt = NULL;
|
474 |
bt_hid_connected_update(hid); |
475 |
} |
476 |
|
477 |
static int bt_hid_new_control_ch(struct bt_l2cap_device_s *dev, |
478 |
struct bt_l2cap_conn_params_s *params)
|
479 |
{ |
480 |
struct bt_hid_device_s *hid = (struct bt_hid_device_s *) dev; |
481 |
|
482 |
if (hid->control)
|
483 |
return 1; |
484 |
|
485 |
hid->control = params; |
486 |
hid->control->opaque = hid; |
487 |
hid->control->close = bt_hid_close_control; |
488 |
hid->control->sdu_in = bt_hid_control_sdu; |
489 |
|
490 |
bt_hid_connected_update(hid); |
491 |
|
492 |
return 0; |
493 |
} |
494 |
|
495 |
static int bt_hid_new_interrupt_ch(struct bt_l2cap_device_s *dev, |
496 |
struct bt_l2cap_conn_params_s *params)
|
497 |
{ |
498 |
struct bt_hid_device_s *hid = (struct bt_hid_device_s *) dev; |
499 |
|
500 |
if (hid->interrupt)
|
501 |
return 1; |
502 |
|
503 |
hid->interrupt = params; |
504 |
hid->interrupt->opaque = hid; |
505 |
hid->interrupt->close = bt_hid_close_interrupt; |
506 |
hid->interrupt->sdu_in = bt_hid_interrupt_sdu; |
507 |
|
508 |
bt_hid_connected_update(hid); |
509 |
|
510 |
return 0; |
511 |
} |
512 |
|
513 |
static void bt_hid_destroy(struct bt_device_s *dev) |
514 |
{ |
515 |
struct bt_hid_device_s *hid = (struct bt_hid_device_s *) dev; |
516 |
|
517 |
if (hid->connected)
|
518 |
bt_hid_send_control(hid, BT_HC_VIRTUAL_CABLE_UNPLUG); |
519 |
bt_l2cap_device_done(&hid->btdev); |
520 |
|
521 |
hid->usbdev->handle_destroy(hid->usbdev); |
522 |
|
523 |
qemu_free(hid); |
524 |
} |
525 |
|
526 |
enum peripheral_minor_class {
|
527 |
class_other = 0 << 4, |
528 |
class_keyboard = 1 << 4, |
529 |
class_pointing = 2 << 4, |
530 |
class_combo = 3 << 4, |
531 |
}; |
532 |
|
533 |
static struct bt_device_s *bt_hid_init(struct bt_scatternet_s *net, |
534 |
USBDevice *dev, enum peripheral_minor_class minor)
|
535 |
{ |
536 |
struct bt_hid_device_s *s = qemu_mallocz(sizeof(*s)); |
537 |
uint32_t class = |
538 |
/* Format type */
|
539 |
(0 << 0) | |
540 |
/* Device class */
|
541 |
(minor << 2) |
|
542 |
(5 << 8) | /* "Peripheral" */ |
543 |
/* Service classes */
|
544 |
(1 << 13) | /* Limited discoverable mode */ |
545 |
(1 << 19); /* Capturing device (?) */ |
546 |
|
547 |
bt_l2cap_device_init(&s->btdev, net); |
548 |
bt_l2cap_sdp_init(&s->btdev); |
549 |
bt_l2cap_psm_register(&s->btdev, BT_PSM_HID_CTRL, |
550 |
BT_HID_MTU, bt_hid_new_control_ch); |
551 |
bt_l2cap_psm_register(&s->btdev, BT_PSM_HID_INTR, |
552 |
BT_HID_MTU, bt_hid_new_interrupt_ch); |
553 |
|
554 |
s->usbdev = dev; |
555 |
s->btdev.device.lmp_name = s->usbdev->devname; |
556 |
usb_hid_datain_cb(s->usbdev, s, bt_hid_datain); |
557 |
|
558 |
s->btdev.device.handle_destroy = bt_hid_destroy; |
559 |
|
560 |
s->btdev.device.class[0] = (class >> 0) & 0xff; |
561 |
s->btdev.device.class[1] = (class >> 8) & 0xff; |
562 |
s->btdev.device.class[2] = (class >> 16) & 0xff; |
563 |
|
564 |
return &s->btdev.device;
|
565 |
} |
566 |
|
567 |
struct bt_device_s *bt_keyboard_init(struct bt_scatternet_s *net) |
568 |
{ |
569 |
return bt_hid_init(net, usb_keyboard_init(), class_keyboard);
|
570 |
} |