diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 5cf58c4c37..6ea46c9ff9 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -12,6 +12,9 @@ New Features notifications. - Added a new PlcCertificateAuthentication to the API module. - Initial version of a new Java UMAS driver. +- Initial version of a new Java SLMP (Mitsubishi MELSEC) driver: + read-only access to word devices (D/W/R) using binary 3E frames + over TCP. - The 'plc4x' proxy driver now supports TLS as a transport and requires mandatory username/password authentication (configured via the new "username" and "password" connection diff --git a/plc4j/drivers/all/pom.xml b/plc4j/drivers/all/pom.xml index a1c5aba6f4..75b330d72a 100644 --- a/plc4j/drivers/all/pom.xml +++ b/plc4j/drivers/all/pom.xml @@ -178,6 +178,12 @@ 0.14.0-SNAPSHOT runtime + + org.apache.plc4x + plc4j-driver-slmp + 0.14.0-SNAPSHOT + runtime + org.apache.plc4x diff --git a/plc4j/drivers/slmp/pom.xml b/plc4j/drivers/slmp/pom.xml new file mode 100644 index 0000000000..8c2aecbae2 --- /dev/null +++ b/plc4j/drivers/slmp/pom.xml @@ -0,0 +1,267 @@ + + + + + + org.apache.plc4x + plc4j-drivers + 0.14.0-SNAPSHOT + + + plc4j-driver-slmp + + PLC4J: Driver: SLMP + Implementation of a PLC4X read-only driver for the SLMP / MELSEC Communication 3E protocol. + + + 2025-08-02T13:55:11Z + + + + + + org.apache.karaf.tooling + karaf-maven-plugin + + + generate-feature-xml + compile + + features-generate-descriptor + verify + + + true + true + + + + build-kar + package + + kar + + + + + + + org.jacoco + jacoco-maven-plugin + + + + pre-integration-test + + prepare-agent-integration + + + failsafeArgLine + ${project.build.directory}/jacoco.exec + true + + + + check-coverage + verify + + check + + + true + + META-INF/versions/** + module-info.class + **/*$*Listener.class + **/readwrite/*.class + **/with*.class + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.70 + + + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + ${project.groupId}.${project.artifactId} + org.apache.plc4x.java.osgi.DriverActivator + org.apache.plc4x.java.api.PlcDriver,org.apache.plc4x.java.slmp.SlmpDriver + + * + + + + + + + + + + org.apache.plc4x + plc4j-api + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-buffers-api + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-buffers-byte + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-config + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-fields + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-utils + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-values + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-spi-drivers + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-transports-api + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-utils-audit-log-api + 0.14.0-SNAPSHOT + + + org.apache.plc4x + plc4j-transports-tcp + 0.14.0-SNAPSHOT + + + + + org.apache.plc4x + plc4j-utils-test-utils + 0.14.0-SNAPSHOT + test + + + org.apache.plc4x + plc4j-transports-test + 0.14.0-SNAPSHOT + test + + + org.apache.plc4x + plc4x-protocols-slmp + 0.14.0-SNAPSHOT + tests + test-jar + test + + + + + + update-generated-code + + + + org.apache.plc4x.plugins + plc4x-maven-plugin + + + generate-driver + generate-sources + + generate-driver + + + slmp + java + read-write + src/main/generated + + + + + + + + + + org.apache.plc4x + plc4x-code-generation-language-java-jp + 0.14.0-SNAPSHOT + provided + + + org.apache.plc4x + plc4x-protocols-slmp + 0.14.0-SNAPSHOT + provided + + + + + + diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/Constants.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/Constants.java new file mode 100644 index 0000000000..84131a4860 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/Constants.java @@ -0,0 +1,28 @@ +// 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 +// +// https://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.slmp.readwrite; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class Constants { + public static final Integer SLMPDEFAULTPORT = 5007; + + public int getSlmpDefaultPort() { + return SLMPDEFAULTPORT; + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceBlock.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceBlock.java new file mode 100644 index 0000000000..f113fcc64b --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceBlock.java @@ -0,0 +1,123 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpDeviceBlock implements Message { + protected final int headDeviceNumber; + + protected final SlmpDeviceCode deviceCode; + + protected final int numberOfPoints; + + public SlmpDeviceBlock(Integer headDeviceNumber, SlmpDeviceCode deviceCode, + Integer numberOfPoints) { + this.headDeviceNumber = headDeviceNumber; + this.deviceCode = deviceCode; + this.numberOfPoints = numberOfPoints; + } + + /** + * Property field headDeviceNumber + */ + public int getHeadDeviceNumber() { + return headDeviceNumber; + } + + /** + * Property field deviceCode + */ + public SlmpDeviceCode getDeviceCode() { + return deviceCode; + } + + /** + * Property field numberOfPoints + */ + public int getNumberOfPoints() { + return numberOfPoints; + } + + public static SlmpDeviceBlock staticParse(ReadBuffer readBuffer) throws BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpDeviceBlock")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: headDeviceNumber + int headDeviceNumber = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 24), WithOption.WithName("headDeviceNumber")); + + // Simple Field (enum): deviceCode + SlmpDeviceCode deviceCode = FieldReaderFactory.readEnumField(DataReaderFactory.readEnum(SlmpDeviceCode::enumForValue, DataReaderFactory.readUnsignedShort(readBuffer, 8)), WithOption.WithName("deviceCode")); + + // Simple Field: numberOfPoints + int numberOfPoints = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("numberOfPoints")); + + readBuffer.popContext(); + return new SlmpDeviceBlock(headDeviceNumber, deviceCode, numberOfPoints); + } + + @Override + public void serialize(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpDeviceBlock")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: headDeviceNumber + FieldWriterFactory.writeSimpleField((int) headDeviceNumber, DataWriterFactory.writeUnsignedInt(writeBuffer, 24), WithOption.WithName("headDeviceNumber")); + + // Simple Field (enum): deviceCode + FieldWriterFactory.writeSimpleEnumField((SlmpDeviceCode) deviceCode, DataWriterFactory.writeEnum(SlmpDeviceCode::getValue, SlmpDeviceCode::name, DataWriterFactory.writeUnsignedShort(writeBuffer, 8)), WithOption.WithName("deviceCode")); + + // Simple Field: numberOfPoints + FieldWriterFactory.writeSimpleField((int) numberOfPoints, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("numberOfPoints")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = 0; + SlmpDeviceBlock _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: headDeviceNumber + lengthInBits += 24; + + // Simple Field: deviceCode + lengthInBits += 8; + + // Simple Field: numberOfPoints + lengthInBits += 16; + + return lengthInBits; + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceCode.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceCode.java new file mode 100644 index 0000000000..d75e37ea19 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceCode.java @@ -0,0 +1,68 @@ +// 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 +// +// https://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.slmp.readwrite; + +import java.util.HashMap; +import java.util.Map; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public enum SlmpDeviceCode { + D((short) 0xA8), + + W((short) 0xB4), + + R((short) 0xAF), + + TN((short) 0xC2), + + M((short) 0x90), + + X((short) 0x9C), + + Y((short) 0x9D), + + B((short) 0xA0); + + private static final Map map; + + static { + map = new HashMap<>(); + for (SlmpDeviceCode value : SlmpDeviceCode.values()) { + map.put(value.getValue(), value); + } + } + + private final short value; + + SlmpDeviceCode(short value) { + this.value = value; + } + + public short getValue() { + return value; + } + + public static SlmpDeviceCode enumForValue(short value) { + return map.get(value); + } + + public static boolean isDefined(short value) { + return map.containsKey(value); + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceSpec.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceSpec.java new file mode 100644 index 0000000000..3cd22b12d8 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpDeviceSpec.java @@ -0,0 +1,103 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpDeviceSpec implements Message { + protected final int deviceNumber; + + protected final SlmpDeviceCode deviceCode; + + public SlmpDeviceSpec(Integer deviceNumber, SlmpDeviceCode deviceCode) { + this.deviceNumber = deviceNumber; + this.deviceCode = deviceCode; + } + + /** + * Property field deviceNumber + */ + public int getDeviceNumber() { + return deviceNumber; + } + + /** + * Property field deviceCode + */ + public SlmpDeviceCode getDeviceCode() { + return deviceCode; + } + + public static SlmpDeviceSpec staticParse(ReadBuffer readBuffer) throws BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpDeviceSpec")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: deviceNumber + int deviceNumber = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 24), WithOption.WithName("deviceNumber")); + + // Simple Field (enum): deviceCode + SlmpDeviceCode deviceCode = FieldReaderFactory.readEnumField(DataReaderFactory.readEnum(SlmpDeviceCode::enumForValue, DataReaderFactory.readUnsignedShort(readBuffer, 8)), WithOption.WithName("deviceCode")); + + readBuffer.popContext(); + return new SlmpDeviceSpec(deviceNumber, deviceCode); + } + + @Override + public void serialize(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpDeviceSpec")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: deviceNumber + FieldWriterFactory.writeSimpleField((int) deviceNumber, DataWriterFactory.writeUnsignedInt(writeBuffer, 24), WithOption.WithName("deviceNumber")); + + // Simple Field (enum): deviceCode + FieldWriterFactory.writeSimpleEnumField((SlmpDeviceCode) deviceCode, DataWriterFactory.writeEnum(SlmpDeviceCode::getValue, SlmpDeviceCode::name, DataWriterFactory.writeUnsignedShort(writeBuffer, 8)), WithOption.WithName("deviceCode")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = 0; + SlmpDeviceSpec _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: deviceNumber + lengthInBits += 24; + + // Simple Field: deviceCode + lengthInBits += 8; + + return lengthInBits; + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpMessage.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpMessage.java new file mode 100644 index 0000000000..f37cee01c4 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpMessage.java @@ -0,0 +1,123 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.buffers.bytebased.WithByteBasedOption; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.EvaluationHelper; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public abstract class SlmpMessage implements Message { + public static final Short SUBHEADERRESERVED = 0x00; + + public SlmpMessage() { + } + + /** + * Discriminator field subHeader + */ + public abstract short getSubHeader(); + + protected abstract void serializeSlmpMessageChild(WriteBuffer writeBuffer) throws BufferException; + + /** + * Const field subHeaderReserved + */ + public short getSubHeaderReserved() { + return SUBHEADERRESERVED; + } + + public static SlmpMessage staticParse(ReadBuffer readBuffer) throws BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpMessage"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Discriminator Field: subHeader + short subHeader = FieldReaderFactory.readDiscriminatorField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("subHeader"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: subHeaderReserved + short subHeaderReserved = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), SUBHEADERRESERVED, WithOption.WithName("subHeaderReserved"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Switch Field + SlmpMessageBuilder builder = null; + if (EvaluationHelper.equals(subHeader, (short) (0x50))) { + builder = SlmpRequestFrame3E.staticParseSlmpMessageBuilder(readBuffer); + } else if (EvaluationHelper.equals(subHeader, (short) (0xD0))) { + builder = SlmpResponseFrame3E.staticParseSlmpMessageBuilder(readBuffer); + } + if (builder == null) { + throw new BufferException("Unsupported case for discriminated type parameters parameters [subHeader]"); + } + + readBuffer.popContext(); + return builder.build(); + } + + @Override + public void serialize(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpMessage"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + short subHeader = (short) getSubHeader(); + // Discriminator Field: subHeader + FieldWriterFactory.writeDiscriminatorField((short) subHeader, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("subHeader"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: subHeaderReserved + FieldWriterFactory.writeConstField((short) SUBHEADERRESERVED, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("subHeaderReserved"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Switch Field + serializeSlmpMessageChild(writeBuffer); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = 0; + SlmpMessage _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Discriminator Field: subHeader + lengthInBits += 8; + + // Const Field: subHeaderReserved + lengthInBits += 8; + + // Switch Field + // (the subtype will instead add the actual length of subtype elements) + + return lengthInBits; + } + + public interface SlmpMessageBuilder { + SlmpMessage build(); + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpMultiBlockReadRequest.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpMultiBlockReadRequest.java new file mode 100644 index 0000000000..2d2ce08820 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpMultiBlockReadRequest.java @@ -0,0 +1,185 @@ +// 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 +// +// https://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.slmp.readwrite; + +import java.util.List; +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpMultiBlockReadRequest extends SlmpRequestData implements Message { + protected final short numberOfWordDeviceBlocks; + + protected final short numberOfBitDeviceBlocks; + + protected final List wordDeviceBlocks; + + protected final List bitDeviceBlocks; + + public SlmpMultiBlockReadRequest(Short numberOfWordDeviceBlocks, Short numberOfBitDeviceBlocks, + List wordDeviceBlocks, List bitDeviceBlocks) { + this.numberOfWordDeviceBlocks = numberOfWordDeviceBlocks; + this.numberOfBitDeviceBlocks = numberOfBitDeviceBlocks; + this.wordDeviceBlocks = wordDeviceBlocks; + this.bitDeviceBlocks = bitDeviceBlocks; + } + + /** + * Discriminator field command + */ + @Override + public int getCommand() { + return (int) 0x0406; + } + + /** + * Property field numberOfWordDeviceBlocks + */ + public short getNumberOfWordDeviceBlocks() { + return numberOfWordDeviceBlocks; + } + + /** + * Property field numberOfBitDeviceBlocks + */ + public short getNumberOfBitDeviceBlocks() { + return numberOfBitDeviceBlocks; + } + + /** + * Property field wordDeviceBlocks + */ + public List getWordDeviceBlocks() { + return wordDeviceBlocks; + } + + /** + * Property field bitDeviceBlocks + */ + public List getBitDeviceBlocks() { + return bitDeviceBlocks; + } + + public static SlmpRequestDataBuilder staticParseSlmpRequestDataBuilder(ReadBuffer readBuffer, + int command) throws BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpMultiBlockReadRequest")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: numberOfWordDeviceBlocks + short numberOfWordDeviceBlocks = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("numberOfWordDeviceBlocks")); + + // Simple Field: numberOfBitDeviceBlocks + short numberOfBitDeviceBlocks = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("numberOfBitDeviceBlocks")); + + // Array Field: wordDeviceBlocks + List wordDeviceBlocks = FieldReaderFactory.readCountArrayField(DataReaderFactory.readComplex(() -> (SlmpDeviceBlock) SlmpDeviceBlock.staticParse(readBuffer), readBuffer), numberOfWordDeviceBlocks, WithOption.WithName("wordDeviceBlocks")); + + // Array Field: bitDeviceBlocks + List bitDeviceBlocks = FieldReaderFactory.readCountArrayField(DataReaderFactory.readComplex(() -> (SlmpDeviceBlock) SlmpDeviceBlock.staticParse(readBuffer), readBuffer), numberOfBitDeviceBlocks, WithOption.WithName("bitDeviceBlocks")); + + readBuffer.popContext(); + return new SlmpRequestDataBuilderImpl(numberOfWordDeviceBlocks, numberOfBitDeviceBlocks, wordDeviceBlocks, bitDeviceBlocks); + } + + protected void serializeSlmpRequestDataChild(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpMultiBlockReadRequest")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: numberOfWordDeviceBlocks + FieldWriterFactory.writeSimpleField((short) numberOfWordDeviceBlocks, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("numberOfWordDeviceBlocks")); + + // Simple Field: numberOfBitDeviceBlocks + FieldWriterFactory.writeSimpleField((short) numberOfBitDeviceBlocks, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("numberOfBitDeviceBlocks")); + + // Array Field: wordDeviceBlocks + FieldWriterFactory.writeComplexTypeArrayField(wordDeviceBlocks, writeBuffer, WithOption.WithName("wordDeviceBlocks")); + + // Array Field: bitDeviceBlocks + FieldWriterFactory.writeComplexTypeArrayField(bitDeviceBlocks, writeBuffer, WithOption.WithName("bitDeviceBlocks")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = super.getLengthInBits(); + SlmpMultiBlockReadRequest _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: numberOfWordDeviceBlocks + lengthInBits += 8; + + // Simple Field: numberOfBitDeviceBlocks + lengthInBits += 8; + + // Array Field: wordDeviceBlocks + if (wordDeviceBlocks != null) { + int i = 0; + for (SlmpDeviceBlock _element : wordDeviceBlocks) { + ThreadLocalHelper.lastItemThreadLocal.set(++i >= wordDeviceBlocks.size()); + lengthInBits += _element.getLengthInBits(); + } + } + + // Array Field: bitDeviceBlocks + if (bitDeviceBlocks != null) { + int i = 0; + for (SlmpDeviceBlock _element : bitDeviceBlocks) { + ThreadLocalHelper.lastItemThreadLocal.set(++i >= bitDeviceBlocks.size()); + lengthInBits += _element.getLengthInBits(); + } + } + + return lengthInBits; + } + + public static class SlmpRequestDataBuilderImpl implements SlmpRequestData.SlmpRequestDataBuilder { + private final short numberOfWordDeviceBlocks; + + private final short numberOfBitDeviceBlocks; + + private final List wordDeviceBlocks; + + private final List bitDeviceBlocks; + + public SlmpRequestDataBuilderImpl(short numberOfWordDeviceBlocks, short numberOfBitDeviceBlocks, + List wordDeviceBlocks, List bitDeviceBlocks) { + this.numberOfWordDeviceBlocks = numberOfWordDeviceBlocks; + this.numberOfBitDeviceBlocks = numberOfBitDeviceBlocks; + this.wordDeviceBlocks = wordDeviceBlocks; + this.bitDeviceBlocks = bitDeviceBlocks; + } + + public SlmpRequestData build() { + return new SlmpMultiBlockReadRequest(numberOfWordDeviceBlocks, numberOfBitDeviceBlocks, wordDeviceBlocks, bitDeviceBlocks); + } + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRandomReadRequest.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRandomReadRequest.java new file mode 100644 index 0000000000..2a38858a62 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRandomReadRequest.java @@ -0,0 +1,186 @@ +// 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 +// +// https://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.slmp.readwrite; + +import java.util.List; +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpRandomReadRequest extends SlmpRequestData implements Message { + protected final short numberOfWordAccessPoints; + + protected final short numberOfDoubleWordAccessPoints; + + protected final List wordAccessDevices; + + protected final List doubleWordAccessDevices; + + public SlmpRandomReadRequest(Short numberOfWordAccessPoints, Short numberOfDoubleWordAccessPoints, + List wordAccessDevices, List doubleWordAccessDevices) { + this.numberOfWordAccessPoints = numberOfWordAccessPoints; + this.numberOfDoubleWordAccessPoints = numberOfDoubleWordAccessPoints; + this.wordAccessDevices = wordAccessDevices; + this.doubleWordAccessDevices = doubleWordAccessDevices; + } + + /** + * Discriminator field command + */ + @Override + public int getCommand() { + return (int) 0x0403; + } + + /** + * Property field numberOfWordAccessPoints + */ + public short getNumberOfWordAccessPoints() { + return numberOfWordAccessPoints; + } + + /** + * Property field numberOfDoubleWordAccessPoints + */ + public short getNumberOfDoubleWordAccessPoints() { + return numberOfDoubleWordAccessPoints; + } + + /** + * Property field wordAccessDevices + */ + public List getWordAccessDevices() { + return wordAccessDevices; + } + + /** + * Property field doubleWordAccessDevices + */ + public List getDoubleWordAccessDevices() { + return doubleWordAccessDevices; + } + + public static SlmpRequestDataBuilder staticParseSlmpRequestDataBuilder(ReadBuffer readBuffer, + int command) throws BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpRandomReadRequest")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: numberOfWordAccessPoints + short numberOfWordAccessPoints = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("numberOfWordAccessPoints")); + + // Simple Field: numberOfDoubleWordAccessPoints + short numberOfDoubleWordAccessPoints = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("numberOfDoubleWordAccessPoints")); + + // Array Field: wordAccessDevices + List wordAccessDevices = FieldReaderFactory.readCountArrayField(DataReaderFactory.readComplex(() -> (SlmpDeviceSpec) SlmpDeviceSpec.staticParse(readBuffer), readBuffer), numberOfWordAccessPoints, WithOption.WithName("wordAccessDevices")); + + // Array Field: doubleWordAccessDevices + List doubleWordAccessDevices = FieldReaderFactory.readCountArrayField(DataReaderFactory.readComplex(() -> (SlmpDeviceSpec) SlmpDeviceSpec.staticParse(readBuffer), readBuffer), numberOfDoubleWordAccessPoints, WithOption.WithName("doubleWordAccessDevices")); + + readBuffer.popContext(); + return new SlmpRequestDataBuilderImpl(numberOfWordAccessPoints, numberOfDoubleWordAccessPoints, wordAccessDevices, doubleWordAccessDevices); + } + + protected void serializeSlmpRequestDataChild(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpRandomReadRequest")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: numberOfWordAccessPoints + FieldWriterFactory.writeSimpleField((short) numberOfWordAccessPoints, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("numberOfWordAccessPoints")); + + // Simple Field: numberOfDoubleWordAccessPoints + FieldWriterFactory.writeSimpleField((short) numberOfDoubleWordAccessPoints, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("numberOfDoubleWordAccessPoints")); + + // Array Field: wordAccessDevices + FieldWriterFactory.writeComplexTypeArrayField(wordAccessDevices, writeBuffer, WithOption.WithName("wordAccessDevices")); + + // Array Field: doubleWordAccessDevices + FieldWriterFactory.writeComplexTypeArrayField(doubleWordAccessDevices, writeBuffer, WithOption.WithName("doubleWordAccessDevices")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = super.getLengthInBits(); + SlmpRandomReadRequest _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: numberOfWordAccessPoints + lengthInBits += 8; + + // Simple Field: numberOfDoubleWordAccessPoints + lengthInBits += 8; + + // Array Field: wordAccessDevices + if (wordAccessDevices != null) { + int i = 0; + for (SlmpDeviceSpec _element : wordAccessDevices) { + ThreadLocalHelper.lastItemThreadLocal.set(++i >= wordAccessDevices.size()); + lengthInBits += _element.getLengthInBits(); + } + } + + // Array Field: doubleWordAccessDevices + if (doubleWordAccessDevices != null) { + int i = 0; + for (SlmpDeviceSpec _element : doubleWordAccessDevices) { + ThreadLocalHelper.lastItemThreadLocal.set(++i >= doubleWordAccessDevices.size()); + lengthInBits += _element.getLengthInBits(); + } + } + + return lengthInBits; + } + + public static class SlmpRequestDataBuilderImpl implements SlmpRequestData.SlmpRequestDataBuilder { + private final short numberOfWordAccessPoints; + + private final short numberOfDoubleWordAccessPoints; + + private final List wordAccessDevices; + + private final List doubleWordAccessDevices; + + public SlmpRequestDataBuilderImpl(short numberOfWordAccessPoints, + short numberOfDoubleWordAccessPoints, List wordAccessDevices, + List doubleWordAccessDevices) { + this.numberOfWordAccessPoints = numberOfWordAccessPoints; + this.numberOfDoubleWordAccessPoints = numberOfDoubleWordAccessPoints; + this.wordAccessDevices = wordAccessDevices; + this.doubleWordAccessDevices = doubleWordAccessDevices; + } + + public SlmpRequestData build() { + return new SlmpRandomReadRequest(numberOfWordAccessPoints, numberOfDoubleWordAccessPoints, wordAccessDevices, doubleWordAccessDevices); + } + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpReadRequest.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpReadRequest.java new file mode 100644 index 0000000000..ee305305d9 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpReadRequest.java @@ -0,0 +1,150 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpReadRequest extends SlmpRequestData implements Message { + protected final int headDeviceNumber; + + protected final SlmpDeviceCode deviceCode; + + protected final int numberOfPoints; + + public SlmpReadRequest(Integer headDeviceNumber, SlmpDeviceCode deviceCode, + Integer numberOfPoints) { + this.headDeviceNumber = headDeviceNumber; + this.deviceCode = deviceCode; + this.numberOfPoints = numberOfPoints; + } + + /** + * Discriminator field command + */ + @Override + public int getCommand() { + return (int) 0x0401; + } + + /** + * Property field headDeviceNumber + */ + public int getHeadDeviceNumber() { + return headDeviceNumber; + } + + /** + * Property field deviceCode + */ + public SlmpDeviceCode getDeviceCode() { + return deviceCode; + } + + /** + * Property field numberOfPoints + */ + public int getNumberOfPoints() { + return numberOfPoints; + } + + public static SlmpRequestDataBuilder staticParseSlmpRequestDataBuilder(ReadBuffer readBuffer, + int command) throws BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpReadRequest")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: headDeviceNumber + int headDeviceNumber = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 24), WithOption.WithName("headDeviceNumber")); + + // Simple Field (enum): deviceCode + SlmpDeviceCode deviceCode = FieldReaderFactory.readEnumField(DataReaderFactory.readEnum(SlmpDeviceCode::enumForValue, DataReaderFactory.readUnsignedShort(readBuffer, 8)), WithOption.WithName("deviceCode")); + + // Simple Field: numberOfPoints + int numberOfPoints = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("numberOfPoints")); + + readBuffer.popContext(); + return new SlmpRequestDataBuilderImpl(headDeviceNumber, deviceCode, numberOfPoints); + } + + protected void serializeSlmpRequestDataChild(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpReadRequest")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: headDeviceNumber + FieldWriterFactory.writeSimpleField((int) headDeviceNumber, DataWriterFactory.writeUnsignedInt(writeBuffer, 24), WithOption.WithName("headDeviceNumber")); + + // Simple Field (enum): deviceCode + FieldWriterFactory.writeSimpleEnumField((SlmpDeviceCode) deviceCode, DataWriterFactory.writeEnum(SlmpDeviceCode::getValue, SlmpDeviceCode::name, DataWriterFactory.writeUnsignedShort(writeBuffer, 8)), WithOption.WithName("deviceCode")); + + // Simple Field: numberOfPoints + FieldWriterFactory.writeSimpleField((int) numberOfPoints, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("numberOfPoints")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = super.getLengthInBits(); + SlmpReadRequest _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: headDeviceNumber + lengthInBits += 24; + + // Simple Field: deviceCode + lengthInBits += 8; + + // Simple Field: numberOfPoints + lengthInBits += 16; + + return lengthInBits; + } + + public static class SlmpRequestDataBuilderImpl implements SlmpRequestData.SlmpRequestDataBuilder { + private final int headDeviceNumber; + + private final SlmpDeviceCode deviceCode; + + private final int numberOfPoints; + + public SlmpRequestDataBuilderImpl(int headDeviceNumber, SlmpDeviceCode deviceCode, + int numberOfPoints) { + this.headDeviceNumber = headDeviceNumber; + this.deviceCode = deviceCode; + this.numberOfPoints = numberOfPoints; + } + + public SlmpRequestData build() { + return new SlmpReadRequest(headDeviceNumber, deviceCode, numberOfPoints); + } + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRequestData.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRequestData.java new file mode 100644 index 0000000000..4394dd10ff --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRequestData.java @@ -0,0 +1,94 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.utils.EvaluationHelper; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public abstract class SlmpRequestData implements Message { + public SlmpRequestData() { + } + + /** + * Discriminator field command + */ + public abstract int getCommand(); + + protected abstract void serializeSlmpRequestDataChild(WriteBuffer writeBuffer) throws + BufferException; + + public static SlmpRequestData staticParse(ReadBuffer readBuffer, int command) throws + BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpRequestData")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Switch Field + SlmpRequestDataBuilder builder = null; + if (EvaluationHelper.equals(command, (int) (0x0401))) { + builder = SlmpReadRequest.staticParseSlmpRequestDataBuilder(readBuffer, command); + } else if (EvaluationHelper.equals(command, (int) (0x0403))) { + builder = SlmpRandomReadRequest.staticParseSlmpRequestDataBuilder(readBuffer, command); + } else if (EvaluationHelper.equals(command, (int) (0x0406))) { + builder = SlmpMultiBlockReadRequest.staticParseSlmpRequestDataBuilder(readBuffer, command); + } + if (builder == null) { + throw new BufferException("Unsupported case for discriminated type parameters parameters [command]"); + } + + readBuffer.popContext(); + return builder.build(); + } + + @Override + public void serialize(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpRequestData")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Switch Field + serializeSlmpRequestDataChild(writeBuffer); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = 0; + SlmpRequestData _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Switch Field + // (the subtype will instead add the actual length of subtype elements) + + return lengthInBits; + } + + public interface SlmpRequestDataBuilder { + SlmpRequestData build(); + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRequestFrame3E.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRequestFrame3E.java new file mode 100644 index 0000000000..042f446bb8 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpRequestFrame3E.java @@ -0,0 +1,255 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.buffers.bytebased.WithByteBasedOption; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpRequestFrame3E extends SlmpMessage implements Message { + public static final Short NETWORK = 0x00; + + public static final Short PCNUMBER = 0xFF; + + public static final Integer REQUESTDESTMODULEIONO = 0x03FF; + + public static final Short REQUESTDESTMODULESTATION = 0x00; + + protected final int monitoringTimer; + + protected final int command; + + protected final int subCommand; + + protected final SlmpRequestData requestData; + + public SlmpRequestFrame3E(Integer monitoringTimer, Integer command, Integer subCommand, + SlmpRequestData requestData) { + this.monitoringTimer = monitoringTimer; + this.command = command; + this.subCommand = subCommand; + this.requestData = requestData; + } + + /** + * Discriminator field subHeader + */ + @Override + public short getSubHeader() { + return (short) 0x50; + } + + /** + * Property field monitoringTimer + */ + public int getMonitoringTimer() { + return monitoringTimer; + } + + /** + * Property field command + */ + public int getCommand() { + return command; + } + + /** + * Property field subCommand + */ + public int getSubCommand() { + return subCommand; + } + + /** + * Property field requestData + */ + public SlmpRequestData getRequestData() { + return requestData; + } + + /** + * Const field network + */ + public short getNetwork() { + return NETWORK; + } + + /** + * Const field pcNumber + */ + public short getPcNumber() { + return PCNUMBER; + } + + /** + * Const field requestDestModuleIoNo + */ + public int getRequestDestModuleIoNo() { + return REQUESTDESTMODULEIONO; + } + + /** + * Const field requestDestModuleStation + */ + public short getRequestDestModuleStation() { + return REQUESTDESTMODULESTATION; + } + + public static SlmpMessageBuilder staticParseSlmpMessageBuilder(ReadBuffer readBuffer) throws + BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpRequestFrame3E"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Const Field: network + short network = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), NETWORK, WithOption.WithName("network"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: pcNumber + short pcNumber = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), PCNUMBER, WithOption.WithName("pcNumber"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleIoNo + int requestDestModuleIoNo = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedInt(readBuffer, 16), REQUESTDESTMODULEIONO, WithOption.WithName("requestDestModuleIoNo"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleStation + short requestDestModuleStation = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), REQUESTDESTMODULESTATION, WithOption.WithName("requestDestModuleStation"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Implicit Field: requestDataLength + int requestDataLength = FieldReaderFactory.readImplicitField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("requestDataLength"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: monitoringTimer + int monitoringTimer = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("monitoringTimer"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: command + int command = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("command"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: subCommand + int subCommand = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("subCommand"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: requestData + SlmpRequestData requestData = FieldReaderFactory.readSimpleField(DataReaderFactory.readComplex(() -> (SlmpRequestData) SlmpRequestData.staticParse(readBuffer, (int) (command)), readBuffer), WithOption.WithName("requestData"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + readBuffer.popContext(); + return new SlmpMessageBuilderImpl(monitoringTimer, command, subCommand, requestData); + } + + protected void serializeSlmpMessageChild(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpRequestFrame3E"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Const Field: network + FieldWriterFactory.writeConstField((short) NETWORK, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("network"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: pcNumber + FieldWriterFactory.writeConstField((short) PCNUMBER, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("pcNumber"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleIoNo + FieldWriterFactory.writeConstField((int) REQUESTDESTMODULEIONO, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("requestDestModuleIoNo"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleStation + FieldWriterFactory.writeConstField((short) REQUESTDESTMODULESTATION, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("requestDestModuleStation"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Implicit Field: requestDataLength + int requestDataLength = (int) ((6) + (requestData.getLengthInBytes())); + FieldWriterFactory.writeImplicitField((int) requestDataLength, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("requestDataLength"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: monitoringTimer + FieldWriterFactory.writeSimpleField((int) monitoringTimer, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("monitoringTimer"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: command + FieldWriterFactory.writeSimpleField((int) command, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("command"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: subCommand + FieldWriterFactory.writeSimpleField((int) subCommand, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("subCommand"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: requestData + FieldWriterFactory.writeSimpleField((SlmpRequestData) requestData, DataWriterFactory.writeComplex(writeBuffer), WithOption.WithName("requestData"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = super.getLengthInBits(); + SlmpRequestFrame3E _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Const Field: network + lengthInBits += 8; + + // Const Field: pcNumber + lengthInBits += 8; + + // Const Field: requestDestModuleIoNo + lengthInBits += 16; + + // Const Field: requestDestModuleStation + lengthInBits += 8; + + // Implicit Field: requestDataLength + lengthInBits += 16; + + // Simple Field: monitoringTimer + lengthInBits += 16; + + // Simple Field: command + lengthInBits += 16; + + // Simple Field: subCommand + lengthInBits += 16; + + // Simple Field: requestData + lengthInBits += requestData.getLengthInBits(); + + return lengthInBits; + } + + public static class SlmpMessageBuilderImpl implements SlmpMessage.SlmpMessageBuilder { + private final int monitoringTimer; + + private final int command; + + private final int subCommand; + + private final SlmpRequestData requestData; + + public SlmpMessageBuilderImpl(int monitoringTimer, int command, int subCommand, + SlmpRequestData requestData) { + this.monitoringTimer = monitoringTimer; + this.command = command; + this.subCommand = subCommand; + this.requestData = requestData; + } + + public SlmpMessage build() { + return new SlmpRequestFrame3E(monitoringTimer, command, subCommand, requestData); + } + } +} diff --git a/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpResponseFrame3E.java b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpResponseFrame3E.java new file mode 100644 index 0000000000..bd44291deb --- /dev/null +++ b/plc4j/drivers/slmp/src/main/generated/org/apache/plc4x/java/slmp/readwrite/SlmpResponseFrame3E.java @@ -0,0 +1,210 @@ +// 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 +// +// https://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.slmp.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.buffers.bytebased.WithByteBasedOption; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; +import org.apache.plc4x.java.spi.utils.StaticHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class SlmpResponseFrame3E extends SlmpMessage implements Message { + public static final Short NETWORK = 0x00; + + public static final Short PCNUMBER = 0xFF; + + public static final Integer REQUESTDESTMODULEIONO = 0x03FF; + + public static final Short REQUESTDESTMODULESTATION = 0x00; + + protected final int endCode; + + protected final byte[] responseData; + + public SlmpResponseFrame3E(Integer endCode, byte[] responseData) { + this.endCode = endCode; + this.responseData = responseData; + } + + /** + * Discriminator field subHeader + */ + @Override + public short getSubHeader() { + return (short) 0xD0; + } + + /** + * Property field endCode + */ + public int getEndCode() { + return endCode; + } + + /** + * Property field responseData + */ + public byte[] getResponseData() { + return responseData; + } + + /** + * Const field network + */ + public short getNetwork() { + return NETWORK; + } + + /** + * Const field pcNumber + */ + public short getPcNumber() { + return PCNUMBER; + } + + /** + * Const field requestDestModuleIoNo + */ + public int getRequestDestModuleIoNo() { + return REQUESTDESTMODULEIONO; + } + + /** + * Const field requestDestModuleStation + */ + public short getRequestDestModuleStation() { + return REQUESTDESTMODULESTATION; + } + + public static SlmpMessageBuilder staticParseSlmpMessageBuilder(ReadBuffer readBuffer) throws + BufferException { + readBuffer.pushContext(WithOption.WithName("SlmpResponseFrame3E"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Const Field: network + short network = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), NETWORK, WithOption.WithName("network"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: pcNumber + short pcNumber = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), PCNUMBER, WithOption.WithName("pcNumber"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleIoNo + int requestDestModuleIoNo = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedInt(readBuffer, 16), REQUESTDESTMODULEIONO, WithOption.WithName("requestDestModuleIoNo"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleStation + short requestDestModuleStation = FieldReaderFactory.readConstField(DataReaderFactory.readUnsignedShort(readBuffer, 8), REQUESTDESTMODULESTATION, WithOption.WithName("requestDestModuleStation"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Implicit Field: responseDataLength + int responseDataLength = FieldReaderFactory.readImplicitField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("responseDataLength"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: endCode + int endCode = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedInt(readBuffer, 16), WithOption.WithName("endCode"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Array Field: responseData + byte[] responseData = readBuffer.readBits(Math.toIntExact(((responseDataLength) - (2)) * 8), WithOption.WithName("responseData"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + readBuffer.popContext(); + return new SlmpMessageBuilderImpl(endCode, responseData); + } + + protected void serializeSlmpMessageChild(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("SlmpResponseFrame3E"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Const Field: network + FieldWriterFactory.writeConstField((short) NETWORK, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("network"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: pcNumber + FieldWriterFactory.writeConstField((short) PCNUMBER, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("pcNumber"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleIoNo + FieldWriterFactory.writeConstField((int) REQUESTDESTMODULEIONO, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("requestDestModuleIoNo"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Const Field: requestDestModuleStation + FieldWriterFactory.writeConstField((short) REQUESTDESTMODULESTATION, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("requestDestModuleStation"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Implicit Field: responseDataLength + int responseDataLength = (int) ((2) + (StaticHelper.COUNT(responseData))); + FieldWriterFactory.writeImplicitField((int) responseDataLength, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("responseDataLength"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Simple Field: endCode + FieldWriterFactory.writeSimpleField((int) endCode, DataWriterFactory.writeUnsignedInt(writeBuffer, 16), WithOption.WithName("endCode"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + // Array Field: responseData + FieldWriterFactory.writeByteArrayField(responseData, DataWriterFactory.writeByteArray(writeBuffer, (int) ((responseData != null) ? responseData.length : 0)), WithOption.WithName("responseData"), WithOption.WithFloatEncoding("IEEE754"), WithOption.WithSignedIntegerEncoding("twos-complement"), WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), WithOption.WithStringEncoding("UTF8")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = super.getLengthInBits(); + SlmpResponseFrame3E _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Const Field: network + lengthInBits += 8; + + // Const Field: pcNumber + lengthInBits += 8; + + // Const Field: requestDestModuleIoNo + lengthInBits += 16; + + // Const Field: requestDestModuleStation + lengthInBits += 8; + + // Implicit Field: responseDataLength + lengthInBits += 16; + + // Simple Field: endCode + lengthInBits += 16; + + // Array Field: responseData + lengthInBits += 8 * ((responseData != null) ? responseData.length : 0); + + return lengthInBits; + } + + public static class SlmpMessageBuilderImpl implements SlmpMessage.SlmpMessageBuilder { + private final int endCode; + + private final byte[] responseData; + + public SlmpMessageBuilderImpl(int endCode, byte[] responseData) { + this.endCode = endCode; + this.responseData = responseData; + } + + public SlmpMessage build() { + return new SlmpResponseFrame3E(endCode, responseData); + } + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpConnection.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpConnection.java new file mode 100644 index 0000000000..216c13fd55 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpConnection.java @@ -0,0 +1,247 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; +import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.api.types.ConnectionStateChangeType; +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.api.value.PlcValue; +import org.apache.plc4x.java.slmp.config.SlmpConfiguration; +import org.apache.plc4x.java.slmp.readwrite.SlmpMessage; +import org.apache.plc4x.java.slmp.readwrite.SlmpReadRequest; +import org.apache.plc4x.java.slmp.readwrite.SlmpRequestFrame3E; +import org.apache.plc4x.java.slmp.readwrite.SlmpResponseFrame3E; +import org.apache.plc4x.java.slmp.tag.SlmpTag; +import org.apache.plc4x.java.slmp.tag.SlmpTagHandler; +import org.apache.plc4x.java.spi.drivers.ConnectionBase; +import org.apache.plc4x.java.spi.drivers.exceptions.MessageCodecException; +import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadRequest; +import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadResponse; +import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcResponseItem; +import org.apache.plc4x.java.spi.drivers.messages.items.PlcResponseItem; +import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler; +import org.apache.plc4x.java.spi.transports.api.TransportInstance; +import org.apache.plc4x.java.spi.values.DefaultPlcValueHandler; +import org.apache.plc4x.java.spi.values.PlcValueHandler; +import org.apache.plc4x.java.utils.auditlog.api.AuditLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +public class SlmpConnection extends ConnectionBase { + + private static final Logger LOGGER = LoggerFactory.getLogger(SlmpConnection.class); + + private SlmpMessageCodec messageCodec; + private final AtomicReference> pendingResponse = new AtomicReference<>(); + + public SlmpConnection(SlmpConfiguration configuration, TransportInstance transportInstance, AuditLog auditLog) { + super(configuration, transportInstance, auditLog); + } + + @Override + protected void onConnect() throws PlcConnectionException { + validateConfiguration(); + messageCodec = new SlmpMessageCodec(transportInstance, this::handleIncomingMessage); + startReceiving(() -> { + try { + messageCodec.processIncomingData(); + } catch (MessageCodecException e) { + LOGGER.error("Error processing incoming SLMP data", e); + } + }); + fireConnectionStateChanged(ConnectionStateChangeType.CONNECTED, null); + LOGGER.info("SLMP connection established"); + } + + /** + * Validates configuration values that are consumed later without their own range check. + * The configuration is populated by direct field injection (bypassing the setters), so this + * is the guaranteed one-time enforcement point covering every construction path. + */ + private void validateConfiguration() throws PlcConnectionException { + SlmpConfiguration configuration = getConfiguration(); + int monitoringTimer = configuration.getMonitoringTimer(); + if (monitoringTimer < 0 || monitoringTimer > 0xFFFF) { + throw new PlcConnectionException("monitoring-timer must be in [0, 65535] but was " + monitoringTimer + + " (it is serialized as an unsigned 16-bit field in the 3E frame)"); + } + int requestTimeout = configuration.getRequestTimeout(); + if (requestTimeout <= 0) { + throw new PlcConnectionException("request-timeout must be > 0 ms but was " + requestTimeout + + " (a non-positive value would time out every request immediately)"); + } + } + + @Override + public boolean isConnected() { + return messageCodec != null && messageCodec.isOpen(); + } + + @Override + public void close() throws Exception { + stopReceiving(); + if (messageCodec != null) { + messageCodec.close(); + } + failPending(new PlcRuntimeException("Connection closed")); + super.close(); + fireConnectionStateChanged(ConnectionStateChangeType.DISCONNECTED, null); + } + + @Override + protected void onTransportDisconnected(Throwable cause) { + super.onTransportDisconnected(cause); + fireConnectionStateChanged(ConnectionStateChangeType.CONNECTION_LOST, + cause != null ? cause.getMessage() : "Connection closed by remote"); + failPending(new PlcRuntimeException( + cause != null ? "Connection lost: " + cause.getMessage() : "Connection closed by remote", cause)); + } + + @Override + protected PlcTagHandler getTagHandler() { + return new SlmpTagHandler(); + } + + @Override + protected PlcValueHandler getValueHandler() { + return new DefaultPlcValueHandler(); + } + + @Override + protected int getMaxConcurrentRequests() { + return 1; + } + + private void handleIncomingMessage(SlmpMessage message) { + CompletableFuture future = pendingResponse.getAndSet(null); + if (future == null) { + LOGGER.warn("Received unsolicited SLMP message"); + return; + } + if (message instanceof SlmpResponseFrame3E response) { + if (!future.complete(response)) { + LOGGER.warn("Late SLMP response for an already-completed request dropped"); + } + } else { + future.completeExceptionally(new PlcRuntimeException("Unexpected SLMP message type: " + message)); + } + } + + private void failPending(Throwable cause) { + CompletableFuture future = pendingResponse.getAndSet(null); + if (future != null) { + future.completeExceptionally(cause); + } + } + + /** + * Sends one 3E request and returns a future for its response. Because the 3E frame carries + * no transaction/correlation id and {@link #getMaxConcurrentRequests()} is 1 (so + * {@code executeThrottled} serializes I/O), a single {@code pendingResponse} slot suffices. + *

+ * Caveat: if a request times out, a late response for it may arrive on the receiver thread + * after the next request has installed its own slot, and would then be mis-attributed to that + * next request. 3E cannot prevent this (no correlation key), so a timed-out read should be + * treated as unreliable by the caller. + *

+ * All slot clean-up is identity-checked (compare-and-clear): the timeout callback runs on the + * delayer thread and may run after the throttle has already released the next request (OpenJDK + * completes dependents LIFO), so an unconditional clear could wipe the successor's freshly + * installed slot and spuriously time it out too. The compare-and-clear is correct regardless + * of that ordering. + */ + private CompletableFuture sendRequest(SlmpRequestFrame3E request) { + CompletableFuture responseFuture = new CompletableFuture<>(); + pendingResponse.set(responseFuture); + try { + messageCodec.send(request); + } catch (MessageCodecException e) { + pendingResponse.compareAndSet(responseFuture, null); + responseFuture.completeExceptionally(new PlcRuntimeException("Failed to send SLMP request", e)); + return responseFuture; + } + responseFuture.orTimeout(getConfiguration().getRequestTimeout(), TimeUnit.MILLISECONDS) + .whenComplete((r, e) -> { + if (e instanceof TimeoutException) { + pendingResponse.compareAndSet(responseFuture, null); + } + }); + return responseFuture; + } + + @Override + protected CompletableFuture onRead(PlcReadRequest readRequest) { + DefaultPlcReadRequest request = (DefaultPlcReadRequest) readRequest; + + LinkedHashMap>> tagFutures = new LinkedHashMap<>(); + CompletableFuture chain = CompletableFuture.completedFuture(null); + for (String tagName : request.getTagNames()) { + SlmpTag tag = (SlmpTag) request.getTag(tagName); + CompletableFuture> tagFuture = + chain.thenComposeAsync(v -> readSingleTag(tag)); + tagFutures.put(tagName, tagFuture); + chain = tagFuture.handle((r, e) -> null); + } + + CompletableFuture allDone = + CompletableFuture.allOf(tagFutures.values().toArray(new CompletableFuture[0])); + // handle, not thenApply: allOf completes exceptionally if ANY tag failed, but a failed + // tag must still yield a response with that tag mapped to an error code below. + return allDone.handle((v, anyTagFailure) -> { + Map> items = new LinkedHashMap<>(); + for (Map.Entry>> e : tagFutures.entrySet()) { + try { + items.put(e.getKey(), e.getValue().join()); + } catch (Exception ex) { + // Partial-failure isolation (v0): a transport/timeout error on one tag fails + // only that tag's entry rather than the whole request. A timeout is surfaced as + // REMOTE_ERROR (the device did not answer in time); any other failure is an + // INTERNAL_ERROR. See sendRequest() for the timeout/correlation caveat. + Throwable cause = (ex instanceof CompletionException && ex.getCause() != null) + ? ex.getCause() : ex; + PlcResponseCode code = (cause instanceof TimeoutException) + ? PlcResponseCode.REMOTE_ERROR : PlcResponseCode.INTERNAL_ERROR; + items.put(e.getKey(), new DefaultPlcResponseItem<>(code, null)); + } + } + return (PlcReadResponse) new DefaultPlcReadResponse(request, items); + }); + } + + private CompletableFuture> readSingleTag(SlmpTag tag) { + SlmpReadRequest data = new SlmpReadRequest( + tag.getDeviceNumber(), tag.getDeviceCode(), tag.getNumberOfPoints()); + SlmpRequestFrame3E frame = new SlmpRequestFrame3E( + getConfiguration().getMonitoringTimer(), 0x0401, 0x0000, data); + return executeThrottled(() -> + sendRequest(frame).thenApply(response -> + SlmpResponseMapper.mapTag(tag, response.getEndCode(), response.getResponseData()))); + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpDataType.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpDataType.java new file mode 100644 index 0000000000..18908a7216 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpDataType.java @@ -0,0 +1,113 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.value.PlcValue; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.buffers.bytebased.ReadBufferByteBased; +import org.apache.plc4x.java.spi.buffers.bytebased.WithByteBasedOption; +import org.apache.plc4x.java.spi.values.PlcDINT; +import org.apache.plc4x.java.spi.values.PlcINT; +import org.apache.plc4x.java.spi.values.PlcList; +import org.apache.plc4x.java.spi.values.PlcREAL; +import org.apache.plc4x.java.spi.values.PlcUDINT; +import org.apache.plc4x.java.spi.values.PlcUINT; +import org.apache.plc4x.java.spi.values.PlcWORD; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Maps a supported PLC4X data type to its SLMP word footprint and the decode of + * little-endian response words into a {@link PlcValue}. The SLMP wire layer leaves + * {@code responseData} as raw bytes; this enum owns typed decoding (word units only). + */ +public enum SlmpDataType { + WORD(1), + INT(1), + UINT(1), + DINT(2), + UDINT(2), + REAL(2); + + private static final Logger LOGGER = LoggerFactory.getLogger(SlmpDataType.class); + + private final int wordsPerElement; + + SlmpDataType(int wordsPerElement) { + this.wordsPerElement = wordsPerElement; + } + + public int getWordsPerElement() { + return wordsPerElement; + } + + /** + * Decode {@code quantity} elements of this type from little-endian SLMP response bytes. + * Returns {@code null} when {@code responseData} is shorter than required (caller maps to INVALID_DATA). + */ + public PlcValue decode(byte[] responseData, int quantity) { + int requiredBytes = quantity * wordsPerElement * 2; + if (responseData == null || responseData.length < requiredBytes) { + return null; + } + ReadBufferByteBased buffer = new ReadBufferByteBased(responseData, + WithByteBasedOption.WithByteOrder("LITTLE_ENDIAN"), + WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), + WithOption.WithSignedIntegerEncoding("twos-complement"), + WithOption.WithFloatEncoding("IEEE754")); + try { + if (quantity == 1) { + return readOne(buffer); + } + List values = new ArrayList<>(quantity); + for (int i = 0; i < quantity; i++) { + values.add(readOne(buffer)); + } + return new PlcList(values); + } catch (BufferException e) { + LOGGER.warn("Failed to decode SLMP {} value", this, e); + return null; + } + } + + private PlcValue readOne(ReadBufferByteBased buffer) throws BufferException { + switch (this) { + case WORD: + // readUnsignedInt(16) returns int 0..65535; readUnsignedShort(16) returns a SIGNED short + // (negative for values > 0x7FFF), which would corrupt unsigned WORD/UINT. + return new PlcWORD(buffer.readUnsignedInt(16)); + case INT: + return new PlcINT(buffer.readSignedShort(16)); + case UINT: + return new PlcUINT(buffer.readUnsignedInt(16)); + case DINT: + return new PlcDINT(buffer.readSignedInt(32)); + case UDINT: + return new PlcUDINT(buffer.readUnsignedLong(32)); + case REAL: + return new PlcREAL(buffer.readFloat(32)); + default: + throw new BufferException("Unsupported SLMP data type " + this); + } + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpDriver.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpDriver.java new file mode 100644 index 0000000000..0dc9b87e9f --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpDriver.java @@ -0,0 +1,96 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.model.PlcTag; +import org.apache.plc4x.java.slmp.config.SlmpConfiguration; +import org.apache.plc4x.java.slmp.config.SlmpTcpTransportConfiguration; +import org.apache.plc4x.java.slmp.tag.SlmpTag; +import org.apache.plc4x.java.spi.config.Configuration; +import org.apache.plc4x.java.spi.drivers.ConnectionBase; +import org.apache.plc4x.java.spi.drivers.DriverBase; +import org.apache.plc4x.java.spi.transports.api.Transport; +import org.apache.plc4x.java.spi.transports.api.TransportInstance; +import org.apache.plc4x.java.spi.transports.api.config.TransportConfiguration; +import org.apache.plc4x.java.utils.auditlog.api.AuditLog; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class SlmpDriver extends DriverBase { + + @Override + public String getProtocolCode() { + return "slmp"; + } + + @Override + public String getProtocolName() { + return "SLMP (MELSEC) 3E"; + } + + @Override + protected Class getConfigurationClass() { + return SlmpConfiguration.class; + } + + @Override + protected Class getTransportConfigurationClass(Transport transport) { + if ("tcp".equals(transport.getTransportCode())) { + return SlmpTcpTransportConfiguration.class; + } + return super.getTransportConfigurationClass(transport); + } + + @Override + public Optional getDefaultTransportCode() { + return Optional.of("tcp"); + } + + @Override + public List getSupportedTransportCodes() { + return List.of("tcp", "test"); + } + + @Override + public Set defaultPorts(String transportCode) { + if ("tcp".equalsIgnoreCase(transportCode)) { + return Set.of(5007); + } + return Set.of(); + } + + @Override + public PlcTag prepareTag(String tagAddress) { + return SlmpTag.of(tagAddress); + } + + @Override + protected ConnectionBase getConnection(Configuration configuration, + TransportInstance transportInstance, + AuditLog auditLog) { + return new SlmpConnection((SlmpConfiguration) configuration, transportInstance, auditLog); + } + + @Override + protected boolean canRead() { + return true; + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpMessageCodec.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpMessageCodec.java new file mode 100644 index 0000000000..e7d5356a8b --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpMessageCodec.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.slmp.readwrite.SlmpMessage; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.buffers.bytebased.ReadBufferByteBased; +import org.apache.plc4x.java.spi.drivers.MessageCodecBase; +import org.apache.plc4x.java.spi.transports.api.TransportInstance; + +import java.util.function.Consumer; + +/** + * Frames {@link SlmpMessage} over a TCP byte stream using the 3E length field. + * The fixed 3E response header is 9 bytes; the 2-byte responseDataLength is + * little-endian at offset 7 (NB: modbus's MBAP length is big-endian — do not + * copy that read here). + */ +public class SlmpMessageCodec extends MessageCodecBase { + + private static final int SLMP_3E_HEADER_SIZE = 9; // subHeader(2) + accessRoute(5) + length(2) + + public SlmpMessageCodec(TransportInstance transportInstance, Consumer messageHandler) { + super("SLMP", transportInstance, messageHandler); + } + + @Override + protected int getMinimumHeaderSize() { + return SLMP_3E_HEADER_SIZE; + } + + @Override + protected int calculateTotalMessageSize(byte[] header, int availableBytes) { + // availableBytes is unused: the total size is fully determined by the length field. + // responseDataLength: little-endian uint16 at byte offset 7..8. + int length = (header[7] & 0xFF) | ((header[8] & 0xFF) << 8); + return SLMP_3E_HEADER_SIZE + length; + } + + @Override + protected SlmpMessage parseMessage(ReadBufferByteBased readBuffer) throws BufferException { + return SlmpMessage.staticParse(readBuffer); + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpResponseMapper.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpResponseMapper.java new file mode 100644 index 0000000000..72f9547fe5 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/SlmpResponseMapper.java @@ -0,0 +1,50 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.api.value.PlcValue; +import org.apache.plc4x.java.slmp.tag.SlmpTag; +import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcResponseItem; +import org.apache.plc4x.java.spi.drivers.messages.items.PlcResponseItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Maps a single 3E response (endCode + raw words) to a per-tag {@link PlcResponseItem}. */ +final class SlmpResponseMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(SlmpResponseMapper.class); + + private SlmpResponseMapper() { + } + + static PlcResponseItem mapTag(SlmpTag tag, int endCode, byte[] responseData) { + if (endCode != 0x0000) { + LOGGER.warn("SLMP device returned endCode {} for {}", String.format("0x%04X", endCode), tag); + return new DefaultPlcResponseItem<>(PlcResponseCode.REMOTE_ERROR, null); + } + PlcValue value = tag.getDataType().decode(responseData, tag.getQuantity()); + if (value == null) { + LOGGER.warn("SLMP response too short for {} ({} bytes)", tag, + responseData == null ? 0 : responseData.length); + return new DefaultPlcResponseItem<>(PlcResponseCode.INVALID_DATA, null); + } + return new DefaultPlcResponseItem<>(PlcResponseCode.OK, value); + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/config/SlmpConfiguration.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/config/SlmpConfiguration.java new file mode 100644 index 0000000000..10c1a6d494 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/config/SlmpConfiguration.java @@ -0,0 +1,59 @@ +/* + * 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 + * + * https://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.slmp.config; + +import org.apache.plc4x.java.spi.config.Configuration; +import org.apache.plc4x.java.spi.config.annotations.ConfigurationParameter; +import org.apache.plc4x.java.spi.config.annotations.Description; +import org.apache.plc4x.java.spi.config.annotations.defaults.IntDefaultValue; + +public class SlmpConfiguration implements Configuration { + + @ConfigurationParameter("monitoring-timer") + @IntDefaultValue(0x0000) + @Description("SLMP monitoring timer written into each 3E request frame (0 = wait infinitely).") + private int monitoringTimer; + + @ConfigurationParameter("request-timeout") + @IntDefaultValue(5_000) + @Description("Client-side timeout in milliseconds awaiting a response.") + private int requestTimeout; + + public int getMonitoringTimer() { + return monitoringTimer; + } + + public void setMonitoringTimer(int monitoringTimer) { + this.monitoringTimer = monitoringTimer; + } + + public int getRequestTimeout() { + return requestTimeout; + } + + public void setRequestTimeout(int requestTimeout) { + this.requestTimeout = requestTimeout; + } + + @Override + public String toString() { + return "SlmpConfiguration{monitoringTimer=" + monitoringTimer + + ", requestTimeout=" + requestTimeout + '}'; + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/config/SlmpTcpTransportConfiguration.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/config/SlmpTcpTransportConfiguration.java new file mode 100644 index 0000000000..a7eba56425 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/config/SlmpTcpTransportConfiguration.java @@ -0,0 +1,30 @@ +/* + * 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 + * + * https://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.slmp.config; + +import org.apache.plc4x.java.slmp.readwrite.Constants; +import org.apache.plc4x.java.transport.tcp.config.TcpTransportConfiguration; + +public class SlmpTcpTransportConfiguration extends TcpTransportConfiguration { + + @Override + public int getDefaultPort() { + return Constants.SLMPDEFAULTPORT; + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/tag/SlmpTag.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/tag/SlmpTag.java new file mode 100644 index 0000000000..946930eb41 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/tag/SlmpTag.java @@ -0,0 +1,196 @@ +/* + * 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 + * + * https://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.slmp.tag; + +import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException; +import org.apache.plc4x.java.api.model.ArrayInfo; +import org.apache.plc4x.java.api.model.PlcTag; +import org.apache.plc4x.java.api.types.PlcValueType; +import org.apache.plc4x.java.slmp.SlmpDataType; +import org.apache.plc4x.java.slmp.readwrite.SlmpDeviceCode; +import org.apache.plc4x.java.spi.drivers.model.DefaultArrayInfo; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A single SLMP word-device tag. v0 supports the word devices D (decimal addr), + * W (hex addr) and R (decimal addr); bit devices are rejected. Address grammar: + * {@code

[:][\[\]]}, e.g. {@code D350}, + * {@code R200:REAL[4]}, {@code W1A:WORD[10]} (W also accepts {@code W0x1A}). + */ +public class SlmpTag implements PlcTag, Serializable { + + public static final Pattern ADDRESS_PATTERN = Pattern.compile( + "^(?[A-Za-z]+)(?0[xX])?(?
[0-9A-Fa-f]+)" + + "(:(?[A-Za-z_]+))?(\\[(?\\d+)])?$"); + + /** Conservative single-frame word ceiling for 3E binary Batch Read (not the exact device max). */ + static final int MAX_POINTS = 960; + + /** Device addresses are serialized as an unsigned 24-bit field in the 3E frame. */ + static final int MAX_DEVICE_NUMBER = 0xFFFFFF; + + private final SlmpDeviceCode deviceCode; + private final int deviceNumber; + private final SlmpDataType dataType; + private final int quantity; + + public SlmpTag(SlmpDeviceCode deviceCode, int deviceNumber, SlmpDataType dataType, int quantity) { + this.deviceCode = deviceCode; + this.deviceNumber = deviceNumber; + this.dataType = dataType; + this.quantity = quantity; + } + + public static SlmpTag of(String addressString) { + Matcher matcher = ADDRESS_PATTERN.matcher(addressString); + if (!matcher.matches()) { + throw new PlcInvalidTagException("Unable to parse SLMP address: " + addressString); + } + String deviceToken = matcher.group("device").toUpperCase(); + SlmpDeviceCode device; + int radix; + switch (deviceToken) { + case "D": + device = SlmpDeviceCode.D; + radix = 10; + break; + case "R": + device = SlmpDeviceCode.R; + radix = 10; + break; + case "W": + device = SlmpDeviceCode.W; + radix = 16; + break; + default: + throw new PlcInvalidTagException( + "device '" + deviceToken + "' not supported in this version (word devices D/W/R only)"); + } + + boolean hasHexPrefix = matcher.group("hexPrefix") != null; + if (hasHexPrefix && radix != 16) { + throw new PlcInvalidTagException("0x prefix is only valid for hex devices (W): " + addressString); + } + int deviceNumber; + try { + deviceNumber = Integer.parseInt(matcher.group("address"), radix); + } catch (NumberFormatException e) { + throw new PlcInvalidTagException("Invalid " + (radix == 16 ? "hex" : "decimal") + + " device number in: " + addressString); + } + if (deviceNumber > MAX_DEVICE_NUMBER) { + throw new PlcInvalidTagException("device number " + deviceNumber + + " exceeds the 24-bit SLMP device-address range [0.." + MAX_DEVICE_NUMBER + "]: " + addressString); + } + + String datatypeToken = matcher.group("datatype"); + SlmpDataType dataType; + if (datatypeToken == null) { + dataType = SlmpDataType.WORD; + } else { + try { + dataType = SlmpDataType.valueOf(datatypeToken.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new PlcInvalidTagException("Unsupported SLMP data type '" + datatypeToken + + "' (supported: WORD, INT, UINT, DINT, UDINT, REAL)"); + } + } + + String quantityToken = matcher.group("quantity"); + int quantity; + if (quantityToken == null) { + quantity = 1; + } else { + try { + quantity = Integer.parseInt(quantityToken); + } catch (NumberFormatException e) { + throw new PlcInvalidTagException("quantity out of range in: " + addressString); + } + } + if (quantity < 1) { + throw new PlcInvalidTagException("quantity must be >= 1 in: " + addressString); + } + + long numberOfPoints = (long) quantity * dataType.getWordsPerElement(); + if (numberOfPoints > MAX_POINTS) { + throw new PlcInvalidTagException("requested " + numberOfPoints + " words exceeds the v0 single-frame " + + "Batch Read ceiling of " + MAX_POINTS + " (no optimizer to split): " + addressString); + } + return new SlmpTag(device, deviceNumber, dataType, quantity); + } + + public SlmpDeviceCode getDeviceCode() { + return deviceCode; + } + + public int getDeviceNumber() { + return deviceNumber; + } + + public SlmpDataType getDataType() { + return dataType; + } + + public int getQuantity() { + return quantity; + } + + /** Number of 16-bit words to request (Batch Read, word units). */ + public int getNumberOfPoints() { + return quantity * dataType.getWordsPerElement(); + } + + @Override + public String getAddressString() { + String addr = (deviceCode == SlmpDeviceCode.W) + ? "0x" + Integer.toHexString(deviceNumber).toUpperCase() + : Integer.toString(deviceNumber); + StringBuilder sb = new StringBuilder(deviceCode.name()).append(addr); + if (dataType != SlmpDataType.WORD || quantity != 1) { + sb.append(':').append(dataType.name()); + } + if (quantity != 1) { + sb.append('[').append(quantity).append(']'); + } + return sb.toString(); + } + + @Override + public PlcValueType getPlcValueType() { + return PlcValueType.valueOf(dataType.name()); + } + + @Override + public List getArrayInfo() { + if (quantity > 1) { + return Collections.singletonList(new DefaultArrayInfo(0, quantity - 1)); + } + return Collections.emptyList(); + } + + @Override + public String toString() { + return "SlmpTag{" + getAddressString() + '}'; + } +} diff --git a/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/tag/SlmpTagHandler.java b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/tag/SlmpTagHandler.java new file mode 100644 index 0000000000..6baace78c8 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/java/org/apache/plc4x/java/slmp/tag/SlmpTagHandler.java @@ -0,0 +1,36 @@ +/* + * 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 + * + * https://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.slmp.tag; + +import org.apache.plc4x.java.api.model.PlcQuery; +import org.apache.plc4x.java.api.model.PlcTag; +import org.apache.plc4x.java.spi.drivers.tags.PlcTagHandler; + +public class SlmpTagHandler implements PlcTagHandler { + + @Override + public PlcTag parseTag(String tagAddress) { + return SlmpTag.of(tagAddress); + } + + @Override + public PlcQuery parseQuery(String query) { + throw new UnsupportedOperationException("SLMP does not support queries"); + } +} diff --git a/plc4j/drivers/slmp/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver b/plc4j/drivers/slmp/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver new file mode 100644 index 0000000000..4c152da6e2 --- /dev/null +++ b/plc4j/drivers/slmp/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver @@ -0,0 +1,19 @@ +# +# 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 +# +# https://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. +# +org.apache.plc4x.java.slmp.SlmpDriver diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpConnectionFailurePathTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpConnectionFailurePathTest.java new file mode 100644 index 0000000000..736e61da13 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpConnectionFailurePathTest.java @@ -0,0 +1,284 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.slmp.config.SlmpConfiguration; +import org.apache.plc4x.java.slmp.readwrite.SlmpResponseFrame3E; +import org.apache.plc4x.java.spi.buffers.bytebased.WriteBufferByteBased; +import org.apache.plc4x.java.spi.transports.api.AsyncTransportInstance; +import org.apache.plc4x.java.spi.transports.api.config.TransportConfiguration; +import org.apache.plc4x.java.spi.transports.api.exceptions.TransportException; +import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; +import org.apache.plc4x.java.utils.auditlog.api.AuditLog; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Failure-path coverage for the single-slot request/response correlation in + * {@link SlmpConnection}: 3E frames carry no correlation id, so a timeout, a + * lost transport or an unsolicited frame must degrade exactly one tag (or + * nothing) instead of poisoning the connection. Mirrors the style of + * {@code ModbusRtuConnectionRequestChainTest}. + */ +class SlmpConnectionFailurePathTest { + + @Test + void timedOutTagIsIsolatedToRemoteErrorWhileOthersSucceed() throws Exception { + ScriptedAsyncTransport transport = new ScriptedAsyncTransport(); + SlmpConnection connection = newConnectedConnection(transport, 200); + + CompletableFuture responseFuture = connection.readRequestBuilder() + .addTagAddress("answered", "D350") + .addTagAddress("silent", "D351") + .build().execute(); + + // Sequential per-tag chain: only the first read may be on the wire. + awaitTrue(() -> transport.writeCount() == 1, 2, TimeUnit.SECONDS); + transport.deliver(responseFrame(0x0000, new byte[]{(byte) 0xAB, 0x56})); + transport.runDataListener(); + + // The second read dispatches once the first completes — and is never answered. + awaitTrue(() -> transport.writeCount() == 2, 2, TimeUnit.SECONDS); + + PlcReadResponse response = responseFuture.get(5, TimeUnit.SECONDS); + assertEquals(PlcResponseCode.OK, response.getResponseCode("answered")); + assertEquals(0x56AB, response.getPlcValue("answered").getInteger()); + assertEquals(PlcResponseCode.REMOTE_ERROR, response.getResponseCode("silent"), + "a device that does not answer in time must surface as REMOTE_ERROR for that tag only"); + } + + @Test + void transportDisconnectFailsThePendingTagAsInternalError() throws Exception { + ScriptedAsyncTransport transport = new ScriptedAsyncTransport(); + SlmpConnection connection = newConnectedConnection(transport, 5000); + + CompletableFuture responseFuture = connection.readRequestBuilder() + .addTagAddress("pending", "D350") + .build().execute(); + awaitTrue(() -> transport.writeCount() == 1, 2, TimeUnit.SECONDS); + + connection.onTransportDisconnected(new PlcRuntimeException("link down")); + + PlcReadResponse response = responseFuture.get(5, TimeUnit.SECONDS); + assertEquals(PlcResponseCode.INTERNAL_ERROR, response.getResponseCode("pending"), + "losing the transport must fail the in-flight tag instead of stranding its future"); + } + + @Test + void failedSendFailsOnlyThatTag() throws Exception { + ScriptedAsyncTransport transport = new ScriptedAsyncTransport(); + SlmpConnection connection = newConnectedConnection(transport, 5000); + + transport.failNextWrite(); + CompletableFuture responseFuture = connection.readRequestBuilder() + .addTagAddress("unsendable", "D350") + .build().execute(); + + PlcReadResponse response = responseFuture.get(5, TimeUnit.SECONDS); + assertEquals(PlcResponseCode.INTERNAL_ERROR, response.getResponseCode("unsendable")); + } + + @Test + void unsolicitedFrameIsIgnoredAndTheConnectionStaysUsable() throws Exception { + ScriptedAsyncTransport transport = new ScriptedAsyncTransport(); + SlmpConnection connection = newConnectedConnection(transport, 5000); + + // A frame nobody asked for must be dropped (logged), not blow up the receiver. + transport.deliver(responseFrame(0x0000, new byte[]{0x01, 0x02})); + transport.runDataListener(); + + // A regular read afterwards still works — the slot was not poisoned. + CompletableFuture responseFuture = connection.readRequestBuilder() + .addTagAddress("after", "D350") + .build().execute(); + awaitTrue(() -> transport.writeCount() == 1, 2, TimeUnit.SECONDS); + transport.deliver(responseFrame(0x0000, new byte[]{(byte) 0xAB, 0x56})); + transport.runDataListener(); + + PlcReadResponse response = responseFuture.get(5, TimeUnit.SECONDS); + assertEquals(PlcResponseCode.OK, response.getResponseCode("after")); + assertEquals(0x56AB, response.getPlcValue("after").getInteger()); + } + + @Test + void connectRejectsMonitoringTimerOutsideUnsigned16Range() { + SlmpConfiguration config = new SlmpConfiguration(); + config.setRequestTimeout(5000); + config.setMonitoringTimer(0x1_0000); // one past the uint16 ceiling serialized into the 3E frame + + SlmpConnection connection = newDisconnectedConnection(config); + assertThrows(PlcConnectionException.class, connection::connect, + "an out-of-range monitoring-timer must be rejected at connect, before it can be truncated on the wire"); + } + + @Test + void connectRejectsNonPositiveRequestTimeout() { + SlmpConfiguration config = new SlmpConfiguration(); + config.setRequestTimeout(0); // orTimeout(0) would time out every request immediately + config.setMonitoringTimer(0x0000); + + SlmpConnection connection = newDisconnectedConnection(config); + assertThrows(PlcConnectionException.class, connection::connect, + "a non-positive request-timeout must be rejected at connect rather than failing every read"); + } + + private static SlmpConnection newDisconnectedConnection(SlmpConfiguration config) { + AuditLog auditLog = mock(AuditLog.class); + when(auditLog.isEnabled()).thenReturn(false); + return new SlmpConnection(config, new ScriptedAsyncTransport(), auditLog); + } + + private static SlmpConnection newConnectedConnection(ScriptedAsyncTransport transport, int requestTimeoutMs) + throws Exception { + SlmpConfiguration config = new SlmpConfiguration(); + config.setRequestTimeout(requestTimeoutMs); + config.setMonitoringTimer(0x0000); + + AuditLog auditLog = mock(AuditLog.class); + when(auditLog.isEnabled()).thenReturn(false); + + SlmpConnection connection = new SlmpConnection(config, transport, auditLog); + connection.connect(); + return connection; + } + + private static byte[] responseFrame(int endCode, byte[] data) throws Exception { + SlmpResponseFrame3E frame = new SlmpResponseFrame3E(endCode, data); + WriteBufferByteBased writeBuffer = new WriteBufferByteBased(new byte[frame.getLengthInBytes()]); + frame.serialize(writeBuffer); + return writeBuffer.getBytes(); + } + + private static void awaitTrue(BooleanSupplier condition, long timeout, TimeUnit unit) throws InterruptedException { + long deadline = System.currentTimeMillis() + unit.toMillis(timeout); + while (!condition.getAsBoolean()) { + if (System.currentTimeMillis() > deadline) { + fail("condition not met within timeout"); + } + Thread.sleep(10); + } + } + + /** + * In-memory transport double implementing the async listener-registration + * contract {@code ConnectionBase.startReceiving} relies on. Bytes queued + * via {@link #deliver(byte[])} are consumed through the real + * TransportInstance read contract by the codec. + */ + static final class ScriptedAsyncTransport implements AsyncTransportInstance { + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private int readPosition; + private boolean open = true; + private final AtomicInteger writeCount = new AtomicInteger(); + private final AtomicBoolean failNextWrite = new AtomicBoolean(false); + private final AtomicReference dataListener = new AtomicReference<>(); + + void deliver(byte[] bytes) { + buffer.writeBytes(bytes); + } + + int writeCount() { + return writeCount.get(); + } + + void failNextWrite() { + failNextWrite.set(true); + } + + void runDataListener() { + Runnable listener = dataListener.get(); + if (listener == null) { + throw new IllegalStateException("No data listener registered"); + } + listener.run(); + } + + @Override + public TransportConfiguration getConfiguration() { + return null; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public int getNumBytesAvailable() { + return buffer.size() - readPosition; + } + + @Override + public byte[] peekReadableBytes(int numBytes) throws TransportException { + if (numBytes > getNumBytesAvailable()) { + throw new TransportException("peek beyond available: " + numBytes); + } + byte[] all = buffer.toByteArray(); + byte[] result = new byte[numBytes]; + System.arraycopy(all, readPosition, result, 0, numBytes); + return result; + } + + @Override + public byte[] read(int numBytes) throws TransportException { + byte[] result = peekReadableBytes(numBytes); + readPosition += numBytes; + return result; + } + + @Override + public void write(byte[] bytes) throws TransportException { + writeCount.incrementAndGet(); + if (failNextWrite.compareAndSet(true, false)) { + throw new TransportException("scripted write failure"); + } + } + + @Override + public void close() { + open = false; + } + + @Override + public void registerDataListener(Runnable listener) { + dataListener.set(listener); + } + + @Override + public void removeDataListener() { + dataListener.set(null); + } + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpDataTypeDecodeTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpDataTypeDecodeTest.java new file mode 100644 index 0000000000..46bfa96785 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpDataTypeDecodeTest.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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.value.PlcValue; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SlmpDataTypeDecodeTest { + + // Wire bytes are little-endian, words LSB-first. D350=0x56AB -> bytes ab 56. + private static byte[] hex(String s) { + byte[] out = new byte[s.length() / 2]; + for (int i = 0; i < out.length; i++) { + out[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16); + } + return out; + } + + @Test + void wordsPerElement() { + assertEquals(1, SlmpDataType.WORD.getWordsPerElement()); + assertEquals(1, SlmpDataType.INT.getWordsPerElement()); + assertEquals(1, SlmpDataType.UINT.getWordsPerElement()); + assertEquals(2, SlmpDataType.DINT.getWordsPerElement()); + assertEquals(2, SlmpDataType.UDINT.getWordsPerElement()); + assertEquals(2, SlmpDataType.REAL.getWordsPerElement()); + } + + @Test + void decodeSingleWordUnsigned() { + PlcValue v = SlmpDataType.WORD.decode(hex("ab56"), 1); + assertEquals(0x56AB, v.getInt()); + } + + @Test + void decodeWordHighBitStaysUnsigned() { + // 0xFFFF little-endian must decode to 65535 (unsigned), NOT -1. + PlcValue v = SlmpDataType.WORD.decode(hex("ffff"), 1); + assertEquals(65535, v.getInt()); + } + + @Test + void decodeSingleIntSigned() { + // 0xFFFF little-endian -> -1 as signed 16 + PlcValue v = SlmpDataType.INT.decode(hex("ffff"), 1); + assertEquals(-1, v.getInt()); + } + + @Test + void decodeDintTwoWordsLowWordFirst() { + // value 0x00010002 stored low-word-first: word0=0x0002 (bytes 02 00), word1=0x0001 (bytes 01 00) + PlcValue v = SlmpDataType.DINT.decode(hex("02000100"), 1); + assertEquals(0x00010002, v.getInt()); + } + + @Test + void decodeRealTwoWords() { + // 1.0f = 0x3F800000; little-endian bytes 00 00 80 3f + PlcValue v = SlmpDataType.REAL.decode(hex("0000803f"), 1); + assertEquals(1.0f, v.getFloat(), 0.0f); + } + + @Test + void decodeListWhenQuantityGreaterThanOne() { + // D350=0x56AB (ab 56), D351=0x170F (0f 17) + PlcValue v = SlmpDataType.WORD.decode(hex("ab560f17"), 2); + assertTrue(v.isList()); + assertEquals(0x56AB, v.getList().get(0).getInt()); + assertEquals(0x170F, v.getList().get(1).getInt()); + } + + @Test + void decodeReturnsNullOnShortResponse() { + // asking for 2 words but only 1 word of data + assertNull(SlmpDataType.WORD.decode(hex("ab56"), 2)); + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpDriverDriverTestsuiteIT.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpDriverDriverTestsuiteIT.java new file mode 100644 index 0000000000..5f2342b3b0 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpDriverDriverTestsuiteIT.java @@ -0,0 +1,28 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.utils.testutils.driver.DriverTestsuiteRunner; + +public class SlmpDriverDriverTestsuiteIT extends DriverTestsuiteRunner { + + public SlmpDriverDriverTestsuiteIT() { + super("/slmp/slmp-driver-testsuite.xml", "org.apache.plc4x.java.slmp.readwrite", false); + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpErrorMappingTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpErrorMappingTest.java new file mode 100644 index 0000000000..78b9de5c78 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpErrorMappingTest.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.slmp.tag.SlmpTag; +import org.apache.plc4x.java.spi.drivers.messages.items.PlcResponseItem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SlmpErrorMappingTest { + + private static byte[] hex(String s) { + byte[] out = new byte[s.length() / 2]; + for (int i = 0; i < out.length; i++) { + out[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16); + } + return out; + } + + @Test + void normalCompletionDecodesValue() { + SlmpTag tag = SlmpTag.of("D350:WORD[2]"); + var item = SlmpResponseMapper.mapTag(tag, 0x0000, hex("ab560f17")); + assertEquals(PlcResponseCode.OK, item.getResponseCode()); + assertEquals(0x56AB, item.getValue().getList().get(0).getInt()); + } + + @Test + void nonZeroEndCodeMapsToRemoteError() { + SlmpTag tag = SlmpTag.of("D350:WORD"); + var item = SlmpResponseMapper.mapTag(tag, 0xC059, new byte[0]); + assertEquals(PlcResponseCode.REMOTE_ERROR, item.getResponseCode()); + assertNull(item.getValue()); + } + + @Test + void shortResponseMapsToInvalidData() { + SlmpTag tag = SlmpTag.of("D350:WORD[2]"); // wants 2 words + var item = SlmpResponseMapper.mapTag(tag, 0x0000, hex("ab56")); // only 1 + assertEquals(PlcResponseCode.INVALID_DATA, item.getResponseCode()); + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpParserSerializerTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpParserSerializerTest.java new file mode 100644 index 0000000000..1a9a1f12de --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpParserSerializerTest.java @@ -0,0 +1,29 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.utils.testutils.parserserializer.ParserSerializerTestsuiteRunner; + +public class SlmpParserSerializerTest extends ParserSerializerTestsuiteRunner { + + public SlmpParserSerializerTest() { + super("/protocols/slmp/ParserSerializerTestsuite.xml"); + } + +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpRequestBuildTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpRequestBuildTest.java new file mode 100644 index 0000000000..1795d9e798 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/SlmpRequestBuildTest.java @@ -0,0 +1,52 @@ +/* + * 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 + * + * https://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.slmp; + +import org.apache.plc4x.java.slmp.readwrite.SlmpReadRequest; +import org.apache.plc4x.java.slmp.readwrite.SlmpRequestFrame3E; +import org.apache.plc4x.java.slmp.tag.SlmpTag; +import org.apache.plc4x.java.spi.buffers.bytebased.WriteBufferByteBased; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SlmpRequestBuildTest { + + private static String toHex(byte[] b) { + StringBuilder sb = new StringBuilder(); + for (byte x : b) { + sb.append(String.format("%02x", x)); + } + return sb.toString(); + } + + @Test + void buildsBatchReadFrameMatchingSh080008Example() throws Exception { + // SH-080008 section 8.1: read D350, 2 words. + SlmpTag tag = SlmpTag.of("D350:WORD[2]"); + SlmpReadRequest data = new SlmpReadRequest( + tag.getDeviceNumber(), tag.getDeviceCode(), tag.getNumberOfPoints()); + SlmpRequestFrame3E frame = new SlmpRequestFrame3E(0x0000, 0x0401, 0x0000, data); + + WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[frame.getLengthInBytes()]); + frame.serialize(buffer); + + assertEquals("500000ffff03000c000000010400005e0100a80200", toHex(buffer.getBytes())); + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/config/SlmpConfigurationTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/config/SlmpConfigurationTest.java new file mode 100644 index 0000000000..065da4f114 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/config/SlmpConfigurationTest.java @@ -0,0 +1,52 @@ +/* + * 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 + * + * https://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.slmp.config; + +import org.apache.plc4x.java.spi.config.ConfigurationFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SlmpConfigurationTest { + + @Test + void gettersAndSetters() { + SlmpConfiguration config = new SlmpConfiguration(); + config.setMonitoringTimer(4); + assertEquals(4, config.getMonitoringTimer()); + config.setRequestTimeout(2000); + assertEquals(2000, config.getRequestTimeout()); + } + + @Test + void defaultsAreApplied() { + SlmpConfiguration config = new ConfigurationFactory() + .createConfiguration(SlmpConfiguration.class, ""); + assertEquals(0x0000, config.getMonitoringTimer()); + assertEquals(5_000, config.getRequestTimeout()); + } + + @Test + void overridesAreParsed() { + SlmpConfiguration config = new ConfigurationFactory() + .createConfiguration(SlmpConfiguration.class, "monitoring-timer=4&request-timeout=2000"); + assertEquals(4, config.getMonitoringTimer()); + assertEquals(2_000, config.getRequestTimeout()); + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/tag/SlmpTagHandlerTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/tag/SlmpTagHandlerTest.java new file mode 100644 index 0000000000..47628313ef --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/tag/SlmpTagHandlerTest.java @@ -0,0 +1,38 @@ +/* + * 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 + * + * https://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.slmp.tag; + +import org.apache.plc4x.java.api.model.PlcTag; +import org.apache.plc4x.java.slmp.readwrite.SlmpDeviceCode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SlmpTagHandlerTest { + + @Test + void parseTagReturnsPopulatedSlmpTag() { + PlcTag tag = new SlmpTagHandler().parseTag("D350:DINT"); + assertInstanceOf(SlmpTag.class, tag); + SlmpTag slmp = (SlmpTag) tag; + assertEquals(SlmpDeviceCode.D, slmp.getDeviceCode()); + assertEquals(350, slmp.getDeviceNumber()); + assertEquals(2, slmp.getNumberOfPoints()); // DINT = 2 words + } +} diff --git a/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/tag/SlmpTagTest.java b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/tag/SlmpTagTest.java new file mode 100644 index 0000000000..fcfbf03005 --- /dev/null +++ b/plc4j/drivers/slmp/src/test/java/org/apache/plc4x/java/slmp/tag/SlmpTagTest.java @@ -0,0 +1,126 @@ +/* + * 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 + * + * https://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.slmp.tag; + +import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException; +import org.apache.plc4x.java.slmp.SlmpDataType; +import org.apache.plc4x.java.slmp.readwrite.SlmpDeviceCode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SlmpTagTest { + + @Test + void parsesDecimalDeviceWithDefaults() { + SlmpTag tag = SlmpTag.of("D350"); + assertEquals(SlmpDeviceCode.D, tag.getDeviceCode()); + assertEquals(350, tag.getDeviceNumber()); + assertEquals(SlmpDataType.WORD, tag.getDataType()); // default WORD + assertEquals(1, tag.getQuantity()); + assertEquals(1, tag.getNumberOfPoints()); + } + + @Test + void parsesDataTypeAndQuantity() { + SlmpTag tag = SlmpTag.of("R200:REAL[4]"); + assertEquals(SlmpDeviceCode.R, tag.getDeviceCode()); + assertEquals(200, tag.getDeviceNumber()); + assertEquals(SlmpDataType.REAL, tag.getDataType()); + assertEquals(4, tag.getQuantity()); + assertEquals(8, tag.getNumberOfPoints()); // 4 * 2 words + } + + @Test + void parsesHexLinkRegisterBareForm() { + SlmpTag tag = SlmpTag.of("W1A:WORD[10]"); + assertEquals(SlmpDeviceCode.W, tag.getDeviceCode()); + assertEquals(0x1A, tag.getDeviceNumber()); // W is hex + assertEquals(10, tag.getQuantity()); + } + + @Test + void parsesHexLinkRegisterExplicit0xForm() { + assertEquals(0x1A, SlmpTag.of("W0x1A").getDeviceNumber()); + assertEquals(0x1A, SlmpTag.of("W0X1A").getDeviceNumber()); + } + + @Test + void parses0xPrefixCombinedWithDatatypeAndQuantity() { + // the documented example form: 0x prefix + datatype + quantity in one address + SlmpTag tag = SlmpTag.of("W0x1A:WORD[10]"); + assertEquals(SlmpDeviceCode.W, tag.getDeviceCode()); + assertEquals(0x1A, tag.getDeviceNumber()); + assertEquals(SlmpDataType.WORD, tag.getDataType()); + assertEquals(10, tag.getQuantity()); + } + + @Test + void canonicalAddressStringRoundTrips() { + assertEquals("D350:INT[2]", SlmpTag.of("D350:INT[2]").getAddressString()); + assertEquals("W0x1A", SlmpTag.of("W1A").getAddressString()); // W prints 0x hex + } + + @Test + void rejectsUnsupportedBitDevice() { + PlcInvalidTagException ex = assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("M100")); + assertTrue(ex.getMessage().contains("M")); + } + + @Test + void rejects0xPrefixOnDecimalDevice() { + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D0x10")); + } + + @Test + void rejectsHexDigitsInDecimalDevice() { + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D1A")); + } + + @Test + void rejectsZeroQuantity() { + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D100:WORD[0]")); + } + + @Test + void rejectsOverCeiling() { + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D0:WORD[961]")); + } + + @Test + void rejectsQuantityOverflowingInt() { + // a quantity beyond Integer.MAX_VALUE must surface as PlcInvalidTagException, + // consistent with the device-number parse, not a raw NumberFormatException + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D0:WORD[999999999999]")); + } + + @Test + void rejectsDeviceNumberExceeding24Bit() { + // device addresses occupy a 24-bit field on the wire; an in-int but out-of-range value + // must be rejected at parse time, not leak a BufferException during serialization + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D16777216")); // 0x1000000, one past the 24-bit max + } + + @Test + void rejectsQuantityWhoseWordCountOverflowsInt() { + // quantity * wordsPerElement must not overflow int and slip past the MAX_POINTS ceiling: + // 2147483647 * 2 words wraps negative in int arithmetic, so the tag would otherwise be accepted + assertThrows(PlcInvalidTagException.class, () -> SlmpTag.of("D0:REAL[2147483647]")); + } +} diff --git a/plc4j/drivers/slmp/src/test/resources/slmp/slmp-driver-testsuite.xml b/plc4j/drivers/slmp/src/test/resources/slmp/slmp-driver-testsuite.xml new file mode 100644 index 0000000000..03d224c60e --- /dev/null +++ b/plc4j/drivers/slmp/src/test/resources/slmp/slmp-driver-testsuite.xml @@ -0,0 +1,70 @@ + + + + SLMP 3E Batch Read + slmp + read-write + slmp + + + transport + test + + + monitoring-timer + 0 + + + true + + Batch Read D350, 2 words + + + + + + value +
D350:WORD[2]
+
+
+
+
+ 500000ffff03000c000000010400005e0100a80200 + d00000ffff030006000000ab560f17 + + + + + + OK + + + 22187 + 5903 + + + + + + + +
+
+
diff --git a/website/asciidoc/modules/users/nav.adoc b/website/asciidoc/modules/users/nav.adoc index 5616166f9f..919658db78 100644 --- a/website/asciidoc/modules/users/nav.adoc +++ b/website/asciidoc/modules/users/nav.adoc @@ -55,6 +55,7 @@ *** xref:protocols/profinet.adoc[] *** xref:protocols/s7.adoc[] *** xref:protocols/simulated.adoc[] +*** xref:protocols/slmp.adoc[] *** xref:protocols/umas.adoc[] ** xref:transports/index.adoc[] diff --git a/website/asciidoc/modules/users/pages/protocols/index.adoc b/website/asciidoc/modules/users/pages/protocols/index.adoc index 089393ecdc..c16fffed18 100644 --- a/website/asciidoc/modules/users/pages/protocols/index.adoc +++ b/website/asciidoc/modules/users/pages/protocols/index.adoc @@ -156,6 +156,13 @@ |icon:check[role="green"] |icon:times[role="red"] +|SLMP (MELSEC) +|icon:times[role="red"] +|icon:times[role="red"] +|icon:times[role="red"] +|icon:exclamation[role="yellow"] +|icon:times[role="red"] + |UMAS |icon:times[role="red"] |icon:times[role="red"] @@ -391,6 +398,17 @@ The following table contains a list of operations and the protocols that support |icon:question[role="red"] |icon:question[role="red"] +|SLMP (MELSEC) +|icon:times[role="red"] +|icon:times[role="red"] +|icon:check[role="green"] +|icon:check[role="green"] +|icon:times[role="red"] +|icon:times[role="red"] +|icon:times[role="red"] +|icon:times[role="red"] +|icon:times[role="red"] + |UMAS |icon:question[role="red"] |icon:check[role="green"] diff --git a/website/asciidoc/modules/users/pages/protocols/slmp.adoc b/website/asciidoc/modules/users/pages/protocols/slmp.adoc new file mode 100644 index 0000000000..51257c8b39 --- /dev/null +++ b/website/asciidoc/modules/users/pages/protocols/slmp.adoc @@ -0,0 +1,116 @@ +// +// 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 +// +// https://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. +// +:imagesdir: ../../images/users/protocols +:icons: font + += SLMP (Mitsubishi MELSEC) + +SLMP (Seamless Message Protocol, also known as MELSEC Communication Protocol / MC protocol) +is used by Mitsubishi Electric MELSEC PLCs such as the iQ-R, iQ-F, Q and L series. + +The driver communicates using binary 3E frames over TCP. + +This initial version is *read-only* and supports reading word devices (D, W and R). + +(Supported by Plc4J Only) + +== Connection String Options + +[cols="2,2a,2a,2a,4a"] +|=== +|Name |Type |Default Value |Required |Description +|Name 4+|SLMP +|Code 4+|`slmp` +|Default Transport 4+|`tcp` +|Default Port 4+|`5007` +|Supported Transports 4+| + - `tcp` +5+|Config options: +|`monitoring-timer` |INT |0| |SLMP monitoring timer written into each 3E request frame (0 = wait infinitely). +|`request-timeout` |INT |5000| |Client-side timeout in milliseconds awaiting a response. +|=== + +== Supported Operations + +[cols="2,2a,5a"] +|=== +|Name |Value |Description + +3+|Supported Operations + +| +2+| `read` +|=== + +== Individual Resource Address Format + +=== Connection String + +SLMP has the following connection string format: +---- +slmp:{transport}://{ip-address}:{port}?{options} +---- +An example connection string would look like: +---- +slmp:tcp://192.168.0.10:5007 +---- +Note the transport, port and option fields are optional. + +=== General Format + +In general all SLMP addresses have this format: + +---- +{device}{address}:{data-type}[{quantity}] +---- + +If the data-type part is omitted, it defaults to `WORD`. +If the quantity part is omitted, the default of `1` is assumed. + +Examples: + +- `D350` (one word from data register 350) +- `D350:DINT` (one 32-bit integer, occupying two words) +- `R200:REAL[4]` (four floats from file register 200) +- `W0x1A:WORD[10]` (ten words from link register 0x1A) + +=== Devices + +The following word devices are supported: + +- `D` (data register, decimal address) +- `R` (file register, decimal address) +- `W` (link register, hexadecimal address; an optional `0x` prefix is accepted) + +The `0x` prefix is only valid for hexadecimally addressed devices. + +A single read request may cover at most 960 words (a conservative single-frame ceiling); +larger requests are rejected instead of being split. + +=== Data Types + +The following data types are supported: + +- WORD (uint 16, default) +- INT (int 16) +- UINT (uint 16) +- DINT (int 32) +- UDINT (uint 32) +- REAL (float 32) + +Multi-word types (DINT, UDINT, REAL) are decoded from consecutive words in +little-endian (low-word-first) order, as transmitted by the PLC.