/* * Copyright - See the COPYRIGHT that is included with this distribution. * pvxs is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. * * Author George S. McIntyre , 2023 * */ #include #include #include #include #include "iocsource.h" #include "dbentry.h" #include "dberrormessage.h" #include "typeutils.h" #include "credentials.h" #include "securityclient.h" #include "securitylogger.h" namespace pvxs { namespace ioc { /** * IOC function that will get data from the database. This will use the provided value prototype to determine the shape * of the data to be returned (if it has a `value` subfield, it is a structure). The provided channel * is used to retrieve the data and the flags forValues and forProperties are used to determine whether to fetch * values, properties or both from the database. * * When the data has been retrieved the provided returnFn is called with the value, otherwise the an * exception is thrown * * @param pDbValueChannel the channel to get the value from * @param pDbPropertiesChannel the channel to get the properties from * @param valuePrototype the value prototype to use to determine the shape of the data * @param getOperationType for values, for properties or for metadata * @param pDbFieldLog the field log of changes if this comes from a subscription */ void IOCSource::get(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& valuePrototype, const GetOperationType getOperationType, db_field_log* pDbFieldLog) { // Assumes this is the leaf node in a group, or is a simple db record.field reference Value value = valuePrototype; // The value that will be returned, if compound then metadata is set here Value valueTarget = valuePrototype; // The part of value that will be retrieved from the database value field bool isCompound = false; if (getOperationType <= FOR_VALUE && value.type() == TypeCode::Any) { auto type = fromDbrType(dbChannelFinalFieldType(pDbValueChannel)); if (dbChannelFinalElements(pDbValueChannel) != 1) { type = type.arrayOf(); } value = valueTarget = TypeDef(type).create(); valuePrototype.from(value); } else if (auto targetCandidate = value["value"]) { isCompound = true; valueTarget = targetCandidate; } // options bit mask LSB to MSB uint32_t options = 0; if (isCompound && getOperationType <= FOR_METADATA) { options = IOC_VALUE_OPTIONS; } if (isCompound && (getOperationType == FOR_VALUE_AND_PROPERTIES || getOperationType == FOR_PROPERTIES)) { options |= IOC_PROPERTIES_OPTIONS; } if (dbChannelFinalElements(pDbValueChannel) == 1) { getScalar(pDbValueChannel, pDbPropertiesChannel, value, valueTarget, options, getOperationType, pDbFieldLog); } else { getArray(pDbValueChannel, pDbPropertiesChannel, value, valueTarget, options, getOperationType, pDbFieldLog); } } /** * Get a scalar value from the database * @param pDbValueChannel the database channel to get the value from * @param pDbPropertiesChannel the database channel to get the properties from * @param value the value to set including metadata if this is a compound value * @param valueTarget where to store the "value" part of the scalar within value * @param requestedOptions the options defining what metadata to get * @param getOperationType for values, for properties or for metadata * @param pDbFieldLog the field log of changes if this comes from a subscription */ void IOCSource::getScalar(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& value, Value& valueTarget, uint32_t& requestedOptions, const GetOperationType getOperationType, db_field_log* pDbFieldLog) { ValueBuffer valueBuffer{}; // Enough for metadata and 1 scalar void* pValueBuffer = &valueBuffer; long nElements = (getOperationType <= FOR_VALUE) ? 1 : 0; long actualOptions = requestedOptions; // Get metadata/properties and field value // Note that metadata will precede the value in the buffer and will be laid out in the options order DBErrorMessage dbErrorMessage; if (getOperationType <= FOR_METADATA) { dbErrorMessage = dbChannelGet(pDbValueChannel, dbChannelFinalFieldType(pDbValueChannel), pValueBuffer, &actualOptions, &nElements, pDbFieldLog); } else { dbErrorMessage = dbChannelGet(pDbPropertiesChannel, dbChannelFinalFieldType(pDbPropertiesChannel), pValueBuffer, &actualOptions, &nElements, pDbFieldLog); } if (dbErrorMessage) { throw std::runtime_error(dbErrorMessage.c_str()); } // Get metadata/properties from buffer if any have been requested getMetadata(value, pValueBuffer, requestedOptions, actualOptions); // Get the value if it has been requested if (getOperationType <= FOR_VALUE) { if (dbChannelFinalFieldType(pDbValueChannel) == DBR_ENUM && valueTarget.type() == TypeCode::Struct) { valueTarget["index"] = *(uint16_t*)(pValueBuffer); } else { getValueFromBuffer(valueTarget, pValueBuffer); } } } /** * Get an array value from the database * * @param pDbValueChannel the database channel to get the value from * @param pDbPropertiesChannel the database channel to get the properties from * @param value the value to set including metadata if this is a compound value * @param valueTarget where to store the "value" part of the array within value * @param requestedOptions the options defining what metadata to get * @param getOperationType for values, for properties or for metadata * @param pDbFieldLog the field log of changes if this comes from a subscription */ void IOCSource::getArray(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& value, Value& valueTarget, uint32_t& requestedOptions, const GetOperationType getOperationType, db_field_log* pDbFieldLog) { // value buffer to store the field we will get from the database including metadata. std::vector valueBuffer; auto nElements = (getOperationType <= FOR_VALUE) ? (long)dbChannelFinalElements(pDbValueChannel) : 0; // maximal number of elements // Initialize the buffer to the maximal size including metadata and zero it out valueBuffer.resize(nElements * pDbValueChannel->addr.field_size + MAX_METADATA_SIZE, '\0'); void* pValueBuffer = &valueBuffer[0]; long actualOptions = requestedOptions; // Get the metadata/properties and value into this buffer // Note that metadata will precede the array value in the buffer and will be laid out in the options order DBErrorMessage dbErrorMessage; if (getOperationType <= FOR_METADATA) { dbErrorMessage = dbChannelGet(pDbValueChannel, dbChannelFinalFieldType(pDbValueChannel), pValueBuffer, &actualOptions, &nElements, pDbFieldLog); } else { dbErrorMessage = dbChannelGet(pDbPropertiesChannel, pDbPropertiesChannel->final_type, pValueBuffer, &actualOptions, &nElements, pDbFieldLog); } if (dbErrorMessage) { throw std::runtime_error(dbErrorMessage.c_str()); } // Get metadata/properties from buffer if any have been requested getMetadata(value, pValueBuffer, requestedOptions, actualOptions); // Get the value array if it has been requested if (getOperationType <= FOR_VALUE) { // Get the array value from the updated buffer pointer // Note: nElements will have been updated with the number of actual elements in the array if (dbChannelFinalFieldType(pDbValueChannel) == DBR_ENUM && valueTarget.type() == TypeCode::Struct) { shared_array values(nElements); for (auto i = 0; i < nElements; i++) { values[i] = ((uint16_t*)pValueBuffer)[i]; } valueTarget["index"] = values.freeze(); } else { getValueFromBuffer(valueTarget, pValueBuffer, nElements); } } } /** * Put a given value to the specified channel. Throw an exception if there are any errors. * * @param pDbChannel the channel to put the value into * @param value the value to put */ void IOCSource::put(dbChannel* pDbChannel, const Value& value) { Value valueSource = value; // TODO may need to handle Array of and Union as special cases as well if (value.type() == TypeCode::Any) { valueSource = valueSource["->"]; } else if (auto sourceCandidate = value["value"]) { valueSource = sourceCandidate; } if (dbChannelFinalElements(pDbChannel) == 1) { putScalar(pDbChannel, valueSource); } else { putArray(pDbChannel, valueSource); } } /** * Put a given scalar value to the specified channel. Throw an exception if there are any errors. * * @param pDbChannel the channel to put the value into * @param value the scalar value to put */ void IOCSource::putScalar(dbChannel* pDbChannel, const Value& value) { ScalarValueBuffer valueBuffer{}; auto pValueBuffer = (char*)&valueBuffer; if (dbChannelFinalFieldType(pDbChannel) == DBR_ENUM) { *(uint16_t*)(pValueBuffer) = (value)["index"].as(); } else { setValueInBuffer(value, pValueBuffer, pDbChannel); } long status; if (dbChannelFieldType(pDbChannel) >= DBF_INLINK && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { status = dbChannelPutField(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, 1); } else { status = dbChannelPut(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, 1); } DBErrorMessage dbErrorMessage(status); if (dbErrorMessage) { throw std::runtime_error(dbErrorMessage.c_str()); } } /** * Put a given array value to the specified channel. Throw an exception if there are any errors. * * @param pDbChannel the channel to put the value into * @param value the array value to put */ void IOCSource::putArray(dbChannel* pDbChannel, const Value& value) { auto valueArray = value.as>(); void* pValueBuffer; long nElements = (long)valueArray.size(); std::vector stringValueBuffer; if (dbChannelFinalFieldType(pDbChannel) == DBR_STRING) { stringValueBuffer.resize(MAX_STRING_SIZE * valueArray.size(), '\0'); char* pCurrent = stringValueBuffer.data(); auto stringArray = valueArray.castTo(); for (auto& element: stringArray) { element.copy(pCurrent, MAX_STRING_SIZE - 1); pCurrent += MAX_STRING_SIZE; } pValueBuffer = stringValueBuffer.data(); } else { // Set the buffer to the internal value buffer as it's the same for the db records for non-string fields pValueBuffer = (void*)valueArray.data(); setValueInBuffer(value, (char*)pValueBuffer, nElements); } long status; if (dbChannelFieldType(pDbChannel) >= DBF_INLINK && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { status = dbChannelPutField(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, nElements); } else { status = dbChannelPut(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, nElements); } DBErrorMessage dbErrorMessage(status); if (dbErrorMessage) { throw std::runtime_error(dbErrorMessage.c_str()); } } /** * Do necessary preprocessing before put operations. Check if put is allowed. * * @param pDbChannel channel to do preprocessing for * @param securityLogger the logger that will audit security events * @param credentials client credentials that are applied to this execution context * @param securityClient the security client. Keep in scope around the put operation */ void IOCSource::doPreProcessing(dbChannel* pDbChannel, SecurityLogger& securityLogger, const Credentials& credentials, const SecurityClient& securityClient) { if (pDbChannel->addr.special == SPC_ATTRIBUTE) { throw std::runtime_error("Unable to put value: Modifications not allowed: S_db_noMod"); } else if (pDbChannel->addr.precord->disp && pDbChannel->addr.pfield != &pDbChannel->addr.precord->disp) { throw std::runtime_error("Unable to put value: Field Disabled: S_db_putDisabled"); } SecurityLogger asWritePvt( asTrapWriteWithData((securityClient.cli)[0], // The user is the first element credentials.cred[0].c_str(), // The user is the first element credentials.host.c_str(), pDbChannel, dbChannelFinalFieldType(pDbChannel), dbChannelFinalElements(pDbChannel), nullptr ) ); securityLogger.swap(asWritePvt); } /** * Do necessary preprocessing before put operations. Check if put is allowed. * * @param securityClient security client applied to this execution context */ void IOCSource::doFieldPreProcessing(const SecurityClient& securityClient) { if (!securityClient.canWrite()) { // TODO this will abort the whole group put operation, so may be a behavior change, need to check throw std::runtime_error("Put not permitted"); } } /** * Do necessary post processing after put operations. If this field is a processing record then do processing * and set status * Note: Only called when dbPutField() is not called. * * @param pDbChannel channel to do post processing for * @param forceProcessing whether to force processing, True, False */ void IOCSource::doPostProcessing(dbChannel* pDbChannel, TriState forceProcessing) { if (pDbChannel->addr.pfield == &pDbChannel->addr.precord->proc || (forceProcessing == True) || (pDbChannel->addr.pfldDes->process_passive && pDbChannel->addr.precord->scan == 0 && dbChannelFinalFieldSize(pDbChannel) < DBR_PUT_ACKT && forceProcessing == Unset)) { if (pDbChannel->addr.precord->pact) { if (dbAccessDebugPUTF && pDbChannel->addr.precord->tpro) { printf("%s: single source onPut to Active '%s', setting RPRO=1\n", epicsThreadGetNameSelf(), pDbChannel->addr.precord->name); } pDbChannel->addr.precord->rpro = TRUE; } else { pDbChannel->addr.precord->putf = TRUE; DBErrorMessage dbErrorMessage(dbProcess(pDbChannel->addr.precord)); if (dbErrorMessage) { throw std::runtime_error(dbErrorMessage.c_str()); } } } } /** * Set a flag that will force processing of record in the specified security control object * * @param pvRequest the request * @param securityControlObject the security control object to update */ void IOCSource::setForceProcessingFlag(const Value& pvRequest, const std::shared_ptr& securityControlObject) { pvRequest["record._options.process"] .as([&securityControlObject](const std::string& forceProcessingOption) { if (forceProcessingOption == "true") { securityControlObject->forceProcessing = True; } else if (forceProcessingOption == "false") { securityControlObject->forceProcessing = False; } }); } /** * Set a return value from the given database value buffer * * @param valueTarget the value to set * @param pValueBuffer pointer to the database value buffer */ void IOCSource::getValueFromBuffer(Value& valueTarget, const void* pValueBuffer) { auto valueType(valueTarget.type()); if (valueType == TypeCode::String) { valueTarget = ((const char*)pValueBuffer); } else { SwitchTypeCodeForTemplatedCall(valueType, getValueFromBuffer, (valueTarget, pValueBuffer)); } } /** * Set a return value from the given database value buffer. This is the array version of the function * * @param valueTarget the value to set * @param pValueBuffer the database value buffer * @param nElements the number of elements in the buffer */ void IOCSource::getValueFromBuffer(Value& valueTarget, const void* pValueBuffer, const long& nElements) { auto valueType(valueTarget.type()); if (valueType == TypeCode::StringA) { shared_array values(nElements); char stringBuffer[MAX_STRING_SIZE + 1]{ 0 }; // Need to do this because some strings may not be null terminated for (auto i = 0; i < nElements; i++) { auto pStringValue = (char*)&((const char*)pValueBuffer)[i * MAX_STRING_SIZE]; strncpy(stringBuffer, pStringValue, MAX_STRING_SIZE); values[i] = (char*)&stringBuffer[0]; } valueTarget = values.freeze().template castTo(); } else { SwitchTypeCodeForTemplatedCall(valueType, getValueFromBuffer, (valueTarget, pValueBuffer, nElements)); } } /** * Set scalar value into given database buffer * * @param valueSource the value to put into the buffer * @param pValueBuffer the database buffer to put it in * @param pDbChannel the db channel */ void IOCSource::setValueInBuffer(const Value& valueSource, char* pValueBuffer, dbChannel* pDbChannel) { auto valueType(valueSource.type()); if (valueType == TypeCode::String) { setStringValueInBuffer(valueSource, pValueBuffer); } else if (valueType == TypeCode::Any || valueType == TypeCode::Union) { SwitchTypeCodeForTemplatedCall(fromDbrType(dbChannelFinalFieldType(pDbChannel)), setValueInBuffer, (valueSource, pValueBuffer)); } else { SwitchTypeCodeForTemplatedCall(valueType, setValueInBuffer, (valueSource, pValueBuffer)); } } /** * Set an array value in the given buffer * * @param valueSource the value to put into the buffer * @param pValueBuffer the database buffer to put it in * @param nElements the number of elements to put into the buffer */ void IOCSource::setValueInBuffer(const Value& valueSource, char* pValueBuffer, long nElements) { auto valueType(valueSource.type()); if (valueType == TypeCode::StringA) { auto sharedValueArray = valueSource.as>(); for (auto i = 0u; i < sharedValueArray.size(); i++, pValueBuffer += MAX_STRING_SIZE) { setStringValueInBuffer(sharedValueArray[i], pValueBuffer); } } else { SwitchTypeCodeForTemplatedCall(valueType, setValueInBuffer, (valueSource, pValueBuffer, nElements)); } } /** * Given a string value source, and a buffer, copy the string contents into the buffer up to the MAX_STRING_SIZE. * Null terminate the string before exiting. * * @param valueSource the string value source * @param pValueBuffer the buffer to copy the string contents to */ void IOCSource::setStringValueInBuffer(const Value& valueSource, char* pValueBuffer) { auto stringValue = valueSource.as(); auto len = std::min(stringValue.length(), (size_t)MAX_STRING_SIZE - 1); stringValue.copy(pValueBuffer, len); pValueBuffer[len] = '\0'; } /** * Set the value field of the given return value to an array of scalars pointed to by pValueBuffer * Supported types are: * TypeCode::Int8 TypeCode::UInt8 * TypeCode::Int16 TypeCode::UInt16 * TypeCode::Int32 TypeCode::UInt32 * TypeCode::Int64 TypeCode::UInt64 * TypeCode::Float32 TypeCode::Float64 * * @tparam valueType the type of the scalars stored in this array. One of the supported types * @param valueTarget the return value * @param pValueBuffer the pointer to the data containing the database data to store in the return value * @param nElements the number of elements in the array */ template void IOCSource::getValueFromBuffer(Value& valueTarget, const void* pValueBuffer, const long& nElements) { shared_array values(nElements); for (auto i = 0; i < nElements; i++) { values[i] = ((valueType*)pValueBuffer)[i]; } valueTarget = values.freeze().template castTo(); } /** * Get the value into the given database value buffer (templated) * * @tparam valueType the type of the scalars stored in this array. One of the supported types * @param valueSource the value to put into the buffer * @param pValueBuffer the database buffer to put it in * @param nElements the number of elements to put into the buffer */ template void IOCSource::setValueInBuffer(const Value& valueSource, void* pValueBuffer, long nElements) { auto valueArray = valueSource.as>(); for (auto i = 0; i < nElements; i++) { ((valueType*)pValueBuffer)[i] = valueArray[i]; } } /** * Utility function to get the TypeCode that the given database channel is configured for * * @param pDbChannel the pointer to the database channel to get the TypeCode for * @param errOnLinks determines whether to throw an error on finding links, default no * @return the TypeCode that the channel is configured for */ TypeCode IOCSource::getChannelValueType(const dbChannel* pDbChannel, const bool errOnLinks) { auto dbChannel(pDbChannel); short dbrType(dbChannelFinalFieldType(dbChannel)); auto nFinalElements(dbChannelFinalElements(dbChannel)); auto nElements(dbChannelElements(dbChannel)); TypeCode valueType; if (dbChannelFieldType(dbChannel) == DBF_STRING && nElements == 1 && dbrType && nFinalElements > 1) { // single character long DBF_STRING being cast to DBF_CHAR array. valueType = TypeCode::String; } else { if (dbrType == DBF_INLINK || dbrType == DBF_OUTLINK || dbrType == DBF_FWDLINK) { if (errOnLinks) { throw std::runtime_error("Link fields not allowed in this context"); } else { // Handle as chars and fail later dbrType = DBF_CHAR; } } valueType = fromDbfType(dbfType(dbrType)); if (valueType != TypeCode::Null && nFinalElements != 1) { valueType = valueType.arrayOf(); } } return valueType; } /** * Get Metadata from the given buffer into the provided value object. The options parameter is used * to select the metadata to retrieve. It must always be retrieved in the specified order * as it is laid out that way by the db subsystems on retrieval. * * @param value the value object to retrieve the metadata into * @param pValueBuffer the db value buffer retrieved from the db subsystem * @param options the options parameter used to select the metadata. */ void IOCSource::getMetadata(Value& value, void*& pValueBuffer, const uint32_t& requestedOptions, const uint32_t& actualOptions) { if (requestedOptions) { // Temporary variable to store metadata while retrieving it Metadata metadata; // Alarm if (requestedOptions & DBR_STATUS) { get4MetadataFields(pValueBuffer, uint16_t, metadata.metadata.status, metadata.metadata.severity, metadata.metadata.acks, metadata.metadata.ackt); if (actualOptions & DBR_STATUS) { checkedSetField(metadata.metadata.status, alarm.status); checkedSetField(metadata.metadata.severity, alarm.severity); checkedSetField(metadata.metadata.acks, alarm.acks); checkedSetField(metadata.metadata.ackt, alarm.ackt); } } // Alarm message if (requestedOptions & DBR_AMSG) { getMetadataString(pValueBuffer, metadata.metadata.amsg); if (actualOptions & DBR_AMSG) { checkedSetStringField(metadata.metadata.amsg, alarm.message); } } // Units if (requestedOptions & DBR_UNITS) { getMetadataBuffer(pValueBuffer, const char, metadata.pUnits, DB_UNITS_SIZE); if (actualOptions & DBR_UNITS && value["display"]) { checkedSetStringField(metadata.pUnits, display.units); } } // Precision if (requestedOptions & DBR_PRECISION) { getMetadataBuffer(pValueBuffer, const dbr_precision, metadata.pPrecision, dbr_precision_size); if (actualOptions & DBR_PRECISION && value["display"]) { checkedSetField(metadata.pPrecision->precision.dp, display.precision); } } // Time if (requestedOptions & DBR_TIME) { get2MetadataFields(pValueBuffer, uint32_t, metadata.metadata.time.secPastEpoch, metadata.metadata.time.nsec); if (actualOptions & DBR_TIME) { checkedSetField(metadata.metadata.time.secPastEpoch + POSIX_TIME_AT_EPICS_EPOCH, timeStamp.secondsPastEpoch); checkedSetField(metadata.metadata.time.nsec, timeStamp.nanoseconds); } } // User tag if (requestedOptions & DBR_UTAG) { getMetadataField(pValueBuffer, uint64_t, metadata.metadata.utag); if (actualOptions & DBR_UTAG) { checkedSetField(metadata.metadata.utag, timeStamp.userTag); } } // Enum strings if (requestedOptions & DBR_ENUM_STRS) { getMetadataBuffer(pValueBuffer, const dbr_enumStrs, metadata.enumStrings, dbr_enumStrs_size); if (actualOptions & DBR_ENUM_STRS && value["value.choices"] && metadata.enumStrings) { shared_array choices(metadata.enumStrings->no_str); for (epicsUInt32 i = 0; i < metadata.enumStrings->no_str; i++) { choices[i] = metadata.enumStrings->strs[i]; } value["value.choices"] = choices.freeze().castTo(); } } // Display long if (requestedOptions & DBR_GR_LONG) { getMetadataBuffer(pValueBuffer, const struct dbr_grLong, metadata.graphicsLong, dbr_grLong_size); if (actualOptions & DBR_GR_LONG && value["display"]) { checkedSetLongField(metadata.graphicsLong->lower_disp_limit, display.limitLow); checkedSetLongField(metadata.graphicsLong->upper_disp_limit, display.limitHigh); } } // Display double if (requestedOptions & DBR_GR_DOUBLE) { getMetadataBuffer(pValueBuffer, const struct dbr_grDouble, metadata.graphicsDouble, dbr_grDouble_size); if (actualOptions & DBR_GR_DOUBLE && value["display"]) { checkedSetDoubleField(metadata.graphicsDouble->lower_disp_limit, display.limitLow); checkedSetDoubleField(metadata.graphicsDouble->upper_disp_limit, display.limitHigh); } } // Control long if (requestedOptions & DBR_CTRL_LONG) { getMetadataBuffer(pValueBuffer, const struct dbr_ctrlLong, metadata.controlLong, dbr_ctrlLong_size); if (actualOptions & DBR_CTRL_LONG && value["control"]) { checkedSetLongField(metadata.controlLong->lower_ctrl_limit, control.limitLow); checkedSetLongField(metadata.controlLong->upper_ctrl_limit, control.limitHigh); } } // Control double if (requestedOptions & DBR_CTRL_DOUBLE) { getMetadataBuffer(pValueBuffer, const struct dbr_ctrlDouble, metadata.controlDouble, dbr_ctrlDouble_size); if (actualOptions & DBR_CTRL_DOUBLE && value["control"]) { checkedSetDoubleField(metadata.controlDouble->lower_ctrl_limit, control.limitLow); checkedSetDoubleField(metadata.controlDouble->upper_ctrl_limit, control.limitHigh); } } // Alarm long if (requestedOptions & DBR_AL_LONG) { getMetadataBuffer(pValueBuffer, const struct dbr_alLong, metadata.alarmLong, dbr_alLong_size); if (actualOptions & DBR_AL_LONG && value["valueAlarm"]) { checkedSetLongField(metadata.alarmLong->lower_alarm_limit, valueAlarm.lowAlarmLimit); checkedSetLongField(metadata.alarmLong->lower_warning_limit, valueAlarm.lowWarningLimit); checkedSetLongField(metadata.alarmLong->upper_warning_limit, valueAlarm.highWarningLimit); checkedSetLongField(metadata.alarmLong->upper_alarm_limit, valueAlarm.highAlarmLimit); } } // Alarm double if (requestedOptions & DBR_AL_DOUBLE) { getMetadataBuffer(pValueBuffer, const struct dbr_alDouble, metadata.alarmDouble, dbr_alDouble_size); if (actualOptions & DBR_AL_DOUBLE && value["valueAlarm"]) { checkedSetDoubleField(metadata.alarmDouble->lower_alarm_limit, valueAlarm.lowAlarmLimit); checkedSetDoubleField(metadata.alarmDouble->lower_warning_limit, valueAlarm.lowWarningLimit); checkedSetDoubleField(metadata.alarmDouble->upper_warning_limit, valueAlarm.highWarningLimit); checkedSetDoubleField(metadata.alarmDouble->upper_alarm_limit, valueAlarm.highAlarmLimit); } } } } } // pvxs } // ioc