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 |
|---|---|---|---|
|
Coils |
Read 1 or more bits |
|
|
Discrete Inputs |
Read 1 or more bits |
|
|
Holding Registers |
Read 1 or more 16-bit words |
|
|
Input Registers |
Read 1 or more 16-bit words |
|
|
Single Coil |
Write 1 bit |
|
|
Single Holding Register |
Write 1 16-bit word |
|
|
Multiple Coils |
Write consecutive bits |
|
|
Multiple Holding Registers |
Write consecutive 16-bit words |
|
|
Holding Registers |
Write and read registers atomically |
|
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();
}