注:原文PDF文档 是从官网下载的 Linux64bit的发布版本中自带的文档,原PDF中的源代码用PDF浏览器查看,有残缺。需要结合源文件中的示例代码进行相应的修改。或参考其它版本的文档。原文代码中的注释并没有译文,这个在使用时再补充进去。
第一篇 章节 一 至 五
第二篇 章节 六
第三篇 章节 七 至 十
第四篇 章节 十一 至 十三
open62541(http://open62541.org)是OPC UA(OPC统一架构)的开源和免费实现,用 C99和C++98语言的公共子系统中编写。该库可用于所有主要的计算机并提供了实现专用OPC-UA客户端和服务器,或基于现有应用程序的通信集成OPC-UA的必要工具。open62541库是独立于平台的。所有特定平台功能是通过可交换的插件实现的。大多操作系统提供了插件实现。
open62541是根据Mozilla Public License v2.0(MPLv2)授权的。这允许open62541库与任何专有软件结合和分发。只有对open62541库本身的更改需要在复制和分发时根据MPLv2授权。客户端和服务器以及插件在公共域中(CC0许可证)。它们可以在任何许可下重用,并且不必进行发布更改。
使用open62541 v1.0构建的示例服务器(server ctt)符合OPC Foundation的“微型嵌入式设备服务器”配置文件,支持OPC UA客户端/服务器通信、订阅、方法调用和安全(加密),安全策略为“Basic128Rsa15”、“Basic256”和“Basic256Sha256”以及facets“方法服务器”和“节点管理”。更多细节 https://open62541.org/certified-sdk。
OPC UA 是一种工业通信协议,已在 IEC 62541系列中标准化。
OPC UA定义
标准本身可从IEC购买或从OPC基金会网站免费下载,网址为https://opcfoundation.org/(您需要使用有效的电子邮件进行注册)。
OPC基金会推动了标准的不断改进和配套规范的发展。配套规范将已建立的概念和可重用组件从应用领域转换为OPC-UA。它们是与应用程序域中已建立的行业委员会或标准化机构联合创建的。此外,OPC基金会还组织了标准传播活动,并提供了合规认证的基础设施和工具。
open62541实现了OPC-UA二进制协议栈以及客户端和服务器SDK。它目前支持Micro-Embedded Device服务器配置文件和一些附加功能。服务器二进制文件的大小可以小于100kb,这取决于所包含的信息模型。
最初的发行版v0.3版本中缺少的功能包括:
除此文档外,您还可以通过以下方式访问open62541社区
作为一个开源项目,我们邀请新的贡献者来帮助改进open62541。问题报告,错误修复和新功能是非常欢迎的。以下是新贡献者良好的起点:
open62541使用CMake构建库和二进制文件。使用git describe自动检测库版本。此命令基于当前标记返回有效的版本字符串。如果不是直接克隆源代码,而是使用某个版本中的tar或zip包,则需要手动指定版本。在这种情况下,使用例如
cmake -DOPEN62541_VERSION=v1.0.3
sudo apt-get install git build-essential gcc pkg-config cmake python
# 启用其它功能
sudo apt-get install cmake-curses-gui # ccmake图形界面
sudo apt-get install libmbedtls-dev # 加密支持
sudo apt-get install check libsubunit-dev # 单元测试
sudo apt-get install python-sphinx graphviz # for 文档生成
sudo apt-get install python-sphinx-rtd-theme # 文档样式
cd open62541
mkdir build
cd build
cmake ..
make
# 选择其他功能
ccmake ..
make
# 生成文档
make doc # html文档
make doc_pdf # pdf文档(需要LaTeX)
在这里,我们将介绍 VisualStudio(2013或更新版本)的构建过程。要使用MinGW构建,只需替换CMake调用中的编译器选择。
cd \open62541
mkdir build
cd build
\cmake.exe .. -G "Visual Studio 14 2015"
sudo easy_install pip
brew install cmake
pip install sphinx # 用户文档生成
pip install sphinx_rtd_theme # 文档样式
brew install graphviz # 文档图形
brew install check # 单元测试
遵循Ubuntu的说明,不使用apt-get命令,因为这些命令都是由上面的包处理的。
下面的过程适用于openbsd5.8,gcc版本4.8.4,cmake版本3.2.3和Python版本2.7.10。
pkg_add gcc python cmake
export CC=egcc CXX=eg++
cd open62541
mkdir build
cd build
cmake ..
make
下面是一个如何在Docker容器中将库构建为Debian包的示例
按照说明安装Docker:https://docs.docker.com/install/linux/docker-ce/debian/
从github获取docker deb builder实用程序,并为所需的Debian或Ubuntu releases生成docker映像
# 创建开发目录 (例如 ~/development)
mkdir ~/development
cd ~/development
# 进入开发目录,克隆docker-deb-builder
git clone https://github.com/tsaarni/docker-deb-builder.git
cd docker-deb-builder
# 生成 Docker 镜像 (例如 Ubuntu 18.04 and 17.04)
docker build -t docker-deb-builder:18.04 -f Dockerfile-ubuntu-18.04 .
docker build -t docker-deb-builder:17.04 -f Dockerfile-ubuntu-17.04 .
制作open62541 git repo的本地副本并签出pack分支
# 制作open62541 git 仓库的本地副本 (e.g. in the home directory)
# 签出一个分支 (e.g. pack/1.0)
cd ~
git clone https://github.com/open62541/open62541.git
cd ~/open62541
git checkout pack/1.0
现在它已经准备好构建Debian/Ubuntu open62541包了
# 进入开发目录
cd ~/development
# 为生成器创建一个本地输出目录,在该目录下可以放置包 build
mkdir output
# 在Docker容器中构建Debian/Ubuntu包(例如Ubuntu-18.04)
./build -i docker-deb-builder:18.04 -o output ~/open62541
成功构建后可在 ~/development/docker-deb-builder/output 目录中找到。
如果open62541库将使用pack分支(例如pack/master或pack/1.0)构建为Debian包,那么如果使用开发分支(例如master或1.0),那么应该在 Debian/rules文件中修改或添加CMake构建选项。
debian/rules中定义CMake构建选项的部分是
...
override_dh_auto_configure:
dh_auto_configure -- -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUA_NAMESPACE_ZERO=FULL -DUA_ENABLE_AMALGAMATION=OFF -DUA_PACK_DEBIAN=ON
...
在Debian打包期间,这个CMake构建选项将作为命令行变量传递给CMake。
open62541项目使用CMake来管理构建选项、代码生成以及为不同的系统和ide生成构建项目。工具ccmake或cmake gui可用于以图形方式设置构建选项。大多数选项可以在代码生成后在ua_config.h(对于单文件版本为open62541.h)中手动更改。但通常不需要调整它们。
CMAKE_BUILD_TYPE
UA_LOGLEVEL
SDK只记录UA_LOGLEVEL中定义级别及以上级别的事件
事件级别如下:
• 600: Fatal
• 500: Error
• 400: Warning
• 300: Info
• 200: Debug
• 100: Trace
UA_MULTITHREADING
多线程支持级别。当前支持的级别如下:
• 0-199: 多线程支持已禁用.
• 100-199: 用UA_THREADSAFE-macro标记的API函数用互斥锁进行内部保护。允许多个线程同时调用SDK的这些函数,而不会造成争用条件。此外,此级别还支持处理来自外部工作线程的异步方法调用。
• >=200: 工作分配给多个内部工作线程。这些工作线程是在SDK中创建的。(实验功能!预计会有错误。)
默认情况下,只生成主库共享对象Libopen62541.so(open62541.dll)或静态链接存档open62541.a(open62541.lib)。其他工件可以通过以下选项指定:
UA_BUILD_EXAMPLES :从examples/*.c编译示例服务器和客户端。
UA_BUILD_UNIT_TESTS :编译单元测试。可以使用maketest执行测试
UA_BUILD_SELFSIGNED_CERTIFICATE :为服务器生成自签名证书(需要openSSL)
UA_ENABLE_SUBSCRIPTIONS:启用订阅
UA_ENABLE_SUBSCRIPTIONS_EVENTS (EXPERIMENTAL) :启用对订阅使用事件。这是一个新功能,目前是实验性的。
UA_ENABLE_SUBSCRIPTIONS_ALARMS_CONDITIONS (EXPERIMENTAL) :为订阅启用A&C。这是一个基于事件构建的新功能,目前标记为实验性的。
UA_ENABLE_METHODCALLS :启用方法服务集。
UA_ENABLE_PARSING:允许解析内置数据类型(Guid、NodeId等)的可读格式。不是SDK必需的实用程序函数。UA_ENABLE_NODEMANAGEMENT :在运行时启用动态添加和删除节点
UA_ENABLE_AMALGAMATION:将一个文件版本编译成open62541.c和open62541.h文件。不建议安装。
UA_ENABLE_IMMUTABLE_NODES:信息模型中的节点不会被编辑,而是被复制和替换。替换是通过原子操作完成的,这样信息模型总是一致的,并且可以从中断或并行线程访问(取决于节点存储插件的实现)。此功能是 UA_MULTITHREADING 的先决条件。 .
UA_ENABLE_COVERAGE:测量单元测试的覆盖率
UA_ENABLE_DISCOVERY:启用发现服务(LDS)
UA_ENABLE_DISCOVERY_MULTICAST:启用具有多播支持的发现服务(LDS-ME)
UA_ENABLE_DISCOVERY_SEMAPHORE:启用发现信号量支持
UA_NAMESPACE_ZERO
命名空间 Namespace zero 包含标准定义的节点。可能不需要完整的 Namespace zero 适用于所有应用。可选选项如下:
高级构建选项 UA_FILE_NS0 可用于重写 XML文件的命名空间。 .
有些选项标记为高级。需要切换高级选项才能在cmake gui中可见。.
UA_ENABLE_TYPEDESCRIPTION:向 UA_DataType 结构添加类型和成员名称。默认启用。
UA_ENABLE_STATUSCODE_DESCRIPTIONS:将状态码的可读名称编译为二进制文件。默认启用
UA_ENABLE_FULL_NS0:使用完整的NS0而不是最小的命名空间0节点集 UA__FILE_NS0用于指定从namespace0文件夹生成NS0的文件。默认值为 Opc.Ua.NodeSet2.xml 文件。
此组包含的构建选项主要用于库本身的开发。
UA_DEBUG:启用不用于生产构建的断言和其他定义
UA_DEBUG_DUMP_PKGS:以hextump格式转储服务器接收到的每个包
open62541足够小,大多数用户都希望静态地将库链接到他们的程序中。如果需要共享库(.dll,.so),可以在CMake中使用BUILD_SHARED_LIBS 选项启用此功能。注意,这个选项修改了ua_config.h文件,该文件也包含在open62541.h中,用于单个文件分发。
通过调整构建配置,可以大大减少 open2541 生成的二进制文件的大小。可以配置需要少于100kB的RAM和ROM的最小服务器。
以下选项影响ROM要求:
首先,在CMake中,构建类型可以设置为 CMAKE_BUILD_TYPE=MinSizeRel。这将设置编译器标志以最小化二进制大小。构建类型也会去掉调试信息。其次,可以通过上述构建标志删除特性来减小二进制大小。
第二,将UA_NAMESPACE_ZERO设置为MINIMAL可减小内置信息模型的大小。在某些情况下,设置此选项可以将二进制大小减少一半。
第三,有些特性可能不需要,可以禁用以减少二进制占用。例如订阅或加密通信。
最后,记录消息会占用二进制文件中的大量空间,在嵌入式场景中可能不需要。将UA_LOGLEVEL设置为大于600(致命)的值将禁用所有日志记录。此外,特性标志UA_ENABLE_TYPEDESCRIPTION和UA_ENABLE_STATUSCODE_description将静态信息添加到仅用于人类可读的日志记录和调试的二进制文件中。
服务器的RAM需求主要由以下设置引起:
确保您可以按照前面的步骤构建共享库。构建示例的更简单的方法是在操作系统中安装open62541(请参阅安装open62541)
cp /path-to/examples/tutorial_server_firststeps.c . # copy the example server
gcc -std=c99 -o server tutorial_server_firststeps.c -lopen62541
open62541库可以为许多操作系统和嵌入式系统构建。本文展示了一个已经测试过的架构的小片段。由于堆栈只使用C99标准,所以有更多支持的体系结构。
在arch文件夹中可以找到实现的体系结构支持的完整列表。
默认情况下支持这些体系结构,并由CMake自动选择。
看看前面关于如何做到这一点的部分。
归功于@cabralfortiss
本文件基于对PR的讨论https://github.com/open62541/open62541/pull/2511。如果你有任何疑问,请先检查一下那里的讨论。
本文档假设您有一个使用LwIP和freeRTOS的基本示例,并且您只想向其中添加一个OPC-UA任务。
为freeRTOS+LwIP构建open62541的主要方法有两种:
在CMake中,选择freertosLWIP 使用变量 UA_ARCHITECTURE 和 UA_ENABLE_AMALGAMATION 启用合并,然后只选择本机编译器。然后试着像往常一样编译。编译将失败,但将生成open62541.h和open62541.c。
注意:如果使用freeRTOS(pvPortMalloc和family)的内存分配函数,则还需要将变量UA_ARCH_FREERTOS_USE_OWN_MEMORY_FUNCTIONS 设置为true。许多用户不得不实现pvPortCalloc和pvPortRealloc。
如果使用终端,命令应该如下所示:
mkdir build_freeRTOS
cd build_freeRTOS
cmake -DUA_ARCHITECTURE=freertosLWIP -DUA_ENABLE_AMALGAMATION=ON ../
make
记住,编译将失败。这不是问题,因为您只需要在您试图编译的目录中找到的生成的文件(open62541.h和open62541.c)。在您正在使用的IDE中导入这些。在所有IDE中没有标准的方法来执行以下操作,但是您需要在项目中执行以下配置:
在编译LwIP时,需要一个名为lwipopts.h的文件。在这个文件中,您将所有配置变量放入。您需要确保有以下配置:
#define LWIP_COMPAT_SOCKETS 0 // 不要在网络函数名中定义名称转换。
#define LWIP_SOCKET 1 // 启用套接字API(通常已设置)
#define LWIP_DNS 1 // 启用lwip_getaddrinfo函数、struct addrinfo等。
#define SO_REUSE 1 // 允许将套接字设置为可重用
#define LWIP_TIMEVAL_PRIVATE 0 // 这是可选的。如果出现编译错误,请设置此标志
对于freeRTOS,有一个类似的文件FreeRTOSConfig.h。通常,您应该有一个包含此文件的示例项目。建议检查的只有两个变量:
#define configCHECK_FOR_STACK_OVERFLOW 1
#define configUSE_MALLOC_FAILED_HOOK 1
在freeRTOS+LwIP中运行OPC-UA服务器时,大多数问题都来自这样一个事实:通常部署在内存有限的嵌入式系统中,因此这些定义将允许检查是否存在内存问题(将节省大量查找隐藏问题的工作)
现在,您需要添加启动OPC UA服务器的任务。
static void opcua_thread(void *arg){
//用于发送和接收缓冲区的默认64KB内存给许多用户造成了问题
UA_UInt32 sendBufferSize = 16000; //64 KB 对我的平台来说太多了
UA_UInt32 recvBufferSize = 16000; //64 KB 对我的平台来说太多了
UA_UInt16 portNumber = 4840;
UA_Server* mUaServer = UA_Server_new();
UA_ServerConfig *uaServerConfig = UA_Server_getConfig(mUaServer);
UA_ServerConfig_setMinimal(uaServerConfig, portNumber, 0, sendBufferSize, recvBufferSize);
//VERY IMPORTANT: 在启动服务器之前,用IP设置主机名
UA_ServerConfig_setCustomHostname(uaServerConfig, UA_STRING("192.168.0.102"));
//其余与示例相同
UA_Boolean running = true;
// 向地址空间添加变量节点
UA_VariableAttributes attr = UA_VariableAttributes_default;
UA_Int32 myInteger = 42;
UA_Variant_setScalarCopy(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US","the answer");
attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US","the answer");
UA_NodeId myIntegerNodeId = UA_NODEID_STRING_ALLOC(1, "the.answer");
UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME_ALLOC(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(mUaServer, myIntegerNodeId, parentNodeId,
parentReferenceNodeId, myIntegerNa
UA_NODEID_NULL, attr, NULL, NULL);
/* 需要在堆上释放分配 */
UA_VariableAttributes_clear(&attr);
UA_NodeId_clear(&myIntegerNodeId);
UA_QualifiedName_clear(&myIntegerName);
UA_StatusCode retval = UA_Server_run(mUaServer, &running);
UA_Server_delete(mUaServer);
}
在主函数中,初始化TCP IP堆栈和所有硬件后,需要添加任务:
//8000是堆栈大小,8是优先级。此值可根据您的需要修改
if(NULL == sys_thread_new("opcua_thread", opcua_thread, NULL, 8000, 8))
LWIP_ASSERT("opcua(): Task creation failed.", 0);
And lastly, in the same file (or any actually) add:
void vApplicationMallocFailedHook(){
for(;;){
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName ){
for(;;){
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
并在每个vTaskDelay中设置一个断点。当堆或堆栈中出现问题时,将调用这些函数。如果程序到了这里,你的内存有问题。
就这样。您的OPC UA服务器应该运行平稳。如果问题访问 https://github.com/open62541/open62541/pull/2511。
可以使用众所周知的 make install 命令安装open62541。这允许您为自己的项目使用预构建的库和头。
要覆盖默认安装目录,请使用 cmake -DCMAKE_INSTALL_PREFIX=/some/path 。基于在您选择的SDK特性上,如构建选项中所述,这些特性也将包括在安装中。因此,我们建议为已安装的二进制文件启用尽可能多的非实验特性。
对于默认安装,建议的cmake选项包括:
git submodule update --init --recursive
mkdir build && cd build
cmake -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUA_NAMESPACE_ZERO=FULL ..
make
sudo make install
这将在0.4中启用以下功能:
以下功能未启用,可以使用生成选项(如“生成选项”中所述)启用:
重要提示:强烈建议不要在安装时使用 UA_ENABLE_AMALGAMATION=ON。这将只生成一个open62541.h头文件,而不是单个头文件。我们鼓励用户使用非合并版本来减小头大小并简化依赖关系管理。
在您自己的CMake项目中,您可以使用以下方法包括open62541库:
# 也可以指定特定版本
# 例如 find_package(open62541 1.0.0)
find_package(open62541 REQUIRED COMPONENTS Events FullNamespace)
add_executable(main main.cpp)
target_link_libraries(main open62541::open62541)
在构建期间启用的功能的完整列表存储在CMake变量 open62541_COMPONENTS_ALL 中
Github允许您以.zip包的形式下载特定的分支。仅使用open62541的.zip包可能会失败:
因此我们提供包分支。它们具有前缀pack/并自动更新以匹配引用的分支。
以下是一些示例:
这些包分支有内联的子模块,版本字符串是硬编码的。如果您需要从源代码构建但不想使用git,请使用这些特定的包版本。
在我们的Github发布页面上,您总能找到每个版本的预构建二进制文件。
本这里 https://open62541.org/releases/ 可以找到最近50次提交的Linux和Windows的单文件版。
Debian软件包可以在我们的官方PPA中找到:
安装:
sudo add-apt-repository ppa:open62541-team/ppa
sudo apt-get update
sudo apt-get install libopen62541-1-dev
Arch 在AUR提供
稳定版本:https://aur.archlinux.org/packages/open62541/
不稳定的生成:https://aur.archlinux.org/packages/open62541-git/
为了添加自定义生成选项(生成选项),可以设置环境变量 OPEN62541_CMAKE_FLAGS
从 OpenBSD 6.7开始,目录misc/open62541可以构建open62541的发布版本。
从OpenBSD镜像安装二进制软件包:pkg_add open62541
本节显示数据类型的基本交互模式。确保在 types.h 中定义。
#include
#include
#include
#include
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); /* 初始化内存 */
char *test = "test";
s.length = strlen(test);
s.data = (UA_Byte*)test;
UA_String s2;
UA_String_copy(&s, &s2);
UA_String_clear(&s2); /* 复制分配了动态内容的堆 */
UA_String s3 = UA_STRING("test2");
UA_String s4 = UA_STRING_ALLOC("test2"); /* 将内容复制到堆 */
UA_Boolean eq = UA_String_equal(&s3, &s4);
UA_String_clear(&s4);
if(!eq)
return;
/* 结构体类型 */
UA_ReadRequest rr;
UA_init(&rr, &UA_TYPES[UA_TYPES_READREQUEST]); /* 泛型方法 */
UA_ReadRequest_init(&rr); /* 上一行的速记 */
rr.requestHeader.timestamp = UA_DateTime_now(); /* 结构体成员 */
rr.nodesToRead = (UA_ReadValueId *)UA_Array_new(5, &UA_TYPES[UA_TYPES_READVALUEID]);
rr.nodesToReadSize = 5; /* 需要知道数组大小 */
UA_ReadRequest *rr2 = UA_ReadRequest_new();
UA_copy(&rr, rr2, &UA_TYPES[UA_TYPES_READREQUEST]);
UA_ReadRequest_clear(&rr);
UA_ReadRequest_delete(rr2);
}
OPC-UA信息模型由节点和节点间的引用组成。每个节点都有一个唯一的NodeId。nodeid指的是具有附加标识符值的命名空间,该标识符值可以是整数、字符串、guid或bytestring。
static void
variables_nodeids(void) {
UA_NodeId id1 = UA_NODEID_NUMERIC(1, 1234);
id1.namespaceIndex = 3;
UA_NodeId id2 = UA_NODEID_STRING(1, "testid"); /* 字符串是静态的 */
UA_Boolean eq = UA_NodeId_equal(&id1, &id2);
if(eq)
return;
UA_NodeId id3;
UA_NodeId_copy(&id2, &id3);
UA_NodeId_clear(&id3);
UA_NodeId id4 = UA_NODEID_STRING_ALLOC(1, "testid"); /* 字符串被复制到堆中 */
UA_NodeId_clear(&id4);
}
数据类型变量属于OPC UA的内置数据类型,用作容器类型。变量可以将任何其他数据类型保存为标量(变量除外)或数组。数组变量还可以表示额外整数数组中数据的维数(例如2x3矩阵)
static void
variables_variants(void) {
/* 设置标量值 */
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_clear(&v2);
/* 设置数组值 */
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]);
/* 设置数组维度 */
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_clear(&v3);
}
它遵循主要功能,利用上述定义
int main(void) {
variables_basic();
variables_nodeids();
variables_variants();
return EXIT_SUCCESS;
}
本系列教程将指导您完成open62541的第一步。要编译这些示例,您需要一个编译器(MS Visual Studio 2015 或更新版本、GCC、Clang和MinGW32都可以使用)。编译说明是为GCC提供的,但应该易于修改。
它也将非常有助于安装一个具有图形前端的OPC-UA客户端,如 Unified Automation 的UAExpert。这将使您能够检查任何OPC UA服务器的信息模型。
首先,从http://open62541.org或在启用“合并”选项的情况下,根据生成说明生成它。从现在开始,我们假设您在当前文件夹中有open62541.c/.h文件。现在创建一个名为myServer.C的新C源文件,其中包含以下内容:
#include
#include
#include
#include
#include
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;
}
这就是一个简单的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服务器。您可以继续使用 client 浏览信息模型。服务器正在监听opc.tcp://localhost:4840 。在接下来的两个部分中,我们将继续详细解释代码的不同部分。
open62541为构建OPC-UA服务器和客户端提供了一个灵活的框架。目标是要有一个核心可容纳所有用例并在所有平台上运行的库。然后用户可以调整库以适应他们的通过配置和开发(特定于平台的)插件来实现用例。核心库仅基于C99甚至不需要基本的POSIX支持。例如,低级网络代码实现为一个可交换的插件。但别担心。open62541为大多数平台和现成的合理默认配置。
在上面的服务器代码中,我们只需使用默认的服务器配置并添加一个TCP网络层正在4840端口侦听。
本例中的代码显示了服务器生命周期管理的三个部分:创建服务器、运行服务器和删除服务器。一旦设置了配置,创建和删除服务器就很简单了。服务器以UA_server_run启动。在内部,服务器使用超时来安排常规任务。在超时之间,服务器在网络层侦听传入的消息。
您可能会问服务器如何知道何时停止运行。为此我们创建了一个全局变量 RUN。此外,我们还注册了stopHandler方法,该方法捕捉操作系统试图关闭程序时接收到的信号(中断)。例如,在终端程序中按ctrl-c时会发生这种情况。然后,信号处理程序将运行的变量设置为false,一旦服务器重新获得控制权,服务器就会关闭。
为了在单线程应用程序中集成OPC-UA和它自己的主循环(例如由GUI工具包提供),可以手动驱动服务器。有关详细信息,请参阅服务器文档中有关服务器生命周期的部分。
所有服务器都需要服务器配置和生命周期管理。我们将在下面的教程中使用它,无需进一步评论。
本教程演示如何使用数据类型以及如何向服务器添加变量节点。首先,我们向服务器添加一个新变量。查看UA_VariableAttributes 结构的定义,以查看为VariableNodes定义的所有属性的列表。
请注意,默认设置将变量值的AccessLevel设置为只读。有关使变量可写,请参见下面的内容。
static void addVariable(UA_Server *server) {
/* 定义整数变量节点的属性 */
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;
/* 将变量节点添加到信息模型中 */
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 addMatrixVariable(UA_Server *server) {
UA_VariableAttributes attr = UA_VariableAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en-US", "Double Matrix");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
/* 设置变量值约束 */
attr.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
attr.valueRank = UA_VALUERANK_TWO_DIMENSIONS;
UA_UInt32 arrayDims[2] = {2,2};
attr.arrayDimensions = arrayDims;
attr.arrayDimensionsSize = 2;
/* 设置值。值的数组维度必须相同。 */
UA_Double zero[4] = {0.0, 0.0, 0.0, 0.0};
UA_Variant_setArray(&attr.value, zero, 4, &UA_TYPES[UA_TYPES_DOUBLE]);
attr.value.arrayDimensions = arrayDims;
attr.value.arrayDimensionsSize = 2;
UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "double.matrix");
UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, "double matrix");
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);
}
现在我们用write服务更改值。这使用同样的服务实现,也可以通过网络由OPC-UA客户端访问。
static void writeVariable(UA_Server *server) {
UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "the.answer");
/* 写一个不同的整数值 */
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);
/* 将值的状态代码设置为错误代码。UA_Server_write函数提供对原始服务的访问。
* 上面的UA_Server_writeValue是使用write服务编写特定节点属性的语法糖。*/
/* 下面代码与上面有相同功能
// 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);
// 使用值将变量重置为正确的状态代码
// wv.value.hasStatus = false;
// wv.value.value = myVar;
// wv.value.hasValue = true;
// UA_Server_write(server, &wv); */
}
注意我们最初是如何将变量node的DataType属性设置为Int32数据类型的NodeId。这禁止写入不是Int32的值。下面的代码显示如何对每次写入执行一致性检查。
static void writeWrongVariable(UA_Server *server) {
UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "the.answer");
/* 写字符 */
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));
}
它遵循主服务器代码,利用上面的定义。(原文就不完整,下面代码简单调用了上面的方法。以后完善)
这句多次出现,意思就是修改原有的服务器代码,添加新的功能。
#include
#include
#include
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));
// 添加节点
addVariable(server);
addMatrixVariable(server);
printf("添加节点\n");
// 修改节点的值
writeVariable(server);
writeWrongVariable(server);
printf("修改节点的值\n");
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
在基于OPC-UA的体系结构中,服务器通常位于信息源附近。在工业环境中,这意味着服务器靠近物理进程,客户端在运行时消耗数据。在上一个教程中,我们了解了如何向OPC-UA信息模型添加变量。本教程演示如何将变量连接到运行时信息,例如从物理进程的度量中。为了简单起见,我们将系统时钟作为底层的“进程”。
下面的代码片段分别涉及在运行时更新变量值的不同方法。这些代码片段一起定义了一个可编译的源文件。
首先,假设已经在服务器中为DateTime类型的值创建了一个标识符为“ns=1,s=current time”的变量。假设当一个新值从底层进程到达时,我们的应用程序就会被触发,我们可以直接写入变量。
#include
#include
#include
#include
#include
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-value-callback");
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 - value callback");
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-value-callback");
UA_QualifiedName currentName = UA_QUALIFIEDNAME(1, "current-time-value-callback");
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) {
updateCurrentTime(server);
}
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-value-callback");
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);
}
它遵循主服务器代码,利用上面的定义
static volatile 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_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addCurrentTimeVariable(server);
addValueCallbackToCurrentTimeVariable(server);
addCurrentTimeDataSourceVariable(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
变量类型有三个函数:
在本教程的示例中,我们用双精度值数组表示二维空间中的一个点。以下函数将相应的VariableTypeNode添加到变量类型的层次结构中。
#include
#include
#include
#include
#include
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);
}
在写入变量的datatype、valuerank和arraydimensions属性时,将强制执行变量类型的约束。这反过来又约束了变量的value属性
static void
writeVariable(UA_Server *server) {
UA_StatusCode retval = UA_Server_writeValueRank(server, pointVariableId, UA_VALUERANK_ONE_OR_M
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting the Value Rank failed with Status Code %s",
UA_StatusCode_name(retval));
}
它遵循主服务器代码,利用上面的定义
static volatile 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_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addVariableType2DPoint(server);
addVariable(server);
addVariableFail(server);
writeVariable(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
假设我们要在OPC-UA信息模型中对一组泵及其运行状态进行建模。当然,所有泵的表示应该遵循相同的基本结构,例如,我们可能在SCADA可视化中有泵的图形化表示,该可视化对所有泵都是可恢复的。
遵循面向对象编程范式,每个泵都由具有以下布局的对象表示:
以下代码手动定义泵及其成员变量。我们省略了对变量值的设置约束,因为这不是本教程的重点,已经讨论过了。(这段代码PDF中被截取掉了,已经不完整了)
#include
#include
#include
#include
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_BA
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, N
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
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, NUL
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,
}
手动构建每个对象需要我们编写大量代码。此外,客户机无法检测对象是否代表泵。(我们可以使用命名约定或类似的方法来检测泵。但这并不是一个干净的解决方案。)此外,我们可能有更多的设备,而不仅仅是水泵。我们要求所有设备共享一些共同的结构。解决方案是在具有继承关系的层次结构中定义对象类型。
标记为强制的子对象与父对象一起自动实例化。这由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, &
/* 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
/* 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, NUL
/* 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,
}
现在我们添加从设备对象类型继承的泵的派生对象类型。结果对象包含所有必需的子变量。这些只是从对象类型复制过来的。对象对对象类型具有类型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);
}
通常,我们希望在新对象上运行构造函数。尤其是在与服务集成的时候,用一个服务在运行时被手动定义。在下面的构造函数示例中,我们只需将pump status设置为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_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);
}
它遵循主服务器代码,利用上面的定义。
static volatile 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_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
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);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
OPC UA信息模型中的对象可能包含与编程语言中的对象类似的方法。方法由MethodNode表示。请注意,多个对象可能引用同一MethodNode。实例化对象类型时,将添加对方法的引用,而不是复制MethodNode。因此,上下文对象的标识符在调用方法时总是显式地声明。
方法回调接受附加到方法节点的自定义数据指针、调用该方法的对象fom的标识符以及输入和输出参数的两个数组作为输入。输入和输出参数都是Variant类型。每个变量可以依次包含(多维)数组或任何数据类型的标量。
方法参数的约束是根据数据类型、值秩和数组维度定义的(类似于变量定义)。参数定义存储在MethodNode的子variableNode中,具有各自的browsename(0,“InputArguments”)和(0,“OutputArguments”)。
该方法接受一个字符串标量并返回一个前缀为“Hello”的字符串标量。SDK在内部检查输入参数的类型和长度,这样我们就不必在回调中验证参数了。
#include
#include
#include
#include
#include
#include
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);
}
该方法以5个整数和一个标量作为输入。它返回数组的一个副本,其中每个条目都以标量递增。
static UA_StatusCode
IncInt32ArrayMethodCallback(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
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 *inputArray = (UA_Int32*)input[0].data;
UA_Int32 delta = *(UA_Int32*)input[1].data;
/* Copy the input array */
UA_StatusCode retval = UA_Variant_setArrayCopy(output, inputArray, 5,&UA_TYPES[UA_TYPES_INT32]);
if(retval != UA_STATUSCODE_GOOD)
return retval;
/* Increate the elements */
UA_Int32 *outputArray = (UA_Int32*)output->data;
for(size_t i = 0; i < input->arrayLength; i++)
outputArray[i] = inputArray[i] + delta;
return UA_STATUSCODE_GOOD;
}
static void
addIncInt32ArrayMethod(UA_Server *server) {
/* Two input arguments */
UA_Argument inputArguments[2];
UA_Argument_init(&inputArguments[0]);
inputArguments[0].description = UA_LOCALIZEDTEXT("en-US", "int32[5] array");
inputArguments[0].name = UA_STRING("int32 array");
inputArguments[0].dataType = UA_TYPES[UA_TYPES_INT32].typeId;
inputArguments[0].valueRank = UA_VALUERANK_ONE_DIMENSION;
UA_UInt32 pInputDimension = 5;
inputArguments[0].arrayDimensionsSize = 1;
inputArguments[0].arrayDimensions = &pInputDimension;
UA_Argument_init(&inputArguments[1]);
inputArguments[1].description = UA_LOCALIZEDTEXT("en-US", "int32 delta");
inputArguments[1].name = UA_STRING("int32 delta");
inputArguments[1].dataType = UA_TYPES[UA_TYPES_INT32].typeId;
inputArguments[1].valueRank = UA_VALUERANK_SCALAR;
/* One output argument */
UA_Argument outputArgument;
UA_Argument_init(&outputArgument);
outputArgument.description = UA_LOCALIZEDTEXT("en-US", "int32[5] array");
outputArgument.name = UA_STRING("each entry is incremented by the delta");
outputArgument.dataType = UA_TYPES[UA_TYPES_INT32].typeId;
outputArgument.valueRank = UA_VALUERANK_ONE_DIMENSION;
UA_UInt32 pOutputDimension = 5;
outputArgument.arrayDimensionsSize = 1;
outputArgument.arrayDimensions = &pOutputDimension;
/* Add the method node */
UA_MethodAttributes incAttr = UA_MethodAttributes_default;
incAttr.description = UA_LOCALIZEDTEXT("en-US", "IncInt32ArrayValues");
incAttr.displayName = UA_LOCALIZEDTEXT("en-US", "IncInt32ArrayValues");
incAttr.executable = true;
incAttr.userExecutable = true;
UA_Server_addMethodNode(server, UA_NODEID_STRING(1, "IncInt32ArrayValues"),
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "IncInt32ArrayValues"),
incAttr, &IncInt32ArrayMethodCallback,
2, inputArguments, 1, &outputArgument,
NULL, NULL);
}
它遵循主服务器代码,利用上面的定义。
static volatile 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_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addHellWorldMethod(server);
addIncInt32ArrayMethod(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
对变量的当前值感兴趣的客户机不需要定期轮询该变量。相反,他可以使用订阅机制来获得有关更改的通知。
所谓的monitoreditem定义客户机想要监视的值(节点属性)和事件。在适当的条件下,将创建通知并将其添加到订阅中。队列中当前的通知会定期发送到客户端。
本地用户也可以添加monitoreditem。在本地,monitoreditem不通过订阅,并且每个都有一个单独的回调方法和一个上下文指针。
#include
#include
#include
#include
#include
#include
static void
dataChangeNotificationCallback(UA_Server *server, UA_UInt32 monitoredItemId,
void *monitoredItemContext, const UA_NodeId *nodeId,
void *nodeContext, UA_UInt32 attributeId,
const UA_DataValue *value) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Received Notification");
}
static void
addMonitoredItemToCurrentTimeVariable(UA_Server *server) {
UA_NodeId currentTimeNodeId =
UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
UA_MonitoredItemCreateRequest monRequest =
UA_MonitoredItemCreateRequest_default(currentTimeNodeId);
monRequest.requestedParameters.samplingInterval = 100.0; /* 100 ms interval */
UA_Server_createDataChangeMonitoredItem(server, UA_TIMESTAMPSTORETURN_SOURCE,
monRequest, NULL, dataChangeNotificationCallback);
}
它遵循主服务器代码,利用上面的定义。
static volatile 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_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addMonitoredItemToCurrentTimeVariable(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
为了理解服务器中发生的许多事情,监视项可能很有用。尽管在许多情况下,数据更改并不能传递足够的信息,从而成为最佳解决方案。事件可以在任何时候生成,包含大量信息,并且可以被过滤,这样客户机只接收他感兴趣的特定属性。
下面的示例将基于服务器方法教程。我们将创建一个从服务器节点生成事件的方法节点。
我们要生成的事件应该非常简单。因为BaseEventType是抽象的,所以我们必须创建自己的事件类型。在内部将对象类型保存为新的ObjectType。
static UA_NodeId eventType;
static UA_StatusCode
addNewEventType(UA_Server *server) {
UA_ObjectTypeAttributes attr = UA_ObjectTypeAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en-US", "SimpleEventType");
attr.description = UA_LOCALIZEDTEXT("en-US", "The simple event type we created");
return UA_Server_addObjectTypeNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEEVENTTYPE),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
UA_QUALIFIEDNAME(0, "SimpleEventType"),
attr, NULL, &eventType);
}
为了设置事件,我们可以首先使用UA_Server_createEvent为我们提供事件的节点表示。我们只需要我们的事件类型。一旦我们有了事件节点,它在内部被保存为一个ObjectNode,我们就可以像定义对象节点的属性一样定义事件的属性。不需要定义属性EventId、ReceiveTime、SourceNode或EventType,因为这些属性是由服务器自动设置的。在本例中,我们将设置字段"Message”和“Severity”,以及使示例UaExpert兼容所需的"Time"。
static UA_StatusCode
setUpEvent(UA_Server *server, UA_NodeId *outId) {
UA_StatusCode retval = UA_Server_createEvent(server, eventType, outId);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
"createEvent failed. StatusCode %s", UA_StatusCode_name(retval));
return retval;
} /
* Set the Event Attributes */
/* Setting the Time is required or else the event will not show up in UAExpert! */
UA_DateTime eventTime = UA_DateTime_now();
UA_Server_writeObjectProperty_scalar(server, *outId, UA_QUALIFIEDNAME(0, "Time"),
&eventTime, &UA_TYPES[UA_TYPES_DATETIME]);
UA_UInt16 eventSeverity = 100;
UA_Server_writeObjectProperty_scalar(server, *outId, UA_QUALIFIEDNAME(0, "Severity"),
&eventSeverity, &UA_TYPES[UA_TYPES_UINT16]);
UA_LocalizedText eventMessage = UA_LOCALIZEDTEXT("en-US", "An event has been generated.");
UA_Server_writeObjectProperty_scalar(server, *outId, UA_QUALIFIEDNAME(0, "Message"),
&eventMessage, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
UA_String eventSourceName = UA_STRING("Server");
UA_Server_writeObjectProperty_scalar(server, *outId, UA_QUALIFIEDNAME(0, "SourceName"),
&eventSourceName, &UA_TYPES[UA_TYPES_STRING]);
return UA_STATUSCODE_GOOD;
}
首先,使用setUpEvent生成表示事件的节点。一旦我们的事件可以进行,我们指定一个节点来发出事件,在本例中是服务器节点。我们可以使用 UA_Server_triggerEvent 在所述节点上触发我们的事件。传递NULL作为最后一个参数意味着我们将不会收到EventId。最后一个布尔参数说明是否应该删除节点。
static UA_StatusCode
generateEventMethodCallback(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_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Creating event");
/* set up event */
UA_NodeId eventNodeId;
UA_StatusCode retval = setUpEvent(server, &eventNodeId);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"Creating event failed. StatusCode %s", UA_StatusCode_name(retval));
return retval;
}
retval = UA_Server_triggerEvent(server, eventNodeId,UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER),NULL, UA_TRUE);
if(retval != UA_STATUSCODE_GOOD)
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,"Triggering event failed. StatusCode %s", UA_StatusCode_name(retval));
return retval;
}
现在,剩下要做的就是创建一个使用回调的方法节点。我们不需要任何输入,作为输出,我们将使用从 triggerEvent 接收的EventId。EventId是由服务器内部生成的,是标识特定事件的随机唯一ID。
此方法节点将添加到基本服务器设置中。
static void
addGenerateEventMethod(UA_Server *server) {
UA_MethodAttributes generateAttr = UA_MethodAttributes_default;
generateAttr.description = UA_LOCALIZEDTEXT("en-US","Generate an event.");
generateAttr.displayName = UA_LOCALIZEDTEXT("en-US","Generate Event");
generateAttr.executable = true;
generateAttr.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, "Generate Event"),
generateAttr, &generateEventMethodCallback,
0, NULL, 0, NULL, NULL, NULL);
}
它遵循主服务器代码,利用上面的定义。
static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
running = false;
}
int main (void) {
/* default server values */
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addNewEventType(server);
addGenerateEventMethod(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
除了使用被监视的项目和事件来观察服务器中的变化外,使用警报和条件服务器模型也很重要。警报是服务器组件状态发生变化时,由服务器根据内部服务器逻辑或用户特定逻辑自动触发的事件。组件的状态通过一个条件来表示。因此,所有条件子项(字段)的值都是组件的实际状态。
以下示例基于服务器事件教程。在继续这个例子之前,请务必理解正常事件的原理!
static UA_NodeId conditionSource;
static UA_NodeId conditionInstance_1;
static UA_NodeId conditionInstance_2;
static UA_StatusCode
addConditionSourceObject(UA_Server *server) {
UA_ObjectAttributes object_attr = UA_ObjectAttributes_default;
object_attr.eventNotifier = 1;
object_attr.displayName = UA_LOCALIZEDTEXT("en", "ConditionSourceObject");
UA_StatusCode retval = UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(0, "ConditionSourceObject"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
object_attr, NULL, &conditionSource);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Creating Condition Source failed. StatusCode %s",
UA_StatusCode_name(retval));
}
/* ConditionSource should be EventNotifier of another Object (usually the
* Server Object). If this Reference is not created by user then the A&C
* Server will create "HasEventSource" reference to the Server Object
* automatically when the condition is created*/
retval = UA_Server_addReference(server, UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASNOTIFIER),
UA_EXPANDEDNODEID_NUMERIC(conditionSource.namespaceIndex,
conditionSource.identifier.numeric)
UA_TRUE);
return retval;
}
从 OffNormalAlarmType 创建条件实例。条件源是在addConditionSourceObject()中创建的对象。条件将通过对条件源的HasComponent 引用在地址空间中公开。
static UA_StatusCode
addCondition_1(UA_Server *server) {
UA_StatusCode retval = addConditionSourceObject(server);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"creating Condition Source failed. StatusCode %s",
UA_StatusCode_name(retval));
}
retval = UA_Server_createCondition(server,
UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OFFNORMALALARMTYPE),
UA_QUALIFIEDNAME(0, "Condition 1"), conditionSource,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
&conditionInstance_1);
return retval;
}
从OffNormalAlarmType创建条件实例。条件源是服务器对象。条件不会在地址空间中公开。
static UA_StatusCode
addCondition_2(UA_Server* server) {
UA_StatusCode retval =
UA_Server_createCondition(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OFFNORMALALARMTYPE),
UA_QUALIFIEDNAME(0, "Condition 2"),
UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER),
UA_NODEID_NULL, &conditionInstance_2);
return retval;
}
static void
addVariable_1_triggerAlarmOfCondition_1(UA_Server* server, UA_NodeId* outNodeId) {
UA_VariableAttributes attr = UA_VariableAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en", "Activate Condition 1");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_Boolean tboolValue = UA_FALSE;
UA_Variant_setScalar(&attr.value, &tboolValue, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_QualifiedName CallbackTestVariableName = UA_QUALIFIEDNAME(0, "Activate Condition 1");
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, UA_NODEID_NULL, parentNodeId,
parentReferenceNodeId, CallbackTestVariableName,
variableTypeNodeId, attr, NULL, outNodeId);
}
static void
addVariable_2_changeSeverityOfCondition_2(UA_Server* server,
UA_NodeId* outNodeId) {
UA_VariableAttributes attr = UA_VariableAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en", "Change Severity Condition 2");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_UInt16 severityValue = 0;
UA_Variant_setScalar(&attr.value, &severityValue, &UA_TYPES[UA_TYPES_UINT16]);
UA_QualifiedName CallbackTestVariableName =
UA_QUALIFIEDNAME(0, "Change Severity Condition 2");
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, UA_NODEID_NULL, parentNodeId,
parentReferenceNodeId, CallbackTestVariableName,
variableTypeNodeId, attr, NULL, outNodeId);
}
static void
addVariable_3_returnCondition_1_toNormalState(UA_Server* server,
UA_NodeId* outNodeId) {
UA_VariableAttributes attr = UA_VariableAttributes_default;
attr.displayName = UA_LOCALIZEDTEXT("en", "Return to Normal Condition 1");
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_Boolean rtn = 0;
UA_Variant_setScalar(&attr.value, &rtn, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_QualifiedName CallbackTestVariableName =
UA_QUALIFIEDNAME(0, "Return to Normal Condition 1");
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, UA_NODEID_NULL, parentNodeId,
parentReferenceNodeId, CallbackTestVariableName,
variableTypeNodeId, attr, NULL, outNodeId);
}
static void
afterWriteCallbackVariable_1(UA_Server* server, const UA_NodeId* sessionId,
void* sessionContext, const UA_NodeId* nodeId,
void* nodeContext, const UA_NumericRange* range,
const UA_DataValue* data) {
UA_QualifiedName activeStateField = UA_QUALIFIEDNAME(0, "ActiveState");
UA_QualifiedName activeStateIdField = UA_QUALIFIEDNAME(0, "Id");
UA_Variant value;
UA_StatusCode retval =
UA_Server_writeObjectProperty_scalar(server, conditionInstance_1,
UA_QUALIFIEDNAME(0, "Time"),
&data->sourceTimestamp,
&UA_TYPES[UA_TYPES_DATETIME]);
if (*(UA_Boolean*)(data->value.data) == true) {
/* By writing "true" in ActiveState/Id, the A&C server will set the
* related fields automatically and then will trigger event
* notification. */
UA_Boolean activeStateId = true;
UA_Variant_setScalar(&value, &activeStateId, &UA_TYPES[UA_TYPES_BOOLEAN]);
retval |= UA_Server_setConditionVariableFieldProperty(server, conditionInstance_1,
&value, activeStateField,
activeStateIdField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ActiveState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
}
else {
/* By writing "false" in ActiveState/Id, the A&C server will set only
* the ActiveState field automatically to the value "Inactive". The user
* should trigger the event manually by calling
* UA_Server_triggerConditionEvent inside the application or call
* ConditionRefresh method with client to update the event notification. */
UA_Boolean activeStateId = false;
UA_Variant_setScalar(&value, &activeStateId, &UA_TYPES[UA_TYPES_BOOLEAN]);
retval = UA_Server_setConditionVariableFieldProperty(server, conditionInstance_1,
&value, activeStateField,
activeStateIdField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ActiveState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
retval = UA_Server_triggerConditionEvent(server, conditionInstance_1,
conditionSource, NULL);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Triggering condition event failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
}
}
回调仅更改条件2的严重性字段。severity字段是ConditionVariableType,因此它的更改会自动触发服务器的事件通知。
static void
afterWriteCallbackVariable_2(UA_Server* server, const UA_NodeId* sessionId,
void* sessionContext, const UA_NodeId* nodeId,
void* nodeContext, const UA_NumericRange* range,
const UA_DataValue* data) {
/* Another way to set fields of conditions */
UA_Server_writeObjectProperty_scalar(server, conditionInstance_2,
UA_QUALIFIEDNAME(0, "Severity"), (UA_UInt16*)data->value.data,
&UA_TYPES[UA_TYPES_UINT16]);
}
RTN = return to normal.
Retain将设置为false,因此不会为条件1生成任何事件(尽管EnabledState/=true)。若要再次将Retain设置为true,应分别调用disable和enable方法。
static void
afterWriteCallbackVariable_3(UA_Server* server,
const UA_NodeId* sessionId, void* sessionContext,
const UA_NodeId* nodeId, void* nodeContext,
const UA_NumericRange* range, const UA_DataValue* data) {
//UA_QualifiedName enabledStateField = UA_QUALIFIEDNAME(0,"EnabledState");
UA_QualifiedName ackedStateField = UA_QUALIFIEDNAME(0, "AckedState");
UA_QualifiedName confirmedStateField = UA_QUALIFIEDNAME(0, "ConfirmedState");
UA_QualifiedName activeStateField = UA_QUALIFIEDNAME(0, "ActiveState");
UA_QualifiedName severityField = UA_QUALIFIEDNAME(0, "Severity");
UA_QualifiedName messageField = UA_QUALIFIEDNAME(0, "Message");
UA_QualifiedName commentField = UA_QUALIFIEDNAME(0, "Comment");
UA_QualifiedName retainField = UA_QUALIFIEDNAME(0, "Retain");
UA_QualifiedName idField = UA_QUALIFIEDNAME(0, "Id");
UA_StatusCode retval =
UA_Server_writeObjectProperty_scalar(server, conditionInstance_1,
UA_QUALIFIEDNAME(0, "Time"),
&data->serverTimestamp,
&UA_TYPES[UA_TYPES_DATETIME]);
UA_Variant value;
UA_Boolean idValue = false;
UA_Variant_setScalar(&value, &idValue, &UA_TYPES[UA_TYPES_BOOLEAN]);
retval |= UA_Server_setConditionVariableFieldProperty(server, conditionInstance_1,
&value, activeStateField,
idField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ActiveState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
retval = UA_Server_setConditionVariableFieldProperty(server, conditionInstance_1,
&value, ackedStateField,
idField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting AckedState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
retval = UA_Server_setConditionVariableFieldProperty(server, conditionInstance_1,
&value, confirmedStateField,
idField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ConfirmedState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
UA_UInt16 severityValue = 100;
UA_Variant_setScalar(&value, &severityValue, &UA_TYPES[UA_TYPES_UINT16]);
retval = UA_Server_setConditionField(server, conditionInstance_1,
&value, severityField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting Severity Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
UA_LocalizedText messageValue =
UA_LOCALIZEDTEXT("en", "Condition returned to normal state");
UA_Variant_setScalar(&value, &messageValue, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
retval = UA_Server_setConditionField(server, conditionInstance_1,
&value, messageField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting Message Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
UA_LocalizedText commentValue = UA_LOCALIZEDTEXT("en", "Normal State");
UA_Variant_setScalar(&value, &commentValue, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
retval = UA_Server_setConditionField(server, conditionInstance_1,
&value, commentField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting Comment Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
UA_Boolean retainValue = false;
UA_Variant_setScalar(&value, &retainValue, &UA_TYPES[UA_TYPES_BOOLEAN]);
retval = UA_Server_setConditionField(server, conditionInstance_1,
&value, retainField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting Retain Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
retval = UA_Server_triggerConditionEvent(server, conditionInstance_1,
conditionSource, NULL);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Triggering condition event failed. StatusCode %s",
UA_StatusCode_name(retval));
return;
}
}
static UA_StatusCode
enteringEnabledStateCallback(UA_Server* server, const UA_NodeId* condition) {
UA_Boolean retain = true;
return UA_Server_writeObjectProperty_scalar(server, *condition,
UA_QUALIFIEDNAME(0, "Retain"),
&retain,
&UA_TYPES[UA_TYPES_BOOLEAN]);
}
这是特定于用户的函数,在确认报警通知时将调用该函数。在本例中,我们将报警设置为非活动状态。服务器负责设置与确认方法相关的标准字段,并触发报警通知。
static UA_StatusCode
enteringAckedStateCallback(UA_Server* server, const UA_NodeId* condition) {
/* deactivate Alarm when acknowledging*/
UA_Boolean activeStateId = false;
UA_Variant value;
UA_QualifiedName activeStateField = UA_QUALIFIEDNAME(0, "ActiveState");
UA_QualifiedName activeStateIdField = UA_QUALIFIEDNAME(0, "Id");
UA_Variant_setScalar(&value, &activeStateId, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_StatusCode retval =
UA_Server_setConditionVariableFieldProperty(server, *condition,
&value, activeStateField,
activeStateIdField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ActiveState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
}
return retval;
}
static UA_StatusCode
enteringConfirmedStateCallback(UA_Server* server, const UA_NodeId* condition) {
/* Deactivate Alarm and put it out of the interesting state (by writing
* false to Retain field) when confirming*/
UA_Boolean activeStateId = false;
UA_Boolean retain = false;
UA_Variant value;
UA_QualifiedName activeStateField = UA_QUALIFIEDNAME(0, "ActiveState");
UA_QualifiedName activeStateIdField = UA_QUALIFIEDNAME(0, "Id");
UA_QualifiedName retainField = UA_QUALIFIEDNAME(0, "Retain");
UA_Variant_setScalar(&value, &activeStateId, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_StatusCode retval =
UA_Server_setConditionVariableFieldProperty(server, *condition,
&value, activeStateField,
activeStateIdField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ActiveState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
}
UA_Variant_setScalar(&value, &retain, &UA_TYPES[UA_TYPES_BOOLEAN]);
retval = UA_Server_setConditionField(server, *condition,
&value, retainField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting ActiveState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
}
return retval;
}
static UA_StatusCode
setUpEnvironment(UA_Server* server) {
UA_NodeId variable_1;
UA_NodeId variable_2;
UA_NodeId variable_3;
UA_ValueCallback callback;
callback.onRead = NULL;
/* Exposed condition 1. We will add to it user specific callbacks when
* entering enabled state, when acknowledging and when confirming. */
UA_StatusCode retval = addCondition_1(server);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"adding condition 1 failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
}
UA_TwoStateVariableChangeCallback userSpecificCallback = enteringEnabledStateCallback;
retval = UA_Server_setConditionTwoStateVariableCallback(server, conditionInstance_1,
conditionSource, false,
userSpecificCallback,
UA_ENTERING_ENABLEDSTATE);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"adding entering enabled state callback failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
}
userSpecificCallback = enteringAckedStateCallback;
retval = UA_Server_setConditionTwoStateVariableCallback(server, conditionInstance_1,
conditionSource, false,
userSpecificCallback,
UA_ENTERING_ACKEDSTATE);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"adding entering acked state callback failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
}
userSpecificCallback = enteringConfirmedStateCallback;
retval = UA_Server_setConditionTwoStateVariableCallback(server, conditionInstance_1,
conditionSource, false,
userSpecificCallback,
UA_ENTERING_CONFIRMEDSTATE);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"adding entering confirmed state callback failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
} /
*Unexposed condition 2. No user specific callbacks, so the server will
* behave in a standard manner upon entering enabled state, acknowledging
*and confirming.We will set Retain field to true and enable the condition
* so we can receive event notifications(we cannot call enable method on
* unexposed condition using a client like UaExpert or Softing).* /
retval = addCondition_2(server);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"adding condition 2 failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
}
UA_Boolean retain = UA_TRUE;
UA_Server_writeObjectProperty_scalar(server, conditionInstance_2,
UA_QUALIFIEDNAME(0, "Retain"),
&retain, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_Variant value;
UA_Boolean enabledStateId = true;
UA_QualifiedName enabledStateField = UA_QUALIFIEDNAME(0, "EnabledState");
UA_QualifiedName enabledStateIdField = UA_QUALIFIEDNAME(0, "Id");
UA_Variant_setScalar(&value, &enabledStateId, &UA_TYPES[UA_TYPES_BOOLEAN]);
retval = UA_Server_setConditionVariableFieldProperty(server, conditionInstance_2,
&value, enabledStateField,
enabledStateIdField);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting EnabledState/Id Field failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
} /
*Add 3 variables to trigger condition events * /
addVariable_1_triggerAlarmOfCondition_1(server, &variable_1);
callback.onWrite = afterWriteCallbackVariable_1;
retval = UA_Server_setVariableNode_valueCallback(server, variable_1, callback);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting variable 1 Callback failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
} /
*Severity can change internally also when the condition disabled and
*retain is false.However, in this case no events will be generated.* /
addVariable_2_changeSeverityOfCondition_2(server, &variable_2);
callback.onWrite = afterWriteCallbackVariable_2;
retval = UA_Server_setVariableNode_valueCallback(server, variable_2, callback);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting variable 2 Callback failed. StatusCode %s",
UA_StatusCode_name(retval));
return retval;
}
addVariable_3_returnCondition_1_toNormalState(server, &variable_3);
callback.onWrite = afterWriteCallbackVariable_3;
retval = UA_Server_setVariableNode_valueCallback(server, variable_3, callback);
if (retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Setting variable 3 Callback failed. StatusCode %s",
UA_StatusCode_name(retval));
}
return retval;
}
它遵循主服务器代码,利用上面的定义。
static UA_Boolean running = true;
static void stopHandler(int sig) {
running = false;
}
int main(void) {
/* default server values */
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server* server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
UA_StatusCode retval = setUpEnvironment(server);
if (retval == UA_STATUSCODE_GOOD)
retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
您应该已经有了以前教程中的基本服务器。open62541同时提供了服务器和客户端API,因此创建客户端与创建服务器一样简单。将以下内容复制到myClient.c文件中:
#include
#include
#include
#include
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");
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);
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);
}
/* Clean up */
UA_Variant_clear(&value);
UA_Client_delete(client); /* Disconnects the client internally */
return EXIT_SUCCESS;
}
编译类似于服务器示例
gcc -std=c99 open62541.c myClient.c -o myClient
在MinGW环境中,必须添加Winsock库
gcc -std=c99 open62541.c myClient.c -lws2_32 -o myClient.exe
正在进行的工作:本教程将在下一个PubSub批处理期间继续扩展。关于PubSub扩展和相应的open62541api的更多详细信息可以在这里找到:13.6 Publish/Subscribe。
PubSub发布示例演示了使用UADP编码通过UDP多播发布信息模型中的信息的最简单方法。
连接处理
可以在运行时创建和删除pubsubconnection。有关系统预配置和连接的更多详细信息可以在 tutorial_pubsub_connection.c 中找到。
#include
#include
#include
#include
#include
#include
UA_NodeId connectionIdent, publishedDataSetIdent, writerGroupIdent;
static void
addPubSubConnection(UA_Server* server, UA_String* transportProfile,
UA_NetworkAddressUrlDataType* networkAddressUrl) {
/* Details about the connection configuration and handling are located
* in the pubsub connection tutorial */
UA_PubSubConnectionConfig connectionConfig;
memset(&connectionConfig, 0, sizeof(connectionConfig));
connectionConfig.name = UA_STRING("UADP Connection 1");
connectionConfig.transportProfileUri = *transportProfile;
connectionConfig.enabled = UA_TRUE;
UA_Variant_setScalar(&connectionConfig.address, networkAddressUrl,
&UA_TYPES[UA_TYPES_NETWORKADDRESSURLDATATYPE]);
/* Changed to static publisherId from random generation to identify
* the publisher on Subscriber side */
connectionConfig.publisherId.numeric = 2234;
UA_Server_addPubSubConnection(server, &connectionConfig, &connectionIdent);
}
PublishedDataSet处理
PublishedDataSet(PDS)和PubSubConnection是顶级实体,可以单独存在。PDS包含已发布字段的集合。所有其他PubSub元素都直接或间接地与PDS或connection链接。
static void
addPublishedDataSet(UA_Server* server) {
/* The PublishedDataSetConfig contains all necessary public
* informations for the creation of a new PublishedDataSet */
UA_PublishedDataSetConfig publishedDataSetConfig;
memset(&publishedDataSetConfig, 0, sizeof(UA_PublishedDataSetConfig));
publishedDataSetConfig.publishedDataSetType = UA_PUBSUB_DATASET_PUBLISHEDITEMS;
publishedDataSetConfig.name = UA_STRING("Demo PDS");
/* Create new PublishedDataSet based on the PublishedDataSetConfig. */
UA_Server_addPublishedDataSet(server, &publishedDataSetConfig, &publishedDataSetIdent);
}
DataSetField处理
DataSetField(DSF)是PDS的一部分,只描述一个已发布的字段。
static void
addDataSetField(UA_Server* server) {
/* Add a field to the previous created PublishedDataSet */
UA_NodeId dataSetFieldIdent;
UA_DataSetFieldConfig dataSetFieldConfig;
memset(&dataSetFieldConfig, 0, sizeof(UA_DataSetFieldConfig));
dataSetFieldConfig.dataSetFieldType = UA_PUBSUB_DATASETFIELD_VARIABLE;
dataSetFieldConfig.field.variable.fieldNameAlias = UA_STRING("Server localtime");
dataSetFieldConfig.field.variable.promotedField = UA_FALSE;
dataSetFieldConfig.field.variable.publishParameters.publishedVariable =
UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
dataSetFieldConfig.field.variable.publishParameters.attributeId = UA_ATTRIBUTEID_VALUE;
UA_Server_addDataSetField(server, publishedDataSetIdent,
&dataSetFieldConfig, &dataSetFieldIdent);
}
WriterGroup处理
WriterGroup(WG)是连接的一部分,包含用于创建消息的主要配置参数。
static void
addWriterGroup(UA_Server* server) {
/* Now we create a new WriterGroupConfig and add the group to the existing
* PubSubConnection. */
UA_WriterGroupConfig writerGroupConfig;
memset(&writerGroupConfig, 0, sizeof(UA_WriterGroupConfig));
writerGroupConfig.name = UA_STRING("Demo WriterGroup");
writerGroupConfig.publishingInterval = 100;
writerGroupConfig.enabled = UA_FALSE;
writerGroupConfig.writerGroupId = 100;
writerGroupConfig.encodingMimeType = UA_PUBSUB_ENCODING_UADP;
writerGroupConfig.messageSettings.encoding = UA_EXTENSIONOBJECT_DECODED;
writerGroupConfig.messageSettings.content.decoded.type = &UA_TYPES[UA_TYPES_UADPWRITERGROUPMES
/* The configuration flags for the messages are encapsulated inside the
* * message- and transport settings extension objects. These extension
* objects are defined by the standard. e.g.
* UadpWriterGroupMessageDataType */
UA_UadpWriterGroupMessageDataType * writerGroupMessage = UA_UadpWriterGroupMessageDataType_new
/* Change message settings of writerGroup to send PublisherId,
* WriterGroupId in GroupHeader and DataSetWriterId in PayloadHeader
* of NetworkMessage */
writerGroupMessage->networkMessageContentMask = (UA_UadpNetworkMessageContentMask)(UA
(UA_UadpNetworkMessageContentMask)UA
(UA_UadpNetworkMessageContentMask)UA
(UA_UadpNetworkMessageContentMask)UA
writerGroupConfig.messageSettings.content.decoded.data = writerGroupMessage;
UA_Server_addWriterGroup(server, connectionIdent, &writerGroupConfig, &writerGroupIdent);
UA_Server_setWriterGroupOperational(server, writerGroupIdent);
UA_UadpWriterGroupMessageDataType_delete(writerGroupMessage);
}
DataSetWriter处理
DataSetWriter(DSW)是WG和PDS之间的粘合剂。DSW仅链接到一个PDS,并包含用于生成消息的附加信息。
static void
addDataSetWriter(UA_Server* server) {
/* We need now a DataSetWriter within the WriterGroup. This means we must
* create a new DataSetWriterConfig and add call the addWriterGroup function. */
UA_NodeId dataSetWriterIdent;
UA_DataSetWriterConfig dataSetWriterConfig;
memset(&dataSetWriterConfig, 0, sizeof(UA_DataSetWriterConfig));
dataSetWriterConfig.name = UA_STRING("Demo DataSetWriter");
dataSetWriterConfig.dataSetWriterId = 62541;
dataSetWriterConfig.keyFrameCount = 10;
UA_Server_addDataSetWriter(server, writerGroupIdent, publishedDataSetIdent,
&dataSetWriterConfig, &dataSetWriterIdent);
}
就这样!您现在正在发布所选字段。打开一个信任的包检查工具,例如wireshark,并查看传出的包。下图列出了本教程创建的包。
open62541订阅服务器API将在稍后发布。如果要处理这些数据报,请查看ua_network_pubsub_networkmessage.c,它已经包含了UADP消息的解码代码。
它遵循主服务器代码,利用上面的定义
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
static int run(UA_String* transportProfile,
UA_NetworkAddressUrlDataType* networkAddressUrl) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server* server = UA_Server_new();
UA_ServerConfig* config = UA_Server_getConfig(server);
UA_ServerConfig_setDefault(config);
/* Details about the connection configuration and handling are located in
* the pubsub connection tutorial */
config->pubsubTransportLayers =
(UA_PubSubTransportLayer*)UA_calloc(2, sizeof(UA_PubSubTransportLayer));
if (!config->pubsubTransportLayers) {
UA_Server_delete(server);
return EXIT_FAILURE;
}
config->pubsubTransportLayers[0] = UA_PubSubTransportLayerUDPMP();
config->pubsubTransportLayersSize++;
在本节中,我们将概述OPC-UA二进制协议。我们关注二进制,因为这是open62541中实现的。基于TCP的二进制协议是OPC-UA最常用的传输层。一般概念也转化为标准中定义的基于HTTP和SOAP的通信。OPC UA中的通信最好从以下关键原则开始理解:
Request / Response 所有通信都基于请求/响应模式。只有客户端可以向服务器发送请求。服务器只能对请求发送响应。通常,服务器托管在(物理)设备上,例如传感器或机床。
Asynchronous Responses 服务器不必立即响应请求,响应可以按不同的顺序发送。这使服务器在处理特定请求之前(例如方法调用或延迟读取传感器时)保持响应。此外,订阅(又称推送通知)是通过特殊请求实现的,在这种请求中,响应被延迟到生成通知为止。
OPC-UA中的客户机-服务器连接由三个嵌套层组成:原始连接(raw connection)、安全通道(SecureChannel)和会话(Session)。有关详细信息,请参阅OPC UA标准第6部分。
raw connection 原始连接通过打开到相应主机名和端口的TCP连接和初始HEL/ACK握手来创建。握手建立连接的基本设置,例如最大消息长度。
SecureChannel 安全通道是在原始TCP连接的基础上创建的。安全通道是用 OpenSecureChannel 请求和响应消息对建立的。注意!即使安全通道是强制的,加密仍然可能被禁用。安全通道的 SecurityMode 可以是None、Sign或SignAndEncrypt。从open62541的0.2版开始,消息签名和加密仍在开发中。
启用消息签名或加密后,OpenSecureChannel消息将使用非对称加密算法(公钥加密)注1进行加密。作为OpenSecureChannel消息的一部分,客户机和服务器在最初不安全的通道上建立一个公共秘密。对于后续消息,公共秘密用于对称加密,其优点是速度快得多。
OPC UA标准第7部分中定义的不同安全策略规定了非对称和对称加密的算法、加密密钥长度、消息签名的哈希函数等。示例安全策略不适用于明文和Basic256Sha256的传输,Basic256Sha256要求RSA使用SHA256证书哈希进行非对称加密,AES256用于对称加密。
注1:这就要求客户机和服务器交换所谓的公钥。公钥可能带有来自密钥签名机构的证书,或者根据外部密钥存储库进行验证。但在本节中,我们将不详细讨论证书管理。
服务器可能的安全策略用端点列表描述。端点共同定义了SecurityMode、SecurityPolicy和对会话进行身份验证的方法(在下一节中讨论),以便连接到某个服务器。GetEndpoints服务返回可用端点的列表。此服务通常可以在没有会话的情况下从未加密的SecureChannel调用。这允许客户端首先发现可用的端点,然后使用打开会话可能需要的适当的安全策略。
Session 会话是在SecureChannel上创建的。这样可以确保用户无需以明文形式发送其凭据(如用户名和密码)即可进行身份验证。当前定义的身份验证机制是匿名登录、用户名/密码、Kerberos和x509证书。后者要求请求消息附带一个签名,以证明发送方拥有用于创建证书的私钥。
建立会话需要两个消息交换:CreateSession和ActicateSession。ActivateSession服务可用于将现有会话切换到其他SecureChannel。这一点很重要,例如,当连接断开并且现有会话被新的SecureChannel重用时。
对于以下OPC UA协议消息结构的介绍,请考虑使用Wireshark工具记录和显示的OPC UA二进制对话示例,如:numref:'UA-Wireshark'。
图5.1:Wireshark中显示的OPC UA对话
Wireshark窗口的顶部按顺序显示对话中的消息。绿线包含应用的筛选器。这里,我们只想看到OPC UA协议消息。第一条消息(从TCP包49到56)显示客户端打开未加密的SecureChannel并检索服务器的端点。然后,从包63开始,根据其中一个端点创建新的连接和SecureChannel。在这个SecureChannel之上,客户端可以创建并激活一个会话。下面的ReadRequest消息将被选中并在底部窗口中详细介绍。
左下角的窗口显示所选ReadRequest消息的结构。消息的目的是调用Read服务。消息体是结构化的消息头。请注意,这里不考虑对消息进行加密或签名。
Message Header 如前所述,OPC-UA定义了一个异步协议。所以反应可能是不正常的。消息头包含一些基本信息,例如消息的长度,以及将消息与SecureChannel和每个请求关联到相应响应的必要信息。“分块”是指对超过最大网络数据包大小的消息进行拆分和重新组合。
Message Body 每个OPC-UA服务都有一个请求和响应数据结构形式的签名。这些是根据OPC UA协议类型系统定义的。请特别参阅与服务请求和响应对应的数据类型的 auto-generated type definitions。消息体以数据类型的标识符开头。然后是消息的主要有效载荷。
右下角的窗口显示所选ReadRequest消息的二进制有效负载。消息头以浅灰色突出显示。以蓝色突出显示的消息体显示编码的ReadRequest数据结构。
下一篇 6 数据类型