A complete CANopen master/client implementation in Rust with async/await support and PEAK CAN hardware integration.
- SDO (Service Data Objects) - Expedited and segmented transfers for reading/writing object dictionary entries
- NMT (Network Management) - Node state control, heartbeat monitoring, and state transitions
- PDO (Process Data Objects) - High-speed real-time data exchange with mapping support
- SYNC (Synchronization) - Network synchronization with counter support (up to 100 Hz tested)
- EMCY (Emergency) - Emergency message handling with 40+ standard error codes
- LSS (Layer Setting Services) - Node configuration and commissioning (all 14 commands)
- Async/await based API using Tokio runtime
- Type-safe message handling with compile-time checks
- Thread-safe concurrent access with Arc + RwLock
- Zero-copy where possible for performance
- Event-driven callbacks for real-time notifications
- Comprehensive error handling with descriptive error types
- PEAK CAN USB adapters (via peak-can-sys)
- Support for 10 kbps to 1 Mbps bus speeds
- Tested at 1 Mbps with 50+ PDO messages/second
Add to your Cargo.toml:
[dependencies]
libcanopen-client = "0.1"
tokio = { version = "1.0", features = ["full"] }
env_logger = "0.11" # Optional: for logginguse libcanopen_client::*;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
env_logger::init();
// Create and connect
let mut canopen = CANopenSimple::new();
canopen.connect(BusSpeed::Baud1M).await?;
// Read device type from node 5
let device_type = canopen.sdo_read_u32(5, 0x1000, 0).await?;
println!("Device type: 0x{:08X}", device_type);
// Write heartbeat producer time (1000ms)
canopen.sdo_write_u16(5, 0x1017, 0, 1000).await?;
// Start node in operational mode
canopen.nmt_start(5).await?;
// Register PDO callback
canopen.register_pdo_callback(0x185, |data| {
println!("PDO received: {:02X?}", data);
}).await;
// Send SYNC message
canopen.send_sync().await?;
Ok(())
}Read and write to device object dictionary:
// Read operations
let value_u8 = canopen.sdo_read_u8(node_id, 0x1001, 0).await?;
let value_u16 = canopen.sdo_read_u16(node_id, 0x1017, 0).await?;
let value_u32 = canopen.sdo_read_u32(node_id, 0x1000, 0).await?;
let raw_data = canopen.sdo_read(node_id, 0x1018, 1).await?;
// Write operations
canopen.sdo_write_u8(node_id, 0x1001, 0, 0x00).await?;
canopen.sdo_write_u16(node_id, 0x1017, 0, 1000).await?;
canopen.sdo_write_u32(node_id, 0x1000, 0, 0x12345678).await?;
canopen.sdo_write(node_id, 0x1018, 1, vec![1,2,3,4]).await?;Segmented transfers (>4 bytes) are handled automatically!
Control node states and monitor heartbeats:
// Node control
canopen.nmt_start(node_id).await?; // Enter operational
canopen.nmt_stop(node_id).await?; // Enter stopped
canopen.nmt_enter_pre_operational(node_id).await?; // Pre-operational
canopen.nmt_reset_node(node_id).await?; // Reset application
canopen.nmt_reset_communication(node_id).await?; // Reset communication
// State monitoring
let state = canopen.get_node_state(node_id).await?;
println!("Node state: {:?}", state);
// Heartbeat callback
canopen.register_heartbeat_callback(node_id, |node_id, state| {
println!("Node {} state changed to {:?}", node_id, state);
}).await;High-speed real-time data exchange:
// Send PDO
canopen.send_pdo(0x185, vec![0x01, 0x02, 0x03, 0x04]).await?;
// Register callback for received PDOs
canopen.register_pdo_callback(0x285, |data| {
println!("Received PDO: {:02X?}", data);
}).await;
// Unregister callback
canopen.unregister_pdo_callback(0x285).await;Network synchronization for coordinated PDO transmission:
// Simple SYNC
canopen.send_sync().await?;
// SYNC with counter (1-240)
canopen.set_sync_counter_enabled(true).await;
canopen.send_sync().await?; // Counter increments automatically
// Monitor SYNC reception
canopen.register_sync_callback(|counter| {
println!("SYNC received, counter: {:?}", counter);
}).await;
// High-frequency SYNC (100 Hz tested successfully)
let mut interval = tokio::time::interval(Duration::from_millis(10));
loop {
interval.tick().await;
canopen.send_sync().await?;
}Monitor device errors and warnings:
// Register emergency handler for specific node
canopen.register_emcy_handler(node_id, |emcy| {
println!("Emergency from node {}: 0x{:04X} - {}",
node_id,
emcy.error_code,
emcy.error_code_description()
);
println!("Error register: 0x{:02X}", emcy.error_register);
println!("Manufacturer data: {:02X?}", emcy.mfr_specific_data);
}).await;
// Get recent emergencies
let recent = canopen.get_recent_emcy(node_id, 10).await;
for emcy in recent {
println!("Error: {}", emcy.error_code_description());
}Node configuration and commissioning with support for discovering multiple devices:
// Switch to configuration mode (all unconfigured slaves)
canopen.lss_switch_state_global(LssMode::Configuration).await?;
// Single device inquiry (backward compatible - returns first response)
let vendor_id = canopen.lss_inquire_vendor_id(1000).await?;
let product_code = canopen.lss_inquire_product_code(1000).await?;
let revision = canopen.lss_inquire_revision_number(1000).await?;
let serial = canopen.lss_inquire_serial_number(1000).await?;
println!("Vendor ID: 0x{:08X}", vendor_id);
// Network discovery - collect responses from ALL unconfigured devices
let vendor_ids = canopen.lss_inquire_vendor_ids(2000).await?;
let product_codes = canopen.lss_inquire_product_codes(2000).await?;
let serial_numbers = canopen.lss_inquire_serial_numbers(2000).await?;
let node_ids = canopen.lss_inquire_node_ids(2000).await?;
println!("Discovered {} device(s):", vendor_ids.len());
for (i, serial) in serial_numbers.iter().enumerate() {
println!(" Device {}: Serial 0x{:08X}", i + 1, serial);
}
// Select specific slave
let address = LssAddress {
vendor_id,
product_code,
revision_number: revision,
serial_number: serial
};
canopen.lss_switch_state_selective(&address).await?;
// Configure node-ID (example - disabled by default for safety)
match canopen.lss_configure_node_id(10, 1000).await? {
LssError::Success => println!("Node-ID configured successfully"),
error => println!("Configuration failed: {}", error.description()),
}
// Store configuration to non-volatile memory
canopen.lss_store_configuration(1000).await?;
// Switch back to operation mode
canopen.lss_switch_state_global(LssMode::Operation).await?;Comprehensive examples are provided in the examples/ directory:
sdo_test.rs- SDO read/write operations with various data typessdo_timeout_test.rs- SDO error handling and timeout behaviornmt_test.rs- NMT state control and heartbeat monitoringpdo_test.rs- PDO transmission and reception with callbackssync_test.rs- SYNC message handling and high-frequency testingemcy_test.rs- Emergency message monitoring and parsinglss_test.rs- LSS commissioning workflow (10 test sections)lss_multi_response_test.rs- LSS network discovery with multiple devices
Run an example:
cargo run --release --example sdo_test
cargo run --release --example pdo_test
cargo run --release --example lss_multi_response_test- ✅ Phase 1-3: Project setup, types, hardware layer
- ✅ Phase 4: SDO client (expedited & segmented transfers)
- ✅ Phase 5: NMT state management
- ✅ Phase 6: Main library and PDO handling
- ✅ Phase 7: SYNC & EMCY protocols
- ✅ Phase 8: LSS (Layer Setting Services)
- ✅ Phase 9: Documentation and CI/CD pipeline
- Complete API documentation with examples
- All public APIs documented with doctests
- Comprehensive README with protocol examples
- GitHub Actions CI/CD pipeline
- ⏳ Phase 10: Advanced testing and benchmarking
- TIME protocol (timestamp synchronization)
- SRDO (Safety-related data objects)
- Block SDO transfers (high-speed bulk data)
- SocketCAN support for Linux
src/
├── lib.rs # Main library API and entry point
├── canopen/
│ ├── mod.rs # CANopen module exports
│ ├── message.rs # CAN message types and COB-ID definitions
│ ├── sdo.rs # SDO client with segmented transfer support
│ ├── nmt.rs # NMT state management and commands
│ ├── pdo.rs # PDO handling with callbacks
│ ├── sync.rs # SYNC message handling with counter
│ ├── emcy.rs # Emergency message processing
│ ├── lss.rs # LSS protocol implementation
│ ├── od.rs # Object Dictionary types
│ └── types.rs # CANopen data type conversions
├── hardware/
│ ├── mod.rs # Hardware abstraction trait
│ └── peak_can.rs # PEAK CAN adapter implementation
├── errors.rs # Comprehensive error types
└── utils.rs # Utility functions
examples/ # Comprehensive protocol examples
├── sdo_test.rs
├── nmt_test.rs
├── pdo_test.rs
├── sync_test.rs
├── emcy_test.rs
└── lss_test.rs
- Async/Await: All I/O operations are non-blocking using Tokio
- Message Passing: Channels (mpsc, broadcast) for inter-task communication
- Event-Driven: Callback system for PDO, heartbeat, SYNC, and EMCY
- Type Safety: Strong typing with enums for states, commands, and errors
- Thread Safety: Arc + RwLock for shared state across async tasks
Tested performance metrics on PEAK CAN USB at 1 Mbps:
- PDO throughput: 50+ messages/second sustained
- SYNC frequency: 100 Hz (10ms period) tested successfully
- SDO transfers: Segmented transfers >4 KB working correctly
- Latency: Sub-millisecond message processing in release mode
- PEAK CAN USB interfaces (PCAN-USB, PCAN-USB Pro, etc.)
- Requires PEAK CAN drivers installed
- Windows support via PCANBasic.dll
- Linux support planned (SocketCAN backend)
All standard CANopen bit rates supported:
- 1 Mbps (default, most common)
- 800 kbps
- 500 kbps
- 250 kbps
- 125 kbps
- 100 kbps
- 50 kbps
- 20 kbps
- 10 kbps
Comprehensive error types with context:
pub enum CANopenError {
Hardware(String), // CAN hardware errors
Sdo { code: u32 }, // SDO abort codes
Timeout, // Operation timeout
InvalidMessage, // Malformed CAN message
InvalidLength(usize), // Data length mismatch
NodeNotFound { node_id: u8 }, // Node not responding
ChannelClosed, // Internal channel error
NotInitialized, // Manager not initialized
// ... and more
}All async operations return Result<T> for proper error propagation.
Core dependencies:
peak-can-sys = "0.1.2"- PEAK CAN hardware interfacetokio- Async runtime with full featuresserde- Serialization with derive macroslog = "0.4"- Logging frameworkthiserror = "1.0"- Error handlingcrossbeam = "0.8"- Concurrent data structuresfutures = "0.3"- Async utilitiesbitflags = "2.0"- Bit flag operationsasync-trait = "0.1"- Async traits
cargo buildcargo build --release# Debug mode (faster to compile, slower to run)
cargo run --example sdo_test
# Release mode (optimized performance)
cargo run --release --example pdo_testcargo testCurrent Test Status: ✅ All 161 tests passing
- 150 unit tests (protocol implementation)
- 11 integration tests
- 17 doctests (+ 3 ignored)
CI/CD automatically runs tests on every push via GitHub Actions.
Enable logging with environment variables:
# Windows PowerShell
$env:RUST_LOG="debug"
cargo run --release --example sdo_test
# Linux/macOS
RUST_LOG=debug cargo run --release --example sdo_testLog levels: error, warn, info, debug, trace
PEAK CAN drivers not found
Solution: Install PEAK CAN drivers from https://www.peak-system.com/
Windows: Ensure PCANBasic.dll is in system PATH
Connection timeout
// Check bus speed matches your network
canopen.connect(BusSpeed::Baud1M).await?;
// Verify CAN termination resistors (120Ω at each end)
// Check physical connectionsSDO timeout
// Increase timeout (default 1000ms)
canopen.sdo_read_with_timeout(node_id, index, subindex, 5000).await?;
// Verify node is in Pre-Operational or Operational state
canopen.nmt_start(node_id).await?;Build errors
# Update Rust toolchain
rustup update
# Clean and rebuild
cargo clean
cargo build --releaseContributions welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure
cargo testpasses - Run
cargo fmtandcargo clippy - Submit a pull request
MIT License
Copyright (c) 2025 Sicris Rey Embay and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
See LICENSE file for full text.
- Original C# libCanopenSimple library
- PEAK System for CAN hardware and drivers
- Rust community for excellent async ecosystem
- CANopen specification (CiA 301, 305, and device profiles)
- peak-can-sys - PEAK CAN Rust bindings
- tokio - Async runtime for Rust
| Feature | Status | Notes |
|---|---|---|
| SDO Client | ✅ Complete | Expedited & segmented |
| NMT Management | ✅ Complete | All commands + heartbeat |
| PDO Handling | ✅ Complete | TX/RX with callbacks |
| SYNC Protocol | ✅ Complete | Counter support, 100Hz tested |
| EMCY Protocol | ✅ Complete | 40+ error codes |
| LSS Protocol | ✅ Complete | All 14 commands |
| PEAK CAN Support | ✅ Complete | All bit rates |
| Documentation | ✅ Complete | Comprehensive docs |
| Unit Tests | ✅ Complete | 161 tests passing |
| CI/CD Pipeline | ✅ Complete | GitHub Actions |
| TIME Protocol | 📋 Future | Optional |
| SRDO | 📋 Future | Optional |
| SocketCAN | 📋 Future | Linux support |
Made with ❤️ using Rust and AI assistance