diff --git a/rodbus/SCFC_README.md b/rodbus/SCFC_README.md new file mode 100644 index 00000000..4df4f99e --- /dev/null +++ b/rodbus/SCFC_README.md @@ -0,0 +1,87 @@ +# Send Custom Function Code (65-72 & 100-110) + +This document provides a detailed overview of the implemented rodbus custom function code feature. These user-defined function codes fall within the range of 65 to 72 and 100 to 110, as specified in the MODBUS Application Protocol Specification V1.1b3 (Page 10, Section 5: Function Code Categories), and allow custom server-side execution logic. + + +## Introduction +The custom function codes enable 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 variable-size data buffer. + + +## Request Structure +| Parameter | Size | Range / Value | +|---------------------|---------------|-----------------------| +| Function code | 1 Byte | 0x41-0x48 / 0x64-0x6E | +| Byte Count (Input) | 1 Byte | 0x00 to 0xFF (N*) | +| Byte Count (Output) | 1 Byte | 0x00 to 0xFF | +| Data | N* x 2 Bytes | 0x0000 to 0xFFFF | + + +## Response Structure +| Parameter | Size | Value/Description | +|---------------------|--------------|-----------------------| +| Function code | 1 Byte | 0x41-0x48 / 0x64-0x6E | +| Byte Count (Input) | 1 Byte | 0x00 to 0xFF (N*) | +| Byte Count (Output) | 1 Byte | 0x00 to 0xFF | +| Data | N* x 2 Bytes | 0x0000 to 0xFFFF | + + +## Error Handling +| Parameter | Size | Description | +|----------------|---------|----------------------| +| Function code | 1 Byte | Function code + 0x80 | +| 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 FC 69 with a buffer of [0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE] (Byte Count = 4 -> 8 bytes): +| Request Field | Hex | Response Field | Hex | +|---------------------------|-----|------------------------|-----| +| Function code | 45 | Function code | 45 | +| Byte Count (Input) | 04 | Byte Count (Input) | 04 | +| Byte Count (Output) | 04 | Byte Count (Output) | 04 | +| Arg1 Hi | C0 | Arg1 Hi | C0 | +| Arg1 Lo | DE | Arg1 Lo | DF | +| Arg2 Hi | CA | Arg2 Hi | CA | +| Arg2 Lo | FE | Arg2 Lo | FF | +| Arg3 Hi | C0 | Arg3 Hi | C0 | +| Arg3 Lo | DE | Arg3 Lo | DF | +| Arg4 Hi | CA | Arg4 Hi | CA | +| Arg4 Lo | FE | Arg4 Lo | FF | + + +## Usage +Make sure that you are in the `rodbus` project directory. + + +### Start the custom_server example +- `cargo run --example custom_server -- tcp` +- Once it's up, run `ed` to enable decoding + +Leave the terminal open and open another terminal. + + +### Start the custom_client example +- `cargo run --example custom_client -- tcp` +- Once it's up, run `ed` to enable decoding + + +### Send the Custom Function Code 69 +In the terminal with the running custom_client example, run: +- `scfc ` +- E.g. `scfc 0x45 0x02 0x02 0xC0DE 0xCAFE` +- The response would be for example: `fc: 0x45, bytes in: 2, bytes out: 2, values: [49375, 51967], hex: [0xC0DF, 0xCAFF]` + + +## Troubleshooting Tips +- Ensure the server and client are using the same communication method 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..baca1464 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, @@ -267,6 +267,22 @@ async fn run_channel(mut channel: Channel) -> Result<(), Box { + // ANCHOR: send_custom_function_code + let fc = 0x45 as u8; + let byte_count_in = 0x04 as u8; + let byte_count_out = 0x04 as u8; + let values = vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]; // i.e.: Voltage Hi = 0xC0 / Voltage Lo = 0xDE / Current Hi = 0xCA / Current Lo = 0xFE + + let result = channel + .send_custom_function_code( + params, + CustomFunctionCode::new(fc, byte_count_in, byte_count_out, values), + ) + .await; + print_write_result(result); + // ANCHOR_END: send_custom_function_code + } _ => println!("unknown command"), } } diff --git a/rodbus/examples/custom_client.rs b/rodbus/examples/custom_client.rs new file mode 100644 index 00000000..df77c0f7 --- /dev/null +++ b/rodbus/examples/custom_client.rs @@ -0,0 +1,235 @@ +use std::error::Error; +use std::net::{IpAddr, Ipv4Addr}; +use std::process::exit; +use std::time::Duration; + +use tokio_stream::StreamExt; +use tokio_util::codec::{FramedRead, LinesCodec}; + +use rodbus::client::*; +use rodbus::*; + +// ANCHOR: runtime_init +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<(), Box> { + // ANCHOR_END: runtime_init + + // ANCHOR: logging + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .init(); + // ANCHOR_END: logging + + let args: Vec = std::env::args().collect(); + let transport: &str = match &args[..] { + [_, x] => x, + _ => { + eprintln!("please specify a transport:"); + eprintln!("usage: outstation (tcp, rtu, tls-ca, tls-self-signed)"); + exit(-1); + } + }; + match transport { + "tcp" => run_tcp().await, + #[cfg(feature = "serial")] + "rtu" => run_rtu().await, + #[cfg(feature = "tls")] + "tls-ca" => run_tls(get_ca_chain_config()?).await, + #[cfg(feature = "tls")] + "tls-self-signed" => run_tls(get_self_signed_config()?).await, + _ => { + eprintln!( + "unknown transport '{transport}', options are (tcp, rtu, tls-ca, tls-self-signed)" + ); + exit(-1); + } + } +} + +struct LoggingListener; +impl Listener for LoggingListener +where + T: std::fmt::Debug, +{ + fn update(&mut self, value: T) -> MaybeAsync<()> { + tracing::info!("Channel Listener: {:?}", value); + MaybeAsync::ready(()) + } +} + +async fn run_tcp() -> Result<(), Box> { + // ANCHOR: create_tcp_channel + let channel = spawn_tcp_client_task( + HostAddr::ip(IpAddr::V4(Ipv4Addr::LOCALHOST), 11502), + 1, + default_retry_strategy(), + DecodeLevel::default(), + Some(Box::new(LoggingListener)), + ); + // ANCHOR_END: create_tcp_channel + + run_channel(channel).await +} + +#[cfg(feature = "serial")] +async fn run_rtu() -> Result<(), Box> { + // ANCHOR: create_rtu_channel + let channel = spawn_rtu_client_task( + "/dev/ttySIM0", // path + rodbus::SerialSettings::default(), // serial settings + 1, // max queued requests + default_retry_strategy(), // retry delays + DecodeLevel::new( + AppDecodeLevel::DataValues, + FrameDecodeLevel::Payload, + PhysDecodeLevel::Nothing, + ), + Some(Box::new(LoggingListener)), + ); + // ANCHOR_END: create_rtu_channel + + run_channel(channel).await +} + +#[cfg(feature = "tls")] +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), 11802), + 1, + default_retry_strategy(), + tls_config, + DecodeLevel::new( + AppDecodeLevel::DataValues, + FrameDecodeLevel::Nothing, + PhysDecodeLevel::Nothing, + ), + Some(Box::new(LoggingListener)), + ); + // ANCHOR_END: create_tls_channel + + run_channel(channel).await +} + +#[cfg(feature = "tls")] +fn get_self_signed_config() -> Result> { + use std::path::Path; + // ANCHOR: tls_self_signed_config + let tls_config = TlsClientConfig::self_signed( + Path::new("./certs/self_signed/entity2_cert.pem"), + Path::new("./certs/self_signed/entity1_cert.pem"), + Path::new("./certs/self_signed/entity1_key.pem"), + None, // no password + MinTlsVersion::V1_2, + )?; + // ANCHOR_END: tls_self_signed_config + + Ok(tls_config) +} + +#[cfg(feature = "tls")] +fn get_ca_chain_config() -> Result> { + use std::path::Path; + // ANCHOR: tls_ca_chain_config + let tls_config = TlsClientConfig::full_pki( + Some("test.com".to_string()), + Path::new("./certs/ca_chain/ca_cert.pem"), + Path::new("./certs/ca_chain/client_cert.pem"), + Path::new("./certs/ca_chain/client_key.pem"), + None, // no password + MinTlsVersion::V1_2, + )?; + // ANCHOR_END: tls_ca_chain_config + + Ok(tls_config) +} + +/*fn print_read_result(result: Result>, RequestError>) +where + T: std::fmt::Display, +{ + match result { + Ok(registers) => { + for register in registers { + println!("index: {} value: {}", register.index, register.value); + } + } + Err(rodbus::RequestError::Exception(exception)) => { + println!("Modbus exception: {exception}"); + } + Err(err) => println!("read error: {err}"), + } +}*/ + +fn print_write_result(result: Result) { + match result { + Ok(_) => { + println!("write successful"); + } + Err(rodbus::RequestError::Exception(exception)) => { + println!("Modbus exception: {exception}"); + } + Err(err) => println!("writer error: {err}"), + } +} + +async fn run_channel(mut channel: Channel) -> Result<(), Box> { + channel.enable().await?; + + // ANCHOR: request_param + let params = RequestParam::new(UnitId::new(1), Duration::from_secs(1)); + // ANCHOR_END: request_param + + let mut reader = FramedRead::new(tokio::io::stdin(), LinesCodec::new()); + while let Some(line) = reader.next().await { + let line = line?; // This handles the Some(Err(e)) case by returning Err(e) + let parts = line.split_whitespace().collect::>(); + match parts.as_slice() { + ["x"] => return Ok(()), + ["ec"] => { + channel.enable().await?; + } + ["dc"] => { + channel.disable().await?; + } + ["ed"] => { + channel + .set_decode_level(DecodeLevel::new( + AppDecodeLevel::DataValues, + FrameDecodeLevel::Payload, + PhysDecodeLevel::Data, + )) + .await?; + } + ["dd"] => { + channel.set_decode_level(DecodeLevel::nothing()).await?; + } + ["scfc", fc_str, bytes_in_str, bytes_out_str, values @ ..] => { + let fc = u8::from_str_radix(fc_str.trim_start_matches("0x"), 16).unwrap(); + let byte_count_in = + u8::from_str_radix(bytes_in_str.trim_start_matches("0x"), 16).unwrap(); + let byte_count_out = + u8::from_str_radix(bytes_out_str.trim_start_matches("0x"), 16).unwrap(); + let values: Vec = values + .iter() + .filter_map(|&v| u16::from_str_radix(v.trim_start_matches("0x"), 16).ok()) + .collect(); + + if (fc >= 65 && fc <= 72) || (fc >= 100 && fc <= 110) { + let result = channel + .send_custom_function_code( + params, + CustomFunctionCode::new(fc, byte_count_in, byte_count_out, values), + ) + .await; + print_write_result(result); + } else { + println!("Error: CFC number is not inside the range of 65-72 or 100-110."); + } + } + _ => println!("unknown command"), + } + } + Ok(()) +} diff --git a/rodbus/examples/custom_server.rs b/rodbus/examples/custom_server.rs new file mode 100644 index 00000000..74d8ed1b --- /dev/null +++ b/rodbus/examples/custom_server.rs @@ -0,0 +1,380 @@ +use std::process::exit; + +use tokio_stream::StreamExt; +use tokio_util::codec::{FramedRead, LinesCodec}; + +use rodbus::server::*; +use rodbus::*; + +struct SimpleHandler { + coils: Vec, + discrete_inputs: Vec, + holding_registers: Vec, + input_registers: Vec, +} + +impl SimpleHandler { + fn new( + coils: Vec, + discrete_inputs: Vec, + holding_registers: Vec, + input_registers: Vec, + ) -> Self { + Self { + coils, + discrete_inputs, + holding_registers, + input_registers, + } + } + + fn coils_as_mut(&mut self) -> &mut [bool] { + self.coils.as_mut_slice() + } + + fn discrete_inputs_as_mut(&mut self) -> &mut [bool] { + self.discrete_inputs.as_mut_slice() + } + + fn holding_registers_as_mut(&mut self) -> &mut [u16] { + self.holding_registers.as_mut_slice() + } + + fn input_registers_as_mut(&mut self) -> &mut [u16] { + self.input_registers.as_mut_slice() + } +} + +// ANCHOR: request_handler +impl RequestHandler for SimpleHandler { + fn read_coil(&self, address: u16) -> Result { + self.coils.get(address as usize).to_result() + } + + fn read_discrete_input(&self, address: u16) -> Result { + self.discrete_inputs.get(address as usize).to_result() + } + + fn read_holding_register(&self, address: u16) -> Result { + self.holding_registers.get(address as usize).to_result() + } + + fn read_input_register(&self, address: u16) -> Result { + self.input_registers.get(address as usize).to_result() + } + + fn write_single_coil(&mut self, value: Indexed) -> Result<(), ExceptionCode> { + tracing::info!( + "write single coil, index: {} value: {}", + value.index, + value.value + ); + + if let Some(coil) = self.coils.get_mut(value.index as usize) { + *coil = value.value; + Ok(()) + } else { + Err(ExceptionCode::IllegalDataAddress) + } + } + + fn process_cfc( + &mut self, + values: CustomFunctionCode, + ) -> Result, ExceptionCode> { + tracing::info!( + "processing custom function code: {}", + values.function_code() + ); + match values.function_code() { + 0x41 => { + // increment each CFC value by 1 and return the result + // Create a new vector to hold the incremented values + let incremented_data = values.iter().map(|&val| val + 1).collect(); + + // Return a new CustomFunctionCode with the incremented data + Ok(CustomFunctionCode::new( + values.function_code(), + values.byte_count_in(), + values.byte_count_out(), + incremented_data, + )) + } + 0x42 => { + // add a new value to the buffer and return the result + // Create a new vector to hold the incremented values + let extended_data = { + let mut extended_data = values.iter().map(|val| *val).collect::>(); + extended_data.push(0xC0DE); + extended_data + }; + + // Return a new CustomFunctionCode with the incremented data + Ok(CustomFunctionCode::new( + values.function_code(), + values.byte_count_in(), + values.byte_count_out(), + extended_data, + )) + } + 0x43 => { + // remove the first value from the buffer and return the result + // Create a new vector to hold the incremented values + let truncated_data = { + let mut truncated_data = values.iter().map(|val| *val).collect::>(); + truncated_data.pop(); + truncated_data + }; + + // Return a new CustomFunctionCode with the incremented data + Ok(CustomFunctionCode::new( + values.function_code(), + values.byte_count_in(), + values.byte_count_out(), + truncated_data, + )) + } + _ => Err(ExceptionCode::IllegalFunction), + } + } + + fn write_single_register(&mut self, value: Indexed) -> Result<(), ExceptionCode> { + tracing::info!( + "write single register, index: {} value: {}", + value.index, + value.value + ); + + if let Some(reg) = self.holding_registers.get_mut(value.index as usize) { + *reg = value.value; + Ok(()) + } else { + Err(ExceptionCode::IllegalDataAddress) + } + } + + fn write_multiple_coils(&mut self, values: WriteCoils) -> Result<(), ExceptionCode> { + tracing::info!("write multiple coils {:?}", values.range); + + let mut result = Ok(()); + + for value in values.iterator { + if let Some(coil) = self.coils.get_mut(value.index as usize) { + *coil = value.value; + } else { + result = Err(ExceptionCode::IllegalDataAddress) + } + } + + result + } + + fn write_multiple_registers(&mut self, values: WriteRegisters) -> Result<(), ExceptionCode> { + tracing::info!("write multiple registers {:?}", values.range); + + let mut result = Ok(()); + + for value in values.iterator { + if let Some(reg) = self.holding_registers.get_mut(value.index as usize) { + *reg = value.value; + } else { + result = Err(ExceptionCode::IllegalDataAddress) + } + } + + result + } +} +// ANCHOR_END: request_handler + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<(), Box> { + // initialize logging + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .init(); + + let args: Vec = std::env::args().collect(); + let transport: &str = match &args[..] { + [_, x] => x, + _ => { + eprintln!("please specify a transport:"); + eprintln!("usage: outstation (tcp, rtu, tls-ca, tls-self-signed)"); + exit(-1); + } + }; + match transport { + "tcp" => run_tcp().await, + #[cfg(feature = "serial")] + "rtu" => run_rtu().await, + #[cfg(feature = "tls")] + "tls-ca" => run_tls(get_ca_chain_config()?).await, + #[cfg(feature = "tls")] + "tls-self-signed" => run_tls(get_self_signed_config()?).await, + _ => { + eprintln!( + "unknown transport '{transport}', options are (tcp, rtu, tls-ca, tls-self-signed)" + ); + exit(-1); + } + } +} + +async fn run_tcp() -> Result<(), Box> { + let (handler, map) = create_handler(); + + // ANCHOR: tcp_server_create + let server = rodbus::server::spawn_tcp_server_task( + 1, + "127.0.0.1:11502".parse()?, + map, + AddressFilter::Any, + DecodeLevel::default(), + ) + .await?; + // ANCHOR_END: tcp_server_create + + run_server(server, handler).await +} + +#[cfg(feature = "serial")] +async fn run_rtu() -> Result<(), Box> { + let (handler, map) = create_handler(); + + // ANCHOR: rtu_server_create + let server = rodbus::server::spawn_rtu_server_task( + "/dev/ttySIM1", + rodbus::SerialSettings::default(), + default_retry_strategy(), + map, + DecodeLevel::new( + AppDecodeLevel::DataValues, + FrameDecodeLevel::Payload, + PhysDecodeLevel::Data, + ), + )?; + // ANCHOR_END: rtu_server_create + + run_server(server, handler).await +} + +#[cfg(feature = "tls")] +async fn run_tls(tls_config: TlsServerConfig) -> Result<(), Box> { + let (handler, map) = create_handler(); + + // ANCHOR: tls_server_create + let server = rodbus::server::spawn_tls_server_task_with_authz( + 1, + "127.0.0.1:11802".parse()?, + map, + ReadOnlyAuthorizationHandler::create(), + tls_config, + AddressFilter::Any, + DecodeLevel::default(), + ) + .await?; + // ANCHOR_END: tls_server_create + + run_server(server, handler).await +} + +fn create_handler() -> ( + ServerHandlerType, + ServerHandlerMap, +) { + // ANCHOR: handler_map_create + let handler = + SimpleHandler::new(vec![false; 10], vec![false; 10], vec![0; 10], vec![0; 10]).wrap(); + + // map unit ids to a handler for processing requests + let map = ServerHandlerMap::single(UnitId::new(1), handler.clone()); + // ANCHOR_END: handler_map_create + + (handler, map) +} + +#[cfg(feature = "tls")] +fn get_self_signed_config() -> Result> { + use std::path::Path; + // ANCHOR: tls_self_signed_config + let tls_config = TlsServerConfig::new( + Path::new("./certs/self_signed/entity1_cert.pem"), + Path::new("./certs/self_signed/entity2_cert.pem"), + Path::new("./certs/self_signed/entity2_key.pem"), + None, // no password + MinTlsVersion::V1_2, + CertificateMode::SelfSigned, + )?; + // ANCHOR_END: tls_self_signed_config + + Ok(tls_config) +} + +#[cfg(feature = "tls")] +fn get_ca_chain_config() -> Result> { + use std::path::Path; + // ANCHOR: tls_ca_chain_config + let tls_config = TlsServerConfig::new( + Path::new("./certs/ca_chain/ca_cert.pem"), + Path::new("./certs/ca_chain/server_cert.pem"), + Path::new("./certs/ca_chain/server_key.pem"), + None, // no password + MinTlsVersion::V1_2, + CertificateMode::AuthorityBased, + )?; + // ANCHOR_END: tls_ca_chain_config + + Ok(tls_config) +} + +async fn run_server( + mut server: ServerHandle, + handler: ServerHandlerType, +) -> Result<(), Box> { + let mut reader = FramedRead::new(tokio::io::stdin(), LinesCodec::new()); + loop { + match reader.next().await.unwrap()?.as_str() { + "x" => return Ok(()), + "ed" => { + // enable decoding + server + .set_decode_level(DecodeLevel::new( + AppDecodeLevel::DataValues, + FrameDecodeLevel::Payload, + PhysDecodeLevel::Data, + )) + .await?; + } + "dd" => { + // disable decoding + server.set_decode_level(DecodeLevel::nothing()).await?; + } + "uc" => { + let mut handler = handler.lock().unwrap(); + for coil in handler.coils_as_mut() { + *coil = !*coil; + } + } + "udi" => { + let mut handler = handler.lock().unwrap(); + for discrete_input in handler.discrete_inputs_as_mut() { + *discrete_input = !*discrete_input; + } + } + "uhr" => { + let mut handler = handler.lock().unwrap(); + for holding_register in handler.holding_registers_as_mut() { + *holding_register += 1; + } + } + "uir" => { + let mut handler = handler.lock().unwrap(); + for input_register in handler.input_registers_as_mut() { + *input_register += 1; + } + } + _ => println!("unknown command"), + } + } +} diff --git a/rodbus/examples/server.rs b/rodbus/examples/server.rs index 4fc490cb..c132eb2c 100644 --- a/rodbus/examples/server.rs +++ b/rodbus/examples/server.rs @@ -167,7 +167,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 +206,7 @@ async fn run_tls(tls_config: TlsServerConfig) -> Result<(), Box, + ) -> Result, RequestError> { + let (tx, rx) = + tokio::sync::oneshot::channel::, RequestError>>(); + let request = wrap( + param, + RequestDetails::SendCustomFunctionCode(CustomFCRequest::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, diff --git a/rodbus/src/client/message.rs b/rodbus/src/client/message.rs index c18abb7e..6d118ecb 100644 --- a/rodbus/src/client/message.rs +++ b/rodbus/src/client/message.rs @@ -8,10 +8,11 @@ use crate::DecodeLevel; use crate::client::requests::read_bits::ReadBits; use crate::client::requests::read_registers::ReadRegisters; +use crate::client::requests::send_custom_fc::CustomFCRequest; use crate::client::requests::write_multiple::MultipleWriteRequest; use crate::client::requests::write_single::SingleWrite; use crate::common::traits::Serialize; -use crate::types::{Indexed, UnitId}; +use crate::types::{CustomFunctionCode, Indexed, UnitId}; use scursor::{ReadCursor, WriteCursor}; use std::time::Duration; @@ -45,6 +46,7 @@ pub(crate) enum RequestDetails { WriteSingleRegister(SingleWrite>), WriteMultipleCoils(MultipleWriteRequest), WriteMultipleRegisters(MultipleWriteRequest), + SendCustomFunctionCode(CustomFCRequest>), } impl Request { @@ -61,7 +63,7 @@ impl Request { payload: &[u8], decode: AppDecodeLevel, ) -> Result<(), RequestError> { - let expected_function = self.details.function(); + let expected_function = self.details.function()?; let mut cursor = ReadCursor::new(payload); let function = match cursor.read_u8() { Ok(x) => x, @@ -119,16 +121,38 @@ impl Request { } impl RequestDetails { - pub(crate) fn function(&self) -> FunctionCode { + pub(crate) fn function(&self) -> Result { match self { - RequestDetails::ReadCoils(_) => FunctionCode::ReadCoils, - RequestDetails::ReadDiscreteInputs(_) => FunctionCode::ReadDiscreteInputs, - RequestDetails::ReadHoldingRegisters(_) => FunctionCode::ReadHoldingRegisters, - RequestDetails::ReadInputRegisters(_) => FunctionCode::ReadInputRegisters, - RequestDetails::WriteSingleCoil(_) => FunctionCode::WriteSingleCoil, - RequestDetails::WriteSingleRegister(_) => FunctionCode::WriteSingleRegister, - RequestDetails::WriteMultipleCoils(_) => FunctionCode::WriteMultipleCoils, - RequestDetails::WriteMultipleRegisters(_) => FunctionCode::WriteMultipleRegisters, + RequestDetails::ReadCoils(_) => Ok(FunctionCode::ReadCoils), + RequestDetails::ReadDiscreteInputs(_) => Ok(FunctionCode::ReadDiscreteInputs), + RequestDetails::ReadHoldingRegisters(_) => Ok(FunctionCode::ReadHoldingRegisters), + RequestDetails::ReadInputRegisters(_) => Ok(FunctionCode::ReadInputRegisters), + RequestDetails::WriteSingleCoil(_) => Ok(FunctionCode::WriteSingleCoil), + RequestDetails::WriteSingleRegister(_) => Ok(FunctionCode::WriteSingleRegister), + RequestDetails::WriteMultipleCoils(_) => Ok(FunctionCode::WriteMultipleCoils), + RequestDetails::WriteMultipleRegisters(_) => Ok(FunctionCode::WriteMultipleRegisters), + RequestDetails::SendCustomFunctionCode(x) => match x.request.function_code() { + 0x41 => Ok(FunctionCode::SendCFC65), + 0x42 => Ok(FunctionCode::SendCFC66), + 0x43 => Ok(FunctionCode::SendCFC67), + 0x44 => Ok(FunctionCode::SendCFC68), + 0x45 => Ok(FunctionCode::SendCFC69), + 0x46 => Ok(FunctionCode::SendCFC70), + 0x47 => Ok(FunctionCode::SendCFC71), + 0x48 => Ok(FunctionCode::SendCFC72), + 0x64 => Ok(FunctionCode::SendCFC100), + 0x65 => Ok(FunctionCode::SendCFC101), + 0x66 => Ok(FunctionCode::SendCFC102), + 0x67 => Ok(FunctionCode::SendCFC103), + 0x68 => Ok(FunctionCode::SendCFC104), + 0x69 => Ok(FunctionCode::SendCFC105), + 0x6A => Ok(FunctionCode::SendCFC106), + 0x6B => Ok(FunctionCode::SendCFC107), + 0x6C => Ok(FunctionCode::SendCFC108), + 0x6D => Ok(FunctionCode::SendCFC109), + 0x6E => Ok(FunctionCode::SendCFC110), + _ => Err(ExceptionCode::IllegalFunction), + }, } } @@ -142,6 +166,7 @@ impl RequestDetails { RequestDetails::WriteSingleRegister(x) => x.failure(err), RequestDetails::WriteMultipleCoils(x) => x.failure(err), RequestDetails::WriteMultipleRegisters(x) => x.failure(err), + RequestDetails::SendCustomFunctionCode(x) => x.failure(err), } } @@ -150,7 +175,7 @@ impl RequestDetails { cursor: ReadCursor, decode: AppDecodeLevel, ) -> Result<(), RequestError> { - let function = self.function(); + let function = self.function()?; match self { RequestDetails::ReadCoils(x) => x.handle_response(cursor, function, decode), RequestDetails::ReadDiscreteInputs(x) => x.handle_response(cursor, function, decode), @@ -162,6 +187,9 @@ impl RequestDetails { RequestDetails::WriteMultipleRegisters(x) => { x.handle_response(cursor, function, decode) } + RequestDetails::SendCustomFunctionCode(x) => { + x.handle_response(cursor, function, decode) + } } } } @@ -177,6 +205,7 @@ impl Serialize for RequestDetails { RequestDetails::WriteSingleRegister(x) => x.serialize(cursor), RequestDetails::WriteMultipleCoils(x) => x.serialize(cursor), RequestDetails::WriteMultipleRegisters(x) => x.serialize(cursor), + RequestDetails::SendCustomFunctionCode(x) => x.serialize(cursor), } } } @@ -241,6 +270,9 @@ impl std::fmt::Display for RequestDetailsDisplay<'_> { } } } + RequestDetails::SendCustomFunctionCode(details) => { + write!(f, "{}", details.request)?; + } } } diff --git a/rodbus/src/client/requests/mod.rs b/rodbus/src/client/requests/mod.rs index bfebd0a4..83353546 100644 --- a/rodbus/src/client/requests/mod.rs +++ b/rodbus/src/client/requests/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod read_bits; pub(crate) mod read_registers; +pub(crate) mod send_custom_fc; pub(crate) mod write_multiple; pub(crate) mod write_single; diff --git a/rodbus/src/client/requests/send_custom_fc.rs b/rodbus/src/client/requests/send_custom_fc.rs new file mode 100644 index 00000000..f4f1820f --- /dev/null +++ b/rodbus/src/client/requests/send_custom_fc.rs @@ -0,0 +1,104 @@ +use std::fmt::Display; + +use crate::client::message::Promise; +use crate::common::function::FunctionCode; +use crate::decode::AppDecodeLevel; +use crate::error::AduParseError; +use crate::error::RequestError; +use crate::CustomFunctionCode; + +use scursor::{ReadCursor, WriteCursor}; + +pub(crate) trait CustomFCOperation: Sized + PartialEq { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError>; + fn parse(cursor: &mut ReadCursor) -> Result; +} + +pub(crate) struct CustomFCRequest +where + T: CustomFCOperation + Display + Send + 'static, +{ + pub(crate) request: CustomFunctionCode, + promise: Promise, +} + +impl CustomFCRequest +where + T: CustomFCOperation + Display + Send + 'static, +{ + pub(crate) fn new(request: CustomFunctionCode, 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()?; + Ok(response) + } +} + +impl CustomFCOperation for CustomFunctionCode { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + cursor.write_u8(self.function_code())?; + cursor.write_u8(self.byte_count_in())?; + cursor.write_u8(self.byte_count_out())?; + + for &item in self.iter() { + cursor.write_u16_be(item)?; + } + + Ok(()) + } + + fn parse(cursor: &mut ReadCursor) -> Result { + let fc = cursor.read_u8()?; + let byte_count_in = cursor.read_u8()?; + let byte_count_out = cursor.read_u8()?; + let len = byte_count_out as usize; + + if len != cursor.remaining() / 2 { + return Err( + AduParseError::InsufficientBytesForByteCount(len, cursor.remaining() / 2).into(), + ); + } + + let mut values = Vec::with_capacity(len); + for _ in 0..len { + values.push(cursor.read_u16_be()?); + } + cursor.expect_empty()?; + + Ok(CustomFunctionCode::new( + fc, + byte_count_in, + byte_count_out, + values, + )) + } +} diff --git a/rodbus/src/client/task.rs b/rodbus/src/client/task.rs index 6b92d238..d36f0144 100644 --- a/rodbus/src/client/task.rs +++ b/rodbus/src/client/task.rs @@ -193,7 +193,7 @@ impl ClientLoop { ) -> Result<(), RequestError> { let bytes = self.writer.format_request( FrameHeader::new_tcp_header(request.id, tx_id), - request.details.function(), + request.details.function()?, &request.details, self.decode, )?; diff --git a/rodbus/src/common/function.rs b/rodbus/src/common/function.rs index e85bf14a..2960afc4 100644 --- a/rodbus/src/common/function.rs +++ b/rodbus/src/common/function.rs @@ -9,6 +9,25 @@ 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 SEND_CFC_65: u8 = 65; + pub(crate) const SEND_CFC_66: u8 = 66; + pub(crate) const SEND_CFC_67: u8 = 67; + pub(crate) const SEND_CFC_68: u8 = 68; + pub(crate) const SEND_CFC_69: u8 = 69; + pub(crate) const SEND_CFC_70: u8 = 70; + pub(crate) const SEND_CFC_71: u8 = 71; + pub(crate) const SEND_CFC_72: u8 = 72; + pub(crate) const SEND_CFC_100: u8 = 100; + pub(crate) const SEND_CFC_101: u8 = 101; + pub(crate) const SEND_CFC_102: u8 = 102; + pub(crate) const SEND_CFC_103: u8 = 103; + pub(crate) const SEND_CFC_104: u8 = 104; + pub(crate) const SEND_CFC_105: u8 = 105; + pub(crate) const SEND_CFC_106: u8 = 106; + pub(crate) const SEND_CFC_107: u8 = 107; + pub(crate) const SEND_CFC_108: u8 = 108; + pub(crate) const SEND_CFC_109: u8 = 109; + pub(crate) const SEND_CFC_110: u8 = 110; } #[derive(Debug, Copy, Clone, PartialEq)] @@ -22,6 +41,25 @@ pub(crate) enum FunctionCode { WriteSingleRegister = constants::WRITE_SINGLE_REGISTER, WriteMultipleCoils = constants::WRITE_MULTIPLE_COILS, WriteMultipleRegisters = constants::WRITE_MULTIPLE_REGISTERS, + SendCFC65 = constants::SEND_CFC_65, + SendCFC66 = constants::SEND_CFC_66, + SendCFC67 = constants::SEND_CFC_67, + SendCFC68 = constants::SEND_CFC_68, + SendCFC69 = constants::SEND_CFC_69, + SendCFC70 = constants::SEND_CFC_70, + SendCFC71 = constants::SEND_CFC_71, + SendCFC72 = constants::SEND_CFC_72, + SendCFC100 = constants::SEND_CFC_100, + SendCFC101 = constants::SEND_CFC_101, + SendCFC102 = constants::SEND_CFC_102, + SendCFC103 = constants::SEND_CFC_103, + SendCFC104 = constants::SEND_CFC_104, + SendCFC105 = constants::SEND_CFC_105, + SendCFC106 = constants::SEND_CFC_106, + SendCFC107 = constants::SEND_CFC_107, + SendCFC108 = constants::SEND_CFC_108, + SendCFC109 = constants::SEND_CFC_109, + SendCFC110 = constants::SEND_CFC_110, } impl Display for FunctionCode { @@ -49,6 +87,27 @@ impl Display for FunctionCode { FunctionCode::WriteMultipleRegisters => { write!(f, "WRITE MULTIPLE REGISTERS ({:#04X})", self.get_value()) } + FunctionCode::SendCFC65 + | FunctionCode::SendCFC66 + | FunctionCode::SendCFC67 + | FunctionCode::SendCFC68 + | FunctionCode::SendCFC69 + | FunctionCode::SendCFC70 + | FunctionCode::SendCFC71 + | FunctionCode::SendCFC72 + | FunctionCode::SendCFC100 + | FunctionCode::SendCFC101 + | FunctionCode::SendCFC102 + | FunctionCode::SendCFC103 + | FunctionCode::SendCFC104 + | FunctionCode::SendCFC105 + | FunctionCode::SendCFC106 + | FunctionCode::SendCFC107 + | FunctionCode::SendCFC108 + | FunctionCode::SendCFC109 + | FunctionCode::SendCFC110 => { + write!(f, "SEND CUSTOM FUNCTION CODE ({:#04X})", self.get_value()) + } } } } @@ -72,6 +131,25 @@ impl FunctionCode { constants::WRITE_SINGLE_REGISTER => Some(FunctionCode::WriteSingleRegister), constants::WRITE_MULTIPLE_COILS => Some(FunctionCode::WriteMultipleCoils), constants::WRITE_MULTIPLE_REGISTERS => Some(FunctionCode::WriteMultipleRegisters), + constants::SEND_CFC_65 => Some(FunctionCode::SendCFC65), + constants::SEND_CFC_66 => Some(FunctionCode::SendCFC66), + constants::SEND_CFC_67 => Some(FunctionCode::SendCFC67), + constants::SEND_CFC_68 => Some(FunctionCode::SendCFC68), + constants::SEND_CFC_69 => Some(FunctionCode::SendCFC69), + constants::SEND_CFC_70 => Some(FunctionCode::SendCFC70), + constants::SEND_CFC_71 => Some(FunctionCode::SendCFC71), + constants::SEND_CFC_72 => Some(FunctionCode::SendCFC72), + constants::SEND_CFC_100 => Some(FunctionCode::SendCFC100), + constants::SEND_CFC_101 => Some(FunctionCode::SendCFC101), + constants::SEND_CFC_102 => Some(FunctionCode::SendCFC102), + constants::SEND_CFC_103 => Some(FunctionCode::SendCFC103), + constants::SEND_CFC_104 => Some(FunctionCode::SendCFC104), + constants::SEND_CFC_105 => Some(FunctionCode::SendCFC105), + constants::SEND_CFC_106 => Some(FunctionCode::SendCFC106), + constants::SEND_CFC_107 => Some(FunctionCode::SendCFC107), + constants::SEND_CFC_108 => Some(FunctionCode::SendCFC108), + constants::SEND_CFC_109 => Some(FunctionCode::SendCFC109), + constants::SEND_CFC_110 => Some(FunctionCode::SendCFC110), _ => None, } } diff --git a/rodbus/src/common/parse.rs b/rodbus/src/common/parse.rs index 8b6dce04..7d2e87e4 100644 --- a/rodbus/src/common/parse.rs +++ b/rodbus/src/common/parse.rs @@ -1,6 +1,6 @@ use crate::common::traits::Parse; use crate::error::*; -use crate::types::{coil_from_u16, AddressRange, Indexed}; +use crate::types::{coil_from_u16, AddressRange, CustomFunctionCode, Indexed}; use scursor::ReadCursor; @@ -28,6 +28,34 @@ impl Parse for Indexed { } } +impl Parse for CustomFunctionCode { + fn parse(cursor: &mut ReadCursor) -> Result { + let fc = cursor.read_u8()?; + let byte_count_in = cursor.read_u8()?; + let byte_count_out = cursor.read_u8()?; + let len = byte_count_in as usize; + + if len != cursor.remaining() / 2 { + return Err( + AduParseError::InsufficientBytesForByteCount(len, cursor.remaining() / 2).into(), + ); + } + + let mut values = Vec::with_capacity(len); + for _ in 0..len { + values.push(cursor.read_u16_be()?); + } + cursor.expect_empty()?; + + Ok(CustomFunctionCode::new( + fc, + byte_count_in, + byte_count_out, + values, + )) + } +} + #[cfg(test)] mod coils { use crate::common::traits::Parse; @@ -64,3 +92,76 @@ mod coils { assert_eq!(result, Ok(Indexed::new(1, 0xCAFE))); } } + +#[cfg(test)] +mod custom_fc { + use crate::common::traits::Parse; + use crate::error::AduParseError; + use crate::types::CustomFunctionCode; + + use scursor::ReadCursor; + + #[test] + fn parse_succeeds_for_single_min_value() { + let mut cursor = ReadCursor::new(&[0x45, 0x01, 0x01, 0x00, 0x00]); + let result = CustomFunctionCode::parse(&mut cursor); + assert_eq!(result, Ok(CustomFunctionCode::new(69, 1, 1, vec![0x0000]))); + } + + #[test] + fn parse_succeeds_for_single_max_value() { + let mut cursor = ReadCursor::new(&[0x45, 0x01, 0x01, 0xFF, 0xFF]); + let result = CustomFunctionCode::parse(&mut cursor); + assert_eq!(result, Ok(CustomFunctionCode::new(69, 1, 1, vec![0xFFFF]))); + } + + #[test] + fn parse_succeeds_for_multiple_min_values() { + let mut cursor = ReadCursor::new(&[0x45, 0x03, 0x3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + let result = CustomFunctionCode::parse(&mut cursor); + assert_eq!( + result, + Ok(CustomFunctionCode::new( + 69, + 3, + 3, + vec![0x0000, 0x0000, 0x0000] + )) + ); + } + + #[test] + fn parse_succeeds_for_multiple_max_values() { + let mut cursor = ReadCursor::new(&[0x45, 0x03, 0x3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + let result = CustomFunctionCode::parse(&mut cursor); + assert_eq!( + result, + Ok(CustomFunctionCode::new( + 69, + 3, + 3, + vec![0xFFFF, 0xFFFF, 0xFFFF] + )) + ); + } + + #[test] + fn parse_fails_for_missing_byte_count() { + let mut cursor = ReadCursor::new(&[0x45, 0x01, 0xFF, 0xFF]); + let result = CustomFunctionCode::parse(&mut cursor); + assert_eq!( + result, + Err(AduParseError::InsufficientBytesForByteCount(1, 0).into()) + ); + } + + #[test] + fn parse_fails_for_missing_data_byte() { + let mut cursor = ReadCursor::new(&[0x00, 0x01, 0xFF]); + let result = CustomFunctionCode::parse(&mut cursor); + assert_eq!( + result, + Err(AduParseError::InsufficientBytesForByteCount(1, 0).into()) + ); + } +} diff --git a/rodbus/src/common/serialize.rs b/rodbus/src/common/serialize.rs index 905cfe01..dc56be85 100644 --- a/rodbus/src/common/serialize.rs +++ b/rodbus/src/common/serialize.rs @@ -7,8 +7,8 @@ use crate::common::traits::Serialize; 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, + coil_from_u16, coil_to_u16, AddressRange, BitIterator, BitIteratorDisplay, CustomFunctionCode, + Indexed, RegisterIterator, RegisterIteratorDisplay, }; use scursor::{ReadCursor, WriteCursor}; @@ -290,6 +290,66 @@ impl Serialize for WriteMultiple { } } +impl Serialize for CustomFunctionCode { + fn serialize(&self, cursor: &mut WriteCursor) -> Result<(), RequestError> { + cursor.write_u8(self.function_code())?; + cursor.write_u8(self.byte_count_in())?; + cursor.write_u8(self.byte_count_out())?; + + 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 fc = match cursor.read_u8() { + Ok(value) => value, + Err(_) => return Ok(()), + }; + let byte_count_in = match cursor.read_u8() { + Ok(value) => value, + Err(_) => return Ok(()), + }; + let byte_count_out = match cursor.read_u8() { + Ok(value) => value, + Err(_) => return Ok(()), + }; + let len = byte_count_in as usize; + + if len != cursor.remaining() / 2 { + return Ok(()); + } + + let mut data = Vec::with_capacity(len); + for _ in 0..len { + let item = match cursor.read_u16_be() { + Ok(value) => value, + Err(_) => return Ok(()), + }; + data.push(item); + } + + let custom_fc = CustomFunctionCode::new(fc, byte_count_in, byte_count_out, data); + + write!(f, "{:?}", custom_fc)?; + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -302,4 +362,46 @@ mod tests { range.serialize(&mut cursor).unwrap(); assert_eq!(buffer, [0x00, 0x03, 0x02, 0x00]); } + + #[test] + fn serialize_succeeds_for_valid_cfc_of_single_min_value() { + let custom_fc = CustomFunctionCode::new(69, 1, 1, vec![0x0000]); + let mut buffer = [0u8; 5]; + let mut cursor = WriteCursor::new(&mut buffer); + custom_fc.serialize(&mut cursor).unwrap(); + assert_eq!(buffer, [0x45, 0x01, 0x01, 0x00, 0x00]); + } + + #[test] + fn serialize_succeeds_for_valid_cfc_of_single_max_value() { + let custom_fc = CustomFunctionCode::new(69, 1, 1, vec![0xFFFF]); + let mut buffer = [0u8; 5]; + let mut cursor = WriteCursor::new(&mut buffer); + custom_fc.serialize(&mut cursor).unwrap(); + assert_eq!(buffer, [0x45, 0x01, 0x01, 0xFF, 0xFF]); + } + + #[test] + fn serialize_succeeds_for_valid_cfc_of_multiple_min_values() { + let custom_fc = CustomFunctionCode::new(69, 3, 3, vec![0x0000, 0x0000, 0x0000]); + let mut buffer = [0u8; 9]; + let mut cursor = WriteCursor::new(&mut buffer); + custom_fc.serialize(&mut cursor).unwrap(); + assert_eq!( + buffer, + [0x45, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); + } + + #[test] + fn serialize_succeeds_for_valid_cfc_of_multiple_max_values() { + let custom_fc = CustomFunctionCode::new(69, 3, 3, vec![0xFFFF, 0xFFFF, 0xFFFF]); + let mut buffer = [0u8; 9]; + let mut cursor = WriteCursor::new(&mut buffer); + custom_fc.serialize(&mut cursor).unwrap(); + assert_eq!( + buffer, + [0x45, 0x03, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] + ); + } } diff --git a/rodbus/src/serial/frame.rs b/rodbus/src/serial/frame.rs index 642f7a8a..663d42e8 100644 --- a/rodbus/src/serial/frame.rs +++ b/rodbus/src/serial/frame.rs @@ -87,6 +87,25 @@ impl RtuParser { FunctionCode::WriteSingleRegister => LengthMode::Fixed(4), FunctionCode::WriteMultipleCoils => LengthMode::Offset(5), FunctionCode::WriteMultipleRegisters => LengthMode::Offset(5), + FunctionCode::SendCFC65 => LengthMode::Offset(1), + FunctionCode::SendCFC66 => LengthMode::Offset(1), + FunctionCode::SendCFC67 => LengthMode::Offset(1), + FunctionCode::SendCFC68 => LengthMode::Offset(1), + FunctionCode::SendCFC69 => LengthMode::Offset(1), + FunctionCode::SendCFC70 => LengthMode::Offset(1), + FunctionCode::SendCFC71 => LengthMode::Offset(1), + FunctionCode::SendCFC72 => LengthMode::Offset(1), + FunctionCode::SendCFC100 => LengthMode::Offset(1), + FunctionCode::SendCFC101 => LengthMode::Offset(1), + FunctionCode::SendCFC102 => LengthMode::Offset(1), + FunctionCode::SendCFC103 => LengthMode::Offset(1), + FunctionCode::SendCFC104 => LengthMode::Offset(1), + FunctionCode::SendCFC105 => LengthMode::Offset(1), + FunctionCode::SendCFC106 => LengthMode::Offset(1), + FunctionCode::SendCFC107 => LengthMode::Offset(1), + FunctionCode::SendCFC108 => LengthMode::Offset(1), + FunctionCode::SendCFC109 => LengthMode::Offset(1), + FunctionCode::SendCFC110 => LengthMode::Offset(1), }, ParserType::Response => match function_code { FunctionCode::ReadCoils => LengthMode::Offset(1), @@ -97,6 +116,25 @@ impl RtuParser { FunctionCode::WriteSingleRegister => LengthMode::Fixed(4), FunctionCode::WriteMultipleCoils => LengthMode::Fixed(4), FunctionCode::WriteMultipleRegisters => LengthMode::Fixed(4), + FunctionCode::SendCFC65 => LengthMode::Offset(1), + FunctionCode::SendCFC66 => LengthMode::Offset(1), + FunctionCode::SendCFC67 => LengthMode::Offset(1), + FunctionCode::SendCFC68 => LengthMode::Offset(1), + FunctionCode::SendCFC69 => LengthMode::Offset(1), + FunctionCode::SendCFC70 => LengthMode::Offset(1), + FunctionCode::SendCFC71 => LengthMode::Offset(1), + FunctionCode::SendCFC72 => LengthMode::Offset(1), + FunctionCode::SendCFC100 => LengthMode::Offset(1), + FunctionCode::SendCFC101 => LengthMode::Offset(1), + FunctionCode::SendCFC102 => LengthMode::Offset(1), + FunctionCode::SendCFC103 => LengthMode::Offset(1), + FunctionCode::SendCFC104 => LengthMode::Offset(1), + FunctionCode::SendCFC105 => LengthMode::Offset(1), + FunctionCode::SendCFC106 => LengthMode::Offset(1), + FunctionCode::SendCFC107 => LengthMode::Offset(1), + FunctionCode::SendCFC108 => LengthMode::Offset(1), + FunctionCode::SendCFC109 => LengthMode::Offset(1), + FunctionCode::SendCFC110 => LengthMode::Offset(1), }, } } diff --git a/rodbus/src/server/handler.rs b/rodbus/src/server/handler.rs index 43658927..05099d1d 100644 --- a/rodbus/src/server/handler.rs +++ b/rodbus/src/server/handler.rs @@ -62,6 +62,14 @@ pub trait RequestHandler: Send + 'static { fn write_multiple_registers(&mut self, _values: WriteRegisters) -> Result<(), ExceptionCode> { Err(ExceptionCode::IllegalFunction) } + + /// Write the CFC custom function code + fn process_cfc( + &mut self, + _values: CustomFunctionCode, + ) -> Result, ExceptionCode> { + Err(ExceptionCode::IllegalFunction) + } } /// Trait useful for converting None into IllegalDataAddress @@ -226,6 +234,16 @@ pub trait AuthorizationHandler: Send + Sync + 'static { ) -> Authorization { Authorization::Deny } + + /// Authorize a Send CFC request + fn process_cfc( + &self, + _unit_id: UnitId, + _value: CustomFunctionCode, + _role: &str, + ) -> Authorization { + Authorization::Deny + } } /// Read-only authorization handler that blindly accepts @@ -304,6 +322,16 @@ impl AuthorizationHandler for ReadOnlyAuthorizationHandler { ) -> Authorization { Authorization::Deny } + + /// Authorize a Send CFC request + fn process_cfc( + &self, + _unit_id: UnitId, + _value: CustomFunctionCode, + _role: &str, + ) -> Authorization { + Authorization::Deny + } } #[cfg(test)] @@ -337,6 +365,177 @@ mod tests { handler.write_single_register(Indexed::new(0, 0)), Err(ExceptionCode::IllegalFunction) ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x41, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x42, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x43, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x44, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x45, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x46, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x47, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x48, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x64, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x65, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x66, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x67, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x68, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x69, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x6A, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x6B, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x6C, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x6D, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); + assert_eq!( + handler.process_cfc(CustomFunctionCode::new( + 0x6E, + 4, + 4, + vec![0x01, 0x02, 0x03, 0x04] + )), + Err(ExceptionCode::IllegalFunction) + ); } #[test] diff --git a/rodbus/src/server/request.rs b/rodbus/src/server/request.rs index 77843d67..ea3d580c 100644 --- a/rodbus/src/server/request.rs +++ b/rodbus/src/server/request.rs @@ -21,6 +21,7 @@ pub(crate) enum Request<'a> { WriteSingleRegister(Indexed), WriteMultipleCoils(WriteCoils<'a>), WriteMultipleRegisters(WriteRegisters<'a>), + SendCustomFunctionCode(CustomFunctionCode), } /// All requests that support broadcast @@ -54,16 +55,38 @@ impl<'a> BroadcastRequest<'a> { } impl<'a> Request<'a> { - pub(crate) fn get_function(&self) -> FunctionCode { + pub(crate) fn get_function(&self) -> Result { match self { - Request::ReadCoils(_) => FunctionCode::ReadCoils, - Request::ReadDiscreteInputs(_) => FunctionCode::ReadDiscreteInputs, - Request::ReadHoldingRegisters(_) => FunctionCode::ReadHoldingRegisters, - Request::ReadInputRegisters(_) => FunctionCode::ReadInputRegisters, - Request::WriteSingleCoil(_) => FunctionCode::WriteSingleCoil, - Request::WriteSingleRegister(_) => FunctionCode::WriteSingleRegister, - Request::WriteMultipleCoils(_) => FunctionCode::WriteMultipleCoils, - Request::WriteMultipleRegisters(_) => FunctionCode::WriteMultipleRegisters, + Request::ReadCoils(_) => Ok(FunctionCode::ReadCoils), + Request::ReadDiscreteInputs(_) => Ok(FunctionCode::ReadDiscreteInputs), + Request::ReadHoldingRegisters(_) => Ok(FunctionCode::ReadHoldingRegisters), + Request::ReadInputRegisters(_) => Ok(FunctionCode::ReadInputRegisters), + Request::WriteSingleCoil(_) => Ok(FunctionCode::WriteSingleCoil), + Request::WriteSingleRegister(_) => Ok(FunctionCode::WriteSingleRegister), + Request::WriteMultipleCoils(_) => Ok(FunctionCode::WriteMultipleCoils), + Request::WriteMultipleRegisters(_) => Ok(FunctionCode::WriteMultipleRegisters), + Request::SendCustomFunctionCode(x) => match x.function_code() { + 0x41 => Ok(FunctionCode::SendCFC65), + 0x42 => Ok(FunctionCode::SendCFC66), + 0x43 => Ok(FunctionCode::SendCFC67), + 0x44 => Ok(FunctionCode::SendCFC68), + 0x45 => Ok(FunctionCode::SendCFC69), + 0x46 => Ok(FunctionCode::SendCFC70), + 0x47 => Ok(FunctionCode::SendCFC71), + 0x48 => Ok(FunctionCode::SendCFC72), + 0x64 => Ok(FunctionCode::SendCFC100), + 0x65 => Ok(FunctionCode::SendCFC101), + 0x66 => Ok(FunctionCode::SendCFC102), + 0x67 => Ok(FunctionCode::SendCFC103), + 0x68 => Ok(FunctionCode::SendCFC104), + 0x69 => Ok(FunctionCode::SendCFC105), + 0x6A => Ok(FunctionCode::SendCFC106), + 0x6B => Ok(FunctionCode::SendCFC107), + 0x6C => Ok(FunctionCode::SendCFC108), + 0x6D => Ok(FunctionCode::SendCFC109), + 0x6E => Ok(FunctionCode::SendCFC110), + _ => Err(ExceptionCode::IllegalFunction), + }, } } @@ -77,6 +100,7 @@ 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::SendCustomFunctionCode(_) => None, } } @@ -109,37 +133,62 @@ impl<'a> Request<'a> { match self { Request::ReadCoils(range) => { let bits = BitWriter::new(*range, |i| handler.read_coil(i)); - writer.format_reply(header, function, &bits, level) + writer.format_reply(header, function.unwrap(), &bits, level) } Request::ReadDiscreteInputs(range) => { let bits = BitWriter::new(*range, |i| handler.read_discrete_input(i)); - writer.format_reply(header, function, &bits, level) + writer.format_reply(header, function.unwrap(), &bits, level) } Request::ReadHoldingRegisters(range) => { let registers = RegisterWriter::new(*range, |i| handler.read_holding_register(i)); - writer.format_reply(header, function, ®isters, level) + writer.format_reply(header, function.unwrap(), ®isters, level) } Request::ReadInputRegisters(range) => { let registers = RegisterWriter::new(*range, |i| handler.read_input_register(i)); - writer.format_reply(header, function, ®isters, level) + writer.format_reply(header, function.unwrap(), ®isters, level) } Request::WriteSingleCoil(request) => { let result = handler.write_single_coil(*request).map(|_| *request); - write_result(function, header, writer, result, level) + write_result(function.unwrap(), header, writer, result, level) } Request::WriteSingleRegister(request) => { let result = handler.write_single_register(*request).map(|_| *request); - write_result(function, header, writer, result, level) + write_result(function.unwrap(), header, writer, result, level) } Request::WriteMultipleCoils(items) => { let result = handler.write_multiple_coils(*items).map(|_| items.range); - write_result(function, header, writer, result, level) + write_result(function.unwrap(), header, writer, result, level) } Request::WriteMultipleRegisters(items) => { let result = handler .write_multiple_registers(*items) .map(|_| items.range); - write_result(function, header, writer, result, level) + write_result(function.unwrap(), header, writer, result, level) + } + Request::SendCustomFunctionCode(request) => { + let result = match function.unwrap() { + FunctionCode::SendCFC65 + | FunctionCode::SendCFC66 + | FunctionCode::SendCFC67 + | FunctionCode::SendCFC68 + | FunctionCode::SendCFC69 + | FunctionCode::SendCFC70 + | FunctionCode::SendCFC71 + | FunctionCode::SendCFC72 + | FunctionCode::SendCFC100 + | FunctionCode::SendCFC101 + | FunctionCode::SendCFC102 + | FunctionCode::SendCFC103 + | FunctionCode::SendCFC104 + | FunctionCode::SendCFC105 + | FunctionCode::SendCFC106 + | FunctionCode::SendCFC107 + | FunctionCode::SendCFC108 + | FunctionCode::SendCFC109 + | FunctionCode::SendCFC110 => handler.process_cfc(request.clone()), + _ => Err(ExceptionCode::IllegalFunction), + }; + write_result(function.unwrap(), header, writer, result, level) } } } @@ -200,6 +249,29 @@ impl<'a> Request<'a> { RegisterIterator::parse_all(range, cursor)?, ))) } + FunctionCode::SendCFC65 + | FunctionCode::SendCFC66 + | FunctionCode::SendCFC67 + | FunctionCode::SendCFC68 + | FunctionCode::SendCFC69 + | FunctionCode::SendCFC70 + | FunctionCode::SendCFC71 + | FunctionCode::SendCFC72 + | FunctionCode::SendCFC100 + | FunctionCode::SendCFC101 + | FunctionCode::SendCFC102 + | FunctionCode::SendCFC103 + | FunctionCode::SendCFC104 + | FunctionCode::SendCFC105 + | FunctionCode::SendCFC106 + | FunctionCode::SendCFC107 + | FunctionCode::SendCFC108 + | FunctionCode::SendCFC109 + | FunctionCode::SendCFC110 => { + let x = Request::SendCustomFunctionCode(CustomFunctionCode::parse(cursor)?); + cursor.expect_empty()?; + Ok(x) + } } } } @@ -217,7 +289,7 @@ impl<'a, 'b> RequestDisplay<'a, 'b> { impl std::fmt::Display for RequestDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.request.get_function())?; + write!(f, "{}", self.request.get_function().unwrap())?; if self.level.data_headers() { match self.request { @@ -253,6 +325,9 @@ impl std::fmt::Display for RequestDisplay<'_, '_> { RegisterIteratorDisplay::new(self.level, items.iterator) )?; } + Request::SendCustomFunctionCode(request) => { + write!(f, " {request}")?; + } } } diff --git a/rodbus/src/server/task.rs b/rodbus/src/server/task.rs index fbb7bfca..2c0c3288 100644 --- a/rodbus/src/server/task.rs +++ b/rodbus/src/server/task.rs @@ -190,7 +190,7 @@ where self.reply_with_error( io, frame.header, - request.get_function(), + request.get_function().unwrap(), ExceptionCode::IllegalFunction, ) .await?; @@ -264,6 +264,10 @@ impl AuthorizationType { Request::WriteMultipleRegisters(x) => { handler.write_multiple_registers(unit_id, x.range, role) } + Request::SendCustomFunctionCode(x) => match x.function_code() { + 0x41..=0x48 | 0x64..=0x6E => handler.process_cfc(unit_id, x.clone(), role), + _ => Authorization::Deny, + }, } } diff --git a/rodbus/src/types.rs b/rodbus/src/types.rs index 0f9d94eb..2de50a0f 100644 --- a/rodbus/src/types.rs +++ b/rodbus/src/types.rs @@ -85,6 +85,15 @@ pub(crate) struct RegisterIteratorDisplay<'a> { level: AppDecodeLevel, } +/// Custom Function Code +#[derive(Clone, Debug, PartialEq)] +pub struct CustomFunctionCode { + fc: u8, + byte_count_in: u8, + byte_count_out: u8, + data: Vec, +} + impl std::fmt::Display for UnitId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:#04X}", self.value) @@ -367,6 +376,72 @@ impl Default for UnitId { } } +impl CustomFunctionCode { + /// Create a new custom function code + pub fn new(fc: u8, byte_count_in: u8, byte_count_out: u8, data: Vec) -> Self { + Self { + fc, + byte_count_in, + byte_count_out, + data, + } + } + + /// Get the function code + pub fn function_code(&self) -> u8 { + self.fc + } + + /// Get the function code + pub fn byte_count_in(&self) -> u8 { + self.byte_count_in + } + + /// Get the function code + pub fn byte_count_out(&self) -> u8 { + self.byte_count_out + } + + /// Get the length of the underlying vector + pub fn len(&self) -> usize { + self.data.len() + } + + /// Check if the underlying vector is empty + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// 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, "fc: {:#X}, ", self.fc)?; + write!(f, "bytes in: {}, ", self.byte_count_in)?; + write!(f, "bytes out: {}, ", self.byte_count_out)?; + write!(f, "values: [")?; + for (i, val) in self.data.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{}", val)?; + } + write!(f, "], ")?; + write!(f, "hex: [")?; + for (i, val) in self.data.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{:#X}", 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..011f4b19 100644 --- a/rodbus/tests/integration_test.rs +++ b/rodbus/tests/integration_test.rs @@ -94,6 +94,67 @@ impl RequestHandler for Handler { } Ok(()) } + + fn process_cfc( + &mut self, + values: CustomFunctionCode, + ) -> Result, ExceptionCode> { + tracing::info!( + "processing custom function code: {}, data: {:?}", + values.function_code(), + values.iter() + ); + match values.function_code() { + 0x41 => { + // increment each CFC value by 1 and return the result + // Create a new vector to hold the incremented values + let incremented_data = values.iter().map(|&val| val + 1).collect(); + + // Return a new CustomFunctionCode with the incremented data + Ok(CustomFunctionCode::new( + values.function_code(), + values.byte_count_in(), + values.byte_count_out(), + incremented_data, + )) + } + 0x42 => { + // add a new value to the buffer and return the result + // Create a new vector to hold the incremented values + let extended_data = { + let mut extended_data = values.iter().map(|val| *val).collect::>(); + extended_data.push(0xC0DE); + extended_data + }; + + // Return a new CustomFunctionCode with the incremented data + Ok(CustomFunctionCode::new( + values.function_code(), + values.byte_count_in(), + values.byte_count_out(), + extended_data, + )) + } + 0x43 => { + // remove the first value from the buffer and return the result + // Create a new vector to hold the incremented values + let truncated_data = { + let mut truncated_data = values.iter().map(|val| *val).collect::>(); + truncated_data.pop(); + truncated_data + }; + + // Return a new CustomFunctionCode with the incremented data + Ok(CustomFunctionCode::new( + values.function_code(), + values.byte_count_in(), + values.byte_count_out(), + truncated_data, + )) + } + _ => Err(ExceptionCode::IllegalFunction), + } + } } async fn test_requests_and_responses() { @@ -222,6 +283,109 @@ async fn test_requests_and_responses() { Indexed::new(2, 0x0506) ] ); + // Test the invalid CFC handlers below 65 + for i in 0..65 { + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(i, 4, 4, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Err(rodbus::ExceptionCode::IllegalFunction.into()) + ); + } + // Test the implemented valid test handlers 65, 66, 67 + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(0x41, 4, 4, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Ok(CustomFunctionCode::new( + 0x41, + 4, + 4, + vec![0xC0DF, 0xCAFF, 0xC0DF, 0xCAFF] + )) + ); + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(0x42, 4, 5, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Ok(CustomFunctionCode::new( + 0x42, + 4, + 5, + vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE, 0xC0DE] + )) + ); + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(0x43, 4, 3, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Ok(CustomFunctionCode::new( + 0x43, + 4, + 3, + vec![0xC0DE, 0xCAFE, 0xC0DE] + )) + ); + // Test the unimplemented valid handlers from 68 to 72 + for i in 68..73 { + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(i, 4, 4, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Err(rodbus::ExceptionCode::IllegalFunction.into()) + ); + } + // Test the invalid handlers from 73 to 99 + for i in 73..100 { + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(i, 4, 4, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Err(rodbus::ExceptionCode::IllegalFunction.into()) + ); + } + // Test the unimplemented valid handlers from 100 to 110 + for i in 100..110 { + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(i, 4, 4, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Err(rodbus::ExceptionCode::IllegalFunction.into()) + ); + } + // Test the invalid CFC handlers from 111 to 255 + for i in 111..=255 { + assert_eq!( + channel + .send_custom_function_code( + params, + CustomFunctionCode::new(i, 4, 4, vec![0xC0DE, 0xCAFE, 0xC0DE, 0xCAFE]) + ) + .await, + Err(rodbus::ExceptionCode::IllegalFunction.into()) + ); + } } #[test]