1091 lines
24 KiB
Vue
1091 lines
24 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 状态显示 -->
|
||
<view class="status-bar">
|
||
<view class="status-item">
|
||
<text class="status-label">蓝牙适配器</text>
|
||
<text class="status-value" :class="adapterState.discovering ? 'active' : ''">
|
||
{{ adapterState.discovering ? '搜索中' : adapterState.available ? '就绪' : '不可用' }}
|
||
</text>
|
||
</view>
|
||
<view class="status-item">
|
||
<text class="status-label">当前连接</text>
|
||
<text class="status-value" :class="connected ? 'connected' : ''">
|
||
{{ connected ? connectedDeviceName || '已连接' : '未连接' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮区域 -->
|
||
<view class="action-section">
|
||
<button type="primary" @click="initBluetooth" :disabled="isInitializing">
|
||
{{ isInitializing ? '初始化中...' : '初始化蓝牙' }}
|
||
</button>
|
||
<button type="warn" v-if="adapterState.available" @click="closeBluetooth">
|
||
关闭蓝牙
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 搜索控制 -->
|
||
<view class="action-section" v-if="adapterState.available">
|
||
<button v-if="!adapterState.discovering" type="primary" @click="startDiscovery">
|
||
开始搜索
|
||
</button>
|
||
<button v-else type="default" @click="stopDiscovery">
|
||
停止搜索
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 设备列表 -->
|
||
<view class="section">
|
||
<view class="section-header">
|
||
<text class="section-title">发现设备 ({{ deviceList.length }})</text>
|
||
<text class="section-subtitle">点击设备连接</text>
|
||
</view>
|
||
<view class="device-list" v-if="deviceList.length > 0">
|
||
<view
|
||
class="device-item"
|
||
v-for="device in deviceList"
|
||
:key="device.deviceId"
|
||
:class="{ connecting: connectingDeviceId === device.deviceId }"
|
||
@click="connectDevice(device)"
|
||
>
|
||
<view class="device-info">
|
||
<text class="device-name">{{ device.name || '未知设备' }}</text>
|
||
<text class="device-id">{{ device.deviceId }}</text>
|
||
</view>
|
||
<view class="device-signal">
|
||
<text class="signal-rssi" v-if="device.RSSI">RSSI: {{ device.RSSI }}</text>
|
||
<text class="signal-status" v-if="connectingDeviceId === device.deviceId">连接中...</text>
|
||
<text class="signal-status connected" v-else-if="connectedDeviceId === device.deviceId">已连接</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="empty-tip" v-else>
|
||
<text>暂无发现设备,请确保目标设备正在广播</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 已连接设备信息 -->
|
||
<view class="section" v-if="connected">
|
||
<view class="section-header">
|
||
<text class="section-title">已连接设备</text>
|
||
<button class="disconnect-btn" size="mini" type="warn" @click="disconnectDevice">
|
||
断开连接
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 服务列表 -->
|
||
<view class="service-list" v-if="serviceList.length > 0">
|
||
<view
|
||
class="service-item"
|
||
v-for="service in serviceList"
|
||
:key="service.uuid"
|
||
:class="{ active: selectedService && selectedService.uuid === service.uuid }"
|
||
@click="selectService(service)"
|
||
>
|
||
<text class="service-name">{{ service.isPrimary ? '[主服务]' : '[服务]' }} {{ service.uuid }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 特征值列表 -->
|
||
<view class="characteristic-list" v-if="characteristicList.length > 0">
|
||
<view
|
||
class="characteristic-item"
|
||
v-for="char in characteristicList"
|
||
:key="char.uuid"
|
||
>
|
||
<view class="char-info">
|
||
<text class="char-uuid">{{ char.uuid }}</text>
|
||
<view class="char-properties">
|
||
<text v-if="char.properties.read" class="prop-tag">可读</text>
|
||
<text v-if="char.properties.write" class="prop-tag write">可写</text>
|
||
<text v-if="char.properties.notify" class="prop-tag notify">通知</text>
|
||
<text v-if="char.properties.indicate" class="prop-tag indicate">指示</text>
|
||
</view>
|
||
</view>
|
||
<view class="char-actions">
|
||
<button
|
||
v-if="char.properties.read"
|
||
size="mini"
|
||
type="primary"
|
||
@click="readCharacteristic(char)"
|
||
>
|
||
读取
|
||
</button>
|
||
<button
|
||
v-if="char.properties.notify || char.properties.indicate"
|
||
size="mini"
|
||
type="default"
|
||
:class="{ active: notifiedCharacteristics.includes(char.uuid) }"
|
||
@click="toggleNotify(char)"
|
||
>
|
||
{{ notifiedCharacteristics.includes(char.uuid) ? '已订阅' : '订阅' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 写入数据 -->
|
||
<view class="write-section" v-if="characteristicList.length > 0 && writeCharacteristicId">
|
||
<view class="write-header">
|
||
<text class="write-title">发送数据</text>
|
||
<text class="write-target">目标: {{ writeCharacteristicId }}</text>
|
||
</view>
|
||
|
||
<!-- 模式切换 -->
|
||
<view class="mode-switch">
|
||
<view
|
||
class="mode-btn"
|
||
:class="{ active: writeMode === 'text' }"
|
||
@click="writeMode = 'text'"
|
||
>
|
||
文本
|
||
</view>
|
||
<view
|
||
class="mode-btn"
|
||
:class="{ active: writeMode === 'hex' }"
|
||
@click="writeMode = 'hex'"
|
||
>
|
||
16进制
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 文本输入 -->
|
||
<input
|
||
v-if="writeMode === 'text'"
|
||
class="write-input"
|
||
v-model="writeTextValue"
|
||
placeholder="输入要发送的文本数据"
|
||
/>
|
||
|
||
<!-- 16进制输入 -->
|
||
<input
|
||
v-else
|
||
class="write-input"
|
||
v-model="writeHexValue"
|
||
placeholder="输入16进制数据(如: 0102FF)"
|
||
/>
|
||
|
||
<button size="default" type="primary" @click="writeCharacteristic">
|
||
发送
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日志区域 -->
|
||
<view class="section log-section">
|
||
<view class="section-header">
|
||
<text class="section-title">操作日志</text>
|
||
<button class="clear-btn" size="mini" @click="clearLog">清空</button>
|
||
</view>
|
||
<scroll-view class="log-list" scroll-y>
|
||
<text class="log-item" v-for="(log, index) in logList" :key="index" :class="log.type">
|
||
[{{ log.time }}] {{ log.message }}
|
||
</text>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
// 蓝牙适配器状态
|
||
adapterState: {
|
||
available: false,
|
||
discovering: false
|
||
},
|
||
isInitializing: false,
|
||
|
||
// 设备列表
|
||
deviceList: [],
|
||
connectingDeviceId: '',
|
||
connectedDeviceId: '',
|
||
connectedDeviceName: '',
|
||
connected: false,
|
||
|
||
// 服务和特征值
|
||
serviceList: [],
|
||
characteristicList: [],
|
||
selectedService: null,
|
||
writeCharacteristicId: '',
|
||
writeServiceId: '',
|
||
|
||
// 订阅的特征值
|
||
notifiedCharacteristics: [],
|
||
|
||
// 写入数据
|
||
writeMode: 'text', // 'text' | 'hex'
|
||
writeTextValue: '',
|
||
writeHexValue: '',
|
||
|
||
// 日志
|
||
logList: []
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.initBluetooth()
|
||
this.setupListeners()
|
||
},
|
||
onUnload() {
|
||
this.cleanup()
|
||
},
|
||
methods: {
|
||
// 添加日志
|
||
addLog(message, type = 'info') {
|
||
const now = new Date()
|
||
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
|
||
this.logList.unshift({ time, message, type })
|
||
// 限制日志数量
|
||
if (this.logList.length > 100) {
|
||
this.logList.pop()
|
||
}
|
||
console.log(`[BLE] ${message}`)
|
||
},
|
||
|
||
// 清空日志
|
||
clearLog() {
|
||
this.logList = []
|
||
},
|
||
|
||
// 设置监听器
|
||
setupListeners() {
|
||
// 监听适配器状态变化
|
||
uni.onBluetoothAdapterStateChange((res) => {
|
||
this.addLog(`适配器状态变化: available=${res.available}, discovering=${res.discovering}`)
|
||
this.adapterState.available = res.available
|
||
this.adapterState.discovering = res.discovering
|
||
|
||
if (!res.available) {
|
||
this.addLog('蓝牙适配器不可用', 'error')
|
||
this.stopDiscovery()
|
||
}
|
||
})
|
||
|
||
// 监听发现设备
|
||
uni.onBluetoothDeviceFound((res) => {
|
||
const devices = res.devices || []
|
||
devices.forEach(device => {
|
||
// 过滤有效的 BLE 设备
|
||
if (!device.name && !device.localName) {
|
||
return
|
||
}
|
||
|
||
// 去重
|
||
const index = this.deviceList.findIndex(d => d.deviceId === device.deviceId)
|
||
if (index === -1) {
|
||
this.deviceList.push(device)
|
||
this.addLog(`发现设备: ${device.name || device.localName} (RSSI: ${device.RSSI || 'N/A'})`)
|
||
} else {
|
||
// 更新信号强度
|
||
this.deviceList[index].RSSI = device.RSSI
|
||
}
|
||
})
|
||
})
|
||
|
||
// 监听连接状态变化
|
||
uni.onBLEConnectionStateChange((res) => {
|
||
this.addLog(`连接状态变化: ${res.deviceId}, connected=${res.connected}`)
|
||
|
||
if (!res.connected) {
|
||
// 设备断开连接
|
||
if (this.connectedDeviceId === res.deviceId) {
|
||
this.connected = false
|
||
this.connectedDeviceId = ''
|
||
this.connectedDeviceName = ''
|
||
this.serviceList = []
|
||
this.characteristicList = []
|
||
this.notifiedCharacteristics = []
|
||
this.addLog('设备已断开连接', 'warn')
|
||
}
|
||
}
|
||
})
|
||
|
||
// 监听特征值变化
|
||
uni.onBLECharacteristicValueChange((res) => {
|
||
const hex = this.ab2hex(res.value)
|
||
this.addLog(`收到数据: ${hex}`, 'success')
|
||
|
||
// 可以在此处理收到的数据
|
||
uni.showToast({
|
||
title: `收到: ${hex.substring(0, 10)}...`,
|
||
icon: 'none',
|
||
duration: 1500
|
||
})
|
||
})
|
||
},
|
||
|
||
// 初始化蓝牙适配器
|
||
initBluetooth() {
|
||
if (this.isInitializing) return
|
||
|
||
this.isInitializing = true
|
||
this.addLog('正在初始化蓝牙适配器...')
|
||
|
||
uni.openBluetoothAdapter({
|
||
success: (res) => {
|
||
this.addLog('蓝牙适配器初始化成功')
|
||
this.adapterState.available = true
|
||
this.isInitializing = false
|
||
|
||
// 获取适配器状态
|
||
this.getAdapterState()
|
||
|
||
// 获取已发现的设备(可能是之前连接过的)
|
||
this.getKnownDevices()
|
||
},
|
||
fail: (err) => {
|
||
this.isInitializing = false
|
||
this.adapterState.available = false
|
||
this.addLog(`初始化失败: ${err.errMsg}`, 'error')
|
||
|
||
uni.showModal({
|
||
title: '蓝牙初始化失败',
|
||
content: err.errMsg || '请检查蓝牙是否开启',
|
||
showCancel: false
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 获取适配器状态
|
||
getAdapterState() {
|
||
uni.getBluetoothAdapterState({
|
||
success: (res) => {
|
||
this.adapterState.available = res.available
|
||
this.adapterState.discovering = res.discovering
|
||
this.addLog(`适配器状态: available=${res.available}, discovering=${res.discovering}`)
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`获取状态失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 获取已发现的设备
|
||
getKnownDevices() {
|
||
uni.getBluetoothDevices({
|
||
success: (res) => {
|
||
if (res.devices && res.devices.length > 0) {
|
||
this.deviceList = res.devices.filter(d => d.name || d.localName)
|
||
this.addLog(`获取到 ${this.deviceList.length} 个已发现设备`)
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`获取设备列表失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 关闭蓝牙
|
||
closeBluetooth() {
|
||
this.cleanup()
|
||
uni.closeBluetoothAdapter({
|
||
success: () => {
|
||
this.addLog('蓝牙适配器已关闭')
|
||
this.adapterState.available = false
|
||
this.adapterState.discovering = false
|
||
}
|
||
})
|
||
},
|
||
|
||
// 开始搜索
|
||
startDiscovery() {
|
||
if (this.adapterState.discovering) {
|
||
this.addLog('已经在搜索中...')
|
||
return
|
||
}
|
||
|
||
this.deviceList = [] // 清空设备列表
|
||
this.addLog('开始搜索 BLE 设备...')
|
||
|
||
uni.startBluetoothDevicesDiscovery({
|
||
allowDuplicatesKey: true, // 允许重复发现
|
||
success: (res) => {
|
||
this.addLog('开始搜索成功')
|
||
this.adapterState.discovering = true
|
||
|
||
// 设置超时自动停止
|
||
setTimeout(() => {
|
||
if (this.adapterState.discovering) {
|
||
this.stopDiscovery()
|
||
}
|
||
}, 30000) // 30秒后自动停止
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`搜索失败: ${err.errMsg}`, 'error')
|
||
|
||
// 处理特定错误
|
||
if (err.errMsg && err.errMsg.includes('Location')) {
|
||
uni.showModal({
|
||
title: '需要位置权限',
|
||
content: 'BLE 蓝牙扫描需要位置权限,请在系统设置中开启位置服务',
|
||
showCancel: false
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
// 停止搜索
|
||
stopDiscovery() {
|
||
uni.stopBluetoothDevicesDiscovery({
|
||
success: () => {
|
||
this.addLog('已停止搜索')
|
||
this.adapterState.discovering = false
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`停止搜索失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 连接设备
|
||
connectDevice(device) {
|
||
if (this.connected && this.connectedDeviceId === device.deviceId) {
|
||
this.addLog('已经连接此设备')
|
||
return
|
||
}
|
||
|
||
// 先断开之前的连接
|
||
if (this.connected) {
|
||
this.disconnectDevice()
|
||
}
|
||
|
||
this.connectingDeviceId = device.deviceId
|
||
this.addLog(`正在连接: ${device.name || device.localName || device.deviceId}...`)
|
||
|
||
uni.createBLEConnection({
|
||
deviceId: device.deviceId,
|
||
timeout: 10000,
|
||
success: (res) => {
|
||
this.addLog('连接成功')
|
||
this.connected = true
|
||
this.connectedDeviceId = device.deviceId
|
||
this.connectedDeviceName = device.name || device.localName
|
||
this.connectingDeviceId = ''
|
||
|
||
// 获取设备服务
|
||
this.getDeviceServices()
|
||
|
||
// 设置 MTU
|
||
this.setMTU()
|
||
},
|
||
fail: (err) => {
|
||
this.connectingDeviceId = ''
|
||
this.addLog(`连接失败: ${err.errMsg}`, 'error')
|
||
|
||
uni.showToast({
|
||
title: '连接失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 设置 MTU
|
||
setMTU() {
|
||
uni.setBLEMTU({
|
||
deviceId: this.connectedDeviceId,
|
||
mtu: 512,
|
||
success: (res) => {
|
||
this.addLog(`MTU 设置成功: ${res.mtu}`)
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`MTU 设置失败: ${err.errMsg}`, 'warn')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 断开连接
|
||
disconnectDevice() {
|
||
if (!this.connectedDeviceId) return
|
||
|
||
this.addLog('正在断开连接...')
|
||
|
||
// 先停止所有通知
|
||
this.notifiedCharacteristics = []
|
||
|
||
uni.closeBLEConnection({
|
||
deviceId: this.connectedDeviceId,
|
||
success: () => {
|
||
this.addLog('已断开连接')
|
||
this.connected = false
|
||
this.connectedDeviceId = ''
|
||
this.connectedDeviceName = ''
|
||
this.serviceList = []
|
||
this.characteristicList = []
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`断开连接失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 获取设备服务
|
||
getDeviceServices() {
|
||
this.addLog('获取设备服务...')
|
||
|
||
uni.getBLEDeviceServices({
|
||
deviceId: this.connectedDeviceId,
|
||
success: (res) => {
|
||
console.log('res.services', res)
|
||
this.serviceList = res.services
|
||
this.addLog(`获取到 ${res.services.length} 个服务`)
|
||
|
||
// 默认选择第一个主服务
|
||
const primaryService = res.services.find(s => s.isPrimary)
|
||
if (primaryService) {
|
||
this.selectService(primaryService)
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`获取服务失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 选择服务
|
||
selectService(service) {
|
||
this.selectedService = service
|
||
this.characteristicList = []
|
||
this.writeCharacteristicId = ''
|
||
this.addLog(`选择服务: ${service.uuid}`)
|
||
|
||
uni.getBLEDeviceCharacteristics({
|
||
deviceId: this.connectedDeviceId,
|
||
serviceId: service.uuid,
|
||
success: (res) => {
|
||
this.characteristicList = res.characteristics
|
||
this.addLog(`获取到 ${res.characteristics.length} 个特征值`)
|
||
|
||
// 默认选择第一个可写的特征值
|
||
const writableChar = res.characteristics.find(c => c.properties.write)
|
||
if (writableChar) {
|
||
this.writeCharacteristicId = writableChar.uuid
|
||
this.writeServiceId = service.uuid
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`获取特征值失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 读取特征值
|
||
readCharacteristic(char) {
|
||
this.addLog(`读取特征值: ${char.uuid}`)
|
||
|
||
uni.readBLECharacteristicValue({
|
||
deviceId: this.connectedDeviceId,
|
||
serviceId: this.selectedService.uuid,
|
||
characteristicId: char.uuid,
|
||
success: () => {
|
||
this.addLog('读取请求已发送,等待数据...')
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`读取失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 切换通知订阅
|
||
toggleNotify(char) {
|
||
const isNotified = this.notifiedCharacteristics.includes(char.uuid)
|
||
|
||
if (isNotified) {
|
||
// 取消订阅
|
||
this.stopNotify(char)
|
||
} else {
|
||
// 订阅
|
||
this.startNotify(char)
|
||
}
|
||
},
|
||
|
||
// 开始订阅通知
|
||
startNotify(char) {
|
||
this.addLog(`订阅通知: ${char.uuid}`)
|
||
|
||
uni.notifyBLECharacteristicValueChange({
|
||
deviceId: this.connectedDeviceId,
|
||
serviceId: this.selectedService.uuid,
|
||
characteristicId: char.uuid,
|
||
state: true,
|
||
success: () => {
|
||
this.notifiedCharacteristics.push(char.uuid)
|
||
this.addLog('订阅成功,等待数据推送...')
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`订阅失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 停止订阅通知
|
||
stopNotify(char) {
|
||
this.addLog(`取消订阅: ${char.uuid}`)
|
||
|
||
uni.notifyBLECharacteristicValueChange({
|
||
deviceId: this.connectedDeviceId,
|
||
serviceId: this.selectedService.uuid,
|
||
characteristicId: char.uuid,
|
||
state: false,
|
||
success: () => {
|
||
const index = this.notifiedCharacteristics.indexOf(char.uuid)
|
||
if (index > -1) {
|
||
this.notifiedCharacteristics.splice(index, 1)
|
||
}
|
||
this.addLog('已取消订阅')
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`取消订阅失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
// 写入特征值
|
||
writeCharacteristic() {
|
||
if (!this.writeCharacteristicId) {
|
||
uni.showToast({
|
||
title: '请先选择可写特征值',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
let buffer
|
||
let displayData = ''
|
||
|
||
if (this.writeMode === 'text') {
|
||
// 文本模式
|
||
if (!this.writeTextValue) {
|
||
uni.showToast({ title: '请输入文本数据', icon: 'none' })
|
||
return
|
||
}
|
||
buffer = this.stringToBuffer(this.writeTextValue)
|
||
displayData = this.writeTextValue
|
||
this.addLog(`发送文本: "${displayData}"`)
|
||
} else {
|
||
// 16进制模式
|
||
if (!this.writeHexValue) {
|
||
uni.showToast({ title: '请输入16进制数据', icon: 'none' })
|
||
return
|
||
}
|
||
const hexStr = this.writeHexValue.replace(/\s/g, '')
|
||
if (!/^[0-9A-Fa-f]+$/.test(hexStr) || hexStr.length % 2 !== 0) {
|
||
uni.showToast({ title: '请输入有效的16进制数据', icon: 'none' })
|
||
return
|
||
}
|
||
buffer = this.hexToBuffer(hexStr)
|
||
displayData = hexStr
|
||
this.addLog(`发送16进制: ${displayData}`)
|
||
}
|
||
|
||
uni.writeBLECharacteristicValue({
|
||
deviceId: this.connectedDeviceId,
|
||
serviceId: this.writeServiceId,
|
||
characteristicId: this.writeCharacteristicId,
|
||
value: buffer,
|
||
writeType: 'writeNoResponse',
|
||
success: () => {
|
||
this.addLog('发送成功', 'success')
|
||
uni.showToast({ title: '发送成功', icon: 'success' })
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`发送失败: ${err.errMsg}`, 'error')
|
||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||
}
|
||
})
|
||
},
|
||
|
||
// 清理
|
||
cleanup() {
|
||
if (this.connected) {
|
||
this.disconnectDevice()
|
||
}
|
||
if (this.adapterState.discovering) {
|
||
this.stopDiscovery()
|
||
}
|
||
this.deviceList = []
|
||
},
|
||
|
||
// ArrayBuffer 转 16进制字符串
|
||
ab2hex(buffer) {
|
||
const hexArr = Array.prototype.map.call(
|
||
new Uint8Array(buffer),
|
||
bit => ('00' + bit.toString(16)).slice(-2)
|
||
)
|
||
return hexArr.join(' ')
|
||
},
|
||
|
||
// 16进制字符串转 ArrayBuffer
|
||
hexToBuffer(hex) {
|
||
const buffer = new ArrayBuffer(hex.length / 2)
|
||
const dataView = new DataView(buffer)
|
||
for (let i = 0; i < hex.length; i += 2) {
|
||
dataView.setUint8(i / 2, parseInt(hex.substr(i, 2), 16))
|
||
}
|
||
return buffer
|
||
},
|
||
|
||
// 字符串转 ArrayBuffer
|
||
stringToBuffer(str) {
|
||
const buffer = new ArrayBuffer(str.length)
|
||
const dataView = new DataView(buffer)
|
||
for (let i = 0; i < str.length; i++) {
|
||
dataView.setUint8(i, str.charCodeAt(i))
|
||
}
|
||
return buffer
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.container {
|
||
padding: 20rpx;
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.status-bar {
|
||
display: flex;
|
||
background: #fff;
|
||
padding: 20rpx;
|
||
border-radius: 12rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.status-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.status-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.status-value {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #666;
|
||
}
|
||
|
||
.status-value.active {
|
||
color: #007aff;
|
||
}
|
||
|
||
.status-value.connected {
|
||
color: #34c759;
|
||
}
|
||
|
||
.action-section {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.action-section button {
|
||
flex: 1;
|
||
}
|
||
|
||
.section {
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.section-subtitle {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.device-list {
|
||
max-height: 400rpx;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.device-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
border-bottom: 1rpx solid #eee;
|
||
}
|
||
|
||
.device-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.device-item:active {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.device-item.connecting {
|
||
background-color: #e8f4ff;
|
||
}
|
||
|
||
.device-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.device-name {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
}
|
||
|
||
.device-id {
|
||
font-size: 22rpx;
|
||
color: #999;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.device-signal {
|
||
text-align: right;
|
||
}
|
||
|
||
.signal-rssi {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
display: block;
|
||
}
|
||
|
||
.signal-status {
|
||
font-size: 24rpx;
|
||
color: #007aff;
|
||
}
|
||
|
||
.signal-status.connected {
|
||
color: #34c759;
|
||
}
|
||
|
||
.empty-tip {
|
||
text-align: center;
|
||
padding: 60rpx 0;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.disconnect-btn {
|
||
margin: 0;
|
||
}
|
||
|
||
.service-list {
|
||
max-height: 300rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.service-item {
|
||
padding: 24rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 8rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.service-item:active {
|
||
background: #eee;
|
||
}
|
||
|
||
.service-item.active {
|
||
background: #e8f4ff;
|
||
border: 1rpx solid #007aff;
|
||
margin-left: -1rpx;
|
||
margin-right: -1rpx;
|
||
}
|
||
|
||
.service-name {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.service-list {
|
||
max-height: 250rpx;
|
||
overflow-y: auto;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.characteristic-list {
|
||
max-height: 400rpx;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.characteristic-item {
|
||
padding: 20rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 8rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.char-info {
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.char-uuid {
|
||
font-size: 24rpx;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.char-properties {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.prop-tag {
|
||
font-size: 20rpx;
|
||
padding: 4rpx 12rpx;
|
||
background: #007aff;
|
||
color: #fff;
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.prop-tag.write {
|
||
background: #5856d6;
|
||
}
|
||
|
||
.prop-tag.notify {
|
||
background: #34c759;
|
||
}
|
||
|
||
.prop-tag.indicate {
|
||
background: #ff9500;
|
||
}
|
||
|
||
.char-actions {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.char-actions button {
|
||
flex: 1;
|
||
}
|
||
|
||
.char-actions button.active {
|
||
background: #34c759;
|
||
color: #fff;
|
||
}
|
||
|
||
.write-section {
|
||
margin-top: 20rpx;
|
||
padding-top: 20rpx;
|
||
border-top: 1rpx solid #eee;
|
||
}
|
||
|
||
.write-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.write-title {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.write-target {
|
||
font-size: 22rpx;
|
||
color: #666;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.mode-switch {
|
||
display: flex;
|
||
margin-bottom: 12rpx;
|
||
border-radius: 8rpx;
|
||
overflow: hidden;
|
||
border: 1rpx solid #007aff;
|
||
}
|
||
|
||
.mode-btn {
|
||
flex: 1;
|
||
padding: 16rpx;
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
background: #fff;
|
||
color: #007aff;
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: #007aff;
|
||
color: #fff;
|
||
}
|
||
|
||
.write-input {
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
padding: 16rpx;
|
||
font-size: 28rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.write-section button {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.log-section {
|
||
min-height: 300rpx;
|
||
}
|
||
|
||
.clear-btn {
|
||
margin: 0;
|
||
}
|
||
|
||
.log-list {
|
||
height: 400rpx;
|
||
background: #1c1c1c;
|
||
border-radius: 8rpx;
|
||
padding: 16rpx;
|
||
}
|
||
|
||
.log-item {
|
||
display: block;
|
||
font-size: 22rpx;
|
||
font-family: 'Courier New', monospace;
|
||
color: #fff;
|
||
margin-bottom: 8rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.log-item.success {
|
||
color: #34c759;
|
||
}
|
||
|
||
.log-item.warn {
|
||
color: #ff9500;
|
||
}
|
||
|
||
.log-item.error {
|
||
color: #ff3b30;
|
||
}
|
||
</style>
|