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
95pub 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
116unsafe 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; };
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; };
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); };
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}