Bittly 经典蓝牙连接用于连接并控制各种经典蓝牙设备。 设备连接后,可通过设置脚本,分帧,转发,心跳等功能,实现数据的发送和接收。

设备 : 通过搜索设备列表选择目标经典蓝牙设备。内容模式 : 选择串口通讯的内容模式,支持文本和HEX两种,用于优化数据展示和解析。字符集 : 当内容模式为文本时,选择用于解析文本数据的字符集。发送包最大尺寸 : 设定串口通讯的单个发送包的最大尺寸(字节)。超过此尺寸的数据将分多个包发送。设置为0时表示不限制尺寸。发送包间隔 : 设置发送大于最大尺寸包时,各包之间的发送间隔(毫秒)。通讯脚本是Bittly平台的核心扩展功能,允许您在各种通讯连接(串口、TCP、UDP、WebSocket等)的关键节点注入自定义JavaScript代码。通过脚本,您可以实现协议解析、数据转换、状态管理、错误处理等高级功能,灵活扩展通讯能力以满足复杂业务需求。
脚本设置 配置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}`);
}
}
| 方法 | 说明 | 示例 |
|---|---|---|
| write(data) | 发送数据 | await $this.write("Hello") |
| variable(key, value) | 获取/设置变量 | $this.variable("status") |
| log(message, level) | 记录日志 | $this.log("信息", "info") |
自定义函数是脚本中封装可重用逻辑的核心方式。Bittly的脚本系统支持多种函数类型,每种类型有不同的作用域、调用方式和适用场景。合理使用自定义函数可以大幅提高脚本的可维护性和复用性。
定义与特点
内部函数是仅在当前脚本文件内部可见和使用的函数。它们的主要特点是:
// 示例1 :
function funcName( paramA, paramB ) {
return paramA + paramB;
}
导出函数是通过export关键字暴露给外部系统(特别是消息模板)的函数。它们的特点是:
// 示例:格式化温度
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
数据分帧是将连续的字节流分割为独立、完整的数据单元(帧)的核心功能。在通信过程中,设备通常以流的形式发送数据,接收方需要准确识别每个独立消息的边界。分帧功能确保您能正确处理各种协议格式,从简单的文本行到复杂的二进制协议。
| 协议类型 | 推荐模式 | 说明 |
|---|---|---|
| 文本日志、命令行 | 换行分帧 | 按换行符分割 |
| Modbus RTU、固定长度协议 | 固定长度分帧 | 每个帧长度固定 |
| 自定义二进制协议 | 匹配头尾分帧 | 有明确的起始和结束标记 |
| 流式数据、简单设备 | 超时分帧 | 数据间隔明显 |
| JSON API、Web服务 | JSON分帧 | 处理JSON格式数据 |
| XML协议、SOAP服务 | XML分帧 | 处理XML格式数据 |
| 复杂文本协议 | 正则表达式分帧 | 灵活的模式匹配 |
| 特殊/混合协议 | 无分帧+脚本处理 | 完全自定义逻辑 |
原始字节流 → 分帧处理 → 完整数据帧 → onFrame回调 → 后续业务处理
↑ ↑
接收缓冲区 分帧规则匹配
⚠️ 数据完整性风险:如果设备发送速度过快或消息分多次到达,可能导致消息不完整或粘包问题。
数据到达 → 启动/重置超时计时器 → 等待更多数据 → 超时 → 提交完整帧
↑
新数据到达
| 参数 | 说明 | 默认值 | 推荐范围 |
|---|---|---|---|
| 超时时间 | 数据间隔超时(毫秒) | 1000 | 10-5000 |
接收数据 → 搜索换行符 → 找到换行符 → 提取并提交行数据
↓ ↓
未找到 保留在缓冲区,等待更多数据
| 参数 | 说明 | 选项 |
|---|---|---|
| 换行符 | 行结束标记 | \n(LF)、\r(CR)、\r\n(CRLF) |
| 包含换行符 | 是否保留换行符 | ☑ 是 / ☐ 否 |
接收数据 → 检查缓冲区长度 → 达到固定长度 → 提交完整帧
↓
未达到长度 → 继续等待
| 参数 | 说明 | 示例 |
|---|---|---|
| 帧长度 | 每帧的固定字节数 | 8、16、32等 |
⚠️ 数据丢失风险:如果实际帧长与配置不符,可能导致:
建议:先通过数据捕获分析实际帧结构。
接收数据 → 搜索帧头 → 找到帧头 → 搜索帧尾 → 找到帧尾 → 提取中间内容
↓ ↓ ↓
未找到 保留数据 继续等待
| 参数 | 说明 | 示例 |
|---|---|---|
| 帧头 | 帧开始标记(可选) | "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"
说明:支持头尾相同的标记
接收数据 → 应用正则匹配 → 找到匹配 → 提取捕获组 → 提交帧
↓
未找到匹配 → 继续等待更多数据
| 参数 | 说明 | 示例 |
|---|---|---|
| 正则表达式 | 匹配模式 | ^\\d{4}-\\d{2}-\\d{2} |
| 忽略大小写 | 是否区分大小写 | ☑ 是 |
接收数据 → JSON语法分析 → 找到完整JSON对象 → 提交帧
↓
不完整JSON → 继续等待更多数据
接收数据 → XML解析器 → 寻找完整元素 → 提交帧
↓
不完整XML → 继续等待
接收数据 → 解析HEX正则表达式 → 匹配二进制模式 → 找到完整帧 → 提交帧
↓
未找到匹配 → 滑动窗口继续搜索
| 参数 | 说明 | 示例 |
|---|---|---|
| 表达式 | HEX正则表达式 | AA55@U8<type>@U16<length>@U8[$(length)] |
# 帧头 + 命令 + 长度 + 数据 + 校验
AA55 # 固定帧头
@U8 # 命令字
@U16<LENGTH> # 数据长度
@U8[$(LENGTH)] # 变长数据
@U16 # CRC校验
# 头部长度 + 头部 + 载荷长度 + 载荷
@U8<HLEN> # 头部长度
@U8[$(HLEN)] # 头部数据
@U16<DATALEN> # 载荷长度
@U8[$(DATALEN)] # 载荷数据
# 魔数 + 版本验证 + 序列号 + 数据
AABBCCDD # 魔数
@U8 # 版本必须为1
@U32 # 序列号
@U16<DATALEN> # 数据长度
@U8[$(DATALEN)] # 实际数据
数据转发是Bittly平台的连接桥接功能,允许您在相同类型的通信连接之间建立单向数据通道。当两个连接都支持相同的数据流模式(文本或HEX)时,可以将源连接接收的数据转发到目标连接。
| 参数 | 说明 | 必填 | 示例 |
|---|---|---|---|
| 目标连接 | 接收转发数据的连接 | 是 | TCP客户端_192.168.1.100:8080 |
注意:
心跳用于在串口连接建立后,按照固定时间间隔向设备发送指定数据,以满足设备协议对心跳的要求,或用于周期性检测设备是否正常响应。
心跳不会影响串口的物理连接状态,仅负责定时发送数据。
心跳支持以下三种模式,三选一,互斥:
切换模式后,仅当前模式生效,其余模式配置将被忽略。
以字符串形式发送心跳内容。
ping\r / \n)ping\r\nping
以十六进制字节流形式发送心跳内容。
AA BB CC DD0xAA 形式aa 与 AA 等价)AA BB CC DD
通过脚本自定义心跳发送逻辑。
heartbeat 函数export async function heartbeat() {
$this.write('PING');
}
$this:当前连接连接对象$this.write(data):向连接发送数据
data 支持字符串或 HEX 字节数组(依据实际接口定义)以下任一情况将关闭心跳:
[#include "variable.md"]
[#include "message.md"]
在 Windows 系统上, 您需要预先将设备与系统进行配对,并进行串口映射, 以便 Bittly 能够识别设备并进行连接。
这里以 Windows 11 系统连接 HC-06 蓝牙模块为例:
打开 Windows 蓝牙置,扫描并将 HC-06 蓝牙模块配对连接。

选择 更多蓝牙设置 将 HC-06 蓝牙模块映射为串口。

完成串口映射后, 您便可以在 Bittly 中选择对应的蓝牙设备并通过配置的串口进行数据通信。