ct/app/components/blueToothTest/item1.vue

1284 lines
44 KiB
Vue
Raw Normal View History

2025-10-08 09:43:52 +08:00
<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>