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
4//! node, using the CANOpen protocol. It is primarily intended to be run on
5//! microcontrollers, and so it is no_std compatible and performs no heap
6//! allocation, instead statically allocating storage. It is also possible to
7//! use it on std environments, for example on linux using socketcan. It
8//! provides the following features:
9//!
10//! * Implements the *LSS* protocol for node discovery and configuration.
11//! * Implements the *NMT* protocol for reporting and controlling the operating
12//! state of nodes.
13//! * Generates an *object dictionary* to represent all of the data which can be
14//! communicated on the bus. This includes a number of standard communication
15//! objects, as well as application specific objects specified by the user.
16//! * Implements an *SDO* server, allowing a remote client to access objects in
17//! the dictionary.
18//! * Implements transmit and receive PDOs, allowing the mapping of objects to
19//! user-specified CAN IDs for reading and writing those objects..
20//! * Provides callback hooks to allow for persistent storage of selected object
21//! values on command.
22//!
23//! # Getting Started
24//!
25//! ## Device Configuration
26//!
27//! A zencan node is configured using a
28//! [DeviceConfig](common::device_config::DeviceConfig) TOML file, see
29//! [common::device_config] module docs for more info.
30//!
31//! ## Code Generation
32//!
33//! The device configuration is used to generate types and static instances for
34//! each object in the object dictionary, as well as some additional objects
35//! like a [NodeMbox], and [NodeState].
36//!
37//! ### Add zencan-build as build dependency
38//!
39//! This crate contains functions to generate the object dictionary code from
40//! the device config TOML file.
41//!
42//! ```toml
43//! [build-dependencies]
44//! zencan-build = "0.0.1"
45//! ```
46//!
47//! ### Add the code generation to your `build.rs` file
48//!
49//! ```ignore
50//! fn main() {
51//! if let Err(e) = zencan_build::build_node_from_device_config("ZENCAN_CONFIG", "zencan_config.toml") {
52//! eprintln!("Failed to parse zencan_config.toml: {}", e.to_string());
53//! std::process::exit(-1);
54//! }
55//! }
56//! ```
57//!
58//! ### Include the generated code in your application
59//!
60//! When including the code, it is included using the name specified in build --
61//! `ZENCAN_CONFIG` in this case. This allows creating multiple object
62//! dictionaries in a single applicaation.
63//!
64//! Typically, an application would add a snippet like this into `main.rs`:
65//!
66//! ```ignore
67//! mod zencan {
68//! zencan_node::include_modules!(ZENCAN_CONFIG);
69//! }
70//! ```
71//!
72//! ## Instantiating the [`Node`] object
73//!
74//! ### Object setup
75//!
76//! Before instantiating the node, you should do any setup of objects that your
77//! applications needs. In particular, you should set the serial number on
78//! Object 0x1018. Now is also a good time to load any persisted object values,
79//! if you have them.
80//!
81//! ### Node Creation
82//!
83//! Instantiate the node by providing it with the OD, the mailbox, and the node
84//! state object, all of which were created by `zencan-build`. You also must
85//! provide a NodeId. You may provide a node ID which has been saved to flash,
86//! or a hard-coded ID, or you can provide
87//! [`NodeId::Unconfigured`](common::NodeId::Unconfigured), in which case the
88//! node will not be fully operational until it is assigned an ID, but it will
89//! respond to LSS commands for discovery and ID assignment.
90//!
91//!
92//! The node object has to be created in two steps, using the statics created by
93//! the `include_modules!`. The first step initializes the object dictionary --
94//! mainly it registers object callbacks so that the callback objects such as
95//! PDO config objects can be written to. The second step instantiates the node
96//! and latches some of the configuration from the object dictionary. In between
97//! these two steps is where the application should make sure that any run-time
98//! loaded object values are stored -- for example this is the time to read back
99//! any object values which have been stored in flash, or to configure the
100//! device serial number.
101//!
102//! ```ignore
103//! // Read saved node ID from flash
104//! let node_id = read_saved_node_id(&mut flash).unwrap_of(NodeId::Unconfigured);
105//!
106//! // Use the UID register to set a unique serial number
107//! zencan::OBJECT1018.set_serial(get_serial());
108//!
109//! // Restore object values from a previous save. The source data is the slice of bytes provided by
110//! // the node storage callback. The application is responsible for storing this somewhere
111//! // (e.g. flash) and restoring it later.
112//! let serialized_object_data: &[u8] = get_object_data();
113//! restore_stored_objects(&zencan::OD_TABLE, serialized_object_data);
114//!
115//! // Initialize node, providing references to the static objects created by `zencan-build`
116//! let mut node = Node::new(
117//! node_id,
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//! Received messages should be passed to the `NODE_MBOX` struct. This can be
129//! done in any thread -- a good way to do it is to have the CAN controller
130//! receive interrupt store messages here directly.
131//!
132//! ```ignore
133//! let msg = zencan_node::common::messages::CanMessage::new(id, &buffer[..msg.len as usize]);
134//! // Ignore error -- as an Err is returned for messages that are not consumed by the node
135//! // stack
136//! zencan::NODE_MBOX.store_message(msg).ok();
137//! ```
138//!
139//! To execute the Node logic, the [`Node::process`] function must be called
140//! periodically. It is provided a callback for transmitting messages. While it
141//! is possible to call process only periodically, the NODE_MBOX object provides
142//! a callback which can be used to notify another task that process should be
143//! called when a message is received and requires processing.
144//!
145//! Here's an example of a lilos task which executes process when either
146//! CAN_NOTIFY is signals, or 10ms has passed since the last notification.
147//!
148//! ```ignore
149//! async fn can_task(
150//! mut node: Node,
151//! mut can_tx: fdcan::Tx<FdCan1, NormalOperationMode>,
152//! ) -> Infallible {
153//! let epoch = lilos::time::TickTime::now();
154//! loop {
155//! lilos::time::with_timeout(Duration::from_millis(10), CAN_NOTIFY.until_next()).await;
156//! let time_us = epoch.elapsed().0 * 1000;
157//! node.process(time_us, &mut |msg| {
158//! let header = zencan_to_fdcan_header(&msg);
159//! if let Err(_) = can_tx.transmit(header, msg.data()) {
160//! defmt::error!("Error transmitting CAN message");
161//! }
162//! });
163//! }
164//! }
165//! ```
166//!
167//! ## Register callbacks
168//!
169//! The application can register callbacks for persistently storing data, or
170//! notifying the processing task. See examples for more info.
171//!
172#![cfg_attr(all(not(test), not(feature = "std")), no_std)]
173#![warn(missing_docs, missing_debug_implementations)]
174#![allow(clippy::comparison_chain)]
175#![cfg_attr(docsrs, feature(doc_cfg))]
176
177mod bootloader;
178mod lss_slave;
179mod node;
180mod node_mbox;
181mod node_state;
182pub mod object_dict;
183pub mod pdo;
184mod persist;
185mod sdo_server;
186pub mod storage;
187
188// Re-export proc macros
189pub use zencan_macro::build_object_dict;
190
191// Re-export types used by generated code
192pub use critical_section;
193pub use zencan_common as common;
194
195pub use bootloader::{BootloaderInfo, BootloaderSection, BootloaderSectionCallbacks};
196#[cfg(feature = "socketcan")]
197#[cfg_attr(docsrs, doc(cfg(feature = "socketcan")))]
198pub use common::open_socketcan;
199pub use node::Node;
200pub use node_mbox::NodeMbox;
201pub use node_state::{NodeState, NodeStateAccess};
202pub use persist::restore_stored_objects;
203pub use sdo_server::SDO_BUFFER_SIZE;
204
205/// Include the code generated for the object dict in the build script.
206#[macro_export]
207macro_rules! include_modules {
208 ($name: tt) => {
209 include!(env!(
210 concat!("ZENCAN_INCLUDE_GENERATED_", stringify!($name),),
211 concat!(
212 "Missing env var ",
213 "ZENCAN_INCLUDE_GENERATED_",
214 stringify!($name),
215 ". Did you generate an object dictionary named ",
216 stringify!($name),
217 " in build.rs?"
218 )
219 ));
220 };
221}