1use std::{collections::HashMap, path::Path};
3
4use crate::{
5 pdo::{PdoCommParameter, PdoMapping},
6 CanId,
7};
8use serde::{de, Deserialize, Deserializer};
9use snafu::{ResultExt, Snafu};
10
11#[derive(Debug, Snafu)]
13pub enum ConfigError {
14 #[snafu(display("IO error loading {path}: {source:?}"))]
16 Io {
17 path: String,
19 source: std::io::Error,
21 },
22 #[snafu(display("Error parsing TOML: {source}"))]
24 TomlDeserialization {
25 source: toml::de::Error,
27 },
28}
29
30#[derive(Clone, Debug, PartialEq)]
32pub struct Store {
33 pub index: u16,
35 pub sub: u8,
37 pub value: StoreValue,
39}
40
41impl Store {
42 pub fn raw_value(&self) -> Vec<u8> {
44 self.value.raw()
45 }
46}
47
48#[allow(missing_docs)]
50#[derive(Clone, Debug, Deserialize, PartialEq)]
51pub enum StoreValue {
52 U32(u32),
53 U16(u16),
54 U8(u8),
55 I32(i32),
56 I16(i16),
57 I8(i8),
58 F32(f32),
59 String(String),
60}
61
62impl StoreValue {
63 pub fn raw(&self) -> Vec<u8> {
65 match self {
66 StoreValue::U32(v) => v.to_le_bytes().to_vec(),
67 StoreValue::U16(v) => v.to_le_bytes().to_vec(),
68 StoreValue::U8(v) => vec![*v],
69 StoreValue::I32(v) => v.to_le_bytes().to_vec(),
70 StoreValue::I16(v) => v.to_le_bytes().to_vec(),
71 StoreValue::I8(v) => vec![*v as u8],
72 StoreValue::F32(v) => v.to_le_bytes().to_vec(),
73 StoreValue::String(ref s) => s.as_bytes().to_vec(),
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
84pub struct NodeConfig(NodeConfigSerializer);
85
86impl NodeConfig {
87 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<NodeConfig, ConfigError> {
89 let path = path.as_ref();
90 let content = std::fs::read_to_string(path).context(IoSnafu {
91 path: path.to_string_lossy(),
92 })?;
93 Self::load_from_str(&content)
94 }
95
96 pub fn load_from_str(s: &str) -> Result<NodeConfig, ConfigError> {
98 let raw_config: NodeConfigSerializer =
99 toml::from_str(s).context(TomlDeserializationSnafu)?;
100
101 Ok(NodeConfig(raw_config))
102 }
103
104 pub fn tpdos(&self) -> &HashMap<usize, PdoConfig> {
106 &self.0.tpdo.0
107 }
108
109 pub fn rpdos(&self) -> &HashMap<usize, PdoConfig> {
111 &self.0.rpdo.0
112 }
113
114 pub fn stores(&self) -> &[Store] {
118 &self.0.store
119 }
120}
121
122#[derive(Clone, Debug, Default, Deserialize)]
123
124pub(crate) struct PdoConfigMapSerializer(
125 #[serde(deserialize_with = "deserialize_pdo_map", default)] pub HashMap<usize, PdoConfig>,
126);
127
128impl From<PdoConfigMapSerializer> for HashMap<usize, PdoConfig> {
129 fn from(value: PdoConfigMapSerializer) -> Self {
130 value.0
131 }
132}
133
134#[derive(Clone, Debug, Default, Deserialize)]
135#[serde(deny_unknown_fields)]
136struct NodeConfigSerializer {
137 #[serde(default)]
138 pub tpdo: PdoConfigMapSerializer,
139 #[serde(default)]
140 pub rpdo: PdoConfigMapSerializer,
141 #[serde(default, deserialize_with = "deserialize_store")]
142 pub store: Vec<Store>,
143}
144
145#[derive(Clone, Debug, Deserialize)]
147#[serde(deny_unknown_fields)]
148struct PdoConfigSerializer {
149 pub cob_id: u32,
151 #[serde(default)]
153 pub extended: bool,
154 pub enabled: bool,
157 #[serde(default)]
159 pub rtr_disabled: bool,
160 pub mappings: Vec<PdoMapping>,
162 pub transmission_type: u8,
169}
170
171#[derive(Clone, Debug, Deserialize, PartialEq)]
173#[serde(try_from = "PdoConfigSerializer")]
174pub struct PdoConfig {
175 pub comm: PdoCommParameter,
177 pub mappings: Vec<PdoMapping>,
179}
180
181#[derive(Clone, Debug, Snafu)]
183#[snafu(display("{message}"))]
184struct PdoConfigParseError {
185 message: String,
186}
187
188impl TryFrom<PdoConfigSerializer> for PdoConfig {
189 type Error = PdoConfigParseError;
190
191 fn try_from(value: PdoConfigSerializer) -> Result<Self, Self::Error> {
192 let cob_id = if value.extended {
193 CanId::extended(value.cob_id)
194 } else {
195 if value.cob_id > 0x7ff {
196 return Err(PdoConfigParseError {
197 message: format!(
198 "COB ID 0x{:x} is out of range for standard ID. Set `extended` to true.",
199 value.cob_id
200 ),
201 });
202 }
203 CanId::std(value.cob_id as u16)
204 };
205
206 Ok(PdoConfig {
207 comm: PdoCommParameter {
208 cob_id,
209 valid: value.enabled,
210 rtr_disabled: value.rtr_disabled,
211 transmission_type: value.transmission_type,
212 },
213 mappings: value.mappings,
214 })
215 }
216}
217
218#[derive(Debug, Clone, Copy, Deserialize)]
219#[serde(rename_all = "lowercase")]
220enum StoreType {
221 U32,
222 U16,
223 U8,
224 I32,
225 I16,
226 I8,
227 F32,
228 String,
229}
230
231#[derive(Debug, Deserialize)]
232#[serde(deny_unknown_fields)]
233struct StoreSerializer {
234 pub index: u16,
235 pub sub: u8,
236 pub value: toml::Value,
237 #[serde(rename = "type")]
238 pub ty: StoreType,
239}
240
241fn deserialize_store<'de, D>(deserializer: D) -> Result<Vec<Store>, D::Error>
242where
243 D: Deserializer<'de>,
244{
245 let raw_store = Vec::<StoreSerializer>::deserialize(deserializer)?;
246
247 let store = raw_store
248 .into_iter()
249 .map(|raw| {
250 let value = match raw.ty {
251 StoreType::U32 => {
252 let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
253 de::Unexpected::Str(&raw.value.to_string()),
254 &"an integer",
255 ))?;
256 Ok(StoreValue::U32(value.try_into().map_err(|_| {
257 de::Error::invalid_value(
258 de::Unexpected::Signed(value),
259 &"an integer in range [0..2^32]",
260 )
261 })?))
262 }
263 StoreType::U16 => {
264 let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
265 de::Unexpected::Str(&raw.value.to_string()),
266 &"an integer",
267 ))?;
268 Ok(StoreValue::U16(value.try_into().map_err(|_| {
269 de::Error::invalid_value(
270 de::Unexpected::Signed(value),
271 &"an integer in range [0..65536]",
272 )
273 })?))
274 }
275 StoreType::U8 => {
276 let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
277 de::Unexpected::Str(&raw.value.to_string()),
278 &"an integer",
279 ))?;
280 Ok(StoreValue::U8(value.try_into().map_err(|_| {
281 de::Error::invalid_value(
282 de::Unexpected::Signed(value),
283 &"an integer in range [0..256]",
284 )
285 })?))
286 }
287 StoreType::I32 => {
288 let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
289 de::Unexpected::Str(&raw.value.to_string()),
290 &"an integer",
291 ))?;
292 Ok(StoreValue::I32(value.try_into().map_err(|_| {
293 de::Error::invalid_value(
294 de::Unexpected::Signed(value),
295 &"an integer in range [-2^31..2^31]",
296 )
297 })?))
298 }
299 StoreType::I16 => {
300 let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
301 de::Unexpected::Str(&raw.value.to_string()),
302 &"an integer",
303 ))?;
304 Ok(StoreValue::I16(value.try_into().map_err(|_| {
305 de::Error::invalid_value(
306 de::Unexpected::Signed(value),
307 &"an integer in range [-32767..32768]",
308 )
309 })?))
310 }
311 StoreType::I8 => {
312 let value = raw.value.as_integer().ok_or(de::Error::invalid_type(
313 de::Unexpected::Str(&raw.value.to_string()),
314 &"an integer",
315 ))?;
316 Ok(StoreValue::I8(value.try_into().map_err(|_| {
317 de::Error::invalid_value(
318 de::Unexpected::Signed(value),
319 &"an integer in range [-127..128]",
320 )
321 })?))
322 }
323 StoreType::F32 => {
324 let value = raw.value.as_float().ok_or(de::Error::invalid_type(
325 de::Unexpected::Str(&raw.value.to_string()),
326 &"a float",
327 ))?;
328 Ok(StoreValue::F32(value as f32))
329 }
330 StoreType::String => {
331 let value = raw.value.as_str().ok_or(de::Error::invalid_type(
332 de::Unexpected::Str(&raw.value.to_string()),
333 &"a string",
334 ))?;
335 Ok(StoreValue::String(value.to_string()))
336 }
337 }?;
338 Ok(Store {
339 index: raw.index,
340 sub: raw.sub,
341 value,
342 })
343 })
344 .collect::<Result<Vec<_>, _>>()?;
345
346 Ok(store)
347}
348
349pub(crate) fn deserialize_pdo_map<'de, D, T>(deserializer: D) -> Result<HashMap<usize, T>, D::Error>
350where
351 D: Deserializer<'de>,
352 T: Deserialize<'de>,
353{
354 let str_map = HashMap::<String, T>::deserialize(deserializer)?;
355 let original_len = str_map.len();
356 let data = {
357 str_map
358 .into_iter()
359 .map(|(str_key, value)| match str_key.parse() {
360 Ok(int_key) => Ok((int_key, value)),
361 Err(_) => Err({
362 de::Error::invalid_value(
363 de::Unexpected::Str(&str_key),
364 &"a non-negative integer",
365 )
366 }),
367 })
368 .collect::<Result<HashMap<_, _>, _>>()?
369 };
370 if data.len() < original_len {
372 return Err(de::Error::custom("detected duplicate integer key"));
373 }
374 Ok(data)
375}
376
377#[cfg(test)]
378mod test {
379 use super::*;
380 use assertables::assert_contains;
381
382 #[test]
383 fn test_out_of_range_standard_id() {
384 let str = r#"
385 [tpdo.0]
386 enabled = true
387 cob_id = 0x800
388 transmission_type = 254
389 mappings = [
390 { index=0x1000, sub=1, size=8 },
391 ]
392 "#;
393
394 let result = NodeConfig::load_from_str(str);
395 assert!(result.is_err());
396 let err = result.unwrap_err();
397 assert_contains!(
398 &err.to_string(),
399 "COB ID 0x800 is out of range for standard ID"
400 );
401 }
402
403 #[test]
404 fn test_extended_cob() {
405 let str = r#"
406 [tpdo.0]
407 enabled = true
408 cob_id = 0x800
409 extended = true
410 transmission_type = 254
411 mappings = [
412 { index=0x1000, sub=1, size=8 },
413 ]
414 "#;
415
416 let result = NodeConfig::load_from_str(str).unwrap();
417 assert_eq!(1, result.tpdos().len());
418 let tpdo = result.tpdos().get(&0).unwrap();
419 assert_eq!(CanId::extended(0x800), tpdo.comm.cob_id);
420 }
421
422 #[test]
423 fn test_node_config_parse() {
424 let str = r#"
425 [tpdo.0]
426 enabled = true
427 cob_id = 0x181
428 transmission_type = 254
429 mappings = [
430 { index=0x1000, sub=1, size=8 },
431 { index=0x1000, sub=2, size=16 },
432 ]
433
434 [[store]]
435 type = "u32"
436 value = 12
437 index = 0x1000
438 sub = 0
439 "#;
440
441 let config = match NodeConfig::load_from_str(str) {
442 Ok(config) => config,
443 Err(e) => {
444 println!("{}", e);
445 panic!("Failed to parse config");
446 }
447 };
448
449 println!("{config:?}");
450 assert_eq!(1, config.tpdos().len());
451 assert_eq!(1, config.stores().len());
452 }
453
454 #[test]
455 fn test_out_of_range_integer() {
456 let str = r#"
457 [[store]]
458 type = "u8"
459 value = 256
460 index = 0x1000
461 sub = 0
462 "#;
463
464 let result = NodeConfig::load_from_str(str);
465 assert!(result.is_err());
466 assert!(result
467 .unwrap_err()
468 .to_string()
469 .contains("expected an integer in range [0..256]"));
470 }
471}