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 = "serial0",
317 Serial1 = "serial1",
318 Serial2 = "serial2",
319 Serial3 = "serial3",
320 Serial4 = "serial4",
321 Serial5 = "serial5",
322 I2c0 = "i2c0",
323 I2c1 = "i2c1",
324 I2c2 = "i2c2",
325 Gpio0 = "gpio0",
326 Gpio1 = "gpio1",
327 Gpio2 = "gpio2",
328 Gpio3 = "gpio3",
329 Gpio4 = "gpio4",
330 Gpio5 = "gpio5"
331);
332
333const LINUX_RESOURCE_SLOT_NAMES: &[&str] = &[
334 SERIAL0_NAME,
335 SERIAL1_NAME,
336 SERIAL2_NAME,
337 SERIAL3_NAME,
338 SERIAL4_NAME,
339 SERIAL5_NAME,
340 I2C0_NAME,
341 I2C1_NAME,
342 I2C2_NAME,
343 GPIO0_NAME,
344 GPIO1_NAME,
345 GPIO2_NAME,
346 GPIO3_NAME,
347 GPIO4_NAME,
348 GPIO5_NAME,
349];
350
351struct SerialSlot {
352 id: LinuxResourcesId,
353 dev_key: &'static str,
354 baudrate_key: &'static str,
355 parity_key: &'static str,
356 stopbits_key: &'static str,
357 timeout_ms_key: &'static str,
358}
359
360#[derive(Clone, Debug)]
361pub struct SerialSlotConfig {
362 pub dev: String,
363 pub baudrate: u32,
364 pub parity: SerialParity,
365 pub stop_bits: SerialStopBits,
366 pub timeout_ms: u64,
367}
368
369const SERIAL_SLOTS: &[SerialSlot] = &[
370 SerialSlot {
371 id: LinuxResourcesId::Serial0,
372 dev_key: SERIAL0_DEV_KEY,
373 baudrate_key: SERIAL0_BAUDRATE_KEY,
374 parity_key: SERIAL0_PARITY_KEY,
375 stopbits_key: SERIAL0_STOPBITS_KEY,
376 timeout_ms_key: SERIAL0_TIMEOUT_MS_KEY,
377 },
378 SerialSlot {
379 id: LinuxResourcesId::Serial1,
380 dev_key: SERIAL1_DEV_KEY,
381 baudrate_key: SERIAL1_BAUDRATE_KEY,
382 parity_key: SERIAL1_PARITY_KEY,
383 stopbits_key: SERIAL1_STOPBITS_KEY,
384 timeout_ms_key: SERIAL1_TIMEOUT_MS_KEY,
385 },
386 SerialSlot {
387 id: LinuxResourcesId::Serial2,
388 dev_key: SERIAL2_DEV_KEY,
389 baudrate_key: SERIAL2_BAUDRATE_KEY,
390 parity_key: SERIAL2_PARITY_KEY,
391 stopbits_key: SERIAL2_STOPBITS_KEY,
392 timeout_ms_key: SERIAL2_TIMEOUT_MS_KEY,
393 },
394 SerialSlot {
395 id: LinuxResourcesId::Serial3,
396 dev_key: SERIAL3_DEV_KEY,
397 baudrate_key: SERIAL3_BAUDRATE_KEY,
398 parity_key: SERIAL3_PARITY_KEY,
399 stopbits_key: SERIAL3_STOPBITS_KEY,
400 timeout_ms_key: SERIAL3_TIMEOUT_MS_KEY,
401 },
402 SerialSlot {
403 id: LinuxResourcesId::Serial4,
404 dev_key: SERIAL4_DEV_KEY,
405 baudrate_key: SERIAL4_BAUDRATE_KEY,
406 parity_key: SERIAL4_PARITY_KEY,
407 stopbits_key: SERIAL4_STOPBITS_KEY,
408 timeout_ms_key: SERIAL4_TIMEOUT_MS_KEY,
409 },
410 SerialSlot {
411 id: LinuxResourcesId::Serial5,
412 dev_key: SERIAL5_DEV_KEY,
413 baudrate_key: SERIAL5_BAUDRATE_KEY,
414 parity_key: SERIAL5_PARITY_KEY,
415 stopbits_key: SERIAL5_STOPBITS_KEY,
416 timeout_ms_key: SERIAL5_TIMEOUT_MS_KEY,
417 },
418];
419
420#[cfg(target_os = "linux")]
421struct I2cSlot {
422 id: LinuxResourcesId,
423 dev_key: &'static str,
424}
425
426#[cfg(target_os = "linux")]
427const I2C_SLOTS: &[I2cSlot] = &[
428 I2cSlot {
429 id: LinuxResourcesId::I2c0,
430 dev_key: I2C0_DEV_KEY,
431 },
432 I2cSlot {
433 id: LinuxResourcesId::I2c1,
434 dev_key: I2C1_DEV_KEY,
435 },
436 I2cSlot {
437 id: LinuxResourcesId::I2c2,
438 dev_key: I2C2_DEV_KEY,
439 },
440];
441
442#[cfg_attr(not(any(target_os = "linux", test)), allow(dead_code))]
443struct GpioSlot {
444 id: LinuxResourcesId,
445 name: &'static str,
446 pin_key: &'static str,
447 direction_key: &'static str,
448 bias_key: &'static str,
449 initial_level_key: &'static str,
450}
451
452macro_rules! gpio_slot {
453 ($id:ident, $name:ident, $pin_key:ident, $direction_key:ident, $bias_key:ident, $initial_level_key:ident) => {
454 GpioSlot {
455 id: LinuxResourcesId::$id,
456 name: $name,
457 pin_key: $pin_key,
458 direction_key: $direction_key,
459 bias_key: $bias_key,
460 initial_level_key: $initial_level_key,
461 }
462 };
463}
464
465#[cfg(any(target_os = "linux", test))]
466#[derive(Copy, Clone, Debug, Eq, PartialEq)]
467enum GpioDirection {
468 Input,
469 Output,
470}
471
472#[cfg(any(target_os = "linux", test))]
473#[derive(Copy, Clone, Debug, Eq, PartialEq)]
474enum GpioBias {
475 Off,
476 PullDown,
477 PullUp,
478}
479
480#[cfg(any(target_os = "linux", test))]
481impl GpioBias {
482 #[cfg(target_os = "linux")]
483 const fn into_rppal(self) -> rppal::gpio::Bias {
484 match self {
485 GpioBias::Off => rppal::gpio::Bias::Off,
486 GpioBias::PullDown => rppal::gpio::Bias::PullDown,
487 GpioBias::PullUp => rppal::gpio::Bias::PullUp,
488 }
489 }
490}
491
492#[cfg(any(target_os = "linux", test))]
493#[derive(Copy, Clone, Debug, Eq, PartialEq)]
494enum GpioInitialLevel {
495 Low,
496 High,
497}
498
499#[cfg(any(target_os = "linux", test))]
500#[derive(Copy, Clone, Debug, Eq, PartialEq)]
501struct GpioSlotConfig {
502 pin: u8,
503 direction: GpioDirection,
504 bias: GpioBias,
505 initial_level: Option<GpioInitialLevel>,
506}
507
508const GPIO_SLOTS: &[GpioSlot] = &[
509 gpio_slot!(
510 Gpio0,
511 GPIO0_NAME,
512 GPIO0_PIN_KEY,
513 GPIO0_DIRECTION_KEY,
514 GPIO0_BIAS_KEY,
515 GPIO0_INITIAL_LEVEL_KEY
516 ),
517 gpio_slot!(
518 Gpio1,
519 GPIO1_NAME,
520 GPIO1_PIN_KEY,
521 GPIO1_DIRECTION_KEY,
522 GPIO1_BIAS_KEY,
523 GPIO1_INITIAL_LEVEL_KEY
524 ),
525 gpio_slot!(
526 Gpio2,
527 GPIO2_NAME,
528 GPIO2_PIN_KEY,
529 GPIO2_DIRECTION_KEY,
530 GPIO2_BIAS_KEY,
531 GPIO2_INITIAL_LEVEL_KEY
532 ),
533 gpio_slot!(
534 Gpio3,
535 GPIO3_NAME,
536 GPIO3_PIN_KEY,
537 GPIO3_DIRECTION_KEY,
538 GPIO3_BIAS_KEY,
539 GPIO3_INITIAL_LEVEL_KEY
540 ),
541 gpio_slot!(
542 Gpio4,
543 GPIO4_NAME,
544 GPIO4_PIN_KEY,
545 GPIO4_DIRECTION_KEY,
546 GPIO4_BIAS_KEY,
547 GPIO4_INITIAL_LEVEL_KEY
548 ),
549 gpio_slot!(
550 Gpio5,
551 GPIO5_NAME,
552 GPIO5_PIN_KEY,
553 GPIO5_DIRECTION_KEY,
554 GPIO5_BIAS_KEY,
555 GPIO5_INITIAL_LEVEL_KEY
556 ),
557];
558
559impl ResourceBundle for LinuxResources {
560 fn build(
561 bundle: cu29::resource::BundleContext<Self>,
562 config: Option<&ComponentConfig>,
563 manager: &mut ResourceManager,
564 ) -> CuResult<()> {
565 for slot in SERIAL_SLOTS {
566 let Some(serial_config) = read_serial_slot_config(config, slot)? else {
567 continue; };
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 use cu29::resource::{NamedResourceBundleDecl, resource_index_by_name};
866
867 #[test]
868 fn named_resource_bundle_decl_matches_public_linux_slot_names() {
869 let declared = <LinuxResources as NamedResourceBundleDecl>::NAMES;
870 assert_eq!(declared, LINUX_RESOURCE_SLOT_NAMES);
871
872 for (idx, name) in declared.iter().enumerate() {
873 assert_eq!(resource_index_by_name::<LinuxResources>(name), idx);
874 }
875 }
876
877 #[test]
878 fn parse_serial_parity_value_accepts_expected_inputs() {
879 assert!(matches!(
880 parse_serial_parity_value("none").unwrap(),
881 SerialParity::None
882 ));
883 assert!(matches!(
884 parse_serial_parity_value("Odd").unwrap(),
885 SerialParity::Odd
886 ));
887 assert!(matches!(
888 parse_serial_parity_value("EVEN").unwrap(),
889 SerialParity::Even
890 ));
891 }
892
893 #[test]
894 fn parse_serial_parity_value_rejects_invalid_input() {
895 assert!(parse_serial_parity_value("mark").is_err());
896 }
897
898 #[test]
899 fn parse_serial_stop_bits_value_accepts_expected_inputs() {
900 assert!(matches!(
901 parse_serial_stop_bits_value(1).unwrap(),
902 SerialStopBits::One
903 ));
904 assert!(matches!(
905 parse_serial_stop_bits_value(2).unwrap(),
906 SerialStopBits::Two
907 ));
908 }
909
910 #[test]
911 fn parse_serial_stop_bits_value_rejects_invalid_input() {
912 assert!(parse_serial_stop_bits_value(0).is_err());
913 assert!(parse_serial_stop_bits_value(3).is_err());
914 }
915
916 #[test]
917 fn parse_gpio_direction_value_accepts_expected_inputs() {
918 assert!(matches!(
919 parse_gpio_direction_value("input").unwrap(),
920 GpioDirection::Input
921 ));
922 assert!(matches!(
923 parse_gpio_direction_value("Output").unwrap(),
924 GpioDirection::Output
925 ));
926 }
927
928 #[test]
929 fn parse_gpio_direction_value_rejects_invalid_input() {
930 assert!(parse_gpio_direction_value("io").is_err());
931 }
932
933 #[test]
934 fn parse_gpio_bias_value_accepts_expected_inputs() {
935 assert!(matches!(
936 parse_gpio_bias_value("off").unwrap(),
937 GpioBias::Off
938 ));
939 assert!(matches!(
940 parse_gpio_bias_value("pull_down").unwrap(),
941 GpioBias::PullDown
942 ));
943 assert!(matches!(
944 parse_gpio_bias_value("PullUp").unwrap(),
945 GpioBias::PullUp
946 ));
947 }
948
949 #[test]
950 fn parse_gpio_bias_value_rejects_invalid_input() {
951 assert!(parse_gpio_bias_value("hold").is_err());
952 }
953
954 #[test]
955 fn parse_gpio_initial_level_value_accepts_expected_inputs() {
956 assert!(matches!(
957 parse_gpio_initial_level_value("low").unwrap(),
958 GpioInitialLevel::Low
959 ));
960 assert!(matches!(
961 parse_gpio_initial_level_value("HIGH").unwrap(),
962 GpioInitialLevel::High
963 ));
964 }
965
966 #[test]
967 fn parse_gpio_initial_level_value_rejects_invalid_input() {
968 assert!(parse_gpio_initial_level_value("toggle").is_err());
969 }
970
971 #[test]
972 fn read_gpio_slot_config_requires_pin_when_aux_keys_are_present() {
973 let mut cfg = ComponentConfig::new();
974 cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
975 assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
976 }
977
978 #[test]
979 fn read_gpio_slot_config_requires_direction_when_pin_is_set() {
980 let mut cfg = ComponentConfig::new();
981 cfg.set(GPIO0_PIN_KEY, 23_u8);
982 assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
983 }
984
985 #[test]
986 fn read_gpio_slot_config_defaults_bias_and_preserves_level_for_output() {
987 let mut cfg = ComponentConfig::new();
988 cfg.set(GPIO0_PIN_KEY, 23_u8);
989 cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
990
991 let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
992 .unwrap()
993 .expect("slot should be configured");
994 assert_eq!(
995 slot_config,
996 GpioSlotConfig {
997 pin: 23,
998 direction: GpioDirection::Output,
999 bias: GpioBias::Off,
1000 initial_level: None,
1001 }
1002 );
1003 }
1004
1005 #[test]
1006 fn read_gpio_slot_config_parses_explicit_initial_level_for_output() {
1007 let mut cfg = ComponentConfig::new();
1008 cfg.set(GPIO0_PIN_KEY, 23_u8);
1009 cfg.set(GPIO0_DIRECTION_KEY, "output".to_string());
1010 cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1011
1012 let slot_config = read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0])
1013 .unwrap()
1014 .expect("slot should be configured");
1015 assert_eq!(
1016 slot_config,
1017 GpioSlotConfig {
1018 pin: 23,
1019 direction: GpioDirection::Output,
1020 bias: GpioBias::Off,
1021 initial_level: Some(GpioInitialLevel::High),
1022 }
1023 );
1024 }
1025
1026 #[test]
1027 fn read_gpio_slot_config_rejects_initial_level_for_input() {
1028 let mut cfg = ComponentConfig::new();
1029 cfg.set(GPIO0_PIN_KEY, 23_u8);
1030 cfg.set(GPIO0_DIRECTION_KEY, "input".to_string());
1031 cfg.set(GPIO0_INITIAL_LEVEL_KEY, "high".to_string());
1032 assert!(read_gpio_slot_config(Some(&cfg), &GPIO_SLOTS[0]).is_err());
1033 }
1034
1035 struct MockIo {
1036 rx: [u8; 4],
1037 rx_len: usize,
1038 tx: [u8; 4],
1039 tx_len: usize,
1040 }
1041
1042 impl MockIo {
1043 fn new(rx: &[u8]) -> Self {
1044 let mut buf = [0_u8; 4];
1045 buf[..rx.len()].copy_from_slice(rx);
1046 Self {
1047 rx: buf,
1048 rx_len: rx.len(),
1049 tx: [0; 4],
1050 tx_len: 0,
1051 }
1052 }
1053 }
1054
1055 impl embedded_io::ErrorType for MockIo {
1056 type Error = core::convert::Infallible;
1057 }
1058
1059 impl embedded_io::Read for MockIo {
1060 fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
1061 let n = core::cmp::min(buf.len(), self.rx_len);
1062 buf[..n].copy_from_slice(&self.rx[..n]);
1063 Ok(n)
1064 }
1065 }
1066
1067 impl embedded_io::Write for MockIo {
1068 fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
1069 let n = core::cmp::min(buf.len(), self.tx.len());
1070 self.tx[..n].copy_from_slice(&buf[..n]);
1071 self.tx_len = n;
1072 Ok(n)
1073 }
1074
1075 fn flush(&mut self) -> Result<(), Self::Error> {
1076 Ok(())
1077 }
1078 }
1079
1080 #[test]
1081 fn exclusive_forwards_embedded_io_traits() {
1082 let mut wrapped = Exclusive::new(MockIo::new(&[1, 2, 3]));
1083
1084 let mut rx = [0_u8; 4];
1085 let read = embedded_io::Read::read(&mut wrapped, &mut rx).unwrap();
1086 assert_eq!(read, 3);
1087 assert_eq!(&rx[..3], &[1, 2, 3]);
1088
1089 let written = embedded_io::Write::write(&mut wrapped, &[9, 8]).unwrap();
1090 assert_eq!(written, 2);
1091 embedded_io::Write::flush(&mut wrapped).unwrap();
1092
1093 let inner = wrapped.into_inner();
1094 assert_eq!(&inner.tx[..inner.tx_len], &[9, 8]);
1095 }
1096}