From 9b0815383bba172211127e99eaeeb7feb437ef90 Mon Sep 17 00:00:00 2001
From: hc <haocheng.xie@respiree.com>
Date: Mon, 13 Apr 2026 17:25:45 +0800
Subject: Add PO3, PT3SBT, HS2S, BG5S device support with verified SDK
 constants

---
 App.tsx | 363 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 298 insertions(+), 65 deletions(-)

(limited to 'App.tsx')

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<string, {module: {Event_Notify: string; disconnect: (mac: string) => 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 = <T,>(
+      eventEmitter: typeof DeviceEventEmitter,
+      eventName: string,
+      match: (ev: Record<string, unknown>) => T | undefined,
+      timeoutMs = 5000,
+    ): Promise<T | undefined> =>
+      new Promise(resolve => {
+        const sub = eventEmitter.addListener(eventName, (ev: Record<string, unknown>) => {
+          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<string, unknown>, results: Record<string, unknown>[]) => boolean, // return true when done
+      timeoutMs = 10000,
+    ): Promise<Record<string, unknown>[]> =>
+      new Promise(resolve => {
+        const results: Record<string, unknown>[] = [];
+        const sub = eventEmitter.addListener(eventName, (ev: Record<string, unknown>) => {
+          if (collect(ev, results)) { sub.remove(); resolve(results); }
+        });
+        setTimeout(() => { sub.remove(); resolve(results); }, timeoutMs);
+      });
+
+    // ── Per-device sync logic ──
+    const syncBP550 = async (mac: string): Promise<Reading[]> => {
+      const notify = bp550?.Event_Notify ?? 'event_notify';
+      const bpEm = getBPEmitter();
+      // Start listening BEFORE calling native method
+      const batteryPromise = awaitEvent<number>(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<string, unknown>[]));
+        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<Reading[]> => {
+      const notify = po3?.Event_Notify ?? 'event_notify';
+      const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.PO3Module) : DeviceEventEmitter;
+      const bp = awaitEvent<number>(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<string, unknown>[]));
+        }
+        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<Reading[]> => {
+      const notify = pt3sbt?.Event_Notify ?? 'event_notify';
+      const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.PT3SBTModule) : DeviceEventEmitter;
+      const bp = awaitEvent<number>(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<string, unknown>[]));
+          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<Reading[]> => {
+      const notify = hs2s?.Event_Notify ?? 'event_notify';
+      const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.HS2SModule) : DeviceEventEmitter;
+      const bp = awaitEvent<number>(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<Reading[]> => {
+      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<string, unknown>[]));
+          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<number | undefined>(resolve => {
-          const sub = bpEmitter.addListener(
-            bp550?.Event_Notify ?? 'event_notify',
-            (ev: Record<string, unknown>) => {
-              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<Record<string, unknown>[]>(resolve => {
-          const results: Record<string, unknown>[] = [];
-          const sub = bpEmitter.addListener(
-            bp550?.Event_Notify ?? 'event_notify',
-            (ev: Record<string, unknown>) => {
-              if (ev.action === 'historicaldata_bp' && ev.data) {
-                results.push(...(ev.data as Record<string, unknown>[]));
-              }
-              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 => (
           <View key={d.mac} style={s.savedRow}>
             <View style={[s.deviceIcon, d.saved && s.deviceIconSaved, !d.nearby && s.deviceOffline]}>
-              <Text style={s.deviceIconText}>BP</Text>
+              <Text style={s.deviceIconText}>{d.type.substring(0, 3)}</Text>
             </View>
             <View style={[s.deviceInfo, !d.nearby && s.deviceOffline]}>
               <Text style={s.deviceType}>
@@ -423,15 +626,45 @@ function DashboardScreen({onBack}: {onBack: () => void}) {
         style={s.list}
         renderItem={({item}) => (
           <View style={s.readingRow}>
-            <View style={s.readingValues}>
-              <Text style={s.readingSys}>{item.sys ?? '--'}</Text>
-              <Text style={s.readingSlash}>/</Text>
-              <Text style={s.readingDia}>{item.dia ?? '--'}</Text>
-              <Text style={s.readingUnit}>mmHg</Text>
-              <Text style={s.readingPulse}>{item.pulse ?? '--'} bpm</Text>
-            </View>
+            {item.sys != null && (
+              <View style={s.readingValues}>
+                <Text style={s.readingSys}>{item.sys}</Text>
+                <Text style={s.readingSlash}>/</Text>
+                <Text style={s.readingDia}>{item.dia ?? '--'}</Text>
+                <Text style={s.readingUnit}>mmHg</Text>
+                <Text style={s.readingPulse}>{item.pulse ?? '--'} bpm</Text>
+              </View>
+            )}
+            {item.spo2 != null && (
+              <View style={s.readingValues}>
+                <Text style={s.readingSys}>{item.spo2}%</Text>
+                <Text style={s.readingUnit}>SpO2</Text>
+                <Text style={s.readingPulse}>{item.pulseRate ?? '--'} bpm</Text>
+              </View>
+            )}
+            {item.temperature != null && (
+              <View style={s.readingValues}>
+                <Text style={s.readingSys}>{item.temperature}</Text>
+                <Text style={s.readingUnit}>{item.tempUnit ?? 'C'}</Text>
+              </View>
+            )}
+            {item.weight != null && (
+              <View style={s.readingValues}>
+                <Text style={s.readingSys}>{item.weight}</Text>
+                <Text style={s.readingUnit}>kg</Text>
+                {item.bodyFat != null && <Text style={s.readingPulse}>{item.bodyFat}% fat</Text>}
+                {item.bmi != null && <Text style={s.readingPulse}>BMI {item.bmi}</Text>}
+              </View>
+            )}
+            {item.glucose != null && (
+              <View style={s.readingValues}>
+                <Text style={s.readingSys}>{item.glucose}</Text>
+                <Text style={s.readingUnit}>mg/dL</Text>
+              </View>
+            )}
             <View style={s.readingMeta}>
               <Text style={s.readingDate}>
+                {item.deviceType ? `${item.deviceType}  ` : ''}
                 {item.date ?? item.fetchedAt.split('T')[0]}
               </Text>
               <Text style={s.readingMac}>{item.mac}</Text>
-- 
cgit 

