import 'dart:async'; import 'package:flutter/material.dart'; import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import 'package:provider/provider.dart'; import '../app/app_router.dart'; import '../app/app_state.dart'; import '../services/beacon_permissions.dart'; import '../services/api.dart'; class BeaconScanScreen extends StatefulWidget { const BeaconScanScreen({super.key}); @override State createState() => _BeaconScanScreenState(); } class _BeaconScanScreenState extends State with SingleTickerProviderStateMixin { String _status = 'Initializing...'; bool _permissionsGranted = false; bool _scanning = false; Map _uuidToBeaconId = {}; final Map> _beaconRssiSamples = {}; // UUID -> List of RSSI values final Map _beaconDetectionCount = {}; // UUID -> detection count late AnimationController _pulseController; late Animation _pulseAnimation; @override void initState() { super.initState(); _pulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(reverse: true); _pulseAnimation = Tween(begin: 0.8, end: 1.2).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); _startScanFlow(); } @override void dispose() { _pulseController.dispose(); super.dispose(); } Future _startScanFlow() async { // Step 1: Request permissions setState(() => _status = 'Requesting permissions...'); final granted = await BeaconPermissions.requestPermissions(); if (!granted) { setState(() { _status = 'Permissions denied - Please enable Location & Bluetooth'; _permissionsGranted = false; }); return; } setState(() => _permissionsGranted = true); // Step 2: Fetch all active beacons from server setState(() => _status = 'Loading beacon data...'); try { _uuidToBeaconId = await Api.listAllBeacons(); debugPrint('[BeaconScan] ========================================'); debugPrint('[BeaconScan] Loaded ${_uuidToBeaconId.length} beacons from database:'); _uuidToBeaconId.forEach((uuid, beaconId) { debugPrint('[BeaconScan] BeaconID=$beaconId'); debugPrint('[BeaconScan] UUID=$uuid'); debugPrint('[BeaconScan] ---'); }); debugPrint('[BeaconScan] ========================================'); } catch (e) { debugPrint('[BeaconScan] Error loading beacons: $e'); _uuidToBeaconId = {}; } if (_uuidToBeaconId.isEmpty) { debugPrint('[BeaconScan] No beacons in database, going to restaurant select'); if (mounted) _navigateToRestaurantSelect(); return; } // Step 3: Perform initial scan setState(() { _status = 'Scanning for nearby beacons...'; _scanning = true; }); await _performInitialScan(); } Future _performInitialScan() async { try { // Initialize beacon monitoring await flutterBeacon.initializeScanning; // Create regions for all known UUIDs final regions = _uuidToBeaconId.keys.map((uuid) { // Format UUID with dashes for the plugin final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}'; debugPrint('[BeaconScan] Creating region for UUID: $uuid -> $formattedUUID'); return Region(identifier: uuid, proximityUUID: formattedUUID); }).toList(); debugPrint('[BeaconScan] Created ${regions.length} regions for scanning'); if (regions.isEmpty) { if (mounted) _navigateToRestaurantSelect(); return; } // Perform 5 scans of 2 seconds each to increase chance of detecting all beacons debugPrint('[BeaconScan] Starting 5 scan cycles of 2 seconds each'); for (int scanCycle = 1; scanCycle <= 5; scanCycle++) { debugPrint('[BeaconScan] ----- Scan cycle $scanCycle/5 -----'); StreamSubscription? subscription; subscription = flutterBeacon.ranging(regions).listen((result) { debugPrint('[BeaconScan] Ranging result: ${result.beacons.length} beacons in range'); for (var beacon in result.beacons) { final rawUUID = beacon.proximityUUID; final uuid = rawUUID.toUpperCase().replaceAll('-', ''); final rssi = beacon.rssi; debugPrint('[BeaconScan] Raw beacon detected: UUID=$rawUUID (normalized: $uuid), RSSI=$rssi, Major=${beacon.major}, Minor=${beacon.minor}'); if (_uuidToBeaconId.containsKey(uuid)) { // Collect RSSI samples for averaging _beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi); _beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1; debugPrint('[BeaconScan] MATCHED! BeaconID=${_uuidToBeaconId[uuid]}, Sample #${_beaconDetectionCount[uuid]}, RSSI=$rssi'); } else { debugPrint('[BeaconScan] UUID not in database, ignoring'); } } }); // Wait 2 seconds for this scan cycle to collect beacon data await Future.delayed(const Duration(seconds: 2)); await subscription.cancel(); // Short pause between scan cycles if (scanCycle < 5) { await Future.delayed(const Duration(milliseconds: 200)); } } debugPrint('[BeaconScan] All scan cycles complete'); if (!mounted) return; // Analyze results and select best beacon debugPrint('[BeaconScan] ===== SCAN COMPLETE ====='); debugPrint('[BeaconScan] Total beacons detected: ${_beaconRssiSamples.length}'); final beaconScores = {}; for (final uuid in _beaconRssiSamples.keys) { final samples = _beaconRssiSamples[uuid]!; final detections = _beaconDetectionCount[uuid]!; final beaconId = _uuidToBeaconId[uuid]!; // Calculate average RSSI final avgRssi = samples.reduce((a, b) => a + b) / samples.length; // Calculate RSSI variance for stability metric final variance = samples.map((r) => (r - avgRssi) * (r - avgRssi)).reduce((a, b) => a + b) / samples.length; beaconScores[uuid] = BeaconScore( uuid: uuid, beaconId: beaconId, avgRssi: avgRssi, minRssi: samples.reduce((a, b) => a < b ? a : b), maxRssi: samples.reduce((a, b) => a > b ? a : b), detectionCount: detections, variance: variance, ); } if (beaconScores.isNotEmpty) { debugPrint('[BeaconScan] Beacon analysis results:'); final sorted = beaconScores.values.toList() ..sort((a, b) => b.avgRssi.compareTo(a.avgRssi)); // Sort by avg RSSI descending for (final score in sorted) { debugPrint('[BeaconScan] - BeaconID=${score.beaconId}:'); debugPrint('[BeaconScan] Avg RSSI: ${score.avgRssi.toStringAsFixed(1)}'); debugPrint('[BeaconScan] Range: ${score.minRssi} to ${score.maxRssi}'); debugPrint('[BeaconScan] Detections: ${score.detectionCount}'); debugPrint('[BeaconScan] Variance: ${score.variance.toStringAsFixed(2)}'); } } debugPrint('[BeaconScan] =========================='); if (beaconScores.isEmpty) { setState(() => _status = 'No beacons nearby'); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) _navigateToRestaurantSelect(); } else { // Find beacon with highest average RSSI and minimum detections final best = _findBestBeacon(beaconScores); if (best != null) { debugPrint('[BeaconScan] Selected beacon: BeaconID=${best.beaconId} (avg RSSI: ${best.avgRssi.toStringAsFixed(1)})'); setState(() => _status = 'Beacon detected! Loading business...'); await _autoSelectBusinessFromBeacon(best.beaconId); } else { debugPrint('[BeaconScan] No beacon met minimum confidence threshold'); setState(() => _status = 'No strong beacon signal'); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) _navigateToRestaurantSelect(); } } } catch (e) { debugPrint('[BeaconScan] Error during scan: $e'); if (mounted) { setState(() => _status = 'Scan error - continuing to manual selection'); await Future.delayed(const Duration(seconds: 1)); if (mounted) _navigateToRestaurantSelect(); } } } Future _autoSelectBusinessFromBeacon(int beaconId) async { try { final mapping = await Api.getBusinessFromBeacon(beaconId: beaconId); if (!mounted) return; // Update app state with selected business and service point final appState = context.read(); appState.setBusinessAndServicePoint( mapping.businessId, mapping.servicePointId, businessName: mapping.businessName, servicePointName: mapping.servicePointName, ); // Update API business ID for headers Api.setBusinessId(mapping.businessId); setState(() => _status = 'Welcome to ${mapping.businessName}!'); await Future.delayed(const Duration(milliseconds: 800)); if (!mounted) return; // Navigate directly to menu (user can browse without logging in) Navigator.of(context).pushReplacementNamed( AppRoutes.menuBrowse, arguments: { 'businessId': mapping.businessId, 'servicePointId': mapping.servicePointId, }, ); } catch (e) { debugPrint('[BeaconScan] Error fetching business from beacon: $e'); if (mounted) { setState(() => _status = 'Beacon not assigned - selecting manually'); await Future.delayed(const Duration(seconds: 1)); if (mounted) _navigateToRestaurantSelect(); } } } BeaconScore? _findBestBeacon(Map scores) { if (scores.isEmpty) return null; // Filter beacons that meet minimum requirements const minDetections = 3; // Must be seen at least 3 times const minRssi = -85; // Minimum average RSSI (beacons further than ~10m will be weaker) final qualified = scores.values.where((score) { return score.detectionCount >= minDetections && score.avgRssi >= minRssi; }).toList(); if (qualified.isEmpty) { debugPrint('[BeaconScan] No beacons met minimum requirements (detections>=$minDetections, avgRSSI>=$minRssi)'); // Fall back to best available beacon if none meet threshold final allSorted = scores.values.toList() ..sort((a, b) => b.avgRssi.compareTo(a.avgRssi)); return allSorted.isNotEmpty ? allSorted.first : null; } // Sort by average RSSI (higher is better/closer) qualified.sort((a, b) => b.avgRssi.compareTo(a.avgRssi)); return qualified.first; } void _navigateToRestaurantSelect() { Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); } void _retryPermissions() async { await BeaconPermissions.openSettings(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_scanning) ScaleTransition( scale: _pulseAnimation, child: Container( width: 100, height: 100, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ Colors.blue.withAlpha(102), Colors.blue.withAlpha(26), ], ), ), child: const Icon( Icons.bluetooth_searching, color: Colors.white, size: 48, ), ), ) else if (_permissionsGranted) const Icon(Icons.bluetooth_searching, color: Colors.white70, size: 64) else const Icon(Icons.bluetooth_disabled, color: Colors.white70, size: 64), const SizedBox(height: 24), Text( _status, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), if (_beaconRssiSamples.isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Found ${_beaconRssiSamples.length} beacon(s)', style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], if (!_permissionsGranted && _status.contains('denied')) ...[ const SizedBox(height: 24), FilledButton( onPressed: _retryPermissions, child: const Text('Open Settings'), ), const SizedBox(height: 12), TextButton( onPressed: _navigateToRestaurantSelect, style: TextButton.styleFrom(foregroundColor: Colors.white70), child: const Text('Skip and select manually'), ), ], if (_permissionsGranted && _scanning) ...[ const SizedBox(height: 24), TextButton( onPressed: _navigateToRestaurantSelect, style: TextButton.styleFrom(foregroundColor: Colors.white70), child: const Text('Skip and select manually'), ), ], ], ), ), ); } } class BeaconScore { final String uuid; final int beaconId; final double avgRssi; final int minRssi; final int maxRssi; final int detectionCount; final double variance; const BeaconScore({ required this.uuid, required this.beaconId, required this.avgRssi, required this.minRssi, required this.maxRssi, required this.detectionCount, required this.variance, }); }