/// Load a db file multiple times substituting a specified macro according to a number range. /// /// The \a dbFile and \a macros arguments are like the normal dbLoadRecords() however /// it is possible to embed a macro within these whose value follows the range \a start to \a stop. /// You can either load the same \a dbFile multiple times with different macros, or even load /// different database files by using \a loopVar as part of the filename. If you want to use a list /// of (non-numeric) substitutions rather than an integer range see dbLoadRecordsList() /// /// The name of the macro to be used for substitution is contained in \a loopVar and needs to be /// reference in an \\ escaped way to make sure EPICS does not try to substitute it too soon. /// as well as the \a macros the \a dbFile is also passed the \a loopVar macro value /// @code /// dbLoadRecordsLoop("file\$(I).db", "P=1,Q=Hello\$(I)", "I", 1, 4) /// @endcode /// /// @param[in] dbFile @copydoc dbLoadRecordsLoopInitArg0 /// @param[in] macros @copydoc dbLoadRecordsLoopInitArg1 /// @param[in] loopVar @copydoc dbLoadRecordsLoopInitArg2 /// @param[in] start @copydoc dbLoadRecordsLoopInitArg3 /// @param[in] stop @copydoc dbLoadRecordsLoopInitArg4 /// @param[in] step @copydoc dbLoadRecordsLoopInitArg5 epicsShareFunc void dbLoadRecordsLoop(const char* dbFile, const char* macros, const char* loopVar, int start, int stop, int step) { char loopVal[32]; if (loopVar == NULL) { dbLoadRecords(dbFile, macros); return; } if (step <= 0) { step = 1; } std::string macros_s, dbFile_s; subMacros(macros_s, macros, loopVar); subMacros(dbFile_s, dbFile, loopVar); MAC_HANDLE* mh = NULL; char macros_exp[1024], dbFile_exp[1024]; macCreateHandle(&mh, NULL); loadMacEnviron(mh); for(int i = start; i <= stop; i += step) { macPushScope(mh); epicsSnprintf(loopVal, sizeof(loopVal), "%d", i); macPutValue(mh, loopVar, loopVal); macExpandString(mh, macros_s.c_str(), macros_exp, sizeof(macros_exp)); macExpandString(mh, dbFile_s.c_str(), dbFile_exp, sizeof(dbFile_exp)); std::ostringstream new_macros; new_macros << macros_exp << (strlen(macros_exp) > 0 ? "," : "") << loopVar << "=" << i; std::cout << "--> (" << i << ") dbLoadRecords(\"" << dbFile_exp << "\",\"" << new_macros.str() << "\")" << std::endl; dbLoadRecords(dbFile_exp, new_macros.str().c_str()); macPopScope(mh); } macDeleteHandle(mh); }
/// Load a db file multiple times according to a list of items separated by known separator(s). /// /// The \a dbFile and \a macros arguments are like the normal dbLoadRecords() however /// it is possible to embed a macro within these whose value takes a value from the \a list. /// You can either load the same \a dbFile multiple times with different macros, or even load /// different database files by using \a loopVar as part of the filename. If you want to use a /// pure numeric range see dbLoadRecordsLoop() /// /// The name of the macro to be used for substitution is contained in \a loopVar and needs to be /// reference in an \\ escaped way to make sure EPICS does not try to substitute it too soon. /// as well as the \a macros the \a dbFile is also passed the \a loopVar macro value /// @code /// dbLoadRecordsList("file\$(S).db", "P=1,Q=Hello\$(S)", "S", "A;B;C", ";") /// @endcode /// /// @param[in] dbFile @copydoc dbLoadRecordsListInitArg0 /// @param[in] macros @copydoc dbLoadRecordsListInitArg1 /// @param[in] loopVar @copydoc dbLoadRecordsListInitArg2 /// @param[in] list @copydoc dbLoadRecordsListInitArg3 /// @param[in] sep @copydoc dbLoadRecordsListInitArg4 epicsShareFunc void dbLoadRecordsList(const char* dbFile, const char* macros, const char* loopVar, const char* list, const char* sep) { static const char* default_sep = ";"; if (loopVar == NULL || list == NULL) { dbLoadRecords(dbFile, macros); return; } if (sep == NULL) { sep = default_sep; } std::string macros_s, dbFile_s; subMacros(macros_s, macros, loopVar); subMacros(dbFile_s, dbFile, loopVar); MAC_HANDLE* mh = NULL; char macros_exp[1024], dbFile_exp[1024]; macCreateHandle(&mh, NULL); loadMacEnviron(mh); char* saveptr = NULL; char* list_tmp = strdup(list); char* list_item = epicsStrtok_r(list_tmp, sep, &saveptr); while(list_item != NULL) { macPushScope(mh); macPutValue(mh, loopVar, list_item); macExpandString(mh, macros_s.c_str(), macros_exp, sizeof(macros_exp)); macExpandString(mh, dbFile_s.c_str(), dbFile_exp, sizeof(dbFile_exp)); std::ostringstream new_macros; new_macros << macros_exp << (strlen(macros_exp) > 0 ? "," : "") << loopVar << "=" << list_item; std::cout << "--> (" << list_item << ") dbLoadRecords(\"" << dbFile_exp << "\",\"" << new_macros.str() << "\")" << std::endl; dbLoadRecords(dbFile_exp, new_macros.str().c_str()); list_item = epicsStrtok_r(NULL, sep, &saveptr); macPopScope(mh); } free(list_tmp); macDeleteHandle(mh); }
int main(int argc,char **argv) { void *inputPvt; MAC_HANDLE *macPvt; char *pval; int narg; char *substitutionName=0; char *templateName=0; int i; inputConstruct(&inputPvt); macCreateHandle(&macPvt,0); macSuppressWarning(macPvt,1); while((argc>1) && (argv[1][0] == '-')) { narg = (strlen(argv[1])==2) ? 2 : 1; pval = (narg==1) ? (argv[1]+2) : argv[2]; if(strncmp(argv[1],"-I",2)==0) { inputAddPath(inputPvt,pval); } else if(strncmp(argv[1],"-o",2)==0) { if(freopen(pval,"w",stdout)==NULL) { fprintf(stderr,"Can't open %s for writing: %s\n", pval, strerror(errno)); exit(1); } } else if(strncmp(argv[1],"-M",2)==0) { addMacroReplacements(macPvt,pval); } else if(strncmp(argv[1],"-S",2)==0) { substitutionName = calloc(strlen(pval)+1,sizeof(char)); strcpy(substitutionName,pval); } else if(strncmp(argv[1],"-V",2)==0) { macSuppressWarning(macPvt,0); narg = 1; /* no argument for this option */ } else { usageExit(); } argc -= narg; for(i=1; i<argc; i++) argv[i] = argv[i + narg]; } if(argc>2) { fprintf(stderr,"too many filename arguments\n"); usageExit(); } if(argc==2) { templateName = calloc(strlen(argv[1])+1,sizeof(char)); strcpy(templateName,argv[1]); } if(!substitutionName) { makeSubstitutions(inputPvt,macPvt,templateName); } else { void *substitutePvt; char *filename = 0; substituteOpen(&substitutePvt,substitutionName); while(substituteGetNextSet(substitutePvt,&filename)) { if(templateName) filename = templateName; if(!filename) { fprintf(stderr,"no template file\n"); usageExit(); } macPushScope(macPvt); while((pval = substituteGetReplacements(substitutePvt))){ addMacroReplacements(macPvt,pval); makeSubstitutions(inputPvt,macPvt,filename); } macPopScope(macPvt); } substituteDestruct(substitutePvt); } inputDestruct(inputPvt); free((void *)templateName); free((void *)substitutionName); return(exitStatus); }
static long dbReadCOM(DBBASE **ppdbbase,const char *filename, FILE *fp, const char *path,const char *substitutions) { long status; inputFile *pinputFile = NULL; char *penv; char **macPairs; if(*ppdbbase == 0) *ppdbbase = dbAllocBase(); pdbbase = *ppdbbase; if(path && strlen(path)>0) { dbPath(pdbbase,path); } else { penv = getenv("EPICS_DB_INCLUDE_PATH"); if(penv) { dbPath(pdbbase,penv); } else { dbPath(pdbbase,"."); } } my_buffer = dbCalloc(MY_BUFFER_SIZE,sizeof(char)); freeListInitPvt(&freeListPvt,sizeof(tempListNode),100); if(substitutions) { if(macCreateHandle(&macHandle,NULL)) { epicsPrintf("macCreateHandle error\n"); status = -1; goto cleanup; } macParseDefns(macHandle,(char *)substitutions,&macPairs); if(macPairs ==NULL) { macDeleteHandle(macHandle); macHandle = NULL; } else { macInstallMacros(macHandle,macPairs); free((void *)macPairs); mac_input_buffer = dbCalloc(MY_BUFFER_SIZE,sizeof(char)); } } pinputFile = dbCalloc(1,sizeof(inputFile)); if(filename) { pinputFile->filename = macEnvExpand(filename); } if(!fp) { FILE *fp1; if(pinputFile->filename) pinputFile->path = dbOpenFile(pdbbase,pinputFile->filename,&fp1); if(!pinputFile->filename || !fp1) { errPrintf(0,__FILE__, __LINE__, "dbRead opening file %s",pinputFile->filename); free((void *)pinputFile->filename); free((void *)pinputFile); status = -1; goto cleanup; } pinputFile->fp = fp1; } else { pinputFile->fp = fp; } pinputFile->line_num = 0; pinputFileNow = pinputFile; my_buffer[0] = '\0'; my_buffer_ptr = my_buffer; ellAdd(&inputFileList,&pinputFile->node); status = pvt_yy_parse(); dbFreePath(pdbbase); if(!status) { /*add RTYP and VERS as an attribute */ DBENTRY dbEntry; DBENTRY *pdbEntry = &dbEntry; long localStatus; dbInitEntry(pdbbase,pdbEntry); localStatus = dbFirstRecordType(pdbEntry); while(!localStatus) { localStatus = dbPutRecordAttribute(pdbEntry,"RTYP", dbGetRecordTypeName(pdbEntry)); if(!localStatus) { localStatus = dbPutRecordAttribute(pdbEntry,"VERS", "none specified"); } if(localStatus) { fprintf(stderr,"dbPutRecordAttribute status %ld\n",status); } else { localStatus = dbNextRecordType(pdbEntry); } } dbFinishEntry(pdbEntry); } cleanup: if(macHandle) macDeleteHandle(macHandle); macHandle = NULL; if(mac_input_buffer) free((void *)mac_input_buffer); mac_input_buffer = NULL; if(freeListPvt) freeListCleanup(freeListPvt); freeListPvt = NULL; if(my_buffer) free((void *)my_buffer); my_buffer = NULL; freeInputFileList(); return(status); }
/// \param[in] configSection @copydoc initArg1 /// \param[in] configFile @copydoc initArg2 /// \param[in] host @copydoc initArg3 /// \param[in] options @copydoc initArg4 /// \param[in] progid @copydoc initArg5 /// \param[in] username @copydoc initArg6 /// \param[in] password @copydoc initArg7 lvDCOMInterface::lvDCOMInterface(const char *configSection, const char* configFile, const char* host, int options, const char* progid, const char* username, const char* password) : m_configSection(configSection), m_pidentity(NULL), m_pxmldom(NULL), m_options(options), m_progid(progid != NULL? progid : ""), m_username(username != NULL? username : ""), m_password(password != NULL ? password : ""), m_mac_env(NULL) { epicsThreadOnce(&onceId, initCOM, NULL); if (host != NULL && host[0] != '\0') { m_host = host; } else { // char name_buffer[MAX_COMPUTERNAME_LENGTH + 1]; // DWORD name_size = MAX_COMPUTERNAME_LENGTH + 1; // if ( GetComputerNameEx(ComputerNameNetBIOS, name_buffer, &name_size) != 0 ) // { // m_host = name_buffer; // } // else // { // m_host = "localhost"; // } m_host = "localhost"; } if (macCreateHandle(&m_mac_env, NULL) != 0) { throw std::runtime_error("Cannot create mac handle"); } // load current environment into m_mac_env, this is so we can create a macEnvExpand() equivalent // but tied to the environment at a specific time. It is useful if we want to load the same // XML file twice but with a macro defined differently in each case for(char** cp = environ; *cp != NULL; ++cp) { char* str_tmp = strdup(*cp); char* equals_loc = strchr(str_tmp, '='); // split name=value string if (equals_loc != NULL) { *equals_loc = '\0'; macPutValue(m_mac_env, str_tmp, equals_loc + 1); } free(str_tmp); } // m_doc = new TiXmlDocument; // if ( !m_doc->LoadFile(configFile) ) // { // delete m_doc; // m_doc = NULL; // throw std::runtime_error("Cannot load " + std::string(configFile) + ": load failure"); // } // m_root = m_doc->RootElement(); DomFromCOM(); short sResult = FALSE; char* configFile_expanded = envExpand(configFile); m_configFile = configFile_expanded; HRESULT hr = m_pxmldom->load(_variant_t(configFile_expanded), &sResult); free(configFile_expanded); if(FAILED(hr)) { throw std::runtime_error("Cannot load XML \"" + m_configFile + "\" (expanded from \"" + std::string(configFile) + "\"): load failure"); } if (sResult != VARIANT_TRUE) { throw std::runtime_error("Cannot load XML \"" + m_configFile + "\" (expanded from \"" + std::string(configFile) + "\"): load failure"); } std::cerr << "Loaded XML config file \"" << m_configFile << "\" (expanded from \"" << configFile << "\")" << std::endl; m_extint = doPath("/lvinput/extint/@path").c_str(); epicsAtExit(epicsExitFunc, this); if (m_progid.size() > 0) { if ( CLSIDFromProgID(CT2W(m_progid.c_str()), &m_clsid) != S_OK ) { throw std::runtime_error("Cannot find progId " + m_progid); } } else { m_clsid = LabVIEW::CLSID_Application; wchar_t* progid_str = NULL; if ( ProgIDFromCLSID(m_clsid, &progid_str) == S_OK ) { m_progid = CW2CT(progid_str); CoTaskMemFree(progid_str); } else { m_progid = "LabVIEW.Application"; } } wchar_t* clsid_str = NULL; if ( StringFromCLSID(m_clsid, &clsid_str) == S_OK ) { std::cerr << "Using ProgID \"" << m_progid << "\" CLSID " << CW2CT(clsid_str) << std::endl; CoTaskMemFree(clsid_str); } else { std::cerr << "Using ProgID \"" << m_progid << "\" but StringFromCLSID() failed" << std::endl; } }
/** Create this driver's NDAttributeList (pAttributeList) by reading an XML file * This clears any existing attributes from this drivers' NDAttributeList and then creates a new list * based on the XML file. These attributes can then be associated with an NDArray by calling asynNDArrayDriver::getAttributes() * passing it pNDArray->pAttributeList. * * The following simple example XML file illustrates the way that both PVAttribute and paramAttribute attributes are defined. * <pre> * <?xml version="1.0" standalone="no" ?> * \<Attributes> * \<Attribute name="AcquireTime" type="EPICS_PV" source="13SIM1:cam1:AcquireTime" dbrtype="DBR_NATIVE" description="Camera acquire time"/> * \<Attribute name="CameraManufacturer" type="PARAM" source="MANUFACTURER" datatype="STRING" description="Camera manufacturer"/> * \</Attributes> * </pre> * Each NDAttribute (currently either an PVAttribute or paramAttribute, but other types may be added in the future) * is defined with an XML <b>Attribute</b> tag. For each attribute there are a number of XML attributes * (unfortunately there are 2 meanings of attribute here: the NDAttribute and the XML attribute). * XML attributes have the syntax name="value". The XML attribute names are case-sensitive and must be lower case, i.e. name="xxx", not NAME="xxx". * The XML attribute values are specified by the XML Schema and are always uppercase for <b>datatype</b> and <b>dbrtype</b> attributes. * The XML attribute names are listed here: * * <b>name</b> determines the name of the NDAttribute. It is required, must be unique, is case-insensitive, * and must start with a letter. It can include only letters, numbers and underscore. (No whitespace or other punctuation.) * * <b>type</b> determines the type of the NDAttribute. "EPICS_PV" creates a PVAttribute, while "PARAM" creates a paramAttribute. * The default is EPICS_PV if this XML attribute is absent. * * <b>source</b> determines the source of the NDAttribute. It is required. If type="EPICS_PV" then this is the name of the EPICS PV, which is * case-sensitive. If type="PARAM" then this is the drvInfo string that is used in EPICS database files (e.g. ADBase.template) to identify * this parameter. * * <b>dbrtype</b> determines the data type that will be used to read an EPICS_PV value with channel access. It can be one of the standard EPICS * DBR types (e.g. "DBR_DOUBLE", "DBR_STRING", ...) or it can be the special type "DBR_NATIVE" which means to use the native channel access * data type for this PV. The default is DBR_NATIVE if this XML attribute is absent. Always use uppercase. * * <b>datatype</b> determines the parameter data type for type="PARAM". It must match the actual data type in the driver or plugin * parameter library, and must be "INT", "DOUBLE", or "STRING". The default is "INT" if this XML attribute is absent. Always use uppercase. * * <b>addr</b> determines the asyn addr (address) for type="PARAM". The default is 0 if the XML attribute is absent. * * <b>description</b> determines the description for this attribute. It is not required, and the default is a NULL string. * */ asynStatus asynNDArrayDriver::readNDAttributesFile() { const char *pName, *pSource, *pAttrType, *pDescription; xmlDocPtr doc; xmlNode *Attr, *Attrs; std::ostringstream buff; std::string buffer; std::ifstream infile; std::string attributesMacros; std::string fileName; MAC_HANDLE *macHandle; char **macPairs; int bufferSize; char *tmpBuffer = 0; int status; static const char *functionName = "readNDAttributesFile"; getStringParam(NDAttributesFile, fileName); getStringParam(NDAttributesMacros, attributesMacros); /* Clear any existing attributes */ this->pAttributeList->clear(); if (fileName.length() == 0) return asynSuccess; infile.open(fileName.c_str()); if (infile.fail()) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s::%s error opening file %s\n", driverName, functionName, fileName.c_str()); setIntegerParam(NDAttributesStatus, NDAttributesFileNotFound); return asynError; } buff << infile.rdbuf(); buffer = buff.str(); // We now have file in memory. Do macro substitution if required if (attributesMacros.length() > 0) { macCreateHandle(&macHandle, 0); status = macParseDefns(macHandle, attributesMacros.c_str(), &macPairs); if (status < 0) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s::%s, error parsing macros\n", driverName, functionName); goto done_macros; } status = macInstallMacros(macHandle, macPairs); if (status < 0) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s::%s, error installed macros\n", driverName, functionName); goto done_macros; } // Create a temporary buffer 10 times larger than input buffer bufferSize = (int)(buffer.length() * 10); tmpBuffer = (char *)malloc(bufferSize); status = macExpandString(macHandle, buffer.c_str(), tmpBuffer, bufferSize); // NOTE: There is a bug in macExpandString up to 3.14.12.6 and 3.15.5 so that it does not return <0 // if there is an undefined macro which is not the last macro in the string. // We work around this by testing also if the returned string contains ",undefined)". This is // unlikely to occur otherwise. Eventually we can remove this test. if ((status < 0) || strstr(tmpBuffer, ",undefined)")) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s::%s, error expanding macros\n", driverName, functionName); goto done_macros; } if (status >= bufferSize) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s::%s, error macro buffer too small\n", driverName, functionName); goto done_macros; } buffer = tmpBuffer; done_macros: macDeleteHandle(macHandle); free(tmpBuffer); if (status < 0) { setIntegerParam(NDAttributesStatus, NDAttributesMacroError); return asynError; } } // Assume failure setIntegerParam(NDAttributesStatus, NDAttributesXMLSyntaxError); doc = xmlReadMemory(buffer.c_str(), (int)buffer.length(), "noname.xml", NULL, 0); if (doc == NULL) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: error creating doc\n", driverName, functionName); return asynError; } Attrs = xmlDocGetRootElement(doc); if ((!xmlStrEqual(Attrs->name, (const xmlChar *)"Attributes"))) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: cannot find Attributes element\n", driverName, functionName); return asynError; } for (Attr = xmlFirstElementChild(Attrs); Attr; Attr = xmlNextElementSibling(Attr)) { pName = (const char *)xmlGetProp(Attr, (const xmlChar *)"name"); if (!pName) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: name attribute not found\n", driverName, functionName); return asynError; } pDescription = (const char *)xmlGetProp(Attr, (const xmlChar *)"description"); if (!pDescription) pDescription = ""; pSource = (const char *)xmlGetProp(Attr, (const xmlChar *)"source"); if (!pSource) { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: source attribute not found for attribute %s\n", driverName, functionName, pName); return asynError; } pAttrType = (const char *)xmlGetProp(Attr, (const xmlChar *)"type"); if (!pAttrType) pAttrType = NDAttribute::attrSourceString(NDAttrSourceEPICSPV); if (strcmp(pAttrType, NDAttribute::attrSourceString(NDAttrSourceEPICSPV)) == 0) { const char *pDBRType = (const char *)xmlGetProp(Attr, (const xmlChar *)"dbrtype"); int dbrType = DBR_NATIVE; if (pDBRType) { if (!strcmp(pDBRType, "DBR_CHAR")) dbrType = DBR_CHAR; else if (!strcmp(pDBRType, "DBR_SHORT")) dbrType = DBR_SHORT; else if (!strcmp(pDBRType, "DBR_ENUM")) dbrType = DBR_ENUM; else if (!strcmp(pDBRType, "DBR_INT")) dbrType = DBR_INT; else if (!strcmp(pDBRType, "DBR_LONG")) dbrType = DBR_LONG; else if (!strcmp(pDBRType, "DBR_FLOAT")) dbrType = DBR_FLOAT; else if (!strcmp(pDBRType, "DBR_DOUBLE")) dbrType = DBR_DOUBLE; else if (!strcmp(pDBRType, "DBR_STRING")) dbrType = DBR_STRING; else if (!strcmp(pDBRType, "DBR_NATIVE")) dbrType = DBR_NATIVE; else { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: unknown dbrType = %s for attribute %s\n", driverName, functionName, pDBRType, pName); return asynError; } } asynPrint(pasynUserSelf, ASYN_TRACEIO_DRIVER, "%s:%s: Name=%s, PVName=%s, pDBRType=%s, dbrType=%d, pDescription=%s\n", driverName, functionName, pName, pSource, pDBRType, dbrType, pDescription); #ifndef EPICS_LIBCOM_ONLY PVAttribute *pPVAttribute = new PVAttribute(pName, pDescription, pSource, dbrType); this->pAttributeList->add(pPVAttribute); #endif } else if (strcmp(pAttrType, NDAttribute::attrSourceString(NDAttrSourceParam)) == 0) { const char *pDataType = (const char *)xmlGetProp(Attr, (const xmlChar *)"datatype"); if (!pDataType) pDataType = "int"; const char *pAddr = (const char *)xmlGetProp(Attr, (const xmlChar *)"addr"); int addr=0; if (pAddr) addr = strtol(pAddr, NULL, 0); asynPrint(pasynUserSelf, ASYN_TRACEIO_DRIVER, "%s:%s: Name=%s, drvInfo=%s, dataType=%s,pDescription=%s\n", driverName, functionName, pName, pSource, pDataType, pDescription); paramAttribute *pParamAttribute = new paramAttribute(pName, pDescription, pSource, addr, this, pDataType); this->pAttributeList->add(pParamAttribute); } else if (strcmp(pAttrType, NDAttribute::attrSourceString(NDAttrSourceFunct)) == 0) { const char *pParam = (const char *)xmlGetProp(Attr, (const xmlChar *)"param"); if (!pParam) pParam = epicsStrDup(""); asynPrint(pasynUserSelf, ASYN_TRACEIO_DRIVER, "%s:%s: Name=%s, function=%s, pParam=%s, pDescription=%s\n", driverName, functionName, pName, pSource, pParam, pDescription); #ifndef EPICS_LIBCOM_ONLY functAttribute *pFunctAttribute = new functAttribute(pName, pDescription, pSource, pParam); this->pAttributeList->add(pFunctAttribute); #endif } else { asynPrint(pasynUserSelf, ASYN_TRACE_ERROR, "%s:%s: unknown attribute type = %s for attribute %s\n", driverName, functionName, pAttrType, pName); return asynError; } } setIntegerParam(NDAttributesStatus, NDAttributesOK); // Wait a short while for channel access callbacks on EPICS PVs epicsThreadSleep(0.5); // Get the initial values this->pAttributeList->updateValues(); return asynSuccess; }