Features, bugfixes and configuration.

- Support for storing config in flash
- Support for keyboard and mouse in any port (hopefully)
- Single-sided operation should work with a USB hub
- Added mouse switch cursor height/offset simple calibration
- Added per-screen settings support
- Mouse speed is configurable per-screen and per-axis
- Small fixes and cleanup
- Added LED feedback
- Updated documentation for usage guide
This commit is contained in:
Hrvoje Cavrak 2024-01-16 18:38:24 +01:00
parent 2711b911ee
commit 99a6e3bf20
19 changed files with 900 additions and 290 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build

View File

@ -33,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/defaults.c
${CMAKE_CURRENT_LIST_DIR}/src/hid_parser.c
${CMAKE_CURRENT_LIST_DIR}/src/utils.c
${CMAKE_CURRENT_LIST_DIR}/src/handlers.c
@ -54,6 +55,7 @@ set(COMMON_INCLUDES
set(COMMON_LINK_LIBRARIES
pico_stdlib
hardware_flash
hardware_uart
hardware_gpio
hardware_pio
@ -96,3 +98,6 @@ target_link_options(board_B PRIVATE
-Xlinker
--print-memory-usage
)
pico_set_linker_script(board_A ${CMAKE_SOURCE_DIR}/memory_map.ld)
pico_set_linker_script(board_B ${CMAKE_SOURCE_DIR}/memory_map.ld)

View File

@ -67,10 +67,7 @@ It should appear as a USB drive on your system. Copy the corresponding board_A.u
Option 1 - Open the case, hold the button while connecting each Pico and copy the right uf2 to it.
Option 2 - Switch a board to BOOTSEL mode by using a special key combination. (**hold down** all of these keys).
- board A: ```Right Shift, F12, A, Left Shift```
- board B: ```Right Shift, F12, B, Left Shift```
Option 2 - Switch a board to BOOTSEL mode by using a special key combination (listed below).
This will make the corresponding Pico board enter the bootloader upgrade mode and act as USB flash drive. Now you can drag-and-drop the .uf2 file to it (you might need to plug in your mouse directly).
@ -153,6 +150,43 @@ The standard process to do that is using isopropyl alcohol and an old toothbrush
[![PCB Assembly Guide](img/yt-video-s.jpg)](https://www.youtube.com/watch?v=LxI9NYi_oOU)
## Usage guide
#### Keyboard shortcuts
_Firmware upgrade_
```Right Shift + F12 + Left Shift + A``` - put board A in FW upgrade mode
```Right Shift + F12 + Left Shift + B``` - put board B in FW upgrade mode
_Usage_
```Right ALT``` - mouse slows down while it's pressed
```Right CTRL + L``` - Lock/Unlock mouse desktop switching
```Caps Lock``` - Switch between outputs
_Config_
```Right Shift + F12 + D``` - remove flash config
```Right Shift + F12 + Y``` - save screen switch offset
#### Switch cursor height calibration
This step is not required, but it can be handy if your screens are not perfectly aligned or differ in size. The objective is to have the mouse pointer come out at exactly the same height.
![Image](img/border_top_s.png)
Just park your mouse on the LARGER screen at the height of the smaller/lower screen (illustrated) and press ```Right Shift + F12 + Y```. Your LED (and caps lock) should flash in confirmation.
Repeat for the bottom border (if it's above the larger screen's border). This will get saved to flash and it should keep this calibration value from now on.
#### Other configuration
Mouse speed can now be configured per output screen and per axis. If you have multiple displays under Linux, your X speed is probably too fast, so you need to configure it in user_config.h and rebuild. In the future, this will be configurable without having to do that.
#### Functional verification
When you connect a new USB peripheral, the board will flash the led twice, and instruct the other board to do the same. This way you can test if USB and outgoing communication works for each board.
Do this test by first plugging the keyboard on one side and then on the other. If everything is OK, leds will flash quickly back and forth in both cases.
## FAQ
1. I just have two Picos, can I do without a PCB and isolator?
@ -177,7 +211,7 @@ Yes, the idea was to make it behave like it was one single computer.
5. Will this work with keyboard/mouse combo dongles, like the Logitech Unifying receiver?
Not with the current version, but there is work ongoing to add support. Testing is one of the main problems, so if there is anyone with the device and some Logitech gear, let me know.
Not tested yet, but the latest version might actually work (please provide feedback).
6. Will this work with wireless mice and keyboards that have separate wireless receivers (one for the mouse, another for the keyboard)?
@ -199,12 +233,12 @@ There are several software alternatives you can use if that works in your partic
## Shortcomings
- Slow mouse movement with some devices.
- Windows 10 broke HID absolute coordinates behavior in KB5003637, so you can't use more than 1 screen on Windows (mouse will stay on the main screen).
- If you have more than one display, the mouse is faster in the X direction on that machine. Will get fixed with per-output configurable speed settings.
- Code needs cleanup, some refactoring etc.
- Occasional bugs and weird behavior.
- Not tested with a wide variety of devices, I don't know how it will work with your hardware. There is a reasonable chance things might not work out-of-the-box.
- Advanced keyboards (with knobs, extra buttons or sliders) will probably face issues where this additional hardware doesn't work.
- Super-modern mice with 300 buttons might see some buttons not work as expected.
- NOTE: Both computers need to be connected and powered on for this to work (as each board gets powered by the computer it plugs into).
## Progress
@ -214,10 +248,10 @@ So, what's the deal with all the enthusiasm? I can't believe it - please allow m
Planned features:
- ~~Proper TinyUSB host integration~~ (done)
- ~~HID report protocol parsing, not just boot protocol~~ (mostly done)
- Support for unified dongle receivers
- ~~Support for USB hubs~~ and single-sided operation
- Configurable screens
- Permament configuration stored in flash
- ~~Support for unified dongle receivers~~
- ~~Support for USB hubs and single-sided operation~~
- Configurable screens (partially)
- ~~Permament configuration stored in flash~~
- Unified firmware for both Picos
- ... and more!

Binary file not shown.

Binary file not shown.

BIN
img/border_top_s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

262
memory_map.ld Normal file
View File

@ -0,0 +1,262 @@
/* Based on GCC ARM embedded samples.
Defines the following symbols for use by code:
__exidx_start
__exidx_end
__etext
__data_start__
__preinit_array_start
__preinit_array_end
__init_array_start
__init_array_end
__fini_array_start
__fini_array_end
__data_end__
__bss_start__
__bss_end__
__end__
end
__HeapLimit
__StackLimit
__StackTop
__stack (== StackTop)
*/
__CONFIG_STORAGE_LEN = 4k;
MEMORY
{
FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k - __CONFIG_STORAGE_LEN
FLASH_CONFIG(rw) : ORIGIN = 0x10000000 + (2048k - __CONFIG_STORAGE_LEN), LENGTH = __CONFIG_STORAGE_LEN
RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k
SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}
ENTRY(_entry_point)
SECTIONS
{
/* Second stage bootloader is prepended to the image. It must be 256 bytes big
and checksummed. It is usually built by the boot_stage2 target
in the Raspberry Pi Pico SDK
*/
.flash_begin : {
__flash_binary_start = .;
} > FLASH
.boot2 : {
__boot2_start__ = .;
KEEP (*(.boot2))
__boot2_end__ = .;
} > FLASH
ASSERT(__boot2_end__ - __boot2_start__ == 256,
"ERROR: Pico second stage bootloader must be 256 bytes in size")
/* The second stage will always enter the image at the start of .text.
The debugger will use the ELF entry point, which is the _entry_point
symbol if present, otherwise defaults to start of .text.
This can be used to transfer control back to the bootrom on debugger
launches only, to perform proper flash setup.
*/
.flashtext : {
__logical_binary_start = .;
KEEP (*(.vectors))
KEEP (*(.binary_info_header))
__binary_info_header_end = .;
KEEP (*(.reset))
}
.rodata : {
/* segments not marked as .flashdata are instead pulled into .data (in RAM) to avoid accidental flash accesses */
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*)))
. = ALIGN(4);
} > FLASH
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
__exidx_start = .;
.ARM.exidx :
{
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > FLASH
__exidx_end = .;
/* Machine inspectable binary information */
. = ALIGN(4);
__binary_info_start = .;
.binary_info :
{
KEEP(*(.binary_info.keep.*))
*(.binary_info.*)
} > FLASH
__binary_info_end = .;
. = ALIGN(4);
/* Vector table goes first in RAM, to avoid large alignment hole */
.ram_vector_table (NOLOAD): {
*(.ram_vector_table)
} > RAM
.text : {
__ram_text_start__ = .;
*(.init)
*(.text*)
*(.fini)
/* Pull all c'tors into .text */
*crtbegin.o(.ctors)
*crtbegin?.o(.ctors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
*(SORT(.ctors.*))
*(.ctors)
/* Followed by destructors */
*crtbegin.o(.dtors)
*crtbegin?.o(.dtors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
*(SORT(.dtors.*))
*(.dtors)
*(.eh_frame*)
. = ALIGN(4);
__ram_text_end__ = .;
} > RAM AT> FLASH
__ram_text_source__ = LOADADDR(.text);
. = ALIGN(4);
.data : {
__data_start__ = .;
*(vtable)
*(.time_critical*)
. = ALIGN(4);
*(.rodata*)
. = ALIGN(4);
*(.data*)
. = ALIGN(4);
*(.after_data.*)
. = ALIGN(4);
/* preinit data */
PROVIDE_HIDDEN (__mutex_array_start = .);
KEEP(*(SORT(.mutex_array.*)))
KEEP(*(.mutex_array))
PROVIDE_HIDDEN (__mutex_array_end = .);
. = ALIGN(4);
/* preinit data */
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP(*(SORT(.preinit_array.*)))
KEEP(*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
/* init data */
PROVIDE_HIDDEN (__init_array_start = .);
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
/* finit data */
PROVIDE_HIDDEN (__fini_array_start = .);
*(SORT(.fini_array.*))
*(.fini_array)
PROVIDE_HIDDEN (__fini_array_end = .);
*(.jcr)
. = ALIGN(4);
/* All data end */
__data_end__ = .;
} > RAM AT> FLASH
/* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */
__etext = LOADADDR(.data);
.uninitialized_data (NOLOAD): {
. = ALIGN(4);
*(.uninitialized_data*)
} > RAM
/* Start and end symbols must be word-aligned */
.scratch_x : {
__scratch_x_start__ = .;
*(.scratch_x.*)
. = ALIGN(4);
__scratch_x_end__ = .;
} > SCRATCH_X AT > FLASH
__scratch_x_source__ = LOADADDR(.scratch_x);
.scratch_y : {
__scratch_y_start__ = .;
*(.scratch_y.*)
. = ALIGN(4);
__scratch_y_end__ = .;
} > SCRATCH_Y AT > FLASH
__scratch_y_source__ = LOADADDR(.scratch_y);
.bss : {
. = ALIGN(4);
__bss_start__ = .;
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
} > RAM
.heap (NOLOAD):
{
__end__ = .;
end = __end__;
KEEP(*(.heap*))
__HeapLimit = .;
} > RAM
/* Configuration flash section (4k in size, end of flash) */
.section_config : {
"ADDR_CONFIG" = .;
} > FLASH_CONFIG
/* .stack*_dummy section doesn't contains any symbols. It is only
* used for linker to calculate size of stack sections, and assign
* values to stack symbols later
*
* stack1 section may be empty/missing if platform_launch_core1 is not used */
/* by default we put core 0 stack at the end of scratch Y, so that if core 1
* stack is not used then all of SCRATCH_X is free.
*/
.stack1_dummy (NOLOAD):
{
*(.stack1*)
} > SCRATCH_X
.stack_dummy (NOLOAD):
{
KEEP(*(.stack*))
} > SCRATCH_Y
.flash_end : {
__flash_binary_end = .;
} > FLASH
/* stack limit is poorly named, but historically is maximum heap ptr */
__StackLimit = ORIGIN(RAM) + LENGTH(RAM);
__StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X);
__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);
__StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy);
__StackBottom = __StackTop - SIZEOF(.stack_dummy);
PROVIDE(__stack = __StackTop);
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")
ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary")
/* todo assert on extra code */
}

30
src/defaults.c Normal file
View File

@ -0,0 +1,30 @@
#include "main.h"
/* Default configuration */
const config_t default_config = {
.magic_header = 0x0B00B1E5,
.output[ACTIVE_OUTPUT_A] =
{
.number = ACTIVE_OUTPUT_A,
.speed_x = MOUSE_SPEED_A_FACTOR_X,
.speed_y = MOUSE_SPEED_A_FACTOR_Y,
.border = {
.top = 0,
.bottom = MAX_SCREEN_COORD,
},
.screen_count = 1,
.screen_index = 0,
},
.output[ACTIVE_OUTPUT_B] =
{
.number = ACTIVE_OUTPUT_B,
.speed_x = MOUSE_SPEED_B_FACTOR_X,
.speed_y = MOUSE_SPEED_B_FACTOR_Y,
.border = {
.top = 0,
.bottom = MAX_SCREEN_COORD,
},
.screen_count = 1,
.screen_index = 0,
},
};

View File

@ -1,17 +1,17 @@
/*
/*
* 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
*
* 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
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -30,6 +30,20 @@ void output_toggle_hotkey_handler(device_state_t* state) {
switch_output(state->active_output);
};
/* This key combo records switch y top coordinate for different-size monitors */
void screen_border_hotkey_handler(device_state_t* state) {
border_size_t *border = &state->config.output[state->active_output].border;
/* To deal away with 2 different keys, if we're above half, it's the top coord and vice versa */
if (state->mouse_y > (MAX_SCREEN_COORD / 2))
border->bottom = state->mouse_y;
else
border->top = state->mouse_y;
send_packet((uint8_t*)border, SYNC_BORDERS_MSG, sizeof(border_size_t));
save_config();
};
/* 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);
@ -40,12 +54,17 @@ void fw_upgrade_hotkey_handler_B(device_state_t* state) {
send_value(ENABLE, FIRMWARE_UPGRADE_MSG);
};
/* This key combo prevents mouse from switching outputs */
void switchlock_hotkey_handler(device_state_t* state) {
state->switch_lock ^= 1;
send_value(state->switch_lock, SWITCH_LOCK_MSG);
send_value(state->switch_lock, SWITCH_LOCK_MSG);
}
/* When pressed, erases stored config in flash and loads defaults */
void wipe_config_hotkey_handler(device_state_t* state) {
wipe_config();
load_config();
}
void mouse_zoom_hotkey_handler(device_state_t* state) {
if (state->mouse_zoom)
@ -67,24 +86,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) {
queue_kbd_report((hid_keyboard_report_t*)packet->data, state);
state->last_activity[ACTIVE_OUTPUT_B] = time_us_64();
}
queue_kbd_report((hid_keyboard_report_t*)packet->data, state);
state->last_activity[BOARD_ROLE] = time_us_64();
}
void handle_mouse_abs_uart_msg(uart_packet_t* packet, device_state_t* state) {
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();
hid_abs_mouse_report_t* mouse_report = (hid_abs_mouse_report_t*)packet->data;
queue_mouse_report(mouse_report, state);
state->mouse_x = mouse_report->x;
state->mouse_y = mouse_report->y;
state->last_activity[BOARD_ROLE] = 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);
restore_leds(state);
}
/* On firmware upgrade message, reboot into the BOOTSEL fw upgrade mode */
@ -98,18 +119,30 @@ 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 */
state->keyboard_leds[ACTIVE_OUTPUT_B] = packet->data[0];
update_leds(state);
state->keyboard_leds[BOARD_ROLE] = packet->data[0];
restore_leds(state);
}
void handle_switch_lock_msg(uart_packet_t* packet, device_state_t* state) {
state->switch_lock = packet->data[0];
state->switch_lock = packet->data[0];
}
/* Handle border syncing message that lets the other device know about monitor height offset */
void handle_sync_borders_msg(uart_packet_t* packet, device_state_t* state) {
border_size_t *border = &state->config.output[state->active_output].border;
memcpy(border, packet->data, sizeof(border_size_t));
save_config();
}
/* When this message is received, flash the locally attached LED to verify serial comms */
void handle_flash_led_msg(uart_packet_t* packet, device_state_t* state) {
blink_led(state);
}
/* 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);
restore_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.

View File

@ -1,17 +1,17 @@
/*
/*
* 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
*
* 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
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -29,36 +29,52 @@ hotkey_combo_t hotkeys[] = {
.action_handler = &output_toggle_hotkey_handler},
/* Holding down right ALT slows the mouse down */
{.modifier = KEYBOARD_MODIFIER_RIGHTALT,
.keys = {},
{.modifier = KEYBOARD_MODIFIER_RIGHTALT,
.keys = {},
.key_count = 0,
.pass_to_os = true,
.action_handler = &mouse_zoom_hotkey_handler},
/* Switch lock */
{.modifier = KEYBOARD_MODIFIER_RIGHTCTRL,
.keys = {HID_KEY_L},
.key_count = 1,
.acknowledge = true,
.action_handler = &switchlock_hotkey_handler},
/* Erase stored config */
{.modifier = KEYBOARD_MODIFIER_RIGHTSHIFT,
.keys = {HID_KEY_F12, HID_KEY_D},
.key_count = 2,
.acknowledge = true,
.action_handler = &wipe_config_hotkey_handler},
/* Record switch y coordinate */
{.modifier = KEYBOARD_MODIFIER_RIGHTSHIFT,
.keys = {HID_KEY_F12, HID_KEY_Y},
.key_count = 2,
.acknowledge = true,
.action_handler = &screen_border_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_F12, HID_KEY_A},
.key_count = 2,
.acknowledge = true,
.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}
};
.acknowledge = true,
.action_handler = &fw_upgrade_hotkey_handler_B}};
/* ==================================================== *
* Keyboard Queue Section
* ==================================================== */
void process_kbd_queue_task(device_state_t *state) {
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. */
@ -77,21 +93,31 @@ void process_kbd_queue_task(device_state_t *state) {
queue_try_remove(&state->kbd_queue, &report);
}
void queue_kbd_report(hid_keyboard_report_t *report, device_state_t *state) {
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) {
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);
}
/* If keys need to go locally, queue packet to kbd queue, else send them through UART */
void send_key(hid_keyboard_report_t* report, device_state_t* state) {
if (state->active_output == BOARD_ROLE) {
queue_kbd_report(report, state);
state->last_activity[BOARD_ROLE] = time_us_64();
} else {
send_packet((uint8_t*)report, KEYBOARD_REPORT_MSG, KBD_REPORT_LENGTH);
}
}
/* ==================================================== *
* Parse and interpret the keys pressed on the keyboard
* Parse and interpret the keys pressed on the keyboard
* ==================================================== */
bool no_keys_are_pressed(const hid_keyboard_report_t* report) {
@ -105,42 +131,51 @@ bool no_keys_are_pressed(const hid_keyboard_report_t* report) {
return true;
}
void process_keyboard_report(uint8_t* raw_report, int length, device_state_t* state) {
hid_keyboard_report_t* keyboard_report = (hid_keyboard_report_t*)raw_report;
if (length < KBD_REPORT_LENGTH)
return;
/* Go through the list of hotkeys, check if any are pressed, then execute their handler */
hotkey_combo_t* check_hotkeys(hid_keyboard_report_t* report, int length, device_state_t* state) {
/* 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 (is_key_pressed(hotkeys[n], report)) {
return &hotkeys[n];
}
}
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 (state->active_output == ACTIVE_OUTPUT_B) {
send_packet(raw_report, KEYBOARD_REPORT_MSG, KBD_REPORT_LENGTH);
} else {
queue_kbd_report(keyboard_report, state);
state->last_activity[ACTIVE_OUTPUT_A] = time_us_64();
}
return NULL;
}
void process_keyboard_report(uint8_t* raw_report, int length, device_state_t* state) {
hid_keyboard_report_t* keyboard_report = (hid_keyboard_report_t*)raw_report;
hotkey_combo_t* hotkey = NULL;
if (length < KBD_REPORT_LENGTH)
return;
/* If no keys are pressed anymore, take care of checking and deactivating stuff */
if(no_keys_are_pressed(keyboard_report))
all_keys_released_handler(state);
else
/* Check if it was a hotkey, makes sense only if a key is pressed */
hotkey = check_hotkeys(keyboard_report, length, state);
/* ... and take appropriate action */
if(hotkey != NULL) {
/* Provide visual feedback we received the action */
if (hotkey->acknowledge)
blink_led(state);
hotkey->action_handler(state);
if (!hotkey->pass_to_os)
return;
}
/* This method will decide if the key gets queued locally or sent through UART */
send_key(keyboard_report, state);
}
/* ============================================================ *
* Check if a specific key combination is present in the report
* Check if a specific key combination is present in the report
* ============================================================ */
bool keypress_check(hotkey_combo_t keypress, const hid_keyboard_report_t* report) {
bool is_key_pressed(hotkey_combo_t keypress, const hid_keyboard_report_t* report) {
int matches = 0;
/* We expect all modifiers specified to be detected in the report */

View File

@ -1,17 +1,17 @@
/*
/*
* 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
*
* 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
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -21,13 +21,60 @@
* ========== Update pico and keyboard LEDs ========== *
* ==================================================== */
void update_leds(device_state_t* state) {
gpio_put(GPIO_LED_PIN, state->active_output == BOARD_ROLE);
void set_keyboard_leds(uint8_t requested_led_state, device_state_t* state) {
static uint8_t new_led_value;
// 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));
new_led_value = requested_led_state;
if (state->keyboard_connected) {
tuh_hid_set_report(state->kbd_dev_addr, state->kbd_instance, 0, HID_REPORT_TYPE_OUTPUT,
&new_led_value, sizeof(uint8_t));
}
}
void restore_leds(device_state_t* state) {
state->onboard_led_state = (state->active_output == BOARD_ROLE);
gpio_put(GPIO_LED_PIN, state->onboard_led_state);
if (state->keyboard_connected) {
uint8_t leds = state->keyboard_leds[state->active_output];
set_keyboard_leds(leds, state);
}
}
void blink_led(device_state_t* state) {
/* Since LEDs might be ON previously, we go OFF, ON, OFF, ON, OFF */
state->blinks_left = 5;
state->last_led_change = time_us_32();
}
void led_blinking_task(device_state_t* state) {
/* 50 ms off, 50 ms on */
const int blink_interval_us = 80000;
static uint8_t leds;
/* If there is no more blinking to be done, exit immediately */
if (state->blinks_left == 0)
return;
/* We have some blinks left to do, check if they are due, exit if not */
if ((time_us_32()) - state->last_led_change < blink_interval_us)
return;
/* Toggle the LED state */
uint8_t new_led_state = gpio_get(GPIO_LED_PIN) ^ 1;
gpio_put(GPIO_LED_PIN, new_led_state);
/* Also keyboard leds (if it's connected locally) since on-board leds are not visible */
leds = new_led_state * 0x07; /* Numlock, capslock, scrollock */
if (state->keyboard_connected)
set_keyboard_leds(leds, state);
/* Decrement the counter and update the last-changed timestamp */
state->blinks_left--;
state->last_led_change = time_us_32();
/* Restore LEDs in the last pass */
if (state->blinks_left == 0)
restore_leds(state);
}

View File

@ -33,7 +33,7 @@ void main(void) {
// Initial state, A is the default output
switch_output(ACTIVE_OUTPUT_A);
while (true) {
// USB device task, needs to run as often as possible
tud_task();
@ -58,11 +58,14 @@ void core1_main() {
global_state.core1_last_loop_pass = time_us_64();
// USB host task, needs to run as often as possible
if (tuh_inited())
if (tuh_inited())
tuh_task();
// Receives data over serial from the other board
receive_char(&in_packet, &global_state);
// Check if LED needs blinking
led_blinking_task(&global_state);
}
}

View File

@ -1,17 +1,17 @@
/*
/*
* 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
*
* 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
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -19,11 +19,15 @@
#include "pico/stdlib.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hardware/flash.h"
#include "hardware/sync.h"
#include "hardware/watchdog.h"
#include "hid_parser.h"
#include "pico/bootrom.h"
#include "pico/multicore.h"
#include "pico/stdlib.h"
@ -31,10 +35,9 @@
#include "pio_usb.h"
#include "tusb.h"
#include "usb_descriptors.h"
#include "hid_parser.h"
#include "user_config.h"
/********* Misc definitions **********/
/********* Misc definitions for better readability **********/
#define PICO_A 0
#define PICO_B 1
@ -50,6 +53,8 @@
#define MAX_REPORT_ITEMS 16
#define MOUSE_BOOT_REPORT_LEN 4
#define NUM_SCREENS 2 // Will be more in the future
/********* 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
@ -85,7 +90,7 @@
* - checksum is simply calculated by XORing all bytes together
*/
enum packet_type_e : uint8_t {
enum packet_type_e {
KEYBOARD_REPORT_MSG = 1,
MOUSE_REPORT_MSG = 2,
OUTPUT_SELECT_MSG = 3,
@ -93,6 +98,8 @@ enum packet_type_e : uint8_t {
MOUSE_ZOOM_MSG = 5,
KBD_SET_REPORT_MSG = 6,
SWITCH_LOCK_MSG = 7,
SYNC_BORDERS_MSG = 8,
FLASH_LED_MSG = 9,
};
/*
@ -124,7 +131,7 @@ typedef struct {
#define RAW_PACKET_LENGTH (START_LENGTH + PACKET_LENGTH)
#define KBD_QUEUE_LENGTH 128
#define MOUSE_QUEUE_LENGTH 2048
#define MOUSE_QUEUE_LENGTH 2048
#define KEYS_IN_USB_REPORT 6
#define KBD_REPORT_LENGTH 8
@ -133,6 +140,37 @@ typedef struct {
/********* Screen **********/
#define MAX_SCREEN_COORD 32767
/********* Configuration storage definitions **********/
typedef struct {
int top; // When jumping from a smaller to a bigger screen, go to THIS top height
int bottom; // When jumping from a smaller to a bigger screen, go to THIS bottom
// height
} border_size_t;
/* Define output parameters */
typedef struct {
int number; // Number of this output (e.g. ACTIVE_OUTPUT_A = 0 etc)
int screen_count; // How many monitors per output (e.g. Output A is Windows with 3 monitors)
int screen_index; // Current active screen
int speed_x; // Mouse speed per output, in direction X
int speed_y; // Mouse speed per output, in direction Y
border_size_t border; // Screen border size/offset to keep cursor at same height when switching
} output_t;
/* Data structure defining how configuration is stored */
typedef struct {
uint32_t magic_header;
uint8_t force_mouse_boot_mode;
output_t output[NUM_SCREENS];
uint32_t checksum;
} config_t;
extern const config_t default_config;
extern config_t ADDR_CONFIG[];
#define ADDR_CONFIG_BASE_ADDR (ADDR_CONFIG)
// -------------------------------------------------------+
typedef void (*action_handler_t)();
@ -142,6 +180,8 @@ typedef struct {
uint8_t keys[6]; // Which keys need to be pressed
uint8_t key_count; // How many keys are pressed
action_handler_t action_handler; // What to execute when the key combination is detected
bool pass_to_os; // True if we are to pass the key to the OS too
bool acknowledge; // True if we are to notify the user about registering keypress
} hotkey_combo_t;
typedef struct TU_ATTR_PACKED {
@ -155,12 +195,12 @@ typedef struct TU_ATTR_PACKED {
typedef enum { IDLE, READING_PACKET, PROCESSING_PACKET } receiver_state_t;
typedef struct {
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 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
uint8_t keyboard_leds[NUM_SCREENS]; // State of keyboard LEDs (index 0 = A, index 1 = B)
uint64_t last_activity[NUM_SCREENS]; // Timestamp of the last input activity (-||-)
receiver_state_t receiver_state; // Storing the state for the simple receiver state machine
uint64_t core1_last_loop_pass; // Timestamp of last core1 loop execution
uint8_t active_output; // Currently selected output (0 = A, 1 = B)
@ -168,35 +208,40 @@ 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
config_t config; // Device configuration, loaded from flash or defaults used
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
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 key_pressed; // We are holding down a key (from the PCs point of view)
/* Connection status flags */
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
/* Feature flags */
bool mouse_zoom; // True when "mouse zoom" is enabled
bool switch_lock; // True when device is prevented from switching
bool onboard_led_state; // True when LED is ON
/* Onboard LED blinky (provide feedback when e.g. mouse connected) */
int32_t blinks_left; // How many blink transitions are left
int32_t last_led_change; // Timestamp of the last time led state transitioned
} device_state_t;
/******* USB Host *********/
void process_mouse_report(uint8_t*, int, device_state_t*);
void check_endpoints(device_state_t* state);
/********* Setup **********/
void initial_setup(void);
void serial_init(void);
void core1_main(void);
/********* Keyboard **********/
bool keypress_check(hotkey_combo_t, const hid_keyboard_report_t*);
bool is_key_pressed(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 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*);
void send_key(hid_keyboard_report_t*, device_state_t*);
/********* Mouse **********/
bool tud_hid_abs_mouse_report(uint8_t report_id,
@ -206,10 +251,15 @@ 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_report(uint8_t*, int, device_state_t*);
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*);
void send_mouse(hid_abs_mouse_report_t*, device_state_t*);
/********* UART **********/
void receive_char(uart_packet_t*, device_state_t*);
@ -217,7 +267,9 @@ void send_packet(const uint8_t*, enum packet_type_e, int);
void send_value(const uint8_t, enum packet_type_e);
/********* LEDs **********/
void update_leds(device_state_t*);
void restore_leds(device_state_t*);
void blink_led(device_state_t*);
void led_blinking_task(device_state_t*);
/********* Checksum **********/
uint8_t calc_checksum(const uint8_t*, int);
@ -226,21 +278,30 @@ bool verify_checksum(const uart_packet_t*);
/********* Watchdog **********/
void kick_watchdog(void);
/********* Configuration **********/
void load_config(void);
void save_config(void);
void wipe_config(void);
/********* Handlers **********/
void output_toggle_hotkey_handler(device_state_t*);
void screen_border_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 wipe_config_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*);
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 handle_sync_borders_msg(uart_packet_t*, device_state_t*);
void handle_flash_led_msg(uart_packet_t*, device_state_t*);
void handle_fw_upgrade_msg(void);
void switch_output(uint8_t);

View File

@ -1,175 +1,221 @@
/*
/*
* 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
*
* 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
* 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
* 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"
int get_mouse_offset(int32_t movement, const int direction) {
int offset = 0;
int offset = 0;
output_t *active_output =
&global_state.config.output[global_state.active_output];
if (direction == DIRECTION_X)
offset = movement * MOUSE_SPEED_FACTOR_X;
else
offset = movement * MOUSE_SPEED_FACTOR_Y;
if (direction == DIRECTION_X)
offset = movement * active_output->speed_x;
else
offset = movement * active_output->speed_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;
/* 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;
return offset;
}
void keep_cursor_on_screen(int16_t* position, const int32_t* movement, const int direction) {
int16_t offset = get_mouse_offset(*movement, direction);
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 */
if (*position + offset < 0)
*position = 0;
/* Lowest we can go is 0 */
if (*position + offset < 0)
*position = 0;
/* Highest we can go is MAX_SCREEN_COORD */
else if (*position + offset > MAX_SCREEN_COORD)
*position = 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 */
else
*position += offset;
/* We're still on screen, all good */
else
*position += offset;
}
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;
}
}
/* If mouse needs to go locally, queue packet to mouse queue, else send them
* through UART */
void send_mouse(hid_abs_mouse_report_t *report, device_state_t *state) {
if (state->active_output == BOARD_ROLE) {
queue_mouse_report(report, state);
state->last_activity[BOARD_ROLE] = time_us_64();
} else {
send_packet((uint8_t *)report, MOUSE_REPORT_MSG, MOUSE_REPORT_LENGTH);
}
}
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++;
}
int16_t scale_y_coord(output_t *from, output_t *to, device_state_t *state) {
int size_to = to->border.bottom - to->border.top;
int size_from = from->border.bottom - from->border.top;
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);
/* If sizes match, there is nothing to do */
if (size_from == size_to)
return state->mouse_y;
/* Moving from bigger ==> smaller screen */
if (size_from < size_to) {
if (state->mouse_y < from->border.top)
return 0;
if (state->mouse_y > from->border.bottom)
return MAX_SCREEN_COORD;
/* y_b = ((y_a - top) * HEIGHT) / (bottom - top) */
return ((state->mouse_y - from->border.top) * MAX_SCREEN_COORD) /
size_from;
}
/* Moving from smaller ==> bigger screen,
y_a = top + (((bottom - top) * y_b) / HEIGHT) */
return to->border.top + ((size_to * state->mouse_y) / MAX_SCREEN_COORD);
}
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;
values->move_x = mouse_report->x;
values->move_y = mouse_report->y;
values->wheel = mouse_report->wheel;
values->buttons = mouse_report->buttons;
}
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};
void process_mouse_report(uint8_t* raw_report, int len, device_state_t* state) {
mouse_values_t values = {0};
/* No switching allowed if explicitly disabled */
if (state->switch_lock)
return;
/* 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);
/* 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);
/* 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);
/* 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);
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 (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 */
send_mouse(&report, state);
if (jump_from_A_to_B) {
switch_output(ACTIVE_OUTPUT_B);
state->mouse_x = MAX_SCREEN_COORD;
state->mouse_y = scale_y_coord(&state->config.output[ACTIVE_OUTPUT_A],
&state->config.output[ACTIVE_OUTPUT_B],
state);
if (state->active_output == ACTIVE_OUTPUT_A) {
send_packet((const uint8_t*)&abs_mouse_report, MOUSE_REPORT_MSG, MOUSE_REPORT_LENGTH);
} else {
queue_mouse_report(&abs_mouse_report, state);
state->last_activity[ACTIVE_OUTPUT_B] = time_us_64();
}
switch_output(ACTIVE_OUTPUT_A);
state->mouse_x = 0;
/* We use the mouse to switch outputs, the logic is in check_mouse_switch() */
check_mouse_switch(&values, state);
state->mouse_y = scale_y_coord(&state->config.output[ACTIVE_OUTPUT_B],
&state->config.output[ACTIVE_OUTPUT_A],
state);
}
}
}
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++;
}
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);
}
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;
values->move_x = mouse_report->x;
values->move_y = mouse_report->y;
values->wheel = mouse_report->wheel;
values->buttons = mouse_report->buttons;
}
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};
/* Move the mouse, depending where the output is supposed to go */
send_mouse(&abs_mouse_report, state);
/* 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};
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;
/* 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;
/* Peek first, if there is anything there... */
if (!queue_try_peek(&state->mouse_queue, &report))
return;
/* If we are suspended, let's wake the host up */
if(tud_suspended())
tud_remote_wakeup();
/* If we are suspended, let's wake the host up */
if (tud_suspended())
tud_remote_wakeup();
/* ... 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);
/* ... 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);
/* ... 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;
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);
queue_try_add(&state->mouse_queue, report);
}

View File

@ -54,7 +54,7 @@ void serial_init() {
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 = PIO_USB_DP_PIN;
config.pin_dp = PIO_USB_DP_PIN_DEFAULT;
tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &config);
/* Initialize and configure TinyUSB Host */
@ -69,6 +69,9 @@ void initial_setup(void) {
/* PIO USB requires a clock multiple of 12 MHz, setting to 120 MHz */
set_sys_clock_khz(120000, true);
/* Search the persistent storage sector in flash for valid config or use defaults */
load_config();
/* Init and enable the on-board LED GPIO as output */
gpio_init(GPIO_LED_PIN);
gpio_set_dir(GPIO_LED_PIN, GPIO_OUT);
@ -92,9 +95,9 @@ void initial_setup(void) {
/* 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);
}
/* ========== End of Initial Board Setup ========== */
/* ========== End of Initial Board Setup ========== */

View File

@ -1,17 +1,17 @@
/*
/*
* 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
*
* 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
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -44,7 +44,6 @@ void send_value(const uint8_t value, enum packet_type_e packet_type) {
* =============== Parsing Packets ================ *
* ================================================== */
void process_packet(uart_packet_t* packet, device_state_t* state) {
if (!verify_checksum(packet)) {
return;
@ -78,6 +77,14 @@ void process_packet(uart_packet_t* packet, device_state_t* state) {
case SWITCH_LOCK_MSG:
handle_switch_lock_msg(packet, state);
break;
case SYNC_BORDERS_MSG:
handle_sync_borders_msg(packet, state);
break;
case FLASH_LED_MSG:
handle_flash_led_msg(packet, state);
break;
}
}
@ -92,10 +99,11 @@ 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;
}

View File

@ -63,7 +63,7 @@ void tud_hid_set_report_cb(uint8_t instance,
/* 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);
restore_leds(&global_state);
else
send_value(leds, KBD_SET_REPORT_MSG);
}
@ -110,8 +110,7 @@ void tuh_hid_mount_cb(uint8_t dev_addr,
/* 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;
global_state.keyboard_connected = true;
break;
case HID_ITF_PROTOCOL_MOUSE:
@ -120,12 +119,17 @@ void tuh_hid_mount_cb(uint8_t dev_addr,
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;
global_state.mouse_connected = true;
break;
}
/* Flash local led to indicate a device was connected */
blink_led(&global_state);
/* Also signal the other board to flash LED, to enable easy verification if serial works */
send_value(ENABLE, FLASH_LED_MSG);
/* Kick off the report querying */
tuh_hid_receive_report(dev_addr, instance);

View File

@ -33,18 +33,24 @@
*
* This affects how fast the mouse moves.
*
* 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_SPEED_A_FACTOR_X: [1-128], mouse moves at this speed in X direction
* MOUSE_SPEED_A_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.
* This is now configurable per-screen.
*
* */
#define MOUSE_SPEED_FACTOR_X 16
#define MOUSE_SPEED_FACTOR_Y 16
/* Output A values */
#define MOUSE_SPEED_A_FACTOR_X 16
#define MOUSE_SPEED_A_FACTOR_Y 16
/* Output B values */
#define MOUSE_SPEED_B_FACTOR_X 16
#define MOUSE_SPEED_B_FACTOR_Y 16
#define MOUSE_JUMP_THRESHOLD 0

View File

@ -1,17 +1,17 @@
/*
/*
* 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
*
* 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
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -51,3 +51,35 @@ void kick_watchdog(void) {
if (current_time - core1_last_loop_pass < CORE1_HANG_TIMEOUT_US)
watchdog_update();
}
/* ================================================== *
* Flash and config functions
* ================================================== */
void wipe_config(void) {
uint32_t ints = save_and_disable_interrupts();
flash_range_erase(PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE);
restore_interrupts(ints);
}
void load_config(void) {
config_t* config = ADDR_CONFIG_BASE_ADDR;
/* If no config is detected, copy default values to our struct. TODO checksum */
if (config->magic_header != 0x0B00B1E5) {
config = (config_t*)&default_config;
}
memcpy(&global_state.config, config, sizeof(config_t));
}
void save_config(void) {
uint8_t buf[FLASH_PAGE_SIZE];
memcpy(buf, &global_state.config, sizeof(config_t));
wipe_config();
uint32_t ints = save_and_disable_interrupts();
flash_range_program(PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE, buf, FLASH_PAGE_SIZE);
restore_interrupts(ints);
}