open62541中文文件
介紹
open62541(http://open62541.org)是一個開源的免費實現OPC UA(OPC統一架構),用C99和C ++ 98語言的通用子集編寫。該庫可與所有主要編譯器一起使用,並提供實現專用OPC UA客戶端和伺服器的必要工具,或將基於OPC UA的通訊整合到現有應用程式中。open62541庫與平臺無關。所有特定於平臺的功能都是通過可交換的外掛實現的。為主要作業系統提供了外掛實現。
open62541根據Mozilla Public License v2.0獲得許可。因此open62541庫可用於非開源專案。只有對open62541庫本身的更改才需要在同一許可下發布。外掛以及伺服器和客戶端示例都屬於公共域(CC0許可證)。它們可以在任何許可下重複使用,並且不必釋出更改。
OPC統一架構
OPC UA是工業通訊協議,已在IEC 62541系列中標準化。OPC UA的核心是
- 一種非同步協議(建立在TCP,HTTP或SOAP之上),通過會話,(在)安全通訊通道(在)原始連線之上定義訊息交換,
- 用於協議訊息的型別系統,具有二進位制和基於XML的編碼方案,
- 資訊建模的元模型,它將面向物件與語義三重關係相結合,並且
- 一組37個標準服務,用於與伺服器端資訊模型互動。每個服務的簽名被定義為協議型別系統中的請求和響應訊息。
標準本身可以從IEC購買或者在OPC基金會的網站上免費下載,網址為https://opcfoundation.org/
OPC基金會推動標準的不斷改進和配套規範的發展。伴隨規範將已建立的概念和可重用元件從應用程式域轉換為OPC UA。它們是與應用領域的既定行業委員會或標準化機構共同建立的。此外,OPC基金會還組織活動以傳播標準,併為合規認證提供基礎設施和工具。
open62541功能
open62541實現了OPC UA二進位制協議棧以及客戶端和伺服器SDK。它目前支援Micro Embedded Device Server Profile以及一些其他功能。伺服器二進位制檔案的大小可能低於100kb,具體取決於所包含的資訊模型。
- 通訊棧
- OPC UA二進位制協議
- 分塊(分割大資訊)
- 可交換網路層(外掛),用於使用自定義網路API(例如,在嵌入式目標上)
- 加密通訊
- 客戶端中的非同步服務請求
- OPC UA二進位制協議
- 資訊模型
- 支援所有OPC UA節點型別(包括方法節點)
- 支援在執行時新增和刪除節點和引用。
- 支援物件和變數型別的繼承和例項化(自定義建構函式/解構函式,子節點的例項化)
- 單個節點的訪問控制
- 訂閱
- 支援訂閱/監視專案以獲取資料更改通知
- 每個受監視值的資源消耗非常低(基於事件的伺服器體系結構)
- 程式碼生成
- 支援從標準XML定義生成資料型別
- 支援從標準XML定義生成伺服器端資訊模型(節點集)
0.3版本系列路線圖中的功能但在初始v0.3版本中缺失的是:
- 客戶端中的加密通訊
- 事件(物件發出的通知,資料更改通知已實現)
- 客戶端中的事件迴圈(後臺任務)
建立open62541
構建示例
使用GCC編譯器,以下呼叫在Linux上構建示例。
cp /path-to/open62541.* . # copy single-file distribution to the local directory
cp /path-to/examples/server_variable.c . # copy the example server
gcc -std=c99 open62541.c server_variable.c -o server
構建庫
在Ubuntu或Debian上使用CMake構建
sudo apt-get install git build-essential gcc pkg-config cmake python python-six
# enable additional features
sudo apt-get install cmake-curses-gui # for the ccmake graphical interface
sudo apt-get install libmbedtls-dev # for encryption support
sudo apt-get install check # for unit tests
sudo apt-get install python-sphinx graphviz # for documentation generation
sudo apt-get install python-sphinx-rtd-theme # documentation style
cd open62541
mkdir build
cd build
cmake ..
make
# select additional features
ccmake ..
make
# build documentation
make doc # html documentation
make doc_pdf # pdf documentation (requires LaTeX)
在Windows上使用CMake構建
這裡我們解釋Visual Studio(2013或更新版本)的構建過程。要使用MinGW構建,只需在呼叫CMake時替換編譯器選擇。
- 下載並安裝
- Python 2.7.x(Python 3.x也可以):https://python.org/downloads
- 使用pip包管理器安裝python-six()
pip install six
- CMake:http://www.cmake.org/cmake/resources/software.html
- Microsoft Visual Studio:https://www.visualstudio.com/products/visual-studio-community-vs
- 下載open62541原始碼(使用git或作為github的zip檔案)
- 開啟命令shell(cmd)並執行
cd <path-to>\open62541
mkdir build
cd build
<path-to>\cmake.exe .. -G "Visual Studio 14 2015"
:: You can use use cmake-gui for a graphical user-interface to select features
- 然後
buildopen62541.sln
在Visual Studio 2015中開啟並像往常一樣構建
在OS X上構建
- 下載並安裝
- Xcode:https://itunes.apple.com/us/app/xcode/id497799835? ls = 1& mt = 12
- 自制:http://brew.sh/
- Pip(python的包管理器,可能是預裝的):
sudo easy_install pip
- 在shell中執行以下命令
brew install cmake
pip install six # python 2/3 compatibility workarounds
pip install sphinx # for documentation generation
pip install sphinx_rtd_theme # documentation style
brew install graphviz # for graphics in the documentation
brew install check # for unit tests
brew install userspace-rcu # for multi-threading support
在沒有apt-get
命令的情況下遵循Ubuntu指令,因為上述包由這些指令處理。
在OpenBSD上構建
以下過程適用於OpenBSD 5.8,包含gcc版本4.8.4,cmake版本3.2.3和Python版本2.7.10。
- 安裝最近的gcc,python和cmake:
pkg_add gcc python cmake
- 告訴系統實際使用最近的gcc(它在OpenBSD上作為egcc安裝):
export CC=egcc CXX=eg++
- 現在按照Ubuntu / Debian的描述進行:
cd open62541
mkdir build
cd build
cmake ..
make
構建選項
open62541專案使用CMake來管理構建選項,用於程式碼生成以及為不同的系統和IDE生成構建專案。ccmake或cmake-gui工具可用於以圖形方式設定構建選項。
在程式碼生成之後,大多數選項都可以手動更改ua_config.h
(open62541.h
對於單檔案版本)。但通常沒有必要調整它們。
構建型別和日誌記錄
CMAKE_BUILD_TYPE
RelWithDebInfo
-O2優化除錯符號Release
-O2優化沒有除錯符號Debug
-O0優化除錯符號MinSizeRel
-Os優化沒有除錯符號
UA_LOGLEVEL
SDK僅記錄在其中定義的級別的事件UA_LOGLEVEL
。記錄事件級別如下:
- 600:致命
- 500:錯誤
- 400:警告
- 300:資訊
- 200:除錯
- 100:跟蹤
UA_BUILD_ *組
預設情況下,只有共享物件libopen62541.so或庫open62541.dll和open62541.dll.a。open62541.lib正在構建。可以通過以下選項指定其他工件:
UA_BUILD_EXAMPLES
編譯示例伺服器和客戶端。靜態和動態二進位制檔案分別連結。examples/xyz.c
UA_BUILD_UNIT_TESTS
使用Check框架編譯單元測試。測試可以執行make test
UA_BUILD_EXAMPLES_NODESET_COMPILER
從節點集XML生成OPC UA資訊模型(實驗)
UA_BUILD_SELFSIGNED_CERTIFICATE
為伺服器生成自簽名證書(需要openSSL)
UA_ENABLE_ *組
該組包含與支援的OPC UA功能相關的構建選項。
UA_ENABLE_SUBSCRIPTIONS
啟用訂閱
UA_ENABLE_METHODCALLS
啟用方法服務集
UA_ENABLE_NODEMANAGEMENT
在執行時啟用動態新增和刪除節點
UA_ENABLE_AMALGAMATION
編譯單個檔案釋放到檔案open62541.c
和open62541.h
UA_ENABLE_MULTITHREADING
啟用多執行緒支援
UA_ENABLE_COVERAGE
測量單元測試的覆蓋範圍
UA_ENABLE_DISCOVERY
啟用發現服務(LDS)
UA_ENABLE_DISCOVERY_MULTICAST
使用多播支援啟用發現服務(LDS-ME)
UA_ENABLE_DISCOVERY_SEMAPHORE
啟用Discovery Semaphore支援
某些選項標記為高階。需要切換高階選項以在cmake GUI中可見。
UA_ENABLE_TYPENAMES
將型別和成員名稱新增到UA_DataType結構。預設情況下啟用。
UA_ENABLE_STATUSCODE_DESCRIPTIONS
將StatusCodes的人類可讀名稱編譯為二進位制檔案。預設情況下啟用。
UA_ENABLE_FULL_NS0
使用完整的NS0而不是最小的名稱空間0節點集 UA_FILE_NS0
用於指定從namespace0資料夾生成NS0的檔案。預設值是Opc.Ua.NodeSet2.xml
UA_ENABLE_NONSTANDARD_UDP
啟用udp擴充套件
UA_DEBUG_ *組
該組包含主要用於庫本身開發的構建選項。
UA_DEBUG
啟用不適用於生產版本的斷言和其他定義
UA_DEBUG_DUMP_PKGS
將伺服器收到的每個包轉儲為hexdump格式
構建共享庫
open62541足夠小,大多數使用者都希望將庫靜態連結到他們的程式中。如果需要共享庫(.dll,.so),可以使用該BUILD_SHARED_LIBS
選項在CMake中啟用此功能。請注意,此選項會修改單檔案分發中ua_config.h
也包含 open62541.h
的檔案。
最小化二進位制大小
通過調整構建配置,可以顯著減小生成的二進位制檔案的大小。首先,在CMake中,可以將構建型別設定為 CMAKE_BUILD_TYPE=MinSizeRel
。這將設定編譯器標誌以最小化二進位制大小。構建型別也會刪除除錯資訊。其次,可以通過上述構建標誌刪除特徵來減少二進位制大小。
特別是,日誌記錄佔用了二進位制檔案中的大量空間,在嵌入式方案中可能不需要。設定UA_LOGLEVEL
為600以上的值(= FATAL)將禁用所有日誌記錄。此外,功能標記 UA_ENABLE_TYPENAMES
和UA_ENABLE_STATUSCODE_DESCRIPTIONS
向二進位制檔案新增靜態資訊,僅用於人類可讀的日誌記錄和除錯。
伺服器的RAM要求主要是由於以下設定:
- 資訊模型的大小
- 連線客戶端的數量
- 已分配的已配置最大郵件大小
安裝和包裝
您可以使用眾所周知的make install命令安裝open62541 。這允許您為自己的專案使用預先構建的庫和標頭。
要覆蓋預設安裝目錄,請使用。根據您選擇的SDK功能,如上所述,這些功能也將包含在安裝中。因此,我們建議為已安裝的二進位制檔案啟用盡可能多的功能。cmake -DCMAKE_INSTALL_PREFIX=/some/path
在您自己的CMake專案中,您可以使用以下命令包含open62541庫:
find_package(open62541 0.4.0 REQUIRED COMPONENTS Events DiscoveryMulticast)
add_executable(main main.cpp )
target_link_libraries(main open62541)
使用資料型別
OPC UA為可以在協議訊息中編碼的值定義型別系統。本教程介紹了可用資料型別及其用法的一些示例。有關完整定義,請參閱有關資料型別的部分。
基本資料處理
本節介紹資料型別的基本互動模式。確保與中的型別定義進行比較ua_types.h
。
#include <assert.h>
#include "open62541.h"
static void
variables_basic(void) {
/* Int32 */
UA_Int32 i = 5;
UA_Int32 j;
UA_Int32_copy(&i, &j);
UA_Int32 *ip = UA_Int32_new();
UA_Int32_copy(&i, ip);
UA_Int32_delete(ip);
/* String */
UA_String s;
UA_String_init(&s); /* _init zeroes out the entire memory of the datatype */
char *test = "test";
s.length = strlen(test);
s.data = (UA_Byte*)test;
UA_String s2;
UA_String_copy(&s, &s2);
UA_String_deleteMembers(&s2); /* Copying heap-allocated the dynamic content */
UA_String s3 = UA_STRING("test2");
UA_String s4 = UA_STRING_ALLOC("test2"); /* Copies the content to the heap */
UA_Boolean eq = UA_String_equal(&s3, &s4);
UA_String_deleteMembers(&s4);
if(!eq)
return;
/* Structured Type */
UA_CallRequest cr;
UA_init(&cr, &UA_TYPES[UA_TYPES_CALLREQUEST]); /* Generic method */
UA_CallRequest_init(&cr); /* Shorthand for the previous line */
cr.requestHeader.timestamp = UA_DateTime_now(); /* Members of a structure */
cr.methodsToCall = (UA_CallMethodRequest *)UA_Array_new(5, &UA_TYPES[UA_TYPES_CALLMETHODREQUEST]);
cr.methodsToCallSize = 5; /* Array size needs to be made known */
UA_CallRequest *cr2 = UA_CallRequest_new();
UA_copy(&cr, cr2, &UA_TYPES[UA_TYPES_CALLREQUEST]);
UA_CallRequest_deleteMembers(&cr);
UA_CallRequest_delete(cr2);
}
NodeIds
OPC UA資訊模型由節點和節點之間的引用組成。每個節點都有一個唯一的NodeId。NodeIds引用具有附加識別符號值的名稱空間,該識別符號值可以是整數,字串,guid或位元組串。
static void
variables_nodeids(void) {
UA_NodeId id1 = UA_NODEID_NUMERIC(1, 1234);
id1.namespaceIndex = 3;
UA_NodeId id2 = UA_NODEID_STRING(1, "testid"); /* the string is static */
UA_Boolean eq = UA_NodeId_equal(&id1, &id2);
if(eq)
return;
UA_NodeId id3;
UA_NodeId_copy(&id2, &id3);
UA_NodeId_deleteMembers(&id3);
UA_NodeId id4 = UA_NODEID_STRING_ALLOC(1, "testid"); /* the string is copied
to the heap */
UA_NodeId_deleteMembers(&id4);
}
變種
資料型別Variant屬於OPC UA的內建資料型別,用作容器型別。變體可以將任何其他資料型別儲存為標量(變體除外)或陣列。陣列變體還可以表示附加整數陣列中的資料的維度(例如,2×3矩陣)。
static void
variables_variants(void) {
/* Set a scalar value */
UA_Variant v;
UA_Int32 i = 42;
UA_Variant_setScalar(&v, &i, &UA_TYPES[UA_TYPES_INT32]);
/* Make a copy */
UA_Variant v2;
UA_Variant_copy(&v, &v2);
UA_Variant_deleteMembers(&v2);
/* Set an array value */
UA_Variant v3;
UA_Double d[9] = {1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0};
UA_Variant_setArrayCopy(&v3, d, 9, &UA_TYPES[UA_TYPES_DOUBLE]);
/* Set array dimensions */
v3.arrayDimensions = (UA_UInt32 *)UA_Array_new(2, &UA_TYPES[UA_TYPES_UINT32]);
v3.arrayDimensionsSize = 2;
v3.arrayDimensions[0] = 3;
v3.arrayDimensions[1] = 3;
UA_Variant_deleteMembers(&v3);
}
它遵循主要功能,利用上述定義。
int main(void) {
variables_basic();
variables_nodeids();
variables_variants();
return 0;
}
構建一個簡單的伺服器
本系列教程將指導您完成open62541的第一步。要編譯示例,您需要一個編譯器(MS Visual Studio 2015或更新版本,GCC,Clang和MinGW32都已知有效)。編制說明是為GCC提供的,但應該很容易適應。
安裝帶有圖形前端的OPC UA客戶端也非常有用,例如Unified Automation的UAExpert。這將使您能夠檢查任何OPC UA伺服器的資訊模型。
要開始,請從http://open62541.org下載open62541單檔案版本, 或者根據構建說明生成它,並啟用“amalgamation”選項。從現在開始,我們假設您擁有open62541.c/.h
當前資料夾中的檔案。現在建立一個myServer.c
使用以下內容呼叫的新C原始檔:
#include <signal.h>
#include "open62541.h"
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_ServerConfig *config = UA_ServerConfig_new_default();
UA_Server *server = UA_Server_new(config);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
UA_ServerConfig_delete(config);
return (int)retval;
}
這就是簡單的OPC UA伺服器所需要的一切。使用GCC編譯器,以下命令生成可執行檔案:
$ gcc -std=c99 open62541.c myServer.c -o myServer
在MinGW環境中,必須新增Winsock庫。
$ gcc -std=c99 open62541.c myServer.c -lws2_32 -o myServer.exe
現在啟動伺服器(使用ctrl-c停止):
$ ./myServer
您現在已經編譯並運行了第一個OPC UA伺服器。您可以繼續使用客戶端瀏覽資訊模型。伺服器正在監聽 opc.tcp://localhost:4840
。在接下來的兩節中,我們將繼續詳細解釋程式碼的不同部分。
伺服器配置和外掛
open62541為構建OPC UA伺服器和客戶端提供了靈活的框架。目標是擁有一個適用於所有用例並在所有平臺上執行的核心庫。然後,使用者可以通過配置和開發(特定於平臺的)外掛來調整庫以適合其用例。核心庫僅基於C99,甚至不需要基本的POSIX支援。例如,低階網路程式碼實現為可交換外掛。但別擔心。open62541提供了大多數平臺的外掛實現和開箱即用的合理預設配置。
在上面的伺服器程式碼中,我們簡單地採用預設伺服器配置並新增一個在埠4840上進行連線的TCP網路層。
伺服器生命週期
此示例中的程式碼顯示了伺服器生命週期管理的三個部分:建立伺服器,執行伺服器和刪除伺服器。一旦配置完成,建立和刪除伺服器就很簡單了。伺服器啟動時UA_Server_run
。在內部,伺服器然後使用超時來安排常規任務。在超時之間,伺服器在網路層上偵聽傳入訊息。
您可能會問伺服器如何知道何時停止執行。為此,我們建立了一個全域性變數running
。此外,我們已經註冊了stopHandler
捕獲程式在作業系統嘗試關閉時接收的訊號(中斷)的方法。例如,當您在終端程式中按ctrl-c時會發生這種情況。然後,訊號處理程式將變數設定running
為false,伺服器一旦收回控制權就會關閉。[1]
為了將OPC UA整合到具有自己的主迴圈的單執行緒應用程式中(例如由GUI工具包提供),可以選擇手動驅動伺服器。有關詳細資訊,請參閱Server Lifecycle上的伺服器文件部分 。
所有伺服器都需要伺服器配置和生命週期管理。我們將在以下教程中使用它而無需進一步評論。
[1] | 在多執行緒應用程式中要小心全域性變數。您可能希望running 在堆上分配變數。 |
將變數新增到伺服器
本教程介紹如何使用資料型別以及如何將變數節點新增到伺服器。首先,我們向伺服器新增一個新變數。檢視UA_VariableAttrbitues
結構的定義以檢視為VariableNodes定義的所有屬性的列表。
請注意,預設設定將變數值的AccessLevel設定為只讀。請參閱下文,瞭解變數是否可寫。
#include <signal.h>
#include <stdio.h>
#include "open62541.h"
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);
}
現在我們使用寫入服務更改值。這使用了相同的服務實現,也可以通過OPC UA客戶端在網路上實現。
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);
}
注意我們最初如何將變數節點的DataType屬性設定為Int32資料型別的NodeId。這禁止寫入不是Int32的值。以下程式碼顯示瞭如何為每次寫入執行此一致性檢查。
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));
}
它遵循主伺服器程式碼,使用上述定義。
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_ServerConfig *config = UA_ServerConfig_new_default();
UA_Server *server = UA_Server_new(config);
addVariable(server);
writeVariable(server);
writeWrongVariable(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
UA_ServerConfig_delete(config);
return (int)retval;
}
將變數與物理過程連線
在基於OPC UA的體系結構中,伺服器通常位於資訊源附近。在工業環境中,這轉化為靠近物理過程的伺服器和在執行時消耗資料的客戶端。在上一個教程中,我們瞭解瞭如何將變數新增到OPC UA資訊模型中。本教程介紹如何將變數連線到執行時資訊,例如從物理過程的測量值。為簡單起見,我們將系統時鐘作為基礎“過程”。
以下程式碼片段分別涉及在執行時更新變數值的不同方法。總之,程式碼片段定義了可編譯的原始檔。
手動更新變數
作為起點,假設已在伺服器中建立了DateTime型別值的變數, 識別符號為“ns = 1,s = current-time”。假設當一個新值從底層程序到達時我們的應用程式被觸發,我們就可以寫入變數。
#include <signal.h>
#include "open62541.h"
static void
updateCurrentTime(UA_Server *server) {
UA_DateTime now = UA_DateTime_now();
UA_Variant value;
UA_Variant_setScalar(&value, &now, &UA_TYPES[UA_TYPES_DATETIME]);
UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time");
UA_Server_writeValue(server, currentNodeId, value);
}
static void
addCurrentTimeVariable(UA_Server *server) {
UA_DateTime now = 0;
UA_VariableAttributes attr = UA_VariableAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en-US", "Current time");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_Variant_setScalar(&attr.value, &now, &UA_TYPES[UA_TYPES_DATETIME]);
UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time");
UA_QualifiedName currentName = UA_QUALIFIEDNAME(1, "current-time");
UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
UA_NodeId variableTypeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);
UA_Server_addVariableNode(server, currentNodeId, parentNodeId,
parentReferenceNodeId, currentName,
variableTypeNodeId, attr, NULL, NULL);
updateCurrentTime(server);
}
變數值回撥
當值連續變化時,例如系統時間,在緊密迴圈中更新值將佔用大量資源。值回撥允許將變數值與外部表示同步。它們將回調附加到在每次讀取之前和每次寫入操作之後執行的變數。
static void
beforeReadTime(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *nodeid, void *nodeContext,
const UA_NumericRange *range, const UA_DataValue *data) {
UA_DateTime now = UA_DateTime_now();
UA_Variant value;
UA_Variant_setScalar(&value, &now, &UA_TYPES[UA_TYPES_DATETIME]);
UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time");
UA_Server_writeValue(server, currentNodeId, value);
}
static void
afterWriteTime(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *nodeId, void *nodeContext,
const UA_NumericRange *range, const UA_DataValue *data) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"The variable was updated");
}
static void
addValueCallbackToCurrentTimeVariable(UA_Server *server) {
UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time");
UA_ValueCallback callback ;
callback.onRead = beforeReadTime;
callback.onWrite = afterWriteTime;
UA_Server_setVariableNode_valueCallback(server, currentNodeId, callback);
}
可變資料來源
使用值回撥,該值仍儲存在變數節點中。所謂的資料來源更進了一步。伺服器將每個讀寫請求重定向到回撥函式。在讀取時,回撥提供當前值的副本。在內部,資料來源需要實現自己的記憶體管理。
static UA_StatusCode
readCurrentTime(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *nodeId, void *nodeContext,
UA_Boolean sourceTimeStamp, const UA_NumericRange *range,
UA_DataValue *dataValue) {
UA_DateTime now = UA_DateTime_now();
UA_Variant_setScalarCopy(&dataValue->value, &now,
&UA_TYPES[UA_TYPES_DATETIME]);
dataValue->hasValue = true;
return UA_STATUSCODE_GOOD;
}
static UA_StatusCode
writeCurrentTime(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *nodeId, void *nodeContext,
const UA_NumericRange *range, const UA_DataValue *data) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Changing the system time is not implemented");
return UA_STATUSCODE_BADINTERNALERROR;
}
static void
addCurrentTimeDataSourceVariable(UA_Server *server) {
UA_VariableAttributes attr = UA_VariableAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en-US", "Current time - data source");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time-datasource");
UA_QualifiedName currentName = UA_QUALIFIEDNAME(1, "current-time-datasource");
UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
UA_NodeId variableTypeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);
UA_DataSource timeDataSource;
timeDataSource.read = readCurrentTime;
timeDataSource.write = writeCurrentTime;
UA_Server_addDataSourceVariableNode(server, currentNodeId, parentNodeId,
parentReferenceNodeId, currentName,
variableTypeNodeId, attr,
timeDataSource, NULL, NULL);
}
它遵循主伺服器程式碼,使用上述定義。
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_ServerConfig *config = UA_ServerConfig_new_default();
UA_Server *server = UA_Server_new(config);
addCurrentTimeVariable(server);
addValueCallbackToCurrentTimeVariable(server);
addCurrentTimeDataSourceVariable(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
UA_ServerConfig_delete(config);
return (int)retval;
}
DataChange通知
對變數的當前值感興趣的客戶端不需要定期輪詢變數。相反,他可以使用訂閱機制來通知有關更改。
在Subscription中,客戶端添加了所謂的MonitoredItems。DataChange MonitoredItem定義監視更改的節點屬性(通常是值屬性)。伺服器在內部讀取定義的時間間隔內的值並生成相應的通知。上面討論的更新節點值的三種方式都可以與通知結合使用。這是因為通知使用標準的讀取服務來查詢值更改。
使用變數型別
變數型別有三個功能:
- 約束該型別變數的可能資料型別,值排名和陣列維度。這允許根據泛型型別定義編寫介面程式碼,因此它適用於所有例項。
- 提供合理的預設值
- 根據變數的型別啟用變數的語義解釋
在本教程的示例中,我們通過double值陣列表示2D空間中的一個點。以下函式將相應的VariableTypeNode新增到變數型別的層次結構中。
#include <signal.h>
#include "open62541.h"
static UA_NodeId pointTypeId;
static void
addVariableType2DPoint(UA_Server *server) {
UA_VariableTypeAttributes vtAttr = UA_VariableTypeAttributes_default;
vtAttr.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
vtAttr.valueRank = UA_VALUERANK_ONE_DIMENSION;
UA_UInt32 arrayDims[1] = {2};
vtAttr.arrayDimensions = arrayDims;
vtAttr.arrayDimensionsSize = 1;
vtAttr.displayName = UA_LOCALIZEDTEXT("en-US", "2DPoint Type");
/* a matching default value is required */
UA_Double zero[2] = {0.0, 0.0};
UA_Variant_setArray(&vtAttr.value, zero, 2, &UA_TYPES[UA_TYPES_DOUBLE]);
UA_Server_addVariableTypeNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
UA_QUALIFIEDNAME(1, "2DPoint Type"), UA_NODEID_NULL,
vtAttr, NULL, &pointTypeId);
}
現在可以在建立新變數期間引用2DPoint的新變數型別。如果未給出值,則在例項化期間複製變數型別的預設值。
static UA_NodeId pointVariableId;
static void
addVariable(UA_Server *server) {
/* Prepare the node attributes */
UA_VariableAttributes vAttr = UA_VariableAttributes_default;
vAttr.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
vAttr.valueRank = UA_VALUERANK_ONE_DIMENSION;
UA_UInt32 arrayDims[1] = {2};
vAttr.arrayDimensions = arrayDims;
vAttr.arrayDimensionsSize = 1;
vAttr.displayName = UA_LOCALIZEDTEXT("en-US", "2DPoint Variable");
vAttr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
/* vAttr.value is left empty, the server instantiates with the default value */
/* Add the node */
UA_Server_addVariableNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "2DPoint Type"), pointTypeId,
vAttr, NULL, &pointVariableId);
}
在建立型別的新變數例項時,會強制執行變數型別的約束。在以下函式中,新增帶有字串值的2DPoint型別的變數 失敗,因為該值與變數型別約束不匹配。
static void
addVariableFail(UA_Server *server) {
/* Prepare the node attributes */
UA_VariableAttributes vAttr = UA_VariableAttributes_default;
vAttr.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
vAttr.valueRank = UA_VALUERANK_SCALAR; /* a scalar. this is not allowed per the variable type */
vAttr.displayName = UA_LOCALIZEDTEXT("en-US", "2DPoint Variable (fail)");
UA_String s = UA_STRING("2dpoint?");
UA_Variant_setScalar(&vAttr.value, &s, &UA_TYPES[UA_TYPES_STRING]);
/* Add the node will return UA_STATUSCODE_BADTYPEMISMATCH*/
UA_Server_addVariableNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "2DPoint Type (fail)"), pointTypeId,
vAttr, NULL, NULL);
}
在編寫變數的資料型別,valuerank和arraydimensions屬性時,會強制執行變數型別的約束。反過來,這會約束變數的value屬性。
static void
writeVariable(UA_Server *server) {
UA_StatusCode retval = UA_Server_writeValueRank(server, pointVariableId, UA_VALUERANK_ONE_OR_MORE_DIMENSIONS);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting the Value Rank failed with Status Code %s",
UA_StatusCode_name(retval));
}
它遵循主伺服器程式碼,使用上述定義。
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_ServerConfig *config = UA_ServerConfig_new_default();
UA_Server *server = UA_Server_new(config);
addVariableType2DPoint(server);
addVariable(server);
addVariableFail(server);
writeVariable(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
UA_ServerConfig_delete(config);
return (int)retval;
}
使用物件和物件型別
使用物件構建資訊模型
假設我們想要在OPC UA資訊模型中對一組泵及其執行時狀態進行建模。當然,所有泵的表示都應遵循相同的基本結構。例如,我們可能在SCADA視覺化中具有泵的圖形表示,該視覺化應對所有泵都是可恢復的。
遵循面向物件的程式設計範例,每個泵都由一個具有以下佈局的物件表示:
以下程式碼手動定義泵及其成員變數。我們省略了對變數值的設定約束,因為這不是本教程的重點,並且已經涵蓋了。
#include <signal.h>
#include "open62541.h"
static void
manuallyDefinePump(UA_Server *server) {
UA_NodeId pumpId; /* get the nodeid assigned by the server */
UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
oAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Pump (Manual)");
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "Pump (Manual)"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
oAttr, NULL, &pumpId);
UA_VariableAttributes mnAttr = UA_VariableAttributes_default;
UA_String manufacturerName = UA_STRING("Pump King Ltd.");
UA_Variant_setScalar(&mnAttr.value, &manufacturerName, &UA_TYPES[UA_TYPES_STRING]);
mnAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ManufacturerName");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "ManufacturerName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, NULL, NULL);
UA_VariableAttributes modelAttr = UA_VariableAttributes_default;
UA_String modelName = UA_STRING("Mega Pump 3000");
UA_Variant_setScalar(&modelAttr.value, &modelName, &UA_TYPES[UA_TYPES_STRING]);
modelAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ModelName");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "ModelName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, NULL, NULL);
UA_VariableAttributes statusAttr = UA_VariableAttributes_default;
UA_Boolean status = true;
UA_Variant_setScalar(&statusAttr.value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
statusAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Status");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Status"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, NULL, NULL);
UA_VariableAttributes rpmAttr = UA_VariableAttributes_default;
UA_Double rpm = 50.0;
UA_Variant_setScalar(&rpmAttr.value, &rpm, &UA_TYPES[UA_TYPES_DOUBLE]);
rpmAttr.displayName = UA_LOCALIZEDTEXT("en-US", "MotorRPM");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "MotorRPMs"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, NULL, NULL);
}
物件型別,型別層次結構和例項化
手動構建每個物件需要我們編寫大量程式碼。此外,客戶端無法檢測到物件代表泵。(我們可能會使用命名約定或類似方法來檢測泵。但這並不是一個乾淨的解決方案。)此外,我們可能擁有的裝置多於泵。我們要求所有裝置共享一些共同的結構。解決方案是在具有繼承關係的層次結構中定義ObjectType。
標記為必需的子項將與父物件一起自動例項化。這由對代表強制建模規則的物件的hasModellingRule引用指示。
/* predefined identifier for later use */
UA_NodeId pumpTypeId = {1, UA_NODEIDTYPE_NUMERIC, {1001}};
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);
}
現在我們為從裝置物件型別繼承的泵新增派生的ObjectType。結果物件包含所有必需的子變數。這些只是從物件型別複製而來。該物件具有hasTypeDefinition
物件型別的型別引用,以便客戶端可以在執行時檢測型別 - 例項關係。
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);
}
通常,我們希望在新物件上執行建構函式。當在執行時例項化物件(使用AddNodes服務)並且可以手動定義與底層程序的整合時,這尤其有用。在以下建構函式示例中,我們只需將泵狀態設定為on。
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_deleteMembers(&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);
}
它遵循主伺服器程式碼,使用上述定義。
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_ServerConfig *config = UA_ServerConfig_new_default();
UA_Server *server = UA_Server_new(config);
manuallyDefinePump(server);
defineObjectTypes(server);
addPumpObjectInstance(server, "pump2");
addPumpObjectInstance(server, "pump3");
addPumpTypeConstructor(server);
addPumpObjectInstance(server, "pump4");
addPumpObjectInstance(server, "pump5");
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
UA_ServerConfig_delete(config);
return (int)retval;
}
向物件新增方法
OPC UA資訊模型中的物件可以包含與程式語言中的物件類似的方法。方法由MethodNode表示。請注意,多個物件可能引用相同的MethodNode。例項化物件型別時,將新增對方法的引用,而不是複製MethodNode。因此,在呼叫方法時,始終顯式宣告上下文物件的識別符號。
方法回撥將附加到方法節點的自定義資料指標,呼叫該方法的物件的識別符號以及輸入和輸出引數的兩個陣列作為輸入。輸入和輸出引數都是Variant型別。每個變體又可以包含任何資料型別的(多維)陣列或標量。
方法引數的約束是根據資料型別,值排名和陣列維度(類似於變數定義)定義的。引數定義儲存在MethodNode的子VariableNodes中,並帶有相應的BrowseNames 和。(0, "InputArguments")
(0, "OutputArguments")
示例:Hello World方法
該方法採用字串標量並返回字首為“Hello”的字串標量。SDK內部檢查輸入引數的型別和長度,這樣我們就不必驗證回撥中的引數。
#include <signal.h>
#include "open62541.h"
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.