快速开始
快速接入蓝牙设备控制的入门指南
接入流程概览
蓝牙设备接入分为三步:获取设备数据 -> 建立连接 -> 发送控制命令。
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)
}
}第三步:发送控制命令
根据连接类型选择对应的发送方式。命令字符串来自设备数据中的 func 或 modes 字段。
基本命令发送
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()进行模糊匹配 - 默认协议回退:当设备的
func和modes数组均为空时,系统会根据modelName自动选择deviceModes.ts中的默认协议命令集