/**
* 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;
}
}
}