Universal Beacon SDK
SDK genérico y robusto para detección de beacons BLE con detección ultra-rápida (100-300ms) y optimizaciones para sistemas empresariales. Ideal para control de acceso, asistencia, marketing de proximidad, IoT, tracking de assets y aplicaciones de salud.
⚡ Destacados
- Detección Ultra-Rápida: De 100ms a 5s según tu caso de uso
- 4 Configuraciones Optimizadas: ultraFast, balanced, enterprise, batterySaver
- Escalabilidad: Maneja 100+ beacons con cache inteligente
- Configuración Total: Control completo de todos los parámetros
- Producción Lista: Usado en sistemas empresariales reales
🚀 Características Principales
- ⚡ Detección Optimizada: 100-300ms en modo ultraFast, configurable hasta 5s
- 🎯 4 Presets Listos: ultraFast, accessControl, enterprise, batterySaver
- 🏢 Escalable: Cache de parsing, priorización, throttling para 100+ beacons
- 🔧 100% Configurable: Control total de scanMode, intervalos, filtros, optimizaciones
- 📡 Detección Universal: iBeacons, Eddystone, formatos personalizados
- 🔋 Monitoreo de Batería: Lectura automática para beacons HOLYIOT (Normal e iBeacon)
- 🎨 Gestión de Registros: Sistema completo para beacons conocidos
- 📊 Eventos en Tiempo Real: Streams reactivos para entrada/salida de rango
- 📏 Cálculo de Distancia: Algoritmos precisos basados en RSSI
- 🔍 Logging Avanzado: Sistema configurable para debugging
- 💾 Persistencia: Almacenamiento local con SharedPreferences
- 📱 Multiplataforma: Android e iOS
- ⚡ Eficiencia Energética: Modo batterySaver con consumo mínimo
📦 Instalación
Añade el SDK a tu pubspec.yaml:
dependencies:
universal_beacon_sdk: ^1.0.0
Luego ejecuta:
flutter pub get
⚡ Inicio Rápido
import 'package:universal_beacon_sdk/universal_beacon_sdk.dart';
// 1. Crear instancia del scanner
final scanner = UniversalBeaconScanner();
// 2. Inicializar con configuración ultra-rápida
await scanner.initialize(
config: BeaconConfig.ultraFast(), // 100-300ms detección
registeredBeacons: [
BeaconRegistry(
id: 'beacon_001',
uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
displayName: 'Puerta Principal',
),
],
);
// 3. Escuchar eventos
scanner.beaconEvents.listen((event) {
if (event.type == BeaconEventType.detected) {
print('¡Beacon detectado en ${event.timestamp}ms!');
print('RSSI: ${event.device.rssi} dBm');
print('Distancia: ${event.device.distance}m');
}
});
// 4. Iniciar escaneo
await scanner.startScanning();
🎯 Configuraciones Optimizadas
⚡ Ultra Rápido (100-300ms)
Ideal para control de acceso, puertas, torniquetes
await scanner.initialize(
config: BeaconConfig.ultraFast(),
);
⚖️ Balanceado (500ms)
Para asistencia, uso general
await scanner.initialize(
config: BeaconConfig.accessControl(),
);
🏢 Empresarial (100+ beacons)
Hospitales, almacenes, campus grandes
await scanner.initialize(
config: BeaconConfig.enterprise(),
);
� Ahorro de Batería
Apps de larga duración, tracking pasivo
await scanner.initialize(
config: BeaconConfig.batterySaver(),
);
🔧 Configuración Personalizada
Control total de todos los parámetros
await scanner.initialize(
config: BeaconConfig(
// Velocidad BLE
scanMode: ScanMode.lowLatency, // opportunistic, lowPower, balanced, lowLatency
// Intervalos (cualquier valor en ms)
scanInterval: 250, // ← Puedes poner 100, 250, 500, 1000, etc.
minUpdateInterval: 50, // ← Control por dispositivo
streamThrottle: 200, // ← Actualización de UI
// Filtros
minRssi: -85, // De -100 a 0
maxDistance: 15.0, // En metros
deviceLostTimeout: 10, // En segundos
// Optimizaciones
prioritizeRegisteredBeacons: true, // Beacons conocidos primero
immediateEventEmission: true, // Eventos instantáneos
enableParsingCache: true, // Cache para reducir CPU 70%
iBeaconOnly: false, // Solo iBeacons o todos
// Debug
logLevel: 2, // 0=errors, 1=warnings, 2=info, 3=verbose
),
);
📊 Tabla Comparativa de Presets
Elige el preset correcto según tus necesidades:
| Preset | Detección | Batería | Beacons | Uso Ideal | Trade-offs |
|---|---|---|---|---|---|
| ultraFast ⚡ | 100-300ms | Alta 🔴 | <50 | Control de acceso, puertas, torniquetes donde el usuario espera respuesta inmediata | Consume más batería, no recomendado para apps que corren todo el día |
| accessControl ⚖️ | 500ms | Media 🟡 | <100 | Asistencia escolar/laboral, check-in/check-out, monitoreo de presencia | Balance óptimo para mayoría de casos |
| enterprise 🏢 | 1-1.5s | Media 🟡 | 100+ | Hospitales, almacenes, campus con muchos beacons simultáneos | Detección más lenta pero maneja gran escala |
| batterySaver 🔋 | 3-7s | Mínima 🟢 | Cualquiera | Tracking de assets a largo plazo, monitoreo pasivo, apps que corren días | Detección muy lenta, no para interacción |
🎯 ¿Cuál Preset Usar?
Usa ultraFast si:
- ✅ Necesitas respuesta inmediata (<500ms)
- ✅ Usuario interactúa activamente (ej: abrir puerta)
- ✅ La app solo escanea cuando se necesita
- ❌ NO si la app corre todo el día
Usa accessControl si:
- ✅ Necesitas buen balance velocidad/batería
- ✅ Detección de 500ms es aceptable
- ✅ Caso de uso típico (asistencia, check-in)
- ✅ No estás seguro cuál elegir → Empieza aquí
Usa enterprise si:
- ✅ Tienes 50+ beacons en el área
- ✅ Rendimiento es crítico con muchos dispositivos
- ✅ Necesitas cache para reducir CPU
- ❌ NO si tienes pocos beacons (usa accessControl)
Usa batterySaver si:
- ✅ La app debe correr días sin recargar
- ✅ Detección no es time-critical
- ✅ Solo necesitas ubicación periódica
- ❌ NO si necesitas interacción en tiempo real
🎯 Casos de Uso
Control de Acceso
import 'package:universal_beacon_sdk/universal_beacon_sdk.dart';
final scanner = UniversalBeaconScanner();
// Configuración ultra-rápida para puertas
await scanner.initialize(
config: BeaconConfig.ultraFast(), // 100-300ms
registeredBeacons: [
BeaconRegistry(
id: 'employee_001',
uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
displayName: 'Juan Pérez',
metadata: {'department': 'IT', 'level': 'admin'},
),
],
);
// Escuchar eventos de acceso
scanner.beaconEvents.listen((event) {
if (event.type == BeaconEventType.detected) {
print('✅ Acceso autorizado: ${event.device.displayName}');
openDoor(); // Puerta abre en ~200ms
}
});
await scanner.startScanning();
Sistema de Asistencia
// Configuración balanceada
await scanner.initialize(
config: BeaconConfig.accessControl(),
registeredBeacons: students,
);
scanner.beaconEvents.listen((event) {
if (event.type == BeaconEventType.detected) {
registerAttendance(event.device);
}
if (event.type == BeaconEventType.lost) {
markExit(event.device);
}
});
// Marcar asistencia
}
});
Marketing de Proximidad
// Configuración para marketing
await scanner.initialize(
config: ScannerConfig.proximityMarketing,
);
// Registrar productos
await scanner.registerBeacon(BeaconRegistry(
id: 'product_001',
uuid: 'F7826DA6-4FA2-4E98-8024-BC5B71E0893E',
displayName: 'Promoción Especial',
category: 'marketing',
metadata: {'campaign': 'summer_sale', 'discount': '20%'},
registeredAt: DateTime.now(),
));
// Activar ofertas por proximidad
scanner.events.listen((event) {
if (event.type == BeaconEventType.registeredEntered) {
final discount = event.registry?.metadata['discount'];
print('¡Oferta especial: $discount de descuento!');
// Mostrar oferta
}
});
🔧 API Principal
Inicialización
final scanner = UniversalBeaconScanner();
// Inicialización básica
await scanner.initialize();
// Inicialización con configuración
await scanner.initialize(
config: BeaconConfig.balanced(),
registeredBeacons: [/* lista de beacons */],
);
// Configuraciones predefinidas
await scanner.initialize(config: ScannerConfig.accessControl);
await scanner.initialize(config: ScannerConfig.attendance);
await scanner.initialize(config: ScannerConfig.proximityMarketing);
Configuración Personalizada
final customConfig = BeaconConfig(
scanInterval: 1000,
minRssi: -80,
maxDistance: 25.0,
deviceLostTimeout: 10,
iBeaconOnly: false,
enableLogging: true,
logLevel: 2,
allowedUuids: ['FDA50693-A4E2-4FB1-AFCF-C6EB07647825'],
);
await scanner.initialize(config: customConfig);
🔋 Monitoreo de Batería
El SDK incluye detección automática de nivel de batería para beacons HOLYIOT NRF52810. Funciona en ambos modos (Normal e iBeacon) sin configuración adicional.
Modos Soportados
1. Modo Normal/Generic BLE
Beacons transmiten serviceData con UUID personalizado 0x5242:
Datos: 41 63 ce 07 42 82 e8 59 03 02 06 00 00
↑ ↑
| byte[1] = 0x63 = 99% batería
2. Modo iBeacon
Beacons transmiten manufacturerData con datos extendidos:
Datos: 06 00 01 09 20 ... [byte 25] [byte 26] [byte 27]
↑ ↑ ↑
Posiciones de batería (prioridad: 25→26→27)
Propiedades Disponibles
scanner.detectedDevices.listen((devices) {
for (var device in devices) {
// Verificar si tiene información de batería
if (device.hasBatteryInfo) {
// Nivel de batería (0-100%)
print('Batería: ${device.batteryLevel}%');
// Icono visual
print('Estado: ${device.batteryIcon}'); // 🔋 o 🪫
// Color para UI ('green', 'orange', 'red')
print('Color: ${device.batteryColor}');
// Timestamp de última lectura
print('Leído: ${device.batteryLastRead}');
// Alertas automáticas
if (device.isBatteryCritical) {
print('⚠️ CRÍTICO: < 10% - Reemplazar inmediatamente');
} else if (device.isBatteryLow) {
print('⚡ BAJO: < 20% - Programar reemplazo');
}
}
}
});
Ejemplo Completo de Monitoreo
// Sistema de alertas de batería
void monitorBatteryStatus() {
scanner.detectedDevices.listen((devices) {
for (var device in devices.where((d) => d.hasBatteryInfo)) {
final batteryInfo = {
'name': device.displayName,
'level': device.batteryLevel,
'icon': device.batteryIcon,
'timestamp': device.batteryLastRead,
};
// Log de estado
print('${batteryInfo['icon']} ${batteryInfo['name']}: ${batteryInfo['level']}%');
// Alertas críticas
if (device.isBatteryCritical) {
sendCriticalAlert(device);
scheduleImmediateReplacement(device);
}
// Alertas preventivas
else if (device.isBatteryLow) {
sendLowBatteryWarning(device);
addToMaintenanceQueue(device);
}
}
});
}
UI de Batería
// Widget de batería con colores
Widget buildBatteryIndicator(BeaconDevice device) {
if (!device.hasBatteryInfo) return SizedBox.shrink();
Color getBatteryColor() {
switch (device.batteryColor) {
case 'green': return Colors.green;
case 'orange': return Colors.orange;
case 'red': return Colors.red;
default: return Colors.grey;
}
}
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: getBatteryColor().withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: getBatteryColor(), width: 1.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(device.batteryIcon, style: TextStyle(fontSize: 12)),
SizedBox(width: 4),
Text(
'${device.batteryLevel}%',
style: TextStyle(
color: getBatteryColor(),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
);
}
Compatibilidad
- ✅ HOLYIOT NRF52810: Ambos modos (Normal e iBeacon)
- ✅ Battery Service Estándar: UUID 0x180F (BLE estándar)
- ✅ Backward Compatible: Beacons sin batería siguen funcionando
- ✅ Automático: Sin configuración manual requerida
- ✅ Performance: Parsing optimizado, sin impacto en velocidad
Gestión de Beacons
// Registrar beacon
final beacon = BeaconRegistry(
id: 'unique_id',
uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
displayName: 'Mi Beacon',
description: 'Descripción del beacon',
category: 'general',
metadata: {'custom': 'data'},
registeredAt: DateTime.now(),
);
await scanner.registerBeacon(beacon);
// Obtener beacons registrados
final registeredBeacons = scanner.registeredBeacons;
// Buscar beacons
final searchResults = scanner.registryManager.searchBeacons('Holy');
Escaneo y Eventos
// Iniciar escaneo
await scanner.startScanning();
// Escuchar todos los eventos
scanner.events.listen((event) {
switch (event.type) {
case BeaconEventType.detected:
print('Nuevo beacon: ${event.device.name}');
break;
case BeaconEventType.registeredEntered:
print('Beacon registrado entró: ${event.device.name}');
break;
case BeaconEventType.lost:
print('Beacon perdido: ${event.device.name}');
break;
}
});
// Escuchar lista de dispositivos
scanner.devices.listen((devices) {
print('Dispositivos detectados: ${devices.length}');
for (final device in devices) {
print('- ${device.name}: ${device.estimatedDistance?.toStringAsFixed(1)}m');
}
});
// Escuchar estado del scanner
scanner.status.listen((status) {
print('Estado: ${status.displayName}');
});
// Detener escaneo
await scanner.stopScanning();
📡 Los 3 Streams Principales
El SDK expone 3 streams que cubren todos los casos de uso:
1️⃣ beaconEvents - Eventos Individuales en Tiempo Real
Cuándo usarlo: Necesitas reaccionar inmediatamente a cada beacon (ej: abrir puerta)
scanner.beaconEvents.listen((event) {
// Cada evento contiene:
// - event.type: BeaconEventType (detected, updated, lost)
// - event.device: BeaconDevice con toda la info
// - event.timestamp: DateTime del evento
switch (event.type) {
case BeaconEventType.detected:
// Primera vez que se detecta el beacon
print('🆕 Nuevo: ${event.device.displayName}');
openDoor(); // Acción inmediata
break;
case BeaconEventType.updated:
// Beacon ya conocido, datos actualizados (RSSI, distancia)
print('🔄 Actualizado: ${event.device.rssi} dBm');
break;
case BeaconEventType.lost:
// Beacon salió del rango o se perdió señal
print('❌ Perdido: ${event.device.displayName}');
closeDoor();
break;
}
});
✅ Casos de uso:
- Control de acceso (abrir puerta cuando se detecta)
- Notificaciones push individuales
- Triggers de acciones específicas
- Logging detallado de eventos
❌ NO usar para:
- Actualizar UI de lista (usa
detectedDevices) - Verificar estado general (usa
status)
2️⃣ detectedDevices - Lista Completa de Beacons Activos
Cuándo usarlo: Necesitas mostrar lista en UI o procesar múltiples beacons
scanner.detectedDevices.listen((devices) {
// devices es List<BeaconDevice>
// Se actualiza según streamThrottle (default: 500ms)
setState(() {
_beaconsList = devices;
_totalBeacons = devices.length;
_registeredCount = devices.where((d) => d.isRegistered).length;
});
// Filtrar por distancia
final nearbyBeacons = devices.where((d) =>
d.distance != null && d.distance! < 5.0
).toList();
// Filtrar por RSSI
final strongSignal = devices.where((d) => d.rssi > -70).toList();
// Ordenar por distancia
devices.sort((a, b) =>
(a.distance ?? 999).compareTo(b.distance ?? 999)
);
});
✅ Casos de uso:
- ListView/GridView de beacons en UI
- Dashboard con estadísticas
- Mapa de beacons cercanos
- Filtrado y ordenamiento de múltiples beacons
❌ NO usar para:
- Reaccionar a beacon específico (usa
beaconEvents) - Solo verificar si está escaneando (usa
status)
3️⃣ status - Estado del Scanner
Cuándo usarlo: Necesitas mostrar estado de conexión/escaneo en UI
scanner.status.listen((status) {
// status es ScannerStatus enum
switch (status) {
case ScannerStatus.idle:
// Scanner inicializado pero no escaneando
print('⚪ Listo para escanear');
_showStartButton();
break;
case ScannerStatus.scanning:
// Escaneando activamente
print('🔵 Escaneando...');
_showStopButton();
_showLoadingIndicator();
break;
case ScannerStatus.stopped:
// Escaneo detenido manualmente
print('🔴 Detenido');
break;
case ScannerStatus.error:
// Error (permisos, bluetooth off, etc)
print('❌ Error');
_showPermissionDialog();
break;
}
});
✅ Casos de uso:
- Mostrar botón Start/Stop
- Indicador de loading
- Mensaje de error de permisos/bluetooth
- Color de estado en UI
❌ NO usar para:
- Información de beacons (usa otros streams)
📋 Ejemplo Completo con los 3 Streams
class BeaconScreen extends StatefulWidget {
@override
_BeaconScreenState createState() => _BeaconScreenState();
}
class _BeaconScreenState extends State<BeaconScreen> {
final scanner = UniversalBeaconScanner();
List<BeaconDevice> _devices = [];
ScannerStatus _status = ScannerStatus.idle;
String? _lastEvent;
@override
void initState() {
super.initState();
_initScanner();
}
Future<void> _initScanner() async {
await scanner.initialize(
config: BeaconConfig.accessControl(),
registeredBeacons: [
BeaconRegistry(
id: 'door_1',
uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
displayName: 'Puerta Principal',
),
],
);
// Stream 1: Eventos individuales para acciones inmediatas
scanner.beaconEvents.listen((event) {
if (event.type == BeaconEventType.detected && event.device.isRegistered) {
_openDoor(event.device);
}
setState(() {
_lastEvent = '${event.type.name}: ${event.device.displayName}';
});
});
// Stream 2: Lista completa para UI
scanner.detectedDevices.listen((devices) {
setState(() {
_devices = devices;
});
});
// Stream 3: Estado para botones e indicadores
scanner.status.listen((status) {
setState(() {
_status = status;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Beacon Scanner'),
actions: [
// Indicador de estado (Stream 3)
Icon(
_status == ScannerStatus.scanning
? Icons.bluetooth_searching
: Icons.bluetooth,
color: _status == ScannerStatus.error ? Colors.red : Colors.green,
),
],
),
body: Column(
children: [
// Último evento (Stream 1)
if (_lastEvent != null)
Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Text(_lastEvent!),
),
),
// Lista de beacons (Stream 2)
Expanded(
child: ListView.builder(
itemCount: _devices.length,
itemBuilder: (context, index) {
final device = _devices[index];
return ListTile(
leading: Icon(
device.isRegistered ? Icons.verified : Icons.bluetooth,
color: device.isRegistered ? Colors.green : Colors.grey,
),
title: Text(device.displayName ?? 'Beacon'),
subtitle: Text('${device.distance?.toStringAsFixed(1)}m'),
trailing: Text('${device.rssi} dBm'),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
// Botón basado en estado (Stream 3)
onPressed: _status == ScannerStatus.scanning
? scanner.stopScanning
: scanner.startScanning,
child: Icon(
_status == ScannerStatus.scanning ? Icons.stop : Icons.play_arrow,
),
),
);
}
void _openDoor(BeaconDevice device) {
print('✅ Abriendo puerta para: ${device.displayName}');
// Tu lógica aquí
}
@override
void dispose() {
scanner.dispose();
super.dispose();
}
}
🎯 BeaconRegistry: ¿Cuándo Usar y Cuándo NO?
✅ USA BeaconRegistry CUANDO:
1. Necesitas identificar beacons específicos
// ✅ Control de acceso - sabes exactamente qué badges esperar
registeredBeacons: [
BeaconRegistry(id: 'employee_123', uuid: '...', displayName: 'Juan'),
BeaconRegistry(id: 'employee_456', uuid: '...', displayName: 'María'),
]
2. Quieres metadata adicional
// ✅ Guardar info del empleado con el beacon
BeaconRegistry(
id: 'emp_001',
uuid: 'FDA50693-...',
metadata: {
'cargo': 'Desarrollador',
'departamento': 'IT',
'nivel_acceso': 'admin',
},
)
3. Necesitas filtrar eventos de beacons conocidos
// ✅ Solo reaccionar a beacons registrados
scanner.beaconEvents.listen((event) {
if (event.device.isRegistered) {
openDoor(); // Solo para empleados registrados
}
});
4. Quieres priorizar detección de beacons importantes
// ✅ Con prioritizeRegisteredBeacons: true
// Los beacons registrados se detectan más rápido
config: BeaconConfig(
prioritizeRegisteredBeacons: true, // Empleados conocidos primero
)
❌ NO USES BeaconRegistry CUANDO:
1. Solo quieres descubrir beacons disponibles
// ❌ NO necesitas registrar nada
// ✅ Simplemente escanea y ve qué hay
await scanner.initialize(config: BeaconConfig.accessControl());
scanner.detectedDevices.listen((devices) {
print('Encontré ${devices.length} beacons');
});
2. No sabes qué beacons esperar
// ❌ Mala idea: registrar todos los UUIDs posibles
// ✅ Mejor: escanea y filtra después por proximidad/RSSI
scanner.detectedDevices.listen((devices) {
final nearby = devices.where((d) => d.distance! < 5).toList();
});
3. Solo necesitas métricas generales
// ❌ No necesitas identificación individual
// ✅ Solo cuenta cuántos beacons hay cerca
final count = devices.length;
final avgDistance = devices.map((d) => d.distance).average;
4. Quieres máxima flexibilidad
// ❌ Registrar beacons limita detección si usas filtros
// ✅ Sin registrar, detectas TODOS los beacons
config: BeaconConfig(
iBeaconOnly: false, // Detecta todos los formatos
allowedUuids: null, // No filtrar por UUID
)
🎯 Regla General:
Usa BeaconRegistry si: Tienes una lista conocida de beacons Y necesitas tratarlos de forma especial (metadata, priorización, filtrado)
No uses BeaconRegistry si: Solo quieres descubrir/monitorear beacons genéricamente
📈 Logging y Debugging
final logger = BeaconLogger();
// Configurar nivel de logging
logger.setLevel(3); // 0: Error, 1: Warning, 2: Info, 3: Debug
// Obtener historial de logs
final history = logger.getHistory(filterLevel: LogLevel.error);
// Exportar logs
final exportedLogs = logger.exportLogs(since: DateTime.now().subtract(Duration(hours: 1)));
// Estadísticas
final stats = logger.getStatistics();
🎛️ Configuraciones Avanzadas
Por Plataforma
final config = BeaconConfig(
scanInterval: 1000,
platformConfig: PlatformSpecificConfig(
android: AndroidConfig(
scanMode: 2, // Balanced
useLowPowerMode: false,
),
ios: IOSConfig(
allowBackgroundScanning: true,
restoreState: true,
),
),
);
Configuración Específica por Beacon
final beacon = BeaconRegistry(
id: 'special_beacon',
uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
displayName: 'Beacon Especial',
config: BeaconSpecificConfig(
notificationCooldown: 5,
maxDistance: 10.0,
minRssi: -70,
trackHistory: true,
customConfig: {'special': true},
),
registeredAt: DateTime.now(),
);
💾 Persistencia y Exportación
// Los registros se guardan automáticamente
// Exportar registros
final exported = scanner.registryManager.exportToJson();
// Importar registros
await scanner.registryManager.importFromJson(exported);
// Estadísticas
final stats = scanner.registryManager.getStatistics();
🧪 Configuraciones de Testing
// Configuración para desarrollo
await scanner.initialize(config: ScannerConfig.debug);
// Configuración para demo
await scanner.initialize(config: ScannerConfig.demo);
📱 Casos de Uso Específicos
IoT y Automatización
scanner.events.listen((event) {
if (event.device.category == 'sensor' &&
event.type == BeaconEventType.registeredEntered) {
// Activar automatización
triggerIoTAction(event.device.metadata);
}
});
Aplicaciones de Salud
await scanner.initialize(config: ScannerConfig.healthcare);
scanner.events.listen((event) {
if (event.device.category == 'medical_device') {
// Procesar datos médicos
processMedicalData(event.device);
}
});
🔧 Personalización Completa
// Configuración empresarial personalizada
final enterpriseConfig = BeaconConfig(
scanInterval: 500,
minRssi: -75,
maxDistance: 50.0,
allowedUuids: [
'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
'E2C56DB5-DFFB-48D2-B060-D0F5A7100000',
],
enablePersistence: true,
logLevel: 2,
);
await scanner.initialize(config: enterpriseConfig);
🆘 Manejo de Errores
try {
await scanner.startScanning();
} catch (e) {
if (e.toString().contains('permissions')) {
// Manejar error de permisos
} else if (e.toString().contains('bluetooth')) {
// Manejar error de Bluetooth
}
}
// Escuchar errores del scanner
scanner.status.listen((status) {
if (status.isError) {
handleScannerError(status);
}
});
✅ Best Practices
🚫 Errores Comunes y Cómo Evitarlos
1. Memory Leaks - Olvidar dispose()
❌ MAL:
class MyWidget extends StatefulWidget {
final scanner = UniversalBeaconScanner();
@override
Widget build(BuildContext context) {
scanner.startScanning(); // ¡Nunca se detiene!
return Container();
}
}
✅ BIEN:
class MyWidget extends StatefulWidget {
final scanner = UniversalBeaconScanner();
@override
void initState() {
super.initState();
_initScanner();
}
@override
void dispose() {
scanner.dispose(); // ← CRÍTICO: Libera recursos
super.dispose();
}
}
2. Permisos - No verificar antes de escanear
❌ MAL:
await scanner.startScanning(); // ¡Puede fallar sin permisos!
✅ BIEN:
// Opción 1: El SDK maneja permisos automáticamente
await scanner.initialize(config: config);
await scanner.startScanning(); // SDK verifica permisos internamente
// Opción 2: Verificación manual explícita
final permissionManager = PermissionManager();
if (!await permissionManager.hasRequiredPermissions()) {
final granted = await permissionManager.checkAndRequestAllPermissions();
if (!granted) {
showPermissionDialog();
return;
}
}
await scanner.startScanning();
3. Configuración Incorrecta para Caso de Uso
❌ MAL:
// Usando batterySaver para control de acceso
config: BeaconConfig.batterySaver() // ¡Detección cada 5s es muy lento!
✅ BIEN:
// Usar el preset correcto según caso de uso
config: BeaconConfig.ultraFast() // Para control de acceso inmediato
4. No Escuchar el Stream Correcto
❌ MAL:
// Usando beaconEvents para actualizar ListView (demasiados updates)
scanner.beaconEvents.listen((event) {
setState(() {
_devices = [...]; // ¡Se actualiza 10+ veces por segundo!
});
});
✅ BIEN:
// Usar detectedDevices para UI (throttled)
scanner.detectedDevices.listen((devices) {
setState(() {
_devices = devices; // Se actualiza cada 500ms (configurable)
});
});
5. Registrar Beacons Innecesariamente
❌ MAL:
// Registrar todos los beacons del mundo "por si acaso"
registeredBeacons: [
BeaconRegistry(id: 'beacon_1', uuid: '...'),
BeaconRegistry(id: 'beacon_2', uuid: '...'),
// ... 500 beacons más
] // ¡Impacto en rendimiento!
✅ BIEN:
// Solo registrar beacons que realmente necesitas identificar
registeredBeacons: [
BeaconRegistry(id: 'main_door', uuid: '...'),
BeaconRegistry(id: 'back_door', uuid: '...'),
] // Mantén la lista pequeña y relevante
6. No Manejar Estado de Bluetooth
❌ MAL:
await scanner.startScanning(); // ¿Qué pasa si BT está off?
✅ BIEN:
scanner.status.listen((status) {
switch (status) {
case ScannerStatus.error:
showBluetoothDialog(); // Pedir al usuario activar BT
break;
case ScannerStatus.scanning:
hideLoadingIndicator();
break;
}
});
7. Configuración Demasiado Agresiva
❌ MAL:
BeaconConfig(
scanInterval: 50, // ¡Muy rápido!
minUpdateInterval: 10, // ¡Demasiado frecuente!
scanMode: ScanMode.lowLatency,
// Batería se agota en 2 horas
)
✅ BIEN:
// Empieza con preset y ajusta solo si es necesario
BeaconConfig.accessControl() // 500ms es suficiente para mayoría de casos
💡 Recomendaciones Generales
✅ DO (Hacer)
- Siempre llama
dispose()cuando el widget se destruye - Usa el preset correcto para tu caso de uso antes de personalizar
- Escucha
statuspara mostrar estado de BT/permisos en UI - Prueba en dispositivo real - el emulador no tiene Bluetooth
- Usa
detectedDevicespara UI,beaconEventspara acciones - Mantén pequeña la lista de
registeredBeacons(<50) - Verifica permisos antes de funcionalidad crítica
- Maneja el caso de Bluetooth apagado
❌ DON'T (No Hacer)
- No olvides
dispose()- causa memory leaks - No uses batterySaver para apps interactivas
- No registres beacons si no necesitas identificarlos
- No uses beaconEvents para actualizar listas en UI
- No asumas permisos están concedidos sin verificar
- No uses ultraFast si la app corre todo el día
- No ignores el estado del scanner en UI
- No escanees continuamente si no es necesario
🎯 Checklist de Implementación
Antes de ir a producción, verifica:
✅dispose()llamado en todos los lugares donde se crea scanner✅ Permisos verificados/solicitados antes de escanear✅ Preset correcto elegido según caso de uso✅ Streams correctos usados (events vs devices vs status)✅ Manejo de Bluetooth apagado implementado✅ Manejo de permisos denegados implementado✅ UI muestra estado del scanner claramente✅ Probado en dispositivo real Android/iOS✅ Probado con Bluetooth apagado/encendido✅ Probado sin permisos concedidos✅ No hay memory leaks (verificado con DevTools)✅ Batería consumida es aceptable para tu caso de uso
📱 Testing en Dispositivos Reales
// Ejemplo de test completo
void testBeaconScanner() async {
final scanner = UniversalBeaconScanner();
print('✓ Test 1: Inicialización');
await scanner.initialize(config: BeaconConfig.accessControl());
print('✓ Test 2: Verificar permisos');
assert(await PermissionManager().hasRequiredPermissions());
print('✓ Test 3: Iniciar escaneo');
await scanner.startScanning();
print('✓ Test 4: Detectar beacons (esperar 10s)');
await Future.delayed(Duration(seconds: 10));
final devices = scanner.currentDevices;
print(' Detectados: ${devices.length} beacons');
print('✓ Test 5: Detener escaneo');
await scanner.stopScanning();
print('✓ Test 6: Cleanup');
await scanner.dispose();
print('✅ Todos los tests pasaron');
}
📚 Recursos Adicionales
📄 Licencia
MIT License - Ver LICENSE para más detalles.
Libraries
- universal_beacon_sdk
- Universal Beacon SDK