canopen-host 0.5.0

Host-side (std) transport and tooling for canopen-rs: Linux SocketCAN transport and EDS parsing.
Documentation

canopen-rs

CI crates.io docs.rs license

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.5). The API will change before 1.0, but the object dictionary, SDO client/server (expedited, segmented, block), PDO, NMT, SYNC, EMCY, TIME, LSS, and EDS parsing are implemented and tested — the whole stack is verified running on a bus, with blocking and async host transports.

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 + node guarding
PDO mapping, TPDO pack / RPDO unpack, connection-set COB-IDs
SYNC, EMCY (emergency + error register), and TIME
Node runtime (OD + SDO + NMT + PDO exchange)
LSS (node-id assignment, incl. in Node)
SDO block transfer (download + upload, CRC-16)
embedded-can bridge + Linux SocketCAN transport (blocking + async/tokio)
EDS / DCF file parsing → object dictionary
Host NMT master: send_nmt + heartbeat monitor
String / DOMAIN value types 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

[dependencies]
canopen-rs = "0.5"      # no_std core: OD, SDO, PDO, NMT, SYNC, EMCY
canopen-host = "0.5"    # 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.5", 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.

Command-line tool

cargo install canopen-cli    # installs the `canopen` binary

canopen eds device.eds                       # inspect an EDS/DCF file (any OS)
canopen codegen device.eds > src/od.rs       # EDS -> compile-time ObjectDictionary
canopen read  can0 0x10 1017 u16             # SDO read  (Linux SocketCAN)
canopen write can0 0x10 1017 u16 1000        # SDO write
canopen nmt   can0 start 0x10                # send an NMT command
canopen monitor can0                         # decode and print bus traffic

Quickstart

Serve an object dictionary (a device node)

use canopen_rs::{Address, Entry, NodeId, ObjectDictionary, Value};
use canopen_rs::node::Node;

// Build the node's object dictionary and wrap it in a Node.
let mut od = ObjectDictionary::<16>::new();
od.insert(Address::new(0x1000, 0), Entry::constant(Value::Unsigned32(0x0004_0192)))?;
od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))?;
let mut node = Node::new(NodeId::new(0x10)?, od);

let boot = node.boot();                 // enter pre-operational, announce boot-up
// send `boot` on the bus, then in your receive loop:
if let Some(tx) = node.on_frame(cob_id, &data) {
    // send `tx.data()` on `tx.cob_id`
}

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 std::time::Duration;
use canopen_host::transport::SocketCan;
use canopen_rs::{Address, DataType, NodeId, Value};

let bus = SocketCan::open("can0")?;                 // or "vcan0" for a virtual bus
bus.set_read_timeout(Duration::from_millis(500))?;
let node = NodeId::new(0x10)?;

// Read object 0x1000 (device type). Expedited or segmented is handled for you.
let device_type = bus.sdo_read(node, Address::new(0x1000, 0), DataType::Unsigned32)?;

// Write object 0x1017 (producer heartbeat time).
bus.sdo_write(node, Address::new(0x1017, 0), Value::Unsigned16(1000))?;

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 canopen_host::eds::Eds;

let eds = Eds::from_file("device.eds")?;
println!("{:?}: {} objects", eds.vendor_name, eds.objects.len());
let od = eds.object_dictionary::<256>()?;   // the same OD type a node runs

Testing & validation

cargo test --workspace                                     # unit + integration + doc tests
cargo build -p canopen-rs --target thumbv7em-none-eabihf   # confirm the core stays no_std
cargo clippy --workspace --all-targets
  • 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 / LSS / block-transfer frame matches byte-for-byte (33/33). It runs offline:

    python3 -m pip install canopen
    python3 tools/interop/python_canopen_oracle.py
    
  • On-bus loopback (Linux, no hardware). Walk the whole stack over a virtual CAN interface — LSS node-id assignment, SDO (expedited + segmented), NMT, a SYNC-triggered PDO exchange, and a block transfer — exercising the SocketCAN transport on a real bus:

    sudo tools/vcan_setup.sh                               # bring up vcan0
    cargo run -p canopen-host --example vcan_loopback
    

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 the embedded-can traits.
  • host (canopen-host)std layer on the core: a Linux SocketCAN transport with one-call sdo_read / sdo_write (blocking, plus an async tokio variant behind the tokio feature), and EDS/DCF parsing (with a build.rs object-dictionary codegen). SocketCAN is gated to Linux; EDS parsing and codegen build 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 canopen library — 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.

Contributing

Contributions are welcome — see CONTRIBUTING.md for the workflow (DCO sign-off, local checks, and the no_std core vs host split). Good entry points are issues labelled good first issue. Questions and ideas are welcome in Discussions. Participation is governed by our Code of Conduct.

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.