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"
This commit is contained in:
Hrvoje Cavrak 2024-01-03 10:48:34 +01:00
parent 6e4eea4b27
commit 560f3dca74
17 changed files with 958 additions and 190 deletions

View File

@ -6,6 +6,8 @@ set(PICO_BOARD=pico)
include(pico_sdk_import.cmake) include(pico_sdk_import.cmake)
set(CMAKE_CXX_FLAGS "-Ofast -Wall -mcpu=cortex-m0plus -mtune=cortex-m0plus") set(CMAKE_CXX_FLAGS "-Ofast -Wall -mcpu=cortex-m0plus -mtune=cortex-m0plus")
set(PICO_COPY_TO_RAM 1)
project(deskhop_project C CXX ASM) project(deskhop_project C CXX ASM)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
@ -31,6 +33,7 @@ target_include_directories(Pico-PIO-USB PRIVATE ${PICO_PIO_USB_DIR})
set(COMMON_SOURCES set(COMMON_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/usb_descriptors.c ${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/utils.c
${CMAKE_CURRENT_LIST_DIR}/src/handlers.c ${CMAKE_CURRENT_LIST_DIR}/src/handlers.c
${CMAKE_CURRENT_LIST_DIR}/src/setup.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/uart.c
${CMAKE_CURRENT_LIST_DIR}/src/usb.c ${CMAKE_CURRENT_LIST_DIR}/src/usb.c
${CMAKE_CURRENT_LIST_DIR}/src/main.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 set(COMMON_INCLUDES
@ -51,8 +56,10 @@ set(COMMON_LINK_LIBRARIES
pico_stdlib pico_stdlib
hardware_uart hardware_uart
hardware_gpio hardware_gpio
hardware_pio
tinyusb_device tinyusb_device
tinyusb_host
pico_multicore pico_multicore
Pico-PIO-USB Pico-PIO-USB
) )
@ -61,7 +68,7 @@ set(COMMON_LINK_LIBRARIES
add_executable(board_A) add_executable(board_A)
target_sources(board_A PUBLIC ${COMMON_SOURCES}) 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_include_directories(board_A PUBLIC ${COMMON_INCLUDES})
target_link_libraries(board_A PUBLIC ${COMMON_LINK_LIBRARIES}) target_link_libraries(board_A PUBLIC ${COMMON_LINK_LIBRARIES})
@ -72,7 +79,7 @@ pico_add_extra_outputs(board_A)
# Pico B - Mouse # Pico B - Mouse
add_executable(board_B) 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_sources(board_B PUBLIC ${COMMON_SOURCES})
target_include_directories(board_B PUBLIC ${COMMON_INCLUDES}) target_include_directories(board_B PUBLIC ${COMMON_INCLUDES})
target_link_libraries(board_B PUBLIC ${COMMON_LINK_LIBRARIES}) target_link_libraries(board_B PUBLIC ${COMMON_LINK_LIBRARIES})

Binary file not shown.

Binary file not shown.

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #include "main.h"
/**=================================================== * /**=================================================== *
@ -5,15 +22,31 @@
* =================================================== */ * =================================================== */
void output_toggle_hotkey_handler(device_state_t* state) { void output_toggle_hotkey_handler(device_state_t* state) {
/* If switching explicitly disabled, return immediately */
if (state->switch_lock)
return;
state->active_output ^= 1; state->active_output ^= 1;
switch_output(state->active_output); switch_output(state->active_output);
}; };
void fw_upgrade_hotkey_handler(device_state_t* state) { /* This key combo puts board A in firmware upgrade mode */
send_value(ENABLE, FIRMWARE_UPGRADE_MSG); void fw_upgrade_hotkey_handler_A(device_state_t* state) {
reset_usb_boot(1 << PICO_DEFAULT_LED_PIN, 0); 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) { void mouse_zoom_hotkey_handler(device_state_t* state) {
if (state->mouse_zoom) if (state->mouse_zoom)
return; 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) { void handle_keyboard_uart_msg(uart_packet_t* packet, device_state_t* state) {
if (state->active_output == ACTIVE_OUTPUT_B) { if (state->active_output == ACTIVE_OUTPUT_B) {
hid_keyboard_report_t* report = (hid_keyboard_report_t*)packet->data; queue_kbd_report((hid_keyboard_report_t*)packet->data, state);
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, report->modifier, report->keycode);
state->last_activity[ACTIVE_OUTPUT_B] = time_us_64(); state->last_activity[ACTIVE_OUTPUT_B] = time_us_64();
} }
} }
void handle_mouse_abs_uart_msg(uart_packet_t* packet, device_state_t* state) { void handle_mouse_abs_uart_msg(uart_packet_t* packet, device_state_t* state) {
if (state->active_output == ACTIVE_OUTPUT_A) { hid_abs_mouse_report_t* mouse_report = (hid_abs_mouse_report_t*)packet->data;
const 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();
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();
}
} }
void handle_output_select_msg(uart_packet_t* packet, device_state_t* state) { void handle_output_select_msg(uart_packet_t* packet, device_state_t* state) {
state->active_output = packet->data[0]; state->active_output = packet->data[0];
if (state->tud_connected)
stop_pressing_any_keys(&global_state);
update_leds(state); update_leds(state);
} }
/* On firmware upgrade message, reboot into the BOOTSEL fw upgrade mode */
void handle_fw_upgrade_msg(void) { void handle_fw_upgrade_msg(void) {
reset_usb_boot(1 << PICO_DEFAULT_LED_PIN, 0); 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) { 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]; state->keyboard_leds[ACTIVE_OUTPUT_B] = packet->data[0];
update_leds(state); 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) { void switch_output(uint8_t new_output) {
global_state.active_output = new_output; global_state.active_output = new_output;
update_leds(&global_state); update_leds(&global_state);
send_value(new_output, OUTPUT_SELECT_MSG); 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
src/hid_parser.c Normal file
View File

@ -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;
}

83
src/hid_parser.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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;

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #include "main.h"
/* ==================================================== * /* ==================================================== *
@ -5,26 +22,74 @@
* ==================================================== */ * ==================================================== */
hotkey_combo_t hotkeys[] = { hotkey_combo_t hotkeys[] = {
// Main keyboard switching hotkey /* Main keyboard switching hotkey */
{.modifier = 0, {.modifier = 0,
.keys = {HOTKEY_TOGGLE}, .keys = {HOTKEY_TOGGLE},
.key_count = 1, .key_count = 1,
.action_handler = &output_toggle_hotkey_handler}, .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, {.modifier = KEYBOARD_MODIFIER_RIGHTALT,
.keys = {}, .keys = {},
.key_count = 0, .key_count = 0,
.action_handler = &mouse_zoom_hotkey_handler}, .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, {.modifier = KEYBOARD_MODIFIER_RIGHTSHIFT | KEYBOARD_MODIFIER_LEFTSHIFT,
.keys = {HID_KEY_P, HID_KEY_H, HID_KEY_X}, .keys = {HID_KEY_F12, HID_KEY_A},
.key_count = 3, .key_count = 2,
.action_handler = &fw_upgrade_hotkey_handler} .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 * 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) if (length < KBD_REPORT_LENGTH)
return; 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++) { for (int n = 0; n < sizeof(hotkeys) / sizeof(hotkeys[0]); n++) {
if (keypress_check(hotkeys[n], keyboard_report)) { if (keypress_check(hotkeys[n], keyboard_report)) {
hotkeys[n].action_handler(state); hotkeys[n].action_handler(state);
return; return;
} }
} }
// If no keys are pressed anymore, take care of checking and deactivating stuff state->key_pressed = !no_keys_are_pressed(keyboard_report);
if (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); 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) { if (state->active_output == ACTIVE_OUTPUT_B) {
send_packet(raw_report, KEYBOARD_REPORT_MSG, KBD_REPORT_LENGTH); send_packet(raw_report, KEYBOARD_REPORT_MSG, KBD_REPORT_LENGTH);
} else { } else {
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, keyboard_report->modifier, queue_kbd_report(keyboard_report, state);
keyboard_report->keycode);
state->last_activity[ACTIVE_OUTPUT_A] = time_us_64(); 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) { bool keypress_check(hotkey_combo_t keypress, const hid_keyboard_report_t* report) {
int matches = 0; 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)) if (keypress.modifier != (report->modifier & keypress.modifier))
return false; return false;
@ -89,13 +154,12 @@ bool keypress_check(hotkey_combo_t keypress, const hid_keyboard_report_t* report
break; 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) { if (matches < n + 1) {
return false; return false;
} }
} }
// Getting here means all of the keys were found. /* Getting here means all of the keys were found. */
return true; return true;
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #include "main.h"
/**==================================================== * /**==================================================== *
@ -7,6 +24,10 @@
void update_leds(device_state_t* state) { void update_leds(device_state_t* state) {
gpio_put(GPIO_LED_PIN, state->active_output == BOARD_ROLE); gpio_put(GPIO_LED_PIN, state->active_output == BOARD_ROLE);
if (BOARD_ROLE == KEYBOARD_PICO_A) // TODO: Will be done in a callback
pio_usb_kbd_set_leds(state->usb_device, 0, state->keyboard_leds[state->active_output]); 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));
}
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #include "main.h"
/********* Global Variable **********/ /********* Global Variable **********/
@ -11,7 +28,8 @@ void main(void) {
// Wait for the board to settle // Wait for the board to settle
sleep_ms(10); sleep_ms(10);
global_state.usb_device = initial_setup(); // Initial board setup
initial_setup();
// Initial state, A is the default output // Initial state, A is the default output
switch_output(ACTIVE_OUTPUT_A); switch_output(ACTIVE_OUTPUT_A);
@ -20,21 +38,16 @@ void main(void) {
// USB device task, needs to run as often as possible // USB device task, needs to run as often as possible
tud_task(); 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 // Verify core1 is still running and if so, reset watchdog timer
kick_watchdog(); 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() { void core1_main() {
@ -44,6 +57,10 @@ void core1_main() {
// Update the timestamp, so core0 can figure out if we're dead // Update the timestamp, so core0 can figure out if we're dead
global_state.core1_last_loop_pass = time_us_64(); 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); receive_char(&in_packet, &global_state);
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once #pragma once
#include "pico/stdlib.h" #include "pico/stdlib.h"
@ -10,14 +27,16 @@
#include "pico/bootrom.h" #include "pico/bootrom.h"
#include "pico/multicore.h" #include "pico/multicore.h"
#include "pico/stdlib.h" #include "pico/stdlib.h"
#include "pico/util/queue.h"
#include "pio_usb.h" #include "pio_usb.h"
#include "tusb.h" #include "tusb.h"
#include "usb_descriptors.h" #include "usb_descriptors.h"
#include "hid_parser.h"
#include "user_config.h" #include "user_config.h"
/********* Misc definitions **********/ /********* Misc definitions **********/
#define KEYBOARD_PICO_A 0 #define PICO_A 0
#define MOUSE_PICO_B 1 #define PICO_B 1
#define ACTIVE_OUTPUT_A 0 #define ACTIVE_OUTPUT_A 0
#define ACTIVE_OUTPUT_B 1 #define ACTIVE_OUTPUT_B 1
@ -25,21 +44,28 @@
#define ENABLE 1 #define ENABLE 1
#define DISABLE 0 #define DISABLE 0
#define DIRECTION_X 0
#define DIRECTION_Y 1
#define MAX_REPORT_ITEMS 16
#define MOUSE_BOOT_REPORT_LEN 4
/********* Pinout definitions **********/ /********* Pinout definitions **********/
#define PIO_USB_DP_PIN 14 // D+ is pin 14, D- is pin 15 #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 #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_TX_PIN 16
#define SERIAL_RX_PIN 17 #define SERIAL_RX_PIN 17
#elif BOARD_ROLE == KEYBOARD_PICO_A #elif BOARD_ROLE == PICO_A
#define SERIAL_TX_PIN 12 #define SERIAL_TX_PIN 12
#define SERIAL_RX_PIN 13 #define SERIAL_RX_PIN 13
#endif #endif
/********* Serial port definitions **********/ /********* Serial port definitions **********/
#define SERIAL_UART uart0 #define SERIAL_UART uart0
#define SERIAL_BAUDRATE 115200 #define SERIAL_BAUDRATE 3686400
#define SERIAL_DATA_BITS 8 #define SERIAL_DATA_BITS 8
#define SERIAL_STOP_BITS 1 #define SERIAL_STOP_BITS 1
#define SERIAL_PARITY UART_PARITY_NONE #define SERIAL_PARITY UART_PARITY_NONE
@ -66,6 +92,7 @@ enum packet_type_e : uint8_t {
FIRMWARE_UPGRADE_MSG = 4, FIRMWARE_UPGRADE_MSG = 4,
MOUSE_ZOOM_MSG = 5, MOUSE_ZOOM_MSG = 5,
KBD_SET_REPORT_MSG = 6, 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 PACKET_LENGTH (TYPE_LENGTH + PACKET_DATA_LENGTH + CHECKSUM_LENGTH)
#define RAW_PACKET_LENGTH (START_LENGTH + PACKET_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 KEYS_IN_USB_REPORT 6
#define KBD_REPORT_LENGTH 8 #define KBD_REPORT_LENGTH 8
#define MOUSE_REPORT_LENGTH 7 #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 enum { IDLE, READING_PACKET, PROCESSING_PACKET } receiver_state_t;
typedef struct { 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) 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 (-||-) 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 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_x; // Store and update the location of our mouse pointer
int16_t mouse_y; 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 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 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; } device_state_t;
@ -146,13 +187,16 @@ void process_mouse_report(uint8_t*, int, device_state_t*);
void check_endpoints(device_state_t* state); void check_endpoints(device_state_t* state);
/********* Setup **********/ /********* Setup **********/
usb_device_t* initial_setup(void); void initial_setup(void);
void serial_init(void); void serial_init(void);
void core1_main(void); void core1_main(void);
/********* Keyboard **********/ /********* Keyboard **********/
bool keypress_check(hotkey_combo_t, const hid_keyboard_report_t*); bool keypress_check(hotkey_combo_t, const hid_keyboard_report_t*);
void process_keyboard_report(uint8_t*, int, device_state_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 **********/ /********* Mouse **********/
bool tud_hid_abs_mouse_report(uint8_t report_id, 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 vertical,
int8_t horizontal); 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 **********/ /********* UART **********/
void receive_char(uart_packet_t*, device_state_t*); void receive_char(uart_packet_t*, device_state_t*);
void send_packet(const uint8_t*, enum packet_type_e, int); void send_packet(const uint8_t*, enum packet_type_e, int);
@ -179,9 +228,11 @@ void kick_watchdog(void);
/********* Handlers **********/ /********* Handlers **********/
void output_toggle_hotkey_handler(device_state_t*); 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 mouse_zoom_hotkey_handler(device_state_t*);
void all_keys_released_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_keyboard_uart_msg(uart_packet_t*, device_state_t*);
void handle_mouse_abs_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_fw_upgrade_msg(void);
void handle_mouse_zoom_msg(uart_packet_t*, device_state_t*); void handle_mouse_zoom_msg(uart_packet_t*, device_state_t*);
void handle_set_report_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); void switch_output(uint8_t);

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #include "main.h"
int get_mouse_offset(int8_t movement) { int get_mouse_offset(int32_t movement, const int direction) {
// Holding a special hotkey enables mouse to slow down as much as possible int offset = 0;
// when you need that extra precision
if (global_state.mouse_zoom) if (direction == DIRECTION_X)
return movement * MOUSE_SPEED_FACTOR >> 2; offset = movement * MOUSE_SPEED_FACTOR_X;
else 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) { void keep_cursor_on_screen(int16_t* position, const int32_t* movement, const int direction) {
int16_t offset = get_mouse_offset(*movement); int16_t offset = get_mouse_offset(*movement, direction);
// Lowest we can go is 0 /* Lowest we can go is 0 */
if (*position + offset < 0) if (*position + offset < 0)
*position = 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) else if (*position + offset > MAX_SCREEN_COORD)
*position = MAX_SCREEN_COORD; *position = MAX_SCREEN_COORD;
// We're still on screen, all good /* We're still on screen, all good */
else else
*position += offset; *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 void check_mouse_switch(const mouse_values_t* values, device_state_t* state) {
if ((state->mouse_x + mouse_report->x) > MAX_SCREEN_COORD && hid_abs_mouse_report_t report = {.y = 0, .x = MAX_SCREEN_COORD};
state->active_output == ACTIVE_OUTPUT_B) {
state->mouse_x = 0; /* No switching allowed if explicitly disabled */
switch_output(ACTIVE_OUTPUT_A); if (state->switch_lock)
return; 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 values->move_x = get_report_value(report, &state->mouse_dev.move_x);
if ((state->mouse_x + mouse_report->x) < 0 && state->active_output == ACTIVE_OUTPUT_A) { values->move_y = get_report_value(report, &state->mouse_dev.move_y);
state->mouse_x = MAX_SCREEN_COORD; values->wheel = get_report_value(report, &state->mouse_dev.wheel);
switch_output(ACTIVE_OUTPUT_B); values->buttons = get_report_value(report, &state->mouse_dev.buttons);
return;
/* 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) { 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*)raw_report; 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. void process_mouse_report(uint8_t* raw_report, int len, device_state_t* state) {
keep_cursor_on_screen(&state->mouse_x, &mouse_report->x); mouse_values_t values = {0};
keep_cursor_on_screen(&state->mouse_y, &mouse_report->y);
/* 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) { 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); send_packet((const uint8_t*)&abs_mouse_report, MOUSE_REPORT_MSG, MOUSE_REPORT_LENGTH);
} else { } else {
tud_hid_abs_mouse_report(REPORT_ID_MOUSE, mouse_report->buttons, state->mouse_x, queue_mouse_report(&abs_mouse_report, state);
state->mouse_y, mouse_report->wheel, 0);
state->last_activity[ACTIVE_OUTPUT_B] = time_us_64(); state->last_activity[ACTIVE_OUTPUT_B] = time_us_64();
} }
// We use the mouse to switch outputs, the logic is in check_mouse_switch() /* We use the mouse to switch outputs, the logic is in check_mouse_switch() */
check_mouse_switch(mouse_report, state); 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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/**================================================== * /**================================================== *
* ============= Initial Board Setup ============== * * ============= Initial Board Setup ============== *
* ================================================== */ * ================================================== */
@ -34,19 +51,21 @@ void serial_init() {
* PIO USB configuration, D+ pin 14, D- pin 15 * 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; static pio_usb_configuration_t config = PIO_USB_DEFAULT_CONFIG;
config.pin_dp = 14; config.pin_dp = PIO_USB_DP_PIN;
config.alarm_pool = (void*)alarm_pool_create(2, 1); 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 * 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 */ /* PIO USB requires a clock multiple of 12 MHz, setting to 120 MHz */
set_sys_clock_khz(120000, true); set_sys_clock_khz(120000, true);
@ -57,23 +76,25 @@ usb_device_t* initial_setup(void) {
/* Initialize and configure UART */ /* Initialize and configure UART */
serial_init(); serial_init();
/* Initialize and configure TinyUSB */ /* Initialize keyboard and mouse queues */
tusb_init(); 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 */ /* Setup RP2040 Core 1 */
multicore_reset_core1(); multicore_reset_core1();
multicore_launch_core1(core1_main); multicore_launch_core1(core1_main);
/* Initialize and configure PIO USB */ /* Initialize and configure TinyUSB Device */
usb_device_t* pio_usb_device = pio_usb_init(); tud_init(BOARD_TUD_RHPORT);
/* Initialize and configure TinyUSB Host */
pio_usb_host_config();
/* Update the core1 initial pass timestamp before enabling the watchdog */ /* Update the core1 initial pass timestamp before enabling the watchdog */
global_state.core1_last_loop_pass = time_us_64(); global_state.core1_last_loop_pass = time_us_64();
/* Setup the watchdog so we reboot and recover from a crash */ /* Setup the watchdog so we reboot and recover from a crash */
watchdog_enable(WATCHDOG_TIMEOUT, WATCHDOG_PAUSE_ON_DEBUG); watchdog_enable(WATCHDOG_TIMEOUT, WATCHDOG_PAUSE_ON_DEBUG);
return pio_usb_device;
} }
/* ========== End of Initial Board Setup ========== */ /* ========== End of Initial Board Setup ========== */

View File

@ -34,6 +34,21 @@
// COMMON CONFIGURATION // 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 // defined by board.mk
#ifndef CFG_TUSB_MCU #ifndef CFG_TUSB_MCU
#error CFG_TUSB_MCU must be defined #error CFG_TUSB_MCU must be defined
@ -56,17 +71,8 @@
#endif #endif
// Device mode with rhport and speed defined by board.mk // 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)
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) #define CFG_TUSB_RHPORT1_MODE (OPT_MODE_HOST | 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
// CFG_TUSB_DEBUG is defined by compiler in DEBUG build // CFG_TUSB_DEBUG is defined by compiler in DEBUG build
// #define CFG_TUSB_DEBUG 0 // #define CFG_TUSB_DEBUG 0
@ -102,7 +108,22 @@
#define CFG_TUD_VENDOR 0 #define CFG_TUD_VENDOR 0
// HID buffer size Should be sufficient to hold ID (if any) + Data // 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 #ifdef __cplusplus
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h"
/**================================================== * /**================================================== *
* =============== Sending Packets ================ * * =============== Sending Packets ================ *
@ -58,6 +74,10 @@ void process_packet(uart_packet_t* packet, device_state_t* state) {
case KBD_SET_REPORT_MSG: case KBD_SET_REPORT_MSG:
handle_set_report_msg(packet, state); handle_set_report_msg(packet, state);
break; 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) { switch (state->receiver_state) {
case IDLE: case IDLE:
if (uart_is_readable(SERIAL_UART)) { if (uart_is_readable(SERIAL_UART)) {
raw_packet[0] = raw_packet[1]; // Remember the previous byte received 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[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) { if (raw_packet[0] == START1 && raw_packet[1] == START2) {
state->receiver_state = READING_PACKET; 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)) { if (uart_is_readable(SERIAL_UART)) {
raw_packet[count++] = uart_getc(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) { if (count >= PACKET_LENGTH) {
state->receiver_state = PROCESSING_PACKET; state->receiver_state = PROCESSING_PACKET;
} }
@ -96,7 +116,7 @@ void receive_char(uart_packet_t* packet, device_state_t* state) {
case PROCESSING_PACKET: case PROCESSING_PACKET:
process_packet(packet, state); process_packet(packet, state);
// Cleanup and return to IDLE when done /* Cleanup and return to IDLE when done */
count = 0; count = 0;
state->receiver_state = IDLE; state->receiver_state = IDLE;
break; break;

160
src/usb.c
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #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 =========== * * =========== 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, uint16_t tud_hid_get_report_cb(uint8_t instance,
uint8_t report_id, uint8_t report_id,
hid_report_type_t report_type, hid_report_type_t report_type,
@ -58,33 +52,111 @@ void tud_hid_set_report_cb(uint8_t instance,
uint8_t leds = buffer[0]; uint8_t leds = buffer[0];
if (KBD_LED_AS_INDICATOR) { 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) if (global_state.active_output)
leds |= KEYBOARD_LED_CAPSLOCK; 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 global_state.keyboard_leds[global_state.active_output] = leds;
// 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);
// If we are board A, update LEDs directly /* If we are board without the keyboard hooked up directly, we need to send this information
else 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); update_leds(&global_state);
else
send_value(leds, KBD_SET_REPORT_MSG);
} }
} }
// Invoked when device is mounted /* Invoked when device is mounted */
void tud_mount_cb(void) void tud_mount_cb(void) {
{ global_state.tud_connected = true;
global_state.tud_connected = true;
} }
// Invoked when device is unmounted /* Invoked when device is unmounted */
void tud_umount_cb(void) void tud_umount_cb(void) {
{ global_state.tud_connected = false;
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;
}

View File

@ -10,7 +10,7 @@
* *
* */ * */
#define KBD_LED_AS_INDICATOR 1 #define KBD_LED_AS_INDICATOR 0
/**===================================================== * /**===================================================== *
* =========== Hotkey for output switching =========== * * =========== Hotkey for output switching =========== *
@ -33,9 +33,18 @@
* *
* This affects how fast the mouse moves. * This affects how fast the mouse moves.
* *
* MOUSE_SPEED_FACTOR: [1-128], higher values will make very little sense, * MOUSE_SPEED_FACTOR_X: [1-128], mouse moves at this speed in X direction
* 16 works well for my mouse, but the option to adjust is here if you need it. * 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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "main.h" #include "main.h"
/**================================================== * /**================================================== *
* ============== Checksum Functions ============== * * ============== 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; uint8_t checksum = 0;
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
@ -22,16 +39,15 @@ bool verify_checksum(const uart_packet_t* packet) {
/**================================================== * /**================================================== *
* ============== Watchdog Functions ============== * * ============== Watchdog Functions ============== *
* ================================================== */ * ================================================== */
void kick_watchdog(void) { void kick_watchdog(void) {
// Read the timer AFTER duplicating the core1 timestamp, /* Read the timer AFTER duplicating the core1 timestamp,
// so it doesn't get updated in the meantime. so it doesn't get updated in the meantime. */
uint64_t core1_last_loop_pass = global_state.core1_last_loop_pass; uint64_t core1_last_loop_pass = global_state.core1_last_loop_pass;
uint64_t current_time = time_us_64(); uint64_t current_time = time_us_64();
// If core1 stops updating the timestamp, we'll stop kicking the watchog and reboot /* 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 (current_time - core1_last_loop_pass < CORE1_HANG_TIMEOUT_US)
watchdog_update(); watchdog_update();
} }