Multiple TCP Modbus Slaves
-
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
-
Good points. The class is now called ModbusSlaveSet and has this relevant code:
protected LinkedHashMap<Integer, ProcessImage> processImages = new LinkedHashMap<Integer, ProcessImage>(); public void addProcessImage(ProcessImage processImage) { processImages.put(processImage.getSlaveId(), processImage); } public ProcessImage getProcessImage(int slaveId) { return processImages.get(slaveId); } public Collection<ProcessImage> getProcessImages() { return processImages.values(); } ...
I used a LinkedHashMap in order to preserve the order in which the process images were added in case the querying order is important to the user.
-
hi, just picked up on this thread.... is this some sort of data source that gives mango the ability to listen on port xxx and accept modbus requests? thus making it a TCP slave? think i needs this :)
Neil
-
This forum is for discussions around the Modbus4J product, which is used in Mango, but otherwise no associated. It is possible to get Mango to act as a modbus slave, but not trivial. If you need this, please use the contact page to request a quote.