> For the complete documentation index, see [llms.txt](https://freeioe.gitbook.io/doc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freeioe.gitbook.io/doc/guide/app_dev/easy/data_collection.md).

# 数据采集

***

## 采集应用

此章将介绍如何开发一个设备数据采集应用。您将学习到什么是数据采集应用，构建此类应用都需要实现什么功能。

### 什么是设备数据采集

设备数据采集应用是通过某种通讯协议，获取其他智能设备中的传感器数据，并将数据发布到 FreeIOE 软件内的应用。简单来说，就是负责设备通讯，并将数据收集上来的应用。

这类应用在 FreeIOE 软件内，是设备数据的生产者。数据上云以及边缘计算类应用都是这些数据的消费者。

![数据采集应用](/files/-Lv9wIKsxV0B5BOS7bx2)

### 创建设备模型

应用在发布设备数据前，需要创建设备模型。设备模型是其他应用去理解该设备的一个描述性说明结构。而应用调用 FreeIOE 的接口注册一个设备实例后，就可以通过这个实例进行数据发布等操作。

#### 设备模型

设备模型的描述包含

* 元信息
* 输入项
* 输出项
* 指令集

**元信息(meta)**

设备元信息包含以下内容:

1. name 设备名称(云平台唯一名称, 如，某某锅炉，某某电表
2. description 为设备产品的具体描述信息，如某某品牌三相交流智能电表
3. inst 为选填属性，如 1#锅炉，3#电表， 办公室空调一号等。 是标记设备的友好名称
4. series 为设备产品的系列号，如 S5500, S7-300
5. manufacturer 设备厂家信息（如：冬笋科技（北京）有限公司)
6. link 选填，设备详细信息的链接地址, 默认会是 `http://device.freeioe.org/devices?name=<meta.name>`

您可在元信息中附加更多的静态信息。请勿使用meta来标记一些动态信息，FreeIOE 在设备实例创建、修改、删除时，会将网关内所有设备信息全量同步到平台。滥用元信息会导致流量浪费和平台的负载增加。

类似元信息，输入项、输出项以及指令集都可以自定义静态属性。

**输入项(inputs)**

设备输入项是从实际传感器设备采集到的数据项，如A相电压、有功总功率等等。 也可以是计算出来的数据项，如当前4G数据发送总量、当日产量等等。只有输入项的数据、才可以进行数据发布。

```lua
local inputs = {}
table.insert(inputs, {
	name='Va',
	unit='V',
	desc='输出电压',
	vt='float',
})
```

1. name 输入项的名称，在本模型内不能重复，支持英文字母数字和下划线(\_)。
2. desc 输入项的描述，建议使用简洁的描述信息。
3. unit 输入项的数据的单位，是可选属性。
4. vt 输入项发布数据的数值类型。类型有：
   * float 双精度浮点类型
   * int 具体限制参考 FreeIOE 编译时定义的整数类型，通常为64位整数
   * string 字符串类型

> 数据发布时，FreeIOE 会自动进行数据的验证以及转换，请务必填写正确的数据类型，确保数据的一致性和严禁性。 FreeIOE 没有 boolean 类型，请使用 int 类型来模拟。通常我们建议 0 代表 false, 1 代表 true

**输出项(outputs)**

当设备具备的对外输出的能力，如模拟量输出、开关量输出、可调节输出电压等等。在模型中声明这些输出项，用户就可从而平台可以进行设备数据下置。

```lua
local outputs = {}
table.insert(outputs, {
	name='V_output1',
	unit='V',
	desc='输出电压',
})
```

> 如同输入项，指令集的名称也有在设备模型内唯一性的要求。 通常用户通过平台下置的数据数值为字符串，应用需要进行数据验证和合理的类型转换。

**指令集(commands)**

当设备支持的执行一些动作指令，如开启，停止，暂停，可以在设备模型中声明这些动作指令。 从而用户可以从平台发送设备指令请求。

```lua
local commands = {}
table.insert(commands, {
	name='start',
	desc='启动设备（无需参数)',
})
```

> 如同输入项，指令集的名称也有在设备模型内唯一性的要求。 平台在下发指令时附带一个param的数据结构，在lua里面是一个table的对象。 如同输出项下置一样，应用需要正确处理传入的参数的数据验证和合理的数据类型转换。

### 设备序列号

每一个具体的设备实例在平台内，都需要一个唯一的设备序列号。如果智能设备没有自己的唯一序列号，您可以使用基础模块的gen\_sn函数来生成。

```lua
local dev = self._api:add_device(sn, meta, inputs, outputs, commands)
```

### 发布设备数据

#### 发布数据

当应用通过设备通讯协议，收到设备数据项的新的数值后，请使用下面接口进行设备数据发布:

```lua
dev:set_input_prop('Va', "value", val, now, 0)
```

其中： val 为协议中解析出来的具体数据，其数据类型请和模型声明中的数据类型(vt)保持一致 now 是数据的时间戳，如协议中没有时间戳（如 modbus），请使用 FreeIOE 提供的系统时间

#### 发布紧急数据

设备紧急数据是一种需要 FreeIOE 尽快将之传输至平台的设备数据。从而平台能够将此数据尽快的推送给用户，让用户尽快执行某种策略。

发布紧急数据的调用:

```lua
dev:set_input_prop_emergency('Va', "value", val, now, 0)
```

> 紧急数据将绕过一系列的上传检测/打包等操作，请谨慎使用。在冬笋平台下，此种数据会优先/即时传输至平台，保证数据的时效性。 请勿滥用此紧急数据接口，因为此接口会同步发布正常的数据，即一次数据会被传输两次（不同通道），滥用此接口会导致平台压力增加和流量使用的增加。

### 发布通讯报文

当应用支持用户在平台查看通讯报文，并根据报文诊断现场设备通讯时，可以使用此通讯报文发布接口。

```lua
dev:dump_comm('IN', msg)
```

其中第一个参数通常为方向性指示标志，如 IN OUT 等。 FreeIOE 并未限制这个参数，但是也请勿使用过长的描述性信息。

### 发布通讯统计

设备统计信息，如连接状态，通讯接收包数，通讯发送包数，通讯接收字节数，通讯发送字节数等等。

统计类型：

* status
* success\_ratio
* error\_ratio
* packets\_in
* packets\_out
* packets\_error
* bytes\_in
* bytes\_out
* bytes\_error

```lua
stat:inc('packets_in', 1)
```

### 输出应用日志

应用应该具备必要的日志信息，能够帮助客户理解应用的工作状态，以及获取错误、警告等信息。

参考[日志接口](/doc/reference/app/logger.md)

```lua
self._log:trace("read input registers done!")
self._log:warning("Got err:", err, "more", "log content", here)
```

### 响应数据下置

当平台的设备输出请求到达网关后，此数据输出处理函数就会被调用。使用 app.base 作为应用基础类时，需要声明：

```lua
--- 处理设备输出项的数据下置请求
-- @param app_src 请求来源应用的实例名
-- @param sn 目标设备序列号
-- @param output 输出项名称
-- @param prop 输出项属性(默认为value)
-- @param value 输出数值
-- @param timestamp 数据源的时间戳，选项数据，可用于校验下置数据是否超过了时效。
function app:on_output(app_src, sn, output, prop, value, timestamp)
	-- 设备数据下置代码实现
end
```

本函数需要正确返回执行结果：执行成功返回true, 失败返回false, \<error>。此函数可以进行sleep, yield操作。

### 响应设备指令

当平台的设备指令请求到达网关后，此指令处理函数就会被调用。使用 app.base 作为应用基础类事，需要声明：

```lua
--- 处理设备指令执行请求
-- @param app_src 请求来源应用的实例名
-- @param sn 目标设备序列号
-- @param command 指令名称
-- @param param 参数(可以是数值，字符串，table)
function app:on_command(app_src, sn, command, param)
	-- 设备指令执行代码实现
end,
```

本函数如同on\_output一样，需要正确返回执行结果。 也可以进行sleep, yield等操作。

### 发布设备事件

当设备/应用出现异常，或者任何其他需要通知用户的事件信息，可以通过设备的事件接口发布。

```lua
local event = require 'app.event'
local info = "/tmp disk is nearly full!!!"
dev:fire_event(event.LEVEL_ERROR, event.EVENT_SYS, info, {used=10000000, free=0})
```

注意请勿频繁调用此接口，建议记录事件发布的时间，确保应用不会无限制的循环发布事件。虽然FreeIOE内置了事件发布的次数限制（20条/分钟)，但是强烈建议应用设定事件发布限制。

### 更多内容

本章我们并没有设计到应用跟设备通讯部分，这部分内容因为跟具体的通讯协议相关，请自行参考应用示例库中一些应用的处理方式。

关于链路通讯可以阅读[设备通讯](/doc/guide/app_dev/easy/dev_connection.md)

### 示例代码

```lua
local app_base = require 'app.base'

local app = app_base:subclass('THIS_IS_AN_SAMPLE_SUB_APP')
app.static.API_VER = 6 -- 可不设置

---
-- 应用对象初始化回调函数
-- @param name: 应用本地安装名称。 如modbus_com_1
-- @param sys: 系统sys接口对象。参考API文档中的sys接口说明
-- @param conf: 应用配置参数。由安装配置中的json数据转换出来的数据对象
function app:on_init()
	--- 设备实例
	self._devs = {}
	self._log:debug("XXXX Application initialized")
end

--- 应用启动函数
function app:on_start()
	--- 生成设备模型唯一序列号
	local sys_id = self._sys:id()
	local sn = self:gen_sn('example_device_serial_number')

	--- 创建设备模型
	local inputs = {
		{name="tag1", desc="tag1 desc", unit="KV", vt="float"}
	}
	local meta = self._api:default_meta()
	meta.name = "Example Device"
	meta.description = "Example Device Meta"
	--- 生成设备模型对象
	local dev = self._api:add_device(sn, meta, inputs)
	self._dev = dev
	--- 生成设备通讯口统计对象
	local stat = dev:stat('port')
	self._stat = stat

	return true
end

--- 应用退出函数
function app:on_close(reason)
    -- 处理通讯链路关闭等
end

--- 应用运行入口
function app:on_run(tms)
	local dev = self._dev
	local stat = self._stat
	dev:dump_comm("IN", "XXXXXXXXXXXX")
	dev:set_input_prop('tag1', "value", math.random())
	stat:inc('packets_in', 1)

	return 10000 --单位是ms, 10000代表下一采集间隔为10秒。 等同于sleep(10000)
end

--- 返回应用对象(标准模块做法)
return app
```
