/* * Logitech Unifying receiver emulation (HID++ protocol). * * Copyright (c) 2014 Peter Wu * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "hw/hw.h" #include "ui/console.h" #include "hw/usb.h" #include "dump.h" #include "hw/input/hid.h" #include "hw/usb/hid-logitech-dj.h" #define COMPILE_ASSERT(cond) typedef char _compile_assert[1 - 2 * !(cond)] /* Report IDs */ enum { HIDPP_SHORT = 0x10, /* 7 bytes */ HIDPP_LONG = 0x11, /* 20 bytes */ DJ_SHORT = 0x20, /* 15 bytes */ DJ_LONG = 0x21, /* 32 bytes */ }; /* helpers to handle the HID++ register queries */ #define SET_REG(reg) (0x108000 | (reg)) #define GET_REG(reg) (0x108100 | (reg)) #define SET_LONG_REG(reg) (0x118200 | (reg)) #define GET_LONG_REG(reg) (0x118300 | (reg)) /* returns the expected length of the report or 0 if invalid */ static unsigned msg_report_length(HidppMsg *msg) { COMPILE_ASSERT(sizeof(HidppMsgShort) == 7); COMPILE_ASSERT(sizeof(HidppMsgLong) == 20); COMPILE_ASSERT(sizeof(DjMsgShort) == 15); COMPILE_ASSERT(sizeof(DjMsgLong) == 32); switch (msg->report_id) { case HIDPP_SHORT: return 7; case HIDPP_LONG: return 20; case DJ_SHORT: return 15; case DJ_LONG: return 32; default: return 0; } } static void hidpp_queue_error(USBLtunifyState *s, HidppMsg *msg, uint8_t err) { HidppMsgShort *report; unsigned slot; if (s->error_queue.n < LQUEUE_SIZE(s->error_queue)) { slot = LQUEUE_WRAP(s->error_queue, s->error_queue.head + s->error_queue.n); s->error_queue.n++; report = &s->error_queue.reports[slot]; report->report_id = HIDPP_SHORT; report->device_index = msg->device_index; report->sub_id = 0x8F; report->params[0] = msg->hidpp_s.sub_id; report->params[1] = msg->hidpp_s.address; report->params[2] = err; report->params[3] = 0; } } static void hidpp_queue_output_report(USBLtunifyState *s, HidppMsg *msg) { unsigned slot; assert(msg_report_length(msg) != 0); if (s->output_queue.n < LQUEUE_SIZE(s->output_queue)) { slot = LQUEUE_WRAP(s->output_queue, s->output_queue.head + s->output_queue.n); s->output_queue.n++; s->output_queue.reports[slot] = *msg; } } static bool hidpp_device_available(USBLtunifyState *s, uint8_t device_index) { /* TODO: consider unpaired / powered off */ return device_index >= 1 && device_index <= MAX_DEVICES && s->devices[device_index - 1].powered_on; } static bool hidpp_process_receiver_report(USBLtunifyState *s, HidppMsg *msg) { LHidDevice *hd; int i; uint8_t *parms = msg->dj_s.payload; assert(msg->device_index == 0xFF); if (msg->report_id == DJ_SHORT) { /* DJ reports */ switch (msg->dj_s.report_type) { case 0x0E: /* Keyboard LEDs */ /* ignored */ break; case 0x80: /* Switch and Keep-Alive */ for (i = 0; i < MAX_DEVICES; i++) { hd = &s->devices[i]; if ((parms[0] >> i) & 1) { hd->mode = LTUNIFY_MODE_DJ; } else { hd->mode = LTUNIFY_MODE_HID; } } // not implemented: keep-alive timeout params[1]; break; case 0x81: /* Get Paired Devices */ memset(msg, 0, sizeof(*msg)); msg->report_id = DJ_SHORT; msg->dj_s.report_type = 0x41; /* Device Paired notif */ parms[0] = 1; /* more notifications will follow bit. */ for (i = 0; i < MAX_DEVICES; i++) { hd = &s->devices[i]; if (!hidpp_device_available(s, i + 1)) { continue; } msg->device_index = i + 1; parms[1] = (uint8_t) hd->info.wireless_pid; parms[2] = (uint8_t) (hd->info.wireless_pid >> 8); parms[3] = (uint8_t) hd->info.report_types; parms[4] = (uint8_t) (hd->info.report_types >> 8); parms[5] = (uint8_t) (hd->info.report_types >> 16); parms[6] = (uint8_t) (hd->info.report_types >> 24); hidpp_queue_output_report(s, msg); } /* end of notifications list */ msg->device_index = 0x00; parms[0] = 2; memset(parms + 1, 0, 6); hidpp_queue_output_report(s, msg); break; default: hidpp_queue_error(s, msg, HIDPP_ERR_INVALID_SUBID); } } else if (msg->report_id == HIDPP_SHORT || msg->report_id == HIDPP_LONG) { LHidReceiver *rcvr = &s->receiver; HidppMsgShort *ms = &msg->hidpp_s; HidppMsgLong *ml = &msg->hidpp_l; unsigned req = (ms->report_id << 16) | (ms->sub_id << 8) | ms->address; switch (req) { case GET_REG(0x00): /* Enable HID++ notifs */ ms->value[0] = ms->value[2] = 0; ms->value[1] = rcvr->reporting_flags; hidpp_queue_output_report(s, msg); break; case SET_REG(0x00): /* Enable HID++ notifs */ rcvr->reporting_flags = ms->value[1]; rcvr->reporting_flags &= REPORTING_FLAG_RCV_MASK; memset(ms->value, 0, 3); hidpp_queue_output_report(s, msg); break; case GET_REG(0x02): /* Connection State */ memset(ms->value, 0, 3); for (i = 0; i < MAX_DEVICES; i++) { if (s->devices[i].info.device_type) { ms->value[1]++; } } hidpp_queue_output_report(s, msg); break; case GET_LONG_REG(0xB3): /* Device Activity */ memset(ml->value, 0, sizeof(ml->value)); ml->report_id = HIDPP_LONG; for (i = 0; i < MAX_DEVICES; i++) { if (s->devices[i].info.device_type) { ml->value[i] = s->devices[i].activity_counter; } } hidpp_queue_output_report(s, msg); break; default: /* unknown request, return error */ switch (req & ~0xFF) { case SET_REG(0): case GET_REG(0): case SET_LONG_REG(0): case GET_LONG_REG(0): /* register request unsatisfied */ hidpp_queue_error(s, msg, HIDPP_ERR_INVALID_ADDRESS); break; default: /* invalid report ID and command SubID combination */ hidpp_queue_error(s, msg, HIDPP_ERR_INVALID_SUBID); } } } else { /* DJ_LONG is unhandled */ return false; } /* report is accepted and possibly processed */ return true; } static void hidpp_process_input_report(USBLtunifyState *s, HidppMsg msg) { /* receiver reports are processed earlier, elsewhere */ assert(msg.device_index != 0xFF); if (hidpp_device_available(s, msg.device_index)) { /* TODO: implement for devices */ hidpp_queue_error(s, &msg, HIDPP_ERR_REQUEST_UNAVAILABLE); } else { hidpp_queue_error(s, &msg, HIDPP_ERR_RESOURCE_ERROR); } } /* process a received report */ static void hidpp_set_report(USBDevice *dev, USBPacket *p, uint8_t *data, size_t len) { USBLtunifyState *s = DO_UPCAST(USBLtunifyState, dev, dev); HidppMsg *msg = (HidppMsg *) data; int report_len; if (len < sizeof(HidppMsgShort)) { goto fail; } report_len = msg_report_length(msg); if (report_len > 0 && len >= report_len) { if (msg->device_index == 0xFF) { /* receiver messages can be processed immediately */ if (!hidpp_process_receiver_report(s, msg)) { goto fail; } } else if (s->input_queue.n < LQUEUE_SIZE(s->input_queue)) { unsigned slot = LQUEUE_WRAP(s->input_queue, s->input_queue.head + s->input_queue.n); s->input_queue.n++; memcpy((uint8_t *) &s->input_queue.reports[slot], data, report_len); } else { /* queue full, cannot accept this report */ hidpp_queue_error(s, msg, HIDPP_ERR_BUSY); } } else { fail: p->status = USB_RET_STALL; } } void usb_ltunify_handle_control_hidpp(USBDevice *dev, USBPacket *p, int request, int value, int index, int length, uint8_t *data) { //USBLtunifyState *s = (USBLtunifyState *) dev; switch (request) { case HID_GET_REPORT: /* FIXME */ break; case HID_SET_REPORT: hidpp_set_report(dev, p, data, length); break; default: p->status = USB_RET_STALL; break; } } static void hidpp_handle_hid(USBDevice *dev, USBPacket *p) { USBLtunifyState *s = (USBLtunifyState *) dev; DjMsgShort msg = { 0 }; uint8_t *data = (uint8_t *) &msg; uint8_t *hid_data = msg.payload; int i; msg.report_id = DJ_SHORT; /* TODO: be more fair, right now the first device always takes all events, * causing delays for other devices. */ for (i = 0; i < MAX_DEVICES; i++) { LHidDevice *hd = &s->devices[i]; if (!hd->info.device_type) { continue; } if (hd->hid->kind == HID_MOUSE) { /* start accepting pointer events if not already */ hid_pointer_activate(hd->hid); } if (!hid_has_events(hd->hid)) { continue; } hid_set_next_idle(hd->hid); msg.device_index = i + 1; if (hd->hid->kind == HID_MOUSE) { msg.report_type = 0x02; hid_pointer_poll(hd->hid, hid_data, sizeof(msg.payload)); } else if (hd->hid->kind == HID_KEYBOARD) { msg.report_type = 0x01; hid_keyboard_poll(hd->hid, hid_data, sizeof(msg.payload)); } usb_dump_complete_data(s->usb_dump_state, p, data, sizeof(msg)); usb_packet_copy(p, data, sizeof(msg)); usb_dump_submit(s->usb_dump_state, p); return; } p->status = USB_RET_NAK; } void usb_ltunify_handle_datain_hidpp(USBDevice *dev, USBPacket *p) { USBLtunifyState *s = (USBLtunifyState *) dev; int slot; int len = 0; HidppMsg *msg = NULL; assert(p->pid == USB_TOKEN_IN); assert(p->ep->nr == 3); /* process input reports */ if (s->input_queue.n > 0) { slot = s->input_queue.head; LQUEUE_INCR(s->input_queue, s->input_queue.head); s->input_queue.n--; hidpp_process_input_report(s, s->input_queue.reports[slot]); } /* try to send a reply to a previously sent request if possible. */ if (s->error_queue.n > 0) { slot = s->error_queue.head; LQUEUE_INCR(s->error_queue, s->error_queue.head); s->error_queue.n--; msg = (HidppMsg *) &s->error_queue.reports[slot]; } else if (s->output_queue.n > 0) { slot = s->output_queue.head; LQUEUE_INCR(s->output_queue, s->output_queue.head); s->output_queue.n--; msg = &s->output_queue.reports[slot]; } if (msg == NULL) { hidpp_handle_hid(dev, p); return; } if (msg != NULL) { len = msg_report_length(msg); assert(len > 0); if (hidpp_device_available(s, msg->device_index)) { s->devices[msg->device_index - 1].activity_counter++; } usb_dump_complete_data(s->usb_dump_state, p, (uint8_t *) msg, len); usb_packet_copy(p, (uint8_t *) msg, len); usb_dump_submit(s->usb_dump_state, p); } else { p->status = USB_RET_NAK; } } static void hidpp_init_device(USBLtunifyState *s, int device_index, int devtype) { LHidDevice *hd; assert(device_index >= 1 && device_index <= MAX_DEVICES); hd = &s->devices[device_index - 1]; memset(hd, 0, sizeof(*hd)); hd->info.device_type = devtype; switch (devtype) { case DEVTYPE_KEYBOARD: hd->hid = &s->hid[IFACE_KBD]; hd->info.report_types = 0x1a; /* Keyboard, Consumer Control, System Control */ hd->info.wireless_pid = 0x2010; hd->info.serial = 0x4b657973; memcpy(hd->info.name, "K800", 4); break; case DEVTYPE_MOUSE: hd->hid = &s->hid[IFACE_MSE]; hd->info.report_types = 4; /* HID Collection: Mouse */ hd->info.wireless_pid = 0x4013; hd->info.serial = 0x52617473; memcpy(hd->info.name, "M525", 4); break; case DEVTYPE_TOUCHPAD: /* TODO: perhaps "unpair" mouse and use this touchpad instead? */ hd->hid = &s->hid[IFACE_MSE]; hd->info.report_types = 0x0e; /* Keyboard, Mouse, Consumer Ctrl */ hd->info.wireless_pid = 0x4026; hd->info.serial = 0x4f4f4f48; memcpy(hd->info.name, "T650", 4); break; } hd->info.protocol_version = 0x0200; /* HID++ 2.0 */ hd->mode = LTUNIFY_MODE_HID; hd->powered_on = true; } void hidpp_reset(USBLtunifyState *s) { int i; for (i = 0; i < MAX_DEVICES; i++) { LHidDevice *hd = &s->devices[i]; if (hd->info.device_type) { hidpp_init_device(s, i + 1, hd->info.device_type); } } } void hidpp_init(USBLtunifyState *s) { LHidReceiver *r = &s->receiver; r->info.serial = 0x4c4f5354; hidpp_init_device(s, 1, DEVTYPE_KEYBOARD); hidpp_init_device(s, 2, DEVTYPE_MOUSE); }