rplc/builder/
config.rs

1use crate::io::Kind;
2use eva_common::value::Value;
3use indexmap::IndexMap;
4use inflector::Inflector;
5use serde::Deserialize;
6use std::error::Error;
7use std::fmt::Write as _;
8use std::fs;
9use std::path::Path;
10
11#[derive(Deserialize, Debug)]
12#[serde(deny_unknown_fields)]
13pub struct Config {
14    version: u16,
15    #[serde(default)]
16    pub(crate) core: CoreConfig,
17    #[serde(default)]
18    context: ContextConfig,
19    #[cfg(feature = "eva")]
20    #[serde(default)]
21    pub(crate) eapi: EapiConfig,
22    #[serde(default)]
23    io: Vec<Io>,
24    #[serde(default)]
25    server: Vec<ServerConfig>,
26}
27
28fn default_stop_timeout() -> f64 {
29    crate::DEFAULT_STOP_TIMEOUT
30}
31
32#[derive(Deserialize, Debug)]
33#[serde(deny_unknown_fields)]
34pub(crate) struct CoreConfig {
35    #[serde(default = "default_stop_timeout")]
36    pub(crate) stop_timeout: f64,
37    #[serde(default)]
38    pub(crate) stack_size: Option<usize>,
39}
40
41impl Default for CoreConfig {
42    fn default() -> Self {
43        Self {
44            stop_timeout: default_stop_timeout(),
45            stack_size: None,
46        }
47    }
48}
49
50#[cfg(feature = "eva")]
51#[inline]
52fn default_eapi_action_pool_size() -> usize {
53    1
54}
55
56#[cfg(feature = "eva")]
57#[derive(Deserialize, Debug)]
58pub(crate) struct EapiConfig {
59    #[serde(default = "default_eapi_action_pool_size")]
60    pub(crate) action_pool_size: usize,
61}
62
63#[cfg(feature = "eva")]
64impl Default for EapiConfig {
65    fn default() -> Self {
66        Self {
67            action_pool_size: default_eapi_action_pool_size(),
68        }
69    }
70}
71
72#[derive(Deserialize, Debug)]
73#[serde(deny_unknown_fields)]
74struct ServerConfig {
75    kind: crate::server::Kind,
76    #[allow(dead_code)]
77    config: Value,
78}
79
80#[derive(Deserialize, Default, Debug)]
81#[serde(deny_unknown_fields)]
82struct ContextConfig {
83    #[serde(default)]
84    serialize: bool,
85    #[cfg(feature = "modbus")]
86    #[serde(default)]
87    modbus: Option<ModbusConfig>,
88    #[serde(default)]
89    fields: IndexMap<String, ContextField>,
90}
91
92#[cfg(feature = "modbus")]
93#[derive(Deserialize, Default, Debug)]
94#[serde(deny_unknown_fields)]
95pub(crate) struct ModbusConfig {
96    #[serde(default)]
97    pub(crate) c: usize,
98    #[serde(default)]
99    pub(crate) d: usize,
100    #[serde(default)]
101    pub(crate) i: usize,
102    #[serde(default)]
103    pub(crate) h: usize,
104}
105
106#[cfg(feature = "modbus")]
107impl ModbusConfig {
108    pub(crate) fn as_const_generics(&self) -> String {
109        format!("{}, {}, {}, {}", self.c, self.d, self.i, self.h)
110    }
111}
112
113#[derive(Deserialize, Debug)]
114#[serde(deny_unknown_fields)]
115struct Io {
116    id: String,
117    kind: Kind,
118    #[allow(dead_code)]
119    #[serde(default)]
120    config: Value,
121    #[allow(dead_code)]
122    #[serde(default)]
123    input: Vec<Value>,
124    #[allow(dead_code)]
125    #[serde(default)]
126    output: Vec<Value>,
127}
128
129#[derive(Deserialize, Debug)]
130#[serde(untagged)]
131enum ContextField {
132    Map(IndexMap<String, ContextField>),
133    Type(String),
134}
135
136impl Config {
137    pub fn load<P: AsRef<Path>>(path: P, context: &tera::Context) -> Result<Self, Box<dyn Error>> {
138        let config_tpl = fs::read_to_string(path)?;
139        let config: Config =
140            serde_yaml::from_str(&tera::Tera::default().render_str(&config_tpl, context)?)?;
141        if config.version != 1 {
142            unimplemented!("config version {} is not supported", config.version);
143        }
144        Ok(config)
145    }
146    #[allow(unreachable_code)]
147    pub fn generate_io<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn Error>> {
148        let mut m = codegen::Scope::new();
149        m.raw(crate::builder::AUTO_GENERATED);
150        m.raw("#[allow(unused_imports)]");
151        m.raw("use crate::plc::context::{Context, CONTEXT};");
152        #[allow(unused_mut)]
153        let mut funcs: Vec<String> = Vec::new();
154        //let mut output_required: bool = false;
155        for i in &self.io {
156            match i.kind {
157                #[cfg(feature = "modbus")]
158                Kind::Modbus => {
159                    m.raw(
160                        crate::io::modbus::generate_io(&i.id, &i.config, &i.input, &i.output)?
161                            .to_string(),
162                    );
163                }
164                #[cfg(feature = "opcua")]
165                Kind::OpcUa => {
166                    m.raw(
167                        crate::io::opcua::generate_io(&i.id, &i.config, &i.input, &i.output)?
168                            .to_string(),
169                    );
170                }
171                #[cfg(feature = "eva")]
172                Kind::Eapi => {
173                    m.raw(
174                        crate::io::eapi::generate_io(&i.id, &i.config, &i.input, &i.output)?
175                            .to_string(),
176                    );
177                }
178            }
179            funcs.push(format!("launch_datasync_{}", i.id.to_lowercase()));
180        }
181        let f_launch_datasync = m.new_fn("launch_datasync").vis("pub");
182        for function in funcs {
183            f_launch_datasync.line(format!("{}();", function));
184        }
185        #[allow(unused_variables)]
186        for (i, serv) in self.server.iter().enumerate() {
187            match serv.kind {
188                #[cfg(feature = "modbus")]
189                crate::server::Kind::Modbus => {
190                    f_launch_datasync.push_block(crate::server::modbus::generate_server_launcher(
191                        i + 1,
192                        &serv.config,
193                        self.context
194                            .modbus
195                            .as_ref()
196                            .expect("modbus not specified in PLC context"),
197                    )?);
198                }
199            }
200        }
201        let f_stop_datasync = m.new_fn("stop_datasync").vis("pub");
202        f_stop_datasync.line("::rplc::tasks::stop_if_no_output_or_sfn();");
203        super::write(path, m.to_string())?;
204        Ok(())
205    }
206    pub fn generate_context<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn Error>> {
207        let mut b = path.as_ref().to_path_buf();
208        b.pop();
209        b.pop();
210        let mut bm = b.clone();
211        b.push("plc_types.rs");
212        bm.push("plc_types");
213        bm.push("mod.rs");
214        let mut m = codegen::Scope::new();
215        m.raw(crate::builder::AUTO_GENERATED);
216        if b.exists() || bm.exists() {
217            m.raw("#[allow(clippy::wildcard_imports)]");
218            m.raw("use crate::plc_types::*;");
219        }
220        m.import("::rplc::export::parking_lot", "RwLock");
221        m.import("::rplc::export::once_cell::sync", "Lazy");
222        if self.context.serialize {
223            m.import("::rplc::export::serde", "Serialize");
224            m.import("::rplc::export::serde", "Deserialize");
225            m.import("::rplc::export::serde", "self");
226        }
227        m.raw("#[allow(dead_code)] pub(crate) static CONTEXT: Lazy<RwLock<Context>> = Lazy::new(<_>::default);");
228        generate_structs(
229            "Context",
230            &self.context.fields,
231            &mut m,
232            #[cfg(feature = "modbus")]
233            self.context.modbus.as_ref(),
234            self.context.serialize,
235        )?;
236        super::write(path, m.to_string())?;
237        Ok(())
238    }
239}
240
241fn parse_iec_type(tp: &str) -> &str {
242    match tp {
243        "BOOL" => "bool",
244        "BYTE" | "USINT" => "u8",
245        "WORD" | "UINT" => "u16",
246        "DWORD" | "UDINT" => "u32",
247        "LWORD" | "ULINT" => "u64",
248        "SINT" => "i8",
249        "INT" => "i16",
250        "DINT" => "i32",
251        "LINT" => "i64",
252        "REAL" => "f32",
253        "LREAL" => "f64",
254        _ => tp,
255    }
256}
257
258fn base_val(tp: &str) -> Option<&str> {
259    match tp {
260        "bool" => Some("false"),
261        "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" => {
262            Some("0")
263        }
264        "f32" | "f64" => Some("0.0"),
265        _ => None,
266    }
267}
268
269fn parse_type(t: &str) -> String {
270    let tp = t.trim();
271    if tp.ends_with(']') && !tp.starts_with('[') {
272        let mut sp = tp.split('[');
273        let mut result = String::new();
274        let base_tp = parse_iec_type(sp.next().unwrap());
275        for d in sp {
276            let mut size_s = d[0..d.len() - 1].trim().replace('_', "");
277            let boxed = if size_s.ends_with('!') {
278                size_s = size_s[..size_s.len() - 1].to_owned();
279                true
280            } else {
281                false
282            };
283            let size = size_s
284                .parse::<usize>()
285                .unwrap_or_else(|e| panic!("{e}: {size_s}"));
286            if result.is_empty() {
287                if boxed {
288                    write!(result, "Box<[{}; {}]>", base_tp, size).unwrap();
289                } else {
290                    write!(result, "[{}; {}]", base_tp, size).unwrap();
291                }
292            } else if boxed {
293                result = format!("Box<[{}; {}]>", result, size);
294            } else {
295                result = format!("[{}; {}]", result, size);
296            }
297        }
298        result
299    } else {
300        parse_iec_type(tp).to_owned()
301    }
302}
303
304fn generate_default(t: &str) -> String {
305    let tp = t.trim();
306    if tp.ends_with(']') && !tp.starts_with('[') {
307        let mut sp = tp.split('[');
308        let mut result = String::new();
309        let base_tp = parse_iec_type(sp.next().unwrap());
310        for d in sp {
311            let mut size_s = d[0..d.len() - 1].trim().replace('_', "");
312            let boxed = if size_s.ends_with('!') {
313                size_s = size_s[..size_s.len() - 1].to_owned();
314                true
315            } else {
316                false
317            };
318            let size = size_s
319                .parse::<usize>()
320                .unwrap_or_else(|e| panic!("{e}: {size_s}"));
321            if result.is_empty() {
322                if boxed {
323                    write!(result, "Box::new(").unwrap();
324                }
325                if let Some(val) = base_val(base_tp) {
326                    write!(result, "[{};{}]", val, size).unwrap();
327                } else {
328                    write!(result, "::std::array::from_fn(|_| <_>::default())").unwrap();
329                }
330                if boxed {
331                    write!(result, ")").unwrap();
332                }
333            } else {
334                let mut r = if boxed {
335                    "Box::new([".to_owned()
336                } else {
337                    "[".to_owned()
338                };
339                for _ in 0..size {
340                    write!(r, "{},", result).unwrap();
341                }
342                write!(r, "]").unwrap();
343                if boxed {
344                    write!(r, ")").unwrap();
345                }
346                result = r;
347            }
348        }
349        result
350    } else {
351        "<_>::default()".to_owned()
352    }
353}
354
355#[allow(clippy::too_many_lines)]
356fn generate_structs(
357    name: &str,
358    fields: &IndexMap<String, ContextField>,
359    scope: &mut codegen::Scope,
360    #[cfg(feature = "modbus")] modbus_config: Option<&ModbusConfig>,
361    serialize: bool,
362) -> Result<(), Box<dyn Error>> {
363    let mut st: codegen::Struct = codegen::Struct::new(name);
364    let mut st_impl: codegen::Impl = codegen::Impl::new(name);
365    st_impl.impl_trait("Default");
366    let default = st_impl.new_fn("default");
367    default.ret("Self");
368    default.line("Self {");
369    st.allow("dead_code")
370        .allow("clippy::module_name_repetitions")
371        .repr("C")
372        .vis("pub");
373    if serialize {
374        st.derive("Serialize").derive("Deserialize");
375        st.attr("serde(crate = \"self::serde\")");
376    }
377    for (k, v) in fields {
378        match v {
379            ContextField::Type(t) => {
380                let mut field = codegen::Field::new(k, parse_type(t));
381                field.vis("pub");
382                if serialize {
383                    field.annotation.push("#[serde(default)]".to_owned());
384                }
385                default.line(format!("{}: {},", field.name, generate_default(t)));
386                st.push_field(field);
387            }
388            ContextField::Map(m) => {
389                let (mut field, sub_name) = if k.ends_with(']') {
390                    let (number, field_name, boxed) = if let Some(pos) = k.rfind('[') {
391                        let mut size_s = k[pos + 1..k.len() - 1].trim().replace('_', "");
392                        let boxed = if size_s.ends_with('!') {
393                            size_s = size_s[..size_s.len() - 1].to_owned();
394                            true
395                        } else {
396                            false
397                        };
398                        let field = size_s.parse::<usize>().map_err(|e| {
399                            eva_common::Error::invalid_params(format!(
400                                "invalid struct name: {} ({})",
401                                k, e
402                            ))
403                        })?;
404                        (field, &k[..pos], boxed)
405                    } else {
406                        return Err(eva_common::Error::invalid_params(format!(
407                            "invalid struct name: {}",
408                            k
409                        ))
410                        .into());
411                    };
412                    let sub_name = format!("{}{}", name, field_name.to_title_case());
413                    let field_value = if boxed {
414                        format!("Box<[{}; {}]>", sub_name, number)
415                    } else {
416                        format!("[{}; {}]", sub_name, number)
417                    };
418                    (codegen::Field::new(field_name, field_value), sub_name)
419                } else {
420                    let sub_name = format!("{}{}", name, k.to_title_case());
421                    (codegen::Field::new(k, &sub_name), sub_name)
422                };
423                field.vis("pub");
424                if serialize {
425                    field.annotation.push("#[serde(default)]".to_owned());
426                }
427                default.line(format!("{}: {},", field.name, generate_default(k)));
428                st.push_field(field);
429                generate_structs(
430                    &sub_name,
431                    m,
432                    scope,
433                    #[cfg(feature = "modbus")]
434                    None,
435                    serialize,
436                )?;
437            }
438        }
439    }
440    #[cfg(feature = "modbus")]
441    if let Some(c) = modbus_config {
442        let mut field = codegen::Field::new(
443            "modbus",
444            format!(
445                "::rplc::export::rmodbus::server::context::ModbusContext<{}>",
446                c.as_const_generics()
447            ),
448        );
449        field.vis("pub");
450        if serialize {
451            field.annotation.push("#[serde(default)]".to_owned());
452        }
453        st.push_field(field);
454        default.line("modbus: <_>::default(),");
455    }
456    default.line("}");
457    scope.push_struct(st);
458    scope.raw("#[allow(clippy::derivable_impls)]");
459    scope.push_impl(st_impl);
460    #[cfg(feature = "modbus")]
461    if let Some(c) = modbus_config {
462        let im = scope.new_impl(&format!(
463            "::rplc::server::modbus::SlaveContext<{}> for Context",
464            c.as_const_generics()
465        ));
466        {
467            let fn_ctx = im
468                .new_fn("modbus_context")
469                .arg_ref_self()
470                .ret(format!(
471                    "&::rplc::export::rmodbus::server::context::ModbusContext<{}>",
472                    c.as_const_generics()
473                ))
474                .attr("inline");
475            fn_ctx.line("&self.modbus");
476        }
477        {
478            let fn_ctx_mut = im
479                .new_fn("modbus_context_mut")
480                .arg_mut_self()
481                .ret(format!(
482                    "&mut ::rplc::export::rmodbus::server::context::ModbusContext<{}>",
483                    c.as_const_generics()
484                ))
485                .attr("inline");
486            fn_ctx_mut.line("&mut self.modbus");
487        }
488    }
489    Ok(())
490}