1use std::os::raw::c_char;
17use std::panic::{catch_unwind, AssertUnwindSafe};
18
19use pamoja_gpio::i2c::{Address, Direction};
20use pamoja_gpio::linux::{self, Line, OpenError};
21use pamoja_gpio::pin::{Edge, Level, Polarity};
22use pamoja_gpio::spi::Mode;
23use pamoja_gpio::GpioError;
24
25use crate::{read_str, set_last_error, PamojaStatus};
26
27pub const PAMOJA_I2C_FRAME_MAX: usize = 2;
29
30pub const PAMOJA_I2C_RESERVED_FROM: u8 = 0x78;
32
33pub const PAMOJA_I2C_RESERVED_BELOW: u8 = 0x08;
35
36const _: () = assert!(PAMOJA_I2C_RESERVED_FROM == pamoja_gpio::i2c::RESERVED_FROM);
39const _: () = assert!(PAMOJA_I2C_RESERVED_BELOW == pamoja_gpio::i2c::RESERVED_BELOW);
40
41#[repr(C)]
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct PamojaI2cAddress {
49 pub value: u16,
51 pub ten_bit: u8,
53}
54
55#[repr(C)]
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum PamojaI2cDirection {
59 Write = 0,
61 Read = 1,
63}
64
65#[repr(C)]
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum PamojaPinLevel {
69 Low = 0,
71 High = 1,
73}
74
75#[repr(C)]
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum PamojaPinEdge {
79 Rising = 0,
81 Falling = 1,
83 Both = 2,
85}
86
87#[repr(C)]
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum PamojaPinPolarity {
91 ActiveHigh = 0,
93 ActiveLow = 1,
95}
96
97#[no_mangle]
112pub unsafe extern "C" fn pamoja_i2c_address_seven_bit(
113 address: u8,
114 out_address: *mut PamojaI2cAddress,
115) -> PamojaStatus {
116 emit(Address::seven_bit(address), out_address)
117}
118
119#[no_mangle]
130pub unsafe extern "C" fn pamoja_i2c_address_ten_bit(
131 address: u16,
132 out_address: *mut PamojaI2cAddress,
133) -> PamojaStatus {
134 emit(Address::ten_bit(address), out_address)
135}
136
137#[no_mangle]
143pub extern "C" fn pamoja_i2c_address_frame_len(address: PamojaI2cAddress) -> usize {
144 borrow(address).map_or(0, Address::frame_len)
145}
146
147#[no_mangle]
161pub unsafe extern "C" fn pamoja_i2c_address_frame(
162 address: PamojaI2cAddress,
163 direction: PamojaI2cDirection,
164 out_frame: *mut u8,
165 out_frame_cap: usize,
166 out_len: *mut usize,
167) -> PamojaStatus {
168 if out_frame.is_null() || out_len.is_null() {
169 set_last_error("out_frame and out_len must not be null".to_owned());
170 return PamojaStatus::InvalidArgument;
171 }
172 let Some(address) = borrow(address) else {
173 set_last_error("address is not a valid I2C address".to_owned());
174 return PamojaStatus::InvalidArgument;
175 };
176 let out = std::slice::from_raw_parts_mut(out_frame, out_frame_cap);
177 match address.write_frame(direction.into(), out) {
178 Ok(written) => {
179 *out_len = written;
180 PamojaStatus::Ok
181 }
182 Err(error) => failed(error),
183 }
184}
185
186#[no_mangle]
194pub extern "C" fn pamoja_i2c_address_is_reserved(address: PamojaI2cAddress) -> bool {
195 borrow(address).is_some_and(Address::is_reserved)
196}
197
198#[no_mangle]
204pub extern "C" fn pamoja_i2c_address_is_general_call(address: PamojaI2cAddress) -> bool {
205 borrow(address).is_some_and(Address::is_general_call)
206}
207
208#[no_mangle]
220pub unsafe extern "C" fn pamoja_spi_mode_cpol_cpha(
221 mode: u8,
222 out_cpol: *mut bool,
223 out_cpha: *mut bool,
224) -> PamojaStatus {
225 if out_cpol.is_null() || out_cpha.is_null() {
226 set_last_error("out_cpol and out_cpha must not be null".to_owned());
227 return PamojaStatus::InvalidArgument;
228 }
229 let Some(mode) = Mode::from_number(mode) else {
230 set_last_error("SPI mode must be 0, 1, 2, or 3".to_owned());
231 return PamojaStatus::InvalidArgument;
232 };
233 let (cpol, cpha) = mode.cpol_cpha();
234 *out_cpol = cpol;
235 *out_cpha = cpha;
236 PamojaStatus::Ok
237}
238
239#[no_mangle]
245pub extern "C" fn pamoja_spi_mode_from_cpol_cpha(cpol: bool, cpha: bool) -> u8 {
246 Mode::from_cpol_cpha(cpol, cpha).number()
247}
248
249#[no_mangle]
255pub extern "C" fn pamoja_pin_level_from_bool(high: bool) -> PamojaPinLevel {
256 Level::from_bool(high).into()
257}
258
259#[no_mangle]
265pub extern "C" fn pamoja_pin_level_inverted(level: PamojaPinLevel) -> PamojaPinLevel {
266 Level::from(level).inverted().into()
267}
268
269#[no_mangle]
276pub extern "C" fn pamoja_pin_edge_triggered_by(
277 edge: PamojaPinEdge,
278 from: PamojaPinLevel,
279 to: PamojaPinLevel,
280) -> bool {
281 Edge::from(edge).triggered_by(from.into(), to.into())
282}
283
284#[no_mangle]
290pub extern "C" fn pamoja_pin_polarity_level(
291 polarity: PamojaPinPolarity,
292 asserted: bool,
293) -> PamojaPinLevel {
294 Polarity::from(polarity).level(asserted).into()
295}
296
297#[no_mangle]
303pub extern "C" fn pamoja_pin_polarity_is_asserted(
304 polarity: PamojaPinPolarity,
305 level: PamojaPinLevel,
306) -> bool {
307 Polarity::from(polarity).is_asserted(level.into())
308}
309
310pub struct PamojaGpioLine {
313 line: Line,
314}
315
316#[no_mangle]
338pub unsafe extern "C" fn pamoja_gpio_line_open_output(
339 chip: *const c_char,
340 line: u32,
341 initial: PamojaPinLevel,
342 out_line: *mut *mut PamojaGpioLine,
343) -> PamojaStatus {
344 open_line(chip, out_line, |chip| {
345 linux::output(chip, line, initial.into())
346 })
347}
348
349#[no_mangle]
366pub unsafe extern "C" fn pamoja_gpio_line_open_input(
367 chip: *const c_char,
368 line: u32,
369 out_line: *mut *mut PamojaGpioLine,
370) -> PamojaStatus {
371 open_line(chip, out_line, |chip| linux::input(chip, line))
372}
373
374#[no_mangle]
391pub unsafe extern "C" fn pamoja_gpio_line_drive(
392 line: *mut PamojaGpioLine,
393 level: PamojaPinLevel,
394) -> PamojaStatus {
395 match on_line(line, |line| line.drive(level.into())) {
396 Ok(()) => PamojaStatus::Ok,
397 Err(status) => status,
398 }
399}
400
401#[no_mangle]
418pub unsafe extern "C" fn pamoja_gpio_line_read(
419 line: *mut PamojaGpioLine,
420 out_level: *mut PamojaPinLevel,
421) -> PamojaStatus {
422 if out_level.is_null() {
423 set_last_error("out_level must not be null".to_owned());
424 return PamojaStatus::InvalidArgument;
425 }
426 match on_line(line, Line::read) {
427 Ok(level) => {
428 *out_level = level.into();
429 PamojaStatus::Ok
430 }
431 Err(status) => status,
432 }
433}
434
435#[no_mangle]
441pub unsafe extern "C" fn pamoja_gpio_line_free(line: *mut PamojaGpioLine) {
442 if !line.is_null() {
443 drop(Box::from_raw(line));
444 }
445}
446
447unsafe fn open_line(
454 chip: *const c_char,
455 out_line: *mut *mut PamojaGpioLine,
456 opening: impl FnOnce(&str) -> Result<Line, OpenError>,
457) -> PamojaStatus {
458 if out_line.is_null() {
459 set_last_error("out_line must not be null".to_owned());
460 return PamojaStatus::InvalidArgument;
461 }
462 *out_line = std::ptr::null_mut();
463 let Some(chip) = read_str(chip, "chip") else {
464 return PamojaStatus::InvalidArgument;
465 };
466 match catch_unwind(AssertUnwindSafe(|| opening(chip))) {
467 Ok(Ok(line)) => {
468 *out_line = Box::into_raw(Box::new(PamojaGpioLine { line }));
469 PamojaStatus::Ok
470 }
471 Ok(Err(error)) => {
472 set_last_error(error.to_string());
473 match error {
474 OpenError::Unsupported => PamojaStatus::Unsupported,
475 OpenError::Gpio { .. } => PamojaStatus::Io,
476 }
477 }
478 Err(_) => panicked(),
479 }
480}
481
482fn panicked() -> PamojaStatus {
484 set_last_error("panic at the FFI boundary".to_owned());
485 PamojaStatus::Panic
486}
487
488unsafe fn on_line<T>(
494 line: *mut PamojaGpioLine,
495 call: impl FnOnce(&mut Line) -> Result<T, linux::LineError>,
496) -> Result<T, PamojaStatus> {
497 if line.is_null() {
498 set_last_error("line must not be null".to_owned());
499 return Err(PamojaStatus::InvalidArgument);
500 }
501 let line = &mut (*line).line;
502 match catch_unwind(AssertUnwindSafe(|| call(line))) {
503 Ok(Ok(value)) => Ok(value),
504 Ok(Err(error)) => {
505 set_last_error(error.to_string());
506 Err(PamojaStatus::Io)
507 }
508 Err(_) => Err(panicked()),
509 }
510}
511
512impl From<PamojaI2cDirection> for Direction {
513 fn from(value: PamojaI2cDirection) -> Self {
514 match value {
515 PamojaI2cDirection::Write => Direction::Write,
516 PamojaI2cDirection::Read => Direction::Read,
517 }
518 }
519}
520
521impl From<PamojaPinLevel> for Level {
522 fn from(value: PamojaPinLevel) -> Self {
523 match value {
524 PamojaPinLevel::Low => Level::Low,
525 PamojaPinLevel::High => Level::High,
526 }
527 }
528}
529
530impl From<Level> for PamojaPinLevel {
531 fn from(value: Level) -> Self {
532 match value {
533 Level::Low => PamojaPinLevel::Low,
534 Level::High => PamojaPinLevel::High,
535 }
536 }
537}
538
539impl From<PamojaPinEdge> for Edge {
540 fn from(value: PamojaPinEdge) -> Self {
541 match value {
542 PamojaPinEdge::Rising => Edge::Rising,
543 PamojaPinEdge::Falling => Edge::Falling,
544 PamojaPinEdge::Both => Edge::Both,
545 }
546 }
547}
548
549impl From<PamojaPinPolarity> for Polarity {
550 fn from(value: PamojaPinPolarity) -> Self {
551 match value {
552 PamojaPinPolarity::ActiveHigh => Polarity::ActiveHigh,
553 PamojaPinPolarity::ActiveLow => Polarity::ActiveLow,
554 }
555 }
556}
557
558fn borrow(address: PamojaI2cAddress) -> Option<Address> {
564 if address.ten_bit == 0 {
565 u8::try_from(address.value)
566 .ok()
567 .and_then(|value| Address::seven_bit(value).ok())
568 } else {
569 Address::ten_bit(address.value).ok()
570 }
571}
572
573unsafe fn emit(
579 address: Result<Address, GpioError>,
580 out_address: *mut PamojaI2cAddress,
581) -> PamojaStatus {
582 if out_address.is_null() {
583 set_last_error("out_address must not be null".to_owned());
584 return PamojaStatus::InvalidArgument;
585 }
586 match address {
587 Ok(address) => {
588 *out_address = PamojaI2cAddress {
589 value: address.value(),
590 ten_bit: u8::from(address.is_ten_bit()),
591 };
592 PamojaStatus::Ok
593 }
594 Err(error) => failed(error),
595 }
596}
597
598fn failed(error: GpioError) -> PamojaStatus {
600 set_last_error(error.to_string());
601 PamojaStatus::InvalidArgument
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 fn seven_bit(value: u8) -> PamojaI2cAddress {
610 let mut address = PamojaI2cAddress {
611 value: 0,
612 ten_bit: 0,
613 };
614 let status = unsafe { pamoja_i2c_address_seven_bit(value, &mut address) };
616 assert_eq!(status, PamojaStatus::Ok);
617 address
618 }
619
620 #[test]
621 fn a_seven_bit_address_frames_as_one_shifted_byte() {
622 let bme = seven_bit(0x76);
623 let mut frame = [0u8; PAMOJA_I2C_FRAME_MAX];
624 let mut len = 0usize;
625
626 unsafe {
628 assert_eq!(
629 pamoja_i2c_address_frame(
630 bme,
631 PamojaI2cDirection::Write,
632 frame.as_mut_ptr(),
633 frame.len(),
634 &mut len
635 ),
636 PamojaStatus::Ok
637 );
638 assert_eq!((len, frame[0]), (1, 0xEC), "(0x76 << 1) | 0");
639 assert_eq!(
640 pamoja_i2c_address_frame(
641 bme,
642 PamojaI2cDirection::Read,
643 frame.as_mut_ptr(),
644 frame.len(),
645 &mut len
646 ),
647 PamojaStatus::Ok
648 );
649 assert_eq!((len, frame[0]), (1, 0xED), "(0x76 << 1) | 1");
650 }
651 }
652
653 #[test]
654 fn a_ten_bit_address_frames_as_the_reserved_prefix_and_the_low_byte() {
655 let mut address = PamojaI2cAddress {
656 value: 0,
657 ten_bit: 0,
658 };
659 let mut frame = [0u8; PAMOJA_I2C_FRAME_MAX];
660 let mut len = 0usize;
661
662 unsafe {
664 assert_eq!(
665 pamoja_i2c_address_ten_bit(0x2A5, &mut address),
666 PamojaStatus::Ok
667 );
668 assert_eq!(pamoja_i2c_address_frame_len(address), 2);
669 assert_eq!(
670 pamoja_i2c_address_frame(
671 address,
672 PamojaI2cDirection::Write,
673 frame.as_mut_ptr(),
674 frame.len(),
675 &mut len
676 ),
677 PamojaStatus::Ok
678 );
679 assert_eq!(len, 2);
680 assert_eq!(frame, [0xF4, 0xA5], "11110 then the top two bits, then r/w");
681 }
682 }
683
684 #[test]
685 fn an_out_of_range_address_is_refused() {
686 let mut address = PamojaI2cAddress {
687 value: 0,
688 ten_bit: 0,
689 };
690 let status = unsafe { pamoja_i2c_address_ten_bit(0x400, &mut address) };
692 assert_eq!(status, PamojaStatus::InvalidArgument);
693 }
694
695 #[test]
696 fn a_buffer_too_small_for_the_frame_is_refused() {
697 let mut address = PamojaI2cAddress {
698 value: 0,
699 ten_bit: 0,
700 };
701 let mut frame = [0u8; 1];
702 let mut len = 0usize;
703
704 unsafe {
706 assert_eq!(
707 pamoja_i2c_address_ten_bit(0x2A5, &mut address),
708 PamojaStatus::Ok
709 );
710 assert_eq!(
711 pamoja_i2c_address_frame(
712 address,
713 PamojaI2cDirection::Write,
714 frame.as_mut_ptr(),
715 frame.len(),
716 &mut len
717 ),
718 PamojaStatus::InvalidArgument
719 );
720 }
721 }
722
723 #[test]
724 fn the_reserved_ranges_are_recognized() {
725 assert!(pamoja_i2c_address_is_reserved(seven_bit(0x00)));
726 assert!(pamoja_i2c_address_is_general_call(seven_bit(0x00)));
727 assert!(pamoja_i2c_address_is_reserved(seven_bit(0x07)));
728 assert!(!pamoja_i2c_address_is_reserved(seven_bit(0x08)));
729 assert!(!pamoja_i2c_address_is_reserved(seven_bit(0x77)));
730 assert!(pamoja_i2c_address_is_reserved(seven_bit(0x78)));
731 }
732
733 #[test]
734 fn the_spi_modes_match_the_pairs_datasheets_quote() {
735 let mut cpol = false;
736 let mut cpha = false;
737 for (number, expected) in [
738 (0u8, (false, false)),
739 (1, (false, true)),
740 (2, (true, false)),
741 (3, (true, true)),
742 ] {
743 unsafe {
745 assert_eq!(
746 pamoja_spi_mode_cpol_cpha(number, &mut cpol, &mut cpha),
747 PamojaStatus::Ok
748 );
749 }
750 assert_eq!((cpol, cpha), expected, "mode {number}");
751 assert_eq!(pamoja_spi_mode_from_cpol_cpha(cpol, cpha), number);
752 }
753 }
754
755 #[test]
756 fn a_mode_number_above_three_is_refused() {
757 let mut cpol = false;
758 let mut cpha = false;
759 let status = unsafe { pamoja_spi_mode_cpol_cpha(4, &mut cpol, &mut cpha) };
761 assert_eq!(status, PamojaStatus::InvalidArgument);
762 }
763
764 #[test]
765 fn an_active_low_relay_is_energized_by_a_low_level() {
766 assert_eq!(
767 pamoja_pin_polarity_level(PamojaPinPolarity::ActiveLow, true),
768 PamojaPinLevel::Low
769 );
770 assert_eq!(
771 pamoja_pin_polarity_level(PamojaPinPolarity::ActiveHigh, true),
772 PamojaPinLevel::High
773 );
774 assert!(pamoja_pin_polarity_is_asserted(
775 PamojaPinPolarity::ActiveLow,
776 PamojaPinLevel::Low
777 ));
778 }
779
780 #[test]
781 fn an_edge_fires_only_on_its_own_transition() {
782 assert!(pamoja_pin_edge_triggered_by(
783 PamojaPinEdge::Rising,
784 PamojaPinLevel::Low,
785 PamojaPinLevel::High
786 ));
787 assert!(!pamoja_pin_edge_triggered_by(
788 PamojaPinEdge::Rising,
789 PamojaPinLevel::High,
790 PamojaPinLevel::Low
791 ));
792 assert!(pamoja_pin_edge_triggered_by(
793 PamojaPinEdge::Both,
794 PamojaPinLevel::High,
795 PamojaPinLevel::Low
796 ));
797 assert!(!pamoja_pin_edge_triggered_by(
798 PamojaPinEdge::Both,
799 PamojaPinLevel::High,
800 PamojaPinLevel::High
801 ));
802 }
803
804 #[test]
805 fn a_level_inverts() {
806 assert_eq!(
807 pamoja_pin_level_inverted(pamoja_pin_level_from_bool(true)),
808 PamojaPinLevel::Low
809 );
810 }
811
812 fn last_error() -> String {
813 unsafe { std::ffi::CStr::from_ptr(crate::pamoja_last_error_message()) }
815 .to_string_lossy()
816 .into_owned()
817 }
818
819 #[test]
820 fn a_line_call_refuses_a_null_argument_by_name() {
821 let chip = std::ffi::CString::new("/dev/gpiochip0").expect("no interior null");
822 let mut level = PamojaPinLevel::Low;
823 unsafe {
825 assert_eq!(
826 pamoja_gpio_line_open_output(
827 chip.as_ptr(),
828 17,
829 PamojaPinLevel::High,
830 std::ptr::null_mut()
831 ),
832 PamojaStatus::InvalidArgument
833 );
834 assert!(last_error().contains("out_line"), "{}", last_error());
835 let mut line = std::ptr::null_mut();
836 assert_eq!(
837 pamoja_gpio_line_open_input(std::ptr::null(), 27, &mut line),
838 PamojaStatus::InvalidArgument
839 );
840 assert!(
841 line.is_null() && last_error().contains("chip"),
842 "{}",
843 last_error()
844 );
845 assert_eq!(
846 pamoja_gpio_line_drive(std::ptr::null_mut(), PamojaPinLevel::Low),
847 PamojaStatus::InvalidArgument
848 );
849 assert_eq!(
850 pamoja_gpio_line_read(std::ptr::null_mut(), &mut level),
851 PamojaStatus::InvalidArgument
852 );
853 pamoja_gpio_line_free(std::ptr::null_mut());
854 }
855 }
856
857 #[cfg(not(target_os = "linux"))]
858 #[test]
859 fn opening_a_line_off_linux_is_unsupported() {
860 let chip = std::ffi::CString::new("/dev/gpiochip0").expect("no interior null");
861 let mut line = std::ptr::null_mut();
862 let opened = unsafe {
864 pamoja_gpio_line_open_output(chip.as_ptr(), 17, PamojaPinLevel::High, &mut line)
865 };
866 assert_eq!(opened, PamojaStatus::Unsupported);
867 assert!(line.is_null());
868 assert!(last_error().contains("only Linux"), "{}", last_error());
869 }
870
871 #[cfg(target_os = "linux")]
872 #[test]
873 fn opening_a_missing_chip_is_an_io_error_that_names_it() {
874 let chip = std::ffi::CString::new("/dev/gpiochip-pamoja-absent").expect("no interior null");
875 let mut line = std::ptr::null_mut();
876 let opened = unsafe { pamoja_gpio_line_open_input(chip.as_ptr(), 27, &mut line) };
878 assert_eq!(opened, PamojaStatus::Io);
879 assert!(line.is_null());
880 assert!(
881 last_error().starts_with("/dev/gpiochip-pamoja-absent line 27: "),
882 "{}",
883 last_error()
884 );
885 }
886}