rmodbus

repository·master·Indexed 41 days ago

https://github.com/alttch/rmodbus

A fast and platform-independent Modbus server framework for Rust. It provides a request/response codec and a context manager for building Modbus-powered applications across TCP/UDP, RTU, and ASCII protocols. Supports both std and no_std environments, with flexible storage options (Full, Small, or Custom-sized) and support for various vector implementations like fixedvec and heapless. Version 0.12.2.

Tokens
12.5K
Snippets
30
Records
40
Agent score
52%

What's inside rmodbus

  1. What is rmodbus

    master
    rmodbus is a framework for building Modbus-powered applications. It is not a standalone client or server, but rather a set of tools including a request/response codec and a context manager. It is designed to be transport- and protocol-independent, making it suitable for Modbus TCP/UDP, RTU, and ASCII. It supports both std and no_std environments and can be used in blocking or async applications.
  2. Manage Modbus server context

    master

    The ModbusContext is the central data store for a Modbus server. It manages coils, discrete inputs, input registers, and holding registers.

    Best Practices:

    • Use one standard context per application.
    • The context is thread-safe but must be protected by a Mutex or RwLock in your application.
    • Lock the context only when required and for short periods to avoid performance degradation.
    • For high-performance or memory-constrained systems, choose an appropriate storage size (see 'Custom-sized storage').

    Common Operations:

    • get_holding(address): Read a holding register.
    • get_holdings_as_f32(address): Read an IEEE754 f32 from registers.
    • set_coil(address, value): Write a coil.
    • set_holdings_bulk(address, data): Write multiple registers at once.
  3. Use different vector types for memory management

    master

    Depending on your target environment and enabled features, rmodbus supports different vector implementations:

    • std (default): Uses std::vec::Vec.
    • fixedvec feature: Uses fixedvec::FixedVec.
    • heapless feature: Uses heapless::Vec.
    • alloc feature: Enables alloc::vec::Vec for no_std environments with an allocator.
  4. Implement a Modbus TCP client

    master

    To use the client, use ModbusRequest to generate frames and guess_response_frame_len to handle variable-length responses.

    Workflow:

    1. Connect to the server via a transport (e.g., TcpStream).
    2. Create a ModbusRequest object with the appropriate unit ID and protocol.
    3. Use methods like generate_set_coils_bulk or generate_get_coils to populate a request buffer.
    4. Write the buffer to the stream.
    5. Read the response. Since Modbus TCP/UDP responses have a known header length, use guess_response_frame_len to determine how many more bytes to read from the stream.
    6. Use parse_ok or parse_bool to validate the response and extract data.
    use std::io::{Read, Write};
    use std::net::TcpStream;
    use std::time::Duration;
    
    use rmodbus::{client::ModbusRequest, guess_response_frame_len, ModbusProto};
    
    let timeout = Duration::from_secs(1);
    
    // open TCP connection
    let mut stream = TcpStream::connect("localhost:5502").unwrap();
    stream.set_read_timeout(Some(timeout)).unwrap();
    stream.set_write_timeout(Some(timeout)).unwrap();
    
    // create request object
    let mut mreq = ModbusRequest::new(1, ModbusProto::TcpUdp);
    mreq.tr_id = 2;
    
    // set 2 coils
    let mut request = Vec::new();
    mreq.generate_set_coils_bulk(0, &[true, true], &mut request).unwrap();
    
    // write request to stream
    stream.write(&request).unwrap();
    
    // read first 6 bytes of response frame
    let mut buf = [0u8; 6];
    stream.read_exact(&mut buf).unwrap();
    let mut response = Vec::new();
    response.extend_from_slice(&buf);
    let len = guess_response_frame_len(&buf, ModbusProto::TcpUdp).unwrap();
    
    // read rest of response frame
    if len > 6 {
        let mut rest = vec![0u8; (len - 6) as usize];
        stream.read_exact(&mut rest).unwrap();
        response.extend(rest);
    }
    
    // check if frame has no Modbus error inside
    mreq.parse_ok(&response).unwrap();
  5. Implement a simple TCP blocking Modbus server

    master

    To build a server, you must provide your own transport layer (e.g., TcpListener). rmodbus is used to parse incoming frames from the stream and process them against a ModbusContext.

    Key steps in the loop:

    1. Read bytes from the stream into a ModbusFrameBuf.
    2. Create a ModbusFrame specifying the unit ID and protocol (e.g., ModbusProto::TcpUdp).
    3. Call frame.parse() to decode the frame.
    4. If frame.processing_required is true, call frame.process_read() or frame.process_write() using your application's ModbusContext.
    5. If frame.response_required is true, call frame.finalize_response() and write the resulting buffer back to the stream.
    use std::io::{Read, Write};
    use std::net::TcpListener;
    use std::thread;
    use std::sync::RwLock;
    use once_cell::sync::Lazy;
    
    use rmodbus::{
        server::{storage::ModbusStorageFull, context::ModbusContext, ModbusFrame},
        ModbusFrameBuf, ModbusProto,
    };
    
    static CONTEXT: Lazy<RwLock<ModbusStorageFull>> = Lazy::new(<_>::default);
    
    pub fn tcpserver(unit: u8, listen: &str) {
        let listener = TcpListener::bind(listen).unwrap();
        println!("listening started, ready to accept");
        for stream in listener.incoming() {
            thread::spawn(move || {
                println!("client connected");
                let mut stream = stream.unwrap();
                loop {
                    let mut buf: ModbusFrameBuf = [0; 256];
                    let mut response = Vec::new(); // for nostd use FixedVec with alloc [u8;256]
                    if stream.read(&mut buf).unwrap_or(0) == 0 {
                        return;
                    }
                    let mut frame = ModbusFrame::new(unit, &buf, ModbusProto::TcpUdp, &mut response);
                    if frame.parse().is_err() {
                        println!("server error");
                        return;
                    }
                    if frame.processing_required {
                        let result = if frame.readonly {
                            frame.process_read(&*CONTEXT.read().unwrap())
                        } else {
                            frame.process_write(&mut *CONTEXT.write().unwrap())
                        };
                        if result.is_err() {
                            println!("frame processing error");
                            return;
                        }
                    }
                    if frame.response_required {
                        frame.finalize_response().unwrap();
                        println!("{:x?}", response.as_slice());
                        if stream.write(response.as_slice()).is_err() {
                            return;
                        }
                    }
                }
            });
        }
    }
  6. Configure rmodbus for `no_std` environments

    master

    To use rmodbus in a no_std environment, disable the default features in your Cargo.toml:

    rmodbus = { version = "*", default-features = false }

    Note that when using no_std, you may need to enable specific features like alloc to use alloc::vec::Vec or use fixedvec/heapless for vector-like structures.

  7. Configure storage size for Modbus context

    master

    Depending on your RAM constraints, you can choose different storage implementations:

    1. Full Storage: ModbusStorageFull provides 10,000 registers of each type (approx. 60KB RAM).
    2. Small Storage: ModbusStorageSmall provides 1,000 registers of each type.
    3. Custom-sized Storage: Use the generic ModbusStorage to define exact sizes for coils, discretes, inputs, and holdings using generic constants.

    Generic Constant Order: ModbusStorage<COILS, DISCRETES, INPUTS, HOLDINGS>

    // Small storage
    use rmodbus::server::{storage::ModbusStorageSmall, context::ModbusContext};
    
    // Custom-sized storage: 128 coils, 16 discretes, 0 inputs, 100 holdings
    use rmodbus::server::{storage::ModbusStorage, context::ModbusContext};
    let context = ModbusStorage::<128, 16, 0, 100>::new();
  8. Implement the ModbusContext trait for custom server storage

    master

    To build a custom Modbus server, you must implement the ModbusContext trait. This trait defines how the server interacts with your underlying data storage for all four Modbus data types: Coils, Discretes, Input Registers, and Holding Registers.

    Methods in this trait allow for:

    • Single access: Getting or setting a single bit (coil/discrete) or register (input/holding).
    • Bulk access: Reading or writing multiple values at once using slices or vectors.
    • Type conversion: Reading/writing multi-register values as u32, u64, f32, or custom types implementing RegisterRepresentable.
    • Byte-level access: Reading/writing data as u8 sequences, with specific support for bit-packed coils/discretes or byte-aligned representations.
  9. Use built-in endianness representations for u32 and u64

    master

    The rmodbus::server::representations module provides pre-defined types for storing multi-register values. These types handle the mapping between large integers and u16 register sequences using different endianness patterns.

    Note on Endianness: In this context, 'Endianness' refers to the order of 16-bit words (registers). The bytes within each individual 16-bit word are always big-endian.

    For example, a u32 like 0x1122_3344 would be represented as 3344 1122 in Little Endian (the low-order word comes first).

    // Example of using representations with a Modbus context
    let mut ctx = ModbusStorageFull::default();
    let to_be_stored: u32 = 123u32;
    
    // Store as little endian at address 1
    ctx.set_inputs_from_representable(1, &U32LittleEndian(to_be_stored)).unwrap();
    
    // Read it back
    let read: U32LittleEndian = ctx.get_inputs_as_representable(1).unwrap();
    assert_eq!(U32LittleEndian(to_be_stored), read);
  10. Implement ModbusContext with ModbusStorage

    master

    The ModbusStorage struct implements the ModbusContext trait, providing a complete set of methods to interact with Modbus registers. This allows you to use ModbusStorage as the data backend for a Modbus server.

    Key capabilities include:

    • Single Register Access: get_coil, get_discrete, get_input, get_holding, set_coil, etc.
    • Bulk Access: get_holdings_bulk, set_coils_bulk, etc.
    • Type Conversions: Methods to read/write registers as u32, u64, or f32 (using IEEE 754 representation).
    • Byte/Buffer Access: Methods like get_holdings_as_u8 and set_holdings_from_u8 for raw byte manipulation.
  11. Implement a Modbus server using ModbusFrame

    master

    To implement a Modbus server, use the ModbusFrame struct to process incoming buffers. The typical workflow involves:

    1. Initializing a ModbusFrame with a unit ID, the input buffer, the protocol (ModbusProto), and a mutable response buffer.
    2. Calling .parse() to validate the frame and extract function codes, registers, and counts.
    3. Checking .processing_required to determine if the request needs to be applied to your data storage.
    4. Using .process_read(&ctx) or .process_write(&mut ctx) to interact with a ModbusContext.
    5. Checking .response_required and calling .finalize_response() to prepare the response buffer for transmission.

    Note: If you are managing your own data storage instead of using ModbusContext, use .get_external_read()/.get_external_write() followed by .process_external_read()/.process_external_write().

    // Example server loop logic
    let mut ctx = ModbusStorageFull::new();
    let unit_id = 1;
    loop {
        let framebuf: ModbusFrameBuf = [0; 256];
        // ... read frame into framebuf ...
        let mut response = FixedVec::new(&mut mem);
        
        let mut frame = ModbusFrame::new(unit_id, &framebuf, ModbusProto::TcpUdp, &mut response);
        
        if frame.parse().is_ok() {
            if frame.processing_required {
                let result = match frame.readonly {
                    true => frame.process_read(&ctx),
                    false => frame.process_write(&mut ctx)
                };
                if result.is_err() {
                    continue;
                }
            }
            if frame.response_required {
                frame.finalize_response();
                // ... send response ...
            }
        }
    }
  12. Process Modbus requests with an external data source

    master

    If you do not want to use the provided ModbusContext trait, you can manually handle reads and writes by extracting the request details and then notifying the frame of the result. This is useful for integrating with custom hardware or existing databases.

    For Writes:

    1. Call get_external_write() to get a Write enum (containing WriteBits or WriteWords).
    2. Apply the write to your system.
    3. Call process_external_write(result) with the outcome of your operation.

    For Reads:

    1. Call get_external_read() to get a Read enum (containing ReadBits or ReadWords).
    2. Fetch the data from your system and populate the provided buffer.
    3. Call process_external_read(result) to finalize the frame state.
    // Example of external write handling
    match frame.get_external_write()? {
        Write::Bits(bits) => {
            // Apply bits to your custom system
            let result = my_custom_system.write_bits(bits.address, bits.count, bits.data);
            frame.process_external_write(result)?;
        }
        Write::Words(words) => {
            // Apply words to your custom system
            let result = my_custom_system.write_words(words.address, words.count, words.data);
            frame.process_external_write(result)?;
        }
    }