This is an automated email from the ASF dual-hosted git repository.

chrisdutz 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 20f08cc253 feat(plc4j/opcua): Implemented initial support for reading 
and writing structs in OPC-UA.
20f08cc253 is described below

commit 20f08cc25315487832ffabb38678a15eb0cbaf40
Author: Christofer Dutz <[email protected]>
AuthorDate: Tue Jul 14 23:48:47 2026 +0200

    feat(plc4j/opcua): Implemented initial support for reading and writing 
structs in OPC-UA.
---
 .../bacnetip/readwrite/model/BACnetVendorId.go     |  34 +-
 .../knxnetip/readwrite/model/KnxManufacturer.go    | 304 ++++++++-
 .../model/BinaryExtensionObjectWithMask.go         |  10 +-
 .../opcua/readwrite/model/ExtensionObject.go       |  28 +-
 .../readwrite/model/ExtensionObjectWithMask.go     |  69 ++-
 .../readwrite/model/NullExtensionObjectWithMask.go |  10 +-
 .../model/RawBinaryExtensionObjectWithMask.go      | 355 +++++++++++
 .../opcua/readwrite/model/StaticHelper.go          |  31 +
 .../readwrite/BinaryExtensionObjectWithMask.java   |  19 +-
 .../java/opcua/readwrite/ExtensionObject.java      |  17 +-
 .../opcua/readwrite/ExtensionObjectWithMask.java   |  40 +-
 .../readwrite/NullExtensionObjectWithMask.java     |  19 +-
 ....java => RawBinaryExtensionObjectWithMask.java} |  71 +--
 .../apache/plc4x/java/opcua/OpcuaConnection.java   | 686 +++++++++++++++++++--
 .../java/opcua/readwrite/utils/StaticHelper.java   |  48 +-
 .../plc4x/java/opcua/OpcuaPlcDriverTest.java       |  72 ++-
 .../milo/examples/server/Plc4xTestNamespace.java   |  72 +++
 .../milo/examples/server/Plc4xTestStruct.java      | 142 +++++
 .../generated/protocols/opcua/opc-manual.mspec     |  23 +-
 protocols/opcua/src/main/xslt/opc-manual.xsl       |  23 +-
 20 files changed, 1870 insertions(+), 203 deletions(-)

diff --git a/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go 
b/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go
index e6d4316d3c..86ee56a1da 100644
--- a/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go
+++ b/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go
@@ -1662,6 +1662,8 @@ const (
        BACnetVendorId_CONTROL_COM_TECHNOLOGIESLLC                              
                    BACnetVendorId = 1627
        BACnetVendorId_ELECTR_SOL                                               
                    BACnetVendorId = 1628
        BACnetVendorId_CORPORACIN_EMPRESARIAL_ALTRA                             
                    BACnetVendorId = 1629
+       BACnetVendorId_IC_VALVES_NANJING_CO_LTD                                 
                    BACnetVendorId = 1630
+       BACnetVendorId_OCCUSPACE_INC                                            
                    BACnetVendorId = 1631
        BACnetVendorId_UNKNOWN_VENDOR                                           
                    BACnetVendorId = 0xFFFF
 )
 
@@ -3289,6 +3291,8 @@ func init() {
                BACnetVendorId_CONTROL_COM_TECHNOLOGIESLLC,
                BACnetVendorId_ELECTR_SOL,
                BACnetVendorId_CORPORACIN_EMPRESARIAL_ALTRA,
+               BACnetVendorId_IC_VALVES_NANJING_CO_LTD,
+               BACnetVendorId_OCCUSPACE_INC,
                BACnetVendorId_UNKNOWN_VENDOR,
        }
 }
@@ -6091,6 +6095,14 @@ func (e BACnetVendorId) VendorId() uint16 {
                { /* '163' */
                        return 163
                }
+       case 1630:
+               { /* '1630' */
+                       return 1630
+               }
+       case 1631:
+               { /* '1631' */
+                       return 1631
+               }
        case 164:
                { /* '164' */
                        return 164
@@ -12583,12 +12595,20 @@ func (e BACnetVendorId) VendorName() string {
                }
        case 1629:
                { /* '1629' */
-                       return "Corporación Empresarial Altra’"
+                       return "Corporación Empresarial Altra"
                }
        case 163:
                { /* '163' */
                        return "SysMik GmbH Dresden"
                }
+       case 1630:
+               { /* '1630' */
+                       return "IC Valves (Nanjing) Co., Ltd."
+               }
+       case 1631:
+               { /* '1631' */
+                       return "Occuspace Inc."
+               }
        case 164:
                { /* '164' */
                        return "HSC Regelungstechnik GmbH"
@@ -17688,6 +17708,10 @@ func BACnetVendorIdByValue(value uint16) (enum 
BACnetVendorId, ok bool) {
                return BACnetVendorId_CORPORACIN_EMPRESARIAL_ALTRA, true
        case 163:
                return BACnetVendorId_SYS_MIK_GMBH_DRESDEN, true
+       case 1630:
+               return BACnetVendorId_IC_VALVES_NANJING_CO_LTD, true
+       case 1631:
+               return BACnetVendorId_OCCUSPACE_INC, true
        case 164:
                return BACnetVendorId_HSC_REGELUNGSTECHNIK_GMBH, true
        case 165:
@@ -20934,6 +20958,10 @@ func BACnetVendorIdByName(value string) (enum 
BACnetVendorId, ok bool) {
                return BACnetVendorId_CORPORACIN_EMPRESARIAL_ALTRA, true
        case "SYS_MIK_GMBH_DRESDEN":
                return BACnetVendorId_SYS_MIK_GMBH_DRESDEN, true
+       case "IC_VALVES_NANJING_CO_LTD":
+               return BACnetVendorId_IC_VALVES_NANJING_CO_LTD, true
+       case "OCCUSPACE_INC":
+               return BACnetVendorId_OCCUSPACE_INC, true
        case "HSC_REGELUNGSTECHNIK_GMBH":
                return BACnetVendorId_HSC_REGELUNGSTECHNIK_GMBH, true
        case "SMART_TEMP_AUSTRALIA_PTY_LTD":
@@ -24252,6 +24280,10 @@ func (e BACnetVendorId) PLC4XEnumName() string {
                return "CORPORACIN_EMPRESARIAL_ALTRA"
        case BACnetVendorId_SYS_MIK_GMBH_DRESDEN:
                return "SYS_MIK_GMBH_DRESDEN"
+       case BACnetVendorId_IC_VALVES_NANJING_CO_LTD:
+               return "IC_VALVES_NANJING_CO_LTD"
+       case BACnetVendorId_OCCUSPACE_INC:
+               return "OCCUSPACE_INC"
        case BACnetVendorId_HSC_REGELUNGSTECHNIK_GMBH:
                return "HSC_REGELUNGSTECHNIK_GMBH"
        case BACnetVendorId_SMART_TEMP_AUSTRALIA_PTY_LTD:
diff --git a/plc4go/protocols/knxnetip/readwrite/model/KnxManufacturer.go 
b/plc4go/protocols/knxnetip/readwrite/model/KnxManufacturer.go
index 68b17dda93..f795e3af3e 100644
--- a/plc4go/protocols/knxnetip/readwrite/model/KnxManufacturer.go
+++ b/plc4go/protocols/knxnetip/readwrite/model/KnxManufacturer.go
@@ -394,7 +394,7 @@ const (
        KnxManufacturer_M_PEAKNX_A_DOGAWIST_COMPANY                          
KnxManufacturer = 348
        KnxManufacturer_M_ACEMATIC                                           
KnxManufacturer = 349
        KnxManufacturer_M_ELAUSYS                                            
KnxManufacturer = 350
-       KnxManufacturer_M_ITK_ENGINEERING_AG                                 
KnxManufacturer = 351
+       KnxManufacturer_M_ITK_ENGINEERING_GMBH                               
KnxManufacturer = 351
        KnxManufacturer_M_INTEGRA_METERING_AG                                
KnxManufacturer = 352
        KnxManufacturer_M_FMS_HOSPITALITY_PTE_LTD                            
KnxManufacturer = 353
        KnxManufacturer_M_NUVO                                               
KnxManufacturer = 354
@@ -844,12 +844,27 @@ const (
        KnxManufacturer_M_ARCOLA_AI_LIMITED                                  
KnxManufacturer = 798
        KnxManufacturer_M_ZHEJIANG_DOOYA_INTELLIGENT_TECHNOLOGY_CO___LTD_    
KnxManufacturer = 799
        KnxManufacturer_M_HUARUI                                             
KnxManufacturer = 800
-       KnxManufacturer_M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V               
KnxManufacturer = 801
+       KnxManufacturer_M_CAMCOM_CABLES_NETHERLANDS                          
KnxManufacturer = 801
        KnxManufacturer_M_BI_TECH_ELECTRONICS                                
KnxManufacturer = 802
        KnxManufacturer_M_BELFAST_CABLES_UK_SOLUTIONS_LTD                    
KnxManufacturer = 803
        KnxManufacturer_M_KAYBE_NETWORKING_LLP                               
KnxManufacturer = 804
-       KnxManufacturer_M_ABB___RESERVED                                     
KnxManufacturer = 805
-       KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED                    
KnxManufacturer = 806
+       KnxManufacturer_M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_ 
KnxManufacturer = 805
+       KnxManufacturer_M_HORIZENTIS                                         
KnxManufacturer = 806
+       KnxManufacturer_M_HOM_CONTROLS                                       
KnxManufacturer = 807
+       KnxManufacturer_M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD        
KnxManufacturer = 808
+       KnxManufacturer_M_KEMCOM_GMBH                                        
KnxManufacturer = 809
+       KnxManufacturer_M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD          
KnxManufacturer = 810
+       KnxManufacturer_M_DOMUSFLOW                                          
KnxManufacturer = 811
+       KnxManufacturer_M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_                
KnxManufacturer = 812
+       KnxManufacturer_M_SMARTOMATIC_GMBH                                   
KnxManufacturer = 813
+       KnxManufacturer_M_T_MAC_TECHNOLOGIES_LTD                             
KnxManufacturer = 814
+       KnxManufacturer_M_SYNCPRO                                            
KnxManufacturer = 815
+       KnxManufacturer_M_HAAR_INFORMATIK                                    
KnxManufacturer = 816
+       KnxManufacturer_M_DIE_BILDUNGSMANUFAKTUR_GMBH                        
KnxManufacturer = 817
+       KnxManufacturer_M_READYKNX                                           
KnxManufacturer = 818
+       KnxManufacturer_M_SMARTVOS                                           
KnxManufacturer = 819
+       KnxManufacturer_M_ABB___RESERVED                                     
KnxManufacturer = 820
+       KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED                    
KnxManufacturer = 821
 )
 
 var KnxManufacturerValues []KnxManufacturer
@@ -1208,7 +1223,7 @@ func init() {
                KnxManufacturer_M_PEAKNX_A_DOGAWIST_COMPANY,
                KnxManufacturer_M_ACEMATIC,
                KnxManufacturer_M_ELAUSYS,
-               KnxManufacturer_M_ITK_ENGINEERING_AG,
+               KnxManufacturer_M_ITK_ENGINEERING_GMBH,
                KnxManufacturer_M_INTEGRA_METERING_AG,
                KnxManufacturer_M_FMS_HOSPITALITY_PTE_LTD,
                KnxManufacturer_M_NUVO,
@@ -1658,10 +1673,25 @@ func init() {
                KnxManufacturer_M_ARCOLA_AI_LIMITED,
                
KnxManufacturer_M_ZHEJIANG_DOOYA_INTELLIGENT_TECHNOLOGY_CO___LTD_,
                KnxManufacturer_M_HUARUI,
-               KnxManufacturer_M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V,
+               KnxManufacturer_M_CAMCOM_CABLES_NETHERLANDS,
                KnxManufacturer_M_BI_TECH_ELECTRONICS,
                KnxManufacturer_M_BELFAST_CABLES_UK_SOLUTIONS_LTD,
                KnxManufacturer_M_KAYBE_NETWORKING_LLP,
+               
KnxManufacturer_M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_,
+               KnxManufacturer_M_HORIZENTIS,
+               KnxManufacturer_M_HOM_CONTROLS,
+               KnxManufacturer_M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD,
+               KnxManufacturer_M_KEMCOM_GMBH,
+               KnxManufacturer_M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD,
+               KnxManufacturer_M_DOMUSFLOW,
+               KnxManufacturer_M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_,
+               KnxManufacturer_M_SMARTOMATIC_GMBH,
+               KnxManufacturer_M_T_MAC_TECHNOLOGIES_LTD,
+               KnxManufacturer_M_SYNCPRO,
+               KnxManufacturer_M_HAAR_INFORMATIK,
+               KnxManufacturer_M_DIE_BILDUNGSMANUFAKTUR_GMBH,
+               KnxManufacturer_M_READYKNX,
+               KnxManufacturer_M_SMARTVOS,
                KnxManufacturer_M_ABB___RESERVED,
                KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED,
        }
@@ -4811,20 +4841,80 @@ func (e KnxManufacturer) Number() uint16 {
                }
        case 805:
                { /* '805' */
-                       return 43954
+                       return 866
                }
        case 806:
                { /* '806' */
-                       return 43959
+                       return 867
+               }
+       case 807:
+               { /* '807' */
+                       return 868
+               }
+       case 808:
+               { /* '808' */
+                       return 869
+               }
+       case 809:
+               { /* '809' */
+                       return 870
                }
        case 81:
                { /* '81' */
                        return 120
                }
+       case 810:
+               { /* '810' */
+                       return 871
+               }
+       case 811:
+               { /* '811' */
+                       return 872
+               }
+       case 812:
+               { /* '812' */
+                       return 873
+               }
+       case 813:
+               { /* '813' */
+                       return 874
+               }
+       case 814:
+               { /* '814' */
+                       return 875
+               }
+       case 815:
+               { /* '815' */
+                       return 876
+               }
+       case 816:
+               { /* '816' */
+                       return 877
+               }
+       case 817:
+               { /* '817' */
+                       return 878
+               }
+       case 818:
+               { /* '818' */
+                       return 879
+               }
+       case 819:
+               { /* '819' */
+                       return 880
+               }
        case 82:
                { /* '82' */
                        return 121
                }
+       case 820:
+               { /* '820' */
+                       return 43954
+               }
+       case 821:
+               { /* '821' */
+                       return 43959
+               }
        case 83:
                { /* '83' */
                        return 122
@@ -6041,7 +6131,7 @@ func (e KnxManufacturer) Name() string {
                }
        case 351:
                { /* '351' */
-                       return "ITK Engineering AG"
+                       return "ITK Engineering GmbH"
                }
        case 352:
                { /* '352' */
@@ -8041,7 +8131,7 @@ func (e KnxManufacturer) Name() string {
                }
        case 801:
                { /* '801' */
-                       return "Technovation Alliance Enterprise B.V"
+                       return "CAMCOM Cables NETHERLANDS"
                }
        case 802:
                { /* '802' */
@@ -8057,20 +8147,80 @@ func (e KnxManufacturer) Name() string {
                }
        case 805:
                { /* '805' */
-                       return "ABB - reserved"
+                       return "Guangzhou Easyctrl Intelligent Technology 
Co.,Ltd."
                }
        case 806:
                { /* '806' */
-                       return "Busch-Jaeger Elektro - reserved"
+                       return "Horizentis"
+               }
+       case 807:
+               { /* '807' */
+                       return "Hom Controls"
+               }
+       case 808:
+               { /* '808' */
+                       return "Shenzhen Golden Security Technology Co.,Ltd"
+               }
+       case 809:
+               { /* '809' */
+                       return "KemCom GmbH"
                }
        case 81:
                { /* '81' */
                        return "Herholdt Controls srl"
                }
+       case 810:
+               { /* '810' */
+                       return "Nanjing puietel I.O.T Technology Co., Ltd"
+               }
+       case 811:
+               { /* '811' */
+                       return "DomusFlow"
+               }
+       case 812:
+               { /* '812' */
+                       return "Beijing Yangrui Technology Co.,Ltd."
+               }
+       case 813:
+               { /* '813' */
+                       return "Smartomatic GmbH"
+               }
+       case 814:
+               { /* '814' */
+                       return "t-mac Technologies Ltd"
+               }
+       case 815:
+               { /* '815' */
+                       return "SYNCPRO"
+               }
+       case 816:
+               { /* '816' */
+                       return "Haar-Informatik"
+               }
+       case 817:
+               { /* '817' */
+                       return "DIE-Bildungsmanufaktur GmbH"
+               }
+       case 818:
+               { /* '818' */
+                       return "readyKNX"
+               }
+       case 819:
+               { /* '819' */
+                       return "smartVOS"
+               }
        case 82:
                { /* '82' */
                        return "Niko-Zublin"
                }
+       case 820:
+               { /* '820' */
+                       return "ABB - reserved"
+               }
+       case 821:
+               { /* '821' */
+                       return "Busch-Jaeger Elektro - reserved"
+               }
        case 83:
                { /* '83' */
                        return "Durable Technologies"
@@ -8723,7 +8873,7 @@ func KnxManufacturerByValue(value uint16) (enum 
KnxManufacturer, ok bool) {
        case 350:
                return KnxManufacturer_M_ELAUSYS, true
        case 351:
-               return KnxManufacturer_M_ITK_ENGINEERING_AG, true
+               return KnxManufacturer_M_ITK_ENGINEERING_GMBH, true
        case 352:
                return KnxManufacturer_M_INTEGRA_METERING_AG, true
        case 353:
@@ -9723,7 +9873,7 @@ func KnxManufacturerByValue(value uint16) (enum 
KnxManufacturer, ok bool) {
        case 800:
                return KnxManufacturer_M_HUARUI, true
        case 801:
-               return KnxManufacturer_M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V, 
true
+               return KnxManufacturer_M_CAMCOM_CABLES_NETHERLANDS, true
        case 802:
                return KnxManufacturer_M_BI_TECH_ELECTRONICS, true
        case 803:
@@ -9731,13 +9881,43 @@ func KnxManufacturerByValue(value uint16) (enum 
KnxManufacturer, ok bool) {
        case 804:
                return KnxManufacturer_M_KAYBE_NETWORKING_LLP, true
        case 805:
-               return KnxManufacturer_M_ABB___RESERVED, true
+               return 
KnxManufacturer_M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_, true
        case 806:
-               return KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED, true
+               return KnxManufacturer_M_HORIZENTIS, true
+       case 807:
+               return KnxManufacturer_M_HOM_CONTROLS, true
+       case 808:
+               return 
KnxManufacturer_M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD, true
+       case 809:
+               return KnxManufacturer_M_KEMCOM_GMBH, true
        case 81:
                return KnxManufacturer_M_HERHOLDT_CONTROLS_SRL, true
+       case 810:
+               return 
KnxManufacturer_M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD, true
+       case 811:
+               return KnxManufacturer_M_DOMUSFLOW, true
+       case 812:
+               return KnxManufacturer_M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_, 
true
+       case 813:
+               return KnxManufacturer_M_SMARTOMATIC_GMBH, true
+       case 814:
+               return KnxManufacturer_M_T_MAC_TECHNOLOGIES_LTD, true
+       case 815:
+               return KnxManufacturer_M_SYNCPRO, true
+       case 816:
+               return KnxManufacturer_M_HAAR_INFORMATIK, true
+       case 817:
+               return KnxManufacturer_M_DIE_BILDUNGSMANUFAKTUR_GMBH, true
+       case 818:
+               return KnxManufacturer_M_READYKNX, true
+       case 819:
+               return KnxManufacturer_M_SMARTVOS, true
        case 82:
                return KnxManufacturer_M_NIKO_ZUBLIN, true
+       case 820:
+               return KnxManufacturer_M_ABB___RESERVED, true
+       case 821:
+               return KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED, true
        case 83:
                return KnxManufacturer_M_DURABLE_TECHNOLOGIES, true
        case 84:
@@ -10342,8 +10522,8 @@ func KnxManufacturerByName(value string) (enum 
KnxManufacturer, ok bool) {
                return KnxManufacturer_M_HOLEC_SYSTEMEN, true
        case "M_ELAUSYS":
                return KnxManufacturer_M_ELAUSYS, true
-       case "M_ITK_ENGINEERING_AG":
-               return KnxManufacturer_M_ITK_ENGINEERING_AG, true
+       case "M_ITK_ENGINEERING_GMBH":
+               return KnxManufacturer_M_ITK_ENGINEERING_GMBH, true
        case "M_INTEGRA_METERING_AG":
                return KnxManufacturer_M_INTEGRA_METERING_AG, true
        case "M_FMS_HOSPITALITY_PTE_LTD":
@@ -11342,22 +11522,52 @@ func KnxManufacturerByName(value string) (enum 
KnxManufacturer, ok bool) {
                return KnxManufacturer_M_INTESIS, true
        case "M_HUARUI":
                return KnxManufacturer_M_HUARUI, true
-       case "M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V":
-               return KnxManufacturer_M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V, 
true
+       case "M_CAMCOM_CABLES_NETHERLANDS":
+               return KnxManufacturer_M_CAMCOM_CABLES_NETHERLANDS, true
        case "M_BI_TECH_ELECTRONICS":
                return KnxManufacturer_M_BI_TECH_ELECTRONICS, true
        case "M_BELFAST_CABLES_UK_SOLUTIONS_LTD":
                return KnxManufacturer_M_BELFAST_CABLES_UK_SOLUTIONS_LTD, true
        case "M_KAYBE_NETWORKING_LLP":
                return KnxManufacturer_M_KAYBE_NETWORKING_LLP, true
-       case "M_ABB___RESERVED":
-               return KnxManufacturer_M_ABB___RESERVED, true
-       case "M_BUSCH_JAEGER_ELEKTRO___RESERVED":
-               return KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED, true
+       case "M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_":
+               return 
KnxManufacturer_M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_, true
+       case "M_HORIZENTIS":
+               return KnxManufacturer_M_HORIZENTIS, true
+       case "M_HOM_CONTROLS":
+               return KnxManufacturer_M_HOM_CONTROLS, true
+       case "M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD":
+               return 
KnxManufacturer_M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD, true
+       case "M_KEMCOM_GMBH":
+               return KnxManufacturer_M_KEMCOM_GMBH, true
        case "M_HERHOLDT_CONTROLS_SRL":
                return KnxManufacturer_M_HERHOLDT_CONTROLS_SRL, true
+       case "M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD":
+               return 
KnxManufacturer_M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD, true
+       case "M_DOMUSFLOW":
+               return KnxManufacturer_M_DOMUSFLOW, true
+       case "M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_":
+               return KnxManufacturer_M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_, 
true
+       case "M_SMARTOMATIC_GMBH":
+               return KnxManufacturer_M_SMARTOMATIC_GMBH, true
+       case "M_T_MAC_TECHNOLOGIES_LTD":
+               return KnxManufacturer_M_T_MAC_TECHNOLOGIES_LTD, true
+       case "M_SYNCPRO":
+               return KnxManufacturer_M_SYNCPRO, true
+       case "M_HAAR_INFORMATIK":
+               return KnxManufacturer_M_HAAR_INFORMATIK, true
+       case "M_DIE_BILDUNGSMANUFAKTUR_GMBH":
+               return KnxManufacturer_M_DIE_BILDUNGSMANUFAKTUR_GMBH, true
+       case "M_READYKNX":
+               return KnxManufacturer_M_READYKNX, true
+       case "M_SMARTVOS":
+               return KnxManufacturer_M_SMARTVOS, true
        case "M_NIKO_ZUBLIN":
                return KnxManufacturer_M_NIKO_ZUBLIN, true
+       case "M_ABB___RESERVED":
+               return KnxManufacturer_M_ABB___RESERVED, true
+       case "M_BUSCH_JAEGER_ELEKTRO___RESERVED":
+               return KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED, true
        case "M_DURABLE_TECHNOLOGIES":
                return KnxManufacturer_M_DURABLE_TECHNOLOGIES, true
        case "M_INNOTEAM":
@@ -12034,8 +12244,8 @@ func (e KnxManufacturer) PLC4XEnumName() string {
                return "M_HOLEC_SYSTEMEN"
        case KnxManufacturer_M_ELAUSYS:
                return "M_ELAUSYS"
-       case KnxManufacturer_M_ITK_ENGINEERING_AG:
-               return "M_ITK_ENGINEERING_AG"
+       case KnxManufacturer_M_ITK_ENGINEERING_GMBH:
+               return "M_ITK_ENGINEERING_GMBH"
        case KnxManufacturer_M_INTEGRA_METERING_AG:
                return "M_INTEGRA_METERING_AG"
        case KnxManufacturer_M_FMS_HOSPITALITY_PTE_LTD:
@@ -13034,22 +13244,52 @@ func (e KnxManufacturer) PLC4XEnumName() string {
                return "M_INTESIS"
        case KnxManufacturer_M_HUARUI:
                return "M_HUARUI"
-       case KnxManufacturer_M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V:
-               return "M_TECHNOVATION_ALLIANCE_ENTERPRISE_B_V"
+       case KnxManufacturer_M_CAMCOM_CABLES_NETHERLANDS:
+               return "M_CAMCOM_CABLES_NETHERLANDS"
        case KnxManufacturer_M_BI_TECH_ELECTRONICS:
                return "M_BI_TECH_ELECTRONICS"
        case KnxManufacturer_M_BELFAST_CABLES_UK_SOLUTIONS_LTD:
                return "M_BELFAST_CABLES_UK_SOLUTIONS_LTD"
        case KnxManufacturer_M_KAYBE_NETWORKING_LLP:
                return "M_KAYBE_NETWORKING_LLP"
-       case KnxManufacturer_M_ABB___RESERVED:
-               return "M_ABB___RESERVED"
-       case KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED:
-               return "M_BUSCH_JAEGER_ELEKTRO___RESERVED"
+       case 
KnxManufacturer_M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_:
+               return "M_GUANGZHOU_EASYCTRL_INTELLIGENT_TECHNOLOGY_CO__LTD_"
+       case KnxManufacturer_M_HORIZENTIS:
+               return "M_HORIZENTIS"
+       case KnxManufacturer_M_HOM_CONTROLS:
+               return "M_HOM_CONTROLS"
+       case KnxManufacturer_M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD:
+               return "M_SHENZHEN_GOLDEN_SECURITY_TECHNOLOGY_CO__LTD"
+       case KnxManufacturer_M_KEMCOM_GMBH:
+               return "M_KEMCOM_GMBH"
        case KnxManufacturer_M_HERHOLDT_CONTROLS_SRL:
                return "M_HERHOLDT_CONTROLS_SRL"
+       case KnxManufacturer_M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD:
+               return "M_NANJING_PUIETEL_I_O_T_TECHNOLOGY_CO___LTD"
+       case KnxManufacturer_M_DOMUSFLOW:
+               return "M_DOMUSFLOW"
+       case KnxManufacturer_M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_:
+               return "M_BEIJING_YANGRUI_TECHNOLOGY_CO__LTD_"
+       case KnxManufacturer_M_SMARTOMATIC_GMBH:
+               return "M_SMARTOMATIC_GMBH"
+       case KnxManufacturer_M_T_MAC_TECHNOLOGIES_LTD:
+               return "M_T_MAC_TECHNOLOGIES_LTD"
+       case KnxManufacturer_M_SYNCPRO:
+               return "M_SYNCPRO"
+       case KnxManufacturer_M_HAAR_INFORMATIK:
+               return "M_HAAR_INFORMATIK"
+       case KnxManufacturer_M_DIE_BILDUNGSMANUFAKTUR_GMBH:
+               return "M_DIE_BILDUNGSMANUFAKTUR_GMBH"
+       case KnxManufacturer_M_READYKNX:
+               return "M_READYKNX"
+       case KnxManufacturer_M_SMARTVOS:
+               return "M_SMARTVOS"
        case KnxManufacturer_M_NIKO_ZUBLIN:
                return "M_NIKO_ZUBLIN"
+       case KnxManufacturer_M_ABB___RESERVED:
+               return "M_ABB___RESERVED"
+       case KnxManufacturer_M_BUSCH_JAEGER_ELEKTRO___RESERVED:
+               return "M_BUSCH_JAEGER_ELEKTRO___RESERVED"
        case KnxManufacturer_M_DURABLE_TECHNOLOGIES:
                return "M_DURABLE_TECHNOLOGIES"
        case KnxManufacturer_M_INNOTEAM:
diff --git 
a/plc4go/protocols/opcua/readwrite/model/BinaryExtensionObjectWithMask.go 
b/plc4go/protocols/opcua/readwrite/model/BinaryExtensionObjectWithMask.go
index 5e79d50a59..693228f94a 100644
--- a/plc4go/protocols/opcua/readwrite/model/BinaryExtensionObjectWithMask.go
+++ b/plc4go/protocols/opcua/readwrite/model/BinaryExtensionObjectWithMask.go
@@ -188,14 +188,6 @@ func (b *_BinaryExtensionObjectWithMask) 
CreateBinaryExtensionObjectWithMaskBuil
 /////////////////////// Accessors for discriminator values.
 ///////////////////////
 
-func (m *_BinaryExtensionObjectWithMask) GetEncodingMaskXmlBody() bool {
-       return bool(false)
-}
-
-func (m *_BinaryExtensionObjectWithMask) GetEncodingMaskBinaryBody() bool {
-       return bool(true)
-}
-
 ///////////////////////
 ///////////////////////
 ///////////////////////////////////////////////////////////
@@ -250,7 +242,7 @@ func (m *_BinaryExtensionObjectWithMask) 
GetLengthInBytes(ctx context.Context) u
        return m.GetLengthInBits(ctx) / 8
 }
 
-func (m *_BinaryExtensionObjectWithMask) parse(ctx context.Context, readBuffer 
utils.ReadBuffer, parent *_ExtensionObjectWithMask, extensionId int32, 
includeEncodingMask bool) (__binaryExtensionObjectWithMask 
BinaryExtensionObjectWithMask, err error) {
+func (m *_BinaryExtensionObjectWithMask) parse(ctx context.Context, readBuffer 
utils.ReadBuffer, parent *_ExtensionObjectWithMask, extensionId int32, 
standardEncoding bool, includeEncodingMask bool) 
(__binaryExtensionObjectWithMask BinaryExtensionObjectWithMask, err error) {
        m.ExtensionObjectWithMaskContract = parent
        parent._SubType = m
        positionAware := readBuffer
diff --git a/plc4go/protocols/opcua/readwrite/model/ExtensionObject.go 
b/plc4go/protocols/opcua/readwrite/model/ExtensionObject.go
index 4cb91024de..1cd66ac7c7 100644
--- a/plc4go/protocols/opcua/readwrite/model/ExtensionObject.go
+++ b/plc4go/protocols/opcua/readwrite/model/ExtensionObject.go
@@ -55,6 +55,11 @@ type ExtensionObjectContract interface {
        GetTypeId() ExpandedNodeId
        // GetExtensionId returns ExtensionId (virtual field)
        GetExtensionId() int32
+       // GetStandardEncoding returns StandardEncoding (virtual field)
+       // Whether the encoding node refers to a well-known standard type 
(namespace 0). Computed here
+       // where typeId is in scope and threaded down like extensionId, so the 
masked-body dispatch can
+       // tell a decodable standard type from a custom one.
+       GetStandardEncoding() bool
        // GetBody returns Body (abstract field)
        GetBody() ExtensionObjectDefinition
        // IsExtensionObject is a marker method to prevent unintentional type 
checks (interfaces of same signature)
@@ -266,6 +271,13 @@ func (pm *_ExtensionObject) GetExtensionId() int32 {
        return int32(utils.InlineIf(bool((m.GetTypeId()) == (nil)), func() any 
{ return int32(int32(0)) }, func() any { return int32(ExtensionId(ctx, 
m.GetTypeId())) }).(int32))
 }
 
+func (pm *_ExtensionObject) GetStandardEncoding() bool {
+       m := pm._SubType
+       ctx := context.Background()
+       _ = ctx
+       return bool(IsStandardEncoding(ctx, m.GetTypeId()))
+}
+
 ///////////////////////
 ///////////////////////
 ///////////////////////////////////////////////////////////
@@ -307,6 +319,8 @@ func (m *_ExtensionObject) getLengthInBits(ctx 
context.Context) uint16 {
 
        // A virtual field doesn't have any in- or output.
 
+       // A virtual field doesn't have any in- or output.
+
        return lengthInBits
 }
 
@@ -368,6 +382,12 @@ func (m *_ExtensionObject) parse(ctx context.Context, 
readBuffer utils.ReadBuffe
        }
        _ = extensionId
 
+       standardEncoding, err := ReadVirtualField[bool](ctx, 
"standardEncoding", (*bool)(nil), IsStandardEncoding(ctx, typeId), 
codegen.WithEncoding("UTF8"))
+       if err != nil {
+               return nil, errors.Wrap(err, fmt.Sprintf("Error parsing 
'standardEncoding' field"))
+       }
+       _ = standardEncoding
+
        // Switch Field (Depending on the discriminator values, passes the 
instantiation to a sub-type)
        var _child ExtensionObject
        switch {
@@ -376,7 +396,7 @@ func (m *_ExtensionObject) parse(ctx context.Context, 
readBuffer utils.ReadBuffe
                        return nil, errors.Wrap(err, "Error parsing sub-type 
RootExtensionObject for type-switch of ExtensionObject")
                }
        case includeEncodingMask == bool(true): // ExtensionObjectWithMask
-               if _child, err = new(_ExtensionObjectWithMask).parse(ctx, 
readBuffer, m, extensionId, includeEncodingMask); err != nil {
+               if _child, err = new(_ExtensionObjectWithMask).parse(ctx, 
readBuffer, m, extensionId, standardEncoding, includeEncodingMask); err != nil {
                        return nil, errors.Wrap(err, "Error parsing sub-type 
ExtensionObjectWithMask for type-switch of ExtensionObject")
                }
        default:
@@ -411,6 +431,12 @@ func (pm *_ExtensionObject) serializeParent(ctx 
context.Context, writeBuffer uti
        if _extensionIdErr := writeBuffer.WriteVirtual(ctx, "extensionId", 
m.GetExtensionId()); _extensionIdErr != nil {
                return errors.Wrap(_extensionIdErr, "Error serializing 
'extensionId' field")
        }
+       // Virtual field
+       standardEncoding := m.GetStandardEncoding()
+       _ = standardEncoding
+       if _standardEncodingErr := writeBuffer.WriteVirtual(ctx, 
"standardEncoding", m.GetStandardEncoding()); _standardEncodingErr != nil {
+               return errors.Wrap(_standardEncodingErr, "Error serializing 
'standardEncoding' field")
+       }
 
        // Switch field (Depending on the discriminator values, passes the 
serialization to a sub-type)
        if _typeSwitchErr := serializeChildFunction(); _typeSwitchErr != nil {
diff --git a/plc4go/protocols/opcua/readwrite/model/ExtensionObjectWithMask.go 
b/plc4go/protocols/opcua/readwrite/model/ExtensionObjectWithMask.go
index b9b19e6e8f..c922ee80b5 100644
--- a/plc4go/protocols/opcua/readwrite/model/ExtensionObjectWithMask.go
+++ b/plc4go/protocols/opcua/readwrite/model/ExtensionObjectWithMask.go
@@ -54,6 +54,13 @@ type ExtensionObjectWithMaskContract interface {
        ExtensionObjectContract
        // GetEncodingMask returns EncodingMask (property field)
        GetEncodingMask() ExtensionObjectEncodingMask
+       // GetBodyKind returns BodyKind (virtual field)
+       // Body kind: 0 = no binary body (null); 1 = binary body of a 
well-known standard type
+       // (encoding node in namespace 0) which the generated dispatch can 
decode; 2 = binary
+       // body of a custom / user-defined type (encoding node in namespace >= 
1) which the
+       // dispatch cannot decode, so the raw bytes are captured for the driver 
to decode against
+       // the type's StructureDefinition.
+       GetBodyKind() int8
        // GetIncludeEncodingMask returns IncludeEncodingMask (discriminator 
field)
        GetIncludeEncodingMask() bool
        // IsExtensionObjectWithMask is a marker method to prevent 
unintentional type checks (interfaces of same signature)
@@ -110,6 +117,8 @@ type ExtensionObjectWithMaskBuilder interface {
        WithEncodingMaskBuilder(func(ExtensionObjectEncodingMaskBuilder) 
ExtensionObjectEncodingMaskBuilder) ExtensionObjectWithMaskBuilder
        // AsBinaryExtensionObjectWithMask converts this build to a subType of 
ExtensionObjectWithMask. It is always possible to return to current builder 
using Done()
        AsBinaryExtensionObjectWithMask() BinaryExtensionObjectWithMaskBuilder
+       // AsRawBinaryExtensionObjectWithMask converts this build to a subType 
of ExtensionObjectWithMask. It is always possible to return to current builder 
using Done()
+       AsRawBinaryExtensionObjectWithMask() 
RawBinaryExtensionObjectWithMaskBuilder
        // AsNullExtensionObjectWithMask converts this build to a subType of 
ExtensionObjectWithMask. It is always possible to return to current builder 
using Done()
        AsNullExtensionObjectWithMask() NullExtensionObjectWithMaskBuilder
        // Build builds the ExtensionObjectWithMask or returns an error if 
something is wrong
@@ -199,6 +208,16 @@ func (b *_ExtensionObjectWithMaskBuilder) 
AsBinaryExtensionObjectWithMask() Bina
        return cb
 }
 
+func (b *_ExtensionObjectWithMaskBuilder) AsRawBinaryExtensionObjectWithMask() 
RawBinaryExtensionObjectWithMaskBuilder {
+       if cb, ok := b.childBuilder.(RawBinaryExtensionObjectWithMaskBuilder); 
ok {
+               return cb
+       }
+       cb := 
NewRawBinaryExtensionObjectWithMaskBuilder().(*_RawBinaryExtensionObjectWithMaskBuilder)
+       cb.parentBuilder = b
+       b.childBuilder = cb
+       return cb
+}
+
 func (b *_ExtensionObjectWithMaskBuilder) AsNullExtensionObjectWithMask() 
NullExtensionObjectWithMaskBuilder {
        if cb, ok := b.childBuilder.(NullExtensionObjectWithMaskBuilder); ok {
                return cb
@@ -285,6 +304,24 @@ func (m *_ExtensionObjectWithMask) GetEncodingMask() 
ExtensionObjectEncodingMask
        return m.EncodingMask
 }
 
+///////////////////////
+///////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+/////////////////////// Accessors for virtual fields.
+///////////////////////
+
+func (pm *_ExtensionObjectWithMask) GetBodyKind() int8 {
+       m := pm._SubType
+       ctx := context.Background()
+       _ = ctx
+       return int8(utils.InlineIf(m.GetEncodingMask().GetBinaryBody(), func() 
any {
+               return int8((utils.InlineIf(m.GetStandardEncoding(), func() any 
{ return int8(int8(1)) }, func() any { return int8(int8(2)) }).(int8)))
+       }, func() any { return int8(int8(0)) }).(int8))
+}
+
 ///////////////////////
 ///////////////////////
 ///////////////////////////////////////////////////////////
@@ -311,6 +348,8 @@ func (m *_ExtensionObjectWithMask) getLengthInBits(ctx 
context.Context) uint16 {
        // Simple field (encodingMask)
        lengthInBits += m.EncodingMask.GetLengthInBits(ctx)
 
+       // A virtual field doesn't have any in- or output.
+
        return lengthInBits
 }
 
@@ -322,7 +361,7 @@ func (m *_ExtensionObjectWithMask) GetLengthInBytes(ctx 
context.Context) uint16
        return m._SubType.GetLengthInBits(ctx) / 8
 }
 
-func (m *_ExtensionObjectWithMask) parse(ctx context.Context, readBuffer 
utils.ReadBuffer, parent *_ExtensionObject, extensionId int32, 
includeEncodingMask bool) (__extensionObjectWithMask ExtensionObjectWithMask, 
err error) {
+func (m *_ExtensionObjectWithMask) parse(ctx context.Context, readBuffer 
utils.ReadBuffer, parent *_ExtensionObject, extensionId int32, standardEncoding 
bool, includeEncodingMask bool) (__extensionObjectWithMask 
ExtensionObjectWithMask, err error) {
        m.ExtensionObjectContract = parent
        positionAware := readBuffer
        _ = positionAware
@@ -338,19 +377,31 @@ func (m *_ExtensionObjectWithMask) parse(ctx 
context.Context, readBuffer utils.R
        }
        m.EncodingMask = encodingMask
 
+       bodyKind, err := ReadVirtualField[int8](ctx, "bodyKind", (*int8)(nil), 
utils.InlineIf(encodingMask.GetBinaryBody(), func() any {
+               return int8((utils.InlineIf(standardEncoding, func() any { 
return int8(int8(1)) }, func() any { return int8(int8(2)) }).(int8)))
+       }, func() any { return int8(int8(0)) }).(int8), 
codegen.WithEncoding("UTF8"))
+       if err != nil {
+               return nil, errors.Wrap(err, fmt.Sprintf("Error parsing 
'bodyKind' field"))
+       }
+       _ = bodyKind
+
        // Switch Field (Depending on the discriminator values, passes the 
instantiation to a sub-type)
        var _child ExtensionObjectWithMask
        switch {
-       case encodingMask.GetXmlBody() == (false) && 
encodingMask.GetBinaryBody() == (true): // BinaryExtensionObjectWithMask
-               if _child, err = new(_BinaryExtensionObjectWithMask).parse(ctx, 
readBuffer, m, extensionId, includeEncodingMask); err != nil {
+       case bodyKind == int8(1): // BinaryExtensionObjectWithMask
+               if _child, err = new(_BinaryExtensionObjectWithMask).parse(ctx, 
readBuffer, m, extensionId, standardEncoding, includeEncodingMask); err != nil {
                        return nil, errors.Wrap(err, "Error parsing sub-type 
BinaryExtensionObjectWithMask for type-switch of ExtensionObjectWithMask")
                }
-       case encodingMask.GetXmlBody() == (false) && 
encodingMask.GetBinaryBody() == (false): // NullExtensionObjectWithMask
-               if _child, err = new(_NullExtensionObjectWithMask).parse(ctx, 
readBuffer, m, extensionId, includeEncodingMask); err != nil {
+       case bodyKind == int8(2): // RawBinaryExtensionObjectWithMask
+               if _child, err = 
new(_RawBinaryExtensionObjectWithMask).parse(ctx, readBuffer, m, extensionId, 
standardEncoding, includeEncodingMask); err != nil {
+                       return nil, errors.Wrap(err, "Error parsing sub-type 
RawBinaryExtensionObjectWithMask for type-switch of ExtensionObjectWithMask")
+               }
+       case bodyKind == int8(0): // NullExtensionObjectWithMask
+               if _child, err = new(_NullExtensionObjectWithMask).parse(ctx, 
readBuffer, m, extensionId, standardEncoding, includeEncodingMask); err != nil {
                        return nil, errors.Wrap(err, "Error parsing sub-type 
NullExtensionObjectWithMask for type-switch of ExtensionObjectWithMask")
                }
        default:
-               return nil, errors.Errorf("Unmapped type for parameters 
[encodingMaskxmlBody=%v, encodingMaskbinaryBody=%v]", 
encodingMask.GetXmlBody(), encodingMask.GetBinaryBody())
+               return nil, errors.Errorf("Unmapped type for parameters 
[bodyKind=%v]", bodyKind)
        }
 
        if closeErr := readBuffer.CloseContext("ExtensionObjectWithMask"); 
closeErr != nil {
@@ -377,6 +428,12 @@ func (pm *_ExtensionObjectWithMask) serializeParent(ctx 
context.Context, writeBu
                if err := WriteSimpleField[ExtensionObjectEncodingMask](ctx, 
"encodingMask", m.GetEncodingMask(), 
WriteComplex[ExtensionObjectEncodingMask](writeBuffer), 
codegen.WithEncoding("UTF8")); err != nil {
                        return errors.Wrap(err, "Error serializing 
'encodingMask' field")
                }
+               // Virtual field
+               bodyKind := m.GetBodyKind()
+               _ = bodyKind
+               if _bodyKindErr := writeBuffer.WriteVirtual(ctx, "bodyKind", 
m.GetBodyKind()); _bodyKindErr != nil {
+                       return errors.Wrap(_bodyKindErr, "Error serializing 
'bodyKind' field")
+               }
 
                // Switch field (Depending on the discriminator values, passes 
the serialization to a sub-type)
                if _typeSwitchErr := serializeChildFunction(); _typeSwitchErr 
!= nil {
diff --git 
a/plc4go/protocols/opcua/readwrite/model/NullExtensionObjectWithMask.go 
b/plc4go/protocols/opcua/readwrite/model/NullExtensionObjectWithMask.go
index f9802f14c2..bc7e57d9fc 100644
--- a/plc4go/protocols/opcua/readwrite/model/NullExtensionObjectWithMask.go
+++ b/plc4go/protocols/opcua/readwrite/model/NullExtensionObjectWithMask.go
@@ -160,14 +160,6 @@ func (b *_NullExtensionObjectWithMask) 
CreateNullExtensionObjectWithMaskBuilder(
 /////////////////////// Accessors for discriminator values.
 ///////////////////////
 
-func (m *_NullExtensionObjectWithMask) GetEncodingMaskXmlBody() bool {
-       return bool(false)
-}
-
-func (m *_NullExtensionObjectWithMask) GetEncodingMaskBinaryBody() bool {
-       return bool(false)
-}
-
 ///////////////////////
 ///////////////////////
 ///////////////////////////////////////////////////////////
@@ -220,7 +212,7 @@ func (m *_NullExtensionObjectWithMask) GetLengthInBytes(ctx 
context.Context) uin
        return m.GetLengthInBits(ctx) / 8
 }
 
-func (m *_NullExtensionObjectWithMask) parse(ctx context.Context, readBuffer 
utils.ReadBuffer, parent *_ExtensionObjectWithMask, extensionId int32, 
includeEncodingMask bool) (__nullExtensionObjectWithMask 
NullExtensionObjectWithMask, err error) {
+func (m *_NullExtensionObjectWithMask) parse(ctx context.Context, readBuffer 
utils.ReadBuffer, parent *_ExtensionObjectWithMask, extensionId int32, 
standardEncoding bool, includeEncodingMask bool) (__nullExtensionObjectWithMask 
NullExtensionObjectWithMask, err error) {
        m.ExtensionObjectWithMaskContract = parent
        parent._SubType = m
        positionAware := readBuffer
diff --git 
a/plc4go/protocols/opcua/readwrite/model/RawBinaryExtensionObjectWithMask.go 
b/plc4go/protocols/opcua/readwrite/model/RawBinaryExtensionObjectWithMask.go
new file mode 100644
index 0000000000..6a5005779d
--- /dev/null
+++ b/plc4go/protocols/opcua/readwrite/model/RawBinaryExtensionObjectWithMask.go
@@ -0,0 +1,355 @@
+/*
+ * 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 model
+
+import (
+       "context"
+       stdErrors "errors"
+       "fmt"
+
+       "github.com/rs/zerolog"
+
+       "github.com/apache/plc4x/plc4go/spi/codegen"
+       . "github.com/apache/plc4x/plc4go/spi/codegen/fields"
+       . "github.com/apache/plc4x/plc4go/spi/codegen/io"
+       "github.com/apache/plc4x/plc4go/spi/errors"
+       "github.com/apache/plc4x/plc4go/spi/utils"
+)
+
+// Code generated by code-generation. DO NOT EDIT.
+
+// RawBinaryExtensionObjectWithMask is the corresponding interface of 
RawBinaryExtensionObjectWithMask
+type RawBinaryExtensionObjectWithMask interface {
+       fmt.Stringer
+       utils.LengthAware
+       utils.Serializable
+       utils.Copyable
+       ExtensionObjectWithMask
+       // GetRawBody returns RawBody (property field)
+       GetRawBody() []byte
+       // GetBody returns Body (virtual field)
+       GetBody() ExtensionObjectDefinition
+       // IsRawBinaryExtensionObjectWithMask is a marker method to prevent 
unintentional type checks (interfaces of same signature)
+       IsRawBinaryExtensionObjectWithMask()
+       // CreateBuilder creates a RawBinaryExtensionObjectWithMaskBuilder
+       CreateRawBinaryExtensionObjectWithMaskBuilder() 
RawBinaryExtensionObjectWithMaskBuilder
+}
+
+// _RawBinaryExtensionObjectWithMask is the data-structure of this message
+type _RawBinaryExtensionObjectWithMask struct {
+       ExtensionObjectWithMaskContract
+       RawBody []byte
+}
+
+var _ RawBinaryExtensionObjectWithMask = 
(*_RawBinaryExtensionObjectWithMask)(nil)
+var _ ExtensionObjectWithMaskRequirements = 
(*_RawBinaryExtensionObjectWithMask)(nil)
+
+// NewRawBinaryExtensionObjectWithMask factory function for 
_RawBinaryExtensionObjectWithMask
+func NewRawBinaryExtensionObjectWithMask(typeId ExpandedNodeId, encodingMask 
ExtensionObjectEncodingMask, rawBody []byte) *_RawBinaryExtensionObjectWithMask 
{
+       _result := &_RawBinaryExtensionObjectWithMask{
+               ExtensionObjectWithMaskContract: 
NewExtensionObjectWithMask(typeId, encodingMask),
+               RawBody:                         rawBody,
+       }
+       
_result.ExtensionObjectWithMaskContract.(*_ExtensionObjectWithMask)._SubType = 
_result
+       return _result
+}
+
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+/////////////////////// Builder
+///////////////////////
+
+// RawBinaryExtensionObjectWithMaskBuilder is a builder for 
RawBinaryExtensionObjectWithMask
+type RawBinaryExtensionObjectWithMaskBuilder interface {
+       utils.Copyable
+       // WithMandatoryFields adds all mandatory fields (convenience for using 
multiple builder calls)
+       WithMandatoryFields(rawBody []byte) 
RawBinaryExtensionObjectWithMaskBuilder
+       // WithRawBody adds RawBody (property field)
+       WithRawBody(...byte) RawBinaryExtensionObjectWithMaskBuilder
+       // Done is used to finish work on this child and return (or create one 
if none) to the parent builder
+       Done() ExtensionObjectWithMaskBuilder
+       // Build builds the RawBinaryExtensionObjectWithMask or returns an 
error if something is wrong
+       Build() (RawBinaryExtensionObjectWithMask, error)
+       // MustBuild does the same as Build but panics on error
+       MustBuild() RawBinaryExtensionObjectWithMask
+}
+
+// NewRawBinaryExtensionObjectWithMaskBuilder() creates a 
RawBinaryExtensionObjectWithMaskBuilder
+func NewRawBinaryExtensionObjectWithMaskBuilder() 
RawBinaryExtensionObjectWithMaskBuilder {
+       return 
&_RawBinaryExtensionObjectWithMaskBuilder{_RawBinaryExtensionObjectWithMask: 
new(_RawBinaryExtensionObjectWithMask)}
+}
+
+type _RawBinaryExtensionObjectWithMaskBuilder struct {
+       *_RawBinaryExtensionObjectWithMask
+
+       parentBuilder *_ExtensionObjectWithMaskBuilder
+
+       collectedErr []error
+}
+
+var _ (RawBinaryExtensionObjectWithMaskBuilder) = 
(*_RawBinaryExtensionObjectWithMaskBuilder)(nil)
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) setParent(contract 
ExtensionObjectWithMaskContract) {
+       b.ExtensionObjectWithMaskContract = contract
+       contract.(*_ExtensionObjectWithMask)._SubType = 
b._RawBinaryExtensionObjectWithMask
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) WithMandatoryFields(rawBody 
[]byte) RawBinaryExtensionObjectWithMaskBuilder {
+       return b.WithRawBody(rawBody...)
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) WithRawBody(rawBody 
...byte) RawBinaryExtensionObjectWithMaskBuilder {
+       b.RawBody = rawBody
+       return b
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) Build() 
(RawBinaryExtensionObjectWithMask, error) {
+       if err := stdErrors.Join(b.collectedErr...); err != nil {
+               return nil, errors.Wrap(err, "error occurred during build")
+       }
+       return b._RawBinaryExtensionObjectWithMask.deepCopy(), nil
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) MustBuild() 
RawBinaryExtensionObjectWithMask {
+       build, err := b.Build()
+       if err != nil {
+               panic(err)
+       }
+       return build
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) Done() 
ExtensionObjectWithMaskBuilder {
+       if b.parentBuilder == nil {
+               b.parentBuilder = 
NewExtensionObjectWithMaskBuilder().(*_ExtensionObjectWithMaskBuilder)
+       }
+       return b.parentBuilder
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) 
buildForExtensionObjectWithMask() (ExtensionObjectWithMask, error) {
+       return b.Build()
+}
+
+func (b *_RawBinaryExtensionObjectWithMaskBuilder) DeepCopy() any {
+       _copy := 
b.CreateRawBinaryExtensionObjectWithMaskBuilder().(*_RawBinaryExtensionObjectWithMaskBuilder)
+       if b.collectedErr != nil {
+               copy(_copy.collectedErr, b.collectedErr)
+       }
+       return _copy
+}
+
+// CreateRawBinaryExtensionObjectWithMaskBuilder creates a 
RawBinaryExtensionObjectWithMaskBuilder
+func (b *_RawBinaryExtensionObjectWithMask) 
CreateRawBinaryExtensionObjectWithMaskBuilder() 
RawBinaryExtensionObjectWithMaskBuilder {
+       if b == nil {
+               return NewRawBinaryExtensionObjectWithMaskBuilder()
+       }
+       return 
&_RawBinaryExtensionObjectWithMaskBuilder{_RawBinaryExtensionObjectWithMask: 
b.deepCopy()}
+}
+
+///////////////////////
+///////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+/////////////////////// Accessors for discriminator values.
+///////////////////////
+
+///////////////////////
+///////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+
+func (m *_RawBinaryExtensionObjectWithMask) GetParent() 
ExtensionObjectWithMaskContract {
+       return m.ExtensionObjectWithMaskContract
+}
+
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+/////////////////////// Accessors for property fields.
+///////////////////////
+
+func (m *_RawBinaryExtensionObjectWithMask) GetRawBody() []byte {
+       return m.RawBody
+}
+
+///////////////////////
+///////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+/////////////////////// Accessors for virtual fields.
+///////////////////////
+
+func (m *_RawBinaryExtensionObjectWithMask) GetBody() 
ExtensionObjectDefinition {
+       ctx := context.Background()
+       _ = ctx
+       return CastExtensionObjectDefinition(nil)
+}
+
+///////////////////////
+///////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+
+// Deprecated: use the interface for direct cast
+func CastRawBinaryExtensionObjectWithMask(structType any) 
RawBinaryExtensionObjectWithMask {
+       if casted, ok := structType.(RawBinaryExtensionObjectWithMask); ok {
+               return casted
+       }
+       if casted, ok := structType.(*RawBinaryExtensionObjectWithMask); ok {
+               return *casted
+       }
+       return nil
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) GetPlx4xTypeName() string {
+       return "RawBinaryExtensionObjectWithMask"
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) GetLengthInBits(ctx 
context.Context) uint16 {
+       lengthInBits := 
uint16(m.ExtensionObjectWithMaskContract.(*_ExtensionObjectWithMask).getLengthInBits(ctx))
+
+       // Implicit Field (bodyLength)
+       lengthInBits += 32
+
+       // Array field
+       if len(m.RawBody) > 0 {
+               lengthInBits += 8 * uint16(len(m.RawBody))
+       }
+
+       // A virtual field doesn't have any in- or output.
+
+       return lengthInBits
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) GetLengthInBytes(ctx 
context.Context) uint16 {
+       return m.GetLengthInBits(ctx) / 8
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) parse(ctx context.Context, 
readBuffer utils.ReadBuffer, parent *_ExtensionObjectWithMask, extensionId 
int32, standardEncoding bool, includeEncodingMask bool) 
(__rawBinaryExtensionObjectWithMask RawBinaryExtensionObjectWithMask, err 
error) {
+       m.ExtensionObjectWithMaskContract = parent
+       parent._SubType = m
+       positionAware := readBuffer
+       _ = positionAware
+       if pullErr := 
readBuffer.PullContext("RawBinaryExtensionObjectWithMask"); pullErr != nil {
+               return nil, errors.Wrap(pullErr, "Error pulling for 
RawBinaryExtensionObjectWithMask")
+       }
+       currentPos := positionAware.GetPos()
+       _ = currentPos
+
+       bodyLength, err := ReadImplicitField[int32](ctx, "bodyLength", 
ReadSignedInt(readBuffer, uint8(32)), codegen.WithEncoding("UTF8"))
+       if err != nil {
+               return nil, errors.Wrap(err, fmt.Sprintf("Error parsing 
'bodyLength' field"))
+       }
+       _ = bodyLength
+
+       rawBody, err := readBuffer.ReadByteArray("rawBody", int(bodyLength), 
codegen.WithEncoding("UTF8"))
+       if err != nil {
+               return nil, errors.Wrap(err, fmt.Sprintf("Error parsing 
'rawBody' field"))
+       }
+       m.RawBody = rawBody
+
+       body, err := ReadVirtualField[ExtensionObjectDefinition](ctx, "body", 
(*ExtensionObjectDefinition)(nil), nil, codegen.WithEncoding("UTF8"))
+       if err != nil {
+               return nil, errors.Wrap(err, fmt.Sprintf("Error parsing 'body' 
field"))
+       }
+       _ = body
+
+       if closeErr := 
readBuffer.CloseContext("RawBinaryExtensionObjectWithMask"); closeErr != nil {
+               return nil, errors.Wrap(closeErr, "Error closing for 
RawBinaryExtensionObjectWithMask")
+       }
+
+       return m, nil
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) Serialize() ([]byte, error) {
+       wb := 
utils.NewWriteBufferByteBased(utils.WithInitialSizeForByteBasedBuffer(int(m.GetLengthInBytes(context.Background()))))
+       if err := m.SerializeWithWriteBuffer(context.Background(), wb); err != 
nil {
+               return nil, err
+       }
+       return wb.GetBytes(), nil
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) SerializeWithWriteBuffer(ctx 
context.Context, writeBuffer utils.WriteBuffer) error {
+       positionAware := writeBuffer
+       _ = positionAware
+       log := zerolog.Ctx(ctx)
+       _ = log
+       ser := func() error {
+               if pushErr := 
writeBuffer.PushContext("RawBinaryExtensionObjectWithMask"); pushErr != nil {
+                       return errors.Wrap(pushErr, "Error pushing for 
RawBinaryExtensionObjectWithMask")
+               }
+               bodyLength := int32(int32(len(m.GetRawBody())))
+               if err := WriteImplicitField(ctx, "bodyLength", bodyLength, 
WriteSignedInt(writeBuffer, 32), codegen.WithEncoding("UTF8")); err != nil {
+                       return errors.Wrap(err, "Error serializing 'bodyLength' 
field")
+               }
+
+               if err := WriteByteArrayField(ctx, "rawBody", m.GetRawBody(), 
WriteByteArray(writeBuffer, 8), codegen.WithEncoding("UTF8")); err != nil {
+                       return errors.Wrap(err, "Error serializing 'rawBody' 
field")
+               }
+               // Virtual field
+               body := m.GetBody()
+               _ = body
+               if _bodyErr := writeBuffer.WriteVirtual(ctx, "body", 
m.GetBody()); _bodyErr != nil {
+                       return errors.Wrap(_bodyErr, "Error serializing 'body' 
field")
+               }
+
+               if popErr := 
writeBuffer.PopContext("RawBinaryExtensionObjectWithMask"); popErr != nil {
+                       return errors.Wrap(popErr, "Error popping for 
RawBinaryExtensionObjectWithMask")
+               }
+               return nil
+       }
+       return 
m.ExtensionObjectWithMaskContract.(*_ExtensionObjectWithMask).serializeParent(ctx,
 writeBuffer, m, ser)
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) 
IsRawBinaryExtensionObjectWithMask() {}
+
+func (m *_RawBinaryExtensionObjectWithMask) DeepCopy() any {
+       return m.deepCopy()
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) deepCopy() 
*_RawBinaryExtensionObjectWithMask {
+       if m == nil {
+               return nil
+       }
+       _RawBinaryExtensionObjectWithMaskCopy := 
&_RawBinaryExtensionObjectWithMask{
+               
m.ExtensionObjectWithMaskContract.(*_ExtensionObjectWithMask).deepCopy(),
+               utils.DeepCopySlice[byte, byte](m.RawBody),
+       }
+       
_RawBinaryExtensionObjectWithMaskCopy.ExtensionObjectWithMaskContract.(*_ExtensionObjectWithMask)._SubType
 = m
+       return _RawBinaryExtensionObjectWithMaskCopy
+}
+
+func (m *_RawBinaryExtensionObjectWithMask) String() string {
+       if m == nil {
+               return "<nil>"
+       }
+       wb := utils.NewWriteBufferBoxBased(
+               utils.WithWriteBufferBoxBasedMergeSingleBoxes(),
+               utils.WithWriteBufferBoxBasedOmitEmptyBoxes(),
+               utils.WithWriteBufferBoxBasedPrintPosLengthFooter(),
+       )
+       if err := wb.WriteSerializable(context.Background(), m); err != nil {
+               return err.Error()
+       }
+       return wb.GetBox().String()
+}
diff --git a/plc4go/protocols/opcua/readwrite/model/StaticHelper.go 
b/plc4go/protocols/opcua/readwrite/model/StaticHelper.go
index 3a62e71c98..870e119e06 100644
--- a/plc4go/protocols/opcua/readwrite/model/StaticHelper.go
+++ b/plc4go/protocols/opcua/readwrite/model/StaticHelper.go
@@ -42,3 +42,34 @@ func ExtensionId(_ context.Context, expandedNodeId 
ExpandedNodeId) int32 {
        }
        return int32(nodeId)
 }
+
+// IsStandardEncoding reports whether the encoding NodeId of an 
ExtensionObject refers to a
+// well-known OPC UA standard type, i.e. it lives in namespace 0 and is not 
referenced by a
+// namespace URI. Custom / user-defined structure encodings live in namespace 
>= 1; their bodies
+// can't be decoded by the generated dispatch and are captured as raw bytes 
instead.
+func IsStandardEncoding(_ context.Context, expandedNodeId ExpandedNodeId) bool 
{
+       if expandedNodeId == nil {
+               return true
+       }
+       // A namespace referenced by URI is, by definition, not the standard 
OPC UA namespace (0).
+       if expandedNodeId.GetNamespaceURISpecified() {
+               return false
+       }
+       switch nodeId := expandedNodeId.GetNodeId().(type) {
+       case NodeIdTwoByte:
+               // The two-byte form is always namespace 0.
+               return true
+       case NodeIdFourByte:
+               return nodeId.GetNamespaceIndex() == 0
+       case NodeIdNumeric:
+               return nodeId.GetNamespaceIndex() == 0
+       case NodeIdString:
+               return nodeId.GetNamespaceIndex() == 0
+       case NodeIdGuid:
+               return nodeId.GetNamespaceIndex() == 0
+       case NodeIdByteString:
+               return nodeId.GetNamespaceIndex() == 0
+       default:
+               return false
+       }
+}
diff --git 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
index 130f93f069..e3e47081e1 100644
--- 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
+++ 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
@@ -39,22 +39,6 @@ public class BinaryExtensionObjectWithMask extends 
ExtensionObjectWithMask imple
     this.body = body;
   }
 
-  /**
-   * Discriminator field encodingMaskXmlBody
-   */
-  @Override
-  public boolean getEncodingMaskXmlBody() {
-    return (boolean) false;
-  }
-
-  /**
-   * Discriminator field encodingMaskBinaryBody
-   */
-  @Override
-  public boolean getEncodingMaskBinaryBody() {
-    return (boolean) true;
-  }
-
   /**
    * Property field body
    */
@@ -63,7 +47,8 @@ public class BinaryExtensionObjectWithMask extends 
ExtensionObjectWithMask imple
   }
 
   public static ExtensionObjectWithMaskBuilder 
staticParseExtensionObjectWithMaskBuilder(
-      ReadBuffer readBuffer, int extensionId, boolean includeEncodingMask) 
throws BufferException {
+      ReadBuffer readBuffer, int extensionId, boolean standardEncoding, 
boolean includeEncodingMask)
+      throws BufferException {
     
readBuffer.pushContext(WithOption.WithName("BinaryExtensionObjectWithMask"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
     int startPos = readBuffer.getPositionInBits();
     boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
diff --git 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObject.java
 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObject.java
index b4149f4651..67687d2d86 100644
--- 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObject.java
+++ 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObject.java
@@ -66,6 +66,13 @@ public abstract class ExtensionObject implements Message {
     return (int) ((((typeId) == (null)) ? 0 : 
StaticHelper.extensionId(typeId)));
   }
 
+  /**
+   * Virtual field standardEncoding
+   */
+  public boolean getStandardEncoding() {
+    return (boolean) (StaticHelper.isStandardEncoding(typeId));
+  }
+
   public static ExtensionObject staticParse(ReadBuffer readBuffer, boolean 
includeEncodingMask)
       throws BufferException {
     readBuffer.pushContext(WithOption.WithName("ExtensionObject"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
@@ -77,12 +84,15 @@ public abstract class ExtensionObject implements Message {
     // Virtual Field: extensionId (doesn't parse anything, just makes the 
value available)
     int extensionId = FieldReaderFactory.readVirtualField(int.class, 
(((typeId) == (null)) ? 0 : StaticHelper.extensionId(typeId)), 
WithOption.WithName("extensionId"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
 
+    // Virtual Field: standardEncoding (doesn't parse anything, just makes the 
value available)
+    boolean standardEncoding = 
FieldReaderFactory.readVirtualField(boolean.class, 
StaticHelper.isStandardEncoding(typeId), 
WithOption.WithName("standardEncoding"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+
     // Switch Field
     ExtensionObjectBuilder builder = null;
     if (EvaluationHelper.equals(includeEncodingMask, (boolean) (false))) {
       builder = 
RootExtensionObject.staticParseExtensionObjectBuilder(readBuffer, extensionId, 
includeEncodingMask);
     } else if (EvaluationHelper.equals(includeEncodingMask, (boolean) (true))) 
{
-      builder = 
ExtensionObjectWithMask.staticParseExtensionObjectBuilder(readBuffer, 
extensionId, includeEncodingMask);
+      builder = 
ExtensionObjectWithMask.staticParseExtensionObjectBuilder(readBuffer, 
extensionId, standardEncoding, includeEncodingMask);
     }
     if (builder == null) {
       throw new BufferException("Unsupported case for discriminated type 
parameters parameters [includeEncodingMask]");
@@ -103,6 +113,9 @@ public abstract class ExtensionObject implements Message {
     // Virtual Field: extensionId (doesn't serialize anything, just makes the 
value available)
     int extensionId = (int) getExtensionId();
 
+    // Virtual Field: standardEncoding (doesn't serialize anything, just makes 
the value available)
+    boolean standardEncoding = (boolean) getStandardEncoding();
+
     // Switch Field
     serializeExtensionObjectChild(writeBuffer);
 
@@ -124,6 +137,8 @@ public abstract class ExtensionObject implements Message {
 
     // Virtual Field: extensionId (doesn't produce any output, just makes the 
value available)
 
+    // Virtual Field: standardEncoding (doesn't produce any output, just makes 
the value available)
+
     // Switch Field
     // (the subtype will instead add the actual length of subtype elements)
 
diff --git 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObjectWithMask.java
 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObjectWithMask.java
index 7ba9e0c135..d1938e88f5 100644
--- 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObjectWithMask.java
+++ 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/ExtensionObjectWithMask.java
@@ -39,16 +39,6 @@ public abstract class ExtensionObjectWithMask extends 
ExtensionObject implements
     this.encodingMask = encodingMask;
   }
 
-  /**
-   * Discriminator field encodingMaskBinaryBody
-   */
-  public abstract boolean getEncodingMaskBinaryBody();
-
-  /**
-   * Discriminator field encodingMaskXmlBody
-   */
-  public abstract boolean getEncodingMaskXmlBody();
-
   protected abstract void serializeExtensionObjectWithMaskChild(WriteBuffer 
writeBuffer) throws
       BufferException;
 
@@ -67,23 +57,36 @@ public abstract class ExtensionObjectWithMask extends 
ExtensionObject implements
     return encodingMask;
   }
 
+  /**
+   * Virtual field bodyKind
+   */
+  public byte getBodyKind() {
+    return (byte) (((encodingMask.getBinaryBody()) ? (((getStandardEncoding()) 
? 1 : 2)) : 0));
+  }
+
   public static ExtensionObjectBuilder 
staticParseExtensionObjectBuilder(ReadBuffer readBuffer,
-      int extensionId, boolean includeEncodingMask) throws BufferException {
+      int extensionId, boolean standardEncoding, boolean includeEncodingMask) 
throws
+      BufferException {
     readBuffer.pushContext(WithOption.WithName("ExtensionObjectWithMask"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
     int startPos = readBuffer.getPositionInBits();
     boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
     // Simple Field: encodingMask
     ExtensionObjectEncodingMask encodingMask = 
FieldReaderFactory.readSimpleField(DataReaderFactory.readComplex(() -> 
(ExtensionObjectEncodingMask) 
ExtensionObjectEncodingMask.staticParse(readBuffer), readBuffer), 
WithOption.WithName("encodingMask"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
 
+    // Virtual Field: bodyKind (doesn't parse anything, just makes the value 
available)
+    byte bodyKind = FieldReaderFactory.readVirtualField(byte.class, 
((encodingMask.getBinaryBody()) ? (((standardEncoding) ? 1 : 2)) : 0), 
WithOption.WithName("bodyKind"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+
     // Switch Field
     ExtensionObjectWithMaskBuilder builder = null;
-    if (EvaluationHelper.equals(encodingMask.getXmlBody(), (boolean) (false)) 
&& EvaluationHelper.equals(encodingMask.getBinaryBody(), (boolean) (true))) {
-      builder = 
BinaryExtensionObjectWithMask.staticParseExtensionObjectWithMaskBuilder(readBuffer,
 extensionId, includeEncodingMask);
-    } else if (EvaluationHelper.equals(encodingMask.getXmlBody(), (boolean) 
(false)) && EvaluationHelper.equals(encodingMask.getBinaryBody(), (boolean) 
(false))) {
-      builder = 
NullExtensionObjectWithMask.staticParseExtensionObjectWithMaskBuilder(readBuffer,
 extensionId, includeEncodingMask);
+    if (EvaluationHelper.equals(bodyKind, (byte) (1))) {
+      builder = 
BinaryExtensionObjectWithMask.staticParseExtensionObjectWithMaskBuilder(readBuffer,
 extensionId, standardEncoding, includeEncodingMask);
+    } else if (EvaluationHelper.equals(bodyKind, (byte) (2))) {
+      builder = 
RawBinaryExtensionObjectWithMask.staticParseExtensionObjectWithMaskBuilder(readBuffer,
 extensionId, standardEncoding, includeEncodingMask);
+    } else if (EvaluationHelper.equals(bodyKind, (byte) (0))) {
+      builder = 
NullExtensionObjectWithMask.staticParseExtensionObjectWithMaskBuilder(readBuffer,
 extensionId, standardEncoding, includeEncodingMask);
     }
     if (builder == null) {
-      throw new BufferException("Unsupported case for discriminated type 
parameters parameters [encodingMask.getXmlBody(), 
encodingMask.getBinaryBody()]");
+      throw new BufferException("Unsupported case for discriminated type 
parameters parameters [bodyKind]");
     }
 
     readBuffer.popContext();
@@ -97,6 +100,9 @@ public abstract class ExtensionObjectWithMask extends 
ExtensionObject implements
     // Simple Field: encodingMask
     FieldWriterFactory.writeSimpleField((ExtensionObjectEncodingMask) 
encodingMask, DataWriterFactory.writeComplex(writeBuffer), 
WithOption.WithName("encodingMask"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
 
+    // Virtual Field: bodyKind (doesn't serialize anything, just makes the 
value available)
+    byte bodyKind = (byte) getBodyKind();
+
     // Switch Field
     serializeExtensionObjectWithMaskChild(writeBuffer);
 
@@ -116,6 +122,8 @@ public abstract class ExtensionObjectWithMask extends 
ExtensionObject implements
     // Simple Field: encodingMask
     lengthInBits += encodingMask.getLengthInBits();
 
+    // Virtual Field: bodyKind (doesn't produce any output, just makes the 
value available)
+
     // Switch Field
     // (the subtype will instead add the actual length of subtype elements)
 
diff --git 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/NullExtensionObjectWithMask.java
 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/NullExtensionObjectWithMask.java
index ab3bf6c2d5..7ad746fbe0 100644
--- 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/NullExtensionObjectWithMask.java
+++ 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/NullExtensionObjectWithMask.java
@@ -33,22 +33,6 @@ public class NullExtensionObjectWithMask extends 
ExtensionObjectWithMask impleme
     super(typeId, encodingMask);
   }
 
-  /**
-   * Discriminator field encodingMaskXmlBody
-   */
-  @Override
-  public boolean getEncodingMaskXmlBody() {
-    return (boolean) false;
-  }
-
-  /**
-   * Discriminator field encodingMaskBinaryBody
-   */
-  @Override
-  public boolean getEncodingMaskBinaryBody() {
-    return (boolean) false;
-  }
-
   /**
    * Virtual field body
    */
@@ -57,7 +41,8 @@ public class NullExtensionObjectWithMask extends 
ExtensionObjectWithMask impleme
   }
 
   public static ExtensionObjectWithMaskBuilder 
staticParseExtensionObjectWithMaskBuilder(
-      ReadBuffer readBuffer, int extensionId, boolean includeEncodingMask) 
throws BufferException {
+      ReadBuffer readBuffer, int extensionId, boolean standardEncoding, 
boolean includeEncodingMask)
+      throws BufferException {
     readBuffer.pushContext(WithOption.WithName("NullExtensionObjectWithMask"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
     int startPos = readBuffer.getPositionInBits();
     boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
diff --git 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/RawBinaryExtensionObjectWithMask.java
similarity index 56%
copy from 
plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
copy to 
plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/RawBinaryExtensionObjectWithMask.java
index 130f93f069..9500401643 100644
--- 
a/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/BinaryExtensionObjectWithMask.java
+++ 
b/plc4j/drivers/opcua/src/main/generated/org/apache/plc4x/java/opcua/readwrite/RawBinaryExtensionObjectWithMask.java
@@ -26,68 +26,67 @@ import 
org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory;
 import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory;
 import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory;
 import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper;
+import org.apache.plc4x.java.spi.utils.StaticHelper;
 
 /**
  * Code generated by code-generation. DO NOT EDIT.
  */
-public class BinaryExtensionObjectWithMask extends ExtensionObjectWithMask 
implements Message {
-  protected final ExtensionObjectDefinition body;
+public class RawBinaryExtensionObjectWithMask extends ExtensionObjectWithMask 
implements Message {
+  protected final byte[] rawBody;
 
-  public BinaryExtensionObjectWithMask(ExpandedNodeId typeId,
-      ExtensionObjectEncodingMask encodingMask, ExtensionObjectDefinition 
body) {
+  public RawBinaryExtensionObjectWithMask(ExpandedNodeId typeId,
+      ExtensionObjectEncodingMask encodingMask, byte[] rawBody) {
     super(typeId, encodingMask);
-    this.body = body;
+    this.rawBody = rawBody;
   }
 
   /**
-   * Discriminator field encodingMaskXmlBody
+   * Property field rawBody
    */
-  @Override
-  public boolean getEncodingMaskXmlBody() {
-    return (boolean) false;
+  public byte[] getRawBody() {
+    return rawBody;
   }
 
   /**
-   * Discriminator field encodingMaskBinaryBody
-   */
-  @Override
-  public boolean getEncodingMaskBinaryBody() {
-    return (boolean) true;
-  }
-
-  /**
-   * Property field body
+   * Virtual field body
    */
   public ExtensionObjectDefinition getBody() {
-    return body;
+    return (ExtensionObjectDefinition) (null);
   }
 
   public static ExtensionObjectWithMaskBuilder 
staticParseExtensionObjectWithMaskBuilder(
-      ReadBuffer readBuffer, int extensionId, boolean includeEncodingMask) 
throws BufferException {
-    
readBuffer.pushContext(WithOption.WithName("BinaryExtensionObjectWithMask"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+      ReadBuffer readBuffer, int extensionId, boolean standardEncoding, 
boolean includeEncodingMask)
+      throws BufferException {
+    
readBuffer.pushContext(WithOption.WithName("RawBinaryExtensionObjectWithMask"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
     int startPos = readBuffer.getPositionInBits();
     boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
     // Implicit Field: bodyLength
     int bodyLength = 
FieldReaderFactory.readImplicitField(DataReaderFactory.readSignedInt(readBuffer,
 32), WithOption.WithName("bodyLength"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
 
-    // Simple Field: body
-    ExtensionObjectDefinition body = 
FieldReaderFactory.readSimpleField(DataReaderFactory.readComplex(() -> 
(ExtensionObjectDefinition) ExtensionObjectDefinition.staticParse(readBuffer, 
(int) (extensionId)), readBuffer), WithOption.WithName("body"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+    // Array Field: rawBody
+    byte[] rawBody = readBuffer.readBits(Math.toIntExact((bodyLength) * 8), 
WithOption.WithName("rawBody"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+
+    // Virtual Field: body (doesn't parse anything, just makes the value 
available)
+    ExtensionObjectDefinition body = 
FieldReaderFactory.readVirtualField(ExtensionObjectDefinition.class, null, 
WithOption.WithName("body"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
 
     readBuffer.popContext();
-    return new ExtensionObjectWithMaskBuilderImpl(body);
+    return new ExtensionObjectWithMaskBuilderImpl(rawBody);
   }
 
   protected void serializeExtensionObjectWithMaskChild(WriteBuffer 
writeBuffer) throws
       BufferException {
-    
writeBuffer.pushContext(WithOption.WithName("BinaryExtensionObjectWithMask"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+    
writeBuffer.pushContext(WithOption.WithName("RawBinaryExtensionObjectWithMask"),
 WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
     int startPos = writeBuffer.getPositionInBits();
     boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
     // Implicit Field: bodyLength
-    int bodyLength = (int) ((((body) == (null)) ? 0 : 
body.getLengthInBytes()));
+    int bodyLength = (int) (StaticHelper.COUNT(rawBody));
     FieldWriterFactory.writeImplicitField((int) bodyLength, 
DataWriterFactory.writeSignedInt(writeBuffer, 32), 
WithOption.WithName("bodyLength"), WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
 
-    // Simple Field: body
-    FieldWriterFactory.writeSimpleField((ExtensionObjectDefinition) body, 
DataWriterFactory.writeComplex(writeBuffer), WithOption.WithName("body"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+    // Array Field: rawBody
+    FieldWriterFactory.writeByteArrayField(rawBody, 
DataWriterFactory.writeByteArray(writeBuffer, (int) ((rawBody != null) ? 
rawBody.length : 0)), WithOption.WithName("rawBody"), 
WithOption.WithFloatEncoding("IEEE754"), 
WithOption.WithSignedIntegerEncoding("twos-complement"), 
WithOption.WithUnsignedIntegerEncoding("unsigned-binary"), 
WithOption.WithStringEncoding("UTF8"));
+
+    // Virtual Field: body (doesn't serialize anything, just makes the value 
available)
+    ExtensionObjectDefinition body = (ExtensionObjectDefinition) getBody();
 
     writeBuffer.popContext();
   }
@@ -100,27 +99,29 @@ public class BinaryExtensionObjectWithMask extends 
ExtensionObjectWithMask imple
   @Override
   public int getLengthInBits() {
     int lengthInBits = super.getLengthInBits();
-    BinaryExtensionObjectWithMask _value = this;
+    RawBinaryExtensionObjectWithMask _value = this;
     boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get();
     // Implicit Field: bodyLength
     lengthInBits += 32;
 
-    // Simple Field: body
-    lengthInBits += body.getLengthInBits();
+    // Array Field: rawBody
+    lengthInBits += 8 * ((rawBody != null) ? rawBody.length : 0);
+
+    // Virtual Field: body (doesn't produce any output, just makes the value 
available)
 
     return lengthInBits;
   }
 
   public static class ExtensionObjectWithMaskBuilderImpl implements 
ExtensionObjectWithMask.ExtensionObjectWithMaskBuilder {
-    private final ExtensionObjectDefinition body;
+    private final byte[] rawBody;
 
-    public ExtensionObjectWithMaskBuilderImpl(ExtensionObjectDefinition body) {
-      this.body = body;
+    public ExtensionObjectWithMaskBuilderImpl(byte[] rawBody) {
+      this.rawBody = rawBody;
     }
 
     public ExtensionObjectWithMask build(ExpandedNodeId typeId,
         ExtensionObjectEncodingMask encodingMask) {
-      return new BinaryExtensionObjectWithMask(typeId, encodingMask, body);
+      return new RawBinaryExtensionObjectWithMask(typeId, encodingMask, 
rawBody);
     }
   }
 }
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
index 07ac00e679..0d132563cc 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
@@ -48,7 +48,12 @@ import 
org.apache.plc4x.java.opcua.context.OpcuaDriverContext;
 import org.apache.plc4x.java.opcua.context.OpcuaWire;
 import org.apache.plc4x.java.opcua.context.SecureChannel;
 import org.apache.plc4x.java.opcua.protocol.OpcuaSubscriptionHandle;
+import org.apache.plc4x.java.opcua.protocol.chunk.PayloadConverter;
 import org.apache.plc4x.java.opcua.readwrite.*;
+import org.apache.plc4x.java.spi.buffers.api.ReadBuffer;
+import org.apache.plc4x.java.spi.buffers.api.WriteBuffer;
+import org.apache.plc4x.java.spi.buffers.bytebased.ReadBufferByteBased;
+import org.apache.plc4x.java.spi.buffers.bytebased.WriteBufferByteBased;
 import org.apache.plc4x.java.opcua.tag.OpcuaPlcTagHandler;
 import org.apache.plc4x.java.opcua.tag.OpcuaTag;
 import org.apache.plc4x.java.spi.drivers.ConnectionBase;
@@ -63,7 +68,12 @@ import 
org.apache.plc4x.java.spi.drivers.messages.DefaultPlcSubscriptionTag;
 import org.apache.plc4x.java.spi.transports.api.TransportInstance;
 import org.apache.plc4x.java.transport.tcp.TcpTransportInstance;
 
+import java.io.ByteArrayInputStream;
 import java.net.InetSocketAddress;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
 import org.apache.plc4x.java.spi.values.*;
 import org.apache.plc4x.java.utils.auditlog.api.AuditLog;
 import org.slf4j.Logger;
@@ -114,6 +124,11 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
     // re-reading — both browsing and (Phase 4) typed read/write resolve 
through this cache.
     private final Map<String, NodeAttributes> nodeTypeCache = new 
ConcurrentHashMap<>();
 
+    // Phase 5: per-session cache of resolved StructureDefinitions (the field 
layout of custom struct
+    // types), keyed by the DataType node address. Populated lazily on the 
first read of a value of
+    // that type and reused for all subsequent reads/writes.
+    private final Map<String, StructureDefinition> structureDefinitionCache = 
new ConcurrentHashMap<>();
+
     public OpcuaConnection(OpcuaConfiguration configuration,
                            TransportInstance<?> transportInstance,
                            AuditLog auditLog) {
@@ -330,9 +345,31 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
             readValueArray
         );
 
-        return conversation.submit(opcuaReadRequest, 
ReadResponse.class).thenApply(response -> {
-            Map<String, PlcResponseItem<PlcValue>> mappedResponse = 
readResponse(tagMap, response.getResults());
-            return new DefaultPlcReadResponse(request, mappedResponse);
+        return conversation.submit(opcuaReadRequest, 
ReadResponse.class).thenCompose(response -> {
+            List<DataValue> results = response.getResults();
+            // Any tag whose value came back as a custom struct (captured as 
raw bytes) needs its
+            // StructureDefinition resolved from the server before the raw 
body can be decoded into a
+            // PlcStruct. Resolve those up-front (cached per type), then map 
the whole response.
+            Map<String, CompletableFuture<StructureDefinition>> structFutures 
= new LinkedHashMap<>();
+            int index = 0;
+            for (String tagName : tagMap.keySet()) {
+                DataValue dataValue = results.get(index++);
+                if (dataValue.getValueSpecified() && 
containsRawStruct(dataValue.getValue())) {
+                    ExpandedNodeId encodingNodeId = 
rawStructEncodingId(dataValue.getValue());
+                    structFutures.put(tagName,
+                        resolveStructureDefinition((OpcuaTag) 
tagMap.get(tagName), encodingNodeId));
+                }
+            }
+            if (structFutures.isEmpty()) {
+                return CompletableFuture.completedFuture(new 
DefaultPlcReadResponse(request,
+                    readResponse(tagMap, results, Collections.emptyMap())));
+            }
+            return CompletableFuture.allOf(structFutures.values().toArray(new 
CompletableFuture[0]))
+                .thenApply(v -> {
+                    Map<String, StructureDefinition> structDefs = new 
HashMap<>();
+                    structFutures.forEach((name, future) -> 
structDefs.put(name, future.getNow(null)));
+                    return new DefaultPlcReadResponse(request, 
readResponse(tagMap, results, structDefs));
+                });
         });
     }
 
@@ -365,6 +402,11 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
     }
 
     public Map<String, PlcResponseItem<PlcValue>> readResponse(Map<String, 
PlcTag> tagMap, List<DataValue> results) {
+        return readResponse(tagMap, results, Collections.emptyMap());
+    }
+
+    public Map<String, PlcResponseItem<PlcValue>> readResponse(Map<String, 
PlcTag> tagMap, List<DataValue> results,
+                                                               Map<String, 
StructureDefinition> structDefs) {
         Map<String, PlcResponseItem<PlcValue>> response = new HashMap<>();
         int index = 0;
         for (String tagName : tagMap.keySet()) {
@@ -373,7 +415,12 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
             DataValue dataValue = results.get(index++);
             PlcResponseCode responseCode = PlcResponseCode.OK;
             if (dataValue.getValueSpecified()) {
-                value = variantToPlcValue(tag, dataValue.getValue());
+                StructureDefinition structDef = structDefs.get(tagName);
+                if (structDef != null && dataValue.getValue() instanceof 
VariantExtensionObject) {
+                    value = extensionObjectToPlcValue((VariantExtensionObject) 
dataValue.getValue(), structDef);
+                } else {
+                    value = variantToPlcValue(tag, dataValue.getValue());
+                }
                 if (value == null) {
                     LOGGER.error("Variant type {} is not supported.", 
dataValue.getValue().getClass());
                     responseCode = PlcResponseCode.UNSUPPORTED;
@@ -389,6 +436,572 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
         return response;
     }
 
+    // 
======================================================================================
+    // Struct (PlcStruct) support — decode custom structure values captured as 
raw ExtensionObject
+    // bodies against the server-declared StructureDefinition.
+    // 
======================================================================================
+
+    /** Whether a Variant carries at least one custom struct captured as raw 
bytes. */
+    private static boolean containsRawStruct(Variant variant) {
+        if (!(variant instanceof VariantExtensionObject)) {
+            return false;
+        }
+        List<ExtensionObject> extensionObjects = ((VariantExtensionObject) 
variant).getValue();
+        if (extensionObjects == null) {
+            return false;
+        }
+        return extensionObjects.stream().anyMatch(eo -> eo instanceof 
RawBinaryExtensionObjectWithMask);
+    }
+
+    /**
+     * Resolves (and caches, by encoding node) the {@link StructureDefinition} 
of a struct-typed
+     * value. Tries the modern DataTypeDefinition attribute first; if the 
server doesn't expose it
+     * (pre-1.04 servers), falls back to the legacy binary type dictionary 
reachable from the
+     * encoding node. Returns null if neither yields a layout.
+     */
+    private CompletableFuture<StructureDefinition> 
resolveStructureDefinition(OpcuaTag tag, ExpandedNodeId encodingNodeId) {
+        String key = encodingNodeId != null ? nodeIdKey(new 
NodeId(encodingNodeId.getNodeId())) : cacheKey(tag);
+        StructureDefinition cached = structureDefinitionCache.get(key);
+        if (cached != null) {
+            return CompletableFuture.completedFuture(cached);
+        }
+        return resolveStructureDefinitionModern(tag).thenCompose(modern -> {
+            if (modern != null) {
+                structureDefinitionCache.put(key, modern);
+                return CompletableFuture.completedFuture(modern);
+            }
+            if (encodingNodeId == null) {
+                return CompletableFuture.completedFuture(null);
+            }
+            return 
resolveStructureDefinitionFromDictionary(encodingNodeId).thenApply(legacy -> {
+                if (legacy != null) {
+                    structureDefinitionCache.put(key, legacy);
+                }
+                return legacy;
+            });
+        }).exceptionally(error -> {
+            LOGGER.warn("Failed to resolve StructureDefinition for tag '{}'", 
tag, error);
+            return null;
+        });
+    }
+
+    /** Modern path: variable DataType (attr 14) -&gt; data-type node -&gt; 
DataTypeDefinition (attr 23). */
+    private CompletableFuture<StructureDefinition> 
resolveStructureDefinitionModern(OpcuaTag tag) {
+        NodeId variableNode = generateNodeId(tag);
+        ReadValueId dataTypeRead = new ReadValueId(variableNode, 
AttributeId.DataType.getValue(),
+            NULL_STRING, new QualifiedName(0, NULL_STRING));
+        ReadRequest request = new 
ReadRequest(conversation.createRequestHeader(), 0.0d,
+            TimestampsToReturn.timestampsToReturnNeither, 
Collections.singletonList(dataTypeRead));
+        return conversation.submit(request, 
ReadResponse.class).thenCompose(response -> {
+            NodeId dataTypeNodeId = 
dataTypeNodeIdOf(response.getResults().get(0));
+            if (dataTypeNodeId == null) {
+                return CompletableFuture.completedFuture(null);
+            }
+            return readStructureDefinition(dataTypeNodeId);
+        });
+    }
+
+    /** Reads the DataTypeDefinition attribute (23) of a data-type node and 
extracts its StructureDefinition. */
+    private CompletableFuture<StructureDefinition> 
readStructureDefinition(NodeId dataTypeNodeId) {
+        ReadValueId definitionRead = new ReadValueId(dataTypeNodeId, 
AttributeId.DataTypeDefinition.getValue(),
+            NULL_STRING, new QualifiedName(0, NULL_STRING));
+        ReadRequest request = new 
ReadRequest(conversation.createRequestHeader(), 0.0d,
+            TimestampsToReturn.timestampsToReturnNeither, 
Collections.singletonList(definitionRead));
+        return conversation.submit(request, 
ReadResponse.class).thenApply(response -> {
+            DataValue value = response.getResults().get(0);
+            if (!value.getValueSpecified() || !(value.getValue() instanceof 
VariantExtensionObject)) {
+                // Server doesn't expose the DataTypeDefinition attribute 
(e.g. pre-1.04 servers
+                // return BadAttributeIdInvalid); the layout must come from 
the legacy type
+                // dictionary instead (handled elsewhere).
+                return null;
+            }
+            List<ExtensionObject> extensionObjects = ((VariantExtensionObject) 
value.getValue()).getValue();
+            if (extensionObjects == null || extensionObjects.isEmpty()) {
+                return null;
+            }
+            // The DataTypeDefinition is a well-known standard type 
(ns=0;i=101), so it is parsed
+            // into a typed BinaryExtensionObjectWithMask whose body is the 
StructureDefinition.
+            ExtensionObjectDefinition body = extensionObjects.get(0).getBody();
+            return (body instanceof StructureDefinition) ? 
(StructureDefinition) body : null;
+        });
+    }
+
+    // Standard reference-type NodeIds used to walk from a struct's 
binary-encoding node to its
+    // entry in the legacy type dictionary: encoding --HasDescription--> 
description, and the
+    // dictionary --HasComponent--> description (so description 
--HasComponent(inverse)--> dictionary).
+    private static final long HAS_COMPONENT = 47L;
+    private static final long HAS_DESCRIPTION = 39L;
+
+    /** The encoding NodeId of the first custom struct (raw ExtensionObject) 
in a Variant, or null. */
+    private static ExpandedNodeId rawStructEncodingId(Variant variant) {
+        if (!(variant instanceof VariantExtensionObject)) {
+            return null;
+        }
+        for (ExtensionObject extensionObject : ((VariantExtensionObject) 
variant).getValue()) {
+            if (extensionObject instanceof RawBinaryExtensionObjectWithMask) {
+                return extensionObject.getTypeId();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Legacy path: resolve a struct's field layout from the binary type 
dictionary. Walks the
+     * encoding node to its DataTypeDescription (which names the type in the 
dictionary) and to the
+     * DataTypeDictionary node (whose value is the OPC binary schema XML), 
then parses that schema.
+     */
+    private CompletableFuture<StructureDefinition> 
resolveStructureDefinitionFromDictionary(ExpandedNodeId encodingNodeId) {
+        NodeId encodingNode = new NodeId(encodingNodeId.getNodeId());
+        return browseFirstTarget(encodingNode, HAS_DESCRIPTION, 
BrowseDirection.browseDirectionForward)
+            .thenCompose(descriptionNode -> {
+                if (descriptionNode == null) {
+                    return CompletableFuture.completedFuture(null);
+                }
+                CompletableFuture<DataValue> nameValue = 
readNodeValue(descriptionNode, AttributeId.Value.getValue());
+                CompletableFuture<NodeId> dictionaryNode =
+                    browseFirstTarget(descriptionNode, HAS_COMPONENT, 
BrowseDirection.browseDirectionInverse);
+                return nameValue.thenCombine(dictionaryNode, (nv, dn) -> new 
Object[]{nv, dn}).thenCompose(pair -> {
+                    String typeName = stringValueOf((DataValue) pair[0]);
+                    NodeId dictNode = (NodeId) pair[1];
+                    if (typeName == null || dictNode == null) {
+                        return CompletableFuture.completedFuture(null);
+                    }
+                    return readNodeValue(dictNode, 
AttributeId.Value.getValue()).thenApply(dictValue -> {
+                        byte[] schema = byteStringValueOf(dictValue);
+                        return schema == null ? null : 
parseBsdStructure(schema, typeName, encodingNode);
+                    });
+                });
+            });
+    }
+
+    /** Browses one reference of the given type/direction and returns the 
first target node, or null. */
+    private CompletableFuture<NodeId> browseFirstTarget(NodeId source, long 
referenceTypeId, BrowseDirection direction) {
+        BrowseDescription description = new BrowseDescription(source, 
direction,
+            new NodeId(new NodeIdNumeric(0, referenceTypeId)), true, 0L, 
BROWSE_RESULT_MASK_ALL);
+        BrowseRequest request = new 
BrowseRequest(conversation.createRequestHeader(),
+            new ViewDescription(new NodeId(new NodeIdTwoByte((short) 0)), 0L, 
0L), 0L,
+            Collections.singletonList(description));
+        return conversation.submit(request, 
BrowseResponse.class).thenApply(response -> {
+            BrowseResult result = response.getResults().get(0);
+            if (result.getReferences() == null || 
result.getReferences().isEmpty()) {
+                return null;
+            }
+            ExpandedNodeId target = result.getReferences().get(0).getNodeId();
+            return target == null ? null : new NodeId(target.getNodeId());
+        });
+    }
+
+    /** Reads a single attribute of a node and returns the raw DataValue. */
+    private CompletableFuture<DataValue> readNodeValue(NodeId node, long 
attributeId) {
+        ReadValueId readValueId = new ReadValueId(node, attributeId, 
NULL_STRING, new QualifiedName(0, NULL_STRING));
+        ReadRequest request = new 
ReadRequest(conversation.createRequestHeader(), 0.0d,
+            TimestampsToReturn.timestampsToReturnNeither, 
Collections.singletonList(readValueId));
+        return conversation.submit(request, 
ReadResponse.class).thenApply(response -> response.getResults().get(0));
+    }
+
+    private static String stringValueOf(DataValue dataValue) {
+        if (dataValue == null || !dataValue.getValueSpecified() || 
!(dataValue.getValue() instanceof VariantString)) {
+            return null;
+        }
+        List<PascalString> values = ((VariantString) 
dataValue.getValue()).getValue();
+        return (values == null || values.isEmpty()) ? null : 
values.get(0).getStringValue();
+    }
+
+    private static byte[] byteStringValueOf(DataValue dataValue) {
+        if (dataValue == null || !dataValue.getValueSpecified() || 
!(dataValue.getValue() instanceof VariantByteString)) {
+            return null;
+        }
+        List<ByteStringArray> arrays = ((VariantByteString) 
dataValue.getValue()).getValue();
+        if (arrays == null || arrays.isEmpty()) {
+            return null;
+        }
+        List<Short> bytes = arrays.get(0).getValue();
+        byte[] result = new byte[bytes.size()];
+        for (int i = 0; i < bytes.size(); i++) {
+            result[i] = bytes.get(i).byteValue();
+        }
+        return result;
+    }
+
+    /**
+     * Parses the OPC binary schema (bsd.xml) for the named structured type 
and turns its fields into
+     * a {@link StructureDefinition} of built-in scalars/arrays. Nested/custom 
field types can't be
+     * expressed this way yet, so such structures return null (decoded via a 
placeholder instead).
+     */
+    private static StructureDefinition parseBsdStructure(byte[] schema, String 
typeName, NodeId encodingNodeId) {
+        try {
+            DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+            factory.setNamespaceAware(true);
+            
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl";, 
true);
+            Document document = factory.newDocumentBuilder().parse(new 
ByteArrayInputStream(schema));
+
+            String localName = typeName.contains(":") ? 
typeName.substring(typeName.indexOf(':') + 1) : typeName;
+            Element match = null;
+            NodeList structuredTypes = document.getElementsByTagNameNS("*", 
"StructuredType");
+            for (int i = 0; i < structuredTypes.getLength(); i++) {
+                Element structuredType = (Element) structuredTypes.item(i);
+                String name = structuredType.getAttribute("Name");
+                if (name.equals(typeName) || name.equals(localName)) {
+                    match = structuredType;
+                    break;
+                }
+            }
+            if (match == null) {
+                return null;
+            }
+
+            NodeList fieldNodes = match.getElementsByTagNameNS("*", "Field");
+            // Arrays are encoded as a preceding int32 length field plus the 
array field (which
+            // references that length via LengthField); the length field 
itself is not a struct
+            // member, so collect and skip those names.
+            Set<String> lengthFields = new HashSet<>();
+            for (int i = 0; i < fieldNodes.getLength(); i++) {
+                String lengthField = ((Element) 
fieldNodes.item(i)).getAttribute("LengthField");
+                if (lengthField != null && !lengthField.isEmpty()) {
+                    lengthFields.add(lengthField);
+                }
+            }
+            List<StructureField> fields = new ArrayList<>();
+            for (int i = 0; i < fieldNodes.getLength(); i++) {
+                Element field = (Element) fieldNodes.item(i);
+                String name = field.getAttribute("Name");
+                if (lengthFields.contains(name)) {
+                    continue;
+                }
+                Integer builtInId = 
bsdTypeNameToBuiltInId(field.getAttribute("TypeName"));
+                if (builtInId == null) {
+                    return null;
+                }
+                boolean isArray = !field.getAttribute("LengthField").isEmpty();
+                fields.add(new StructureField(new PascalString(name), null,
+                    new NodeId(new NodeIdNumeric(0, builtInId.longValue())), 
isArray ? 1 : -1, null, 0L, false));
+            }
+            return new StructureDefinition(encodingNodeId, new NodeId(new 
NodeIdNumeric(0, 22L)),
+                StructureType.structureTypeStructure, fields);
+        } catch (Exception e) {
+            LOGGER.warn("Failed to parse type dictionary for '{}'", typeName, 
e);
+            return null;
+        }
+    }
+
+    /** Maps an OPC binary-schema TypeName (e.g. "opc:Int32") to its OPC UA 
built-in data-type id. */
+    private static Integer bsdTypeNameToBuiltInId(String typeName) {
+        if (typeName == null) {
+            return null;
+        }
+        String local = typeName.contains(":") ? 
typeName.substring(typeName.indexOf(':') + 1) : typeName;
+        switch (local) {
+            case "Boolean":    return 1;
+            case "SByte":      return 2;
+            case "Byte":       return 3;
+            case "Int16":      return 4;
+            case "UInt16":     return 5;
+            case "Int32":      return 6;
+            case "UInt32":     return 7;
+            case "Int64":      return 8;
+            case "UInt64":     return 9;
+            case "Float":      return 10;
+            case "Double":     return 11;
+            case "String":
+            case "CharArray":  return 12;
+            case "DateTime":   return 13;
+            case "Guid":       return 14;
+            case "ByteString": return 15;
+            default:           return null;
+        }
+    }
+
+    /** Turns a struct Variant (single or array of ExtensionObjects) into a 
PlcStruct / PlcList of PlcStruct. */
+    private PlcValue extensionObjectToPlcValue(VariantExtensionObject variant, 
StructureDefinition definition) {
+        List<ExtensionObject> extensionObjects = variant.getValue();
+        List<PlcValue> values = new ArrayList<>(extensionObjects.size());
+        for (ExtensionObject extensionObject : extensionObjects) {
+            if (extensionObject instanceof RawBinaryExtensionObjectWithMask) {
+                values.add(decodeStruct(((RawBinaryExtensionObjectWithMask) 
extensionObject).getRawBody(), definition));
+            } else {
+                // Standard (well-known) structure types are already parsed; 
surface them textually
+                // for now (decoding those to PlcStruct is a later step).
+                values.add(new PlcSTRING(extensionObject.toString()));
+            }
+        }
+        return structurePlcValues(values, variant);
+    }
+
+    /** Decodes a struct's raw binary body into a PlcStruct using its 
StructureDefinition. */
+    private PlcValue decodeStruct(byte[] rawBody, StructureDefinition 
definition) {
+        try {
+            ReadBufferByteBased buffer = new ReadBufferByteBased(rawBody, 
PayloadConverter.LITTLE_ENDIAN);
+            Map<String, PlcValue> fields = new LinkedHashMap<>();
+            boolean withOptionalFields =
+                definition.getStructureType() == 
StructureType.structureTypeStructureWithOptionalFields;
+            // Structures with optional fields are prefixed by a bit mask (one 
bit per optional field
+            // in declaration order) telling which ones are actually present 
in the encoding.
+            long optionalMask = withOptionalFields ? 
buffer.readUnsignedInt(32) : 0L;
+            int optionalIndex = 0;
+            for (StructureField field : definition.getFields()) {
+                String name = field.getName() != null ? 
field.getName().getStringValue() : null;
+                if (field.getIsOptional()) {
+                    boolean present = (optionalMask & (1L << optionalIndex)) 
!= 0;
+                    optionalIndex++;
+                    if (!present) {
+                        continue;
+                    }
+                }
+                fields.put(name, decodeField(buffer, field));
+            }
+            return new PlcStruct(fields);
+        } catch (Exception e) {
+            LOGGER.error("Failed to decode struct value", e);
+            return null;
+        }
+    }
+
+    /** Decodes a single struct field (scalar or 1-D array) from the buffer. */
+    private static PlcValue decodeField(ReadBuffer buffer, StructureField 
field) throws Exception {
+        Long dataTypeId = numericIdentifierOf(field.getDataType());
+        if (field.getValueRank() < 0) {
+            return decodeScalar(buffer, dataTypeId);
+        }
+        int length = buffer.readSignedInt(32);
+        if (length < 0) {
+            return new PlcList(Collections.emptyList());
+        }
+        List<PlcValue> elements = new ArrayList<>(length);
+        for (int i = 0; i < length; i++) {
+            elements.add(decodeScalar(buffer, dataTypeId));
+        }
+        return new PlcList(elements);
+    }
+
+    /** Decodes one scalar value of the given OPC UA built-in data type from 
the buffer. */
+    private static PlcValue decodeScalar(ReadBuffer buffer, Long 
builtInDataTypeId) throws Exception {
+        int id = builtInDataTypeId == null ? -1 : builtInDataTypeId.intValue();
+        // Each OPC UA built-in is read with the smallest buffer reader that 
covers its bit width
+        // (readUnsignedByte only handles 1-7 bits, so 8-bit and unsigned 
values step up a size).
+        switch (id) {
+            case 1:  return new PlcBOOL(buffer.readUnsignedShort(8) != 0);
+            case 2:  return new PlcSINT(buffer.readSignedByte(8));
+            case 3:  return new PlcUSINT(buffer.readUnsignedShort(8));
+            case 4:  return new PlcINT(buffer.readSignedShort(16));
+            case 5:  return new PlcUINT(buffer.readUnsignedInt(16));
+            case 6:  return new PlcDINT(buffer.readSignedInt(32));
+            case 7:  return new PlcUDINT(buffer.readUnsignedLong(32));
+            case 8:  return new PlcLINT(buffer.readSignedLong(64));
+            case 9:  return new PlcULINT(buffer.readUnsignedBigInteger(64));
+            case 10: return new PlcREAL(buffer.readFloat(32));
+            case 11: return new PlcLREAL(buffer.readDouble(64));
+            case 12: return new 
PlcSTRING(PascalString.staticParse(buffer).getStringValue());
+            default:
+                throw new PlcRuntimeException("Unsupported struct field data 
type id " + id
+                    + " (nested structs, enums and non-builtin types are not 
yet decoded)");
+        }
+    }
+
+    /** Canonical key for a NodeId (namespace + identifier) used to cache 
resolved type layouts. */
+    private static String nodeIdKey(NodeId nodeId) {
+        return namespaceOf(nodeId) + ":" + nodeId.getNodeId().getIdentifier();
+    }
+
+    // 
======================================================================================
+    // Struct (PlcStruct) support — WRITE side: encode a PlcStruct back into a 
custom-struct
+    // ExtensionObject (Phase 5d), mirroring the read decoding.
+    // 
======================================================================================
+
+    private static final long HAS_ENCODING = 38L;
+    private static final ExtensionObjectEncodingMask 
STRUCT_BINARY_ENCODING_MASK =
+        new ExtensionObjectEncodingMask(false, false, true);
+    // The field layout + binary-encoding node needed to write a value of a 
custom struct type.
+    private final Map<String, StructWriteInfo> structWriteInfoCache = new 
ConcurrentHashMap<>();
+
+    private static final class StructWriteInfo {
+        private final StructureDefinition definition;
+        private final ExpandedNodeId encodingNodeId;
+
+        private StructWriteInfo(StructureDefinition definition, ExpandedNodeId 
encodingNodeId) {
+            this.definition = definition;
+            this.encodingNodeId = encodingNodeId;
+        }
+    }
+
+    /** Whether a written value is a struct (a single PlcStruct — arrays of 
structs are a later step). */
+    private static boolean isStructValue(PlcValue value) {
+        return value != null && value.isStruct();
+    }
+
+    /** Resolves the struct layout + encoding node for every struct-valued 
write tag. */
+    private CompletableFuture<Map<String, StructWriteInfo>> 
resolveStructWriteInfos(DefaultPlcWriteRequest request) {
+        Map<String, StructWriteInfo> resolved = new ConcurrentHashMap<>();
+        List<CompletableFuture<?>> futures = new ArrayList<>();
+        for (String tagName : request.getTagNames()) {
+            if (!isStructValue(request.getPlcValue(tagName))) {
+                continue;
+            }
+            futures.add(resolveStructWriteInfo((OpcuaTag) 
request.getTag(tagName))
+                .thenAccept(info -> {
+                    if (info != null) {
+                        resolved.put(tagName, info);
+                    }
+                }));
+        }
+        return CompletableFuture.allOf(futures.toArray(new 
CompletableFuture[0])).thenApply(v -> resolved);
+    }
+
+    /**
+     * Resolves (and caches) the layout and binary-encoding node of a 
struct-typed node for writing:
+     * reads the node's DataType, finds its "Default Binary" encoding node and 
its StructureDefinition
+     * (modern DataTypeDefinition attribute, else the legacy type dictionary).
+     */
+    private CompletableFuture<StructWriteInfo> resolveStructWriteInfo(OpcuaTag 
tag) {
+        String key = cacheKey(tag);
+        StructWriteInfo cached = structWriteInfoCache.get(key);
+        if (cached != null) {
+            return CompletableFuture.completedFuture(cached);
+        }
+        NodeId variableNode = generateNodeId(tag);
+        return readNodeValue(variableNode, 
AttributeId.DataType.getValue()).thenCompose(dataTypeValue -> {
+            NodeId dataTypeNode = dataTypeNodeIdOf(dataTypeValue);
+            if (dataTypeNode == null) {
+                return CompletableFuture.completedFuture(null);
+            }
+            return 
browseBinaryEncodingNode(dataTypeNode).thenCompose(encodingNodeId -> {
+                if (encodingNodeId == null) {
+                    return CompletableFuture.completedFuture(null);
+                }
+                return 
readStructureDefinition(dataTypeNode).thenCompose(modern -> {
+                    if (modern != null) {
+                        return CompletableFuture.completedFuture(new 
StructWriteInfo(modern, encodingNodeId));
+                    }
+                    return 
resolveStructureDefinitionFromDictionary(encodingNodeId)
+                        .thenApply(legacy -> legacy == null ? null : new 
StructWriteInfo(legacy, encodingNodeId));
+                });
+            });
+        }).thenApply(info -> {
+            if (info != null) {
+                structWriteInfoCache.put(key, info);
+            }
+            return info;
+        }).exceptionally(error -> {
+            LOGGER.warn("Failed to resolve struct write info for tag '{}'", 
tag, error);
+            return null;
+        });
+    }
+
+    /** Browses a data-type node's encodings and returns its "Default Binary" 
encoding node. */
+    private CompletableFuture<ExpandedNodeId> browseBinaryEncodingNode(NodeId 
dataTypeNode) {
+        BrowseDescription description = new BrowseDescription(dataTypeNode, 
BrowseDirection.browseDirectionForward,
+            new NodeId(new NodeIdNumeric(0, HAS_ENCODING)), true, 0L, 
BROWSE_RESULT_MASK_ALL);
+        BrowseRequest request = new 
BrowseRequest(conversation.createRequestHeader(),
+            new ViewDescription(new NodeId(new NodeIdTwoByte((short) 0)), 0L, 
0L), 0L,
+            Collections.singletonList(description));
+        return conversation.submit(request, 
BrowseResponse.class).thenApply(response -> {
+            List<ReferenceDescription> references = 
response.getResults().get(0).getReferences();
+            if (references == null || references.isEmpty()) {
+                return null;
+            }
+            for (ReferenceDescription reference : references) {
+                if ("Default 
Binary".equals(qualifiedNameValue(reference.getBrowseName()))) {
+                    return reference.getNodeId();
+                }
+            }
+            return references.get(0).getNodeId();
+        });
+    }
+
+    /** Encodes a PlcStruct as a single-element ExtensionObject Variant of the 
given custom type. */
+    private Variant encodeStructVariant(PlcValue value, StructWriteInfo info) {
+        byte[] body = encodeStructBody(value, info.definition);
+        ExtensionObject extensionObject =
+            new RawBinaryExtensionObjectWithMask(info.encodingNodeId, 
STRUCT_BINARY_ENCODING_MASK, body);
+        return new VariantExtensionObject(false, false, null, 
Collections.emptyList(), null,
+            Collections.singletonList(extensionObject));
+    }
+
+    /** Serialises a PlcStruct into the OPC UA binary body described by its 
StructureDefinition. */
+    private static byte[] encodeStructBody(PlcValue value, StructureDefinition 
definition) {
+        try {
+            byte[] body = new byte[structBodySize(value, definition)];
+            WriteBufferByteBased buffer = new WriteBufferByteBased(body, 
PayloadConverter.LITTLE_ENDIAN);
+            for (StructureField field : definition.getFields()) {
+                encodeField(buffer, field, structField(value, field));
+            }
+            return body;
+        } catch (Exception e) {
+            throw new PlcRuntimeException("Failed to encode struct value", e);
+        }
+    }
+
+    private static PlcValue structField(PlcValue struct, StructureField field) 
{
+        String name = field.getName() != null ? 
field.getName().getStringValue() : null;
+        PlcValue value = struct.getValue(name);
+        if (value == null) {
+            throw new PlcRuntimeException("Missing struct field '" + name + "' 
in value to write");
+        }
+        return value;
+    }
+
+    private static void encodeField(WriteBuffer buffer, StructureField field, 
PlcValue value) throws Exception {
+        Long dataTypeId = numericIdentifierOf(field.getDataType());
+        if (field.getValueRank() < 0) {
+            encodeScalar(buffer, dataTypeId, value);
+            return;
+        }
+        List<? extends PlcValue> elements = value.getList();
+        buffer.writeSignedInt(32, elements.size());
+        for (PlcValue element : elements) {
+            encodeScalar(buffer, dataTypeId, element);
+        }
+    }
+
+    private static void encodeScalar(WriteBuffer buffer, Long 
builtInDataTypeId, PlcValue value) throws Exception {
+        int id = builtInDataTypeId == null ? -1 : builtInDataTypeId.intValue();
+        switch (id) {
+            case 1:  buffer.writeUnsignedShort(8, (short) (value.getBoolean() 
? 1 : 0)); break;
+            case 2:  buffer.writeSignedByte(8, value.getByte()); break;
+            case 3:  buffer.writeUnsignedShort(8, value.getShort()); break;
+            case 4:  buffer.writeSignedShort(16, value.getShort()); break;
+            case 5:  buffer.writeUnsignedInt(16, value.getInt()); break;
+            case 6:  buffer.writeSignedInt(32, value.getInt()); break;
+            case 7:  buffer.writeUnsignedLong(32, value.getLong()); break;
+            case 8:  buffer.writeSignedLong(64, value.getLong()); break;
+            case 9:  buffer.writeUnsignedBigInteger(64, 
value.getBigInteger()); break;
+            case 10: buffer.writeFloat(32, value.getFloat()); break;
+            case 11: buffer.writeDouble(64, value.getDouble()); break;
+            case 12: new PascalString(value.getString()).serialize(buffer); 
break;
+            default:
+                throw new PlcRuntimeException("Unsupported struct field data 
type id " + id + " for writing");
+        }
+    }
+
+    private static int structBodySize(PlcValue value, StructureDefinition 
definition) {
+        int size = 0;
+        for (StructureField field : definition.getFields()) {
+            PlcValue fieldValue = structField(value, field);
+            Long dataTypeId = numericIdentifierOf(field.getDataType());
+            if (field.getValueRank() < 0) {
+                size += scalarSize(dataTypeId, fieldValue);
+            } else {
+                size += 4; // int32 array length
+                for (PlcValue element : fieldValue.getList()) {
+                    size += scalarSize(dataTypeId, element);
+                }
+            }
+        }
+        return size;
+    }
+
+    private static int scalarSize(Long builtInDataTypeId, PlcValue value) {
+        int id = builtInDataTypeId == null ? -1 : builtInDataTypeId.intValue();
+        switch (id) {
+            case 1: case 2: case 3:          return 1;
+            case 4: case 5:                  return 2;
+            case 6: case 7: case 10:         return 4;
+            case 8: case 9: case 11:         return 8;
+            case 12: return new 
PascalString(value.getString()).getLengthInBytes();
+            default:
+                throw new PlcRuntimeException("Unsupported struct field data 
type id " + id + " for writing");
+        }
+    }
+
     // The standard "Objects" folder — the usual entry point into a server's 
address space.
     private static final String OBJECTS_FOLDER_ADDRESS = "ns=0;i=85";
     // HierarchicalReferences (i=33): browsing only these (plus subtypes) 
yields the clean
@@ -1593,37 +2206,40 @@ public class OpcuaConnection extends 
ConnectionBase<OpcuaConfiguration> implemen
         // Phase 4: for tags without an explicit ;TYPE suffix, resolve the 
server-declared data
         // type (via the session type cache) up-front so the write is built 
with the authoritative
         // OPC UA type instead of a lossy Java-value guess. Then assemble and 
submit the write.
-        return resolveWriteTypes(request).thenCompose(serverAttributes -> {
-            RequestHeader requestHeader = conversation.createRequestHeader();
-            List<WriteValue> writeValueList = new 
ArrayList<>(request.getTagNames().size());
-            for (String tagName : request.getTagNames()) {
-                OpcuaTag tag = (OpcuaTag) request.getTag(tagName);
-
-                NodeId nodeId = generateNodeId(tag);
-
-                writeValueList.add(new WriteValue(nodeId,
-                    tag.getAttributeId().getValue(),
-                    indexRangeOf(tag),
-                    new DataValue(
-                        false,
-                        false,
-                        false,
-                        false,
-                        false,
-                        true,
-                        fromPlcValue(tagName, tag, writeRequest, 
serverAttributes.get(tagName)),
-                        null,
-                        null,
-                        null,
-                        null,
-                        null)));
-            }
-
-            WriteRequest opcuaWriteRequest = new WriteRequest(requestHeader, 
writeValueList);
-
-            return conversation.submit(opcuaWriteRequest, WriteResponse.class)
-                .thenApply(response -> writeResponse(request, response));
-        });
+        return resolveWriteTypes(request).thenCompose(serverAttributes ->
+            resolveStructWriteInfos(request).thenCompose(structInfos -> {
+                RequestHeader requestHeader = 
conversation.createRequestHeader();
+                List<WriteValue> writeValueList = new 
ArrayList<>(request.getTagNames().size());
+                for (String tagName : request.getTagNames()) {
+                    OpcuaTag tag = (OpcuaTag) request.getTag(tagName);
+
+                    NodeId nodeId = generateNodeId(tag);
+
+                    Variant variant;
+                    if (isStructValue(request.getPlcValue(tagName))) {
+                        // Phase 5d: encode a PlcStruct back into a 
custom-struct ExtensionObject.
+                        StructWriteInfo structInfo = structInfos.get(tagName);
+                        if (structInfo == null) {
+                            throw new PlcRuntimeException("Cannot resolve the 
structure layout to write "
+                                + "tag '" + tagName + "'");
+                        }
+                        variant = 
encodeStructVariant(request.getPlcValue(tagName), structInfo);
+                    } else {
+                        variant = fromPlcValue(tagName, tag, writeRequest, 
serverAttributes.get(tagName));
+                    }
+
+                    writeValueList.add(new WriteValue(nodeId,
+                        tag.getAttributeId().getValue(),
+                        indexRangeOf(tag),
+                        new DataValue(false, false, false, false, false, true,
+                            variant, null, null, null, null, null)));
+                }
+
+                WriteRequest opcuaWriteRequest = new 
WriteRequest(requestHeader, writeValueList);
+
+                return conversation.submit(opcuaWriteRequest, 
WriteResponse.class)
+                    .thenApply(response -> writeResponse(request, response));
+            }));
     }
 
     /**
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/readwrite/utils/StaticHelper.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/readwrite/utils/StaticHelper.java
index 7ae036ec39..2d29e5ead3 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/readwrite/utils/StaticHelper.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/readwrite/utils/StaticHelper.java
@@ -19,8 +19,14 @@
 package org.apache.plc4x.java.opcua.readwrite.utils;
 
 import java.nio.charset.StandardCharsets;
-import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.opcua.readwrite.ExpandedNodeId;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdByteString;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdFourByte;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdGuid;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdNumeric;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdString;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdTwoByte;
+import org.apache.plc4x.java.opcua.readwrite.NodeIdTypeDefinition;
 
 public class StaticHelper {
 
@@ -40,7 +46,45 @@ public class StaticHelper {
         try {
             return 
Integer.parseInt(expandedNodeId.getNodeId().getIdentifier());
         } catch (NumberFormatException e) {
-            throw new PlcRuntimeException("Invalid node id, expected number, 
found " + expandedNodeId.getNodeId().getClass().getName());
+            // Non-numeric encoding NodeId (string/guid/opaque). Well-known 
standard types always
+            // use numeric ids, so this is a custom/user-defined type whose 
body is captured as raw
+            // bytes (bodyKind = 2) and never dispatched through 
ExtensionObjectDefinition — the
+            // extensionId is unused there. Return a sentinel that matches no 
known type instead of
+            // failing the whole parse.
+            return -1;
+        }
+    }
+
+    /**
+     * Whether the encoding NodeId of an ExtensionObject refers to a 
well-known OPC UA standard type,
+     * i.e. it lives in namespace 0 and is not referenced by a namespace URI. 
Custom / user-defined
+     * structure encodings live in namespace &ge; 1; their bodies cannot be 
parsed by the generated
+     * {@code ExtensionObjectDefinition} dispatch and are instead captured as 
raw bytes (which the
+     * driver decodes against the type's StructureDefinition).
+     */
+    public static boolean isStandardEncoding(ExpandedNodeId expandedNodeId) {
+        if (expandedNodeId == null) {
+            return true;
+        }
+        // A namespace referenced by URI is, by definition, not the standard 
OPC UA namespace (0).
+        if (expandedNodeId.getNamespaceURISpecified()) {
+            return false;
+        }
+        NodeIdTypeDefinition nodeId = expandedNodeId.getNodeId();
+        if (nodeId instanceof NodeIdTwoByte) {
+            // The two-byte form is always namespace 0.
+            return true;
+        } else if (nodeId instanceof NodeIdFourByte fourByte) {
+            return fourByte.getNamespaceIndex() == 0;
+        } else if (nodeId instanceof NodeIdNumeric numeric) {
+            return numeric.getNamespaceIndex() == 0;
+        } else if (nodeId instanceof NodeIdString string) {
+            return string.getNamespaceIndex() == 0;
+        } else if (nodeId instanceof NodeIdGuid guid) {
+            return guid.getNamespaceIndex() == 0;
+        } else if (nodeId instanceof NodeIdByteString byteString) {
+            return byteString.getNamespaceIndex() == 0;
         }
+        return false;
     }
 }
diff --git 
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
index 6f75866b01..b100023b40 100644
--- 
a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
+++ 
b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
@@ -439,21 +439,65 @@ public class OpcuaPlcDriverTest {
         readOnly.run();
     }
 
-    @org.junit.jupiter.api.Disabled("Enable once OPC UA struct (PlcStruct) 
support and the custom "
-        + "struct nodes in Plc4xTestNamespace land (feature Phase 5 + task 
T1b).")
     @Test
-    void comprehensiveStructsSpec() throws Exception {
-        // Acceptance spec for struct addressing. OPC UA has no per-field node 
addressing like ADS
-        // (g_simple.s8): a struct is one node whose value is a PlcStruct, 
navigated client-side.
-        BasicPlcTest test = new BasicPlcTest(tcpConnectionAddress, new 
PlcNullAuthentication(),
-            true, true, true, true, 1);
-        java.util.Map<String, org.apache.plc4x.java.api.value.PlcValue> simple 
= new java.util.LinkedHashMap<>();
-        simple.put("s8", new PlcSINT(-8));
-        simple.put("u16", new PlcUINT(1600));
-        simple.put("r64", new PlcLREAL(-0.125d));
-        simple.put("str", new PlcSTRING("struct-string"));
-        test.addTestCase("ns=3;s=Test/Struct/Simple", new PlcStruct(simple));
-        test.run();
+    void writesCustomStructRoundTrip() throws Exception {
+        // A struct is one node whose value is a PlcStruct (OPC UA has no 
per-field node addressing).
+        // Write a PlcStruct, then read it back and confirm every field 
round-tripped (5d encode).
+        try (PlcConnection connection = new 
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+            java.util.Map<String, org.apache.plc4x.java.api.value.PlcValue> 
simple = new java.util.LinkedHashMap<>();
+            simple.put("Foo", new PlcSTRING("written-struct"));
+            simple.put("Bar", new PlcDINT(987654));
+            simple.put("Baz", new PlcBOOL(false));
+            simple.put("Qux", new PlcLREAL(-1.25d));
+
+            PlcWriteResponse write = connection.writeRequestBuilder()
+                .addTagAddress("s", "ns=3;s=Test/Struct/Simple", new 
PlcStruct(simple))
+                .build().execute().get(30, TimeUnit.SECONDS);
+            
assertThat(write.getResponseCode("s")).isEqualTo(PlcResponseCode.OK);
+
+            PlcReadResponse read = connection.readRequestBuilder()
+                .addTagAddress("s", "ns=3;s=Test/Struct/Simple")
+                .build().execute().get(30, TimeUnit.SECONDS);
+            org.apache.plc4x.java.api.value.PlcValue value = 
read.getPlcValue("s");
+            assertThat(value.isStruct()).isTrue();
+            
assertThat(value.getStruct().get("Foo").getString()).isEqualTo("written-struct");
+            
assertThat(value.getStruct().get("Bar").getInt()).isEqualTo(987654);
+            assertThat(value.getStruct().get("Baz").getBoolean()).isFalse();
+            
assertThat(value.getStruct().get("Qux").getDouble()).isEqualTo(-1.25d);
+        }
+    }
+
+    @Test
+    void customStructIsReadableWithoutCrashing() throws Exception {
+        // 5a: a custom (user-defined) struct value is captured as a raw 
ExtensionObject body instead
+        // of crashing the parser (it threw BufferException before). The value 
round-trips at least as
+        // a placeholder even when the field layout can't be resolved on this 
server.
+        try (PlcConnection connection = new 
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+            PlcReadResponse response = connection.readRequestBuilder()
+                .addTagAddress("s", "ns=3;s=Test/Struct/Simple")
+                .build().execute().get(30, TimeUnit.SECONDS);
+            
assertThat(response.getResponseCode("s")).isEqualTo(PlcResponseCode.OK);
+        }
+    }
+
+    @Test
+    void readsCustomStructAsPlcStruct() throws Exception {
+        // Plc4xTestStruct instance in the namespace: foo="hello-struct", 
bar=12345, baz=true, qux=2.5.
+        // Exercises the whole struct read path: 5a (raw body), the 
field-layout resolution (modern
+        // DataTypeDefinition attribute, or the legacy type dictionary against 
Milo) and 5c (decode).
+        try (PlcConnection connection = new 
DefaultPlcDriverManager().getConnection(tcpConnectionAddress)) {
+            PlcReadResponse response = connection.readRequestBuilder()
+                .addTagAddress("s", "ns=3;s=Test/Struct/Simple")
+                .build().execute().get(30, TimeUnit.SECONDS);
+            
assertThat(response.getResponseCode("s")).isEqualTo(PlcResponseCode.OK);
+
+            org.apache.plc4x.java.api.value.PlcValue value = 
response.getPlcValue("s");
+            assertThat(value.isStruct()).isTrue();
+            
assertThat(value.getStruct().get("Foo").getString()).isEqualTo("hello-struct");
+            assertThat(value.getStruct().get("Bar").getInt()).isEqualTo(12345);
+            assertThat(value.getStruct().get("Baz").getBoolean()).isTrue();
+            
assertThat(value.getStruct().get("Qux").getDouble()).isEqualTo(2.5d);
+        }
     }
 
     @Test
diff --git 
a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
 
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
index abe1c054c0..c48bcc4e06 100644
--- 
a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
+++ 
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestNamespace.java
@@ -28,20 +28,28 @@ import java.util.List;
 import org.eclipse.milo.opcua.sdk.core.AccessLevel;
 import org.eclipse.milo.opcua.sdk.core.Reference;
 import org.eclipse.milo.opcua.sdk.core.ValueRank;
+import org.eclipse.milo.opcua.sdk.core.ValueRanks;
 import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
 import org.eclipse.milo.opcua.sdk.server.api.DataItem;
 import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle;
 import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
+import org.eclipse.milo.opcua.sdk.server.dtd.DataTypeDictionaryManager;
 import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
 import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
 import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
 import org.eclipse.milo.opcua.stack.core.Identifiers;
 import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
 import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
 import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
 import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
 import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
 import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.StructureType;
+import org.eclipse.milo.opcua.stack.core.types.structured.StructureDefinition;
+import org.eclipse.milo.opcua.stack.core.types.structured.StructureDescription;
+import org.eclipse.milo.opcua.stack.core.types.structured.StructureField;
 
 /**
  * A second server namespace (index 3) dedicated to PLC4X integration testing. 
Where Milo's
@@ -59,12 +67,15 @@ public class Plc4xTestNamespace extends 
ManagedNamespaceWithLifecycle {
     public static final String NAMESPACE_URI = "urn:apache:plc4x:test";
 
     private final SubscriptionModel subscriptionModel;
+    private final DataTypeDictionaryManager dictionaryManager;
 
     public Plc4xTestNamespace(OpcUaServer server) {
         super(server, NAMESPACE_URI);
 
         subscriptionModel = new SubscriptionModel(server, this);
+        dictionaryManager = new DataTypeDictionaryManager(getNodeContext(), 
NAMESPACE_URI);
         getLifecycleManager().addLifecycle(subscriptionModel);
+        getLifecycleManager().addLifecycle(dictionaryManager);
         getLifecycleManager().addStartupTask(this::createAndAddNodes);
     }
 
@@ -85,6 +96,67 @@ public class Plc4xTestNamespace extends 
ManagedNamespaceWithLifecycle {
         addScalarNodes(root);
         addArrayNodes(root);
         addMatrixNodes(root);
+        try {
+            registerStructType();
+            addStructNode(root);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to register the custom test 
struct type", e);
+        }
+    }
+
+    // 
=====================================================================================
+    // Custom struct — a user-defined structure (namespace >= 1) whose 
encoding the generated OPC UA
+    // model can't decode; it exercises the driver's PlcStruct read/write 
support. Registering it
+    // with a StructureDefinition exposes both the DataTypeDefinition 
attribute and the legacy type
+    // dictionary, so both driver code paths can resolve the field layout.
+    // 
=====================================================================================
+    private void registerStructType() throws Exception {
+        NodeId dataTypeId = 
Plc4xTestStruct.TYPE_ID.toNodeIdOrThrow(getServer().getNamespaceTable());
+        NodeId binaryEncodingId = 
Plc4xTestStruct.BINARY_ENCODING_ID.toNodeIdOrThrow(getServer().getNamespaceTable());
+
+        dictionaryManager.registerStructureCodec(
+            new Plc4xTestStruct.Codec().asBinaryCodec(), "Plc4xTestStruct", 
dataTypeId, binaryEncodingId);
+
+        UInteger maxStringLength = 
getServer().getConfig().getLimits().getMaxStringLength();
+        StructureField[] fields = new StructureField[]{
+            new StructureField("Foo", LocalizedText.NULL_VALUE, 
Identifiers.String,
+                ValueRanks.Scalar, null, maxStringLength, false),
+            new StructureField("Bar", LocalizedText.NULL_VALUE, 
Identifiers.Int32,
+                ValueRanks.Scalar, null, uint(0), false),
+            new StructureField("Baz", LocalizedText.NULL_VALUE, 
Identifiers.Boolean,
+                ValueRanks.Scalar, null, uint(0), false),
+            new StructureField("Qux", LocalizedText.NULL_VALUE, 
Identifiers.Double,
+                ValueRanks.Scalar, null, uint(0), false)
+        };
+        StructureDefinition definition = new StructureDefinition(
+            binaryEncodingId, Identifiers.Structure, StructureType.Structure, 
fields);
+        StructureDescription description = new StructureDescription(
+            dataTypeId, new QualifiedName(getNamespaceIndex(), 
"Plc4xTestStruct"), definition);
+        dictionaryManager.registerStructureDescription(description, 
binaryEncodingId);
+    }
+
+    private void addStructNode(UaFolderNode root) throws Exception {
+        UaFolderNode folder = childFolder(root, "Struct");
+        NodeId dataTypeId = 
Plc4xTestStruct.TYPE_ID.toNodeIdOrThrow(getServer().getNamespaceTable());
+        NodeId binaryEncodingId = 
Plc4xTestStruct.BINARY_ENCODING_ID.toNodeIdOrThrow(getServer().getNamespaceTable());
+
+        UaVariableNode node = new 
UaVariableNode.UaVariableNodeBuilder(getNodeContext())
+            .setNodeId(newNodeId("Test/Struct/Simple"))
+            .setAccessLevel(AccessLevel.READ_WRITE)
+            .setUserAccessLevel(AccessLevel.READ_WRITE)
+            .setBrowseName(newQualifiedName("Simple"))
+            .setDisplayName(LocalizedText.english("Simple"))
+            .setDataType(dataTypeId)
+            .setTypeDefinition(Identifiers.BaseDataVariableType)
+            .build();
+
+        Plc4xTestStruct value = new Plc4xTestStruct("hello-struct", 12345, 
true, 2.5d);
+        ExtensionObject encoded = ExtensionObject.encodeDefaultBinary(
+            getServer().getSerializationContext(), value, binaryEncodingId);
+        node.setValue(new DataValue(new Variant(encoded)));
+
+        getNodeManager().addNode(node);
+        folder.addOrganizes(node);
     }
 
     // 
=====================================================================================
diff --git 
a/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestStruct.java
 
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestStruct.java
new file mode 100644
index 0000000000..ceb4441834
--- /dev/null
+++ 
b/plc4j/drivers/opcua/src/test/java/org/eclipse/milo/examples/server/Plc4xTestStruct.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.eclipse.milo.examples.server;
+
+import org.eclipse.milo.opcua.stack.core.UaSerializationException;
+import org.eclipse.milo.opcua.stack.core.serialization.SerializationContext;
+import org.eclipse.milo.opcua.stack.core.serialization.UaDecoder;
+import org.eclipse.milo.opcua.stack.core.serialization.UaEncoder;
+import org.eclipse.milo.opcua.stack.core.serialization.UaStructure;
+import 
org.eclipse.milo.opcua.stack.core.serialization.codecs.GenericDataTypeCodec;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
+
+import java.util.Objects;
+
+/**
+ * A simple custom (user-defined) OPC UA structure used to exercise the PLC4X 
driver's struct
+ * (PlcStruct) read/write support. Its four fields cover the common scalar 
cases (string, signed
+ * int, boolean, floating point). Registered in {@link Plc4xTestNamespace} 
together with a
+ * StructureDefinition so the server exposes both the DataTypeDefinition 
attribute and the legacy
+ * type dictionary.
+ */
+public class Plc4xTestStruct implements UaStructure {
+
+    public static final ExpandedNodeId TYPE_ID = 
ExpandedNodeId.parse(String.format(
+        "nsu=%s;s=%s", Plc4xTestNamespace.NAMESPACE_URI, 
"DataType.Plc4xTestStruct"));
+
+    public static final ExpandedNodeId BINARY_ENCODING_ID = 
ExpandedNodeId.parse(String.format(
+        "nsu=%s;s=%s", Plc4xTestNamespace.NAMESPACE_URI, 
"DataType.Plc4xTestStruct.BinaryEncoding"));
+
+    private final String foo;
+    private final int bar;
+    private final boolean baz;
+    private final double qux;
+
+    public Plc4xTestStruct() {
+        this("", 0, false, 0.0d);
+    }
+
+    public Plc4xTestStruct(String foo, int bar, boolean baz, double qux) {
+        this.foo = foo;
+        this.bar = bar;
+        this.baz = baz;
+        this.qux = qux;
+    }
+
+    public String getFoo() {
+        return foo;
+    }
+
+    public int getBar() {
+        return bar;
+    }
+
+    public boolean isBaz() {
+        return baz;
+    }
+
+    public double getQux() {
+        return qux;
+    }
+
+    @Override
+    public ExpandedNodeId getTypeId() {
+        return TYPE_ID;
+    }
+
+    @Override
+    public ExpandedNodeId getBinaryEncodingId() {
+        return BINARY_ENCODING_ID;
+    }
+
+    @Override
+    public ExpandedNodeId getXmlEncodingId() {
+        return ExpandedNodeId.NULL_VALUE;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Plc4xTestStruct that = (Plc4xTestStruct) o;
+        return bar == that.bar && baz == that.baz
+            && Double.compare(that.qux, qux) == 0 && Objects.equals(foo, 
that.foo);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(foo, bar, baz, qux);
+    }
+
+    @Override
+    public String toString() {
+        return "Plc4xTestStruct{foo=" + foo + ", bar=" + bar + ", baz=" + baz 
+ ", qux=" + qux + '}';
+    }
+
+    public static class Codec extends GenericDataTypeCodec<Plc4xTestStruct> {
+        @Override
+        public Class<Plc4xTestStruct> getType() {
+            return Plc4xTestStruct.class;
+        }
+
+        @Override
+        public Plc4xTestStruct decode(SerializationContext context, UaDecoder 
decoder)
+                throws UaSerializationException {
+            String foo = decoder.readString("Foo");
+            int bar = decoder.readInt32("Bar");
+            boolean baz = decoder.readBoolean("Baz");
+            double qux = decoder.readDouble("Qux");
+            return new Plc4xTestStruct(foo, bar, baz, qux);
+        }
+
+        @Override
+        public void encode(SerializationContext context, UaEncoder encoder, 
Plc4xTestStruct value)
+                throws UaSerializationException {
+            encoder.writeString("Foo", value.foo);
+            encoder.writeInt32("Bar", value.bar);
+            encoder.writeBoolean("Baz", value.baz);
+            encoder.writeDouble("Qux", value.qux);
+        }
+    }
+}
diff --git 
a/protocols/opcua/src/main/generated/protocols/opcua/opc-manual.mspec 
b/protocols/opcua/src/main/generated/protocols/opcua/opc-manual.mspec
index 2792aa1b4b..a8cfc6fcfe 100644
--- a/protocols/opcua/src/main/generated/protocols/opcua/opc-manual.mspec
+++ b/protocols/opcua/src/main/generated/protocols/opcua/opc-manual.mspec
@@ -167,18 +167,33 @@
     [abstract ExtensionObjectDefinition body]
     [simple ExpandedNodeId typeId]
     [virtual int 32 extensionId 'typeId == null ? 0 : 
STATIC_CALL("extensionId", typeId)']
+    // Whether the encoding node refers to a well-known standard type 
(namespace 0). Computed here
+    // where typeId is in scope and threaded down like extensionId, so the 
masked-body dispatch can
+    // tell a decodable standard type from a custom one.
+    [virtual bit standardEncoding 'STATIC_CALL("isStandardEncoding", typeId)']
     [typeSwitch includeEncodingMask
         ['false' RootExtensionObject (int 32 extensionId)
             [simple ExtensionObjectDefinition('extensionId') body]
         ]
-        ['true' ExtensionObjectWithMask (int 32 extensionId)
+        ['true' ExtensionObjectWithMask (int 32 extensionId, bit 
standardEncoding)
             [simple ExtensionObjectEncodingMask encodingMask]
-            [typeSwitch encodingMask.xmlBody, encodingMask.binaryBody
-                ['false', 'true' BinaryExtensionObjectWithMask
+            // Body kind: 0 = no binary body (null); 1 = binary body of a 
well-known standard type
+            // (encoding node in namespace 0) which the generated dispatch can 
decode; 2 = binary
+            // body of a custom / user-defined type (encoding node in 
namespace >= 1) which the
+            // dispatch cannot decode, so the raw bytes are captured for the 
driver to decode against
+            // the type's StructureDefinition.
+            [virtual int 8 bodyKind 'encodingMask.binaryBody ? 
(standardEncoding ? 1 : 2) : 0']
+            [typeSwitch bodyKind
+                ['1' BinaryExtensionObjectWithMask
                     [implicit int 32 bodyLength 'body == null ? 0 : 
body.lengthInBytes']
                     [simple ExtensionObjectDefinition('extensionId') body]
                 ]
-                ['false', 'false' NullExtensionObjectWithMask
+                ['2' RawBinaryExtensionObjectWithMask
+                    [implicit int 32 bodyLength 'COUNT(rawBody)']
+                    [array byte rawBody count 'bodyLength']
+                    [virtual ExtensionObjectDefinition('0') body 'null']
+                ]
+                ['0' NullExtensionObjectWithMask
                     [virtual ExtensionObjectDefinition('0') body 'null']
                 ]
             ]
diff --git a/protocols/opcua/src/main/xslt/opc-manual.xsl 
b/protocols/opcua/src/main/xslt/opc-manual.xsl
index e784faa05c..cf82205cef 100644
--- a/protocols/opcua/src/main/xslt/opc-manual.xsl
+++ b/protocols/opcua/src/main/xslt/opc-manual.xsl
@@ -216,18 +216,33 @@
     [abstract ExtensionObjectDefinition body]
     [simple ExpandedNodeId typeId]
     [virtual int 32 extensionId 'typeId == null ? 0 : 
STATIC_CALL("extensionId", typeId)']
+    // Whether the encoding node refers to a well-known standard type 
(namespace 0). Computed here
+    // where typeId is in scope and threaded down like extensionId, so the 
masked-body dispatch can
+    // tell a decodable standard type from a custom one.
+    [virtual bit standardEncoding 'STATIC_CALL("isStandardEncoding", typeId)']
     [typeSwitch includeEncodingMask
         ['false' RootExtensionObject (int 32 extensionId)
             [simple ExtensionObjectDefinition('extensionId') body]
         ]
-        ['true' ExtensionObjectWithMask (int 32 extensionId)
+        ['true' ExtensionObjectWithMask (int 32 extensionId, bit 
standardEncoding)
             [simple ExtensionObjectEncodingMask encodingMask]
-            [typeSwitch encodingMask.xmlBody, encodingMask.binaryBody
-                ['false', 'true' BinaryExtensionObjectWithMask
+            // Body kind: 0 = no binary body (null); 1 = binary body of a 
well-known standard type
+            // (encoding node in namespace 0) which the generated dispatch can 
decode; 2 = binary
+            // body of a custom / user-defined type (encoding node in 
namespace >= 1) which the
+            // dispatch cannot decode, so the raw bytes are captured for the 
driver to decode against
+            // the type's StructureDefinition.
+            [virtual int 8 bodyKind 'encodingMask.binaryBody ? 
(standardEncoding ? 1 : 2) : 0']
+            [typeSwitch bodyKind
+                ['1' BinaryExtensionObjectWithMask
                     [implicit int 32 bodyLength 'body == null ? 0 : 
body.lengthInBytes']
                     [simple ExtensionObjectDefinition('extensionId') body]
                 ]
-                ['false', 'false' NullExtensionObjectWithMask
+                ['2' RawBinaryExtensionObjectWithMask
+                    [implicit int 32 bodyLength 'COUNT(rawBody)']
+                    [array byte rawBody count 'bodyLength']
+                    [virtual ExtensionObjectDefinition('0') body 'null']
+                ]
+                ['0' NullExtensionObjectWithMask
                     [virtual ExtensionObjectDefinition('0') body 'null']
                 ]
             ]


Reply via email to