/* * 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) (0x108300 | (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 void hidpp_notif_device_connection(USBLtunifyState *s, uint8_t device_index) { HidppMsgShort msg; LHidDevice *hd; uint8_t devinfo = 0; assert(device_index >= 1 && device_index <= MAX_DEVICES); hd = &s->devices[device_index - 1]; msg.report_id = HIDPP_SHORT; msg.device_index = device_index; msg.sub_id = 0x41; /* Device Connection notif */ msg.params[0] = hd->info.protocol_type; /* device info */ devinfo = hd->info.device_type & 0xF; devinfo |= !!(hd->reporting_flags & REPORTING_FLAG_RCV_SOFTWARE_PRESENT) << 4; devinfo |= (!!hd->link_encrypted << 5); devinfo |= (!hd->powered_on << 6); // bit7 = connect reason: packet with (1) or without (0) payload msg.params[1] = devinfo; msg.params[2] = (uint8_t) hd->info.wireless_pid; msg.params[3] = (uint8_t) (hd->info.wireless_pid >> 8); hidpp_queue_output_report(s, (HidppMsg *) &msg); } /* undocumented, guessed version information */ static void hidpp_process_version_request(USBLtunifyState *s, HidppMsgShort *msg, struct firmware_version *version) { assert(msg->report_id == HIDPP_SHORT); // device_index is either device or receiver assert(msg->sub_id == 0x81); assert(msg->address == 0xF1); switch (msg->value[0]) { case 1: /* firmware version */ msg->value[1] = version->fw_major; msg->value[2] = version->fw_minor; break; case 2: /* firmware build */ msg->value[1] = (uint8_t) (version->fw_build >> 8); msg->value[2] = (uint8_t) version->fw_build; break; case 4: /* bootloader version */ msg->value[1] = version->bl_major; msg->value[2] = version->bl_minor; break; case 5: /* returned by receiver ?? */ msg->value[1] = 44; msg->value[2] = 11; break; default: hidpp_queue_error(s, (HidppMsg *) msg, HIDPP_ERR_INVALID_VALUE); return; } hidpp_queue_output_report(s, (HidppMsg *) msg); } static void hidpp_process_receiver_report(USBLtunifyState *s, HidppMsg *msg) { LHidDevice *hd; int i; uint8_t *parms; assert(msg->device_index == 0xFF); assert(msg->report_id != DJ_LONG); if (msg->report_id == DJ_SHORT) { parms = msg->dj_s.payload; /* 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; parms = ml->value; /* NOTE: drops 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 SET_REG(0x02): /* Connection State? (undocumented) */ if ((ms->value[0] & 2) && (rcvr->reporting_flags & REPORTING_FLAG_RCV_WIRELESS_NOTIFS)) { for (i = 0; i < MAX_DEVICES; i++) { if (hidpp_device_available(s, i + 1)) { hidpp_notif_device_connection(s, i + 1); } } } memset(ms->value, 0, 3); 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; case GET_LONG_REG(0xB5): /* Pairing information */ if ((parms[0] >> 4) >= 2 && (parms[0] >> 4) <= 4 && hidpp_device_available(s, 1 + (parms[0] & 0xF))) { LHidDevice *hd = &s->devices[parms[0] & 0xF]; ml->report_id = HIDPP_LONG; memset(parms + 1, 0, sizeof(ml->value) - 1); if (parms[0] & 0x20) { /* Unifying Device pairing information */ parms[1] = 0x42; /* Destination ID (???) */ parms[2] = hd->report_interval; parms[3] = (uint8_t) hd->info.wireless_pid; parms[4] = (uint8_t) (hd->info.wireless_pid >> 8); parms[7] = hd->info.device_type; } else if (parms[0] & 0x30) { /* Unifying Device extended pairing info */ parms[1] = (uint8_t) hd->info.serial; parms[2] = (uint8_t) (hd->info.serial >> 8); parms[3] = (uint8_t) (hd->info.serial >> 16); parms[4] = (uint8_t) (hd->info.serial >> 24); parms[5] = (uint8_t) hd->info.report_types; parms[6] = (uint8_t) (hd->info.report_types >> 8); parms[7] = (uint8_t) (hd->info.report_types >> 16); parms[8] = (uint8_t) (hd->info.report_types >> 24); parms[9] = hd->info.usability_info; } else if (parms[0] & 0x40) { /* Unifying Device name */ parms[1] = strlen(hd->info.name); memcpy(parms + 2, hd->info.name, parms[1]); } hidpp_queue_output_report(s, msg); } else if (parms[0] == 2 || parms[0] == 3) { /* undocumented, presumably receiver commands */ LHidReceiver *rcvr = &s->receiver; ml->report_id = HIDPP_LONG; memset(parms + 1, 0, sizeof(ml->value) - 1); if (parms[0] == 2) { // ?? } else if (parms[0] == 3) { parms[1] = (uint8_t) rcvr->info.serial; parms[2] = (uint8_t) (rcvr->info.serial >> 8); parms[3] = (uint8_t) (rcvr->info.serial >> 16); parms[4] = (uint8_t) (rcvr->info.serial >> 24); //parms[5] ?? parms[6] = MAX_DEVICES; /* guessed */ //parms[7] ?? } hidpp_queue_output_report(s, msg); } else { /* invalid command or unavailable device */ hidpp_queue_error(s, msg, HIDPP_ERR_INVALID_VALUE); } break; case GET_REG(0xF1): hidpp_process_version_request(s, ms, &rcvr->info.version); 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); } } } } /* called for HID++ reports targeted at devices */ static void hidpp_process_device_hidpp10(USBLtunifyState *s, HidppMsg *msg) { LHidDevice *hd = &s->devices[msg->device_index - 1]; HidppMsgLong *ml = &msg->hidpp_l; unsigned req = (ml->report_id << 16) | (ml->sub_id << 8) | ml->address; uint8_t *parms = ml->value; /* NOTE: drops address */ switch (req) { case GET_REG(0x00): /* Enable HID++ notifs */ parms[0] = (uint8_t) hd->reporting_flags; parms[1] = 0; parms[2] = (uint8_t) (hd->reporting_flags >> 8); hidpp_queue_output_report(s, msg); break; case SET_REG(0x00): /* Enable HID++ notifs */ hd->reporting_flags = (parms[2] << 8) | parms[0]; hd->reporting_flags &= REPORTING_FLAG_DEV_MASK; memset(parms, 0, 3); hidpp_queue_output_report(s, msg); break; case GET_REG(0xF1): hidpp_process_version_request(s, &msg->hidpp_s, &hd->info.version); 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); } } } /* HID++ 2.0 */ static void hidpp20_queue_error(USBLtunifyState *s, Hidpp20Msg *func, uint8_t err) { Hidpp20Msg error_report = { .report_id = func->report_id, .device_index = func->device_index, .feat_index = 0xFF, .func = func->func, .params = { err } }; hidpp_queue_output_report(s, (HidppMsg *) &error_report); } /* called for HID++ 2.0 reports targeted at devices */ static void hidpp_process_device_hidpp20(USBLtunifyState *s, HidppMsg *msg) { LHidDevice *hd = &s->devices[msg->device_index - 1]; Hidpp20Msg *func = (Hidpp20Msg *) msg; int r; r = hidpp20_feature_call(hd, func); /* note: if r == 0, then the function is void and there is no output */ if (r > 0) { hidpp_queue_output_report(s, msg); } else if (r < 0) { hidpp20_queue_error(s, func, -r); } } 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)) { LHidDevice *hd = &s->devices[msg.device_index - 1]; if (msg.report_id == HIDPP_SHORT || msg.report_id == HIDPP_LONG) { if (hd->info.protocol_version >= 0x0200) { hidpp_process_device_hidpp20(s, &msg); } else { hidpp_process_device_hidpp10(s, &msg); } } } 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; /* fail if the report is too short, or if trying to write a long report */ if (len < sizeof(HidppMsgShort) || msg->report_id == DJ_LONG) { 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 */ hidpp_process_receiver_report(s, msg); } 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; hd->info.protocol_type = PROTO_UNIFYING; 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); hd->report_interval = 20; 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); hd->report_interval = 8; 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); hd->report_interval = 8; break; } hidpp20_init_features(hd); 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; r->info.version = (struct firmware_version) { 24, 0, 18, 0, 6 }; hidpp_init_device(s, 1, DEVTYPE_KEYBOARD); hidpp_init_device(s, 2, DEVTYPE_MOUSE); }