slowtec / tokio-modbus

A tokio-based modbus library
Apache License 2.0
405 stars 120 forks source link

Return Modbus exception codes for client and server. #237

Closed benjamin-nw closed 8 months ago

benjamin-nw commented 9 months ago

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:

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(&reg_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.

Optional

Questions

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.