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

Reply via email to