ct/app/components/blueToothTest/item1.vue
2025-10-08 09:43:52 +08:00

1284 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
/**
* 蓝牙设备测试页面
* 功能使用Web Bluetooth API连接和控制蓝牙设备
* 支持设备扫描、连接、数据通信、WiFi配置、自定义命令等
*/
// 页面布局配置
definePageMeta({
layout: 'empty' // 使用空布局,不包含头部和底部
})
/**
* 蓝牙设备状态管理
* 用于跟踪蓝牙连接的各种状态信息
*/
const bluetoothState = ref({
isSupported: false, // 浏览器是否支持Web Bluetooth API
isConnected: false, // 是否已连接到设备
isScanning: false, // 是否正在扫描设备
devices: [] as BluetoothDevice[], // 发现的设备列表
selectedDevice: null as BluetoothDevice | null, // 当前选中的设备
server: null as BluetoothRemoteGATTServer | null, // GATT服务器连接
service: null as BluetoothRemoteGATTService | null, // 目标服务
characteristic: null as BluetoothRemoteGATTCharacteristic | null, // 通用特征值
writeCharacteristic: null as BluetoothRemoteGATTCharacteristic | null, // 写入特征值
readCharacteristic: null as BluetoothRemoteGATTCharacteristic | null // 读取特征值
})
/**
* 设备信息管理
* 存储连接设备的基本信息和状态
*/
const deviceInfo = ref({
name: '', // 设备名称
id: '', // 设备唯一标识符
connected: false, // 连接状态
batteryLevel: 0, // 电池电量百分比
signalStrength: 0, // 信号强度
firmwareVersion: '', // 固件版本
deviceType: '', // 设备类型
services: [] as string[] // 设备支持的服务列表
})
/**
* 控制命令管理
* 存储各种设备控制命令的参数
*/
const controlCommands = ref({
ledOn: false, // LED开关状态
motorSpeed: 0, // 电机速度 (0-100)
temperature: 0, // 温度设置 (0-50°C)
humidity: 0, // 湿度设置 (0-100%)
wifiSsid: '', // WiFi网络名称
wifiPassword: '', // WiFi密码
customData: '' // 自定义数据命令
})
/**
* 设备配置管理
* 存储蓝牙设备的服务UUID和通信配置
*/
const deviceConfig = ref({
serviceUuid: '000000ff-0000-1000-8000-00805f9b34fb', // 主服务UUID (小写格式)
writeUuid: '0000ff01-0000-1000-8000-00805f9b34fb', // 写入特征值UUID
readUuid: '0000ff02-0000-1000-8000-00805f9b34fb', // 读取特征值UUID
sequenceControl: 0, // 序列号控制
isEncrypted: false, // 是否启用加密
isChecksum: true // 是否启用校验和
})
/**
* 日志信息管理
* 存储操作日志,用于调试和状态跟踪
*/
const logs = ref<string[]>([])
/**
* 页面初始化
* 检查浏览器对Web Bluetooth API的支持情况
*/
onMounted(() => {
// 检查浏览器是否支持Web Bluetooth API
bluetoothState.value.isSupported = 'bluetooth' in navigator
if (!bluetoothState.value.isSupported) {
addLog('浏览器不支持Web Bluetooth API')
} else {
addLog('Web Bluetooth API 支持正常')
}
})
/**
* 添加日志记录
* @param message 日志消息内容
* 功能将操作日志添加到日志列表包含时间戳最多保留50条记录
*/
const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString() // 获取当前时间
logs.value.unshift(`[${timestamp}] ${message}`) // 将新日志添加到列表开头
// 限制日志数量,避免内存占用过多
if (logs.value.length > 50) {
logs.value = logs.value.slice(0, 50)
}
}
/**
* 扫描所有蓝牙设备
* 功能:扫描附近所有可用的蓝牙设备,不限制服务类型
*/
const scanDevices = async () => {
// 检查浏览器支持
if (!bluetoothState.value.isSupported) {
addLog('浏览器不支持蓝牙')
return
}
try {
bluetoothState.value.isScanning = true
addLog('开始扫描蓝牙设备...')
// 请求设备选择,接受所有设备类型
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true, // 接受所有设备
optionalServices: [
'battery_service', // 电池服务
'device_information', // 设备信息服务
'generic_access' // 通用访问服务
]
})
if (device) {
// 保存选中的设备信息
bluetoothState.value.selectedDevice = device
deviceInfo.value.name = device.name || '未知设备'
deviceInfo.value.id = device.id
addLog(`发现设备: ${deviceInfo.value.name}`)
// 监听设备断开连接事件
device.addEventListener('gattserverdisconnected', onDeviceDisconnected)
}
} catch (error) {
addLog(`扫描失败: ${error}`)
} finally {
bluetoothState.value.isScanning = false
}
}
/**
* 扫描特定服务设备
* 功能:只扫描包含指定服务的蓝牙设备,用于精确匹配目标设备
*/
const scanSpecificService = async () => {
// 检查浏览器支持
if (!bluetoothState.value.isSupported) {
addLog('浏览器不支持蓝牙')
return
}
try {
bluetoothState.value.isScanning = true
addLog('开始扫描特定服务设备...')
// 请求设备选择,只显示包含指定服务的设备
const device = await navigator.bluetooth.requestDevice({
filters: [
{ services: [deviceConfig.value.serviceUuid] } // 过滤条件:包含指定服务
],
optionalServices: [
'battery_service', // 电池服务
'device_information', // 设备信息服务
'generic_access' // 通用访问服务
]
})
if (device) {
// 保存选中的设备信息
bluetoothState.value.selectedDevice = device
deviceInfo.value.name = device.name || '未知设备'
deviceInfo.value.id = device.id
addLog(`发现设备: ${deviceInfo.value.name}`)
// 监听设备断开连接事件
device.addEventListener('gattserverdisconnected', onDeviceDisconnected)
}
} catch (error) {
addLog(`扫描特定服务失败: ${error}`)
} finally {
bluetoothState.value.isScanning = false
}
}
/**
* 连接蓝牙设备
* 功能建立与选中设备的GATT连接并初始化设备服务
*/
const connectDevice = async () => {
// 检查是否已选择设备
if (!bluetoothState.value.selectedDevice) {
addLog('请先选择设备')
return
}
try {
addLog('正在连接设备...')
// 建立GATT连接
bluetoothState.value.server = await bluetoothState.value.selectedDevice.gatt?.connect()
if (bluetoothState.value.server) {
// 更新连接状态
bluetoothState.value.isConnected = true
deviceInfo.value.connected = true
addLog('设备连接成功')
// 初始化设备服务和特征值
await initDevice()
}
} catch (error) {
addLog(`连接失败: ${error}`)
}
}
/**
* 断开设备连接
* 功能:主动断开与设备的连接
*/
const disconnectDevice = async () => {
if (bluetoothState.value.server) {
bluetoothState.value.server.disconnect()
onDeviceDisconnected()
}
}
/**
* 设备断开连接处理
* 功能:处理设备断开连接事件,清理相关状态和资源
*/
const onDeviceDisconnected = () => {
// 重置连接状态
bluetoothState.value.isConnected = false
deviceInfo.value.connected = false
// 清理连接资源
bluetoothState.value.server = null
bluetoothState.value.service = null
bluetoothState.value.characteristic = null
bluetoothState.value.writeCharacteristic = null
bluetoothState.value.readCharacteristic = null
addLog('设备已断开连接')
}
/**
* 初始化设备
* 功能:发现设备服务,获取特征值,启用数据通知,发送初始化命令
*/
const initDevice = async () => {
if (!bluetoothState.value.server) return
try {
addLog('正在初始化设备...')
// 获取设备的所有主要服务
const services = await bluetoothState.value.server.getPrimaryServices()
deviceInfo.value.services = services.map(s => s.uuid)
addLog(`发现 ${services.length} 个服务`)
// 打印所有服务UUID用于调试
services.forEach((service, index) => {
addLog(`服务 ${index + 1}: ${service.uuid}`)
})
// 查找目标服务(不区分大小写匹配)
const targetService = services.find(s =>
s.uuid.toLowerCase() === deviceConfig.value.serviceUuid.toLowerCase()
)
if (targetService) {
bluetoothState.value.service = targetService
addLog('找到目标服务')
// 获取目标服务的所有特征值
const characteristics = await targetService.getCharacteristics()
addLog(`发现 ${characteristics.length} 个特征值`)
// 打印所有特征值UUID用于调试
characteristics.forEach((char, index) => {
addLog(`特征值 ${index + 1}: ${char.uuid}`)
})
// 查找读写特征值(不区分大小写匹配)
bluetoothState.value.writeCharacteristic = characteristics.find(c =>
c.uuid.toLowerCase() === deviceConfig.value.writeUuid.toLowerCase()
)
bluetoothState.value.readCharacteristic = characteristics.find(c =>
c.uuid.toLowerCase() === deviceConfig.value.readUuid.toLowerCase()
)
if (bluetoothState.value.writeCharacteristic) {
addLog('找到写入特征值')
}
if (bluetoothState.value.readCharacteristic) {
addLog('找到读取特征值')
// 启用特征值通知,用于接收设备数据
await bluetoothState.value.readCharacteristic.startNotifications()
bluetoothState.value.readCharacteristic.addEventListener('characteristicvaluechanged', onCharacteristicValueChanged)
addLog('已启用数据通知')
}
// 获取设备基本信息(如电池电量)
await getDeviceInfo()
// 发送初始化命令获取固件版本
await sendCustomData('get_fw')
} else {
addLog('未找到目标服务,尝试获取设备基本信息')
await getDeviceInfo()
// 如果没有目标服务,尝试使用通用访问服务进行通信
const genericAccessService = services.find(s => s.uuid === '00001800-0000-1000-8000-00805f9b34fb')
if (genericAccessService) {
addLog('找到通用访问服务,尝试获取特征值')
const characteristics = await genericAccessService.getCharacteristics()
// 打印通用访问服务的特征值
characteristics.forEach((char, index) => {
addLog(`通用访问特征值 ${index + 1}: ${char.uuid}`)
})
// 查找可写的特征值
const writableChars = characteristics.filter(c => c.properties.write || c.properties.writeWithoutResponse)
if (writableChars.length > 0) {
bluetoothState.value.writeCharacteristic = writableChars[0]
addLog(`使用通用访问服务的写入特征值: ${writableChars[0].uuid}`)
}
// 查找可读的特征值
const readableChars = characteristics.filter(c => c.properties.read || c.properties.notify)
if (readableChars.length > 0) {
bluetoothState.value.readCharacteristic = readableChars[0]
addLog(`使用通用访问服务的读取特征值: ${readableChars[0].uuid}`)
// 尝试读取设备名称
const deviceNameChar = characteristics.find(c => c.uuid === '00002a00-0000-1000-8000-00805f9b34fb')
if (deviceNameChar && deviceNameChar.properties.read) {
try {
const deviceNameValue = await deviceNameChar.readValue()
const deviceName = String.fromCharCode(...new Uint8Array(deviceNameValue.buffer))
deviceInfo.value.name = deviceName
addLog(`读取设备名称: ${deviceName}`)
} catch (readError) {
addLog(`读取设备名称失败: ${readError}`)
}
}
// 尝试读取外观信息
const appearanceChar = characteristics.find(c => c.uuid === '00002a01-0000-1000-8000-00805f9b34fb')
if (appearanceChar && appearanceChar.properties.read) {
try {
const appearanceValue = await appearanceChar.readValue()
const appearance = new DataView(appearanceValue.buffer).getUint16(0, true)
addLog(`设备外观: 0x${appearance.toString(16)}`)
} catch (readError) {
addLog(`读取设备外观失败: ${readError}`)
}
}
// 尝试启用通知(如果支持)
if (readableChars[0].properties.notify) {
try {
await bluetoothState.value.readCharacteristic.startNotifications()
bluetoothState.value.readCharacteristic.addEventListener('characteristicvaluechanged', onCharacteristicValueChanged)
addLog('已启用通用访问服务的数据通知')
} catch (notifyError) {
addLog('无法启用通用访问服务的数据通知')
}
} else {
addLog('该特征值不支持通知功能')
}
}
}
}
} catch (error) {
addLog(`设备初始化失败: ${error}`)
}
}
/**
* 特征值变化处理
* 功能:处理从设备接收到的数据,解析并显示相关信息
* @param event 特征值变化事件
*/
const onCharacteristicValueChanged = (event: Event) => {
const characteristic = event.target as BluetoothRemoteGATTCharacteristic
const value = characteristic.value
if (value) {
// 将接收到的数据转换为文本
const data = new Uint8Array(value.buffer)
const text = String.fromCharCode(...data)
addLog(`收到数据: ${text}`)
// 处理特定的设备响应
if (text.includes('wifi_ok')) {
addLog('WiFi连接成功')
} else if (text.includes('wifi_err')) {
addLog('WiFi连接失败')
} else if (text.includes('fw_version')) {
// 解析固件版本信息
deviceInfo.value.firmwareVersion = text.replace('fw_version:', '')
addLog(`固件版本: ${deviceInfo.value.firmwareVersion}`)
}
}
}
/**
* 获取设备信息
* 功能:读取设备的基本信息,如电池电量等
*/
const getDeviceInfo = async () => {
if (!bluetoothState.value.server) return
try {
// 尝试获取电池服务并读取电池电量
try {
const batteryService = await bluetoothState.value.server.getPrimaryService('battery_service')
const batteryCharacteristic = await batteryService.getCharacteristic('battery_level')
const batteryValue = await batteryCharacteristic.readValue()
deviceInfo.value.batteryLevel = batteryValue.getUint8(0)
addLog(`电池电量: ${deviceInfo.value.batteryLevel}%`)
} catch (batteryError) {
addLog('设备不支持电池服务')
deviceInfo.value.batteryLevel = 0
}
// 尝试获取设备信息服务
try {
const deviceInfoService = await bluetoothState.value.server.getPrimaryService('device_information')
const characteristics = await deviceInfoService.getCharacteristics()
// 尝试读取设备名称
const deviceNameChar = characteristics.find(c => c.uuid === '00002a00-0000-1000-8000-00805f9b34fb')
if (deviceNameChar) {
const deviceNameValue = await deviceNameChar.readValue()
const deviceName = String.fromCharCode(...new Uint8Array(deviceNameValue.buffer))
addLog(`设备名称: ${deviceName}`)
}
// 尝试读取固件版本
const firmwareChar = characteristics.find(c => c.uuid === '00002a26-0000-1000-8000-00805f9b34fb')
if (firmwareChar) {
const firmwareValue = await firmwareChar.readValue()
const firmwareVersion = String.fromCharCode(...new Uint8Array(firmwareValue.buffer))
deviceInfo.value.firmwareVersion = firmwareVersion
addLog(`固件版本: ${firmwareVersion}`)
}
} catch (deviceInfoError) {
addLog('设备不支持设备信息服务')
}
} catch (error) {
addLog(`获取设备信息失败: ${error}`)
}
}
/**
* 发送数据到设备
* 功能:通过写入特征值向设备发送数据
* @param data 要发送的数据Uint8Array格式
* @returns 发送是否成功
*/
const sendDataToDevice = async (data: Uint8Array) => {
if (!bluetoothState.value.writeCharacteristic) {
addLog('写入特征值不可用,尝试查找可写特征值')
// 尝试查找其他可写的特征值
if (bluetoothState.value.server) {
try {
const services = await bluetoothState.value.server.getPrimaryServices()
for (const service of services) {
const characteristics = await service.getCharacteristics()
const writableChars = characteristics.filter(c =>
c.properties.write || c.properties.writeWithoutResponse
)
if (writableChars.length > 0) {
bluetoothState.value.writeCharacteristic = writableChars[0]
addLog(`找到可写特征值: ${writableChars[0].uuid}`)
break
}
}
} catch (error) {
addLog(`查找可写特征值失败: ${error}`)
return false
}
}
if (!bluetoothState.value.writeCharacteristic) {
addLog('未找到任何可写的特征值,无法发送控制命令')
addLog('提示该设备可能不支持通过蓝牙进行控制或者需要特定的服务UUID')
return false
}
}
try {
// 通过写入特征值发送数据
await bluetoothState.value.writeCharacteristic.writeValue(data)
return true
} catch (error) {
addLog(`发送数据失败: ${error}`)
return false
}
}
/**
* 字符串转ArrayBuffer
* 功能将字符串转换为ArrayBuffer格式用于蓝牙数据传输
* @param str 要转换的字符串
* @returns ArrayBuffer格式的数据
*/
const stringToBuffer = (str: string): ArrayBuffer => {
const bytes = new Uint8Array(str.length)
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i) // 将字符转换为ASCII码
}
return bytes.buffer
}
/**
* 发送自定义数据
* 功能:向设备发送自定义字符串数据
* @param data 要发送的字符串数据
*/
const sendCustomData = async (data: string) => {
if (!bluetoothState.value.isConnected) {
addLog('设备未连接')
return
}
try {
// 将字符串转换为ArrayBuffer
const buffer = stringToBuffer(data)
const success = await sendDataToDevice(new Uint8Array(buffer))
if (success) {
addLog(`发送自定义数据: ${data}`)
}
} catch (error) {
addLog(`发送自定义数据失败: ${error}`)
}
}
/**
* 发送WiFi配置
* 功能向设备发送WiFi网络配置信息SSID和密码
*/
const sendWiFiConfig = async () => {
if (!bluetoothState.value.isConnected) {
addLog('设备未连接')
return
}
if (!controlCommands.value.wifiSsid || !controlCommands.value.wifiPassword) {
addLog('请输入WiFi名称和密码')
return
}
try {
addLog('正在发送WiFi配置...')
// 发送WiFi网络名称SSID
const ssidBuffer = stringToBuffer(controlCommands.value.wifiSsid)
await sendDataToDevice(new Uint8Array(ssidBuffer))
// 发送WiFi密码
const passwordBuffer = stringToBuffer(controlCommands.value.wifiPassword)
await sendDataToDevice(new Uint8Array(passwordBuffer))
addLog('WiFi配置发送完成')
} catch (error) {
addLog(`发送WiFi配置失败: ${error}`)
}
}
/**
* 发送控制命令
* 功能向设备发送各种控制命令如LED、电机、温度、湿度等
* @param command 命令类型
* @param value 命令参数值
*/
const sendCommand = async (command: string, value?: number) => {
if (!bluetoothState.value.isConnected) {
addLog('设备未连接')
return
}
try {
let data: Uint8Array
// 根据命令类型构造不同的数据包
switch (command) {
case 'led':
// LED控制0x01 + 状态(0x00关闭/0x01开启)
data = new Uint8Array([0x01, value ? 0x01 : 0x00])
addLog(`发送LED命令: ${value ? '开启' : '关闭'}`)
break
case 'motor':
// 电机控制0x02 + 速度值(0-100)
data = new Uint8Array([0x02, value || 0])
addLog(`发送电机速度: ${value}%`)
break
case 'temperature':
// 温度控制0x03 + 温度值(0-50)
data = new Uint8Array([0x03, value || 0])
addLog(`设置温度: ${value}°C`)
break
case 'humidity':
// 湿度控制0x04 + 湿度值(0-100)
data = new Uint8Array([0x04, value || 0])
addLog(`设置湿度: ${value}%`)
break
default:
addLog(`未知命令: ${command}`)
return
}
// 发送构造好的数据包
await sendDataToDevice(data)
} catch (error) {
addLog(`发送命令失败: ${error}`)
}
}
/**
* 控制LED开关
* 功能切换LED灯的开关状态
*/
const toggleLED = () => {
controlCommands.value.ledOn = !controlCommands.value.ledOn
sendCommand('led', controlCommands.value.ledOn ? 1 : 0)
}
/**
* 控制电机速度
* 功能:设置电机运行速度
* @param speed 电机速度 (0-100)
*/
const setMotorSpeed = (speed: number) => {
controlCommands.value.motorSpeed = speed
sendCommand('motor', speed)
}
/**
* 设置温度
* 功能:设置设备的目标温度
* @param temp 目标温度 (0-50°C)
*/
const setTemperature = (temp: number) => {
controlCommands.value.temperature = temp
sendCommand('temperature', temp)
}
/**
* 设置湿度
* 功能:设置设备的目标湿度
* @param humidity 目标湿度 (0-100%)
*/
const setHumidity = (humidity: number) => {
controlCommands.value.humidity = humidity
sendCommand('humidity', humidity)
}
/**
* 发送自定义命令
* 功能:发送用户输入的自定义数据命令
*/
const sendCustomCommand = () => {
if (controlCommands.value.customData) {
sendCustomData(controlCommands.value.customData)
}
}
/**
* 读取通用访问服务信息
* 功能:读取通用访问服务的所有可用信息
*/
const readGenericAccessInfo = async () => {
if (!bluetoothState.value.isConnected) {
addLog('设备未连接')
return
}
try {
addLog('读取通用访问服务信息...')
// 获取通用访问服务
const genericAccessService = await bluetoothState.value.server.getPrimaryService('00001800-0000-1000-8000-00805f9b34fb')
const characteristics = await genericAccessService.getCharacteristics()
for (const char of characteristics) {
addLog(`特征值: ${char.uuid}`)
addLog(`属性: ${JSON.stringify(char.properties)}`)
// 根据特征值UUID读取对应信息
if (char.properties.read) {
try {
const value = await char.readValue()
switch (char.uuid) {
case '00002a00-0000-1000-8000-00805f9b34fb': // 设备名称
const deviceName = String.fromCharCode(...new Uint8Array(value.buffer))
deviceInfo.value.name = deviceName
addLog(`设备名称: ${deviceName}`)
break
case '00002a01-0000-1000-8000-00805f9b34fb': // 外观
const appearance = new DataView(value.buffer).getUint16(0, true)
addLog(`设备外观: 0x${appearance.toString(16)}`)
break
case '00002a02-0000-1000-8000-00805f9b34fb': // 隐私标志
const privacyFlag = new DataView(value.buffer).getUint8(0)
addLog(`隐私标志: ${privacyFlag}`)
break
case '00002a03-0000-1000-8000-00805f9b34fb': // 重新连接地址
const reconnectAddress = Array.from(new Uint8Array(value.buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join(':')
addLog(`重新连接地址: ${reconnectAddress}`)
break
case '00002a04-0000-1000-8000-00805f9b34fb': // 外设首选连接参数
const params = new DataView(value.buffer)
const minInterval = params.getUint16(0, true)
const maxInterval = params.getUint16(2, true)
const latency = params.getUint16(4, true)
const timeout = params.getUint16(6, true)
addLog(`连接参数: 最小间隔=${minInterval}ms, 最大间隔=${maxInterval}ms, 延迟=${latency}, 超时=${timeout}ms`)
break
case '00002aa6-0000-1000-8000-00805f9b34fb': // 中央地址解析
const centralAddressResolution = new DataView(value.buffer).getUint8(0)
addLog(`中央地址解析: ${centralAddressResolution ? '支持' : '不支持'}`)
break
default:
// 其他特征值,尝试作为文本读取
const text = String.fromCharCode(...new Uint8Array(value.buffer))
addLog(`未知特征值数据: ${text}`)
break
}
} catch (readError) {
addLog(`读取特征值 ${char.uuid} 失败: ${readError}`)
}
}
}
} catch (error) {
addLog(`读取通用访问服务信息失败: ${error}`)
}
}
/**
* 查找可写特征值
* 功能:在所有服务中查找支持写入操作的特征值
*/
const findWritableCharacteristics = async () => {
if (!bluetoothState.value.isConnected) {
addLog('设备未连接')
return
}
try {
addLog('查找可写特征值...')
const services = await bluetoothState.value.server.getPrimaryServices()
const writableChars = []
for (const service of services) {
addLog(`检查服务: ${service.uuid}`)
try {
const characteristics = await service.getCharacteristics()
for (const char of characteristics) {
const properties = char.properties
addLog(`特征值: ${char.uuid}`)
addLog(`属性: ${JSON.stringify(properties)}`)
// 检查是否支持写入
if (properties.write || properties.writeWithoutResponse) {
writableChars.push({
serviceUuid: service.uuid,
characteristicUuid: char.uuid,
properties: properties
})
addLog(`✓ 找到可写特征值: ${char.uuid}`)
}
// 检查是否支持读取
if (properties.read) {
addLog(`✓ 找到可读特征值: ${char.uuid}`)
}
// 检查是否支持通知
if (properties.notify) {
addLog(`✓ 找到可通知特征值: ${char.uuid}`)
}
}
} catch (charError) {
addLog(`获取特征值失败: ${charError}`)
}
}
if (writableChars.length > 0) {
addLog(`共找到 ${writableChars.length} 个可写特征值:`)
writableChars.forEach((char, index) => {
addLog(`${index + 1}. 服务: ${char.serviceUuid}`)
addLog(` 特征值: ${char.characteristicUuid}`)
addLog(` 属性: ${JSON.stringify(char.properties)}`)
})
// 使用第一个可写特征值
const firstWritable = writableChars[0]
const service = await bluetoothState.value.server.getPrimaryService(firstWritable.serviceUuid)
const characteristic = await service.getCharacteristic(firstWritable.characteristicUuid)
bluetoothState.value.writeCharacteristic = characteristic
addLog(`已设置写入特征值: ${firstWritable.characteristicUuid}`)
} else {
addLog('未找到任何可写的特征值')
addLog('该设备可能不支持通过蓝牙进行控制')
}
} catch (error) {
addLog(`查找可写特征值失败: ${error}`)
}
}
/**
* 尝试通用设备通信
* 功能:当设备没有特定服务时,尝试使用通用方式进行通信
*/
const tryGenericCommunication = async () => {
if (!bluetoothState.value.isConnected) {
addLog('设备未连接')
return
}
try {
addLog('尝试通用设备通信...')
// 首先读取通用访问服务信息
await readGenericAccessInfo()
// 查找可写特征值
await findWritableCharacteristics()
// 获取所有服务
const services = await bluetoothState.value.server.getPrimaryServices()
for (const service of services) {
addLog(`检查服务: ${service.uuid}`)
try {
const characteristics = await service.getCharacteristics()
for (const char of characteristics) {
addLog(`特征值: ${char.uuid}, 属性: ${JSON.stringify(char.properties)}`)
// 尝试读取可读的特征值
if (char.properties.read) {
try {
const value = await char.readValue()
const data = new Uint8Array(value.buffer)
const text = String.fromCharCode(...data)
addLog(`读取数据: ${text}`)
} catch (readError) {
addLog(`读取失败: ${readError}`)
}
}
// 尝试启用通知
if (char.properties.notify) {
try {
await char.startNotifications()
char.addEventListener('characteristicvaluechanged', onCharacteristicValueChanged)
addLog(`已启用通知: ${char.uuid}`)
} catch (notifyError) {
addLog(`启用通知失败: ${notifyError}`)
}
}
}
} catch (charError) {
addLog(`获取特征值失败: ${charError}`)
}
}
} catch (error) {
addLog(`通用通信失败: ${error}`)
}
}
/**
* 清除日志
* 功能:清空操作日志列表
*/
const clearLogs = () => {
logs.value = []
}
</script>
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
<div class="max-w-6xl mx-auto">
<!-- 页面标题 -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
蓝牙设备测试
</h1>
<p class="text-gray-600 dark:text-gray-300">
Web Bluetooth API 设备连接与控制
</p>
</div>
<!-- 支持状态 -->
<div class="mb-6">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center space-x-2">
<div
class="w-3 h-3 rounded-full"
:class="bluetoothState.isSupported ? 'bg-green-500' : 'bg-red-500'"
></div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
Web Bluetooth API {{ bluetoothState.isSupported ? '支持' : '不支持' }}
</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 设备控制面板 -->
<div class="space-y-6">
<!-- 设备扫描 -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
设备扫描
</h2>
<div class="space-y-4">
<button
@click="scanDevices"
:disabled="bluetoothState.isScanning || !bluetoothState.isSupported"
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
{{ bluetoothState.isScanning ? '扫描中...' : '扫描所有设备' }}
</button>
<button
@click="scanSpecificService"
:disabled="bluetoothState.isScanning || !bluetoothState.isSupported"
class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
{{ bluetoothState.isScanning ? '扫描中...' : '扫描特定服务设备' }}
</button>
<button
@click="connectDevice"
:disabled="!bluetoothState.selectedDevice || bluetoothState.isConnected"
class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
连接设备
</button>
<button
@click="disconnectDevice"
:disabled="!bluetoothState.isConnected"
class="w-full px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
断开连接
</button>
<button
@click="readGenericAccessInfo"
:disabled="!bluetoothState.isConnected"
class="w-full px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
读取通用访问信息
</button>
<button
@click="findWritableCharacteristics"
:disabled="!bluetoothState.isConnected"
class="w-full px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
查找可写特征值
</button>
<button
@click="tryGenericCommunication"
:disabled="!bluetoothState.isConnected"
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
通用设备通信
</button>
</div>
</div>
<!-- 设备信息 -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
设备信息
</h2>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-300">设备名称:</span>
<span class="text-gray-900 dark:text-white">{{ deviceInfo.name || '未连接' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-300">设备ID:</span>
<span class="text-gray-900 dark:text-white text-sm">{{ deviceInfo.id || '未连接' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-300">连接状态:</span>
<span
class="font-medium"
:class="deviceInfo.connected ? 'text-green-600' : 'text-red-600'"
>
{{ deviceInfo.connected ? '已连接' : '未连接' }}
</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-300">电池电量:</span>
<span class="text-gray-900 dark:text-white">{{ deviceInfo.batteryLevel }}%</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-300">固件版本:</span>
<span class="text-gray-900 dark:text-white">{{ deviceInfo.firmwareVersion || '未知' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-300">服务数量:</span>
<span class="text-gray-900 dark:text-white">{{ deviceInfo.services.length }}</span>
</div>
</div>
</div>
<!-- 设备控制 -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
设备控制
</h2>
<div class="space-y-4">
<!-- LED控制 -->
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">LED控制</span>
<button
@click="toggleLED"
:disabled="!bluetoothState.isConnected"
class="px-4 py-2 rounded-lg font-medium transition-colors"
:class="controlCommands.ledOn
? 'bg-yellow-500 hover:bg-yellow-600 text-white'
: 'bg-gray-300 hover:bg-gray-400 text-gray-700'"
>
{{ controlCommands.ledOn ? '关闭' : '开启' }}
</button>
</div>
<!-- 电机速度控制 -->
<div>
<label class="block text-gray-600 dark:text-gray-300 mb-2">
电机速度: {{ controlCommands.motorSpeed }}%
</label>
<input
type="range"
min="0"
max="100"
v-model="controlCommands.motorSpeed"
@input="setMotorSpeed(controlCommands.motorSpeed)"
:disabled="!bluetoothState.isConnected"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
</div>
<!-- 温度控制 -->
<div>
<label class="block text-gray-600 dark:text-gray-300 mb-2">
温度设置: {{ controlCommands.temperature }}°C
</label>
<input
type="range"
min="0"
max="50"
v-model="controlCommands.temperature"
@input="setTemperature(controlCommands.temperature)"
:disabled="!bluetoothState.isConnected"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
</div>
<!-- 湿度控制 -->
<div>
<label class="block text-gray-600 dark:text-gray-300 mb-2">
湿度设置: {{ controlCommands.humidity }}%
</label>
<input
type="range"
min="0"
max="100"
v-model="controlCommands.humidity"
@input="setHumidity(controlCommands.humidity)"
:disabled="!bluetoothState.isConnected"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
</div>
<!-- WiFi配置 -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
WiFi配置
</h2>
<div class="space-y-4">
<div>
<label class="block text-gray-600 dark:text-gray-300 mb-2">WiFi名称</label>
<input
type="text"
v-model="controlCommands.wifiSsid"
placeholder="请输入WiFi名称"
:disabled="!bluetoothState.isConnected"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label class="block text-gray-600 dark:text-gray-300 mb-2">WiFi密码</label>
<input
type="password"
v-model="controlCommands.wifiPassword"
placeholder="请输入WiFi密码"
:disabled="!bluetoothState.isConnected"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<button
@click="sendWiFiConfig"
:disabled="!bluetoothState.isConnected"
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
发送WiFi配置
</button>
</div>
</div>
<!-- 自定义数据 -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
自定义数据
</h2>
<div class="space-y-4">
<div>
<label class="block text-gray-600 dark:text-gray-300 mb-2">自定义命令</label>
<input
type="text"
v-model="controlCommands.customData"
placeholder="请输入自定义命令"
:disabled="!bluetoothState.isConnected"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<button
@click="sendCustomCommand"
:disabled="!bluetoothState.isConnected"
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
发送自定义数据
</button>
</div>
</div>
</div>
<!-- 日志面板 -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
操作日志
</h2>
<button
@click="clearLogs"
class="px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded transition-colors"
>
清除
</button>
</div>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 h-96 overflow-y-auto">
<div class="space-y-1">
<div
v-for="(log, index) in logs"
:key="index"
class="text-sm text-gray-700 dark:text-gray-300 font-mono"
>
{{ log }}
</div>
<div v-if="logs.length === 0" class="text-gray-500 dark:text-gray-400 text-center py-8">
暂无日志信息
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 自定义滚动条 */
.overflow-y-auto::-webkit-scrollbar {
width: 6px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.dark .overflow-y-auto::-webkit-scrollbar-track {
background: #374151;
}
.dark .overflow-y-auto::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dark .overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* 滑块样式 */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type="range"]::-webkit-slider-track {
background: #d1d5db;
height: 8px;
border-radius: 4px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background: #3b82f6;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-track {
background: #d1d5db;
height: 8px;
border-radius: 4px;
border: none;
}
input[type="range"]::-moz-range-thumb {
background: #3b82f6;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
border: none;
}
</style>