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/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/RecordConnector.cpp b/devOpcuaSup/RecordConnector.cpp index e83a5ce2..590683d5 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; @@ -85,14 +96,25 @@ void processCallback (epicsCallback *pcallback, const ProcessReason reason) prec = static_cast(pUsr); if (!prec || !prec->dpvt) return; + // Do not process writeComplete on struct elements that have not been written + if (reason == writeComplete && !prec->pact) + return; + RecordConnector *pvt = static_cast(prec->dpvt); 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); } diff --git a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp index e7cbce2f..976e70b2 100644 --- a/devOpcuaSup/UaSdk/DataElementUaSdk.cpp +++ b/devOpcuaSup/UaSdk/DataElementUaSdk.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include @@ -110,12 +112,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)) { @@ -145,65 +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}); - 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_Clear(&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 @@ -262,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 () { @@ -307,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; @@ -462,6 +498,7 @@ DataElementUaSdk::readScalar (char *value, const size_t num, ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadScalar(upd.get(), "CString", num); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -486,9 +523,37 @@ 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 (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); + 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; } if (statusCode) *statusCode = stat; if (statusText) { @@ -560,6 +625,7 @@ DataElementUaSdk::readArray (char *value, const epicsUInt32 len, ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadArray(upd.get(), num, epicsTypeString(value)); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -602,7 +668,6 @@ DataElementUaSdk::readArray (char *value, const epicsUInt32 len, strncpy(value + i * len, UaString(arr[i]).toUtf8(), len); (value + i * len)[len - 1] = '\0'; } - prec->udf = false; } } if (statusCode) *statusCode = stat; @@ -650,6 +715,7 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadArray(upd.get(), num, epicsTypeString(*value)); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -690,7 +756,6 @@ DataElementUaSdk::readArray (epicsUInt8 *value, const e elemsWritten = static_cast(arr.size()); if (num < elemsWritten) elemsWritten = num; memcpy(value, arr.data(), sizeof(epicsUInt8) * elemsWritten); - prec->udf = false; } } if (statusCode) *statusCode = stat; @@ -887,17 +952,59 @@ 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; + 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_Clear(&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 (incomingData.type()) { + switch (type) { case OpcUaType_String: { // Scope of Guard G Guard G(outgoingLock); outgoingData.setString(static_cast(value)); markAsDirty(); + ret = 0; break; } case OpcUaType_LocalizedText: @@ -908,6 +1015,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: @@ -918,122 +1026,134 @@ 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(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; } @@ -1051,7 +1171,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) @@ -1067,7 +1187,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 { @@ -1077,16 +1197,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); @@ -1094,7 +1215,7 @@ DataElementUaSdk::writeArray (const char **value, const epicsUInt32 len, markAsDirty(); } - dbgWriteArray(num, epicsTypeString(*value)); + dbgWriteArray(num, epicsTypeString(value)); } return ret; } @@ -1198,7 +1319,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 b9ad542c..ebef3bc8 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. @@ -679,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 @@ -776,6 +777,7 @@ class DataElementUaSdk : 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: @@ -808,7 +810,6 @@ class DataElementUaSdk : public DataElement (void) recGblSetSevr(prec, READ_ALARM, MINOR_ALARM); } *value = v; - prec->udf = false; } } if (statusCode) *statusCode = stat; @@ -854,6 +855,7 @@ class DataElementUaSdk : public DataElement ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadArray(upd.get(), num, epicsTypeString(*value)); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -893,7 +895,6 @@ class DataElementUaSdk : public DataElement UaVariant_to(upd->getData(), arr); elemsWritten = num < arr.length() ? num : arr.length(); memcpy(value, arr.rawData(), sizeof(ET) * elemsWritten); - prec->udf = false; } } if (statusCode) *statusCode = stat; @@ -933,17 +934,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: @@ -951,9 +950,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: @@ -961,9 +958,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: @@ -971,9 +966,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: @@ -981,9 +974,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: @@ -991,19 +982,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: @@ -1011,9 +1000,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: @@ -1021,9 +1008,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: @@ -1031,9 +1016,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: @@ -1041,9 +1024,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: @@ -1054,18 +1035,24 @@ 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; } // 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/UaSdk/ItemUaSdk.cpp b/devOpcuaSup/UaSdk/ItemUaSdk.cpp index a029fcb4..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) { @@ -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 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 6f8b42ee..82a546a5 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()*2)); 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); @@ -1173,7 +1177,6 @@ void SessionUaSdk::connectionStatusChanged ( rebuildNodeIds(); registerNodes(); createAllSubscriptions(); - addAllMonitoredItems(); } if (serverConnectionStatus != UaClient::ConnectionWarningWatchdogTimeout) { if (debug) { @@ -1192,6 +1195,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; @@ -1210,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, @@ -1228,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()*2 != 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()); @@ -1237,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 " @@ -1245,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/UaSdk/SubscriptionUaSdk.cpp b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp index c83ab8be..5687dd29 100644 --- a/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp +++ b/devOpcuaSup/UaSdk/SubscriptionUaSdk.cpp @@ -199,20 +199,21 @@ 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 << "** 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 << 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()); + items[i]->setIncomingEvent(ProcessReason::connectionLoss); } } } diff --git a/devOpcuaSup/devOpcua.cpp b/devOpcuaSup/devOpcua.cpp index db2028fe..82177a9e 100644 --- a/devOpcuaSup/devOpcua.cpp +++ b/devOpcuaSup/devOpcua.cpp @@ -699,7 +699,64 @@ opcua_write_analog (REC *prec) return ret; } -// enum output +// enum + +template +inline void updateEnumInfos (REC *prec, RecordConnector *pcon) +{ + if (pcon->pdataelement->enumChoices && + pcon->state() == ConnectionStatus::initialRead && + (!prec->sdef || prec->sdef & 0x10)) + { // no strings/value defined by user (but maybe by OPC-UA server) + prec->sdef = 0x1f; // mark strings/values as defined by OPC-UA + 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 >= 16) break; + } + while (i < 16) { + // 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); + } +} + + +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); + traceReadPrint(pdbc, pcon, ret, !!value, prec->rval, "RVAL"); + manageStateAndBiniProcessing(pcon); + } + if (nextReason) + pcon->requestRecordProcessing(nextReason); + } CATCH() + return ret; +} template long @@ -726,6 +783,7 @@ 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) { *rval &= prec->mask; if (prec->shft) @@ -736,13 +794,14 @@ opcua_write_enum (REC *prec) for (epicsUInt16 i = 0; i < 16; i++) { if (*pstate_values == *rval) { prec->val = i; + recGblResetAlarms(prec); break; } pstate_values++; } } else { - // no defined states - prec->val = static_cast(*rval); + // no defined states + prec->val = static_cast(*rval); } } traceReadPrint(pdbc, pcon, ret, !!rval, prec->val, prec->rval); @@ -1007,6 +1066,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 +1118,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 +1131,7 @@ opcua_read_array (REC *prec) template long opcua_write_array (REC *prec) -{ +{ long ret = 0; TRY { Guard G(pcon->lock); @@ -1084,6 +1148,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 +1202,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 +1344,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 dcacf46f..284d3bb6 100644 --- a/devOpcuaSup/open62541/DataElementOpen62541.cpp +++ b/devOpcuaSup/open62541/DataElementOpen62541.cpp @@ -107,32 +107,46 @@ 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 -UA_Boolean -UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, +// 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 *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; - const UA_DataType *typelists[2] = { UA_TYPES, &type[-type->typeIndex] }; - 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]; - const UA_DataType *mt = &typelists[!m->namespaceZero][m->memberTypeIndex]; + const UA_DataType *mt = memberTypeOf(type, m); offset += m->padding; if(strcmp(memberName, m->memberName) == 0) { *outOffset = offset; *outMemberType = mt; *outIsArray = m->isArray; - return true; + *outIsOptional = m->isOptional; + 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 { @@ -149,29 +163,31 @@ UA_DataType_getStructMember(const UA_DataType *type, const char *memberName, } } - return false; + return 0; } -#endif -bool +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: - { - 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; + 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", @@ -179,8 +195,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", @@ -191,10 +207,23 @@ DataElementOpen62541::createMap (const UA_DataType *type, for (auto &it : elements) { auto pelem = it.lock(); - if (UA_DataType_getStructMember(type, pelem->name.c_str(), + if ((pelem->index = UA_DataType_getStructMemberExt(type, pelem->name.c_str(), &pelem->offset, &pelem->memberType, - &pelem->isArray)) { + &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 @@ -204,18 +233,16 @@ 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 @@ -230,6 +257,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)) { @@ -265,7 +295,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 @@ -282,28 +312,39 @@ 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); + 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) { + 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); } } } @@ -313,6 +354,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)); @@ -350,35 +393,41 @@ 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); - 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; + 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 { - 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; @@ -413,7 +462,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 @@ -543,6 +593,7 @@ DataElementOpen62541::readScalar (char *value, const size_t num, ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadScalar(upd.get(), "CString", num); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -567,40 +618,73 @@ 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; } - default: - if (data.type) - UA_print(data.data, data.type, &buffer); + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_INT32: + { + if (enumChoices) { + auto it = enumChoices->find(*static_cast(payload)); + if (it != enumChoices->end()) { + buffer = UA_String_fromChars(it->second.c_str()); + break; + } + } + // no enum or index not found: fall through } + default: + if (type) + UA_print(payload, type, &buffer); + }; if (n > datastring->length) n = datastring->length; strncpy(value, reinterpret_cast(datastring->data), n); value[n] = '\0'; UA_String_clear(&buffer); - prec->udf = false; UA_Variant_clear(&data); } if (statusCode) *statusCode = stat; @@ -673,6 +757,7 @@ DataElementOpen62541::readArray (char *value, const epicsUInt32 len, ProcessReason nReason; std::shared_ptr upd = incomingQueue.popUpdate(&nReason); dbgReadArray(upd.get(), num, epicsTypeString(value)); + prec->udf = false; switch (upd->getType()) { case ProcessReason::readFailure: @@ -713,7 +798,6 @@ DataElementOpen62541::readArray (char *value, const epicsUInt32 len, strncpy(value + i * len, reinterpret_cast(static_cast(data.data)[i].data), len); (value + i * len)[len - 1] = '\0'; } - prec->udf = false; } UA_Variant_clear(&data); } @@ -905,168 +989,201 @@ 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 = 0; + long ret = 1; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; long l; unsigned long ul; double d; + char* end = nullptr; - 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; + UA_Variant_clear(&outgoingData); // unlikely but we may still have unsent old data to discard + 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]); + } + } } - 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]); + + switch (typeKindOf(type)) { + 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; + ret = 0; + 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; + ret = 0; + 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; + ret = 0; + 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; + case UA_DATATYPEKIND_BYTE: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_SBYTE: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT16: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT16: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT32: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_ENUM: + case UA_DATATYPEKIND_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)) { + UA_Int32 val = static_cast(l); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_INT32]); + markAsDirty(); + ret = 0; + } + break; + case UA_DATATYPEKIND_UINT64: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_INT64: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_FLOAT: + 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(); + ret = 0; + } + break; + case UA_DATATYPEKIND_DOUBLE: + { + d = strtod(value, &end); + if (end != value) { + UA_Double val = static_cast(d); + status = UA_Variant_setScalarCopy(&outgoingData, &val, &UA_TYPES[UA_TYPES_DOUBLE]); + } + break; } - 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]); - markAsDirty(); - } else { + default: + errlogPrintf("%s : unsupported conversion from string to %s for outgoing data\n", + prec->name, variantTypeString(incomingData)); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; } - 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 { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; + 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; + } } - 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); + } // Scope of Guard G + 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)) { @@ -1075,7 +1192,8 @@ DataElementOpen62541::writeScalar (const char *value, const epicsUInt32 len, dbC (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); ret = 1; } - dbgWriteScalar(); + if (ret == 0) + dbgWriteScalar(); return ret; } @@ -1093,7 +1211,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) @@ -1109,7 +1227,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 { @@ -1117,36 +1235,24 @@ 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_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(); } - 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; @@ -1215,7 +1321,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..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; /** @@ -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) @@ -788,7 +803,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); @@ -800,15 +815,14 @@ 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: (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 +832,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; @@ -918,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); } @@ -1001,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); } @@ -1042,158 +1063,135 @@ class DataElementOpen62541 : public DataElement writeScalar (const ET &value, dbCommon *prec) { - long ret = 0; + long ret = 1; 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]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - 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]); + 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; + ret = 0; + 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]); - markAsDirty(); - } else { - (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); - ret = 1; - } - 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); - 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 { - (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); + 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; } - 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); + 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); } - break; } - default: - ret = 1; - errlogPrintf("%s : unsupported conversion for outgoing data\n", + if (ret != 0) { + errlogPrintf("%s : value out of range\n", prec->name); (void) recGblSetSevr(prec, WRITE_ALARM, INVALID_ALARM); } @@ -1204,13 +1202,14 @@ class DataElementOpen62541 : public DataElement ret = 1; } - dbgWriteScalar(); + if (ret == 0) + dbgWriteScalar(); return ret; } // 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); @@ -1242,6 +1241,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(); } @@ -1264,17 +1264,41 @@ 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 */ + 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) +{ + // 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/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..8d5ba8d5 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. @@ -153,6 +153,7 @@ class ItemOpen62541 : 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 UA_DataValue &value, ProcessReason reason); @@ -202,7 +203,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 */ 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` . diff --git a/devOpcuaSup/open62541/SessionOpen62541.cpp b/devOpcuaSup/open62541/SessionOpen62541.cpp index c93db70d..68f03f48 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,19 @@ 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()*2, &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); @@ -656,8 +678,7 @@ SessionOpen62541::processRequests (std::vector> &ba status=UA_Client_sendAsyncReadRequest(client, &request, [] (UA_Client *client, void *userdata, - UA_UInt32 - requestId, + UA_UInt32 requestId, UA_ReadResponse *response) { static_cast(userdata)->readComplete(requestId, response); @@ -862,8 +883,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}); @@ -943,8 +963,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() @@ -1040,8 +1059,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 || @@ -1524,6 +1542,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() { @@ -1531,6 +1561,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), @@ -1634,31 +1674,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, @@ -1674,6 +1807,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) { @@ -1682,21 +1820,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) { @@ -1708,6 +1835,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) { @@ -1755,9 +1886,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; } @@ -1808,20 +1937,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; @@ -2131,9 +2260,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; @@ -2152,10 +2282,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; } @@ -2164,13 +2294,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 { @@ -2182,12 +2328,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 @@ -2345,7 +2489,6 @@ SessionOpen62541::connectionStatusChanged ( rebuildNodeIds(); registerNodes(); createAllSubscriptions(); - addAllMonitoredItems(); if (debug) { std::cout << "Session " << name << ": triggering initial read for all " @@ -2363,6 +2506,7 @@ SessionOpen62541::connectionStatusChanged ( // status needs to be updated before requests are being issued sessionState = newSessionState; reader.pushRequest(cargo, menuPriorityHIGH); + addAllMonitoredItems(); break; } @@ -2396,7 +2540,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()*2 != response->resultsSize) errlogPrintf("OPC UA session %s: (readComplete) received a callback " "with %llu values for a request containing %llu items\n", name.c_str(), @@ -2407,11 +2551,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; } @@ -2419,8 +2573,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 a2123ea4..820204be 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 @@ -420,12 +439,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; diff --git a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp index 624cb76a..eaa216dd 100644 --- a/devOpcuaSup/open62541/SubscriptionOpen62541.cpp +++ b/devOpcuaSup/open62541/SubscriptionOpen62541.cpp @@ -163,27 +163,28 @@ 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) - std::cout << "** Monitored item " << monitoredItemCreateRequest.itemToMonitor.nodeId + it->setRevisedSamplingInterval(monitoredItemCreateResult.revisedSamplingInterval); + it->setRevisedQueueSize(monitoredItemCreateResult.revisedQueueSize); + if (debug >= 5) { + 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 << 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; + it->setIncomingEvent(ProcessReason::connectionLoss); } i++; }