Skip to content

Commit a104339

Browse files
authored
feat(hid_service): Added BLE HID Service (#160)
* 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
1 parent ca6182f commit a104339

File tree

113 files changed

+1411
-199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+1411
-199
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ jobs:
5959
target: esp32s3
6060
- path: 'components/hid-rp/example'
6161
target: esp32s3
62+
- path: 'components/hid_service/example'
63+
target: esp32s3
6264
- path: 'components/i2c/example'
6365
target: esp32
6466
- path: 'components/joystick/example'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
idf_component_register(
2+
INCLUDE_DIRS "include"
3+
REQUIRES "esp-nimble-cpp" "base_component"
4+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# The following lines of boilerplate have to be in your project's CMakeLists
2+
# in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.5)
4+
5+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
6+
7+
8+
# add the component directories that we want to use
9+
set(EXTRA_COMPONENT_DIRS
10+
"../../../components/"
11+
)
12+
13+
set(
14+
COMPONENTS
15+
"main esptool_py ble_gatt_server hid_service hid-rp"
16+
CACHE STRING
17+
"List of components to include"
18+
)
19+
20+
project(hid_service_example)
21+
22+
set(CMAKE_CXX_STANDARD 20)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# BLE HID Service Example
2+
3+
This example shows how to use the `espp::HidService` class together with the
4+
`espp::BleGattServer` class to create and manage a BLE GATT server that provides
5+
an HID service. It uses the `hid-rp` component's `espp::GamepadReport<>` to
6+
define a HID gamepad report descriptor and generate input reports.
7+
8+
https://github.com/esp-cpp/espp/assets/213467/20ff49e3-42e2-4e69-9c91-d1926071f665
9+
10+
## How to use example
11+
12+
### Hardware Required
13+
14+
This example should run on any ESP32s3 development board as it requires no
15+
peripheral connections.
16+
17+
### Build and Flash
18+
19+
Build the project and flash it to the board, then run monitor tool to view serial output:
20+
21+
```
22+
idf.py -p PORT flash monitor
23+
```
24+
25+
(Replace PORT with the name of the serial port to use.)
26+
27+
(To exit the serial monitor, type ``Ctrl-]``.)
28+
29+
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
30+
31+
## Example Output
32+
33+
![CleanShot 2024-02-28 at 17 23 49](https://github.com/esp-cpp/espp/assets/213467/80199bb6-15e8-4396-af4a-d9a4b8b95ace)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
idf_component_register(SRC_DIRS "."
2+
INCLUDE_DIRS ".")
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#include <chrono>
2+
#include <vector>
3+
4+
#include "ble_gatt_server.hpp"
5+
#include "hid_service.hpp"
6+
7+
#include "hid-rp-gamepad.hpp"
8+
9+
using namespace std::chrono_literals;
10+
11+
extern "C" void app_main(void) {
12+
espp::Logger logger({.tag = "Hid Service Example", .level = espp::Logger::Verbosity::INFO});
13+
logger.info("Starting");
14+
15+
//! [hid service example]
16+
17+
// NOTE: esp-nimble-cpp already depends on nvs_flash and initializes
18+
// nvs_flash in the NimBLEDevice::init(), so we don't have to do that
19+
// to store bonding info
20+
21+
// create the GATT server
22+
espp::BleGattServer ble_gatt_server;
23+
std::string device_name = "ESP++ HID";
24+
ble_gatt_server.set_log_level(espp::Logger::Verbosity::INFO);
25+
ble_gatt_server.set_callbacks({
26+
.connect_callback = [&](NimBLEConnInfo &conn_info) { logger.info("Device connected"); },
27+
.disconnect_callback = [&](NimBLEConnInfo &conn_info) { logger.info("Device disconnected"); },
28+
.authentication_complete_callback =
29+
[&](NimBLEConnInfo &conn_info) { logger.info("Device authenticated"); },
30+
});
31+
ble_gatt_server.init(device_name);
32+
ble_gatt_server.set_advertise_on_disconnect(true);
33+
34+
// for HID we need to set some security
35+
bool bonding = true;
36+
bool mitm = false;
37+
bool secure_connections = true;
38+
ble_gatt_server.set_security(bonding, mitm, secure_connections);
39+
// and some i/o and key config
40+
ble_gatt_server.set_io_capabilities(BLE_HS_IO_NO_INPUT_OUTPUT);
41+
ble_gatt_server.set_init_key_distribution(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
42+
ble_gatt_server.set_resp_key_distribution(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
43+
44+
// let's create a HID service
45+
espp::HidService hid_service;
46+
hid_service.init(ble_gatt_server.server());
47+
48+
// configure it some
49+
uint8_t country_code = 0x00;
50+
uint8_t hid_info_flags = 0x01;
51+
hid_service.set_info(country_code, hid_info_flags);
52+
53+
static constexpr uint8_t report_id = 1;
54+
static constexpr size_t num_buttons = 15;
55+
static constexpr int joystick_min = 0;
56+
static constexpr int joystick_max = 65535;
57+
static constexpr int trigger_min = 0;
58+
static constexpr int trigger_max = 1024;
59+
60+
using Gamepad = espp::GamepadReport<num_buttons, joystick_min, joystick_max, trigger_min,
61+
trigger_max, report_id>;
62+
Gamepad gamepad_input_report;
63+
64+
// Generate the report descriptor for the gamepad
65+
auto descriptor = gamepad_input_report.get_descriptor();
66+
67+
logger.info("Report Descriptor:");
68+
logger.info(" Size: {}", descriptor.size());
69+
logger.info(" Data: {::#02x}", descriptor);
70+
71+
// set the report map (vector of bytes)
72+
hid_service.set_report_map(descriptor);
73+
74+
// use the HID service to make an input report characteristic
75+
auto input_report = hid_service.input_report(report_id);
76+
77+
// now that we've made the input characteristic, we can start the service
78+
hid_service.start();
79+
// starts the device info service and battery service, see
80+
// hid_service_example for more info
81+
ble_gatt_server.start_services();
82+
83+
// now start the gatt server
84+
ble_gatt_server.start();
85+
86+
// let's set some of the service data
87+
auto &battery_service = ble_gatt_server.battery_service();
88+
battery_service.set_battery_level(99);
89+
90+
auto &device_info_service = ble_gatt_server.device_info_service();
91+
uint8_t vendor_source = 0x02; // USB
92+
uint16_t vid = 0x045E; // Microsoft
93+
uint16_t pid = 0x02FD; // Xbox One Controller
94+
uint16_t product_version = 0x0100;
95+
device_info_service.set_pnp_id(vendor_source, vid, pid, product_version);
96+
device_info_service.set_manufacturer_name("ESP-CPP");
97+
device_info_service.set_model_number("esp-hid-01");
98+
device_info_service.set_serial_number("1234567890");
99+
device_info_service.set_software_version("1.0.0");
100+
device_info_service.set_firmware_version("1.0.0");
101+
device_info_service.set_hardware_version("1.0.0");
102+
103+
// now lets start advertising
104+
espp::BleGattServer::AdvertisingData adv_data = {
105+
.name = device_name,
106+
.appearance = 0x03C4, // Gamepad
107+
.services =
108+
{
109+
// these are the services that we want to advertise
110+
hid_service.uuid(), // hid service
111+
},
112+
.service_data =
113+
{
114+
// these are the service data that we want to advertise
115+
},
116+
};
117+
espp::BleGattServer::AdvertisingParameters adv_params = {};
118+
ble_gatt_server.start_advertising(adv_data, adv_params);
119+
120+
// now lets update the battery level and send an input report every second
121+
uint8_t battery_level = 99;
122+
// change the gamepad inputs every second
123+
int button_index = 1;
124+
while (true) {
125+
auto start = std::chrono::steady_clock::now();
126+
127+
// update the battery level
128+
battery_service.set_battery_level(battery_level);
129+
battery_level = (battery_level % 100) + 1;
130+
131+
// cycle through the possible d-pad states
132+
Gamepad::Hat hat = (Gamepad::Hat)button_index;
133+
// use the button index to set the position of the right joystick
134+
float angle = 2.0f * M_PI * button_index / num_buttons;
135+
136+
gamepad_input_report.reset();
137+
gamepad_input_report.set_hat(hat);
138+
gamepad_input_report.set_button(button_index, true);
139+
// joystick inputs are in the range [-1, 1] float
140+
gamepad_input_report.set_right_joystick(cos(angle), sin(angle));
141+
gamepad_input_report.set_left_joystick(sin(angle), cos(angle));
142+
// trigger inputs are in the range [0, 1] float
143+
gamepad_input_report.set_accelerator(std::abs(sin(angle)));
144+
gamepad_input_report.set_brake(std::abs(cos(angle)));
145+
146+
button_index = (button_index % num_buttons) + 1;
147+
148+
// send an input report
149+
auto report = gamepad_input_report.get_report();
150+
logger.debug("Sending report data ({}): {::#02x}", report.size(), report);
151+
input_report->notify(report);
152+
153+
// sleep
154+
std::this_thread::sleep_until(start + 1s);
155+
}
156+
//! [hid service example]
157+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Name, Type, SubType, Offset, Size
2+
nvs, data, nvs, 0x9000, 0x6000
3+
phy_init, data, phy, 0xf000, 0x1000
4+
factory, app, factory, 0x10000, 2M
5+
littlefs, data, spiffs, , 1M
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
CONFIG_IDF_TARGET="esp32s3"
2+
3+
# on the ESP32S3, which has native USB, we need to set the console so that the
4+
# CLI can be configured correctly:
5+
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
6+
7+
# Common ESP-related
8+
#
9+
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
10+
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
11+
12+
CONFIG_FREERTOS_HZ=1000
13+
14+
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
15+
16+
#
17+
# BT config
18+
#
19+
CONFIG_BT_ENABLED=y
20+
CONFIG_BT_BLUEDROID_ENABLED=n
21+
CONFIG_BT_NIMBLE_ENABLED=y
22+
CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y
23+
CONFIG_BT_NIMBLE_NVS_PERSIST=y
24+
CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=100
25+
CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
26+
27+
# NOTE: we can support extended advertising (longer advertisement packets) by
28+
# enabling the following:
29+
# CONFIG_BT_NIMBLE_EXT_ADV=y
30+
#
31+
# HOWEVER: we don't currently support this as the API to ble_gatt_server will
32+
# need to support this compile-time definition.
33+
34+
# Set the default Tx power level (P9 = +9dBm = the default)
35+
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y
36+
37+
# Support modem sleep (low power mode)
38+
# CONFIG_BT_CTRL_MODEM_SLEEP=y
39+
40+
# Set the ESP-NIMBLE-CPP Config
41+
CONFIG_NIMBLE_CPP_LOG_LEVEL_NONE=y

0 commit comments

Comments
 (0)