Skip to main content

pamoja_ffi/
modbus_client.rs

1//! The C ABI for a Modbus client on a serial port, and the devices it polls.
2//!
3//! A client runs each transaction to the Modbus over Serial Line specification over a
4//! [`PamojaSerialPort`]: the kernel's serial device on a Linux board, or a port whose far end
5//! is a [`PamojaModbusLine`] of simulated devices. A device is a [`PamojaModbusServer`], a unit
6//! address and the four tables it serves, which also answers a single frame on its own. The
7//! program keeps its handle to each device after putting it on a line, so it can look at what
8//! a write did.
9//!
10//! A failed transaction returns a status with the reason in the last error message, and fills
11//! a [`PamojaModbusClientError`] when the caller passes one, so a program can tell a device's
12//! exception from a timeout without reading the message.
13
14use std::panic::{catch_unwind, AssertUnwindSafe};
15use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
16use std::time::Duration;
17
18use pamoja_hal::port::SerialPort;
19use pamoja_modbus::{Client, ClientError, Line, Server};
20
21use crate::modbus::{failed, out_slot, panicked, read_values};
22use crate::port::{PamojaSerialPort, PamojaSerialSettings};
23use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
24
25/// A client error kind: the call succeeded.
26pub const PAMOJA_MODBUS_CLIENT_OK: u8 = 0;
27
28/// A client error kind: the request could not be built, from a quantity outside what one
29/// request carries.
30pub const PAMOJA_MODBUS_CLIENT_REQUEST: u8 = 1;
31
32/// A client error kind: a read was addressed to the broadcast address, which no device answers.
33pub const PAMOJA_MODBUS_CLIENT_BROADCAST_READ: u8 = 2;
34
35/// A client error kind: the serial port failed.
36pub const PAMOJA_MODBUS_CLIENT_PORT: u8 = 3;
37
38/// A client error kind: no complete reply arrived within the response timeout.
39pub const PAMOJA_MODBUS_CLIENT_TIMEOUT: u8 = 4;
40
41/// A client error kind: the reply failed its CRC or is not the shape its function gives.
42pub const PAMOJA_MODBUS_CLIENT_FRAME: u8 = 5;
43
44/// A client error kind: a reply came back from another unit.
45pub const PAMOJA_MODBUS_CLIENT_WRONG_UNIT: u8 = 6;
46
47/// A client error kind: a reply answered another function.
48pub const PAMOJA_MODBUS_CLIENT_WRONG_FUNCTION: u8 = 7;
49
50/// A client error kind: a well-formed reply that does not answer the request.
51pub const PAMOJA_MODBUS_CLIENT_MISMATCH: u8 = 8;
52
53/// A client error kind: the device refused the request with an exception.
54pub const PAMOJA_MODBUS_CLIENT_EXCEPTION: u8 = 9;
55
56/// Why a client call failed, for a program that branches on it.
57#[repr(C)]
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub struct PamojaModbusClientError {
60    /// One of the `PAMOJA_MODBUS_CLIENT_*` kinds, [`PAMOJA_MODBUS_CLIENT_OK`] on success.
61    pub kind: u8,
62    /// The unit the call asked.
63    pub unit: u8,
64    /// The function the call asked, for an exception or a reply to another function.
65    pub function: u8,
66    /// What the reply named instead: the unit that answered, or the function it answered.
67    pub found: u8,
68    /// The exception code the device answered with.
69    pub exception: u8,
70    /// How many bytes of a reply had arrived before a timeout.
71    pub received: u32,
72}
73
74/// A Modbus device: a unit address and the four tables it serves. Opaque; release it with
75/// [`pamoja_modbus_server_free`]. A line it is on keeps its own share of it.
76pub struct PamojaModbusServer {
77    server: Arc<Mutex<Server>>,
78}
79
80/// Several devices on one simulated line. Opaque; release it with
81/// [`pamoja_modbus_line_free`]. A port made from it keeps its own share of it.
82pub struct PamojaModbusLine {
83    line: Arc<Mutex<Line>>,
84}
85
86/// A Modbus RTU client on a serial port. Opaque; release it with [`pamoja_modbus_client_free`].
87pub struct PamojaModbusClient {
88    client: Mutex<Client>,
89}
90
91fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
92    mutex.lock().unwrap_or_else(PoisonError::into_inner)
93}
94
95/// Makes a device at a unit address, with every table empty.
96///
97/// # Arguments
98///
99/// * `unit` - its address, 1 to 247.
100/// * `out_server` - receives the device.
101///
102/// # Returns
103///
104/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null `out_server` or a unit
105/// outside 1 to 247, with the reason in the last error message.
106///
107/// # Safety
108///
109/// `out_server` must be a writable pointer or null.
110#[no_mangle]
111pub unsafe extern "C" fn pamoja_modbus_server_new(
112    unit: u8,
113    out_server: *mut *mut PamojaModbusServer,
114) -> PamojaStatus {
115    let out_server = match out_slot(out_server, "out_server") {
116        Ok(slot) => slot,
117        Err(status) => return status,
118    };
119    match Server::new(unit) {
120        Ok(server) => {
121            *out_server = Box::into_raw(Box::new(PamojaModbusServer {
122                server: Arc::new(Mutex::new(server)),
123            }));
124            PamojaStatus::Ok
125        }
126        Err(error) => failed(error),
127    }
128}
129
130/// Runs a call on a device, turning a null handle or a panic into a status.
131///
132/// # Safety
133///
134/// `server` must be a live handle or null.
135unsafe fn on_server(
136    server: *const PamojaModbusServer,
137    call: impl FnOnce(&mut Server),
138) -> PamojaStatus {
139    let Some(server) = server.as_ref() else {
140        set_last_error("server must not be null".to_owned());
141        return PamojaStatus::InvalidArgument;
142    };
143    match catch_unwind(AssertUnwindSafe(|| call(&mut lock(&server.server)))) {
144        Ok(()) => PamojaStatus::Ok,
145        Err(_) => panicked(),
146    }
147}
148
149/// Sets coils from an address on, adding any the device did not have.
150///
151/// # Arguments
152///
153/// * `server` - the device.
154/// * `start` - the first coil's address.
155/// * `values` - one byte per coil, non-zero for on.
156/// * `len` - how many coils.
157///
158/// # Returns
159///
160/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
161///
162/// # Safety
163///
164/// `server` must be a live handle or null, and `values` must point to `len` readable bytes.
165#[no_mangle]
166pub unsafe extern "C" fn pamoja_modbus_server_set_coils(
167    server: *const PamojaModbusServer,
168    start: u16,
169    values: *const u8,
170    len: usize,
171) -> PamojaStatus {
172    match read_values(values, len, "values") {
173        Ok(values) => {
174            let values: Vec<bool> = values.into_iter().map(|value| value != 0).collect();
175            on_server(server, |server| server.set_coils(start, &values))
176        }
177        Err(status) => status,
178    }
179}
180
181/// Sets discrete inputs from an address on, adding any the device did not have.
182///
183/// # Arguments
184///
185/// * `server` - the device.
186/// * `start` - the first input's address.
187/// * `values` - one byte per input, non-zero for on.
188/// * `len` - how many inputs.
189///
190/// # Returns
191///
192/// As [`pamoja_modbus_server_set_coils`].
193///
194/// # Safety
195///
196/// As [`pamoja_modbus_server_set_coils`].
197#[no_mangle]
198pub unsafe extern "C" fn pamoja_modbus_server_set_discrete_inputs(
199    server: *const PamojaModbusServer,
200    start: u16,
201    values: *const u8,
202    len: usize,
203) -> PamojaStatus {
204    match read_values(values, len, "values") {
205        Ok(values) => {
206            let values: Vec<bool> = values.into_iter().map(|value| value != 0).collect();
207            on_server(server, |server| server.set_discrete_inputs(start, &values))
208        }
209        Err(status) => status,
210    }
211}
212
213/// Sets holding registers from an address on, adding any the device did not have.
214///
215/// # Arguments
216///
217/// * `server` - the device.
218/// * `start` - the first register's address.
219/// * `values` - the values, in address order.
220/// * `len` - how many registers.
221///
222/// # Returns
223///
224/// As [`pamoja_modbus_server_set_coils`].
225///
226/// # Safety
227///
228/// `server` must be a live handle or null, and `values` must point to `len` readable values.
229#[no_mangle]
230pub unsafe extern "C" fn pamoja_modbus_server_set_holding_registers(
231    server: *const PamojaModbusServer,
232    start: u16,
233    values: *const u16,
234    len: usize,
235) -> PamojaStatus {
236    match read_values(values, len, "values") {
237        Ok(values) => on_server(server, |server| {
238            server.set_holding_registers(start, &values)
239        }),
240        Err(status) => status,
241    }
242}
243
244/// Sets input registers from an address on, adding any the device did not have.
245///
246/// # Arguments
247///
248/// * `server` - the device.
249/// * `start` - the first register's address.
250/// * `values` - the values, in address order.
251/// * `len` - how many registers.
252///
253/// # Returns
254///
255/// As [`pamoja_modbus_server_set_coils`].
256///
257/// # Safety
258///
259/// As [`pamoja_modbus_server_set_holding_registers`].
260#[no_mangle]
261pub unsafe extern "C" fn pamoja_modbus_server_set_input_registers(
262    server: *const PamojaModbusServer,
263    start: u16,
264    values: *const u16,
265    len: usize,
266) -> PamojaStatus {
267    match read_values(values, len, "values") {
268        Ok(values) => on_server(server, |server| server.set_input_registers(start, &values)),
269        Err(status) => status,
270    }
271}
272
273/// Reads one entry of a device's table, reporting whether the device has it.
274///
275/// # Safety
276///
277/// `server` must be a live handle or null, and `out` a writable pointer or null.
278unsafe fn entry<T>(
279    server: *const PamojaModbusServer,
280    out: *mut T,
281    read: impl FnOnce(&Server) -> Option<T>,
282) -> bool {
283    let (Some(server), Some(out)) = (server.as_ref(), out.as_mut()) else {
284        return false;
285    };
286    match read(&lock(&server.server)) {
287        Some(value) => {
288            *out = value;
289            true
290        }
291        None => false,
292    }
293}
294
295/// Reads a coil.
296///
297/// # Arguments
298///
299/// * `server` - the device.
300/// * `address` - the coil's address.
301/// * `out_on` - receives its state.
302///
303/// # Returns
304///
305/// `true` with the state in `out_on` when the device has the coil; `false` when it does not,
306/// or for a null argument.
307///
308/// # Safety
309///
310/// `server` must be a live handle or null, and `out_on` a writable pointer or null.
311#[no_mangle]
312pub unsafe extern "C" fn pamoja_modbus_server_coil(
313    server: *const PamojaModbusServer,
314    address: u16,
315    out_on: *mut bool,
316) -> bool {
317    entry(server, out_on, |server| server.coil(address))
318}
319
320/// Reads a discrete input.
321///
322/// # Arguments
323///
324/// * `server` - the device.
325/// * `address` - the input's address.
326/// * `out_on` - receives its state.
327///
328/// # Returns
329///
330/// As [`pamoja_modbus_server_coil`].
331///
332/// # Safety
333///
334/// As [`pamoja_modbus_server_coil`].
335#[no_mangle]
336pub unsafe extern "C" fn pamoja_modbus_server_discrete_input(
337    server: *const PamojaModbusServer,
338    address: u16,
339    out_on: *mut bool,
340) -> bool {
341    entry(server, out_on, |server| server.discrete_input(address))
342}
343
344/// Reads a holding register.
345///
346/// # Arguments
347///
348/// * `server` - the device.
349/// * `address` - the register's address.
350/// * `out_value` - receives its value.
351///
352/// # Returns
353///
354/// `true` with the value in `out_value` when the device has the register; `false` when it
355/// does not, or for a null argument.
356///
357/// # Safety
358///
359/// `server` must be a live handle or null, and `out_value` a writable pointer or null.
360#[no_mangle]
361pub unsafe extern "C" fn pamoja_modbus_server_holding_register(
362    server: *const PamojaModbusServer,
363    address: u16,
364    out_value: *mut u16,
365) -> bool {
366    entry(server, out_value, |server| server.holding_register(address))
367}
368
369/// Reads an input register.
370///
371/// # Arguments
372///
373/// * `server` - the device.
374/// * `address` - the register's address.
375/// * `out_value` - receives its value.
376///
377/// # Returns
378///
379/// As [`pamoja_modbus_server_holding_register`].
380///
381/// # Safety
382///
383/// As [`pamoja_modbus_server_holding_register`].
384#[no_mangle]
385pub unsafe extern "C" fn pamoja_modbus_server_input_register(
386    server: *const PamojaModbusServer,
387    address: u16,
388    out_value: *mut u16,
389) -> bool {
390    entry(server, out_value, |server| server.input_register(address))
391}
392
393/// Returns a device's unit address, or 0 for a null handle.
394///
395/// # Safety
396///
397/// `server` must be a live handle or null.
398#[no_mangle]
399pub unsafe extern "C" fn pamoja_modbus_server_unit(server: *const PamojaModbusServer) -> u8 {
400    server
401        .as_ref()
402        .map_or(0, |server| lock(&server.server).unit())
403}
404
405/// Returns how many requests a device has carried out, broadcasts included and refusals not,
406/// or 0 for a null handle.
407///
408/// # Safety
409///
410/// `server` must be a live handle or null.
411#[no_mangle]
412pub unsafe extern "C" fn pamoja_modbus_server_served(server: *const PamojaModbusServer) -> usize {
413    server
414        .as_ref()
415        .map_or(0, |server| lock(&server.server).served())
416}
417
418/// Answers one RTU frame, as the device on the line does.
419///
420/// # Arguments
421///
422/// * `server` - the device.
423/// * `frame` - the frame as it came off the line, CRC included.
424/// * `len` - its length.
425/// * `out_buffer` - receives the frame to send back, or null when the device stays silent: the
426///   frame failed its CRC, is for another unit, or is a broadcast, whose write the device still
427///   carries out.
428///
429/// # Returns
430///
431/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
432///
433/// # Safety
434///
435/// `server` must be a live handle or null, `frame` must point to `len` readable bytes, and
436/// `out_buffer` must be a writable pointer or null. A returned buffer is the caller's to
437/// release with [`pamoja_buffer_free`](crate::pamoja_buffer_free).
438#[no_mangle]
439pub unsafe extern "C" fn pamoja_modbus_server_answer(
440    server: *const PamojaModbusServer,
441    frame: *const u8,
442    len: usize,
443    out_buffer: *mut *mut PamojaBuffer,
444) -> PamojaStatus {
445    let out_buffer = match out_slot(out_buffer, "out_buffer") {
446        Ok(slot) => slot,
447        Err(status) => return status,
448    };
449    let frame = match read_bytes(frame, len) {
450        Ok(frame) => frame,
451        Err(status) => return status,
452    };
453    on_server(server, |server| {
454        if let Some(reply) = server.answer(&frame) {
455            *out_buffer = PamojaBuffer::into_raw(reply.as_bytes().to_vec());
456        }
457    })
458}
459
460/// Releases the caller's handle to a device. A line it is on keeps its own share. A null
461/// pointer is ignored.
462///
463/// # Safety
464///
465/// `server` must be a handle that has not been freed, or null.
466#[no_mangle]
467pub unsafe extern "C" fn pamoja_modbus_server_free(server: *mut PamojaModbusServer) {
468    if !server.is_null() {
469        drop(Box::from_raw(server));
470    }
471}
472
473/// Makes a line with no devices on it.
474///
475/// # Returns
476///
477/// The line, which the caller releases with [`pamoja_modbus_line_free`].
478#[no_mangle]
479pub extern "C" fn pamoja_modbus_line_new() -> *mut PamojaModbusLine {
480    Box::into_raw(Box::new(PamojaModbusLine {
481        line: Arc::new(Mutex::new(Line::new())),
482    }))
483}
484
485/// Puts a device on a line. The line shares the device, so the caller's handle still reads
486/// and changes it.
487///
488/// # Arguments
489///
490/// * `line` - the line.
491/// * `server` - the device.
492///
493/// # Returns
494///
495/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
496///
497/// # Safety
498///
499/// `line` and `server` must be live handles or null.
500#[no_mangle]
501pub unsafe extern "C" fn pamoja_modbus_line_attach(
502    line: *const PamojaModbusLine,
503    server: *const PamojaModbusServer,
504) -> PamojaStatus {
505    let (Some(line), Some(server)) = (line.as_ref(), server.as_ref()) else {
506        set_last_error("line and server must not be null".to_owned());
507        return PamojaStatus::InvalidArgument;
508    };
509    lock(&line.line).attach_shared(Arc::clone(&server.server));
510    PamojaStatus::Ok
511}
512
513/// Returns how many devices are on a line, or 0 for a null handle.
514///
515/// # Safety
516///
517/// `line` must be a live handle or null.
518#[no_mangle]
519pub unsafe extern "C" fn pamoja_modbus_line_len(line: *const PamojaModbusLine) -> usize {
520    line.as_ref()
521        .map_or(0, |line| lock(&line.line).units().len())
522}
523
524/// Makes a serial port with a line on its far end: every frame written reaches each device,
525/// and whatever they answer waits to be read.
526///
527/// # Arguments
528///
529/// * `line` - the line; devices put on it later are on the port too.
530/// * `settings` - the speed and character format the line runs at.
531/// * `out_port` - receives the port, which the caller releases with
532///   [`pamoja_serial_port_free`](crate::port::pamoja_serial_port_free).
533///
534/// # Returns
535///
536/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or settings
537/// the port does not have.
538///
539/// # Safety
540///
541/// `line` must be a live handle or null, and `out_port` a writable pointer or null.
542#[no_mangle]
543pub unsafe extern "C" fn pamoja_modbus_line_port(
544    line: *const PamojaModbusLine,
545    settings: PamojaSerialSettings,
546    out_port: *mut *mut PamojaSerialPort,
547) -> PamojaStatus {
548    let out_port = match out_slot(out_port, "out_port") {
549        Ok(slot) => slot,
550        Err(status) => return status,
551    };
552    let Some(line) = line.as_ref() else {
553        set_last_error("line must not be null".to_owned());
554        return PamojaStatus::InvalidArgument;
555    };
556    let settings = match settings.settings() {
557        Ok(settings) => settings,
558        Err(status) => return status,
559    };
560    let port = SerialPort::simulated(settings, Arc::clone(&line.line));
561    *out_port = Box::into_raw(Box::new(PamojaSerialPort { port }));
562    PamojaStatus::Ok
563}
564
565/// Releases the caller's handle to a line. A port made from it keeps its own share. A null
566/// pointer is ignored.
567///
568/// # Safety
569///
570/// `line` must be a handle that has not been freed, or null.
571#[no_mangle]
572pub unsafe extern "C" fn pamoja_modbus_line_free(line: *mut PamojaModbusLine) {
573    if !line.is_null() {
574        drop(Box::from_raw(line));
575    }
576}
577
578/// Returns the silence that separates two frames at a line's speed and format: 3.5
579/// characters, and a fixed 1750 microseconds above 19200 baud.
580///
581/// # Arguments
582///
583/// * `settings` - the line's speed and character format.
584/// * `out_nanos` - receives the silence, in nanoseconds.
585///
586/// # Returns
587///
588/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null `out_nanos` or
589/// settings the port does not have.
590///
591/// # Safety
592///
593/// `out_nanos` must be a writable pointer or null.
594#[no_mangle]
595pub unsafe extern "C" fn pamoja_modbus_frame_gap_nanos(
596    settings: PamojaSerialSettings,
597    out_nanos: *mut u64,
598) -> PamojaStatus {
599    let Some(out_nanos) = out_nanos.as_mut() else {
600        set_last_error("out_nanos must not be null".to_owned());
601        return PamojaStatus::InvalidArgument;
602    };
603    match settings.settings() {
604        Ok(settings) => {
605            *out_nanos = u64::try_from(Client::frame_gap(settings).as_nanos()).unwrap_or(u64::MAX);
606            PamojaStatus::Ok
607        }
608        Err(status) => status,
609    }
610}
611
612/// Makes a client on a port, with a one-second response timeout and a 100 ms turnaround.
613///
614/// # Arguments
615///
616/// * `port` - the line; the client holds its own share of it.
617/// * `out_client` - receives the client.
618///
619/// # Returns
620///
621/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
622///
623/// # Safety
624///
625/// `port` must be a live handle or null, and `out_client` a writable pointer or null.
626#[no_mangle]
627pub unsafe extern "C" fn pamoja_modbus_client_new(
628    port: *const PamojaSerialPort,
629    out_client: *mut *mut PamojaModbusClient,
630) -> PamojaStatus {
631    let out_client = match out_slot(out_client, "out_client") {
632        Ok(slot) => slot,
633        Err(status) => return status,
634    };
635    let Some(port) = port.as_ref() else {
636        set_last_error("port must not be null".to_owned());
637        return PamojaStatus::InvalidArgument;
638    };
639    *out_client = Box::into_raw(Box::new(PamojaModbusClient {
640        client: Mutex::new(Client::new(port.port.clone())),
641    }));
642    PamojaStatus::Ok
643}
644
645/// Sets how long a client waits for a whole reply once a request has gone out.
646///
647/// # Arguments
648///
649/// * `client` - the client.
650/// * `micros` - the response timeout.
651///
652/// # Safety
653///
654/// `client` must be a live handle or null.
655#[no_mangle]
656pub unsafe extern "C" fn pamoja_modbus_client_set_response_timeout(
657    client: *const PamojaModbusClient,
658    micros: u64,
659) {
660    if let Some(client) = client.as_ref() {
661        lock(&client.client).set_response_timeout(Duration::from_micros(micros));
662    }
663}
664
665/// Sets how long a client leaves the line quiet after a broadcast.
666///
667/// # Arguments
668///
669/// * `client` - the client.
670/// * `micros` - the turnaround delay.
671///
672/// # Safety
673///
674/// `client` must be a live handle or null.
675#[no_mangle]
676pub unsafe extern "C" fn pamoja_modbus_client_set_turnaround(
677    client: *const PamojaModbusClient,
678    micros: u64,
679) {
680    if let Some(client) = client.as_ref() {
681        lock(&client.client).set_turnaround(Duration::from_micros(micros));
682    }
683}
684
685/// Returns a client's response timeout, in microseconds, or 0 for a null handle.
686///
687/// # Safety
688///
689/// `client` must be a live handle or null.
690#[no_mangle]
691pub unsafe extern "C" fn pamoja_modbus_client_response_timeout_micros(
692    client: *const PamojaModbusClient,
693) -> u64 {
694    client
695        .as_ref()
696        .map_or(0, |client| micros(lock(&client.client).response_timeout()))
697}
698
699/// Returns a client's turnaround delay, in microseconds, or 0 for a null handle.
700///
701/// # Safety
702///
703/// `client` must be a live handle or null.
704#[no_mangle]
705pub unsafe extern "C" fn pamoja_modbus_client_turnaround_micros(
706    client: *const PamojaModbusClient,
707) -> u64 {
708    client
709        .as_ref()
710        .map_or(0, |client| micros(lock(&client.client).turnaround()))
711}
712
713fn micros(duration: Duration) -> u64 {
714    u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
715}
716
717/// Runs a transaction, filling the caller's error and the last error message when it fails.
718///
719/// # Safety
720///
721/// `client` must be a live handle or null, and `out_error` a writable pointer or null.
722unsafe fn transact<T>(
723    client: *const PamojaModbusClient,
724    unit: u8,
725    out_error: *mut PamojaModbusClientError,
726    call: impl FnOnce(&mut Client) -> Result<T, ClientError>,
727) -> Result<T, PamojaStatus> {
728    let mut report = PamojaModbusClientError {
729        unit,
730        ..PamojaModbusClientError::default()
731    };
732    let Some(client) = client.as_ref() else {
733        set_last_error("client must not be null".to_owned());
734        return Err(PamojaStatus::InvalidArgument);
735    };
736    let outcome = catch_unwind(AssertUnwindSafe(|| call(&mut lock(&client.client))));
737    let result = match outcome {
738        Ok(Ok(value)) => Ok(value),
739        Ok(Err(error)) => {
740            set_last_error(error.to_string());
741            let status = match error {
742                ClientError::Request(_) | ClientError::BroadcastRead => {
743                    PamojaStatus::InvalidArgument
744                }
745                ClientError::Port(_) | ClientError::Timeout { .. } => PamojaStatus::Io,
746                ClientError::Exception { .. } => PamojaStatus::Other,
747                _ => PamojaStatus::Codec,
748            };
749            describe(&error, &mut report);
750            Err(status)
751        }
752        Err(_) => Err(panicked()),
753    };
754    if let Some(out_error) = out_error.as_mut() {
755        *out_error = report;
756    }
757    result
758}
759
760fn describe(error: &ClientError, report: &mut PamojaModbusClientError) {
761    report.kind = match *error {
762        ClientError::Request(_) => PAMOJA_MODBUS_CLIENT_REQUEST,
763        ClientError::BroadcastRead => PAMOJA_MODBUS_CLIENT_BROADCAST_READ,
764        ClientError::Port(_) => PAMOJA_MODBUS_CLIENT_PORT,
765        ClientError::Timeout { received, .. } => {
766            report.received = u32::try_from(received).unwrap_or(u32::MAX);
767            PAMOJA_MODBUS_CLIENT_TIMEOUT
768        }
769        ClientError::Frame(_) => PAMOJA_MODBUS_CLIENT_FRAME,
770        ClientError::WrongUnit { found, .. } => {
771            report.found = found;
772            PAMOJA_MODBUS_CLIENT_WRONG_UNIT
773        }
774        ClientError::WrongFunction { expected, found } => {
775            report.function = expected;
776            report.found = found;
777            PAMOJA_MODBUS_CLIENT_WRONG_FUNCTION
778        }
779        ClientError::Mismatch { .. } => PAMOJA_MODBUS_CLIENT_MISMATCH,
780        ClientError::Exception {
781            function,
782            exception,
783            ..
784        } => {
785            report.function = function;
786            report.exception = exception.code();
787            PAMOJA_MODBUS_CLIENT_EXCEPTION
788        }
789    };
790}
791
792/// Copies what a read returned into the caller's array, which holds `quantity` values.
793///
794/// # Safety
795///
796/// `out` must point to `quantity` writable values, or be null when `quantity` is 0.
797unsafe fn read_into<T: Copy, U>(
798    client: *const PamojaModbusClient,
799    unit: u8,
800    quantity: u16,
801    out: *mut U,
802    out_error: *mut PamojaModbusClientError,
803    convert: impl Fn(T) -> U,
804    call: impl FnOnce(&mut Client) -> Result<Vec<T>, ClientError>,
805) -> PamojaStatus {
806    if out.is_null() && quantity > 0 {
807        set_last_error("out_values must not be null".to_owned());
808        return PamojaStatus::InvalidArgument;
809    }
810    match transact(client, unit, out_error, call) {
811        Ok(values) => {
812            let out = std::slice::from_raw_parts_mut(out, values.len().min(usize::from(quantity)));
813            for (slot, value) in out.iter_mut().zip(values) {
814                *slot = convert(value);
815            }
816            PamojaStatus::Ok
817        }
818        Err(status) => status,
819    }
820}
821
822/// Reads coils, function `0x01`.
823///
824/// # Arguments
825///
826/// * `client` - the client.
827/// * `unit` - the device, 1 to 247.
828/// * `start` - the first coil's address.
829/// * `quantity` - how many, 1 to 2000.
830/// * `out_values` - receives one byte per coil, 1 for on, in address order.
831/// * `out_error` - receives why the call failed, or null.
832///
833/// # Returns
834///
835/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a request that cannot be sent;
836/// [`PamojaStatus::Io`] for a port failure or a timeout; [`PamojaStatus::Codec`] for a reply
837/// that fails its checks; or [`PamojaStatus::Other`] for the device's exception. The reason
838/// is in the last error message and, in detail, in `out_error`.
839///
840/// # Safety
841///
842/// `client` must be a live handle or null, `out_values` must point to `quantity` writable
843/// bytes, and `out_error` must be a writable pointer or null.
844#[no_mangle]
845pub unsafe extern "C" fn pamoja_modbus_client_read_coils(
846    client: *const PamojaModbusClient,
847    unit: u8,
848    start: u16,
849    quantity: u16,
850    out_values: *mut u8,
851    out_error: *mut PamojaModbusClientError,
852) -> PamojaStatus {
853    read_into(
854        client,
855        unit,
856        quantity,
857        out_values,
858        out_error,
859        u8::from,
860        |client| client.read_coils(unit, start, quantity),
861    )
862}
863
864/// Reads discrete inputs, function `0x02`.
865///
866/// # Arguments
867///
868/// As [`pamoja_modbus_client_read_coils`], for inputs.
869///
870/// # Returns
871///
872/// As [`pamoja_modbus_client_read_coils`].
873///
874/// # Safety
875///
876/// As [`pamoja_modbus_client_read_coils`].
877#[no_mangle]
878pub unsafe extern "C" fn pamoja_modbus_client_read_discrete_inputs(
879    client: *const PamojaModbusClient,
880    unit: u8,
881    start: u16,
882    quantity: u16,
883    out_values: *mut u8,
884    out_error: *mut PamojaModbusClientError,
885) -> PamojaStatus {
886    read_into(
887        client,
888        unit,
889        quantity,
890        out_values,
891        out_error,
892        u8::from,
893        |client| client.read_discrete_inputs(unit, start, quantity),
894    )
895}
896
897/// Reads holding registers, function `0x03`.
898///
899/// # Arguments
900///
901/// * `client` - the client.
902/// * `unit` - the device, 1 to 247.
903/// * `start` - the first register's address.
904/// * `quantity` - how many, 1 to 125.
905/// * `out_values` - receives the values, in address order.
906/// * `out_error` - receives why the call failed, or null.
907///
908/// # Returns
909///
910/// As [`pamoja_modbus_client_read_coils`].
911///
912/// # Safety
913///
914/// `client` must be a live handle or null, `out_values` must point to `quantity` writable
915/// values, and `out_error` must be a writable pointer or null.
916#[no_mangle]
917pub unsafe extern "C" fn pamoja_modbus_client_read_holding_registers(
918    client: *const PamojaModbusClient,
919    unit: u8,
920    start: u16,
921    quantity: u16,
922    out_values: *mut u16,
923    out_error: *mut PamojaModbusClientError,
924) -> PamojaStatus {
925    read_into(
926        client,
927        unit,
928        quantity,
929        out_values,
930        out_error,
931        |v| v,
932        |client| client.read_holding_registers(unit, start, quantity),
933    )
934}
935
936/// Reads input registers, function `0x04`.
937///
938/// # Arguments
939///
940/// As [`pamoja_modbus_client_read_holding_registers`], for input registers.
941///
942/// # Returns
943///
944/// As [`pamoja_modbus_client_read_coils`].
945///
946/// # Safety
947///
948/// As [`pamoja_modbus_client_read_holding_registers`].
949#[no_mangle]
950pub unsafe extern "C" fn pamoja_modbus_client_read_input_registers(
951    client: *const PamojaModbusClient,
952    unit: u8,
953    start: u16,
954    quantity: u16,
955    out_values: *mut u16,
956    out_error: *mut PamojaModbusClientError,
957) -> PamojaStatus {
958    read_into(
959        client,
960        unit,
961        quantity,
962        out_values,
963        out_error,
964        |v| v,
965        |client| client.read_input_registers(unit, start, quantity),
966    )
967}
968
969/// Writes one coil, function `0x05`.
970///
971/// # Arguments
972///
973/// * `client` - the client.
974/// * `unit` - the device, 1 to 247, or 0 to broadcast to every device.
975/// * `address` - the coil's address.
976/// * `on` - the state to write.
977/// * `out_error` - receives why the call failed, or null.
978///
979/// # Returns
980///
981/// As [`pamoja_modbus_client_read_coils`].
982///
983/// # Safety
984///
985/// `client` must be a live handle or null, and `out_error` a writable pointer or null.
986#[no_mangle]
987pub unsafe extern "C" fn pamoja_modbus_client_write_single_coil(
988    client: *const PamojaModbusClient,
989    unit: u8,
990    address: u16,
991    on: bool,
992    out_error: *mut PamojaModbusClientError,
993) -> PamojaStatus {
994    status(transact(client, unit, out_error, |client| {
995        client.write_single_coil(unit, address, on)
996    }))
997}
998
999/// Writes one holding register, function `0x06`.
1000///
1001/// # Arguments
1002///
1003/// * `client` - the client.
1004/// * `unit` - the device, 1 to 247, or 0 to broadcast to every device.
1005/// * `address` - the register's address.
1006/// * `value` - the value to write.
1007/// * `out_error` - receives why the call failed, or null.
1008///
1009/// # Returns
1010///
1011/// As [`pamoja_modbus_client_read_coils`].
1012///
1013/// # Safety
1014///
1015/// As [`pamoja_modbus_client_write_single_coil`].
1016#[no_mangle]
1017pub unsafe extern "C" fn pamoja_modbus_client_write_single_register(
1018    client: *const PamojaModbusClient,
1019    unit: u8,
1020    address: u16,
1021    value: u16,
1022    out_error: *mut PamojaModbusClientError,
1023) -> PamojaStatus {
1024    status(transact(client, unit, out_error, |client| {
1025        client.write_single_register(unit, address, value)
1026    }))
1027}
1028
1029/// Writes a run of coils, function `0x0F`.
1030///
1031/// # Arguments
1032///
1033/// * `client` - the client.
1034/// * `unit` - the device, 1 to 247, or 0 to broadcast to every device.
1035/// * `start` - the first coil's address.
1036/// * `values` - one byte per coil, non-zero for on, 1 to 1968 of them.
1037/// * `len` - how many coils.
1038/// * `out_error` - receives why the call failed, or null.
1039///
1040/// # Returns
1041///
1042/// As [`pamoja_modbus_client_read_coils`].
1043///
1044/// # Safety
1045///
1046/// `client` must be a live handle or null, `values` must point to `len` readable bytes, and
1047/// `out_error` must be a writable pointer or null.
1048#[no_mangle]
1049pub unsafe extern "C" fn pamoja_modbus_client_write_multiple_coils(
1050    client: *const PamojaModbusClient,
1051    unit: u8,
1052    start: u16,
1053    values: *const u8,
1054    len: usize,
1055    out_error: *mut PamojaModbusClientError,
1056) -> PamojaStatus {
1057    let values: Vec<bool> = match read_values(values, len, "values") {
1058        Ok(values) => values.into_iter().map(|value| value != 0).collect(),
1059        Err(status) => return status,
1060    };
1061    status(transact(client, unit, out_error, |client| {
1062        client.write_multiple_coils(unit, start, &values)
1063    }))
1064}
1065
1066/// Writes a run of holding registers, function `0x10`.
1067///
1068/// # Arguments
1069///
1070/// * `client` - the client.
1071/// * `unit` - the device, 1 to 247, or 0 to broadcast to every device.
1072/// * `start` - the first register's address.
1073/// * `values` - the values, 1 to 123 of them, in address order.
1074/// * `len` - how many registers.
1075/// * `out_error` - receives why the call failed, or null.
1076///
1077/// # Returns
1078///
1079/// As [`pamoja_modbus_client_read_coils`].
1080///
1081/// # Safety
1082///
1083/// `client` must be a live handle or null, `values` must point to `len` readable values, and
1084/// `out_error` must be a writable pointer or null.
1085#[no_mangle]
1086pub unsafe extern "C" fn pamoja_modbus_client_write_multiple_registers(
1087    client: *const PamojaModbusClient,
1088    unit: u8,
1089    start: u16,
1090    values: *const u16,
1091    len: usize,
1092    out_error: *mut PamojaModbusClientError,
1093) -> PamojaStatus {
1094    let values = match read_values(values, len, "values") {
1095        Ok(values) => values,
1096        Err(status) => return status,
1097    };
1098    status(transact(client, unit, out_error, |client| {
1099        client.write_multiple_registers(unit, start, &values)
1100    }))
1101}
1102
1103fn status(result: Result<(), PamojaStatus>) -> PamojaStatus {
1104    result.err().unwrap_or(PamojaStatus::Ok)
1105}
1106
1107/// Releases a client. Its port stays open while another holder has it. A null pointer is
1108/// ignored.
1109///
1110/// # Safety
1111///
1112/// `client` must be a handle that has not been freed, or null.
1113#[no_mangle]
1114pub unsafe extern "C" fn pamoja_modbus_client_free(client: *mut PamojaModbusClient) {
1115    if !client.is_null() {
1116        drop(Box::from_raw(client));
1117    }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use super::*;
1123    use crate::port::{
1124        pamoja_serial_port_free, pamoja_serial_port_waited_micros, PAMOJA_PARITY_EVEN,
1125    };
1126    use std::ptr;
1127
1128    fn line_settings() -> PamojaSerialSettings {
1129        PamojaSerialSettings {
1130            baud: 19_200,
1131            parity: PAMOJA_PARITY_EVEN,
1132            stop_bits: 1,
1133        }
1134    }
1135
1136    #[test]
1137    fn a_client_polls_devices_on_a_simulated_line_through_the_c_abi() {
1138        unsafe {
1139            let mut meter = ptr::null_mut();
1140            assert_eq!(pamoja_modbus_server_new(17, &mut meter), PamojaStatus::Ok);
1141            let registers = [2301u16, 418, 0];
1142            assert_eq!(
1143                pamoja_modbus_server_set_holding_registers(meter, 107, registers.as_ptr(), 3),
1144                PamojaStatus::Ok
1145            );
1146            let line = pamoja_modbus_line_new();
1147            assert_eq!(pamoja_modbus_line_attach(line, meter), PamojaStatus::Ok);
1148            assert_eq!(pamoja_modbus_line_len(line), 1);
1149            let mut port = ptr::null_mut();
1150            assert_eq!(
1151                pamoja_modbus_line_port(line, line_settings(), &mut port),
1152                PamojaStatus::Ok
1153            );
1154            let mut client = ptr::null_mut();
1155            assert_eq!(
1156                pamoja_modbus_client_new(port, &mut client),
1157                PamojaStatus::Ok
1158            );
1159
1160            let mut values = [0u16; 3];
1161            let mut error = PamojaModbusClientError::default();
1162            assert_eq!(
1163                pamoja_modbus_client_read_holding_registers(
1164                    client,
1165                    17,
1166                    107,
1167                    3,
1168                    values.as_mut_ptr(),
1169                    &mut error
1170                ),
1171                PamojaStatus::Ok
1172            );
1173            assert_eq!(values, [2301, 418, 0]);
1174            assert_eq!(error.kind, PAMOJA_MODBUS_CLIENT_OK);
1175
1176            assert_eq!(
1177                pamoja_modbus_client_write_single_register(client, 17, 109, 5, &mut error),
1178                PamojaStatus::Ok
1179            );
1180            let mut value = 0u16;
1181            assert!(pamoja_modbus_server_holding_register(
1182                meter, 109, &mut value
1183            ));
1184            assert_eq!(value, 5);
1185            assert!(!pamoja_modbus_server_holding_register(
1186                meter, 110, &mut value
1187            ));
1188
1189            assert_eq!(
1190                pamoja_modbus_client_read_holding_registers(
1191                    client,
1192                    17,
1193                    108,
1194                    3,
1195                    values.as_mut_ptr(),
1196                    &mut error
1197                ),
1198                PamojaStatus::Other
1199            );
1200            assert_eq!(error.kind, PAMOJA_MODBUS_CLIENT_EXCEPTION);
1201            assert_eq!(
1202                (error.unit, error.function, error.exception),
1203                (17, 0x03, 0x02)
1204            );
1205
1206            pamoja_modbus_client_set_response_timeout(client, 250_000);
1207            assert_eq!(
1208                pamoja_modbus_client_response_timeout_micros(client),
1209                250_000
1210            );
1211            let before = pamoja_serial_port_waited_micros(port);
1212            assert_eq!(
1213                pamoja_modbus_client_read_holding_registers(
1214                    client,
1215                    18,
1216                    107,
1217                    1,
1218                    values.as_mut_ptr(),
1219                    &mut error
1220                ),
1221                PamojaStatus::Io
1222            );
1223            assert_eq!(error.kind, PAMOJA_MODBUS_CLIENT_TIMEOUT);
1224            assert_eq!(
1225                pamoja_serial_port_waited_micros(port) - before,
1226                2_005 + 250_000
1227            );
1228            assert_eq!(pamoja_modbus_server_served(meter), 2);
1229
1230            pamoja_modbus_client_free(client);
1231            pamoja_serial_port_free(port);
1232            pamoja_modbus_line_free(line);
1233            pamoja_modbus_server_free(meter);
1234        }
1235    }
1236
1237    #[test]
1238    fn a_device_answers_a_frame_on_its_own_and_stays_silent_for_another_unit() {
1239        unsafe {
1240            let mut meter = ptr::null_mut();
1241            assert_eq!(pamoja_modbus_server_new(17, &mut meter), PamojaStatus::Ok);
1242            let coils = [1u8, 0, 1];
1243            assert_eq!(
1244                pamoja_modbus_server_set_coils(meter, 0, coils.as_ptr(), 3),
1245                PamojaStatus::Ok
1246            );
1247            let request = pamoja_modbus::Pdu::read_coils(0, 3).to_adu(17);
1248            let mut reply = ptr::null_mut();
1249            assert_eq!(
1250                pamoja_modbus_server_answer(
1251                    meter,
1252                    request.as_bytes().as_ptr(),
1253                    request.as_bytes().len(),
1254                    &mut reply
1255                ),
1256                PamojaStatus::Ok
1257            );
1258            assert!(!reply.is_null());
1259            crate::pamoja_buffer_free(reply);
1260
1261            let other = pamoja_modbus::Pdu::read_coils(0, 3).to_adu(18);
1262            assert_eq!(
1263                pamoja_modbus_server_answer(
1264                    meter,
1265                    other.as_bytes().as_ptr(),
1266                    other.as_bytes().len(),
1267                    &mut reply
1268                ),
1269                PamojaStatus::Ok
1270            );
1271            assert!(reply.is_null());
1272            let mut on = false;
1273            assert!(pamoja_modbus_server_coil(meter, 2, &mut on));
1274            assert!(on);
1275
1276            let mut refused = ptr::null_mut();
1277            assert_eq!(
1278                pamoja_modbus_server_new(248, &mut refused),
1279                PamojaStatus::InvalidArgument
1280            );
1281            assert!(refused.is_null());
1282            pamoja_modbus_server_free(meter);
1283        }
1284    }
1285
1286    #[test]
1287    fn the_frame_gap_crosses_in_nanoseconds() {
1288        let mut nanos = 0;
1289        unsafe {
1290            assert_eq!(
1291                pamoja_modbus_frame_gap_nanos(line_settings(), &mut nanos),
1292                PamojaStatus::Ok
1293            );
1294        }
1295        assert_eq!(nanos, 2_005_210);
1296    }
1297}