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 outTyped property registry:
register_float_propertyplus signed/unsigned 8/16/32/64-bit integer andboolvariants (no exceptions; usesstd::error_code)Auto-discovery: builds the endpoint-0 JSON descriptor and
json_crcThread-safe; user getters/setters are never invoked while a lock is held
No direct hardware dependencies; uses
std::functionfor 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
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
-
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.
-
template<typename T>