Skip to content

Commit b2a08db

Browse files
ryanmiocursoragent
andcommitted
Add scan cancellation when device is selected
Scanner now stops immediately when user selects a device or closes the modal. Returns ScanController with cancel() function. Prevents wasted battery and network usage from continuing to scan after selection. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b692681 commit b2a08db

File tree

2 files changed

+110
-89
lines changed

2 files changed

+110
-89
lines changed

boat-telemetry-app/src/components/DeviceScannerModal.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import {
33
View,
44
Text,
@@ -9,7 +9,7 @@ import {
99
ActivityIndicator,
1010
} from 'react-native';
1111
import AsyncStorage from '@react-native-async-storage/async-storage';
12-
import { ScannedDevice, scanForDevices } from '../services/networkScanService';
12+
import { ScannedDevice, scanForDevices, ScanController } from '../services/networkScanService';
1313
import { COLORS, FONTS } from '../constants/Theme';
1414

1515
interface DeviceScannerModalProps {
@@ -36,6 +36,7 @@ export const DeviceScannerModal: React.FC<DeviceScannerModalProps> = ({
3636
const [totalToScan, setTotalToScan] = useState(0);
3737
const [scanComplete, setScanComplete] = useState(false);
3838
const [recentlyUsedIPs, setRecentlyUsedIPs] = useState<string[]>([]);
39+
const scanControllerRef = useRef<ScanController | null>(null);
3940

4041
// Load recently found IPs on mount
4142
useEffect(() => {
@@ -81,7 +82,7 @@ export const DeviceScannerModal: React.FC<DeviceScannerModalProps> = ({
8182
setTotalToScan(0);
8283

8384
try {
84-
const finalDevices = await scanForDevices(
85+
const controller = scanForDevices(
8586
(foundDevices, checked, total) => {
8687
// Real-time updates: show devices as they're found
8788
setIpsChecked(checked);
@@ -99,6 +100,11 @@ export const DeviceScannerModal: React.FC<DeviceScannerModalProps> = ({
99100
recentlyUsedIPs
100101
);
101102

103+
// Store controller so we can cancel scan later
104+
scanControllerRef.current = controller;
105+
106+
const finalDevices = await controller.promise;
107+
102108
console.log(`[DeviceScanner] Scan complete. Found ${finalDevices.length} devices`);
103109

104110
// Final update with type filtering
@@ -114,15 +120,24 @@ export const DeviceScannerModal: React.FC<DeviceScannerModalProps> = ({
114120
} finally {
115121
setScanning(false);
116122
setScanComplete(true);
123+
scanControllerRef.current = null;
117124
}
118125
};
119126

120127
const handleSelectDevice = (ip: string) => {
128+
// Cancel ongoing scan when device is selected
129+
if (scanControllerRef.current) {
130+
scanControllerRef.current.cancel();
131+
}
121132
onSelectDevice(ip);
122133
handleClose();
123134
};
124135

125136
const handleClose = () => {
137+
// Cancel ongoing scan when modal is closed
138+
if (scanControllerRef.current) {
139+
scanControllerRef.current.cancel();
140+
}
126141
setDevices([]);
127142
setDevicesFound(0);
128143
setScanComplete(false);

boat-telemetry-app/src/services/networkScanService.ts

Lines changed: 92 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export interface ScannedDevice {
1313
lastSeen: number;
1414
}
1515

16+
export interface ScanController {
17+
cancel: () => void;
18+
promise: Promise<ScannedDevice[]>;
19+
}
20+
1621
const TIMEOUT_MS = 2000; // 2 second timeout per device
1722
const QUICK_PROBE_TIMEOUT_MS = 1000; // Faster timeout for known IPs
1823

@@ -185,108 +190,109 @@ async function getLocalIP(): Promise<string | null> {
185190
*
186191
* @param onProgress - Callback called as devices are found (passes devices array for real-time display)
187192
* @param recentlyUsedIPs - IPs that were recently found (should be provided by component)
188-
* @returns Promise that resolves when scan is complete
193+
* @returns ScanController with cancel() and promise
189194
*/
190-
export async function scanForDevices(
195+
export function scanForDevices(
191196
onProgress?: (devices: ScannedDevice[], checked: number, total: number) => void,
192197
recentlyUsedIPs: string[] = []
193-
): Promise<ScannedDevice[]> {
194-
const devices: ScannedDevice[] = [];
195-
let checkedCount = 0;
196-
197-
// #region agent log - scan start
198-
fetch('http://127.0.0.1:7242/ingest/d3a9473c-c512-41da-a870-4c36bb5dd5ec',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'networkScanService.ts:195',message:'scanForDevices start',data:{recentIPsLength:recentlyUsedIPs.length},timestamp:Date.now(),hypothesisId:'H3_scan_progress'})}).catch(()=>{});
199-
// #endregion
198+
): ScanController {
199+
let cancelled = false;
200200

201-
console.log('[NetworkScan] Starting progressive device scan...');
202-
console.log('[NetworkScan] Recently used IPs:', recentlyUsedIPs);
203-
204-
// Phase 1: Quickly probe recently used IPs (should be instant if devices are online)
205-
if (recentlyUsedIPs.length > 0) {
206-
console.log(`[NetworkScan] Phase 1: Probing ${recentlyUsedIPs.length} recently used IPs...`);
201+
const promise = (async (): Promise<ScannedDevice[]> => {
202+
const devices: ScannedDevice[] = [];
203+
let checkedCount = 0;
207204

208-
const recentResults = await Promise.all(
209-
recentlyUsedIPs.map(ip => probeIP(ip, QUICK_PROBE_TIMEOUT_MS))
210-
);
211-
212-
recentResults.forEach((result, idx) => {
213-
if (result) {
214-
console.log(`[NetworkScan] Found recent device at ${result.ip}`);
215-
devices.push(result);
216-
}
217-
checkedCount++;
218-
});
219-
220-
// #region agent log - phase 1 complete
221-
fetch('http://127.0.0.1:7242/ingest/d3a9473c-c512-41da-a870-4c36bb5dd5ec',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'networkScanService.ts:219',message:'Phase 1 complete',data:{devicesFound:devices.length,checkedCount},timestamp:Date.now(),hypothesisId:'H3_scan_progress'})}).catch(()=>{});
222-
// #endregion
223-
224-
// Report progress after recent IPs checked - pass devices array for real-time display
225-
const totalEstimate = recentlyUsedIPs.length + COMMON_IP_RANGES.reduce((sum, r) => sum + (r.end - r.start + 1), 0);
226-
onProgress?.([...devices], checkedCount, totalEstimate);
227-
}
205+
console.log('[NetworkScan] Starting progressive device scan...');
206+
console.log('[NetworkScan] Recently used IPs:', recentlyUsedIPs);
207+
208+
// Phase 1: Quickly probe recently used IPs (should be instant if devices are online)
209+
if (recentlyUsedIPs.length > 0) {
210+
console.log(`[NetworkScan] Phase 1: Probing ${recentlyUsedIPs.length} recently used IPs...`);
211+
212+
const recentResults = await Promise.all(
213+
recentlyUsedIPs.map(ip => probeIP(ip, QUICK_PROBE_TIMEOUT_MS))
214+
);
215+
216+
recentResults.forEach((result, idx) => {
217+
if (result) {
218+
console.log(`[NetworkScan] Found recent device at ${result.ip}`);
219+
devices.push(result);
220+
}
221+
checkedCount++;
222+
});
228223

229-
// Phase 2: Scan subnet ranges in parallel for faster coverage
230-
console.log('[NetworkScan] Phase 2: Scanning full subnets in parallel...');
231-
232-
const allIPs: string[] = [];
233-
for (const range of COMMON_IP_RANGES) {
234-
for (let i = range.start; i <= range.end; i++) {
235-
allIPs.push(`${range.base}.${i}`);
224+
// Report progress after recent IPs checked - pass devices array for real-time display
225+
const totalEstimate = recentlyUsedIPs.length + COMMON_IP_RANGES.reduce((sum, r) => sum + (r.end - r.start + 1), 0);
226+
onProgress?.([...devices], checkedCount, totalEstimate);
236227
}
237-
}
238-
239-
// Filter out IPs we already checked
240-
const recentIPSet = new Set(recentlyUsedIPs);
241-
const uncheckedIPs = allIPs.filter(ip => !recentIPSet.has(ip));
242-
243-
console.log(`[NetworkScan] Scanning ${uncheckedIPs.length} unchecked IP addresses`);
244-
245-
// #region agent log - phase 2 start
246-
fetch('http://127.0.0.1:7242/ingest/d3a9473c-c512-41da-a870-4c36bb5dd5ec',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'networkScanService.ts:244',message:'Phase 2 start',data:{uncheckedIPsLength:uncheckedIPs.length},timestamp:Date.now(),hypothesisId:'H3_scan_progress'})}).catch(()=>{});
247-
// #endregion
248228

249-
// Probe in parallel batches for speed
250-
const batchSize = 15; // Increased batch size since we're doing it faster
251-
252-
for (let i = 0; i < uncheckedIPs.length; i += batchSize) {
253-
const batch = uncheckedIPs.slice(i, i + batchSize);
229+
// Phase 2: Scan subnet ranges in parallel for faster coverage
230+
console.log('[NetworkScan] Phase 2: Scanning full subnets in parallel...');
254231

255-
const results = await Promise.all(batch.map(ip => probeIP(ip)));
256-
257-
results.forEach(result => {
258-
if (result) {
259-
console.log(`[NetworkScan] Found device at ${result.ip} (type: ${result.type})`);
260-
261-
// Avoid duplicates
262-
if (!devices.find(d => d.ip === result.ip)) {
263-
devices.push(result);
264-
saveRecentIP(result.ip);
265-
}
232+
const allIPs: string[] = [];
233+
for (const range of COMMON_IP_RANGES) {
234+
for (let i = range.start; i <= range.end; i++) {
235+
allIPs.push(`${range.base}.${i}`);
266236
}
267-
});
237+
}
268238

269-
checkedCount += batch.length;
239+
// Filter out IPs we already checked
240+
const recentIPSet = new Set(recentlyUsedIPs);
241+
const uncheckedIPs = allIPs.filter(ip => !recentIPSet.has(ip));
270242

271-
// Call progress callback with current devices array for real-time UI updates
272-
onProgress?.([...devices], recentlyUsedIPs.length + checkedCount, recentlyUsedIPs.length + allIPs.length);
273-
}
274-
275-
console.log(`[NetworkScan] Scan complete. Found ${devices.length} devices.`);
243+
console.log(`[NetworkScan] Scanning ${uncheckedIPs.length} unchecked IP addresses`);
276244

277-
// #region agent log - scan complete
278-
fetch('http://127.0.0.1:7242/ingest/d3a9473c-c512-41da-a870-4c36bb5dd5ec',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'networkScanService.ts:279',message:'Scan complete',data:{devicesFound:devices.length},timestamp:Date.now(),hypothesisId:'H3_scan_progress'})}).catch(()=>{});
279-
// #endregion
245+
// Probe in parallel batches for speed
246+
const batchSize = 15;
247+
248+
for (let i = 0; i < uncheckedIPs.length; i += batchSize) {
249+
// Check if scan was cancelled
250+
if (cancelled) {
251+
console.log('[NetworkScan] Scan cancelled by user');
252+
break;
253+
}
254+
255+
const batch = uncheckedIPs.slice(i, i + batchSize);
256+
257+
const results = await Promise.all(batch.map(ip => probeIP(ip)));
258+
259+
results.forEach(result => {
260+
if (result) {
261+
console.log(`[NetworkScan] Found device at ${result.ip} (type: ${result.type})`);
262+
263+
// Avoid duplicates
264+
if (!devices.find(d => d.ip === result.ip)) {
265+
devices.push(result);
266+
saveRecentIP(result.ip);
267+
}
268+
}
269+
});
280270

281-
// Sort by type (telemetry first), then by IP
282-
devices.sort((a, b) => {
283-
if (a.type !== b.type) {
284-
return a.type === 'telemetry' ? -1 : 1;
271+
checkedCount += batch.length;
272+
273+
// Call progress callback with current devices array for real-time UI updates
274+
onProgress?.([...devices], recentlyUsedIPs.length + checkedCount, recentlyUsedIPs.length + allIPs.length);
285275
}
286-
return a.ip.localeCompare(b.ip);
287-
});
288276

289-
return devices;
277+
console.log(`[NetworkScan] Scan ${cancelled ? 'cancelled' : 'complete'}. Found ${devices.length} devices.`);
278+
279+
// Sort by type (telemetry first), then by IP
280+
devices.sort((a, b) => {
281+
if (a.type !== b.type) {
282+
return a.type === 'telemetry' ? -1 : 1;
283+
}
284+
return a.ip.localeCompare(b.ip);
285+
});
286+
287+
return devices;
288+
})();
289+
290+
return {
291+
cancel: () => {
292+
cancelled = true;
293+
},
294+
promise,
295+
};
290296
}
291297

292298
/**

0 commit comments

Comments
 (0)