adonna 0.1.2
adonna: ^0.1.2 copied to clipboard
A Flutter plugin that wraps the Feasycom FeasyBeacon SDK for Android and iOS, providing cross-platform BLE beacon functionality with background monitoring capabilities.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:adonna/adonna.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:io';
import 'dart:convert';
import 'dart:typed_data';
void main() {
runApp(const AdonnaExampleApp());
}
class AdonnaExampleApp extends StatelessWidget {
const AdonnaExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Adonna Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const AdonnaHomePage(),
);
}
}
class AdonnaHomePage extends StatefulWidget {
const AdonnaHomePage({super.key});
@override
State<AdonnaHomePage> createState() => _AdonnaHomePageState();
}
class _AdonnaHomePageState extends State<AdonnaHomePage> with TickerProviderStateMixin {
late TabController _tabController;
bool _isScanning = false;
bool _isConnected = false;
bool _isBackgroundMonitoring = false;
String? _connectedAddress;
String? _connectedName;
String _pin = '';
Map<String, dynamic>? _deviceInfo;
// Device lists
final Map<String, Map<String, dynamic>> _scannedDevices = {};
final Map<String, Map<String, dynamic>> _favoriteDevices = {};
// Background monitoring
final TextEditingController _uuidController = TextEditingController();
final TextEditingController _majorController = TextEditingController();
final TextEditingController _thresholdController = TextEditingController();
final List<Map<String, dynamic>> _backgroundEvents = [];
// iOS background monitoring
bool _isIosBackgroundMonitoring = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadFavorites();
_setupStreams();
_ensurePermissions();
// Set default UUID for testing
_uuidController.text = '12345678-1234-1234-1234-123456789012';
_thresholdController.text = '100';
}
@override
void dispose() {
_tabController.dispose();
_uuidController.dispose();
_majorController.dispose();
_thresholdController.dispose();
super.dispose();
}
void _setupStreams() {
// Scan events
FeasyBeacon.scanStream.listen((event) {
if (event is Map<String, dynamic>) {
final address = event['address'] as String?;
if (address != null) {
setState(() {
_scannedDevices[address] = Map<String, dynamic>.from(event);
});
}
}
});
// Background events
FeasyBeacon.backgroundEvents.listen((event) {
if (event is Map<String, dynamic>) {
setState(() {
_backgroundEvents.insert(0, Map<String, dynamic>.from(event));
if (_backgroundEvents.length > 50) {
_backgroundEvents.removeLast();
}
});
}
});
// OTA progress
FeasyBeacon.otaProgressStream.listen((event) {
if (event is Map<String, dynamic>) {
final progress = event['progress'] as int?;
final status = event['status'] as String?;
if (progress != null && status != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('OTA: $status - $progress%')));
}
}
});
// Device info stream
FeasyBeacon.deviceInfoStream.listen((event) {
if (event is Map<String, dynamic>) {
setState(() {
_deviceInfo = Map<String, dynamic>.from(event);
});
}
});
// Packet stream
FeasyBeacon.packetStream.listen((event) {
if (event is Map<String, dynamic>) {
final data = event['data'] as List<int>?;
if (data != null) {
final hexData = data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Packet: $hexData')));
}
}
});
}
Future<void> _ensurePermissions() async {
await Permission.bluetooth.request();
await Permission.location.request();
await Permission.notification.request();
}
Future<void> _loadFavorites() async {
final prefs = await SharedPreferences.getInstance();
final favoritesJson = prefs.getString('favorite_devices');
if (favoritesJson != null) {
final favorites = Map<String, dynamic>.from(jsonDecode(favoritesJson));
setState(() {
_favoriteDevices.clear();
favorites.forEach((key, value) {
_favoriteDevices[key] = Map<String, dynamic>.from(value);
});
});
}
}
Future<void> _saveFavorites() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('favorite_devices', jsonEncode(_favoriteDevices));
}
Future<void> _addToFavorites(String address, Map<String, dynamic> device) async {
setState(() {
_favoriteDevices[address] = Map<String, dynamic>.from(device);
});
await _saveFavorites();
}
Future<void> _removeFromFavorites(String address) async {
setState(() {
_favoriteDevices.remove(address);
});
await _saveFavorites();
}
Future<void> _toggleScan() async {
if (_isScanning) {
await FeasyBeacon.stopScan();
setState(() {
_isScanning = false;
});
} else {
await FeasyBeacon.startScan();
setState(() {
_isScanning = true;
_scannedDevices.clear();
});
}
}
Future<void> _connectToDevice(String address) async {
try {
final success = await FeasyBeacon.connect(address: address);
if (success) {
setState(() {
_isConnected = true;
_connectedAddress = address;
_connectedName = _scannedDevices[address]?['name'] ?? 'Unknown';
});
// Add to favorites automatically
if (_scannedDevices.containsKey(address)) {
await _addToFavorites(address, _scannedDevices[address]!);
}
// Stop scanning when connected
if (_isScanning) {
await _toggleScan();
}
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Connected successfully!')));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Failed to connect')));
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Connection error: $e')));
}
}
Future<void> _disconnect() async {
try {
await FeasyBeacon.disconnect();
setState(() {
_isConnected = false;
_connectedAddress = null;
_connectedName = null;
_deviceInfo = null;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Disconnected')));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Disconnect error: $e')));
}
}
Future<void> _getDeviceInfo() async {
if (!_isConnected) return;
try {
final info = await FeasyBeacon.getDeviceInfo();
setState(() {
_deviceInfo = info;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Device info loaded')));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get device info: $e')));
}
}
Future<void> _toggleBackgroundMonitoring() async {
if (_isBackgroundMonitoring) {
await FeasyBeacon.stopBackgroundMonitoring();
setState(() {
_isBackgroundMonitoring = false;
});
} else {
final success = await FeasyBeacon.startBackgroundMonitoring(
config: {
'uuid': _uuidController.text,
'major': _majorController.text.isNotEmpty ? int.parse(_majorController.text) : null,
'thresholdMs': int.parse(_thresholdController.text),
},
);
if (success) {
setState(() {
_isBackgroundMonitoring = true;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Background monitoring started')));
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Failed to start background monitoring')));
}
}
}
Future<void> _toggleIosBackgroundMonitoring() async {
if (_isIosBackgroundMonitoring) {
await FeasyBeacon.stopBackgroundMonitoring();
setState(() {
_isIosBackgroundMonitoring = false;
});
} else {
final success = await FeasyBeacon.startIosBackgroundMonitoring(
uuid: _uuidController.text,
major: _majorController.text.isNotEmpty ? int.parse(_majorController.text) : null,
thresholdMs: int.parse(_thresholdController.text),
);
if (success) {
setState(() {
_isIosBackgroundMonitoring = true;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('iOS background monitoring started')));
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Failed to start iOS background monitoring')));
}
}
}
Future<void> _triggerOta() async {
if (!_isConnected) return;
try {
// Mock OTA file for testing
final mockFile = File('mock_dfu.bin');
if (!await mockFile.exists()) {
await mockFile.writeAsBytes(List.generate(1024, (i) => i % 256));
}
final fileBytes = await mockFile.readAsBytes();
final dfuInfo = await FeasyBeacon.checkDfuFile(fileBytes);
if (dfuInfo != null) {
final success = await FeasyBeacon.startOtaUpdate(fileBytes);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('OTA update started')));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Failed to start OTA update')));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid DFU file')));
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('OTA error: $e')));
}
}
Widget _buildDeviceList() {
return ListView.builder(
itemCount: _scannedDevices.length,
itemBuilder: (context, index) {
final address = _scannedDevices.keys.elementAt(index);
final device = _scannedDevices[address]!;
final isFavorite = _favoriteDevices.containsKey(address);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: Icon(isFavorite ? Icons.favorite : Icons.bluetooth, color: isFavorite ? Colors.red : Colors.blue),
title: Text(device['name'] ?? 'Unknown Device'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('MAC: $address'),
Text('RSSI: ${device['rssi'] ?? 'N/A'}'),
if (device['beaconTypes'] != null) Text('Types: ${(device['beaconTypes'] as List).join(', ')}'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isConnected && _connectedAddress == address)
const Icon(Icons.link, color: Colors.green)
else if (!_isConnected)
IconButton(
icon: const Icon(Icons.link),
onPressed: () => _connectToDevice(address),
tooltip: 'Connect',
),
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
onPressed: () {
if (isFavorite) {
_removeFromFavorites(address);
} else {
_addToFavorites(address, device);
}
},
tooltip: isFavorite ? 'Remove from favorites' : 'Add to favorites',
),
],
),
isThreeLine: true,
),
);
},
);
}
Widget _buildFavoritesList() {
return ListView.builder(
itemCount: _favoriteDevices.length,
itemBuilder: (context, index) {
final address = _favoriteDevices.keys.elementAt(index);
final device = _favoriteDevices[address]!;
return Dismissible(
key: Key(address),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) => _removeFromFavorites(address),
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: const Icon(Icons.favorite, color: Colors.red),
title: Text(device['name'] ?? 'Unknown Device'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('MAC: $address'),
Text('RSSI: ${device['rssi'] ?? 'N/A'}'),
if (device['beaconTypes'] != null) Text('Types: ${(device['beaconTypes'] as List).join(', ')}'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isConnected && _connectedAddress == address)
const Icon(Icons.link, color: Colors.green)
else if (!_isConnected)
IconButton(
icon: const Icon(Icons.link),
onPressed: () => _connectToDevice(address),
tooltip: 'Connect',
),
],
),
isThreeLine: true,
),
),
);
},
);
}
Widget _buildBackgroundMonitoringTab() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Background Monitoring Configuration',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextField(
controller: _uuidController,
decoration: const InputDecoration(labelText: 'iBeacon UUID', border: OutlineInputBorder()),
),
const SizedBox(height: 8),
TextField(
controller: _majorController,
decoration: const InputDecoration(labelText: 'Major (optional)', border: OutlineInputBorder()),
keyboardType: TextInputType.number,
),
const SizedBox(height: 8),
TextField(
controller: _thresholdController,
decoration: const InputDecoration(labelText: 'Threshold (ms)', border: OutlineInputBorder()),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _toggleBackgroundMonitoring,
style: ElevatedButton.styleFrom(
backgroundColor: _isBackgroundMonitoring ? Colors.red : Colors.green,
),
child: Text(_isBackgroundMonitoring ? 'Stop Android BG' : 'Start Android BG'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _toggleIosBackgroundMonitoring,
style: ElevatedButton.styleFrom(
backgroundColor: _isIosBackgroundMonitoring ? Colors.red : Colors.green,
),
child: Text(_isIosBackgroundMonitoring ? 'Stop iOS BG' : 'Start iOS BG'),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
Expanded(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text('Background Events', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
Expanded(
child: _backgroundEvents.isEmpty
? const Center(child: Text('No background events yet'))
: ListView.builder(
itemCount: _backgroundEvents.length,
itemBuilder: (context, index) {
final event = _backgroundEvents[index];
return ListTile(
leading: const Icon(Icons.notifications),
title: Text('Button Press Detected'),
subtitle: Text(
'UUID: ${event['uuid'] ?? 'N/A'}\n'
'Major: ${event['major'] ?? 'N/A'}\n'
'Interval: ${event['intervalMs'] ?? 'N/A'}ms',
),
isThreeLine: true,
);
},
),
),
],
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Adonna Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.bluetooth_searching), text: 'Devices'),
Tab(icon: Icon(Icons.favorite), text: 'Favorites'),
Tab(icon: Icon(Icons.settings), text: 'Background'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [_buildDeviceList(), _buildFavoritesList(), _buildBackgroundMonitoringTab()],
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: _toggleScan,
tooltip: _isScanning ? 'Stop Scan' : 'Start Scan',
child: Icon(_isScanning ? Icons.stop : Icons.search),
),
const SizedBox(height: 8),
if (_isConnected) ...[
FloatingActionButton(
onPressed: _disconnect,
tooltip: 'Disconnect',
backgroundColor: Colors.red,
child: const Icon(Icons.link_off),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: _getDeviceInfo,
tooltip: 'Get Device Info',
backgroundColor: Colors.orange,
child: const Icon(Icons.info),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: _triggerOta,
tooltip: 'Trigger OTA',
backgroundColor: Colors.purple,
child: const Icon(Icons.system_update),
),
],
],
),
bottomNavigationBar: _isConnected
? Container(
padding: const EdgeInsets.all(16),
color: Colors.green.shade100,
child: Row(
children: [
const Icon(Icons.link, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Connected to: $_connectedName', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('MAC: $_connectedAddress'),
],
),
),
if (_deviceInfo != null)
IconButton(
icon: const Icon(Icons.expand_more),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Device Info'),
content: SingleChildScrollView(
child: Text(const JsonEncoder.withIndent(' ').convert(_deviceInfo)),
),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close')),
],
),
);
},
),
],
),
)
: null,
);
}
}