commit 9a929530d9619aaba53a3f72c94ec1ff551ffcff Author: awalol Date: Thu Mar 5 16:21:25 2026 +0800 First Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2435c20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +!.vscode/* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2d98e37 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,92 @@ +# Generated Cmake Pico project file + +cmake_minimum_required(VERSION 3.13) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Initialise pico_sdk from installed location +# (note this can come from environment, CMake cache etc) + +# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work == +if (WIN32) + set(USERHOME $ENV{USERPROFILE}) +else () + set(USERHOME $ENV{HOME}) +endif () +set(sdkVersion 2.2.0) +set(toolchainVersion 14_2_Rel1) +set(picotoolVersion 2.2.0-a4) +set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake) +if (EXISTS ${picoVscode}) + include(${picoVscode}) +endif () +# ==================================================================================== +set(PICO_BOARD pico2_w CACHE STRING "Board type") + +# Pull in Raspberry Pi Pico SDK (must be before project) +include(pico_sdk_import.cmake) + +project(ds5-bridge C CXX ASM) + +# Initialise the Raspberry Pi Pico SDK +pico_sdk_init() + +# Add executable. Default name is the project name, version 0.1 + +add_executable(ds5-bridge + src/usb_descriptors.c + src/main.cpp + src/bt.cpp + src/usb.cpp + src/audio.cpp +) + +add_library(wdl_resampler STATIC + lib/WDL/resample.cpp +) + +target_include_directories(wdl_resampler PUBLIC + lib/WDL +) + +target_compile_options(wdl_resampler PRIVATE -fsigned-char) +target_compile_options(ds5-bridge PRIVATE -fsigned-char) + +pico_set_program_name(ds5-bridge "ds5-bridge") +pico_set_program_version(ds5-bridge "0.1") + +# Modify the below lines to enable/disable output over UART/USB +pico_enable_stdio_uart(ds5-bridge 0) +pico_enable_stdio_usb(ds5-bridge 1) + +# Add the standard library to the build +target_link_libraries(ds5-bridge + pico_stdlib + pico_multicore +) + +# Add the standard include files to the build +#target_include_directories(ds5-bridge PRIVATE +# ${CMAKE_CURRENT_LIST_DIR} +#) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# Add any user requested libraries +target_link_libraries(ds5-bridge + hardware_interp + hardware_timer + pico_btstack_classic + pico_cyw43_arch_none + pico_btstack_cyw43 + tinyusb_device + tinyusb_board + wdl_resampler +) + +pico_add_extra_outputs(ds5-bridge) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..951ef4e --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Pico2W DualSense 5 Bridge +> 将 Pico2W 变成 DS5 手柄的无线适配器 + +# 功能特点 + - 支持HD震动 + +# 使用方法 +1. 按住 Pico 上的BOOTSEL进入刷机 +2. 将 .uf2 文件拖入进去 +3. 将 DS5 手柄进入蓝牙配对模式 +4. Enjoy it + +# 当前问题: +1. 使用PS键进行重连时,会配对失败 +2. 陀螺仪功能在游戏异常(但是在线检测工具正常) + +# 未来计划 +1. 耳机与扬声器支持 + +# 致谢 + - [rafaelvaloto/Pico_W-Dualsense](https://github.com/rafaelvaloto/Pico_W-Dualsense) - 灵感来源 + - [egormanga/SAxense](https://github.com/egormanga/SAxense) - 震动报文 \ No newline at end of file diff --git a/pico_sdk_import.cmake b/pico_sdk_import.cmake new file mode 100644 index 0000000..d493cc2 --- /dev/null +++ b/pico_sdk_import.cmake @@ -0,0 +1,121 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG)) + set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG}) + message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')") +endif () + +if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG) + set(PICO_SDK_FETCH_FROM_GIT_TAG "master") + message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG") +endif() + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +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") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") +set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + else () + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + endif () + + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/src/audio.cpp b/src/audio.cpp new file mode 100644 index 0000000..499f536 --- /dev/null +++ b/src/audio.cpp @@ -0,0 +1,96 @@ +// +// Created by awalol on 2026/3/5. +// + +#include "audio.h" +#include "bt.h" +#include "resample.h" +#include "tusb.h" +#include "pico/time.h" +#include + +#define INPUT_CHANNELS 4 +#define OUTPUT_CHANNELS 2 +#define SAMPLE_SIZE 64 +#define REPORT_SIZE 142 +#define REPORT_ID 0x32 + +static WDL_Resampler resampler; +static uint8_t reportSeqCounter = 0; +static uint8_t packetCounter = 0; + +void process_audio() { + // 1. 读取 USB 音频数据 + if (!tud_audio_available()){ + return; + } + + int16_t raw[1024]; + uint32_t bytes_read = tud_audio_read(raw, sizeof(raw)); + int frames = bytes_read / (INPUT_CHANNELS * sizeof(int16_t)); + if (frames == 0){ + return; + } + + // 2. 从4ch中提取ch3/ch4,转换为float输入重采样器 + WDL_ResampleSample *in_buf; + int nsamples = resampler.ResamplePrepare(frames, OUTPUT_CHANNELS, &in_buf); + + for (int i = 0; i < nsamples; i++) { + in_buf[i * 2] = (WDL_ResampleSample) raw[i * INPUT_CHANNELS + 2] / 32768.0f; + in_buf[i * 2 + 1] = (WDL_ResampleSample) raw[i * INPUT_CHANNELS + 3] / 32768.0f; + } + + // 3. 48kHz -> 3kHz 重采样 + WDL_ResampleSample out_buf[SAMPLE_SIZE]; // 64 floats = 32帧 × 2ch + int out_frames = resampler.ResampleOut(out_buf, nsamples, SAMPLE_SIZE / OUTPUT_CHANNELS, OUTPUT_CHANNELS); + + static int8_t haptic_buf[SAMPLE_SIZE]; + static int haptic_buf_pos = 0; + + // 4. 转换为int8并缓冲,满64字节即组包发送 + for (int i = 0; i < out_frames; i++) { + int val_l = (int) (out_buf[i * 2] * 254.0f); + int val_r = (int) (out_buf[i * 2 + 1] * 254.0f); + haptic_buf[haptic_buf_pos++] = (int8_t) std::clamp(val_l, -128, 127); + haptic_buf[haptic_buf_pos++] = (int8_t) std::clamp(val_r, -128, 127); + + if (haptic_buf_pos != SAMPLE_SIZE) { + continue; + } + uint8_t pkt[REPORT_SIZE] = {}; + pkt[0] = REPORT_ID; + pkt[1] = reportSeqCounter << 4; + reportSeqCounter = (reportSeqCounter + 1) & 0x0F; + pkt[2] = 0x11 | (1 << 7); + pkt[3] = 7; + pkt[4] = 0b11111110; + pkt[5] = 0; + pkt[6] = 0; + pkt[7] = 0; + pkt[8] = 0; + pkt[9] = 0xFF; + pkt[10] = packetCounter++; + pkt[11] = 0x12 | (1 << 7); + pkt[12] = SAMPLE_SIZE; + memcpy(pkt + 13, haptic_buf, SAMPLE_SIZE); + + bt_write(pkt, sizeof(pkt)); + haptic_buf_pos = 0; + } +} + +void core1_entry() { + resampler.SetMode(true, 0, false); + resampler.SetRates(48000, 3000); + resampler.SetFeedMode(true); + resampler.Prealloc(2, 480, 32); + + while (1) { + if (!tud_ready()) { + sleep_ms(10); + continue; + } + process_audio(); + } +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..e088d9d --- /dev/null +++ b/src/audio.h @@ -0,0 +1,10 @@ +// +// Created by awalol on 2026/3/5. +// + +#ifndef DS5_BRIDGE_AUDIO_H +#define DS5_BRIDGE_AUDIO_H + +void core1_entry(); + +#endif //DS5_BRIDGE_AUDIO_H \ No newline at end of file diff --git a/src/bt.cpp b/src/bt.cpp new file mode 100644 index 0000000..ca60f5c --- /dev/null +++ b/src/bt.cpp @@ -0,0 +1,387 @@ +// +// Created by awalol on 2026/3/4. +// + +#include + +#include "bt.h" + +#include +#include + +#include "btstack_event.h" +#include "l2cap.h" +#include "pico/cyw43_arch.h" +#include "pico/stdio.h" +#include "utils.h" + +static btstack_packet_callback_registration_t hci_event_callback_registration,l2cap_event_callback_registration; +static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); +static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); +static bd_addr_t current_device_addr; +static bool device_found = false; +static uint16_t hid_control_cid; +static uint16_t hid_interrupt_cid; +static bt_data_callback_t bt_data_callback = NULL; +std::unordered_map> feature_data; + +void bt_register_data_callback(bt_data_callback_t callback) { + bt_data_callback = callback; +} + +void bt_send_packet(uint8_t *data, uint16_t len) { + if (hid_interrupt_cid != 0) { + l2cap_send(hid_interrupt_cid, data, len); + } +} + +void bt_send_control(uint8_t *data, uint16_t len) { + if (hid_control_cid != 0) { + l2cap_send(hid_control_cid, data, len); + } +} + +int bt_init() { + if (cyw43_arch_init()) { + printf("Failed to initialize CYW43\n"); + return 1; + } + + l2cap_init(); + gap_connectable_control(1); + gap_discoverable_control(1); + l2cap_register_service(l2cap_packet_handler, PSM_HID_CONTROL, 0xffff, LEVEL_2); + l2cap_register_service(l2cap_packet_handler, PSM_HID_INTERRUPT, 0xffff, LEVEL_2); + + hci_event_callback_registration.callback = &hci_packet_handler; + hci_add_event_handler(&hci_event_callback_registration); + l2cap_event_callback_registration.callback = & l2cap_packet_handler; + l2cap_add_event_handler(&l2cap_event_callback_registration); + + hci_power_control(HCI_POWER_ON); + return 0; +} + +/*int main() { + stdio_init_all(); + + while (!stdio_usb_connected()) { + sleep_ms(100); + } + printf("USB Serial connected!\n"); + + init(); + + while (1) { + sleep_ms(10); + } +}*/ + +static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + (void) channel; + + const uint8_t event_type = hci_event_packet_get_type(packet); + + switch (event_type) { + case BTSTACK_EVENT_STATE: { + const uint8_t state = btstack_event_state_get_state(packet); + printf("[BT] State: %u\n",state); + if (state == HCI_STATE_WORKING) { + printf("[BT] Stack ready, start inquiry\n"); + gap_inquiry_start(30); + } + break; + } + case HCI_EVENT_INQUIRY_RESULT: + case HCI_EVENT_INQUIRY_RESULT_WITH_RSSI: + case HCI_EVENT_EXTENDED_INQUIRY_RESPONSE: { + bd_addr_t addr; + uint32_t cod; + + if (event_type == HCI_EVENT_INQUIRY_RESULT) { + cod = hci_event_inquiry_result_get_class_of_device(packet); + hci_event_inquiry_result_get_bd_addr(packet, addr); + } else if (event_type == HCI_EVENT_INQUIRY_RESULT_WITH_RSSI) { + cod = hci_event_inquiry_result_with_rssi_get_class_of_device(packet); + hci_event_inquiry_result_with_rssi_get_bd_addr(packet, addr); + } else { + cod = hci_event_extended_inquiry_response_get_class_of_device(packet); + hci_event_extended_inquiry_response_get_bd_addr(packet, addr); + } + + // CoD 0x002508 = Gamepad (Major: Peripheral, Minor: Gamepad) + if ((cod & 0x000F00) == 0x000500) { + printf("[HCI] Gamepad found: %s (CoD: 0x%06x)\n", bd_addr_to_str(addr), (unsigned int)cod); + bd_addr_copy(current_device_addr, addr); + device_found = true; + gap_inquiry_stop(); + } + break; + } + + case GAP_EVENT_INQUIRY_COMPLETE: + case HCI_EVENT_INQUIRY_COMPLETE: { + printf("[HCI] Inquiry complete\n"); + if (device_found) { + printf("[HCI] Connecting to %s...\n", bd_addr_to_str(current_device_addr)); + hci_send_cmd(&hci_create_connection, current_device_addr, + hci_usable_acl_packet_types(), 0, 0, 0, 1); + } + break; + } + case HCI_EVENT_COMMAND_STATUS: { + const uint8_t status = hci_event_command_status_get_status(packet); + const uint16_t opcode = hci_event_command_status_get_command_opcode(packet); + printf("[HCI] CmdStatus %s(0x%04X) status=0x%02X\n", opcode_to_str(opcode), opcode, status); + if (opcode == HCI_OPCODE_HCI_CREATE_CONNECTION && status != ERROR_CODE_SUCCESS) { + device_found = false; + printf("[HCI] Create connection rejected, restart inquiry\n"); + gap_inquiry_start(30); + } + break; + } + + case HCI_EVENT_COMMAND_COMPLETE: { + const uint8_t status = hci_event_command_complete_get_return_parameters(packet)[0]; + const uint16_t opcode = hci_event_command_complete_get_command_opcode(packet); + printf("[HCI] CmdComplete %s(0x%04X) status=0x%02X\n", opcode_to_str(opcode), opcode, status); + break; + } + + case HCI_EVENT_CONNECTION_COMPLETE: { + const uint8_t status = hci_event_connection_complete_get_status(packet); + if (status == 0) { + const hci_con_handle_t handle = hci_event_connection_complete_get_connection_handle(packet); + hci_event_connection_complete_get_bd_addr(packet, current_device_addr); + printf("[HCI] ACL connected handle=0x%04X\n", handle); + printf("[HCI] Request authentication on handle=0x%04X\n", handle); + hci_send_cmd(&hci_authentication_requested, handle); + } else { + device_found = false; + printf("[HCI] ACL connect failed status=0x%02X, restart inquiry\n", status); + gap_inquiry_start(30); + } + break; + } + + case HCI_EVENT_LINK_KEY_REQUEST: { + bd_addr_t addr; + hci_event_link_key_request_get_bd_addr(packet, addr); + link_key_t link_key; + link_key_type_t link_key_type; + if (gap_get_link_key_for_bd_addr(addr, link_key, &link_key_type)) { + printf("[HCI] Link key request from %s, reply stored key type=%u\n", bd_addr_to_str(addr), + (unsigned int)link_key_type); + hci_send_cmd(&hci_link_key_request_reply, addr, link_key); + } else { + printf("[HCI] Link key request from %s, no key, force re-pair\n", bd_addr_to_str(addr)); + hci_send_cmd(&hci_link_key_request_negative_reply, addr); + } + break; + } + + case HCI_EVENT_USER_CONFIRMATION_REQUEST: { + bd_addr_t addr; + hci_event_user_confirmation_request_get_bd_addr(packet, addr); + printf("[HCI] User confirmation request from %s, accept\n", bd_addr_to_str(addr)); + hci_send_cmd(&hci_user_confirmation_request_reply, addr); + break; + } + + case HCI_EVENT_PIN_CODE_REQUEST: { + bd_addr_t addr; + hci_event_pin_code_request_get_bd_addr(packet, addr); + printf("[HCI] Legacy pin request from %s, reply 0000\n", bd_addr_to_str(addr)); + gap_pin_code_response(addr, "0000"); + break; + } + + case HCI_EVENT_AUTHENTICATION_COMPLETE: { + const uint8_t status = hci_event_authentication_complete_get_status(packet); + const hci_con_handle_t handle = hci_event_authentication_complete_get_connection_handle(packet); + printf("[HCI] Authentication complete handle=0x%04X status=0x%02X\n", handle, status); + if (status != ERROR_CODE_SUCCESS) { + printf("[HCI] Authentication failed, drop stored key for %s\n", bd_addr_to_str(current_device_addr)); + gap_drop_link_key_for_bd_addr(current_device_addr); + }else { + hci_send_cmd(&hci_set_connection_encryption, handle, 1); + } + break; + } + + case HCI_EVENT_ENCRYPTION_CHANGE: { + const uint8_t status = hci_event_encryption_change_get_status(packet); + const hci_con_handle_t handle = hci_event_encryption_change_get_connection_handle(packet); + const uint8_t enabled = hci_event_encryption_change_get_encryption_enabled(packet); + printf("[HCI] Encryption change handle=0x%04X status=0x%02X enabled=%u\n", handle, status, enabled); + if (status == ERROR_CODE_SUCCESS && enabled) { + printf("[L2CAP] Open HID channels\n"); + if (hid_control_cid == 0) { + l2cap_create_channel(l2cap_packet_handler, current_device_addr, PSM_HID_CONTROL, 0xffff, &hid_control_cid); + } else if (hid_interrupt_cid == 0){ + l2cap_create_channel(l2cap_packet_handler,current_device_addr,PSM_HID_INTERRUPT,0xffff,&hid_interrupt_cid); + } + } + break; + } + + case HCI_EVENT_CONNECTION_REQUEST: { + bd_addr_t addr; + hci_event_connection_request_get_bd_addr(packet, addr); + const uint32_t cod = hci_event_connection_request_get_class_of_device(packet); + printf("[HCI] Incoming ACL request from %s cod=0x%06x\n", bd_addr_to_str(addr), (unsigned int) cod); + if ((cod & 0x000F00) == 0x000500) { + bd_addr_copy(current_device_addr, addr); + gap_inquiry_stop(); + hci_send_cmd(&hci_accept_connection_request, addr, 0x01); + } + break; + } + + case HCI_EVENT_DISCONNECTION_COMPLETE: { + const uint8_t reason = hci_event_disconnection_complete_get_reason(packet); + device_found = false; + hid_control_cid = 0; + hid_interrupt_cid = 0; + feature_data.clear(); + printf("[HCI] Disconnected reason=0x%02X, restart inquiry\n", reason); + gap_inquiry_start(30); + break; + } + } +} + +static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + (void) channel; + + if (packet_type == L2CAP_DATA_PACKET) { + if (channel == hid_interrupt_cid) { + printf("[L2CAP] HID Interrupt data len=%u\n", size); + printf_hexdump(packet, size); + bt_data_callback(INTERRUPT, packet, size); + } else if (channel == hid_control_cid) { + if (size > 1 && packet[0] == 0xA3) { + uint8_t report_id = packet[1]; + feature_data[report_id].assign(packet + 1, packet + size); + printf("[L2CAP] Stored Feature Report %d, len=%u\n", report_id, size - 1); + } + printf("[L2CAP] HID Control data len=%u\n", size); + printf_hexdump(packet, size); + bt_data_callback(CONTROL, packet, size); + } else { + printf("[L2CAP] Data on unknown channel 0x%04X (Interrupt: 0x%04X, Control: 0x%04X)\n", channel, hid_interrupt_cid, hid_control_cid); + } + return; + } + + const uint8_t event_type = hci_event_packet_get_type(packet); + switch (event_type) { + case L2CAP_EVENT_CHANNEL_OPENED: { + const uint8_t status = l2cap_event_channel_opened_get_status(packet); + if (status == 0) { + const uint16_t psm = l2cap_event_channel_opened_get_psm(packet); + + if (psm == PSM_HID_CONTROL) { + printf("[L2CAP] HID Control opened cid=0x%04X\n", hid_control_cid); + } else if (psm == PSM_HID_INTERRUPT) { + printf("[L2CAP] HID Interrupt opened cid=0x%04X\n", hid_interrupt_cid); + + printf("Ready\n"); + uint8_t get_feature[41] = { + 0x43, + 0x05 + }; + l2cap_send(hid_control_cid, get_feature, 41); + + uint8_t report32[142]; + report32[0] = 0x32; + report32[1] = 0x10; + uint8_t packet_0x10[] = + { + 0x90, // Packet: 0x10 + 0x3f, // 63 + // SetStateData + 0xfd, 0xf7, 0x0, 0x0, 0x7f, 0x7f, + 0xff, 0x9, 0x0, 0xf, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, + 0x7, 0x0, 0x0, 0x2, 0x1, + 0x00, + 0xff,0xd7,0x00 // RGB LED: R, G, B + }; + memcpy(report32 + 2,packet_0x10,sizeof(packet_0x10)); + bt_write(report32,sizeof(report32)); + init_feature(); + } + + if (hid_control_cid != 0 && hid_interrupt_cid != 0) { + printf("[L2CAP] HID channels ready, request CAN_SEND_NOW for SET_PROTOCOL\n"); + l2cap_request_can_send_now_event(hid_control_cid); + } + } else { + const uint16_t psm = l2cap_event_channel_opened_get_psm(packet); + hid_control_cid = 0; + hid_interrupt_cid = 0; + device_found = false; + printf("[L2CAP] Open failed psm=0x%04X status=0x%02X\n", psm, status); + } + break; + } + + case L2CAP_EVENT_INCOMING_CONNECTION: { + const uint16_t local_cid = l2cap_event_incoming_connection_get_local_cid(packet); + const uint16_t psm = l2cap_event_incoming_connection_get_psm(packet); + printf("[L2CAP] Incoming connection psm=0x%04X cid=0x%04X\n", psm, local_cid); + l2cap_accept_connection(local_cid); + break; + } + + case L2CAP_EVENT_CHANNEL_CLOSED: { + const uint16_t local_cid = l2cap_event_channel_closed_get_local_cid(packet); + if (local_cid == hid_control_cid) { + hid_control_cid = 0; + printf("[L2CAP] HID Control closed cid=0x%04X\n", local_cid); + } else if (local_cid == hid_interrupt_cid) { + hid_interrupt_cid = 0; + printf("[L2CAP] HID Interrupt closed cid=0x%04X\n", local_cid); + } else { + printf("[L2CAP] Channel closed cid=0x%04X\n", local_cid); + } + break; + } + + case L2CAP_EVENT_CAN_SEND_NOW: { + printf("L2CAP_EVENT_CAN_SEND_NOW\n"); + + break; + } + } +} + +void bt_write(uint8_t* data,uint16_t len) { + if (!device_found) return; + uint8_t buffer[len + 1]; + buffer[0] = 0xA2; + memcpy(buffer + 1,data,len); + fill_output_report_checksum(buffer + 1,len); + l2cap_send(hid_interrupt_cid,buffer,sizeof(buffer)); +} + +uint8_t* get_feature_data(uint8_t reportId,uint16_t len) { + if (feature_data.find(reportId) == feature_data.end() || feature_data[reportId].empty()) { + if (hid_control_cid != 0) { + uint8_t get_feature[] = {0x43, reportId}; + l2cap_send(hid_control_cid, get_feature, len); + printf("[L2CAP] Requesting Feature Report 0x%02X\n", reportId); + } + return 0; + } + return feature_data[reportId].data(); +} + +void init_feature() { + get_feature_data(0x09,20); + get_feature_data(0x20,64); + get_feature_data(0x05,41); +} \ No newline at end of file diff --git a/src/bt.h b/src/bt.h new file mode 100644 index 0000000..c77ee46 --- /dev/null +++ b/src/bt.h @@ -0,0 +1,25 @@ +// +// Created by awalol on 2026/3/4. +// + +#ifndef DS5_BRIDGE_BT_H +#define DS5_BRIDGE_BT_H + +#include + +enum CHANNEL_TYPE { + INTERRUPT, + CONTROL +}; + +typedef void (*bt_data_callback_t)(CHANNEL_TYPE channel, uint8_t *data, uint16_t len); + +int bt_init(); +void bt_register_data_callback(bt_data_callback_t callback); +void bt_send_packet(uint8_t *data, uint16_t len); +void bt_send_control(uint8_t *data, uint16_t len); +void bt_write(uint8_t* data,uint16_t len); +uint8_t* get_feature_data(uint8_t reportId,uint16_t len); +void init_feature(); + +#endif //DS5_BRIDGE_BT_H \ No newline at end of file diff --git a/src/btstack_config.h b/src/btstack_config.h new file mode 100644 index 0000000..41ac7f8 --- /dev/null +++ b/src/btstack_config.h @@ -0,0 +1,42 @@ +// https://github.com/rafaelvaloto/Pico_W-Dualsense/blob/main/btstack_config.h + +// c +#ifndef BTSTACK_CONFIG_H +#define BTSTACK_CONFIG_H + +#define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE +#ifndef ENABLE_CLASSIC +#define ENABLE_CLASSIC +#endif + + +// CYW43 HCI Transport requires pre-buffer space for packet header + +// Se estiver 1 ou 2, o 0x31 do DualSense causa estouro +#define MAX_NR_HCI_ACL_PACKETS 4 + +#define MAX_NR_HCI_CONNECTIONS 4 +#define MAX_NR_L2CAP_CHANNELS 4 +#define MAX_NR_L2CAP_SERVICES 4 +// +#define HCI_ACL_PAYLOAD_SIZE 256 +#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4 +#define HCI_OUTGOING_PRE_BUFFER_SIZE 4 + + +#define MAX_NR_RFCOMM_MULTIPLEXERS 0 +#define MAX_NR_RFCOMM_SERVICES 0 +#define MAX_NR_RFCOMM_CHANNELS 0 + +// CYW43 específico - necessário para o transport layer + +#define NVM_NUM_LINK_KEYS 4 +#define NVM_NUM_DEVICE_DB_ENTRIES 4 +#define HAVE_EMBEDDED_TIME_MS + +// Logging +#define ENABLE_PRINTF_HEXDUMP +#define ENABLE_LOG_INFO +#define ENABLE_LOG_ERROR + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f2a2758 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,100 @@ +// +// Created by awalol on 2026/3/4. +// + +#include +#include "bsp/board_api.h" +#include "usb.h" +#include "bt.h" +#include "utils.h" +#include "pico/multicore.h" +#include "resample.h" +#include "audio.h" + +int reportSeqCounter = 0; +uint8_t packetCounter = 0; + +uint8_t interrupt_in_data[63]; + +bool interrupt_in_cb(repeating_timer* rt) { + if (tud_ready()) { + tud_hid_report(0x01,interrupt_in_data,63); + } + return true; +} + +void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) { + printf("[Main] BT data callback: channel=%u len=%u\n", channel, len); + if (channel == INTERRUPT && data[1] == 0x31) { + memcpy(interrupt_in_data,data + 3,63); + } +} + +// Invoked when received GET_REPORT control request +// Application must fill buffer report's content and return its length. +// Return zero will cause the stack to STALL request +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) { + (void) itf; + (void) report_id; + (void) report_type; + (void) buffer; + (void) reqlen; + + uint8_t *feature_data = get_feature_data(report_id, reqlen); + if (feature_data) { + memcpy(buffer, feature_data, reqlen); + } + + return feature_data ? reqlen : 0; +} + +// Invoked when received SET_REPORT control request or +// received data on OUT endpoint ( Report ID = 0, Type = 0 ) +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) { + // This example doesn't use multiple report and report ID + (void) itf; + (void) report_id; + (void) report_type; + (void) buffer; + (void) bufsize; + + switch (buffer[0]) { + case 0x02: { + uint8_t outputData[78]; + outputData[0] = 0x31; + outputData[1] = reportSeqCounter << 4; + if (++reportSeqCounter == 256) { + reportSeqCounter = 0; + } + outputData[2] = 0x10; + memcpy(outputData + 3,buffer + 1,bufsize - 1); + bt_write(outputData,sizeof(outputData)); + break; + } + } +} + +int main() { + board_init(); + + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + bt_init(); + bt_register_data_callback(on_bt_data); + + multicore_launch_core1(core1_entry); + repeating_timer rt{}; + add_repeating_timer_ms(4,interrupt_in_cb,nullptr,&rt); + + while (1) { + tud_task(); + } +} diff --git a/src/tusb_config.h b/src/tusb_config.h new file mode 100644 index 0000000..e217624 --- /dev/null +++ b/src/tusb_config.h @@ -0,0 +1,135 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus + extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Board Specific Configuration +//--------------------------------------------------------------------+ + +// RHPort number used for device can be defined by board.mk, default to port 0 +#ifndef BOARD_TUD_RHPORT +#define BOARD_TUD_RHPORT 0 +#endif + +// RHPort max operational speed can defined by board.mk +#ifndef BOARD_TUD_MAX_SPEED +#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +// Enable Device stack +#define CFG_TUD_ENABLED 1 + +// Default is max speed that hardware controller could support with on-chip PHY +#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_AUDIO 1 +#define CFG_TUD_HID 1 +#define CFG_TUD_CDC 1 +#define CFG_TUD_MSC 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 0 + +// HID buffer size Should be sufficient to hold ID (if any) + Data +#define CFG_TUD_HID_EP_BUFSIZE 64 + +//-------------------------------------------------------------------- +// AUDIO CLASS DRIVER CONFIGURATION +//-------------------------------------------------------------------- + +#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX 4 + +// 16bit data in 16bit slots +#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_RX 2 +#define CFG_TUD_AUDIO_FUNC_1_RESOLUTION_RX 16 + +// Microphone (IN/TX) path: 2-channel, 16-bit +#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX 2 +#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX 2 +#define CFG_TUD_AUDIO_FUNC_1_RESOLUTION_TX 16 + +// UAC1 Full-Speed endpoint size +#define CFG_TUD_AUDIO_FUNC_1_SAMPLE_RATE 48000 +#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX 392 +#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX 196 + +#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ 2048 +#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ (4 * CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX) + +// Enable OUT EP (speaker) and IN EP (mic) +#define CFG_TUD_AUDIO_ENABLE_EP_OUT 1 +#define CFG_TUD_AUDIO_ENABLE_EP_IN 1 + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_CONFIG_H_ */ \ No newline at end of file diff --git a/src/usb.cpp b/src/usb.cpp new file mode 100644 index 0000000..5c134ff --- /dev/null +++ b/src/usb.cpp @@ -0,0 +1,162 @@ +// +// Created by awalol on 2026/3/4. +// + +#include "usb.h" +#include "bsp/board_api.h" + +uint8_t mute[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX + 1]; // +1 for master channel 0 +int16_t volume[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX + 1]; // +1 for master channel 0 +#define UAC1_ENTITY_SPK_FEATURE_UNIT 0x02 +#define UAC1_ENTITY_MIC_FEATURE_UNIT 0x05 + +/*int main() { + board_init(); + + tusb_rhport_init_t dev_init = { + .role = TUSB_ROLE_DEVICE, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + while (1) { + tud_task(); + } +}*/ + +//--------------------------------------------------------------------+ +// Audio Callback Functions +//--------------------------------------------------------------------+ + +//--------------------------------------------------------------------+ +// UAC1 Helper Functions +//--------------------------------------------------------------------+ + +static bool audio10_set_req_entity(tusb_control_request_t const *p_request, uint8_t *pBuff) { + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t entityID = TU_U16_HIGH(p_request->wIndex); + + // If request is for our speaker feature unit + if (entityID == UAC1_ENTITY_SPK_FEATURE_UNIT) { + switch (ctrlSel) { + case AUDIO10_FU_CTRL_MUTE: + switch (p_request->bRequest) { + case AUDIO10_CS_REQ_SET_CUR: + // Only 1st form is supported + TU_VERIFY(p_request->wLength == 1); + + mute[channelNum] = pBuff[0]; + + TU_LOG2(" Set Mute: %d of channel: %u\r\n", mute[channelNum], channelNum); + return true; + + default: + return false; // not supported + } + + case AUDIO10_FU_CTRL_VOLUME: + switch (p_request->bRequest) { + case AUDIO10_CS_REQ_SET_CUR: + // Only 1st form is supported + TU_VERIFY(p_request->wLength == 2); + + volume[channelNum] = (int16_t) tu_unaligned_read16(pBuff) / 256; + + TU_LOG2(" Set Volume: %d dB of channel: %u\r\n", volume[channelNum], channelNum); + return true; + + default: + return false; // not supported + } + + // Unknown/Unsupported control + default: + TU_BREAKPOINT(); + return false; + } + } + + return false; +} + +static bool audio10_get_req_entity(uint8_t rhport, tusb_control_request_t const *p_request) { + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t entityID = TU_U16_HIGH(p_request->wIndex); + + // If request is for our speaker feature unit + if (entityID == UAC1_ENTITY_SPK_FEATURE_UNIT || entityID == UAC1_ENTITY_MIC_FEATURE_UNIT) { + switch (ctrlSel) { + case AUDIO10_FU_CTRL_MUTE: + // Audio control mute cur parameter block consists of only one byte - we thus can send it right away + // There does not exist a range parameter block for mute + TU_LOG2(" Get Mute of channel: %u\r\n", channelNum); + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &mute[channelNum], 1); + + case AUDIO10_FU_CTRL_VOLUME: + switch (p_request->bRequest) { + case AUDIO10_CS_REQ_GET_CUR: + TU_LOG2(" Get Volume of channel: %u\r\n", channelNum); { + int16_t vol = (int16_t) volume[channelNum]; + vol = vol * 256; // convert to 1/256 dB units + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &vol, sizeof(vol)); + } + + case AUDIO10_CS_REQ_GET_MIN: + TU_LOG2(" Get Volume min of channel: %u\r\n", channelNum); { + int16_t min = -90; // -90 dB + min = min * 256; // convert to 1/256 dB units + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &min, sizeof(min)); + } + + case AUDIO10_CS_REQ_GET_MAX: + TU_LOG2(" Get Volume max of channel: %u\r\n", channelNum); { + int16_t max = 30; // +30 dB + max = max * 256; // convert to 1/256 dB units + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &max, sizeof(max)); + } + + case AUDIO10_CS_REQ_GET_RES: + TU_LOG2(" Get Volume res of channel: %u\r\n", channelNum); { + int16_t res = 1; // 1 dB + res = res * 256; // convert to 1/256 dB units + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &res, sizeof(res)); + } + // Unknown/Unsupported control + default: + TU_BREAKPOINT(); + return false; + } + break; + + // Unknown/Unsupported control + default: + TU_BREAKPOINT(); + return false; + } + } + + return false; +} + +// Invoked when audio class specific get request received for an entity +bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request) { + (void) rhport; + + return audio10_get_req_entity(rhport, p_request); +} + +// Invoked when audio class specific set request received for an entity +bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request, uint8_t *buf) { + (void) rhport; + + return audio10_set_req_entity(p_request, buf); +} + +void tud_hid_report_complete_cb(uint8_t instance, uint8_t const *report, uint16_t len) { + (void) instance; + (void) len; +} diff --git a/src/usb.h b/src/usb.h new file mode 100644 index 0000000..92f7267 --- /dev/null +++ b/src/usb.h @@ -0,0 +1,21 @@ +// +// Created by awalol on 2026/3/4. +// + +#ifndef DS5_BRIDGE_USB_H +#define DS5_BRIDGE_USB_H + +#include "tusb.h" + +// typedef void (*usb_hid_get_report_callback_t)(uint16_t channel, uint8_t *data, uint16_t len); +// typedef void (*usb_hid_set_report_callback_t)(uint16_t channel, uint8_t *data, uint16_t len); + +/*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); +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);*/ +bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request); +bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request, uint8_t *buf); +void tud_hid_report_complete_cb(uint8_t instance, uint8_t const *report, uint16_t len); + +#endif //DS5_BRIDGE_USB_H \ No newline at end of file diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c new file mode 100644 index 0000000..a372dd9 --- /dev/null +++ b/src/usb_descriptors.c @@ -0,0 +1,550 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 HiFiPhile + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "bsp/board_api.h" +#include "tusb.h" + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +static tusb_desc_device_t const desc_device = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + + // Use Interface Association Descriptor (IAD) for Audio + // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1) + /*.bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD,*/ + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0x054C, + .idProduct = 0x0CE6, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + +// Invoked when received GET DEVICE DESCRIPTOR +// Application return pointer to descriptor +uint8_t const *tud_descriptor_device_cb(void) { + return (uint8_t const *) &desc_device; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ + + +#define EPNUM_AUDIO 0x01 + +#define CONFIG_UAC1_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_AUDIO10_SPEAKER_STEREO_FB_DESC_LEN(2)) + +uint8_t const descriptor_configuration[] = { + // --- CONFIGURATION DESCRIPTOR --- + 0x09, // bLength + 0x02, // bDescriptorType (CONFIGURATION) + 0xE3, 0x00, // wTotalLength: 227 + 0x04, // bNumInterfaces: 4 + 0x01, // bConfigurationValue: 1 + 0x00, // iConfiguration: 0 + 0xC0, // bmAttributes: SELF-POWERED, NO REMOTE-WAKEUP + 0xFA, // bMaxPower: 500mA (250 * 2mA) + + // --- INTERFACE DESCRIPTOR (0.0): Audio Control --- + 0x09, // bLength + 0x04, // bDescriptorType (INTERFACE) + 0x00, // bInterfaceNumber: 0 + 0x00, // bAlternateSetting: 0 + 0x00, // bNumEndpoints: 0 + 0x01, // bInterfaceClass: Audio (0x01) + 0x01, // bInterfaceSubClass: Audio Control (0x01) + 0x00, // bInterfaceProtocol: 0x00 + 0x00, // iInterface: 0 + + // Class-specific AC Interface Header Descriptor + 0x0A, // bLength: 10 + 0x24, // bDescriptorType: CS_INTERFACE (0x24) + 0x01, // bDescriptorSubtype: Header (0x01) + 0x00, 0x01, // bcdADC: 1.00 + 0x49, 0x00, // wTotalLength: 73 (0x0049) + 0x02, // bInCollection: 2 streaming interfaces + 0x01, // baInterfaceNr(1): Interface 1 + 0x02, // baInterfaceNr(2): Interface 2 + + // Input Terminal Descriptor (Terminal ID 1: USB Streaming → Output to Speaker) + 0x0C, // bLength: 12 + 0x24, // bDescriptorType: CS_INTERFACE + 0x02, // bDescriptorSubtype: Input Terminal + 0x01, // bTerminalID: 1 + 0x01, 0x01, // wTerminalType: USB Streaming (0x0101) + 0x06, // bAssocTerminal: 6 (paired with USB OUT terminal) + 0x04, // bNrChannels: 4 + 0x33, 0x00, // wChannelConfig: L/R Front + L/R Surround (0x0033) + 0x00, // iChannelNames: 0 + 0x00, // iTerminal: 0 + + // Feature Unit Descriptor (Unit ID 2 ← from Terminal 1) + 0x0C, // bLength: 12 + 0x24, // bDescriptorType: CS_INTERFACE + 0x06, // bDescriptorSubtype: Feature Unit + 0x02, // bUnitID: 2 + 0x01, // bSourceID: 1 + 0x01, // bControlSize: 1 byte per control + 0x03, // bmaControls[0]: Master – Mute, Volume + 0x00, 0x00, 0x00, 0x00, 0x00, // bmaControls[1..4]: No per-channel controls + + // Output Terminal Descriptor (Terminal ID 3: Speaker ← from Unit 2) + 0x09, // bLength: 9 + 0x24, // bDescriptorType: CS_INTERFACE + 0x03, // bDescriptorSubtype: Output Terminal + 0x03, // bTerminalID: 3 + 0x01, 0x03, // wTerminalType: Speaker (0x0301) + 0x04, // bAssocTerminal: 4 (paired with mic input) + 0x02, // bSourceID: 2 (Feature Unit) + 0x00, // iTerminal: 0 + + // Input Terminal Descriptor (Terminal ID 4: Headset Mic) + 0x0C, // bLength: 12 + 0x24, // bDescriptorType: CS_INTERFACE + 0x02, // bDescriptorSubtype: Input Terminal + 0x04, // bTerminalID: 4 + 0x02, 0x04, // wTerminalType: Headset (0x0402) + 0x03, // bAssocTerminal: 3 (paired with speaker) + 0x02, // bNrChannels: 2 + 0x03, 0x00, // wChannelConfig: L/R Front (0x0003) + 0x00, // iChannelNames: 0 + 0x00, // iTerminal: 0 + + // Feature Unit Descriptor (Unit ID 5 ← from Terminal 4) + 0x09, // bLength: 9 + 0x24, // bDescriptorType: CS_INTERFACE + 0x06, // bDescriptorSubtype: Feature Unit + 0x05, // bUnitID: 5 + 0x04, // bSourceID: 4 + 0x01, // bControlSize: 1 + 0x03, // bmaControls[0]: Master – Mute, Volume + 0x00, // bmaControls[1]: Ch1 – no controls + 0x00, // iFeature: 0 + + // Output Terminal Descriptor (Terminal ID 6: USB Streaming ← from Unit 5) + 0x09, // bLength: 9 + 0x24, // bDescriptorType: CS_INTERFACE + 0x03, // bDescriptorSubtype: Output Terminal + 0x06, // bTerminalID: 6 + 0x01, 0x01, // wTerminalType: USB Streaming (0x0101) + 0x01, // bAssocTerminal: 1 + 0x05, // bSourceID: 5 + 0x00, // iTerminal: 0 + + // --- INTERFACE DESCRIPTOR (1.0): Audio Streaming (OUT - Alternate 0) --- + 0x09, // bLength + 0x04, // bDescriptorType (INTERFACE) + 0x01, // bInterfaceNumber: 1 + 0x00, // bAlternateSetting: 0 + 0x00, // bNumEndpoints: 0 + 0x01, // bInterfaceClass: Audio + 0x02, // bInterfaceSubClass: Audio Streaming + 0x00, // bInterfaceProtocol + 0x00, // iInterface + + // --- INTERFACE DESCRIPTOR (1.1): Audio Streaming (OUT - Alternate 1) --- + 0x09, // bLength + 0x04, // bDescriptorType (INTERFACE) + 0x01, // bInterfaceNumber: 1 + 0x01, // bAlternateSetting: 1 + 0x01, // bNumEndpoints: 1 + 0x01, // bInterfaceClass: Audio + 0x02, // bInterfaceSubClass: Audio Streaming + 0x00, // bInterfaceProtocol + 0x00, // iInterface + + // AS General Descriptor (for Interface 1.1) + 0x07, // bLength: 7 + 0x24, // bDescriptorType: CS_INTERFACE + 0x01, // bDescriptorSubtype: AS_GENERAL + 0x01, // bTerminalLink: connected to Terminal ID 1 + 0x01, // bDelay: 1 frame + 0x01, 0x00, // wFormatTag: PCM (0x0001) + + // Format Type Descriptor (4-channel, 16-bit, 48kHz) + 0x0B, // bLength: 11 + 0x24, // bDescriptorType: CS_INTERFACE + 0x02, // bDescriptorSubtype: FORMAT_TYPE + 0x01, // bFormatType: TYPE_I + 0x04, // bNrChannels: 4 + 0x02, // bSubframeSize: 2 bytes/sample + 0x10, // bBitResolution: 16 bits + 0x01, // bSamFreqType: 1 discrete frequency + 0x80, 0xBB, 0x00, // tSamFreq: 48000 Hz (0x00BB80) + + // Endpoint Descriptor (Audio OUT: EP1) + 0x09, // bLength + 0x05, // bDescriptorType (ENDPOINT) + 0x01, // bEndpointAddress: OUT EP1 + 0x09, // bmAttributes: Isochronous, Adaptive + 0x88, 0x01, // wMaxPacketSize: 392 bytes + 0x04, // bInterval: 4 (1/(2^(4-1)) ms ≈ 125 µs/frame) + 0x00, // bRefresh + 0x00, // bSynchAddress + + // Class-specific Audio Streaming Endpoint Descriptor (EP1) + 0x07, // bLength + 0x25, // bDescriptorType: CS_ENDPOINT + 0x01, // bDescriptorSubtype: GENERAL + 0x00, // Attributes: No pitch/sampling freq control + 0x00, // Lock Delay Units: Undefined + 0x00, 0x00, // Lock Delay: 0 + + // --- INTERFACE DESCRIPTOR (2.0): Audio Streaming IN (Alternate 0) --- + 0x09, // bLength + 0x04, // bDescriptorType (INTERFACE) + 0x02, // bInterfaceNumber: 2 + 0x00, // bAlternateSetting: 0 + 0x00, // bNumEndpoints: 0 + 0x01, // bInterfaceClass: Audio + 0x02, // bInterfaceSubClass: Audio Streaming + 0x00, // bInterfaceProtocol + 0x00, // iInterface + + // --- INTERFACE DESCRIPTOR (2.1): Audio Streaming IN (Alternate 1) --- + 0x09, // bLength + 0x04, // bDescriptorType (INTERFACE) + 0x02, // bInterfaceNumber: 2 + 0x01, // bAlternateSetting: 1 + 0x01, // bNumEndpoints: 1 + 0x01, // bInterfaceClass: Audio + 0x02, // bInterfaceSubClass: Audio Streaming + 0x00, // bInterfaceProtocol + 0x00, // iInterface + + // AS General Descriptor (for Interface 2.1) + 0x07, // bLength: 7 + 0x24, // bDescriptorType: CS_INTERFACE + 0x01, // bDescriptorSubtype: AS_GENERAL + 0x06, // bTerminalLink: connected to Terminal ID 6 + 0x01, // bDelay: 1 frame + 0x01, 0x00, // wFormatTag: PCM (0x0001) + + // Format Type Descriptor (2-channel, 16-bit, 48kHz) + 0x0B, // bLength: 11 + 0x24, // bDescriptorType: CS_INTERFACE + 0x02, // bDescriptorSubtype: FORMAT_TYPE + 0x01, // bFormatType: TYPE_I + 0x02, // bNrChannels: 2 + 0x02, // bSubframeSize: 2 + 0x10, // bBitResolution: 16 + 0x01, // bSamFreqType: 1 + 0x80, 0xBB, 0x00, // tSamFreq: 48000 Hz + + // Endpoint Descriptor (Audio IN: EP2) + 0x09, // bLength + 0x05, // bDescriptorType (ENDPOINT) + 0x82, // bEndpointAddress: IN EP2 + 0x05, // bmAttributes: Isochronous, Asynchronous + 0xC4, 0x00, // wMaxPacketSize: 196 bytes + 0x04, // bInterval: 4 + 0x00, // bRefresh + 0x00, // bSynchAddress + + // Class-specific Audio Streaming Endpoint Descriptor (EP2) + 0x07, // bLength + 0x25, // bDescriptorType: CS_ENDPOINT + 0x01, // bDescriptorSubtype: GENERAL + 0x00, // Attributes: No controls + 0x00, // Lock Delay Units + 0x00, 0x00, // Lock Delay + + // --- INTERFACE DESCRIPTOR (3.0): HID (DualSense 5 Gamepad + Touchpad) --- + 0x09, // bLength + 0x04, // bDescriptorType (INTERFACE) + 0x03, // bInterfaceNumber: 3 + 0x00, // bAlternateSetting: 0 + 0x02, // bNumEndpoints: 2 (IN + OUT) + 0x03, // bInterfaceClass: HID + 0x00, // bInterfaceSubClass: None + 0x00, // bInterfaceProtocol: None + 0x00, // iInterface + + // HID Descriptor + 0x09, // bLength: 9 + 0x21, // bDescriptorType (HID) + 0x11, 0x01, // bcdHID: 1.11 + 0x00, // bCountryCode: Not localized + 0x01, // bNumDescriptors: 1 report descriptor + 0x22, // bDescriptorType: Report + 0x21, 0x01, // wDescriptorLength: 289 (0x0121) + + // Endpoint Descriptor (HID IN: EP4) + 0x07, // bLength + 0x05, // bDescriptorType (ENDPOINT) + 0x84, // bEndpointAddress: IN EP4 + 0x03, // bmAttributes: Interrupt + 0x40, 0x00, // wMaxPacketSize: 64 + 0x06, // bInterval: 6 (polling every 8ms) + + // Endpoint Descriptor (HID OUT: EP3) + 0x07, // bLength + 0x05, // bDescriptorType (ENDPOINT) + 0x03, // bEndpointAddress: OUT EP3 + 0x03, // bmAttributes: Interrupt + 0x40, 0x00, // wMaxPacketSize: 64 + 0x06, // bInterval: 6 +}; + +// Invoked when received GET CONFIGURATION DESCRIPTOR +// Application return pointer to descriptor +// Descriptor contents must exist long enough for transfer to complete +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { + (void) index; // for multiple configurations + return descriptor_configuration; +} + +//--------------------------------------------------------------------+ +// HID Report Descriptor +//--------------------------------------------------------------------+ + +uint8_t const desc_hid_report[] = { + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x05, // Usage (Game Pad) + 0xA1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID (1) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x09, 0x32, // Usage (Z) + 0x09, 0x35, // Usage (Rz) + 0x09, 0x33, // Usage (Rx) + 0x09, 0x34, // Usage (Ry) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x06, // Report Count (6) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) + 0x09, 0x20, // Usage (0x20) + 0x95, 0x01, // Report Count (1) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x39, // Usage (Hat switch) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x07, // Logical Maximum (7) + 0x35, 0x00, // Physical Minimum (0) + 0x46, 0x3B, 0x01, // Physical Maximum (315) + 0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter) + 0x75, 0x04, // Report Size (4) + 0x95, 0x01, // Report Count (1) + 0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State) + 0x65, 0x00, // Unit (None) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x0F, // Usage Maximum (0x0F) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x0F, // Report Count (15) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) + 0x09, 0x21, // Usage (0x21) + 0x95, 0x0D, // Report Count (13) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) + 0x09, 0x22, // Usage (0x22) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x34, // Report Count (52) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x85, 0x02, // Report ID (2) + 0x09, 0x23, // Usage (0x23) + 0x95, 0x2F, // Report Count (47) + 0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x05, // Report ID (5) + 0x09, 0x33, // Usage (0x33) + 0x95, 0x28, // Report Count (40) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x08, // Report ID (8) + 0x09, 0x34, // Usage (0x34) + 0x95, 0x2F, // Report Count (47) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x09, // Report ID (9) + 0x09, 0x24, // Usage (0x24) + 0x95, 0x13, // Report Count (19) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x0A, // Report ID (10) + 0x09, 0x25, // Usage (0x25) + 0x95, 0x1A, // Report Count (26) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x0B, 0x09, 0x41, 0x95, 0x29, 0xB1, 0x02, // ID 0B (新增项) + 0x85, 0x0C, 0x09, 0x42, 0x95, 0x29, 0xB1, 0x02, // ID 0C (新增项) + 0x85, 0x20, // Report ID (32) + 0x09, 0x26, // Usage (0x26) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x21, // Report ID (33) + 0x09, 0x27, // Usage (0x27) + 0x95, 0x04, // Report Count (4) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x22, // Report ID (34) + 0x09, 0x40, // Usage (0x40) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x80, // Report ID (128) + 0x09, 0x28, // Usage (0x28) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x81, // Report ID (129) + 0x09, 0x29, // Usage (0x29) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x82, // Report ID (130) + 0x09, 0x2A, // Usage (0x2A) + 0x95, 0x09, // Report Count (9) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x83, // Report ID (131) + 0x09, 0x2B, // Usage (0x2B) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x84, // Report ID (132) + 0x09, 0x2C, // Usage (0x2C) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0x85, // Report ID (133) + 0x09, 0x2D, // Usage (0x2D) + 0x95, 0x02, // Report Count (2) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xA0, // Report ID (160) + 0x09, 0x2E, // Usage (0x2E) + 0x95, 0x01, // Report Count (1) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xE0, // Report ID (224) + 0x09, 0x2F, // Usage (0x2F) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xF0, // Report ID (240) + 0x09, 0x30, // Usage (0x30) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xF1, // Report ID (241) + 0x09, 0x31, // Usage (0x31) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xF2, // Report ID (242) + 0x09, 0x32, // Usage (0x32) + 0x95, 0x0F, // Report Count (15) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xF4, // Report ID (244) + 0x09, 0x35, // Usage (0x35) + 0x95, 0x3F, // Report Count (63) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0x85, 0xF5, // Report ID (245) + 0x09, 0x36, // Usage (0x36) + 0x95, 0x03, // Report Count (3) + 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) + 0xC0, // End Collection +}; + +// Invoked when received GET HID REPORT DESCRIPTOR +// Application return pointer to descriptor +// Descriptor contents must exist long enough for transfer to complete +uint8_t const *tud_hid_descriptor_report_cb(uint8_t itf) { + (void) itf; + return desc_hid_report; +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +// String Descriptor Index +enum { + STRID_LANGID = 0, + STRID_MANUFACTURER, + STRID_PRODUCT, + STRID_SERIAL, +}; + +// array of pointer to string descriptors +static char const *string_desc_arr[] = +{ + (const char[]){0x09, 0x04}, // 0: is supported language is English (0x0409) + "TinyUSB", // 1: Manufacturer + "Wireless Controller", // 2: Product + NULL, // 3: Serials will use unique ID if possible + "UAC2 Speaker", // 4: Audio Interface + "UAC1 Speaker", // 5: UAC1 Audio Interface +}; + +static uint16_t _desc_str[60 + 1]; + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void) langid; + size_t chr_count; + + switch (index) { + case STRID_LANGID: + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + break; + + case STRID_SERIAL: + chr_count = board_usb_get_serial(_desc_str + 1, 32); + break; + + default: + // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. + // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors + + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) return NULL; + + const char *str = string_desc_arr[index]; + + // Cap at max char + chr_count = strlen(str); + size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; // -1 for string type + if (chr_count > max_count) chr_count = max_count; + + // Convert ASCII string into UTF-16 + for (size_t i = 0; i < chr_count; i++) { + _desc_str[1 + i] = str[i]; + } + break; + } + + // first byte is length (including header), second byte is string type + _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2)); + + return _desc_str; +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..c4a3c31 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,215 @@ +// +// Created by awalol on 2026/3/4. +// + +#include + +#include "hci_cmd.h" + +inline const char *opcode_to_str(const uint16_t opcode) { + switch (opcode) { + case HCI_OPCODE_HCI_INQUIRY: + return "HCI_INQUIRY"; + case HCI_OPCODE_HCI_INQUIRY_CANCEL: + return "HCI_INQUIRY_CANCEL"; + case HCI_OPCODE_HCI_CREATE_CONNECTION: + return "HCI_CREATE_CONNECTION"; + case HCI_OPCODE_HCI_ACCEPT_CONNECTION_REQUEST: + return "HCI_ACCEPT_CONNECTION_REQUEST"; + case HCI_OPCODE_HCI_LINK_KEY_REQUEST_REPLY: + return "HCI_LINK_KEY_REQUEST_REPLY"; + case HCI_OPCODE_HCI_LINK_KEY_REQUEST_NEGATIVE_REPLY: + return "HCI_LINK_KEY_REQUEST_NEGATIVE_REPLY"; + case HCI_OPCODE_HCI_REJECT_CONNECTION_REQUEST: + return "HCI_REJECT_CONNECTION_REQUEST"; + case HCI_OPCODE_HCI_AUTHENTICATION_REQUESTED: + return "HCI_AUTHENTICATION_REQUESTED"; + case HCI_OPCODE_HCI_SET_CONNECTION_ENCRYPTION: + return "HCI_SET_CONNECTION_ENCRYPTION"; + case HCI_OPCODE_HCI_READ_REMOTE_SUPPORTED_FEATURES_COMMAND: + return "HCI_READ_REMOTE_SUPPORTED_FEATURES"; + case HCI_OPCODE_HCI_READ_REMOTE_EXTENDED_FEATURES_COMMAND: + return "HCI_READ_REMOTE_EXTENDED_FEATURES"; + case HCI_OPCODE_HCI_SWITCH_ROLE_COMMAND: + return "HCI_SWITCH_ROLE"; + case HCI_OPCODE_HCI_IO_CAPABILITY_REQUEST_REPLY: + return "HCI_IO_CAPABILITY_REQUEST_REPLY"; + case HCI_OPCODE_HCI_USER_CONFIRMATION_REQUEST_REPLY: + return "HCI_USER_CONFIRMATION_REQUEST_REPLY"; + case HCI_OPCODE_HCI_DISCONNECT: + return "HCI_DISCONNECT"; + case HCI_OPCODE_HCI_SET_EVENT_MASK: + return "HCI_SET_EVENT_MASK"; + case HCI_OPCODE_HCI_RESET: + return "HCI_RESET"; + case HCI_OPCODE_HCI_WRITE_LOCAL_NAME: + return "HCI_WRITE_LOCAL_NAME"; + case HCI_OPCODE_HCI_READ_LOCAL_NAME: + return "HCI_READ_LOCAL_NAME"; + case HCI_OPCODE_HCI_WRITE_PAGE_TIMEOUT: + return "HCI_WRITE_PAGE_TIMEOUT"; + case HCI_OPCODE_HCI_WRITE_SCAN_ENABLE: + return "HCI_WRITE_SCAN_ENABLE"; + case HCI_OPCODE_HCI_WRITE_CLASS_OF_DEVICE: + return "HCI_WRITE_CLASS_OF_DEVICE"; + case HCI_OPCODE_HCI_WRITE_INQUIRY_MODE: + return "HCI_WRITE_INQUIRY_MODE"; + case HCI_OPCODE_HCI_WRITE_EXTENDED_INQUIRY_RESPONSE: + return "HCI_WRITE_EXTENDED_INQUIRY_RESPONSE"; + case HCI_OPCODE_HCI_WRITE_PAGE_SCAN_TYPE: + return "HCI_WRITE_PAGE_SCAN_TYPE"; + case HCI_OPCODE_HCI_WRITE_SIMPLE_PAIRING_MODE: + return "HCI_WRITE_SIMPLE_PAIRING_MODE"; + case HCI_OPCODE_HCI_SET_EVENT_MASK_2: + return "HCI_SET_EVENT_MASK_2"; + case HCI_OPCODE_HCI_WRITE_LE_HOST_SUPPORTED: + return "HCI_WRITE_LE_HOST_SUPPORTED"; + case HCI_OPCODE_HCI_WRITE_SECURE_CONNECTIONS_HOST_SUPPORT: + return "HCI_WRITE_SECURE_CONNECTIONS_HOST_SUPPORT"; + case HCI_OPCODE_HCI_WRITE_DEFAULT_LINK_POLICY_SETTING: + return "HCI_WRITE_DEFAULT_LINK_POLICY_SETTING"; + case HCI_OPCODE_HCI_READ_LOCAL_VERSION_INFORMATION: + return "HCI_READ_LOCAL_VERSION_INFORMATION"; + case HCI_OPCODE_HCI_READ_LOCAL_SUPPORTED_COMMANDS: + return "HCI_READ_LOCAL_SUPPORTED_COMMANDS"; + case HCI_OPCODE_HCI_READ_LOCAL_SUPPORTED_FEATURES: + return "HCI_READ_LOCAL_SUPPORTED_FEATURES"; + case HCI_OPCODE_HCI_READ_BUFFER_SIZE: + return "HCI_READ_BUFFER_SIZE"; + case HCI_OPCODE_HCI_READ_BD_ADDR: + return "HCI_READ_BD_ADDR"; + case HCI_OPCODE_HCI_READ_ENCRYPTION_KEY_SIZE: + return "HCI_READ_ENCRYPTION_KEY_SIZE"; + case 0xFC01: + return "HCI_VENDOR_0xFC01"; + default: + return "UNKNOWN_OPCODE"; + } +} + +inline uint32_t crc32(const uint8_t* data, size_t size) { + uint32_t crc = ~0xEADA2D49; // 0xA2 seed + + while (size--) { + crc ^= *data++; + for (unsigned i = 0; i < 8; i++) + crc = ((crc >> 1) ^ (0xEDB88320 & -(crc & 1))); + } + + return ~crc; +} + +inline void fill_output_report_checksum(uint8_t* outputData,size_t len) +{ + uint32_t crc = crc32(outputData, len - 4); + outputData[len - 4] = (crc >> 0) & 0xFF; + outputData[len - 3] = (crc >> 8) & 0xFF; + outputData[len - 2] = (crc >> 16) & 0xFF; + outputData[len - 1] = (crc >> 24) & 0xFF; +} + +enum PowerState : uint8_t { + Discharging = 0x00, // Use PowerPercent + Charging = 0x01, // Use PowerPercent + Complete = 0x02, // PowerPercent not valid? assume 100%? + AbnormalVoltage = 0x0A, // PowerPercent not valid? + AbnormalTemperature = 0x0B, // PowerPercent not valid? + ChargingError = 0x0F // PowerPercent not valid? +}; + +enum Direction : uint8_t { + North = 0, + NorthEast, + East, + SouthEast, + South, + SouthWest, + West, + NorthWest, + None = 8 +}; + +struct __attribute__((packed)) TouchFingerData { // 4 + /*0.0*/ uint32_t Index : 7; + /*0.7*/ uint32_t NotTouching : 1; + /*1.0*/ uint32_t FingerX : 12; + /*2.4*/ uint32_t FingerY : 12; +}; + +struct __attribute__((packed)) TouchData { // 9 + /*0*/ TouchFingerData Finger[2]; + /*8*/ uint8_t Timestamp; +}; + +struct __attribute__((packed)) USBGetStateData { // 63 +/* 0 */ uint8_t LeftStickX; +/* 1 */ uint8_t LeftStickY; +/* 2 */ uint8_t RightStickX; +/* 3 */ uint8_t RightStickY; +/* 4 */ uint8_t TriggerLeft; +/* 5 */ uint8_t TriggerRight; +/* 6 */ uint8_t SeqNo; // always 0x01 on BT +/* 7.0*/ Direction DPad : 4; +/* 7.4*/ uint8_t ButtonSquare : 1; +/* 7.5*/ uint8_t ButtonCross : 1; +/* 7.6*/ uint8_t ButtonCircle : 1; +/* 7.7*/ uint8_t ButtonTriangle : 1; +/* 8.0*/ uint8_t ButtonL1 : 1; +/* 8.1*/ uint8_t ButtonR1 : 1; +/* 8.2*/ uint8_t ButtonL2 : 1; +/* 8.3*/ uint8_t ButtonR2 : 1; +/* 8.4*/ uint8_t ButtonCreate : 1; +/* 8.5*/ uint8_t ButtonOptions : 1; +/* 8.6*/ uint8_t ButtonL3 : 1; +/* 8.7*/ uint8_t ButtonR3 : 1; +/* 9.0*/ uint8_t ButtonHome : 1; +/* 9.1*/ uint8_t ButtonPad : 1; +/* 9.2*/ uint8_t ButtonMute : 1; +/* 9.3*/ uint8_t UNK1 : 1; // appears unused +/* 9.4*/ uint8_t ButtonLeftFunction : 1; // DualSense Edge +/* 9.5*/ uint8_t ButtonRightFunction : 1; // DualSense Edge +/* 9.6*/ uint8_t ButtonLeftPaddle : 1; // DualSense Edge +/* 9.7*/ uint8_t ButtonRightPaddle : 1; // DualSense Edge +/*10 */ uint8_t UNK2; // appears unused +/*11 */ uint32_t UNK_COUNTER; // Linux driver calls this reserved, tools leak calls the 2 high bytes "random" +/*15 */ int16_t AngularVelocityX; +/*17 */ int16_t AngularVelocityZ; +/*19 */ int16_t AngularVelocityY; +/*21 */ int16_t AccelerometerX; +/*23 */ int16_t AccelerometerY; +/*25 */ int16_t AccelerometerZ; +/*27 */ uint32_t SensorTimestamp; +/*31 */ int8_t Temperature; // reserved2 in Linux driver +/*32 */ TouchData Touch; +/*41.0*/ uint8_t TriggerRightStopLocation: 4; // trigger stop can be a range from 0 to 9 (F/9.0 for Apple interface) +/*41.4*/ uint8_t TriggerRightStatus: 4; +/*42.0*/ uint8_t TriggerLeftStopLocation: 4; +/*42.4*/ uint8_t TriggerLeftStatus: 4; // 0 feedbackNoLoad + // 1 feedbackLoadApplied + // 0 weaponReady + // 1 weaponFiring + // 2 weaponFired + // 0 vibrationNotVibrating + // 1 vibrationIsVibrating +/*43 */ uint32_t HostTimestamp; // mirrors data from report write +/*47.0*/ uint8_t TriggerRightEffect: 4; // Active trigger effect, previously we thought this was status max +/*47.4*/ uint8_t TriggerLeftEffect: 4; // 0 for reset and all other effects + // 1 for feedback effect + // 2 for weapon effect + // 3 for vibration +/*48 */ uint32_t DeviceTimeStamp; +/*52.0*/ uint8_t PowerPercent : 4; // 0x00-0x0A +/*52.4*/ PowerState Power : 4; +/*53.0*/ uint8_t PluggedHeadphones : 1; +/*53.1*/ uint8_t PluggedMic : 1; +/*53.2*/ uint8_t MicMuted: 1; // Mic muted by powersave/mute command +/*53.3*/ uint8_t PluggedUsbData : 1; +/*53.4*/ uint8_t PluggedUsbPower : 1; // appears that this cannot be 1 if PluggedUsbData is 1 +/*53.5*/ uint8_t UsbPowerOnBT : 1; // appears this is only 1 if BT connected and USB powered +/*53.5*/ uint8_t DockDetect : 1; +/*53.5*/ uint8_t PluggedUnk : 1; +/*54.0*/ uint8_t PluggedExternalMic : 1; // Is external mic active (automatic in mic auto mode) +/*54.1*/ uint8_t HapticLowPassFilter : 1; // Is the Haptic Low-Pass-Filter active? +/*54.2*/ uint8_t PluggedUnk3 : 6; +/*55 */ uint8_t AesCmac[8]; +}; \ No newline at end of file