This is an automated email from the ASF dual-hosted git repository.
cdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new 76e025d9e4 feat: Initial version of a new Java UMAS driver.
76e025d9e4 is described below
commit 76e025d9e4908e7fa5f24e2ff462f3f3db062339
Author: Christofer Dutz <[email protected]>
AuthorDate: Wed Apr 1 12:25:46 2026 +0200
feat: Initial version of a new Java UMAS driver.
---
RELEASE_NOTES | 1 +
plc4j/drivers/umas/pom.xml | 6 +-
.../plc4x/java/umas/readwrite/UmasDriver.java | 158 ++++
.../umas/readwrite/UmasFunctionKeyTracker.java | 56 ++
.../readwrite/configuration/UmasConfiguration.java | 76 ++
.../UmasTcpTransportConfiguration.java | 31 +
.../umas/readwrite/context/UmasDriverContext.java | 216 +++++
.../umas/readwrite/protocol/UmasProtocolLogic.java | 893 +++++++++++++++++++++
.../java/umas/readwrite/tag/SymbolicUmasTag.java | 102 +++
.../plc4x/java/umas/readwrite/tag/UmasTag.java | 42 +
.../java/umas/readwrite/tag/UmasTagHandler.java | 42 +
.../services/org.apache.plc4x.java.api.PlcDriver | 1 +
.../plc4x/java/umas/manual/ManualUmasBrowse.java | 70 ++
.../umas/src/test/resources/logback-test.xml | 36 +
14 files changed, 1725 insertions(+), 5 deletions(-)
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 73c3ffadbc..704970b275 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -11,6 +11,7 @@ New Features
providing a min time interval to prevent excessive
notifications.
- Added a new PlcCertificateAuthentication to the API module.
+- Initial version of a new Java UMAS driver.
Incompatible changes
--------------------
diff --git a/plc4j/drivers/umas/pom.xml b/plc4j/drivers/umas/pom.xml
index 1c75641eed..40a11ba78e 100644
--- a/plc4j/drivers/umas/pom.xml
+++ b/plc4j/drivers/umas/pom.xml
@@ -100,7 +100,7 @@
<version>0.14.0-SNAPSHOT</version>
</dependency>
- <!--dependency>
+ <dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-transport-tcp</artifactId>
<version>0.14.0-SNAPSHOT</version>
@@ -110,10 +110,6 @@
<groupId>io.netty</groupId>
<artifactId>netty-buffer</artifactId>
</dependency>
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-transport</artifactId>
- </dependency-->
<dependency>
<groupId>org.slf4j</groupId>
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/UmasDriver.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/UmasDriver.java
new file mode 100644
index 0000000000..e45fe27695
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/UmasDriver.java
@@ -0,0 +1,158 @@
+/*
+ * 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.umas.readwrite;
+
+import io.netty.buffer.ByteBuf;
+import org.apache.plc4x.java.api.model.PlcTag;
+import org.apache.plc4x.java.spi.configuration.PlcConnectionConfiguration;
+import org.apache.plc4x.java.spi.configuration.PlcTransportConfiguration;
+import org.apache.plc4x.java.spi.connection.GeneratedDriverBase;
+import org.apache.plc4x.java.spi.connection.ProtocolStackConfigurer;
+import org.apache.plc4x.java.spi.connection.SingleProtocolStackConfigurer;
+import org.apache.plc4x.java.spi.generation.ReadBufferByteBased;
+import org.apache.plc4x.java.spi.optimizer.BaseOptimizer;
+import org.apache.plc4x.java.spi.optimizer.SingleTagOptimizer;
+import org.apache.plc4x.java.umas.readwrite.configuration.UmasConfiguration;
+import
org.apache.plc4x.java.umas.readwrite.configuration.UmasTcpTransportConfiguration;
+import org.apache.plc4x.java.umas.readwrite.context.UmasDriverContext;
+import org.apache.plc4x.java.umas.readwrite.protocol.UmasProtocolLogic;
+import org.apache.plc4x.java.umas.readwrite.tag.SymbolicUmasTag;
+import org.apache.plc4x.java.umas.readwrite.tag.UmasTag;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.ToIntFunction;
+
+/**
+ * PLC4J driver for the UMAS protocol (Schneider Electric Modicon PLCs).
+ * UMAS is tunneled inside Modbus/TCP using function code 0x5A.
+ * Connection URL format: {@code umas:tcp://host:port?unit-identifier=0}
+ */
+public class UmasDriver extends GeneratedDriverBase<ModbusTcpADU> {
+
+ @Override
+ public String getProtocolCode() {
+ return "umas";
+ }
+
+ @Override
+ public String getProtocolName() {
+ return "UMAS (Schneider Electric)";
+ }
+
+ @Override
+ protected Class<? extends PlcConnectionConfiguration>
getConfigurationClass() {
+ return UmasConfiguration.class;
+ }
+
+ @Override
+ protected Optional<Class<? extends PlcTransportConfiguration>>
getTransportConfigurationClass(String transportCode) {
+ if ("tcp".equals(transportCode)) {
+ return Optional.of(UmasTcpTransportConfiguration.class);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ protected Optional<String> getDefaultTransportCode() {
+ return Optional.of("tcp");
+ }
+
+ @Override
+ protected List<String> getSupportedTransportCodes() {
+ return Collections.singletonList("tcp");
+ }
+
+ @Override
+ protected boolean awaitSetupComplete() {
+ return true;
+ }
+
+ @Override
+ protected boolean awaitDisconnectComplete() {
+ return true;
+ }
+
+ @Override
+ protected boolean canPing() {
+ return true;
+ }
+
+ @Override
+ protected boolean canRead() {
+ return true;
+ }
+
+ @Override
+ protected boolean canWrite() {
+ return true;
+ }
+
+ @Override
+ protected boolean canBrowse() {
+ return true;
+ }
+
+ @Override
+ protected BaseOptimizer getOptimizer() {
+ return new SingleTagOptimizer();
+ }
+
+ @Override
+ protected ProtocolStackConfigurer<ModbusTcpADU> getStackConfigurer() {
+ return SingleProtocolStackConfigurer.builder(
+ ModbusTcpADU.class,
+ (io) -> {
+ // UMAS responses use function key 0xFE and need the
original request's
+ // function key for type discrimination. Peek at the
transaction ID from
+ // the MBAP header (first 2 bytes, big-endian) without
advancing the
+ // read position, then look up the tracked function key.
+ byte[] header = ((ReadBufferByteBased) io).getBytes(0, 2);
+ int transactionId = ((header[0] & 0xFF) << 8) | (header[1]
& 0xFF);
+ short fk =
UmasFunctionKeyTracker.consumeFunctionKey(transactionId);
+ return (ModbusTcpADU) ModbusTcpADU.staticParse(io, fk);
+ })
+ .withProtocol(UmasProtocolLogic.class)
+ .withDriverContext(UmasDriverContext.class)
+ .withPacketSizeEstimator(ByteLengthEstimator.class)
+ .build();
+ }
+
+ @Override
+ public PlcTag prepareTag(String tagAddress) {
+ return SymbolicUmasTag.of(tagAddress);
+ }
+
+ /**
+ * Estimates packet length from the Modbus/TCP MBAP header.
+ * Header layout: transactionId(2) + protocolId(2) + length(2) + unitId(1)
= 7 bytes.
+ * Total size = 6 + length field value.
+ */
+ public static class ByteLengthEstimator implements ToIntFunction<ByteBuf> {
+ @Override
+ public int applyAsInt(ByteBuf byteBuf) {
+ if (byteBuf.readableBytes() >= 6) {
+ return byteBuf.getUnsignedShort(byteBuf.readerIndex() + 4) + 6;
+ }
+ return -1;
+ }
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/UmasFunctionKeyTracker.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/UmasFunctionKeyTracker.java
new file mode 100644
index 0000000000..02ded4cedc
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/UmasFunctionKeyTracker.java
@@ -0,0 +1,56 @@
+/*
+ * 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.umas.readwrite;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks UMAS request function keys for response discrimination during
parsing.
+ * <p>
+ * UMAS responses use a generic function key (0xFE) and require the original
+ * request's function key to determine the correct response subtype. The
protocol
+ * logic records the function key before sending each request, and the parser
+ * retrieves it by peeking at the transaction ID from the MBAP header bytes.
+ */
+public class UmasFunctionKeyTracker {
+
+ private static final Map<Integer, Short> PENDING_FUNCTION_KEYS = new
ConcurrentHashMap<>();
+
+ /**
+ * Records that a request with the given transaction ID used the specified
+ * UMAS function key. Called by the protocol logic before sending a
request.
+ */
+ public static void trackRequest(int transactionId, short functionKey) {
+ PENDING_FUNCTION_KEYS.put(transactionId, functionKey);
+ }
+
+ /**
+ * Retrieves and removes the function key for the given transaction ID.
+ * Called by the parser lambda after peeking the transaction ID from the
+ * raw MBAP header bytes.
+ *
+ * @return the function key, or 0 if not tracked (e.g. unsolicited message)
+ */
+ public static short consumeFunctionKey(int transactionId) {
+ Short fk = PENDING_FUNCTION_KEYS.remove(transactionId);
+ return fk != null ? fk : (short) 0;
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/configuration/UmasConfiguration.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/configuration/UmasConfiguration.java
new file mode 100644
index 0000000000..6657512932
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/configuration/UmasConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * 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.umas.readwrite.configuration;
+
+import org.apache.plc4x.java.spi.configuration.PlcConnectionConfiguration;
+import
org.apache.plc4x.java.spi.configuration.annotations.ConfigurationParameter;
+import org.apache.plc4x.java.spi.configuration.annotations.Description;
+import
org.apache.plc4x.java.spi.configuration.annotations.defaults.IntDefaultValue;
+
+public class UmasConfiguration implements PlcConnectionConfiguration {
+
+ @ConfigurationParameter("unit-identifier")
+ @IntDefaultValue(0)
+ @Description("Modbus unit identifier (slave address). UMAS typically uses
0.")
+ private int unitIdentifier;
+
+ @ConfigurationParameter("request-timeout")
+ @IntDefaultValue(4000)
+ @Description("Timeout in milliseconds for UMAS requests.")
+ private int requestTimeout;
+
+ @ConfigurationParameter("max-frame-size")
+ @IntDefaultValue(65535)
+ @Description("Maximum UMAS frame size. The PLC reports its actual limit
during InitComms.")
+ private int maxFrameSize;
+
+ public int getUnitIdentifier() {
+ return unitIdentifier;
+ }
+
+ public void setUnitIdentifier(int unitIdentifier) {
+ this.unitIdentifier = unitIdentifier;
+ }
+
+ public int getRequestTimeout() {
+ return requestTimeout;
+ }
+
+ public void setRequestTimeout(int requestTimeout) {
+ this.requestTimeout = requestTimeout;
+ }
+
+ public int getMaxFrameSize() {
+ return maxFrameSize;
+ }
+
+ public void setMaxFrameSize(int maxFrameSize) {
+ this.maxFrameSize = maxFrameSize;
+ }
+
+ @Override
+ public String toString() {
+ return "UmasConfiguration{" +
+ "unitIdentifier=" + unitIdentifier +
+ ", requestTimeout=" + requestTimeout +
+ ", maxFrameSize=" + maxFrameSize +
+ '}';
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/configuration/UmasTcpTransportConfiguration.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/configuration/UmasTcpTransportConfiguration.java
new file mode 100644
index 0000000000..207eb7dc50
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/configuration/UmasTcpTransportConfiguration.java
@@ -0,0 +1,31 @@
+/*
+ * 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.umas.readwrite.configuration;
+
+import org.apache.plc4x.java.umas.readwrite.Constants;
+import org.apache.plc4x.java.transport.tcp.DefaultTcpTransportConfiguration;
+
+public class UmasTcpTransportConfiguration extends
DefaultTcpTransportConfiguration {
+
+ @Override
+ public int getDefaultPort() {
+ return Constants.UMASTCPDEFAULTPORT;
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/context/UmasDriverContext.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/context/UmasDriverContext.java
new file mode 100644
index 0000000000..aa85ee5dc7
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/context/UmasDriverContext.java
@@ -0,0 +1,216 @@
+/*
+ * 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.umas.readwrite.context;
+
+import org.apache.plc4x.java.spi.context.DriverContext;
+import org.apache.plc4x.java.umas.readwrite.UmasArrayDimension;
+import org.apache.plc4x.java.umas.readwrite.UmasDataType;
+import org.apache.plc4x.java.umas.readwrite.UmasUDTDefinition;
+import org.apache.plc4x.java.umas.readwrite.UmasUnlocatedVariableReference;
+import org.apache.plc4x.java.umas.readwrite.configuration.UmasConfiguration;
+import org.apache.plc4x.java.spi.configuration.HasConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Maintains the runtime context for a UMAS driver connection.
+ * Stores connection-specific state populated during the handshake
+ * and used for subsequent read/write/browse operations.
+ */
+public class UmasDriverContext implements DriverContext,
HasConfiguration<UmasConfiguration> {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(UmasDriverContext.class);
+
+ private UmasConfiguration configuration;
+
+ // Modbus transaction ID counter, wraps at 0xFFFF
+ private final AtomicInteger transactionIdGenerator = new AtomicInteger(1);
+
+ // PLC identification (populated during PlcIdent handshake step)
+ private volatile String plcHostname;
+ private volatile int plcModel;
+ private volatile int plcFirmwareVersion;
+
+ // Negotiated protocol parameters
+ private volatile int maxFrameSize = 65535;
+ private volatile short pairingKey;
+ private volatile long hardwareId;
+ private volatile long projectCrc;
+
+ // Custom type definitions: type index -> field definitions
+ private final Map<Integer, List<UmasUDTDefinition>> customTypeFields = new
ConcurrentHashMap<>();
+ private final Map<Integer, String> customTypeNames = new
ConcurrentHashMap<>();
+ private final Map<Integer, Integer> customTypeElementTypeIds = new
ConcurrentHashMap<>();
+ private final Map<Integer, List<UmasArrayDimension>> customTypeDimensions
= new ConcurrentHashMap<>();
+
+ // Symbol table: symbolic name -> variable reference
+ private final Map<String, UmasUnlocatedVariableReference> symbolTable =
new ConcurrentHashMap<>();
+
+ // Data type table: type ID -> UmasDataType
+ private final Map<Integer, UmasDataType> dataTypeTable = new
ConcurrentHashMap<>();
+
+ @Override
+ public void setConfiguration(UmasConfiguration configuration) {
+ this.configuration = configuration;
+ this.maxFrameSize = configuration.getMaxFrameSize();
+ }
+
+ public UmasConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public int getNextTransactionId() {
+ int id = transactionIdGenerator.getAndIncrement();
+ if (id > 0xFFFF) {
+ transactionIdGenerator.compareAndSet(id + 1, 1);
+ return id & 0xFFFF;
+ }
+ return id;
+ }
+
+ // --- PLC identification ---
+
+ public String getPlcHostname() {
+ return plcHostname;
+ }
+
+ public void setPlcHostname(String plcHostname) {
+ this.plcHostname = plcHostname;
+ }
+
+ public int getPlcModel() {
+ return plcModel;
+ }
+
+ public void setPlcModel(int plcModel) {
+ this.plcModel = plcModel;
+ }
+
+ public int getPlcFirmwareVersion() {
+ return plcFirmwareVersion;
+ }
+
+ public void setPlcFirmwareVersion(int plcFirmwareVersion) {
+ this.plcFirmwareVersion = plcFirmwareVersion;
+ }
+
+ // --- Negotiated parameters ---
+
+ public int getMaxFrameSize() {
+ return maxFrameSize;
+ }
+
+ public void setMaxFrameSize(int maxFrameSize) {
+ this.maxFrameSize = maxFrameSize;
+ }
+
+ public short getPairingKey() {
+ return pairingKey;
+ }
+
+ public void setPairingKey(short pairingKey) {
+ this.pairingKey = pairingKey;
+ }
+
+ public long getHardwareId() {
+ return hardwareId;
+ }
+
+ public void setHardwareId(long hardwareId) {
+ this.hardwareId = hardwareId;
+ }
+
+ public long getProjectCrc() {
+ return projectCrc;
+ }
+
+ public void setProjectCrc(long projectCrc) {
+ this.projectCrc = projectCrc;
+ }
+
+ // --- Custom type operations ---
+
+ public void addCustomType(int typeIndex, String typeName,
List<UmasUDTDefinition> fields) {
+ customTypeNames.put(typeIndex, typeName);
+ customTypeFields.put(typeIndex, fields);
+ }
+
+ public void addArrayType(int typeIndex, String typeName, int
elementTypeId, List<UmasArrayDimension> dimensions) {
+ customTypeNames.put(typeIndex, typeName);
+ customTypeFields.put(typeIndex, Collections.emptyList());
+ customTypeElementTypeIds.put(typeIndex, elementTypeId);
+ customTypeDimensions.put(typeIndex, dimensions);
+ }
+
+ public Optional<Integer> getArrayElementTypeId(int typeIndex) {
+ return Optional.ofNullable(customTypeElementTypeIds.get(typeIndex));
+ }
+
+ public Optional<List<UmasArrayDimension>> getArrayDimensions(int
typeIndex) {
+ return Optional.ofNullable(customTypeDimensions.get(typeIndex));
+ }
+
+ public Optional<List<UmasUDTDefinition>> getCustomTypeFields(int
typeIndex) {
+ return Optional.ofNullable(customTypeFields.get(typeIndex));
+ }
+
+ public Optional<String> getCustomTypeName(int typeIndex) {
+ return Optional.ofNullable(customTypeNames.get(typeIndex));
+ }
+
+ // --- Symbol table operations ---
+
+ public void addSymbol(String name, UmasUnlocatedVariableReference
reference) {
+ symbolTable.put(name.toLowerCase(), reference);
+ }
+
+ public Optional<UmasUnlocatedVariableReference> getSymbol(String name) {
+ return Optional.ofNullable(symbolTable.get(name.toLowerCase()));
+ }
+
+ public Map<String, UmasUnlocatedVariableReference> getSymbolTable() {
+ return Collections.unmodifiableMap(symbolTable);
+ }
+
+ public int getSymbolCount() {
+ return symbolTable.size();
+ }
+
+ // --- Data type table operations ---
+
+ public void addDataType(int typeId, UmasDataType dataType) {
+ dataTypeTable.put(typeId, dataType);
+ }
+
+ public Optional<UmasDataType> getDataType(int typeId) {
+ return Optional.ofNullable(dataTypeTable.get(typeId));
+ }
+
+ public Map<Integer, UmasDataType> getDataTypeTable() {
+ return Collections.unmodifiableMap(dataTypeTable);
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/protocol/UmasProtocolLogic.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/protocol/UmasProtocolLogic.java
new file mode 100644
index 0000000000..4514d99e8b
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/protocol/UmasProtocolLogic.java
@@ -0,0 +1,893 @@
+/*
+ * 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.umas.readwrite.protocol;
+
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.model.ArrayInfo;
+import org.apache.plc4x.java.api.model.PlcTag;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.api.types.PlcValueType;
+import org.apache.plc4x.java.api.value.PlcValue;
+import org.apache.plc4x.java.spi.ConversationContext;
+import org.apache.plc4x.java.spi.Plc4xProtocolBase;
+import org.apache.plc4x.java.spi.configuration.HasConfiguration;
+import org.apache.plc4x.java.spi.connection.PlcTagHandler;
+import org.apache.plc4x.java.spi.generation.ByteOrder;
+import org.apache.plc4x.java.spi.generation.ReadBuffer;
+import org.apache.plc4x.java.spi.generation.ReadBufferByteBased;
+import org.apache.plc4x.java.spi.messages.*;
+import org.apache.plc4x.java.spi.messages.utils.DefaultPlcResponseItem;
+import org.apache.plc4x.java.spi.messages.utils.PlcResponseItem;
+import org.apache.plc4x.java.spi.model.DefaultArrayInfo;
+import org.apache.plc4x.java.spi.transaction.RequestTransactionManager;
+import org.apache.plc4x.java.spi.values.PlcRawByteArray;
+import org.apache.plc4x.java.umas.readwrite.*;
+import org.apache.plc4x.java.umas.readwrite.UmasFunctionKeyTracker;
+import org.apache.plc4x.java.umas.readwrite.configuration.UmasConfiguration;
+import org.apache.plc4x.java.umas.readwrite.context.UmasDriverContext;
+import org.apache.plc4x.java.umas.readwrite.tag.SymbolicUmasTag;
+import org.apache.plc4x.java.umas.readwrite.tag.UmasTag;
+import org.apache.plc4x.java.umas.readwrite.tag.UmasTagHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Protocol logic for the UMAS driver.
+ * Handles connection handshake, read, write, browse, and ping operations.
+ */
+public class UmasProtocolLogic extends Plc4xProtocolBase<ModbusTcpADU>
implements HasConfiguration<UmasConfiguration> {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(UmasProtocolLogic.class);
+
+ private static final int CUSTOM_TYPE_THRESHOLD = 0x1A;
+ private static final int RECORD_TYPE_DD02 = 0xDD02;
+ private static final int RECORD_TYPE_DD03 = 0xDD03;
+ private static final int SYMBOL_TABLE_BLOCK = 0xFFFF;
+
+ private UmasConfiguration configuration;
+ private UmasDriverContext umasDriverContext;
+ private RequestTransactionManager tm;
+ private Duration requestTimeout;
+ private short unitIdentifier;
+
+ @Override
+ public void setConfiguration(UmasConfiguration configuration) {
+ this.configuration = configuration;
+ this.requestTimeout =
Duration.ofMillis(configuration.getRequestTimeout());
+ this.unitIdentifier = (short) configuration.getUnitIdentifier();
+ this.tm = new RequestTransactionManager(1);
+ }
+
+ @Override
+ public void
setDriverContext(org.apache.plc4x.java.spi.context.DriverContext driverContext)
{
+ super.setDriverContext(driverContext);
+ this.umasDriverContext = (UmasDriverContext) driverContext;
+ }
+
+ @Override
+ public PlcTagHandler getTagHandler() {
+ return new UmasTagHandler();
+ }
+
+ @Override
+ public void close(ConversationContext<ModbusTcpADU> context) {
+ if (tm != null) {
+ tm.shutdown();
+ }
+ }
+
+ // ========================================================================
+ // Connection handshake (fully async — must not block the Netty event loop)
+ // ========================================================================
+
+ @Override
+ public void onConnect(ConversationContext<ModbusTcpADU> context) {
+ // Chain all handshake steps asynchronously to avoid blocking the
Netty event loop.
+ performPlcIdentAsync(context)
+ .thenCompose(v -> performInitCommsAsync(context))
+ .thenCompose(v -> performRepeatAsync(context))
+ .thenCompose(v -> performReadMemoryBlockAsync(context, 0x30, 0,
33))
+ .thenCompose(v -> performProjectInfoAsync(context, (short) 1))
+ .thenCompose(v -> performReadMemoryBlockAsync(context, 0x13, 0,
33))
+ .thenCompose(v -> performProjectInfoAsync(context, (short) 0))
+ .thenCompose(v -> performProjectInfoAsync(context, (short) 4))
+ .thenCompose(v -> performProjectInfoAsync(context, (short) 1))
+ .thenCompose(v -> performProjectInfoAsync(context, (short) 3))
+ .thenAccept(v -> {
+ LOGGER.info("UMAS connection established to PLC: hostname={},
model={}, firmware={}",
+ umasDriverContext.getPlcHostname(),
umasDriverContext.getPlcModel(),
+ umasDriverContext.getPlcFirmwareVersion());
+ context.fireConnected();
+ })
+ .exceptionally(e -> {
+ LOGGER.error("UMAS handshake failed", e);
+ context.getChannel().close();
+ return null;
+ });
+ }
+
+ @Override
+ public void onDisconnect(ConversationContext<ModbusTcpADU> context) {
+ context.fireDisconnected();
+ }
+
+ private CompletableFuture<Void>
performPlcIdentAsync(ConversationContext<ModbusTcpADU> context) {
+ return sendAsyncRequest(context, new UmasPDUPlcIdentRequest((short)
0), "PlcIdent")
+ .thenAccept(response -> {
+ if (response instanceof UmasPDUPlcIdentResponse identResponse)
{
+
umasDriverContext.setPlcHostname(identResponse.getHostname());
+ umasDriverContext.setPlcModel(identResponse.getModel());
+
umasDriverContext.setPlcFirmwareVersion(identResponse.getComVersion());
+ LOGGER.info("PlcIdent: hostname={}, model={},
comVersion={}",
+ identResponse.getHostname(), identResponse.getModel(),
identResponse.getComVersion());
+ } else {
+ throw new PlcRuntimeException("PlcIdent: unexpected
response type: " + response.getClass().getSimpleName());
+ }
+ });
+ }
+
+ private CompletableFuture<Void>
performInitCommsAsync(ConversationContext<ModbusTcpADU> context) {
+ return sendAsyncRequest(context, new UmasInitCommsRequest((short) 0,
(short) 0x00), "InitComms")
+ .thenAccept(response -> {
+ if (response instanceof UmasInitCommsResponse initResponse) {
+
umasDriverContext.setMaxFrameSize(initResponse.getMaxFrameSize());
+
umasDriverContext.setPlcFirmwareVersion(initResponse.getFirmwareVersion());
+ LOGGER.info("InitComms: maxFrameSize={},
firmwareVersion={}",
+ initResponse.getMaxFrameSize(),
initResponse.getFirmwareVersion());
+ } else {
+ throw new PlcRuntimeException("InitComms: unexpected
response type: " + response.getClass().getSimpleName());
+ }
+ });
+ }
+
+ private CompletableFuture<Void>
performRepeatAsync(ConversationContext<ModbusTcpADU> context) {
+ int echoSize = umasDriverContext.getMaxFrameSize() - 3;
+ byte[] echoData = new byte[echoSize];
+ java.util.Arrays.fill(echoData, 1, echoSize, (byte) 0x54);
+ return sendAsyncRequest(context, new UmasPDURepeatRequest((short) 0,
echoData), "Repeat")
+ .thenAccept(response -> {
+ if (response instanceof UmasPDURepeatResponse repeatResponse) {
+ LOGGER.info("Repeat: echo OK, {} bytes returned",
+ repeatResponse.getBlock() != null ?
repeatResponse.getBlock().length : 0);
+ } else {
+ throw new PlcRuntimeException("Repeat: unexpected response
type: " + response.getClass().getSimpleName());
+ }
+ });
+ }
+
+ private CompletableFuture<Void>
performReadMemoryBlockAsync(ConversationContext<ModbusTcpADU> context, int
blockNumber, int offset, int numberOfBytes) {
+ String stepName = "ReadMemoryBlock(0x" + String.format("%02X",
blockNumber) + ")";
+ UmasPDUItem request = new UmasPDUReadMemoryBlockRequest(
+ (short) 0, (short) 0x01, blockNumber, offset, 0, numberOfBytes);
+ return sendAsyncRequest(context, request, stepName)
+ .thenAccept(response -> {
+ if (response instanceof UmasPDUReadMemoryBlockResponse
readResponse) {
+ byte[] block = readResponse.getBlock();
+ LOGGER.info("{}: {} bytes received", stepName, block !=
null ? block.length : 0);
+ if (blockNumber == 0x30 && block != null && block.length
>= 9) {
+ long hardwareId = (block[5] & 0xFFL) | ((block[6] &
0xFFL) << 8)
+ | ((block[7] & 0xFFL) << 16) | ((block[8] & 0xFFL)
<< 24);
+ umasDriverContext.setHardwareId(hardwareId);
+ LOGGER.info("{}: extracted hardwareId=0x{}", stepName,
String.format("%08X", hardwareId));
+ }
+ } else {
+ LOGGER.warn("{}: unexpected response type: {}", stepName,
response.getClass().getSimpleName());
+ }
+ });
+ }
+
+ private CompletableFuture<Void>
performProjectInfoAsync(ConversationContext<ModbusTcpADU> context, short
subcode) {
+ return sendAsyncRequest(context, new UmasPDUProjectInfoRequest((short)
0, subcode), "ProjectInfo(subcode=" + subcode + ")")
+ .thenAccept(response -> {
+ if (response instanceof UmasPDUProjectInfoResponse
projectInfoResponse) {
+ byte[] block = projectInfoResponse.getBlock();
+ LOGGER.info("ProjectInfo(subcode={}): {} bytes", subcode,
block != null ? block.length : 0);
+ } else {
+ LOGGER.warn("ProjectInfo(subcode={}): unexpected response
type: {}", subcode, response.getClass().getSimpleName());
+ }
+ });
+ }
+
+ // ========================================================================
+ // Async request/response helper (non-blocking, safe for Netty event loop)
+ // ========================================================================
+
+ private CompletableFuture<UmasPDUItem>
sendAsyncRequest(ConversationContext<ModbusTcpADU> context, UmasPDUItem item,
String stepName) {
+ int transactionId = umasDriverContext.getNextTransactionId();
+ ModbusTcpADU request = buildModbusTcpADU(transactionId, item);
+
+ CompletableFuture<UmasPDUItem> future = new CompletableFuture<>();
+ context.sendRequest(request)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(e -> future.completeExceptionally(new
PlcConnectionException(stepName + " timed out")))
+ .onError((p, e) -> future.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ try {
+ future.complete(extractUmasResponse(p, stepName));
+ } catch (PlcConnectionException e) {
+ future.completeExceptionally(e);
+ }
+ });
+
+ return future;
+ }
+
+ // ========================================================================
+ // Ping
+ // ========================================================================
+
+ @Override
+ public CompletableFuture<PlcPingResponse> ping(PlcPingRequest pingRequest)
{
+ CompletableFuture<PlcPingResponse> future = new CompletableFuture<>();
+ int transactionId = umasDriverContext.getNextTransactionId();
+ UmasPDUItem statusRequest = new
UmasPDUPlcStatusRequest(umasDriverContext.getPairingKey());
+ ModbusTcpADU request = buildModbusTcpADU(transactionId, statusRequest);
+
+
+ RequestTransactionManager.RequestTransaction transaction =
tm.startRequest();
+ transaction.submit(() -> conversationContext.sendRequest(request)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(future::completeExceptionally)
+ .onError((p, e) -> future.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ transaction.endRequest();
+ future.complete(new DefaultPlcPingResponse(pingRequest,
PlcResponseCode.OK));
+ }));
+ return future;
+ }
+
+ // ========================================================================
+ // Read
+ // ========================================================================
+
+ @Override
+ public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest)
{
+ CompletableFuture<PlcReadResponse> future = new CompletableFuture<>();
+ DefaultPlcReadRequest request = (DefaultPlcReadRequest) readRequest;
+
+ // Process tags sequentially via the transaction manager
+ CompletableFuture.supplyAsync(() -> {
+ Map<String, PlcResponseItem<PlcValue>> responseItems = new
LinkedHashMap<>();
+ for (String tagName : request.getTagNames()) {
+ PlcTag tag = request.getTag(tagName);
+ responseItems.put(tagName, readSingleTag(tagName, tag));
+ }
+ return new DefaultPlcReadResponse(request, responseItems);
+ }).whenComplete((response, throwable) -> {
+ if (throwable != null) {
+ future.completeExceptionally(throwable);
+ } else {
+ future.complete(response);
+ }
+ });
+
+ return future;
+ }
+
+ private PlcResponseItem<PlcValue> readSingleTag(String tagName, PlcTag
tag) {
+ if (!(tag instanceof SymbolicUmasTag symbolicTag)) {
+ LOGGER.warn("Read tag '{}' is not a SymbolicUmasTag: {}", tagName,
tag.getClass().getSimpleName());
+ return new
DefaultPlcResponseItem<>(PlcResponseCode.INVALID_ADDRESS, null);
+ }
+
+ String symbolicAddress =
symbolicTag.getSymbolicAddress().toLowerCase();
+ Optional<UmasUnlocatedVariableReference> symbolOpt =
umasDriverContext.getSymbol(symbolicAddress);
+ if (symbolOpt.isEmpty()) {
+ LOGGER.warn("Read tag '{}': symbol '{}' not found in symbol
table", tagName, symbolicAddress);
+ return new DefaultPlcResponseItem<>(PlcResponseCode.NOT_FOUND,
null);
+ }
+
+ UmasUnlocatedVariableReference symbol = symbolOpt.get();
+
+ try {
+ VariableReadRequestReference readRef = buildReadReference(symbol);
+
+ int transactionId = umasDriverContext.getNextTransactionId();
+ UmasPDUItem readReq = new UmasPDUReadVariableRequest(
+ umasDriverContext.getPairingKey(),
+ umasDriverContext.getProjectCrc(),
+ (short) 1,
+ List.of(readRef));
+
+ ModbusTcpADU modbusTcpADU = buildModbusTcpADU(transactionId,
readReq);
+
+ CompletableFuture<ModbusTcpADU> responseFuture = new
CompletableFuture<>();
+ RequestTransactionManager.RequestTransaction transaction =
tm.startRequest();
+ transaction.submit(() ->
conversationContext.sendRequest(modbusTcpADU)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(responseFuture::completeExceptionally)
+ .onError((p, e) -> responseFuture.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ transaction.endRequest();
+ responseFuture.complete(p);
+ }));
+
+ ModbusTcpADU response =
responseFuture.get(configuration.getRequestTimeout() + 1000,
TimeUnit.MILLISECONDS);
+ UmasPDUItem responseItem = extractUmasResponse(response,
"ReadVariable(" + tagName + ")");
+
+ if (responseItem instanceof UmasPDUReadVariableResponse
readResponse) {
+ PlcValue value = parseReadResponse(symbol,
readResponse.getBlock());
+ return new DefaultPlcResponseItem<>(PlcResponseCode.OK, value);
+ } else if (responseItem instanceof UmasPDUErrorResponse) {
+ return new
DefaultPlcResponseItem<>(PlcResponseCode.REMOTE_ERROR, null);
+ } else {
+ return new
DefaultPlcResponseItem<>(PlcResponseCode.INTERNAL_ERROR, null);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Read tag '{}' failed: {}", tagName, e.getMessage());
+ return new DefaultPlcResponseItem<>(PlcResponseCode.REMOTE_ERROR,
null);
+ }
+ }
+
+ private VariableReadRequestReference
buildReadReference(UmasUnlocatedVariableReference symbol) {
+ int dataTypeId = symbol.getDataType();
+ byte dataSizeIndex;
+ if (UmasDataType.isDefined((short) dataTypeId)) {
+ UmasDataType umasType = UmasDataType.enumForValue((short)
dataTypeId);
+ dataSizeIndex = (byte) umasType.getRequestSize();
+ } else {
+ dataSizeIndex = (byte) 3;
+ }
+ return new VariableReadRequestReference(
+ (byte) 0, dataSizeIndex, symbol.getBlock(),
+ (int) symbol.getOffset(), (short) 0, null);
+ }
+
+ private PlcValue parseReadResponse(UmasUnlocatedVariableReference symbol,
byte[] block) throws Exception {
+ if (block == null || block.length == 0) {
+ throw new PlcConnectionException("Read response has empty data
block");
+ }
+ int dataTypeId = symbol.getDataType();
+ if (UmasDataType.isDefined((short) dataTypeId)) {
+ UmasDataType umasType = UmasDataType.enumForValue((short)
dataTypeId);
+ ReadBuffer readBuffer = new ReadBufferByteBased(block,
ByteOrder.LITTLE_ENDIAN);
+ return DataItem.staticParse(readBuffer, umasType, 1);
+ }
+ return new PlcRawByteArray(block);
+ }
+
+ // ========================================================================
+ // Write
+ // ========================================================================
+
+ @Override
+ public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest
writeRequest) {
+ CompletableFuture<PlcWriteResponse> future = new CompletableFuture<>();
+ DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest;
+
+ CompletableFuture.supplyAsync(() -> {
+ Map<String, PlcResponseCode> responseCodes = new LinkedHashMap<>();
+ for (String tagName : request.getTagNames()) {
+ PlcTag tag = request.getTag(tagName);
+ PlcValue value = request.getPlcValue(tagName);
+ responseCodes.put(tagName, writeSingleTag(tagName, tag,
value));
+ }
+ return new DefaultPlcWriteResponse(request, responseCodes);
+ }).whenComplete((response, throwable) -> {
+ if (throwable != null) {
+ future.completeExceptionally(throwable);
+ } else {
+ future.complete(response);
+ }
+ });
+
+ return future;
+ }
+
+ private PlcResponseCode writeSingleTag(String tagName, PlcTag tag,
PlcValue value) {
+ if (!(tag instanceof SymbolicUmasTag symbolicTag)) {
+ return PlcResponseCode.INVALID_ADDRESS;
+ }
+
+ String symbolicAddress =
symbolicTag.getSymbolicAddress().toLowerCase();
+ Optional<UmasUnlocatedVariableReference> symbolOpt =
umasDriverContext.getSymbol(symbolicAddress);
+ if (symbolOpt.isEmpty()) {
+ return PlcResponseCode.NOT_FOUND;
+ }
+
+ UmasUnlocatedVariableReference symbol = symbolOpt.get();
+
+ try {
+ byte[] serializedData = serializeValue(symbol, value);
+ VariableWriteRequestReference writeRef =
buildWriteReference(symbol, serializedData);
+
+ int transactionId = umasDriverContext.getNextTransactionId();
+ UmasPDUItem writeReq = new UmasPDUWriteVariableRequest(
+ umasDriverContext.getPairingKey(),
+ umasDriverContext.getProjectCrc(),
+ (short) 1,
+ List.of(writeRef));
+
+ ModbusTcpADU modbusTcpADU = buildModbusTcpADU(transactionId,
writeReq);
+
+ CompletableFuture<ModbusTcpADU> responseFuture = new
CompletableFuture<>();
+ RequestTransactionManager.RequestTransaction transaction =
tm.startRequest();
+ transaction.submit(() ->
conversationContext.sendRequest(modbusTcpADU)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(responseFuture::completeExceptionally)
+ .onError((p, e) -> responseFuture.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ transaction.endRequest();
+ responseFuture.complete(p);
+ }));
+
+ ModbusTcpADU response =
responseFuture.get(configuration.getRequestTimeout() + 1000,
TimeUnit.MILLISECONDS);
+ UmasPDUItem responseItem = extractUmasResponse(response,
"WriteVariable(" + tagName + ")");
+
+ if (responseItem instanceof UmasPDUWriteVariableResponse) {
+ return PlcResponseCode.OK;
+ } else if (responseItem instanceof UmasPDUErrorResponse) {
+ return PlcResponseCode.REMOTE_ERROR;
+ } else {
+ return PlcResponseCode.INTERNAL_ERROR;
+ }
+ } catch (Exception e) {
+ LOGGER.error("Write tag '{}' failed: {}", tagName, e.getMessage());
+ return PlcResponseCode.REMOTE_ERROR;
+ }
+ }
+
+ private VariableWriteRequestReference
buildWriteReference(UmasUnlocatedVariableReference symbol, byte[] data) {
+ int dataTypeId = symbol.getDataType();
+ byte dataSizeIndex;
+ if (UmasDataType.isDefined((short) dataTypeId)) {
+ UmasDataType umasType = UmasDataType.enumForValue((short)
dataTypeId);
+ dataSizeIndex = (byte) umasType.getDataTypeSize();
+ } else {
+ dataSizeIndex = (byte) data.length;
+ }
+ return new VariableWriteRequestReference(
+ (byte) 0, dataSizeIndex, symbol.getBlock(),
+ (int) symbol.getOffset(), 0, null, data);
+ }
+
+ private byte[] serializeValue(UmasUnlocatedVariableReference symbol,
PlcValue value) throws PlcConnectionException {
+ int dataTypeId = symbol.getDataType();
+ if (!UmasDataType.isDefined((short) dataTypeId)) {
+ if (value.getRaw() != null) {
+ return value.getRaw();
+ }
+ throw new PlcConnectionException("Cannot serialize value for
unknown data type: " + dataTypeId);
+ }
+
+ UmasDataType umasType = UmasDataType.enumForValue((short) dataTypeId);
+ return serializeForType(umasType, value);
+ }
+
+ private static byte[] serializeForType(UmasDataType umasType, PlcValue
value) {
+ return switch (umasType) {
+ case BOOL, EBOOL, UNKNOWN2, UNKNOWN3 ->
+ new byte[]{(byte) (value.getBoolean() ? 1 : 0)};
+ case BYTE, UNKNOWN11, UNKNOWN12, UNKNOWN13, UNKNOWN17, UNKNOWN18,
UNKNOWN19, UNKNOWN20, UNKNOWN24 ->
+ new byte[]{value.getByte()};
+ case INT -> {
+ ByteBuffer buf =
ByteBuffer.allocate(2).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putShort(value.getShort());
+ yield buf.array();
+ }
+ case UINT -> {
+ ByteBuffer buf =
ByteBuffer.allocate(2).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putShort((short) (value.getInteger() & 0xFFFF));
+ yield buf.array();
+ }
+ case DINT -> {
+ ByteBuffer buf =
ByteBuffer.allocate(4).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putInt(value.getInteger());
+ yield buf.array();
+ }
+ case UDINT -> {
+ ByteBuffer buf =
ByteBuffer.allocate(4).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putInt((int) (value.getLong() & 0xFFFFFFFFL));
+ yield buf.array();
+ }
+ case REAL -> {
+ ByteBuffer buf =
ByteBuffer.allocate(4).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putFloat(value.getFloat());
+ yield buf.array();
+ }
+ case STRING -> {
+ byte[] strBytes =
value.getString().getBytes(StandardCharsets.US_ASCII);
+ byte[] result = new byte[strBytes.length + 1];
+ System.arraycopy(strBytes, 0, result, 0, strBytes.length);
+ yield result;
+ }
+ case TIME, DATE, TOD -> {
+ ByteBuffer buf =
ByteBuffer.allocate(4).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putInt((int) (value.getLong() & 0xFFFFFFFFL));
+ yield buf.array();
+ }
+ case DATE_AND_TIME -> {
+ ByteBuffer buf =
ByteBuffer.allocate(8).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putLong(value.getLong());
+ yield buf.array();
+ }
+ case WORD -> {
+ ByteBuffer buf =
ByteBuffer.allocate(2).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putShort((short) (value.getInteger() & 0xFFFF));
+ yield buf.array();
+ }
+ case DWORD -> {
+ ByteBuffer buf =
ByteBuffer.allocate(4).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ buf.putInt((int) (value.getLong() & 0xFFFFFFFFL));
+ yield buf.array();
+ }
+ };
+ }
+
+ // ========================================================================
+ // Browse
+ // ========================================================================
+
+ @Override
+ public CompletableFuture<PlcBrowseResponse> browse(PlcBrowseRequest
browseRequest) {
+ CompletableFuture<PlcBrowseResponse> future = new
CompletableFuture<>();
+
+ CompletableFuture.supplyAsync(() -> {
+ Map<String, PlcResponseCode> responseCodes = new LinkedHashMap<>();
+ Map<String, List<PlcBrowseItem>> values = new LinkedHashMap<>();
+
+ for (String queryName : browseRequest.getQueryNames()) {
+ try {
+ List<PlcBrowseItem> items = executeBrowse();
+ responseCodes.put(queryName, PlcResponseCode.OK);
+ values.put(queryName, items);
+ } catch (Exception e) {
+ LOGGER.error("Browse query '{}' failed: {}", queryName,
e.getMessage());
+ responseCodes.put(queryName, PlcResponseCode.REMOTE_ERROR);
+ values.put(queryName, Collections.emptyList());
+ }
+ }
+ return new DefaultPlcBrowseResponse(browseRequest, responseCodes,
values);
+ }).whenComplete((response, throwable) -> {
+ if (throwable != null) {
+ future.completeExceptionally(throwable);
+ } else {
+ future.complete(response);
+ }
+ });
+
+ return future;
+ }
+
+ @Override
+ public CompletableFuture<PlcBrowseResponse>
browseWithInterceptor(PlcBrowseRequest browseRequest,
PlcBrowseRequestInterceptor interceptor) {
+ CompletableFuture<PlcBrowseResponse> future = new
CompletableFuture<>();
+
+ CompletableFuture.supplyAsync(() -> {
+ Map<String, PlcResponseCode> responseCodes = new LinkedHashMap<>();
+ Map<String, List<PlcBrowseItem>> values = new LinkedHashMap<>();
+
+ for (String queryName : browseRequest.getQueryNames()) {
+ try {
+ List<PlcBrowseItem> items = executeBrowse();
+ // Deliver each item through the interceptor
+ if (interceptor != null) {
+ for (PlcBrowseItem item : items) {
+ interceptor.intercept(queryName,
browseRequest.getQuery(queryName), item);
+ }
+ }
+ responseCodes.put(queryName, PlcResponseCode.OK);
+ values.put(queryName, items);
+ } catch (Exception e) {
+ LOGGER.error("Browse query '{}' failed: {}", queryName,
e.getMessage());
+ responseCodes.put(queryName, PlcResponseCode.REMOTE_ERROR);
+ values.put(queryName, Collections.emptyList());
+ }
+ }
+ return new DefaultPlcBrowseResponse(browseRequest, responseCodes,
values);
+ }).whenComplete((response, throwable) -> {
+ if (throwable != null) {
+ future.completeExceptionally(throwable);
+ } else {
+ future.complete(response);
+ }
+ });
+
+ return future;
+ }
+
+ private List<PlcBrowseItem> executeBrowse() throws Exception {
+ // Phase 1: Download datatype names
+ List<UmasDatatypeReference> datatypeRefs = downloadDatatypeNames();
+ LOGGER.info("Browse: downloaded {} datatype references",
datatypeRefs.size());
+
+ // Phase 2: Resolve custom types
+ resolveCustomTypes(datatypeRefs);
+
+ // Phase 3: Download symbol table
+ List<UmasUnlocatedVariableReference> symbols = downloadSymbolTable();
+ LOGGER.info("Browse: downloaded {} symbols", symbols.size());
+
+ for (UmasUnlocatedVariableReference symbol : symbols) {
+ umasDriverContext.addSymbol(symbol.getValue(), symbol);
+ }
+
+ return convertToBrowseItems(symbols);
+ }
+
+ private List<UmasDatatypeReference> downloadDatatypeNames() throws
Exception {
+ int transactionId = umasDriverContext.getNextTransactionId();
+ UmasPDUItem request = new UmasPDUReadUnlocatedVariableNamesRequest(
+ umasDriverContext.getPairingKey(), RECORD_TYPE_DD03, (short) 0x03,
+ umasDriverContext.getHardwareId(), 0, 0, null);
+
+ ModbusTcpADU modbusTcpADU = buildModbusTcpADU(transactionId, request);
+
+ CompletableFuture<ModbusTcpADU> responseFuture = new
CompletableFuture<>();
+ RequestTransactionManager.RequestTransaction transaction =
tm.startRequest();
+ transaction.submit(() -> conversationContext.sendRequest(modbusTcpADU)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(responseFuture::completeExceptionally)
+ .onError((p, e) -> responseFuture.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ transaction.endRequest();
+ responseFuture.complete(p);
+ }));
+
+ ModbusTcpADU response =
responseFuture.get(configuration.getRequestTimeout() + 1000,
TimeUnit.MILLISECONDS);
+ UmasPDUItem responseItem = extractUmasResponse(response,
"BrowseDatatypeNames");
+
+ if (!(responseItem instanceof UmasPDUReadUnlocatedVariableResponse
unlocatedResponse)) {
+ throw new PlcConnectionException("BrowseDatatypeNames: unexpected
response: " + responseItem.getClass().getSimpleName());
+ }
+
+ byte[] block = unlocatedResponse.getBlock();
+ if (block == null || block.length == 0) {
+ return Collections.emptyList();
+ }
+
+ ReadBuffer readBuffer = new ReadBufferByteBased(block,
ByteOrder.LITTLE_ENDIAN);
+ UmasPDUReadDatatypeNamesResponse parsed =
UmasPDUReadDatatypeNamesResponse.staticParse(readBuffer);
+ return parsed.getRecords();
+ }
+
+ private void resolveCustomTypes(List<UmasDatatypeReference> datatypeRefs)
throws Exception {
+ for (int i = 0; i < datatypeRefs.size(); i++) {
+ UmasDatatypeReference ref = datatypeRefs.get(i);
+ int typeId = CUSTOM_TYPE_THRESHOLD + i;
+ short primitiveId = ref.getDataType();
+ if (UmasDataType.isDefined(primitiveId)) {
+ umasDriverContext.addDataType(typeId,
UmasDataType.enumForValue(primitiveId));
+ }
+ }
+
+ for (int i = 0; i < datatypeRefs.size(); i++) {
+ UmasDatatypeReference ref = datatypeRefs.get(i);
+ int typeId = CUSTOM_TYPE_THRESHOLD + i;
+ if (ref.getClassIdentifier() != 0) {
+ resolveCustomType(typeId, ref);
+ }
+ }
+ }
+
+ private void resolveCustomType(int typeIndex, UmasDatatypeReference ref)
throws Exception {
+ int transactionId = umasDriverContext.getNextTransactionId();
+ UmasPDUItem request = new UmasPDUReadUnlocatedVariableNamesRequest(
+ umasDriverContext.getPairingKey(), RECORD_TYPE_DD02, (short) 0x03,
+ umasDriverContext.getHardwareId(), typeIndex, 0, 0);
+
+ ModbusTcpADU modbusTcpADU = buildModbusTcpADU(transactionId, request);
+
+ CompletableFuture<ModbusTcpADU> responseFuture = new
CompletableFuture<>();
+ RequestTransactionManager.RequestTransaction transaction =
tm.startRequest();
+ transaction.submit(() -> conversationContext.sendRequest(modbusTcpADU)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(responseFuture::completeExceptionally)
+ .onError((p, e) -> responseFuture.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ transaction.endRequest();
+ responseFuture.complete(p);
+ }));
+
+ ModbusTcpADU response =
responseFuture.get(configuration.getRequestTimeout() + 1000,
TimeUnit.MILLISECONDS);
+ UmasPDUItem responseItem = extractUmasResponse(response,
"ResolveType(" + ref.getValue() + ")");
+
+ if (!(responseItem instanceof UmasPDUReadUnlocatedVariableResponse
unlocatedResponse)) {
+ LOGGER.warn("ResolveType({}): unexpected response: {}",
ref.getValue(), responseItem.getClass().getSimpleName());
+ return;
+ }
+
+ byte[] block = unlocatedResponse.getBlock();
+ if (block == null || block.length < 2) {
+ return;
+ }
+
+ parseCustomTypeBlock(typeIndex, ref, block);
+ }
+
+ private void parseCustomTypeBlock(int typeIndex, UmasDatatypeReference
ref, byte[] block) throws Exception {
+ int classId = block[0] & 0xFF;
+
+ if (classId == 0x04) {
+ ReadBuffer readBuffer = new ReadBufferByteBased(block,
ByteOrder.LITTLE_ENDIAN);
+ UmasArrayTypeDefinition arrayDef =
UmasArrayTypeDefinition.staticParse(readBuffer);
+ umasDriverContext.addArrayType(typeIndex, ref.getValue(),
+ arrayDef.getElementTypeId(), arrayDef.getDimensions());
+ } else {
+ ReadBuffer readBuffer = new ReadBufferByteBased(block,
ByteOrder.LITTLE_ENDIAN);
+ UmasPDUReadUmasUDTDefinitionResponse udtResponse =
+ UmasPDUReadUmasUDTDefinitionResponse.staticParse(readBuffer);
+ umasDriverContext.addCustomType(typeIndex, ref.getValue(),
udtResponse.getRecords());
+ }
+ }
+
+ private List<UmasUnlocatedVariableReference> downloadSymbolTable() throws
Exception {
+ int transactionId = umasDriverContext.getNextTransactionId();
+ UmasPDUItem request = new UmasPDUReadUnlocatedVariableNamesRequest(
+ umasDriverContext.getPairingKey(), RECORD_TYPE_DD02, (short) 0x03,
+ umasDriverContext.getHardwareId(), SYMBOL_TABLE_BLOCK, 0, 0);
+
+ ModbusTcpADU modbusTcpADU = buildModbusTcpADU(transactionId, request);
+
+ CompletableFuture<ModbusTcpADU> responseFuture = new
CompletableFuture<>();
+ RequestTransactionManager.RequestTransaction transaction =
tm.startRequest();
+ transaction.submit(() -> conversationContext.sendRequest(modbusTcpADU)
+ .expectResponse(ModbusTcpADU.class, requestTimeout)
+ .onTimeout(responseFuture::completeExceptionally)
+ .onError((p, e) -> responseFuture.completeExceptionally(e))
+ .check(p -> p.getTransactionIdentifier() == transactionId)
+ .handle(p -> {
+ transaction.endRequest();
+ responseFuture.complete(p);
+ }));
+
+ ModbusTcpADU response =
responseFuture.get(configuration.getRequestTimeout() + 1000,
TimeUnit.MILLISECONDS);
+ UmasPDUItem responseItem = extractUmasResponse(response,
"BrowseSymbolTable");
+
+ if (!(responseItem instanceof UmasPDUReadUnlocatedVariableResponse
unlocatedResponse)) {
+ throw new PlcConnectionException("BrowseSymbolTable: unexpected
response: " + responseItem.getClass().getSimpleName());
+ }
+
+ byte[] block = unlocatedResponse.getBlock();
+ if (block == null || block.length == 0) {
+ return Collections.emptyList();
+ }
+
+ ReadBuffer readBuffer = new ReadBufferByteBased(block,
ByteOrder.LITTLE_ENDIAN);
+ UmasPDUReadUnlocatedVariableNamesResponse parsed =
+ UmasPDUReadUnlocatedVariableNamesResponse.staticParse(readBuffer);
+ return parsed.getRecords();
+ }
+
+ private List<PlcBrowseItem>
convertToBrowseItems(List<UmasUnlocatedVariableReference> symbols) {
+ List<PlcBrowseItem> items = new ArrayList<>(symbols.size());
+ for (UmasUnlocatedVariableReference symbol : symbols) {
+ items.add(buildBrowseItem(symbol.getValue(),
symbol.getDataType()));
+ }
+ return items;
+ }
+
+ private PlcBrowseItem buildBrowseItem(String name, int dataTypeId) {
+ PlcValueType plcValueType;
+ List<ArrayInfo> arrayInfo = Collections.emptyList();
+ Map<String, PlcBrowseItem> children = Collections.emptyMap();
+
+ Optional<Integer> elementTypeId =
umasDriverContext.getArrayElementTypeId(dataTypeId);
+ if (elementTypeId.isPresent()) {
+ plcValueType = resolveValueType(elementTypeId.get());
+ arrayInfo = buildArrayInfo(dataTypeId);
+ children = buildStructChildren(elementTypeId.get());
+ } else if
(umasDriverContext.getCustomTypeFields(dataTypeId).isPresent()) {
+ plcValueType = PlcValueType.Struct;
+ children = buildStructChildren(dataTypeId);
+ } else {
+ plcValueType = mapToPlcValueType(dataTypeId);
+ }
+
+ SymbolicUmasTag tag = new SymbolicUmasTag(name, plcValueType,
Collections.emptyList());
+ return new DefaultPlcBrowseItem(tag, name, true, true, true, false,
+ arrayInfo, children, Collections.emptyMap());
+ }
+
+ private PlcValueType resolveValueType(int typeId) {
+ if (umasDriverContext.getCustomTypeFields(typeId).isPresent()) {
+ return PlcValueType.Struct;
+ }
+ if (umasDriverContext.getArrayElementTypeId(typeId).isPresent()) {
+ return
resolveValueType(umasDriverContext.getArrayElementTypeId(typeId).get());
+ }
+ return mapToPlcValueType(typeId);
+ }
+
+ private List<ArrayInfo> buildArrayInfo(int typeId) {
+ Optional<List<UmasArrayDimension>> dims =
umasDriverContext.getArrayDimensions(typeId);
+ if (dims.isEmpty() || dims.get().isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<ArrayInfo> result = new ArrayList<>();
+ for (UmasArrayDimension dim : dims.get()) {
+ result.add(new DefaultArrayInfo((int) dim.getStartIndex(), (int)
dim.getUpperBound()));
+ }
+ return result;
+ }
+
+ private Map<String, PlcBrowseItem> buildStructChildren(int typeId) {
+ Optional<List<UmasUDTDefinition>> fields =
umasDriverContext.getCustomTypeFields(typeId);
+ if (fields.isEmpty() || fields.get().isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, PlcBrowseItem> children = new LinkedHashMap<>();
+ for (UmasUDTDefinition field : fields.get()) {
+ children.put(field.getValue(), buildBrowseItem(field.getValue(),
field.getDataType()));
+ }
+ return children;
+ }
+
+ private static PlcValueType mapToPlcValueType(int dataTypeId) {
+ if (!UmasDataType.isDefined((short) dataTypeId)) {
+ return PlcValueType.RAW_BYTE_ARRAY;
+ }
+ UmasDataType umasType = UmasDataType.enumForValue((short) dataTypeId);
+ return switch (umasType) {
+ case BOOL, EBOOL, UNKNOWN2, UNKNOWN3 -> PlcValueType.BOOL;
+ case BYTE, UNKNOWN11, UNKNOWN12, UNKNOWN13, UNKNOWN17, UNKNOWN18,
+ UNKNOWN19, UNKNOWN20, UNKNOWN24 -> PlcValueType.BYTE;
+ case INT -> PlcValueType.INT;
+ case UINT -> PlcValueType.UINT;
+ case DINT -> PlcValueType.DINT;
+ case UDINT -> PlcValueType.UDINT;
+ case REAL -> PlcValueType.REAL;
+ case STRING -> PlcValueType.STRING;
+ case TIME -> PlcValueType.TIME;
+ case DATE -> PlcValueType.DATE;
+ case TOD -> PlcValueType.TIME_OF_DAY;
+ case DATE_AND_TIME -> PlcValueType.DATE_AND_TIME;
+ case WORD -> PlcValueType.WORD;
+ case DWORD -> PlcValueType.DWORD;
+ };
+ }
+
+ // ========================================================================
+ // Helpers
+ // ========================================================================
+
+ private ModbusTcpADU buildModbusTcpADU(int transactionId, UmasPDUItem
item) {
+ // Track the function key for response discrimination in the parser
+ UmasFunctionKeyTracker.trackRequest(transactionId,
item.getUmasFunctionKey());
+ UmasPDU umasPdu = new UmasPDU(item);
+ return new ModbusTcpADU(transactionId, unitIdentifier, umasPdu);
+ }
+
+ private UmasPDUItem extractUmasResponse(ModbusTcpADU response, String
stepName) throws PlcConnectionException {
+ ModbusPDU pdu = response.getPdu();
+ if (pdu instanceof ModbusPDUError errorPdu) {
+ throw new PlcConnectionException(stepName + " received Modbus
error: " + errorPdu.getExceptionCode());
+ }
+ if (!(pdu instanceof UmasPDU umasPdu)) {
+ throw new PlcConnectionException(stepName + " received unexpected
PDU type: " + pdu.getClass().getSimpleName());
+ }
+ return umasPdu.getItem();
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/SymbolicUmasTag.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/SymbolicUmasTag.java
new file mode 100644
index 0000000000..4e477c4cfa
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/SymbolicUmasTag.java
@@ -0,0 +1,102 @@
+/*
+ * 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.umas.readwrite.tag;
+
+import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
+import org.apache.plc4x.java.api.model.ArrayInfo;
+import org.apache.plc4x.java.api.types.PlcValueType;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * UMAS tag identified by a symbolic name.
+ * Supports IEC 61131-3 conventions:
+ * <ul>
+ * <li>{@code g_r32} -- simple global variable</li>
+ * <li>{@code g_plant.meta.r32} -- nested struct member access</li>
+ * <li>{@code g_arrInt[3]} -- array element access</li>
+ * <li>{@code g_plant.items[2].value} -- mixed struct/array access</li>
+ * </ul>
+ */
+public class SymbolicUmasTag implements UmasTag {
+
+ private static final Pattern SYMBOLIC_ADDRESS_PATTERN =
+
Pattern.compile("^([a-zA-Z_]\\w*)(\\[\\d+])*(\\.([a-zA-Z_]\\w*)(\\[\\d+])*)*$");
+
+ private final String symbolicAddress;
+ private final PlcValueType dataType;
+ private final List<ArrayInfo> arrayInfo;
+
+ public SymbolicUmasTag(String symbolicAddress, PlcValueType dataType,
List<ArrayInfo> arrayInfo) {
+ this.symbolicAddress = Objects.requireNonNull(symbolicAddress);
+ this.dataType = dataType;
+ this.arrayInfo = Objects.requireNonNull(arrayInfo);
+ }
+
+ public static SymbolicUmasTag of(String address) {
+ if (!matches(address)) {
+ throw new PlcInvalidTagException(address,
SYMBOLIC_ADDRESS_PATTERN, "{symbolic-address}");
+ }
+ return new SymbolicUmasTag(address, null, Collections.emptyList());
+ }
+
+ public static boolean matches(String address) {
+ return SYMBOLIC_ADDRESS_PATTERN.matcher(address).matches();
+ }
+
+ public String getSymbolicAddress() {
+ return symbolicAddress;
+ }
+
+ @Override
+ public String getAddressString() {
+ return symbolicAddress;
+ }
+
+ @Override
+ public PlcValueType getPlcValueType() {
+ return dataType != null ? dataType : PlcValueType.NULL;
+ }
+
+ @Override
+ public List<ArrayInfo> getArrayInfo() {
+ return arrayInfo;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SymbolicUmasTag that)) return false;
+ return Objects.equals(symbolicAddress, that.symbolicAddress);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(symbolicAddress);
+ }
+
+ @Override
+ public String toString() {
+ return "SymbolicUmasTag{symbolicAddress='" + symbolicAddress + "'}";
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/UmasTag.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/UmasTag.java
new file mode 100644
index 0000000000..4db94ca618
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/UmasTag.java
@@ -0,0 +1,42 @@
+/*
+ * 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.umas.readwrite.tag;
+
+import org.apache.plc4x.java.api.model.ArrayInfo;
+import org.apache.plc4x.java.api.model.PlcTag;
+
+/**
+ * Marker interface for all UMAS tag types.
+ * UMAS tags use symbolic names that correspond to variables in the Schneider
Electric
+ * PLC project.
+ */
+public interface UmasTag extends PlcTag {
+
+ default int getTotalNumberOfElements() {
+ if (getArrayInfo() == null || getArrayInfo().isEmpty()) {
+ return 1;
+ }
+ int total = 1;
+ for (ArrayInfo arrayInfo : getArrayInfo()) {
+ total *= arrayInfo.getSize();
+ }
+ return total;
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/UmasTagHandler.java
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/UmasTagHandler.java
new file mode 100644
index 0000000000..530c63364f
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/java/org/apache/plc4x/java/umas/readwrite/tag/UmasTagHandler.java
@@ -0,0 +1,42 @@
+/*
+ * 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.umas.readwrite.tag;
+
+import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
+import org.apache.plc4x.java.api.model.PlcQuery;
+import org.apache.plc4x.java.api.model.PlcTag;
+import org.apache.plc4x.java.spi.connection.PlcTagHandler;
+
+public class UmasTagHandler implements PlcTagHandler {
+
+ @Override
+ public PlcTag parseTag(String tagAddress) {
+ if (SymbolicUmasTag.matches(tagAddress)) {
+ return SymbolicUmasTag.of(tagAddress);
+ }
+ throw new PlcInvalidTagException(tagAddress);
+ }
+
+ @Override
+ public PlcQuery parseQuery(String query) {
+ // Browse queries are passed through; the browse operation handles
filtering
+ return null;
+ }
+
+}
diff --git
a/plc4j/drivers/umas/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver
b/plc4j/drivers/umas/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver
new file mode 100644
index 0000000000..3998f7c828
--- /dev/null
+++
b/plc4j/drivers/umas/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver
@@ -0,0 +1 @@
+org.apache.plc4x.java.umas.readwrite.UmasDriver
diff --git
a/plc4j/drivers/umas/src/test/java/org/apache/plc4x/java/umas/manual/ManualUmasBrowse.java
b/plc4j/drivers/umas/src/test/java/org/apache/plc4x/java/umas/manual/ManualUmasBrowse.java
new file mode 100644
index 0000000000..20ff46e1e5
--- /dev/null
+++
b/plc4j/drivers/umas/src/test/java/org/apache/plc4x/java/umas/manual/ManualUmasBrowse.java
@@ -0,0 +1,70 @@
+package org.apache.plc4x.java.umas.manual;
+
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcBrowseItem;
+import org.apache.plc4x.java.api.messages.PlcBrowseResponse;
+import org.apache.plc4x.java.api.model.PlcQuery;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+
+import java.util.stream.Collectors;
+
+/**
+ * In order to run this using the simulator, we need to forward port 502 on
all devices to 127.0.0.1 by running the
+ * following command on a shell with admin privileges:
+ * netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=502
connectaddress=127.0.0.1 connectport=502
+ */
+public class ManualUmasBrowse {
+
+ public static void main(String[] args) throws Exception {
+ long startTime = System.currentTimeMillis();
+ try (PlcConnection connection =
PlcDriverManager.getDefault().getConnectionManager().getConnection("umas://192.168.42.99")){
+ PlcBrowseResponse plcBrowseResponse =
connection.browseRequestBuilder()
+ .addQuery("all", "**")
+ .build().executeWithInterceptor((queryName, query, item) -> {
+ outputItem(queryName, query, item, 0);
+ return true;
+ }).get();
+
+ long endTime = System.currentTimeMillis();
+ int numNodes = 0;
+ for (String queryName : plcBrowseResponse.getQueryNames()) {
+ if (plcBrowseResponse.getResponseCode(queryName) !=
PlcResponseCode.OK) {
+ continue;
+ }
+
+ for (PlcBrowseItem value :
plcBrowseResponse.getValues(queryName)) {
+ numNodes += countNodes(value);
+ }
+ }
+ System.out.printf("Took %dms returned %d nodes%n", endTime -
startTime, numNodes);
+ }
+ }
+
+ protected static void outputItem(String queryName, PlcQuery query,
PlcBrowseItem item, int level) {
+ System.out.printf("%s- %s: name: %s address: %s - type: %s%s%s%n",
+ " ".repeat(level),
+ queryName,
+ item.getName(),
+ item.getTag().getAddressString(),
+ item.getTag().getPlcValueType(),
+ (item.getArrayInformation() != null) &&
!item.getArrayInformation().isEmpty() ? " " +
item.getArrayInformation().stream().map(arrayInfo -> "[" +
arrayInfo.getLowerBound() + ".." + arrayInfo.getUpperBound() +
"]").collect(Collectors.joining()) : "",
+ (item.getOptions() != null) && !item.getOptions().isEmpty() ? " {"
+ item.getOptions().entrySet().stream().map(stringPlcValueEntry ->
stringPlcValueEntry.getKey() + ": \"" +
stringPlcValueEntry.getValue().toString() + "\"").collect(Collectors.joining())
+ "}" : "");
+ if ((item.getChildren() != null) && !item.getChildren().isEmpty()) {
+ item.getChildren().forEach((s, plcBrowseItem) ->
outputItem(queryName, query, plcBrowseItem, level + 1));
+ }
+ }
+
+ protected static int countNodes(PlcBrowseItem browseItem) {
+ if(browseItem.getChildren().isEmpty()) {
+ return 1;
+ }
+
+ int nodes = 1;
+ for (PlcBrowseItem childBrowseItem :
browseItem.getChildren().values()) {
+ nodes += countNodes(childBrowseItem);
+ }
+ return nodes;
+ }
+
+}
diff --git a/plc4j/drivers/umas/src/test/resources/logback-test.xml
b/plc4j/drivers/umas/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..cc4250b212
--- /dev/null
+++ b/plc4j/drivers/umas/src/test/resources/logback-test.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ -->
+<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ http://ch.qos.logback/xml/ns/logback
+
https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -
%msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="info">
+ <appender-ref ref="STDOUT" />
+ </root>
+
+</configuration>
\ No newline at end of file