diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca26b1222..647c69de1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,6 +57,8 @@ jobs: target: esp32s3 - path: 'components/gt911/example' target: esp32s3 + - path: 'components/hid-rp/example' + target: esp32s3 - path: 'components/i2c/example' target: esp32 - path: 'components/joystick/example' diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 45e40fb85..9f12037f4 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -22,4 +22,4 @@ jobs: esp_idf_version: release/v5.2 # (Optional) cppcheck args - cppcheck_args: -i$GITHUB_WORKSPACE/lib -i$GITHUB_WORKSPACE/external -i$GITHUB_WORKSPACE/components/esp_littlefs -i$GITHUB_WORKSPACE/components/esp-nimble-cpp -i$GITHUB_WORKSPACE/components/lvgl -i$GITHUB_WORKSPACE/components/esp-dsp --force --enable=all --inline-suppr --inconclusive --platform=mips32 --suppressions-list=$GITHUB_WORKSPACE/suppressions.txt + cppcheck_args: -i$GITHUB_WORKSPACE/lib -i$GITHUB_WORKSPACE/external -i$GITHUB_WORKSPACE/components/esp_littlefs -i$GITHUB_WORKSPACE/components/esp-nimble-cpp -i$GITHUB_WORKSPACE/components/hid-rp/include/hid -i$GITHUB_WORKSPACE/components/lvgl -i$GITHUB_WORKSPACE/components/esp-dsp --force --enable=all --inline-suppr --inconclusive --platform=mips32 --suppressions-list=$GITHUB_WORKSPACE/suppressions.txt diff --git a/.gitmodules b/.gitmodules index 58ab79986..a64724cd2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "components/esp-nimble-cpp"] path = components/esp-nimble-cpp url = git@github.com:esp-cpp/esp-nimble-cpp +[submodule "external/hid-rp"] + path = external/hid-rp + url = https://github.com/intergatedcircuits/hid-rp diff --git a/components/hid-rp/CMakeLists.txt b/components/hid-rp/CMakeLists.txt new file mode 100644 index 000000000..fe5ef9077 --- /dev/null +++ b/components/hid-rp/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register( + INCLUDE_DIRS "include" "../../external/hid-rp/hid-rp" +) diff --git a/components/hid-rp/example/CMakeLists.txt b/components/hid-rp/example/CMakeLists.txt new file mode 100644 index 000000000..9083029fa --- /dev/null +++ b/components/hid-rp/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py hid-rp logger" + CACHE STRING + "List of components to include" + ) + +project(hid_rp_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/hid-rp/example/README.md b/components/hid-rp/example/README.md new file mode 100644 index 000000000..bda66891d --- /dev/null +++ b/components/hid-rp/example/README.md @@ -0,0 +1,33 @@ +# HID-RP Example + +This example shows how to use the +[`hid-rp`](https://github.com/intergatedcircuits/hid-rp) library which is +bundled into the hid-rp component within espp. + +It provides an example of a somewhat configurable HID Gamepad using the +`esppp::GamepadReport<>` template class. + +## How to use example + +### Hardware Required + +This example should run on any ESP32s3 development board as it requires no +peripheral connections. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +![CleanShot 2024-02-28 at 16 38 11](https://github.com/esp-cpp/espp/assets/213467/d86a5977-5db1-44bc-9df9-fcbb751392a5) diff --git a/components/hid-rp/example/main/CMakeLists.txt b/components/hid-rp/example/main/CMakeLists.txt new file mode 100644 index 000000000..a941e22ba --- /dev/null +++ b/components/hid-rp/example/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS ".") diff --git a/components/hid-rp/example/main/hid_rp_example.cpp b/components/hid-rp/example/main/hid_rp_example.cpp new file mode 100644 index 000000000..72ec3e047 --- /dev/null +++ b/components/hid-rp/example/main/hid_rp_example.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include "logger.hpp" + +#include "hid-rp-gamepad.hpp" +#include "hid-rp.hpp" + +using namespace std::chrono_literals; + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "Hid RP Example", .level = espp::Logger::Verbosity::INFO}); + logger.info("Starting"); + + //! [hid rp example] + static constexpr uint8_t report_id = 1; + static constexpr size_t num_buttons = 15; + static constexpr int joystick_min = 0; + static constexpr int joystick_max = 65535; + static constexpr int trigger_min = 0; + static constexpr int trigger_max = 1024; + + using Gamepad = espp::GamepadReport; + Gamepad gamepad_input_report; + + // Generate the report descriptor for the gamepad + auto descriptor = gamepad_input_report.get_descriptor(); + + logger.info("Report Descriptor:"); + logger.info(" Size: {}", descriptor.size()); + logger.info(" Data: {::#02x}", descriptor); + + Gamepad::Hat hat = Gamepad::Hat::UP_RIGHT; + int button_index = 5; + float angle = 2.0f * M_PI * button_index / num_buttons; + + gamepad_input_report.reset(); + gamepad_input_report.set_hat(hat); + gamepad_input_report.set_button(button_index, true); + // joystick inputs are in the range [-1, 1] float + gamepad_input_report.set_right_joystick(cos(angle), sin(angle)); + gamepad_input_report.set_left_joystick(sin(angle), cos(angle)); + // trigger inputs are in the range [0, 1] float + gamepad_input_report.set_accelerator(std::abs(sin(angle))); + gamepad_input_report.set_brake(std::abs(cos(angle))); + + button_index = (button_index % num_buttons) + 1; + + // send an input report + auto report = gamepad_input_report.get_report(); + logger.info("Input report:"); + logger.info(" Size: {}", report.size()); + logger.info(" Data: {::#02x}", report); + //! [hid rp example] +} diff --git a/components/hid-rp/example/partitions.csv b/components/hid-rp/example/partitions.csv new file mode 100644 index 000000000..e799571a8 --- /dev/null +++ b/components/hid-rp/example/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x6000 +phy_init, data, phy, 0xf000, 0x1000 +factory, app, factory, 0x10000, 2M +littlefs, data, spiffs, , 1M diff --git a/components/hid-rp/example/sdkconfig.defaults b/components/hid-rp/example/sdkconfig.defaults new file mode 100644 index 000000000..ae260f60e --- /dev/null +++ b/components/hid-rp/example/sdkconfig.defaults @@ -0,0 +1,14 @@ +CONFIG_IDF_TARGET="esp32s3" + +# on the ESP32S3, which has native USB, we need to set the console so that the +# CLI can be configured correctly: +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y + +# Common ESP-related +# +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +CONFIG_FREERTOS_HZ=1000 + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y diff --git a/components/hid-rp/include/hid-rp-gamepad.hpp b/components/hid-rp/include/hid-rp-gamepad.hpp new file mode 100644 index 000000000..4b413251a --- /dev/null +++ b/components/hid-rp/include/hid-rp-gamepad.hpp @@ -0,0 +1,222 @@ +#pragma once + +#include + +#include "hid-rp.hpp" + +namespace espp { + +/// HID Gamepad Report +/// This class implements a HID Gamepad with a configurable number of buttons, a +/// hat switch, 4 joystick axes and two trigger axes. It supports setting the +/// buttons, hat switch, joysticks, and triggers, as well as serializing the +/// input report and getting the report descriptor. +/// +/// \section hid_rp_ex1 HID-RP Example +/// \snippet hid_rp_example.cpp hid rp example +template +class GamepadReport : public hid::report::base { +public: + /// Possible Hat switch directions + enum class Hat { + CENTERED = 0x0f, ///< Centered, no direction pressed. + UP = 1, + UP_RIGHT, + RIGHT, + DOWN_RIGHT, + DOWN, + DOWN_LEFT, + LEFT, + UP_LEFT + }; + +protected: + static constexpr size_t button_count = BUTTON_COUNT; + static constexpr size_t num_button_padding = BUTTON_COUNT % 8 ? 8 - (BUTTON_COUNT % 8) : 0; + static constexpr uint16_t joystick_min = JOYSTICK_MIN; + static constexpr uint16_t joystick_max = JOYSTICK_MAX; + static constexpr uint16_t joystick_center = (joystick_min + joystick_max) / 2; + static constexpr uint16_t trigger_min = TRIGGER_MIN; + static constexpr uint16_t trigger_max = TRIGGER_MAX; + static constexpr uint16_t trigger_center = TRIGGER_MIN; + static constexpr size_t joystick_value_range = joystick_max - joystick_min; + static constexpr uint16_t joystick_range = joystick_value_range / 2; + static constexpr size_t trigger_range = trigger_max - trigger_min; + static constexpr size_t num_joystick_bits = num_bits(joystick_value_range); + static constexpr size_t num_joystick_padding = + num_joystick_bits % 8 ? 8 - (num_joystick_bits % 8) : 0; + static constexpr size_t num_trigger_bits = num_bits(trigger_range); + static constexpr size_t num_trigger_padding = + num_trigger_bits % 8 ? 8 - (num_trigger_bits % 8) : 0; + + std::array joystick_axes{0}; + std::array trigger_axes{0}; + std::uint8_t hat_switch{0}; + hid::report_bitset + buttons; + +public: + /// Reset the gamepad inputs + constexpr void reset() { + std::fill(joystick_axes.begin(), joystick_axes.end(), joystick_center); + std::fill(trigger_axes.begin(), trigger_axes.end(), trigger_center); + set_hat(Hat::CENTERED); + buttons.reset(); + } + + /// Set the left joystick X and Y axis values + /// @param lx left joystick x axis value, in the range [-1, 1] + /// @param ly left joystick y axis value, in the range [-1, 1] + constexpr void set_left_joystick(float lx, float ly) { + set_joystick_axis(0, lx); + set_joystick_axis(1, ly); + } + /// Set the right joystick X and Y axis values + /// @param rx right joystick x axis value, in the range [-1, 1] + /// @param ry right joystick y axis value, in the range [-1, 1] + constexpr void set_right_joystick(float rx, float ry) { + set_joystick_axis(2, rx); + set_joystick_axis(3, ry); + } + /// Set the brake trigger value + /// @param value brake trigger value, in the range [0, 1] + constexpr void set_brake(float value) { set_trigger_axis(0, value); } + /// Set the accelerator trigger value + /// @param value accelerator trigger value, in the range [0, 1] + constexpr void set_accelerator(float value) { set_trigger_axis(1, value); } + /// Set the hat switch (d-pad) value + /// @param hat Hat enum / direction to set + constexpr void set_hat(Hat hat) { set_hat_switch(uint8_t(hat)); } + /// Set the button value + /// \param button_index The button for which you want to set the value. + /// Should be between 1 and BUTTON_COUNT + /// \param value The true/false value you want to se the button to. + constexpr void set_button(int button_index, bool value) { + // buttons[button_index] = value; + buttons.set(hid::page::button(button_index), value); + } + + constexpr void set_joystick_axis(size_t index, std::uint16_t value) { + if (index < 4) { + joystick_axes[index] = std::clamp(value, joystick_min, joystick_max); + } + } + constexpr void set_joystick_axis(size_t index, float value) { + if (index < 4) { + joystick_axes[index] = + std::clamp(static_cast(value * joystick_range + joystick_center), + joystick_min, joystick_max); + } + } + constexpr void set_trigger_axis(size_t index, std::uint16_t value) { + if (index < 2) { + trigger_axes[index] = std::clamp(value, trigger_min, trigger_max); + } + } + constexpr void set_trigger_axis(size_t index, float value) { + if (index < 2) { + trigger_axes[index] = + std::clamp(static_cast(value * trigger_range + trigger_center), + trigger_min, trigger_max); + } + } + constexpr void set_hat_switch(std::uint8_t value) { hat_switch = (value & 0xf); } + + /// Get the input report as a vector of bytes + /// \return The input report as a vector of bytes. + constexpr auto get_report() { + // the first two bytes are the id and size, which we don't want... + size_t offset = 2; + auto report_data = this->data() + offset; + auto report_size = sizeof(*this) - offset; + return std::vector(report_data, report_data + report_size); + } + + /// Get the report descriptor as a vector of bytes + /// \return The report descriptor as a vector of bytes. + static constexpr auto get_descriptor() { + using namespace hid::page; + using namespace hid::rdf; + + // clang-format off + auto desc = descriptor( + usage_page(), + usage(generic_desktop::GAMEPAD), + collection::application( + report_id(REPORT_ID), + usage(generic_desktop::POINTER), + + // left joystick + collection::physical( + usage(generic_desktop::X), + logical_limits<1, 4>(JOYSTICK_MIN, JOYSTICK_MAX), + report_count(1), + report_size(num_joystick_bits), + input::absolute_variable(), + usage(generic_desktop::Y), + logical_limits<1, 4>(JOYSTICK_MIN, JOYSTICK_MAX), + report_count(1), + report_size(num_joystick_bits), + input::absolute_variable() + ), + + // right joystick + usage(generic_desktop::POINTER), + collection::physical( + usage(generic_desktop::Z), + logical_limits<1, 4>(JOYSTICK_MIN, JOYSTICK_MAX), + report_count(1), + report_size(num_joystick_bits), + input::absolute_variable(), + usage(generic_desktop::RZ), + logical_limits<1, 4>(JOYSTICK_MIN, JOYSTICK_MAX), + report_count(1), + report_size(num_joystick_bits), + input::absolute_variable() + ), + + // left trigger + usage_page(), + usage(simulation::BRAKE), + logical_limits<1, 2>(TRIGGER_MIN, TRIGGER_MAX), + report_size(num_trigger_bits), + report_count(1), + input::absolute_variable(), + input::padding(num_trigger_padding), + + // right trigger + usage_page(), + usage(simulation::ACCELERATOR), + logical_limits<1, 2>(TRIGGER_MIN, TRIGGER_MAX), + report_size(num_trigger_bits), + report_count(1), + input::absolute_variable(), + input::padding(num_trigger_padding), + + // hat switch + usage_page(), + usage(generic_desktop::HAT_SWITCH), + logical_limits<1, 1>(1, 8), + physical_limits<2, 2>(0, 315), + unit::unit_item<2>(0x0014), // system: english rotation, length: centimeter + report_size(4), + report_count(1), + input::absolute_variable(static_cast(main::field_flags::NULL_STATE)), + input::padding(4), + + // buttons + usage_page