Modbus Master

Modbus is one of the most widely deployed communication protocols in industrial automation. It provides a standardized method for reading sensor measurements, monitoring electrical meters, and configuring variable frequency drives.

Adapnex provides client functionality through the ModbusMaster class for Ethernet-based Modbus TCP communication.

Architecture and Concurrency Model

All request methods on ModbusMaster are synchronous blocking operations. When a method is called, the calling thread transmits the request frame across the transport layer and pauses until the slave device responds or the configured timeout duration expires.

Executing blocking network calls on a real-time cyclic task introduces latency jitter and risks triggering task watchdog overruns. For this reason, Modbus communication is best organized within a dedicated freewheeling task group or background worker task.

The ModbusMaster class is completely thread-safe internally. Multiple application tasks can share a single master instance to poll different registers concurrently without external locking.

Instantiating ModbusMaster

To create a ModbusMaster, first establish a TCPConnection using the TCPConnection::Create() static factory method. Pass the resulting connection along with a request timeout duration to the master constructor:

#include "adapnex.h"

// Connect to a Modbus TCP device at 192.168.1.50:502 with a 2-second connection timeout
auto connection = TCPConnection::Create("192.168.1.50", 502, 2s);

if (connection) {
    // Instantiate master with a 500ms request timeout
    ModbusMaster master(std::move(connection), 500ms);
}

Alternatively, you can specify the target address using an IPv4 byte array:

auto connection = TCPConnection::Create(std::array<uint8_t, 4>{192, 168, 1, 50}, 502, 2s);

Reading and Writing Data

The master implements standard Modbus function codes for bit-level coils and discrete inputs, as well as 16-bit registers:

Function Code Modbus Object Operation ModbusMaster Method

1

Coils

Read 1 or more bits

ReadCoils()

2

Discrete Inputs

Read 1 or more bits

ReadDiscreteInputs()

3

Holding Registers

Read 1 or more 16-bit words

ReadHoldingRegisters()

4

Input Registers

Read 1 or more 16-bit words

ReadInputRegisters()

5

Single Coil

Write 1 bit

WriteSingleCoil()

6

Single Holding Register

Write 1 16-bit word

WriteSingleRegister()

15

Multiple Coils

Write consecutive bits

WriteMultipleCoils()

16

Multiple Holding Registers

Write consecutive 16-bit words

WriteMultipleRegisters()

23

Holding Registers

Write and read registers atomically

WriteReadMultipleRegisters()

All multi-value methods accept data buffers either as standard C++ std::span containers or through vector iterator ranges.

Handling Results and Exceptions

Every operational method returns a ModbusMaster::Result status code:

  • Success (kSuccess = 0): The slave responded normally with the requested data.

  • Negative Values (Transport / Local Errors): Indicate local timeout, physical disconnection, or malformed frame reception:

    • kErrorTimeout: The slave failed to respond within the configured timeout window.

    • kErrorCRC: The received response failed checksum validation.

    • kErrorTransport: A socket transmission failure occurred.

    • kErrorInvalidResponse: The received frame structure was corrupt or unexpected.

  • Positive Values (Slave Exceptions): Indicate standard Modbus application errors returned directly by the slave device:

    • kExceptionIllegalFunction (1): The requested function code is unsupported by the slave.

    • kExceptionIllegalDataAddress (2): The register or coil address does not exist on the slave.

    • kExceptionIllegalDataValue (3): The written value is outside the allowed parameter range.

    • kExceptionSlaveDeviceFailure (4): An unrecoverable internal fault occurred on the slave device.

    • kExceptionSlaveDeviceBusy (6): The slave is engaged in a long-duration command.

When working with TCP connections, call Reconnect() to re-establish dropped network sessions.

Full Example: Background Modbus Polling

The following complete application pairs a deterministic 10ms real-time control task group with a background freewheeling task group that polls a remote power meter over Modbus TCP. State is published safely to the real-time loop using atomic variables:

#include <atomic>
#include <thread>
#include "adapnex.h"

// Background task polling power telemetry over Modbus TCP
class EnergyMeterTask final : public Task {
public:
    std::atomic<float> grid_power_kw = 0.0f;
    std::atomic<bool> meter_online = false;

private:
    std::unique_ptr<ModbusMaster> master;

    void Update() override {
        // Initialize or re-establish connection if needed
        if (!master) {
            auto conn = TCPConnection::Create("192.168.1.120", 502, 1s);
            if (!conn) {
                meter_online.store(false, std::memory_order_relaxed);
                std::this_thread::sleep_for(1s);
                return;
            }
            master = std::make_unique<ModbusMaster>(std::move(conn), 250ms);
        }

        // Buffer for two 16-bit holding registers (32-bit float power value)
        std::array<uint16_t, 2> raw_registers = {};

        // Slave ID 1, Register 3000
        const auto result = master->ReadHoldingRegisters(1, 3000, raw_registers);

        if (result == ModbusMaster::Result::kSuccess) {
            // Decode 32-bit IEEE float from two 16-bit Modbus registers
            uint32_t combined = (static_cast<uint32_t>(raw_registers[0]) << 16) | raw_registers[1];
            float power_val = 0.0f;
            std::memcpy(&power_val, &combined, sizeof(float));

            grid_power_kw.store(power_val, std::memory_order_relaxed);
            meter_online.store(true, std::memory_order_relaxed);
        } else {
            meter_online.store(false, std::memory_order_relaxed);

            // Reconnect if the socket closed
            if (result == ModbusMaster::Result::kErrorTransport) {
                master->Reconnect(500ms);
            }
        }

        // Rest briefly between polls to avoid saturating network bandwidth
        std::this_thread::sleep_for(100ms);
    }
};

// Real-time cyclic control task
class MachineControlTask final : public Task {
public:
    EnergyMeterTask *meter = nullptr;

    void Update() override {
        if (!meter) {
            return;
        }

        const bool online = meter->meter_online.load(std::memory_order_relaxed);
        const float power = meter->grid_power_kw.load(std::memory_order_relaxed);

        // React immediately to telemetry within the deterministic 10ms cycle
        if (online && power > 50.0f) {
            // Shed non-critical auxiliary loads
        }
    }
};

void setup() {
    // 1. Create task groups
    const auto control_group = Application::CreateCyclicTaskGroup({.period = 10ms});
    const auto background_group = Application::CreateFreewheelingTaskGroup();

    // 2. Instantiate tasks
    const auto meter_task = background_group->CreateTask<EnergyMeterTask>();
    const auto control_task = control_group->CreateTask<MachineControlTask>();

    // 3. Provide telemetry reference to control task
    control_task->meter = meter_task.get();
}