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 = "serial0",
317        Serial1 = "serial1",
318        Serial2 = "serial2",
319        Serial3 = "serial3",
320        Serial4 = "serial4",
321        Serial5 = "serial5",
322        I2c0 = "i2c0",
323        I2c1 = "i2c1",
324        I2c2 = "i2c2",
325        Gpio0 = "gpio0",
326        Gpio1 = "gpio1",
327        Gpio2 = "gpio2",
328        Gpio3 = "gpio3",
329        Gpio4 = "gpio4",
330        Gpio5 = "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    use cu29::resource::{NamedResourceBundleDecl, resource_index_by_name};
866
867    #[test]
868    fn named_resource_bundle_decl_matches_public_linux_slot_names() {
869        let declared = <LinuxResources as NamedResourceBundleDecl>::NAMES;
870        assert_eq!(declared, LINUX_RESOURCE_SLOT_NAMES);
871
872        for (idx, name) in declared.iter().enumerate() {
873            assert_eq!(resource_index_by_name::<LinuxResources>(name), idx);
874        }
875    }
876
877    #[test]
878    fn parse_serial_parity_value_accepts_expected_inputs() {
879        assert!(matches!(
880            parse_serial_parity_value("none").unwrap(),
881            SerialParity::None
882        ));
883        assert!(matches!(
884            parse_serial_parity_value("Odd").unwrap(),
885            SerialParity::Odd
886        ));
887        assert!(matches!(
888            parse_serial_parity_value("EVEN").unwrap(),
889            SerialParity::Even
890        ));
891    }
892
893    #[test]
894    fn parse_serial_parity_value_rejects_invalid_input() {
895        assert!(parse_serial_parity_value("mark").is_err());
896    }
897
898    #[test]
899    fn parse_serial_stop_bits_value_accepts_expected_inputs() {
900        assert!(matches!(
901            parse_serial_stop_bits_value(1).unwrap(),
902            SerialStopBits::One
903        ));
904        assert!(matches!(
905            parse_serial_stop_bits_value(2).unwrap(),
906            SerialStopBits::Two
907        ));
908    }
909
910    #[test]
911    fn parse_serial_stop_bits_value_rejects_invalid_input() {
912        assert!(parse_serial_stop_bits_value(0).is_err());
913        assert!(parse_serial_stop_bits_value(3).is_err());
914    }
915
916    #[test]
917    fn parse_gpio_direction_value_accepts_expected_inputs() {
918        assert!(matches!(
919            parse_gpio_direction_value("input").unwrap(),
920            GpioDirection::Input
921        ));
922        assert!(matches!(
923            parse_gpio_direction_value("Output").unwrap(),
924            GpioDirection::Output
925        ));
926    }
927
928    #[test]
929    fn parse_gpio_direction_value_rejects_invalid_input() {
930        assert!(parse_gpio_direction_value("io").is_err());
931    }
932
933    #[test]
934    fn parse_gpio_bias_value_accepts_expected_inputs() {
935        assert!(matches!(
936            parse_gpio_bias_value("off").unwrap(),
937            GpioBias::Off
938        ));
939        assert!(matches!(
940            parse_gpio_bias_value("pull_down").unwrap(),
941            GpioBias::PullDown
942        ));
943        assert!(matches!(
944            parse_gpio_bias_value("PullUp").unwrap(),
945            GpioBias::PullUp
946        ));
947    }
948
949    #[test]
950    fn parse_gpio_bias_value_rejects_invalid_input() {
951        assert!(parse_gpio_bias_value("hold").is_err());
952    }
953
954    #[test]
955    fn parse_gpio_initial_level_value_accepts_expected_inputs() {
956        assert!(matches!(
957            parse_gpio_initial_level_value("low").unwrap(),
958            GpioInitialLevel::Low
959        ));
960        assert!(matches!(
961            parse_gpio_initial_level_value("HIGH").unwrap(),
962            GpioInitialLevel::High
963        ));
964    }
965
966    #[test]
967    fn parse_gpio_initial_level_value_rejects_invalid_input() {
968        assert!(parse_gpio_initial_level_value("toggle").is_err());
969    }
970
971    #[test]
972    fn read_gpio_slot_config_requires_pin_when_aux_keys_are_present() {
973        let mut cfg = ComponentConfig::new();
974        cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
975        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
976    }
977
978    #[test]
979    fn read_gpio_slot_config_requires_direction_when_pin_is_set() {
980        let mut cfg = ComponentConfig::new();
981        cfg.set(GPIO0_PIN_KEY, 23_u8);
982        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
983    }
984
985    #[test]
986    fn read_gpio_slot_config_defaults_bias_and_preserves_level_for_output() {
987        let mut cfg = ComponentConfig::new();
988        cfg.set(GPIO0_PIN_KEY, 23_u8);
989        cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
990
991        let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
992            .unwrap()
993            .expect("slot should be configured");
994        assert_eq!(
995            slot_config,
996            GpioSlotConfig {
997                pin: 23,
998                direction: GpioDirection::Output,
999                bias: GpioBias::Off,
1000                initial_level: None,
1001            }
1002        );
1003    }
1004
1005    #[test]
1006    fn read_gpio_slot_config_parses_explicit_initial_level_for_output() {
1007        let mut cfg = ComponentConfig::new();
1008        cfg.set(GPIO0_PIN_KEY, 23_u8);
1009        cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
1010        cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1011
1012        let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
1013            .unwrap()
1014            .expect("slot should be configured");
1015        assert_eq!(
1016            slot_config,
1017            GpioSlotConfig {
1018                pin: 23,
1019                direction: GpioDirection::Output,
1020                bias: GpioBias::Off,
1021                initial_level: Some(GpioInitialLevel::High),
1022            }
1023        );
1024    }
1025
1026    #[test]
1027    fn read_gpio_slot_config_rejects_initial_level_for_input() {
1028        let mut cfg = ComponentConfig::new();
1029        cfg.set(GPIO0_PIN_KEY, 23_u8);
1030        cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
1031        cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1032        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1033    }
1034
1035    struct MockIo {
1036        rx: [u8; 4],
1037        rx_len: usize,
1038        tx: [u8; 4],
1039        tx_len: usize,
1040    }
1041
1042    impl MockIo {
1043        fn new(rx: &[u8]) -> Self {
1044            let mut buf = [0_u8; 4];
1045            buf[..rx.len()].copy_from_slice(rx);
1046            Self {
1047                rx: buf,
1048                rx_len: rx.len(),
1049                tx: [0; 4],
1050                tx_len: 0,
1051            }
1052        }
1053    }
1054
1055    impl embedded_io::ErrorType for MockIo {
1056        type Error = core::convert::Infallible;
1057    }
1058
1059    impl embedded_io::Read for MockIo {
1060        fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
1061            let n = core::cmp::min(buf.len(), self.rx_len);
1062            buf[..n].copy_from_slice(&self.rx[..n]);
1063            Ok(n)
1064        }
1065    }
1066
1067    impl embedded_io::Write for MockIo {
1068        fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
1069            let n = core::cmp::min(buf.len(), self.tx.len());
1070            self.tx[..n].copy_from_slice(&buf[..n]);
1071            self.tx_len = n;
1072            Ok(n)
1073        }
1074
1075        fn flush(&mut self) -> Result<(), Self::Error> {
1076            Ok(())
1077        }
1078    }
1079
1080    #[test]
1081    fn exclusive_forwards_embedded_io_traits() {
1082        let mut wrapped = Exclusive::new(MockIo::new(&[1, 2, 3]));
1083
1084        let mut rx = [0_u8; 4];
1085        let read = embedded_io::Read::read(&mut wrapped, &mut rx).unwrap();
1086        assert_eq!(read, 3);
1087        assert_eq!(&rx[..3], &[1, 2, 3]);
1088
1089        let written = embedded_io::Write::write(&mut wrapped, &[9, 8]).unwrap();
1090        assert_eq!(written, 2);
1091        embedded_io::Write::flush(&mut wrapped).unwrap();
1092
1093        let inner = wrapped.into_inner();
1094        assert_eq!(&inner.tx[..inner.tx_len], &[9, 8]);
1095    }
1096}