feat(mp): BLE 血氧仪支持 + 服务发现增强
- 新增 Pulse Oximeter Service (0x1822) 支持,含 SFLOAT 解析 - 连接后自动扫描全部服务,发现并订阅已知健康 UUID - 设备同步页展示已发现的服务和可用数据类型标签 - 新增 BLEDiscoveredService / BLEDiscoveredCharacteristic 类型
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user