/* * This file is part of DeskHop (https://github.com/hrvach/deskhop). * Copyright (c) 2024 Hrvoje Cavrak * * Based on the TinyUSB HID parser routine and the amazing USB2N64 * adapter (https://github.com/pdaxrom/usb2n64-adapter) * * 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, version 3. * * 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, see . */ #include "main.h" #include "hid_parser.h" #define IS_BLOCK_END (collection.start == collection.end) #define MAX_BUTTONS 16 enum { SIZE_0_BIT = 0, SIZE_8_BIT = 1, SIZE_16_BIT = 2, SIZE_32_BIT = 3 }; /* Size is 0, 1, 2, or 3, describing cases of no data, 8-bit, 16-bit, or 32-bit data. */ uint32_t get_descriptor_value(uint8_t const *report, int size) { switch (size) { case SIZE_8_BIT: return report[0]; case SIZE_16_BIT: return tu_u16(report[1], report[0]); case SIZE_32_BIT: return tu_u32(report[3], report[2], report[1], report[0]); default: return 0; } } /* We store all globals as unsigned to avoid countless switch/cases. In case of e.g. min/max, we need to treat some data as signed retroactively. */ int32_t to_signed(globals_t *data) { switch (data->hdr.size) { case SIZE_8_BIT: return (int8_t)data->val; case SIZE_16_BIT: return (int16_t)data->val; default: return data->val; } } /* Given a value struct with size and offset in bits, find and return a value from the HID report */ int32_t get_report_value(uint8_t* report, report_val_t *val) { /* Calculate the bit offset within the byte */ uint8_t offset_in_bits = val->offset % 8; /* Calculate the remaining bits in the first byte */ uint8_t remaining_bits = 8 - offset_in_bits; /* Calculate the byte offset in the array */ uint8_t byte_offset = val->offset >> 3; /* Create a mask for the specified number of bits */ uint32_t mask = (1u << val->size) - 1; /* Initialize the result value with the bits from the first byte */ int32_t result = report[byte_offset] >> offset_in_bits; /* Move to the next byte and continue fetching bits until the desired length is reached */ while (val->size > remaining_bits) { result |= report[++byte_offset] << remaining_bits; remaining_bits += 8; } /* Apply the mask to retain only the desired number of bits */ result = result & mask; /* Special case if result is negative. Check if the most significant bit of 'val' is set */ if (result & ((mask >> 1) + 1)) { /* If it is set, sign-extend 'val' by filling the higher bits with 1s */ result |= (0xFFFFFFFFU << val->size); } return result; } /* This method is far from a generalized HID descriptor parsing, but should work * well enough to find the basic values we care about to move the mouse around. * Your descriptor for a mouse with 2 wheels and 264 buttons might not parse correctly. **/ uint8_t parse_report_descriptor(mouse_t *mouse, uint8_t arr_count, uint8_t const *report, uint16_t desc_len) { /* Get these elements and store them in the proper place in the mouse struct * For example, to match wheel, we want collection usage to be HID_USAGE_DESKTOP_MOUSE, page to be HID_USAGE_PAGE_DESKTOP, * usage to be HID_USAGE_DESKTOP_WHEEL, then if all of that is matched we store the value to mouse->wheel */ const usage_map_t usage_map[] = { {HID_USAGE_DESKTOP_MOUSE, HID_USAGE_PAGE_BUTTON, HID_USAGE_DESKTOP_POINTER, &mouse->buttons}, {HID_USAGE_DESKTOP_MOUSE, HID_USAGE_PAGE_DESKTOP, HID_USAGE_DESKTOP_X, &mouse->move_x}, {HID_USAGE_DESKTOP_MOUSE, HID_USAGE_PAGE_DESKTOP, HID_USAGE_DESKTOP_Y, &mouse->move_y}, {HID_USAGE_DESKTOP_MOUSE, HID_USAGE_PAGE_DESKTOP, HID_USAGE_DESKTOP_WHEEL, &mouse->wheel}, }; /* Some variables used for keeping tabs on parsing */ uint8_t usage_count = 0; uint8_t g_usage = 0; uint32_t offset_in_bits = 0; uint8_t usages[64] = {0}; uint8_t* p_usage = usages; collection_t collection = {0}; /* as tag is 4 bits, there can be 16 different tags in global header type */ globals_t globals[16] = {0}; for (int len = desc_len; len > 0; len--) { header_t header = *(header_t *)report++; uint32_t data = get_descriptor_value(report, header.size); switch (header.type) { case RI_TYPE_MAIN: // Keep count of collections, starts and ends collection.start += (header.tag == RI_MAIN_COLLECTION); collection.end += (header.tag == RI_MAIN_COLLECTION_END); if (header.tag == RI_MAIN_INPUT) { for (int i = 0; i < globals[RI_GLOBAL_REPORT_COUNT].val; i++) { /* If we don't have as many usages as elements, the usage for the previous element applies */ if (i && i >= usage_count ) { *(p_usage + i) = *(p_usage + usage_count - 1); } const usage_map_t *map = usage_map; /* Only focus on the items we care about (buttons, x and y, wheels, etc) */ for (int j=0; jreport_usage == g_usage && map->usage_page == globals[RI_GLOBAL_USAGE_PAGE].val && map->usage == *(p_usage + i)) { /* Buttons are the ones that appear multiple times, will handle them properly For now, let's just aggregate the length and combine them into one :) */ if (map->element->size) { map->element->size++; continue; } /* Store the found element's attributes */ map->element->offset = offset_in_bits; map->element->size = globals[RI_GLOBAL_REPORT_SIZE].val; map->element->min = to_signed(&globals[RI_GLOBAL_LOGICAL_MIN]); map->element->max = to_signed(&globals[RI_GLOBAL_LOGICAL_MAX]); } }; /* Iterate times and increase offset by amount, moving by x bits */ offset_in_bits += globals[RI_GLOBAL_REPORT_SIZE].val; } /* Advance the usage array pointer by global report count and reset the count variable */ p_usage += globals[RI_GLOBAL_REPORT_COUNT].val; usage_count = 0; } break; case RI_TYPE_GLOBAL: /* There are just 16 possible tags, store any one that comes along to an array instead of doing switch and 16 cases */ globals[header.tag].val = data; globals[header.tag].hdr = header; if (header.tag == RI_GLOBAL_REPORT_ID) { /* Important to track, if report IDs are used reports are preceded/offset by a 1-byte ID value */ if(g_usage == HID_USAGE_DESKTOP_MOUSE) mouse->report_id = data; mouse->uses_report_id = true; } break; case RI_TYPE_LOCAL: if (header.tag == RI_LOCAL_USAGE) { /* If we are not within a collection, the usage tag applies to the entire section */ if (IS_BLOCK_END) g_usage = data; else *(p_usage + usage_count++) = data; } break; } /* If header specified some non-zero length data, move by that much to get to the new byte we should interpret as a header element */ report += header.size; len -= header.size; } return 0; }