#![cfg(all(not(target_arch = "wasm32"), feature = "c-server"))]
#![allow(clippy::missing_safety_doc)]
use core::ffi::c_void;
use std::sync::Mutex;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mbus_pool_lock() {}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mbus_pool_unlock() {}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mbus_server_lock(_id: u16) {}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mbus_server_unlock(_id: u16) {}
use mbus_ffi::c::error::MbusStatusCode;
#[cfg(feature = "coils")]
use mbus_ffi::c::server::callbacks::MbusServerReadCoilsReq;
#[cfg(feature = "holding-registers")]
use mbus_ffi::c::server::callbacks::MbusServerReadHoldingRegistersReq;
#[cfg(feature = "coils")]
use mbus_ffi::c::server::callbacks::MbusServerWriteSingleCoilReq;
use mbus_ffi::c::server::config::MbusServerConfig;
use mbus_ffi::c::server::tcp_server::{
mbus_tcp_server_connect, mbus_tcp_server_disconnect, mbus_tcp_server_free,
mbus_tcp_server_is_connected, mbus_tcp_server_new, mbus_tcp_server_poll,
};
use mbus_ffi::c::server::{MBUS_INVALID_SERVER_ID, MbusServerExceptionCode, MbusServerHandlers};
use mbus_ffi::c::transport::MbusTransportCallbacks;
static SENT_BYTES: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
static RECV_FRAME: Mutex<Option<Vec<u8>>> = Mutex::new(None);
static COIL_CB_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static WRITE_COIL_CB_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static TEST_SERIAL: Mutex<()> = Mutex::new(());
fn reset_test_state() {
SENT_BYTES.lock().unwrap_or_else(|e| e.into_inner()).clear();
*RECV_FRAME.lock().unwrap_or_else(|e| e.into_inner()) = None;
COIL_CB_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
WRITE_COIL_CB_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
}
unsafe extern "C" fn test_connect(_userdata: *mut c_void) -> MbusStatusCode {
MbusStatusCode::MbusOk
}
unsafe extern "C" fn test_disconnect(_userdata: *mut c_void) -> MbusStatusCode {
MbusStatusCode::MbusOk
}
unsafe extern "C" fn test_send(
data: *const u8,
len: u16,
_userdata: *mut c_void,
) -> MbusStatusCode {
let bytes = unsafe { core::slice::from_raw_parts(data, len as usize) };
SENT_BYTES
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(bytes.to_vec());
MbusStatusCode::MbusOk
}
unsafe extern "C" fn test_recv(
buffer: *mut u8,
buffer_cap: u16,
out_len: *mut u16,
_userdata: *mut c_void,
) -> MbusStatusCode {
let mut guard = RECV_FRAME.lock().unwrap_or_else(|e| e.into_inner());
match guard.take() {
Some(frame) => {
let copy_len = frame.len().min(buffer_cap as usize);
unsafe {
core::ptr::copy_nonoverlapping(frame.as_ptr(), buffer, copy_len);
*out_len = copy_len as u16;
}
MbusStatusCode::MbusOk
}
None => {
unsafe {
*out_len = 0;
}
MbusStatusCode::MbusOk
}
}
}
unsafe extern "C" fn test_is_connected(_userdata: *mut c_void) -> u8 {
1
}
fn make_transport_callbacks() -> MbusTransportCallbacks {
MbusTransportCallbacks {
userdata: core::ptr::null_mut(),
on_connect: Some(test_connect),
on_disconnect: Some(test_disconnect),
on_send: Some(test_send),
on_recv: Some(test_recv),
on_is_connected: Some(test_is_connected),
}
}
#[cfg(feature = "coils")]
unsafe extern "C" fn test_on_read_coils(
req: *mut MbusServerReadCoilsReq,
_userdata: *mut c_void,
) -> MbusServerExceptionCode {
COIL_CB_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let req = unsafe { &mut *req };
let needed = (req.quantity as usize).div_ceil(8);
let needed = needed.min(req.out_data_len);
if needed == 0 {
return MbusServerExceptionCode::ServerDeviceFailure;
}
let slice = unsafe { core::slice::from_raw_parts_mut(req.out_data, needed) };
slice.fill(0);
if needed > 0 {
slice[0] = 0b0000_0101;
}
req.out_byte_count = needed as u8;
MbusServerExceptionCode::Ok
}
#[cfg(feature = "coils")]
unsafe extern "C" fn test_on_write_single_coil(
_req: *const MbusServerWriteSingleCoilReq,
_userdata: *mut c_void,
) -> MbusServerExceptionCode {
WRITE_COIL_CB_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
MbusServerExceptionCode::Ok
}
#[cfg(feature = "holding-registers")]
unsafe extern "C" fn test_on_read_holding_registers(
req: *mut MbusServerReadHoldingRegistersReq,
_userdata: *mut c_void,
) -> MbusServerExceptionCode {
let req = unsafe { &mut *req };
let byte_count = (req.quantity as usize * 2).min(req.out_data_len);
if byte_count == 0 {
return MbusServerExceptionCode::ServerDeviceFailure;
}
let slice = unsafe { core::slice::from_raw_parts_mut(req.out_data, byte_count) };
for chunk in slice.chunks_mut(2) {
if chunk.len() == 2 {
chunk[0] = 0xDE;
chunk[1] = 0xAD;
}
}
req.out_byte_count = byte_count as u8;
MbusServerExceptionCode::Ok
}
fn make_all_null_handlers() -> MbusServerHandlers {
MbusServerHandlers {
userdata: core::ptr::null_mut(),
#[cfg(feature = "coils")]
on_read_coils: None,
#[cfg(feature = "coils")]
on_write_single_coil: None,
#[cfg(feature = "coils")]
on_write_multiple_coils: None,
#[cfg(feature = "discrete-inputs")]
on_read_discrete_inputs: None,
#[cfg(feature = "holding-registers")]
on_read_holding_registers: None,
#[cfg(feature = "holding-registers")]
on_write_single_register: None,
#[cfg(feature = "holding-registers")]
on_write_multiple_registers: None,
#[cfg(feature = "holding-registers")]
on_mask_write_register: None,
#[cfg(feature = "holding-registers")]
on_read_write_multiple_registers: None,
#[cfg(feature = "input-registers")]
on_read_input_registers: None,
#[cfg(feature = "fifo")]
on_read_fifo_queue: None,
#[cfg(feature = "file-record")]
on_read_file_record: None,
#[cfg(feature = "file-record")]
on_write_file_record: None,
#[cfg(feature = "diagnostics")]
on_read_exception_status: None,
#[cfg(feature = "diagnostics")]
on_diagnostics: None,
#[cfg(feature = "diagnostics")]
on_get_comm_event_counter: None,
#[cfg(feature = "diagnostics")]
on_get_comm_event_log: None,
#[cfg(feature = "diagnostics")]
on_report_server_id: None,
#[cfg(feature = "diagnostics")]
on_read_device_identification: None,
}
}
fn make_test_handlers() -> MbusServerHandlers {
MbusServerHandlers {
#[cfg(feature = "coils")]
on_read_coils: Some(test_on_read_coils),
#[cfg(feature = "coils")]
on_write_single_coil: Some(test_on_write_single_coil),
#[cfg(feature = "holding-registers")]
on_read_holding_registers: Some(test_on_read_holding_registers),
..make_all_null_handlers()
}
}
fn make_test_config() -> MbusServerConfig {
MbusServerConfig {
slave_address: 1,
response_timeout_ms: 1_000,
}
}
#[test]
fn tcp_server_new_null_transport_returns_invalid_id() {
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(core::ptr::null(), &handlers, &config) };
assert_eq!(
id, MBUS_INVALID_SERVER_ID,
"null transport should return MBUS_INVALID_SERVER_ID"
);
}
#[test]
fn tcp_server_new_null_handlers_returns_invalid_id() {
let transport = make_transport_callbacks();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, core::ptr::null(), &config) };
assert_eq!(
id, MBUS_INVALID_SERVER_ID,
"null handlers should return MBUS_INVALID_SERVER_ID"
);
}
#[test]
fn tcp_server_new_null_config_returns_invalid_id() {
let transport = make_transport_callbacks();
let handlers = make_test_handlers();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, core::ptr::null()) };
assert_eq!(
id, MBUS_INVALID_SERVER_ID,
"null config should return MBUS_INVALID_SERVER_ID"
);
}
#[test]
fn tcp_server_new_incomplete_transport_returns_invalid_id() {
let transport = MbusTransportCallbacks {
userdata: core::ptr::null_mut(),
on_connect: Some(test_connect),
on_disconnect: Some(test_disconnect),
on_send: Some(test_send),
on_recv: None,
on_is_connected: Some(test_is_connected),
};
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_eq!(
id, MBUS_INVALID_SERVER_ID,
"transport with missing callback should return MBUS_INVALID_SERVER_ID"
);
}
#[test]
fn tcp_server_new_and_free_succeeds() {
let _guard = TEST_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let transport = make_transport_callbacks();
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_ne!(id, MBUS_INVALID_SERVER_ID, "expected a valid server ID");
mbus_tcp_server_free(id);
mbus_tcp_server_free(id); }
#[test]
fn tcp_server_is_connected_after_connect() {
let _guard = TEST_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let transport = make_transport_callbacks();
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_ne!(id, MBUS_INVALID_SERVER_ID);
assert!(mbus_tcp_server_is_connected(id));
let status = mbus_tcp_server_connect(id);
assert_eq!(status, MbusStatusCode::MbusOk, "connect should succeed");
assert!(mbus_tcp_server_is_connected(id));
let status = mbus_tcp_server_disconnect(id);
assert_eq!(status, MbusStatusCode::MbusOk, "disconnect should succeed");
mbus_tcp_server_free(id);
}
#[test]
fn tcp_server_connect_with_invalid_id_returns_error() {
let status = mbus_tcp_server_connect(MBUS_INVALID_SERVER_ID);
assert_ne!(
status,
MbusStatusCode::MbusOk,
"invalid ID should not succeed"
);
}
#[test]
fn tcp_server_poll_with_invalid_id_returns_error() {
let status = mbus_tcp_server_poll(MBUS_INVALID_SERVER_ID);
assert_ne!(
status,
MbusStatusCode::MbusOk,
"invalid ID should not succeed"
);
}
fn build_tcp_frame(txn_id: u16, unit_id: u8, fc: u8, payload: &[u8]) -> Vec<u8> {
let pdu_len = 1 + payload.len();
let mbap_len = (1 + pdu_len) as u16;
let mut frame = vec![
(txn_id >> 8) as u8,
txn_id as u8,
0x00, 0x00,
(mbap_len >> 8) as u8,
mbap_len as u8,
unit_id,
fc,
];
frame.extend_from_slice(payload);
frame
}
fn build_fc01_request() -> Vec<u8> {
build_tcp_frame(1, 1, 0x01, &[0x00, 0x01, 0x00, 0x08])
}
fn build_fc03_request() -> Vec<u8> {
build_tcp_frame(2, 1, 0x03, &[0x00, 0x00, 0x00, 0x02])
}
fn build_fc05_request() -> Vec<u8> {
build_tcp_frame(3, 1, 0x05, &[0x00, 0x02, 0xFF, 0x00])
}
const MBAP_HEADER_LEN: usize = 7;
#[cfg(feature = "coils")]
#[test]
fn fc01_read_coils_dispatches_callback_and_returns_success_response() {
let _guard = TEST_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
reset_test_state();
*RECV_FRAME.lock().unwrap() = Some(build_fc01_request());
let transport = make_transport_callbacks();
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_ne!(id, MBUS_INVALID_SERVER_ID);
let status = mbus_tcp_server_poll(id);
assert_eq!(status, MbusStatusCode::MbusOk);
assert_eq!(
COIL_CB_CALLS.load(std::sync::atomic::Ordering::SeqCst),
1,
"on_read_coils should be called once"
);
let sent = SENT_BYTES.lock().unwrap();
assert_eq!(sent.len(), 1, "exactly one response frame expected");
let frame = &sent[0];
assert!(
frame.len() >= MBAP_HEADER_LEN + 2,
"response must contain MBAP(7) + FC(1) + byte_count(1) + data"
);
assert_eq!(frame[MBAP_HEADER_LEN], 0x01, "response FC should be 0x01");
let byte_count = frame[MBAP_HEADER_LEN + 1];
assert_eq!(byte_count, 1, "byte_count should be 1 for 8 coils");
assert_eq!(
frame[MBAP_HEADER_LEN + 2],
0b0000_0101,
"coil data byte should match test pattern"
);
mbus_tcp_server_free(id);
}
#[cfg(feature = "holding-registers")]
#[test]
fn fc03_read_holding_registers_dispatches_callback() {
let _guard = TEST_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
reset_test_state();
*RECV_FRAME.lock().unwrap() = Some(build_fc03_request());
let transport = make_transport_callbacks();
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_ne!(id, MBUS_INVALID_SERVER_ID);
let status = mbus_tcp_server_poll(id);
assert_eq!(status, MbusStatusCode::MbusOk);
let sent = SENT_BYTES.lock().unwrap();
assert_eq!(sent.len(), 1, "exactly one response frame expected");
let frame = &sent[0];
assert_eq!(frame[MBAP_HEADER_LEN], 0x03);
assert_eq!(
frame[MBAP_HEADER_LEN + 1],
4,
"byte_count should be 4 for 2 registers"
);
assert_eq!(
&frame[MBAP_HEADER_LEN + 2..MBAP_HEADER_LEN + 6],
&[0xDE, 0xAD, 0xDE, 0xAD]
);
mbus_tcp_server_free(id);
}
#[cfg(feature = "coils")]
#[test]
fn fc05_write_single_coil_dispatches_callback_and_echoes() {
let _guard = TEST_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
reset_test_state();
*RECV_FRAME.lock().unwrap() = Some(build_fc05_request());
let transport = make_transport_callbacks();
let handlers = make_test_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_ne!(id, MBUS_INVALID_SERVER_ID);
let status = mbus_tcp_server_poll(id);
assert_eq!(status, MbusStatusCode::MbusOk);
assert_eq!(
WRITE_COIL_CB_CALLS.load(std::sync::atomic::Ordering::SeqCst),
1,
"on_write_single_coil should be called once"
);
let sent = SENT_BYTES.lock().unwrap();
assert_eq!(sent.len(), 1);
let frame = &sent[0];
assert_eq!(frame[MBAP_HEADER_LEN], 0x05, "response FC should be 0x05");
mbus_tcp_server_free(id);
}
#[cfg(feature = "coils")]
#[test]
fn null_callback_slot_returns_illegal_function_exception() {
let _guard = TEST_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
reset_test_state();
*RECV_FRAME.lock().unwrap() = Some(build_fc01_request());
let transport = make_transport_callbacks();
let handlers = make_all_null_handlers();
let config = make_test_config();
let id = unsafe { mbus_tcp_server_new(&transport, &handlers, &config) };
assert_ne!(id, MBUS_INVALID_SERVER_ID);
let status = mbus_tcp_server_poll(id);
assert_eq!(status, MbusStatusCode::MbusOk);
assert_eq!(COIL_CB_CALLS.load(std::sync::atomic::Ordering::SeqCst), 0);
let sent = SENT_BYTES.lock().unwrap();
assert_eq!(sent.len(), 1, "exception response should be sent");
let frame = &sent[0];
assert!(frame.len() >= MBAP_HEADER_LEN + 2);
assert_eq!(frame[MBAP_HEADER_LEN], 0x81, "exception FC should be 0x81");
assert_eq!(
frame[MBAP_HEADER_LEN + 1],
0x01,
"exception code should be IllegalFunction"
);
mbus_tcp_server_free(id);
}