summaryrefslogtreecommitdiff
path: root/App.tsx
diff options
context:
space:
mode:
authorhc <haocheng.xie@respiree.com>2026-04-13 15:17:52 +0800
committerhc <haocheng.xie@respiree.com>2026-04-13 15:17:52 +0800
commitd6d9a09d505d11148599a95a5be3e1351edbe0ac (patch)
treea5f5891983d1ff207e99f683a5e151519cef4980 /App.tsx
parente4fb9966e762852bf17f21c8406501d42fae0b61 (diff)
Local iHealth SDK, device detail screen, iOS event fixes
Diffstat (limited to 'App.tsx')
-rw-r--r--App.tsx561
1 files changed, 364 insertions, 197 deletions
diff --git a/App.tsx b/App.tsx
index 09e446f..9dca202 100644
--- a/App.tsx
+++ b/App.tsx
@@ -5,14 +5,18 @@ import {
5 Text, 5 Text,
6 TouchableOpacity, 6 TouchableOpacity,
7 FlatList, 7 FlatList,
8 ScrollView,
8 Platform, 9 Platform,
9 PermissionsAndroid, 10 PermissionsAndroid,
10 DeviceEventEmitter, 11 DeviceEventEmitter,
12 NativeEventEmitter,
11 NativeModules, 13 NativeModules,
14 Alert,
12} from 'react-native'; 15} from 'react-native';
13import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; 16import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';
14 17
15// Device type string names accepted by startDiscovery on both platforms 18// ── Native module types ──────────────────────────────────────────────
19
16const DEVICE_TYPE_KEYS = [ 20const DEVICE_TYPE_KEYS = [
17 'AM3S', 'AM4', 'PO3', 'BP5', 'BP5S', 'BP3L', 'BP7', 'BP7S', 21 'AM3S', 'AM4', 'PO3', 'BP5', 'BP5S', 'BP3L', 'BP7', 'BP7S',
18 'KN550', 'HS2', 'HS2S', 'HS4S', 'BG1', 'BG1S', 'BG5', 'BG5S', 22 'KN550', 'HS2', 'HS2S', 'HS4S', 'BG1', 'BG1S', 'BG5', 'BG5S',
@@ -20,40 +24,22 @@ const DEVICE_TYPE_KEYS = [
20] as const; 24] as const;
21 25
22type DeviceTypeName = (typeof DEVICE_TYPE_KEYS)[number]; 26type DeviceTypeName = (typeof DEVICE_TYPE_KEYS)[number];
23
24// The native module exports string constants on iOS, number constants on Android.
25type DiscoveryConstant = string | number; 27type DiscoveryConstant = string | number;
26 28
27interface IHealthDeviceManager { 29interface IHealthDeviceManager {
28 // Device type constants (string on iOS, number on Android) 30 AM3S: DiscoveryConstant; AM4: DiscoveryConstant; PO3: DiscoveryConstant;
29 AM3S: DiscoveryConstant; 31 BP5: DiscoveryConstant; BP5S: DiscoveryConstant; BP3L: DiscoveryConstant;
30 AM4: DiscoveryConstant; 32 BP7: DiscoveryConstant; BP7S: DiscoveryConstant; KN550: DiscoveryConstant;
31 PO3: DiscoveryConstant; 33 HS2: DiscoveryConstant; HS2S: DiscoveryConstant; HS4S: DiscoveryConstant;
32 BP5: DiscoveryConstant; 34 BG1: DiscoveryConstant; BG1S: DiscoveryConstant; BG5: DiscoveryConstant;
33 BP5S: DiscoveryConstant; 35 BG5S: DiscoveryConstant; ECG3: DiscoveryConstant; BTM: DiscoveryConstant;
34 BP3L: DiscoveryConstant; 36 TS28B: DiscoveryConstant; NT13B: DiscoveryConstant;
35 BP7: DiscoveryConstant;
36 BP7S: DiscoveryConstant;
37 KN550: DiscoveryConstant;
38 HS2: DiscoveryConstant;
39 HS2S: DiscoveryConstant;
40 HS4S: DiscoveryConstant;
41 BG1: DiscoveryConstant;
42 BG1S: DiscoveryConstant;
43 BG5: DiscoveryConstant;
44 BG5S: DiscoveryConstant;
45 ECG3: DiscoveryConstant;
46 BTM: DiscoveryConstant;
47 TS28B: DiscoveryConstant;
48 NT13B: DiscoveryConstant;
49 // Event name constants
50 Event_Scan_Device: string; 37 Event_Scan_Device: string;
51 Event_Scan_Finish: string; 38 Event_Scan_Finish: string;
52 Event_Device_Connected: string; 39 Event_Device_Connected: string;
53 Event_Device_Connect_Failed: string; 40 Event_Device_Connect_Failed: string;
54 Event_Device_Disconnect: string; 41 Event_Device_Disconnect: string;
55 Event_Authenticate_Result: string; 42 Event_Authenticate_Result: string;
56 // Methods — startDiscovery takes a device type name string per official docs
57 startDiscovery(type: DeviceTypeName | string): void; 43 startDiscovery(type: DeviceTypeName | string): void;
58 stopDiscovery(): void; 44 stopDiscovery(): void;
59 connectDevice(mac: string, type: string): void; 45 connectDevice(mac: string, type: string): void;
@@ -63,8 +49,20 @@ interface IHealthDeviceManager {
63 getDevicesIDPS(mac: string, callback: (idps: Record<string, string>) => void): void; 49 getDevicesIDPS(mac: string, callback: (idps: Record<string, string>) => void): void;
64} 50}
65 51
52interface IBP550BTModule {
53 Event_Notify: string;
54 getBattery(mac: string): void;
55 getOffLineNum(mac: string): void;
56 getOffLineData(mac: string): void;
57 getFunctionInfo(mac: string): void;
58 disconnect(mac: string): void;
59 getAllConnectedDevices(): void;
60}
61
66const iHealthDeviceManagerModule = 62const iHealthDeviceManagerModule =
67 NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; 63 NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager;
64const BP550BTModule =
65 NativeModules.BP550BTModule as IBP550BTModule;
68 66
69type Device = { 67type Device = {
70 mac: string; 68 mac: string;
@@ -73,45 +71,227 @@ type Device = {
73 timestamp: number; 71 timestamp: number;
74}; 72};
75 73
74// ── Helpers ──────────────────────────────────────────────────────────
75
76async function requestAndroidPermissions(): Promise<boolean> { 76async function requestAndroidPermissions(): Promise<boolean> {
77 if (Platform.OS !== 'android') return true; 77 if (Platform.OS !== 'android') return true;
78
79 const apiLevel = Platform.Version;
80 const permissions: string[] = []; 78 const permissions: string[] = [];
81 79 if (Platform.Version >= 31) {
82 if (apiLevel >= 31) {
83 permissions.push( 80 permissions.push(
84 PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, 81 PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
85 PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, 82 PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
86 ); 83 );
87 } 84 }
88 permissions.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); 85 permissions.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
89
90 const results = await PermissionsAndroid.requestMultiple(permissions as any); 86 const results = await PermissionsAndroid.requestMultiple(permissions as any);
91 return Object.values(results).every( 87 return Object.values(results).every(
92 r => r === PermissionsAndroid.RESULTS.GRANTED, 88 r => r === PermissionsAndroid.RESULTS.GRANTED,
93 ); 89 );
94} 90}
95 91
96function DeviceIcon({type}: {type: string}) { 92function getEmitter() {
97 const icons: Record<string, string> = { 93 return Platform.OS === 'ios'
98 BP: 'BP', 94 ? new NativeEventEmitter(NativeModules.iHealthDeviceManagerModule)
99 AM: 'AM', 95 : DeviceEventEmitter;
100 PO: 'PO', 96}
101 BG: 'BG', 97
102 HS: 'HS', 98function getBPEmitter() {
103 ECG: 'ECG', 99 return Platform.OS === 'ios'
104 BTM: 'BTM', 100 ? new NativeEventEmitter(NativeModules.BP550BTModule)
101 : DeviceEventEmitter;
102}
103
104// ── Device Detail Screen ─────────────────────────────────────────────
105
106function DeviceScreen({
107 device,
108 onBack,
109}: {
110 device: Device;
111 onBack: () => void;
112}) {
113 const [connected, setConnected] = useState(false);
114 const [connecting, setConnecting] = useState(false);
115 const [log, setLog] = useState<string[]>([]);
116
117 const addLog = (msg: string) =>
118 setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]);
119
120 useEffect(() => {
121 const emitter = getEmitter();
122
123 const connSub = emitter.addListener(
124 iHealthDeviceManagerModule.Event_Device_Connected ?? 'event_device_connected',
125 (e: {mac: string; type: string}) => {
126 if (e.mac === device.mac) {
127 setConnected(true);
128 setConnecting(false);
129 addLog(`Connected to ${e.type}`);
130 }
131 },
132 );
133
134 const failSub = emitter.addListener(
135 iHealthDeviceManagerModule.Event_Device_Connect_Failed ?? 'event_device_connect_failed',
136 (e: {mac: string; errorid?: number}) => {
137 if (e.mac === device.mac) {
138 setConnecting(false);
139 addLog(`Connection failed (error: ${e.errorid ?? 'unknown'})`);
140 }
141 },
142 );
143
144 const dcSub = emitter.addListener(
145 iHealthDeviceManagerModule.Event_Device_Disconnect ?? 'event_device_disconnect',
146 (e: {mac: string}) => {
147 if (e.mac === device.mac) {
148 setConnected(false);
149 addLog('Disconnected');
150 }
151 },
152 );
153
154 // BP550BT event listener
155 const bpEmitter = getBPEmitter();
156 const notifySub = bpEmitter.addListener(
157 BP550BTModule?.Event_Notify ?? 'event_notify',
158 (e: Record<string, unknown>) => {
159 addLog(JSON.stringify(e, null, 2));
160 },
161 );
162
163 return () => {
164 connSub.remove();
165 failSub.remove();
166 dcSub.remove();
167 notifySub.remove();
168 };
169 }, [device.mac]);
170
171 const connect = () => {
172 setConnecting(true);
173 addLog(`Connecting to ${device.mac}...`);
174 iHealthDeviceManagerModule.connectDevice(device.mac, device.type);
175 };
176
177 const disconnect = () => {
178 addLog('Disconnecting...');
179 if (BP550BTModule) {
180 BP550BTModule.disconnect(device.mac);
181 } else {
182 iHealthDeviceManagerModule.disconnectDevice(device.mac, device.type);
183 }
105 }; 184 };
106 const prefix = Object.keys(icons).find(k => type.startsWith(k)) || '?'; 185
186 const actions: {label: string; fn: () => void; needsConnect: boolean}[] = [
187 {
188 label: 'Get Battery',
189 needsConnect: true,
190 fn: () => {
191 addLog('Requesting battery...');
192 BP550BTModule.getBattery(device.mac);
193 },
194 },
195 {
196 label: 'Get Function Info',
197 needsConnect: true,
198 fn: () => {
199 addLog('Requesting function info...');
200 BP550BTModule.getFunctionInfo(device.mac);
201 },
202 },
203 {
204 label: 'Get Offline Count',
205 needsConnect: true,
206 fn: () => {
207 addLog('Requesting offline data count...');
208 BP550BTModule.getOffLineNum(device.mac);
209 },
210 },
211 {
212 label: 'Get Offline Data',
213 needsConnect: true,
214 fn: () => {
215 addLog('Requesting offline data...');
216 BP550BTModule.getOffLineData(device.mac);
217 },
218 },
219 {
220 label: 'Get IDPS',
221 needsConnect: false,
222 fn: () => {
223 addLog('Requesting IDPS...');
224 iHealthDeviceManagerModule.getDevicesIDPS(device.mac, (idps) => {
225 addLog(`IDPS: ${JSON.stringify(idps, null, 2)}`);
226 });
227 },
228 },
229 ];
230
107 return ( 231 return (
108 <View style={styles.deviceIcon}> 232 <SafeAreaView style={styles.container}>
109 <Text style={styles.deviceIconText}>{icons[prefix] || '?'}</Text> 233 <TouchableOpacity style={styles.backButton} onPress={onBack}>
110 </View> 234 <Text style={styles.backText}>Back</Text>
235 </TouchableOpacity>
236
237 <Text style={styles.title}>{device.type}</Text>
238 <Text style={styles.deviceMac}>{device.mac}</Text>
239 <Text style={[styles.statusBadge, connected && styles.statusConnected]}>
240 {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'}
241 </Text>
242
243 {!connected && !connecting && (
244 <TouchableOpacity style={styles.button} onPress={connect}>
245 <Text style={styles.buttonText}>Connect</Text>
246 </TouchableOpacity>
247 )}
248 {connected && (
249 <TouchableOpacity
250 style={[styles.button, styles.buttonStop]}
251 onPress={disconnect}>
252 <Text style={styles.buttonText}>Disconnect</Text>
253 </TouchableOpacity>
254 )}
255
256 <Text style={styles.sectionTitle}>Actions</Text>
257 <ScrollView
258 horizontal
259 showsHorizontalScrollIndicator={false}
260 style={styles.actionsRow}>
261 {actions.map(a => (
262 <TouchableOpacity
263 key={a.label}
264 style={[
265 styles.actionButton,
266 a.needsConnect && !connected && styles.actionDisabled,
267 ]}
268 disabled={a.needsConnect && !connected}
269 onPress={a.fn}>
270 <Text
271 style={[
272 styles.actionText,
273 a.needsConnect && !connected && styles.actionTextDisabled,
274 ]}>
275 {a.label}
276 </Text>
277 </TouchableOpacity>
278 ))}
279 </ScrollView>
280
281 <Text style={styles.sectionTitle}>Log</Text>
282 <FlatList
283 data={log}
284 keyExtractor={(_, i) => String(i)}
285 renderItem={({item}) => <Text style={styles.logLine}>{item}</Text>}
286 style={styles.logList}
287 />
288 </SafeAreaView>
111 ); 289 );
112} 290}
113 291
114function App() { 292// ── Scanner Screen ───────────────────────────────────────────────────
293
294function ScannerScreen({onSelectDevice}: {onSelectDevice: (d: Device) => void}) {
115 const [devices, setDevices] = useState<Device[]>([]); 295 const [devices, setDevices] = useState<Device[]>([]);
116 const [scanning, setScanning] = useState(false); 296 const [scanning, setScanning] = useState(false);
117 const [error, setError] = useState<string | null>(null); 297 const [error, setError] = useState<string | null>(null);
@@ -119,18 +299,15 @@ function App() {
119 299
120 useEffect(() => { 300 useEffect(() => {
121 if (!iHealthDeviceManagerModule) { 301 if (!iHealthDeviceManagerModule) {
122 setError('iHealth native module not found. Check linking.'); 302 setError('iHealth native module not found.');
123 return; 303 return;
124 } 304 }
305 const emitter = getEmitter();
125 306
126 // The iHealth SDK uses the global DeviceEventEmitter (not NativeEventEmitter) 307 const scanSub = emitter.addListener(
127 // because the iOS native module doesn't subclass RCTEventEmitter.
128 const scanSub = DeviceEventEmitter.addListener(
129 iHealthDeviceManagerModule.Event_Scan_Device ?? 'event_scan_device', 308 iHealthDeviceManagerModule.Event_Scan_Device ?? 'event_scan_device',
130 (event: {mac: string; type: string; rssi?: number}) => { 309 (event: {mac: string; type: string; rssi?: number}) => {
131 const {mac = '', type = 'Unknown', rssi} = event; 310 const {mac = '', type = 'Unknown', rssi} = event;
132
133 // Filter out AM3 — likely false positives from non-iHealth heart rate devices
134 if (type === 'AM3') return; 311 if (type === 'AM3') return;
135 312
136 const existing = devicesRef.current.findIndex(d => d.mac === mac); 313 const existing = devicesRef.current.findIndex(d => d.mac === mac);
@@ -149,11 +326,9 @@ function App() {
149 }, 326 },
150 ); 327 );
151 328
152 const finishSub = DeviceEventEmitter.addListener( 329 const finishSub = emitter.addListener(
153 iHealthDeviceManagerModule.Event_Scan_Finish ?? 'event_scan_finish', 330 iHealthDeviceManagerModule.Event_Scan_Finish ?? 'event_scan_finish',
154 () => { 331 () => setScanning(false),
155 setScanning(false);
156 },
157 ); 332 );
158 333
159 return () => { 334 return () => {
@@ -164,187 +339,179 @@ function App() {
164 339
165 const startScan = async () => { 340 const startScan = async () => {
166 setError(null); 341 setError(null);
167
168 const granted = await requestAndroidPermissions(); 342 const granted = await requestAndroidPermissions();
169 if (!granted) { 343 if (!granted) {
170 setError('Bluetooth permissions denied'); 344 setError('Bluetooth permissions denied');
171 return; 345 return;
172 } 346 }
173
174 devicesRef.current = []; 347 devicesRef.current = [];
175 setDevices([]); 348 setDevices([]);
176 setScanning(true); 349 setScanning(true);
177 350
178 // Discover all device types. 'ALL' hits the default case in getDiscoveryType() 351 if (Platform.OS === 'android') {
179 // which maps to DiscoveryTypeEnum.All (MIX = BLE + BT Classic + WiFi). 352 try { iHealthDeviceManagerModule.startDiscovery('ALL'); }
180 // Requires ACCESS_NETWORK_STATE permission for WiFi scan. 353 catch (e) { console.log('Discovery failed:', e); }
181 try { 354 } else {
182 iHealthDeviceManagerModule.startDiscovery('ALL'); 355 const iosTypes = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4',
183 } catch (e) { 356 'PO3', 'HS2', 'HS2S', 'HS4S', 'BG5S', 'BG1S', 'PO1', 'ECG3'];
184 console.log('Failed to start discovery:', e); 357 let i = 0;
358 const scanNext = () => {
359 if (i < iosTypes.length) {
360 try { iHealthDeviceManagerModule.startDiscovery(iosTypes[i]); }
361 catch (e) { console.log(`Failed: ${iosTypes[i]}`, e); }
362 i++;
363 setTimeout(scanNext, 2000);
364 }
365 };
366 setTimeout(scanNext, 2000);
185 } 367 }
186 }; 368 };
187 369
188 const stopScan = () => { 370 const stopScan = () => {
189 try { 371 try { iHealthDeviceManagerModule.stopDiscovery(); }
190 iHealthDeviceManagerModule.stopDiscovery(); 372 catch (e) { console.log('Stop failed:', e); }
191 } catch (e) {
192 console.log('Failed to stop discovery:', e);
193 }
194 setScanning(false); 373 setScanning(false);
195 }; 374 };
196 375
197 const renderDevice = ({item}: {item: Device}) => (
198 <View style={styles.deviceRow}>
199 <DeviceIcon type={item.type} />
200 <View style={styles.deviceInfo}>
201 <Text style={styles.deviceType}>{item.type}</Text>
202 <Text style={styles.deviceMac}>{item.mac}</Text>
203 </View>
204 {item.rssi != null && (
205 <Text style={styles.deviceRssi}>{item.rssi} dBm</Text>
206 )}
207 </View>
208 );
209
210 return ( 376 return (
211 <SafeAreaProvider> 377 <SafeAreaView style={styles.container}>
212 <SafeAreaView style={styles.container}> 378 <Text style={styles.title}>iHealth Scanner</Text>
213 <Text style={styles.title}>iHealth Scanner</Text> 379 <Text style={styles.subtitle}>
214 <Text style={styles.subtitle}> 380 {scanning
215 {scanning 381 ? `Scanning... (${devices.length} found)`
216 ? `Scanning... (${devices.length} found)` 382 : `${devices.length} device(s) found`}
217 : `${devices.length} device(s) found`} 383 </Text>
218 </Text>
219 384
220 {error && <Text style={styles.error}>{error}</Text>} 385 {error && <Text style={styles.error}>{error}</Text>}
221 386
222 <TouchableOpacity 387 <TouchableOpacity
223 style={[styles.button, scanning && styles.buttonStop]} 388 style={[styles.button, scanning && styles.buttonStop]}
224 onPress={scanning ? stopScan : startScan}> 389 onPress={scanning ? stopScan : startScan}>
225 <Text style={styles.buttonText}> 390 <Text style={styles.buttonText}>
226 {scanning ? 'Stop Scan' : 'Start Scan'} 391 {scanning ? 'Stop Scan' : 'Start Scan'}
392 </Text>
393 </TouchableOpacity>
394
395 <FlatList
396 data={devices}
397 keyExtractor={item => item.mac}
398 renderItem={({item}) => (
399 <TouchableOpacity
400 style={styles.deviceRow}
401 onPress={() => {
402 stopScan();
403 onSelectDevice(item);
404 }}>
405 <View style={styles.deviceIcon}>
406 <Text style={styles.deviceIconText}>
407 {item.type.substring(0, 3)}
408 </Text>
409 </View>
410 <View style={styles.deviceInfo}>
411 <Text style={styles.deviceType}>{item.type}</Text>
412 <Text style={styles.deviceMac}>{item.mac}</Text>
413 </View>
414 {item.rssi != null && (
415 <Text style={styles.deviceRssi}>{item.rssi} dBm</Text>
416 )}
417 <Text style={styles.chevron}>{'>'}</Text>
418 </TouchableOpacity>
419 )}
420 style={styles.list}
421 contentContainerStyle={devices.length === 0 && styles.emptyList}
422 ListEmptyComponent={
423 <Text style={styles.emptyText}>
424 {scanning
425 ? 'Looking for iHealth devices...'
426 : 'Tap "Start Scan" to find nearby iHealth devices'}
227 </Text> 427 </Text>
228 </TouchableOpacity> 428 }
429 />
430 </SafeAreaView>
431 );
432}
229 433
230 <FlatList 434// ── App Root ─────────────────────────────────────────────────────────
231 data={devices} 435
232 keyExtractor={item => item.mac} 436function App() {
233 renderItem={renderDevice} 437 const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
234 style={styles.list} 438
235 contentContainerStyle={devices.length === 0 && styles.emptyList} 439 return (
236 ListEmptyComponent={ 440 <SafeAreaProvider>
237 <Text style={styles.emptyText}> 441 {selectedDevice ? (
238 {scanning 442 <DeviceScreen
239 ? 'Looking for iHealth devices...' 443 device={selectedDevice}
240 : 'Tap "Start Scan" to find nearby iHealth devices'} 444 onBack={() => setSelectedDevice(null)}
241 </Text>
242 }
243 /> 445 />
244 </SafeAreaView> 446 ) : (
447 <ScannerScreen onSelectDevice={setSelectedDevice} />
448 )}
245 </SafeAreaProvider> 449 </SafeAreaProvider>
246 ); 450 );
247} 451}
248 452
453// ── Styles ───────────────────────────────────────────────────────────
454
249const styles = StyleSheet.create({ 455const styles = StyleSheet.create({
250 container: { 456 container: {flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16},
251 flex: 1, 457 title: {fontSize: 28, fontWeight: '700', color: '#1a1a1a', marginTop: 16},
252 backgroundColor: '#f5f5f5', 458 subtitle: {fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16},
253 paddingHorizontal: 16,
254 },
255 title: {
256 fontSize: 28,
257 fontWeight: '700',
258 color: '#1a1a1a',
259 marginTop: 16,
260 },
261 subtitle: {
262 fontSize: 14,
263 color: '#666',
264 marginTop: 4,
265 marginBottom: 16,
266 },
267 error: { 459 error: {
268 color: '#d32f2f', 460 color: '#d32f2f', fontSize: 13, marginBottom: 8,
269 fontSize: 13, 461 backgroundColor: '#ffebee', padding: 8, borderRadius: 6,
270 marginBottom: 8,
271 backgroundColor: '#ffebee',
272 padding: 8,
273 borderRadius: 6,
274 }, 462 },
275 button: { 463 button: {
276 backgroundColor: '#2196F3', 464 backgroundColor: '#2196F3', paddingVertical: 14,
277 paddingVertical: 14, 465 borderRadius: 10, alignItems: 'center', marginBottom: 16,
278 borderRadius: 10,
279 alignItems: 'center',
280 marginBottom: 16,
281 },
282 buttonStop: {
283 backgroundColor: '#f44336',
284 },
285 buttonText: {
286 color: '#fff',
287 fontSize: 16,
288 fontWeight: '600',
289 },
290 list: {
291 flex: 1,
292 },
293 emptyList: {
294 flex: 1,
295 justifyContent: 'center',
296 alignItems: 'center',
297 },
298 emptyText: {
299 color: '#999',
300 fontSize: 15,
301 textAlign: 'center',
302 }, 466 },
467 buttonStop: {backgroundColor: '#f44336'},
468 buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'},
469 list: {flex: 1},
470 emptyList: {flex: 1, justifyContent: 'center', alignItems: 'center'},
471 emptyText: {color: '#999', fontSize: 15, textAlign: 'center'},
303 deviceRow: { 472 deviceRow: {
304 flexDirection: 'row', 473 flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff',
305 alignItems: 'center', 474 padding: 14, borderRadius: 10, marginBottom: 8,
306 backgroundColor: '#fff', 475 shadowColor: '#000', shadowOffset: {width: 0, height: 1},
307 padding: 14, 476 shadowOpacity: 0.05, shadowRadius: 2, elevation: 1,
308 borderRadius: 10,
309 marginBottom: 8,
310 shadowColor: '#000',
311 shadowOffset: {width: 0, height: 1},
312 shadowOpacity: 0.05,
313 shadowRadius: 2,
314 elevation: 1,
315 }, 477 },
316 deviceIcon: { 478 deviceIcon: {
317 width: 44, 479 width: 44, height: 44, borderRadius: 22, backgroundColor: '#e3f2fd',
318 height: 44, 480 justifyContent: 'center', alignItems: 'center', marginRight: 12,
319 borderRadius: 22,
320 backgroundColor: '#e3f2fd',
321 justifyContent: 'center',
322 alignItems: 'center',
323 marginRight: 12,
324 },
325 deviceIconText: {
326 fontSize: 13,
327 fontWeight: '700',
328 color: '#1565c0',
329 }, 481 },
330 deviceInfo: { 482 deviceIconText: {fontSize: 12, fontWeight: '700', color: '#1565c0'},
331 flex: 1, 483 deviceInfo: {flex: 1},
484 deviceType: {fontSize: 16, fontWeight: '600', color: '#1a1a1a'},
485 deviceMac: {
486 fontSize: 12, color: '#888', marginTop: 2,
487 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
332 }, 488 },
333 deviceType: { 489 deviceRssi: {fontSize: 12, color: '#999', marginLeft: 8},
334 fontSize: 16, 490 chevron: {fontSize: 18, color: '#ccc', marginLeft: 8},
491 // Device screen
492 backButton: {marginTop: 12, marginBottom: 4},
493 backText: {color: '#2196F3', fontSize: 16},
494 statusBadge: {
495 fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16,
335 fontWeight: '600', 496 fontWeight: '600',
336 color: '#1a1a1a',
337 }, 497 },
338 deviceMac: { 498 statusConnected: {color: '#4caf50'},
339 fontSize: 12, 499 sectionTitle: {
340 color: '#888', 500 fontSize: 16, fontWeight: '600', color: '#333',
341 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', 501 marginTop: 8, marginBottom: 8,
342 marginTop: 2, 502 },
503 actionsRow: {flexGrow: 0, marginBottom: 12},
504 actionButton: {
505 backgroundColor: '#e3f2fd', paddingVertical: 10, paddingHorizontal: 16,
506 borderRadius: 8, marginRight: 8,
343 }, 507 },
344 deviceRssi: { 508 actionDisabled: {backgroundColor: '#eee'},
345 fontSize: 12, 509 actionText: {color: '#1565c0', fontSize: 13, fontWeight: '600'},
346 color: '#999', 510 actionTextDisabled: {color: '#bbb'},
347 marginLeft: 8, 511 logList: {flex: 1, marginTop: 4},
512 logLine: {
513 fontSize: 11, color: '#555', paddingVertical: 3,
514 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
348 }, 515 },
349}); 516});
350 517