Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
93e1cf6
feat(wdi): protocol core for the Wheelchair Digital Interface
finger563 Sep 11, 2026
bf58ca2
feat(wdi): device role core (WdiDevice) with keepalive state machine
finger563 Sep 11, 2026
282488d
feat(wdi): build the HID report descriptor with hid-rp
finger563 Sep 11, 2026
dc39a60
feat(wdi): bundle the protocol core into the host C++/Python library
finger563 Sep 11, 2026
1c9030a
feat(wdi): BLE peripheral (device role) + example
finger563 Sep 11, 2026
ea0d73f
feat(wdi): USB HID device (device role) + example
finger563 Sep 12, 2026
222a11c
feat(usb_host): espp::UsbHost (USB Host HID) component + example
finger563 Sep 12, 2026
50d5e31
Merge branch 'feat/usb-host' into feat/wdi-host
finger563 Sep 12, 2026
d72b99c
feat(wdi): host role (WdiHost) over USB Host HID + BLE central
finger563 Sep 12, 2026
3386006
fix(usb_host): address review feedback (teardown race, dangling span,…
finger563 Sep 12, 2026
2b92ba3
fix(wdi): field-accurate HID descriptor, BLE HID-over-GATT, review fixes
finger563 Sep 12, 2026
4221c04
Merge branch 'feat/wdi' into feat/wdi-host
finger563 Sep 12, 2026
391580c
fix(wdi): host-role review follow-ups (thread-safety, deps, docs)
finger563 Sep 12, 2026
95089ca
docs(wdi): correct the descriptor/BLE note in the README layering sec…
finger563 Sep 12, 2026
77224a1
fix(wdi): null-check createCharacteristic() results in WdiBlePeripheral
finger563 Sep 12, 2026
a0ab3c0
fix(usb_host): use https for the repository URL in the manifest
finger563 Sep 12, 2026
f2711d5
Merge branches 'feat/wdi' and 'feat/usb-host' into feat/wdi-host
finger563 Sep 12, 2026
5ad016d
fix(wdi): WdiBleCentral re-entrancy + subscribe error handling
finger563 Sep 12, 2026
586758d
fix(wdi): make WdiBlePeripheral::make_device_config const (functionCo…
finger563 Sep 12, 2026
4d5d0c2
Merge branch 'feat/wdi' into feat/wdi-host
finger563 Sep 12, 2026
8958ec3
fix(wdi): clear host-role static-analysis findings
finger563 Sep 12, 2026
2c20290
fix(usb_host): guard HidDevice ops on disconnect; refine error mapping
finger563 Sep 12, 2026
7f9ab0a
fix(wdi): clamp velocity tenths, py::bytes parse bindings, test comment
finger563 Sep 12, 2026
1f1efb4
Merge branch 'feat/wdi' into feat/wdi-host
finger563 Sep 12, 2026
bb22d9a
Merge remote-tracking branch 'origin/feat/usb-host' into feat/wdi-host
finger563 Sep 12, 2026
0583272
fix(usb_host): run driver ops + user callbacks on a dispatch task (no…
finger563 Sep 12, 2026
edfe2fa
fix(wdi): WdiBlePeripheral::start() no longer calls the deprecated Ni…
finger563 Sep 12, 2026
b26b201
Merge remote-tracking branch 'origin/feat/usb-host' into feat/wdi-host
finger563 Sep 12, 2026
ce6904c
Merge remote-tracking branch 'origin/feat/wdi' into feat/wdi-host
finger563 Sep 12, 2026
b74ab0b
docs(usb_host): document the threading model (dispatch task, callback…
finger563 Sep 12, 2026
a4424ce
fix(wdi): host-role self-review — re-entrancy, BLE client lifetime, U…
finger563 Sep 12, 2026
e6030b2
fix(wdi): suppress functionConst on the host wrappers' poll()/send_fe…
finger563 Sep 12, 2026
2cbfd1f
fix(wdi): include <algorithm> in wdi_usb_host.hpp (std::equal was tra…
finger563 Sep 12, 2026
5a63dfd
fix(usb_host): address review round 3 (teardown safety, UTF-8, queue …
finger563 Sep 12, 2026
edac021
fix(wdi): address review round 3 (scope wording, PyBytes check, CMake…
finger563 Sep 12, 2026
bc9c219
Merge remote-tracking branches 'origin/feat/usb-host' and 'origin/fea…
finger563 Sep 12, 2026
048d3da
chore(wdi): host role lands here — un-mark the "follow-up" wording; m…
finger563 Sep 12, 2026
101dfdd
Merge origin/main (squash-merged #788/#789/#790) into feat/wdi-host
finger563 Sep 12, 2026
c57fb55
fix(usb_host): address review round 4 (no const_cast, hard queue boun…
finger563 Sep 12, 2026
3fe73ad
Merge remote-tracking branch 'origin/feat/usb-host' into feat/wdi-host
finger563 Sep 12, 2026
2dba38a
ci(usb_host): publish the usb_host component to the component registry
finger563 Sep 12, 2026
4530d6d
ci(wdi): publish the wdi component to the component registry
finger563 Sep 12, 2026
d833ddf
fix(usb_host): address review round 5 (enqueue/teardown race, buffer …
finger563 Sep 12, 2026
6c084a6
Merge remote-tracking branch 'origin/feat/usb-host' into feat/wdi-host
finger563 Sep 12, 2026
21967a0
Merge remote-tracking branch 'origin/main' into feat/wdi-host
finger563 Sep 12, 2026
28eb52d
fix(wdi): host-testable WDI descriptor detection that skips HID long …
finger563 Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ jobs:
target: esp32s3
- path: 'components/vl53l/example'
target: esp32s3
- path: 'components/wdi/ble_example'
target: esp32s3
- path: 'components/wdi/ble_central_example'
target: esp32s3
- path: 'components/wdi/usb_example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
- path: 'components/wdi/usb_host_example'
target: esp32s3
- path: 'components/wifi/example'
target: esp32
- path: 'components/wrover-kit/example'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ jobs:
components/usb_host
components/utils
components/vl53l
components/wdi
components/wifi
components/wrover-kit
components/ws-s3-geek
Expand Down
70 changes: 67 additions & 3 deletions components/wdi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ its own `REQUIRES` (the examples show this):
| `wdi_hid.hpp` | HID report descriptor | `hid-rp` |
| `wdi_usb.hpp` | USB device (`WdiUsbPeripheral`) | `usb_device`, `hid-rp` |
| `wdi_ble.hpp` | BLE peripheral (`WdiBlePeripheral`) | `esp-nimble-cpp` (+ `hid-rp`, for the Report Map) |
| `wdi_usb_host.hpp` | USB host (`WdiUsbHost`) — *host role, follow-up PR* | `usb_host`, `hid-rp` |
| `wdi_ble_central.hpp` | BLE central (`WdiBleCentral`) — *host role, follow-up PR* | `esp-nimble-cpp` |
| `wdi_usb_host.hpp` | USB host (`WdiUsbHost`) | `usb_host`, `hid-rp` |
| `wdi_ble_central.hpp` | BLE central (`WdiBleCentral`) | `esp-nimble-cpp` |

This keeps a project that only wants the protocol core (or a single transport)
from pulling in the BLE and USB stacks it does not use.
Expand Down Expand Up @@ -159,6 +159,63 @@ See `usb_example/` for a full runnable example (esp32s3). Because the native USB
port is given to TinyUSB, the console runs on UART0 (with USB-Serial-JTAG as an
early-boot secondary).

## Host role (`espp::WdiHost`)

`WdiHost` (in `wdi_host.hpp`) is the wheelchair side, transport-agnostic and the
mirror image of `WdiDevice`: give it a `send` callback (put an OUTPUT report on
the wire) and feed it the app's INPUT reports via `handle_input()`. It owns the
keepalive **watchdog** — call `poll()` periodically and it fires
`on_disconnected` (the caller must drive-disable) once the app has gone quiet for
3 keepalive windows. Request-Feedback triggers a Feedback reply; Keepalive
triggers a Keepalive-Response (the host's UUID). Time is read through a
caller-supplied clock so it is fully host-testable.

```cpp
espp::WdiHost::Config cfg;
cfg.host_uuid = espp::WdiHost::make_host_uuid(0x000B /*LUCI*/, random14);
cfg.on_control = [](const espp::wdi::ControlReport &c) { /* drive the chair */ };
cfg.on_disconnected = [] { /* DRIVE DISABLE */ };
cfg.send = [&](espp::wdi::ReportId id, std::span<const uint8_t> body) {
return hid_device.send_output_report(static_cast<uint8_t>(id), body, ec); // USB HID Output
};
espp::WdiHost host(cfg);
host.set_feedback(fb); // status the chair reports back
// transport RX (HID IN / BLE notify): host.handle_input(id, bytes);
host.poll(); // watchdog (drive-disable on timeout)
```

### USB HID host (`espp::WdiUsbHost`)

`wdi_usb_host.hpp` wraps `WdiHost` with an `espp::UsbHost` (USB Host HID): it
enumerates an attached WDI HID device (an accessory running `WdiUsbPeripheral`),
routes its Input reports into `handle_input()`, and sends Feedback /
Keepalive-Response as HID Output reports. See `usb_host_example/` (esp32s3). Built
with the component manager on (the USB host stack — `usb` + `usb_host_hid` — comes
from the registry; see the `usb_host` component).

```cpp
espp::WdiUsbHost host({.on_control = ..., .on_disconnected = ..., .host_uuid = uuid});
std::error_code ec;
host.initialize(ec);
// loop: host.set_feedback(fb); host.poll(); // poll() drive-disables on timeout
```

### BLE central (`espp::WdiBleCentral`)

`wdi_ble_central.hpp` wraps `WdiHost` with a NimBLE central: after
`NimBLEDevice::init()`, `scan_and_connect()` finds a WDI peripheral, subscribes to
the Control / Request-Feedback / Keepalive notify characteristics
(→ `handle_input()`), and writes Feedback / Keepalive-Response. See
`ble_central_example/` (esp32s3).

```cpp
NimBLEDevice::init("espp WDI host");
espp::WdiBleCentral host({.on_control = ..., .on_disconnected = ..., .host_uuid = uuid});
std::error_code ec;
host.scan_and_connect(5000, ec);
// loop: host.set_feedback(fb); host.poll();
```

## Status

- [x] Protocol core + host tests (`test/wdi_protocol_host_test.cpp`)
Expand All @@ -168,7 +225,12 @@ early-boot secondary).
GATT service + characteristics on `ble_gatt_server`, with a `ble_example`
- [x] Device role — **USB HID device** (`WdiUsbPeripheral`, `wdi_usb.hpp`): the WDI
HID report descriptor on `espp::UsbDevice`, with a `usb_example`
- [ ] Host role — USB Host HID + BLE central
- [x] Host role core — `WdiHost`, keepalive watchdog, host-tested
(`test/wdi_host_host_test.cpp`)
- [x] Host role — **USB Host HID** (`WdiUsbHost`, `wdi_usb_host.hpp`): the WDI host
on `espp::UsbHost`, with a `usb_host_example`
- [x] Host role — **BLE central** (`WdiBleCentral`, `wdi_ble_central.hpp`): a NimBLE
central connecting to a WDI peripheral, with a `ble_central_example`

## Testing

Expand All @@ -180,6 +242,8 @@ c++ -std=c++20 -Wall -Wextra -Werror -I components/wdi/include \
components/wdi/test/wdi_protocol_host_test.cpp -o wdi_test && ./wdi_test
c++ -std=c++20 -Wall -Wextra -Werror -I components/wdi/include \
components/wdi/test/wdi_device_host_test.cpp -o wdi_dev_test && ./wdi_dev_test
c++ -std=c++20 -Wall -Wextra -Werror -I components/wdi/include \
components/wdi/test/wdi_host_host_test.cpp -o wdi_host_test && ./wdi_host_test
```

The hid-rp report descriptor also builds on a host (hid-rp is header-only; add it
Expand Down
22 changes: 22 additions & 0 deletions components/wdi/ble_central_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.20)

set(ENV{IDF_COMPONENT_MANAGER} "0")
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 wdi esp-nimble-cpp hid-rp"
CACHE STRING
"List of components to include"
)

project(wdi_ble_central_example)

set(CMAKE_CXX_STANDARD 20)
1 change: 1 addition & 0 deletions components/wdi/ble_central_example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
idf_component_register(SRC_DIRS "." INCLUDE_DIRS "." REQUIRES wdi esp-nimble-cpp hid-rp)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <chrono>
#include <thread>

#include "esp_random.h"

#include "NimBLEDevice.h"

#include "logger.hpp"
#include "wdi_ble_central.hpp"

using namespace std::chrono_literals;

// WDI (Wheelchair Digital Interface) BLE **central** example: act as the
// wheelchair (BLE central) and talk to a WDI peripheral accessory (for example
// another ESP running the wdi ble_example). The central scans for the WDI
// service, connects, receives Control reports, replies to Keepalive /
// Request-Feedback, and runs the keepalive watchdog that drive-disables if the
// accessory goes quiet.
//
// SAFETY: this only *emulates* the wheelchair side for development. Do not wire a
// real chair's motion to on_control without the manufacturer's guidance.
extern "C" void app_main(void) {
espp::Logger logger({.tag = "WDI BLE Host", .level = espp::Logger::Verbosity::INFO});
logger.info("Starting WDI BLE central example");

NimBLEDevice::init("espp WDI host");

// Build this host's identity (manufacturer id + 14 random bytes).
uint8_t rnd[14];
esp_fill_random(rnd, sizeof(rnd));
auto uuid = espp::WdiHost::make_host_uuid(
static_cast<uint16_t>(espp::wdi::ManufacturerId::LuciMobility), rnd);

espp::WdiBleCentral host({
.on_control =
[&](const espp::wdi::ControlReport &c) {
logger.info("control: x={} y={} drive_enable={}", c.x, c.y,
c.has(espp::wdi::ControlBit::DriveEnable));
},
.on_connected = [&] { logger.info("WDI accessory connected"); },
.on_disconnected = [&] { logger.warn("WDI accessory disconnected -> DRIVE DISABLE"); },
.host_uuid = uuid,
.log_level = espp::Logger::Verbosity::INFO,
});

// Report a plausible chair status back to the accessory.
espp::wdi::FeedbackReport fb;
fb.set(espp::wdi::FeedbackBit::DriveEnabled);
fb.speed = 3;
fb.profile = 1;
host.set_feedback(fb);

// Scan + connect (retrying until a WDI peripheral is found), then run the
// keepalive watchdog. If the link drops, scan again.
while (true) {
if (!host.is_connected()) {
std::error_code ec;
logger.info("scanning for a WDI peripheral...");
if (!host.scan_and_connect(5000, ec)) {
logger.warn("no peripheral yet ({}); retrying", ec.message());
std::this_thread::sleep_for(1s);
continue;
}
}
host.poll();
std::this_thread::sleep_for(50ms);
}
}
4 changes: 4 additions & 0 deletions components/wdi/ble_central_example/partitions.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x6000
phy_init, data, phy, 0xf000, 0x1000
factory, app, factory, 0x10000, 2M
13 changes: 13 additions & 0 deletions components/wdi/ble_central_example/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_FREERTOS_HZ=1000
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
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_HOST_TASK_STACK_SIZE=8192
CONFIG_NIMBLE_CPP_LOG_LEVEL_NONE=y
2 changes: 2 additions & 0 deletions components/wdi/ble_central_example/sdkconfig.defaults.esp32s3
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
4 changes: 3 additions & 1 deletion components/wdi/idf_component.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
## IDF Component Manager Manifest File
license: "MIT"
description: "Wheelchair Digital Interface (WDI / Open-Mobility-Hub Wheelchair HID): report protocol + device role over USB and BLE (host role in a follow-up)"
description: "Wheelchair Digital Interface (WDI / Open-Mobility-Hub Wheelchair HID): report protocol + device and host roles over USB and BLE"
url: "https://github.com/esp-cpp/espp/tree/main/components/wdi"
repository: "https://github.com/esp-cpp/espp.git"
maintainers:
- William Emfinger <waemfinger@gmail.com>
documentation: "https://esp-cpp.github.io/espp/wdi/wdi.html"
examples:
- path: ble_example
- path: ble_central_example
- path: usb_example
- path: usb_host_example
tags:
- cpp
- Component
Expand Down
Loading
Loading