/* $OpenBSD: hidmt.c,v 1.12 2020/07/09 21:01:08 jcs Exp $ */ /* * HID multitouch driver for devices conforming to Windows Precision Touchpad * standard * * https://msdn.microsoft.com/en-us/library/windows/hardware/dn467314%28v=vs.85%29.aspx * https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/touchscreen-packet-reporting-modes * * Copyright (c) 2016 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include /* #define HIDMT_DEBUG */ #ifdef HIDMT_DEBUG #define DPRINTF(x) printf x #else #define DPRINTF(x) #endif #define HID_UNIT_CM 0x11 #define HID_UNIT_INCH 0x13 /* * Calculate the horizontal or vertical resolution, in device units per * millimeter. * * With the length unit specified by the descriptor (centimeter or inch), * the result is: * (logical_maximum - logical_minimum) / ((physical_maximum - * physical_minimum) * 10^unit_exponent) * * The descriptors should encode the unit exponent as a signed half-byte. * However, this function accepts the values from -8 to -1 in both the * 4-bit format and the usual encoding. Other values beyond the 4-bit * range are treated as undefined. Possibly a misinterpretation of * section 6.2.2.7 of the HID specification (v1.11) has been turned into * a standard here, see (from www.usb.org) * HUTRR39: "HID Sensor Usage Tables", sect. 3.9, 3.10, 4.2.1 * for an official exegesis and * https://patchwork.kernel.org/patch/3033191 * for details and a different view. */ int hidmt_get_resolution(struct hid_item *h) { int log_extent, phy_extent, exponent; if (h->unit != HID_UNIT_CM && h->unit != HID_UNIT_INCH) return (0); log_extent = h->logical_maximum - h->logical_minimum; phy_extent = h->physical_maximum - h->physical_minimum; if (log_extent <= 0 || phy_extent <= 0) return (0); exponent = h->unit_exponent; if (exponent < -8 || exponent > 15) /* See above. */ return (0); if (exponent > 7) exponent -= 16; for (; exponent < 0 && log_extent <= INT_MAX / 10; exponent++) log_extent *= 10; for (; exponent > 0 && phy_extent <= INT_MAX / 10; exponent--) phy_extent *= 10; if (exponent != 0) return (0); if (h->unit == HID_UNIT_INCH) { /* Map inches to mm. */ if ((phy_extent > INT_MAX / 127) || (log_extent > INT_MAX / 5)) return (0); log_extent *= 5; phy_extent *= 127; } else { /* Map cm to mm. */ if (phy_extent > INT_MAX / 10) return (0); phy_extent *= 10; } return (log_extent / phy_extent); } int hidmt_setup(struct device *self, struct hidmt *mt, void *desc, int dlen) { struct hid_location cap; int32_t d; uint8_t *rep; int capsize; struct hid_data *hd; struct hid_item h; mt->sc_device = self; mt->sc_rep_input_size = hid_report_size(desc, dlen, hid_input, mt->sc_rep_input); mt->sc_minx = mt->sc_miny = mt->sc_maxx = mt->sc_maxy = 0; capsize = hid_report_size(desc, dlen, hid_feature, mt->sc_rep_cap); rep = malloc(capsize, M_DEVBUF, M_NOWAIT | M_ZERO); if (mt->hidev_report_type_conv == NULL) panic("no report type conversion function"); if (mt->hidev_get_report(mt->sc_device, mt->hidev_report_type_conv(hid_feature), mt->sc_rep_cap, rep, capsize)) { printf("\n%s: failed getting capability report\n", self->dv_xname); return 1; } /* find maximum number of contacts being reported per input report */ mt->sc_num_contacts = HIDMT_MAX_CONTACTS; if (hid_locate(desc, dlen, HID_USAGE2(HUP_DIGITIZERS, HUD_CONTACT_MAX), mt->sc_rep_cap, hid_feature, &cap, NULL)) { d = hid_get_udata(rep, capsize, &cap); if (d > HIDMT_MAX_CONTACTS) printf("\n%s: contacts %d > max %d\n", self->dv_xname, d, HIDMT_MAX_CONTACTS); else mt->sc_num_contacts = d; } /* find whether this is a clickpad or not */ if (hid_locate(desc, dlen, HID_USAGE2(HUP_DIGITIZERS, HUD_BUTTON_TYPE), mt->sc_rep_cap, hid_feature, &cap, NULL)) { d = hid_get_udata(rep, capsize, &cap); mt->sc_clickpad = (d == 0); } else { /* if there's not a 2nd button, this is probably a clickpad */ if (!hid_locate(desc, dlen, HID_USAGE2(HUP_BUTTON, 2), mt->sc_rep_input, hid_input, &cap, NULL)) mt->sc_clickpad = 1; } /* * Walk HID descriptor and store usages we care about to know what to * pluck out of input reports. */ SIMPLEQ_INIT(&mt->sc_inputs); hd = hid_start_parse(desc, dlen, hid_input); while (hid_get_item(hd, &h)) { struct hidmt_data *input; if (h.report_ID != mt->sc_rep_input) continue; if (h.kind != hid_input) continue; switch (h.usage) { /* contact level usages */ case HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_X): if (h.logical_maximum - h.logical_minimum) { mt->sc_minx = h.logical_minimum; mt->sc_maxx = h.logical_maximum; mt->sc_resx = hidmt_get_resolution(&h); } break; case HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_Y): if (h.logical_maximum - h.logical_minimum) { mt->sc_miny = h.logical_minimum; mt->sc_maxy = h.logical_maximum; mt->sc_resy = hidmt_get_resolution(&h); } break; case HID_USAGE2(HUP_DIGITIZERS, HUD_TIP_SWITCH): case HID_USAGE2(HUP_DIGITIZERS, HUD_CONFIDENCE): case HID_USAGE2(HUP_DIGITIZERS, HUD_WIDTH): case HID_USAGE2(HUP_DIGITIZERS, HUD_HEIGHT): case HID_USAGE2(HUP_DIGITIZERS, HUD_CONTACTID): /* report level usages */ case HID_USAGE2(HUP_DIGITIZERS, HUD_CONTACTCOUNT): case HID_USAGE2(HUP_BUTTON, 0x01): case HID_USAGE2(HUP_BUTTON, 0x02): case HID_USAGE2(HUP_BUTTON, 0x03): break; default: continue; } input = malloc(sizeof(*input), M_DEVBUF, M_NOWAIT | M_ZERO); memcpy(&input->loc, &h.loc, sizeof(struct hid_location)); input->usage = h.usage; SIMPLEQ_INSERT_TAIL(&mt->sc_inputs, input, entry); } hid_end_parse(hd); if (mt->sc_maxx <= 0 || mt->sc_maxy <= 0) { printf("\n%s: invalid max X/Y %d/%d\n", self->dv_xname, mt->sc_maxx, mt->sc_maxy); return 1; } if (hidmt_set_input_mode(mt, HIDMT_INPUT_MODE_MT_TOUCHPAD)) { printf("\n%s: switch to multitouch mode failed\n", self->dv_xname); return 1; } return 0; } void hidmt_configure(struct hidmt *mt) { struct wsmousehw *hw; if (mt->sc_wsmousedev == NULL) return; hw = wsmouse_get_hw(mt->sc_wsmousedev); hw->type = WSMOUSE_TYPE_TOUCHPAD; hw->hw_type = (mt->sc_clickpad ? WSMOUSEHW_CLICKPAD : WSMOUSEHW_TOUCHPAD); hw->x_min = mt->sc_minx; hw->x_max = mt->sc_maxx; hw->y_min = mt->sc_miny; hw->y_max = mt->sc_maxy; hw->h_res = mt->sc_resx; hw->v_res = mt->sc_resy; hw->mt_slots = HIDMT_MAX_CONTACTS; wsmouse_configure(mt->sc_wsmousedev, NULL, 0); } void hidmt_attach(struct hidmt *mt, const struct wsmouse_accessops *ops) { struct wsmousedev_attach_args a; printf(": %spad, %d contact%s\n", (mt->sc_clickpad ? "click" : "touch"), mt->sc_num_contacts, (mt->sc_num_contacts == 1 ? "" : "s")); a.accessops = ops; a.accesscookie = mt->sc_device; mt->sc_wsmousedev = config_found(mt->sc_device, &a, wsmousedevprint); hidmt_configure(mt); } int hidmt_detach(struct hidmt *mt, int flags) { int rv = 0; if (mt->sc_wsmousedev != NULL) rv = config_detach(mt->sc_wsmousedev, flags); return (rv); } int hidmt_set_input_mode(struct hidmt *mt, uint16_t mode) { if (mt->hidev_report_type_conv == NULL) panic("no report type conversion function"); return mt->hidev_set_report(mt->sc_device, mt->hidev_report_type_conv(hid_feature), mt->sc_rep_config, &mode, sizeof(mode)); } void hidmt_input(struct hidmt *mt, uint8_t *data, u_int len) { struct hidmt_data *hi; struct hidmt_contact hc; int32_t d, firstu = 0; int contactcount = 0, seencontacts = 0, tips = 0, buttons = 0, i, s, z; if (len != mt->sc_rep_input_size) { DPRINTF(("%s: %s: length %d not %d, ignoring\n", mt->sc_device->dv_xname, __func__, len, mt->sc_rep_input_size)); return; } /* * "In Parallel mode, devices report all contact information in a * single packet. Each physical contact is represented by a logical * collection that is embedded in the top-level collection." * * Since additional contacts that were not present will still be in the * report with contactid=0 but contactids are zero-based, find * contactcount first. */ SIMPLEQ_FOREACH(hi, &mt->sc_inputs, entry) { if (hi->usage == HID_USAGE2(HUP_DIGITIZERS, HUD_CONTACTCOUNT)) contactcount = hid_get_udata(data, len, &hi->loc); } if (contactcount) mt->sc_cur_contactcount = contactcount; else { /* * "In Hybrid mode, the number of contacts that can be reported * in one report is less than the maximum number of contacts * that the device supports. For example, a device that supports * a maximum of 4 concurrent physical contacts, can set up its * top-level collection to deliver a maximum of two contacts in * one report. If four contact points are present, the device * can break these up into two serial reports that deliver two * contacts each. * * "When a device delivers data in this manner, the Contact * Count usage value in the first report should reflect the * total number of contacts that are being delivered in the * hybrid reports. The other serial reports should have a * contact count of zero (0)." */ contactcount = mt->sc_cur_contactcount; } if (!contactcount) { DPRINTF(("%s: %s: no contactcount in report\n", mt->sc_device->dv_xname, __func__)); return; } /* * Walk through each input we know about and fetch its data from the * report, storing it in a temporary contact. Once we see our first * usage again, we'll know we saw all usages being presented for that * contact. */ bzero(&hc, sizeof(struct hidmt_contact)); SIMPLEQ_FOREACH(hi, &mt->sc_inputs, entry) { d = hid_get_udata(data, len, &hi->loc); if (firstu && hi->usage == firstu) { if (seencontacts < contactcount) { hc.seen = 1; i = wsmouse_id_to_slot( mt->sc_wsmousedev, hc.contactid); if (i >= 0) memcpy(&mt->sc_contacts[i], &hc, sizeof(struct hidmt_contact)); seencontacts++; } bzero(&hc, sizeof(struct hidmt_contact)); } else if (!firstu) firstu = hi->usage; switch (hi->usage) { case HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_X): hc.x = d; break; case HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_Y): if (mt->sc_flags & HIDMT_REVY) hc.y = mt->sc_maxy - d; else hc.y = d; break; case HID_USAGE2(HUP_DIGITIZERS, HUD_TIP_SWITCH): hc.tip = d; if (d) tips++; break; case HID_USAGE2(HUP_DIGITIZERS, HUD_CONFIDENCE): hc.confidence = d; break; case HID_USAGE2(HUP_DIGITIZERS, HUD_WIDTH): hc.width = d; break; case HID_USAGE2(HUP_DIGITIZERS, HUD_HEIGHT): hc.height = d; break; case HID_USAGE2(HUP_DIGITIZERS, HUD_CONTACTID): hc.contactid = d; break; /* these will only appear once per report */ case HID_USAGE2(HUP_DIGITIZERS, HUD_CONTACTCOUNT): if (d) contactcount = d; break; case HID_USAGE2(HUP_BUTTON, 0x01): case HID_USAGE2(HUP_BUTTON, 0x02): if (d != 0) buttons |= 1; break; case HID_USAGE2(HUP_BUTTON, 0x03): if (d != 0) buttons |= 1 << 2; break; } } if (seencontacts < contactcount) { hc.seen = 1; i = wsmouse_id_to_slot(mt->sc_wsmousedev, hc.contactid); if (i >= 0) memcpy(&mt->sc_contacts[i], &hc, sizeof(struct hidmt_contact)); seencontacts++; } s = spltty(); if (mt->sc_buttons != buttons) { wsmouse_buttons(mt->sc_wsmousedev, buttons); mt->sc_buttons = buttons; } for (i = 0; i < HIDMT_MAX_CONTACTS; i++) { if (!mt->sc_contacts[i].seen) continue; mt->sc_contacts[i].seen = 0; DPRINTF(("%s: %s: contact %d of %d: id %d, x %d, y %d, " "touch %d, confidence %d, width %d, height %d " "(button 0x%x)\n", mt->sc_device->dv_xname, __func__, i + 1, contactcount, mt->sc_contacts[i].contactid, mt->sc_contacts[i].x, mt->sc_contacts[i].y, mt->sc_contacts[i].tip, mt->sc_contacts[i].confidence, mt->sc_contacts[i].width, mt->sc_contacts[i].height, mt->sc_buttons)); if (mt->sc_contacts[i].tip && !mt->sc_contacts[i].confidence) continue; /* Report width as pressure. */ z = (mt->sc_contacts[i].tip ? imax(mt->sc_contacts[i].width, 50) : 0); wsmouse_mtstate(mt->sc_wsmousedev, i, mt->sc_contacts[i].x, mt->sc_contacts[i].y, z); } wsmouse_input_sync(mt->sc_wsmousedev); splx(s); } int hidmt_enable(struct hidmt *mt) { if (mt->sc_enabled) return EBUSY; mt->sc_enabled = 1; return 0; } int hidmt_ioctl(struct hidmt *mt, u_long cmd, caddr_t data, int flag, struct proc *p) { struct wsmouse_calibcoords *wsmc = (struct wsmouse_calibcoords *)data; int wsmode; switch (cmd) { case WSMOUSEIO_GTYPE: { struct wsmousehw *hw = wsmouse_get_hw(mt->sc_wsmousedev); *(u_int *)data = hw->type; break; } case WSMOUSEIO_GCALIBCOORDS: wsmc->minx = mt->sc_minx; wsmc->maxx = mt->sc_maxx; wsmc->miny = mt->sc_miny; wsmc->maxy = mt->sc_maxy; wsmc->swapxy = 0; wsmc->resx = mt->sc_resx; wsmc->resy = mt->sc_resy; break; case WSMOUSEIO_SETMODE: wsmode = *(u_int *)data; if (wsmode != WSMOUSE_COMPAT && wsmode != WSMOUSE_NATIVE) { printf("%s: invalid mode %d\n", mt->sc_device->dv_xname, wsmode); return (EINVAL); } DPRINTF(("%s: changing mode to %s\n", mt->sc_device->dv_xname, (wsmode == WSMOUSE_COMPAT ? "compat" : "native"))); wsmouse_set_mode(mt->sc_wsmousedev, wsmode); break; default: return -1; } return 0; } void hidmt_disable(struct hidmt *mt) { mt->sc_enabled = 0; }