open62541 (R 1.1.2)中文文档 (译文)第一篇 (1 - 5)

open62541(R 1.1.2) 文档

注:原文PDF文档 是从官网下载的 Linux64bit的发布版本中自带的文档,原PDF中的源代码用PDF浏览器查看,有残缺。需要结合源文件中的示例代码进行相应的修改。或参考其它版本的文档。原文代码中的注释并没有译文,这个在使用时再补充进去。

第一篇 章节 一 至 五

第二篇 章节 六

第三篇 章节 七 至 十

第四篇 章节 十一 至 十三

1、简介

  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。

1.1 OPC统一体系结构

OPC UA 是一种工业通信协议,已在 IEC 62541系列中标准化。

OPC UA定义

  • 一种异步协议(建立在TCP、HTTP或SOAP之上),通过会话交换信息,(在上面)安全的通信通道,(在上面)原始连接上,
  • 基于二进制和基于XML的编码方案的协议消息类型系统
  • 信息建模的元模型,它将面向对象与语义三元关系相结合
  • 与服务器端信息模型交互的37个标准服务集。在协议类型系统中,每个服务的签名被定义为请求和响应消息

标准本身可从IEC购买或从OPC基金会网站免费下载,网址为https://opcfoundation.org/(您需要使用有效的电子邮件进行注册)。

OPC基金会推动了标准的不断改进和配套规范的发展。配套规范将已建立的概念和可重用组件从应用领域转换为OPC-UA。它们是与应用程序域中已建立的行业委员会或标准化机构联合创建的。此外,OPC基金会还组织了标准传播活动,并提供了合规认证的基础设施和工具。

1.2 open62541功能

open62541实现了OPC-UA二进制协议栈以及客户端和服务器SDK。它目前支持Micro-Embedded Device服务器配置文件和一些附加功能。服务器二进制文件的大小可以小于100kb,这取决于所包含的信息模型。

  • 通讯栈
    • OPC-UA二进制协议
    • 分块(大消息的拆分)
    • 可交换网络层(插件),用于使用自定义网络API(例如,在嵌入式目标上)
    • 加密通讯
    • 客户端异步服务请求
  • 信息模型
    • 支持所有OPC UA节点类型(包括方法节点)
    • 支持在运行时添加和删除节点(包括引用节点)
    • 支持对象和变量类型的继承和实例化(自定义构造函数/析构函数、子节点的实例化)
    • 单个节点的访问控制
  • 订阅
    • 支持数据更改通知的订阅/监视数据项
    • 每个监视值的低资源消耗(基于事件的服务器架构)
  • 代码生成
    • 支持从标准XML定义生成数据类型
    • 支持从标准XML定义生成服务器端信息模型(节点集合)

最初的发行版v0.3版本中缺少的功能包括:

  • 客户端加密通信
  • 事件(实现对象发出的通知、数据更改通知)
  • 客户端中的事件循环(后台任务)

1.3 寻求帮助

除此文档外,您还可以通过以下方式访问open62541社区

1.4 贡献

作为一个开源项目,我们邀请新的贡献者来帮助改进open62541。问题报告,错误修复和新功能是非常欢迎的。以下是新贡献者良好的起点:

2、生成open62541

open62541使用CMake构建库和二进制文件。使用git describe自动检测库版本。此命令基于当前标记返回有效的版本字符串。如果不是直接克隆源代码,而是使用某个版本中的tar或zip包,则需要手动指定版本。在这种情况下,使用例如

cmake -DOPEN62541_VERSION=v1.0.3

2.1 生成类库

2.1.1 在 Ubuntu 或 Debian 上用CMake生成

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)

2.1.2 在 Windows 上用CMake生成

在这里,我们将介绍 VisualStudio(2013或更新版本)的构建过程。要使用MinGW构建,只需替换CMake调用中的编译器选择。

cd <path-to>\open62541
mkdir build
cd build
<path-to>\cmake.exe .. -G "Visual Studio 14 2015"
  • 您可以使用 cmake-gui 作为图形用户界面来选择特性。

2.1.3  在 OS X 上生成

 sudo easy_install pip
  • 在shell中运行以下内容
brew install cmake
pip install sphinx # 用户文档生成
pip install sphinx_rtd_theme # 文档样式
brew install graphviz # 文档图形
brew install check # 单元测试

遵循Ubuntu的说明,不使用apt-get命令,因为这些命令都是由上面的包处理的。

2.1.3  在 OpenBSD 上生成

下面的过程适用于openbsd5.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

2.1.5在Ubuntu或Debian上用CMake在Docker容器内构建Debian包

下面是一个如何在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 目录中找到。

2.1.6 CMake构建选项和Debian打包

如果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。

2.2 生成选项

open62541项目使用CMake来管理构建选项、代码生成以及为不同的系统和ide生成构建项目。工具ccmake或cmake gui可用于以图形方式设置构建选项。大多数选项可以在代码生成后在ua_config.h(对于单文件版本为open62541.h)中手动更改。但通常不需要调整它们。

2.2.1 主要生成选项

CMAKE_BUILD_TYPE

  • RelWithDebInfo -O2 使用调试符号进行优化
  • Release -O2 无调试符号的优化
  • Debug -O0 使用调试符号进行优化
  • MinSizeRel -Os 无调试符号的优化

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中创建的。(实验功能!预计会有错误。)

2.2.2 生成可选项 

默认情况下,只生成主库共享对象Libopen62541.so(open62541.dll)或静态链接存档open62541.a(open62541.lib)。其他工件可以通过以下选项指定:

UA_BUILD_EXAMPLES :从examples/*.c编译示例服务器和客户端。
UA_BUILD_UNIT_TESTS :编译单元测试。可以使用maketest执行测试
UA_BUILD_SELFSIGNED_CERTIFICATE :为服务器生成自签名证书(需要openSSL)

2.2.3 SDK详细功能

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 适用于所有应用。可选选项如下:

  •  MINIMAL:与大多数客户端兼容的基本 Namespace zero 。但是 Namespace zero 太小了以至于不能通过CTT(OPC基金会的一致性测试工具)
  • • REDUCED:通过CTT的 Namespace zero。
  • • FULL:从官方XML定义生成的完整 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 文件。

2.2.4 生成调试选项

此组包含的构建选项主要用于库本身的开发。
UA_DEBUG:启用不用于生产构建的断言和其他定义
UA_DEBUG_DUMP_PKGS:以hextump格式转储服务器接收到的每个包

2.2.5 生成共享库

open62541足够小,大多数用户都希望静态地将库链接到他们的程序中。如果需要共享库(.dll,.so),可以在CMake中使用BUILD_SHARED_LIBS 选项启用此功能。注意,这个选项修改了ua_config.h文件,该文件也包含在open62541.h中,用于单个文件分发。

2.2.6 最小化二进制大小

通过调整构建配置,可以大大减少 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需求主要由以下设置引起:

  • 信息模型的大小
  • 连接的客户端数
  • 预先分配的配置的最大消息大小

2.3 生成示例

确保您可以按照前面的步骤构建共享库。构建示例的更简单的方法是在操作系统中安装open62541(请参阅安装open62541)

cp /path-to/examples/tutorial_server_firststeps.c . # copy the example server
gcc -std=c99 -o server tutorial_server_firststeps.c -lopen62541

2.4 生成支持特定系统

open62541库可以为许多操作系统和嵌入式系统构建。本文展示了一个已经测试过的架构的小片段。由于堆栈只使用C99标准,所以有更多支持的体系结构。

在arch文件夹中可以找到实现的体系结构支持的完整列表。

2.4.1 Windows, Linux, MacOS

默认情况下支持这些体系结构,并由CMake自动选择。

看看前面关于如何做到这一点的部分。

2.4.2 freeRTOS + LwIP

归功于@cabralfortiss

本文件基于对PR的讨论https://github.com/open62541/open62541/pull/2511。如果你有任何疑问,请先检查一下那里的讨论。

本文档假设您有一个使用LwIP和freeRTOS的基本示例,并且您只想向其中添加一个OPC-UA任务。

为freeRTOS+LwIP构建open62541的主要方法有两种:

  • 在CMake中选择交叉编译器,设置编译所需的标志(每个微控制器不同,因此可能比较困难),然后在文件夹中运行make,生成库。这个方法可能很难实现,因为您需要指定include文件和一些其他配置。
  • 使用freeRTOSLWIP体系结构生成open6254.h和open6254.c文件,然后将这些文件放入用于编译的IDE中的项目中。这是最简单的方法,文档只关注这种方法。

在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中没有标准的方法来执行以下操作,但是您需要在项目中执行以下配置:

  • 添加open62541.c文件进行编译
  • 将变量 UA_ARCHITECTURE_FREERTOSLWIP 添加到编译中
  • 确保open62541.h位于编译中包含的文件夹中。

在编译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。

3、安装open62541

3.1手动安装

可以使用众所周知的 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中启用以下功能:

  • Discovery(发现)
  •  FullNamespace(完整命名空间)
  • Methods(方法)
  • Subscriptions(订阅)、

以下功能未启用,可以使用生成选项(如“生成选项”中所述)启用:

  • Amalgamation(合并)
  • DiscoveryMulticast(发现多播)
  • Encryption(加密)
  • Multithreading(多线程)
  • Subscriptions(订阅)

重要提示:强烈建议不要在安装时使用 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

3.2预构建包

3.2.1包分支

Github允许您以.zip包的形式下载特定的分支。仅使用open62541的.zip包可能会失败:

  • CMake使用 git describe --tags 自动检测版本字符串。zip包不包含任何git信息
  • 构建堆栈期间的特定选项需要额外的git子模块,这些子模块没有内联在.zip中

因此我们提供包分支。它们具有前缀pack/并自动更新以匹配引用的分支。

以下是一些示例:

这些包分支有内联的子模块,版本字符串是硬编码的。如果您需要从源代码构建但不想使用git,请使用这些特定的包版本。

3.2.2预构建二进制文件

在我们的Github发布页面上,您总能找到每个版本的预构建二进制文件。

本这里 https://open62541.org/releases/  可以找到最近50次提交的Linux和Windows的单文件版。

3.2.3 Debian

Debian软件包可以在我们的官方PPA中找到:

  • 每日生成(基于主分支):https://launchpad.net/~open62541 team/+archive/ubuntu/daily
  • 发布版本(从版本0.4开始):https://launchpad.net/~open62541 team/+archive/ubuntu/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

3.2.4 OpenBSD

从 OpenBSD 6.7开始,目录misc/open62541可以构建open62541的发布版本。

从OpenBSD镜像安装二进制软件包:pkg_add open62541

4、教程

4.1 数据类型

本节显示数据类型的基本交互模式。确保在 types.h 中定义。

#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <stdlib.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); /* 初始化内存 */
	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);
}

4.1.2 NodeIds(节点ID)

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);
}

4.1.3 Variants(变体)

数据类型变量属于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;
}

4.2构建简单服务器

本系列教程将指导您完成open62541的第一步。要编译这些示例,您需要一个编译器(MS Visual Studio 2015 或更新版本、GCC、Clang和MinGW32都可以使用)。编译说明是为GCC提供的,但应该易于修改。

它也将非常有助于安装一个具有图形前端的OPC-UA客户端,如 Unified AutomationUAExpert。这将使您能够检查任何OPC UA服务器的信息模型。

首先,从http://open62541.org或在启用“合并”选项的情况下,根据生成说明生成它。从现在开始,我们假设您在当前文件夹中有open62541.c/.h文件。现在创建一个名为myServer.C的新C源文件,其中包含以下内容:

#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.h>
static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
	running = false;
}
int main(void) {
	signal(SIGINT, stopHandler);
	signal(SIGTERM, stopHandler);
	UA_Server *server = UA_Server_new();
	UA_ServerConfig_setDefault(UA_Server_getConfig(server));
	UA_StatusCode retval = UA_Server_run(server, &running);
	UA_Server_delete(server);
	return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

这就是一个简单的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 。在接下来的两个部分中,我们将继续详细解释代码的不同部分。

4.2.1服务器配置和插件

open62541为构建OPC-UA服务器和客户端提供了一个灵活的框架。目标是要有一个核心可容纳所有用例并在所有平台上运行的库。然后用户可以调整库以适应他们的通过配置和开发(特定于平台的)插件来实现用例。核心库仅基于C99甚至不需要基本的POSIX支持。例如,低级网络代码实现为一个可交换的插件。但别担心。open62541为大多数平台和现成的合理默认配置。

在上面的服务器代码中,我们只需使用默认的服务器配置并添加一个TCP网络层正在4840端口侦听。

4.2.2服务器生命周期

本例中的代码显示了服务器生命周期管理的三个部分:创建服务器、运行服务器和删除服务器。一旦设置了配置,创建和删除服务器就很简单了。服务器以UA_server_run启动。在内部,服务器使用超时来安排常规任务。在超时之间,服务器在网络层侦听传入的消息。

您可能会问服务器如何知道何时停止运行。为此我们创建了一个全局变量 RUN。此外,我们还注册了stopHandler方法,该方法捕捉操作系统试图关闭程序时接收到的信号(中断)。例如,在终端程序中按ctrl-c时会发生这种情况。然后,信号处理程序将运行的变量设置为false,一旦服务器重新获得控制权,服务器就会关闭。

为了在单线程应用程序中集成OPC-UA和它自己的主循环(例如由GUI工具包提供),可以手动驱动服务器。有关详细信息,请参阅服务器文档中有关服务器生命周期的部分。

所有服务器都需要服务器配置和生命周期管理。我们将在下面的教程中使用它,无需进一步评论。

4.3向服务器添加变量

本教程演示如何使用数据类型以及如何向服务器添加变量节点。首先,我们向服务器添加一个新变量。查看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 <signal.h>
#include <stdlib.h>
#include <open62541.h>

static volatile UA_Boolean running = true;
static void stopHandler(int sig) {
	UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
	running = false;
}

int main(void) {

	signal(SIGINT, stopHandler);
	signal(SIGTERM, stopHandler);
	UA_Server *server = UA_Server_new();
	UA_ServerConfig_setDefault(UA_Server_getConfig(server));

	// 添加节点
	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;
}

4.4将变量与物理过程连接

在基于OPC-UA的体系结构中,服务器通常位于信息源附近。在工业环境中,这意味着服务器靠近物理进程,客户端在运行时消耗数据。在上一个教程中,我们了解了如何向OPC-UA信息模型添加变量。本教程演示如何将变量连接到运行时信息,例如从物理进程的度量中。为了简单起见,我们将系统时钟作为底层的“进程”。

下面的代码片段分别涉及在运行时更新变量值的不同方法。这些代码片段一起定义了一个可编译的源文件。

4.4.1手动更新变量

首先,假设已经在服务器中为DateTime类型的值创建了一个标识符为“ns=1,s=current time”的变量。假设当一个新值从底层进程到达时,我们的应用程序就会被触发,我们可以直接写入变量。

#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.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-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);
}

4.4.2变量值回调

当一个值不断变化时,例如系统时间,在一个紧循环中更新该值将占用大量资源。值回调允许将变量值与外部表示形式同步。它们将回调附加到每次读操作之前和每次写入操作之后执行的变量。

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);
}

4.4.3可变数据源

使用值回调时,该值仍存储在变量节点中。所谓的数据源更进一步。服务器将每个读写请求重定向到回调函数。读取时,回调提供当前值的副本。在内部,数据源需要实现自己的内存管理。

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;
}

4.5使用变量类型

变量类型有三个函数:

  • 约束该类型变量的可能数据类型、值秩和数组维度。这允许根据泛型类型定义编写接口代码,因此它适用于所有实例。
  • 提供一个合理的默认值
  • 根据变量类型对变量进行语义解释

在本教程的示例中,我们用双精度值数组表示二维空间中的一个点。以下函数将相应的VariableTypeNode添加到变量类型的层次结构中。

#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.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);
}

在写入变量的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;
}

4.6使用对象和对象类型

4.6.1使用对象构建信息模型

假设我们要在OPC-UA信息模型中对一组泵及其运行状态进行建模。当然,所有泵的表示应该遵循相同的基本结构,例如,我们可能在SCADA可视化中有泵的图形化表示,该可视化对所有泵都是可恢复的。

遵循面向对象编程范式,每个泵都由具有以下布局的对象表示:

对象模型

以下代码手动定义泵及其成员变量。我们省略了对变量值的设置约束,因为这不是本教程的重点,已经讨论过了。(这段代码PDF中被截取掉了,已经不完整了)

#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.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_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,
}

4.6.2对象类型、类型层次结构和实例化

手动构建每个对象需要我们编写大量代码。此外,客户机无法检测对象是否代表泵。(我们可以使用命名约定或类似的方法来检测泵。但这并不是一个干净的解决方案。)此外,我们可能有更多的设备,而不仅仅是水泵。我们要求所有设备共享一些共同的结构。解决方案是在具有继承关系的层次结构中定义对象类型。

对象模型

标记为强制的子对象与父对象一起自动实例化。这由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;
}

4.7向对象添加方法

OPC UA信息模型中的对象可能包含与编程语言中的对象类似的方法。方法由MethodNode表示。请注意,多个对象可能引用同一MethodNode。实例化对象类型时,将添加对方法的引用,而不是复制MethodNode。因此,上下文对象的标识符在调用方法时总是显式地声明。

方法回调接受附加到方法节点的自定义数据指针、调用该方法的对象fom的标识符以及输入和输出参数的两个数组作为输入。输入和输出参数都是Variant类型。每个变量可以依次包含(多维)数组或任何数据类型的标量。

方法参数的约束是根据数据类型、值秩和数组维度定义的(类似于变量定义)。参数定义存储在MethodNode的子variableNode中,具有各自的browsename(0,“InputArguments”)和(0,“OutputArguments”)。

4.7.1示例:Hello World方法

该方法接受一个字符串标量并返回一个前缀为“Hello”的字符串标量。SDK在内部检查输入参数的类型和长度,这样我们就不必在回调中验证参数了。

#include <open62541/client_config_default.h>
#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.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.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);
}

4.7.2增加数组值方法

该方法以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;
}

4.8 局部监视数据项

对变量的当前值感兴趣的客户机不需要定期轮询该变量。相反,他可以使用订阅机制来获得有关更改的通知。

所谓的monitoreditem定义客户机想要监视的值(节点属性)和事件。在适当的条件下,将创建通知并将其添加到订阅中。队列中当前的通知会定期发送到客户端。

本地用户也可以添加monitoreditem。在本地,monitoreditem不通过订阅,并且每个都有一个单独的回调方法和一个上下文指针。

#include <open62541/client_subscriptions.h>
#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdlib.h>
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;
}

4.9生成事件

为了理解服务器中发生的许多事情,监视项可能很有用。尽管在许多情况下,数据更改并不能传递足够的信息,从而成为最佳解决方案。事件可以在任何时候生成,包含大量信息,并且可以被过滤,这样客户机只接收他感兴趣的特定属性。

4.9.1调用方法发出事件

下面的示例将基于服务器方法教程。我们将创建一个从服务器节点生成事件的方法节点。

我们要生成的事件应该非常简单。因为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);
}

4.9.2 设置事件

为了设置事件,我们可以首先使用UA_Server_createEvent为我们提供事件的节点表示。我们只需要我们的事件类型。一旦我们有了事件节点,它在内部被保存为一个ObjectNode,我们就可以像定义对象节点的属性一样定义事件的属性。不需要定义属性EventId、ReceiveTime、SourceNodeEventType,因为这些属性是由服务器自动设置的。在本例中,我们将设置字段"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;
}

4.9.3触发事件

首先,使用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 接收的EventIdEventId是由服务器内部生成的,是标识特定事件的随机唯一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;
}

4.10使用报警和条件服务器

除了使用被监视的项目和事件来观察服务器中的变化外,使用警报和条件服务器模型也很重要。警报是服务器组件状态发生变化时,由服务器根据内部服务器逻辑或用户特定逻辑自动触发的事件。组件的状态通过一个条件来表示。因此,所有条件子项(字段)的值都是组件的实际状态。

4.10.1通过改变状态触发报警事件

以下示例基于服务器事件教程。在继续这个例子之前,请务必理解正常事件的原理!

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;
}

4.11构建简单客户端

您应该已经有了以前教程中的基本服务器。open62541同时提供了服务器和客户端API,因此创建客户端与创建服务器一样简单。将以下内容复制到myClient.c文件中:

#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>
#include <open62541/plugin/log_stdout.h>
#include <stdlib.h>
int main(void) {
	UA_Client* client = UA_Client_new();
	UA_ClientConfig_setDefault(UA_Client_getConfig(client));
	UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
	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

4.11.1进一步任务

  • 尝试通过更改连接 opc.tcp://localhost:4840 ,连接到其他OPC UA服务器到一个适当的地址(记住查询的节点包含在任何OPC UA服务器中)。
  • 尝试在示例服务器(在构建简单服务器时内置)中使用“UA_Client_write”函数设置变量节点的值(ns=1,i=”the.answer”)。示例服务器需要更多的修改,即更改请求类型。答案可以在“examples/client.c”中找到。

4.12使用发布/订阅

正在进行的工作:本教程将在下一个PubSub批处理期间继续扩展。关于PubSub扩展和相应的open62541api的更多详细信息可以在这里找到:13.6 Publish/Subscribe。

4.12.1发布字段

PubSub发布示例演示了使用UADP编码通过UDP多播发布信息模型中的信息的最简单方法。

连接处理

可以在运行时创建和删除pubsubconnection。有关系统预配置和连接的更多详细信息可以在 tutorial_pubsub_connection.c 中找到。

#include <open62541/plugin/log_stdout.h>
#include <open62541/plugin/pubsub_ethernet.h>
#include <open62541/plugin/pubsub_udp.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
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++;

5、协议

在本节中,我们将概述OPC-UA二进制协议。我们关注二进制,因为这是open62541中实现的。基于TCP的二进制协议是OPC-UA最常用的传输层。一般概念也转化为标准中定义的基于HTTP和SOAP的通信。OPC UA中的通信最好从以下关键原则开始理解:

Request / Response 所有通信都基于请求/响应模式。只有客户端可以向服务器发送请求。服务器只能对请求发送响应。通常,服务器托管在(物理)设备上,例如传感器或机床。

Asynchronous Responses 服务器不必立即响应请求,响应可以按不同的顺序发送。这使服务器在处理特定请求之前(例如方法调用或延迟读取传感器时)保持响应。此外,订阅(又称推送通知)是通过特殊请求实现的,在这种请求中,响应被延迟到生成通知为止。

5.1建立连接

OPC-UA中的客户机-服务器连接由三个嵌套层组成:原始连接(raw connection)、安全通道(SecureChannel)和会话(Session)。有关详细信息,请参阅OPC UA标准第6部分。

raw connection 原始连接通过打开到相应主机名和端口的TCP连接和初始HEL/ACK握手来创建。握手建立连接的基本设置,例如最大消息长度。

SecureChannel 安全通道是在原始TCP连接的基础上创建的。安全通道是用 OpenSecureChannel 请求和响应消息对建立的。注意!即使安全通道是强制的,加密仍然可能被禁用。安全通道的 SecurityMode 可以是None、SignSignAndEncrypt。从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重用时。

5.2协议报文结构

对于以下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 数据类型

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值