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