Skip to content

Commit

Permalink
Feature: Add hid-rp submodule and component with example (#159)
Browse files Browse the repository at this point in the history
* feat(hid-rp): add hid-rp library
* Add hid-rp library as external library
* Add hid-rp component with additional generated hid pages and hid-rp-gamepad (espp::GamepadReport) class
* Add example showing use of espp::GamepadReport to set input report values, get the serialized input report data, and get the report descriptor.

* feat(range_mapper): update range mapper
* Allow range mapper to be default initialized
* Remove assert and instead exit early if bad config is provided
* Ensure default values allow for map/unmap to run without crashing

* readme: update

* update and rebuild docs, change GamepadReport to be class to be included in docs and update example

* update ci

* fix static analysis
  • Loading branch information
finger563 authored Feb 28, 2024
1 parent 9b0b77e commit ca6182f
Show file tree
Hide file tree
Showing 155 changed files with 4,832 additions and 214 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/static_analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@
[submodule "components/esp-nimble-cpp"]
path = components/esp-nimble-cpp
url = [email protected]:esp-cpp/esp-nimble-cpp
[submodule "external/hid-rp"]
path = external/hid-rp
url = https://github.com/intergatedcircuits/hid-rp
3 changes: 3 additions & 0 deletions components/hid-rp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
idf_component_register(
INCLUDE_DIRS "include" "../../external/hid-rp/hid-rp"
)
22 changes: 22 additions & 0 deletions components/hid-rp/example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 33 additions & 0 deletions components/hid-rp/example/README.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions components/hid-rp/example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
idf_component_register(SRC_DIRS "."
INCLUDE_DIRS ".")
56 changes: 56 additions & 0 deletions components/hid-rp/example/main/hid_rp_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include <chrono>
#include <vector>

#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<num_buttons, joystick_min, joystick_max, trigger_min,
trigger_max, report_id>;
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]
}
5 changes: 5 additions & 0 deletions components/hid-rp/example/partitions.csv
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions components/hid-rp/example/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -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
222 changes: 222 additions & 0 deletions components/hid-rp/include/hid-rp-gamepad.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#pragma once

#include <algorithm>

#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 <size_t BUTTON_COUNT, uint16_t JOYSTICK_MIN = 0, uint16_t JOYSTICK_MAX = 65535,
uint16_t TRIGGER_MIN = 0, uint16_t TRIGGER_MAX = 1023, uint8_t REPORT_ID = 0>
class GamepadReport : public hid::report::base<hid::report::type::INPUT, REPORT_ID> {
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<std::uint16_t, 4> joystick_axes{0};
std::array<std::uint16_t, 2> trigger_axes{0};
std::uint8_t hat_switch{0};
hid::report_bitset<hid::page::button, hid::page::button(1), hid::page::button(BUTTON_COUNT)>
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<std::uint16_t>(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<std::uint16_t>(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<uint8_t>(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<generic_desktop>(),
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<simulation>(),
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<simulation>(),
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<generic_desktop>(),
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>(main::field_flags::NULL_STATE)),
input::padding(4),

// buttons
usage_page<button>(),
usage_limits(button(1), button(BUTTON_COUNT)),
logical_limits<1, 1>(0, 1),
report_size(1),
report_count(BUTTON_COUNT),
input::absolute_variable(),
input::padding(num_button_padding)
)
);
// clang-format on
return std::vector<uint8_t>(desc.begin(), desc.end());
}
};
} // namespace espp
Loading

0 comments on commit ca6182f

Please sign in to comment.