From 731c74eeb41b387be449382135da115f8e4c960d Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 4 Mar 2025 15:26:39 +0100 Subject: [PATCH 01/64] doc: add recipe for building with open62541 on Deb-12 --- ...from source with open62541 - Debian-12.txt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 documentation/Building from source with open62541 - Debian-12.txt diff --git a/documentation/Building from source with open62541 - Debian-12.txt b/documentation/Building from source with open62541 - Debian-12.txt new file mode 100644 index 00000000..6e448af7 --- /dev/null +++ b/documentation/Building from source with open62541 - Debian-12.txt @@ -0,0 +1,86 @@ +# Container debian:12 + +# Install general dependencies + +apt update +apt install -y build-essential curl + +# Install EPICS dependencies +apt install -y libreadline-dev + +# Install open62541 dependencies +apt install -y cmake python3 libssl-dev libxml2-dev + +# Create installation dir cd into it +mkdir /opt/epics +cd /opt/epics + +# Download EPICS Base, OPCUA Device Support, open62541 +curl -L https://github.com/epics-base/epics-base/releases/download/R7.0.8.1/base-7.0.8.1.tar.gz | tar xzf - +curl -L https://github.com/epics-modules/opcua/archive/refs/tags/v0.10.0.tar.gz | tar xzf - +curl -L https://github.com/open62541/open62541/archive/refs/tags/v1.3.15.tar.gz | tar xzf - + +# Configure EPICS Base (set C++ standard to C++11) +cat >> base-7.0.8.1/configure/os/CONFIG_SITE.Common.linuxCommon < RELEASE.local < opcua-0.10.0/configure/CONFIG_SITE.local <<\EOF +# Path to the Open62541 installation +OPEN62541 = /usr/local + +# How the Open62541 SDK shared libraries are deployed +# SYSTEM = shared libs are in a system location +# PROVIDED = shared libs are in $(OPEN62541_SHRLIB_DIR) +# INSTALL = shared libs are installed (copied) into this module +# EMBED = link SDK code statically into libopcua, +# SDK libraries are not required on target system +OPEN62541_DEPLOY_MODE = PROVIDED +OPEN62541_LIB_DIR = $(OPEN62541)/lib +OPEN62541_SHRLIB_DIR = $(OPEN62541_LIB_DIR) +# How the Open62541 libraries were built +OPEN62541_USE_CRYPTO = YES +OPEN62541_USE_XMLPARSER = YES +EOF + +# Build the OPCUA Device Support +make -j4 -C opcua-0.10.0 + +### Done ### + +# Now, e.g., run the example IOC +( cd opcua-0.10.0/exampleTop/iocBoot/iocS7-1500/; ../../bin/linux-x86_64/opcuaIoc st.cmd ) \ No newline at end of file From ac8b3e0d4b5e6dc624d4fc484a807f29e360cb6c Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 10 Mar 2025 11:28:51 +0100 Subject: [PATCH 02/64] open62541: fix mapping epicsUInt8 -> UA_BYTE in writeArray() --- devOpcuaSup/open62541/DataElementOpen62541.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index dcacf46f..fa91993d 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -1161,7 +1161,7 @@ DataElementOpen62541::writeArray (const epicsInt8 *value, const epicsUInt32 num, long DataElementOpen62541::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) { - return writeArray(value, num, &UA_TYPES[UA_TYPES_SBYTE], prec); + return writeArray(value, num, &UA_TYPES[UA_TYPES_BYTE], prec); } long From 685d350d0ead902d41b8488b76a509c21ac3bf22 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Sun, 9 Mar 2025 16:19:43 +0100 Subject: [PATCH 03/64] uasdk: support arrays of OpcUa_Boolean, mapped to UCHAR --- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 48 ++++++++++++++++++-------- devOpcuaSup/UaSdk/DataElementUaSdk.h | 2 ++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index e7cbce2f..bc9f74df 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -625,7 +625,8 @@ DataElementUaSdk::readArray (char *value, const epicsUInt32 len, } // Specialization for epicsUInt8 / OpcUa_Byte -// (needed because UaByteArray API is different from all other UaXxxArray classes) +// needed because UaByteArray API is different from all other UaXxxArray classes +// and because epicsUInt8 is also used for arrays of OpcUa_Boolean // CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here template<> long @@ -676,7 +677,7 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (data.type() != expectedType) { + } else if (data.type() != OpcUaType_Byte && data.type() != OpcUaType_Boolean) { errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", prec->name, variantTypeString(data.type()), epicsTypeString(*value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); @@ -685,11 +686,19 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - UaByteArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = static_cast(arr.size()); - if (num < elemsWritten) elemsWritten = num; - memcpy(value, arr.data(), sizeof(epicsUInt8) * elemsWritten); + if (data.type() == OpcUaType_Byte) { + UaByteArray arr; + UaVariant_to(upd->getData(), arr); + elemsWritten = static_cast(arr.size()); + if (num < elemsWritten) elemsWritten = num; + memcpy(value, arr.data(), sizeof(epicsUInt8) * elemsWritten); + } else { + UaBooleanArray arr; + UaVariant_to(upd->getData(), arr); + elemsWritten = static_cast(arr.length()); + if (num < elemsWritten) elemsWritten = num; + memcpy(value, arr.rawData(), sizeof(epicsUInt8) * elemsWritten); + } prec->udf = false; } } @@ -1100,7 +1109,8 @@ DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, } // Specialization for epicsUInt8 / OpcUa_Byte -// (needed because UaByteArray API is different from all other UaXxxArray classes) +// needed because UaByteArray API is different from all other UaXxxArray classes +// and because epicsUInt8 is also used for arrays of OpcUa_Boolean // CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here template<> long @@ -1114,7 +1124,7 @@ DataElementUaSdk::writeArray (const epicsUI errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; - } else if (incomingData.type() != targetType) { + } else if (incomingData.type() != OpcUaType_Byte && incomingData.type() != OpcUaType_Boolean) { errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", prec->name, variantTypeString(incomingData.type()), @@ -1123,13 +1133,21 @@ DataElementUaSdk::writeArray (const epicsUI (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; } else { - UaByteArray arr(reinterpret_cast(value), static_cast(num)); - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); + if (incomingData.type() == OpcUaType_Byte) { + UaByteArray arr(reinterpret_cast(value), static_cast(num)); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + } else { + UaBooleanArray arr(static_cast(num), const_cast(value)); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } } - dbgWriteArray(num, epicsTypeString(*value)); } return ret; diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index b9ad542c..2294aa01 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -728,6 +728,7 @@ class DataElementUaSdk : public DataElement OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value) { return variant.toInt64(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Double &value) { return variant.toDouble(value); } + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaBooleanArray &value) { return variant.toBoolArray(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaSByteArray &value) { return variant.toSByteArray(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaByteArray &value) { return variant.toByteArray(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt16Array &value) { return variant.toInt16Array(value); } @@ -740,6 +741,7 @@ class DataElementUaSdk : public DataElement OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaDoubleArray &value) { return variant.toDoubleArray(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaStringArray &value) { return variant.toStringArray(value); } + void UaVariant_set(UaVariant &variant, UaBooleanArray &value) { variant.setBoolArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaSByteArray &value) { variant.setSByteArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaByteArray &value) { variant.setByteArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaInt16Array &value) { variant.setInt16Array(value, OpcUa_True); } From b8666d5892f85635601b50084e0c11b04422efc2 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 10 Mar 2025 10:40:47 +0100 Subject: [PATCH 04/64] open62541: support arrays of UA_BOOLEAN, mapped to UCHAR --- .../open62541/DataElementOpen62541.cpp | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index fa91993d..59db2a0d 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -736,6 +736,92 @@ DataElementOpen62541::readArray (char *value, const epicsUInt32 len, return ret; } +// Specialization for epicsUInt8 +// needed because epicsUInt8 is used for arrays of UA_BYTE and UA_BOOLEAN +// CAVEAT: changes in the template (in DataElementOpen62541.h) must be reflected here +template<> +long +DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (num && value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UA_Variant &data = upd->getData(); + if (UA_Variant_isScalar(&data)) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (typeKindOf(data) != UA_DATATYPEKIND_BYTE && typeKindOf(data) != UA_DATATYPEKIND_BOOLEAN) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(data), epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = static_cast(num) < data.arrayLength ? num : static_cast(data.arrayLength); + memcpy(value, data.data, elemsWritten); + prec->udf = false; + } + UA_Variant_clear(&data); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; +} + long DataElementOpen62541::readArray (epicsInt8 *value, const epicsUInt32 num, epicsUInt32 *numRead, @@ -1152,6 +1238,49 @@ DataElementOpen62541::writeArray (const char **value, const epicsUInt32 len, return ret; } +// Specialization for epicsUInt8 +// needed because epicsUInt8 is used for arrays of UA_BYTE and UA_BOOLEAN +// CAVEAT: changes in the template (in DataElementOpen62541.h) must be reflected here +template<> +long +DataElementOpen62541::writeArray(const epicsUInt8 *value, + const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec) +{ + long ret = 0; + + if (UA_Variant_isScalar(&incomingData)) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (typeKindOf(incomingData) != UA_DATATYPEKIND_BYTE && typeKindOf(incomingData) != UA_DATATYPEKIND_BOOLEAN) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + UA_StatusCode status; + { // Scope of Guard G + Guard G(outgoingLock); + status = UA_Variant_setArrayCopy(&outgoingData, value, num, incomingData.type); + markAsDirty(); + } + if (UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : array copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + dbgWriteArray(num, epicsTypeString(*value)); + } + } + return ret; +} + long DataElementOpen62541::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) { From 098e0b810ff1c9be9fa316d5866a9b165a8b662c Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Tue, 25 Mar 2025 15:48:32 +0100 Subject: [PATCH 05/64] add Boolean arrays to Demo db files --- .../UaDemoServerDb/Demo.Dynamic.Arrays.db | 10 ++++++++++ .../UaDemoServerDb/Demo.Static.Arrays.db | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Dynamic.Arrays.db b/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Dynamic.Arrays.db index 71e0f5bb..e7d5d123 100644 --- a/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Dynamic.Arrays.db +++ b/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Dynamic.Arrays.db @@ -5,6 +5,16 @@ # waveform records connecting to arrays of different types +record(waveform, "$(P)$(R)wfbool") { + field(DTYP, "OPCUA") + field(INP, "@$(SUBS) ns=2;s=Demo.Dynamic.Arrays.Boolean") + field(FTVL, "UCHAR") + field(NELM, "10") + field(SCAN, "I/O Intr") + field(TSE, "-2") + field(TPRO, "$(DEBUG=0)") +} + record(waveform, "$(P)$(R)wfchar") { field(DTYP, "OPCUA") field(INP, "@$(SUBS) ns=2;s=Demo.Dynamic.Arrays.SByte") diff --git a/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Static.Arrays.db b/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Static.Arrays.db index b0733323..852d1040 100644 --- a/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Static.Arrays.db +++ b/exampleTop/DeviceDbApp/UaDemoServerDb/Demo.Static.Arrays.db @@ -5,6 +5,25 @@ # aai/aao records connecting to arrays of different types +record(aai, "$(P)$(R)aaibool") { + field(DTYP, "OPCUA") + field(INP, "@$(SUBS) ns=2;s=Demo.Static.Arrays.Boolean") + field(FTVL, "UCHAR") + field(NELM, "10") + field(SCAN, "I/O Intr") + field(TSE, "-2") + field(TPRO, "$(DEBUG=0)") +} + +record(aao, "$(P)$(R)aaobool") { + field(DTYP, "OPCUA") + field(OUT, "@$(SUBS) ns=2;s=Demo.Static.Arrays.Boolean") + field(FTVL, "UCHAR") + field(NELM, "10") + field(TSE, "-2") + field(TPRO, "$(DEBUG=0)") +} + record(aai, "$(P)$(R)aaichar") { field(DTYP, "OPCUA") field(INP, "@$(SUBS) ns=2;s=Demo.Static.Arrays.SByte") From 61a02d36eb3029dbc00a31ed6b461e4b611df11c Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 9 Oct 2024 11:33:34 +0200 Subject: [PATCH 06/64] uasdk: Fix for bug in "fixConnectionStatus" Commit "fix connection state handling" merged in 7b650af had a bug: For UaSDK, the item was not setting the state for is own itemRecord. --- devOpcuaSup/UaSdk/ItemUaSdk.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index a029fcb4..8bc32bac 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.cpp +++ b/devOpcuaSup/UaSdk/ItemUaSdk.cpp @@ -219,6 +219,8 @@ ItemUaSdk::setState(const ConnectionStatus state) if (auto pd = dataTree.root().lock()) { pd->setState(state); } + if (linkinfo.isItemRecord) + recConnector->setState(state); } void From d885ee32da0084bb1296842063ff680d813dc9da Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 25 Sep 2024 10:47:42 +0200 Subject: [PATCH 07/64] fix arrayWrite for arrays of strings: They are char[num][len], not char*[num] --- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 15 ++++---- devOpcuaSup/UaSdk/DataElementUaSdk.h | 2 +- .../open62541/DataElementOpen62541.cpp | 37 ++++++------------- devOpcuaSup/open62541/DataElementOpen62541.h | 2 +- 4 files changed, 22 insertions(+), 34 deletions(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index bc9f74df..06739d05 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -1060,7 +1060,7 @@ DataElementUaSdk::dbgWriteArray (const epicsUInt32 targetSize, const std::string // Write array for EPICS String / OpcUa_String long -DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, +DataElementUaSdk::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec) @@ -1076,7 +1076,7 @@ DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, prec->name, variantTypeString(incomingData.type()), variantTypeString(targetType), - epicsTypeString(*value)); + epicsTypeString(value)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; } else { @@ -1086,16 +1086,17 @@ DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, char *val = nullptr; const char *pval; // add zero termination if necessary - if (memchr(value[i], '\0', len) == nullptr) { + if (value[len-1] != '\0') { val = new char[len+1]; - strncpy(val, value[i], len); + strncpy(val, value, len); val[len] = '\0'; pval = val; } else { - pval = value[i]; + pval = value; } UaString(pval).copyTo(&arr[i]); delete[] val; + value += len; } { // Scope of Guard G Guard G(outgoingLock); @@ -1103,7 +1104,7 @@ DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, markAsDirty(); } - dbgWriteArray(num, epicsTypeString(*value)); + dbgWriteArray(num, epicsTypeString(value)); } return ret; } @@ -1216,7 +1217,7 @@ DataElementUaSdk::writeArray (const epicsFloat64 *value, const epicsUInt32 num, long DataElementUaSdk::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) { - return writeArray(&value, len, num, OpcUaType_String, prec); + return writeArray(value, len, num, OpcUaType_String, prec); } void diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index 2294aa01..678a5fe8 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -1067,7 +1067,7 @@ class DataElementUaSdk : public DataElement // Write array value for EPICS String / OpcUa_String long - writeArray (const char **value, const epicsUInt32 len, + writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec); diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 59db2a0d..5e84b575 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -1179,7 +1179,7 @@ DataElementOpen62541::dbgWriteArray (const epicsUInt32 targetSize, const std::st // Write array for EPICS String / UA_String long -DataElementOpen62541::writeArray (const char **value, const epicsUInt32 len, +DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, const UA_DataType *targetType, dbCommon *prec) @@ -1195,7 +1195,7 @@ DataElementOpen62541::writeArray (const char **value, const epicsUInt32 len, prec->name, variantTypeString(incomingData), variantTypeString(targetType), - epicsTypeString(*value)); + epicsTypeString(value)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; } else { @@ -1203,36 +1203,23 @@ DataElementOpen62541::writeArray (const char **value, const epicsUInt32 len, if (!arr) { errlogPrintf("%s : out of memory\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; } else { for (epicsUInt32 i = 0; i < num; i++) { - char *val = nullptr; - const char *pval; - // add zero termination if necessary - if (memchr(value[i], '\0', len) == nullptr) { - val = new char[static_cast(len)+1]; // static_cast to avoid warning C26451 - strncpy(val, value[i], len); - val[len] = '\0'; - pval = val; - } else { - pval = value[i]; + arr[i].length = strnlen(value, len); + if (arr[i].length) { + arr[i].data = static_cast(UA_malloc(arr[i].length)); + memcpy(arr[i].data, value, arr[i].length); } - arr[i] = UA_STRING_ALLOC(pval); - delete[] val; + value += len; } - UA_StatusCode status; { // Scope of Guard G Guard G(outgoingLock); - status = UA_Variant_setArrayCopy(&outgoingData, arr, num, targetType); + UA_Variant_setArray(&outgoingData, arr, num, targetType); markAsDirty(); } - if (UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : array copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - dbgWriteArray(num, epicsTypeString(*value)); - } + + dbgWriteArray(num, epicsTypeString(value)); } } return ret; @@ -1344,7 +1331,7 @@ DataElementOpen62541::writeArray (const epicsFloat64 *value, const epicsUInt32 n long DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) { - return writeArray(&value, len, num, &UA_TYPES[UA_TYPES_STRING], prec); + return writeArray(value, len, num, &UA_TYPES[UA_TYPES_STRING], prec); } void diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index 979f1068..a9c2c17f 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -1210,7 +1210,7 @@ class DataElementOpen62541 : public DataElement // Write array value for EPICS String / UA_String long - writeArray (const char **value, const epicsUInt32 len, + writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, const UA_DataType *targetType, dbCommon *prec); From 84c8dce18980c3712c28fa7f3b034c47061be77e Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Tue, 18 Jun 2024 15:20:30 +0200 Subject: [PATCH 08/64] unconditionally show error when monitor item fails --- devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp | 19 ++++++++--------- .../open62541/SubscriptionOpen62541.cpp | 21 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index c83ab8be..b77761ef 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -199,20 +199,19 @@ SubscriptionUaSdk::addMonitoredItems () std::cout << "Subscription " << name << "@" << psessionuasdk->getName() << ": created " << items.size() << " monitored items (" << status.toString().toUtf8() << ")" << std::endl; - if (debug >= 5) { - for (i = 0; i < items.size(); i++) { - UaNodeId node(monitoredItemCreateRequests[i].ItemToMonitor.NodeId); - if (OpcUa_IsGood(monitoredItemCreateResults[i].StatusCode)) - std::cout << "** Monitored item " << node.toXmlString().toUtf8() + for (i = 0; i < items.size(); i++) { + if (OpcUa_IsGood(monitoredItemCreateResults[i].StatusCode)) { + if (debug >= 5) + std::cout << "** Monitored item " << UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8() << " succeeded with id " << monitoredItemCreateResults[i].MonitoredItemId << " revised sampling interval " << monitoredItemCreateResults[i].RevisedSamplingInterval << " revised queue size " << monitoredItemCreateResults[i].RevisedQueueSize << std::endl; - else - std::cout << "** Monitored item " << node.toXmlString().toUtf8() - << " failed with error " - << UaStatus(monitoredItemCreateResults[i].StatusCode).toString().toUtf8() - << std::endl; + } else { + errlogPrintf("OPC UA record %s monitored item %s failed with error %s\n", + items[i]->recConnector->getRecordName(), + UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8(), + UaStatus(monitoredItemCreateResults[i].StatusCode).toString().toUtf8()); } } } diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index 624cb76a..031a1b1f 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -163,27 +163,26 @@ SubscriptionOpen62541::addMonitoredItems () monitoredItemCreateRequest.requestedParameters.discardOldest = it->linkinfo.discardOldest; monitoredItemCreateResult = UA_Client_MonitoredItems_createDataChange( session.client, subscriptionSettings.subscriptionId, UA_TIMESTAMPSTORETURN_BOTH, - monitoredItemCreateRequest, items[i], [] (UA_Client *client, UA_UInt32 subId, void *subContext, + monitoredItemCreateRequest, it, [] (UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value) { static_cast(subContext)-> dataChange(monId, *static_cast(monContext), value); }, nullptr /* deleteCallback */); if (monitoredItemCreateResult.statusCode == UA_STATUSCODE_GOOD) { - items[i]->setRevisedSamplingInterval(monitoredItemCreateResult.revisedSamplingInterval); - items[i]->setRevisedQueueSize(monitoredItemCreateResult.revisedQueueSize); - } - if (debug >= 5) { - if (monitoredItemCreateResult.statusCode == UA_STATUSCODE_GOOD) + it->setRevisedSamplingInterval(monitoredItemCreateResult.revisedSamplingInterval); + it->setRevisedQueueSize(monitoredItemCreateResult.revisedQueueSize); + if (debug >= 5) { std::cout << "** Monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId << " succeeded with id " << monitoredItemCreateResult.monitoredItemId << " revised sampling interval " << monitoredItemCreateResult.revisedSamplingInterval << " revised queue size " << monitoredItemCreateResult.revisedQueueSize << std::endl; - else - std::cout << "** Monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId - << " failed with error " - << UA_StatusCode_name(monitoredItemCreateResult.statusCode) - << std::endl; + } + } else { + std::cerr << "OPC UA record " << it->recConnector->getRecordName() + << " monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId + << " failed with error " << UA_StatusCode_name(monitoredItemCreateResult.statusCode) + << std::endl; } i++; } From ab433d5ee3daf9343bf8199975a24ff7bf57ce5c Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 18 Sep 2024 17:28:09 +0200 Subject: [PATCH 09/64] show connectionLoss/COMM_ALARM when monitoredItem setup failed Value would appear frozen when installing monitoredItem failed for example because the server has reached the limit of monitored items or if an item cannot be monitored because it used invalid filters like a deadband filter on a string. show connectionLoss/COMM_ALARM when monitoredItem setup failed Value would simply appear frozen otherwise. Needed to revert order of initial read and montior setup so that errors in monitor setup are not cleared by successful initial read. --- devOpcuaSup/UaSdk/SessionUaSdk.cpp | 3 ++- devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp | 4 +++- devOpcuaSup/open62541/SessionOpen62541.cpp | 2 +- devOpcuaSup/open62541/SubscriptionOpen62541.cpp | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.cpp b/devOpcuaSup/UaSdk/SessionUaSdk.cpp index 6f8b42ee..99a458e5 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SessionUaSdk.cpp @@ -1173,7 +1173,6 @@ void SessionUaSdk::connectionStatusChanged ( rebuildNodeIds(); registerNodes(); createAllSubscriptions(); - addAllMonitoredItems(); } if (serverConnectionStatus != UaClient::ConnectionWarningWatchdogTimeout) { if (debug) { @@ -1192,6 +1191,8 @@ void SessionUaSdk::connectionStatusChanged ( // status needs to be updated before requests are being issued serverConnectionStatus = serverStatus; reader.pushRequest(cargo, menuPriorityHIGH); + epicsThreadSleep(0.01); // Don't know how to wait for initial read to complete + addAllMonitoredItems(); } break; diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index b77761ef..5687dd29 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -202,7 +202,8 @@ SubscriptionUaSdk::addMonitoredItems () for (i = 0; i < items.size(); i++) { if (OpcUa_IsGood(monitoredItemCreateResults[i].StatusCode)) { if (debug >= 5) - std::cout << "** Monitored item " << UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8() + std::cout << "** OPC UA record " << items[i]->recConnector->getRecordName() + << " monitored item " << UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8() << " succeeded with id " << monitoredItemCreateResults[i].MonitoredItemId << " revised sampling interval " << monitoredItemCreateResults[i].RevisedSamplingInterval << " revised queue size " << monitoredItemCreateResults[i].RevisedQueueSize @@ -212,6 +213,7 @@ SubscriptionUaSdk::addMonitoredItems () items[i]->recConnector->getRecordName(), UaNodeId(monitoredItemCreateRequests[i].ItemToMonitor.NodeId).toXmlString().toUtf8(), UaStatus(monitoredItemCreateResults[i].StatusCode).toString().toUtf8()); + items[i]->setIncomingEvent(ProcessReason::connectionLoss); } } } diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index 3694422b..2f6010a5 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.cpp +++ b/devOpcuaSup/open62541/SessionOpen62541.cpp @@ -2347,7 +2347,6 @@ SessionOpen62541::connectionStatusChanged ( rebuildNodeIds(); registerNodes(); createAllSubscriptions(); - addAllMonitoredItems(); if (debug) { std::cout << "Session " << name << ": triggering initial read for all " @@ -2365,6 +2364,7 @@ SessionOpen62541::connectionStatusChanged ( // status needs to be updated before requests are being issued sessionState = newSessionState; reader.pushRequest(cargo, menuPriorityHIGH); + addAllMonitoredItems(); break; } diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index 031a1b1f..eaa216dd 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -172,7 +172,8 @@ SubscriptionOpen62541::addMonitoredItems () it->setRevisedSamplingInterval(monitoredItemCreateResult.revisedSamplingInterval); it->setRevisedQueueSize(monitoredItemCreateResult.revisedQueueSize); if (debug >= 5) { - std::cout << "** Monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId + std::cout << "** OPC UA record " << it->recConnector->getRecordName() + << " monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId << " succeeded with id " << monitoredItemCreateResult.monitoredItemId << " revised sampling interval " << monitoredItemCreateResult.revisedSamplingInterval << " revised queue size " << monitoredItemCreateResult.revisedQueueSize @@ -183,6 +184,7 @@ SubscriptionOpen62541::addMonitoredItems () << " monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId << " failed with error " << UA_StatusCode_name(monitoredItemCreateResult.statusCode) << std::endl; + it->setIncomingEvent(ProcessReason::connectionLoss); } i++; } From 5ef94dee0051c5651f564a162d56a1ce1a3cb111 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Mon, 17 Jun 2024 14:11:03 +0200 Subject: [PATCH 10/64] break infinite loop when updating elements with FLNK to item record When updating records from the driver, disable FLNK unless SCAN="I/O Intr". Otherwise if the (output) record is a structure element and the FLNK causes the parent opcuaItem record to process, an inifnite processing loop is created. Also, the user probably only expects the FLNK to process when actively writing to the output record, not when updating it from the server. Fixes issue #145. --- devOpcuaSup/RecordConnector.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/devOpcuaSup/RecordConnector.cpp b/devOpcuaSup/RecordConnector.cpp index e83a5ce2..c2c6bff4 100644 --- a/devOpcuaSup/RecordConnector.cpp +++ b/devOpcuaSup/RecordConnector.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #define epicsExportSharedSymbols #include "RecordConnector.h" @@ -76,6 +77,16 @@ long reProcess (dbCommon *prec) return status; } +#if EPICS_VERSION_INT >= VERSION_INT(3,16,0,1) +#define SAVE_FLNK(prec) struct lset *lset = prec->flnk.lset +#define DISABLE_FLNK(prec) prec->flnk.lset = NULL +#define RESTORE_FLNK(prec) prec->flnk.lset = lset +#else +#define SAVE_FLNK(prec) short type = prec->flnk.type +#define DISABLE_FLNK(prec) prec->flnk.type = 0 +#define RESTORE_FLNK(prec) prec->flnk.type = type +#endif + void processCallback (epicsCallback *pcallback, const ProcessReason reason) { void *pUsr; @@ -89,10 +100,17 @@ void processCallback (epicsCallback *pcallback, const ProcessReason reason) dbScanLock(prec); ProcessReason oldreason = pvt->reason; pvt->reason = reason; + + // Do not process FLNK on updates if not "I/O Intr" + SAVE_FLNK(prec); + if (reason != writeComplete && prec->scan != menuScanI_O_Intr) + DISABLE_FLNK(prec); if (prec->pact) reProcess(prec); else dbProcess(prec); + RESTORE_FLNK(prec); + pvt->reason = oldreason; dbScanUnlock(prec); } From fc789228c491337b36431c7dec3cb8d111604917 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Thu, 10 Oct 2024 16:47:09 +0200 Subject: [PATCH 11/64] open62541: bugfix: don't use timefrom element with wrong type --- devOpcuaSup/open62541/DataElementOpen62541.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 5e84b575..a3869425 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -179,8 +179,8 @@ DataElementOpen62541::createMap (const UA_DataType *type, timefrom->c_str(), typeKindName(typeKindOf(timeMemberType)), timeIsArray ? "[]" : ""); - } - timesrc = timeOffset; + } else + timesrc = timeOffset; } else { errlogPrintf( "%s: timestamp element %s not found - using source timestamp\n", From 90dcdce2b9d0c5cfb9e9e617bed6e74804bfbfeb Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 25 Sep 2024 14:44:05 +0200 Subject: [PATCH 12/64] open62541: rename nodeid to nodeId for consistency --- devOpcuaSup/open62541/ItemOpen62541.cpp | 16 ++++++++-------- devOpcuaSup/open62541/ItemOpen62541.h | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/devOpcuaSup/open62541/ItemOpen62541.cpp b/devOpcuaSup/open62541/ItemOpen62541.cpp index 930e5809..c5ef95e9 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.cpp +++ b/devOpcuaSup/open62541/ItemOpen62541.cpp @@ -46,7 +46,7 @@ ItemOpen62541::ItemOpen62541(const linkInfo &info) , lastStatus(UA_STATUSCODE_BADSERVERNOTCONNECTED) , lastReason(ProcessReason::connectionLoss) { - UA_NodeId_init(&nodeid); + UA_NodeId_init(&nodeId); if (linkinfo.subscription != "" && linkinfo.monitor) { subscription = SubscriptionOpen62541::find(linkinfo.subscription); subscription->addItemOpen62541(this); @@ -61,18 +61,18 @@ ItemOpen62541::~ItemOpen62541 () { subscription->removeItemOpen62541(this); session->removeItemOpen62541(this); - UA_NodeId_clear(&nodeid); + UA_NodeId_clear(&nodeId); } void ItemOpen62541::rebuildNodeId () { UA_UInt16 ns = session->mapNamespaceIndex(linkinfo.namespaceIndex); - UA_NodeId_clear(&nodeid); + UA_NodeId_clear(&nodeId); if (linkinfo.identifierIsNumeric) { - nodeid = UA_NODEID_NUMERIC(ns, linkinfo.identifierNumber); + nodeId = UA_NODEID_NUMERIC(ns, linkinfo.identifierNumber); } else { - nodeid = UA_NODEID_STRING_ALLOC(ns, linkinfo.identifierString.c_str()); + nodeId = UA_NODEID_STRING_ALLOC(ns, linkinfo.identifierString.c_str()); } registered = false; } @@ -90,8 +90,8 @@ ItemOpen62541::show (int level) const { std::cout << "item" << " ns="; - if (nodeid.namespaceIndex != linkinfo.namespaceIndex) - std::cout << nodeid.namespaceIndex << "(" << linkinfo.namespaceIndex << ")"; + if (nodeId.namespaceIndex != linkinfo.namespaceIndex) + std::cout << nodeId.namespaceIndex << "(" << linkinfo.namespaceIndex << ")"; else std::cout << linkinfo.namespaceIndex; if (linkinfo.identifierIsNumeric) @@ -115,7 +115,7 @@ ItemOpen62541::show (int level) const << " monitor=" << (linkinfo.monitor ? "y" : "n") << " registered="; if (registered) - std::cout << nodeid; + std::cout << nodeId; else std::cout << "-"; std::cout << "(" << (linkinfo.registerNode ? "y" : "n") << ")" << std::endl; diff --git a/devOpcuaSup/open62541/ItemOpen62541.h b/devOpcuaSup/open62541/ItemOpen62541.h index 146de8bf..8ef78a19 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.h +++ b/devOpcuaSup/open62541/ItemOpen62541.h @@ -94,13 +94,13 @@ class ItemOpen62541 : public Item * @brief Setter for the node id of this item. * @return node id */ - void setRegisteredNodeId(const UA_NodeId &id) { UA_NodeId_copy(&id, &nodeid); registered = true; } + void setRegisteredNodeId(const UA_NodeId &id) { UA_NodeId_copy(&id, &nodeId); registered = true; } /** * @brief Getter that returns the node id of this item. * @return node id */ - const UA_NodeId &getNodeId() const { return nodeid; } + const UA_NodeId &getNodeId() const { return nodeId; } /** * @brief Setter for the status of a read operation. @@ -202,7 +202,7 @@ class ItemOpen62541 : public Item private: SubscriptionOpen62541 *subscription; /**< raw pointer to subscription (if monitored) */ SessionOpen62541 *session; /**< raw pointer to session */ - UA_NodeId nodeid; /**< node id of this item */ + UA_NodeId nodeId; /**< node id of this item */ bool registered; /**< flag for registration status */ UA_Double revisedSamplingInterval; /**< server-revised sampling interval */ UA_UInt32 revisedQueueSize; /**< server-revised queue size */ From a1bd5aebe2b9c7307520c771ecab90c15b926a06 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 25 Sep 2024 14:50:40 +0200 Subject: [PATCH 13/64] open62541: clear outgoingData before setting it outgoingData may still contain old yet unsent data that needs to be discarded to avoid a memory leak --- .../open62541/DataElementOpen62541.cpp | 276 +++++++++--------- devOpcuaSup/open62541/DataElementOpen62541.h | 268 ++++++++--------- 2 files changed, 260 insertions(+), 284 deletions(-) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index a3869425..ee3f709c 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -999,161 +999,148 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC unsigned long ul; double d; - switch (typeKindOf(incomingData)) { - case UA_DATATYPEKIND_STRING: - { - UA_String val; - val.length = strnlen(value, len); - val.data = const_cast(reinterpret_cast(value)); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); - markAsDirty(); - } - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - UA_LocalizedText val; - val.locale = reinterpret_cast(incomingData.data)->locale; - val.text.length = strnlen(value, len); - val.text.data = const_cast(reinterpret_cast(value)); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); - markAsDirty(); - } - break; - } - case UA_DATATYPEKIND_BOOLEAN: { // Scope of Guard G Guard G(outgoingLock); - UA_Boolean val = strchr("YyTt1", *value) != NULL; - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); - markAsDirty(); - break; - } - case UA_DATATYPEKIND_BYTE: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_Byte val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_SBYTE: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_SByte val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT16: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_UInt16 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT16: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_Int16 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + switch (typeKindOf(incomingData)) { + case UA_DATATYPEKIND_STRING: + { + UA_String val; + val.length = strnlen(value, len); + val.data = const_cast(reinterpret_cast(value)); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_UINT32: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_UInt32 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + UA_LocalizedText val; + val.locale = reinterpret_cast(incomingData.data)->locale; + val.text.length = strnlen(value, len); + val.text.data = const_cast(reinterpret_cast(value)); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_INT32: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_Int32 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + case UA_DATATYPEKIND_BOOLEAN: + { + UA_Boolean val = strchr("YyTt1", *value) != NULL; + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_UINT64: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { - Guard G(outgoingLock); - UA_UInt64 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT64: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { - Guard G(outgoingLock); - UA_Int64 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + case UA_DATATYPEKIND_BYTE: + ul = strtoul(value, nullptr, 0); + if (isWithinRange(ul)) { + UA_Byte val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_SBYTE: + l = strtol(value, nullptr, 0); + if (isWithinRange(l)) { + UA_SByte val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_UINT16: + ul = strtoul(value, nullptr, 0); + if (isWithinRange(ul)) { + UA_UInt16 val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_INT16: + l = strtol(value, nullptr, 0); + if (isWithinRange(l)) { + UA_Int16 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_UINT32: + ul = strtoul(value, nullptr, 0); + if (isWithinRange(ul)) { + UA_UInt32 val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_INT32: + l = strtol(value, nullptr, 0); + if (isWithinRange(l)) { + UA_Int32 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_UINT64: + ul = strtoul(value, nullptr, 0); + if (isWithinRange(ul)) { + UA_UInt64 val = static_cast(ul); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_INT64: + l = strtol(value, nullptr, 0); + if (isWithinRange(l)) { + UA_Int64 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_FLOAT: + d = strtod(value, nullptr); + if (isWithinRange(d)) { + UA_Float val = static_cast(d); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_DOUBLE: + { + d = strtod(value, nullptr); + UA_Double val = static_cast(d); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_FLOAT: - d = strtod(value, nullptr); - if (isWithinRange(d)) { - Guard G(outgoingLock); - UA_Float val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); - markAsDirty(); - } else { + default: + errlogPrintf("%s : unsupported conversion for outgoing data\n", + prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; } - break; - case UA_DATATYPEKIND_DOUBLE: - { - d = strtod(value, nullptr); - Guard G(outgoingLock); - UA_Double val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); - break; - } - default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } if (ret == 0 && UA_STATUS_IS_BAD(status)) { errlogPrintf("%s : scalar copy failed: %s\n", @@ -1215,7 +1202,8 @@ DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, } { // Scope of Guard G Guard G(outgoingLock); - UA_Variant_setArray(&outgoingData, arr, num, targetType); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + UA_Variant_setArray(&outgoingData, arr, num, targetType); // no copy but move arr content markAsDirty(); } diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index a9c2c17f..3d5d7b91 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -1045,157 +1045,144 @@ class DataElementOpen62541 : public DataElement long ret = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; - switch (typeKindOf(incomingData)) { - case UA_DATATYPEKIND_BOOLEAN: { // Scope of Guard G Guard G(outgoingLock); - UA_Boolean val = (value != 0); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); - markAsDirty(); - break; - } - case UA_DATATYPEKIND_BYTE: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Byte val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + switch (typeKindOf(incomingData)) { + case UA_DATATYPEKIND_BOOLEAN: + { + UA_Boolean val = (value != 0); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_SBYTE: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_SByte val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_UInt16 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Int16 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_UInt32 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_INT32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Int32 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_UINT64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_UInt64 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); + case UA_DATATYPEKIND_BYTE: + if (isWithinRange(value)) { + UA_Byte val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_SBYTE: + if (isWithinRange(value)) { + UA_SByte val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_UINT16: + if (isWithinRange(value)) { + UA_UInt16 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_INT16: + if (isWithinRange(value)) { + UA_Int16 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_UINT32: + if (isWithinRange(value)) { + UA_UInt32 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_INT32: + if (isWithinRange(value)) { + UA_Int32 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_UINT64: + if (isWithinRange(value)) { + UA_UInt64 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_INT64: + if (isWithinRange(value)) { + UA_Int64 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_FLOAT: + if (isWithinRange(value)) { + UA_Float val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_DOUBLE: + if (isWithinRange(value)) { + UA_Double val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); + markAsDirty(); + } else { + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + break; + case UA_DATATYPEKIND_STRING: + { + std::string strval = std::to_string(value); + UA_String val; + val.length = strval.length(); + val.data = const_cast(reinterpret_cast(strval.c_str())); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_INT64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Int64 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + std::string strval = std::to_string(value); + UA_LocalizedText val; + val.locale = reinterpret_cast(incomingData.data)->locale; + val.text.length = strval.length(); + val.text.data = const_cast(reinterpret_cast(strval.c_str())); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + break; } - break; - case UA_DATATYPEKIND_FLOAT: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Float val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + default: ret = 1; - } - break; - case UA_DATATYPEKIND_DOUBLE: - if (isWithinRange(value)) { - Guard G(outgoingLock); - UA_Double val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); - } else { + errlogPrintf("%s : unsupported conversion for outgoing data\n", + prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - break; - case UA_DATATYPEKIND_STRING: - { - std::string strval = std::to_string(value); - UA_String val; - val.length = strval.length(); - val.data = const_cast(reinterpret_cast(strval.c_str())); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); - markAsDirty(); - } - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - std::string strval = std::to_string(value); - UA_LocalizedText val; - val.locale = reinterpret_cast(incomingData.data)->locale; - val.text.length = strval.length(); - val.text.data = const_cast(reinterpret_cast(strval.c_str())); - { // Scope of Guard G - Guard G(outgoingLock); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); - markAsDirty(); } - break; - } - default: - ret = 1; - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } if (ret == 0 && UA_STATUS_IS_BAD(status)) { errlogPrintf("%s : scalar copy failed: %s\n", @@ -1242,6 +1229,7 @@ class DataElementOpen62541 : public DataElement UA_StatusCode status; { // Scope of Guard G Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard status = UA_Variant_setArrayCopy(&outgoingData, value, num, targetType); markAsDirty(); } From 5a4543676b68bc6616418f31d2bf0356d33b6db0 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Tue, 27 Feb 2024 15:04:45 +0100 Subject: [PATCH 14/64] improve Enum handling Read enum type info with strings at connect Identify enum types during initial read and assign string maps Update enum strings in mbbi/mbbo records at initial read Convert enums to and from strings for string records and char waveforms If user did not define mbbi/mbbo choice values, use VAL instead of RVAL. Closes issue #158. --- devOpcuaSup/DataElement.h | 4 + devOpcuaSup/Session.h | 2 + devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 146 +++++---- devOpcuaSup/UaSdk/DataElementUaSdk.h | 70 ++--- devOpcuaSup/UaSdk/ItemUaSdk.cpp | 4 +- devOpcuaSup/UaSdk/ItemUaSdk.h | 3 +- devOpcuaSup/UaSdk/SessionUaSdk.cpp | 30 +- devOpcuaSup/UaSdk/SessionUaSdk.h | 10 + devOpcuaSup/devOpcua.cpp | 115 ++++++- .../open62541/DataElementOpen62541.cpp | 145 +++++---- devOpcuaSup/open62541/DataElementOpen62541.h | 148 +++++---- devOpcuaSup/open62541/SessionOpen62541.cpp | 284 ++++++++++++++---- devOpcuaSup/open62541/SessionOpen62541.h | 35 ++- 13 files changed, 684 insertions(+), 312 deletions(-) diff --git a/devOpcuaSup/DataElement.h b/devOpcuaSup/DataElement.h index e09055db..a86af336 100644 --- a/devOpcuaSup/DataElement.h +++ b/devOpcuaSup/DataElement.h @@ -13,6 +13,7 @@ #ifndef DEVOPCUA_DATAELEMENT_H #define DEVOPCUA_DATAELEMENT_H +#include #include #include #include @@ -24,6 +25,8 @@ namespace DevOpcua { +typedef std::map EnumChoices; + class RecordConnector; /** @@ -795,6 +798,7 @@ class DataElement virtual void requestRecordProcessing(const ProcessReason reason) const = 0; const std::string name; /**< element name */ + const EnumChoices* enumChoices = nullptr; /**< enum definition if this element is an enum */ protected: /** diff --git a/devOpcuaSup/Session.h b/devOpcuaSup/Session.h index d1b2283c..4acffe83 100644 --- a/devOpcuaSup/Session.h +++ b/devOpcuaSup/Session.h @@ -355,6 +355,8 @@ class epicsShareClass Session bool autoConnect; /**< auto (re)connect flag */ std::string securityIdentityFile; /**< full path to file with Identity token settings */ std::string securityUserName; /**< user name set in Username token */ + + const int no_of_properties_read = 2; /* always read type and value */ }; diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index 06739d05..66364cfc 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -110,12 +111,17 @@ DataElementUaSdk::show (const int level, const unsigned int indent) const void DataElementUaSdk::setIncomingData(const UaVariant &value, ProcessReason reason, - const std::string *timefrom) + const std::string *timefrom, + const UaNodeId *typeId) { // Make a copy of this element and cache it incomingData = value; if (isLeaf()) { + if (typeId && pconnector->state() == ConnectionStatus::initialRead) { + delete enumChoices; + enumChoices = pitem->session->getEnumChoices(typeId); + } if ((pconnector->state() == ConnectionStatus::initialRead && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) || (pconnector->state() == ConnectionStatus::up)) { @@ -183,6 +189,8 @@ DataElementUaSdk::setIncomingData(const UaVariant &value, for (int i = 0; i < definition.childrenCount(); i++) { if (pelem->name == definition.child(i).name().toUtf8()) { elementMap.insert({i, it}); + delete pelem->enumChoices; + pelem->enumChoices = pitem->session->getEnumChoices(definition.child(i).enumDefinition()); pelem->setIncomingData(genericValue.value(i), reason); } } @@ -486,7 +494,19 @@ DataElementUaSdk::readScalar (char *value, const size_t num, if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - strncpy(value, upd->getData().toString().toUtf8(), num); + UaVariant& data = upd->getData(); + if (enumChoices) { + OpcUa_Int32 enumIndex; + data.toInt32(enumIndex); + auto it = enumChoices->find(enumIndex); + if (it != enumChoices->end() && !it->second.empty()) { + strncpy(value, it->second.c_str(), num); + } else { + strncpy(value, data.toString().toUtf8(), num); + } + } else { + strncpy(value, data.toString().toUtf8(), num); + } value[num-1] = '\0'; prec->udf = false; } @@ -896,10 +916,11 @@ DataElementUaSdk::writeScalar (const epicsFloat64 &value, dbCommon *prec) long DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommon *prec) { - long ret = 0; + long ret = 1; long l; unsigned long ul; double d; + char* end = nullptr; switch (incomingData.type()) { case OpcUaType_String: @@ -907,6 +928,7 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo Guard G(outgoingLock); outgoingData.setString(static_cast(value)); markAsDirty(); + ret = 0; break; } case OpcUaType_LocalizedText: @@ -917,6 +939,7 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo localizedText.setText(static_cast(value)); // keep the Locale outgoingData.setLocalizedText(localizedText); markAsDirty(); + ret = 0; break; } case OpcUaType_Boolean: @@ -927,122 +950,133 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo else outgoingData.setBoolean(false); markAsDirty(); + ret = 0; break; } case OpcUaType_Byte: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { Guard G(outgoingLock); outgoingData.setByte(static_cast(ul)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_SByte: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { Guard G(outgoingLock); outgoingData.setSByte(static_cast(l)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_UInt16: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { Guard G(outgoingLock); outgoingData.setUInt16(static_cast(ul)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Int16: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { Guard G(outgoingLock); outgoingData.setInt16(static_cast(l)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_UInt32: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { Guard G(outgoingLock); outgoingData.setUInt32(static_cast(ul)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Int32: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (enumChoices) { + // first test enum strings then numeric values + // in case a string starts with a number but means a different value + for (auto it: *enumChoices) + if (it.second == value) { + l = static_cast(it.first); + ret = 0; + end = nullptr; + break; + } + if (ret != 0 && end != value) + for (auto it: *enumChoices) + if (l == it.first) { + ret = 0; + break; + } + if (ret != 0) + break; + } + if (end != value && isWithinRange(l)) { Guard G(outgoingLock); outgoingData.setInt32(static_cast(l)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_UInt64: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { Guard G(outgoingLock); outgoingData.setUInt64(static_cast(ul)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Int64: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { Guard G(outgoingLock); outgoingData.setInt64(static_cast(l)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Float: - d = strtod(value, nullptr); - if (isWithinRange(d)) { + d = strtod(value, &end); + if (end != value && isWithinRange(d)) { Guard G(outgoingLock); outgoingData.setFloat(static_cast(d)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Double: { - d = strtod(value, nullptr); - Guard G(outgoingLock); - outgoingData.setDouble(static_cast(d)); - markAsDirty(); + d = strtod(value, &end); + if (end != value) { + Guard G(outgoingLock); + outgoingData.setDouble(static_cast(d)); + markAsDirty(); + ret = 0; + } break; } default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); + errlogPrintf("%s : unsupported conversion from string to %s for outgoing data\n", + prec->name, variantTypeString(incomingData.type())); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + return -1; } - - dbgWriteScalar(); + if (ret != 0) { + errlogPrintf("%s : value \"%s\" out of range\n", + prec->name, value); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret == 0) + dbgWriteScalar(); return ret; } diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index 678a5fe8..20636c93 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -190,6 +190,8 @@ class DataElementUaSdk : public DataElement DataElementUaSdk(const std::string &name, ItemUaSdk *item); + ~DataElementUaSdk() { delete enumChoices; } + /** * @brief Create a DataElement and add it to the item's dataTree. * @@ -238,10 +240,12 @@ class DataElementUaSdk : public DataElement * @param value new value for this data element * @param reason reason for this value update * @param timefrom name of element to read item timestamp from + * @param typeId data type of the data element */ void setIncomingData(const UaVariant &value, ProcessReason reason, - const std::string *timefrom = nullptr); + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr); /** * @brief Push an incoming event into the DataElement. @@ -935,17 +939,15 @@ class DataElementUaSdk : public DataElement writeScalar (const ET &value, dbCommon *prec) { - long ret = 0; + long ret = 1; switch (incomingData.type()) { case OpcUaType_Boolean: { // Scope of Guard G Guard G(outgoingLock); - if (value == 0) - outgoingData.setBoolean(false); - else - outgoingData.setBoolean(true); + outgoingData.setBoolean(value != 0); markAsDirty(); + ret = 0; break; } case OpcUaType_Byte: @@ -953,9 +955,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setByte(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_SByte: @@ -963,9 +963,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setSByte(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_UInt16: @@ -973,9 +971,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setUInt16(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Int16: @@ -983,9 +979,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setInt16(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_UInt32: @@ -993,19 +987,17 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setUInt32(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; - case OpcUaType_Int32: - if (isWithinRange(value)) { + case OpcUaType_Int32: // may be an enum + if (isWithinRange(value) && + (!enumChoices || + enumChoices->find(static_cast(value)) != enumChoices->end())) { Guard G(outgoingLock); outgoingData.setInt32(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_UInt64: @@ -1013,9 +1005,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setUInt64(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Int64: @@ -1023,9 +1013,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setInt64(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Float: @@ -1033,9 +1021,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setFloat(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_Double: @@ -1043,9 +1029,7 @@ class DataElementUaSdk : public DataElement Guard G(outgoingLock); outgoingData.setDouble(static_cast(value)); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case OpcUaType_String: @@ -1056,12 +1040,18 @@ class DataElementUaSdk : public DataElement break; } default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", + errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", + prec->name, epicsTypeString(value), variantTypeString(incomingData.type())); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret != 0) { + errlogPrintf("%s : value out of range\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } - dbgWriteScalar(); + if (ret == 0) + dbgWriteScalar(); return ret; } diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index 8bc32bac..97b7825b 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.cpp +++ b/devOpcuaSup/UaSdk/ItemUaSdk.cpp @@ -150,7 +150,7 @@ ItemUaSdk::uaToEpicsTime (const UaDateTime &dt, const OpcUa_UInt16 pico10) } void -ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason) +ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason, const UaNodeId* typeId) { tsClient = epicsTime::getCurrent(); if (OpcUa_IsNotBad(value.StatusCode)) { @@ -177,7 +177,7 @@ ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason) const std::string *timefrom = nullptr; if (linkinfo.timestamp == LinkOptionTimestamp::data && linkinfo.timestampElement.length()) timefrom = &linkinfo.timestampElement; - pd->setIncomingData(value.Value, reason, timefrom); + pd->setIncomingData(value.Value, reason, timefrom, typeId); } if (linkinfo.isItemRecord) { diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.h b/devOpcuaSup/UaSdk/ItemUaSdk.h index 47a7a75b..409b19fa 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.h +++ b/devOpcuaSup/UaSdk/ItemUaSdk.h @@ -168,8 +168,9 @@ class ItemUaSdk : public Item * * @param value new value for this data element * @param reason reason for this value update + * @param typeId data type id of the item */ - void setIncomingData(const OpcUa_DataValue &value, ProcessReason reason); + void setIncomingData(const OpcUa_DataValue &value, ProcessReason reason, const UaNodeId *typeId = nullptr); /** * @brief Push an incoming event down the root element. diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.cpp b/devOpcuaSup/UaSdk/SessionUaSdk.cpp index 99a458e5..ffa41ff8 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SessionUaSdk.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #ifdef HAS_SECURITY #include @@ -405,9 +406,12 @@ SessionUaSdk::processRequests(std::vector> &batch) ServiceSettings serviceSettings; OpcUa_UInt32 id = getTransactionId(); - nodesToRead.create(static_cast(batch.size())); + nodesToRead.create(static_cast(batch.size() * no_of_properties_read)); OpcUa_UInt32 i = 0; for (auto c : batch) { + c->item->getNodeId().copyTo(&nodesToRead[i].NodeId); + nodesToRead[i].AttributeId = OpcUa_Attributes_DataType; + i++; c->item->getNodeId().copyTo(&nodesToRead[i].NodeId); nodesToRead[i].AttributeId = OpcUa_Attributes_Value; itemsToRead->push_back(c->item); @@ -1211,6 +1215,19 @@ void SessionUaSdk::connectionStatusChanged ( serverConnectionStatus = serverStatus; } +const EnumChoices* +SessionUaSdk::getEnumChoices(const UaEnumDefinition& enumDefinition) const +{ + if (!enumDefinition.childrenCount()) + return nullptr; + auto enumChoices = new EnumChoices; + for (int i = 0; i < enumDefinition.childrenCount(); i++) { + const UaEnumValue& choice = enumDefinition.child(i); + enumChoices->emplace(choice.value(), choice.name().toUtf8()); + } + return enumChoices; +} + void SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, const UaStatus &result, @@ -1229,7 +1246,7 @@ SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, << ": (readComplete) getting data for read service" << " (transaction id " << transactionId << "; data for " << values.length() << " items)" << std::endl; - if ((*it->second).size() != values.length()) + if ((*it->second).size() * no_of_properties_read != values.length()) errlogPrintf("OPC UA session %s: (readComplete) received a callback " "with %u values for a request containing %lu items\n", name.c_str(), values.length(), (*it->second).size()); @@ -1238,6 +1255,11 @@ SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, if (i >= values.length()) { item->setIncomingEvent(ProcessReason::readFailure); } else { + UaNodeId typeId; + if (OpcUa_IsGood(values[i].StatusCode)) { + UaVariant(values[i].Value).toNodeId(typeId); + } + i++; if (debug >= 5) { std::cout << "** Session " << name.c_str() << ": (readComplete) getting data for item " @@ -1246,9 +1268,9 @@ SessionUaSdk::readComplete (OpcUa_UInt32 transactionId, ProcessReason reason = ProcessReason::readComplete; if (OpcUa_IsNotGood(values[i].StatusCode)) reason = ProcessReason::readFailure; - item->setIncomingData(values[i], reason); + item->setIncomingData(values[i], reason, &typeId); + i++; } - i++; } outstandingOps.erase(it); } else { diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.h b/devOpcuaSup/UaSdk/SessionUaSdk.h index efee6622..ed8ec6d6 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.h +++ b/devOpcuaSup/UaSdk/SessionUaSdk.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,7 @@ #include "RequestQueueBatcher.h" #include "Session.h" #include "Registry.h" +#include "DataElement.h" namespace DevOpcua { @@ -115,6 +117,14 @@ class SessionUaSdk */ virtual const std::string & getName() const override; + /** + * @brief Get pointer to enumChoices if argument refers to enum type, else nullptr + */ + const EnumChoices* getEnumChoices(const UaEnumDefinition& enumDefinition) const; + + const EnumChoices* getEnumChoices(const UaNodeId* typeId) const + { return typeId ? getEnumChoices(puasession->enumDefinition(*typeId)) : nullptr; } + /** * @brief Get a structure definition from the session dictionary. * @param dataTypeId data type of the extension object diff --git a/devOpcuaSup/devOpcua.cpp b/devOpcuaSup/devOpcua.cpp index db2028fe..967c4016 100644 --- a/devOpcuaSup/devOpcua.cpp +++ b/devOpcuaSup/devOpcua.cpp @@ -699,7 +699,83 @@ opcua_write_analog (REC *prec) return ret; } -// enum output +// enum + +#define NUMBER_OF_ENUM_CHOICES 16 +#define ENUMS_BY_OPCUA 0x10 +#define ENUM_VALUES_CHECKED 0x20 +#define ENUM_VALUES_DEFINED 0x40 + +template +inline void updateEnumInfos (REC *prec, RecordConnector *pcon) +{ + if (pcon->pdataelement->enumChoices && + pcon->state() == ConnectionStatus::initialRead && + (!prec->sdef || prec->sdef & ENUMS_BY_OPCUA)) + { // no strings/value defined by user (but maybe by OPC-UA server) + prec->sdef |= ENUMS_BY_OPCUA | ENUM_VALUES_CHECKED | ENUM_VALUES_DEFINED; + const EnumChoices &enumChoices = *pcon->pdataelement->enumChoices; + auto pvl = &prec->zrvl; + auto pst = &prec->zrst; + size_t i = 0; + for (auto it: enumChoices) { + pvl[i] = it.first; + strncpy(pst[i], it.second.c_str(), sizeof(pst[i])-1); + if (++i >= NUMBER_OF_ENUM_CHOICES) break; + } + while (i < NUMBER_OF_ENUM_CHOICES) { + // clear possible other old strings/values + pvl[i] = ~0; // as invalid as possible + memset(pst[i], 0, sizeof(pst[i])); + i++; + } + db_post_events(prec, &prec->val, DBE_PROPERTY); + } else if (prec->sdef && !(prec->sdef & ENUM_VALUES_CHECKED)) { + // check if user defined strings but no values + prec->sdef |= ENUM_VALUES_CHECKED; + auto pvl = &prec->zrvl; + for (int i = 0; i < NUMBER_OF_ENUM_CHOICES; i++) { + if (*pvl) { + prec->sdef |= ENUM_VALUES_DEFINED; + break; + } + } + } +} + + +template +long +opcua_read_enum (REC *prec) +{ + long ret = 0; + TRY { + Guard G(pcon->lock); + ProcessReason nextReason = ProcessReason::none; + + if (pcon->reason == ProcessReason::none || pcon->reason == ProcessReason::readRequest) { + if (pcon->state() == ConnectionStatus::up) { + prec->pact = true; + pcon->requestOpcuaRead(); + } else { + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + } + } else { + epicsUInt32 *value = useReadValue(pcon) ? &prec->rval : nullptr; + ret = pcon->readScalar(value, &nextReason); + updateEnumInfos (prec, pcon); + if (ret == 0 && value && !(prec->sdef & ENUM_VALUES_DEFINED)) { + prec->val = *value; + ret = 2; + } + traceReadPrint(pdbc, pcon, ret, !!value, prec->rval, "RVAL"); + manageStateAndBiniProcessing(pcon); + } + if (nextReason) + pcon->requestRecordProcessing(nextReason); + } CATCH() + return ret; +} template long @@ -714,8 +790,13 @@ opcua_write_enum (REC *prec) if (pcon->state() == ConnectionStatus::down) { (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); } else if (pcon->state() != ConnectionStatus::initialRead) { - traceWritePrint(pdbc, pcon, prec->rval, "RVAL"); - pcon->writeScalar(prec->rval); + if (prec->sdef & ENUM_VALUES_DEFINED) { + traceWritePrint(pdbc, pcon, prec->rval, "RVAL"); + pcon->writeScalar(prec->rval); + } else { + traceWritePrint(pdbc, pcon, prec->val, "VAL"); + pcon->writeScalar(prec->val); + } if (pcon->plinkinfo->linkedToItem && (pcon->state() == ConnectionStatus::up || pcon->state() == ConnectionStatus::initialWrite)) { @@ -727,13 +808,15 @@ opcua_write_enum (REC *prec) epicsUInt32 *rval = useReadValue(pcon) ? &prec->rval : nullptr; ret = pcon->readScalar(rval, &nextReason); if (ret == 0 && rval) { + recGblResetAlarms(prec); // get mbbo out of SOFT alarm after val was out of range + updateEnumInfos (prec, pcon); *rval &= prec->mask; if (prec->shft) *rval >>= prec->shft; - if (prec->sdef) { + if (prec->sdef & ENUM_VALUES_DEFINED) { epicsUInt32 *pstate_values = &prec->zrvl; prec->val = 65535; // unknown state - for (epicsUInt16 i = 0; i < 16; i++) { + for (epicsUInt16 i = 0; i < NUMBER_OF_ENUM_CHOICES; i++) { if (*pstate_values == *rval) { prec->val = i; break; @@ -741,8 +824,8 @@ opcua_write_enum (REC *prec) pstate_values++; } } else { - // no defined states prec->val = static_cast(*rval); + ret = 2; } } traceReadPrint(pdbc, pcon, ret, !!rval, prec->val, prec->rval); @@ -1007,6 +1090,11 @@ opcua_read_array (REC *prec) &prec->nord, &nextReason); break; case menuFtypeCHAR: + if (pcon->pdataelement->enumChoices) { + ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason); + if (bptr) prec->nord = static_cast(strlen(static_cast(bptr))); + break; + } ret = pcon->readArray(static_cast(bptr), prec->nelm, &prec->nord, &nextReason); break; @@ -1054,7 +1142,7 @@ opcua_read_array (REC *prec) break; } if (nord != prec->nord) - db_post_events(prec, &prec->nord, DBE_VALUE | DBE_LOG); + db_post_events(prec, &prec->nord, DBE_VALUE | DBE_LOG); traceReadArrayPrint(pdbc, pcon, ret, !!bptr, prec->nord); manageStateAndBiniProcessing(pcon); } @@ -1067,7 +1155,7 @@ opcua_read_array (REC *prec) template long opcua_write_array (REC *prec) -{ +{ long ret = 0; TRY { Guard G(pcon->lock); @@ -1084,6 +1172,10 @@ opcua_write_array (REC *prec) ret = pcon->writeArray(static_cast(prec->bptr), MAX_STRING_SIZE, prec->nord); break; case menuFtypeCHAR: + if (pcon->pdataelement->enumChoices) { + ret = pcon->writeScalar(static_cast(prec->bptr), prec->nord); + break; + } ret = pcon->writeArray(static_cast(prec->bptr), prec->nord); break; case menuFtypeUCHAR: @@ -1134,6 +1226,11 @@ opcua_write_array (REC *prec) &prec->nord, &nextReason); break; case menuFtypeCHAR: + if (pcon->pdataelement->enumChoices) { + ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason); + if (bptr) prec->nord = static_cast(strlen(static_cast(bptr))); + break; + } ret = pcon->readArray(static_cast(bptr), prec->nelm, &prec->nord, &nextReason); break; @@ -1271,7 +1368,7 @@ SUP (devLiOpcua, longin, int32_val, read) SUP (devLoOpcua, longout, int32_val, write) SUP (devBiOpcua, bi, uint32_rval, read) SUP (devBoOpcua, bo, bo, write) -SUPM(devMbbiOpcua, mbbi, uint32_rval, read) +SUPM(devMbbiOpcua, mbbi, enum, read) SUPM(devMbboOpcua, mbbo, enum, write) SUPM(devMbbiDirectOpcua, mbbiDirect, uint32_rval, read) SUPM(devMbboDirectOpcua, mbboDirect, mbbod, write) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index ee3f709c..71c800cf 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -195,6 +195,9 @@ DataElementOpen62541::createMap (const UA_DataType *type, &pelem->offset, &pelem->memberType, &pelem->isArray)) { + if (typeKindOf(pelem->memberType) == UA_DATATYPEKIND_ENUM) { + pelem->enumChoices = pitem->session->getEnumChoices(&pelem->memberType->typeId); + } } else { std::cerr << "Item " << pitem << ": element " << pelem->name @@ -230,6 +233,9 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, UA_Variant_copy(&value, &incomingData); if (isLeaf()) { + if (pconnector->state() == ConnectionStatus::initialRead && typeKindOf(value) == UA_DATATYPEKIND_ENUM) { + enumChoices = pitem->session->getEnumChoices(&value.type->typeId); + } if ((pconnector->state() == ConnectionStatus::initialRead && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) || (pconnector->state() == ConnectionStatus::up)) { @@ -313,6 +319,8 @@ DataElementOpen62541::setIncomingEvent (ProcessReason reason) { if (isLeaf()) { Guard(pconnector->lock); + if (reason == ProcessReason::connectionLoss && enumChoices) + enumChoices = nullptr; bool wasFirst = false; // Put the event on the queue UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason)); @@ -360,7 +368,9 @@ DataElementOpen62541::updateDataInStruct (void* container, void* memberData = static_cast(container) + pelem->offset; const UA_Variant& data = pelem->getOutgoingData(); const UA_DataType* memberType = pelem->memberType; - assert(memberType == data.type); + assert(memberType == data.type || + (typeKindOf(memberType) == UA_DATATYPEKIND_ENUM && + typeKindOf(data.type) == UA_DATATYPEKIND_INT32)); if (pelem->isArray) { size_t& arrayLength = *reinterpret_cast(memberData); @@ -591,6 +601,18 @@ DataElementOpen62541::readScalar (char *value, const size_t num, UA_print(&dt, data.type, &buffer); break; } + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_INT32: + { + if (enumChoices) { + auto it = enumChoices->find(*static_cast(data.data)); + if (it != enumChoices->end()) { + buffer = UA_String_fromChars(it->second.c_str()); + break; + } + } + // no enum or index not found: fall through + } default: if (data.type) UA_print(data.data, data.type, &buffer); @@ -993,11 +1015,12 @@ DataElementOpen62541::writeScalar (const epicsFloat64 &value, dbCommon *prec) long DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbCommon *prec) { - long ret = 0; + long ret = 1; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; long l; unsigned long ul; double d; + char* end = nullptr; { // Scope of Guard G Guard G(outgoingLock); @@ -1010,6 +1033,7 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC val.data = const_cast(reinterpret_cast(value)); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); markAsDirty(); + ret = 0; break; } case UA_DATATYPEKIND_LOCALIZEDTEXT: @@ -1020,6 +1044,7 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC val.text.data = const_cast(reinterpret_cast(value)); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); markAsDirty(); + ret = 0; break; } case UA_DATATYPEKIND_BOOLEAN: @@ -1027,128 +1052,140 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC UA_Boolean val = strchr("YyTt1", *value) != NULL; status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); markAsDirty(); + ret = 0; break; } case UA_DATATYPEKIND_BYTE: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { UA_Byte val = static_cast(ul); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_SBYTE: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { UA_SByte val = static_cast(l); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_UINT16: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { UA_UInt16 val = static_cast(ul); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_INT16: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { UA_Int16 val = static_cast(l); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_UINT32: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { UA_UInt32 val = static_cast(ul); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; + case UA_DATATYPEKIND_ENUM: case UA_DATATYPEKIND_INT32: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (enumChoices) { + // first test enum strings then numeric values + // in case a string starts with a number but means a different value + for (auto it: *enumChoices) + if (it.second == value) { + l = static_cast(it.first); + ret = 0; + end = nullptr; + break; + } + if (ret != 0 && end != value) + for (auto it: *enumChoices) + if (l == it.first) { + ret = 0; + break; + } + if (ret != 0) + break; + } + if (end != value && isWithinRange(l)) { UA_Int32 val = static_cast(l); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_UINT64: - ul = strtoul(value, nullptr, 0); - if (isWithinRange(ul)) { + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { UA_UInt64 val = static_cast(ul); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_INT64: - l = strtol(value, nullptr, 0); - if (isWithinRange(l)) { + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { UA_Int64 val = static_cast(l); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_FLOAT: - d = strtod(value, nullptr); - if (isWithinRange(d)) { + d = strtod(value, &end); + if (end != value && isWithinRange(d)) { UA_Float val = static_cast(d); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_DOUBLE: { - d = strtod(value, nullptr); - UA_Double val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); + d = strtod(value, &end); + if (end != value) { + UA_Double val = static_cast(d); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); + markAsDirty(); + ret = 0; + } break; } default: - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); + errlogPrintf("%s : unsupported conversion from string to %s for outgoing data\n", + prec->name, variantTypeString(incomingData)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } } + if (ret != 0) { + errlogPrintf("%s : value \"%s\" out of range\n", + prec->name, value); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } if (ret == 0 && UA_STATUS_IS_BAD(status)) { errlogPrintf("%s : scalar copy failed: %s\n", prec->name, UA_StatusCode_name(status)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; } - dbgWriteScalar(); + if (ret == 0) + dbgWriteScalar(); return ret; } diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index 3d5d7b91..a67cb78e 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -788,7 +788,7 @@ class DataElementOpen62541 : public DataElement char *statusText, const epicsUInt32 statusTextLen) { - long ret = 0; + long ret = 1; if (incomingQueue.empty()) { errlogPrintf("%s: incoming data queue empty\n", prec->name); @@ -804,11 +804,9 @@ class DataElementOpen62541 : public DataElement switch (upd->getType()) { case ProcessReason::readFailure: (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; break; case ProcessReason::connectionLoss: (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; break; case ProcessReason::incomingData: case ProcessReason::readComplete: @@ -818,90 +816,99 @@ class DataElementOpen62541 : public DataElement if (UA_STATUS_IS_BAD(stat)) { // No valid OPC UA value (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; } else { // Valid OPC UA value, so try to convert UA_Variant &data = upd->getData(); switch(typeKindOf(data)) { case UA_DATATYPEKIND_BOOLEAN: *value = (*static_cast(data.data) != 0); + ret = 0; break; case UA_DATATYPEKIND_BYTE: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = *static_cast(data.data); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_SBYTE: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = *static_cast(data.data); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_INT16: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_UINT16: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_INT32: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_UINT32: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_INT64: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_UINT64: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_FLOAT: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } break; case UA_DATATYPEKIND_DOUBLE: - if (isWithinRange(*static_cast(data.data))) + if (isWithinRange(*static_cast(data.data))) { *value = static_cast(*static_cast(data.data)); - else - ret = 1; + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + if (!enumChoices || enumChoices->find(static_cast(*value)) != enumChoices->end()) + ret = 0; + } break; case UA_DATATYPEKIND_STRING: { UA_String* s = static_cast(data.data); - if (!string_to(std::string(reinterpret_cast(s->data), s->length), *value)) - ret = 1; + if (string_to(std::string(reinterpret_cast(s->data), s->length), *value)) + ret = 0; break; } case UA_DATATYPEKIND_LOCALIZEDTEXT: { UA_LocalizedText* lt = static_cast(data.data); - if (!string_to(std::string(reinterpret_cast(lt->text.data), lt->text.length), *value)) - ret = 1; + if (string_to(std::string(reinterpret_cast(lt->text.data), lt->text.length), *value)) + ret = 0; break; } default: - ret = 1; + errlogPrintf("%s : unsupported type kind %s for incoming data\n", + prec->name, typeKindName(typeKindOf(data))); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); } if (ret == 1) { UA_String datastring = UA_STRING_NULL; @@ -1042,7 +1049,7 @@ class DataElementOpen62541 : public DataElement writeScalar (const ET &value, dbCommon *prec) { - long ret = 0; + long ret = 1; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; { // Scope of Guard G @@ -1054,6 +1061,7 @@ class DataElementOpen62541 : public DataElement UA_Boolean val = (value != 0); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); markAsDirty(); + ret = 0; break; } case UA_DATATYPEKIND_BYTE: @@ -1061,9 +1069,7 @@ class DataElementOpen62541 : public DataElement UA_Byte val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_SBYTE: @@ -1071,9 +1077,7 @@ class DataElementOpen62541 : public DataElement UA_SByte val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_UINT16: @@ -1081,9 +1085,7 @@ class DataElementOpen62541 : public DataElement UA_UInt16 val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_INT16: @@ -1091,9 +1093,7 @@ class DataElementOpen62541 : public DataElement UA_Int16 val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_UINT32: @@ -1101,19 +1101,18 @@ class DataElementOpen62541 : public DataElement UA_UInt32 val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; + case UA_DATATYPEKIND_ENUM: case UA_DATATYPEKIND_INT32: - if (isWithinRange(value)) { + if (isWithinRange(value) && + (!enumChoices || + enumChoices->find(static_cast(value)) != enumChoices->end())) { UA_Int32 val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_UINT64: @@ -1121,9 +1120,7 @@ class DataElementOpen62541 : public DataElement UA_UInt64 val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_INT64: @@ -1131,9 +1128,7 @@ class DataElementOpen62541 : public DataElement UA_Int64 val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_FLOAT: @@ -1141,9 +1136,7 @@ class DataElementOpen62541 : public DataElement UA_Float val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_DOUBLE: @@ -1151,9 +1144,7 @@ class DataElementOpen62541 : public DataElement UA_Double val = static_cast(value); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + ret = 0; } break; case UA_DATATYPEKIND_STRING: @@ -1164,6 +1155,7 @@ class DataElementOpen62541 : public DataElement val.data = const_cast(reinterpret_cast(strval.c_str())); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); markAsDirty(); + ret = 0; break; } case UA_DATATYPEKIND_LOCALIZEDTEXT: @@ -1175,15 +1167,20 @@ class DataElementOpen62541 : public DataElement val.text.data = const_cast(reinterpret_cast(strval.c_str())); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); markAsDirty(); + ret = 0; break; } default: - ret = 1; - errlogPrintf("%s : unsupported conversion for outgoing data\n", - prec->name); + errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", + prec->name, epicsTypeString(value), variantTypeString(incomingData)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } } + if (ret != 0) { + errlogPrintf("%s : value out of range\n", + prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } if (ret == 0 && UA_STATUS_IS_BAD(status)) { errlogPrintf("%s : scalar copy failed: %s\n", prec->name, UA_StatusCode_name(status)); @@ -1191,7 +1188,8 @@ class DataElementOpen62541 : public DataElement ret = 1; } - dbgWriteScalar(); + if (ret == 0) + dbgWriteScalar(); return ret; } diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index 2f6010a5..7bc463b1 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.cpp +++ b/devOpcuaSup/open62541/SessionOpen62541.cpp @@ -107,7 +107,7 @@ operator << (std::ostream& os, const UA_Variant &ua_variant) if (ua_variant.type == nullptr) return os << "NO_TYPE"; UA_String s = UA_STRING_NULL; if (UA_Variant_isScalar(&ua_variant)) { - if (ua_variant.type == &UA_TYPES[UA_TYPES_DATETIME]) { + if (ua_variant.type->typeKind == UA_DATATYPEKIND_DATETIME) { // UA_print does not correct printed time for time zone UA_Int64 tOffset = UA_DateTime_localTimeUtcOffset(); UA_DateTime dt = *static_cast(ua_variant.data); @@ -116,7 +116,16 @@ operator << (std::ostream& os, const UA_Variant &ua_variant) } else { UA_print(ua_variant.data, ua_variant.type, &s); } - os << s << " (" << ua_variant.type->typeName << ')'; + os << s << " ("; + switch (ua_variant.type->typeKind) { + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + os << typeKindName(ua_variant.type->typeKind) << ' '; + break; + } + os << ua_variant.type->typeName << ')'; } else { os << s << '{'; char* data = static_cast(ua_variant.data); @@ -127,7 +136,16 @@ operator << (std::ostream& os, const UA_Variant &ua_variant) os << s; UA_String_clear(&s); } - os << s << "} (" << ua_variant.type->typeName + os << s << "} ("; + switch (ua_variant.type->typeKind) { + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + os << typeKindName(ua_variant.type->typeKind) << ' '; + break; + } + os << ua_variant.type->typeName << '[' << ua_variant.arrayLength << "])"; } UA_String_clear(&s); @@ -220,15 +238,16 @@ operator != (const std::string& str, const UA_String& ua_string) return !(str == ua_string); } -inline std::string& -operator += (std::string& str, const UA_String& ua_string) +inline std::string +to_string (const UA_String& ua_string) { - return str.append(reinterpret_cast(ua_string.data), ua_string.length); + return std::string(reinterpret_cast(ua_string.data), ua_string.length); } -const char* typeKindName(UA_UInt32 typeKind) +const char* typeKindName(int typeKind) { static const char* typeKindNames[] = { + "None", "Boolean", "SByte", "Byte", @@ -262,8 +281,8 @@ const char* typeKindName(UA_UInt32 typeKind) "BitfieldCluster", "???" }; - if (typeKind > 31) typeKind = 31; - return typeKindNames[typeKind]; + if (typeKind < -1 || typeKind > 31) typeKind = 31; + return typeKindNames[typeKind+1]; } @@ -639,16 +658,20 @@ SessionOpen62541::processRequests (std::vector> &ba UA_ReadRequest_init(&request); request.maxAge = 0; request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH; - request.nodesToReadSize = batch.size(); - request.nodesToRead = static_cast(UA_Array_new(batch.size(), &UA_TYPES[UA_TYPES_READVALUEID])); + request.nodesToRead = static_cast( + UA_Array_new(batch.size() * no_of_properties_read, &UA_TYPES[UA_TYPES_READVALUEID])); UA_UInt32 i = 0; for (auto c : batch) { + UA_NodeId_copy(&c->item->getNodeId(), &request.nodesToRead[i].nodeId); + request.nodesToRead[i].attributeId = UA_ATTRIBUTEID_DATATYPE; + i++; UA_NodeId_copy(&c->item->getNodeId(), &request.nodesToRead[i].nodeId); request.nodesToRead[i].attributeId = UA_ATTRIBUTEID_VALUE; itemsToRead->push_back(c->item); i++; } + request.nodesToReadSize = i; { Guard G(clientlock); @@ -864,8 +887,7 @@ SessionOpen62541::updateNamespaceMap(const UA_String *nsArray, UA_UInt16 nsCount if (namespaceMap.size()) { nsIndexMap.clear(); for (UA_UInt16 i = 0; i < nsCount; i++) { - std::string ns; - ns += nsArray[i]; + std::string ns = to_string(nsArray[i]); auto it = namespaceMap.find(ns); if (it != namespaceMap.end()) nsIndexMap.insert({it->second, i}); @@ -945,8 +967,7 @@ SessionOpen62541::showSecurity () } for (size_t k = 0; k < endpointDescriptionsLength; k++) { - if (std::string(reinterpret_cast(endpointDescriptions[k].endpointUrl.data), - endpointDescriptions[k].endpointUrl.length).compare(0, 7, "opc.tcp") == 0) { + if (to_string(endpointDescriptions[k].endpointUrl).compare(0, 7, "opc.tcp") == 0) { char dash = '-'; std::string marker; if (isConnected() @@ -1042,8 +1063,7 @@ SessionOpen62541::setupSecurity () int selectedSecurityLevel = -1; int selectedEndpoint = -1; for (size_t k = 0; k < endpointDescriptionsLength; k++) { - if (std::string(reinterpret_cast(endpointDescriptions[k].endpointUrl.data), - endpointDescriptions[k].endpointUrl.length).compare(0, 7, "opc.tcp") == 0) { + if (to_string(endpointDescriptions[k].endpointUrl).compare(0, 7, "opc.tcp") == 0) { if (reqSecurityMode == RequestedSecurityMode::Best || OpcUaSecurityMode(reqSecurityMode) == endpointDescriptions[k].securityMode) { if (reqSecurityPolicyUri.find("#None") != std::string::npos || @@ -1526,6 +1546,18 @@ SessionOpen62541::getTypeIndexByName(UA_UInt16 nsIndex, const char* typeName) return UnknownType; } +// Wrapper to use lambda with capture in C callback +struct LambdaHolder { + std::function func; +}; + +static UA_StatusCode iteratorCallbackAdapter(UA_NodeId childId, UA_Boolean isInverse, + UA_NodeId referenceTypeId, void *handle) +{ + LambdaHolder *holder = static_cast(handle); + return holder->func(childId, isInverse, referenceTypeId, handle); +} + void SessionOpen62541::readCustomTypeDictionaries() { @@ -1533,6 +1565,16 @@ SessionOpen62541::readCustomTypeDictionaries() UA_ClientConfig *config = UA_Client_getConfig(client); if (debug) std::cout << "Session " << name + << ": reading Enums" + << std::endl; + UA_Client_forEachChildNodeCall(client, UA_NODEID_NUMERIC(0, UA_NS0ID_ENUMERATION), + [] (UA_NodeId childNodeId, UA_Boolean isInverse, UA_NodeId referenceTypeId, void *handle) + { + return static_cast(handle)-> + enumIteratorCallback(childNodeId, referenceTypeId); + }, this); + if (debug) + std::cout << "\nSession " << name << ": reading type dictionaries" << std::endl; UA_Client_forEachChildNodeCall(client, UA_NODEID_NUMERIC(0, UA_NS0ID_OPCBINARYSCHEMA_TYPESYSTEM), @@ -1636,31 +1678,124 @@ SessionOpen62541::clearCustomTypeDictionaries() enumTypes.clear(); } +UA_StatusCode +SessionOpen62541::enumIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId) +{ + UA_StatusCode status = UA_STATUSCODE_GOOD; + UA_QualifiedName typeName; + UA_QualifiedName_init(&typeName); + UA_Client_readBrowseNameAttribute(client, childId, &typeName); + UA_NodeId binaryEncodingId = UA_NODEID_NULL; + UA_NodeId_copy(&childId, &binaryEncodingId); + if (debug >= 4) + std::cout << "\nEnum " << typeName + << " { # binaryEncodingId: " << binaryEncodingId + << std::endl; + EnumChoices enumChoices; + LambdaHolder holder; + holder.func = [&enumChoices, this] (UA_NodeId childNodeId, UA_Boolean isInverse, UA_NodeId referenceTypeId, void *handle) + { + return this->enumChoiceIteratorCallback(childNodeId, referenceTypeId, enumChoices); + }; + status = UA_Client_forEachChildNodeCall(client, childId, iteratorCallbackAdapter, &holder); + if (debug >= 4) + std::cout << "};" << std::endl; + if (enumChoices.size() > 0) { + if (debug >= 5) + std::cout << "# adding enum " << typeName << " to known types" << std::endl; + enumTypes.emplace(binaryEncodingId, std::move(enumChoices)); + binaryTypeIds.emplace(to_string(typeName.name), binaryEncodingId); + + UA_DataType customDataType = {}; + customDataType.memSize = sizeof(UA_UInt32); + customDataType.typeKind = UA_DATATYPEKIND_ENUM; + customDataType.pointerFree = true; + customDataType.overlayable = UA_BINARY_OVERLAYABLE_INTEGER; +#ifndef UA_DATATYPES_USE_POINTER + customDataType.typeIndex = static_cast(customTypes.size()); +#endif + UA_NodeId_copy(&binaryEncodingId, &customDataType.binaryEncodingId); + UA_NodeId_copy(&binaryEncodingId, &customDataType.typeId); + char* ctypeName = static_cast(calloc(typeName.name.length+1, 1)); + if (!customDataType.typeName) { + status = UA_STATUSCODE_BADOUTOFMEMORY; + } else { + strncpy(ctypeName, reinterpret_cast(typeName.name.data), typeName.name.length); + customDataType.typeName = ctypeName; + customTypes.push_back(customDataType); + } + } + + UA_QualifiedName_clear(&typeName); + return status; +} + +UA_StatusCode +SessionOpen62541::enumChoiceIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId, EnumChoices& enumChoices) +{ + UA_StatusCode status = UA_STATUSCODE_GOOD; + const UA_NodeId hasProperty = UA_NODEID_NUMERIC(0, UA_NS0ID_HASPROPERTY); + + if (UA_NodeId_equal(&referenceTypeId, &hasProperty)) + { + UA_Variant value; + UA_Variant_init(&value); + UA_Client_readValueAttribute(client, childId, &value); + if (UA_Variant_hasArrayType(&value, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT])) + { + UA_LocalizedText* choices = static_cast(value.data); + for (size_t i = 0; i < value.arrayLength; i++) { + if (debug >= 4) + std::cout << " " << i << " = " << choices[i].text << ';' << std::endl; + enumChoices.emplace(static_cast(i), to_string(choices[i].text)); + } + } + else if (UA_Variant_hasArrayType(&value, &UA_TYPES[UA_TYPES_EXTENSIONOBJECT])) + { + UA_ExtensionObject* choices = static_cast(value.data); + for (size_t i = 0; i < value.arrayLength; i++) { + if (choices[i].encoding == UA_EXTENSIONOBJECT_DECODED && + choices[i].content.decoded.type == &UA_TYPES[UA_TYPES_ENUMVALUETYPE]) + { + UA_EnumValueType *enumValue = static_cast(choices[i].content.decoded.data); + if (debug >= 4) + std::cout << " " << enumValue->value << " = " << enumValue->displayName.text << ';' << std::endl; + enumChoices.emplace(static_cast(enumValue->value), + to_string(enumValue->displayName.text)); + } + } + } + UA_Variant_clear(&value); + } + return status; +} + UA_StatusCode SessionOpen62541::typeSystemIteratorCallback(const UA_NodeId& dictNodeId) { UA_QualifiedName dictName; UA_QualifiedName_init(&dictName); + if (debug) { + // Show the dict name in debug messages + UA_Client_readBrowseNameAttribute(client, dictNodeId, &dictName); + } if (dictNodeId.namespaceIndex == 0) { // custom type dictionaries only if (debug) { - UA_Client_readBrowseNameAttribute(client, dictNodeId, &dictName); std::cout << "Session " << name - << ": ignoring types of system dict " << dictNodeId - << " " << dictName - << std::endl; + << ": ignoring system dict " << dictName + << std::endl; + UA_QualifiedName_clear(&dictName); } return UA_STATUSCODE_GOOD; } if (debug) { - UA_Client_readBrowseNameAttribute(client, dictNodeId, &dictName); std::cout << "Session " << name - << ": browsing types of custom dict " << dictNodeId - << " " << dictName + << ": browsing custom dict " << dictName + << " for binary encoding IDs" << std::endl; } - UA_QualifiedName_clear(&dictName); // Browse the dictionary for binaryTypeIds UA_Client_forEachChildNodeCall(client, dictNodeId, @@ -1676,6 +1811,11 @@ SessionOpen62541::typeSystemIteratorCallback(const UA_NodeId& dictNodeId) UA_Client_readValueAttribute(client, dictNodeId, &xmldata); if (UA_Variant_hasScalarType(&xmldata, &UA_TYPES[UA_TYPES_BYTESTRING])) { UA_ByteString* xmlstring = static_cast(xmldata.data); + if (debug >= 5) + std::cout << "\nSession " << name + << ": Data type XML of dict " << dictName + << '\n' << *xmlstring + << std::endl; xmlDocPtr xmldoc = xmlReadMemory(reinterpret_cast(xmlstring->data), static_cast(xmlstring->length), NULL, NULL, 0); if (xmldoc) { @@ -1684,21 +1824,10 @@ SessionOpen62541::typeSystemIteratorCallback(const UA_NodeId& dictNodeId) } } UA_Variant_clear(&xmldata); + UA_QualifiedName_clear(&dictName); return UA_STATUSCODE_GOOD; } -// Wrapper to use lambda with capture in C callback -struct LambdaHolder { - std::function func; -}; - -static UA_StatusCode iteratorCallbackAdapter(UA_NodeId childId, UA_Boolean isInverse, - UA_NodeId referenceTypeId, void *handle) -{ - LambdaHolder *holder = static_cast(handle); - return holder->func(childId, isInverse, referenceTypeId, handle); -} - UA_StatusCode SessionOpen62541::dictIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId) { @@ -1710,6 +1839,10 @@ SessionOpen62541::dictIteratorCallback(const UA_NodeId& childId, const UA_NodeId UA_QualifiedName typeName; UA_QualifiedName_init(&typeName); UA_Client_readBrowseNameAttribute(client, childId, &typeName); + if (debug >= 5) + std::cout << "Session " << name + << ": type " << childId << " = " << typeName + << std::endl; LambdaHolder holder; holder.func = [&typeName, this] (UA_NodeId childNodeId, UA_Boolean isInverse, UA_NodeId referenceTypeId, void *handle) { @@ -1757,9 +1890,7 @@ SessionOpen62541::typeIteratorCallback(const UA_NodeId& childId, const UA_NodeId // Need copy because content of non-numeric (e.g. string) childId is freed after this function returns UA_NodeId binaryEncodingId = UA_NODEID_NULL; UA_NodeId_copy(&childId, &binaryEncodingId); - binaryTypeIds.emplace( - std::string(reinterpret_cast(typeName.name.data), typeName.name.length), - binaryEncodingId); + binaryTypeIds.emplace(to_string(typeName.name), binaryEncodingId); } return UA_STATUSCODE_GOOD; } @@ -1810,20 +1941,20 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) break; } + auto binaryType = binaryTypeIds.find(typeName); + if (binaryType == binaryTypeIds.end()) { + if (debug) + std::cerr << "Session " << name + << ": Ignoring type " << typeName + << " which has no binaryEncodingId" << std::endl; + continue; + } + UA_NodeId_copy(&binaryType->second, &customDataType.binaryEncodingId); + if (strcmp(nodeKind, "StructuredType") == 0) { UA_UInt32 structureAlignment = 0; UA_UInt32 memberSize; - auto binaryType = binaryTypeIds.find(typeName); - if (binaryType == binaryTypeIds.end()) { - if (debug) - std::cerr << "Session " << name - << ": Ignoring type " << typeName - << " which has no binaryEncodingId" << std::endl; - continue; - } - UA_NodeId_copy(&binaryType->second, &customDataType.binaryEncodingId); - const char* baseType = getProp(node, "BaseType"); if (baseType && strcmp(baseType, "ua:Union") == 0) customDataType.typeKind = UA_DATATYPEKIND_UNION; @@ -2133,9 +2264,10 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) customDataType.overlayable = UA_BINARY_OVERLAYABLE_INTEGER; if (debug >= 4) - std::cout << "\nenum " << typeName << " {" + std::cout << "\nEnum " << typeName + << " { # binaryEncodingId: " << customDataType.binaryEncodingId << std::endl; - std::vector> choices; + EnumChoices enumChoices; for (xmlNode* choice = node->children; choice; choice = choice->next) { if (choice->type != XML_ELEMENT_NODE) continue; @@ -2154,10 +2286,10 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) const char* choiceName = getProp(choice, "Name"); const char* choiceValue = getProp(choice, "Value"); if (debug >= 4) - std::cout << " " << choiceName << " = " << choiceValue << ';' << std::endl; - choices.emplace_back(static_cast(atoll(choiceValue)), choiceName); + std::cout << " " << choiceValue << " = " << choiceName << ';' << std::endl; + enumChoices.emplace(atol(choiceValue), choiceName); } - enumTypes.emplace(typeName, std::move(choices)); + enumTypes.emplace(customDataType.binaryEncodingId, std::move(enumChoices)); if (debug >= 4) std::cout << "};" << std::endl; } @@ -2166,13 +2298,29 @@ SessionOpen62541::parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex) if (typeName) { if (debug >= 5) std::cout << "# adding type " << typeName << " to known types" << std::endl; - customDataType.typeId = UA_NODEID_STRING(nsIndex, const_cast(typeName)); + UA_NodeId_copy(&customDataType.binaryEncodingId, &customDataType.typeId); customDataType.typeName = strdup(typeName); customTypes.push_back(customDataType); } } } +const EnumChoices* +SessionOpen62541::getEnumChoices(const UA_NodeId* typeId) +{ + Guard G(clientlock); + if (client && typeId) { + const UA_DataType* type = UA_Client_findDataType(client, typeId); + if (type && type->typeKind == UA_DATATYPEKIND_ENUM) + { + auto enumChoices = enumTypes.find(*typeId); + if (enumChoices != enumTypes.end()) + return &enumChoices->second; + } + } + return nullptr; +} + void SessionOpen62541::showCustomDataTypes(int level) const { @@ -2184,12 +2332,10 @@ SessionOpen62541::showCustomDataTypes(int level) const std::cout << " {"; if (type.typeKind == UA_DATATYPEKIND_ENUM) { - for (auto choice: enumTypes.find(type.typeName)->second) - { - std::cout << "\n " << choice.second << " = " << choice.first; - } + for (auto it: enumTypes.at(type.typeId)) + std::cout << "\n " << it.first << " = " << it.second; } else - for (size_t i = 0; i < type.membersSize; i++) { + for (UA_UInt32 i = 0; i < type.membersSize; i++) { UA_DataTypeMember member = type.members[i]; const UA_DataType& memberType = #ifdef UA_DATATYPES_USE_POINTER @@ -2397,7 +2543,7 @@ SessionOpen62541::readComplete (UA_UInt32 transactionId, << " (transaction id " << transactionId << "; data for " << response->resultsSize << " items)" << std::endl; - if ((*it->second).size() != response->resultsSize) + if ((*it->second).size() * no_of_properties_read != response->resultsSize) errlogPrintf("OPC UA session %s: (readComplete) received a callback " "with %llu values for a request containing %llu items\n", name.c_str(), @@ -2408,11 +2554,21 @@ SessionOpen62541::readComplete (UA_UInt32 transactionId, if (i >= response->resultsSize) { item->setIncomingEvent(ProcessReason::readFailure); } else { + const UA_DataType* type = nullptr; + if (!UA_STATUS_IS_BAD(response->results[i].status)) { + type = UA_Client_findDataType(client, static_cast(response->results[i].value.data)); + } + i++; + if (typeKindOf(type) == UA_DATATYPEKIND_ENUM && + typeKindOf(response->results[i].value.type) == UA_DATATYPEKIND_INT32) { + // Enums arrive as INT32. Tweak the type to what we find in structs for better diagnosics. + response->results[i].value.type = type; + } if (debug >= 5) { std::cout << "** Session " << name << ": (readComplete) getting data for item " << item - << " = " << response->results[i].value + << "\" = " << response->results[i].value << ' ' << UA_StatusCode_name(response->results[i].status) << std::endl; } @@ -2420,8 +2576,8 @@ SessionOpen62541::readComplete (UA_UInt32 transactionId, if (UA_STATUS_IS_BAD(response->results[i].status)) reason = ProcessReason::readFailure; item->setIncomingData(response->results[i], reason); + i++; } - i++; } outstandingOps.erase(it); } else { diff --git a/devOpcuaSup/open62541/SessionOpen62541.h b/devOpcuaSup/open62541/SessionOpen62541.h index 868a6bf0..cfa65b0b 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.h +++ b/devOpcuaSup/open62541/SessionOpen62541.h @@ -23,6 +23,8 @@ #include #include +#include +#include #ifndef UA_BUILTIN_TYPES_COUNT // Newer open62541 since version 1.3 uses type pointer @@ -39,8 +41,28 @@ #include #include #include +#include #include +/* Allow UA_NodeId as key */ +template<> +struct std::hash +{ + inline std::size_t operator()(const UA_NodeId& nodeId) const noexcept + { + return UA_NodeId_hash(&nodeId); + } +}; + +template<> +struct std::equal_to +{ + inline bool operator()(const UA_NodeId& nodeId1, const UA_NodeId& nodeId2) const noexcept + { + return UA_NodeId_equal(&nodeId1, &nodeId2); + } +}; + namespace DevOpcua { class SubscriptionOpen62541; @@ -78,7 +100,7 @@ std::ostream& operator << (std::ostream& os, const UA_NodeId& ua_nodeId); std::ostream& operator << (std::ostream& os, const UA_Variant &ua_variant); -const char* typeKindName(UA_UInt32 typeKind); +const char* typeKindName(int typeKind); // Open62541 has no ClientSecurityInfo structure // Make our own for convenience @@ -178,12 +200,9 @@ class SessionOpen62541 virtual const std::string & getName() const override; /** - * @brief Get a structure definition from the session dictionary. - * @param dataTypeId data type of the extension object - * @return structure definition + * @brief Get pointer to enumChoices if typeId refers to enum type, else nullptr */ -// UA_StructureDefinition structureDefinition(const UaNodeId &dataTypeId) -// { return puasession->structureDefinition(dataTypeId); } + const EnumChoices* getEnumChoices(const UA_NodeId* typeId); /** * @brief Request a beginRead service for an item @@ -419,12 +438,14 @@ class SessionOpen62541 /** open62541 type dictionary handling */ std::vector customTypes; /**< descriptions of custom (non-standard) OPC-UA types */ std::map binaryTypeIds; /**< server defined binary ids of custom types */ - std::map>> enumTypes; + std::unordered_map enumTypes; /**< all the enum definitions from the server */ void readCustomTypeDictionaries(); /**< read custom types from the server */ void clearCustomTypeDictionaries(); /**< clear old custom types */ void parseCustomDataTypes(xmlNode* node, UA_UInt16 nsIndex); /**< parse XML representation of custom types */ size_t getTypeIndexByName(UA_UInt16 nsIndex, const char* typeName); UA_StatusCode typeSystemIteratorCallback(const UA_NodeId& dictNodeId); + UA_StatusCode enumIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId); + UA_StatusCode enumChoiceIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId, EnumChoices& enumChoices); UA_StatusCode dictIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId); UA_StatusCode typeIteratorCallback(const UA_NodeId& childId, const UA_NodeId& referenceTypeId, const UA_QualifiedName& typeName); void showCustomDataTypes(int level) const; From 892ea11ef5bea9f6c22a25f4e481eb5804cc1bee Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Fri, 21 Mar 2025 10:51:41 +0100 Subject: [PATCH 15/64] add OptStruct support --- .ci-local/install-open62541.py | 3 - .ci-local/open62541-1.3.patch | 24 -- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 346 +++++++++++------- .../open62541/DataElementOpen62541.cpp | 124 ++++--- devOpcuaSup/open62541/DataElementOpen62541.h | 51 ++- devOpcuaSup/open62541/README.md | 19 - 6 files changed, 329 insertions(+), 238 deletions(-) delete mode 100644 .ci-local/open62541-1.3.patch diff --git a/.ci-local/install-open62541.py b/.ci-local/install-open62541.py index 58510f26..23bf8e2e 100644 --- a/.ci-local/install-open62541.py +++ b/.ci-local/install-open62541.py @@ -86,9 +86,6 @@ if cue.ci['static']: build_shared = 'OFF' - if ver[0] == '1' and ver[1] == '3': - sp.check_call(['patch', '-p1', '-i', os.path.join(curdir, '.ci-local', 'open62541-1.3.patch')], cwd=sdkdir) - sp.check_call(['cmake', '..', '-G', generator, '-DBUILD_SHARED_LIBS={0}'.format(build_shared), diff --git a/.ci-local/open62541-1.3.patch b/.ci-local/open62541-1.3.patch deleted file mode 100644 index 0c24cd7c..00000000 --- a/.ci-local/open62541-1.3.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff -uir open62541.orig/include/open62541/types.h open62541/include/open62541/types.h ---- open62541.orig/include/open62541/types.h 2024-08-09 11:27:52.000000000 +0200 -+++ open62541/include/open62541/types.h 2024-09-03 10:58:38.813382735 +0200 -@@ -1067,7 +1067,7 @@ - * If the member is an array, the offset points to the (size_t) length field. - * (The array pointer comes after the length field without any padding.) */ - #ifdef UA_ENABLE_TYPEDESCRIPTION --UA_Boolean -+UA_Boolean UA_EXPORT - UA_DataType_getStructMember(const UA_DataType *type, - const char *memberName, - size_t *outOffset, -diff -uir open62541.orig/src/ua_types.c open62541/src/ua_types.c ---- open62541.orig/src/ua_types.c 2024-09-03 11:00:06.897318665 +0200 -+++ open62541/src/ua_types.c 2024-09-02 13:27:49.688344016 +0200 -@@ -1882,7 +1882,7 @@ - } - - #ifdef UA_ENABLE_TYPEDESCRIPTION --UA_Boolean -+UA_Boolean UA_EXPORT - UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, - size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray) { diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index 66364cfc..17be203a 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -151,67 +152,91 @@ DataElementUaSdk::setIncomingData(const UaVariant &value, // Try to get the structure definition from the dictionary UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); if (!definition.isNull()) { - if (!definition.isUnion()) { - // ExtensionObject is a structure - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) - extensionObject.changeEncoding(UaExtensionObject::Binary); - UaGenericStructureValue genericValue; - genericValue.setGenericValue(extensionObject, definition); - - if (!mapped) { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - if (timefrom) { - for (int i = 0; i < definition.childrenCount(); i++) { - if (*timefrom == definition.child(i).name().toUtf8()) { - timesrc = i; - pitem->tsData = epicsTimeFromUaVariant(genericValue.value(i)); - } - } - OpcUa_BuiltInType t = genericValue.value(timesrc).type(); - if (timesrc == -1) { - errlogPrintf( - "%s: timestamp element %s not found - using source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str()); - } else if (t != OpcUaType_DateTime) { - errlogPrintf("%s: timestamp element %s has invalid type %s - using " - "source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str(), - variantTypeString(t)); - timesrc = -1; + // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields + if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) + extensionObject.changeEncoding(UaExtensionObject::Binary); + if (!mapped) { + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + if (timefrom) { + for (int i = 0; i < definition.childrenCount(); i++) { + if (*timefrom == definition.child(i).name().toUtf8()) { + timesrc = i; } } - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - delete pelem->enumChoices; - pelem->enumChoices = pitem->session->getEnumChoices(definition.child(i).enumDefinition()); - pelem->setIncomingData(genericValue.value(i), reason); - } + OpcUa_BuiltInType t = definition.child(timesrc).valueType(); + if (timesrc == -1) { + errlogPrintf( + "%s: timestamp element %s not found - using source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str()); + } else if (t != OpcUaType_DateTime) { + errlogPrintf("%s: timestamp element %s has invalid type %s - using " + "source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str(), + variantTypeString(t)); + timesrc = -1; + } + } + for (auto &it : elements) { + auto pelem = it.lock(); + for (int i = 0; i < definition.childrenCount(); i++) { + if (pelem->name == definition.child(i).name().toUtf8()) { + elementMap.insert({i, it}); + delete pelem->enumChoices; + pelem->enumChoices = pitem->session->getEnumChoices(definition.child(i).enumDefinition()); } } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; - } else { - if (timefrom) { - if (timesrc >= 0) - pitem->tsData = epicsTimeFromUaVariant(genericValue.value(timesrc)); - else - pitem->tsData = pitem->tsSource; + } + if (debug() >= 5) + std::cout << " ** " << elementMap.size() << "/" << elements.size() + << " child elements mapped to a " + << "structure of " << definition.childrenCount() << " elements" << std::endl; + mapped = true; + } + + if (timesrc >= 0) + pitem->tsData = epicsTimeFromUaVariant(UaGenericStructureValue(extensionObject, definition).value(timesrc)); + else + pitem->tsData = pitem->tsSource; + + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + OpcUa_StatusCode stat; + if (definition.isUnion()) { + UaGenericUnionValue genericValue(extensionObject, definition); + int index = genericValue.switchValue() - 1; + if (it.first == index) { + pelem->setIncomingData(genericValue.value(), reason); + stat = OpcUa_Good; + } else { + stat = OpcUa_BadNoData; } - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - pelem->setIncomingData(genericValue.value(it.first), reason); + } else { + const UaVariant &memberValue = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); + if (stat == OpcUa_Good) { + pelem->setIncomingData(memberValue, reason); } } + if (stat == OpcUa_BadNoData) { + // Absent optional field or union choice not taken: + // Pass a fake value with correct data type so that + // later writing will not fail with type mismatch. + const UaStructureField &field = definition.child(it.first); + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = field.valueType(); + fakeValue.ArrayType = field.valueRank() != 0; + if (debug()) + std::cerr << pitem->recConnector->getRecordName() + << " element " << pelem->name + << (definition.isUnion() ? " not taken choice " : " absent optional " ) + << variantTypeString((OpcUa_BuiltInType)fakeValue.Datatype) + << (fakeValue.ArrayType ? " array" : " scalar") + << std::endl; + pelem->setIncomingData(fakeValue, ProcessReason::readFailure); + } } } else @@ -270,33 +295,6 @@ DataElementUaSdk::setState(const ConnectionStatus state) } } -// Helper to update one data structure element from pointer to child -bool -DataElementUaSdk::updateDataInGenericValue (UaGenericStructureValue &value, - const int index, - std::shared_ptr pelem) -{ - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - value.setField(index, pelem->getOutgoingData()); - pelem->isdirty = false; - updated = true; - } - } - if (debug() >= 4) { - if (updated) { - std::cout << "Data from child element " << pelem->name - << " inserted into data structure" << std::endl; - } else { - std::cout << "Data from child element " << pelem->name - << " ignored (not dirty)" << std::endl; - } - } - return updated; -} - const UaVariant & DataElementUaSdk::getOutgoingData () { @@ -315,57 +313,87 @@ DataElementUaSdk::getOutgoingData () // Try to get the structure definition from the dictionary UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (!definition.isNull()) { - if (!definition.isUnion()) { - // ExtensionObject is a structure - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - UaGenericStructureValue genericValue; - genericValue.setGenericValue(extensionObject, definition); - - if (!mapped) { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - if (updateDataInGenericValue(genericValue, i, pelem)) - isdirty = true; - } - } - } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; - } else { - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - if (updateDataInGenericValue(genericValue, it.first, pelem)) - isdirty = true; - } - } - if (isdirty) { - if (debug() >= 4) - std::cout << "Encoding changed data structure to outgoingData of element " << name - << std::endl; - genericValue.toExtensionObject(extensionObject); - outgoingData.setExtensionObject(extensionObject, OpcUa_True); - } else { - if (debug() >= 4) - std::cout << "Returning unchanged outgoingData of element " << name - << std::endl; - } - } - - } else + if (definition.isNull()) { errlogPrintf( "Cannot get a structure definition for extensionObject with dataTypeID %s " "/ encodingTypeID %s - check access to type dictionary\n", extensionObject.dataTypeId().toString().toUtf8(), extensionObject.encodingTypeId().toString().toUtf8()); + return outgoingData; + } + + // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields + if (!mapped) { + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + for (auto &it : elements) { + auto pelem = it.lock(); + for (int i = 0; i < definition.childrenCount(); i++) { + if (pelem->name == definition.child(i).name().toUtf8()) { + elementMap.insert({i, it}); + } + } + } + if (debug() >= 5) + std::cout << " ** " << elementMap.size() << "/" << elements.size() + << " child elements mapped to a " + << "structure of " << definition.childrenCount() << " elements" << std::endl; + mapped = true; + } + if (definition.isUnion()) { + UaGenericUnionValue genericUnion(extensionObject, definition); + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { // Scope of Guard G + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + genericUnion.setValue(it.first + 1, pelem->getOutgoingData()); + pelem->isdirty = false; + isdirty = true; + updated = true; + } + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into union" : " ignored (not dirty)") + << std::endl; + } + if (isdirty) + genericUnion.toExtensionObject(extensionObject); + + } else { + UaGenericStructureValue genericStruct(extensionObject, definition); + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { // Scope of Guard G + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + genericStruct.setField(it.first, pelem->getOutgoingData()); + pelem->isdirty = false; + isdirty = true; + updated = true; + } + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into structure" : " ignored (not dirty)") + << std::endl; + } + if (isdirty) + genericStruct.toExtensionObject(extensionObject); + } + if (isdirty) { + if (debug() >= 4) + std::cout << "Encoding changed data structure to outgoingData of element " << name + << std::endl; + outgoingData.setExtensionObject(extensionObject, OpcUa_True); + } else { + if (debug() >= 4) + std::cout << "Returning unchanged outgoingData of element " << name + << std::endl; + } } } return outgoingData; @@ -495,7 +523,24 @@ DataElementUaSdk::readScalar (char *value, const size_t num, (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } UaVariant& data = upd->getData(); - if (enumChoices) { + if (data.type() == OpcUaType_ExtensionObject) { + UaExtensionObject extensionObject; + data.toExtensionObject(extensionObject); + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isUnion()) { + memset(value, 0, num); + UaGenericUnionValue genericValue(extensionObject, definition); + int switchValue = genericValue.switchValue(); + if (switchValue > 0) { + snprintf(value, num, "%s:%s", + definition.child(switchValue-1).name().toUtf8(), + genericValue.value().toString().toUtf8()); + } else { + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } + } + } else if (enumChoices) { OpcUa_Int32 enumIndex; data.toInt32(enumIndex); auto it = enumChoices->find(enumIndex); @@ -921,8 +966,48 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo unsigned long ul; double d; char* end = nullptr; + OpcUa_BuiltInType type = incomingData.type(); - switch (incomingData.type()) { + if (type == OpcUaType_ExtensionObject) + { + UaExtensionObject extensionObject; + incomingData.toExtensionObject(extensionObject); + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isUnion()) { + if (value[0] == '\0') { + } else for (int i = 0; i < definition.childrenCount(); i++) { + UaGenericUnionValue genericValue(extensionObject, definition); + const UaString& memberName = definition.child(i).name(); + epicsUInt32 namelen = static_cast(memberName.length()); + if ((strncmp(value, memberName.toUtf8(), namelen) == 0) + && value[namelen] == ':') { + // temporarily set incomingData to selected union member type + UaVariant saveValue = incomingData; + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = definition.child(i).valueType(); + incomingData = fakeValue; + const UaNodeId& typeId = definition.child(i).typeId(); + enumChoices = pitem->session->getEnumChoices(&typeId); + // recurse to set union member + ret = writeScalar(value+namelen+1, len-(namelen+1), prec); + // restore incomingData type to union + delete enumChoices; + enumChoices = nullptr; + incomingData = saveValue; + if (ret == 0) { + Guard G(outgoingLock); + genericValue.setValue(i+1, outgoingData); + genericValue.toExtensionObject(extensionObject); + outgoingData.setExtensionObject(extensionObject,true); + } + return ret; + } + } + } + } + + switch (type) { case OpcUaType_String: { // Scope of Guard G Guard G(outgoingLock); @@ -1066,10 +1151,11 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo } default: errlogPrintf("%s : unsupported conversion from string to %s for outgoing data\n", - prec->name, variantTypeString(incomingData.type())); + prec->name, variantTypeString(type)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); return -1; } + if (ret != 0) { errlogPrintf("%s : value \"%s\" out of range\n", prec->name, value); diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 71c800cf..a906bbaa 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -107,28 +107,35 @@ DataElementOpen62541::show (const int level, const unsigned int indent) const #error Set UA_ENABLE_TYPEDESCRIPTION in open62541 #endif -#ifndef UA_DATATYPES_USE_POINTER // Older open62541 version 1.2 has no UA_DataType_getStructMember // Code from open62541 version 1.3.7 modified for compatibility with version 1.2 +// and extended to return isOptional flag UA_Boolean -UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, +UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray) { + UA_Boolean *outIsArray, UA_Boolean *outIsOptional) { if(type->typeKind != UA_DATATYPEKIND_STRUCTURE && type->typeKind != UA_DATATYPEKIND_OPTSTRUCT) return false; size_t offset = 0; +#ifndef UA_DATATYPES_USE_POINTER const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; +#endif for(size_t i = 0; i < type->membersSize; ++i) { const UA_DataTypeMember *m = &type->members[i]; +#ifndef UA_DATATYPES_USE_POINTER const UA_DataType *mt = &typelists[!m->namespaceZero][m->memberTypeIndex]; +#else + const UA_DataType *mt = m->memberType; +#endif offset += m->padding; if(strcmp(memberName, m->memberName) == 0) { *outOffset = offset; *outMemberType = mt; *outIsArray = m->isArray; + *outIsOptional = m->isOptional; return true; } @@ -151,27 +158,28 @@ UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, return false; } -#endif -bool +void DataElementOpen62541::createMap (const UA_DataType *type, const std::string *timefrom) { switch (typeKindOf(type)) { case UA_DATATYPEKIND_STRUCTURE: - { + case UA_DATATYPEKIND_OPTSTRUCT: { if (debug() >= 5) std::cout << " ** creating index-to-element map for child elements" << std::endl; if (timefrom) { const UA_DataType *timeMemberType; UA_Boolean timeIsArray; + UA_Boolean timeIsOptional; size_t timeOffset; - if (UA_DataType_getStructMember(type, timefrom->c_str(), + if (UA_DataType_getStructMemberExt(type, timefrom->c_str(), &timeOffset, &timeMemberType, - &timeIsArray)) { + &timeIsArray, + &timeIsOptional)) { if (typeKindOf(timeMemberType) != UA_TYPES_DATETIME || timeIsArray) { errlogPrintf("%s: timestamp element %s has invalid type %s%s - using " "source timestamp\n", @@ -191,10 +199,11 @@ DataElementOpen62541::createMap (const UA_DataType *type, for (auto &it : elements) { auto pelem = it.lock(); - if (UA_DataType_getStructMember(type, pelem->name.c_str(), + if (UA_DataType_getStructMemberExt(type, pelem->name.c_str(), &pelem->offset, &pelem->memberType, - &pelem->isArray)) { + &pelem->isArray, + &pelem->isOptional)) { if (typeKindOf(pelem->memberType) == UA_DATATYPEKIND_ENUM) { pelem->enumChoices = pitem->session->getEnumChoices(&pelem->memberType->typeId); } @@ -207,18 +216,17 @@ DataElementOpen62541::createMap (const UA_DataType *type, } if (debug() >= 5) std::cout << " ** " << elements.size() - << " child elements mapped to a " - << "structure of " << type->membersSize << " elements" << std::endl; + << " child elements mapped to " + << variantTypeString(type) + << " of " << type->membersSize << " elements" << std::endl; break; } default: - std::cerr << "Item " << pitem - << " has unimplemented type " << variantTypeString(type) + std::cerr << "Error: " << this + << " is not a structure or an optstruct but a " << typeKindName(typeKindOf(type)) << std::endl; - return false; } mapped = true; - return true; } // Getting the timestamp and status information from the Item assumes that only one thread @@ -271,7 +279,7 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, const UA_DataType *type = value.type; char* container = static_cast(value.data); - if (type->typeKind == UA_DATATYPEKIND_EXTENSIONOBJECT) { + if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { UA_ExtensionObject &extensionObject = *reinterpret_cast(container); if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { // Access content of decoded extension objects @@ -288,28 +296,32 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, if (!mapped) createMap(type, timefrom); - if (timefrom) { - if (timesrc >= 0) - pitem->tsData = ItemOpen62541::uaToEpicsTime(*reinterpret_cast(container + timesrc), 0); - else - pitem->tsData = pitem->tsSource; - } + if (timesrc >= 0) + pitem->tsData = ItemOpen62541::uaToEpicsTime(*reinterpret_cast(container + timesrc), 0); + else + pitem->tsData = pitem->tsSource; for (auto &it : elements) { auto pelem = it.lock(); - char* memberData = container + pelem->offset; const UA_DataType* memberType = pelem->memberType; + char* memberData = container + pelem->offset; UA_Variant memberValue; - if (pelem->isArray) - { - size_t arrayLength = *reinterpret_cast(memberData); + size_t arrayLength = 0; // default to scalar + if (pelem->isArray) { + arrayLength = *reinterpret_cast(memberData); memberData = *reinterpret_cast(memberData + sizeof(size_t)); - if (arrayLength == 0) memberData = reinterpret_cast(UA_EMPTY_ARRAY_SENTINEL); - UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); - } else { - UA_Variant_setScalar(&memberValue, memberData, memberType); + } else if (pelem->isOptional) { + /* optional scalar stored through pointer like an array */ + memberData = *reinterpret_cast(memberData); } - pelem->setIncomingData(memberValue, reason); + UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); + if (debug() && !memberData && pelem->isOptional) { + std::cerr << pelem << " absent optional " << variantTypeString(memberType) + << (pelem->isArray ? " array" : " scalar" ) + << std::endl; + } + pelem->setIncomingData(memberValue, + pelem->isOptional && !memberData ? ProcessReason::readFailure : reason); } } } @@ -358,37 +370,39 @@ DataElementOpen62541::setState(const ConnectionStatus state) // Helper to update one data structure element from pointer to child bool -DataElementOpen62541::updateDataInStruct (void* container, - std::shared_ptr pelem) +DataElementOpen62541::updateDataInStruct(void* container, + std::shared_ptr pelem) { bool updated = false; { // Scope of Guard G Guard G(pelem->outgoingLock); if (pelem->isDirty()) { - void* memberData = static_cast(container) + pelem->offset; - const UA_Variant& data = pelem->getOutgoingData(); + char* memberData = static_cast(container) + pelem->offset; + const UA_Variant& elementData = pelem->getOutgoingData(); const UA_DataType* memberType = pelem->memberType; - assert(memberType == data.type || + assert(memberType == elementData.type || (typeKindOf(memberType) == UA_DATATYPEKIND_ENUM && - typeKindOf(data.type) == UA_DATATYPEKIND_INT32)); - if (pelem->isArray) - { - size_t& arrayLength = *reinterpret_cast(memberData); - void*& arrayData = *reinterpret_cast(static_cast(memberData) + sizeof(size_t)); - UA_Array_delete(arrayData, arrayLength, memberType); - UA_StatusCode status = UA_Array_copy(data.data, data.arrayLength, &arrayData, memberType); - if (status == UA_STATUSCODE_GOOD) { - arrayLength = data.arrayLength; - } else { - arrayLength = 0; - std::cerr << "Item " << pitem - << ": inserting data from from child element" << pelem->name - << " failed (" << UA_StatusCode_name(status) << ')' - << std::endl; - return false; - } + typeKindOf(elementData.type) == UA_DATATYPEKIND_INT32)); + if (!pelem->isArray && !pelem->isOptional) { + // mandatory scalar: shallow copy + UA_clear(memberData, memberType); + void* data = pelem->moveOutgoingData(); + memcpy(memberData, data, memberType->memSize); + UA_free(data); } else { - UA_copy(data.data, memberData, memberType); + // array or optional scalar: move content + void **memberDataPtr; + if (pelem->isArray) /* mandatory or optional array stored as length and pointer */ { + size_t& arrayLength = *reinterpret_cast(memberData); + memberDataPtr = reinterpret_cast(memberData + sizeof(size_t)); + UA_Array_delete(*memberDataPtr, arrayLength, memberType); + arrayLength = elementData.arrayLength; + } else /* optional scalar stored through pointer */ { + memberDataPtr = reinterpret_cast(memberData); + if (*memberDataPtr) /* absent optional has nullptr here */ + UA_Array_delete(*memberDataPtr, 1, memberType); + } + *memberDataPtr = pelem->moveOutgoingData(); } pelem->isdirty = false; updated = true; diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index a67cb78e..56c4f29e 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -722,6 +722,21 @@ class DataElementOpen62541 : public DataElement */ virtual void clearOutgoingData() { UA_Variant_clear(&outgoingData); } + /** + * @brief Move the contents of the current outgoing data. + * + * Avoids a deep copy by moving the contents out of the outgoing data + * and clearing it afterwards. + * + * Call holding outgoingLock! + */ + void* moveOutgoingData() { + void* data = outgoingData.data; + outgoingData.data = nullptr; + UA_Variant_clear(&outgoingData); + return data; + } + /** * @brief Create processing requests for record(s) attached to this element. * See DevOpcua::DataElement::requestRecordProcessing @@ -746,7 +761,7 @@ class DataElementOpen62541 : public DataElement void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; bool updateDataInStruct(void* container, std::shared_ptr pelem); - bool createMap(const UA_DataType *type, const std::string* timefrom); + void createMap(const UA_DataType *type, const std::string* timefrom); // Structure always returns true to ensure full traversal bool isDirty() const { return isdirty || !isleaf; } @@ -757,7 +772,7 @@ class DataElementOpen62541 : public DataElement pitem->markAsDirty(); } - // Get the time stamp from the incoming object + // Get the time stamp from the incoming object const epicsTime &getIncomingTimeStamp() const { ProcessReason reason = pitem->getReason(); if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) @@ -800,6 +815,7 @@ class DataElementOpen62541 : public DataElement ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadScalar(upd.get(), epicsTypeString(*value)); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -925,7 +941,6 @@ class DataElementOpen62541 : public DataElement if (UA_STATUS_IS_UNCERTAIN(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - prec->udf = false; } UA_Variant_clear(&data); } @@ -1008,7 +1023,6 @@ class DataElementOpen62541 : public DataElement } elemsWritten = static_cast(num) < data.arrayLength ? num : static_cast(data.arrayLength); memcpy(value, data.data, sizeof(ET) * elemsWritten); - prec->udf = false; } UA_Variant_clear(&data); } @@ -1250,17 +1264,40 @@ class DataElementOpen62541 : public DataElement std::unordered_map> elementMap; ptrdiff_t timesrc; - const UA_DataType *memberType; /**< type of this element */ - UA_Boolean isArray; /**< is this element an array? */ - size_t offset; /**< data offset of this element in parent structure */ + const UA_DataType *memberType = NULL; /**< type of this element */ + UA_Boolean isArray = false; /**< is this element an array? */ + UA_Boolean isOptional = false; /**< is this element optional? */ + size_t offset = 0; /**< data offset of this element in parent structure */ bool mapped; /**< child name to index mapping done */ UpdateQueue incomingQueue; /**< queue of incoming values */ UA_Variant incomingData; /**< cache of latest incoming value */ epicsMutex &outgoingLock; /**< data lock for outgoing value */ UA_Variant outgoingData; /**< cache of latest outgoing value */ bool isdirty; /**< outgoing value has been (or needs to be) updated */ + + friend std::ostream& operator << (std::ostream& os, const DataElementOpen62541& element); }; + +// print the full element name, e.g.: item elem.array[index].elem +inline std::ostream& operator << (std::ostream& os, const DataElementOpen62541& element) +{ + // Skip the [ROOT] element in printing and print the item name instead + if (!element.parent) + return os << element.pitem; + os << element.parent; + if (!element.parent->parent) + os << " element "; + else if (element.name[0] != '[') + os << '.'; + return os << element.name; +} + +inline std::ostream& operator << (std::ostream& os, const DataElementOpen62541* pelement) +{ + return os << *pelement; +} + } // namespace DevOpcua #endif // DEVOPCUA_DATAELEMENTOPEN62541_H diff --git a/devOpcuaSup/open62541/README.md b/devOpcuaSup/open62541/README.md index 4d57898c..ea4ef730 100644 --- a/devOpcuaSup/open62541/README.md +++ b/devOpcuaSup/open62541/README.md @@ -35,25 +35,6 @@ but it does not get the attention that the server parts get. Do *not* use the download link on the open62541 web site. Use their GitHub Release Page instead. -### Bugfix for Shared Build - -The 1.3 release series need the following fix to be applied -when building shared libraries: - -```Diff ---- src/ua_types.c.orig 2024-09-02 11:31:15.514006029 +0200 -+++ src/ua_types.c 2024-09-02 11:31:29.499010032 +0200 -@@ -1882,7 +1882,7 @@ - } - - #ifdef UA_ENABLE_TYPEDESCRIPTION --UA_Boolean -+UA_Boolean UA_EXPORT - UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, - size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray) { -``` - ### On Linux * Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . From 293b766f1666666d07ada4880a3b604551670050 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Fri, 11 Oct 2024 13:37:39 +0200 Subject: [PATCH 16/64] add Union support --- devOpcuaSup/UaSdk/DataElementUaSdk.h | 3 - .../open62541/DataElementOpen62541.cpp | 169 +++++++++++++----- devOpcuaSup/open62541/DataElementOpen62541.h | 3 +- 3 files changed, 127 insertions(+), 48 deletions(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index 20636c93..3ba64b3b 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -683,9 +683,6 @@ class DataElementUaSdk : public DataElement const std::string &targetTypeName) const; void checkWriteArray(OpcUa_BuiltInType expectedType, const std::string &targetTypeName) const; void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; - bool updateDataInGenericValue(UaGenericStructureValue &value, - const int index, - std::shared_ptr pelem); // Structure always returns true to ensure full traversal bool isDirty() const { return isdirty || !isleaf; } void diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index a906bbaa..1b56199d 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -109,26 +109,31 @@ DataElementOpen62541::show (const int level, const unsigned int indent) const // Older open62541 version 1.2 has no UA_DataType_getStructMember // Code from open62541 version 1.3.7 modified for compatibility with version 1.2 -// and extended to return isOptional flag -UA_Boolean +// and extended to return isOptional flag and index for union + +inline const UA_DataType* +memberTypeOf(const UA_DataType *type, const UA_DataTypeMember *m) { +#ifdef UA_DATATYPES_USE_POINTER + return m->memberType; +#else + const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; + return &typelists[!m->namespaceZero][m->memberTypeIndex]; +#endif +} + +UA_UInt32 UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, size_t *outOffset, const UA_DataType **outMemberType, UA_Boolean *outIsArray, UA_Boolean *outIsOptional) { if(type->typeKind != UA_DATATYPEKIND_STRUCTURE && - type->typeKind != UA_DATATYPEKIND_OPTSTRUCT) - return false; + type->typeKind != UA_DATATYPEKIND_OPTSTRUCT && + type->typeKind != UA_DATATYPEKIND_UNION) + return 0; size_t offset = 0; -#ifndef UA_DATATYPES_USE_POINTER - const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; -#endif - for(size_t i = 0; i < type->membersSize; ++i) { + for(UA_UInt32 i = 0; i < type->membersSize; ++i) { const UA_DataTypeMember *m = &type->members[i]; -#ifndef UA_DATATYPES_USE_POINTER - const UA_DataType *mt = &typelists[!m->namespaceZero][m->memberTypeIndex]; -#else - const UA_DataType *mt = m->memberType; -#endif + const UA_DataType *mt = memberTypeOf(type, m); offset += m->padding; if(strcmp(memberName, m->memberName) == 0) { @@ -136,10 +141,12 @@ UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, *outMemberType = mt; *outIsArray = m->isArray; *outIsOptional = m->isOptional; - return true; + return i+1; } - if(!m->isOptional) { + if (type->typeKind == UA_DATATYPEKIND_UNION) { + offset = 0; + } else if(!m->isOptional) { if(!m->isArray) { offset += mt->memSize; } else { @@ -156,19 +163,20 @@ UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, } } - return false; + return 0; } void DataElementOpen62541::createMap (const UA_DataType *type, const std::string *timefrom) { + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + switch (typeKindOf(type)) { case UA_DATATYPEKIND_STRUCTURE: - case UA_DATATYPEKIND_OPTSTRUCT: { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - + case UA_DATATYPEKIND_OPTSTRUCT: + case UA_DATATYPEKIND_UNION: if (timefrom) { const UA_DataType *timeMemberType; UA_Boolean timeIsArray; @@ -199,11 +207,20 @@ DataElementOpen62541::createMap (const UA_DataType *type, for (auto &it : elements) { auto pelem = it.lock(); - if (UA_DataType_getStructMemberExt(type, pelem->name.c_str(), + if ((pelem->index = UA_DataType_getStructMemberExt(type, pelem->name.c_str(), &pelem->offset, &pelem->memberType, &pelem->isArray, - &pelem->isOptional)) { + &pelem->isOptional))) { + if (debug() >= 5) + std::cout << typeKindName(typeKindOf(type)) + << " " << pelem + << " index=" << pelem->index + << " offset=" << pelem->offset + << " type=" << variantTypeString(pelem->memberType) + << (pelem->isArray ? "[]" : "") + << (pelem->isOptional ? " optional" : "") + << std::endl; if (typeKindOf(pelem->memberType) == UA_DATATYPEKIND_ENUM) { pelem->enumChoices = pitem->session->getEnumChoices(&pelem->memberType->typeId); } @@ -220,7 +237,6 @@ DataElementOpen62541::createMap (const UA_DataType *type, << variantTypeString(type) << " of " << type->membersSize << " elements" << std::endl; break; - } default: std::cerr << "Error: " << this << " is not a structure or an optstruct but a " << typeKindName(typeKindOf(type)) @@ -314,14 +330,21 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, /* optional scalar stored through pointer like an array */ memberData = *reinterpret_cast(memberData); } + if (type->typeKind == UA_DATATYPEKIND_UNION && + pelem->index != *reinterpret_cast(container)) { + // union option not taken + memberData = nullptr; + } UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); - if (debug() && !memberData && pelem->isOptional) { - std::cerr << pelem << " absent optional " << variantTypeString(memberType) + if (debug() && !memberData) { + std::cerr << pitem->recConnector->getRecordName() + << " " << pelem + << (type->typeKind == UA_DATATYPEKIND_UNION ? " not taken choice " : " absent optional ") + << variantTypeString(memberType) << (pelem->isArray ? " array" : " scalar" ) << std::endl; } - pelem->setIncomingData(memberValue, - pelem->isOptional && !memberData ? ProcessReason::readFailure : reason); + pelem->setIncomingData(memberValue, memberData ? reason : ProcessReason::readFailure); } } } @@ -387,6 +410,9 @@ DataElementOpen62541::updateDataInStruct(void* container, // mandatory scalar: shallow copy UA_clear(memberData, memberType); void* data = pelem->moveOutgoingData(); + if (typeKindOf(outgoingData) == UA_DATATYPEKIND_UNION) { + *reinterpret_cast(container) = pelem->index; + } memcpy(memberData, data, memberType->memSize); UA_free(data); } else { @@ -437,7 +463,8 @@ DataElementOpen62541::getOutgoingData () isdirty = false; const UA_DataType *type = outgoingData.type; void* container = outgoingData.data; - if (type && type->typeKind == UA_DATATYPEKIND_EXTENSIONOBJECT) { + + if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { UA_ExtensionObject &extensionObject = *reinterpret_cast(container); if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { // Access content decoded extension objects @@ -591,35 +618,57 @@ DataElementOpen62541::readScalar (char *value, const size_t num, if (UA_STATUS_IS_UNCERTAIN(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - UA_Variant &data = upd->getData(); - size_t n = num-1; + UA_String buffer = UA_STRING_NULL; UA_String *datastring = &buffer; - switch (typeKindOf(data)) { + size_t n = num-1; + + UA_Variant &data = upd->getData(); + void* payload = data.data; + const UA_DataType* type = data.type; + + if (type->typeKind == UA_DATATYPEKIND_UNION) + { + UA_UInt32 switchfield = *static_cast(payload)-1; + if (switchfield >= type->membersSize) { + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + break; + } + const UA_DataTypeMember *member = &type->members[switchfield]; + payload = static_cast(payload) + member->padding; + type = memberTypeOf(type, member); + + // prefix value string with switch choice name + size_t len = snprintf(value, n, "%s:", member->memberName); + value += len; + n -= len; + } + + switch (type->typeKind) { case UA_DATATYPEKIND_STRING: { - datastring = static_cast(data.data); + datastring = static_cast(payload); break; } case UA_DATATYPEKIND_LOCALIZEDTEXT: { - datastring = &static_cast(data.data)->text; + datastring = &static_cast(payload)->text; break; } case UA_DATATYPEKIND_DATETIME: { // UA_print does not correct printed time for time zone UA_Int64 tOffset = UA_DateTime_localTimeUtcOffset(); - UA_DateTime dt = *static_cast(data.data); + UA_DateTime dt = *static_cast(payload); dt += tOffset; - UA_print(&dt, data.type, &buffer); + UA_print(&dt, type, &buffer); break; } case UA_DATATYPEKIND_ENUM: case UA_DATATYPEKIND_INT32: { if (enumChoices) { - auto it = enumChoices->find(*static_cast(data.data)); + auto it = enumChoices->find(*static_cast(payload)); if (it != enumChoices->end()) { buffer = UA_String_fromChars(it->second.c_str()); break; @@ -628,9 +677,9 @@ DataElementOpen62541::readScalar (char *value, const size_t num, // no enum or index not found: fall through } default: - if (data.type) - UA_print(data.data, data.type, &buffer); - } + if (type) + UA_print(payload, type, &buffer); + }; if (n > datastring->length) n = datastring->length; strncpy(value, reinterpret_cast(datastring->data), n); @@ -1027,7 +1076,7 @@ DataElementOpen62541::writeScalar (const epicsFloat64 &value, dbCommon *prec) } long -DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbCommon *prec) +DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon *prec) { long ret = 1; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; @@ -1039,7 +1088,25 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC { // Scope of Guard G Guard G(outgoingLock); UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard - switch (typeKindOf(incomingData)) { + const UA_DataType* type = incomingData.type; + + int switchfield = -1; + if (typeKindOf(type) == UA_DATATYPEKIND_UNION) { + if (value[0] == '\0') { + switchfield = 0; + } else for (UA_UInt32 i = 0; i < type->membersSize; i++) { + size_t namelen = strlen(type->members[i].memberName); + if (strncmp(value, type->members[i].memberName, namelen) == 0 + && value[namelen] == ':') { + value += namelen+1; + len -= namelen+1; + switchfield = i+1; + type = memberTypeOf(type, &type->members[i]); + } + } + } + + switch (typeKindOf(type)) { case UA_DATATYPEKIND_STRING: { UA_String val; @@ -1176,8 +1243,6 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC if (end != value) { UA_Double val = static_cast(d); status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); - ret = 0; } break; } @@ -1186,7 +1251,23 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC prec->name, variantTypeString(incomingData)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } - } + if (switchfield >= 0) { + // manually wrap value from outgoingData into union + void *p = UA_new(incomingData.type); + if (p) { + *static_cast(p) = switchfield; + if (switchfield > 0) { + memcpy(static_cast(p) + incomingData.type->members[switchfield-1].padding, + outgoingData.data, outgoingData.type->memSize); + UA_free(outgoingData.data); + } + UA_Variant_setScalar(&outgoingData, p, incomingData.type); + status = UA_STATUSCODE_GOOD; + markAsDirty(); + ret = 0; + } + } + } // Scope of Guard G if (ret != 0) { errlogPrintf("%s : value \"%s\" out of range\n", prec->name, value); diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index 56c4f29e..a129e1a2 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -597,7 +597,7 @@ class DataElementOpen62541 : public DataElement * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) */ virtual long int writeScalar(const char *value, - const epicsUInt32 num, + epicsUInt32 num, dbCommon *prec) override; /** @@ -1268,6 +1268,7 @@ class DataElementOpen62541 : public DataElement UA_Boolean isArray = false; /**< is this element an array? */ UA_Boolean isOptional = false; /**< is this element optional? */ size_t offset = 0; /**< data offset of this element in parent structure */ + UA_UInt32 index = 0; /**< element index (for unions) */ bool mapped; /**< child name to index mapping done */ UpdateQueue incomingQueue; /**< queue of incoming values */ UA_Variant incomingData; /**< cache of latest incoming value */ From 8da935944d877ecd489b52fb8a2f88fb7c771d5e Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Tue, 29 Oct 2024 10:52:32 +0100 Subject: [PATCH 17/64] open62541: move incoming data instead of copy --- devOpcuaSup/open62541/DataElementOpen62541.cpp | 7 +++++-- devOpcuaSup/open62541/ItemOpen62541.cpp | 3 ++- devOpcuaSup/open62541/ItemOpen62541.h | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 1b56199d..32590d4b 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -252,9 +252,11 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, ProcessReason reason, const std::string *timefrom) { - // Make a copy of this element and cache it + // Cache this element. We can make a shallow copy because + // ItemOpen62541::setIncomingData marks the original response data as "ours". + // Member data is owned by the [ROOT] element. UA_Variant_clear(&incomingData); - UA_Variant_copy(&value, &incomingData); + incomingData = value; if (isLeaf()) { if (pconnector->state() == ConnectionStatus::initialRead && typeKindOf(value) == UA_DATATYPEKIND_ENUM) { @@ -336,6 +338,7 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, memberData = nullptr; } UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); + memberValue.storageType = UA_VARIANT_DATA_NODELETE; // Keep ownership of data if (debug() && !memberData) { std::cerr << pitem->recConnector->getRecordName() << " " << pelem diff --git a/devOpcuaSup/open62541/ItemOpen62541.cpp b/devOpcuaSup/open62541/ItemOpen62541.cpp index c5ef95e9..1f762e76 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.cpp +++ b/devOpcuaSup/open62541/ItemOpen62541.cpp @@ -154,7 +154,7 @@ ItemOpen62541::uaToEpicsTime (const UA_DateTime &dt, const UA_UInt16 pico10) } void -ItemOpen62541::setIncomingData(const UA_DataValue &value, ProcessReason reason) +ItemOpen62541::setIncomingData(UA_DataValue &value, ProcessReason reason) { tsClient = epicsTime::getCurrent(); if (!UA_STATUS_IS_BAD(value.status)) { @@ -182,6 +182,7 @@ ItemOpen62541::setIncomingData(const UA_DataValue &value, ProcessReason reason) if (linkinfo.timestamp == LinkOptionTimestamp::data && linkinfo.timestampElement.length()) timefrom = &linkinfo.timestampElement; pd->setIncomingData(value.value, reason, timefrom); + value.value.storageType = UA_VARIANT_DATA_NODELETE; // take ownership of data } if (linkinfo.isItemRecord) { diff --git a/devOpcuaSup/open62541/ItemOpen62541.h b/devOpcuaSup/open62541/ItemOpen62541.h index 8ef78a19..7554b25e 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.h +++ b/devOpcuaSup/open62541/ItemOpen62541.h @@ -154,7 +154,7 @@ class ItemOpen62541 : public Item * @param value new value for this data element * @param reason reason for this value update */ - void setIncomingData(const UA_DataValue &value, ProcessReason reason); + void setIncomingData(UA_DataValue &value, ProcessReason reason); /** * @brief Push an incoming event down the root element. From 66e630c9a6c5a9db4649efdf56294e36ff2fef01 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Tue, 18 Jun 2024 14:02:37 +0200 Subject: [PATCH 18/64] implement deadband option Not available for UaSdk on Windows because of this linker error: SubscriptionUaSdk.obj : error LNK2001: unresolved external symbol OpcUa_DataChangeFilter_EncodeableType It seems OpcUa_DataChangeFilter_EncodeableType is not properly exported for Windows. --- devOpcuaSup/UaSdk/ItemUaSdk.cpp | 1 + devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp | 21 +++++++++++++++++++ devOpcuaSup/devOpcua.h | 1 + devOpcuaSup/linkParser.cpp | 4 ++++ devOpcuaSup/open62541/ItemOpen62541.cpp | 1 + .../open62541/SubscriptionOpen62541.cpp | 11 ++++++++++ 6 files changed, 39 insertions(+) diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index 97b7825b..ad2ac308 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.cpp +++ b/devOpcuaSup/UaSdk/ItemUaSdk.cpp @@ -104,6 +104,7 @@ ItemUaSdk::show (int level) const << " dataDirty=" << (dataTreeDirty ? "y" : "n") << " context=" << linkinfo.subscription << "@" << session->getName() << " sampling=" << revisedSamplingInterval << "(" << linkinfo.samplingInterval << ")" + << " deadband=" << linkinfo.deadband << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" << " cqsize=" << linkinfo.clientQueueSize << " discard=" << (linkinfo.discardOldest ? "old" : "new") diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index 5687dd29..9cc95a33 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -178,6 +179,26 @@ SubscriptionUaSdk::addMonitoredItems () monitoredItemCreateRequests[i].RequestedParameters.SamplingInterval = it->linkinfo.samplingInterval; monitoredItemCreateRequests[i].RequestedParameters.QueueSize = it->linkinfo.queueSize; monitoredItemCreateRequests[i].RequestedParameters.DiscardOldest = it->linkinfo.discardOldest; + if (it->linkinfo.deadband > 0.0) { +#ifdef _WIN32 + errlogPrintf("OPC UA subscription %s@%s: deadband not implemented for UA SDK on Windows\n", + name.c_str(), psessionuasdk->getName().c_str()); +#else + OpcUa_DataChangeFilter* pDataChangeFilter = NULL; + status = OpcUa_EncodeableObject_CreateExtension( + &OpcUa_DataChangeFilter_EncodeableType, + &monitoredItemCreateRequests[i].RequestedParameters.Filter, + (OpcUa_Void**)&pDataChangeFilter); + if (status.isBad()) { + errlogPrintf("OPC UA subscription %s@%s: cannot create deadband filter: %s\n", + name.c_str(), psessionuasdk->getName().c_str(), status.toString().toUtf8()); + } else { + pDataChangeFilter->DeadbandType = OpcUa_DeadbandType_Absolute; + pDataChangeFilter->DeadbandValue = it->linkinfo.deadband; + pDataChangeFilter->Trigger = OpcUa_DataChangeTrigger_StatusValue; + } +#endif + } i++; } diff --git a/devOpcuaSup/devOpcua.h b/devOpcuaSup/devOpcua.h index 7c1901fa..3b8453a2 100644 --- a/devOpcuaSup/devOpcua.h +++ b/devOpcuaSup/devOpcua.h @@ -101,6 +101,7 @@ typedef struct linkInfo { epicsUInt32 queueSize; epicsUInt32 clientQueueSize; bool discardOldest = true; + double deadband = 0; std::string element; std::list elementPath; diff --git a/devOpcuaSup/linkParser.cpp b/devOpcuaSup/linkParser.cpp index 88706f90..4ce86249 100644 --- a/devOpcuaSup/linkParser.cpp +++ b/devOpcuaSup/linkParser.cpp @@ -275,6 +275,9 @@ parseLink (dbCommon *prec, const DBEntry &ent) } else if (pinfo->linkedToItem && optname == "sampling") { if (epicsParseDouble(optval.c_str(), &pinfo->samplingInterval, nullptr)) throw std::runtime_error(SB() << "error converting '" << optval << "' to Double"); + } else if (pinfo->linkedToItem && optname == "deadband") { + if (epicsParseDouble(optval.c_str(), &pinfo->deadband, nullptr)) + throw std::runtime_error(SB() << "error converting '" << optval << "' to Double"); } else if (pinfo->linkedToItem && optname == "qsize") { if (epicsParseUInt32(optval.c_str(), &pinfo->queueSize, 0, nullptr)) throw std::runtime_error(SB() << "error converting '" << optval << "' to UInt32"); @@ -352,6 +355,7 @@ parseLink (dbCommon *prec, const DBEntry &ent) else std::cout << " id(s)=" << pinfo->identifierString; std::cout << " sampling=" << pinfo->samplingInterval + << " deadband=" << pinfo->deadband << " qsize=" << pinfo->queueSize << " cqsize=" << pinfo->clientQueueSize << " discard=" << (pinfo->discardOldest ? "old" : "new") diff --git a/devOpcuaSup/open62541/ItemOpen62541.cpp b/devOpcuaSup/open62541/ItemOpen62541.cpp index 1f762e76..7d8b3b3c 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.cpp +++ b/devOpcuaSup/open62541/ItemOpen62541.cpp @@ -104,6 +104,7 @@ ItemOpen62541::show (int level) const << " dataDirty=" << (dataTreeDirty ? "y" : "n") << " context=" << linkinfo.subscription << "@" << session->getName() << " sampling=" << revisedSamplingInterval << "(" << linkinfo.samplingInterval << ")" + << " deadband=" << linkinfo.deadband << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" << " cqsize=" << linkinfo.clientQueueSize << " discard=" << (linkinfo.discardOldest ? "old" : "new") diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index eaa216dd..e5821b7c 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -148,6 +148,7 @@ SubscriptionOpen62541::addMonitoredItems () UA_UInt32 i; UA_MonitoredItemCreateRequest monitoredItemCreateRequest; UA_MonitoredItemCreateResult monitoredItemCreateResult; + UA_DataChangeFilter dataChangeFilter; if (items.size()) { monitoredItemCreateResult.statusCode = UA_STATUSCODE_GOOD; // suppress compiler warning @@ -161,6 +162,16 @@ SubscriptionOpen62541::addMonitoredItems () monitoredItemCreateRequest.requestedParameters.samplingInterval = it->linkinfo.samplingInterval; monitoredItemCreateRequest.requestedParameters.queueSize = it->linkinfo.queueSize; monitoredItemCreateRequest.requestedParameters.discardOldest = it->linkinfo.discardOldest; + if (it->linkinfo.deadband > 0.0) { + UA_DataChangeFilter_init(&dataChangeFilter); + dataChangeFilter.deadbandType = UA_DEADBANDTYPE_ABSOLUTE; + dataChangeFilter.deadbandValue = it->linkinfo.deadband; + dataChangeFilter.trigger = UA_DATACHANGETRIGGER_STATUSVALUE; + UA_ExtensionObject *filter = &monitoredItemCreateRequest.requestedParameters.filter; + filter->content.decoded.data = &dataChangeFilter; + filter->content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER]; + filter->encoding = UA_EXTENSIONOBJECT_DECODED; + } monitoredItemCreateResult = UA_Client_MonitoredItems_createDataChange( session.client, subscriptionSettings.subscriptionId, UA_TIMESTAMPSTORETURN_BOTH, monitoredItemCreateRequest, it, [] (UA_Client *client, UA_UInt32 subId, void *subContext, From ba4d4a5e798c5b63ebd1aea55045b2c57ba693cb Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Thu, 7 Nov 2024 10:47:28 +0100 Subject: [PATCH 19/64] support more string types * XmlElement: like String * ByteString: convert to/from hex bytes * LocalizedText: with access to elements locale and text * QualifiedName: with access to elements namespaceIndex and name --- devOpcuaSup/DataElement.h | 10 +- devOpcuaSup/RecordConnector.h | 8 +- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 780 +++++++++++++----- devOpcuaSup/UaSdk/DataElementUaSdk.h | 29 +- devOpcuaSup/devOpcua.cpp | 49 +- .../open62541/DataElementOpen62541.cpp | 473 ++++++++--- devOpcuaSup/open62541/DataElementOpen62541.h | 26 +- 7 files changed, 1013 insertions(+), 362 deletions(-) diff --git a/devOpcuaSup/DataElement.h b/devOpcuaSup/DataElement.h index a86af336..aa1cae37 100644 --- a/devOpcuaSup/DataElement.h +++ b/devOpcuaSup/DataElement.h @@ -204,18 +204,20 @@ class DataElement * the OPC UA status of the item related to this DataElement. * * @param[out] value target string buffer - * @param[in] num length of target string buffer (including null byte) + * @param[in] len length of target string buffer (including null byte) * @param[in] prec pointer to EPICS record * @param[out] nextReason set to ProcessReason for the next update, `none` if last element + * @param[out] lenRead actual number of chars read (may be NULL) * @param[out] statusCode set to the OPC UA status code of the update * @param[out] statusText set to the OPC UA status text of the update (will be null terminated) * @param[in] statusTextLen length of the statusText buffer * * @return status 0 = ok, 1 = error */ - virtual long readScalar(char *value, const size_t num, + virtual long readScalar(char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason = nullptr, + epicsUInt32* lenRead = nullptr, epicsUInt32 *statusCode = nullptr, char *statusText = nullptr, const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) = 0; @@ -594,13 +596,13 @@ class DataElement * @brief Write outgoing classic C string (char[]) data. * * @param value pointer to null terminated source string - * @param num source string size + * @param len source string length * @param prec pointer to EPICS record * * @return status 0 = ok, 1 = error */ virtual long writeScalar(const char *value, - const epicsUInt32 num, + const epicsUInt32 len, dbCommon *prec) = 0; /** diff --git a/devOpcuaSup/RecordConnector.h b/devOpcuaSup/RecordConnector.h index 8c822086..c195b87e 100644 --- a/devOpcuaSup/RecordConnector.h +++ b/devOpcuaSup/RecordConnector.h @@ -45,9 +45,9 @@ class RecordConnector return pdataelement->readScalar(val, prec, nextReason); } - long int readScalar(char *val, const epicsUInt32 num, ProcessReason *nextReason = nullptr) + long int readScalar(char *val, const epicsUInt32 len, ProcessReason *nextReason = nullptr, epicsUInt32 *lenRead = nullptr) { - return pdataelement->readScalar(val, num, prec, nextReason); + return pdataelement->readScalar(val, len, prec, nextReason, lenRead); } template @@ -56,9 +56,9 @@ class RecordConnector return pdataelement->writeScalar(val, prec); } - long int writeScalar(const char* val, const epicsUInt32 num) + long int writeScalar(const char* val, const epicsUInt32 len) { - return pdataelement->writeScalar(val, num, prec); + return pdataelement->writeScalar(val, len, prec); } template diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index 17be203a..d319519a 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -107,6 +107,87 @@ DataElementUaSdk::show (const int level, const unsigned int indent) const } } +void +DataElementUaSdk::createMap(const UaStructureDefinition& definition, + const std::string *timefrom) +{ + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + + if (timefrom) { + for (int i = 0; i < definition.childrenCount(); i++) { + if (*timefrom == definition.child(i).name().toUtf8()) { + timesrc = i; + } + } + OpcUa_BuiltInType t = definition.child(timesrc).valueType(); + if (timesrc == -1) { + errlogPrintf( + "%s: timestamp element %s not found - using source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str()); + } else if (t != OpcUaType_DateTime) { + errlogPrintf("%s: timestamp element %s has invalid type %s - using " + "source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str(), + variantTypeString(t)); + timesrc = -1; + } + } + for (auto &it : elements) { + auto pelem = it.lock(); + for (int i = 0; i < definition.childrenCount(); i++) { + if (pelem->name == definition.child(i).name().toUtf8()) { + elementMap.insert({i, it}); + delete pelem->enumChoices; + pelem->enumChoices = pitem->session->getEnumChoices(definition.child(i).enumDefinition()); + } + } + } + if (debug() >= 5) + std::cout << " ** " << elementMap.size() << "/" << elements.size() + << " child elements mapped to a " + << "structure of " << definition.childrenCount() << " elements" << std::endl; + mapped = true; +} + +void +DataElementUaSdk::createMap(const UaLocalizedText&) +{ + for (auto &it : elements) { + auto pelem = it.lock(); + if (pelem->name == "locale") + elementMap.insert({0, it}); + else if (pelem->name == "text") + elementMap.insert({1, it}); + else + errlogPrintf("Item %s %s element %s not found\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + pelem->name.c_str()); + } + mapped = true; +} + +void +DataElementUaSdk::createMap(const UaQualifiedName&) +{ + for (auto &it : elements) { + auto pelem = it.lock(); + if (pelem->name == "namespaceIndex") + elementMap.insert({0, it}); + else if (pelem->name == "name") + elementMap.insert({1, it}); + else + errlogPrintf("Item %s %s element %s not found\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + pelem->name.c_str()); + } + mapped = true; +} + // Getting the timestamp and status information from the Item assumes that only one thread // is pushing data into the Item's DataElement structure at any time. void @@ -145,101 +226,16 @@ DataElementUaSdk::setIncomingData(const UaVariant &value, std::cout << "Element " << name << " splitting structured data to " << elements.size() << " child elements" << std::endl; - if (value.type() == OpcUaType_ExtensionObject) { + switch (value.type()) { + case OpcUaType_ExtensionObject: { UaExtensionObject extensionObject; value.toExtensionObject(extensionObject); + if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) + extensionObject.changeEncoding(UaExtensionObject::Binary); // Try to get the structure definition from the dictionary UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (!definition.isNull()) { - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) - extensionObject.changeEncoding(UaExtensionObject::Binary); - if (!mapped) { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - if (timefrom) { - for (int i = 0; i < definition.childrenCount(); i++) { - if (*timefrom == definition.child(i).name().toUtf8()) { - timesrc = i; - } - } - OpcUa_BuiltInType t = definition.child(timesrc).valueType(); - if (timesrc == -1) { - errlogPrintf( - "%s: timestamp element %s not found - using source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str()); - } else if (t != OpcUaType_DateTime) { - errlogPrintf("%s: timestamp element %s has invalid type %s - using " - "source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str(), - variantTypeString(t)); - timesrc = -1; - } - } - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - delete pelem->enumChoices; - pelem->enumChoices = pitem->session->getEnumChoices(definition.child(i).enumDefinition()); - } - } - } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; - } - - if (timesrc >= 0) - pitem->tsData = epicsTimeFromUaVariant(UaGenericStructureValue(extensionObject, definition).value(timesrc)); - else - pitem->tsData = pitem->tsSource; - - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - OpcUa_StatusCode stat; - if (definition.isUnion()) { - UaGenericUnionValue genericValue(extensionObject, definition); - int index = genericValue.switchValue() - 1; - if (it.first == index) { - pelem->setIncomingData(genericValue.value(), reason); - stat = OpcUa_Good; - } else { - stat = OpcUa_BadNoData; - } - } else { - const UaVariant &memberValue = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); - if (stat == OpcUa_Good) { - pelem->setIncomingData(memberValue, reason); - } - } - if (stat == OpcUa_BadNoData) { - // Absent optional field or union choice not taken: - // Pass a fake value with correct data type so that - // later writing will not fail with type mismatch. - const UaStructureField &field = definition.child(it.first); - OpcUa_Variant fakeValue; - OpcUa_Variant_Initialize(&fakeValue); - fakeValue.Datatype = field.valueType(); - fakeValue.ArrayType = field.valueRank() != 0; - if (debug()) - std::cerr << pitem->recConnector->getRecordName() - << " element " << pelem->name - << (definition.isUnion() ? " not taken choice " : " absent optional " ) - << variantTypeString((OpcUa_BuiltInType)fakeValue.Datatype) - << (fakeValue.ArrayType ? " array" : " scalar") - << std::endl; - pelem->setIncomingData(fakeValue, ProcessReason::readFailure); - } - } - - } else + if (definition.isNull()) { errlogPrintf( "Cannot get a structure definition for item %s element %s (dataTypeId %s " "encodingTypeId %s) - check access to type dictionary\n", @@ -247,6 +243,104 @@ DataElementUaSdk::setIncomingData(const UaVariant &value, name.c_str(), extensionObject.dataTypeId().toString().toUtf8(), extensionObject.encodingTypeId().toString().toUtf8()); + return; + } + + // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields + if (!mapped) + createMap(definition, timefrom); + + if (timesrc >= 0) + pitem->tsData = epicsTimeFromUaVariant(UaGenericStructureValue(extensionObject, definition).value(timesrc)); + else + pitem->tsData = pitem->tsSource; + + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + OpcUa_StatusCode stat; + if (definition.isUnion()) { + UaGenericUnionValue genericValue(extensionObject, definition); + int index = genericValue.switchValue() - 1; + if (it.first == index) { + pelem->setIncomingData(genericValue.value(), reason); + stat = OpcUa_Good; + } else { + stat = OpcUa_BadNoData; + } + } else { + const UaVariant &memberValue = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); + if (stat == OpcUa_Good) { + pelem->setIncomingData(memberValue, reason); + } + } + if (stat == OpcUa_BadNoData) { + // Absent optional field or union choice not taken: + // Pass a fake value with correct data type so that + // later writing will not fail with type mismatch. + const UaStructureField &field = definition.child(it.first); + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = field.valueType(); + fakeValue.ArrayType = field.valueRank() != 0; + if (debug()) + std::cerr << pitem->recConnector->getRecordName() + << " element " << pelem->name + << (definition.isUnion() ? " not taken choice " : " absent optional " ) + << variantTypeString((OpcUa_BuiltInType)fakeValue.Datatype) + << (fakeValue.ArrayType ? " array" : " scalar") + << std::endl; + pelem->setIncomingData(fakeValue, ProcessReason::readFailure); + } + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedText localizedText; + value.toLocalizedText(localizedText); + if (!mapped) + createMap(localizedText); + + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + UaVariant memberValue; + switch (it.first) { + case 0: + memberValue.setString(localizedText.locale()); + break; + case 1: + memberValue.setString(localizedText.text()); + break; + } + pelem->setIncomingData(memberValue, reason); + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedName qualifiedName; + value.toQualifiedName(qualifiedName); + if (!mapped) + createMap(qualifiedName); + + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + UaVariant memberValue; + switch (it.first) { + case 0: + memberValue.setUInt16(qualifiedName.namespaceIndex()); + break; + case 1: + memberValue.setString(qualifiedName.name()); + break; + } + pelem->setIncomingData(memberValue, reason); + } + break; + } + default: + errlogPrintf("%s: %s is no structured data but a %s\n", + pitem->recConnector->getRecordName(), + name.c_str(), + variantTypeString(value.type())); } } } @@ -305,7 +399,8 @@ DataElementUaSdk::getOutgoingData () outgoingData = incomingData; isdirty = false; - if (outgoingData.type() == OpcUaType_ExtensionObject) { + switch (outgoingData.type()) { + case OpcUaType_ExtensionObject: { UaExtensionObject extensionObject; outgoingData.toExtensionObject(extensionObject); if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) @@ -323,23 +418,9 @@ DataElementUaSdk::getOutgoingData () } // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - if (!mapped) { - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - } - } - } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; - } + if (!mapped) + createMap(definition); + if (definition.isUnion()) { UaGenericUnionValue genericUnion(extensionObject, definition); for (auto &it : elementMap) { @@ -384,16 +465,95 @@ DataElementUaSdk::getOutgoingData () if (isdirty) genericStruct.toExtensionObject(extensionObject); } - if (isdirty) { - if (debug() >= 4) - std::cout << "Encoding changed data structure to outgoingData of element " << name - << std::endl; + + if (isdirty) outgoingData.setExtensionObject(extensionObject, OpcUa_True); - } else { - if (debug() >= 4) - std::cout << "Returning unchanged outgoingData of element " << name - << std::endl; + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedText localizedText; + outgoingData.toLocalizedText(localizedText); + if (!mapped) + createMap(localizedText); + + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { // Scope of Guard G + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + switch (it.first) { + case 0: + localizedText.setLocale(pelem->getOutgoingData().toString()); + break; + case 1: + localizedText.setText(pelem->getOutgoingData().toString()); + break; + } + pelem->isdirty = false; + isdirty = true; + updated = true; + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into LocalizedText" : " ignored (not dirty)") + << std::endl; + } + } + if (isdirty) + outgoingData.setLocalizedText(localizedText); + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedName qualifiedName; + outgoingData.toQualifiedName(qualifiedName); + if (!mapped) + createMap(qualifiedName); + + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { // Scope of Guard G + Guard G(pelem->outgoingLock); + OpcUa_UInt16 ns; + if (pelem->isDirty()) { + switch (it.first) { + case 0: + if (pelem->getOutgoingData().toUInt16(ns) == OpcUa_Good) + qualifiedName.setNamespaceIndex(ns); + break; + case 1: + qualifiedName.setQualifiedName(pelem->getOutgoingData().toString(), qualifiedName.namespaceIndex()); + break; + } + pelem->isdirty = false; + isdirty = true; + updated = true; + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into QualifiedName" : " ignored (not dirty)") + << std::endl; + } } + if (isdirty) + outgoingData.setQualifiedName(qualifiedName); + break; + } + default: + errlogPrintf("%s: %s is no structured data but a %s\n", + pitem->recConnector->getRecordName(), + name.c_str(), + variantTypeString(outgoingData.type())); + return outgoingData; + } + if (debug() >= 4) { + if (isdirty) + std::cout << "Encoding changed data structure to outgoingData of element " << name + << std::endl; + else + std::cout << "Returning unchanged outgoingData of element " << name + << std::endl; } } return outgoingData; @@ -477,11 +637,69 @@ DataElementUaSdk::readScalar (epicsFloat64 *value, return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); } +// Helper functions to convert between OpcUa_ByteString and hex encoded C string +// We use our own functions instead of UaByteString methods toHex/fromHex because +// they work on UaString, not C string, which would require additional copy. +static int printByteString(const OpcUa_ByteString& byteString, char* encoded, int len) +{ + static const char hexdigits[] = "0123456789ABCDEF"; + int i, l = 0; + for (i = 0; i < byteString.Length && l < len-3; i++) { + encoded[l++] = hexdigits[byteString.Data[i] >> 4]; + encoded[l++] = hexdigits[byteString.Data[i] & 0xf]; + } + encoded[l] = 0; // terminate + return l; +} + +// parseByteString accepts hex strings with optional spaces separating units of 1 or multiple of 2 hex digits +static int parseByteString(OpcUa_ByteString& byteString, const char* encoded, int len) +{ + OpcUa_ByteString_Clear(&byteString); + byteString.Data = static_cast(OpcUa_Alloc((len+1)/2)); // can never be longer, shorter does not hurt + if (!byteString.Data) return -1; + int l = 0; + bool firstDigit = true; + while (len--) { + OpcUa_Byte c, b; + c = *encoded++; + if (!c) break; + if (isblank(c)) { + firstDigit = true; + continue; + } + if (!isxdigit(c)) { + // invalid character + OpcUa_ByteString_Clear(&byteString); + return -1; + } + b = c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); + if (len && isxdigit(c = *encoded)) { + firstDigit = false; + encoded++; + len--; + b <<= 4; + b |= c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); + } else { + if (!firstDigit || (c && !isblank(c) && !isxdigit(c))) { + // 1 is the only odd number of digits allowed + // because otherwise where is the byte border? 12|3 or 1|23 ? + OpcUa_ByteString_Clear(&byteString); + return -1; + } + } + byteString.Data[l++] = b; + } + byteString.Length = l; + return l; +} + // CString type needs specialization long -DataElementUaSdk::readScalar (char *value, const size_t num, +DataElementUaSdk::readScalar (char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason, + epicsUInt32 *lenRead, epicsUInt32 *statusCode, char *statusText, const epicsUInt32 statusTextLen) @@ -497,7 +715,7 @@ DataElementUaSdk::readScalar (char *value, const size_t num, ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), "CString", num); + dbgReadScalar(upd.get(), "CString", len); switch (upd->getType()) { case ProcessReason::readFailure: @@ -511,7 +729,7 @@ DataElementUaSdk::readScalar (char *value, const size_t num, case ProcessReason::incomingData: case ProcessReason::readComplete: { - if (num && value) { + if (len && value) { OpcUa_StatusCode stat = upd->getStatus(); if (OpcUa_IsNotGood(stat)) { // No valid OPC UA value @@ -523,16 +741,18 @@ DataElementUaSdk::readScalar (char *value, const size_t num, (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } UaVariant& data = upd->getData(); + UaString str; + const void* s = nullptr; + OpcUa_Int32 n = 0; if (data.type() == OpcUaType_ExtensionObject) { UaExtensionObject extensionObject; data.toExtensionObject(extensionObject); UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); if (definition.isUnion()) { - memset(value, 0, num); UaGenericUnionValue genericValue(extensionObject, definition); int switchValue = genericValue.switchValue(); if (switchValue > 0) { - snprintf(value, num, "%s:%s", + n = snprintf(value, len, "%s:%s", definition.child(switchValue-1).name().toUtf8(), genericValue.value().toString().toUtf8()); } else { @@ -540,20 +760,49 @@ DataElementUaSdk::readScalar (char *value, const size_t num, ret = 1; } } - } else if (enumChoices) { - OpcUa_Int32 enumIndex; - data.toInt32(enumIndex); - auto it = enumChoices->find(enumIndex); - if (it != enumChoices->end() && !it->second.empty()) { - strncpy(value, it->second.c_str(), num); - } else { - strncpy(value, data.toString().toUtf8(), num); - } } else { - strncpy(value, data.toString().toUtf8(), num); + const OpcUa_Variant* variant = data; + switch (variant->Datatype) { + case OpcUaType_Byte: + case OpcUaType_SByte: + if (variant->ArrayType == OpcUa_VariantArrayType_Array) { + n = variant->Value.Array.Length; + s = variant->Value.Array.Value.Array; + } else if (variant->ArrayType == OpcUa_VariantArrayType_Matrix) { + OpcUa_VariantMatrix_GetElementCount(&variant->Value.Matrix, &n); + s = variant->Value.Matrix.Value.Array; + } + if (static_cast(n) > len) + n = len; + break; + case OpcUaType_ByteString: + n = printByteString(variant->Value.ByteString, value, len); + } + if (enumChoices) { + OpcUa_Int32 enumIndex; + data.toInt32(enumIndex); + auto it = enumChoices->find(enumIndex); + if (it != enumChoices->end() && !it->second.empty()) { + s = it->second.c_str(); + n = static_cast(it->second.length()); + if (static_cast(n) >= len) + n = len-1; + } + } + if (!n) { + str = data.toString(); + s = str.toUtf8(); + n = str.length(); + if (static_cast(n) >= len) + n = len-1; + } + if (s) + memcpy(value, s, n); } - value[num-1] = '\0'; + memset(value+n, 0, len-n); prec->udf = false; + if (lenRead) + *lenRead = n; } if (statusCode) *statusCode = stat; if (statusText) { @@ -603,7 +852,7 @@ DataElementUaSdk::dbgReadArray (const UpdateUaSdk *upd, // Read array for EPICS String / OpcUa_String long int -DataElementUaSdk::readArray (char *value, const epicsUInt32 len, +DataElementUaSdk::readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, OpcUa_BuiltInType expectedType, @@ -651,21 +900,66 @@ DataElementUaSdk::readArray (char *value, const epicsUInt32 len, errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (data.type() != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; } else { if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - UaStringArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); - (value + i * len)[len - 1] = '\0'; + switch (data.type()) { + case OpcUaType_String: { + UaStringArray arr; + UaVariant_to(upd->getData(), arr); + elemsWritten = num < arr.length() ? num : arr.length(); + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); + (value + i * len)[len - 1] = '\0'; + } + break; + } + case OpcUaType_XmlElement: { + UaXmlElementArray arr; + UaVariant_to(upd->getData(), arr); + elemsWritten = num < arr.length() ? num : arr.length(); + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); + (value + i * len)[len - 1] = '\0'; + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedTextArray arr; + UaVariant_to(upd->getData(), arr); + elemsWritten = num < arr.length() ? num : arr.length(); + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + strncpy(value + i * len, UaString(arr[i].Text).toUtf8(), len); + (value + i * len)[len - 1] = '\0'; + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedNameArray arr; + UaVariant_to(upd->getData(), arr); + elemsWritten = num < arr.length() ? num : arr.length(); + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + strncpy(value + i * len, UaString(arr[i].Name).toUtf8(), len); + (value + i * len)[len - 1] = '\0'; + } + break; + } + case OpcUaType_ByteString: { + UaByteStringArray arr; + upd->getData().toByteStringArray(arr); + elemsWritten = num < arr.length() ? num : arr.length(); + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + int l = printByteString(arr[i], value + i * len, len); + memset(value + i * len + l, 0, len - l); + } + break; + } + default: + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(data.type()), epicsTypeString(value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; } prec->udf = false; } @@ -737,29 +1031,29 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e ret = 1; } else { // Valid OPC UA value, so try to convert - UaVariant &data = upd->getData(); - if (!data.isArray()) { + UaVariant &variant = upd->getData(); + if (!variant.isArray() && variant.type() != OpcUaType_ByteString) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (data.type() != OpcUaType_Byte && data.type() != OpcUaType_Boolean) { + } else if (variant.type() != expectedType && variant.type() != OpcUaType_Boolean && variant.type() != OpcUaType_ByteString) { errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(*value)); + prec->name, variantTypeString(variant.type()), epicsTypeString(*value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; } else { if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - if (data.type() == OpcUaType_Byte) { + if (variant.type() != OpcUaType_Boolean) { UaByteArray arr; - UaVariant_to(upd->getData(), arr); + UaVariant_to(variant, arr); // works for ByteArray and ByteString ! elemsWritten = static_cast(arr.size()); if (num < elemsWritten) elemsWritten = num; memcpy(value, arr.data(), sizeof(epicsUInt8) * elemsWritten); } else { UaBooleanArray arr; - UaVariant_to(upd->getData(), arr); + UaVariant_to(variant, arr); elemsWritten = static_cast(arr.length()); if (num < elemsWritten) elemsWritten = num; memcpy(value, arr.rawData(), sizeof(epicsUInt8) * elemsWritten); @@ -907,7 +1201,7 @@ DataElementUaSdk::readArray (epicsFloat64 *value, const epicsUInt32 num, } long -DataElementUaSdk::readArray (char *value, const epicsUInt32 len, +DataElementUaSdk::readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, @@ -959,7 +1253,7 @@ DataElementUaSdk::writeScalar (const epicsFloat64 &value, dbCommon *prec) } long -DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommon *prec) +DataElementUaSdk::writeScalar (const char *value, epicsUInt32 len, dbCommon *prec) { long ret = 1; long l; @@ -1011,7 +1305,15 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo case OpcUaType_String: { // Scope of Guard G Guard G(outgoingLock); - outgoingData.setString(static_cast(value)); + outgoingData.setString(UaByteString(len, (OpcUa_Byte*)value)); // UaByteString supports len + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_XmlElement: + { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setXmlElement(UaByteString(len, (OpcUa_Byte*)value)); markAsDirty(); ret = 0; break; @@ -1020,13 +1322,52 @@ DataElementUaSdk::writeScalar (const char *value, const epicsUInt32 len, dbCommo { // Scope of Guard G Guard G(outgoingLock); UaLocalizedText localizedText; - incomingData.toLocalizedText(localizedText); - localizedText.setText(static_cast(value)); // keep the Locale + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + localizedText.setLocale(UaByteString(static_cast(sep - value), (OpcUa_Byte*)value)); + value = sep + 1; + len -= static_cast(sep + 1 - value); + } else { // keep the locale + incomingData.toLocalizedText(localizedText); + } + localizedText.setText(UaByteString(len, (OpcUa_Byte*)value)); outgoingData.setLocalizedText(localizedText); markAsDirty(); ret = 0; break; } + case OpcUaType_QualifiedName: + { // Scope of Guard G + Guard G(outgoingLock); + UaQualifiedName qualifiedName; + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + qualifiedName.setNamespaceIndex(atoi(value)); + value = sep + 1; + len -= static_cast(sep + 1 - value); + } else { // keep the namespace + incomingData.toQualifiedName(qualifiedName); + } + qualifiedName.setQualifiedName(UaByteString(len, (OpcUa_Byte*)value), qualifiedName.namespaceIndex()); + outgoingData.setQualifiedName(qualifiedName); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_ByteString: + { // Scope of Guard G + OpcUa_ByteString bs; + OpcUa_ByteString_Initialize(&bs); + if (parseByteString(bs, value, len) >= 0) { + UaByteString byteString; + byteString.attach(&bs); // wrap, no copy + Guard G(outgoingLock); + outgoingData.setByteString(byteString, true); // move, no copy + markAsDirty(); + ret = 0; + } + break; + } case OpcUaType_Boolean: { // Scope of Guard G Guard G(outgoingLock); @@ -1180,7 +1521,7 @@ DataElementUaSdk::dbgWriteArray (const epicsUInt32 targetSize, const std::string // Write array for EPICS String / OpcUa_String long -DataElementUaSdk::writeArray (const char *value, const epicsUInt32 len, +DataElementUaSdk::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec) @@ -1191,39 +1532,93 @@ DataElementUaSdk::writeArray (const char *value, const epicsUInt32 len, errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; - } else if (incomingData.type() != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; } else { - UaStringArray arr; - arr.create(static_cast(num)); - for (epicsUInt32 i = 0; i < num; i++) { - char *val = nullptr; - const char *pval; - // add zero termination if necessary - if (value[len-1] != '\0') { - val = new char[len+1]; - strncpy(val, value, len); - val[len] = '\0'; - pval = val; - } else { - pval = value; + switch (incomingData.type()) { + case OpcUaType_String: { + UaStringArray arr; + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + UaString(UaByteString(len, (OpcUa_Byte*)value + i * len)).copyTo(&arr[i]); } - UaString(pval).copyTo(&arr[i]); - delete[] val; - value += len; + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; } - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); + case OpcUaType_XmlElement: { + UaXmlElementArray arr; + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + UaByteString(len, (OpcUa_Byte*)value + i * len).copyTo(&arr[i]); + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedTextArray arr; + UaLocalizedTextArray incoming; + UaVariant_to(incomingData, incoming); + + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + UaString(UaByteString(static_cast(sep - value), (OpcUa_Byte*)value)).copyTo(&arr[i].Locale); + } else if (i < incoming.length()) { + UaString(incoming[i].Locale).copyTo(&arr[i].Locale); + } else if (i > 0) { + UaString(arr[i-1].Locale).copyTo(&arr[i].Locale); + } + UaString(UaByteString(len - (sep ? static_cast(sep+1-value) : 0), (OpcUa_Byte*)(sep ? sep+1 : value))).copyTo(&arr[i].Text); + value += len; + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedNameArray arr; + UaQualifiedNameArray incoming; + UaVariant_to(incomingData, incoming); + + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].NamespaceIndex = atoi(value); + } else if (i < incoming.length()) { + arr[i].NamespaceIndex = incoming[i].NamespaceIndex; + } else if (i > 0) { + arr[i].NamespaceIndex = arr[i-1].NamespaceIndex; + } + UaString(UaByteString(len - (sep ? static_cast(sep+1-value) : 0), (OpcUa_Byte*)(sep ? sep+1 : value))).copyTo(&arr[i].Name); + value += len; + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + default: + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; } - dbgWriteArray(num, epicsTypeString(value)); } return ret; @@ -1241,7 +1636,16 @@ DataElementUaSdk::writeArray (const epicsUI { long ret = 0; - if (!incomingData.isArray()) { + if (!incomingData.isArray() && incomingData.type() == OpcUaType_ByteString) { + UaByteString bs(num, reinterpret_cast(const_cast(value))); + { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setByteString(bs, true); // move, no copy + markAsDirty(); + } + + dbgWriteScalar(); + } else if (!incomingData.isArray()) { errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; @@ -1335,7 +1739,7 @@ DataElementUaSdk::writeArray (const epicsFloat64 *value, const epicsUInt32 num, } long -DataElementUaSdk::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) +DataElementUaSdk::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, len, num, OpcUaType_String, prec); } diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index 3ba64b3b..2889cabb 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -331,9 +331,10 @@ class DataElementUaSdk : public DataElement * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) */ - virtual long int readScalar(char *value, const size_t num, + virtual long int readScalar(char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason = nullptr, + epicsUInt32 *lenRead = nullptr, epicsUInt32 *statusCode = nullptr, char *statusText = nullptr, const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; @@ -484,7 +485,7 @@ class DataElementUaSdk : public DataElement * See the DataElement API method it overrides * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) */ - virtual long int readArray(char *value, const epicsUInt32 len, + virtual long int readArray(char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, @@ -645,7 +646,7 @@ class DataElementUaSdk : public DataElement * See the DataElement API method it overrides * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) */ - virtual long int writeArray(const char *value, const epicsUInt32 len, + virtual long int writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) override; @@ -683,6 +684,10 @@ class DataElementUaSdk : public DataElement const std::string &targetTypeName) const; void checkWriteArray(OpcUa_BuiltInType expectedType, const std::string &targetTypeName) const; void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; + void createMap(const UaStructureDefinition& definition, const std::string *timefrom = nullptr); + void createMap(const UaLocalizedText&); + void createMap(const UaQualifiedName&); + // Structure always returns true to ensure full traversal bool isDirty() const { return isdirty || !isleaf; } void @@ -741,6 +746,9 @@ class DataElementUaSdk : public DataElement OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaFloatArray &value) { return variant.toFloatArray(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaDoubleArray &value) { return variant.toDoubleArray(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaStringArray &value) { return variant.toStringArray(value); } + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaXmlElementArray &value) { return variant.toXmlElementArray(value); } + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaLocalizedTextArray &value) { return variant.toLocalizedTextArray(value); } + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaQualifiedNameArray &value) { return variant.toQualifiedNameArray(value); } void UaVariant_set(UaVariant &variant, UaBooleanArray &value) { variant.setBoolArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaSByteArray &value) { variant.setSByteArray(value, OpcUa_True); } @@ -754,6 +762,9 @@ class DataElementUaSdk : public DataElement void UaVariant_set(UaVariant &variant, UaFloatArray &value) { variant.setFloatArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaDoubleArray &value) { variant.setDoubleArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaStringArray &value) { variant.setStringArray(value, OpcUa_True); } + void UaVariant_set(UaVariant &variant, UaXmlElementArray &value) { variant.setXmlElementArray(value, OpcUa_True); } + void UaVariant_set(UaVariant &variant, UaLocalizedTextArray &value) { variant.setLocalizedTextArray(value, OpcUa_True); } + void UaVariant_set(UaVariant &variant, UaQualifiedNameArray &value) { variant.setQualifiedNameArray(value, OpcUa_True); } // Read scalar value as templated function on EPICS type and OPC UA type // value == nullptr is allowed and leads to the value being dropped (ignored), @@ -878,14 +889,14 @@ class DataElementUaSdk : public DataElement ret = 1; } else { // Valid OPC UA value, so try to convert - UaVariant &data = upd->getData(); - if (!data.isArray()) { + UaVariant &variant = upd->getData(); + if (!variant.isArray()) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (data.type() != expectedType) { + } else if (variant.type() != expectedType) { errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(*value)); + prec->name, variantTypeString(variant.type()), epicsTypeString(*value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; } else { @@ -920,7 +931,7 @@ class DataElementUaSdk : public DataElement // Read array value for EPICS String / OpcUa_String long - readArray (char *value, const epicsUInt32 len, + readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, OpcUa_BuiltInType expectedType, @@ -1054,7 +1065,7 @@ class DataElementUaSdk : public DataElement // Write array value for EPICS String / OpcUa_String long - writeArray (const char *value, const epicsUInt32 len, + writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec); diff --git a/devOpcuaSup/devOpcua.cpp b/devOpcuaSup/devOpcua.cpp index 967c4016..3e5b4cb7 100644 --- a/devOpcuaSup/devOpcua.cpp +++ b/devOpcuaSup/devOpcua.cpp @@ -949,8 +949,8 @@ opcua_read_string_val (REC *prec) } } else { char *value = useReadValue(pcon) ? prec->val : nullptr; - unsigned short num = value ? MAX_STRING_SIZE : 0; - ret = pcon->readScalar(value, num, &nextReason); + unsigned short len = value ? MAX_STRING_SIZE : 0; + ret = pcon->readScalar(value, len, &nextReason); traceReadPrint(pdbc, pcon, ret, !!value, prec->val); manageStateAndBiniProcessing(pcon); } @@ -1015,9 +1015,7 @@ opcua_read_lstring_val (REC *prec) } } else { char *value = useReadValue(pcon) ? prec->val : nullptr; - ret = pcon->readScalar(value, prec->sizv, &nextReason); - if (value) - prec->len = static_cast(strlen(prec->val) + 1); + ret = pcon->readScalar(value, prec->sizv, &nextReason, &prec->len); traceReadPrint(pdbc, pcon, ret, !!value, prec->val); manageStateAndBiniProcessing(pcon); } @@ -1051,9 +1049,7 @@ opcua_write_lstring_val (REC *prec) } } else { char *value = useReadValue(pcon) ? prec->val : nullptr; - ret = pcon->readScalar(value, prec->sizv, &nextReason); - if (value) - prec->len = static_cast(strlen(prec->val) + 1); + ret = pcon->readScalar(value, prec->sizv, &nextReason, &prec->len); traceReadPrint(pdbc, pcon, ret, !!value, prec->val); manageStateAndBiniProcessing(pcon); } @@ -1085,19 +1081,14 @@ opcua_read_array (REC *prec) } else { void *bptr = useReadValue(pcon) ? prec->bptr : nullptr; switch (prec->ftvl) { + case menuFtypeCHAR: + /* Allow to handle CHAR arrays as long strings */ + ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason, &prec->nord); + break; case menuFtypeSTRING: ret = pcon->readArray(static_cast(bptr), MAX_STRING_SIZE, prec->nelm, &prec->nord, &nextReason); break; - case menuFtypeCHAR: - if (pcon->pdataelement->enumChoices) { - ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason); - if (bptr) prec->nord = static_cast(strlen(static_cast(bptr))); - break; - } - ret = pcon->readArray(static_cast(bptr), prec->nelm, - &prec->nord, &nextReason); - break; case menuFtypeUCHAR: ret = pcon->readArray(static_cast(bptr), prec->nelm, &prec->nord, &nextReason); @@ -1168,16 +1159,13 @@ opcua_write_array (REC *prec) } else if (pcon->state() != ConnectionStatus::initialRead) { traceWriteArrayPrint(pdbc, pcon, prec->nord); switch (prec->ftvl) { + case menuFtypeCHAR: + /* Allow to handle CHAR arrays as long strings */ + ret = pcon->writeScalar(static_cast(prec->bptr), prec->nord); + break; case menuFtypeSTRING: ret = pcon->writeArray(static_cast(prec->bptr), MAX_STRING_SIZE, prec->nord); break; - case menuFtypeCHAR: - if (pcon->pdataelement->enumChoices) { - ret = pcon->writeScalar(static_cast(prec->bptr), prec->nord); - break; - } - ret = pcon->writeArray(static_cast(prec->bptr), prec->nord); - break; case menuFtypeUCHAR: ret = pcon->writeArray(static_cast(prec->bptr), prec->nord); break; @@ -1221,19 +1209,14 @@ opcua_write_array (REC *prec) } else { void *bptr = useReadValue(pcon) ? prec->bptr : nullptr; switch (prec->ftvl) { + case menuFtypeCHAR: + /* Allow to handle CHAR arrays as long strings */ + ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason, &prec->nord); + break; case menuFtypeSTRING: ret = pcon->readArray(static_cast(bptr), MAX_STRING_SIZE, prec->nelm, &prec->nord, &nextReason); break; - case menuFtypeCHAR: - if (pcon->pdataelement->enumChoices) { - ret = pcon->readScalar(static_cast(bptr), prec->nelm, &nextReason); - if (bptr) prec->nord = static_cast(strlen(static_cast(bptr))); - break; - } - ret = pcon->readArray(static_cast(bptr), prec->nelm, - &prec->nord, &nextReason); - break; case menuFtypeUCHAR: ret = pcon->readArray(static_cast(bptr), prec->nelm, &prec->nord, &nextReason); diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 32590d4b..300e85ec 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -125,50 +125,78 @@ UA_UInt32 UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, size_t *outOffset, const UA_DataType **outMemberType, UA_Boolean *outIsArray, UA_Boolean *outIsOptional) { - if(type->typeKind != UA_DATATYPEKIND_STRUCTURE && - type->typeKind != UA_DATATYPEKIND_OPTSTRUCT && - type->typeKind != UA_DATATYPEKIND_UNION) - return 0; size_t offset = 0; - for(UA_UInt32 i = 0; i < type->membersSize; ++i) { - const UA_DataTypeMember *m = &type->members[i]; - const UA_DataType *mt = memberTypeOf(type, m); - offset += m->padding; - - if(strcmp(memberName, m->memberName) == 0) { - *outOffset = offset; - *outMemberType = mt; - *outIsArray = m->isArray; - *outIsOptional = m->isOptional; - return i+1; - } - - if (type->typeKind == UA_DATATYPEKIND_UNION) { - offset = 0; - } else if(!m->isOptional) { - if(!m->isArray) { - offset += mt->memSize; - } else { - offset += sizeof(size_t); - offset += sizeof(void*); + switch(type->typeKind) { + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + case UA_DATATYPEKIND_UNION: + for(UA_UInt32 i = 0; i < type->membersSize; ++i) { + const UA_DataTypeMember *m = &type->members[i]; + const UA_DataType *mt = memberTypeOf(type, m); + offset += m->padding; + + if(strcmp(memberName, m->memberName) == 0) { + *outOffset = offset; + *outMemberType = mt; + *outIsArray = m->isArray; + *outIsOptional = m->isOptional; + return i+1; } - } else { /* field is optional */ - if(!m->isArray) { - offset += sizeof(void *); - } else { - offset += sizeof(size_t); - offset += sizeof(void *); + + if (type->typeKind == UA_DATATYPEKIND_UNION) { + offset = 0; + } else if(!m->isOptional) { + if(!m->isArray) { + offset += mt->memSize; + } else { + offset += sizeof(size_t); + offset += sizeof(void*); + } + } else { /* field is optional */ + if(!m->isArray) { + offset += sizeof(void *); + } else { + offset += sizeof(size_t); + offset += sizeof(void *); + } } } + break; + case UA_DATATYPEKIND_LOCALIZEDTEXT: + *outMemberType = &UA_TYPES[UA_TYPES_STRING]; + *outIsArray = 0; + *outIsOptional = 0; + if (strcmp(memberName, "locale") == 0) { + *outOffset = offsetof(UA_LocalizedText, locale); + return 1; + } + if (strcmp(memberName, "text") == 0) { + *outOffset = offsetof(UA_LocalizedText, text); + return 2; + } + break; + case UA_DATATYPEKIND_QUALIFIEDNAME: + *outIsArray = 0; + *outIsOptional = 0; + if (strcmp(memberName, "namespaceIndex") == 0) { + *outMemberType = &UA_TYPES[UA_DATATYPEKIND_UINT16]; + *outOffset = offsetof(UA_QualifiedName, namespaceIndex); + return 1; + } + if (strcmp(memberName, "name") == 0) { + *outMemberType = &UA_TYPES[UA_TYPES_STRING]; + *outOffset = offsetof(UA_QualifiedName, name); + return 2; + } + break; } - return 0; } void -DataElementOpen62541::createMap (const UA_DataType *type, - const std::string *timefrom) +DataElementOpen62541::createMap(const UA_DataType *type, + const std::string *timefrom) { if (debug() >= 5) std::cout << " ** creating index-to-element map for child elements" << std::endl; @@ -177,6 +205,8 @@ DataElementOpen62541::createMap (const UA_DataType *type, case UA_DATATYPEKIND_STRUCTURE: case UA_DATATYPEKIND_OPTSTRUCT: case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_LOCALIZEDTEXT: + case UA_DATATYPEKIND_QUALIFIEDNAME: if (timefrom) { const UA_DataType *timeMemberType; UA_Boolean timeIsArray; @@ -239,7 +269,7 @@ DataElementOpen62541::createMap (const UA_DataType *type, break; default: std::cerr << "Error: " << this - << " is not a structure or an optstruct but a " << typeKindName(typeKindOf(type)) + << " is no structured data but a " << typeKindName(typeKindOf(type)) << std::endl; } mapped = true; @@ -482,7 +512,7 @@ DataElementOpen62541::getOutgoingData () } if (!mapped) - createMap(type, nullptr); + createMap(type); for (auto &it : elements) { auto pelem = it.lock(); @@ -576,11 +606,66 @@ DataElementOpen62541::readScalar (epicsFloat64 *value, return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); } +// Helper functions to convert between UA_ByteString and hex encoded C string +static size_t printByteString(const UA_ByteString& byteString, char* encoded, size_t len) +{ + static const char hexdigits[] = "0123456789ABCDEF"; + size_t i, l = 0; + for (i = 0; i < byteString.length && l < len-3; i++) { + encoded[l++] = hexdigits[byteString.data[i] >> 4]; + encoded[l++] = hexdigits[byteString.data[i] & 0xf]; + } + encoded[l] = 0; + return l; +} + +static int parseByteString(UA_ByteString& byteString, const char* encoded, int len) +{ + UA_ByteString_clear(&byteString); + byteString.data = static_cast(UA_malloc(len+1/2)); // can never be longer + if (!byteString.data) return -1; + int l = 0; + bool firstDigit = true; + while (len--) { + UA_Byte c, b; + c = *encoded++; + if (!c) break; + if (isblank(c)) { + firstDigit = true; + continue; + } + if (!isxdigit(c)) { + // invalid character + UA_ByteString_clear(&byteString); + return -1; + } + b = c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); + if (len && isxdigit(c = *encoded)) { + firstDigit = false; + encoded++; + len--; + b <<= 4; + b |= c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); + } else { + if (!firstDigit || (c && !isblank(c) && !isxdigit(c))) { + // 1 is the only odd number of digits allowed + // because otherwise where is the byte border? 12|3 or 1|23 ? + UA_ByteString_clear(&byteString); + return -1; + } + } + byteString.data[l++] = b; + } + byteString.length = l; + return l; +} + // CString type needs specialization long -DataElementOpen62541::readScalar (char *value, const size_t num, +DataElementOpen62541::readScalar (char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason, + epicsUInt32 *lenRead, epicsUInt32 *statusCode, char *statusText, const epicsUInt32 statusTextLen) @@ -596,7 +681,7 @@ DataElementOpen62541::readScalar (char *value, const size_t num, ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), "CString", num); + dbgReadScalar(upd.get(), "CString", len); switch (upd->getType()) { case ProcessReason::readFailure: @@ -610,7 +695,7 @@ DataElementOpen62541::readScalar (char *value, const size_t num, case ProcessReason::incomingData: case ProcessReason::readComplete: { - if (num && value) { + if (len && value) { UA_StatusCode stat = upd->getStatus(); if (UA_STATUS_IS_BAD(stat)) { // No valid OPC UA value @@ -624,11 +709,11 @@ DataElementOpen62541::readScalar (char *value, const size_t num, UA_String buffer = UA_STRING_NULL; UA_String *datastring = &buffer; - size_t n = num-1; + size_t n = len-1; - UA_Variant &data = upd->getData(); - void* payload = data.data; - const UA_DataType* type = data.type; + UA_Variant &variant = upd->getData(); + void* payload = variant.data; + const UA_DataType* type = variant.type; if (type->typeKind == UA_DATATYPEKIND_UNION) { @@ -642,20 +727,32 @@ DataElementOpen62541::readScalar (char *value, const size_t num, type = memberTypeOf(type, member); // prefix value string with switch choice name - size_t len = snprintf(value, n, "%s:", member->memberName); - value += len; - n -= len; + size_t l = snprintf(value, n, "%s:", member->memberName); + value += l; + n -= l; } switch (type->typeKind) { case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: { - datastring = static_cast(payload); + datastring = static_cast(payload); break; } case UA_DATATYPEKIND_LOCALIZEDTEXT: { - datastring = &static_cast(payload)->text; + datastring = &static_cast(payload)->text; + break; + } + case UA_DATATYPEKIND_QUALIFIEDNAME: + { + datastring = &static_cast(payload)->name; + break; + } + case UA_DATATYPEKIND_BYTESTRING: + { + n = printByteString(*static_cast(payload), value, len); + datastring = nullptr; break; } case UA_DATATYPEKIND_DATETIME: @@ -667,6 +764,15 @@ DataElementOpen62541::readScalar (char *value, const size_t num, UA_print(&dt, type, &buffer); break; } + case UA_DATATYPEKIND_BYTE: + case UA_DATATYPEKIND_SBYTE: + { + buffer.data = static_cast(payload); + buffer.length = UA_Variant_isScalar(&variant) ? 1 : variant.arrayLength; + variant.storageType = UA_VARIANT_DATA_NODELETE; // we have moved ownership + n++; + break; + } case UA_DATATYPEKIND_ENUM: case UA_DATATYPEKIND_INT32: { @@ -683,13 +789,17 @@ DataElementOpen62541::readScalar (char *value, const size_t num, if (type) UA_print(payload, type, &buffer); }; - if (n > datastring->length) - n = datastring->length; - strncpy(value, reinterpret_cast(datastring->data), n); - value[n] = '\0'; + if (datastring) { + if (n > datastring->length) + n = datastring->length; + memcpy(value, reinterpret_cast(datastring->data), n); + } + memset(value+n, 0, len-n); + if (lenRead) + *lenRead = static_cast(n); UA_String_clear(&buffer); prec->udf = false; - UA_Variant_clear(&data); + UA_Variant_clear(&variant); } if (statusCode) *statusCode = stat; if (statusText) { @@ -739,7 +849,7 @@ DataElementOpen62541::dbgReadArray (const UpdateOpen62541 *upd, // Read array for EPICS String / UA_String long int -DataElementOpen62541::readArray (char *value, const epicsUInt32 len, +DataElementOpen62541::readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, const UA_DataType *expectedType, @@ -782,28 +892,60 @@ DataElementOpen62541::readArray (char *value, const epicsUInt32 len, ret = 1; } else { // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - if (UA_Variant_isScalar(&data)) { + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant)) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (data.type != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data), epicsTypeString(value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; } else { if (UA_STATUS_IS_UNCERTAIN(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - elemsWritten = static_cast(num < data.arrayLength ? num : data.arrayLength); - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, reinterpret_cast(static_cast(data.data)[i].data), len); - (value + i * len)[len - 1] = '\0'; + elemsWritten = static_cast(num < variant.arrayLength ? num : variant.arrayLength); + switch (typeKindOf(variant.type)) { + case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + const UA_String& str = static_cast(variant.data)[i]; + size_t l = str.length; + if (l >= len) l = len-1; + strncpy(value + i * len, reinterpret_cast(str.data), l); + (value + i * len)[l] = '\0'; + } + break; + case UA_DATATYPEKIND_LOCALIZEDTEXT: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + const UA_String& str = static_cast(variant.data)[i].text; + size_t l = str.length; + if (l >= len) l = len-1; + strncpy(value + i * len, reinterpret_cast(str.data), l); + (value + i * len)[l] = '\0'; + } + break; + case UA_DATATYPEKIND_QUALIFIEDNAME: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + const UA_String& str = static_cast(variant.data)[i].name; + size_t l = str.length; + if (l >= len) l = len-1; + strncpy(value + i * len, reinterpret_cast(str.data), l); + (value + i * len)[l] = '\0'; + } + break; + case UA_DATATYPEKIND_BYTESTRING: + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + size_t l = printByteString(static_cast(variant.data)[i], value + i * len, len); + memset(value + i * len + l, 0, len - l); + } + break; + default: + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(variant), epicsTypeString(value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; } prec->udf = false; } - UA_Variant_clear(&data); + UA_Variant_clear(&variant); } if (statusCode) *statusCode = stat; if (statusText) { @@ -871,25 +1013,33 @@ DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt3 ret = 1; } else { // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - if (UA_Variant_isScalar(&data)) { + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant) && variant.type == &UA_TYPES[UA_TYPES_BYTESTRING]) { + const UA_ByteString& bs = *static_cast(variant.data); + elemsWritten = static_cast(bs.length); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, bs.data, elemsWritten); + } else if (UA_Variant_isScalar(&variant)) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (typeKindOf(data) != UA_DATATYPEKIND_BYTE && typeKindOf(data) != UA_DATATYPEKIND_BOOLEAN) { + } else if (typeKindOf(variant) != UA_DATATYPEKIND_BYTE && typeKindOf(variant) != UA_DATATYPEKIND_BOOLEAN) { errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data), epicsTypeString(*value)); + prec->name, variantTypeString(variant), epicsTypeString(*value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; } else { if (UA_STATUS_IS_UNCERTAIN(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - elemsWritten = static_cast(num) < data.arrayLength ? num : static_cast(data.arrayLength); - memcpy(value, data.data, elemsWritten); + elemsWritten = static_cast(variant.arrayLength); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, variant.data, elemsWritten); prec->udf = false; } - UA_Variant_clear(&data); + UA_Variant_clear(&variant); } if (statusCode) *statusCode = stat; if (statusText) { @@ -1031,7 +1181,7 @@ DataElementOpen62541::readArray (epicsFloat64 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (char *value, const epicsUInt32 len, +DataElementOpen62541::readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, @@ -1098,7 +1248,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon if (value[0] == '\0') { switchfield = 0; } else for (UA_UInt32 i = 0; i < type->membersSize; i++) { - size_t namelen = strlen(type->members[i].memberName); + epicsUInt32 namelen = static_cast(strlen(type->members[i].memberName)); if (strncmp(value, type->members[i].memberName, namelen) == 0 && value[namelen] == ':') { value += namelen+1; @@ -1111,11 +1261,12 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon switch (typeKindOf(type)) { case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: { UA_String val; val.length = strnlen(value, len); val.data = const_cast(reinterpret_cast(value)); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; break; @@ -1123,18 +1274,55 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon case UA_DATATYPEKIND_LOCALIZEDTEXT: { UA_LocalizedText val; - val.locale = reinterpret_cast(incomingData.data)->locale; + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + val.locale.length = sep - value; + val.locale.data = const_cast(reinterpret_cast(value)); + len -= static_cast(sep + 1 - value); + value = sep + 1; + } else { // keep the locale + val.locale = reinterpret_cast(incomingData.data)->locale; + } val.text.length = strnlen(value, len); val.text.data = const_cast(reinterpret_cast(value)); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; break; } + case UA_DATATYPEKIND_QUALIFIEDNAME: + { + UA_QualifiedName val; + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + val.namespaceIndex = atoi(value); + len -= static_cast(sep + 1 - value); + value = sep + 1; + } else { // keep the namespace + val.namespaceIndex = reinterpret_cast(incomingData.data)->namespaceIndex; + } + val.name.length = strnlen(value, len); + val.name.data = const_cast(reinterpret_cast(value)); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_BYTESTRING: + { + UA_ByteString val; + UA_ByteString_init(&val); + if (parseByteString(val, value, len) >= 0) { + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); + markAsDirty(); + ret = 0; + } + break; + } case UA_DATATYPEKIND_BOOLEAN: { UA_Boolean val = strchr("YyTt1", *value) != NULL; - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; break; @@ -1143,7 +1331,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon ul = strtoul(value, &end, 0); if (end != value && isWithinRange(ul)) { UA_Byte val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1152,7 +1340,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon l = strtol(value, &end, 0); if (end != value && isWithinRange(l)) { UA_SByte val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1161,7 +1349,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon ul = strtoul(value, &end, 0); if (end != value && isWithinRange(ul)) { UA_UInt16 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1170,7 +1358,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon l = strtol(value, &end, 0); if (end != value && isWithinRange(l)) { UA_Int16 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1179,7 +1367,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon ul = strtoul(value, &end, 0); if (end != value && isWithinRange(ul)) { UA_UInt32 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1217,7 +1405,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon ul = strtoul(value, &end, 0); if (end != value && isWithinRange(ul)) { UA_UInt64 val = static_cast(ul); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1226,7 +1414,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon l = strtol(value, &end, 0); if (end != value && isWithinRange(l)) { UA_Int64 val = static_cast(l); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1235,7 +1423,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon d = strtod(value, &end); if (end != value && isWithinRange(d)) { UA_Float val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); markAsDirty(); ret = 0; } @@ -1245,7 +1433,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon d = strtod(value, &end); if (end != value) { UA_Double val = static_cast(d); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); + status = UA_Variant_setScalarCopy(&outgoingData, &val, type); } break; } @@ -1256,15 +1444,15 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon } if (switchfield >= 0) { // manually wrap value from outgoingData into union - void *p = UA_new(incomingData.type); + void *p = UA_new(type); if (p) { *static_cast(p) = switchfield; if (switchfield > 0) { - memcpy(static_cast(p) + incomingData.type->members[switchfield-1].padding, + memcpy(static_cast(p) + type->members[switchfield-1].padding, outgoingData.data, outgoingData.type->memSize); UA_free(outgoingData.data); } - UA_Variant_setScalar(&outgoingData, p, incomingData.type); + UA_Variant_setScalar(&outgoingData, p, type); status = UA_STATUSCODE_GOOD; markAsDirty(); ret = 0; @@ -1299,9 +1487,19 @@ DataElementOpen62541::dbgWriteArray (const epicsUInt32 targetSize, const std::st } } +static inline +UA_String UA_StringNCopy(const char *src, size_t maxlength) +{ + UA_String s; + s.length = src ? strnlen(src, maxlength) : 0; + s.data = s.length ? static_cast(UA_malloc(s.length)) : nullptr; + if (s.data) memcpy(s.data, src, s.length); + return s; +} + // Write array for EPICS String / UA_String long -DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, +DataElementOpen62541::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, const UA_DataType *targetType, dbCommon *prec) @@ -1312,33 +1510,75 @@ DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; - } else if (incomingData.type != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData), - variantTypeString(targetType), - epicsTypeString(value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; } else { - UA_String *arr = static_cast(UA_Array_new(num, &UA_TYPES[UA_TYPES_STRING])); - if (!arr) { + const UA_DataType* type = incomingData.type; + void* data = UA_Array_new(num, type); + if (!data) { errlogPrintf("%s : out of memory\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; } else { - for (epicsUInt32 i = 0; i < num; i++) { - arr[i].length = strnlen(value, len); - if (arr[i].length) { - arr[i].data = static_cast(UA_malloc(arr[i].length)); - memcpy(arr[i].data, value, arr[i].length); + switch(typeKindOf(type)) { + case UA_DATATYPEKIND_STRING: + case UA_DATATYPEKIND_XMLELEMENT: + case UA_DATATYPEKIND_BYTESTRING: { + UA_String *arr = static_cast(data); + for (epicsUInt32 i = 0; i < num; i++) { + arr[i] = UA_StringNCopy(value, len); + value += len; + } + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: { + UA_LocalizedText *arr = static_cast(data); + for (epicsUInt32 i = 0; i < num; i++) { + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].locale = UA_StringNCopy(value, sep - value); + } else if (i < incomingData.arrayLength) { + UA_copy(&reinterpret_cast( + incomingData.data)[i].locale, + &arr[i].locale, &UA_TYPES[UA_TYPES_STRING]); + } else if (i > 0) { + UA_copy(&arr[i-1].locale, + &arr[i].locale, &UA_TYPES[UA_TYPES_STRING]); + } + arr[i].text = UA_StringNCopy(sep ? sep+1 : value, sep ? len - (sep+1-value) : len); + value += len; + } + break; + } + case UA_DATATYPEKIND_QUALIFIEDNAME: { + UA_QualifiedName *arr = static_cast(data); + for (epicsUInt32 i = 0; i < num; i++) { + const char* sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].namespaceIndex = atoi(value); + } else if (i < incomingData.arrayLength) { + arr[i].namespaceIndex = reinterpret_cast( + incomingData.data)[i].namespaceIndex; + } else if (i > 0) { + arr[i].namespaceIndex = arr[i-1].namespaceIndex; + } + arr[i].name = UA_StringNCopy(sep ? sep+1 : value, sep ? len - (sep+1-value) : len); + value += len; } - value += len; + break; + } + default: + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData), + variantTypeString(targetType), + epicsTypeString(value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + UA_Array_delete(data, num, type); + ret = 1; } { // Scope of Guard G Guard G(outgoingLock); UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard - UA_Variant_setArray(&outgoingData, arr, num, targetType); // no copy but move arr content + UA_Variant_setArray(&outgoingData, data, num, type); // no copy but move content markAsDirty(); } @@ -1360,7 +1600,18 @@ DataElementOpen62541::writeArray(const epicsUInt8 *value, { long ret = 0; - if (UA_Variant_isScalar(&incomingData)) { + if (UA_Variant_isScalar(&incomingData) && incomingData.type == &UA_TYPES[UA_TYPES_BYTESTRING]) { + UA_ByteString bs; + bs.length = num; + bs.data = static_cast(const_cast(value)); + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_setScalarCopy(&outgoingData, &bs, incomingData.type); + markAsDirty(); + } + + dbgWriteScalar(); + } else if (UA_Variant_isScalar(&incomingData)) { errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; @@ -1452,7 +1703,7 @@ DataElementOpen62541::writeArray (const epicsFloat64 *value, const epicsUInt32 n } long -DataElementOpen62541::writeArray (const char *value, const epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, len, num, &UA_TYPES[UA_TYPES_STRING], prec); } diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index a129e1a2..b86899e5 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -389,12 +389,13 @@ class DataElementOpen62541 : public DataElement * @brief Read incoming data as classic C string (char[]). * * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + * DevOpcua::DataElement::readScalar(char*,const epicsUInt32,dbCommon*,ProcessReason*,epicsUInt32*,epicsUInt32*,char*,const epicsUInt32) */ - virtual long int readScalar(char *value, const size_t num, + virtual long int readScalar(char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason = nullptr, + epicsUInt32* lenRead = nullptr, epicsUInt32 *statusCode = nullptr, char *statusText = nullptr, const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; @@ -545,7 +546,7 @@ class DataElementOpen62541 : public DataElement * See the DataElement API method it overrides * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) */ - virtual long int readArray(char *value, const epicsUInt32 len, + virtual long int readArray(char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, @@ -597,7 +598,7 @@ class DataElementOpen62541 : public DataElement * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) */ virtual long int writeScalar(const char *value, - epicsUInt32 num, + const epicsUInt32 len, dbCommon *prec) override; /** @@ -760,8 +761,7 @@ class DataElementOpen62541 : public DataElement void checkWriteArray(const UA_DataType *expectedType, const std::string &targetTypeName) const; void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; bool updateDataInStruct(void* container, std::shared_ptr pelem); - - void createMap(const UA_DataType *type, const std::string* timefrom); + void createMap(const UA_DataType *type, const std::string* timefrom = nullptr); // Structure always returns true to ensure full traversal bool isDirty() const { return isdirty || !isleaf; } @@ -1007,24 +1007,24 @@ class DataElementOpen62541 : public DataElement ret = 1; } else { // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - if (UA_Variant_isScalar(&data)) { + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant)) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (data.type != expectedType) { + } else if (variant.type != expectedType) { errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data), epicsTypeString(*value)); + prec->name, variantTypeString(variant), epicsTypeString(*value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; } else { if (UA_STATUS_IS_UNCERTAIN(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - elemsWritten = static_cast(num) < data.arrayLength ? num : static_cast(data.arrayLength); - memcpy(value, data.data, sizeof(ET) * elemsWritten); + elemsWritten = static_cast(num) < variant.arrayLength ? num : static_cast(variant.arrayLength); + memcpy(value, variant.data, sizeof(ET) * elemsWritten); } - UA_Variant_clear(&data); + UA_Variant_clear(&variant); } if (statusCode) *statusCode = stat; if (statusText) { From c5811c4a4f15cd6d594e8cbfd8ed68a30329e31c Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Thu, 20 Mar 2025 11:08:53 +0100 Subject: [PATCH 20/64] uasdk: avoid double copy of arrays --- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 141 ++++++++++++------------- devOpcuaSup/UaSdk/DataElementUaSdk.h | 28 ++--- 2 files changed, 74 insertions(+), 95 deletions(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index d319519a..c043ae86 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -895,8 +895,8 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, ret = 1; } else { // Valid OPC UA value, so try to convert - UaVariant &data = upd->getData(); - if (!data.isArray()) { + UaVariant &variant = upd->getData(); + if (!variant.isArray()) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; @@ -904,60 +904,58 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - switch (data.type()) { + elemsWritten = variant.arraySize(); + if (elemsWritten > num) elemsWritten = num; + const OpcUa_VariantArrayUnion& arrayData = + static_cast(variant)->Value.Array.Value; + size_t l; + switch (variant.type()) { case OpcUaType_String: { - UaStringArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); - (value + i * len)[len - 1] = '\0'; + l = OpcUa_String_StrSize(&arrayData.StringArray[i]); + if (l >= len) l = len - 1; + memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.StringArray[i]), l); + memset(value + i * len + l, 0, len - l); } break; } case OpcUaType_XmlElement: { - UaXmlElementArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); - (value + i * len)[len - 1] = '\0'; + l = arrayData.XmlElementArray[i].Length; + if (l >= len) l = len - 1; + memcpy(value + i * len, arrayData.XmlElementArray[i].Data, l); + memset(value + i * len + l, 0, len - l); } break; } case OpcUaType_LocalizedText: { - UaLocalizedTextArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, UaString(arr[i].Text).toUtf8(), len); - (value + i * len)[len - 1] = '\0'; + l = OpcUa_String_StrSize(&arrayData.LocalizedTextArray[i].Text); + if (l >= len) l = len - 1; + memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.LocalizedTextArray[i].Text), l); + memset(value + i * len + l, 0, len - l); } break; } case OpcUaType_QualifiedName: { - UaQualifiedNameArray arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); for (epicsUInt32 i = 0; i < elemsWritten; i++) { - strncpy(value + i * len, UaString(arr[i].Name).toUtf8(), len); - (value + i * len)[len - 1] = '\0'; + l = OpcUa_String_StrSize(&arrayData.QualifiedNameArray[i].Name); + if (l >= len) l = len - 1; + memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.QualifiedNameArray[i].Name), l); + memset(value + i * len + l, 0, len - l); } break; } case OpcUaType_ByteString: { - UaByteStringArray arr; - upd->getData().toByteStringArray(arr); - elemsWritten = num < arr.length() ? num : arr.length(); for (epicsUInt32 i = 0; i < elemsWritten; i++) { - int l = printByteString(arr[i], value + i * len, len); + l = printByteString(arrayData.ByteStringArray[i], value + i * len, len); memset(value + i * len + l, 0, len - l); } break; } default: errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(data.type()), epicsTypeString(value)); + prec->name, variantTypeString(variant.type()), epicsTypeString(value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; } @@ -989,14 +987,14 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, // CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here template<> long -DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) +DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) { long ret = 0; epicsUInt32 elemsWritten = 0; @@ -1032,11 +1030,17 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e } else { // Valid OPC UA value, so try to convert UaVariant &variant = upd->getData(); - if (!variant.isArray() && variant.type() != OpcUaType_ByteString) { + if (!variant.isArray() && variant.type() == OpcUaType_ByteString) { + const OpcUa_ByteString& bs = + static_cast(variant)->Value.ByteString; + elemsWritten = bs.Length; + if (elemsWritten > num) elemsWritten = num; + memcpy(value, bs.Data, elemsWritten); + } else if (!variant.isArray()) { errlogPrintf("%s : incoming data is not an array\n", prec->name); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); ret = 1; - } else if (variant.type() != expectedType && variant.type() != OpcUaType_Boolean && variant.type() != OpcUaType_ByteString) { + } else if (variant.type() != expectedType && variant.type() != OpcUaType_Boolean) { errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", prec->name, variantTypeString(variant.type()), epicsTypeString(*value)); (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); @@ -1045,20 +1049,11 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - if (variant.type() != OpcUaType_Boolean) { - UaByteArray arr; - UaVariant_to(variant, arr); // works for ByteArray and ByteString ! - elemsWritten = static_cast(arr.size()); - if (num < elemsWritten) elemsWritten = num; - memcpy(value, arr.data(), sizeof(epicsUInt8) * elemsWritten); - } else { - UaBooleanArray arr; - UaVariant_to(variant, arr); - elemsWritten = static_cast(arr.length()); - if (num < elemsWritten) elemsWritten = num; - memcpy(value, arr.rawData(), sizeof(epicsUInt8) * elemsWritten); - } - prec->udf = false; + elemsWritten = variant.arraySize(); + if (elemsWritten > num) elemsWritten = num; + memcpy(value, + static_cast(variant)->Value.Array.Value.Array, + elemsWritten); } } if (statusCode) *statusCode = stat; @@ -1089,7 +1084,7 @@ DataElementUaSdk::readArray (epicsInt8 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_SByte, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_SByte, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1101,7 +1096,7 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_Byte, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_Byte, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1113,7 +1108,7 @@ DataElementUaSdk::readArray (epicsInt16 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_Int16, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_Int16, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1125,7 +1120,7 @@ DataElementUaSdk::readArray (epicsUInt16 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_UInt16, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_UInt16, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1137,7 +1132,7 @@ DataElementUaSdk::readArray (epicsInt32 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_Int32, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_Int32, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1149,7 +1144,7 @@ DataElementUaSdk::readArray (epicsUInt32 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_UInt32, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_UInt32, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1161,7 +1156,7 @@ DataElementUaSdk::readArray (epicsInt64 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_Int64, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_Int64, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1173,7 +1168,7 @@ DataElementUaSdk::readArray (epicsUInt64 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_UInt64, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_UInt64, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1185,7 +1180,7 @@ DataElementUaSdk::readArray (epicsFloat32 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_Float, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_Float, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1197,7 +1192,7 @@ DataElementUaSdk::readArray (epicsFloat64 *value, const epicsUInt32 num, char *statusText, const epicsUInt32 statusTextLen) { - return readArray(value, num, numRead, OpcUaType_Double, prec, nextReason, statusCode, statusText, statusTextLen); + return readArray(value, num, numRead, OpcUaType_Double, prec, nextReason, statusCode, statusText, statusTextLen); } long @@ -1562,20 +1557,20 @@ DataElementUaSdk::writeArray (const char *value, epicsUInt32 len, } case OpcUaType_LocalizedText: { UaLocalizedTextArray arr; - UaLocalizedTextArray incoming; - UaVariant_to(incomingData, incoming); + OpcUa_UInt32 arraySize = incomingData.arraySize(); + const OpcUa_LocalizedText* incoming = static_cast(incomingData)->Value.Array.Value.LocalizedTextArray; arr.create(num); for (epicsUInt32 i = 0; i < num; i++) { const char* sep = static_cast(memchr(value, '|', len)); if (sep) { - UaString(UaByteString(static_cast(sep - value), (OpcUa_Byte*)value)).copyTo(&arr[i].Locale); - } else if (i < incoming.length()) { - UaString(incoming[i].Locale).copyTo(&arr[i].Locale); + OpcUa_String_AttachToString(const_cast(value), static_cast(sep - value), 0, OpcUa_True, OpcUa_False, &arr[i].Locale); + } else if (i < arraySize) { + OpcUa_String_StrnCpy(&arr[i].Locale, &incoming[i].Locale, OPCUA_STRING_LENDONTCARE); } else if (i > 0) { - UaString(arr[i-1].Locale).copyTo(&arr[i].Locale); + OpcUa_String_StrnCpy(&arr[i].Locale, &arr[i-1].Locale, OPCUA_STRING_LENDONTCARE); } - UaString(UaByteString(len - (sep ? static_cast(sep+1-value) : 0), (OpcUa_Byte*)(sep ? sep+1 : value))).copyTo(&arr[i].Text); + OpcUa_String_AttachToString(const_cast(sep ? sep+1 : value), OPCUA_STRINGLENZEROTERMINATED, 0, OpcUa_True, OpcUa_False, &arr[i].Text); value += len; } { // Scope of Guard G @@ -1587,20 +1582,20 @@ DataElementUaSdk::writeArray (const char *value, epicsUInt32 len, } case OpcUaType_QualifiedName: { UaQualifiedNameArray arr; - UaQualifiedNameArray incoming; - UaVariant_to(incomingData, incoming); + OpcUa_UInt32 arraySize = incomingData.arraySize(); + const OpcUa_QualifiedName* incoming = static_cast(incomingData)->Value.Array.Value.QualifiedNameArray; arr.create(num); for (epicsUInt32 i = 0; i < num; i++) { const char* sep = static_cast(memchr(value, '|', len)); if (sep) { arr[i].NamespaceIndex = atoi(value); - } else if (i < incoming.length()) { + } else if (i < arraySize) { arr[i].NamespaceIndex = incoming[i].NamespaceIndex; } else if (i > 0) { arr[i].NamespaceIndex = arr[i-1].NamespaceIndex; } - UaString(UaByteString(len - (sep ? static_cast(sep+1-value) : 0), (OpcUa_Byte*)(sep ? sep+1 : value))).copyTo(&arr[i].Name); + OpcUa_String_AttachToString(const_cast(sep ? sep+1 : value), OPCUA_STRINGLENZEROTERMINATED, 0, OpcUa_True, OpcUa_False, &arr[i].Name); value += len; } { // Scope of Guard G diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index 2889cabb..71c8da36 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -734,22 +734,6 @@ class DataElementUaSdk : public DataElement OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value) { return variant.toInt64(value); } OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Double &value) { return variant.toDouble(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaBooleanArray &value) { return variant.toBoolArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaSByteArray &value) { return variant.toSByteArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaByteArray &value) { return variant.toByteArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt16Array &value) { return variant.toInt16Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaUInt16Array &value) { return variant.toUInt16Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt32Array &value) { return variant.toInt32Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaUInt32Array &value) { return variant.toUInt32Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaInt64Array &value) { return variant.toInt64Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaUInt64Array &value) { return variant.toUInt64Array(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaFloatArray &value) { return variant.toFloatArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaDoubleArray &value) { return variant.toDoubleArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaStringArray &value) { return variant.toStringArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaXmlElementArray &value) { return variant.toXmlElementArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaLocalizedTextArray &value) { return variant.toLocalizedTextArray(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, UaQualifiedNameArray &value) { return variant.toQualifiedNameArray(value); } - void UaVariant_set(UaVariant &variant, UaBooleanArray &value) { variant.setBoolArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaSByteArray &value) { variant.setSByteArray(value, OpcUa_True); } void UaVariant_set(UaVariant &variant, UaByteArray &value) { variant.setByteArray(value, OpcUa_True); } @@ -845,7 +829,7 @@ class DataElementUaSdk : public DataElement // Read array value as templated function on EPICS type and OPC UA type // (latter *must match* OPC UA type enum argument) // CAVEAT: changes must also be reflected in specializations (in DataElementUaSdk.cpp) - template + template long readArray (ET *value, const epicsUInt32 num, epicsUInt32 *numRead, @@ -903,11 +887,11 @@ class DataElementUaSdk : public DataElement if (OpcUa_IsUncertain(stat)) { (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } - OT arr; - UaVariant_to(upd->getData(), arr); - elemsWritten = num < arr.length() ? num : arr.length(); - memcpy(value, arr.rawData(), sizeof(ET) * elemsWritten); - prec->udf = false; + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, static_cast(variant) + ->Value.Array.Value.Array, sizeof(ET) * elemsWritten); } } if (statusCode) *statusCode = stat; From 1bcc6d7ae5c82a7fa7fefb9174961eadece6283a Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Mon, 24 Mar 2025 10:10:37 +0100 Subject: [PATCH 21/64] optimize memory clearing in readArray(char*, ...) Clear array once at the beginning to avoid per-element memset. Clear old size in order to wipe older longer array data. Don't need to clear more because older shorter data is followed by already cleared memory. --- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 10 ++++------ devOpcuaSup/open62541/DataElementOpen62541.cpp | 15 +++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index c043ae86..3133afdc 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -865,6 +865,9 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, long ret = 0; epicsUInt32 elemsWritten = 0; + // clear *old* array content + memset(value, 0, *numRead * len); + if (incomingQueue.empty()) { errlogPrintf("%s : incoming data queue empty\n", prec->name); *numRead = 0; @@ -915,7 +918,6 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, l = OpcUa_String_StrSize(&arrayData.StringArray[i]); if (l >= len) l = len - 1; memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.StringArray[i]), l); - memset(value + i * len + l, 0, len - l); } break; } @@ -924,7 +926,6 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, l = arrayData.XmlElementArray[i].Length; if (l >= len) l = len - 1; memcpy(value + i * len, arrayData.XmlElementArray[i].Data, l); - memset(value + i * len + l, 0, len - l); } break; } @@ -933,7 +934,6 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, l = OpcUa_String_StrSize(&arrayData.LocalizedTextArray[i].Text); if (l >= len) l = len - 1; memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.LocalizedTextArray[i].Text), l); - memset(value + i * len + l, 0, len - l); } break; } @@ -942,14 +942,12 @@ DataElementUaSdk::readArray (char *value, epicsUInt32 len, l = OpcUa_String_StrSize(&arrayData.QualifiedNameArray[i].Name); if (l >= len) l = len - 1; memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.QualifiedNameArray[i].Name), l); - memset(value + i * len + l, 0, len - l); } break; } case OpcUaType_ByteString: { for (epicsUInt32 i = 0; i < elemsWritten; i++) { - l = printByteString(arrayData.ByteStringArray[i], value + i * len, len); - memset(value + i * len + l, 0, len - l); + printByteString(arrayData.ByteStringArray[i], value + i * len, len); } break; } diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 300e85ec..05b64a9e 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -862,6 +862,9 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, long ret = 0; epicsUInt32 elemsWritten = 0; + // clear *old* array content + memset(value, 0, *numRead * len); + if (incomingQueue.empty()) { errlogPrintf("%s : incoming data queue empty\n", prec->name); *numRead = 0; @@ -909,8 +912,7 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, const UA_String& str = static_cast(variant.data)[i]; size_t l = str.length; if (l >= len) l = len-1; - strncpy(value + i * len, reinterpret_cast(str.data), l); - (value + i * len)[l] = '\0'; + memcpy(value + i * len, str.data, l); } break; case UA_DATATYPEKIND_LOCALIZEDTEXT: @@ -918,8 +920,7 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, const UA_String& str = static_cast(variant.data)[i].text; size_t l = str.length; if (l >= len) l = len-1; - strncpy(value + i * len, reinterpret_cast(str.data), l); - (value + i * len)[l] = '\0'; + memcpy(value + i * len, str.data, l); } break; case UA_DATATYPEKIND_QUALIFIEDNAME: @@ -927,14 +928,12 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, const UA_String& str = static_cast(variant.data)[i].name; size_t l = str.length; if (l >= len) l = len-1; - strncpy(value + i * len, reinterpret_cast(str.data), l); - (value + i * len)[l] = '\0'; + memcpy(value + i * len, str.data, l); } break; case UA_DATATYPEKIND_BYTESTRING: for (epicsUInt32 i = 0; i < elemsWritten; i++) { - size_t l = printByteString(static_cast(variant.data)[i], value + i * len, len); - memset(value + i * len + l, 0, len - l); + printByteString(static_cast(variant.data)[i], value + i * len, len); } break; default: From 8be97825cac0bb14800118a25216a4d92f964534 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 31 Mar 2025 09:29:11 +0200 Subject: [PATCH 22/64] Set version to 0.12.0-dev --- configure/CONFIG_OPCUA_VERSION | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configure/CONFIG_OPCUA_VERSION b/configure/CONFIG_OPCUA_VERSION index 3fb8cb21..c488f981 100644 --- a/configure/CONFIG_OPCUA_VERSION +++ b/configure/CONFIG_OPCUA_VERSION @@ -1,5 +1,5 @@ # Version between releases: next number plus flag EPICS_OPCUA_MAJOR_VERSION = 0 -EPICS_OPCUA_MINOR_VERSION = 11 +EPICS_OPCUA_MINOR_VERSION = 12 EPICS_OPCUA_MAINTENANCE_VERSION = 0 -EPICS_OPCUA_DEVELOPMENT_FLAG = 0 +EPICS_OPCUA_DEVELOPMENT_FLAG = 1 From 13b1222e6c7df660aad40080fef841d48da8ec8e Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Thu, 17 Apr 2025 19:27:38 +0200 Subject: [PATCH 23/64] doc: update "Cheat Sheet" for v0.11 --- ...EPICS Support for OPC UA - Cheat Sheet.pdf | Bin 93098 -> 128191 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/EPICS Support for OPC UA - Cheat Sheet.pdf b/documentation/EPICS Support for OPC UA - Cheat Sheet.pdf index a91a7f9dd9e3ab4e81e9e8676d2b3ec9c9f0e895..e199473bb9b704115fdfee13b482de8cdaa74e71 100644 GIT binary patch literal 128191 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ðE$2oK3b1q#L(FQ+s6UF>$BR4PF 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�)}y&f?2u?RS~kb_ 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 literal 93098 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�)DxLLC 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ቐ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 syr<#h*aq{jcJMf&@hXMA)}n)#Qo(3-*v!c>n+a From d206f88ee948125fabac1d8c99800997a21299f2 Mon Sep 17 00:00:00 2001 From: Edmund Blomley Date: Wed, 23 Apr 2025 05:07:55 +0200 Subject: [PATCH 24/64] Adjust issue and documentation links to the official epics-modules organistation. --- devOpcuaSup/UaSdk/README.md | 2 +- devOpcuaSup/open62541/README.md | 2 +- exampleTop/DeviceDbApp/S7-1500Db/README.md | 6 +++--- exampleTop/DeviceDbApp/UaDemoServerDb/README.md | 6 +++--- exampleTop/README.md | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/devOpcuaSup/UaSdk/README.md b/devOpcuaSup/UaSdk/README.md index 671691c7..2ec10fc9 100644 --- a/devOpcuaSup/UaSdk/README.md +++ b/devOpcuaSup/UaSdk/README.md @@ -115,7 +115,7 @@ On newer Windows systems, the Windows system version of the openssl DLLs are not ## Feedback / Reporting issues -Please use the GitHub project's [issue tracker](https://github.com/ralphlange/opcua/issues). +Please use the GitHub project's [issue tracker](https://github.com/epics-modules/opcua/issues). ## Known issues diff --git a/devOpcuaSup/open62541/README.md b/devOpcuaSup/open62541/README.md index ea4ef730..310efb9f 100644 --- a/devOpcuaSup/open62541/README.md +++ b/devOpcuaSup/open62541/README.md @@ -194,7 +194,7 @@ consider linking your IOCs statically. ## Feedback / Reporting issues -Please use the GitHub project's [issue tracker](https://github.com/ralphlange/opcua/issues). +Please use the GitHub project's [issue tracker](https://github.com/epics-modules/opcua/issues). ## Known issues diff --git a/exampleTop/DeviceDbApp/S7-1500Db/README.md b/exampleTop/DeviceDbApp/S7-1500Db/README.md index b48ecec8..031f1fd9 100644 --- a/exampleTop/DeviceDbApp/S7-1500Db/README.md +++ b/exampleTop/DeviceDbApp/S7-1500Db/README.md @@ -79,7 +79,7 @@ script and the database links. ## Feedback / Reporting issues Please use the OPC UA Device Support Module's GitHub -[issue tracker](https://github.com/ralphlange/opcua/issues). +[issue tracker](https://github.com/epics-modules/opcua/issues). ## License @@ -88,7 +88,7 @@ that is distributed subject to a Software License Agreement found in file LICENSE that is included with its distribution. -[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/ralphlange/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf -[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/ralphlange/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf +[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf +[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf [uaexpert]: https://www.unified-automation.com/products/development-tools/uaexpert.html [release_notes_1500]: https://support.industry.siemens.com/cs/document/109478459/firmware-update-s7-1500-cpus-incl-displays-and-et200-cpus-(et200sp-et200pro)?dti=0&lc=en-WW diff --git a/exampleTop/DeviceDbApp/UaDemoServerDb/README.md b/exampleTop/DeviceDbApp/UaDemoServerDb/README.md index fae935a0..b1e5ab7c 100644 --- a/exampleTop/DeviceDbApp/UaDemoServerDb/README.md +++ b/exampleTop/DeviceDbApp/UaDemoServerDb/README.md @@ -43,7 +43,7 @@ script and the database links. ## Feedback / Reporting issues Please use the OPC UA Device Support Module's GitHub -[issue tracker](https://github.com/ralphlange/opcua/issues). +[issue tracker](https://github.com/epics-modules/opcua/issues). ## License @@ -52,6 +52,6 @@ that is distributed subject to a Software License Agreement found in file LICENSE that is included with its distribution. -[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/ralphlange/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf -[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/ralphlange/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf +[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf +[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf [uaexpert]: https://www.unified-automation.com/products/development-tools/uaexpert.html diff --git a/exampleTop/README.md b/exampleTop/README.md index 73f8d543..1b7d6404 100644 --- a/exampleTop/README.md +++ b/exampleTop/README.md @@ -86,7 +86,7 @@ script and the database links. ## Feedback / Reporting issues Please use the OPC UA Device Support Module's GitHub -[issue tracker](https://github.com/ralphlange/opcua/issues). +[issue tracker](https://github.com/epics-modules/opcua/issues). ## Contributing @@ -108,5 +108,5 @@ that is distributed subject to a Software License Agreement found in file LICENSE that is included with its distribution. -[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/ralphlange/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf -[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/ralphlange/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf +[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf +[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf From 5a8ccb215d032e518c0df38ea0db36698f9cb47c Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 1 Aug 2025 11:35:57 +0200 Subject: [PATCH 25/64] Remove left over debug print in 'opcuaOptions' introduced in a58ddae4 --- devOpcuaSup/iocshIntegration.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/devOpcuaSup/iocshIntegration.cpp b/devOpcuaSup/iocshIntegration.cpp index 1d305929..5db2f6eb 100644 --- a/devOpcuaSup/iocshIntegration.cpp +++ b/devOpcuaSup/iocshIntegration.cpp @@ -311,7 +311,6 @@ opcuaOptionsCallFunc(const iocshArgBuf *args) } else if (strcmp(args[0].sval, "help") == 0) { std::cout << opcuaOptionsUsage.c_str() << std::endl; } else { - std::cerr << args[1].aval.ac << " options: " << args[1].aval.av[0] << std::endl; if (args[1].aval.ac <= 1) { errlogPrintf("missing argument #2 (options)\n"); } else { From e5079da544b8f0276b90fbe85ac28e703239b98c Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 4 Aug 2025 13:58:30 +0200 Subject: [PATCH 26/64] uasdk: fix "double free" crashes with structured data fixes #142 --- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index 3133afdc..7f211247 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -268,7 +268,7 @@ DataElementUaSdk::setIncomingData(const UaVariant &value, stat = OpcUa_BadNoData; } } else { - const UaVariant &memberValue = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); + const UaVariant memberValue = structure.value(it.first, &stat); if (stat == OpcUa_Good) { pelem->setIncomingData(memberValue, reason); } From f797e3d5ee2dd58c38e0a284875a9fbcfe0b80ee Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 5 Aug 2025 09:35:20 +0200 Subject: [PATCH 27/64] uasdk: fix commit e5079da5 ("double free" crashes) - remove wrongly picked change --- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index 7f211247..4cd692b9 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -268,7 +268,7 @@ DataElementUaSdk::setIncomingData(const UaVariant &value, stat = OpcUa_BadNoData; } } else { - const UaVariant memberValue = structure.value(it.first, &stat); + const UaVariant memberValue = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); if (stat == OpcUa_Good) { pelem->setIncomingData(memberValue, reason); } From 2f5eef13defb4a52abdaa107ac7827c3a24cffc6 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 5 Aug 2025 13:59:53 +0200 Subject: [PATCH 28/64] ci: migrate UASDK GHA ubuntu jobs 20.04->22.04 - GitHub shut down ubuntu-20.04 runners on 01-Apr-2025 - requires base >= 3.15.9 (PCAS fix for newer compilers) - downgrade libssl to 1.1.1 on Ubuntu 22+ (UASDK < 1.8) --- .ci-local/install-sdk.py | 10 ++++++ .github/workflows/ci-build-uasdk-linux.yml | 36 +++++++++++----------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/.ci-local/install-sdk.py b/.ci-local/install-sdk.py index 38b4f937..d19e80d8 100644 --- a/.ci-local/install-sdk.py +++ b/.ci-local/install-sdk.py @@ -10,6 +10,8 @@ import re curdir = os.getcwd() +openssl_debs = ['libssl1.1_1.1.1f-1ubuntu2.24_amd64.deb', 'libssl-dev_1.1.1f-1ubuntu2.24_amd64.deb'] +openssl_url = 'http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl' sys.path.append(os.path.join(curdir, '.ci')) import cue @@ -74,3 +76,11 @@ os.remove(os.path.join(cue.toolsdir, enc_name)) os.remove(os.path.join(cue.toolsdir, tar_name)) + + # Downgrade libssl to 1.1.1 on GHA's Ubuntu 22.04 and up + if cue.ci['service'] == 'github-actions' and cue.ci['os'] == 'linux': + for deb in openssl_debs: + sp.check_call(['curl', '-fsSLO', '--retry', '3', '{0}/{1}'.format(openssl_url, deb)], + cwd=cue.toolsdir) + sp.check_call(['sudo', 'dpkg', '-i', deb], + cwd=cue.toolsdir) diff --git a/.github/workflows/ci-build-uasdk-linux.yml b/.github/workflows/ci-build-uasdk-linux.yml index ff2e5b12..4dec3af6 100644 --- a/.github/workflows/ci-build-uasdk-linux.yml +++ b/.github/workflows/ci-build-uasdk-linux.yml @@ -44,62 +44,62 @@ jobs: include: # Oldest supported Base 7.x - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "R7.0.4" uasdk: "1.6.3" - name: "7.0.4 Ub-20 v1.6.3" + name: "7.0.4 Ub-22 v1.6.3" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "7.0" uasdk: "1.6.3" - name: "7.0 Ub-20 v1.6.3" + name: "7.0 Ub-22 v1.6.3" # Oldest supported Base 3.15.x - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default - base: "R3.15.7" + base: "R3.15.9" uasdk: "1.6.3" - name: "3.15.7 Ub-20 v1.6.3" + name: "3.15.9 Ub-22 v1.6.3" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "3.15" uasdk: "1.6.3" - name: "3.15 Ub-20 v1.6.3" + name: "3.15 Ub-22 v1.6.3" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "R7.0.4" uasdk: "1.7.2" - name: "7.0.4 Ub-20 v1.7.2" + name: "7.0.4 Ub-22 v1.7.2" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "7.0" uasdk: "1.7.2" - name: "7.0 Ub-20 v1.7.2" + name: "7.0 Ub-22 v1.7.2" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default - base: "R3.15.7" + base: "R3.15.9" uasdk: "1.7.2" - name: "3.15.7 Ub-20 v1.7.2" + name: "3.15.9 Ub-22 v1.7.2" - - os: ubuntu-20.04 + - os: ubuntu-22.04 cmp: gcc configuration: default base: "3.15" uasdk: "1.7.2" - name: "3.15 Ub-20 v1.7.2" + name: "3.15 Ub-22 v1.7.2" steps: - uses: actions/checkout@v4 From 11993acc4c0dff7256ae4a6ade69053aec07fc03 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 5 Aug 2025 11:52:32 +0200 Subject: [PATCH 29/64] ci: allow manual start for GHA jobs --- .github/workflows/ci-build-open62541-linux.yml | 1 + .github/workflows/ci-build-open62541-win.yml | 1 + .github/workflows/ci-build-uasdk-linux.yml | 1 + .github/workflows/ci-build-uasdk-win.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/ci-build-open62541-linux.yml b/.github/workflows/ci-build-open62541-linux.yml index 5d98781d..b9d73c73 100644 --- a/.github/workflows/ci-build-open62541-linux.yml +++ b/.github/workflows/ci-build-open62541-linux.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/UaSdk/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci diff --git a/.github/workflows/ci-build-open62541-win.yml b/.github/workflows/ci-build-open62541-win.yml index d104b39a..a1811e98 100644 --- a/.github/workflows/ci-build-open62541-win.yml +++ b/.github/workflows/ci-build-open62541-win.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/UaSdk/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci diff --git a/.github/workflows/ci-build-uasdk-linux.yml b/.github/workflows/ci-build-uasdk-linux.yml index 4dec3af6..db23820a 100644 --- a/.github/workflows/ci-build-uasdk-linux.yml +++ b/.github/workflows/ci-build-uasdk-linux.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/open62541/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci diff --git a/.github/workflows/ci-build-uasdk-win.yml b/.github/workflows/ci-build-uasdk-win.yml index e967e2c6..afd6a210 100644 --- a/.github/workflows/ci-build-uasdk-win.yml +++ b/.github/workflows/ci-build-uasdk-win.yml @@ -19,6 +19,7 @@ on: - '**/*.md' - 'devOpcuaSup/open62541/*' pull_request: + workflow_dispatch: env: SETUP_PATH: .ci-local:.ci From 01ad660d6a80219f6d76287641851e611c85ab37 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 5 Aug 2025 16:19:35 +0200 Subject: [PATCH 30/64] ci: update GitHub Actions Windows jobs - bump runners to windows-2022 (windows-2019 is no longer supported) - bump VS version to 17 (vs2022) - rename Python binary to python3 - choco install openssl (v3) for open62541@VS - update path of lib tool (VS) - MSYS2: install mingw64-cmake, -toolchain, -python msys2-make, -patch - VS: install MSYS2/tools-git (for gendef) --- .ci-local/install-msvc-deps.py | 2 +- .ci-local/install-open62541.py | 4 +- .github/workflows/ci-build-open62541-win.yml | 55 ++++++++++++-------- .github/workflows/ci-build-uasdk-win.yml | 22 ++++---- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/.ci-local/install-msvc-deps.py b/.ci-local/install-msvc-deps.py index 8944f12c..e4bac6fd 100644 --- a/.ci-local/install-msvc-deps.py +++ b/.ci-local/install-msvc-deps.py @@ -11,7 +11,7 @@ import re gendef = 'C:/msys64/mingw64/bin/gendef' -lib = 'C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x64/lib.exe' +lib = 'C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/MSVC/14.44.35207/bin/HostX64/x64/lib.exe' curdir = os.getcwd() diff --git a/.ci-local/install-open62541.py b/.ci-local/install-open62541.py index 23bf8e2e..20e948ae 100644 --- a/.ci-local/install-open62541.py +++ b/.ci-local/install-open62541.py @@ -79,8 +79,8 @@ if cue.ci['os'] == 'windows': if cue.ci['compiler'] == 'gcc': generator = 'MinGW Makefiles' - elif cue.ci['compiler'] == 'vs2019': - generator = 'Visual Studio 16 2019' + elif cue.ci['compiler'] == 'vs2022': + generator = 'Visual Studio 17 2022' build_shared = 'ON' if cue.ci['static']: diff --git a/.github/workflows/ci-build-open62541-win.yml b/.github/workflows/ci-build-open62541-win.yml index a1811e98..4a444471 100644 --- a/.github/workflows/ci-build-open62541-win.yml +++ b/.github/workflows/ci-build-open62541-win.yml @@ -39,49 +39,57 @@ jobs: BCFG: ${{ matrix.configuration }} BASE: ${{ matrix.base }} OPEN62541: ${{ matrix.open62541 }} + CHOCO: openssl strategy: fail-fast: false matrix: include: - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "3.15" open62541: "1.3.12" - name: "3.15 Win2019 v1.3.12 VS2019 DLL" + name: "3.15 Win2022 v1.3.12 VS2022 DLL" - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "7.0" open62541: "1.3.12" - name: "7.0 Win2019 v1.3.12 VS2019 DLL" + name: "7.0 Win2022 v1.3.12 VS2022 DLL" steps: - uses: actions/checkout@v4 with: submodules: true + # Install gendef.exe + - uses: msys2/setup-msys2@v2 + with: + release: false + msystem: mingw64 + install: >- + mingw-w64-x86_64-tools-git - name: Download and compile open62541 client run: | - python .ci-local/install-open62541.py + python3 .ci-local/install-open62541.py - name: Prepare build dependencies run: | - python .ci/cue.py prepare + python3 .ci/cue.py prepare - name: Install MSVC dependencies run: | - python .ci-local/install-msvc-deps.py + python3 .ci-local/install-msvc-deps.py - name: Add DLL locations to PATH run: | "C:/Users/runneradmin/.cache/open62541/bin" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.cache/libxml2-2.9.3/bin" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.cache/iconv-1.14/bin" | Out-File -FilePath $env:GITHUB_PATH -Append - name: Build main module - run: python .ci/cue.py build + run: python3 .ci/cue.py build - name: Run main module tests - run: python .ci/cue.py test + run: python3 .ci/cue.py test - name: Collect and show test results - run: python .ci/cue.py test-results + run: python3 .ci/cue.py test-results build-msys2: name: ${{ matrix.name }} @@ -101,17 +109,17 @@ jobs: matrix: include: - - os: windows-2019 + - os: windows-2022 configuration: default base: "3.15" open62541: "1.3.12" - name: "3.15 Win2019 v1.3.12 MinGW DLL" + name: "3.15 Win2022 v1.3.12 MinGW DLL" - - os: windows-2019 + - os: windows-2022 configuration: default base: "7.0" open62541: "1.3.12" - name: "7.0 Win2019 v1.3.12 MinGW DLL" + name: "7.0 Win2022 v1.3.12 MinGW DLL" steps: - uses: msys2/setup-msys2@v2 @@ -120,23 +128,28 @@ jobs: msystem: mingw64 install: >- git + make + patch + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-cmake + mingw-w64-x86_64-python pacboy: openssl:p - uses: actions/checkout@v4 with: submodules: true - name: Download and compile open62541 client run: | - python .ci-local/install-open62541.py + python3 .ci-local/install-open62541.py - name: Prepare build dependencies run: | - python .ci/cue.py prepare + python3 .ci/cue.py prepare - name: Build main module run: | export PATH=$PATH:/c/Users/runneradmin/.cache/open62541/bin - python .ci/cue.py build + python3 .ci/cue.py build - name: Run main module tests run: | export PATH=$PATH:/c/Users/runneradmin/.cache/open62541/bin - python .ci/cue.py test + python3 .ci/cue.py test - name: Collect and show test results - run: python .ci/cue.py test-results + run: python3 .ci/cue.py test-results diff --git a/.github/workflows/ci-build-uasdk-win.yml b/.github/workflows/ci-build-uasdk-win.yml index afd6a210..7b4cbcf8 100644 --- a/.github/workflows/ci-build-uasdk-win.yml +++ b/.github/workflows/ci-build-uasdk-win.yml @@ -44,19 +44,19 @@ jobs: matrix: include: - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "3.15" uasdk: "1.6.3" - name: "3.15 Win2019 v1.6.3 VS2019 DLL" + name: "3.15 Win2022 v1.6.3 VS2022 DLL" - - os: windows-2019 - cmp: vs2019 + - os: windows-2022 + cmp: vs2022 configuration: default base: "7.0" uasdk: "1.6.3" - name: "7.0 Win2019 v1.6.3 VS2019 DLL" + name: "7.0 Win2022 v1.6.3 VS2022 DLL" steps: - uses: actions/checkout@v4 @@ -64,18 +64,18 @@ jobs: submodules: true - name: Download and install UASDK client run: | - python .ci-local/install-sdk.py + python3 .ci-local/install-sdk.py - name: Prepare build dependencies run: | - python .ci/cue.py prepare + python3 .ci/cue.py prepare - name: Add DLL locations to PATH run: | "C:/Users/runneradmin/.source/sdk/bin" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.source/sdk/third-party/win64/vs2015/libxml2/out32dll" | Out-File -FilePath $env:GITHUB_PATH -Append "C:/Users/runneradmin/.source/sdk/third-party/win64/vs2015/openssl/out32dll" | Out-File -FilePath $env:GITHUB_PATH -Append - name: Build main module - run: python .ci/cue.py build + run: python3 .ci/cue.py build - name: Run main module tests - run: python .ci/cue.py test + run: python3 .ci/cue.py test - name: Collect and show test results - run: python .ci/cue.py test-results + run: python3 .ci/cue.py test-results From 10c99e88969adeb03f9cebde7e44d5e44f1d9417 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 5 Aug 2025 14:48:35 +0200 Subject: [PATCH 31/64] ci: update open62541 v1.3 series to 1.3.15 --- .github/workflows/ci-build-open62541-linux.yml | 16 ++++++++-------- .github/workflows/ci-build-open62541-win.yml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-build-open62541-linux.yml b/.github/workflows/ci-build-open62541-linux.yml index b9d73c73..8f669187 100644 --- a/.github/workflows/ci-build-open62541-linux.yml +++ b/.github/workflows/ci-build-open62541-linux.yml @@ -49,30 +49,30 @@ jobs: cmp: gcc configuration: default base: "R7.0.4" - open62541: "1.3.12" - name: "7.0.4 Ub-22 v1.3.12" + open62541: "1.3.15" + name: "7.0.4 Ub-22 v1.3.15" - os: ubuntu-22.04 cmp: gcc configuration: default base: "7.0" - open62541: "1.3.12" - name: "7.0 Ub-22 v1.3.12" + open62541: "1.3.15" + name: "7.0 Ub-22 v1.3.15" # Oldest supported Base 3.15.x - os: ubuntu-22.04 cmp: gcc configuration: default base: "R3.15.9" - open62541: "1.3.12" - name: "3.15.7 Ub-22 v1.3.12" + open62541: "1.3.15" + name: "3.15.9 Ub-22 v1.3.15" - os: ubuntu-22.04 cmp: gcc configuration: default base: "3.15" - open62541: "1.3.12" - name: "3.15 Ub-22 v1.3.12" + open62541: "1.3.15" + name: "3.15 Ub-22 v1.3.15" - os: ubuntu-22.04 cmp: gcc diff --git a/.github/workflows/ci-build-open62541-win.yml b/.github/workflows/ci-build-open62541-win.yml index 4a444471..a63952e0 100644 --- a/.github/workflows/ci-build-open62541-win.yml +++ b/.github/workflows/ci-build-open62541-win.yml @@ -49,15 +49,15 @@ jobs: cmp: vs2022 configuration: default base: "3.15" - open62541: "1.3.12" - name: "3.15 Win2022 v1.3.12 VS2022 DLL" + open62541: "1.3.15" + name: "3.15 Win2022 v1.3.15 VS2022 DLL" - os: windows-2022 cmp: vs2022 configuration: default base: "7.0" - open62541: "1.3.12" - name: "7.0 Win2022 v1.3.12 VS2022 DLL" + open62541: "1.3.15" + name: "7.0 Win2022 v1.3.15 VS2022 DLL" steps: - uses: actions/checkout@v4 @@ -112,14 +112,14 @@ jobs: - os: windows-2022 configuration: default base: "3.15" - open62541: "1.3.12" - name: "3.15 Win2022 v1.3.12 MinGW DLL" + open62541: "1.3.15" + name: "3.15 Win2022 v1.3.15 MinGW DLL" - os: windows-2022 configuration: default base: "7.0" - open62541: "1.3.12" - name: "7.0 Win2022 v1.3.12 MinGW DLL" + open62541: "1.3.15" + name: "7.0 Win2022 v1.3.15 MinGW DLL" steps: - uses: msys2/setup-msys2@v2 From 70eecd5cb24ab5058747ee8a9e7127ef157e9260 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Wed, 6 Aug 2025 11:08:46 +0200 Subject: [PATCH 32/64] open62541: fix a bug in gcc < 7 - merge PR #189 by Simon Rose (ESS) (closes #189) --- devOpcuaSup/open62541/SessionOpen62541.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/devOpcuaSup/open62541/SessionOpen62541.h b/devOpcuaSup/open62541/SessionOpen62541.h index cfa65b0b..d0a3d788 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.h +++ b/devOpcuaSup/open62541/SessionOpen62541.h @@ -44,9 +44,11 @@ #include #include +namespace std { + /* Allow UA_NodeId as key */ template<> -struct std::hash +struct hash { inline std::size_t operator()(const UA_NodeId& nodeId) const noexcept { @@ -55,7 +57,7 @@ struct std::hash }; template<> -struct std::equal_to +struct equal_to { inline bool operator()(const UA_NodeId& nodeId1, const UA_NodeId& nodeId2) const noexcept { @@ -63,6 +65,8 @@ struct std::equal_to } }; +} + namespace DevOpcua { class SubscriptionOpen62541; From b771d29df064b272654c65bafd5685d6ed7f525e Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Mon, 29 Sep 2025 15:45:25 +0200 Subject: [PATCH 33/64] bugfix: only mbbi.ZRVL was tested --- devOpcuaSup/devOpcua.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devOpcuaSup/devOpcua.cpp b/devOpcuaSup/devOpcua.cpp index 3e5b4cb7..69453a4e 100644 --- a/devOpcuaSup/devOpcua.cpp +++ b/devOpcuaSup/devOpcua.cpp @@ -735,7 +735,7 @@ inline void updateEnumInfos (REC *prec, RecordConnector *pcon) prec->sdef |= ENUM_VALUES_CHECKED; auto pvl = &prec->zrvl; for (int i = 0; i < NUMBER_OF_ENUM_CHOICES; i++) { - if (*pvl) { + if (*pvl++) { prec->sdef |= ENUM_VALUES_DEFINED; break; } From 089dc30b3f19878a7d61c664dc6eee32f7b4457b Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 1 Oct 2025 14:42:37 +0200 Subject: [PATCH 34/64] bugfix: only disable FLNK on non-I/O Intr updates --- devOpcuaSup/RecordConnector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devOpcuaSup/RecordConnector.cpp b/devOpcuaSup/RecordConnector.cpp index c2c6bff4..bb06415f 100644 --- a/devOpcuaSup/RecordConnector.cpp +++ b/devOpcuaSup/RecordConnector.cpp @@ -103,7 +103,7 @@ void processCallback (epicsCallback *pcallback, const ProcessReason reason) // Do not process FLNK on updates if not "I/O Intr" SAVE_FLNK(prec); - if (reason != writeComplete && prec->scan != menuScanI_O_Intr) + if (reason == incomingData && prec->scan != menuScanI_O_Intr) DISABLE_FLNK(prec); if (prec->pact) reProcess(prec); From 8a9a676cfbd525be33f8d28ddbbdbd531975896c Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 10 Oct 2025 14:24:00 +0200 Subject: [PATCH 35/64] README: minor improvements - swap UA SDK and open62541 descriptions - explicit: no support for open62541 v1.4 series --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f64fdf94..9fb29a81 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ The module is under development, but releases are stable and robust. There are two choices for the low-level OPC UA client library: -1. The commercially available +1. The free open source client of the [open62541 project SDK][open62541.sdk]. \ + This integration is relatively new (added 2024) + and is gradually introduced in production at more and more places. + +2. The commercially available [Unified Automation C++ Based OPC UA Client SDK][unified.sdk]. \ This is the original implementation. - -2. The open source client of the [open62541 project SDK][open62541.sdk]. \ - This integration is relatively new (2024) - and hasn't seen much in-production use yet. ## Prerequisites @@ -60,7 +60,8 @@ There are two choices for the low-level OPC UA client library: ### Using the open62541 SDK * The [open62541 SDK][open62541.sdk] \ - Choose a recent release (1.2 and 1.3 are supported). + Choose a recent release from series 1.2 or 1.3 + (release seriens 1.4 is not supported yet). Download the sources using GitHub releases, not through the project website. * For OPC UA security support (authentication/encryption), you need From 7a516ad3b6afcb065123ed27aac088bfdc20be7e Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 10 Oct 2025 14:26:42 +0200 Subject: [PATCH 36/64] Set version to 0.11.1 --- configure/CONFIG_OPCUA_VERSION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure/CONFIG_OPCUA_VERSION b/configure/CONFIG_OPCUA_VERSION index c488f981..b9a7c1cb 100644 --- a/configure/CONFIG_OPCUA_VERSION +++ b/configure/CONFIG_OPCUA_VERSION @@ -1,5 +1,5 @@ # Version between releases: next number plus flag EPICS_OPCUA_MAJOR_VERSION = 0 -EPICS_OPCUA_MINOR_VERSION = 12 -EPICS_OPCUA_MAINTENANCE_VERSION = 0 -EPICS_OPCUA_DEVELOPMENT_FLAG = 1 +EPICS_OPCUA_MINOR_VERSION = 11 +EPICS_OPCUA_MAINTENANCE_VERSION = 1 +EPICS_OPCUA_DEVELOPMENT_FLAG = 0 From 63083529174029814a548b78978a1b07a2d43e4e Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Sun, 12 Oct 2025 14:22:04 +0200 Subject: [PATCH 37/64] Set version to 0.12.0-dev --- configure/CONFIG_OPCUA_VERSION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure/CONFIG_OPCUA_VERSION b/configure/CONFIG_OPCUA_VERSION index b9a7c1cb..c488f981 100644 --- a/configure/CONFIG_OPCUA_VERSION +++ b/configure/CONFIG_OPCUA_VERSION @@ -1,5 +1,5 @@ # Version between releases: next number plus flag EPICS_OPCUA_MAJOR_VERSION = 0 -EPICS_OPCUA_MINOR_VERSION = 11 -EPICS_OPCUA_MAINTENANCE_VERSION = 1 -EPICS_OPCUA_DEVELOPMENT_FLAG = 0 +EPICS_OPCUA_MINOR_VERSION = 12 +EPICS_OPCUA_MAINTENANCE_VERSION = 0 +EPICS_OPCUA_DEVELOPMENT_FLAG = 1 From e64c6cf4a98f82284522328c87a90a6edcdae356 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Tue, 21 Oct 2025 15:20:28 +0200 Subject: [PATCH 38/64] open62541: fix crash in arrays when server disconnects --- devOpcuaSup/open62541/DataElementOpen62541.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541.cpp index 05b64a9e..e438ee08 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -863,7 +863,8 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, epicsUInt32 elemsWritten = 0; // clear *old* array content - memset(value, 0, *numRead * len); + if (value) + memset(value, 0, *numRead * len); if (incomingQueue.empty()) { errlogPrintf("%s : incoming data queue empty\n", prec->name); From 4d9e0582a176d40698606d077788cff797f6d5cc Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Wed, 22 Oct 2025 11:24:39 +0200 Subject: [PATCH 39/64] bugfix: update mbbo enum strings even if initial value is invalid --- devOpcuaSup/devOpcua.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devOpcuaSup/devOpcua.cpp b/devOpcuaSup/devOpcua.cpp index 69453a4e..99ea106f 100644 --- a/devOpcuaSup/devOpcua.cpp +++ b/devOpcuaSup/devOpcua.cpp @@ -807,9 +807,9 @@ opcua_write_enum (REC *prec) } else { epicsUInt32 *rval = useReadValue(pcon) ? &prec->rval : nullptr; ret = pcon->readScalar(rval, &nextReason); + updateEnumInfos (prec, pcon); if (ret == 0 && rval) { recGblResetAlarms(prec); // get mbbo out of SOFT alarm after val was out of range - updateEnumInfos (prec, pcon); *rval &= prec->mask; if (prec->shft) *rval >>= prec->shft; From 3daf59985edd11f89f738c3232c1a22aaae818e6 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Mon, 27 Oct 2025 10:53:29 +0100 Subject: [PATCH 40/64] bugfix: make sure initial read has finished before setting up monitors, even with many, many nodes --- devOpcuaSup/UaSdk/SessionUaSdk.cpp | 6 +++++- devOpcuaSup/open62541/SessionOpen62541.cpp | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.cpp b/devOpcuaSup/UaSdk/SessionUaSdk.cpp index ffa41ff8..eba12d3c 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SessionUaSdk.cpp @@ -1195,7 +1195,11 @@ void SessionUaSdk::connectionStatusChanged ( // status needs to be updated before requests are being issued serverConnectionStatus = serverStatus; reader.pushRequest(cargo, menuPriorityHIGH); - epicsThreadSleep(0.01); // Don't know how to wait for initial read to complete + // Wait for initial read to finish + while (!reader.empty(menuPriorityHIGH)) { + epicsThreadSleep(.1); + } + epicsThreadSleep(.1); addAllMonitoredItems(); } break; diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index 7bc463b1..c05d0e34 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.cpp +++ b/devOpcuaSup/open62541/SessionOpen62541.cpp @@ -2510,6 +2510,11 @@ SessionOpen62541::connectionStatusChanged ( // status needs to be updated before requests are being issued sessionState = newSessionState; reader.pushRequest(cargo, menuPriorityHIGH); + // Wait for initial read to finish + while (!reader.empty(menuPriorityHIGH)) { + epicsThreadSleep(.1); + } + epicsThreadSleep(.1); addAllMonitoredItems(); break; } From f9b0c92faabf466e389782b7747d2162456a2b20 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 17 Nov 2025 13:53:36 +0100 Subject: [PATCH 41/64] Set version to 0.11.2 --- configure/CONFIG_OPCUA_VERSION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure/CONFIG_OPCUA_VERSION b/configure/CONFIG_OPCUA_VERSION index c488f981..1eb79999 100644 --- a/configure/CONFIG_OPCUA_VERSION +++ b/configure/CONFIG_OPCUA_VERSION @@ -1,5 +1,5 @@ # Version between releases: next number plus flag EPICS_OPCUA_MAJOR_VERSION = 0 -EPICS_OPCUA_MINOR_VERSION = 12 -EPICS_OPCUA_MAINTENANCE_VERSION = 0 -EPICS_OPCUA_DEVELOPMENT_FLAG = 1 +EPICS_OPCUA_MINOR_VERSION = 11 +EPICS_OPCUA_MAINTENANCE_VERSION = 2 +EPICS_OPCUA_DEVELOPMENT_FLAG = 0 From 3dd45596a51b7b02092e72200c9bcc13aafbc40a Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 17 Nov 2025 13:56:02 +0100 Subject: [PATCH 42/64] Set version to 0.12.0-dev --- configure/CONFIG_OPCUA_VERSION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure/CONFIG_OPCUA_VERSION b/configure/CONFIG_OPCUA_VERSION index 1eb79999..c488f981 100644 --- a/configure/CONFIG_OPCUA_VERSION +++ b/configure/CONFIG_OPCUA_VERSION @@ -1,5 +1,5 @@ # Version between releases: next number plus flag EPICS_OPCUA_MAJOR_VERSION = 0 -EPICS_OPCUA_MINOR_VERSION = 11 -EPICS_OPCUA_MAINTENANCE_VERSION = 2 -EPICS_OPCUA_DEVELOPMENT_FLAG = 0 +EPICS_OPCUA_MINOR_VERSION = 12 +EPICS_OPCUA_MAINTENANCE_VERSION = 0 +EPICS_OPCUA_DEVELOPMENT_FLAG = 1 From ea6ab9a1fbfbb0a846207ee8869ba0c085acfff5 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 17 Nov 2025 15:23:43 +0100 Subject: [PATCH 43/64] doc: update and streamline open62541/UASDK READMEs --- devOpcuaSup/UaSdk/README.md | 8 ++++++-- devOpcuaSup/open62541/README.md | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/devOpcuaSup/UaSdk/README.md b/devOpcuaSup/UaSdk/README.md index 2ec10fc9..13be073e 100644 --- a/devOpcuaSup/UaSdk/README.md +++ b/devOpcuaSup/UaSdk/README.md @@ -3,8 +3,12 @@ ## Status :warning: -This module is stable, with some expected development to be happening. -Please contact the author [Ralph Lange](mailto:ralph.lange@gmx.de) for details. +This module is stable, +with some expected development still to be happening. + +Please contact the authors +[Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and +[Ralph Lange](mailto:ralph.lange@gmx.de) for details. :warning: ## Prerequisites diff --git a/devOpcuaSup/open62541/README.md b/devOpcuaSup/open62541/README.md index 310efb9f..ce6e9217 100644 --- a/devOpcuaSup/open62541/README.md +++ b/devOpcuaSup/open62541/README.md @@ -3,14 +3,14 @@ ## Status :warning: -This module is under development and still experimental. +This module is relatively new (2014), +but gaining popularity +and getting more and more hours of production use. -**Structured OPC UA data types are not supported at this time.** - -:warning: Please contact the authors [Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and [Ralph Lange](mailto:ralph.lange@gmx.de) for details. +:warning: ## Prerequisites From 1454c6746ba9aa205ec728beae3f3aae63997e89 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 17 Nov 2025 15:27:01 +0100 Subject: [PATCH 44/64] doc: fix rendering of READMEs --- devOpcuaSup/UaSdk/README.md | 2 +- devOpcuaSup/open62541/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devOpcuaSup/UaSdk/README.md b/devOpcuaSup/UaSdk/README.md index 13be073e..25984d79 100644 --- a/devOpcuaSup/UaSdk/README.md +++ b/devOpcuaSup/UaSdk/README.md @@ -6,10 +6,10 @@ This module is stable, with some expected development still to be happening. +:warning: Please contact the authors [Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and [Ralph Lange](mailto:ralph.lange@gmx.de) for details. -:warning: ## Prerequisites diff --git a/devOpcuaSup/open62541/README.md b/devOpcuaSup/open62541/README.md index ce6e9217..62a2464f 100644 --- a/devOpcuaSup/open62541/README.md +++ b/devOpcuaSup/open62541/README.md @@ -7,10 +7,10 @@ This module is relatively new (2014), but gaining popularity and getting more and more hours of production use. +:warning: Please contact the authors [Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and [Ralph Lange](mailto:ralph.lange@gmx.de) for details. -:warning: ## Prerequisites From 2802918c4b875d94c4c221113d4cc0c9d8a8bdb8 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 12 Dec 2025 20:11:51 +0100 Subject: [PATCH 45/64] Refactor the `DataElement*` class into `Node` and `Leaf` - Create `DataElement*Node` and `DataElement*Leaf` classes, make `DataElement*` an abstract base class - Move logic for handling structures to `DataElement*Node`, for data access to `DataElement*Leaf` - Update `ElementTree` to handle the new hierarchy - Update `ElementTreeTest` in unit tests - Clean up includes - make for-range loops use 'const auto &it' where possible --- devOpcuaSup/DataElement.h | 37 +- devOpcuaSup/ElementTree.h | 57 +- devOpcuaSup/UaSdk/DataElementUaSdk.cpp | 1753 ----------------- devOpcuaSup/UaSdk/DataElementUaSdk.h | 1074 +--------- devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp | 1506 ++++++++++++++ devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h | 1030 ++++++++++ devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp | 489 +++++ devOpcuaSup/UaSdk/DataElementUaSdkNode.h | 68 + devOpcuaSup/UaSdk/ItemUaSdk.cpp | 59 +- devOpcuaSup/UaSdk/ItemUaSdk.h | 6 +- devOpcuaSup/UaSdk/Makefile.config | 3 +- devOpcuaSup/UaSdk/SessionUaSdk.cpp | 3 +- devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp | 1 + devOpcuaSup/open62541/DataElementOpen62541.h | 1162 +---------- ...62541.cpp => DataElementOpen62541Leaf.cpp} | 591 +----- .../open62541/DataElementOpen62541Leaf.h | 1176 +++++++++++ .../open62541/DataElementOpen62541Node.cpp | 414 ++++ .../open62541/DataElementOpen62541Node.h | 84 + devOpcuaSup/open62541/ItemOpen62541.cpp | 2 +- devOpcuaSup/open62541/ItemOpen62541.h | 6 +- devOpcuaSup/open62541/Makefile.config | 3 +- .../open62541/SubscriptionOpen62541.cpp | 2 +- unitTestApp/src/ElementTreeTest.cpp | 346 ++-- unitTestApp/src/UaSdk/Makefile.config | 2 +- unitTestApp/src/UaSdk/RangeCheckTest.cpp | 2 +- unitTestApp/src/open62541/Makefile.config | 2 +- 26 files changed, 5246 insertions(+), 4632 deletions(-) delete mode 100644 devOpcuaSup/UaSdk/DataElementUaSdk.cpp create mode 100644 devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp create mode 100644 devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h create mode 100644 devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp create mode 100644 devOpcuaSup/UaSdk/DataElementUaSdkNode.h rename devOpcuaSup/open62541/{DataElementOpen62541.cpp => DataElementOpen62541Leaf.cpp} (67%) create mode 100644 devOpcuaSup/open62541/DataElementOpen62541Leaf.h create mode 100644 devOpcuaSup/open62541/DataElementOpen62541Node.cpp create mode 100644 devOpcuaSup/open62541/DataElementOpen62541Node.h diff --git a/devOpcuaSup/DataElement.h b/devOpcuaSup/DataElement.h index aa1cae37..9018fdd7 100644 --- a/devOpcuaSup/DataElement.h +++ b/devOpcuaSup/DataElement.h @@ -14,9 +14,6 @@ #define DEVOPCUA_DATAELEMENT_H #include -#include -#include -#include #include #include @@ -63,13 +60,6 @@ class DataElement RecordConnector *pconnector, const std::list &elementPath); - /** - * @brief Get the type of element (inside a structure). - * - * @return true if element is a leaf (has no child elements) - */ - bool isLeaf() const { return isleaf; } - /** * @brief Setter to create a (bidirectional) link to a RecordConnector. * @@ -799,35 +789,10 @@ class DataElement */ virtual void requestRecordProcessing(const ProcessReason reason) const = 0; - const std::string name; /**< element name */ const EnumChoices* enumChoices = nullptr; /**< enum definition if this element is an enum */ protected: - /** - * @brief Constructor for node DataElement, to be used by implementations. - * - * @param name structure element name (empty otherwise) - */ - DataElement(const std::string &name = "") - : name(name) - , pconnector(nullptr) - , isleaf(false) - {} - - /** - * @brief Constructor for leaf DataElement, to be used by implementations. - * - * @param name structure element name (empty otherwise) - * @param connector - */ - DataElement(RecordConnector *pconnector, const std::string &name = "") - : name(name) - , pconnector(pconnector) - , isleaf(true) - {} - - RecordConnector *pconnector; /**< pointer to connector (if leaf) */ - const bool isleaf; /**< flag for leaf property */ + RecordConnector *pconnector = nullptr; }; } // namespace DevOpcua diff --git a/devOpcuaSup/ElementTree.h b/devOpcuaSup/ElementTree.h index a80a7ffd..def4a0b8 100644 --- a/devOpcuaSup/ElementTree.h +++ b/devOpcuaSup/ElementTree.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2020-2021 ITER Organization. +* Copyright (c) 2020-2025 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -41,35 +41,28 @@ namespace DevOpcua { * The template types used (all specific to the implementation) and their * constraints: * - * E is the Element class - * with a constructor E::E(const std::string &name, I *item) - * and methods void E::setParent(std::shared_ptr parent); - * void E::addChild(std::weak_ptr child); - * std::shared_ptr findChild(const std::string &name) - * bool E::isLeaf(); - * and a public const std::string E::name; + * NE is the Node Element class + * with a constructor NE::NE(const std::string &name, I *item) + * and methods void NE::setParent(std::shared_ptr parent); + * void NE::addChild(std::weak_ptr child); + * std::shared_ptr findChild(const std::string &name) * - * I is the Item class. + * E is the common base class of Node Element (NE) and Leaf Element + * with the method void E::setParent(std::shared_ptr parent); + * + * I is the Item class */ -template +template class ElementTree { public: - ElementTree(I *i) - : item(i) - {} - /** * @brief Return root element * * @return weak_ptr to root element */ - std::weak_ptr - root() const - { - return rootElement; - } + std::weak_ptr root() const { return rootElement; } /* Allow testing as 'if (tree) ...' */ explicit operator bool() const { return !rootElement.expired(); } @@ -82,8 +75,7 @@ class ElementTree * * @return shared_ptr to the nearest existing node in the tree, shared_ptr<>() if no overlap */ - std::shared_ptr - nearestNode(std::list &path) + std::shared_ptr nearestNode(std::list &path) { bool found; @@ -97,7 +89,7 @@ class ElementTree do { auto part = path.begin(); found = false; - if (elem) { + if (elem && !elem->isLeaf()) { auto nextelem = elem->findChild(*part); if (nextelem) { found = true; @@ -107,10 +99,7 @@ class ElementTree } } while (found && !path.empty()); - if (elem) - return elem; - else - return std::shared_ptr(); + return elem; } /** @@ -120,17 +109,17 @@ class ElementTree * @param path full path of the leaf * * @throws runtime_error when trying to add elements to a leaf node + * when adding with empty path but [ROOT] exists */ - void - addLeaf(std::shared_ptr leaf, const std::list &fullpath) + void addLeaf (std::shared_ptr leaf, const std::list &fullpath, I *item) { std::shared_ptr elem(leaf); std::list path(fullpath); auto branch = nearestNode(path); + if (branch && branch->isLeaf()) - throw std::runtime_error(SB() - << "can't add leaf to existing leaf " << branch->name); + throw std::runtime_error(SB() << "can't add leaf to existing leaf " << branch->name); if (path.empty()) { if (rootElement.lock()) throw std::runtime_error(SB() << "root node does already exist"); @@ -138,7 +127,7 @@ class ElementTree } else { path.pop_back(); // remove the leaf name for (auto rit = path.crbegin(); rit != path.crend(); ++rit) { - auto node = std::make_shared(*rit, item); + auto node = std::make_shared(*rit, item); node->addChild(elem); elem->setParent(node); elem = std::move(node); @@ -147,7 +136,7 @@ class ElementTree branch->addChild(elem); elem->setParent(branch); } else { - auto node = std::make_shared("[ROOT]", item); + auto node = std::make_shared("[ROOT]", item); node->addChild(elem); elem->setParent(node); rootElement = std::move(node); @@ -155,9 +144,11 @@ class ElementTree } } +public: + void setRoot(std::shared_ptr root) { rootElement = root; } + private: std::weak_ptr rootElement; - I *item; }; } // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp deleted file mode 100644 index 4cd692b9..00000000 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ /dev/null @@ -1,1753 +0,0 @@ -/*************************************************************************\ -* Copyright (c) 2018-2020 ITER Organization. -* This module is distributed subject to a Software License Agreement found -* in file LICENSE that is included with this distribution. -\*************************************************************************/ - -/* - * Author: Ralph Lange - * - * based on prototype work by Bernhard Kuner - * and example code from the Unified Automation C++ Based OPC UA Client SDK - */ - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "ItemUaSdk.h" -#include "DataElementUaSdk.h" -#include "UpdateQueue.h" -#include "RecordConnector.h" - -namespace DevOpcua { - -/* Specific implementation of DataElement's "factory" method */ -void -DataElement::addElementToTree(Item *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - DataElementUaSdk::addElementToTree(static_cast(item), pconnector, elementPath); -} - -DataElementUaSdk::DataElementUaSdk (const std::string &name, - ItemUaSdk *item, - RecordConnector *pconnector) - : DataElement(pconnector, name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) -{} - -DataElementUaSdk::DataElementUaSdk (const std::string &name, - ItemUaSdk *item) - : DataElement(name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(0ul) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) -{} - -void -DataElementUaSdk::addElementToTree(ItemUaSdk *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - std::string name("[ROOT]"); - if (elementPath.size()) - name = elementPath.back(); - - auto leaf = std::make_shared(name, item, pconnector); - item->dataTree.addLeaf(leaf, elementPath); - // reference from connector after adding to the tree worked - pconnector->setDataElement(leaf); -} - -void -DataElementUaSdk::show (const int level, const unsigned int indent) const -{ - std::string ind(indent*2, ' '); - std::cout << ind; - if (isLeaf()) { - std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" - << pconnector->getRecordName() - << " type=" << variantTypeString(incomingData.type()) - << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "@" << pitem->linkinfo.timestampElement; - std::cout << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) - << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; - } else { - std::cout << "node=" << name << " children=" << elements.size() - << " mapped=" << (mapped ? "y" : "n") << "\n"; - for (auto it : elements) { - if (auto pelem = it.lock()) { - pelem->show(level, indent + 1); - } - } - } -} - -void -DataElementUaSdk::createMap(const UaStructureDefinition& definition, - const std::string *timefrom) -{ - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - - if (timefrom) { - for (int i = 0; i < definition.childrenCount(); i++) { - if (*timefrom == definition.child(i).name().toUtf8()) { - timesrc = i; - } - } - OpcUa_BuiltInType t = definition.child(timesrc).valueType(); - if (timesrc == -1) { - errlogPrintf( - "%s: timestamp element %s not found - using source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str()); - } else if (t != OpcUaType_DateTime) { - errlogPrintf("%s: timestamp element %s has invalid type %s - using " - "source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str(), - variantTypeString(t)); - timesrc = -1; - } - } - for (auto &it : elements) { - auto pelem = it.lock(); - for (int i = 0; i < definition.childrenCount(); i++) { - if (pelem->name == definition.child(i).name().toUtf8()) { - elementMap.insert({i, it}); - delete pelem->enumChoices; - pelem->enumChoices = pitem->session->getEnumChoices(definition.child(i).enumDefinition()); - } - } - } - if (debug() >= 5) - std::cout << " ** " << elementMap.size() << "/" << elements.size() - << " child elements mapped to a " - << "structure of " << definition.childrenCount() << " elements" << std::endl; - mapped = true; -} - -void -DataElementUaSdk::createMap(const UaLocalizedText&) -{ - for (auto &it : elements) { - auto pelem = it.lock(); - if (pelem->name == "locale") - elementMap.insert({0, it}); - else if (pelem->name == "text") - elementMap.insert({1, it}); - else - errlogPrintf("Item %s %s element %s not found\n", - pitem->nodeid->toString().toUtf8(), - name.c_str(), - pelem->name.c_str()); - } - mapped = true; -} - -void -DataElementUaSdk::createMap(const UaQualifiedName&) -{ - for (auto &it : elements) { - auto pelem = it.lock(); - if (pelem->name == "namespaceIndex") - elementMap.insert({0, it}); - else if (pelem->name == "name") - elementMap.insert({1, it}); - else - errlogPrintf("Item %s %s element %s not found\n", - pitem->nodeid->toString().toUtf8(), - name.c_str(), - pelem->name.c_str()); - } - mapped = true; -} - -// Getting the timestamp and status information from the Item assumes that only one thread -// is pushing data into the Item's DataElement structure at any time. -void -DataElementUaSdk::setIncomingData(const UaVariant &value, - ProcessReason reason, - const std::string *timefrom, - const UaNodeId *typeId) -{ - // Make a copy of this element and cache it - incomingData = value; - - if (isLeaf()) { - if (typeId && pconnector->state() == ConnectionStatus::initialRead) { - delete enumChoices; - enumChoices = pitem->session->getEnumChoices(typeId); - } - if ((pconnector->state() == ConnectionStatus::initialRead - && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) - || (pconnector->state() == ConnectionStatus::up)) { - Guard(pconnector->lock); - bool wasFirst = false; - // Make a copy of the value for this element and put it on the queue - UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason, value, getIncomingReadStatus())); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Element " << name << " set data (" - << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } - } else { - if (debug() >= 5) - std::cout << "Element " << name << " splitting structured data to " - << elements.size() << " child elements" << std::endl; - - switch (value.type()) { - case OpcUaType_ExtensionObject: { - UaExtensionObject extensionObject; - value.toExtensionObject(extensionObject); - if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) - extensionObject.changeEncoding(UaExtensionObject::Binary); - - // Try to get the structure definition from the dictionary - UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (definition.isNull()) { - errlogPrintf( - "Cannot get a structure definition for item %s element %s (dataTypeId %s " - "encodingTypeId %s) - check access to type dictionary\n", - pitem->nodeid->toString().toUtf8(), - name.c_str(), - extensionObject.dataTypeId().toString().toUtf8(), - extensionObject.encodingTypeId().toString().toUtf8()); - return; - } - - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - if (!mapped) - createMap(definition, timefrom); - - if (timesrc >= 0) - pitem->tsData = epicsTimeFromUaVariant(UaGenericStructureValue(extensionObject, definition).value(timesrc)); - else - pitem->tsData = pitem->tsSource; - - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - OpcUa_StatusCode stat; - if (definition.isUnion()) { - UaGenericUnionValue genericValue(extensionObject, definition); - int index = genericValue.switchValue() - 1; - if (it.first == index) { - pelem->setIncomingData(genericValue.value(), reason); - stat = OpcUa_Good; - } else { - stat = OpcUa_BadNoData; - } - } else { - const UaVariant memberValue = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); - if (stat == OpcUa_Good) { - pelem->setIncomingData(memberValue, reason); - } - } - if (stat == OpcUa_BadNoData) { - // Absent optional field or union choice not taken: - // Pass a fake value with correct data type so that - // later writing will not fail with type mismatch. - const UaStructureField &field = definition.child(it.first); - OpcUa_Variant fakeValue; - OpcUa_Variant_Initialize(&fakeValue); - fakeValue.Datatype = field.valueType(); - fakeValue.ArrayType = field.valueRank() != 0; - if (debug()) - std::cerr << pitem->recConnector->getRecordName() - << " element " << pelem->name - << (definition.isUnion() ? " not taken choice " : " absent optional " ) - << variantTypeString((OpcUa_BuiltInType)fakeValue.Datatype) - << (fakeValue.ArrayType ? " array" : " scalar") - << std::endl; - pelem->setIncomingData(fakeValue, ProcessReason::readFailure); - } - } - break; - } - case OpcUaType_LocalizedText: { - UaLocalizedText localizedText; - value.toLocalizedText(localizedText); - if (!mapped) - createMap(localizedText); - - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - UaVariant memberValue; - switch (it.first) { - case 0: - memberValue.setString(localizedText.locale()); - break; - case 1: - memberValue.setString(localizedText.text()); - break; - } - pelem->setIncomingData(memberValue, reason); - } - break; - } - case OpcUaType_QualifiedName: { - UaQualifiedName qualifiedName; - value.toQualifiedName(qualifiedName); - if (!mapped) - createMap(qualifiedName); - - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - UaVariant memberValue; - switch (it.first) { - case 0: - memberValue.setUInt16(qualifiedName.namespaceIndex()); - break; - case 1: - memberValue.setString(qualifiedName.name()); - break; - } - pelem->setIncomingData(memberValue, reason); - } - break; - } - default: - errlogPrintf("%s: %s is no structured data but a %s\n", - pitem->recConnector->getRecordName(), - name.c_str(), - variantTypeString(value.type())); - } - } -} - -void -DataElementUaSdk::setIncomingEvent (ProcessReason reason) -{ - if (isLeaf()) { - Guard(pconnector->lock); - bool wasFirst = false; - // Put the event on the queue - UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason)); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Element " << name << " set event (" - << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setIncomingEvent(reason); - } - if (reason == ProcessReason::connectionLoss) { - elementMap.clear(); - timesrc = -1; - mapped = false; - } - } -} - -void -DataElementUaSdk::setState(const ConnectionStatus state) -{ - if (isLeaf()) { - Guard(pconnector->lock); - pconnector->setState(state); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setState(state); - } - } -} - -const UaVariant & -DataElementUaSdk::getOutgoingData () -{ - if (!isLeaf()) { - if (debug() >= 4) - std::cout << "Element " << name << " updating structured data from " - << elements.size() << " child elements" << std::endl; - - outgoingData = incomingData; - isdirty = false; - switch (outgoingData.type()) { - case OpcUaType_ExtensionObject: { - UaExtensionObject extensionObject; - outgoingData.toExtensionObject(extensionObject); - if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) - extensionObject.changeEncoding(UaExtensionObject::Binary); - - // Try to get the structure definition from the dictionary - UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (definition.isNull()) { - errlogPrintf( - "Cannot get a structure definition for extensionObject with dataTypeID %s " - "/ encodingTypeID %s - check access to type dictionary\n", - extensionObject.dataTypeId().toString().toUtf8(), - extensionObject.encodingTypeId().toString().toUtf8()); - return outgoingData; - } - - // Decode the ExtensionObject to a UaGenericValue to provide access to the structure fields - if (!mapped) - createMap(definition); - - if (definition.isUnion()) { - UaGenericUnionValue genericUnion(extensionObject, definition); - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - genericUnion.setValue(it.first + 1, pelem->getOutgoingData()); - pelem->isdirty = false; - isdirty = true; - updated = true; - } - } - if (debug() >= 4) - std::cout << "Data from child element " << pelem->name - << (updated ? " inserted into union" : " ignored (not dirty)") - << std::endl; - } - if (isdirty) - genericUnion.toExtensionObject(extensionObject); - - } else { - UaGenericStructureValue genericStruct(extensionObject, definition); - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - genericStruct.setField(it.first, pelem->getOutgoingData()); - pelem->isdirty = false; - isdirty = true; - updated = true; - } - } - if (debug() >= 4) - std::cout << "Data from child element " << pelem->name - << (updated ? " inserted into structure" : " ignored (not dirty)") - << std::endl; - } - if (isdirty) - genericStruct.toExtensionObject(extensionObject); - } - - if (isdirty) - outgoingData.setExtensionObject(extensionObject, OpcUa_True); - break; - } - case OpcUaType_LocalizedText: { - UaLocalizedText localizedText; - outgoingData.toLocalizedText(localizedText); - if (!mapped) - createMap(localizedText); - - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - switch (it.first) { - case 0: - localizedText.setLocale(pelem->getOutgoingData().toString()); - break; - case 1: - localizedText.setText(pelem->getOutgoingData().toString()); - break; - } - pelem->isdirty = false; - isdirty = true; - updated = true; - } - if (debug() >= 4) - std::cout << "Data from child element " << pelem->name - << (updated ? " inserted into LocalizedText" : " ignored (not dirty)") - << std::endl; - } - } - if (isdirty) - outgoingData.setLocalizedText(localizedText); - break; - } - case OpcUaType_QualifiedName: { - UaQualifiedName qualifiedName; - outgoingData.toQualifiedName(qualifiedName); - if (!mapped) - createMap(qualifiedName); - - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - OpcUa_UInt16 ns; - if (pelem->isDirty()) { - switch (it.first) { - case 0: - if (pelem->getOutgoingData().toUInt16(ns) == OpcUa_Good) - qualifiedName.setNamespaceIndex(ns); - break; - case 1: - qualifiedName.setQualifiedName(pelem->getOutgoingData().toString(), qualifiedName.namespaceIndex()); - break; - } - pelem->isdirty = false; - isdirty = true; - updated = true; - } - if (debug() >= 4) - std::cout << "Data from child element " << pelem->name - << (updated ? " inserted into QualifiedName" : " ignored (not dirty)") - << std::endl; - } - } - if (isdirty) - outgoingData.setQualifiedName(qualifiedName); - break; - } - default: - errlogPrintf("%s: %s is no structured data but a %s\n", - pitem->recConnector->getRecordName(), - name.c_str(), - variantTypeString(outgoingData.type())); - return outgoingData; - } - if (debug() >= 4) { - if (isdirty) - std::cout << "Encoding changed data structure to outgoingData of element " << name - << std::endl; - else - std::cout << "Returning unchanged outgoingData of element " << name - << std::endl; - } - } - return outgoingData; -} - -void -DataElementUaSdk::dbgReadScalar (const UpdateUaSdk *upd, - const std::string &targetTypeName, - const size_t targetSize) const -{ - if (isLeaf() && debug()) { - char time_buf[40]; - upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); - ProcessReason reason = upd->getType(); - - std::cout << pconnector->getRecordName() << ": "; - if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { - std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "(@" << pconnector->plinkinfo->timestampElement << ")"; - std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" - << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; - UaVariant &data = upd->getData(); - if (data.type() == OpcUaType_String) - std::cout << "'" << data.toString().toUtf8() << "'"; - else - std::cout << data.toString().toUtf8(); - std::cout << " (" << variantTypeString(data.type()) << ")" - << " as " << targetTypeName; - if (targetSize) - std::cout << "[" << targetSize << "]"; - } else { - std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); - } - std::cout << " --- remaining queue " << incomingQueue.size() - << "/" << incomingQueue.capacity() << std::endl; - } -} - -long -DataElementUaSdk::readScalar (epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readScalar (epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readScalar (epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readScalar (epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); -} - -// Helper functions to convert between OpcUa_ByteString and hex encoded C string -// We use our own functions instead of UaByteString methods toHex/fromHex because -// they work on UaString, not C string, which would require additional copy. -static int printByteString(const OpcUa_ByteString& byteString, char* encoded, int len) -{ - static const char hexdigits[] = "0123456789ABCDEF"; - int i, l = 0; - for (i = 0; i < byteString.Length && l < len-3; i++) { - encoded[l++] = hexdigits[byteString.Data[i] >> 4]; - encoded[l++] = hexdigits[byteString.Data[i] & 0xf]; - } - encoded[l] = 0; // terminate - return l; -} - -// parseByteString accepts hex strings with optional spaces separating units of 1 or multiple of 2 hex digits -static int parseByteString(OpcUa_ByteString& byteString, const char* encoded, int len) -{ - OpcUa_ByteString_Clear(&byteString); - byteString.Data = static_cast(OpcUa_Alloc((len+1)/2)); // can never be longer, shorter does not hurt - if (!byteString.Data) return -1; - int l = 0; - bool firstDigit = true; - while (len--) { - OpcUa_Byte c, b; - c = *encoded++; - if (!c) break; - if (isblank(c)) { - firstDigit = true; - continue; - } - if (!isxdigit(c)) { - // invalid character - OpcUa_ByteString_Clear(&byteString); - return -1; - } - b = c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); - if (len && isxdigit(c = *encoded)) { - firstDigit = false; - encoded++; - len--; - b <<= 4; - b |= c - (isdigit(c) ? '0' : isupper(c) ? 'A'-10 : 'a'-10); - } else { - if (!firstDigit || (c && !isblank(c) && !isxdigit(c))) { - // 1 is the only odd number of digits allowed - // because otherwise where is the byte border? 12|3 or 1|23 ? - OpcUa_ByteString_Clear(&byteString); - return -1; - } - } - byteString.Data[l++] = b; - } - byteString.Length = l; - return l; -} - -// CString type needs specialization -long -DataElementUaSdk::readScalar (char *value, const epicsUInt32 len, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *lenRead, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), "CString", len); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (len && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so copy over - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - UaVariant& data = upd->getData(); - UaString str; - const void* s = nullptr; - OpcUa_Int32 n = 0; - if (data.type() == OpcUaType_ExtensionObject) { - UaExtensionObject extensionObject; - data.toExtensionObject(extensionObject); - UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (definition.isUnion()) { - UaGenericUnionValue genericValue(extensionObject, definition); - int switchValue = genericValue.switchValue(); - if (switchValue > 0) { - n = snprintf(value, len, "%s:%s", - definition.child(switchValue-1).name().toUtf8(), - genericValue.value().toString().toUtf8()); - } else { - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } - } - } else { - const OpcUa_Variant* variant = data; - switch (variant->Datatype) { - case OpcUaType_Byte: - case OpcUaType_SByte: - if (variant->ArrayType == OpcUa_VariantArrayType_Array) { - n = variant->Value.Array.Length; - s = variant->Value.Array.Value.Array; - } else if (variant->ArrayType == OpcUa_VariantArrayType_Matrix) { - OpcUa_VariantMatrix_GetElementCount(&variant->Value.Matrix, &n); - s = variant->Value.Matrix.Value.Array; - } - if (static_cast(n) > len) - n = len; - break; - case OpcUaType_ByteString: - n = printByteString(variant->Value.ByteString, value, len); - } - if (enumChoices) { - OpcUa_Int32 enumIndex; - data.toInt32(enumIndex); - auto it = enumChoices->find(enumIndex); - if (it != enumChoices->end() && !it->second.empty()) { - s = it->second.c_str(); - n = static_cast(it->second.length()); - if (static_cast(n) >= len) - n = len-1; - } - } - if (!n) { - str = data.toString(); - s = str.toUtf8(); - n = str.length(); - if (static_cast(n) >= len) - n = len-1; - } - if (s) - memcpy(value, s, n); - } - memset(value+n, 0, len-n); - prec->udf = false; - if (lenRead) - *lenRead = n; - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; -} - -void -DataElementUaSdk::dbgReadArray (const UpdateUaSdk *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const -{ - if (isLeaf() && debug()) { - char time_buf[40]; - upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); - ProcessReason reason = upd->getType(); - - std::cout << pconnector->getRecordName() << ": "; - if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { - std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); - if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) - std::cout << "@" << pconnector->plinkinfo->timestampElement; - std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" - << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; - UaVariant &data = upd->getData(); - std::cout << " array of " << variantTypeString(data.type()) - << "[" << upd->getData().arraySize() << "]" - << " into " << targetTypeName << "[" << targetSize << "]"; - } else { - std::cout << "(client time "<< time_buf << ") " << processReasonString(reason); - } - std::cout << " --- remaining queue " << incomingQueue.size() - << "/" << incomingQueue.capacity() << std::endl; - } -} - -// Read array for EPICS String / OpcUa_String -long int -DataElementUaSdk::readArray (char *value, epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - epicsUInt32 elemsWritten = 0; - - // clear *old* array content - memset(value, 0, *numRead * len); - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UaVariant &variant = upd->getData(); - if (!variant.isArray()) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - elemsWritten = variant.arraySize(); - if (elemsWritten > num) elemsWritten = num; - const OpcUa_VariantArrayUnion& arrayData = - static_cast(variant)->Value.Array.Value; - size_t l; - switch (variant.type()) { - case OpcUaType_String: { - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - l = OpcUa_String_StrSize(&arrayData.StringArray[i]); - if (l >= len) l = len - 1; - memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.StringArray[i]), l); - } - break; - } - case OpcUaType_XmlElement: { - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - l = arrayData.XmlElementArray[i].Length; - if (l >= len) l = len - 1; - memcpy(value + i * len, arrayData.XmlElementArray[i].Data, l); - } - break; - } - case OpcUaType_LocalizedText: { - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - l = OpcUa_String_StrSize(&arrayData.LocalizedTextArray[i].Text); - if (l >= len) l = len - 1; - memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.LocalizedTextArray[i].Text), l); - } - break; - } - case OpcUaType_QualifiedName: { - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - l = OpcUa_String_StrSize(&arrayData.QualifiedNameArray[i].Name); - if (l >= len) l = len - 1; - memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.QualifiedNameArray[i].Name), l); - } - break; - } - case OpcUaType_ByteString: { - for (epicsUInt32 i = 0; i < elemsWritten; i++) { - printByteString(arrayData.ByteStringArray[i], value + i * len, len); - } - break; - } - default: - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(variant.type()), epicsTypeString(value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } - prec->udf = false; - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; -} - -// Specialization for epicsUInt8 / OpcUa_Byte -// needed because UaByteArray API is different from all other UaXxxArray classes -// and because epicsUInt8 is also used for arrays of OpcUa_Boolean -// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here -template<> -long -DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UaVariant &variant = upd->getData(); - if (!variant.isArray() && variant.type() == OpcUaType_ByteString) { - const OpcUa_ByteString& bs = - static_cast(variant)->Value.ByteString; - elemsWritten = bs.Length; - if (elemsWritten > num) elemsWritten = num; - memcpy(value, bs.Data, elemsWritten); - } else if (!variant.isArray()) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (variant.type() != expectedType && variant.type() != OpcUaType_Boolean) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(variant.type()), epicsTypeString(*value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - elemsWritten = variant.arraySize(); - if (elemsWritten > num) elemsWritten = num; - memcpy(value, - static_cast(variant)->Value.Array.Value.Array, - elemsWritten); - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num) - *numRead = elemsWritten; - return ret; -} - -long -DataElementUaSdk::readArray (epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_SByte, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Byte, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Int16, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_UInt16, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Int32, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_UInt32, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Int64, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_UInt64, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Float, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, num, numRead, OpcUaType_Double, prec, nextReason, statusCode, statusText, statusTextLen); -} - -long -DataElementUaSdk::readArray (char *value, epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) -{ - return readArray(value, len, num, numRead, OpcUaType_String, prec, nextReason, statusCode, statusText, statusTextLen); -} - -inline -void -DataElementUaSdk::dbgWriteScalar () const -{ - if (isLeaf() && debug()) { - std::cout << pconnector->getRecordName() << ": set outgoing data (" - << variantTypeString(outgoingData.type()) << ") to value "; - if (outgoingData.type() == OpcUaType_String) - std::cout << "'" << outgoingData.toString().toUtf8() << "'"; - else - std::cout << outgoingData.toString().toUtf8(); - std::cout << std::endl; - } -} - -long -DataElementUaSdk::writeScalar (const epicsInt32 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const epicsUInt32 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const epicsInt64 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const epicsFloat64 &value, dbCommon *prec) -{ - return writeScalar(value, prec); -} - -long -DataElementUaSdk::writeScalar (const char *value, epicsUInt32 len, dbCommon *prec) -{ - long ret = 1; - long l; - unsigned long ul; - double d; - char* end = nullptr; - OpcUa_BuiltInType type = incomingData.type(); - - if (type == OpcUaType_ExtensionObject) - { - UaExtensionObject extensionObject; - incomingData.toExtensionObject(extensionObject); - UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); - if (definition.isUnion()) { - if (value[0] == '\0') { - } else for (int i = 0; i < definition.childrenCount(); i++) { - UaGenericUnionValue genericValue(extensionObject, definition); - const UaString& memberName = definition.child(i).name(); - epicsUInt32 namelen = static_cast(memberName.length()); - if ((strncmp(value, memberName.toUtf8(), namelen) == 0) - && value[namelen] == ':') { - // temporarily set incomingData to selected union member type - UaVariant saveValue = incomingData; - OpcUa_Variant fakeValue; - OpcUa_Variant_Initialize(&fakeValue); - fakeValue.Datatype = definition.child(i).valueType(); - incomingData = fakeValue; - const UaNodeId& typeId = definition.child(i).typeId(); - enumChoices = pitem->session->getEnumChoices(&typeId); - // recurse to set union member - ret = writeScalar(value+namelen+1, len-(namelen+1), prec); - // restore incomingData type to union - delete enumChoices; - enumChoices = nullptr; - incomingData = saveValue; - if (ret == 0) { - Guard G(outgoingLock); - genericValue.setValue(i+1, outgoingData); - genericValue.toExtensionObject(extensionObject); - outgoingData.setExtensionObject(extensionObject,true); - } - return ret; - } - } - } - } - - switch (type) { - case OpcUaType_String: - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setString(UaByteString(len, (OpcUa_Byte*)value)); // UaByteString supports len - markAsDirty(); - ret = 0; - break; - } - case OpcUaType_XmlElement: - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setXmlElement(UaByteString(len, (OpcUa_Byte*)value)); - markAsDirty(); - ret = 0; - break; - } - case OpcUaType_LocalizedText: - { // Scope of Guard G - Guard G(outgoingLock); - UaLocalizedText localizedText; - const char* sep = static_cast(memchr(value, '|', len)); - if (sep) { - localizedText.setLocale(UaByteString(static_cast(sep - value), (OpcUa_Byte*)value)); - value = sep + 1; - len -= static_cast(sep + 1 - value); - } else { // keep the locale - incomingData.toLocalizedText(localizedText); - } - localizedText.setText(UaByteString(len, (OpcUa_Byte*)value)); - outgoingData.setLocalizedText(localizedText); - markAsDirty(); - ret = 0; - break; - } - case OpcUaType_QualifiedName: - { // Scope of Guard G - Guard G(outgoingLock); - UaQualifiedName qualifiedName; - const char* sep = static_cast(memchr(value, '|', len)); - if (sep) { - qualifiedName.setNamespaceIndex(atoi(value)); - value = sep + 1; - len -= static_cast(sep + 1 - value); - } else { // keep the namespace - incomingData.toQualifiedName(qualifiedName); - } - qualifiedName.setQualifiedName(UaByteString(len, (OpcUa_Byte*)value), qualifiedName.namespaceIndex()); - outgoingData.setQualifiedName(qualifiedName); - markAsDirty(); - ret = 0; - break; - } - case OpcUaType_ByteString: - { // Scope of Guard G - OpcUa_ByteString bs; - OpcUa_ByteString_Initialize(&bs); - if (parseByteString(bs, value, len) >= 0) { - UaByteString byteString; - byteString.attach(&bs); // wrap, no copy - Guard G(outgoingLock); - outgoingData.setByteString(byteString, true); // move, no copy - markAsDirty(); - ret = 0; - } - break; - } - case OpcUaType_Boolean: - { // Scope of Guard G - Guard G(outgoingLock); - if (strchr("YyTt1", *value)) - outgoingData.setBoolean(true); - else - outgoingData.setBoolean(false); - markAsDirty(); - ret = 0; - break; - } - case OpcUaType_Byte: - ul = strtoul(value, &end, 0); - if (end != value && isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setByte(static_cast(ul)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_SByte: - l = strtol(value, &end, 0); - if (end != value && isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setSByte(static_cast(l)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_UInt16: - ul = strtoul(value, &end, 0); - if (end != value && isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setUInt16(static_cast(ul)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Int16: - l = strtol(value, &end, 0); - if (end != value && isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setInt16(static_cast(l)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_UInt32: - ul = strtoul(value, &end, 0); - if (end != value && isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setUInt32(static_cast(ul)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Int32: - l = strtol(value, &end, 0); - if (enumChoices) { - // first test enum strings then numeric values - // in case a string starts with a number but means a different value - for (auto it: *enumChoices) - if (it.second == value) { - l = static_cast(it.first); - ret = 0; - end = nullptr; - break; - } - if (ret != 0 && end != value) - for (auto it: *enumChoices) - if (l == it.first) { - ret = 0; - break; - } - if (ret != 0) - break; - } - if (end != value && isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setInt32(static_cast(l)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_UInt64: - ul = strtoul(value, &end, 0); - if (end != value && isWithinRange(ul)) { - Guard G(outgoingLock); - outgoingData.setUInt64(static_cast(ul)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Int64: - l = strtol(value, &end, 0); - if (end != value && isWithinRange(l)) { - Guard G(outgoingLock); - outgoingData.setInt64(static_cast(l)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Float: - d = strtod(value, &end); - if (end != value && isWithinRange(d)) { - Guard G(outgoingLock); - outgoingData.setFloat(static_cast(d)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Double: - { - d = strtod(value, &end); - if (end != value) { - Guard G(outgoingLock); - outgoingData.setDouble(static_cast(d)); - markAsDirty(); - ret = 0; - } - break; - } - default: - errlogPrintf("%s : unsupported conversion from string to %s for outgoing data\n", - prec->name, variantTypeString(type)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - return -1; - } - - if (ret != 0) { - errlogPrintf("%s : value \"%s\" out of range\n", - prec->name, value); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - if (ret == 0) - dbgWriteScalar(); - return ret; -} - -inline -void -DataElementUaSdk::dbgWriteArray (const epicsUInt32 targetSize, const std::string &targetTypeName) const -{ - if (isLeaf() && debug()) { - std::cout << pconnector->getRecordName() << ": writing array of " - << targetTypeName << "[" << targetSize << "] as " - << variantTypeString(outgoingData.type()) << "["<< outgoingData.arraySize() << "]" - << std::endl; - } -} - -// Write array for EPICS String / OpcUa_String -long -DataElementUaSdk::writeArray (const char *value, epicsUInt32 len, - const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec) -{ - long ret = 0; - - if (!incomingData.isArray()) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - switch (incomingData.type()) { - case OpcUaType_String: { - UaStringArray arr; - arr.create(num); - for (epicsUInt32 i = 0; i < num; i++) { - UaString(UaByteString(len, (OpcUa_Byte*)value + i * len)).copyTo(&arr[i]); - } - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - break; - } - case OpcUaType_XmlElement: { - UaXmlElementArray arr; - arr.create(num); - for (epicsUInt32 i = 0; i < num; i++) { - UaByteString(len, (OpcUa_Byte*)value + i * len).copyTo(&arr[i]); - } - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - break; - } - case OpcUaType_LocalizedText: { - UaLocalizedTextArray arr; - OpcUa_UInt32 arraySize = incomingData.arraySize(); - const OpcUa_LocalizedText* incoming = static_cast(incomingData)->Value.Array.Value.LocalizedTextArray; - - arr.create(num); - for (epicsUInt32 i = 0; i < num; i++) { - const char* sep = static_cast(memchr(value, '|', len)); - if (sep) { - OpcUa_String_AttachToString(const_cast(value), static_cast(sep - value), 0, OpcUa_True, OpcUa_False, &arr[i].Locale); - } else if (i < arraySize) { - OpcUa_String_StrnCpy(&arr[i].Locale, &incoming[i].Locale, OPCUA_STRING_LENDONTCARE); - } else if (i > 0) { - OpcUa_String_StrnCpy(&arr[i].Locale, &arr[i-1].Locale, OPCUA_STRING_LENDONTCARE); - } - OpcUa_String_AttachToString(const_cast(sep ? sep+1 : value), OPCUA_STRINGLENZEROTERMINATED, 0, OpcUa_True, OpcUa_False, &arr[i].Text); - value += len; - } - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - break; - } - case OpcUaType_QualifiedName: { - UaQualifiedNameArray arr; - OpcUa_UInt32 arraySize = incomingData.arraySize(); - const OpcUa_QualifiedName* incoming = static_cast(incomingData)->Value.Array.Value.QualifiedNameArray; - - arr.create(num); - for (epicsUInt32 i = 0; i < num; i++) { - const char* sep = static_cast(memchr(value, '|', len)); - if (sep) { - arr[i].NamespaceIndex = atoi(value); - } else if (i < arraySize) { - arr[i].NamespaceIndex = incoming[i].NamespaceIndex; - } else if (i > 0) { - arr[i].NamespaceIndex = arr[i-1].NamespaceIndex; - } - OpcUa_String_AttachToString(const_cast(sep ? sep+1 : value), OPCUA_STRINGLENZEROTERMINATED, 0, OpcUa_True, OpcUa_False, &arr[i].Name); - value += len; - } - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - break; - } - default: - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - dbgWriteArray(num, epicsTypeString(value)); - } - return ret; -} - -// Specialization for epicsUInt8 / OpcUa_Byte -// needed because UaByteArray API is different from all other UaXxxArray classes -// and because epicsUInt8 is also used for arrays of OpcUa_Boolean -// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here -template<> -long -DataElementUaSdk::writeArray (const epicsUInt8 *value, const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec) -{ - long ret = 0; - - if (!incomingData.isArray() && incomingData.type() == OpcUaType_ByteString) { - UaByteString bs(num, reinterpret_cast(const_cast(value))); - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setByteString(bs, true); // move, no copy - markAsDirty(); - } - - dbgWriteScalar(); - } else if (!incomingData.isArray()) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type() != OpcUaType_Byte && incomingData.type() != OpcUaType_Boolean) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (incomingData.type() == OpcUaType_Byte) { - UaByteArray arr(reinterpret_cast(value), static_cast(num)); - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - } else { - UaBooleanArray arr(static_cast(num), const_cast(value)); - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - } - dbgWriteArray(num, epicsTypeString(*value)); - } - return ret; -} - -long -DataElementUaSdk::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_SByte, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Byte, prec); -} - -long -DataElementUaSdk::writeArray (const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Int16, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_UInt16, prec); -} - -long -DataElementUaSdk::writeArray (const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Int32, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_UInt32, prec); -} - -long -DataElementUaSdk::writeArray (const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Int64, prec); -} - -long -DataElementUaSdk::writeArray (const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_UInt64, prec); -} - -long -DataElementUaSdk::writeArray (const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Float, prec); -} - -long -DataElementUaSdk::writeArray (const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, num, OpcUaType_Double, prec); -} - -long -DataElementUaSdk::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) -{ - return writeArray(value, len, num, OpcUaType_String, prec); -} - -void -DataElementUaSdk::requestRecordProcessing (const ProcessReason reason) const -{ - if (isLeaf()) { - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - pelem->requestRecordProcessing(reason); - } - } -} - -} // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.h b/devOpcuaSup/UaSdk/DataElementUaSdk.h index 71c8da36..44d9316e 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2020 ITER Organization. +* Copyright (c) 2018-2025 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -14,222 +14,87 @@ #ifndef DEVOPCUA_DATAELEMENTUASDK_H #define DEVOPCUA_DATAELEMENTUASDK_H -#include -#include #include #include -#include -#include -#include #include +#include #include -#include "DataElement.h" #include "devOpcua.h" -#include "RecordConnector.h" -#include "Update.h" -#include "UpdateQueue.h" #include "ItemUaSdk.h" namespace DevOpcua { -class ItemUaSdk; - -typedef Update UpdateUaSdk; - -inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } -inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } -inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } -inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } -inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } -inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } -inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } -inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } -inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } -inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } -inline const char *epicsTypeString (const char*) { return "epicsString"; } +class DataElementUaSdkNode; inline const char * variantTypeString (const OpcUa_BuiltInType type) { switch(type) { - case OpcUaType_Null: return "OpcUa_Null"; - case OpcUaType_Boolean: return "OpcUa_Boolean"; - case OpcUaType_SByte: return "OpcUa_SByte"; - case OpcUaType_Byte: return "OpcUa_Byte"; - case OpcUaType_Int16: return "OpcUa_Int16"; - case OpcUaType_UInt16: return "OpcUa_UInt16"; - case OpcUaType_Int32: return "OpcUa_Int32"; - case OpcUaType_UInt32: return "OpcUa_UInt32"; - case OpcUaType_Int64: return "OpcUa_Int64"; - case OpcUaType_UInt64: return "OpcUa_UInt64"; - case OpcUaType_Float: return "OpcUa_Float"; - case OpcUaType_Double: return "OpcUa_Double"; - case OpcUaType_String: return "OpcUa_String"; - case OpcUaType_DateTime: return "OpcUa_DateTime"; - case OpcUaType_Guid: return "OpcUa_Guid"; - case OpcUaType_ByteString: return "OpcUa_ByteString"; - case OpcUaType_XmlElement: return "OpcUa_XmlElement"; - case OpcUaType_NodeId: return "OpcUa_NodeId"; - case OpcUaType_ExpandedNodeId: return "OpcUa_ExpandedNodeId"; - case OpcUaType_StatusCode: return "OpcUa_StatusCode"; - case OpcUaType_QualifiedName: return "OpcUa_QualifiedName"; - case OpcUaType_LocalizedText: return "OpcUa_LocalizedText"; - case OpcUaType_ExtensionObject: return "OpcUa_ExtensionObject"; - case OpcUaType_DataValue: return "OpcUa_DataValue"; - case OpcUaType_Variant: return "OpcUa_Variant"; - case OpcUaType_DiagnosticInfo: return "OpcUa_DiagnosticInfo"; + case OpcUaType_Null: return "OpcUa_Null"; + case OpcUaType_Boolean: return "OpcUa_Boolean"; + case OpcUaType_SByte: return "OpcUa_SByte"; + case OpcUaType_Byte: return "OpcUa_Byte"; + case OpcUaType_Int16: return "OpcUa_Int16"; + case OpcUaType_UInt16: return "OpcUa_UInt16"; + case OpcUaType_Int32: return "OpcUa_Int32"; + case OpcUaType_UInt32: return "OpcUa_UInt32"; + case OpcUaType_Int64: return "OpcUa_Int64"; + case OpcUaType_UInt64: return "OpcUa_UInt64"; + case OpcUaType_Float: return "OpcUa_Float"; + case OpcUaType_Double: return "OpcUa_Double"; + case OpcUaType_String: return "OpcUa_String"; + case OpcUaType_DateTime: return "OpcUa_DateTime"; + case OpcUaType_Guid: return "OpcUa_Guid"; + case OpcUaType_ByteString: return "OpcUa_ByteString"; + case OpcUaType_XmlElement: return "OpcUa_XmlElement"; + case OpcUaType_NodeId: return "OpcUa_NodeId"; + case OpcUaType_ExpandedNodeId: return "OpcUa_ExpandedNodeId"; + case OpcUaType_StatusCode: return "OpcUa_StatusCode"; + case OpcUaType_QualifiedName: return "OpcUa_QualifiedName"; + case OpcUaType_LocalizedText: return "OpcUa_LocalizedText"; + case OpcUaType_ExtensionObject: return "OpcUa_ExtensionObject"; + case OpcUaType_DataValue: return "OpcUa_DataValue"; + case OpcUaType_Variant: return "OpcUa_Variant"; + case OpcUaType_DiagnosticInfo: return "OpcUa_DiagnosticInfo"; } return "Illegal Value"; } -// Template for range check when writing -template -inline bool isWithinRange (const FROM &value) { - return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); -} - -// Specializations for unsigned to signed to avoid compiler warnings -template<> -inline bool isWithinRange (const epicsUInt32 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt32 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt32 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const epicsUInt64 &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -// Simple-check specializations for converting signed to unsigned of same or wider type -template<> inline bool isWithinRange (const epicsInt8 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt16 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt32 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt8 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt16 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt32 &value) { return !(value < 0); } -template<> inline bool isWithinRange (const epicsInt64 &value) { return !(value < 0); } - -// No-check-needed specializations for converting to same or wider type -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsInt32 &) { return true; } - -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } -template<> inline bool isWithinRange (const epicsUInt32 &) { return true; } - -template<> inline bool isWithinRange (const epicsInt64 &) { return true; } -template<> inline bool isWithinRange (const epicsInt64 &) { return true; } -template<> inline bool isWithinRange (const epicsInt64 &) { return true; } - -template<> inline bool isWithinRange (const epicsFloat64 &) { return true; } /** * @brief The DataElementUaSdk implementation of a single piece of data. * + * This class implements the common parts of a Node and a Leaf element. * See DevOpcua::DataElement + * */ -class DataElementUaSdk : public DataElement +class DataElementUaSdk { -public: - /** - * @brief Constructor for DataElement from record connector. - * - * Creates the final (leaf) element of the data structure. The record connector - * holds a shared pointer to its leaf, while the data element has a weak pointer - * to the record connector. - * - * @param name name of the element (empty for root element) - * @param pitem pointer to corresponding ItemUaSdk - * @param pconnector pointer to record connector to link to - */ - DataElementUaSdk(const std::string &name, - ItemUaSdk *pitem, - RecordConnector *pconnector); - - /** - * @brief Constructor for DataElement from child element. - * - * Creates an intermediate (node) element of the data structure. The child holds - * a shared pointer, while the parent has a weak pointer in its list/map of child - * nodes, to facilitate traversing the structure when data updates come in. - * - * @param name name of the element - * @param item pointer to corresponding ItemUaSdk - */ - DataElementUaSdk(const std::string &name, - ItemUaSdk *item); - - ~DataElementUaSdk() { delete enumChoices; } - - /** - * @brief Create a DataElement and add it to the item's dataTree. - * - * @param item item to add the element to - * @param pconnector pointer to the leaf's record connector - * @param elementPath full path to the element - */ - static void addElementToTree(ItemUaSdk *item, - RecordConnector *pconnector, - const std::list &elementPath); - - /* ElementTree node interface methods */ - void - addChild(std::weak_ptr elem) - { - elements.push_back(elem); - } + friend class DataElementUaSdkNode; + friend class DataElementUaSdkLeaf; - std::shared_ptr - findChild(const std::string &name) - { - for (auto it : elements) - if (auto pit = it.lock()) - if (pit->name == name) - return pit; - return std::shared_ptr(); - } +public: + DataElementUaSdk (const std::string &name, class ItemUaSdk *pitem) + : name(name) + , pitem(pitem) + , outgoingLock(pitem->dataTreeWriteLock) + {} + virtual ~DataElementUaSdk(); - void - setParent(std::shared_ptr elem) - { - parent = elem; - } + /* ElementTree interface */ + void setParent(std::shared_ptr elem) { parent = elem; } + virtual bool isLeaf() const = 0; + virtual void addChild(std::weak_ptr elem) = 0; + virtual std::shared_ptr findChild(const std::string &name) const = 0; /** - * @brief Print configuration and status. See DevOpcua::DataElement::show + * @brief Print configuration and status. + * See DevOpcua::DataElement::show */ - void show(const int level, const unsigned int indent) const override; + virtual void show(const int level, const unsigned int indent) const = 0; /** * @brief Push an incoming data value into the DataElement. @@ -242,10 +107,11 @@ class DataElementUaSdk : public DataElement * @param timefrom name of element to read item timestamp from * @param typeId data type of the data element */ - void setIncomingData(const UaVariant &value, - ProcessReason reason, - const std::string *timefrom = nullptr, - const UaNodeId *typeId = nullptr); + virtual void setIncomingData(const UaVariant &value, + ProcessReason reason, + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr) + = 0; /** * @brief Push an incoming event into the DataElement. @@ -255,12 +121,12 @@ class DataElementUaSdk : public DataElement * * @param reason reason for this value update */ - void setIncomingEvent(ProcessReason reason); + virtual void setIncomingEvent(ProcessReason reason) = 0; /** * @brief Set the EPICS-related state of the element and all sub-elements. */ - void setState(const ConnectionStatus state); + virtual void setState(const ConnectionStatus state) = 0; /** * @brief Get the outgoing data value from the DataElement. @@ -270,385 +136,7 @@ class DataElementUaSdk : public DataElement * * @return reference to outgoing data */ - const UaVariant &getOutgoingData(); - - /** - * @brief Read incoming data as a scalar epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as classic C string (char[]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - - virtual long int readScalar(char *value, const epicsUInt32 len, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *lenRead = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of EPICS String (char[40]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(char *value, epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Write outgoing scalar epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsUInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) - */ - virtual long int writeScalar(const epicsFloat64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing classic C string (char[]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeScalar(const char *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const char *value, epicsUInt32 len, - const epicsUInt32 num, - dbCommon *prec) override; + virtual const UaVariant &getOutgoingData() = 0; /** * @brief Clear (discard) the current outgoing data. @@ -660,452 +148,34 @@ class DataElementUaSdk : public DataElement * oldest element from the queue, allowing access to the next element * with the next send. */ - virtual void clearOutgoingData() { outgoingData.clear(); } + virtual void clearOutgoingData() = 0; /** * @brief Create processing requests for record(s) attached to this element. * See DevOpcua::DataElement::requestRecordProcessing */ - virtual void requestRecordProcessing(const ProcessReason reason) const override; + virtual void requestRecordProcessing(const ProcessReason reason) const = 0; /** - * @brief Get debug level from record (via RecordConnector). + * @brief Get debug level from item or record (via RecordConnector). * @return debug level */ - int debug() const { return (isLeaf() ? pconnector->debug() : pitem->debug()); } - -private: - void dbgWriteScalar () const; - void dbgReadScalar(const UpdateUaSdk *upd, - const std::string &targetTypeName, - const size_t targetSize = 0) const; - void dbgReadArray(const UpdateUaSdk *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const; - void checkWriteArray(OpcUa_BuiltInType expectedType, const std::string &targetTypeName) const; - void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; - void createMap(const UaStructureDefinition& definition, const std::string *timefrom = nullptr); - void createMap(const UaLocalizedText&); - void createMap(const UaQualifiedName&); - - // Structure always returns true to ensure full traversal - bool isDirty() const { return isdirty || !isleaf; } - void - markAsDirty() - { - isdirty = true; - pitem->markAsDirty(); - } - - // Convert the time stamp from a data element - epicsTime - epicsTimeFromUaVariant(const UaVariant &data) const - { - UaDateTime dt; - UaStatus s = data.toDateTime(dt); - if (s.isGood()) { - return ItemUaSdk::uaToEpicsTime(dt, 0); - } - return pitem->tsSource; - } - - // Get the time stamp from the incoming object - const epicsTime &getIncomingTimeStamp() const { - ProcessReason reason = pitem->getReason(); - if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) - && isLeaf()) - switch (pconnector->plinkinfo->timestamp) { - case LinkOptionTimestamp::server: - return pitem->tsServer; - case LinkOptionTimestamp::source: - return pitem->tsSource; - case LinkOptionTimestamp::data: - return pitem->tsData; - } - return pitem->tsClient; - } + virtual int debug() const { return pitem->debug(); } - // Get the read status from the incoming object - OpcUa_StatusCode getIncomingReadStatus() const { return pitem->getLastStatus().code(); } + const std::string name; /**< element name */ - // Overloaded helper functions that wrap the UaVariant::toXxx() and UaVariant::setXxx methods - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int32 &value) { return variant.toInt32(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_UInt32 &value) { return variant.toUInt32(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value) { return variant.toInt64(value); } - OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Double &value) { return variant.toDouble(value); } - - void UaVariant_set(UaVariant &variant, UaBooleanArray &value) { variant.setBoolArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaSByteArray &value) { variant.setSByteArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaByteArray &value) { variant.setByteArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaInt16Array &value) { variant.setInt16Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaUInt16Array &value) { variant.setUInt16Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaInt32Array &value) { variant.setInt32Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaUInt32Array &value) { variant.setUInt32Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaInt64Array &value) { variant.setInt64Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaUInt64Array &value) { variant.setUInt64Array(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaFloatArray &value) { variant.setFloatArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaDoubleArray &value) { variant.setDoubleArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaStringArray &value) { variant.setStringArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaXmlElementArray &value) { variant.setXmlElementArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaLocalizedTextArray &value) { variant.setLocalizedTextArray(value, OpcUa_True); } - void UaVariant_set(UaVariant &variant, UaQualifiedNameArray &value) { variant.setQualifiedNameArray(value, OpcUa_True); } - - // Read scalar value as templated function on EPICS type and OPC UA type - // value == nullptr is allowed and leads to the value being dropped (ignored), - // including the extended status - template - long - readScalar (ET *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) - { - long ret = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - OT v; - if (OpcUa_IsNotGood(UaVariant_to(upd->getData(), v))) { - errlogPrintf("%s : incoming data (%s) out-of-bounds\n", - prec->name, - upd->getData().toString().toUtf8()); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - *value = v; - prec->udf = false; - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; - } - - // Read array value as templated function on EPICS type and OPC UA type - // (latter *must match* OPC UA type enum argument) - // CAVEAT: changes must also be reflected in specializations (in DataElementUaSdk.cpp) - template - long - readArray (ET *value, const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) - { - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - OpcUa_StatusCode stat = upd->getStatus(); - if (OpcUa_IsNotGood(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UaVariant &variant = upd->getData(); - if (!variant.isArray()) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (variant.type() != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(variant.type()), epicsTypeString(*value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (OpcUa_IsUncertain(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - elemsWritten = variant.arraySize(); - if (elemsWritten > num) - elemsWritten = num; - memcpy(value, static_cast(variant) - ->Value.Array.Value.Array, sizeof(ET) * elemsWritten); - } - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; - } - - // Read array value for EPICS String / OpcUa_String - long - readArray (char *value, epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - OpcUa_BuiltInType expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen); - - // Write scalar value as templated function on EPICS type - template - long - writeScalar (const ET &value, - dbCommon *prec) - { - long ret = 1; - - switch (incomingData.type()) { - case OpcUaType_Boolean: - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setBoolean(value != 0); - markAsDirty(); - ret = 0; - break; - } - case OpcUaType_Byte: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setByte(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_SByte: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setSByte(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_UInt16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setUInt16(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Int16: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setInt16(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_UInt32: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setUInt32(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Int32: // may be an enum - if (isWithinRange(value) && - (!enumChoices || - enumChoices->find(static_cast(value)) != enumChoices->end())) { - Guard G(outgoingLock); - outgoingData.setInt32(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_UInt64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setUInt64(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Int64: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setInt64(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Float: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setFloat(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_Double: - if (isWithinRange(value)) { - Guard G(outgoingLock); - outgoingData.setDouble(static_cast(value)); - markAsDirty(); - ret = 0; - } - break; - case OpcUaType_String: - { // Scope of Guard G - Guard G(outgoingLock); - outgoingData.setString(static_cast(std::to_string(value).c_str())); - markAsDirty(); - break; - } - default: - errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", - prec->name, epicsTypeString(value), variantTypeString(incomingData.type())); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - if (ret != 0) { - errlogPrintf("%s : value out of range\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - - if (ret == 0) - dbgWriteScalar(); - return ret; - } - - // Write array value for EPICS String / OpcUa_String - long - writeArray (const char *value, epicsUInt32 len, - const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec); - - // Write array value as templated function on EPICS type, OPC UA container and simple (element) types - // (latter *must match* OPC UA type enum argument) - // CAVEAT: changes must also be reflected in the specialization (in DataElementUaSdk.cpp) - template - long - writeArray (const ET *value, const epicsUInt32 num, - OpcUa_BuiltInType targetType, - dbCommon *prec) - { - long ret = 0; - - if (!incomingData.isArray()) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type() != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData.type()), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - // The array methods must cast away the constness of their value argument - // as the UA SDK API uses non-const parameters - ST *val = const_cast(reinterpret_cast(value)); - CT arr(static_cast(num), val); - { // Scope of Guard G - Guard G(outgoingLock); - UaVariant_set(outgoingData, arr); - markAsDirty(); - } - - dbgWriteArray(num, epicsTypeString(*value)); - } - return ret; - } - - ItemUaSdk *pitem; /**< corresponding item */ - std::vector> elements; /**< children (if node) */ - std::shared_ptr parent; /**< parent */ +private: + virtual bool isDirty() const = 0; + virtual void markAsDirty() = 0; - std::unordered_map> elementMap; - int timesrc; +protected: + ItemUaSdk *pitem; /**< corresponding item */ + std::shared_ptr parent; /**< parent */ - bool mapped; /**< child name to index mapping done */ - UpdateQueue incomingQueue; /**< queue of incoming values */ - UaVariant incomingData; /**< cache of latest incoming value */ - epicsMutex &outgoingLock; /**< data lock for outgoing value */ - UaVariant outgoingData; /**< cache of latest outgoing value */ - bool isdirty; /**< outgoing value has been (or needs to be) updated */ + UaVariant incomingData; /**< cache of latest incoming value */ + epicsMutex &outgoingLock; /**< data lock for outgoing value */ + UaVariant outgoingData; /**< cache of latest outgoing value */ + bool isdirty; /**< outgoing value has been (or needs to be) updated */ }; } // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp new file mode 100644 index 00000000..87e1ecc3 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp @@ -0,0 +1,1506 @@ +/*************************************************************************\ +* Copyright (c) 2018-2025 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#include "DataElementUaSdkLeaf.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "DataElementUaSdk.h" +#include "DataElementUaSdkNode.h" +#include "ItemUaSdk.h" +#include "RecordConnector.h" + +namespace DevOpcua +{ + +DataElementUaSdkLeaf::DataElementUaSdkLeaf (const std::string &name, + class ItemUaSdk *pitem, + class RecordConnector *pconnector) + : DataElementUaSdk(name, pitem) + , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) +{} + +/* Explicitly implement the destructor here (allows the compiler to place the vtable) */ +DataElementUaSdk::~DataElementUaSdk() = default; + +/* Specific implementation of DataElement's "factory" method */ +void +DataElement::addElementToTree(Item *item, RecordConnector *pconnector, const std::list &elementPath) +{ + DataElementUaSdkLeaf::addElementToTree(static_cast(item), pconnector, elementPath); +} + +void +DataElementUaSdkLeaf::addElementToTree(ItemUaSdk *item, + RecordConnector *pconnector, + const std::list &elementPath) +{ + std::string name("[ROOT]"); + if (elementPath.size()) + name = elementPath.back(); + + auto leaf = std::make_shared(name, item, pconnector); + item->dataTree.addLeaf(leaf, elementPath, item); + // reference from connector after adding to the tree worked + pconnector->setDataElement(leaf); + leaf->pconnector = pconnector; +} + +void +DataElementUaSdkLeaf::show(const int, const unsigned int indent) const +{ + std::string ind(indent * 2, ' '); + std::cout << ind; + std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" << pconnector->getRecordName() + << " type=" << variantTypeString(incomingData.type()) + << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "@" << pitem->linkinfo.timestampElement; + std::cout << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) + << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; +} + +void +DataElementUaSdkLeaf::setIncomingData (const UaVariant &value, + ProcessReason reason, + const std::string *, + const UaNodeId *typeId) +{ + incomingData = value; + + if (typeId && pconnector->state() == ConnectionStatus::initialRead) { + delete enumChoices; + enumChoices = pitem->session->getEnumChoices(typeId); + } + if ((pconnector->state() == ConnectionStatus::initialRead + && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) + || (pconnector->state() == ConnectionStatus::up)) { + Guard(pconnector->lock); + bool wasFirst = false; + // Make a copy of the value for this element and put it on the queue + UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason, value, getIncomingReadStatus())); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Element " << name << " set data (" << processReasonString(reason) << ") for record " + << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); + } +} + +void +DataElementUaSdkLeaf::setIncomingEvent (ProcessReason reason) +{ + Guard(pconnector->lock); + bool wasFirst = false; + UpdateUaSdk *u(new UpdateUaSdk(getIncomingTimeStamp(), reason)); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Element " << name << " set event (" << processReasonString(reason) << ") for record " + << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); +} + +void +DataElementUaSdkLeaf::setState(const ConnectionStatus state) +{ + Guard(pconnector->lock); + pconnector->setState(state); +} + +const UaVariant & +DataElementUaSdkLeaf::getOutgoingData() +{ + return outgoingData; +} + +void +DataElementUaSdkLeaf::clearOutgoingData() +{ + outgoingData.clear(); +} + +void +DataElementUaSdkLeaf::requestRecordProcessing(const ProcessReason reason) const +{ + pconnector->requestRecordProcessing(reason); +} + +int +DataElementUaSdkLeaf::debug() const +{ + return pconnector->debug(); +} + +const epicsTime & +DataElementUaSdkLeaf::getIncomingTimeStamp() const +{ + ProcessReason reason = pitem->getReason(); + if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete)) + switch (pconnector->plinkinfo->timestamp) { + case LinkOptionTimestamp::server: + return pitem->tsServer; + case LinkOptionTimestamp::source: + return pitem->tsSource; + case LinkOptionTimestamp::data: + return pitem->tsData; + } + return pitem->tsClient; +} + +OpcUa_StatusCode +DataElementUaSdkLeaf::getIncomingReadStatus() const +{ + return pitem->getLastStatus().code(); +} + +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_Int32 &value) +{ + return variant.toInt32(value); +} +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_UInt32 &value) +{ + return variant.toUInt32(value); +} +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value) +{ + return variant.toInt64(value); +} +OpcUa_StatusCode +DataElementUaSdkLeaf::UaVariant_to(const UaVariant &variant, OpcUa_Double &value) +{ + return variant.toDouble(value); +} + +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaBooleanArray &value) +{ + variant.setBoolArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaSByteArray &value) +{ + variant.setSByteArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaByteArray &value) +{ + variant.setByteArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaInt16Array &value) +{ + variant.setInt16Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaUInt16Array &value) +{ + variant.setUInt16Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaInt32Array &value) +{ + variant.setInt32Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaUInt32Array &value) +{ + variant.setUInt32Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaInt64Array &value) +{ + variant.setInt64Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaUInt64Array &value) +{ + variant.setUInt64Array(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaFloatArray &value) +{ + variant.setFloatArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaDoubleArray &value) +{ + variant.setDoubleArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaStringArray &value) +{ + variant.setStringArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaXmlElementArray &value) +{ + variant.setXmlElementArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaLocalizedTextArray &value) +{ + variant.setLocalizedTextArray(value, OpcUa_True); +} +void +DataElementUaSdkLeaf::UaVariant_set(UaVariant &variant, UaQualifiedNameArray &value) +{ + variant.setQualifiedNameArray(value, OpcUa_True); +} + +void +DataElementUaSdkLeaf::dbgReadScalar(const UpdateUaSdk *upd, + const std::string &targetTypeName, + const size_t targetSize) const +{ + if (isLeaf() && debug()) { + char time_buf[40]; + upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); + ProcessReason reason = upd->getType(); + + std::cout << pconnector->getRecordName() << ": "; + if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { + std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "(@" << pconnector->plinkinfo->timestampElement << ")"; + std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" + << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; + UaVariant &data = upd->getData(); + if (data.type() == OpcUaType_String) + std::cout << "'" << data.toString().toUtf8() << "'"; + else + std::cout << data.toString().toUtf8(); + std::cout << " (" << variantTypeString(data.type()) << ")" + << " as " << targetTypeName; + if (targetSize) + std::cout << "[" << targetSize << "]"; + } else { + std::cout << "(client time " << time_buf << ") " << processReasonString(reason); + } + std::cout << " --- remaining queue " << incomingQueue.size() << "/" << incomingQueue.capacity() << std::endl; + } +} + +long +DataElementUaSdkLeaf::readScalar(epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readScalar(epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readScalar(epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readScalar(epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readScalar(value, prec, nextReason, statusCode, statusText, statusTextLen); +} + +// Helper functions to convert between OpcUa_ByteString and hex encoded C string +// We use our own functions instead of UaByteString methods toHex/fromHex because +// they work on UaString, not C string, which would require additional copy. +static int +printByteString(const OpcUa_ByteString &byteString, char *encoded, int len) +{ + static const char hexdigits[] = "0123456789ABCDEF"; + int i, l = 0; + for (i = 0; i < byteString.Length && l < len - 3; i++) { + encoded[l++] = hexdigits[byteString.Data[i] >> 4]; + encoded[l++] = hexdigits[byteString.Data[i] & 0xf]; + } + encoded[l] = 0; // terminate + return l; +} + +// parseByteString accepts hex strings with optional spaces separating units of 1 or multiple of 2 hex digits +static int +parseByteString(OpcUa_ByteString &byteString, const char *encoded, int len) +{ + OpcUa_ByteString_Clear(&byteString); + byteString.Data + = static_cast(OpcUa_Alloc((len + 1) / 2)); // can never be longer, shorter does not hurt + if (!byteString.Data) + return -1; + int l = 0; + bool firstDigit = true; + while (len--) { + OpcUa_Byte c, b; + c = *encoded++; + if (!c) + break; + if (isblank(c)) { + firstDigit = true; + continue; + } + if (!isxdigit(c)) { + // invalid character + OpcUa_ByteString_Clear(&byteString); + return -1; + } + b = c - (isdigit(c) ? '0' : isupper(c) ? 'A' - 10 : 'a' - 10); + if (len && isxdigit(c = *encoded)) { + firstDigit = false; + encoded++; + len--; + b <<= 4; + b |= c - (isdigit(c) ? '0' : isupper(c) ? 'A' - 10 : 'a' - 10); + } else { + if (!firstDigit || (c && !isblank(c) && !isxdigit(c))) { + // 1 is the only odd number of digits allowed + // because otherwise where is the byte border? 12|3 or 1|23 ? + OpcUa_ByteString_Clear(&byteString); + return -1; + } + } + byteString.Data[l++] = b; + } + byteString.Length = l; + return l; +} + +// CString type needs specialization +long +DataElementUaSdkLeaf::readScalar(char *value, + const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *lenRead, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), "CString", len); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (len && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so copy over + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + UaVariant &data = upd->getData(); + UaString str; + const void *s = nullptr; + OpcUa_Int32 n = 0; + if (data.type() == OpcUaType_ExtensionObject) { + UaExtensionObject extensionObject; + data.toExtensionObject(extensionObject); + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isUnion()) { + UaGenericUnionValue genericValue(extensionObject, definition); + int switchValue = genericValue.switchValue(); + if (switchValue > 0) { + n = snprintf(value, + len, + "%s:%s", + definition.child(switchValue - 1).name().toUtf8(), + genericValue.value().toString().toUtf8()); + } else { + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } + } + } else { + const OpcUa_Variant *variant = data; + switch (variant->Datatype) { + case OpcUaType_Byte: + case OpcUaType_SByte: + if (variant->ArrayType == OpcUa_VariantArrayType_Array) { + n = variant->Value.Array.Length; + s = variant->Value.Array.Value.Array; + } else if (variant->ArrayType == OpcUa_VariantArrayType_Matrix) { + OpcUa_VariantMatrix_GetElementCount(&variant->Value.Matrix, &n); + s = variant->Value.Matrix.Value.Array; + } + if (static_cast(n) > len) + n = len; + break; + case OpcUaType_ByteString: + n = printByteString(variant->Value.ByteString, value, len); + } + if (enumChoices) { + OpcUa_Int32 enumIndex; + data.toInt32(enumIndex); + const auto &it = enumChoices->find(enumIndex); + if (it != enumChoices->end() && !it->second.empty()) { + s = it->second.c_str(); + n = static_cast(it->second.length()); + if (static_cast(n) >= len) + n = len - 1; + } + } + if (!n) { + str = data.toString(); + s = str.toUtf8(); + n = str.length(); + if (static_cast(n) >= len) + n = len - 1; + } + if (s) + memcpy(value, s, n); + } + memset(value + n, 0, len - n); + prec->udf = false; + if (lenRead) + *lenRead = n; + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + return ret; +} + +void +DataElementUaSdkLeaf::dbgReadArray(const UpdateUaSdk *upd, + const epicsUInt32 targetSize, + const std::string &targetTypeName) const +{ + if (isLeaf() && debug()) { + char time_buf[40]; + upd->getTimeStamp().strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S.%09f"); + ProcessReason reason = upd->getType(); + + std::cout << pconnector->getRecordName() << ": "; + if (reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) { + std::cout << "(" << linkOptionTimestampString(pconnector->plinkinfo->timestamp); + if (pconnector->plinkinfo->timestamp == LinkOptionTimestamp::data) + std::cout << "@" << pconnector->plinkinfo->timestampElement; + std::cout << " time " << time_buf << ") read " << processReasonString(reason) << " (" + << UaStatus(upd->getStatus()).toString().toUtf8() << ") "; + UaVariant &data = upd->getData(); + std::cout << " array of " << variantTypeString(data.type()) << "[" << upd->getData().arraySize() << "]" + << " into " << targetTypeName << "[" << targetSize << "]"; + } else { + std::cout << "(client time " << time_buf << ") " << processReasonString(reason); + } + std::cout << " --- remaining queue " << incomingQueue.size() << "/" << incomingQueue.capacity() << std::endl; + } +} + +// Read array for EPICS String / OpcUa_String +long int +DataElementUaSdkLeaf::readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + // clear *old* array content + memset(value, 0, *numRead * len); + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (num && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UaVariant &variant = upd->getData(); + if (!variant.isArray()) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + const OpcUa_VariantArrayUnion &arrayData + = static_cast(variant)->Value.Array.Value; + size_t l; + switch (variant.type()) { + case OpcUaType_String: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = OpcUa_String_StrSize(&arrayData.StringArray[i]); + if (l >= len) + l = len - 1; + memcpy(value + i * len, OpcUa_String_GetRawString(&arrayData.StringArray[i]), l); + } + break; + } + case OpcUaType_XmlElement: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = arrayData.XmlElementArray[i].Length; + if (l >= len) + l = len - 1; + memcpy(value + i * len, arrayData.XmlElementArray[i].Data, l); + } + break; + } + case OpcUaType_LocalizedText: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = OpcUa_String_StrSize(&arrayData.LocalizedTextArray[i].Text); + if (l >= len) + l = len - 1; + memcpy( + value + i * len, OpcUa_String_GetRawString(&arrayData.LocalizedTextArray[i].Text), l); + } + break; + } + case OpcUaType_QualifiedName: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + l = OpcUa_String_StrSize(&arrayData.QualifiedNameArray[i].Name); + if (l >= len) + l = len - 1; + memcpy( + value + i * len, OpcUa_String_GetRawString(&arrayData.QualifiedNameArray[i].Name), l); + } + break; + } + case OpcUaType_ByteString: { + for (epicsUInt32 i = 0; i < elemsWritten; i++) { + printByteString(arrayData.ByteStringArray[i], value + i * len, len); + } + break; + } + default: + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, + variantTypeString(variant.type()), + epicsTypeString(value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } + prec->udf = false; + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; +} + +// Specialization for epicsUInt8 / OpcUa_Byte +// needed because UaByteArray API is different from all other UaXxxArray classes +// and because epicsUInt8 is also used for arrays of OpcUa_Boolean +// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here +template<> +long +DataElementUaSdkLeaf::readArray(epicsUInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (num && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UaVariant &variant = upd->getData(); + if (!variant.isArray() && variant.type() == OpcUaType_ByteString) { + const OpcUa_ByteString &bs = static_cast(variant)->Value.ByteString; + elemsWritten = bs.Length; + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, bs.Data, elemsWritten); + } else if (!variant.isArray()) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (variant.type() != expectedType && variant.type() != OpcUaType_Boolean) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, + variantTypeString(variant.type()), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, static_cast(variant)->Value.Array.Value.Array, elemsWritten); + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + if (num) + *numRead = elemsWritten; + return ret; +} + +long +DataElementUaSdkLeaf::readArray(epicsInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_SByte, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Byte, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Int16, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_UInt16, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Int32, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_UInt32, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Int64, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsUInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_UInt64, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsFloat32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Float, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(epicsFloat64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, num, numRead, OpcUaType_Double, prec, nextReason, statusCode, statusText, statusTextLen); +} + +long +DataElementUaSdkLeaf::readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) +{ + return readArray( + value, len, num, numRead, OpcUaType_String, prec, nextReason, statusCode, statusText, statusTextLen); +} + +inline void +DataElementUaSdkLeaf::dbgWriteScalar() const +{ + if (isLeaf() && debug()) { + std::cout << pconnector->getRecordName() << ": set outgoing data (" << variantTypeString(outgoingData.type()) + << ") to value "; + if (outgoingData.type() == OpcUaType_String) + std::cout << "'" << outgoingData.toString().toUtf8() << "'"; + else + std::cout << outgoingData.toString().toUtf8(); + std::cout << std::endl; + } +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsInt32 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsUInt32 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsInt64 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const epicsFloat64 &value, dbCommon *prec) +{ + return writeScalar(value, prec); +} + +long +DataElementUaSdkLeaf::writeScalar(const char *value, epicsUInt32 len, dbCommon *prec) +{ + long ret = 1; + long l; + unsigned long ul; + double d; + char *end = nullptr; + OpcUa_BuiltInType type = incomingData.type(); + + if (type == OpcUaType_ExtensionObject) { + UaExtensionObject extensionObject; + incomingData.toExtensionObject(extensionObject); + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isUnion()) { + if (value[0] == '\0') { + } else + for (int i = 0; i < definition.childrenCount(); i++) { + UaGenericUnionValue genericValue(extensionObject, definition); + const UaString &memberName = definition.child(i).name(); + epicsUInt32 namelen = static_cast(memberName.length()); + if ((strncmp(value, memberName.toUtf8(), namelen) == 0) && value[namelen] == ':') { + // temporarily set incomingData to selected union member type + UaVariant saveValue = incomingData; + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = definition.child(i).valueType(); + incomingData = fakeValue; + const UaNodeId &typeId = definition.child(i).typeId(); + enumChoices = pitem->session->getEnumChoices(&typeId); + // recurse to set union member + ret = writeScalar(value + namelen + 1, len - (namelen + 1), prec); + // restore incomingData type to union + delete enumChoices; + enumChoices = nullptr; + incomingData = saveValue; + if (ret == 0) { + Guard G(outgoingLock); + genericValue.setValue(i + 1, outgoingData); + genericValue.toExtensionObject(extensionObject); + outgoingData.setExtensionObject(extensionObject, true); + } + return ret; + } + } + } + } + + switch (type) { + case OpcUaType_String: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setString(UaByteString(len, (OpcUa_Byte *) value)); // UaByteString supports len + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_XmlElement: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setXmlElement(UaByteString(len, (OpcUa_Byte *) value)); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_LocalizedText: { // Scope of Guard G + Guard G(outgoingLock); + UaLocalizedText localizedText; + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + localizedText.setLocale(UaByteString(static_cast(sep - value), (OpcUa_Byte *) value)); + value = sep + 1; + len -= static_cast(sep + 1 - value); + } else { // keep the locale + incomingData.toLocalizedText(localizedText); + } + localizedText.setText(UaByteString(len, (OpcUa_Byte *) value)); + outgoingData.setLocalizedText(localizedText); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_QualifiedName: { // Scope of Guard G + Guard G(outgoingLock); + UaQualifiedName qualifiedName; + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + qualifiedName.setNamespaceIndex(atoi(value)); + value = sep + 1; + len -= static_cast(sep + 1 - value); + } else { // keep the namespace + incomingData.toQualifiedName(qualifiedName); + } + qualifiedName.setQualifiedName(UaByteString(len, (OpcUa_Byte *) value), qualifiedName.namespaceIndex()); + outgoingData.setQualifiedName(qualifiedName); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_ByteString: { // Scope of Guard G + OpcUa_ByteString bs; + OpcUa_ByteString_Initialize(&bs); + if (parseByteString(bs, value, len) >= 0) { + UaByteString byteString; + byteString.attach(&bs); // wrap, no copy + Guard G(outgoingLock); + outgoingData.setByteString(byteString, true); // move, no copy + markAsDirty(); + ret = 0; + } + break; + } + case OpcUaType_Boolean: { // Scope of Guard G + Guard G(outgoingLock); + if (strchr("YyTt1", *value)) + outgoingData.setBoolean(true); + else + outgoingData.setBoolean(false); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_Byte: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setByte(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_SByte: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setSByte(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt16: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setUInt16(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int16: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setInt16(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt32: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setUInt32(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int32: + l = strtol(value, &end, 0); + if (enumChoices) { + // first test enum strings then numeric values + // in case a string starts with a number but means a different value + for (const auto &it : *enumChoices) + if (it.second == value) { + l = static_cast(it.first); + ret = 0; + end = nullptr; + break; + } + if (ret != 0 && end != value) + for (const auto &it : *enumChoices) + if (l == it.first) { + ret = 0; + break; + } + if (ret != 0) + break; + } + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setInt32(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt64: + ul = strtoul(value, &end, 0); + if (end != value && isWithinRange(ul)) { + Guard G(outgoingLock); + outgoingData.setUInt64(static_cast(ul)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int64: + l = strtol(value, &end, 0); + if (end != value && isWithinRange(l)) { + Guard G(outgoingLock); + outgoingData.setInt64(static_cast(l)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Float: + d = strtod(value, &end); + if (end != value && isWithinRange(d)) { + Guard G(outgoingLock); + outgoingData.setFloat(static_cast(d)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Double: { + d = strtod(value, &end); + if (end != value) { + Guard G(outgoingLock); + outgoingData.setDouble(static_cast(d)); + markAsDirty(); + ret = 0; + } + break; + } + default: + errlogPrintf( + "%s : unsupported conversion from string to %s for outgoing data\n", prec->name, variantTypeString(type)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + return -1; + } + + if (ret != 0) { + errlogPrintf("%s : value \"%s\" out of range\n", prec->name, value); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret == 0) + dbgWriteScalar(); + return ret; +} + +inline void +DataElementUaSdkLeaf::dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const +{ + if (isLeaf() && debug()) { + std::cout << pconnector->getRecordName() << ": writing array of " << targetTypeName << "[" << targetSize + << "] as " << variantTypeString(outgoingData.type()) << "[" << outgoingData.arraySize() << "]" + << std::endl; + } +} + +// Write array for EPICS String / OpcUa_String +long +DataElementUaSdkLeaf::writeArray( + const char *value, epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec) +{ + long ret = 0; + + if (!incomingData.isArray()) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + switch (incomingData.type()) { + case OpcUaType_String: { + UaStringArray arr; + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + UaString(UaByteString(len, (OpcUa_Byte *) value + i * len)).copyTo(&arr[i]); + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_XmlElement: { + UaXmlElementArray arr; + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + UaByteString(len, (OpcUa_Byte *) value + i * len).copyTo(&arr[i]); + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedTextArray arr; + OpcUa_UInt32 arraySize = incomingData.arraySize(); + const OpcUa_LocalizedText *incoming + = static_cast(incomingData)->Value.Array.Value.LocalizedTextArray; + + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + OpcUa_String_AttachToString(const_cast(value), + static_cast(sep - value), + 0, + OpcUa_True, + OpcUa_False, + &arr[i].Locale); + } else if (i < arraySize) { + OpcUa_String_StrnCpy(&arr[i].Locale, &incoming[i].Locale, OPCUA_STRING_LENDONTCARE); + } else if (i > 0) { + OpcUa_String_StrnCpy(&arr[i].Locale, &arr[i - 1].Locale, OPCUA_STRING_LENDONTCARE); + } + OpcUa_String_AttachToString(const_cast(sep ? sep + 1 : value), + OPCUA_STRINGLENZEROTERMINATED, + 0, + OpcUa_True, + OpcUa_False, + &arr[i].Text); + value += len; + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedNameArray arr; + OpcUa_UInt32 arraySize = incomingData.arraySize(); + const OpcUa_QualifiedName *incoming + = static_cast(incomingData)->Value.Array.Value.QualifiedNameArray; + + arr.create(num); + for (epicsUInt32 i = 0; i < num; i++) { + const char *sep = static_cast(memchr(value, '|', len)); + if (sep) { + arr[i].NamespaceIndex = atoi(value); + } else if (i < arraySize) { + arr[i].NamespaceIndex = incoming[i].NamespaceIndex; + } else if (i > 0) { + arr[i].NamespaceIndex = arr[i - 1].NamespaceIndex; + } + OpcUa_String_AttachToString(const_cast(sep ? sep + 1 : value), + OPCUA_STRINGLENZEROTERMINATED, + 0, + OpcUa_True, + OpcUa_False, + &arr[i].Name); + value += len; + } + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + break; + } + default: + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } + dbgWriteArray(num, epicsTypeString(value)); + } + return ret; +} + +// Specialization for epicsUInt8 / OpcUa_Byte +// needed because UaByteArray API is different from all other UaXxxArray classes +// and because epicsUInt8 is also used for arrays of OpcUa_Boolean +// CAVEAT: changes in the template (in DataElementUaSdk.h) must be reflected here +template<> +long +DataElementUaSdkLeaf::writeArray(const epicsUInt8 *value, + const epicsUInt32 num, + OpcUa_BuiltInType targetType, + dbCommon *prec) +{ + long ret = 0; + + if (!incomingData.isArray() && incomingData.type() == OpcUaType_ByteString) { + UaByteString bs(num, reinterpret_cast(const_cast(value))); + { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setByteString(bs, true); // move, no copy + markAsDirty(); + } + + dbgWriteScalar(); + } else if (!incomingData.isArray()) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (incomingData.type() != OpcUaType_Byte && incomingData.type() != OpcUaType_Boolean) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (incomingData.type() == OpcUaType_Byte) { + UaByteArray arr(reinterpret_cast(value), static_cast(num)); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + } else { + UaBooleanArray arr(static_cast(num), const_cast(value)); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + } + dbgWriteArray(num, epicsTypeString(*value)); + } + return ret; +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_SByte, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Byte, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Int16, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_UInt16, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Int32, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_UInt32, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Int64, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_UInt64, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Float, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, num, OpcUaType_Double, prec); +} + +long +DataElementUaSdkLeaf::writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) +{ + return writeArray(value, len, num, OpcUaType_String, prec); +} + +} // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h new file mode 100644 index 00000000..6fdb4b57 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h @@ -0,0 +1,1030 @@ +/*************************************************************************\ +* Copyright (c) 2018-2025 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#ifndef DEVOPCUA_DATAELEMENTUASDKLEAF_H +#define DEVOPCUA_DATAELEMENTUASDKLEAF_H + +#include + +#include "DataElementUaSdk.h" +#include "RecordConnector.h" +#include "Update.h" +#include "UpdateQueue.h" + +namespace DevOpcua { + +class ItemUaSdk; + +typedef Update UpdateUaSdk; + +inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } +inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } +inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } +inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } +inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } +inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } +inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } +inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } +inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } +inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } +inline const char *epicsTypeString (const char *) { return "epicsString"; } + +// Template for range check when writing +template +inline bool +isWithinRange (const FROM &value) +{ + return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); +} + +// Specializations for unsigned to signed to avoid compiler warnings +template<> +inline bool +isWithinRange(const epicsUInt32 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt32 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt32 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool +isWithinRange(const epicsUInt64 &value) +{ + return !(value > static_cast(std::numeric_limits::max())); +} + +// Simple-check specializations for converting signed to unsigned of same or wider type +template<> +inline bool +isWithinRange(const epicsInt8 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt16 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt32 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt8 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt16 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt32 &value) +{ + return !(value < 0); +} +template<> +inline bool +isWithinRange(const epicsInt64 &value) +{ + return !(value < 0); +} + +// No-check-needed specializations for converting to same or wider type +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt32 &) +{ + return true; +} + +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsUInt32 &) +{ + return true; +} + +template<> +inline bool +isWithinRange(const epicsInt64 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt64 &) +{ + return true; +} +template<> +inline bool +isWithinRange(const epicsInt64 &) +{ + return true; +} + +template<> +inline bool +isWithinRange(const epicsFloat64 &) +{ + return true; +} + +class DataElementUaSdkLeaf + : public DataElement + , public DataElementUaSdk +{ +public: + /** + * @brief Constructor for DataElement (leaf) from record connector. + * + * Creates a final leaf element of the data structure. + * To avoid a circular dependency, + * the record connector holds a shared pointer to its leaf, + * while the data element has a weak pointer to the record connector. + * + * @param name name of the element (empty for root element) + * @param pitem pointer to corresponding ItemUaSdk + * @param pconnector pointer to record connector to link to + */ + DataElementUaSdkLeaf(const std::string &name, ItemUaSdk *pitem, RecordConnector *pconnector); + virtual ~DataElementUaSdkLeaf () override { delete enumChoices; } + + /* ElementTree node interface methods */ + virtual bool isLeaf () const override { return true; } + virtual void addChild (std::weak_ptr elem) override {} + virtual std::shared_ptr findChild (const std::string &name) const override + { + return std::shared_ptr(); + } + + /** + * @brief Create a DataElement (leaf) and add it to the item's dataTree. + * + * @param item item to add the element to + * @param pconnector pointer to the leaf's record connector + * @param elementPath full path to the element + */ + static void + addElementToTree(ItemUaSdk *item, RecordConnector *pconnector, const std::list &elementPath); + + virtual void show(const int level, const unsigned int indent) const override; + + virtual void setIncomingData(const UaVariant &value, + ProcessReason reason, + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr) override; + virtual void setIncomingEvent(ProcessReason reason) override; + virtual void setState(const ConnectionStatus state) override; + virtual const UaVariant &getOutgoingData() override; + virtual void clearOutgoingData() override; + virtual void requestRecordProcessing(const ProcessReason reason) const override; + virtual int debug() const override; + + /** + * @brief Read incoming data as a scalar epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as a scalar epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as a scalar epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as a scalar epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as classic C string (char[]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(char*,const size_t,dbCommon*,ProcessReason*,epicsUInt32*,char*,const + * epicsUInt32) + */ + + virtual long int readScalar(char *value, + const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *lenRead = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt8*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt8*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt8 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt16*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt16*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt16 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt32*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt32*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt64*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsUInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt64*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsFloat32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat32*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat32 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat64*,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat64 *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Read incoming data as array of EPICS String (char[40]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const + * epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) override; + + /** + * @brief Write outgoing scalar epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt32 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt64 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsUInt32 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) + */ + virtual long int writeScalar(const epicsFloat64 &value, dbCommon *prec) override; + + /** + * @brief Write outgoing classic C string (char[]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeScalar(const char *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) override; + + /** + * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) override; + +private: + virtual bool isDirty () const override { return isdirty; } + virtual void markAsDirty () override + { + isdirty = true; + pitem->markAsDirty(); + } + + void dbgWriteScalar() const; + void dbgReadScalar(const UpdateUaSdk *upd, const std::string &targetTypeName, const size_t targetSize = 0) const; + void dbgReadArray(const UpdateUaSdk *upd, const epicsUInt32 targetSize, const std::string &targetTypeName) const; + void checkWriteArray(OpcUa_BuiltInType expectedType, const std::string &targetTypeName) const; + void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; + + const epicsTime &getIncomingTimeStamp() const; + OpcUa_StatusCode getIncomingReadStatus() const; + + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int32 &value); + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_UInt32 &value); + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Int64 &value); + OpcUa_StatusCode UaVariant_to(const UaVariant &variant, OpcUa_Double &value); + + void UaVariant_set(UaVariant &variant, UaBooleanArray &value); + void UaVariant_set(UaVariant &variant, UaSByteArray &value); + void UaVariant_set(UaVariant &variant, UaByteArray &value); + void UaVariant_set(UaVariant &variant, UaInt16Array &value); + void UaVariant_set(UaVariant &variant, UaUInt16Array &value); + void UaVariant_set(UaVariant &variant, UaInt32Array &value); + void UaVariant_set(UaVariant &variant, UaUInt32Array &value); + void UaVariant_set(UaVariant &variant, UaInt64Array &value); + void UaVariant_set(UaVariant &variant, UaUInt64Array &value); + void UaVariant_set(UaVariant &variant, UaFloatArray &value); + void UaVariant_set(UaVariant &variant, UaDoubleArray &value); + void UaVariant_set(UaVariant &variant, UaStringArray &value); + void UaVariant_set(UaVariant &variant, UaXmlElementArray &value); + void UaVariant_set(UaVariant &variant, UaLocalizedTextArray &value); + void UaVariant_set(UaVariant &variant, UaQualifiedNameArray &value); + + // Read scalar value as templated function on EPICS type and OPC UA type + // value == nullptr is allowed and leads to the value being dropped (ignored), + // including the extended status + template + long readScalar (ET *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) + { + long ret = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + OT v; + if (OpcUa_IsNotGood(UaVariant_to(upd->getData(), v))) { + errlogPrintf( + "%s : incoming data (%s) out-of-bounds\n", prec->name, upd->getData().toString().toUtf8()); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + *value = v; + prec->udf = false; + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + return ret; + } + + // Read array value as templated function on EPICS type and OPC UA type + // (latter *must match* OPC UA type enum argument) + // CAVEAT: changes must also be reflected in specializations (in DataElementUaSdk.cpp) + template + long readArray (ET *value, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE + 1) + { + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: { + if (num && value) { + OpcUa_StatusCode stat = upd->getStatus(); + if (OpcUa_IsNotGood(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UaVariant &variant = upd->getData(); + if (!variant.isArray()) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (variant.type() != expectedType) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, + variantTypeString(variant.type()), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (OpcUa_IsUncertain(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = variant.arraySize(); + if (elemsWritten > num) + elemsWritten = num; + memcpy(value, + static_cast(variant)->Value.Array.Value.Array, + sizeof(ET) * elemsWritten); + } + } + if (statusCode) + *statusCode = stat; + if (statusText) { + strncpy(statusText, UaStatus(stat).toString().toUtf8(), statusTextLen); + statusText[statusTextLen - 1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) + *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; + } + + // Read array value for EPICS String / OpcUa_String + long readArray(char *value, + epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + OpcUa_BuiltInType expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen); + + // Write scalar value as templated function on EPICS type + template long writeScalar (const ET &value, dbCommon *prec) + { + long ret = 1; + + switch (incomingData.type()) { + case OpcUaType_Boolean: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setBoolean(value != 0); + markAsDirty(); + ret = 0; + break; + } + case OpcUaType_Byte: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setByte(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_SByte: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setSByte(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt16: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setUInt16(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int16: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setInt16(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt32: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setUInt32(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int32: // may be an enum + if (isWithinRange(value) + && (!enumChoices || enumChoices->find(static_cast(value)) != enumChoices->end())) { + Guard G(outgoingLock); + outgoingData.setInt32(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_UInt64: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setUInt64(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Int64: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setInt64(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Float: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setFloat(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_Double: + if (isWithinRange(value)) { + Guard G(outgoingLock); + outgoingData.setDouble(static_cast(value)); + markAsDirty(); + ret = 0; + } + break; + case OpcUaType_String: { // Scope of Guard G + Guard G(outgoingLock); + outgoingData.setString(static_cast(std::to_string(value).c_str())); + markAsDirty(); + break; + } + default: + errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", + prec->name, + epicsTypeString(value), + variantTypeString(incomingData.type())); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret != 0) { + errlogPrintf("%s : value out of range\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + + if (ret == 0) + dbgWriteScalar(); + return ret; + } + + // Write array value for EPICS String / OpcUa_String + long + writeArray(const char *value, epicsUInt32 len, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec); + + // Write array value as templated function on EPICS type, OPC UA container and simple (element) types + // (latter *must match* OPC UA type enum argument) + // CAVEAT: changes must also be reflected in the specialization (in DataElementUaSdk.cpp) + template + long writeArray (const ET *value, const epicsUInt32 num, OpcUa_BuiltInType targetType, dbCommon *prec) + { + long ret = 0; + + if (!incomingData.isArray()) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (incomingData.type() != targetType) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData.type()), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + // The array methods must cast away the constness of their value argument + // as the UA SDK API uses non-const parameters + ST *val = const_cast(reinterpret_cast(value)); + CT arr(static_cast(num), val); + { // Scope of Guard G + Guard G(outgoingLock); + UaVariant_set(outgoingData, arr); + markAsDirty(); + } + + dbgWriteArray(num, epicsTypeString(*value)); + } + return ret; + } + + UpdateQueue incomingQueue; /**< queue of incoming values */ +}; + +} // namespace DevOpcua + +#endif // DEVOPCUA_DATAELEMENTUASDKLEAF_H diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp b/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp new file mode 100644 index 00000000..55659a87 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp @@ -0,0 +1,489 @@ +/*************************************************************************\ +* Copyright (c) 2018-2025 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#include "DataElementUaSdkNode.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ItemUaSdk.h" +#include "RecordConnector.h" + +namespace DevOpcua { + +DataElementUaSdkNode::DataElementUaSdkNode (const std::string &name, class ItemUaSdk *item) + : DataElementUaSdk(name, item) + , timesrc(-1) + , mapped(false) +{} + +void +DataElementUaSdkNode::addChild (std::weak_ptr elem) +{ + elements.push_back(elem); +} + +std::shared_ptr +DataElementUaSdkNode::findChild (const std::string &name) const +{ + for (const auto &it : elements) + if (auto pit = it.lock()) + if (pit->name == name) + return pit; + return std::shared_ptr(); +} + +void +DataElementUaSdkNode::show (const int level, const unsigned int indent) const +{ + std::string ind(indent * 2, ' '); + std::cout << ind; + std::cout << "node=" << name << " children=" << elements.size() << " mapped=" << (mapped ? "y" : "n") << "\n"; + for (const auto &it : elements) { + if (auto pelem = it.lock()) { + pelem->show(level, indent + 1); + } + } +} + +void +DataElementUaSdkNode::createMap (const UaStructureDefinition &definition, const std::string *timefrom) +{ + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + + if (timefrom) { + for (int i = 0; i < definition.childrenCount(); i++) { + if (*timefrom == definition.child(i).name().toUtf8()) { + timesrc = i; + } + } + OpcUa_BuiltInType t = definition.child(timesrc).valueType(); + if (timesrc == -1) { + errlogPrintf("%s: timestamp element %s not found - using source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str()); + } else if (t != OpcUaType_DateTime) { + errlogPrintf("%s: timestamp element %s has invalid type %s - using " + "source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str(), + variantTypeString(t)); + timesrc = -1; + } + } + for (const auto &it : elements) { + auto pelem = it.lock(); + for (int i = 0; i < definition.childrenCount(); i++) { + if (pelem->name == definition.child(i).name().toUtf8()) + elementMap.insert({i, it}); + } + } + if (debug() >= 5) + std::cout << " ** " << elementMap.size() << "/" << elements.size() << " child elements mapped to a " + << "structure of " << definition.childrenCount() << " elements" << std::endl; + mapped = true; +} + +void +DataElementUaSdkNode::createMap (const UaLocalizedText &) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + if (pelem->name == "locale") + elementMap.insert({0, it}); + else if (pelem->name == "text") + elementMap.insert({1, it}); + else + errlogPrintf("Item %s %s element %s not found\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + pelem->name.c_str()); + } + mapped = true; +} + +void +DataElementUaSdkNode::createMap (const UaQualifiedName &) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + if (pelem->name == "namespaceIndex") + elementMap.insert({0, it}); + else if (pelem->name == "name") + elementMap.insert({1, it}); + else + errlogPrintf("Item %s %s element %s not found\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + pelem->name.c_str()); + } + mapped = true; +} + +void +DataElementUaSdkNode::setIncomingData (const UaVariant &value, + ProcessReason reason, + const std::string *timefrom, + const UaNodeId *typeId) +{ + incomingData = value; + + if (debug() >= 5) + std::cout << "Element " << name << " splitting structured data to " << elements.size() << " child elements" + << std::endl; + + switch (value.type()) { + case OpcUaType_ExtensionObject: { + UaExtensionObject extensionObject; + value.toExtensionObject(extensionObject); + if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) + extensionObject.changeEncoding(UaExtensionObject::Binary); + + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isNull()) { + errlogPrintf("Cannot get a structure definition for item %s element %s (dataTypeId %s " + "encodingTypeId %s) - check " + "access to type " + "dictionary\n", + pitem->nodeid->toString().toUtf8(), + name.c_str(), + extensionObject.dataTypeId().toString().toUtf8(), + extensionObject.encodingTypeId().toString().toUtf8()); + return; + } + + if (!mapped) + createMap(definition, timefrom); + + if (timefrom) { + if (timesrc >= 0) + pitem->tsData + = epicsTimeFromUaVariant(UaGenericStructureValue(extensionObject, definition).value(timesrc)); + else + pitem->tsData = pitem->tsSource; + } + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + OpcUa_StatusCode stat; + if (definition.isUnion()) { + UaGenericUnionValue genericValue(extensionObject, definition); + int index = genericValue.switchValue() - 1; + if (it.first == index) { + pelem->setIncomingData(genericValue.value(), reason); + stat = OpcUa_Good; + } else { + stat = OpcUa_BadNoData; + } + } else { + const UaVariant memberValue + = UaGenericStructureValue(extensionObject, definition).value(it.first, &stat); + if (stat == OpcUa_Good) { + pelem->setIncomingData(memberValue, reason); + } + } + if (stat == OpcUa_BadNoData) { + const UaStructureField &field = definition.child(it.first); + OpcUa_Variant fakeValue; + OpcUa_Variant_Initialize(&fakeValue); + fakeValue.Datatype = field.valueType(); + fakeValue.ArrayType = field.valueRank() != 0; + if (debug()) + std::cerr << pitem->recConnector->getRecordName() << " element " << pelem->name + << (definition.isUnion() ? " not taken choice " : " absent optional ") + << variantTypeString((OpcUa_BuiltInType) fakeValue.Datatype) + << (fakeValue.ArrayType ? " array" : " scalar") << std::endl; + pelem->setIncomingData(fakeValue, ProcessReason::readFailure); + } + } + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedText localizedText; + value.toLocalizedText(localizedText); + if (!mapped) + createMap(localizedText); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + UaVariant memberValue; + switch (it.first) { + case 0: + memberValue.setString(localizedText.locale()); + break; + case 1: + memberValue.setString(localizedText.text()); + break; + } + pelem->setIncomingData(memberValue, reason); + } + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedName qualifiedName; + value.toQualifiedName(qualifiedName); + if (!mapped) + createMap(qualifiedName); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + UaVariant memberValue; + switch (it.first) { + case 0: + memberValue.setUInt16(qualifiedName.namespaceIndex()); + break; + case 1: + memberValue.setString(qualifiedName.name()); + break; + } + pelem->setIncomingData(memberValue, reason); + } + break; + } + default: + errlogPrintf("%s: %s is no structured data but a %s\n", + pitem->recConnector->getRecordName(), + name.c_str(), + variantTypeString(value.type())); + } +} + +void +DataElementUaSdkNode::setIncomingEvent (ProcessReason reason) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setIncomingEvent(reason); + } + if (reason == ProcessReason::connectionLoss) { + elementMap.clear(); + timesrc = -1; + mapped = false; + } +} + +void +DataElementUaSdkNode::setState (const ConnectionStatus state) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setState(state); + } +} + +const UaVariant & +DataElementUaSdkNode::getOutgoingData () +{ + if (debug() >= 4) + std::cout << "Element " << name << " updating structured data from " << elements.size() << " child elements" + << std::endl; + + outgoingData = incomingData; + bool isdirty = false; + switch (outgoingData.type()) { + case OpcUaType_ExtensionObject: { + UaExtensionObject extensionObject; + outgoingData.toExtensionObject(extensionObject); + if (extensionObject.encoding() == UaExtensionObject::EncodeableObject) + extensionObject.changeEncoding(UaExtensionObject::Binary); + + UaStructureDefinition definition = pitem->structureDefinition(extensionObject.encodingTypeId()); + if (definition.isNull()) { + errlogPrintf("Cannot get a structure definition for extensionObject with dataTypeID %s " + "/ encodingTypeID %s - " + "check access to type " + "dictionary\n", + extensionObject.dataTypeId().toString().toUtf8(), + extensionObject.encodingTypeId().toString().toUtf8()); + return outgoingData; + } + + if (!mapped) + createMap(definition); + + if (definition.isUnion()) { + UaGenericUnionValue genericUnion(extensionObject, definition); + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + genericUnion.setValue(it.first + 1, pelem->getOutgoingData()); + pelem->isdirty = false; + isdirty = true; + updated = true; + } + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into union" : " ignored (not dirty)") << std::endl; + } + if (isdirty) + genericUnion.toExtensionObject(extensionObject); + + } else { + UaGenericStructureValue genericStruct(extensionObject, definition); + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + genericStruct.setField(it.first, pelem->getOutgoingData()); + pelem->isdirty = false; + isdirty = true; + updated = true; + } + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into structure" : " ignored (not dirty)") << std::endl; + } + if (isdirty) + genericStruct.toExtensionObject(extensionObject); + } + + if (isdirty) + outgoingData.setExtensionObject(extensionObject, OpcUa_True); + break; + } + case OpcUaType_LocalizedText: { + UaLocalizedText localizedText; + outgoingData.toLocalizedText(localizedText); + if (!mapped) + createMap(localizedText); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + switch (it.first) { + case 0: + localizedText.setLocale(pelem->getOutgoingData().toString()); + break; + case 1: + localizedText.setText(pelem->getOutgoingData().toString()); + break; + } + pelem->isdirty = false; + isdirty = true; + updated = true; + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into LocalizedText" : " ignored (not dirty)") << std::endl; + } + } + if (isdirty) + outgoingData.setLocalizedText(localizedText); + break; + } + case OpcUaType_QualifiedName: { + UaQualifiedName qualifiedName; + outgoingData.toQualifiedName(qualifiedName); + if (!mapped) + createMap(qualifiedName); + + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + bool updated = false; + { + Guard G(pelem->outgoingLock); + OpcUa_UInt16 ns; + if (pelem->isDirty()) { + switch (it.first) { + case 0: + if (pelem->getOutgoingData().toUInt16(ns) == OpcUa_Good) + qualifiedName.setNamespaceIndex(ns); + break; + case 1: + qualifiedName.setQualifiedName(pelem->getOutgoingData().toString(), + qualifiedName.namespaceIndex()); + break; + } + pelem->isdirty = false; + isdirty = true; + updated = true; + } + if (debug() >= 4) + std::cout << "Data from child element " << pelem->name + << (updated ? " inserted into QualifiedName" : " ignored (not dirty)") << std::endl; + } + } + if (isdirty) + outgoingData.setQualifiedName(qualifiedName); + break; + } + default: + errlogPrintf("%s: %s is no structured data but a %s\n", + pitem->recConnector->getRecordName(), + name.c_str(), + variantTypeString(outgoingData.type())); + return outgoingData; + } + if (debug() >= 4) { + if (isdirty) + std::cout << "Encoding changed data structure to outgoingData of element " << name << std::endl; + else + std::cout << "Returning unchanged outgoingData of element " << name << std::endl; + } + return outgoingData; +} + +void +DataElementUaSdkNode::clearOutgoingData () +{ + outgoingData.clear(); +} + +void +DataElementUaSdkNode::requestRecordProcessing (const ProcessReason reason) const +{ + for (const auto &it : elementMap) { + auto pelem = it.second.lock(); + pelem->requestRecordProcessing(reason); + } +} + +epicsTime +DataElementUaSdkNode::epicsTimeFromUaVariant (const UaVariant &data) const +{ + UaDateTime dt; + UaStatus s = data.toDateTime(dt); + if (s.isGood()) { + return ItemUaSdk::uaToEpicsTime(dt, 0); + } + return pitem->tsSource; +} + +#define LOG_ERROR_AND_RETURN(method) \ + errlogPrintf("DataElementUaSdkNode::%s called on a node element. This should not happen.\n", #method); \ + return 1; +} // namespace DevOpcua diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkNode.h b/devOpcuaSup/UaSdk/DataElementUaSdkNode.h new file mode 100644 index 00000000..7cf6b035 --- /dev/null +++ b/devOpcuaSup/UaSdk/DataElementUaSdkNode.h @@ -0,0 +1,68 @@ +/*************************************************************************\ +* Copyright (c) 2018-2025 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + * + * based on prototype work by Bernhard Kuner + * and example code from the Unified Automation C++ Based OPC UA Client SDK + */ + +#ifndef DEVOPCUA_DATAELEMENTUASDKNODE_H +#define DEVOPCUA_DATAELEMENTUASDKNODE_H + +#include +#include +#include + +#include + +#include "DataElementUaSdk.h" + +namespace DevOpcua { + +class DataElementUaSdkNode : public DataElementUaSdk +{ +public: + DataElementUaSdkNode(const std::string &name, class ItemUaSdk *item); + virtual ~DataElementUaSdkNode () override {} + + /* ElementTree node interface methods */ + virtual bool isLeaf() const override { return false; } + virtual void addChild(std::weak_ptr elem) override; + virtual std::shared_ptr findChild(const std::string &name) const override; + + virtual void show(const int level, const unsigned int indent) const override; + + virtual void setIncomingData(const UaVariant &value, + ProcessReason reason, + const std::string *timefrom = nullptr, + const UaNodeId *typeId = nullptr) override; + virtual void setIncomingEvent(ProcessReason reason) override; + virtual void setState(const ConnectionStatus state) override; + virtual const UaVariant &getOutgoingData() override; + virtual void clearOutgoingData() override; + virtual void requestRecordProcessing(const ProcessReason reason) const override; + +private: + virtual bool isDirty() const override { return true; } + virtual void markAsDirty() override {} + + void createMap(const UaStructureDefinition& definition, const std::string *timefrom = nullptr); + void createMap(const UaLocalizedText&); + void createMap(const UaQualifiedName&); + + epicsTime epicsTimeFromUaVariant(const UaVariant &data) const; + + std::vector> elements; + std::unordered_map> elementMap; + int timesrc; + bool mapped; +}; + +} + +#endif // DEVOPCUA_DATAELEMENTUASDKNODE_H diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index ad2ac308..2725174f 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.cpp +++ b/devOpcuaSup/UaSdk/ItemUaSdk.cpp @@ -11,40 +11,41 @@ * and example code from the Unified Automation C++ Based OPC UA Client SDK */ +#include "ItemUaSdk.h" + #include #include +#include #include #include -#include -#include "devOpcua.h" +#include "DataElementUaSdk.h" #include "RecordConnector.h" -#include "opcuaItemRecord.h" -#include "ItemUaSdk.h" -#include "SubscriptionUaSdk.h" #include "SessionUaSdk.h" -#include "DataElementUaSdk.h" +#include "SubscriptionUaSdk.h" +#include "devOpcua.h" +#include "opcuaItemRecord.h" -namespace DevOpcua { +namespace DevOpcua +{ using namespace UaClientSdk; /* Specific implementation of Item's factory method */ Item * -Item::newItem(const linkInfo &info) +Item::newItem (const linkInfo &info) { return new ItemUaSdk(info); } -ItemUaSdk::ItemUaSdk(const linkInfo &info) +ItemUaSdk::ItemUaSdk (const linkInfo &info) : Item(info) , subscription(nullptr) , session(nullptr) , registered(false) , revisedSamplingInterval(0.0) , revisedQueueSize(0) - , dataTree(this) , dataTreeDirty(false) , lastStatus(OpcUa_BadServerNotConnected) , lastReason(ProcessReason::connectionLoss) @@ -78,7 +79,7 @@ ItemUaSdk::rebuildNodeId () } void -ItemUaSdk::requestWriteIfDirty() +ItemUaSdk::requestWriteIfDirty () { Guard G(dataTreeWriteLock); if (dataTreeDirty) @@ -100,19 +101,15 @@ ItemUaSdk::show (int level) const std::cout << ";s=" << linkinfo.identifierString; std::cout << " record=" << recConnector->getRecordName() << " state=" << connectionStatusString(recConnector->state()) - << " status=" << UaStatus(lastStatus).toString().toUtf8() - << " dataDirty=" << (dataTreeDirty ? "y" : "n") + << " status=" << UaStatus(lastStatus).toString().toUtf8() << " dataDirty=" << (dataTreeDirty ? "y" : "n") << " context=" << linkinfo.subscription << "@" << session->getName() << " sampling=" << revisedSamplingInterval << "(" << linkinfo.samplingInterval << ")" - << " deadband=" << linkinfo.deadband - << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" - << " cqsize=" << linkinfo.clientQueueSize - << " discard=" << (linkinfo.discardOldest ? "old" : "new") + << " deadband=" << linkinfo.deadband << " qsize=" << revisedQueueSize << "(" << linkinfo.queueSize << ")" + << " cqsize=" << linkinfo.clientQueueSize << " discard=" << (linkinfo.discardOldest ? "old" : "new") << " timestamp=" << linkOptionTimestampString(linkinfo.timestamp); if (linkinfo.timestamp == LinkOptionTimestamp::data) std::cout << "@" << linkinfo.timestampElement; - std::cout << " bini=" << linkOptionBiniString(linkinfo.bini) - << " output=" << (linkinfo.isOutput ? "y" : "n") + std::cout << " bini=" << linkOptionBiniString(linkinfo.bini) << " output=" << (linkinfo.isOutput ? "y" : "n") << " monitor=" << (linkinfo.monitor ? "y" : "n") << " registered=" << (registered ? nodeid->toString().toUtf8() : "-") << "(" << (linkinfo.registerNode ? "y" : "n") << ")" << std::endl; @@ -125,13 +122,14 @@ ItemUaSdk::show (int level) const } } -int ItemUaSdk::debug() const +int +ItemUaSdk::debug () const { return recConnector->debug(); } void -ItemUaSdk::copyAndClearOutgoingData(_OpcUa_WriteValue &wvalue) +ItemUaSdk::copyAndClearOutgoingData (_OpcUa_WriteValue &wvalue) { Guard G(dataTreeWriteLock); if (auto pd = dataTree.root().lock()) { @@ -146,12 +144,12 @@ ItemUaSdk::uaToEpicsTime (const UaDateTime &dt, const OpcUa_UInt16 pico10) { epicsTimeStamp ts; ts.secPastEpoch = static_cast(dt.toTime_t()) - POSIX_TIME_AT_EPICS_EPOCH; - ts.nsec = static_cast(static_cast(dt) % UA_SECS_TO_100NS) * 100 + pico10 / 100; + ts.nsec = static_cast(static_cast(dt) % UA_SECS_TO_100NS) * 100 + pico10 / 100; return epicsTime(ts); } void -ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason, const UaNodeId* typeId) +ItemUaSdk::setIncomingData (const OpcUa_DataValue &value, ProcessReason reason, const UaNodeId *typeId) { tsClient = epicsTime::getCurrent(); if (OpcUa_IsNotBad(value.StatusCode)) { @@ -182,9 +180,8 @@ ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason, c } if (linkinfo.isItemRecord) { - if (recConnector->state() == ConnectionStatus::initialRead - && reason == ProcessReason::readComplete - && recConnector->bini() == LinkOptionBini::write) { + if (recConnector->state() == ConnectionStatus::initialRead && reason == ProcessReason::readComplete + && recConnector->bini() == LinkOptionBini::write) { recConnector->setState(ConnectionStatus::initialWrite); recConnector->requestRecordProcessing(ProcessReason::writeRequest); } else { @@ -194,7 +191,7 @@ ItemUaSdk::setIncomingData(const OpcUa_DataValue &value, ProcessReason reason, c } void -ItemUaSdk::setIncomingEvent(const ProcessReason reason) +ItemUaSdk::setIncomingEvent (const ProcessReason reason) { tsClient = epicsTime::getCurrent(); setReason(reason); @@ -215,7 +212,7 @@ ItemUaSdk::setIncomingEvent(const ProcessReason reason) } void -ItemUaSdk::setState(const ConnectionStatus state) +ItemUaSdk::setState (const ConnectionStatus state) { if (auto pd = dataTree.root().lock()) { pd->setState(state); @@ -225,7 +222,7 @@ ItemUaSdk::setState(const ConnectionStatus state) } void -ItemUaSdk::markAsDirty() +ItemUaSdk::markAsDirty () { if (recConnector->plinkinfo->isItemRecord) { Guard G(dataTreeWriteLock); @@ -238,12 +235,12 @@ ItemUaSdk::markAsDirty() } void -ItemUaSdk::getStatus(epicsUInt32 *code, char *text, const epicsUInt32 len, epicsTimeStamp *ts) +ItemUaSdk::getStatus (epicsUInt32 *code, char *text, const epicsUInt32 len, epicsTimeStamp *ts) { *code = lastStatus.code(); if (text && len) { strncpy(text, lastStatus.toString().toUtf8(), len); - text[len-1] = '\0'; + text[len - 1] = '\0'; } if (ts && recConnector) { diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.h b/devOpcuaSup/UaSdk/ItemUaSdk.h index 409b19fa..2122613a 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.h +++ b/devOpcuaSup/UaSdk/ItemUaSdk.h @@ -24,7 +24,6 @@ #include #include "Item.h" -#include "opcuaItemRecord.h" #include "devOpcua.h" #include "ElementTree.h" #include "SessionUaSdk.h" @@ -35,6 +34,7 @@ using namespace UaClientSdk; class SubscriptionUaSdk; class DataElementUaSdk; +class DataElementUaSdkNode; struct linkInfo; /** @@ -45,6 +45,8 @@ struct linkInfo; class ItemUaSdk : public Item { friend class DataElementUaSdk; + friend class DataElementUaSdkNode; + friend class DataElementUaSdkLeaf; public: /** @@ -222,7 +224,7 @@ class ItemUaSdk : public Item bool registered; /**< flag for registration status */ OpcUa_Double revisedSamplingInterval; /**< server-revised sampling interval */ OpcUa_UInt32 revisedQueueSize; /**< server-revised queue size */ - ElementTree dataTree; /**< data element tree */ + ElementTree dataTree; /**< data element tree */ epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ bool dataTreeDirty; /**< true if any element has been modified */ UaStatusCode lastStatus; /**< status code of most recent service */ diff --git a/devOpcuaSup/UaSdk/Makefile.config b/devOpcuaSup/UaSdk/Makefile.config index 5573499d..36b0743d 100644 --- a/devOpcuaSup/UaSdk/Makefile.config +++ b/devOpcuaSup/UaSdk/Makefile.config @@ -27,7 +27,8 @@ opcua_SRCS += SessionUaSdk.cpp opcua_SRCS += Subscription.cpp opcua_SRCS += SubscriptionUaSdk.cpp opcua_SRCS += ItemUaSdk.cpp -opcua_SRCS += DataElementUaSdk.cpp +opcua_SRCS += DataElementUaSdkNode.cpp +opcua_SRCS += DataElementUaSdkLeaf.cpp DBD_INSTALLS += opcua.dbd diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.cpp b/devOpcuaSup/UaSdk/SessionUaSdk.cpp index eba12d3c..ac07796c 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SessionUaSdk.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -53,7 +52,7 @@ #include "RequestQueueBatcher.h" #include "SessionUaSdk.h" #include "SubscriptionUaSdk.h" -#include "DataElementUaSdk.h" +#include "DataElementUaSdkNode.h" #include "ItemUaSdk.h" namespace DevOpcua { diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index 9cc95a33..72f71ddb 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -31,6 +31,7 @@ #define epicsExportSharedSymbols #include "SubscriptionUaSdk.h" +#include "RecordConnector.h" #include "ItemUaSdk.h" #include "DataElementUaSdk.h" #include "Registry.h" diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index b86899e5..43ed4231 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -13,10 +13,7 @@ #ifndef DEVOPCUA_DATAELEMENTOPEN62541_H #define DEVOPCUA_DATAELEMENTOPEN62541_H -#include "DataElement.h" -#include "RecordConnector.h" #include "Update.h" -#include "UpdateQueue.h" #include "ItemOpen62541.h" #include @@ -36,26 +33,11 @@ #define UA_STATUS_IS_UNCERTAIN(status) (((status)&UA_STATUSCODE_UNCERTAIN)!=0) #include -#include -#include -#include namespace DevOpcua { typedef Update UpdateOpen62541; -inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } -inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } -inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } -inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } -inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } -inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } -inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } -inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } -inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } -inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } -inline const char *epicsTypeString (const char*) { return "epicsString"; } - inline const char * variantTypeString (const UA_DataType *type) { @@ -77,222 +59,35 @@ inline int typeKindOf(const UA_Variant& v) return typeKindOf(v.type); } -// Template for range check when writing -template -inline bool isWithinRange (const FROM &value) { - return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); -} - -// Specializations for unsigned to signed to avoid compiler warnings -template<> -inline bool isWithinRange (const unsigned int &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned int &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned int &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long &value) { - return !(value > std::numeric_limits::max()); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -template<> -inline bool isWithinRange (const unsigned long long &value) { - return !(value > static_cast(std::numeric_limits::max())); -} - -// Simple-check specializations for converting signed to unsigned of same or wider type -template<> inline bool isWithinRange (const char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const short &value) { return !(value < 0); } -template<> inline bool isWithinRange (const int &value) { return !(value < 0); } -template<> inline bool isWithinRange (const char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const short &value) { return !(value < 0); } -template<> inline bool isWithinRange (const int &value) { return !(value < 0); } -template<> inline bool isWithinRange (const long &value) { return !(value < 0); } -template<> inline bool isWithinRange (const char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } -template<> inline bool isWithinRange (const short &value) { return !(value < 0); } -template<> inline bool isWithinRange (const int &value) { return !(value < 0); } -template<> inline bool isWithinRange (const long &value) { return !(value < 0); } -template<> inline bool isWithinRange (const long long &value) { return !(value < 0); } - -// No-check-needed specializations for converting to same or wider type -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } -template<> inline bool isWithinRange (const int &) { return true; } - -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } -template<> inline bool isWithinRange (const unsigned int &) { return true; } - -template<> inline bool isWithinRange (const long &) { return true; } -template<> inline bool isWithinRange (const long &) { return true; } - -template<> inline bool isWithinRange (const long long &) { return true; } -template<> inline bool isWithinRange (const long long &) { return true; } -template<> inline bool isWithinRange (const long long &) { return true; } - -template<> inline bool isWithinRange (const double &) { return true; } - -// long long may or may not be the same size as long -template<> -inline bool isWithinRange (const long long &value) { - return sizeof(long long) > sizeof(long) ? !(value < 0 || value > static_cast(std::numeric_limits::max())) : !(value < 0); -} - -// Helper function to convert strings to numeric types - -inline bool string_to(const std::string& s, epicsInt32& value) { - try { - value = std::stol(s, 0, 0); - return true; - } catch (...) { - return false; - } -} - -inline bool string_to(const std::string& s, epicsInt64& value) { - try { - value = std::stoll(s, 0, 0); - return true; - } catch (...) { - return false; - } -} - -inline bool string_to(const std::string& s, epicsUInt32& value) { - try { - long long v = std::stoll(s, 0, 0); - if (!isWithinRange(v)) return false; - value = static_cast(v); - return true; - } catch (...) { - return false; - } -} - -inline bool string_to(const std::string& s, epicsFloat64& value) { - try { - value = std::stod(s, 0); - return true; - } catch (...) { - return false; - } -} - /** * @brief The DataElementOpen62541 implementation of a single piece of data. * * See DevOpcua::DataElement */ -class DataElementOpen62541 : public DataElement +class DataElementOpen62541 { -public: - /** - * @brief Constructor for DataElement from record connector. - * - * Creates the final (leaf) element of the data structure. The record connector - * holds a shared pointer to its leaf, while the data element has a weak pointer - * to the record connector. - * - * @param name name of the element (empty for root element) - * @param pitem pointer to corresponding ItemOpen62541 - * @param pconnector pointer to record connector to link to - */ - DataElementOpen62541(const std::string &name, - ItemOpen62541 *pitem, - RecordConnector *pconnector); - - /** - * @brief Constructor for DataElement from child element. - * - * Creates an intermediate (node) element of the data structure. The child holds - * a shared pointer, while the parent has a weak pointer in its list/map of child - * nodes, to facilitate traversing the structure when data updates come in. - * - * @param name name of the element - * @param item pointer to corresponding ItemOpen62541 - */ - DataElementOpen62541(const std::string &name, - ItemOpen62541 *item); + friend class DataElementOpen62541Node; + friend class DataElementOpen62541Leaf; - /** - * @brief Create a DataElement and add it to the item's dataTree. - * - * @param item item to add the element to - * @param pconnector pointer to the leaf's record connector - * @param elementPath full path to the element - */ - static void addElementToTree(ItemOpen62541 *item, - RecordConnector *pconnector, - const std::list &elementPath); +public: + DataElementOpen62541 (const std::string &name, class ItemOpen62541 *pitem) + : name(name) + , pitem(pitem) + , outgoingLock(pitem->dataTreeWriteLock) + {} + virtual ~DataElementOpen62541(); /* ElementTree node interface methods */ - void - addChild(std::weak_ptr elem) - { - elements.push_back(elem); - } - - std::shared_ptr - findChild(const std::string &name) - { - for (auto it : elements) - if (auto pit = it.lock()) - if (pit->name == name) - return pit; - return std::shared_ptr(); - } - - void - setParent(std::shared_ptr elem) - { - parent = elem; - } + void setParent (std::shared_ptr elem) { parent = elem; } + virtual bool isLeaf() const = 0; + virtual void addChild(std::weak_ptr elem) = 0; + virtual std::shared_ptr findChild(const std::string &name) const = 0; /** - * @brief Print configuration and status. See DevOpcua::DataElement::show + * @brief Print configuration and status. + * See DevOpcua::DataElement::show */ - void show(const int level, const unsigned int indent) const override; + virtual void show(const int level, const unsigned int indent) const = 0; /** * @brief Push an incoming data value into the DataElement. @@ -304,9 +99,8 @@ class DataElementOpen62541 : public DataElement * @param reason reason for this value update * @param timefrom name of element to read item timestamp from */ - void setIncomingData(const UA_Variant &value, - ProcessReason reason, - const std::string *timefrom = nullptr); + virtual void setIncomingData(const UA_Variant &value, ProcessReason reason, const std::string *timefrom = nullptr) + = 0; /** * @brief Push an incoming event into the DataElement. @@ -316,12 +110,12 @@ class DataElementOpen62541 : public DataElement * * @param reason reason for this value update */ - void setIncomingEvent(ProcessReason reason); + virtual void setIncomingEvent(ProcessReason reason) = 0; /** * @brief Set the EPICS-related state of the element and all sub-elements. */ - void setState(const ConnectionStatus state); + virtual void setState(const ConnectionStatus state) = 0; /** * @brief Get the outgoing data value from the DataElement. @@ -331,385 +125,8 @@ class DataElementOpen62541 : public DataElement * * @return reference to outgoing data */ - const UA_Variant &getOutgoingData(); - - /** - * @brief Read incoming data as a scalar epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsInt64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsUInt32 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as a scalar epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readScalar(epicsFloat64 *value, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as classic C string (char[]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readScalar(char*,const epicsUInt32,dbCommon*,ProcessReason*,epicsUInt32*,epicsUInt32*,char*,const epicsUInt32) - */ - - virtual long int readScalar(char *value, const epicsUInt32 len, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32* lenRead = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt8. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt8 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt16. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt16 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsUInt64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsUInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsUInt64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat32. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat32 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of epicsFloat64. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(epicsFloat64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(epicsFloat64 *value, const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Read incoming data as array of EPICS String (char[40]). - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) - */ - virtual long int readArray(char *value, epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - dbCommon *prec, - ProcessReason *nextReason = nullptr, - epicsUInt32 *statusCode = nullptr, - char *statusText = nullptr, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; - - /** - * @brief Write outgoing scalar epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) - */ - virtual long int writeScalar(const epicsInt64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) - */ - virtual long int writeScalar(const epicsUInt32 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing scalar epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) - */ - virtual long int writeScalar(const epicsFloat64 &value, - dbCommon *prec) override; - - /** - * @brief Write outgoing classic C string (char[]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeScalar(const char *value, - const epicsUInt32 len, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt8 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt8 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt16 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt16 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsUInt64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsUInt64 *value, - const epicsUInt32 num, - dbCommon *prec) override; + virtual const UA_Variant &getOutgoingData() = 0; - /** - * @brief Write outgoing array of epicsFloat32 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat32 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of epicsFloat64 data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const epicsFloat64 *value, - const epicsUInt32 num, - dbCommon *prec) override; - - /** - * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. - * - * See the DataElement API method it overrides - * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) - */ - virtual long int writeArray(const char *value, const epicsUInt32 len, - const epicsUInt32 num, - dbCommon *prec) override; /** * @brief Clear (discard) the current outgoing data. @@ -721,7 +138,7 @@ class DataElementOpen62541 : public DataElement * oldest element from the queue, allowing access to the next element * with the next send. */ - virtual void clearOutgoingData() { UA_Variant_clear(&outgoingData); } + virtual void clearOutgoingData() = 0; /** * @brief Move the contents of the current outgoing data. @@ -731,7 +148,8 @@ class DataElementOpen62541 : public DataElement * * Call holding outgoingLock! */ - void* moveOutgoingData() { + void *moveOutgoingData () + { void* data = outgoingData.data; outgoingData.data = nullptr; UA_Variant_clear(&outgoingData); @@ -742,544 +160,38 @@ class DataElementOpen62541 : public DataElement * @brief Create processing requests for record(s) attached to this element. * See DevOpcua::DataElement::requestRecordProcessing */ - virtual void requestRecordProcessing(const ProcessReason reason) const override; + virtual void requestRecordProcessing(const ProcessReason reason) const = 0; /** * @brief Get debug level from record (via RecordConnector). * @return debug level */ - int debug() const { return (isLeaf() ? pconnector->debug() : pitem->debug()); } - -private: - void dbgWriteScalar () const; - void dbgReadScalar(const UpdateOpen62541 *upd, - const std::string &targetTypeName, - const size_t targetSize = 0) const; - void dbgReadArray(const UpdateOpen62541 *upd, - const epicsUInt32 targetSize, - const std::string &targetTypeName) const; - void checkWriteArray(const UA_DataType *expectedType, const std::string &targetTypeName) const; - void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; - bool updateDataInStruct(void* container, std::shared_ptr pelem); - void createMap(const UA_DataType *type, const std::string* timefrom = nullptr); - - // Structure always returns true to ensure full traversal - bool isDirty() const { return isdirty || !isleaf; } - void - markAsDirty() - { - isdirty = true; - pitem->markAsDirty(); - } - - // Get the time stamp from the incoming object - const epicsTime &getIncomingTimeStamp() const { - ProcessReason reason = pitem->getReason(); - if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) - && isLeaf()) - switch (pconnector->plinkinfo->timestamp) { - case LinkOptionTimestamp::server: - return pitem->tsServer; - case LinkOptionTimestamp::source: - return pitem->tsSource; - case LinkOptionTimestamp::data: - return pitem->tsData; - } - return pitem->tsClient; - } - - // Get the read status from the incoming object - UA_StatusCode getIncomingReadStatus() const { return pitem->getLastStatus(); } - - // Read scalar value as templated function on EPICS type - // value == nullptr is allowed and leads to the value being dropped (ignored), - // including the extended status - template - long - readScalar (ET *value, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen) - { - long ret = 1; - - if (incomingQueue.empty()) { - errlogPrintf("%s: incoming data queue empty\n", prec->name); - if (nextReason) - *nextReason = ProcessReason::none; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadScalar(upd.get(), epicsTypeString(*value)); - prec->udf = false; + virtual int debug() const { return pitem->debug(); } - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (value) { - UA_StatusCode stat = upd->getStatus(); - if (UA_STATUS_IS_BAD(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - } else { - // Valid OPC UA value, so try to convert - UA_Variant &data = upd->getData(); - switch(typeKindOf(data)) { - case UA_DATATYPEKIND_BOOLEAN: - *value = (*static_cast(data.data) != 0); - ret = 0; - break; - case UA_DATATYPEKIND_BYTE: - if (isWithinRange(*static_cast(data.data))) { - *value = *static_cast(data.data); - ret = 0; - } - break; - case UA_DATATYPEKIND_SBYTE: - if (isWithinRange(*static_cast(data.data))) { - *value = *static_cast(data.data); - ret = 0; - } - break; - case UA_DATATYPEKIND_INT16: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_UINT16: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_INT32: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_UINT32: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_INT64: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_UINT64: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_FLOAT: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_DOUBLE: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - ret = 0; - } - break; - case UA_DATATYPEKIND_ENUM: - if (isWithinRange(*static_cast(data.data))) { - *value = static_cast(*static_cast(data.data)); - if (!enumChoices || enumChoices->find(static_cast(*value)) != enumChoices->end()) - ret = 0; - } - break; - case UA_DATATYPEKIND_STRING: - { - UA_String* s = static_cast(data.data); - if (string_to(std::string(reinterpret_cast(s->data), s->length), *value)) - ret = 0; - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - UA_LocalizedText* lt = static_cast(data.data); - if (string_to(std::string(reinterpret_cast(lt->text.data), lt->text.length), *value)) - ret = 0; - break; - } - default: - errlogPrintf("%s : unsupported type kind %s for incoming data\n", - prec->name, typeKindName(typeKindOf(data))); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - } - if (ret == 1) { - UA_String datastring = UA_STRING_NULL; - if (data.type) - UA_print(data.data, data.type, &datastring); - errlogPrintf("%s : incoming data (%s %.*s) out-of-bounds for %s\n", - prec->name, - variantTypeString(data), - static_cast(datastring.length), datastring.data, - epicsTypeString(*value)); - UA_String_clear(&datastring); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - } else { - if (UA_STATUS_IS_UNCERTAIN(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - } - UA_Variant_clear(&data); - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } + const std::string name; - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - return ret; - } - - // Read array value as templated function on EPICS type - // CAVEAT: changes must also be reflected in specializations (in DataElementOpen62541.cpp) - template - long - readArray (ET *value, const epicsUInt32 num, - epicsUInt32 *numRead, - const UA_DataType *expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) - { - long ret = 0; - epicsUInt32 elemsWritten = 0; - - if (incomingQueue.empty()) { - errlogPrintf("%s : incoming data queue empty\n", prec->name); - *numRead = 0; - return 1; - } - - ProcessReason nReason; - std::shared_ptr upd = incomingQueue.popUpdate(&nReason); - dbgReadArray(upd.get(), num, epicsTypeString(*value)); - - switch (upd->getType()) { - case ProcessReason::readFailure: - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::connectionLoss: - (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); - ret = 1; - break; - case ProcessReason::incomingData: - case ProcessReason::readComplete: - { - if (num && value) { - UA_StatusCode stat = upd->getStatus(); - if (UA_STATUS_IS_BAD(stat)) { - // No valid OPC UA value - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - // Valid OPC UA value, so try to convert - UA_Variant &variant = upd->getData(); - if (UA_Variant_isScalar(&variant)) { - errlogPrintf("%s : incoming data is not an array\n", prec->name); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else if (variant.type != expectedType) { - errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", - prec->name, variantTypeString(variant), epicsTypeString(*value)); - (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); - ret = 1; - } else { - if (UA_STATUS_IS_UNCERTAIN(stat)) { - (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); - } - elemsWritten = static_cast(num) < variant.arrayLength ? num : static_cast(variant.arrayLength); - memcpy(value, variant.data, sizeof(ET) * elemsWritten); - } - UA_Variant_clear(&variant); - } - if (statusCode) *statusCode = stat; - if (statusText) { - strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); - statusText[statusTextLen-1] = '\0'; - } - } - break; - } - default: - break; - } - - prec->time = upd->getTimeStamp(); - if (nextReason) *nextReason = nReason; - if (num && value) - *numRead = elemsWritten; - return ret; - } - - // Read array value for EPICS String / UA_String - long - readArray (char *value, const epicsUInt32 len, - const epicsUInt32 num, - epicsUInt32 *numRead, - const UA_DataType *expectedType, - dbCommon *prec, - ProcessReason *nextReason, - epicsUInt32 *statusCode, - char *statusText, - const epicsUInt32 statusTextLen); - - // Write scalar value as templated function on EPICS type - template - long - writeScalar (const ET &value, - dbCommon *prec) - { - long ret = 1; - UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; - - { // Scope of Guard G - Guard G(outgoingLock); - UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard - switch (typeKindOf(incomingData)) { - case UA_DATATYPEKIND_BOOLEAN: - { - UA_Boolean val = (value != 0); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); - markAsDirty(); - ret = 0; - break; - } - case UA_DATATYPEKIND_BYTE: - if (isWithinRange(value)) { - UA_Byte val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_SBYTE: - if (isWithinRange(value)) { - UA_SByte val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_UINT16: - if (isWithinRange(value)) { - UA_UInt16 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_INT16: - if (isWithinRange(value)) { - UA_Int16 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_UINT32: - if (isWithinRange(value)) { - UA_UInt32 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_ENUM: - case UA_DATATYPEKIND_INT32: - if (isWithinRange(value) && - (!enumChoices || - enumChoices->find(static_cast(value)) != enumChoices->end())) { - UA_Int32 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_UINT64: - if (isWithinRange(value)) { - UA_UInt64 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_INT64: - if (isWithinRange(value)) { - UA_Int64 val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_FLOAT: - if (isWithinRange(value)) { - UA_Float val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_DOUBLE: - if (isWithinRange(value)) { - UA_Double val = static_cast(value); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); - markAsDirty(); - ret = 0; - } - break; - case UA_DATATYPEKIND_STRING: - { - std::string strval = std::to_string(value); - UA_String val; - val.length = strval.length(); - val.data = const_cast(reinterpret_cast(strval.c_str())); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); - markAsDirty(); - ret = 0; - break; - } - case UA_DATATYPEKIND_LOCALIZEDTEXT: - { - std::string strval = std::to_string(value); - UA_LocalizedText val; - val.locale = reinterpret_cast(incomingData.data)->locale; - val.text.length = strval.length(); - val.text.data = const_cast(reinterpret_cast(strval.c_str())); - status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); - markAsDirty(); - ret = 0; - break; - } - default: - errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", - prec->name, epicsTypeString(value), variantTypeString(incomingData)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - } - if (ret != 0) { - errlogPrintf("%s : value out of range\n", - prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - } - if (ret == 0 && UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : scalar copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - - } - if (ret == 0) - dbgWriteScalar(); - return ret; - } - - // Write array value for EPICS String / UA_String - long - writeArray (const char *value, const epicsUInt32 len, - const epicsUInt32 num, - const UA_DataType *targetType, - dbCommon *prec); - - // Write array value as templated function on EPICS type, OPC UA container and simple (element) types - // (latter *must match* OPC UA type enum argument) - // CAVEAT: changes must also be reflected in the specialization (in DataElementOpen62541.cpp) - template - long - writeArray (const ET *value, const epicsUInt32 num, - const UA_DataType *targetType, - dbCommon *prec) - { - long ret = 0; - - if (UA_Variant_isScalar(&incomingData)) { - errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else if (incomingData.type != targetType) { - errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", - prec->name, - variantTypeString(incomingData), - variantTypeString(targetType), - epicsTypeString(*value)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - UA_StatusCode status; - { // Scope of Guard G - Guard G(outgoingLock); - UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard - status = UA_Variant_setArrayCopy(&outgoingData, value, num, targetType); - markAsDirty(); - } - if (UA_STATUS_IS_BAD(status)) { - errlogPrintf("%s : array copy failed: %s\n", - prec->name, UA_StatusCode_name(status)); - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } else { - dbgWriteArray(num, epicsTypeString(*value)); - } - } - return ret; - } +private: + virtual bool isDirty() const = 0; + virtual void markAsDirty() = 0; +protected: ItemOpen62541 *pitem; /**< corresponding item */ - std::vector> elements; /**< children (if node) */ std::shared_ptr parent; /**< parent */ - std::unordered_map> elementMap; - ptrdiff_t timesrc; + UA_Variant incomingData; /**< cache of latest incoming value */ + epicsMutex &outgoingLock; /**< data lock for outgoing value */ + UA_Variant outgoingData; /**< cache of latest outgoing value */ + bool isdirty; /**< outgoing value has been (or needs to be) updated */ - const UA_DataType *memberType = NULL; /**< type of this element */ + const UA_DataType *memberType = nullptr; /**< type of this element */ UA_Boolean isArray = false; /**< is this element an array? */ UA_Boolean isOptional = false; /**< is this element optional? */ size_t offset = 0; /**< data offset of this element in parent structure */ UA_UInt32 index = 0; /**< element index (for unions) */ - bool mapped; /**< child name to index mapping done */ - UpdateQueue incomingQueue; /**< queue of incoming values */ - UA_Variant incomingData; /**< cache of latest incoming value */ - epicsMutex &outgoingLock; /**< data lock for outgoing value */ - UA_Variant outgoingData; /**< cache of latest outgoing value */ - bool isdirty; /**< outgoing value has been (or needs to be) updated */ friend std::ostream& operator << (std::ostream& os, const DataElementOpen62541& element); }; - // print the full element name, e.g.: item elem.array[index].elem inline std::ostream& operator << (std::ostream& os, const DataElementOpen62541& element) { diff --git a/devOpcuaSup/open62541/DataElementOpen62541.cpp b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp similarity index 67% rename from devOpcuaSup/open62541/DataElementOpen62541.cpp rename to devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp index e438ee08..efb7c39d 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp @@ -11,8 +11,10 @@ */ #define epicsExportSharedSymbols -#include "DataElementOpen62541.h" +#include "DataElementOpen62541Leaf.h" +#include "DataElementOpen62541Node.h" #include "ItemOpen62541.h" +#include "RecordConnector.h" #include #include @@ -26,80 +28,52 @@ namespace DevOpcua { -/* Specific implementation of DataElement's "factory" method */ -void -DataElement::addElementToTree(Item *item, - RecordConnector *pconnector, - const std::list &elementPath) -{ - DataElementOpen62541::addElementToTree(static_cast(item), pconnector, elementPath); -} - -DataElementOpen62541::DataElementOpen62541 (const std::string &name, - ItemOpen62541 *item, - RecordConnector *pconnector) - : DataElement(pconnector, name) - , pitem(item) - , timesrc(-1) - , mapped(false) +DataElementOpen62541Leaf::DataElementOpen62541Leaf (const std::string &name, + ItemOpen62541 *item, + RecordConnector *pconnector) + : DataElementOpen62541(name, item) , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) { UA_Variant_init(&incomingData); UA_Variant_init(&outgoingData); } -DataElementOpen62541::DataElementOpen62541 (const std::string &name, - ItemOpen62541 *item) - : DataElement(name) - , pitem(item) - , timesrc(-1) - , mapped(false) - , incomingQueue(0ul) - , outgoingLock(pitem->dataTreeWriteLock) - , isdirty(false) +/* Explicitly implement the destructor here (allows the compiler to place the vtable) */ +DataElementOpen62541::~DataElementOpen62541() = default; + +/* Specific implementation of DataElement's "factory" method */ +void +DataElement::addElementToTree (Item *item, RecordConnector *pconnector, const std::list &elementPath) { - UA_Variant_init(&incomingData); - UA_Variant_init(&outgoingData); + DataElementOpen62541Leaf::addElementToTree(static_cast(item), pconnector, elementPath); } void -DataElementOpen62541::addElementToTree(ItemOpen62541 *item, - RecordConnector *pconnector, - const std::list &elementPath) +DataElementOpen62541Leaf::addElementToTree (ItemOpen62541 *item, + RecordConnector *pconnector, + const std::list &elementPath) { std::string name("[ROOT]"); if (elementPath.size()) name = elementPath.back(); - auto leaf = std::make_shared(name, item, pconnector); - item->dataTree.addLeaf(leaf, elementPath); + auto leaf = std::make_shared(name, item, pconnector); + item->dataTree.addLeaf(leaf, elementPath, item); // reference from connector after adding to the tree worked pconnector->setDataElement(leaf); + leaf->pconnector = pconnector; } void -DataElementOpen62541::show (const int level, const unsigned int indent) const +DataElementOpen62541Leaf::show (const int level, const unsigned int indent) const { - std::string ind(static_cast(indent)*2, ' '); // static_cast to avoid warning C26451 + std::string ind(static_cast(indent) * 2, ' '); // static_cast to avoid warning C26451 std::cout << ind; - if (isLeaf()) { - std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" - << pconnector->getRecordName() - << " type=" << variantTypeString(incomingData) - << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp) - << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) - << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; - } else { - std::cout << "node=" << name << " children=" << elements.size() - << " mapped=" << (mapped ? "y" : "n") << "\n"; - for (auto it : elements) { - if (auto pelem = it.lock()) { - pelem->show(level, indent + 1); - } - } - } + std::cout << "leaf=" << name << " record(" << pconnector->getRecordType() << ")=" << pconnector->getRecordName() + << " type=" << variantTypeString(incomingData) + << " timestamp=" << linkOptionTimestampString(pconnector->plinkinfo->timestamp) + << " bini=" << linkOptionBiniString(pconnector->plinkinfo->bini) + << " monitor=" << (pconnector->plinkinfo->monitor ? "y" : "n") << "\n"; } #ifndef UA_ENABLE_TYPEDESCRIPTION @@ -121,166 +95,10 @@ memberTypeOf(const UA_DataType *type, const UA_DataTypeMember *m) { #endif } -UA_UInt32 -UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, - size_t *outOffset, const UA_DataType **outMemberType, - UA_Boolean *outIsArray, UA_Boolean *outIsOptional) { - - size_t offset = 0; - switch(type->typeKind) { - case UA_DATATYPEKIND_STRUCTURE: - case UA_DATATYPEKIND_OPTSTRUCT: - case UA_DATATYPEKIND_UNION: - for(UA_UInt32 i = 0; i < type->membersSize; ++i) { - const UA_DataTypeMember *m = &type->members[i]; - const UA_DataType *mt = memberTypeOf(type, m); - offset += m->padding; - - if(strcmp(memberName, m->memberName) == 0) { - *outOffset = offset; - *outMemberType = mt; - *outIsArray = m->isArray; - *outIsOptional = m->isOptional; - return i+1; - } - - if (type->typeKind == UA_DATATYPEKIND_UNION) { - offset = 0; - } else if(!m->isOptional) { - if(!m->isArray) { - offset += mt->memSize; - } else { - offset += sizeof(size_t); - offset += sizeof(void*); - } - } else { /* field is optional */ - if(!m->isArray) { - offset += sizeof(void *); - } else { - offset += sizeof(size_t); - offset += sizeof(void *); - } - } - } - break; - case UA_DATATYPEKIND_LOCALIZEDTEXT: - *outMemberType = &UA_TYPES[UA_TYPES_STRING]; - *outIsArray = 0; - *outIsOptional = 0; - if (strcmp(memberName, "locale") == 0) { - *outOffset = offsetof(UA_LocalizedText, locale); - return 1; - } - if (strcmp(memberName, "text") == 0) { - *outOffset = offsetof(UA_LocalizedText, text); - return 2; - } - break; - case UA_DATATYPEKIND_QUALIFIEDNAME: - *outIsArray = 0; - *outIsOptional = 0; - if (strcmp(memberName, "namespaceIndex") == 0) { - *outMemberType = &UA_TYPES[UA_DATATYPEKIND_UINT16]; - *outOffset = offsetof(UA_QualifiedName, namespaceIndex); - return 1; - } - if (strcmp(memberName, "name") == 0) { - *outMemberType = &UA_TYPES[UA_TYPES_STRING]; - *outOffset = offsetof(UA_QualifiedName, name); - return 2; - } - break; - } - return 0; -} - -void -DataElementOpen62541::createMap(const UA_DataType *type, - const std::string *timefrom) -{ - if (debug() >= 5) - std::cout << " ** creating index-to-element map for child elements" << std::endl; - - switch (typeKindOf(type)) { - case UA_DATATYPEKIND_STRUCTURE: - case UA_DATATYPEKIND_OPTSTRUCT: - case UA_DATATYPEKIND_UNION: - case UA_DATATYPEKIND_LOCALIZEDTEXT: - case UA_DATATYPEKIND_QUALIFIEDNAME: - if (timefrom) { - const UA_DataType *timeMemberType; - UA_Boolean timeIsArray; - UA_Boolean timeIsOptional; - size_t timeOffset; - - if (UA_DataType_getStructMemberExt(type, timefrom->c_str(), - &timeOffset, - &timeMemberType, - &timeIsArray, - &timeIsOptional)) { - if (typeKindOf(timeMemberType) != UA_TYPES_DATETIME || timeIsArray) { - errlogPrintf("%s: timestamp element %s has invalid type %s%s - using " - "source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str(), - typeKindName(typeKindOf(timeMemberType)), - timeIsArray ? "[]" : ""); - } else - timesrc = timeOffset; - } else { - errlogPrintf( - "%s: timestamp element %s not found - using source timestamp\n", - pitem->recConnector->getRecordName(), - timefrom->c_str()); - } - } - - for (auto &it : elements) { - auto pelem = it.lock(); - if ((pelem->index = UA_DataType_getStructMemberExt(type, pelem->name.c_str(), - &pelem->offset, - &pelem->memberType, - &pelem->isArray, - &pelem->isOptional))) { - if (debug() >= 5) - std::cout << typeKindName(typeKindOf(type)) - << " " << pelem - << " index=" << pelem->index - << " offset=" << pelem->offset - << " type=" << variantTypeString(pelem->memberType) - << (pelem->isArray ? "[]" : "") - << (pelem->isOptional ? " optional" : "") - << std::endl; - if (typeKindOf(pelem->memberType) == UA_DATATYPEKIND_ENUM) { - pelem->enumChoices = pitem->session->getEnumChoices(&pelem->memberType->typeId); - } - } else { - std::cerr << "Item " << pitem - << ": element " << pelem->name - << " not found in " << variantTypeString(type) - << std::endl; - } - } - if (debug() >= 5) - std::cout << " ** " << elements.size() - << " child elements mapped to " - << variantTypeString(type) - << " of " << type->membersSize << " elements" << std::endl; - break; - default: - std::cerr << "Error: " << this - << " is no structured data but a " << typeKindName(typeKindOf(type)) - << std::endl; - } - mapped = true; -} - // Getting the timestamp and status information from the Item assumes that only one thread // is pushing data into the Item's DataElement structure at any time. void -DataElementOpen62541::setIncomingData (const UA_Variant &value, - ProcessReason reason, - const std::string *timefrom) +DataElementOpen62541Leaf::setIncomingData (const UA_Variant &value, ProcessReason reason, const std::string *timefrom) { // Cache this element. We can make a shallow copy because // ItemOpen62541::setIncomingData marks the original response data as "ours". @@ -288,254 +106,64 @@ DataElementOpen62541::setIncomingData (const UA_Variant &value, UA_Variant_clear(&incomingData); incomingData = value; - if (isLeaf()) { - if (pconnector->state() == ConnectionStatus::initialRead && typeKindOf(value) == UA_DATATYPEKIND_ENUM) { - enumChoices = pitem->session->getEnumChoices(&value.type->typeId); - } - if ((pconnector->state() == ConnectionStatus::initialRead - && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) - || (pconnector->state() == ConnectionStatus::up)) { - - Guard(pconnector->lock); - bool wasFirst = false; - // Make a copy of the value for this element and put it on the queue - UA_Variant *valuecopy (new UA_Variant); - UA_Variant_copy(&value, valuecopy); // As a non-C++ object, UA_Variant has no copy constructor - UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason, std::unique_ptr(valuecopy), getIncomingReadStatus())); - incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); - if (debug() >= 5) - std::cout << "Item " << pitem - << " element " << name - << " set data (" << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" - << std::endl; - if (wasFirst) - pconnector->requestRecordProcessing(reason); - } - } else { - if (UA_Variant_isEmpty(&value)) - return; - - if (debug() >= 5) - std::cout << "Item " << pitem - << " element " << name - << " splitting structured data to " - << elements.size() << " child elements" - << std::endl; - - const UA_DataType *type = value.type; - char* container = static_cast(value.data); - if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { - UA_ExtensionObject &extensionObject = *reinterpret_cast(container); - if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { - // Access content of decoded extension objects - type = extensionObject.content.decoded.type; - container = static_cast(extensionObject.content.decoded.data); - } else { - std::cerr << "Cannot get a structure definition for item " << pitem - << " because binaryEncodingId " << extensionObject.content.encoded.typeId - << " is not in the type dictionary." << std::endl; - return; - } - } - - if (!mapped) - createMap(type, timefrom); - - if (timesrc >= 0) - pitem->tsData = ItemOpen62541::uaToEpicsTime(*reinterpret_cast(container + timesrc), 0); - else - pitem->tsData = pitem->tsSource; - - for (auto &it : elements) { - auto pelem = it.lock(); - const UA_DataType* memberType = pelem->memberType; - char* memberData = container + pelem->offset; - UA_Variant memberValue; - size_t arrayLength = 0; // default to scalar - if (pelem->isArray) { - arrayLength = *reinterpret_cast(memberData); - memberData = *reinterpret_cast(memberData + sizeof(size_t)); - } else if (pelem->isOptional) { - /* optional scalar stored through pointer like an array */ - memberData = *reinterpret_cast(memberData); - } - if (type->typeKind == UA_DATATYPEKIND_UNION && - pelem->index != *reinterpret_cast(container)) { - // union option not taken - memberData = nullptr; - } - UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); - memberValue.storageType = UA_VARIANT_DATA_NODELETE; // Keep ownership of data - if (debug() && !memberData) { - std::cerr << pitem->recConnector->getRecordName() - << " " << pelem - << (type->typeKind == UA_DATATYPEKIND_UNION ? " not taken choice " : " absent optional ") - << variantTypeString(memberType) - << (pelem->isArray ? " array" : " scalar" ) - << std::endl; - } - pelem->setIncomingData(memberValue, memberData ? reason : ProcessReason::readFailure); - } + if (pconnector->state() == ConnectionStatus::initialRead && typeKindOf(value) == UA_DATATYPEKIND_ENUM) { + enumChoices = pitem->session->getEnumChoices(&value.type->typeId); } -} - -void -DataElementOpen62541::setIncomingEvent (ProcessReason reason) -{ - if (isLeaf()) { + if ((pconnector->state() == ConnectionStatus::initialRead + && (reason == ProcessReason::readComplete || reason == ProcessReason::readFailure)) + || (pconnector->state() == ConnectionStatus::up)) { Guard(pconnector->lock); - if (reason == ProcessReason::connectionLoss && enumChoices) - enumChoices = nullptr; bool wasFirst = false; - // Put the event on the queue - UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason)); + // Make a copy of the value for this element and put it on the queue + UA_Variant *valuecopy(new UA_Variant); + UA_Variant_copy(&value, valuecopy); // As a non-C++ object, UA_Variant has no copy constructor + UpdateOpen62541 *u(new UpdateOpen62541( + getIncomingTimeStamp(), reason, std::unique_ptr(valuecopy), getIncomingReadStatus())); incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); if (debug() >= 5) - std::cout << "Element " << name << " set event (" - << processReasonString(reason) - << ") for record " << pconnector->getRecordName() - << " (queue use " << incomingQueue.size() - << "/" << incomingQueue.capacity() << ")" - << std::endl; + std::cout << "Item " << pitem << " element " << name << " set data (" << processReasonString(reason) + << ") for record " << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; if (wasFirst) pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setIncomingEvent(reason); - } } } void -DataElementOpen62541::setState(const ConnectionStatus state) +DataElementOpen62541Leaf::setIncomingEvent (ProcessReason reason) { - if (isLeaf()) { - Guard(pconnector->lock); - pconnector->setState(state); - } else { - for (auto &it : elements) { - auto pelem = it.lock(); - pelem->setState(state); - } - } + Guard(pconnector->lock); + if (reason == ProcessReason::connectionLoss && enumChoices) + enumChoices = nullptr; + bool wasFirst = false; + // Put the event on the queue + UpdateOpen62541 *u(new UpdateOpen62541(getIncomingTimeStamp(), reason)); + incomingQueue.pushUpdate(std::shared_ptr(u), &wasFirst); + if (debug() >= 5) + std::cout << "Element " << name << " set event (" << processReasonString(reason) << ") for record " + << pconnector->getRecordName() << " (queue use " << incomingQueue.size() << "/" + << incomingQueue.capacity() << ")" << std::endl; + if (wasFirst) + pconnector->requestRecordProcessing(reason); } -// Helper to update one data structure element from pointer to child -bool -DataElementOpen62541::updateDataInStruct(void* container, - std::shared_ptr pelem) +void +DataElementOpen62541Leaf::setState (const ConnectionStatus state) { - bool updated = false; - { // Scope of Guard G - Guard G(pelem->outgoingLock); - if (pelem->isDirty()) { - char* memberData = static_cast(container) + pelem->offset; - const UA_Variant& elementData = pelem->getOutgoingData(); - const UA_DataType* memberType = pelem->memberType; - assert(memberType == elementData.type || - (typeKindOf(memberType) == UA_DATATYPEKIND_ENUM && - typeKindOf(elementData.type) == UA_DATATYPEKIND_INT32)); - if (!pelem->isArray && !pelem->isOptional) { - // mandatory scalar: shallow copy - UA_clear(memberData, memberType); - void* data = pelem->moveOutgoingData(); - if (typeKindOf(outgoingData) == UA_DATATYPEKIND_UNION) { - *reinterpret_cast(container) = pelem->index; - } - memcpy(memberData, data, memberType->memSize); - UA_free(data); - } else { - // array or optional scalar: move content - void **memberDataPtr; - if (pelem->isArray) /* mandatory or optional array stored as length and pointer */ { - size_t& arrayLength = *reinterpret_cast(memberData); - memberDataPtr = reinterpret_cast(memberData + sizeof(size_t)); - UA_Array_delete(*memberDataPtr, arrayLength, memberType); - arrayLength = elementData.arrayLength; - } else /* optional scalar stored through pointer */ { - memberDataPtr = reinterpret_cast(memberData); - if (*memberDataPtr) /* absent optional has nullptr here */ - UA_Array_delete(*memberDataPtr, 1, memberType); - } - *memberDataPtr = pelem->moveOutgoingData(); - } - pelem->isdirty = false; - updated = true; - } - } - if (debug() >= 4) { - if (updated) { - std::cout << "Data from child element " << pelem->name - << " inserted into data structure" << std::endl; - } else { - std::cout << "Data from child element " << pelem->name - << " ignored (not dirty)" << std::endl; - } - } - return updated; + Guard(pconnector->lock); + pconnector->setState(state); } - const UA_Variant & -DataElementOpen62541::getOutgoingData () +DataElementOpen62541Leaf::getOutgoingData () { - if (!isLeaf()) { - if (debug() >= 4) - std::cout << "Item " << pitem - << " element " << name - << " updating structured data from " - << elements.size() << " child elements" - << std::endl; - - UA_Variant_clear(&outgoingData); - UA_Variant_copy(&incomingData, &outgoingData); - isdirty = false; - const UA_DataType *type = outgoingData.type; - void* container = outgoingData.data; - - if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { - UA_ExtensionObject &extensionObject = *reinterpret_cast(container); - if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { - // Access content decoded extension objects - type = extensionObject.content.decoded.type; - container = extensionObject.content.decoded.data; - } else { - std::cerr << "Cannot get a structure definition for item " << pitem - << " because binaryEncodingId " << extensionObject.content.encoded.typeId - << " is not in the type dictionary." << std::endl; - return outgoingData; - } - } - - if (!mapped) - createMap(type); - - for (auto &it : elements) { - auto pelem = it.lock(); - if (updateDataInStruct(container, pelem)) - isdirty = true; - } - if (isdirty) { - if (debug() >= 4) - std::cout << "Encoding changed data structure to outgoingData of element " << name - << std::endl; - } else { - if (debug() >= 4) - std::cout << "Returning unchanged outgoingData of element " << name - << std::endl; - } - } return outgoingData; } void -DataElementOpen62541::dbgReadScalar (const UpdateOpen62541 *upd, - const std::string &targetTypeName, - const size_t targetSize) const +DataElementOpen62541Leaf::dbgReadScalar (const UpdateOpen62541 *upd, + const std::string &targetTypeName, + const size_t targetSize) const { if (isLeaf() && debug()) { char time_buf[40]; @@ -563,7 +191,7 @@ DataElementOpen62541::dbgReadScalar (const UpdateOpen62541 *upd, } long -DataElementOpen62541::readScalar (epicsInt32 *value, +DataElementOpen62541Leaf::readScalar (epicsInt32 *value, dbCommon *prec, ProcessReason *nextReason, epicsUInt32 *statusCode, @@ -574,7 +202,7 @@ DataElementOpen62541::readScalar (epicsInt32 *value, } long -DataElementOpen62541::readScalar (epicsInt64 *value, +DataElementOpen62541Leaf::readScalar (epicsInt64 *value, dbCommon *prec, ProcessReason *nextReason, epicsUInt32 *statusCode, @@ -585,7 +213,7 @@ DataElementOpen62541::readScalar (epicsInt64 *value, } long -DataElementOpen62541::readScalar (epicsUInt32 *value, +DataElementOpen62541Leaf::readScalar (epicsUInt32 *value, dbCommon *prec, ProcessReason *nextReason, epicsUInt32 *statusCode, @@ -596,7 +224,7 @@ DataElementOpen62541::readScalar (epicsUInt32 *value, } long -DataElementOpen62541::readScalar (epicsFloat64 *value, +DataElementOpen62541Leaf::readScalar (epicsFloat64 *value, dbCommon *prec, ProcessReason *nextReason, epicsUInt32 *statusCode, @@ -662,7 +290,7 @@ static int parseByteString(UA_ByteString& byteString, const char* encoded, int l // CString type needs specialization long -DataElementOpen62541::readScalar (char *value, const epicsUInt32 len, +DataElementOpen62541Leaf::readScalar (char *value, const epicsUInt32 len, dbCommon *prec, ProcessReason *nextReason, epicsUInt32 *lenRead, @@ -819,7 +447,7 @@ DataElementOpen62541::readScalar (char *value, const epicsUInt32 len, } void -DataElementOpen62541::dbgReadArray (const UpdateOpen62541 *upd, +DataElementOpen62541Leaf::dbgReadArray (const UpdateOpen62541 *upd, const epicsUInt32 targetSize, const std::string &targetTypeName) const { @@ -849,7 +477,7 @@ DataElementOpen62541::dbgReadArray (const UpdateOpen62541 *upd, // Read array for EPICS String / UA_String long int -DataElementOpen62541::readArray (char *value, epicsUInt32 len, +DataElementOpen62541Leaf::readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, const UA_DataType *expectedType, @@ -971,7 +599,7 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, // CAVEAT: changes in the template (in DataElementOpen62541.h) must be reflected here template<> long -DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsUInt8 *value, const epicsUInt32 num, epicsUInt32 *numRead, const UA_DataType *expectedType, dbCommon *prec, @@ -1061,7 +689,7 @@ DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt3 } long -DataElementOpen62541::readArray (epicsInt8 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsInt8 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1073,7 +701,7 @@ DataElementOpen62541::readArray (epicsInt8 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsUInt8 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1085,7 +713,7 @@ DataElementOpen62541::readArray (epicsUInt8 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsInt16 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsInt16 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1097,7 +725,7 @@ DataElementOpen62541::readArray (epicsInt16 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsUInt16 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsUInt16 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1109,7 +737,7 @@ DataElementOpen62541::readArray (epicsUInt16 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsInt32 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsInt32 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1121,7 +749,7 @@ DataElementOpen62541::readArray (epicsInt32 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsUInt32 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsUInt32 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1133,7 +761,7 @@ DataElementOpen62541::readArray (epicsUInt32 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsInt64 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsInt64 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1145,7 +773,7 @@ DataElementOpen62541::readArray (epicsInt64 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsUInt64 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsUInt64 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1157,7 +785,7 @@ DataElementOpen62541::readArray (epicsUInt64 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsFloat32 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsFloat32 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1169,7 +797,7 @@ DataElementOpen62541::readArray (epicsFloat32 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (epicsFloat64 *value, const epicsUInt32 num, +DataElementOpen62541Leaf::readArray (epicsFloat64 *value, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, ProcessReason *nextReason, @@ -1181,7 +809,7 @@ DataElementOpen62541::readArray (epicsFloat64 *value, const epicsUInt32 num, } long -DataElementOpen62541::readArray (char *value, epicsUInt32 len, +DataElementOpen62541Leaf::readArray (char *value, epicsUInt32 len, const epicsUInt32 num, epicsUInt32 *numRead, dbCommon *prec, @@ -1195,7 +823,7 @@ DataElementOpen62541::readArray (char *value, epicsUInt32 len, inline void -DataElementOpen62541::dbgWriteScalar () const +DataElementOpen62541Leaf::dbgWriteScalar () const { if (isLeaf() && debug()) { std::cout << pconnector->getRecordName() @@ -1205,31 +833,31 @@ DataElementOpen62541::dbgWriteScalar () const } long -DataElementOpen62541::writeScalar (const epicsInt32 &value, dbCommon *prec) +DataElementOpen62541Leaf::writeScalar (const epicsInt32 &value, dbCommon *prec) { return writeScalar(value, prec); } long -DataElementOpen62541::writeScalar (const epicsUInt32 &value, dbCommon *prec) +DataElementOpen62541Leaf::writeScalar (const epicsUInt32 &value, dbCommon *prec) { return writeScalar(value, prec); } long -DataElementOpen62541::writeScalar (const epicsInt64 &value, dbCommon *prec) +DataElementOpen62541Leaf::writeScalar (const epicsInt64 &value, dbCommon *prec) { return writeScalar(value, prec); } long -DataElementOpen62541::writeScalar (const epicsFloat64 &value, dbCommon *prec) +DataElementOpen62541Leaf::writeScalar (const epicsFloat64 &value, dbCommon *prec) { return writeScalar(value, prec); } long -DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon *prec) +DataElementOpen62541Leaf::writeScalar (const char *value, epicsUInt32 len, dbCommon *prec) { long ret = 1; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; @@ -1378,7 +1006,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon if (enumChoices) { // first test enum strings then numeric values // in case a string starts with a number but means a different value - for (auto it: *enumChoices) + for (const auto &it: *enumChoices) if (it.second == value) { l = static_cast(it.first); ret = 0; @@ -1386,7 +1014,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon break; } if (ret != 0 && end != value) - for (auto it: *enumChoices) + for (const auto &it: *enumChoices) if (l == it.first) { ret = 0; break; @@ -1477,7 +1105,7 @@ DataElementOpen62541::writeScalar (const char *value, epicsUInt32 len, dbCommon inline void -DataElementOpen62541::dbgWriteArray (const epicsUInt32 targetSize, const std::string &targetTypeName) const +DataElementOpen62541Leaf::dbgWriteArray (const epicsUInt32 targetSize, const std::string &targetTypeName) const { if (isLeaf() && debug()) { std::cout << pconnector->getRecordName() << ": writing array of " @@ -1499,7 +1127,7 @@ UA_String UA_StringNCopy(const char *src, size_t maxlength) // Write array for EPICS String / UA_String long -DataElementOpen62541::writeArray (const char *value, epicsUInt32 len, +DataElementOpen62541Leaf::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, const UA_DataType *targetType, dbCommon *prec) @@ -1593,7 +1221,7 @@ DataElementOpen62541::writeArray (const char *value, epicsUInt32 len, // CAVEAT: changes in the template (in DataElementOpen62541.h) must be reflected here template<> long -DataElementOpen62541::writeArray(const epicsUInt8 *value, +DataElementOpen62541Leaf::writeArray(const epicsUInt8 *value, const epicsUInt32 num, const UA_DataType *targetType, dbCommon *prec) @@ -1643,82 +1271,75 @@ DataElementOpen62541::writeArray(const epicsUInt8 *value, } long -DataElementOpen62541::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsInt8 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_SBYTE], prec); } long -DataElementOpen62541::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsUInt8 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_BYTE], prec); } long -DataElementOpen62541::writeArray (const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsInt16 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_INT16], prec); } long -DataElementOpen62541::writeArray (const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsUInt16 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT16], prec); } long -DataElementOpen62541::writeArray (const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsInt32 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_INT32], prec); } long -DataElementOpen62541::writeArray (const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsUInt32 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT32], prec); } long -DataElementOpen62541::writeArray (const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsInt64 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_INT64], prec); } long -DataElementOpen62541::writeArray (const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsUInt64 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_UINT64], prec); } long -DataElementOpen62541::writeArray (const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsFloat32 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_FLOAT], prec); } long -DataElementOpen62541::writeArray (const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const epicsFloat64 *value, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, num, &UA_TYPES[UA_TYPES_DOUBLE], prec); } long -DataElementOpen62541::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) +DataElementOpen62541Leaf::writeArray (const char *value, epicsUInt32 len, const epicsUInt32 num, dbCommon *prec) { return writeArray(value, len, num, &UA_TYPES[UA_TYPES_STRING], prec); } void -DataElementOpen62541::requestRecordProcessing (const ProcessReason reason) const +DataElementOpen62541Leaf::requestRecordProcessing (const ProcessReason reason) const { - if (isLeaf()) { - pconnector->requestRecordProcessing(reason); - } else { - for (auto &it : elementMap) { - auto pelem = it.second.lock(); - pelem->requestRecordProcessing(reason); - } - } + pconnector->requestRecordProcessing(reason); } } // namespace DevOpcua diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.h b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h new file mode 100644 index 00000000..ae74d32f --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h @@ -0,0 +1,1176 @@ +/*************************************************************************\ +* Copyright (c) 2018-2023 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#ifndef DEVOPCUA_DATAELEMENTOPEN62541LEAF_H +#define DEVOPCUA_DATAELEMENTOPEN62541LEAF_H + +#include "DataElementOpen62541.h" +#include "RecordConnector.h" +#include "Update.h" +#include "UpdateQueue.h" + +#include +#include +#include +#include + +#include +#ifndef UA_STATUSCODE_BAD // Not yet defined in open62541 version 1.2 +#define UA_STATUSCODE_BAD 0x80000000 +#endif +#define UA_STATUS_IS_BAD(status) (((status)&UA_STATUSCODE_BAD)!=0) + +#ifndef UA_STATUSCODE_UNCERTAIN +#define UA_STATUSCODE_UNCERTAIN 0x40000000 +#endif +#define UA_STATUS_IS_UNCERTAIN(status) (((status)&UA_STATUSCODE_UNCERTAIN)!=0) + +#include +#include +#include + +namespace DevOpcua { + +typedef Update UpdateOpen62541; + +inline const char *epicsTypeString (const epicsInt8 &) { return "epicsInt8"; } +inline const char *epicsTypeString (const epicsUInt8 &) { return "epicsUInt8"; } +inline const char *epicsTypeString (const epicsInt16 &) { return "epicsInt16"; } +inline const char *epicsTypeString (const epicsUInt16 &) { return "epicsUInt16"; } +inline const char *epicsTypeString (const epicsInt32 &) { return "epicsInt32"; } +inline const char *epicsTypeString (const epicsUInt32 &) { return "epicsUInt32"; } +inline const char *epicsTypeString (const epicsInt64 &) { return "epicsInt64"; } +inline const char *epicsTypeString (const epicsUInt64 &) { return "epicsUInt64"; } +inline const char *epicsTypeString (const epicsFloat32 &) { return "epicsFloat32"; } +inline const char *epicsTypeString (const epicsFloat64 &) { return "epicsFloat64"; } +inline const char *epicsTypeString (const char*) { return "epicsString"; } + +// Template for range check when writing +template +inline bool isWithinRange (const FROM &value) { + return !(value < std::numeric_limits::lowest() || value > std::numeric_limits::max()); +} + +// Specializations for unsigned to signed to avoid compiler warnings +template<> +inline bool isWithinRange (const unsigned int &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned int &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned int &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long &value) { + return !(value > std::numeric_limits::max()); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +template<> +inline bool isWithinRange (const unsigned long long &value) { + return !(value > static_cast(std::numeric_limits::max())); +} + +// Simple-check specializations for converting signed to unsigned of same or wider type +template<> inline bool isWithinRange (const char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const short &value) { return !(value < 0); } +template<> inline bool isWithinRange (const int &value) { return !(value < 0); } +template<> inline bool isWithinRange (const char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const short &value) { return !(value < 0); } +template<> inline bool isWithinRange (const int &value) { return !(value < 0); } +template<> inline bool isWithinRange (const long &value) { return !(value < 0); } +template<> inline bool isWithinRange (const char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const signed char &value) { return !(value < 0); } +template<> inline bool isWithinRange (const short &value) { return !(value < 0); } +template<> inline bool isWithinRange (const int &value) { return !(value < 0); } +template<> inline bool isWithinRange (const long &value) { return !(value < 0); } +template<> inline bool isWithinRange (const long long &value) { return !(value < 0); } + +// No-check-needed specializations for converting to same or wider type +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } +template<> inline bool isWithinRange (const int &) { return true; } + +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } +template<> inline bool isWithinRange (const unsigned int &) { return true; } + +template<> inline bool isWithinRange (const long &) { return true; } +template<> inline bool isWithinRange (const long &) { return true; } + +template<> inline bool isWithinRange (const long long &) { return true; } +template<> inline bool isWithinRange (const long long &) { return true; } +template<> inline bool isWithinRange (const long long &) { return true; } + +template<> inline bool isWithinRange (const double &) { return true; } + +// long long may or may not be the same size as long +template<> +inline bool isWithinRange (const long long &value) { + return sizeof(long long) > sizeof(long) ? !(value < 0 || value > static_cast(std::numeric_limits::max())) : !(value < 0); +} + +// Helper function to convert strings to numeric types + +inline bool string_to(const std::string& s, epicsInt32& value) { + try { + value = std::stol(s, 0, 0); + return true; + } catch (...) { + return false; + } +} + +inline bool string_to(const std::string& s, epicsInt64& value) { + try { + value = std::stoll(s, 0, 0); + return true; + } catch (...) { + return false; + } +} + +inline bool string_to(const std::string& s, epicsUInt32& value) { + try { + long long v = std::stoll(s, 0, 0); + if (!isWithinRange(v)) return false; + value = static_cast(v); + return true; + } catch (...) { + return false; + } +} + +inline bool string_to(const std::string& s, epicsFloat64& value) { + try { + value = std::stod(s, 0); + return true; + } catch (...) { + return false; + } +} + +/** + * @brief The DataElementOpen62541 implementation of a single piece of data. + * + * See DevOpcua::DataElement + */ +class DataElementOpen62541Leaf + : public DataElement + , public DataElementOpen62541 +{ +public: + /** + * @brief Constructor for DataElement (leaf) from record connector. + * + * Creates the final (leaf) element of the data structure. The record connector + * holds a shared pointer to its leaf, while the data element has a weak pointer + * to the record connector. + * + * @param name name of the element (empty for root element) + * @param pitem pointer to corresponding ItemOpen62541 + * @param pconnector pointer to record connector to link to + */ + DataElementOpen62541Leaf(const std::string &name, ItemOpen62541 *pitem, RecordConnector *pconnector); + virtual ~DataElementOpen62541Leaf() override { delete enumChoices; } + + /* ElementTree node interface methods */ + virtual bool isLeaf () const override { return true; } + virtual void addChild (std::weak_ptr elem) override {} + virtual std::shared_ptr findChild (const std::string &name) const override + { + return std::shared_ptr(); + } + + /** + * @brief Create a DataElement and add it to the item's dataTree. + * + * @param item item to add the element to + * @param pconnector pointer to the leaf's record connector + * @param elementPath full path to the element + */ + static void + addElementToTree(ItemOpen62541 *item, RecordConnector *pconnector, const std::list &elementPath); + + virtual void show(const int level, const unsigned int indent) const override; + + + virtual void + setIncomingData(const UA_Variant &value, ProcessReason reason, const std::string *timefrom = nullptr) override; + + virtual void setIncomingEvent(ProcessReason reason) override; + + /** + * @brief Set the EPICS-related state of the element and all sub-elements. + */ + void setState(const ConnectionStatus state) override; + + /** + * @brief Get the outgoing data value from the DataElement. + * + * Called from the OPC UA client worker thread when data is being + * assembled in OPC UA session for sending. + * + * @return reference to outgoing data + */ + const UA_Variant &getOutgoingData() override; + + /** + * @brief Read incoming data as a scalar epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as a scalar epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsInt64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsInt64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as a scalar epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsUInt32 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as a scalar epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(epicsFloat64*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readScalar(epicsFloat64 *value, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as classic C string (char[]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readScalar(char*,const epicsUInt32,dbCommon*,ProcessReason*,epicsUInt32*,epicsUInt32*,char*,const epicsUInt32) + */ + + virtual long int readScalar(char *value, const epicsUInt32 len, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32* lenRead = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt8. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt8*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt8 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt16 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt16. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt16*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt16 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsInt64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsUInt64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsUInt64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsUInt64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsFloat32. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat32*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat32 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of epicsFloat64. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(epicsFloat64*,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(epicsFloat64 *value, const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Read incoming data as array of EPICS String (char[40]). + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::readArray(char*,const epicsUInt32,const epicsUInt32,epicsUInt32*,dbCommon*,ProcessReason*,epicsUInt32*,char*,const epicsUInt32) + */ + virtual long int readArray(char *value, epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + dbCommon *prec, + ProcessReason *nextReason = nullptr, + epicsUInt32 *statusCode = nullptr, + char *statusText = nullptr, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) override; + + /** + * @brief Write outgoing scalar epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt32 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsInt64&,dbCommon*) + */ + virtual long int writeScalar(const epicsInt64 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsUInt32&,dbCommon*) + */ + virtual long int writeScalar(const epicsUInt32 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing scalar epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const epicsFloat64&,dbCommon*) + */ + virtual long int writeScalar(const epicsFloat64 &value, + dbCommon *prec) override; + + /** + * @brief Write outgoing classic C string (char[]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeScalar(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeScalar(const char *value, + const epicsUInt32 len, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt8 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt8 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt8*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt8 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt16 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt16 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt16*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt16 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt32 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt32 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsInt64 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsUInt64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsUInt64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsUInt64 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat32 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat32*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat32 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of epicsFloat64 data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const epicsFloat64*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const epicsFloat64 *value, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Write outgoing array of EPICS String (char[MAX_STRING_SIZE]) data. + * + * See the DataElement API method it overrides + * DevOpcua::DataElement::writeArray(const char*,const epicsUInt32,dbCommon*) + */ + virtual long int writeArray(const char *value, const epicsUInt32 len, + const epicsUInt32 num, + dbCommon *prec) override; + + /** + * @brief Clear (discard) the current outgoing data. + * + * Called by the low level connection (OPC UA session) + * after it is done accessing the data in the context of sending. + * + * In case an implementation uses a queue, this should remove the + * oldest element from the queue, allowing access to the next element + * with the next send. + */ + virtual void clearOutgoingData() { UA_Variant_clear(&outgoingData); } + + /** + * @brief Create processing requests for record(s) attached to this element. + * See DevOpcua::DataElement::requestRecordProcessing + */ + virtual void requestRecordProcessing(const ProcessReason reason) const override; + + /** + * @brief Get debug level from record (via RecordConnector). + * @return debug level + */ + int debug() const { return (isLeaf() ? pconnector->debug() : pitem->debug()); } + +private: + virtual bool isDirty () const override { return isdirty; } + virtual void markAsDirty () override + { + isdirty = true; + pitem->markAsDirty(); + } + + void dbgWriteScalar () const; + void dbgReadScalar(const UpdateOpen62541 *upd, + const std::string &targetTypeName, + const size_t targetSize = 0) const; + void dbgReadArray(const UpdateOpen62541 *upd, + const epicsUInt32 targetSize, + const std::string &targetTypeName) const; + void checkWriteArray(const UA_DataType *expectedType, const std::string &targetTypeName) const; + void dbgWriteArray(const epicsUInt32 targetSize, const std::string &targetTypeName) const; + bool updateDataInStruct(void* container, std::shared_ptr pelem); + void createMap(const UA_DataType *type, const std::string* timefrom = nullptr); + + // Get the time stamp from the incoming object + const epicsTime &getIncomingTimeStamp() const { + ProcessReason reason = pitem->getReason(); + if ((reason == ProcessReason::incomingData || reason == ProcessReason::readComplete) + && isLeaf()) + switch (pconnector->plinkinfo->timestamp) { + case LinkOptionTimestamp::server: + return pitem->tsServer; + case LinkOptionTimestamp::source: + return pitem->tsSource; + case LinkOptionTimestamp::data: + return pitem->tsData; + } + return pitem->tsClient; + } + + // Get the read status from the incoming object + UA_StatusCode getIncomingReadStatus() const { return pitem->getLastStatus(); } + + // Read scalar value as templated function on EPICS type + // value == nullptr is allowed and leads to the value being dropped (ignored), + // including the extended status + template + long + readScalar (ET *value, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen) + { + long ret = 1; + + if (incomingQueue.empty()) { + errlogPrintf("%s: incoming data queue empty\n", prec->name); + if (nextReason) + *nextReason = ProcessReason::none; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadScalar(upd.get(), epicsTypeString(*value)); + prec->udf = false; + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } else { + // Valid OPC UA value, so try to convert + UA_Variant &data = upd->getData(); + switch(typeKindOf(data)) { + case UA_DATATYPEKIND_BOOLEAN: + *value = (*static_cast(data.data) != 0); + ret = 0; + break; + case UA_DATATYPEKIND_BYTE: + if (isWithinRange(*static_cast(data.data))) { + *value = *static_cast(data.data); + ret = 0; + } + break; + case UA_DATATYPEKIND_SBYTE: + if (isWithinRange(*static_cast(data.data))) { + *value = *static_cast(data.data); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT16: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT16: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT32: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT32: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT64: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT64: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_FLOAT: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_DOUBLE: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + if (isWithinRange(*static_cast(data.data))) { + *value = static_cast(*static_cast(data.data)); + if (!enumChoices || enumChoices->find(static_cast(*value)) != enumChoices->end()) + ret = 0; + } + break; + case UA_DATATYPEKIND_STRING: + { + UA_String* s = static_cast(data.data); + if (string_to(std::string(reinterpret_cast(s->data), s->length), *value)) + ret = 0; + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + UA_LocalizedText* lt = static_cast(data.data); + if (string_to(std::string(reinterpret_cast(lt->text.data), lt->text.length), *value)) + ret = 0; + break; + } + default: + errlogPrintf("%s : unsupported type kind %s for incoming data\n", + prec->name, typeKindName(typeKindOf(data))); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } + if (ret == 1) { + UA_String datastring = UA_STRING_NULL; + if (data.type) + UA_print(data.data, data.type, &datastring); + errlogPrintf("%s : incoming data (%s %.*s) out-of-bounds for %s\n", + prec->name, + variantTypeString(data), + static_cast(datastring.length), datastring.data, + epicsTypeString(*value)); + UA_String_clear(&datastring); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + } + UA_Variant_clear(&data); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + return ret; + } + + // Read array value as templated function on EPICS type + // CAVEAT: changes must also be reflected in specializations (in DataElementOpen62541.cpp) + template + long + readArray (ET *value, const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen = MAX_STRING_SIZE+1) + { + long ret = 0; + epicsUInt32 elemsWritten = 0; + + if (incomingQueue.empty()) { + errlogPrintf("%s : incoming data queue empty\n", prec->name); + *numRead = 0; + return 1; + } + + ProcessReason nReason; + std::shared_ptr upd = incomingQueue.popUpdate(&nReason); + dbgReadArray(upd.get(), num, epicsTypeString(*value)); + + switch (upd->getType()) { + case ProcessReason::readFailure: + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::connectionLoss: + (void) recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + ret = 1; + break; + case ProcessReason::incomingData: + case ProcessReason::readComplete: + { + if (num && value) { + UA_StatusCode stat = upd->getStatus(); + if (UA_STATUS_IS_BAD(stat)) { + // No valid OPC UA value + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + // Valid OPC UA value, so try to convert + UA_Variant &variant = upd->getData(); + if (UA_Variant_isScalar(&variant)) { + errlogPrintf("%s : incoming data is not an array\n", prec->name); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else if (variant.type != expectedType) { + errlogPrintf("%s : incoming data type (%s) does not match EPICS array type (%s)\n", + prec->name, variantTypeString(variant), epicsTypeString(*value)); + (void) recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + ret = 1; + } else { + if (UA_STATUS_IS_UNCERTAIN(stat)) { + (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); + } + elemsWritten = static_cast(num) < variant.arrayLength ? num : static_cast(variant.arrayLength); + memcpy(value, variant.data, sizeof(ET) * elemsWritten); + } + UA_Variant_clear(&variant); + } + if (statusCode) *statusCode = stat; + if (statusText) { + strncpy(statusText, UA_StatusCode_name(stat), statusTextLen); + statusText[statusTextLen-1] = '\0'; + } + } + break; + } + default: + break; + } + + prec->time = upd->getTimeStamp(); + if (nextReason) *nextReason = nReason; + if (num && value) + *numRead = elemsWritten; + return ret; + } + + // Read array value for EPICS String / UA_String + long + readArray (char *value, const epicsUInt32 len, + const epicsUInt32 num, + epicsUInt32 *numRead, + const UA_DataType *expectedType, + dbCommon *prec, + ProcessReason *nextReason, + epicsUInt32 *statusCode, + char *statusText, + const epicsUInt32 statusTextLen); + + // Write scalar value as templated function on EPICS type + template + long + writeScalar (const ET &value, + dbCommon *prec) + { + long ret = 1; + UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; + + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + switch (typeKindOf(incomingData)) { + case UA_DATATYPEKIND_BOOLEAN: + { + UA_Boolean val = (value != 0); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BOOLEAN]); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_BYTE: + if (isWithinRange(value)) { + UA_Byte val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_BYTE]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_SBYTE: + if (isWithinRange(value)) { + UA_SByte val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_SBYTE]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT16: + if (isWithinRange(value)) { + UA_UInt16 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT16]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT16: + if (isWithinRange(value)) { + UA_Int16 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT16]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT32: + if (isWithinRange(value)) { + UA_UInt32 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT32]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_INT32: + if (isWithinRange(value) && + (!enumChoices || + enumChoices->find(static_cast(value)) != enumChoices->end())) { + UA_Int32 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT64: + if (isWithinRange(value)) { + UA_UInt64 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_UINT64]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT64: + if (isWithinRange(value)) { + UA_Int64 val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT64]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_FLOAT: + if (isWithinRange(value)) { + UA_Float val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_FLOAT]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_DOUBLE: + if (isWithinRange(value)) { + UA_Double val = static_cast(value); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_STRING: + { + std::string strval = std::to_string(value); + UA_String val; + val.length = strval.length(); + val.data = const_cast(reinterpret_cast(strval.c_str())); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_STRING]); + markAsDirty(); + ret = 0; + break; + } + case UA_DATATYPEKIND_LOCALIZEDTEXT: + { + std::string strval = std::to_string(value); + UA_LocalizedText val; + val.locale = reinterpret_cast(incomingData.data)->locale; + val.text.length = strval.length(); + val.text.data = const_cast(reinterpret_cast(strval.c_str())); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); + markAsDirty(); + ret = 0; + break; + } + default: + errlogPrintf("%s : unsupported conversion from %s to %s for outgoing data\n", + prec->name, epicsTypeString(value), variantTypeString(incomingData)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + } + if (ret != 0) { + errlogPrintf("%s : value out of range\n", + prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + } + if (ret == 0 && UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : scalar copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + + } + if (ret == 0) + dbgWriteScalar(); + return ret; + } + + // Write array value for EPICS String / UA_String + long + writeArray (const char *value, const epicsUInt32 len, + const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec); + + // Write array value as templated function on EPICS type, OPC UA container and simple (element) types + // (latter *must match* OPC UA type enum argument) + // CAVEAT: changes must also be reflected in the specialization (in DataElementOpen62541.cpp) + template + long + writeArray (const ET *value, const epicsUInt32 num, + const UA_DataType *targetType, + dbCommon *prec) + { + long ret = 0; + + if (UA_Variant_isScalar(&incomingData)) { + errlogPrintf("%s : OPC UA data type is not an array\n", prec->name); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else if (incomingData.type != targetType) { + errlogPrintf("%s : OPC UA data type (%s) does not match expected type (%s) for EPICS array (%s)\n", + prec->name, + variantTypeString(incomingData), + variantTypeString(targetType), + epicsTypeString(*value)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + UA_StatusCode status; + { // Scope of Guard G + Guard G(outgoingLock); + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + status = UA_Variant_setArrayCopy(&outgoingData, value, num, targetType); + markAsDirty(); + } + if (UA_STATUS_IS_BAD(status)) { + errlogPrintf("%s : array copy failed: %s\n", + prec->name, UA_StatusCode_name(status)); + (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); + ret = 1; + } else { + dbgWriteArray(num, epicsTypeString(*value)); + } + } + return ret; + } + + UpdateQueue incomingQueue; /**< queue of incoming values */ +}; + +} // namespace DevOpcua + +#endif // DEVOPCUA_DATAELEMENTOPEN62541LEAF_H diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.cpp b/devOpcuaSup/open62541/DataElementOpen62541Node.cpp new file mode 100644 index 00000000..87bda87b --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.cpp @@ -0,0 +1,414 @@ +/*************************************************************************\ +* Copyright (c) 2018-2023 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#define epicsExportSharedSymbols +#include "DataElementOpen62541Node.h" +#include "ItemOpen62541.h" +#include "RecordConnector.h" + +#include +#include + +#include + +#include +#include +#include + +namespace DevOpcua { + +#ifndef UA_ENABLE_TYPEDESCRIPTION + // Without type description, we cannot get struct members + #error Set UA_ENABLE_TYPEDESCRIPTION in open62541 +#endif + +// Older open62541 version 1.2 has no UA_DataType_getStructMember +// Code from open62541 version 1.3.7 modified for compatibility with version 1.2 +// and extended to return isOptional flag and index for union + +inline const UA_DataType* +memberTypeOf(const UA_DataType *type, const UA_DataTypeMember *m) { +#ifdef UA_DATATYPES_USE_POINTER + return m->memberType; +#else + const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; + return &typelists[!m->namespaceZero][m->memberTypeIndex]; +#endif +} + +UA_UInt32 +UA_DataType_getStructMemberExt(const UA_DataType *type, const char *memberName, + size_t *outOffset, const UA_DataType **outMemberType, + UA_Boolean *outIsArray, UA_Boolean *outIsOptional) { + + size_t offset = 0; + switch(type->typeKind) { + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + case UA_DATATYPEKIND_UNION: + for(UA_UInt32 i = 0; i < type->membersSize; ++i) { + const UA_DataTypeMember *m = &type->members[i]; + const UA_DataType *mt = memberTypeOf(type, m); + offset += m->padding; + + if(strcmp(memberName, m->memberName) == 0) { + *outOffset = offset; + *outMemberType = mt; + *outIsArray = m->isArray; + *outIsOptional = m->isOptional; + return i+1; + } + + if (type->typeKind == UA_DATATYPEKIND_UNION) { + offset = 0; + } else if(!m->isOptional) { + if(!m->isArray) { + offset += mt->memSize; + } else { + offset += sizeof(size_t); + offset += sizeof(void*); + } + } else { /* field is optional */ + if(!m->isArray) { + offset += sizeof(void *); + } else { + offset += sizeof(size_t); + offset += sizeof(void *); + } + } + } + break; + case UA_DATATYPEKIND_LOCALIZEDTEXT: + *outMemberType = &UA_TYPES[UA_TYPES_STRING]; + *outIsArray = 0; + *outIsOptional = 0; + if (strcmp(memberName, "locale") == 0) { + *outOffset = offsetof(UA_LocalizedText, locale); + return 1; + } + if (strcmp(memberName, "text") == 0) { + *outOffset = offsetof(UA_LocalizedText, text); + return 2; + } + break; + case UA_DATATYPEKIND_QUALIFIEDNAME: + *outIsArray = 0; + *outIsOptional = 0; + if (strcmp(memberName, "namespaceIndex") == 0) { + *outMemberType = &UA_TYPES[UA_DATATYPEKIND_UINT16]; + *outOffset = offsetof(UA_QualifiedName, namespaceIndex); + return 1; + } + if (strcmp(memberName, "name") == 0) { + *outMemberType = &UA_TYPES[UA_TYPES_STRING]; + *outOffset = offsetof(UA_QualifiedName, name); + return 2; + } + break; + } + return 0; +} + +DataElementOpen62541Node::DataElementOpen62541Node (const std::string &name, ItemOpen62541 *item) + : DataElementOpen62541(name, item) + , timesrc(-1) + , mapped(false) +{ + UA_Variant_init(&incomingData); + UA_Variant_init(&outgoingData); +} + +DataElementOpen62541Node::~DataElementOpen62541Node() = default; + +void +DataElementOpen62541Node::createMap (const UA_DataType *type, const std::string *timefrom) +{ + if (debug() >= 5) + std::cout << " ** creating index-to-element map for child elements" << std::endl; + + switch (typeKindOf(type)) { + case UA_DATATYPEKIND_STRUCTURE: + case UA_DATATYPEKIND_OPTSTRUCT: + case UA_DATATYPEKIND_UNION: + case UA_DATATYPEKIND_LOCALIZEDTEXT: + case UA_DATATYPEKIND_QUALIFIEDNAME: + if (timefrom) { + const UA_DataType *timeMemberType; + UA_Boolean timeIsArray; + UA_Boolean timeIsOptional; + size_t timeOffset; + + if (UA_DataType_getStructMemberExt( + type, timefrom->c_str(), &timeOffset, &timeMemberType, &timeIsArray, &timeIsOptional)) { + if (typeKindOf(timeMemberType) != UA_TYPES_DATETIME || timeIsArray) { + errlogPrintf("%s: timestamp element %s has invalid type %s%s - using " + "source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str(), + typeKindName(typeKindOf(timeMemberType)), + timeIsArray ? "[]" : ""); + } else + timesrc = timeOffset; + } else { + errlogPrintf("%s: timestamp element %s not found - using source timestamp\n", + pitem->recConnector->getRecordName(), + timefrom->c_str()); + } + } + + for (const auto &it : elements) { + auto pelem = it.lock(); + if ((pelem->index = UA_DataType_getStructMemberExt(type, + pelem->name.c_str(), + &pelem->offset, + &pelem->memberType, + &pelem->isArray, + &pelem->isOptional))) { + if (debug() >= 5) + std::cout << typeKindName(typeKindOf(type)) << " " << pelem << " index=" << pelem->index + << " offset=" << pelem->offset << " type=" << variantTypeString(pelem->memberType) + << (pelem->isArray ? "[]" : "") << (pelem->isOptional ? " optional" : "") << std::endl; + } else { + std::cerr << "Item " << pitem << ": element " << pelem->name << " not found in " + << variantTypeString(type) << std::endl; + } + } + if (debug() >= 5) + std::cout << " ** " << elements.size() << " child elements mapped to " << variantTypeString(type) << " of " + << type->membersSize << " elements" << std::endl; + break; + default: + std::cerr << "Error: " << this << " is no structured data but a " << typeKindName(typeKindOf(type)) + << std::endl; + } + mapped = true; +} + +void +DataElementOpen62541Node::show (const int level, const unsigned int indent) const +{ + std::string ind(indent * 2, ' '); + std::cout << ind; + std::cout << "node=" << name << " children=" << elements.size() << " mapped=" << (mapped ? "y" : "n") << "\n"; + for (const auto &it : elements) { + if (auto pelem = it.lock()) { + pelem->show(level, indent + 1); + } + } +} + +// Getting the timestamp and status information from the Item assumes that only one thread +// is pushing data into the Item's DataElement structure at any time. +void +DataElementOpen62541Node::setIncomingData (const UA_Variant &value, ProcessReason reason, const std::string *timefrom) +{ + // Cache this element. We can make a shallow copy because + // ItemOpen62541::setIncomingData marks the original response data as "ours". + // Member data is owned by the [ROOT] element. + UA_Variant_clear(&incomingData); + incomingData = value; + + if (UA_Variant_isEmpty(&value)) + return; + + if (debug() >= 5) + std::cout << "Item " << pitem << " element " << name << " splitting structured data to " << elements.size() + << " child elements" << std::endl; + + const UA_DataType *type = value.type; + char *container = static_cast(value.data); + if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { + UA_ExtensionObject &extensionObject = *reinterpret_cast(container); + if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { + // Access content of decoded extension objects + type = extensionObject.content.decoded.type; + container = static_cast(extensionObject.content.decoded.data); + } else { + std::cerr << "Cannot get a structure definition for item " << pitem << " because binaryEncodingId " + << extensionObject.content.encoded.typeId << " is not in the type dictionary." << std::endl; + return; + } + } + + if (!mapped) + createMap(type, timefrom); + + if (timefrom) { + if (timesrc >= 0) + pitem->tsData = ItemOpen62541::uaToEpicsTime(*reinterpret_cast(container + timesrc), 0); + else + pitem->tsData = pitem->tsSource; + } + + for (const auto &it : elements) { + auto pelem = it.lock(); + const UA_DataType *memberType = pelem->memberType; + char *memberData = container + pelem->offset; + UA_Variant memberValue; + size_t arrayLength = 0; // default to scalar + if (pelem->isArray) { + arrayLength = *reinterpret_cast(memberData); + memberData = *reinterpret_cast(memberData + sizeof(size_t)); + } else if (pelem->isOptional) { + /* optional scalar stored through pointer like an array */ + memberData = *reinterpret_cast(memberData); + } + if (type->typeKind == UA_DATATYPEKIND_UNION && pelem->index != *reinterpret_cast(container)) { + // union option not taken + memberData = nullptr; + } + UA_Variant_setArray(&memberValue, memberData, arrayLength, memberType); + memberValue.storageType = UA_VARIANT_DATA_NODELETE; // Keep ownership of data + if (debug() && !memberData) { + std::cerr << pitem->recConnector->getRecordName() << " " << pelem + << (type->typeKind == UA_DATATYPEKIND_UNION ? " not taken choice " : " absent optional ") + << variantTypeString(memberType) << (pelem->isArray ? " array" : " scalar") << std::endl; + } + pelem->setIncomingData(memberValue, memberData ? reason : ProcessReason::readFailure); + } +} + +void +DataElementOpen62541Node::setIncomingEvent (ProcessReason reason) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setIncomingEvent(reason); + } + if (reason == ProcessReason::connectionLoss) { + elementMap.clear(); + timesrc = -1; + mapped = false; + } +} + +void +DataElementOpen62541Node::setState (const ConnectionStatus state) +{ + for (const auto &it : elements) { + auto pelem = it.lock(); + pelem->setState(state); + } +} + +// Helper to update one data structure element from pointer to child +bool +DataElementOpen62541Node::updateDataInStruct (void *container, std::shared_ptr pelem) +{ + bool updated = false; + { // Scope of Guard G + Guard G(pelem->outgoingLock); + if (pelem->isDirty()) { + char *memberData = static_cast(container) + pelem->offset; + const UA_Variant &elementData = pelem->getOutgoingData(); + const UA_DataType *memberType = pelem->memberType; + assert(memberType == elementData.type + || (typeKindOf(memberType) == UA_DATATYPEKIND_ENUM + && typeKindOf(elementData.type) == UA_DATATYPEKIND_INT32)); + if (!pelem->isArray && !pelem->isOptional) { + // mandatory scalar: shallow copy + UA_clear(memberData, memberType); + void *data = pelem->moveOutgoingData(); + if (typeKindOf(outgoingData) == UA_DATATYPEKIND_UNION) { + *reinterpret_cast(container) = pelem->index; + } + memcpy(memberData, data, memberType->memSize); + UA_free(data); + } else { + // array or optional scalar: move content + void **memberDataPtr; + if (pelem->isArray) /* mandatory or optional array stored as length and pointer */ { + size_t &arrayLength = *reinterpret_cast(memberData); + memberDataPtr = reinterpret_cast(memberData + sizeof(size_t)); + UA_Array_delete(*memberDataPtr, arrayLength, memberType); + arrayLength = elementData.arrayLength; + } else /* optional scalar stored through pointer */ { + memberDataPtr = reinterpret_cast(memberData); + if (*memberDataPtr) /* absent optional has nullptr here */ + UA_Array_delete(*memberDataPtr, 1, memberType); + } + *memberDataPtr = pelem->moveOutgoingData(); + } + pelem->isdirty = false; + updated = true; + } + } + if (debug() >= 4) { + if (updated) { + std::cout << "Data from child element " << pelem->name << " inserted into data structure" << std::endl; + } else { + std::cout << "Data from child element " << pelem->name << " ignored (not dirty)" << std::endl; + } + } + return updated; +} + +const UA_Variant & +DataElementOpen62541Node::getOutgoingData () +{ + if (debug() >= 4) + std::cout << "Item " << pitem << " element " << name << " updating structured data from " << elements.size() + << " child elements" << std::endl; + + UA_Variant_clear(&outgoingData); + UA_Variant_copy(&incomingData, &outgoingData); + isdirty = false; + const UA_DataType *type = outgoingData.type; + void *container = outgoingData.data; + + if (typeKindOf(type) == UA_DATATYPEKIND_EXTENSIONOBJECT) { + UA_ExtensionObject &extensionObject = *reinterpret_cast(container); + if (extensionObject.encoding >= UA_EXTENSIONOBJECT_DECODED) { + // Access content decoded extension objects + type = extensionObject.content.decoded.type; + container = extensionObject.content.decoded.data; + } else { + std::cerr << "Cannot get a structure definition for item " << pitem << " because binaryEncodingId " + << extensionObject.content.encoded.typeId << " is not in the type dictionary." << std::endl; + return outgoingData; + } + } + + if (!mapped) + createMap(type); + + for (const auto &it : elements) { + auto pelem = it.lock(); + if (updateDataInStruct(container, pelem)) + isdirty = true; + } + if (isdirty) { + if (debug() >= 4) + std::cout << "Encoding changed data structure to outgoingData of element " << name << std::endl; + } else { + if (debug() >= 4) + std::cout << "Returning unchanged outgoingData of element " << name << std::endl; + } + return outgoingData; +} + +void +DataElementOpen62541Node::clearOutgoingData () +{ + UA_Variant_clear(&outgoingData); +} + +void +DataElementOpen62541Node::requestRecordProcessing (const ProcessReason reason) const +{ + for (auto &it : elementMap) { + auto pelem = it.second.lock(); + pelem->requestRecordProcessing(reason); + } +} + +} // namespace DevOpcua diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.h b/devOpcuaSup/open62541/DataElementOpen62541Node.h new file mode 100644 index 00000000..ee7929cd --- /dev/null +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.h @@ -0,0 +1,84 @@ +/*************************************************************************\ +* Copyright (c) 2018-2023 ITER Organization. +* This module is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Dirk Zimoch + * + * based on the UaSdk implementation by Ralph Lange + */ + +#ifndef DEVOPCUA_DATAELEMENTOPEN62541NODE_H +#define DEVOPCUA_DATAELEMENTOPEN62541NODE_H + +#include "DataElementOpen62541.h" + +#include +#include +#include +#include + +#include +#ifndef UA_STATUSCODE_BAD // Not yet defined in open62541 version 1.2 +#define UA_STATUSCODE_BAD 0x80000000 +#endif +#define UA_STATUS_IS_BAD(status) (((status)&UA_STATUSCODE_BAD)!=0) + +#ifndef UA_STATUSCODE_UNCERTAIN +#define UA_STATUSCODE_UNCERTAIN 0x40000000 +#endif +#define UA_STATUS_IS_UNCERTAIN(status) (((status)&UA_STATUSCODE_UNCERTAIN)!=0) + +namespace DevOpcua { + +class DataElementOpen62541Node : public DataElementOpen62541 +{ +public: + DataElementOpen62541Node(const std::string &name, + ItemOpen62541 *item); + virtual ~DataElementOpen62541Node() override; + + /* ElementTree node interface methods */ + virtual bool isLeaf() const override { return false; } + virtual void addChild(std::weak_ptr elem) override + { + elements.push_back(elem); + } + virtual std::shared_ptr findChild(const std::string &name) const override + { + for (const auto &it : elements) + if (auto pit = it.lock()) + if (pit->name == name) + return pit; + return std::shared_ptr(); + } + + virtual void show(const int level, const unsigned int indent) const override; + + virtual void setIncomingData(const UA_Variant &value, + ProcessReason reason, + const std::string *timefrom = nullptr) override; + virtual void setIncomingEvent(ProcessReason reason) override; + virtual void setState(const ConnectionStatus state) override; + virtual const UA_Variant &getOutgoingData() override; + virtual void clearOutgoingData() override; + virtual void requestRecordProcessing(const ProcessReason reason) const override; + +private: + virtual bool isDirty() const override { return true; } + virtual void markAsDirty() override {} + + bool updateDataInStruct(void* container, std::shared_ptr pelem); + void createMap(const UA_DataType *type, const std::string* timefrom = nullptr); + + std::vector> elements; /**< children (if node) */ + std::unordered_map> elementMap; + ptrdiff_t timesrc; + bool mapped; /**< child name to index mapping done */ +}; + +} // namespace DevOpcua + +#endif // DEVOPCUA_DATAELEMENTOPEN62541NODE_H diff --git a/devOpcuaSup/open62541/ItemOpen62541.cpp b/devOpcuaSup/open62541/ItemOpen62541.cpp index 7d8b3b3c..3c5f0799 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.cpp +++ b/devOpcuaSup/open62541/ItemOpen62541.cpp @@ -12,6 +12,7 @@ #define epicsExportSharedSymbols #include "ItemOpen62541.h" +#include "RecordConnector.h" #include "SubscriptionOpen62541.h" #include "SessionOpen62541.h" #include "DataElementOpen62541.h" @@ -41,7 +42,6 @@ ItemOpen62541::ItemOpen62541(const linkInfo &info) , registered(false) , revisedSamplingInterval(0.0) , revisedQueueSize(0) - , dataTree(this) , dataTreeDirty(false) , lastStatus(UA_STATUSCODE_BADSERVERNOTCONNECTED) , lastReason(ProcessReason::connectionLoss) diff --git a/devOpcuaSup/open62541/ItemOpen62541.h b/devOpcuaSup/open62541/ItemOpen62541.h index 7554b25e..e1f9a9a1 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.h +++ b/devOpcuaSup/open62541/ItemOpen62541.h @@ -19,7 +19,9 @@ namespace DevOpcua { +class SubscriptionOpen62541; class DataElementOpen62541; +class DataElementOpen62541Node; /** * @brief The ItemOpen62541 inplementation of an OPC UA item. @@ -29,6 +31,8 @@ class DataElementOpen62541; class ItemOpen62541 : public Item { friend class DataElementOpen62541; + friend class DataElementOpen62541Node; + friend class DataElementOpen62541Leaf; public: /** @@ -206,7 +210,7 @@ class ItemOpen62541 : public Item bool registered; /**< flag for registration status */ UA_Double revisedSamplingInterval; /**< server-revised sampling interval */ UA_UInt32 revisedQueueSize; /**< server-revised queue size */ - ElementTree dataTree; /**< data element tree */ + ElementTree dataTree; /**< data element tree */ epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ bool dataTreeDirty; /**< true if any element has been modified */ UA_StatusCode lastStatus; /**< status code of most recent service */ diff --git a/devOpcuaSup/open62541/Makefile.config b/devOpcuaSup/open62541/Makefile.config index d1b82cb7..7cfc7646 100644 --- a/devOpcuaSup/open62541/Makefile.config +++ b/devOpcuaSup/open62541/Makefile.config @@ -33,7 +33,8 @@ opcua_SRCS += SessionOpen62541.cpp opcua_SRCS += Subscription.cpp opcua_SRCS += SubscriptionOpen62541.cpp opcua_SRCS += ItemOpen62541.cpp -opcua_SRCS += DataElementOpen62541.cpp +opcua_SRCS += DataElementOpen62541Node.cpp +opcua_SRCS += DataElementOpen62541Leaf.cpp DBD_INSTALLS += opcua.dbd diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index e5821b7c..e6576628 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -12,8 +12,8 @@ #define epicsExportSharedSymbols #include "SubscriptionOpen62541.h" -#include "DataElementOpen62541.h" #include "ItemOpen62541.h" +#include "RecordConnector.h" #include "Registry.h" #include "devOpcua.h" diff --git a/unitTestApp/src/ElementTreeTest.cpp b/unitTestApp/src/ElementTreeTest.cpp index da9978cf..d3d15082 100644 --- a/unitTestApp/src/ElementTreeTest.cpp +++ b/unitTestApp/src/ElementTreeTest.cpp @@ -21,87 +21,138 @@ namespace { using namespace DevOpcua; +class TestItem +{ +public: + TestItem () {} + ~TestItem () {} +}; + // Test class with the minimal required interface // and test reference counting -class TestItem; +class TestNode; -class TestNode +class TestBase { public: - TestNode(const std::string &name, std::shared_ptr parent) + TestBase(const std::string &name, TestItem *item) : name(name) - , parent(parent) + , item(item) { instanceCount++; } + virtual ~TestBase() { instanceCount--; } - TestNode(const std::string &name, TestItem *item) - : name(name) - , item(item) + void setParent(std::shared_ptr elem) { parent = elem; } + virtual bool isLeaf() = 0; + virtual bool hasChild(TestBase *c) const = 0; + virtual void addChild(std::weak_ptr elem) = 0; + virtual std::shared_ptr findChild(const std::string &name) const = 0; + + virtual std::shared_ptr lastElement() const = 0; + virtual std::shared_ptr firstElement() const = 0; + + static unsigned int instances() { return instanceCount; } + + const std::string name; + +private: + static unsigned int instanceCount; + std::shared_ptr parent; + TestItem *item; +}; + +class TestNode : public TestBase +{ +public: + TestNode(const std::string &name, TestItem* item = nullptr) + : TestBase(name, item) { instanceCount++; } + virtual ~TestNode() override { instanceCount--; } + + virtual void addChild(std::weak_ptr elem) override { elements.push_back(elem); } - void - addChild(std::weak_ptr elem) + virtual std::shared_ptr findChild(const std::string &name) const override { - elements.push_back(elem); + for (const auto &it : elements) + if (auto pit = it.lock()) + if (pit->name == name) + return pit; + return std::shared_ptr(); } - void - setParent(std::shared_ptr elem) + virtual bool hasChild(TestBase *c) const override { - parent = elem; + for (const auto &it : elements) + if (auto pit = it.lock()) + if (pit && pit.get() == c) + return true; + return false; } - std::shared_ptr - findChild(const std::string &name) + virtual std::shared_ptr lastElement() const override { - for (auto it : elements) - if (auto pit = it.lock()) - if (pit->name == name) - return pit; - return std::shared_ptr(); + return elements.back().lock(); } - bool - isLeaf() + virtual std::shared_ptr firstElement() const override { - return name[0] == 'l'; + return elements.front().lock(); } - static unsigned int - instances() + virtual bool isLeaf() override { return false; } + + static unsigned int instances() { return instanceCount; } + + std::vector> elements; + +private: + static unsigned int instanceCount; +}; + +class TestLeaf : public TestBase +{ +public: + TestLeaf(const std::string &name, TestItem *item = nullptr) + : TestBase(name, item) { - return instanceCount; + instanceCount++; } + virtual ~TestLeaf() override { instanceCount--; } + + virtual bool hasChild(TestBase *c) const override { return false; } - bool - hasChild(std::shared_ptr &test) + virtual void addChild(std::weak_ptr elem) override {} + + virtual std::shared_ptr findChild(const std::string &name) const override { - bool found = false; - for (auto it : elements) { - auto el = it.lock(); - if (el && el.get() == test.get()) { - found = true; - break; - } - } - return found; + return std::shared_ptr(); } - ~TestNode() { instanceCount--; } - const std::string name; - std::vector> elements; -private: - std::shared_ptr parent; + virtual std::shared_ptr lastElement() const override + { + return std::shared_ptr(); + } + + virtual std::shared_ptr firstElement() const override + { + return std::shared_ptr(); + } + + virtual bool isLeaf() override { return true; } + + static unsigned int instances() { return instanceCount; } +private: static unsigned int instanceCount; - TestItem *item; }; +unsigned int TestBase::instanceCount = 0; unsigned int TestNode::instanceCount = 0; +unsigned int TestLeaf::instanceCount = 0; // Fixture // r0 = empty @@ -112,40 +163,42 @@ class CreateStructureTest : public ::testing::Test { protected: CreateStructureTest() - : r0(item) - , r1(item) + : r0() + , r1() { - n01 = std::make_shared("n01", nullptr); - n011 = std::make_shared("n011", n01); + n01 = std::make_shared("n01"); + n011 = std::make_shared("n011"); n01->addChild(n011); n011->setParent(n01); - n012 = std::make_shared("n012", n01); + n012 = std::make_shared("n012"); n01->addChild(n012); n012->setParent(n01); - r1.addLeaf(n01, {"n01"}); + r1.addLeaf(std::shared_ptr(n01), {"n01"}, &it); - l0 = std::make_shared("l0", item); - l1 = std::make_shared("l1", item); + l0 = std::make_shared("l0"); + l1 = std::make_shared("l1"); + fixBaseInstances = TestBase::instances(); fixNodeInstances = TestNode::instances(); + fixLeafInstances = TestLeaf::instances(); } - unsigned int - addedNodes() - { - return TestNode::instances() - fixNodeInstances; - } + unsigned int addedNodes() { return TestNode::instances() - fixNodeInstances; } + unsigned int addedBases() { return TestBase::instances() - fixBaseInstances; } + unsigned int addedLeafs() { return TestLeaf::instances() - fixLeafInstances; } - TestItem *item = nullptr; - ElementTree r0; - ElementTree r1; - std::shared_ptr l0; - std::shared_ptr l1; + ElementTree r0; + ElementTree r1; + std::shared_ptr l0; + std::shared_ptr l1; std::shared_ptr n01; std::shared_ptr n011; std::shared_ptr n012; + unsigned int fixBaseInstances; unsigned int fixNodeInstances; + unsigned int fixLeafInstances; + TestItem it; }; /* std::shared_ptr @@ -160,7 +213,9 @@ class CreateStructureTest : public ::testing::Test TEST_F(CreateStructureTest, fixtureSetup_RefCountersOk) { - EXPECT_EQ(TestNode::instances(), 6u) << "fixture does not contain 6 TestNode instances"; + EXPECT_EQ(TestBase::instances(), 6u) << "fixture does not contain 6 TestBase instances"; + EXPECT_EQ(TestNode::instances(), 4u) << "fixture does not contain 4 TestNode instances"; + EXPECT_EQ(TestLeaf::instances(), 2u) << "fixture does not contain 2 TestLeaf instances"; } TEST_F(CreateStructureTest, operatorBool_EmptyAndNonemptyTree) @@ -172,7 +227,7 @@ TEST_F(CreateStructureTest, operatorBool_EmptyAndNonemptyTree) TEST_F(CreateStructureTest, nearestNode_NodeLeafToEmptyRoot) { auto path(splitString("n0.l0")); - std::shared_ptr closest = r0.nearestNode(path); + std::shared_ptr closest = r0.nearestNode(path); EXPECT_EQ(closest, r0.root().lock()) << "checking node+leaf against empty root doesn't return an empty pointer"; EXPECT_EQ(path.size(), 2u) << "checking node+leaf against empty root changes the path size"; @@ -181,7 +236,7 @@ TEST_F(CreateStructureTest, nearestNode_NodeLeafToEmptyRoot) TEST_F(CreateStructureTest, nearestNode_LeafToRootNode) { auto path(splitString("l0")); - std::shared_ptr closest = r1.nearestNode(path); + std::shared_ptr closest = r1.nearestNode(path); EXPECT_EQ(closest, r1.root().lock()) << "checking leaf against root node doesn't return the root itself"; EXPECT_EQ(path.size(), 1u) << "checking leaf against root doesn't return a path of size 1"; @@ -191,7 +246,7 @@ TEST_F(CreateStructureTest, nearestNode_LeafToRootNode) TEST_F(CreateStructureTest, nearestNode_NodeLeafToRootNode) { auto path(splitString("n02.l0")); - std::shared_ptr closest = r1.nearestNode(path); + std::shared_ptr closest = r1.nearestNode(path); EXPECT_EQ(closest, r1.root().lock()) << "checking node+leaf against root node doesn't return the root itself"; EXPECT_EQ(path.size(), 2u) << "checking node+leaf against root doesn't return a path of size 2"; @@ -204,7 +259,7 @@ TEST_F(CreateStructureTest, nearestNode_NodeLeafToRootNode) TEST_F(CreateStructureTest, nearestNode_NodeLeafToSecondNode) { auto path(splitString("n01.n013.l0")); - std::shared_ptr closest = r1.nearestNode(path); + std::shared_ptr closest = r1.nearestNode(path); EXPECT_EQ(closest, n01) << "checking node+leaf against 2nd node n01 doesn't return n01"; EXPECT_EQ(path.size(), 2u) << "checking node+leaf against 2nd node doesn't return a path of size 2"; @@ -217,7 +272,7 @@ TEST_F(CreateStructureTest, nearestNode_NodeLeafToSecondNode) TEST_F(CreateStructureTest, nearestNode_NodeLeafToThirdNode) { auto path(splitString("n01.n011.n0112.l0")); - std::shared_ptr closest = r1.nearestNode(path); + std::shared_ptr closest = r1.nearestNode(path); EXPECT_EQ(closest, n011) << "checking node+leaf against 3rd node n011 doesn't return n011"; EXPECT_EQ(path.size(), 2u) << "checking node+leaf against 3rd node doesn't return a path of size 2"; @@ -230,8 +285,8 @@ TEST_F(CreateStructureTest, nearestNode_NodeLeafToThirdNode) TEST_F(CreateStructureTest, nearestNode_2NodeLeafToEmptyRoot) { auto path(splitString("n2.n21.l0")); - std::shared_ptr empty; - std::shared_ptr closest = r0.nearestNode(path); + std::shared_ptr empty; + std::shared_ptr closest = r0.nearestNode(path); EXPECT_EQ(closest, empty) << "checking 2nodes+leaf against non-existing root doesn't return "; EXPECT_EQ(path.size(), 3u) @@ -253,179 +308,160 @@ TEST_F(CreateStructureTest, nearestNode_2NodeLeafToEmptyRoot) TEST_F(CreateStructureTest, addLeaf_UnnamedLeafToEmptyRoot) { - r0.addLeaf(l0, {}); + r0.addLeaf(l0, {}, &it); EXPECT_EQ(addedNodes(), 0u) << "adding unnamed leaf to empty root creates additional nodes"; - EXPECT_EQ(r0.root().lock(), l0) << "adding unnamed leaf to empty root doesn't make leaf the root node"; + EXPECT_EQ(r0.root().lock(), l0) + << "adding unnamed leaf to empty root doesn't make leaf the root node"; } TEST_F(CreateStructureTest, addLeaf_NamedLeafToEmptyRoot) { - r0.addLeaf(l0, {"l0"}); - EXPECT_EQ(addedNodes(), 1u) << "adding named leaf to empty root doesn't create 1 additional node"; + r0.addLeaf(l0, {"l0"}, &it); + EXPECT_EQ(addedNodes(), 1u) + << "adding named leaf to empty root doesn't create 1 additional node"; auto node = r0.root().lock(); - EXPECT_EQ(node->hasChild(l0), true) + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding named leaf to empty root doesn't show leaf l0 as a child of the root node"; } TEST_F(CreateStructureTest, addLeaf_NodeLeafToEmptyRoot) { - r0.addLeaf(l0, splitString("n0.l0")); + r0.addLeaf(l0, splitString("n0.l0"), &it); EXPECT_EQ(addedNodes(), 2u) << "adding node+leaf to empty root doesn't create 2 intermediate nodes"; - auto node1 = r0.root().lock(); - auto node2 = node1->elements.back().lock(); - EXPECT_EQ(node2->hasChild(l0), true) << "adding node+leaf to empty root doesn't show " - "leaf l0 as a child of the 2nd intermediate node " - << node2->name; + auto node = r0.root().lock()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding node+leaf to empty root doesn't show " + "leaf l0 as a child of the 2nd intermediate node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_2NodeLeafToEmptyRoot) { - r0.addLeaf(l0, splitString("n0.n01.l0")); + r0.addLeaf(l0, splitString("n0.n01.l0"), &it); EXPECT_EQ(addedNodes(), 3u) << "adding 2nodes+leaf to empty root doesn't create 3 intermediate nodes"; - auto node1 = r0.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.back().lock(); - EXPECT_EQ(node3->hasChild(l0), true) << "adding 2nodes+leaf to empty root doesn't show " - "leaf l0 as a child of the 3rd intermediate node " - << node3->name; + auto node = r0.root().lock()->lastElement()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding 2nodes+leaf to empty root doesn't show " + "leaf l0 as a child of the 3rd intermediate node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_LeafToExistingRoot) { - r1.addLeaf(l0, splitString("l0")); + r1.addLeaf(l0, splitString("l0"), &it); EXPECT_EQ(addedNodes(), 0u) << "adding leaf to existing root creates intermediate nodes"; auto node = r1.root().lock(); - EXPECT_EQ(node->hasChild(l0), true) << "adding leaf to existing root doesn't show " - "leaf l0 as a child of the root node " - << node->name; + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding leaf to existing root doesn't show " + "leaf l0 as a child of the root node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_NodeLeafToExistingRoot) { - r1.addLeaf(l0, splitString("n02.l0")); + r1.addLeaf(l0, splitString("n02.l0"), &it); EXPECT_EQ(addedNodes(), 1u) << "adding node+leaf to existing root doesn't create intermediate node"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - EXPECT_EQ(node2->hasChild(l0), true) << "adding node+leaf to existing root doesn't show " - "leaf l0 as the last child of the intermediate node " - << node2->name; + auto node = r1.root().lock()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) + << "adding node+leaf to existing root doesn't show " + "leaf l0 as the last child of the intermediate node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_2NodeLeafToExistingRoot) { - r1.addLeaf(l0, splitString("n02.n021.l0")); + r1.addLeaf(l0, splitString("n02.n021.l0"), &it); EXPECT_EQ(addedNodes(), 2u) << "adding 2nodes+leaf to existing root doesn't create 2 intermediate nodes"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.back().lock(); - EXPECT_EQ(node3->hasChild(l0), true) + auto node = r1.root().lock()->lastElement()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding 2nodes+leaf to existing root doesn't show " "leaf l0 as the last child of the 3rd intermediate node " - << node3->name; + << node->name; } TEST_F(CreateStructureTest, addLeaf_LeafToSecondNode) { - r1.addLeaf(l0, splitString("n01.l0")); + r1.addLeaf(l0, splitString("n01.l0"), &it); EXPECT_EQ(addedNodes(), 0u) << "adding leaf to second node creates intermediate nodes"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - EXPECT_EQ(node2->hasChild(l0), true) << "adding leaf to 2nd node doesn't show " - "leaf l0 as the last child of the 2nd node " - << node2->name; + auto node = r1.root().lock()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding leaf to 2nd node doesn't show " + "leaf l0 as the last child of the 2nd node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_NodeLeafToSecondNode) { - r1.addLeaf(l0, splitString("n01.n013.l0")); + r1.addLeaf(l0, splitString("n01.n013.l0"), &it); EXPECT_EQ(addedNodes(), 1u) << "adding node+leaf to second node doesn't create intermediate node"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.back().lock(); - EXPECT_EQ(node3->hasChild(l0), true) << "adding node+leaf to 2nd node doesn't show " - "leaf l0 as the last child of the 2nd+new node " - << node3->name; + auto node = r1.root().lock()->lastElement()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding node+leaf to 2nd node doesn't show " + "leaf l0 as the last child of the 2nd+new node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_2NodeLeafToSecondNode) { - r1.addLeaf(l0, splitString("n01.n013.n0131.l0")); + r1.addLeaf(l0, splitString("n01.n013.n0131.l0"), &it); EXPECT_EQ(addedNodes(), 2u) << "adding 2nodes+leaf to 2nd node doesn't create 2 intermediate nodes"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.back().lock(); - auto node4 = node3->elements.back().lock(); - EXPECT_EQ(node4->hasChild(l0), true) << "adding 2nodes+leaf to 2nd node doesn't show " - "leaf l0 as the last child of the 2nd+2new node " - << node4->name; + auto node = r1.root().lock()->lastElement()->lastElement()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding 2nodes+leaf to 2nd node doesn't show " + "leaf l0 as the last child of the 2nd+2new node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_LeafToThirdNode) { - r1.addLeaf(l0, splitString("n01.n011.l0")); + r1.addLeaf(l0, splitString("n01.n011.l0"), &it); EXPECT_EQ(addedNodes(), 0u) << "adding leaf to 3rd node creates intermediate nodes"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.front().lock(); - EXPECT_EQ(node3->hasChild(l0), true) << "adding leaf to 3rd node doesn't show " - "leaf l0 as the last child of the 3rd node " - << node3->name; + auto node = r1.root().lock()->lastElement()->firstElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding leaf to 3rd node doesn't show " + "leaf l0 as the last child of the 3rd node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_NodeLeafToThirdNode) { - r1.addLeaf(l0, splitString("n01.n011.n0112.l0")); - EXPECT_EQ(addedNodes(), 1u) - << "adding node+leaf to 3rd node doesn't create intermediate node"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.front().lock(); - auto node4 = node3->elements.back().lock(); - EXPECT_EQ(node4->hasChild(l0), true) << "adding node+leaf to 3rd node doesn't show " - "leaf l0 as the last child of the 3rd+new node " - << node4->name; + r1.addLeaf(l0, splitString("n01.n011.n0112.l0"), &it); + EXPECT_EQ(addedNodes(), 1u) << "adding node+leaf to 3rd node doesn't create intermediate node"; + auto node = r1.root().lock()->lastElement()->firstElement()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding node+leaf to 3rd node doesn't show " + "leaf l0 as the last child of the 3rd+new node " + << node->name; } TEST_F(CreateStructureTest, addLeaf_2NodeLeafToThirdNode) { - r1.addLeaf(l0, splitString("n01.n011.n0112.n01122.l0")); + r1.addLeaf(l0, splitString("n01.n011.n0112.n01122.l0"), &it); EXPECT_EQ(addedNodes(), 2u) << "adding 2nodes+leaf to third node doesn't create 2 intermediate nodes"; - auto node1 = r1.root().lock(); - auto node2 = node1->elements.back().lock(); - auto node3 = node2->elements.front().lock(); - auto node4 = node3->elements.back().lock(); - auto node5 = node4->elements.back().lock(); - EXPECT_EQ(node5->hasChild(l0), true) << "adding 2nodes+leaf to 3rd node doesn't show " - "leaf l0 as the last child of the 3rd+2new node " - << node5->name; + auto node = r1.root().lock()->lastElement()->firstElement()->lastElement()->lastElement(); + EXPECT_EQ(node->hasChild(l0.get()), true) << "adding 2nodes+leaf to 3rd node doesn't show " + "leaf l0 as the last child of the 3rd+2new node " + << node->name; } // Error conditions TEST_F(CreateStructureTest, addLeaf_LeafUnderExistingLeaf_throws) { - r1.addLeaf(l0, splitString("n0.l0")); - EXPECT_THROW(r1.addLeaf(l1, splitString("n0.l0.l1")), std::runtime_error) + r1.addLeaf(l0, splitString("n0.l0"), &it); + EXPECT_THROW(r1.addLeaf(l1, splitString("n0.l0.l1"), &it), std::runtime_error) << "adding leaf under leaf didn't throw"; } TEST_F(CreateStructureTest, addLeaf_NodeLeafUnderExistingLeaf_throws) { - r1.addLeaf(l0, splitString("n0.l0")); - EXPECT_THROW(r1.addLeaf(l1, splitString("n0.l0.n1.l1")), std::runtime_error) + r1.addLeaf(l0, splitString("n0.l0"), &it); + EXPECT_THROW(r1.addLeaf(l1, splitString("n0.l0.n1.l1"), &it), std::runtime_error) << "adding node+leaf under leaf didn't throw"; } TEST_F(CreateStructureTest, addLeaf_UnnamedLeafToExistingRoot_throws) { - EXPECT_THROW(r1.addLeaf(l0, {}), std::runtime_error) + EXPECT_THROW(r1.addLeaf(l0, {}, &it), std::runtime_error) << "adding unnamed leaf to existing root didn't throw"; } diff --git a/unitTestApp/src/UaSdk/Makefile.config b/unitTestApp/src/UaSdk/Makefile.config index 74d44feb..584f21ac 100644 --- a/unitTestApp/src/UaSdk/Makefile.config +++ b/unitTestApp/src/UaSdk/Makefile.config @@ -10,7 +10,7 @@ USR_INCLUDES += $(foreach lib, $(_UASDK_MODS),-I$(UASDK)/include/$(lib)) -UASDK_OPCUA_OBJS += SessionUaSdk SubscriptionUaSdk ItemUaSdk DataElementUaSdk +UASDK_OPCUA_OBJS += SessionUaSdk SubscriptionUaSdk ItemUaSdk # Copied from CONFIG_OPCUA - needed here for building the test executables diff --git a/unitTestApp/src/UaSdk/RangeCheckTest.cpp b/unitTestApp/src/UaSdk/RangeCheckTest.cpp index ee26e68d..577d39fc 100644 --- a/unitTestApp/src/UaSdk/RangeCheckTest.cpp +++ b/unitTestApp/src/UaSdk/RangeCheckTest.cpp @@ -12,7 +12,7 @@ #include -#include "DataElementUaSdk.h" +#include "DataElementUaSdkLeaf.h" namespace { diff --git a/unitTestApp/src/open62541/Makefile.config b/unitTestApp/src/open62541/Makefile.config index 49b41142..d1009947 100644 --- a/unitTestApp/src/open62541/Makefile.config +++ b/unitTestApp/src/open62541/Makefile.config @@ -10,7 +10,7 @@ USR_INCLUDES += -I$(OPEN62541)/include -OPEN62541_OPCUA_OBJS += SessionOpen62541 SubscriptionOpen62541 ItemOpen62541 DataElementOpen62541 +OPEN62541_OPCUA_OBJS += SessionOpen62541 SubscriptionOpen62541 ItemOpen62541 # repeated here as the CONFIG_OPEN62541 only does it for PROVIDED open62541_DIR = $(OPEN62541_LIB_DIR) From 8a0ce757f6137cfb8657a37ea0c78eb76a6fd299 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Mon, 17 Nov 2025 17:21:42 +0100 Subject: [PATCH 46/64] Clean up unused includes --- devOpcuaSup/UaSdk/SessionUaSdk.cpp | 2 -- devOpcuaSup/UaSdk/Subscription.cpp | 2 -- devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp | 1 - devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp | 1 - devOpcuaSup/open62541/SessionOpen62541.cpp | 3 --- devOpcuaSup/open62541/SessionOpen62541.h | 1 - devOpcuaSup/open62541/Subscription.cpp | 1 - devOpcuaSup/open62541/SubscriptionOpen62541.cpp | 1 + 8 files changed, 1 insertion(+), 11 deletions(-) diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.cpp b/devOpcuaSup/UaSdk/SessionUaSdk.cpp index ac07796c..447c58df 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SessionUaSdk.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -52,7 +51,6 @@ #include "RequestQueueBatcher.h" #include "SessionUaSdk.h" #include "SubscriptionUaSdk.h" -#include "DataElementUaSdkNode.h" #include "ItemUaSdk.h" namespace DevOpcua { diff --git a/devOpcuaSup/UaSdk/Subscription.cpp b/devOpcuaSup/UaSdk/Subscription.cpp index 1a64d9e4..43b04bfc 100644 --- a/devOpcuaSup/UaSdk/Subscription.cpp +++ b/devOpcuaSup/UaSdk/Subscription.cpp @@ -16,9 +16,7 @@ #include #define epicsExportSharedSymbols -#include "Session.h" #include "Subscription.h" -#include "SessionUaSdk.h" #include "SubscriptionUaSdk.h" namespace DevOpcua { diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index 72f71ddb..8d7c45b1 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -32,7 +32,6 @@ #define epicsExportSharedSymbols #include "SubscriptionUaSdk.h" #include "RecordConnector.h" -#include "ItemUaSdk.h" #include "DataElementUaSdk.h" #include "Registry.h" #include "devOpcua.h" diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp index efb7c39d..e9a34e82 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp @@ -12,7 +12,6 @@ #define epicsExportSharedSymbols #include "DataElementOpen62541Leaf.h" -#include "DataElementOpen62541Node.h" #include "ItemOpen62541.h" #include "RecordConnector.h" diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index c05d0e34..e61d8b61 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.cpp +++ b/devOpcuaSup/open62541/SessionOpen62541.cpp @@ -15,7 +15,6 @@ #include "SessionOpen62541.h" #include "SubscriptionOpen62541.h" #include "DataElementOpen62541.h" -#include "ItemOpen62541.h" #include "linkParser.h" #ifdef HAS_XMLPARSER @@ -47,8 +46,6 @@ #include #include #include -#include -#include #include /* loadFile helper from open62541 examples */ diff --git a/devOpcuaSup/open62541/SessionOpen62541.h b/devOpcuaSup/open62541/SessionOpen62541.h index d0a3d788..0ad71899 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.h +++ b/devOpcuaSup/open62541/SessionOpen62541.h @@ -41,7 +41,6 @@ #include #include #include -#include #include namespace std { diff --git a/devOpcuaSup/open62541/Subscription.cpp b/devOpcuaSup/open62541/Subscription.cpp index 412d8ee8..b66c680b 100644 --- a/devOpcuaSup/open62541/Subscription.cpp +++ b/devOpcuaSup/open62541/Subscription.cpp @@ -16,7 +16,6 @@ #include #define epicsExportSharedSymbols -#include "Session.h" #include "Subscription.h" #include "SessionOpen62541.h" #include "SubscriptionOpen62541.h" diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index e6576628..c06e0b83 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -12,6 +12,7 @@ #define epicsExportSharedSymbols #include "SubscriptionOpen62541.h" +#include "SessionOpen62541.h" #include "ItemOpen62541.h" #include "RecordConnector.h" #include "Registry.h" From f193438f1d4f40cac0454568c39c1b94d64a7cdd Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 12 Dec 2025 11:19:33 +0100 Subject: [PATCH 47/64] open62541: more includes cleanup - merge branch 'dirk-zimoch/refactor-element-tree' - more fixes - compiles against UASDK 1.7.4, open62541 1.3.16 and 1.2.10 --- devOpcuaSup/open62541/DataElementOpen62541.h | 5 ----- .../open62541/DataElementOpen62541Leaf.cpp | 1 + devOpcuaSup/open62541/DataElementOpen62541Leaf.h | 11 ----------- devOpcuaSup/open62541/DataElementOpen62541Node.h | 16 ---------------- devOpcuaSup/open62541/ItemOpen62541.h | 2 ++ devOpcuaSup/open62541/SessionOpen62541.cpp | 1 + devOpcuaSup/open62541/SessionOpen62541.h | 1 + devOpcuaSup/open62541/SubscriptionOpen62541.cpp | 1 - 8 files changed, 5 insertions(+), 33 deletions(-) diff --git a/devOpcuaSup/open62541/DataElementOpen62541.h b/devOpcuaSup/open62541/DataElementOpen62541.h index 43ed4231..900b39db 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.h +++ b/devOpcuaSup/open62541/DataElementOpen62541.h @@ -16,11 +16,6 @@ #include "Update.h" #include "ItemOpen62541.h" -#include -#include -#include -#include - #include #ifndef UA_STATUSCODE_BAD // Not yet defined in open62541 version 1.2 #define UA_STATUSCODE_BAD 0x80000000 diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp index e9a34e82..efb7c39d 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp @@ -12,6 +12,7 @@ #define epicsExportSharedSymbols #include "DataElementOpen62541Leaf.h" +#include "DataElementOpen62541Node.h" #include "ItemOpen62541.h" #include "RecordConnector.h" diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.h b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h index ae74d32f..4f5293dd 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Leaf.h +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h @@ -23,17 +23,6 @@ #include #include -#include -#ifndef UA_STATUSCODE_BAD // Not yet defined in open62541 version 1.2 -#define UA_STATUSCODE_BAD 0x80000000 -#endif -#define UA_STATUS_IS_BAD(status) (((status)&UA_STATUSCODE_BAD)!=0) - -#ifndef UA_STATUSCODE_UNCERTAIN -#define UA_STATUSCODE_UNCERTAIN 0x40000000 -#endif -#define UA_STATUS_IS_UNCERTAIN(status) (((status)&UA_STATUSCODE_UNCERTAIN)!=0) - #include #include #include diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.h b/devOpcuaSup/open62541/DataElementOpen62541Node.h index ee7929cd..5ae1fd70 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Node.h +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.h @@ -15,22 +15,6 @@ #include "DataElementOpen62541.h" -#include -#include -#include -#include - -#include -#ifndef UA_STATUSCODE_BAD // Not yet defined in open62541 version 1.2 -#define UA_STATUSCODE_BAD 0x80000000 -#endif -#define UA_STATUS_IS_BAD(status) (((status)&UA_STATUSCODE_BAD)!=0) - -#ifndef UA_STATUSCODE_UNCERTAIN -#define UA_STATUSCODE_UNCERTAIN 0x40000000 -#endif -#define UA_STATUS_IS_UNCERTAIN(status) (((status)&UA_STATUSCODE_UNCERTAIN)!=0) - namespace DevOpcua { class DataElementOpen62541Node : public DataElementOpen62541 diff --git a/devOpcuaSup/open62541/ItemOpen62541.h b/devOpcuaSup/open62541/ItemOpen62541.h index e1f9a9a1..95fa8c8b 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.h +++ b/devOpcuaSup/open62541/ItemOpen62541.h @@ -17,6 +17,8 @@ #include "ElementTree.h" #include "SessionOpen62541.h" +#include + namespace DevOpcua { class SubscriptionOpen62541; diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index e61d8b61..5b17ca70 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.cpp +++ b/devOpcuaSup/open62541/SessionOpen62541.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include diff --git a/devOpcuaSup/open62541/SessionOpen62541.h b/devOpcuaSup/open62541/SessionOpen62541.h index 0ad71899..d0a3d788 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.h +++ b/devOpcuaSup/open62541/SessionOpen62541.h @@ -41,6 +41,7 @@ #include #include #include +#include #include namespace std { diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index c06e0b83..e6576628 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -12,7 +12,6 @@ #define epicsExportSharedSymbols #include "SubscriptionOpen62541.h" -#include "SessionOpen62541.h" #include "ItemOpen62541.h" #include "RecordConnector.h" #include "Registry.h" From 4932fedc52920ab01d86990481366703ecabbebd Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 12 Dec 2025 20:56:34 +0100 Subject: [PATCH 48/64] Add .iocsh_history to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 49b23da6..7d64ac7a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ envPaths /*Top/configure/*.local O.*/ +.iocsh_history /QtC-* /.qtc_* .idea/ From 485922c1e9b5bcce34d87be7bc5a6545209f8986 Mon Sep 17 00:00:00 2001 From: Dirk Zimoch Date: Fri, 16 Jan 2026 12:07:34 +0100 Subject: [PATCH 49/64] fix gcc-15 problem gcc-15 mangles variables inside namespaces even if they are declared extern "C". That causes linker errors. This is probably a bug in gcc-15. Observed with gcc version 15.1.1. --- devOpcuaSup/iocshIntegration.cpp | 6 ++---- devOpcuaSup/opcuaItemRecord.cpp | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/devOpcuaSup/iocshIntegration.cpp b/devOpcuaSup/iocshIntegration.cpp index 5db2f6eb..0260e136 100644 --- a/devOpcuaSup/iocshIntegration.cpp +++ b/devOpcuaSup/iocshIntegration.cpp @@ -1106,8 +1106,6 @@ void opcuaIocshRegister () iocshRegister(&opcuaShowDataFuncDef, opcuaShowDataCallFunc); } -extern "C" { -epicsExportRegistrar(opcuaIocshRegister); -} - } // namespace + +extern "C" { epicsExportRegistrar(opcuaIocshRegister); } diff --git a/devOpcuaSup/opcuaItemRecord.cpp b/devOpcuaSup/opcuaItemRecord.cpp index 8a45fa90..ab0f89b4 100644 --- a/devOpcuaSup/opcuaItemRecord.cpp +++ b/devOpcuaSup/opcuaItemRecord.cpp @@ -203,6 +203,7 @@ rset opcuaItemRSET = { get_control_double, get_alarm_double }; -extern "C" { epicsExportAddress(rset, opcuaItemRSET); } } // namespace + +extern "C" { epicsExportAddress(rset, opcuaItemRSET); } From bce2c760366ac9f2db45509c5ebdf75c6c236329 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 20 Jan 2026 16:32:07 +0100 Subject: [PATCH 50/64] Add per-item counters for structure complexity and show() them --- devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp | 16 ++++++++++++---- devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h | 6 +++--- devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp | 15 +++++++++++---- devOpcuaSup/UaSdk/DataElementUaSdkNode.h | 6 +++--- devOpcuaSup/UaSdk/ItemUaSdk.cpp | 9 +++++++-- devOpcuaSup/UaSdk/ItemUaSdk.h | 4 +++- .../open62541/DataElementOpen62541Leaf.cpp | 9 ++++++++- devOpcuaSup/open62541/DataElementOpen62541Leaf.h | 4 ++-- .../open62541/DataElementOpen62541Node.cpp | 8 ++++++-- devOpcuaSup/open62541/DataElementOpen62541Node.h | 2 +- devOpcuaSup/open62541/ItemOpen62541.cpp | 13 +++++++++---- devOpcuaSup/open62541/ItemOpen62541.h | 4 +++- 12 files changed, 68 insertions(+), 28 deletions(-) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp index 87e1ecc3..db0e8c2b 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2025 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -39,11 +39,19 @@ namespace DevOpcua { DataElementUaSdkLeaf::DataElementUaSdkLeaf (const std::string &name, - class ItemUaSdk *pitem, + class ItemUaSdk *item, class RecordConnector *pconnector) - : DataElementUaSdk(name, pitem) + : DataElementUaSdk(name, item) , incomingQueue(pconnector->plinkinfo->clientQueueSize, pconnector->plinkinfo->discardOldest) -{} +{ + item->dataTreeNoOfLeafs++; +} + +DataElementUaSdkLeaf::~DataElementUaSdkLeaf() +{ + delete enumChoices; + pitem->dataTreeNoOfLeafs--; +} /* Explicitly implement the destructor here (allows the compiler to place the vtable) */ DataElementUaSdk::~DataElementUaSdk() = default; diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h index 6fdb4b57..962e8134 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdkLeaf.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2025 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -241,8 +241,8 @@ class DataElementUaSdkLeaf * @param pitem pointer to corresponding ItemUaSdk * @param pconnector pointer to record connector to link to */ - DataElementUaSdkLeaf(const std::string &name, ItemUaSdk *pitem, RecordConnector *pconnector); - virtual ~DataElementUaSdkLeaf () override { delete enumChoices; } + DataElementUaSdkLeaf(const std::string &name, ItemUaSdk *item, RecordConnector *pconnector); + virtual ~DataElementUaSdkLeaf () override; /* ElementTree node interface methods */ virtual bool isLeaf () const override { return true; } diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp b/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp index 55659a87..67ff3fb1 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdkNode.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2025 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -34,11 +34,18 @@ namespace DevOpcua { -DataElementUaSdkNode::DataElementUaSdkNode (const std::string &name, class ItemUaSdk *item) - : DataElementUaSdk(name, item) +DataElementUaSdkNode::DataElementUaSdkNode (const std::string &name, class ItemUaSdk *pitem) + : DataElementUaSdk(name, pitem) , timesrc(-1) , mapped(false) -{} +{ + pitem->dataTreeNoOfNodes++; +} + +DataElementUaSdkNode::~DataElementUaSdkNode() +{ + pitem->dataTreeNoOfNodes--; +} void DataElementUaSdkNode::addChild (std::weak_ptr elem) diff --git a/devOpcuaSup/UaSdk/DataElementUaSdkNode.h b/devOpcuaSup/UaSdk/DataElementUaSdkNode.h index 7cf6b035..25eb12d4 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdkNode.h +++ b/devOpcuaSup/UaSdk/DataElementUaSdkNode.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2025 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -27,8 +27,8 @@ namespace DevOpcua { class DataElementUaSdkNode : public DataElementUaSdk { public: - DataElementUaSdkNode(const std::string &name, class ItemUaSdk *item); - virtual ~DataElementUaSdkNode () override {} + DataElementUaSdkNode(const std::string &name, class ItemUaSdk *pitem); + virtual ~DataElementUaSdkNode () override; /* ElementTree node interface methods */ virtual bool isLeaf() const override { return false; } diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index 2725174f..f3538c5d 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.cpp +++ b/devOpcuaSup/UaSdk/ItemUaSdk.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2021 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -47,6 +47,8 @@ ItemUaSdk::ItemUaSdk (const linkInfo &info) , revisedSamplingInterval(0.0) , revisedQueueSize(0) , dataTreeDirty(false) + , dataTreeNoOfNodes(0) + , dataTreeNoOfLeafs(0) , lastStatus(OpcUa_BadServerNotConnected) , lastReason(ProcessReason::connectionLoss) { @@ -112,7 +114,10 @@ ItemUaSdk::show (int level) const std::cout << " bini=" << linkOptionBiniString(linkinfo.bini) << " output=" << (linkinfo.isOutput ? "y" : "n") << " monitor=" << (linkinfo.monitor ? "y" : "n") << " registered=" << (registered ? nodeid->toString().toUtf8() : "-") << "(" - << (linkinfo.registerNode ? "y" : "n") << ")" << std::endl; + << (linkinfo.registerNode ? "y" : "n") << ")"; + if (!(dataTreeNoOfNodes == 0 && dataTreeNoOfLeafs == 1)) + std::cout << " dataNodes=" << dataTreeNoOfNodes << " dataLeafs=" << dataTreeNoOfLeafs; + std::cout << std::endl; if (level >= 1) { if (auto re = dataTree.root().lock()) { diff --git a/devOpcuaSup/UaSdk/ItemUaSdk.h b/devOpcuaSup/UaSdk/ItemUaSdk.h index 2122613a..f7eeb206 100644 --- a/devOpcuaSup/UaSdk/ItemUaSdk.h +++ b/devOpcuaSup/UaSdk/ItemUaSdk.h @@ -225,8 +225,10 @@ class ItemUaSdk : public Item OpcUa_Double revisedSamplingInterval; /**< server-revised sampling interval */ OpcUa_UInt32 revisedQueueSize; /**< server-revised queue size */ ElementTree dataTree; /**< data element tree */ - epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ + epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ bool dataTreeDirty; /**< true if any element has been modified */ + unsigned int dataTreeNoOfNodes; /**< number of nodes */ + unsigned int dataTreeNoOfLeafs; /**< number of leafs */ UaStatusCode lastStatus; /**< status code of most recent service */ ProcessReason lastReason; /**< most recent processing reason */ epicsTime tsClient; /**< client (local) time stamp */ diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp index efb7c39d..1b223c01 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -36,6 +36,13 @@ DataElementOpen62541Leaf::DataElementOpen62541Leaf (const std::string &name, { UA_Variant_init(&incomingData); UA_Variant_init(&outgoingData); + item->dataTreeNoOfLeafs++; +} + +DataElementOpen62541Leaf::~DataElementOpen62541Leaf() +{ + delete enumChoices; + pitem->dataTreeNoOfLeafs--; } /* Explicitly implement the destructor here (allows the compiler to place the vtable) */ diff --git a/devOpcuaSup/open62541/DataElementOpen62541Leaf.h b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h index 4f5293dd..d3eb3d51 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Leaf.h +++ b/devOpcuaSup/open62541/DataElementOpen62541Leaf.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -207,7 +207,7 @@ class DataElementOpen62541Leaf * @param pconnector pointer to record connector to link to */ DataElementOpen62541Leaf(const std::string &name, ItemOpen62541 *pitem, RecordConnector *pconnector); - virtual ~DataElementOpen62541Leaf() override { delete enumChoices; } + virtual ~DataElementOpen62541Leaf() override; /* ElementTree node interface methods */ virtual bool isLeaf () const override { return true; } diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.cpp b/devOpcuaSup/open62541/DataElementOpen62541Node.cpp index 87bda87b..58ea09e0 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Node.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -125,9 +125,13 @@ DataElementOpen62541Node::DataElementOpen62541Node (const std::string &name, Ite { UA_Variant_init(&incomingData); UA_Variant_init(&outgoingData); + item->dataTreeNoOfNodes++; } -DataElementOpen62541Node::~DataElementOpen62541Node() = default; +DataElementOpen62541Node::~DataElementOpen62541Node() +{ + pitem->dataTreeNoOfNodes--; +} void DataElementOpen62541Node::createMap (const UA_DataType *type, const std::string *timefrom) diff --git a/devOpcuaSup/open62541/DataElementOpen62541Node.h b/devOpcuaSup/open62541/DataElementOpen62541Node.h index 5ae1fd70..efb912d3 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541Node.h +++ b/devOpcuaSup/open62541/DataElementOpen62541Node.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ diff --git a/devOpcuaSup/open62541/ItemOpen62541.cpp b/devOpcuaSup/open62541/ItemOpen62541.cpp index 3c5f0799..89fc4475 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.cpp +++ b/devOpcuaSup/open62541/ItemOpen62541.cpp @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -43,6 +43,8 @@ ItemOpen62541::ItemOpen62541(const linkInfo &info) , revisedSamplingInterval(0.0) , revisedQueueSize(0) , dataTreeDirty(false) + , dataTreeNoOfNodes(0) + , dataTreeNoOfLeafs(0) , lastStatus(UA_STATUSCODE_BADSERVERNOTCONNECTED) , lastReason(ProcessReason::connectionLoss) { @@ -117,9 +119,12 @@ ItemOpen62541::show (int level) const << " registered="; if (registered) std::cout << nodeId; - else std::cout << "-"; - std::cout << "(" << (linkinfo.registerNode ? "y" : "n") << ")" - << std::endl; + else + std::cout << "-"; + std::cout << "(" << (linkinfo.registerNode ? "y" : "n") << ")"; + if (!(dataTreeNoOfNodes == 0 && dataTreeNoOfLeafs == 1)) + std::cout << " dataNodes=" << dataTreeNoOfNodes << " dataLeafs=" << dataTreeNoOfLeafs; + std::cout << std::endl; if (level >= 1) { if (auto re = dataTree.root().lock()) { diff --git a/devOpcuaSup/open62541/ItemOpen62541.h b/devOpcuaSup/open62541/ItemOpen62541.h index 95fa8c8b..ce8d3197 100644 --- a/devOpcuaSup/open62541/ItemOpen62541.h +++ b/devOpcuaSup/open62541/ItemOpen62541.h @@ -1,5 +1,5 @@ /*************************************************************************\ -* Copyright (c) 2018-2023 ITER Organization. +* Copyright (c) 2018-2026 ITER Organization. * This module is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. \*************************************************************************/ @@ -215,6 +215,8 @@ class ItemOpen62541 : public Item ElementTree dataTree; /**< data element tree */ epicsMutex dataTreeWriteLock; /**< lock for dirty flag */ bool dataTreeDirty; /**< true if any element has been modified */ + unsigned int dataTreeNoOfNodes; /**< number of nodes */ + unsigned int dataTreeNoOfLeafs; /**< number of leafs */ UA_StatusCode lastStatus; /**< status code of most recent service */ ProcessReason lastReason; /**< most recent processing reason */ epicsTime tsClient; /**< client (local) time stamp */ From b6f5099e327d909eaa828aaa4ea97d21d053100b Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 14 Apr 2026 16:26:07 +0200 Subject: [PATCH 51/64] ci: bump actions/checkout to v6 --- .github/workflows/ci-build-open62541-linux.yml | 2 +- .github/workflows/ci-build-open62541-win.yml | 4 ++-- .github/workflows/ci-build-uasdk-linux.yml | 2 +- .github/workflows/ci-build-uasdk-win.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-build-open62541-linux.yml b/.github/workflows/ci-build-open62541-linux.yml index 8f669187..33fd160e 100644 --- a/.github/workflows/ci-build-open62541-linux.yml +++ b/.github/workflows/ci-build-open62541-linux.yml @@ -89,7 +89,7 @@ jobs: name: "7.0 Ub-22 v1.2.10" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and compile open62541 client diff --git a/.github/workflows/ci-build-open62541-win.yml b/.github/workflows/ci-build-open62541-win.yml index a63952e0..7c7c1b91 100644 --- a/.github/workflows/ci-build-open62541-win.yml +++ b/.github/workflows/ci-build-open62541-win.yml @@ -60,7 +60,7 @@ jobs: name: "7.0 Win2022 v1.3.15 VS2022 DLL" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true # Install gendef.exe @@ -134,7 +134,7 @@ jobs: mingw-w64-x86_64-cmake mingw-w64-x86_64-python pacboy: openssl:p - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and compile open62541 client diff --git a/.github/workflows/ci-build-uasdk-linux.yml b/.github/workflows/ci-build-uasdk-linux.yml index db23820a..8b45364d 100644 --- a/.github/workflows/ci-build-uasdk-linux.yml +++ b/.github/workflows/ci-build-uasdk-linux.yml @@ -103,7 +103,7 @@ jobs: name: "3.15 Ub-22 v1.7.2" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and install UASDK client diff --git a/.github/workflows/ci-build-uasdk-win.yml b/.github/workflows/ci-build-uasdk-win.yml index 7b4cbcf8..0a403d34 100644 --- a/.github/workflows/ci-build-uasdk-win.yml +++ b/.github/workflows/ci-build-uasdk-win.yml @@ -59,7 +59,7 @@ jobs: name: "7.0 Win2022 v1.6.3 VS2022 DLL" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Download and install UASDK client From 49157862dbd19db819e59f5d5c39b092c4ead119 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 29 May 2026 11:46:07 +0200 Subject: [PATCH 52/64] Rename Registry.h -> OpcuaRegistry.h - avoid ambiguity to EPICS Base's registry.h on Windows --- devOpcuaSup/{Registry.h => OpcuaRegistry.h} | 6 +++--- devOpcuaSup/UaSdk/Session.cpp | 2 +- devOpcuaSup/UaSdk/SessionUaSdk.h | 4 ++-- devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp | 6 +++--- devOpcuaSup/UaSdk/SubscriptionUaSdk.h | 2 +- devOpcuaSup/iocshIntegration.cpp | 11 ++++++----- devOpcuaSup/open62541/Session.cpp | 4 ++-- devOpcuaSup/open62541/SessionOpen62541.h | 4 ++-- devOpcuaSup/open62541/SubscriptionOpen62541.cpp | 2 +- devOpcuaSup/open62541/SubscriptionOpen62541.h | 2 +- unitTestApp/src/RegistryTest.cpp | 2 +- 11 files changed, 23 insertions(+), 22 deletions(-) rename devOpcuaSup/{Registry.h => OpcuaRegistry.h} (97%) diff --git a/devOpcuaSup/Registry.h b/devOpcuaSup/OpcuaRegistry.h similarity index 97% rename from devOpcuaSup/Registry.h rename to devOpcuaSup/OpcuaRegistry.h index eda87146..1e805c46 100644 --- a/devOpcuaSup/Registry.h +++ b/devOpcuaSup/OpcuaRegistry.h @@ -8,8 +8,8 @@ * Author: Ralph Lange */ -#ifndef DEVOPCUA_REGISTRY_H -#define DEVOPCUA_REGISTRY_H +#ifndef DEVOPCUA_OPCUAREGISTRY_H +#define DEVOPCUA_OPCUAREGISTRY_H #include #include @@ -158,4 +158,4 @@ class Registry } // namespace DevOpcua -#endif // DEVOPCUA_REGISTRY_H +#endif // DEVOPCUA_OPCUAREGISTRY_H diff --git a/devOpcuaSup/UaSdk/Session.cpp b/devOpcuaSup/UaSdk/Session.cpp index 3e6a60cd..446242fb 100644 --- a/devOpcuaSup/UaSdk/Session.cpp +++ b/devOpcuaSup/UaSdk/Session.cpp @@ -25,9 +25,9 @@ #include #define epicsExportSharedSymbols +#include "OpcuaRegistry.h" #include "Session.h" #include "SessionUaSdk.h" -#include "Registry.h" #define st(s) #s #define str(s) st(s) diff --git a/devOpcuaSup/UaSdk/SessionUaSdk.h b/devOpcuaSup/UaSdk/SessionUaSdk.h index ed8ec6d6..fac201cf 100644 --- a/devOpcuaSup/UaSdk/SessionUaSdk.h +++ b/devOpcuaSup/UaSdk/SessionUaSdk.h @@ -30,10 +30,10 @@ #include #include +#include "DataElement.h" +#include "OpcuaRegistry.h" #include "RequestQueueBatcher.h" #include "Session.h" -#include "Registry.h" -#include "DataElement.h" namespace DevOpcua { diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index 8d7c45b1..2695e151 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -30,10 +30,10 @@ #include #define epicsExportSharedSymbols -#include "SubscriptionUaSdk.h" -#include "RecordConnector.h" #include "DataElementUaSdk.h" -#include "Registry.h" +#include "OpcuaRegistry.h" +#include "RecordConnector.h" +#include "SubscriptionUaSdk.h" #include "devOpcua.h" namespace DevOpcua { diff --git a/devOpcuaSup/UaSdk/SubscriptionUaSdk.h b/devOpcuaSup/UaSdk/SubscriptionUaSdk.h index 9bde8840..c444b741 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.h +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.h @@ -25,9 +25,9 @@ #include #include +#include "OpcuaRegistry.h" #include "SessionUaSdk.h" #include "Subscription.h" -#include "Registry.h" namespace DevOpcua { diff --git a/devOpcuaSup/iocshIntegration.cpp b/devOpcuaSup/iocshIntegration.cpp index 0260e136..7e9f1d91 100644 --- a/devOpcuaSup/iocshIntegration.cpp +++ b/devOpcuaSup/iocshIntegration.cpp @@ -26,14 +26,15 @@ #include #include -#include // defines epicsExportSharedSymbols +#include // defines epicsExportSharedSymbols + +#include "OpcuaRegistry.h" +#include "RecordConnector.h" +#include "Session.h" +#include "Subscription.h" #include "devOpcua.h" #include "iocshVariables.h" #include "linkParser.h" -#include "Session.h" -#include "Subscription.h" -#include "Registry.h" -#include "RecordConnector.h" namespace DevOpcua { diff --git a/devOpcuaSup/open62541/Session.cpp b/devOpcuaSup/open62541/Session.cpp index 25e46348..864a113a 100644 --- a/devOpcuaSup/open62541/Session.cpp +++ b/devOpcuaSup/open62541/Session.cpp @@ -11,9 +11,9 @@ */ #define epicsExportSharedSymbols -#include "SessionOpen62541.h" #include "Session.h" -#include "Registry.h" +#include "OpcuaRegistry.h" +#include "SessionOpen62541.h" #include "devOpcua.h" #include diff --git a/devOpcuaSup/open62541/SessionOpen62541.h b/devOpcuaSup/open62541/SessionOpen62541.h index d0a3d788..83974001 100644 --- a/devOpcuaSup/open62541/SessionOpen62541.h +++ b/devOpcuaSup/open62541/SessionOpen62541.h @@ -13,9 +13,9 @@ #ifndef DEVOPCUA_SESSIONOPEN62541_H #define DEVOPCUA_SESSIONOPEN62541_H -#include "Session.h" -#include "Registry.h" +#include "OpcuaRegistry.h" #include "RequestQueueBatcher.h" +#include "Session.h" #include #include diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index e6576628..894609e4 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -13,8 +13,8 @@ #define epicsExportSharedSymbols #include "SubscriptionOpen62541.h" #include "ItemOpen62541.h" +#include "OpcuaRegistry.h" #include "RecordConnector.h" -#include "Registry.h" #include "devOpcua.h" #include diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.h b/devOpcuaSup/open62541/SubscriptionOpen62541.h index 75530260..948e2ae3 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.h +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.h @@ -13,8 +13,8 @@ #ifndef DEVOPCUA_SUBSCRIPTIONOPEN62541_H #define DEVOPCUA_SUBSCRIPTIONOPEN62541_H +#include "OpcuaRegistry.h" #include "Subscription.h" -#include "Registry.h" #include diff --git a/unitTestApp/src/RegistryTest.cpp b/unitTestApp/src/RegistryTest.cpp index e43938f6..d769ec9c 100644 --- a/unitTestApp/src/RegistryTest.cpp +++ b/unitTestApp/src/RegistryTest.cpp @@ -14,7 +14,7 @@ #include #include -#include "Registry.h" +#include "OpcuaRegistry.h" namespace { From 15f053fcaf28c16f70e7b08188a2f8f5cbadeb78 Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Fri, 29 May 2026 12:24:03 +0200 Subject: [PATCH 53/64] ci: patch .ci-local/install-open62541.py to support Windows MinGW builds --- .ci-local/install-open62541.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.ci-local/install-open62541.py b/.ci-local/install-open62541.py index 20e948ae..94fb3167 100644 --- a/.ci-local/install-open62541.py +++ b/.ci-local/install-open62541.py @@ -86,14 +86,18 @@ if cue.ci['static']: build_shared = 'OFF' - sp.check_call(['cmake', '..', - '-G', generator, - '-DBUILD_SHARED_LIBS={0}'.format(build_shared), - '-DCMAKE_BUILD_TYPE=RelWithDebInfo', - '-DUA_ENABLE_ENCRYPTION=OPENSSL', - '-DUA_ENABLE_ENCRYPTION_OPENSSL=ON', - '-DCMAKE_INSTALL_PREFIX={0}'.format(installdir)], - cwd=builddir) + cmake_args = ['cmake', '..', + '-G', generator, + '-DBUILD_SHARED_LIBS={0}'.format(build_shared), + '-DCMAKE_BUILD_TYPE=RelWithDebInfo', + '-DUA_ENABLE_ENCRYPTION=OPENSSL', + '-DUA_ENABLE_ENCRYPTION_OPENSSL=ON', + '-DCMAKE_INSTALL_PREFIX={0}'.format(installdir)] + + if cue.ci['os'] == 'windows' and cue.ci['compiler'] == 'gcc': + cmake_args.append('-DCMAKE_C_FLAGS=-Wno-error=jump-misses-init') + + sp.check_call(cmake_args, cwd=builddir) sp.check_call(['cmake', '--build', '.', '--config', 'RelWithDebInfo'], cwd=builddir) sp.check_call(['cmake', '--install', '.', '--config', 'RelWithDebInfo'], cwd=builddir) From 5a06719b2e515ea7a4dd72c0a123d73542af58ce Mon Sep 17 00:00:00 2001 From: Ralph Lange Date: Tue, 16 Jun 2026 16:34:27 +0200 Subject: [PATCH 54/64] doc: add Sphinx-based documentation - Migrate Using-Security.md and READMEs to proper Sphinx - Configure Sphinx with ReadTheDocs theme and MyST parser - Use Markdown with semantic line breaks - Add versionadded markers for key features - Organize content into Tutorials, How-to Guides, Reference and Explanation (Diataxis structure) Co-authored-by: google-labs-jules[bot] --- README.md | 162 +-------- Using-Security.md | 201 ----------- devOpcuaSup/UaSdk/README.md | 149 +-------- devOpcuaSup/open62541/README.md | 222 +----------- ...EPICS Support for OPC UA - Cheat Sheet.pdf | Bin .../EPICS Support for OPC UA - SRS.pdf | Bin documentation/conf.py | 29 ++ documentation/credits.md | 30 ++ documentation/explanation/opcua_concepts.md | 190 +++++++++++ .../explanation/security_concepts.md | 112 +++++++ documentation/how-to/debugging.md | 74 ++++ documentation/how-to/plc_configuration.md | 127 +++++++ .../how-to/record_configuration.md.autosave | 250 ++++++++++++++ .../how-to/record_configuration_scalar.md | 206 ++++++++++++ .../how-to/record_configuration_structure.md | 174 ++++++++++ .../how-to/security_configuration.md | 136 ++++++++ documentation/index.md | 110 ++++++ documentation/license.md | 5 + documentation/reference/cheat_sheet.md | 8 + documentation/reference/iocsh_commands.md | 230 +++++++++++++ documentation/reference/record_types.md | 21 ++ documentation/reference/requirements_spec.md | 7 + documentation/tutorials/build_application.md | 225 +++++++++++++ documentation/tutorials/build_client_lib.md | 31 ++ documentation/tutorials/build_devsup.md | 38 +++ documentation/tutorials/getting_started.md | 50 +++ .../tutorials/option_a_open62541.markdown | 315 ++++++++++++++++++ .../tutorials/option_b_uasdk.markdown | 216 ++++++++++++ documentation/tutorials/run_verify_ioc.md | 33 ++ exampleTop/DeviceDbApp/S7-1500Db/README.md | 4 +- .../DeviceDbApp/UaDemoServerDb/README.md | 4 +- exampleTop/README.md | 14 +- 32 files changed, 2648 insertions(+), 725 deletions(-) delete mode 100644 Using-Security.md rename documentation/{ => _static}/EPICS Support for OPC UA - Cheat Sheet.pdf (100%) rename documentation/{ => _static}/EPICS Support for OPC UA - SRS.pdf (100%) create mode 100644 documentation/conf.py create mode 100644 documentation/credits.md create mode 100644 documentation/explanation/opcua_concepts.md create mode 100644 documentation/explanation/security_concepts.md create mode 100644 documentation/how-to/debugging.md create mode 100644 documentation/how-to/plc_configuration.md create mode 100644 documentation/how-to/record_configuration.md.autosave create mode 100644 documentation/how-to/record_configuration_scalar.md create mode 100644 documentation/how-to/record_configuration_structure.md create mode 100644 documentation/how-to/security_configuration.md create mode 100644 documentation/index.md create mode 100644 documentation/license.md create mode 100644 documentation/reference/cheat_sheet.md create mode 100644 documentation/reference/iocsh_commands.md create mode 100644 documentation/reference/record_types.md create mode 100644 documentation/reference/requirements_spec.md create mode 100644 documentation/tutorials/build_application.md create mode 100644 documentation/tutorials/build_client_lib.md create mode 100644 documentation/tutorials/build_devsup.md create mode 100644 documentation/tutorials/getting_started.md create mode 100644 documentation/tutorials/option_a_open62541.markdown create mode 100644 documentation/tutorials/option_b_uasdk.markdown create mode 100644 documentation/tutorials/run_verify_ioc.md diff --git a/README.md b/README.md index 9fb29a81..6f84bc5b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![open62541][badge.open62541-win]](https://github.com/epics-modules/opcua/actions/workflows/ci-build-open62541-win.yml) [![UA SDK][badge.uasdk-win]](https://github.com/epics-modules/opcua/actions/workflows/ci-build-uasdk-win.yml) -# opcua - EPICS Device Support for OPC UA +# EPICS Device Support for OPC UA [EPICS](https://epics-controls.org) Device Support module for interfacing to the OPC UA protocol. @@ -16,148 +16,22 @@ of the low level client library. Linux and Windows native builds are supported. -## Status - -The module is under development, but releases are stable and robust. - -There are two choices for the low-level OPC UA client library: - -1. The free open source client of the [open62541 project SDK][open62541.sdk]. \ - This integration is relatively new (added 2024) - and is gradually introduced in production at more and more places. - -2. The commercially available - [Unified Automation C++ Based OPC UA Client SDK][unified.sdk]. \ - This is the original implementation. - -## Prerequisites - -* A C++ compiler that supports the C++11 standard. \ - Microsoft Visual C++ needs to be from Visual Studio 2015 or newer. - g++ needs to be 4.6 or above. - -* [EPICS Base](https://epics-controls.org/resources-and-support/base/) - 3.15 (>= 3.15.7) or EPICS 7 (>= 7.0.4). - -* The [gtest module](https://github.com/epics-modules/gtest) if you want - to compile and run the unit tests that are based on Google Test. - -### Using the Unified Automation Client SDK - -* Unified Automation C++ Based [OPC UA Client SDK][unified.sdk] - (1.5/1.6/1.7 are supported, as well as their evaluation bundles; - 1.8 is having trouble). - -* For OPC UA security support (authentication/encryption), you need - openssl/libcrypto on your system - both when compiling the SDK and when - generating any binaries (IOCs). - -* In `CONFIG_SITE.local`, set `UASDK` to the path of your SDK installation. - -* For more details, refer to the `README.md` in the - [`devOpcuaSup/UaSdk`][uasdk.dir] directory. - -### Using the open62541 SDK - -* The [open62541 SDK][open62541.sdk] \ - Choose a recent release from series 1.2 or 1.3 - (release seriens 1.4 is not supported yet). - Download the sources using GitHub releases, not through the project website. - -* For OPC UA security support (authentication/encryption), you need - openssl/libcrypto on your system - both when compiling the SDK and when - generating any binaries (IOCs). - -* In `CONFIG_SITE.local`, set `OPEN62541` to the path of your SDK installation. - -* For more details, refer to the `README.md` in the - [`devOpcuaSup/open62541`][open62541.dir] directory. - -## Building the module - -This is a standard EPICS module. - -Inside the `configure` subdirectory or one level above the TOP location -(TOP is where this README file resides), create a file `RELEASE.local` -that sets `EPICS_BASE` and `GTEST` to the absolute paths inside your EPICS -installation. The `GTEST` module is needed to compile and run the tests. -Not defining it produces a clean build, but without any tests. - -Configure the compiler on Linux to use the C++11 standard by adding - -```makefile -USR_CXXFLAGS_Linux += -std=c++11 -``` - -to the `CONFIG_SITE` file (or one of the host/target specific site -configuration files). \ -It is preferable to set this option globally in EPICS Base. - -The configuration necessary when building against a specific client library -is documented in the `README.md` file inside the respective subdirectory of -`devOpcuaSup`. - -## Using the module - -IOC applications that use the module need to - -* add an entry to the Device Support module in their `RELEASE.local` file -* include `opcua.dbd` when building the IOC's DBD file -* include `opcua` in the support libraries for the IOC binary. - -## Documentation - -Sparse, but getting better. - -The documentation folder of the Device Support module contains the -[Requirements Specification (SRS)][requirements.pdf] giving an introduction -and the list of requirements that should convey a good idea of the planned -features. - -The [Cheat Sheet][cheatsheet.pdf] explains the configuration in the startup -script and the database links. - -## Binary Distribution - -Please look at the "Assets" sections of specific releases -on the [release page](https://github.com/epics-modules/opcua/releases) -for the binary distribution tars. -These tars contain an EPICS module -with a binary Linux shared library (`libopcua.so.`) -that contains (embeds) the Unified Automation low-level client. - -In your build setup, the module from the binary distribution -can be used like a support module built from source. -The binary device support is fully functional -and can be used without limitations or any fees. - -You need to download the binary distribution tar that matches -your Linux distribution and the exact EPICS Base version, -else you will not be able to create IOCs. - -## Feedback / Reporting issues - -Please use the GitHub project's -[issue tracker](https://github.com/epics-modules/opcua/issues). - -## Credits - -This module is based on extensive -[prototype work](https://github.com/bkuner/opcUaUnifiedAutomation) -by Bernhard Kuner (HZB/BESSY) and uses ideas and code snippets from -Michael Davidsaver (Osprey DCS). - -Support for the open62541 client library -and many fixes and new features were added by Dirk Zimoch (PSI) -with additional help from Carsten Winkler (HZB/BESSY). - -The end-to-end test suite is a reduced clone of the test application -that has been developed at the ESS by Ross Elliot and Karl Vestin. +- [**Documentation**](https://epics-modules.github.io/opcua) - + the things you should know +- [Source Code](https://github.com/epics-modules/opcua) - + everything you never wanted to know +- [OPC UA Room on EPICS Chat](https://matrix.to/#/#opcua:epics-controls.org) - + quick help and discussions +- [Usage / FAQ (Wiki)](https://github.com/epics-modules/opcua/wiki) - + hints & experiences with specific servers +- [Issue Tracker](https://github.com/epics-modules/opcua/issues>) - + when things don't work ## License -This module is distributed subject to a Software License Agreement found -in the file `LICENSE` that is included with this distribution. +This module is distributed subject to a Software License Agreement +found in the file `LICENSE` +that is included with this distribution and shown above. [badge.version]: https://img.shields.io/github/v/release/epics-modules/opcua?sort=semver @@ -167,11 +41,3 @@ in the file `LICENSE` that is included with this distribution. [badge.uasdk-linux]: https://github.com/epics-modules/opcua/actions/workflows/ci-build-uasdk-linux.yml/badge.svg [badge.open62541-win]: https://github.com/epics-modules/opcua/actions/workflows/ci-build-open62541-win.yml/badge.svg [badge.open62541-linux]: https://github.com/epics-modules/opcua/actions/workflows/ci-build-open62541-linux.yml/badge.svg - -[unified.sdk]: https://www.unified-automation.com/products/client-sdk/c-ua-client-sdk.html -[open62541.sdk]: https://open62541.org/ - -[uasdk.dir]: https://github.com/epics-modules/opcua/tree/master/devOpcuaSup/UaSdk -[open62541.dir]: https://github.com/epics-modules/opcua/tree/master/devOpcuaSup/open62541 -[requirements.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20SRS.pdf -[cheatsheet.pdf]: https://docs.google.com/viewer?url=https://raw.githubusercontent.com/epics-modules/opcua/master/documentation/EPICS%20Support%20for%20OPC%20UA%20-%20Cheat%20Sheet.pdf diff --git a/Using-Security.md b/Using-Security.md deleted file mode 100644 index 21287816..00000000 --- a/Using-Security.md +++ /dev/null @@ -1,201 +0,0 @@ -# Using OPC UA Security - -## Concepts - -The security features of OPC UA are based on the widely used X.509 certificates and the concepts of *Public Key Infrastructure* (PKI). - -Certificates are filed in a *Certificate Store*, containing trusted and own certificates as well as certificates from certificate authorities (CAs). On Linux, a file based store is used, defined by four specific locations for certificates and revocation lists. - -In the basic, explicit form, trusting a certificate usually means that the certificate (file) is moved into a specific folder in the certificate store. - -When using certificate authorities (CAs), a certificate is trusted if it is signed by a trusted CA. This approach is preferable for a larger installation, as it does not require updating all servers and clients when new peers with new certificates are added. - -A good analogy to certificates are passports: In a world with only a handful people, fabricating your passport yourself is reasonable. In reality, with millions of passport holders, it helps a lot that passports are fabricated by government agencies and this fact can be verified by checking holograms, watermarks etc. - -### Self-signed Certificates - -Servers often come with preloaded self-signed server certificates and a setting that will generously trust any valid self-signed client certificate. Generic clients typically offer an easy way to create a self-signed client certificate. - -These settings are intended to get users started with security easily and create minimal frustration during development. These "simple" self-signed certificates are good enough for securing the communication, but they are not very practical for authentication: every self-signed certificate must be trusted explicitly - or be accepted by default. - -### Communication Security - -Each OPC UA application (server and client) needs an *Application Instance Certificate* and a related public/private key pair to identify itself to its communication peers. The public key is contained in the certificate; the private key is secret and used for signing and encryption of messages. - -For a secure communication to be established, both peers must trust the certificate of the other. - -OPC UA defines three *Message Security Modes*: - -| Message Security Mode | | -| --------------------- | ------------------------------------------- | -| None | No security is applied. | -| Sign | All messages are signed, but not encrypted. | -| SignAndEncrypt | All messages are signed and encrypted. | - -As security algorithms are getting obsolete, an increasing number of *Security Policies* (sets of security algorithms and key lengths) are defined, by their unique URIs: - -| Security Policy | URI | -| -------------------------- | ------------------------------------------------------------ | -| None | http://opcfoundation.org/UA/SecurityPolicy#None | -| Basic128Rsa15 *(obsolete)* | http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15 | -| Basic256 | http://opcfoundation.org/UA/SecurityPolicy#Basic256 | -| Basic256Sha256 | http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 | -| Aes128_Sha256_RsaOaep | http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep | -| Aes256_Sha256_RsaPss | http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss | - -### Client Authentication (Identity) - -For client authentication related to managing the authorization of access to a server, OPC UA defines four *User Token Types* (methods of user authentication): - -| User Token Type | | -| ------------------------ | ------------------------------------------ | -| Anonymous Identity Token | No user information available. | -| Username Identity Token | User identified by name and password. | -| X.509 Identity Token | User identified by an X.509v3 certificate. | -| Issued Identity Token | User identified by a WS-SecurityToken. | - -### Endpoint Discovery - -In order to establish a secure connection, the client has to find out which security features the server provides. This process is called *Discovery*. - -In its answer to a discovery request, a server provides a list of *Endpoints* for the client to connect to, with different sets of security features. - -As a debugging tool, the iocShell command `opcuaShowSecurity` runs the discovery service and displays the available endpoints with their features and their *security level* (used by the IOC to find the endpoint providing the best security). - -## IOC Configuration - -An IOC using the OPC UA Device Support is considered to be a production system. As such, it has to be fully configured and supplied with the certificates needed to connect to the configured OPC UA servers. Other than general purpose and example clients, the IOC cannot and should not interactively create self-signed certificates or change the trust for a certificate that a server presents. - -To support setting up the certificate store, the `opcuaSaveRejected` iocShell command will configure the IOC to save rejected (untrusted) server certificates in a specified location (default: `/tmp/@`), using DER format. Copying such a certificate file to the IOC's certificate store (under `trusted/certs`) will explicitly trust the certificate and allow secure connections to the server. - -When using the UA SDK low level client, the trusted server and CA certificates (under `trusted/certs`) must use DER format. ([See below](#managing-certificates) for how to convert between formats.) - -Alternatively, you can use a general purpose client (e.g., the `UaExpert` tool) to connect to the server, trust its certificate, and use the certificate file from that client's certificate store. (You could also import it into your certificate management tool, [see below]((#managing-certificates)).) - -*Important Note:* During the process of endpoint discovery and saving the untrusted server certificate, a man-in-the-middle could send its own certificate to the IOC. Only add a server certificate to the trusted certificate store if you are sure it actually originates from the server (e.g., on a trusted network, after verifying its thumb print). This act of trusting is the crucial and weakest point when using self-signed certificates. - -### Certificate Store Setup - -The iocShell command `opcuaSetupPKI` sets the location(s) for the certificate store. The IOC needs to have read access to all files in the PKI store, further access should not be granted as this might compromise security. - -In its simple form, the single argument defines the location of the PKI certificate store, using default subdirectories for trusted peer and CA certificates (`trusted/certs`), CA certificate revocation lists (`trusted/crl`), intermediate issuer certificates (`issuers/certs`) and their certificate revocation lists (`issuers/crl`). - -In the fully detailed form (using four arguments), the four locations are specified separately. - -### Client Certificate - -The iocShell command `opcuaClientCertificate` sets the locations for the client certificate (PEM or DER format) and the matching private key (PEM format). - -### Session Security Setting - -Two security-related session options are used to configure the security features for a given OPC UA session, by calling `opcuaSetOption` in the iocShell. - -Setting `sec-mode` selects the specified message security mode for the connection. The special keyword "best" (default) will have the IOC choose the best mode, based on the server-supplied *security level*. Setting `sec-policy` (to the short name, not the full URI) selects a specific policy. If multiple endpoints match the option settings, the IOC will always choose the best available security. - -*Note:* To connect without security, you have to explicitly set `sec-mode` to `None`. - -If no matching endpoint is discovered or the server certificate is untrusted, the IOC will not connect. - -### Identity (Client Authentication) - -Without configuration, an Anonymous Identity Token will be used. - -To use a Username Identity or a Certificate Identity Token, prepare an identity file. This file should only be readable by the IOC. Configure the filename through the session option `sec-id`. - -In the identity file, lines starting with `#` are considered comments and ignored. - -To use a Username Identity Token, set `user=` and `pass=`, each on a separate line. - -To use a Certificate Identity Token, set `cert=` and `key=`, each on a separate line, to identify the DER or PEM certificate and PEM private key to use. If the private key is password protected, add another line setting `pass=`. Normally, the server will use the Common Name (CN) property of the certificate to determine the user name for authorization. - -## Managing Certificates - -The underlying OpenSSL library provides a command line utility. - -For creating and managing a larger number of certificates, -[Xca](https://hohnstaedt.de/xca/) is a powerful and popular GUI -for X.509 certificate and key handling, -including certificate authority (CA) functionality, -which is the most efficient way -to manage the certificates for a larger installation. - -The `openssl` command line utility can be used to convert certificates (and keys) between formats. To convert certificate `` from DER to PEM format, use: - -```bash -openssl x509 -inform der -in .der -out .pem -``` - -To convert in the other direction, use: - -```bash -openssl x509 -in .pem -outform der -out .der -``` - -The UA SDK client and the underlying OpenSSL library are picky in terms of file name extensions. DER format requires certificates to carry a `.der` extension and revocation lists to be named `.crl`, while PEM format works only if files have a `.pem` extension. Otherwise you may experience unspecific `Bad` status errors. - -### Creating Application Instance Certificates - -Creating a self-signed certificate for OPC UA use is pretty straight-forward. Follow the documented procedure, giving your certificate/key pair the following properties: - -- Choose the issuer information to match your situation. - -- Sign using `SHA 256` (i.e., `sha256WithRSAEncryption`). - -- X509v3 Basic Constraints: **critical**, `CA:FALSE`. - -- X509v3 Key Usage: **critical**, `Digital Signature`, `Non Repudiation`, `Key Encipherment`, `Data Encipherment`, `Certificate Sign`. - -- X509v3 Extended Key Usage: **critical**, `TLS Web Server Authentication`, `TLS Web Client Authentication`. - -- X509v3 Subject Alternative Name: `URI:urn:@:EPICS:IOC`, `DNS:` - with `` being the IOC name,`` being the hostname (i.e., the result of a `gethostname()` call) of the machine that runs the IOC. The URI tag *must* match what the Device Support module sets as its application URI. - Alternatively, a numerical `IP Address` tag can be used instead of `DNS`. - -The source tree contains an Xca certificate template that can be imported and modified to fit your needs, which will simplify generating new IOC client certificates. - -For the IOC, save the certificate in DER or PEM format, the private key as PEM. The server may need different formats - refer to the documentation of your server for more details. - -A simple client certificate/key pair can also be created using the `openssl` command line utility, e.g.: - -```bash -openssl req -x509 -newkey rsa:2048 -keyout private_key.pem -out cert.pem -sha256 -days 365 -nodes -addext "subjectAltName=URI:urn:@:EPICS:IOC,IP:" -``` - -### Certificates and Network / DNS Setup - -The `URI:`, `DNS:` and `IP:` entries in the Subject Alternative Name section require the network and DNS to be set up correctly, otherwise the certificates will not work. - -This applies to both the IOC and the server. If any of the two uses a host name in its certificate that doesn't match the host name that the other side gets when doing a reverse lookup, the connection is likely to fail. - -The IOC uses the `gethostname()` result in the `URI:` entry, which might differ from its DNS host name that has to appear in the `DNS:` entry. Depending on your DNS setup, the host names in the `DNS:` entry need to be simple or fully qualified. Finding out the right way to set up your certificates may be frustrating and time consuming. - -### Creating Identity Token Certificates - -Create a self-signed certificate, following the documented procedure and giving your certificate/key pair the following properties: - -- Choose the issuer information to match your situation. - The Common Name (CN) property of the certificate will normally be used by the server to determine the user name for authorization. - -- Sign using `SHA 256` (i.e., `sha256WithRSAEncryption`). - -- X509v3 Basic Constraints: **critical**, `CA:FALSE`. - -*Note:* The Unified Automation example server accepts any trusted certificate, without further verification and without using certificate properties for authorization. - -### Using a Certificate Authority - -A complete discussion of this concept and the design and implementation of an appropriate public key infrastructure for a specific installation is outside the scope of this README. The [Xca online documentation](https://hohnstaedt.de/xca/index.php/documentation/stepbystep) has more details and step-by-step guides. - -The basic steps are: - -- Create a Certificate Authority. - In the simplest form this means creating a Root CA certificate/key that is configured to sign other certificates. - The CA certificate needs to be placed in DER format inside the `trusted/certs` directory of the IOC's PKI store. - -- Create a Certificate Revocation List. - Even if you do not plan to use the Certificate Revocation List (CRL) mechanism to deal with invalidation of certificates, you need to create an empty CRL for your CA certificate and place it in DER format inside the `trusted/crl` directory of the IOC's PKI store. Otherwise your CA certificate will not work. - -- Create Application Instance and Identity Token Certificates signed by your CA. - When creating new certificates for your servers and clients, instead of selecting "Create a self-signed certificate" under the "Source" tab, choose a different certificate (your CA certificate) that will be used to sign the newly created certificate. - -Using Xca templates can simplify certificate creation and greatly improve consistency. diff --git a/devOpcuaSup/UaSdk/README.md b/devOpcuaSup/UaSdk/README.md index 25984d79..9b72baa0 100644 --- a/devOpcuaSup/UaSdk/README.md +++ b/devOpcuaSup/UaSdk/README.md @@ -1,145 +1,6 @@ -# Unified Automation C++ Based OPC UA Client SDK +# Using the Unified Automation C++ Based OPC UA Client SDK -## Status - -:warning: -This module is stable, -with some expected development still to be happening. - -:warning: -Please contact the authors -[Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and -[Ralph Lange](mailto:ralph.lange@gmx.de) for details. - -## Prerequisites - -* Unified Automation C++ Based [OPC UA Client SDK][unified.sdk] - (1.5/1.6/1.7 are supported, as well as their evaluation bundles). - -* Windows and Linux builds are supported. - -* Using the evaluation binary bundles by Unified Automation (free download) is supported. - However, the EVAL bundle contains a shared library (DLL), namely the stack component, see below, so only shared builds of this Device Support will work. - -### Linux - -* For OPC UA security support (authentication/encryption), you need `libcrypto` on your system - both when compiling the SDK and when generating any binaries (IOCs). - The name of the package you have to install depends on the Linux distribution: `openssl-devel` on RedHat/CentOS/Fedora, `libssl-dev` on Debian/Ubuntu. - Use the `CONFIG_SITE.local` file in the module where the binary is created to set this option. - -* The OPC UA Client SDK sets `BUILD_SHARED_LIBS=OFF` as default. - To create shared SDK libraries, build the SDK using - - ```Shell - ./buildSdk.sh -s ON - ``` - In many versions of the SDK, the `buildSdk.sh` build script does not apply the `-s ON` setting the the stack component. To fix this and create a complete shared library set of the SDK, apply the following patch (shown for 1.5.5): - - ```Diff - --- buildSdk.sh - +++ buildSdk.sh - @@ -95,7 +95,7 @@ - cd $UASDKDIR/build$config || { echo "cd $UASDKDIR/build$config failed."; exit 1; } - # create the Makefile using CMake - # Just create only the SDK Makefiles - - cmake "$TOOLCHAIN" "$OPTION" -DBUILD_EXAMPLES=off -DBUILD_UACLIENTCPP_APP=off -DBUILD_UASERVERCPP_APP=off -DENABLE_GCC_FORTIFY_SOURCE=off -DCMAKE_BUILD_TYPE=$config -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS -DCMAKE_INSTALL_PREFIX=$CMAKE_INSTALL_PREFIX $UASDKDIR - + cmake "$TOOLCHAIN" "$OPTION" -DBUILD_EXAMPLES=off -DBUILD_UACLIENTCPP_APP=off -DBUILD_UASERVERCPP_APP=off -DENABLE_GCC_FORTIFY_SOURCE=off -DCMAKE_BUILD_TYPE=$config -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS -DBUILD_SHARED_STACK=$BUILD_SHARED_LIBS -DCMAKE_INSTALL_PREFIX=$CMAKE_INSTALL_PREFIX $UASDKDIR - # build - make "$JOBS" || { echo "make failed."; exit 1; } - # install - ``` - -### Windows - -* Binary libraries (DLLs and headers) of matching versions for openssl and libxml2 are provided under the `third-party` directory of the SDK bundle. - -## Building the device support module - -Inside the `configure` subdirectory or one level above the TOP location, create a file `RELEASE.local` that sets `EPICS_BASE` to the absolute path of your EPICS installation. - -Inside the `configure` subdirectory or one level above the TOP location, create a file `CONFIG_SITE.local` that sets the absolute path of your SDK installation as well as the SDK build and deploy features if necessary. - -```Makefile -# Path to the Unified Automation OPC UA C++ SDK -UASDK = /usr/local/opcua/uasdkcppclient-v1.5.3-346/sdk - -# How the Unified Automation SDK shared libraries are deployed -# SYSTEM = shared libs are in a system location -# PROVIDED = shared libs are in $(UASDK_SHRLIB_DIR) -# INSTALL = shared libs are installed (copied) into this module -# EMBED = link SDK code statically into libopcua, -# SDK libraries are not required on target system -UASDK_DEPLOY_MODE = PROVIDED -UASDK_LIB_DIR = $(UASDK)/lib -UASDK_SHRLIB_DIR = $(UASDK_LIB_DIR) -# How the Unified Automation SDK libraries were built -UASDK_USE_CRYPTO = YES -UASDK_USE_XMLPARSER = YES -``` - -The SDK related configuration only has to be done in this module, which creates a `CONFIG_OPCUA` file that is automatically included by all downstream modules, so that the configuration is always consistent. - -When using the evaluation bundle of the SDK, only regular (shared) builds are supported. -Do not set `STATIC_BUILD = YES` in your configuration. -Also, the evaluation bundle only contains binary libraries to be used with the MSVC compilers. MinGW can not be used with the libraries from the bundle (C++ name mangling is different.) - -### Windows Specifics - -Windows builds should use `UASDK_DEPLOY_MODE = PROVIDED` and set `UASDK` to the location of the Unified Automation bundle. - -All paths in EPICS build configuration files must use Windows "short names" where needed, e.g. -```Makefile -UASDK = C:/PROGRA~2/UnifiedAutomation/UaSdkCppBundleEval -``` - -In your `CONFIG_SITE.local`, you also need to define the locations of the supplied third-party dependencies. - -```makefile -# Prerequisites: libxml2 openssl -LIBXML2 = $(UASDK)/third-party/win64/vs2015/libxml2 -OPENSSL = $(UASDK)/third-party/win64/vs2015/openssl -``` - -Different versions of the Unified Automation bundle have these libraries in different folders, under different names. Check `configure/CONFIG_SITE.Common.win32-x86` and adapt the settings to your situation. - -If you do not have Administrator rights or if you want to unpack multiple versions of the UA SDK bundle in parallel, [UniExtract2][uniextract2] is your friend. - -*Note:* -As always on Windows, the build environment as well as the runtime environment (for any IOC) need to have the locations of all DLLs in the PATH to be able to run binaries (which is part of the build for the unit tests). You will need to add the appropriate locations for EPICS Base, the opcua module, the UASDK library and all other dependencies you installed. -Alternatively, you can put copies of all needed DLLs into the location of the binaries. (Remember that `make distclean` will remove those copies.) - -#### DLL Hell - -Be aware that over time, Windows systems will have a large number of possibly different DLLs with the same names and/or versions being used concurrently on the system - a situation that is hard to maintain and known as [DLL Hell](https://en.wikipedia.org/wiki/DLL_Hell). - -If you want your IOC binaries to be deployable without depending on specific DLLs being present on the target system, consider linking your IOCs statically. (As stated above, static builds are not available when -using the evaluation bundles.) - -On newer Windows systems, the Windows system version of the openssl DLLs are not compatible with the UA SDK bundled versions (see below). The unit tests Makefile copies the vendor-supplied DLLs next to the test binaries to avoid a mismatch. - -## Feedback / Reporting issues - -Please use the GitHub project's [issue tracker](https://github.com/epics-modules/opcua/issues). - -## Known issues - -Reported by Shi Li (ASIPP): -The C++ SDK version 1.7.1 has a bug that shows as monitored items not getting updates anymore after the server has disconnected and reconnected. A reboot of the IOC restores good behaviour until the server disconnects again. Updating the C++ SDK to version 1.7.2 resolves the issue. - -Reported by Carsten Winkler (HZB/BESSY): -On Windows 10, you may experience a version mismatch between the openssl DLLs that are part of the Windows system and a different version of the same DLLs that are part of the UA SDK bundle. -In that case, copying the DLLs from the UA SDK into the same directory as the unit tests (`...\unitTestApp\src\O.win...`) and your executable (IOC binary) seems to be the only reasonable workaround. - -## Credits - -This module is based on extensive [prototype work](https://github.com/bkuner/opcUaUnifiedAutomation) by Bernhard Kuner (HZB/BESSY) and uses ideas and code snippets from Michael Davidsaver (Osprey DCS). -Support for Windows builds with help from Carsten Winkler (HZB/BESSY). - -## License - -This module is distributed subject to a Software License Agreement found in file LICENSE that is included with this distribution. - - - -[unified.sdk]: https://www.unified-automation.com/products/client-sdk/c-ua-client-sdk.html -[uniextract2]: https://github.com/Bioruebe/UniExtract2 +Please refer to the +[**documentation**](https://ralphlange.github.io/opcua/tutorials/20_build_client_lib.html#option-b-unified-automation-c-client-sdk) +for instructions how to build the module +using the Unified Automation SDK low-level client. diff --git a/devOpcuaSup/open62541/README.md b/devOpcuaSup/open62541/README.md index 62a2464f..10a5b247 100644 --- a/devOpcuaSup/open62541/README.md +++ b/devOpcuaSup/open62541/README.md @@ -1,218 +1,6 @@ -# Open62541 Client C Library +# Using the open62541 Client C Library -## Status - -:warning: -This module is relatively new (2014), -but gaining popularity -and getting more and more hours of production use. - -:warning: -Please contact the authors -[Dirk Zimoch](mailto:dirk.zimoch@psi.ch) and -[Ralph Lange](mailto:ralph.lange@gmx.de) for details. - -## Prerequisites - -* [Open62541](https://www.open62541.org/) SDK / C library - (v1.2 and v1.3 series have been tested; their system packages should also work. - open62541 v1.4 changed the header file structure and does not work yet.) - -* Cmake (3.x) if you're building Open62541 from sources. - -* For OPC UA security support (authentication/encryption), we suggest using the openssl plugin for Open62541. For that to work, you need openssl on your system - both when compiling the client library and when generating any binaries (IOCs). - On Linux, the name of the package you have to install depends on the distribution: development packages are called `openssl-devel` on RedHat/CentOS/Fedora and `libssl-dev` on Debian/Ubuntu, runtime packages are called `openssl` on RedHat/CentOS/Fedora and `libssl` on Debian/Ubuntu, respectively. - See below for Windows instructions. - In your `CONFIG_SITE.local` file (see below), enable the OPC UA Security option. - -## Building Open62541 - -Note: -The Open62541 project is focused on the server implementation of OPC UA. -The client functionality is fully supported, complete and usable, -but it does not get the attention that the server parts get. - -Do *not* use the download link on the open62541 web site. -Use their GitHub Release Page instead. - -### On Linux - -* Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . - -* The cmake build of Open62541 creates a static library by default. This type of library is needed for the EMBED type of Device Support build (see below). - - To create shared libraries, build the Open62541 library setting the cmake option `BUILD_SHARED_LIBS=ON`. - - ```Shell - cmake .. -DBUILD_SHARED_LIBS=ON [...] - ``` - - You can create both types by running two builds with different configurations in the same build directory. Remove the CMakeCache in between to get clean configurations. - -* Open62541 supports multiple low-level libraries to implement OPC UA Security. The most widely used option (that also keeps things simple by staying in line with the UA SDK client) is based on openssl. We suggest to select that by setting the cmake option `UA_ENABLE_ENCRYPTION=OPENSSL`. - -* Given all the above, for a Linux build, a reasonable option setting would be: - - ```shell - cmake .. -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - - to create a shared library, and - - ```shell - cmake .. -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - - to create the static variant. - - There are a lot of other options available, but most of them affect only the server part and are not relevant for use of the client. - - For Open62541 version 1.2, set `UA_ENABLE_ENCRYPTION_OPENSSL=ON` instead of the OPENSSL setting shown above. You may also have to explicitly set the Open62541 version string in file `CMakeLists.txt`, as the auto-detection doesn't work when you're not working under git. - -* The default installation location is below `/usr/local`. Normally, this is a system location, so that the deploy mode setting (see below) would be `SYSTEM`. - If you want to install into another location, set the option `CMAKE_INSTALL_PREFIX=/other/location`. In that case, the deploy mode setting (see below) would be `PROVIDED` and the library location needs to be set. - The same is true - if you want to use the Open62541 library - in its build location (not recommended). - The library location (set in `CONFIG_SITE.local`) - will be `$(OPEN62541)/build/bin` or `$(OPEN62541)/build/bin/Release` - or something different, - depending on your installation, OS and choice of build directory name. - -* After running `cmake`, run `make` followed by `make install`. Installation into a system location will need root permission. - -### On Windows - -See the [Windows Installation How-To][windows-howto] for an overview and introduction to the different compilation options under Windows. - -#### Using MSYS2/MinGW - -* Install the necessary tools and dependencies using the `pacman` system package manager. You will need `mingw-w64-x86_64-cmake`, `mingw-w64-x86_64-libxml2` and `mingw-w64-x86_64-openssl`. - -* Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . - -* A reasonable option setting for running `cmake` would look similar to the Linux variant: - - ```shell - cmake .. -G "MinGW Makefiles" \ - -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - -* Run `mingw32-make` followed by `mingw32-make install`. - -#### Using MSVC - -* Get the necessary prerequisites and install them: - Python version 3 can be found [here][python]. - `libml2` and `iconv` binaries for Windows can be found [here][libxml2-mhils] or [here][libxml2-xmlsoft]. Install them in an appropriate location. - You might have to rename `.dll.a` DLL export stubs to `.lib` to work properly with the EPICS build system. - `openssl` can be found [here][openssl.slpro]. The installer makes sure that MSVC finds it (without explicit configuration). - -* Unpack the open62541 distribution. Create a build directory on the top level and `cd` into it. We'll use the usual convention of calling it `build` . - -* A reasonable option setting for running `cmake` would look like this: - - ```shell - cmake .. -DBUILD_SHARED_LIBS=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DUA_ENABLE_ENCRYPTION=OPENSSL - ``` - - plus `-DCMAKE_INSTALL_PREFIX=/other/location` if you want to select where things are installed. - - The cmake configuration seems to have a bug (seen on 1.3.6) that always selects `Release` for the path of the library to install. The easiest fix is to replace that string with the appropriate path (`Debug`) in two places in the block between lines 35 and 55 of the file `cmake_install.cmake`. - -* run `cmake --build .` followed by `cmake --install .` to build and install the library. - -## Building the device support module - -Inside the `configure` subdirectory or one level above the TOP location, create a file `RELEASE.local` that sets `EPICS_BASE` to the absolute path of your EPICS installation. - -Inside the `configure` subdirectory or one level above the TOP location, create a file `CONFIG_SITE.local` that sets the absolute path of your Open62541 installation as well as its build and deploy features if necessary. -You also need to configure the locations of the other dependencies that you installed. - -```Makefile -# Path to the Open62541 C++ installation -OPEN62541 = /other/location - -# How the Open62541 shared libraries are deployed -# SYSTEM = shared libs are in a system location -# PROVIDED = shared libs are in $(OPEN62541_SHRLIB_DIR) -# INSTALL = shared libs are installed (copied) into this module -# EMBED = link Open62541 code statically into libopcua, -# the Open62541 libraries are not required on target system -OPEN62541_DEPLOY_MODE = PROVIDED -OPEN62541_LIB_DIR = $(OPEN62541)/lib -OPEN62541_SHRLIB_DIR = $(OPEN62541_LIB_DIR) -# How the Open62541 libraries were built -OPEN62541_USE_CRYPTO = YES -``` - -The Open62541 related configuration only has to be done in this module, which creates a `CONFIG_OPCUA` file that is automatically included by all downstream modules (those creating IOCs), so that the configuration is always consistent. - -### On Windows - -All paths in EPICS build configuration files must use Windows "short names" where needed, e.g. - -```Makefile -OPEN62541 = C:/PROGRA~2/open62541-1.3.6-MinGW/ -``` - -If you're using MSYS2/MinGW64, you need to set the location of the MinGW64 installation in a CONFIG_SITE file, e.g. - -```makefile -# Location of MSYS2/MinGW64 -MSYSTEM_PREFIX = C:/msys64/mingw64 -``` - -When using MSVC you need to add the settings for the dependencies that you installed in non-system locations: - -```makefile -# Prerequisites: libxml2 iconv -LIBXML2 = C:/dependency/location/libxml2-2.9.3 -ICONV = C:/dependency/location/iconv-1.14 -``` - -*Note:* -As always on Windows, the build environment as well as the runtime environment (for any IOC) need to have the locations of all DLLs in the PATH to be able to run binaries (which is part of the build for the unit tests). You will need to add the appropriate locations for EPICS Base, the opcua module, the open62541 library and all other dependencies you installed. -Alternatively, you can put copies of all needed DLLs into the location of the binaries. (Remember that `make distclean` will remove those copies.) - -#### DLL Hell - -Be aware that over time, Windows systems will have a large number of possibly different DLLs with the same names and/or versions being used concurrently on the system - a situation that is hard to maintain and known as [DLL Hell](https://en.wikipedia.org/wiki/DLL_Hell). - -If you want your IOC binaries to be deployable -without depending on specific DLLs being present on the target system, -consider linking your IOCs statically. - -## Feedback / Reporting issues - -Please use the GitHub project's [issue tracker](https://github.com/epics-modules/opcua/issues). - -## Known issues - -n/a - -## Credits - -The adaption of the Open62541 client library has been done by Dirk Zimoch (PSI) with help from Carsten Winkler (HZB/BESSY II). Additional EPICS build system magic by Ralph Lange (ITER). - -## License - -This module is distributed subject to a Software License Agreement found -in file LICENSE that is included with this distribution. - - - -[windows-howto]: https://docs.epics-controls.org/projects/how-tos/en/latest/getting-started/installation-windows.html -[python]: https://www.python.org/downloads/windows/ -[libxml2-mhils]: https://github.com/mhils/libxml2-win-binaries/releases -[libxml2-xmlsoft]: http://xmlsoft.org/sources/win32/64bit/ -[openssl.slpro]: https://slproweb.com/products/Win32OpenSSL.html +Please refer to the +[**documentation**](https://epics-modules.github.io/opcua/tutorials/20_build_client_lib.html#option-a-open62541-client-sdk) +for instructions how to build the module +using the open62541 low-level client. diff --git a/documentation/EPICS Support for OPC UA - Cheat Sheet.pdf b/documentation/_static/EPICS Support for OPC UA - Cheat Sheet.pdf similarity index 100% rename from documentation/EPICS Support for OPC UA - Cheat Sheet.pdf rename to documentation/_static/EPICS Support for OPC UA - Cheat Sheet.pdf diff --git a/documentation/EPICS Support for OPC UA - SRS.pdf b/documentation/_static/EPICS Support for OPC UA - SRS.pdf similarity index 100% rename from documentation/EPICS Support for OPC UA - SRS.pdf rename to documentation/_static/EPICS Support for OPC UA - SRS.pdf diff --git a/documentation/conf.py b/documentation/conf.py new file mode 100644 index 00000000..da9a8124 --- /dev/null +++ b/documentation/conf.py @@ -0,0 +1,29 @@ +# Configuration file for the Sphinx documentation builder. + +project = 'EPICS OPC UA Device Support' +copyright = '2025, Ralph Lange' +author = 'Ralph Lange' + +# -- General configuration --------------------------------------------------- + +extensions = [ + 'myst_parser', + 'sphinx.ext.napoleon', + 'sphinx_rtd_theme', +] + +templates_path = ['_templates'] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# -- MyST Parser configuration ----------------------------------------------- + +myst_enable_extensions = [ + "colon_fence", + "deflist", + "substitution", +] diff --git a/documentation/credits.md b/documentation/credits.md new file mode 100644 index 00000000..fb06512e --- /dev/null +++ b/documentation/credits.md @@ -0,0 +1,30 @@ +# Credits + +## Authors + +__Bernhard Kuner__ +evaluated the available OPC UA client libraries +and implemented a Device Support prototype +based on the Unified Automation client library example code. + +__Ralph Lange__ +added support for the Unified Automation SDK +and is a main author of the OPC UA Device Support implementation. + +__Dirk Zimoch__ +added support for the open62541 SDK client library +and is a main author of the OPC UA Device Support implementation. + +__Carsten Winkler__ +contributed to the open62541 support and helped with the Windows builds. + +__Karl Vestin__ +and __Ross Elliot__ (ESS) +developed the end-to-end test suite. + +## Documentation + +__Mohor Gartner__ +and __Mikel Rojo__ +wrote the OPC UA User Manual for ITER's CODAC Core System, +which was used as a base for parts of this documentation. diff --git a/documentation/explanation/opcua_concepts.md b/documentation/explanation/opcua_concepts.md new file mode 100644 index 00000000..0190d735 --- /dev/null +++ b/documentation/explanation/opcua_concepts.md @@ -0,0 +1,190 @@ +# Basics, Terms and Concepts + +## Basics + +### What is EPICS? + +[https://docs.epics-controls.org/en/latest/guides/EPICS_Intro.html][epics-intro] + +[https://epics-controls.org/][epics-web] + +### What is OPC UA? + +OPC Unified Architecture (OPC UA) is a cross-platform, +open-source, +IEC62541 standard for data exchange from sensors to cloud applications +developed by the [OPC Foundation][opc-foundation]. + +The OPC UA protocol has become increasingly popular +with vendors of controllers (like PLCs) and sensors +as a very powerful integration tool. +It allows connecting almost any device to almost any SCADA client, +across vendors, with a certification mechanism that ensures compatibility. + +OPC UA integrates all functionality of the individual specifications +of the former OPC Classic protocol into one extensible framework, +providing: + +* _Functional equivalence_: + all COM OPC Classic specifications are mapped to UA + +* _Platform independence_: + from embedded to cloud-based infrastructure + +* _Security_: + encryption, authentication, and auditing + +* _Extensibility_: + ability to add new features without affecting existing applications + +* _Information modelling_: + for defining complex information + +OPC UA is designed for exchanging information in an object-oriented manner, +rather than as isolated data points. +This increases the accessibility of plant floor data +by allowing to re-use information stored in a common object. + +The [Wikipedia page on OPC UA][opcua-wiki] has a good overview +of its characteristics, history and specification. + +The complete [OPC UA specification][opcua-spec] is freely available +on the OPC Foundation web site. + +## OPC UA Terms and Concepts + +### Node + +A _Node_ is a uniquely identified piece of information on the server. +The OPC information model is a mesh network based on nodes. +These nodes are objects holding process data or any kind of meta information, +such as attributes, references, methods, types, ... + +### NodeID + +A _NodeID_ is an identifier for a node, e.g., for a particular OPC UA item. +A Node ID consists of: +* a namespace index number +* an identifier type +* an identifier + +The identifier can be either a string identifier, consisting of a name string, +or a numerical identifier, consisting of an integer. +Whenever a client requests to either read or write data to an OPC UA item, +it identifies the node by means of its NodeID. + +### Item + +Within the Data Access part of OPC UA, +an _Item_ is any piece of accessible process data on a server. +It can represent a single data point or a block of structured data. +Elements of structured data can be simple data points +or other structured items. + +### Data Element + +In the context of this Device Support only +(i.e., not part of the OPC UA specification), +a _Data Element_ is a unique data point belonging to a server-side item. +If that item is of basic or array type, the data element corresponds +to the value of the item. +If the item is of a structured type, the data element corresponds +to one element of the item's structure. + +### Namespace + +A _Namespace_ is one of multiple server-side scopes for node identifiers, +which the server uses to avoid name clashes. + +A namespace is uniquely defined by an URI. +To improve performance, the server keeps a table of the URIs +and the index in that table is used between client and server. +Some namespaces (and their index numbers) +are reserved by the OPC UA specification for the protocol and the server. + +### Channel + +In OPC UA, a _Channel_ is the low-level transport connection +between a client and a server. +(The OPC UA Device Support module only supports the TCP transport.) +A channel can be secured by running it through SSL to be encrypted, +and/or by signing all messages that are sent through it. + +### Session + +A _Session_ is a named connection +between an OPC UA client and an OPC UA server, +using the transport provided by the channel. +Typically, there is one session between each client and each server. +While it is possible to have multiple sessions +between a single client/server pair, +this increases resource usage on both ends and is generally not recommended +unless there is a particular justifying reason +(such as making use of parallel processing). + +### Subscription + +A _Subscription_ is a mechanism within a session +that the client uses to retrieve value updates from a server +without the need to poll regularly. +A client establishes a subscription within a session and adds items to it, +requesting that the server notify the client +whenever data of the _Monitored Items_ within that subscription changes. + +When defining a subscription, +the client can specify a _publishing interval_ for the server, +telling it how often it would like to receive updated data. +For each monitored item that is part of a subscription, +the client can also specify its _sampling interval_, +which defines the rate at which the server checks the data source for changes. +If the OPC UA server supports server-side queueing of data samples, +the client can also specify for each monitored item the size of that queue. + +## The OPC UA Device Support + +EPICS applications run processes called IOCs, +which interface device data to the EPICS Process Database, +which provides it to the control system through the EPICS network protocols, +Channel Access and pvAccess. + +The Device Support layer enables records in the EPICS Process Database +to communicate with different device types. +The OPC UA Device Support is a client of the OPC UA protocol, +which can read, write and monitor variables residing on an OPC UA server. +(The relevant part of the OPC UA specification is called "Data Access".) + +If your IOC is built correctly (see [Building an IOC](building-an-ioc)), +the OPC UA Device Support is ready to be used. +You can set up OPC UA sessions and subscriptions from the IOC startup script +through commands in the IOCshell +and link EPICS database records to remote OPC UA variables +through the configuration of your EPICS records. + +To support commissioning and troubleshooting, additional IOCshell commands +allow introspection of the Device Support's internal data structures. +The regular EPICS trace and debug modes can be used to investigate +the OPC UA operations at run time. + +### Addressing Basic/Array and Structured Types + +If an OPC UA item is of basic or array type, +a single EPICS data record connects to it. +That record is configured with the session or subscription to be used, +the nodeID of the item and options that apply to the item level. + +If an OPC UA item is of a structured type, a single instance +of the special opcuaItemRecord type represents the complete structure. +Many EPICS data records each connect to a single structure element. +The opcuaItemRecord is configured with all item level configuration. +The single element data records refer to the opcuaItemRecord +and specify the address of the strcuture element they connect to +as well as data element level options. + +% external links + +[epics-intro]: https://docs.epics-controls.org/en/latest/guides/EPICS_Intro.html +[epics-web]: https://epics-controls.org/ + +[opc-foundation]: https://opcfoundation.org/ +[opcua-wiki]: https://en.wikipedia.org/wiki/OPC_Unified_Architecture +[opcua-spec]: https://opcfoundation.org/developer-tools/documents/?type=Specification diff --git a/documentation/explanation/security_concepts.md b/documentation/explanation/security_concepts.md new file mode 100644 index 00000000..ec4b6a39 --- /dev/null +++ b/documentation/explanation/security_concepts.md @@ -0,0 +1,112 @@ +# OPC UA Security Concepts + +The security features of OPC UA +are based on the widely used X.509 certificates +and the concepts of *Public Key Infrastructure* (PKI). + +Certificates are filed in a *Certificate Store*, +containing trusted and own certificates +as well as certificates from certificate authorities (CAs). +On Linux, a file based store is used, +defined by four specific locations for certificates and revocation lists. + +In the basic, explicit form, +trusting a certificate usually means +that the certificate (file) is moved into a specific folder +in the certificate store. + +When using certificate authorities (CAs), +a certificate is trusted if it is signed by a trusted CA. +This approach is preferable for a larger installation, +as it does not require updating all servers and clients +when new peers with new certificates are added. + +A good analogy to certificates are passports: +In a world with only a handful people, +fabricating your passport yourself is reasonable. +In reality, with millions of passport holders, +it helps a lot that passports are fabricated by government agencies +and this fact can be verified by checking holograms, watermarks etc. + +## Self-signed Certificates + +Servers often come with preloaded self-signed server certificates +and a setting that will generously trust +any valid self-signed client certificate. +Generic clients typically offer an easy way +to create a self-signed client certificate. + +These settings are intended to get users started with security easily +and create minimal frustration during development. +Such "simple" self-signed certificates +are good enough for securing the communication, +but they are not very practical for authentication: +every self-signed certificate must be trusted explicitly - +or be accepted by default. + +## Communication Security + +Each OPC UA application (server and client) needs an *Application Instance Certificate* and a related public/private key pair to identify itself to its communication peers. The public key is contained in the certificate; the private key is secret and used for signing and encryption of messages. + +For a secure communication to be established, both peers must trust the certificate of the other. + +OPC UA defines three *Message Security Modes*: + +| Message Security Mode | | +| --------------------- | ------------------------------------------- | +| None | No security is applied. | +| Sign | All messages are signed, but not encrypted. | +| SignAndEncrypt | All messages are signed and encrypted. | + +As security algorithms are getting obsolete, +an increasing number of *Security Policies* +(sets of security algorithms and key lengths) are defined, +identified their unique URIs: + +:::{list-table} +:header-rows: 1 + +* - Security Policy + - URI +* - None + - http://opcfoundation.org/UA/SecurityPolicy#None +* - Basic128Rsa15 *(obsolete)* + - http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15 +* - Basic256 + - http://opcfoundation.org/UA/SecurityPolicy#Basic256 +* - Basic256Sha256 + - http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 +* - Aes128_Sha256_RsaOaep + - http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep +* - Aes256_Sha256_RsaPss + - http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss +::: + +## Client Authentication (Identity) + +For client authentication +related to managing the authorization of access to a server, +OPC UA defines four *User Token Types* (methods of user authentication): + +| User Token Type | | +| ------------------------ | ------------------------------------------ | +| Anonymous Identity Token | No user information available. | +| Username Identity Token | User identified by name and password. | +| X.509 Identity Token | User identified by an X.509v3 certificate. | +| Issued Identity Token | User identified by a WS-SecurityToken. | + +## Endpoint Discovery + +In order to establish a secure connection, +the client has to find out which security features the server provides. +This process is called *Discovery*. + +In its answer to a discovery request, +a server provides a list of *Endpoints* for the client to connect to, +with different sets of security features. + +As a debugging tool, +the iocShell command `opcuaShowSecurity` runs the discovery service +and displays the available endpoints +with their features and their *security level* +(used by the IOC to find the endpoint providing the best security). diff --git a/documentation/how-to/debugging.md b/documentation/how-to/debugging.md new file mode 100644 index 00000000..3dc56eea --- /dev/null +++ b/documentation/how-to/debugging.md @@ -0,0 +1,74 @@ +# Debugging and Tracing + +The OPC UA Device Support module provides +several tools for debugging and tracing. + +## IOC Shell Commands + +Several commands are available for inspecting the state of the module: + +* `opcuaShow`: + Prints information about + sessions, subscriptions, items, and data elements. + Supports glob patterns. +* `opcuaShowSecurity`: + Prints information about the IOCś security setup + and discovered endpoints on the remote server. +* `opcuaConnect` / `opcuaDisconnect`: + Manually manage session connections. + +Example: +``` +opcuaShow OPC* 1 +``` + +## Session/Subscription Debugging + +Sessions and Subscriptions have a `debug` option. +Setting it to `>= 1` (verbosity level) +will enable printing debug information +when the client handles things related to it. + +Example: +``` +opcuaOption OPC1 debug=5 +``` + +This option can be changed at runtime. +To see debug messages from initial connection handling, +enable it before `iocInit`. + +## EPICS Record Debugging + +You can enable tracing and debugging for specific EPICS records +by setting the `TPRO` field to a value `>= 1`. + +* In `.db` file: + `field(TPRO, "1")` +* On the IOC: + `dbpf .TPRO 1` +* In a shell: + `caput .TPRO 1` + +## Adaptive Concurrency Monitoring + +:::{versionadded} 0.12 +Adaptive concurrency automatically throttles requests +based on server response times. +This allows the IOC to gracefully handle slow and fast servers, +adapting automatically to the available bandwidth. + +If adaptive concurrency is enabled, +you can monitor its state using `opcuaShow`. +It will display current limits and performance metrics. +::: + +## Using UaExpert (External Tool) + +UaExpert is a powerful free graphical OPC UA client tool +from Unified Automation (it uses the same UA SDK) +that can be used to: +1. Test connection to the OPC UA server. +2. Browse the address space. +3. Identify `NamespaceIndex` and `Identifier` (NodeID) + for EPICS record configuration. diff --git a/documentation/how-to/plc_configuration.md b/documentation/how-to/plc_configuration.md new file mode 100644 index 00000000..4c26d815 --- /dev/null +++ b/documentation/how-to/plc_configuration.md @@ -0,0 +1,127 @@ +# Configuring the OPC UA Server on a Siemens S7-1500 Series PLC + +This guide details the steps to configure a Siemens S7-1500 PLC +as an OPC UA server using TIA Portal. + +## Activate OPC UA Server + +1. Open your project in TIA Portal. +2. Navigate to your PLC's properties (e.g., `PLC_1 [CPU 1518-4 PN/DP]`). +3. Go to `General` > `OPC UA` > `Server` > `General`. +4. Check the `Activate OPC UA server` box. +5. Ensure that one of the `Server addresses` (`opc.tcp://...`) + matches the IP address of your PLC on the network. + The default OPC UA port is 4840. + +### Using OPC UA with a CP Module + +OPC UA works using a CP module +when respecting the following minimal requirements: + +| Feature | Minimum Requirement | +| ---------------------- | ------------------- | +| Virtual interface (W1) | CPU FW **V2.8** | +| Configuration in TIA | TIA Portal **V16** | +| OPC UA via CP | Supported via W1 | +| Compatible CP | CP 1543‑1 FW ≥ **V2.2** (recommended ≥ V3.0) | + + +## Configure OPC UA Server Options + +1. In the PLC properties, + navigate to `General` > `OPC UA` > `Server` > `Options`. +2. **Port:** + Adjust the `Port` number + if a different port than the default 4840 is required. +3. **Minimum sampling interval:** + Defines the minimum rate (in ms) + at which the server can check data sources for changes. +4. **Minimum publishing interval:** + Defines the minimum rate (in ms) + at which the server can send updated data to clients. + + :::{hint} + For S7-1500 PLCs, + especially small or mid-size ones (e.g., S7-1516), + starting with 250ms intervals + and grouping variables into structures + is a good practice for improving performance. + ::: + +## Configure OPC UA Server Security + +1. In the PLC properties, + navigate to `General` > `OPC UA` > `Server` > `Security`. +2. **Security Policies:** + Select the desired security policy. + For basic testing and development, + you might select `No security`. + For production environments, + stronger options like `Basic128Rsa15-Sign & Encrypt` are recommended. +3. **Guest Authentication:** + If no specific user authentication is required, + check `Enable guest authentication`. + + :::{attention} + On the IOC, OPC UA Security is enabled by default. + To connect to any OPC UA server *without* security, + you must set the option `sec-mode=None` + for the concerned session(s) in your EPICS IOC configuration. + ::: + +## Configure OPC UA Runtime License + +1. In the PLC properties, + navigate to `General` > `Runtime licenses` > `OPC UA`. +2. Ensure the `Type of purchased license` + is at least the same level as the `Type of required license` + (e.g., `SIMATIC OPC UA S7-1500 large`). + +## Define Variables in PLC Data Structures + +Organize your PLC variables into data blocks +that will be exposed as OPC UA nodes. + +1. **Create Data Blocks:** + In TIA Portal, + create new data blocks (e.g., `DB1`, `DB2`, `DB3`, `DB4`). +2. **Define Variables:** + Inside each data block, + define the variables you want to expose. + You can use structured user-defined data types, + which will appear as structured data on OPC UA. + Structured data is very efficient when using subscriptions. + For structures, the server on the S7 always creates + a node for the structure + and separate nodes for each of its elements. + +### Example PLC Data Types and EPICS Record Type Compatibility + +:::{list-table} +:header-rows: 1 + +* - PLC data type + - EPICS Record type +* - Real, LReal + - ai/ao +* - Bool + - bi/bo +* - Byte/Word/DWord, most *Int + - longin/longout, mbbi/mbbo, mbbiDirect/mbboDirect, bi/bo, ai/ao (RVAL) +* - LInt/ULInt + - int64in/int64out +* - String + - stringin/stringout, lsi/lso +* - Array[m..n] of * + - aai/aao/waveform (with compatible FTVL) +* - Struct (UDT) + - opcuaItem +::: + +Arrays that start at a non-zero index on the PLC +are shifted to start from zero over OPC UA. + +:::{note} +Structured items can *only* be handled +using the `opcuaItem` record type. +::: diff --git a/documentation/how-to/record_configuration.md.autosave b/documentation/how-to/record_configuration.md.autosave new file mode 100644 index 00000000..ec6e6857 --- /dev/null +++ b/documentation/how-to/record_configuration.md.autosave @@ -0,0 +1,250 @@ +# Configuring EPICS Records for OPC UA + +This guide shows how to configure EPICS records +for various OPC UA data exchange scenarios. + +## Single Scalar Values + +Individual variables (OPC UA Items), +mapped to single EPICS records. + +### General INP/OUT Link Field Format + +`@ ns=;= +[