import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import 'api.dart'; import 'beacon_permissions.dart'; /// Result of a beacon scan class BeaconScanResult { final BeaconLookupResult? bestBeacon; final int beaconsFound; final String? error; const BeaconScanResult({ this.bestBeacon, this.beaconsFound = 0, this.error, }); bool get success => error == null; bool get foundBeacon => bestBeacon != null; } /// Global beacon scanner service for rescanning from anywhere in the app class BeaconScannerService { static final BeaconScannerService _instance = BeaconScannerService._internal(); factory BeaconScannerService() => _instance; BeaconScannerService._internal(); bool _isScanning = false; bool get isScanning => _isScanning; // Callbacks for UI updates final _scanStateController = StreamController.broadcast(); Stream get scanStateStream => _scanStateController.stream; /// Perform a beacon scan /// If [businessId] is provided, only scans for that business's beacons (optimized) /// Otherwise, scans for all beacons and looks them up Future scan({int? businessId}) async { if (_isScanning) { return const BeaconScanResult(error: "Scan already in progress"); } _isScanning = true; _scanStateController.add(true); try { // Request permissions final granted = await BeaconPermissions.requestPermissions(); if (!granted) { return const BeaconScanResult(error: "Permissions denied"); } // Check Bluetooth final bluetoothOn = await BeaconPermissions.ensureBluetoothEnabled(); if (!bluetoothOn) { return const BeaconScanResult(error: "Bluetooth is off"); } // Get beacon UUIDs to scan for Map knownBeacons = {}; if (businessId != null && businessId > 0) { // Optimized: only get beacons for this business debugPrint('[BeaconScanner] Scanning for business $businessId beacons only'); try { knownBeacons = await Api.listBeaconsByBusiness(businessId: businessId); debugPrint('[BeaconScanner] Got ${knownBeacons.length} beacons for business'); } catch (e) { debugPrint('[BeaconScanner] Failed to get business beacons: $e'); } } // Fall back to all beacons if business-specific didn't work if (knownBeacons.isEmpty) { debugPrint('[BeaconScanner] Fetching all beacon UUIDs'); try { knownBeacons = await Api.listAllBeacons(); debugPrint('[BeaconScanner] Got ${knownBeacons.length} total beacons'); } catch (e) { debugPrint('[BeaconScanner] Failed to fetch beacons: $e'); return BeaconScanResult(error: "Failed to fetch beacons: $e"); } } if (knownBeacons.isEmpty) { return const BeaconScanResult(error: "No beacons configured"); } // Initialize scanning await flutterBeacon.initializeScanning; // Create regions for all known UUIDs final regions = knownBeacons.keys.map((uuid) { final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}'; return Region(identifier: uuid, proximityUUID: formattedUUID); }).toList(); // Collect RSSI samples final Map> rssiSamples = {}; final Map detectionCounts = {}; debugPrint('[BeaconScanner] Starting 2-second scan...'); StreamSubscription? subscription; subscription = flutterBeacon.ranging(regions).listen((result) { for (var beacon in result.beacons) { final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', ''); final rssi = beacon.rssi; rssiSamples.putIfAbsent(uuid, () => []).add(rssi); detectionCounts[uuid] = (detectionCounts[uuid] ?? 0) + 1; debugPrint('[BeaconScanner] Found $uuid RSSI=$rssi'); } }); await Future.delayed(const Duration(milliseconds: 2000)); await subscription.cancel(); if (rssiSamples.isEmpty) { debugPrint('[BeaconScanner] No beacons detected'); return const BeaconScanResult(beaconsFound: 0); } // Lookup found beacons debugPrint('[BeaconScanner] Looking up ${rssiSamples.length} beacons...'); final uuids = rssiSamples.keys.toList(); List lookupResults = []; try { lookupResults = await Api.lookupBeacons(uuids); debugPrint('[BeaconScanner] Server returned ${lookupResults.length} registered beacons'); } catch (e) { debugPrint('[BeaconScanner] Lookup error: $e'); return BeaconScanResult( beaconsFound: rssiSamples.length, error: "Failed to lookup beacons", ); } // Find the best registered beacon final bestBeacon = _findBestBeacon(lookupResults, rssiSamples, detectionCounts); debugPrint('[BeaconScanner] Best beacon: ${bestBeacon?.beaconName ?? "none"}'); return BeaconScanResult( bestBeacon: bestBeacon, beaconsFound: rssiSamples.length, ); } catch (e) { debugPrint('[BeaconScanner] Scan error: $e'); return BeaconScanResult(error: e.toString()); } finally { _isScanning = false; _scanStateController.add(false); } } /// Find the best registered beacon based on RSSI BeaconLookupResult? _findBestBeacon( List registeredBeacons, Map> rssiSamples, Map detectionCounts, ) { if (registeredBeacons.isEmpty) return null; BeaconLookupResult? best; double bestAvgRssi = -999; for (final beacon in registeredBeacons) { final samples = rssiSamples[beacon.uuid]; if (samples == null || samples.isEmpty) continue; final detections = detectionCounts[beacon.uuid] ?? 0; if (detections < 2) continue; // Need at least 2 detections final avgRssi = samples.reduce((a, b) => a + b) / samples.length; if (avgRssi > bestAvgRssi && avgRssi >= -85) { bestAvgRssi = avgRssi; best = beacon; } } // Fall back to strongest if none meet threshold if (best == null) { for (final beacon in registeredBeacons) { final samples = rssiSamples[beacon.uuid]; if (samples == null || samples.isEmpty) continue; final avgRssi = samples.reduce((a, b) => a + b) / samples.length; if (avgRssi > bestAvgRssi) { bestAvgRssi = avgRssi; best = beacon; } } } return best; } void dispose() { _scanStateController.close(); } }