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;
}
代碼解析:
- 調用自定義信息模型中提供的myNS()函數來添加新建的信息模型,這樣在OPC UA Server裏就可以看到我們定義的對象類型節點了,即MyObjectType
- 對象類型中的方法比較特殊,與變量不一樣,類似於C++類中的成員函數,不管用對象類型生成多少對象,其包含的方法都只會指向同一個方法,而變量則會與對象一起生成,對象之間互不干擾
- 使用UA_Server_setMethodNode_callback()給方法節點設置方法,注意不能使用UA_Server_addMethodNode(),因爲方法已經在信息模型中添加好了,只不過是一個空殼
- 多次調用UA_Server_setMethodNode_callback(),只會使用最後一次調用所添加的方法
- 使用UA_Server_addObjectNode()來創建對象節點,參數中對象類型的NodeId是UA_NODEID_NUMERIC(2, 1002),就是使用UaModeler創建的對象類型
可能會問:我怎麼知道對象類型的NodeId以及其方法的NodeId呢?有2種方法:
- 先用代碼測試一下,代碼中只調用myNS(),不去創建對象,編譯後運行server,然後使用UaExpert去連接,連接後去地址空間窗口中去查看,
在ObjectTypes裏找到MyObjectType並展開,在右側的屬性窗口中就可以看到NodeId了
- 使用路徑搜索,因爲我們知道對象類型的名稱,所以使用路徑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裏。
過程稍微複雜了一點,本人寫的也是累的一批。希望看過的同學能給個贊,謝謝。
本文創建的對象類型比較簡單,如果需要創建複雜的類型或多個類型,則需要自己探索,如果搞懂了本文的例子,應該沒有什麼問題。
如果有寫的不對的地方,希望能留言指正,謝謝閱讀。