From 039d7fabe293a0078b2486c7fa23dfaa4c73f203 Mon Sep 17 00:00:00 2001 From: Marcel Springer Date: Tue, 17 Oct 2023 14:46:10 +0000 Subject: [PATCH 1/3] Adds possibility to specify exceptions in services and extract exceptios in clients --- examples/tcp-client-custom-fn.rs | 7 +- examples/tcp-rtu-server-async.rs | 394 +++++++++++++++++++++++++++++++ src/codec/mod.rs | 156 ++++-------- src/codec/rtu.rs | 5 +- src/codec/tcp.rs | 5 +- src/frame/mod.rs | 243 ++++++++++++++++++- src/lib.rs | 5 +- 7 files changed, 705 insertions(+), 110 deletions(-) create mode 100644 examples/tcp-rtu-server-async.rs diff --git a/examples/tcp-client-custom-fn.rs b/examples/tcp-client-custom-fn.rs index 93a2b481..91293f66 100644 --- a/examples/tcp-client-custom-fn.rs +++ b/examples/tcp-client-custom-fn.rs @@ -5,6 +5,8 @@ use std::borrow::Cow; +use tokio_modbus::FunctionCode; + #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { use tokio_modbus::prelude::*; @@ -15,7 +17,10 @@ async fn main() -> Result<(), Box> { println!("Fetching the coupler ID"); let rsp = ctx - .call(Request::Custom(0x66, Cow::Borrowed(&[0x11, 0x42]))) + .call(Request::Custom( + FunctionCode::Custom(0x66), + Cow::Borrowed(&[0x11, 0x42]), + )) .await?; match rsp { diff --git a/examples/tcp-rtu-server-async.rs b/examples/tcp-rtu-server-async.rs new file mode 100644 index 00000000..609f23d3 --- /dev/null +++ b/examples/tcp-rtu-server-async.rs @@ -0,0 +1,394 @@ +/// This example combines a rtu-server and a tcp-server with the same underlying data structure +/// You can test this on your computer by generating a virtual serial interface with +/// sudo socat -d -d pty,raw,nonblock,echo=0,link=/dev/tty-simu-server pty,raw,echo=0,link=/dev/tty-simu-client +/// +use std::{ + borrow::Cow, collections::HashMap, net::SocketAddr, pin::Pin, sync::Arc, time::Duration, +}; +use tokio::{net::TcpListener, sync::Mutex}; +use tokio_modbus::{ + prelude::*, + server::tcp::{accept_tcp_connection, Server}, + Address, Exception, ExceptionResponse, ExtractExceptionResponse, GetFunctionCode, Quantity, + ResponsePdu, +}; +use tokio_serial::SerialStream; + +pub struct ModbusResult(Result); + +impl Into for ModbusResult { + fn into(self) -> ResponsePdu { + self.0.into() + } +} + +pub struct ExampleData { + pub input_registers: Arc>>, + pub holding_registers: Arc>>, + pub discrete_inputs: Arc>>, + pub coils: Arc>>, +} + +impl ExampleData { + pub async fn read_coils( + &self, + address: Address, + quantity: Quantity, + ) -> Result { + let coils = self.coils.lock().await; + let mut data: Vec = Vec::new(); + for index in 0..quantity { + match coils.get(&(address + index)) { + Some(value) => data.push(*value), + None => return Err(Exception::IllegalDataAddress), + } + } + Ok(Response::ReadCoils(data)) + } + pub async fn read_discrete_inputs( + &self, + address: Address, + quantity: Quantity, + ) -> Result { + let discrete_inputs = self.discrete_inputs.lock().await; + let mut data: Vec = Vec::new(); + for index in 0..quantity { + match discrete_inputs.get(&(address + index)) { + Some(value) => data.push(*value), + None => return Err(Exception::IllegalDataAddress), + } + } + Ok(Response::ReadDiscreteInputs(data)) + } + + pub async fn write_single_coil( + &self, + address: Address, + new_value: bool, + ) -> Result { + let mut coils = self.coils.lock().await; + match coils.get_mut(&address) { + Some(coil) => *coil = new_value, + None => return Err(Exception::IllegalDataAddress), + } + + Ok(Response::WriteSingleCoil(address, new_value)) + } + + pub async fn write_multiple_coils<'a>( + &self, + address: Address, + new_values: Cow<'a, [bool]>, + ) -> Result { + let mut coils = self.coils.lock().await; + // first check that all coils exist + for index in 0..new_values.len() as u16 { + if coils.get_mut(&(address + index)).is_none() { + return Err(Exception::IllegalDataAddress); + } + } + // then write data + for index in 0..new_values.len() { + match coils.get_mut(&(address + index as u16)) { + Some(coil) => *coil = *new_values.get(index).unwrap(), + None => return Err(Exception::IllegalDataAddress), + } + } + + Ok(Response::WriteMultipleCoils( + address, + new_values.len() as u16, + )) + } + + pub async fn read_input_registers( + &self, + address: Address, + quantity: Quantity, + ) -> Result { + let input_registers = self.input_registers.lock().await; + let mut data: Vec = Vec::with_capacity(quantity as usize); + for index in 0..quantity { + match input_registers.get(&(address + index)) { + Some(value) => data.push(*value), + None => return Err(Exception::IllegalDataAddress), + } + } + Ok(Response::ReadInputRegisters(data)) + } + pub async fn read_holding_registers( + &self, + address: Address, + quantity: Quantity, + ) -> Result { + let holding_registers = self.holding_registers.lock().await; + let mut data: Vec = Vec::with_capacity(quantity as usize); + for index in 0..quantity { + match holding_registers.get(&(address + index)) { + Some(value) => data.push(*value), + None => return Err(Exception::IllegalDataAddress), + } + } + Ok(Response::ReadHoldingRegisters(data)) + } + pub async fn write_single_register( + &self, + address: Address, + new_value: u16, + ) -> Result { + let mut holding_registers = self.holding_registers.lock().await; + match holding_registers.get_mut(&address) { + Some(value) => *value = new_value, + None => return Err(Exception::IllegalDataAddress), + } + Ok(Response::WriteSingleRegister(address, new_value)) + } + + pub async fn write_multiple_registers<'a>( + &self, + address: Address, + new_values: Cow<'a, [u16]>, + ) -> Result { + let mut holding_registers = self.holding_registers.lock().await; + // first check that all holding registers exist + for index in 0..new_values.len() as u16 { + if holding_registers.get_mut(&(address + index)).is_none() { + return Err(Exception::IllegalDataAddress); + } + } + // then write data + for index in 0..new_values.len() { + match holding_registers.get_mut(&(address + index as u16)) { + Some(coil) => *coil = *new_values.get(index).unwrap(), + None => return Err(Exception::IllegalDataAddress), + } + } + + Ok(Response::WriteMultipleRegisters( + address, + new_values.len() as u16, + )) + } + + pub async fn restore(&self) { + let mut input_registers = HashMap::new(); + input_registers.insert(0, 1234); + input_registers.insert(1, 5678); + let mut holding_registers = HashMap::new(); + holding_registers.insert(0, 10); + holding_registers.insert(1, 20); + holding_registers.insert(2, 30); + holding_registers.insert(3, 40); + + let mut coils = HashMap::new(); + coils.insert(0, true); + coils.insert(1, true); + coils.insert(2, true); + coils.insert(3, true); + coils.insert(4, false); + coils.insert(5, false); + coils.insert(6, false); + coils.insert(7, false); + + coils.insert(8, true); + coils.insert(9, false); + coils.insert(10, true); + coils.insert(11, false); + coils.insert(12, true); + coils.insert(13, false); + coils.insert(14, true); + coils.insert(15, false); + + *self.input_registers.lock().await = input_registers; + *self.holding_registers.lock().await = holding_registers; + *self.coils.lock().await = coils.clone(); + *self.discrete_inputs.lock().await = coils; + } + + fn new() -> Self { + let data = ExampleData { + input_registers: Arc::new(Mutex::new(HashMap::new())), + holding_registers: Arc::new(Mutex::new(HashMap::new())), + discrete_inputs: Arc::new(Mutex::new(HashMap::new())), + coils: Arc::new(Mutex::new(HashMap::new())), + }; + data + } +} + +impl ExampleData { + pub async fn async_call(&self, req: Request<'static>) -> ModbusResult { + let function_code = req.function_code(); + let result = match req { + Request::ReadCoils(address, quantity) => self.read_coils(address, quantity).await, + Request::ReadDiscreteInputs(address, quantity) => { + self.read_discrete_inputs(address, quantity).await + } + Request::WriteSingleCoil(address, coil) => self.write_single_coil(address, coil).await, + Request::WriteMultipleCoils(address, coils) => { + self.write_multiple_coils(address, coils).await + } + Request::ReadInputRegisters(address, quantity) => { + self.read_input_registers(address, quantity).await + } + Request::ReadHoldingRegisters(address, quantity) => { + self.read_holding_registers(address, quantity).await + } + Request::WriteSingleRegister(address, word) => { + self.write_single_register(address, word).await + } + Request::WriteMultipleRegisters(address, words) => { + self.write_multiple_registers(address, words).await + } + _ => Err(Exception::IllegalFunction), + }; + match result { + Ok(result) => ModbusResult(Ok(result)), + Err(exception) => ModbusResult(Err(ExceptionResponse { + function: function_code, + exception, + })), + } + } +} + +#[derive(Clone)] +pub struct ExampleService { + data: Arc, +} + +impl ExampleService {} + +impl tokio_modbus::server::Service for ExampleService { + type Request = Request<'static>; + type Response = ModbusResult; + type Error = std::io::Error; + type Future = Pin< + Box> + Send + Sync>, + >; + fn call(&self, req: Self::Request) -> Self::Future { + let data = self.data.clone(); + Box::pin(async move { + let response = data.async_call(req).await; + Ok(response) + }) + } +} + +impl ExampleService { + fn new(data: Arc) -> Self { + // Insert some test data as register values. + Self { data } + } +} + +pub async fn tcp_server(socket_addr: SocketAddr, data: Arc) -> anyhow::Result<()> { + let listener = TcpListener::bind(socket_addr).await?; + let server = Server::new(listener); + + let on_connected = move |stream, socket_addr| { + let cloned_data = data.clone(); + let new_service = move |_socket_addr| Ok(Some(ExampleService::new(cloned_data.clone()))); + async move { accept_tcp_connection(stream, socket_addr, new_service) } + }; + let on_process_error = |err| { + eprintln!("{err}"); + }; + server.serve(&on_connected, on_process_error).await?; + Ok(()) +} + +pub async fn rtu_server(tty_path: &str, data: Arc) -> anyhow::Result<()> { + let builder = tokio_serial::new(tty_path, 19200); + let server_serial = tokio_serial::SerialStream::open(&builder).unwrap(); + let server = tokio_modbus::server::rtu::Server::new(server_serial); + let service = ExampleService::new(data); + server.serve_forever(service).await?; + Ok(()) +} + +/// Helper function implementing reading registers from a HashMap. +pub async fn server_context( + socket_addr: SocketAddr, + tty_path: &str, + data: Arc, +) -> anyhow::Result<()> { + let (_, _) = tokio::join!( + tcp_server(socket_addr, data.clone()), + rtu_server(tty_path, data) + ); + + Ok(()) +} + +async fn client_execute(mut ctx: impl Reader + Writer, client_name: &str) { + println!("{client_name}: Reading 2 input registers..."); + let response = ctx.read_input_registers(0x00, 2).await.unwrap(); + println!("{client_name}: The result is '{response:?}'"); + assert_eq!(response, [1234, 5678]); + + println!("{client_name}: Writing 2 holding registers..."); + ctx.write_multiple_registers(0x01, &[7777, 8888]) + .await + .unwrap(); + + // Read back a block including the two registers we wrote. + println!("{client_name}: Reading 4 holding registers..."); + let response = ctx.read_holding_registers(0x00, 4).await.unwrap(); + println!("{client_name}: The result is '{response:?}'"); + assert_eq!(response, [10, 7777, 8888, 40]); + + // Now we try to read with an invalid register address. + // This should return a Modbus exception response with the code + // IllegalDataAddress. + println!("{client_name}: Reading nonexisting holding register address... (should return IllegalDataAddress)"); + let response = ctx.read_holding_registers(0x100, 1).await; + println!("{client_name}: The result is '{response:?}'"); + assert!(response.is_err()); + let maybe_exception_response = response.err().unwrap().exception_response(); + assert!(maybe_exception_response.is_ok()); + let exception_response = maybe_exception_response.ok().unwrap(); + assert_eq!(exception_response.exception, Exception::IllegalDataAddress); + + println!("{client_name}: Done.") +} + +async fn tcp_client_context(socket_addr: SocketAddr) { + let client_name = "TCP-client"; + println!("{client_name}: Connecting client..."); + let ctx = tcp::connect(socket_addr).await.unwrap(); + client_execute(ctx, client_name).await; +} + +async fn rtu_client_context(tty_path: &str) { + let client_name = "RTU-client"; + let slave = Slave(0x17); + + println!("{client_name}: Connecting client..."); + let builder = tokio_serial::new(tty_path, 19200); + let port = SerialStream::open(&builder).unwrap(); + + let ctx = rtu::attach_slave(port, slave); + client_execute(ctx, client_name).await; +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let socket_addr: SocketAddr = "127.0.0.1:5502".parse().unwrap(); + let socket_addr_server = socket_addr.clone(); + let data = Arc::new(ExampleData::new()); + data.restore().await; + let data_cloned = data.clone(); + let server_handle = tokio::task::spawn(async move { + server_context(socket_addr_server, "/dev/tty-simu-server", data_cloned).await + }); + // Give the server some time for starting up + tokio::time::sleep(Duration::from_secs(1)).await; + + tcp_client_context(socket_addr).await; + data.restore().await; + rtu_client_context("/dev/tty-simu-client").await; + server_handle.abort(); + + Ok(()) +} diff --git a/src/codec/mod.rs b/src/codec/mod.rs index 3fd561ea..68639f71 100644 --- a/src/codec/mod.rs +++ b/src/codec/mod.rs @@ -45,7 +45,7 @@ impl<'a> TryFrom> for Bytes { use crate::frame::Request::*; let cnt = request_byte_count(&req); let mut data = BytesMut::with_capacity(cnt); - data.put_u8(req_to_fn_code(&req)); + data.put_u8(req.function_code().into()); match req { ReadCoils(address, quantity) | ReadDiscreteInputs(address, quantity) @@ -121,7 +121,7 @@ impl From for Bytes { use crate::frame::Response::*; let cnt = response_byte_count(&rsp); let mut data = BytesMut::with_capacity(cnt); - data.put_u8(rsp_to_fn_code(&rsp)); + data.put_u8(rsp.function_code().into()); match rsp { ReadCoils(coils) | ReadDiscreteInputs(coils) => { let packed_coils = pack_coils(&coils); @@ -168,8 +168,9 @@ impl From for Bytes { impl From for Bytes { fn from(ex: ExceptionResponse) -> Bytes { let mut data = BytesMut::with_capacity(2); - debug_assert!(ex.function < 0x80); - data.put_u8(ex.function + 0x80); + let function: u8 = ex.function.into(); + debug_assert!(function < 0x80); + data.put_u8(function + 0x80); data.put_u8(ex.exception.into()); data.freeze() } @@ -187,15 +188,19 @@ impl TryFrom for Request<'static> { fn try_from(bytes: Bytes) -> Result { use crate::frame::Request::*; let mut rdr = Cursor::new(&bytes); - let fn_code = rdr.read_u8()?; + let fn_code: FunctionCode = rdr.read_u8()?.into(); let req = match fn_code { - 0x01 => ReadCoils(rdr.read_u16::()?, rdr.read_u16::()?), - 0x02 => ReadDiscreteInputs(rdr.read_u16::()?, rdr.read_u16::()?), - 0x05 => WriteSingleCoil( + FunctionCode::ReadCoils => { + ReadCoils(rdr.read_u16::()?, rdr.read_u16::()?) + } + FunctionCode::ReadDiscreteInputs => { + ReadDiscreteInputs(rdr.read_u16::()?, rdr.read_u16::()?) + } + FunctionCode::WriteSingleCoil => WriteSingleCoil( rdr.read_u16::()?, coil_to_bool(rdr.read_u16::()?)?, ), - 0x0F => { + FunctionCode::WriteMultipleCoils => { let address = rdr.read_u16::()?; let quantity = rdr.read_u16::()?; let byte_count = rdr.read_u8()?; @@ -205,13 +210,17 @@ impl TryFrom for Request<'static> { let x = &bytes[6..]; WriteMultipleCoils(address, unpack_coils(x, quantity).into()) } - 0x04 => ReadInputRegisters(rdr.read_u16::()?, rdr.read_u16::()?), - 0x03 => { + FunctionCode::ReadInputRegisters => { + ReadInputRegisters(rdr.read_u16::()?, rdr.read_u16::()?) + } + FunctionCode::ReadHoldingRegisters => { ReadHoldingRegisters(rdr.read_u16::()?, rdr.read_u16::()?) } - 0x06 => WriteSingleRegister(rdr.read_u16::()?, rdr.read_u16::()?), + FunctionCode::WriteSingleRegister => { + WriteSingleRegister(rdr.read_u16::()?, rdr.read_u16::()?) + } - 0x10 => { + FunctionCode::WriteMultipleRegisters => { let address = rdr.read_u16::()?; let quantity = rdr.read_u16::()?; let byte_count = rdr.read_u8()?; @@ -224,13 +233,13 @@ impl TryFrom for Request<'static> { } WriteMultipleRegisters(address, data.into()) } - 0x16 => { + FunctionCode::MaskWriteRegister => { let address = rdr.read_u16::()?; let and_mask = rdr.read_u16::()?; let or_mask = rdr.read_u16::()?; MaskWriteRegister(address, and_mask, or_mask) } - 0x17 => { + FunctionCode::ReadWriteMultipleRegisters => { let read_address = rdr.read_u16::()?; let read_quantity = rdr.read_u16::()?; let write_address = rdr.read_u16::()?; @@ -245,8 +254,10 @@ impl TryFrom for Request<'static> { } ReadWriteMultipleRegisters(read_address, read_quantity, write_address, data.into()) } - fn_code if fn_code < 0x80 => Custom(fn_code, bytes[1..].to_vec().into()), - fn_code => { + FunctionCode::Custom(fn_code) if fn_code < 0x80 => { + Custom(FunctionCode::Custom(fn_code), bytes[1..].to_vec().into()) + } + FunctionCode::Custom(fn_code) => { return Err(Error::new( ErrorKind::InvalidData, format!("Invalid function code: 0x{fn_code:0>2X}"), @@ -335,7 +346,7 @@ impl TryFrom for Response { } _ => { let mut bytes = bytes; - Custom(fn_code, bytes.split_off(1)) + Custom(FunctionCode::Custom(fn_code), bytes.split_off(1)) } }; Ok(rsp) @@ -355,6 +366,7 @@ impl TryFrom for ExceptionResponse { )); } let function = fn_err_code - 0x80; + let function = FunctionCode::from(function); let exception = Exception::try_from(rdr.read_u8()?)?; Ok(ExceptionResponse { function, @@ -438,41 +450,6 @@ fn unpack_coils(bytes: &[u8], count: u16) -> Vec { res } -fn req_to_fn_code(req: &Request<'_>) -> u8 { - use crate::frame::Request::*; - match *req { - ReadCoils(_, _) => 0x01, - ReadDiscreteInputs(_, _) => 0x02, - WriteSingleCoil(_, _) => 0x05, - WriteMultipleCoils(_, _) => 0x0F, - ReadInputRegisters(_, _) => 0x04, - ReadHoldingRegisters(_, _) => 0x03, - WriteSingleRegister(_, _) => 0x06, - WriteMultipleRegisters(_, _) => 0x10, - MaskWriteRegister(_, _, _) => 0x16, - ReadWriteMultipleRegisters(_, _, _, _) => 0x17, - Custom(code, _) => code, - Disconnect => unreachable!(), - } -} - -fn rsp_to_fn_code(rsp: &Response) -> u8 { - use crate::frame::Response::*; - match *rsp { - ReadCoils(_) => 0x01, - ReadDiscreteInputs(_) => 0x02, - WriteSingleCoil(_, _) => 0x05, - WriteMultipleCoils(_, _) => 0x0F, - ReadInputRegisters(_) => 0x04, - ReadHoldingRegisters(_) => 0x03, - WriteSingleRegister(_, _) => 0x06, - WriteMultipleRegisters(_, _) => 0x10, - MaskWriteRegister(_, _, _) => 0x16, - ReadWriteMultipleRegisters(_) => 0x17, - Custom(code, _) => code, - } -} - fn request_byte_count(req: &Request<'_>) -> usize { use crate::frame::Request::*; match *req { @@ -551,51 +528,10 @@ mod tests { assert_eq!(unpack_coils(&[0xff, 0b11], 10), &[true; 10]); } - #[test] - fn function_code_from_request() { - use crate::frame::Request::*; - assert_eq!(req_to_fn_code(&ReadCoils(0, 0)), 1); - assert_eq!(req_to_fn_code(&ReadDiscreteInputs(0, 0)), 2); - assert_eq!(req_to_fn_code(&WriteSingleCoil(0, true)), 5); - assert_eq!( - req_to_fn_code(&WriteMultipleCoils(0, Cow::Borrowed(&[]))), - 0x0F - ); - assert_eq!(req_to_fn_code(&ReadInputRegisters(0, 0)), 0x04); - assert_eq!(req_to_fn_code(&ReadHoldingRegisters(0, 0)), 0x03); - assert_eq!(req_to_fn_code(&WriteSingleRegister(0, 0)), 0x06); - assert_eq!( - req_to_fn_code(&WriteMultipleRegisters(0, Cow::Borrowed(&[]))), - 0x10 - ); - assert_eq!(req_to_fn_code(&MaskWriteRegister(0, 0, 0)), 0x16); - assert_eq!( - req_to_fn_code(&ReadWriteMultipleRegisters(0, 0, 0, Cow::Borrowed(&[]))), - 0x17 - ); - assert_eq!(req_to_fn_code(&Custom(88, Cow::Borrowed(&[]))), 88); - } - - #[test] - fn function_code_from_response() { - use crate::frame::Response::*; - assert_eq!(rsp_to_fn_code(&ReadCoils(vec![])), 1); - assert_eq!(rsp_to_fn_code(&ReadDiscreteInputs(vec![])), 2); - assert_eq!(rsp_to_fn_code(&WriteSingleCoil(0x0, false)), 5); - assert_eq!(rsp_to_fn_code(&WriteMultipleCoils(0x0, 0x0)), 0x0F); - assert_eq!(rsp_to_fn_code(&ReadInputRegisters(vec![])), 0x04); - assert_eq!(rsp_to_fn_code(&ReadHoldingRegisters(vec![])), 0x03); - assert_eq!(rsp_to_fn_code(&WriteSingleRegister(0, 0)), 0x06); - assert_eq!(rsp_to_fn_code(&WriteMultipleRegisters(0, 0)), 0x10); - assert_eq!(rsp_to_fn_code(&MaskWriteRegister(0, 0, 0)), 0x16); - assert_eq!(rsp_to_fn_code(&ReadWriteMultipleRegisters(vec![])), 0x17); - assert_eq!(rsp_to_fn_code(&Custom(99, Bytes::from_static(&[]))), 99); - } - #[test] fn exception_response_into_bytes() { let bytes: Bytes = ExceptionResponse { - function: 0x03, + function: FunctionCode::ReadHoldingRegisters, exception: Exception::IllegalDataAddress, } .into(); @@ -612,7 +548,7 @@ mod tests { assert_eq!( rsp, ExceptionResponse { - function: 0x03, + function: FunctionCode::ReadHoldingRegisters, exception: Exception::IllegalDataAddress, } ); @@ -623,7 +559,7 @@ mod tests { let req_pdu: Bytes = Request::ReadCoils(0x01, 5).try_into().unwrap(); let rsp_pdu: Bytes = Response::ReadCoils(vec![]).into(); let ex_pdu: Bytes = ExceptionResponse { - function: 0x03, + function: FunctionCode::ReadHoldingRegisters, exception: Exception::ServerDeviceFailure, } .into(); @@ -827,9 +763,12 @@ mod tests { #[test] fn custom() { - let bytes: Bytes = Request::Custom(0x55, Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF])) - .try_into() - .unwrap(); + let bytes: Bytes = Request::Custom( + FunctionCode::Custom(0x55), + Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF]), + ) + .try_into() + .unwrap(); assert_eq!(bytes[0], 0x55); assert_eq!(bytes[1], 0xCC); assert_eq!(bytes[2], 0x88); @@ -960,7 +899,10 @@ mod tests { let req = Request::try_from(bytes).unwrap(); assert_eq!( req, - Request::Custom(0x55, Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF])) + Request::Custom( + FunctionCode::Custom(0x55), + Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF]) + ) ); } } @@ -1072,8 +1014,11 @@ mod tests { #[test] fn custom() { - let bytes: Bytes = - Response::Custom(0x55, Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF])).into(); + let bytes: Bytes = Response::Custom( + FunctionCode::Custom(0x55), + Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF]), + ) + .into(); assert_eq!(bytes[0], 0x55); assert_eq!(bytes[1], 0xCC); assert_eq!(bytes[2], 0x88); @@ -1197,7 +1142,10 @@ mod tests { let rsp = Response::try_from(bytes).unwrap(); assert_eq!( rsp, - Response::Custom(0x55, Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF])) + Response::Custom( + FunctionCode::Custom(0x55), + Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF]) + ) ); } } diff --git a/src/codec/rtu.rs b/src/codec/rtu.rs index d2e60075..444249ab 100644 --- a/src/codec/rtu.rs +++ b/src/codec/rtu.rs @@ -710,7 +710,10 @@ mod tests { let ResponseAdu { pdu, .. } = codec.decode(&mut buf).unwrap().unwrap(); if let ResponsePdu(Err(err)) = pdu { - assert_eq!(format!("{err}"), "Modbus function 2: Illegal data value"); + assert_eq!( + format!("{err}"), + "Modbus function FunctionCode::ReadDiscreteInputs: Illegal data value" + ); assert_eq!(buf.len(), 0); } else { panic!("unexpected response") diff --git a/src/codec/tcp.rs b/src/codec/tcp.rs index 547c0cb1..f379ae41 100644 --- a/src/codec/tcp.rs +++ b/src/codec/tcp.rs @@ -243,7 +243,10 @@ mod tests { assert_eq!(hdr.transaction_id, TRANSACTION_ID); assert_eq!(hdr.unit_id, UNIT_ID); if let ResponsePdu(Err(err)) = pdu { - assert_eq!(format!("{err}"), "Modbus function 2: Illegal data value"); + assert_eq!( + format!("{err}"), + "Modbus function FunctionCode::ReadDiscreteInputs: Illegal data value" + ); assert_eq!(buf.len(), 1); } else { panic!("unexpected response") diff --git a/src/frame/mod.rs b/src/frame/mod.rs index fb516058..03a1e54a 100644 --- a/src/frame/mod.rs +++ b/src/frame/mod.rs @@ -12,7 +12,80 @@ use std::{borrow::Cow, error, fmt}; use crate::bytes::Bytes; /// A Modbus function code is represented by an unsigned 8 bit integer. -pub type FunctionCode = u8; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FunctionCode { + ReadCoils, //=> 0x01, + ReadDiscreteInputs, // => 0x02, + WriteSingleCoil, //(_, _) => 0x05, + WriteMultipleCoils, //(_, _) => 0x0F, + ReadInputRegisters, //(_) => 0x04, + ReadHoldingRegisters, //(_) => 0x03, + WriteSingleRegister, //(_, _) => 0x06, + WriteMultipleRegisters, //(_, _) => 0x10, + MaskWriteRegister, //(_, _, _) => 0x16, + ReadWriteMultipleRegisters, //(_) => 0x17, + Custom(u8), // => code, +} + +impl Into for FunctionCode { + fn into(self) -> u8 { + match self { + FunctionCode::ReadCoils => 0x01, + FunctionCode::ReadDiscreteInputs => 0x02, + FunctionCode::WriteSingleCoil => 0x05, + FunctionCode::WriteMultipleCoils => 0x0f, + FunctionCode::ReadInputRegisters => 0x04, + FunctionCode::ReadHoldingRegisters => 0x03, + FunctionCode::WriteSingleRegister => 0x06, + FunctionCode::WriteMultipleRegisters => 0x10, + FunctionCode::MaskWriteRegister => 0x16, + FunctionCode::ReadWriteMultipleRegisters => 0x17, + FunctionCode::Custom(code) => code, + } + } +} + +impl From for FunctionCode { + fn from(value: u8) -> Self { + match value { + 0x01 => FunctionCode::ReadCoils, + 0x02 => FunctionCode::ReadDiscreteInputs, + 0x05 => FunctionCode::WriteSingleCoil, + 0x0f => FunctionCode::WriteMultipleCoils, + 0x04 => FunctionCode::ReadInputRegisters, + 0x03 => FunctionCode::ReadHoldingRegisters, + 0x06 => FunctionCode::WriteSingleRegister, + 0x10 => FunctionCode::WriteMultipleRegisters, + 0x16 => FunctionCode::MaskWriteRegister, + 0x17 => FunctionCode::ReadWriteMultipleRegisters, + value => FunctionCode::Custom(value), + } + } +} + +impl std::fmt::Display for FunctionCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FunctionCode::ReadCoils => f.write_str("FunctionCode::ReadCoils"), + FunctionCode::ReadDiscreteInputs => f.write_str("FunctionCode::ReadDiscreteInputs"), + FunctionCode::WriteSingleCoil => f.write_str("FunctionCode::WriteSingleCoil"), + FunctionCode::WriteMultipleCoils => f.write_str("FunctionCode::WriteMultipleCoils"), + FunctionCode::ReadInputRegisters => f.write_str("FunctionCode::ReadInputRegisters"), + FunctionCode::ReadHoldingRegisters => f.write_str("FunctionCode::ReadHoldingRegisters"), + FunctionCode::WriteSingleRegister => f.write_str("FunctionCode::WriteSingleRegister"), + FunctionCode::WriteMultipleRegisters => { + f.write_str("FunctionCode::WriteMultipleRegisters") + } + FunctionCode::MaskWriteRegister => f.write_str("FunctionCode::MaskWriteRegister"), + FunctionCode::ReadWriteMultipleRegisters => { + f.write_str("FunctionCode::ReadWriteMultipleRegisters") + } + FunctionCode::Custom(value) => { + f.write_str(format!("FunctionCode::Custom({})", value).as_str()) + } + } + } +} /// A Modbus protocol address is represented by 16 bit from `0` to `65535`. /// @@ -36,6 +109,11 @@ pub(crate) type Word = u16; /// Number of items to process. pub type Quantity = u16; +/// A Trait that is implemented on types that have a function code +pub trait GetFunctionCode { + fn function_code(&self) -> FunctionCode; +} + /// A request represents a message from the client (master) to the server (slave). #[derive(Debug, Clone, PartialEq, Eq)] pub enum Request<'a> { @@ -138,6 +216,30 @@ impl<'a> Request<'a> { Disconnect => Disconnect, } } + + // pub fn into_exception(self, exception: Exception) -> ExceptionResponse{ + + // } +} + +impl<'a> GetFunctionCode for Request<'a> { + fn function_code(&self) -> FunctionCode { + use Request::*; + match *self { + ReadCoils(_, _) => FunctionCode::ReadCoils, + ReadDiscreteInputs(_, _) => FunctionCode::ReadDiscreteInputs, + WriteSingleCoil(_, _) => FunctionCode::WriteSingleCoil, + WriteMultipleCoils(_, _) => FunctionCode::WriteMultipleCoils, + ReadInputRegisters(_, _) => FunctionCode::ReadInputRegisters, + ReadHoldingRegisters(_, _) => FunctionCode::ReadHoldingRegisters, + WriteSingleRegister(_, _) => FunctionCode::WriteSingleRegister, + WriteMultipleRegisters(_, _) => FunctionCode::WriteMultipleRegisters, + MaskWriteRegister(_, _, _) => FunctionCode::MaskWriteRegister, + ReadWriteMultipleRegisters(_, _, _, _) => FunctionCode::ReadWriteMultipleRegisters, + Custom(code, _) => code, + Disconnect => unreachable!(), + } + } } /// A Modbus request with slave included @@ -225,6 +327,25 @@ pub enum Response { Custom(FunctionCode, Bytes), } +impl GetFunctionCode for Response { + fn function_code(&self) -> FunctionCode { + use Response::*; + match *self { + ReadCoils(_) => FunctionCode::ReadCoils, + ReadDiscreteInputs(_) => FunctionCode::ReadDiscreteInputs, + WriteSingleCoil(_, _) => FunctionCode::WriteSingleCoil, + WriteMultipleCoils(_, _) => FunctionCode::WriteMultipleCoils, + ReadInputRegisters(_) => FunctionCode::ReadInputRegisters, + ReadHoldingRegisters(_) => FunctionCode::ReadHoldingRegisters, + WriteSingleRegister(_, _) => FunctionCode::WriteSingleRegister, + WriteMultipleRegisters(_, _) => FunctionCode::WriteMultipleRegisters, + MaskWriteRegister(_, _, _) => FunctionCode::MaskWriteRegister, + ReadWriteMultipleRegisters(_) => FunctionCode::ReadWriteMultipleRegisters, + Custom(code, _) => code, + } + } +} + /// A server (slave) exception. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] @@ -271,6 +392,27 @@ pub struct ExceptionResponse { pub exception: Exception, } +/// Convenience trait for downcasting std::io::Error to ExceptionResponse +pub trait ExtractExceptionResponse { + fn exception_response(self) -> Result; +} + +fn try_downcast_error(error: std::io::Error) -> Result +where + T: error::Error + Send + Sync + 'static, +{ + match error.get_ref().map(|inner| inner.is::()) { + Some(true) => Ok(*error.into_inner().unwrap().downcast::().unwrap()), + _ => Err(error), + } +} + +impl ExtractExceptionResponse for std::io::Error { + fn exception_response(self) -> Result { + try_downcast_error(self) + } +} + /// Represents a message from the client (slave) to the server (master). #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RequestPdu<'a>(pub(crate) Request<'a>); @@ -289,7 +431,7 @@ impl<'a> From> for Request<'a> { /// Represents a message from the server (slave) to the client (master). #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ResponsePdu(pub(crate) Result); +pub struct ResponsePdu(pub(crate) Result); impl From for ResponsePdu { fn from(from: Response) -> Self { @@ -362,3 +504,100 @@ impl error::Error for ExceptionResponse { self.exception.description() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn function_code_from_request() { + use crate::frame::Request::*; + assert_eq!(ReadCoils(0, 0).function_code(), FunctionCode::ReadCoils); + assert_eq!( + ReadDiscreteInputs(0, 0).function_code(), + FunctionCode::ReadDiscreteInputs + ); + assert_eq!( + WriteSingleCoil(0, true).function_code(), + FunctionCode::WriteSingleCoil + ); + assert_eq!( + WriteMultipleCoils(0, Cow::Borrowed(&[])).function_code(), + FunctionCode::WriteMultipleCoils + ); + assert_eq!( + ReadInputRegisters(0, 0).function_code(), + FunctionCode::ReadInputRegisters + ); + assert_eq!( + ReadHoldingRegisters(0, 0).function_code(), + FunctionCode::ReadHoldingRegisters + ); + assert_eq!( + WriteSingleRegister(0, 0).function_code(), + FunctionCode::WriteSingleRegister + ); + assert_eq!( + WriteMultipleRegisters(0, Cow::Borrowed(&[])).function_code(), + FunctionCode::WriteMultipleRegisters + ); + assert_eq!( + MaskWriteRegister(0, 0, 0).function_code(), + FunctionCode::MaskWriteRegister + ); + assert_eq!( + ReadWriteMultipleRegisters(0, 0, 0, Cow::Borrowed(&[])).function_code(), + FunctionCode::ReadWriteMultipleRegisters + ); + assert_eq!( + Custom(FunctionCode::Custom(88), Cow::Borrowed(&[])).function_code(), + FunctionCode::Custom(88) + ); + } + + #[test] + fn function_code_from_response() { + use crate::frame::Response::*; + assert_eq!(ReadCoils(vec![]).function_code(), FunctionCode::ReadCoils); + assert_eq!( + ReadDiscreteInputs(vec![]).function_code(), + FunctionCode::ReadDiscreteInputs + ); + assert_eq!( + WriteSingleCoil(0x0, false).function_code(), + FunctionCode::WriteSingleCoil + ); + assert_eq!( + WriteMultipleCoils(0x0, 0x0).function_code(), + FunctionCode::WriteMultipleCoils + ); + assert_eq!( + ReadInputRegisters(vec![]).function_code(), + FunctionCode::ReadInputRegisters + ); + assert_eq!( + ReadHoldingRegisters(vec![]).function_code(), + FunctionCode::ReadHoldingRegisters + ); + assert_eq!( + WriteSingleRegister(0, 0).function_code(), + FunctionCode::WriteSingleRegister + ); + assert_eq!( + WriteMultipleRegisters(0, 0).function_code(), + FunctionCode::WriteMultipleRegisters + ); + assert_eq!( + MaskWriteRegister(0, 0, 0).function_code(), + FunctionCode::MaskWriteRegister + ); + assert_eq!( + ReadWriteMultipleRegisters(vec![]).function_code(), + FunctionCode::ReadWriteMultipleRegisters + ); + assert_eq!( + Custom(FunctionCode::Custom(99), Bytes::from_static(&[])).function_code(), + FunctionCode::Custom(99) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index c1be935c..42f97461 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,10 @@ pub use self::slave::{Slave, SlaveId}; mod codec; mod frame; -pub use self::frame::{Address, FunctionCode, Quantity, Request, Response}; +pub use self::frame::{ + Address, Exception, ExceptionResponse, ExtractExceptionResponse, FunctionCode, GetFunctionCode, + Quantity, Request, Response, ResponsePdu, +}; mod service; From df5a7f879ed9befa2fb496a8f93fdccd67f3648a Mon Sep 17 00:00:00 2001 From: Marcel Springer Date: Tue, 17 Oct 2023 15:35:09 +0000 Subject: [PATCH 2/3] fixes failed pre-commit hook in ci pipeline --- examples/tcp-rtu-server-async.rs | 31 +++++++++++++++++-------------- src/frame/mod.rs | 15 +++++++++------ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/examples/tcp-rtu-server-async.rs b/examples/tcp-rtu-server-async.rs index 609f23d3..3a807f77 100644 --- a/examples/tcp-rtu-server-async.rs +++ b/examples/tcp-rtu-server-async.rs @@ -1,7 +1,12 @@ -/// This example combines a rtu-server and a tcp-server with the same underlying data structure -/// You can test this on your computer by generating a virtual serial interface with -/// sudo socat -d -d pty,raw,nonblock,echo=0,link=/dev/tty-simu-server pty,raw,echo=0,link=/dev/tty-simu-client -/// +// SPDX-FileCopyrightText: Copyright (c) 2017-2023 slowtec GmbH +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! # TCP / RTU server with shared data +//! +//! This example combines a rtu-server and a tcp-server with the same underlying data structure. +//! You can test this on your computer by generating a virtual serial interface with: +//! sudo socat -d -d pty,raw,nonblock,echo=0,link=/dev/tty-simu-server pty,raw,echo=0,link=/dev/tty-simu-client +//! use std::{ borrow::Cow, collections::HashMap, net::SocketAddr, pin::Pin, sync::Arc, time::Duration, }; @@ -16,9 +21,9 @@ use tokio_serial::SerialStream; pub struct ModbusResult(Result); -impl Into for ModbusResult { - fn into(self) -> ResponsePdu { - self.0.into() +impl From for ResponsePdu { + fn from(value: ModbusResult) -> Self { + value.0.into() } } @@ -206,13 +211,12 @@ impl ExampleData { } fn new() -> Self { - let data = ExampleData { + ExampleData { input_registers: Arc::new(Mutex::new(HashMap::new())), holding_registers: Arc::new(Mutex::new(HashMap::new())), discrete_inputs: Arc::new(Mutex::new(HashMap::new())), coils: Arc::new(Mutex::new(HashMap::new())), - }; - data + } } } @@ -375,12 +379,11 @@ async fn rtu_client_context(tty_path: &str) { #[tokio::main] async fn main() -> Result<(), Box> { let socket_addr: SocketAddr = "127.0.0.1:5502".parse().unwrap(); - let socket_addr_server = socket_addr.clone(); let data = Arc::new(ExampleData::new()); data.restore().await; - let data_cloned = data.clone(); - let server_handle = tokio::task::spawn(async move { - server_context(socket_addr_server, "/dev/tty-simu-server", data_cloned).await + let server_handle = tokio::task::spawn({ + let data = data.clone(); + async move { server_context(socket_addr, "/dev/tty-simu-server", data).await } }); // Give the server some time for starting up tokio::time::sleep(Duration::from_secs(1)).await; diff --git a/src/frame/mod.rs b/src/frame/mod.rs index 03a1e54a..4f957a3c 100644 --- a/src/frame/mod.rs +++ b/src/frame/mod.rs @@ -27,9 +27,9 @@ pub enum FunctionCode { Custom(u8), // => code, } -impl Into for FunctionCode { - fn into(self) -> u8 { - match self { +impl From for u8 { + fn from(val: FunctionCode) -> u8 { + match val { FunctionCode::ReadCoils => 0x01, FunctionCode::ReadDiscreteInputs => 0x02, FunctionCode::WriteSingleCoil => 0x05, @@ -81,7 +81,7 @@ impl std::fmt::Display for FunctionCode { f.write_str("FunctionCode::ReadWriteMultipleRegisters") } FunctionCode::Custom(value) => { - f.write_str(format!("FunctionCode::Custom({})", value).as_str()) + f.write_str(format!("FunctionCode::Custom({value})").as_str()) } } } @@ -392,7 +392,7 @@ pub struct ExceptionResponse { pub exception: Exception, } -/// Convenience trait for downcasting std::io::Error to ExceptionResponse +/// Convenience trait for downcasting `std::io::Error` to `ExceptionResponse` pub trait ExtractExceptionResponse { fn exception_response(self) -> Result; } @@ -401,7 +401,10 @@ fn try_downcast_error(error: std::io::Error) -> Result where T: error::Error + Send + Sync + 'static, { - match error.get_ref().map(|inner| inner.is::()) { + match error + .get_ref() + .map(<(dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static)>::is::) + { Some(true) => Ok(*error.into_inner().unwrap().downcast::().unwrap()), _ => Err(error), } From 0aa5d11811b6bbba57d0dab1e447a6a867f1186d Mon Sep 17 00:00:00 2001 From: Marcel Springer Date: Wed, 18 Oct 2023 05:48:04 +0000 Subject: [PATCH 3/3] reverted FunctionCode newtype --- examples/tcp-client-custom-fn.rs | 7 +- examples/tcp-rtu-server-async.rs | 3 +- src/codec/mod.rs | 156 +++++++++++++------- src/codec/rtu.rs | 5 +- src/codec/tcp.rs | 5 +- src/frame/mod.rs | 238 +++---------------------------- src/lib.rs | 4 +- 7 files changed, 130 insertions(+), 288 deletions(-) diff --git a/examples/tcp-client-custom-fn.rs b/examples/tcp-client-custom-fn.rs index 91293f66..93a2b481 100644 --- a/examples/tcp-client-custom-fn.rs +++ b/examples/tcp-client-custom-fn.rs @@ -5,8 +5,6 @@ use std::borrow::Cow; -use tokio_modbus::FunctionCode; - #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { use tokio_modbus::prelude::*; @@ -17,10 +15,7 @@ async fn main() -> Result<(), Box> { println!("Fetching the coupler ID"); let rsp = ctx - .call(Request::Custom( - FunctionCode::Custom(0x66), - Cow::Borrowed(&[0x11, 0x42]), - )) + .call(Request::Custom(0x66, Cow::Borrowed(&[0x11, 0x42]))) .await?; match rsp { diff --git a/examples/tcp-rtu-server-async.rs b/examples/tcp-rtu-server-async.rs index 3a807f77..212576c8 100644 --- a/examples/tcp-rtu-server-async.rs +++ b/examples/tcp-rtu-server-async.rs @@ -14,8 +14,7 @@ use tokio::{net::TcpListener, sync::Mutex}; use tokio_modbus::{ prelude::*, server::tcp::{accept_tcp_connection, Server}, - Address, Exception, ExceptionResponse, ExtractExceptionResponse, GetFunctionCode, Quantity, - ResponsePdu, + Address, Exception, ExceptionResponse, ExtractExceptionResponse, Quantity, ResponsePdu, }; use tokio_serial::SerialStream; diff --git a/src/codec/mod.rs b/src/codec/mod.rs index 68639f71..d90271da 100644 --- a/src/codec/mod.rs +++ b/src/codec/mod.rs @@ -45,7 +45,7 @@ impl<'a> TryFrom> for Bytes { use crate::frame::Request::*; let cnt = request_byte_count(&req); let mut data = BytesMut::with_capacity(cnt); - data.put_u8(req.function_code().into()); + data.put_u8(req_to_fn_code(&req)); match req { ReadCoils(address, quantity) | ReadDiscreteInputs(address, quantity) @@ -121,7 +121,7 @@ impl From for Bytes { use crate::frame::Response::*; let cnt = response_byte_count(&rsp); let mut data = BytesMut::with_capacity(cnt); - data.put_u8(rsp.function_code().into()); + data.put_u8(rsp_to_fn_code(&rsp)); match rsp { ReadCoils(coils) | ReadDiscreteInputs(coils) => { let packed_coils = pack_coils(&coils); @@ -168,9 +168,8 @@ impl From for Bytes { impl From for Bytes { fn from(ex: ExceptionResponse) -> Bytes { let mut data = BytesMut::with_capacity(2); - let function: u8 = ex.function.into(); - debug_assert!(function < 0x80); - data.put_u8(function + 0x80); + debug_assert!(ex.function < 0x80); + data.put_u8(ex.function + 0x80); data.put_u8(ex.exception.into()); data.freeze() } @@ -188,19 +187,15 @@ impl TryFrom for Request<'static> { fn try_from(bytes: Bytes) -> Result { use crate::frame::Request::*; let mut rdr = Cursor::new(&bytes); - let fn_code: FunctionCode = rdr.read_u8()?.into(); + let fn_code = rdr.read_u8()?; let req = match fn_code { - FunctionCode::ReadCoils => { - ReadCoils(rdr.read_u16::()?, rdr.read_u16::()?) - } - FunctionCode::ReadDiscreteInputs => { - ReadDiscreteInputs(rdr.read_u16::()?, rdr.read_u16::()?) - } - FunctionCode::WriteSingleCoil => WriteSingleCoil( + 0x01 => ReadCoils(rdr.read_u16::()?, rdr.read_u16::()?), + 0x02 => ReadDiscreteInputs(rdr.read_u16::()?, rdr.read_u16::()?), + 0x05 => WriteSingleCoil( rdr.read_u16::()?, coil_to_bool(rdr.read_u16::()?)?, ), - FunctionCode::WriteMultipleCoils => { + 0x0F => { let address = rdr.read_u16::()?; let quantity = rdr.read_u16::()?; let byte_count = rdr.read_u8()?; @@ -210,17 +205,13 @@ impl TryFrom for Request<'static> { let x = &bytes[6..]; WriteMultipleCoils(address, unpack_coils(x, quantity).into()) } - FunctionCode::ReadInputRegisters => { - ReadInputRegisters(rdr.read_u16::()?, rdr.read_u16::()?) - } - FunctionCode::ReadHoldingRegisters => { + 0x04 => ReadInputRegisters(rdr.read_u16::()?, rdr.read_u16::()?), + 0x03 => { ReadHoldingRegisters(rdr.read_u16::()?, rdr.read_u16::()?) } - FunctionCode::WriteSingleRegister => { - WriteSingleRegister(rdr.read_u16::()?, rdr.read_u16::()?) - } + 0x06 => WriteSingleRegister(rdr.read_u16::()?, rdr.read_u16::()?), - FunctionCode::WriteMultipleRegisters => { + 0x10 => { let address = rdr.read_u16::()?; let quantity = rdr.read_u16::()?; let byte_count = rdr.read_u8()?; @@ -233,13 +224,13 @@ impl TryFrom for Request<'static> { } WriteMultipleRegisters(address, data.into()) } - FunctionCode::MaskWriteRegister => { + 0x16 => { let address = rdr.read_u16::()?; let and_mask = rdr.read_u16::()?; let or_mask = rdr.read_u16::()?; MaskWriteRegister(address, and_mask, or_mask) } - FunctionCode::ReadWriteMultipleRegisters => { + 0x17 => { let read_address = rdr.read_u16::()?; let read_quantity = rdr.read_u16::()?; let write_address = rdr.read_u16::()?; @@ -254,10 +245,8 @@ impl TryFrom for Request<'static> { } ReadWriteMultipleRegisters(read_address, read_quantity, write_address, data.into()) } - FunctionCode::Custom(fn_code) if fn_code < 0x80 => { - Custom(FunctionCode::Custom(fn_code), bytes[1..].to_vec().into()) - } - FunctionCode::Custom(fn_code) => { + fn_code if fn_code < 0x80 => Custom(fn_code, bytes[1..].to_vec().into()), + fn_code => { return Err(Error::new( ErrorKind::InvalidData, format!("Invalid function code: 0x{fn_code:0>2X}"), @@ -346,7 +335,7 @@ impl TryFrom for Response { } _ => { let mut bytes = bytes; - Custom(FunctionCode::Custom(fn_code), bytes.split_off(1)) + Custom(fn_code, bytes.split_off(1)) } }; Ok(rsp) @@ -366,7 +355,6 @@ impl TryFrom for ExceptionResponse { )); } let function = fn_err_code - 0x80; - let function = FunctionCode::from(function); let exception = Exception::try_from(rdr.read_u8()?)?; Ok(ExceptionResponse { function, @@ -450,6 +438,41 @@ fn unpack_coils(bytes: &[u8], count: u16) -> Vec { res } +pub(crate) const fn req_to_fn_code(req: &Request<'_>) -> u8 { + use crate::frame::Request::*; + match *req { + ReadCoils(_, _) => 0x01, + ReadDiscreteInputs(_, _) => 0x02, + WriteSingleCoil(_, _) => 0x05, + WriteMultipleCoils(_, _) => 0x0F, + ReadInputRegisters(_, _) => 0x04, + ReadHoldingRegisters(_, _) => 0x03, + WriteSingleRegister(_, _) => 0x06, + WriteMultipleRegisters(_, _) => 0x10, + MaskWriteRegister(_, _, _) => 0x16, + ReadWriteMultipleRegisters(_, _, _, _) => 0x17, + Custom(code, _) => code, + Disconnect => unreachable!(), + } +} + +const fn rsp_to_fn_code(rsp: &Response) -> u8 { + use crate::frame::Response::*; + match *rsp { + ReadCoils(_) => 0x01, + ReadDiscreteInputs(_) => 0x02, + WriteSingleCoil(_, _) => 0x05, + WriteMultipleCoils(_, _) => 0x0F, + ReadInputRegisters(_) => 0x04, + ReadHoldingRegisters(_) => 0x03, + WriteSingleRegister(_, _) => 0x06, + WriteMultipleRegisters(_, _) => 0x10, + MaskWriteRegister(_, _, _) => 0x16, + ReadWriteMultipleRegisters(_) => 0x17, + Custom(code, _) => code, + } +} + fn request_byte_count(req: &Request<'_>) -> usize { use crate::frame::Request::*; match *req { @@ -528,10 +551,51 @@ mod tests { assert_eq!(unpack_coils(&[0xff, 0b11], 10), &[true; 10]); } + #[test] + fn function_code_from_request() { + use crate::frame::Request::*; + assert_eq!(req_to_fn_code(&ReadCoils(0, 0)), 1); + assert_eq!(req_to_fn_code(&ReadDiscreteInputs(0, 0)), 2); + assert_eq!(req_to_fn_code(&WriteSingleCoil(0, true)), 5); + assert_eq!( + req_to_fn_code(&WriteMultipleCoils(0, Cow::Borrowed(&[]))), + 0x0F + ); + assert_eq!(req_to_fn_code(&ReadInputRegisters(0, 0)), 0x04); + assert_eq!(req_to_fn_code(&ReadHoldingRegisters(0, 0)), 0x03); + assert_eq!(req_to_fn_code(&WriteSingleRegister(0, 0)), 0x06); + assert_eq!( + req_to_fn_code(&WriteMultipleRegisters(0, Cow::Borrowed(&[]))), + 0x10 + ); + assert_eq!(req_to_fn_code(&MaskWriteRegister(0, 0, 0)), 0x16); + assert_eq!( + req_to_fn_code(&ReadWriteMultipleRegisters(0, 0, 0, Cow::Borrowed(&[]))), + 0x17 + ); + assert_eq!(req_to_fn_code(&Custom(88, Cow::Borrowed(&[]))), 88); + } + + #[test] + fn function_code_from_response() { + use crate::frame::Response::*; + assert_eq!(rsp_to_fn_code(&ReadCoils(vec![])), 1); + assert_eq!(rsp_to_fn_code(&ReadDiscreteInputs(vec![])), 2); + assert_eq!(rsp_to_fn_code(&WriteSingleCoil(0x0, false)), 5); + assert_eq!(rsp_to_fn_code(&WriteMultipleCoils(0x0, 0x0)), 0x0F); + assert_eq!(rsp_to_fn_code(&ReadInputRegisters(vec![])), 0x04); + assert_eq!(rsp_to_fn_code(&ReadHoldingRegisters(vec![])), 0x03); + assert_eq!(rsp_to_fn_code(&WriteSingleRegister(0, 0)), 0x06); + assert_eq!(rsp_to_fn_code(&WriteMultipleRegisters(0, 0)), 0x10); + assert_eq!(rsp_to_fn_code(&MaskWriteRegister(0, 0, 0)), 0x16); + assert_eq!(rsp_to_fn_code(&ReadWriteMultipleRegisters(vec![])), 0x17); + assert_eq!(rsp_to_fn_code(&Custom(99, Bytes::from_static(&[]))), 99); + } + #[test] fn exception_response_into_bytes() { let bytes: Bytes = ExceptionResponse { - function: FunctionCode::ReadHoldingRegisters, + function: 0x03, exception: Exception::IllegalDataAddress, } .into(); @@ -548,7 +612,7 @@ mod tests { assert_eq!( rsp, ExceptionResponse { - function: FunctionCode::ReadHoldingRegisters, + function: 0x03, exception: Exception::IllegalDataAddress, } ); @@ -559,7 +623,7 @@ mod tests { let req_pdu: Bytes = Request::ReadCoils(0x01, 5).try_into().unwrap(); let rsp_pdu: Bytes = Response::ReadCoils(vec![]).into(); let ex_pdu: Bytes = ExceptionResponse { - function: FunctionCode::ReadHoldingRegisters, + function: 0x03, exception: Exception::ServerDeviceFailure, } .into(); @@ -763,12 +827,9 @@ mod tests { #[test] fn custom() { - let bytes: Bytes = Request::Custom( - FunctionCode::Custom(0x55), - Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF]), - ) - .try_into() - .unwrap(); + let bytes: Bytes = Request::Custom(0x55, Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF])) + .try_into() + .unwrap(); assert_eq!(bytes[0], 0x55); assert_eq!(bytes[1], 0xCC); assert_eq!(bytes[2], 0x88); @@ -899,10 +960,7 @@ mod tests { let req = Request::try_from(bytes).unwrap(); assert_eq!( req, - Request::Custom( - FunctionCode::Custom(0x55), - Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF]) - ) + Request::Custom(0x55, Cow::Borrowed(&[0xCC, 0x88, 0xAA, 0xFF])) ); } } @@ -1014,11 +1072,8 @@ mod tests { #[test] fn custom() { - let bytes: Bytes = Response::Custom( - FunctionCode::Custom(0x55), - Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF]), - ) - .into(); + let bytes: Bytes = + Response::Custom(0x55, Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF])).into(); assert_eq!(bytes[0], 0x55); assert_eq!(bytes[1], 0xCC); assert_eq!(bytes[2], 0x88); @@ -1142,10 +1197,7 @@ mod tests { let rsp = Response::try_from(bytes).unwrap(); assert_eq!( rsp, - Response::Custom( - FunctionCode::Custom(0x55), - Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF]) - ) + Response::Custom(0x55, Bytes::from_static(&[0xCC, 0x88, 0xAA, 0xFF])) ); } } diff --git a/src/codec/rtu.rs b/src/codec/rtu.rs index 444249ab..d2e60075 100644 --- a/src/codec/rtu.rs +++ b/src/codec/rtu.rs @@ -710,10 +710,7 @@ mod tests { let ResponseAdu { pdu, .. } = codec.decode(&mut buf).unwrap().unwrap(); if let ResponsePdu(Err(err)) = pdu { - assert_eq!( - format!("{err}"), - "Modbus function FunctionCode::ReadDiscreteInputs: Illegal data value" - ); + assert_eq!(format!("{err}"), "Modbus function 2: Illegal data value"); assert_eq!(buf.len(), 0); } else { panic!("unexpected response") diff --git a/src/codec/tcp.rs b/src/codec/tcp.rs index f379ae41..547c0cb1 100644 --- a/src/codec/tcp.rs +++ b/src/codec/tcp.rs @@ -243,10 +243,7 @@ mod tests { assert_eq!(hdr.transaction_id, TRANSACTION_ID); assert_eq!(hdr.unit_id, UNIT_ID); if let ResponsePdu(Err(err)) = pdu { - assert_eq!( - format!("{err}"), - "Modbus function FunctionCode::ReadDiscreteInputs: Illegal data value" - ); + assert_eq!(format!("{err}"), "Modbus function 2: Illegal data value"); assert_eq!(buf.len(), 1); } else { panic!("unexpected response") diff --git a/src/frame/mod.rs b/src/frame/mod.rs index 4f957a3c..6bc198c6 100644 --- a/src/frame/mod.rs +++ b/src/frame/mod.rs @@ -9,83 +9,10 @@ pub(crate) mod tcp; use std::{borrow::Cow, error, fmt}; -use crate::bytes::Bytes; +use crate::{bytes::Bytes, codec::req_to_fn_code}; /// A Modbus function code is represented by an unsigned 8 bit integer. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FunctionCode { - ReadCoils, //=> 0x01, - ReadDiscreteInputs, // => 0x02, - WriteSingleCoil, //(_, _) => 0x05, - WriteMultipleCoils, //(_, _) => 0x0F, - ReadInputRegisters, //(_) => 0x04, - ReadHoldingRegisters, //(_) => 0x03, - WriteSingleRegister, //(_, _) => 0x06, - WriteMultipleRegisters, //(_, _) => 0x10, - MaskWriteRegister, //(_, _, _) => 0x16, - ReadWriteMultipleRegisters, //(_) => 0x17, - Custom(u8), // => code, -} - -impl From for u8 { - fn from(val: FunctionCode) -> u8 { - match val { - FunctionCode::ReadCoils => 0x01, - FunctionCode::ReadDiscreteInputs => 0x02, - FunctionCode::WriteSingleCoil => 0x05, - FunctionCode::WriteMultipleCoils => 0x0f, - FunctionCode::ReadInputRegisters => 0x04, - FunctionCode::ReadHoldingRegisters => 0x03, - FunctionCode::WriteSingleRegister => 0x06, - FunctionCode::WriteMultipleRegisters => 0x10, - FunctionCode::MaskWriteRegister => 0x16, - FunctionCode::ReadWriteMultipleRegisters => 0x17, - FunctionCode::Custom(code) => code, - } - } -} - -impl From for FunctionCode { - fn from(value: u8) -> Self { - match value { - 0x01 => FunctionCode::ReadCoils, - 0x02 => FunctionCode::ReadDiscreteInputs, - 0x05 => FunctionCode::WriteSingleCoil, - 0x0f => FunctionCode::WriteMultipleCoils, - 0x04 => FunctionCode::ReadInputRegisters, - 0x03 => FunctionCode::ReadHoldingRegisters, - 0x06 => FunctionCode::WriteSingleRegister, - 0x10 => FunctionCode::WriteMultipleRegisters, - 0x16 => FunctionCode::MaskWriteRegister, - 0x17 => FunctionCode::ReadWriteMultipleRegisters, - value => FunctionCode::Custom(value), - } - } -} - -impl std::fmt::Display for FunctionCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - FunctionCode::ReadCoils => f.write_str("FunctionCode::ReadCoils"), - FunctionCode::ReadDiscreteInputs => f.write_str("FunctionCode::ReadDiscreteInputs"), - FunctionCode::WriteSingleCoil => f.write_str("FunctionCode::WriteSingleCoil"), - FunctionCode::WriteMultipleCoils => f.write_str("FunctionCode::WriteMultipleCoils"), - FunctionCode::ReadInputRegisters => f.write_str("FunctionCode::ReadInputRegisters"), - FunctionCode::ReadHoldingRegisters => f.write_str("FunctionCode::ReadHoldingRegisters"), - FunctionCode::WriteSingleRegister => f.write_str("FunctionCode::WriteSingleRegister"), - FunctionCode::WriteMultipleRegisters => { - f.write_str("FunctionCode::WriteMultipleRegisters") - } - FunctionCode::MaskWriteRegister => f.write_str("FunctionCode::MaskWriteRegister"), - FunctionCode::ReadWriteMultipleRegisters => { - f.write_str("FunctionCode::ReadWriteMultipleRegisters") - } - FunctionCode::Custom(value) => { - f.write_str(format!("FunctionCode::Custom({value})").as_str()) - } - } - } -} +pub type FunctionCode = u8; /// A Modbus protocol address is represented by 16 bit from `0` to `65535`. /// @@ -109,11 +36,6 @@ pub(crate) type Word = u16; /// Number of items to process. pub type Quantity = u16; -/// A Trait that is implemented on types that have a function code -pub trait GetFunctionCode { - fn function_code(&self) -> FunctionCode; -} - /// A request represents a message from the client (master) to the server (slave). #[derive(Debug, Clone, PartialEq, Eq)] pub enum Request<'a> { @@ -217,28 +139,9 @@ impl<'a> Request<'a> { } } - // pub fn into_exception(self, exception: Exception) -> ExceptionResponse{ - - // } -} - -impl<'a> GetFunctionCode for Request<'a> { - fn function_code(&self) -> FunctionCode { - use Request::*; - match *self { - ReadCoils(_, _) => FunctionCode::ReadCoils, - ReadDiscreteInputs(_, _) => FunctionCode::ReadDiscreteInputs, - WriteSingleCoil(_, _) => FunctionCode::WriteSingleCoil, - WriteMultipleCoils(_, _) => FunctionCode::WriteMultipleCoils, - ReadInputRegisters(_, _) => FunctionCode::ReadInputRegisters, - ReadHoldingRegisters(_, _) => FunctionCode::ReadHoldingRegisters, - WriteSingleRegister(_, _) => FunctionCode::WriteSingleRegister, - WriteMultipleRegisters(_, _) => FunctionCode::WriteMultipleRegisters, - MaskWriteRegister(_, _, _) => FunctionCode::MaskWriteRegister, - ReadWriteMultipleRegisters(_, _, _, _) => FunctionCode::ReadWriteMultipleRegisters, - Custom(code, _) => code, - Disconnect => unreachable!(), - } + #[must_use] + pub fn function_code(&self) -> FunctionCode { + req_to_fn_code(self) } } @@ -327,25 +230,6 @@ pub enum Response { Custom(FunctionCode, Bytes), } -impl GetFunctionCode for Response { - fn function_code(&self) -> FunctionCode { - use Response::*; - match *self { - ReadCoils(_) => FunctionCode::ReadCoils, - ReadDiscreteInputs(_) => FunctionCode::ReadDiscreteInputs, - WriteSingleCoil(_, _) => FunctionCode::WriteSingleCoil, - WriteMultipleCoils(_, _) => FunctionCode::WriteMultipleCoils, - ReadInputRegisters(_) => FunctionCode::ReadInputRegisters, - ReadHoldingRegisters(_) => FunctionCode::ReadHoldingRegisters, - WriteSingleRegister(_, _) => FunctionCode::WriteSingleRegister, - WriteMultipleRegisters(_, _) => FunctionCode::WriteMultipleRegisters, - MaskWriteRegister(_, _, _) => FunctionCode::MaskWriteRegister, - ReadWriteMultipleRegisters(_) => FunctionCode::ReadWriteMultipleRegisters, - Custom(code, _) => code, - } - } -} - /// A server (slave) exception. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] @@ -393,6 +277,21 @@ pub struct ExceptionResponse { } /// Convenience trait for downcasting `std::io::Error` to `ExceptionResponse` +/// An Easy way to extract an `ExceptionResponse` from `std::io::Error` +/// ``` +/// use tokio_modbus::{Exception, ExceptionResponse}; +/// use tokio_modbus::client::Reader; +/// use tokio_modbus::ExtractExceptionResponse; +/// async fn read_holding_registeres(mut ctx: impl Reader){ +/// let response: Result, std::io::Error> = ctx.read_holding_registers(0x100, 1).await; +/// assert!(response.is_err()); +/// // The call to exception_response() extracts the `ExceptionsResponse`` if it is one. Otherwise the `std::io::Error` is returned +/// let maybe_exception_response: Result = response.err().unwrap().exception_response(); +/// assert!(maybe_exception_response.is_ok()); +/// let exception_response: ExceptionResponse = maybe_exception_response.ok().unwrap(); +/// assert_eq!(exception_response.exception, Exception::IllegalDataAddress); +/// } +/// ``` pub trait ExtractExceptionResponse { fn exception_response(self) -> Result; } @@ -507,100 +406,3 @@ impl error::Error for ExceptionResponse { self.exception.description() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn function_code_from_request() { - use crate::frame::Request::*; - assert_eq!(ReadCoils(0, 0).function_code(), FunctionCode::ReadCoils); - assert_eq!( - ReadDiscreteInputs(0, 0).function_code(), - FunctionCode::ReadDiscreteInputs - ); - assert_eq!( - WriteSingleCoil(0, true).function_code(), - FunctionCode::WriteSingleCoil - ); - assert_eq!( - WriteMultipleCoils(0, Cow::Borrowed(&[])).function_code(), - FunctionCode::WriteMultipleCoils - ); - assert_eq!( - ReadInputRegisters(0, 0).function_code(), - FunctionCode::ReadInputRegisters - ); - assert_eq!( - ReadHoldingRegisters(0, 0).function_code(), - FunctionCode::ReadHoldingRegisters - ); - assert_eq!( - WriteSingleRegister(0, 0).function_code(), - FunctionCode::WriteSingleRegister - ); - assert_eq!( - WriteMultipleRegisters(0, Cow::Borrowed(&[])).function_code(), - FunctionCode::WriteMultipleRegisters - ); - assert_eq!( - MaskWriteRegister(0, 0, 0).function_code(), - FunctionCode::MaskWriteRegister - ); - assert_eq!( - ReadWriteMultipleRegisters(0, 0, 0, Cow::Borrowed(&[])).function_code(), - FunctionCode::ReadWriteMultipleRegisters - ); - assert_eq!( - Custom(FunctionCode::Custom(88), Cow::Borrowed(&[])).function_code(), - FunctionCode::Custom(88) - ); - } - - #[test] - fn function_code_from_response() { - use crate::frame::Response::*; - assert_eq!(ReadCoils(vec![]).function_code(), FunctionCode::ReadCoils); - assert_eq!( - ReadDiscreteInputs(vec![]).function_code(), - FunctionCode::ReadDiscreteInputs - ); - assert_eq!( - WriteSingleCoil(0x0, false).function_code(), - FunctionCode::WriteSingleCoil - ); - assert_eq!( - WriteMultipleCoils(0x0, 0x0).function_code(), - FunctionCode::WriteMultipleCoils - ); - assert_eq!( - ReadInputRegisters(vec![]).function_code(), - FunctionCode::ReadInputRegisters - ); - assert_eq!( - ReadHoldingRegisters(vec![]).function_code(), - FunctionCode::ReadHoldingRegisters - ); - assert_eq!( - WriteSingleRegister(0, 0).function_code(), - FunctionCode::WriteSingleRegister - ); - assert_eq!( - WriteMultipleRegisters(0, 0).function_code(), - FunctionCode::WriteMultipleRegisters - ); - assert_eq!( - MaskWriteRegister(0, 0, 0).function_code(), - FunctionCode::MaskWriteRegister - ); - assert_eq!( - ReadWriteMultipleRegisters(vec![]).function_code(), - FunctionCode::ReadWriteMultipleRegisters - ); - assert_eq!( - Custom(FunctionCode::Custom(99), Bytes::from_static(&[])).function_code(), - FunctionCode::Custom(99) - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index 42f97461..1890464c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,8 +43,8 @@ mod codec; mod frame; pub use self::frame::{ - Address, Exception, ExceptionResponse, ExtractExceptionResponse, FunctionCode, GetFunctionCode, - Quantity, Request, Response, ResponsePdu, + Address, Exception, ExceptionResponse, ExtractExceptionResponse, FunctionCode, Quantity, + Request, Response, ResponsePdu, }; mod service;