工业物联网的点对点应用原型

面对工业物联网广阔的应用空间,点对点(机器对机器)架构的编程是一个挑战。统一的编程环境为通过网络发布和订阅感兴趣的数据提供了一种直观的方法。

通过Robert A. Dolin和Bernd Gauweiler 2014年4月23日

工业物联网(IIoT)有着非常广阔的应用空间。所设想的应用程序可以分为三个主要体系结构:一个客户端/服务器架构,其中人通过设备(如移动电话)与互联网交互;一个机器对机器(M2M)客户端/服务器架构,其中远程设备向企业系统提供数据以进行分析和决策支持(如车辆跟踪系统);以及点对点(机器对机器)架构,其中设备自主协作并做出本地决策(仅在例外情况下或定期连接到企业系统以发布警报或重要数据)。这些点对点应用程序通常对机器或进程执行本地控制,以响应本地数据。这类具有编程挑战性的应用程序很难原型化。

组成Internet协议套件的各种协议都非常适合客户机/服务器体系结构,因为今天几乎所有的Internet通信都遵循这种体系结构模型。我们感兴趣的一个领域[1],[2]是在互联网协议(IP)的UDP传输层之上应用点对点架构及其所需的服务。Python环境有一种简单直观的方法来表达分布式函数,这些函数通过网络发布数据和订阅感兴趣的数据。该环境对应用程序隐藏了通信方面,使应用程序的点对点、分布式特性隐式化。应用程序只需要关心它所操作的数据——就像非通信应用程序一样。可以在很短的时间内对符合此体系结构的各种应用程序进行原型化。

1 . Python环境

一个简单的类库可以将Python脚本转换为IIoT设备,并将本地操作的控制应用程序提升为基于ip的联网解决方案。这个库是为了便于使用而设计的,集中在一个单一的主类上。此应用程序类及其子类通常通过在初始化时计算有用的默认设置来进行自供应。应用程序编程接口(api)可用于微调配置参数,以便在必要时满足特定应用程序的需求。设计周期以python为中心,通常不需要任何其他专业工具。网络上节点之间的信息通过发布者/订阅者数据模型进行交换,其中交换的单位是数据点。数据点是具有语义的值。(详情见下文。)

Python脚本定义了与网络接口的数据点:输入数据点获取从网络接收的数据,例如温度设定值。输出数据点将数据传输到网络,例如当前恒温器的位置。输入和输出数据点实现数据点类型,支持不同的物理或抽象数据,如温度值或报警条件。

附加配置数据随属性一起提供。属性被实现为具有持久值存储的输入数据点。属性在简单数据点上展开,以包含特定的语义:最高报警温度阈值、温度报警滞后或类似的描述。

输入和输出数据点和属性通常被分组到概要文件中。概要文件是一组数据点,它们构成了服务的网络可见接口,并结合了定义良好的行为。在讨论的示例中,服务是一个二氧化碳传感器。该服务的作用是公布空气中存在的二氧化碳量。此发布的频率由配置文件中嵌入的属性决定。Python库允许在一行代码中通过块对象实现完整的配置文件,包括所有相关的数据点和属性:

co2Sensor = application.block (co2Sensor ())

数据点、属性和配置文件的定义可以支持和促进具有多个供应商提供的设备的解决方案。基于现有的标准[6]和丰富的类型和概要定义集合[7],兼容的设备可以相互识别,并在不同的供应商或硬件解决方案之间交换数据。

2通用数据模型

点对点控制网络必须处理以不确定时间表到达的数据,这些时间表表示物理、逻辑或抽象实体的多样性。例如,一个数据项可能表示以摄氏为单位的温度值,以大端字节顺序的IEEE 754双精度浮点值编码。另一个数据项可能使用范围从0到999的自然数表示袋子中的豆子数量,以无符号16位小端标量编码,而其他数据可能通过合适的数据结构表示照明场景的复杂定义,或者使用UTF-16 unicode表示人类可读的字母数字形式的邮政地址。

在表示已知实体(如温度值)的数据中,并使用已知编码(如大端位数格式的IEEE 754双精度浮点值),控制网络还必须识别语义:室温设定值的有效值范围可能在15到28摄氏度之间,并应用于驱动恒温散热器阀的控制算法,而另一个以相同方式编码的温度值可能表示火灾探测器的滞后,或定义室温设定值的较低有效值。

然后必须对数据进行条件处理,以便本地应用程序可以对数据进行操作。例如,应用程序可能需要在接收到8字节浮点值时旋转字节顺序,并将结果解释为双精度浮点值。

当算法产生输出数据时,该数据也可能需要条件调节。例如,可以报告当前的温度值。这可以作为一个大端16位无符号整数值在网络上交换,实现一个从0到655.35的定点变量,分辨率为0.01 c。这种编程模型为这些数据设定条件,使应用程序以其自然形式呈现数据;本例中的浮点值。该模型自动应用范围和分辨率限制,并且透明地处理该数据的应用程序表示和网络表示之间的转码。

控制网络中的对等体必须就每个数据项所描述的实体、其编码和语义达成一致。其他方面(如明确的范围限制、定义良好的默认值或发生故障时的回退行为)也通常在对等体之间达成一致。

为了达成这样的协议,控制网络中的对等体要么携带所有必要的元数据,以使接收方能够完全理解他们可能遇到的任何数据,要么确保数据只到达具有先前建立的合同和信任级别的接收方。

这给设备开发人员和将多个控制设备集成到一个分布式的点对点控制网络中的人员带来了很大的困难。设备开发人员通常需要深入了解所涉及的流程、协议和过程,以便编写正确地调整输出数据并正确识别、分类和解释传入数据的代码。将多个设备集成到一个这样的网络中的人员需要了解每个设备的属性和功能,以便仅将那些“说同一种语言”的设备组合在一起。例如,一个通用的开关可能能够控制一个通用的灯,但是一个工业洗衣机可能需要一个比通用开关所能提供的更复杂的“开/关”信号。

此编程模型包括自动评估元数据并将其应用于数据解释和数据调节过程的方法和服务,仅将应用程序与适当的条件数据连接起来。这些方法通过提供透明的综合数据调理服务,大大改进了现有技术。

图1说明了该编程模型提供的数据调节服务和网络数据流。

3数据处理

点对点网络由许多独立运行的设备组成,通常在需要知道的基础上共享数据。例如,恒温散热器阀可能需要接收来自其他设备的当前室温设定值。同样的恒温散热器可能不需要知道设定值何时或为什么改变,它通常也不需要知道天花板灯或花园洒水器的任何控制操作。

也就是说,一个新的温度设定值可以从一个输入设备传送到同一房间的所有恒温散热器阀。同时,其他房间的恒温散热器阀可能从其他来源接收其他设定值,而花园洒水器在同一网络上作为逻辑上独立的单元进行控制。

为了以这种方式促进点对点通信,传输数据的设备(数据发布者)需要知道如何到达适用的目标(数据订阅者),并且可能需要知道是否以及如何接受或拒绝传入的数据。这种互知包括源地址和/或目的地址的知识、从接收节点上定义的多个数据点中选择正确数据点的信息、路由详细信息和传输属性(例如服务类型、检测中断或启用重传的时间以及类似的控制数据)。

此编程模型包括抽象服务,以促进应用程序脚本条款中的合同提供和接受,在此模型的实现中隐藏所需的低级细节。这简化了分布式控制应用程序的开发。通过支持内部契约管理服务,这种编程模型还大大减少了从分布式应用程序组成网络时对专用集成步骤的需求,并且在许多情况下消除了对专用集成步骤的需求。

图2显示了一个简单的控制网络,其中包含四个不同的设备,每个设备提供一个应用程序。该图说明了建立和维护这种契约所需的一组低级寻址细节,以及应用程序专业领域。

四、设备寻址

点对点控制网络实现基于契约的分布式算法,并建立了关于数据及其语义的信任,以及寻址和传输属性,以便对等点A可以通过网络到达对等点B。

但是,每个参与设备也必须获得自己唯一的标识:物理设备A必须具有逻辑标识A,物理设备B必须具有逻辑标识B,才能使数据从A流向B。

在某些系统中,逻辑标识与物理标识相同:设备之间通过唯一的物理方面(如唯一的MAC-ID地址)来寻址。在大多数网络中,逻辑寻址的抽象是首选,因为它可以在不通知其他节点替换物理地址的情况下进行设备替换,效率更高,并且支持由多链路组成的网络拓扑。

使用逻辑寻址的系统必须为每个设备获取并应用一个合适的、明确的逻辑地址。在任何一个系统中,设备都需要使用所选的寻址形式来相互寻址。

该编程模型包括用于自动获取和维护明确的逻辑设备地址数据的服务,通常不需要专用的中央机器或人工参与。这简化了设备的安装;这些服务与这个编程模型的集成使得开发人员可以轻松地创建这样的设备。

五、规划模型

Python编程语言[3]提供了一个快速的开发环境,支持用解释性语言进行面向对象的开发,集成了字节码编译和可选的本地二进制代码编译[4],[5]。

使用Python为分布式点对点控制网络创建应用程序,可以为应用程序领域使用流行的、快速上市的应用程序开发,而应用程序领域目前由传统编程语言和策略主导,因此通常需要更长的开发周期。

编程模型只需要Python编程语言,因为不再需要专有的工具和语言,同时提供一种方法,通过标准的Python语言特性,继续使用现有的代码。

六、合同履行

为开放的、多供应商的点对点控制网络实现应用程序需要声明应用程序的接口,并将应用程序的中央控制算法与该接口集成。

接口定义了给定应用程序如何与网络上的对等程序通信。这样的接口通常由一个或多个概要文件的实现组成,从一个开放的行业标准中定义的概要文件库中选择。[6]

在传统技术中,实现接口主要由使用接口定义工具和语言提供正确定义的手动和知识密集型过程组成。下面,示例继续,查看简单标准二氧化碳传感器配置文件的假设应用程序接口的定义和实现。这个简单的标准概要文件将一个用于当前二氧化碳水平的输出数据点与三个用于提供将二氧化碳水平发送到订阅者节点的频率和条件的强制性数据点结合在一起。

图4演示了如何基于图3中概述的类实现特定的配置文件,使用标准二氧化碳传感器配置文件SFPTco2Sensor[7]作为示例。

Python编程模型包括所有标准配置文件、数据点和属性类型,统称为资源。总共有超过800种标准的、预定义的资源[6]可供Python应用程序使用。另外,特定于应用程序的资源可以在相同的Python编程模型中定义。应用程序在必要时导入这些Python资源,以将不必要加载的资源消耗降至最低。

图3显示了使用该框架的应用程序可以实例化的几种资源类型的示例。典型的应用程序使用Application类的block()工厂方法来实现所需的对象。Application类是这个框架提供的单例类,它提供了所有顶级功能和服务。

下面的八行Python代码实现了一个完整且功能齐全的应用程序,包括一个标准二氧化碳传感器配置文件的正确实现。不包括应用程序算法,该算法将通过外设输入对物理CO2传感器设备进行采样,并通过输出数据点报告电流值,受配置属性值中表示的阈值和时间约束的约束。此代码示例中定义的编程属性为应用程序定义了一个强制键,该键用于在网络中标识应用程序。

从izot.device.application导入应用
从izot.resources.profiles。导入co2Sensor

app = Application()
app.programId = "9F:FF:FF:00:00:00:01"
co2Sensor = app.block(co2Sensor())

app.start ()
而真正的:
app.service ()

图4还使用相同标准二氧化碳传感器配置文件的示例说明了块工厂的操作。因为协议栈的API是单线程的,所以Application对象允许异步线程以几种方式发送事件信号或请求活动:

控制网络协议栈通过应用程序类捕获并转换为服务信号的异步事件发出事件可用性的信号。这允许对协议栈引发的事件进行同步处理。这些事件包括通知新到达的网络数据,或通知完成事件(先前启动的事务的失败或成功)。

在使用并发处理的应用程序中,一个主处理上下文(例如线程)实例化与此模型一起提供的Application类。同一应用程序的其他处理线程可能本质上调用单线程API。在这种情况下,API调用及其所有参数由发出此API调用的线程进入受保护的函数队列,并引发服务信号。以这种方式排队的API调用由Application类的方法自动处理,并且对应用程序和应用程序开发人员是透明的。

当堆栈指示给定数据点的新数据可用性时,这个原始应用程序数据将从堆栈中收集、格式化并按照图1所示的方式转发给应用程序。

完成事件通知触发相应的事件。

service方法通过在主处理线程中执行所有排队的API调用来清空函数队列。

此编程模型支持在不同线程中进行并发处理的应用程序,以便与数据点对象及其值进行交互。每个数据点对象都支持标准的Python with语句。当代码在数据点对象的with语句中执行时,该对象被锁定,不能被其他线程访问。当with子句终止时,对象将被解锁并添加到受保护的待办事项队列中。服务信号也在这个时候发出。

服务方法通过对每个排队的数据点对象应用适当的操作来处理此待办事项列表,除非该对象被锁定。工作线程可以在with子句之外显式地锁定数据点对象,尽管通常不建议这样做,以防止死锁情况的风险。

在服务方法执行的另一个步骤中,在必要时为用于组织和维护网络的可互操作自安装协议(ISI)[2]提供服务。

服务例程还在有时间保护的轮询基础上刷新选定的动态数据点属性。时间保护的轮循算法按照创建数据点的顺序一个接一个地处理数据点,但是在每个服务调用中这样做所花费的时间不会超过可配置的(通常是很小的)时间。当下一次服务调用发生时,此处理将在前一个周期停止的地方继续。以这种方式处理完所有数据点后,继续处理第一个数据点。

与数据点相关的一些属性可能会由于合同的建立或维护,或由另一方执行的其他网络管理操作而发生变化。例如,网络管理工具可以连接一个输入数据点和一个输出数据点。此过程管理许多低级属性(其中一些如图2所示)。本地应用程序可能不知道这些更改。本地应用程序通常不需要了解这些更改,但是某些应用程序经常检查某些方面。

为了报告一个数据点是否连接或绑定到至少一个其他数据点,此服务将is_bound数据点属性的当前状态从堆栈中继到相应的数据点对象,该对象通过其布尔is_bound属性公开它。例如,应用程序可以使用它来启用计时器来监视更新的输入数据的及时到达,并且可以为当前未连接的输入数据点禁用该计时器。

7数据解释和条件处理

对于跨网络的传输,数据通常以适合给定技术的形式表示。例如,基于xml的解决方案使用合适的编码(如UTF-8)将数据交换为人类可读的文本信息。其他系统支持二进制数据交换以提高效率,其中数据根据网络技术定义的规则进行格式化。例如,一种网络技术可能要求使用小端字节顺序交换由一个以上字节组成的所有数字数据。

特定的数据类型通常会规定额外的规则。例如,一种网络数据类型可以定义为以0.01摄氏度为单位的温度值,或者以0.1秒为单位的持续时间,两者都使用无符号16位整数量交付。

一旦接收方应用了特定于网络的规则(例如字节和位排序),生成的原始应用程序数据就受这些附加规则的约束,这些规则通常作为元数据的一部分提供,或者与管理此特定数据项接收的合同一起提供。其他类型实现肤浅的值范围限制,以确保网络数据与相关物理实体之间的匹配。例如,由IEEE 754双精度浮点值表示的温度值可能受到限制,使温度值不能低于零开尔文。

使用传统方法,通过应用比例因子、偏移量和范围限制将原始应用数据转换为可用于计算和其他算法的代数数据及其反转的负担留给了开发人员。

用于分布式点对点控制的Python编程模型内置了对此类元数据的访问,并在将传入数据呈现给应用程序时自动应用所需的转换,以及在调整传出数据以传输到网络时自动应用所需的转换。

图1说明了这个编程框架如何将这些服务作为完全透明的服务提供给应用程序。

在这个Python编程模型中,应用程序开发人员不需要关心数据调节任务,因为编程模型会自动应用适当的规则。与传统方法相比,这使得该技术更容易学习,更容易接近,并显著减少了开发时间、员工培训投入的时间和应用程序中的错误风险。

8设备寻址

大多数控制网络支持外部实体来分配和分配唯一的逻辑设备地址。该模型可选地支持这样一个中央仲裁器,但默认为[2]中详细介绍的可互操作自安装协议的实现。

ISI协议以及对它的支持内置在这个编程模型中,自动为每个参与设备分配和维护唯一的设备地址。

传统的编程模型需要大量的代码来利用中央仲裁器提供的服务。实现动态主机配置协议(DHCP)客户机的代码就是一个例子。

在这个编程模型中,不需要任何代码来完成相同的工作。核心ISI服务由Application类及其底层服务提供者自动且完全透明地处理。这些包括启动、停止和定期维护ISI引擎;可靠地维护ISI实施所需的非易失性、可修改数据的持久数据存储;并将低级协议细节抽象到应用程序专业领域(图2)。

9合同成立

为了在点对点网络中以有用的方式交换数据,数据交换通常基于元数据的传输,或先前商定的合同,或两者的组合。

在该模型中,ISI协议[2]用于创建、管理和删除这些合约。虽然ISI协议提供了这样做的标准机制,但通常需要对应用程序和底层网络技术有深入的了解。使用传统的编程模型,应用程序开发人员必须提供提供给其他人的契约的定义,必须定义同级提供的哪些契约是可以接受的,并且必须以相当低级的术语实现契约以及数据连接契约和本地应用程序之间的映射。

有了这个编程模型,应用程序开发人员可以使用提供的抽象来关注契约和契约接受条款的表达式。这减少了所需的培训水平、开发时间和误差范围。使用此模型的开发人员不需要提供对数据点索引或数字数据类型标识符的低级、特定于协议的引用。相反,开发人员可以用应用程序本身的语言表达预期的契约,例如,通过引用应用程序中实现的一组数据点。这样的声明只需要三行格式化的源代码。

要接受对等方提供的合约,应用程序必须确定所提供的合约是否可接受,然后指示ISI协议如何将该合约应用于本地数据点。在传统方法中,这是使用底层通信协议的低级方面进行通信的,因此需要对这些方面有深入的了解。

在此模型中,使用更抽象的模型来表示本地数据点项的契约应用规则,从而允许应用程序开发人员用应用程序本身的语言来表示这些规则。例如,应用程序可以引用在应用程序中实现的一组数据点,而不是低级实现细节。

与概要文件类似,ISI协议支持程序集的概念。程序集是一种特定于应用程序的分组数据点对象的方法,目的是建立和维护数据交换契约。任何与ISI协议协商的数据交换合同都引用一个或多个完整的程序集;就这些契约服务而言,ISI程序集是一个原子实体。在遗留实现中,契约和程序集的细节通过一系列事件进行通信,每个事件一次请求一个低级细节,例如特定程序集的特定成员的全局数据点索引。虽然在资源严重受限的设备上使用这种实现非常节约资源,但它很难理解、实现和维护。

这个Python编程模型为ISI组装和契约服务提供了一个抽象,由assembly和Enrollment类管理。这些类允许应用程序开发人员通过应用程序已经可用的对象来表达程序集和契约的细节(在ISI中称为注册)。因此,应用程序开发人员可以用熟悉的语言表达这些细节,而不需要暴露于低级协议、资源和数据点属性(尽管这种低级访问是作为选项支持的)。

Assembly类维护一个数据点对象列表的列表,该列表在构造程序集对象时提供。

假设有四个数据点对象A1到A4,下面描述了带有assembly对象构造函数的示例程序集:

组装= izot.isi.Assembly (((A1), (A2、A3), (A4)))

代码示例使用程序集描述的规范化形式,即使用列表(或其他适当的容器)对对象进行分组,即使它们只包含一个项。在指定程序集时,应用程序开发人员不需要提供规范化表单,因为不需要将单个对象包装到容器中。下面的代码示例产生完全相同的程序集形状:

组装= izot.isi.Assembly (

A1,
(A2、A3),
A4

可以通过单独引用数据点来创建包含一个数据点的简单程序集。但是,检查assembly属性总是产生一个规范化的描述:

Trivial = izot.isi。大会(A)
print(trivial.assembly)# print((A,),)

X.超越多态API

在传统的编程语言中,例如C编程语言家族,数据类型通常是严格的。例如,当应用程序接口需要1…65535整数范围内的数字数据类型标识符时,必须提供这样一个整数值,或者必须提供一个可以转换为整数的值。

Python编程语言实现了一种不同的方法,称为duck typing。在duck类型中,通常不验证数据类型或将其转换为预期类型。相反,只要数据能够满足所需的操作,它就被接受。实际上,这通常意味着只要数据的行为与期望的数据(例如,整数)相似,就可以接受数据,即使它不一定是那种类型(整数)。

这允许本质上的多态编程。例如,在ANSI-C中实现的函数可能需要在数组中使用数字索引来寻址数组的一个元素。该索引通常需要作为(无符号)整数数据提供。在duck类型中,任何数据类型都可以用于等效函数的参数,只要所提供的数据能够明确地指向数组中的一个元素。

这个Python编程模型支持duck类型原则,但使用运行时类型识别技术来提供额外的抽象层和自由度,从而大大减少了学习模型所需的时间和精力。

例如,ISI协议需要一个数字数据类型标识符来定义数据连接契约。在C编程模型中,需要一个(无符号)整数数据类型标识符来满足这一要求。在duck类型原则的规则下,任何可以在需要时产生这种类型标识符的数据类型都是受支持的。在这种编程模型中,应用程序可以提供数字(无符号)整数数据类型标识符,或者提供能够产生这种类型标识符的对象,例如数据类型或数据点对象。

应用程序通常处理数据点对象,并在初始化阶段处理数据类型对象。编程模型检测何时使用这些类型,并从这些对象中获取所需的信息,以便将它们用于数字类型标识符的位置。下面的代码摘录演示了在多态Python API上展开的实现,其中相关部分以粗体突出显示。所示的代码片段实现了Python ISI注册对象的type_id属性和相关的setter (__set_type_id函数)。

Def __set_type_id(self, v):
如果self.__created_from_raw:
提高AttributeError (
“此Enrollment对象是只读的”

if isinstance(v, interface. datpoint):
自我。__type_id = v.get_data_item()._key
if isinstance(v, base.DataType):
自我。__type_id = v._key
其他:
自我。__type_id = self。_in_range('数据点类型id ', v, 0, 255)

Type_id = property()
Lambda self: self.__type_id,
__set_type_id,
无,"""主数据点类型ID, any为零。

主数据点类型为key,如果未指定则为零。您也可以将数据点对象分配给此属性。该属性在分配时也接受一个Datapoint或DataType对象,但只维护并返回数字类型标识符。
”“”)

西Linux电脑免费下载

所描述的Python环境的完全可操作版本可从https://iiot.echelon.com/get-started下载。下载的源代码格式,是免费提供给学术和商业用途。要使用该环境,需要Linux计算机。(我们使用的是树莓派家族,但Beaglebone Black, Plug Computers家族的成员,以及其他潜在的产品都是类似的好选择。)

- Robert A. Dolin是Echelon的首席技术官和系统架构师。本文作者是Echelon公司的高级软件工程师,作者Mark T. Hoske是CFE Media的内容经理。控制工程mhoske@cfemedia.com

关键概念

  • 工业物联网具有广阔的应用空间。
  • 点对点(机器对机器或M2M)体系结构是一类具有挑战性的应用程序。
  • 统一的编程环境提供了一种直观的方法来表达分布式函数,这些函数通过网络发布数据和订阅感兴趣的数据。

考虑一下这个

工业物联网应用程序开发人员使用一个允许用一行代码实现完整配置文件的库,还能完成多少工作?

在线额外

这是一个控制工程可能发行数字版独家,包括额外的编程示例和信息上面和更多的信息和参考下面。

Robert A. Dolin,他是Echelon的首席技术官和系统架构师,自1989年以来一直在公司工作。他是15项Echelon专利的主要或共同发明人,并撰写了多篇关于设备网络和互联网的论文。他拥有加州大学伯克利分校(University of California at Berkeley)的电气工程和计算机科学学士学位。

Bernd Gauweiler是Echelon Corp.的高级软件工程师,在工业控制和自动化方面有很强的背景。20多年来,他一直专注于嵌入式系统和分布式应用程序编程语言和工具,重点是可互操作的多厂商系统。他拥有德国凯泽斯劳滕应用科学大学(University of Applied Sciences)电气工程和工业自动化专业的工程师学位(dip - ing,相当于学士学位)。

参考文献

[1]张志刚,“基于网络的信息感知与控制”。美国专利5,490,276,1996年2月6日发布

[2]高维乐等,“网络中设备的简单安装”。美国专利8,374,104,于2013年2月12日发布

[3] van Rossum等人,Python编程语言,https://python.org

[4] Bradshaw, Behnel, Seljebotn等人,Cython开源项目,https://cython.org

[5] Rigo等人,PyPy开源项目,https://pypy.org

[6] LonMark International,标准资源文档,https://types.lonmark.org

[7]二氧化碳传感器,中国,https://www.lonmark.org/technical_resources/guidelines/docs/profiles/1070_10.pdf