ODrive ASCII Protocol Component
Overview
espp::OdriveAscii implements a minimal, dependency-free server for the
ODrive-compatible ASCII protocol. It parses incoming bytes into commands and
produces response bytes for transmission by the caller. It does not perform any
I/O itself and is transport-agnostic (UART, USB CDC, socket, etc.).
Features
Register string-addressable properties with read/write callbacks (no exceptions; uses std::error_code)
High-rate commands:
p(position),v(velocity),t/c(torque/current)Property access commands:
r <path>,w <path> <value>Re-entrant, thread-safe; minimal allocations
No direct hardware dependencies; uses
std::functionfor DI
Basic Usage
espp::OdriveAscii proto({.log_level = espp::Logger::Verbosity::INFO});
float position = 0.0f;
proto.register_float_property("axis0.encoder.pos_estimate",
[&]() { return position; },
[&](float v, std::error_code &ec) { position = v; ec.clear(); return true; });
proto.on_position_command([&](int axis, float pos, std::optional<float> vel_ff,
std::optional<float> torque_ff, std::error_code &ec) {
(void)axis; position = pos; return true;
});
// Feed incoming data (from UART, etc.)
auto resp = proto.process_bytes(std::span<const uint8_t>(rx_buf, rx_len));
// Transmit resp back over the same transport
Commands
help: prints brief usager <path>: reads a registered property, returns<value>\nw <path> <value>: writes a registered property, returnsOK\norERR\np <axis> <pos> [vel_ff [torque_ff]]: position setpoint; returnsOK\norERR\nv <axis> <vel> [torque_ff]: velocity setpointc <axis> <torque_nm>: torque (Nm) setpointt <axis> <goal_pos_turns>: trajectory goal position (turns)f <axis>: feedback (returns “<pos> <vel>n”)es <axis> <abs_pos_turns>: set encoder absolute position (turns)
Notes
This is a pragmatic subset designed for easy integration with the Python
odrive package’s ASCII endpoint. Extend by registering additional properties
that mirror your controller’s configuration/state. The component never touches
hardware or transport layers.
API Reference
Header File
Classes
-
class OdriveAscii : public espp::BaseComponent
Minimal ODrive-compatible ASCII protocol server.
Parses ODrive-style ASCII commands from an input byte stream and generates ASCII responses for the caller to transmit. This component does not perform any I/O itself; applications must feed incoming bytes via process_bytes() and send the returned response bytes over their transport (UART/USB/etc.).
Supported commands (subset of ODrive ASCII):
r <path>
w <path> <value>
p <axis> <pos> [vel_ff [torque_ff]]
v <axis> <vel> [torque_ff]
c <axis> <torque_nm>
t <axis> <goal_pos_turns>
f <axis>
es <axis> <abs_pos_turns>
help
Applications integrate by registering properties and command callbacks via std::function dependency injection. The component is thread-safe.
For more information about this protocol see the odrive documentation: https://docs.odriverobotics.com/v/latest/manual/ascii-protocol.html
Basic Example
// Simulated motor state struct { float position = 0.0f; float velocity = 0.0f; float torque = 0.0f; } state; OdriveAscii::Config cfg; cfg.log_level = Logger::Verbosity::INFO; OdriveAscii proto(cfg); // Register some read/write properties matching ODrive-style paths proto.register_float_property( "axis0.encoder.pos_estimate", [&]() { return state.position; }, [&](float v, std::error_code &ec) { ec.clear(); state.position = v; return true; }); proto.register_float_property("axis0.encoder.vel_estimate", [&]() { return state.velocity; }); proto.register_float_property( "axis0.controller.input_pos", [&]() { return state.position; }, [&](float v, std::error_code &ec) { ec.clear(); state.position = v; return true; }); // High-rate command callbacks proto.on_position_command([&](int axis, float pos, std::optional<float> vel_ff, std::optional<float> torque_ff, std::error_code &ec) { (void)axis; ec.clear(); state.position = pos; if (vel_ff.has_value()) state.velocity = *vel_ff; if (torque_ff.has_value()) state.torque = *torque_ff; return true; }); proto.on_velocity_command( [&](int axis, float vel, std::optional<float> torque_ff, std::error_code &ec) { (void)axis; ec.clear(); state.velocity = vel; if (torque_ff.has_value()) state.torque = *torque_ff; return true; }); proto.on_torque_command([&](int axis, float tq, std::error_code &ec) { (void)axis; ec.clear(); state.torque = tq; return true; }); // Trajectory: t <axis> <goal_pos_turns> proto.on_trajectory_command([&](int axis, float goal, std::error_code &ec) { (void)axis; ec.clear(); state.position = goal; return true; }); // Feedback: f <axis> -> "pos vel\n" proto.on_feedback_request([&](int axis, float &pos_out, float &vel_out, std::error_code &ec) { (void)axis; ec.clear(); pos_out = state.position; vel_out = state.velocity; return true; }); // Encoder set absolute: es <axis> <abs_pos_turns> proto.on_encoder_set_absolute([&](int axis, float abs_pos, std::error_code &ec) { (void)axis; ec.clear(); state.position = abs_pos; return true; }); // ------------------------ Basic scripted self-test ------------------------ { const char *script = "r axis0.encoder.pos_estimate\r\n" "w axis0.controller.input_pos 12.34\n" "r axis0.encoder.pos_estimate\n" "p 0 1.0 0.5 0.1\r\n" "v 0 2.0 0.2\r\n" "t 0 0.3\n"; std::span<const uint8_t> bytes(reinterpret_cast<const uint8_t *>(script), strlen(script)); auto response = proto.process_bytes(bytes); std::string out(reinterpret_cast<const char *>(response.data()), response.size()); logger.info("Scripted responses:\n{}", out); }
Console Example
#if defined(ESP_PLATFORM) configure_blocking_console(); #endif // Read from stdin and feed the protocol; print any responses to stdout std::vector<uint8_t> rx; rx.reserve(128); while (true) { int ch = std::getc(stdin); if (ch == EOF) { // Brief yield; on ESP this shouldn't happen under normal console use continue; } uint8_t b = static_cast<uint8_t>(ch & 0xFF); rx.push_back(b); // Process in small chunks or on line endings for responsiveness if (b == '\n' || b == '\r' || rx.size() >= 64) { auto resp = proto.process_bytes(rx); rx.clear(); if (!resp.empty()) { // Write raw bytes out (void)fwrite(resp.data(), 1, resp.size(), stdout); fflush(stdout); } } }
Public Types
-
using read_fn = std::function<std::string(std::error_code &ec)>
Read-only property accessor. Returns textual value, sets ec on error.
-
using write_fn = std::function<bool(std::string_view, std::error_code &ec)>
Write property accessor. Receives textual value; return true on success; set ec on error.
-
using position_command_fn = std::function<bool(int axis, float pos, const std::optional<float> &vel_ff, const std::optional<float> &torque_ff, std::error_code &ec)>
Callback for high-rate position command ‘p’. Signature: (axis, pos, vel_ff, torque_ff) -> bool.
-
using velocity_command_fn = std::function<bool(int axis, float vel, const std::optional<float> &torque_ff, std::error_code &ec)>
Callback for high-rate velocity command ‘v’. Signature: (axis, vel, torque_ff) -> bool.
-
using torque_command_fn = std::function<bool(int axis, float torque_nm, std::error_code &ec)>
Callback for high-rate torque/current command ‘c’. Signature: (axis, torque) -> bool.
-
using trajectory_command_fn = std::function<bool(int axis, float goal_pos_turns, std::error_code &ec)>
Callback for trajectory command ‘t’. Signature: (axis, goal_pos_turns) -> bool.
-
using feedback_command_fn = std::function<bool(int axis, float &pos_out, float &vel_out, std::error_code &ec)>
Callback for feedback request ‘f’. Signature: (axis, out_pos_turns, out_vel_turns_per_s) -> bool.
-
using encoder_set_abs_position_fn = std::function<bool(int axis, float abs_pos_turns, std::error_code &ec)>
Callback for encoder set absolute position ‘es’. Signature: (axis, abs_pos_turns) -> bool.
Public Functions
-
inline explicit OdriveAscii(const Config &config)
Create an OdriveAscii protocol server.
- Parameters:
config – Configuration parameters.
-
inline void register_property(const std::string &path, const read_fn &read = nullptr, const write_fn &write = nullptr)
Register a property accessible via r/w path.
- Parameters:
path – Canonical ODrive-style path (e.g., “axis0.encoder.pos_estimate”).
read – Optional read function; if omitted, property is write-only.
write – Optional write function; if omitted, property is read-only.
-
inline void register_float_property(const std::string &path, const std::function<float()> &getter, const std::function<bool(float, std::error_code&)> &setter = nullptr)
Helper to register a numeric property using typed getter/setter. Converts to/from string using std::to_string / std::strtod.
Note
The float is formatted using “{:0.6g}”.
- Parameters:
path – Property path.
getter – Getter function; if null, property is write-only.
setter – Setter function; if null, property is read-only.
-
inline void register_int_property(const std::string &path, const std::function<int32_t()> &getter, const std::function<bool(int32_t, std::error_code&)> &setter = nullptr)
Helper to register an integer property using typed getter/setter.
Note
The integer is formatted using std::to_string.
- Parameters:
path – Property path.
getter – Getter function; if null, property is write-only.
setter – Setter function; if null, property is read-only.
-
inline void register_bool_property(const std::string &path, const std::function<bool()> &getter, const std::function<bool(bool, std::error_code&)> &setter = nullptr)
Helper to register a boolean property using typed getter/setter. Accepts “0/1”, “true/false” (case-insensitive) when writing.
Note
The boolean is formatted as “0” or “1”.
- Parameters:
path – Property path.
getter – Getter function; if null, property is write-only.
setter – Setter function; if null, property is read-only.
-
inline void on_position_command(const position_command_fn &fn)
Set the position command callback.
Note
This is a high-rate command; the callback should be efficient and avoid blocking.
- Parameters:
fn – Callback function.
-
inline void on_velocity_command(const velocity_command_fn &fn)
Set the velocity command callback.
Note
This is a high-rate command; the callback should be efficient and avoid blocking.
- Parameters:
fn – Callback function.
-
inline void on_torque_command(const torque_command_fn &fn)
Set the torque/current command callback.
Note
This is a high-rate command; the callback should be efficient and avoid blocking.
- Parameters:
fn – Callback function.
-
inline void on_trajectory_command(const trajectory_command_fn &fn)
Set the trajectory command callback.
Note
This is a high-rate command; the callback should be efficient and avoid blocking.
- Parameters:
fn – Callback function.
-
inline void on_feedback_request(const feedback_command_fn &fn)
Set the feedback request callback.
Note
This is a high-rate command; the callback should be efficient and avoid blocking.
- Parameters:
fn – Callback function.
-
inline void on_encoder_set_absolute(const encoder_set_abs_position_fn &fn)
Set the encoder set absolute position callback.
Note
This is a high-rate command; the callback should be efficient and avoid blocking.
- Parameters:
fn – Callback function.
-
std::vector<uint8_t> process_bytes(std::span<const uint8_t> data)
Process a chunk of input bytes; returns response bytes to transmit (if any).
- Parameters:
data – Incoming bytes (may contain partial or multiple lines) using CR/LF line endings.
- Returns:
Response bytes (possibly empty). May contain multiple response lines.
-
void clear_buffer()
Clear any buffered input state.
-
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
Configuration for the OdriveAscii server.