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

Return Modbus exception codes for client and server. #237

Closed
1 of 7 tasks
benjamin-nw opened this issue Dec 27, 2023 · 0 comments
Closed
1 of 7 tasks

Return Modbus exception codes for client and server. #237

benjamin-nw opened this issue Dec 27, 2023 · 0 comments

Comments

@benjamin-nw
Copy link
Contributor

benjamin-nw commented Dec 27, 2023

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

  • Expose Exception type in the public API. Publicly export ExceptionResponse and Exception types #218
  • BREAKING CHANGE Update the Client, Reader and Writer trait in order to return an Exception.
  • BREAKING CHANGE Update the server Service trait in order to return an Exception if an Error occurs.
  • BREAKING CHANGE Update the server process function to correctly build a Response or an ExceptionResponse.
  • Update the examples to show how to return a Response or an Exception.
  • Update the documentation to show how to return a Response or an Exception.

Optional

Questions

  • Should Exception implement the trait Error in order to be able to handle errors more easily ?
    • example: let data = ctx.read_input_registers(0x1000, 7).await??;

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.

@benjamin-nw benjamin-nw closed this as not planned Won't fix, can't repro, duplicate, stale Jan 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant