蓝牙设备对接文档

广播协议详情

BLE 广播数据包的编码格式与结构

广播数据载荷

广播指令以 Hex 字符串形式存储在 deviceModes.ts 中,发送时需要转为字节数组并编码到 BLE Advertisement 数据包中。Android 和 iOS 使用不同的编码方式(详见平台差异)。

Android — Manufacturer Specific Data

Android 使用 BLE Advertisement 的 Manufacturer Specific Data(类型 0xFF)字段携带指令数据:

Advertisement Data:
┌───────────────────────────────────────────┐
│ AD Structure                              │
├────────────┬────────────┬─────────────────┤
│ Length (1B)│ Type (1B)  │ Data            │
│            │ 0xFF       │                 │
│            │            ├────────┬────────┤
│            │            │ MfgID  │ Payload│
│            │            │ (2B LE)│ (N B)  │
│            │            │ 0xFF00 │ 命令   │
└────────────┴────────────┴────────┴────────┘
  • AD Type: 0xFF(Manufacturer Specific Data)
  • Manufacturer ID: 255(0x00FF,Little-Endian 存储为 0xFF 0x00
  • Payload: Hex 命令字符串转换后的字节数组

广播参数

参数说明
Manufacturer ID255 (0x00FF)固定制造商标识
Instance ID1固定实例 ID,避免资源泄漏
Connectabletrue广播可连接
Timeout0由 App 控制停止(800ms 自动停止)
ModeLOW_LATENCY高功率低延迟模式
TX PowerHIGH最大发射功率

Service UUID

Android 广播额外携带一个固定的 Service UUID,用于标识广播来源:

0000FADE-0000-1000-8000-00805F9B34FB

此 UUID 不携带指令数据,仅作为广播身份标识。

Hex 字符串转字节数组

BluetoothService.hexStringToBytes() 负责将 Hex 字符串转为字节数组:

public static hexStringToBytes(hexString: string): Uint8Array {
  // 1. 移除空格并转为小写
  const cleanHex = hexString.replace(/\s/g, '').toLowerCase()

  // 2. 确保长度为偶数(奇数时前补 0)
  const normalizedHex = cleanHex.length % 2 === 0 ? cleanHex : `0${cleanHex}`

  // 3. 每 2 个字符解析为 1 个字节
  const bytes = new Uint8Array(normalizedHex.length / 2)
  for (let i = 0; i < normalizedHex.length; i += 2) {
    bytes[i / 2] = Number.parseInt(normalizedHex.substring(i, i + 2), 16)
  }
  return bytes
}

编码示例

发送 Default 设备模式 1 指令 6db643ce97fe427ce49c6c

Hex 字符串:  6db643ce97fe427ce49c6c
字节数组:    [0x6D, 0xB6, 0x43, 0xCE, 0x97, 0xFE,
              0x42, 0x7C, 0xE4, 0x9C, 0x6C]
载荷长度:    11 字节

广播指令发送流程

// 1. 清理 Hex 字符串
const cleanedCommand = command.replace(/\s/g, '')

// 2. 转为字节数组
const dataBytes = BluetoothService.hexStringToBytes(cleanedCommand)

// 3. 转为数字数组(BLE Advertiser 要求)
const dataArray = Array.from(dataBytes)

// 4. 调用原生广播 API
await bleAdvertiser.startAdvertising({
  manufacturerId: 255,
  data: dataArray,
  instanceId: 1
})

命令到电机强度映射

bleCommandModes 定义了广播命令与电机强度的映射关系。当设备使用点对点连接时,此映射用于将广播命令转换为 PrivateProtocol 的电机控制参数。

type BleCommandMode = {
  command: string      // 广播命令 (Hex)
  motors: number[]     // 电机强度 [电机1, 电机2, 电机3],0=停止,1-9=强度
}

Default 设备命令映射

export const bleCommandModes = {
  'default': [
    // 停止命令
    { command: '6db643ce97fe427ce5157d', motors: [0, 0, 0] },

    // 长指令(modes): 模式 1-9
    { command: '6db643ce97fe427ce49c6c', motors: [1, 1, 1] },
    { command: '6db643ce97fe427ce7075e', motors: [2, 2, 2] },
    // ... 档位 3-9

    // 短指令(func): 档位 1-9
    { command: '6db643ce97fe427cf41d7c', motors: [1, 1, 1] },
    { command: '6db643ce97fe427cf7864e', motors: [2, 2, 2] },
    // ... 档位 3-9
  ]
}

getBlePayloadCommand

将高层命令参数转为 PrivateProtocol 电机控制帧:

export const getBlePayloadCommand = (
  modelName: string,
  data: { amplitude: number, vibration: number, i3: number }
): Uint8Array => {
  return PrivateProtocol.makeMotorControlCombined(
    data.amplitude,  // 电机 1 强度
    data.vibration,  // 电机 2 强度
    data.i3          // 电机 3 强度
  )
}

getBlePayloadCommandByCode

将广播命令字符串转为可通过蓝牙发送的 Uint8Array。该函数同时支持广播模式和点对点模式的命令转换:

export const getBlePayloadCommandByCode = (
  modelName: string,
  isPrivate: any,
  command: string
): Uint8Array => {
  // 1. 确定协议类型
  let model: string
  if (isPrivate) {
    model = 'default'
  } else {
    // 根据 modelName 匹配 bleCommandModes 的键
    model = Object.keys(bleCommandModes).find(m =>
      modelName.toLowerCase().includes(m.toLowerCase())
    )
  }

  // 2. 在映射表中查找命令
  const commands = model ? bleCommandModes[model] : undefined
  const startCommand = commands?.find(
    m => m.command.toLowerCase() === command.toLowerCase()
  )

  // 3a. 找到映射 → 通过 PrivateProtocol 生成控制帧
  if (commands && startCommand) {
    const [amplitude, vibration, i3] = startCommand.motors
    return getBlePayloadCommand(modelName, { amplitude, vibration, i3 })
  }

  // 3b. 未找到映射 → 直接将 Hex 字符串转为字节数组
  const hexStr = command.replace(/\s/g, '')
  const bytes = new Uint8Array(hexStr.length / 2)
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hexStr.slice(i * 2, i * 2 + 2), 16)
  }
  return bytes
}

命令转换流程图

输入: modelName, isPrivate, command (字符串)


判断协议类型

    ├── isPrivate → model = 'default'

    └── 非 Private → 根据 modelName 匹配


在 bleCommandModes[model] 中查找 command

    ├── 找到 → 提取 motors 数组
    │         → PrivateProtocol.makeMotorControlCombined()
    │         → 输出 Uint8Array

    └── 未找到 → 直接 Hex 转 Uint8Array
              → 输出 Uint8Array

不同设备类型的数据编码

设备类型命令格式编码方式载荷大小
Default22 字符 Hex (6db6...)Manufacturer Data 字节数组11 字节
NNG / NONO22 字符 Hex (6DB6...)Manufacturer Data 字节数组11 字节
MY10 字符 Hex (AA0B...)Manufacturer Data 字节数组5 字节
PrivateProtocol 设备命令映射 → 电机参数PrivateProtocol.makeMotorControlCombined()协议帧大小