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) -> data-type node ->
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 ≥ 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']
]
]