私有协议 (PrivateProtocol)
基于 0xAB 帧格式的设备控制协议
概述
PrivateProtocol 是面向私有协议设备(isPrivate === 1)的 BLE GATT 通信协议。它使用 0xAB 作为控制帧头、0xBA 作为通知帧头,支持多电机独立控制和加热控制。
BLE UUID
| 特征 | UUID |
|---|---|
| Service | 0000ff00-0000-1000-8000-00805f9b34fb |
| Write | 0000ff02-0000-1000-8000-00805f9b34fb |
| Notify | 0000ff01-0000-1000-8000-00805f9b34fb |
// PrivateProtocol.ts
public static readonly SERVICE_UUID = '0000ff00-0000-1000-8000-00805f9b34fb'
public static readonly WRITE_UUID = '0000ff02-0000-1000-8000-00805f9b34fb'
public static readonly NOTIFY_UUID = '0000ff01-0000-1000-8000-00805f9b34fb'帧格式
控制帧(App -> 设备)
所有控制帧以 0xAB 开头,紧跟命令类型字节:
[0xAB] [命令类型] [数据...]
命令类型:
0x00 = 认证
0x01 = 电机控制
0x02 = 加热控制
0x04 = 特殊功能(如出油控制)通知帧(设备 -> App)
所有设备通知帧以 0xBA 开头:
[0xBA] [通知类型] [数据...]
通知类型:
0x00 = 认证通知
0x01 = 状态通知认证帧 (0x00)
详细的认证流程请参阅 设备认证。
// 认证响应帧: 0xAB 0x00 [CRC8] 0xFF 0xFF
public static makeAuth(crc: number): Uint8Array {
return new Uint8Array([
PrivateProtocol.NEW_DATA0, // 0xAB
0x00,
crc,
0xFF,
0xFF
])
}电机控制帧 (0x01)
基本格式
字节 [0]: 0xAB -- 帧头
字节 [1]: 0x01 -- 电机控制类型
字节 [2]: Motor1 强度 (0-10)
字节 [3]: Motor2 强度 (0-10)
字节 [4]: Motor3 强度 (0-10)强度范围
每个电机的强度值范围为 0-10:
0= 停止1-10= 强度等级
固定三电机控制
makeMotorControlCombined 方法构造三电机控制帧。当 i2 或 i3 为 0 时,会自动同步为 i1 的值:
public static makeMotorControlCombined(
i1: number, i2: number, i3: number
): Uint8Array {
if (i2 === 0 || i3 === 0) {
i2 = i3 = i1
}
const b2 = (i1 < 0) ? 0x00 : Math.max(0, Math.min(10, Math.round(i1 % 10)))
const b3 = (i2 < 0) ? 0x00 : Math.max(0, Math.min(10, Math.round(i2 % 10)))
const b4 = (i3 < 0) ? 0x00 : Math.max(0, Math.min(10, Math.round(i3 % 10)))
return new Uint8Array([
PrivateProtocol.NEW_DATA0, // 0xAB
0x01,
b2, b3, b4
])
}示例
// 三个电机全部设为强度 5
AB 01 05 05 05
// 电机 1 强度 3,其他停止
AB 01 03 00 00
// 所有电机停止
AB 01 00 00 00动态数组模式
makeMotorControlFromArray 支持动态长度的电机控制,适用于任意数量电机的设备:
/**
* 根据指令序号数组创建电机控制组合帧(动态长度)
* @param commandArray 指令序号数组,例如 [0, 1, 4] 或 [0, 1, 4, 2, 3]
* @returns Uint8Array 蓝牙命令字节数组
*/
public static makeMotorControlFromArray(commandArray: number[]): Uint8Array {
if (!commandArray || commandArray.length === 0) {
return new Uint8Array([
PrivateProtocol.NEW_DATA0, // 0xAB
0x01
])
}
const motorValues: number[] = []
for (let i = 0; i < commandArray.length; i++) {
const value = commandArray[i] ?? 0
// 限制在 0x00-0xFF (0-255) 范围内
const processedValue = (value < 0) ? 0x00 : Math.min(0xFF, Math.round(value))
motorValues.push(processedValue)
}
return new Uint8Array([
PrivateProtocol.NEW_DATA0, // 0xAB
0x01,
...motorValues
])
}
// 示例:
// 输入: [0, 1, 4] => Uint8Array([0xAB, 0x01, 0x00, 0x01, 0x04])
// 输入: [0, 1, 4, 2, 3] => Uint8Array([0xAB, 0x01, 0x00, 0x01, 0x04, 0x02, 0x03])位置映射规则
func 数据结构中,每个功能通过 sort 字段确定在命令数组中的位置:
func.sort - 1 = 数组位置索引
func 的 intensity = 该位置的电机强度值
示例:
func = [{key: "thrust", sort: 1}, {key: "vibrate", sort: 2}, {key: "suction", sort: 3}]
selectedIntensities = Map([["thrust", 5], ["suction", 3]])
commandArray = [5, 0, 3]
// ^sort=1 ^sort=2(未选中) ^sort=3加热控制帧 (0x02)
字节 [0]: 0xAB -- 帧头
字节 [1]: 0x02 -- 加热控制类型
字节 [2]: 0x01 (开) / 0x00 (关)
字节 [3]: 0xFF -- 填充
字节 [4]: 0xFF -- 填充public static makeHeatControl(on: boolean): Uint8Array {
return new Uint8Array([
PrivateProtocol.NEW_DATA0, // 0xAB
0x02,
on ? 0x01 : 0x00,
0xFF,
0xFF
])
}
// 开启加热: AB 02 01 FF FF
// 关闭加热: AB 02 00 FF FF直接命令
以 "AB" 开头的十六进制字符串命令会直接转换为 Uint8Array 发送,不经过 makeMotorControlFromArray 转换:
// 判断是否为直接命令
function isDirectCommand(command: string): boolean {
return command?.toUpperCase().startsWith('AB')
}
// 直接命令示例:
// "AB0401FFFF" => Uint8Array([0xAB, 0x04, 0x01, 0xFF, 0xFF]) -- 出油开启
// "AB0400FFFF" => Uint8Array([0xAB, 0x04, 0x00, 0xFF, 0xFF]) -- 出油关闭
// "AB010909FF" => Uint8Array([0xAB, 0x01, 0x09, 0x09, 0xFF]) -- 一键暴击设备通知
认证通知 (0xBA 0x00)
连接后设备首先发送认证帧,详见 设备认证:
字节 [0]: 0xBA
字节 [1]: 0x00(认证类型)
字节 [2-3]: 客户编号 (ClientID)
字节 [4-5]: 硬件版本
字节 [6-11]: 软件版本
字节 [12]: 电池电量 (0-100)状态通知 (0xBA 0x01)
设备定期上报运行状态:
字节 [0]: 0xBA
字节 [1]: 0x01(状态类型)
字节 [2]: 电池电量 (0-100)
字节 [3]: Motor1 强度 (0-10)
字节 [4]: Motor2 强度 (0-10)
字节 [5]: Motor3 强度 (0-10)通知解析代码
public static parseDeviceNotification(data: Uint8Array): DeviceNotification | null {
if (!data || data.length < 2) return null
const frame = data[0] & 0xFF
const type = data[1] & 0xFF
// 认证通知: 0xBA 0x00
if (frame === 0xBA && type === 0x00) {
return this.parseAuthNotification(data)
}
// 状态通知: 0xBA 0x01
if (frame === 0xBA && type === 0x01) {
return this.parseStatusNotification(data)
}
return { type: 'unknown', frame, typeCode: type, rawData: data }
}指令生成器
功能指令生成器 (function-command-generator)
用于从 func 数据结构生成蓝牙命令。输入为 Map<functionKey, intensity>:
import { generateFunctionBluetoothCommands } from '@/device/function-command-generator'
// 设备功能列表
const deviceFunctions = [
{ key: "thrust", sort: 1, maxIntensity: 9, commands: [...] },
{ key: "vibrate", sort: 2, maxIntensity: 9, commands: [...] },
{ key: "suction", sort: 3, maxIntensity: 3, commands: [...] }
]
// 选中: 抽插强度5, 吮吸强度2
const selectedIntensities = new Map([["thrust", 5], ["suction", 2]])
const commands = generateFunctionBluetoothCommands(
selectedIntensities,
deviceFunctions,
connectedDevice // { deviceModel: { isPrivate: 1 } }
)处理逻辑:
- 遍历选中的功能,查找对应的
command字符串 - 以
"AB"开头的命令 -> 直接发送(directCommands) - 其他命令 -> 通过
generateFunctionSequenceArray生成位置数组,再通过makeMotorControlFromArray转换 - 合并直接命令和转换后的命令
模式指令生成器 (mode-command-generator)
用于从 modes 数据结构生成蓝牙命令。输入为 Set<"modeIndex-cmdIndex">:
import { generateBluetoothCommands } from '@/device/mode-command-generator'
// 设备模式列表
const deviceModes = [
{ name: "抽插", sort: 1, commands: [
{ sort: 0, command: "0", name: "停止" },
{ sort: 21, command: "21", name: "模式一" },
{ sort: 22, command: "22", name: "模式二" }
]},
{ name: "出油", sort: 3, commands: [
{ sort: 0, command: "AB0400FFFF", name: "关闭出油" },
{ sort: 1, command: "AB0401FFFF", name: "开始出油" }
]}
]
// 选中: 抽插模式一 (modeIndex=0, cmdIndex=0)
const selectedKeys = new Set(["0-0"])
const commands = generateBluetoothCommands(
selectedKeys,
deviceModes,
connectedDevice, // { deviceModel: { isPrivate: 1 } }
previousSelectedKeys // 上次选中(用于增量发送)
)处理逻辑:
- 解析
"modeIndex-cmdIndex"键,定位对应的mode和command - 过滤掉停止指令(
sort === 0或sort < 0),只处理sort > 0的命令 - 以
"AB"开头的命令 -> 直接发送 - 其他命令 -> 通过
generateCommandSequenceArray+makeMotorControlFromArray转换 - 普通设备支持增量发送(只发送变化的模式命令)
命令类型判断
两个生成器共用相同的判断逻辑:
| 命令格式 | 类型 | 处理方式 |
|---|---|---|
"1", "9" | 数字命令 | 通过 makeMotorControlFromArray 转换为私有协议格式 |
"21", "28" | 模式命令 | 通过 makeMotorControlFromArray 转换为私有协议格式 |
"AB0401FFFF" | 直接命令 | 直接转换为 Uint8Array 发送 |
"AB010909FF" | 直接命令 | 直接转换为 Uint8Array 发送 |
发送命令
通过 UnifiedBluetoothManager.sendCommand 发送命令字符串:
public async sendCommand(command: string): Promise<boolean> {
const connectedDevice = useDeviceStore.getState().connectedDevice
const modelName = connectedDevice?.deviceModel?.modelName
const isPrivate = connectedDevice?.deviceModel?.isPrivate
// 自动处理命令转换
const encoded = getBlePayloadCommandByCode(modelName, isPrivate, command)
if (encoded.length === 0) return false
return this.write(encoded, connectedDevice)
}getBlePayloadCommandByCode 的转换逻辑:
- 如果
isPrivate,查找bleCommandModes['default']中的命令映射 - 找到映射 -> 提取电机强度,调用
makeMotorControlCombined - 未找到映射 -> 将命令字符串直接作为十六进制转换为
Uint8Array
完整示例
import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'
// 1. 电机控制 -- 三电机同步强度 5
await unifiedBluetoothManager.sendCommand("5")
// 2. 预设模式 -- 抽插模式一
await unifiedBluetoothManager.sendCommand("21")
// 3. 特殊功能 -- 出油开启(直接命令)
await unifiedBluetoothManager.sendCommand("AB0401FFFF")
// 4. 特殊功能 -- 一键暴击(直接命令)
await unifiedBluetoothManager.sendCommand("AB010909FF")
// 5. 加热控制 -- 开启加热
await unifiedBluetoothManager.sendCommand("AB0201FFFF")
// 6. 停止所有
await unifiedBluetoothManager.sendCommand("0")
// 7. 使用功能指令生成器
import { generateFunctionBluetoothCommands } from '@/device/function-command-generator'
const commands = generateFunctionBluetoothCommands(
new Map([["thrust", 5], ["suction", 2]]),
device.func,
connectedDevice
)
// commands: ["AB010500020000..."](根据设备功能转换)
for (const cmd of commands.filter(c => c)) {
await unifiedBluetoothManager.sendCommand(cmd)
}