FluentModbus

repository·dev·Indexed 43 days ago

https://github.com/apollo3zehn/fluentmodbus

A .NET Standard library (supporting 2.0 and 2.1) providing high-level implementations for Modbus TCP and RTU servers and clients. It supports various Modbus functions including FC01, FC02, FC03, FC04, FC05, FC06, FC15, FC16, and FC23. The library features asynchronous and synchronous operating modes, custom request validation via RequestValidator, and high-performance buffer access for register updates.

Tokens
3.7K
Snippets
7
Records
9
Agent score
55%

What's inside FluentModbus

  1. Overview of FluentModbus capabilities

    dev

    FluentModbus is a .NET Standard library (supporting 2.0 and 2.1) designed for easy process data exchange via Modbus TCP and RTU. It provides both server (slave) and client (master) implementations.

    Supported Modbus functions include:

    • Class 0: FC03 (ReadHoldingRegisters), FC16 (WriteMultipleRegisters)
    • Class 1: FC01 (ReadCoils), FC02 (ReadDiscreteInputs), FC04 (ReadInputRegisters), FC05 (WriteSingleCoil), FC06 (WriteSingleRegister)
    • Class 2 (Partial): FC15 (WriteMultipleCoils), FC23 (ReadWriteMultipleRegisters)
  2. Configure Modbus TCP Server operating modes

    dev

    The ModbusTcpServer supports two operating modes:

    1. Asynchronous Mode (Default): All client requests are answered immediately. This is easier to implement but can potentially slow down the server if there are many requests in a short time.
    2. Synchronous Mode: You can opt-in by passing isAsynchronous: false (or by setting isAsynchronous: true to the constructor depending on the specific overload, though the sample suggests new ModbusTcpServer(isAsynchronous: true) is the way to enable the mode where you manually trigger updates). In this mode, accumulated client requests are processed only when you explicitly call server.Update().

    To enable synchronous mode, use:

    var server = new ModbusTcpServer(isAsynchronous: true, serverLogger);
  3. Update Modbus TCP Server registers

    dev

    The server can provide data to clients by updating its internal registers. There are two primary ways to do this:

    1. Standard Form (Span<short>): Provides more flexibility using GetHoldingRegisters(). You can use extension methods like SetLittleEndian<T> to write values.
    2. High Performance Form (Buffer Interpretation): Provides less flexibility but higher performance by interpreting the buffer as specific types using GetHoldingRegisterBuffer<T>(). Supported types include byte, short, and int.

    Important: When updating registers in a multi-threaded environment, always wrap the update logic in a lock (server.Lock) block to prevent race conditions with client requests.

    // Inside a loop or periodic task
    lock (server.Lock)
    {
        // Option A: Standard flexibility
        var registers = server.GetHoldingRegisters();
        registers.SetLittleEndian<int>(startingAddress: 5, 12345);
    
        // Option B: High performance buffer access
        var int_buffer = server.GetHoldingRegisterBuffer<int>();
        int_buffer[40] = 100;
    }
  4. Configure Modbus RTU Server and Client

    dev

    To implement Modbus RTU communication, you need a ModbusRtuServer and a ModbusRtuClient connected via COM ports.

    Key Requirements:

    • COM Ports: You must have real or virtual COM ports available. For local testing, you can use a COM port bridge to link two ports (e.g., COM1 for the server and COM2 for the client).
    • Unit Identifier: The client must use the same unitIdentifier that was assigned to the server during instantiation (e.g., 0x01).
    • Server Events: You can subscribe to the RegistersChanged event on the server to detect when register addresses have been modified.

    Server Modes:

    • Asynchronous (Default): Client requests are answered immediately. This is easier to implement but can slow down the server under high load.
    • Synchronous: Opt-in via new ModbusRtuServer(isAsynchronous: true). In this mode, you must manually call server.Update() to process accumulated client requests.
    /* Create Server */
    using var server = new ModbusRtuServer(unitIdentifier: 1)
    {
        EnableRaisingEvents = true
    };
    
    server.RegistersChanged += (sender, registerAddresses) =>
    {
        // registerAddresses contains modified register addresses
    };
    
    /* Create Client */
    var client = new ModbusRtuClient();
  5. Update Modbus RTU Server Registers

    dev

    The ModbusRtuServer provides two ways to update register data. When updating registers manually in a loop, it is recommended to use server.Lock to synchronize buffer access between your application logic and the Modbus client.

    Option A: Standard (Flexible) Use GetHoldingRegisters() to get a Span<short>. This allows for more flexible data manipulation using extension methods like SetLittleEndian<T>.

    Option B: High Performance (Direct Buffer Access) Use GetHoldingRegisterBuffer<T>() to interpret the buffer directly as a specific type (byte, short, int, etc.). This is faster but offers less flexibility than the standard method.

    // Synchronize access using the server lock
    lock (server.Lock)
    {
        // Option A: Standard
        var registers = server.GetHoldingRegisters();
        registers.SetLittleEndian<int>(startingAddress: 5, value);
    
        // Option B: High Performance
        var int_buffer = server.GetHoldingRegisterBuffer<int>();
        int_buffer[40] = value;
    }
  6. Implement custom request validation with RequestValidator

    dev

    You can implement custom business logic for validating Modbus requests by assigning a method to the RequestValidator property of a ModbusTcpServer (or an RTU server).

    Key behaviors:

    • The validator function is called every time a client sends a request.
    • It complements rather than replaces default server checks. The server will still perform its own validation for absolute register limits and unknown function codes.
    • The validator must return a ModbusExceptionCode. Returning ModbusExceptionCode.OK allows the request to proceed, while other codes will trigger a Modbus exception response to the client.
    • The validator receives RequestValidatorArgs, which provides access to the Address, FunctionCode, and other request details.
    var server = new ModbusTcpServer()
    {
        RequestValidator = this.ModbusValidator
    };
    
    private ModbusExceptionCode ModbusValidator(RequestValidatorArgs args)
    {
        // Example: check if address is within valid holding register limits
        var holdingLimits = args.Address >= 50 && args.Address < 90 ||
                            args.Address >= 2000 && args.Address < 2100;
    
        // Example: check if address is within valid input register limits
        var inputLimits = args.Address >= 1000 && args.Address < 2000;
    
        return (args.FunctionCode, holdingLimits, inputLimits) switch
        {
            // holding registers
            (ModbusFunctionCode.ReadHoldingRegisters, true, _)          => ModbusExceptionCode.OK,
            (ModbusFunctionCode.ReadWriteMultipleRegisters, true, _)    => ModbusExceptionCode.OK,
            (ModbusFunctionCode.WriteMultipleRegisters, true, _)        => ModbusExceptionCode.OK,
            (ModbusFunctionCode.WriteSingleRegister, true, _)           => ModbusExceptionCode.OK,
    
            (ModbusFunctionCode.ReadHoldingRegisters, false, _)         => ModbusExceptionCode.IllegalDataAddress,
            (ModbusFunctionCode.ReadWriteMultipleRegisters, false, _)   => ModbusExceptionCode.IllegalDataAddress,
            (ModbusFunctionCode.WriteMultipleRegisters, false, _)       => ModbusExceptionCode.IllegalDataAddress,
            (ModbusFunctionCode.WriteSingleRegister, false, _)          => ModbusExceptionCode.IllegalDataAddress,
    
            // input registers
            (ModbusFunctionCode.ReadInputRegisters, _, true)            => ModbusExceptionCode.OK,
            (ModbusFunctionCode.ReadInputRegisters, _, false)           => ModbusExceptionCode.IllegalDataAddress,
    
            // deny other function codes
            _                                                           => ModbusExceptionCode.IllegalFunction
        };
    }
  7. Set up a Modbus TCP Server and Client

    dev

    To begin using Modbus TCP, initialize a ModbusTcpServer with a logger and enable event raising if you need to monitor register changes. For the client, initialize a ModbusTcpClient.

    Note that when performing server work, you must use server.Lock to synchronize buffer access between your application logic and the Modbus clients.

    /* create Modbus TCP server */
    using var server = new ModbusTcpServer(serverLogger)
    {
        EnableRaisingEvents = true
    };
    
    /* subscribe to the 'RegistersChanged' event */
    server.RegistersChanged += (sender, registerAddresses) =>
    {
        // registerAddresses contains a list of modified register addresses
    };
    
    /* create Modbus TCP client */
    var client = new ModbusTcpClient();
  8. Perform Modbus TCP Client operations

    dev

    The ModbusTcpClient provides methods to read and write various Modbus data types.

    Data Handling Note: Most methods return data as a Span<T>. While efficient, Span<T> cannot be stored in heap-allocated objects or used across certain async boundaries easily. If you need to persist the data or move it out of the immediate scope, call .ToArray() to copy it into a standard array (e.g., var dataArray = data.ToArray();).

    Common operations include:

    • ReadHoldingRegisters<T>(unitIdentifier, startingAddress, count)
    • WriteMultipleRegisters(unitIdentifier, startingAddress, data)
    • ReadCoils(unitIdentifier, startingAddress, count)
    • ReadDiscreteInputs<T>(unitIdentifier, startingAddress, count)
    • ReadInputRegisters(unitIdentifier, startingAddress, count)
    • WriteSingleCoil(unitIdentifier, registerAddress, value)
    • WriteSingleRegister(unitIdentifier, registerAddress, value)
    client.Connect();
    
    try
    {
        // Read Holding Registers (FC03)
        Span<byte> data = client.ReadHoldingRegisters<byte>(0x00, 0, 10);
    
        // Write Multiple Registers (FC16)
        client.WriteMultipleRegisters(0x00, 0, new byte[] { 10, 0, 20, 0, 30, 0, 255, 0, 255, 1 });
    
        // Write Single Coil (FC05)
        client.WriteSingleCoil(0x00, 0, true);
    }
    finally
    {
        client.Disconnect();
    }
  9. Perform Modbus RTU Client Operations

    dev

    The ModbusRtuClient provides methods to execute standard Modbus function codes.

    Important Notes:

    • Data Types: Most read methods return data as a Span<T>. If you need to store this data in a standard .NET array or pass it to APIs that don't support Spans, call .ToArray() on the result.
    • Unit Identifier: Always pass the correct unitIdentifier (the ID assigned to the server) to every method call.

    Supported Operations:

    • ReadHoldingRegisters<T>(unitIdentifier, startingAddress, count) (FC03)
    • WriteMultipleRegisters(unitIdentifier, startingAddress, data) (FC16)
    • ReadCoils(unitIdentifier, startingAddress, count) (FC01)
    • ReadDiscreteInputs(unitIdentifier, startingAddress, count) (FC02)
    • ReadInputRegisters<T>(unitIdentifier, startingAddress, count) (FC04)
    • WriteSingleCoil(unitIdentifier, registerAddress, value) (FC05)
    • WriteSingleRegister(unitIdentifier, registerAddress, value) (FC06)
    var unitIdentifier = 0x01;
    var startingAddress = 0;
    
    // Read Holding Registers
    Span<byte> data = client.ReadHoldingRegisters<byte>(unitIdentifier, startingAddress, 10);
    
    // Write Multiple Registers
    client.WriteMultipleRegisters(unitIdentifier, startingAddress, new byte[] { 10, 00, 20, 00, 30, 00, 255, 00, 255, 01 });
    
    // Read Coils
    Span<byte> coilData = client.ReadCoils(unitIdentifier, startingAddress, 10);
    
    // Write Single Coil
    client.WriteSingleCoil(unitIdentifier, 0, true);