Use native scanner across all beacon services, simplify splash
- Update beacon_scan_screen and beacon_scanner_service to use native Android scanner with Flutter plugin fallback for iOS - Add native permission checking for fast path startup - Simplify splash screen: remove bouncing logo animation, show only spinner on black background for clean transition - Native splash is now black-only to match Flutter splash Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d5f0721215
commit
982152383a
6 changed files with 345 additions and 447 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/black" />
|
<item android:drawable="@android:color/black" />
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/black" />
|
<item android:drawable="@android:color/black" />
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart';
|
import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../app/app_router.dart';
|
import '../app/app_router.dart';
|
||||||
import '../app/app_state.dart';
|
import '../app/app_state.dart';
|
||||||
|
import '../services/beacon_channel.dart';
|
||||||
import '../services/beacon_permissions.dart';
|
import '../services/beacon_permissions.dart';
|
||||||
import '../services/api.dart';
|
import '../services/api.dart';
|
||||||
|
|
||||||
|
|
@ -112,6 +114,77 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
|
|
||||||
Future<void> _performInitialScan() async {
|
Future<void> _performInitialScan() async {
|
||||||
try {
|
try {
|
||||||
|
if (_uuidToBeaconId.isEmpty) {
|
||||||
|
if (mounted) _navigateToRestaurantSelect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use native scanner on Android, Flutter plugin on iOS
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await _scanWithNativeScanner();
|
||||||
|
} else {
|
||||||
|
await _scanWithFlutterPlugin();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[BeaconScan] Scan error: $e');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_scanning = false;
|
||||||
|
_status = 'Scan error - continuing to manual selection';
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
if (mounted) _navigateToRestaurantSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Native Android scanner - fast and accurate
|
||||||
|
Future<void> _scanWithNativeScanner() async {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _status = _scanMessages[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[BeaconScan] Using native Android scanner...');
|
||||||
|
final detectedBeacons = await BeaconChannel.startScan(
|
||||||
|
regions: _uuidToBeaconId.keys.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (detectedBeacons.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_scanning = false;
|
||||||
|
_status = 'No nearby tables detected';
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
if (mounted) _navigateToRestaurantSelect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to known beacons and build scores
|
||||||
|
final beaconScores = <String, BeaconScore>{};
|
||||||
|
for (final beacon in detectedBeacons) {
|
||||||
|
if (_uuidToBeaconId.containsKey(beacon.uuid)) {
|
||||||
|
_beaconRssiSamples[beacon.uuid] = [beacon.rssi];
|
||||||
|
_beaconDetectionCount[beacon.uuid] = beacon.samples;
|
||||||
|
|
||||||
|
beaconScores[beacon.uuid] = BeaconScore(
|
||||||
|
uuid: beacon.uuid,
|
||||||
|
beaconId: _uuidToBeaconId[beacon.uuid]!,
|
||||||
|
avgRssi: beacon.rssi.toDouble(),
|
||||||
|
minRssi: beacon.rssi,
|
||||||
|
maxRssi: beacon.rssi,
|
||||||
|
detectionCount: beacon.samples,
|
||||||
|
variance: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _processBeaconScores(beaconScores);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iOS fallback using Flutter plugin
|
||||||
|
Future<void> _scanWithFlutterPlugin() async {
|
||||||
// Initialize beacon monitoring
|
// Initialize beacon monitoring
|
||||||
await flutterBeacon.initializeScanning;
|
await flutterBeacon.initializeScanning;
|
||||||
|
|
||||||
|
|
@ -120,7 +193,6 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
|
|
||||||
// Create regions for all known UUIDs
|
// Create regions for all known UUIDs
|
||||||
final regions = _uuidToBeaconId.keys.map((uuid) {
|
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)}';
|
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);
|
return Region(identifier: uuid, proximityUUID: formattedUUID);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
@ -130,18 +202,16 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform scan cycles
|
// Perform scan cycles (fewer cycles needed)
|
||||||
for (int scanCycle = 1; scanCycle <= 3; scanCycle++) {
|
for (int scanCycle = 1; scanCycle <= 2; scanCycle++) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _status = _scanMessages[scanCycle - 1]);
|
setState(() => _status = _scanMessages[scanCycle - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<RangingResult>? subscription;
|
StreamSubscription<RangingResult>? subscription;
|
||||||
|
|
||||||
subscription = flutterBeacon.ranging(regions).listen((result) {
|
subscription = flutterBeacon.ranging(regions).listen((result) {
|
||||||
for (var beacon in result.beacons) {
|
for (var beacon in result.beacons) {
|
||||||
final rawUUID = beacon.proximityUUID;
|
final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', '');
|
||||||
final uuid = rawUUID.toUpperCase().replaceAll('-', '');
|
|
||||||
final rssi = beacon.rssi;
|
final rssi = beacon.rssi;
|
||||||
|
|
||||||
if (_uuidToBeaconId.containsKey(uuid)) {
|
if (_uuidToBeaconId.containsKey(uuid)) {
|
||||||
|
|
@ -154,16 +224,15 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
await Future.delayed(const Duration(milliseconds: 2000));
|
await Future.delayed(const Duration(milliseconds: 2000));
|
||||||
await subscription.cancel();
|
await subscription.cancel();
|
||||||
|
|
||||||
if (scanCycle < 3) {
|
if (scanCycle < 2) {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// Analyze results and select best beacon
|
// Analyze results
|
||||||
final beaconScores = <String, BeaconScore>{};
|
final beaconScores = <String, BeaconScore>{};
|
||||||
|
|
||||||
for (final uuid in _beaconRssiSamples.keys) {
|
for (final uuid in _beaconRssiSamples.keys) {
|
||||||
final samples = _beaconRssiSamples[uuid]!;
|
final samples = _beaconRssiSamples[uuid]!;
|
||||||
final detections = _beaconDetectionCount[uuid]!;
|
final detections = _beaconDetectionCount[uuid]!;
|
||||||
|
|
@ -183,6 +252,13 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _processBeaconScores(beaconScores);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process beacon scores and navigate
|
||||||
|
Future<void> _processBeaconScores(Map<String, BeaconScore> beaconScores) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (beaconScores.isEmpty) {
|
if (beaconScores.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_scanning = false;
|
_scanning = false;
|
||||||
|
|
@ -191,7 +267,8 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
if (mounted) _navigateToRestaurantSelect();
|
if (mounted) _navigateToRestaurantSelect();
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
final best = _findBestBeacon(beaconScores);
|
final best = _findBestBeacon(beaconScores);
|
||||||
if (best != null) {
|
if (best != null) {
|
||||||
setState(() => _status = 'Beacon detected! Loading business...');
|
setState(() => _status = 'Beacon detected! Loading business...');
|
||||||
|
|
@ -205,17 +282,6 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
if (mounted) _navigateToRestaurantSelect();
|
if (mounted) _navigateToRestaurantSelect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_scanning = false;
|
|
||||||
_status = 'Scan error - continuing to manual selection';
|
|
||||||
});
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
if (mounted) _navigateToRestaurantSelect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _autoSelectBusinessFromBeacon(int beaconId) async {
|
Future<void> _autoSelectBusinessFromBeacon(int beaconId) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "dart:math";
|
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:provider/provider.dart";
|
import "package:provider/provider.dart";
|
||||||
|
|
||||||
|
|
@ -10,7 +9,6 @@ import "../services/auth_storage.dart";
|
||||||
import "../services/beacon_cache.dart";
|
import "../services/beacon_cache.dart";
|
||||||
import "../services/beacon_channel.dart";
|
import "../services/beacon_channel.dart";
|
||||||
import "../services/beacon_permissions.dart";
|
import "../services/beacon_permissions.dart";
|
||||||
import "../services/preload_cache.dart";
|
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
@ -19,142 +17,28 @@ class SplashScreen extends StatefulWidget {
|
||||||
State<SplashScreen> createState() => _SplashScreenState();
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMixin {
|
class _SplashScreenState extends State<SplashScreen> {
|
||||||
// Bouncing logo animation
|
|
||||||
late AnimationController _bounceController;
|
|
||||||
double _x = 100;
|
|
||||||
double _y = 100;
|
|
||||||
double _dx = 2.5;
|
|
||||||
double _dy = 2.0;
|
|
||||||
Color _logoColor = Colors.white;
|
|
||||||
final Random _random = Random();
|
|
||||||
|
|
||||||
// Rotating status text
|
|
||||||
Timer? _statusTimer;
|
|
||||||
int _statusIndex = 0;
|
|
||||||
static const List<String> _statusPhrases = [
|
|
||||||
"scanning...",
|
|
||||||
"listening...",
|
|
||||||
"searching...",
|
|
||||||
"locating...",
|
|
||||||
"analyzing...",
|
|
||||||
"connecting...",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Beacon scanning state
|
// Beacon scanning state
|
||||||
bool _scanComplete = false;
|
|
||||||
BeaconLookupResult? _bestBeacon;
|
BeaconLookupResult? _bestBeacon;
|
||||||
|
|
||||||
static const List<Color> _colors = [
|
|
||||||
Colors.white,
|
|
||||||
Colors.red,
|
|
||||||
Colors.green,
|
|
||||||
Colors.blue,
|
|
||||||
Colors.yellow,
|
|
||||||
Colors.purple,
|
|
||||||
Colors.cyan,
|
|
||||||
Colors.orange,
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
print('[Splash] Starting with native beacon scanner');
|
debugPrint('[Splash] Starting...');
|
||||||
|
|
||||||
// Start bouncing animation
|
|
||||||
_bounceController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 16), // ~60fps
|
|
||||||
)..addListener(_updatePosition)..repeat();
|
|
||||||
|
|
||||||
// Start rotating status text (randomized)
|
|
||||||
_statusTimer = Timer.periodic(const Duration(milliseconds: 1600), (_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
int newIndex;
|
|
||||||
do {
|
|
||||||
newIndex = _random.nextInt(_statusPhrases.length);
|
|
||||||
} while (newIndex == _statusIndex && _statusPhrases.length > 1);
|
|
||||||
_statusIndex = newIndex;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the initialization flow
|
|
||||||
_initializeApp();
|
_initializeApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updatePosition() {
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
const logoWidth = 180.0;
|
|
||||||
const logoHeight = 60.0;
|
|
||||||
|
|
||||||
// Skip if screen size not yet available
|
|
||||||
if (size.width <= logoWidth || size.height <= logoHeight) return;
|
|
||||||
|
|
||||||
final maxX = size.width - logoWidth;
|
|
||||||
final maxY = size.height - logoHeight;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_x += _dx;
|
|
||||||
_y += _dy;
|
|
||||||
|
|
||||||
// Bounce off edges and change color
|
|
||||||
if (_x <= 0 || _x >= maxX) {
|
|
||||||
_dx = -_dx;
|
|
||||||
_changeColor();
|
|
||||||
}
|
|
||||||
if (_y <= 0 || _y >= maxY) {
|
|
||||||
_dy = -_dy;
|
|
||||||
_changeColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep in bounds
|
|
||||||
_x = _x.clamp(0.0, maxX);
|
|
||||||
_y = _y.clamp(0.0, maxY);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _changeColor() {
|
|
||||||
final newColor = _colors[_random.nextInt(_colors.length)];
|
|
||||||
if (newColor != _logoColor) {
|
|
||||||
_logoColor = newColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initializeApp() async {
|
Future<void> _initializeApp() async {
|
||||||
// Run auth check and preloading in parallel for faster startup
|
debugPrint('[Splash] Initializing...');
|
||||||
print('[Splash] Starting parallel initialization...');
|
|
||||||
|
|
||||||
// Start preloading data in background (fire and forget for non-critical data)
|
// Run auth check and beacon prep in parallel
|
||||||
PreloadCache.preloadAll();
|
final authFuture = _checkAuth();
|
||||||
|
final beaconPrepFuture = _prepareBeaconScan();
|
||||||
|
|
||||||
// Check for saved auth credentials
|
// Wait for both to complete
|
||||||
print('[Splash] Checking for saved auth credentials...');
|
await Future.wait([authFuture, beaconPrepFuture]);
|
||||||
final credentials = await AuthStorage.loadAuth();
|
|
||||||
if (credentials != null && mounted) {
|
|
||||||
print('[Splash] Found saved credentials: UserID=${credentials.userId}, token=${credentials.token.substring(0, 8)}...');
|
|
||||||
Api.setAuthToken(credentials.token);
|
|
||||||
|
|
||||||
// Validate token is still valid by calling profile endpoint
|
// Now do the beacon scan (needs permissions from prep)
|
||||||
print('[Splash] Validating token with server...');
|
|
||||||
final isValid = await _validateToken();
|
|
||||||
if (isValid && mounted) {
|
|
||||||
print('[Splash] Token is valid');
|
|
||||||
final appState = context.read<AppState>();
|
|
||||||
appState.setUserId(credentials.userId);
|
|
||||||
} else {
|
|
||||||
print('[Splash] Token is invalid or expired, clearing saved auth');
|
|
||||||
await AuthStorage.clearAuth();
|
|
||||||
Api.clearAuthToken();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print('[Splash] No saved credentials found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start beacon scanning
|
|
||||||
await _performBeaconScan();
|
await _performBeaconScan();
|
||||||
|
|
||||||
// Navigate based on results
|
// Navigate based on results
|
||||||
|
|
@ -162,130 +46,100 @@ class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMix
|
||||||
_navigateToNextScreen();
|
_navigateToNextScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates the stored token by making a profile API call
|
Future<void> _checkAuth() async {
|
||||||
|
final credentials = await AuthStorage.loadAuth();
|
||||||
|
if (credentials != null && mounted) {
|
||||||
|
debugPrint('[Splash] Found saved credentials');
|
||||||
|
Api.setAuthToken(credentials.token);
|
||||||
|
|
||||||
|
// Validate token in background - don't block startup
|
||||||
|
_validateToken().then((isValid) {
|
||||||
|
if (isValid && mounted) {
|
||||||
|
final appState = context.read<AppState>();
|
||||||
|
appState.setUserId(credentials.userId);
|
||||||
|
} else {
|
||||||
|
AuthStorage.clearAuth();
|
||||||
|
Api.clearAuthToken();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _prepareBeaconScan() async {
|
||||||
|
// Request permissions (this is the slow part)
|
||||||
|
await BeaconPermissions.requestPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _validateToken() async {
|
Future<bool> _validateToken() async {
|
||||||
try {
|
try {
|
||||||
final profile = await Api.getProfile();
|
final profile = await Api.getProfile();
|
||||||
return profile.userId > 0;
|
return profile.userId > 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[Splash] Token validation failed: $e');
|
debugPrint('[Splash] Token validation failed: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _performBeaconScan() async {
|
Future<void> _performBeaconScan() async {
|
||||||
print('[Splash] Starting beacon scan...');
|
// Check permissions (already requested in parallel)
|
||||||
|
final hasPerms = await BeaconPermissions.checkPermissions();
|
||||||
// Request permissions if needed
|
if (!hasPerms) {
|
||||||
final granted = await BeaconPermissions.requestPermissions();
|
debugPrint('[Splash] Permissions not granted');
|
||||||
if (!granted) {
|
|
||||||
print('[Splash] Permissions denied');
|
|
||||||
_scanComplete = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Bluetooth is enabled
|
// Check Bluetooth
|
||||||
print('[Splash] Checking Bluetooth state...');
|
final bluetoothOn = await BeaconPermissions.isBluetoothEnabled();
|
||||||
final bluetoothOn = await BeaconPermissions.ensureBluetoothEnabled();
|
|
||||||
if (!bluetoothOn) {
|
if (!bluetoothOn) {
|
||||||
print('[Splash] Bluetooth is OFF - cannot scan for beacons');
|
debugPrint('[Splash] Bluetooth is OFF');
|
||||||
_scanComplete = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
print('[Splash] Bluetooth is ON');
|
|
||||||
|
|
||||||
// Load known beacons from cache or server (for UUID filtering)
|
// Load known beacons from cache or server
|
||||||
print('[Splash] Loading beacon list...');
|
|
||||||
Map<String, int> knownBeacons = {};
|
Map<String, int> knownBeacons = {};
|
||||||
|
|
||||||
// Try cache first
|
|
||||||
final cached = await BeaconCache.load();
|
final cached = await BeaconCache.load();
|
||||||
if (cached != null && cached.isNotEmpty) {
|
if (cached != null && cached.isNotEmpty) {
|
||||||
print('[Splash] Got ${cached.length} beacon UUIDs from cache');
|
|
||||||
knownBeacons = cached;
|
knownBeacons = cached;
|
||||||
// Refresh cache in background (fire and forget)
|
// Refresh cache in background
|
||||||
Api.listAllBeacons().then((fresh) {
|
Api.listAllBeacons().then((fresh) => BeaconCache.save(fresh));
|
||||||
BeaconCache.save(fresh);
|
|
||||||
print('[Splash] Background refresh: saved ${fresh.length} beacons to cache');
|
|
||||||
}).catchError((e) {
|
|
||||||
print('[Splash] Background refresh failed: $e');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// No cache - must fetch from server
|
|
||||||
try {
|
try {
|
||||||
knownBeacons = await Api.listAllBeacons();
|
knownBeacons = await Api.listAllBeacons();
|
||||||
print('[Splash] Got ${knownBeacons.length} beacon UUIDs from server');
|
|
||||||
// Save to cache
|
|
||||||
await BeaconCache.save(knownBeacons);
|
await BeaconCache.save(knownBeacons);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[Splash] Failed to fetch beacons: $e');
|
debugPrint('[Splash] Failed to fetch beacons: $e');
|
||||||
_scanComplete = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (knownBeacons.isEmpty) {
|
if (knownBeacons.isEmpty) return;
|
||||||
print('[Splash] No beacons configured');
|
|
||||||
_scanComplete = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use native beacon scanner
|
// Scan for beacons
|
||||||
try {
|
debugPrint('[Splash] Scanning...');
|
||||||
print('[Splash] Starting native beacon scan...');
|
|
||||||
|
|
||||||
// Scan using native channel
|
|
||||||
final detectedBeacons = await BeaconChannel.startScan(
|
final detectedBeacons = await BeaconChannel.startScan(
|
||||||
regions: knownBeacons.keys.toList(),
|
regions: knownBeacons.keys.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (detectedBeacons.isEmpty) {
|
if (detectedBeacons.isEmpty) {
|
||||||
print('[Splash] No beacons detected');
|
debugPrint('[Splash] No beacons detected');
|
||||||
_scanComplete = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[Splash] Detected ${detectedBeacons.length} beacons');
|
debugPrint('[Splash] Found ${detectedBeacons.length} beacons');
|
||||||
|
|
||||||
// Filter to only known beacons with good RSSI
|
// Filter to known beacons
|
||||||
final validBeacons = detectedBeacons
|
final validBeacons = detectedBeacons
|
||||||
.where((b) => knownBeacons.containsKey(b.uuid) && b.rssi >= -85)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (validBeacons.isEmpty) {
|
|
||||||
print('[Splash] No valid beacons (known + RSSI >= -85)');
|
|
||||||
|
|
||||||
// Fall back to strongest detected beacon that's known
|
|
||||||
final knownDetected = detectedBeacons
|
|
||||||
.where((b) => knownBeacons.containsKey(b.uuid))
|
.where((b) => knownBeacons.containsKey(b.uuid))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (knownDetected.isNotEmpty) {
|
if (validBeacons.isEmpty) return;
|
||||||
validBeacons.add(knownDetected.first);
|
|
||||||
print('[Splash] Using fallback: ${knownDetected.first.uuid} RSSI=${knownDetected.first.rssi}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validBeacons.isEmpty) {
|
// Look up business info
|
||||||
print('[Splash] No known beacons found');
|
|
||||||
_scanComplete = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up business info for detected beacons
|
|
||||||
final uuids = validBeacons.map((b) => b.uuid).toList();
|
final uuids = validBeacons.map((b) => b.uuid).toList();
|
||||||
print('[Splash] Looking up ${uuids.length} beacons...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final lookupResults = await Api.lookupBeacons(uuids);
|
final lookupResults = await Api.lookupBeacons(uuids);
|
||||||
print('[Splash] Server returned ${lookupResults.length} registered beacons');
|
|
||||||
|
|
||||||
// Find the best beacon (strongest RSSI that's registered)
|
|
||||||
if (lookupResults.isNotEmpty) {
|
if (lookupResults.isNotEmpty) {
|
||||||
// Build a map of UUID -> RSSI from detected beacons
|
|
||||||
final rssiMap = {for (var b in validBeacons) b.uuid: b.rssi};
|
final rssiMap = {for (var b in validBeacons) b.uuid: b.rssi};
|
||||||
|
|
||||||
// Find the best registered beacon based on RSSI
|
|
||||||
BeaconLookupResult? best;
|
BeaconLookupResult? best;
|
||||||
int bestRssi = -999;
|
int bestRssi = -999;
|
||||||
|
|
||||||
|
|
@ -298,16 +152,11 @@ class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMix
|
||||||
}
|
}
|
||||||
|
|
||||||
_bestBeacon = best;
|
_bestBeacon = best;
|
||||||
print('[Splash] Best beacon: ${_bestBeacon?.beaconName ?? "none"} (RSSI=$bestRssi)');
|
debugPrint('[Splash] Best: ${_bestBeacon?.beaconName} (RSSI=$bestRssi)');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[Splash] Error looking up beacons: $e');
|
debugPrint('[Splash] Lookup error: $e');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
print('[Splash] Scan error: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
_scanComplete = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _navigateToNextScreen() async {
|
Future<void> _navigateToNextScreen() async {
|
||||||
|
|
@ -374,61 +223,19 @@ class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMix
|
||||||
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
|
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_bounceController.dispose();
|
|
||||||
_statusTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return const Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Stack(
|
body: Center(
|
||||||
children: [
|
child: SizedBox(
|
||||||
// Centered static status text
|
width: 24,
|
||||||
Center(
|
height: 24,
|
||||||
child: Column(
|
child: CircularProgressIndicator(
|
||||||
mainAxisSize: MainAxisSize.min,
|
strokeWidth: 2,
|
||||||
children: [
|
color: Colors.white54,
|
||||||
const Text(
|
|
||||||
"site survey",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
_statusPhrases[_statusIndex],
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Bouncing logo
|
|
||||||
Positioned(
|
|
||||||
left: _x,
|
|
||||||
top: _y,
|
|
||||||
child: Text(
|
|
||||||
"PAYFRIT",
|
|
||||||
style: TextStyle(
|
|
||||||
color: _logoColor,
|
|
||||||
fontSize: 38,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
letterSpacing: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,27 @@ import 'beacon_channel.dart';
|
||||||
class BeaconPermissions {
|
class BeaconPermissions {
|
||||||
static Future<bool> requestPermissions() async {
|
static Future<bool> requestPermissions() async {
|
||||||
try {
|
try {
|
||||||
// Request location permission (required for Bluetooth scanning)
|
// On Android, check native first (fast path)
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final hasPerms = await BeaconChannel.hasPermissions();
|
||||||
|
if (hasPerms) {
|
||||||
|
debugPrint('[BeaconPermissions] Native check: granted');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
debugPrint('[BeaconPermissions] Native check: not granted, requesting...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request via Flutter plugin (slow but shows system dialogs)
|
||||||
final locationStatus = await Permission.locationWhenInUse.request();
|
final locationStatus = await Permission.locationWhenInUse.request();
|
||||||
debugPrint('[BeaconPermissions] Location: $locationStatus');
|
debugPrint('[BeaconPermissions] Location: $locationStatus');
|
||||||
|
|
||||||
bool bluetoothGranted = true;
|
bool bluetoothGranted = true;
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
// iOS uses a single Bluetooth permission
|
|
||||||
final bluetoothStatus = await Permission.bluetooth.request();
|
final bluetoothStatus = await Permission.bluetooth.request();
|
||||||
debugPrint('[BeaconPermissions] Bluetooth (iOS): $bluetoothStatus');
|
debugPrint('[BeaconPermissions] Bluetooth (iOS): $bluetoothStatus');
|
||||||
bluetoothGranted = bluetoothStatus.isGranted;
|
bluetoothGranted = bluetoothStatus.isGranted;
|
||||||
} else {
|
} else {
|
||||||
// Android 12+ requires separate scan/connect permissions
|
|
||||||
final bluetoothScan = await Permission.bluetoothScan.request();
|
final bluetoothScan = await Permission.bluetoothScan.request();
|
||||||
final bluetoothConnect = await Permission.bluetoothConnect.request();
|
final bluetoothConnect = await Permission.bluetoothConnect.request();
|
||||||
debugPrint('[BeaconPermissions] BluetoothScan: $bluetoothScan, BluetoothConnect: $bluetoothConnect');
|
debugPrint('[BeaconPermissions] BluetoothScan: $bluetoothScan, BluetoothConnect: $bluetoothConnect');
|
||||||
|
|
@ -28,16 +36,10 @@ class BeaconPermissions {
|
||||||
}
|
}
|
||||||
|
|
||||||
final allGranted = locationStatus.isGranted && bluetoothGranted;
|
final allGranted = locationStatus.isGranted && bluetoothGranted;
|
||||||
|
debugPrint('[BeaconPermissions] All granted: $allGranted');
|
||||||
if (allGranted) {
|
|
||||||
debugPrint('[BeaconPermissions] All permissions granted');
|
|
||||||
} else {
|
|
||||||
debugPrint('[BeaconPermissions] Permissions denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
return allGranted;
|
return allGranted;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[BeaconPermissions] Error requesting permissions: $e');
|
debugPrint('[BeaconPermissions] Error: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,19 +139,15 @@ class BeaconPermissions {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> checkPermissions() async {
|
static Future<bool> checkPermissions() async {
|
||||||
final locationStatus = await Permission.locationWhenInUse.status;
|
// On Android, use native check (much faster)
|
||||||
|
if (Platform.isAndroid) {
|
||||||
bool bluetoothGranted = true;
|
return await BeaconChannel.hasPermissions();
|
||||||
if (Platform.isIOS) {
|
|
||||||
final bluetoothStatus = await Permission.bluetooth.status;
|
|
||||||
bluetoothGranted = bluetoothStatus.isGranted;
|
|
||||||
} else {
|
|
||||||
final bluetoothScan = await Permission.bluetoothScan.status;
|
|
||||||
final bluetoothConnect = await Permission.bluetoothConnect.status;
|
|
||||||
bluetoothGranted = bluetoothScan.isGranted && bluetoothConnect.isGranted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return locationStatus.isGranted && bluetoothGranted;
|
// iOS: use Flutter plugin
|
||||||
|
final locationStatus = await Permission.locationWhenInUse.status;
|
||||||
|
final bluetoothStatus = await Permission.bluetooth.status;
|
||||||
|
return locationStatus.isGranted && bluetoothStatus.isGranted;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> openSettings() async {
|
static Future<void> openSettings() async {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart';
|
import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart';
|
||||||
|
|
||||||
import 'api.dart';
|
import 'api.dart';
|
||||||
|
import 'beacon_channel.dart';
|
||||||
import 'beacon_permissions.dart';
|
import 'beacon_permissions.dart';
|
||||||
|
|
||||||
/// Result of a beacon scan
|
/// Result of a beacon scan
|
||||||
|
|
@ -88,64 +90,62 @@ class BeaconScannerService {
|
||||||
return const BeaconScanResult(error: "No beacons configured");
|
return const BeaconScanResult(error: "No beacons configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize scanning
|
// Use native scanner on Android, Flutter plugin on iOS
|
||||||
await flutterBeacon.initializeScanning;
|
List<DetectedBeacon> detectedBeacons = [];
|
||||||
|
|
||||||
// Create regions for all known UUIDs
|
if (Platform.isAndroid) {
|
||||||
final regions = knownBeacons.keys.map((uuid) {
|
debugPrint('[BeaconScanner] Using native Android scanner...');
|
||||||
final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}';
|
detectedBeacons = await BeaconChannel.startScan(
|
||||||
return Region(identifier: uuid, proximityUUID: formattedUUID);
|
regions: knownBeacons.keys.toList(),
|
||||||
}).toList();
|
);
|
||||||
|
} else {
|
||||||
// Collect RSSI samples
|
// iOS: use Flutter plugin
|
||||||
final Map<String, List<int>> rssiSamples = {};
|
debugPrint('[BeaconScanner] Using Flutter plugin for iOS...');
|
||||||
final Map<String, int> detectionCounts = {};
|
detectedBeacons = await _scanWithFlutterPlugin(knownBeacons);
|
||||||
|
|
||||||
debugPrint('[BeaconScanner] Starting 2-second scan...');
|
|
||||||
StreamSubscription<RangingResult>? 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));
|
if (detectedBeacons.isEmpty) {
|
||||||
await subscription.cancel();
|
|
||||||
|
|
||||||
if (rssiSamples.isEmpty) {
|
|
||||||
debugPrint('[BeaconScanner] No beacons detected');
|
debugPrint('[BeaconScanner] No beacons detected');
|
||||||
return const BeaconScanResult(beaconsFound: 0);
|
return const BeaconScanResult(beaconsFound: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup found beacons
|
debugPrint('[BeaconScanner] Detected ${detectedBeacons.length} beacons');
|
||||||
debugPrint('[BeaconScanner] Looking up ${rssiSamples.length} beacons...');
|
|
||||||
final uuids = rssiSamples.keys.toList();
|
|
||||||
List<BeaconLookupResult> lookupResults = [];
|
|
||||||
|
|
||||||
|
// Filter to only known beacons
|
||||||
|
final validBeacons = detectedBeacons
|
||||||
|
.where((b) => knownBeacons.containsKey(b.uuid))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (validBeacons.isEmpty) {
|
||||||
|
debugPrint('[BeaconScanner] No known beacons found');
|
||||||
|
return BeaconScanResult(beaconsFound: detectedBeacons.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup found beacons
|
||||||
|
final uuids = validBeacons.map((b) => b.uuid).toList();
|
||||||
|
debugPrint('[BeaconScanner] Looking up ${uuids.length} beacons...');
|
||||||
|
|
||||||
|
List<BeaconLookupResult> lookupResults = [];
|
||||||
try {
|
try {
|
||||||
lookupResults = await Api.lookupBeacons(uuids);
|
lookupResults = await Api.lookupBeacons(uuids);
|
||||||
debugPrint('[BeaconScanner] Server returned ${lookupResults.length} registered beacons');
|
debugPrint('[BeaconScanner] Server returned ${lookupResults.length} registered beacons');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[BeaconScanner] Lookup error: $e');
|
debugPrint('[BeaconScanner] Lookup error: $e');
|
||||||
return BeaconScanResult(
|
return BeaconScanResult(
|
||||||
beaconsFound: rssiSamples.length,
|
beaconsFound: validBeacons.length,
|
||||||
error: "Failed to lookup beacons",
|
error: "Failed to lookup beacons",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the best registered beacon
|
// Find the best registered beacon based on RSSI
|
||||||
final bestBeacon = _findBestBeacon(lookupResults, rssiSamples, detectionCounts);
|
final rssiMap = {for (var b in validBeacons) b.uuid: b.rssi};
|
||||||
|
final bestBeacon = _findBestBeacon(lookupResults, rssiMap);
|
||||||
|
|
||||||
debugPrint('[BeaconScanner] Best beacon: ${bestBeacon?.beaconName ?? "none"}');
|
debugPrint('[BeaconScanner] Best beacon: ${bestBeacon?.beaconName ?? "none"}');
|
||||||
|
|
||||||
return BeaconScanResult(
|
return BeaconScanResult(
|
||||||
bestBeacon: bestBeacon,
|
bestBeacon: bestBeacon,
|
||||||
beaconsFound: rssiSamples.length,
|
beaconsFound: validBeacons.length,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[BeaconScanner] Scan error: $e');
|
debugPrint('[BeaconScanner] Scan error: $e');
|
||||||
|
|
@ -156,27 +156,59 @@ class BeaconScannerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// iOS fallback: scan with Flutter plugin
|
||||||
|
Future<List<DetectedBeacon>> _scanWithFlutterPlugin(Map<String, int> knownBeacons) async {
|
||||||
|
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<String, List<int>> rssiSamples = {};
|
||||||
|
|
||||||
|
debugPrint('[BeaconScanner] iOS: Starting 2-second scan...');
|
||||||
|
StreamSubscription<RangingResult>? 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 2000));
|
||||||
|
await subscription.cancel();
|
||||||
|
|
||||||
|
// Convert to DetectedBeacon format
|
||||||
|
return rssiSamples.entries.map((entry) {
|
||||||
|
final samples = entry.value;
|
||||||
|
final avgRssi = samples.reduce((a, b) => a + b) ~/ samples.length;
|
||||||
|
return DetectedBeacon(
|
||||||
|
uuid: entry.key,
|
||||||
|
rssi: avgRssi,
|
||||||
|
samples: samples.length,
|
||||||
|
);
|
||||||
|
}).toList()..sort((a, b) => b.rssi.compareTo(a.rssi));
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the best registered beacon based on RSSI
|
/// Find the best registered beacon based on RSSI
|
||||||
BeaconLookupResult? _findBestBeacon(
|
BeaconLookupResult? _findBestBeacon(
|
||||||
List<BeaconLookupResult> registeredBeacons,
|
List<BeaconLookupResult> registeredBeacons,
|
||||||
Map<String, List<int>> rssiSamples,
|
Map<String, int> rssiMap,
|
||||||
Map<String, int> detectionCounts,
|
|
||||||
) {
|
) {
|
||||||
if (registeredBeacons.isEmpty) return null;
|
if (registeredBeacons.isEmpty) return null;
|
||||||
|
|
||||||
BeaconLookupResult? best;
|
BeaconLookupResult? best;
|
||||||
double bestAvgRssi = -999;
|
int bestRssi = -999;
|
||||||
|
|
||||||
|
// First pass: find beacon with RSSI >= -85
|
||||||
for (final beacon in registeredBeacons) {
|
for (final beacon in registeredBeacons) {
|
||||||
final samples = rssiSamples[beacon.uuid];
|
final rssi = rssiMap[beacon.uuid] ?? -100;
|
||||||
if (samples == null || samples.isEmpty) continue;
|
if (rssi > bestRssi && rssi >= -85) {
|
||||||
|
bestRssi = rssi;
|
||||||
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;
|
best = beacon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,12 +216,9 @@ class BeaconScannerService {
|
||||||
// Fall back to strongest if none meet threshold
|
// Fall back to strongest if none meet threshold
|
||||||
if (best == null) {
|
if (best == null) {
|
||||||
for (final beacon in registeredBeacons) {
|
for (final beacon in registeredBeacons) {
|
||||||
final samples = rssiSamples[beacon.uuid];
|
final rssi = rssiMap[beacon.uuid] ?? -100;
|
||||||
if (samples == null || samples.isEmpty) continue;
|
if (rssi > bestRssi) {
|
||||||
|
bestRssi = rssi;
|
||||||
final avgRssi = samples.reduce((a, b) => a + b) / samples.length;
|
|
||||||
if (avgRssi > bestAvgRssi) {
|
|
||||||
bestAvgRssi = avgRssi;
|
|
||||||
best = beacon;
|
best = beacon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue