]> git.basschouten.com Git - openhab-addons.git/blob
f5c99731471c88c2810c7c3a21938d001ca2333a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.transport.modbus.test;
14
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.*;
19
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;
39
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;
59
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;
67
68 /**
69  * @author Sami Salonen - Initial contribution
70  */
71 public class SmokeTest extends IntegrationTestSupport {
72
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();
78     static {
79         try {
80             Socket.setSocketImplFactory(socketSpy);
81         } catch (IOException e) {
82             fail("Could not install socket spy in SmokeTest");
83         }
84     }
85
86     /**
87      * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI
88      *
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
91      *
92      * @return
93      */
94     private boolean isRunningInCI() {
95         return "true".equals(System.getenv("CI")) || StringUtils.isNotBlank(System.getenv("JENKINS_HOME"));
96     }
97
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));
104         }
105     }
106
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)));
112         }
113     }
114
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)));
120         }
121     }
122
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)));
128         }
129     }
130
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)));
136         }
137     }
138
139     @BeforeEach
140     public void setUpSocketSpy() throws IOException {
141         socketSpy.sockets.clear();
142     }
143
144     /**
145      * Test handling of slave error responses. In this case, error code = 2, illegal data address, since no data.
146      *
147      * @throws Exception
148      */
149     @Test
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();
162                     }, failure -> {
163                         errorCount.incrementAndGet();
164                         lastError.set(failure.getCause());
165                         callbackCalled.countDown();
166                     });
167             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
168
169             assertThat(okCount.get(), is(equalTo(0)));
170             assertThat(errorCount.get(), is(equalTo(1)));
171             assertTrue(lastError.get() instanceof ModbusSlaveErrorResponseException, lastError.toString());
172         }
173     }
174
175     /**
176      * Test handling of connection error responses.
177      *
178      * @throws Exception
179      */
180     @Test
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
183         // timeout
184         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("10.255.255.1", 9999);
185         EndpointPoolConfiguration configuration = new EndpointPoolConfiguration();
186         configuration.setConnectTimeoutMillis(100);
187
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,
193                 configuration)) {
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();
199                     }, failure -> {
200                         errorCount.incrementAndGet();
201                         lastError.set(failure.getCause());
202                         callbackCalled.countDown();
203                     });
204             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
205
206             assertThat(okCount.get(), is(equalTo(0)));
207             assertThat(errorCount.get(), is(equalTo(1)));
208             assertTrue(lastError.get() instanceof ModbusConnectionException, lastError.toString());
209         }
210     }
211
212     /**
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)
215      *
216      * @throws Exception
217      */
218     @Test
219     public void testIOError() throws Exception {
220         artificialServerWait = 60000;
221         ModbusSlaveEndpoint endpoint = getEndpoint();
222
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();
233                     }, failure -> {
234                         errorCount.incrementAndGet();
235                         lastError.set(failure.getCause());
236                         callbackCalled.countDown();
237                     });
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());
242         }
243     }
244
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))));
248         generateData();
249         ModbusSlaveEndpoint endpoint = getEndpoint();
250
251         AtomicInteger unexpectedCount = new AtomicInteger();
252         CountDownLatch callbackCalled = new CountDownLatch(1);
253         AtomicReference<Object> lastData = new AtomicReference<>();
254
255         final int offset = 1;
256
257         try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) {
258             comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, functionCode, offset, count, 1),
259                     result -> {
260                         Optional<@NonNull BitArray> bitsOptional = result.getBits();
261                         if (bitsOptional.isPresent()) {
262                             lastData.set(bitsOptional.get());
263                         } else {
264                             unexpectedCount.incrementAndGet();
265                         }
266                         callbackCalled.countDown();
267                     }, failure -> {
268                         unexpectedCount.incrementAndGet();
269                         callbackCalled.countDown();
270                     });
271             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
272
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);
279             } else {
280                 testCoilValues(bits, offset);
281             }
282         }
283     }
284
285     @Test
286     public void testOneOffReadWithDiscrete1() throws Exception {
287         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 1);
288     }
289
290     @Test
291     public void testOneOffReadWithDiscrete7() throws Exception {
292         // less than byte
293         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 7);
294     }
295
296     @Test
297     public void testOneOffReadWithDiscrete8() throws Exception {
298         // exactly one byte
299         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 8);
300     }
301
302     @Test
303     public void testOneOffReadWithDiscrete13() throws Exception {
304         // larger than byte, less than word (16 bit)
305         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 13);
306     }
307
308     @Test
309     public void testOneOffReadWithDiscrete18() throws Exception {
310         // larger than word (16 bit)
311         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 18);
312     }
313
314     @Test
315     public void testOneOffReadWithCoils1() throws Exception {
316         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 1);
317     }
318
319     @Test
320     public void testOneOffReadWithCoils7() throws Exception {
321         // less than byte
322         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 7);
323     }
324
325     @Test
326     public void testOneOffReadWithCoils8() throws Exception {
327         // exactly one byte
328         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 8);
329     }
330
331     @Test
332     public void testOneOffReadWithCoils13() throws Exception {
333         // larger than byte, less than word (16 bit)
334         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 13);
335     }
336
337     @Test
338     public void testOneOffReadWithCoils18() throws Exception {
339         // larger than word (16 bit)
340         testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 18);
341     }
342
343     /**
344      *
345      * @throws Exception
346      */
347     @Test
348     public void testOneOffReadWithHolding() throws Exception {
349         generateData();
350         ModbusSlaveEndpoint endpoint = getEndpoint();
351
352         AtomicInteger unexpectedCount = new AtomicInteger();
353         CountDownLatch callbackCalled = new CountDownLatch(1);
354         AtomicReference<Object> lastData = new AtomicReference<>();
355
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());
362                         } else {
363                             unexpectedCount.incrementAndGet();
364                         }
365                         callbackCalled.countDown();
366                     }, failure -> {
367                         unexpectedCount.incrementAndGet();
368                         callbackCalled.countDown();
369                     });
370             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
371
372             assertThat(unexpectedCount.get(), is(equalTo(0)));
373             ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get();
374             assertThat(registers.size(), is(equalTo(15)));
375             testHoldingValues(registers, 1);
376         }
377     }
378
379     /**
380      *
381      * @throws Exception
382      */
383     @Test
384     public void testOneOffReadWithInput() throws Exception {
385         generateData();
386         ModbusSlaveEndpoint endpoint = getEndpoint();
387
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());
397                         } else {
398                             unexpectedCount.incrementAndGet();
399                         }
400                         callbackCalled.countDown();
401                     }, failure -> {
402                         unexpectedCount.incrementAndGet();
403                         callbackCalled.countDown();
404                     });
405             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
406
407             assertThat(unexpectedCount.get(), is(equalTo(0)));
408             ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get();
409             assertThat(registers.size(), is(equalTo(15)));
410             testInputValues(registers, 1);
411         }
412     }
413
414     /**
415      *
416      * @throws Exception
417      */
418     @Test
419     public void testOneOffWriteMultipleCoil() throws Exception {
420         LoggerFactory.getLogger(this.getClass()).error("STARTING MULTIPLE");
421         generateData();
422         ModbusSlaveEndpoint endpoint = getEndpoint();
423
424         AtomicInteger unexpectedCount = new AtomicInteger();
425         AtomicReference<Object> lastData = new AtomicReference<>();
426
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());
431             }, failure -> {
432                 unexpectedCount.incrementAndGet();
433             });
434             waitForAssert(() -> {
435                 assertThat(unexpectedCount.get(), is(equalTo(0)));
436                 assertThat(lastData.get(), is(notNullValue()));
437
438                 ModbusResponse response = (ModbusResponse) lastData.get();
439                 assertThat(response.getFunctionCode(), is(equalTo(15)));
440
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)));
449             }, 6000, 10);
450         }
451         LoggerFactory.getLogger(this.getClass()).error("ENDINGMULTIPLE");
452     }
453
454     /**
455      * Write is out-of-bounds, slave should return error
456      *
457      * @throws Exception
458      */
459     @Test
460     public void testOneOffWriteMultipleCoilError() throws Exception {
461         generateData();
462         ModbusSlaveEndpoint endpoint = getEndpoint();
463
464         AtomicInteger unexpectedCount = new AtomicInteger();
465         CountDownLatch callbackCalled = new CountDownLatch(1);
466         AtomicReference<Exception> lastError = new AtomicReference<>();
467
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();
473             }, failure -> {
474                 lastError.set(failure.getCause());
475                 callbackCalled.countDown();
476             });
477             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
478
479             assertThat(unexpectedCount.get(), is(equalTo(0)));
480             assertTrue(lastError.get() instanceof ModbusSlaveErrorResponseException, lastError.toString());
481
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)));
490         }
491     }
492
493     /**
494      *
495      * @throws Exception
496      */
497     @Test
498     public void testOneOffWriteSingleCoil() throws Exception {
499         generateData();
500         ModbusSlaveEndpoint endpoint = getEndpoint();
501
502         AtomicInteger unexpectedCount = new AtomicInteger();
503         CountDownLatch callbackCalled = new CountDownLatch(1);
504         AtomicReference<Object> lastData = new AtomicReference<>();
505
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();
511             }, failure -> {
512                 unexpectedCount.incrementAndGet();
513                 callbackCalled.countDown();
514             });
515             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
516
517             assertThat(unexpectedCount.get(), is(equalTo(0)));
518             ModbusResponse response = (ModbusResponse) lastData.get();
519             assertThat(response.getFunctionCode(), is(equalTo(5)));
520
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)));
526         }
527     }
528
529     /**
530      *
531      * Write is out-of-bounds, slave should return error
532      *
533      * @throws Exception
534      */
535     @Test
536     public void testOneOffWriteSingleCoilError() throws Exception {
537         generateData();
538         ModbusSlaveEndpoint endpoint = getEndpoint();
539
540         AtomicInteger unexpectedCount = new AtomicInteger();
541         CountDownLatch callbackCalled = new CountDownLatch(1);
542         AtomicReference<Exception> lastError = new AtomicReference<>();
543
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),
547                     result -> {
548                         unexpectedCount.incrementAndGet();
549                         callbackCalled.countDown();
550                     }, failure -> {
551                         lastError.set(failure.getCause());
552                         callbackCalled.countDown();
553                     });
554             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
555
556             assertThat(unexpectedCount.get(), is(equalTo(0)));
557             assertTrue(lastError.get() instanceof ModbusSlaveErrorResponseException, lastError.toString());
558
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)));
564         }
565     }
566
567     /**
568      * Testing regular polling of coils
569      *
570      * Amount of requests is timed, and average poll period is checked
571      *
572      * @throws Exception
573      */
574     @Test
575     public void testRegularReadEvery150msWithCoil() throws Exception {
576         generateData();
577         ModbusSlaveEndpoint endpoint = getEndpoint();
578
579         AtomicInteger unexpectedCount = new AtomicInteger();
580         CountDownLatch callbackCalled = new CountDownLatch(5);
581         AtomicInteger dataReceived = new AtomicInteger();
582
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,
587                     result -> {
588                         Optional<@NonNull BitArray> bitsOptional = result.getBits();
589                         if (bitsOptional.isPresent()) {
590                             BitArray bits = bitsOptional.get();
591                             dataReceived.incrementAndGet();
592                             try {
593                                 assertThat(bits.size(), is(equalTo(15)));
594                                 testCoilValues(bits, 1);
595                             } catch (AssertionError e) {
596                                 unexpectedCount.incrementAndGet();
597                             }
598                         } else {
599                             unexpectedCount.incrementAndGet();
600                         }
601                         callbackCalled.countDown();
602                     }, failure -> {
603                         unexpectedCount.incrementAndGet();
604                         callbackCalled.countDown();
605                     });
606             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
607
608             long end = System.currentTimeMillis();
609             assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
610         }
611     }
612
613     /**
614      * Testing regular polling of holding registers
615      *
616      * Amount of requests is timed, and average poll period is checked
617      *
618      * @throws Exception
619      */
620     @Test
621     public void testRegularReadEvery150msWithHolding() throws Exception {
622         generateData();
623         ModbusSlaveEndpoint endpoint = getEndpoint();
624
625         AtomicInteger unexpectedCount = new AtomicInteger();
626         CountDownLatch callbackCalled = new CountDownLatch(5);
627         AtomicInteger dataReceived = new AtomicInteger();
628
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();
637                             try {
638                                 assertThat(registers.size(), is(equalTo(15)));
639                                 testHoldingValues(registers, 1);
640                             } catch (AssertionError e) {
641                                 unexpectedCount.incrementAndGet();
642                             }
643                         } else {
644                             unexpectedCount.incrementAndGet();
645                         }
646                         callbackCalled.countDown();
647                     }, failure -> {
648                         unexpectedCount.incrementAndGet();
649                         callbackCalled.countDown();
650                     });
651             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
652             long end = System.currentTimeMillis();
653             assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
654         }
655     }
656
657     @Test
658     public void testRegularReadFirstErrorThenOK() throws Exception {
659         generateData();
660         ModbusSlaveEndpoint endpoint = getEndpoint();
661
662         AtomicInteger unexpectedCount = new AtomicInteger();
663         CountDownLatch callbackCalled = new CountDownLatch(5);
664         AtomicInteger dataReceived = new AtomicInteger();
665
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();
674                             try {
675                                 assertThat(registers.size(), is(equalTo(15)));
676                                 testHoldingValues(registers, 1);
677                             } catch (AssertionError e) {
678                                 unexpectedCount.incrementAndGet();
679                             }
680
681                         } else {
682                             unexpectedCount.incrementAndGet();
683                         }
684                         callbackCalled.countDown();
685                     }, failure -> {
686                         unexpectedCount.incrementAndGet();
687                         callbackCalled.countDown();
688                     });
689             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
690             long end = System.currentTimeMillis();
691             assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
692         }
693     }
694
695     /**
696      *
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
704      */
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);
710
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,
715                 String.format(
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));
719     }
720
721     @Test
722     public void testUnregisterPollingOnClose() throws Exception {
723         ModbusSlaveEndpoint endpoint = getEndpoint();
724
725         AtomicInteger unexpectedCount = new AtomicInteger();
726         AtomicInteger errorCount = new AtomicInteger();
727         CountDownLatch successfulCountDownLatch = new CountDownLatch(3);
728         AtomicInteger expectedReceived = new AtomicInteger();
729
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();
738                         } else {
739                             // bits
740                             unexpectedCount.incrementAndGet();
741                         }
742                     }, failure -> {
743                         if (spi.getDigitalInCount() > 0) {
744                             // No errors expected after server filled with data
745                             unexpectedCount.incrementAndGet();
746                         } else {
747                             expectedReceived.incrementAndGet();
748                             errorCount.incrementAndGet();
749                             generateData();
750                             successfulCountDownLatch.countDown();
751                         }
752                     });
753             // Wait for N successful responses before proceeding with assertions of poll rate
754             assertTrue(successfulCountDownLatch.await(60, TimeUnit.SECONDS));
755
756             long end = System.currentTimeMillis();
757             assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600);
758
759             // wait some more and ensure nothing comes back
760             Thread.sleep(500);
761             assertThat(unexpectedCount.get(), is(equalTo(0)));
762         }
763     }
764
765     @Test
766     public void testUnregisterPollingExplicit() throws Exception {
767         ModbusSlaveEndpoint endpoint = getEndpoint();
768
769         AtomicInteger unexpectedCount = new AtomicInteger();
770         AtomicInteger errorCount = new AtomicInteger();
771         CountDownLatch callbackCalled = new CountDownLatch(3);
772         AtomicInteger expectedReceived = new AtomicInteger();
773
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();
781                         } else {
782                             // bits
783                             unexpectedCount.incrementAndGet();
784                         }
785                         callbackCalled.countDown();
786                     }, failure -> {
787                         if (spi.getDigitalInCount() > 0) {
788                             // No errors expected after server filled with data
789                             unexpectedCount.incrementAndGet();
790                         } else {
791                             expectedReceived.incrementAndGet();
792                             errorCount.incrementAndGet();
793                             generateData();
794                         }
795                     });
796             assertTrue(callbackCalled.await(60, TimeUnit.SECONDS));
797             long end = System.currentTimeMillis();
798             assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600);
799
800             // Explicitly unregister the regular poll
801             comms.unregisterRegularPoll(task);
802
803             // wait some more and ensure nothing comes back
804             Thread.sleep(500);
805             assertThat(unexpectedCount.get(), is(equalTo(0)));
806         }
807     }
808
809     @SuppressWarnings("null")
810     @Test
811     public void testPoolConfigurationWithoutListener() throws Exception {
812         EndpointPoolConfiguration defaultConfig = modbusManager.getEndpointPoolConfiguration(getEndpoint());
813         assertThat(defaultConfig, is(notNullValue()));
814
815         EndpointPoolConfiguration newConfig = new EndpointPoolConfiguration();
816         newConfig.setConnectMaxTries(5);
817         try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(getEndpoint(),
818                 newConfig)) {
819             // Sets configuration for the endpoint implicitly
820         }
821
822         assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()).getConnectMaxTries(), is(equalTo(5)));
823         assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(not(equalTo(defaultConfig))));
824
825         // Reset config
826         try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(getEndpoint(), null)) {
827             // Sets configuration for the endpoint implicitly
828         }
829         // Should match the default
830         assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(equalTo(defaultConfig)));
831     }
832
833     @Test
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");
839
840         // Generate server data
841         generateData();
842
843         EndpointPoolConfiguration config = new EndpointPoolConfiguration();
844         config.setReconnectAfterMillis(9_000_000);
845
846         // 1. capture open connections at this point
847         long openSocketsBefore = getNumberOfOpenClients(socketSpy);
848         assertThat(openSocketsBefore, is(equalTo(0L)));
849
850         // 2. make poll, binding opens the tcp connection
851         try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, config)) {
852             {
853                 CountDownLatch latch = new CountDownLatch(1);
854                 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1),
855                         response -> {
856                             latch.countDown();
857                         }, failure -> {
858                             latch.countDown();
859                         });
860                 assertTrue(latch.await(60, TimeUnit.SECONDS));
861             }
862             waitForAssert(() -> {
863                 // 3. ensure one open connection
864                 long openSocketsAfter = getNumberOfOpenClients(socketSpy);
865                 assertThat(openSocketsAfter, is(equalTo(1L)));
866             });
867             try (ModbusCommunicationInterface comms2 = modbusManager.newModbusCommunicationInterface(endpoint,
868                     config)) {
869                 {
870                     CountDownLatch latch = new CountDownLatch(1);
871                     comms.submitOneTimePoll(
872                             new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1), response -> {
873                                 latch.countDown();
874                             }, failure -> {
875                                 latch.countDown();
876                             });
877                     assertTrue(latch.await(60, TimeUnit.SECONDS));
878                 }
879                 assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L)));
880                 // wait for moment (to check that no connections are closed)
881                 Thread.sleep(1000);
882                 // no more than 1 connection, even though requests are going through
883                 assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L)));
884             }
885             Thread.sleep(1000);
886             // Still one connection open even after closing second connection
887             assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L)));
888
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)));
895         });
896     }
897
898     @Test
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");
904
905         // Generate server data
906         generateData();
907
908         EndpointPoolConfiguration config = new EndpointPoolConfiguration();
909         config.setReconnectAfterMillis(2_000);
910
911         // 1. capture open connections at this point
912         long openSocketsBefore = getNumberOfOpenClients(socketSpy);
913         assertThat(openSocketsBefore, is(equalTo(0L)));
914
915         // 2. make poll, binding opens the tcp connection
916         try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, config)) {
917             {
918                 CountDownLatch latch = new CountDownLatch(1);
919                 comms.submitOneTimePoll(new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1),
920                         response -> {
921                             latch.countDown();
922                         }, failure -> {
923                             latch.countDown();
924                         });
925                 assertTrue(latch.await(60, TimeUnit.SECONDS));
926             }
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)));
932             });
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)));
939             }, 60_000, 50);
940
941         }
942     }
943
944     private long getNumberOfOpenClients(SpyingSocketFactory socketSpy) {
945         final InetAddress testServerAddress;
946         try {
947             testServerAddress = localAddress();
948         } catch (UnknownHostException e) {
949             throw new RuntimeException(e);
950         }
951         return socketSpy.sockets.stream().filter(this::isConnectedToTestServer).count();
952     }
953
954     /**
955      * Spy all sockets that are created
956      *
957      * @author Sami Salonen
958      *
959      */
960     private static class SpyingSocketFactory implements SocketImplFactory {
961
962         Queue<SocketImpl> sockets = new ConcurrentLinkedQueue<SocketImpl>();
963
964         @Override
965         public SocketImpl createSocketImpl() {
966             SocketImpl socket = newSocksSocketImpl();
967             sockets.add(socket);
968             return socket;
969         }
970     }
971
972     private static SocketImpl newSocksSocketImpl() {
973         try {
974             Class<?> socksSocketImplClass = Class.forName("java.net.SocksSocketImpl");
975             Class<?> socketImplClass = SocketImpl.class;
976
977             // // For Debugging
978             // for (Method method : socketImplClass.getDeclaredMethods()) {
979             // LoggerFactory.getLogger("foobar")
980             // .error("SocketImpl." + method.getName() + Arrays.toString(method.getParameters()));
981             // }
982             // for (Constructor constructor : socketImplClass.getDeclaredConstructors()) {
983             // LoggerFactory.getLogger("foobar")
984             // .error("SocketImpl." + constructor.getName() + Arrays.toString(constructor.getParameters()));
985             // }
986             // for (Method method : socksSocketImplClass.getDeclaredMethods()) {
987             // LoggerFactory.getLogger("foobar")
988             // .error("SocksSocketImpl." + method.getName() + Arrays.toString(method.getParameters()));
989             // }
990             // for (Constructor constructor : socksSocketImplClass.getDeclaredConstructors()) {
991             // LoggerFactory.getLogger("foobar").error(
992             // "SocksSocketImpl." + constructor.getName() + Arrays.toString(constructor.getParameters()));
993             // }
994
995             try {
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",
1003                         boolean.class);
1004                 socketImplCreateMethod.setAccessible(true);
1005                 Object socketImpl = socketImplCreateMethod.invoke(/* null since we deal with static method */ null,
1006                         /* server */false);
1007
1008                 Constructor<?> socksSocketImplConstructor = socksSocketImplClass
1009                         .getDeclaredConstructor(socketImplClass);
1010                 socksSocketImplConstructor.setAccessible(true);
1011                 return (SocketImpl) socksSocketImplConstructor.newInstance(socketImpl);
1012             }
1013         } catch (Exception e) {
1014             throw new RuntimeException(e);
1015         }
1016     }
1017
1018     private boolean isConnectedToTestServer(SocketImpl impl) {
1019         final InetAddress testServerAddress;
1020         try {
1021             testServerAddress = localAddress();
1022         } catch (UnknownHostException e) {
1023             throw new RuntimeException(e);
1024         }
1025
1026         final int port;
1027         boolean connected = true;
1028         final InetAddress address;
1029         try {
1030             Method getPort = SocketImpl.class.getDeclaredMethod("getPort");
1031             getPort.setAccessible(true);
1032             port = (int) getPort.invoke(impl);
1033
1034             Method getInetAddressMethod = SocketImpl.class.getDeclaredMethod("getInetAddress");
1035             getInetAddressMethod.setAccessible(true);
1036             address = (InetAddress) getInetAddressMethod.invoke(impl);
1037
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);
1042             try {
1043                 getOption.invoke(impl, StandardSocketOptions.SO_KEEPALIVE);
1044             } catch (InvocationTargetException e) {
1045                 if (e.getTargetException() instanceof IOException) {
1046                     connected = false;
1047                 } else {
1048                     throw e;
1049                 }
1050             }
1051         } catch (InvocationTargetException | SecurityException | IllegalArgumentException | IllegalAccessException
1052                 | NoSuchMethodException e) {
1053             throw new RuntimeException(e);
1054         }
1055         return port == tcpModbusPort && connected && address.equals(testServerAddress);
1056     }
1057 }