设备类型
广播设备与点对点设备的分类与区别
设备分类
系统根据 API 返回的 deviceModel 字段中的 connectionType、isPrivate、isAuth 三个关键属性,将设备分为不同类别并决定对应的通信方式和协议。
关键字段说明
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 并回复鉴权帧 |
鉴权流程:
- 建立 GATT 连接
- 订阅 Notify 特征
- 设备发送鉴权通知
0xBA 0x00 [data...] - App 计算
CRC-8(多项式0x1D,初始值0xFF,最终异或0xFF) - App 回复鉴权帧
0xAB 0x00 [CRC] 0xFF 0xFF - 鉴权成功,可以发送控制命令
设备型号前缀路由
系统根据设备型号名称(modelName)的前缀自动选择对应的命令集和协议:
| 型号匹配规则 | 使用协议/命令集 | 说明 |
|---|---|---|
| 默认(不匹配其他规则) | Default 命令集 | 标准十六进制命令,前缀 6db643ce97fe427c |
型号含 NNG 或 NONO | NNG/NONO 命令集 | 前缀 6DB64324F89D427C |
型号含 MY | MY 命令集 | 前缀 AA0B,10 字符命令 |
型号含 Vx、Mi 或 Amorlinkvex | VxMi Protocol | 步进电机设备,使用 UART Service |
型号含 Easy 或 EasyLive | EasyLive Protocol | Easy 系列设备 |
// 协议选择逻辑示例(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 的区别
func 和 modes 是设备数据中两种不同用途的指令集:
func(通用功能指令)
用于所有业务场景中需要联动设备的功能控制,包括但不限于:
- 视频通话互动
- 远程控制
- 游戏联动
- 音乐节奏同步
- 自定义模式
func 提供基于强度档位的精细控制能力,通常由滑块或档位按钮触发。每个功能通过 key 标识(如 vibrate、thrust、suction),通过 intensity 值(-1 到 maxIntensity)选择对应命令。
modes(经典模式指令)
仅用于经典模式 UI 中的预设模式切换。用户通过点击预设模式按钮快速切换设备的工作模式,无需手动调节强度。
每个模式分组(如"抽插"、"吮吸")包含多个预设命令,通过 sort 值标识(sort <= 0 为停止命令)。
对比表
| 属性 | func(通用功能指令) | modes(经典模式指令) |
|---|---|---|
| 用途 | 通用功能控制 | 经典模式控制 |
| 适用范围 | 所有业务场景 | 仅经典模式 UI |
| 数据结构 | Map<functionKey, intensity> | Set<"modeIndex-cmdIndex"> |
| 命令定位 | 通过 key + intensity | 通过 modeIndex + cmdIndex |
| 位置计算 | func.sort - 1 | mode.sort - 1 |
| 停止检测 | intensity === -1 或 intensity === 0 | sort <= 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