diff options
| author | hc <haocheng.xie@respiree.com> | 2026-04-13 17:25:45 +0800 |
|---|---|---|
| committer | hc <haocheng.xie@respiree.com> | 2026-04-13 17:25:45 +0800 |
| commit | 9b0815383bba172211127e99eaeeb7feb437ef90 (patch) | |
| tree | a3588969a6db6b0addb801300c43d71172ad39b9 | |
| parent | f22bb303a081d6779592b90a68feee2203d7962a (diff) | |
| -rw-r--r-- | App.tsx | 363 | ||||
| -rw-r--r-- | DEVICE_STATUS.txt | 17 |
2 files changed, 315 insertions, 65 deletions
| @@ -60,8 +60,56 @@ interface IBP550BTModule { | |||
| 60 | getAllConnectedDevices(): void; | 60 | getAllConnectedDevices(): void; |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | interface 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 | |||
| 71 | interface 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 | |||
| 80 | interface 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 | |||
| 89 | interface 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 | |||
| 63 | const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; | 97 | const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; |
| 64 | const bp550 = NativeModules.BP550BTModule as IBP550BTModule; | 98 | const bp550 = NativeModules.BP550BTModule as IBP550BTModule; |
| 99 | const po3 = NativeModules.PO3Module as IPO3Module; | ||
| 100 | const pt3sbt = NativeModules.PT3SBTModule as IPT3SBTModule; | ||
| 101 | const hs2s = NativeModules.HS2SModule as IHS2SModule; | ||
| 102 | const bg5s = NativeModules.BG5SModule as IBG5SModule; | ||
| 103 | |||
| 104 | // Device type → module mapping | ||
| 105 | const 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 | ||
| 72 | type Reading = { | 120 | type 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 @@ | |||
| 1 | Device Support Status | ||
| 2 | ===================== | ||
| 3 | |||
| 4 | TESTED & WORKING: | ||
| 5 | - KN-550BT (BP550BT) — Blood Pressure Monitor | ||
| 6 | Scan, connect, get battery, get offline data all verified on Android. | ||
| 7 | |||
| 8 | NOT YET TESTED: | ||
| 9 | - PO3 — Pulse Oximeter | ||
| 10 | - PT3SBT — Infrared Thermometer | ||
| 11 | - HS2S — Wireless Scale | ||
| 12 | - BG5S — Blood Glucose Monitor | ||
| 13 | |||
| 14 | These 4 devices have sync logic implemented based on decompiled SDK | ||
| 15 | constant values, but have not been tested with physical hardware. | ||
| 16 | The event action names and data field keys may need adjustment once | ||
| 17 | tested with real devices. | ||
