org.openhab.io.transport.modbus.test.SmokeTest.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.io.transport.modbus.test.SmokeTest.java

Source

/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.io.transport.modbus.test;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeFalse;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.openhab.io.transport.modbus.BasicBitArray;
import org.openhab.io.transport.modbus.BasicModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.BasicModbusWriteCoilRequestBlueprint;
import org.openhab.io.transport.modbus.BasicPollTaskImpl;
import org.openhab.io.transport.modbus.BasicWriteTask;
import org.openhab.io.transport.modbus.BitArray;
import org.openhab.io.transport.modbus.ModbusConnectionException;
import org.openhab.io.transport.modbus.ModbusManagerListener;
import org.openhab.io.transport.modbus.ModbusReadCallback;
import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.openhab.io.transport.modbus.ModbusResponse;
import org.openhab.io.transport.modbus.ModbusSlaveErrorResponseException;
import org.openhab.io.transport.modbus.ModbusSlaveIOException;
import org.openhab.io.transport.modbus.ModbusWriteCallback;
import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint;
import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration;
import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
import org.openhab.io.transport.modbus.internal.BitArrayWrappingBitVector;
import org.slf4j.LoggerFactory;

import net.wimpi.modbus.msg.ModbusRequest;
import net.wimpi.modbus.msg.WriteCoilRequest;
import net.wimpi.modbus.msg.WriteMultipleCoilsRequest;
import net.wimpi.modbus.procimg.SimpleDigitalIn;
import net.wimpi.modbus.procimg.SimpleDigitalOut;
import net.wimpi.modbus.procimg.SimpleRegister;

/**
 *
 * @author Sami Salonen
 *
 */
public class SmokeTest extends IntegrationTestSupport {

    private static final int COIL_EVERY_N_TRUE = 2;
    private static final int DISCRETE_EVERY_N_TRUE = 3;
    private static final int HOLDING_REGISTER_MULTIPLIER = 1;
    private static final int INPUT_REGISTER_MULTIPLIER = 10;

    /**
     * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI
     *
     * Travis CI is detected using CI environment variable, see https://docs.travis-ci.com/user/environment-variables/
     * Jenkins CI is detected using JENKINS_HOME environment variable
     *
     * @return
     */
    private boolean isRunningInCI() {
        return "true".equals(System.getenv("CI")) || StringUtils.isNotBlank(System.getenv("JENKINS_HOME"));
    }

    private void generateData() {
        for (int i = 0; i < 100; i++) {
            spi.addRegister(new SimpleRegister(i * HOLDING_REGISTER_MULTIPLIER));
            spi.addInputRegister(new SimpleRegister(i * INPUT_REGISTER_MULTIPLIER));
            spi.addDigitalOut(new SimpleDigitalOut(i % COIL_EVERY_N_TRUE == 0));
            spi.addDigitalIn(new SimpleDigitalIn(i % DISCRETE_EVERY_N_TRUE == 0));
        }
    }

    private void testCoilValues(BitArray bits, int offsetInBitArray) {
        for (int i = 0; i < bits.size(); i++) {
            boolean expected = (i + offsetInBitArray) % COIL_EVERY_N_TRUE == 0;
            assertThat(String.format("i=%d, expecting %b, got %b", i, bits.getBit(i), expected), bits.getBit(i),
                    is(equalTo(expected)));
        }
    }

    private void testDiscreteValues(BitArray bits, int offsetInBitArray) {
        for (int i = 0; i < bits.size(); i++) {
            boolean expected = (i + offsetInBitArray) % DISCRETE_EVERY_N_TRUE == 0;
            assertThat(String.format("i=%d, expecting %b, got %b", i, bits.getBit(i), expected), bits.getBit(i),
                    is(equalTo(expected)));
        }
    }

    private void testHoldingValues(ModbusRegisterArray registers, int offsetInRegisters) {
        for (int i = 0; i < registers.size(); i++) {
            int expected = (i + offsetInRegisters) * HOLDING_REGISTER_MULTIPLIER;
            assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i).toUnsignedShort(),
                    expected), registers.getRegister(i).toUnsignedShort(), is(equalTo(expected)));
        }
    }

    private void testInputValues(ModbusRegisterArray registers, int offsetInRegisters) {
        for (int i = 0; i < registers.size(); i++) {
            int expected = (i + offsetInRegisters) * INPUT_REGISTER_MULTIPLIER;
            assertThat(String.format("i=%d, expecting %d, got %d", i, registers.getRegister(i).toUnsignedShort(),
                    expected), registers.getRegister(i).toUnsignedShort(), is(equalTo(expected)));
        }
    }

    /**
     * Test handling of slave error responses. In this case, error code = 2, illegal data address, since no data.
     *
     * @throws InterruptedException
     */
    @Test
    public void testSlaveReadErrorResponse() throws InterruptedException {
        ModbusSlaveEndpoint endpoint = getEndpoint();
        AtomicInteger okCount = new AtomicInteger();
        AtomicInteger errorCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Exception> lastError = new AtomicReference<>();
        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        okCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        lastError.set(error);
                        errorCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        okCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(5, TimeUnit.SECONDS);
        assertThat(okCount.get(), is(equalTo(0)));
        assertThat(errorCount.get(), is(equalTo(1)));
        assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException);
    }

    /**
     * Test handling of connection error responses.
     *
     * @throws InterruptedException
     */
    @Test
    public void testSlaveConnectionError() throws InterruptedException {
        // In the test we have non-responding slave (see http://stackoverflow.com/a/904609), and we use short connection
        // timeout
        ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("10.255.255.1", 9999);
        EndpointPoolConfiguration configuration = new EndpointPoolConfiguration();
        configuration.setConnectTimeoutMillis(100);
        modbusManager.setEndpointPoolConfiguration(endpoint, configuration);

        AtomicInteger okCount = new AtomicInteger();
        AtomicInteger errorCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Exception> lastError = new AtomicReference<>();
        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        okCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        lastError.set(error);
                        errorCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        okCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(5, TimeUnit.SECONDS);
        assertThat(okCount.get(), is(equalTo(0)));
        assertThat(errorCount.get(), is(equalTo(1)));
        assertTrue(lastError.toString(), lastError.get() instanceof ModbusConnectionException);
    }

    /**
     * Have super slow connection response, eventually resulting as timeout (due to default timeout of 3 s in
     * net.wimpi.modbus.Modbus.DEFAULT_TIMEOUT)
     *
     * @throws InterruptedException
     */
    @Test
    public void testIOError() throws InterruptedException {
        artificialServerWait = 30000;
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger okCount = new AtomicInteger();
        AtomicInteger errorCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Exception> lastError = new AtomicReference<>();
        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        okCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        lastError.set(error);
                        errorCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        okCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(15, TimeUnit.SECONDS);
        assertThat(okCount.get(), is(equalTo(0)));
        assertThat(lastError.toString(), errorCount.get(), is(equalTo(1)));
        assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveIOException);
    }

    /**
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffReadWithCoil() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Object> lastData = new AtomicReference<>();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint,
                new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, ModbusReadFunctionCode.READ_COILS, 1, 15, 1),
                new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        lastData.set(bits);
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(5, TimeUnit.SECONDS);
        assertThat(unexpectedCount.get(), is(equalTo(0)));
        BitArray bits = (BitArray) lastData.get();
        assertThat(bits.size(), is(equalTo(15)));
        testCoilValues(bits, 1);
    }

    /**
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffReadWithDiscrete() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Object> lastData = new AtomicReference<>();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_INPUT_DISCRETES, 1, 15, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        lastData.set(bits);
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(5, TimeUnit.SECONDS);
        assertThat(unexpectedCount.get(), is(equalTo(0)));
        BitArray bits = (BitArray) lastData.get();
        assertThat(bits.size(), is(equalTo(15)));
        testDiscreteValues(bits, 1);
    }

    /**
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffReadWithHolding() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Object> lastData = new AtomicReference<>();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        lastData.set(registers);
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {

                        unexpectedCount.incrementAndGet();

                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(5, TimeUnit.SECONDS);
        assertThat(unexpectedCount.get(), is(equalTo(0)));
        ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get();
        assertThat(registers.size(), is(equalTo(15)));
        testHoldingValues(registers, 1);
    }

    /**
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffReadWithInput() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Object> lastData = new AtomicReference<>();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_INPUT_REGISTERS, 1, 15, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        lastData.set(registers);
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimePoll(task);
        callbackCalled.await(5, TimeUnit.SECONDS);
        assertThat(unexpectedCount.get(), is(equalTo(0)));
        ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get();
        assertThat(registers.size(), is(equalTo(15)));
        testInputValues(registers, 1);
    }

    /**
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffWriteMultipleCoil() throws InterruptedException {
        LoggerFactory.getLogger(this.getClass()).error("STARTING MULTIPLE");
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        AtomicReference<Object> lastData = new AtomicReference<>();

        BitArray bits = new BasicBitArray(true, true, false, false, true, true);
        BasicWriteTask task = new BasicWriteTask(endpoint,
                new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1),
                new ModbusWriteCallback() {

                    @Override
                    public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) {
                        lastData.set(response);
                    }

                    @Override
                    public void onError(ModbusWriteRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                    }
                });
        modbusManager.submitOneTimeWrite(task);
        waitForAssert(() -> {
            assertThat(unexpectedCount.get(), is(equalTo(0)));
            assertThat(lastData.get(), is(notNullValue()));

            ModbusResponse response = (ModbusResponse) lastData.get();
            assertThat(response.getFunctionCode(), is(equalTo(15)));

            assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
            ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
            assertThat(request.getFunctionCode(), is(equalTo(15)));
            assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3)));
            assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size())));
            assertThat(new BitArrayWrappingBitVector(((WriteMultipleCoilsRequest) request).getCoils(), bits.size()),
                    is(equalTo(bits)));
        }, 6000, 10);
        LoggerFactory.getLogger(this.getClass()).error("ENDINGMULTIPLE");
    }

    /**
     * Write is out-of-bounds, slave should return error
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffWriteMultipleCoilError() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Exception> lastError = new AtomicReference<>();

        BitArray bits = new BasicBitArray(500);
        BasicWriteTask task = new BasicWriteTask(endpoint,
                new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1),
                new ModbusWriteCallback() {

                    @Override
                    public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusWriteRequestBlueprint request, Exception error) {
                        lastError.set(error);
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimeWrite(task);
        callbackCalled.await(5, TimeUnit.SECONDS);

        assertThat(unexpectedCount.get(), is(equalTo(0)));
        assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException);

        assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
        ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
        assertThat(request.getFunctionCode(), is(equalTo(15)));
        assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3)));
        assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size())));
        assertThat(new BitArrayWrappingBitVector(((WriteMultipleCoilsRequest) request).getCoils(), bits.size()),
                is(equalTo(bits)));
    }

    /**
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffWriteSingleCoil() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Object> lastData = new AtomicReference<>();

        BitArray bits = new BasicBitArray(true);
        BasicWriteTask task = new BasicWriteTask(endpoint,
                new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, false, 1),
                new ModbusWriteCallback() {

                    @Override
                    public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) {
                        lastData.set(response);
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusWriteRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimeWrite(task);
        callbackCalled.await(5, TimeUnit.SECONDS);

        assertThat(unexpectedCount.get(), is(equalTo(0)));
        ModbusResponse response = (ModbusResponse) lastData.get();
        assertThat(response.getFunctionCode(), is(equalTo(5)));

        assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
        ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
        assertThat(request.getFunctionCode(), is(equalTo(5)));
        assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(3)));
        assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true)));
    }

    /**
     *
     * Write is out-of-bounds, slave should return error
     *
     * @throws InterruptedException
     */
    @Test
    public void testOneOffWriteSingleCoilError() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(1);
        AtomicReference<Exception> lastError = new AtomicReference<>();

        BitArray bits = new BasicBitArray(true);
        BasicWriteTask task = new BasicWriteTask(endpoint,
                new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 300, bits, false, 1),
                new ModbusWriteCallback() {

                    @Override
                    public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusWriteRequestBlueprint request, Exception error) {
                        lastError.set(error);
                        callbackCalled.countDown();
                    }
                });
        modbusManager.submitOneTimeWrite(task);
        callbackCalled.await(5, TimeUnit.SECONDS);

        assertThat(unexpectedCount.get(), is(equalTo(0)));
        assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException);

        assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1)));
        ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0);
        assertThat(request.getFunctionCode(), is(equalTo(5)));
        assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(300)));
        assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true)));
    }

    /**
     * Testing regular polling of coils
     *
     * Amount of requests is timed, and average poll period is checked
     *
     * @throws InterruptedException
     */
    @Test
    public void testRegularReadEvery150msWithCoil() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(5);
        AtomicInteger dataReceived = new AtomicInteger();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint,
                new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, ModbusReadFunctionCode.READ_COILS, 1, 15, 1),
                new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        dataReceived.incrementAndGet();

                        try {
                            assertThat(bits.size(), is(equalTo(15)));
                            testCoilValues(bits, 1);
                        } catch (AssertionError e) {
                            unexpectedCount.incrementAndGet();
                        }

                        callbackCalled.countDown();
                    }
                });
        long start = System.currentTimeMillis();
        modbusManager.registerRegularPoll(task, 150, 0);
        callbackCalled.await(5, TimeUnit.SECONDS);
        long end = System.currentTimeMillis();
        assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
    }

    /**
     * Testing regular polling of holding registers
     *
     * Amount of requests is timed, and average poll period is checked
     *
     * @throws InterruptedException
     */
    @Test
    public void testRegularReadEvery150msWithHolding() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(5);
        AtomicInteger dataReceived = new AtomicInteger();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        dataReceived.incrementAndGet();

                        try {
                            assertThat(registers.size(), is(equalTo(15)));
                            testHoldingValues(registers, 1);
                        } catch (AssertionError e) {
                            unexpectedCount.incrementAndGet();
                        }

                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        long start = System.currentTimeMillis();
        modbusManager.registerRegularPoll(task, 150, 0);
        callbackCalled.await(5, TimeUnit.SECONDS);
        long end = System.currentTimeMillis();
        assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
    }

    @Test
    public void testRegularReadFirstErrorThenOK() throws InterruptedException {
        generateData();
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(5);
        AtomicInteger dataReceived = new AtomicInteger();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        dataReceived.incrementAndGet();

                        try {
                            assertThat(registers.size(), is(equalTo(15)));
                            testHoldingValues(registers, 1);
                        } catch (AssertionError e) {
                            unexpectedCount.incrementAndGet();
                        }

                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        long start = System.currentTimeMillis();
        modbusManager.registerRegularPoll(task, 150, 0);
        callbackCalled.await(5, TimeUnit.SECONDS);
        modbusManager.unregisterRegularPoll(task);
        long end = System.currentTimeMillis();
        assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500);
    }

    /**
     *
     * @param unexpectedCount number of unexpected callback calls
     * @param callbackCalled number of callback calls (including unexpected)
     * @param dataReceived number of expected callback calls (onBits or onRegisters)
     * @param pollStartMillis poll start time in milliepoch
     * @param expectedPollAverageMin average poll period should be at least greater than this
     * @param expectedPollAverageMax average poll period less than this
     * @throws InterruptedException
     */
    private void assertPollDetails(AtomicInteger unexpectedCount, AtomicInteger expectedCount, long pollStartMillis,
            long pollEndMillis, int expectedPollAverageMin, int expectedPollAverageMax)
            throws InterruptedException {
        int responses = expectedCount.get();
        assertThat(unexpectedCount.get(), is(equalTo(0)));
        assertTrue(responses > 1);

        // Rest of the (timing-sensitive) assertions are not run in CI
        assumeFalse("Running in CI! Will not test timing-sensitive details", isRunningInCI());
        float averagePollPeriodMillis = ((float) (pollEndMillis - pollStartMillis)) / (responses - 1);
        assertTrue(String.format(
                "Measured avarage poll period %f ms (%d responses in %d ms) is not withing expected limits [%d, %d]",
                averagePollPeriodMillis, responses, pollEndMillis - pollStartMillis, expectedPollAverageMin,
                expectedPollAverageMax),
                averagePollPeriodMillis > expectedPollAverageMin
                        && averagePollPeriodMillis < expectedPollAverageMax);
    }

    @Test
    public void testUnregisterPolling() throws InterruptedException {
        ModbusSlaveEndpoint endpoint = getEndpoint();

        AtomicInteger unexpectedCount = new AtomicInteger();
        AtomicInteger errorCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(3);
        AtomicInteger expectedReceived = new AtomicInteger();

        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() {

                    @Override
                    public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
                        expectedReceived.incrementAndGet();
                        callbackCalled.countDown();
                    }

                    @Override
                    public void onError(ModbusReadRequestBlueprint request, Exception error) {
                        if (spi.getDigitalInCount() > 0) {
                            // No errors expected after server filled with data
                            unexpectedCount.incrementAndGet();
                        } else {
                            expectedReceived.incrementAndGet();
                            errorCount.incrementAndGet();
                            generateData();
                            callbackCalled.countDown();
                        }
                    }

                    @Override
                    public void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
                        unexpectedCount.incrementAndGet();
                        callbackCalled.countDown();
                    }
                });
        long start = System.currentTimeMillis();
        modbusManager.registerRegularPoll(task, 200, 0);
        callbackCalled.await(5, TimeUnit.SECONDS);
        modbusManager.unregisterRegularPoll(task);
        long end = System.currentTimeMillis();
        assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600);

        // wait some more and ensure nothing comes back
        Thread.sleep(500);
        assertThat(unexpectedCount.get(), is(equalTo(0)));
    }

    @SuppressWarnings("null")
    @Test
    public void testPoolConfigurationWithoutListener() {
        EndpointPoolConfiguration defaultConfig = modbusManager.getEndpointPoolConfiguration(getEndpoint());
        assertThat(defaultConfig, is(notNullValue()));

        EndpointPoolConfiguration newConfig = new EndpointPoolConfiguration();
        newConfig.setConnectMaxTries(5);
        modbusManager.setEndpointPoolConfiguration(getEndpoint(), newConfig);
        assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()).getConnectMaxTries(), is(equalTo(5)));
        assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(not(equalTo(defaultConfig))));

        // Reset config
        modbusManager.setEndpointPoolConfiguration(getEndpoint(), null);
        // Should matc hdefault
        assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(equalTo(defaultConfig)));
    }

    @SuppressWarnings("null")
    @Test
    public void testPoolConfigurationListenerAndChanges() {
        AtomicInteger expectedCount = new AtomicInteger();
        AtomicInteger unexpectedCount = new AtomicInteger();
        CountDownLatch callbackCalled = new CountDownLatch(2);
        modbusManager.addListener(new ModbusManagerListener() {

            @Override
            public void onEndpointPoolConfigurationSet(ModbusSlaveEndpoint endpoint,
                    EndpointPoolConfiguration configuration) {
                if ((callbackCalled.getCount() == 2L && configuration.getConnectMaxTries() == 50)
                        || (callbackCalled.getCount() == 1L && configuration == null)) {
                    expectedCount.incrementAndGet();
                } else {
                    unexpectedCount.incrementAndGet();
                }
                callbackCalled.countDown();
            }
        });
        EndpointPoolConfiguration defaultConfig = modbusManager.getEndpointPoolConfiguration(getEndpoint());
        assertThat(defaultConfig, is(notNullValue()));

        EndpointPoolConfiguration newConfig = new EndpointPoolConfiguration();
        newConfig.setConnectMaxTries(50);
        modbusManager.setEndpointPoolConfiguration(getEndpoint(), newConfig);
        assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()).getConnectMaxTries(), is(equalTo(50)));
        assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(not(equalTo(defaultConfig))));

        assertThat(unexpectedCount.get(), is(equalTo(0)));
        assertThat(callbackCalled.getCount(), is(equalTo(1L)));

        // Reset config
        modbusManager.setEndpointPoolConfiguration(getEndpoint(), null);
        // Should match default
        assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(equalTo(defaultConfig)));

        // change callback should have been called twice (countdown at zero)
        assertThat(unexpectedCount.get(), is(equalTo(0)));
        assertThat(expectedCount.get(), is(equalTo(2)));
        assertThat(callbackCalled.getCount(), is(equalTo(0L)));

    }

    @Test
    public void testGetRegisteredRegularPolls() {
        ModbusSlaveEndpoint endpoint = getEndpoint();
        BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), null);
        BasicPollTaskImpl task2 = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID,
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 16, 2), null);

        modbusManager.registerRegularPoll(task, 50, 0);
        modbusManager.registerRegularPoll(task2, 50, 0);
        assertThat(modbusManager.getRegisteredRegularPolls(),
                is(equalTo(Stream.of(task, task2).collect(Collectors.toSet()))));
        modbusManager.unregisterRegularPoll(task);
        assertThat(modbusManager.getRegisteredRegularPolls(),
                is(equalTo(Stream.of(task2).collect(Collectors.toSet()))));

    }
}