diff --git a/CHANGELOG.md b/CHANGELOG.md index b53a7829..7552837b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ # Changelog +## v0.10.0 (Unreleased) + +- Exclude `rtu-server`/`tcp-server` from default features. +- Feature: Retrieve the `FunctionCode` of a `Request`/`Response`. + +### Breaking Changes + +- `FunctionCode`: Replace `type` alias with `enum`. + ## v0.9.0 (2023-07-26) - Optimization: Avoid allocations when writing multiple coils/registers. diff --git a/Cargo.toml b/Cargo.toml index 4482e1db..501d8c23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ pem = "3.0.3" webpki = "0.22.4" [features] -default = ["rtu", "tcp", "rtu-server", "tcp-server"] +default = ["rtu", "tcp"] rtu = ["futures-util/sink"] tcp = ["tokio/net", "futures-util/sink"] rtu-sync = ["rtu", "sync", "dep:tokio-serial"] diff --git a/src/codec/mod.rs b/src/codec/mod.rs index 98b98246..057927ea 100644 --- a/src/codec/mod.rs +++ b/src/codec/mod.rs @@ -45,7 +45,7 @@ impl<'a> TryFrom> for Bytes { use crate::frame::Request::*; let cnt = request_byte_count(&req); let mut data = BytesMut::with_capacity(cnt); - data.put_u8(req_to_fn_code(&req)); + data.put_u8(req.function_code().value()); match req { ReadCoils(address, quantity) | ReadDiscreteInputs(address, quantity) @@ -121,7 +121,7 @@ impl From for Bytes { use crate::frame::Response::*; let cnt = response_byte_count(&rsp); let mut data = BytesMut::with_capacity(cnt); - data.put_u8(rsp_to_fn_code(&rsp)); + data.put_u8(rsp.function_code().value()); match rsp { ReadCoils(coils) | ReadDiscreteInputs(coils) => { let packed_coils = pack_coils(&coils); @@ -168,8 +168,8 @@ impl From for Bytes { impl From for Bytes { fn from(ex: ExceptionResponse) -> Bytes { let mut data = BytesMut::with_capacity(2); - debug_assert!(ex.function < 0x80); - data.put_u8(ex.function + 0x80); + debug_assert!(ex.function.value() < 0x80); + data.put_u8(ex.function.value() + 0x80); data.put_u8(ex.exception.into()); data.freeze() } @@ -357,7 +357,7 @@ impl TryFrom for ExceptionResponse { let function = fn_err_code - 0x80; let exception = Exception::try_from(rdr.read_u8()?)?; Ok(ExceptionResponse { - function, + function: FunctionCode::new(function), exception, }) } @@ -438,41 +438,6 @@ fn unpack_coils(bytes: &[u8], count: u16) -> Vec { res } -fn req_to_fn_code(req: &Request<'_>) -> u8 { - use crate::frame::Request::*; - match *req { - ReadCoils(_, _) => 0x01, - ReadDiscreteInputs(_, _) => 0x02, - WriteSingleCoil(_, _) => 0x05, - WriteMultipleCoils(_, _) => 0x0F, - ReadInputRegisters(_, _) => 0x04, - ReadHoldingRegisters(_, _) => 0x03, - WriteSingleRegister(_, _) => 0x06, - WriteMultipleRegisters(_, _) => 0x10, - MaskWriteRegister(_, _, _) => 0x16, - ReadWriteMultipleRegisters(_, _, _, _) => 0x17, - Custom(code, _) => code, - Disconnect => unreachable!(), - } -} - -fn rsp_to_fn_code(rsp: &Response) -> u8 { - use crate::frame::Response::*; - match *rsp { - ReadCoils(_) => 0x01, - ReadDiscreteInputs(_) => 0x02, - WriteSingleCoil(_, _) => 0x05, - WriteMultipleCoils(_, _) => 0x0F, - ReadInputRegisters(_) => 0x04, - ReadHoldingRegisters(_) => 0x03, - WriteSingleRegister(_, _) => 0x06, - WriteMultipleRegisters(_, _) => 0x10, - MaskWriteRegister(_, _, _) => 0x16, - ReadWriteMultipleRegisters(_) => 0x17, - Custom(code, _) => code, - } -} - fn request_byte_count(req: &Request<'_>) -> usize { use crate::frame::Request::*; match *req { @@ -551,51 +516,10 @@ mod tests { assert_eq!(unpack_coils(&[0xff, 0b11], 10), &[true; 10]); } - #[test] - fn function_code_from_request() { - use crate::frame::Request::*; - assert_eq!(req_to_fn_code(&ReadCoils(0, 0)), 1); - assert_eq!(req_to_fn_code(&ReadDiscreteInputs(0, 0)), 2); - assert_eq!(req_to_fn_code(&WriteSingleCoil(0, true)), 5); - assert_eq!( - req_to_fn_code(&WriteMultipleCoils(0, Cow::Borrowed(&[]))), - 0x0F - ); - assert_eq!(req_to_fn_code(&ReadInputRegisters(0, 0)), 0x04); - assert_eq!(req_to_fn_code(&ReadHoldingRegisters(0, 0)), 0x03); - assert_eq!(req_to_fn_code(&WriteSingleRegister(0, 0)), 0x06); - assert_eq!( - req_to_fn_code(&WriteMultipleRegisters(0, Cow::Borrowed(&[]))), - 0x10 - ); - assert_eq!(req_to_fn_code(&MaskWriteRegister(0, 0, 0)), 0x16); - assert_eq!( - req_to_fn_code(&ReadWriteMultipleRegisters(0, 0, 0, Cow::Borrowed(&[]))), - 0x17 - ); - assert_eq!(req_to_fn_code(&Custom(88, Cow::Borrowed(&[]))), 88); - } - - #[test] - fn function_code_from_response() { - use crate::frame::Response::*; - assert_eq!(rsp_to_fn_code(&ReadCoils(vec![])), 1); - assert_eq!(rsp_to_fn_code(&ReadDiscreteInputs(vec![])), 2); - assert_eq!(rsp_to_fn_code(&WriteSingleCoil(0x0, false)), 5); - assert_eq!(rsp_to_fn_code(&WriteMultipleCoils(0x0, 0x0)), 0x0F); - assert_eq!(rsp_to_fn_code(&ReadInputRegisters(vec![])), 0x04); - assert_eq!(rsp_to_fn_code(&ReadHoldingRegisters(vec![])), 0x03); - assert_eq!(rsp_to_fn_code(&WriteSingleRegister(0, 0)), 0x06); - assert_eq!(rsp_to_fn_code(&WriteMultipleRegisters(0, 0)), 0x10); - assert_eq!(rsp_to_fn_code(&MaskWriteRegister(0, 0, 0)), 0x16); - assert_eq!(rsp_to_fn_code(&ReadWriteMultipleRegisters(vec![])), 0x17); - assert_eq!(rsp_to_fn_code(&Custom(99, Bytes::from_static(&[]))), 99); - } - #[test] fn exception_response_into_bytes() { let bytes: Bytes = ExceptionResponse { - function: 0x03, + function: FunctionCode::ReadHoldingRegisters, exception: Exception::IllegalDataAddress, } .into(); @@ -612,7 +536,7 @@ mod tests { assert_eq!( rsp, ExceptionResponse { - function: 0x03, + function: FunctionCode::ReadHoldingRegisters, exception: Exception::IllegalDataAddress, } ); @@ -623,7 +547,7 @@ mod tests { let req_pdu: Bytes = Request::ReadCoils(0x01, 5).try_into().unwrap(); let rsp_pdu: Bytes = Response::ReadCoils(vec![]).into(); let ex_pdu: Bytes = ExceptionResponse { - function: 0x03, + function: FunctionCode::ReadHoldingRegisters, exception: Exception::ServerDeviceFailure, } .into(); diff --git a/src/frame/mod.rs b/src/frame/mod.rs index 317c1597..9e7b4e85 100644 --- a/src/frame/mod.rs +++ b/src/frame/mod.rs @@ -7,12 +7,90 @@ pub(crate) mod rtu; #[cfg(feature = "tcp")] pub(crate) mod tcp; -use std::{borrow::Cow, error, fmt}; +use std::{ + borrow::Cow, + error, + fmt::{self, Display}, +}; use crate::bytes::Bytes; -/// A Modbus function code is represented by an unsigned 8 bit integer. -pub type FunctionCode = u8; +/// A Modbus function code. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FunctionCode { + /// Modbus Function Code: `01` (`0x01`). + ReadCoils, + /// Modbus Function Code: `02` (`0x02`). + ReadDiscreteInputs, + + /// Modbus Function Code: `05` (`0x05`). + WriteSingleCoil, + /// Modbus Function Code: `06` (`0x06`). + WriteSingleRegister, + + /// Modbus Function Code: `03` (`0x03`). + ReadHoldingRegisters, + /// Modbus Function Code: `04` (`0x04`). + ReadInputRegisters, + + /// Modbus Function Code: `15` (`0x0F`). + WriteMultipleCoils, + /// Modbus Function Code: `16` (`0x10`). + WriteMultipleRegisters, + + /// Modbus Function Code: `22` (`0x16`). + MaskWriteRegister, + + /// Modbus Function Code: `23` (`0x17`). + ReadWriteMultipleRegisters, + + /// Custom Modbus Function Code. + Custom(u8), +} + +impl FunctionCode { + /// Create a new [`FunctionCode`] with `value`. + #[must_use] + pub const fn new(value: u8) -> Self { + match value { + 0x01 => FunctionCode::ReadCoils, + 0x02 => FunctionCode::ReadDiscreteInputs, + 0x05 => FunctionCode::WriteSingleCoil, + 0x06 => FunctionCode::WriteSingleRegister, + 0x03 => FunctionCode::ReadHoldingRegisters, + 0x04 => FunctionCode::ReadInputRegisters, + 0x0F => FunctionCode::WriteMultipleCoils, + 0x10 => FunctionCode::WriteMultipleRegisters, + 0x16 => FunctionCode::MaskWriteRegister, + 0x17 => FunctionCode::ReadWriteMultipleRegisters, + code => FunctionCode::Custom(code), + } + } + + /// Get the [`u8`] value of the current [`FunctionCode`]. + #[must_use] + pub const fn value(self) -> u8 { + match self { + FunctionCode::ReadCoils => 0x01, + FunctionCode::ReadDiscreteInputs => 0x02, + FunctionCode::WriteSingleCoil => 0x05, + FunctionCode::WriteSingleRegister => 0x06, + FunctionCode::ReadHoldingRegisters => 0x03, + FunctionCode::ReadInputRegisters => 0x04, + FunctionCode::WriteMultipleCoils => 0x0F, + FunctionCode::WriteMultipleRegisters => 0x10, + FunctionCode::MaskWriteRegister => 0x16, + FunctionCode::ReadWriteMultipleRegisters => 0x17, + FunctionCode::Custom(code) => code, + } + } +} + +impl Display for FunctionCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.value().fmt(f) + } +} /// A Modbus protocol address is represented by 16 bit from `0` to `65535`. /// @@ -95,7 +173,7 @@ pub enum Request<'a> { /// A raw Modbus request. /// The first parameter is the Modbus function code. /// The second parameter is the raw bytes of the request. - Custom(FunctionCode, Cow<'a, [u8]>), + Custom(u8, Cow<'a, [u8]>), /// A poison pill for stopping the client service and to release /// the underlying transport, e.g. for disconnecting from an @@ -138,6 +216,34 @@ impl<'a> Request<'a> { Disconnect => Disconnect, } } + + /// Get the [`FunctionCode`] of the [`Request`]. + #[must_use] + pub const fn function_code(&self) -> FunctionCode { + use Request::*; + + match self { + ReadCoils(_, _) => FunctionCode::ReadCoils, + ReadDiscreteInputs(_, _) => FunctionCode::ReadDiscreteInputs, + + WriteSingleCoil(_, _) => FunctionCode::WriteSingleCoil, + WriteMultipleCoils(_, _) => FunctionCode::WriteMultipleCoils, + + ReadInputRegisters(_, _) => FunctionCode::ReadInputRegisters, + ReadHoldingRegisters(_, _) => FunctionCode::ReadHoldingRegisters, + + WriteSingleRegister(_, _) => FunctionCode::WriteSingleRegister, + WriteMultipleRegisters(_, _) => FunctionCode::WriteMultipleRegisters, + + MaskWriteRegister(_, _, _) => FunctionCode::MaskWriteRegister, + + ReadWriteMultipleRegisters(_, _, _, _) => FunctionCode::ReadWriteMultipleRegisters, + + Custom(code, _) => FunctionCode::Custom(*code), + + Disconnect => unreachable!(), + } + } } /// A Modbus request with slave included @@ -222,7 +328,35 @@ pub enum Response { /// Response to a raw Modbus request /// The first parameter contains the returned Modbus function code /// The second parameter contains the bytes read following the function code - Custom(FunctionCode, Bytes), + Custom(u8, Bytes), +} + +impl Response { + /// Get the [`FunctionCode`] of the [`Response`]. + #[must_use] + pub const fn function_code(&self) -> FunctionCode { + use Response::*; + + match self { + ReadCoils(_) => FunctionCode::ReadCoils, + ReadDiscreteInputs(_) => FunctionCode::ReadDiscreteInputs, + + WriteSingleCoil(_, _) => FunctionCode::WriteSingleCoil, + WriteMultipleCoils(_, _) => FunctionCode::WriteMultipleCoils, + + ReadInputRegisters(_) => FunctionCode::ReadInputRegisters, + ReadHoldingRegisters(_) => FunctionCode::ReadHoldingRegisters, + + WriteSingleRegister(_, _) => FunctionCode::WriteSingleRegister, + WriteMultipleRegisters(_, _) => FunctionCode::WriteMultipleRegisters, + + MaskWriteRegister(_, _, _) => FunctionCode::MaskWriteRegister, + + ReadWriteMultipleRegisters(_) => FunctionCode::ReadWriteMultipleRegisters, + + Custom(code, _) => FunctionCode::Custom(*code), + } + } } /// A server (slave) exception. @@ -362,3 +496,159 @@ impl error::Error for ExceptionResponse { self.exception.description() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_function_code() { + assert_eq!(FunctionCode::ReadCoils, FunctionCode::new(0x01)); + assert_eq!(FunctionCode::ReadDiscreteInputs, FunctionCode::new(0x02)); + + assert_eq!(FunctionCode::WriteSingleCoil, FunctionCode::new(0x05)); + assert_eq!(FunctionCode::WriteSingleRegister, FunctionCode::new(0x06)); + + assert_eq!(FunctionCode::ReadHoldingRegisters, FunctionCode::new(0x03)); + assert_eq!(FunctionCode::ReadInputRegisters, FunctionCode::new(0x04)); + + assert_eq!(FunctionCode::WriteMultipleCoils, FunctionCode::new(0x0F)); + assert_eq!( + FunctionCode::WriteMultipleRegisters, + FunctionCode::new(0x10) + ); + + assert_eq!(FunctionCode::MaskWriteRegister, FunctionCode::new(0x016)); + + assert_eq!( + FunctionCode::ReadWriteMultipleRegisters, + FunctionCode::new(0x017) + ); + + assert_eq!(FunctionCode::Custom(70), FunctionCode::new(70)); + } + + #[test] + fn function_code_values() { + assert_eq!(FunctionCode::ReadCoils.value(), 0x01); + assert_eq!(FunctionCode::ReadDiscreteInputs.value(), 0x02); + + assert_eq!(FunctionCode::WriteSingleCoil.value(), 0x05); + assert_eq!(FunctionCode::WriteSingleRegister.value(), 0x06); + + assert_eq!(FunctionCode::ReadHoldingRegisters.value(), 0x03); + assert_eq!(FunctionCode::ReadInputRegisters.value(), 0x04); + + assert_eq!(FunctionCode::WriteMultipleCoils.value(), 0x0F); + assert_eq!(FunctionCode::WriteMultipleRegisters.value(), 0x10); + + assert_eq!(FunctionCode::MaskWriteRegister.value(), 0x016); + + assert_eq!(FunctionCode::ReadWriteMultipleRegisters.value(), 0x017); + + assert_eq!(FunctionCode::Custom(70).value(), 70); + } + + #[test] + fn function_code_from_request() { + use Request::*; + + assert_eq!(ReadCoils(0, 0).function_code(), FunctionCode::ReadCoils); + assert_eq!( + ReadDiscreteInputs(0, 0).function_code(), + FunctionCode::ReadDiscreteInputs + ); + + assert_eq!( + WriteSingleCoil(0, true).function_code(), + FunctionCode::WriteSingleCoil + ); + assert_eq!( + WriteMultipleCoils(0, Cow::Borrowed(&[])).function_code(), + FunctionCode::WriteMultipleCoils + ); + + assert_eq!( + ReadInputRegisters(0, 0).function_code(), + FunctionCode::ReadInputRegisters + ); + assert_eq!( + ReadHoldingRegisters(0, 0).function_code(), + FunctionCode::ReadHoldingRegisters + ); + + assert_eq!( + WriteSingleRegister(0, 0).function_code(), + FunctionCode::WriteSingleRegister + ); + assert_eq!( + WriteMultipleRegisters(0, Cow::Borrowed(&[])).function_code(), + FunctionCode::WriteMultipleRegisters + ); + + assert_eq!( + MaskWriteRegister(0, 0, 0).function_code(), + FunctionCode::MaskWriteRegister + ); + + assert_eq!( + ReadWriteMultipleRegisters(0, 0, 0, Cow::Borrowed(&[])).function_code(), + FunctionCode::ReadWriteMultipleRegisters + ); + + assert_eq!(Custom(88, Cow::Borrowed(&[])).function_code().value(), 88); + } + + #[test] + fn function_code_from_response() { + use Response::*; + + assert_eq!(ReadCoils(vec![]).function_code(), FunctionCode::ReadCoils); + assert_eq!( + ReadDiscreteInputs(vec![]).function_code(), + FunctionCode::ReadDiscreteInputs + ); + + assert_eq!( + WriteSingleCoil(0x0, false).function_code(), + FunctionCode::WriteSingleCoil + ); + assert_eq!( + WriteMultipleCoils(0x0, 0x0).function_code(), + FunctionCode::WriteMultipleCoils + ); + + assert_eq!( + ReadInputRegisters(vec![]).function_code(), + FunctionCode::ReadInputRegisters + ); + assert_eq!( + ReadHoldingRegisters(vec![]).function_code(), + FunctionCode::ReadHoldingRegisters + ); + + assert_eq!( + WriteSingleRegister(0, 0).function_code(), + FunctionCode::WriteSingleRegister + ); + assert_eq!( + WriteMultipleRegisters(0, 0).function_code(), + FunctionCode::WriteMultipleRegisters + ); + + assert_eq!( + MaskWriteRegister(0, 0, 0).function_code(), + FunctionCode::MaskWriteRegister + ); + + assert_eq!( + ReadWriteMultipleRegisters(vec![]).function_code(), + FunctionCode::ReadWriteMultipleRegisters + ); + + assert_eq!( + Custom(99, Bytes::from_static(&[])).function_code().value(), + 99 + ); + } +}