Skip to main content

pamoja_ffi/
port.rs

1//! The C ABI for one serial port that a program and every driver on it share.
2//!
3//! A port is the kernel's serial device on a Linux board, a line looped back on itself, one
4//! end of a null-modem pair, or a script of what a driver is expected to write, and the same
5//! calls work on each: a write, and a read that waits up to a timeout for the first byte. On
6//! anything but the kernel's device a read never waits; the time it would have waited is
7//! counted instead, so a test of a device that never answers runs at once.
8//!
9//! A port holds its line in shared hands. [`pamoja_serial_port_clone`] gives another holder,
10//! such as a thread that reads, the same port, and the line closes when the last holder
11//! lets go. A script is built here and copied onto a port, so its handle stays the caller's
12//! to free. A failed read or write returns [`PamojaStatus::Io`] with the reason in the last
13//! error message.
14
15use 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
25/// No parity bit.
26pub const PAMOJA_PARITY_NONE: u8 = 0;
27
28/// A parity bit that makes the count of ones even, what Modbus RTU asks for by default.
29pub const PAMOJA_PARITY_EVEN: u8 = 1;
30
31/// A parity bit that makes the count of ones odd.
32pub const PAMOJA_PARITY_ODD: u8 = 2;
33
34/// A port kind: the kernel's serial device, with a real line on the other end.
35pub const PAMOJA_SERIAL_PORT_DEVICE: u8 = 0;
36
37/// A port kind: the port's own output, looped back to its input.
38pub const PAMOJA_SERIAL_PORT_LOOPED: u8 = 1;
39
40/// A port kind: the other end of a null-modem pair.
41pub const PAMOJA_SERIAL_PORT_PAIRED: u8 = 2;
42
43/// A port kind: a simulated device that answers each write.
44pub const PAMOJA_SERIAL_PORT_SIMULATED: u8 = 3;
45
46/// A port kind: a script of the writes a driver is expected to make.
47pub const PAMOJA_SERIAL_PORT_SCRIPTED: u8 = 4;
48
49/// A port's speed and character format: eight data bits, with the parity and stop bits given.
50#[repr(C)]
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct PamojaSerialSettings {
53    /// The speed, in bits a second.
54    pub baud: u32,
55    /// [`PAMOJA_PARITY_NONE`], [`PAMOJA_PARITY_EVEN`], or [`PAMOJA_PARITY_ODD`].
56    pub parity: u8,
57    /// 1 or 2.
58    pub stop_bits: u8,
59}
60
61/// One serial port, shared with every holder of it. Opaque; release it with
62/// [`pamoja_serial_port_free`].
63pub struct PamojaSerialPort {
64    pub(crate) port: SerialPort,
65}
66
67/// The writes a driver is expected to make and the bytes the far end sends, in order. Opaque;
68/// release it with [`pamoja_serial_script_free`].
69pub 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    /// Reads the settings back, refusing a parity or a stop bit count the port does not have.
92    ///
93    /// # Returns
94    ///
95    /// The settings, or [`PamojaStatus::InvalidArgument`] with the reason in the last error
96    /// message.
97    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/// Returns the format most devices start in: eight data bits, no parity, one stop bit.
126///
127/// # Arguments
128///
129/// * `baud` - the speed, in bits a second.
130///
131/// # Returns
132///
133/// The settings.
134#[no_mangle]
135pub extern "C" fn pamoja_serial_settings(baud: u32) -> PamojaSerialSettings {
136    Settings::new(baud).into()
137}
138
139/// Returns the bits one character takes on the wire: a start bit, eight data bits, the parity
140/// bit if there is one, and the stop bits.
141///
142/// # Arguments
143///
144/// * `settings` - the format.
145///
146/// # Returns
147///
148/// The bit count, or 0 for settings the port does not accept.
149#[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/// Returns how long one character takes on the wire.
157///
158/// # Arguments
159///
160/// * `settings` - the speed and format.
161///
162/// # Returns
163///
164/// The time in nanoseconds, rounded up, or 0 for settings the port does not accept.
165#[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/// Opens the kernel's serial device raw, at a speed and a character format. Whatever the device
173/// received before it was opened is dropped.
174///
175/// # Arguments
176///
177/// * `path` - the device file: `/dev/serial0` for a Raspberry Pi's own UART, `/dev/ttyUSB0` or
178///   `/dev/ttyACM0` for a USB adapter.
179/// * `settings` - the speed, one of the standard rates from 1200 to 921600, and the format.
180/// * `out_port` - receives the port, or null when opening fails.
181///
182/// # Returns
183///
184/// [`PamojaStatus::Ok`] with the port in `out_port`; [`PamojaStatus::Unsupported`] on any
185/// platform but Linux; [`PamojaStatus::InvalidArgument`] for a null or non-UTF-8 argument or
186/// settings the port does not have; or [`PamojaStatus::Io`] when the device cannot be opened
187/// or set up, with a last error message that names it.
188///
189/// # Safety
190///
191/// `path` must be a null-terminated string or null, and `out_port` a writable pointer or null.
192#[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/// Creates a line looped back on itself: every byte written is waiting to be read, as with TX
227/// wired to RX.
228///
229/// # Arguments
230///
231/// * `settings` - the speed and format the line runs at.
232///
233/// # Returns
234///
235/// The port, which the caller releases with [`pamoja_serial_port_free`], or null for settings
236/// the port does not have.
237#[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/// Creates the two ends of a null-modem pair: what one end writes, the other reads.
248///
249/// # Arguments
250///
251/// * `settings` - the speed and format both ends run at.
252/// * `out_one` - receives one end.
253/// * `out_other` - receives the other end.
254///
255/// # Returns
256///
257/// [`PamojaStatus::Ok`] with both ends set, each released with [`pamoja_serial_port_free`];
258/// or [`PamojaStatus::InvalidArgument`] for a null out pointer or settings the port does not
259/// have.
260///
261/// # Safety
262///
263/// `out_one` and `out_other` must be writable pointers or null.
264#[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/// Creates an empty script.
287///
288/// # Returns
289///
290/// The script, which the caller releases with [`pamoja_serial_script_free`].
291#[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/// Adds a write the program is expected to make, in one call.
297///
298/// # Arguments
299///
300/// * `script` - the script.
301/// * `bytes` - the bytes of the write.
302/// * `len` - how many bytes.
303///
304/// # Returns
305///
306/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
307///
308/// # Safety
309///
310/// `script` must be a live handle or null, and `bytes` must point to `len` readable bytes.
311#[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/// Adds bytes the far end sends, readable once every step before them has happened.
321///
322/// # Arguments
323///
324/// * `script` - the script.
325/// * `bytes` - what arrives.
326/// * `len` - how many bytes.
327///
328/// # Returns
329///
330/// As [`pamoja_serial_script_write`].
331///
332/// # Safety
333///
334/// `script` must be a live handle or null, and `bytes` must point to `len` readable bytes.
335#[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/// Returns how many steps a script holds.
345///
346/// # Arguments
347///
348/// * `script` - the script.
349///
350/// # Returns
351///
352/// The count, or 0 for a null script.
353///
354/// # Safety
355///
356/// `script` must be a live handle or null.
357#[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/// Releases a script. A null pointer is ignored.
363///
364/// # Safety
365///
366/// `script` must be a handle that has not been freed, or null.
367#[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/// Creates a port that plays a script: each write has to be the one the script expects next,
375/// and the bytes the far end sends become readable as the script reaches them.
376///
377/// # Arguments
378///
379/// * `settings` - the speed and format the line runs at.
380/// * `script` - the steps to copy; the caller still owns the handle.
381///
382/// # Returns
383///
384/// The port, which the caller releases with [`pamoja_serial_port_free`], or null for a null
385/// script or settings the port does not have.
386///
387/// # Safety
388///
389/// `script` must be a live handle or null.
390#[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/// Gives another holder the same port, such as a thread that reads while the caller writes.
406///
407/// # Arguments
408///
409/// * `port` - the port.
410///
411/// # Returns
412///
413/// A new handle to the same port, released with [`pamoja_serial_port_free`], or null for a null
414/// port.
415///
416/// # Safety
417///
418/// `port` must be a live handle or null.
419#[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/// Returns what is on the other end of a port.
430///
431/// # Arguments
432///
433/// * `port` - the port.
434///
435/// # Returns
436///
437/// One of the `PAMOJA_SERIAL_PORT_` kinds; the looped code for a null port.
438///
439/// # Safety
440///
441/// `port` must be a live handle or null.
442#[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/// Returns the speed and format a port runs at.
454///
455/// # Arguments
456///
457/// * `port` - the port.
458///
459/// # Returns
460///
461/// The settings, or 9600 8N1 for a null port.
462///
463/// # Safety
464///
465/// `port` must be a live handle or null.
466#[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/// Writes bytes, and on the kernel's device waits until they have left the UART.
476///
477/// # Arguments
478///
479/// * `port` - the port.
480/// * `bytes` - the bytes, in order.
481/// * `len` - how many bytes.
482///
483/// # Returns
484///
485/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument; or
486/// [`PamojaStatus::Io`] when a script expected another write or the device failed, with the
487/// reason in the last error message.
488///
489/// # Safety
490///
491/// `port` must be a live handle or null, and `bytes` must point to `len` readable bytes.
492#[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/// Reads what has arrived, waiting up to a timeout for the first byte when nothing has. On any
506/// port but the kernel's device the read does not wait, and the timeout is counted instead.
507///
508/// # Arguments
509///
510/// * `port` - the port.
511/// * `out` - receives the bytes.
512/// * `capacity` - the most bytes to read.
513/// * `timeout_micros` - how long to wait for the first byte.
514/// * `out_len` - receives how many bytes were read, zero when the timeout passed with nothing.
515///
516/// # Returns
517///
518/// As [`pamoja_serial_port_write`], and [`PamojaStatus::InvalidArgument`] for a null `out_len`.
519///
520/// # Safety
521///
522/// `port` must be a live handle or null, `out` must point to `capacity` writable bytes, and
523/// `out_len` must be a writable pointer or null.
524#[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/// Drops whatever has arrived and not been read, as a client does before a request so a stale
553/// reply cannot be taken for the new one.
554///
555/// # Arguments
556///
557/// * `port` - the port.
558///
559/// # Returns
560///
561/// As [`pamoja_serial_port_write`].
562///
563/// # Safety
564///
565/// `port` must be a live handle or null.
566#[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/// Waits, as a protocol does to leave the line silent between frames: the process sleeps on
574/// the kernel's device, and anywhere else the wait is only counted.
575///
576/// # Arguments
577///
578/// * `port` - the port.
579/// * `micros` - how long.
580///
581/// # Safety
582///
583/// `port` must be a live handle or null.
584#[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/// Returns how many bytes have been written through a port and every holder of it.
592///
593/// # Safety
594///
595/// `port` must be a live handle or null.
596#[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/// Returns how many bytes have been read through a port and every holder of it.
602///
603/// # Safety
604///
605/// `port` must be a live handle or null.
606#[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/// Returns how long reads have waited without an answer, and waits have waited, in
612/// microseconds, whether or not the process slept through it.
613///
614/// # Safety
615///
616/// `port` must be a live handle or null.
617#[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/// Reports how many steps a scripted port has left.
623///
624/// # Arguments
625///
626/// * `port` - the port.
627/// * `out_remaining` - receives the steps not yet reached.
628///
629/// # Returns
630///
631/// `true` with the count in `out_remaining` for a scripted port; `false` for any other port, a
632/// null port, or a null `out_remaining`.
633///
634/// # Safety
635///
636/// `port` must be a live handle or null, and `out_remaining` a writable pointer or null.
637#[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/// Releases a holder's share of a port. The line closes when no holder has it. A null pointer is
655/// ignored.
656///
657/// # Safety
658///
659/// `port` must be a handle that has not been freed, or null.
660#[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
671/// Adds a step to a script, turning a null script or null bytes into a status.
672///
673/// # Safety
674///
675/// `script` must be a live handle or null, and `bytes` must point to `len` readable bytes.
676unsafe 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
695/// Runs a call on a port, turning a failure or a panic into a status.
696///
697/// # Safety
698///
699/// `port` must be a live handle or null.
700unsafe 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
718/// Records a caught panic and reports it as [`PamojaStatus::Panic`].
719fn 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}