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);
|
font-size: var(--tk-font-body-lg);
|
||||||
color: $tx2;
|
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 { DataSyncScheduler } from '@/services/ble/DataSyncScheduler';
|
||||||
import { uploadReadings } from '@/services/device-sync';
|
import { uploadReadings } from '@/services/device-sync';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
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 { useElderClass } from '@/hooks/useElderClass';
|
||||||
import PageShell from '@/components/ui/PageShell';
|
import PageShell from '@/components/ui/PageShell';
|
||||||
import ContentCard from '@/components/ui/ContentCard';
|
import ContentCard from '@/components/ui/ContentCard';
|
||||||
@@ -80,6 +80,7 @@ export default function DeviceSync() {
|
|||||||
const [errorMsg, setErrorMsg] = useState('');
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
|
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
|
||||||
const [pendingCount, setPendingCount] = useState(0);
|
const [pendingCount, setPendingCount] = useState(0);
|
||||||
|
const [discoveredServices, setDiscoveredServices] = useState<BLEDiscoveredService[]>([]);
|
||||||
|
|
||||||
const scheduler = useMemo(() => new DataSyncScheduler({ intervalMs: 60 * 60 * 1000 }), []);
|
const scheduler = useMemo(() => new DataSyncScheduler({ intervalMs: 60 * 60 * 1000 }), []);
|
||||||
|
|
||||||
@@ -163,6 +164,8 @@ export default function DeviceSync() {
|
|||||||
setErrorMsg('');
|
setErrorMsg('');
|
||||||
try {
|
try {
|
||||||
await getBleManager().connect(device);
|
await getBleManager().connect(device);
|
||||||
|
const conn = getBleManager().getConnection();
|
||||||
|
setDiscoveredServices(conn?.discoveredServices ?? []);
|
||||||
setPageState('connected');
|
setPageState('connected');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setErrorMsg(e instanceof Error ? e.message : '连接失败');
|
setErrorMsg(e instanceof Error ? e.message : '连接失败');
|
||||||
@@ -217,6 +220,7 @@ export default function DeviceSync() {
|
|||||||
setLiveReadings([]);
|
setLiveReadings([]);
|
||||||
setSyncCount(0);
|
setSyncCount(0);
|
||||||
setErrorMsg('');
|
setErrorMsg('');
|
||||||
|
setDiscoveredServices([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const latestReading = liveReadings.length > 0 ? liveReadings[liveReadings.length - 1] : null;
|
const latestReading = liveReadings.length > 0 ? liveReadings[liveReadings.length - 1] : null;
|
||||||
@@ -359,6 +363,54 @@ export default function DeviceSync() {
|
|||||||
</View>
|
</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 = () => {
|
const renderLatestReading = () => {
|
||||||
if (!latestReading) return null;
|
if (!latestReading) return null;
|
||||||
return (
|
return (
|
||||||
@@ -525,6 +577,7 @@ export default function DeviceSync() {
|
|||||||
{pageState === 'connected' && (
|
{pageState === 'connected' && (
|
||||||
<View className="ds-body">
|
<View className="ds-body">
|
||||||
{renderConnectedStatus()}
|
{renderConnectedStatus()}
|
||||||
|
{renderServiceDiscovery()}
|
||||||
{renderLatestReading()}
|
{renderLatestReading()}
|
||||||
{renderReadingsHistory()}
|
{renderReadingsHistory()}
|
||||||
{renderConnectedActions()}
|
{renderConnectedActions()}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type {
|
|||||||
BLEConnectionChangeResult,
|
BLEConnectionChangeResult,
|
||||||
BLECharacteristicChangeResult,
|
BLECharacteristicChangeResult,
|
||||||
BLEServiceItem,
|
BLEServiceItem,
|
||||||
|
BLEDiscoveredService,
|
||||||
|
BLEDiscoveredCharacteristic,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/** BLE 连接管理 — 封装连接/断开/服务发现/通知订阅/数据监听 */
|
/** BLE 连接管理 — 封装连接/断开/服务发现/通知订阅/数据监听 */
|
||||||
@@ -121,10 +123,24 @@ export class BLEConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发现服务并启用通知 */
|
/** 已知的健康相关 Characteristic UUID(用于自动发现和订阅) */
|
||||||
|
private static readonly HEALTH_CHAR_UUIDS: Record<string, string> = {
|
||||||
|
'2A37': 'heart_rate', // Heart Rate Measurement
|
||||||
|
'2A38': 'heart_rate_loc', // Body Sensor Location
|
||||||
|
'2A1C': 'temperature', // Temperature Measurement
|
||||||
|
'2A35': 'blood_pressure', // Blood Pressure Measurement
|
||||||
|
'2A5F': 'blood_oxygen', // PLX Continuous Measurement
|
||||||
|
'2A5E': 'blood_oxygen_spot',// PLX Spot-Check Measurement
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 发现服务并启用通知 — 先订阅适配器指定的,再扫描全部服务尝试自动发现 */
|
||||||
private async discoverServices(device: BLEDevice): Promise<void> {
|
private async discoverServices(device: BLEDevice): Promise<void> {
|
||||||
const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId });
|
const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId });
|
||||||
const services = servicesRes.services || [];
|
const services = servicesRes.services || [];
|
||||||
|
const discoveredServices: BLEDiscoveredService[] = [];
|
||||||
|
|
||||||
|
// ── 第一轮:订阅适配器预定义的 Characteristic(保持向后兼容) ──
|
||||||
|
const subscribedCharUUIDs = new Set<string>();
|
||||||
|
|
||||||
for (const { service: svcUUID, characteristic: charUUID } of device.adapter!.notifyCharacteristics) {
|
for (const { service: svcUUID, characteristic: charUUID } of device.adapter!.notifyCharacteristics) {
|
||||||
const svc = services.find((s: BLEServiceItem) =>
|
const svc = services.find((s: BLEServiceItem) =>
|
||||||
@@ -137,13 +153,90 @@ export class BLEConnection {
|
|||||||
serviceId: svc.uuid,
|
serviceId: svc.uuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Taro.notifyBLECharacteristicValueChange({
|
try {
|
||||||
deviceId: device.deviceId,
|
await Taro.notifyBLECharacteristicValueChange({
|
||||||
serviceId: svc.uuid,
|
deviceId: device.deviceId,
|
||||||
characteristicId: charUUID,
|
serviceId: svc.uuid,
|
||||||
state: true,
|
characteristicId: charUUID,
|
||||||
|
state: true,
|
||||||
|
});
|
||||||
|
subscribedCharUUIDs.add(charUUID.toUpperCase().replace(/-/g, '').slice(-4));
|
||||||
|
console.log(`[ble] 已订阅适配器预定义: ${svcUUID} / ${charUUID}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[ble] 订阅失败 (预定义): ${charUUID}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 第二轮:扫描全部服务,发现并订阅健康相关 Characteristic ──
|
||||||
|
for (const svc of services) {
|
||||||
|
const svcUUID = svc.uuid.toUpperCase();
|
||||||
|
const discoveredChars: BLEDiscoveredCharacteristic[] = [];
|
||||||
|
|
||||||
|
let charsRes: Taro.getBLEDeviceCharacteristics.SuccessCallbackResult;
|
||||||
|
try {
|
||||||
|
charsRes = await Taro.getBLEDeviceCharacteristics({
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
serviceId: svc.uuid,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[ble] 读取特征列表失败: ${svcUUID}`, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characteristics = charsRes.characteristics || [];
|
||||||
|
|
||||||
|
for (const char of characteristics) {
|
||||||
|
const charUUIDShort = char.uuid.toUpperCase().replace(/-/g, '').slice(-4);
|
||||||
|
const props = char.properties || {};
|
||||||
|
|
||||||
|
const discoveredChar: BLEDiscoveredCharacteristic = {
|
||||||
|
uuid: char.uuid,
|
||||||
|
properties: {
|
||||||
|
read: !!props.read,
|
||||||
|
write: !!props.write,
|
||||||
|
notify: !!props.notify,
|
||||||
|
indicate: !!props.indicate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
discoveredChars.push(discoveredChar);
|
||||||
|
|
||||||
|
// 如果是已知的健康 UUID 且尚未订阅,尝试订阅
|
||||||
|
if (
|
||||||
|
BLEConnection.HEALTH_CHAR_UUIDS[charUUIDShort] &&
|
||||||
|
!subscribedCharUUIDs.has(charUUIDShort) &&
|
||||||
|
(props.notify || props.indicate)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await Taro.notifyBLECharacteristicValueChange({
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
serviceId: svc.uuid,
|
||||||
|
characteristicId: char.uuid,
|
||||||
|
state: true,
|
||||||
|
});
|
||||||
|
subscribedCharUUIDs.add(charUUIDShort);
|
||||||
|
console.log(`[ble] 自动发现并订阅: ${BLEConnection.HEALTH_CHAR_UUIDS[charUUIDShort]} (${svcUUID} / ${char.uuid})`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[ble] 自动订阅失败: ${char.uuid}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discoveredServices.push({
|
||||||
|
uuid: svc.uuid,
|
||||||
|
isPrimary: !!svc.isPrimary,
|
||||||
|
characteristics: discoveredChars,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 存储发现结果到连接信息
|
||||||
|
if (this.conn) {
|
||||||
|
this.conn = { ...this.conn, discoveredServices };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ble] 服务发现完成: ${discoveredServices.length} 个服务, 已订阅 ${subscribedCharUUIDs.size} 个特征`);
|
||||||
|
console.log(`[ble] 已订阅的健康特征:`, [...subscribedCharUUIDs].map(
|
||||||
|
(s) => `${s}(${BLEConnection.HEALTH_CHAR_UUIDS[s] ?? '未知'})`
|
||||||
|
).join(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手动读取特征值 */
|
/** 手动读取特征值 */
|
||||||
|
|||||||
@@ -23,8 +23,44 @@ const SERVICES: Record<string, { uuid: string; chars: { notify: string; read: st
|
|||||||
read: '00002A35-0000-1000-8000-00805F9B34FB',
|
read: '00002A35-0000-1000-8000-00805F9B34FB',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pulse_oximeter: {
|
||||||
|
uuid: '00001822-0000-1000-8000-00805F9B34FB',
|
||||||
|
chars: {
|
||||||
|
// PLX Continuous Measurement — 实时血氧+脉率
|
||||||
|
notify: '00002A5F-0000-1000-8000-00805F9B34FB',
|
||||||
|
// PLX Spot-Check Measurement — 单次测量
|
||||||
|
read: '00002A5E-0000-1000-8000-00805F9B34FB',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── IEEE 11073 SFLOAT 解析(Bluetooth SIG 医疗 Profile 通用格式) ──
|
||||||
|
|
||||||
|
/** 特殊 SFLOAT 值 */
|
||||||
|
const SFLOAT_NAN = 0x07FF;
|
||||||
|
const SFLOAT_NRES = 0x0800;
|
||||||
|
const SFLOAT_POS_INF = 0x07FE;
|
||||||
|
const SFLOAT_NEG_INF = 0x0802;
|
||||||
|
|
||||||
|
function parseSFLOAT(view: DataView, offset: number): number | null {
|
||||||
|
if (offset + 2 > view.byteLength) return null;
|
||||||
|
const raw = view.getUint16(offset, true);
|
||||||
|
|
||||||
|
if (raw === SFLOAT_NAN || raw === SFLOAT_NRES) return null;
|
||||||
|
if (raw === SFLOAT_POS_INF) return Infinity;
|
||||||
|
if (raw === SFLOAT_NEG_INF) return -Infinity;
|
||||||
|
|
||||||
|
const signM = (raw >> 15) & 0x01;
|
||||||
|
const exp = (raw >> 12) & 0x07;
|
||||||
|
const mantissa = raw & 0x0FFF;
|
||||||
|
|
||||||
|
// 指数用 3 位补码表示(0-3 正,4-7 负)
|
||||||
|
const exponent = exp >= 4 ? exp - 8 : exp;
|
||||||
|
const signedMantissa = signM ? -(mantissa ^ 0x0FFF) - 1 : mantissa;
|
||||||
|
|
||||||
|
return signedMantissa * Math.pow(10, exponent);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 解析器 ──
|
// ── 解析器 ──
|
||||||
|
|
||||||
function parseHeartRate(data: ArrayBuffer): NormalizedReading | null {
|
function parseHeartRate(data: ArrayBuffer): NormalizedReading | null {
|
||||||
@@ -66,6 +102,39 @@ function parseTemperature(data: ArrayBuffer): NormalizedReading | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Pulse Oximeter Service 数据
|
||||||
|
* PLX Continuous Measurement (0x2A5F) 和 Spot-Check (0x2A5E) 共用
|
||||||
|
* 格式: Flags(1B) + SpO2(SFLOAT 2B) + PulseRate(SFLOAT 2B) + optional...
|
||||||
|
*/
|
||||||
|
function parsePulseOximeter(data: ArrayBuffer): NormalizedReading[] {
|
||||||
|
const view = new DataView(data);
|
||||||
|
if (view.byteLength < 5) return [];
|
||||||
|
|
||||||
|
const spO2 = parseSFLOAT(view, 1);
|
||||||
|
const pulseRate = parseSFLOAT(view, 3);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const results: NormalizedReading[] = [];
|
||||||
|
|
||||||
|
if (spO2 !== null && spO2 >= 0 && spO2 <= 100) {
|
||||||
|
results.push({
|
||||||
|
device_type: 'blood_oxygen',
|
||||||
|
values: { blood_oxygen: Math.round(spO2), unit: '%' },
|
||||||
|
measured_at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pulseRate !== null && pulseRate > 0 && pulseRate <= 300) {
|
||||||
|
results.push({
|
||||||
|
device_type: 'heart_rate',
|
||||||
|
values: { heart_rate: Math.round(pulseRate) },
|
||||||
|
measured_at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 工厂函数 ──
|
// ── 工厂函数 ──
|
||||||
|
|
||||||
export interface GenericAdapterConfig {
|
export interface GenericAdapterConfig {
|
||||||
@@ -100,20 +169,23 @@ export function createGenericBleAdapter(config: GenericAdapterConfig): DeviceAda
|
|||||||
): NormalizedReading[] {
|
): NormalizedReading[] {
|
||||||
const upper = charUUID.toUpperCase();
|
const upper = charUUID.toUpperCase();
|
||||||
|
|
||||||
// Heart Rate Measurement
|
// Heart Rate Measurement (0x2A37)
|
||||||
const hrsChar = SERVICES.heart_rate.chars.notify.toUpperCase();
|
if (upper.includes('2A37')) {
|
||||||
if (upper === hrsChar || upper.includes('2A37')) {
|
|
||||||
const result = parseHeartRate(data);
|
const result = parseHeartRate(data);
|
||||||
return result ? [result] : [];
|
return result ? [result] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature Measurement
|
// Temperature Measurement (0x2A1C)
|
||||||
const htChar = SERVICES.health_thermometer.chars.notify.toUpperCase();
|
if (upper.includes('2A1C')) {
|
||||||
if (upper === htChar || upper.includes('2A1C')) {
|
|
||||||
const result = parseTemperature(data);
|
const result = parseTemperature(data);
|
||||||
return result ? [result] : [];
|
return result ? [result] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pulse Oximeter Continuous (0x2A5F) / Spot-Check (0x2A5E)
|
||||||
|
if (upper.includes('2A5F') || upper.includes('2A5E')) {
|
||||||
|
return parsePulseOximeter(data);
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -155,7 +227,7 @@ export const HuaweiBandAdapter = createGenericBleAdapter({
|
|||||||
'华为手环',
|
'华为手环',
|
||||||
'华为手表',
|
'华为手表',
|
||||||
],
|
],
|
||||||
profiles: ['heart_rate', 'health_thermometer'],
|
profiles: ['heart_rate', 'health_thermometer', 'pulse_oximeter'],
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export interface BLEConnection {
|
|||||||
adapter: DeviceAdapter;
|
adapter: DeviceAdapter;
|
||||||
connectedAt?: number;
|
connectedAt?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
/** 连接后扫描到的全部服务(用于调试和展示) */
|
||||||
|
discoveredServices?: BLEDiscoveredService[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 同步操作结果 */
|
/** 同步操作结果 */
|
||||||
@@ -96,7 +98,20 @@ export interface BLEManagerConfig {
|
|||||||
export type GenericBLEProfile =
|
export type GenericBLEProfile =
|
||||||
| 'heart_rate' // Heart Rate Service (0x180D)
|
| 'heart_rate' // Heart Rate Service (0x180D)
|
||||||
| 'health_thermometer' // Health Thermometer Service (0x1809)
|
| 'health_thermometer' // Health Thermometer Service (0x1809)
|
||||||
| 'blood_pressure'; // Blood Pressure Service (0x1810)
|
| 'blood_pressure' // Blood Pressure Service (0x1810)
|
||||||
|
| 'pulse_oximeter'; // Pulse Oximeter Service (0x1822)
|
||||||
|
|
||||||
|
/** BLE 服务发现结果(连接后扫描到的全部服务/特征) */
|
||||||
|
export interface BLEDiscoveredCharacteristic {
|
||||||
|
uuid: string;
|
||||||
|
properties: { read: boolean; write: boolean; notify: boolean; indicate: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BLEDiscoveredService {
|
||||||
|
uuid: string;
|
||||||
|
isPrimary: boolean;
|
||||||
|
characteristics: BLEDiscoveredCharacteristic[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 微信 BLE 扫描回调结果 */
|
/** 微信 BLE 扫描回调结果 */
|
||||||
export interface BLEScanResult {
|
export interface BLEScanResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user