Skip to main content

pamoja_ffi/
modbus.rs

1//! The C ABI for Modbus RTU framing.
2//!
3//! These functions wrap [`pamoja_modbus`] for callers that reach the SDK through
4//! the flat C boundary. Each request builder produces a complete RTU frame, CRC
5//! included, ready to put on an RS485 line: the PDU and the frame around it are
6//! one step here rather than two, because a caller crossing this boundary has no
7//! use for a PDU on its own.
8//!
9//! A received frame goes the other way through [`pamoja_modbus_frame_parse`],
10//! which validates the CRC before anything else can be read from it. The values a
11//! device returned then come out as typed series, registers as `uint16` and coils
12//! as one byte per bit, rather than as bytes the caller would have to unpack.
13
14use std::panic::{catch_unwind, AssertUnwindSafe};
15use std::ptr;
16
17use pamoja_modbus::{crc16, Adu, ModbusError, Pdu};
18
19use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
20
21/// An opaque handle to a parsed Modbus RTU frame with a verified CRC.
22///
23/// Read it with the `pamoja_modbus_frame_*` calls, then release it with
24/// [`pamoja_modbus_frame_free`].
25pub struct PamojaModbusFrame {
26    adu: Adu,
27}
28
29/// An opaque handle to the 16-bit registers a device returned.
30///
31/// Read it with [`pamoja_registers_data`] and [`pamoja_registers_len`], then
32/// release it with [`pamoja_registers_free`].
33pub struct PamojaRegisters {
34    registers: Vec<u16>,
35}
36
37/// Computes the CRC-16/MODBUS that every RTU frame ends with.
38///
39/// # Returns
40///
41/// The checksum over `bytes`, or the checksum of an empty input when `bytes` is
42/// null and `bytes_len` is 0.
43///
44/// # Safety
45///
46/// `bytes` must point to at least `bytes_len` readable bytes, or be null when
47/// `bytes_len` is 0.
48#[no_mangle]
49pub unsafe extern "C" fn pamoja_modbus_crc16(bytes: *const u8, bytes_len: usize) -> u16 {
50    match read_bytes(bytes, bytes_len) {
51        Ok(bytes) => crc16(&bytes),
52        Err(_) => 0,
53    }
54}
55
56/// Builds a read-coils request frame (function `0x01`).
57///
58/// # Returns
59///
60/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
61/// holding the frame, which the caller must release with
62/// [`pamoja_buffer_free`](crate::pamoja_buffer_free).
63///
64/// # Safety
65///
66/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
67#[no_mangle]
68pub unsafe extern "C" fn pamoja_modbus_read_coils(
69    address: u8,
70    start: u16,
71    count: u16,
72    out_buffer: *mut *mut PamojaBuffer,
73) -> PamojaStatus {
74    let out_buffer = match out_slot(out_buffer, "out_buffer") {
75        Ok(slot) => slot,
76        Err(status) => return status,
77    };
78    request(out_buffer, address, || Ok(Pdu::read_coils(start, count)))
79}
80
81/// Builds a read-discrete-inputs request frame (function `0x02`).
82///
83/// # Returns
84///
85/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
86/// holding the frame.
87///
88/// # Safety
89///
90/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
91#[no_mangle]
92pub unsafe extern "C" fn pamoja_modbus_read_discrete_inputs(
93    address: u8,
94    start: u16,
95    count: u16,
96    out_buffer: *mut *mut PamojaBuffer,
97) -> PamojaStatus {
98    let out_buffer = match out_slot(out_buffer, "out_buffer") {
99        Ok(slot) => slot,
100        Err(status) => return status,
101    };
102    request(out_buffer, address, || {
103        Ok(Pdu::read_discrete_inputs(start, count))
104    })
105}
106
107/// Builds a read-holding-registers request frame (function `0x03`).
108///
109/// # Returns
110///
111/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
112/// holding the frame.
113///
114/// # Safety
115///
116/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
117#[no_mangle]
118pub unsafe extern "C" fn pamoja_modbus_read_holding_registers(
119    address: u8,
120    start: u16,
121    count: u16,
122    out_buffer: *mut *mut PamojaBuffer,
123) -> PamojaStatus {
124    let out_buffer = match out_slot(out_buffer, "out_buffer") {
125        Ok(slot) => slot,
126        Err(status) => return status,
127    };
128    request(out_buffer, address, || {
129        Ok(Pdu::read_holding_registers(start, count))
130    })
131}
132
133/// Builds the reply a device sends to a read-holding-registers request.
134///
135/// # Returns
136///
137/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
138/// holding the frame.
139///
140/// # Safety
141///
142/// `values` must point to at least `values_len` readable `uint16_t`, and `out_buffer`
143/// to a writable `*mut PamojaBuffer`.
144#[no_mangle]
145pub unsafe extern "C" fn pamoja_modbus_read_holding_registers_reply(
146    address: u8,
147    values: *const u16,
148    values_len: usize,
149    out_buffer: *mut *mut PamojaBuffer,
150) -> PamojaStatus {
151    registers_reply(address, values, values_len, out_buffer, true)
152}
153
154/// Builds the reply a device sends to a read-input-registers request.
155///
156/// # Returns
157///
158/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
159/// holding the frame.
160///
161/// # Safety
162///
163/// `values` must point to at least `values_len` readable `uint16_t`, and `out_buffer`
164/// to a writable `*mut PamojaBuffer`.
165#[no_mangle]
166pub unsafe extern "C" fn pamoja_modbus_read_input_registers_reply(
167    address: u8,
168    values: *const u16,
169    values_len: usize,
170    out_buffer: *mut *mut PamojaBuffer,
171) -> PamojaStatus {
172    registers_reply(address, values, values_len, out_buffer, false)
173}
174
175unsafe fn registers_reply(
176    address: u8,
177    values: *const u16,
178    values_len: usize,
179    out_buffer: *mut *mut PamojaBuffer,
180    holding: bool,
181) -> PamojaStatus {
182    let out_buffer = match out_slot(out_buffer, "out_buffer") {
183        Ok(slot) => slot,
184        Err(status) => return status,
185    };
186    let values = match read_values(values, values_len, "values") {
187        Ok(values) => values,
188        Err(status) => return status,
189    };
190    request(out_buffer, address, || {
191        if holding {
192            Pdu::read_holding_registers_reply(&values)
193        } else {
194            Pdu::read_input_registers_reply(&values)
195        }
196    })
197}
198
199/// Builds a read-input-registers request frame (function `0x04`).
200///
201/// # Returns
202///
203/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
204/// holding the frame.
205///
206/// # Safety
207///
208/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
209#[no_mangle]
210pub unsafe extern "C" fn pamoja_modbus_read_input_registers(
211    address: u8,
212    start: u16,
213    count: u16,
214    out_buffer: *mut *mut PamojaBuffer,
215) -> PamojaStatus {
216    let out_buffer = match out_slot(out_buffer, "out_buffer") {
217        Ok(slot) => slot,
218        Err(status) => return status,
219    };
220    request(out_buffer, address, || {
221        Ok(Pdu::read_input_registers(start, count))
222    })
223}
224
225/// Builds a write-single-coil request frame (function `0x05`).
226///
227/// # Returns
228///
229/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
230/// holding the frame.
231///
232/// # Safety
233///
234/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
235#[no_mangle]
236pub unsafe extern "C" fn pamoja_modbus_write_single_coil(
237    address: u8,
238    coil: u16,
239    on: bool,
240    out_buffer: *mut *mut PamojaBuffer,
241) -> PamojaStatus {
242    let out_buffer = match out_slot(out_buffer, "out_buffer") {
243        Ok(slot) => slot,
244        Err(status) => return status,
245    };
246    request(out_buffer, address, || Ok(Pdu::write_single_coil(coil, on)))
247}
248
249/// Builds a write-single-register request frame (function `0x06`).
250///
251/// # Returns
252///
253/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
254/// holding the frame.
255///
256/// # Safety
257///
258/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
259#[no_mangle]
260pub unsafe extern "C" fn pamoja_modbus_write_single_register(
261    address: u8,
262    register: u16,
263    value: u16,
264    out_buffer: *mut *mut PamojaBuffer,
265) -> PamojaStatus {
266    let out_buffer = match out_slot(out_buffer, "out_buffer") {
267        Ok(slot) => slot,
268        Err(status) => return status,
269    };
270    request(out_buffer, address, || {
271        Ok(Pdu::write_single_register(register, value))
272    })
273}
274
275/// Builds a write-multiple-registers request frame (function `0x10`).
276///
277/// # Returns
278///
279/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
280/// holding the frame, or [`PamojaStatus::InvalidArgument`] if `count` is zero or
281/// beyond what one request may carry.
282///
283/// # Safety
284///
285/// `values` must point to at least `count` readable `uint16` values, or be null
286/// when `count` is 0, and `out_buffer` must point to a writable
287/// `*mut PamojaBuffer`.
288#[no_mangle]
289pub unsafe extern "C" fn pamoja_modbus_write_multiple_registers(
290    address: u8,
291    start: u16,
292    values: *const u16,
293    count: usize,
294    out_buffer: *mut *mut PamojaBuffer,
295) -> PamojaStatus {
296    let out_buffer = match out_slot(out_buffer, "out_buffer") {
297        Ok(slot) => slot,
298        Err(status) => return status,
299    };
300    let values = match read_values(values, count, "values") {
301        Ok(values) => values,
302        Err(status) => return status,
303    };
304    request(out_buffer, address, || {
305        Pdu::write_multiple_registers(start, &values)
306    })
307}
308
309/// Builds a write-multiple-coils request frame (function `0x0F`).
310///
311/// # Returns
312///
313/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
314/// holding the frame, or [`PamojaStatus::InvalidArgument`] if `count` is zero or
315/// beyond what one request may carry.
316///
317/// # Safety
318///
319/// `values` must point to at least `count` readable bytes, one per coil and
320/// non-zero for on, or be null when `count` is 0, and `out_buffer` must point to
321/// a writable `*mut PamojaBuffer`.
322#[no_mangle]
323pub unsafe extern "C" fn pamoja_modbus_write_multiple_coils(
324    address: u8,
325    start: u16,
326    values: *const u8,
327    count: usize,
328    out_buffer: *mut *mut PamojaBuffer,
329) -> PamojaStatus {
330    let out_buffer = match out_slot(out_buffer, "out_buffer") {
331        Ok(slot) => slot,
332        Err(status) => return status,
333    };
334    let values = match read_values(values, count, "values") {
335        Ok(values) => values,
336        Err(status) => return status,
337    };
338    let coils: Vec<bool> = values.into_iter().map(|value| value != 0).collect();
339    request(out_buffer, address, || {
340        Pdu::write_multiple_coils(start, &coils)
341    })
342}
343
344/// Builds a request frame from a raw function code and data.
345///
346/// This is the escape hatch for the function codes the SDK does not name.
347///
348/// # Returns
349///
350/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
351/// holding the frame, or [`PamojaStatus::InvalidArgument`] if the data is longer
352/// than a PDU may be.
353///
354/// # Safety
355///
356/// `data` must point to at least `data_len` readable bytes, or be null when
357/// `data_len` is 0, and `out_buffer` must point to a writable
358/// `*mut PamojaBuffer`.
359#[no_mangle]
360pub unsafe extern "C" fn pamoja_modbus_raw(
361    address: u8,
362    function: u8,
363    data: *const u8,
364    data_len: usize,
365    out_buffer: *mut *mut PamojaBuffer,
366) -> PamojaStatus {
367    let out_buffer = match out_slot(out_buffer, "out_buffer") {
368        Ok(slot) => slot,
369        Err(status) => return status,
370    };
371    let data = match read_bytes(data, data_len) {
372        Ok(data) => data,
373        Err(status) => return status,
374    };
375    request(out_buffer, address, || Pdu::raw(function, &data))
376}
377
378/// Parses a received RTU frame, verifying its CRC.
379///
380/// # Returns
381///
382/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a new handle the
383/// caller must release with [`pamoja_modbus_frame_free`], or
384/// [`PamojaStatus::Codec`] if the frame is truncated, oversized, or its CRC does
385/// not match its contents.
386///
387/// # Safety
388///
389/// `bytes` must point to at least `bytes_len` readable bytes, or be null when
390/// `bytes_len` is 0, and `out_frame` must point to a writable
391/// `*mut PamojaModbusFrame`.
392#[no_mangle]
393pub unsafe extern "C" fn pamoja_modbus_frame_parse(
394    bytes: *const u8,
395    bytes_len: usize,
396    out_frame: *mut *mut PamojaModbusFrame,
397) -> PamojaStatus {
398    let out_frame = match out_slot(out_frame, "out_frame") {
399        Ok(slot) => slot,
400        Err(status) => return status,
401    };
402    let bytes = match read_bytes(bytes, bytes_len) {
403        Ok(bytes) => bytes,
404        Err(status) => return status,
405    };
406    match catch_unwind(AssertUnwindSafe(|| Adu::parse(&bytes))) {
407        Ok(Ok(adu)) => {
408            *out_frame = Box::into_raw(Box::new(PamojaModbusFrame { adu }));
409            PamojaStatus::Ok
410        }
411        Ok(Err(error)) => failed(error),
412        Err(_) => panicked(),
413    }
414}
415
416/// Returns the unit address a frame is addressed to or came from.
417///
418/// # Returns
419///
420/// The address, or 0 if `frame` is null.
421///
422/// # Safety
423///
424/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`], or null.
425#[no_mangle]
426pub unsafe extern "C" fn pamoja_modbus_frame_address(frame: *const PamojaModbusFrame) -> u8 {
427    if frame.is_null() {
428        return 0;
429    }
430    (*frame).adu.address()
431}
432
433/// Returns a frame's function code.
434///
435/// An exception response carries the request's function code with its high bit
436/// set, so this returns that byte as it appeared on the wire.
437///
438/// # Returns
439///
440/// The function code, or 0 if `frame` is null.
441///
442/// # Safety
443///
444/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`], or null.
445#[no_mangle]
446pub unsafe extern "C" fn pamoja_modbus_frame_function(frame: *const PamojaModbusFrame) -> u8 {
447    if frame.is_null() {
448        return 0;
449    }
450    (*frame).adu.function_code()
451}
452
453/// Returns the exception code a device reported.
454///
455/// # Returns
456///
457/// The exception code, or 0 when the frame is not an exception response, is not
458/// one this SDK names, or `frame` is null. Zero is not a defined exception code,
459/// so it is unambiguous.
460///
461/// # Safety
462///
463/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`], or null.
464#[no_mangle]
465pub unsafe extern "C" fn pamoja_modbus_frame_exception(frame: *const PamojaModbusFrame) -> u8 {
466    if frame.is_null() {
467        return 0;
468    }
469    (*frame)
470        .adu
471        .exception()
472        .map_or(0, pamoja_modbus::Exception::code)
473}
474
475/// Returns a pointer to a frame's PDU: the function code and its data, without
476/// the address or the CRC.
477///
478/// Use [`pamoja_modbus_frame_pdu_len`] for the length. The pointer is valid until
479/// the handle is freed.
480///
481/// # Returns
482///
483/// A pointer to the PDU, or null if `frame` is null.
484///
485/// # Safety
486///
487/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`], or null.
488#[no_mangle]
489pub unsafe extern "C" fn pamoja_modbus_frame_pdu(frame: *const PamojaModbusFrame) -> *const u8 {
490    if frame.is_null() {
491        return ptr::null();
492    }
493    (*frame).adu.pdu().as_ptr()
494}
495
496/// Returns the length in bytes of a frame's PDU.
497///
498/// # Returns
499///
500/// The length, or 0 if `frame` is null.
501///
502/// # Safety
503///
504/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`], or null.
505#[no_mangle]
506pub unsafe extern "C" fn pamoja_modbus_frame_pdu_len(frame: *const PamojaModbusFrame) -> usize {
507    if frame.is_null() {
508        return 0;
509    }
510    (*frame).adu.pdu().len()
511}
512
513/// Reads the 16-bit registers out of a read-registers response.
514///
515/// # Returns
516///
517/// [`PamojaStatus::Ok`] on success, with `*out_registers` set to a new handle the
518/// caller must release with [`pamoja_registers_free`], or
519/// [`PamojaStatus::Codec`] if the response is not a well-formed read-registers
520/// reply.
521///
522/// # Safety
523///
524/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`] or null, and
525/// `out_registers` must point to a writable `*mut PamojaRegisters`.
526#[no_mangle]
527pub unsafe extern "C" fn pamoja_modbus_frame_registers(
528    frame: *const PamojaModbusFrame,
529    out_registers: *mut *mut PamojaRegisters,
530) -> PamojaStatus {
531    let out_registers = match out_slot(out_registers, "out_registers") {
532        Ok(slot) => slot,
533        Err(status) => return status,
534    };
535    if frame.is_null() {
536        set_last_error("frame must not be null".to_owned());
537        return PamojaStatus::InvalidArgument;
538    }
539    let adu = (*frame).adu;
540    match catch_unwind(AssertUnwindSafe(|| {
541        adu.response()
542            .registers()
543            .map(|registers| registers.collect::<Vec<u16>>())
544    })) {
545        Ok(Ok(registers)) => {
546            *out_registers = Box::into_raw(Box::new(PamojaRegisters { registers }));
547            PamojaStatus::Ok
548        }
549        Ok(Err(error)) => failed(error),
550        Err(_) => panicked(),
551    }
552}
553
554/// Reads the coils or discrete inputs out of a read-bits response.
555///
556/// # Returns
557///
558/// [`PamojaStatus::Ok`] on success, with `*out_buffer` set to a new buffer handle
559/// holding one byte per coil, `1` for on and `0` for off, which the caller must
560/// release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
561/// [`PamojaStatus::Codec`] if the response does not carry `count` bits.
562///
563/// # Safety
564///
565/// `frame` must be a live handle from [`pamoja_modbus_frame_parse`] or null, and
566/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
567#[no_mangle]
568pub unsafe extern "C" fn pamoja_modbus_frame_coils(
569    frame: *const PamojaModbusFrame,
570    count: u16,
571    out_buffer: *mut *mut PamojaBuffer,
572) -> PamojaStatus {
573    let out_buffer = match out_slot(out_buffer, "out_buffer") {
574        Ok(slot) => slot,
575        Err(status) => return status,
576    };
577    if frame.is_null() {
578        set_last_error("frame must not be null".to_owned());
579        return PamojaStatus::InvalidArgument;
580    }
581    let adu = (*frame).adu;
582    match catch_unwind(AssertUnwindSafe(|| {
583        adu.response()
584            .coils(count)
585            .map(|coils| coils.map(u8::from).collect::<Vec<u8>>())
586    })) {
587        Ok(Ok(coils)) => {
588            *out_buffer = PamojaBuffer::into_raw(coils);
589            PamojaStatus::Ok
590        }
591        Ok(Err(error)) => failed(error),
592        Err(_) => panicked(),
593    }
594}
595
596/// Releases a parsed frame handle.
597///
598/// Passing null is a no-op.
599///
600/// # Safety
601///
602/// `frame` must be a handle from [`pamoja_modbus_frame_parse`] that has not
603/// already been freed, or null. After this call it must not be used again.
604#[no_mangle]
605pub unsafe extern "C" fn pamoja_modbus_frame_free(frame: *mut PamojaModbusFrame) {
606    if !frame.is_null() {
607        drop(Box::from_raw(frame));
608    }
609}
610
611/// Returns a pointer to the registers a device returned.
612///
613/// Use [`pamoja_registers_len`] for the count. The pointer is valid until the
614/// handle is freed.
615///
616/// # Returns
617///
618/// A pointer to the registers, or null if `registers` is null.
619///
620/// # Safety
621///
622/// `registers` must be a live handle from [`pamoja_modbus_frame_registers`], or
623/// null.
624#[no_mangle]
625pub unsafe extern "C" fn pamoja_registers_data(registers: *const PamojaRegisters) -> *const u16 {
626    if registers.is_null() {
627        return ptr::null();
628    }
629    (*registers).registers.as_ptr()
630}
631
632/// Returns how many registers a device returned.
633///
634/// # Returns
635///
636/// The count, or 0 if `registers` is null.
637///
638/// # Safety
639///
640/// `registers` must be a live handle from [`pamoja_modbus_frame_registers`], or
641/// null.
642#[no_mangle]
643pub unsafe extern "C" fn pamoja_registers_len(registers: *const PamojaRegisters) -> usize {
644    if registers.is_null() {
645        return 0;
646    }
647    (*registers).registers.len()
648}
649
650/// Releases a register series.
651///
652/// Passing null is a no-op.
653///
654/// # Safety
655///
656/// `registers` must be a handle from [`pamoja_modbus_frame_registers`] that has
657/// not already been freed, or null. After this call it must not be used again.
658#[no_mangle]
659pub unsafe extern "C" fn pamoja_registers_free(registers: *mut PamojaRegisters) {
660    if !registers.is_null() {
661        drop(Box::from_raw(registers));
662    }
663}
664
665/// Builds a PDU, wraps it in a frame for `address`, and hands back the bytes.
666///
667/// # Safety
668///
669/// `out_buffer` must point to a writable `*mut PamojaBuffer`.
670unsafe fn request(
671    out_buffer: &mut *mut PamojaBuffer,
672    address: u8,
673    build: impl FnOnce() -> Result<Pdu, ModbusError>,
674) -> PamojaStatus {
675    match catch_unwind(AssertUnwindSafe(|| {
676        build().map(|pdu| pdu.to_adu(address).as_bytes().to_vec())
677    })) {
678        Ok(Ok(bytes)) => {
679            *out_buffer = PamojaBuffer::into_raw(bytes);
680            PamojaStatus::Ok
681        }
682        Ok(Err(error)) => failed(error),
683        Err(_) => panicked(),
684    }
685}
686
687/// Rejects a null out-pointer and borrows the slot it names, cleared.
688///
689/// # Safety
690///
691/// `out` must be null or point to a writable `*mut T` that outlives the call.
692unsafe fn out_slot<'a, T>(out: *mut *mut T, name: &str) -> Result<&'a mut *mut T, PamojaStatus> {
693    if out.is_null() {
694        set_last_error(format!("{name} must not be null"));
695        return Err(PamojaStatus::InvalidArgument);
696    }
697    let slot = &mut *out;
698    *slot = ptr::null_mut();
699    Ok(slot)
700}
701
702/// Copies a borrowed array of `count` values, treating a zero count as empty.
703///
704/// # Safety
705///
706/// When `count` is non-zero, `ptr` must point to at least `count` readable `T`
707/// values.
708unsafe fn read_values<T: Copy>(
709    ptr: *const T,
710    count: usize,
711    name: &str,
712) -> Result<Vec<T>, PamojaStatus> {
713    if count == 0 {
714        Ok(Vec::new())
715    } else if ptr.is_null() {
716        set_last_error(format!(
717            "{name} must not be null when its count is non-zero"
718        ));
719        Err(PamojaStatus::InvalidArgument)
720    } else {
721        Ok(std::slice::from_raw_parts(ptr, count).to_vec())
722    }
723}
724
725/// Records a Modbus error and maps it onto its status.
726fn failed(error: ModbusError) -> PamojaStatus {
727    set_last_error(error.to_string());
728    match error {
729        ModbusError::InvalidValueCount => PamojaStatus::InvalidArgument,
730        ModbusError::FrameTooShort
731        | ModbusError::FrameTooLong
732        | ModbusError::CrcMismatch { .. }
733        | ModbusError::MalformedResponse => PamojaStatus::Codec,
734    }
735}
736
737/// Records a caught panic and reports it as [`PamojaStatus::Panic`].
738fn panicked() -> PamojaStatus {
739    set_last_error("panic at the FFI boundary".to_owned());
740    PamojaStatus::Panic
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
747
748    /// Copies a buffer handle's bytes out and releases the handle.
749    ///
750    /// # Safety
751    ///
752    /// `buffer` must be a live handle that has not already been freed.
753    unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
754        let bytes =
755            std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
756                .to_vec();
757        pamoja_buffer_free(buffer);
758        bytes
759    }
760
761    #[test]
762    fn a_read_request_matches_the_specification_example() {
763        let mut out = ptr::null_mut();
764        // Safety: the out-pointer is writable.
765        let frame = unsafe {
766            assert_eq!(
767                pamoja_modbus_read_holding_registers(0x11, 0x006B, 3, &mut out),
768                PamojaStatus::Ok
769            );
770            take(out)
771        };
772        assert_eq!(
773            frame,
774            vec![0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87],
775            "the frame carries the address, the PDU, and the CRC"
776        );
777    }
778
779    #[test]
780    fn a_reply_parses_into_its_registers() {
781        let on_wire = Adu::from_pdu(0x11, &[0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64])
782            .expect("assemble");
783        let bytes = on_wire.as_bytes();
784        let mut frame = ptr::null_mut();
785        let mut registers = ptr::null_mut();
786
787        // Safety: the input is a valid slice and the out-pointers are writable.
788        unsafe {
789            assert_eq!(
790                pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
791                PamojaStatus::Ok
792            );
793            assert_eq!(pamoja_modbus_frame_address(frame), 0x11);
794            assert_eq!(pamoja_modbus_frame_function(frame), 0x03);
795            assert_eq!(pamoja_modbus_frame_exception(frame), 0);
796            assert_eq!(
797                pamoja_modbus_frame_registers(frame, &mut registers),
798                PamojaStatus::Ok
799            );
800            let values = std::slice::from_raw_parts(
801                pamoja_registers_data(registers),
802                pamoja_registers_len(registers),
803            )
804            .to_vec();
805            pamoja_registers_free(registers);
806            pamoja_modbus_frame_free(frame);
807            assert_eq!(values, vec![0x022B, 0x0000, 0x0064]);
808        }
809    }
810
811    #[test]
812    fn a_bit_reply_unpacks_one_byte_per_coil() {
813        // A read-coils reply carrying the bits 1,0,1,1 in one byte.
814        let on_wire = Adu::from_pdu(0x11, &[0x01, 0x01, 0b0000_1101]).expect("assemble");
815        let bytes = on_wire.as_bytes();
816        let mut frame = ptr::null_mut();
817        let mut coils = ptr::null_mut();
818
819        // Safety: the input is a valid slice and the out-pointers are writable.
820        unsafe {
821            assert_eq!(
822                pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
823                PamojaStatus::Ok
824            );
825            assert_eq!(
826                pamoja_modbus_frame_coils(frame, 4, &mut coils),
827                PamojaStatus::Ok
828            );
829            let values = take(coils);
830            pamoja_modbus_frame_free(frame);
831            assert_eq!(values, vec![1, 0, 1, 1]);
832        }
833    }
834
835    #[test]
836    fn a_corrupt_frame_is_refused() {
837        let mut bytes = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03])
838            .expect("assemble")
839            .as_bytes()
840            .to_vec();
841        bytes[2] ^= 0xFF;
842        let mut frame = ptr::null_mut();
843
844        // Safety: the input is a valid slice and the out-pointer is writable.
845        let status = unsafe { pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame) };
846        assert_eq!(
847            status,
848            PamojaStatus::Codec,
849            "a frame mangled on the wire must not reach the application"
850        );
851        assert!(frame.is_null());
852    }
853
854    #[test]
855    fn an_exception_reply_reports_its_code() {
856        // Function 0x03 with the high bit set, then exception 0x02.
857        let on_wire = Adu::from_pdu(0x11, &[0x83, 0x02]).expect("assemble");
858        let bytes = on_wire.as_bytes();
859        let mut frame = ptr::null_mut();
860
861        // Safety: the input is a valid slice and the out-pointer is writable.
862        unsafe {
863            assert_eq!(
864                pamoja_modbus_frame_parse(bytes.as_ptr(), bytes.len(), &mut frame),
865                PamojaStatus::Ok
866            );
867            assert_eq!(pamoja_modbus_frame_exception(frame), 0x02);
868            assert_eq!(pamoja_modbus_frame_pdu_len(frame), 2);
869            pamoja_modbus_frame_free(frame);
870        }
871    }
872
873    #[test]
874    fn an_empty_write_is_rejected() {
875        let mut out = ptr::null_mut();
876        // Safety: a null values pointer is allowed when the count is zero.
877        let status =
878            unsafe { pamoja_modbus_write_multiple_registers(0x11, 0, ptr::null(), 0, &mut out) };
879        assert_eq!(status, PamojaStatus::InvalidArgument);
880        assert!(out.is_null());
881    }
882
883    #[test]
884    fn the_checksum_matches_the_frame_it_ends() {
885        let frame = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03]).expect("assemble");
886        let bytes = frame.as_bytes();
887        let split = bytes.len() - 2;
888        // Safety: the input is a valid slice.
889        let computed = unsafe { pamoja_modbus_crc16(bytes.as_ptr(), split) };
890        assert_eq!(computed.to_le_bytes(), bytes[split..]);
891    }
892}