[카테고리:] 생활코딩

  • open62541 browse

    open62541이 OPC UA 규격에 맞춰 browse 기능을 지원한다. OPC UA 문서에 어떻게 사용하는지 알 수 있는데, 멤버만 볼 수 있다. 일단 대충 필요한 실린더를 구성했다 하자. 상용 PLC 메이커가 OPC UA 기능을 구현한다면 내부에 server 기능으로 넣을 듯 하다. 지금 구할 수 없으니, 대충 비슷하게 만들고 싶다. 여기서 문제를 알았다. 한번 Variable을 할당하면 어떻게 바꾸지? callback으로 바꿀 수 있는데, nodeId를 알아야 한다. nodeId를 null로 설정하면 server가 임의로 할당한다.

    아래 그림 NodeID i=50235, Status Value를 True로 만들고 싶다. 어떻게??

    이럴때 browse를 사용한다.(아마도..) sample로 구현되어 있는데, example 디렉토리 tutorial_server_object.c에 있다. 가장 간단한 방법이 아래와 같다.

    #define TEST
    #ifdef TEST
    
    	//테스트
    	//각 설비값을 설정하는 부분
    	UA_Variant tmp_value;
    	bool bitoff = 0;
    	UA_Variant_setScalar(&tmp_value, &bitoff, &UA_TYPES[UA_TYPES_BOOLEAN]);
    	UA_NodeId tmpNodeId = UA_NODEID_STRING(1, "Status");
    	UA_Server_writeValue(server, tmpNodeId, tmp_value);
    
        /* Find the NodeId of the status child variable */
        UA_RelativePathElement rpe;
        UA_RelativePathElement_init(&rpe);
        rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
        rpe.isInverse = false;
        rpe.includeSubtypes = true;
        //rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
    	rpe.targetName = UA_QUALIFIEDNAME(1, "latch");
    	//rpe.targetName = UA_QUALIFIEDNAME(1, "variable");
    
    
        UA_BrowsePath bp;
        UA_BrowsePath_init(&bp);
    	UA_NodeId *nodeId  = UA_NodeId_new();
    	*nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    	//UA_NodeId tmp = {0, UA_NODEIDTYPE_NUMERIC, {50238}};
    	//printf("\nNode id is %d\n", tmp);
    	//UA_NodeId *nodeId = &tmp;
        bp.startingNode = *nodeId;
        bp.relativePath.elementsSize = 1;
        bp.relativePath.elements = &rpe;
    
        UA_BrowsePathResult bpr =
            UA_Server_translateBrowsePathToNodeIds(server, &bp);
        if(bpr.statusCode != UA_STATUSCODE_GOOD ||
           bpr.targetsSize < 1)
            return bpr.statusCode;
    
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "found target is %d", bpr.targets->targetId.nodeId.identifier.numeric);
    	//print로 structure를 출력할 수 없음.
    	//printf("\nnode id is %d\n", (UA_NodeId)bpr.targets->targetId.nodeId);
        //* Set the status value */
        UA_Boolean status = true;
        UA_Variant value;
        UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
        UA_Server_writeValue(server, bpr.targets->targetId.nodeId, value);
    	//UA_Server_writeValue(server, tmp, value);
        UA_BrowsePathResult_clear(&bpr);
    #endif

    여기를 gdb로 보면 시작 위치를 설정, 알고 있는 정보를 입력, 두 구조체를 비교하여 찾는다. 찾으면 0을 반환한다.

    450	    UA_RelativePathElement_init(&rpe);
    (gdb) 
    451	    rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
    (gdb) 
    452	    rpe.isInverse = false;
    (gdb) 
    453	    rpe.includeSubtypes = true;
    (gdb) 
    455		rpe.targetName = UA_QUALIFIEDNAME(1, "latch");
    (gdb) 
    460	    UA_BrowsePath_init(&bp);
    (gdb) 
    461		UA_NodeId *nodeId  = UA_NodeId_new();
    (gdb) 
    462		*nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    (gdb) 
    466	    bp.startingNode = *nodeId;
    (gdb) 
    467	    bp.relativePath.elementsSize = 1;
    (gdb) 
    468	    bp.relativePath.elements = &rpe;
    (gdb) 
    471	        UA_Server_translateBrowsePathToNodeIds(server, &bp);
    (gdb) 
    472	    if(bpr.statusCode != UA_STATUSCODE_GOOD ||
    (gdb) display rpe
    1: rpe = {referenceTypeId = {namespaceIndex = 0, identifierType = UA_NODEIDTYPE_NUMERIC, identifier = {numeric = 47, string = {length = 47, 
            data = 0x136b4 <main+692> "b?K\342\003"}, guid = {data1 = 47, data2 = 14004, data3 = 1, data4 = "\257\362\377\276l\362\377\276"}, byteString = {
            length = 47, data = 0x136b4 <main+692> "b?K\342\003"}}}, isInverse = false, includeSubtypes = true, targetName = {namespaceIndex = 1, name = {
          length = 5, data = 0x13bbc "latch"}}}
    (gdb) display bp
    2: bp = {startingNode = {namespaceIndex = 0, identifierType = UA_NODEIDTYPE_NUMERIC, identifier = {numeric = 85, string = {length = 85, data = 0x52a48 ""}, 
          guid = {data1 = 85, data2 = 10824, data3 = 5, data4 = "\024\361\377\276\234\016\001"}, byteString = {length = 85, data = 0x52a48 ""}}}, 
      relativePath = {elementsSize = 1, elements = 0xbefff26c}}
    (gdb) display bpr
    3: bpr = {statusCode = 0, targetsSize = 1, targets = 0x2c9c8}
    (gdb) display *bpr.targets
    4: *bpr.targets = {targetId = {nodeId = {namespaceIndex = 0, identifierType = UA_NODEIDTYPE_NUMERIC, identifier = {numeric = 50234, string = {
              length = 50234, data = 0x0}, guid = {data1 = 50234, data2 = 0, data3 = 0, data4 = "\000\000\000\000\000\000\000"}, byteString = {length = 50234, 
              data = 0x0}}}, namespaceUri = {length = 0, data = 0x0}, serverIndex = 0}, remainingPathIndex = 4294967295}
    (gdb) n
    473	       bpr.targetsSize < 1)
    1: rpe = {referenceTypeId = {namespaceIndex = 0, identifierType = UA_NODEIDTYPE_NUMERIC, identifier = {numeric = 47, string = {length = 47, 
            data = 0x136b4 <main+692> "b?K\342\003"}, guid = {data1 = 47, data2 = 14004, data3 = 1, data4 = "\257\362\377\276l\362\377\276"}, byteString = {
            length = 47, data = 0x136b4 <main+692> "b?K\342\003"}}}, isInverse = false, includeSubtypes = true, targetName = {namespaceIndex = 1, name = {
          length = 5, data = 0x13bbc "latch"}}}
    2: bp = {startingNode = {namespaceIndex = 0, identifierType = UA_NODEIDTYPE_NUMERIC, identifier = {numeric = 85, string = {length = 85, data = 0x52a48 ""}, 
          guid = {data1 = 85, data2 = 10824, data3 = 5, data4 = "\024\361\377\276\234\016\001"}, byteString = {length = 85, data = 0x52a48 ""}}}, 
      relativePath = {elementsSize = 1, elements = 0xbefff26c}}
    3: bpr = {statusCode = 0, targetsSize = 1, targets = 0x2c9c8}
    4: *bpr.targets = {targetId = {nodeId = {namespaceIndex = 0, identifierType = UA_NODEIDTYPE_NUMERIC, identifier = {numeric = 50234, string = {
              length = 50234, data = 0x0}, guid = {data1 = 50234, data2 = 0, data3 = 0, data4 = "\000\000\000\000\000\000\000"}, byteString = {length = 50234, 
              data = 0x0}}}, namespaceUri = {length = 0, data = 0x0}, serverIndex = 0}, remainingPathIndex = 4294967295}
    

    어디에서 시작할 지 정해줘야 한다. 가장 상위인 Object에서 시작했다. NodeId i=84다. 이 번호도 정했을 것 같다. 문제는 이 설정이 어떻게 되어 있는지 Objects 바로 아래 항목만 찾는다. 위 방식으로 NodeId i=50234까지는 찾았다. 그러나 Varible이 없어 write를 하더라도 효과 없다. 하위 항목인 1:Status, NodeId i=50235를 찾아야 했다. 편법으로 시작점을 50243으로 넣어 줬다.

    #define TEST
    #ifdef TEST
    
    	//테스트
    	//각 설비값을 설정하는 부분
    	UA_Variant tmp_value;
    	bool bitoff = 0;
    	UA_Variant_setScalar(&tmp_value, &bitoff, &UA_TYPES[UA_TYPES_BOOLEAN]);
    	UA_NodeId tmpNodeId = UA_NODEID_STRING(1, "Status");
    	UA_Server_writeValue(server, tmpNodeId, tmp_value);
    
        /* Find the NodeId of the status child variable */
        UA_RelativePathElement rpe;
        UA_RelativePathElement_init(&rpe);
        rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
        rpe.isInverse = false;
        rpe.includeSubtypes = true;
        //rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
    	rpe.targetName = UA_QUALIFIEDNAME(1, "latch");
    	//rpe.targetName = UA_QUALIFIEDNAME(1, "variable");
    
    
        UA_BrowsePath bp;
        UA_BrowsePath_init(&bp);
    	UA_NodeId *nodeId  = UA_NodeId_new();
    	*nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    	//UA_NodeId tmp = {0, UA_NODEIDTYPE_NUMERIC, {50238}};
    	//printf("\nNode id is %d\n", tmp);
    	//UA_NodeId *nodeId = &tmp;
        bp.startingNode = *nodeId;
        bp.relativePath.elementsSize = 1;
        bp.relativePath.elements = &rpe;
    
        UA_BrowsePathResult bpr =
            UA_Server_translateBrowsePathToNodeIds(server, &bp);
        if(bpr.statusCode != UA_STATUSCODE_GOOD ||
           bpr.targetsSize < 1)
            return bpr.statusCode;
    
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "found target is %d", bpr.targets->targetId.nodeId.identifier.numeric);
    	//print로 structure를 출력할 수 없음.
    	//printf("\nnode id is %d\n", (UA_NodeId)bpr.targets->targetId.nodeId);
        //* Set the status value */
        UA_Boolean status = true;
        UA_Variant value;
        UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
        UA_Server_writeValue(server, bpr.targets->targetId.nodeId, value);
    	//UA_Server_writeValue(server, tmp, value);
    
    	//다시 아래로 내림.
    	bp.startingNode = bpr.targets->targetId.nodeId;
    	rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
        UA_BrowsePathResult bpr2 =
            UA_Server_translateBrowsePathToNodeIds(server, &bp);
        if(bpr2.statusCode != UA_STATUSCODE_GOOD ||
           bpr2.targetsSize < 1)
            return bpr.statusCode;
    
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "found target is %d", bpr2.targets->targetId.nodeId.identifier.numeric);
    	UA_Server_writeValue(server, bpr2.targets->targetId.nodeId, value);
        UA_BrowsePathResult_clear(&bpr);
    #endif
    

    Variable을 제대로 수정한다. 이제 rucursive browse를 어떻게 해야 하는지 알아야겠다. 삽질해도 보람차다.ㅠㅠ

  • open62541 server 설정

    tutorial을 따라하다 보면 내가 무엇을 모르는지 잘 모른다. 아래 같은 그림을 그리고 싶었는데 잘 안된다. 내가 서버를 건드릴 일은 없겠지만, 클라이언트를 테스트하려면 서버가 필요하다.

    #include "open62541.h"
    
    #include <signal.h>
    #include <stdlib.h>
    
    static volatile UA_Boolean running = true;
    static void stopHandler(int sig) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
        running = false;
    }
    
    static void
    beforeReadVal(UA_Server *server,
                   const UA_NodeId *sessionId, void *sessionContext,
                   const UA_NodeId *nodeid, void *nodeContext,
                   const UA_NumericRange *range, const UA_DataValue *data) {
        //updateCurrentTime(server);
    	int tmpVal = rand()%1000;
        UA_Variant value;
        UA_Variant_setScalar(&value, &tmpVal, &UA_TYPES[UA_TYPES_INT32]);
        UA_NodeId currentNodeId = UA_NODEID_STRING(1, "Test Var:Me");
        UA_Server_writeValue(server, currentNodeId, value);
    }
    
    
    /* predefined identifier for later use */
    UA_NodeId pumpTypeId = {1, UA_NODEIDTYPE_NUMERIC, {1001}};
    UA_NodeId cylTypeId = {1, UA_NODEIDTYPE_NUMERIC, {2001}};
    
    static void
    defineObjectTypes(UA_Server *server) {
        /* Define the object type for "Device" */
        UA_NodeId deviceTypeId; /* get the nodeid assigned by the server */
        UA_ObjectTypeAttributes dtAttr = UA_ObjectTypeAttributes_default;
        dtAttr.displayName = UA_LOCALIZEDTEXT("en-US", "DeviceType");
        UA_Server_addObjectTypeNode(server, UA_NODEID_NULL,
                                    UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
                                    UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                    UA_QUALIFIEDNAME(1, "DeviceType"), dtAttr,
                                    NULL, &deviceTypeId);
    
        UA_VariableAttributes mnAttr = UA_VariableAttributes_default;
        mnAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ManufacturerName");
        UA_NodeId manufacturerNameId;
        UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "ManufacturerName"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, NULL, &manufacturerNameId);
        /* Make the manufacturer name mandatory */
        UA_Server_addReference(server, manufacturerNameId,
                               UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
                               UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
    
    
        UA_VariableAttributes modelAttr = UA_VariableAttributes_default;
        modelAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ModelName");
        UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "ModelName"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, NULL, NULL);
    
        /* Define the object type for "Pump" */
        UA_ObjectTypeAttributes ptAttr = UA_ObjectTypeAttributes_default;
        ptAttr.displayName = UA_LOCALIZEDTEXT("en-US", "PumpType");
        UA_Server_addObjectTypeNode(server, pumpTypeId,
                                    deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                    UA_QUALIFIEDNAME(1, "PumpType"), ptAttr,
                                    NULL, NULL);
    
        UA_VariableAttributes statusAttr = UA_VariableAttributes_default;
        statusAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Status");
        statusAttr.valueRank = UA_VALUERANK_SCALAR;
        UA_NodeId statusId;
        UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "Status"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, NULL, &statusId);
        /* Make the status variable mandatory */
        UA_Server_addReference(server, statusId,
                               UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
                               UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
    
        UA_VariableAttributes rpmAttr = UA_VariableAttributes_default;
        rpmAttr.displayName = UA_LOCALIZEDTEXT("en-US", "MotorRPM");
        rpmAttr.valueRank = UA_VALUERANK_SCALAR;
        UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "MotorRPMs"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, NULL, NULL);
    }
    
    static void
    defineCylTypes(UA_Server *server) {
        /* Define the object type for "Device" */
        UA_NodeId deviceTypeId; /* get the nodeid assigned by the server */
        UA_ObjectTypeAttributes dtAttr = UA_ObjectTypeAttributes_default;
        dtAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Cylinder Type");
        UA_Server_addObjectTypeNode(server, UA_NODEID_NULL,
                                    UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
                                    UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                    UA_QUALIFIEDNAME(1, "Cylinder Type"), dtAttr,
                                    NULL, &deviceTypeId);
    
        UA_VariableAttributes mnAttr = UA_VariableAttributes_default;
        mnAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Manufacturer Name");
        UA_NodeId manufacturerNameId;
        UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "Cylinder"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, NULL, &manufacturerNameId);
    
        UA_VariableAttributes modelAttr = UA_VariableAttributes_default;
    	UA_NodeId modelNameId;
        modelAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Cyl Model Name");
        UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "Cyl Model Name"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, NULL, &modelNameId);
    
        UA_Server_addReference(server,modelNameId,
                               UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
                               UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
    
        /* Define the object type for "Pump" */
        UA_ObjectTypeAttributes ptAttr = UA_ObjectTypeAttributes_default;
        ptAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Running State");
        UA_Server_addObjectTypeNode(server, cylTypeId,
                                    deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
                                    UA_QUALIFIEDNAME(1, "Running State"), ptAttr,
                                    NULL, NULL);
    
    
        UA_VariableAttributes statusAttr = UA_VariableAttributes_default;
        statusAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Status");
        statusAttr.valueRank = UA_VALUERANK_SCALAR;
        UA_NodeId statusId;
        UA_Server_addVariableNode(server, UA_NODEID_NULL, cylTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "Status"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, NULL, &statusId);
        /* Make the manufacturer name mandatory */
    	// UA_Server_addReference()를 어떻게 하냐에 따라  statusId
    	// instance 만들경우 표시
        UA_Server_addReference(server, statusId,
                               UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
                               UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
    }
    
    static void
    addPumpObjectInstance(UA_Server *server, char *name) {
        UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
        oAttr.displayName = UA_LOCALIZEDTEXT("en-US", name);
        UA_Server_addObjectNode(server, UA_NODEID_NULL,
                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                                UA_QUALIFIEDNAME(1, name),
                                pumpTypeId, /* this refers to the object type
                                               identifier */
                                oAttr, NULL, NULL);
    }
    
    static void
    addCylObjectInstance(UA_Server *server, char *name) {
        UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
        oAttr.displayName = UA_LOCALIZEDTEXT("en-US", name);
    	/*
    	UA_Server_addObjectNode(UA_Server *server, const UA_NodeId requestedNewNodeId,
                            const UA_NodeId parentNodeId,
                            const UA_NodeId referenceTypeId,
                            const UA_QualifiedName browseName,
                            const UA_NodeId typeDefinition,
                            const UA_ObjectAttributes attr,
                            void *nodeContext, UA_NodeId *outNewNodeId) {
    						*/
    
        UA_Server_addObjectNode(server, UA_NODEID_NULL,
                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                                UA_QUALIFIEDNAME(1, name),
                                cylTypeId, /* this refers to the object type
                                               identifier */
                                oAttr, NULL, NULL);
    }
    
    static UA_StatusCode
    pumpTypeConstructor(UA_Server *server,
                        const UA_NodeId *sessionId, void *sessionContext,
                        const UA_NodeId *typeId, void *typeContext,
                        const UA_NodeId *nodeId, void **nodeContext) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "New pump created");
    
        /* Find the NodeId of the status child variable */
        UA_RelativePathElement rpe;
        UA_RelativePathElement_init(&rpe);
        rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
        rpe.isInverse = false;
        rpe.includeSubtypes = false;
        rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
    
        UA_BrowsePath bp;
        UA_BrowsePath_init(&bp);
        bp.startingNode = *nodeId;
        bp.relativePath.elementsSize = 1;
        bp.relativePath.elements = &rpe;
    
        UA_BrowsePathResult bpr =
            UA_Server_translateBrowsePathToNodeIds(server, &bp);
        if(bpr.statusCode != UA_STATUSCODE_GOOD ||
           bpr.targetsSize < 1)
            return bpr.statusCode;
    
        /* Set the status value */
        UA_Boolean status = true;
        UA_Variant value;
        UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
        UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value);
        UA_BrowsePathResult_clear(&bpr);
    
        /* At this point we could replace the node context .. */
    
        return UA_STATUSCODE_GOOD;
    }
    
    static void
    addPumpTypeConstructor(UA_Server *server) {
        UA_NodeTypeLifecycle lifecycle;
        lifecycle.constructor = pumpTypeConstructor;
        lifecycle.destructor = NULL;
        UA_Server_setNodeTypeLifecycle(server, pumpTypeId, lifecycle);
    }
    
    
    
    
    //method 추가
    static UA_StatusCode
    helloWorldMethodCallback(UA_Server *server,
                             const UA_NodeId *sessionId, void *sessionHandle,
                             const UA_NodeId *methodId, void *methodContext,
                             const UA_NodeId *objectId, void *objectContext,
                             size_t inputSize, const UA_Variant *input,
                             size_t outputSize, UA_Variant *output) {
        UA_String *inputStr = (UA_String*)input->data;
        UA_String tmp = UA_STRING_ALLOC("Hello ");
        if(inputStr->length > 0) {
            tmp.data = (UA_Byte *)UA_realloc(tmp.data, tmp.length + inputStr->length);
            memcpy(&tmp.data[tmp.length], inputStr->data, inputStr->length);
            tmp.length += inputStr->length;
        }
        UA_Variant_setScalarCopy(output, &tmp, &UA_TYPES[UA_TYPES_STRING]);
        UA_String_clear(&tmp);
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Hello World was called");
        return UA_STATUSCODE_GOOD;
    }
    
    static void
    addHellWorldMethod(UA_Server *server) {
        UA_Argument inputArgument;
        UA_Argument_init(&inputArgument);
        inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String");
        inputArgument.name = UA_STRING("MyInput");
        inputArgument.dataType = UA_TYPES[UA_TYPES_STRING].typeId;
        inputArgument.valueRank = UA_VALUERANK_SCALAR;
    
        UA_Argument outputArgument;
        UA_Argument_init(&outputArgument);
        outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String");
        outputArgument.name = UA_STRING("MyOutput");
        outputArgument.dataType = UA_TYPES[UA_TYPES_STRING].typeId;
        outputArgument.valueRank = UA_VALUERANK_SCALAR;
    
        UA_MethodAttributes helloAttr = UA_MethodAttributes_default;
        helloAttr.description = UA_LOCALIZEDTEXT("en-US","Say `Hello World`");
        helloAttr.displayName = UA_LOCALIZEDTEXT("en-US","Hello World");
        helloAttr.executable = true;
        helloAttr.userExecutable = true;
        UA_Server_addMethodNode(server, UA_NODEID_NUMERIC(1,62541),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                UA_QUALIFIEDNAME(1, "hello world"),
                                helloAttr, &helloWorldMethodCallback,
                                1, &inputArgument, 1, &outputArgument, NULL, NULL);
    }
    
    
    int main(void) {
        signal(SIGINT, stopHandler);
        signal(SIGTERM, stopHandler);
    
        UA_Server *server = UA_Server_new();
        UA_ServerConfig_setDefault(UA_Server_getConfig(server));
    	UA_ServerConfig* config = UA_Server_getConfig(server);
    	config->verifyRequestTimestamp = UA_RULEHANDLING_ACCEPT;
    
    	//addVariable(server);
    	//writeVariable(server);
    	//writeWrongVariable(server);
    
    
    	//vendor, serial, variable 순으로 추가.
    
    	//공통으로 사용할 부분 설정.
    	UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    	UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
    	//variable 추가.
    	UA_VariableAttributes varAttr = UA_VariableAttributes_default;
    	UA_Int32 varName = 10;
        UA_Variant_setScalar(&varAttr.value, &varName, &UA_TYPES[UA_TYPES_INT32]);
        UA_NodeId varNodeId = UA_NODEID_STRING(1, "Test Var:Me");
    	UA_QualifiedName myVarName = UA_QUALIFIEDNAME(1, "variable");
        UA_Server_addVariableNode(server, varNodeId, parentNodeId,
                                  parentReferenceNodeId, myVarName,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), varAttr, NULL, NULL);
    
    	//add callback
        UA_ValueCallback callback ;
        callback.onRead = beforeReadVal;
        callback.onWrite = NULL;
    	UA_NodeId currentNodeId = UA_NODEID_STRING(1, "Test Var:Me");
        UA_Server_setVariableNode_valueCallback(server, currentNodeId, callback);
    
    	defineObjectTypes(server);
    	defineCylTypes(server);
    	addPumpObjectInstance(server, "pump2");
    	addPumpObjectInstance(server, "pump3");
    	addPumpTypeConstructor(server);
    	addPumpObjectInstance(server, "pump4");
    	addPumpObjectInstance(server, "pump5");
    
    	addCylObjectInstance(server, "pin1");
    	addCylObjectInstance(server, "pin2");
    
    
    
    
    	//method
    	addHellWorldMethod(server);
    
    
    	//서버 구동.
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Starting server...");
    	UA_StatusCode retval = UA_Server_run(server, &running);
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Shutdown server...");
    
    
    	UA_Server_delete(server);
    	return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
    }

    object를 define할 때 어느 변수를 인스턴스에 포함할지 결정할 수 있다. defineCylTypes 함수 중 UA_Server_addReference의 변수로 전에 선언한 id를 넘겨주면 붙는다.

        UA_NodeId statusId;
        UA_Server_addVariableNode(server, UA_NODEID_NULL, cylTypeId,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                  UA_QUALIFIEDNAME(1, "Status"),
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, NULL, &statusId);
        /* Make the manufacturer name mandatory */
    	// UA_Server_addReference()를 어떻게 하냐에 따라  statusId
    	// instance 만들경우 표시
        UA_Server_addReference(server, statusId,
                               UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
                               UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);

    꼬리의 꼬리를 무는 OPC UA.

  • open62541 method call

    튜토리얼에 method call이 있어 따라 해봤다. method를 서버에 설정하고, client에서 반응을 보고 싶었는데, 잘 안됬다. 두 시간 삽질만에 UA_VARINT에서 UA_String을 출력했다. 처음에는 몰랐는데 gdb로 구조를 보면 대략 알 수 있다. UA_String도 구조체라 data와 length로 구성된다. UA_Variant로 data를 출력하려면 UA_String으로 캐스팅하여 data의 data를 찾아야 한다.–;

    (gdb) 
    45		/* Call a remote method */
    46		UA_Variant input;
    47		UA_String argString = UA_STRING("Hello Server");
    48		UA_Variant_init(&input);
    49		//UA_Variant_setScalarCopy(&input, &argString, &UA_TYPES[UA_TYPES_STRING]);
    50		UA_Variant_setScalarCopy(&input, &argString, &UA_TYPES[UA_TYPES_STRING]);
    51		UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "input data is %s\n",(UA_String*)input.data);
    52	
    53		size_t outputSize;
    54		UA_Variant *output;
    (gdb) display input
    1: input = {type = 0xbefff4c8, storageType = (unknown: 3070066356), arrayLength = 0, data = 0xb6ffd13c, arrayDimensionsSize = 4, 
      arrayDimensions = 0xb6ffd14c}
    (gdb) n
    48		UA_Variant_init(&input);
    1: input = {type = 0xbefff4c8, storageType = (unknown: 3070066356), arrayLength = 0, data = 0xb6ffd13c, arrayDimensionsSize = 4, 
      arrayDimensions = 0xb6ffd14c}
    (gdb) print argString
    $1 = {length = 12, data = 0x10f0c "Hello Server"}
    (gdb) print argString->data
    $2 = (UA_Byte *) 0x10f0c "Hello Server"
    (gdb) n
    50		UA_Variant_setScalarCopy(&input, &argString, &UA_TYPES[UA_TYPES_STRING]);
    1: input = {type = 0x0, storageType = UA_VARIANT_DATA, arrayLength = 0, data = 0x0, arrayDimensionsSize = 0, arrayDimensions = 0x0}
    (gdb) n
    51		UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "input data is %s\n",(UA_String*)input.data);
    1: input = {type = 0x22250 <UA_TYPES+704>, storageType = UA_VARIANT_DATA, arrayLength = 0, data = 0x282d8, arrayDimensionsSize = 0, arrayDimensions = 0x0}
    (gdb) print input->data
    $3 = (void *) 0x282d8
    (gdb) print input->data->data
    Attempt to dereference a generic pointer.
    (gdb) print (UA_String*)(input->data)->data
    Attempt to dereference a generic pointer.
    (gdb) print (UA_String*)(input.data)->data
    Attempt to dereference a generic pointer.
    (gdb) display argString
    2: argString = {length = 12, data = 0x10f0c "Hello Server"}
    (gdb) display
    1: input = {type = 0x22250 <UA_TYPES+704>, storageType = UA_VARIANT_DATA, arrayLength = 0, data = 0x282d8, arrayDimensionsSize = 0, arrayDimensions = 0x0}
    2: argString = {length = 12, data = 0x10f0c "Hello Server"}
    (gdb) display &argString
    3: &argString = (UA_String *) 0xbefff368
    [2020-09-07 04:29:15.352 (UTC+0900)] info/userland	input data is Hello Server��
    Method call was successful, and 1 returned values available.
    [2020-09-07 04:29:15.352 (UTC+0900)] info/userland	output data is Hello Hello Server
    [2020-09-07 04:29:15.353 (UTC+0900)] info/client	Client Status: ChannelState: Closed, SessionState: Closed, ConnectStatus: Good
    int main(void) {
        UA_Client *client = UA_Client_new();
        UA_ClientConfig_setDefault(UA_Client_getConfig(client));
        UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
    	printf("retval is %d\n",retval);
        if(retval != UA_STATUSCODE_GOOD) {
            UA_Client_delete(client);
            return (int)retval;
        }
        /* Read attribute */
        UA_Int32 value2 = 0;
        printf("\nReading the value of node (1, \"Test Var:Me\"):\n");
        UA_Variant *val = UA_Variant_new();
        retval = UA_Client_readValueAttribute(client, UA_NODEID_STRING(1, "Test Var:Me"), val);
        if(retval == UA_STATUSCODE_GOOD && UA_Variant_isScalar(val) &&
    			val->type == &UA_TYPES[UA_TYPES_INT32]) {
    		value2 = *(UA_Int32*)val->data;
    		//printf("the value is: %i\n", value2);
    		UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"myData is %d",value2);
    
    	}
    
    	//method call
    
    #ifdef UA_ENABLE_METHODCALLS
    	/* Call a remote method */
    	UA_Variant input;
    	UA_String argString = UA_STRING("Hello Server");
    	UA_Variant_init(&input);
    	//UA_Variant_setScalarCopy(&input, &argString, &UA_TYPES[UA_TYPES_STRING]);
    	UA_Variant_setScalarCopy(&input, &argString, &UA_TYPES[UA_TYPES_STRING]);
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "input data is %s\n",((UA_String*)input.data)->data);
    	size_t outputSize;
    	UA_Variant *output;
    	retval = UA_Client_call(client, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
    			UA_NODEID_NUMERIC(1, 62541), 1, &input, &outputSize, &output);
    
    	if(retval == UA_STATUSCODE_GOOD) {
    		printf("Method call was successful, and %lu returned values available.\n",
    				(unsigned long)outputSize);
    
    		UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "output data is %s\n",((UA_String*)output->data)->data);
    
    		UA_Array_delete(output, outputSize, &UA_TYPES[UA_TYPES_VARIANT]);
    	} else {
    		printf("Method call was unsuccessful, and %x returned values available.\n", retval);
    	}
    	UA_Variant_clear(&input);
    #endif
    
    	UA_Variant_delete(val);
    	/* Clean up */
    	//	UA_Variant_clear(&value);
    	UA_Variant_clear(&value2);
    
    	UA_Client_delete(client); /* Disconnects the client internally */
    	return EXIT_SUCCESS;
    }
    
    

  • open62541 server – client variable 전달

    tutorial을 열심히 따라했지만 변수를 추가하다 막혀 못했다. 내가 OPC-UA 프로토콜을 제대로 이해하지 못한 탓이다. 아래를 참조하여 변수를 서버에 간단히 추가했다.

    https://open62541.org/doc/current/tutorial_server_variabletype.html

    클라이언트가 서버 변수를 읽으려 했으나, 쉽게 하지 못했다. tutorial이 현재 시각을 쉽게 보여줘서 너무 일찍 시작하였다. callback 함수까지 설정했어야 했는데, 설명하지 않았다. 너무 불친절하다. 다행히도 좋은 동영상을 보고 이해했다. 다음 장에 나온 Connecting a Variable with a Physical Process 항목까지 읽고 따라 했어야 했다.

    https://open62541.org/doc/current/tutorial_server_datasource.html
    #include "open62541.h"
    
    #include <signal.h>
    #include <stdlib.h>
    
    static volatile UA_Boolean running = true;
    static void stopHandler(int sig) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
        running = false;
    }
    
    
    static void
    addVariable(UA_Server *server) {
        /* Define the attribute of the myInteger variable node */
        UA_VariableAttributes attr = UA_VariableAttributes_default;
        UA_Int32 myInteger = 42;
        UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
        attr.description = UA_LOCALIZEDTEXT("en-US","the answer");
        attr.displayName = UA_LOCALIZEDTEXT("en-US","the answer");
        attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId;
        attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
    
        /* Add the variable node to the information model */
        UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "the.answer");
        UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, "the answer");
        UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
        UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
        UA_Server_addVariableNode(server, myIntegerNodeId, parentNodeId,
                                  parentReferenceNodeId, myIntegerName,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, NULL);
    }
    
    static void
    writeVariable(UA_Server *server) {
        UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "the.answer");
    
        /* Write a different integer value */
        UA_Int32 myInteger = 43;
        UA_Variant myVar;
        UA_Variant_init(&myVar);
        UA_Variant_setScalar(&myVar, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
        UA_Server_writeValue(server, myIntegerNodeId, myVar);
    	 /* Set the status code of the value to an error code. The function
         * UA_Server_write provides access to the raw service. The above
         * UA_Server_writeValue is syntactic sugar for writing a specific node
         * attribute with the write service. */
        UA_WriteValue wv;
        UA_WriteValue_init(&wv);
        wv.nodeId = myIntegerNodeId;
        wv.attributeId = UA_ATTRIBUTEID_VALUE;
        wv.value.status = UA_STATUSCODE_BADNOTCONNECTED;
        wv.value.hasStatus = true;
        UA_Server_write(server, &wv);
    
        /* Reset the variable to a good statuscode with a value */
        wv.value.hasStatus = false;
        wv.value.value = myVar;
        wv.value.hasValue = true;
        UA_Server_write(server, &wv);
    }
    
    
    static void
    writeWrongVariable(UA_Server *server) {
        UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "the.answer");
    
        /* Write a string */
        UA_String myString = UA_STRING("test");
        UA_Variant myVar;
        UA_Variant_init(&myVar);
        UA_Variant_setScalar(&myVar, &myString, &UA_TYPES[UA_TYPES_STRING]);
        UA_StatusCode retval = UA_Server_writeValue(server, myIntegerNodeId, myVar);
        printf("Writing a string returned statuscode %s\n", UA_StatusCode_name(retval));
    }
    
    static void
    beforeReadVal(UA_Server *server,
                   const UA_NodeId *sessionId, void *sessionContext,
                   const UA_NodeId *nodeid, void *nodeContext,
                   const UA_NumericRange *range, const UA_DataValue *data) {
        //updateCurrentTime(server);
    	int tmpVal = rand()%1000;
        UA_Variant value;
        UA_Variant_setScalar(&value, &tmpVal, &UA_TYPES[UA_TYPES_INT32]);
        UA_NodeId currentNodeId = UA_NODEID_STRING(1, "Test Var:Me");
        UA_Server_writeValue(server, currentNodeId, value);
    	
    
    }
    
    int main(void) {
        signal(SIGINT, stopHandler);
        signal(SIGTERM, stopHandler);
    
        UA_Server *server = UA_Server_new();
        UA_ServerConfig_setDefault(UA_Server_getConfig(server));
    	//UA_ServerConfig* config = UA_Server_getConfig(server);
    	//config->verifyRequestTimestamp = UA_RULEHANDLING_ACCEPT;
    
    	//addVariable(server);
    	//writeVariable(server);
    	//writeWrongVariable(server);
    
    
    	//vendor, serial, variable 순으로 추가.
    
    	//공통으로 사용할 부분 설정.
    	UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    	UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
    //
    //	//vendor name 추가.
    //	UA_VariableAttributes vnAttr = UA_VariableAttributes_default;
    //	UA_String vendorName = UA_STRING("test vendor");
    //    UA_Variant_setScalar(&vnAttr.value, &vendorName, &UA_TYPES[UA_TYPES_STRING]);
    //    UA_NodeId vendorNodeId = UA_NODEID_STRING(1, "Test VendorName:Me");
    //	UA_QualifiedName myVendorName = UA_QUALIFIEDNAME(1, "the answer");
    //    UA_Server_addVariableNode(server, vendorNodeId, parentNodeId,
    //                              parentReferenceNodeId, myVendorName,
    //                              UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), vnAttr, NULL, NULL);
    //
    //
    //	//serial no 추가.
    //	UA_VariableAttributes serialAttr = UA_VariableAttributes_default;
    //	UA_Int32 serialName = 123456;
    //    UA_Variant_setScalar(&serialAttr.value, &serialName, &UA_TYPES[UA_TYPES_INT32]);
    //    UA_NodeId serialNodeId = UA_NODEID_STRING(1, "Test serial:Me");
    //	UA_QualifiedName mySerialName = UA_QUALIFIEDNAME(1, "serial No");
    //    UA_Server_addVariableNode(server, serialNodeId, parentNodeId,
    //                              parentReferenceNodeId, mySerialName,
    //                              UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), serialAttr, NULL, NULL);
    //
    	//variable 추가.
    	UA_VariableAttributes varAttr = UA_VariableAttributes_default;
    	UA_Int32 varName = 10;
        UA_Variant_setScalar(&varAttr.value, &varName, &UA_TYPES[UA_TYPES_INT32]);
        UA_NodeId varNodeId = UA_NODEID_STRING(1, "Test Var:Me");
    	UA_QualifiedName myVarName = UA_QUALIFIEDNAME(1, "variable");
        UA_Server_addVariableNode(server, varNodeId, parentNodeId,
                                  parentReferenceNodeId, myVarName,
                                  UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), varAttr, NULL, NULL);
    
    	//add callback
        UA_ValueCallback callback ;
        callback.onRead = beforeReadVal;
        callback.onWrite = NULL;
    	UA_NodeId currentNodeId = UA_NODEID_STRING(1, "Test Var:Me");
        UA_Server_setVariableNode_valueCallback(server, currentNodeId, callback);
    
    
    	//서버 구동.
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Starting server...");
        UA_StatusCode retval = UA_Server_run(server, &running);
    	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Shutdown server...");
    
    
        UA_Server_delete(server);
        return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    
    #include "open62541.h"
    
    #include <stdlib.h>
    
    int main(void) {
        UA_Client *client = UA_Client_new();
        UA_ClientConfig_setDefault(UA_Client_getConfig(client));
        UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
    	printf("retval is %d\n",retval);
        if(retval != UA_STATUSCODE_GOOD) {
            UA_Client_delete(client);
            return (int)retval;
        }
    
        /* Read the value attribute of the node. UA_Client_readValueAttribute is a
         * wrapper for the raw read service available as UA_Client_Service_read. */
    
    
    	
    //    UA_Variant value; /* Variants can hold scalar values and arrays of any type */
    //    UA_Variant_init(&value);
    //
    //    /* NodeId of the variable holding the current time */
    //    const UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
    //	retval = UA_Client_readValueAttribute(client, nodeId, &value);
    //	printf("test\n");
    //    if(retval == UA_STATUSCODE_GOOD &&
    //       UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DATETIME])) {
    //        UA_DateTime raw_date = *(UA_DateTime *) value.data;
    //        UA_DateTimeStruct dts = UA_DateTime_toStruct(raw_date);
    //        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "date is: %u-%u-%u %u:%u:%u.%03u\n",
    //                    dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
    //
    //    }
    //
    	//const UA_NodeId nodeId2 = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
    //	const UA_NodeId nodeId2 = UA_NODEID_STRING(1, "the.answer");
    
    //	UA_Variant value2;
    //	UA_Variant_init(&value2);
    //	UA_Int32 node_value=0;
    //
    //	retval = UA_Client_readValueAttribute(client, nodeId2, &value2);
    //	if(retval == UA_STATUSCODE_GOOD &&
    //			//UA_Variant_hasScalarType(&value2, &UA_TYPES[UA_TYPES_INT32])) {
    //			UA_Variant_isScalar(&value2)) {
    //		UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"myData is ");
    //
    //	}
    //
        /* Read attribute */
        UA_Int32 value2 = 0;
        printf("\nReading the value of node (1, \"Test Var:Me\"):\n");
        UA_Variant *val = UA_Variant_new();
    	printf("retval is %d\n",retval);
        retval = UA_Client_readValueAttribute(client, UA_NODEID_STRING(1, "Test Var:Me"), val);
    	printf("retval is %d\n",retval);
        if(retval == UA_STATUSCODE_GOOD && UA_Variant_isScalar(val) &&
    			val->type == &UA_TYPES[UA_TYPES_INT32]) {
    		value2 = *(UA_Int32*)val->data;
    		//printf("the value is: %i\n", value2);
    		UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"myData is %d",value2);
    
    	}
    	UA_Variant_delete(val);
    	/* Clean up */
    //	UA_Variant_clear(&value);
    	UA_Variant_clear(&value2);
    
    	UA_Client_delete(client); /* Disconnects the client internally */
    	return EXIT_SUCCESS;
    }

    노드 ID, 데이터 타입 등 까다롭게 맞춰줘야 한다. 틀리면 못 읽는다.

  • raspberry pi + opc-ua + open62541 삽질기

    하. 드디어 여기까지 왔다. opc ua를 설치하면 자동화 기기에서 정보를 쉽게 읽을 수 있다. 여기에서 여러 오픈소스 프로그램을 찾았다. 소개한 홈페이지와 마찬가지로 가볍게 C로 작성한 open62541를 설치하기로 했다.

    3달 전만해도 PLC가 당연히 OPC를 지원할 줄 알았으나, 지멘스 S7-1500 최신 제품, MELSEC 정도나 OPCUA를 지원한다. 나름 최신 기술인가보다. OPC 솔루션을 가진 회사에 연락하면 쉽게 지원받을 수 있는데 가격대가 높다. 7만원 짜리 라즈베리 하드웨어에 몇 천만원 소프트웨어를 올리면 미친 짓이다. 쓸 사람도 없고. 이런 기능을 PLC 메이커가 좀 쉽게 해줘야 하는데, 아쉽다.

    설치는 정말 그대로 따라하면 된다. 컴파일 후 open62541.c와 open62541.h 파일을 현재 폴더에 넣고 #include “open62541.c”로 하던가, gcc 표준 라이브러리 경로를 찾아 디렉토리 구조째 넣어주고 <>으로 감싸도 된다. tutorial은 뭔가 잘못되었는지 잘 실행되지 않는다. 참조한 사이트 작성자가 했듯이 주석처리하면 된다. 실행하면 서버가 열리고 접속할 수 있다. 웹 브라우저가 아닌 OPC 클라이언트가 필요하다.

    pi@raspberrypi:~/OPCUA/sample $ ./myServer 
    [2020-09-04 07:01:39.438 (UTC+0900)] warn/server	AccessControl: Unconfigured AccessControl. Users have all permissions.
    [2020-09-04 07:01:39.438 (UTC+0900)] info/server	AccessControl: Anonymous login is enabled
    [2020-09-04 07:01:39.438 (UTC+0900)] warn/server	Username/Password configured, but no encrypting SecurityPolicy. This can leak credentials on the network.
    [2020-09-04 07:01:39.438 (UTC+0900)] warn/userland	AcceptAll Certificate Verification. Any remote certificate will be accepted.
    [2020-09-04 07:01:39.439 (UTC+0900)] info/network	TCP network layer listening on opc.tcp://raspberrypi:4840/

    PLC가 opcua를 지원하면 서버, 클라이언트 양쪽 다 지원할 듯 하다. 여기를 보고 다음단계로 가보자.

    동영상 다 보고, 코드 몇 개 실행하면 자가격리 끝나 있겠다. 아 허리야.

    이어서…

    위 방법대로 sample 코드를 컴파일하면 에러가 무지하게 뜬다. 아마도 중복으로 선언되어 문제인 듯 하다. 여기에 있는 raspberry용 파일을 받아 압축을 푼 뒤, include에 해당하는 c, h 파일을 gcc 경로에 넣어주면 된다. 혹시나 해서 build한 버전의 c, h 파일을 모두 찾아 되는지 해보았지만 실패했다. 다음 3개 경로에서 찾을 수 있는데, raspberry용 파일과 linux 파일이 조금 다르다. raspberry는 헤더를 포함하는 부분이 좀 간략하게 되어 있다. server.h를 예를 들면.

    pi@raspberrypi:~/OPCUA/open62541 $ find ./ -type d -iname open62541
    ./include/open62541
    ./plugins/include/open62541
    ./build/src_generated/open62541
    ./build/CMakeFiles/open62541-object.dir/src_generated/open62541
    ./build/CMakeFiles/Export/lib/cmake/open62541

    결국 라즈베리용 파일을 해당 경로에 넣어준다. library는 build 아래 bin에 디렉토리에 libopen62541.so을 만든다. 라즈베리용 라이브러리보다 좀 작다. 이 linux용 build 라이브러리를 사용하면 오류난다. 라즈베리용 라이브러리를 사용해야 한다. raspberry에서 컴파일 할 수도 있어 보이지만, readme에 표시되지 않았다. 결국 되니 패스하겠다.

    정리하면.

    1. open62541 컴파일하여 open62541.c, open62541.h를 얻음.
    2. 라즈베리용 파일을 구하여 압축 해제.(build 후 header를 중복으로 선언한 부분을 수정한 듯 함. 조금씩 다름)
    3. include에 포함된 c, h를 해당 경로로 이동.
    4. lib에 포함된 .so를 각 환경에 맞는 경로로 이동.
    5. gcc -std=c99 -lopen62541 sample.c -o target
    6. 이런 형식으로 컴파일.

    라면 끝인 줄 알았으나, 아래 두 방법 중 하나를 선택하면 된다.

    • 컴파일 한 open62541.c, open62541.h 사용.
      1. ccmake 중 UA_ENABLE_AMALGAMATION=on 을 선택하여 open62541.c open62541.h를 만들면 끝나도록 한다.
      2. 라이브러리를 만들면 해당 디렉토리에 복사.

    In CMake, select freertosLWIP using the variable UA_ARCHITECTURE, enable amalgamation using the UA_ENABLE_AMALGAMATION variable and just select the native compilers. Then try to compile as always. The compilation will fail, but the open62541.h and open62541.c will be generated.

    • 라즈베리용 헤더, 라이브러리 사용.
      1. 컴파일 필요 없음.
      2. 적당히 include, lib 파일을 찾아 복사해 줌.
      3. tutorial 코드를 그대로 사용할 수 있음.
    #include <open62541/plugin/log_stdout.h>
    #include <open62541/server.h>
    #include <open62541/server_config_default.h>
    //#include <open62541/open62541.h>
    
    #include <signal.h>
    #include <stdlib.h>
    
    static volatile UA_Boolean running = true;
    static void stopHandler(int sig) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
        running = false;
    }
    
    int main(void) {
        signal(SIGINT, stopHandler);
        signal(SIGTERM, stopHandler);
    
        UA_Server *server = UA_Server_new();
        UA_ServerConfig_setDefault(UA_Server_getConfig(server));
    
        UA_StatusCode retval = UA_Server_run(server, &running);
    
        UA_Server_delete(server);
        return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
    }

    위 코드에서 open62541/open62541.h를 주석 처리했다. open62541.h 파일을 현재 디렉토리로 옮긴 후 #include “open62541.h”으로 쓰고, 위 앞 3줄을 주석처리해도 컴파일 된다. 끝.