This is Yet Another CanOpen liBrary for Rust. It was developed with the goal of providing full functionality, including SDO server and client, for both embedded devices (no_std) and desktop platforms.
Implementation was heavily LLM-assisted, but verified on real hardware, using both embassy on STM32 and ESP32, and Linux with a USB-CAN adapter.
The OD (object dictionary) can either be imported from an EDS file, or specified in-code through a dsl:
object_dictionary! { pub struct NodeOd { [0x1000] device_type: u32 = 0x0000_0191, ro; [0x1001] error_register: u8 = 0x00, ro; [0x1018] identity: record { [1] vendor_id: u32 = 0x0000_CAFE, ro; [2] product_code: u32 = 0x0001, ro; [3] revision: u32 = 0x0001_0000, ro; [4] serial_number: u32 = 0x0000_0001, ro; }; // CiA 401-style process I/O. Perspective is the physical process, // not the bus: an "input" is read from the world and published on // the bus (TPDO); an "output" is commanded from the bus (RPDO) // and driven into the world. [0x6000] inputs: record { [1] button: u8 = 0, ro, pdo; // PB7 (0=released, 1=pressed) }; [0x6200] outputs: record { [1] led: u8 = 0, rw, pdo; // PB8 (0=off, 1=on) }; // Bus-loopback test object. It has no physical-world meaning, so it // lives in the manufacturer-specific area (0x2000..=0x5FFF) instead // of the device-profile area. Names are from the device's view: // echo_in arrives from the bus, echo_out is sent back. [0x2000] echo: record { [1] echo_in: u16 = 0, rw, pdo; // written by remote [2] echo_out: u16 = 0, ro, pdo; // node mirrors echo_in here }; // TPDO1: data this node sends (0x181 for node 1). // - event_driven: send on change, not tied to SYNC. Other options: // sync_acyclic, sync_cyclic(N), or a raw CiA 301 value (e.g. 255). // - inhibit_time: minimum spacing between sends. event_timer: periodic // fallback — send even if nothing changed (omit to disable). Both // take unit suffixes (50ms, 0.1s, 500us) or raw CiA 301 values. // - Fields are packed into one CAN frame: [button (1 byte) | echo_out (2 bytes)] tpdo[1](transmission_type = event_driven, inhibit_time = 50ms, event_timer = 1s) { button, echo_out, }; // RPDO1: data this node receives (0x201 for node 1). // - event_driven: apply values to the OD immediately on arrival. With // sync_acyclic, values would be buffered until the next SYNC pulse // (useful for coordinated updates). // - Fields are unpacked from the CAN frame: [led (1 byte) | echo_in (2 bytes)] // - Writing to these emits a typed NodeOdChange, which wakes main via // EVENT_SIGNAL. rpdo[1](transmission_type = event_driven) { led, echo_in, }; } }
PDOs, SDOs, NMT, heartbeat, EMCY, and SYNC are implemented. The inline,
reactive, and scan application models are agreed but their public APIs are not
implemented yet; the current event-queue APIs are transitional. See
_Tasks/application-models.md for the design and implementation status.
Before using the stack for command or safety traffic, read the canonical
_Tasks/known-issues.md defect index and the
_Tasks/conformance-matrix.md. In particular,
RPDO acceptance is not yet transactional for malformed process images.
Nevertheless, I am already using it for a few private projects, both implementating CANopen nodes, or interacting with existing hardware like DS402 motor drivers.