From 9b0815383bba172211127e99eaeeb7feb437ef90 Mon Sep 17 00:00:00 2001 From: hc Date: Mon, 13 Apr 2026 17:25:45 +0800 Subject: Add PO3, PT3SBT, HS2S, BG5S device support with verified SDK constants --- App.tsx | 363 ++++++++++++++++++++++++++++++++++++++++++++---------- DEVICE_STATUS.txt | 17 +++ 2 files changed, 315 insertions(+), 65 deletions(-) create mode 100644 DEVICE_STATUS.txt diff --git a/App.tsx b/App.tsx index 4ce26df..a8a2083 100644 --- a/App.tsx +++ b/App.tsx @@ -60,8 +60,56 @@ interface IBP550BTModule { getAllConnectedDevices(): void; } +interface IPO3Module { + Event_Notify: string; + getBattery(mac: string): void; + startMeasure(mac: string): void; + getHistoryData(mac: string): void; + disconnect(mac: string): void; +} + +interface IPT3SBTModule { + Event_Notify: string; + getBattery(mac: string): void; + getHistoryData(mac: string): void; + getHistoryCount(mac: string): void; + setUnit(mac: string, unit: number): void; + disconnect(mac: string): void; +} + +interface IHS2SModule { + Event_Notify: string; + getBattery(mac: string): void; + getMemoryDataCount(mac: string, id: number): void; + getMemoryData(mac: string, id: number): void; + getAnonymousMemoryData(mac: string): void; + disconnect(mac: string): void; +} + +interface IBG5SModule { + Event_Notify: string; + getStatusInfo(mac: string): void; + getOfflineData(mac: string): void; + startMeasure(mac: string, type: number): void; + disConnect(mac: string): void; +} + const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; const bp550 = NativeModules.BP550BTModule as IBP550BTModule; +const po3 = NativeModules.PO3Module as IPO3Module; +const pt3sbt = NativeModules.PT3SBTModule as IPT3SBTModule; +const hs2s = NativeModules.HS2SModule as IHS2SModule; +const bg5s = NativeModules.BG5SModule as IBG5SModule; + +// Device type → module mapping +const DEVICE_MODULES: Record void} | null; label: string}> = { + KN550: {module: bp550, label: 'Blood Pressure'}, + 'KN-550BT': {module: bp550, label: 'Blood Pressure'}, + PO3: {module: po3, label: 'Pulse Oximeter'}, + PT3SBT: {module: pt3sbt, label: 'Thermometer'}, + HS2S: {module: hs2s, label: 'Scale'}, + BG5S: {module: bg5s, label: 'Glucose Monitor'}, +}; // ── Types ──────────────────────────────────────────────────────────── @@ -71,11 +119,26 @@ type SavedDevice = {mac: string; type: string; addedAt: string}; type Reading = { mac: string; + deviceType?: string; + // BP sys?: number; dia?: number; pulse?: number; - date?: string; + // SpO2 + spo2?: number; + pulseRate?: number; + // Temperature + temperature?: number; + tempUnit?: string; + // Weight/Scale + weight?: number; + bodyFat?: number; + bmi?: number; + // Glucose + glucose?: number; + // Common battery?: number; + date?: string; fetchedAt: string; }; @@ -218,69 +281,209 @@ function DashboardScreen({onBack}: {onBack: () => void}) { }, ); - // When connected, pull data + // Helper: listen for a native event, resolve on match, timeout + const awaitEvent = ( + eventEmitter: typeof DeviceEventEmitter, + eventName: string, + match: (ev: Record) => T | undefined, + timeoutMs = 5000, + ): Promise => + new Promise(resolve => { + const sub = eventEmitter.addListener(eventName, (ev: Record) => { + const result = match(ev); + if (result !== undefined) { sub.remove(); resolve(result); } + }); + setTimeout(() => { sub.remove(); resolve(undefined); }, timeoutMs); + }); + + // Helper: collect events into array until done signal + const collectEvents = ( + eventEmitter: typeof DeviceEventEmitter, + eventName: string, + collect: (ev: Record, results: Record[]) => boolean, // return true when done + timeoutMs = 10000, + ): Promise[]> => + new Promise(resolve => { + const results: Record[] = []; + const sub = eventEmitter.addListener(eventName, (ev: Record) => { + if (collect(ev, results)) { sub.remove(); resolve(results); } + }); + setTimeout(() => { sub.remove(); resolve(results); }, timeoutMs); + }); + + // ── Per-device sync logic ── + const syncBP550 = async (mac: string): Promise => { + const notify = bp550?.Event_Notify ?? 'event_notify'; + const bpEm = getBPEmitter(); + // Start listening BEFORE calling native method + const batteryPromise = awaitEvent(bpEm, notify, + ev => ev.action === 'battery_bp' && ev.battery != null ? ev.battery as number : undefined); + bp550.getBattery(mac); + const battery = await batteryPromise; + + const dataPromise = collectEvents(bpEm, notify, (ev, res) => { + if (ev.action === 'historicaldata_bp' && ev.data) + res.push(...(ev.data as Record[])); + return ev.action === 'get_historical_over_bp' || (ev.action === 'offlinenum' && ev.offlinenum === 0); + }); + bp550.getOffLineData(mac); + const data = await dataPromise; + + const readings: Reading[] = data.map(d => ({ + mac, deviceType: 'BP', sys: d.sys as number, dia: d.dia as number, + pulse: d.heartRate as number, date: d.date as string, + battery, fetchedAt: new Date().toISOString(), + })); + if (readings.length === 0 && battery != null) + readings.push({mac, deviceType: 'BP', battery, fetchedAt: new Date().toISOString()}); + try { bp550.disconnect(mac); } catch (_) {} + return readings; + }; + + const syncPO3 = async (mac: string): Promise => { + const notify = po3?.Event_Notify ?? 'event_notify'; + const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.PO3Module) : DeviceEventEmitter; + const bp = awaitEvent(em, notify, + ev => ev.action === 'battery_po' && ev.battery != null ? ev.battery as number : undefined); + po3.getBattery(mac); + const battery = await bp; + + const dp = collectEvents(em, notify, (ev, res) => { + if (ev.action === 'offlineData_po' && ev.offlinedata) { + const arr = Array.isArray(ev.offlinedata) ? ev.offlinedata : [ev.offlinedata]; + res.push(...(arr as Record[])); + } + return ev.action === 'offlineData_po' || ev.action === 'noOfflineData_po'; + }); + po3.getHistoryData(mac); + const data = await dp; + + const readings: Reading[] = data.map(d => ({ + mac, deviceType: 'SpO2', spo2: d.bloodoxygen as number, + pulseRate: d.heartrate as number, date: d.measuredate as string, + battery, fetchedAt: new Date().toISOString(), + })); + if (readings.length === 0 && battery != null) + readings.push({mac, deviceType: 'SpO2', battery, fetchedAt: new Date().toISOString()}); + try { po3.disconnect(mac); } catch (_) {} + return readings; + }; + + const syncPT3SBT = async (mac: string): Promise => { + const notify = pt3sbt?.Event_Notify ?? 'event_notify'; + const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.PT3SBTModule) : DeviceEventEmitter; + const bp = awaitEvent(em, notify, + ev => ev.action === 'action_get_battery' && ev.battery != null ? ev.battery as number : undefined); + pt3sbt.getBattery(mac); + const battery = await bp; + + const dp = collectEvents(em, notify, (ev, res) => { + if (ev.action === 'action_get_history_data' && ev.history) { + const arr = Array.isArray(ev.history) ? ev.history : [ev.history]; + res.push(...(arr as Record[])); + return true; + } + return false; + }); + pt3sbt.getHistoryData(mac); + const data = await dp; + + const readings: Reading[] = data.map(d => ({ + mac, deviceType: 'Temp', temperature: d.Tbody as number, + date: d.ts as string, battery, fetchedAt: new Date().toISOString(), + })); + if (readings.length === 0 && battery != null) + readings.push({mac, deviceType: 'Temp', battery, fetchedAt: new Date().toISOString()}); + try { pt3sbt.disconnect(mac); } catch (_) {} + return readings; + }; + + const syncHS2S = async (mac: string): Promise => { + const notify = hs2s?.Event_Notify ?? 'event_notify'; + const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.HS2SModule) : DeviceEventEmitter; + const bp = awaitEvent(em, notify, + ev => ev.action === 'battery_hs' && ev.battery != null ? ev.battery as number : undefined); + hs2s.getBattery(mac); + const battery = await bp; + + const dp = collectEvents(em, notify, (ev, res) => { + if (ev.action === 'action_history_data' && ev.weight != null) { + res.push(ev); + } + return ev.action === 'action_anonymous_data_num' || ev.action === 'action_anonymous_data'; + }); + hs2s.getAnonymousMemoryData(mac); + const data = await dp; + + const readings: Reading[] = data.map(d => ({ + mac, deviceType: 'Scale', weight: d.weight as number, + bodyFat: d.body_fit_percentage as number, + bmi: d.body_mass_index as number, + date: d.data_measure_time as string, + battery, fetchedAt: new Date().toISOString(), + })); + if (readings.length === 0 && battery != null) + readings.push({mac, deviceType: 'Scale', battery, fetchedAt: new Date().toISOString()}); + try { hs2s.disconnect(mac); } catch (_) {} + return readings; + }; + + const syncBG5S = async (mac: string): Promise => { + const notify = bg5s?.Event_Notify ?? 'event_notify'; + const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.BG5SModule) : DeviceEventEmitter; + + const dp = collectEvents(em, notify, (ev, res) => { + if (ev.action === 'action_get_offline_data' && ev.offline_data) { + const arr = Array.isArray(ev.offline_data) ? ev.offline_data : [ev.offline_data]; + res.push(...(arr as Record[])); + return true; + } + if (ev.action === 'action_get_status_info' && ev.info_offline_data_num === 0) return true; + return false; + }); + bg5s.getOfflineData(mac); + const data = await dp; + + const readings: Reading[] = data.map(d => ({ + mac, deviceType: 'Glucose', glucose: d.data_value as number, + date: d.data_measure_time as string, + fetchedAt: new Date().toISOString(), + })); + try { bg5s.disConnect(mac); } catch (_) {} + return readings; + }; + + // When connected, pull data based on device type const connSub = emitter.addListener( mgr.Event_Device_Connected ?? 'event_device_connected', async (e: {mac: string; type: string}) => { if (e.mac !== syncingMac.current) return; setStatus(`Connected to ${e.mac.slice(-4)}, reading...`); - // Get battery - const battery = await new Promise(resolve => { - const sub = bpEmitter.addListener( - bp550?.Event_Notify ?? 'event_notify', - (ev: Record) => { - if (ev.action === 'battery_bp' && ev.battery != null) { - sub.remove(); resolve(ev.battery as number); - } - }, - ); - bp550.getBattery(e.mac); - setTimeout(() => { sub.remove(); resolve(undefined); }, 5000); - }); - - // Get offline data - const offlineData = await new Promise[]>(resolve => { - const results: Record[] = []; - const sub = bpEmitter.addListener( - bp550?.Event_Notify ?? 'event_notify', - (ev: Record) => { - if (ev.action === 'historicaldata_bp' && ev.data) { - results.push(...(ev.data as Record[])); - } - if (ev.action === 'get_historical_over_bp') { - sub.remove(); resolve(results); - } - if (ev.action === 'offlinenum' && ev.offlinenum === 0) { - sub.remove(); resolve(results); - } - }, - ); - bp550.getOffLineData(e.mac); - setTimeout(() => { sub.remove(); resolve(results); }, 10000); - }); - - // Save new readings - const newReadings = [...readingsRef.current]; - for (const d of offlineData) { - newReadings.push({ - mac: e.mac, sys: d.sys as number | undefined, - dia: d.dia as number | undefined, - pulse: d.heartRate as number | undefined, - date: d.date as string | undefined, - battery, fetchedAt: new Date().toISOString(), - }); + let newReadings: Reading[] = []; + try { + switch (e.type) { + case 'KN550': case 'KN-550BT': newReadings = await syncBP550(e.mac); break; + case 'PO3': newReadings = await syncPO3(e.mac); break; + case 'PT3SBT': newReadings = await syncPT3SBT(e.mac); break; + case 'HS2S': newReadings = await syncHS2S(e.mac); break; + case 'BG5S': newReadings = await syncBG5S(e.mac); break; + default: + // Unknown device — just disconnect + try { mgr.disconnectDevice(e.mac, e.type); } catch (_) {} + } + } catch (err) { + console.log('Sync error:', err); } - if (offlineData.length === 0 && battery != null) { - newReadings.push({mac: e.mac, battery, fetchedAt: new Date().toISOString()}); + + if (newReadings.length > 0) { + const all = [...readingsRef.current, ...newReadings]; + readingsRef.current = all; + setReadings(all); + await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(all)); } - readingsRef.current = newReadings; - setReadings(newReadings); - await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(newReadings)); - // Disconnect and resume scanning - try { bp550.disconnect(e.mac); } catch (_) {} - setStatus(`Synced ${e.mac.slice(-4)} (${offlineData.length} readings)`); + setStatus(`Synced ${e.mac.slice(-4)} (${newReadings.length} readings)`); syncingMac.current = null; setTimeout(startScan, 3000); }, @@ -336,8 +539,8 @@ function DashboardScreen({onBack}: {onBack: () => void}) { const isDeviceSaved = (mac: string) => savedRef.current.some(d => d.mac === mac); const recentReadings = readings - .filter(r => r.sys != null) - .slice(-20) + .filter(r => r.sys != null || r.spo2 != null || r.temperature != null || r.weight != null || r.glucose != null) + .slice(-30) .reverse(); return ( @@ -387,7 +590,7 @@ function DashboardScreen({onBack}: {onBack: () => void}) { return merged.map(d => ( - BP + {d.type.substring(0, 3)} @@ -423,15 +626,45 @@ function DashboardScreen({onBack}: {onBack: () => void}) { style={s.list} renderItem={({item}) => ( - - {item.sys ?? '--'} - / - {item.dia ?? '--'} - mmHg - {item.pulse ?? '--'} bpm - + {item.sys != null && ( + + {item.sys} + / + {item.dia ?? '--'} + mmHg + {item.pulse ?? '--'} bpm + + )} + {item.spo2 != null && ( + + {item.spo2}% + SpO2 + {item.pulseRate ?? '--'} bpm + + )} + {item.temperature != null && ( + + {item.temperature} + {item.tempUnit ?? 'C'} + + )} + {item.weight != null && ( + + {item.weight} + kg + {item.bodyFat != null && {item.bodyFat}% fat} + {item.bmi != null && BMI {item.bmi}} + + )} + {item.glucose != null && ( + + {item.glucose} + mg/dL + + )} + {item.deviceType ? `${item.deviceType} ` : ''} {item.date ?? item.fetchedAt.split('T')[0]} {item.mac} diff --git a/DEVICE_STATUS.txt b/DEVICE_STATUS.txt new file mode 100644 index 0000000..9b0b172 --- /dev/null +++ b/DEVICE_STATUS.txt @@ -0,0 +1,17 @@ +Device Support Status +===================== + +TESTED & WORKING: +- KN-550BT (BP550BT) — Blood Pressure Monitor + Scan, connect, get battery, get offline data all verified on Android. + +NOT YET TESTED: +- PO3 — Pulse Oximeter +- PT3SBT — Infrared Thermometer +- HS2S — Wireless Scale +- BG5S — Blood Glucose Monitor + +These 4 devices have sync logic implemented based on decompiled SDK +constant values, but have not been tested with physical hardware. +The event action names and data field keys may need adjustment once +tested with real devices. -- cgit