import React, {useState, useEffect, useRef, useCallback} from 'react'; import { StyleSheet, View, Text, TouchableOpacity, FlatList, ScrollView, Platform, PermissionsAndroid, DeviceEventEmitter, NativeEventEmitter, NativeModules, ActivityIndicator, } from 'react-native'; import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; import AsyncStorage from '@react-native-async-storage/async-storage'; // ── Native module types ────────────────────────────────────────────── const DEVICE_TYPE_KEYS = [ 'AM3S', 'AM4', 'PO3', 'BP5', 'BP5S', 'BP3L', 'BP7', 'BP7S', 'KN550', 'HS2', 'HS2S', 'HS4S', 'BG1', 'BG1S', 'BG5', 'BG5S', 'ECG3', 'BTM', 'TS28B', 'NT13B', ] as const; type DeviceTypeName = (typeof DEVICE_TYPE_KEYS)[number]; type DiscoveryConstant = string | number; interface IHealthDeviceManager { AM3S: DiscoveryConstant; AM4: DiscoveryConstant; PO3: DiscoveryConstant; BP5: DiscoveryConstant; BP5S: DiscoveryConstant; BP3L: DiscoveryConstant; BP7: DiscoveryConstant; BP7S: DiscoveryConstant; KN550: DiscoveryConstant; HS2: DiscoveryConstant; HS2S: DiscoveryConstant; HS4S: DiscoveryConstant; BG1: DiscoveryConstant; BG1S: DiscoveryConstant; BG5: DiscoveryConstant; BG5S: DiscoveryConstant; ECG3: DiscoveryConstant; BTM: DiscoveryConstant; TS28B: DiscoveryConstant; NT13B: DiscoveryConstant; Event_Scan_Device: string; Event_Scan_Finish: string; Event_Device_Connected: string; Event_Device_Connect_Failed: string; Event_Device_Disconnect: string; Event_Authenticate_Result: string; startDiscovery(type: DeviceTypeName | string): void; stopDiscovery(): void; connectDevice(mac: string, type: string): void; disconnectDevice(mac: string, type: string): void; sdkAuthWithLicense(license: string): void; authenConfigureInfo(userName: string, clientID: string, clientSecret: string): void; getDevicesIDPS(mac: string, callback: (idps: Record) => void): void; } interface IBP550BTModule { Event_Notify: string; getBattery(mac: string): void; getOffLineNum(mac: string): void; getOffLineData(mac: string): void; getFunctionInfo(mac: string): void; disconnect(mac: string): void; 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 ──────────────────────────────────────────────────────────── type Device = {mac: string; type: string; rssi?: number; timestamp: number}; type SavedDevice = {mac: string; type: string; addedAt: string}; type Reading = { mac: string; deviceType?: string; // BP sys?: number; dia?: number; pulse?: number; // 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; }; type Screen = 'home' | 'dashboard' | 'debug' | 'debug-device'; const STORAGE_KEY_DEVICES = '@ihealth/saved_devices'; const STORAGE_KEY_READINGS = '@ihealth/readings'; // ── Helpers ────────────────────────────────────────────────────────── async function requestAndroidPermissions(): Promise { if (Platform.OS !== 'android') return true; const perms: string[] = []; if (Platform.Version >= 31) { perms.push( PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, ); } perms.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); const r = await PermissionsAndroid.requestMultiple(perms as any); return Object.values(r).every(v => v === PermissionsAndroid.RESULTS.GRANTED); } function getEmitter() { return Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.iHealthDeviceManagerModule) : DeviceEventEmitter; } function getBPEmitter() { return Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.BP550BTModule) : DeviceEventEmitter; } // ── Home Screen ────────────────────────────────────────────────────── function HomeScreen({onNav}: {onNav: (s: Screen) => void}) { return ( iHealth Blood Pressure Monitor onNav('dashboard')}> Dashboard View your devices and readings onNav('debug')}> Debug Scanner Scan all devices, raw logs ); } // ── Dashboard Screen ───────────────────────────────────────────────── function DashboardScreen({onBack}: {onBack: () => void}) { const [savedDevices, setSavedDevices] = useState([]); const [readings, setReadings] = useState([]); const [foundDevices, setFoundDevices] = useState([]); const [status, setStatus] = useState('Starting...'); const foundRef = useRef([]); const savedRef = useRef([]); const readingsRef = useRef([]); const syncingMac = useRef(null); // Load saved data on mount useEffect(() => { (async () => { const [devJson, readJson] = await Promise.all([ AsyncStorage.getItem(STORAGE_KEY_DEVICES), AsyncStorage.getItem(STORAGE_KEY_READINGS), ]); const devs: SavedDevice[] = devJson ? JSON.parse(devJson) : []; const reads: Reading[] = readJson ? JSON.parse(readJson) : []; setSavedDevices(devs); savedRef.current = devs; setReadings(reads); readingsRef.current = reads; await requestAndroidPermissions(); })(); }, []); // Always-on scan loop: restart scan every time it finishes useEffect(() => { const emitter = getEmitter(); const bpEmitter = getBPEmitter(); const startScan = () => { try { mgr.startDiscovery('ALL'); } catch (_) {} setStatus('Scanning...'); }; // When a device is found const scanSub = emitter.addListener( mgr.Event_Scan_Device ?? 'event_scan_device', (e: {mac: string; type: string; rssi?: number}) => { if (e.type === 'AM3') return; // Track all found devices const exists = foundRef.current.find(d => d.mac === e.mac); if (!exists) { const updated = [...foundRef.current, {mac: e.mac, type: e.type, rssi: e.rssi, timestamp: Date.now()}]; foundRef.current = updated; setFoundDevices(updated); } // Auto-connect if it's a saved device we haven't synced yet const isSaved = savedRef.current.some(d => d.mac === e.mac); if (isSaved && !syncingMac.current) { syncingMac.current = e.mac; setStatus(`Found ${e.mac.slice(-4)}, connecting...`); try { mgr.stopDiscovery(); } catch (_) {} mgr.connectDevice(e.mac, e.type); } }, ); // When scan finishes, restart after a short delay const finSub = emitter.addListener( mgr.Event_Scan_Finish ?? 'event_scan_finish', () => { if (!syncingMac.current) { setTimeout(startScan, 3000); } }, ); // 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...`); 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 (newReadings.length > 0) { const all = [...readingsRef.current, ...newReadings]; readingsRef.current = all; setReadings(all); await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(all)); } setStatus(`Synced ${e.mac.slice(-4)} (${newReadings.length} readings)`); syncingMac.current = null; setTimeout(startScan, 3000); }, ); const failSub = emitter.addListener( mgr.Event_Device_Connect_Failed ?? 'event_device_connect_failed', () => { setStatus('Connect failed, resuming scan...'); syncingMac.current = null; setTimeout(startScan, 3000); }, ); const dcSub = emitter.addListener( mgr.Event_Device_Disconnect ?? 'event_device_disconnect', () => { if (syncingMac.current) { syncingMac.current = null; setTimeout(startScan, 3000); } }, ); // Start first scan startScan(); return () => { scanSub.remove(); finSub.remove(); connSub.remove(); failSub.remove(); dcSub.remove(); try { mgr.stopDiscovery(); } catch (_) {} }; }, []); const addDevice = async (dev: Device) => { const newDev: SavedDevice = {mac: dev.mac, type: dev.type, addedAt: new Date().toISOString()}; const updated = [...savedRef.current, newDev]; savedRef.current = updated; setSavedDevices(updated); await AsyncStorage.setItem(STORAGE_KEY_DEVICES, JSON.stringify(updated)); const f = foundRef.current.filter(d => d.mac !== dev.mac); foundRef.current = f; setFoundDevices(f); }; const removeDevice = async (mac: string) => { const updated = savedRef.current.filter(d => d.mac !== mac); savedRef.current = updated; setSavedDevices(updated); await AsyncStorage.setItem(STORAGE_KEY_DEVICES, JSON.stringify(updated)); }; const isDeviceSaved = (mac: string) => savedRef.current.some(d => d.mac === mac); const recentReadings = readings .filter(r => r.sys != null || r.spo2 != null || r.temperature != null || r.weight != null || r.glucose != null) .slice(-30) .reverse(); return ( Home Dashboard 🦄✨ {/* Show status only when actively syncing a device */} {status.includes('connecting') || status.includes('reading') || status.includes('Synced') ? ( {status} ) : null} {/* All devices — unified list, saved ones marked */} Devices ({foundDevices.length} nearby, {savedDevices.length} saved) {/* Merge: all found devices + saved devices not currently found */} {(() => { const allMacs = new Set([ ...foundDevices.map(d => d.mac), ...savedDevices.map(d => d.mac), ]); const merged = Array.from(allMacs).map(mac => { const found = foundDevices.find(d => d.mac === mac); const saved = savedDevices.find(d => d.mac === mac); return { mac, type: found?.type ?? saved?.type ?? 'KN550', rssi: found?.rssi, nearby: !!found, saved: !!saved, }; }); // Sort: saved first, then nearby, then rest merged.sort((a, b) => { if (a.saved !== b.saved) return a.saved ? -1 : 1; if (a.nearby !== b.nearby) return a.nearby ? -1 : 1; return 0; }); return merged.map(d => ( {d.type.substring(0, 3)} {d.type} {d.saved ? ' ' : ''} {d.saved && SAVED} {d.mac} {!d.nearby ? ' (offline)' : d.rssi != null ? ` ${d.rssi} dBm` : ''} {d.saved ? ( removeDevice(d.mac)}> Remove ) : ( addDevice({mac: d.mac, type: d.type, timestamp: Date.now()})}> Save )} )); })()} {/* Readings */} Readings ({recentReadings.length}) String(i)} style={s.list} renderItem={({item}) => ( {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} )} ListEmptyComponent={ {savedDevices.length === 0 ? 'Add a device to start tracking' : 'No readings yet. Sync to pull data.'} } /> ); } // ── Debug Scanner Screen ───────────────────────────────────────────── function DebugScannerScreen({ onBack, onSelectDevice, }: { onBack: () => void; onSelectDevice: (d: Device) => void; }) { const [devices, setDevices] = useState([]); const [scanning, setScanning] = useState(false); const [error, setError] = useState(null); const devicesRef = useRef([]); useEffect(() => { if (!mgr) { setError('iHealth native module not found.'); return; } const emitter = getEmitter(); const sub = emitter.addListener( mgr.Event_Scan_Device ?? 'event_scan_device', (e: {mac: string; type: string; rssi?: number}) => { if (e.type === 'AM3') return; const {mac = '', type = 'Unknown', rssi} = e; const idx = devicesRef.current.findIndex(d => d.mac === mac); let updated: Device[]; if (idx >= 0) { updated = [...devicesRef.current]; updated[idx] = {mac, type, rssi, timestamp: Date.now()}; } else { updated = [...devicesRef.current, {mac, type, rssi, timestamp: Date.now()}]; } devicesRef.current = updated; setDevices(updated); }, ); const finSub = emitter.addListener( mgr.Event_Scan_Finish ?? 'event_scan_finish', () => setScanning(false), ); return () => { sub.remove(); finSub.remove(); }; }, []); const startScan = async () => { setError(null); const ok = await requestAndroidPermissions(); if (!ok) { setError('Permissions denied'); return; } devicesRef.current = []; setDevices([]); setScanning(true); if (Platform.OS === 'android') { try { mgr.startDiscovery('ALL'); } catch (e) { console.log(e); } } else { const types = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4', 'PO3', 'HS2', 'HS2S', 'HS4S', 'BG5S', 'BG1S', 'PO1', 'ECG3']; let i = 0; const next = () => { if (i < types.length) { try { mgr.startDiscovery(types[i]); } catch (_) {} i++; setTimeout(next, 2000); } }; setTimeout(next, 2000); } }; const stopScan = () => { try { mgr.stopDiscovery(); } catch (_) {} setScanning(false); }; return ( Home Debug Scanner {scanning ? `Scanning... (${devices.length})` : `${devices.length} device(s)`} {error && {error}} {scanning ? 'Stop' : 'Start Scan'} item.mac} renderItem={({item}) => ( { stopScan(); onSelectDevice(item); }}> {item.type.substring(0, 3)} {item.type} {item.mac} {item.rssi != null && {item.rssi} dBm} {'>'} )} style={s.list} contentContainerStyle={devices.length === 0 && s.emptyList} ListEmptyComponent={ {scanning ? 'Scanning...' : 'Tap Start Scan'} } /> ); } // ── Debug Device Screen ────────────────────────────────────────────── function DebugDeviceScreen({device, onBack}: {device: Device; onBack: () => void}) { const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [log, setLog] = useState([]); const addLog = (msg: string) => setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]); useEffect(() => { const emitter = getEmitter(); const bpEmitter = getBPEmitter(); const connSub = emitter.addListener( mgr.Event_Device_Connected ?? 'event_device_connected', (e: {mac: string}) => { if (e.mac === device.mac) { setConnected(true); setConnecting(false); addLog('Connected'); } }, ); const failSub = emitter.addListener( mgr.Event_Device_Connect_Failed ?? 'event_device_connect_failed', (e: {mac: string; errorid?: number}) => { if (e.mac === device.mac) { setConnecting(false); addLog(`Connect failed: ${e.errorid}`); } }, ); const dcSub = emitter.addListener( mgr.Event_Device_Disconnect ?? 'event_device_disconnect', (e: {mac: string}) => { if (e.mac === device.mac) { setConnected(false); addLog('Disconnected'); } }, ); const notSub = bpEmitter.addListener( bp550?.Event_Notify ?? 'event_notify', (e: Record) => addLog(JSON.stringify(e, null, 2)), ); return () => { connSub.remove(); failSub.remove(); dcSub.remove(); notSub.remove(); }; }, [device.mac]); const actions = [ {label: 'Get Battery', fn: () => { addLog('Battery...'); bp550.getBattery(device.mac); }}, {label: 'Function Info', fn: () => { addLog('FuncInfo...'); bp550.getFunctionInfo(device.mac); }}, {label: 'Offline Count', fn: () => { addLog('OfflineNum...'); bp550.getOffLineNum(device.mac); }}, {label: 'Offline Data', fn: () => { addLog('OfflineData...'); bp550.getOffLineData(device.mac); }}, {label: 'IDPS', fn: () => { addLog('IDPS...'); mgr.getDevicesIDPS(device.mac, idps => addLog(JSON.stringify(idps, null, 2))); }}, ]; return ( Back {device.type} {device.mac} {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'} {!connected && !connecting && ( { setConnecting(true); addLog('Connecting...'); mgr.connectDevice(device.mac, device.type); }}> Connect )} {connected && ( { addLog('Disconnecting...'); bp550.disconnect(device.mac); }}> Disconnect )} {actions.map(a => ( {a.label} ))} Log String(i)} renderItem={({item}) => {item}} style={s.logList} /> ); } // ── App Root ───────────────────────────────────────────────────────── function App() { const [screen, setScreen] = useState('home'); const [debugDevice, setDebugDevice] = useState(null); return ( {screen === 'home' && } {screen === 'dashboard' && ( setScreen('home')} /> )} {screen === 'debug' && ( setScreen('home')} onSelectDevice={d => { setDebugDevice(d); setScreen('debug-device'); }} /> )} {screen === 'debug-device' && debugDevice && ( setScreen('debug')} /> )} ); } // ── Styles ─────────────────────────────────────────────────────────── const s = StyleSheet.create({ container: {flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16}, // Home homeCenter: {flex: 1, justifyContent: 'center'}, homeTitle: {fontSize: 36, fontWeight: '800', color: '#1a1a1a', textAlign: 'center'}, homeSubtitle: {fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 40}, homeButton: { backgroundColor: '#2196F3', padding: 20, borderRadius: 14, marginBottom: 12, alignItems: 'center', }, homeButtonSecondary: {backgroundColor: '#fff', borderWidth: 1, borderColor: '#ddd'}, homeButtonText: {color: '#fff', fontSize: 18, fontWeight: '700'}, homeButtonTextSecondary: {color: '#333'}, homeButtonSub: {color: 'rgba(255,255,255,0.8)', fontSize: 13, marginTop: 4}, // Common title: {fontSize: 28, fontWeight: '700', color: '#1a1a1a', marginTop: 16}, subtitle: {fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16}, sectionTitle: {fontSize: 16, fontWeight: '600', color: '#333', marginTop: 8, marginBottom: 8}, error: {color: '#d32f2f', fontSize: 13, marginBottom: 8, backgroundColor: '#ffebee', padding: 8, borderRadius: 6}, button: {backgroundColor: '#2196F3', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 12}, buttonStop: {backgroundColor: '#f44336'}, buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'}, buttonOutline: {backgroundColor: 'transparent', borderWidth: 1.5, borderColor: '#2196F3'}, buttonTextOutline: {color: '#2196F3'}, list: {flex: 1}, emptyList: {flex: 1, justifyContent: 'center', alignItems: 'center'}, emptyText: {color: '#999', fontSize: 15, textAlign: 'center'}, backButton: {marginTop: 12, marginBottom: 4}, backText: {color: '#2196F3', fontSize: 16}, chevron: {fontSize: 18, color: '#ccc', marginLeft: 8}, // Device rows deviceRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', padding: 14, borderRadius: 10, marginBottom: 8, shadowColor: '#000', shadowOffset: {width: 0, height: 1}, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1, }, deviceIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#e3f2fd', justifyContent: 'center', alignItems: 'center', marginRight: 12, }, deviceIconText: {fontSize: 12, fontWeight: '700', color: '#1565c0'}, deviceInfo: {flex: 1}, deviceType: {fontSize: 16, fontWeight: '600', color: '#1a1a1a'}, deviceMac: {fontSize: 12, color: '#888', marginTop: 2, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'}, deviceRssi: {fontSize: 12, color: '#999', marginLeft: 8}, // Dashboard savedRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', padding: 14, borderRadius: 10, marginBottom: 8, elevation: 1, }, removeText: {color: '#f44336', fontSize: 13, fontWeight: '600'}, deviceOffline: {opacity: 0.5}, deviceIconSaved: {backgroundColor: '#c8e6c9'}, savedBadge: {fontSize: 10, color: '#4caf50', fontWeight: '700'}, foundRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#e8f5e9', padding: 14, borderRadius: 10, marginBottom: 8, }, addButton: {backgroundColor: '#4caf50', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 8}, addButtonText: {color: '#fff', fontSize: 14, fontWeight: '600'}, syncBar: {flexDirection: 'row', alignItems: 'center', padding: 10, backgroundColor: '#e3f2fd', borderRadius: 8, marginBottom: 8}, syncText: {color: '#1565c0', fontSize: 13, marginLeft: 8}, syncDone: {color: '#4caf50', fontSize: 13, marginBottom: 8, fontWeight: '600'}, scanningRow: {flexDirection: 'row', alignItems: 'center', padding: 12}, scanningText: {color: '#666', fontSize: 13, marginLeft: 8}, // Readings readingRow: { backgroundColor: '#fff', padding: 14, borderRadius: 10, marginBottom: 8, elevation: 1, }, readingValues: {flexDirection: 'row', alignItems: 'baseline'}, readingSys: {fontSize: 28, fontWeight: '700', color: '#1a1a1a'}, readingSlash: {fontSize: 20, color: '#999', marginHorizontal: 4}, readingDia: {fontSize: 28, fontWeight: '700', color: '#1a1a1a'}, readingUnit: {fontSize: 12, color: '#999', marginLeft: 6}, readingPulse: {fontSize: 14, color: '#666', marginLeft: 16}, readingMeta: {flexDirection: 'row', justifyContent: 'space-between', marginTop: 4}, readingDate: {fontSize: 12, color: '#999'}, readingMac: {fontSize: 10, color: '#bbb', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'}, // Debug device statusBadge: {fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16, fontWeight: '600'}, statusConnected: {color: '#4caf50'}, actionsRow: {flexGrow: 0, marginBottom: 12}, actionButton: {backgroundColor: '#e3f2fd', paddingVertical: 10, paddingHorizontal: 16, borderRadius: 8, marginRight: 8}, actionDisabled: {backgroundColor: '#eee'}, actionText: {color: '#1565c0', fontSize: 13, fontWeight: '600'}, actionTextDisabled: {color: '#bbb'}, logList: {flex: 1, marginTop: 4}, logLine: {fontSize: 11, color: '#555', paddingVertical: 3, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'}, }); export default App;