summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--App.tsx363
-rw-r--r--DEVICE_STATUS.txt17
2 files changed, 315 insertions, 65 deletions
diff --git a/App.tsx b/App.tsx
index 4ce26df..a8a2083 100644
--- a/App.tsx
+++ b/App.tsx
@@ -60,8 +60,56 @@ interface IBP550BTModule {
60 getAllConnectedDevices(): void; 60 getAllConnectedDevices(): void;
61} 61}
62 62
63interface IPO3Module {
64 Event_Notify: string;
65 getBattery(mac: string): void;
66 startMeasure(mac: string): void;
67 getHistoryData(mac: string): void;
68 disconnect(mac: string): void;
69}
70
71interface IPT3SBTModule {
72 Event_Notify: string;
73 getBattery(mac: string): void;
74 getHistoryData(mac: string): void;
75 getHistoryCount(mac: string): void;
76 setUnit(mac: string, unit: number): void;
77 disconnect(mac: string): void;
78}
79
80interface IHS2SModule {
81 Event_Notify: string;
82 getBattery(mac: string): void;
83 getMemoryDataCount(mac: string, id: number): void;
84 getMemoryData(mac: string, id: number): void;
85 getAnonymousMemoryData(mac: string): void;
86 disconnect(mac: string): void;
87}
88
89interface IBG5SModule {
90 Event_Notify: string;
91 getStatusInfo(mac: string): void;
92 getOfflineData(mac: string): void;
93 startMeasure(mac: string, type: number): void;
94 disConnect(mac: string): void;
95}
96
63const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; 97const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager;
64const bp550 = NativeModules.BP550BTModule as IBP550BTModule; 98const bp550 = NativeModules.BP550BTModule as IBP550BTModule;
99const po3 = NativeModules.PO3Module as IPO3Module;
100const pt3sbt = NativeModules.PT3SBTModule as IPT3SBTModule;
101const hs2s = NativeModules.HS2SModule as IHS2SModule;
102const bg5s = NativeModules.BG5SModule as IBG5SModule;
103
104// Device type → module mapping
105const DEVICE_MODULES: Record<string, {module: {Event_Notify: string; disconnect: (mac: string) => void} | null; label: string}> = {
106 KN550: {module: bp550, label: 'Blood Pressure'},
107 'KN-550BT': {module: bp550, label: 'Blood Pressure'},
108 PO3: {module: po3, label: 'Pulse Oximeter'},
109 PT3SBT: {module: pt3sbt, label: 'Thermometer'},
110 HS2S: {module: hs2s, label: 'Scale'},
111 BG5S: {module: bg5s, label: 'Glucose Monitor'},
112};
65 113
66// ── Types ──────────────────────────────────────────────────────────── 114// ── Types ────────────────────────────────────────────────────────────
67 115
@@ -71,11 +119,26 @@ type SavedDevice = {mac: string; type: string; addedAt: string};
71 119
72type Reading = { 120type Reading = {
73 mac: string; 121 mac: string;
122 deviceType?: string;
123 // BP
74 sys?: number; 124 sys?: number;
75 dia?: number; 125 dia?: number;
76 pulse?: number; 126 pulse?: number;
77 date?: string; 127 // SpO2
128 spo2?: number;
129 pulseRate?: number;
130 // Temperature
131 temperature?: number;
132 tempUnit?: string;
133 // Weight/Scale
134 weight?: number;
135 bodyFat?: number;
136 bmi?: number;
137 // Glucose
138 glucose?: number;
139 // Common
78 battery?: number; 140 battery?: number;
141 date?: string;
79 fetchedAt: string; 142 fetchedAt: string;
80}; 143};
81 144
@@ -218,69 +281,209 @@ function DashboardScreen({onBack}: {onBack: () => void}) {
218 }, 281 },
219 ); 282 );
220 283
221 // When connected, pull data 284 // Helper: listen for a native event, resolve on match, timeout
285 const awaitEvent = <T,>(
286 eventEmitter: typeof DeviceEventEmitter,
287 eventName: string,
288 match: (ev: Record<string, unknown>) => T | undefined,
289 timeoutMs = 5000,
290 ): Promise<T | undefined> =>
291 new Promise(resolve => {
292 const sub = eventEmitter.addListener(eventName, (ev: Record<string, unknown>) => {
293 const result = match(ev);
294 if (result !== undefined) { sub.remove(); resolve(result); }
295 });
296 setTimeout(() => { sub.remove(); resolve(undefined); }, timeoutMs);
297 });
298
299 // Helper: collect events into array until done signal
300 const collectEvents = (
301 eventEmitter: typeof DeviceEventEmitter,
302 eventName: string,
303 collect: (ev: Record<string, unknown>, results: Record<string, unknown>[]) => boolean, // return true when done
304 timeoutMs = 10000,
305 ): Promise<Record<string, unknown>[]> =>
306 new Promise(resolve => {
307 const results: Record<string, unknown>[] = [];
308 const sub = eventEmitter.addListener(eventName, (ev: Record<string, unknown>) => {
309 if (collect(ev, results)) { sub.remove(); resolve(results); }
310 });
311 setTimeout(() => { sub.remove(); resolve(results); }, timeoutMs);
312 });
313
314 // ── Per-device sync logic ──
315 const syncBP550 = async (mac: string): Promise<Reading[]> => {
316 const notify = bp550?.Event_Notify ?? 'event_notify';
317 const bpEm = getBPEmitter();
318 // Start listening BEFORE calling native method
319 const batteryPromise = awaitEvent<number>(bpEm, notify,
320 ev => ev.action === 'battery_bp' && ev.battery != null ? ev.battery as number : undefined);
321 bp550.getBattery(mac);
322 const battery = await batteryPromise;
323
324 const dataPromise = collectEvents(bpEm, notify, (ev, res) => {
325 if (ev.action === 'historicaldata_bp' && ev.data)
326 res.push(...(ev.data as Record<string, unknown>[]));
327 return ev.action === 'get_historical_over_bp' || (ev.action === 'offlinenum' && ev.offlinenum === 0);
328 });
329 bp550.getOffLineData(mac);
330 const data = await dataPromise;
331
332 const readings: Reading[] = data.map(d => ({
333 mac, deviceType: 'BP', sys: d.sys as number, dia: d.dia as number,
334 pulse: d.heartRate as number, date: d.date as string,
335 battery, fetchedAt: new Date().toISOString(),
336 }));
337 if (readings.length === 0 && battery != null)
338 readings.push({mac, deviceType: 'BP', battery, fetchedAt: new Date().toISOString()});
339 try { bp550.disconnect(mac); } catch (_) {}
340 return readings;
341 };
342
343 const syncPO3 = async (mac: string): Promise<Reading[]> => {
344 const notify = po3?.Event_Notify ?? 'event_notify';
345 const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.PO3Module) : DeviceEventEmitter;
346 const bp = awaitEvent<number>(em, notify,
347 ev => ev.action === 'battery_po' && ev.battery != null ? ev.battery as number : undefined);
348 po3.getBattery(mac);
349 const battery = await bp;
350
351 const dp = collectEvents(em, notify, (ev, res) => {
352 if (ev.action === 'offlineData_po' && ev.offlinedata) {
353 const arr = Array.isArray(ev.offlinedata) ? ev.offlinedata : [ev.offlinedata];
354 res.push(...(arr as Record<string, unknown>[]));
355 }
356 return ev.action === 'offlineData_po' || ev.action === 'noOfflineData_po';
357 });
358 po3.getHistoryData(mac);
359 const data = await dp;
360
361 const readings: Reading[] = data.map(d => ({
362 mac, deviceType: 'SpO2', spo2: d.bloodoxygen as number,
363 pulseRate: d.heartrate as number, date: d.measuredate as string,
364 battery, fetchedAt: new Date().toISOString(),
365 }));
366 if (readings.length === 0 && battery != null)
367 readings.push({mac, deviceType: 'SpO2', battery, fetchedAt: new Date().toISOString()});
368 try { po3.disconnect(mac); } catch (_) {}
369 return readings;
370 };
371
372 const syncPT3SBT = async (mac: string): Promise<Reading[]> => {
373 const notify = pt3sbt?.Event_Notify ?? 'event_notify';
374 const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.PT3SBTModule) : DeviceEventEmitter;
375 const bp = awaitEvent<number>(em, notify,
376 ev => ev.action === 'action_get_battery' && ev.battery != null ? ev.battery as number : undefined);
377 pt3sbt.getBattery(mac);
378 const battery = await bp;
379
380 const dp = collectEvents(em, notify, (ev, res) => {
381 if (ev.action === 'action_get_history_data' && ev.history) {
382 const arr = Array.isArray(ev.history) ? ev.history : [ev.history];
383 res.push(...(arr as Record<string, unknown>[]));
384 return true;
385 }
386 return false;
387 });
388 pt3sbt.getHistoryData(mac);
389 const data = await dp;
390
391 const readings: Reading[] = data.map(d => ({
392 mac, deviceType: 'Temp', temperature: d.Tbody as number,
393 date: d.ts as string, battery, fetchedAt: new Date().toISOString(),
394 }));
395 if (readings.length === 0 && battery != null)
396 readings.push({mac, deviceType: 'Temp', battery, fetchedAt: new Date().toISOString()});
397 try { pt3sbt.disconnect(mac); } catch (_) {}
398 return readings;
399 };
400
401 const syncHS2S = async (mac: string): Promise<Reading[]> => {
402 const notify = hs2s?.Event_Notify ?? 'event_notify';
403 const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.HS2SModule) : DeviceEventEmitter;
404 const bp = awaitEvent<number>(em, notify,
405 ev => ev.action === 'battery_hs' && ev.battery != null ? ev.battery as number : undefined);
406 hs2s.getBattery(mac);
407 const battery = await bp;
408
409 const dp = collectEvents(em, notify, (ev, res) => {
410 if (ev.action === 'action_history_data' && ev.weight != null) {
411 res.push(ev);
412 }
413 return ev.action === 'action_anonymous_data_num' || ev.action === 'action_anonymous_data';
414 });
415 hs2s.getAnonymousMemoryData(mac);
416 const data = await dp;
417
418 const readings: Reading[] = data.map(d => ({
419 mac, deviceType: 'Scale', weight: d.weight as number,
420 bodyFat: d.body_fit_percentage as number,
421 bmi: d.body_mass_index as number,
422 date: d.data_measure_time as string,
423 battery, fetchedAt: new Date().toISOString(),
424 }));
425 if (readings.length === 0 && battery != null)
426 readings.push({mac, deviceType: 'Scale', battery, fetchedAt: new Date().toISOString()});
427 try { hs2s.disconnect(mac); } catch (_) {}
428 return readings;
429 };
430
431 const syncBG5S = async (mac: string): Promise<Reading[]> => {
432 const notify = bg5s?.Event_Notify ?? 'event_notify';
433 const em = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.BG5SModule) : DeviceEventEmitter;
434
435 const dp = collectEvents(em, notify, (ev, res) => {
436 if (ev.action === 'action_get_offline_data' && ev.offline_data) {
437 const arr = Array.isArray(ev.offline_data) ? ev.offline_data : [ev.offline_data];
438 res.push(...(arr as Record<string, unknown>[]));
439 return true;
440 }
441 if (ev.action === 'action_get_status_info' && ev.info_offline_data_num === 0) return true;
442 return false;
443 });
444 bg5s.getOfflineData(mac);
445 const data = await dp;
446
447 const readings: Reading[] = data.map(d => ({
448 mac, deviceType: 'Glucose', glucose: d.data_value as number,
449 date: d.data_measure_time as string,
450 fetchedAt: new Date().toISOString(),
451 }));
452 try { bg5s.disConnect(mac); } catch (_) {}
453 return readings;
454 };
455
456 // When connected, pull data based on device type
222 const connSub = emitter.addListener( 457 const connSub = emitter.addListener(
223 mgr.Event_Device_Connected ?? 'event_device_connected', 458 mgr.Event_Device_Connected ?? 'event_device_connected',
224 async (e: {mac: string; type: string}) => { 459 async (e: {mac: string; type: string}) => {
225 if (e.mac !== syncingMac.current) return; 460 if (e.mac !== syncingMac.current) return;
226 setStatus(`Connected to ${e.mac.slice(-4)}, reading...`); 461 setStatus(`Connected to ${e.mac.slice(-4)}, reading...`);
227 462
228 // Get battery 463 let newReadings: Reading[] = [];
229 const battery = await new Promise<number | undefined>(resolve => { 464 try {
230 const sub = bpEmitter.addListener( 465 switch (e.type) {
231 bp550?.Event_Notify ?? 'event_notify', 466 case 'KN550': case 'KN-550BT': newReadings = await syncBP550(e.mac); break;
232 (ev: Record<string, unknown>) => { 467 case 'PO3': newReadings = await syncPO3(e.mac); break;
233 if (ev.action === 'battery_bp' && ev.battery != null) { 468 case 'PT3SBT': newReadings = await syncPT3SBT(e.mac); break;
234 sub.remove(); resolve(ev.battery as number); 469 case 'HS2S': newReadings = await syncHS2S(e.mac); break;
235 } 470 case 'BG5S': newReadings = await syncBG5S(e.mac); break;
236 }, 471 default:
237 ); 472 // Unknown device — just disconnect
238 bp550.getBattery(e.mac); 473 try { mgr.disconnectDevice(e.mac, e.type); } catch (_) {}
239 setTimeout(() => { sub.remove(); resolve(undefined); }, 5000); 474 }
240 }); 475 } catch (err) {
241 476 console.log('Sync error:', err);
242 // Get offline data
243 const offlineData = await new Promise<Record<string, unknown>[]>(resolve => {
244 const results: Record<string, unknown>[] = [];
245 const sub = bpEmitter.addListener(
246 bp550?.Event_Notify ?? 'event_notify',
247 (ev: Record<string, unknown>) => {
248 if (ev.action === 'historicaldata_bp' && ev.data) {
249 results.push(...(ev.data as Record<string, unknown>[]));
250 }
251 if (ev.action === 'get_historical_over_bp') {
252 sub.remove(); resolve(results);
253 }
254 if (ev.action === 'offlinenum' && ev.offlinenum === 0) {
255 sub.remove(); resolve(results);
256 }
257 },
258 );
259 bp550.getOffLineData(e.mac);
260 setTimeout(() => { sub.remove(); resolve(results); }, 10000);
261 });
262
263 // Save new readings
264 const newReadings = [...readingsRef.current];
265 for (const d of offlineData) {
266 newReadings.push({
267 mac: e.mac, sys: d.sys as number | undefined,
268 dia: d.dia as number | undefined,
269 pulse: d.heartRate as number | undefined,
270 date: d.date as string | undefined,
271 battery, fetchedAt: new Date().toISOString(),
272 });
273 } 477 }
274 if (offlineData.length === 0 && battery != null) { 478
275 newReadings.push({mac: e.mac, battery, fetchedAt: new Date().toISOString()}); 479 if (newReadings.length > 0) {
480 const all = [...readingsRef.current, ...newReadings];
481 readingsRef.current = all;
482 setReadings(all);
483 await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(all));
276 } 484 }
277 readingsRef.current = newReadings;
278 setReadings(newReadings);
279 await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(newReadings));
280 485
281 // Disconnect and resume scanning 486 setStatus(`Synced ${e.mac.slice(-4)} (${newReadings.length} readings)`);
282 try { bp550.disconnect(e.mac); } catch (_) {}
283 setStatus(`Synced ${e.mac.slice(-4)} (${offlineData.length} readings)`);
284 syncingMac.current = null; 487 syncingMac.current = null;
285 setTimeout(startScan, 3000); 488 setTimeout(startScan, 3000);
286 }, 489 },
@@ -336,8 +539,8 @@ function DashboardScreen({onBack}: {onBack: () => void}) {
336 const isDeviceSaved = (mac: string) => savedRef.current.some(d => d.mac === mac); 539 const isDeviceSaved = (mac: string) => savedRef.current.some(d => d.mac === mac);
337 540
338 const recentReadings = readings 541 const recentReadings = readings
339 .filter(r => r.sys != null) 542 .filter(r => r.sys != null || r.spo2 != null || r.temperature != null || r.weight != null || r.glucose != null)
340 .slice(-20) 543 .slice(-30)
341 .reverse(); 544 .reverse();
342 545
343 return ( 546 return (
@@ -387,7 +590,7 @@ function DashboardScreen({onBack}: {onBack: () => void}) {
387 return merged.map(d => ( 590 return merged.map(d => (
388 <View key={d.mac} style={s.savedRow}> 591 <View key={d.mac} style={s.savedRow}>
389 <View style={[s.deviceIcon, d.saved && s.deviceIconSaved, !d.nearby && s.deviceOffline]}> 592 <View style={[s.deviceIcon, d.saved && s.deviceIconSaved, !d.nearby && s.deviceOffline]}>
390 <Text style={s.deviceIconText}>BP</Text> 593 <Text style={s.deviceIconText}>{d.type.substring(0, 3)}</Text>
391 </View> 594 </View>
392 <View style={[s.deviceInfo, !d.nearby && s.deviceOffline]}> 595 <View style={[s.deviceInfo, !d.nearby && s.deviceOffline]}>
393 <Text style={s.deviceType}> 596 <Text style={s.deviceType}>
@@ -423,15 +626,45 @@ function DashboardScreen({onBack}: {onBack: () => void}) {
423 style={s.list} 626 style={s.list}
424 renderItem={({item}) => ( 627 renderItem={({item}) => (
425 <View style={s.readingRow}> 628 <View style={s.readingRow}>
426 <View style={s.readingValues}> 629 {item.sys != null && (
427 <Text style={s.readingSys}>{item.sys ?? '--'}</Text> 630 <View style={s.readingValues}>
428 <Text style={s.readingSlash}>/</Text> 631 <Text style={s.readingSys}>{item.sys}</Text>
429 <Text style={s.readingDia}>{item.dia ?? '--'}</Text> 632 <Text style={s.readingSlash}>/</Text>
430 <Text style={s.readingUnit}>mmHg</Text> 633 <Text style={s.readingDia}>{item.dia ?? '--'}</Text>
431 <Text style={s.readingPulse}>{item.pulse ?? '--'} bpm</Text> 634 <Text style={s.readingUnit}>mmHg</Text>
432 </View> 635 <Text style={s.readingPulse}>{item.pulse ?? '--'} bpm</Text>
636 </View>
637 )}
638 {item.spo2 != null && (
639 <View style={s.readingValues}>
640 <Text style={s.readingSys}>{item.spo2}%</Text>
641 <Text style={s.readingUnit}>SpO2</Text>
642 <Text style={s.readingPulse}>{item.pulseRate ?? '--'} bpm</Text>
643 </View>
644 )}
645 {item.temperature != null && (
646 <View style={s.readingValues}>
647 <Text style={s.readingSys}>{item.temperature}</Text>
648 <Text style={s.readingUnit}>{item.tempUnit ?? 'C'}</Text>
649 </View>
650 )}
651 {item.weight != null && (
652 <View style={s.readingValues}>
653 <Text style={s.readingSys}>{item.weight}</Text>
654 <Text style={s.readingUnit}>kg</Text>
655 {item.bodyFat != null && <Text style={s.readingPulse}>{item.bodyFat}% fat</Text>}
656 {item.bmi != null && <Text style={s.readingPulse}>BMI {item.bmi}</Text>}
657 </View>
658 )}
659 {item.glucose != null && (
660 <View style={s.readingValues}>
661 <Text style={s.readingSys}>{item.glucose}</Text>
662 <Text style={s.readingUnit}>mg/dL</Text>
663 </View>
664 )}
433 <View style={s.readingMeta}> 665 <View style={s.readingMeta}>
434 <Text style={s.readingDate}> 666 <Text style={s.readingDate}>
667 {item.deviceType ? `${item.deviceType} ` : ''}
435 {item.date ?? item.fetchedAt.split('T')[0]} 668 {item.date ?? item.fetchedAt.split('T')[0]}
436 </Text> 669 </Text>
437 <Text style={s.readingMac}>{item.mac}</Text> 670 <Text style={s.readingMac}>{item.mac}</Text>
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 @@
1Device Support Status
2=====================
3
4TESTED & WORKING:
5- KN-550BT (BP550BT) — Blood Pressure Monitor
6 Scan, connect, get battery, get offline data all verified on Android.
7
8NOT YET TESTED:
9- PO3 — Pulse Oximeter
10- PT3SBT — Infrared Thermometer
11- HS2S — Wireless Scale
12- BG5S — Blood Glucose Monitor
13
14These 4 devices have sync logic implemented based on decompiled SDK
15constant values, but have not been tested with physical hardware.
16The event action names and data field keys may need adjustment once
17tested with real devices.