Chapter2

复杂变量类型 #

在我们考虑 ModellingRules 以及子类型的工作原理之前,让我们先了解一下复杂变量类型。它们与复杂对象类型非常相似。主要区别在于它们只能将Variables用作InstanceDeclarations,而不是对象或方法。方法是为对象定义的;因此很明显它们不适合变量类型。变量始终是对象或其他节点的一部分,因此变量类型包含对象作为其一部分是没有意义的。变量类型只能公开其他变量,要么描述变量(如提供变量提供的值的工程单位),要么公开值结构的部分。例如,变量可以提供具有多个字段的复杂数据类型,子变量将公开每个字段。但也有可能变量提供由其他三个变量提供的测量值的平均值,因此这些变量成为它的子变量。通过将这些子变量作为 InstanceDeclarations 提供,VariableType 将此信息形式化,以便客户端知道变量类型的每个实例都将具有该信息。

上一节中为 InstanceDeclarations 定义的规则也适用于 VariableTypes,因为只有变量可以用作 InstanceDeclarations。

ModellingRules #

如果 TypeDefinition(即 ObjectType 或 VariableType)引用的每个实例具有 ModellingRule,则它将成为 InstanceDeclaration。ModellingRule 指定 InstanceDeclaration 相对于 ObjectType 实例会发生什么情况。有三个基本选择,也称为 ModellingRule 的 NamingRule。

  • 第一个选择是使 InstanceDeclaration 成为强制性的(Mandatory),这意味着每个实例都必须具有与 InstanceDeclaration 具有相同 BrowsePath 的对应项,并且必须与 InstanceDeclaration 属于同一类型(当它是 Object 或 Variable 时)或该类型的子类型。

  • 第二个选择是使其成为可选的(Optional),也就是说,每个实例都可以有这样的对应项。但并不要求每个实例都有这样的对应项。

  • 第三个选择是使其成为约束(Constraint),这意味着 InstanceDeclaration 为 TypeDefinition 的实例定义了一个约束。稍后我们将详细了解可能的限制。例如,基数限制指定 TypeDefinition 的实例应引用具有与 InstanceDeclaration 相同类型的定义范围的实例。

ModellingRules 是 OPC UA 中的一个可扩展概念,即服务器或标准信息模型可以定义自己的 ModellingRules。但是,它们始终必须指定前面提到的 NamingRules 之一。

地址空间中的ModellingRules #

ModellingRules 表示为 ModellingRule 类型的对象。每个 ModellingRule 都有一个名为 NamingRule 的变量(更准确地说是特性,参见第 2.6 节)。它包含 ModellingRule 的 NamingRule。InstanceDeclarations 引用具有 ReferenceType HasModellingRule 的 ModellingRule 对象来指定其 ModellingRule。每个节点只能使用 HasModellingRule 引用一个 ModellingRule。图 1.20 左侧显示了 ModellingRules 在地址空间中的使用方式。为了简化图表,我们在本书中使用右侧所示的符号,其中 ModellingRule 作为括号中的文本添加到节点。

地址空间中的ModellingRules

ModellingRules——强制(Mandatory)和可选(Optional) #

有两个标准 ModellingRules,分别称为 Optional 和 Mandatory,其名称与 NamingRule 相同。让我们看一个例子来了解这两个 ModellingRules 是如何工作的。在图 1.21 中,AddressType 具有 InstanceDeclarations Street,ModellingRule Optional,以及 City,ModellingRule Mandatory。这意味着每个实例都必须有一个 City,也可以有一个 Street。在图 1.21 中,您可以看到 Address1 既有 City,也有 Street。在这种情况下,两个实例也都有 ModellingRules。只要它们未被 TypeDefinition 引用,它们就不是 InstanceDeclarations。在这种情况下,允许对实例使用任何 ModellingRule。通常,普通实例没有 ModellingRules,如 Address2-4 中所示。Address2 省略了 Street,只提供了 City。在 Address3 和 4 中,您可以看到两者共享同一个 City。 ModellingRules Optional 和 Mandatory 未指定在创建 TypeDefinition 的新实例时服务器如何处理 InstanceDeclarations。它可以为 InstanceDeclarations 创建新的节点,也可以仅引用现有节点。TypeDefinition 的实例只需引用具有相同 BrowsePath 和相同类型(或子类型)的实例。例如,服务器还可以引用 InstanceDeclaration 节点,从而创建类似于静态类变量的东西,该变量对所有实例都具有相同的值。在运行时,只要每个类型的实例上始终有一个具有正确 BrowseName 和类型的节点,节点就可以更改。

应用可选和强制ModellingRules

在查看简单的 ModellingRules 之后,让我们看看变量和方法的所有权。如前所述,方法和变量必须始终由对象或对象类型使用 HasComponent 或 HasProperty 引用来引用。但由于变量和方法可以共享并属于多个对象,因此它们不属于一个对象。因此,如果删除了图 1.21 中的 Address3,则必须删除其下方的 Street[4],因为它没有引用它的 Node,但不能删除 City,因为它仍由 Address4 使用。

复杂 TypeDefinitions 的实例可以再次用作 InstanceDeclarations。在图 1.22 中,您可以看到 Address 被用作 InstanceDeclaration。因此,基于 InstanceDeclarations City 和 Street 的实例也成为 InstanceDeclarations。这意味着存在有关 ModellingRules 的规则。ModellingRules 可能会更改;但是,NamingRule 必须保持不变。唯一的例外是 Optional,它可以被 Mandatory 替换。通常,只有在约束条件收紧而不是放松的情况下,才可以替换 ModellingRules。在图 1.22 中,Street 的 ModellingRule 从 Optional 更改为 Mandatory。因此,有效实例是同时具有两者的 Employee1。不允许 Employee2 不提供 Street,虽然 AddressType 不需要它。

在图 1.22 中,揭示了另一条有关 ModellingRules 的规则。Address 对象具有 ModellingRule Optional,并且位于通往 Street 和 City 的唯一路径上。这意味着,当未提供 Address 时,Street 和 City 不必提供,正如您在有效的 Employee3 对象中看到的那样。

用作 InstanceDeclarations 并基于 InstanceDeclarations 的实例

在我们了解如何使用 NamingRule Constraint 以及如何创建我们自己的 ModellingRules 之前,让我们先考虑一些更复杂的例子。首先,让我们看看如果从 TypeDefinition 到 InstanceDeclaration 有多个 BrowsePath 会发生什么。图 1.23 给出了一个例子。TemperatureSensorType 具有 EngineeringUnit 变量(组织在Folder Configuration之下)和 Temperature 变量(组织在Measurement之下)。Temperature 变量使用 EngineeringUnit 变量来公开其工程单位。

有两种特殊情况;第一种情况是,两个不同的引用将相同的源与相同的目标连接起来。在图 1.23 中,您可以看到 Measurement 引用了 Temperature 两次。在这种情况下,在每个实例上,Measurement 的实例必须通过这两个引用引用相同的节点;不允许指向两个不同的节点。

第二种情况是存在两条不同的间接路径。在图 1.23 中,EngineeringUnit 由两条不同的路径引用。在这种情况下,允许来自 Configuration 中的一个节点和 Temperature 中的一个不同的节点的引用。在这个例子中,这可能没有意义,但在其他用例中,这是一种合理的方法,例如,当使用共享(静态)类变量时。

多个路径引用的 InstanceDeclarations 示例

现在让我们看看如果我们在 InstanceDeclarations 之间添加非层次引用会发生什么。在图 1.24 中,您可以看到一个 DeviceType 在两个子设备之间具有非层次引用。实例可能会或可能不会提供这种非层次引用。此行为是特定于服务器的,适用于由 OPC UA 定义的 ModellingRules。但是,您可以定义自己的 ModellingRule,指定如何处理 InstanceDeclarations 之间的非层次引用。

InstanceDeclarations 之间的非层次引用

ModellingRules——约束(Constraints) #

在了解了强制和可选InstanceDeclarations之后,让我们来看看用于定义约束的InstanceDeclarations,因此它们的 ModellingRule 中具有NamingRule Constraint。我们首先来看看当前 OPC UA 规范定义的唯一一个定义Constraint的 ModellingRule。这个 ModellingRule 称为 ExposesItsArray,可用于具有数组作为数据类型的VariableTypes。语义是数组的每个条目也作为子变量公开。图 1.25 对此进行了举例说明。TeamType 包含一个字符串数组和一个使用 ModellingRule ExposesItsArray 的ConstraintVariable。该类型的实例必须为数组的每个条目都有一个子变量,正如您在图 1.25 中的 UABookTeam 变量中所见。公开子变量允许引用数组的单个条目,因为它们作为地址空间中的节点公开。请注意,无需暴露数组即可访问数组的单个条目(订阅、读取或写入它们)。这可以通过 OPC UA 服务直接通过寻址数组的部分来完成(参见第 5 章)。

使用 ExposesItsArray ModellingRules

ExposesItsArray 只是 Constraint ModellingRules 的一个示例。您可以定义自己的 ModellingRules,以便在模型中定义约束。建模中的典型约束是基数限制。一种类型的实例应引用另一种类型的 n 和 m 个实例。这种约束可以通过 Constraint ModellingRule 公开。具有这种 ModellingRule 的 InstanceDeclaration 可用作包含最小值和最大值的两个 TypeDefinitions 之间的代理对象(参见第 3.3.8 节),并引用寻址的 ReferenceType。这种 ModellingRule 可能会集成到 OPC UA 的第二版中。因此,我们不会公开这种 ModellingRule 的任何细节,因为我们对其进行的建模可能与规范中略有不同。

最后,让我们讨论一下为什么 OPC UA 定义的 ModellingRules 不涵盖所有方面,例如非层次结构引用的情况或如何基于 InstanceDeclarations 创建实例。OPC UA 工作组开始定义所有这些内容。但事实证明,处理这个问题的方法和用例各不相同,其中一种或另一种可能性更为合适。指定所有这些可能性将导致相对较多的 ModellingRules,这将变得难以理解。此外,这些额外信息有多大用处也值得怀疑。在针对类型进行编程的情况下,您只需要知道目标节点的层次路径,并且必须知道该节点是可选的还是必需的。所有这些都由定义的 ModellingRules 提供。在基于类型创建实例时,服务器负责确保所有 InstanceDeclarations 的对应项都基于 ModellingRules 而存在。是否创建新节点或共享节点由服务器负责,不一定必须向客户端公开。因此,OPC UA 提供的 ModellingRules 是一个很好的基础,可以通过其他 ModellingRules 进行扩展,尤其是那些与约束相关的 ModellingRules。

复杂类型的子类型 #

简单类型的子类型化已在 2.5.1 节(ObjectTypes)和 2.5.2 节(VariableTypes)中进行了说明。对于变量类型,数据类型的使用可以在子类型中受到限制,因此子类型只能使用与超类型中定义的相同的数据类型,包括对数组大小的限制等。因此,客户端知道他们可以像使用超类型一样使用子类型。

关于复杂类型的子类型化,必须满足相同的保证。当复杂类型被子类型化时,仍然必须满足超类型的基本特征。因此,强制性的 InstanceDeclaration 也必须在子类型的每个实例上有效。一般来说,超类型的每个约束也必须在子类型上得到满足,并且只能进一步限制。这意味着可选的 InstanceDeclaration 可以在子类型上成为强制性的,但不能将强制性的 InstanceDeclaration 设为可选的。

在地址空间中公开复杂类型的子类型有两种可能。由于超类型的每个 InstanceDeclaration 在子类型上都有效,因此一种解决方案是每个子类型复制超类型的所有 InstanceDeclaration 或引用相同的节点。另一种解决方案是它们不被复制,但客户端也需要请求超类型的 InstanceDeclarations 才能获得子类型的全貌。第二种方法通常用于面向对象的编程语言,其中继承超类型的变量而无需将代码复制到子类型。OPC UA 也选择了第二种方法,以避免在具有多个级别的类型层次结构中节点爆炸。因此,除非您想要覆盖它们,否则不必在子类型上重复 InstanceDeclarations。在图 1.26 中,显示了复杂类型的子类型与面向对象类的子类型的比较。面向对象的类 Address 由 InternationalAddress 子类型化。这里只添加了一个额外的变量 Country,其他变量没有指定,但由 Address 继承。ObjectType InternationalAddressType 也适用同样的方法。只添加了 InstanceDeclaration Country,其他 InstanceDeclarations 都是继承的。要获得 TypeDefinition 的完整图景,您需要将超类型的 InstanceDeclarations 与 TypeDefinition 结合起来。这种组合称为完全继承的 InstanceDeclarationHierarchy,如图 1.26 中 InternationalAddressType 所示。这里捕获了所有信息,以便针对类型进行编程或了解至少需要实例化什么。

面向对象类和复杂类型的子类型

要使用完全继承的 InstanceDeclarationHierarchy,所有 InstanceDeclarations 都必须具有唯一的 BrowsePaths。因此,子类型不能对不同的 InstanceDeclaration 使用相同的 BrowsePath。但是,子类型能够覆盖超类型的现有 InstanceDeclaration(图 1.27)。例如,在InternationalAddressType中,可能需要提供 Street。因此,ModellingRule Optional 不再适用。正如我们之前所了解的,允许在 InstanceDeclaration 中将 Optional 更改为 Mandatory。对于子类型也是如此。因此,修改后的 InternationalAddressType 将创建自己的 Street 变量并将其定义为强制性的。由于在超类型中使用了相同的 BrowsePath,因此它不是额外的 InstanceDeclaration,而是 InternationalAddressType 的 InstanceDeclaration 覆盖了 AddressType 的 InstanceDeclaration。因此,完全继承的 InstanceDeclarationHierarchy 仍然只有一个 Street 变量,但这次 ModellingRule 已经发生了变化,因为使用了重写的 InstanceDeclaration。在 [UA 第 3 部分] 中,定义了如何获取完全继承的 InstanceDeclarationHierarchy 的详细算法,考虑了超类型链。

当然,在重写 InstanceDeclarations 时必须应用一些规则。一般来说,约束只能收紧,不能放松。这意味着必须使用与重写的 InstanceDeclaration 相同的类型或子类型,并且 ModellingRules 只能受到进一步限制。例如,如果有一个约束 ModellingRule 规定 Reference 必须存在 3-6 次,则允许增加下限并减少上限,但不能反过来。所以 4-5 是允许的,2-5 或 4-8 是不允许的。

在对复杂 TypeDefinitions 进行子类型化时覆盖 InstanceDeclarations

让我们考虑另一个使用更复杂的 InstanceDeclarationHierarchy 进行子类型化和覆盖的示例。在图 1.28 中,我们已经在图 1.23 中引入了 TemperatureSensorType。EngineeringUnit 变量由两个 BrowsePath 以及 Temperature 变量引用。子类型 MyTemperatureSensorType 派生自 TemperatureSensorType。在此子类型中,必须将 Temperature 的可选 EngineeringUnit 设为强制项。由于 EngineeringUnit 变量未直接由 ObjectType 引用,因此无法直接覆盖它。相反,必须覆盖 EngineeringUnit 路径上的第一个 InstanceDeclaration,并且必须复制该节点下 EngineeringUnit 的完整路径才能覆盖 EngineeringUnit。在图 1.28 中,您可以看到从 Measurement 开始的路径以及 Temperature 变量被覆盖,在其下方是 EngineeringUnit,将其 ModellingRule 更改为 Mandatory。

让我们看看这对于完全继承的 InstanceDeclarationHierarchy 意味着什么。如您所见,子类型中仅提供了 ObjectType 和 Measurement 之间的一个引用。但是,由于两个引用必须始终引用同一个节点,因此它们引用完全继承的 InstanceDeclarationHierarchy 中的一个节点。EngineeringUnit 可通过超类型中的两个路径访问,并且只在一个路径中覆盖。在这种情况下,完全继承的 InstanceDeclarationHierarchy 必须复制 EngineeringUnit,对于覆盖的路径提供更改的 ModellingRule,对于未覆盖的路径提供原始的 ModellingRule。请注意,尽管完全继承的 InstanceDeclarationHierarchy 引用了两个称为 EngineeringUnit 的不同节点,但 MyTemperatureSensorType 的实例可以在两个路径中引用同一个节点。

在 OPC UA 中,每个 ObjectType 必须是 [UA 第 5 部分] 中定义的 BaseObjectType 的子类型,因此只有一个类型层次结构。规范并未将类型层次结构限制为单继承。因此,多继承(即具有多个超类型)是一种选择。但是,规范仅指定了单继承的规则(仅复杂类型需要)[5]。因此,建议尽可能使用单继承。

覆盖具有多个 BrowsePaths 的 InstanceDeclarations

复杂的 TypeDefinitions 具有具有唯一 BrowsePaths 的 InstanceDeclarations。ModellingRules 定义了 TypeDefinitions 实例上允许的内容和不允许的内容。

实例可以具有指向不同节点的相同 BrowsePath;TranslateBrowsePathsToNodeIds 服务可用于根据 TypeDefinition 收集节点。

OPC UA 定义了一个开放模型。只要它不受 TypeDefinition 上的某些约束定义,就可以向 TypeDefinition 的实例添加引用,从而向这些实例添加变量、方法等。但是,服务器可能始终限制这些功能,而无需在 TypeDefinition 上明确说明它们。

在对 TypeDefinitions 进行子类型化时,必须始终保证仍然满足超类型的约束。这包括 ModellingRules 的语义、变量的数据类型以及任何其他约束。

可以通过在子类型中提供相同的 BrowsePath 来覆盖 InstanceDeclarations。

数据变量和特性 #

OPC UA 定义了两种变量:数据变量(Data Vaiables)和特性(Property)。当数据建模时,即当您必须决定使用数据变量还是特性来表示某些数据时,这两个概念之间的区分并不总是容易的。这导致客户端也并不总是容易决定如何处理特性和数据变量。但是,在下文中,我们将尝试解释这些概念之间的区别,并在第 3 章中给出何时使用什么概念的指南。

数据变量用于表示对象的数据,例如温度传感器的温度或流量变送器的流量。数据变量可以是复杂的,也就是说,它们可以具有包含部分数据的子变量和描述它们的特性(见下文)。复杂数据变量用法的一个示例是,当数据变量提供包含设备测量的所有数据的复杂类型和仅包含部分测量数据的子变量时。例如,该设备可以测量温度和流量。这两个值都捕获在一个数据变量中,并且也分别在子变量中公开。另一个示例是通过三个温度传感器的测量值的平均值计算的温度。表示各个测量值的变量可以作为聚合变量的子变量公开。

特性用于表示节点的特征,例如,包含测量温度的工程单位,无论是以°C、°K 还是华氏度为单位。通常,当需要描述节点的某些特征时,就会使用特性,而这些特征不会被节点的属性捕获。方法的输入参数和输出参数是特性的另一个示例。特性很简单。它们不能公开子变量,并且始终是每个层次结构的叶子,也就是说,不能是任何层次引用的来源。

这是数据变量和特性的简要定义。虽然这个定义非常清楚地将某些数据区分为数据变量和特性,但中间有一个很大的灰色区域不清楚。指示设备是生成真实数据还是模拟数据的可写标志是数据变量还是特性?存储在 OPC UA 服务器中的供应商地址是数据变量还是特性?

查看数据变量和特性的另一种方法是了解数据存储的位置以及更改频率。预计数据变量通常会经常更改其值,并且通常由底层设备提供,而特性的值则不会经常更改,并且存储在某些配置数据库中。但是,OPC UA 标准并未对此进行规定,因为无法明确指定数据经常更改或不更改的含义。是每毫秒一次、每秒一次还是每天一次?并且根据服务器的模式,此行为可能会有很大不同。在设计系统时,由于设备未连接或未激活,因此温度设备的温度可能根本不会改变。由于不同的工程任务,工程单位可能会更改多次。在运行时,情况会有所不同。不过,考虑在线数据与配置数据可能会帮助您思考。

除了语义上的考虑,数据变量和特性之间还存在语法差异。

每个节点可能具有特性。它们使用 HasProperty 引用连接。特性必须属于至少一个节点,即成为至少一个 HasProperty 引用的目标。特性没有类型,或者更准确地说,所有特性都指向称为 PropertyType 的同一VaribaleType。特性的语义由其 BrowseName 定义。由于语义由 BrowseName 定义,因此节点的每个特性都必须具有唯一的 BrowseName。特性不能是任何分层引用的来源,这意味着特性不能具有特性。

数据变量必须属于对象或对象类型。因此,它们必须由来自对象、对象类型、变量或变量类型的 HasComponent 引用。允许来自变量或变量类型的引用,因为它们用于公开复杂的数据变量,并且这种复杂数据变量的根又是对象或对象类型的一部分。数据变量是有类型的。每个数据变量都是 BaseDataVariableType 类型或子类型。反过来,数据变量的 BrowseName 不必是唯一的,因为语义由类型定义。

数据变量是更强大的概念,而特性使用起来非常简单。如果您必须决定是使用数据变量还是特性,并且无法根据本节中描述的语义差异做出决定,则应考虑语法。如果您需要使用特性未直接提供的某些功能(例如,通过子类型扩展),则必须选择数据变量。否则,您应该考虑使用特性,因为它们更易于处理。

特性不能是数据变量,反之亦然。这意味着特性不能由 HasComponent 引用,并且必须是 PropertyType 类型,而数据变量不能由 HasProperty 引用,并且必须是 BaseDataVariableType 类型或子类型。

特性不能是任何分层引用的来源。但是,它们可以是非分层引用的来源。每当您需要向特性添加信息时,都必须使用非分层引用。这意味着当您需要以相同的方式为数据变量和特性添加信息时,必须使用非分层引用。OPC UA 规范使用此方法,例如,当引用变量的历史配置时。

特性必须具有唯一的BrowserNames,即任何节点都不能使用 HasProperty 引用引用具有相同BrowserName的两个特性。

对象、变量和方法的 ModelParent #

像 HasComponent 这样的 ReferenceTypes 可以很好地指示所引用的组件包含或描述其父级的某些特征。但是,当查看 TypeDefinition 及其实例时,您会发现某些节点可能由多个节点共享,例如,静态类变量包含所有实例的相同值。只要客户端只读取该节点的数据,它就不应该关心该节点是否共享。但是,一旦客户端打算更改节点,就希望客户端知道在什么范围内更改了节点。当然,客户端可以浏览反向引用,以了解节点被引用的频率。但是,可能不提供反向引用,这在具有静态类变量时是可以预料的,因为您通常不想引用类型的每个实例。此外,让其他几个节点引用节点并不能为您提供使用哪个节点来定义其范围的信息。

为此,OPC UA 引入了称为 ModelParent 的概念。它由 HasModelParent 引用建模,该引用从所包含的节点指向定义所包含节点范围的父节点。在类变量的示例中,它将是 TypeDefinition 节点。让我们看一个具体的例子。图 1.29 中的 DeviceType 具有一个名为 Icon 的特性,表示图标[6],例如用于展示类型的树控件。像 Device1 这样的实例共享此图标,也就是说,它们指向同一个节点。如果客户端想要更改单个实例 Device1 的图标,它不能只编写一个新的图标,因为这会影响 TypeDefinition 和所有其他实例,如 Device2。客户端可以创建一个新的图标节点并让 Device1 引用该节点。因此,客户端可以在 Device1 的范围内进行更改,而无需更改其他实例的 TypeDefinition。如果客户端想要更改 TypeDefinition 的图标,它可以通过遵循 HasModelParent 引用来意识到图标在正确的范围内。在这种情况下,图标的改变也会影响实例,但这是改变 TypeDefinition 的图标时的预期用途。

ModelParents 使用示例

必须为使用标准 ModellingRules Optional、Mandatory 和 ExposesItsArray 的所有实例提供 HasModelParent 引用。允许为具有其他 ModellingRules 或没有 ModellingRules 的对象、变量和方法提供引用,但这不是必需的。因此,如果该功能可用,客户应该使用它,但他们不能期望在没有 ModellingRules 或未由 OPC UA 定义的 ModellingRules 的节点上提供该功能。

数据类型 #

除了变量和变量类型的Value属性之外,所有属性都具有固定的数据类型。变量和变量类型的DataType属性与ValueRank和ArrayDimensions属性一起使用,以定义特定变量或变量类型的Value属性的数据类型。变量用于定义事件字段,因此这也适用于事件字段。使用相同机制来定义方法中参数数据类型。ValueRank和ArrayDimensions属性用于定义数据类型是标量还是数组。DataType属性用于定义用作标量或数组的类型。该属性包含DataType节点的NodeId。DataType表示为地址空间中的节点。这允许服务器定义其自己的DataType,并允许客户端访问有关DataType的信息。 OPC UA 区分了四种数据类型:

  • 内置数据类型是 OPC UA 规范定义的一组固定的数据类型,无法通过标准化或特定于供应商的信息模型进行扩展。它们提供基本类型,如 Int32、Boolean、Double,以及 OPC UA 特定类型,如 NodeId、LocalizedText 和 QualifiedName。内置数据类型的完整列表可在 [UA 第 6 部分] 中找到。

  • 简单数据类型是内置数据类型的子类型。它们在网络上的处理方式与它们的超类型完全相同,也就是说,当服务器发送并由客户端接收或反之亦然时,无法将简单数据类型的具体值与其超类型的相同值区分开来。但是,客户端可以访问变量的DataType属性以获取有关简单数据类型的信息。简单数据类型的一个例子是 Duration,它是 Double 的子类型,定义以毫秒为单位的时间间隔。信息模型可以添加自己的简单数据类型。

  • 枚举数据类型表示一组离散的命名值。枚举的处理方式始终与线路上的内置数据类型 Int32 相同。枚举数据类型的一个示例是 NodeClass 中使用的 NodeClass 属性。信息模型可以添加自己的枚举数据类型。

  • 结构化数据类型表示结构化数据。它们是指定用户定义的复杂数据类型的最强大的构造。结构化数据类型的一个示例是用于定义方法参数的数据类型。它包含参数的名称、数据类型和描述。信息模型可以添加自己的结构化数据类型。

除了这些数据类型之外,还有一组抽象数据类型不属于这些类别,仅用于组织数据类型层次结构。

DataType节点类 #

所有数据类型都表示为地址空间中的DataType节点类的节点。此节点类的属性总结在表 1.10 中。

根据DataType的特征,提供附加信息。所有DataType都在DataType层次结构中进行管理。此层次结构仅支持单继承,即除了用作每个层次结构根的 BaseDataType 之外,每个数据类型都只有一个超类型。

DataType 的属性
属性 数据类型 描述
包含表1.1中定义的所有公共属性
IsAbstract Boolean 指示DataType是否抽象。抽象DataType可用于DataType属性。但是,具体值必须属于具体DataType

内置和简单数据类型 #

1.30 显示了内置数据类型和一些简单数据类型的数据类型层次结构。由于内置数据类型的处理由 OPC UA 规范定义,因此无需将有关这些数据类型的附加信息添加到地址空间中。简单数据类型的处理由其超类型定义。在第 2.8.5 节中,我们将描述一些具有特殊处理的内置数据类型的特征,例如 NodeId 和 LocalizedText。

内置和简单数据类型的数据类型层次结构

枚举数据类型 #

枚举数据类型在DataType层次结构中被标识为抽象数据类型Emumeration的子类型。由于枚举数据类型始终以 Int32 形式处理,因此地址空间中唯一需要的附加信息是将离散的命名值集映射到整数值。因此,将一个名为 EnumStrings 的标准特性(包含 LocalizedText 数组)添加到DataType节点。每个整数值都可以映射到数组中的条目。

在图 1.31 中,这由名为 MotorStatus 的用户定义枚举数据类型举例说明。数据类型节点具有一个名为 EnumStrings 的属性,其中包含具有离散命名值集的 LocalizedText 数组。使用此数据类型的变量提供指向数组的基于零的 Int32 值,例如,图 1.31 中的 Motor1 变量状态。

枚举数据类型的示例

结构化数据类型 #

结构化数据类型是最强大也是最复杂的数据类型。它们始终是抽象数据类型(称为Structure)的子类型。数据类型由发送方编码并由接收方解码。内置数据类型也是如此,但这里的编码由 OPC UA 规范明确定义。但对于结构化数据类型,并未定义线路上的处理。因此,对于每种结构化数据类型,必须定义如何编码它们。服务器必须提供此信息,以便客户端在收到此信息时可以解码数据,并且如果要将值写入服务器,可以对数据进行编码。在详细介绍之前,让我们先看看它是如何工作的。在图 1.32 中,您可以看到 OPC UA Client1 使用 OPC UA Binary 作为数据编码连接到 OPC UA 服务器(有关详细信息,请参阅第 6.2 节)。Client1 请求数据 (1.1)。服务器以内部格式从其数据源获取此数据,并将其转换为 OPC UA 二进制 (1.2)。然后它将编码的数据发送到Client1 (1.3)。Client1 必须将数据解码为其内部格式才能使用它 (1.4)。对于使用 XML 数据编码连接到 OPC UA 服务器的Client2,处理方式类似。Client2 请求数据 (2.1),服务器使用其内部格式并对数据进行编码 (2.2)。但在这种情况下,数据被编码为 OPC UA XML 格式并以此方式发送到客户端 (2.3)。Client2 必须将这些数据解码为其内部格式 (2.4)。因此,您可以看到,根据连接的数据编码,使用了两种编码。

编码和解码数据

上述场景用于所有内置数据类型,因此也用于枚举和简单数据类型。[UA 第 6 部分] 中为二进制和 XML 定义了每个内置数据类型的编码。对于结构化数据类型,编码未定义,因此必须由提供这些数据类型的服务器提供。查看上述场景,似乎服务器必须为每种结构化数据类型提供 XML 编码和二进制编码。这是可取的,但不是必需的。也可以通过二进制编码连接发送 XML 编码值,反之亦然。因此,服务器只需提供一种编码,该编码可用于两种数据编码选择的连接。在连接中使用不同编码值的可能性允许另一种场景。对于使用相同内部格式的数据的特定客户端和服务器,可能希望在线上也使用该格式,以减少编码和解码数据的工作量。OPC UA 通过允许服务器提供和客户端选择其他编码来提供这种可能性。该场景如图 1.33 所示。在这里,客户端请求数据 (1.1),服务器可以获取其内部数据而无需对其进行编码 (1.2)。当发送它们 (1.3)时,它们嵌入在编码消息中,客户端可直接使用而无需解码它们 (1.4)。

用户自定义的编码

请注意,使用特定编码不一定是个好主意。除了互操作性问题之外,当发送的数据量大于例如以 OPC UA 二进制编码的相同数据时,性能可能会下降。通过摆脱编码和解码而获得的性能可能会在线路上传输更多数据时丢失两次。因此,这是一个应该避免的非常特殊的用例。预计特定的标准信息模型可能需要此功能,尽管到目前为止,事实证明目前尚未使用此功能(据作者所知)。

在对编码的工作原理进行了长时间的讨论之后(第 6.2 节中有更多详细信息),让我们看看这对结构化数据类型意味着什么。结构化数据类型可以提供多种编码,而具体的结构化数据类型必须提供至少一种编码。服务器必须在地址空间中提供编码,以便客户端可以获取有关编码的信息。因此,表示结构化数据类型的DataType节点指向表示编码的DataTypeEncoding对象(参见图 1.34 中的示例)。此类对象的 NodeID 与线路上的每个值一起发送,因此接收方知道发送方使用了哪种编码。客户端可以选择他们想要接收的具体值的编码,也可以将其保持打开状态以获取默认编码。因此,定义了两个 BrowseName:“Default Binary”和“Default XML”,两者的 NamespaceIndex 均为 0(有关命名空间的详细信息,请参见第 2.8.5 节)。具体的结构化数据类型必须引用其中至少一个。如果同时提供两者,则使用 XML 数据编码连接的默认“Default XML”,使用 OPC UA 二进制连接的默认“Default Binary”。如果只提供其中一个,则在两种情况下都将其用作默认值。

当然,客户端需要获取编码如何工作的信息。因此,服务器提供了一个 DataTypeDictionary 变量,其中包含有关几种 DataType 编码的信息。这可能会产生大量的数据,通常客户端应该读取一次 DataTypeDictionary 并持久缓存它。重新连接到服务器时,客户端只需检查信息是否已更改[7],然后才需要更新缓存的信息。对于存储在 DataTypeDictionary 中的每个 DataType,都会公开一个 DataTypeDescription 变量,该变量包含指向 DataTypeDictionary 中的 DataType 的指针。在图 1.34 中,以 MyTypeDictionary 为例,其中包含 MyType1 和 MyType2 的编码信息。除了指向 DataTypeDictionary 中的DataType的指针之外,DataTypeDescription 还可以提供直接包含编码信息的可选特性 DictionaryFragment。当 DataTypeDictionary 变得很大并且某些客户端不想读取整个字典而只想读取有关某些 DataType 的信息时,这很有用。在图 1.34 中可以看到这样一个 Property,它使用 OPC Binary 来定义具有两个整数的 DataType MyType1 的编码。

结构化数据类型的示例

由于 DataTypeEncoding 表示具体的编码,因此它指向 DataTypeDescription 变量。这是一种间接方式;指向 DataTypeDictionary 的指针并不直接存储在 DataTypeEncoding 中。这样做是因为多个 DataTypeEncoding 可能会选择相同的 DataTypeDescription,如图 1.34 所示。您可以将 DataTypeEncoding 对象视为代理对象,该对象仅用于将一些信息(在这种情况下为名称和 NodeId)放入引用中(有关代理对象的详细信息,请参阅第 3.3.8 节)。

最后,服务器必须指定如何在 DataTypeDictionary 中定义DataType。“如何”由 DataTypeSystem 指定。OPC UA 规范预定义的 DataTypeSystems 是 [UA 第 3 部分] 中定义为附录的 OPC 二进制和 [W3C04a] 和 [W3C04b] 中定义的 W3C XML 模式。但是,服务器可以使用其他 DataTypeSystems 为其数据类型定义特定编码。当服务器仅提供这些编码作为默认编码时,它们不能指望通用客户端能够解释这些数据。DataTypeDescription 使用的 DataTypeDictionary 指针的格式取决于 DataTypeSystem。对于 OPC Binary,它是数据类型的名称;对于 XML Schema,它是指向架构元素的 XPath 表达式。

特殊的内置数据类型 #

在介绍了不同类型的 DataType(其中大多数可用于创建用户定义的 DataType)之后,让我们简要介绍一些内置 DataType。我们不会详细介绍 Int32、Boolean 或 Double 的表示方式。但是,一些内置 DataType 在内部使用特定结构,需要解释该结构才能理解它们的用法。

我们正在考虑的第一个内置 DataType 是 NodeId。它是一种非常基本的 DataType,用于在各个地方寻址节点。NodeId 是一种内置 DataType。但是,此 DataType 背后有一个结构,如图 1.35 所示。NodeId 的第一部分是 NamespaceIndex,后跟定义最后一部分(标识符)数据类型的枚举。标识符可以是数值、GUID、字符串或不透明值(字节字符串)。它与 NamespaceIndex 一起唯一地标识 OPC UA 服务器中的节点。显然,NodeId 的长度取决于标识符的具体值和 IdentifierType。经常使用的 NodeId(如 DataType NodeId 和 DataTypeEncoding NodeId)应使用较小的 NodeId,最好是数字 NodeId。Service RegisterNodes 允许服务器将相对较长的 NodeId 转换为较短的 NodeId,以便客户端多次使用(有关详细信息,请参阅第 5 章)。

NodeId 的结构

为了优化目的,引入了 NodeId 的 NamespaceIndex(以及 QualifiedName)。使用 NamespaceIndex 代替 Namespace URI,如“http://opcfoundation.org/UA/”,即 OPC UA 的 Namespace URI。Namespace URI 与标识符结合使用,在 OPC UA 服务器的地址空间中创建唯一 ID。Namespace URI 标识定义标识符的命名机构。命名机构是 OPC 基金会、定义标准信息模型的其他组织、服务器供应商或使用 OPC UA 服务器公开其信息的系统。

NamespaceIndex 是指向每个 OPC UA 服务器提供的 NamespaceArray 的指针。因此,客户端只需要读取 NamespaceArray 一次,之后在 NodeId 中仅使用小整数值而不是大字符串值。在 NodeId 中使用 Namespace URI 代替 NamespaceIndex 会导致巨大的开销。NodeId 在服务调用中非常常用,因为它们用于寻址节点。图 1.36 显示了如何使用 NamespaceIndex 的示例。

NamespaceArray 和 NamespaceIndex

客户端保证在连接期间不会删除 NamespaceArray 的任何条目。但是,可能会添加新条目,因此客户端应订阅 NamespaceArray 的更改。当客户端与服务器断开连接时,它对 NamespaceArray 没有任何保证。NamespaceArray 可能已完全更改,包括数组的顺序。因此,当客户端重新连接时,索引“3”表示的 Namespace URI 可能由索引“5”表示。因此,客户端不应在不存储 Namespace URI 的情况下保留 NodeId 或 QualifiedName。该规则有一个例外:NamespaceIndex“0”始终为 OPC UA 的 Namespace URI 保留。

让我们看一下使用类似于 NamespaceIndex 概念的内置 DataType ExpandedNodeId。ExpandedNodeId 主要用作服务参数,但在某些情况下,将其用作变量的值也是合理的,例如用于 AuditEvents(参见第 9.5 节)。 ExpandedNodeId 允许引用另一个 OPC UA 服务器的节点。例如,一些供应商联系信息仅存储在一个公司范围的 OPC UA 服务器中,但被其他几个 OPC UA 服务器引用。TypeDefinitions 也可以在一个服务器中管理,并被使用这些类型的其他几个服务器引用(即具有这些类型的实例)。因此,ExpandedNodeId 具有与 NodeId 类似的结构,但有两个附加字段(参见图 1.37)。如果实际的 Namespace URI 不在提供 ExpandedNodeId 的服务器的 NamespaceArray 中,则 NamespaceURI 字段允许存储实际的 Namespace URI。其次,ServerIndex 指向实际管理节点的服务器。与 NamespaceIndex 一样,ServerIndex 指向 ServerArray。适用与 NamespaceArray 相同的约束。ServerIndex 0 为本地服务器保留,也就是说,如果 ExpandedNodeId 的 ServerIndex 为 0,则引用本地节点。

ExpandedNodeId 的结构

QualifiedName DataType 用作 BrowseName。与 NodeId 一样,它具有 NamespaceIndex,遵循与 NodeId 中相同的规则。此外,它还包含一个字符串,表示符合 Namespace 要求的名称。图 1.38 总结了此结构。

QualifiedName 的结构

LocalizedText 数据类型提供本地化文本。其结构如图 1.39 所示。它包含本地化文本(字符串)和区域标识符(第二个字符串,符合 RFC 3066 [A101] 的规定)。

LocalizedText 的结构

尽管 LocalizedText 的结构相对简单,但在处理 LocalizedText 时仍需遵循一些注意事项。当客户端连接到服务器时,它会指定请求的语言环境的优先级列表。服务器根据该列表返回所有 LocalizedText 类型的值。它会尝试使用列表中的第一个语言环境返回值,如果该语言环境不可用,则使用下一个语言环境,依此类推。如果客户端指定的所有语言环境都不可用,服务器将使用可用于值的默认语言环境。因此,客户端应始终检查 LocalizedText 类型值的返回语言环境。

编写 LocalizedText 值稍微复杂一些。通过编写 LocalizedText,客户端仅更改要写入的值中指定的语言环境的值。这可能导致值在一个语言环境中更改,但在另一个语言环境中仍为旧值的情况。为了避免这种情况,编写 LocalizedText 时运用以下规则。

  • 写入具有具体语言环境的值将更改现有值,或者在之前不可用时添加具有语言环境的值。

  • 当将空文本与具体语言环境一起写入时,将删除语言环境的值。

  • 通过为文本和语言环境写入空值,将删除所有语言环境的所有条目。

这些规则允许想要首先更改所有语言环境值的客户端通过写入空值来删除所有值,然后为所有所需语言环境添加所需值(这可以在一次写入服务调用中完成,请参阅第 5 章)。

数据类型总结 #

在 OPC UA 协议中,内置、简单和枚举数据类型的编码是固定的和优化的。应用程序应在适当的时候使用这些类型。但是,为了支持复杂的用户定义类型,必须使用结构化数据类型。结构化数据类型允许用户定义编码。但它们在线路上的开销很小,因为 DataTypeEncoding NodeID 会随每个值一起发送。简单数据类型像其内置数据类型一样放在线路上,因此针对传输进行了优化。另一方面,它们不是类型保存的,也就是说,接收该值的客户端无法将它们与内置数据类型区分开来。可以从变量的数据类型属性中获得数据类型信息,但如果变量仅定义超类型并且值使用子类型,则此信息会丢失。一个例子是提供简单数据类型 Money 的变量,其中各个值将是美元、欧元等子类型。客户端将无法识别子类型,并且使用这些子类型发送的值不会被服务器识别。对于这些场景,需要使用结构化数据类型,其中类型信息与每个值一起发送。

服务器应该在其地址空间中公开DataType节点,但这不是必需的。因此,客户端必须能够处理并非每个服务器都提供它们的事实。

变量可以指向抽象数据类型。具体值必须始终属于具体数据类型。具体值可以是变量指定的数据类型的子类型。因此,客户端必须处理这样一个事实:它们接收的不是变量指定的数据类型,而是它的子类型。对于内置数据类型,这没有区别,但对于结构化数据类型,子类型的结构可能不同。

客户端可以选择他们想要接收的结构化数据类型的编码,而与建立连接时选择的数据编码无关。

视图 #

我们终于来到了 OPC UA 的最后一个 NodeClass,即视图。视图用于限制大型地址空间中可见节点和引用的数量。通过使用视图,服务器可以组织其地址空间并提供针对特定任务或用例定制的视图。例如,服务器可以提供用于维护服务器的视图。对于该任务,只有包含维护信息的节点才是重要的,其他节点可以隐藏。

有两种方式可以查看 OPC UA 中的视图:

  • 视图表示为地址空间中的节点。此节点为视图内容提供了入口点。视图的所有节点都必须从视图节点开始可访问。但是,它们不必由视图节点直接引用;它们也可以由连接到视图节点的其他节点间接引用。

  • 浏览地址空间时,视图节点的 NodeID 可用作过滤器参数。通过使用视图作为过滤器,服务器可以限制对其他节点的引用。因此,在视图上下文中浏览地址空间的客户端将只能看到地址空间的片段。请注意,视图上下文仅在浏览和查询地址空间时用于服务,而不是在读取或写入具体节点时使用。

通过结合这两种查看视图的方式,您可以获得完整的画面。当您想要访问视图的内容时,通常从视图节点开始,然后使用视图作为过滤器在视图上下文中浏览。让我们看一个例子。在图 1.40 中,您可以看到一个地址空间,其中有一些包含维护信息的设备。名为Maintenance的视图引用所有设备,当在视图上下文中浏览时,仅返回以粗体显示的引用。负责维护的用户可以从视图节点开始,并在视图上下文中浏览以获取所有相关信息。用户不必从视图节点开始浏览。他还可以使用不同的起始节点(或具有可用的设备 NodeID)来到设备,从而从设备节点开始在视图上下文中浏览。

组织地址空间的视图示例

有几种不同的方法可以使用视图来组织地址空间。第一种方法如图 1.40 所示。这里,有一个节点组织,视图为隐藏一些信息的节点提供了一个额外的入口点。另一种方法是,所有节点都组织在视图节点下,因此客户端始终从视图节点开始访问地址空间。图 1.41 对此进行了举例说明,其中视图Engineering和Online是进入地址空间的唯一入口点。并非所有客户端都能够在视图上下文中浏览。在这种情况下,他们应该将视图节点视为Folder对象,以允许访问这些地址空间。

视图作为地址空间唯一入口点的示例

当然,也可以将这两种方法结合起来,如图 1.42 所示。这里,Engineering视图和Online视图是进入地址空间的入口点,而Maintenance视图则用作指向其他视图的附加视图。

地址空间中视图的组合使用

1.11 总结了 View NodeClass 的属性。设置 ContainsNoLoops 属性后,客户端可以使用此信息来优化其显示,因为它们知道不会发生循环。当 View 是 EventNotifier 时,可以保证作为 View 一部分的所有 EventNotifier 对象的事件也通过 View Node 发送。

View 的属性
属性 数据类型 描述
包含表1.1中定义的所有公共属性
ContainsNoLoops Boolean 此属性表示视图中包含的节点在遵循分层引用时是否跨越非循环层次结构
EventNotifier Byte 此属性表示一个位掩码,用于标识视图是否可用于订阅事件以及事件的历史记录是否可访问和更改

视图可用于跟踪地址空间的不同版本。ViewVersion属性会在视图内容发生更改时更新。此处的内容是指属于视图的引用和节点,而不是这些节点的属性。客户端可以访问视图的不同版本。详细信息在 2.11.3 节中定义。

在 OPC UA 版本 1 中,视图始终由服务器定义,也就是说,客户端没有(标准)方式来定义视图。客户端只能使用服务器提供的视图。

视图不能组合,也就是说,客户端一次只能在一个视图的上下文中浏览。但是,服务器可以在内部实现其逻辑,即一个视图基于另一个视图。但这些信息不能以标准方式向客户端公开。

事件 #

订阅 EventNotifier 时,事件通过通知接收。它们通常在地址空间中不可见(警报和条件除外 - 参见第 4.9 节)。事件是按类型分类的,并且根据事件的类型,事件具有不同的字段。OPC UA 定义了可以扩展的 EventTypes 的基本层次结构。因此,需要服务器在地址空间中公开其 EventType 层次结构,以便客户端可以检索此信息。使用有关 EventType 层次结构的信息,客户端可以创建过滤器,以筛选他们感兴趣的事件字段以及他们想要接收的事件类型。如图 1.43 所示。

使用 EventType 层次结构订阅事件

有关事件过滤器如何工作的详细信息,请参见第 5 章。为了在地址空间中表示事件类型,没有引入新的 NodeClass,而是使用 NodeClass ObjectType。这很有意义,原因如下。

  • 无需额外信息即可公开事件类型,因此 ObjectType 是一种的合理方法,它支持继承,Variables可用于公开事件可用字段。

  • 无需引入额外的 NodeClass,客户端可以使用处理 ObjectType 的机制来处理事件类型。

  • 某些事件将在地址空间中表示为对象,因此无论如何都必须为它们创建 ObjectType。

对于普通的 EventType,即在地址空间中不可见(作为节点)的事件的 EventType,使用抽象的 ObjectType。OPC UA 定义 BaseEventType;所有其他 EventType 都必须直接或间接地从它继承。在图 1.44 中,BaseEventType 及其变量以及一些示例 EventType 一起显示,以指出 EventType 的可能性。BaseEventType 使用特性直接在其下方公开其字段。大多数 EventType 都应如此。但是,允许使用数据变量(例如)公开复杂变量,并使用对象对变量进行分组,如示例 EventType MaintenanceType 中所示。事件的类型信息定义事件的类别,可用于过滤事件。可以为此目的引入没有额外 InstanceDeclarations 的 EventType。例如,客户端可以订阅所有 CriticalMaintenanceType 类型的事件。

分组事件的另一种机制是提供 EventNotifiers 的层次结构。ReferenceType HasNotifier 用于此目的。当 EventNotifier 引用另一个 EventNotifier 时,可以保证引用的 EventNotifier 公开的所有事件也由引用的 EventNotifier 公开。事件由 EventNotifiers 公开,但实际生成事件的事件源不一定是 EventNotifier。事件的源在事件的字段中公开。预计源通常作为地址空间中的节点公开,尽管这不是必需的。如果源作为节点公开,EventNotifier 可以使用 HasEventSource 引用引用这些节点。这完成了 EventNotifier 层次结构,如图 1.45 所示。

EventType 层次结构
EventNotifiers 的层次结构

有一个 EventNotifier,它在逻辑上引用了所有其他 EventNotifier,即Server对象。客户端可以订阅该对象以获取服务器的所有事件(除了绑定到视图的事件,如 ModelChangeEvents,请参阅第 2.11.3 节)。

最后,TypeDefinition 节点已经可以通过使用 GeneratesEvent 引用从 TypeDefinition 节点引用到 EventType 来公开其实例可能生成的事件类型。不能保证实例会生成引用的 EventType 的事件,但在大多数情况下这是可以预料到的。例如,可能有一个旨在生成Maintenance事件的设备,但具体硬件的构建方式是,只要系统正在运行,就不会发生这种情况。

需要公开 EventType 层次结构。不需要在 TypeDefinitions 中公开 EventNotifier 层次结构或 GeneratesEvent 引用。

EventType 被建模为 ObjectType,因此必须遵循 ObjectType 的规则(例如 InstanceDeclarations 的唯一 BrowsePath,或 InstanceDeclarations 的继承)。NodeClass Variable的 InstanceDeclarations 定义事件的字段。此处,与基于 ObjectType 的实例的组件适用相同的规则。如果整个 BrowsePath 具有Mandatory InstanceDeclarations,则必须在事件中提供该字段。如果它是Optional,则不必提供。

历史访问 #

OPC UA 处理历史有三个方面。首先是当前数据的历史记录。这可以回答以下问题:过去三天温度传感器的值是多少?这类似于 OPC HDA 中捕获的内容。其次是事件的历史记录。它回答以下问题:过去一小时内发生了什么事件?这在传统 OPC 中没有捕获。第三个方面是地址空间结构的历史记录。这可以回答以下问题:过去两周地址空间的结构发生了怎样的变化? 我们将在以下小节中简要介绍这三个方面。

历史数据 #

OPC UA 允许访问和更改变量值属性的历史记录。为此目的引入了特殊服务(以及用于访问事件相关历史记录,如下一节所述)。第 5 章描述了这些服务的工作原理。OPC UA 地址空间模型在变量节点上有三个不同的属性,用于处理值属性的历史记录。 AccessLevel 和 UserAccessLevel 指示历史记录是否可访问和可更改,前者一般表示,而后者则考虑当前连接用户的访问权限。这些属性指示某些历史记录是否可用,但不指示当前是否收集了历史记录。因此,使用 Historizing 属性来指示当前是否收集了历史记录。所有三个属性都以变量的粒度工作。该模型总结在图 1.46 中。除了这些变量之外,还可以公开(并允许操作)数据历史化的配置。这将在第 4.6 节中描述。

OPC UA 仅允许对变量的值属性进行历史化。如果服务器收集其他属性的历史记录并希望公开该属性并使其在地址空间中可访问,最合适的方法是为每个历史化属性创建一个特性,并将这些历史化属性添加到节点。

地址空间中的历史数据

历史事件 #

事件的历史记录可以从 EventNotifiers(即对象和视图)中获取。EventNotifier 属性指示是否可以访问和操作事件的历史记录。如何在 EventNotifier 中公开哪些事件被历史记录尚未标准化,因此也没有像变量那样的历史记录属性。但是,可以公开事件如何被历史记录的配置,并且可以包含该信息(有关事件历史记录的配置,请参阅第 4.6 节)。图 1.47 显示了事件历史记录的模型。

地址空间中的历史数据

历史地址空间 #

除了处理当前数据和事件的历史记录之外,第三个方面是处理地址空间中的更改。地址空间中的节点和引用可能会随时间推移而添加或删除。OPC UA 允许客户端跟踪这些更改并通过引用不同的时间点来访问地址空间的不同版本。请注意,这是许多服务器不支持的可选功能。

为了跟踪地址空间的更改,OPC UA 支持 NodeVersion 特性和 ModelChangeEvents。对于节点,服务器必须始终支持 ModelChangeEvents 和 NodeVersion,或者两者都不支持。NodeVersion 是节点上的特性,每次从节点添加或删除引用时都会更新。请注意,变量或变量类型与其数据类型的关系不是建模为引用,而是建模为属性,在这种情况下被视为引用。因此,数据类型属性的更改会导致 NodeVersion 和 ModelChangeEvent 的更改。 NodeVersion 是按节点提供的,也就是说,客户端可以缓存节点的引用,只要节点版本没有改变,他们就不需要重新浏览节点。

ModelChangeEvent 是在视图(或整个地址空间的服务器对象)的上下文中生成的,并允许在一个 ModelChangeEvent 中跟踪多个更改。对地址空间的变化感兴趣的客户端通常应该订阅 ModelChangeEvent;对小片段感兴趣的客户端可以查看单个 NodeVersion 的变化。图 1.48 举例说明了 NodeVersion 和 ModelChangeEvent 的处理。在左侧,您可以看到发生某些变化之前的地址空间。然后,添加和删除了一些引用,在右侧,您可以看到新的地址空间,包括更新的 NodeVersion 和生成的 ModelChangeEvent。OPC UA 提供 BaseModelChangeEvent 仅表示某些内容已更改,并提供包含更改的 GeneralModelChangeEvent。在图 1.48 中,第二种类型的事件也用于指示更改。

使用NodeVersion和ModelChangeEvent可以相对轻松地跟踪引用的更改。当服务器不提供此功能时,客户端获取有关引用更改的信息的唯一可能性是定期浏览或查询地址空间。

NodeVersion 和 ModelChangeEvent 跟踪地址空间的变化

为了访问不同版本的地址空间,浏览和查询服务允许指定它们想要访问的地址空间的某个版本或某个时间点。这只会影响查询和浏览,这意味着在地址空间的不同版本中管理的不是节点的具体属性值,而是引用,从而间接地确定特定节点是否可以在地址空间的具体版本中访问。这意味着客户端无法在地址空间的不同版本和地址空间的更改跟踪之间建立直接联系。当客户端访问旧版本的地址空间时,它们仍然只能读取节点的 NodeVersion 的当前值,而不是旧版本地址空间中有效的值。在图 1.49 中,以不同版本的地址空间为例。在左侧,您可以看到在 ViewVersion 1 中浏览视图时获得的引用,在右侧,您可以看到在浏览ViewVersion 2 时获得的引用。一旦视图的内容发生变化,就必须增加 ViewVersion。但与 ModelChangeEvents 一样,只需增加一次 ViewVersion 即可捕获多个更改。ViewVersion特性与 NodeVersion 特性不同。在这里,还会跟踪视图内容的更改,这些更改不会影响直接连接到视图节点的引用。例如,当从视图中删除Maintenance对象到 Var3 的引用时,必须增加ViewVersion,尽管视图节点的NodeVersion特性不会改变。

地址空间的不同版本

有关如何访问和操作当前数据和事件的历史记录的详细信息,请参见第 5 章。如何访问配置请参见第 4.6 节。本节中描述的如何处理属性的规则之前已经捕获过。UserAccessLevel 必须是 AccessLevel 的子集,并且只有当 AccessLevel 定义对历史记录的访问时,才能设置 Historizing。节点只能提供历史记录,而不提供当前数据或当前事件。

没有标准方法定义在事件历史记录中提供哪些事件。它可以是事件订阅可访问的事件的任何子集,并且由于可以操作事件的历史记录,因此它还可以提供其他事件。为 HasNotifier 定义的规则不适用于事件的历史记录。

如果提供了 NodeVersion,则必须生成 ModelChangeEvent,反之亦然。地址空间可能只为地址空间中的某些节点提供 NodeVersion,而不为其他节点提供。

地址空间模型和信息模型 #

本章介绍的概念为在 OPC UA 中建模数据奠定了基础。不同的 NodeClass 及其固定的属性集定义了 OPC UA 的元模型。除了 NodeClass 之外,元模型内部还使用了一些标准节点,因此也可以将其视为元模型的一部分。具体来说,这些是基本 ReferenceTypes(如 HasSubtype)和基本 TypeDefinitions(如 PropertyType),也是标准特性(如方法的输入和输出参数)。就 OPC UA 而言,元模型称为地址空间模型。

地址空间模型、信息模型和数据

OPC UA 信息模型使用地址空间模型的概念来定义其自己的特定于域的类型和约束以及定义明确的实例。最后,基于信息模型创建服务器的具体数据,如图 1.50 所示。

通常,服务器将支持多个信息模型,其中一些可能基于其他信息模型。OPC UA 规范已经定义了包含基本类型的基本信息模型。其中一些已经是 OPC UA 元模型的一部分,而其他部分是附加信息,例如,用作服务器地址空间的入口点或用于公开服务器的诊断信息。基于基本信息模型,可以派生出其他信息模型以用于特定于域的目的。最后,服务器可以扩展这些模型以定义一些特定于服务器的类型,从而定义由服务器提供的特定数据使用的服务器特定信息模型。这在图 1.51 中进行了示例说明。基本信息模型由拓扑和设备信息模型扩展。设备信息模型由包含特定于供应商的设备类型的特定于供应商的设备信息模型扩展。最后,特定于服务器的信息模型扩展了拓扑和特定于供应商的信息模型,并由服务器的实例使用。在特定于服务器的信息模型中,提供了一些预配置的设备类型和进入地址空间的入口点。第 4 章详细介绍了标准信息模型及其处理方法。

地址空间模型和几个信息模型

总结 #

关键信息 #

OPC UA 地址空间由节点和它们之间的引用组成。节点属于不同的 NodeClass,用于不同的目的。每个 NodeClass 都有一组固定的属性,而引用没有属性。

基本 NodeClass 是对象,用于构造地址空间并提供事件、变量以及方法(可以在服务器中执行)。使用这些简单的构造,您可以公开当前使用 Classic OPC 完成的所有数据。

为对象和变量创建详细的 TypeDefinitions 允许在 OPC UA 服务器中提供更多信息。使用实例作为 InstanceDeclarations,可以定义复杂的 TypeDefinition,并且可以使用这些 TypeDefinitions 的知识对 OPC UA 客户端进行编程。ReferenceTypes 允许为节点之间的引用指定语义。用户定义的 DataTypes 允许以所需的格式交换复杂数据。

视图可用于为不同任务组织 OPC UA 地址空间,仅提供特定任务所需的信息。

通过创建对象 EventNotifier,事件无缝集成到该模型中。 可以在提供实际数据的相同位置访问事件历史记录和当前数据。 OPC UA 允许使用 NodeVersion 特性和 ModelChangeEvents 跟踪地址空间的变化,并且可以使用 ViewVersion 访问地址空间的不同版本。

OPC UA 地址空间定义了 OPC UA 的元模型。 基本信息模型为创建针对特定领域的标准或特定于供应商的信息模型奠定了基础。

借助这些功能,OPC UA 允许提供简单的地址空间(如在传统 OPC 中所做的那样)以及具有丰富类型模型的地址空间,以揭示所提供数据的详细语义。

在哪里可以找到更多信息? #

地址空间模型在 [UA 第 3 部分] 中定义;地址空间中使用的基本类型在 [UA 第 5 部分] 中定义。在本书的附录 A 中,您可以找到本章中使用的符号的描述。在附录 B 中,给出了 NodeClass 及其属性的摘要。附录 C 提供了基本信息模型,包括地址空间模型一部分的 ReferenceTypes。

下一步是什么? #

在下一章中,我们将看一个示例,说明如何使用您在本章中刚刚学到的建模概念。之后,我们将描述一些如何使用这些建模概念的最佳实践。在第 4 章中,我们将介绍标准信息模型,包括基本信息模型和 OPC UA 规范提供的数据访问、程序、警报和条件等特定信息模型。在本章之后,您应该了解有关 OPC UA 中建模信息的所有内容,我们将研究 OPC UA 的服务,了解如何实际访问和操作 OPC UA 数据。

[1] 请注意,HasSubtype 引用的建模指向从超类型到子类型。它是一个分层引用,因此在该方向上公开类型层次结构是有意义的。然而,通常在 UML 等建模语言中,子类型指向其超类型。

[2] WriteMask 和 UserWriteMask 也可以使用;但是,由于对象和对象类型具有不同的属性,因此这里可能需要通过某些服务器内部逻辑进行更改。

[3] 请注意,用作 InstanceDeclarations 的所有对象和变量都是类型化的,尽管到目前为止显示的大多数示例中都没有公开类型。对用作实例的对象和变量有效的所有约束也对用作 InstanceDeclarations 的对象和变量有效。

[4] 没有明确规定谁必须删除节点。删除节点可以由服务器或客户端自动完成。但是,如果客户端请求仅删除 Address3,则服务器必须同时删除Street节点或拒绝该请求。

[5] 由于在使用多重继承时处理冲突的策略不同,因此提供一种特定的语义将会排除使用不同语义的模型的简单映射。

[6] 顺便说一句:Icon是 [UA 第 3 部分] 中定义的对象和对象类型的可选标准特性

[7] 有一个标准特性,称为 DataTypeVersion,指示信息是否已更改。当该特性的值未更改时,客户端可以使用其缓存版本。