蓝牙设备对接文档

最佳实践

广播设备的频率控制、节流策略与常见问题

广播策略管理器

BroadcastStrategyManager 是高频广播场景的核心管理器,负责频率控制、强度变化检测、停止指令去重和数据压缩。它封装了 BluetoothService,为上层业务(滑动、音乐、视频、语音等)提供统一的广播入口。

import { BroadcastStrategyManager, BroadcastPresets } from '@/broadcast/broadcast-strategy'

// 创建滑动模式的策略管理器
const manager = new BroadcastStrategyManager(BroadcastPresets.swipe)

// 添加数据点,管理器自动决定是否广播
manager.addDataPoint({ intensity: 5, timestamp: Date.now() })

预设配置

系统提供五种预设配置,针对不同业务场景优化:

预设模式广播间隔相似度阈值压缩比平滑说明
swipe (滑动)150ms0.950.2开启实时响应手势操作
music (音乐)200ms0.850.4开启跟随音频节拍变化
video (视频)200ms0.90.3开启跟随视频内容,过滤伪随机噪音
voice (语音)150ms0.90.3开启实时响应环境声音
performance (高性能)150ms0.80.6关闭最大压缩,最低延迟

配置参数详解

interface BroadcastStrategyConfig {
  maxHistoryPoints: number       // 最大历史数据点数(默认 500)
  similarityThreshold: number    // 相似度阈值 0-1(越高越严格)
  compressionRatio: number       // 压缩比例 0-1
  broadcastInterval: number      // 广播间隔 (ms)
  enableSmoothing: boolean       // 是否启用平滑算法
  enableCompression: boolean     // 是否启用数据压缩
  useSimplifiedMode: boolean     // 是否使用简化模式(减少缓冲延迟)
}

所有预设默认启用 useSimplifiedMode(简化模式),跳过压缩缓冲区直接广播,以获得最低延迟。

频率控制策略

档位变化过滤

核心过滤规则:只在档位变化 >= 1.0 时发送新指令。由于长指令会持续执行直到收到停止指令,相同档位无需重复发送。

private shouldBroadcast(dataPoint: BroadcastDataPoint): boolean {
  // 首次广播,无条件通过
  if (!this.lastBroadcastData) return true

  const intensityDiff = Math.abs(
    dataPoint.intensity - this.lastBroadcastData.intensity
  )

  // 档位变化 >= 1.0 时发送
  if (intensityDiff >= 1.0) return true

  // 相同档位不重复发送(长指令会持续执行)
  return false
}

停止指令去重

停止指令(intensity === 0)在 500ms 内去重,避免重复发送:

if (dataPoint.intensity === 0) {
  if (this.lastBroadcastData?.intensity === 0) {
    const timeDiff = now - this.lastBroadcastData.timestamp
    if (timeDiff < 500) {
      return false  // 500ms 内跳过重复停止指令
    }
  }
  return true  // 允许发送停止指令
}

BluetoothService 层防抖

BluetoothService 内部还有 100ms 防抖机制,非停止指令在 100ms 内的重复请求会被跳过:

// 停止指令绕过防抖,立即发送
if (!isStopCommand && now - this.lastBroadcastTime < 100) {
  return false  // 广播频率过高,跳过
}

视频模式特殊处理

视频模式有额外的去重和噪音过滤规则,针对视频内容的伪随机特性进行优化:

private shouldBroadcastVideo(dataPoint: BroadcastDataPoint): boolean {
  // 停止指令:50ms 内去重(比普通模式更短)
  if (dataPoint.intensity === 0) {
    if (lastIntensity === 0 && timeDiff < 50) return false
    return true
  }

  // 首次广播无条件通过
  if (!this.lastBroadcastData) return true

  // 基本频率控制:20ms 最小间隔
  if (timeDiff < 20) return false

  // 微小变化过滤:强度差 < 0.5 且间隔 < 40ms
  if (intensityDiff < 0.5 && timeDiff < 40) return false

  // 伪随机噪音过滤:强度差 < 1.0 且间隔 < 60ms
  if (intensityDiff < 1.0 && timeDiff < 60) return false

  return true
}

视频模式 vs 普通模式对比

过滤规则普通模式视频模式
最小广播间隔100ms (BluetoothService 防抖)20ms
档位变化阈值>= 1.0渐进式过滤
停止指令去重窗口500ms50ms
微小变化过滤< 0.5 强度差 + < 40ms 间隔
伪随机噪音过滤< 1.0 强度差 + < 60ms 间隔
使用指令类型长指令 (getClassicModes)长指令 (getClassicModes)

自动停止行为

广播自动停止(App 侧)

每次广播在 800ms 后自动停止,释放 BLE 广播资源:

发送广播


启动 800ms 定时器

    ├── 800ms 内发送新指令 → 取消旧定时器 → 发送新广播 → 重新启动 800ms 定时器

    └── 800ms 到期 → 自动调用 stopBroadcasting() → 释放广播资源

设备自动停止(设备侧)

  • 长指令 (Classic Modes): 设备持续执行,必须收到停止指令才停止
  • 短指令 (Short Broadcast Modes): 设备执行约 2 秒后自动停止,无需发送停止指令

BroadcastStrategyManager 内部维护设备状态同步,设置 2 秒定时器模拟设备自动停止:

// 设置 2 秒后的自动停止回调
this.deviceAutoStopTimer = setTimeout(() => {
  this.isDeviceActive = false
  this.handleDeviceAutoStop()  // 同步 UI 状态
}, 2000)

MonstroRush 广播设备示例

MonstroRush(型号 MIT-010connectionType: 1)是一个典型的广播设备,支持组合模式、抽插模式和吮吸模式。

设备特点

  • connectionType: 1 — 广播连接,无需蓝牙搜索配对
  • isPrivate: 0 — 使用标准 Hex 广播命令
  • 所有命令均为 22 位 Hex 字符串
  • 支持多功能(组合、抽插、吮吸)独立控制

完整控制示例

import { bluetoothService } from '@/utils/bluetooth/BluetoothService'
import { useDeviceStore } from '@/stores/device-store'

class MonstroRushController {
  private readonly STOP_COMMAND = "6db643ce97fe427ce5157d"

  private readonly COMBINATION_COMMANDS = [
    "6db643ce97fe427cf5946d", // 基础档
    "6db643ce97fe427cf41d7c", // 档位 1
    "6db643ce97fe427cf7864e", // 档位 2
    "6db643ce97fe427cf60f5f", // 档位 3
    "6db643ce97fe427cf1b02b", // 档位 4
    "6db643ce97fe427cf0393a", // 档位 5
    "6db643ce97fe427cf3a208", // 档位 6
    "6db643ce97fe427cf22b19", // 档位 7
    "6db643ce97fe427cfddce1", // 档位 8
    "6db643ce97fe427cfc55f0"  // 档位 9
  ]

  private readonly THRUST_COMMANDS = [
    "6db643ce97fe427cd5964c", // 基础档
    "6db643ce97fe427cd41f5d", // 模式 1
    "6db643ce97fe427cd7846f", // 模式 2
    "6db643ce97fe427cd60d7e", // 模式 3
    "6db643ce97fe427cd1b20a", // 模式 4
    "6db643ce97fe427cd03b1b", // 模式 5
    "6db643ce97fe427cd3a029", // 模式 6
    "6db643ce97fe427cd22938", // 模式 7
    "6db643ce97fe427cdddec0", // 模式 8
    "6db643ce97fe427cdc57d1"  // 模式 9
  ]

  private readonly SUCTION_COMMANDS = [
    "6db643ce97fe427ca5113f", // 基础档
    "6db643ce97fe427ca4982e", // 模式 1
    "6db643ce97fe427ca7031c", // 模式 2
    "6db643ce97fe427ca68a0d", // 模式 3
    "6db643ce97fe427ca13579", // 模式 4
    "6db643ce97fe427ca0bc68", // 模式 5
    "6db643ce97fe427ca3275a", // 模式 6
    "6db643ce97fe427ca2ae4b", // 模式 7
    "6db643ce97fe427cad59b3", // 模式 8
    "6db643ce97fe427cacd0a2"  // 模式 9
  ]

  /** 设置组合模式档位 (0-9) */
  async setCombinationLevel(level: number): Promise<boolean> {
    return await bluetoothService.broadcastCommand(
      this.COMBINATION_COMMANDS[level]
    )
  }

  /** 设置抽插模式 (0-9) */
  async setThrustMode(mode: number): Promise<boolean> {
    return await bluetoothService.broadcastCommand(
      this.THRUST_COMMANDS[mode]
    )
  }

  /** 设置吮吸模式 (0-9) */
  async setSuctionMode(mode: number): Promise<boolean> {
    return await bluetoothService.broadcastCommand(
      this.SUCTION_COMMANDS[mode]
    )
  }

  /** 停止所有功能 */
  async stopAll(): Promise<boolean> {
    return await bluetoothService.broadcastCommand(this.STOP_COMMAND)
  }
}

// 使用示例
const controller = new MonstroRushController()
await controller.setCombinationLevel(3)   // 组合模式档位 3
await controller.setThrustMode(5)         // 抽插模式 5
await controller.setSuctionMode(1)        // 吮吸模式 1
await controller.stopAll()               // 停止所有

常见问题

1. TOO_MANY_ADVERTISERS 错误

症状: 广播失败,返回错误码 2。

原因: Android 系统限制同时活跃的广播实例数。未正确停止的广播实例会占用系统资源。

解决方案:

  1. 确保始终使用固定的 instanceId = 1
  2. 系统自动执行双重清理(stopAll → 等待 500ms → 再次 stopAll → 等待 200ms → 重试)
  3. 如果自动恢复失败,建议用户关闭并重新打开蓝牙

2. 广播指令丢失或设备无响应

症状: 发送了广播命令但设备没有反应。

可能原因及解决:

  • 广播频率过高 — 100ms 防抖机制跳过了指令。降低发送频率或使用 BroadcastStrategyManager
  • 蓝牙未启用 — 检查 checkBluetooth() 返回值
  • 设备距离过远 — BLE 广播的有效范围通常为 10-30 米
  • 前一个广播未停止 — 互斥锁排队延迟了新指令

3. 停止指令延迟

症状: 发送停止指令后设备响应慢。

原因: 停止指令可能被排在操作队列中等待。

系统优化: broadcastCommand() 对停止指令有特殊优先处理 — 立即取消定时器、清空队列、停止当前广播后再发送停止指令。

4. iOS 设备广播不生效

症状: Android 设备正常,iOS 设备无法控制。

原因: iOS 使用 Service UUID 编码方式,需要确保设备端同时支持 Manufacturer Data(Android)和 Service UUID 列表(iOS)两种解析方式。

检查项:

  • 确认设备固件支持 iOS 编码格式
  • 确认 UUID 编码算法正确

5. 长指令设备不停止

症状: 发送了短指令但设备持续运行不停止。

原因: 可能错误使用了长指令(getClassicModes())而非短指令(getShortBroadcastModes())。长指令需要显式发送停止指令。

解决: 检查使用的指令类型,或在适当时机发送停止指令 getStopCommand(modelName)

6. 视频模式设备频繁启停

症状: 视频模式下设备频繁振动和停止,体验不佳。

原因: 视频伪随机算法产生的小幅抖动触发了频繁广播。

解决: 使用 BroadcastPresets.video 预设配置,已内置微变化过滤和噪音过滤规则。

资源管理

清理策略

在以下时机执行资源清理:

  • App 进入后台
  • 切换设备
  • 遇到广播错误
  • 用户手动断开
// 完整清理流程
await bluetoothService.stopAllAdvertising()

// BroadcastStrategyManager 清理
manager.clear()

实例 ID 管理

始终使用固定 instanceId = 1,避免创建过多实例导致系统资源耗尽。BluetoothService 内部通过 instanceIdCounter = 1 确保每次广播使用相同的实例 ID。