Files
akbleserver/pages/control.uvue
2026-03-16 10:37:46 +08:00

964 lines
36 KiB
Plaintext
Raw Permalink 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.
<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>