Multiple TCP Modbus Slaves
-
Hmm, currently no. But it seems like the architecture wouldn't be too difficult to change to support.
-
Matthew,
Based on scanning the modbus4j source, I don't think this will be too much of an issue. I plan on creating my own ModbusFactory and ModbusSlave device. Each request which comes in will be routed to the modbus slave with the matching slave id. I believe I will have to put that routing in my custom IpRequestHandler.
Regards.
-
Will you share your gum with the rest of the class when you're done? :)
-
Sure. I'm in the process of beating it into submission to see if it works.
BTW, can you recommend any good Modbus or BACnet test tools?
Regards.
-
We use Modbus4J and BACnet4J for testing in house. :) But when developing BACnet4J, VTS was used.
-
Matthew,
After a small delay, I'm actually back to trying to implement my proposed solution for multiple tcp modbus slaves sharing a single server socket.
I've been attempting to use a product called: "Modpoll Modbus Master Simulator" (http://www.modbusdriver.com/modpoll.html) to validate my implementation.
This program is a standalone modbus server used to poll a modbus slave. I have written values to input registers and am now attempting to poll the values. I am using the following parameters:
modpoll -m tcp -t 3:hex -p 1502 -a 45 -1 -r 1 10.1.18.101
These parameters will:
use modbus tcp
use 16-bit input register data type with hex display
use port 1502
use slave id 45
will poll only once
will start at register offset 1
with a slave device ip of 10.1.18.101I get an error back from this program no matter what I do.
(Illegal Data Address exception response!)
My questions are these:
Have you tried this program to validate your modbus stack?
If not, can you give it a whirl?From what I've been able to discern is the request is coming in well formed, but I never see the modbus code attempting to create the IpMessageResponse so I am not sure where the problem is.
At the risk of beating a dead horse from another topic, I've traced it about as far up the modbus stack as I can ;-)
Any help would be appreciated.
Fred
-
Can you send your Modbus4J code too?
-
At this point, I've hacked my code back down to the point where I'm creating a TcpSlave object and have written a few token input register values. I am still experiencing the same problem.
I'm down to pretty much just Modbus4J code.
I'm using modpoll on a tcp modbus slave simulator (http://www.plcsimulator.org/) and I can successfully poll input register values. I wanted to see if you had any luck using modpoll on a known, working Modbus4J slave server.
Regards,
Fred -
Yes, i know. But the way that you set up your listener matters.
Have you checked whether your listener is throwing a IllegalDataAddressException? If you're trying to read registers for which a value has not explicitly been set, this exception is thrown. I.e. if you have values in registers 10 to 19, and you try to read <= 9 or >= 20, you'll get an exception.
-
Ok Matthew, you were spot on. I subclassed BasicProcessImage to see what indexes were being requested and sure enough, I was requesting a value at an index for which I had not provided a value.
But this does highlight a lack of understanding on my part about Modbus in general. Let's say I have a few dozen sensors which have float values that I want to expose via Modbus. At any point in time, some of those sensors may be considered "offline" - their current value is not available.
How is this situation handled in Modbus? Especially in light of attempting to request multiple values over an offset range? If the 10th sensor is offline out of a request for 20 sensor values, the request cannot be fulfilled. If each sensor value is requested individually, then the requester can draw the inference the sensor is offline. Requesting each value seems grossly inefficient. How is this typically handled in a more traditional Modbus deployment?
I appreciate any insight you can provide,
Regards.
-
Hi Fred,
The modbus spec doesn't provide any clarity on this, and implementations differ. This is why the Modbus4J master allows the setting of "contiguous reads only".
The choice to throw an exception on the listener side was an arbitrary one. Recognizing it as such, i've added a allowInvalidAddress attribute to the BasicProcessImage class. Changes have been checked into the CVS repo.
Regarding points that go offline, again, it can be a local decision to consider such points as being an invalid address or not. Depends on the equipment.
-
Matthew,
First off, thanks for the quick response.
I looked at what you checked in to CVS. It appears that if the register map in the process image does not have a value, 0 is returned if "allowInvalidAddress" is true. For my sensor population, 0 is as valid as any other value.
Is it possible to just not have the value for the request registers just not included in the response? I've tried to do a wire trace to see if Modbus returns its value with requested registers or is it the case where the response is just a block of data that is expected to be the start of the requested range and all values are then an index off of that range?
Regards,
Fred
-
Not possible. A modbus response is just string of bytes. There's no provision (beyond returning an exception response, which would apply to the entire request) for excluding a value or returning null.
A possible workaround is to return a value that can reliably be interpreted as being invalid, like 0xFFFF or something. This will work with holding/input registers, but not for discretes.
-
In case the "possible workaround" works for you, i've added invalidAddressValue to the class too. Check CVS.
-
Matthew,
Thanks again. I can add that to my slave configuration as an option.
At the risk of going to the well too often - is the default for Modbus slaves little endian? Do slaves typically allow this to be configured?
Regards,
Fred
-
I believe it's big endian. I'd have to look it up to know for sure.
-
Hi Fred,
I actually have a need to support multiple slaves myself. Did you get around to doing this? I'll need it pretty soon, so if not i'll whip it up myself.
-
Matthew,
I did get around to it and it appears to be working.
At the heart of it, I decoupled the ProcessImage object from the TcpSlave. I ended up creating my own custom IpRequestHandler named appropriately CustomIpRequestHandler. The code for it is:
public class CustomIpRequestHandler extends IpRequestHandler { private ProcessImageRegistry _processImageRegistry; public CustomIpRequestHandler(ProcessImageRegistry processImageRegistry) { super(0, null); _processImageRegistry = processImageRegistry; } public MessageResponse handleRequest(MessageRequest req) throws Exception { CustomIpMessageRequest tcpRequest = (CustomIpMessageRequest)req; ModbusRequest request = tcpRequest.getModbusRequest(); ProcessImage processImage = _processImageRegistry.getProcessImage(request.getSlaveId()); if (processImage == null) { return null; } ModbusResponse response = request.handle(processImage); return new IpMessageResponse(response, tcpRequest.getTransactionId()); } }
You've obviously noticed it called the superclass constructor with bogus parameters. My subclass takes a reference to a new object which I called a "ProcessImageRegistry". This class is basically backed by a concurrent hashmap that is keyed by slave id. There is more to my ProcessImageRegistry than what I've described here that is specific to my application.
So then I ripped off your TcpConnectionHandler which was specific to a TcpSlave but now deals with the ProcessImageRegistry.
class TcpConnectionHandler implements Runnable { private final Socket _socket; private TestableTransport _transport; private ListenerConnection _conn; private ProcessImageRegistry _processImageRegistry; private MessagingConnectionListener _exceptionListener = new DefaultExceptionListener(); TcpConnectionHandler(Socket socket, ProcessImageRegistry processImageRegistry) throws ModbusInitException { _socket = socket; _processImageRegistry = processImageRegistry; try { _transport = new TestableTransport(_socket.getInputStream(), _socket.getOutputStream(), "Modbus4J TcpSlave"); } catch (IOException e) { throw new ModbusInitException(e); } } public void run() { CustomIpMessageParser ipMessageParser = new CustomIpMessageParser(); CustomIpRequestHandler ipRequestHandler = new CustomIpRequestHandler(_processImageRegistry); _conn = new ListenerConnection(ipRequestHandler); _conn.addListener(_exceptionListener); try { _conn.start(_transport, ipMessageParser); } catch (IOException e) { _exceptionListener.receivedException(new ModbusInitException(e)); } // Monitor the socket to detect when it gets closed. while (true) { try { _transport.testInputStream(); } catch (IOException e) { break; } try { Thread.sleep(500); } catch (InterruptedException e) {} } _conn.close(); try { _socket.close(); } catch (IOException e) { _exceptionListener.receivedException(new ModbusInitException(e)); } } }
I ended up having to create a CustomIpMessageParser:
public class CustomIpMessageParser extends IpMessageParser { protected MessageResponse parseResponseImpl(ByteQueue queue) throws Exception { return CustomIpMessageResponse.createIpMessageResponse(queue); } protected MessageRequest parseRequestImpl(ByteQueue queue) throws Exception { return CustomIpMessageRequest.createIpMessageRequest(queue); } }
The reason I need to do this what to create a CustomIpMessageRequest and a CustomIpMessageResponse. This was because I needed the transactionId in both the IpMessageRequest and IpMessageResponse but that field was private to both of those classes.
My custom subclasses simply provided a getter for the transactionId.
public class CustomIpMessageRequest extends IpMessageRequest { private int _transactionId; static CustomIpMessageRequest createIpMessageRequest(ByteQueue queue) throws ModbusTransportException { // Remove the IP header int transactionId = ModbusUtils.popShort(queue); int protocolId = ModbusUtils.popShort(queue); if (protocolId != ModbusUtils.IP_PROTOCOL_ID) { throw new ModbusTransportException("Unsupported IP protocol id: "+ protocolId); } ModbusUtils.popShort(queue); // Length, which we don't care about. // Create the modbus response. ModbusRequest request = ModbusRequest.createModbusRequest(queue); return new CustomIpMessageRequest(request, transactionId); } public CustomIpMessageRequest(ModbusRequest modbusRequest, int transactionId) { super(modbusRequest, transactionId); _transactionId = transactionId; } public void isValidResponse(MessageResponse res) throws MessageMismatchException { if (!(res instanceof CustomIpMessageResponse)) { throw new MessageMismatchException("Response is not an CustomIpMessageResponse: "+ res.getClass()); } CustomIpMessageResponse response = (CustomIpMessageResponse)res; if (_transactionId != response.getTransactionId()) { throw new MessageMismatchException("Response transaction id does not match: expected="+ _transactionId + ", received="+ response.getTransactionId()); } getModbusRequest().matches(response.getModbusResponse()); } public int getTransactionId() { return _transactionId; } } public class CustomIpMessageResponse extends IpMessageResponse { private int _transactionId; static CustomIpMessageResponse createIpMessageResponse(ByteQueue queue) throws ModbusTransportException { // Remove the IP header int transactionId = ModbusUtils.popShort(queue); int protocolId = ModbusUtils.popShort(queue); if (protocolId != ModbusUtils.IP_PROTOCOL_ID) { throw new ModbusTransportException("Unsupported IP protocol id: "+ protocolId); } ModbusUtils.popShort(queue); // Length, which we don't care about. // Create the modbus response. ModbusResponse response = ModbusResponse.createModbusResponse(queue); return new CustomIpMessageResponse(response, transactionId); } public CustomIpMessageResponse(ModbusResponse modbusResponse, int transactionId) { super(modbusResponse, transactionId); _transactionId = transactionId; } public int getTransactionId() { return _transactionId; } }
My data model allows for the idea of one or more "spill over" slaves where if the address range for one slave is insufficient to publish out everything the customer wants to see on mobus, they can configure additional slaves to handle a population larger than say 5000 float values (analog inputs) on a single slave.
I hope this helps and if you want some clarification, I'd be happy to elaborate further.
Hey, I did want to ask you a question though. The current modbus4j uses 5 digit register offsets. Is that because 5 digit is the most common? Would supporting 6 digit register offsets be a major issue?
Regards,
Fred
-
Hi Fred,
I took a slightly different approach. I started in ProcessImage by adding the slave id:
public interface ProcessImage { int getSlaveId(); ...
Then, i changed ModbusSlave (possibly creating semantics confusion) as so:
abstract public class ModbusSlave extends Modbus { protected List<ProcessImage> processImages = new ArrayList<ProcessImage>(); public void addProcessImage(ProcessImage processImage) { processImages.add(processImage); } public ProcessImage getProcessImage(int slaveId) { for (ProcessImage processImage : processImages) { if (processImage.getSlaveId() == slaveId) return processImage; } return null; } public List<ProcessImage> getProcessImages() { return processImages; } ...
The rest of the changes pretty much fall out of that. In particular, the request handler now looks like this:
abstract public class BaseRequestHandler implements RequestHandler { protected ModbusSlave slave; public BaseRequestHandler(ModbusSlave slave) { this.slave = slave; } protected ModbusResponse handleRequestImpl(ModbusRequest request) throws ModbusTransportException { int slaveId = request.getSlaveId(); // Check the slave id. if (slaveId == 0) { // Broadcast message. Send to all process images. for (ProcessImage processImage : slave.getProcessImages()) request.handle(processImage); return null; } // Find the process image to which to send. ProcessImage processImage = slave.getProcessImage(slaveId); if (processImage == null) return null; return request.handle(processImage); } }
Will this work for you? I still need to test and check in, but i thought i'd give you a headzup.
-
Matthew,
That should work. I think you're right in that you might have some class name confusion as people may view the slave at the device id level instead of the networking ip/port level as the new semantics are.
Not that I think it would be a big performance hit, but with the process images backed by a hashmap keyed by slave id, you don't have to do a linear search to find the requested process image.
I may keep my current implementation simply because my process image registry is a collection of process images keyed first by slave id and secondly by my concept of a slave configuration (allowing for more than one process image if a "spill over" process image is needed). This class is thread safe and handles the addition/removal of process images by my slave objects. That said, I think this logic could easily be moved into the ModbusSlave class where if necessary I can create my own implementation.
So the answer to your question is yes with the reservation on the class naming as you pointed out.
Regards,
Fred