Skip to content

Commit

Permalink
New features, bugfixes and optimizations
Browse files Browse the repository at this point in the history
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"
  • Loading branch information
Hrvoje Cavrak committed Jan 3, 2024
1 parent 6e4eea4 commit 560f3dc
Show file tree
Hide file tree
Showing 17 changed files with 956 additions and 188 deletions.
11 changes: 9 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
)
Expand All @@ -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})

Expand All @@ -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})
Expand Down
Binary file modified binaries/board_A.uf2
Binary file not shown.
Binary file modified binaries/board_B.uf2
Binary file not shown.
68 changes: 53 additions & 15 deletions src/handlers.c
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

#include "main.h"

/**=================================================== *
* ============ Hotkey Handler Routines ============ *
* =================================================== */

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;
Expand All @@ -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);
}
Expand All @@ -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);
}
210 changes: 210 additions & 0 deletions src/hid_parser.c
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#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; j<sizeof(usage_map)/sizeof(usage_map[0]); j++, map++) {
/* Filter based on usage criteria */
if (map->report_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 <count> times and increase offset by <size> amount, moving by <count> x <size> 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;
}
Loading

0 comments on commit 560f3dc

Please sign in to comment.