Return Modbus exception codes for client and server.
benjamin-nw opened this issue · 0 comments
This is a tracking issue for progress on adding Modbus Exception
code available to use in the client
and server
side.
Goal
The goal is to provide:
- A simple API to use for the user in the
client
side.- When calling a modbus function, the user should easily access the wanted data or the exception.
- A simple API to use for the user in the
server
side.- When implementing a server, the user should be able to simply return exception codes, and the server should send the appropriate Response type.
Use cases
I'll try to present the wanted usage of this library from my pov. I'm currently using this library as a client
and a server
, and the use cases showed here is what I feel is a good way of using a modbus library in Rust.
Please, feel free to add your use case if you think this proposition is not very good for it. Or, if you agree with the proposition.
Client
The client must be able to call a modbus function and easily see what failed.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use tokio_modbus::prelude::*;
let socket_addr = "127.0.0.1:5502".parse().unwrap();
let mut ctx = tcp::connect(socket_addr).await?;
loop {
// Read input registers and return if a fatal communication error occured
let data = ctx.read_input_registers(0x1000, 7).await?;
match data {
Ok(data) => println!("My data: {:?}", data), // Handle your data here
Err(exception) => println!("Modbus exception: {}", exception), // Handle the exception here
}
}
Ok(())
}
Server
The server is responsible to return a valid response or an exception according to the user needs.
The server should be very simple to implement, because if we follow the Modbus spec, if an error occurs, the server must send a specific Exception
. Thus removing the need to have std::io::Error
on the server implementation side.
Thus, implementing the server will only require returning the correct Response
for a Request
. Returning the appropriate Exception
when an error arise in the implementation (following the modbus specification). If any error should occur during the processing of the modbus command, the Exception::ServerDeviceFailure
must be sent, and the user implementing the server should log the error in order to be able to understand what happened.
use tokio_modbus::{
prelude::*,
server::tcp::{accept_tcp_connection, Server},
};
struct ExampleService {
input_registers: Arc<Mutex<HashMap<u16, u16>>>,
holding_registers: Arc<Mutex<HashMap<u16, u16>>>,
}
impl tokio_modbus::server::Service for ExampleService {
type Request = Request<'static>;
type Response = Response;
type Error = Exception;
type Future = future::Ready<Result<Self::Response, Self::Error>>;
fn call(&self, req: Self::Request) -> Self::Future {
future::ready(self.handle(req))
}
}
impl ExampleService {
fn handle(&self, req: Request<'static>) -> Result<Response, Exception> {
match req {
Request::ReadInputRegisters(addr, cnt) => register_read(&self.input_registers.lock().unwrap(), addr, cnt).map(Response::ReadInputRegisters),
_ => Err(Exception::IllegalFunction),
}
}
}
fn register_read(registers: &HashMap<u16, u16>, addr: Address, cnt: Quantity) -> Result<Vec<u16>, Exception> {
let mut response_value = vec![0; cnt.into()];
for i in 0..cnt {
let reg_addr = addr + i;
if let Some(r) = registers.get(®_addr) {
response_values[i as usize] = *r;
} else {
println!("SERVER: Exception::IllegalDataAddress");
return Err(Exception::IllegalDataAddress);
}
}
Ok(response_values)
}
Here, the server will take care of building the appropriate ExceptionResponse
, because it already has the Request
, it can then build the Response
with the FunctionCode
and the returned Exception
.
Mandatory
We need a few things before being able to send and receive Exception
code.
- Expose
Exception
type in the public API. #218 - BREAKING CHANGE Update the
Client
,Reader
andWriter
trait in order to return anException
. - BREAKING CHANGE Update the server
Service
trait in order to return anException
if an Error occurs. - BREAKING CHANGE Update the server
process
function to correctly build aResponse
or anExceptionResponse
. - Update the examples to show how to return a
Response
or anException
. - Update the documentation to show how to return a
Response
or anException
.
Optional
- BREAKING CHANGE Update
FunctionCode
type in order to simplify the library. #236
Questions
- Should
Exception
implement the traitError
in order to be able to handle errors more easily ?- example:
let data = ctx.read_input_registers(0x1000, 7).await??;
- example:
Previous propositions and discussion
Breaking Changes
This proposition includes a lot of breaking changes to try to simplify the client/server implementation. Since we are not in 1.0
, I hope it's not too much of a change.
Tell me if you think we should do it another way. Or if you have other plans.
Please, feel free to give your feedback in order to improve the current proposition, I'll be updating this issue if more questions arise.