Automation Professionals' Advanced Modbus Driver

Hi

We have been experimenting with this module and its running as a modbus server.

The module seems to run fine on windows, but on linux I get this error:

ServerTCPChannel 05Nov2021 14:48:26 [ModbusTCPServer] Listener Error TCP Channel ServerSocket[unbound]
java.net.BindException: Permission denied (Bind failed)

any tips on how to solve the permission issue? Im running Ubuntu 20.04.3 LTS and its bound to 0.0.0.0
ufw has open port 502.

Solved( linux require root access for ports below 1024)

1 Like

Instead of running as root, I recommend adding the following in your systemd configuration:

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
2 Likes

Hi @pturmel any chance you’re Modbus Driver supports function code 23? I don’t see it listed above. We’ve had to use UCON in the past to build this out but would prefer something of the shelf. There is an oven controller we’re working with that requires it:

No, sorry. I didn’t implement it because there’s no way in Ignition’s OPC server to signal that a read and a write belong together. If I added support for this, it would have to be through a scripting call.

Let me investigate.

It looks like it would be relatively easy to add a script function of the following form:

system.opc.rawModbus(devName, unit, functionCode, payload, responseLen)

devName would be a string, unit, functionCode, and responseLen would be integers, and payload would be a byte array or list of bytes.

It would return a CompletableFuture, from which you would get the result and response payload. (As a QualifiedValue.)

You would have to construct the byte payload to send and decode the byte payload response.

Give me a few days to work this in.

We already have it built out in Kepware so don’t go too crazy. it would be nice to have an alternative to our custom protocol within ucon but not that big a deal.

Too late. Already coded. Doing some testing. (:

Considering the approach allows supporting any exotic function desired, I think it is worth including.

The scripting function system.opc.rawModbus() has now been implemented with the signature shown above. The user manual hasn’t been updated yet, but y’all can play with it:

For Ignition v8.1

For Ignition v7.9

Note that this function is available in Gateway scope only. For use elsewhere, like in the Designer’s script console, I recommend a gateway message handler of this form:

def handleMessage(payload):
	from java.lang import Throwable
	import traceback

	dev, unit, func, pay, rsplen = [payload.get(x, None) for x in ['devName', 'unit', 'functionCode', 'payload', 'responseLen']]
	try:
		return system.opc.rawModbus(dev, unit, func, pay, rsplen).get()
	except Throwable, t:
		return serialization.stringException(t)
	except:
		return traceback.format_exc()

In a designer script console, @plarochelle’s desired use of function 23 would look like this:

from java.io import ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream
from com.inductiveautomation.ignition.common.model.values import QualifiedValue

# Raw implementation of write/read multiple registers, function 23.
# Start by constructing the payload of the Modbus PDU.  It is
# a UINT read starting address and a UINT number of read registers (<=125),
# then UINT write starting address and UINT number to write (<=121),
# then USINT bytes to follow, then INT/UINT per register.

baos = ByteArrayOutputStream()
dos = DataOutputStream(baos)
# Read start address and quantity
dos.writeShort(0x2006)
dos.writeShort(1)
# Write start address and quantity
dos.writeShort(0x2006)
dos.writeShort(11)
# Write registers bytes to follow
dos.writeByte(22)
# Write registers
dos.writeShort(0x5753)
dos.writeShort(1)
dos.writeShort(5)
dos.writeFloat(654.321)
dos.writeFloat(123.456)

# Ensure data is in the byte stream
dos.flush()

rpcPayload = {'devName': 'P540Rack485', 'unit': 1, 'functionCode': 23, 'payload': baos.toByteArray(), 'responseLen': 3}
retv = system.util.sendRequest('ModbusTest', 'RawModbus', rpcPayload)

if isinstance(retv, QualifiedValue):
	if retv.quality.good:
		bais = ByteArrayInputStream(retv.value)
		dis = DataInputStream(bais)
		bCount = dis.readByte()
		print "Bytes to follow=%d" % bCount
		while bCount > 0:
			bCount -= 2
			register = dis.readShort()
			print "Register = %d" % register
	else:
		print repr(retv)
else:
	print retv

I don’t actually have a device that accepts that function code, so I get a bad QV that indicates the function is unsupported. Testing from someone with such a device would be appreciated, of course.

If you wish to test with something more common, the following is a raw implementation of the common “read holding registers” function 3:

from java.io import ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream
from com.inductiveautomation.ignition.common.model.values import QualifiedValue

# Raw implementation of read multiple registers, function 3.
# Start by constructing the payload of the Modbus PDU.  It is
# a UINT starting address and a UINT number of registers (<=125).

baos = ByteArrayOutputStream()
dos = DataOutputStream(baos)
dos.writeShort(0)
dos.writeShort(5)
dos.flush()

rpcPayload = {'devName': 'P540Rack485', 'unit': 2, 'functionCode': 3, 'payload': baos.toByteArray(), 'responseLen': 11}
retv = system.util.sendRequest('ModbusTest', 'RawModbus', rpcPayload)

if isinstance(retv, QualifiedValue):
	if retv.quality.good:
		bais = ByteArrayInputStream(retv.value)
		dis = DataInputStream(bais)
		bCount = dis.readByte()
		print "Bytes to follow=%d" % bCount
		while bCount > 0:
			bCount -= 2
			register = dis.readShort()
			print "Register = %d" % register
	else:
		print repr(retv)
else:
	print retv

{ Edited to remove links. See below. }

1 Like

Ah, that message handler uses one of my common scripts to convert backtraces to strings so they can go through the network back to the designer or a Vision client. Available here.

Manual has been updated to describe the rawModbus script function. Release version 1.0.0:

For Ignition v8.1.

For Ignition v7.9.

Prior version links removed. Purchase through my Module Sales page.

2 Likes

Bugfix version 1.0.1. Fixes a null pointer exception when a given register is subscribed in multiple places at the same pace, and any of them are the bare IR, HR, or XR datatype. /:

For Ignition v8.1.

For Ignition v7.9.

2 Likes

Bugfix version 1.0.3. @Kevin.Herron enlightened me on the use of the zero slave address in Modbus TCP, and caused me to review my assumptions. This version stops ignoring address zero as it isn't just the broadcast address.

For Ignition v8.1.

For Ignition v7.9.

3 Likes

Hey @pturmel.

I’m trying to use your module and I think I’m doing something wrong.

I have one Ignition gateway running your Modbus Server device and it is bound to 127.0.0.1

It seems to have bound correctly to the port.

There is a separate gateway I’m trying to use to connect to that Modbus Server and it does get connected, but the values being read from the local gateway and the remote gateway are different.

Can you tell me if the Modbus Server, with unit 1 defined, will serve the same value to a local gateway tag as it does to an external Modbus client?

that is localhost. So it will only receive connections on the same server. I think you meant to use 0.0.0.0. That's the wildcard to listen on all interfaces. Remote server will then point at the local server's external IP address.

So I changed the address to 0.0.0.0 but the values are still different between the two.

I am just trying a holding register:
[PLC-FC1]HR2517

It is the same on both systems (local Ignition and remote)
Both are showing up as connected and I can write and read from both but they do not match.

Show me your configurations.

{ Just to clarify: Server device on one gateway listening on 0.0.0.0, Client device on other gateway looking at first gateway’s external IP. }

Client Side (my machine):

Server Side (running your module)


Thanks

Ignition’s native client defaults to node 0 when left out. Mine defaults to node 1 when left out.

1 Like

That was it. Thank you!