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