Skip to content

Commit

Permalink
Client API: Handle Modbus exceptions (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
creberust authored Mar 15, 2024
1 parent 51d1003 commit f8a8834
Show file tree
Hide file tree
Showing 20 changed files with 634 additions and 470 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@

# Changelog

## v0.12.0 (unreleased)

- Client: Support handling of _Modbus_ exceptions by using nested `Result`s.

### Breaking Changes

- Client: All methods in the `Client`, `Reader` and `Writer` traits now return
nested `Result` values that both need to be handled explicitly.

```diff
async fn read_coils(&mut self, _: Address, _: Quantity)
- -> Result<Vec<Coil>, std::io::Error>;
+ -> Result<Result<Vec<Coil>, Exception>, std::io::Error>;
```

The type alias `tokio_modbus::Result<T>` facilitates referencing the new return
types.

```rust
pub type Result<T> = Result<Result<T, Exception>, std::io::Error>
```

## v0.11.0 (2024-01-28)

- Server: Remove `Sync` and `Unpin` trait bounds from `Service::call()` future
Expand Down
56 changes: 22 additions & 34 deletions examples/rtu-over-tcp-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,26 @@ impl tokio_modbus::server::Service for ExampleService {
fn call(&self, req: Self::Request) -> Self::Future {
println!("{}", req.slave);
match req.request {
Request::ReadInputRegisters(addr, cnt) => {
match register_read(&self.input_registers.lock().unwrap(), addr, cnt) {
Ok(values) => future::ready(Ok(Response::ReadInputRegisters(values))),
Err(err) => future::ready(Err(err)),
}
}
Request::ReadHoldingRegisters(addr, cnt) => {
match register_read(&self.holding_registers.lock().unwrap(), addr, cnt) {
Ok(values) => future::ready(Ok(Response::ReadHoldingRegisters(values))),
Err(err) => future::ready(Err(err)),
}
}
Request::WriteMultipleRegisters(addr, values) => {
match register_write(&mut self.holding_registers.lock().unwrap(), addr, &values) {
Ok(_) => future::ready(Ok(Response::WriteMultipleRegisters(
addr,
values.len() as u16,
))),
Err(err) => future::ready(Err(err)),
}
}
Request::WriteSingleRegister(addr, value) => {
match register_write(
Request::ReadInputRegisters(addr, cnt) => future::ready(
register_read(&self.input_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadInputRegisters),
),
Request::ReadHoldingRegisters(addr, cnt) => future::ready(
register_read(&self.holding_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadHoldingRegisters),
),
Request::WriteMultipleRegisters(addr, values) => future::ready(
register_write(&mut self.holding_registers.lock().unwrap(), addr, &values)
.map(|_| Response::WriteMultipleRegisters(addr, values.len() as u16)),
),
Request::WriteSingleRegister(addr, value) => future::ready(
register_write(
&mut self.holding_registers.lock().unwrap(),
addr,
std::slice::from_ref(&value),
) {
Ok(_) => future::ready(Ok(Response::WriteSingleRegister(addr, value))),
Err(err) => future::ready(Err(err)),
}
}
)
.map(|_| Response::WriteSingleRegister(addr, value)),
),
_ => {
println!("SERVER: Exception::IllegalFunction - Unimplemented function code in request: {req:?}");
future::ready(Err(Exception::IllegalFunction))
Expand Down Expand Up @@ -170,28 +159,27 @@ async fn client_context(socket_addr: SocketAddr) {
println!("CLIENT: Reading 2 input registers...");
let response = ctx.read_input_registers(0x00, 2).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, [1234, 5678]);
assert_eq!(response, Ok(vec![1234, 5678]));

println!("CLIENT: Writing 2 holding registers...");
ctx.write_multiple_registers(0x01, &[7777, 8888])
.await
.unwrap()
.unwrap();

// Read back a block including the two registers we wrote.
println!("CLIENT: Reading 4 holding registers...");
let response = ctx.read_holding_registers(0x00, 4).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, [10, 7777, 8888, 40]);
assert_eq!(response, Ok(vec![10, 7777, 8888, 40]));

// Now we try to read with an invalid register address.
// This should return a Modbus exception response with the code
// IllegalDataAddress.
println!("CLIENT: Reading nonexisting holding register address... (should return IllegalDataAddress)");
let response = ctx.read_holding_registers(0x100, 1).await;
let response = ctx.read_holding_registers(0x100, 1).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert!(response.is_err());
// TODO: How can Modbus client identify Modbus exception responses? E.g. here we expect IllegalDataAddress
// Question here: https://github.com/slowtec/tokio-modbus/issues/169
assert_eq!(response, Err(Exception::IllegalDataAddress));

println!("CLIENT: Done.")
},
Expand Down
14 changes: 11 additions & 3 deletions examples/rtu-server-address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Give the server some time for stating up
thread::sleep(Duration::from_secs(1));

println!("Connecting client...");
println!("CLIENT: Connecting client...");
let client_serial = tokio_serial::SerialStream::open(&builder).unwrap();
let mut ctx = rtu::attach_slave(client_serial, slave);
println!("Reading input registers...");
println!("CLIENT: Reading input registers...");
let rsp = ctx.read_input_registers(0x00, 7).await?;
println!("The result is '{rsp:#x?}'"); // The result is '[0x0,0x0,0x77,0x0,0x0,0x0,0x0,]'
println!("CLIENT: The result is '{rsp:#x?}'");
assert_eq!(rsp, Ok(vec![0x0, 0x0, 0x77, 0x0, 0x0, 0x0, 0x0]));

println!("CLIENT: Reading with illegal function... (should return IllegalFunction)");
let response = ctx.read_holding_registers(0x100, 1).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, Err(Exception::IllegalFunction));

println!("CLIENT: Done.");

Ok(())
}
19 changes: 16 additions & 3 deletions examples/rtu-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ impl tokio_modbus::server::Service for Service {
registers[2] = 0x77;
future::ready(Ok(Response::ReadInputRegisters(registers)))
}
Request::ReadHoldingRegisters(_, _) => {
future::ready(Err(Exception::IllegalDataAddress))
}
_ => unimplemented!(),
}
}
Expand All @@ -45,12 +48,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Give the server some time for stating up
thread::sleep(Duration::from_secs(1));

println!("Connecting client...");
println!("CLIENT: Connecting client...");
let client_serial = tokio_serial::SerialStream::open(&builder).unwrap();
let mut ctx = rtu::attach(client_serial);
println!("Reading input registers...");
println!("CLIENT: Reading input registers...");
let rsp = ctx.read_input_registers(0x00, 7).await?;
println!("The result is '{rsp:#x?}'"); // The result is '[0x0,0x0,0x77,0x0,0x0,0x0,0x0,]'
println!("CLIENT: The result is '{rsp:#x?}'");
assert_eq!(rsp, Ok(vec![0x0, 0x0, 0x77, 0x0, 0x0, 0x0, 0x0]));

// Now we try to read with an invalid register address.
// This should return a Modbus exception response with the code
// IllegalDataAddress.
println!("CLIENT: Reading nonexisting holding register address... (should return IllegalDataAddress)");
let response = ctx.read_holding_registers(0x100, 1).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, Err(Exception::IllegalDataAddress));

println!("CLIENT: Done.");
Ok(())
}
2 changes: 1 addition & 1 deletion examples/tcp-client-custom-fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Fetching the coupler ID");
let rsp = ctx
.call(Request::Custom(0x66, Cow::Borrowed(&[0x11, 0x42])))
.await?;
.await??;

match rsp {
Response::Custom(f, rsp) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/tcp-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ctx = tcp::connect(socket_addr).await?;

println!("Fetching the coupler ID");
let data = ctx.read_input_registers(0x1000, 7).await?;
let data = ctx.read_input_registers(0x1000, 7).await??;

let bytes: Vec<u8> = data.iter().fold(vec![], |mut x, elem| {
x.push((elem & 0xff) as u8);
Expand Down
58 changes: 22 additions & 36 deletions examples/tcp-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,26 @@ impl tokio_modbus::server::Service for ExampleService {

fn call(&self, req: Self::Request) -> Self::Future {
match req {
Request::ReadInputRegisters(addr, cnt) => {
match register_read(&self.input_registers.lock().unwrap(), addr, cnt) {
Ok(values) => future::ready(Ok(Response::ReadInputRegisters(values))),
Err(err) => future::ready(Err(err)),
}
}
Request::ReadHoldingRegisters(addr, cnt) => {
match register_read(&self.holding_registers.lock().unwrap(), addr, cnt) {
Ok(values) => future::ready(Ok(Response::ReadHoldingRegisters(values))),
Err(err) => future::ready(Err(err)),
}
}
Request::WriteMultipleRegisters(addr, values) => {
match register_write(&mut self.holding_registers.lock().unwrap(), addr, &values) {
Ok(_) => future::ready(Ok(Response::WriteMultipleRegisters(
addr,
values.len() as u16,
))),
Err(err) => future::ready(Err(err)),
}
}
Request::WriteSingleRegister(addr, value) => {
match register_write(
Request::ReadInputRegisters(addr, cnt) => future::ready(
register_read(&self.input_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadInputRegisters),
),
Request::ReadHoldingRegisters(addr, cnt) => future::ready(
register_read(&self.holding_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadHoldingRegisters),
),
Request::WriteMultipleRegisters(addr, values) => future::ready(
register_write(&mut self.holding_registers.lock().unwrap(), addr, &values)
.map(|_| Response::WriteMultipleRegisters(addr, values.len() as u16)),
),
Request::WriteSingleRegister(addr, value) => future::ready(
register_write(
&mut self.holding_registers.lock().unwrap(),
addr,
std::slice::from_ref(&value),
) {
Ok(_) => future::ready(Ok(Response::WriteSingleRegister(addr, value))),
Err(err) => future::ready(Err(err)),
}
}
)
.map(|_| Response::WriteSingleRegister(addr, value)),
),
_ => {
println!("SERVER: Exception::IllegalFunction - Unimplemented function code in request: {req:?}");
future::ready(Err(Exception::IllegalFunction))
Expand Down Expand Up @@ -101,7 +90,6 @@ fn register_read(
if let Some(r) = registers.get(&reg_addr) {
response_values[i as usize] = *r;
} else {
// TODO: Return a Modbus Exception response `IllegalDataAddress` https://github.com/slowtec/tokio-modbus/issues/165
println!("SERVER: Exception::IllegalDataAddress");
return Err(Exception::IllegalDataAddress);
}
Expand All @@ -122,7 +110,6 @@ fn register_write(
if let Some(r) = registers.get_mut(&reg_addr) {
*r = *value;
} else {
// TODO: Return a Modbus Exception response `IllegalDataAddress` https://github.com/slowtec/tokio-modbus/issues/165
println!("SERVER: Exception::IllegalDataAddress");
return Err(Exception::IllegalDataAddress);
}
Expand Down Expand Up @@ -170,28 +157,27 @@ async fn client_context(socket_addr: SocketAddr) {
println!("CLIENT: Reading 2 input registers...");
let response = ctx.read_input_registers(0x00, 2).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, [1234, 5678]);
assert_eq!(response, Ok(vec![1234, 5678]));

println!("CLIENT: Writing 2 holding registers...");
ctx.write_multiple_registers(0x01, &[7777, 8888])
.await
.unwrap()
.unwrap();

// Read back a block including the two registers we wrote.
println!("CLIENT: Reading 4 holding registers...");
let response = ctx.read_holding_registers(0x00, 4).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, [10, 7777, 8888, 40]);
assert_eq!(response, Ok(vec![10, 7777, 8888, 40]));

// Now we try to read with an invalid register address.
// This should return a Modbus exception response with the code
// IllegalDataAddress.
println!("CLIENT: Reading nonexisting holding register address... (should return IllegalDataAddress)");
let response = ctx.read_holding_registers(0x100, 1).await;
let response = ctx.read_holding_registers(0x100, 1).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert!(response.is_err());
// TODO: How can Modbus client identify Modbus exception responses? E.g. here we expect IllegalDataAddress
// Question here: https://github.com/slowtec/tokio-modbus/issues/169
assert_eq!(response, Err(Exception::IllegalDataAddress));

println!("CLIENT: Done.")
},
Expand Down
2 changes: 1 addition & 1 deletion examples/tls-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Reading Holding Registers");
let data = ctx.read_holding_registers(40000, 68).await?;
println!("Holding Registers Data is '{:?}'", data);
ctx.disconnect().await?;
ctx.disconnect().await??;

Ok(())
}
56 changes: 22 additions & 34 deletions examples/tls-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,37 +82,26 @@ impl tokio_modbus::server::Service for ExampleService {

fn call(&self, req: Self::Request) -> Self::Future {
match req {
Request::ReadInputRegisters(addr, cnt) => {
match register_read(&self.input_registers.lock().unwrap(), addr, cnt) {
Ok(values) => future::ready(Ok(Response::ReadInputRegisters(values))),
Err(err) => future::ready(Err(err)),
}
}
Request::ReadHoldingRegisters(addr, cnt) => {
match register_read(&self.holding_registers.lock().unwrap(), addr, cnt) {
Ok(values) => future::ready(Ok(Response::ReadHoldingRegisters(values))),
Err(err) => future::ready(Err(err)),
}
}
Request::WriteMultipleRegisters(addr, values) => {
match register_write(&mut self.holding_registers.lock().unwrap(), addr, &values) {
Ok(_) => future::ready(Ok(Response::WriteMultipleRegisters(
addr,
values.len() as u16,
))),
Err(err) => future::ready(Err(err)),
}
}
Request::WriteSingleRegister(addr, value) => {
match register_write(
Request::ReadInputRegisters(addr, cnt) => future::ready(
register_read(&self.input_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadInputRegisters),
),
Request::ReadHoldingRegisters(addr, cnt) => future::ready(
register_read(&self.holding_registers.lock().unwrap(), addr, cnt)
.map(Response::ReadHoldingRegisters),
),
Request::WriteMultipleRegisters(addr, values) => future::ready(
register_write(&mut self.holding_registers.lock().unwrap(), addr, &values)
.map(|_| Response::WriteMultipleRegisters(addr, values.len() as u16)),
),
Request::WriteSingleRegister(addr, value) => future::ready(
register_write(
&mut self.holding_registers.lock().unwrap(),
addr,
std::slice::from_ref(&value),
) {
Ok(_) => future::ready(Ok(Response::WriteSingleRegister(addr, value))),
Err(err) => future::ready(Err(err)),
}
}
)
.map(|_| Response::WriteSingleRegister(addr, value)),
),
_ => {
println!("SERVER: Exception::IllegalFunction - Unimplemented function code in request: {req:?}");
future::ready(Err(Exception::IllegalFunction))
Expand Down Expand Up @@ -273,28 +262,27 @@ async fn client_context(socket_addr: SocketAddr) {
println!("CLIENT: Reading 2 input registers...");
let response = ctx.read_input_registers(0x00, 2).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, [1234, 5678]);
assert_eq!(response, Ok(vec![1234, 5678]));

println!("CLIENT: Writing 2 holding registers...");
ctx.write_multiple_registers(0x01, &[7777, 8888])
.await
.unwrap()
.unwrap();

// Read back a block including the two registers we wrote.
println!("CLIENT: Reading 4 holding registers...");
let response = ctx.read_holding_registers(0x00, 4).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert_eq!(response, [10, 7777, 8888, 40]);
assert_eq!(response, Ok(vec![10, 7777, 8888, 40]));

// Now we try to read with an invalid register address.
// This should return a Modbus exception response with the code
// IllegalDataAddress.
println!("CLIENT: Reading nonexisting holding register address... (should return IllegalDataAddress)");
let response = ctx.read_holding_registers(0x100, 1).await;
let response = ctx.read_holding_registers(0x100, 1).await.unwrap();
println!("CLIENT: The result is '{response:?}'");
assert!(response.is_err());
// TODO: How can Modbus client identify Modbus exception responses? E.g. here we expect IllegalDataAddress
// Question here: https://github.com/slowtec/tokio-modbus/issues/169
assert_eq!(response, Err(Exception::IllegalDataAddress));

println!("CLIENT: Done.")
},
Expand Down
Loading

0 comments on commit f8a8834

Please sign in to comment.