串口连接

Bittly 的串口连接模块提供了与串口设备通信的完整解决方案。通过可视化配置,您可以轻松建立、管理并复用串口连接,用于指令控制、流程自动化、上位机面板等多种场景。

串口连接

快速开始

创建串口连接

  1. 在项目导航栏点击 通讯连接新建连接
  2. 选择 串口 类型
  3. 配置基本参数(设备路径、波特率等)
  4. 点击 打开连接 按钮打开串口连接
  5. 点击 保存 完成创建,方便后续使用

使用串口连接

  • 创建后可被指令管理流程设计可视化面板等模块调用
  • 支持多模块同时使用同一连接,避免重复配置

连接配置

1. 基本参数

参数 说明 默认值
连接名称 连接的标识名称,建议描述用途 -
设备路径 串口设备路径(如 COM1、/dev/ttyUSB0) -
波特率 数据传输速率(bps) 9600
数据位 每个字节的数据位数 8
停止位 停止位数 1
校验位 错误校验方式 无校验
流控 流量控制方式 无流控

设备路径刷新:点击输入框右侧的 🔄 按钮,系统将自动扫描并列出当前可用的串口设备。

2. 高级参数

参数 说明 默认值 推荐场景
初始化延时 连接建立后的等待时间(毫秒) 100 设备启动较慢时
内容模式 数据显示格式:文本/HEX 文本 文本:可读字符 HEX:二进制数据
字符集 文本模式下的字符编码 UTF-8 根据设备协议选择
发送包最大尺寸 单次发送的最大字节数(0=无限制) 0 设备缓冲区较小时
发送包间隔 分包发送时的间隔时间(毫秒) 10 高速发送时防丢包

流控方式详解

流控类型 说明 适用场景
无流控 不进行流量控制 大多数简单设备
硬件流控 使用 RTS/CTS 引脚控制 高速传输、数据量大
软件流控 使用 XON/XOFF 字符控制 不支持硬流控的设备

推荐选择

  • 普通传感器:无流控
  • 高速数据采集:硬件流控
  • 旧式终端设备:软件流控

内容模式说明

文本模式

  • 适合传输可读字符(ASCII、UTF-8等)
  • 自动处理换行符(\r\n 转换)
  • 支持字符集选择
  • 示例场景:调试信息、配置文件、日志数据

HEX模式

  • 适合传输二进制数据
  • 以十六进制显示和输入
  • 支持字节级精确控制
  • 示例场景:固件升级、图像传输、加密数据

切换注意:切换模式后,已配置的指令可能需要调整数据格式。

故障排查

问题现象 可能原因 解决方案
找不到设备路径 1. 设备未连接 2. 驱动未安装 3. 权限不足 1. 检查物理连接 2. 安装对应驱动 3. 以管理员运行
连接成功但无数据 1. 波特率不匹配 2. 线路故障 3. 设备未响应 1. 确认设备波特率 2. 检查接线 3. 重启设备
数据乱码 1. 波特率错误 2. 字符集不匹配 3. 校验位错误 1. 调整波特率 2. 切换字符集 3. 检查校验设置
连接频繁断开 1. 流控不匹配 2. 电源不稳定 3. 线缆干扰 1. 调整流控方式 2. 使用稳压电源 3. 更换屏蔽线缆

与其他模块集成

在指令管理中调用

  1. 创建新指令时,选择已配置的串口连接
  2. 配置发送数据和接收解析规则
  3. 指令会自动使用该连接进行通信

在可视化面板中调用

  1. 拖拽串口连接控件到面板
  2. 配置通讯器选择需要连接的串口连接实例
  3. 绑定输入输出到变量或由面板脚本控制输入输出

高级功能

通讯脚本

通讯脚本是Bittly平台的核心扩展功能,允许您在各种通讯连接(串口、TCP、UDP、WebSocket等)的关键节点注入自定义JavaScript代码。通过脚本,您可以实现协议解析、数据转换、状态管理、错误处理等高级功能,灵活扩展通讯能力以满足复杂业务需求。

快速开始

创建第一个脚本
  1. 在通讯连接中打开 脚本设置 配置
  2. 点击脚本编辑按钮打开脚本编辑器
  3. 在脚本编辑器中编写脚本逻辑
  4. 点击 确定 保存脚本
  5. 脚本将自动应用于该连接的所有通信过程
脚本生效范围
  • 单连接生效:脚本配置在特定连接中,仅影响该连接的通信
  • 非实时生效:保存后并非立即生效,需重启连接
脚本执行环境
  • JavaScript ES2020+:支持现代JavaScript语法
  • 异步执行:支持async/await异步操作

生命周期回调函数

连接管理回调
连接建立前:beforeConnect()
/**
 * 在建立物理连接前执行
 * 用途:验证参数、初始化变量、记录日志
 * 异常处理:抛出异常将阻止连接建立
 */
export async function beforeConnect() {
    $this.log(`准备建立${$this.type}连接: ${$this.name}`);

    // 示例:检查必需变量是否存在
    const deviceId = $this.variable("device_id");
    if (!deviceId) {
        throw new Error("设备ID未配置,请检查环境变量");
    }

    // 示例:记录连接开始时间
    $this.variable("connect_start_time", Date.now());
}
连接建立后:afterConnect()
/**
 * 在连接成功建立后执行
 * 用途:发送初始化指令、设置设备状态等
 * 异常处理:抛出异常将导致连接断开
 */
export async function afterConnect() {
    $this.log(`${$this.type}连接已成功建立`);

    // 写入书数据
    await $this.write([0xAA, 0x01, 0x00]); 

    // 示例:更新设备状态
    $this.variable("device_status", "connected");

    // 示例:记录连接耗时
    const startTime = $this.variable("connect_start_time");
    const duration = Date.now() - startTime;
    $this.log(`连接建立耗时: ${duration}ms`);
}
连接关闭前:beforeClose()
/**
 * 在主动关闭连接前执行
 * 用途:发送断开指令、清理资源、保存状态
 */
export async function beforeClose() {
    $this.log(`准备关闭${$this.type}连接`);

    // 示例:发送设备停机指令
    if ($this.variable("device_type") === "motor") {
        await $this.write("STOP\r\n");
        await $this.delay(100); // 等待指令执行
    }

    // 示例:保存最后的状态数据
    const lastData = $this.variable("last_received");
    if (lastData) {
        await $this.saveToDatabase("last_status", lastData);
    }
}
连接关闭后:afterClose()
/**
 * 在连接关闭后执行
 * 用途:更新状态、发送通知、清理定时器
 */
export async function afterClose() {
    // 示例:更新设备状态
    $this.variable("device_status", "disconnected");

    // 示例:发送连接断开通知
    if ($this.variable("require_notification")) {
        await $this.sendNotification({
            type: "connection_closed",
            connection: $this.name,
            time: new Date().toISOString()
        });
    }

    $this.log(`${$this.type}连接已关闭`);
}
数据处理回调
原始数据接收:onData(data)
/**
 * 每次接收到原始数据时触发(可能不完整)
 * 参数:data - Buffer类型原始数据
 * 返回:处理后的数据或null(丢弃数据)
 * 用途:数据校验、日志记录、简单过滤
 */
export async function onData(data) {
    // 记录原始数据(调试用)
    $this.variable("last_raw_data", data.toString('hex'));

    // 示例:过滤空数据
    if (data.length === 0) {
        $this.log("接收到空数据,已忽略");
        return null;
    }

    // 示例:简单数据校验(检查起始字节)
    if (data[0] !== 0xAA && data[0] !== 0x55) {
        $this.log(`无效起始字节: 0x${data[0].toString(16)}`);
        return null;
    }

    // 示例:统计接收数据量
    const totalBytes = ($this.variable("total_received_bytes") || 0) + data.length;
    $this.variable("total_received_bytes", totalBytes);

    return data; // 返回原始数据继续处理
}
完整数据帧接收:onFrame(frame)
/**
 * 接收到完整数据帧时触发(需要先在连接配置中启用并配置帧规则)
 * 参数:frame - Buffer类型,完整的原始数据帧
 * 返回值:Buffer | null - 处理后的数据帧或null(丢弃该帧)
 * 用途:数据帧校验、协议解析、帧内容转换
 * 
 * 重要限制:
 * 1. 必须返回Buffer类型或null,不能返回其他数据类型
 * 2. 如果返回null,该帧将被丢弃,不会传递给后续处理流程
 * 3. 如果返回Buffer,将作为新的帧数据继续传递
 */
export async function onFrame(frame) {
    $this.log(`收到完整数据帧,长度: ${frame.length}字节`);

    try {
        // 示例:Modbus RTU协议解析
        if ($this.variable("protocol") === "modbus_rtu") {
            return await parseModbusFrame(frame);
        }
        // 默认返回原始帧
        return frame;
    } catch (error) {
        $this.log(`帧处理错误: ${error.message}`);
        return null; // 丢弃错误帧
    }
}

// 自定义函数实现帧处理
async function parseModbusFrame() {
    // 解析数据帧实现
}
数据发送回调
发送前处理:beforeWrite(data)
/**
 * 在发送数据前执行
 * 参数:data - 原始发送数据(字符串或Buffer)
 * 返回:处理后的发送数据或null(取消发送)
 * 用途:数据加密、添加协议头、数据压缩
 */
export async function beforeWrite(data) {
    // 记录发送日志
    $this.log(`准备发送数据: ${data.toString('hex')}`);

    // 示例:添加协议帧头
    if ($this.variable("add_frame_header")) {
        const header = Buffer.from([0xAA, 0x55]); // 帧头
        const length = Buffer.from([data.length]);
        const framedData = Buffer.concat([header, length, Buffer.from(data)]);

        // 计算并添加校验和
        const checksum = calculateChecksum(framedData);
        framedData.writeUInt8(checksum, framedData.length);

        return framedData;
    }

    // 示例:数据加密(AES)
    if ($this.variable("encryption_enabled")) {
        const key = $this.variable("encryption_key");
        const encrypted = await encryptAES(data, key);
        return encrypted;
    }

    return data; // 返回原始数据
}
发送后处理:afterWrite(data)
/**
 * 在数据发送成功后执行
 * 参数:data - 实际发送的数据(Buffer)
 * 用途:发送确认、状态更新、统计计数
 */
export async function afterWrite(data) {
    // 示例:更新发送统计
    const sendCount = ($this.variable("total_send_count") || 0) + 1;
    $this.variable("total_send_count", sendCount);

    const totalBytes = ($this.variable("total_sent_bytes") || 0) + data.length;
    $this.variable("total_sent_bytes", totalBytes);

    // 示例:设置最后发送时间
    $this.variable("last_send_time", Date.now());

    // 示例:等待设备响应(特定场景)
    if ($this.variable("wait_for_response")) {
        await $this.delay($this.variable("response_timeout") || 1000);
        // 检查是否收到响应
        const hasResponse = $this.variable("last_response_time");
        if (!hasResponse) {
            $this.log("警告:发送后未收到响应");
        }
    }
}
心跳回调
自定义心跳:heartbeat()
/**
 * 周期性执行(需在连接设置中启用心跳)
 * 用途:保活检测、状态查询、定期上报
 */
export async function heartbeat() {
    try {
        await $this.write("PING");
        $this.variable("last_heartbeat_sent", Date.now());

        // 检查上次心跳响应
        const lastResponse = $this.variable("last_heartbeat_response");
        const timeout = $this.variable("heartbeat_timeout") || 30000;

        if (lastResponse && Date.now() - lastResponse > timeout) {
            $this.log("心跳响应超时,可能连接已断开");
            $this.variable("connection_healthy", false);
        }

    } catch (error) {
        $this.log(`心跳发送失败: ${error.message}`);
    }
}

脚本上下文对象

$this - 当前连接对象
核心方法
方法 说明 示例
write(data) 发送数据 await $this.write("Hello")
variable(key, value) 获取/设置变量 $this.variable("status")
log(message, level) 记录日志 $this.log("信息", "info")

自定义函数

自定义函数是脚本中封装可重用逻辑的核心方式。Bittly的脚本系统支持多种函数类型,每种类型有不同的作用域、调用方式和适用场景。合理使用自定义函数可以大幅提高脚本的可维护性和复用性。

内部函数(脚本内使用)

定义与特点

内部函数是仅在当前脚本文件内部可见和使用的函数。它们的主要特点是:

  1. 作用域限制:只能在定义它们的脚本文件中被调用
  2. 隐私性:不会被外部模块(如消息模板、其他脚本)访问
  3. 灵活性:可以包含异步操作、复杂逻辑和私有变量
  4. 封装性:隐藏实现细节,提供清晰的API接口
// 示例1 : 
function funcName( paramA, paramB ) {
   return paramA + paramB;
}
导出函数(可在消息模板中使用)

导出函数是通过export关键字暴露给外部系统(特别是消息模板)的函数。它们的特点是:

  1. 公开访问:可以被消息模板、其他脚本(通过特定方式)调用
  2. 同步执行:消息模板中只能调用同步函数
  3. 参数限制:参数和返回值类型需适合模板引擎处理
  4. 无副作用:理想情况下应该是纯函数,避免修改外部状态
// 示例:格式化温度
export function formatTemperature(rawValue) {
    // rawValue可能是16进制字符串或数字
    const value = typeof rawValue === 'string' 
        ? parseInt(rawValue, 16) 
        : rawValue;

    // 假设原始值为0.1度单位
    const celsius = value * 0.1;
    const fahrenheit = celsius * 1.8 + 32;

    return {
        celsius: celsius.toFixed(1),
        fahrenheit: fahrenheit.toFixed(1)
    };
}

// 示例: 生成设备ID
export function generateDeviceId(prefix = "DEV") {
    const timestamp = Date.now().toString(36);
    const random = Math.random().toString(36).substr(2, 5);
    return `${prefix}_${timestamp}_${random}`.toUpperCase();
}
函数调用规范
// 正确:内部调用导出函数
export async function processData(data) {
    const temp = formatTemperature(data.value); // 调用导出函数
    return temp;
}

// 错误:消息模板中调用异步函数(不允许)
export async function asyncInTemplate() {
    // 这个函数不能在消息模板中调用
}

// 正确:消息模板调用
// 消息模板内容:当前温度: {{ formatTemperature(0x64).celsius }}°C

数据分帧

数据分帧是将连续的字节流分割为独立、完整的数据单元(帧)的核心功能。在通信过程中,设备通常以流的形式发送数据,接收方需要准确识别每个独立消息的边界。分帧功能确保您能正确处理各种协议格式,从简单的文本行到复杂的二进制协议。

启用分帧功能

  1. 进入通讯连接配置页面
  2. 在右侧配置面板找到分帧设置部分
  3. 选择适合您协议的分帧模式
  4. 配置相关参数
  5. 保存连接配置
分帧模式选择指南
协议类型 推荐模式 说明
文本日志、命令行 换行分帧 按换行符分割
Modbus RTU、固定长度协议 固定长度分帧 每个帧长度固定
自定义二进制协议 匹配头尾分帧 有明确的起始和结束标记
流式数据、简单设备 超时分帧 数据间隔明显
JSON API、Web服务 JSON分帧 处理JSON格式数据
XML协议、SOAP服务 XML分帧 处理XML格式数据
复杂文本协议 正则表达式分帧 灵活的模式匹配
特殊/混合协议 无分帧+脚本处理 完全自定义逻辑
分帧在数据处理流程中的位置
原始字节流 → 分帧处理 → 完整数据帧 → onFrame回调 → 后续业务处理
       ↑           ↑
   接收缓冲区   分帧规则匹配

1. 无分帧

工作原理
  • 接收到的数据立即传递给后续处理步骤
  • 不做任何边界检测或缓冲
  • 适合:已在外部分帧、数据量小、实时性要求高的场景
注意事项

⚠️ 数据完整性风险:如果设备发送速度过快或消息分多次到达,可能导致消息不完整或粘包问题。

2. 超时分帧

工作原理
数据到达 → 启动/重置超时计时器 → 等待更多数据 → 超时 → 提交完整帧
                  ↑
              新数据到达
配置参数
参数 说明 默认值 推荐范围
超时时间 数据间隔超时(毫秒) 1000 10-5000
适用场景
  • 数据有明显停顿间隔的设备
  • 交互式命令行接口
  • 低速传感器数据采集

3. 换行分帧

工作原理
接收数据 → 搜索换行符 → 找到换行符 → 提取并提交行数据
              ↓                        ↓
          未找到          保留在缓冲区,等待更多数据
配置参数
参数 说明 选项
换行符 行结束标记 \n(LF)、\r(CR)、\r\n(CRLF)
包含换行符 是否保留换行符 ☑ 是 / ☐ 否
换行符处理规则
  • LF(\n):Unix/Linux系统标准
  • CR(\r):旧式Mac系统
  • CRLF(\r\n):Windows标准
适用场景
  • 日志文件处理
  • 命令行输出
  • 简单文本协议
  • Telnet/SSH会话

4. 固定长度分帧

工作原理
接收数据 → 检查缓冲区长度 → 达到固定长度 → 提交完整帧
                   ↓
            未达到长度 → 继续等待
配置参数
参数 说明 示例
帧长度 每帧的固定字节数 8、16、32等
注意事项

⚠️ 数据丢失风险:如果实际帧长与配置不符,可能导致:

  1. 帧过短:等待超时或永远无法提交
  2. 帧过长:被截断,数据丢失

建议:先通过数据捕获分析实际帧结构。

5. 匹配头尾分帧

工作原理
接收数据 → 搜索帧头 → 找到帧头 → 搜索帧尾 → 找到帧尾 → 提取中间内容
     ↓           ↓           ↓
未找到      保留数据    继续等待
配置参数
参数 说明 示例
帧头 帧开始标记(可选) "START"、0xAA55
帧尾 帧结束标记(可选) "END"、0x0D0A
超时时间 帧尾搜索超时 1000ms
四种匹配模式

模式A:有头有尾

帧头: "<packet>"
帧尾: "</packet>"
数据: "hello<packet>data</packet>world"
结果: "data"

模式B:有头无尾

帧头: "BEGIN"
数据: "BEGINframe1BEGINframe2"
结果: "frame1", "frame2"
说明:下一个帧头作为当前帧的结束

模式C:无头有尾

帧尾: "END"
数据: "line1ENDline2END"
结果: "line1", "line2"
说明:帧尾之前的所有数据为一帧

模式D:头尾相同

帧头=帧尾: "EOF"
数据: "EOFcontentEOFEOFnextEOF"
结果: "content", "next"
说明:支持头尾相同的标记
适用场景
  • 带边界标记的文本协议

6. 正则表达式分帧

工作原理
接收数据 → 应用正则匹配 → 找到匹配 → 提取捕获组 → 提交帧
               ↓
           未找到匹配 → 继续等待更多数据
配置参数
参数 说明 示例
正则表达式 匹配模式 ^\\d{4}-\\d{2}-\\d{2}
忽略大小写 是否区分大小写 ☑ 是
适用场景
  • 复杂文本协议
  • 日志文件模式提取
  • 半结构化数据
  • 数据清洗和提取

7. JSON分帧

工作原理
接收数据 → JSON语法分析 → 找到完整JSON对象 → 提交帧
                ↓
          不完整JSON → 继续等待更多数据
适用场景
  • RESTful API通信
  • WebSocket JSON消息
  • 配置文件流式读取
  • 日志聚合服务

8. XML分帧

工作原理
接收数据 → XML解析器 → 寻找完整元素 → 提交帧
              ↓
          不完整XML → 继续等待
适用场景
  • SOAP Web服务
  • XML-RPC通信
  • RSS/Atom订阅
  • 配置文件读取

9. HEX正则表达式分帧

工作原理
接收数据 → 解析HEX正则表达式 → 匹配二进制模式 → 找到完整帧 → 提交帧
               ↓
           未找到匹配 → 滑动窗口继续搜索
配置参数
参数 说明 示例
表达式 HEX正则表达式 AA55@U8<type>@U16<length>@U8[$(length)]
适用场景
  • 自定义二进制协议
  • 工业控制协议(Modbus、CAN等)
  • 嵌入式设备通信
  • 二进制文件格式解析
示例1:简单协议帧
# 帧头 + 命令 + 长度 + 数据 + 校验
AA55          # 固定帧头
@U8           # 命令字
@U16<LENGTH>  # 数据长度
@U8[$(LENGTH)] # 变长数据
@U16          # CRC校验
示例2:嵌套长度协议
# 头部长度 + 头部 + 载荷长度 + 载荷
@U8<HLEN>     # 头部长度
@U8[$(HLEN)]  # 头部数据
@U16<DATALEN>   # 载荷长度
@U8[$(DATALEN)] # 载荷数据
示例3:带版本验证协议
# 魔数 + 版本验证 + 序列号 + 数据
AABBCCDD      # 魔数
@U8           # 版本必须为1
@U32          # 序列号
@U16<DATALEN> # 数据长度
@U8[$(DATALEN)] # 实际数据

数据转发

数据转发是Bittly平台的连接桥接功能,允许您在相同类型的通信连接之间建立单向数据通道。当两个连接都支持相同的数据流模式(文本或HEX)时,可以将源连接接收的数据转发到目标连接。

核心概念

  • 类型匹配:仅支持相同数据流类型的连接间转发
  • 单向传输:数据从源连接流向目标连接,即当源通讯连接收到数据后发送给目标通讯连接。
  • 简单配置:只需选择目标连接即可

配置步骤

  1. 进入 通讯连接 管理页面
  2. 选择要配置的源连接(如串口COM1)
  3. 在连接详情中找到 转发设置 区域
  4. 点击 转发目标
  5. 从列表中选择目标连接(仅显示兼容类型)
  6. 保存配置

配置参数

参数 说明 必填 示例
目标连接 接收转发数据的连接 TCP客户端_192.168.1.100:8080

注意

  • 修改转发配置后,需要重新连接或重启连接才能生效
  • 已建立的转发不会实时更新配置
  • 目标连接断开时,转发自动暂停

心跳

心跳用于在串口连接建立后,按照固定时间间隔向设备发送指定数据,以满足设备协议对心跳的要求,或用于周期性检测设备是否正常响应。

心跳不会影响串口的物理连接状态,仅负责定时发送数据

基本行为说明

  • 心跳在串口连接成功后启用
  • 首次心跳发送时间 = 连接成功后 + 心跳时间间隔
  • 当连接断开时,心跳会自动停止
  • 心跳发送失败不会自动重连

心跳模式

心跳支持以下三种模式,三选一,互斥

  • 文本(Text)
  • HEX
  • 脚本(Script)

切换模式后,仅当前模式生效,其余模式配置将被忽略。

文本(Text)模式

字符串形式发送心跳内容。

配置项
  • 时间:心跳发送间隔,单位为毫秒(ms)
  • 内容:要发送的字符串内容,例如:ping
行为说明
  • 字符串按 UTF-8 编码发送
  • 不会自动追加换行符(\r / \n
  • 如协议需要换行,请手动填写,例如:ping\r\n
示例
ping

HEX 模式

十六进制字节流形式发送心跳内容。

配置项
  • 时间:心跳发送间隔,单位为毫秒(ms)
  • 内容:HEX 字节序列,例如:AA BB CC DD
规则说明
  • 每两个十六进制字符表示一个字节
  • 字节之间使用空格分隔
  • 不支持 0xAA 形式
  • 不区分大小写(aaAA 等价)
示例
AA BB CC DD

脚本(Script)模式

通过脚本自定义心跳发送逻辑。

配置项
  • 时间:心跳发送间隔,单位为毫秒(ms)
行为说明
  • Bittly 会按照配置的时间间隔,自动定时调用脚本中的 heartbeat 函数
  • 用户无需自行实现定时逻辑
  • 每次调用代表一次心跳触发
示例
export async function heartbeat() {
    $this.write('PING');
}
脚本上下文说明
  • $this:当前连接连接对象
  • $this.write(data):向连接发送数据
    • data 支持字符串或 HEX 字节数组(依据实际接口定义)

关闭心跳

以下任一情况将关闭心跳:

  • 未配置心跳时间
  • 心跳时间 ≤ 0
  • 连接断开
页面目录