zencan_node/lib.rs
1//! A library to implement a CANOpen node in Rust
2//!
3//! Zencan-node is a library to implement CAN communications for an embedded node, using the CANOpen
4//! protocol. It is primarily intended to be run on microcontrollers, and so it is no_std compatible
5//! and performs no heap allocation, instead statically allocating storage. It is also possible to
6//! use it on std environments, for example on linux using socketcan. It provides the following
7//! features:
8//!
9//! * Implements the *LSS* protocol for node discovery and configuration.
10//! * Implements the *NMT* protocol for reporting and controlling the operating state of nodes.
11//! * Generates an *object dictionary* to represent all of the data which can be communicated on the
12//! bus. This includes a number of standard communication objects, as well as application specific
13//! objects specified by the user.
14//! * Implements an *SDO* server, allowing a remote client to access objects in the dictionary.
15//! * Implements transmit and receive PDOs, allowing the mapping of objects to user-specified CAN
16//! IDs for reading and writing those objects.
17//! * Provides callback hooks to allow for persistent storage of selected object values on command.
18//!
19//! # Getting Started
20//!
21//! ## Device Configuration
22//!
23//! A zencan node is configured using a [DeviceConfig](common::device_config::DeviceConfig) TOML
24//! file, see [common::device_config] module docs for more info.
25//!
26//! ## Code Generation
27//!
28//! The device configuration is used to generate types and static instances for each object in the
29//! object dictionary, as well as some additional objects like a [NodeMbox], and [NodeState].
30//!
31//! ### Add zencan-build as build dependency
32//!
33//! This crate contains functions to generate the object dictionary code from the device config TOML
34//! file.
35//!
36//! ```toml
37//! [build-dependencies]
38//! zencan-build = "0.0.1"
39//! ```
40//!
41//! ### Add the code generation to your `build.rs` file
42//!
43//! ```ignore
44//! fn main() {
45//! if let Err(e) = zencan_build::build_node_from_device_config("ZENCAN_CONFIG", "zencan_config.toml") {
46//! eprintln!("Failed to parse zencan_config.toml: {}", e.to_string());
47//! std::process::exit(-1);
48//! }
49//! }
50//! ```
51//!
52//! ### Include the generated code in your application
53//!
54//! When including the code, it is included using the name specified in build -- `ZENCAN_CONFIG` in
55//! this case. This allows creating multiple object dictionaries in a single application.
56//!
57//! Typically, an application would add a snippet like this into `main.rs`:
58//!
59//! ```ignore
60//! mod zencan {
61//! zencan_node::include_modules!(ZENCAN_CONFIG);
62//! }
63//! ```
64//!
65//! ## Instantiating the [`Node`] object
66//!
67//! ### Object setup
68//!
69//! One of the first things you should do before instantiating a node is set the serial number on
70//! the 0x1018 object. Devices are identified by an "identity" that includes the vendor, product,
71//! revision, and a serial number. If you are going to put more than one of a particular device type
72//! on a network, each should somehow come up with a unique serial. Most MCUs will have a unique ID
73//! register which can be used for this purpose.
74//!
75//! ```ignore
76//! // Use the UID register to set a unique serial number
77//! zencan::OBJECT1018.set_serial(get_serial());
78//! ```
79//!
80//! ### Node Creation
81//!
82//!
83//! Instantiate the node by providing it with the node ID, a set of event callbacks, the object
84//! dictionary, the mailbox, and the node state object.
85//!
86//! - Node ID: This is the ID boot up ID of the node. It can be stored in flash, it can be a
87//! constant, it can be set by DIP switches, etc. It can also be left as `NodeId::Unconfigured`.
88//! It is then possible to configure the node ID over the bus using the LSS protocol.
89//! - Object dictionary: This is a table where all of the objects are stored. It is created as a
90//! static variable by `zencan-build`, and is called `OD_TABLE`.
91//! - Mailbox: This is a data structure for receiving incoming CAN messages. It buffers received
92//! messages so that messages can be pass to it in an interrupt, and then processed in the next
93//! call to `process`. It is defined by the generated code in a static variable named `NODE_MBOX`.
94//! - Node state: This is a global state structure which provides some communications between the
95//! Node and objects such as PDO configuration objects, or special purpose object like the Save
96//! Command object. It is defined by the generated code in a static variable named `NODE_STATE`.
97//!
98//! There are a variety of callback functions you may provide as well, although they are not
99//! required.
100//!
101//!
102//! ```ignore
103//! // Get references to the functions which save to flash
104//! let store_node_config = &mut store_node_config;
105//! let store_objects = &mut store_objects;
106//!
107//! let callbacks = Callbacks {
108//! store_node_config: Some(store_node_config),
109//! store_objects: Some(store_objects),
110//! ..Default::default()
111//! };
112//!
113//!
114//! // Initialize node, providing references to the static objects created by `zencan-build`
115//! let mut node = Node::new(
116//! NodeId::Unconfigured,
117//! callbacks,
118//! &zencan::NODE_MBOX,
119//! &zencan::NODE_STATE,
120//! &zencan::OD_TABLE,
121//! );
122//! ```
123//!
124//! ## Handling CAN messages
125//!
126//! The application has to handle sending and receiving CAN messages.
127//!
128//! The NODE_MBOX struct acts as a mailbox for both incoming and outgoing mailboxes, and the
129//! application must pass messages between the mailbox and the CAN controller. This can be done in
130//! any thread -- a good way to do it is to have the CAN controller interrupt store messages here
131//! directly.
132//!
133//! ```ignore
134//! // Assuming we've received a message (id, and buffer) from somewhere, pass it to the mailbox
135//! let msg = zencan_node::common::messages::CanMessage::new(id, &buffer[..msg.len as usize]);
136//! // Ignore error -- as an Err is returned for messages that are not consumed by the node
137//! // stack. You may handle those some other way, or simply drop them.
138//! zencan::NODE_MBOX.store_message(msg).ok();
139//! ```
140//!
141//! Outgoing messages can be read from the mbox using the [`NodeMbox::next_transmit_message`]
142//! function. A callback can be registered (see [`NodeMbox::set_transmit_notify_callback`]) to be
143//! notified when new messages are queued for transmission -- this can be used to e.g. push the
144//! first message(s) to the CAN controller to initiate an IRQ driven transmit look, or to wake an
145//! async task which is responsible for moving messages from the node to the CAN controller.
146//!
147//! ```ignore
148//! #[embassy_executor::task]
149//! async fn twai_tx_task(mut twai_tx: TwaiTx<'static, Async>) {
150//! loop {
151//! while let Some(msg) = zencan::NODE_MBOX.next_transmit_message() {
152//! let frame =
153//! EspTwaiFrame::new(StandardId::new(msg.id.raw() as u16).unwrap(), msg.data())
154//! .unwrap();
155//! if let Err(e) = twai_tx.transmit_async(&frame).await {
156//! log::error!("Error sending CAN message: {e:?}");
157//! }
158//! }
159//!
160//! // Wait for wakeup signal when new CAN messages become ready for sending
161//! CANOPEN_TX_SIGNAL.wait().await;
162//! }
163//! }
164//! ```
165//!
166//! ## Calling periodic process method
167//!
168//! To execute the Node logic, the [`Node::process`] function must be called periodically. While it
169//! is possible to call process only periodically, the NODE_MBOX object provides a
170//! [callback](NodeMbox::set_process_notify_callback) which can be used to notify another task that
171//! process should be called when a message is received and requires processing.
172//!
173//! Here's an example of a lilos task which executes process when either CAN_NOTIFY is signals, or
174//! 10ms has passed since the last notification.
175//!
176//! ```ignore
177//! async fn can_task(
178//! mut node: Node,
179//! ) -> Infallible {
180//! let epoch = lilos::time::TickTime::now();
181//! loop {
182//! lilos::time::with_timeout(Duration::from_millis(10), CAN_NOTIFY.until_next()).await;
183//! let time_us = epoch.elapsed().0 * 1000;
184//! node.process(time_us);
185//! }
186//! }
187//! ```
188//!
189#![cfg_attr(all(not(test), not(feature = "std")), no_std)]
190#![warn(missing_docs, missing_debug_implementations)]
191#![allow(clippy::comparison_chain)]
192#![cfg_attr(docsrs, feature(doc_cfg))]
193
194mod bootloader;
195mod lss_slave;
196mod node;
197mod node_mbox;
198mod node_state;
199pub mod object_dict;
200pub mod pdo;
201mod persist;
202pub mod priority_queue;
203mod sdo_server;
204pub mod storage;
205
206// Re-export proc macros
207pub use zencan_macro::build_object_dict;
208
209// Re-export types used by generated code
210pub use critical_section;
211pub use embedded_io;
212pub use zencan_common as common;
213
214pub use bootloader::{BootloaderInfo, BootloaderSection, BootloaderSectionCallbacks};
215#[cfg(all(feature = "socketcan", target_os = "linux"))]
216#[cfg_attr(docsrs, doc(all(feature = "socketcan", target_os = "linux")))]
217pub use common::open_socketcan;
218pub use node::{Callbacks, Node};
219pub use node_mbox::NodeMbox;
220pub use node_state::NodeState;
221pub use persist::{restore_stored_comm_objects, restore_stored_objects};
222pub use sdo_server::SDO_BUFFER_SIZE;
223
224/// Include the code generated for the object dict in the build script.
225#[macro_export]
226macro_rules! include_modules {
227 ($name: tt) => {
228 include!(env!(concat!(
229 "ZENCAN_INCLUDE_GENERATED_",
230 stringify!($name),
231 )));
232 };
233}