/** * Copyright (c) 2010-2017 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.modbus.internal; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; import java.io.File; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Dictionary; import java.util.Hashtable; import org.apache.commons.lang.NotImplementedException; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.openhab.core.events.EventPublisher; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.types.State; import org.openhab.model.item.binding.BindingConfigParseException; import gnu.io.SerialPort; import net.wimpi.modbus.Modbus; import net.wimpi.modbus.ModbusCoupler; import net.wimpi.modbus.ModbusIOException; import net.wimpi.modbus.io.ModbusTransport; import net.wimpi.modbus.msg.ModbusRequest; import net.wimpi.modbus.net.ModbusSerialListener; import net.wimpi.modbus.net.ModbusTCPListener; import net.wimpi.modbus.net.ModbusUDPListener; import net.wimpi.modbus.net.SerialConnection; import net.wimpi.modbus.net.SerialConnectionFactory; import net.wimpi.modbus.net.TCPSlaveConnection; import net.wimpi.modbus.net.TCPSlaveConnection.ModbusTCPTransportFactory; import net.wimpi.modbus.net.TCPSlaveConnectionFactory; import net.wimpi.modbus.net.UDPSlaveTerminal; import net.wimpi.modbus.net.UDPSlaveTerminal.ModbusUDPTransportFactoryImpl; import net.wimpi.modbus.net.UDPSlaveTerminalFactory; import net.wimpi.modbus.net.UDPTerminal; import net.wimpi.modbus.procimg.SimpleProcessImage; import net.wimpi.modbus.util.AtomicCounter; import net.wimpi.modbus.util.SerialParameters; public class TestCaseSupport { public enum ServerType { TCP, UDP, SERIAL } /** * Servers to test * Serial is system dependent */ public static final ServerType[] TEST_SERVERS = new ServerType[] { ServerType.TCP // ServerType.UDP, // ServerType.SERIAL }; // One can perhaps test SERIAL with https://github.com/freemed/tty0tty // and using those virtual ports? Not the same thing as real serial device of course private static String SERIAL_SERVER_PORT = "/dev/pts/7"; private static String SERIAL_CLIENT_PORT = "/dev/pts/8"; private static SerialParameters SERIAL_PARAMETERS_CLIENT = new SerialParameters(SERIAL_CLIENT_PORT, 115200, SerialPort.FLOWCONTROL_NONE, SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE, Modbus.SERIAL_ENCODING_ASCII, false, 1000); private static SerialParameters SERIAL_PARAMETERS_SERVER = new SerialParameters(SERIAL_SERVER_PORT, SERIAL_PARAMETERS_CLIENT.getBaudRate(), SERIAL_PARAMETERS_CLIENT.getFlowControlIn(), SERIAL_PARAMETERS_CLIENT.getFlowControlOut(), SERIAL_PARAMETERS_CLIENT.getDatabits(), SERIAL_PARAMETERS_CLIENT.getStopbits(), SERIAL_PARAMETERS_CLIENT.getParity(), SERIAL_PARAMETERS_CLIENT.getEncoding(), SERIAL_PARAMETERS_CLIENT.isEcho(), 1000); static { System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace"); System.setProperty("gnu.io.rxtx.SerialPorts", SERIAL_SERVER_PORT + File.pathSeparator + SERIAL_CLIENT_PORT); } /** * Max time to wait for connections/requests from client */ protected int MAX_WAIT_REQUESTS_MILLIS = 1000; /** * The server runs in single thread, only one connection is accepted at a time. * This makes the tests as strict as possible -- connection must be closed. */ private static final int SERVER_THREADS = 1; // "infinity", we execute manually manually protected static long REFRESH_INTERVAL = 1000000L; protected static String SLAVE_NAME = "slave1"; protected static String SLAVE2_NAME = "slave2"; protected static int SLAVE_UNIT_ID = 1; private static AtomicCounter udpServerIndex = new AtomicCounter(0); @Mock protected EventPublisher eventPublisher; @Spy protected TCPSlaveConnectionFactory tcpConnectionFactory = new TCPSlaveConnectionFactoryImpl(); @Spy protected UDPSlaveTerminalFactory udpTerminalFactory = new UDPSlaveTerminalFactoryImpl(); @Spy protected SerialConnectionFactory serialConnectionFactory = new SerialConnectionFactoryImpl(); protected ResultCaptor<ModbusRequest> modbustRequestCaptor; protected ModbusBinding binding; protected ModbusTCPListener tcpListener; protected ModbusUDPListener udpListener; protected ModbusSerialListener serialListener; protected SimpleProcessImage spi; protected int tcpModbusPort = -1; protected int udpModbusPort = -1; protected ServerType serverType = ServerType.TCP; protected long artificialServerWait = 0; private Thread serialServerThread = new Thread("ModbusBindingTestsSerialServer") { @Override public void run() { serialListener = new ModbusSerialListener(SERIAL_PARAMETERS_SERVER); }; }; protected static InetAddress localAddress() throws UnknownHostException { return InetAddress.getLocalHost(); } protected static Dictionary<String, Object> addSlave(Dictionary<String, Object> config, ServerType serverType, String connection, String slaveName, String type, String valuetype, int slaveId, int start, int length) { /** * Add a modbus slave to config */ putSlaveConfigParameter(config, serverType, slaveName, "connection", connection); putSlaveConfigParameter(config, serverType, slaveName, "id", String.valueOf(slaveId)); putSlaveConfigParameter(config, serverType, slaveName, "type", type); if (valuetype != null) { putSlaveConfigParameter(config, serverType, slaveName, "valuetype", valuetype); } putSlaveConfigParameter(config, serverType, slaveName, "start", String.valueOf(start)); putSlaveConfigParameter(config, serverType, slaveName, "length", String.valueOf(length)); return config; } /** * Add slave to config using configured serverType */ protected Dictionary<String, Object> addSlave(Dictionary<String, Object> config, String slaveName, String type, String valuetype, int start, int length) throws UnknownHostException { String connection; if (ServerType.TCP.equals(serverType)) { int port = tcpModbusPort; connection = String.format("%s:%d", localAddress().getHostAddress(), port); } else if (ServerType.UDP.equals(serverType)) { int port = udpModbusPort; connection = String.format("%s:%d", localAddress().getHostAddress(), port); } else if (ServerType.SERIAL.equals(serverType)) { connection = String.format("%s:%d:%d:%s:%s:%s", SERIAL_PARAMETERS_CLIENT.getPortName(), SERIAL_PARAMETERS_CLIENT.getBaudRate(), SERIAL_PARAMETERS_CLIENT.getDatabits(), SERIAL_PARAMETERS_CLIENT.getParityString(), SERIAL_PARAMETERS_CLIENT.getStopbitsString(), SERIAL_PARAMETERS_CLIENT.getEncoding()); } else { throw new NotImplementedException(); } return addSlave(config, serverType, connection, slaveName, type, valuetype, 1, start, length); } protected static void putSlaveConfigParameter(Dictionary<String, Object> config, ServerType serverType, String slaveName, String paramName, String paramValue) { String protocol = null; if (ServerType.TCP.equals(serverType)) { protocol = "tcp"; } else if (ServerType.UDP.equals(serverType)) { protocol = "udp"; } else if (ServerType.SERIAL.equals(serverType)) { protocol = "serial"; } config.put(String.format("%s.%s.%s", protocol, slaveName, paramName), paramValue); } protected static Dictionary<String, Object> newLongPollBindingConfig() { Dictionary<String, Object> config = new Hashtable<>(); config.put("poll", String.valueOf(REFRESH_INTERVAL)); return config; } protected void configureSwitchItemBinding(int items, String slaveName, int itemOffset, String itemPrefix, State initialState) throws BindingConfigParseException { Assert.assertEquals(REFRESH_INTERVAL, binding.getRefreshInterval()); final ModbusGenericBindingProvider provider = new ModbusGenericBindingProvider(); for (int itemIndex = itemOffset; itemIndex < items + itemOffset; itemIndex++) { SwitchItem item = new SwitchItem(String.format("%sItem%d", itemPrefix, itemIndex + 1)); if (initialState != null) { item.setState(initialState); } provider.processBindingConfiguration("test.items", item, String.format("%s:%d", slaveName, itemIndex)); } binding.setEventPublisher(eventPublisher); binding.addBindingProvider(provider); } protected void configureSwitchItemBinding(int items, String slaveName, int itemOffset) throws BindingConfigParseException { configureSwitchItemBinding(items, slaveName, itemOffset, "", null); } protected void configureNumberItemBinding(int items, String slaveName, int itemOffset, String itemPrefix, State initialState) throws BindingConfigParseException { Assert.assertEquals(REFRESH_INTERVAL, binding.getRefreshInterval()); final ModbusGenericBindingProvider provider = new ModbusGenericBindingProvider(); for (int itemIndex = itemOffset; itemIndex < items + itemOffset; itemIndex++) { NumberItem item = new NumberItem(String.format("%sItem%d", itemPrefix, itemIndex + 1)); if (initialState != null) { item.setState(initialState); } provider.processBindingConfiguration("test.items", item, String.format("%s:%d", slaveName, itemIndex)); } binding.setEventPublisher(eventPublisher); binding.addBindingProvider(provider); } protected void configureNumberItemBinding(int items, String slaveName, int itemOffset) throws BindingConfigParseException { configureNumberItemBinding(items, slaveName, itemOffset, "", null); } protected void verifyBitItems(String expectedBits, int itemOffset, String itemPrefix) { for (int bitIndex = 0; bitIndex < expectedBits.length(); bitIndex++) { char bit = expectedBits.charAt(bitIndex); State state; if (bit == '0') { state = OnOffType.OFF; } else if (bit == '1') { state = OnOffType.ON; } else { throw new RuntimeException("invalid testdata"); } verify(eventPublisher).postUpdate(String.format("%sItem%d", itemPrefix, bitIndex + itemOffset + 1), state); } } protected void verifyBitItems(String expectedBits, int itemOffset) { verifyBitItems(expectedBits, itemOffset, ""); } protected void verifyBitItems(String expectedBits) { verifyBitItems(expectedBits, 0, ""); } @Before public void setUp() throws Exception { modbustRequestCaptor = new ResultCaptor<>(artificialServerWait); MockitoAnnotations.initMocks(this); startServer(); } @After public void tearDown() { stopServer(); } protected void waitForRequests(int expectedRequestCount) { int sleepMillis = 10; int waited = 0; AssertionError lastError = new AssertionError("Connections not established in time!"); while (waited < MAX_WAIT_REQUESTS_MILLIS) { try { assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(expectedRequestCount))); } catch (AssertionError e) { lastError = e; try { Thread.sleep(sleepMillis); waited += sleepMillis; } catch (InterruptedException e1) { throw new AssertionError("test interrupted"); } continue; } // OK! return; } // Requests not established in time throw lastError; } protected void waitForConnectionsReceived(int expectedConnections) { int sleepMillis = 10; int waited = 0; AssertionError lastError = new AssertionError("Connections not established in time!"); while (waited < MAX_WAIT_REQUESTS_MILLIS) { try { if (ServerType.TCP.equals(serverType)) { verify(tcpConnectionFactory, times(expectedConnections)).create(any(Socket.class)); } else if (ServerType.UDP.equals(serverType)) { // No-op // verify(udpTerminalFactory, times(expectedConnections)).create(any(InetAddress.class), // any(Integer.class)); } else if (ServerType.SERIAL.equals(serverType)) { // No-op } else { throw new NotImplementedException(); } } catch (AssertionError e) { lastError = e; try { Thread.sleep(sleepMillis); waited += sleepMillis; } catch (InterruptedException e1) { throw new AssertionError("test interrupted"); } continue; } // OK! return; } System.err.println("Connections not established in time!"); throw lastError; } private void startServer() throws UnknownHostException, InterruptedException { spi = new SimpleProcessImage(); ModbusCoupler.getReference().setProcessImage(spi); ModbusCoupler.getReference().setMaster(false); ModbusCoupler.getReference().setUnitID(SLAVE_UNIT_ID); if (ServerType.TCP.equals(serverType)) { startTCPServer(); } else if (ServerType.UDP.equals(serverType)) { startUDPServer(); } else if (ServerType.SERIAL.equals(serverType)) { startSerialServer(); } else { throw new NotImplementedException(); } } private void stopServer() { if (ServerType.TCP.equals(serverType)) { tcpListener.stop(); } else if (ServerType.UDP.equals(serverType)) { udpListener.stop(); System.err.println(udpModbusPort); } else if (ServerType.SERIAL.equals(serverType)) { try { serialServerThread.join(100); } catch (InterruptedException e) { e.printStackTrace(); } serialServerThread.interrupt(); } else { throw new NotImplementedException(); } } private void startUDPServer() throws UnknownHostException, InterruptedException { udpListener = new ModbusUDPListener(localAddress(), udpTerminalFactory); for (int portCandidate = 10000 + udpServerIndex.increment(); portCandidate < 20000; portCandidate++) { try { DatagramSocket socket = new DatagramSocket(portCandidate); socket.close(); udpListener.setPort(portCandidate); break; } catch (SocketException e) { continue; } } udpListener.start(); waitForUDPServerStartup(); Assert.assertNotSame(-1, udpModbusPort); Assert.assertNotSame(0, udpModbusPort); } private void waitForUDPServerStartup() throws InterruptedException { // Query server port. It seems to take time (probably due to thread starting) int sleep_millis = 5; int total_try_millis = 10000; // 10sec for (int tries = 0; tries < Math.max(1, total_try_millis / sleep_millis); tries++) { udpModbusPort = udpListener.getLocalPort(); if (udpModbusPort != -1) { break; } Thread.sleep(sleep_millis); } } private void startTCPServer() throws UnknownHostException, InterruptedException { // Serve single user at a time tcpListener = new ModbusTCPListener(SERVER_THREADS, localAddress(), tcpConnectionFactory); // Use any open port tcpListener.setPort(0); tcpListener.start(); // Query server port. It seems to take time (probably due to thread starting) waitForTCPServerStartup(); Assert.assertNotSame(-1, tcpModbusPort); Assert.assertNotSame(0, tcpModbusPort); } private void waitForTCPServerStartup() throws InterruptedException { int sleep_millis = 5; int total_try_millis = 10000; // 10sec for (int tries = 0; tries < Math.max(1, total_try_millis / sleep_millis); tries++) { tcpModbusPort = tcpListener.getLocalPort(); if (tcpModbusPort != -1) { break; } Thread.sleep(sleep_millis); } } private void startSerialServer() throws UnknownHostException, InterruptedException { serialServerThread.start(); Thread.sleep(1000); } /** * Transport factory that spies the created transport items */ public class SpyingModbusTCPTransportFactory extends ModbusTCPTransportFactory { @Override public ModbusTransport create(Socket socket) { ModbusTransport transport = spy(super.create(socket)); // Capture requests produced by our server transport try { doAnswer(modbustRequestCaptor).when(transport).readRequest(); } catch (ModbusIOException e) { throw new RuntimeException(e); } return transport; } } public class SpyingModbusUDPTransportFactory extends ModbusUDPTransportFactoryImpl { @Override public ModbusTransport create(UDPTerminal terminal) { ModbusTransport transport = spy(super.create(terminal)); // Capture requests produced by our server transport try { doAnswer(modbustRequestCaptor).when(transport).readRequest(); } catch (ModbusIOException e) { throw new RuntimeException(e); } return transport; } } public class TCPSlaveConnectionFactoryImpl implements TCPSlaveConnectionFactory { @Override public TCPSlaveConnection create(Socket socket) { return new TCPSlaveConnection(socket, new SpyingModbusTCPTransportFactory()); } } public class UDPSlaveTerminalFactoryImpl implements UDPSlaveTerminalFactory { @Override public UDPSlaveTerminal create(InetAddress interfac, int port) { UDPSlaveTerminal terminal = new UDPSlaveTerminal(interfac, new SpyingModbusUDPTransportFactory(), 1); terminal.setLocalPort(port); return terminal; } } public class SerialConnectionFactoryImpl implements SerialConnectionFactory { @Override public SerialConnection create(SerialParameters parameters) { SerialConnection serialConnection = new SerialConnection(parameters) { @Override public ModbusTransport getModbusTransport() { ModbusTransport transport = spy(super.getModbusTransport()); try { doAnswer(modbustRequestCaptor).when(transport).readRequest(); } catch (ModbusIOException e) { throw new RuntimeException(e); } return transport; } }; return serialConnection; } } }