Multiple TCP Modbus Slaves
-
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.