Modbus Client

See the Modbus Protocol page for an introduction to this module.

The Modbus TCP client is an Ethernet implementation of the widely supported, widely used Modbus protocol. The Modbus client is designed in Lua and utilizes the Barracuda App Server socket API. The Modbus client enables business logic, implemented in the Lua scripting language, to communicate with Modbus TCP servers. The Modbus client can also be used for bridging Modbus with protocols such as HTTP, WebSockets, SMQ, MQTT, etc.

Modbus RTU

The Modbus TCP Client is designed for versatility and can function as a Modbus RTU client when paired with the appropriate driver. Specifically, the ESP32 port of the Barracuda App Server comes equipped with this necessary driver. Please refer to the ESP32 Modbus RTU Client for details.

The ESP32 RTU adapter accepts unit identifiers 1 through 247. Unit 0 broadcasts are unsupported and throw before queuing or transmitting a request. The TCP client continues to accept unit 0.

Conversion between Modbus and Lua types

Modbus supports bit values and 16 bit words. Bit values are converted to/from Lua boolean values. Word values are converted to/from Lua numbers. Lua can store both integer and floating point values in the number type.

The Modbus client supports additional types that enable the conversion of types to and from the Modbus 16 bit word value. The following additional types are supported: dword (4 bytes), float (4 bytes), double (8 bytes) and string (raw data). The length parameter counts values of the selected type, or bytes for string. For example, reading 4 double values requests 16 words (32 bytes). Writing a 5 byte string sends 3 words, with a zero byte added at the end. String reads round an odd byte count up to a complete word and return all received bytes, including the extra byte.

Error handling

Transaction methods return a result on success and nil, err on failure. The value err is a string for a socket or local protocol error, or a number for a Modbus exception response. Incorrect API usage can throw; see the method contracts below.

Argument checking and request encoding complete before a transaction is registered. If either throws, no request is sent and no pending callback is retained for that call.

An unsupported register value type throws before sending. Use word, dword, float, double or string; omitting the type selects word.

A reply must match the request's unit, operation and expected payload size. Write acknowledgements must echo the requested address and value or quantity. A mismatched or malformed reply produces nil, "invalidresponse" and closes the connection; in asynchronous mode, the callback rules apply.

When in cosocket mode, all functions return the Modbus transaction number if the Modbus socket write operation succeeds. Any Modbus response exception code will be sent to the callback function -- i.e., on error, the callback will receive the arguments: nil,error.

If an asynchronous send returns nil, err, that submission is removed from the pending queue. Handle the returned error; there is no later onresp notification for that failed submission. The existing connection-close notification rules still apply to onclose and other pending requests.

Transport failures and Modbus response errors close the connection; create a new client to reconnect. An asynchronous socket-read timeout can be retried under the timeout rules below, retaining any partial reply. Argument errors that throw before sending do not close the connection.

Modes of operation

The Modbus client can operate in the two socket modes provided by the Barracuda App Server socket API: blocking mode, and cosocket mode.

The default socket blocking mode is designed exclusively for LSP pages, where a connection may be opened, a request sent, and the LSP page waits in blocking mode for the Modbus response. Modbus code, not operating in an LSP page context, must use cosocket mode. When in cosocket mode, it is essential to understand the limitations that apply to method socket:write(), which is used by the Modbus stack when sending data.

Blocking mode:
mb,err = require"modbus.client".connect("localhost")
if mb then
   local data,err = mb:rcoil(0, 5)
   -- Print return table as JSON
   if data then trace(ba.json.encode(data)) end
   mb:close()
end

In this code, the Modbus instance uses blocking mode, meaning the mb:rcoil() method pauses execution until the Modbus server responds. This mode is optimal for LSP pages, which establish a Modbus TCP connection and then sequentially execute commands. It ensures synchronous operation, with each command processed in order.

Cosocket mode:

For most applications, the cosocket mode (non-blocking mode) is the preferred choice. This approach is exemplified in the following code, which demonstrates the non-blocking mode's usage. It is important to note that when using this mode, you need to specify a callback function.

local function callback(data, err)
   if data then
      trace(ba.json.encode(data))
   end
end

mb,err = require"modbus.client".connect("localhost",{async=true})
if mb then
   mb:rcoil(0, 5, callback)
end

The cosocket mode enables pipelining of Modbus requests and enables multiple requests to be issued without having to wait for the Modbus response.

The Modbus client creates its own cosocket when operating in cosocket mode (config option async). When operating in cosocket mode, the cosocket waits for TCP messages sent from the Modbus server, parses the response independently from any sent messages, and dispatches the parsed response to the asynchronous callback function provided when the request was initiated.

The Modbus methods do not return the result when operating in cosocket mode. Instead, the methods return the Modbus transaction number. The same transaction number is passed into the callback function when the server sends the response message.

The callback function receives four arguments, where you are required to use at least two. The following two functions illustrate the data passed into the callback and how the data may be used:

function callback(data, err, transaction, mb) -- if all arguments are used function callback(data, err) -- Minimum set of arguments

Callback function's arguments:

The following example shows how to open a Modbus server connection, send one asynchronous message, and how to close the connection. The purpose with the two assert calls is to further explain the callback function's arguments.

local mb -- The Modbus instance
local transaction -- last sent transaction number

local function mycallback(data, err, _transaction, _mb)
   assert(transaction == _transaction) -- integrity check
   assert(mb == _mb) -- Integrity check
   if data then
      -- data is a table with boolean values
   else
      trace("Failed, error code:", err)
   end
   mb:close() -- Close connection
end

mb,err = require"modbus.client".connect("localhost", {async=true})
if mb then
   transaction = mb:rcoil(0, 5, mycallback) -- Initiate request
end

API for Creating a Modbus Client

Transaction methods take the following optional arguments: [,uid] [,onresp].

The brackets indicate they are optional. The onresp callback function is required when operating in Cosocket mode. The 'uid', defaulting to one, is the Unit Identifier. This identifier is required for Modbus/TCP devices that function as composites of multiple Modbus devices, such as in Modbus/TCP to Modbus RTU gateways. It specifies the Slave Address of the device behind the gateway. However, devices natively capable of Modbus/TCP typically overlook the Unit Identifier.

function connect(addr [,op])

Creates and connects a Modbus client instance:
mb, err=require"modbus.client".connect(addr,op)

Parameters

Return values

Throws

Throws for an unsupported addr type or invalid arguments passed to the underlying socket constructor. Callback errors are not caught by the Modbus module.

An owned socket uses asynchronous transactions even when op.async is omitted. The returned client is ready to send requests before start() is called. Call start() once in the owning coroutine; it receives and dispatches replies until the receive loop ends. This is the same startup convention used by the RTU adapter.

ba.socket.event(function()
   local mb, start = require"modbus.client".connect("localhost")
   if not mb then trace("Connect failed", start); return end
   -- Retain mb here if other application code will initiate requests.
   local transaction, err = mb:rcoil(0, 5, function(data, err)
      if data then trace(ba.json.encode(data))
      else trace("Read failed", err) end
   end)
   if not transaction then trace("Send failed", err); return end
   start() -- Dispatch replies in this socket's coroutine.
end)

start()

Runs the receive loop returned by connect() for an owned socket or RTU adapter.

Parameters

None. The function retains its Modbus client.

Return values

None. It returns after the receive loop ends. Replies and connection errors are delivered through the configured callbacks.

Throws

Errors raised by callbacks or the transport implementation propagate. Call this function once in the coroutine that owns the transport.

Modbus Object Methods

method mb:rcoil(addr, len [,uid] [,onresp])

Read coil(s) (function code 1)

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

method mb:wcoil(addr, val [,uid] [,onresp])

Write single coil (function code 5) or write multiple coils (function code 15)

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

method mb:discrete(addr, len [,uid] [,onresp])

Read discrete input (function code 2)

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

method mb:rholding(addr, len [,vtype] [,uid] [,onresp])

Read holding register(s) (function code 3)

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

-- Example:
local data = mb:rholding(4000, 10, "dword")
if data then trace(ba.json.encode(data)) end
method mb:wholding(addr, val [,vtype] [,uid] [,onresp])

Write single register (function code 6) or write multiple registers (function code 16)

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

-- Example:
mb:wholding(4000, {1,2,3,4,5,6,7,8,9,10}, "dword")
method mb:register(addr, len [,vtype] [,uid] [,onresp])

Read input register(s) aka analog input register(s) (function code 4)

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

method mb:readwrite(raddr, rlen, waddr, wval [,vtype] [,uid] [,onresp])

Read/write multiple registers (function code 23). This function code performs a combination of one read operation and one write operation in a single Modbus transaction. Note that this operation is not widely supported by Modbus servers.

Parameters

Return values

Throws

Throws for incorrect argument types, a quantity outside the supported range, an invalid numeric uid, or a missing asynchronous callback. Argument conversion errors can also throw. Supply a supported value type and valid addresses and values; the module does not validate every protocol constraint.

method mb:connected()

Parameters

None.

Return values

Throws

Errors from an invalid underlying socket object propagate.

method mb:close()

Closes the socket connection. This function also terminates the cosocket if the Modbus client is operating in asynchronous mode. The optional onclose callback will not be called when this method is called.

As the asynchronous receive loop finishes, each pending request receives onresp(nil, "closed", transaction, mb). Here transaction is its integer request identifier and mb is the client table. Completed requests are not notified again. Pending requests are removed before callbacks run, and repeated close calls do not repeat their notifications. Notification order is unspecified.

Callbacks run in the receiving coroutine, so they may run after close() returns. If connect() returned a start function, that function must run to finish the receive loop and deliver notifications, including when the client was closed before start(). If a cancellation callback throws, the remaining pending callbacks are still attempted, then the first error is raised in the receiving coroutine.

Parameters

None.

Return values

Throws

Errors from the underlying socket close operation propagate.