canopen-rs
A no_std-first CANopen (CiA 301) protocol stack in Rust, built to run
unchanged on a bare-metal microcontroller node and on a host
(Linux/SocketCAN).
Status: early development (0.1). The API will change before 1.0, but the object dictionary, SDO client/server, PDO, NMT, SYNC, EMCY, and EDS parsing are implemented and tested.
Why
The Rust CANopen landscape has several partial attempts but no clear winner —
and none delivers a stack that works on both embedded and host targets. That
dual target is exactly what people keep asking for. canopen-rs closes that
gap with a transport-agnostic, allocation-free core and a thin host layer on
top.
What works today
| Area | Status |
|---|---|
| Object dictionary (typed entries, access control) | ✅ |
| Data types + little-endian value codec | ✅ |
| SDO — expedited and segmented transfer | ✅ |
| SDO server (services the OD) and client (drives transactions) | ✅ |
| NMT state machine, node-control, heartbeat / boot-up | ✅ |
| PDO mapping, TPDO pack / RPDO unpack, connection-set COB-IDs | ✅ |
| SYNC (producer counter) and EMCY (emergency + error register) | ✅ |
embedded-can bridge + Linux SocketCAN transport |
✅ |
| EDS / DCF file parsing → object dictionary | ✅ |
| SDO block transfer, LSS, heartbeat-consumer timeout | planned |
Everything in the core is #![no_std], #![deny(unsafe_code)], and builds for
thumbv7em-none-eabihf; SDO frame codecs are validated against known-good byte
sequences, and the client/server are exercised end-to-end.
Install
[]
= "0.1" # no_std core: OD, SDO, PDO, NMT, SYNC, EMCY
= "0.1" # std host layer: SocketCAN transport + EDS parsing
The core is no_std by default; enable std for std::error::Error impls
(canopen-rs = { version = "0.1", features = ["std"] }). On a microcontroller,
depend only on canopen-rs and drive it with your HAL's embedded-can
implementation — no host crate needed. In canopen-host, the SocketCAN
transport is compiled only on Linux; EDS parsing builds everywhere. MSRV:
Rust 1.75.
Quickstart
Serve an object dictionary (a device node)
use ;
use Node;
// Build the node's object dictionary and wrap it in a Node.
let mut od = new;
od.insert?;
od.insert?;
let mut node = new;
let boot = node.boot; // enter pre-operational, announce boot-up
// send `boot` on the bus, then in your receive loop:
if let Some = node.on_frame
Node bundles the object dictionary, SDO server, and NMT state: on_frame
serves SDO requests (expedited or segmented, with standard abort codes),
advances NMT from node-control commands, and applies received PDOs. Add PDOs
with node.add_rpdo/add_tpdo, and get SYNC-triggered transmit frames from
node.sync_tpdos(). It's sans-I/O — the same code runs on a host or an MCU.
Talk to a device (a master), over SocketCAN
use Duration;
use SocketCan;
use ;
let bus = open?; // or "vcan0" for a virtual bus
bus.set_read_timeout?;
let node = new?;
// Read object 0x1000 (device type). Expedited or segmented is handled for you.
let device_type = bus.sdo_read?;
// Write object 0x1017 (producer heartbeat time).
bus.sdo_write?;
On a microcontroller, use the canopen_rs::transport::{frame_from, cob_id}
helpers with any HAL that implements the embedded-can traits instead of
SocketCAN — the client and server code is identical.
Load a device's EDS file into an object dictionary
use Eds;
let eds = from_file?;
println!;
let od = eds.?; // the same OD type a node runs
Testing & validation
-
Independent wire-format cross-check. The frame codecs are validated against [
python-canopen], a mature implementation used against real hardware — every SDO / NMT / EMCY / SYNC / PDO frame matches byte-for-byte (21/21). It runs offline: -
On-bus loopback (Linux, no hardware). Run a full SDO server ↔ client exchange — expedited and segmented — over a virtual CAN interface, the first time the SocketCAN transport actually executes on a bus:
Design
core(canopen-rs) —no_std, allocation-free, transport-agnostic. The object dictionary, the message codecs, the NMT state machine, and the SDO server/client. The server and client are sans-I/O: they consume and produce frames but never touch a bus, so the same logic runs on host and MCU. CAN frames flow through theembedded-cantraits.host(canopen-host) —stdlayer on the core: a Linux SocketCAN transport with one-callsdo_read/sdo_write, and EDS/DCF parsing. SocketCAN is gated to Linux; EDS parsing builds everywhere.
canopen-rs/
├── core/ # no_std core protocol stack (crate: canopen-rs)
└── host/ # std host transport + EDS (crate: canopen-host)
References studied (not copied)
- The Python
canopenlibrary — the most complete reference for the object model, SDO/PDO semantics, and EDS handling (host-only, BSD-licensed). zencan— the most advanced Rust prior art.- The CiA 301 specification as the authoritative source of truth.
License
Licensed under either of MIT or Apache-2.0 at your option. Unless you explicitly state otherwise, any contribution you submit for inclusion shall be dual-licensed as above, without additional terms.