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//! reset_app: None,
111//! reset_comms: None,
112//! enter_operational: None,
113//! enter_stopped: None,
114//! enter_preoperational: None,
115//! };
116//!
117//!
118//! // Initialize node, providing references to the static objects created by `zencan-build`
119//! let mut node = Node::new(
120//! NodeId::Unconfigured,
121//! callbacks,
122//! &zencan::NODE_MBOX,
123//! &zencan::NODE_STATE,
124//! &zencan::OD_TABLE,
125//! );
126//! ```
127//!
128//! ## Handling CAN messages
129//!
130//! The application has to handle sending and receiving CAN messages.
131//!
132//! The NODE_MBOX struct acts as a mailbox for both incoming and outgoing mailboxes, and the
133//! application must pass messages between the mailbox and the CAN controller. This can be done in
134//! any thread -- a good way to do it is to have the CAN controller interrupt store messages here
135//! directly.
136//!
137//! ```ignore
138//! // Assuming we've received a message (id, and buffer) from somewhere, pass it to the mailbox
139//! let msg = zencan_node::common::messages::CanMessage::new(id, &buffer[..msg.len as usize]);
140//! // Ignore error -- as an Err is returned for messages that are not consumed by the node
141//! // stack. You may handle those some other way, or simply drop them.
142//! zencan::NODE_MBOX.store_message(msg).ok();
143//! ```
144//!
145//! Outgoing messages can be read from the mbox using the [`NodeMbox::next_transmit_message`]
146//! function. A callback can be registered (see [`NodeMbox::set_transmit_notify_callback`]) to be
147//! notified when new messages are queued for transmission -- this can be used to e.g. push the
148//! first message(s) to the CAN controller to initiate an IRQ driven transmit look, or to wake an
149//! async task which is responsible for moving messages from the node to the CAN controller.
150//!
151//! ```ignore
152//! #[embassy_executor::task]
153//! async fn twai_tx_task(mut twai_tx: TwaiTx<'static, Async>) {
154//! loop {
155//! while let Some(msg) = zencan::NODE_MBOX.next_transmit_message() {
156//! let frame =
157//! EspTwaiFrame::new(StandardId::new(msg.id.raw() as u16).unwrap(), msg.data())
158//! .unwrap();
159//! if let Err(e) = twai_tx.transmit_async(&frame).await {
160//! log::error!("Error sending CAN message: {e:?}");
161//! }
162//! }
163//!
164//! // Wait for wakeup signal when new CAN messages become ready for sending
165//! CANOPEN_TX_SIGNAL.wait().await;
166//! }
167//! }
168//! ```
169//!
170//! ## Calling periodic process method
171//!
172//! To execute the Node logic, the [`Node::process`] function must be called periodically. While it
173//! is possible to call process only periodically, the NODE_MBOX object provides a
174//! [callback](NodeMbox::set_process_notify_callback) which can be used to notify another task that
175//! process should be called when a message is received and requires processing.
176//!
177//! Here's an example of a lilos task which executes process when either CAN_NOTIFY is signals, or
178//! 10ms has passed since the last notification.
179//!
180//! ```ignore
181//! async fn can_task(
182//! mut node: Node,
183//! ) -> Infallible {
184//! let epoch = lilos::time::TickTime::now();
185//! loop {
186//! lilos::time::with_timeout(Duration::from_millis(10), CAN_NOTIFY.until_next()).await;
187//! let time_us = epoch.elapsed().0 * 1000;
188//! node.process(time_us);
189//! }
190//! }
191//! ```
192//!
193#![cfg_attr(all(not(test), not(feature = "std")), no_std)]
194#![warn(missing_docs, missing_debug_implementations)]
195#![allow(clippy::comparison_chain)]
196#![cfg_attr(docsrs, feature(doc_cfg))]
197
198mod bootloader;
199mod lss_slave;
200mod node;
201mod node_mbox;
202mod node_state;
203pub mod object_dict;
204pub mod pdo;
205mod persist;
206pub mod priority_queue;
207mod sdo_server;
208pub mod storage;
209
210// Re-export proc macros
211pub use zencan_macro::build_object_dict;
212
213// Re-export types used by generated code
214pub use critical_section;
215pub use zencan_common as common;
216
217pub use bootloader::{BootloaderInfo, BootloaderSection, BootloaderSectionCallbacks};
218#[cfg(feature = "socketcan")]
219#[cfg_attr(docsrs, doc(cfg(feature = "socketcan")))]
220pub use common::open_socketcan;
221pub use node::{Callbacks, Node};
222pub use node_mbox::NodeMbox;
223pub use node_state::NodeState;
224pub use persist::{restore_stored_comm_objects, restore_stored_objects};
225pub use sdo_server::SDO_BUFFER_SIZE;
226
227/// Include the code generated for the object dict in the build script.
228#[macro_export]
229macro_rules! include_modules {
230 ($name: tt) => {
231 include!(env!(concat!(
232 "ZENCAN_INCLUDE_GENERATED_",
233 stringify!($name),
234 )));
235 };
236}