TCP客户端
驱动说明
该驱动每个模型可以配置一个服务器地址或为为每个设备单独配置一个服务器地址, 同一模型内服务器地址相同的设备将会使用同一个 TCP
连接(如果模型内所有设备都未配置服务器地址, 则该模型内的所有设备共用同一个连接).
驱动启动后, 每个模型都会主动与服务端建立连接, 然后接收服务端的数据.
- 当模型没有添加任何设备时, 该模型不会与服务器建立连接.
- 即使多个模型连接同一服务器, 也会为每个模型会创建一个独立的连接.
驱动脚本执行流程说明
以下处理流程以模型为单位, 多个模型之间独立执行, 互不影响.
- 当与服务器成功建立连接后, 会执行 连接处理脚本, 此时
state
参数的值为true
. - 当接收到服务端发送的数据时, 会读取所有的发送数据并存放到
Buffer
中, 然后调用 数据包拆分脚本. - 如果从已接收到的数据中拆分出完整的数据包,则将完整的数据包封装为
Buffer
交由 数据处理脚本. - 如果未从已接收到的数据中拆分出完整的数据包, 则根据
数据包拆分脚本
返回的数据决定是等待接收更多的数据或是丢弃部分数据. - 当连接断开时, 会执行
连接处理脚本
, 此时state
参数的值为false
.
客户端对象
客户端对象为当前 TCP
连接对象. 该对象除了提供与服务器通讯的功能外, 并且供一些常用的功能用于简化使用,提高效率.
在 数据包拆分脚本
, 数据处理脚本
, 指令处理脚本
和 连接处理脚本
函数的参数中提供了 client
对象参数, 在脚本中可以通过使用 client
中提供的函数完成数据的发送与采集.
该对象提供了以下函数:
与服务器交互
指令相关
媒体库(未实现)
- getMediaFile(path) 请求媒体库文件
- getMediaFileByURL(url) 请求媒体库文件
- uploadMediaFile(filename,catalog,action,data) 上传文件到媒体库
- deleteMediaFile(path) 删除媒体库文件
工作表
- saveWorkTableRow(tableId, rowData) 向工作表中写入数据
- updateWorkTableRow(tableId,query,rowData) 更新工作表数据(未实现)
- updateWorkTableRowById(tableId,rowId,rowData) 根据记录标识更新工作表数据
数据存储
- getContext(contextId) 获取数据上下文
- removeContext(contextId) 删除数据上下文
- getContextIds() 获取全部上下文标识
- getDeviceContext(deviceId) 获取设备数据上下文
- removeDeviceContext(deviceId) 删除设备上下文
- getDeviceContextIds() 获取全部设备上下文标识
向服务端发送数据
用于向服务端发送数据.
例如: 在接收到数据时向服务端发送 ack
信息或发送心跳数据.
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
data | Buffer | 发送的内容 | Buffer.from("hello") 代表要发送 "hello" |
返回值
string
或 undefined
.
如果参数不正确(字节数组为空或空数组)或发送失败则返回 string
内容为错误说明, 如果发送成功则返回 undefined
.
示例
function handler(client, buffer) {
// 向客户端发送 hello world
client.send(Buffer.from("hello world"));
}
向平台上报指令执行结果
上报指令执行结果.
有些场景指令的执行结果反馈是异步的, 在指令发送后, 过一段时间才会收到响应报文,
此时可以在接收到响应报文时通过 client.reportCommand(...)
方法上报指令执行结果.
通过该方法上报的结果信息可以在平台中 指令状态管理
页面中查看.
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
serialNo | String | 平台指令序号 | "f160a24b-1780-89e7-cd48-b4c0073bd0fe" |
table | String | 表标识 | "tcp_client_driver" |
deviceId | String | 设备标识 | "ST10001" |
state | String | 状态标识 | 自定义状态标识, 例如: Success, Failed 等 |
result | String | 结果数据 | 有些指令可能有响应数据, 可以将结果数据保存在 result 字段中. 例如: 读取设备配置指令, 会返回设备配置信息 |
serialNo
为平台在发送指令时生成, 会传入到指令处理脚本
中, 当指令执行结果为异步反馈时, 可以将serialNo
保存到设备上下文
中, 以便在后续reportCommand
使用.参数中的
table
来自于指令处理函数
的参数, 当需要通过reportCommand
上报自定义的命令执行结果信息时, 需要在指令处理函数
中保存相关参数信息以便后续使用.
返回值
string
或 undefined
. 如果返回 undefined
则表示发送成功, 否则返回数据为错误原因.
示例
// 模拟指令处理脚本, 假设指令执行结果为异步反馈
function handler(client, serialNo, table, deviceId, commmand) {
// 获取设备上下文
const deviceContext = client.getDeviceContext(deviceId);
// 将平台指令序号和表标识保存到设备上下文中, 以便后面使用
deviceContext.put(command.name, {"serialNo": serialNo, "table": table});
// 其它
}
// 数据处理脚本, 模拟收到指令执行结果
function handler(client, buffer) {
// 假设读取到的数据为指令执行结果
// 从数据中读取出设备标识
const deviceId = buffer.slice(4, 20).toString();
// 从数据中读取出指令标识
const commandName = buffer.slice(20, 30).toString();
// 批令执行成功标识
const state = buffer[30];
// 执行结果信息
const result = buffer.slice(31, 50).toString();
// 获取设备上下文
const deviceContext = client.getDeviceContext(deviceId);
// 从设备上下文中获取并删除指令信息
const command = deviceContext.getAndRemove(commandName);
// 发送指令执行结果
const sendResult = client.reportCommand(command.serialNo, command.table, deviceId, state == 0 ? 'SUCCESS' : 'FAILED', result);
if (!sendResult) {
console.log("批令发送失败:", sendResult);
}
}
请求媒体库文件-路径
请求媒体库文件
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
path | String | 文件路径 | 文件在媒体库的路径. 例如: /background/bg1.png 为目录 background 中的 bg1.png 文件 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
data | object | 文件信息 | 请求成功时该字段存在 |
name | string | 文件名 | 文件名. 例如: bg1.png |
size | number | 文件大小 | 单位: 字节 |
data | Buffer | 文件内容 | Buffer中包含文件内的全部数据 |
示例
function handler(client) {
const response = client.getMediaFile("/background/bg1.png");
if (!response.success) {
console.log("请求媒体库文件失败:", response.message);
return;
}
// 文件信息
const fileInfo = response.data;
// 文件名
const filename = fileInfo.name;
// 文件大小
const fileSize = fileInfo.size;
// 文件内容
const fileData = fileInfo.data;
}
请求媒体库文件-URL
请求媒体库文件
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
url | String | 文件 url | 文件在媒体库请求 url, 该信息一般由媒体库组件或附件组件获得. 例如: /core/fileServer/mediaLibrary/default/test/hello.txt |
url 中 default
为项目ID, test
为目录, hello.txt
为文件名.
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
data | object | 文件信息 | 请求成功时该字段存在 |
name | string | 文件名 | 文件名. 例如: bg1.png |
size | number | 文件大小 | 单位: 字节 |
data | Buffer | 文件内容 | Buffer中包含文件内的全部数据 |
示例
function handler(client) {
const response = client.getMediaFileByURL("/core/fileServer/mediaLibrary/default/test/hello.txt");
if (!response.success) {
console.log("请求媒体库文件失败:", response.message);
return;
}
// 文件信息
const fileInfo = response.data;
// 文件名
const filename = fileInfo.name;
// 文件大小
const fileSize = fileInfo.size;
// 文件内容
const fileData = fileInfo.data;
}
上传文件到媒体库
上传文件到媒体库
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
filename | String | 文件名 | 例如: hello.txt |
catalog | String | 目录 | 文件上传到的目录, 支持多级目录. 例如: image/background |
action | String | 存在同名文件时的动作 | cover : 覆盖已有文件.rename : 新上传文件名后面自动加1.append : 在文件尾部追加内容 |
data | Buffer | 文件内容 | 文件的内容 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 上传 hello.txt 到 data/note 目录, 文件内容为 "hello world"
const response = client.uploadMediaFile("hello.txt", "data/note", "cover", Buffer.from("hello world"));
if (!response.success) {
console.log("上传文件失败:", response.message);
return;
}
}
删除媒体库文件
删除媒体库文件
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
path | String | 文件路径 | 文件在媒体库的路径. 例如: /background/bg1.png 为目录 background 中的 bg1.png 文件 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 删除目录 background 中的 bg1.png 文件
const response = client.deleteMediaFile("/background/bg1.png");
if (!response.success) {
console.log("删除媒体库文件失败:", response.message);
return;
}
}
向工作表中写入数据
向工作表写入一条数据
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
tableId | String | 工作表标识 | 例如: student |
rowData | object | 写入数据 | 该字段为 JSON 对象, 内容根据工作表定义填写. 例如: {"id": "1","name": "小明", "age": 18} |
rowData
对象中必须包含 id
字段, 并且该字段做为数据的唯一标识, 必须唯一.
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
data | string | 记录ID | 请求成功时, 为新增记录的的ID |
示例
function handler(client) {
// 删除目录 background 中的 bg1.png 文件
const response = client.saveWorkTableRow("student", {"id": "1", "name": "小明", "age": 18});
if (!response.success) {
console.log("写入数据失败:", response.message);
return;
}
// 新增记录ID
const rowId = response.data;
}
更新工作表数据
更新工作表中的数据
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
tableId | String | 工作表标识 | 例如: student |
query | object | 过滤条件 | 该字段为 JSON 对象, 过滤出要更新哪些记录. 例如: {"name": "小明"} 更新 name 为 "小明" 的所有记录 |
rowData | object | 写入数据 | 该字段为 JSON 对象, 内容根据工作表定义填写. 例如: {"age": 19} 更新 age 字段的值为 19 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 更新 name 字段值为 "小明" 所有记录的 age 字段的值为 19
const response = client.updateWorkTableRow("student", {"name": "小明"}, {"age": 19});
if (!response.success) {
console.log("更新数据失败:", response.message);
return;
}
}
根据记录标识更新工作表数据
根据记录标识更新记录
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
table | String | 表标识 | 例如: "student" |
rowId | String | 记录ID | 例如: "1" |
rowData | object | 更新内容 | 该字段为 JSON 对象, 要更新的内容. 例如: {"name": "小明", "age": 18} |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
success | bool | 是否成功 | 请求是否成功. true: 成功, false: 失败 |
message | string | 信息 | 请求失败时为失败原因 |
示例
function handler(client) {
// 更新ID为 "1" 的记录, 更新 name 字段值为 "小明", age 字段的值为 18
const response = client.updateWorkTableRowById("student", "1", {"name": "小明", "age": 18});
if (!response.success) {
console.log("更新记录失败:", response.message);
return;
}
}
获取数据上下文
用于获取上下文对象, 可以在上下文中存储数据. 详细信息
第一次调用的时候才会创建上下文
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
contextId | String | 上下文标识 | "myContext" |
返回值
Object
. 数据上下文对象
示例
function handler(client) {
// 获取一个标识为 myContext 的上下文
var myContext = client.getContext("myContext");
}
删除数据上下文
删除上下文对象
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
contextId | String | 上下文标识 | "myContext" |
返回值
无
示例
function handler(client) {
// 删除标识为 myContext 的上下文
client.removeContext("myContext");
}
获取全部上下文标识
获取全部上下文标识(不包含设备上下文)
参数说明
无
返回值
String[]
示例
function handler(client) {
// 创建两个上下文
const context1 = client.getContext("context1");
const context2 = client.getContext("context2");
// 获取全部上下文标识 ["context1", "context2"]
const contextIds = client.getContextIds();
}
获取设备数据上下文
用于获取设备上下文对象, 使用设备标识作为上下文标识, 可以在上下文中存储数据. 详细信息
与 getContext
不同的是, 当设备被删除后, 重启驱动时会自动清理被删除设备的上下文对象.
第一次调用的时候才会创建上下文
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
deviceId | String | 设备标识 | "ST10001" |
返回值
Object
. 数据上下文对象
示例
function handler(client) {
// 获取设备 ST10001 的上下文
var context = client.getDeviceContext("ST10001");
}
删除设备上下文
删除设备上下文对象
参数说明
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
deviceId | String | 上下文标识 | "ST10001" |
返回值
无
示例
function handler(client) {
// 删除设备 ST10001 的上下文
client.removeDeviceContext("ST10001");
}
获取全部设备上下文标识
获取全部设备上下文标识(只包含设备上下文)
参数说明
无
返回值
String[]
示例
function handler(client) {
// 创建两个设备上下文
const context1 = client.getDeviceContext("ST10001");
const context2 = client.getDeviceContext("ST10002");
// 获取全部设备上下文标识 ["ST10001", "ST10002"]
const contextIds = client.getDeviceContextIds();
}
数据上下文
上下文对象, 用来存储数据, 上下文中的数据可以在不同的脚本中共享. 例如: 可以在 指令处理脚本中
写入数据,
然后从 数据处理脚本
中读取数据. 不同上下文彼此独立, 互不影响.
可以根据需求创建多个上下文对象, 但是上下文对象以及上下文中的数据需要及时清理, 否则会造成 OOM
问题,
导致驱动程序崩溃.
向上下文中保存数据
用于向上下文中存储数据. 如果 key
已存在则会覆盖已有数据.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
value | any | 数据项的值 |
返回值
无
示例
function handler(client, request) {
// 获取或创建标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 向上下文中存储一个数值
context.put("number", 3.141);
// 向上下文中存储一个对象
context.put("object", {name: "张三", age: 18});
// 获取或创建设备 ST10001 的上下文
const deviceContext = client.getDeviceContext("ST10001");
// 向上下文中存储一个字符串
deviceContext.put("string", "this is a string");
// 向上下文中存储一个数值
deviceContext.put("number", 3.141);
// 向上下文中存储一个对象
deviceContext.put("object", {name: "张三", age: 18});
}
判断上下文中是否包含指定数据
判断上下文中是否存在指定的 key
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
bool
. true
表示 key
存在, false
表示 key
不存在
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 返回 true
context.containsKey("string");
// 返回 false
context.containsKey("string1");
}
获取数据
从上下文中获取指定的 key
对应的数据. 如果 key
不存在则返回 undefined
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
any
或 undefined
. 返回 put
时写入的数据.
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 返回 "this is a string"
context.get("string");
// 返回 undefined
context.get("string1");
}
获取并删除数据
从上下文中获取指定的 key
对应的数据并且在返回后 删除 该 key
. 如果 key
不存在则返回 undefined
.
该函数返回后, 再使用 get
或 getAndRemove
均返回 undefined
.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
any
或 undefined
. 返回 put
时写入的数据.
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 返回 "this is a string"
context.getAndRemove("string");
// 返回 undefined
context.get("string");
// 返回 undefined
context.getAndRemove("string");
}
删除数据
从上下文中删除指定的 key
, 如果 key
不存在则不执行任何操作.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
key | string | 数据项的 key |
返回值
无
示例
function handler(client) {
// 获取标识为 myContext 的上下文
const context = client.getContext("myContext");
// 向上下文中存储一个字符串
context.put("string", "this is a string");
// 数据被删除
context.remove("string");
// 不执行任何操作
context.remove("string");
}
内置对象
crc 循环冗余校验
驱动中内置了 crc
对象, 可以在脚本中直接使用 crc
对象中的校验函数.
- checksum16(Buffer,Poly) CRC-16循环冗余校验
- checksum32(Buffer,Poly) CRC-32循环冗余校验
- checksum64(Buffer,Poly) CRC-64循环冗余校验
- checksumModbus(Buffer) CRC-Modbus循环冗余校验
CRC-16循环冗余校验
使用 checksum16(Buffer, Poly)
实现 16位
的循环冗余校验
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
data | Buffer | 被校验的数据 |
poly | int | 多项式 |
返回值
uint16
. 16位无符号整型
示例
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 多项式
const poly = 0x1234;
// 返回 uint16 的校验码
const checksum = crc.checksum16(data, poly);
}
CRC-32循环冗余校验
使用 checksum32(Buffer, Poly)
实现 32位
的循环冗余校验
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
data | Buffer | 被校验的数据 |
poly | int | 多项式 |
返回值
uint32
. 32位无符号整型
示例
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 多项式
const poly = 0xedb88320;
// 返回 uint32 的校验码
const checksum = crc.checksum32(data, poly);
}
CRC-64循环冗余校验
使用 checksum64(Buffer, Poly)
实现 64位
的循环校验码
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
data | Buffer | 被校验的数据 |
poly | int | 多项式 |
返回值
uint64
. 64位无符号整型
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 多项式
const poly = 0xD800000000000000;
// 返回 uint64 的校验码
const checksum = crc.checksum64(data, poly);
// 使用内置函数, 将校验码转换为字节数组(大端字节序)
const checksumBytes = bigEndian.encodeUint64(checksum);
}
CRC-Modbus循环冗余校验
使用 checksumModbus(Buffer)
实现 modbus
的循环校验码.
modbus
校验与 crc16
相似, 校验码类型为 uint16
. 另外, checksumModbus
不需要 poly
多项式参数.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
data | Buffer | 被校验的数据 |
返回值
uint16
. 16位无符号整型
function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 返回 uint16 的校验码
const checksum = crc.checksumModbus(data);
}
内置函数
数据包拆分函数
- createFixedLengthSplitFn(Length,ByteOrder) 固定长度头
- createDelimiterSplitFn(Delimiter) 固定分隔符
- createStartEndDelimiterSplitFn(StartChars,EndChars) 固定开始和结束符
内置数据包拆分函数仅能用于 数据拆包脚本 中
固定长度头
该函数会取前 N
个字节作为长度信息, 然后根据长度信息读取主体内容.
+--------+-----------+
| Length | Data |
+--------+-----------+
在数据包拆分脚本中直接使用内置的拆分函数, 如下所示:
// 创建长度头为 4 个字节, 长度头为小端字节序
// 第 1 个参数为长度占用的字节数量
// 第 2 个参数为长度头的字节序, 取值可以为 little(小端字节序) 或 big(大端字节序)
const handler = createFixedLengthSplitFn(4, "little");
固定分隔符
该函数会使用固定分隔符拆分数据包. 使用方式如下所示:
// 创建使用 "\r\n" 作为分隔符
const handler = createDelimiterSplitFn("\r\n");
数据包内容中不能包含分隔符
固定开始和结束符
该函数会读取指定开始符和结束符之间的数据做为一个完整的数据包. 使用方式如下所示:
// 创建以 '@' 开头并且以 '#' 结尾的数据包拆分函数
// 第 1 个参数为开始符
// 第 2 个参数为结束符
const handler = createStartEndDelimiterSplitFn("@", "#");
开始符
和 结束符
可以包含多个字符.
如果一个数据包中存在多个 开始符
或 结束符
时无法使用该内置函数.
例如: 使用 @
和 #
作为开始符和结束符时, 数据包主体部分不能包含 @
和 #
, 否则拆包结果不正确.
脚本说明
脚本语言: JavasScript ECMAScript 5.1
驱动使用时要求提供 数据包拆分脚本
, 数据处理脚本
, 连接处理脚本
和 指令处理脚本
脚本函数来处理接收和发送数据过程中的协议和数据格式问题.
在脚本的上下文中内置了 Buffer
包, 可用于处理接收或发送二进制数据.
除此之外, 还内置了 lodash
, crypto-js
, moment
, xml-js
和 formulajs(Excel函数)
包.
所有的脚本中的函数名必须为 handler
, 参数列表参考各脚本说明.
数据包拆分脚本
该脚本用于处理接收数据过程中的半包和粘包问题, 该函数需要根据数据格式判断接收到的字节数组中是否包含完整的数据包, 如果包含了完整数据包则返回完据包的起始位置和长度. 或者当数据包有错误时丢掉该数据包或有问题部分的数据.
如果字节数组中不包含完整的数据包, 直接返回 undefined
表示需要从客户端接收更多的数据.
函数定义如下:
/**
* 数据包拆分脚本, 从字节流中提取出完整的数据包, 或丢弃部分数据.
*
* @param {Object} client 客户端对象
* @param {Buffer} buffer 未处理的数据
* @return {Object}
*/
function handler(client, buffer) {
// 拆包逻辑
return {"package": {"start": 0, "length": 128, "next": 129}, "drop": 0};
}
驱动内置了常用的数据包拆分函数. 详情
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端对象 |
buffer | Buffer | 从客户端接收到的数据 |
返回值说明
- 当字节数组中包含完整数据包时, 返回包含
package
字段的对象. - 如果字节数组中存在错误, 则返回包含
drop
字段的对象, 丢弃掉错误的数据. - 如果字节数组中即不包含完整的数据包也不存在错误, 直接返回
undefined
, 表示需要从客户端接收更多的数据.
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
package | object | 数据包信息, 如果要丢弃数据该字段不需要返回 | {"start": 0, "length": 128, "next": 129} |
start | 数值 | 数据包在 data 中的起始位置 | 4 表示第 5 个字节为数据包的起始位置 |
length | 数值 | 数据包的长度, 表示从 start 后多少个字节为一个完整的数据包 | start=4, length=128 表示从第 5 个字节开始(包括第 5 个字节)连续 128 个字节为一个完整的数据包 |
next | 数值 | 下一个数据包的起始位置 | 例如: 在使用 \r\n 作为数据包间的固定分隔符时, 下一个数据包的起始位置应该为 start + length + 2 |
drop | 数值 | 要丢弃的字节数量, 即丢弃字节数组中 data[0:drop] 内容 | 128 表示丢弃前 128 个字节. 一般用于数据包错误时丢弃掉错误的数据包场景 |
正常接收数据
当数据包拆分脚本返回以下内容时, 表示已接收到的数据中包含了完整的数据包. 该数据包在所有数据中的起始位置为 0
,
长度为 128
, 下一个数据包的起始位置为 129
{
"package": {
"start": 0,
"length": 128,
"next": 129
}
}
例如, 使用 \r\n
作为数据包间的分隔符, 以解析该数据时 {"a":1,"b":2}\r\n{"a":3,"b":4}\r\n
,
拆分结果为 {"package":{"start":0,"length":13,"next":15}}
.
此时, 数据处理脚本
中接收到的数据为 {"a":1,"b":2}
, 并不会包含 \r\n
,
而下一次数据拆分脚本接收到的数据为 {"a":3,"b":4}\r\n
.
包含错误或不完整数据包时
返回以下内容时, 表示接收到的数据不完整, 需要丢弃 0 - 12 部分内容.
{
"drop": 12
}
例如, 接收到的内容为 :1,"b":2}\r\n{"a":3,"b":4}\r\n
时, :1,"b":2}\r\n
为不完整的数据包需要丢弃,
则需要返回 {"drop":11}
. 下一次数据拆分脚本接收到的数据为 {"a":3,"b":4}\r\n
不包含完整的数据包和错误包时
例如, 接收到的数据为 {"a":1,
未找到分隔符 \r\n
, 此时返回 undefined
表示需要接收更多的数据.
当客户端发送 "b":2}\r\n{"a":3,"b":4}\r\n
数据时, 会再次调用 数据包拆分脚本
, 此时 Buffer
中的数据为 {"a":1,"b":2}\r\n{"a":3,"b":4}\r\n
, 此时可以提取出完整的数据包.
注: 如果返回结果中同时包含了
drop
和package
字段并且drop
的值大于 0 时, 只做丢弃数据处理.
示例
// 校验数据包
function check(data) {
return true;
}
// 使用固定分隔符进行拆包实例逻辑
function handler(client, buffer) {
// 查找分隔符 \r\n
const index = buffer.indexOf("\r\n", 0);
// 如果未找到分隔符, 表示当前字节数组中不包含完整的数据包, 直接返回 undefined
if (index < 0) {
return undefined;
}
// 从 buffer 复制出完整数据包
const pkg = Buffer.alloc(index);
buffer.copy(pkg, 0, 0, index);
// 数据包校验, 如果数据错误可直接返回 drop
if (!check(pkg)) {
return {"drop": index + 2};
}
return {"package": {"start": 0, "length": index, "next": index + 2}};
}
数据处理脚本
该脚本用于处理 数据包拆分脚本
函数解析得到的完整数据包, 根据协议和数据格式将数据包解析为平台定义的数据格式.
函数定义如下:
/**
* 数据处理脚本, 解析从客户端接收到的数据并转换为平台规定的数据格式
*
* @param {Object} client 客户端对象
* @param {Buffer} 由 '数据包拆分脚本' 拆分得到的完整数据包
* @return {Array} 解析出的采集数据信息
*/
function handler(client, buffer) {
// 数据包处理逻辑
// 如果返回空数组或 undefined, 则表示未解析出任何有效数据
// 返回结果必须为数组, 数组中每个元素为一个设备的实时数据信息
return [
{
"id": "d01", // 设备标识
"time": 1665999863637, // 数据采集时间(ms), unix 时间戳
"values": {"key1": "str", "key2": 123} // 数据点, key 为数据点的标识, value: 为数据点的值
}
];
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端连接对象 |
buffer | Buffer | 由 splitHandler 函数得到的数据包 |
返回值
参数名 | 参数类型 | 参数说明 | 示例值 |
---|---|---|---|
array[object] | 对象数组 | 返回值 | [{"id":"d01","time":1665999863637,"values":{"temperature":17.5,"humidity":35.7}}] |
id | 字符串 | 资产编号或设备标识 | d01 |
time | 数值 | 时间戳(ms) | 1664256913000 |
fields | 对象 | 数据点信息 | {"temperature":17.5,"humidity":35.7} |
key | 字符串 | 数据点标识 | "temperature" |
value | any | 数据点的值 | 17.5 |
注: 返回值必须为
Object[]
或undefined
其中之一. 返回空数组表示未从接收到的数据中解析出有效数据,undefined
表示无返回结果, 驱动程序无须处理返回结果. 例如: 接收到的数据为心跳数据, 不包含采集数据.
示例
function handler(client, buffer) {
// 以 json 格式为例, 例如: {"id":"d01","time":"2022-10-17 17:57:32","values":[{"name":"temperature","data":17.5},{"name":"humidity","data":35.7}]}
const jsonData = JSON.parse(buffer.toString());
const time = moment(jsonData.time, "YYYY-MM-DD HH:mm:ss");
const values = {};
for (let i = 0; i < jsonData.values.length; i++) {
const value = jsonData.values[i];
values[value.name] = value.data;
}
return [
{id: jsonData.id, values: values, time: time.valueOf()}
]
}
指令处理脚本
该脚本用于将发送的指令内容转换为字节数组, 当向设备发送指令时, 驱动会将要发送的内容先经过 命令处理脚本
函数处理,
返回结果作为实际发送的内容.
函数定义如下:
/**
* 指令处理脚本, 当发送指令时将指令信息转换为字节数组
*
* @param {object} client 客户端对象
* @param {string} serialNo 平台指令下发序号
* @param {string} deviceId 设备标识
* @param {object} command 指令信息, 详细格式说明见驱动配置文档
* @return {Buffer} 最终发送数据
*/
function handler(client, serialNo, deviceId, command) {
// 数据转换处理, 将待发送内容转换为 Buffer
// 返回结果必须为字节数组
return Buffer.from("hello"); // 表示发送 "hello"
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端对象 |
serialNo | string | 平台指令下发序号 |
table | string | 表标识, 该字段用于 client.reportCommand 的参数 |
deviceId | string | 自定义设备标识 |
command | object | 指令信息, 格式如下 |
指令格式如下:
以 test
指令为例
{
"name": "test",
"showName": "测试",
"ops": [
{
"value": "123"
}
],
"params": {
"test": {
"attr1": "value1",
"attr2": 123
}
}
}
字段名 | 参数类型 | 参数说明 |
---|---|---|
name | 字符串 | 指令名称 |
showName | 字符串 | 指令显示名称 |
ops.value | 字符串 | 指令中配置的发送内容 |
params | 对象 | 数据写入配置 |
defaultValue | 对象 | 数据写入配置中各字段的默认值 |
注:
ops.value
通常为实际发送的内容, 该字段为必填值.opts
为数组, 目前长度固定为 1.当要发送的数据内容比较复杂时, 可以先转发送内容转换为 base64 格式的字符串, 然后在在指令脚本中再进行 base64 解码后再发送.
返回值说明
必须值必须为 Buffer
对象.
注: 可以使用
Buffer.from("this is a string"")
等方法创建Buffer
对象. 关于Buffer
的使用说明可以点击查看
示例
// 指令发送内容为 base64 格式, 在发送时先进行 base64 解码再发送
function handler(client, serialNo, table, deviceId, command) {
// 从指令信息中取出要发送的内容, 格式为 base64
const value = data.ops[0].value;
// 将要发送的内容转换为 buffer, 由于指令中的发送内容为 base64, 所在指定编码
return Buffer.from(value, 'hex');
}
连接处理脚本
当客户端连接到驱动或断开时, 会调用 连接处理脚本
.
函数定义如下:
/**
* 连接处理脚本, 当与服务端连接建立或断开时执行的操作
*
* @param {Object} client 客户端对象
* @param {boolean} state 连接状态, true: 已连接, false: 已断开
* @param {array} deviceIds 使用该连接的设备ID列表
*/
function handler(client, state, deviceIds) {
if (state) {
// 当客户端连接到驱动时执行操作
} else {
// 当连接断开时执行操作
}
}
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端对象 |
state | bool | 连接状态. true: 连接已建立, false: 连接已断开 |
deviceIds | string[] | 使用该连接的设备ID列表 |
返回值说明
无返回值
示例
function handler(client, state, deviceIds) {
// 当客户端连接到驱动时, 向客户端发送指定数据
if (state) {
client.send(Buffer.from("start report"));
} else {
// 连接断开时, 执行一些操作. 例如: 清理上下文对象等
}
}
定时器脚本
用于周期性的执行一些操作. 例如: 定时发送心跳.
函数定义如下:
/**
* 定时器脚本
*
* @param {object} client 客户端对象
* @param {array} deviceIds 使用同一连接的设备ID列表
*/
function handler(client, deviceIds) {
// 发送自定义数据
client.send(Buffer.from("keep alive"));
}
当没有任何客户端连接到驱动时, 定时器脚本不会执行.
参数说明
参数名 | 参数类型 | 参数说明 |
---|---|---|
client | object | 客户端对象 |
deviceIds | string[] | 设备ID列表. 即同一模型内连接同一服务器的所有设备ID列表 |
返回值说明
无返回值
示例
function handler(client, deviceIds) {
// 定时发送心跳
client.send(Buffer.from("keepalive data"));
}
示例
假设某设备使用 TCP
上报数据, 并且上报的数据格式如下所示:
开始符 长度 SN Time Data CRC 结束符
# 2byte 8byte 8byte ... 2byte @
# 0 100 17010032 1674982174145 V:220;A:0.14;S=1200;@
字段说明:
- 开始符固定为 '#'
- 长度为 2 个字节, 表示 '#' 与 '@' 之间所有数据的长度, 不包括 '#' 和 '@'
- SN 为设备编号, 8 个字节
- Time 为数据采集时间, 8 个字节, unix 时间戳(ms)
- Data 为数据点信息, 格式为
key1:value1;key2:value2;...
- CRC ModBus CRC 校验, 从开始符到 CRC 之前所有数据的 CRC 校验码
根据上述的报文格式, 数据拆包脚本和数据处理脚本如下所示:
// 数据拆包脚本
function handler(client, buffer) {
// 查找开始符 '#'
const startIndex = buffer.indexOf('#');
// 如果没有找到开始符, 则说明该报文不正确, 丢掉当前缓冲区内所有的数据
if(startIndex < 0) {
return {"drop": buffer.length};
}
// 如果找到了开始符, 则查找结束符 '@'
const endIndex = buffer.indexOf('@', startIndex);
// 如果没有找到结束符, 则说明该报文不完整, 需要接收更多的数据
if(endIndex < 0) {
// 如果开始符不在第一个位置, 则丢弃开始符之前的数据. 因为 '#' 之前的数据是无效的数据
if(startIndex > 0) {
return {"drop": startIndex};
}
// 返回 undefined 表示需要接收更多的数据
return undefined;
}
// 读取报文长度信息. 这里以大端字节序为例, 读取 2 个字节的长度信息
// 如果是小端字节序, 则使用 buffer.readUInt16LE(startIndex + 1)
const length = buffer.readUint16BE(startIndex + 1);
// 如果报文内的长度信息与实际的长度信息不匹配, 则丢弃该报文
if(startIndex + length + 2 > endIndex) {
// 丢弃 '@' 之前的所有数据(包含 '@')
return {"drop": endIndex};
}
// 复制 '#' 与 CRC 之前的内容
const content = Buffer.alloc(length);
content.copy(buffer, 0, startIndex + 1, startIndex + length - 2);
const checksum = crc.checksumModbus(content);
// 读取报文中的 crc 校验码(以大端字节序为例)
const expectedChecksum = buffer.readUint16BE(startIndex + length - 2);
// 如果计算得到的 crc 校验码与报文中的 crc 校验码不匹配, 则丢弃该报文
if(expectedChecksum !== checksum) {
// 校验失败, 丢弃该报文
return {"drop": endIndex};
}
// 返回报文内容, 即 '#' 之后到 CRC 之前的内容
return {"package": {"start": startIndex + 1, "length": length - 2, "next": endIndex}};
}