diff options
Diffstat (limited to 'hw/bt/hid.c')
-rw-r--r-- | hw/bt/hid.c | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/hw/bt/hid.c b/hw/bt/hid.c new file mode 100644 index 0000000000..af494e1e06 --- /dev/null +++ b/hw/bt/hid.c @@ -0,0 +1,553 @@ +/* + * QEMU Bluetooth HID Profile wrapper for USB HID. + * + * Copyright (C) 2007-2008 OpenMoko, Inc. + * Written by Andrzej Zaborowski <andrew@openedhand.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 or + * (at your option) version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, if not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu-common.h" +#include "qemu/timer.h" +#include "ui/console.h" +#include "hw/input/hid.h" +#include "hw/bt.h" + +enum hid_transaction_req { + BT_HANDSHAKE = 0x0, + BT_HID_CONTROL = 0x1, + BT_GET_REPORT = 0x4, + BT_SET_REPORT = 0x5, + BT_GET_PROTOCOL = 0x6, + BT_SET_PROTOCOL = 0x7, + BT_GET_IDLE = 0x8, + BT_SET_IDLE = 0x9, + BT_DATA = 0xa, + BT_DATC = 0xb, +}; + +enum hid_transaction_handshake { + BT_HS_SUCCESSFUL = 0x0, + BT_HS_NOT_READY = 0x1, + BT_HS_ERR_INVALID_REPORT_ID = 0x2, + BT_HS_ERR_UNSUPPORTED_REQUEST = 0x3, + BT_HS_ERR_INVALID_PARAMETER = 0x4, + BT_HS_ERR_UNKNOWN = 0xe, + BT_HS_ERR_FATAL = 0xf, +}; + +enum hid_transaction_control { + BT_HC_NOP = 0x0, + BT_HC_HARD_RESET = 0x1, + BT_HC_SOFT_RESET = 0x2, + BT_HC_SUSPEND = 0x3, + BT_HC_EXIT_SUSPEND = 0x4, + BT_HC_VIRTUAL_CABLE_UNPLUG = 0x5, +}; + +enum hid_protocol { + BT_HID_PROTO_BOOT = 0, + BT_HID_PROTO_REPORT = 1, +}; + +enum hid_boot_reportid { + BT_HID_BOOT_INVALID = 0, + BT_HID_BOOT_KEYBOARD, + BT_HID_BOOT_MOUSE, +}; + +enum hid_data_pkt { + BT_DATA_OTHER = 0, + BT_DATA_INPUT, + BT_DATA_OUTPUT, + BT_DATA_FEATURE, +}; + +#define BT_HID_MTU 48 + +/* HID interface requests */ +#define GET_REPORT 0xa101 +#define GET_IDLE 0xa102 +#define GET_PROTOCOL 0xa103 +#define SET_REPORT 0x2109 +#define SET_IDLE 0x210a +#define SET_PROTOCOL 0x210b + +struct bt_hid_device_s { + struct bt_l2cap_device_s btdev; + struct bt_l2cap_conn_params_s *control; + struct bt_l2cap_conn_params_s *interrupt; + HIDState hid; + + int proto; + int connected; + int data_type; + int intr_state; + struct { + int len; + uint8_t buffer[1024]; + } dataother, datain, dataout, feature, intrdataout; + enum { + bt_state_ready, + bt_state_transaction, + bt_state_suspend, + } state; +}; + +static void bt_hid_reset(struct bt_hid_device_s *s) +{ + struct bt_scatternet_s *net = s->btdev.device.net; + + /* Go as far as... */ + bt_l2cap_device_done(&s->btdev); + bt_l2cap_device_init(&s->btdev, net); + + hid_reset(&s->hid); + s->proto = BT_HID_PROTO_REPORT; + s->state = bt_state_ready; + s->dataother.len = 0; + s->datain.len = 0; + s->dataout.len = 0; + s->feature.len = 0; + s->intrdataout.len = 0; + s->intr_state = 0; +} + +static int bt_hid_out(struct bt_hid_device_s *s) +{ + if (s->data_type == BT_DATA_OUTPUT) { + /* nothing */ + ; + } + + if (s->data_type == BT_DATA_FEATURE) { + /* XXX: + * does this send a USB_REQ_CLEAR_FEATURE/USB_REQ_SET_FEATURE + * or a SET_REPORT? */ + ; + } + + return -1; +} + +static int bt_hid_in(struct bt_hid_device_s *s) +{ + s->datain.len = hid_keyboard_poll(&s->hid, s->datain.buffer, + sizeof(s->datain.buffer)); + return s->datain.len; +} + +static void bt_hid_send_handshake(struct bt_hid_device_s *s, int result) +{ + *s->control->sdu_out(s->control, 1) = + (BT_HANDSHAKE << 4) | result; + s->control->sdu_submit(s->control); +} + +static void bt_hid_send_control(struct bt_hid_device_s *s, int operation) +{ + *s->control->sdu_out(s->control, 1) = + (BT_HID_CONTROL << 4) | operation; + s->control->sdu_submit(s->control); +} + +static void bt_hid_disconnect(struct bt_hid_device_s *s) +{ + /* Disconnect s->control and s->interrupt */ +} + +static void bt_hid_send_data(struct bt_l2cap_conn_params_s *ch, int type, + const uint8_t *data, int len) +{ + uint8_t *pkt, hdr = (BT_DATA << 4) | type; + int plen; + + do { + plen = MIN(len, ch->remote_mtu - 1); + pkt = ch->sdu_out(ch, plen + 1); + + pkt[0] = hdr; + if (plen) + memcpy(pkt + 1, data, plen); + ch->sdu_submit(ch); + + len -= plen; + data += plen; + hdr = (BT_DATC << 4) | type; + } while (plen == ch->remote_mtu - 1); +} + +static void bt_hid_control_transaction(struct bt_hid_device_s *s, + const uint8_t *data, int len) +{ + uint8_t type, parameter; + int rlen, ret = -1; + if (len < 1) + return; + + type = data[0] >> 4; + parameter = data[0] & 0xf; + + switch (type) { + case BT_HANDSHAKE: + case BT_DATA: + switch (parameter) { + default: + /* These are not expected to be sent this direction. */ + ret = BT_HS_ERR_INVALID_PARAMETER; + } + break; + + case BT_HID_CONTROL: + if (len != 1 || (parameter != BT_HC_VIRTUAL_CABLE_UNPLUG && + s->state == bt_state_transaction)) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + switch (parameter) { + case BT_HC_NOP: + break; + case BT_HC_HARD_RESET: + case BT_HC_SOFT_RESET: + bt_hid_reset(s); + break; + case BT_HC_SUSPEND: + if (s->state == bt_state_ready) + s->state = bt_state_suspend; + else + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + case BT_HC_EXIT_SUSPEND: + if (s->state == bt_state_suspend) + s->state = bt_state_ready; + else + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + case BT_HC_VIRTUAL_CABLE_UNPLUG: + bt_hid_disconnect(s); + break; + default: + ret = BT_HS_ERR_INVALID_PARAMETER; + } + break; + + case BT_GET_REPORT: + /* No ReportIDs declared. */ + if (((parameter & 8) && len != 3) || + (!(parameter & 8) && len != 1) || + s->state != bt_state_ready) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + if (parameter & 8) + rlen = data[2] | (data[3] << 8); + else + rlen = INT_MAX; + switch (parameter & 3) { + case BT_DATA_OTHER: + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + case BT_DATA_INPUT: + /* Here we can as well poll s->usbdev */ + bt_hid_send_data(s->control, BT_DATA_INPUT, + s->datain.buffer, MIN(rlen, s->datain.len)); + break; + case BT_DATA_OUTPUT: + bt_hid_send_data(s->control, BT_DATA_OUTPUT, + s->dataout.buffer, MIN(rlen, s->dataout.len)); + break; + case BT_DATA_FEATURE: + bt_hid_send_data(s->control, BT_DATA_FEATURE, + s->feature.buffer, MIN(rlen, s->feature.len)); + break; + } + break; + + case BT_SET_REPORT: + if (len < 2 || len > BT_HID_MTU || s->state != bt_state_ready || + (parameter & 3) == BT_DATA_OTHER || + (parameter & 3) == BT_DATA_INPUT) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + s->data_type = parameter & 3; + if (s->data_type == BT_DATA_OUTPUT) { + s->dataout.len = len - 1; + memcpy(s->dataout.buffer, data + 1, s->dataout.len); + } else { + s->feature.len = len - 1; + memcpy(s->feature.buffer, data + 1, s->feature.len); + } + if (len == BT_HID_MTU) + s->state = bt_state_transaction; + else + bt_hid_out(s); + break; + + case BT_GET_PROTOCOL: + if (len != 1 || s->state == bt_state_transaction) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + *s->control->sdu_out(s->control, 1) = s->proto; + s->control->sdu_submit(s->control); + break; + + case BT_SET_PROTOCOL: + if (len != 1 || s->state == bt_state_transaction || + (parameter != BT_HID_PROTO_BOOT && + parameter != BT_HID_PROTO_REPORT)) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + s->proto = parameter; + s->hid.protocol = parameter; + ret = BT_HS_SUCCESSFUL; + break; + + case BT_GET_IDLE: + if (len != 1 || s->state == bt_state_transaction) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + *s->control->sdu_out(s->control, 1) = s->hid.idle; + s->control->sdu_submit(s->control); + break; + + case BT_SET_IDLE: + if (len != 2 || s->state == bt_state_transaction) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + + s->hid.idle = data[1]; + /* XXX: Does this generate a handshake? */ + break; + + case BT_DATC: + if (len > BT_HID_MTU || s->state != bt_state_transaction) { + ret = BT_HS_ERR_INVALID_PARAMETER; + break; + } + if (s->data_type == BT_DATA_OUTPUT) { + memcpy(s->dataout.buffer + s->dataout.len, data + 1, len - 1); + s->dataout.len += len - 1; + } else { + memcpy(s->feature.buffer + s->feature.len, data + 1, len - 1); + s->feature.len += len - 1; + } + if (len < BT_HID_MTU) { + bt_hid_out(s); + s->state = bt_state_ready; + } + break; + + default: + ret = BT_HS_ERR_UNSUPPORTED_REQUEST; + } + + if (ret != -1) + bt_hid_send_handshake(s, ret); +} + +static void bt_hid_control_sdu(void *opaque, const uint8_t *data, int len) +{ + struct bt_hid_device_s *hid = opaque; + + bt_hid_control_transaction(hid, data, len); +} + +static void bt_hid_datain(HIDState *hs) +{ + struct bt_hid_device_s *hid = + container_of(hs, struct bt_hid_device_s, hid); + + /* If suspended, wake-up and send a wake-up event first. We might + * want to also inspect the input report and ignore event like + * mouse movements until a button event occurs. */ + if (hid->state == bt_state_suspend) { + hid->state = bt_state_ready; + } + + if (bt_hid_in(hid) > 0) + /* TODO: when in boot-mode precede any Input reports with the ReportID + * byte, here and in GetReport/SetReport on the Control channel. */ + bt_hid_send_data(hid->interrupt, BT_DATA_INPUT, + hid->datain.buffer, hid->datain.len); +} + +static void bt_hid_interrupt_sdu(void *opaque, const uint8_t *data, int len) +{ + struct bt_hid_device_s *hid = opaque; + + if (len > BT_HID_MTU || len < 1) + goto bad; + if ((data[0] & 3) != BT_DATA_OUTPUT) + goto bad; + if ((data[0] >> 4) == BT_DATA) { + if (hid->intr_state) + goto bad; + + hid->data_type = BT_DATA_OUTPUT; + hid->intrdataout.len = 0; + } else if ((data[0] >> 4) == BT_DATC) { + if (!hid->intr_state) + goto bad; + } else + goto bad; + + memcpy(hid->intrdataout.buffer + hid->intrdataout.len, data + 1, len - 1); + hid->intrdataout.len += len - 1; + hid->intr_state = (len == BT_HID_MTU); + if (!hid->intr_state) { + memcpy(hid->dataout.buffer, hid->intrdataout.buffer, + hid->dataout.len = hid->intrdataout.len); + bt_hid_out(hid); + } + + return; +bad: + fprintf(stderr, "%s: bad transaction on Interrupt channel.\n", + __FUNCTION__); +} + +/* "Virtual cable" plug/unplug event. */ +static void bt_hid_connected_update(struct bt_hid_device_s *hid) +{ + int prev = hid->connected; + + hid->connected = hid->control && hid->interrupt; + + /* Stop page-/inquiry-scanning when a host is connected. */ + hid->btdev.device.page_scan = !hid->connected; + hid->btdev.device.inquiry_scan = !hid->connected; + + if (hid->connected && !prev) { + hid_reset(&hid->hid); + hid->proto = BT_HID_PROTO_REPORT; + } + + /* Should set HIDVirtualCable in SDP (possibly need to check that SDP + * isn't destroyed yet, in case we're being called from handle_destroy) */ +} + +static void bt_hid_close_control(void *opaque) +{ + struct bt_hid_device_s *hid = opaque; + + hid->control = NULL; + bt_hid_connected_update(hid); +} + +static void bt_hid_close_interrupt(void *opaque) +{ + struct bt_hid_device_s *hid = opaque; + + hid->interrupt = NULL; + bt_hid_connected_update(hid); +} + +static int bt_hid_new_control_ch(struct bt_l2cap_device_s *dev, + struct bt_l2cap_conn_params_s *params) +{ + struct bt_hid_device_s *hid = (struct bt_hid_device_s *) dev; + + if (hid->control) + return 1; + + hid->control = params; + hid->control->opaque = hid; + hid->control->close = bt_hid_close_control; + hid->control->sdu_in = bt_hid_control_sdu; + + bt_hid_connected_update(hid); + + return 0; +} + +static int bt_hid_new_interrupt_ch(struct bt_l2cap_device_s *dev, + struct bt_l2cap_conn_params_s *params) +{ + struct bt_hid_device_s *hid = (struct bt_hid_device_s *) dev; + + if (hid->interrupt) + return 1; + + hid->interrupt = params; + hid->interrupt->opaque = hid; + hid->interrupt->close = bt_hid_close_interrupt; + hid->interrupt->sdu_in = bt_hid_interrupt_sdu; + + bt_hid_connected_update(hid); + + return 0; +} + +static void bt_hid_destroy(struct bt_device_s *dev) +{ + struct bt_hid_device_s *hid = (struct bt_hid_device_s *) dev; + + if (hid->connected) + bt_hid_send_control(hid, BT_HC_VIRTUAL_CABLE_UNPLUG); + bt_l2cap_device_done(&hid->btdev); + + hid_free(&hid->hid); + + g_free(hid); +} + +enum peripheral_minor_class { + class_other = 0 << 4, + class_keyboard = 1 << 4, + class_pointing = 2 << 4, + class_combo = 3 << 4, +}; + +static struct bt_device_s *bt_hid_init(struct bt_scatternet_s *net, + enum peripheral_minor_class minor) +{ + struct bt_hid_device_s *s = g_malloc0(sizeof(*s)); + uint32_t class = + /* Format type */ + (0 << 0) | + /* Device class */ + (minor << 2) | + (5 << 8) | /* "Peripheral" */ + /* Service classes */ + (1 << 13) | /* Limited discoverable mode */ + (1 << 19); /* Capturing device (?) */ + + bt_l2cap_device_init(&s->btdev, net); + bt_l2cap_sdp_init(&s->btdev); + bt_l2cap_psm_register(&s->btdev, BT_PSM_HID_CTRL, + BT_HID_MTU, bt_hid_new_control_ch); + bt_l2cap_psm_register(&s->btdev, BT_PSM_HID_INTR, + BT_HID_MTU, bt_hid_new_interrupt_ch); + + hid_init(&s->hid, HID_KEYBOARD, bt_hid_datain); + s->btdev.device.lmp_name = "BT Keyboard"; + + s->btdev.device.handle_destroy = bt_hid_destroy; + + s->btdev.device.class[0] = (class >> 0) & 0xff; + s->btdev.device.class[1] = (class >> 8) & 0xff; + s->btdev.device.class[2] = (class >> 16) & 0xff; + + return &s->btdev.device; +} + +struct bt_device_s *bt_keyboard_init(struct bt_scatternet_s *net) +{ + return bt_hid_init(net, class_keyboard); +} |