Conversation
New `wdi` component implementing the Open-Mobility-Hub Wheelchair HID spec (v3.2). This first commit is the host-testable, ESP-free protocol core shared by every transport (USB / BLE) and role (device / host): - The five HID reports (Control 0x01, Feedback 0x02, Request Feedback 0x03, Keepalive 0x04, Keepalive Response 0x05) as structs with serialize()/parse(). - ControlBit / FeedbackBit bitfield enums, ManufacturerId, keepalive timing constants, and the shared HID report descriptor (usage page 0xFF00) built with std::to_array so its length is deduced. - Little-endian report fields; the 128-bit Host UUID kept big-endian per spec. - Host unit test covering report round-trips, nibble packing (speed/profile, velocity), release/zero reports, wrong-size rejection, the descriptor shape, and the big-endian manufacturer id. Builds + passes with plain c++ -std=c++20 -Wall -Wextra -Werror. Component skeleton (CMakeLists / idf_component.yml / README) registers the include dir; the device role (USB HID + BLE peripheral) and host role follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
WdiDevice is the app / accessory side of the WDI interface. Transport-agnostic: a `send` callback puts a report on the wire (USB HID Input report or BLE notify), and handle_output() consumes the host's reports (Feedback 0x02, Keepalive Response 0x05). It owns the keepalive state machine: - send_control() / send_release() / request_feedback() / send_keepalive() build and transmit the reports; Control / Request-Feedback / Keepalive all reset the keepalive timer (per spec), and a failed send does NOT reset it. - poll() emits a Keepalive when the interval (~233 ms) has elapsed since the last transmit; ms_until_keepalive() reports the remaining time. No internal timer -- call poll() from an espp::Timer / Task on device (kept out of the core so it is host-testable). Time comes from a caller-supplied clock (default steady ms). - handle_output() parses + routes Feedback / Keepalive Response, stores the host UUID + last feedback, and ignores wrong-direction / malformed reports. Depends only on the C++20 stdlib + the protocol core, so it is unit-tested on a host (fake clock + mock send): control-resets-timer, keepalive timing edges, request-feedback reset, failed-send behavior, and feedback/UUID routing. Passes under -Wall -Wextra -Werror. USB / BLE transport bindings + examples follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Replace the hand-rolled report-descriptor bytes with an idiomatic hid-rp descriptor (new wdi_hid.hpp): a custom vendor usage page (0xFF00) + one report item per report id, using the same raw-vendor-usage idiom as the espp switch-pro descriptor. Removed the hand-written kReportDescriptor from the dependency-free core (detail/wdi_protocol.hpp) — only the USB HID transport needs a descriptor (BLE carries the same reports as GATT characteristics), so wdi_hid.hpp (which pulls in hid-rp) is separate and the core / BLE path stays hid-rp-free. hid-rp is header-only + stdlib-only, so wdi_hid.hpp is still host-testable (add hid-rp as -isystem so its non-Werror-clean third-party headers don't break -Werror). The new host test asserts the built descriptor's structure (vendor page, 5 report ids, per-report counts 18/19/1/1/16, 3 Input + 2 Output items); it comes out to the same 55 bytes as the hand-verified descriptor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Make the WDI core available off-device for CI/interop testing and for building a
WDI host (the wheelchair side) on a PC to test a real peripheral against:
- C++ host library: add components/wdi/include to ESPP_INCLUDES (header-only;
no sources). WdiDevice + the protocol structs are now on the host lib's include
path. hid-rp (for wdi_hid.hpp) is already an ESPP include.
- Python: hand-written pybind11 bindings (lib/python_bindings/wdi_bindings.cpp,
registered via py_init_wdi in module.cpp, added to ESPP_PYTHON_SOURCES) exposing
espp.wdi.{ReportId, ControlBit, FeedbackBit, ManufacturerId, ControlReport,
FeedbackReport, HostUuid} with serialize()/parse() -- enough to build/test a WDI
host from Python. Kept out of the generated bindings (like dispatcher_bindings).
Compiles clean against pybind11.
- python/wdi_test.py: Python mirror of the C++ host test (report round-trips,
nibble packing, host-uuid big-endian, and a parse-Control/build-Feedback
host-side round-trip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
WdiBlePeripheral (wdi_ble.hpp) wraps the transport-agnostic WdiDevice with the WDI
GATT service on esp-nimble-cpp / espp::BleGattServer:
- Service 10A50001-…, characteristics 10A5000{6..A}. Control / Request-Feedback /
Keepalive are READ|NOTIFY (device→central); Feedback / Keepalive-Response are
READ|WRITE_NR (central→device).
- WdiDevice's send is wired to characteristic notify(); the write characteristics'
NimBLECharacteristicCallbacks route received bytes into WdiDevice.handle_output().
App API forwards send_control / request_feedback / poll / host_uuid.
Adds ble_example/ (esp32s3): brings up BleGattServer, installs + advertises the WDI
service, and sweeps a demo joystick with drive-enable while polling keepalives.
Builds clean on IDF v6.1 (67% flash free).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
WdiUsbPeripheral (wdi_usb.hpp) wraps the transport-agnostic WdiDevice with an espp::UsbDevice HID interface using the WDI report descriptor (wdi_hid.hpp): - Control / Request-Feedback / Keepalive are HID Input reports (device->host via write_hid_report()); Feedback / Keepalive-Response are HID Output reports (host->device) delivered through HidFunction::on_receive (has_out_endpoint) -- the usb_device HID-OUT support merged with switch_pro (#787). The report id is byte 0 of the received span; the rest is routed to WdiDevice.handle_output(). - App API forwards send_control / request_feedback / poll / host_uuid. Adds usb_example/ (esp32s3): enumerates as a WDI HID device and sweeps a demo joystick with drive-enable while polling keepalives. Console on UART0 (native USB goes to TinyUSB). Builds clean on IDF v6.1 (58% flash free). feat/wdi is rebased on main (which now has the usb_device HID-OUT extension). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
🟡 Changes recommended
Critical protocol, safety, transport, dependency, and integration issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds the WDI v3.2 device-role component for USB HID and BLE, including protocol logic, peripherals, bindings, examples, and tests.
Changes:
- Implements report serialization, parsing, HID descriptors, and device state management.
- Adds BLE and USB peripheral adapters with ESP32 examples.
- Adds C++/Python bindings and host-side tests.
File summaries
| File | Description and final review notes |
|---|---|
python/wdi_test.py |
Python binding tests. moderate (1 vote): not included in automated Python testing. |
lib/python_bindings/wdi_bindings.cpp |
WDI Python API bindings. |
lib/python_bindings/module.cpp |
Registers WDI bindings. |
lib/espp.cmake |
Host include and binding integration. |
components/wdi/usb_example/sdkconfig.defaults |
USB/TinyUSB configuration. |
components/wdi/usb_example/main/wdi_usb_example.cpp |
USB example. critical (2 votes): starts with forward motion enabled; default should send a release and make motion opt-in. |
components/wdi/usb_example/main/CMakeLists.txt |
USB example dependencies. |
components/wdi/usb_example/CMakeLists.txt |
USB example project configuration. |
components/wdi/test/wdi_protocol_host_test.cpp |
Protocol host tests. |
components/wdi/test/wdi_hid_host_test.cpp |
HID descriptor tests. |
components/wdi/test/wdi_device_host_test.cpp |
Device-state host tests. |
components/wdi/README.md |
Component documentation and safety guidance. |
components/wdi/include/wdi.hpp |
Device state machine. critical (1 vote): enforce the 100 ms release interval; synchronize cross-task state, callbacks, and keepalive timestamps; persist and validate host identity before transmitting. |
components/wdi/include/wdi_usb.hpp |
USB peripheral wrapper. |
components/wdi/include/wdi_hid.hpp |
HID descriptor. critical (2 votes): expose required per-report fields and usages instead of opaque byte arrays. |
components/wdi/include/wdi_ble.hpp |
BLE peripheral. critical (1 vote): persist and validate host identity, stop reports on mismatch, and delay advertising. critical (1 vote): add mandatory characteristics 0x02–0x05 and the report map. |
components/wdi/include/detail/wdi_protocol.hpp |
Protocol reports and serialization. nit (2 votes): correct the Modifier/vendor1 comment. moderate (1 vote): reject or represent reserved velocity fractions as unknown. |
components/wdi/idf_component.yml |
Registry metadata. critical (3 votes): declare public NimBLE, HID-RP, and USB dependencies. moderate (1 vote): add examples to build CI and the component to the upload allowlist. moderate (1 vote): add the component to the explicit upload list. nit (1 vote): accurately describe the implemented device role. nit (2 votes) and moderate (1 vote): add documentation, Doxygen, build-matrix, and publishing entries. |
components/wdi/CMakeLists.txt |
Component build metadata. critical (3 votes): declare esp-nimble-cpp, hid-rp, and usb_device as public dependencies. |
components/wdi/ble_example/sdkconfig.defaults.esp32s3 |
ESP32-S3 BLE configuration. |
components/wdi/ble_example/sdkconfig.defaults |
BLE configuration. |
components/wdi/ble_example/partitions.csv |
BLE partition layout. |
components/wdi/ble_example/main/wdi_ble_example.cpp |
BLE example. critical (2 votes): starts with forward motion enabled; default should send a release and make motion opt-in. |
components/wdi/ble_example/main/CMakeLists.txt |
BLE example dependencies. |
components/wdi/ble_example/CMakeLists.txt |
BLE example project configuration. moderate (1 vote): add both examples to the build matrix. |
Review details
Suppressed comments (7)
components/wdi/ble_example/CMakeLists.txt:22
- Neither newly added example is present in the build matrix (
build.yml), so the claimed BLE and USB example builds are not exercised by CI and compile/configuration regressions can merge unnoticed. Add matrix entries for bothcomponents/wdi/ble_exampleandcomponents/wdi/usb_example(the repository adds one entry per example project).
project(wdi_ble_example)
set(CMAKE_CXX_STANDARD 20)
components/wdi/idf_component.yml:11
- The two new example paths are not present in
.github/workflows/build.yml, andcomponents/wdiis not present in.github/workflows/upload_components.yml. Consequently neither transport example is compiled by the example matrix, and the component is excluded from the registry dry-run/release allowlist despite being advertised in this manifest. Add both examples to build CI and addcomponents/wdito the upload list.
examples:
- path: ble_example
- path: usb_example
components/wdi/idf_component.yml:11
upload_components.ymluses an explicit allowlist for both PR dry-run validation and release publication, but nocomponents/wdientry is added in this PR. As a result, this new manifest will never be validated or uploaded by the repository's component publishing workflow; add the component to that list.
examples:
- path: ble_example
- path: usb_example
components/wdi/idf_component.yml:3
- The manifest description says this component provides host roles over USB and BLE, while the PR explicitly leaves USB Host HID and BLE central support for a follow-up and the README marks that role unimplemented. Published metadata should describe the currently implemented protocol core and device role rather than overstate the available API.
description: "Wheelchair Digital Interface (WDI / Open-Mobility-Hub Wheelchair HID): report protocol + device and host roles over USB and BLE"
components/wdi/idf_component.yml:11
- This new component is not referenced by
.github/workflows/build.yml,.github/workflows/upload_components.yml, ordoc/Doxyfile, and nodoc/enWDI page exists. Consequently the advertised documentation URL is dead, the examples are absent from the normal build matrix, and the component is absent from the explicit publishing allowlist. Add the required documentation, Doxygen, build, and upload entries before publishing the component.
documentation: "https://esp-cpp.github.io/espp/wdi/wdi.html"
examples:
- path: ble_example
- path: usb_example
components/wdi/include/detail/wdi_protocol.hpp:219
- The velocity fraction reserves nibble values 0xA–0xF, but this parser accepts them and
velocity_mph()then reports impossible values such as 15.5 mph for 0xF. Reject reserved fractions when parsing/serializing, or represent them as unknown so the helper cannot expose an invalid speed.
r.speed = static_cast<uint8_t>((p[12] >> 4) & 0x0F);
r.profile = static_cast<uint8_t>(p[12] & 0x0F);
r.velocity_whole = static_cast<uint8_t>((p[13] >> 4) & 0x0F);
r.velocity_tenths = static_cast<uint8_t>(p[13] & 0x0F);
python/wdi_test.py:5
- This new binding test is not part of the automated Python test command:
pyproject.tomlrunsrtps_bindings_smoke.pyanddispatcher_test.py, but neverwdi_test.py. Add this script to the wheel/CI smoke test so the newly exposedespp.wdiAPI is checked instead of remaining a manual-only test.
"""WDI (Wheelchair Digital Interface) Python binding test.
Exercises the espp.wdi protocol core (ControlReport / FeedbackReport / HostUuid
serialize+parse, bitfields, enums) -- the Python mirror of
components/wdi/test/wdi_protocol_host_test.cpp. This is also how a WDI *host* (the
- Files reviewed: 25/25 changed files
- Comments generated: 13
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
|
|
||
| /// @brief Send an all-zero "release" Control report (neutral joystick, no flags). | ||
| bool send_release() { return send_control(wdi::ControlReport{}); } |
| case wdi::ReportId::KeepaliveResponse: | ||
| if (auto uuid = wdi::HostUuid::parse(payload)) { | ||
| host_uuid_ = *uuid; | ||
| if (config_.on_keepalive_response) | ||
| config_.on_keepalive_response(*uuid); |
| break; | ||
| default: | ||
| return false; // host->device reports are not sent by the device | ||
| } |
Address review feedback + static analysis on the WDI device role. - wdi_hid.hpp: rebuild the report descriptor to describe the REAL report fields (Control = 2x SInt8 axes + 4x 32-bit flag fields; Feedback = 3x 32-bit flags + packed speed/profile, velocity, odometer + 4 reserved bytes; etc.) instead of opaque byte arrays, so a host can introspect it. static_asserts tie the field decomposition to the protocol core's report sizes so the descriptor and serialize()/parse() can't drift. Descriptor is now 156 bytes. - wdi_ble.hpp: add the HID-over-GATT characteristics the WDI spec defines (10A50002 Report Map, 10A50003 HID Information, 10A50004 HID Control Point, 10A50005 Protocol Mode). The Report Map serves the SAME descriptor as USB, so BLE reports are introspectable too. notify_report() made const. - wdi.hpp: fix data races between the transport RX task and the app task - last_tx_ms_ is atomic; host_uuid_ / last_feedback_ are mutex-guarded. - detail/wdi_protocol.hpp: HostUuid::serialize() returns by const reference (returnByReference); clarify the vendor1 Modifier comment (the spec defines a vendor-scope Modifier bit distinct from standard1's). - examples: send a neutral release first and do NOT assert DriveEnable in the demo sweep (a spec-compliant chair ignores motion without DriveEnable), so the test pattern can't command motion on connect. - suppressions.txt: scope unreadVariable/redundantAssignment for the host tests (cppcheck can't see reads through the injected-clock std::function). - README: document the field-accurate descriptor, the HOGP characteristics, and the per-transport component dependencies. Descriptor + all host tests pass; usb_example (58% free) and ble_example (67% free) build clean on IDF v6.1 esp32s3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…tion The Report Map characteristic serves the HID descriptor over BLE too, so the descriptor is not USB-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
Addressed the review + static-analysis feedback in the latest commits. Summary of dispositions: Fixed
Clarified (not a bug)
Deferred to a follow-up (tracked)
🤖 Generated with Claude Code |
NimBLEService::createCharacteristic() can return nullptr (e.g. out of memory). Guard all of the WDI characteristics before dereferencing them (setValue / setCallbacks), bailing with a logged error, instead of an unconditional null-deref. Addresses a review comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 13 comments.
…nst) Now that notify_report() is const, the make_device_config() helper only calls const members, so cppcheck flags it as const-able. Make it const. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Address the second review round: - FeedbackReport::serialize() clamps velocity_tenths to 9 so an out-of-range value can't encode an invalid 10..15 nibble. - Python bindings: parse() takes py::bytes instead of std::string, so a Python str can't be passed and silently UTF-8-encoded into wrong bytes; add <array>. - wdi_hid_host_test.cpp: correct the "non-overlapping" comment on count_item. Protocol + HID host tests pass; bindings syntax-check against pybind11. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…mBLEService::start() NimBLE v2 starts every service when the server starts; NimBLEService::start() is a deprecated no-op. Keep start() for API symmetry with make_service() but make it an explicit no-op instead of calling the deprecated API. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
Self-review follow-up (pushed): |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
… comment) - README dependency table / manifest description: this PR ships the protocol core + device role; the host-role headers it lists are delivered by the stacked follow-up PR -- say so instead of implying they exist here. - Python bindings: check the PyBytes_AsStringAndSize() return and propagate the TypeError CPython raised (py::error_already_set) instead of building a span from an unset pointer. - CMakeLists comment: base_component is required because the transport role classes derive from BaseComponent; the protocol core and the WdiDevice / WdiHost cores are dependency-free (the old comment said wdi.hpp used Logger). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Keep this branch's wording for the WDI README dependency table and manifest description: the device-role PR said the host-role headers arrive in a follow-up, and this PR is that follow-up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
What
New
wdicomponent implementing the Open-Mobility-Hub Wheelchair HID spec (v3.2) — a standard interface between a powered wheelchair and an app/accessory over USB or BLE. This PR is the device role (the app / accessory that drives the chair) over both transports; the host role (the wheelchair) is a follow-up.Layers
detail/wdi_protocol.hpp) — host-testable, ESP-free: the five reports (Control/Feedback/Request-Feedback/Keepalive/Keepalive-Response) as structs withserialize()/parse(), theControlBit/FeedbackBitenums, manufacturer IDs, keepalive timing. LE fields; the 128-bit Host UUID stays big-endian per spec.wdi_hid.hpp) — the vendor (usage page 0xFF00) descriptor built withhid-rp.WdiDevice,wdi.hpp) — transport-agnostic: asendcallback +handle_output(), with the keepalive state machine (poll()emits keepalives when due;send_control/request_feedbackreset the timer). No internal timer, so it's host-testable via an injected clock.WdiBlePeripheral,wdi_ble.hpp) — the WDI GATT service onble_gatt_server+ble_example/.WdiUsbPeripheral,wdi_usb.hpp) — the WDI HID descriptor onespp::UsbDevice(Input reports out; Output reports in viaHidFunction::on_receive) +usb_example/.Host library / interop
The protocol core is bundled into the espp host library: C++ via
wdi/include, Python viaespp.wdibindings (ControlReport/FeedbackReport/HostUuid+ enums,serialize/parse) — enough to build/test a WDI host on a PC. Tests:test/wdi_protocol_host_test.cpp,test/wdi_device_host_test.cpp,test/wdi_hid_host_test.cpp,python/wdi_test.py.Notes
Both examples build clean on IDF v6.1 / esp32s3. Wheelchairs are safety-critical: the component can emulate a WDI device for development — see the README's safety note. Host role (USB Host HID + BLE central) to follow.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU