Skip to main content

TCP客户端

驱动说明

该驱动每个模型可以配置一个服务器地址或为为每个设备单独配置一个服务器地址, 同一模型内服务器地址相同的设备将会使用同一个 TCP 连接(如果模型内所有设备都未配置服务器地址, 则该模型内的所有设备共用同一个连接).

驱动启动后, 每个模型都会主动与服务端建立连接, 然后接收服务端的数据.

tip
  1. 当模型没有添加任何设备时, 该模型不会与服务器建立连接.
  2. 即使多个模型连接同一服务器, 也会为每个模型会创建一个独立的连接.

驱动脚本执行流程说明

以下处理流程以模型为单位, 多个模型之间独立执行, 互不影响.

  1. 当与服务器成功建立连接后, 会执行 连接处理脚本, 此时 state 参数的值为 true.
  2. 当接收到服务端发送的数据时, 会读取所有的发送数据并存放到 Buffer 中, 然后调用 数据包拆分脚本.
  3. 如果从已接收到的数据中拆分出完整的数据包,则将完整的数据包封装为 Buffer 交由 数据处理脚本.
  4. 如果未从已接收到的数据中拆分出完整的数据包, 则根据 数据包拆分脚本 返回的数据决定是等待接收更多的数据或是丢弃部分数据.
  5. 当连接断开时, 会执行 连接处理脚本, 此时 state 参数的值为 false.

客户端对象

客户端对象为当前 TCP 连接对象. 该对象除了提供与服务器通讯的功能外, 并且供一些常用的功能用于简化使用,提高效率.

数据包拆分脚本, 数据处理脚本, 指令处理脚本连接处理脚本 函数的参数中提供了 client 对象参数, 在脚本中可以通过使用 client 中提供的函数完成数据的发送与采集.

该对象提供了以下函数:

与服务器交互

指令相关

媒体库(未实现)

工作表

数据存储

向服务端发送数据

用于向服务端发送数据. 例如: 在接收到数据时向服务端发送 ack 信息或发送心跳数据.

参数说明

参数名参数类型参数说明示例值
dataBuffer发送的内容Buffer.from("hello") 代表要发送 "hello"

返回值

stringundefined. 如果参数不正确(字节数组为空或空数组)或发送失败则返回 string 内容为错误说明, 如果发送成功则返回 undefined.

示例

function handler(client, buffer) {
// 向客户端发送 hello world
client.send(Buffer.from("hello world"));
}

向平台上报指令执行结果

上报指令执行结果. 有些场景指令的执行结果反馈是异步的, 在指令发送后, 过一段时间才会收到响应报文, 此时可以在接收到响应报文时通过 client.reportCommand(...) 方法上报指令执行结果. 通过该方法上报的结果信息可以在平台中 指令状态管理 页面中查看.

参数说明

参数名参数类型参数说明示例值
serialNoString平台指令序号"f160a24b-1780-89e7-cd48-b4c0073bd0fe"
tableString表标识"tcp_client_driver"
deviceIdString设备标识"ST10001"
stateString状态标识自定义状态标识, 例如: Success, Failed 等
resultString结果数据有些指令可能有响应数据, 可以将结果数据保存在 result 字段中. 例如: 读取设备配置指令, 会返回设备配置信息
tip
  1. serialNo 为平台在发送指令时生成, 会传入到 指令处理脚本 中, 当指令执行结果为异步反馈时, 可以将 serialNo 保存到 设备上下文 中, 以便在后续 reportCommand 使用.

  2. 参数中的 table 来自于 指令处理函数 的参数, 当需要通过 reportCommand 上报自定义的命令执行结果信息时, 需要在 指令处理函数 中保存相关参数信息以便后续使用.

返回值

stringundefined. 如果返回 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);
}
}

请求媒体库文件-路径

请求媒体库文件

参数说明

参数名参数类型参数说明示例值
pathString文件路径文件在媒体库的路径. 例如: /background/bg1.png 为目录 background 中的 bg1.png 文件

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因
dataobject文件信息请求成功时该字段存在
    namestring文件名文件名. 例如: bg1.png
    sizenumber文件大小单位: 字节
    dataBuffer文件内容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

请求媒体库文件

参数说明

参数名参数类型参数说明示例值
urlString文件 url文件在媒体库请求 url, 该信息一般由媒体库组件或附件组件获得. 例如: /core/fileServer/mediaLibrary/default/test/hello.txt
tip

url 中 default 为项目ID, test 为目录, hello.txt 为文件名.

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因
dataobject文件信息请求成功时该字段存在
    namestring文件名文件名. 例如: bg1.png
    sizenumber文件大小单位: 字节
    dataBuffer文件内容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;
}

上传文件到媒体库

上传文件到媒体库

参数说明

参数名参数类型参数说明示例值
filenameString文件名例如: hello.txt
catalogString目录文件上传到的目录, 支持多级目录. 例如: image/background
actionString存在同名文件时的动作cover: 覆盖已有文件.
rename: 新上传文件名后面自动加1.
append: 在文件尾部追加内容
dataBuffer文件内容文件的内容

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因

示例

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

删除媒体库文件

删除媒体库文件

参数说明

参数名参数类型参数说明示例值
pathString文件路径文件在媒体库的路径. 例如: /background/bg1.png 为目录 background 中的 bg1.png 文件

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因

示例

function handler(client) {
// 删除目录 background 中的 bg1.png 文件
const response = client.deleteMediaFile("/background/bg1.png");
if (!response.success) {
console.log("删除媒体库文件失败:", response.message);
return;
}
}

向工作表中写入数据

向工作表写入一条数据

参数说明

参数名参数类型参数说明示例值
tableIdString工作表标识例如: student
rowDataobject写入数据该字段为 JSON 对象, 内容根据工作表定义填写.
例如: {"id": "1","name": "小明", "age": 18}
tip

rowData 对象中必须包含 id 字段, 并且该字段做为数据的唯一标识, 必须唯一.

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因
datastring记录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;
}

更新工作表数据

更新工作表中的数据

参数说明

参数名参数类型参数说明示例值
tableIdString工作表标识例如: student
queryobject过滤条件该字段为 JSON 对象, 过滤出要更新哪些记录. 例如: {"name": "小明"} 更新 name 为 "小明" 的所有记录
rowDataobject写入数据该字段为 JSON 对象, 内容根据工作表定义填写. 例如: {"age": 19} 更新 age 字段的值为 19

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因

示例

function handler(client) {
// 更新 name 字段值为 "小明" 所有记录的 age 字段的值为 19
const response = client.updateWorkTableRow("student", {"name": "小明"}, {"age": 19});
if (!response.success) {
console.log("更新数据失败:", response.message);
return;
}
}

根据记录标识更新工作表数据

根据记录标识更新记录

参数说明

参数名参数类型参数说明示例值
tableString表标识例如: "student"
rowIdString记录ID例如: "1"
rowDataobject更新内容该字段为 JSON 对象, 要更新的内容.
例如: {"name": "小明", "age": 18}

返回值

参数名参数类型参数说明示例值
successbool是否成功请求是否成功. true: 成功, false: 失败
messagestring信息请求失败时为失败原因

示例

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

获取数据上下文

用于获取上下文对象, 可以在上下文中存储数据. 详细信息

tip

第一次调用的时候才会创建上下文

参数说明

参数名参数类型参数说明示例值
contextIdString上下文标识"myContext"

返回值

Object. 数据上下文对象

示例

function handler(client) {
// 获取一个标识为 myContext 的上下文
var myContext = client.getContext("myContext");
}

删除数据上下文

删除上下文对象

参数说明

参数名参数类型参数说明示例值
contextIdString上下文标识"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 不同的是, 当设备被删除后, 重启驱动时会自动清理被删除设备的上下文对象.

tip

第一次调用的时候才会创建上下文

参数说明

参数名参数类型参数说明示例值
deviceIdString设备标识"ST10001"

返回值

Object. 数据上下文对象

示例

function handler(client) {
// 获取设备 ST10001 的上下文
var context = client.getDeviceContext("ST10001");
}

删除设备上下文

删除设备上下文对象

参数说明

参数名参数类型参数说明示例值
deviceIdString上下文标识"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();
}

数据上下文

上下文对象, 用来存储数据, 上下文中的数据可以在不同的脚本中共享. 例如: 可以在 指令处理脚本中 写入数据, 然后从 数据处理脚本 中读取数据. 不同上下文彼此独立, 互不影响.

caution

可以根据需求创建多个上下文对象, 但是上下文对象以及上下文中的数据需要及时清理, 否则会造成 OOM 问题, 导致驱动程序崩溃.

向上下文中保存数据

用于向上下文中存储数据. 如果 key 已存在则会覆盖已有数据.

参数说明

参数名参数类型参数说明
keystring数据项的 key
valueany数据项的值

返回值

示例

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

参数说明

参数名参数类型参数说明
keystring数据项的 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

参数说明

参数名参数类型参数说明
keystring数据项的 key

返回值

anyundefined. 返回 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.

tip

该函数返回后, 再使用 getgetAndRemove 均返回 undefined.

参数说明

参数名参数类型参数说明
keystring数据项的 key

返回值

anyundefined. 返回 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 不存在则不执行任何操作.

参数说明

参数名参数类型参数说明
keystring数据项的 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 对象中的校验函数.

CRC-16循环冗余校验

使用 checksum16(Buffer, Poly) 实现 16位 的循环冗余校验

参数说明

参数名参数类型参数说明
dataBuffer被校验的数据
polyint多项式

返回值

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位 的循环冗余校验

参数说明

参数名参数类型参数说明
dataBuffer被校验的数据
polyint多项式

返回值

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位 的循环校验码

参数说明

参数名参数类型参数说明
dataBuffer被校验的数据
polyint多项式

返回值

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 多项式参数.

参数说明

参数名参数类型参数说明
dataBuffer被校验的数据

返回值

uint16. 16位无符号整型

function handler() {
// 示例数据
const data = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);

// 返回 uint16 的校验码
const checksum = crc.checksumModbus(data);
}

内置函数

数据包拆分函数

tip

内置数据包拆分函数仅能用于 数据拆包脚本

固定长度头

该函数会取前 N 个字节作为长度信息, 然后根据长度信息读取主体内容.

  +--------+-----------+
| Length | Data |
+--------+-----------+

在数据包拆分脚本中直接使用内置的拆分函数, 如下所示:

// 创建长度头为 4 个字节, 长度头为小端字节序
// 第 1 个参数为长度占用的字节数量
// 第 2 个参数为长度头的字节序, 取值可以为 little(小端字节序) 或 big(大端字节序)
const handler = createFixedLengthSplitFn(4, "little");

固定分隔符

该函数会使用固定分隔符拆分数据包. 使用方式如下所示:

// 创建使用  "\r\n" 作为分隔符
const handler = createDelimiterSplitFn("\r\n");
caution

数据包内容中不能包含分隔符

固定开始和结束符

该函数会读取指定开始符和结束符之间的数据做为一个完整的数据包. 使用方式如下所示:

// 创建以 '@' 开头并且以 '#' 结尾的数据包拆分函数
// 第 1 个参数为开始符
// 第 2 个参数为结束符
const handler = createStartEndDelimiterSplitFn("@", "#");
info

开始符结束符 可以包含多个字符.

caution

如果一个数据包中存在多个 开始符结束符 时无法使用该内置函数.
例如: 使用 @# 作为开始符和结束符时, 数据包主体部分不能包含 @#, 否则拆包结果不正确.

脚本说明

脚本语言: JavasScript ECMAScript 5.1

驱动使用时要求提供 数据包拆分脚本, 数据处理脚本, 连接处理脚本指令处理脚本 脚本函数来处理接收和发送数据过程中的协议和数据格式问题.

在脚本的上下文中内置了 Buffer 包, 可用于处理接收或发送二进制数据.

除此之外, 还内置了 lodash, crypto-js, moment, xml-jsformulajs(Excel函数) 包.

tip

所有的脚本中的函数名必须为 handler, 参数列表参考各脚本说明.

数据包拆分脚本

该脚本用于处理接收数据过程中的半包和粘包问题, 该函数需要根据数据格式判断接收到的字节数组中是否包含完整的数据包, 如果包含了完整数据包则返回完据包的起始位置和长度. 或者当数据包有错误时丢掉该数据包或有问题部分的数据.

如果字节数组中不包含完整的数据包, 直接返回 undefined 表示需要从客户端接收更多的数据.

函数定义如下:

/**
* 数据包拆分脚本, 从字节流中提取出完整的数据包, 或丢弃部分数据.
*
* @param {Object} client 客户端对象
* @param {Buffer} buffer 未处理的数据
* @return {Object}
*/
function handler(client, buffer) {
// 拆包逻辑
return {"package": {"start": 0, "length": 128, "next": 129}, "drop": 0};
}
tip

驱动内置了常用的数据包拆分函数. 详情

参数说明

参数名参数类型参数说明
clientobject客户端对象
bufferBuffer从客户端接收到的数据

返回值说明

  • 当字节数组中包含完整数据包时, 返回包含 package 字段的对象.
  • 如果字节数组中存在错误, 则返回包含 drop 字段的对象, 丢弃掉错误的数据.
  • 如果字节数组中即不包含完整的数据包也不存在错误, 直接返回 undefined, 表示需要从客户端接收更多的数据.
参数名参数类型参数说明示例值
packageobject数据包信息, 如果要丢弃数据该字段不需要返回{"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, 此时可以提取出完整的数据包.

注: 如果返回结果中同时包含了 droppackage 字段并且 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: 为数据点的值
}
];
}

参数说明

参数名参数类型参数说明
clientobject客户端连接对象
bufferBuffersplitHandler 函数得到的数据包

返回值

参数名参数类型参数说明示例值
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"
valueany数据点的值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"
}

参数说明

参数名参数类型参数说明
clientobject客户端对象
serialNostring平台指令下发序号
tablestring表标识, 该字段用于 client.reportCommand 的参数
deviceIdstring自定义设备标识
commandobject指令信息, 格式如下

指令格式如下:

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 {
// 当连接断开时执行操作
}
}

参数说明

参数名参数类型参数说明
clientobject客户端对象
statebool连接状态. true: 连接已建立, false: 连接已断开
deviceIdsstring[]使用该连接的设备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"));
}
info

当没有任何客户端连接到驱动时, 定时器脚本不会执行.

参数说明

参数名参数类型参数说明
clientobject客户端对象
deviceIdsstring[]设备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}};
}