學習open62541 --- [15] 使用建模工具UaModeler

UaModeler是一個OPC UA信息模型的建模工具,和UaExpert同出一個網站,可以去其網站下載(需要註冊一個賬號),也可以點擊這裏進行下載(本人下載後傳到百度雲上)。注意,這是個商業軟件,免費使用時可創建的Node數量有限,不過用來學習足夠了。

使用SIOME建模的文章請點擊這裏,西門子出的免費軟件,也非常好用。

在之前的系列文章中,我們往OPC UA Server裏添加東西都是使用代碼,當工程比較大時這麼做就有點繁瑣了。本文主要講述如何使用UaModeler進行建模,並在代碼中使用建好的模型。


一 安裝UaModeler

下載後解壓,然後點擊這個exe文件進行安裝,
在這裏插入圖片描述
安裝ok後打開,界面如下,
在這裏插入圖片描述
在Help下有個Handbook,裏面描述了常用的使用方法,大家也可以直接參考這個手冊。
在這裏插入圖片描述


二 使用UaModeler

1. 創建新工程

點擊File->New Project,
在這裏插入圖片描述
在彈出的界面裏,輸入Project Name並選擇工程保存位置,然後點擊Next,
在這裏插入圖片描述
在下一個Generate Code界面裏,選擇生成的代碼類型,這裏選擇ansi_c v1_9,然後再選擇一下輸出代碼的路徑,最後點擊Next,(代碼類型可隨意選,我們最後並不使用UaModeler去生成代碼
在這裏插入圖片描述
後面2個界面選Next就行了,使用默認配置,
在這裏插入圖片描述
在這裏插入圖片描述
然後下一個界面裏可以根據需要修改Namespace URI,最後點擊Finish
在這裏插入圖片描述
工程新建好後,界面如下,
在這裏插入圖片描述
我們新建的model在Projet窗口下,即example.ua
在這裏插入圖片描述

2. 添加Object Type節點

我們想新增一個Object Type,即對象類型,該類型含有2個變量和一個方法,這個方法有2個輸入參數和一個輸出參數。

在Information Model下展開Types->ObjectTypes,選中BaseObjectType,然後右擊,
在這裏插入圖片描述
點擊Add New Type,在中間的窗口中輸入想要添加的Object Type名稱,這裏填入MyObjectType,
在這裏插入圖片描述
然後點擊Children右側的那個倒三角進行展開,
在這裏插入圖片描述
點擊Select NodeClass,選擇Variable,
在這裏插入圖片描述
然後在Name欄下輸入名字Var1,並更改DataType爲Int32,
在這裏插入圖片描述
先點擊Double右側的那個下拉符號,下拉後點擊最下面的Add,
在這裏插入圖片描述
在彈出的界面裏選擇數據類型,
在這裏插入圖片描述
同樣,我們再添加一個Variable,叫Var2,
在這裏插入圖片描述
再添加一個方法,
在這裏插入圖片描述
名字叫Func,
在這裏插入圖片描述
展開這個Method,設置其輸入和輸出參數,
在這裏插入圖片描述
這裏爲這個方法設置2個輸入參數:input0和input1,1個輸出參數:output0,類型都是Int32,如下圖(注意:只要Add Argument裏沒有輸入名稱,那麼這個就不算作參數)
在這裏插入圖片描述
對於添加的方法,還需要注意一點:我們並沒有爲方法添加代碼邏輯,只能算一個空殼,後面在使用這個Object Type生成對象時纔會添加邏輯。

其它都採取默認,然後保存工程,這樣這個Object Type就創建好了。

3. 生成xml文件

使用UaModeler建模只需要最終生成的xml,不需要其生成的代碼,後續會使用open62541自帶的工具生成相關代碼。

在Project窗口選中example.ua,右擊,選擇Export XML(第一次會詢問是否要設置模型版本號,可以設置也可以不設置,採取默認也行)
在這裏插入圖片描述
生成OK後,在工程目錄下可以看到生成的xml文件,如下,
在這裏插入圖片描述


三 使用open62541處理XML

1. 配置open62541

首先,需要對open62541進行配置,先打開dos窗口或shell窗口,cd到open62541源碼目錄下,執行下面的命令,

git submodule update --init

會下載一些必須的子模塊,用於代碼生成。

然後,打開open62541源碼目錄下的CMakeLists.txt,找到UA_ENABLE_AMALGAMATION設置爲ON,接着找到下面這段設置,

# Namespace Zero
set(UA_NAMESPACE_ZERO "REDUCED" CACHE STRING "Completeness of the generated namespace zero (minimal/reduced/full)")
SET_PROPERTY(CACHE UA_NAMESPACE_ZERO PROPERTY STRINGS "MINIMAL" "REDUCED" "FULL")

把UA_NAMESPACE_ZERO的值由REDUCED改爲FULL,然後執行以下操作,

  • 在open62541源碼目錄下新建build目錄,並cd進入
  • 執行cmake .. && make,會比較耗時

OK後把open62541.h和libopen62541.a拷貝到自定義工程目錄,例如如下,
在這裏插入圖片描述
myNS是本次的工程目錄,也可以根據需要自定義任意目錄

PS:由於UA_NAMESPACE_ZERO變成FULL,所以libopen62541.a也變大了很多

2. 生成自定義信息模型代碼

這一步就使用到了之前生成的example.xml,先把該xml文件拷貝到tools/nodeset_compiler下,然後執行下面的命令,最後一個參數myNS用來指示生成的代碼文件名稱,

python ./nodeset_compiler.py --types-array=UA_TYPES --existing ../../deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml --xml example.xml myNS

打印如下,表示生成成功
在這裏插入圖片描述
在當前路徑下輸入ls,可以看到生成了myNS.c和myNS.h,這2個文件就是我們需要的,
在這裏插入圖片描述
把myNS.c和myNS.h拷貝到如下src目錄,
在這裏插入圖片描述
打開myNS.h,其中有段編譯控制,

#ifdef UA_ENABLE_AMALGAMATION
# include "open62541.h"
#else
# include <open62541/server.h>
#endif

直接改成如下,因爲我們使用的是open62541.h

# include "open62541.h"

3. 編寫OPC UA Server代碼

在src目錄下添加文件server.c,
在這裏插入圖片描述
其內容如下,創建了2個對象,分別叫myNSObject和myNSObject2,

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include <signal.h>
#include <stdio.h>
#include "open62541.h"

/* Files myNS.h and myNS.c are created from myNS.xml */
#include "myNS.h"

UA_Boolean running = true;

static void stopHandler(int sign) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}

// 這個方法的功能是把輸入參數累加,傳給輸出參數
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_Int32 value = 0;
    for (size_t i = 0; i < inputSize; ++i)
    {
    	UA_Int32 * ptr = (UA_Int32 *)input[i].data;
    	value += (*ptr);
    }

    UA_Variant_setScalarCopy(output, &value, &UA_TYPES[UA_TYPES_INT32]);
    
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Hello World was called");
    
    return UA_STATUSCODE_GOOD;
}


int main(int argc, char **argv) 
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    UA_StatusCode retval;
    /* create nodes from nodeset */
    if (myNS(server) != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Could not add the example nodeset. "
            "Check previous output for any error.");
        retval = UA_STATUSCODE_BADUNEXPECTEDERROR;
    } else {
        
        // 方法節點的NodeId是UA_NODEID_NUMERIC(2, 7001)
        UA_Server_setMethodNode_callback(server, UA_NODEID_NUMERIC(2, 7001), &helloWorldMethodCallback);
        
        
        UA_NodeId createdNodeId;
        UA_ObjectAttributes object_attr = UA_ObjectAttributes_default;

        object_attr.description = UA_LOCALIZEDTEXT("en-US", "myNSObject");
        object_attr.displayName = UA_LOCALIZEDTEXT("en-US", "myNSObject");

        // we assume that the myNS nodeset was added in namespace 2.
        // You should always use UA_Server_addNamespace to check what the
        // namespace index is for a given namespace URI. UA_Server_addNamespace
        // will just return the index if it is already added.
        UA_Server_addObjectNode(server, UA_NODEID_NULL,
                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                                UA_QUALIFIEDNAME(1, "myNSObject"),
                                UA_NODEID_NUMERIC(2, 1002),
                                object_attr, NULL, &createdNodeId);
        
        
        
        UA_NodeId createdNodeId2;
        UA_ObjectAttributes object_attr2 = UA_ObjectAttributes_default;

        object_attr2.description = UA_LOCALIZEDTEXT("en-US", "myNSObject2");
        object_attr2.displayName = UA_LOCALIZEDTEXT("en-US", "myNSObject2");

        // we assume that the myNS nodeset was added in namespace 2.
        // You should always use UA_Server_addNamespace to check what the
        // namespace index is for a given namespace URI. UA_Server_addNamespace
        // will just return the index if it is already added.
        UA_Server_addObjectNode(server, UA_NODEID_NULL,
                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                                UA_QUALIFIEDNAME(1, "myNSObject2"),
                                UA_NODEID_NUMERIC(2, 1002),
                                object_attr2, NULL, &createdNodeId2);
        
        
        retval = UA_Server_run(server, &running);
    }
    UA_Server_delete(server);
    return (int) retval;
}

代碼解析:

  1. 調用自定義信息模型中提供的myNS()函數來添加新建的信息模型,這樣在OPC UA Server裏就可以看到我們定義的對象類型節點了,即MyObjectType
  2. 對象類型中的方法比較特殊,與變量不一樣,類似於C++類中的成員函數,不管用對象類型生成多少對象,其包含的方法都只會指向同一個方法,而變量則會與對象一起生成,對象之間互不干擾
  3. 使用UA_Server_setMethodNode_callback()給方法節點設置方法,注意不能使用UA_Server_addMethodNode(),因爲方法已經在信息模型中添加好了,只不過是一個空殼
  4. 多次調用UA_Server_setMethodNode_callback(),只會使用最後一次調用所添加的方法
  5. 使用UA_Server_addObjectNode()來創建對象節點,參數中對象類型的NodeId是UA_NODEID_NUMERIC(2, 1002),就是使用UaModeler創建的對象類型

可能會問:我怎麼知道對象類型的NodeId以及其方法的NodeId呢?有2種方法:

  1. 先用代碼測試一下,代碼中只調用myNS(),不去創建對象,編譯後運行server,然後使用UaExpert去連接,連接後去地址空間窗口中去查看,
    在這裏插入圖片描述
    在ObjectTypes裏找到MyObjectType並展開,在右側的屬性窗口中就可以看到NodeId了
    在這裏插入圖片描述
  2. 使用路徑搜索,因爲我們知道對象類型的名稱,所以使用路徑Root->Types->ObjectTypes->MyObjectType就可以搜到了,路徑搜索可參照這篇文章

如果是正式應用,推薦第2種方法去獲得NodeId

整體工程結構如下,
在這裏插入圖片描述
CMakeLists.txt內容如下,

cmake_minimum_required(VERSION 3.5)

project(myNamespace)

set(EXECUTABLE_OUTPUT_PATH  ${PROJECT_SOURCE_DIR}/bin)

add_definitions(-std=c99)

include_directories(${PROJECT_SOURCE_DIR}/open62541)
include_directories(${PROJECT_SOURCE_DIR}/src)

link_directories(${PROJECT_SOURCE_DIR}/open62541)


add_executable(server ${PROJECT_SOURCE_DIR}/src/server.c ${PROJECT_SOURCE_DIR}/src/myNS.c)
target_link_libraries(server libopen62541.a)

cd到build目錄下執行cmake .. && make,生成的elf文件在bin目錄下,由於libopen62541.a變大了,所以鏈接時會比較慢。

3. 使用UaExpert進行連接

連接OK後,可以看到創建的2個對象都成功生成了。
在這裏插入圖片描述
展開這2個對象,可以看到它們的方法Func的NodeId都是一樣的,而變量的則是不同的,這也印證了前面的說法,
在這裏插入圖片描述
可以執行一下這個方法來測試一下,右擊Func,點擊Call,
在這裏插入圖片描述
在彈出的界面裏輸入2個參數值,然後點擊Call,
在這裏插入圖片描述
最後會在輸出參數裏得到300,和期望的一樣
在這裏插入圖片描述
驗證OK!


四 總結

本文主要講述如何使用UaModeler來創建信息模型,然後生成對應的xml文件,最後使用open62541自帶的工具把信息模型轉成代碼並添加到OPC UA Server裏。

過程稍微複雜了一點,本人寫的也是累的一批。希望看過的同學能給個贊,謝謝。

本文創建的對象類型比較簡單,如果需要創建複雜的類型或多個類型,則需要自己探索,如果搞懂了本文的例子,應該沒有什麼問題。

如果有寫的不對的地方,希望能留言指正,謝謝閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章