1use 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
25pub const PAMOJA_MODBUS_CLIENT_OK: u8 = 0;
27
28pub const PAMOJA_MODBUS_CLIENT_REQUEST: u8 = 1;
31
32pub const PAMOJA_MODBUS_CLIENT_BROADCAST_READ: u8 = 2;
34
35pub const PAMOJA_MODBUS_CLIENT_PORT: u8 = 3;
37
38pub const PAMOJA_MODBUS_CLIENT_TIMEOUT: u8 = 4;
40
41pub const PAMOJA_MODBUS_CLIENT_FRAME: u8 = 5;
43
44pub const PAMOJA_MODBUS_CLIENT_WRONG_UNIT: u8 = 6;
46
47pub const PAMOJA_MODBUS_CLIENT_WRONG_FUNCTION: u8 = 7;
49
50pub const PAMOJA_MODBUS_CLIENT_MISMATCH: u8 = 8;
52
53pub const PAMOJA_MODBUS_CLIENT_EXCEPTION: u8 = 9;
55
56#[repr(C)]
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub struct PamojaModbusClientError {
60 pub kind: u8,
62 pub unit: u8,
64 pub function: u8,
66 pub found: u8,
68 pub exception: u8,
70 pub received: u32,
72}
73
74pub struct PamojaModbusServer {
77 server: Arc<Mutex<Server>>,
78}
79
80pub struct PamojaModbusLine {
83 line: Arc<Mutex<Line>>,
84}
85
86pub 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#[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
130unsafe 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#[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#[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#[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#[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
273unsafe 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
717unsafe 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
792unsafe 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#[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#[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#[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#[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#[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#[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#[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#[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#[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}