2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.transport.modbus.test;
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.junit.jupiter.api.Assumptions.*;
20 import java.io.IOException;
21 import java.lang.reflect.Constructor;
22 import java.lang.reflect.InvocationTargetException;
23 import java.lang.reflect.Method;
24 import java.net.InetAddress;
25 import java.net.Socket;
26 import java.net.SocketImpl;
27 import java.net.SocketImplFactory;
28 import java.net.SocketOption;
29 import java.net.StandardSocketOptions;
30 import java.net.UnknownHostException;
31 import java.util.BitSet;
32 import java.util.Optional;
33 import java.util.Queue;
34 import java.util.concurrent.ConcurrentLinkedQueue;
35 import java.util.concurrent.CountDownLatch;
36 import java.util.concurrent.TimeUnit;
37 import java.util.concurrent.atomic.AtomicInteger;
38 import java.util.concurrent.atomic.AtomicReference;
40 import org.apache.commons.lang.StringUtils;
41 import org.eclipse.jdt.annotation.NonNull;
42 import org.junit.jupiter.api.BeforeEach;
43 import org.junit.jupiter.api.Test;
44 import org.openhab.io.transport.modbus.BitArray;
45 import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
46 import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
47 import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
48 import org.openhab.io.transport.modbus.ModbusRegisterArray;
49 import org.openhab.io.transport.modbus.ModbusResponse;
50 import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
51 import org.openhab.io.transport.modbus.PollTask;
52 import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration;
53 import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
54 import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
55 import org.openhab.io.transport.modbus.exception.ModbusConnectionException;
56 import org.openhab.io.transport.modbus.exception.ModbusSlaveErrorResponseException;
57 import org.openhab.io.transport.modbus.exception.ModbusSlaveIOException;
58 import org.slf4j.LoggerFactory;
60 import net.wimpi.modbus.msg.ModbusRequest;
61 import net.wimpi.modbus.msg.WriteCoilRequest;
62 import net.wimpi.modbus.msg.WriteMultipleCoilsRequest;
63 import net.wimpi.modbus.procimg.SimpleDigitalIn;
64 import net.wimpi.modbus.procimg.SimpleDigitalOut;
65 import net.wimpi.modbus.procimg.SimpleRegister;
66 import net.wimpi.modbus.util.BitVector;
69 * @author Sami Salonen - Initial contribution
71 public class SmokeTest extends IntegrationTestSupport {
73 private static final int COIL_EVERY_N_TRUE = 2;
74 private static final int DISCRETE_EVERY_N_TRUE = 3;
75 private static final int HOLDING_REGISTER_MULTIPLIER = 1;
76 private static final int INPUT_REGISTER_MULTIPLIER = 10;
77 private static final SpyingSocketFactory socketSpy = new SpyingSocketFactory();
80 Socket.setSocketImplFactory(socketSpy);
81 } catch (IOException e) {
82 fail("Could not install socket spy in SmokeTest");
87 * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI
89 * Travis CI is detected using CI environment variable, see https://docs.travis-ci.com/user/environment-variables/
90 * Jenkins CI is detected using JENKINS_HOME environment variable
94 private boolean isRunningInCI() {
95 return "true".equals(System.getenv("CI")) || StringUtils.isNotBlank(System.getenv("JENKINS_HOME"));
98 private void generateData() {
99 for (int i = 0; i < 100; i++) {
100 spi.addRegister(new SimpleRegister(i * HOLDING_REGISTER_MULTIPLIER));
101 spi.addInputRegister(new SimpleRegister(i * INPUT_REGISTER_MULTIPLIER));
102 spi.addDigitalOut(new SimpleDigitalOut(i % COIL_EVERY_N_TRUE == 0));
103 spi.addDigitalIn(new SimpleDigitalIn(i % DISCRETE_EVERY_N_TRUE == 0));
107 private void testCoilValues(BitArray bits, int offsetInBitArray) {
108 for (int i = 0; i < bits.size(); i++) {
109 boolean expected = (i + offsetInBitArray) % COIL_EVERY_N_TRUE == 0;
110 assertThat(String.format("i=%d, expecting %b, got %b", i, bits.getBit(i), expected), bits.getBit(i),
111 is(equalTo(expected)));
115 private void testDiscreteValues(BitArray bits, int offsetInBitArray) {
116 for (int i = 0; i < bits.size(); i++) {
117 boolean expected = (i + offsetInBitArray) % DISCRETE_EVERY_N_TRUE == 0;
118 assertThat(String.format("i=%d, expecting %b, got %b", i, bits.getBit(i), expected), bits.getBit(i),
119 is(equalTo(expected)));
123 private void testHoldingValues(ModbusRegisterArray registers, int offsetInRegisters) {
124 for (int i = 0; i < registers.size(); i++) {
125 int expected = (i + offsetInRegisters) * HOLDING_REGISTER_MULTIPLIER;
126 assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i), expected),
127 registers.getRegister(i), is(equalTo(expected)));
131 private void testInputValues(ModbusRegisterArray registers, int offsetInRegisters) {
132 for (int i = 0; i < registers.size(); i++) {
133 int expected = (i + offsetInRegisters) * INPUT_REGISTER_MULTIPLIER;
134 assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i), expected),
135 registers.getRegister(i), is(equalTo(expected)));
140 public void setUpSocketSpy() throws IOException {
141 socketSpy.sockets.clear();
145 * Test handling of slave error responses. In this case, error code = 2, illegal data address, since no data.
150 public void testSlaveReadErrorResponse() throws Exception {
151 ModbusSlaveEndpoint endpoint = getEndpoint();
152 AtomicInteger okCount = new AtomicInteger();
153 AtomicInteger errorCount = new AtomicInteger();
154 CountDownLatch callbackCalled = new CountDownLatch(1);
155 AtomicReference<Exception> lastError = new AtomicReference<>();
156 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
157 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
158 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), result -> {
159 assert result.getRegisters().isPresent();
160 okCount.incrementAndGet();
161 callbackCalled.countDown();
163 errorCount.incrementAndGet();
164 lastError.set(failure.getCause());
165 callbackCalled.countDown();
167 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
169 assertThat(okCount.get(), is(equalTo(0)));
170 assertThat(errorCount.get(), is(equalTo(1)));
171 assertTrue(lastError.get() instanceof ModbusSlaveErrorResponseException, lastError.toString());
176 * Test handling of connection error responses.
181 public void testSlaveConnectionError() throws Exception {
182 // In the test we have non-responding slave (see http://stackoverflow.com/a/904609), and we use short connection
184 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("10.255.255.1", 9999);
185 EndpointPoolConfiguration configuration = new EndpointPoolConfiguration();
186 configuration.setConnectTimeoutMillis(100);
188 AtomicInteger okCount = new AtomicInteger();
189 AtomicInteger errorCount = new AtomicInteger();
190 CountDownLatch callbackCalled = new CountDownLatch(1);
191 AtomicReference<Exception> lastError = new AtomicReference<>();
192 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint,
194 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
195 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), result -> {
196 assert result.getRegisters().isPresent();
197 okCount.incrementAndGet();
198 callbackCalled.countDown();
200 errorCount.incrementAndGet();
201 lastError.set(failure.getCause());
202 callbackCalled.countDown();
204 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
206 assertThat(okCount.get(), is(equalTo(0)));
207 assertThat(errorCount.get(), is(equalTo(1)));
208 assertTrue(lastError.get() instanceof ModbusConnectionException, lastError.toString());
213 * Have super slow connection response, eventually resulting as timeout (due to default timeout of 3 s in
214 * net.wimpi.modbus.Modbus.DEFAULT_TIMEOUT)
219 public void testIOError() throws Exception {
220 artificialServerWait = 60000;
221 ModbusSlaveEndpoint endpoint = getEndpoint();
223 AtomicInteger okCount = new AtomicInteger();
224 AtomicInteger errorCount = new AtomicInteger();
225 CountDownLatch callbackCalled = new CountDownLatch(1);
226 AtomicReference<Exception> lastError = new AtomicReference<>();
227 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
228 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
229 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), result -> {
230 assert result.getRegisters().isPresent();
231 okCount.incrementAndGet();
232 callbackCalled.countDown();
234 errorCount.incrementAndGet();
235 lastError.set(failure.getCause());
236 callbackCalled.countDown();
238 assertTrue(callbackCalled.await(15, TimeUnit.SECONDS));
239 assertThat(okCount.get(), is(equalTo(0)));
240 assertThat(lastError.toString(), errorCount.get(), is(equalTo(1)));
241 assertTrue(lastError.get() instanceof ModbusSlaveIOException, lastError.toString());
245 public void testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode functionCode, int count) throws Exception {
246 assertThat(functionCode, is(anyOf(equalTo(ModbusReadFunctionCode.READ_INPUT_DISCRETES),
247 equalTo(ModbusReadFunctionCode.READ_COILS))));
249 ModbusSlaveEndpoint endpoint = getEndpoint();
251 AtomicInteger unexpectedCount = new AtomicInteger();
252 CountDownLatch callbackCalled = new CountDownLatch(1);
253 AtomicReference<Object> lastData = new AtomicReference<>();
255 final int offset = 1;
257 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
258 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, functionCode, offset, count, 1),
260 Optional<@NonNull BitArray> bitsOptional = result.getBits();
261 if (bitsOptional.isPresent()) {
262 lastData.set(bitsOptional.get());
264 unexpectedCount.incrementAndGet();
266 callbackCalled.countDown();
268 unexpectedCount.incrementAndGet();
269 callbackCalled.countDown();
271 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
273 assertThat(unexpectedCount.get(), is(equalTo(0)));
274 BitArray bits = (BitArray) lastData.get();
275 assertThat(bits, notNullValue());
276 assertThat(bits.size(), is(equalTo(count)));
277 if (functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES) {
278 testDiscreteValues(bits, offset);
280 testCoilValues(bits, offset);
286 public void testOneOffReadWithDiscrete1() throws Exception {
287 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 1);
291 public void testOneOffReadWithDiscrete7() throws Exception {
293 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 7);
297 public void testOneOffReadWithDiscrete8() throws Exception {
299 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 8);
303 public void testOneOffReadWithDiscrete13() throws Exception {
304 // larger than byte, less than word (16 bit)
305 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 13);
309 public void testOneOffReadWithDiscrete18() throws Exception {
310 // larger than word (16 bit)
311 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 18);
315 public void testOneOffReadWithCoils1() throws Exception {
316 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 1);
320 public void testOneOffReadWithCoils7() throws Exception {
322 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 7);
326 public void testOneOffReadWithCoils8() throws Exception {
328 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 8);
332 public void testOneOffReadWithCoils13() throws Exception {
333 // larger than byte, less than word (16 bit)
334 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 13);
338 public void testOneOffReadWithCoils18() throws Exception {
339 // larger than word (16 bit)
340 testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 18);
348 public void testOneOffReadWithHolding() throws Exception {
350 ModbusSlaveEndpoint endpoint = getEndpoint();
352 AtomicInteger unexpectedCount = new AtomicInteger();
353 CountDownLatch callbackCalled = new CountDownLatch(1);
354 AtomicReference<Object> lastData = new AtomicReference<>();
356 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
357 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
358 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), result -> {
359 Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters();
360 if (registersOptional.isPresent()) {
361 lastData.set(registersOptional.get());
363 unexpectedCount.incrementAndGet();
365 callbackCalled.countDown();
367 unexpectedCount.incrementAndGet();
368 callbackCalled.countDown();
370 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
372 assertThat(unexpectedCount.get(), is(equalTo(0)));
373 ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get();
374 assertThat(registers.size(), is(equalTo(15)));
375 testHoldingValues(registers, 1);
384 public void testOneOffReadWithInput() throws Exception {
386 ModbusSlaveEndpoint endpoint = getEndpoint();
388 AtomicInteger unexpectedCount = new AtomicInteger();
389 CountDownLatch callbackCalled = new CountDownLatch(1);
390 AtomicReference<Object> lastData = new AtomicReference<>();
391 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
392 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
393 ModbusReadFunctionCode.READ_INPUT_REGISTERS, 1, 15, 1), result -> {
394 Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters();
395 if (registersOptional.isPresent()) {
396 lastData.set(registersOptional.get());
398 unexpectedCount.incrementAndGet();
400 callbackCalled.countDown();
402 unexpectedCount.incrementAndGet();
403 callbackCalled.countDown();
405 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
407 assertThat(unexpectedCount.get(), is(equalTo(0)));
408 ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get();
409 assertThat(registers.size(), is(equalTo(15)));
410 testInputValues(registers, 1);
419 public void testOneOffWriteMultipleCoil() throws Exception {
420 LoggerFactory.getLogger(this.getClass()).error("STARTING MULTIPLE");
422 ModbusSlaveEndpoint endpoint = getEndpoint();
424 AtomicInteger unexpectedCount = new AtomicInteger();
425 AtomicReference<Object> lastData = new AtomicReference<>();
427 BitArray bits = new BitArray(true, true, false, false, true, true);
428 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
429 comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1), result -> {
430 lastData.set(result.getResponse());
432 unexpectedCount.incrementAndGet();
434 waitForAssert(() -> {
435 assertThat(unexpectedCount.get(), is(equalTo(0)));
436 assertThat(lastData.get(), is(notNullValue()));
438 ModbusResponse response = (ModbusResponse) lastData.get();
439 assertThat(response.getFunctionCode(), is(equalTo(15)));
441 assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
442 ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
443 assertThat(request.getFunctionCode(), is(equalTo(15)));
444 assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3)));
445 assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size())));
446 BitVector writeRequestCoils = ((WriteMultipleCoilsRequest) request).getCoils();
447 BitArray writtenBits = new BitArray(BitSet.valueOf(writeRequestCoils.getBytes()), bits.size());
448 assertThat(writtenBits, is(equalTo(bits)));
451 LoggerFactory.getLogger(this.getClass()).error("ENDINGMULTIPLE");
455 * Write is out-of-bounds, slave should return error
460 public void testOneOffWriteMultipleCoilError() throws Exception {
462 ModbusSlaveEndpoint endpoint = getEndpoint();
464 AtomicInteger unexpectedCount = new AtomicInteger();
465 CountDownLatch callbackCalled = new CountDownLatch(1);
466 AtomicReference<Exception> lastError = new AtomicReference<>();
468 BitArray bits = new BitArray(500);
469 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
470 comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1), result -> {
471 unexpectedCount.incrementAndGet();
472 callbackCalled.countDown();
474 lastError.set(failure.getCause());
475 callbackCalled.countDown();
477 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
479 assertThat(unexpectedCount.get(), is(equalTo(0)));
480 assertTrue(lastError.get() instanceof ModbusSlaveErrorResponseException, lastError.toString());
482 assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
483 ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
484 assertThat(request.getFunctionCode(), is(equalTo(15)));
485 assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3)));
486 assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size())));
487 BitVector writeRequestCoils = ((WriteMultipleCoilsRequest) request).getCoils();
488 BitArray writtenBits = new BitArray(BitSet.valueOf(writeRequestCoils.getBytes()), bits.size());
489 assertThat(writtenBits, is(equalTo(bits)));
498 public void testOneOffWriteSingleCoil() throws Exception {
500 ModbusSlaveEndpoint endpoint = getEndpoint();
502 AtomicInteger unexpectedCount = new AtomicInteger();
503 CountDownLatch callbackCalled = new CountDownLatch(1);
504 AtomicReference<Object> lastData = new AtomicReference<>();
506 BitArray bits = new BitArray(true);
507 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
508 comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, false, 1), result -> {
509 lastData.set(result.getResponse());
510 callbackCalled.countDown();
512 unexpectedCount.incrementAndGet();
513 callbackCalled.countDown();
515 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
517 assertThat(unexpectedCount.get(), is(equalTo(0)));
518 ModbusResponse response = (ModbusResponse) lastData.get();
519 assertThat(response.getFunctionCode(), is(equalTo(5)));
521 assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
522 ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
523 assertThat(request.getFunctionCode(), is(equalTo(5)));
524 assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(3)));
525 assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true)));
531 * Write is out-of-bounds, slave should return error
536 public void testOneOffWriteSingleCoilError() throws Exception {
538 ModbusSlaveEndpoint endpoint = getEndpoint();
540 AtomicInteger unexpectedCount = new AtomicInteger();
541 CountDownLatch callbackCalled = new CountDownLatch(1);
542 AtomicReference<Exception> lastError = new AtomicReference<>();
544 BitArray bits = new BitArray(true);
545 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
546 comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 300, bits, false, 1),
548 unexpectedCount.incrementAndGet();
549 callbackCalled.countDown();
551 lastError.set(failure.getCause());
552 callbackCalled.countDown();
554 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
556 assertThat(unexpectedCount.get(), is(equalTo(0)));
557 assertTrue(lastError.get() instanceof ModbusSlaveErrorResponseException, lastError.toString());
559 assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
560 ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
561 assertThat(request.getFunctionCode(), is(equalTo(5)));
562 assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(300)));
563 assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true)));
568 * Testing regular polling of coils
570 * Amount of requests is timed, and average poll period is checked
575 public void testRegularReadEvery150msWithCoil() throws Exception {
577 ModbusSlaveEndpoint endpoint = getEndpoint();
579 AtomicInteger unexpectedCount = new AtomicInteger();
580 CountDownLatch callbackCalled = new CountDownLatch(5);
581 AtomicInteger dataReceived = new AtomicInteger();
583 long start = System.currentTimeMillis();
584 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
585 comms.registerRegularPoll(
586 new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, ModbusReadFunctionCode.READ_COILS, 1, 15, 1), 150, 0,
588 Optional<@NonNull BitArray> bitsOptional = result.getBits();
589 if (bitsOptional.isPresent()) {
590 BitArray bits = bitsOptional.get();
591 dataReceived.incrementAndGet();
593 assertThat(bits.size(), is(equalTo(15)));
594 testCoilValues(bits, 1);
595 } catch (AssertionError e) {
596 unexpectedCount.incrementAndGet();
599 unexpectedCount.incrementAndGet();
601 callbackCalled.countDown();
603 unexpectedCount.incrementAndGet();
604 callbackCalled.countDown();
606 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
608 long end = System.currentTimeMillis();
609 assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
614 * Testing regular polling of holding registers
616 * Amount of requests is timed, and average poll period is checked
621 public void testRegularReadEvery150msWithHolding() throws Exception {
623 ModbusSlaveEndpoint endpoint = getEndpoint();
625 AtomicInteger unexpectedCount = new AtomicInteger();
626 CountDownLatch callbackCalled = new CountDownLatch(5);
627 AtomicInteger dataReceived = new AtomicInteger();
629 long start = System.currentTimeMillis();
630 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
631 comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
632 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 150, 0, result -> {
633 Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters();
634 if (registersOptional.isPresent()) {
635 ModbusRegisterArray registers = registersOptional.get();
636 dataReceived.incrementAndGet();
638 assertThat(registers.size(), is(equalTo(15)));
639 testHoldingValues(registers, 1);
640 } catch (AssertionError e) {
641 unexpectedCount.incrementAndGet();
644 unexpectedCount.incrementAndGet();
646 callbackCalled.countDown();
648 unexpectedCount.incrementAndGet();
649 callbackCalled.countDown();
651 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
652 long end = System.currentTimeMillis();
653 assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
658 public void testRegularReadFirstErrorThenOK() throws Exception {
660 ModbusSlaveEndpoint endpoint = getEndpoint();
662 AtomicInteger unexpectedCount = new AtomicInteger();
663 CountDownLatch callbackCalled = new CountDownLatch(5);
664 AtomicInteger dataReceived = new AtomicInteger();
666 long start = System.currentTimeMillis();
667 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
668 comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
669 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 150, 0, result -> {
670 Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters();
671 if (registersOptional.isPresent()) {
672 ModbusRegisterArray registers = registersOptional.get();
673 dataReceived.incrementAndGet();
675 assertThat(registers.size(), is(equalTo(15)));
676 testHoldingValues(registers, 1);
677 } catch (AssertionError e) {
678 unexpectedCount.incrementAndGet();
682 unexpectedCount.incrementAndGet();
684 callbackCalled.countDown();
686 unexpectedCount.incrementAndGet();
687 callbackCalled.countDown();
689 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
690 long end = System.currentTimeMillis();
691 assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
697 * @param unexpectedCount number of unexpected callback calls
698 * @param callbackCalled number of callback calls (including unexpected)
699 * @param dataReceived number of expected callback calls (onBits or onRegisters)
700 * @param pollStartMillis poll start time in milliepoch
701 * @param expectedPollAverageMin average poll period should be at least greater than this
702 * @param expectedPollAverageMax average poll period less than this
703 * @throws InterruptedException
705 private void assertPollDetails(AtomicInteger unexpectedCount, AtomicInteger expectedCount, long pollStartMillis,
706 long pollEndMillis, int expectedPollAverageMin, int expectedPollAverageMax) throws InterruptedException {
707 int responses = expectedCount.get();
708 assertThat(unexpectedCount.get(), is(equalTo(0)));
709 assertTrue(responses > 1);
711 // Rest of the (timing-sensitive) assertions are not run in CI
712 assumeFalse(isRunningInCI(), "Running in CI! Will not test timing-sensitive details");
713 float averagePollPeriodMillis = ((float) (pollEndMillis - pollStartMillis)) / (responses - 1);
714 assertTrue(averagePollPeriodMillis > expectedPollAverageMin && averagePollPeriodMillis < expectedPollAverageMax,
716 "Measured avarage poll period %f ms (%d responses in %d ms) is not withing expected limits [%d, %d]",
717 averagePollPeriodMillis, responses, pollEndMillis - pollStartMillis, expectedPollAverageMin,
718 expectedPollAverageMax));
722 public void testUnregisterPollingOnClose() throws Exception {
723 ModbusSlaveEndpoint endpoint = getEndpoint();
725 AtomicInteger unexpectedCount = new AtomicInteger();
726 AtomicInteger errorCount = new AtomicInteger();
727 CountDownLatch successfulCountDownLatch = new CountDownLatch(3);
728 AtomicInteger expectedReceived = new AtomicInteger();
730 long start = System.currentTimeMillis();
731 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
732 comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
733 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 200, 0, result -> {
734 Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters();
735 if (registersOptional.isPresent()) {
736 expectedReceived.incrementAndGet();
737 successfulCountDownLatch.countDown();
740 unexpectedCount.incrementAndGet();
743 if (spi.getDigitalInCount() > 0) {
744 // No errors expected after server filled with data
745 unexpectedCount.incrementAndGet();
747 expectedReceived.incrementAndGet();
748 errorCount.incrementAndGet();
750 successfulCountDownLatch.countDown();
753 // Wait for N successful responses before proceeding with assertions of poll rate
754 assertTrue(successfulCountDownLatch.await(60, TimeUnit.SECONDS));
756 long end = System.currentTimeMillis();
757 assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600);
759 // wait some more and ensure nothing comes back
761 assertThat(unexpectedCount.get(), is(equalTo(0)));
766 public void testUnregisterPollingExplicit() throws Exception {
767 ModbusSlaveEndpoint endpoint = getEndpoint();
769 AtomicInteger unexpectedCount = new AtomicInteger();
770 AtomicInteger errorCount = new AtomicInteger();
771 CountDownLatch callbackCalled = new CountDownLatch(3);
772 AtomicInteger expectedReceived = new AtomicInteger();
774 long start = System.currentTimeMillis();
775 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
776 PollTask task = comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID,
777 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 200, 0, result -> {
778 Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters();
779 if (registersOptional.isPresent()) {
780 expectedReceived.incrementAndGet();
783 unexpectedCount.incrementAndGet();
785 callbackCalled.countDown();
787 if (spi.getDigitalInCount() > 0) {
788 // No errors expected after server filled with data
789 unexpectedCount.incrementAndGet();
791 expectedReceived.incrementAndGet();
792 errorCount.incrementAndGet();
796 assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
797 long end = System.currentTimeMillis();
798 assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600);
800 // Explicitly unregister the regular poll
801 comms.unregisterRegularPoll(task);
803 // wait some more and ensure nothing comes back
805 assertThat(unexpectedCount.get(), is(equalTo(0)));
809 @SuppressWarnings("null")
811 public void testPoolConfigurationWithoutListener() throws Exception {
812 EndpointPoolConfiguration defaultConfig = modbusManager.getEndpointPoolConfiguration(getEndpoint());
813 assertThat(defaultConfig, is(notNullValue()));
815 EndpointPoolConfiguration newConfig = new EndpointPoolConfiguration();
816 newConfig.setConnectMaxTries(5);
817 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(getEndpoint(),
819 // Sets configuration for the endpoint implicitly
822 assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()).getConnectMaxTries(), is(equalTo(5)));
823 assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(not(equalTo(defaultConfig))));
826 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(getEndpoint(), null)) {
827 // Sets configuration for the endpoint implicitly
829 // Should match the default
830 assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(equalTo(defaultConfig)));
834 public void testConnectionCloseAfterLastCommunicationInterfaceClosed() throws IllegalArgumentException, Exception {
835 assumeFalse(isRunningInCI(), "Running in CI! Will not test timing-sensitive details");
836 ModbusSlaveEndpoint endpoint = getEndpoint();
837 assumeTrue(endpoint instanceof ModbusTCPSlaveEndpoint,
838 "Connection closing test supported only with TCP slaves");
840 // Generate server data
843 EndpointPoolConfiguration config = new EndpointPoolConfiguration();
844 config.setReconnectAfterMillis(9_000_000);
846 // 1. capture open connections at this point
847 long openSocketsBefore = getNumberOfOpenClients(socketSpy);
848 assertThat(openSocketsBefore, is(equalTo(0L)));
850 // 2. make poll, binding opens the tcp connection
851 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, config)) {
853 CountDownLatch latch = new CountDownLatch(1);
854 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1),
860 assertTrue(latch.await(60, TimeUnit.SECONDS));
862 waitForAssert(() -> {
863 // 3. ensure one open connection
864 long openSocketsAfter = getNumberOfOpenClients(socketSpy);
865 assertThat(openSocketsAfter, is(equalTo(1L)));
867 try (ModbusCommunicationInterface comms2 = modbusManager.newModbusCommunicationInterface(endpoint,
870 CountDownLatch latch = new CountDownLatch(1);
871 comms.submitOneTimePoll(
872 new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1), response -> {
877 assertTrue(latch.await(60, TimeUnit.SECONDS));
879 assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L)));
880 // wait for moment (to check that no connections are closed)
882 // no more than 1 connection, even though requests are going through
883 assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L)));
886 // Still one connection open even after closing second connection
887 assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L)));
889 } // 4. close (the last) comms
890 // ensure that open connections are closed
891 // (despite huge "reconnect after millis")
892 waitForAssert(() -> {
893 long openSocketsAfterClose = getNumberOfOpenClients(socketSpy);
894 assertThat(openSocketsAfterClose, is(equalTo(0L)));
899 public void testConnectionCloseAfterOneOffPoll() throws IllegalArgumentException, Exception {
900 assumeFalse(isRunningInCI(), "Running in CI! Will not test timing-sensitive details");
901 ModbusSlaveEndpoint endpoint = getEndpoint();
902 assumeTrue(endpoint instanceof ModbusTCPSlaveEndpoint,
903 "Connection closing test supported only with TCP slaves");
905 // Generate server data
908 EndpointPoolConfiguration config = new EndpointPoolConfiguration();
909 config.setReconnectAfterMillis(2_000);
911 // 1. capture open connections at this point
912 long openSocketsBefore = getNumberOfOpenClients(socketSpy);
913 assertThat(openSocketsBefore, is(equalTo(0L)));
915 // 2. make poll, binding opens the tcp connection
916 try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, config)) {
918 CountDownLatch latch = new CountDownLatch(1);
919 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1),
925 assertTrue(latch.await(60, TimeUnit.SECONDS));
927 // Right after the poll we should have one connection open
928 waitForAssert(() -> {
929 // 3. ensure one open connection
930 long openSocketsAfter = getNumberOfOpenClients(socketSpy);
931 assertThat(openSocketsAfter, is(equalTo(1L)));
933 // 4. Connection should close itself by the commons pool eviction policy (checking for old idle connection
934 // every now and then)
935 waitForAssert(() -> {
936 // 3. ensure one open connection
937 long openSocketsAfter = getNumberOfOpenClients(socketSpy);
938 assertThat(openSocketsAfter, is(equalTo(0L)));
944 private long getNumberOfOpenClients(SpyingSocketFactory socketSpy) {
945 final InetAddress testServerAddress;
947 testServerAddress = localAddress();
948 } catch (UnknownHostException e) {
949 throw new RuntimeException(e);
951 return socketSpy.sockets.stream().filter(this::isConnectedToTestServer).count();
955 * Spy all sockets that are created
957 * @author Sami Salonen
960 private static class SpyingSocketFactory implements SocketImplFactory {
962 Queue<SocketImpl> sockets = new ConcurrentLinkedQueue<SocketImpl>();
965 public SocketImpl createSocketImpl() {
966 SocketImpl socket = newSocksSocketImpl();
972 private static SocketImpl newSocksSocketImpl() {
974 Class<?> socksSocketImplClass = Class.forName("java.net.SocksSocketImpl");
975 Class<?> socketImplClass = SocketImpl.class;
978 // for (Method method : socketImplClass.getDeclaredMethods()) {
979 // LoggerFactory.getLogger("foobar")
980 // .error("SocketImpl." + method.getName() + Arrays.toString(method.getParameters()));
982 // for (Constructor constructor : socketImplClass.getDeclaredConstructors()) {
983 // LoggerFactory.getLogger("foobar")
984 // .error("SocketImpl." + constructor.getName() + Arrays.toString(constructor.getParameters()));
986 // for (Method method : socksSocketImplClass.getDeclaredMethods()) {
987 // LoggerFactory.getLogger("foobar")
988 // .error("SocksSocketImpl." + method.getName() + Arrays.toString(method.getParameters()));
990 // for (Constructor constructor : socksSocketImplClass.getDeclaredConstructors()) {
991 // LoggerFactory.getLogger("foobar").error(
992 // "SocksSocketImpl." + constructor.getName() + Arrays.toString(constructor.getParameters()));
996 Constructor<?> constructor = socksSocketImplClass.getDeclaredConstructor();
997 constructor.setAccessible(true);
998 return (SocketImpl) constructor.newInstance();
999 } catch (NoSuchMethodException e) {
1000 // Newer Javas (Java 14->) do not have default constructor 'SocksSocketImpl()'
1001 // Instead we use "static SocketImpl.createPlatformSocketImpl" and "SocksSocketImpl(SocketImpl)
1002 Method socketImplCreateMethod = socketImplClass.getDeclaredMethod("createPlatformSocketImpl",
1004 socketImplCreateMethod.setAccessible(true);
1005 Object socketImpl = socketImplCreateMethod.invoke(/* null since we deal with static method */ null,
1008 Constructor<?> socksSocketImplConstructor = socksSocketImplClass
1009 .getDeclaredConstructor(socketImplClass);
1010 socksSocketImplConstructor.setAccessible(true);
1011 return (SocketImpl) socksSocketImplConstructor.newInstance(socketImpl);
1013 } catch (Exception e) {
1014 throw new RuntimeException(e);
1018 private boolean isConnectedToTestServer(SocketImpl impl) {
1019 final InetAddress testServerAddress;
1021 testServerAddress = localAddress();
1022 } catch (UnknownHostException e) {
1023 throw new RuntimeException(e);
1027 boolean connected = true;
1028 final InetAddress address;
1030 Method getPort = SocketImpl.class.getDeclaredMethod("getPort");
1031 getPort.setAccessible(true);
1032 port = (int) getPort.invoke(impl);
1034 Method getInetAddressMethod = SocketImpl.class.getDeclaredMethod("getInetAddress");
1035 getInetAddressMethod.setAccessible(true);
1036 address = (InetAddress) getInetAddressMethod.invoke(impl);
1038 // hacky (but java8-14 compatible) way to know if socket is open
1039 // SocketImpl.getOption throws IOException when socket is closed
1040 Method getOption = SocketImpl.class.getDeclaredMethod("getOption", SocketOption.class);
1041 getOption.setAccessible(true);
1043 getOption.invoke(impl, StandardSocketOptions.SO_KEEPALIVE);
1044 } catch (InvocationTargetException e) {
1045 if (e.getTargetException() instanceof IOException) {
1051 } catch (InvocationTargetException | SecurityException | IllegalArgumentException | IllegalAccessException
1052 | NoSuchMethodException e) {
1053 throw new RuntimeException(e);
1055 return port == tcpModbusPort && connected && address.equals(testServerAddress);