feat(mp): BLE 血氧仪支持 + 服务发现增强

- 新增 Pulse Oximeter Service (0x1822) 支持,含 SFLOAT 解析
- 连接后自动扫描全部服务,发现并订阅已知健康 UUID
- 设备同步页展示已发现的服务和可用数据类型标签
- 新增 BLEDiscoveredService / BLEDiscoveredCharacteristic 类型
This commit is contained in:
iven
2026-05-25 13:43:16 +08:00
parent ef1b8eb348
commit a24c18155f
5 changed files with 307 additions and 15 deletions

View File

@@ -927,3 +927,62 @@
font-size: var(--tk-font-body-lg);
color: $tx2;
}
// ─── 服务发现信息 ───
.ds-services-info {
margin-bottom: var(--tk-gap-md) !important;
}
.ds-services-info__title {
display: block;
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $tx;
margin-bottom: var(--tk-gap-sm);
}
.ds-services-info__caps {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.ds-cap-tag {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: $r-xs;
font-size: var(--tk-font-cap);
&--on {
background: rgba($acc, 0.08);
color: $acc;
}
&--off {
background: $surface-alt;
color: $tx3;
}
}
.ds-cap-tag__dot {
font-size: 10px;
}
.ds-cap-tag__text {
font-size: var(--tk-font-cap);
}
.ds-services-info__hint {
margin-top: var(--tk-gap-sm);
background: $wrn-l;
border-radius: $r-xs;
padding: 8px 12px;
}
.ds-services-info__hint-text {
font-size: var(--tk-font-cap);
color: $wrn;
line-height: 1.5;
}

View File

@@ -10,7 +10,7 @@ import { CustomBandAdapter, HuaweiBandAdapter, FallbackAdapter } from '@/service
import { DataSyncScheduler } from '@/services/ble/DataSyncScheduler';
import { uploadReadings } from '@/services/device-sync';
import { useAuthStore } from '@/stores/auth';
import type { BLEDevice, NormalizedReading } from '@/services/ble/types';
import type { BLEDevice, NormalizedReading, BLEDiscoveredService } from '@/services/ble/types';
import { useElderClass } from '@/hooks/useElderClass';
import PageShell from '@/components/ui/PageShell';
import ContentCard from '@/components/ui/ContentCard';
@@ -80,6 +80,7 @@ export default function DeviceSync() {
const [errorMsg, setErrorMsg] = useState('');
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
const [pendingCount, setPendingCount] = useState(0);
const [discoveredServices, setDiscoveredServices] = useState<BLEDiscoveredService[]>([]);
const scheduler = useMemo(() => new DataSyncScheduler({ intervalMs: 60 * 60 * 1000 }), []);
@@ -163,6 +164,8 @@ export default function DeviceSync() {
setErrorMsg('');
try {
await getBleManager().connect(device);
const conn = getBleManager().getConnection();
setDiscoveredServices(conn?.discoveredServices ?? []);
setPageState('connected');
} catch (e: unknown) {
setErrorMsg(e instanceof Error ? e.message : '连接失败');
@@ -217,6 +220,7 @@ export default function DeviceSync() {
setLiveReadings([]);
setSyncCount(0);
setErrorMsg('');
setDiscoveredServices([]);
}, []);
const latestReading = liveReadings.length > 0 ? liveReadings[liveReadings.length - 1] : null;
@@ -359,6 +363,54 @@ export default function DeviceSync() {
</View>
);
/** 渲染 BLE 服务发现信息 */
const renderServiceDiscovery = () => {
if (discoveredServices.length === 0) return null;
// 检查各类健康数据是否在已发现的 UUID 中可用
const hasCharShort = (short: string) =>
discoveredServices.some((s) =>
s.characteristics.some((c) =>
c.uuid.toUpperCase().replace(/-/g, '').slice(-4) === short,
),
);
const capabilities = [
{ key: '2A37', label: '心率', available: hasCharShort('2A37') },
{ key: '2A5F', label: '血氧(实时)', available: hasCharShort('2A5F') },
{ key: '2A5E', label: '血氧(单次)', available: hasCharShort('2A5E') },
{ key: '2A1C', label: '体温', available: hasCharShort('2A1C') },
{ key: '2A35', label: '血压', available: hasCharShort('2A35') },
];
const availableCount = capabilities.filter((c) => c.available).length;
return (
<ContentCard variant="outlined" padding="md" margin="none" className="ds-services-info">
<Text className="ds-services-info__title">
({discoveredServices.length} , {availableCount} )
</Text>
<View className="ds-services-info__caps">
{capabilities.map((cap) => (
<View key={cap.key} className={`ds-cap-tag ${cap.available ? 'ds-cap-tag--on' : 'ds-cap-tag--off'}`}>
<Text className="ds-cap-tag__dot">{cap.available ? '●' : '○'}</Text>
<Text className="ds-cap-tag__text">{cap.label}</Text>
</View>
))}
</View>
{availableCount <= 1 && (
<View className="ds-services-info__hint">
<Text className="ds-services-info__hint-text">
{selectedDevice?.name?.includes('HUAWEI') || selectedDevice?.name?.includes('HW')
? '华为手环的睡眠/步数/压力数据使用私有协议,需要华为运动健康 App 同步'
: '此设备仅暴露少量标准健康服务,更多数据请使用设备官方 App 同步'}
</Text>
</View>
)}
</ContentCard>
);
};
const renderLatestReading = () => {
if (!latestReading) return null;
return (
@@ -525,6 +577,7 @@ export default function DeviceSync() {
{pageState === 'connected' && (
<View className="ds-body">
{renderConnectedStatus()}
{renderServiceDiscovery()}
{renderLatestReading()}
{renderReadingsHistory()}
{renderConnectedActions()}