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.
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.
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.
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.
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.
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.
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:
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
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.
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)
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.
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.
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.
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.
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
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")
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.
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.
Parameters
None.
Return values
Throws
Errors from an invalid underlying socket object propagate.
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.