From 560f3dca740db2be5f00f1e37c2fec11679ac393 Mon Sep 17 00:00:00 2001 From: Hrvoje Cavrak Date: Wed, 3 Jan 2024 10:48:34 +0100 Subject: [PATCH] New features, bugfixes and optimizations Some of the features implemented in this release are: - TinyUSB used to handle HOST management as well - USB hub support (tried an ancient one and it worked) - Early and buggy support for mouse on the keyboard side but have no unified usb receivers to test - Rudimentary HID report descriptor parsing, support for mice that don't send wheel data unless in report protocol mode - Implemented queueing for keyboard/mouse messages with hid report send verification - Split firmware upgrade shortcut to two now it's left-shift + F12 + A + right shift to write board A left-shift + F12 + B + right shift to write board B - Fixed keyboard stuck in outputing chars if you hold down a key and change screens while doing it - Implemented cursor hiding, so the screen we are moving away from parks cursor at top right corner and it looks more natural and feels intuitive - Implemented switch lock, use Ctrl + L to lock and unlock desktop switching - Implemented jump threshold, works like barrier opacity - you can define if mouse immediately jumps over or you need to give it a bit of a "nudge" --- CMakeLists.txt | 11 ++- binaries/board_A.uf2 | Bin 107008 -> 121344 bytes binaries/board_B.uf2 | Bin 106496 -> 120832 bytes src/handlers.c | 68 ++++++++++---- src/hid_parser.c | 210 +++++++++++++++++++++++++++++++++++++++++++ src/hid_parser.h | 83 +++++++++++++++++ src/keyboard.c | 108 +++++++++++++++++----- src/led.c | 25 +++++- src/main.c | 43 ++++++--- src/main.h | 68 ++++++++++++-- src/mouse.c | 201 ++++++++++++++++++++++++++++++++--------- src/setup.c | 47 +++++++--- src/tusb_config.h | 45 +++++++--- src/uart.c | 32 +++++-- src/usb.c | 160 ++++++++++++++++++++++++--------- src/user_config.h | 17 +++- src/utils.c | 30 +++++-- 17 files changed, 958 insertions(+), 190 deletions(-) create mode 100644 src/hid_parser.c create mode 100644 src/hid_parser.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6718bed..cdf0cdb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,8 @@ set(PICO_BOARD=pico) include(pico_sdk_import.cmake) set(CMAKE_CXX_FLAGS "-Ofast -Wall -mcpu=cortex-m0plus -mtune=cortex-m0plus") +set(PICO_COPY_TO_RAM 1) + project(deskhop_project C CXX ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) @@ -31,6 +33,7 @@ target_include_directories(Pico-PIO-USB PRIVATE ${PICO_PIO_USB_DIR}) set(COMMON_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/usb_descriptors.c + ${CMAKE_CURRENT_LIST_DIR}/src/hid_parser.c ${CMAKE_CURRENT_LIST_DIR}/src/utils.c ${CMAKE_CURRENT_LIST_DIR}/src/handlers.c ${CMAKE_CURRENT_LIST_DIR}/src/setup.c @@ -40,6 +43,8 @@ set(COMMON_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/uart.c ${CMAKE_CURRENT_LIST_DIR}/src/usb.c ${CMAKE_CURRENT_LIST_DIR}/src/main.c + ${PICO_TINYUSB_PATH}/src/portable/raspberrypi/pio_usb/dcd_pio_usb.c + ${PICO_TINYUSB_PATH}/src/portable/raspberrypi/pio_usb/hcd_pio_usb.c ) set(COMMON_INCLUDES @@ -51,8 +56,10 @@ set(COMMON_LINK_LIBRARIES pico_stdlib hardware_uart hardware_gpio + hardware_pio tinyusb_device + tinyusb_host pico_multicore Pico-PIO-USB ) @@ -61,7 +68,7 @@ set(COMMON_LINK_LIBRARIES add_executable(board_A) target_sources(board_A PUBLIC ${COMMON_SOURCES}) -target_compile_definitions(board_A PRIVATE BOARD_ROLE=0) +target_compile_definitions(board_A PRIVATE BOARD_ROLE=0 PIO_USB_USE_TINYUSB=1 PIO_USB_DP_PIN_DEFAULT=14) target_include_directories(board_A PUBLIC ${COMMON_INCLUDES}) target_link_libraries(board_A PUBLIC ${COMMON_LINK_LIBRARIES}) @@ -72,7 +79,7 @@ pico_add_extra_outputs(board_A) # Pico B - Mouse add_executable(board_B) -target_compile_definitions(board_B PRIVATE BOARD_ROLE=1) +target_compile_definitions(board_B PRIVATE BOARD_ROLE=1 PIO_USB_USE_TINYUSB=1 PIO_USB_DP_PIN_DEFAULT=14) target_sources(board_B PUBLIC ${COMMON_SOURCES}) target_include_directories(board_B PUBLIC ${COMMON_INCLUDES}) target_link_libraries(board_B PUBLIC ${COMMON_LINK_LIBRARIES}) diff --git a/binaries/board_A.uf2 b/binaries/board_A.uf2 index 844392a710211c189b3b0f0163242f224d6b2f4a..8502f5199f1311c855ed811bcbc8b6d75bf6cace 100644 GIT binary patch delta 31680 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 delta 23358 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 delta 23536 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 diff --git a/src/handlers.c b/src/handlers.c index 79f58bc..b8e161e 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -1,3 +1,20 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" /**=================================================== * @@ -5,15 +22,31 @@ * =================================================== */ void output_toggle_hotkey_handler(device_state_t* state) { + /* If switching explicitly disabled, return immediately */ + if (state->switch_lock) + return; + state->active_output ^= 1; switch_output(state->active_output); }; -void fw_upgrade_hotkey_handler(device_state_t* state) { - send_value(ENABLE, FIRMWARE_UPGRADE_MSG); +/* This key combo puts board A in firmware upgrade mode */ +void fw_upgrade_hotkey_handler_A(device_state_t* state) { reset_usb_boot(1 << PICO_DEFAULT_LED_PIN, 0); }; +/* This key combo puts board B in firmware upgrade mode */ +void fw_upgrade_hotkey_handler_B(device_state_t* state) { + send_value(ENABLE, FIRMWARE_UPGRADE_MSG); +}; + + +void switchlock_hotkey_handler(device_state_t* state) { + state->switch_lock ^= 1; + send_value(state->switch_lock, SWITCH_LOCK_MSG); +} + + void mouse_zoom_hotkey_handler(device_state_t* state) { if (state->mouse_zoom) return; @@ -35,29 +68,26 @@ void all_keys_released_handler(device_state_t* state) { void handle_keyboard_uart_msg(uart_packet_t* packet, device_state_t* state) { if (state->active_output == ACTIVE_OUTPUT_B) { - hid_keyboard_report_t* report = (hid_keyboard_report_t*)packet->data; - - tud_hid_keyboard_report(REPORT_ID_KEYBOARD, report->modifier, report->keycode); + queue_kbd_report((hid_keyboard_report_t*)packet->data, state); state->last_activity[ACTIVE_OUTPUT_B] = time_us_64(); } } void handle_mouse_abs_uart_msg(uart_packet_t* packet, device_state_t* state) { - if (state->active_output == ACTIVE_OUTPUT_A) { - const hid_abs_mouse_report_t* mouse_report = (hid_abs_mouse_report_t*)packet->data; - - tud_hid_abs_mouse_report(REPORT_ID_MOUSE, mouse_report->buttons, mouse_report->x, - mouse_report->y, mouse_report->wheel, 0); - - state->last_activity[ACTIVE_OUTPUT_A] = time_us_64(); - } + hid_abs_mouse_report_t* mouse_report = (hid_abs_mouse_report_t*)packet->data; + queue_mouse_report(mouse_report, state); + state->last_activity[ACTIVE_OUTPUT_A] = time_us_64(); } void handle_output_select_msg(uart_packet_t* packet, device_state_t* state) { state->active_output = packet->data[0]; + if (state->tud_connected) + stop_pressing_any_keys(&global_state); + update_leds(state); } +/* On firmware upgrade message, reboot into the BOOTSEL fw upgrade mode */ void handle_fw_upgrade_msg(void) { reset_usb_boot(1 << PICO_DEFAULT_LED_PIN, 0); } @@ -67,14 +97,22 @@ void handle_mouse_zoom_msg(uart_packet_t* packet, device_state_t* state) { } void handle_set_report_msg(uart_packet_t* packet, device_state_t* state) { - // Only board B sends LED state through this message type + /* Only board B sends LED state through this message type */ state->keyboard_leds[ACTIVE_OUTPUT_B] = packet->data[0]; update_leds(state); } -// Update output variable, set LED on/off and notify the other board +void handle_switch_lock_msg(uart_packet_t* packet, device_state_t* state) { + state->switch_lock = packet->data[0]; +} + +/* Update output variable, set LED on/off and notify the other board so they are in sync. */ void switch_output(uint8_t new_output) { global_state.active_output = new_output; update_leds(&global_state); send_value(new_output, OUTPUT_SELECT_MSG); + + /* If we were holding a key down and drag the mouse to another screen, the key gets stuck. + Changing outputs = no more keypresses on the previous system. */ + stop_pressing_any_keys(&global_state); } diff --git a/src/hid_parser.c b/src/hid_parser.c new file mode 100644 index 0000000..3e25a62 --- /dev/null +++ b/src/hid_parser.c @@ -0,0 +1,210 @@ +/* + * 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; +} diff --git a/src/hid_parser.h b/src/hid_parser.h new file mode 100644 index 0000000..4d9381b --- /dev/null +++ b/src/hid_parser.h @@ -0,0 +1,83 @@ +/* + * 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 . + */ +#pragma once +#define MAX_REPORTS 32 + +/* Counts how many collection starts and ends we've seen, when they equalize + (and not zero), we are at the end of a block */ +typedef struct { + uint8_t start; + uint8_t end; +} collection_t; + +/* Header byte is unpacked to size/type/tag using this struct */ +typedef struct TU_ATTR_PACKED { + uint8_t size : 2; + uint8_t type : 2; + uint8_t tag : 4; +} header_t; + +/* We store a header block and corresponding data in an array of these + to avoid having to use numerous switch-case checks */ +typedef struct { + header_t hdr; + uint32_t val; +} globals_t; + +// Extended precision mouse movement information +typedef struct { + int32_t move_x; + int32_t move_y; + int32_t wheel; + int32_t pan; + uint32_t buttons; +} mouse_values_t; + +/* Describes where can we find a value in a HID report */ +typedef struct { + uint16_t offset; // In bits + uint8_t size; // In bits + int32_t min; + int32_t max; +} report_val_t; + +/* Defines information about HID report format for the mouse. */ +typedef struct { + report_val_t buttons; + + report_val_t move_x; + report_val_t move_y; + + report_val_t wheel; + + bool uses_report_id; + uint8_t report_id; + uint8_t protocol; +} mouse_t; + +/* For each element type we're interested in there is an entry +in an array of these, defining its usage and in case matched, where to +store the data. */ +typedef struct { + uint8_t report_usage; + uint8_t usage_page; + uint8_t usage; + report_val_t* element; +} usage_map_t; diff --git a/src/keyboard.c b/src/keyboard.c index 448c3da..e13b05e 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -1,3 +1,20 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" /* ==================================================== * @@ -5,26 +22,74 @@ * ==================================================== */ hotkey_combo_t hotkeys[] = { - // Main keyboard switching hotkey + /* Main keyboard switching hotkey */ {.modifier = 0, .keys = {HOTKEY_TOGGLE}, .key_count = 1, .action_handler = &output_toggle_hotkey_handler}, - // Holding down right ALT slows the mouse down + /* Holding down right ALT slows the mouse down */ {.modifier = KEYBOARD_MODIFIER_RIGHTALT, .keys = {}, - .key_count = 0, - .action_handler = &mouse_zoom_hotkey_handler}, + .key_count = 0, + .action_handler = &mouse_zoom_hotkey_handler}, - // Hold down left shift + right shift + P + H + X ==> firmware upgrade mode + /* Switch lock */ + {.modifier = KEYBOARD_MODIFIER_RIGHTCTRL, + .keys = {HID_KEY_L}, + .key_count = 1, + .action_handler = &switchlock_hotkey_handler}, + + /* Hold down left shift + right shift + F12 + A ==> firmware upgrade mode for board A (kbd) */ {.modifier = KEYBOARD_MODIFIER_RIGHTSHIFT | KEYBOARD_MODIFIER_LEFTSHIFT, - .keys = {HID_KEY_P, HID_KEY_H, HID_KEY_X}, - .key_count = 3, - .action_handler = &fw_upgrade_hotkey_handler} + .keys = {HID_KEY_F12, HID_KEY_A}, + .key_count = 2, + .action_handler = &fw_upgrade_hotkey_handler_A}, + + /* Hold down left shift + right shift + F12 + B ==> firmware upgrade mode for board B (mouse) */ + {.modifier = KEYBOARD_MODIFIER_RIGHTSHIFT | KEYBOARD_MODIFIER_LEFTSHIFT, + .keys = {HID_KEY_F12, HID_KEY_B}, + .key_count = 2, + .action_handler = &fw_upgrade_hotkey_handler_B} }; +/* ==================================================== * + * Keyboard Queue Section + * ==================================================== */ + +void process_kbd_queue_task(device_state_t *state) { + hid_keyboard_report_t report; + + /* If we're not connected, we have nowhere to send reports to. */ + if (!state->tud_connected) + return; + + /* Peek first, if there is anything there... */ + if (!queue_try_peek(&state->kbd_queue, &report)) + return; + + /* ... try sending it to the host, if it's successful */ + bool succeeded = tud_hid_keyboard_report(REPORT_ID_KEYBOARD, report.modifier, report.keycode); + + /* ... then we can remove it from the queue. Race conditions shouldn't happen [tm] */ + if (succeeded) + queue_try_remove(&state->kbd_queue, &report); +} + +void queue_kbd_report(hid_keyboard_report_t *report, device_state_t *state) { + /* It wouldn't be fun to queue up a bunch of messages and then dump them all on host */ + if (!state->tud_connected) + return; + + queue_try_add(&state->kbd_queue, report); +} + +void stop_pressing_any_keys(device_state_t *state) { + static hid_keyboard_report_t no_keys_pressed_report = {0, 0, {0}}; + queue_try_add(&state->kbd_queue, &no_keys_pressed_report); +} + /* ==================================================== * * Parse and interpret the keys pressed on the keyboard * ==================================================== */ @@ -46,26 +111,26 @@ void process_keyboard_report(uint8_t* raw_report, int length, device_state_t* st if (length < KBD_REPORT_LENGTH) return; - // Go through the list of hotkeys, check if any are pressed, then execute their handler + /* Go through the list of hotkeys, check if any are pressed, then execute their handler */ for (int n = 0; n < sizeof(hotkeys) / sizeof(hotkeys[0]); n++) { if (keypress_check(hotkeys[n], keyboard_report)) { hotkeys[n].action_handler(state); return; } - } + } - // If no keys are pressed anymore, take care of checking and deactivating stuff - if (no_keys_are_pressed(keyboard_report)) { + state->key_pressed = !no_keys_are_pressed(keyboard_report); + + /* If no keys are pressed anymore, take care of checking and deactivating stuff */ + if (!state->key_pressed) { all_keys_released_handler(state); } - // If keys need to go to output B, send them through UART, otherwise send a HID report directly + /* If keys need to go to output B, send them through UART, otherwise send a HID report directly */ if (state->active_output == ACTIVE_OUTPUT_B) { send_packet(raw_report, KEYBOARD_REPORT_MSG, KBD_REPORT_LENGTH); - } else { - tud_hid_keyboard_report(REPORT_ID_KEYBOARD, keyboard_report->modifier, - keyboard_report->keycode); - + } else { + queue_kbd_report(keyboard_report, state); state->last_activity[ACTIVE_OUTPUT_A] = time_us_64(); } } @@ -78,7 +143,7 @@ void process_keyboard_report(uint8_t* raw_report, int length, device_state_t* st bool keypress_check(hotkey_combo_t keypress, const hid_keyboard_report_t* report) { int matches = 0; - // We expect all modifiers specified to be detected in the report + /* We expect all modifiers specified to be detected in the report */ if (keypress.modifier != (report->modifier & keypress.modifier)) return false; @@ -89,13 +154,12 @@ bool keypress_check(hotkey_combo_t keypress, const hid_keyboard_report_t* report break; } } - // If any of the keys are not found, we can bail out early. + /* If any of the keys are not found, we can bail out early. */ if (matches < n + 1) { return false; } } - // Getting here means all of the keys were found. + /* Getting here means all of the keys were found. */ return true; -} - +} \ No newline at end of file diff --git a/src/led.c b/src/led.c index 4a8d9e6..b75c631 100644 --- a/src/led.c +++ b/src/led.c @@ -1,3 +1,20 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" /**==================================================== * @@ -7,6 +24,10 @@ void update_leds(device_state_t* state) { gpio_put(GPIO_LED_PIN, state->active_output == BOARD_ROLE); - if (BOARD_ROLE == KEYBOARD_PICO_A) - pio_usb_kbd_set_leds(state->usb_device, 0, state->keyboard_leds[state->active_output]); + // TODO: Will be done in a callback + if (BOARD_ROLE == PICO_A) { + uint8_t* leds = &(state->keyboard_leds[state->active_output]); + tuh_hid_set_report(global_state.kbd_dev_addr, global_state.kbd_instance, 0, + HID_REPORT_TYPE_OUTPUT, leds, sizeof(uint8_t)); + } } diff --git a/src/main.c b/src/main.c index 36574d8..872752f 100644 --- a/src/main.c +++ b/src/main.c @@ -1,3 +1,20 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" /********* Global Variable **********/ @@ -11,7 +28,8 @@ void main(void) { // Wait for the board to settle sleep_ms(10); - global_state.usb_device = initial_setup(); + // Initial board setup + initial_setup(); // Initial state, A is the default output switch_output(ACTIVE_OUTPUT_A); @@ -20,21 +38,16 @@ void main(void) { // USB device task, needs to run as often as possible tud_task(); - // If we are not yet connected to the PC, don't bother with host - // If host task becomes too slow, move it to the second core - if (global_state.tud_connected) { - // Execute HOST task periodically - pio_usb_host_task(); - - // Query devices and handle reports - if (global_state.usb_device && global_state.usb_device->connected) { - check_endpoints(&global_state); - } - } - // Verify core1 is still running and if so, reset watchdog timer kick_watchdog(); + + // Check if there were any keypresses and send them + process_kbd_queue_task(&global_state); + + // Check if there were any mouse movements and send them + process_mouse_queue_task(&global_state); } + } void core1_main() { @@ -44,6 +57,10 @@ void core1_main() { // Update the timestamp, so core0 can figure out if we're dead global_state.core1_last_loop_pass = time_us_64(); + // USB host task, needs to run as often as possible + tuh_task(); + + // Receives data over serial from the other board receive_char(&in_packet, &global_state); } } diff --git a/src/main.h b/src/main.h index bf7008c..6208f0c 100644 --- a/src/main.h +++ b/src/main.h @@ -1,3 +1,20 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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 . + */ + #pragma once #include "pico/stdlib.h" @@ -10,14 +27,16 @@ #include "pico/bootrom.h" #include "pico/multicore.h" #include "pico/stdlib.h" +#include "pico/util/queue.h" #include "pio_usb.h" #include "tusb.h" #include "usb_descriptors.h" +#include "hid_parser.h" #include "user_config.h" /********* Misc definitions **********/ -#define KEYBOARD_PICO_A 0 -#define MOUSE_PICO_B 1 +#define PICO_A 0 +#define PICO_B 1 #define ACTIVE_OUTPUT_A 0 #define ACTIVE_OUTPUT_B 1 @@ -25,21 +44,28 @@ #define ENABLE 1 #define DISABLE 0 +#define DIRECTION_X 0 +#define DIRECTION_Y 1 + +#define MAX_REPORT_ITEMS 16 +#define MOUSE_BOOT_REPORT_LEN 4 + /********* Pinout definitions **********/ #define PIO_USB_DP_PIN 14 // D+ is pin 14, D- is pin 15 #define GPIO_LED_PIN 25 // LED is connected to pin 25 on a PICO -#if BOARD_ROLE == MOUSE_PICO_B +#if BOARD_ROLE == PICO_B #define SERIAL_TX_PIN 16 #define SERIAL_RX_PIN 17 -#elif BOARD_ROLE == KEYBOARD_PICO_A +#elif BOARD_ROLE == PICO_A #define SERIAL_TX_PIN 12 #define SERIAL_RX_PIN 13 #endif /********* Serial port definitions **********/ #define SERIAL_UART uart0 -#define SERIAL_BAUDRATE 115200 +#define SERIAL_BAUDRATE 3686400 + #define SERIAL_DATA_BITS 8 #define SERIAL_STOP_BITS 1 #define SERIAL_PARITY UART_PARITY_NONE @@ -66,6 +92,7 @@ enum packet_type_e : uint8_t { FIRMWARE_UPGRADE_MSG = 4, MOUSE_ZOOM_MSG = 5, KBD_SET_REPORT_MSG = 6, + SWITCH_LOCK_MSG = 7, }; /* @@ -96,6 +123,9 @@ typedef struct { #define PACKET_LENGTH (TYPE_LENGTH + PACKET_DATA_LENGTH + CHECKSUM_LENGTH) #define RAW_PACKET_LENGTH (START_LENGTH + PACKET_LENGTH) +#define KBD_QUEUE_LENGTH 128 +#define MOUSE_QUEUE_LENGTH 256 + #define KEYS_IN_USB_REPORT 6 #define KBD_REPORT_LENGTH 8 #define MOUSE_REPORT_LENGTH 7 @@ -125,7 +155,9 @@ typedef struct TU_ATTR_PACKED { typedef enum { IDLE, READING_PACKET, PROCESSING_PACKET } receiver_state_t; typedef struct { - usb_device_t* usb_device; // USB device structure (keyboard or mouse) + uint8_t kbd_dev_addr; // Address of the keyboard device + uint8_t kbd_instance; // Keyboard instance (d'uh - isn't this a useless comment) + uint8_t keyboard_leds[2]; // State of keyboard LEDs (index 0 = A, index 1 = B) uint64_t last_activity[2]; // Timestamp of the last input activity (-||-) receiver_state_t receiver_state; // Storing the state for the simple receiver state machine @@ -136,8 +168,17 @@ typedef struct { int16_t mouse_x; // Store and update the location of our mouse pointer int16_t mouse_y; + mouse_t mouse_dev; // Mouse device specifics, e.g. stores locations for keys in report + queue_t kbd_queue; // Queue that stores keyboard reports + queue_t mouse_queue; // Queue that stores mouse reports + bool tud_connected; // True when TinyUSB device successfully connects + bool keyboard_connected; // True when our keyboard is connected locally + bool mouse_connected; // True when a mouse is connected locally bool mouse_zoom; // True when "mouse zoom" is enabled + bool switch_lock; // True when device is prevented from switching + + bool key_pressed; // We are holding down a key (from the PCs point of view) } device_state_t; @@ -146,13 +187,16 @@ void process_mouse_report(uint8_t*, int, device_state_t*); void check_endpoints(device_state_t* state); /********* Setup **********/ -usb_device_t* initial_setup(void); +void initial_setup(void); void serial_init(void); void core1_main(void); /********* Keyboard **********/ bool keypress_check(hotkey_combo_t, const hid_keyboard_report_t*); void process_keyboard_report(uint8_t*, int, device_state_t*); +void stop_pressing_any_keys(device_state_t*); +void queue_kbd_report(hid_keyboard_report_t*, device_state_t*); +void process_kbd_queue_task(device_state_t*); /********* Mouse **********/ bool tud_hid_abs_mouse_report(uint8_t report_id, @@ -162,6 +206,11 @@ bool tud_hid_abs_mouse_report(uint8_t report_id, int8_t vertical, int8_t horizontal); +uint8_t parse_report_descriptor(mouse_t* mouse, uint8_t arr_count, uint8_t const* desc_report, uint16_t desc_len); +int32_t get_report_value(uint8_t* report, report_val_t *val); +void process_mouse_queue_task(device_state_t*); +void queue_mouse_report(hid_abs_mouse_report_t*, device_state_t*); + /********* UART **********/ void receive_char(uart_packet_t*, device_state_t*); void send_packet(const uint8_t*, enum packet_type_e, int); @@ -179,9 +228,11 @@ void kick_watchdog(void); /********* Handlers **********/ void output_toggle_hotkey_handler(device_state_t*); -void fw_upgrade_hotkey_handler(device_state_t*); +void fw_upgrade_hotkey_handler_A(device_state_t*); +void fw_upgrade_hotkey_handler_B(device_state_t*); void mouse_zoom_hotkey_handler(device_state_t*); void all_keys_released_handler(device_state_t*); +void switchlock_hotkey_handler(device_state_t*); void handle_keyboard_uart_msg(uart_packet_t*, device_state_t*); void handle_mouse_abs_uart_msg(uart_packet_t*, device_state_t*); @@ -189,6 +240,7 @@ void handle_output_select_msg(uart_packet_t*, device_state_t*); void handle_fw_upgrade_msg(void); void handle_mouse_zoom_msg(uart_packet_t*, device_state_t*); void handle_set_report_msg(uart_packet_t*, device_state_t*); +void handle_switch_lock_msg(uart_packet_t*, device_state_t*); void switch_output(uint8_t); diff --git a/src/mouse.c b/src/mouse.c index 90d24b0..582873f 100644 --- a/src/mouse.c +++ b/src/mouse.c @@ -1,72 +1,189 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" -int get_mouse_offset(int8_t movement) { - // Holding a special hotkey enables mouse to slow down as much as possible - // when you need that extra precision - if (global_state.mouse_zoom) - return movement * MOUSE_SPEED_FACTOR >> 2; +int get_mouse_offset(int32_t movement, const int direction) { + int offset = 0; + + if (direction == DIRECTION_X) + offset = movement * MOUSE_SPEED_FACTOR_X; else - return movement * MOUSE_SPEED_FACTOR; + offset = movement * MOUSE_SPEED_FACTOR_Y; + + /* Holding a special hotkey enables mouse to slow down as much as possible + when you need that extra precision */ + if (global_state.mouse_zoom) + offset = offset >> 2; + + return offset; } -void keep_cursor_on_screen(int16_t* position, const int8_t* movement) { - int16_t offset = get_mouse_offset(*movement); +void keep_cursor_on_screen(int16_t* position, const int32_t* movement, const int direction) { + int16_t offset = get_mouse_offset(*movement, direction); - // Lowest we can go is 0 + /* Lowest we can go is 0 */ if (*position + offset < 0) *position = 0; - // Highest we can go is MAX_SCREEN_COORD + /* Highest we can go is MAX_SCREEN_COORD */ else if (*position + offset > MAX_SCREEN_COORD) *position = MAX_SCREEN_COORD; - // We're still on screen, all good + /* We're still on screen, all good */ else *position += offset; } -void check_mouse_switch(const hid_mouse_report_t* mouse_report, device_state_t* state) { - // End of screen right switches screen B->A - if ((state->mouse_x + mouse_report->x) > MAX_SCREEN_COORD && - state->active_output == ACTIVE_OUTPUT_B) { - state->mouse_x = 0; - switch_output(ACTIVE_OUTPUT_A); + +void check_mouse_switch(const mouse_values_t* values, device_state_t* state) { + hid_abs_mouse_report_t report = {.y = 0, .x = MAX_SCREEN_COORD}; + + /* No switching allowed if explicitly disabled */ + if (state->switch_lock) return; + + /* End of screen left switches screen A->B */ + bool jump_from_A_to_B = (state->mouse_x + values->move_x < -MOUSE_JUMP_THRESHOLD && + state->active_output == ACTIVE_OUTPUT_A); + + /* End of screen right switches screen B->A */ + bool jump_from_B_to_A = (state->mouse_x + values->move_x > MAX_SCREEN_COORD + MOUSE_JUMP_THRESHOLD && + state->active_output == ACTIVE_OUTPUT_B); + + if (jump_from_A_to_B || jump_from_B_to_A) { + /* Hide mouse pointer in the upper right corner on the system we are switching FROM + If the mouse is locally attached to the current board or notify other board if not */ + if (state->active_output == state->mouse_connected) + queue_mouse_report(&report, state); + else + send_packet((const uint8_t*)&report, MOUSE_REPORT_MSG, MOUSE_REPORT_LENGTH); + + if (jump_from_A_to_B) { + switch_output(ACTIVE_OUTPUT_B); + state->mouse_x = MAX_SCREEN_COORD; + } else { + switch_output(ACTIVE_OUTPUT_A); + state->mouse_x = 0; + } + } +} + +void extract_values_report_protocol(uint8_t* report, + device_state_t* state, + mouse_values_t* values) { + /* If Report ID is used, the report is prefixed by the report ID so we have to move by 1 byte */ + if (state->mouse_dev.uses_report_id) { + /* Move past the ID to parse the report */ + report++; } - // End of screen left switches screen A->B - if ((state->mouse_x + mouse_report->x) < 0 && state->active_output == ACTIVE_OUTPUT_A) { - state->mouse_x = MAX_SCREEN_COORD; - switch_output(ACTIVE_OUTPUT_B); - return; + values->move_x = get_report_value(report, &state->mouse_dev.move_x); + values->move_y = get_report_value(report, &state->mouse_dev.move_y); + values->wheel = get_report_value(report, &state->mouse_dev.wheel); + values->buttons = get_report_value(report, &state->mouse_dev.buttons); + + /* Mice generally come in 3 categories - 8-bit, 12-bit and 16-bit. */ + switch (state->mouse_dev.move_x.size) { + case 12: + /* If we're already 12 bit, great! */ + break; + case 16: + /* Initially we downscale fancy mice to 12-bits, + adding a 32-bit internal coordinate tracking is TODO */ + values->move_x >>= 4; + values->move_y >>= 4; + break; + default: + /* 8-bit is the default, upscale to 12-bit. */ + values->move_x <<= 4; + values->move_y <<= 4; } } -void process_mouse_report(uint8_t* raw_report, int len, device_state_t* state) { - hid_mouse_report_t* mouse_report = (hid_mouse_report_t*)raw_report; +void extract_values_boot_protocol(uint8_t* report, device_state_t* state, mouse_values_t* values) { + hid_mouse_report_t* mouse_report = (hid_mouse_report_t*)report; + /* For 8-bit values, we upscale them to 12-bit, TODO: 16 bit */ + values->move_x = mouse_report->x << 4; + values->move_y = mouse_report->y << 4; + values->wheel = mouse_report->wheel; + values->buttons = mouse_report->buttons; +} - // We need to enforce the cursor doesn't go off-screen, that would be bad. - keep_cursor_on_screen(&state->mouse_x, &mouse_report->x); - keep_cursor_on_screen(&state->mouse_y, &mouse_report->y); +void process_mouse_report(uint8_t* raw_report, int len, device_state_t* state) { + mouse_values_t values = {0}; + + /* Interpret values depending on the current protocol used */ + if (state->mouse_dev.protocol == HID_PROTOCOL_BOOT) + extract_values_boot_protocol(raw_report, state, &values); + else + extract_values_report_protocol(raw_report, state, &values); + + /* We need to enforce the cursor doesn't go off-screen, that would be bad. */ + keep_cursor_on_screen(&state->mouse_x, &values.move_x, DIRECTION_X); + keep_cursor_on_screen(&state->mouse_y, &values.move_y, DIRECTION_Y); + + hid_abs_mouse_report_t abs_mouse_report = { + .buttons = values.buttons, + .x = state->mouse_x, + .y = state->mouse_y, + .wheel = values.wheel, + .pan = 0 + }; if (state->active_output == ACTIVE_OUTPUT_A) { - hid_abs_mouse_report_t abs_mouse_report; - - abs_mouse_report.buttons = mouse_report->buttons; - abs_mouse_report.x = state->mouse_x; - abs_mouse_report.y = state->mouse_y; - abs_mouse_report.wheel = mouse_report->wheel; - abs_mouse_report.pan = 0; - send_packet((const uint8_t*)&abs_mouse_report, MOUSE_REPORT_MSG, MOUSE_REPORT_LENGTH); - } else { - tud_hid_abs_mouse_report(REPORT_ID_MOUSE, mouse_report->buttons, state->mouse_x, - state->mouse_y, mouse_report->wheel, 0); - + queue_mouse_report(&abs_mouse_report, state); state->last_activity[ACTIVE_OUTPUT_B] = time_us_64(); } - // We use the mouse to switch outputs, the logic is in check_mouse_switch() - check_mouse_switch(mouse_report, state); -} \ No newline at end of file + /* We use the mouse to switch outputs, the logic is in check_mouse_switch() */ + check_mouse_switch(&values, state); +} + +/* ==================================================== * + * Mouse Queue Section + * ==================================================== */ + +void process_mouse_queue_task(device_state_t* state) { + hid_abs_mouse_report_t report = {0}; + + /* We need to be connected to the host to send messages */ + if (!state->tud_connected) + return; + + /* Peek first, if there is anything there... */ + if (!queue_try_peek(&state->mouse_queue, &report)) + return; + + /* ... try sending it to the host, if it's successful */ + bool succeeded = tud_hid_abs_mouse_report(REPORT_ID_MOUSE, report.buttons, report.x, report.y, + report.wheel, report.pan); + + /* ... then we can remove it from the queue */ + if (succeeded) + queue_try_remove(&state->mouse_queue, &report); +} + +void queue_mouse_report(hid_abs_mouse_report_t* report, device_state_t* state) { + /* It wouldn't be fun to queue up a bunch of messages and then dump them all on host */ + if (!state->tud_connected) + return; + + queue_try_add(&state->mouse_queue, report); +} diff --git a/src/setup.c b/src/setup.c index ac41217..ef8c47d 100644 --- a/src/setup.c +++ b/src/setup.c @@ -1,3 +1,20 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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 . + */ + /**================================================== * * ============= Initial Board Setup ============== * * ================================================== */ @@ -34,19 +51,21 @@ void serial_init() { * PIO USB configuration, D+ pin 14, D- pin 15 * ================================================== */ -usb_device_t* pio_usb_init(void) { +void pio_usb_host_config(void) { + /* tuh_configure() must be called before tuh_init() */ static pio_usb_configuration_t config = PIO_USB_DEFAULT_CONFIG; - config.pin_dp = 14; - config.alarm_pool = (void*)alarm_pool_create(2, 1); + config.pin_dp = PIO_USB_DP_PIN; + tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &config); - return pio_usb_host_init(&config); + /* Initialize and configure TinyUSB Host */ + tuh_init(1); } /* ================================================== * * Perform initial board/usb setup * ================================================== */ -usb_device_t* initial_setup(void) { +void initial_setup(void) { /* PIO USB requires a clock multiple of 12 MHz, setting to 120 MHz */ set_sys_clock_khz(120000, true); @@ -57,23 +76,25 @@ usb_device_t* initial_setup(void) { /* Initialize and configure UART */ serial_init(); - /* Initialize and configure TinyUSB */ - tusb_init(); - + /* Initialize keyboard and mouse queues */ + queue_init(&global_state.kbd_queue, sizeof(hid_keyboard_report_t), KBD_QUEUE_LENGTH); + queue_init(&global_state.mouse_queue, sizeof(hid_abs_mouse_report_t), MOUSE_QUEUE_LENGTH); + /* Setup RP2040 Core 1 */ multicore_reset_core1(); multicore_launch_core1(core1_main); - /* Initialize and configure PIO USB */ - usb_device_t* pio_usb_device = pio_usb_init(); + /* Initialize and configure TinyUSB Device */ + tud_init(BOARD_TUD_RHPORT); + + /* Initialize and configure TinyUSB Host */ + pio_usb_host_config(); /* Update the core1 initial pass timestamp before enabling the watchdog */ global_state.core1_last_loop_pass = time_us_64(); /* Setup the watchdog so we reboot and recover from a crash */ watchdog_enable(WATCHDOG_TIMEOUT, WATCHDOG_PAUSE_ON_DEBUG); - - return pio_usb_device; } -/* ========== End of Initial Board Setup ========== */ \ No newline at end of file +/* ========== End of Initial Board Setup ========== */ diff --git a/src/tusb_config.h b/src/tusb_config.h index 868424e..04e72a4 100644 --- a/src/tusb_config.h +++ b/src/tusb_config.h @@ -34,6 +34,21 @@ // COMMON CONFIGURATION //-------------------------------------------------------------------- +#define CFG_TUSB_OS OPT_OS_PICO + +// Enable device stack +#define CFG_TUD_ENABLED 1 + +// RHPort number used for device is port 0 +#define BOARD_TUD_RHPORT 0 + +// RHPort number used for host is port 1 +#define BOARD_TUH_RHPORT 1 + +// Enable host stack with pio-usb if Pico-PIO-USB library is available +#define CFG_TUH_ENABLED 1 +#define CFG_TUH_RPI_PIO_USB 1 + // defined by board.mk #ifndef CFG_TUSB_MCU #error CFG_TUSB_MCU must be defined @@ -56,17 +71,8 @@ #endif // Device mode with rhport and speed defined by board.mk -#if BOARD_DEVICE_RHPORT_NUM == 0 - #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) -#elif BOARD_DEVICE_RHPORT_NUM == 1 - #define CFG_TUSB_RHPORT1_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) -#else - #error "Incorrect RHPort configuration" -#endif - -#ifndef CFG_TUSB_OS -#define CFG_TUSB_OS OPT_OS_NONE -#endif +#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) +#define CFG_TUSB_RHPORT1_MODE (OPT_MODE_HOST | BOARD_DEVICE_RHPORT_SPEED) // CFG_TUSB_DEBUG is defined by compiler in DEBUG build // #define CFG_TUSB_DEBUG 0 @@ -102,7 +108,22 @@ #define CFG_TUD_VENDOR 0 // HID buffer size Should be sufficient to hold ID (if any) + Data -#define CFG_TUD_HID_EP_BUFSIZE 16 +#define CFG_TUD_HID_EP_BUFSIZE 32 + +//-------------------------------------------------------------------- +// HOST CONFIGURATION +//-------------------------------------------------------------------- + +// Size of buffer to hold descriptors and other data used for enumeration +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 +// max device support (excluding hub device) +#define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports + +#define CFG_TUH_HID 4 +#define CFG_TUH_HID_EPIN_BUFSIZE 64 +#define CFG_TUH_HID_EPOUT_BUFSIZE 64 #ifdef __cplusplus } diff --git a/src/uart.c b/src/uart.c index 9b16043..563bac7 100644 --- a/src/uart.c +++ b/src/uart.c @@ -1,5 +1,21 @@ -#include "main.h" +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" /**================================================== * * =============== Sending Packets ================ * @@ -58,6 +74,10 @@ void process_packet(uart_packet_t* packet, device_state_t* state) { case KBD_SET_REPORT_MSG: handle_set_report_msg(packet, state); break; + + case SWITCH_LOCK_MSG: + handle_switch_lock_msg(packet, state); + break; } } @@ -72,10 +92,10 @@ void receive_char(uart_packet_t* packet, device_state_t* state) { switch (state->receiver_state) { case IDLE: if (uart_is_readable(SERIAL_UART)) { - raw_packet[0] = raw_packet[1]; // Remember the previous byte received - raw_packet[1] = uart_getc(SERIAL_UART); // ... and try to match packet start + raw_packet[0] = raw_packet[1]; /* Remember the previous byte received */ + raw_packet[1] = uart_getc(SERIAL_UART); /* ... and try to match packet start */ - // If we found 0xAA 0x55, we're in sync and can move on to read/process the packet + /* If we found 0xAA 0x55, we're in sync and can move on to read/process the packet */ if (raw_packet[0] == START1 && raw_packet[1] == START2) { state->receiver_state = READING_PACKET; } @@ -86,7 +106,7 @@ void receive_char(uart_packet_t* packet, device_state_t* state) { if (uart_is_readable(SERIAL_UART)) { raw_packet[count++] = uart_getc(SERIAL_UART); - // Check if a complete packet is received + /* Check if a complete packet is received */ if (count >= PACKET_LENGTH) { state->receiver_state = PROCESSING_PACKET; } @@ -96,7 +116,7 @@ void receive_char(uart_packet_t* packet, device_state_t* state) { case PROCESSING_PACKET: process_packet(packet, state); - // Cleanup and return to IDLE when done + /* Cleanup and return to IDLE when done */ count = 0; state->receiver_state = IDLE; break; diff --git a/src/usb.c b/src/usb.c index 44aae76..d095216 100644 --- a/src/usb.c +++ b/src/usb.c @@ -1,35 +1,29 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" -/**================================================== * - * ========== Query endpoints for reports ========= * - * ================================================== */ - -void check_endpoints(device_state_t* state) { - uint8_t raw_report[64]; - - // Iterate through all endpoints and check for data - for (int ep_idx = 0; ep_idx < PIO_USB_DEV_EP_CNT; ep_idx++) { - endpoint_t* ep = pio_usb_get_endpoint(state->usb_device, ep_idx); - - if (ep == NULL) { - continue; - } - - int len = pio_usb_get_in_data(ep, raw_report, sizeof(raw_report)); - - if (len > 0) { - if (BOARD_ROLE == KEYBOARD_PICO_A) - process_keyboard_report(raw_report, len, state); - else - process_mouse_report(raw_report, len, state); - } - } -} - /**================================================== * * =========== TinyUSB Device Callbacks =========== * * ================================================== */ +/* Invoked when we get GET_REPORT control request. + * We are expected to fill buffer with the report content, update reqlen + * and return its length. We return 0 to STALL the request. */ uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, @@ -58,33 +52,111 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t leds = buffer[0]; if (KBD_LED_AS_INDICATOR) { - leds = leds & 0xFD; // 1111 1101 (Clear Caps Lock bit) + leds = leds & 0xFD; /* 1111 1101 (Clear Caps Lock bit) */ if (global_state.active_output) leds |= KEYBOARD_LED_CAPSLOCK; } - - global_state.keyboard_leds[global_state.active_output] = leds; - // If we are board B, we need to set this information to the other one since that one - // has the keyboard connected to it (and LEDs you can turn on :-)) - if (BOARD_ROLE == MOUSE_PICO_B) - send_value(leds, KBD_SET_REPORT_MSG); + global_state.keyboard_leds[global_state.active_output] = leds; - // If we are board A, update LEDs directly - else + /* If we are board without the keyboard hooked up directly, we need to send this information + to the other one since that one has the keyboard connected to it (and LEDs you can turn on :)) */ + if (global_state.keyboard_connected) update_leds(&global_state); + else + send_value(leds, KBD_SET_REPORT_MSG); } } -// Invoked when device is mounted -void tud_mount_cb(void) -{ - global_state.tud_connected = true; +/* Invoked when device is mounted */ +void tud_mount_cb(void) { + global_state.tud_connected = true; } -// Invoked when device is unmounted -void tud_umount_cb(void) -{ - global_state.tud_connected = false; -} \ No newline at end of file +/* Invoked when device is unmounted */ +void tud_umount_cb(void) { + global_state.tud_connected = false; +} + +/**================================================== * + * =============== USB HOST Section =============== * + * ================================================== */ + +void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) { + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + switch (itf_protocol) { + case HID_ITF_PROTOCOL_KEYBOARD: + global_state.keyboard_connected = false; + break; + + case HID_ITF_PROTOCOL_MOUSE: + global_state.mouse_connected = false; + + /* Clear this so reconnecting a mouse doesn't try to continue in HID REPORT protocol */ + memset(&global_state.mouse_dev, 0, sizeof(global_state.mouse_dev)); + break; + } +} + +void tuh_hid_mount_cb(uint8_t dev_addr, + uint8_t instance, + uint8_t const* desc_report, + uint16_t desc_len) { + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + + switch (itf_protocol) { + case HID_ITF_PROTOCOL_KEYBOARD: + /* Keeping this is needed for setting leds from device set_report callback */ + global_state.kbd_dev_addr = dev_addr; + global_state.kbd_instance = instance; + + global_state.keyboard_connected = true; + break; + + case HID_ITF_PROTOCOL_MOUSE: + /* Switch to using protocol report instead of boot report, it's more complicated but + at least we get all the information we need (looking at you, mouse wheel) */ + if (tuh_hid_get_protocol(dev_addr, instance) == HID_PROTOCOL_BOOT) { + tuh_hid_set_protocol(dev_addr, instance, HID_PROTOCOL_REPORT); + } + + parse_report_descriptor(&global_state.mouse_dev, MAX_REPORTS, desc_report, desc_len); + + global_state.mouse_connected = true; + break; + } + + /* Kick off the report querying */ + tuh_hid_receive_report(dev_addr, instance); +} + +/* Invoked when received report from device via interrupt endpoint */ +void tuh_hid_report_received_cb(uint8_t dev_addr, + uint8_t instance, + uint8_t const* report, + uint16_t len) { + (void)len; + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + + switch (itf_protocol) { + case HID_ITF_PROTOCOL_KEYBOARD: + process_keyboard_report((uint8_t*)report, len, &global_state); + break; + + case HID_ITF_PROTOCOL_MOUSE: + process_mouse_report((uint8_t*)report, len, &global_state); + break; + } + + /* Continue requesting reports */ + tuh_hid_receive_report(dev_addr, instance); +} + +/* Set protocol in a callback. If we were called, command succeeded. We're only + doing this for the mouse anyway, so we can only be called about the mouse */ +void tuh_hid_set_protocol_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t protocol) { + (void) dev_addr; + (void) idx; + global_state.mouse_dev.protocol = protocol; +} diff --git a/src/user_config.h b/src/user_config.h index f0da291..27626c3 100644 --- a/src/user_config.h +++ b/src/user_config.h @@ -10,7 +10,7 @@ * * */ -#define KBD_LED_AS_INDICATOR 1 +#define KBD_LED_AS_INDICATOR 0 /**===================================================== * * =========== Hotkey for output switching =========== * @@ -33,9 +33,18 @@ * * This affects how fast the mouse moves. * - * MOUSE_SPEED_FACTOR: [1-128], higher values will make very little sense, - * 16 works well for my mouse, but the option to adjust is here if you need it. + * MOUSE_SPEED_FACTOR_X: [1-128], mouse moves at this speed in X direction + * MOUSE_SPEED_FACTOR_Y: [1-128], mouse moves at this speed in Y direction + * + * MOUSE_JUMP_THRESHOLD: [0-32768], sets the "force" you need to use to drag the + * mouse to another screen, 0 meaning no force needed at all, and ~500 some force + * needed, ~1000 no accidental jumps, you need to really mean it. + * + * TODO: make this configurable per-screen. * * */ -#define MOUSE_SPEED_FACTOR 16 +#define MOUSE_SPEED_FACTOR_X 1 +#define MOUSE_SPEED_FACTOR_Y 1 + +#define MOUSE_JUMP_THRESHOLD 0 diff --git a/src/utils.c b/src/utils.c index 1e85b66..deb71fe 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,10 +1,27 @@ +/* + * This file is part of DeskHop (https://github.com/hrvach/deskhop). + * Copyright (c) 2024 Hrvoje Cavrak + * + * 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" /**================================================== * * ============== Checksum Functions ============== * * ================================================== */ -uint8_t calc_checksum(const uint8_t* data, int length) { +uint8_t calc_checksum(const uint8_t* data, int length) { uint8_t checksum = 0; for (int i = 0; i < length; i++) { @@ -22,16 +39,15 @@ bool verify_checksum(const uart_packet_t* packet) { /**================================================== * * ============== Watchdog Functions ============== * * ================================================== */ - + void kick_watchdog(void) { - // Read the timer AFTER duplicating the core1 timestamp, - // so it doesn't get updated in the meantime. + /* Read the timer AFTER duplicating the core1 timestamp, + so it doesn't get updated in the meantime. */ uint64_t core1_last_loop_pass = global_state.core1_last_loop_pass; uint64_t current_time = time_us_64(); - // If core1 stops updating the timestamp, we'll stop kicking the watchog and reboot - if (current_time - core1_last_loop_pass < CORE1_HANG_TIMEOUT_US) + /* If core1 stops updating the timestamp, we'll stop kicking the watchog and reboot */ + if (current_time - core1_last_loop_pass < CORE1_HANG_TIMEOUT_US) watchdog_update(); } -