summaryrefslogtreecommitdiff
path: root/App.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'App.tsx')
-rw-r--r--App.tsx363
1 files changed, 298 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>