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::VisibleString(s) => string_literal("VisibleString", s.as_bytes()),
92        Value::OctetString(s) => string_literal("OctetString", s.as_bytes()),
93        Value::Domain(s) => string_literal("Domain", s.as_bytes()),
94        // `Value` is #[non_exhaustive]; every current variant is handled above.
95        _ => panic!("codegen: unsupported value variant {value:?}"),
96    }
97}
98
99/// A Rust literal for a variable-length value: reconstruct the bounded
100/// [`ByteString`](canopen_rs::ByteString) from its bytes. The EDS parser already
101/// caps content at `MAX_STRING_LEN`, so `from_bytes` cannot fail at runtime.
102fn string_literal(variant: &str, bytes: &[u8]) -> String {
103    let mut list = String::new();
104    for (i, b) in bytes.iter().enumerate() {
105        if i > 0 {
106            list.push_str(", ");
107        }
108        let _ = write!(list, "{b:#04x}");
109    }
110    format!("canopen_rs::Value::{variant}(canopen_rs::ByteString::from_bytes(&[{list}]).unwrap())")
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use canopen_rs::{Address, ObjectDictionary};
117
118    const SAMPLE: &str = "\
119[1000]
120ParameterName=Device type
121DataType=0x0007
122AccessType=ro
123DefaultValue=0x00040192
124
125[1008]
126ParameterName=Device name
127DataType=0x0009
128AccessType=const
129DefaultValue=Widget
130
131[1017]
132ParameterName=Producer heartbeat time
133DataType=0x0006
134AccessType=rw
135DefaultValue=1000
136";
137
138    // The exact shape `generate_object_dictionary` emits for SAMPLE — kept here
139    // so the compiler proves the generated pattern is valid Rust that builds a
140    // correct object dictionary. If the generator's format changes, update both.
141    #[allow(clippy::needless_return)]
142    fn generated_sample() -> ObjectDictionary<3> {
143        let mut od = ObjectDictionary::new();
144        od.insert(
145            Address::new(0x1000, 0x00),
146            canopen_rs::Entry {
147                value: canopen_rs::Value::Unsigned32(0x00040192),
148                access: AccessType::Ro,
149            },
150        )
151        .unwrap();
152        od.insert(
153            Address::new(0x1008, 0x00),
154            canopen_rs::Entry {
155                value: canopen_rs::Value::VisibleString(
156                    canopen_rs::ByteString::from_bytes(&[0x57, 0x69, 0x64, 0x67, 0x65, 0x74])
157                        .unwrap(),
158                ),
159                access: AccessType::Const,
160            },
161        )
162        .unwrap();
163        od.insert(
164            Address::new(0x1017, 0x00),
165            canopen_rs::Entry {
166                value: canopen_rs::Value::Unsigned16(0x03e8),
167                access: AccessType::Rw,
168            },
169        )
170        .unwrap();
171        od
172    }
173
174    #[test]
175    fn generated_shape_builds_a_correct_od() {
176        let od = generated_sample();
177        assert_eq!(
178            od.read(Address::new(0x1000, 0)).unwrap(),
179            Value::Unsigned32(0x0004_0192)
180        );
181        assert_eq!(
182            od.read(Address::new(0x1017, 0)).unwrap(),
183            Value::Unsigned16(1000)
184        );
185        match od.read(Address::new(0x1008, 0)).unwrap() {
186            Value::VisibleString(bs) => assert_eq!(bs.as_str(), Some("Widget")),
187            other => panic!("expected VISIBLE_STRING, got {other:?}"),
188        }
189    }
190
191    #[test]
192    fn generator_emits_the_expected_source() {
193        let eds = Eds::parse(SAMPLE).unwrap();
194        let src = generate_object_dictionary(&eds, "device_od");
195
196        assert!(src.contains("pub fn device_od() -> canopen_rs::ObjectDictionary<3> {"));
197        assert!(src.contains(
198            "od.insert(canopen_rs::Address::new(0x1000, 0x00), canopen_rs::Entry { value: canopen_rs::Value::Unsigned32(0x00040192), access: canopen_rs::AccessType::Ro }).unwrap();"
199        ));
200        assert!(src.contains(
201            "od.insert(canopen_rs::Address::new(0x1017, 0x00), canopen_rs::Entry { value: canopen_rs::Value::Unsigned16(0x03e8), access: canopen_rs::AccessType::Rw }).unwrap();"
202        ));
203        // The VISIBLE_STRING object (the variant that used to panic codegen).
204        assert!(src.contains(
205            "canopen_rs::Value::VisibleString(canopen_rs::ByteString::from_bytes(&[0x57, 0x69, 0x64, 0x67, 0x65, 0x74]).unwrap())"
206        ));
207    }
208}