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
93pub 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
114unsafe impl<T: Send> Sync for Exclusive<T> {}
117
118impl<T: std::io::Read> std::io::Read for Exclusive<T> {
119 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
120 self.0.read(buf)
121 }
122}
123
124impl<T: std::io::Write> std::io::Write for Exclusive<T> {
125 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
126 self.0.write(buf)
127 }
128
129 fn flush(&mut self) -> std::io::Result<()> {
130 self.0.flush()
131 }
132}
133
134impl<T: EmbeddedErrorType> EmbeddedErrorType for Exclusive<T> {
135 type Error = T::Error;
136}
137
138impl<T: EmbeddedRead> EmbeddedRead for Exclusive<T> {
139 fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
140 self.0.read(buf)
141 }
142}
143
144impl<T: EmbeddedWrite> EmbeddedWrite for Exclusive<T> {
145 fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
146 self.0.write(buf)
147 }
148
149 fn flush(&mut self) -> Result<(), Self::Error> {
150 self.0.flush()
151 }
152}
153
154impl<T> embedded_hal::i2c::ErrorType for Exclusive<T>
155where
156 T: embedded_hal::i2c::ErrorType,
157{
158 type Error = T::Error;
159}
160
161impl<T> embedded_hal::i2c::I2c for Exclusive<T>
162where
163 T: embedded_hal::i2c::I2c,
164{
165 fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> {
166 self.0.read(address, read)
167 }
168
169 fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> {
170 self.0.write(address, write)
171 }
172
173 fn write_read(
174 &mut self,
175 address: u8,
176 write: &[u8],
177 read: &mut [u8],
178 ) -> Result<(), Self::Error> {
179 self.0.write_read(address, write, read)
180 }
181
182 fn transaction(
183 &mut self,
184 address: u8,
185 operations: &mut [embedded_hal::i2c::Operation<'_>],
186 ) -> Result<(), Self::Error> {
187 self.0.transaction(address, operations)
188 }
189}
190
191impl<T> embedded_hal::digital::ErrorType for Exclusive<T>
192where
193 T: embedded_hal::digital::ErrorType,
194{
195 type Error = T::Error;
196}
197
198impl<T> embedded_hal::digital::OutputPin for Exclusive<T>
199where
200 T: embedded_hal::digital::OutputPin,
201{
202 fn set_low(&mut self) -> Result<(), Self::Error> {
203 self.0.set_low()
204 }
205
206 fn set_high(&mut self) -> Result<(), Self::Error> {
207 self.0.set_high()
208 }
209}
210
211impl<T> embedded_hal::digital::StatefulOutputPin for Exclusive<T>
212where
213 T: embedded_hal::digital::StatefulOutputPin,
214{
215 fn is_set_high(&mut self) -> Result<bool, Self::Error> {
216 self.0.is_set_high()
217 }
218
219 fn is_set_low(&mut self) -> Result<bool, Self::Error> {
220 self.0.is_set_low()
221 }
222}
223
224impl<T> embedded_hal::digital::InputPin for Exclusive<T>
225where
226 T: embedded_hal::digital::InputPin,
227{
228 fn is_high(&mut self) -> Result<bool, Self::Error> {
229 self.0.is_high()
230 }
231
232 fn is_low(&mut self) -> Result<bool, Self::Error> {
233 self.0.is_low()
234 }
235}
236
237pub struct LinuxSerialPort {
238 inner: Exclusive<Box<dyn serialport::SerialPort>>,
239}
240
241impl LinuxSerialPort {
242 pub fn new(inner: Box<dyn serialport::SerialPort>) -> Self {
243 Self {
244 inner: Exclusive::new(inner),
245 }
246 }
247
248 pub fn open(dev: &str, baudrate: u32, timeout_ms: u64) -> std::io::Result<Self> {
249 let config = SerialSlotConfig {
250 dev: dev.to_string(),
251 baudrate,
252 parity: DEFAULT_SERIAL_PARITY,
253 stop_bits: DEFAULT_SERIAL_STOPBITS,
254 timeout_ms,
255 };
256 Self::open_with_config(&config)
257 }
258
259 pub fn open_with_config(config: &SerialSlotConfig) -> std::io::Result<Self> {
260 let port = serialport::new(config.dev.as_str(), config.baudrate)
261 .parity(config.parity)
262 .stop_bits(config.stop_bits)
263 .timeout(std::time::Duration::from_millis(config.timeout_ms))
264 .open()?;
265 Ok(Self::new(port))
266 }
267}
268
269impl std::io::Read for LinuxSerialPort {
270 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
271 self.inner.read(buf)
272 }
273}
274
275impl std::io::Write for LinuxSerialPort {
276 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
277 self.inner.write(buf)
278 }
279
280 fn flush(&mut self) -> std::io::Result<()> {
281 self.inner.flush()
282 }
283}
284
285impl embedded_io::ErrorType for LinuxSerialPort {
286 type Error = std::io::Error;
287}
288
289impl EmbeddedRead for LinuxSerialPort {
290 fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
291 std::io::Read::read(self, buf)
292 }
293}
294
295impl EmbeddedWrite for LinuxSerialPort {
296 fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
297 std::io::Write::write(self, buf)
298 }
299
300 fn flush(&mut self) -> Result<(), Self::Error> {
301 std::io::Write::flush(self)
302 }
303}
304
305#[cfg(target_os = "linux")]
306pub type LinuxI2c = Exclusive<linux_embedded_hal::I2cdev>;
307#[cfg(target_os = "linux")]
308pub type LinuxOutputPin = Exclusive<rppal::gpio::IoPin>;
309#[cfg(target_os = "linux")]
310pub type LinuxInputPin = Exclusive<rppal::gpio::InputPin>;
311
312pub struct LinuxResources;
313
314bundle_resources!(
315 LinuxResources:
316 Serial0,
317 Serial1,
318 Serial2,
319 Serial3,
320 Serial4,
321 Serial5,
322 I2c0,
323 I2c1,
324 I2c2,
325 Gpio0,
326 Gpio1,
327 Gpio2,
328 Gpio3,
329 Gpio4,
330 Gpio5
331);
332
333const LINUX_RESOURCE_SLOT_NAMES: &[&str] = &[
334 SERIAL0_NAME,
335 SERIAL1_NAME,
336 SERIAL2_NAME,
337 SERIAL3_NAME,
338 SERIAL4_NAME,
339 SERIAL5_NAME,
340 I2C0_NAME,
341 I2C1_NAME,
342 I2C2_NAME,
343 GPIO0_NAME,
344 GPIO1_NAME,
345 GPIO2_NAME,
346 GPIO3_NAME,
347 GPIO4_NAME,
348 GPIO5_NAME,
349];
350
351struct SerialSlot {
352 id: LinuxResourcesId,
353 dev_key: &'static str,
354 baudrate_key: &'static str,
355 parity_key: &'static str,
356 stopbits_key: &'static str,
357 timeout_ms_key: &'static str,
358}
359
360#[derive(Clone, Debug)]
361pub struct SerialSlotConfig {
362 pub dev: String,
363 pub baudrate: u32,
364 pub parity: SerialParity,
365 pub stop_bits: SerialStopBits,
366 pub timeout_ms: u64,
367}
368
369const SERIAL_SLOTS: &[SerialSlot] = &[
370 SerialSlot {
371 id: LinuxResourcesId::Serial0,
372 dev_key: SERIAL0_DEV_KEY,
373 baudrate_key: SERIAL0_BAUDRATE_KEY,
374 parity_key: SERIAL0_PARITY_KEY,
375 stopbits_key: SERIAL0_STOPBITS_KEY,
376 timeout_ms_key: SERIAL0_TIMEOUT_MS_KEY,
377 },
378 SerialSlot {
379 id: LinuxResourcesId::Serial1,
380 dev_key: SERIAL1_DEV_KEY,
381 baudrate_key: SERIAL1_BAUDRATE_KEY,
382 parity_key: SERIAL1_PARITY_KEY,
383 stopbits_key: SERIAL1_STOPBITS_KEY,
384 timeout_ms_key: SERIAL1_TIMEOUT_MS_KEY,
385 },
386 SerialSlot {
387 id: LinuxResourcesId::Serial2,
388 dev_key: SERIAL2_DEV_KEY,
389 baudrate_key: SERIAL2_BAUDRATE_KEY,
390 parity_key: SERIAL2_PARITY_KEY,
391 stopbits_key: SERIAL2_STOPBITS_KEY,
392 timeout_ms_key: SERIAL2_TIMEOUT_MS_KEY,
393 },
394 SerialSlot {
395 id: LinuxResourcesId::Serial3,
396 dev_key: SERIAL3_DEV_KEY,
397 baudrate_key: SERIAL3_BAUDRATE_KEY,
398 parity_key: SERIAL3_PARITY_KEY,
399 stopbits_key: SERIAL3_STOPBITS_KEY,
400 timeout_ms_key: SERIAL3_TIMEOUT_MS_KEY,
401 },
402 SerialSlot {
403 id: LinuxResourcesId::Serial4,
404 dev_key: SERIAL4_DEV_KEY,
405 baudrate_key: SERIAL4_BAUDRATE_KEY,
406 parity_key: SERIAL4_PARITY_KEY,
407 stopbits_key: SERIAL4_STOPBITS_KEY,
408 timeout_ms_key: SERIAL4_TIMEOUT_MS_KEY,
409 },
410 SerialSlot {
411 id: LinuxResourcesId::Serial5,
412 dev_key: SERIAL5_DEV_KEY,
413 baudrate_key: SERIAL5_BAUDRATE_KEY,
414 parity_key: SERIAL5_PARITY_KEY,
415 stopbits_key: SERIAL5_STOPBITS_KEY,
416 timeout_ms_key: SERIAL5_TIMEOUT_MS_KEY,
417 },
418];
419
420#[cfg(target_os = "linux")]
421struct I2cSlot {
422 id: LinuxResourcesId,
423 dev_key: &'static str,
424}
425
426#[cfg(target_os = "linux")]
427const I2C_SLOTS: &[I2cSlot] = &[
428 I2cSlot {
429 id: LinuxResourcesId::I2c0,
430 dev_key: I2C0_DEV_KEY,
431 },
432 I2cSlot {
433 id: LinuxResourcesId::I2c1,
434 dev_key: I2C1_DEV_KEY,
435 },
436 I2cSlot {
437 id: LinuxResourcesId::I2c2,
438 dev_key: I2C2_DEV_KEY,
439 },
440];
441
442#[cfg_attr(not(any(target_os = "linux", test)), allow(dead_code))]
443struct GpioSlot {
444 id: LinuxResourcesId,
445 name: &'static str,
446 pin_key: &'static str,
447 direction_key: &'static str,
448 bias_key: &'static str,
449 initial_level_key: &'static str,
450}
451
452macro_rules! gpio_slot {
453 ($id:ident, $name:ident, $pin_key:ident, $direction_key:ident, $bias_key:ident, $initial_level_key:ident) => {
454 GpioSlot {
455 id: LinuxResourcesId::$id,
456 name: $name,
457 pin_key: $pin_key,
458 direction_key: $direction_key,
459 bias_key: $bias_key,
460 initial_level_key: $initial_level_key,
461 }
462 };
463}
464
465#[cfg(any(target_os = "linux", test))]
466#[derive(Copy, Clone, Debug, Eq, PartialEq)]
467enum GpioDirection {
468 Input,
469 Output,
470}
471
472#[cfg(any(target_os = "linux", test))]
473#[derive(Copy, Clone, Debug, Eq, PartialEq)]
474enum GpioBias {
475 Off,
476 PullDown,
477 PullUp,
478}
479
480#[cfg(any(target_os = "linux", test))]
481impl GpioBias {
482 #[cfg(target_os = "linux")]
483 const fn into_rppal(self) -> rppal::gpio::Bias {
484 match self {
485 GpioBias::Off => rppal::gpio::Bias::Off,
486 GpioBias::PullDown => rppal::gpio::Bias::PullDown,
487 GpioBias::PullUp => rppal::gpio::Bias::PullUp,
488 }
489 }
490}
491
492#[cfg(any(target_os = "linux", test))]
493#[derive(Copy, Clone, Debug, Eq, PartialEq)]
494enum GpioInitialLevel {
495 Low,
496 High,
497}
498
499#[cfg(any(target_os = "linux", test))]
500#[derive(Copy, Clone, Debug, Eq, PartialEq)]
501struct GpioSlotConfig {
502 pin: u8,
503 direction: GpioDirection,
504 bias: GpioBias,
505 initial_level: Option<GpioInitialLevel>,
506}
507
508const GPIO_SLOTS: &[GpioSlot] = &[
509 gpio_slot!(
510 Gpio0,
511 GPIO0_NAME,
512 GPIO0_PIN_KEY,
513 GPIO0_DIRECTION_KEY,
514 GPIO0_BIAS_KEY,
515 GPIO0_INITIAL_LEVEL_KEY
516 ),
517 gpio_slot!(
518 Gpio1,
519 GPIO1_NAME,
520 GPIO1_PIN_KEY,
521 GPIO1_DIRECTION_KEY,
522 GPIO1_BIAS_KEY,
523 GPIO1_INITIAL_LEVEL_KEY
524 ),
525 gpio_slot!(
526 Gpio2,
527 GPIO2_NAME,
528 GPIO2_PIN_KEY,
529 GPIO2_DIRECTION_KEY,
530 GPIO2_BIAS_KEY,
531 GPIO2_INITIAL_LEVEL_KEY
532 ),
533 gpio_slot!(
534 Gpio3,
535 GPIO3_NAME,
536 GPIO3_PIN_KEY,
537 GPIO3_DIRECTION_KEY,
538 GPIO3_BIAS_KEY,
539 GPIO3_INITIAL_LEVEL_KEY
540 ),
541 gpio_slot!(
542 Gpio4,
543 GPIO4_NAME,
544 GPIO4_PIN_KEY,
545 GPIO4_DIRECTION_KEY,
546 GPIO4_BIAS_KEY,
547 GPIO4_INITIAL_LEVEL_KEY
548 ),
549 gpio_slot!(
550 Gpio5,
551 GPIO5_NAME,
552 GPIO5_PIN_KEY,
553 GPIO5_DIRECTION_KEY,
554 GPIO5_BIAS_KEY,
555 GPIO5_INITIAL_LEVEL_KEY
556 ),
557];
558
559impl ResourceBundle for LinuxResources {
560 fn build(
561 bundle: cu29::resource::BundleContext<Self>,
562 config: Option<&ComponentConfig>,
563 manager: &mut ResourceManager,
564 ) -> CuResult<()> {
565 for slot in SERIAL_SLOTS {
566 let Some(serial_config) = read_serial_slot_config(config, slot)? else {
567 continue; };
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; };
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); };
683 let baudrate = get_u32(config, slot.baudrate_key)?.unwrap_or(DEFAULT_SERIAL_BAUDRATE);
684 let parity = get_serial_parity(config, slot.parity_key)?.unwrap_or(DEFAULT_SERIAL_PARITY);
685 let stop_bits =
686 get_serial_stop_bits(config, slot.stopbits_key)?.unwrap_or(DEFAULT_SERIAL_STOPBITS);
687 let timeout_ms = get_u64(config, slot.timeout_ms_key)?.unwrap_or(DEFAULT_SERIAL_TIMEOUT_MS);
688
689 Ok(Some(SerialSlotConfig {
690 dev,
691 baudrate,
692 parity,
693 stop_bits,
694 timeout_ms,
695 }))
696}
697
698#[cfg(any(target_os = "linux", test))]
699fn read_gpio_slot_config(
700 config: Option<&ComponentConfig>,
701 slot: &GpioSlot,
702) -> CuResult<Option<GpioSlotConfig>> {
703 let pin = get_u8(config, slot.pin_key)?;
704 let direction = get_string(config, slot.direction_key)?;
705 let bias = get_string(config, slot.bias_key)?;
706 let initial_level = get_string(config, slot.initial_level_key)?;
707
708 let Some(pin) = pin else {
709 if direction.is_some() || bias.is_some() || initial_level.is_some() {
710 return Err(CuError::from(format!(
711 "Config key '{}' is required when configuring {}",
712 slot.pin_key, slot.name
713 )));
714 }
715 return Ok(None);
716 };
717
718 let direction_raw = direction.ok_or_else(|| {
719 CuError::from(format!(
720 "Config key '{}' is required when '{}' is set",
721 slot.direction_key, slot.pin_key
722 ))
723 })?;
724 let direction = parse_gpio_direction_value(direction_raw.as_str())?;
725
726 let bias = match bias {
727 Some(raw) => parse_gpio_bias_value(raw.as_str())?,
728 None => GpioBias::Off,
729 };
730
731 let initial_level = match initial_level {
732 Some(raw) => Some(parse_gpio_initial_level_value(raw.as_str())?),
733 None => None,
734 };
735
736 if matches!(direction, GpioDirection::Input) && initial_level.is_some() {
737 return Err(CuError::from(format!(
738 "Config key '{}' is only valid when '{}' is 'output'",
739 slot.initial_level_key, slot.direction_key
740 )));
741 }
742
743 Ok(Some(GpioSlotConfig {
744 pin,
745 direction,
746 bias,
747 initial_level,
748 }))
749}
750
751fn get_serial_parity(
752 config: Option<&ComponentConfig>,
753 key: &str,
754) -> CuResult<Option<SerialParity>> {
755 let Some(raw) = get_string(config, key)? else {
756 return Ok(None);
757 };
758 Ok(Some(parse_serial_parity_value(raw.as_str())?))
759}
760
761fn get_serial_stop_bits(
762 config: Option<&ComponentConfig>,
763 key: &str,
764) -> CuResult<Option<SerialStopBits>> {
765 let Some(raw) = get_u8(config, key)? else {
766 return Ok(None);
767 };
768 Ok(Some(parse_serial_stop_bits_value(raw)?))
769}
770
771fn parse_serial_parity_value(raw: &str) -> CuResult<SerialParity> {
772 let normalized = raw.trim().to_ascii_lowercase();
773 match normalized.as_str() {
774 "none" => Ok(SerialParity::None),
775 "odd" => Ok(SerialParity::Odd),
776 "even" => Ok(SerialParity::Even),
777 _ => Err(CuError::from(format!(
778 "Invalid parity '{raw}'. Expected one of: none, odd, even"
779 ))),
780 }
781}
782
783fn parse_serial_stop_bits_value(raw: u8) -> CuResult<SerialStopBits> {
784 match raw {
785 1 => Ok(SerialStopBits::One),
786 2 => Ok(SerialStopBits::Two),
787 _ => Err(CuError::from(format!(
788 "Invalid stopbits value '{raw}'. Expected 1 or 2"
789 ))),
790 }
791}
792
793#[cfg(any(target_os = "linux", test))]
794fn parse_gpio_direction_value(raw: &str) -> CuResult<GpioDirection> {
795 let normalized = raw.trim().to_ascii_lowercase();
796 match normalized.as_str() {
797 "input" => Ok(GpioDirection::Input),
798 "output" => Ok(GpioDirection::Output),
799 _ => Err(CuError::from(format!(
800 "Invalid GPIO direction '{raw}'. Expected one of: input, output"
801 ))),
802 }
803}
804
805#[cfg(any(target_os = "linux", test))]
806fn parse_gpio_bias_value(raw: &str) -> CuResult<GpioBias> {
807 let normalized = raw.trim().to_ascii_lowercase();
808 match normalized.as_str() {
809 "off" | "none" => Ok(GpioBias::Off),
810 "pull_down" | "pulldown" => Ok(GpioBias::PullDown),
811 "pull_up" | "pullup" => Ok(GpioBias::PullUp),
812 _ => Err(CuError::from(format!(
813 "Invalid GPIO bias '{raw}'. Expected one of: off, pull_up, pull_down"
814 ))),
815 }
816}
817
818#[cfg(any(target_os = "linux", test))]
819fn parse_gpio_initial_level_value(raw: &str) -> CuResult<GpioInitialLevel> {
820 let normalized = raw.trim().to_ascii_lowercase();
821 match normalized.as_str() {
822 "low" => Ok(GpioInitialLevel::Low),
823 "high" => Ok(GpioInitialLevel::High),
824 _ => Err(CuError::from(format!(
825 "Invalid GPIO initial level '{raw}'. Expected one of: low, high"
826 ))),
827 }
828}
829
830fn slot_name(id: LinuxResourcesId) -> &'static str {
831 LINUX_RESOURCE_SLOT_NAMES[id as usize]
832}
833
834fn get_string(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<String>> {
835 match config {
836 Some(cfg) => Ok(cfg.get::<String>(key)?.filter(|value| !value.is_empty())),
837 None => Ok(None),
838 }
839}
840
841fn get_u8(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u8>> {
842 match config {
843 Some(cfg) => Ok(cfg.get::<u8>(key)?),
844 None => Ok(None),
845 }
846}
847
848fn get_u32(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u32>> {
849 match config {
850 Some(cfg) => Ok(cfg.get::<u32>(key)?),
851 None => Ok(None),
852 }
853}
854
855fn get_u64(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u64>> {
856 match config {
857 Some(cfg) => Ok(cfg.get::<u64>(key)?),
858 None => Ok(None),
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn parse_serial_parity_value_accepts_expected_inputs() {
868 assert!(matches!(
869 parse_serial_parity_value("none").unwrap(),
870 SerialParity::None
871 ));
872 assert!(matches!(
873 parse_serial_parity_value("Odd").unwrap(),
874 SerialParity::Odd
875 ));
876 assert!(matches!(
877 parse_serial_parity_value("EVEN").unwrap(),
878 SerialParity::Even
879 ));
880 }
881
882 #[test]
883 fn parse_serial_parity_value_rejects_invalid_input() {
884 assert!(parse_serial_parity_value("mark").is_err());
885 }
886
887 #[test]
888 fn parse_serial_stop_bits_value_accepts_expected_inputs() {
889 assert!(matches!(
890 parse_serial_stop_bits_value(1).unwrap(),
891 SerialStopBits::One
892 ));
893 assert!(matches!(
894 parse_serial_stop_bits_value(2).unwrap(),
895 SerialStopBits::Two
896 ));
897 }
898
899 #[test]
900 fn parse_serial_stop_bits_value_rejects_invalid_input() {
901 assert!(parse_serial_stop_bits_value(0).is_err());
902 assert!(parse_serial_stop_bits_value(3).is_err());
903 }
904
905 #[test]
906 fn parse_gpio_direction_value_accepts_expected_inputs() {
907 assert!(matches!(
908 parse_gpio_direction_value("input").unwrap(),
909 GpioDirection::Input
910 ));
911 assert!(matches!(
912 parse_gpio_direction_value("Output").unwrap(),
913 GpioDirection::Output
914 ));
915 }
916
917 #[test]
918 fn parse_gpio_direction_value_rejects_invalid_input() {
919 assert!(parse_gpio_direction_value("io").is_err());
920 }
921
922 #[test]
923 fn parse_gpio_bias_value_accepts_expected_inputs() {
924 assert!(matches!(
925 parse_gpio_bias_value("off").unwrap(),
926 GpioBias::Off
927 ));
928 assert!(matches!(
929 parse_gpio_bias_value("pull_down").unwrap(),
930 GpioBias::PullDown
931 ));
932 assert!(matches!(
933 parse_gpio_bias_value("PullUp").unwrap(),
934 GpioBias::PullUp
935 ));
936 }
937
938 #[test]
939 fn parse_gpio_bias_value_rejects_invalid_input() {
940 assert!(parse_gpio_bias_value("hold").is_err());
941 }
942
943 #[test]
944 fn parse_gpio_initial_level_value_accepts_expected_inputs() {
945 assert!(matches!(
946 parse_gpio_initial_level_value("low").unwrap(),
947 GpioInitialLevel::Low
948 ));
949 assert!(matches!(
950 parse_gpio_initial_level_value("HIGH").unwrap(),
951 GpioInitialLevel::High
952 ));
953 }
954
955 #[test]
956 fn parse_gpio_initial_level_value_rejects_invalid_input() {
957 assert!(parse_gpio_initial_level_value("toggle").is_err());
958 }
959
960 #[test]
961 fn read_gpio_slot_config_requires_pin_when_aux_keys_are_present() {
962 let mut cfg = ComponentConfig::new();
963 cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
964 assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
965 }
966
967 #[test]
968 fn read_gpio_slot_config_requires_direction_when_pin_is_set() {
969 let mut cfg = ComponentConfig::new();
970 cfg.set(GPIO0_PIN_KEY, 23_u8);
971 assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
972 }
973
974 #[test]
975 fn read_gpio_slot_config_defaults_bias_and_preserves_level_for_output() {
976 let mut cfg = ComponentConfig::new();
977 cfg.set(GPIO0_PIN_KEY, 23_u8);
978 cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
979
980 let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
981 .unwrap()
982 .expect("slot should be configured");
983 assert_eq!(
984 slot_config,
985 GpioSlotConfig {
986 pin: 23,
987 direction: GpioDirection::Output,
988 bias: GpioBias::Off,
989 initial_level: None,
990 }
991 );
992 }
993
994 #[test]
995 fn read_gpio_slot_config_parses_explicit_initial_level_for_output() {
996 let mut cfg = ComponentConfig::new();
997 cfg.set(GPIO0_PIN_KEY, 23_u8);
998 cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
999 cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1000
1001 let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
1002 .unwrap()
1003 .expect("slot should be configured");
1004 assert_eq!(
1005 slot_config,
1006 GpioSlotConfig {
1007 pin: 23,
1008 direction: GpioDirection::Output,
1009 bias: GpioBias::Off,
1010 initial_level: Some(GpioInitialLevel::High),
1011 }
1012 );
1013 }
1014
1015 #[test]
1016 fn read_gpio_slot_config_rejects_initial_level_for_input() {
1017 let mut cfg = ComponentConfig::new();
1018 cfg.set(GPIO0_PIN_KEY, 23_u8);
1019 cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
1020 cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1021 assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1022 }
1023
1024 struct MockIo {
1025 rx: [u8; 4],
1026 rx_len: usize,
1027 tx: [u8; 4],
1028 tx_len: usize,
1029 }
1030
1031 impl MockIo {
1032 fn new(rx: &[u8]) -> Self {
1033 let mut buf = [0_u8; 4];
1034 buf[..rx.len()].copy_from_slice(rx);
1035 Self {
1036 rx: buf,
1037 rx_len: rx.len(),
1038 tx: [0; 4],
1039 tx_len: 0,
1040 }
1041 }
1042 }
1043
1044 impl embedded_io::ErrorType for MockIo {
1045 type Error = core::convert::Infallible;
1046 }
1047
1048 impl embedded_io::Read for MockIo {
1049 fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
1050 let n = core::cmp::min(buf.len(), self.rx_len);
1051 buf[..n].copy_from_slice(&self.rx[..n]);
1052 Ok(n)
1053 }
1054 }
1055
1056 impl embedded_io::Write for MockIo {
1057 fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
1058 let n = core::cmp::min(buf.len(), self.tx.len());
1059 self.tx[..n].copy_from_slice(&buf[..n]);
1060 self.tx_len = n;
1061 Ok(n)
1062 }
1063
1064 fn flush(&mut self) -> Result<(), Self::Error> {
1065 Ok(())
1066 }
1067 }
1068
1069 #[test]
1070 fn exclusive_forwards_embedded_io_traits() {
1071 let mut wrapped = Exclusive::new(MockIo::new(&[1, 2, 3]));
1072
1073 let mut rx = [0_u8; 4];
1074 let read = embedded_io::Read::read(&mut wrapped, &mut rx).unwrap();
1075 assert_eq!(read, 3);
1076 assert_eq!(&rx[..3], &[1, 2, 3]);
1077
1078 let written = embedded_io::Write::write(&mut wrapped, &[9, 8]).unwrap();
1079 assert_eq!(written, 2);
1080 embedded_io::Write::flush(&mut wrapped).unwrap();
1081
1082 let inner = wrapped.into_inner();
1083 assert_eq!(&inner.tx[..inner.tx_len], &[9, 8]);
1084 }
1085}