蓝牙设备对接文档

设备类型

广播设备与点对点设备的分类与区别

设备分类

系统根据 API 返回的 deviceModel 字段中的 connectionTypeisPrivateisAuth 三个关键属性,将设备分为不同类别并决定对应的通信方式和协议。

关键字段说明

connectionType — 连接类型

类型说明蓝牙搜索
0点对点 (P2P)通过 WebBLE / Capacitor BLE 建立 GATT 直连需要
1广播 (Broadcast)通过 BLE Advertising 广播服务发送指令不需要

isPrivate — 协议类型

说明
0使用标准十六进制命令,直接将命令字符串转为字节数组发送
1使用私有协议 (PrivateProtocol),命令需要通过 AB 帧格式编码

isPrivate === 1 时,命令会通过 bleCommandModes 映射表查找对应的电机强度参数,再由 PrivateProtocol.makeMotorControlCombined() 生成控制帧。

isAuth — 鉴权需求

说明
0无需鉴权,连接成功后即可发送命令
1需要 CRC8 握手鉴权。连接后设备发送 0xBA 0x00 鉴权请求,App 计算 CRC-8 并回复鉴权帧

鉴权流程:

  1. 建立 GATT 连接
  2. 订阅 Notify 特征
  3. 设备发送鉴权通知 0xBA 0x00 [data...]
  4. App 计算 CRC-8(多项式 0x1D,初始值 0xFF,最终异或 0xFF
  5. App 回复鉴权帧 0xAB 0x00 [CRC] 0xFF 0xFF
  6. 鉴权成功,可以发送控制命令

设备型号前缀路由

系统根据设备型号名称(modelName)的前缀自动选择对应的命令集和协议:

型号匹配规则使用协议/命令集说明
默认(不匹配其他规则)Default 命令集标准十六进制命令,前缀 6db643ce97fe427c
型号含 NNGNONONNG/NONO 命令集前缀 6DB64324F89D427C
型号含 MYMY 命令集前缀 AA0B,10 字符命令
型号含 VxMiAmorlinkvexVxMi Protocol步进电机设备,使用 UART Service
型号含 EasyEasyLiveEasyLive ProtocolEasy 系列设备
// 协议选择逻辑示例(deviceModes.ts)
export const getClassicModes = (modelName: string | undefined) => {
  if (!modelName) {
    return modes           // 默认协议
  }
  if (modelName.includes('NNG') || modelName.includes('NONO')) {
    return nngModes        // NNG/NONO 协议
  }
  if (modelName.includes('MY')) {
    return myModes          // MY 协议
  }
  return modes              // 默认协议
}

设备数据结构

设备信息通过 API 获取,完整的数据结构如下:

Device(设备)

interface Device {
  id: string                    // 设备唯一 ID
  name: string                  // 设备名称(如 "深渊兽"、"汐月")
  pic: string                   // 设备图片 URL
  deviceCode: string            // 设备码(用于查询)
  deviceModel: DeviceModel      // 设备型号信息
  func: DeviceFunction[]        // 通用功能指令(用于所有业务场景)
  modes: DeviceMode[]           // 经典模式指令(仅用于经典模式 UI)
}

DeviceModel(设备型号)

interface DeviceModel {
  id: string
  modelName: string             // 型号名称(如 "ML_BD_001"、"ICPBCM_4")
  connectionType: number        // 0 = 点对点, 1 = 广播
  isPrivate: number             // 0 = 标准命令, 1 = 私有协议
  isAuth: number                // 0 = 无需鉴权, 1 = 需要 CRC8 握手
  serviceUUID: string | null
  writeUUID: string | null
  readUUID: string | null
}

DeviceFunction(设备功能)

interface DeviceFunction {
  key: string                   // 功能键(如 "vibrate", "thrust", "suction")
  name: string                  // 功能名称(如 "振动", "抽插", "吮吸")
  sort: number                  // 排序(决定电机映射位置)
  maxIntensity: number          // 最大强度档位(如 3、9)
  commands: FunctionCommand[]   // 命令列表
}

interface FunctionCommand {
  intensity: number             // 强度档位(-1 = 停止, 0-9 = 档位)
  command: string               // 蓝牙命令(十六进制字符串)
  name: string                  // 命令名称
}

DeviceMode(设备模式)

interface DeviceMode {
  name: string                  // 模式名称(如 "抽插", "吮吸")
  sort: number                  // 排序
  commands: ModeCommand[]       // 模式命令列表
}

interface ModeCommand {
  sort: number                  // 排序(0 或 -1 = 停止命令)
  command: string               // 蓝牙命令
  name: string                  // 命令名称
}

func 与 modes 的区别

funcmodes 是设备数据中两种不同用途的指令集:

func(通用功能指令)

用于所有业务场景中需要联动设备的功能控制,包括但不限于:

  • 视频通话互动
  • 远程控制
  • 游戏联动
  • 音乐节奏同步
  • 自定义模式

func 提供基于强度档位的精细控制能力,通常由滑块或档位按钮触发。每个功能通过 key 标识(如 vibratethrustsuction),通过 intensity 值(-1 到 maxIntensity)选择对应命令。

modes(经典模式指令)

仅用于经典模式 UI 中的预设模式切换。用户通过点击预设模式按钮快速切换设备的工作模式,无需手动调节强度。

每个模式分组(如"抽插"、"吮吸")包含多个预设命令,通过 sort 值标识(sort <= 0 为停止命令)。

对比表

属性func(通用功能指令)modes(经典模式指令)
用途通用功能控制经典模式控制
适用范围所有业务场景仅经典模式 UI
数据结构Map<functionKey, intensity>Set<"modeIndex-cmdIndex">
命令定位通过 key + intensity通过 modeIndex + cmdIndex
位置计算func.sort - 1mode.sort - 1
停止检测intensity === -1intensity === 0sort <= 0
增量发送不支持(全量发送)支持(仅发送变化的模式)
UI 形态强度滑块/档位按钮预设模式按钮

业务场景对应关系

业务场景使用指令说明
经典模式modes用户点击预设模式按钮切换
视频通话互动func根据互动强度实时控制设备
远程控制func控制方调节强度,被控方设备响应
游戏联动func根据游戏事件触发设备响应
音乐节奏同步func根据音乐节拍控制设备强度
自定义模式func用户自定义的强度曲线控制

设备示例数据

点对点设备示例

// 点对点设备(需要蓝牙搜索配对)
const pointToPointDevice: Device = {
  id: "ada70143-...",
  name: "白色恋人",
  deviceCode: "1586",
  deviceModel: {
    modelName: "PH-001",
    connectionType: 0,        // 点对点
    isPrivate: 1,             // 使用私有协议
    isAuth: 0,                // 无需鉴权
  },
  func: [
    {
      name: "抽插",
      key: "thrust",
      maxIntensity: 9,
      sort: 1,
      commands: [
        { intensity: -1, command: "0", name: "停止" },
        { intensity: 1, command: "1", name: "档位1" },
        // ... 档位 2-9
      ]
    }
  ],
  modes: [
    {
      name: "抽插",
      sort: 1,
      commands: [
        { sort: -1, command: "0", name: "停止" },
        { sort: 21, command: "21", name: "模式一" },
        { sort: 22, command: "22", name: "模式二" },
        // ... 模式三至模式八
      ]
    }
  ]
}

广播设备示例

// 广播设备(无需蓝牙搜索)
const broadcastDevice: Device = {
  id: "fcb29cc6-...",
  name: "MonstroRush",
  deviceCode: "1504",
  deviceModel: {
    modelName: "MIT-010",
    connectionType: 1,        // 广播
    isPrivate: 0,             // 标准十六进制命令
    isAuth: 0,                // 无需鉴权
  },
  func: [
    {
      name: "组合模式",
      key: "combination",
      maxIntensity: 9,
      sort: 1,
      commands: [
        { intensity: -1, command: "6db643ce97fe427ce5157d", name: "停止" },
        { intensity: 1, command: "6db643ce97fe427cf41d7c", name: "档位1" },
        // ... 档位 2-9
      ]
    }
  ],
  modes: [
    {
      name: "抽插",
      sort: 3,
      commands: [
        { sort: -1, command: "6db643ce97fe427ce5157d", name: "停止" },
        { sort: 1, command: "6db643ce97fe427cd41f5d", name: "模式1" },
        // ... 模式 2-9
      ]
    }
  ]
}

命令类型说明

设备的命令字符串根据格式不同,处理方式也不同:

命令格式示例处理方式
数字命令"1", "9"通过 PrivateProtocol 转换为电机控制帧
模式命令"21", "28"通过 PrivateProtocol 转换为电机控制帧
AB 前缀命令"AB0401FFFF"直接转为 Uint8Array 发送(特殊功能命令)
22 位十六进制"6db643ce97fe427cf41d7c"广播设备直接使用,点对点设备通过映射表转换
// 命令转换入口(getBlePayloadCommandByCode)
// 1. isPrivate === 1 → 使用 'default' 映射表
// 2. isPrivate === 0 → 根据 modelName 匹配映射表
// 3. 找到映射 → 提取电机强度 → PrivateProtocol.makeMotorControlCombined()
// 4. 未找到映射 → 将十六进制字符串直接转为 Uint8Array