├── spaceball.jpg ├── spaceball.uf2 ├── spaceball_pico2.uf2 ├── .gitmodules ├── src ├── .clang-format ├── tusb_config.h ├── spaceball.c └── descriptors.c ├── CMakeLists.txt ├── README.md └── pico_sdk_import.cmake /spaceball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/spaceball-2003/HEAD/spaceball.jpg -------------------------------------------------------------------------------- /spaceball.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/spaceball-2003/HEAD/spaceball.uf2 -------------------------------------------------------------------------------- /spaceball_pico2.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/spaceball-2003/HEAD/spaceball_pico2.uf2 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pico-sdk"] 2 | path = pico-sdk 3 | url = https://github.com/raspberrypi/pico-sdk 4 | [submodule "tinyusb"] 5 | path = tinyusb 6 | url = https://github.com/hathach/tinyusb 7 | -------------------------------------------------------------------------------- /src/.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Chromium 4 | ColumnLimit: 0 5 | Cpp11BracedListStyle: false 6 | IndentWidth: 4 7 | SpaceAfterCStyleCast: true 8 | AlignAfterOpenBracket: DontAlign 9 | ... 10 | -------------------------------------------------------------------------------- /src/tusb_config.h: -------------------------------------------------------------------------------- 1 | #ifndef _TUSB_CONFIG_H_ 2 | #define _TUSB_CONFIG_H_ 3 | 4 | #define BOARD_DEVICE_RHPORT_NUM 0 5 | #define BOARD_DEVICE_RHPORT_SPEED OPT_MODE_FULL_SPEED 6 | #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) 7 | 8 | #define CFG_TUSB_MEM_SECTION 9 | #define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4))) 10 | 11 | #define CFG_TUD_ENDPOINT0_SIZE 64 12 | 13 | #define CFG_TUD_HID 1 14 | #define CFG_TUD_CDC 0 15 | #define CFG_TUD_MSC 0 16 | #define CFG_TUD_MIDI 0 17 | #define CFG_TUD_VENDOR 0 18 | 19 | #define CFG_TUD_HID_EP_BUFSIZE 64 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | add_compile_definitions(PICO_DEFAULT_UART_BAUD_RATE=921600) 4 | 5 | set(PICO_SDK_PATH "${CMAKE_CURRENT_LIST_DIR}/pico-sdk") 6 | set(PICO_TINYUSB_PATH "${CMAKE_CURRENT_LIST_DIR}/tinyusb") 7 | 8 | include(pico_sdk_import.cmake) 9 | 10 | project(spaceball) 11 | 12 | pico_sdk_init() 13 | 14 | add_compile_options(-Wall) 15 | 16 | add_executable(spaceball 17 | src/spaceball.c 18 | src/descriptors.c 19 | ) 20 | 21 | target_include_directories(spaceball PRIVATE src) 22 | 23 | target_link_libraries(spaceball 24 | pico_stdlib 25 | tinyusb_device 26 | tinyusb_board 27 | ) 28 | 29 | pico_add_extra_outputs(spaceball) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spaceball 2003 with modern software 2 | 3 | ![Spaceball 2003 connected to a Raspberry Pi Pico](spaceball.jpg) 4 | 5 | This project lets you use an old Spaceball 2003 controller with modern software on modern operating systems. It simulates a 3DConnexion SpaceMouse Pro, so it can be used with software like Fusion 360, 3ds Max, SolidWorks, Inventor, Maya and many others. No special software is required on the computer, apart from 3DxWare. From the computer's point of view, your old Spaceball will look like a real SpaceMouse Pro connected over USB. 6 | 7 | You can either use the [RP2040-RS232](https://github.com/jfedor2/rp2040-rs232) adapter or you can make one yourself as described below. If you use the RP2040-RS232 adapter, the multi-firmware that comes with it includes the Spaceball 2003 firmware. You can also just use the one linked here. 8 | 9 | To make one yourself you will need: 10 | 11 | - Raspberry Pi Pico 12 | - [Pololu 23201a Serial Adapter](https://www.pololu.com/product/126) 13 | - null modem adapter and DB9 gender changer (either as two separate adapters or one that does both) 14 | - breadboard and some jumper wires 15 | 16 | Make the following connections between the Pico and the serial adapter: 17 | 18 | | Pico | serial adapter | 19 | | -----: | ------ | 20 | | 3V3 (pin 36) | VCC | 21 | | GND (pin 23) | GND | 22 | | GPIO20 (pin 26) | RX | 23 | | GPIO21 (pin 27) | TX | 24 | 25 | If you don't have the original power supply for the Spaceball, you can power it from the Pico's 5V pin. 26 | 27 | Flash the Pico with the [spaceball.uf2](spaceball.uf2) firmware the usual way: hold the BOOTSEL button while connecting the board to the computer, then copy the UF2 file to the USB drive that shows up. 28 | 29 | Install 3DxWare on your computer and enjoy. 30 | 31 | Buttons on the Spaceball (`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, pick button) are mapped to the following buttons on the emulated SpaceMouse Pro: `1`, `2`, `3`, `4`, `Esc`, `Ctrl`, `Alt`, `Shift`, `Menu`. You can assign functions to them in 3Dconnexion's software. 32 | 33 | ## How to compile the firmware 34 | 35 | ``` 36 | git clone https://github.com/jfedor2/spaceball-2003.git 37 | cd spaceball-2003 38 | git submodule update --init 39 | mkdir build 40 | cd build 41 | cmake .. 42 | make 43 | ``` 44 | -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | FetchContent_Declare( 33 | pico_sdk 34 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 35 | GIT_TAG master 36 | ) 37 | if (NOT pico_sdk) 38 | message("Downloading Raspberry Pi Pico SDK") 39 | FetchContent_Populate(pico_sdk) 40 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 41 | endif () 42 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 43 | else () 44 | message(FATAL_ERROR 45 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 46 | ) 47 | endif () 48 | endif () 49 | 50 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 51 | if (NOT EXISTS ${PICO_SDK_PATH}) 52 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 53 | endif () 54 | 55 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 56 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 57 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 58 | endif () 59 | 60 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 61 | 62 | include(${PICO_SDK_INIT_CMAKE_FILE}) 63 | -------------------------------------------------------------------------------- /src/spaceball.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define SERIAL_MOUSE_RX_PIN 21 11 | #define SERIAL_MOUSE_TX_PIN 20 12 | #define SERIAL_MOUSE_UART uart1 13 | 14 | uint16_t trans_report[3]; 15 | uint16_t rot_report[3]; 16 | uint8_t buttons_report[6]; 17 | 18 | uint8_t trans_pending = 0; 19 | uint8_t rot_pending = 0; 20 | uint8_t buttons_pending = 0; 21 | 22 | uint8_t button_bits[] = { 12, 13, 14, 15, 22, 25, 23, 24, 0, 1, 2, 4, 5, 8, 26 }; 23 | 24 | int main() { 25 | board_init(); 26 | tusb_init(); 27 | stdio_init_all(); 28 | 29 | printf("hello\n"); 30 | 31 | gpio_set_function(SERIAL_MOUSE_RX_PIN, GPIO_FUNC_UART); 32 | gpio_set_function(SERIAL_MOUSE_TX_PIN, GPIO_FUNC_UART); 33 | uart_init(SERIAL_MOUSE_UART, 9600); 34 | uart_set_translate_crlf(SERIAL_MOUSE_UART, false); 35 | uart_set_format(SERIAL_MOUSE_UART, 8, 1, UART_PARITY_NONE); 36 | 37 | sleep_ms(500); 38 | // make it send data without polling, set rate to 20ms 39 | uint8_t init_buf[] = { '\r', 'M', 'S', 'S', '\r', 'P', '@', 'T', '@', 'T', '\r' }; 40 | uart_write_blocking(SERIAL_MOUSE_UART, init_buf, sizeof(init_buf)); 41 | 42 | uint8_t buf[64]; 43 | uint8_t idx = 0; 44 | bool escaped = false; 45 | 46 | while (true) { 47 | tud_task(); 48 | if (tud_hid_ready()) { 49 | if (trans_pending) { 50 | tud_hid_report(1, trans_report, 6); 51 | trans_pending = 0; 52 | } else if (rot_pending) { 53 | tud_hid_report(2, rot_report, 6); 54 | rot_pending = 0; 55 | } else if (buttons_pending) { 56 | tud_hid_report(3, buttons_report, 6); 57 | buttons_pending = 0; 58 | } 59 | } 60 | 61 | if (uart_is_readable(SERIAL_MOUSE_UART)) { 62 | char c = uart_getc(SERIAL_MOUSE_UART); 63 | if (escaped) { 64 | switch (c) { 65 | case 'M': 66 | buf[idx++] = '\r'; 67 | break; 68 | case '^': 69 | buf[idx++] = '^'; 70 | break; 71 | } 72 | escaped = false; 73 | } else { 74 | if (c == '^') { 75 | escaped = true; 76 | } else { 77 | buf[idx++] = c; 78 | } 79 | } 80 | idx %= sizeof(buf); 81 | 82 | printf("%02x ", c); 83 | 84 | if (c == '\r') { 85 | printf("\n"); 86 | 87 | if ((idx >= 2) && (buf[0] == '\n') && (buf[1] == '@')) { 88 | for (int i = 1; i < idx - 1; i++) { 89 | printf("%c", buf[i]); 90 | } 91 | printf("\n"); 92 | } 93 | 94 | if ((buf[0] == 'D') && (idx == 16)) { 95 | int16_t values[6]; 96 | for (int i = 0; i < 6; i++) { 97 | values[i] = buf[3 + 2 * i] << 8 | buf[3 + 2 * i + 1]; 98 | } 99 | 100 | trans_report[0] = values[0]; 101 | trans_report[1] = -values[2]; 102 | trans_report[2] = -values[1]; 103 | rot_report[0] = values[3]; 104 | rot_report[1] = -values[5]; 105 | rot_report[2] = -values[4]; 106 | 107 | trans_pending = 1; 108 | rot_pending = 1; 109 | } 110 | 111 | if ((buf[0] == 'K') && (idx == 4)) { 112 | uint16_t buttons = (buf[2] & 0x0F) | ((buf[1] & 0x1F) << 4); 113 | memset(buttons_report, 0, sizeof(buttons_report)); 114 | 115 | for (int i = 0; i < 12; i++) { 116 | if (buttons & (1 << i)) { 117 | buttons_report[button_bits[i] / 8] |= 1 << (button_bits[i] % 8); 118 | } 119 | } 120 | 121 | buttons_pending = 1; 122 | } 123 | idx = 0; 124 | } 125 | } 126 | } 127 | 128 | return 0; 129 | } 130 | 131 | void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) { 132 | } 133 | 134 | uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) { 135 | return 0; 136 | } 137 | -------------------------------------------------------------------------------- /src/descriptors.c: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2023 Jacek Fedorynski 5 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | */ 26 | 27 | #include 28 | 29 | // 3Dconnexion SpaceMouse Pro 30 | #define USB_VID 0x046D 31 | #define USB_PID 0xC62B 32 | 33 | // SpaceMouse Pro 34 | const uint8_t report_descriptor[] = { 35 | 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 36 | 0x09, 0x08, // Usage (Multi-axis Controller) 37 | 0xA1, 0x01, // Collection (Application) 38 | 0xA1, 0x00, // Collection (Physical) 39 | 0x85, 0x01, // Report ID (1) 40 | 0x16, 0xA2, 0xFE, // Logical Minimum (-350) 41 | 0x26, 0x5E, 0x01, // Logical Maximum (350) 42 | 0x36, 0x88, 0xFA, // Physical Minimum (-1400) 43 | 0x46, 0x78, 0x05, // Physical Maximum (1400) 44 | 0x55, 0x0C, // Unit Exponent (-4) 45 | 0x65, 0x11, // Unit (System: SI Linear, Length: Centimeter) 46 | 0x09, 0x30, // Usage (X) 47 | 0x09, 0x31, // Usage (Y) 48 | 0x09, 0x32, // Usage (Z) 49 | 0x75, 0x10, // Report Size (16) 50 | 0x95, 0x03, // Report Count (3) 51 | 0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) 52 | 0xC0, // End Collection 53 | 0xA1, 0x00, // Collection (Physical) 54 | 0x85, 0x02, // Report ID (2) 55 | 0x09, 0x33, // Usage (Rx) 56 | 0x09, 0x34, // Usage (Ry) 57 | 0x09, 0x35, // Usage (Rz) 58 | 0x75, 0x10, // Report Size (16) 59 | 0x95, 0x03, // Report Count (3) 60 | 0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) 61 | 0xC0, // End Collection 62 | 0xA1, 0x02, // Collection (Logical) 63 | 0x85, 0x03, // Report ID (3) 64 | 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 65 | 0x05, 0x09, // Usage Page (Button) 66 | 0x19, 0x01, // Usage Minimum (0x01) 67 | 0x29, 0x03, // Usage Maximum (0x03) 68 | 0x15, 0x00, // Logical Minimum (0) 69 | 0x25, 0x01, // Logical Maximum (1) 70 | 0x35, 0x00, // Physical Minimum (0) 71 | 0x45, 0x01, // Physical Maximum (1) 72 | 0x75, 0x01, // Report Size (1) 73 | 0x95, 0x03, // Report Count (3) 74 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 75 | 0x95, 0x01, // Report Count (1) 76 | 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 77 | 0x19, 0x05, // Usage Minimum (0x05) 78 | 0x29, 0x06, // Usage Maximum (0x06) 79 | 0x95, 0x02, // Report Count (2) 80 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 81 | 0x95, 0x02, // Report Count (2) 82 | 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 83 | 0x09, 0x09, // Usage (0x09) 84 | 0x95, 0x01, // Report Count (1) 85 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 86 | 0x95, 0x03, // Report Count (3) 87 | 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 88 | 0x19, 0x0D, // Usage Minimum (0x0D) 89 | 0x29, 0x10, // Usage Maximum (0x10) 90 | 0x95, 0x04, // Report Count (4) 91 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 92 | 0x95, 0x06, // Report Count (6) 93 | 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 94 | 0x19, 0x17, // Usage Minimum (0x17) 95 | 0x29, 0x1B, // Usage Maximum (0x1B) 96 | 0x95, 0x05, // Report Count (5) 97 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 98 | 0x95, 0x15, // Report Count (21) 99 | 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 100 | 0xC0, // End Collection 101 | 0xA1, 0x02, // Collection (Logical) 102 | 0x85, 0x04, // Report ID (4) 103 | 0x05, 0x08, // Usage Page (LEDs) 104 | 0x09, 0x4B, // Usage (Generic Indicator) 105 | 0x15, 0x00, // Logical Minimum (0) 106 | 0x25, 0x01, // Logical Maximum (1) 107 | 0x95, 0x01, // Report Count (1) 108 | 0x75, 0x01, // Report Size (1) 109 | 0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 110 | 0x95, 0x01, // Report Count (1) 111 | 0x75, 0x07, // Report Size (7) 112 | 0x91, 0x03, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 113 | 0xC0, // End Collection 114 | 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) 115 | 0x09, 0x01, // Usage (0x01) 116 | 0xA1, 0x02, // Collection (Logical) 117 | 0x15, 0x80, // Logical Minimum (-128) 118 | 0x25, 0x7F, // Logical Maximum (127) 119 | 0x75, 0x08, // Report Size (8) 120 | 0x09, 0x3A, // Usage (0x3A) 121 | 0xA1, 0x02, // Collection (Logical) 122 | 0x85, 0x05, // Report ID (5) 123 | 0x09, 0x20, // Usage (0x20) 124 | 0x95, 0x01, // Report Count (1) 125 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 126 | 0xC0, // End Collection 127 | 0xA1, 0x02, // Collection (Logical) 128 | 0x85, 0x06, // Report ID (6) 129 | 0x09, 0x21, // Usage (0x21) 130 | 0x95, 0x01, // Report Count (1) 131 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 132 | 0xC0, // End Collection 133 | 0xA1, 0x02, // Collection (Logical) 134 | 0x85, 0x07, // Report ID (7) 135 | 0x09, 0x22, // Usage (0x22) 136 | 0x95, 0x01, // Report Count (1) 137 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 138 | 0xC0, // End Collection 139 | 0xA1, 0x02, // Collection (Logical) 140 | 0x85, 0x08, // Report ID (8) 141 | 0x09, 0x23, // Usage (0x23) 142 | 0x95, 0x07, // Report Count (7) 143 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 144 | 0xC0, // End Collection 145 | 0xA1, 0x02, // Collection (Logical) 146 | 0x85, 0x09, // Report ID (9) 147 | 0x09, 0x24, // Usage (0x24) 148 | 0x95, 0x07, // Report Count (7) 149 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 150 | 0xC0, // End Collection 151 | 0xA1, 0x02, // Collection (Logical) 152 | 0x85, 0x0A, // Report ID (10) 153 | 0x09, 0x25, // Usage (0x25) 154 | 0x95, 0x07, // Report Count (7) 155 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 156 | 0xC0, // End Collection 157 | 0xA1, 0x02, // Collection (Logical) 158 | 0x85, 0x0B, // Report ID (11) 159 | 0x09, 0x26, // Usage (0x26) 160 | 0x95, 0x01, // Report Count (1) 161 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 162 | 0xC0, // End Collection 163 | 0xA1, 0x02, // Collection (Logical) 164 | 0x85, 0x13, // Report ID (19) 165 | 0x09, 0x2E, // Usage (0x2E) 166 | 0x95, 0x01, // Report Count (1) 167 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 168 | 0xC0, // End Collection 169 | 0xA1, 0x02, // Collection (Logical) 170 | 0x85, 0x14, // Report ID (20) 171 | 0x09, 0x2F, // Usage (0x2F) 172 | 0x95, 0x04, // Report Count (4) 173 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 174 | 0xC0, // End Collection 175 | 0xA1, 0x02, // Collection (Logical) 176 | 0x85, 0x15, // Report ID (21) 177 | 0x09, 0x30, // Usage (0x30) 178 | 0x95, 0x01, // Report Count (1) 179 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 180 | 0xC0, // End Collection 181 | 0xA1, 0x02, // Collection (Logical) 182 | 0x85, 0x16, // Report ID (22) 183 | 0x19, 0x01, // Usage Minimum (0x01) 184 | 0x29, 0x1F, // Usage Maximum (0x1F) 185 | 0x15, 0x00, // Logical Minimum (0) 186 | 0x25, 0x01, // Logical Maximum (1) 187 | 0x35, 0x00, // Physical Minimum (0) 188 | 0x45, 0x01, // Physical Maximum (1) 189 | 0x75, 0x01, // Report Size (1) 190 | 0x95, 0x1F, // Report Count (31) 191 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 192 | 0x95, 0x01, // Report Count (1) 193 | 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 194 | 0xC0, // End Collection 195 | 0xC0, // End Collection 196 | 0xC0, // End Collection 197 | }; 198 | 199 | tusb_desc_device_t const desc_device = { 200 | .bLength = sizeof(tusb_desc_device_t), 201 | .bDescriptorType = TUSB_DESC_DEVICE, 202 | .bcdUSB = 0x0200, 203 | .bDeviceClass = 0x00, 204 | .bDeviceSubClass = 0x00, 205 | .bDeviceProtocol = 0x00, 206 | .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, 207 | 208 | .idVendor = USB_VID, 209 | .idProduct = USB_PID, 210 | .bcdDevice = 0x0100, 211 | 212 | .iManufacturer = 0x01, 213 | .iProduct = 0x02, 214 | .iSerialNumber = 0x00, 215 | 216 | .bNumConfigurations = 0x01, 217 | }; 218 | 219 | #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) 220 | #define EPNUM_HID 0x81 221 | 222 | uint8_t const desc_configuration[] = { 223 | // Config number, interface count, string index, total length, attribute, power in mA 224 | TUD_CONFIG_DESCRIPTOR(1, 1, 0, CONFIG_TOTAL_LEN, 0, 500), 225 | 226 | // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval 227 | TUD_HID_DESCRIPTOR(0, 0, HID_ITF_PROTOCOL_NONE, sizeof(report_descriptor), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 1) 228 | }; 229 | 230 | char const* string_desc_arr[] = { 231 | (const char[]){ 0x09, 0x04 }, // 0: is supported language is English (0x0409) 232 | "3Dconnexion", // 1: Manufacturer 233 | "SpaceMouse Pro", // 2: Product 234 | }; 235 | 236 | // Invoked when received GET DEVICE DESCRIPTOR 237 | // Application return pointer to descriptor 238 | uint8_t const* tud_descriptor_device_cb() { 239 | return (uint8_t const*) &desc_device; 240 | } 241 | 242 | // Invoked when received GET CONFIGURATION DESCRIPTOR 243 | // Application return pointer to descriptor 244 | // Descriptor contents must exist long enough for transfer to complete 245 | uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { 246 | return desc_configuration; 247 | } 248 | 249 | // Invoked when received GET HID REPORT DESCRIPTOR 250 | // Application return pointer to descriptor 251 | // Descriptor contents must exist long enough for transfer to complete 252 | uint8_t const* tud_hid_descriptor_report_cb(uint8_t itf) { 253 | return report_descriptor; 254 | } 255 | 256 | static uint16_t _desc_str[32]; 257 | 258 | // Invoked when received GET STRING DESCRIPTOR request 259 | // Application return pointer to descriptor, whose contents must exist long enough for transfer to complete 260 | uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { 261 | uint8_t chr_count; 262 | 263 | if (index == 0) { 264 | memcpy(&_desc_str[1], string_desc_arr[0], 2); 265 | chr_count = 1; 266 | } else { 267 | // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. 268 | // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors 269 | 270 | if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) 271 | return NULL; 272 | 273 | const char* str = string_desc_arr[index]; 274 | 275 | // Cap at max char 276 | chr_count = strlen(str); 277 | if (chr_count > 31) 278 | chr_count = 31; 279 | 280 | // Convert ASCII string into UTF-16 281 | for (uint8_t i = 0; i < chr_count; i++) { 282 | _desc_str[1 + i] = str[i]; 283 | } 284 | } 285 | 286 | // first byte is length (including header), second byte is string type 287 | _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); 288 | 289 | return _desc_str; 290 | } 291 | --------------------------------------------------------------------------------