tokio-modbus

repository·main·Indexed 19 days ago

https://github.com/slowtec/tokio-modbus

A pure Rust implementation of the Modbus protocol built on the Tokio runtime. It provides asynchronous and synchronous APIs for implementing Modbus clients (masters) and servers (slaves) over TCP and RTU. The library includes traits for reading and writing data (Reader, Writer) and a Context struct for unified Modbus operations.

Tokens
11.4K
Snippets
35
Records
48
Agent score
68%

What's inside tokio-modbus

  1. Configure tokio-modbus Cargo features

    main

    You can customize the library by enabling or disabling specific features. Use default-features = false to avoid pulling in unused protocols or modes.

    Available features:

    • rtu: Asynchronous RTU client (default)
    • tcp: Asynchronous TCP client (default)
    • rtu-sync: Synchronous RTU client
    • tcp-sync: Synchronous TCP client
    • rtu-server: (Asynchronous) RTU server
    • tcp-server: (Asynchronous) TCP server
    • rtu-over-tcp-server: (Asynchronous) RTU over TCP server
    # Example: Asynchronous TCP client only
    [dependencies]
    tokio-modbus = { version = "*", default-features = false, features = ["tcp"] }
    
    # Example: Asynchronous RTU client only
    [dependencies]
    tokio-modbus = { version = "*", default-features = false, features = ["rtu"] }
    
    # Example: RTU server
    [dependencies]
    tokio-modbus = { version = "*", default-features = false, features = ["rtu-server"] }
    
    # Example: TCP server
    [dependencies]
    tokio-modbus = { version = "*", default-features = false, features = ["tcp-server"] }
  2. Read Device Identification (Function 0x2B)

    main

    The ReadDeviceIdentification request allows a client to retrieve metadata about a device.

    Request Parameters:

    • ReadCode: Specifies the type of access (Basic, Regular, Extended, or Specific).
    • ObjectId: The specific object ID to retrieve (used with ReadCode::Specific).

    Response Structure (ReadDeviceIdentificationResponse):

    • read_code: The type of access performed.
    • conformity_level: The device's identification support level.
    • more_follows: A boolean indicating if more objects are available.
    • next_object_id: The ID of the next object to request if more_follows is true.
    • device_id_objects: A list of DeviceIdObject containing the actual data.

    Extracting Data: Use DeviceIdObject::value_as_str() to attempt to interpret the raw bytes of an object (like VendorName or ProductCode) as a UTF-8 string.

    // Conceptual usage of device identification
    // Requesting basic identification
    let req = Request::ReadDeviceIdentification(ReadCode::Basic, 0x00);
    
    // Processing the response
    if let Response::ReadDeviceIdentification(resp) = response {
        for obj in resp.device_id_objects {
            if let Some(name) = obj.value_as_str() {
                println!("Object ID {}: {}", obj.id, name);
            }
        }
    }
  3. Understand the Modbus Client traits

    main

    The tokio-modbus client API is built around several asynchronous traits that define how to interact with a Modbus slave.

    • Client: The base trait for transport-independent asynchronous communication. It provides the call method for raw Modbus function invocation and a disconnect method for graceful shutdown.
    • Reader: An extension of Client that provides high-level methods for reading data (coils, discrete inputs, holding registers, input registers, etc.).
    • Writer: An extension of Client that provides high-level methods for writing data (single/multiple coils, single/multiple registers, masked writes, etc.).
    • SlaveContext: A trait used to manage the target Slave (unit ID) for subsequent requests.

    Most users will interact with these capabilities through the Context struct, which wraps a boxed Client and implements Client, Reader, Writer, and SlaveContext.

  4. Use smart pointers with the Service trait

    main
    The Service trait includes a blanket implementation for types that implement Deref where the target type also implements Service. This allows you to use smart pointers (like Box<T>, Arc<T>, or &T) directly as a Service without manual forwarding.
  5. Use the Context struct for Modbus operations

    main

    The Context struct is a convenient wrapper that provides a unified interface for all Modbus operations. It implements the Client, Reader, Writer, and SlaveContext traits. You can create a Context from a Box<dyn Client> using Context::from(client) or by using the From implementation.

    Because Context implements SlaveContext, you can use set_slave to specify which Modbus slave ID you are communicating with before performing reads or writes.

  6. Implement a Modbus RTU over TCP server

    main

    To run a Modbus RTU over TCP server, use the Server struct. You must provide a TcpListener to Server::new() and then call either serve() or serve_until().

    Connection Handling

    When a new TCP connection is accepted, the server uses an on_connected callback to instantiate the Modbus Service and the transport layer.

    • If on_connected returns Ok(Some((service, transport))), the connection is processed.
    • If on_connected returns Ok(None), the connection is rejected, but the server continues listening.
    • If on_connected returns Err, the server stops listening and serve() returns an error.

    Lifecycle Management

    • serve(): Runs indefinitely, listening for new connections. It uses an on_process_error callback to handle errors occurring during request processing for individual clients.
    • serve_until(): Runs until an abort_signal (a Future that resolves to ()) is triggered. It returns a Terminated enum indicating whether the server finished naturally or was aborted.
    use tokio::net::TcpListener;
    use tokio_modbus::server::Server;
    
    // 1. Create a listener
    let listener = TcpListener::bind("127.0.0.1:5020").await.unwrap();
    
    // 2. Initialize the server
    let server = Server::new(listener);
    
    // 3. Define how to handle new connections
    let on_connected = |stream, addr| async move {
        // Return your Modbus Service and the transport (e.g., the TcpStream)
        // This is often done via accept_tcp_connection helper
        todo!("Implement connection logic")
    };
    
    // 4. Run the server
    server.serve(&on_connected, |err| {
        eprintln!("Error processing request: {}", err);
    }).await.unwrap();
  7. Implement a Modbus TCP Server with `Server::serve`

    main

    To run a Modbus TCP server, use the Server::new constructor with a tokio::net::TcpListener, then call serve.

    You must provide two closures:

    1. on_connected: A factory function that takes a TcpStream and SocketAddr and returns a future resolving to io::Result<Option<(S, T)>>. S is your implementation of the Service trait, and T is the transport (usually the TcpStream itself).
      • Returning Ok(Some((service, transport))) accepts the connection.
      • Returning Ok(None) rejects the connection but keeps the server running.
      • Returning Err stops the entire server.
    2. on_process_error: A closure called whenever a connection-specific task encounters an error. It must be Clone + Send + 'static.
    use tokio::net::TcpListener;
    use tokio_modbus::server::Service;
    // ... import your Service implementation ...
    
    let listener = TcpListener::bind("127.0.0.1:5502").await.unwrap();
    let server = Server::new(listener);
    
    server.serve(
        &|stream, addr| async move {
            // Return your service and the stream
            Ok(Some((my_service, stream)))
        },
        |err| eprintln("Connection error: {}", err),
    ).await.unwrap();
  8. Use the synchronous Modbus client API

    main

    The synchronous API provides a blocking interface for Modbus operations, wrapping the underlying asynchronous implementation. You can interact with Modbus devices using the Context struct, which implements the Client, Reader, and Writer traits.

    To use the synchronous API, you must ensure the appropriate transport features are enabled (e.g., rtu-sync or tcp-sync). The Context allows you to manage a global timeout for all subsequent operations.

    // Example conceptual usage of the synchronous Context
    // Note: Actual instantiation depends on the transport (TCP/RTU)
    let mut ctx = Context::new(/* transport specific args */);
    
    // Set a timeout for operations
    ctx.set_timeout(Some(Duration::from_secs(5)));
    
    // Read holding registers
    let registers = ctx.read_holding_registers(0x00, 10)?; 
    
    // Write a single register
    ctx.write_single_register(0x00, 0x1234)?; 
  9. Run tests for tokio-modbus

    main

    To verify your installation or test the library's features, you can run tests using cargo test. To ensure all protocol implementations are working, run tests with all features enabled.

    # Run standard tests
    cargo test --workspace
    
    # Run tests for all available features
    cargo test --workspace --all-features
  10. Write data using the Writer trait

    main

    If your client implements the Writer trait (or is a Context), you can use the following methods to modify data on a Modbus slave:

    MethodModbus FunctionDescription
    write_single_coil(addr, coil)0x05Write a single coil
    write_single_register(addr, word)0x06Write a single holding register
    write_multiple_coils(addr, coils)0x0FWrite multiple coils
    write_multiple_registers(addr, words)0x10Write multiple holding registers
    masked_write_register(addr, and_mask, or_mask)0x16Set or clear individual bits of a holding register
    write_file_record(sub_requests)0x15Write file records

    All methods return a Result<()>, except write_file_record which returns Result<Vec<WriteFileRecordSubRequest>>.