#[cfg(feature = "coils")]
pub mod coil;
#[cfg(feature = "diagnostics")]
pub mod diagnostic;
#[cfg(feature = "discrete-inputs")]
pub mod discrete_input;
#[cfg(feature = "fifo")]
pub mod fifo_queue;
#[cfg(feature = "file-record")]
pub mod file_record;
#[cfg(feature = "registers")]
pub mod register;
use crate::app::RequestErrorNotifier;
#[cfg(feature = "diagnostics")]
use diagnostic::ReadDeviceIdCode;
use heapless::Vec;
use mbus_core::data_unit::common::{ModbusMessage, SlaveAddress, derive_length_from_bytes};
use mbus_core::function_codes::public::EncapsulatedInterfaceType;
use mbus_core::transport::{UidSaddrFrom, UnitIdOrSlaveAddr};
use mbus_core::{
data_unit::common::{self, MAX_ADU_FRAME_LEN},
errors::MbusError,
transport::{ModbusConfig, ModbusSerialConfig, TimeKeeper, Transport, TransportType},
};
#[cfg(feature = "logging")]
macro_rules! client_log_debug {
($($arg:tt)*) => {
log::debug!($($arg)*)
};
}
#[cfg(not(feature = "logging"))]
macro_rules! client_log_debug {
($($arg:tt)*) => {{
let _ = core::format_args!($($arg)*);
}};
}
#[cfg(feature = "logging")]
macro_rules! client_log_trace {
($($arg:tt)*) => {
log::trace!($($arg)*)
};
}
#[cfg(not(feature = "logging"))]
macro_rules! client_log_trace {
($($arg:tt)*) => {{
let _ = core::format_args!($($arg)*);
}};
}
type ResponseHandler<T, A, const N: usize> =
fn(&mut ClientServices<T, A, N>, &ExpectedResponse<T, A, N>, &ModbusMessage);
#[doc(hidden)]
pub trait SerialQueueSizeOne {}
impl SerialQueueSizeOne for [(); 1] {}
pub type SerialClientServices<TRANSPORT, APP> = ClientServices<TRANSPORT, APP, 1>;
#[cfg(feature = "coils")]
pub struct CoilsApi<'a, TRANSPORT, APP, const N: usize> {
client: &'a mut ClientServices<TRANSPORT, APP, N>,
}
#[cfg(feature = "coils")]
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::CoilResponse,
{
pub fn coils(&mut self) -> CoilsApi<'_, TRANSPORT, APP, N> {
CoilsApi { client: self }
}
pub fn with_coils<R>(
&mut self,
f: impl FnOnce(&mut CoilsApi<'_, TRANSPORT, APP, N>) -> R,
) -> R {
let mut api = self.coils();
f(&mut api)
}
}
#[cfg(feature = "coils")]
impl<TRANSPORT, APP, const N: usize> CoilsApi<'_, TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::CoilResponse,
{
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_multiple_coils(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
) -> Result<(), MbusError> {
self.client
.read_multiple_coils(txn_id, unit_id_slave_addr, address, quantity)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_single_coil(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
) -> Result<(), MbusError> {
self.client
.read_single_coil(txn_id, unit_id_slave_addr, address)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_single_coil(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: bool,
) -> Result<(), MbusError> {
self.client
.write_single_coil(txn_id, unit_id_slave_addr, address, value)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_multiple_coils(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
values: &crate::services::coil::Coils,
) -> Result<(), MbusError> {
self.client
.write_multiple_coils(txn_id, unit_id_slave_addr, address, values)
}
}
#[cfg(feature = "discrete-inputs")]
pub struct DiscreteInputsApi<'a, TRANSPORT, APP, const N: usize> {
client: &'a mut ClientServices<TRANSPORT, APP, N>,
}
#[cfg(feature = "discrete-inputs")]
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::DiscreteInputResponse,
{
pub fn discrete_inputs(&mut self) -> DiscreteInputsApi<'_, TRANSPORT, APP, N> {
DiscreteInputsApi { client: self }
}
pub fn with_discrete_inputs<R>(
&mut self,
f: impl FnOnce(&mut DiscreteInputsApi<'_, TRANSPORT, APP, N>) -> R,
) -> R {
let mut api = self.discrete_inputs();
f(&mut api)
}
}
#[cfg(feature = "discrete-inputs")]
impl<TRANSPORT, APP, const N: usize> DiscreteInputsApi<'_, TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::DiscreteInputResponse,
{
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_discrete_inputs(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
) -> Result<(), MbusError> {
self.client
.read_discrete_inputs(txn_id, unit_id_slave_addr, address, quantity)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_single_discrete_input(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
) -> Result<(), MbusError> {
self.client
.read_single_discrete_input(txn_id, unit_id_slave_addr, address)
}
}
#[cfg(feature = "registers")]
pub struct RegistersApi<'a, TRANSPORT, APP, const N: usize> {
client: &'a mut ClientServices<TRANSPORT, APP, N>,
}
#[cfg(feature = "registers")]
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::RegisterResponse,
{
pub fn registers(&mut self) -> RegistersApi<'_, TRANSPORT, APP, N> {
RegistersApi { client: self }
}
pub fn with_registers<R>(
&mut self,
f: impl FnOnce(&mut RegistersApi<'_, TRANSPORT, APP, N>) -> R,
) -> R {
let mut api = self.registers();
f(&mut api)
}
}
#[cfg(feature = "registers")]
impl<TRANSPORT, APP, const N: usize> RegistersApi<'_, TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::RegisterResponse,
{
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_holding_registers(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
from_address: u16,
quantity: u16,
) -> Result<(), MbusError> {
self.client
.read_holding_registers(txn_id, unit_id_slave_addr, from_address, quantity)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_single_holding_register(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
) -> Result<(), MbusError> {
self.client
.read_single_holding_register(txn_id, unit_id_slave_addr, address)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_input_registers(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
) -> Result<(), MbusError> {
self.client
.read_input_registers(txn_id, unit_id_slave_addr, address, quantity)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_single_input_register(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
) -> Result<(), MbusError> {
self.client
.read_single_input_register(txn_id, unit_id_slave_addr, address)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_single_register(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: u16,
) -> Result<(), MbusError> {
self.client
.write_single_register(txn_id, unit_id_slave_addr, address, value)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_multiple_registers(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
values: &[u16],
) -> Result<(), MbusError> {
self.client
.write_multiple_registers(txn_id, unit_id_slave_addr, address, quantity, values)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_write_multiple_registers(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
read_address: u16,
read_quantity: u16,
write_address: u16,
write_values: &[u16],
) -> Result<(), MbusError> {
self.client.read_write_multiple_registers(
txn_id,
unit_id_slave_addr,
read_address,
read_quantity,
write_address,
write_values,
)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn mask_write_register(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
and_mask: u16,
or_mask: u16,
) -> Result<(), MbusError> {
self.client
.mask_write_register(txn_id, unit_id_slave_addr, address, and_mask, or_mask)
}
}
#[cfg(feature = "diagnostics")]
pub struct DiagnosticApi<'a, TRANSPORT, APP, const N: usize> {
client: &'a mut ClientServices<TRANSPORT, APP, N>,
}
#[cfg(feature = "diagnostics")]
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::DiagnosticsResponse,
{
pub fn diagnostic(&mut self) -> DiagnosticApi<'_, TRANSPORT, APP, N> {
DiagnosticApi { client: self }
}
pub fn with_diagnostic<R>(
&mut self,
f: impl FnOnce(&mut DiagnosticApi<'_, TRANSPORT, APP, N>) -> R,
) -> R {
let mut api = self.diagnostic();
f(&mut api)
}
}
#[cfg(feature = "diagnostics")]
impl<TRANSPORT, APP, const N: usize> DiagnosticApi<'_, TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::DiagnosticsResponse,
{
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_device_identification(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
read_device_id_code: crate::services::diagnostic::ReadDeviceIdCode,
object_id: crate::services::diagnostic::ObjectId,
) -> Result<(), MbusError> {
self.client.read_device_identification(
txn_id,
unit_id_slave_addr,
read_device_id_code,
object_id,
)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn encapsulated_interface_transport(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
mei_type: EncapsulatedInterfaceType,
data: &[u8],
) -> Result<(), MbusError> {
self.client
.encapsulated_interface_transport(txn_id, unit_id_slave_addr, mei_type, data)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_exception_status(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
) -> Result<(), MbusError> {
self.client
.read_exception_status(txn_id, unit_id_slave_addr)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn diagnostics(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
sub_function: mbus_core::function_codes::public::DiagnosticSubFunction,
data: &[u16],
) -> Result<(), MbusError> {
self.client
.diagnostics(txn_id, unit_id_slave_addr, sub_function, data)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn get_comm_event_counter(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
) -> Result<(), MbusError> {
self.client
.get_comm_event_counter(txn_id, unit_id_slave_addr)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn get_comm_event_log(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
) -> Result<(), MbusError> {
self.client.get_comm_event_log(txn_id, unit_id_slave_addr)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn report_server_id(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
) -> Result<(), MbusError> {
self.client.report_server_id(txn_id, unit_id_slave_addr)
}
}
#[cfg(feature = "fifo")]
pub struct FifoApi<'a, TRANSPORT, APP, const N: usize> {
client: &'a mut ClientServices<TRANSPORT, APP, N>,
}
#[cfg(feature = "fifo")]
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::FifoQueueResponse,
{
pub fn fifo(&mut self) -> FifoApi<'_, TRANSPORT, APP, N> {
FifoApi { client: self }
}
pub fn with_fifo<R>(&mut self, f: impl FnOnce(&mut FifoApi<'_, TRANSPORT, APP, N>) -> R) -> R {
let mut api = self.fifo();
f(&mut api)
}
}
#[cfg(feature = "fifo")]
impl<TRANSPORT, APP, const N: usize> FifoApi<'_, TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::FifoQueueResponse,
{
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_fifo_queue(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
) -> Result<(), MbusError> {
self.client
.read_fifo_queue(txn_id, unit_id_slave_addr, address)
}
}
#[cfg(feature = "file-record")]
pub struct FileRecordsApi<'a, TRANSPORT, APP, const N: usize> {
client: &'a mut ClientServices<TRANSPORT, APP, N>,
}
#[cfg(feature = "file-record")]
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::FileRecordResponse,
{
pub fn file_records(&mut self) -> FileRecordsApi<'_, TRANSPORT, APP, N> {
FileRecordsApi { client: self }
}
pub fn with_file_records<R>(
&mut self,
f: impl FnOnce(&mut FileRecordsApi<'_, TRANSPORT, APP, N>) -> R,
) -> R {
let mut api = self.file_records();
f(&mut api)
}
}
#[cfg(feature = "file-record")]
impl<TRANSPORT, APP, const N: usize> FileRecordsApi<'_, TRANSPORT, APP, N>
where
TRANSPORT: Transport,
APP: ClientCommon + crate::app::FileRecordResponse,
{
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn read_file_record(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
sub_request: &crate::services::file_record::SubRequest,
) -> Result<(), MbusError> {
self.client
.read_file_record(txn_id, unit_id_slave_addr, sub_request)
}
#[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
pub fn write_file_record(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
sub_request: &crate::services::file_record::SubRequest,
) -> Result<(), MbusError> {
self.client
.write_file_record(txn_id, unit_id_slave_addr, sub_request)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Single {
address: u16,
value: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Multiple {
address: u16,
quantity: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Mask {
address: u16,
and_mask: u16,
or_mask: u16,
}
#[cfg(feature = "diagnostics")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Diag {
device_id_code: ReadDeviceIdCode,
encap_type: EncapsulatedInterfaceType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OperationMeta {
Other,
Single(Single),
Multiple(Multiple),
Masking(Mask),
#[cfg(feature = "diagnostics")]
Diag(Diag),
}
impl OperationMeta {
fn address(&self) -> u16 {
match self {
OperationMeta::Single(s) => s.address,
OperationMeta::Multiple(m) => m.address,
OperationMeta::Masking(m) => m.address,
_ => 0,
}
}
fn value(&self) -> u16 {
match self {
OperationMeta::Single(s) => s.value,
_ => 0,
}
}
fn quantity(&self) -> u16 {
match self {
OperationMeta::Single(_) => 1,
OperationMeta::Multiple(m) => m.quantity,
_ => 0,
}
}
fn and_mask(&self) -> u16 {
match self {
OperationMeta::Masking(m) => m.and_mask,
_ => 0,
}
}
fn or_mask(&self) -> u16 {
match self {
OperationMeta::Masking(m) => m.or_mask,
_ => 0,
}
}
fn is_single(&self) -> bool {
matches!(self, OperationMeta::Single(_))
}
fn single_value(&self) -> u16 {
match self {
OperationMeta::Single(s) => s.value,
_ => 0,
}
}
fn device_id_code(&self) -> ReadDeviceIdCode {
match self {
#[cfg(feature = "diagnostics")]
OperationMeta::Diag(d) => d.device_id_code,
_ => ReadDeviceIdCode::default(),
}
}
fn encap_type(&self) -> EncapsulatedInterfaceType {
match self {
#[cfg(feature = "diagnostics")]
OperationMeta::Diag(d) => d.encap_type,
_ => EncapsulatedInterfaceType::default(),
}
}
}
#[derive(Debug)]
pub(crate) struct ExpectedResponse<T, A, const N: usize> {
pub txn_id: u16,
pub unit_id_or_slave_addr: u8,
pub original_adu: Vec<u8, MAX_ADU_FRAME_LEN>,
pub sent_timestamp: u64,
pub retries_left: u8,
pub retry_attempt_index: u8,
pub next_retry_timestamp: Option<u64>,
pub handler: ResponseHandler<T, A, N>,
pub operation_meta: OperationMeta,
}
#[derive(Debug)]
pub struct ClientServices<TRANSPORT, APP, const N: usize = 1> {
app: APP,
transport: TRANSPORT,
config: ModbusConfig,
rxed_frame: Vec<u8, MAX_ADU_FRAME_LEN>,
expected_responses: Vec<ExpectedResponse<TRANSPORT, APP, N>, N>,
next_timeout_check: Option<u64>,
}
pub trait ClientCommon: RequestErrorNotifier + TimeKeeper {}
impl<T> ClientCommon for T where T: RequestErrorNotifier + TimeKeeper {}
impl<T, APP, const N: usize> ClientServices<T, APP, N>
where
T: Transport,
APP: ClientCommon,
{
fn dispatch_response(&mut self, message: &ModbusMessage) {
let wire_txn_id = message.transaction_id();
let unit_id_or_slave_addr = message.unit_id_or_slave_addr();
let index = if self.transport.transport_type().is_tcp_type() {
self.expected_responses.iter().position(|r| {
r.txn_id == wire_txn_id && r.unit_id_or_slave_addr == unit_id_or_slave_addr.into()
})
} else {
self.expected_responses
.iter()
.position(|r| r.unit_id_or_slave_addr == unit_id_or_slave_addr.into())
};
let expected = match index {
Some(i) => self.expected_responses.swap_remove(i),
None => {
client_log_debug!(
"dropping unmatched response: txn_id={}, unit_id_or_slave_addr={}",
wire_txn_id,
unit_id_or_slave_addr.get()
);
return;
}
};
let request_txn_id = expected.txn_id;
client_log_trace!(
"dispatching response: txn_id={}, unit_id_or_slave_addr={}, queue_len_after_pop={}",
request_txn_id,
unit_id_or_slave_addr.get(),
self.expected_responses.len()
);
if let Some(exception_code) = message.pdu().error_code() {
client_log_debug!(
"modbus exception response: txn_id={}, unit_id_or_slave_addr={}, code=0x{:02X}",
request_txn_id,
unit_id_or_slave_addr.get(),
exception_code
);
self.app.request_failed(
request_txn_id,
unit_id_or_slave_addr,
MbusError::ModbusException(exception_code),
);
return;
}
(expected.handler)(self, &expected, message);
}
}
impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
TRANSPORT: Transport,
TRANSPORT::Error: Into<MbusError>,
APP: RequestErrorNotifier + TimeKeeper,
{
pub fn poll(&mut self) {
match self.transport.recv() {
Ok(frame) => {
client_log_trace!("received {} transport bytes", frame.len());
if self.rxed_frame.extend_from_slice(frame.as_slice()).is_err() {
client_log_debug!(
"received frame buffer overflow while appending {} bytes; clearing receive buffer",
frame.len()
);
self.rxed_frame.clear();
}
while !self.rxed_frame.is_empty() {
match self.ingest_frame() {
Ok(consumed) => {
client_log_trace!(
"ingested complete frame consuming {} bytes from rx buffer len {}",
consumed,
self.rxed_frame.len()
);
let len = self.rxed_frame.len();
if consumed < len {
self.rxed_frame.copy_within(consumed.., 0);
self.rxed_frame.truncate(len - consumed);
} else {
self.rxed_frame.clear();
}
}
Err(MbusError::BufferTooSmall) => {
client_log_trace!(
"incomplete frame in rx buffer; waiting for more bytes (buffer_len={})",
self.rxed_frame.len()
);
break;
}
Err(err) => {
client_log_debug!(
"frame parse/resync event: error={:?}, buffer_len={}; dropping 1 byte",
err,
self.rxed_frame.len()
);
let len = self.rxed_frame.len();
if len > 1 {
self.rxed_frame.copy_within(1.., 0);
self.rxed_frame.truncate(len - 1);
} else {
self.rxed_frame.clear();
}
}
}
}
}
Err(err) => {
let recv_error: MbusError = err.into();
let is_connection_loss = matches!(
recv_error,
MbusError::ConnectionClosed
| MbusError::ConnectionFailed
| MbusError::ConnectionLost
| MbusError::IoError
) || !self.transport.is_connected();
if is_connection_loss {
client_log_debug!(
"connection loss detected during poll: error={:?}, pending_requests={}",
recv_error,
self.expected_responses.len()
);
self.fail_all_pending_requests(MbusError::ConnectionLost);
let _ = self.transport.disconnect();
self.rxed_frame.clear();
} else {
client_log_trace!("non-fatal recv status during poll: {:?}", recv_error);
}
}
}
self.handle_timeouts();
}
fn fail_all_pending_requests(&mut self, error: MbusError) {
let pending_count = self.expected_responses.len();
client_log_debug!(
"failing {} pending request(s) with error {:?}",
pending_count,
error
);
while let Some(response) = self.expected_responses.pop() {
self.app.request_failed(
response.txn_id,
UnitIdOrSlaveAddr::from_u8(response.unit_id_or_slave_addr),
error,
);
}
self.next_timeout_check = None;
}
fn handle_timeouts(&mut self) {
if self.expected_responses.is_empty() {
self.next_timeout_check = None;
return;
}
let current_millis = self.app.current_millis();
if let Some(check_at) = self.next_timeout_check
&& current_millis < check_at
{
client_log_trace!(
"skipping timeout scan until {}, current_millis={}",
check_at,
current_millis
);
return;
}
let response_timeout_ms = self.response_timeout_ms();
let retry_backoff = self.config.retry_backoff_strategy();
let retry_jitter = self.config.retry_jitter_strategy();
let retry_random_fn = self.config.retry_random_fn();
let expected_responses = &mut self.expected_responses;
let mut i = 0;
let mut new_next_check = u64::MAX;
while i < expected_responses.len() {
let expected_response = &mut expected_responses[i];
if let Some(retry_at) = expected_response.next_retry_timestamp {
if current_millis >= retry_at {
client_log_debug!(
"retry due now: txn_id={}, unit_id_or_slave_addr={}, retry_attempt_index={}, retries_left={}",
expected_response.txn_id,
expected_response.unit_id_or_slave_addr,
expected_response.retry_attempt_index.saturating_add(1),
expected_response.retries_left
);
if let Err(_e) = self.transport.send(&expected_response.original_adu) {
let response = expected_responses.swap_remove(i);
client_log_debug!(
"retry send failed: txn_id={}, unit_id_or_slave_addr={}; dropping request",
response.txn_id,
response.unit_id_or_slave_addr
);
self.app.request_failed(
response.txn_id,
UnitIdOrSlaveAddr::from_u8(response.unit_id_or_slave_addr),
MbusError::SendFailed,
);
continue;
}
expected_response.retries_left =
expected_response.retries_left.saturating_sub(1);
expected_response.retry_attempt_index =
expected_response.retry_attempt_index.saturating_add(1);
expected_response.sent_timestamp = current_millis;
expected_response.next_retry_timestamp = None;
let expires_at = current_millis.saturating_add(response_timeout_ms);
if expires_at < new_next_check {
new_next_check = expires_at;
}
i += 1;
continue;
}
if retry_at < new_next_check {
new_next_check = retry_at;
}
i += 1;
continue;
}
let expires_at = expected_response
.sent_timestamp
.saturating_add(response_timeout_ms);
if current_millis > expires_at {
if expected_response.retries_left == 0 {
let response = expected_responses.swap_remove(i);
client_log_debug!(
"request exhausted retries: txn_id={}, unit_id_or_slave_addr={}",
response.txn_id,
response.unit_id_or_slave_addr
);
self.app.request_failed(
response.txn_id,
UnitIdOrSlaveAddr::from_u8(response.unit_id_or_slave_addr),
MbusError::NoRetriesLeft,
);
continue;
}
let next_attempt = expected_response.retry_attempt_index.saturating_add(1);
let base_delay_ms = retry_backoff.delay_ms_for_retry(next_attempt);
let retry_delay_ms = retry_jitter.apply(base_delay_ms, retry_random_fn) as u64;
let retry_at = current_millis.saturating_add(retry_delay_ms);
expected_response.next_retry_timestamp = Some(retry_at);
client_log_debug!(
"scheduling retry: txn_id={}, unit_id_or_slave_addr={}, next_attempt={}, delay_ms={}, retry_at={}",
expected_response.txn_id,
expected_response.unit_id_or_slave_addr,
next_attempt,
retry_delay_ms,
retry_at
);
if retry_delay_ms == 0 {
client_log_trace!(
"retry delay is zero; retry will be processed in the same poll cycle for txn_id={}",
expected_response.txn_id
);
continue;
}
if retry_at < new_next_check {
new_next_check = retry_at;
}
i += 1;
continue;
}
if expires_at < new_next_check {
new_next_check = expires_at;
}
i += 1;
}
if new_next_check != u64::MAX {
self.next_timeout_check = Some(new_next_check);
} else {
self.next_timeout_check = None;
}
}
fn add_an_expectation(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
frame: &heapless::Vec<u8, MAX_ADU_FRAME_LEN>,
operation_meta: OperationMeta,
handler: ResponseHandler<TRANSPORT, APP, N>,
) -> Result<(), MbusError> {
client_log_trace!(
"queueing expected response: txn_id={}, unit_id_or_slave_addr={}, queue_len_before={}",
txn_id,
unit_id_slave_addr.get(),
self.expected_responses.len()
);
self.expected_responses
.push(ExpectedResponse {
txn_id,
unit_id_or_slave_addr: unit_id_slave_addr.get(),
original_adu: frame.clone(),
sent_timestamp: self.app.current_millis(),
retries_left: self.retry_attempts(),
retry_attempt_index: 0,
next_retry_timestamp: None,
handler,
operation_meta,
})
.map_err(|_| MbusError::TooManyRequests)?;
Ok(())
}
}
impl<TRANSPORT: Transport, APP: ClientCommon, const N: usize> ClientServices<TRANSPORT, APP, N> {
pub fn new(
mut transport: TRANSPORT,
app: APP,
config: ModbusConfig,
) -> Result<Self, MbusError> {
let transport_type = transport.transport_type();
if matches!(
transport_type,
TransportType::StdSerial(_) | TransportType::CustomSerial(_)
) && N != 1
{
return Err(MbusError::InvalidNumOfExpectedRsps);
}
transport
.connect(&config)
.map_err(|_e| MbusError::ConnectionFailed)?;
client_log_debug!(
"client created with transport_type={:?}, queue_capacity={}",
transport_type,
N
);
Ok(Self {
app,
transport,
rxed_frame: Vec::new(),
config,
expected_responses: Vec::new(),
next_timeout_check: None,
})
}
pub fn app(&self) -> &APP {
&self.app
}
pub fn is_connected(&self) -> bool {
self.transport.is_connected()
}
pub fn reconnect(&mut self) -> Result<(), MbusError>
where
TRANSPORT::Error: Into<MbusError>,
{
client_log_debug!(
"reconnect requested; pending_requests={}",
self.expected_responses.len()
);
self.fail_all_pending_requests(MbusError::ConnectionLost);
self.rxed_frame.clear();
self.next_timeout_check = None;
let _ = self.transport.disconnect();
self.transport.connect(&self.config).map_err(|e| e.into())
}
pub fn new_serial(
mut transport: TRANSPORT,
app: APP,
config: ModbusSerialConfig,
) -> Result<Self, MbusError>
where
[(); N]: SerialQueueSizeOne,
{
let transport_type = transport.transport_type();
if !matches!(
transport_type,
TransportType::StdSerial(_) | TransportType::CustomSerial(_)
) {
return Err(MbusError::InvalidTransport);
}
let config = ModbusConfig::Serial(config);
transport
.connect(&config)
.map_err(|_e| MbusError::ConnectionFailed)?;
client_log_debug!("serial client created with queue_capacity={}", N);
Ok(Self {
app,
transport,
rxed_frame: Vec::new(),
config,
expected_responses: Vec::new(),
next_timeout_check: None,
})
}
fn response_timeout_ms(&self) -> u64 {
match &self.config {
ModbusConfig::Tcp(config) => config.response_timeout_ms as u64,
ModbusConfig::Serial(config) => config.response_timeout_ms as u64,
}
}
fn retry_attempts(&self) -> u8 {
match &self.config {
ModbusConfig::Tcp(config) => config.retry_attempts,
ModbusConfig::Serial(config) => config.retry_attempts,
}
}
fn ingest_frame(&mut self) -> Result<usize, MbusError> {
let frame = self.rxed_frame.as_slice();
let transport_type = self.transport.transport_type();
client_log_trace!(
"attempting frame ingest: transport_type={:?}, buffer_len={}",
transport_type,
frame.len()
);
let expected_length = match derive_length_from_bytes(frame, transport_type) {
Some(len) => len,
None => return Err(MbusError::BufferTooSmall),
};
client_log_trace!("derived expected frame length={}", expected_length);
if expected_length > MAX_ADU_FRAME_LEN {
client_log_debug!(
"derived frame length {} exceeds MAX_ADU_FRAME_LEN {}",
expected_length,
MAX_ADU_FRAME_LEN
);
return Err(MbusError::BasicParseError);
}
if self.rxed_frame.len() < expected_length {
return Err(MbusError::BufferTooSmall);
}
let message = match common::decompile_adu_frame(&frame[..expected_length], transport_type) {
Ok(value) => value,
Err(err) => {
client_log_debug!(
"decompile_adu_frame failed for {} bytes: {:?}",
expected_length,
err
);
return Err(err); }
};
use mbus_core::data_unit::common::AdditionalAddress;
use mbus_core::transport::TransportType::*;
let message = match self.transport.transport_type() {
StdTcp | CustomTcp => {
let mbap_header = match message.additional_address() {
AdditionalAddress::MbapHeader(header) => header,
_ => return Ok(expected_length),
};
let additional_addr = AdditionalAddress::MbapHeader(*mbap_header);
ModbusMessage::new(additional_addr, message.pdu)
}
StdSerial(_) | CustomSerial(_) => {
let slave_addr = match message.additional_address() {
AdditionalAddress::SlaveAddress(addr) => addr.address(),
_ => return Ok(expected_length),
};
let additional_address =
AdditionalAddress::SlaveAddress(SlaveAddress::new(slave_addr)?);
ModbusMessage::new(additional_address, message.pdu)
}
};
self.dispatch_response(&message);
client_log_trace!("frame dispatch complete for {} bytes", expected_length);
Ok(expected_length)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::CoilResponse;
use crate::app::DiagnosticsResponse;
use crate::app::DiscreteInputResponse;
use crate::app::FifoQueueResponse;
use crate::app::FileRecordResponse;
use crate::app::RegisterResponse;
use crate::services::coil::Coils;
use crate::services::diagnostic::ConformityLevel;
use crate::services::diagnostic::DeviceIdentificationResponse;
use crate::services::diagnostic::ObjectId;
use crate::services::discrete_input::DiscreteInputs;
use crate::services::fifo_queue::FifoQueue;
use crate::services::file_record::MAX_SUB_REQUESTS_PER_PDU;
use crate::services::file_record::SubRequest;
use crate::services::file_record::SubRequestParams;
use crate::services::register::Registers;
use core::cell::RefCell; use core::str::FromStr;
use heapless::Deque;
use heapless::Vec;
use mbus_core::errors::MbusError;
use mbus_core::function_codes::public::DiagnosticSubFunction;
use mbus_core::transport::checksum;
use mbus_core::transport::TransportType;
use mbus_core::transport::{
BackoffStrategy, BaudRate, JitterStrategy, ModbusConfig, ModbusSerialConfig,
ModbusTcpConfig, Parity, SerialMode,
};
const MOCK_DEQUE_CAPACITY: usize = 10;
fn rand_zero() -> u32 {
0
}
fn rand_upper_percent_20() -> u32 {
40
}
fn make_serial_config() -> ModbusSerialConfig {
ModbusSerialConfig {
port_path: heapless::String::<64>::from_str("/dev/ttyUSB0").unwrap(),
mode: SerialMode::Rtu,
baud_rate: BaudRate::Baud19200,
data_bits: mbus_core::transport::DataBits::Eight,
stop_bits: 1,
parity: Parity::Even,
response_timeout_ms: 100,
retry_attempts: 0,
retry_backoff_strategy: BackoffStrategy::Immediate,
retry_jitter_strategy: JitterStrategy::None,
retry_random_fn: None,
}
}
fn make_serial_client() -> ClientServices<MockTransport, MockApp, 1> {
let transport = MockTransport {
transport_type: Some(TransportType::StdSerial(SerialMode::Rtu)),
..Default::default()
};
let app = MockApp::default();
ClientServices::<MockTransport, MockApp, 1>::new_serial(transport, app, make_serial_config())
.unwrap()
}
fn make_rtu_exception_adu(
unit_id: UnitIdOrSlaveAddr,
function_code: u8,
exception_code: u8,
) -> Vec<u8, MAX_ADU_FRAME_LEN> {
let mut frame = Vec::new();
frame.push(unit_id.get()).unwrap();
frame.push(function_code | 0x80).unwrap();
frame.push(exception_code).unwrap();
let crc = checksum::crc16(frame.as_slice()).to_le_bytes();
frame.extend_from_slice(&crc).unwrap();
frame
}
#[derive(Debug, Default)]
struct MockTransport {
pub sent_frames: RefCell<Deque<Vec<u8, MAX_ADU_FRAME_LEN>, MOCK_DEQUE_CAPACITY>>, pub recv_frames: RefCell<Deque<Vec<u8, MAX_ADU_FRAME_LEN>, MOCK_DEQUE_CAPACITY>>, pub recv_error: RefCell<Option<MbusError>>,
pub connect_should_fail: bool,
pub send_should_fail: bool,
pub is_connected_flag: RefCell<bool>,
pub transport_type: Option<TransportType>,
}
impl Transport for MockTransport {
type Error = MbusError;
fn connect(&mut self, _config: &ModbusConfig) -> Result<(), Self::Error> {
if self.connect_should_fail {
return Err(MbusError::ConnectionFailed);
}
*self.is_connected_flag.borrow_mut() = true;
Ok(())
}
fn disconnect(&mut self) -> Result<(), Self::Error> {
*self.is_connected_flag.borrow_mut() = false;
Ok(())
}
fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error> {
if self.send_should_fail {
return Err(MbusError::SendFailed);
}
let mut vec_adu = Vec::new();
vec_adu
.extend_from_slice(adu)
.map_err(|_| MbusError::BufferLenMissmatch)?;
self.sent_frames
.borrow_mut()
.push_back(vec_adu)
.map_err(|_| MbusError::BufferLenMissmatch)?;
Ok(())
}
fn recv(&mut self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error> {
if let Some(err) = self.recv_error.borrow_mut().take() {
return Err(err);
}
self.recv_frames
.borrow_mut()
.pop_front()
.ok_or(MbusError::Timeout)
}
fn is_connected(&self) -> bool {
*self.is_connected_flag.borrow()
}
fn transport_type(&self) -> TransportType {
self.transport_type.unwrap_or(TransportType::StdTcp)
}
}
#[derive(Debug, Default)]
struct MockApp {
pub received_coil_responses: RefCell<Vec<(u16, UnitIdOrSlaveAddr, Coils), 10>>, pub received_write_single_coil_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, u16, bool), 10>>,
pub received_write_multiple_coils_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, u16, u16), 10>>,
pub received_discrete_input_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, DiscreteInputs, u16), 10>>,
pub received_holding_register_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, Registers, u16), 10>>,
pub received_input_register_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, Registers, u16), 10>>,
pub received_write_single_register_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, u16, u16), 10>>,
pub received_write_multiple_register_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, u16, u16), 10>>,
pub received_read_write_multiple_registers_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, Registers), 10>>,
pub received_mask_write_register_responses: RefCell<Vec<(u16, UnitIdOrSlaveAddr), 10>>,
pub received_read_fifo_queue_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, FifoQueue), 10>>,
pub received_read_file_record_responses: RefCell<
Vec<
(
u16,
UnitIdOrSlaveAddr,
Vec<SubRequestParams, MAX_SUB_REQUESTS_PER_PDU>,
),
10,
>,
>,
pub received_write_file_record_responses: RefCell<Vec<(u16, UnitIdOrSlaveAddr), 10>>,
pub received_read_device_id_responses:
RefCell<Vec<(u16, UnitIdOrSlaveAddr, DeviceIdentificationResponse), 10>>,
pub failed_requests: RefCell<Vec<(u16, UnitIdOrSlaveAddr, MbusError), 10>>,
pub current_time: RefCell<u64>, }
impl CoilResponse for MockApp {
fn read_coils_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
coils: &Coils,
) {
self.received_coil_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, coils.clone()))
.unwrap();
}
fn read_single_coil_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: bool,
) {
let mut values_vec = [0x00, 1];
values_vec[0] = if value { 0x01 } else { 0x00 }; let coils = Coils::new(address, 1)
.unwrap()
.with_values(&values_vec, 1)
.unwrap();
self.received_coil_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, coils))
.unwrap();
}
fn write_single_coil_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: bool,
) {
self.received_write_single_coil_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, address, value))
.unwrap();
}
fn write_multiple_coils_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
) {
self.received_write_multiple_coils_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, address, quantity))
.unwrap();
}
}
impl DiscreteInputResponse for MockApp {
fn read_multiple_discrete_inputs_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
inputs: &DiscreteInputs,
) {
self.received_discrete_input_responses
.borrow_mut()
.push((
txn_id,
unit_id_slave_addr,
inputs.clone(),
inputs.quantity(),
))
.unwrap();
}
fn read_single_discrete_input_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: bool,
) {
let mut values = [0u8; mbus_core::models::discrete_input::MAX_DISCRETE_INPUT_BYTES];
values[0] = if value { 0x01 } else { 0x00 };
let inputs = DiscreteInputs::new(address, 1)
.unwrap()
.with_values(&values, 1)
.unwrap();
self.received_discrete_input_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, inputs, 1))
.unwrap();
}
}
impl RequestErrorNotifier for MockApp {
fn request_failed(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
error: MbusError,
) {
self.failed_requests
.borrow_mut()
.push((txn_id, unit_id_slave_addr, error))
.unwrap();
}
}
impl RegisterResponse for MockApp {
fn read_multiple_holding_registers_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
registers: &Registers,
) {
let quantity = registers.quantity();
self.received_holding_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, registers.clone(), quantity))
.unwrap();
}
fn read_single_input_register_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: u16,
) {
let values = [value];
let registers = Registers::new(address, 1)
.unwrap()
.with_values(&values, 1)
.unwrap();
self.received_input_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, registers, 1))
.unwrap();
}
fn read_single_holding_register_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: u16,
) {
let data = [value];
let registers = Registers::new(address, 1)
.unwrap()
.with_values(&data, 1)
.unwrap();
self.received_holding_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, registers, 1))
.unwrap();
}
fn read_multiple_input_registers_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
registers: &Registers,
) {
let quantity = registers.quantity();
self.received_input_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, registers.clone(), quantity))
.unwrap();
}
fn write_single_register_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: u16,
) {
self.received_write_single_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, address, value))
.unwrap();
}
fn write_multiple_registers_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
quantity: u16,
) {
self.received_write_multiple_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, address, quantity))
.unwrap();
}
fn read_write_multiple_registers_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
registers: &Registers,
) {
self.received_read_write_multiple_registers_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, registers.clone()))
.unwrap();
}
fn mask_write_register_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
) {
self.received_mask_write_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr))
.unwrap();
}
fn read_single_register_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
address: u16,
value: u16,
) {
let data = [value];
let registers = Registers::new(address, 1)
.unwrap()
.with_values(&data, 1)
.unwrap();
self.received_holding_register_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, registers, 1))
.unwrap();
}
}
impl FifoQueueResponse for MockApp {
fn read_fifo_queue_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
fifo_queue: &FifoQueue,
) {
self.received_read_fifo_queue_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, fifo_queue.clone()))
.unwrap();
}
}
impl FileRecordResponse for MockApp {
fn read_file_record_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
data: &[SubRequestParams],
) {
let mut vec = Vec::new();
vec.extend_from_slice(data).unwrap();
self.received_read_file_record_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, vec))
.unwrap();
}
fn write_file_record_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
) {
self.received_write_file_record_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr))
.unwrap();
}
}
impl DiagnosticsResponse for MockApp {
fn read_device_identification_response(
&mut self,
txn_id: u16,
unit_id_slave_addr: UnitIdOrSlaveAddr,
response: &DeviceIdentificationResponse,
) {
self.received_read_device_id_responses
.borrow_mut()
.push((txn_id, unit_id_slave_addr, response.clone()))
.unwrap();
}
fn encapsulated_interface_transport_response(
&mut self,
_: u16,
_: UnitIdOrSlaveAddr,
_: EncapsulatedInterfaceType,
_: &[u8],
) {
}
fn diagnostics_response(
&mut self,
_: u16,
_: UnitIdOrSlaveAddr,
_: DiagnosticSubFunction,
_: &[u16],
) {
}
fn get_comm_event_counter_response(
&mut self,
_: u16,
_: UnitIdOrSlaveAddr,
_: u16,
_: u16,
) {
}
fn get_comm_event_log_response(
&mut self,
_: u16,
_: UnitIdOrSlaveAddr,
_: u16,
_: u16,
_: u16,
_: &[u8],
) {
}
fn read_exception_status_response(&mut self, _: u16, _: UnitIdOrSlaveAddr, _: u8) {}
fn report_server_id_response(&mut self, _: u16, _: UnitIdOrSlaveAddr, _: &[u8]) {}
}
impl TimeKeeper for MockApp {
fn current_millis(&self) -> u64 {
*self.current_time.borrow()
}
}
#[test]
fn test_client_services_new_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config);
assert!(client_services.is_ok());
assert!(client_services.unwrap().transport.is_connected());
}
#[test]
fn test_client_services_new_connection_failure() {
let mut transport = MockTransport::default();
transport.connect_should_fail = true;
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config);
assert!(client_services.is_err());
assert_eq!(client_services.unwrap_err(), MbusError::ConnectionFailed);
}
#[test]
fn test_client_services_new_serial_success() {
let transport = MockTransport {
transport_type: Some(TransportType::StdSerial(SerialMode::Rtu)),
..Default::default()
};
let app = MockApp::default();
let serial_config = ModbusSerialConfig {
port_path: heapless::String::<64>::from_str("/dev/ttyUSB0").unwrap(),
mode: SerialMode::Rtu,
baud_rate: BaudRate::Baud19200,
data_bits: mbus_core::transport::DataBits::Eight,
stop_bits: 1,
parity: Parity::Even,
response_timeout_ms: 1000,
retry_attempts: 1,
retry_backoff_strategy: BackoffStrategy::Immediate,
retry_jitter_strategy: JitterStrategy::None,
retry_random_fn: None,
};
let client_services =
ClientServices::<MockTransport, MockApp, 1>::new_serial(transport, app, serial_config);
assert!(client_services.is_ok());
}
#[test]
fn test_reconnect_success_flushes_pending_requests() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(1).unwrap();
client_services.read_single_coil(10, unit_id, 0).unwrap();
assert_eq!(client_services.expected_responses.len(), 1);
let reconnect_result = client_services.reconnect();
assert!(reconnect_result.is_ok());
assert!(client_services.is_connected());
assert!(client_services.expected_responses.is_empty());
let failed_requests = client_services.app().failed_requests.borrow();
assert_eq!(failed_requests.len(), 1);
assert_eq!(failed_requests[0].0, 10);
assert_eq!(failed_requests[0].2, MbusError::ConnectionLost);
}
#[test]
fn test_reconnect_failure_propagates_connect_error() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
client_services.transport.connect_should_fail = true;
let reconnect_result = client_services.reconnect();
assert!(reconnect_result.is_err());
assert_eq!(reconnect_result.unwrap_err(), MbusError::ConnectionFailed);
assert!(!client_services.is_connected());
}
#[test]
fn test_read_multiple_coils_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0001;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 8;
client_services
.read_multiple_coils(txn_id, unit_id, address, quantity)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x08, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_read_multiple_coils_invalid_quantity() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0001;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 0;
let result = client_services.read_multiple_coils(txn_id, unit_id, address, quantity); assert_eq!(result.unwrap_err(), MbusError::InvalidQuantity);
}
#[test]
fn test_read_multiple_coils_send_failure() {
let mut transport = MockTransport::default();
transport.send_should_fail = true;
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0001;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 8;
let result = client_services.read_multiple_coils(txn_id, unit_id, address, quantity); assert_eq!(result.unwrap_err(), MbusError::SendFailed);
}
#[test]
fn test_ingest_frame_wrong_fc() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let response_adu = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x03, 0x01, 0xB3];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services.app().received_coil_responses.borrow();
assert!(received_responses.is_empty());
}
#[test]
fn test_ingest_frame_malformed_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let malformed_adu = [0x01, 0x02, 0x03];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&malformed_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services.app().received_coil_responses.borrow();
assert!(received_responses.is_empty());
}
#[test]
fn test_ingest_frame_unknown_txn_id() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let response_adu = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01, 0x01, 0xB3];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services.app().received_coil_responses.borrow();
assert!(received_responses.is_empty());
}
#[test]
fn test_ingest_frame_pdu_parse_failure() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0001;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 8;
client_services
.read_multiple_coils(txn_id, unit_id, address, quantity) .unwrap();
let response_adu = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x01, 0x01, 0x01, 0xB3, 0x00,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services.app().received_coil_responses.borrow();
assert!(received_responses.is_empty());
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_client_services_read_single_coil_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0002;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0005;
client_services .read_single_coil(txn_id, unit_id, address)
.unwrap();
let sent_adu = client_services
.transport
.sent_frames
.borrow_mut()
.pop_front()
.unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x05, 0x00, 0x01, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
let response_adu = [0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01, 0x01, 0x01];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services.app().received_coil_responses.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_coils) = &received_responses[0];
let rcv_quantity = rcv_coils.quantity();
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_coils.from_address(), address);
assert_eq!(rcv_coils.quantity(), 1); assert_eq!(&rcv_coils.values()[..1], &[0x01]); assert_eq!(rcv_quantity, 1);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_read_single_coil_request_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0002;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0005;
client_services
.read_single_coil(txn_id, unit_id, address) .unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x05, 0x00, 0x01, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
assert_eq!(client_services.expected_responses.len(), 1); let single_read = client_services.expected_responses[0]
.operation_meta
.is_single();
assert!(single_read);
}
#[test]
fn test_write_single_coil_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0003;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x000A;
let value = true;
client_services
.write_single_coil(txn_id, unit_id, address, value) .unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x03, 0x00, 0x00, 0x00, 0x06, 0x01, 0x05, 0x00, 0x0A, 0xFF, 0x00, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
assert_eq!(client_services.expected_responses.len(), 1);
let expected_address = client_services.expected_responses[0]
.operation_meta
.address();
let expected_value = client_services.expected_responses[0].operation_meta.value() != 0;
assert_eq!(expected_address, address);
assert_eq!(expected_value, value);
}
#[test]
fn test_client_services_write_single_coil_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0003;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x000A;
let value = true;
client_services .write_single_coil(txn_id, unit_id, address, value)
.unwrap();
let sent_adu = client_services
.transport
.sent_frames
.borrow_mut()
.pop_front()
.unwrap();
#[rustfmt::skip]
let expected_request_adu: [u8; 12] = [
0x00, 0x03, 0x00, 0x00, 0x00, 0x06, 0x01, 0x05, 0x00, 0x0A, 0xFF, 0x00, ];
assert_eq!(sent_adu.as_slice(), &expected_request_adu);
let response_adu = [
0x00, 0x03, 0x00, 0x00, 0x00, 0x06, 0x01, 0x05, 0x00, 0x0A, 0xFF, 0x00,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_write_single_coil_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_address, rcv_value) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(*rcv_address, address);
assert_eq!(*rcv_value, value);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_write_multiple_coils_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0004;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 10;
let mut values = Coils::new(address, quantity).unwrap();
for i in 0..quantity {
values.set_value(address + i, i % 2 == 0).unwrap();
}
client_services
.write_multiple_coils(txn_id, unit_id, address, &values) .unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 15] = [
0x00, 0x04, 0x00, 0x00, 0x00, 0x09, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0A, 0x02, 0x55, 0x01, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
assert_eq!(client_services.expected_responses.len(), 1);
let expected_address = client_services.expected_responses[0]
.operation_meta
.address();
let expected_quantity = client_services.expected_responses[0]
.operation_meta
.quantity();
assert_eq!(expected_address, address);
assert_eq!(expected_quantity, quantity);
}
#[test]
fn test_client_services_write_multiple_coils_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0004;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 10;
let mut values = Coils::new(address, quantity).unwrap();
for i in 0..quantity {
values.set_value(address + i, i % 2 == 0).unwrap();
}
client_services .write_multiple_coils(txn_id, unit_id, address, &values)
.unwrap();
let sent_adu = client_services
.transport
.sent_frames
.borrow_mut()
.pop_front()
.unwrap();
#[rustfmt::skip]
let expected_request_adu: [u8; 15] = [
0x00, 0x04, 0x00, 0x00, 0x00, 0x09, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0A, 0x02, 0x55, 0x01, ];
assert_eq!(sent_adu.as_slice(), &expected_request_adu);
let response_adu = [
0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0A,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_write_multiple_coils_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_address, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(*rcv_address, address);
assert_eq!(*rcv_quantity, quantity);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_client_services_read_coils_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0001;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 8;
client_services
.read_multiple_coils(txn_id, unit_id, address, quantity) .unwrap();
let sent_adu = client_services
.transport
.sent_frames
.borrow_mut()
.pop_front()
.unwrap(); assert_eq!(
sent_adu.as_slice(),
&[
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x08
]
);
assert_eq!(client_services.expected_responses.len(), 1); let from_address = client_services.expected_responses[0]
.operation_meta
.address();
let expected_quantity = client_services.expected_responses[0]
.operation_meta
.quantity();
assert_eq!(expected_quantity, quantity);
assert_eq!(from_address, address);
let response_adu = [0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01, 0x01, 0xB3];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services.app().received_coil_responses.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_coils) = &received_responses[0];
let rcv_quantity = rcv_coils.quantity();
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_coils.from_address(), address);
assert_eq!(rcv_coils.quantity(), quantity);
assert_eq!(&rcv_coils.values()[..1], &[0xB3]);
assert_eq!(rcv_quantity, quantity);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_client_services_timeout_with_retry() {
let transport = MockTransport::default();
transport.recv_frames.borrow_mut().clear();
let app = MockApp::default();
let mut tcp_config = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config.response_timeout_ms = 100; tcp_config.retry_attempts = 1; let config = ModbusConfig::Tcp(tcp_config);
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0005;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
client_services
.read_single_coil(txn_id, unit_id, address)
.unwrap();
*client_services.app().current_time.borrow_mut() = 150;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 2);
assert_eq!(client_services.expected_responses.len(), 1); assert_eq!(client_services.expected_responses[0].retries_left, 0);
*client_services.app().current_time.borrow_mut() = 300;
client_services.poll();
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_client_services_concurrent_timeouts() {
let transport = MockTransport::default();
let app = MockApp::default();
let mut tcp_config = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config.response_timeout_ms = 100;
tcp_config.retry_attempts = 1;
let config = ModbusConfig::Tcp(tcp_config);
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_single_coil(1, unit_id, 0x0000)
.unwrap();
client_services
.read_single_coil(2, unit_id, 0x0001)
.unwrap();
assert_eq!(client_services.expected_responses.len(), 2);
assert_eq!(client_services.transport.sent_frames.borrow().len(), 2);
*client_services.app().current_time.borrow_mut() = 150;
client_services.poll();
assert_eq!(client_services.expected_responses.len(), 2);
assert_eq!(client_services.expected_responses[0].retries_left, 0);
assert_eq!(client_services.expected_responses[1].retries_left, 0);
assert_eq!(client_services.transport.sent_frames.borrow().len(), 4);
*client_services.app().current_time.borrow_mut() = 300;
client_services.poll();
assert!(client_services.expected_responses.is_empty());
let failed_requests = client_services.app().failed_requests.borrow();
assert_eq!(failed_requests.len(), 2);
let has_txn_1 = failed_requests
.iter()
.any(|(txn, _, err)| *txn == 1 && *err == MbusError::NoRetriesLeft);
let has_txn_2 = failed_requests
.iter()
.any(|(txn, _, err)| *txn == 2 && *err == MbusError::NoRetriesLeft);
assert!(has_txn_1, "Transaction 1 should have failed");
assert!(has_txn_2, "Transaction 2 should have failed");
}
#[test]
fn test_poll_connection_loss_flushes_pending_requests() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(1).unwrap();
client_services.read_single_coil(1, unit_id, 0).unwrap();
client_services.read_single_coil(2, unit_id, 1).unwrap();
assert_eq!(client_services.expected_responses.len(), 2);
*client_services.transport.is_connected_flag.borrow_mut() = false;
*client_services.transport.recv_error.borrow_mut() = Some(MbusError::ConnectionClosed);
client_services.poll();
assert!(client_services.expected_responses.is_empty());
assert_eq!(client_services.next_timeout_check, None);
let failed_requests = client_services.app().failed_requests.borrow();
assert_eq!(failed_requests.len(), 2);
assert!(
failed_requests
.iter()
.all(|(txn, _, err)| (*txn == 1 || *txn == 2) && *err == MbusError::ConnectionLost)
);
}
#[test]
fn test_fixed_backoff_schedules_and_does_not_retry_early() {
let transport = MockTransport::default();
let app = MockApp::default();
let mut tcp_config = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config.response_timeout_ms = 100;
tcp_config.retry_attempts = 1;
tcp_config.retry_backoff_strategy = BackoffStrategy::Fixed { delay_ms: 50 };
let config = ModbusConfig::Tcp(tcp_config);
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
client_services
.read_single_coil(1, UnitIdOrSlaveAddr::new(1).unwrap(), 0)
.unwrap();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 1);
*client_services.app().current_time.borrow_mut() = 101;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 1);
assert_eq!(
client_services.expected_responses[0].next_retry_timestamp,
Some(151)
);
*client_services.app().current_time.borrow_mut() = 150;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 1);
*client_services.app().current_time.borrow_mut() = 151;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 2);
}
#[test]
fn test_exponential_backoff_growth() {
let transport = MockTransport::default();
let app = MockApp::default();
let mut tcp_config = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config.response_timeout_ms = 100;
tcp_config.retry_attempts = 2;
tcp_config.retry_backoff_strategy = BackoffStrategy::Exponential {
base_delay_ms: 50,
max_delay_ms: 500,
};
let config = ModbusConfig::Tcp(tcp_config);
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
client_services
.read_single_coil(7, UnitIdOrSlaveAddr::new(1).unwrap(), 0)
.unwrap();
*client_services.app().current_time.borrow_mut() = 101;
client_services.poll();
assert_eq!(
client_services.expected_responses[0].next_retry_timestamp,
Some(151)
);
*client_services.app().current_time.borrow_mut() = 151;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 2);
*client_services.app().current_time.borrow_mut() = 252;
client_services.poll();
assert_eq!(
client_services.expected_responses[0].next_retry_timestamp,
Some(352)
);
*client_services.app().current_time.borrow_mut() = 352;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 3);
}
#[test]
fn test_jitter_bounds_with_random_source_lower_bound() {
let transport = MockTransport::default();
let app = MockApp::default();
let mut tcp_config = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config.response_timeout_ms = 100;
tcp_config.retry_attempts = 1;
tcp_config.retry_backoff_strategy = BackoffStrategy::Fixed { delay_ms: 100 };
tcp_config.retry_jitter_strategy = JitterStrategy::Percentage { percent: 20 };
tcp_config.retry_random_fn = Some(rand_zero);
let config = ModbusConfig::Tcp(tcp_config);
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
client_services
.read_single_coil(10, UnitIdOrSlaveAddr::new(1).unwrap(), 0)
.unwrap();
*client_services.app().current_time.borrow_mut() = 101;
client_services.poll();
assert_eq!(
client_services.expected_responses[0].next_retry_timestamp,
Some(181)
);
}
#[test]
fn test_jitter_bounds_with_random_source_upper_bound() {
let transport3 = MockTransport::default();
let app3 = MockApp::default();
let mut tcp_config3 = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config3.response_timeout_ms = 100;
tcp_config3.retry_attempts = 1;
tcp_config3.retry_backoff_strategy = BackoffStrategy::Fixed { delay_ms: 100 };
tcp_config3.retry_jitter_strategy = JitterStrategy::Percentage { percent: 20 };
tcp_config3.retry_random_fn = Some(rand_upper_percent_20);
let config3 = ModbusConfig::Tcp(tcp_config3);
let mut client_services3 =
ClientServices::<MockTransport, MockApp, 10>::new(transport3, app3, config3).unwrap();
client_services3
.read_single_coil(12, UnitIdOrSlaveAddr::new(1).unwrap(), 0)
.unwrap();
*client_services3.app.current_time.borrow_mut() = 101;
client_services3.poll();
assert_eq!(
client_services3.expected_responses[0].next_retry_timestamp,
Some(221)
);
}
#[test]
fn test_jitter_falls_back_without_random_source() {
let transport2 = MockTransport::default();
let app2 = MockApp::default();
let mut tcp_config2 = ModbusTcpConfig::new("127.0.0.1", 502).unwrap();
tcp_config2.response_timeout_ms = 100;
tcp_config2.retry_attempts = 1;
tcp_config2.retry_backoff_strategy = BackoffStrategy::Fixed { delay_ms: 100 };
tcp_config2.retry_jitter_strategy = JitterStrategy::Percentage { percent: 20 };
tcp_config2.retry_random_fn = None;
let config2 = ModbusConfig::Tcp(tcp_config2);
let mut client_services2 =
ClientServices::<MockTransport, MockApp, 10>::new(transport2, app2, config2).unwrap();
client_services2
.read_single_coil(11, UnitIdOrSlaveAddr::new(1).unwrap(), 0)
.unwrap();
*client_services2.app.current_time.borrow_mut() = 101;
client_services2.poll();
assert_eq!(
client_services2.expected_responses[0].next_retry_timestamp,
Some(201)
);
}
#[test]
fn test_serial_retry_scheduling_uses_backoff() {
let transport = MockTransport {
transport_type: Some(TransportType::StdSerial(SerialMode::Rtu)),
..Default::default()
};
let app = MockApp::default();
let serial_config = ModbusSerialConfig {
port_path: heapless::String::<64>::from_str("/dev/ttyUSB0").unwrap(),
mode: SerialMode::Rtu,
baud_rate: BaudRate::Baud9600,
data_bits: mbus_core::transport::DataBits::Eight,
stop_bits: 1,
parity: Parity::None,
response_timeout_ms: 100,
retry_attempts: 1,
retry_backoff_strategy: BackoffStrategy::Fixed { delay_ms: 25 },
retry_jitter_strategy: JitterStrategy::None,
retry_random_fn: None,
};
let mut client_services = ClientServices::<MockTransport, MockApp, 1>::new(
transport,
app,
ModbusConfig::Serial(serial_config),
)
.unwrap();
client_services
.read_single_coil(1, UnitIdOrSlaveAddr::new(1).unwrap(), 0)
.unwrap();
*client_services.app().current_time.borrow_mut() = 101;
client_services.poll();
assert_eq!(
client_services.expected_responses[0].next_retry_timestamp,
Some(126)
);
*client_services.app().current_time.borrow_mut() = 126;
client_services.poll();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 2);
}
#[test]
fn test_too_many_requests_error() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 1>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_multiple_coils(1, unit_id, 0, 1)
.unwrap();
assert_eq!(client_services.expected_responses.len(), 1);
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let result = client_services.read_multiple_coils(2, unit_id, 0, 1);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MbusError::TooManyRequests);
assert_eq!(client_services.expected_responses.len(), 1); }
#[test]
fn test_read_holding_registers_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0005;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 2;
client_services
.read_holding_registers(txn_id, unit_id, address, quantity)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x05, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_client_services_read_holding_registers_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0005;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 2;
client_services
.read_holding_registers(txn_id, unit_id, address, quantity)
.unwrap();
let response_adu = [
0x00, 0x05, 0x00, 0x00, 0x00, 0x07, 0x01, 0x03, 0x04, 0x12, 0x34, 0x56, 0x78,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_holding_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_registers, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_registers.from_address(), address);
assert_eq!(rcv_registers.quantity(), quantity);
assert_eq!(&rcv_registers.values()[..2], &[0x1234, 0x5678]);
assert_eq!(*rcv_quantity, quantity);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_read_input_registers_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0006;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 2;
client_services
.read_input_registers(txn_id, unit_id, address, quantity)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x01, 0x04, 0x00, 0x00, 0x00, 0x02, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_client_services_read_input_registers_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0006;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 2;
client_services
.read_input_registers(txn_id, unit_id, address, quantity)
.unwrap();
let response_adu = [
0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x01, 0x04, 0x04, 0xAA, 0xBB, 0xCC, 0xDD,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_input_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_registers, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_registers.from_address(), address);
assert_eq!(rcv_registers.quantity(), quantity);
assert_eq!(&rcv_registers.values()[..2], &[0xAABB, 0xCCDD]);
assert_eq!(*rcv_quantity, quantity);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_write_single_register_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0007;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0001;
let value = 0x1234;
client_services
.write_single_register(txn_id, unit_id, address, value)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x01, 0x06, 0x00, 0x01, 0x12, 0x34, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_client_services_write_single_register_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0007;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0001;
let value = 0x1234;
client_services
.write_single_register(txn_id, unit_id, address, value)
.unwrap();
let response_adu = [
0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x01, 0x06, 0x00, 0x01, 0x12, 0x34,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_write_single_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_address, rcv_value) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(*rcv_address, address);
assert_eq!(*rcv_value, value);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_write_multiple_registers_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0008;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0001;
let quantity = 2;
let values = [0x1234, 0x5678];
client_services
.write_multiple_registers(txn_id, unit_id, address, quantity, &values)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 17] = [ 0x00, 0x08, 0x00, 0x00, 0x00, 0x0B, 0x01, 0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x12, 0x34, 0x56, 0x78, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_client_services_write_multiple_registers_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0008;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0001;
let quantity = 2;
let values = [0x1234, 0x5678];
client_services
.write_multiple_registers(txn_id, unit_id, address, quantity, &values)
.unwrap();
let response_adu = [
0x00, 0x08, 0x00, 0x00, 0x00, 0x06, 0x01, 0x10, 0x00, 0x01, 0x00, 0x02,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_write_multiple_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_address, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(*rcv_address, address);
assert_eq!(*rcv_quantity, quantity);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_client_services_handles_exception_response() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0009;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 0x0000;
let quantity = 1;
client_services
.read_holding_registers(txn_id, unit_id, address, quantity)
.unwrap();
let exception_adu = [
0x00, 0x09, 0x00, 0x00, 0x00, 0x03, 0x01, 0x83, 0x02, ];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&exception_adu).unwrap())
.unwrap();
client_services.poll();
assert!(
client_services
.app
.received_holding_register_responses
.borrow()
.is_empty()
);
assert_eq!(client_services.app().failed_requests.borrow().len(), 1);
let (failed_txn, failed_unit, failed_err) =
&client_services.app().failed_requests.borrow()[0];
assert_eq!(*failed_txn, txn_id);
assert_eq!(*failed_unit, unit_id);
assert_eq!(*failed_err, MbusError::ModbusException(0x02));
}
#[test]
fn test_serial_exception_coil_response_fails_immediately_with_request_txn_id() {
let mut client_services = make_serial_client();
let txn_id = 0x2001;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let mut values = Coils::new(0x0000, 10).unwrap();
values.set_value(0x0000, true).unwrap();
values.set_value(0x0001, false).unwrap();
values.set_value(0x0002, true).unwrap();
values.set_value(0x0003, false).unwrap();
values.set_value(0x0004, true).unwrap();
values.set_value(0x0005, false).unwrap();
values.set_value(0x0006, true).unwrap();
values.set_value(0x0007, false).unwrap();
values.set_value(0x0008, true).unwrap();
values.set_value(0x0009, false).unwrap();
client_services
.write_multiple_coils(txn_id, unit_id, 0x0000, &values)
.unwrap();
let exception_adu = make_rtu_exception_adu(unit_id, 0x0F, 0x01);
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(exception_adu)
.unwrap();
client_services.poll();
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, txn_id);
assert_eq!(failed[0].1, unit_id);
assert_eq!(failed[0].2, MbusError::ModbusException(0x01));
assert!(
client_services
.app
.received_write_multiple_coils_responses
.borrow()
.is_empty()
);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_serial_exception_register_response_fails_immediately_with_request_txn_id() {
let mut client_services = make_serial_client();
let txn_id = 0x2002;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_holding_registers(txn_id, unit_id, 0x0000, 1)
.unwrap();
let exception_adu = make_rtu_exception_adu(unit_id, 0x03, 0x02);
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(exception_adu)
.unwrap();
client_services.poll();
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, txn_id);
assert_eq!(failed[0].1, unit_id);
assert_eq!(failed[0].2, MbusError::ModbusException(0x02));
assert!(
client_services
.app
.received_holding_register_responses
.borrow()
.is_empty()
);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_serial_exception_discrete_input_response_fails_immediately_with_request_txn_id() {
let mut client_services = make_serial_client();
let txn_id = 0x2003;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_discrete_inputs(txn_id, unit_id, 0x0000, 8)
.unwrap();
let exception_adu = make_rtu_exception_adu(unit_id, 0x02, 0x02);
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(exception_adu)
.unwrap();
client_services.poll();
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, txn_id);
assert_eq!(failed[0].1, unit_id);
assert_eq!(failed[0].2, MbusError::ModbusException(0x02));
assert!(
client_services
.app
.received_discrete_input_responses
.borrow()
.is_empty()
);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_serial_exception_fifo_response_fails_immediately_with_request_txn_id() {
let mut client_services = make_serial_client();
let txn_id = 0x2004;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_fifo_queue(txn_id, unit_id, 0x0001)
.unwrap();
let exception_adu = make_rtu_exception_adu(unit_id, 0x18, 0x01);
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(exception_adu)
.unwrap();
client_services.poll();
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, txn_id);
assert_eq!(failed[0].1, unit_id);
assert_eq!(failed[0].2, MbusError::ModbusException(0x01));
assert!(
client_services
.app
.received_read_fifo_queue_responses
.borrow()
.is_empty()
);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_serial_exception_file_record_response_fails_immediately_with_request_txn_id() {
let mut client_services = make_serial_client();
let txn_id = 0x2005;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let mut sub_req = SubRequest::new();
sub_req.add_read_sub_request(4, 1, 2).unwrap();
client_services
.read_file_record(txn_id, unit_id, &sub_req)
.unwrap();
let exception_adu = make_rtu_exception_adu(unit_id, 0x14, 0x02);
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(exception_adu)
.unwrap();
client_services.poll();
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, txn_id);
assert_eq!(failed[0].1, unit_id);
assert_eq!(failed[0].2, MbusError::ModbusException(0x02));
assert!(
client_services
.app
.received_read_file_record_responses
.borrow()
.is_empty()
);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_serial_exception_diagnostic_response_fails_immediately_with_request_txn_id() {
let mut client_services = make_serial_client();
let txn_id = 0x2006;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_device_identification(
txn_id,
unit_id,
ReadDeviceIdCode::Basic,
ObjectId::from(0x00),
)
.unwrap();
let exception_adu = make_rtu_exception_adu(unit_id, 0x2B, 0x01);
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(exception_adu)
.unwrap();
client_services.poll();
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].0, txn_id);
assert_eq!(failed[0].1, unit_id);
assert_eq!(failed[0].2, MbusError::ModbusException(0x01));
assert!(
client_services
.app
.received_read_device_id_responses
.borrow()
.is_empty()
);
assert!(client_services.expected_responses.is_empty());
}
#[test]
fn test_read_single_holding_register_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_single_holding_register(10, unit_id, 100)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x0A, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x64, 0x00, 0x01, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
assert_eq!(client_services.expected_responses.len(), 1);
let single_read = client_services.expected_responses[0]
.operation_meta
.is_single();
assert!(single_read);
}
#[test]
fn test_client_services_read_single_holding_register_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 10;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 100;
client_services
.read_single_holding_register(txn_id, unit_id, address)
.unwrap();
let response_adu = [
0x00, 0x0A, 0x00, 0x00, 0x00, 0x05, 0x01, 0x03, 0x02, 0x12, 0x34,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_holding_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_registers, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_registers.from_address(), address);
assert_eq!(rcv_registers.quantity(), 1);
assert_eq!(&rcv_registers.values()[..1], &[0x1234]);
assert_eq!(*rcv_quantity, 1);
}
#[test]
fn test_read_single_input_register_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_single_input_register(10, unit_id, 100)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 12] = [
0x00, 0x0A, 0x00, 0x00, 0x00, 0x06, 0x01, 0x04, 0x00, 0x64, 0x00, 0x01, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
assert_eq!(client_services.expected_responses.len(), 1);
let single_read = client_services.expected_responses[0]
.operation_meta
.is_single();
assert!(single_read);
}
#[test]
fn test_client_services_read_single_input_register_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 10;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 100;
client_services
.read_single_input_register(txn_id, unit_id, address)
.unwrap();
let response_adu = [
0x00, 0x0A, 0x00, 0x00, 0x00, 0x05, 0x01, 0x04, 0x02, 0x12, 0x34,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_input_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_registers, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_registers.from_address(), address);
assert_eq!(rcv_registers.quantity(), 1);
assert_eq!(&rcv_registers.values()[..1], &[0x1234]);
assert_eq!(*rcv_quantity, 1);
}
#[test]
fn test_read_write_multiple_registers_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let write_values = [0xAAAA, 0xBBBB];
client_services
.read_write_multiple_registers(11, unit_id, 10, 2, 20, &write_values)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 21] = [
0x00, 0x0B, 0x00, 0x00, 0x00, 0x0F, 0x01, 0x17, 0x00, 0x0A, 0x00, 0x02, 0x00, 0x14, 0x00, 0x02, 0x04, 0xAA, 0xAA, 0xBB, 0xBB, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_mask_write_register_sends_valid_adu() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.mask_write_register(12, unit_id, 30, 0xF0F0, 0x0F0F)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let sent_adu = sent_frames.front().unwrap();
#[rustfmt::skip]
let expected_adu: [u8; 14] = [
0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, 0x01, 0x16, 0x00, 0x1E, 0xF0, 0xF0, 0x0F, 0x0F, ];
assert_eq!(sent_adu.as_slice(), &expected_adu);
}
#[test]
fn test_client_services_read_write_multiple_registers_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 11;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let read_address = 10;
let read_quantity = 2;
let write_address = 20;
let write_values = [0xAAAA, 0xBBBB];
client_services
.read_write_multiple_registers(
txn_id,
unit_id,
read_address,
read_quantity,
write_address,
&write_values,
)
.unwrap();
let response_adu = [
0x00, 0x0B, 0x00, 0x00, 0x00, 0x07, 0x01, 0x17, 0x04, 0x12, 0x34, 0x56, 0x78,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_read_write_multiple_registers_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_registers) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_registers.from_address(), read_address);
assert_eq!(rcv_registers.quantity(), read_quantity);
assert_eq!(&rcv_registers.values()[..2], &[0x1234, 0x5678]);
}
#[test]
fn test_client_services_mask_write_register_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 12;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 30;
let and_mask = 0xF0F0;
let or_mask = 0x0F0F;
client_services
.mask_write_register(txn_id, unit_id, address, and_mask, or_mask)
.unwrap();
let response_adu = [
0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, 0x01, 0x16, 0x00, 0x1E, 0xF0, 0xF0, 0x0F, 0x0F,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_mask_write_register_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
}
#[test]
fn test_client_services_read_fifo_queue_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 13;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 40;
client_services
.read_fifo_queue(txn_id, unit_id, address)
.unwrap();
#[rustfmt::skip]
let response_adu = [
0x00, 0x0D, 0x00, 0x00, 0x00, 0x0A, 0x01, 0x18, 0x00, 0x06, 0x00, 0x02, 0xAA, 0xAA, 0xBB, 0xBB, ];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_read_fifo_queue_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_fifo_queue) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_fifo_queue.length(), 2);
assert_eq!(&rcv_fifo_queue.queue()[..2], &[0xAAAA, 0xBBBB]);
}
#[test]
fn test_client_services_read_file_record_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 14;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let mut sub_req = SubRequest::new();
sub_req.add_read_sub_request(4, 1, 2).unwrap();
client_services
.read_file_record(txn_id, unit_id, &sub_req)
.unwrap();
let response_adu = [
0x00, 0x0E, 0x00, 0x00, 0x00, 0x09, 0x01, 0x14, 0x06, 0x05, 0x06, 0x12, 0x34, 0x56,
0x78,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_read_file_record_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_data) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_data.len(), 1);
assert_eq!(
rcv_data[0].record_data.as_ref().unwrap().as_slice(),
&[0x1234, 0x5678]
);
}
#[test]
fn test_client_services_write_file_record_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 15;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let mut sub_req = SubRequest::new();
let mut data = Vec::new();
data.push(0x1122).unwrap();
sub_req.add_write_sub_request(4, 1, 1, data).unwrap();
client_services
.write_file_record(txn_id, unit_id, &sub_req)
.unwrap();
let response_adu = [
0x00, 0x0F, 0x00, 0x00, 0x00, 0x0C, 0x01, 0x15, 0x09, 0x06, 0x00, 0x04, 0x00, 0x01,
0x00, 0x01, 0x11, 0x22,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_write_file_record_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
}
#[test]
fn test_client_services_read_discrete_inputs_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 16;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 50;
let quantity = 8;
client_services
.read_discrete_inputs(txn_id, unit_id, address, quantity)
.unwrap();
let response_adu = [0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x01, 0xAA];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_discrete_input_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_inputs, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_inputs.from_address(), address);
assert_eq!(rcv_inputs.quantity(), quantity);
assert_eq!(rcv_inputs.values(), &[0xAA]);
assert_eq!(*rcv_quantity, quantity);
}
#[test]
fn test_client_services_read_single_discrete_input_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 17;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let address = 10;
client_services
.read_single_discrete_input(txn_id, unit_id, address)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let expected_request = [
0x00, 0x11, 0x00, 0x00, 0x00, 0x06, 0x01, 0x02, 0x00, 0x0A, 0x00, 0x01,
];
assert_eq!(sent_frames.front().unwrap().as_slice(), &expected_request);
drop(sent_frames);
let response_adu = [0x00, 0x11, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x01, 0x01];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_discrete_input_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_inputs, rcv_quantity) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_inputs.from_address(), address);
assert_eq!(rcv_inputs.quantity(), 1);
assert_eq!(rcv_inputs.value(address).unwrap(), true);
assert_eq!(*rcv_quantity, 1);
}
#[test]
fn test_client_services_read_device_identification_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 20;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let read_code = ReadDeviceIdCode::Basic;
let object_id = ObjectId::from(0x00);
client_services
.read_device_identification(txn_id, unit_id, read_code, object_id)
.unwrap();
let sent_frames = client_services.transport.sent_frames.borrow();
assert_eq!(sent_frames.len(), 1);
let expected_request = [
0x00, 0x14, 0x00, 0x00, 0x00, 0x05, 0x01, 0x2B, 0x0E, 0x01, 0x00,
];
assert_eq!(sent_frames.front().unwrap().as_slice(), &expected_request);
drop(sent_frames);
let response_adu = [
0x00, 0x14, 0x00, 0x00, 0x00, 0x0D, 0x01, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00, 0x01,
0x00, 0x03, 0x46, 0x6F, 0x6F,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
let received_responses = client_services
.app
.received_read_device_id_responses
.borrow();
assert_eq!(received_responses.len(), 1);
let (rcv_txn_id, rcv_unit_id, rcv_resp) = &received_responses[0];
assert_eq!(*rcv_txn_id, txn_id);
assert_eq!(*rcv_unit_id, unit_id);
assert_eq!(rcv_resp.read_device_id_code, ReadDeviceIdCode::Basic);
assert_eq!(
rcv_resp.conformity_level,
ConformityLevel::BasicStreamAndIndividual
);
assert_eq!(rcv_resp.number_of_objects, 1);
assert_eq!(&rcv_resp.objects_data[..5], &[0x00, 0x03, 0x46, 0x6F, 0x6F]);
}
#[test]
fn test_client_services_read_device_identification_multi_transaction() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let txn_id_1 = 21;
client_services
.read_device_identification(
txn_id_1,
unit_id,
ReadDeviceIdCode::Basic,
ObjectId::from(0x00),
)
.unwrap();
let txn_id_2 = 22;
client_services
.read_device_identification(
txn_id_2,
unit_id,
ReadDeviceIdCode::Regular,
ObjectId::from(0x00),
)
.unwrap();
assert_eq!(client_services.transport.sent_frames.borrow().len(), 2);
let response_adu_2 = [
0x00, 0x16, 0x00, 0x00, 0x00, 0x08, 0x01, 0x2B, 0x0E, 0x02, 0x82, 0x00, 0x00, 0x00,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu_2).unwrap())
.unwrap();
client_services.poll();
{
let received_responses = client_services
.app
.received_read_device_id_responses
.borrow();
assert_eq!(received_responses.len(), 1);
assert_eq!(received_responses[0].0, txn_id_2);
assert_eq!(
received_responses[0].2.read_device_id_code,
ReadDeviceIdCode::Regular
);
}
let response_adu_1 = [
0x00, 0x15, 0x00, 0x00, 0x00, 0x08, 0x01, 0x2B, 0x0E, 0x01, 0x81, 0x00, 0x00, 0x00,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu_1).unwrap())
.unwrap();
client_services.poll();
{
let received_responses = client_services
.app
.received_read_device_id_responses
.borrow();
assert_eq!(received_responses.len(), 2);
assert_eq!(received_responses[1].0, txn_id_1);
assert_eq!(
received_responses[1].2.read_device_id_code,
ReadDeviceIdCode::Basic
);
}
}
#[test]
fn test_client_services_read_device_identification_mismatch_code() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 30;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
client_services
.read_device_identification(
txn_id,
unit_id,
ReadDeviceIdCode::Basic,
ObjectId::from(0x00),
)
.unwrap();
let response_adu = [
0x00, 0x1E, 0x00, 0x00, 0x00, 0x08, 0x01, 0x2B, 0x0E, 0x02, 0x81, 0x00, 0x00, 0x00,
];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&response_adu).unwrap())
.unwrap();
client_services.poll();
assert!(
client_services
.app
.received_read_device_id_responses
.borrow()
.is_empty()
);
let failed = client_services.app().failed_requests.borrow();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].2, MbusError::InvalidDeviceIdentification);
}
#[test]
fn test_client_services_read_exception_status_e2e_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 40;
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let err = client_services.read_exception_status(txn_id, unit_id).err();
assert_eq!(err, Some(MbusError::InvalidTransport));
}
#[test]
fn test_client_services_diagnostics_query_data_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let data = [0x1234, 0x5678];
let sub_function = DiagnosticSubFunction::ReturnQueryData;
let err = client_services
.diagnostics(50, unit_id, sub_function, &data)
.err();
assert_eq!(err, Some(MbusError::InvalidTransport));
}
#[test]
fn test_client_services_get_comm_event_counter_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let err = client_services.get_comm_event_counter(60, unit_id).err();
assert_eq!(err, Some(MbusError::InvalidTransport));
}
#[test]
fn test_client_services_report_server_id_success() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let unit_id = UnitIdOrSlaveAddr::new(0x01).unwrap();
let err = client_services.report_server_id(70, unit_id).err();
assert_eq!(err, Some(MbusError::InvalidTransport));
}
#[test]
fn test_broadcast_read_multiple_coils_not_allowed() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0001;
let unit_id = UnitIdOrSlaveAddr::new_broadcast_address();
let address = 0x0000;
let quantity = 8;
let res = client_services.read_multiple_coils(txn_id, unit_id, address, quantity);
assert_eq!(res.unwrap_err(), MbusError::BroadcastNotAllowed);
}
#[test]
fn test_broadcast_write_single_coil_tcp_not_allowed() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0002;
let unit_id = UnitIdOrSlaveAddr::new_broadcast_address();
let res = client_services.write_single_coil(txn_id, unit_id, 0x0000, true);
assert_eq!(res.unwrap_err(), MbusError::BroadcastNotAllowed);
}
#[test]
fn test_broadcast_write_multiple_coils_tcp_not_allowed() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0003;
let unit_id = UnitIdOrSlaveAddr::new_broadcast_address();
let mut values = Coils::new(0x0000, 2).unwrap();
values.set_value(0x0000, true).unwrap();
values.set_value(0x0001, false).unwrap();
let res = client_services.write_multiple_coils(txn_id, unit_id, 0x0000, &values);
assert_eq!(res.unwrap_err(), MbusError::BroadcastNotAllowed);
}
#[test]
fn test_broadcast_read_discrete_inputs_not_allowed() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let txn_id = 0x0006;
let unit_id = UnitIdOrSlaveAddr::new_broadcast_address();
let res = client_services.read_discrete_inputs(txn_id, unit_id, 0x0000, 2);
assert_eq!(res.unwrap_err(), MbusError::BroadcastNotAllowed);
}
#[test]
fn test_client_services_clears_buffer_on_overflow() {
let transport = MockTransport::default();
let app = MockApp::default();
let config = ModbusConfig::Tcp(ModbusTcpConfig::new("127.0.0.1", 502).unwrap());
let mut client_services =
ClientServices::<MockTransport, MockApp, 10>::new(transport, app, config).unwrap();
let initial_garbage = [0xFF; MAX_ADU_FRAME_LEN - 10];
client_services
.rxed_frame
.extend_from_slice(&initial_garbage)
.unwrap();
let chunk = [0xAA; 20];
client_services
.transport
.recv_frames
.borrow_mut()
.push_back(Vec::from_slice(&chunk).unwrap())
.unwrap();
client_services.poll();
assert!(
client_services.rxed_frame.is_empty(),
"Buffer should be cleared on overflow to prevent crashing and recover from stream noise."
);
}
}