Skip to content

Commit

Permalink
feat(hid_service): Added BLE HID Service (#160)
Browse files Browse the repository at this point in the history
* feat(hid_service): Added BLE HID Service
* Added BLE HID service component and associated example
* Update and rebuild docs
* Update CI

* readme: update

* add video to readme as well

* update how the hat value is sent so it uses the button index - which helps test invalid values and shows the center value
  • Loading branch information
finger563 authored Feb 28, 2024
1 parent ca6182f commit a104339
Show file tree
Hide file tree
Showing 113 changed files with 1,411 additions and 199 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ jobs:
target: esp32s3
- path: 'components/hid-rp/example'
target: esp32s3
- path: 'components/hid_service/example'
target: esp32s3
- path: 'components/i2c/example'
target: esp32
- path: 'components/joystick/example'
Expand Down
4 changes: 4 additions & 0 deletions components/hid_service/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
idf_component_register(
INCLUDE_DIRS "include"
REQUIRES "esp-nimble-cpp" "base_component"
)
22 changes: 22 additions & 0 deletions components/hid_service/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 ble_gatt_server hid_service hid-rp"
CACHE STRING
"List of components to include"
)

project(hid_service_example)

set(CMAKE_CXX_STANDARD 20)
33 changes: 33 additions & 0 deletions components/hid_service/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# BLE HID Service Example

This example shows how to use the `espp::HidService` class together with the
`espp::BleGattServer` class to create and manage a BLE GATT server that provides
an HID service. It uses the `hid-rp` component's `espp::GamepadReport<>` to
define a HID gamepad report descriptor and generate input reports.

https://github.com/esp-cpp/espp/assets/213467/20ff49e3-42e2-4e69-9c91-d1926071f665

## 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 17 23 49](https://github.com/esp-cpp/espp/assets/213467/80199bb6-15e8-4396-af4a-d9a4b8b95ace)
2 changes: 2 additions & 0 deletions components/hid_service/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 ".")
157 changes: 157 additions & 0 deletions components/hid_service/example/main/hid_service_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#include <chrono>
#include <vector>

#include "ble_gatt_server.hpp"
#include "hid_service.hpp"

#include "hid-rp-gamepad.hpp"

using namespace std::chrono_literals;

extern "C" void app_main(void) {
espp::Logger logger({.tag = "Hid Service Example", .level = espp::Logger::Verbosity::INFO});
logger.info("Starting");

//! [hid service example]

// NOTE: esp-nimble-cpp already depends on nvs_flash and initializes
// nvs_flash in the NimBLEDevice::init(), so we don't have to do that
// to store bonding info

// create the GATT server
espp::BleGattServer ble_gatt_server;
std::string device_name = "ESP++ HID";
ble_gatt_server.set_log_level(espp::Logger::Verbosity::INFO);
ble_gatt_server.set_callbacks({
.connect_callback = [&](NimBLEConnInfo &conn_info) { logger.info("Device connected"); },
.disconnect_callback = [&](NimBLEConnInfo &conn_info) { logger.info("Device disconnected"); },
.authentication_complete_callback =
[&](NimBLEConnInfo &conn_info) { logger.info("Device authenticated"); },
});
ble_gatt_server.init(device_name);
ble_gatt_server.set_advertise_on_disconnect(true);

// for HID we need to set some security
bool bonding = true;
bool mitm = false;
bool secure_connections = true;
ble_gatt_server.set_security(bonding, mitm, secure_connections);
// and some i/o and key config
ble_gatt_server.set_io_capabilities(BLE_HS_IO_NO_INPUT_OUTPUT);
ble_gatt_server.set_init_key_distribution(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
ble_gatt_server.set_resp_key_distribution(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);

// let's create a HID service
espp::HidService hid_service;
hid_service.init(ble_gatt_server.server());

// configure it some
uint8_t country_code = 0x00;
uint8_t hid_info_flags = 0x01;
hid_service.set_info(country_code, hid_info_flags);

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);

// set the report map (vector of bytes)
hid_service.set_report_map(descriptor);

// use the HID service to make an input report characteristic
auto input_report = hid_service.input_report(report_id);

// now that we've made the input characteristic, we can start the service
hid_service.start();
// starts the device info service and battery service, see
// hid_service_example for more info
ble_gatt_server.start_services();

// now start the gatt server
ble_gatt_server.start();

// let's set some of the service data
auto &battery_service = ble_gatt_server.battery_service();
battery_service.set_battery_level(99);

auto &device_info_service = ble_gatt_server.device_info_service();
uint8_t vendor_source = 0x02; // USB
uint16_t vid = 0x045E; // Microsoft
uint16_t pid = 0x02FD; // Xbox One Controller
uint16_t product_version = 0x0100;
device_info_service.set_pnp_id(vendor_source, vid, pid, product_version);
device_info_service.set_manufacturer_name("ESP-CPP");
device_info_service.set_model_number("esp-hid-01");
device_info_service.set_serial_number("1234567890");
device_info_service.set_software_version("1.0.0");
device_info_service.set_firmware_version("1.0.0");
device_info_service.set_hardware_version("1.0.0");

// now lets start advertising
espp::BleGattServer::AdvertisingData adv_data = {
.name = device_name,
.appearance = 0x03C4, // Gamepad
.services =
{
// these are the services that we want to advertise
hid_service.uuid(), // hid service
},
.service_data =
{
// these are the service data that we want to advertise
},
};
espp::BleGattServer::AdvertisingParameters adv_params = {};
ble_gatt_server.start_advertising(adv_data, adv_params);

// now lets update the battery level and send an input report every second
uint8_t battery_level = 99;
// change the gamepad inputs every second
int button_index = 1;
while (true) {
auto start = std::chrono::steady_clock::now();

// update the battery level
battery_service.set_battery_level(battery_level);
battery_level = (battery_level % 100) + 1;

// cycle through the possible d-pad states
Gamepad::Hat hat = (Gamepad::Hat)button_index;
// use the button index to set the position of the right joystick
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.debug("Sending report data ({}): {::#02x}", report.size(), report);
input_report->notify(report);

// sleep
std::this_thread::sleep_until(start + 1s);
}
//! [hid service example]
}
5 changes: 5 additions & 0 deletions components/hid_service/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
41 changes: 41 additions & 0 deletions components/hid_service/example/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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

#
# BT config
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=100
CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192

# NOTE: we can support extended advertising (longer advertisement packets) by
# enabling the following:
# CONFIG_BT_NIMBLE_EXT_ADV=y
#
# HOWEVER: we don't currently support this as the API to ble_gatt_server will
# need to support this compile-time definition.

# Set the default Tx power level (P9 = +9dBm = the default)
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y

# Support modem sleep (low power mode)
# CONFIG_BT_CTRL_MODEM_SLEEP=y

# Set the ESP-NIMBLE-CPP Config
CONFIG_NIMBLE_CPP_LOG_LEVEL_NONE=y
Loading

0 comments on commit a104339

Please sign in to comment.