Initial commit

This commit is contained in:
2026-03-16 10:37:46 +08:00
commit c052a67816
508 changed files with 22987 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
<template>
<view>
<view>
<button @tap="loadLogs('')">加载历史数据</button>
</view>
<view v-if="logs.length === 0">暂无数据</view>
<view v-for="item in logs" :key="item.key">
<text>{{ item.type }} | {{ item.timestamp }} | {{ item.data }}</text>
</view>
</view>
</template>
<script lang="uts">
import { sqliteContext } from '@/ak/sqlite.uts'
type LogItem = {
type: string;
timestamp: number;
data: string;
key: string;
};
export default {
name: "blecommu",
data() {
return {
logs: [] as LogItem[]
}
},
methods: {
loadLogs(deviceId: string = '') {
this.logs = [];
// 查询 ble_data_log
sqliteContext.selectSql({
sql: `SELECT id, device_id, service_id, char_id, direction, data, timestamp FROM ble_data_log ORDER BY timestamp DESC`,
success: (res) => {
console.log('查询 ble_data_log 成功', res);
if (Array.isArray(res.data) && res.data.length > 0) {
for (const row of res.data) {
this.logs.push({
type: row[4] === 'send' ? '发送' : '接收',
timestamp: parseInt(row[6]),
data: `service:${row[2]} char:${row[3]} data:${row[5]}`,
key: row[4] + '_' + row[0]
});
}
}
// 查询 ble_event_log
sqliteContext.selectSql({
sql: `SELECT id, device_id, event_type, detail, timestamp FROM ble_event_log WHERE device_id = '${deviceId}' ORDER BY timestamp DESC`,
success: (res2) => {
console.log('查询 ble_event_log 成功', res2);
if (Array.isArray(res2.data) && res2.data.length > 0) {
for (const row of res2.data) {
this.logs.push({
type: '事件',
timestamp: parseInt(row[4]),
data: `type:${row[2]} detail:${row[3]}`,
key: 'event_' + row[0]
});
}
}
this.logs.sort((a, b) => b.timestamp - a.timestamp);
},
fail: (err2) => {
console.log('查询 ble_event_log 失败', err2);
}
});
},
fail: (err) => {
console.log('查询 ble_data_log 失败', err);
}
});
}
}
}
</script>
<style>
</style>

439
components/ringcard.uvue Normal file
View File

@@ -0,0 +1,439 @@
<template>
<view
ref="fullscreenCard"
class="ring-card"
:style="[connected ? connectedstyle : '', isFullscreen ? fullscreenstyle : '']"
>
<!-- 顶部信息区(可选:全屏时可移到左侧) -->
<!-- <view class="top-info"> ... </view> -->
<view class="main-content">
<!-- 左侧信息+操作栏 -->
<view class="side-bar">
<view class="info">
<text class="name">{{ name ?? '未知设备' }}</text>
<text class="id" v-if="isFullscreen">({{ deviceId ?? '' }})</text>
<text>电量: {{ batteryText }}</text>
<text>时间: {{ time ?? '--' }}</text>
<text>步数: {{ steps ?? '--' }}</text>
<text>软件版本: {{ swVersion ?? '--' }}</text>
<text>硬件版本: {{ hwVersion ?? '--' }}</text>
<text>血氧: {{ spo2Text }}</text>
<text>心率: {{ heartRate != null ? heartRate : '--' }}</text>
<text>体温: {{ temperatureText }}</text>
</view>
<view class="actions-vertical">
<button @click="connect" v-if="!connected">连接</button>
<button @click="disconnect" v-if="connected">断开</button>
<button @click="refreshInfo" v-if="connected">刷新</button>
<button @click="measureSpo2WithEvents" v-if="connected && oximetryMeasuring==false">测血氧</button>
<button disabled v-if="oximetryMeasuring">测量中...</button>
<button @click="gotoHistory">历史数据</button>
<button @click="onFullscreen" v-if="!isFullscreen" class="fullscreen-btn">全屏</button>
<button @click="onFullscreen" v-if="isFullscreen" class="fullscreen-exit-btn">退出全屏</button>
</view>
</view>
<!-- 中间ak-charts和AI评论 -->
<view class="center-area">
<ak-charts :option="ppgChartOption" canvas-id="ppg-canvas"
style="width: 100%; height: 40vh; background: #fff; border: 1px solid #eee; margin-top: 10px;" />
<view class="ai-comment">
<text>AI评论区</text>
<!-- 这里可以插入AI分析结果 -->
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
// Platform-specific entrypoint: import per-platform index to avoid bundler pulling Android-only code into web builds
// #ifdef APP-ANDROID
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/app-android/index.uts'
// #endif
// #ifdef WEB
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/web/index.uts'
// #endif
import {BleEvent, BleEventPayload} from '@/uni_modules/ak-sbsrv/utssdk/interface.uts'
import { ProtocolHandler } from '@/uni_modules/ak-sbsrv/utssdk/protocol-handler.uts'
import AkCharts from '@/uni_modules/ak-charts/components/ak-charts.uvue'
type PpgWaveformItem = {
red: number;
ir: number;
x: number;
y: number;
z: number;
}
type PpgWaveformPacket= {
seq: number;
num: number;
data: PpgWaveformItem[];
}
export default {
components: {
AkCharts
},
props: {
name: String,
deviceId: String,
isFullscreen: {
type: Boolean,
default: false
}
},
data() {
return {
connectedstyle:'border-color: #4caf50;',
fullscreenstyle:'padding: 24px;',
connected: false,
battery: null as number | null,
time: null as string | null,
steps: null as number | null,
swVersion: null as string | null,
hwVersion: null as string | null,
spo2: null as number | null,
heartRate: null as number | null,
temperature: null as number | null,
oximetryMeasuring: false as boolean,
handler: null as ProtocolHandler | null,
refreshTimer: null as number | null, // 定时器句柄
oximetryTimer: null as number | null,
ppgWaveforms: [] as PpgWaveformPacket[],
isBigScreen: false
}
},
mounted() {
// 判断是否为大屏
uni.getSystemInfo({
success: (res) => {
this.isBigScreen = res.screenWidth >= 900; // 900px 仅为示例
}
});
},
computed: {
batteryBarStyle(): any {
const battery = this.battery;
let width = (battery != null ? battery : 0) + '%';
let background = '#ccc';
if (battery != null) {
if (battery < 20) background = '#f44336';
else if (battery < 50) background = '#ff9800';
else background = '#4caf50';
}
return { width, background };
},
batteryText(): string {
const battery = this.battery;
return battery != null ? battery.toString() + '%' : '--';
},
spo2Text(): string {
const spo2 = this.spo2;
return spo2 != null ? spo2.toString() + '%' : '--';
},
temperatureText(): string {
const temperature = this.temperature;
return temperature != null ? temperature.toString() + '℃' : '--';
},
// ak-charts option
ppgChartOption(): any {
// 展示最近240个点
const redPoints: number[] = [];
const irPoints: number[] = [];
for (let i = Math.max(0, this.ppgWaveforms.length - 20); i < this.ppgWaveforms.length; i++) {
const packet = this.ppgWaveforms[i];
for (let j = 0; j < packet.data.length; j++) {
redPoints.push(packet.data[j].red);
irPoints.push(packet.data[j].ir);
}
}
const labels = redPoints.map((_, idx) => idx.toString());
return {
type: 'line',
data: redPoints,
// series: [
// { name: '红光', data: redPoints, color: '#4caf50' },
// { name: '红外', data: irPoints, color: '#f44336' }
// ],
labels: labels,
smooth: true
};
},
},
methods: {
async connect() {
try {
await bluetoothService.connectDevice(this.deviceId!, 'BLE', { timeout: 30000 });
this.connected = true;
await this.initHandler();
await this.refreshInfo();
this.clearRefreshTimer();
this.refreshTimer = setInterval(() => {
this.refreshInfo();
}, 300 * 1000);
} catch (e) {
console.log(e)
}
},
async disconnect() {
await bluetoothService.disconnectDevice(this.deviceId!, 'BLE');
this.connected = false;
this.handler = null;
this.clearRefreshTimer();
},
clearRefreshTimer() {
const timer = this.refreshTimer;
if (timer != null) {
clearInterval(timer);
this.refreshTimer = null;
}
},
async initHandler() {
const res = await bluetoothService.getAutoBleInterfaces(this.deviceId!);
const handler = new ProtocolHandler(bluetoothService);
handler.setConnectionParameters(this.deviceId!, res.serviceId, res.writeCharId, res.notifyCharId);
await handler.initialize();
handler.onPushData((data) => {
console.log(data)
// ...主动推送处理...
// 波形响应
if (data.length >= 6 && data[2] === 0x32 && data[1] ==0x00) {
console.log(data[3])
this.parseBloodOxygenWave(data);
}
});
this.handler = handler;
},
async refreshInfo() {
if (this.handler == null) await this.initHandler();
const handler = this.handler;
if (handler != null) {
this.battery = await handler.testBatteryLevel();
this.swVersion = await handler.testVersionInfo(false);
this.hwVersion = await handler.testVersionInfo(true);
this.time = await handler.testGetTime();
this.steps = await handler.testStepCount();
}
},
async measureBattery() {
if (!this.connected) await this.connect();
if (this.handler == null) await this.initHandler();
const handler = this.handler;
if (handler != null) {
this.battery = await handler.testBatteryLevel();
return this.battery;
}
return null;
},
async clearSteps() {
if (!this.connected) await this.connect();
if (this.handler == null) await this.initHandler();
const handler = this.handler;
if (handler != null && typeof handler.testClearStepCount() === 'function') {
await handler.testClearStepCount();
this.steps = 0;
}
},
async measureSpo2WithEvents() {
if (!this.connected) await this.connect();
if (this.handler == null) await this.initHandler();
const handler = this.handler;
if (handler != null) {
this.oximetryMeasuring = true;
// 清理上一次的定时器
const timer = this.oximetryTimer;
if (timer != null) {
clearTimeout(timer);
this.oximetryTimer = null;
}
// 设置测量超时时间如60秒
this.oximetryTimer = setTimeout(() => {
this.oximetryMeasuring = false;
handler.oximetryWithEvents({ stop: true }, null);
uni.showToast({ title: '血氧测量已结束', icon: 'none' });
this.oximetryTimer = null;
}, 60000); // 60秒
handler.oximetryWithEvents({}, (data: Uint8Array) => {
console.log(data);
// 结果响应
if (data.length >= 9 && data[2] === 0x32 && data[3] === 0x00) {
this.heartRate = data[5];
this.spo2 = data[6];
let tempRaw = (data[8] << 8) | data[7];
if ((tempRaw & 0x8000) > 0) tempRaw = tempRaw - 0x10000;
this.temperature = tempRaw / 100;
}
// 波形响应
if (data.length >= 6 && data[2] === 0x32 && data[3] === 0x01) {
const frameid = data[1];
if (frameid === 0) { // 只处理frameid为0的包
const seq = data[4];
const num = data[5];
const waveArr: PpgWaveformItem[] = [];
for (let i = 0; i < num; i++) {
const base = 6 + i * 14;
if ((base + 13) >= data.length) break;
let red = (data[base + 3] << 24) | (data[base + 2] << 16) | (data[base + 1] << 8) | data[base + 0];
if ((red & 0x80000000) !== 0) red = red - 0x100000000;
let ir = (data[base + 7] << 24) | (data[base + 6] << 16) | (data[base + 5] << 8) | data[base + 4];
if ((ir & 0x80000000) !== 0) ir = ir - 0x100000000;
let x = (data[base + 9] << 8) | data[base + 8];
if ((x & 0x8000) !== 0) x = x - 0x10000;
let y = (data[base + 11] << 8) | data[base + 10];
if ((y & 0x8000) !== 0) y = y - 0x10000;
let z = (data[base + 13] << 8) | data[base + 12];
if ((z & 0x8000) !== 0) z = z - 0x10000;
waveArr.push({ red, ir, x, y, z });
}
// 按seq去重拼包
const idx = this.ppgWaveforms.findIndex(p => p.seq === seq);
if (idx >= 0) {
this.ppgWaveforms[idx] = { seq, num, data: waveArr };
} else {
this.ppgWaveforms.push({ seq, num, data: waveArr });
}
// ak-charts 自动响应数据变化无需手动draw
}
}
// 波形结束通知
// if (data.length >= 4 && data[2] === 0x32 && data[3] === 0xFF) {
// this.oximetryMeasuring = false;
// if (this.oximetryTimer != null) {
// const timer = this.oximetryTimer as number;
// clearTimeout(timer);
// this.oximetryTimer = null;
// }
// }
});
}
},
onConnectionStateChanged(state: number) {
if (state === 0) {
this.connected = false;
this.handler = null;
this.clearRefreshTimer();
// 这里可以加UI变灰等处理
} else if (state === 2) {
this.connected = true;
// 这里可以加UI恢复等处理
}
},
gotoHistory() {
uni.navigateTo({
url: `/components/blecommu?deviceId=${this.deviceId}`
});
},
onFullscreen() {
if (this.isFullscreen) {
this.$emit('exit-fullscreen', this.deviceId);
} else {
this.$emit('request-fullscreen', this.deviceId);
}
},
parseBloodOxygenWave(data: Uint8Array) {
const seq = data[4];
const num = data[5];
const waveArr: PpgWaveformItem[] = [];
for (let i = 0; i < num; i++) {
const base = 6 + i * 14;
if ((base + 13) >= data.length) break;
// 红色PPG
let red = (data[base + 3] << 24) | (data[base + 2] << 16) | (data[base + 1] << 8) | data[base + 0];
if ((red & 0x80000000) !== 0) red = red - 0x100000000;
// 红外PPG
let ir = (data[base + 7] << 24) | (data[base + 6] << 16) | (data[base + 5] << 8) | data[base + 4];
if ((ir & 0x80000000) !== 0) ir = ir - 0x100000000;
// X加速度
let x = (data[base + 9] << 8) | data[base + 8];
if ((x & 0x8000) !== 0) x = x - 0x10000;
// Y加速度
let y = (data[base + 11] << 8) | data[base + 10];
if ((y & 0x8000) !== 0) y = y - 0x10000;
// Z加速度
let z = (data[base + 13] << 8) | data[base + 12];
if ((z & 0x8000) !== 0) z = z - 0x10000;
waveArr.push({ red, ir, x, y, z });
}
console.log(seq, num, data)
this.ppgWaveforms.push({ seq, num, data: waveArr });
}
},
beforeUnmount() {
this.clearRefreshTimer();
},
}
</script>
<style scoped>
.ring-card {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
background: #ffffff;
overflow: auto;
padding: 0;
}
.main-content {
flex: 1;
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.side-bar {
width: 20%;
min-width: 180px;
max-width: 320px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
background: #f8f8f8;
padding: 24px 12px 0 24px;
box-sizing: border-box;
}
.info {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 14px;
margin-bottom: 20px;
}
.actions-vertical {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.center-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 24px 0 0 0;
background-color: #fff;
}
.ai-comment {
margin-top: 24px;
width: 90%;
min-height: 60px;
background: #f5f5f5;
border-radius: 8px;
padding: 12px;
font-size: 15px;
color: #333;
}
.fullscreen-btn, .fullscreen-exit-btn {
width: 100px;
height: 36px;
font-size: 16px;
}
</style>