universal_beacon_sdk 1.0.2
universal_beacon_sdk: ^1.0.2 copied to clipboard
SDK genérico y robusto para detección de beacons BLE. Ideal para control de acceso, sistemas de asistencia, marketing de proximidad, automatización IoT, tracking de assets y aplicaciones empresariales.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:universal_beacon_sdk/universal_beacon_sdk.dart';
void main() {
runApp(const BeaconSDKExampleApp());
}
class BeaconSDKExampleApp extends StatelessWidget {
const BeaconSDKExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Universal Beacon SDK Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const BeaconScannerPage(),
);
}
}
class BeaconScannerPage extends StatefulWidget {
const BeaconScannerPage({super.key});
@override
State<BeaconScannerPage> createState() => _BeaconScannerPageState();
}
class _BeaconScannerPageState extends State<BeaconScannerPage> {
final UniversalBeaconScanner _scanner = UniversalBeaconScanner();
List<BeaconDevice> _detectedDevices = [];
ScannerStatus _status = ScannerStatus.idle;
String _statusMessage = '';
@override
void initState() {
super.initState();
_initializeSDK();
}
Future<void> _initializeSDK() async {
// Registrar algunos beacons de ejemplo
final exampleBeacons = [
BeaconRegistry(
id: 'holy_shun',
uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',
displayName: 'Holy-Shun',
description: 'Dispositivo Holy Shun',
category: 'holy_device',
metadata: {'type': 'shun', 'version': '1.0'},
registeredAt: DateTime.now(),
),
BeaconRegistry(
id: 'holy_jin',
uuid: 'E2C56DB5-DFFB-48D2-B060-D0F5A7100000',
displayName: 'Holy-Jin',
description: 'Dispositivo Holy Jin',
category: 'holy_device',
metadata: {'type': 'jin', 'version': '1.0'},
registeredAt: DateTime.now(),
),
];
// Inicializar con configuración balanceada
final success = await _scanner.initialize(
config: ScannerConfig.balanced,
registeredBeacons: exampleBeacons,
);
if (success) {
// Escuchar eventos
_scanner.events.listen(_onBeaconEvent);
// Escuchar dispositivos detectados
_scanner.devices.listen((devices) {
setState(() {
_detectedDevices = devices;
});
});
// Escuchar estado del scanner
_scanner.status.listen((status) {
setState(() {
_status = status;
_statusMessage = _getStatusMessage(status);
});
});
setState(() {
_statusMessage = 'SDK inicializado correctamente';
});
} else {
setState(() {
_statusMessage = 'Error inicializando SDK';
});
}
}
void _onBeaconEvent(BeaconEvent event) {
final message = _getEventMessage(event);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: _getEventColor(event.type),
duration: const Duration(seconds: 2),
),
);
}
String _getEventMessage(BeaconEvent event) {
switch (event.type) {
case BeaconEventType.detected:
return '📱 Nuevo beacon: ${event.device.name}';
case BeaconEventType.registeredEntered:
return '✅ ${event.device.name} entró en rango';
case BeaconEventType.registeredExited:
return '❌ ${event.device.name} salió de rango';
case BeaconEventType.lost:
return '👻 Beacon perdido: ${event.device.name}';
default:
return '🔄 ${event.device.name} actualizado';
}
}
Color _getEventColor(BeaconEventType type) {
switch (type) {
case BeaconEventType.detected:
case BeaconEventType.registeredEntered:
return Colors.green;
case BeaconEventType.lost:
case BeaconEventType.registeredExited:
return Colors.red;
default:
return Colors.blue;
}
}
String _getStatusMessage(ScannerStatus status) {
switch (status) {
case ScannerStatus.idle:
return 'Inactivo';
case ScannerStatus.initializing:
return 'Inicializando...';
case ScannerStatus.ready:
return 'Listo para escanear';
case ScannerStatus.scanning:
return 'Escaneando beacons...';
case ScannerStatus.stopped:
return 'Escaneo detenido';
case ScannerStatus.bluetoothOff:
return 'Bluetooth desactivado';
case ScannerStatus.permissionDenied:
return 'Permisos denegados';
case ScannerStatus.error:
return 'Error en el scanner';
}
}
Future<void> _toggleScanning() async {
if (_scanner.isScanning) {
await _scanner.stopScanning();
} else {
await _scanner.startScanning();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Universal Beacon SDK'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Estado del scanner
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Icon(
_getStatusIcon(_status),
color: _getStatusColor(_status),
),
const SizedBox(width: 8),
Text(
'Estado: $_statusMessage',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _status.canStartScanning
? _toggleScanning
: null,
icon: Icon(_scanner.isScanning
? Icons.stop
: Icons.play_arrow),
label: Text(
_scanner.isScanning ? 'Detener' : 'Iniciar'),
),
),
const SizedBox(width: 16),
Text(
'${_detectedDevices.length} beacons',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Lista de dispositivos detectados
Expanded(
child: _detectedDevices.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bluetooth_searching,
size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No hay beacons detectados',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Inicia el escaneo para detectar beacons cercanos',
style: TextStyle(color: Colors.grey),
),
],
),
)
: ListView.builder(
itemCount: _detectedDevices.length,
itemBuilder: (context, index) {
final device = _detectedDevices[index];
return BeaconCard(device: device);
},
),
),
],
),
),
);
}
IconData _getStatusIcon(ScannerStatus status) {
switch (status) {
case ScannerStatus.scanning:
return Icons.bluetooth_searching;
case ScannerStatus.ready:
return Icons.bluetooth;
case ScannerStatus.error:
case ScannerStatus.bluetoothOff:
case ScannerStatus.permissionDenied:
return Icons.error;
default:
return Icons.bluetooth_disabled;
}
}
Color _getStatusColor(ScannerStatus status) {
switch (status) {
case ScannerStatus.scanning:
return Colors.blue;
case ScannerStatus.ready:
return Colors.green;
case ScannerStatus.error:
case ScannerStatus.bluetoothOff:
case ScannerStatus.permissionDenied:
return Colors.red;
default:
return Colors.grey;
}
}
@override
void dispose() {
_scanner.dispose();
super.dispose();
}
}
class BeaconCard extends StatelessWidget {
final BeaconDevice device;
const BeaconCard({super.key, required this.device});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Icono del tipo de beacon
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: device.isRegistered ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
device.isRegistered ? Icons.verified : Icons.bluetooth,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
// Información principal
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
device.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
if (device.category != null)
Text(
device.category!,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
// RSSI y distancia
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${device.rssi} dBm',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getRssiColor(device.rssi),
),
),
if (device.estimatedDistance != null)
Text(
'${device.estimatedDistance!.toStringAsFixed(1)}m',
style: const TextStyle(fontSize: 12),
),
],
),
],
),
if (device.uuid != null) ...[
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.fingerprint, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
device.uuid!,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
),
),
),
],
),
],
if (device.major != null && device.minor != null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.tag, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Text(
'Major: ${device.major}, Minor: ${device.minor}',
style: const TextStyle(fontSize: 12),
),
],
),
],
// Proximidad
const SizedBox(height: 8),
Row(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getProximityColor(device.proximityLevel),
borderRadius: BorderRadius.circular(12),
),
child: Text(
device.proximityLevel.displayName,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
const Spacer(),
Text(
'Visto: ${_formatLastSeen(device.lastSeen)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
],
),
),
);
}
Color _getRssiColor(int rssi) {
if (rssi > -50) return Colors.green;
if (rssi > -70) return Colors.orange;
return Colors.red;
}
Color _getProximityColor(ProximityLevel level) {
switch (level) {
case ProximityLevel.immediate:
return Colors.green;
case ProximityLevel.near:
return Colors.orange;
case ProximityLevel.far:
return Colors.red;
case ProximityLevel.unknown:
return Colors.grey;
}
}
String _formatLastSeen(DateTime lastSeen) {
final diff = DateTime.now().difference(lastSeen);
if (diff.inSeconds < 60) {
return 'ahora';
} else if (diff.inMinutes < 60) {
return '${diff.inMinutes}m';
} else {
return '${diff.inHours}h';
}
}
}