Skip to main content

cu_linux_resources/
lib.rs

1use cu29::bundle_resources;
2use cu29::prelude::*;
3use cu29::resource::{ResourceBundle, ResourceManager};
4use embedded_io::{ErrorType as EmbeddedErrorType, Read as EmbeddedRead, Write as EmbeddedWrite};
5use serialport::{Parity as SerialParity, StopBits as SerialStopBits};
6use std::string::String;
7
8pub const SERIAL0_DEV_KEY: &str = "serial0_dev";
9pub const SERIAL0_BAUDRATE_KEY: &str = "serial0_baudrate";
10pub const SERIAL0_PARITY_KEY: &str = "serial0_parity";
11pub const SERIAL0_STOPBITS_KEY: &str = "serial0_stopbits";
12pub const SERIAL0_TIMEOUT_MS_KEY: &str = "serial0_timeout_ms";
13pub const SERIAL0_NAME: &str = "serial0";
14
15pub const SERIAL1_DEV_KEY: &str = "serial1_dev";
16pub const SERIAL1_BAUDRATE_KEY: &str = "serial1_baudrate";
17pub const SERIAL1_PARITY_KEY: &str = "serial1_parity";
18pub const SERIAL1_STOPBITS_KEY: &str = "serial1_stopbits";
19pub const SERIAL1_TIMEOUT_MS_KEY: &str = "serial1_timeout_ms";
20pub const SERIAL1_NAME: &str = "serial1";
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";
27pub const SERIAL2_NAME: &str = "serial2";
28
29pub const SERIAL3_DEV_KEY: &str = "serial3_dev";
30pub const SERIAL3_BAUDRATE_KEY: &str = "serial3_baudrate";
31pub const SERIAL3_PARITY_KEY: &str = "serial3_parity";
32pub const SERIAL3_STOPBITS_KEY: &str = "serial3_stopbits";
33pub const SERIAL3_TIMEOUT_MS_KEY: &str = "serial3_timeout_ms";
34pub const SERIAL3_NAME: &str = "serial3";
35
36pub const SERIAL4_DEV_KEY: &str = "serial4_dev";
37pub const SERIAL4_BAUDRATE_KEY: &str = "serial4_baudrate";
38pub const SERIAL4_PARITY_KEY: &str = "serial4_parity";
39pub const SERIAL4_STOPBITS_KEY: &str = "serial4_stopbits";
40pub const SERIAL4_TIMEOUT_MS_KEY: &str = "serial4_timeout_ms";
41pub const SERIAL4_NAME: &str = "serial4";
42
43pub const SERIAL5_DEV_KEY: &str = "serial5_dev";
44pub const SERIAL5_BAUDRATE_KEY: &str = "serial5_baudrate";
45pub const SERIAL5_PARITY_KEY: &str = "serial5_parity";
46pub const SERIAL5_STOPBITS_KEY: &str = "serial5_stopbits";
47pub const SERIAL5_TIMEOUT_MS_KEY: &str = "serial5_timeout_ms";
48pub const SERIAL5_NAME: &str = "serial5";
49
50pub const I2C0_DEV_KEY: &str = "i2c0_dev";
51pub const I2C1_DEV_KEY: &str = "i2c1_dev";
52pub const I2C2_DEV_KEY: &str = "i2c2_dev";
53pub const I2C0_NAME: &str = "i2c0";
54pub const I2C1_NAME: &str = "i2c1";
55pub const I2C2_NAME: &str = "i2c2";
56
57pub const GPIO0_NAME: &str = "gpio0";
58pub const GPIO1_NAME: &str = "gpio1";
59pub const GPIO2_NAME: &str = "gpio2";
60pub const GPIO3_NAME: &str = "gpio3";
61pub const GPIO4_NAME: &str = "gpio4";
62pub const GPIO5_NAME: &str = "gpio5";
63pub const GPIO0_PIN_KEY: &str = "gpio0_pin";
64pub const GPIO1_PIN_KEY: &str = "gpio1_pin";
65pub const GPIO2_PIN_KEY: &str = "gpio2_pin";
66pub const GPIO3_PIN_KEY: &str = "gpio3_pin";
67pub const GPIO4_PIN_KEY: &str = "gpio4_pin";
68pub const GPIO5_PIN_KEY: &str = "gpio5_pin";
69pub const GPIO0_DIRECTION_KEY: &str = "gpio0_direction";
70pub const GPIO1_DIRECTION_KEY: &str = "gpio1_direction";
71pub const GPIO2_DIRECTION_KEY: &str = "gpio2_direction";
72pub const GPIO3_DIRECTION_KEY: &str = "gpio3_direction";
73pub const GPIO4_DIRECTION_KEY: &str = "gpio4_direction";
74pub const GPIO5_DIRECTION_KEY: &str = "gpio5_direction";
75pub const GPIO0_BIAS_KEY: &str = "gpio0_bias";
76pub const GPIO1_BIAS_KEY: &str = "gpio1_bias";
77pub const GPIO2_BIAS_KEY: &str = "gpio2_bias";
78pub const GPIO3_BIAS_KEY: &str = "gpio3_bias";
79pub const GPIO4_BIAS_KEY: &str = "gpio4_bias";
80pub const GPIO5_BIAS_KEY: &str = "gpio5_bias";
81pub const GPIO0_INITIAL_LEVEL_KEY: &str = "gpio0_initial_level";
82pub const GPIO1_INITIAL_LEVEL_KEY: &str = "gpio1_initial_level";
83pub const GPIO2_INITIAL_LEVEL_KEY: &str = "gpio2_initial_level";
84pub const GPIO3_INITIAL_LEVEL_KEY: &str = "gpio3_initial_level";
85pub const GPIO4_INITIAL_LEVEL_KEY: &str = "gpio4_initial_level";
86pub const GPIO5_INITIAL_LEVEL_KEY: &str = "gpio5_initial_level";
87
88pub const DEFAULT_SERIAL_BAUDRATE: u32 = 115_200;
89pub const DEFAULT_SERIAL_TIMEOUT_MS: u64 = 50;
90pub const DEFAULT_SERIAL_PARITY: SerialParity = SerialParity::None;
91pub const DEFAULT_SERIAL_STOPBITS: SerialStopBits = SerialStopBits::One;
92
93/// Wrapper for resources that are logically exclusive/owned by a single
94/// component but still need to satisfy `Sync` bounds at registration time.
95///
96/// This keeps synchronization adaptation at the bundle/resource boundary instead
97/// of pushing wrappers into every bridge/task that consumes the resource.
98pub struct Exclusive<T>(T);
99
100impl<T> Exclusive<T> {
101    pub const fn new(inner: T) -> Self {
102        Self(inner)
103    }
104
105    pub fn into_inner(self) -> T {
106        self.0
107    }
108
109    pub fn get_mut(&mut self) -> &mut T {
110        &mut self.0
111    }
112}
113
114// SAFETY: `Exclusive<T>` is only handed out by value via `take()` for owned
115// resources. The wrapped `T` is not concurrently aliased through this wrapper.
116unsafe impl<T: Send> Sync for Exclusive<T> {}
117
118impl<T: std::io::Read> std::io::Read for Exclusive<T> {
119    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
120        self.0.read(buf)
121    }
122}
123
124impl<T: std::io::Write> std::io::Write for Exclusive<T> {
125    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
126        self.0.write(buf)
127    }
128
129    fn flush(&mut self) -> std::io::Result<()> {
130        self.0.flush()
131    }
132}
133
134impl<T: EmbeddedErrorType> EmbeddedErrorType for Exclusive<T> {
135    type Error = T::Error;
136}
137
138impl<T: EmbeddedRead> EmbeddedRead for Exclusive<T> {
139    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
140        self.0.read(buf)
141    }
142}
143
144impl<T: EmbeddedWrite> EmbeddedWrite for Exclusive<T> {
145    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
146        self.0.write(buf)
147    }
148
149    fn flush(&mut self) -> Result<(), Self::Error> {
150        self.0.flush()
151    }
152}
153
154impl<T> embedded_hal::i2c::ErrorType for Exclusive<T>
155where
156    T: embedded_hal::i2c::ErrorType,
157{
158    type Error = T::Error;
159}
160
161impl<T> embedded_hal::i2c::I2c for Exclusive<T>
162where
163    T: embedded_hal::i2c::I2c,
164{
165    fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> {
166        self.0.read(address, read)
167    }
168
169    fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> {
170        self.0.write(address, write)
171    }
172
173    fn write_read(
174        &mut self,
175        address: u8,
176        write: &[u8],
177        read: &mut [u8],
178    ) -> Result<(), Self::Error> {
179        self.0.write_read(address, write, read)
180    }
181
182    fn transaction(
183        &mut self,
184        address: u8,
185        operations: &mut [embedded_hal::i2c::Operation<'_>],
186    ) -> Result<(), Self::Error> {
187        self.0.transaction(address, operations)
188    }
189}
190
191impl<T> embedded_hal::digital::ErrorType for Exclusive<T>
192where
193    T: embedded_hal::digital::ErrorType,
194{
195    type Error = T::Error;
196}
197
198impl<T> embedded_hal::digital::OutputPin for Exclusive<T>
199where
200    T: embedded_hal::digital::OutputPin,
201{
202    fn set_low(&mut self) -> Result<(), Self::Error> {
203        self.0.set_low()
204    }
205
206    fn set_high(&mut self) -> Result<(), Self::Error> {
207        self.0.set_high()
208    }
209}
210
211impl<T> embedded_hal::digital::StatefulOutputPin for Exclusive<T>
212where
213    T: embedded_hal::digital::StatefulOutputPin,
214{
215    fn is_set_high(&mut self) -> Result<bool, Self::Error> {
216        self.0.is_set_high()
217    }
218
219    fn is_set_low(&mut self) -> Result<bool, Self::Error> {
220        self.0.is_set_low()
221    }
222}
223
224impl<T> embedded_hal::digital::InputPin for Exclusive<T>
225where
226    T: embedded_hal::digital::InputPin,
227{
228    fn is_high(&mut self) -> Result<bool, Self::Error> {
229        self.0.is_high()
230    }
231
232    fn is_low(&mut self) -> Result<bool, Self::Error> {
233        self.0.is_low()
234    }
235}
236
237pub struct LinuxSerialPort {
238    inner: Exclusive<Box<dyn serialport::SerialPort>>,
239}
240
241impl LinuxSerialPort {
242    pub fn new(inner: Box<dyn serialport::SerialPort>) -> Self {
243        Self {
244            inner: Exclusive::new(inner),
245        }
246    }
247
248    pub fn open(dev: &str, baudrate: u32, timeout_ms: u64) -> std::io::Result<Self> {
249        let config = SerialSlotConfig {
250            dev: dev.to_string(),
251            baudrate,
252            parity: DEFAULT_SERIAL_PARITY,
253            stop_bits: DEFAULT_SERIAL_STOPBITS,
254            timeout_ms,
255        };
256        Self::open_with_config(&config)
257    }
258
259    pub fn open_with_config(config: &SerialSlotConfig) -> std::io::Result<Self> {
260        let port = serialport::new(config.dev.as_str(), config.baudrate)
261            .parity(config.parity)
262            .stop_bits(config.stop_bits)
263            .timeout(std::time::Duration::from_millis(config.timeout_ms))
264            .open()?;
265        Ok(Self::new(port))
266    }
267}
268
269impl std::io::Read for LinuxSerialPort {
270    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
271        self.inner.read(buf)
272    }
273}
274
275impl std::io::Write for LinuxSerialPort {
276    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
277        self.inner.write(buf)
278    }
279
280    fn flush(&mut self) -> std::io::Result<()> {
281        self.inner.flush()
282    }
283}
284
285impl embedded_io::ErrorType for LinuxSerialPort {
286    type Error = std::io::Error;
287}
288
289impl EmbeddedRead for LinuxSerialPort {
290    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
291        std::io::Read::read(self, buf)
292    }
293}
294
295impl EmbeddedWrite for LinuxSerialPort {
296    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
297        std::io::Write::write(self, buf)
298    }
299
300    fn flush(&mut self) -> Result<(), Self::Error> {
301        std::io::Write::flush(self)
302    }
303}
304
305#[cfg(target_os = "linux")]
306pub type LinuxI2c = Exclusive<linux_embedded_hal::I2cdev>;
307#[cfg(target_os = "linux")]
308pub type LinuxOutputPin = Exclusive<rppal::gpio::IoPin>;
309#[cfg(target_os = "linux")]
310pub type LinuxInputPin = Exclusive<rppal::gpio::InputPin>;
311
312pub struct LinuxResources;
313
314bundle_resources!(
315    LinuxResources:
316        Serial0,
317        Serial1,
318        Serial2,
319        Serial3,
320        Serial4,
321        Serial5,
322        I2c0,
323        I2c1,
324        I2c2,
325        Gpio0,
326        Gpio1,
327        Gpio2,
328        Gpio3,
329        Gpio4,
330        Gpio5
331);
332
333const LINUX_RESOURCE_SLOT_NAMES: &[&str] = &[
334    SERIAL0_NAME,
335    SERIAL1_NAME,
336    SERIAL2_NAME,
337    SERIAL3_NAME,
338    SERIAL4_NAME,
339    SERIAL5_NAME,
340    I2C0_NAME,
341    I2C1_NAME,
342    I2C2_NAME,
343    GPIO0_NAME,
344    GPIO1_NAME,
345    GPIO2_NAME,
346    GPIO3_NAME,
347    GPIO4_NAME,
348    GPIO5_NAME,
349];
350
351struct SerialSlot {
352    id: LinuxResourcesId,
353    dev_key: &'static str,
354    baudrate_key: &'static str,
355    parity_key: &'static str,
356    stopbits_key: &'static str,
357    timeout_ms_key: &'static str,
358}
359
360#[derive(Clone, Debug)]
361pub struct SerialSlotConfig {
362    pub dev: String,
363    pub baudrate: u32,
364    pub parity: SerialParity,
365    pub stop_bits: SerialStopBits,
366    pub timeout_ms: u64,
367}
368
369const SERIAL_SLOTS: &[SerialSlot] = &[
370    SerialSlot {
371        id: LinuxResourcesId::Serial0,
372        dev_key: SERIAL0_DEV_KEY,
373        baudrate_key: SERIAL0_BAUDRATE_KEY,
374        parity_key: SERIAL0_PARITY_KEY,
375        stopbits_key: SERIAL0_STOPBITS_KEY,
376        timeout_ms_key: SERIAL0_TIMEOUT_MS_KEY,
377    },
378    SerialSlot {
379        id: LinuxResourcesId::Serial1,
380        dev_key: SERIAL1_DEV_KEY,
381        baudrate_key: SERIAL1_BAUDRATE_KEY,
382        parity_key: SERIAL1_PARITY_KEY,
383        stopbits_key: SERIAL1_STOPBITS_KEY,
384        timeout_ms_key: SERIAL1_TIMEOUT_MS_KEY,
385    },
386    SerialSlot {
387        id: LinuxResourcesId::Serial2,
388        dev_key: SERIAL2_DEV_KEY,
389        baudrate_key: SERIAL2_BAUDRATE_KEY,
390        parity_key: SERIAL2_PARITY_KEY,
391        stopbits_key: SERIAL2_STOPBITS_KEY,
392        timeout_ms_key: SERIAL2_TIMEOUT_MS_KEY,
393    },
394    SerialSlot {
395        id: LinuxResourcesId::Serial3,
396        dev_key: SERIAL3_DEV_KEY,
397        baudrate_key: SERIAL3_BAUDRATE_KEY,
398        parity_key: SERIAL3_PARITY_KEY,
399        stopbits_key: SERIAL3_STOPBITS_KEY,
400        timeout_ms_key: SERIAL3_TIMEOUT_MS_KEY,
401    },
402    SerialSlot {
403        id: LinuxResourcesId::Serial4,
404        dev_key: SERIAL4_DEV_KEY,
405        baudrate_key: SERIAL4_BAUDRATE_KEY,
406        parity_key: SERIAL4_PARITY_KEY,
407        stopbits_key: SERIAL4_STOPBITS_KEY,
408        timeout_ms_key: SERIAL4_TIMEOUT_MS_KEY,
409    },
410    SerialSlot {
411        id: LinuxResourcesId::Serial5,
412        dev_key: SERIAL5_DEV_KEY,
413        baudrate_key: SERIAL5_BAUDRATE_KEY,
414        parity_key: SERIAL5_PARITY_KEY,
415        stopbits_key: SERIAL5_STOPBITS_KEY,
416        timeout_ms_key: SERIAL5_TIMEOUT_MS_KEY,
417    },
418];
419
420#[cfg(target_os = "linux")]
421struct I2cSlot {
422    id: LinuxResourcesId,
423    dev_key: &'static str,
424}
425
426#[cfg(target_os = "linux")]
427const I2C_SLOTS: &[I2cSlot] = &[
428    I2cSlot {
429        id: LinuxResourcesId::I2c0,
430        dev_key: I2C0_DEV_KEY,
431    },
432    I2cSlot {
433        id: LinuxResourcesId::I2c1,
434        dev_key: I2C1_DEV_KEY,
435    },
436    I2cSlot {
437        id: LinuxResourcesId::I2c2,
438        dev_key: I2C2_DEV_KEY,
439    },
440];
441
442#[cfg_attr(not(any(target_os = "linux", test)), allow(dead_code))]
443struct GpioSlot {
444    id: LinuxResourcesId,
445    name: &'static str,
446    pin_key: &'static str,
447    direction_key: &'static str,
448    bias_key: &'static str,
449    initial_level_key: &'static str,
450}
451
452macro_rules! gpio_slot {
453    ($id:ident, $name:ident, $pin_key:ident, $direction_key:ident, $bias_key:ident, $initial_level_key:ident) => {
454        GpioSlot {
455            id: LinuxResourcesId::$id,
456            name: $name,
457            pin_key: $pin_key,
458            direction_key: $direction_key,
459            bias_key: $bias_key,
460            initial_level_key: $initial_level_key,
461        }
462    };
463}
464
465#[cfg(any(target_os = "linux", test))]
466#[derive(Copy, Clone, Debug, Eq, PartialEq)]
467enum GpioDirection {
468    Input,
469    Output,
470}
471
472#[cfg(any(target_os = "linux", test))]
473#[derive(Copy, Clone, Debug, Eq, PartialEq)]
474enum GpioBias {
475    Off,
476    PullDown,
477    PullUp,
478}
479
480#[cfg(any(target_os = "linux", test))]
481impl GpioBias {
482    #[cfg(target_os = "linux")]
483    const fn into_rppal(self) -> rppal::gpio::Bias {
484        match self {
485            GpioBias::Off => rppal::gpio::Bias::Off,
486            GpioBias::PullDown => rppal::gpio::Bias::PullDown,
487            GpioBias::PullUp => rppal::gpio::Bias::PullUp,
488        }
489    }
490}
491
492#[cfg(any(target_os = "linux", test))]
493#[derive(Copy, Clone, Debug, Eq, PartialEq)]
494enum GpioInitialLevel {
495    Low,
496    High,
497}
498
499#[cfg(any(target_os = "linux", test))]
500#[derive(Copy, Clone, Debug, Eq, PartialEq)]
501struct GpioSlotConfig {
502    pin: u8,
503    direction: GpioDirection,
504    bias: GpioBias,
505    initial_level: Option<GpioInitialLevel>,
506}
507
508const GPIO_SLOTS: &[GpioSlot] = &[
509    gpio_slot!(
510        Gpio0,
511        GPIO0_NAME,
512        GPIO0_PIN_KEY,
513        GPIO0_DIRECTION_KEY,
514        GPIO0_BIAS_KEY,
515        GPIO0_INITIAL_LEVEL_KEY
516    ),
517    gpio_slot!(
518        Gpio1,
519        GPIO1_NAME,
520        GPIO1_PIN_KEY,
521        GPIO1_DIRECTION_KEY,
522        GPIO1_BIAS_KEY,
523        GPIO1_INITIAL_LEVEL_KEY
524    ),
525    gpio_slot!(
526        Gpio2,
527        GPIO2_NAME,
528        GPIO2_PIN_KEY,
529        GPIO2_DIRECTION_KEY,
530        GPIO2_BIAS_KEY,
531        GPIO2_INITIAL_LEVEL_KEY
532    ),
533    gpio_slot!(
534        Gpio3,
535        GPIO3_NAME,
536        GPIO3_PIN_KEY,
537        GPIO3_DIRECTION_KEY,
538        GPIO3_BIAS_KEY,
539        GPIO3_INITIAL_LEVEL_KEY
540    ),
541    gpio_slot!(
542        Gpio4,
543        GPIO4_NAME,
544        GPIO4_PIN_KEY,
545        GPIO4_DIRECTION_KEY,
546        GPIO4_BIAS_KEY,
547        GPIO4_INITIAL_LEVEL_KEY
548    ),
549    gpio_slot!(
550        Gpio5,
551        GPIO5_NAME,
552        GPIO5_PIN_KEY,
553        GPIO5_DIRECTION_KEY,
554        GPIO5_BIAS_KEY,
555        GPIO5_INITIAL_LEVEL_KEY
556    ),
557];
558
559impl ResourceBundle for LinuxResources {
560    fn build(
561        bundle: cu29::resource::BundleContext<Self>,
562        config: Option<&ComponentConfig>,
563        manager: &mut ResourceManager,
564    ) -> CuResult<()> {
565        for slot in SERIAL_SLOTS {
566            let Some(serial_config) = read_serial_slot_config(config, slot)? else {
567                continue; // Skip slots without explicit config
568            };
569            match LinuxSerialPort::open_with_config(&serial_config) {
570                Ok(serial) => {
571                    manager.add_owned(bundle.key(slot.id), serial)?;
572                }
573                Err(err) => {
574                    warning!(
575                        "LinuxResources: skipping serial slot {} (dev {}): {}",
576                        slot_name(slot.id),
577                        serial_config.dev,
578                        err.to_string()
579                    );
580                }
581            }
582        }
583
584        #[cfg(target_os = "linux")]
585        for slot in I2C_SLOTS {
586            let Some(dev) = get_string(config, slot.dev_key)? else {
587                continue; // Skip slots without explicit config
588            };
589            match linux_embedded_hal::I2cdev::new(&dev) {
590                Ok(i2c) => {
591                    manager.add_owned(bundle.key(slot.id), Exclusive::new(i2c))?;
592                }
593                Err(err) => {
594                    warning!(
595                        "LinuxResources: skipping i2c slot {} (dev {}): {}",
596                        slot_name(slot.id),
597                        dev,
598                        err.to_string()
599                    );
600                }
601            }
602        }
603
604        #[cfg(target_os = "linux")]
605        {
606            let mut configured_gpio_slots: std::vec::Vec<(
607                LinuxResourcesId,
608                &'static str,
609                GpioSlotConfig,
610            )> = std::vec::Vec::new();
611
612            for slot in GPIO_SLOTS {
613                let Some(slot_config) = read_gpio_slot_config(config, slot)? else {
614                    continue;
615                };
616                configured_gpio_slots.push((slot.id, slot.name, slot_config));
617            }
618
619            if !configured_gpio_slots.is_empty() {
620                let gpio = rppal::gpio::Gpio::new().map_err(|err| {
621                    CuError::new_with_cause("Failed to initialize GPIO subsystem", err)
622                })?;
623
624                for (slot_id, slot_name, slot_config) in configured_gpio_slots {
625                    let pin = match gpio.get(slot_config.pin) {
626                        Ok(pin) => pin,
627                        Err(err) => {
628                            warning!(
629                                "LinuxResources: skipping gpio slot {} (pin {}): {}",
630                                slot_name,
631                                slot_config.pin,
632                                err.to_string()
633                            );
634                            continue;
635                        }
636                    };
637                    match slot_config.direction {
638                        GpioDirection::Input => {
639                            let mut pin = pin.into_input();
640                            pin.set_bias(slot_config.bias.into_rppal());
641                            manager.add_owned(bundle.key(slot_id), Exclusive::new(pin))?;
642                        }
643                        GpioDirection::Output => {
644                            let mut pin = pin.into_io(rppal::gpio::Mode::Output);
645                            pin.set_bias(slot_config.bias.into_rppal());
646                            if let Some(initial_level) = slot_config.initial_level {
647                                match initial_level {
648                                    GpioInitialLevel::Low => pin.set_low(),
649                                    GpioInitialLevel::High => pin.set_high(),
650                                }
651                            }
652                            manager.add_owned(bundle.key(slot_id), Exclusive::new(pin))?;
653                        }
654                    }
655                }
656            }
657        }
658
659        #[cfg(not(target_os = "linux"))]
660        {
661            for slot in GPIO_SLOTS {
662                if let Some(pin) = get_u8(config, slot.pin_key)? {
663                    warning!(
664                        "LinuxResources: requested gpio slot {} on pin {} but GPIO is only supported on Linux",
665                        slot_name(slot.id),
666                        pin
667                    );
668                }
669            }
670        }
671
672        Ok(())
673    }
674}
675
676fn read_serial_slot_config(
677    config: Option<&ComponentConfig>,
678    slot: &SerialSlot,
679) -> CuResult<Option<SerialSlotConfig>> {
680    let Some(dev) = get_string(config, slot.dev_key)? else {
681        return Ok(None); // No device configured for this slot
682    };
683    let baudrate = get_u32(config, slot.baudrate_key)?.unwrap_or(DEFAULT_SERIAL_BAUDRATE);
684    let parity = get_serial_parity(config, slot.parity_key)?.unwrap_or(DEFAULT_SERIAL_PARITY);
685    let stop_bits =
686        get_serial_stop_bits(config, slot.stopbits_key)?.unwrap_or(DEFAULT_SERIAL_STOPBITS);
687    let timeout_ms = get_u64(config, slot.timeout_ms_key)?.unwrap_or(DEFAULT_SERIAL_TIMEOUT_MS);
688
689    Ok(Some(SerialSlotConfig {
690        dev,
691        baudrate,
692        parity,
693        stop_bits,
694        timeout_ms,
695    }))
696}
697
698#[cfg(any(target_os = "linux", test))]
699fn read_gpio_slot_config(
700    config: Option<&ComponentConfig>,
701    slot: &GpioSlot,
702) -> CuResult<Option<GpioSlotConfig>> {
703    let pin = get_u8(config, slot.pin_key)?;
704    let direction = get_string(config, slot.direction_key)?;
705    let bias = get_string(config, slot.bias_key)?;
706    let initial_level = get_string(config, slot.initial_level_key)?;
707
708    let Some(pin) = pin else {
709        if direction.is_some() || bias.is_some() || initial_level.is_some() {
710            return Err(CuError::from(format!(
711                "Config key '{}' is required when configuring {}",
712                slot.pin_key, slot.name
713            )));
714        }
715        return Ok(None);
716    };
717
718    let direction_raw = direction.ok_or_else(|| {
719        CuError::from(format!(
720            "Config key '{}' is required when '{}' is set",
721            slot.direction_key, slot.pin_key
722        ))
723    })?;
724    let direction = parse_gpio_direction_value(direction_raw.as_str())?;
725
726    let bias = match bias {
727        Some(raw) => parse_gpio_bias_value(raw.as_str())?,
728        None => GpioBias::Off,
729    };
730
731    let initial_level = match initial_level {
732        Some(raw) => Some(parse_gpio_initial_level_value(raw.as_str())?),
733        None => None,
734    };
735
736    if matches!(direction, GpioDirection::Input) && initial_level.is_some() {
737        return Err(CuError::from(format!(
738            "Config key '{}' is only valid when '{}' is 'output'",
739            slot.initial_level_key, slot.direction_key
740        )));
741    }
742
743    Ok(Some(GpioSlotConfig {
744        pin,
745        direction,
746        bias,
747        initial_level,
748    }))
749}
750
751fn get_serial_parity(
752    config: Option<&ComponentConfig>,
753    key: &str,
754) -> CuResult<Option<SerialParity>> {
755    let Some(raw) = get_string(config, key)? else {
756        return Ok(None);
757    };
758    Ok(Some(parse_serial_parity_value(raw.as_str())?))
759}
760
761fn get_serial_stop_bits(
762    config: Option<&ComponentConfig>,
763    key: &str,
764) -> CuResult<Option<SerialStopBits>> {
765    let Some(raw) = get_u8(config, key)? else {
766        return Ok(None);
767    };
768    Ok(Some(parse_serial_stop_bits_value(raw)?))
769}
770
771fn parse_serial_parity_value(raw: &str) -> CuResult<SerialParity> {
772    let normalized = raw.trim().to_ascii_lowercase();
773    match normalized.as_str() {
774        "none" => Ok(SerialParity::None),
775        "odd" => Ok(SerialParity::Odd),
776        "even" => Ok(SerialParity::Even),
777        _ => Err(CuError::from(format!(
778            "Invalid parity '{raw}'. Expected one of: none, odd, even"
779        ))),
780    }
781}
782
783fn parse_serial_stop_bits_value(raw: u8) -> CuResult<SerialStopBits> {
784    match raw {
785        1 => Ok(SerialStopBits::One),
786        2 => Ok(SerialStopBits::Two),
787        _ => Err(CuError::from(format!(
788            "Invalid stopbits value '{raw}'. Expected 1 or 2"
789        ))),
790    }
791}
792
793#[cfg(any(target_os = "linux", test))]
794fn parse_gpio_direction_value(raw: &str) -> CuResult<GpioDirection> {
795    let normalized = raw.trim().to_ascii_lowercase();
796    match normalized.as_str() {
797        "input" => Ok(GpioDirection::Input),
798        "output" => Ok(GpioDirection::Output),
799        _ => Err(CuError::from(format!(
800            "Invalid GPIO direction '{raw}'. Expected one of: input, output"
801        ))),
802    }
803}
804
805#[cfg(any(target_os = "linux", test))]
806fn parse_gpio_bias_value(raw: &str) -> CuResult<GpioBias> {
807    let normalized = raw.trim().to_ascii_lowercase();
808    match normalized.as_str() {
809        "off" | "none" => Ok(GpioBias::Off),
810        "pull_down" | "pulldown" => Ok(GpioBias::PullDown),
811        "pull_up" | "pullup" => Ok(GpioBias::PullUp),
812        _ => Err(CuError::from(format!(
813            "Invalid GPIO bias '{raw}'. Expected one of: off, pull_up, pull_down"
814        ))),
815    }
816}
817
818#[cfg(any(target_os = "linux", test))]
819fn parse_gpio_initial_level_value(raw: &str) -> CuResult<GpioInitialLevel> {
820    let normalized = raw.trim().to_ascii_lowercase();
821    match normalized.as_str() {
822        "low" => Ok(GpioInitialLevel::Low),
823        "high" => Ok(GpioInitialLevel::High),
824        _ => Err(CuError::from(format!(
825            "Invalid GPIO initial level '{raw}'. Expected one of: low, high"
826        ))),
827    }
828}
829
830fn slot_name(id: LinuxResourcesId) -> &'static str {
831    LINUX_RESOURCE_SLOT_NAMES[id as usize]
832}
833
834fn get_string(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<String>> {
835    match config {
836        Some(cfg) => Ok(cfg.get::<String>(key)?.filter(|value| !value.is_empty())),
837        None => Ok(None),
838    }
839}
840
841fn get_u8(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u8>> {
842    match config {
843        Some(cfg) => Ok(cfg.get::<u8>(key)?),
844        None => Ok(None),
845    }
846}
847
848fn get_u32(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u32>> {
849    match config {
850        Some(cfg) => Ok(cfg.get::<u32>(key)?),
851        None => Ok(None),
852    }
853}
854
855fn get_u64(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u64>> {
856    match config {
857        Some(cfg) => Ok(cfg.get::<u64>(key)?),
858        None => Ok(None),
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn parse_serial_parity_value_accepts_expected_inputs() {
868        assert!(matches!(
869            parse_serial_parity_value("none").unwrap(),
870            SerialParity::None
871        ));
872        assert!(matches!(
873            parse_serial_parity_value("Odd").unwrap(),
874            SerialParity::Odd
875        ));
876        assert!(matches!(
877            parse_serial_parity_value("EVEN").unwrap(),
878            SerialParity::Even
879        ));
880    }
881
882    #[test]
883    fn parse_serial_parity_value_rejects_invalid_input() {
884        assert!(parse_serial_parity_value("mark").is_err());
885    }
886
887    #[test]
888    fn parse_serial_stop_bits_value_accepts_expected_inputs() {
889        assert!(matches!(
890            parse_serial_stop_bits_value(1).unwrap(),
891            SerialStopBits::One
892        ));
893        assert!(matches!(
894            parse_serial_stop_bits_value(2).unwrap(),
895            SerialStopBits::Two
896        ));
897    }
898
899    #[test]
900    fn parse_serial_stop_bits_value_rejects_invalid_input() {
901        assert!(parse_serial_stop_bits_value(0).is_err());
902        assert!(parse_serial_stop_bits_value(3).is_err());
903    }
904
905    #[test]
906    fn parse_gpio_direction_value_accepts_expected_inputs() {
907        assert!(matches!(
908            parse_gpio_direction_value("input").unwrap(),
909            GpioDirection::Input
910        ));
911        assert!(matches!(
912            parse_gpio_direction_value("Output").unwrap(),
913            GpioDirection::Output
914        ));
915    }
916
917    #[test]
918    fn parse_gpio_direction_value_rejects_invalid_input() {
919        assert!(parse_gpio_direction_value("io").is_err());
920    }
921
922    #[test]
923    fn parse_gpio_bias_value_accepts_expected_inputs() {
924        assert!(matches!(
925            parse_gpio_bias_value("off").unwrap(),
926            GpioBias::Off
927        ));
928        assert!(matches!(
929            parse_gpio_bias_value("pull_down").unwrap(),
930            GpioBias::PullDown
931        ));
932        assert!(matches!(
933            parse_gpio_bias_value("PullUp").unwrap(),
934            GpioBias::PullUp
935        ));
936    }
937
938    #[test]
939    fn parse_gpio_bias_value_rejects_invalid_input() {
940        assert!(parse_gpio_bias_value("hold").is_err());
941    }
942
943    #[test]
944    fn parse_gpio_initial_level_value_accepts_expected_inputs() {
945        assert!(matches!(
946            parse_gpio_initial_level_value("low").unwrap(),
947            GpioInitialLevel::Low
948        ));
949        assert!(matches!(
950            parse_gpio_initial_level_value("HIGH").unwrap(),
951            GpioInitialLevel::High
952        ));
953    }
954
955    #[test]
956    fn parse_gpio_initial_level_value_rejects_invalid_input() {
957        assert!(parse_gpio_initial_level_value("toggle").is_err());
958    }
959
960    #[test]
961    fn read_gpio_slot_config_requires_pin_when_aux_keys_are_present() {
962        let mut cfg = ComponentConfig::new();
963        cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
964        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
965    }
966
967    #[test]
968    fn read_gpio_slot_config_requires_direction_when_pin_is_set() {
969        let mut cfg = ComponentConfig::new();
970        cfg.set(GPIO0_PIN_KEY, 23_u8);
971        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
972    }
973
974    #[test]
975    fn read_gpio_slot_config_defaults_bias_and_preserves_level_for_output() {
976        let mut cfg = ComponentConfig::new();
977        cfg.set(GPIO0_PIN_KEY, 23_u8);
978        cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
979
980        let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
981            .unwrap()
982            .expect("slot should be configured");
983        assert_eq!(
984            slot_config,
985            GpioSlotConfig {
986                pin: 23,
987                direction: GpioDirection::Output,
988                bias: GpioBias::Off,
989                initial_level: None,
990            }
991        );
992    }
993
994    #[test]
995    fn read_gpio_slot_config_parses_explicit_initial_level_for_output() {
996        let mut cfg = ComponentConfig::new();
997        cfg.set(GPIO0_PIN_KEY, 23_u8);
998        cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
999        cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1000
1001        let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
1002            .unwrap()
1003            .expect("slot should be configured");
1004        assert_eq!(
1005            slot_config,
1006            GpioSlotConfig {
1007                pin: 23,
1008                direction: GpioDirection::Output,
1009                bias: GpioBias::Off,
1010                initial_level: Some(GpioInitialLevel::High),
1011            }
1012        );
1013    }
1014
1015    #[test]
1016    fn read_gpio_slot_config_rejects_initial_level_for_input() {
1017        let mut cfg = ComponentConfig::new();
1018        cfg.set(GPIO0_PIN_KEY, 23_u8);
1019        cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
1020        cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1021        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1022    }
1023
1024    struct MockIo {
1025        rx: [u8; 4],
1026        rx_len: usize,
1027        tx: [u8; 4],
1028        tx_len: usize,
1029    }
1030
1031    impl MockIo {
1032        fn new(rx: &[u8]) -> Self {
1033            let mut buf = [0_u8; 4];
1034            buf[..rx.len()].copy_from_slice(rx);
1035            Self {
1036                rx: buf,
1037                rx_len: rx.len(),
1038                tx: [0; 4],
1039                tx_len: 0,
1040            }
1041        }
1042    }
1043
1044    impl embedded_io::ErrorType for MockIo {
1045        type Error = core::convert::Infallible;
1046    }
1047
1048    impl embedded_io::Read for MockIo {
1049        fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
1050            let n = core::cmp::min(buf.len(), self.rx_len);
1051            buf[..n].copy_from_slice(&self.rx[..n]);
1052            Ok(n)
1053        }
1054    }
1055
1056    impl embedded_io::Write for MockIo {
1057        fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
1058            let n = core::cmp::min(buf.len(), self.tx.len());
1059            self.tx[..n].copy_from_slice(&buf[..n]);
1060            self.tx_len = n;
1061            Ok(n)
1062        }
1063
1064        fn flush(&mut self) -> Result<(), Self::Error> {
1065            Ok(())
1066        }
1067    }
1068
1069    #[test]
1070    fn exclusive_forwards_embedded_io_traits() {
1071        let mut wrapped = Exclusive::new(MockIo::new(&[1, 2, 3]));
1072
1073        let mut rx = [0_u8; 4];
1074        let read = embedded_io::Read::read(&mut wrapped, &mut rx).unwrap();
1075        assert_eq!(read, 3);
1076        assert_eq!(&rx[..3], &[1, 2, 3]);
1077
1078        let written = embedded_io::Write::write(&mut wrapped, &[9, 8]).unwrap();
1079        assert_eq!(written, 2);
1080        embedded_io::Write::flush(&mut wrapped).unwrap();
1081
1082        let inner = wrapped.into_inner();
1083        assert_eq!(&inner.tx[..inner.tx_len], &[9, 8]);
1084    }
1085}