1use std::os::raw::c_char;
16use std::panic::{catch_unwind, AssertUnwindSafe};
17use std::time::Duration;
18
19use pamoja_hal::port::{
20 OpenError, Parity, PortError, PortKind, PortStep, SerialPort, Settings, StopBits,
21};
22
23use crate::{read_bytes, read_str, set_last_error, PamojaStatus};
24
25pub const PAMOJA_PARITY_NONE: u8 = 0;
27
28pub const PAMOJA_PARITY_EVEN: u8 = 1;
30
31pub const PAMOJA_PARITY_ODD: u8 = 2;
33
34pub const PAMOJA_SERIAL_PORT_DEVICE: u8 = 0;
36
37pub const PAMOJA_SERIAL_PORT_LOOPED: u8 = 1;
39
40pub const PAMOJA_SERIAL_PORT_PAIRED: u8 = 2;
42
43pub const PAMOJA_SERIAL_PORT_SIMULATED: u8 = 3;
45
46pub const PAMOJA_SERIAL_PORT_SCRIPTED: u8 = 4;
48
49#[repr(C)]
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct PamojaSerialSettings {
53 pub baud: u32,
55 pub parity: u8,
57 pub stop_bits: u8,
59}
60
61pub struct PamojaSerialPort {
64 pub(crate) port: SerialPort,
65}
66
67pub struct PamojaSerialScript {
70 steps: Vec<PortStep>,
71}
72
73impl From<Settings> for PamojaSerialSettings {
74 fn from(settings: Settings) -> Self {
75 PamojaSerialSettings {
76 baud: settings.baud,
77 parity: match settings.parity {
78 Parity::None => PAMOJA_PARITY_NONE,
79 Parity::Even => PAMOJA_PARITY_EVEN,
80 Parity::Odd => PAMOJA_PARITY_ODD,
81 },
82 stop_bits: match settings.stop_bits {
83 StopBits::One => 1,
84 StopBits::Two => 2,
85 },
86 }
87 }
88}
89
90impl PamojaSerialSettings {
91 pub(crate) fn settings(self) -> Result<Settings, PamojaStatus> {
98 let parity = match self.parity {
99 PAMOJA_PARITY_NONE => Parity::None,
100 PAMOJA_PARITY_EVEN => Parity::Even,
101 PAMOJA_PARITY_ODD => Parity::Odd,
102 other => {
103 set_last_error(format!("parity {other} is not none, even, or odd"));
104 return Err(PamojaStatus::InvalidArgument);
105 }
106 };
107 let stop_bits = match self.stop_bits {
108 1 => StopBits::One,
109 2 => StopBits::Two,
110 other => {
111 set_last_error(format!("{other} stop bits is not 1 or 2"));
112 return Err(PamojaStatus::InvalidArgument);
113 }
114 };
115 if self.baud == 0 {
116 set_last_error("the speed must be above zero".to_owned());
117 return Err(PamojaStatus::InvalidArgument);
118 }
119 Ok(Settings::new(self.baud)
120 .with_parity(parity)
121 .with_stop_bits(stop_bits))
122 }
123}
124
125#[no_mangle]
135pub extern "C" fn pamoja_serial_settings(baud: u32) -> PamojaSerialSettings {
136 Settings::new(baud).into()
137}
138
139#[no_mangle]
150pub extern "C" fn pamoja_serial_settings_bits_per_character(settings: PamojaSerialSettings) -> u32 {
151 settings
152 .settings()
153 .map_or(0, |settings| settings.bits_per_character())
154}
155
156#[no_mangle]
166pub extern "C" fn pamoja_serial_settings_character_nanos(settings: PamojaSerialSettings) -> u64 {
167 settings
168 .settings()
169 .map_or(0, |settings| settings.character_nanos())
170}
171
172#[no_mangle]
193pub unsafe extern "C" fn pamoja_serial_port_open(
194 path: *const c_char,
195 settings: PamojaSerialSettings,
196 out_port: *mut *mut PamojaSerialPort,
197) -> PamojaStatus {
198 if out_port.is_null() {
199 set_last_error("out_port must not be null".to_owned());
200 return PamojaStatus::InvalidArgument;
201 }
202 *out_port = std::ptr::null_mut();
203 let Some(path) = read_str(path, "path") else {
204 return PamojaStatus::InvalidArgument;
205 };
206 let settings = match settings.settings() {
207 Ok(settings) => settings,
208 Err(status) => return status,
209 };
210 match catch_unwind(AssertUnwindSafe(|| SerialPort::open(path, settings))) {
211 Ok(Ok(port)) => {
212 *out_port = into_raw(port);
213 PamojaStatus::Ok
214 }
215 Ok(Err(error)) => {
216 set_last_error(error.to_string());
217 match error {
218 OpenError::Unsupported => PamojaStatus::Unsupported,
219 OpenError::Device { .. } => PamojaStatus::Io,
220 }
221 }
222 Err(_) => panicked(),
223 }
224}
225
226#[no_mangle]
238pub extern "C" fn pamoja_serial_port_looped(
239 settings: PamojaSerialSettings,
240) -> *mut PamojaSerialPort {
241 match settings.settings() {
242 Ok(settings) => into_raw(SerialPort::looped(settings)),
243 Err(_) => std::ptr::null_mut(),
244 }
245}
246
247#[no_mangle]
265pub unsafe extern "C" fn pamoja_serial_port_pair(
266 settings: PamojaSerialSettings,
267 out_one: *mut *mut PamojaSerialPort,
268 out_other: *mut *mut PamojaSerialPort,
269) -> PamojaStatus {
270 if out_one.is_null() || out_other.is_null() {
271 set_last_error("out_one and out_other must not be null".to_owned());
272 return PamojaStatus::InvalidArgument;
273 }
274 *out_one = std::ptr::null_mut();
275 *out_other = std::ptr::null_mut();
276 let settings = match settings.settings() {
277 Ok(settings) => settings,
278 Err(status) => return status,
279 };
280 let (one, other) = SerialPort::pair(settings);
281 *out_one = into_raw(one);
282 *out_other = into_raw(other);
283 PamojaStatus::Ok
284}
285
286#[no_mangle]
292pub extern "C" fn pamoja_serial_script_new() -> *mut PamojaSerialScript {
293 Box::into_raw(Box::new(PamojaSerialScript { steps: Vec::new() }))
294}
295
296#[no_mangle]
312pub unsafe extern "C" fn pamoja_serial_script_write(
313 script: *mut PamojaSerialScript,
314 bytes: *const u8,
315 len: usize,
316) -> PamojaStatus {
317 add_step(script, bytes, len, PortStep::Write)
318}
319
320#[no_mangle]
336pub unsafe extern "C" fn pamoja_serial_script_read(
337 script: *mut PamojaSerialScript,
338 bytes: *const u8,
339 len: usize,
340) -> PamojaStatus {
341 add_step(script, bytes, len, PortStep::Read)
342}
343
344#[no_mangle]
358pub unsafe extern "C" fn pamoja_serial_script_len(script: *const PamojaSerialScript) -> usize {
359 script.as_ref().map_or(0, |script| script.steps.len())
360}
361
362#[no_mangle]
368pub unsafe extern "C" fn pamoja_serial_script_free(script: *mut PamojaSerialScript) {
369 if !script.is_null() {
370 drop(Box::from_raw(script));
371 }
372}
373
374#[no_mangle]
391pub unsafe extern "C" fn pamoja_serial_port_scripted(
392 settings: PamojaSerialSettings,
393 script: *const PamojaSerialScript,
394) -> *mut PamojaSerialPort {
395 let Some(script) = script.as_ref() else {
396 set_last_error("script must not be null".to_owned());
397 return std::ptr::null_mut();
398 };
399 match settings.settings() {
400 Ok(settings) => into_raw(SerialPort::scripted(settings, script.steps.clone())),
401 Err(_) => std::ptr::null_mut(),
402 }
403}
404
405#[no_mangle]
420pub unsafe extern "C" fn pamoja_serial_port_clone(
421 port: *const PamojaSerialPort,
422) -> *mut PamojaSerialPort {
423 match port.as_ref() {
424 Some(port) => into_raw(port.port.clone()),
425 None => std::ptr::null_mut(),
426 }
427}
428
429#[no_mangle]
443pub unsafe extern "C" fn pamoja_serial_port_kind(port: *const PamojaSerialPort) -> u8 {
444 match port.as_ref().map(|port| port.port.kind()) {
445 Some(PortKind::Device) => PAMOJA_SERIAL_PORT_DEVICE,
446 Some(PortKind::Looped) | None => PAMOJA_SERIAL_PORT_LOOPED,
447 Some(PortKind::Paired) => PAMOJA_SERIAL_PORT_PAIRED,
448 Some(PortKind::Simulated) => PAMOJA_SERIAL_PORT_SIMULATED,
449 Some(PortKind::Scripted) => PAMOJA_SERIAL_PORT_SCRIPTED,
450 }
451}
452
453#[no_mangle]
467pub unsafe extern "C" fn pamoja_serial_port_settings(
468 port: *const PamojaSerialPort,
469) -> PamojaSerialSettings {
470 port.as_ref()
471 .map_or(Settings::new(9_600), |port| port.port.settings())
472 .into()
473}
474
475#[no_mangle]
493pub unsafe extern "C" fn pamoja_serial_port_write(
494 port: *const PamojaSerialPort,
495 bytes: *const u8,
496 len: usize,
497) -> PamojaStatus {
498 let bytes = match read_bytes(bytes, len) {
499 Ok(bytes) => bytes,
500 Err(status) => return status,
501 };
502 on_port(port, |port| port.write(&bytes))
503}
504
505#[no_mangle]
525pub unsafe extern "C" fn pamoja_serial_port_read(
526 port: *const PamojaSerialPort,
527 out: *mut u8,
528 capacity: usize,
529 timeout_micros: u64,
530 out_len: *mut usize,
531) -> PamojaStatus {
532 let Some(out_len) = out_len.as_mut() else {
533 set_last_error("out_len must not be null".to_owned());
534 return PamojaStatus::InvalidArgument;
535 };
536 *out_len = 0;
537 let buffer: &mut [u8] = if capacity == 0 {
538 &mut []
539 } else if out.is_null() {
540 set_last_error("out must not be null".to_owned());
541 return PamojaStatus::InvalidArgument;
542 } else {
543 std::slice::from_raw_parts_mut(out, capacity)
544 };
545 let timeout = Duration::from_micros(timeout_micros);
546 on_port(port, |port| {
547 *out_len = port.read(buffer, timeout)?;
548 Ok(())
549 })
550}
551
552#[no_mangle]
567pub unsafe extern "C" fn pamoja_serial_port_discard_input(
568 port: *const PamojaSerialPort,
569) -> PamojaStatus {
570 on_port(port, SerialPort::discard_input)
571}
572
573#[no_mangle]
585pub unsafe extern "C" fn pamoja_serial_port_wait(port: *const PamojaSerialPort, micros: u64) {
586 if let Some(port) = port.as_ref() {
587 port.port.wait(Duration::from_micros(micros));
588 }
589}
590
591#[no_mangle]
597pub unsafe extern "C" fn pamoja_serial_port_written(port: *const PamojaSerialPort) -> usize {
598 port.as_ref().map_or(0, |port| port.port.written())
599}
600
601#[no_mangle]
607pub unsafe extern "C" fn pamoja_serial_port_received(port: *const PamojaSerialPort) -> usize {
608 port.as_ref().map_or(0, |port| port.port.received())
609}
610
611#[no_mangle]
618pub unsafe extern "C" fn pamoja_serial_port_waited_micros(port: *const PamojaSerialPort) -> u64 {
619 port.as_ref().map_or(0, |port| port.port.waited_micros())
620}
621
622#[no_mangle]
638pub unsafe extern "C" fn pamoja_serial_port_remaining(
639 port: *const PamojaSerialPort,
640 out_remaining: *mut usize,
641) -> bool {
642 let (Some(port), Some(out)) = (port.as_ref(), out_remaining.as_mut()) else {
643 return false;
644 };
645 match port.port.remaining() {
646 Some(remaining) => {
647 *out = remaining;
648 true
649 }
650 None => false,
651 }
652}
653
654#[no_mangle]
661pub unsafe extern "C" fn pamoja_serial_port_free(port: *mut PamojaSerialPort) {
662 if !port.is_null() {
663 drop(Box::from_raw(port));
664 }
665}
666
667fn into_raw(port: SerialPort) -> *mut PamojaSerialPort {
668 Box::into_raw(Box::new(PamojaSerialPort { port }))
669}
670
671unsafe fn add_step(
677 script: *mut PamojaSerialScript,
678 bytes: *const u8,
679 len: usize,
680 step: fn(Vec<u8>) -> PortStep,
681) -> PamojaStatus {
682 let Some(script) = script.as_mut() else {
683 set_last_error("script must not be null".to_owned());
684 return PamojaStatus::InvalidArgument;
685 };
686 match read_bytes(bytes, len) {
687 Ok(bytes) => {
688 script.steps.push(step(bytes));
689 PamojaStatus::Ok
690 }
691 Err(status) => status,
692 }
693}
694
695unsafe fn on_port(
701 port: *const PamojaSerialPort,
702 call: impl FnOnce(&SerialPort) -> Result<(), PortError>,
703) -> PamojaStatus {
704 let Some(port) = port.as_ref() else {
705 set_last_error("port must not be null".to_owned());
706 return PamojaStatus::InvalidArgument;
707 };
708 match catch_unwind(AssertUnwindSafe(|| call(&port.port))) {
709 Ok(Ok(())) => PamojaStatus::Ok,
710 Ok(Err(error)) => {
711 set_last_error(error.to_string());
712 PamojaStatus::Io
713 }
714 Err(_) => panicked(),
715 }
716}
717
718fn panicked() -> PamojaStatus {
720 set_last_error("panic at the FFI boundary".to_owned());
721 PamojaStatus::Panic
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use std::ffi::{CStr, CString};
728 use std::ptr;
729
730 fn last_error() -> String {
731 let message = crate::pamoja_last_error_message();
732 if message.is_null() {
733 return String::new();
734 }
735 unsafe { CStr::from_ptr(message) }
736 .to_string_lossy()
737 .into_owned()
738 }
739
740 #[test]
741 fn settings_cross_the_boundary_and_are_checked() {
742 let settings = pamoja_serial_settings(9_600);
743 assert_eq!(
744 settings,
745 PamojaSerialSettings {
746 baud: 9_600,
747 parity: PAMOJA_PARITY_NONE,
748 stop_bits: 1
749 }
750 );
751 let modbus = PamojaSerialSettings {
752 parity: PAMOJA_PARITY_EVEN,
753 ..settings
754 };
755 assert_eq!(pamoja_serial_settings_bits_per_character(modbus), 11);
756 assert_eq!(pamoja_serial_settings_character_nanos(modbus), 1_145_834);
757 let odd = PamojaSerialSettings {
758 parity: 7,
759 ..settings
760 };
761 assert_eq!(pamoja_serial_settings_bits_per_character(odd), 0);
762 assert!(pamoja_serial_port_looped(odd).is_null());
763 assert!(last_error().contains("parity 7"), "{}", last_error());
764 }
765
766 #[test]
767 fn a_pair_carries_bytes_both_ways_and_counts_an_empty_read() {
768 unsafe {
769 let (mut one, mut other) = (ptr::null_mut(), ptr::null_mut());
770 assert_eq!(
771 pamoja_serial_port_pair(pamoja_serial_settings(115_200), &mut one, &mut other),
772 PamojaStatus::Ok
773 );
774 assert_eq!(pamoja_serial_port_kind(one), PAMOJA_SERIAL_PORT_PAIRED);
775 let message = b"t=21.5";
776 assert_eq!(
777 pamoja_serial_port_write(one, message.as_ptr(), message.len()),
778 PamojaStatus::Ok
779 );
780 let reader = pamoja_serial_port_clone(other);
781 let mut buffer = [0u8; 16];
782 let mut got = 0;
783 assert_eq!(
784 pamoja_serial_port_read(
785 reader,
786 buffer.as_mut_ptr(),
787 buffer.len(),
788 50_000,
789 &mut got
790 ),
791 PamojaStatus::Ok
792 );
793 assert_eq!(&buffer[..got], message);
794 assert_eq!(
795 pamoja_serial_port_read(other, buffer.as_mut_ptr(), buffer.len(), 50_000, &mut got),
796 PamojaStatus::Ok
797 );
798 assert_eq!(got, 0);
799 assert_eq!(pamoja_serial_port_waited_micros(other), 50_000);
800 assert_eq!(pamoja_serial_port_received(other), message.len());
801 pamoja_serial_port_wait(one, 1_750);
802 assert_eq!(pamoja_serial_port_waited_micros(one), 1_750);
803 assert_eq!(pamoja_serial_port_settings(one).baud, 115_200);
804 pamoja_serial_port_free(reader);
805 pamoja_serial_port_free(one);
806 pamoja_serial_port_free(other);
807 }
808 }
809
810 #[test]
811 fn a_script_refuses_an_unexpected_write() {
812 unsafe {
813 let script = pamoja_serial_script_new();
814 assert_eq!(
815 pamoja_serial_script_write(script, b"?".as_ptr(), 1),
816 PamojaStatus::Ok
817 );
818 assert_eq!(
819 pamoja_serial_script_read(script, b"42".as_ptr(), 2),
820 PamojaStatus::Ok
821 );
822 assert_eq!(pamoja_serial_script_len(script), 2);
823 let port = pamoja_serial_port_scripted(pamoja_serial_settings(9_600), script);
824 pamoja_serial_script_free(script);
825 assert_eq!(pamoja_serial_port_kind(port), PAMOJA_SERIAL_PORT_SCRIPTED);
826 assert_eq!(
827 pamoja_serial_port_write(port, b"!".as_ptr(), 1),
828 PamojaStatus::Io
829 );
830 assert!(last_error().contains("expected 3f"), "{}", last_error());
831 assert_eq!(
832 pamoja_serial_port_write(port, b"?".as_ptr(), 1),
833 PamojaStatus::Ok
834 );
835 let mut remaining = 9;
836 assert!(pamoja_serial_port_remaining(port, &mut remaining));
837 assert_eq!(remaining, 0);
838 let mut buffer = [0u8; 4];
839 let mut got = 0;
840 pamoja_serial_port_read(port, buffer.as_mut_ptr(), buffer.len(), 0, &mut got);
841 assert_eq!(&buffer[..got], b"42");
842 assert_eq!(pamoja_serial_port_discard_input(port), PamojaStatus::Ok);
843 pamoja_serial_port_free(port);
844 }
845 }
846
847 #[test]
848 fn a_looped_line_and_the_null_checks() {
849 unsafe {
850 let port = pamoja_serial_port_looped(pamoja_serial_settings(9_600));
851 assert_eq!(
852 pamoja_serial_port_write(port, b"abc".as_ptr(), 3),
853 PamojaStatus::Ok
854 );
855 let mut buffer = [0u8; 3];
856 let mut got = 0;
857 assert_eq!(
858 pamoja_serial_port_read(port, buffer.as_mut_ptr(), 3, 0, ptr::null_mut()),
859 PamojaStatus::InvalidArgument
860 );
861 assert_eq!(
862 pamoja_serial_port_read(port, buffer.as_mut_ptr(), 3, 0, &mut got),
863 PamojaStatus::Ok
864 );
865 assert_eq!(&buffer, b"abc");
866 assert_eq!(pamoja_serial_port_written(port), 3);
867 assert_eq!(
868 pamoja_serial_port_write(ptr::null(), b"x".as_ptr(), 1),
869 PamojaStatus::InvalidArgument
870 );
871 pamoja_serial_port_free(port);
872
873 let path = CString::new("/dev/serial0").unwrap();
874 let mut opened = ptr::null_mut();
875 let status = pamoja_serial_port_open(
876 path.as_ptr(),
877 pamoja_serial_settings(115_200),
878 &mut opened,
879 );
880 if cfg!(target_os = "linux") {
881 assert_ne!(status, PamojaStatus::Unsupported);
882 } else {
883 assert_eq!(status, PamojaStatus::Unsupported);
884 }
885 pamoja_serial_port_free(opened);
886 }
887 }
888}