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::function for 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 usage

  • r <path>: reads a registered property, returns <value>\n

  • w <path> <value>: writes a registered property, returns OK\n or ERR\n

  • p <axis> <pos> [vel_ff [torque_ff]]: position setpoint; returns OK\n or ERR\n

  • v <axis> <vel> [torque_ff]: velocity setpoint

  • c <axis> <torque_nm>: torque (Nm) setpoint

  • t <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

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

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

set_log_level

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

get_log_level

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

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.

Public Members

size_t max_line_length = {256}

Maximum accepted ASCII line length.

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Logger verbosity.