Wheelchair Digital Interface (WDI)
The wdi component implements the Open-Mobility-Hub Wheelchair HID
specification (v3.2) — a standard interface that lets an accessory (special
switches, an alternative joystick, a phone app, a companion MCU) drive a powered
wheelchair and receive status/telemetry back, over USB or Bluetooth LE.
The component is layered so the same protocol serves every combination of role and transport:
Protocol core (
include/detail/wdi_protocol.hpp) — host-testable and ESP-free: the five HID reports (Control, Feedback, Request-Feedback, Keepalive, Keepalive-Response), their bitfields, and pack/parse helpers.HID report descriptor (
include/wdi_hid.hpp) — the vendor (usage page 0xFF00) report descriptor, built with the espphid-rpcomponent. Only the USB HID transport needs it (BLE carries the same reports as GATT characteristics).Device role — the app / accessory: sends Control, receives Feedback.
espp::WdiDevice(wdi.hpp): the transport-agnostic core with the app’s keepalive state machine.espp::WdiBlePeripheral(wdi_ble.hpp): the WDI GATT service onble_gatt_server.espp::WdiUsbPeripheral(wdi_usb.hpp): the WDI HID descriptor onespp::UsbDevice.
Host role — the wheelchair: receives Control, sends Feedback, and runs the keepalive watchdog (drive-disable if the accessory goes quiet).
espp::WdiHost(wdi_host.hpp): the transport-agnostic core with the host’s keepalive watchdog.espp::WdiBleCentral(wdi_ble_central.hpp): a NimBLE central that connects to a WDI peripheral.espp::WdiUsbHost(wdi_usb_host.hpp): anespp::UsbHost(USB Host HID) that talks to a WDI HID device.
Report directions are named from the device (accessory) point of view — an Input report is device→host (Control / Request-Feedback / Keepalive), an Output report is host→device (Feedback / Keepalive-Response). All payloads are little-endian except the 128-bit Host UUID, which is big-endian per the spec.
Keepalive / timeout
The app sends a Control / Request-Feedback / Keepalive report every ~233 ms; the
host’s window is 257 ms and it disconnects + drive-disables after 3
consecutive missed windows. WdiDevice::poll() emits a keepalive when one is
due; WdiHost::poll() fires the disconnect callback when the watchdog expires.
Both take an injectable clock, so both cores are unit-tested on a host
(test/wdi_device_host_test.cpp, test/wdi_host_host_test.cpp).
Safety
This component can emulate a WDI device or host for development and testing. A powered wheelchair is safety-critical: do not connect an emulator to a real chair without the manufacturer’s guidance, and honor the keepalive / drive-disable semantics — a lost link must drop to a safe, stopped state.
Examples
components/wdi/ble_example— the device role over BLE (advertises the WDI service and drives a wheelchair).components/wdi/usb_example— the device role over USB (enumerates as a WDI HID device).components/wdi/ble_central_example— the host role over BLE (scans for and connects to a WDI peripheral).components/wdi/usb_host_example— the host role over USB (enumerates a WDI HID device from the host side).
API Reference
Header File
Header File
Classes
-
class WdiDevice
The WDI **device** role (app / accessory driving the wheelchair).
Public Types
-
using send_fn = std::function<bool(wdi::ReportId id, std::span<const uint8_t> payload)>
Transmit a report to the host. `id` is the report id; `payload` is the report body (no report-id byte). Return true if it was sent. The transport binding maps this to a USB HID Input report or a BLE notify.
-
using feedback_fn = std::function<void(const wdi::FeedbackReport&)>
Invoked when a Feedback (0x02) report arrives from the host.
-
using host_uuid_fn = std::function<void(const wdi::HostUuid&)>
Invoked when a Keepalive Response (0x05) arrives (the host’s UUID).
-
using clock_fn = std::function<uint32_t()>
Monotonic clock in milliseconds.
Public Functions
-
inline bool send_control(const wdi::ControlReport &control)
Send a Control report (joystick + flags). Resets the keepalive timer.
-
inline bool send_release()
Send an all-zero “release” Control report (neutral joystick, no flags).
-
inline bool request_feedback()
Ask the host to send a Feedback report. Resets the keepalive timer.
-
inline bool send_keepalive()
Send a Keepalive heartbeat (normally emitted automatically by poll()).
-
inline bool poll()
Emit a keepalive if the interval has elapsed since the last transmit. Call this periodically (e.g. from an espp::Timer or Task). Returns true if a keepalive was actually sent this call.
-
inline uint32_t ms_until_keepalive() const
Milliseconds until the next keepalive is due (0 if due now).
-
inline void handle_output(wdi::ReportId id, std::span<const uint8_t> payload)
Feed a received OUTPUT report (host→device): Feedback (0x02) or Keepalive Response (0x05). Other ids are ignored. The transport binding calls this from its HID SET_REPORT / BLE write handler.
-
inline std::optional<wdi::HostUuid> host_uuid() const
The host’s identity from the most recent Keepalive Response, if any. Safe to call from a different task than handle_output().
-
inline std::optional<wdi::FeedbackReport> last_feedback() const
The most recently received Feedback report, if any. Safe to call from a different task than handle_output().
-
struct Config
Public Members
-
feedback_fn on_feedback = {nullptr}
called with each Feedback report
-
host_uuid_fn on_keepalive_response = {nullptr}
called with each Keepalive Response
-
uint32_t keepalive_interval_ms = {wdi::kAppKeepaliveIntervalMs}
Keepalive send interval (ms). The spec’s app sends every ~233 ms (24 ms margin before the host’s 257 ms window); sending Control or Request-Feedback also resets the timer.
-
feedback_fn on_feedback = {nullptr}
-
using send_fn = std::function<bool(wdi::ReportId id, std::span<const uint8_t> payload)>
Header File
Header File
Classes
-
class WdiBlePeripheral : public espp::BaseComponent
The WDI device role over BLE (a GATT peripheral).
Public Functions
-
inline void make_service(NimBLEServer *server)
Create the WDI service + characteristics on `server`. Call after BleGattServer::init() (which creates the NimBLEServer) and before start().
-
inline void start()
Kept for API symmetry with make_service(); NimBLE starts every service when the server starts (NimBLEService::start() is a deprecated no-op), so there is nothing to do here.
-
inline bool poll()
Emit a keepalive if due; call periodically (e.g. from an espp::Timer).
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static inline NimBLEUUID service_uuid()
The WDI GATT service UUID (advertise this so a wheelchair finds it).
-
struct Config
Public Members
-
WdiDevice::feedback_fn on_feedback = {nullptr}
called with each Feedback report
-
WdiDevice::host_uuid_fn on_keepalive_response = {nullptr}
called with the host’s UUID
-
uint32_t keepalive_interval_ms = {wdi::kAppKeepaliveIntervalMs}
keepalive send interval
-
WdiDevice::feedback_fn on_feedback = {nullptr}
-
inline void make_service(NimBLEServer *server)
-
class WriteCb : public NimBLECharacteristicCallbacks
Header File
Classes
-
class WdiUsbPeripheral : public espp::BaseComponent
The WDI device role over USB (a HID device).
Public Functions
-
inline bool initialize(std::error_code &ec)
Install the TinyUSB driver + WDI HID interface.
-
inline bool poll()
Emit a keepalive if due; call periodically (e.g. from an espp::Timer).
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
-
struct Config
Public Members
-
WdiDevice::feedback_fn on_feedback = {nullptr}
called with each Feedback report
-
WdiDevice::host_uuid_fn on_keepalive_response = {nullptr}
called with the host’s UUID
-
uint32_t keepalive_interval_ms = {wdi::kAppKeepaliveIntervalMs}
keepalive send interval
-
uint16_t vid = {0x1209}
USB VID (default: pid.codes); set your own.
-
uint16_t pid = {0x0d32}
USB PID.
-
std::string manufacturer = {"espp"}
USB manufacturer string.
-
std::string product = {"espp WDI"}
USB product string.
-
std::string interface_name = {"WDI"}
HID interface string.
-
uint8_t poll_interval_ms = {10}
HID interrupt IN polling interval.
-
WdiDevice::feedback_fn on_feedback = {nullptr}
-
inline bool initialize(std::error_code &ec)
Header File
Classes
-
class WdiHost
The WDI **host** role (the wheelchair receiving Control, sending Feedback).
Public Types
-
using send_fn = std::function<bool(wdi::ReportId id, std::span<const uint8_t> payload)>
Transmit an OUTPUT report to the app. `id` is the report id; `payload` is the report body (no report-id byte). Return true if it was sent. The transport binding maps this to a USB HID Output report (SET_REPORT) or a BLE write.
-
using control_fn = std::function<void(const wdi::ControlReport&)>
Invoked when a Control (0x01) report arrives from the app. The wheelchair should act on it (or, on a release / disconnect, stop).
-
using feedback_provider_fn = std::function<wdi::FeedbackReport()>
Supplies the current Feedback to send (on Request-Feedback or send_feedback()). If unset, the last value from set_feedback() is used.
-
using link_fn = std::function<void()>
Link state change (connected when the app is talking; disconnected when the keepalive watchdog expires).
-
using clock_fn = std::function<uint32_t()>
Monotonic clock in milliseconds.
Public Functions
-
inline void handle_input(wdi::ReportId id, std::span<const uint8_t> payload)
Feed a received INPUT report (app→host): Control (0x01), Request-Feedback (0x03) or Keepalive (0x04). Any of them refreshes the watchdog and marks the link connected. Request-Feedback triggers a Feedback reply; Keepalive triggers a Keepalive-Response reply.
Note
The 1-byte trigger reports are deliberately not validated (size or the 0x01 value): a peer that got as far as sending one on the right characteristic / report id is alive, which is all the host needs.
-
inline void set_feedback(const wdi::FeedbackReport &fb)
Update the Feedback the host reports (used when no feedback provider is configured, and as the value sent by send_feedback()).
-
inline bool send_feedback()
Send a Feedback report now (host→app). Returns true if sent.
-
inline bool send_keepalive_response()
Send a Keepalive Response (the host’s UUID) now. Returns true if sent.
-
inline bool poll()
Check the keepalive watchdog; call periodically. If the app has been quiet for `missed_windows_to_disconnect` windows, the link is marked disconnected (fire on_disconnected — the caller must drive-disable). Returns true if a disconnect transition happened this call.
Note
A transport binding may report the same link loss again (the USB / BLE detach arriving after the watchdog already fired), so on_disconnected can be invoked twice for one event; it must be idempotent (drive-disable is).
-
inline bool is_connected() const
Whether the app is currently considered connected (talking).
-
inline uint32_t ms_until_timeout() const
Milliseconds until the watchdog expires (0 if already expired / down).
-
inline std::optional<wdi::ControlReport> last_control() const
The most recently received Control report, if any.
Public Static Functions
-
static inline wdi::HostUuid make_host_uuid(uint16_t manufacturer_id, std::span<const uint8_t> random14)
Build a Host UUID from a manufacturer id and 14 random bytes (the spec’s RFC-4122 v4 layout). The manufacturer id is stored big-endian in bytes 0..1; the version / variant nibbles are set on the random part. Pass your own randomness (e.g. esp_fill_random / a PRNG).
-
struct Config
Public Members
-
control_fn on_control = {nullptr}
called with each Control report
-
feedback_provider_fn feedback = {nullptr}
current Feedback to report (optional)
-
wdi::HostUuid host_uuid = {}
The host’s 128-bit identity, returned in Keepalive Responses. Set at least the manufacturer id (see make_host_uuid()).
-
uint32_t keepalive_window_ms = {wdi::kHostKeepaliveWindowMs}
Per-window timeout (ms). The app sends every ~233 ms; the host’s window is 257 ms.
-
uint32_t missed_windows_to_disconnect = {wdi::kHostMissedWindowsToDisconnect}
Consecutive missed windows before disconnect + drive-disable (spec: 3).
-
control_fn on_control = {nullptr}
-
using send_fn = std::function<bool(wdi::ReportId id, std::span<const uint8_t> payload)>
Header File
Classes
-
class WdiUsbHost : public espp::BaseComponent
The WDI host role over USB (a USB host talking to a WDI HID device).
Public Functions
-
inline bool initialize(std::error_code &ec)
Install the USB host stack and start looking for a WDI device.
-
inline void set_feedback(const wdi::FeedbackReport &fb)
Update the Feedback reported to the accessory (host->device).
-
inline bool send_feedback()
Send a Feedback report now (if a device is connected).
-
inline bool poll()
Run the keepalive watchdog; call periodically (e.g. from a Timer). Fires on_disconnected if the accessory has gone quiet too long.
-
inline bool is_connected() const
Whether a WDI accessory is currently connected and talking.
-
inline std::optional<wdi::ControlReport> last_control() const
The most recent Control report, if any.
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static inline bool looks_like_wdi(std::span<const uint8_t> d)
Does a HID report descriptor describe a WDI device? See wdi::looks_like_wdi_descriptor() (host-tested in test/wdi_hid_host_test.cpp); this is what decides which HID device the host adopts.
-
struct Config
Public Members
-
WdiHost::control_fn on_control = {nullptr}
a Control report arrived
-
WdiHost::feedback_provider_fn feedback = {nullptr}
current Feedback to report
-
wdi::HostUuid host_uuid = {}
the host’s identity (see WdiHost::make_host_uuid)
-
WdiHost::control_fn on_control = {nullptr}
-
inline bool initialize(std::error_code &ec)
Header File
Classes
-
class WdiBleCentral : public espp::BaseComponent
The WDI host role over BLE (a GATT central talking to a WDI peripheral).
Public Functions
-
inline ~WdiBleCentral()
Disconnects and releases the NimBLE client. Detaches our callbacks before doing so, so a disconnect event that lands after this object is gone cannot call into it.
-
inline bool scan_and_connect(uint32_t scan_ms, std::error_code &ec)
Scan for a peripheral advertising the WDI service and connect to the first one found. Blocks up to `scan_ms`.
-
inline bool connect(const NimBLEAddress &address, std::error_code &ec)
Connect to a specific peripheral address, discover the WDI service, subscribe to its notify characteristics, and start the host role.
-
inline void set_feedback(const wdi::FeedbackReport &fb)
Update the Feedback reported to the accessory (host->app).
-
inline bool send_feedback()
Send a Feedback report now (if connected).
-
inline bool poll()
Run the keepalive watchdog; call periodically.
-
inline bool is_connected() const
Whether a WDI accessory is connected and talking.
-
inline std::optional<wdi::ControlReport> last_control() const
The most recent Control report, if any.
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static inline NimBLEUUID service_uuid()
The WDI service UUID (scan for peripherals advertising this).
-
struct Config
Public Members
-
WdiHost::control_fn on_control = {nullptr}
a Control report arrived
-
WdiHost::feedback_provider_fn feedback = {nullptr}
current Feedback to report
-
wdi::HostUuid host_uuid = {}
the host’s identity
-
WdiHost::control_fn on_control = {nullptr}
-
inline ~WdiBleCentral()