Skip to main content

canopen_host/
codegen.rs

1//! Generate a compile-time object dictionary from an EDS/DCF file.
2//!
3//! [`generate_object_dictionary`](crate::codegen::generate_object_dictionary)
4//! emits Rust source for a function that builds a
5//! [`canopen_rs::ObjectDictionary`] populated from a parsed
6//! [`Eds`](crate::eds::Eds). Run it from a `build.rs` so a device's `.eds`
7//! becomes a typed, zero-runtime-parse OD:
8//!
9//! ```ignore
10//! // build.rs
11//! use canopen_host::{codegen::generate_object_dictionary, eds::Eds};
12//! use std::{env, fs, path::Path};
13//!
14//! fn main() {
15//!     let eds = Eds::from_file("device.eds").unwrap();
16//!     let src = generate_object_dictionary(&eds, "device_od");
17//!     let out = Path::new(&env::var("OUT_DIR").unwrap()).join("device_od.rs");
18//!     fs::write(out, src).unwrap();
19//!     println!("cargo:rerun-if-changed=device.eds");
20//! }
21//! ```
22//!
23//! ```ignore
24//! // in your crate:
25//! include!(concat!(env!("OUT_DIR"), "/device_od.rs"));
26//! let od = device_od();   // ObjectDictionary<N>, ready to serve
27//! ```
28
29use core::fmt::Write as _;
30
31use canopen_rs::{AccessType, Value};
32
33use crate::eds::Eds;
34
35/// Emit Rust source for a `pub fn <fn_name>() -> canopen_rs::ObjectDictionary<N>`
36/// that inserts every object in `eds`, with `N` sized to fit exactly.
37///
38/// The generated code refers to everything by absolute `canopen_rs::` path, so
39/// it compiles regardless of the including module's imports (the crate need only
40/// depend on `canopen-rs`).
41pub fn generate_object_dictionary(eds: &Eds, fn_name: &str) -> String {
42    let n = eds.objects.len();
43    let mut out = String::new();
44    let _ = writeln!(
45        out,
46        "/// Object dictionary generated from an EDS by canopen-host."
47    );
48    let _ = writeln!(
49        out,
50        "pub fn {fn_name}() -> canopen_rs::ObjectDictionary<{n}> {{"
51    );
52    let _ = writeln!(out, "    let mut od = canopen_rs::ObjectDictionary::new();");
53    for obj in &eds.objects {
54        let _ = writeln!(
55            out,
56            "    od.insert(canopen_rs::Address::new({:#06x}, {:#04x}), canopen_rs::Entry {{ value: {}, access: canopen_rs::AccessType::{} }}).unwrap();",
57            obj.address.index,
58            obj.address.subindex,
59            value_literal(&obj.default_value),
60            access_variant(obj.access),
61        );
62    }
63    let _ = writeln!(out, "    od");
64    let _ = writeln!(out, "}}");
65    out
66}
67
68fn access_variant(access: AccessType) -> &'static str {
69    match access {
70        AccessType::Ro => "Ro",
71        AccessType::Wo => "Wo",
72        AccessType::Rw => "Rw",
73        AccessType::Const => "Const",
74    }
75}
76
77/// A Rust literal expression for a [`Value`], fully qualified.
78fn value_literal(value: &Value) -> String {
79    match value {
80        Value::Boolean(b) => format!("canopen_rs::Value::Boolean({b})"),
81        Value::Unsigned8(x) => format!("canopen_rs::Value::Unsigned8({x:#04x})"),
82        Value::Unsigned16(x) => format!("canopen_rs::Value::Unsigned16({x:#06x})"),
83        Value::Unsigned32(x) => format!("canopen_rs::Value::Unsigned32({x:#010x})"),
84        Value::Unsigned64(x) => format!("canopen_rs::Value::Unsigned64({x:#018x})"),
85        Value::Integer8(x) => format!("canopen_rs::Value::Integer8({x})"),
86        Value::Integer16(x) => format!("canopen_rs::Value::Integer16({x})"),
87        Value::Integer32(x) => format!("canopen_rs::Value::Integer32({x})"),
88        Value::Integer64(x) => format!("canopen_rs::Value::Integer64({x})"),
89        Value::Real32(x) => format!("canopen_rs::Value::Real32({x:?}f32)"),
90        Value::Real64(x) => format!("canopen_rs::Value::Real64({x:?}f64)"),
91        // `Value` is #[non_exhaustive]; every current variant is handled above.
92        _ => panic!("codegen: unsupported value variant {value:?}"),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use canopen_rs::{Address, ObjectDictionary};
100
101    const SAMPLE: &str = "\
102[1000]
103ParameterName=Device type
104DataType=0x0007
105AccessType=ro
106DefaultValue=0x00040192
107
108[1017]
109ParameterName=Producer heartbeat time
110DataType=0x0006
111AccessType=rw
112DefaultValue=1000
113";
114
115    // The exact shape `generate_object_dictionary` emits for SAMPLE — kept here
116    // so the compiler proves the generated pattern is valid Rust that builds a
117    // correct object dictionary. If the generator's format changes, update both.
118    #[allow(clippy::needless_return)]
119    fn generated_sample() -> ObjectDictionary<2> {
120        let mut od = ObjectDictionary::new();
121        od.insert(
122            Address::new(0x1000, 0x00),
123            canopen_rs::Entry {
124                value: canopen_rs::Value::Unsigned32(0x00040192),
125                access: AccessType::Ro,
126            },
127        )
128        .unwrap();
129        od.insert(
130            Address::new(0x1017, 0x00),
131            canopen_rs::Entry {
132                value: canopen_rs::Value::Unsigned16(0x03e8),
133                access: AccessType::Rw,
134            },
135        )
136        .unwrap();
137        od
138    }
139
140    #[test]
141    fn generated_shape_builds_a_correct_od() {
142        let od = generated_sample();
143        assert_eq!(
144            od.read(Address::new(0x1000, 0)).unwrap(),
145            Value::Unsigned32(0x0004_0192)
146        );
147        assert_eq!(
148            od.read(Address::new(0x1017, 0)).unwrap(),
149            Value::Unsigned16(1000)
150        );
151    }
152
153    #[test]
154    fn generator_emits_the_expected_source() {
155        let eds = Eds::parse(SAMPLE).unwrap();
156        let src = generate_object_dictionary(&eds, "device_od");
157
158        assert!(src.contains("pub fn device_od() -> canopen_rs::ObjectDictionary<2> {"));
159        assert!(src.contains(
160            "od.insert(canopen_rs::Address::new(0x1000, 0x00), canopen_rs::Entry { value: canopen_rs::Value::Unsigned32(0x00040192), access: canopen_rs::AccessType::Ro }).unwrap();"
161        ));
162        assert!(src.contains(
163            "od.insert(canopen_rs::Address::new(0x1017, 0x00), canopen_rs::Entry { value: canopen_rs::Value::Unsigned16(0x03e8), access: canopen_rs::AccessType::Rw }).unwrap();"
164        ));
165    }
166}