import React, {useState, useEffect, useRef} from 'react'; import { StyleSheet, View, Text, TouchableOpacity, FlatList, ScrollView, Platform, PermissionsAndroid, DeviceEventEmitter, NativeEventEmitter, NativeModules, Alert, } from 'react-native'; import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; // ── 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; } const iHealthDeviceManagerModule = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; const BP550BTModule = NativeModules.BP550BTModule as IBP550BTModule; type Device = { mac: string; type: string; rssi?: number; timestamp: number; }; // ── Helpers ────────────────────────────────────────────────────────── async function requestAndroidPermissions(): Promise { if (Platform.OS !== 'android') return true; const permissions: string[] = []; if (Platform.Version >= 31) { permissions.push( PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, ); } permissions.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); const results = await PermissionsAndroid.requestMultiple(permissions as any); return Object.values(results).every( r => r === 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; } // ── Device Detail Screen ───────────────────────────────────────────── function DeviceScreen({ 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 connSub = emitter.addListener( iHealthDeviceManagerModule.Event_Device_Connected ?? 'event_device_connected', (e: {mac: string; type: string}) => { if (e.mac === device.mac) { setConnected(true); setConnecting(false); addLog(`Connected to ${e.type}`); } }, ); const failSub = emitter.addListener( iHealthDeviceManagerModule.Event_Device_Connect_Failed ?? 'event_device_connect_failed', (e: {mac: string; errorid?: number}) => { if (e.mac === device.mac) { setConnecting(false); addLog(`Connection failed (error: ${e.errorid ?? 'unknown'})`); } }, ); const dcSub = emitter.addListener( iHealthDeviceManagerModule.Event_Device_Disconnect ?? 'event_device_disconnect', (e: {mac: string}) => { if (e.mac === device.mac) { setConnected(false); addLog('Disconnected'); } }, ); // BP550BT event listener const bpEmitter = getBPEmitter(); const notifySub = bpEmitter.addListener( BP550BTModule?.Event_Notify ?? 'event_notify', (e: Record) => { addLog(JSON.stringify(e, null, 2)); }, ); return () => { connSub.remove(); failSub.remove(); dcSub.remove(); notifySub.remove(); }; }, [device.mac]); const connect = () => { setConnecting(true); addLog(`Connecting to ${device.mac}...`); iHealthDeviceManagerModule.connectDevice(device.mac, device.type); }; const disconnect = () => { addLog('Disconnecting...'); if (BP550BTModule) { BP550BTModule.disconnect(device.mac); } else { iHealthDeviceManagerModule.disconnectDevice(device.mac, device.type); } }; const actions: {label: string; fn: () => void; needsConnect: boolean}[] = [ { label: 'Get Battery', needsConnect: true, fn: () => { addLog('Requesting battery...'); BP550BTModule.getBattery(device.mac); }, }, { label: 'Get Function Info', needsConnect: true, fn: () => { addLog('Requesting function info...'); BP550BTModule.getFunctionInfo(device.mac); }, }, { label: 'Get Offline Count', needsConnect: true, fn: () => { addLog('Requesting offline data count...'); BP550BTModule.getOffLineNum(device.mac); }, }, { label: 'Get Offline Data', needsConnect: true, fn: () => { addLog('Requesting offline data...'); BP550BTModule.getOffLineData(device.mac); }, }, { label: 'Get IDPS', needsConnect: false, fn: () => { addLog('Requesting IDPS...'); iHealthDeviceManagerModule.getDevicesIDPS(device.mac, (idps) => { addLog(`IDPS: ${JSON.stringify(idps, null, 2)}`); }); }, }, ]; return ( Back {device.type} {device.mac} {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'} {!connected && !connecting && ( Connect )} {connected && ( Disconnect )} Actions {actions.map(a => ( {a.label} ))} Log String(i)} renderItem={({item}) => {item}} style={styles.logList} /> ); } // ── Scanner Screen ─────────────────────────────────────────────────── function ScannerScreen({onSelectDevice}: {onSelectDevice: (d: Device) => void}) { const [devices, setDevices] = useState([]); const [scanning, setScanning] = useState(false); const [error, setError] = useState(null); const devicesRef = useRef([]); useEffect(() => { if (!iHealthDeviceManagerModule) { setError('iHealth native module not found.'); return; } const emitter = getEmitter(); const scanSub = emitter.addListener( iHealthDeviceManagerModule.Event_Scan_Device ?? 'event_scan_device', (event: {mac: string; type: string; rssi?: number}) => { const {mac = '', type = 'Unknown', rssi} = event; if (type === 'AM3') return; const existing = devicesRef.current.findIndex(d => d.mac === mac); let updated: Device[]; if (existing >= 0) { updated = [...devicesRef.current]; updated[existing] = {mac, type, rssi, timestamp: Date.now()}; } else { updated = [ ...devicesRef.current, {mac, type, rssi, timestamp: Date.now()}, ]; } devicesRef.current = updated; setDevices(updated); }, ); const finishSub = emitter.addListener( iHealthDeviceManagerModule.Event_Scan_Finish ?? 'event_scan_finish', () => setScanning(false), ); return () => { scanSub.remove(); finishSub.remove(); }; }, []); const startScan = async () => { setError(null); const granted = await requestAndroidPermissions(); if (!granted) { setError('Bluetooth permissions denied'); return; } devicesRef.current = []; setDevices([]); setScanning(true); if (Platform.OS === 'android') { try { iHealthDeviceManagerModule.startDiscovery('ALL'); } catch (e) { console.log('Discovery failed:', e); } } else { const iosTypes = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4', 'PO3', 'HS2', 'HS2S', 'HS4S', 'BG5S', 'BG1S', 'PO1', 'ECG3']; let i = 0; const scanNext = () => { if (i < iosTypes.length) { try { iHealthDeviceManagerModule.startDiscovery(iosTypes[i]); } catch (e) { console.log(`Failed: ${iosTypes[i]}`, e); } i++; setTimeout(scanNext, 2000); } }; setTimeout(scanNext, 2000); } }; const stopScan = () => { try { iHealthDeviceManagerModule.stopDiscovery(); } catch (e) { console.log('Stop failed:', e); } setScanning(false); }; return ( iHealth Scanner {scanning ? `Scanning... (${devices.length} found)` : `${devices.length} device(s) found`} {error && {error}} {scanning ? 'Stop Scan' : '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={styles.list} contentContainerStyle={devices.length === 0 && styles.emptyList} ListEmptyComponent={ {scanning ? 'Looking for iHealth devices...' : 'Tap "Start Scan" to find nearby iHealth devices'} } /> ); } // ── App Root ───────────────────────────────────────────────────────── function App() { const [selectedDevice, setSelectedDevice] = useState(null); return ( {selectedDevice ? ( setSelectedDevice(null)} /> ) : ( )} ); } // ── Styles ─────────────────────────────────────────────────────────── const styles = StyleSheet.create({ container: {flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16}, title: {fontSize: 28, fontWeight: '700', color: '#1a1a1a', marginTop: 16}, subtitle: {fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16}, error: { color: '#d32f2f', fontSize: 13, marginBottom: 8, backgroundColor: '#ffebee', padding: 8, borderRadius: 6, }, button: { backgroundColor: '#2196F3', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 16, }, buttonStop: {backgroundColor: '#f44336'}, buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'}, list: {flex: 1}, emptyList: {flex: 1, justifyContent: 'center', alignItems: 'center'}, emptyText: {color: '#999', fontSize: 15, textAlign: 'center'}, 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}, chevron: {fontSize: 18, color: '#ccc', marginLeft: 8}, // Device screen backButton: {marginTop: 12, marginBottom: 4}, backText: {color: '#2196F3', fontSize: 16}, statusBadge: { fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16, fontWeight: '600', }, statusConnected: {color: '#4caf50'}, sectionTitle: { fontSize: 16, fontWeight: '600', color: '#333', marginTop: 8, marginBottom: 8, }, 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;