Device Discovery: Multicast & Subnet Scans
Nearby device discovery in LocalSend runs two parallel strategies — a UDP multicast announce/response and HTTP scans (favorites first, then a full /24 subnet sweep as a fallback) — both funneling every found Device through a single RegisterDeviceAction into the in-memory NearbyDevicesState cache. The send tab reads this cache directly (merging IP-keyed and signaling-fingerprint-keyed devices via allDevices) to populate the device picker, so tapping a device there is the exact point where discovery output becomes the target of an outgoing transfer.
StartSmartScan: escalate only when needed
app/lib/provider/network/scan_facade.dart · lines 16–59Multicast dispatch fires immediately (fire-and-forget), while the favorite-device HTTP scan is awaited right after — both cheap methods run before anything expensive.
Gives multicast/favorite results a 1-second head start before deciding whether more scanning is needed; forceLegacy skips this grace period entirely.
Escalates to a full subnet HTTP sweep only if the device cache is still empty and the user hasn't navigated away from the send tab (or forceLegacy overrides both checks).
Otherwise it just logs why the legacy scan was skipped — either devices were already found, or the user left the send tab — instead of silently sweeping the subnet anyway.
Future<void> reduce() async { // Try performant Multicast/UDP method first ref.redux(nearbyDevicesProvider).dispatch(StartMulticastScan()); // At the same time, try to discover favorites final favorites = ref.read(favoritesProvider); final https = ref.read(settingsProvider).https; await ref.redux(nearbyDevicesProvider).dispatchAsync(StartFavoriteScan(devices: favorites, https: https)); if (!forceLegacy) { // Wait a bit before trying the legacy method. // Skip waiting if [forceLegacy] is true. await sleepAsync(1000); } // If no devices has been found, then switch to legacy discovery mode // which is purely HTTP/TCP based. final stillEmpty = ref.read(nearbyDevicesProvider).devices.isEmpty; final stillInSendTab = ref.read(homePageControllerProvider).currentTab == HomeTab.send; if (forceLegacy || (stillEmpty && stillInSendTab)) { final networkInterfaces = ref.read(localIpProvider).localIps.take(maxInterfaces).toList(); if (networkInterfaces.isNotEmpty) { await dispatchAsync(StartLegacySubnetScan(subnets: networkInterfaces)); } } else { if (!stillEmpty) { emitMessage('Already found devices. This network seem to work, no need to start legacy scan.'); } if (!stillInSendTab) { emitMessage('User left the send tab. No need to start legacy scan.'); } } }
Stream<Device> getStream({required String networkInterface, required int port, required bool https}) { final ipList = List.generate(256, (i) => '${networkInterface.split('.').take(3).join('.')}.$i').where((ip) => ip != networkInterface).toList(); _runners[networkInterface]?.stop(); _runners[networkInterface] = TaskRunner<Device?>( initialTasks: List.generate( ipList.length, (index) => () async => _doRequest(ipList[index], port, https), ), concurrency: 50, ); return _runners[networkInterface]!.stream.where((device) => device != null).cast<Device>(); }
common/lib/src/task/discovery/http_scan_discovery.dart · lines 24–36class RegisterDeviceAction extends AsyncReduxAction<NearbyDevicesService, NearbyDevicesState> { final Device device; RegisterDeviceAction(this.device); @override bool get trackOrigin => false; @override Future<NearbyDevicesState> reduce() async { assert(device.ip?.isNotEmpty ?? false, 'IP must not be empty'); final favoriteDevice = notifier._favoriteService.state.firstWhereOrNull((e) => e.fingerprint == device.fingerprint); if (favoriteDevice != null && !favoriteDevice.customAlias) { // Update existing favorite with new alias await external(notifier._favoriteService).dispatchAsync(UpdateFavoriteAction(favoriteDevice.copyWith(alias: device.alias))); } else { await Future.microtask(() {}); } return state.copyWith( devices: {...state.devices}..update(device.ip!, (_) => device, ifAbsent: () => device), ); }}
app/lib/provider/network/nearby_devices_provider.dart · lines 72–95app/lib/model/state/nearby_devices_state.dartCheck your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
Why can two Device entries with the same fingerprint coexist in signalingDevices but never in devices?
What actually triggers StartSmartScan to run StartLegacySubnetScan's full /24 HTTP sweep?
How does the app avoid launching two concurrent HTTP scans of the same local subnet?