summaryrefslogtreecommitdiff
path: root/hw/bt-hid.c
diff options
context:
space:
mode:
Diffstat (limited to 'hw/bt-hid.c')
-rw-r--r--hw/bt-hid.c571
1 files changed, 571 insertions, 0 deletions
diff --git a/hw/bt-hid.c b/hw/bt-hid.c
new file mode 100644
index 0000000000..4602d9e06e
--- /dev/null
+++ b/hw/bt-hid.c
@@ -0,0 +1,571 @@
+/*
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+ * MA 02111-1307 USA
+ */
+
+#include "qemu-common.h"
+#include "usb.h"
+#include "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;
+ USBDevice *usbdev;
+
+ 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);
+
+ s->usbdev->handle_reset(s->usbdev);
+ 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)
+{
+ USBPacket p;
+
+ if (s->data_type == BT_DATA_OUTPUT) {
+ p.pid = USB_TOKEN_OUT;
+ p.devep = 1;
+ p.data = s->dataout.buffer;
+ p.len = s->dataout.len;
+ s->dataout.len = s->usbdev->handle_data(s->usbdev, &p);
+
+ return s->dataout.len;
+ }
+
+ if (s->data_type == BT_DATA_FEATURE) {
+ /* XXX:
+ * does this send a USB_REQ_CLEAR_FEATURE/USB_REQ_SET_FEATURE
+ * or a SET_REPORT? */
+ p.devep = 0;
+ }
+
+ return -1;
+}
+
+static int bt_hid_in(struct bt_hid_device_s *s)
+{
+ USBPacket p;
+
+ p.pid = USB_TOKEN_IN;
+ p.devep = 1;
+ p.data = s->datain.buffer;
+ p.len = sizeof(s->datain.buffer);
+ s->datain.len = s->usbdev->handle_data(s->usbdev, &p);
+
+ 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->usbdev->handle_control(s->usbdev, SET_PROTOCOL, s->proto, 0, 0, 0);
+ 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->usbdev->handle_control(s->usbdev, GET_IDLE, 0, 0, 1,
+ s->control->sdu_out(s->control, 1));
+ 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;
+ }
+
+ /* We don't need to know about the Idle Rate here really,
+ * so just pass it on to the device. */
+ ret = s->usbdev->handle_control(s->usbdev,
+ SET_IDLE, data[1], 0, 0, 0) ?
+ BT_HS_SUCCESSFUL : BT_HS_ERR_INVALID_PARAMETER;
+ /* 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;
+
+ return bt_hid_control_transaction(hid, data, len);
+}
+
+static void bt_hid_datain(void *opaque)
+{
+ struct bt_hid_device_s *hid = opaque;
+
+ /* 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->usbdev->handle_reset(hid->usbdev);
+ 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 = 0;
+ bt_hid_connected_update(hid);
+}
+
+static void bt_hid_close_interrupt(void *opaque)
+{
+ struct bt_hid_device_s *hid = opaque;
+
+ hid->interrupt = 0;
+ 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->usbdev->handle_destroy(hid->usbdev);
+
+ qemu_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,
+ USBDevice *dev, enum peripheral_minor_class minor)
+{
+ struct bt_hid_device_s *s = qemu_mallocz(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);
+
+ s->usbdev = dev;
+ s->btdev.device.lmp_name = s->usbdev->devname;
+ usb_hid_datain_cb(s->usbdev, s, bt_hid_datain);
+
+ 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, usb_keyboard_init(), class_keyboard);
+}