Automation Professionals' Advanced Modbus Driver

There’s a note in the manual that you have to restart the device yourself after editing the node list. So you can make multiple edits before any take effect. Open the regular device settings and just hit save.

Excellent Phil!
Both the server and client were able to read/write anything I threw at them. Running through codes 01-06, 15, 16, 22 yielded exactly what I’d expect.

A couple things (8.1):

  1. Double check your documentation section on supported function codes, I think your chart is misaligned.


  2. Is there a syntax to select “None” of a certain register for a client (for example, only Input/Holding registers and no coils)? If I try to leave the range blank, I get the following result:


DefaultExceptionMapper	05Jan2021 16:22:30	Unexpected error occurred
org.apache.wicket.WicketRuntimeException: Error calling method: public void com.automation_pros.modbus.config.settings.ModbusCommonUnitProps.setCoilsRangeString(java.lang.String) on object: com.automation_pros.modbus.config.settings.ModbusClientUnitProps@13ce2397

at org.apache.wicket.core.util.lang.PropertyResolver$MethodGetAndSet.setValue(PropertyResolver.java:1210)

at org.apache.wicket.core.util.lang.PropertyResolver$ObjectAndGetSetter.setValue(PropertyResolver.java:641)

at org.apache.wicket.core.util.lang.PropertyResolver.setValue(PropertyResolver.java:143)

at org.apache.wicket.model.AbstractPropertyModel.setObject(AbstractPropertyModel.java:132)

at org.apache.wicket.Component.setDefaultModelObject(Component.java:3056)

at org.apache.wicket.markup.html.form.FormComponent.setModelObject(FormComponent.java:1544)

at org.apache.wicket.markup.html.form.FormComponent.updateModel(FormComponent.java:1096)

at org.apache.wicket.markup.html.form.Form$FormModelUpdateVisitor.component(Form.java:227)

at org.apache.wicket.markup.html.form.Form$FormModelUpdateVisitor.component(Form.java:197)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:274)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrderHelper(Visits.java:262)

at org.apache.wicket.util.visit.Visits.visitPostOrder(Visits.java:245)

at org.apache.wicket.markup.html.form.FormComponent.visitComponentsPostOrder(FormComponent.java:422)

at org.apache.wicket.markup.html.form.Form.internalUpdateFormComponentModels(Form.java:1792)

at org.apache.wicket.markup.html.form.Form.updateFormComponentModels(Form.java:1756)

at org.apache.wicket.markup.html.form.Form.process(Form.java:912)

at org.apache.wicket.markup.html.form.Form.onFormSubmitted(Form.java:769)

at org.apache.wicket.markup.html.form.Form.onFormSubmitted(Form.java:702)

at jdk.internal.reflect.GeneratedMethodAccessor57.invoke(Unknown Source)

at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.base/java.lang.reflect.Method.invoke(Unknown Source)

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:258)

at org.apache.wicket.RequestListenerInterface.invoke(RequestListenerInterface.java:216)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.invokeListener(ListenerInterfaceRequestHandler.java:240)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.respond(ListenerInterfaceRequestHandler.java:226)

at org.apache.wicket.request.cycle.RequestCycle$HandlerExecutor.respond(RequestCycle.java:814)

at org.apache.wicket.request.RequestHandlerStack.execute(RequestHandlerStack.java:64)

at org.apache.wicket.request.cycle.RequestCycle.execute(RequestCycle.java:253)

at org.apache.wicket.request.cycle.RequestCycle.processRequest(RequestCycle.java:210)

at org.apache.wicket.request.cycle.RequestCycle.processRequestAndDetach(RequestCycle.java:281)

at org.apache.wicket.protocol.http.WicketFilter.processRequest(WicketFilter.java:188)

at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:245)

at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)

at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)

at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)

at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)

at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)

at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)

at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)

at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.Server.handle(Server.java:500)

at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)

at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)

at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)

at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270)

at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)

at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)

at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)

at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388)

at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)

at java.base/java.lang.Thread.run(Unknown Source)

java.lang.NullPointerException: null
  1. I’m having trouble deleting Unit configurations. Also pressing the cancel button seems to encounter the same problem.


DefaultExceptionMapper	05Jan2021 16:32:23	Unexpected error occurred
org.apache.wicket.WicketRuntimeException: Method onLinkClicked of interface org.apache.wicket.markup.html.link.ILinkListener targeted at [Link [Component id = confirm]] on component [Link [Component id = confirm]] threw an exception

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:268)

at org.apache.wicket.RequestListenerInterface.invoke(RequestListenerInterface.java:216)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.invokeListener(ListenerInterfaceRequestHandler.java:240)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.respond(ListenerInterfaceRequestHandler.java:226)

at org.apache.wicket.request.cycle.RequestCycle$HandlerExecutor.respond(RequestCycle.java:814)

at org.apache.wicket.request.RequestHandlerStack.execute(RequestHandlerStack.java:64)

at org.apache.wicket.request.cycle.RequestCycle.execute(RequestCycle.java:253)

at org.apache.wicket.request.cycle.RequestCycle.processRequest(RequestCycle.java:210)

at org.apache.wicket.request.cycle.RequestCycle.processRequestAndDetach(RequestCycle.java:281)

at org.apache.wicket.protocol.http.WicketFilter.processRequest(WicketFilter.java:188)

at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:245)

at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)

at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)

at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)

at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)

at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)

at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)

at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)

at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.Server.handle(Server.java:500)

at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)

at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)

at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)

at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270)

at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)

at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)

at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)

at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388)

at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)

at java.base/java.lang.Thread.run(Unknown Source)

Caused by: java.lang.reflect.InvocationTargetException: null

at jdk.internal.reflect.GeneratedMethodAccessor51.invoke(Unknown Source)

at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.base/java.lang.reflect.Method.invoke(Unknown Source)

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:258)

... 46 common frames omitted

Caused by: java.lang.IllegalArgumentException: Replacement component must have the same id as the component it will replace. Replacement id [[unitsPanel]], replaced id [[config-contents]].

at org.apache.wicket.Component.replaceWith(Component.java:2732)

at com.inductiveautomation.ignition.gateway.web.pages.Config.setConfigPanel(Config.java:227)

at com.inductiveautomation.ignition.gateway.web.components.ConfirmationPanel.onConfirm(ConfirmationPanel.java:102)

at com.inductiveautomation.ignition.gateway.web.components.ConfirmationPanel$1.onClick(ConfirmationPanel.java:58)

at org.apache.wicket.markup.html.link.Link.onLinkClicked(Link.java:189)

... 50 common frames omitted
  1. It’d be nice if you could apply edits from the configuration page directly.

Still, great work! Before getting my hands on this I was most excited about the server functionality, but it looks like there’s a lot of improvements over the IA modbus client module too.

  1. Heh.
  2. Leaving it blank was supposed to work. ):
  3. Eww. Wicket.
  4. Good idea. I even had it, too. Just hadn’t worked out the incantation, yet.

Will work on these later this month (in monster client outage at the moment.)

2 Likes

New release candidates:

For v7.9: here.

For v8.1: here.

Be aware that some wicket resources changed, so a gateway restart is required after upgrade if a prior version was installed.

All of Ben’s items have been addressed, and the alternate address formats have been implemented as described in the manual.

3 Likes

Couple more bugs fixed. Links above updated, broken RCs removed.

1 Like

Forgot to post links to updates I made back in April. ):

Above comment updated.

Did you fix CRC problem in serial mode?

Yes.

Another pair of release candidates:

For v7.9: here.

For v8.1: here.

As before, some wicket resources changed, so a gateway restart is required after upgrade if a prior version was installed. (New configuration fields for extra RTU framing time.)

{ Removing links to prior RCs. }

Another pair of release candidates. I was playing with an Automation Direct P1000’s RS485 port this past week and discovered another CRC bug. ):

In my prior testing, I was using registers that were steadily counting. The bug I found was that a CRC with bit 15 set was erroneously sign-extended to 32 bits, and then rejected. But the register was changing fast enough the half that were getting through were hiding the half that were being discarded.

RTU must not be very popular with any of y’all, as no-one else noticed. /:

Anyways:

For v7.9: here.

For v8.1 here.

Edit: Darn it! Server mode had the same problem.
Edit2: Removed links. See below.

1 Like

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