964 lines
36 KiB
Plaintext
964 lines
36 KiB
Plaintext
<template>
|
||
<view class="container">
|
||
<button @click="scanDevices" :disabled="scanning">扫描蓝牙设备</button>
|
||
<view v-if="scanning">正在扫描...</view>
|
||
|
||
<view class="section-title">设备列表</view>
|
||
<view v-if="devices.length">
|
||
<view v-for="item in devices" :key="item.deviceId" class="device-item">
|
||
<view class="device-info">
|
||
<text class="device-name">{{ item.name || '未知设备' }}</text>
|
||
<text class="device-id">({{ item.deviceId }})</text>
|
||
<text :class="['status', item.connected ? 'connected' : '']">{{ item.connected ? '已连接' : '未连接' }}</text>
|
||
</view>
|
||
<view class="device-actions">
|
||
<button v-if="!item.connected" @click="connectDevice(item.deviceId)" class="btn-connect">连接</button>
|
||
<button v-else @click="disconnectDevice(item.deviceId)" class="btn-disconnect">断开</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="empty-list">暂无设备,请点击扫描</view>
|
||
|
||
<view class="section-title">已连接设备({{ connectedDevices.length }})</view>
|
||
<view v-if="connectedDevices.length">
|
||
<view v-for="item in connectedDevices" :key="item.deviceId" class="device-item connected-item">
|
||
<view class="device-info">
|
||
<text class="device-name">{{ item.name || '未知设备' }}</text>
|
||
<text class="device-id">({{ item.deviceId }})</text>
|
||
|
||
<!-- 显示电池状态 -->
|
||
<view v-if="batteryStatus[item.deviceId]" class="battery-status">
|
||
<view class="battery-container">
|
||
<view class="battery-level"
|
||
:style="{width: batteryStatus[item.deviceId].level >= 0 ? batteryStatus[item.deviceId].level + '%' : '0%'}">
|
||
</view>
|
||
<text v-if="batteryStatus[item.deviceId].isCharging" class="charging-icon">⚡</text>
|
||
</view>
|
||
<text class="battery-text">
|
||
{{ batteryStatus[item.deviceId].level >= 0 ? batteryStatus[item.deviceId].level + '%' : '未知' }}
|
||
{{ batteryStatus[item.deviceId].isCharging ? ' (充电中)' : '' }}
|
||
<span v-if="batteryStatus[item.deviceId].chargingStatus"> | 充电状态: {{ batteryStatus[item.deviceId].chargingStatus }}</span>
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="device-actions">
|
||
<button @click="disconnectDevice(item.deviceId)" class="btn-disconnect">断开</button>
|
||
<button @click="manualDiscoverServices(item.deviceId)" class="btn-discover">发现服务</button>
|
||
<button @click="checkBatteryStatus(item.deviceId)"
|
||
class="btn-battery"
|
||
:disabled="checkingBatteryStatus[item.deviceId]">
|
||
{{ checkingBatteryStatus[item.deviceId] ? '检查中...' : '检查电量' }}
|
||
</button>
|
||
<button @click="checkChargingStatus(item.deviceId)" class="btn-battery">测试充电状态</button>
|
||
<button @click="checkStepCount(item.deviceId)" class="btn-battery">检查步数</button>
|
||
<button @click="checkHwVersion(item.deviceId)" class="btn-battery">查询硬件版本</button>
|
||
<button @click="checkSwVersion(item.deviceId)" class="btn-battery">查询软件版本</button>
|
||
<button @click="getDeviceTime(item.deviceId)" class="btn-battery">获取设备时间</button>
|
||
<button @click="setDeviceTime(item.deviceId)" class="btn-battery">设置设备时间</button>
|
||
<button @click="checkHeartRate(item.deviceId)" class="btn-battery">测试心率</button>
|
||
<button @click="checkOximetry(item.deviceId)" class="btn-battery">测试血氧</button>
|
||
<button @click="checkOximetryWithEvents(item.deviceId)" class="btn-battery">测试血氧(事件流)</button>
|
||
</view>
|
||
<view v-if="stepCount[item.deviceId] !== undefined" style="margin-top:4px;color:#2196F3;display:flex;align-items:center;gap:8px;">
|
||
步数: {{ stepCount[item.deviceId] }}
|
||
<button @click="clearStepCount(item.deviceId)" class="btn-battery">步数清零</button>
|
||
|
||
</view>
|
||
<view v-if="deviceTime[item.deviceId]" style="margin-top:2px;color:#009688;">
|
||
设备时间: {{ deviceTime[item.deviceId] }}
|
||
</view>
|
||
<view v-if="hwVersion[item.deviceId]" style="margin-top:2px;color:#795548;">硬件版本: {{ hwVersion[item.deviceId] }}</view>
|
||
<view v-if="swVersion[item.deviceId]" style="margin-top:2px;color:#607D8B;">软件版本: {{ swVersion[item.deviceId] }}</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="empty-list">暂无已连接设备</view>
|
||
|
||
<!-- 服务和特征值管理 -->
|
||
<view v-if="selectedDevice" class="device-detail">
|
||
<view class="section-title">设备详情: {{ selectedDevice.name }}</view>
|
||
|
||
<view class="services-container">
|
||
<view v-for="service in services" :key="service.uuid"
|
||
:class="['service-item', service.uuid.startsWith('bae') ? 'bae-service' : '']">
|
||
<view class="service-header" @click="toggleService(service.uuid)">
|
||
<text class="service-uuid">{{ service.uuid }}</text>
|
||
<text class="service-tag" v-if="service.uuid.startsWith('bae')">BAE服务</text>
|
||
<text class="expand-icon">{{ expandedServices.includes(service.uuid) ? '▼' : '▶' }}</text>
|
||
</view>
|
||
|
||
<view v-if="expandedServices.includes(service.uuid)" class="characteristics-container">
|
||
<view v-if="characteristics[service.uuid] && characteristics[service.uuid].length" class="characteristics-list">
|
||
<view v-for="char in characteristics[service.uuid]" :key="char.uuid" class="characteristic-item">
|
||
<view class="char-header">
|
||
<text class="char-uuid">{{ char.uuid }}</text>
|
||
<view class="char-properties">
|
||
<text v-if="char.properties.read" class="prop-tag read">R</text>
|
||
<text v-if="char.properties.write" class="prop-tag write">W</text>
|
||
<text v-if="char.properties.notify" class="prop-tag notify">N</text>
|
||
<text v-if="char.properties.indicate" class="prop-tag indicate">I</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 可写特征值显示输入框 -->
|
||
<view v-if="char.properties.write" class="char-write">
|
||
<input
|
||
type="text"
|
||
v-model="writeValues[char.uuid]"
|
||
placeholder="输入要写入的数据"
|
||
class="write-input"
|
||
/>
|
||
<button
|
||
@click="writeCharacteristic(selectedDevice.deviceId, service.uuid, char.uuid, writeValues[char.uuid])"
|
||
class="btn-write"
|
||
>
|
||
写入
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 可订阅特征值显示订阅按钮和数据 -->
|
||
<view v-if="char.properties.notify || char.properties.indicate" class="char-notify">
|
||
<button
|
||
v-if="!subscribedCharacteristics[char.uuid]"
|
||
@click="subscribeCharacteristic(selectedDevice.deviceId, service.uuid, char.uuid)"
|
||
class="btn-subscribe"
|
||
>
|
||
订阅
|
||
</button>
|
||
<button
|
||
v-else
|
||
class="btn-subscribed"
|
||
disabled
|
||
>
|
||
已订阅
|
||
</button>
|
||
|
||
<view v-if="notifyData[char.uuid]" class="notify-data">
|
||
<text class="data-label">收到数据:</text>
|
||
<text class="data-value">{{ notifyData[char.uuid] }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="empty-list">正在加载特征值...</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/web/index.uts'
|
||
import { BleDevice, BleService, BleCharacteristic, BatteryStatus } from '@/uni_modules/ak-sbsrv/interface.uts'
|
||
import { ProtocolHandler } from '@/uni_modules/ak-sbsrv/utssdk/protocol-handler.uts'
|
||
|
||
// 类型声明集中管理
|
||
interface ControlData {
|
||
devices: BleDevice[];
|
||
connectedDevices: BleDevice[];
|
||
scanning: boolean;
|
||
selectedDevice: BleDevice | null;
|
||
services: BleService[];
|
||
characteristics: Record<string, BleCharacteristic[]>;
|
||
expandedServices: string[];
|
||
writeValues: Record<string, string>;
|
||
subscribedCharacteristics: Record<string, boolean>;
|
||
notifyData: Record<string, string>;
|
||
batteryStatus: Record<string, BatteryStatus>;
|
||
checkingBatteryStatus: Record<string, boolean>;
|
||
stepCount: Record<string, number>;
|
||
hwVersion: Record<string, string>;
|
||
swVersion: Record<string, string>;
|
||
deviceTime: Record<string, string>; // 新增:存储设备时间
|
||
}
|
||
|
||
export default {
|
||
data(): ControlData {
|
||
return {
|
||
devices: [],
|
||
connectedDevices: [],
|
||
scanning: false,
|
||
selectedDevice: null,
|
||
services: [],
|
||
characteristics: {},
|
||
expandedServices: [],
|
||
writeValues: {},
|
||
subscribedCharacteristics: {},
|
||
notifyData: {},
|
||
batteryStatus: {},
|
||
checkingBatteryStatus: {}
|
||
,
|
||
stepCount: {},
|
||
hwVersion: {},
|
||
swVersion: {},
|
||
deviceTime: {} // 新增
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.refreshConnectedDevices();
|
||
if (typeof bluetoothService.onConnectionStateChange === 'function') {
|
||
bluetoothService.onConnectionStateChange((deviceId, state) => {
|
||
this.refreshConnectedDevices();
|
||
if (typeof this.$forceUpdate === 'function') {
|
||
this.$forceUpdate();
|
||
}
|
||
});
|
||
}
|
||
},
|
||
methods: {
|
||
// 统一错误提示
|
||
showError(msg: string, e?: any) {
|
||
uni.showToast({ title: msg + (e?.message ? ': ' + e.message : ''), icon: 'none' });
|
||
if (e) console.error(msg, e);
|
||
},
|
||
// 刷新已连接设备
|
||
refreshConnectedDevices() {
|
||
this.connectedDevices = bluetoothService.getConnectedDevices();
|
||
},
|
||
// 扫描设备
|
||
async scanDevices() {
|
||
this.scanning = true;
|
||
try {
|
||
const result = await bluetoothService!.scanDevices();
|
||
console.log(result)
|
||
if (result!=null && result.length > 0) {
|
||
const device = result[0];
|
||
console.log(device)
|
||
const existingDeviceIndex = this.devices.findIndex(d => d.deviceId === device.deviceId);
|
||
if (existingDeviceIndex >= 0) {
|
||
this.devices[existingDeviceIndex] = device;
|
||
} else {
|
||
this.devices.push(device);
|
||
}
|
||
}
|
||
this.refreshConnectedDevices();
|
||
} catch (e: any) {
|
||
this.showError('扫描失败,请确保浏览器支持Web蓝牙API', e);
|
||
} finally {
|
||
this.scanning = false;
|
||
}
|
||
},
|
||
// 连接设备
|
||
async connectDevice(deviceId: string) {
|
||
try {
|
||
await bluetoothService.connectDevice(deviceId);
|
||
this.updateDeviceConnection(deviceId, true);
|
||
this.refreshConnectedDevices();
|
||
const device = this.devices.find(d => d.deviceId === deviceId);
|
||
if (device) this.selectDevice(device);
|
||
uni.showToast({ title: '连接成功' });
|
||
} catch (e) {
|
||
this.showError('连接设备失败', e);
|
||
}
|
||
},
|
||
// 断开设备连接
|
||
async disconnectDevice(deviceId: string) {
|
||
try {
|
||
await bluetoothService.disconnectDevice(deviceId);
|
||
this.updateDeviceConnection(deviceId, false);
|
||
if (this.selectedDevice && this.selectedDevice.deviceId === deviceId) {
|
||
this.selectedDevice = null;
|
||
this.services = [];
|
||
this.characteristics = {};
|
||
this.expandedServices = [];
|
||
}
|
||
this.refreshConnectedDevices();
|
||
uni.showToast({ title: '已断开连接' });
|
||
} catch (e) {
|
||
this.showError('断开连接失败', e);
|
||
}
|
||
},
|
||
// 更新设备连接状态
|
||
updateDeviceConnection(deviceId: string, connected: boolean) {
|
||
const index = this.devices.findIndex(d => d.deviceId === deviceId);
|
||
if (index >= 0) this.devices[index].connected = connected;
|
||
},
|
||
// 选择设备并自动发现服务
|
||
selectDevice(device: BleDevice) {
|
||
// 响应式赋值,确保 UI 刷新
|
||
this.selectedDevice = { ...device };
|
||
this.discoverServices(device.deviceId);
|
||
if (typeof this.$forceUpdate === 'function') {
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
// 手动发现服务
|
||
async manualDiscoverServices(deviceId: string) {
|
||
const device = this.devices.find(d => d.deviceId === deviceId) ||
|
||
this.connectedDevices.find(d => d.deviceId === deviceId);
|
||
if (device) this.selectDevice(device);
|
||
},
|
||
// 发现服务
|
||
async discoverServices(deviceId: string) {
|
||
try {
|
||
this.services = [];
|
||
this.characteristics = {};
|
||
this.expandedServices = [];
|
||
const services = await bluetoothService.discoverServices(deviceId);
|
||
if (!services || !Array.isArray(services)) {
|
||
throw new Error('服务发现返回值无效');
|
||
}
|
||
this.services = services;
|
||
for (const service of services) {
|
||
if (service && service.uuid && service.uuid.startsWith('bae')) {
|
||
this.expandedServices.push(service.uuid);
|
||
await this.getCharacteristics(deviceId, service.uuid);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
this.showError('发现服务失败', e);
|
||
}
|
||
},
|
||
// 展开/折叠服务
|
||
async toggleService(serviceId: string) {
|
||
const index = this.expandedServices.indexOf(serviceId);
|
||
if (index >= 0) {
|
||
this.expandedServices.splice(index, 1);
|
||
} else {
|
||
this.expandedServices.push(serviceId);
|
||
if (!this.characteristics[serviceId] || this.characteristics[serviceId].length === 0) {
|
||
await this.getCharacteristics(this.selectedDevice!.deviceId, serviceId);
|
||
}
|
||
}
|
||
},
|
||
// 获取服务的特征值
|
||
async getCharacteristics(deviceId: string, serviceId: string) {
|
||
try {
|
||
const chars = await bluetoothService.getCharacteristics(deviceId, serviceId);
|
||
this.$set(this.characteristics, serviceId, chars);
|
||
for (const char of chars) {
|
||
if (char.properties.write) {
|
||
this.$set(this.writeValues, char.uuid, '');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
this.showError('获取特征值失败', e);
|
||
}
|
||
},
|
||
// 写入特征值
|
||
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: string) {
|
||
if (!value) {
|
||
uni.showToast({ title: '请输入要写入的数据', icon: 'none' });
|
||
return;
|
||
}
|
||
try {
|
||
const ok = await bluetoothService.writeCharacteristic(deviceId, serviceId, characteristicId, value);
|
||
if (ok) {
|
||
uni.showToast({ title: '写入成功' });
|
||
this.$set(this.writeValues, characteristicId, '');
|
||
} else {
|
||
uni.showToast({ title: '写入失败', icon: 'none' });
|
||
}
|
||
} catch (e) {
|
||
this.showError('写入特征值失败', e);
|
||
}
|
||
},
|
||
// 订阅特征值变化
|
||
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string) {
|
||
try {
|
||
await bluetoothService.subscribeCharacteristic(
|
||
deviceId,
|
||
serviceId,
|
||
characteristicId,
|
||
(data) => {
|
||
let displayData;
|
||
if (data.data instanceof Uint8Array) {
|
||
displayData = Array.from(data.data as Uint8Array)
|
||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||
.join(' ');
|
||
} else {
|
||
displayData = data.data.toString();
|
||
}
|
||
this.$set(this.notifyData, characteristicId, displayData);
|
||
}
|
||
);
|
||
this.$set(this.subscribedCharacteristics, characteristicId, true);
|
||
uni.showToast({ title: '订阅成功' });
|
||
} catch (e) {
|
||
this.showError('订阅特征值失败', e);
|
||
}
|
||
},
|
||
// 检查设备电池状态
|
||
async checkBatteryStatus(deviceId: string) {
|
||
try {
|
||
this.$set(this.checkingBatteryStatus, deviceId, true);
|
||
// 查找私有服务和特征
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
// 这里假设私有服务UUID和特征有特定前缀(如'bae'),请根据实际协议调整
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
console.log(this.services)
|
||
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
// 查找可写和可通知特征
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
console.log(this.characteristics[privateService.uuid])
|
||
console.log(writeChar,' aa ',notifyChar)
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
// 初始化协议处理器
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
// 调用私有协议获取电量
|
||
const batteryLevel = await handler.testBatteryLevel();
|
||
this.$set(this.batteryStatus, deviceId, {
|
||
level: batteryLevel,
|
||
isCharging: false // 如需充电状态可扩展testChargingStatus
|
||
});
|
||
uni.showToast({ title: '获取电池信息成功' });
|
||
} catch (e: any) {
|
||
this.showError('获取电池信息失败', e);
|
||
this.$set(this.batteryStatus, deviceId, {
|
||
level: -1,
|
||
isCharging: false
|
||
});
|
||
} finally {
|
||
this.$set(this.checkingBatteryStatus, deviceId, false);
|
||
}
|
||
},
|
||
// 检查步数
|
||
async checkStepCount(deviceId: string) {
|
||
try {
|
||
// 确保服务和特征已发现
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
const stepCount = await handler.testStepCount();
|
||
this.$set(this.stepCount, deviceId, stepCount);
|
||
uni.showToast({ title: '步数获取成功' });
|
||
} catch (e: any) {
|
||
this.showError('获取步数失败', e);
|
||
this.$set(this.stepCount, deviceId, -1);
|
||
}
|
||
},
|
||
// 步数清零
|
||
async clearStepCount(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
await handler.testClearStepCount();
|
||
uni.showToast({ title: '步数已清零' });
|
||
// 清零后自动刷新步数
|
||
await this.checkStepCount(deviceId);
|
||
} catch (e: any) {
|
||
this.showError('步数清零失败', e);
|
||
}
|
||
},
|
||
// 获取设备时间
|
||
async getDeviceTime(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
const timeStr = await handler.testGetTime();
|
||
this.$set(this.deviceTime, deviceId, timeStr);
|
||
uni.showToast({ title: '获取设备时间成功' });
|
||
} catch (e: any) {
|
||
this.showError('获取设备时间失败', e);
|
||
this.$set(this.deviceTime, deviceId, '获取失败');
|
||
}
|
||
},
|
||
// 获取设备时间
|
||
async setDeviceTime(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
const timeStr = await handler.testTimeSync();
|
||
this.$set(this.deviceTime, deviceId, timeStr);
|
||
uni.showToast({ title: '设置设备时间成功' });
|
||
} catch (e: any) {
|
||
this.showError('设置设备时间失败', e);
|
||
this.$set(this.deviceTime, deviceId, '获取失败');
|
||
}
|
||
},
|
||
// 查询硬件版本
|
||
async checkHwVersion(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
const version = await handler.testVersionInfo(true); // true=硬件版本
|
||
this.$set(this.hwVersion, deviceId, version);
|
||
uni.showToast({ title: '硬件版本获取成功' });
|
||
} catch (e: any) {
|
||
this.showError('获取硬件版本失败', e);
|
||
this.$set(this.hwVersion, deviceId, '获取失败');
|
||
}
|
||
},
|
||
// 查询软件版本
|
||
async checkSwVersion(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
const version = await handler.testVersionInfo(false); // false=软件版本
|
||
this.$set(this.swVersion, deviceId, version);
|
||
uni.showToast({ title: '软件版本获取成功' });
|
||
} catch (e: any) {
|
||
this.showError('获取软件版本失败', e);
|
||
this.$set(this.swVersion, deviceId, '获取失败');
|
||
}
|
||
},
|
||
// 测试充电状态
|
||
async checkChargingStatus(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
const status = await handler.testChargingStatus();
|
||
let statusText = '未知';
|
||
if (status === 0) statusText = '未充电';
|
||
else if (status === 1) statusText = '充电中';
|
||
else if (status === 2) statusText = '已充满';
|
||
if (!this.batteryStatus[deviceId]) {
|
||
this.$set(this.batteryStatus, deviceId, { level: -1, isCharging: false, chargingStatus: statusText });
|
||
} else {
|
||
this.$set(this.batteryStatus[deviceId], 'chargingStatus', statusText);
|
||
}
|
||
uni.showToast({ title: '充电状态: ' + statusText });
|
||
} catch (e: any) {
|
||
this.showError('获取充电状态失败', e);
|
||
if (!this.batteryStatus[deviceId]) {
|
||
this.$set(this.batteryStatus, deviceId, { level: -1, isCharging: false, chargingStatus: '未知' });
|
||
} else {
|
||
this.$set(this.batteryStatus[deviceId], 'chargingStatus', '未知');
|
||
}
|
||
}
|
||
},
|
||
// 测试心率(事件流式)
|
||
async checkHeartRate(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
// 使用事件流方式
|
||
const { stop, onData } = handler.heartRateWithEvents();
|
||
let lastHeartRate = null;
|
||
let timer = null;
|
||
onData((data: Uint8Array) => {
|
||
// 假设心率值在data[5],可根据协议调整
|
||
if (data.length >= 6 && data[2] === 0x31 && data[3] === 0x00) {
|
||
const heartRate = data[5];
|
||
lastHeartRate = heartRate;
|
||
uni.showToast({ title: '心率: ' + heartRate, icon: 'success' });
|
||
// 3秒后自动停止
|
||
if (timer) clearTimeout(timer);
|
||
timer = setTimeout(() => {
|
||
stop();
|
||
uni.showToast({ title: '心率测量已结束', icon: 'none' });
|
||
}, 3000);
|
||
}
|
||
});
|
||
} catch (e: any) {
|
||
this.showError('获取心率失败', e);
|
||
}
|
||
},
|
||
// 测试血氧(事件流式)
|
||
async checkOximetryWithEvents(deviceId: string) {
|
||
try {
|
||
if (!this.services || this.services.length === 0) {
|
||
await this.discoverServices(deviceId);
|
||
}
|
||
const privateService = this.services.find(service => service.uuid.startsWith('bae'));
|
||
if (!privateService) throw new Error('未找到私有协议服务');
|
||
if (!this.characteristics[privateService.uuid] || this.characteristics[privateService.uuid].length === 0) {
|
||
await this.getCharacteristics(deviceId, privateService.uuid);
|
||
}
|
||
const writeChar = this.characteristics[privateService.uuid].find(char => char.properties.write);
|
||
const notifyChar = this.characteristics[privateService.uuid].find(char => char.properties.notify);
|
||
if (!writeChar || !notifyChar) throw new Error('未找到私有协议特征');
|
||
const handler = new ProtocolHandler(bluetoothService);
|
||
handler.setConnectionParameters(deviceId, privateService.uuid, writeChar.uuid, notifyChar.uuid);
|
||
await handler.initialize();
|
||
// 使用事件流方式
|
||
const { stop, onData } = handler.oximetryWithEvents();
|
||
let timer = null;
|
||
onData((data: Uint8Array) => {
|
||
// 协议:data[4]=佩戴状态, data[5]=心率, data[6]=血氧, data[7:8]=温度(int16,0.01℃,小端)
|
||
if (data.length >= 9 && data[2] === 0x32 && data[3] === 0x00) {
|
||
const wearStatus = data[4];
|
||
const heartRate = data[5];
|
||
const spo2 = data[6];
|
||
// 温度为有符号短整型,小端序
|
||
let tempRaw = (data[8] << 8) | data[7];
|
||
if (tempRaw & 0x8000) tempRaw = tempRaw - 0x10000;
|
||
const temp = tempRaw / 100;
|
||
console.log({ title: `血氧: ${spo2}% 心率: ${heartRate} 温度: ${temp}℃` });
|
||
// 3秒后自动停止
|
||
if (timer) clearTimeout(timer);
|
||
timer = setTimeout(() => {
|
||
stop();
|
||
uni.showToast({ title: '血氧测量已结束', icon: 'none' });
|
||
}, 3000);
|
||
}
|
||
});
|
||
} catch (e: any) {
|
||
this.showError('获取血氧失败', e);
|
||
}
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.container {
|
||
padding: 20px;
|
||
}
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin: 20px 0 10px;
|
||
padding-bottom: 5px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
.device-item {
|
||
margin: 10px 0;
|
||
padding: 12px;
|
||
border: 1px solid #eee;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.connected-item {
|
||
background-color: #f0f9ff;
|
||
border-color: #b3e0ff;
|
||
}
|
||
.device-info {
|
||
flex: 1;
|
||
}
|
||
.device-name {
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
margin-right: 5px;
|
||
}
|
||
.device-id {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-right: 10px;
|
||
}
|
||
.status {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
.status.connected {
|
||
color: #4CAF50;
|
||
}
|
||
.device-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
.btn-connect {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
font-size: 14px;
|
||
border-radius: 4px;
|
||
}
|
||
.btn-disconnect {
|
||
background-color: #F44336;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
font-size: 14px;
|
||
border-radius: 4px;
|
||
}
|
||
.btn-discover {
|
||
background-color: #2196F3;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
font-size: 14px;
|
||
border-radius: 4px;
|
||
}
|
||
.empty-list {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #999;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* 设备详情样式 */
|
||
.device-detail {
|
||
margin-top: 20px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
background-color: #f9f9f9;
|
||
}
|
||
|
||
/* 服务列表样式 */
|
||
.services-container {
|
||
margin-top: 10px;
|
||
}
|
||
.service-item {
|
||
margin-bottom: 10px;
|
||
border: 1px solid #eee;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
.bae-service {
|
||
border-color: #4CAF50;
|
||
}
|
||
.service-header {
|
||
padding: 10px;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
}
|
||
.bae-service .service-header {
|
||
background-color: #e8f5e9;
|
||
}
|
||
.service-uuid {
|
||
flex: 1;
|
||
font-family: monospace;
|
||
}
|
||
.service-tag {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
margin-right: 10px;
|
||
}
|
||
.expand-icon {
|
||
margin-left: 10px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 特征值列表样式 */
|
||
.characteristics-container {
|
||
padding: 10px;
|
||
background-color: white;
|
||
}
|
||
.characteristic-item {
|
||
padding: 10px;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.char-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
.char-uuid {
|
||
flex: 1;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
}
|
||
.char-properties {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
.prop-tag {
|
||
width: 18px;
|
||
height: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 12px;
|
||
border-radius: 2px;
|
||
}
|
||
.prop-tag.read {
|
||
background-color: #2196F3;
|
||
}
|
||
.prop-tag.write {
|
||
background-color: #FF9800;
|
||
}
|
||
.prop-tag.notify {
|
||
background-color: #9C27B0;
|
||
}
|
||
.prop-tag.indicate {
|
||
background-color: #795548;
|
||
}
|
||
|
||
/* 写入区域样式 */
|
||
.char-write {
|
||
display: flex;
|
||
margin-top: 8px;
|
||
}
|
||
.write-input {
|
||
flex: 1;
|
||
padding: 6px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
margin-right: 8px;
|
||
}
|
||
.btn-write {
|
||
background-color: #FF9800;
|
||
color: white;
|
||
border-radius: 4px;
|
||
padding: 6px 12px;
|
||
}
|
||
|
||
/* 通知区域样式 */
|
||
.char-notify {
|
||
margin-top: 8px;
|
||
}
|
||
.btn-subscribe {
|
||
background-color: #9C27B0;
|
||
color: white;
|
||
border-radius: 4px;
|
||
padding: 6px 12px;
|
||
}
|
||
.btn-subscribed {
|
||
background-color: #7B1FA2;
|
||
color: white;
|
||
border-radius: 4px;
|
||
padding: 6px 12px;
|
||
opacity: 0.7;
|
||
}
|
||
.notify-data {
|
||
margin-top: 8px;
|
||
padding: 8px;
|
||
background-color: #f0f0f0;
|
||
border-radius: 4px;
|
||
}
|
||
.data-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-right: 8px;
|
||
}
|
||
.data-value {
|
||
font-family: monospace;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 电池状态样式 */
|
||
.battery-status {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.battery-container {
|
||
width: 40px;
|
||
height: 16px;
|
||
border: 1px solid #999;
|
||
border-radius: 2px;
|
||
padding: 1px;
|
||
position: relative;
|
||
margin-right: 8px;
|
||
background-color: #f0f0f0;
|
||
}
|
||
.battery-level {
|
||
height: 100%;
|
||
background-color: #4CAF50;
|
||
border-radius: 1px;
|
||
}
|
||
.battery-text {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
.charging-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
color: #FF9800;
|
||
font-size: 10px;
|
||
}
|
||
.btn-battery {
|
||
background-color: #607D8B;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
font-size: 14px;
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|