This is an automated email from the ASF dual-hosted git repository. sruehl pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-plc4x.git
commit c326e36d5ca3b038226c7e924e1678ecf286eeb1 Author: Sebastian Rühl <sru...@apache.org> AuthorDate: Thu Jul 5 14:18:33 2018 +0200 added modbus connection tests --- .../modbus/connection/BaseModbusPlcConnection.java | 24 +- .../modbus/connection/ModbusTcpPlcConnection.java | 4 + .../{CoilAddress.java => CoilModbusAddress.java} | 8 +- .../java/modbus/netty/Plc4XModbusProtocol.java | 14 +- .../connection/BaseModbusPlcConnectionTest.java | 166 ++++++++++++ .../connection/ModbusConnectionFactoryTest.java | 95 +++++++ .../connection/ModbusSerialPlcConnectionTest.java | 286 +++++++++++++++++++++ .../connection/ModbusTcpPlcConnectionTests.java | 134 ++++++++++ 8 files changed, 711 insertions(+), 20 deletions(-) diff --git a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnection.java b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnection.java index 3c65ee6..0056363 100644 --- a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnection.java +++ b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnection.java @@ -18,6 +18,7 @@ under the License. */ package org.apache.plc4x.java.modbus.connection; +import io.netty.channel.ChannelFuture; import org.apache.commons.lang3.StringUtils; import org.apache.plc4x.java.api.connection.PlcReader; import org.apache.plc4x.java.api.connection.PlcWriter; @@ -65,8 +66,8 @@ public abstract class BaseModbusPlcConnection extends AbstractPlcConnection impl return ReadHoldingRegistersModbusAddress.of(addressString); } else if (ReadInputRegistersModbusAddress.ADDRESS_PATTERN.matcher(addressString).matches()) { return ReadInputRegistersModbusAddress.of(addressString); - } else if (CoilAddress.ADDRESS_PATTERN.matcher(addressString).matches()) { - return CoilAddress.of(addressString); + } else if (CoilModbusAddress.ADDRESS_PATTERN.matcher(addressString).matches()) { + return CoilModbusAddress.of(addressString); } else if (RegisterAddress.ADDRESS_PATTERN.matcher(addressString).matches()) { return RegisterAddress.of(addressString); } @@ -76,19 +77,24 @@ public abstract class BaseModbusPlcConnection extends AbstractPlcConnection impl @Override public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) { CompletableFuture<PlcReadResponse> readFuture = new CompletableFuture<>(); - PlcRequestContainer<PlcReadRequest, PlcReadResponse> container = - new PlcRequestContainer<>(readRequest, readFuture); - channel.writeAndFlush(container); + ChannelFuture channelFuture = channel.writeAndFlush(new PlcRequestContainer<>(readRequest, readFuture)); + channelFuture.addListener(future -> { + if (!future.isSuccess()) { + readFuture.completeExceptionally(future.cause()); + } + }); return readFuture; } @Override public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) { CompletableFuture<PlcWriteResponse> writeFuture = new CompletableFuture<>(); - PlcRequestContainer<PlcWriteRequest, PlcWriteResponse> container = - new PlcRequestContainer<>(writeRequest, writeFuture); - channel.writeAndFlush(container); + ChannelFuture channelFuture = channel.writeAndFlush(new PlcRequestContainer<>(writeRequest, writeFuture)); + channelFuture.addListener(future -> { + if (!future.isSuccess()) { + writeFuture.completeExceptionally(future.cause()); + } + }); return writeFuture; } - } diff --git a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnection.java b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnection.java index c418031..92f5337 100644 --- a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnection.java +++ b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnection.java @@ -71,4 +71,8 @@ public class ModbusTcpPlcConnection extends BaseModbusPlcConnection { } }; } + + public InetAddress getRemoteAddress() { + return ((TcpSocketChannelFactory) channelFactory).getAddress(); + } } diff --git a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/model/CoilAddress.java b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/model/CoilModbusAddress.java similarity index 86% rename from plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/model/CoilAddress.java rename to plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/model/CoilModbusAddress.java index fb83da9..414c968 100644 --- a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/model/CoilAddress.java +++ b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/model/CoilModbusAddress.java @@ -23,20 +23,20 @@ import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class CoilAddress extends ModbusAddress { +public class CoilModbusAddress extends ModbusAddress { public static final Pattern ADDRESS_PATTERN = Pattern.compile("coil:" + ModbusAddress.ADDRESS_PATTERN); - protected CoilAddress(int address) { + protected CoilModbusAddress(int address) { super(address); } - public static CoilAddress of(String addressString) { + public static CoilModbusAddress of(String addressString) { Matcher matcher = ADDRESS_PATTERN.matcher(addressString); if (!matcher.matches()) { throw new PlcRuntimeException(addressString + " doesn't match" + ADDRESS_PATTERN); } int address = Integer.valueOf(matcher.group("address")); - return new CoilAddress(address); + return new CoilModbusAddress(address); } } diff --git a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/netty/Plc4XModbusProtocol.java b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/netty/Plc4XModbusProtocol.java index 288cda7..8f751fa 100644 --- a/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/netty/Plc4XModbusProtocol.java +++ b/plc4j/protocols/modbus/src/main/java/org/apache/plc4x/java/modbus/netty/Plc4XModbusProtocol.java @@ -89,15 +89,15 @@ public class Plc4XModbusProtocol extends MessageToMessageCodec<ModbusTcpPayload, int intToWrite = register[0] << 8 | register[1]; modbusRequest = new WriteSingleRegisterRequest(registerAddress.getAddress(), intToWrite); } - } else if (address instanceof CoilAddress) { - CoilAddress coilAddress = (CoilAddress) address; + } else if (address instanceof CoilModbusAddress) { + CoilModbusAddress coilModbusAddress = (CoilModbusAddress) address; if (quantity > 1) { byte[] bytesToWrite = produceCoilValue(writeRequestItem.getValues()); - modbusRequest = new WriteMultipleCoilsRequest(coilAddress.getAddress(), quantity, bytesToWrite); + modbusRequest = new WriteMultipleCoilsRequest(coilModbusAddress.getAddress(), quantity, bytesToWrite); } else { byte[] coil = produceCoilValue(writeRequestItem.getValues()); boolean booleanToWrite = (coil[0] >> 8) == 1; - modbusRequest = new WriteSingleCoilRequest(coilAddress.getAddress(), booleanToWrite); + modbusRequest = new WriteSingleCoilRequest(coilModbusAddress.getAddress(), booleanToWrite); } } else { throw new PlcProtocolException("Unsupported address type" + address.getClass()); @@ -119,9 +119,9 @@ public class Plc4XModbusProtocol extends MessageToMessageCodec<ModbusTcpPayload, ModbusAddress address = (ModbusAddress) readRequestItem.getAddress(); ModbusPdu modbusRequest; - if (address instanceof CoilAddress) { - CoilAddress coilAddress = (CoilAddress) address; - modbusRequest = new ReadCoilsRequest(coilAddress.getAddress(), quantity); + if (address instanceof CoilModbusAddress) { + CoilModbusAddress coilModbusAddress = (CoilModbusAddress) address; + modbusRequest = new ReadCoilsRequest(coilModbusAddress.getAddress(), quantity); } else if (address instanceof RegisterAddress) { RegisterAddress registerAddress = (RegisterAddress) address; modbusRequest = new ReadHoldingRegistersRequest(registerAddress.getAddress(), quantity); diff --git a/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnectionTest.java b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnectionTest.java new file mode 100644 index 0000000..9c00a4b --- /dev/null +++ b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/BaseModbusPlcConnectionTest.java @@ -0,0 +1,166 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.plc4x.java.modbus.connection; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.api.messages.PlcWriteRequest; +import org.apache.plc4x.java.api.messages.PlcWriteResponse; +import org.apache.plc4x.java.api.messages.specific.TypeSafePlcReadRequest; +import org.apache.plc4x.java.api.messages.specific.TypeSafePlcReadResponse; +import org.apache.plc4x.java.api.messages.specific.TypeSafePlcWriteRequest; +import org.apache.plc4x.java.api.messages.specific.TypeSafePlcWriteResponse; +import org.apache.plc4x.java.base.connection.ChannelFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.apache.plc4x.java.base.util.Junit5Backport.assertThrows; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class BaseModbusPlcConnectionTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(BaseModbusPlcConnectionTest.class); + + private BaseModbusPlcConnection SUT; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ChannelFactory channelFactory; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Channel channel; + + @Before + public void setUp() throws Exception { + SUT = new BaseModbusPlcConnection(channelFactory, null) { + @Override + protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) { + return null; + } + }; + + when(channelFactory.createChannel(any())).thenReturn(channel); + + SUT.connect(); + } + + @Test + public void lazyConstructor() { + new BaseModbusPlcConnection(channelFactory, null) { + @Override + protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) { + return null; + } + }; + } + + @Test + public void read() { + CompletableFuture<PlcReadResponse> read = SUT.read(mock(PlcReadRequest.class)); + assertNotNull(read); + CompletableFuture<TypeSafePlcReadResponse<Object>> typeSafeRead = SUT.read(mock(TypeSafePlcReadRequest.class)); + assertNotNull(typeSafeRead); + + simulatePipelineError(() -> SUT.read(mock(PlcReadRequest.class))); + simulatePipelineError(() -> SUT.read(mock(TypeSafePlcReadRequest.class))); + } + + @Test + public void write() { + CompletableFuture<PlcWriteResponse> write = SUT.write(mock(PlcWriteRequest.class)); + assertNotNull(write); + CompletableFuture<TypeSafePlcWriteResponse<Object>> typeSafeWrite = SUT.write(mock(TypeSafePlcWriteRequest.class)); + assertNotNull(typeSafeWrite); + + simulatePipelineError(() -> SUT.write(mock(PlcWriteRequest.class))); + simulatePipelineError(() -> SUT.write(mock(TypeSafePlcWriteRequest.class))); + } + + public void simulatePipelineError(FutureProducingTestRunnable futureProducingTestRunnable) { + ChannelFuture channelFuture = mock(ChannelFuture.class); + // Simulate error in the pipeline + when(channelFuture.addListener(any())).thenAnswer(invocation -> { + Future future = mock(Future.class); + when(future.isSuccess()).thenReturn(false); + when(future.cause()).thenReturn(new DummyException()); + GenericFutureListener genericFutureListener = invocation.getArgument(0); + genericFutureListener.operationComplete(future); + return mock(ChannelFuture.class); + }); + when(channel.writeAndFlush(any())).thenReturn(channelFuture); + assertThrows(DummyException.class, () -> { + CompletableFuture completableFuture = futureProducingTestRunnable.run(); + try { + completableFuture.get(3, TimeUnit.SECONDS); + fail("Should have thrown a ExecutionException"); + } catch (ExecutionException e) { + if (e.getCause() instanceof DummyException) { + throw (DummyException) e.getCause(); + } + throw e; + } + }); + } + + @Test + public void testToString() { + String s = SUT.toString(); + assertNotNull(s); + } + + /** + * Variant of {@link Runnable} which adds a {@code throws Exception} to the {@code run} signature. + */ + private interface TestRunnable { + /** + * @throws Exception when the test throws a exception. + * @see Runnable#run() + */ + void run() throws Exception; + } + + private static class DummyException extends Exception { + + } + + @FunctionalInterface + private interface FutureProducingTestRunnable { + CompletableFuture run() throws Exception; + } +} \ No newline at end of file diff --git a/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusConnectionFactoryTest.java b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusConnectionFactoryTest.java new file mode 100644 index 0000000..c4e3711 --- /dev/null +++ b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusConnectionFactoryTest.java @@ -0,0 +1,95 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.plc4x.java.modbus.connection; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.plc4x.java.base.connection.AbstractPlcConnection; +import org.apache.plc4x.java.base.connection.SerialChannelFactory; +import org.apache.plc4x.java.base.connection.TcpSocketChannelFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetAddress; + +import static org.apache.plc4x.java.base.util.Junit5Backport.assertThrows; +import static org.junit.Assert.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class ModbusConnectionFactoryTest { + + @InjectMocks + private ModbusConnectionFactory SUT; + + @Mock + private InetAddress inetAddress; + + + @Test + public void modbusTcpPlcConnectionOf() throws Exception { + { + assertThrows(NullPointerException.class, () -> SUT.modbusTcpPlcConnectionOf(null, null, null)); + } + { + ModbusTcpPlcConnection modbusTcpPlcConnection = SUT.modbusTcpPlcConnectionOf(inetAddress, null, null); + assertGeneratedPort(modbusTcpPlcConnection); + } + { + ModbusTcpPlcConnection modbusTcpPlcConnection = SUT.modbusTcpPlcConnectionOf(inetAddress, 13, null); + assertEquals(inetAddress, modbusTcpPlcConnection.getRemoteAddress()); + assertPort(modbusTcpPlcConnection, 13); + } + { + ModbusTcpPlcConnection modbusTcpPlcConnection = SUT.modbusTcpPlcConnectionOf(inetAddress, null, "xyz"); + assertEquals(inetAddress, modbusTcpPlcConnection.getRemoteAddress()); + assertGeneratedPort(modbusTcpPlcConnection); + } + } + + public void assertGeneratedPort(ModbusTcpPlcConnection modbusTcpPlcConnection) throws Exception { + assertPort(modbusTcpPlcConnection, 502); + } + + public void assertPort(ModbusTcpPlcConnection modbusTcpPlcConnection, int port) throws Exception { + TcpSocketChannelFactory channelFactory = (TcpSocketChannelFactory) FieldUtils + .getDeclaredField(AbstractPlcConnection.class, "channelFactory", true) + .get(modbusTcpPlcConnection); + assertEquals(port, channelFactory.getPort()); + } + + @Test + public void modbusSerialPlcConnectionOf() throws Exception { + { + assertThrows(NullPointerException.class, () -> SUT.modbusSerialPlcConnectionOf(null, null)); + } + { + ModbusSerialPlcConnection modbusSerialPlcConnection = SUT.modbusSerialPlcConnectionOf("/dev/ttyS01", null); + assertPort(modbusSerialPlcConnection, "/dev/ttyS01"); + } + } + + public void assertPort(ModbusSerialPlcConnection modbusSerialPlcConnection, String serialPort) throws Exception { + SerialChannelFactory channelFactory = (SerialChannelFactory) FieldUtils + .getDeclaredField(AbstractPlcConnection.class, "channelFactory", true) + .get(modbusSerialPlcConnection); + assertEquals(serialPort, channelFactory.getSerialPort()); + } +} \ No newline at end of file diff --git a/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusSerialPlcConnectionTest.java b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusSerialPlcConnectionTest.java new file mode 100644 index 0000000..4339c42 --- /dev/null +++ b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusSerialPlcConnectionTest.java @@ -0,0 +1,286 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.plc4x.java.modbus.connection; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.jsc.JSerialCommDeviceAddress; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.base.connection.AbstractPlcConnection; +import org.apache.plc4x.java.base.connection.SerialChannelFactory; +import org.apache.plc4x.java.modbus.model.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +@Ignore("Not yet implemented in modbus") +public class ModbusSerialPlcConnectionTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(ModbusSerialPlcConnectionTest.class); + + private ModbusSerialPlcConnection SUT; + + @Before + public void setUp() { + SUT = ModbusSerialPlcConnection.of("/dev/tty0", null); + } + + @After + public void tearDown() { + SUT = null; + } + + @Test + public void emptyParseAddress() { + try { + SUT.parseAddress(""); + } catch (IllegalArgumentException exception) { + assertTrue("Unexpected exception", exception.getMessage().startsWith("address doesn't match ")); + } + } + + @Test + public void parseCoilModbusAddress() { + try { + CoilModbusAddress address = (CoilModbusAddress) SUT.parseAddress("0/1"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseMaskWriteRegisterModbusAddress() { + try { + MaskWriteRegisterModbusAddress address = (MaskWriteRegisterModbusAddress) SUT.parseAddress("0/1"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseReadDiscreteInputsModbusAddress() { + try { + ReadDiscreteInputsModbusAddress address = (ReadDiscreteInputsModbusAddress) SUT.parseAddress("0/1"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseReadHoldingRegistersModbusAddress() { + try { + ReadHoldingRegistersModbusAddress address = (ReadHoldingRegistersModbusAddress) SUT.parseAddress("0/1"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseReadInputRegistersModbusAddress() { + try { + ReadInputRegistersModbusAddress address = (ReadInputRegistersModbusAddress) SUT.parseAddress("0/1"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseRegisterAddress() { + try { + RegisterAddress address = (RegisterAddress) SUT.parseAddress("0/1"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void testRead() throws Exception { + prepareSerialSimulator(); + CompletableFuture<PlcReadResponse> read = SUT.read(new PlcReadRequest(String.class, SUT.parseAddress("0/0"))); + PlcReadResponse plcReadResponse = read.get(30, TimeUnit.SECONDS); + assertNotNull(plcReadResponse); + } + + private void prepareSerialSimulator() throws Exception { + Field channelFactoryField = FieldUtils.getField(AbstractPlcConnection.class, "channelFactory", true); + SerialChannelFactory serialChannelFactory = (SerialChannelFactory) channelFactoryField.get(SUT); + SerialChannelFactory serialChannelFactorySpied = spy(serialChannelFactory); + EmbeddedChannel embeddedChannel = new EmbeddedChannel(SUT.getChannelHandler(null)); + embeddedChannel.connect(new JSerialCommDeviceAddress("/dev/tty0")); + doReturn(embeddedChannel).when(serialChannelFactorySpied).createChannel(any()); + channelFactoryField.set(SUT, serialChannelFactorySpied); + SUT.connect(); + new SerialSimulator(embeddedChannel).start(); + } + + private class SerialSimulator extends Thread { + + private EmbeddedChannel embeddedChannel; + + private SimulatorState state = SimulatorState.RECEIVE_REQUEST; + + private byte[] currentInvokeId = new byte[0]; + + public SerialSimulator(EmbeddedChannel embeddedChannel) { + super("Serial Simulator"); + this.embeddedChannel = embeddedChannel; + } + + @Override + public void run() { + while (true) { + LOGGER.trace("in state {}. CurrentInvokeId: {}", state, currentInvokeId); + switch (state) { + // Receiving state + case RECEIVE_REQUEST: { + LOGGER.info("Waiting for normal message"); + ByteBuf outputBuffer; + while ((outputBuffer = embeddedChannel.readOutbound()) == null) { + LOGGER.trace("No buffer available yet"); + if (!trySleep()) { + return; + } + } + // TODO + int headerBytes = 4711; + LOGGER.info("Skipping " + headerBytes + " bytes"); + outputBuffer.skipBytes(headerBytes); + short dataLength = outputBuffer.readUnsignedByte(); + LOGGER.info("Expect at least " + dataLength + "bytes"); + while (outputBuffer.readableBytes() < dataLength) { + if (!trySleep()) { + return; + } + } + byte[] bytes = new byte[dataLength]; + LOGGER.info("Read " + dataLength + "bytes. Having " + outputBuffer.readableBytes() + "bytes"); + outputBuffer.readBytes(bytes); + currentInvokeId = Arrays.copyOfRange(bytes, 28, 32); + // TODO + outputBuffer.skipBytes(4711); + LOGGER.info("Wrote Inbound"); + state = SimulatorState.ACK_MESSAGE; + if (!trySleep()) { + return; + } + } + break; + case ACK_MESSAGE: { + // TODO + ByteBuf byteBuf = Unpooled.buffer(); + try { + MethodUtils.invokeMethod(byteBuf, true, "setRefCnt", 2); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + embeddedChannel.writeOneInbound(byteBuf); + LOGGER.info("Acked Message"); + state = SimulatorState.SEND_RESPONSE; + } + case SEND_RESPONSE: { + LOGGER.info("Sending data message"); + //TODO: + ByteBuf byteBuf = Unpooled.buffer(); + try { + MethodUtils.invokeMethod(byteBuf, true, "setRefCnt", 2); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + embeddedChannel.writeOneInbound(byteBuf); + LOGGER.info("Wrote Inbound"); + state = SimulatorState.WAIT_FOR_ACK; + if (!trySleep()) { + return; + } + } + break; + case WAIT_FOR_ACK: { + LOGGER.info("Waiting for ack message"); + ByteBuf outputBuffer; + while ((outputBuffer = embeddedChannel.readOutbound()) == null) { + if (!trySleep()) { + return; + } + } + //TODO: + int headerBytes = 4711; + LOGGER.info("Skipping " + headerBytes + " bytes"); + outputBuffer.skipBytes(headerBytes); + short dataLength = outputBuffer.readUnsignedByte(); + LOGGER.info("Expect " + dataLength + "bytes"); + state = SimulatorState.DONE; + if (!trySleep()) { + return; + } + } + case DONE: { + LOGGER.info("Plc is Done. Goodbye"); + return; + } + default: + throw new IllegalStateException("Illegal state number" + state); + } + } + + } + + private boolean trySleep() { + try { + TimeUnit.MILLISECONDS.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + return false; + } + return true; + } + } + + private enum SimulatorState { + RECEIVE_REQUEST, + ACK_MESSAGE, + SEND_RESPONSE, + WAIT_FOR_ACK, + DONE + } +} \ No newline at end of file diff --git a/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnectionTests.java b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnectionTests.java new file mode 100644 index 0000000..10293e3 --- /dev/null +++ b/plc4j/protocols/modbus/src/test/java/org/apache/plc4x/java/modbus/connection/ModbusTcpPlcConnectionTests.java @@ -0,0 +1,134 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +package org.apache.plc4x.java.modbus.connection; + +import io.netty.channel.Channel; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.plc4x.java.modbus.model.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; + +public class ModbusTcpPlcConnectionTests { + + private static final Logger LOGGER = LoggerFactory.getLogger(ModbusTcpPlcConnectionTests.class); + + private ModbusTcpPlcConnection SUT; + + private Channel channelMock; + + private ExecutorService executorService; + + @Before + public void setUp() throws Exception { + SUT = ModbusTcpPlcConnection.of(InetAddress.getByName("localhost"), null); + channelMock = mock(Channel.class, RETURNS_DEEP_STUBS); + FieldUtils.writeField(SUT, "channel", channelMock, true); + executorService = Executors.newFixedThreadPool(10); + } + + @After + public void tearDown() { + executorService.shutdownNow(); + SUT = null; + } + + @Test + public void emptyParseAddress() { + try { + SUT.parseAddress(""); + } catch (IllegalArgumentException exception) { + assertTrue("Unexpected exception", exception.getMessage().startsWith("address doesn't match ")); + } + } + + @Test + public void parseCoilModbusAddress() { + try { + CoilModbusAddress address = (CoilModbusAddress) SUT.parseAddress("coil:0"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseMaskWriteRegisterModbusAddress() { + try { + MaskWriteRegisterModbusAddress address = (MaskWriteRegisterModbusAddress) SUT.parseAddress("maskwrite:1/2/3"); + assertEquals(address.getAddress(), 1); + assertEquals(address.getAndMask(), 2); + assertEquals(address.getOrMask(), 3); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseReadDiscreteInputsModbusAddress() { + try { + ReadDiscreteInputsModbusAddress address = (ReadDiscreteInputsModbusAddress) SUT.parseAddress("readdiscreteinputs:0"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseReadHoldingRegistersModbusAddress() { + try { + ReadHoldingRegistersModbusAddress address = (ReadHoldingRegistersModbusAddress) SUT.parseAddress("readholdingregisters:0"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseReadInputRegistersModbusAddress() { + try { + ReadInputRegistersModbusAddress address = (ReadInputRegistersModbusAddress) SUT.parseAddress("readinputregisters:0"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + + @Test + public void parseRegisterAddress() { + try { + RegisterAddress address = (RegisterAddress) SUT.parseAddress("register:0"); + assertEquals(address.getAddress(), 0); + } catch (IllegalArgumentException exception) { + fail("valid data block address"); + } + } + +} \ No newline at end of file