蓝牙设备对接文档

快速开始

快速接入蓝牙设备控制的入门指南

接入流程概览

蓝牙设备接入分为三步:获取设备数据 -> 建立连接 -> 发送控制命令

1. 获取设备数据(API)


2. 判断连接类型(connectionType)

   ├── connectionType === 1 → 广播连接(无需搜索)

   └── connectionType === 0 → 点对点连接(需蓝牙搜索配对)


3. 发送控制命令

   ├── 广播设备 → bluetoothService.broadcastCommand(command)

   └── 点对点设备 → unifiedBluetoothManager.sendCommand(command)

第一步:获取设备数据

通过 API 获取用户绑定的设备列表或通过设备码查询设备信息。

获取绑定设备列表

// GET /api/device/linked
// 返回用户已绑定的设备列表
const response = await fetch('/api/device/linked', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer <token>',
    'Content-Type': 'application/json'
  }
})

const { data } = await response.json()
// data.devices: Device[] — 设备列表

通过设备码查询

// POST /api/device/query
// 通过设备码查询设备信息
const response = await fetch('/api/device/query', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer <token>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ deviceCode: '1586' })
})

const { data } = await response.json()
// data: Device — 设备信息

返回的设备数据结构

// 关键字段
const device = {
  id: "ada70143-...",
  name: "白色恋人",
  deviceModel: {
    modelName: "PH-001",
    connectionType: 0,    // 0=点对点, 1=广播
    isPrivate: 1,         // 0=标准命令, 1=私有协议
    isAuth: 0,            // 0=无需鉴权, 1=需要CRC8握手
  },
  func: [...],            // 通用功能指令
  modes: [...]            // 经典模式指令
}

第二步:判断连接类型并建立连接

根据 device.deviceModel.connectionType 判断使用广播还是点对点连接。

function getConnectionType(device: Device): 'broadcast' | 'point-to-point' {
  const { connectionType } = device.deviceModel
  return connectionType === 1 ? 'broadcast' : 'point-to-point'
}

广播连接(connectionType === 1)

广播设备无需搜索配对,初始化广播服务后即可发送命令。

import { bluetoothService } from '@/utils/bluetooth/BluetoothService'

async function connectBroadcast(device: Device): Promise<boolean> {
  // 1. 初始化广播服务
  const initSuccess = await bluetoothService.initialize()
  if (!initSuccess) {
    console.error('广播服务初始化失败')
    return false
  }

  // 2. 检查蓝牙是否启用
  const bluetoothEnabled = await bluetoothService.checkBluetooth()
  if (!bluetoothEnabled) {
    console.error('蓝牙未启用')
    return false
  }

  // 3. 保存设备状态(广播连接无需实际配对)
  console.log('广播连接成功:', device.name)
  return true
}

点对点连接(connectionType === 0)

点对点设备需要通过蓝牙搜索找到设备并建立 GATT 连接。

import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'

async function connectPointToPoint(device: Device): Promise<boolean> {
  const { modelName, isPrivate, isAuth } = device.deviceModel

  return new Promise((resolve) => {
    // 设置连接超时(15秒)
    const timeoutId = setTimeout(() => {
      console.warn('连接超时')
      unifiedBluetoothManager.stopScan()
      resolve(false)
    }, 15000)

    // 开始扫描并连接
    unifiedBluetoothManager.startScanCallbackByCode(
      modelName,                     // 设备型号名称,用于匹配蓝牙设备
      { isPrivate, isAuth },         // 协议和鉴权信息
      async (foundDevice) => {       // 设备发现回调
        clearTimeout(timeoutId)

        if (foundDevice) {
          console.log('设备连接成功:', foundDevice.name)
          resolve(true)
        } else {
          console.log('设备连接失败')
          resolve(false)
        }
      },
      (devices) => {                 // 扫描完成回调
        console.log('扫描完成,发现设备数:', devices?.length || 0)
      }
    )
  })
}

统一连接入口

async function connectDevice(device: Device): Promise<boolean> {
  const connectionType = getConnectionType(device)

  if (connectionType === 'broadcast') {
    return await connectBroadcast(device)
  } else {
    return await connectPointToPoint(device)
  }
}

第三步:发送控制命令

根据连接类型选择对应的发送方式。命令字符串来自设备数据中的 funcmodes 字段。

基本命令发送

import { bluetoothService } from '@/utils/bluetooth/BluetoothService'
import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'

async function sendDeviceCommand(
  device: Device,
  functionKey: string,
  intensity: number
): Promise<boolean> {
  // 从 func 中查找对应命令
  const func = device.func.find(f => f.key === functionKey)
  const cmd = func?.commands.find(c => c.intensity === intensity)

  if (!cmd) {
    console.error('未找到对应命令:', functionKey, intensity)
    return false
  }

  const { connectionType } = device.deviceModel

  if (connectionType === 1) {
    // 广播设备:通过 BluetoothService 广播
    return await bluetoothService.broadcastCommand(cmd.command)
  } else {
    // 点对点设备:通过 UnifiedBluetoothManager 发送
    return await unifiedBluetoothManager.sendCommand(cmd.command)
  }
}

// 使用示例
await sendDeviceCommand(device, 'vibrate', 5)    // 振动档位5
await sendDeviceCommand(device, 'thrust', 3)     // 抽插档位3
await sendDeviceCommand(device, 'vibrate', -1)   // 停止振动

广播设备命令示例

// 广播设备直接使用十六进制命令
await bluetoothService.broadcastCommand('6db643ce97fe427cf41d7c')  // 档位1
await bluetoothService.broadcastCommand('6db643ce97fe427cf0393a')  // 档位5
await bluetoothService.broadcastCommand('6db643ce97fe427ce5157d')  // 停止

点对点设备命令示例

// 点对点设备通过 sendCommand 自动处理编码
await unifiedBluetoothManager.sendCommand('5')            // 档位5
await unifiedBluetoothManager.sendCommand('AB0401FFFF')   // 特殊功能(出油)
await unifiedBluetoothManager.sendCommand('0')            // 停止

完整 React Hook 示例

以下是一个完整的 useBluetooth Hook,封装了连接、断开和命令发送的逻辑:

import { useState, useCallback, useEffect } from 'react'
import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'
import { bluetoothService } from '@/utils/bluetooth/BluetoothService'

interface UseBluetoothReturn {
  isConnected: boolean
  isConnecting: boolean
  connectedDevice: Device | null
  connect: (device: Device) => Promise<boolean>
  disconnect: () => Promise<void>
  sendCommand: (functionKey: string, intensity: number) => Promise<boolean>
  error: string | null
}

export function useBluetooth(): UseBluetoothReturn {
  const [isConnecting, setIsConnecting] = useState(false)
  const [isConnected, setIsConnected] = useState(false)
  const [connectedDevice, setConnectedDevice] = useState<Device | null>(null)
  const [error, setError] = useState<string | null>(null)

  /**
   * 连接设备
   */
  const connect = useCallback(async (device: Device): Promise<boolean> => {
    setIsConnecting(true)
    setError(null)

    try {
      const { connectionType, isPrivate, isAuth } = device.deviceModel

      if (connectionType === 1) {
        // 广播连接
        const initSuccess = await bluetoothService.initialize()
        if (!initSuccess) {
          throw new Error('广播服务初始化失败')
        }

        const bluetoothEnabled = await bluetoothService.checkBluetooth()
        if (!bluetoothEnabled) {
          throw new Error('请开启蓝牙')
        }

        setConnectedDevice(device)
        setIsConnected(true)
        return true

      } else {
        // 点对点连接
        return new Promise((resolve) => {
          const timeout = setTimeout(() => {
            setError('连接超时,请重试')
            resolve(false)
          }, 15000)

          unifiedBluetoothManager.startScanCallbackByCode(
            device.deviceModel.modelName,
            { isPrivate, isAuth },
            async (foundDevice) => {
              clearTimeout(timeout)

              if (foundDevice) {
                setConnectedDevice(device)
                setIsConnected(true)
                resolve(true)
              } else {
                setError('连接失败,请重试')
                resolve(false)
              }
            },
            () => {}
          )
        })
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : '连接失败'
      setError(message)
      return false
    } finally {
      setIsConnecting(false)
    }
  }, [])

  /**
   * 断开连接
   */
  const disconnect = useCallback(async (): Promise<void> => {
    try {
      // 发送停止命令
      if (connectedDevice) {
        const stopCmd = connectedDevice.func[0]?.commands.find(
          (c: any) => c.intensity === -1
        )
        if (stopCmd) {
          const { connectionType } = connectedDevice.deviceModel
          if (connectionType === 1) {
            await bluetoothService.broadcastCommand(stopCmd.command)
          } else {
            await unifiedBluetoothManager.sendCommand(stopCmd.command)
          }
        }
      }

      // 断开蓝牙
      await unifiedBluetoothManager.disconnect()

      // 更新状态
      setConnectedDevice(null)
      setIsConnected(false)
    } catch (err) {
      console.error('断开连接失败:', err)
    }
  }, [connectedDevice])

  /**
   * 发送命令
   */
  const sendCommand = useCallback(async (
    functionKey: string,
    intensity: number
  ): Promise<boolean> => {
    if (!connectedDevice) {
      setError('未连接设备')
      return false
    }

    const func = connectedDevice.func.find((f: any) => f.key === functionKey)
    const cmd = func?.commands.find((c: any) => c.intensity === intensity)

    if (!cmd) {
      setError('未找到对应命令')
      return false
    }

    try {
      const { connectionType } = connectedDevice.deviceModel

      if (connectionType === 1) {
        return await bluetoothService.broadcastCommand(cmd.command)
      } else {
        return await unifiedBluetoothManager.sendCommand(cmd.command)
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : '命令发送失败'
      setError(message)
      return false
    }
  }, [connectedDevice])

  // 页面卸载时发送停止命令
  useEffect(() => {
    return () => {
      if (isConnected && connectedDevice) {
        const stopCmd = connectedDevice.func[0]?.commands.find(
          (c: any) => c.intensity === -1
        )
        if (stopCmd) {
          const { connectionType } = connectedDevice.deviceModel
          if (connectionType === 1) {
            bluetoothService.broadcastCommand(stopCmd.command).catch(console.error)
          } else {
            unifiedBluetoothManager.sendCommand(stopCmd.command).catch(console.error)
          }
        }
      }
    }
  }, [isConnected, connectedDevice])

  return {
    isConnected,
    isConnecting,
    connectedDevice,
    connect,
    disconnect,
    sendCommand,
    error
  }
}

React 组件示例

使用上面的 useBluetooth Hook 构建一个简单的设备控制界面:

import React, { useState } from 'react'
import { useBluetooth } from './useBluetooth'

interface BluetoothDemoProps {
  devices: Device[]
}

export function BluetoothDemo({ devices }: BluetoothDemoProps) {
  const {
    isConnected,
    isConnecting,
    connectedDevice,
    connect,
    disconnect,
    sendCommand,
    error
  } = useBluetooth()

  const [intensities, setIntensities] = useState<Record<string, number>>({})

  // 处理设备连接
  const handleConnect = async (device: Device) => {
    const success = await connect(device)
    if (success) {
      console.log('连接成功!')
    }
  }

  // 处理强度变更
  const handleIntensityChange = async (functionKey: string, value: number) => {
    setIntensities(prev => ({ ...prev, [functionKey]: value }))
    await sendCommand(functionKey, value)
  }

  // 处理停止
  const handleStop = async () => {
    if (connectedDevice) {
      for (const func of connectedDevice.func) {
        await sendCommand(func.key, -1)
        setIntensities(prev => ({ ...prev, [func.key]: 0 }))
      }
    }
  }

  return (
    <div>
      <h2>蓝牙设备控制 Demo</h2>

      {/* 错误提示 */}
      {error && <div style={{ color: 'red' }}>{error}</div>}

      {/* 未连接状态:显示设备列表 */}
      {!isConnected && (
        <div>
          <h3>可用设备</h3>
          {devices.map(device => (
            <div key={device.id} style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 12 }}>
              <img src={device.pic} alt={device.name} width={48} height={48} />
              <span>{device.name}</span>
              <span>{device.deviceModel.connectionType === 1 ? '广播' : '点对点'}</span>
              <button onClick={() => handleConnect(device)} disabled={isConnecting}>
                {isConnecting ? '连接中...' : '连接'}
              </button>
            </div>
          ))}
        </div>
      )}

      {/* 已连接状态:显示控制面板 */}
      {isConnected && connectedDevice && (
        <div>
          <h3>{connectedDevice.name} - 已连接</h3>

          {/* 功能控制 */}
          {connectedDevice.func.map((func: any) => (
            <div key={func.key} style={{ marginBottom: 16 }}>
              <label>{func.name}</label>
              <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
                {Array.from({ length: func.maxIntensity }, (_, i) => i + 1).map(
                  (level: number) => (
                    <button
                      key={level}
                      onClick={() => handleIntensityChange(func.key, level)}
                      style={{
                        background: intensities[func.key] === level ? '#3b82f6' : '#e5e7eb',
                        color: intensities[func.key] === level ? 'white' : 'black',
                        border: 'none',
                        borderRadius: 8,
                        width: 40,
                        height: 40,
                        cursor: 'pointer'
                      }}
                    >
                      {level}
                    </button>
                  )
                )}
              </div>
            </div>
          ))}

          {/* 操作按钮 */}
          <div style={{ display: 'flex', gap: 12, marginTop: 24 }}>
            <button onClick={handleStop} style={{ background: '#ef4444', color: 'white', padding: '8px 16px', border: 'none', borderRadius: 8 }}>
              停止
            </button>
            <button onClick={disconnect} style={{ background: '#6b7280', color: 'white', padding: '8px 16px', border: 'none', borderRadius: 8 }}>
              断开连接
            </button>
          </div>
        </div>
      )}
    </div>
  )
}

注意事项

  • 命令节流:系统内置 CommandQueueManager 对命令进行 300ms 节流,避免蓝牙通道拥塞
  • 自动停止:广播的短命令在设备端 2 秒后自动停止,经典模式命令会持续执行直到收到停止指令
  • 设备重连:点对点设备的连接信息会持久化到 localStorage(key: ble_last_connected_device),支持断线重连
  • Web 兼容性:Web Bluetooth API 要求 HTTPS 环境,且仅部分浏览器支持(Chrome、Edge、Opera)
  • 平台差异:Android 和 iOS 上同一设备的蓝牙名称可能不同。系统使用 includeBleDeviceName() 进行模糊匹配
  • 默认协议回退:当设备的 funcmodes 数组均为空时,系统会根据 modelName 自动选择 deviceModes.ts 中的默认协议命令集