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
8#[cfg(all(unix, feature = "serial-rts"))]
9mod serial_rts;
10#[cfg(all(unix, feature = "serial-rts"))]
11pub use serial_rts::{LinuxSerialRtsPin, SerialRtsError};
12
13pub const SERIAL0_DEV_KEY: &str = "serial0_dev";
14pub const SERIAL0_BAUDRATE_KEY: &str = "serial0_baudrate";
15pub const SERIAL0_PARITY_KEY: &str = "serial0_parity";
16pub const SERIAL0_STOPBITS_KEY: &str = "serial0_stopbits";
17pub const SERIAL0_TIMEOUT_MS_KEY: &str = "serial0_timeout_ms";
18pub const SERIAL0_NAME: &str = "serial0";
19
20pub const SERIAL1_DEV_KEY: &str = "serial1_dev";
21pub const SERIAL1_BAUDRATE_KEY: &str = "serial1_baudrate";
22pub const SERIAL1_PARITY_KEY: &str = "serial1_parity";
23pub const SERIAL1_STOPBITS_KEY: &str = "serial1_stopbits";
24pub const SERIAL1_TIMEOUT_MS_KEY: &str = "serial1_timeout_ms";
25pub const SERIAL1_NAME: &str = "serial1";
26
27pub const SERIAL2_DEV_KEY: &str = "serial2_dev";
28pub const SERIAL2_BAUDRATE_KEY: &str = "serial2_baudrate";
29pub const SERIAL2_PARITY_KEY: &str = "serial2_parity";
30pub const SERIAL2_STOPBITS_KEY: &str = "serial2_stopbits";
31pub const SERIAL2_TIMEOUT_MS_KEY: &str = "serial2_timeout_ms";
32pub const SERIAL2_NAME: &str = "serial2";
33
34pub const SERIAL3_DEV_KEY: &str = "serial3_dev";
35pub const SERIAL3_BAUDRATE_KEY: &str = "serial3_baudrate";
36pub const SERIAL3_PARITY_KEY: &str = "serial3_parity";
37pub const SERIAL3_STOPBITS_KEY: &str = "serial3_stopbits";
38pub const SERIAL3_TIMEOUT_MS_KEY: &str = "serial3_timeout_ms";
39pub const SERIAL3_NAME: &str = "serial3";
40
41pub const SERIAL4_DEV_KEY: &str = "serial4_dev";
42pub const SERIAL4_BAUDRATE_KEY: &str = "serial4_baudrate";
43pub const SERIAL4_PARITY_KEY: &str = "serial4_parity";
44pub const SERIAL4_STOPBITS_KEY: &str = "serial4_stopbits";
45pub const SERIAL4_TIMEOUT_MS_KEY: &str = "serial4_timeout_ms";
46pub const SERIAL4_NAME: &str = "serial4";
47
48pub const SERIAL5_DEV_KEY: &str = "serial5_dev";
49pub const SERIAL5_BAUDRATE_KEY: &str = "serial5_baudrate";
50pub const SERIAL5_PARITY_KEY: &str = "serial5_parity";
51pub const SERIAL5_STOPBITS_KEY: &str = "serial5_stopbits";
52pub const SERIAL5_TIMEOUT_MS_KEY: &str = "serial5_timeout_ms";
53pub const SERIAL5_NAME: &str = "serial5";
54
55pub const I2C0_DEV_KEY: &str = "i2c0_dev";
56pub const I2C1_DEV_KEY: &str = "i2c1_dev";
57pub const I2C2_DEV_KEY: &str = "i2c2_dev";
58pub const I2C0_NAME: &str = "i2c0";
59pub const I2C1_NAME: &str = "i2c1";
60pub const I2C2_NAME: &str = "i2c2";
61
62pub const GPIO0_NAME: &str = "gpio0";
63pub const GPIO1_NAME: &str = "gpio1";
64pub const GPIO2_NAME: &str = "gpio2";
65pub const GPIO3_NAME: &str = "gpio3";
66pub const GPIO4_NAME: &str = "gpio4";
67pub const GPIO5_NAME: &str = "gpio5";
68pub const GPIO0_PIN_KEY: &str = "gpio0_pin";
69pub const GPIO1_PIN_KEY: &str = "gpio1_pin";
70pub const GPIO2_PIN_KEY: &str = "gpio2_pin";
71pub const GPIO3_PIN_KEY: &str = "gpio3_pin";
72pub const GPIO4_PIN_KEY: &str = "gpio4_pin";
73pub const GPIO5_PIN_KEY: &str = "gpio5_pin";
74pub const GPIO0_DIRECTION_KEY: &str = "gpio0_direction";
75pub const GPIO1_DIRECTION_KEY: &str = "gpio1_direction";
76pub const GPIO2_DIRECTION_KEY: &str = "gpio2_direction";
77pub const GPIO3_DIRECTION_KEY: &str = "gpio3_direction";
78pub const GPIO4_DIRECTION_KEY: &str = "gpio4_direction";
79pub const GPIO5_DIRECTION_KEY: &str = "gpio5_direction";
80pub const GPIO0_BIAS_KEY: &str = "gpio0_bias";
81pub const GPIO1_BIAS_KEY: &str = "gpio1_bias";
82pub const GPIO2_BIAS_KEY: &str = "gpio2_bias";
83pub const GPIO3_BIAS_KEY: &str = "gpio3_bias";
84pub const GPIO4_BIAS_KEY: &str = "gpio4_bias";
85pub const GPIO5_BIAS_KEY: &str = "gpio5_bias";
86pub const GPIO0_INITIAL_LEVEL_KEY: &str = "gpio0_initial_level";
87pub const GPIO1_INITIAL_LEVEL_KEY: &str = "gpio1_initial_level";
88pub const GPIO2_INITIAL_LEVEL_KEY: &str = "gpio2_initial_level";
89pub const GPIO3_INITIAL_LEVEL_KEY: &str = "gpio3_initial_level";
90pub const GPIO4_INITIAL_LEVEL_KEY: &str = "gpio4_initial_level";
91pub const GPIO5_INITIAL_LEVEL_KEY: &str = "gpio5_initial_level";
92
93pub const DEFAULT_SERIAL_BAUDRATE: u32 = 115_200;
94pub const DEFAULT_SERIAL_TIMEOUT_MS: u64 = 50;
95pub const DEFAULT_SERIAL_PARITY: SerialParity = SerialParity::None;
96pub const DEFAULT_SERIAL_STOPBITS: SerialStopBits = SerialStopBits::One;
97
98/// Wrapper for resources that are logically exclusive/owned by a single
99/// component but still need to satisfy `Sync` bounds at registration time.
100///
101/// This keeps synchronization adaptation at the bundle/resource boundary instead
102/// of pushing wrappers into every bridge/task that consumes the resource.
103pub struct Exclusive<T>(T);
104
105impl<T> Exclusive<T> {
106    pub const fn new(inner: T) -> Self {
107        Self(inner)
108    }
109
110    pub fn into_inner(self) -> T {
111        self.0
112    }
113
114    pub fn get_mut(&mut self) -> &mut T {
115        &mut self.0
116    }
117}
118
119// SAFETY: `Exclusive<T>` is only handed out by value via `take()` for owned
120// resources. The wrapped `T` is not concurrently aliased through this wrapper.
121unsafe impl<T: Send> Sync for Exclusive<T> {}
122
123impl<T: std::io::Read> std::io::Read for Exclusive<T> {
124    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
125        self.0.read(buf)
126    }
127}
128
129impl<T: std::io::Write> std::io::Write for Exclusive<T> {
130    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
131        self.0.write(buf)
132    }
133
134    fn flush(&mut self) -> std::io::Result<()> {
135        self.0.flush()
136    }
137}
138
139impl<T: EmbeddedErrorType> EmbeddedErrorType for Exclusive<T> {
140    type Error = T::Error;
141}
142
143impl<T: EmbeddedRead> EmbeddedRead for Exclusive<T> {
144    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
145        self.0.read(buf)
146    }
147}
148
149impl<T: EmbeddedWrite> EmbeddedWrite 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    /// Open a UART and an independently owned, active-low RTS# output.
248    #[cfg(all(unix, feature = "serial-rts"))]
249    pub fn open_with_rts(config: &SerialSlotConfig) -> std::io::Result<(Self, LinuxSerialRtsPin)> {
250        let (port, rts) = serial_rts::open(config)?;
251        Ok((Self::new(Box::new(port)), rts))
252    }
253
254    pub fn new(inner: Box<dyn serialport::SerialPort>) -> Self {
255        Self {
256            inner: Exclusive::new(inner),
257        }
258    }
259
260    pub fn open(dev: &str, baudrate: u32, timeout_ms: u64) -> std::io::Result<Self> {
261        let config = SerialSlotConfig {
262            dev: dev.to_string(),
263            baudrate,
264            parity: DEFAULT_SERIAL_PARITY,
265            stop_bits: DEFAULT_SERIAL_STOPBITS,
266            timeout_ms,
267        };
268        Self::open_with_config(&config)
269    }
270
271    pub fn open_with_config(config: &SerialSlotConfig) -> std::io::Result<Self> {
272        let port = serialport::new(config.dev.as_str(), config.baudrate)
273            .parity(config.parity)
274            .stop_bits(config.stop_bits)
275            .timeout(std::time::Duration::from_millis(config.timeout_ms))
276            .open()?;
277        Ok(Self::new(port))
278    }
279}
280
281impl std::io::Read for LinuxSerialPort {
282    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
283        self.inner.read(buf)
284    }
285}
286
287impl std::io::Write for LinuxSerialPort {
288    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
289        self.inner.write(buf)
290    }
291
292    fn flush(&mut self) -> std::io::Result<()> {
293        self.inner.flush()
294    }
295}
296
297impl embedded_io::ErrorType for LinuxSerialPort {
298    type Error = std::io::Error;
299}
300
301impl EmbeddedRead for LinuxSerialPort {
302    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
303        std::io::Read::read(self, buf)
304    }
305}
306
307impl EmbeddedWrite for LinuxSerialPort {
308    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
309        std::io::Write::write(self, buf)
310    }
311
312    fn flush(&mut self) -> Result<(), Self::Error> {
313        std::io::Write::flush(self)
314    }
315}
316
317/// Native nonblocking UART for resource stacks. Configure a serial slot with
318/// `serialN_nonblocking: true` to export this type.
319#[cfg(unix)]
320#[derive(Debug)]
321pub struct LinuxNonblockingSerialPort(std::fs::File);
322
323#[cfg(unix)]
324impl LinuxNonblockingSerialPort {
325    /// Open a nonblocking UART and an independently owned, active-low RTS# output.
326    #[cfg(feature = "serial-rts")]
327    pub fn open_with_rts(config: &SerialSlotConfig) -> std::io::Result<(Self, LinuxSerialRtsPin)> {
328        let (port, rts) = serial_rts::open(config)?;
329        Ok((Self::from_native(port)?, rts))
330    }
331
332    pub fn open_with_config(config: &SerialSlotConfig) -> std::io::Result<Self> {
333        let port = serialport::new(&config.dev, config.baudrate)
334            .parity(config.parity)
335            .stop_bits(config.stop_bits)
336            .open_native()?;
337        Self::from_native(port)
338    }
339
340    /// Take ownership of an already configured native UART.
341    pub fn from_native(port: serialport::TTYPort) -> std::io::Result<Self> {
342        use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd};
343        let fd = port.as_raw_fd();
344        // SAFETY: the live TTYPort owns fd; fcntl changes only its status flags.
345        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
346        if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
347            return Err(std::io::Error::last_os_error());
348        }
349        // SAFETY: ownership transfers once from TTYPort into File.
350        Ok(Self(unsafe {
351            std::fs::File::from_raw_fd(port.into_raw_fd())
352        }))
353    }
354
355    fn result(result: std::io::Result<usize>) -> std::io::Result<usize> {
356        match result {
357            Err(error)
358                if matches!(
359                    error.kind(),
360                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
361                ) =>
362            {
363                Ok(0)
364            }
365            other => other,
366        }
367    }
368}
369#[cfg(unix)]
370impl embedded_io::ErrorType for LinuxNonblockingSerialPort {
371    type Error = std::io::Error;
372}
373#[cfg(unix)]
374impl cu_serial::SerialIo for LinuxNonblockingSerialPort {
375    fn try_read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
376        let result = std::io::Read::read(&mut self.0, bytes);
377        if !bytes.is_empty() && matches!(result, Ok(0)) {
378            return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
379        }
380        Self::result(result)
381    }
382    fn try_write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
383        Self::result(std::io::Write::write(&mut self.0, bytes))
384    }
385}
386
387#[cfg(target_os = "linux")]
388pub type LinuxI2c = Exclusive<linux_embedded_hal::I2cdev>;
389#[cfg(target_os = "linux")]
390pub type LinuxOutputPin = Exclusive<rppal::gpio::IoPin>;
391#[cfg(target_os = "linux")]
392pub type LinuxInputPin = Exclusive<rppal::gpio::InputPin>;
393
394pub struct LinuxResources;
395
396bundle_resources!(
397    LinuxResources:
398        Serial0 = "serial0",
399        Serial1 = "serial1",
400        Serial2 = "serial2",
401        Serial3 = "serial3",
402        Serial4 = "serial4",
403        Serial5 = "serial5",
404        I2c0 = "i2c0",
405        I2c1 = "i2c1",
406        I2c2 = "i2c2",
407        Gpio0 = "gpio0",
408        Gpio1 = "gpio1",
409        Gpio2 = "gpio2",
410        Gpio3 = "gpio3",
411        Gpio4 = "gpio4",
412        Gpio5 = "gpio5",
413        Serial0Rts = "serial0_rts",
414        Serial1Rts = "serial1_rts",
415        Serial2Rts = "serial2_rts",
416        Serial3Rts = "serial3_rts",
417        Serial4Rts = "serial4_rts",
418        Serial5Rts = "serial5_rts"
419);
420
421const LINUX_RESOURCE_SLOT_NAMES: &[&str] = &[
422    SERIAL0_NAME,
423    SERIAL1_NAME,
424    SERIAL2_NAME,
425    SERIAL3_NAME,
426    SERIAL4_NAME,
427    SERIAL5_NAME,
428    I2C0_NAME,
429    I2C1_NAME,
430    I2C2_NAME,
431    GPIO0_NAME,
432    GPIO1_NAME,
433    GPIO2_NAME,
434    GPIO3_NAME,
435    GPIO4_NAME,
436    GPIO5_NAME,
437    "serial0_rts",
438    "serial1_rts",
439    "serial2_rts",
440    "serial3_rts",
441    "serial4_rts",
442    "serial5_rts",
443];
444
445struct SerialSlot {
446    id: LinuxResourcesId,
447    rts_id: LinuxResourcesId,
448    dev_key: &'static str,
449    baudrate_key: &'static str,
450    parity_key: &'static str,
451    stopbits_key: &'static str,
452    timeout_ms_key: &'static str,
453}
454
455#[derive(Clone, Debug)]
456pub struct SerialSlotConfig {
457    pub dev: String,
458    pub baudrate: u32,
459    pub parity: SerialParity,
460    pub stop_bits: SerialStopBits,
461    pub timeout_ms: u64,
462}
463
464const SERIAL_SLOTS: &[SerialSlot] = &[
465    SerialSlot {
466        id: LinuxResourcesId::Serial0,
467        rts_id: LinuxResourcesId::Serial0Rts,
468        dev_key: SERIAL0_DEV_KEY,
469        baudrate_key: SERIAL0_BAUDRATE_KEY,
470        parity_key: SERIAL0_PARITY_KEY,
471        stopbits_key: SERIAL0_STOPBITS_KEY,
472        timeout_ms_key: SERIAL0_TIMEOUT_MS_KEY,
473    },
474    SerialSlot {
475        id: LinuxResourcesId::Serial1,
476        rts_id: LinuxResourcesId::Serial1Rts,
477        dev_key: SERIAL1_DEV_KEY,
478        baudrate_key: SERIAL1_BAUDRATE_KEY,
479        parity_key: SERIAL1_PARITY_KEY,
480        stopbits_key: SERIAL1_STOPBITS_KEY,
481        timeout_ms_key: SERIAL1_TIMEOUT_MS_KEY,
482    },
483    SerialSlot {
484        id: LinuxResourcesId::Serial2,
485        rts_id: LinuxResourcesId::Serial2Rts,
486        dev_key: SERIAL2_DEV_KEY,
487        baudrate_key: SERIAL2_BAUDRATE_KEY,
488        parity_key: SERIAL2_PARITY_KEY,
489        stopbits_key: SERIAL2_STOPBITS_KEY,
490        timeout_ms_key: SERIAL2_TIMEOUT_MS_KEY,
491    },
492    SerialSlot {
493        id: LinuxResourcesId::Serial3,
494        rts_id: LinuxResourcesId::Serial3Rts,
495        dev_key: SERIAL3_DEV_KEY,
496        baudrate_key: SERIAL3_BAUDRATE_KEY,
497        parity_key: SERIAL3_PARITY_KEY,
498        stopbits_key: SERIAL3_STOPBITS_KEY,
499        timeout_ms_key: SERIAL3_TIMEOUT_MS_KEY,
500    },
501    SerialSlot {
502        id: LinuxResourcesId::Serial4,
503        rts_id: LinuxResourcesId::Serial4Rts,
504        dev_key: SERIAL4_DEV_KEY,
505        baudrate_key: SERIAL4_BAUDRATE_KEY,
506        parity_key: SERIAL4_PARITY_KEY,
507        stopbits_key: SERIAL4_STOPBITS_KEY,
508        timeout_ms_key: SERIAL4_TIMEOUT_MS_KEY,
509    },
510    SerialSlot {
511        id: LinuxResourcesId::Serial5,
512        rts_id: LinuxResourcesId::Serial5Rts,
513        dev_key: SERIAL5_DEV_KEY,
514        baudrate_key: SERIAL5_BAUDRATE_KEY,
515        parity_key: SERIAL5_PARITY_KEY,
516        stopbits_key: SERIAL5_STOPBITS_KEY,
517        timeout_ms_key: SERIAL5_TIMEOUT_MS_KEY,
518    },
519];
520
521#[cfg(target_os = "linux")]
522struct I2cSlot {
523    id: LinuxResourcesId,
524    dev_key: &'static str,
525}
526
527#[cfg(target_os = "linux")]
528const I2C_SLOTS: &[I2cSlot] = &[
529    I2cSlot {
530        id: LinuxResourcesId::I2c0,
531        dev_key: I2C0_DEV_KEY,
532    },
533    I2cSlot {
534        id: LinuxResourcesId::I2c1,
535        dev_key: I2C1_DEV_KEY,
536    },
537    I2cSlot {
538        id: LinuxResourcesId::I2c2,
539        dev_key: I2C2_DEV_KEY,
540    },
541];
542
543#[cfg_attr(not(any(target_os = "linux", test)), allow(dead_code))]
544struct GpioSlot {
545    id: LinuxResourcesId,
546    name: &'static str,
547    pin_key: &'static str,
548    direction_key: &'static str,
549    bias_key: &'static str,
550    initial_level_key: &'static str,
551}
552
553macro_rules! gpio_slot {
554    ($id:ident, $name:ident, $pin_key:ident, $direction_key:ident, $bias_key:ident, $initial_level_key:ident) => {
555        GpioSlot {
556            id: LinuxResourcesId::$id,
557            name: $name,
558            pin_key: $pin_key,
559            direction_key: $direction_key,
560            bias_key: $bias_key,
561            initial_level_key: $initial_level_key,
562        }
563    };
564}
565
566#[cfg(any(target_os = "linux", test))]
567#[derive(Copy, Clone, Debug, Eq, PartialEq)]
568enum GpioDirection {
569    Input,
570    Output,
571}
572
573#[cfg(any(target_os = "linux", test))]
574#[derive(Copy, Clone, Debug, Eq, PartialEq)]
575enum GpioBias {
576    Off,
577    PullDown,
578    PullUp,
579}
580
581#[cfg(any(target_os = "linux", test))]
582impl GpioBias {
583    #[cfg(target_os = "linux")]
584    const fn into_rppal(self) -> rppal::gpio::Bias {
585        match self {
586            GpioBias::Off => rppal::gpio::Bias::Off,
587            GpioBias::PullDown => rppal::gpio::Bias::PullDown,
588            GpioBias::PullUp => rppal::gpio::Bias::PullUp,
589        }
590    }
591}
592
593#[cfg(any(target_os = "linux", test))]
594#[derive(Copy, Clone, Debug, Eq, PartialEq)]
595enum GpioInitialLevel {
596    Low,
597    High,
598}
599
600#[cfg(any(target_os = "linux", test))]
601#[derive(Copy, Clone, Debug, Eq, PartialEq)]
602struct GpioSlotConfig {
603    pin: u8,
604    direction: GpioDirection,
605    bias: GpioBias,
606    initial_level: Option<GpioInitialLevel>,
607}
608
609const GPIO_SLOTS: &[GpioSlot] = &[
610    gpio_slot!(
611        Gpio0,
612        GPIO0_NAME,
613        GPIO0_PIN_KEY,
614        GPIO0_DIRECTION_KEY,
615        GPIO0_BIAS_KEY,
616        GPIO0_INITIAL_LEVEL_KEY
617    ),
618    gpio_slot!(
619        Gpio1,
620        GPIO1_NAME,
621        GPIO1_PIN_KEY,
622        GPIO1_DIRECTION_KEY,
623        GPIO1_BIAS_KEY,
624        GPIO1_INITIAL_LEVEL_KEY
625    ),
626    gpio_slot!(
627        Gpio2,
628        GPIO2_NAME,
629        GPIO2_PIN_KEY,
630        GPIO2_DIRECTION_KEY,
631        GPIO2_BIAS_KEY,
632        GPIO2_INITIAL_LEVEL_KEY
633    ),
634    gpio_slot!(
635        Gpio3,
636        GPIO3_NAME,
637        GPIO3_PIN_KEY,
638        GPIO3_DIRECTION_KEY,
639        GPIO3_BIAS_KEY,
640        GPIO3_INITIAL_LEVEL_KEY
641    ),
642    gpio_slot!(
643        Gpio4,
644        GPIO4_NAME,
645        GPIO4_PIN_KEY,
646        GPIO4_DIRECTION_KEY,
647        GPIO4_BIAS_KEY,
648        GPIO4_INITIAL_LEVEL_KEY
649    ),
650    gpio_slot!(
651        Gpio5,
652        GPIO5_NAME,
653        GPIO5_PIN_KEY,
654        GPIO5_DIRECTION_KEY,
655        GPIO5_BIAS_KEY,
656        GPIO5_INITIAL_LEVEL_KEY
657    ),
658];
659
660impl ResourceBundle for LinuxResources {
661    fn build(
662        bundle: cu29::resource::BundleContext<Self>,
663        config: Option<&ComponentConfig>,
664        manager: &mut ResourceManager,
665    ) -> CuResult<()> {
666        for slot in SERIAL_SLOTS {
667            let rts = read_serial_rts_config(config, slot)?;
668            let Some(serial_config) = read_serial_slot_config(config, slot)? else {
669                continue; // Skip slots without explicit config
670            };
671            let nonblocking_key = format!("{}_nonblocking", slot_name(slot.id));
672            let nonblocking = config
673                .and_then(|cfg| cfg.get::<bool>(&nonblocking_key).transpose())
674                .transpose()?
675                .unwrap_or(false);
676            if rts {
677                #[cfg(all(unix, feature = "serial-rts"))]
678                {
679                    if nonblocking {
680                        let (serial, pin) = LinuxNonblockingSerialPort::open_with_rts(
681                            &serial_config,
682                        )
683                        .map_err(|err| CuError::new_with_cause("Open serial RTS resource", err))?;
684                        manager.add_owned(bundle.key(slot.id), serial)?;
685                        manager.add_owned(bundle.key(slot.rts_id), pin)?;
686                    } else {
687                        let (serial, pin) = LinuxSerialPort::open_with_rts(&serial_config)
688                            .map_err(|err| {
689                                CuError::new_with_cause("Open serial RTS resource", err)
690                            })?;
691                        manager.add_owned(bundle.key(slot.id), serial)?;
692                        manager.add_owned(bundle.key(slot.rts_id), pin)?;
693                    }
694                    continue;
695                }
696                #[cfg(not(all(unix, feature = "serial-rts")))]
697                return Err(CuError::from(
698                    "Serial RTS requires Unix and the cu-linux-resources/serial-rts feature",
699                ));
700            }
701            if nonblocking {
702                #[cfg(unix)]
703                {
704                    let serial = LinuxNonblockingSerialPort::open_with_config(&serial_config)
705                        .map_err(|err| {
706                            CuError::new_with_cause("Open nonblocking serial resource", err)
707                        })?;
708                    manager.add_owned(bundle.key(slot.id), serial)?;
709                    continue;
710                }
711                #[cfg(not(unix))]
712                return Err(CuError::from("Nonblocking serial resources require Unix"));
713            }
714            match LinuxSerialPort::open_with_config(&serial_config) {
715                Ok(serial) => {
716                    manager.add_owned(bundle.key(slot.id), serial)?;
717                }
718                Err(err) => {
719                    warning!(
720                        "LinuxResources: skipping serial slot {} (dev {}): {}",
721                        slot_name(slot.id),
722                        serial_config.dev,
723                        err.to_string()
724                    );
725                }
726            }
727        }
728
729        #[cfg(target_os = "linux")]
730        for slot in I2C_SLOTS {
731            let Some(dev) = get_string(config, slot.dev_key)? else {
732                continue; // Skip slots without explicit config
733            };
734            match linux_embedded_hal::I2cdev::new(&dev) {
735                Ok(i2c) => {
736                    manager.add_owned(bundle.key(slot.id), Exclusive::new(i2c))?;
737                }
738                Err(err) => {
739                    warning!(
740                        "LinuxResources: skipping i2c slot {} (dev {}): {}",
741                        slot_name(slot.id),
742                        dev,
743                        err.to_string()
744                    );
745                }
746            }
747        }
748
749        #[cfg(target_os = "linux")]
750        {
751            let mut configured_gpio_slots: std::vec::Vec<(
752                LinuxResourcesId,
753                &'static str,
754                GpioSlotConfig,
755            )> = std::vec::Vec::new();
756
757            for slot in GPIO_SLOTS {
758                let Some(slot_config) = read_gpio_slot_config(config, slot)? else {
759                    continue;
760                };
761                configured_gpio_slots.push((slot.id, slot.name, slot_config));
762            }
763
764            if !configured_gpio_slots.is_empty() {
765                let gpio = rppal::gpio::Gpio::new().map_err(|err| {
766                    CuError::new_with_cause("Failed to initialize GPIO subsystem", err)
767                })?;
768
769                for (slot_id, slot_name, slot_config) in configured_gpio_slots {
770                    let pin = match gpio.get(slot_config.pin) {
771                        Ok(pin) => pin,
772                        Err(err) => {
773                            warning!(
774                                "LinuxResources: skipping gpio slot {} (pin {}): {}",
775                                slot_name,
776                                slot_config.pin,
777                                err.to_string()
778                            );
779                            continue;
780                        }
781                    };
782                    match slot_config.direction {
783                        GpioDirection::Input => {
784                            let mut pin = pin.into_input();
785                            pin.set_bias(slot_config.bias.into_rppal());
786                            manager.add_owned(bundle.key(slot_id), Exclusive::new(pin))?;
787                        }
788                        GpioDirection::Output => {
789                            let mut pin = pin.into_io(rppal::gpio::Mode::Output);
790                            pin.set_bias(slot_config.bias.into_rppal());
791                            if let Some(initial_level) = slot_config.initial_level {
792                                match initial_level {
793                                    GpioInitialLevel::Low => pin.set_low(),
794                                    GpioInitialLevel::High => pin.set_high(),
795                                }
796                            }
797                            manager.add_owned(bundle.key(slot_id), Exclusive::new(pin))?;
798                        }
799                    }
800                }
801            }
802        }
803
804        #[cfg(not(target_os = "linux"))]
805        {
806            for slot in GPIO_SLOTS {
807                if let Some(pin) = get_u8(config, slot.pin_key)? {
808                    warning!(
809                        "LinuxResources: requested gpio slot {} on pin {} but GPIO is only supported on Linux",
810                        slot_name(slot.id),
811                        pin
812                    );
813                }
814            }
815        }
816
817        Ok(())
818    }
819}
820
821fn read_serial_slot_config(
822    config: Option<&ComponentConfig>,
823    slot: &SerialSlot,
824) -> CuResult<Option<SerialSlotConfig>> {
825    let Some(dev) = get_string(config, slot.dev_key)? else {
826        return Ok(None); // No device configured for this slot
827    };
828    let baudrate = get_u32(config, slot.baudrate_key)?.unwrap_or(DEFAULT_SERIAL_BAUDRATE);
829    let parity = get_serial_parity(config, slot.parity_key)?.unwrap_or(DEFAULT_SERIAL_PARITY);
830    let stop_bits =
831        get_serial_stop_bits(config, slot.stopbits_key)?.unwrap_or(DEFAULT_SERIAL_STOPBITS);
832    let timeout_ms = get_u64(config, slot.timeout_ms_key)?.unwrap_or(DEFAULT_SERIAL_TIMEOUT_MS);
833
834    Ok(Some(SerialSlotConfig {
835        dev,
836        baudrate,
837        parity,
838        stop_bits,
839        timeout_ms,
840    }))
841}
842
843#[cfg(any(target_os = "linux", test))]
844fn read_gpio_slot_config(
845    config: Option<&ComponentConfig>,
846    slot: &GpioSlot,
847) -> CuResult<Option<GpioSlotConfig>> {
848    let pin = get_u8(config, slot.pin_key)?;
849    let direction = get_string(config, slot.direction_key)?;
850    let bias = get_string(config, slot.bias_key)?;
851    let initial_level = get_string(config, slot.initial_level_key)?;
852
853    let Some(pin) = pin else {
854        if direction.is_some() || bias.is_some() || initial_level.is_some() {
855            return Err(CuError::from(format!(
856                "Config key '{}' is required when configuring {}",
857                slot.pin_key, slot.name
858            )));
859        }
860        return Ok(None);
861    };
862
863    let direction_raw = direction.ok_or_else(|| {
864        CuError::from(format!(
865            "Config key '{}' is required when '{}' is set",
866            slot.direction_key, slot.pin_key
867        ))
868    })?;
869    let direction = parse_gpio_direction_value(direction_raw.as_str())?;
870
871    let bias = match bias {
872        Some(raw) => parse_gpio_bias_value(raw.as_str())?,
873        None => GpioBias::Off,
874    };
875
876    let initial_level = match initial_level {
877        Some(raw) => Some(parse_gpio_initial_level_value(raw.as_str())?),
878        None => None,
879    };
880
881    if matches!(direction, GpioDirection::Input) && initial_level.is_some() {
882        return Err(CuError::from(format!(
883            "Config key '{}' is only valid when '{}' is 'output'",
884            slot.initial_level_key, slot.direction_key
885        )));
886    }
887
888    Ok(Some(GpioSlotConfig {
889        pin,
890        direction,
891        bias,
892        initial_level,
893    }))
894}
895
896fn get_serial_parity(
897    config: Option<&ComponentConfig>,
898    key: &str,
899) -> CuResult<Option<SerialParity>> {
900    let Some(raw) = get_string(config, key)? else {
901        return Ok(None);
902    };
903    Ok(Some(parse_serial_parity_value(raw.as_str())?))
904}
905
906fn get_serial_stop_bits(
907    config: Option<&ComponentConfig>,
908    key: &str,
909) -> CuResult<Option<SerialStopBits>> {
910    let Some(raw) = get_u8(config, key)? else {
911        return Ok(None);
912    };
913    Ok(Some(parse_serial_stop_bits_value(raw)?))
914}
915
916fn parse_serial_parity_value(raw: &str) -> CuResult<SerialParity> {
917    let normalized = raw.trim().to_ascii_lowercase();
918    match normalized.as_str() {
919        "none" => Ok(SerialParity::None),
920        "odd" => Ok(SerialParity::Odd),
921        "even" => Ok(SerialParity::Even),
922        _ => Err(CuError::from(format!(
923            "Invalid parity '{raw}'. Expected one of: none, odd, even"
924        ))),
925    }
926}
927
928fn parse_serial_stop_bits_value(raw: u8) -> CuResult<SerialStopBits> {
929    match raw {
930        1 => Ok(SerialStopBits::One),
931        2 => Ok(SerialStopBits::Two),
932        _ => Err(CuError::from(format!(
933            "Invalid stopbits value '{raw}'. Expected 1 or 2"
934        ))),
935    }
936}
937
938#[cfg(any(target_os = "linux", test))]
939fn parse_gpio_direction_value(raw: &str) -> CuResult<GpioDirection> {
940    let normalized = raw.trim().to_ascii_lowercase();
941    match normalized.as_str() {
942        "input" => Ok(GpioDirection::Input),
943        "output" => Ok(GpioDirection::Output),
944        _ => Err(CuError::from(format!(
945            "Invalid GPIO direction '{raw}'. Expected one of: input, output"
946        ))),
947    }
948}
949
950#[cfg(any(target_os = "linux", test))]
951fn parse_gpio_bias_value(raw: &str) -> CuResult<GpioBias> {
952    let normalized = raw.trim().to_ascii_lowercase();
953    match normalized.as_str() {
954        "off" | "none" => Ok(GpioBias::Off),
955        "pull_down" | "pulldown" => Ok(GpioBias::PullDown),
956        "pull_up" | "pullup" => Ok(GpioBias::PullUp),
957        _ => Err(CuError::from(format!(
958            "Invalid GPIO bias '{raw}'. Expected one of: off, pull_up, pull_down"
959        ))),
960    }
961}
962
963#[cfg(any(target_os = "linux", test))]
964fn parse_gpio_initial_level_value(raw: &str) -> CuResult<GpioInitialLevel> {
965    let normalized = raw.trim().to_ascii_lowercase();
966    match normalized.as_str() {
967        "low" => Ok(GpioInitialLevel::Low),
968        "high" => Ok(GpioInitialLevel::High),
969        _ => Err(CuError::from(format!(
970            "Invalid GPIO initial level '{raw}'. Expected one of: low, high"
971        ))),
972    }
973}
974
975fn read_serial_rts_config(config: Option<&ComponentConfig>, slot: &SerialSlot) -> CuResult<bool> {
976    let key = slot_name(slot.rts_id);
977    let enabled = config
978        .and_then(|cfg| cfg.get::<bool>(key).transpose())
979        .transpose()?
980        .unwrap_or(false);
981    if enabled && get_string(config, slot.dev_key)?.is_none() {
982        return Err(CuError::from(format!("{key} requires {}", slot.dev_key)));
983    }
984    Ok(enabled)
985}
986
987fn slot_name(id: LinuxResourcesId) -> &'static str {
988    LINUX_RESOURCE_SLOT_NAMES[id as usize]
989}
990
991fn get_string(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<String>> {
992    match config {
993        Some(cfg) => Ok(cfg.get::<String>(key)?.filter(|value| !value.is_empty())),
994        None => Ok(None),
995    }
996}
997
998fn get_u8(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u8>> {
999    match config {
1000        Some(cfg) => Ok(cfg.get::<u8>(key)?),
1001        None => Ok(None),
1002    }
1003}
1004
1005fn get_u32(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u32>> {
1006    match config {
1007        Some(cfg) => Ok(cfg.get::<u32>(key)?),
1008        None => Ok(None),
1009    }
1010}
1011
1012fn get_u64(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u64>> {
1013    match config {
1014        Some(cfg) => Ok(cfg.get::<u64>(key)?),
1015        None => Ok(None),
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022    use cu29::resource::{NamedResourceBundleDecl, resource_index_by_name};
1023
1024    #[test]
1025    fn named_resource_bundle_decl_matches_public_linux_slot_names() {
1026        let declared = <LinuxResources as NamedResourceBundleDecl>::NAMES;
1027        assert_eq!(declared, LINUX_RESOURCE_SLOT_NAMES);
1028
1029        for (idx, name) in declared.iter().enumerate() {
1030            assert_eq!(resource_index_by_name::<LinuxResources>(name), idx);
1031        }
1032    }
1033
1034    fn rts_config(value: &str) -> ComponentConfig {
1035        let ron = format!(
1036            r#"(resources: [(id: "board", provider: "cu_linux_resources::LinuxResources",
1037                config: {{ "serial0_rts": {value} }})], tasks: [], cnx: [])"#,
1038        );
1039        cu29::config::CuConfig::deserialize_ron(&ron)
1040            .unwrap()
1041            .resources
1042            .remove(0)
1043            .config
1044            .unwrap()
1045    }
1046
1047    #[test]
1048    fn serial_rts_is_opt_in_and_requires_a_device() {
1049        let slot = &SERIAL_SLOTS[0];
1050        assert!(!read_serial_rts_config(None, slot).unwrap());
1051        let mut config = rts_config("false");
1052        assert!(!read_serial_rts_config(Some(&config), slot).unwrap());
1053        config = rts_config("true");
1054        assert!(read_serial_rts_config(Some(&config), slot).is_err());
1055        config.set("serial0_dev", "/dev/ttyUSB0".to_string());
1056        assert!(read_serial_rts_config(Some(&config), slot).unwrap());
1057        assert!(!read_serial_rts_config(Some(&config), &SERIAL_SLOTS[1]).unwrap());
1058        config.set("serial0_rts", "invalid".to_string());
1059        assert!(read_serial_rts_config(Some(&config), slot).is_err());
1060    }
1061
1062    #[cfg(not(feature = "serial-rts"))]
1063    #[test]
1064    fn serial_rts_request_without_feature_fails_before_opening_the_device() {
1065        let mut config = rts_config("true");
1066        config.set("serial0_dev", "/not/a/serial/device".to_string());
1067        let mut manager = ResourceManager::new(&[LINUX_RESOURCE_SLOT_NAMES.len()]);
1068        let error = LinuxResources::build(
1069            cu29::resource::BundleContext::new(cu29::resource::BundleIndex::new(0), "board"),
1070            Some(&config),
1071            &mut manager,
1072        )
1073        .unwrap_err();
1074        assert!(error.to_string().contains("serial-rts feature"));
1075    }
1076
1077    #[test]
1078    fn parse_serial_parity_value_accepts_expected_inputs() {
1079        assert!(matches!(
1080            parse_serial_parity_value("none").unwrap(),
1081            SerialParity::None
1082        ));
1083        assert!(matches!(
1084            parse_serial_parity_value("Odd").unwrap(),
1085            SerialParity::Odd
1086        ));
1087        assert!(matches!(
1088            parse_serial_parity_value("EVEN").unwrap(),
1089            SerialParity::Even
1090        ));
1091    }
1092
1093    #[test]
1094    fn parse_serial_parity_value_rejects_invalid_input() {
1095        assert!(parse_serial_parity_value("mark").is_err());
1096    }
1097
1098    #[test]
1099    fn parse_serial_stop_bits_value_accepts_expected_inputs() {
1100        assert!(matches!(
1101            parse_serial_stop_bits_value(1).unwrap(),
1102            SerialStopBits::One
1103        ));
1104        assert!(matches!(
1105            parse_serial_stop_bits_value(2).unwrap(),
1106            SerialStopBits::Two
1107        ));
1108    }
1109
1110    #[test]
1111    fn parse_serial_stop_bits_value_rejects_invalid_input() {
1112        assert!(parse_serial_stop_bits_value(0).is_err());
1113        assert!(parse_serial_stop_bits_value(3).is_err());
1114    }
1115
1116    #[test]
1117    fn parse_gpio_direction_value_accepts_expected_inputs() {
1118        assert!(matches!(
1119            parse_gpio_direction_value("input").unwrap(),
1120            GpioDirection::Input
1121        ));
1122        assert!(matches!(
1123            parse_gpio_direction_value("Output").unwrap(),
1124            GpioDirection::Output
1125        ));
1126    }
1127
1128    #[test]
1129    fn parse_gpio_direction_value_rejects_invalid_input() {
1130        assert!(parse_gpio_direction_value("io").is_err());
1131    }
1132
1133    #[test]
1134    fn parse_gpio_bias_value_accepts_expected_inputs() {
1135        assert!(matches!(
1136            parse_gpio_bias_value("off").unwrap(),
1137            GpioBias::Off
1138        ));
1139        assert!(matches!(
1140            parse_gpio_bias_value("pull_down").unwrap(),
1141            GpioBias::PullDown
1142        ));
1143        assert!(matches!(
1144            parse_gpio_bias_value("PullUp").unwrap(),
1145            GpioBias::PullUp
1146        ));
1147    }
1148
1149    #[test]
1150    fn parse_gpio_bias_value_rejects_invalid_input() {
1151        assert!(parse_gpio_bias_value("hold").is_err());
1152    }
1153
1154    #[test]
1155    fn parse_gpio_initial_level_value_accepts_expected_inputs() {
1156        assert!(matches!(
1157            parse_gpio_initial_level_value("low").unwrap(),
1158            GpioInitialLevel::Low
1159        ));
1160        assert!(matches!(
1161            parse_gpio_initial_level_value("HIGH").unwrap(),
1162            GpioInitialLevel::High
1163        ));
1164    }
1165
1166    #[test]
1167    fn parse_gpio_initial_level_value_rejects_invalid_input() {
1168        assert!(parse_gpio_initial_level_value("toggle").is_err());
1169    }
1170
1171    #[test]
1172    fn read_gpio_slot_config_requires_pin_when_aux_keys_are_present() {
1173        let mut cfg = ComponentConfig::new();
1174        cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
1175        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1176    }
1177
1178    #[test]
1179    fn read_gpio_slot_config_requires_direction_when_pin_is_set() {
1180        let mut cfg = ComponentConfig::new();
1181        cfg.set(GPIO0_PIN_KEY, 23_u8);
1182        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1183    }
1184
1185    #[test]
1186    fn read_gpio_slot_config_defaults_bias_and_preserves_level_for_output() {
1187        let mut cfg = ComponentConfig::new();
1188        cfg.set(GPIO0_PIN_KEY, 23_u8);
1189        cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
1190
1191        let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
1192            .unwrap()
1193            .expect("slot should be configured");
1194        assert_eq!(
1195            slot_config,
1196            GpioSlotConfig {
1197                pin: 23,
1198                direction: GpioDirection::Output,
1199                bias: GpioBias::Off,
1200                initial_level: None,
1201            }
1202        );
1203    }
1204
1205    #[test]
1206    fn read_gpio_slot_config_parses_explicit_initial_level_for_output() {
1207        let mut cfg = ComponentConfig::new();
1208        cfg.set(GPIO0_PIN_KEY, 23_u8);
1209        cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
1210        cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1211
1212        let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
1213            .unwrap()
1214            .expect("slot should be configured");
1215        assert_eq!(
1216            slot_config,
1217            GpioSlotConfig {
1218                pin: 23,
1219                direction: GpioDirection::Output,
1220                bias: GpioBias::Off,
1221                initial_level: Some(GpioInitialLevel::High),
1222            }
1223        );
1224    }
1225
1226    #[test]
1227    fn read_gpio_slot_config_rejects_initial_level_for_input() {
1228        let mut cfg = ComponentConfig::new();
1229        cfg.set(GPIO0_PIN_KEY, 23_u8);
1230        cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
1231        cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1232        assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1233    }
1234
1235    struct MockIo {
1236        rx: [u8; 4],
1237        rx_len: usize,
1238        tx: [u8; 4],
1239        tx_len: usize,
1240    }
1241
1242    impl MockIo {
1243        fn new(rx: &[u8]) -> Self {
1244            let mut buf = [0_u8; 4];
1245            buf[..rx.len()].copy_from_slice(rx);
1246            Self {
1247                rx: buf,
1248                rx_len: rx.len(),
1249                tx: [0; 4],
1250                tx_len: 0,
1251            }
1252        }
1253    }
1254
1255    impl embedded_io::ErrorType for MockIo {
1256        type Error = core::convert::Infallible;
1257    }
1258
1259    impl embedded_io::Read for MockIo {
1260        fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
1261            let n = core::cmp::min(buf.len(), self.rx_len);
1262            buf[..n].copy_from_slice(&self.rx[..n]);
1263            Ok(n)
1264        }
1265    }
1266
1267    impl embedded_io::Write for MockIo {
1268        fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
1269            let n = core::cmp::min(buf.len(), self.tx.len());
1270            self.tx[..n].copy_from_slice(&buf[..n]);
1271            self.tx_len = n;
1272            Ok(n)
1273        }
1274
1275        fn flush(&mut self) -> Result<(), Self::Error> {
1276            Ok(())
1277        }
1278    }
1279
1280    #[test]
1281    fn exclusive_forwards_embedded_io_traits() {
1282        let mut wrapped = Exclusive::new(MockIo::new(&[1, 2, 3]));
1283
1284        let mut rx = [0_u8; 4];
1285        let read = embedded_io::Read::read(&mut wrapped, &mut rx).unwrap();
1286        assert_eq!(read, 3);
1287        assert_eq!(&rx[..3], &[1, 2, 3]);
1288
1289        let written = embedded_io::Write::write(&mut wrapped, &[9, 8]).unwrap();
1290        assert_eq!(written, 2);
1291        embedded_io::Write::flush(&mut wrapped).unwrap();
1292
1293        let inner = wrapped.into_inner();
1294        assert_eq!(&inner.tx[..inner.tx_len], &[9, 8]);
1295    }
1296}
1297
1298#[cfg(all(test, unix))]
1299mod nonblocking_tests {
1300    use super::*;
1301    use cu_serial::SerialIo;
1302    #[test]
1303    fn native_uart_reports_backpressure_and_preserves_binary_bytes() {
1304        let (a, b) = serialport::TTYPort::pair().unwrap();
1305        let mut tx = LinuxNonblockingSerialPort::from_native(a).unwrap();
1306        let mut rx = LinuxNonblockingSerialPort::from_native(b).unwrap();
1307        let mut buf = [0; 16];
1308        assert_eq!(rx.try_read(&mut buf).unwrap(), 0);
1309        assert_eq!(tx.try_write(&[0, 0x7e, 0x7d, 255]).unwrap(), 4);
1310        let mut received = 0;
1311        for _ in 0..1000 {
1312            received += rx.try_read(&mut buf[received..4]).unwrap();
1313            if received == 4 {
1314                break;
1315            }
1316            std::thread::yield_now();
1317        }
1318        assert_eq!(&buf[..received], &[0, 0x7e, 0x7d, 255]);
1319        let data = [42; 65536];
1320        let written = tx.try_write(&data).unwrap();
1321        assert!(written > 0 && written < data.len());
1322        let mut blocked = false;
1323        for _ in 0..128 {
1324            if tx.try_write(&data).unwrap() == 0 {
1325                blocked = true;
1326                break;
1327            }
1328        }
1329        assert!(blocked);
1330    }
1331}