Skip to main content

cu_linux_resources/
lib.rs

1use cu29::bundle_resources;
2use cu29::prelude::*;
3use cu29::resource::{ResourceBundle, ResourceManager};
4use embedded_io::{Read as EmbeddedRead, Write as EmbeddedWrite};
5#[cfg(feature = "embedded-io-07")]
6use embedded_io_07 as embedded_io07;
7use serialport::{Parity as SerialParity, StopBits as SerialStopBits};
8use std::string::String;
9
10pub const SERIAL0_DEV_KEY: &str = "serial0_dev";
11pub const SERIAL0_BAUDRATE_KEY: &str = "serial0_baudrate";
12pub const SERIAL0_PARITY_KEY: &str = "serial0_parity";
13pub const SERIAL0_STOPBITS_KEY: &str = "serial0_stopbits";
14pub const SERIAL0_TIMEOUT_MS_KEY: &str = "serial0_timeout_ms";
15
16pub const SERIAL1_DEV_KEY: &str = "serial1_dev";
17pub const SERIAL1_BAUDRATE_KEY: &str = "serial1_baudrate";
18pub const SERIAL1_PARITY_KEY: &str = "serial1_parity";
19pub const SERIAL1_STOPBITS_KEY: &str = "serial1_stopbits";
20pub const SERIAL1_TIMEOUT_MS_KEY: &str = "serial1_timeout_ms";
21
22pub const SERIAL2_DEV_KEY: &str = "serial2_dev";
23pub const SERIAL2_BAUDRATE_KEY: &str = "serial2_baudrate";
24pub const SERIAL2_PARITY_KEY: &str = "serial2_parity";
25pub const SERIAL2_STOPBITS_KEY: &str = "serial2_stopbits";
26pub const SERIAL2_TIMEOUT_MS_KEY: &str = "serial2_timeout_ms";
27
28pub const SERIAL3_DEV_KEY: &str = "serial3_dev";
29pub const SERIAL3_BAUDRATE_KEY: &str = "serial3_baudrate";
30pub const SERIAL3_PARITY_KEY: &str = "serial3_parity";
31pub const SERIAL3_STOPBITS_KEY: &str = "serial3_stopbits";
32pub const SERIAL3_TIMEOUT_MS_KEY: &str = "serial3_timeout_ms";
33
34pub const SERIAL4_DEV_KEY: &str = "serial4_dev";
35pub const SERIAL4_BAUDRATE_KEY: &str = "serial4_baudrate";
36pub const SERIAL4_PARITY_KEY: &str = "serial4_parity";
37pub const SERIAL4_STOPBITS_KEY: &str = "serial4_stopbits";
38pub const SERIAL4_TIMEOUT_MS_KEY: &str = "serial4_timeout_ms";
39
40pub const SERIAL5_DEV_KEY: &str = "serial5_dev";
41pub const SERIAL5_BAUDRATE_KEY: &str = "serial5_baudrate";
42pub const SERIAL5_PARITY_KEY: &str = "serial5_parity";
43pub const SERIAL5_STOPBITS_KEY: &str = "serial5_stopbits";
44pub const SERIAL5_TIMEOUT_MS_KEY: &str = "serial5_timeout_ms";
45
46pub const I2C0_DEV_KEY: &str = "i2c0_dev";
47pub const I2C1_DEV_KEY: &str = "i2c1_dev";
48pub const I2C2_DEV_KEY: &str = "i2c2_dev";
49
50pub const GPIO_OUT0_PIN_KEY: &str = "gpio_out0_pin";
51pub const GPIO_OUT1_PIN_KEY: &str = "gpio_out1_pin";
52pub const GPIO_OUT2_PIN_KEY: &str = "gpio_out2_pin";
53pub const GPIO_IN0_PIN_KEY: &str = "gpio_in0_pin";
54pub const GPIO_IN1_PIN_KEY: &str = "gpio_in1_pin";
55pub const GPIO_IN2_PIN_KEY: &str = "gpio_in2_pin";
56
57pub const DEFAULT_SERIAL_BAUDRATE: u32 = 115_200;
58pub const DEFAULT_SERIAL_TIMEOUT_MS: u64 = 50;
59pub const DEFAULT_SERIAL_PARITY: SerialParity = SerialParity::None;
60pub const DEFAULT_SERIAL_STOPBITS: SerialStopBits = SerialStopBits::One;
61
62/// Wrapper for resources that are logically exclusive/owned by a single
63/// component but still need to satisfy `Sync` bounds at registration time.
64///
65/// This keeps synchronization adaptation at the bundle/resource boundary instead
66/// of pushing wrappers into every bridge/task that consumes the resource.
67pub struct Exclusive<T>(T);
68
69impl<T> Exclusive<T> {
70    pub const fn new(inner: T) -> Self {
71        Self(inner)
72    }
73
74    pub fn into_inner(self) -> T {
75        self.0
76    }
77
78    pub fn get_mut(&mut self) -> &mut T {
79        &mut self.0
80    }
81}
82
83// SAFETY: `Exclusive<T>` is only handed out by value via `take()` for owned
84// resources. The wrapped `T` is not concurrently aliased through this wrapper.
85unsafe impl<T: Send> Sync for Exclusive<T> {}
86
87impl<T: std::io::Read> std::io::Read for Exclusive<T> {
88    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
89        self.0.read(buf)
90    }
91}
92
93impl<T: std::io::Write> std::io::Write for Exclusive<T> {
94    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
95        self.0.write(buf)
96    }
97
98    fn flush(&mut self) -> std::io::Result<()> {
99        self.0.flush()
100    }
101}
102
103#[cfg(feature = "embedded-io-07")]
104impl<T: embedded_io07::ErrorType> embedded_io07::ErrorType for Exclusive<T> {
105    type Error = T::Error;
106}
107
108#[cfg(feature = "embedded-io-07")]
109impl<T: embedded_io07::Read> embedded_io07::Read for Exclusive<T> {
110    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
111        self.0.read(buf)
112    }
113}
114
115#[cfg(feature = "embedded-io-07")]
116impl<T: embedded_io07::Write> embedded_io07::Write for Exclusive<T> {
117    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
118        self.0.write(buf)
119    }
120
121    fn flush(&mut self) -> Result<(), Self::Error> {
122        self.0.flush()
123    }
124}
125
126impl<T> embedded_hal::i2c::ErrorType for Exclusive<T>
127where
128    T: embedded_hal::i2c::ErrorType,
129{
130    type Error = T::Error;
131}
132
133impl<T> embedded_hal::i2c::I2c for Exclusive<T>
134where
135    T: embedded_hal::i2c::I2c,
136{
137    fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> {
138        self.0.read(address, read)
139    }
140
141    fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> {
142        self.0.write(address, write)
143    }
144
145    fn write_read(
146        &mut self,
147        address: u8,
148        write: &[u8],
149        read: &mut [u8],
150    ) -> Result<(), Self::Error> {
151        self.0.write_read(address, write, read)
152    }
153
154    fn transaction(
155        &mut self,
156        address: u8,
157        operations: &mut [embedded_hal::i2c::Operation<'_>],
158    ) -> Result<(), Self::Error> {
159        self.0.transaction(address, operations)
160    }
161}
162
163impl<T> embedded_hal::digital::ErrorType for Exclusive<T>
164where
165    T: embedded_hal::digital::ErrorType,
166{
167    type Error = T::Error;
168}
169
170impl<T> embedded_hal::digital::OutputPin for Exclusive<T>
171where
172    T: embedded_hal::digital::OutputPin,
173{
174    fn set_low(&mut self) -> Result<(), Self::Error> {
175        self.0.set_low()
176    }
177
178    fn set_high(&mut self) -> Result<(), Self::Error> {
179        self.0.set_high()
180    }
181}
182
183impl<T> embedded_hal::digital::StatefulOutputPin for Exclusive<T>
184where
185    T: embedded_hal::digital::StatefulOutputPin,
186{
187    fn is_set_high(&mut self) -> Result<bool, Self::Error> {
188        self.0.is_set_high()
189    }
190
191    fn is_set_low(&mut self) -> Result<bool, Self::Error> {
192        self.0.is_set_low()
193    }
194}
195
196impl<T> embedded_hal::digital::InputPin for Exclusive<T>
197where
198    T: embedded_hal::digital::InputPin,
199{
200    fn is_high(&mut self) -> Result<bool, Self::Error> {
201        self.0.is_high()
202    }
203
204    fn is_low(&mut self) -> Result<bool, Self::Error> {
205        self.0.is_low()
206    }
207}
208
209pub struct LinuxSerialPort {
210    inner: Exclusive<Box<dyn serialport::SerialPort>>,
211}
212
213impl LinuxSerialPort {
214    pub fn new(inner: Box<dyn serialport::SerialPort>) -> Self {
215        Self {
216            inner: Exclusive::new(inner),
217        }
218    }
219
220    pub fn open(dev: &str, baudrate: u32, timeout_ms: u64) -> std::io::Result<Self> {
221        let config = SerialSlotConfig {
222            dev: dev.to_string(),
223            baudrate,
224            parity: DEFAULT_SERIAL_PARITY,
225            stop_bits: DEFAULT_SERIAL_STOPBITS,
226            timeout_ms,
227        };
228        Self::open_with_config(&config)
229    }
230
231    pub fn open_with_config(config: &SerialSlotConfig) -> std::io::Result<Self> {
232        let port = serialport::new(config.dev.as_str(), config.baudrate)
233            .parity(config.parity)
234            .stop_bits(config.stop_bits)
235            .timeout(std::time::Duration::from_millis(config.timeout_ms))
236            .open()?;
237        Ok(Self::new(port))
238    }
239}
240
241impl std::io::Read for LinuxSerialPort {
242    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
243        self.inner.read(buf)
244    }
245}
246
247impl std::io::Write for LinuxSerialPort {
248    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
249        self.inner.write(buf)
250    }
251
252    fn flush(&mut self) -> std::io::Result<()> {
253        self.inner.flush()
254    }
255}
256
257impl embedded_io::ErrorType for LinuxSerialPort {
258    type Error = std::io::Error;
259}
260
261impl EmbeddedRead for LinuxSerialPort {
262    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
263        std::io::Read::read(self, buf)
264    }
265}
266
267impl EmbeddedWrite for LinuxSerialPort {
268    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
269        std::io::Write::write(self, buf)
270    }
271
272    fn flush(&mut self) -> Result<(), Self::Error> {
273        std::io::Write::flush(self)
274    }
275}
276
277#[cfg(target_os = "linux")]
278pub type LinuxI2c = Exclusive<linux_embedded_hal::I2cdev>;
279#[cfg(target_os = "linux")]
280pub type LinuxOutputPin = Exclusive<rppal::gpio::OutputPin>;
281#[cfg(target_os = "linux")]
282pub type LinuxInputPin = Exclusive<rppal::gpio::InputPin>;
283
284pub struct LinuxResources;
285
286bundle_resources!(
287    LinuxResources:
288        Serial0,
289        Serial1,
290        Serial2,
291        Serial3,
292        Serial4,
293        Serial5,
294        I2c0,
295        I2c1,
296        I2c2,
297        GpioOut0,
298        GpioOut1,
299        GpioOut2,
300        GpioIn0,
301        GpioIn1,
302        GpioIn2
303);
304
305const LINUX_RESOURCE_SLOT_NAMES: &[&str] = &[
306    "serial0",
307    "serial1",
308    "serial2",
309    "serial3",
310    "serial4",
311    "serial5",
312    "i2c0",
313    "i2c1",
314    "i2c2",
315    "gpio_out0",
316    "gpio_out1",
317    "gpio_out2",
318    "gpio_in0",
319    "gpio_in1",
320    "gpio_in2",
321];
322
323struct SerialSlot {
324    id: LinuxResourcesId,
325    dev_key: &'static str,
326    baudrate_key: &'static str,
327    parity_key: &'static str,
328    stopbits_key: &'static str,
329    timeout_ms_key: &'static str,
330}
331
332#[derive(Clone, Debug)]
333pub struct SerialSlotConfig {
334    pub dev: String,
335    pub baudrate: u32,
336    pub parity: SerialParity,
337    pub stop_bits: SerialStopBits,
338    pub timeout_ms: u64,
339}
340
341const SERIAL_SLOTS: &[SerialSlot] = &[
342    SerialSlot {
343        id: LinuxResourcesId::Serial0,
344        dev_key: SERIAL0_DEV_KEY,
345        baudrate_key: SERIAL0_BAUDRATE_KEY,
346        parity_key: SERIAL0_PARITY_KEY,
347        stopbits_key: SERIAL0_STOPBITS_KEY,
348        timeout_ms_key: SERIAL0_TIMEOUT_MS_KEY,
349    },
350    SerialSlot {
351        id: LinuxResourcesId::Serial1,
352        dev_key: SERIAL1_DEV_KEY,
353        baudrate_key: SERIAL1_BAUDRATE_KEY,
354        parity_key: SERIAL1_PARITY_KEY,
355        stopbits_key: SERIAL1_STOPBITS_KEY,
356        timeout_ms_key: SERIAL1_TIMEOUT_MS_KEY,
357    },
358    SerialSlot {
359        id: LinuxResourcesId::Serial2,
360        dev_key: SERIAL2_DEV_KEY,
361        baudrate_key: SERIAL2_BAUDRATE_KEY,
362        parity_key: SERIAL2_PARITY_KEY,
363        stopbits_key: SERIAL2_STOPBITS_KEY,
364        timeout_ms_key: SERIAL2_TIMEOUT_MS_KEY,
365    },
366    SerialSlot {
367        id: LinuxResourcesId::Serial3,
368        dev_key: SERIAL3_DEV_KEY,
369        baudrate_key: SERIAL3_BAUDRATE_KEY,
370        parity_key: SERIAL3_PARITY_KEY,
371        stopbits_key: SERIAL3_STOPBITS_KEY,
372        timeout_ms_key: SERIAL3_TIMEOUT_MS_KEY,
373    },
374    SerialSlot {
375        id: LinuxResourcesId::Serial4,
376        dev_key: SERIAL4_DEV_KEY,
377        baudrate_key: SERIAL4_BAUDRATE_KEY,
378        parity_key: SERIAL4_PARITY_KEY,
379        stopbits_key: SERIAL4_STOPBITS_KEY,
380        timeout_ms_key: SERIAL4_TIMEOUT_MS_KEY,
381    },
382    SerialSlot {
383        id: LinuxResourcesId::Serial5,
384        dev_key: SERIAL5_DEV_KEY,
385        baudrate_key: SERIAL5_BAUDRATE_KEY,
386        parity_key: SERIAL5_PARITY_KEY,
387        stopbits_key: SERIAL5_STOPBITS_KEY,
388        timeout_ms_key: SERIAL5_TIMEOUT_MS_KEY,
389    },
390];
391
392#[cfg(target_os = "linux")]
393struct I2cSlot {
394    id: LinuxResourcesId,
395    dev_key: &'static str,
396}
397
398#[cfg(target_os = "linux")]
399const I2C_SLOTS: &[I2cSlot] = &[
400    I2cSlot {
401        id: LinuxResourcesId::I2c0,
402        dev_key: I2C0_DEV_KEY,
403    },
404    I2cSlot {
405        id: LinuxResourcesId::I2c1,
406        dev_key: I2C1_DEV_KEY,
407    },
408    I2cSlot {
409        id: LinuxResourcesId::I2c2,
410        dev_key: I2C2_DEV_KEY,
411    },
412];
413
414struct GpioSlot {
415    id: LinuxResourcesId,
416    key: &'static str,
417}
418
419const GPIO_OUT_SLOTS: &[GpioSlot] = &[
420    GpioSlot {
421        id: LinuxResourcesId::GpioOut0,
422        key: GPIO_OUT0_PIN_KEY,
423    },
424    GpioSlot {
425        id: LinuxResourcesId::GpioOut1,
426        key: GPIO_OUT1_PIN_KEY,
427    },
428    GpioSlot {
429        id: LinuxResourcesId::GpioOut2,
430        key: GPIO_OUT2_PIN_KEY,
431    },
432];
433
434const GPIO_IN_SLOTS: &[GpioSlot] = &[
435    GpioSlot {
436        id: LinuxResourcesId::GpioIn0,
437        key: GPIO_IN0_PIN_KEY,
438    },
439    GpioSlot {
440        id: LinuxResourcesId::GpioIn1,
441        key: GPIO_IN1_PIN_KEY,
442    },
443    GpioSlot {
444        id: LinuxResourcesId::GpioIn2,
445        key: GPIO_IN2_PIN_KEY,
446    },
447];
448
449impl ResourceBundle for LinuxResources {
450    fn build(
451        bundle: cu29::resource::BundleContext<Self>,
452        config: Option<&ComponentConfig>,
453        manager: &mut ResourceManager,
454    ) -> CuResult<()> {
455        for slot in SERIAL_SLOTS {
456            let Some(serial_config) = read_serial_slot_config(config, slot)? else {
457                continue; // Skip slots without explicit config
458            };
459            match LinuxSerialPort::open_with_config(&serial_config) {
460                Ok(serial) => {
461                    manager.add_owned(bundle.key(slot.id), serial)?;
462                }
463                Err(err) => {
464                    warning!(
465                        "LinuxResources: skipping serial slot {} (dev {}): {}",
466                        slot_name(slot.id),
467                        serial_config.dev,
468                        err.to_string()
469                    );
470                }
471            }
472        }
473
474        #[cfg(target_os = "linux")]
475        for slot in I2C_SLOTS {
476            let Some(dev) = get_string(config, slot.dev_key)? else {
477                continue; // Skip slots without explicit config
478            };
479            match linux_embedded_hal::I2cdev::new(&dev) {
480                Ok(i2c) => {
481                    manager.add_owned(bundle.key(slot.id), Exclusive::new(i2c))?;
482                }
483                Err(err) => {
484                    warning!(
485                        "LinuxResources: skipping i2c slot {} (dev {}): {}",
486                        slot_name(slot.id),
487                        dev,
488                        err.to_string()
489                    );
490                }
491            }
492        }
493
494        #[cfg(target_os = "linux")]
495        {
496            let mut configured_gpio_out: std::vec::Vec<(LinuxResourcesId, u8)> =
497                std::vec::Vec::new();
498            let mut configured_gpio_in: std::vec::Vec<(LinuxResourcesId, u8)> =
499                std::vec::Vec::new();
500
501            for slot in GPIO_OUT_SLOTS {
502                if let Some(pin) = get_u8(config, slot.key)? {
503                    configured_gpio_out.push((slot.id, pin));
504                }
505            }
506            for slot in GPIO_IN_SLOTS {
507                if let Some(pin) = get_u8(config, slot.key)? {
508                    configured_gpio_in.push((slot.id, pin));
509                }
510            }
511
512            if !configured_gpio_out.is_empty() || !configured_gpio_in.is_empty() {
513                let gpio = rppal::gpio::Gpio::new().map_err(|err| {
514                    CuError::new_with_cause("Failed to initialize GPIO subsystem", err)
515                })?;
516
517                for (slot_id, pin) in configured_gpio_out {
518                    match gpio.get(pin) {
519                        Ok(pin) => {
520                            manager.add_owned(
521                                bundle.key(slot_id),
522                                Exclusive::new(pin.into_output()),
523                            )?;
524                        }
525                        Err(err) => {
526                            warning!(
527                                "LinuxResources: skipping gpio output slot {} (pin {}): {}",
528                                slot_name(slot_id),
529                                pin,
530                                err.to_string()
531                            );
532                        }
533                    }
534                }
535
536                for (slot_id, pin) in configured_gpio_in {
537                    match gpio.get(pin) {
538                        Ok(pin) => {
539                            manager
540                                .add_owned(bundle.key(slot_id), Exclusive::new(pin.into_input()))?;
541                        }
542                        Err(err) => {
543                            warning!(
544                                "LinuxResources: skipping gpio input slot {} (pin {}): {}",
545                                slot_name(slot_id),
546                                pin,
547                                err.to_string()
548                            );
549                        }
550                    }
551                }
552            }
553        }
554
555        #[cfg(not(target_os = "linux"))]
556        {
557            for slot in GPIO_OUT_SLOTS {
558                if let Some(pin) = get_u8(config, slot.key)? {
559                    warning!(
560                        "LinuxResources: requested gpio output slot {} on pin {} but GPIO is only supported on Linux",
561                        slot_name(slot.id),
562                        pin
563                    );
564                }
565            }
566            for slot in GPIO_IN_SLOTS {
567                if let Some(pin) = get_u8(config, slot.key)? {
568                    warning!(
569                        "LinuxResources: requested gpio input slot {} on pin {} but GPIO is only supported on Linux",
570                        slot_name(slot.id),
571                        pin
572                    );
573                }
574            }
575        }
576
577        Ok(())
578    }
579}
580
581fn read_serial_slot_config(
582    config: Option<&ComponentConfig>,
583    slot: &SerialSlot,
584) -> CuResult<Option<SerialSlotConfig>> {
585    let Some(dev) = get_string(config, slot.dev_key)? else {
586        return Ok(None); // No device configured for this slot
587    };
588    let baudrate = get_u32(config, slot.baudrate_key)?.unwrap_or(DEFAULT_SERIAL_BAUDRATE);
589    let parity = get_serial_parity(config, slot.parity_key)?.unwrap_or(DEFAULT_SERIAL_PARITY);
590    let stop_bits =
591        get_serial_stop_bits(config, slot.stopbits_key)?.unwrap_or(DEFAULT_SERIAL_STOPBITS);
592    let timeout_ms = get_u64(config, slot.timeout_ms_key)?.unwrap_or(DEFAULT_SERIAL_TIMEOUT_MS);
593
594    Ok(Some(SerialSlotConfig {
595        dev,
596        baudrate,
597        parity,
598        stop_bits,
599        timeout_ms,
600    }))
601}
602
603fn get_serial_parity(
604    config: Option<&ComponentConfig>,
605    key: &str,
606) -> CuResult<Option<SerialParity>> {
607    let Some(raw) = get_string(config, key)? else {
608        return Ok(None);
609    };
610    Ok(Some(parse_serial_parity_value(raw.as_str())?))
611}
612
613fn get_serial_stop_bits(
614    config: Option<&ComponentConfig>,
615    key: &str,
616) -> CuResult<Option<SerialStopBits>> {
617    let Some(raw) = get_u8(config, key)? else {
618        return Ok(None);
619    };
620    Ok(Some(parse_serial_stop_bits_value(raw)?))
621}
622
623fn parse_serial_parity_value(raw: &str) -> CuResult<SerialParity> {
624    let normalized = raw.trim().to_ascii_lowercase();
625    match normalized.as_str() {
626        "none" => Ok(SerialParity::None),
627        "odd" => Ok(SerialParity::Odd),
628        "even" => Ok(SerialParity::Even),
629        _ => Err(CuError::from(format!(
630            "Invalid parity '{raw}'. Expected one of: none, odd, even"
631        ))),
632    }
633}
634
635fn parse_serial_stop_bits_value(raw: u8) -> CuResult<SerialStopBits> {
636    match raw {
637        1 => Ok(SerialStopBits::One),
638        2 => Ok(SerialStopBits::Two),
639        _ => Err(CuError::from(format!(
640            "Invalid stopbits value '{raw}'. Expected 1 or 2"
641        ))),
642    }
643}
644
645fn slot_name(id: LinuxResourcesId) -> &'static str {
646    LINUX_RESOURCE_SLOT_NAMES[id as usize]
647}
648
649fn get_string(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<String>> {
650    match config {
651        Some(cfg) => Ok(cfg.get::<String>(key)?.filter(|value| !value.is_empty())),
652        None => Ok(None),
653    }
654}
655
656fn get_u8(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u8>> {
657    match config {
658        Some(cfg) => Ok(cfg.get::<u8>(key)?),
659        None => Ok(None),
660    }
661}
662
663fn get_u32(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u32>> {
664    match config {
665        Some(cfg) => Ok(cfg.get::<u32>(key)?),
666        None => Ok(None),
667    }
668}
669
670fn get_u64(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u64>> {
671    match config {
672        Some(cfg) => Ok(cfg.get::<u64>(key)?),
673        None => Ok(None),
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    #[test]
682    fn parse_serial_parity_value_accepts_expected_inputs() {
683        assert!(matches!(
684            parse_serial_parity_value("none").unwrap(),
685            SerialParity::None
686        ));
687        assert!(matches!(
688            parse_serial_parity_value("Odd").unwrap(),
689            SerialParity::Odd
690        ));
691        assert!(matches!(
692            parse_serial_parity_value("EVEN").unwrap(),
693            SerialParity::Even
694        ));
695    }
696
697    #[test]
698    fn parse_serial_parity_value_rejects_invalid_input() {
699        assert!(parse_serial_parity_value("mark").is_err());
700    }
701
702    #[test]
703    fn parse_serial_stop_bits_value_accepts_expected_inputs() {
704        assert!(matches!(
705            parse_serial_stop_bits_value(1).unwrap(),
706            SerialStopBits::One
707        ));
708        assert!(matches!(
709            parse_serial_stop_bits_value(2).unwrap(),
710            SerialStopBits::Two
711        ));
712    }
713
714    #[test]
715    fn parse_serial_stop_bits_value_rejects_invalid_input() {
716        assert!(parse_serial_stop_bits_value(0).is_err());
717        assert!(parse_serial_stop_bits_value(3).is_err());
718    }
719
720    #[cfg(feature = "embedded-io-07")]
721    struct MockIo {
722        rx: [u8; 4],
723        rx_len: usize,
724        tx: [u8; 4],
725        tx_len: usize,
726    }
727
728    #[cfg(feature = "embedded-io-07")]
729    impl MockIo {
730        fn new(rx: &[u8]) -> Self {
731            let mut buf = [0_u8; 4];
732            buf[..rx.len()].copy_from_slice(rx);
733            Self {
734                rx: buf,
735                rx_len: rx.len(),
736                tx: [0; 4],
737                tx_len: 0,
738            }
739        }
740    }
741
742    #[cfg(feature = "embedded-io-07")]
743    impl embedded_io_07::ErrorType for MockIo {
744        type Error = core::convert::Infallible;
745    }
746
747    #[cfg(feature = "embedded-io-07")]
748    impl embedded_io_07::Read for MockIo {
749        fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
750            let n = core::cmp::min(buf.len(), self.rx_len);
751            buf[..n].copy_from_slice(&self.rx[..n]);
752            Ok(n)
753        }
754    }
755
756    #[cfg(feature = "embedded-io-07")]
757    impl embedded_io_07::Write for MockIo {
758        fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
759            let n = core::cmp::min(buf.len(), self.tx.len());
760            self.tx[..n].copy_from_slice(&buf[..n]);
761            self.tx_len = n;
762            Ok(n)
763        }
764
765        fn flush(&mut self) -> Result<(), Self::Error> {
766            Ok(())
767        }
768    }
769
770    #[cfg(feature = "embedded-io-07")]
771    #[test]
772    fn exclusive_forwards_embedded_io_07_traits() {
773        let mut wrapped = Exclusive::new(MockIo::new(&[1, 2, 3]));
774
775        let mut rx = [0_u8; 4];
776        let read = embedded_io_07::Read::read(&mut wrapped, &mut rx).unwrap();
777        assert_eq!(read, 3);
778        assert_eq!(&rx[..3], &[1, 2, 3]);
779
780        let written = embedded_io_07::Write::write(&mut wrapped, &[9, 8]).unwrap();
781        assert_eq!(written, 2);
782        embedded_io_07::Write::flush(&mut wrapped).unwrap();
783
784        let inner = wrapped.into_inner();
785        assert_eq!(&inner.tx[..inner.tx_len], &[9, 8]);
786    }
787}