diff --git a/rodbus/RWMR_README.md b/rodbus/RWMR_README.md new file mode 100644 index 00000000..f46d4b6e --- /dev/null +++ b/rodbus/RWMR_README.md @@ -0,0 +1,77 @@ +# 23 (0x17) Read/Write Multiple registers + +This document provides an overview of the Function Code 23 (0x17) (Read & Write Multiple Registers) request as specified in the MODBUS Application Protocol. + + +## Description +This function code performs a combination of one read operation and one write operation in a single MODBUS transaction. The write operation is performed before the read. + +The request specifies the starting address and number of holding registers to be read as well as the starting address, number of holding registers, and the data to be written. The byte count specifies the number of bytes to follow in the write data field. + +The normal response contains the data from the group of registers that were read. The byte count field specifies the quantity of bytes to follow in the read data field. + + +## Request Structure +| Parameter | Size | Range / Value | +|------------------------|--------------|--------------------------| +| Function code | 1 Byte | 0x17 | +| Read Starting Address | 2 Bytes | 0x0000 to 0xFFFF | +| Quantity to Read | 2 Bytes | 0x0001 to 0x007D | +| Write Starting Address | 2 Bytes | 0x0000 to 0xFFFF | +| Quantity to Write | 2 Bytes | 0x0001 to 0x0079 | +| Write Byte Count | 1 Byte | 2 x N* | +| Write Registers Value | N* x 2 Bytes | | +( N* = Quantity to Write ) + +## Response Structure +| Parameter | Size | Value / Description | +|----------------------|--------------|------------------------| +| Function code | 1 Byte | 0x17 | +| Byte Count | 1 Byte | 2 x N* | +| Read Registers value | N* x 2 Bytes | | +( N* = Quantity to Read ) + +## Error Handling +| Parameter | Size | Description | +|----------------|---------|-----------------------------------| +| Error code | 1 Byte | Function code + 0x80 = 0x97 (151) | +| Exception code | 1 Byte | 01 or 02 or 03 or 04 | + +### Error Codes: +- **01**: Illegal Function +- **02**: Illegal Data Address +- **03**: Illegal Data Value +- **04**: Server Device Failure + + +## Example +Here is an example of a request to read six registers starting at register 4, and to write three +registers starting at register 15: + +| Request Field | Hex | Response Field | Hex | +|--------------------------------|-----|----------------------------|-----| +| Function | 17 | Function | 17 | +| Read Starting Address Hi | 00 | Byte Count | 0C | +| Read Starting Address Lo | 03 | Read Registers value Hi | 00 | +| Quantity to Read Hi | 00 | Read Registers value Lo | FE | +| Quantity to Read Lo | 06 | Read Registers value Hi | 0A | +| Write Starting Address Hi | 00 | Read Registers value Lo | CD | +| Write Starting Address Lo | 0E | Read Registers value Hi | 00 | +| Quantity to Write Hi | 00 | Read Registers value Lo | 01 | +| Quantity to Write Lo | 03 | Read Registers value Hi | 00 | +| Write Byte Count | 06 | Read Registers value Lo | 03 | +| Write Registers Value Hi (1st) | 00 | Read Registers value Hi | 00 | +| Write Registers Value Lo (1st) | FF | Read Registers value Lo | 0D | +| Write Registers Value Hi (2nd) | 00 | Read Registers value Hi | 00 | +| Write Registers Value Lo (2nd) | FF | Read Registers value Lo | FF | +| Write Registers Value Hi (3rd) | 00 | | | +| Write Registers Value Lo (3rd) | FF | | | + + +## Troubleshooting Tips +- Ensure the server and client are using the same communication method and are connected to each other +- Check for any returned error code in the response and refer to the error handling section for resolution + + +## Additional Resources +- For more information on the MODBUS protocol and function codes, refer to the [MODBUS Application Protocol Specification V1.1b3](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf), Page 38 (Read/Write Multiple registers). diff --git a/rodbus/WCFC_README.md b/rodbus/WCFC_README.md new file mode 100644 index 00000000..1867d359 --- /dev/null +++ b/rodbus/WCFC_README.md @@ -0,0 +1,64 @@ +# 69 (0x45) Write Custom Function Code + +This document provides a detailed overview of the custom function code (0x45) used in the MODBUS Application Protocol. This function code is user-defined and falls within the range of 65 to 72, as specified in the MODBUS Application Protocol Specification V1.1b3 (Page 10, Section 5: Function Code Categories). + + +## Introduction +The 0x45 function code enables the implementation of user-defined logic on a remote server device. It facilitates the transmission, reception, and processing of a custom function code with a fixed-size data buffer. This buffer currently supports 4 arguments, each 2 bytes (u16) in size, allowing for the execution of custom logic remotely. + +**Note:** To increase flexibility, support for a variable-length data buffer will be included in a future update. + + +## Request Structure +| Parameter | Size | Range / Value | +|--------------------|----------|-----------------------| +| Function code | 1 Byte | 0x45 | +| Length | 2 Bytes | 0x0004 | +| Data | 8 Bytes | 0x0000 to 0xFFFF | + + +## Response Structure +| Parameter | Size | Value/Description | +|---------------|---------|----------------------| +| Function code | 1 Byte | 0x45 | +| Length | 2 Bytes | 0x0004 | +| Data | 8 Bytes | 0x0000 to 0xFFFF | + + +## Error Handling +| Parameter | Size | Description | +|----------------|---------|-----------------------------------| +| Function code | 1 Byte | Function code + 0x80 = 0xC5 (197) | +| Exception code | 1 Byte | 01 or 02 or 03 or 04 | + +### Error Codes: +- **01**: Illegal Function +- **02**: Illegal Data Address +- **03**: Illegal Data Value +- **04**: Server Device Failure + + +## Usage Example +### Request to send the custom buffer [0xC0, 0xDE, 0xCA, 0xFE]: + +| Request Field | Hex | Response Field | Hex | +|---------------------------|-----|------------------------|-----| +| Function | 45 | Function | 45 | +| Length | 04 | Byte Count | 04 | +| Arg1 | C0 | Arg1 | C0 | +| Arg2 | DE | Arg2 | DE | +| Arg3 | CA | Arg3 | CA | +| Arg4 | FE | Arg4 | FE | + + +## Modify and Test Server-Side Buffer Handling +The server currently forwards the Custom Function Code buffer to the client again without alteration. To test modifying or processing the buffer on the remote server device, edit the `write_custom_function_code()` function in `src/examples/client.rs` and `src/examples/server.rs` as needed. + + +## Troubleshooting Tips +- Ensure the server and client are using the same communication and are connected to each other. +- Check for any error codes in the response and refer to the error handling section for resolution. + + +## Additional Resources +- For more information on the MODBUS protocol and function codes, refer to the [MODBUS Application Protocol Specification V1.1b3](https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf). diff --git a/rodbus/examples/client.rs b/rodbus/examples/client.rs index 9403fb87..236a2299 100644 --- a/rodbus/examples/client.rs +++ b/rodbus/examples/client.rs @@ -61,7 +61,7 @@ where async fn run_tcp() -> Result<(), Box> { // ANCHOR: create_tcp_channel let channel = spawn_tcp_client_task( - HostAddr::ip(IpAddr::V4(Ipv4Addr::LOCALHOST), 502), + HostAddr::ip(IpAddr::V4(Ipv4Addr::LOCALHOST), 10502), 1, default_retry_strategy(), DecodeLevel::default(), @@ -96,7 +96,7 @@ async fn run_rtu() -> Result<(), Box> { async fn run_tls(tls_config: TlsClientConfig) -> Result<(), Box> { // ANCHOR: create_tls_channel let channel = spawn_tls_client_task( - HostAddr::ip(IpAddr::V4(Ipv4Addr::LOCALHOST), 802), + HostAddr::ip(IpAddr::V4(Ipv4Addr::LOCALHOST), 10802), 1, default_retry_strategy(), tls_config, @@ -178,7 +178,7 @@ async fn run_channel(mut channel: Channel) -> Result<(), Box Result<(), Box Result<(), Box { + // ANCHOR: read_write_multiple_registers + let read_range = AddressRange::try_from(0x01, 0x04).unwrap(); + let write_range = AddressRange::try_from(0x01, 0x04).unwrap(); + let write_values = vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]; + + let result = channel + .read_write_multiple_registers( + params, + ReadWriteMultiple::new(read_range, write_range, write_values).unwrap(), + ) + .await; + print_write_result(result); + // ANCHOR_END: read_write_multiple_registers + } + "wcfc" => { + // ANCHOR: write_custom_function_code + let length = 0x04 as usize; + let values = [0xC0, 0xDE, 0xCA, 0xFE]; // i.e.: Voltage Hi = 0xC0 / Voltage Lo = 0xDE / Current Hi = 0xCA / Current Lo = 0xFE + + let result = channel + .write_custom_function_code( + params, + CustomFunctionCode::new(length, values) + ) + .await; + print_write_result(result); + // ANCHOR_END: write_custom_function_code + } _ => println!("unknown command"), } } diff --git a/rodbus/examples/server.rs b/rodbus/examples/server.rs index 4fc490cb..011c475b 100644 --- a/rodbus/examples/server.rs +++ b/rodbus/examples/server.rs @@ -78,6 +78,16 @@ impl RequestHandler for SimpleHandler { } } + fn write_custom_function_code(&mut self, values: CustomFunctionCode) -> Result<(), ExceptionCode> { + let mut custom_fc_args = [0_u16; 4]; // i.e.: Voltage Hi = 0x02, Voltage Lo = 0x03, Current Hi = 0x04, Current Lo = 0x05 + for (i, &value) in values.iter().enumerate() { + custom_fc_args[i] = value; + } + tracing::info!("processing custom function code arguments: {:?}", custom_fc_args); + + Ok(()) + } + fn write_single_register(&mut self, value: Indexed) -> Result<(), ExceptionCode> { tracing::info!( "write single register, index: {} value: {}", @@ -167,7 +177,7 @@ async fn run_tcp() -> Result<(), Box> { // ANCHOR: tcp_server_create let server = rodbus::server::spawn_tcp_server_task( 1, - "127.0.0.1:502".parse()?, + "127.0.0.1:10502".parse()?, map, AddressFilter::Any, DecodeLevel::default(), @@ -206,7 +216,7 @@ async fn run_tls(tls_config: TlsServerConfig) -> Result<(), Box Result { + let (tx, rx) = tokio::sync::oneshot::channel::>(); + let request = wrap( + param, + RequestDetails::WriteCustomFunctionCode(WriteCustomFunctionCode::new(request, Promise::channel(tx))), + ); + self.tx.send(request).await?; + rx.await? + } + /// Write a single coil on the server pub async fn write_single_coil( &mut self, @@ -229,6 +247,24 @@ impl Channel { rx.await? } + /// Read & Write multiple contiguous registers on the server + pub async fn read_write_multiple_registers( + &mut self, + param: RequestParam, + request: ReadWriteMultiple, + ) -> Result>, RequestError> { + let (tx, rx) = tokio::sync::oneshot::channel::>, RequestError>>(); + let request = wrap( + param, + RequestDetails::ReadWriteMultipleRegisters(MultipleReadWriteRequest::new( + request, + Promise::channel(tx) + )), + ); + self.tx.send(request).await?; + rx.await? + } + /// Dynamically change the protocol decoding level of the channel pub async fn set_decode_level(&mut self, level: DecodeLevel) -> Result<(), Shutdown> { self.tx diff --git a/rodbus/src/client/message.rs b/rodbus/src/client/message.rs index c18abb7e..da1bdb13 100644 --- a/rodbus/src/client/message.rs +++ b/rodbus/src/client/message.rs @@ -10,8 +10,10 @@ use crate::client::requests::read_bits::ReadBits; use crate::client::requests::read_registers::ReadRegisters; use crate::client::requests::write_multiple::MultipleWriteRequest; use crate::client::requests::write_single::SingleWrite; +use crate::client::requests::read_write_multiple::MultipleReadWriteRequest; +use crate::client::requests::write_custom_fc::WriteCustomFunctionCode; use crate::common::traits::Serialize; -use crate::types::{Indexed, UnitId}; +use crate::types::{Indexed, UnitId, CustomFunctionCode}; use scursor::{ReadCursor, WriteCursor}; use std::time::Duration; @@ -45,6 +47,8 @@ pub(crate) enum RequestDetails { WriteSingleRegister(SingleWrite>), WriteMultipleCoils(MultipleWriteRequest), WriteMultipleRegisters(MultipleWriteRequest), + ReadWriteMultipleRegisters(MultipleReadWriteRequest), + WriteCustomFunctionCode(WriteCustomFunctionCode), } impl Request { @@ -129,6 +133,8 @@ impl RequestDetails { RequestDetails::WriteSingleRegister(_) => FunctionCode::WriteSingleRegister, RequestDetails::WriteMultipleCoils(_) => FunctionCode::WriteMultipleCoils, RequestDetails::WriteMultipleRegisters(_) => FunctionCode::WriteMultipleRegisters, + RequestDetails::ReadWriteMultipleRegisters(_) => FunctionCode::ReadWriteMultipleRegisters, + RequestDetails::WriteCustomFunctionCode(_) => FunctionCode::WriteCustomFunctionCode, } } @@ -142,6 +148,8 @@ impl RequestDetails { RequestDetails::WriteSingleRegister(x) => x.failure(err), RequestDetails::WriteMultipleCoils(x) => x.failure(err), RequestDetails::WriteMultipleRegisters(x) => x.failure(err), + RequestDetails::ReadWriteMultipleRegisters(x) => x.failure(err), + RequestDetails::WriteCustomFunctionCode(x) => x.failure(err), } } @@ -161,6 +169,12 @@ impl RequestDetails { RequestDetails::WriteMultipleCoils(x) => x.handle_response(cursor, function, decode), RequestDetails::WriteMultipleRegisters(x) => { x.handle_response(cursor, function, decode) + }, + RequestDetails::ReadWriteMultipleRegisters(x) => { + x.handle_response(cursor, function, decode) + }, + RequestDetails::WriteCustomFunctionCode(x) => { + x.handle_response(cursor, function, decode) } } } @@ -177,6 +191,8 @@ impl Serialize for RequestDetails { RequestDetails::WriteSingleRegister(x) => x.serialize(cursor), RequestDetails::WriteMultipleCoils(x) => x.serialize(cursor), RequestDetails::WriteMultipleRegisters(x) => x.serialize(cursor), + RequestDetails::ReadWriteMultipleRegisters(x) => x.serialize(cursor), + RequestDetails::WriteCustomFunctionCode(x) => x.serialize(cursor), } } } @@ -241,6 +257,18 @@ impl std::fmt::Display for RequestDetailsDisplay<'_> { } } } + RequestDetails::ReadWriteMultipleRegisters(details) => { + write!(f, "read_range: ({}) / ", details.request.read_range)?; + write!(f, "write_range: ({})", details.request.write_range)?; + if self.level.data_values() { + for x in details.request.iter() { + write!(f, "\n{x}")?; + } + } + } + RequestDetails::WriteCustomFunctionCode(details) => { + write!(f, "{}", details.request)?; + } } } diff --git a/rodbus/src/client/mod.rs b/rodbus/src/client/mod.rs index 368e5e42..6a497d41 100644 --- a/rodbus/src/client/mod.rs +++ b/rodbus/src/client/mod.rs @@ -12,6 +12,7 @@ pub(crate) mod task; pub use crate::client::channel::*; pub use crate::client::listener::*; pub use crate::client::requests::write_multiple::WriteMultiple; +pub use crate::client::requests::read_write_multiple::ReadWriteMultiple; pub use crate::retry::*; #[cfg(feature = "tls")] diff --git a/rodbus/src/client/requests/mod.rs b/rodbus/src/client/requests/mod.rs index bfebd0a4..a1980d13 100644 --- a/rodbus/src/client/requests/mod.rs +++ b/rodbus/src/client/requests/mod.rs @@ -2,3 +2,5 @@ pub(crate) mod read_bits; pub(crate) mod read_registers; pub(crate) mod write_multiple; pub(crate) mod write_single; +pub(crate) mod read_write_multiple; +pub(crate) mod write_custom_fc; \ No newline at end of file diff --git a/rodbus/src/client/requests/read_write_multiple.rs b/rodbus/src/client/requests/read_write_multiple.rs new file mode 100644 index 00000000..ac8059ac --- /dev/null +++ b/rodbus/src/client/requests/read_write_multiple.rs @@ -0,0 +1,141 @@ +use crate::client::message::Promise; +use crate::common::function::FunctionCode; +use crate::common::traits::Serialize; +use crate::decode::AppDecodeLevel; +use crate::error::RequestError; +use crate::error::InvalidRequest; +use crate::types::{AddressRange, Indexed, RegisterIterator, RegisterIteratorDisplay}; + +use scursor::{ReadCursor, WriteCursor}; + +/// Collection of values and starting address +/// +/// Used when making write multiple coil/register requests +#[derive(Debug, Clone, PartialEq)] +pub struct ReadWriteMultiple { + /// starting address + pub(crate) read_range: AddressRange, + /// starting address + pub(crate) write_range: AddressRange, + /// vector of values + pub(crate) values: Vec, +} + +pub(crate) struct ReadWriteMultipleIterator<'a, T> { + range: AddressRange, + pos: u16, + iter: std::slice::Iter<'a, T>, +} + +impl ReadWriteMultiple { + /// Create new collection of values + pub fn new( + read_range: AddressRange, + write_range: AddressRange, + values: Vec, + ) -> Result { + let values_count = values.len() as u16; + if write_range.count != values_count{ + return Err(RequestError::BadRequest(InvalidRequest::CountTooBigForType(write_range.count, values_count))); + } + + Ok(Self { + read_range, + write_range, + values, + }) + } + + pub(crate) fn iter(&self) -> ReadWriteMultipleIterator<'_, T> { + ReadWriteMultipleIterator::new(self.write_range, self.values.iter()) + } +} + +impl<'a, T> ReadWriteMultipleIterator<'a, T> { + fn new(range: AddressRange, iter: std::slice::Iter<'a, T>) -> Self { + Self { + range, + pos: 0, + iter, + } + } +} + +impl Iterator for ReadWriteMultipleIterator<'_, T> +where + T: Copy, +{ + type Item = Indexed; + + fn next(&mut self) -> Option { + let next = self.iter.next(); + + match next { + Some(next) => { + let result = Indexed::new(self.range.start + self.pos, *next); + self.pos += 1; + Some(result) + } + None => None, + } + } + + // implementing this allows collect to optimize the vector capacity + fn size_hint(&self) -> (usize, Option) { + let remaining = (self.range.count - self.pos) as usize; + (remaining, Some(remaining)) + } +} + +pub(crate) struct MultipleReadWriteRequest +where + ReadWriteMultiple: Serialize, +{ + pub(crate) request: ReadWriteMultiple, + promise: Promise>>, +} + +impl MultipleReadWriteRequest +where + ReadWriteMultiple: Serialize, +{ + pub(crate) fn new(request: ReadWriteMultiple, promise: Promise>>) -> Self { + Self { request, promise } + } + + pub(crate) fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + self.request.serialize(cursor) + } + + pub(crate) fn failure(&mut self, err: RequestError) { + self.promise.failure(err) + } + + pub(crate) fn handle_response( + &mut self, + mut cursor: ReadCursor, + function: FunctionCode, + decode: AppDecodeLevel, + ) -> Result<(), RequestError> { + let response = Self::parse_registers_response(self.request.read_range, &mut cursor)?; + + if decode.data_headers() { + tracing::info!("PDU RX - {} {}", function, RegisterIteratorDisplay::new(decode, response)); + } else if decode.header() { + tracing::info!("PDU RX - {}", function); + } + + self.promise.success(response.collect()); + Ok(()) + } + + fn parse_registers_response<'a>( + range: AddressRange, + cursor: &'a mut ReadCursor, + ) -> Result, RequestError> { + // there's a byte-count here that we don't actually need + cursor.read_u8()?; + // the reset is a sequence of bits + RegisterIterator::parse_all(range, cursor) + } +} diff --git a/rodbus/src/client/requests/write_custom_fc.rs b/rodbus/src/client/requests/write_custom_fc.rs new file mode 100644 index 00000000..5bb7f9fa --- /dev/null +++ b/rodbus/src/client/requests/write_custom_fc.rs @@ -0,0 +1,90 @@ +use std::fmt::Display; + +use crate::CustomFunctionCode; +use crate::client::message::Promise; +use crate::common::function::FunctionCode; +use crate::decode::AppDecodeLevel; +use crate::error::AduParseError; +use crate::error::RequestError; + +use scursor::{ReadCursor, WriteCursor}; + +pub(crate) trait WriteCustomFunctionCodeOperation: Sized + PartialEq { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError>; + fn parse(cursor: &mut ReadCursor) -> Result; +} + +pub(crate) struct WriteCustomFunctionCode +where + T: WriteCustomFunctionCodeOperation + Display + Send + 'static, +{ + pub(crate) request: T, + promise: Promise, +} + +impl WriteCustomFunctionCode +where + T: WriteCustomFunctionCodeOperation + Display + Send + 'static, +{ + pub(crate) fn new(request: T, promise: Promise) -> Self { + Self { request, promise } + } + + pub(crate) fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + self.request.serialize(cursor) + } + + pub(crate) fn failure(&mut self, err: RequestError) { + self.promise.failure(err) + } + + pub(crate) fn handle_response( + &mut self, + cursor: ReadCursor, + function: FunctionCode, + decode: AppDecodeLevel, + ) -> Result<(), RequestError> { + let response = self.parse_all(cursor)?; + + if decode.data_headers() { + tracing::info!("PDU RX - {} {}", function, response); + } else if decode.header() { + tracing::info!("PDU RX - {}", function); + } + + self.promise.success(response); + Ok(()) + } + + fn parse_all(&self, mut cursor: ReadCursor) -> Result { + let response = T::parse(&mut cursor)?; + cursor.expect_empty()?; + if self.request != response { + return Err(AduParseError::ReplyEchoMismatch.into()); + } + Ok(response) + } +} + +impl WriteCustomFunctionCodeOperation for CustomFunctionCode { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + cursor.write_u16_be(self.len() as u16)?; + + for &item in self.iter() { + cursor.write_u16_be(item)?; + } + Ok(()) + } + + fn parse(cursor: &mut ReadCursor) -> Result { + let len = cursor.read_u16_be()? as usize; + let val1 = cursor.read_u16_be()?; + let val2 = cursor.read_u16_be()?; + let val3 = cursor.read_u16_be()?; + let val4 = cursor.read_u16_be()?; + + let values = [val1, val2, val3, val4]; + + Ok(CustomFunctionCode::new(len, values)) + } +} diff --git a/rodbus/src/common/function.rs b/rodbus/src/common/function.rs index e85bf14a..0359eba5 100644 --- a/rodbus/src/common/function.rs +++ b/rodbus/src/common/function.rs @@ -9,6 +9,8 @@ mod constants { pub(crate) const WRITE_SINGLE_REGISTER: u8 = 6; pub(crate) const WRITE_MULTIPLE_COILS: u8 = 15; pub(crate) const WRITE_MULTIPLE_REGISTERS: u8 = 16; + pub(crate) const READ_WRITE_MULTIPLE_REGISTERS: u8 = 23; + pub(crate) const WRITE_CUSTOM_FUNCTION_CODE: u8 = 69; } #[derive(Debug, Copy, Clone, PartialEq)] @@ -22,6 +24,8 @@ pub(crate) enum FunctionCode { WriteSingleRegister = constants::WRITE_SINGLE_REGISTER, WriteMultipleCoils = constants::WRITE_MULTIPLE_COILS, WriteMultipleRegisters = constants::WRITE_MULTIPLE_REGISTERS, + ReadWriteMultipleRegisters = constants::READ_WRITE_MULTIPLE_REGISTERS, + WriteCustomFunctionCode = constants::WRITE_CUSTOM_FUNCTION_CODE, } impl Display for FunctionCode { @@ -49,6 +53,12 @@ impl Display for FunctionCode { FunctionCode::WriteMultipleRegisters => { write!(f, "WRITE MULTIPLE REGISTERS ({:#04X})", self.get_value()) } + FunctionCode::ReadWriteMultipleRegisters => { + write!(f, "READ/WRITE MULTIPLE REGISTERS ({:#04X})", self.get_value()) + } + FunctionCode::WriteCustomFunctionCode => { + write!(f, "WRITE CUSTOM FUNCTION CODE ({:#04X})", self.get_value()) + } } } } @@ -72,6 +82,8 @@ impl FunctionCode { constants::WRITE_SINGLE_REGISTER => Some(FunctionCode::WriteSingleRegister), constants::WRITE_MULTIPLE_COILS => Some(FunctionCode::WriteMultipleCoils), constants::WRITE_MULTIPLE_REGISTERS => Some(FunctionCode::WriteMultipleRegisters), + constants::READ_WRITE_MULTIPLE_REGISTERS => Some(FunctionCode::ReadWriteMultipleRegisters), + constants::WRITE_CUSTOM_FUNCTION_CODE => Some(FunctionCode::WriteCustomFunctionCode), _ => None, } } diff --git a/rodbus/src/common/parse.rs b/rodbus/src/common/parse.rs index 8b6dce04..ca5e3fbf 100644 --- a/rodbus/src/common/parse.rs +++ b/rodbus/src/common/parse.rs @@ -1,6 +1,7 @@ +use crate::client::ReadWriteMultiple; use crate::common::traits::Parse; use crate::error::*; -use crate::types::{coil_from_u16, AddressRange, Indexed}; +use crate::types::{coil_from_u16, AddressRange, Indexed, CustomFunctionCode}; use scursor::ReadCursor; @@ -28,6 +29,31 @@ impl Parse for Indexed { } } +impl Parse for CustomFunctionCode { + fn parse(cursor: &mut ReadCursor) -> Result { + let len = cursor.read_u16_be()? as usize; + let values = [cursor.read_u16_be()?, cursor.read_u16_be()?, cursor.read_u16_be()?, cursor.read_u16_be()?]; + + Ok(CustomFunctionCode::new(len, values)) + } +} + +impl Parse for ReadWriteMultiple { + fn parse(cursor: &mut ReadCursor) -> Result { + let read_range = AddressRange::parse(cursor)?; + let write_range = AddressRange::parse(cursor)?; + + // ignore data byte count + cursor.read_u8()?; + let mut values = Vec::new(); + for _ in 0..write_range.count { + values.push(cursor.read_u16_be()?); + } + + Ok(ReadWriteMultiple::new(read_range, write_range, values)?) + } +} + #[cfg(test)] mod coils { use crate::common::traits::Parse; @@ -63,4 +89,147 @@ mod coils { let result = Indexed::::parse(&mut cursor); assert_eq!(result, Ok(Indexed::new(1, 0xCAFE))); } + + #[test] + fn parse_succeeds_for_valid_custom_function_code() { + let mut cursor = ReadCursor::new(&[0x00, 0x04, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE, 0xC0, 0xDE]); + let result = crate::types::CustomFunctionCode::parse(&mut cursor); + assert_eq!(result, Ok(crate::types::CustomFunctionCode::new(4, [0xCAFE, 0xC0DE, 0xCAFE, 0xC0DE]))); + } + + #[test] + fn parse_fails_for_invalid_custom_function_code() { + let mut cursor = ReadCursor::new(&[0x00, 0x04, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE, 0xC0]); + let result = crate::types::CustomFunctionCode::parse(&mut cursor); + assert_eq!(result, Err(AduParseError::InsufficientBytes.into())); + } } + +#[cfg(test)] +mod read_write_multiple_registers { + use crate::common::traits::Parse; + use crate::error::AduParseError; + use crate::types::AddressRange; + use crate::client::requests::read_write_multiple::ReadWriteMultiple; + use crate::RequestError; + + use scursor::ReadCursor; + + //ANCHOR: parse read_write_multiple_request + + /// Write a single zero value to register 1 (index 0) - Minimum test + /// Read 5 registers starting at register 1 (index 0-4) afterwards + /// + /// read_range start: 0x00, count: 0x05 + /// write_range start: 0x00, count: 0x01 + /// value length = 2 bytes, value = 0x0000 + #[test] + fn parse_succeeds_for_valid_read_write_multiple_request_of_single_zero_register_write() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00]); + let result = ReadWriteMultiple::::parse(&mut cursor); + let check = ReadWriteMultiple::::new(AddressRange::try_from(0x00, 0x05).unwrap(), AddressRange::try_from(0x00, 0x01).unwrap(), vec![0x00]); + assert_eq!(result, check); + } + + /// Write a single 0xFFFF value to register 0xFFFF (index 65.535) - Limit test + /// Read 5 registers starting at register 0xFFFB (65.531-65.535) afterwards + /// + /// read_range start: 0xFFFB, count: 0x05 + /// write_range start: 0xFFFF, count: 0x01 + /// value length = 2 bytes, value = 0xFFFF + #[test] + fn parse_succeeds_for_valid_read_write_multiple_request_of_single_u16_register_write() { + let mut cursor = ReadCursor::new(&[0xFF, 0xFB, 0x00, 0x05, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0xFF, 0xFF]); + let result = ReadWriteMultiple::::parse(&mut cursor); + let check = ReadWriteMultiple::::new(AddressRange::try_from(0xFFFB, 0x05).unwrap(), AddressRange::try_from(0xFFFF, 0x01).unwrap(), vec![0xFFFF]); + assert_eq!(result, check); + } + + /// Write multiple zero values to registers 1, 2 and 3 (index 0-2) - Minimum test + /// Read 5 registers starting at register 1 (0-4) afterwards + /// + /// read_range start: 0x00, count: 0x05 + /// write_range start: 0x00, count: 0x03 + /// values length = 6 bytes, values = 0x0000, 0x0000, 0x0000 + #[test] + fn parse_succeeds_for_valid_read_write_multiple_request_of_multiple_zero_register_write() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + let result = ReadWriteMultiple::::parse(&mut cursor); + let check = ReadWriteMultiple::::new(AddressRange::try_from(0x00, 0x05).unwrap(), AddressRange::try_from(0x00, 0x03).unwrap(), vec![0x00, 0x00, 0x00]); + assert_eq!(result, check); + } + + /// Write multiple 0xFFFF values to registers 0xFFFD, 0xFFFE and 0xFFFF (index 65.533 - 65.535) - Limit test + /// Read 5 registers starting at register 0xFFFB (65.531-65.535) afterwards + /// + /// read_range start: 0xFFFB, count: 0x05 + /// write_range start: 0xFFFD, count: 0x03 + /// values length = 6 bytes, values = 0xFFFF, 0xFFFF, 0xFFFF + #[test] + fn parse_succeeds_for_valid_read_write_multiple_request_of_multiple_u16_register_write() { + let mut cursor = ReadCursor::new(&[0xFF, 0xFB, 0x00, 0x05, 0xFF, 0xFD, 0x00, 0x03, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + let result = ReadWriteMultiple::::parse(&mut cursor); + let check = ReadWriteMultiple::::new(AddressRange::try_from(0xFFFB, 0x05).unwrap(), AddressRange::try_from(0xFFFD, 0x03).unwrap(), vec![0xFFFF, 0xFFFF, 0xFFFF]); + assert_eq!(result, check); + } + + /// Write multiple values to registers 1, 2 and 3 (index 0-2) - Limit test + /// Read 5 registers starting at register 1 (0-4) afterwards + /// fails because: Byte count of 6 specified but only 4 bytes provided + /// + /// read_range start: 0x0000, count: 0x05 + /// write_range start: 0x0000, count: 0x03 + /// values length = 6 bytes, values = 0xCAFE, 0xC0DE (4 bytes) + #[test] + fn parse_fails_for_invalid_read_write_multiple_request_of_insufficient_values() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0xCA, 0xFE, 0xC0, 0xDE]); + let result = ReadWriteMultiple::::parse(&mut cursor); + assert_eq!(result, Err(RequestError::BadResponse(AduParseError::InsufficientBytes.into()))); + } + + /// Write multiple values to registers 1, 2 and 3 (index 0-2) - Limit test + /// Read 5 registers starting at register 1 (0-4) afterwards + /// fails because: Byte count of 6 specified but only 5 bytes provided + /// + /// read_range start: 0x0000, count: 0x05 + /// write_range start: 0x0000, count: 0x03 + /// values length = 6 bytes, values = 0xCAFE, 0xC0DE, 0xCA (5 bytes) + #[test] + fn parse_fails_for_invalid_read_write_multiple_request_of_insufficient_bytes() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA]); + let result = ReadWriteMultiple::::parse(&mut cursor); + assert_eq!(result, Err(RequestError::BadResponse(AduParseError::InsufficientBytes.into()))); + } + + /// Write multiple values to registers 1, 2 and 3 (index 0-2) - Limit test + /// Read 5 registers starting at register 1 (0-4) afterwards + /// fails because: Byte count of 5 specified but only 5 bytes provided + /// + /// read_range start: 0x0000, count: 0x05 + /// write_range start: 0x0000, count: 0x03 + /// values length = 4 bytes, values = 0xCAFE, 0xC0DE, 0xCA (5 bytes) + #[test] + fn parse_fails_for_invalid_read_write_multiple_request_of_too_much_bytes() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x04, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA]); + let result = ReadWriteMultiple::::parse(&mut cursor); + assert_eq!(result, Err(RequestError::BadResponse(AduParseError::InsufficientBytes.into()))); + } + + /// TODO: The test case should fail, but it succeeds. Need to test this more, as we need to implement a check for the correct provided byte count. For now, the test assumes that the request succeeds. + /// Write multiple values to registers 1, 2 and 3 (index 0-2) - Limit test + /// Read 5 registers starting at register 1 (0-4) afterwards + /// fails because: Byte count of 5 specified but only 5 bytes provided + /// + /// read_range start: 0x0000, count: 0x05 + /// write_range start: 0x0000, count: 0x03 + /// values length = 4 bytes, values = 0xCAFE, 0xC0DE, 0xCAFE (6 bytes) + #[test] + fn parse_fails_for_invalid_read_write_multiple_request_of_too_much_values() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x04, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE]); + let result = ReadWriteMultiple::::parse(&mut cursor); + assert_eq!(result, ReadWriteMultiple::::new(AddressRange::try_from(0x00, 0x05).unwrap(), AddressRange::try_from(0x00, 0x03).unwrap(), vec![0xCAFE, 0xC0DE, 0xCAFE])); + //assert_eq!(result, Err(RequestError::BadResponse(AduParseError::InsufficientBytes.into()))); + } + + //ANCHOR_END: parse read_write_multiple_request +} \ No newline at end of file diff --git a/rodbus/src/common/serialize.rs b/rodbus/src/common/serialize.rs index 905cfe01..0a481af7 100644 --- a/rodbus/src/common/serialize.rs +++ b/rodbus/src/common/serialize.rs @@ -1,5 +1,6 @@ use std::convert::TryFrom; +use crate::client::requests::read_write_multiple::ReadWriteMultiple; use crate::client::WriteMultiple; use crate::common::traits::Loggable; use crate::common::traits::Parse; @@ -8,7 +9,7 @@ use crate::error::{InternalError, RequestError}; use crate::server::response::{BitWriter, RegisterWriter}; use crate::types::{ coil_from_u16, coil_to_u16, AddressRange, BitIterator, BitIteratorDisplay, Indexed, - RegisterIterator, RegisterIteratorDisplay, + RegisterIterator, RegisterIteratorDisplay, CustomFunctionCode, }; use scursor::{ReadCursor, WriteCursor}; @@ -290,6 +291,59 @@ impl Serialize for WriteMultiple { } } +impl Serialize for ReadWriteMultiple { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + self.read_range.serialize(cursor)?; + self.write_range.serialize(cursor)?; + self.values.as_slice().serialize(cursor) + } +} + +impl Serialize for CustomFunctionCode { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + cursor.write_u16_be(self.len() as u16)?; + + for &item in self.iter() { + cursor.write_u16_be(item)?; + } + Ok(()) + } +} + +impl Loggable for CustomFunctionCode { + fn log( + &self, + payload: &[u8], + level: crate::decode::AppDecodeLevel, + f: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + if level.data_headers() { + let mut cursor = ReadCursor::new(payload); + + let len = match cursor.read_u16_be() { + Ok(value) => value as usize, + Err(_) => return Ok(()), + }; + + let mut data = [0_u16; 4]; + + for i in 0..4 { + data[i] = match cursor.read_u16_be() { + Ok(value) => value, + Err(_) => return Ok(()), + }; + } + + let custom_fc = CustomFunctionCode::new(len, data); + + write!(f, "{:?}", custom_fc)?; + + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -302,4 +356,105 @@ mod tests { range.serialize(&mut cursor).unwrap(); assert_eq!(buffer, [0x00, 0x03, 0x02, 0x00]); } + + #[test] + fn serializes_valid_custom_function_code() { + let custom_fc = CustomFunctionCode::new(4, [0xCAFE, 0xC0DE, 0xCAFE, 0xC0DE]); + let mut buffer = [0u8; 10]; + let mut cursor = WriteCursor::new(&mut buffer); + custom_fc.serialize(&mut cursor).unwrap(); + assert_eq!(buffer, [0x00, 0x04, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE, 0xC0, 0xDE]); + } + + //ANCHOR: serialize read_write_multiple_request + + /// Write a single zero value to register 1 (index 0) - Minimum test + /// Read the registers 1 - 5 (index 0 - 4) afterwards + #[test] + fn serialize_succeeds_for_valid_read_write_multiple_request_of_single_u16_zero_value() { + // read 5 registers starting at register 2 + let read_range = AddressRange::try_from(0x00, 0x05).unwrap(); + // write 1 register starting at register 1 + let write_range = AddressRange::try_from(0x00, 0x01).unwrap(); + // write 1 value that has the value 0 + let values = vec![0u16; 1]; + + // construct the request + let request = ReadWriteMultiple::new(read_range, write_range, values).unwrap(); + + // serialize the request + let mut buffer = [0u8; 11]; + let mut cursor = WriteCursor::new(&mut buffer); + request.serialize(&mut cursor).unwrap(); + + assert_eq!(buffer, [0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00]); + } + + /// Write a single 0xFFFF value to register 0xFFFF (65.535) - Maximum test + /// Read the register 0xFFFF (65.535) afterwards + #[test] + fn serialize_succeeds_for_valid_read_write_multiple_request_of_single_u16_value() { + // read only register 0xFFFF + let read_range = AddressRange::try_from(0xFFFF, 0x01).unwrap(); + // write only register 0xFFFF + let write_range = AddressRange::try_from(0xFFFF, 0x01).unwrap(); + // write a single value of 0xFFFF (65.535) + let values = vec![0xFFFF]; + + // construct the request + let request = ReadWriteMultiple::new(read_range, write_range, values).unwrap(); + + // serialize the request + let mut buffer = [0u8; 11]; + let mut cursor = WriteCursor::new(&mut buffer); + request.serialize(&mut cursor).unwrap(); + + assert_eq!(buffer, [0xFF, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0xFF, 0xFF]); + } + + /// Write three zero values to registers 1, 2 and 3 (index 0 - 2) - Minimum test + /// Read the registers 1 - 5 (index 0 - 4) afterwards + #[test] + fn serialize_succeeds_for_valid_read_write_multiple_request_of_three_u16_zero_values() { + // read 5 registers starting at register 0x00 + let read_range = AddressRange::try_from(0x00, 0x05).unwrap(); + // write 3 registers starting at register 0x00 + let write_range = AddressRange::try_from(0x00, 0x03).unwrap(); + // write 3 values with a value of 0 + let values = vec![0x00, 0x00, 0x00]; + + // construct the request + let request = ReadWriteMultiple::new(read_range, write_range, values).unwrap(); + + // serialize the request + let mut buffer = [0u8; 15]; + let mut cursor = WriteCursor::new(&mut buffer); + request.serialize(&mut cursor).unwrap(); + + assert_eq!(buffer, [0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + } + + /// Write three 0xFFFF values to registers 0xFFFD, 0xFFFE and 0xFFFF (65.533 - 65.535) - Maximum test + /// Read the registers 0xFFFB - 0xFFFF (65.531 - 65.535) afterwards + #[test] + fn serialize_succeeds_for_valid_read_write_multiple_request_of_three_u16_values() { + // read 5 registers starting at register 0xFFFB + let read_range = AddressRange::try_from(0xFFFB, 0x05).unwrap(); + // write 3 registers starting at register 0xFFFD + let write_range = AddressRange::try_from(0xFFFD, 0x03).unwrap(); + // write 3 values with a value of 0xFFFF + let values = vec![0xFFFF, 0xFFFF, 0xFFFF]; + + // construct the request + let request = ReadWriteMultiple::new(read_range, write_range, values).unwrap(); + + // serialize the request + let mut buffer = [0u8; 15]; + let mut cursor = WriteCursor::new(&mut buffer); + request.serialize(&mut cursor).unwrap(); + + assert_eq!(buffer, [0xFF, 0xFB, 0x00, 0x05, 0xFF, 0xFD, 0x00, 0x03, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + } + + //ANCHOR_END: serialize read_write_multiple_request } diff --git a/rodbus/src/serial/frame.rs b/rodbus/src/serial/frame.rs index 642f7a8a..da8c3200 100644 --- a/rodbus/src/serial/frame.rs +++ b/rodbus/src/serial/frame.rs @@ -87,6 +87,8 @@ impl RtuParser { FunctionCode::WriteSingleRegister => LengthMode::Fixed(4), FunctionCode::WriteMultipleCoils => LengthMode::Offset(5), FunctionCode::WriteMultipleRegisters => LengthMode::Offset(5), + FunctionCode::ReadWriteMultipleRegisters => LengthMode::Offset(9), + FunctionCode::WriteCustomFunctionCode => LengthMode::Offset(1), }, ParserType::Response => match function_code { FunctionCode::ReadCoils => LengthMode::Offset(1), @@ -97,6 +99,8 @@ impl RtuParser { FunctionCode::WriteSingleRegister => LengthMode::Fixed(4), FunctionCode::WriteMultipleCoils => LengthMode::Fixed(4), FunctionCode::WriteMultipleRegisters => LengthMode::Fixed(4), + FunctionCode::ReadWriteMultipleRegisters => LengthMode::Fixed(4), + FunctionCode::WriteCustomFunctionCode => LengthMode::Offset(1), }, } } diff --git a/rodbus/src/server/handler.rs b/rodbus/src/server/handler.rs index 43658927..2453fac5 100644 --- a/rodbus/src/server/handler.rs +++ b/rodbus/src/server/handler.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; use crate::exception::ExceptionCode; -use crate::server::{WriteCoils, WriteRegisters}; +use crate::server::{WriteCoils, WriteRegisters, ReadWriteRegisters}; use crate::types::*; /// Trait implemented by the user to process requests received from the client @@ -62,6 +62,16 @@ pub trait RequestHandler: Send + 'static { fn write_multiple_registers(&mut self, _values: WriteRegisters) -> Result<(), ExceptionCode> { Err(ExceptionCode::IllegalFunction) } + + /// Read and write multiple registers + fn read_write_multiple_registers(&mut self, _values: ReadWriteRegisters) -> Result<(), ExceptionCode> { + Err(ExceptionCode::IllegalFunction) + } + + /// Write a custom function code + fn write_custom_function_code(&mut self, _values: CustomFunctionCode) -> Result<(), ExceptionCode> { + Err(ExceptionCode::IllegalFunction) + } } /// Trait useful for converting None into IllegalDataAddress @@ -176,7 +186,7 @@ pub trait AuthorizationHandler: Send + Sync + 'static { ) -> Authorization { Authorization::Deny } - + /// Authorize a Read Holding Registers request fn read_holding_registers( &self, @@ -226,6 +236,22 @@ pub trait AuthorizationHandler: Send + Sync + 'static { ) -> Authorization { Authorization::Deny } + + /// Authorize a Read Write Multiple Registers request + fn read_write_multiple_registers( + &self, + _unit_id: UnitId, + _read_range: AddressRange, + _write_range: AddressRange, + _role: &str, + ) -> Authorization { + Authorization::Deny + } + + /// Authorize a Write Custom Function Code request + fn write_custom_function_code(&self, _value: CustomFunctionCode, _role: &str) -> Authorization { + Authorization::Deny + } } /// Read-only authorization handler that blindly accepts @@ -304,6 +330,22 @@ impl AuthorizationHandler for ReadOnlyAuthorizationHandler { ) -> Authorization { Authorization::Deny } + + /// Authorize a Read Write Multiple Registers request + fn read_write_multiple_registers( + &self, + _unit_id: UnitId, + _read_range: AddressRange, + _write_range: AddressRange, + _role: &str, + ) -> Authorization { + Authorization::Allow + } + + /// Authorize a Write Custom Function Code request + fn write_custom_function_code(&self, _value: CustomFunctionCode, _role: &str) -> Authorization { + Authorization::Allow + } } #[cfg(test)] diff --git a/rodbus/src/server/request.rs b/rodbus/src/server/request.rs index 77843d67..2c61e1d1 100644 --- a/rodbus/src/server/request.rs +++ b/rodbus/src/server/request.rs @@ -21,6 +21,8 @@ pub(crate) enum Request<'a> { WriteSingleRegister(Indexed), WriteMultipleCoils(WriteCoils<'a>), WriteMultipleRegisters(WriteRegisters<'a>), + ReadWriteMultipleRegisters(ReadWriteRegisters<'a>), + WriteCustomFunctionCode(CustomFunctionCode), } /// All requests that support broadcast @@ -64,6 +66,8 @@ impl<'a> Request<'a> { Request::WriteSingleRegister(_) => FunctionCode::WriteSingleRegister, Request::WriteMultipleCoils(_) => FunctionCode::WriteMultipleCoils, Request::WriteMultipleRegisters(_) => FunctionCode::WriteMultipleRegisters, + Request::ReadWriteMultipleRegisters(_) => FunctionCode::ReadWriteMultipleRegisters, + Request::WriteCustomFunctionCode(_) => FunctionCode::WriteCustomFunctionCode, } } @@ -77,6 +81,8 @@ impl<'a> Request<'a> { Request::WriteSingleRegister(x) => Some(BroadcastRequest::WriteSingleRegister(x)), Request::WriteMultipleCoils(x) => Some(BroadcastRequest::WriteMultipleCoils(x)), Request::WriteMultipleRegisters(x) => Some(BroadcastRequest::WriteMultipleRegisters(x)), + Request::ReadWriteMultipleRegisters(_) => None, + Request::WriteCustomFunctionCode(_) => None, } } @@ -141,6 +147,18 @@ impl<'a> Request<'a> { .map(|_| items.range); write_result(function, header, writer, result, level) } + Request::ReadWriteMultipleRegisters(items) => { + let write_registers = &WriteRegisters::new(items.write_range, items.iterator); + let _ = handler.write_multiple_registers(*write_registers).map(|_| write_registers.range); + + let read_registers = ReadRegistersRange{ inner: items.read_range }; + let read_res = RegisterWriter::new(read_registers, |i| handler.read_holding_register(i)); + writer.format_reply(header, function, &read_res, level) + } + Request::WriteCustomFunctionCode(request) => { + let result = handler.write_custom_function_code(*request).map(|_| *request); + write_result(function, header, writer, result, level) + } } } @@ -200,6 +218,25 @@ impl<'a> Request<'a> { RegisterIterator::parse_all(range, cursor)?, ))) } + FunctionCode::ReadWriteMultipleRegisters => { + let read_range = AddressRange::parse(cursor)?; + let write_range = AddressRange::parse(cursor)?; + // don't care about the count, validated b/c all bytes are consumed + cursor.read_u8()?; + let iterator = RegisterIterator::parse_all(write_range, cursor)?; + let read_write_registers = ReadWriteRegisters::new( + read_range, + write_range, + iterator, + ); + Ok(Request::ReadWriteMultipleRegisters(read_write_registers)) + } + FunctionCode::WriteCustomFunctionCode => { + let x = + Request::WriteCustomFunctionCode(CustomFunctionCode::parse(cursor)?); + cursor.expect_empty()?; + Ok(x) + } } } } @@ -253,6 +290,18 @@ impl std::fmt::Display for RequestDisplay<'_, '_> { RegisterIteratorDisplay::new(self.level, items.iterator) )?; } + Request::ReadWriteMultipleRegisters(request) => { + write!( + f, + " {} {} {}", + request.read_range, + request.write_range, + RegisterIteratorDisplay::new(self.level, request.iterator) + )?; + } + Request::WriteCustomFunctionCode(request) => { + write!(f, " {request}")?; + } } } @@ -386,4 +435,123 @@ mod tests { ) } } -} + + mod read_write_multiple_registers { + use scursor::ReadCursor; + + use super::super::*; + use crate::error::AduParseError; + + //ANCHOR: parse read_write_multiple_request + + /// Write a single zero value to register 1 (index 0) - Minimum test + /// Read 5 registers starting at register 1 (index 0-4) afterwards + /// + /// read_range start: 0x00, count: 0x05 + /// write_range start: 0x00, count: 0x01 + /// value length = 2 bytes, value = 0x0000 + #[test] + fn can_parse_read_write_multiple_registers_request_of_single_zero_register_write() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00]); + let registers = match Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor).unwrap() { + Request::ReadWriteMultipleRegisters(registers) => registers, + _ => panic!("bad match"), + }; + assert_eq!(registers.read_range, AddressRange::try_from(0x00, 0x05).unwrap()); + assert_eq!(registers.write_range, AddressRange::try_from(0x00, 0x01).unwrap()); + assert_eq!(registers.iterator.collect::>>(), vec![Indexed::new(0x0000, 0x0000)]); + } + + /// Write a single 0xFFFF value to register 0xFFFF (index 65.535) - Limit test + /// Read 5 registers starting at register 0xFFFB (65.531-65.535) afterwards + /// + /// read_range start: 0xFFFB, count: 0x05 + /// write_range start: 0xFFFF, count: 0x01 + /// value length = 2 bytes, value = 0xFFFF + #[test] + fn can_parse_read_write_multiple_registers_request_of_single_u16_register_write() { + let mut cursor = ReadCursor::new(&[0xFF, 0xFB, 0x00, 0x05, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0xFF, 0xFF]); + let registers = match Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor).unwrap() { + Request::ReadWriteMultipleRegisters(registers) => registers, + _ => panic!("bad match"), + }; + assert_eq!(registers.read_range, AddressRange::try_from(0xFFFB, 0x05).unwrap()); + assert_eq!(registers.write_range, AddressRange::try_from(0xFFFF, 0x01).unwrap()); + assert_eq!(registers.iterator.collect::>>(), vec![Indexed::new(0xFFFF, 0xFFFF)]); + } + + /// Write multiple zero values to registers 1, 2 and 3 (index 0-2) - Minimum test + /// Read 5 registers starting at register 1 (0-4) afterwards + /// + /// read_range start: 0x00, count: 0x05 + /// write_range start: 0x00, count: 0x03 + /// values length = 6 bytes, values = 0x0000, 0x0000, 0x0000 + #[test] + fn can_parse_read_write_multiple_registers_request_of_multiple_zero_register_write() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + let registers = match Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor).unwrap() { + Request::ReadWriteMultipleRegisters(registers) => registers, + _ => panic!("bad match"), + }; + assert_eq!(registers.read_range, AddressRange::try_from(0x00, 0x05).unwrap()); + assert_eq!(registers.write_range, AddressRange::try_from(0x00, 0x03).unwrap()); + assert_eq!(registers.iterator.collect::>>(), vec![Indexed::new(0x0000, 0x0000), Indexed::new(0x0001, 0x0000), Indexed::new(0x0002, 0x0000)]); + } + + /// Write multiple 0xFFFF values to registers 0xFFFD, 0xFFFE and 0xFFFF (index 65.533 - 65.535) - Limit test + /// Read 5 registers starting at register 0xFFFB (65.531-65.535) afterwards + /// + /// read_range start: 0xFFFB, count: 0x05 + /// write_range start: 0xFFFD, count: 0x03 + /// values length = 6 bytes, values = 0xFFFF, 0xFFFF, 0xFFFF + #[test] + fn parse_succeeds_for_valid_read_write_multiple_request_of_multiple_u16_register_write() { + let mut cursor = ReadCursor::new(&[0xFF, 0xFB, 0x00, 0x05, 0xFF, 0xFD, 0x00, 0x03, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + let registers = match Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor).unwrap() { + Request::ReadWriteMultipleRegisters(registers) => registers, + _ => panic!("bad match"), + }; + assert_eq!(registers.read_range, AddressRange::try_from(0xFFFB, 0x05).unwrap()); + assert_eq!(registers.write_range, AddressRange::try_from(0xFFFD, 0x03).unwrap()); + assert_eq!(registers.iterator.collect::>>(), vec![Indexed::new(0xFFFD, 0xFFFF), Indexed::new(0xFFFE, 0xFFFF), Indexed::new(0xFFFF, 0xFFFF)]); + } + + #[test] + fn parse_fails_when_too_few_bytes_for_write_byte_count() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA]); + let err = Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor) + .err() + .unwrap(); + assert_eq!(err, AduParseError::InsufficientBytes.into()); + } + + #[test] + fn parse_fails_when_too_many_bytes_for_write_byte_count() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE, 0xC0]); + let err = Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor) + .err() + .unwrap(); + assert_eq!(err, AduParseError::TrailingBytes(1).into()); + } + + #[test] + fn parse_fails_when_specified_byte_count_not_present() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE]); + let err = Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor) + .err() + .unwrap(); + assert_eq!(err, AduParseError::InsufficientBytes.into()); + } + + #[test] + fn parse_fails_when_too_many_bytes_present() { + let mut cursor = ReadCursor::new(&[0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x06, 0xCA, 0xFE, 0xC0, 0xDE, 0xCA, 0xFE, 0xC0, 0xDE]); + let err = Request::parse(FunctionCode::ReadWriteMultipleRegisters, &mut cursor) + .err() + .unwrap(); + assert_eq!(err, AduParseError::TrailingBytes(2).into()); + } + + //ANCHOR_END: parse read_write_multiple_request + } +} \ No newline at end of file diff --git a/rodbus/src/server/task.rs b/rodbus/src/server/task.rs index fbb7bfca..18068866 100644 --- a/rodbus/src/server/task.rs +++ b/rodbus/src/server/task.rs @@ -264,6 +264,10 @@ impl AuthorizationType { Request::WriteMultipleRegisters(x) => { handler.write_multiple_registers(unit_id, x.range, role) } + Request::ReadWriteMultipleRegisters(x) => { + handler.read_write_multiple_registers(unit_id, x.read_range, x.write_range, role) + } + Request::WriteCustomFunctionCode(x) => handler.write_custom_function_code(*x, role), } } diff --git a/rodbus/src/server/types.rs b/rodbus/src/server/types.rs index 9e7fad72..4d9971b5 100644 --- a/rodbus/src/server/types.rs +++ b/rodbus/src/server/types.rs @@ -29,3 +29,21 @@ impl<'a> WriteRegisters<'a> { Self { range, iterator } } } + + +/// Request to write registers received by the server +#[derive(Debug, Copy, Clone)] +pub struct ReadWriteRegisters<'a> { + /// address range of the read request + pub read_range: AddressRange, + /// address range of the write request + pub write_range: AddressRange, + /// lazy iterator over the register values to write + pub iterator: RegisterIterator<'a>, +} + +impl<'a> ReadWriteRegisters<'a> { + pub(crate) fn new(read_range: AddressRange, write_range: AddressRange, iterator: RegisterIterator<'a>) -> Self { + Self { read_range, write_range, iterator } + } +} \ No newline at end of file diff --git a/rodbus/src/types.rs b/rodbus/src/types.rs index 0f9d94eb..a952384d 100644 --- a/rodbus/src/types.rs +++ b/rodbus/src/types.rs @@ -85,6 +85,13 @@ pub(crate) struct RegisterIteratorDisplay<'a> { level: AppDecodeLevel, } +/// Custom Function Code +#[derive(Clone, Debug, Copy, PartialEq)] +pub struct CustomFunctionCode { + len: usize, + data: [u16; 4], +} + impl std::fmt::Display for UnitId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:#04X}", self.value) @@ -367,6 +374,36 @@ impl Default for UnitId { } } +impl CustomFunctionCode { + /// Create a new custom function code + pub fn new(len: usize, data: [u16; 4]) -> Self { + Self { len, data } + } + + /// Get the length of the underlying vector + pub fn len(&self) -> usize { + self.data.len() + } + + /// Iterate over the underlying vector + pub fn iter(&self) -> std::slice::Iter { + self.data.iter() + } +} + +impl std::fmt::Display for CustomFunctionCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "values: [")?; + for (i, val) in self.data.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{}", val)?; + } + write!(f, "]") + } +} + #[cfg(test)] mod tests { use crate::error::*; diff --git a/rodbus/tests/integration_test.rs b/rodbus/tests/integration_test.rs index d49b9ca5..b8b586fb 100644 --- a/rodbus/tests/integration_test.rs +++ b/rodbus/tests/integration_test.rs @@ -13,6 +13,7 @@ struct Handler { pub discrete_inputs: [bool; 10], pub holding_registers: [u16; 10], pub input_registers: [u16; 10], + pub custom_function_code: [u16; 5], } impl Handler { @@ -22,6 +23,7 @@ impl Handler { discrete_inputs: [false; 10], holding_registers: [0; 10], input_registers: [0; 10], + custom_function_code: [0; 5], } } } @@ -94,6 +96,37 @@ impl RequestHandler for Handler { } Ok(()) } + + fn write_custom_function_code(&mut self, values: CustomFunctionCode) -> Result<(), ExceptionCode> { + for (i, &value) in values.iter().enumerate() { + match self.custom_function_code.get_mut(i) { + Some(c) => *c = value, + None => return Err(ExceptionCode::IllegalDataAddress), + } + } + Ok(()) + } + + fn read_write_multiple_registers(&mut self, _values: ReadWriteRegisters) -> Result<(), ExceptionCode> { + //self.write_multiple_registers(WriteRegisters {range: w_range, iterator: _values.iterator}).unwrap(); + for x in _values.iterator { + match self.holding_registers.get_mut(x.index as usize) { + Some(c) => *c = x.value, + None => return Err(ExceptionCode::IllegalDataAddress), + } + } + + let range = _values.read_range; + let mut r_values = Vec::new(); + for i in range.start..(range.start+range.count) { + match self.holding_registers.get(i as usize) { + Some(x) => r_values.push(Indexed::new(i, *x)), + None => return Err(ExceptionCode::IllegalDataAddress), + } + } + Ok(()) + } + } async fn test_requests_and_responses() { @@ -120,7 +153,7 @@ async fn test_requests_and_responses() { channel.enable().await.unwrap(); - let params = RequestParam::new(UnitId::new(0x01), Duration::from_secs(1)); + let params = RequestParam::new(UnitId::new(0x01), Duration::from_secs(900)); { let mut guard = handler.lock().unwrap(); @@ -222,6 +255,27 @@ async fn test_requests_and_responses() { Indexed::new(2, 0x0506) ] ); + assert_eq!( + channel + .write_custom_function_code(params, CustomFunctionCode::new(0x04, [0xC0, 0xDE, 0xCA, 0xFE])) + .await + .unwrap(), + CustomFunctionCode::new(4, [0xC0, 0xDE, 0xCA, 0xFE]) + ); + assert_eq!( + channel + .read_write_multiple_registers(params, ReadWriteMultiple::new(AddressRange::try_from(0, 5).unwrap(), AddressRange::try_from(0, 3).unwrap(), vec![0xC0DE, 0xCAFE, 0xC0DE]).unwrap()) + .await + .unwrap(), + vec![ + Indexed::new(0, 0xC0DE), + Indexed::new(1, 0xCAFE), + Indexed::new(2, 0xC0DE), + Indexed::new(3, 0x0000), + Indexed::new(4, 0x0000) + ] + ); + } #[test]