Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refacto: new Client API #248

Merged
merged 31 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
831415f
feat(lib): add `Result` specialized type for public API
creberust Feb 2, 2024
8bcc9a5
refacto(client): change return type from `io::Result<Response>` to `i…
creberust Feb 2, 2024
2b678c5
refacto(client: sync): change return type from `io::Result<Response>`…
creberust Feb 2, 2024
ff7753a
refacto(service): update `tcp` client with new API
creberust Feb 2, 2024
cf61b8f
refacto(service): update `rtu` client with new API
creberust Feb 2, 2024
a708092
tests: update `Exception` integration test with new client API
creberust Feb 2, 2024
ed9aa22
examples: update clients with the new API
creberust Feb 2, 2024
bfc06df
fix: clippy warnings
creberust Feb 2, 2024
9a28c95
refacto(client): only import `std::io`, not full path
creberust Feb 5, 2024
41afe94
refacto(service): only import `std::io`, not full path
creberust Feb 5, 2024
289b4f0
refacto(examples): updated `tcp-server` with new `Client` API
creberust Feb 9, 2024
6f22254
refacto(examples): updated `tls-server` with new `Client` API
creberust Feb 9, 2024
4368d78
refacto(examples): updated `rtu-over-tcp-server` with new `Client` API
creberust Feb 13, 2024
dec1a97
refacto(examples): updated `rtu-server` with new `Client` API
creberust Feb 13, 2024
94e7755
refacto(examples): updated `rtu-server-address` with new `Client` API
creberust Feb 13, 2024
484f21f
refacto(lib): add `Result` type alias
creberust Feb 14, 2024
a002568
refacto: update `crate::Result` to `Result`
creberust Feb 14, 2024
3176718
examples(tls-client): fix clippy warning
creberust Feb 14, 2024
3e0d8d6
docs(changelog): add CHANGELOG entries
creberust Feb 19, 2024
b8ce59d
docs(changelog): fix indentation
creberust Feb 19, 2024
9a880cd
docs(changelog): add missing code blocks context
creberust Feb 19, 2024
5ca95c0
refacto(service): move `verify_response_header` in parent mod
creberust Feb 22, 2024
988bbc7
docs(client): add comments explaining why we can use `unreachable` in…
creberust Feb 22, 2024
e811b18
docs(client): improve error message if `unreachable` is reached
creberust Feb 22, 2024
3289858
feat(error): add helper function to generate error message
creberust Feb 22, 2024
0e33be3
refacto(client): call helper function to generate the error message
creberust Feb 22, 2024
164082d
fix(error): remove trailing whitespaces
creberust Feb 22, 2024
ae698ce
Merge branch 'main' into refacto-client-api
uklotzde Mar 9, 2024
6d2c31c
Fix clippy issues for --no-default-features
uklotzde Mar 9, 2024
84d419e
Merge pull request #1 from uklotzde/refacto-client-api
creberust Mar 11, 2024
fcc1510
doc(changelog): apply suggestion to changelog to improve wording
creberust Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
```

creberust marked this conversation as resolved.
Show resolved Hide resolved
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