ODrive Native (Fibre endpoint) Protocol Component

Overview

espp::OdriveNative implements a transport-agnostic server for the ODrive legacy native (Fibre endpoint) binary protocol (firmware <= 0.5.x), as used over the USB vendor interface where each bulk transfer carries exactly one packet. It parses one inbound request packet and produces one response packet; it performs no I/O itself.

Applications register typed properties from dotted paths (mirroring espp::OdriveAscii). Endpoint ids are assigned sequentially starting at 1 (endpoint 0 is the JSON descriptor blob), and the compact JSON descriptor and its CRC are finalized lazily. This lets a legacy odrivetool / fibre-python client auto-discover the object tree and perform typed get/set.

The CRC / packet packing / type codecs / JSON descriptor / dispatch logic lives in espp::detail::OdriveNativeCore, a host-buildable wire core that depends only on the C++ standard library, so the protocol can be unit-tested off-target.

Features

  • Transport-agnostic: one packet in via process_bytes, one response packet out

  • Typed property registry: register_float_property plus signed/unsigned 8/16/32/64-bit integer and bool variants (no exceptions; uses std::error_code)

  • Auto-discovery: builds the endpoint-0 JSON descriptor and json_crc

  • Thread-safe; user getters/setters are never invoked while a lock is held

  • No direct hardware dependencies; uses std::function for DI

Basic Usage

espp::OdriveNative proto({.log_level = espp::Logger::Verbosity::INFO});
float vbus = 24.0f, input_pos = 0.0f;
proto.register_float_property("vbus_voltage", [&]() { return vbus; });
proto.register_float_property("axis0.controller.input_pos",
                              [&]() { return input_pos; },
                              [&](float v, std::error_code &ec) { input_pos = v; ec.clear(); return true; });

// One USB bulk transfer == one packet.
auto resp = proto.process_bytes(std::span<const uint8_t>(rx_buf, rx_len));
// Transmit resp back over the same transport (empty when no response expected)

Protocol

The authoritative wire specification (packet format, CRC-16 with poly 0x3d65 / init 0x1337, endpoint dispatch, little-endian type codecs, and the compact JSON schema) is documented in components/odrive_native/PROTOCOL.md.

Notes

This component implements the property (primitive get/set) surface of the legacy protocol; functions / endpoint refs are not implemented yet. Wiring to a concrete USB device stack is handled in a later phase.

API Reference

Header File

Classes

class OdriveNative : public espp::BaseComponent, public espp::detail::OdriveNativeCore

ODrive legacy native (Fibre endpoint) binary protocol server.

Implements the packet-based ODrive legacy endpoint protocol (fw <= 0.5.x) as used over the USB vendor interface, where each USB bulk transfer carries exactly one packet. The component is transport-agnostic and performs no I/O itself: feed one inbound request packet to process_bytes() and transmit the returned response packet (empty when no response is expected).

Applications register typed properties from dotted paths (mirroring espp::OdriveAscii). Endpoint ids are assigned sequentially starting at 1 (endpoint 0 is reserved for the JSON descriptor blob), and the compact JSON descriptor plus its CRC are finalized lazily on first use. This lets a legacy odrivetool / fibre-python client auto-discover the object tree and perform typed get/set.

The registration API and dispatch are provided by espp::detail:: OdriveNativeCore, a host-buildable wire core with no ESP dependencies; this class adds the espp logging identity via BaseComponent.

See PROTOCOL.md for the authoritative wire specification.

Basic Example

  // Simulated motor state.
  struct {
    float vbus_voltage = 24.0f;
    float input_pos = 0.0f;
    int32_t axis_state = 1;
  } state;

  OdriveNative::Config cfg;
  cfg.log_level = Logger::Verbosity::INFO;
  OdriveNative proto(cfg);

  // Register a small object tree of simulated-motor properties. Endpoint ids
  // are assigned in registration order starting at 1.
  proto.register_float_property("vbus_voltage", [&]() { return state.vbus_voltage; }); // id 1
  proto.register_float_property(
      "axis0.controller.input_pos", [&]() { return state.input_pos; }, // id 2 (rw)
      [&](float v, std::error_code &ec) {
        ec.clear();
        state.input_pos = v;
        return true;
      });
  proto.register_int32_property(
      "axis0.current_state", [&]() { return state.axis_state; }, // id 3 (rw)
      [&](int32_t v, std::error_code &ec) {
        ec.clear();
        state.axis_state = v;
        return true;
      });

  const uint16_t json_crc = proto.json_crc();
  logger.info("Endpoint JSON descriptor ({} bytes, crc=0x{:04x}):\n{}", proto.json().size(),
              json_crc, proto.json());

  // 1) endpoint-0 read: fetch the JSON descriptor (offset 0, up to 512 bytes).
  {
    std::vector<uint8_t> offset;
    put_u32(offset, 0);
    auto req = make_packet(0x0001, /*endpoint*/ 0, /*expect*/ true, /*output_len*/ 512, offset,
                           /*trailer*/ 1 /*PROTOCOL_VERSION*/);
    auto resp = proto.process_bytes(req);
    std::string json(resp.begin() + 2, resp.end());
    logger.info("endpoint-0 read -> {} bytes: {}", resp.size(), json);
  }

  // 2) write axis0.controller.input_pos (endpoint 2) = 3.14f.
  {
    const float value = 3.14f;
    std::vector<uint8_t> payload(4);
    std::memcpy(payload.data(), &value, 4);
    auto req =
        make_packet(0x0002, /*endpoint*/ 2, /*expect*/ true, /*output_len*/ 0, payload, json_crc);
    (void)proto.process_bytes(req);
    logger.info("wrote input_pos=3.14 -> state.input_pos={}", state.input_pos);
  }

  // 3) read axis0.controller.input_pos back (endpoint 2, output_len=4).
  {
    auto req = make_packet(0x0003, /*endpoint*/ 2, /*expect*/ true, /*output_len*/ 4,
                           std::span<const uint8_t>{}, json_crc);
    auto resp = proto.process_bytes(req);
    float readback = 0.0f;
    if (resp.size() >= 6)
      std::memcpy(&readback, resp.data() + 2, 4);
    logger.info("read input_pos -> resp [{}] value={}", to_hex(resp), readback);
  }

Public Types

template<typename T>
using getter_fn = std::function<T()>

Read accessor: return the current typed value.

template<typename T>
using setter_fn = std::function<bool(T, std::error_code&)>

Write accessor: apply a typed value, set ec on error, return true on ok.

using error_callback_fn = std::function<void(const std::string&)>

Callback invoked with a human-readable message when a request is dropped or a write fails. The wire protocol has no error channel, so without this hook such failures are invisible to the device application. (Kept as a plain std::function so this core stays ESP-free; espp::OdriveNative wires it to its logger.)

Public Functions

inline explicit OdriveNative(const Config &config)

Create an OdriveNative protocol server.

Parameters:

config – Configuration parameters.

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

inline void set_error_callback(const error_callback_fn &cb)

Set the (optional) error callback. May be invoked from whatever context calls process_bytes(); keep it short and non-blocking.

inline void register_bool_property(const std::string &path, const getter_fn<bool> &getter, const setter_fn<bool> &setter = nullptr)

Register a bool property. Wire size is 1 byte, serialized as 0/1.

inline void finalize()

Build (or rebuild) the JSON descriptor and its CRC. Called lazily by process_bytes(); safe to call explicitly.

inline std::string json()

The compact JSON descriptor bytes (endpoint 0 blob).

inline uint16_t json_crc()

CRC-16 over the JSON descriptor (the canary for endpoints > 0).

inline std::vector<uint8_t> process_bytes(std::span<const uint8_t> in)

Process exactly one inbound packet and return the response packet.

Parameters:

in – One complete request packet (one USB bulk transfer).

Returns:

Response packet bytes, or empty if no response is expected / the packet is ignored.

struct Config

Configuration for the OdriveNative server.

Public Members

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

Logger verbosity.