Table of Contents

Modbus TCP

The following sample show how a ModbusTCP server and client can interact with each other. The full sample is located here.

Notes

Note

In this sample, the server runs in asynchronous mode, i.e. all client requests are answered immediately. Since this can potentially slow down the server when there are too many requests within short time, there is also the synchronous operating mode, which can be opted-in via new ModbusTcpServer(isAsynchronous: true). When you do so, the accumulated client requests are processed with a call to server.Update(). See the introduction for a short sample.

Setup

First, create the server and client instances and a logger to print the progress to the console:

static async Task Main(string[] args)
{
    /* create logger */
    var loggerFactory = LoggerFactory.Create(loggingBuilder =>
    {
        loggingBuilder.SetMinimumLevel(LogLevel.Debug);
        loggingBuilder.AddConsole();
    });

    var serverLogger = loggerFactory.CreateLogger("Server");
    var clientLogger = loggerFactory.CreateLogger("Client");

    /* create Modbus TCP server */
    using var server = new ModbusTcpServer(serverLogger)
    {
        // see 'RegistersChanged' event below
        EnableRaisingEvents = true
    };

    /* subscribe to the 'RegistersChanged' event (in case you need it) */
    server.RegistersChanged += (sender, registerAddresses) =>
    {
        // the variable 'registerAddresses' contains a list of modified register addresses
    };

    /* create Modbus TCP client */
    var client = new ModbusTcpClient();

When everything is prepared, first start the server ...


    /* run Modbus TCP server */
    var cts = new CancellationTokenSource();

    var task_server = Task.Run(async () =>
    {
        server.Start();
        serverLogger.LogInformation("Server started.");

        while (!cts.IsCancellationRequested)
        {
            // lock is required to synchronize buffer access between this application and one or more Modbus clients
            lock (server.Lock)
            {
                DoServerWork(server);
            }

            // update server buffer content once per second
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
    }, cts.Token);

... and then the client:


    /* run Modbus TCP client */
    var task_client = Task.Run(() =>
    {
        client.Connect();

        try
        {
            DoClientWork(client, clientLogger);
        }
        catch (Exception ex)
        {
            clientLogger.LogError(ex.Message);
        }

        client.Disconnect();

        Console.WriteLine("Tests finished. Press any key to continue.");
        Console.ReadKey(intercept: true);
    });

Finally, wait for the the client to finish its work and then stop the server using the cancellation token source.

    // wait for client task to finish
    await task_client;

    // stop server
    cts.Cancel();
    await task_server;

    server.Stop();
    serverLogger.LogInformation("Server stopped.");
}

Server operation

While the server runs, the method below is executed periodically once per second. This method shows how the server can update it's registers to provide new data to the clients.

static void DoServerWork(ModbusTcpServer server)
{
    var random = new Random();

    // Option A: normal performance version, more flexibility

        /* get buffer in standard form (Span<short>) */
        var registers = server.GetHoldingRegisters();
        registers.SetLittleEndian<int>(startingAddress: 5, random.Next());

    // Option B: high performance version, less flexibility

        /* interpret buffer as array of bytes (8 bit) */
        var byte_buffer = server.GetHoldingRegisterBuffer<byte>();
        byte_buffer[20] = (byte)(random.Next() >> 24);

        /* interpret buffer as array of shorts (16 bit) */
        var short_buffer = server.GetHoldingRegisterBuffer<short>();
        short_buffer[30] = (short)(random.Next(0, 100) >> 16);

        /* interpret buffer as array of ints (32 bit) */
        var int_buffer = server.GetHoldingRegisterBuffer<int>();
        int_buffer[40] = random.Next(0, 100);
}

Client operation

When the client is started, the method below is executed only once. After that, the client stops and the user is prompted to enter any key to finish the program.

Note

All data is returned as Span<T>, which has great advantages but also some limitations as described in the repository's README. If you run into these limitations, simply call ToArray() to copy the data into a standard .NET array (e.g. var newData = data.ToArray();)

static void DoClientWork(ModbusTcpClient client, ILogger logger)
{
    Span<byte> data;

    var sleepTime = TimeSpan.FromMilliseconds(100);
    var unitIdentifier = 0x00;
    var startingAddress = 0;
    var registerAddress = 0;

    // ReadHoldingRegisters = 0x03,        // FC03
    data = client.ReadHoldingRegisters<byte>(unitIdentifier, startingAddress, 10);
    logger.LogInformation("FC03 - ReadHoldingRegisters: Done");
    Thread.Sleep(sleepTime);

    // WriteMultipleRegisters = 0x10,      // FC16
    client.WriteMultipleRegisters(unitIdentifier, startingAddress, new byte[] { 10, 00, 20, 00, 30, 00, 255, 00, 255, 01 });
    logger.LogInformation("FC16 - WriteMultipleRegisters: Done");
    Thread.Sleep(sleepTime);

    // ReadCoils = 0x01,                   // FC01
    data = client.ReadCoils(unitIdentifier, startingAddress, 10);
    logger.LogInformation("FC01 - ReadCoils: Done");
    Thread.Sleep(sleepTime);

    // ReadDiscreteInputs = 0x02,          // FC02
    data = client.ReadDiscreteInputs<byte>(unitIdentifier, startingAddress, 10);
    logger.LogInformation("FC02 - ReadDiscreteInputs: Done");
    Thread.Sleep(sleepTime);

    // ReadInputRegisters = 0x04,          // FC04
    data = client.ReadInputRegisters(unitIdentifier, startingAddress, 10);
    logger.LogInformation("FC04 - ReadInputRegisters: Done");
    Thread.Sleep(sleepTime);

    // WriteSingleCoil = 0x05,             // FC05
    client.WriteSingleCoil(unitIdentifier, registerAddress, true);
    logger.LogInformation("FC05 - WriteSingleCoil: Done");
    Thread.Sleep(sleepTime);

    // WriteSingleRegister = 0x06,         // FC06
    client.WriteSingleRegister(unitIdentifier, registerAddress, 127);
    logger.LogInformation("FC06 - WriteSingleRegister: Done");
}