mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add detailed logging for BLE connection and scanning processes
This commit is contained in:
@@ -40,20 +40,36 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
void _initializeBleService() {
|
void _initializeBleService() {
|
||||||
_bleService.onConnectionStateChanged = (isConnected) {
|
_bleService.onConnectionStateChanged = (isConnected) {
|
||||||
|
print('🔔 [Provider] Connection state callback fired: $isConnected');
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
connectionState: isConnected
|
connectionState: isConnected
|
||||||
? ConnectionState.connected
|
? ConnectionState.connected
|
||||||
: ConnectionState.disconnected,
|
: ConnectionState.disconnected,
|
||||||
lastUpdate: DateTime.now(),
|
lastUpdate: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}');
|
||||||
|
print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
print(' Notified listeners');
|
||||||
};
|
};
|
||||||
|
|
||||||
_bleService.onError = (error) {
|
_bleService.onError = (error) {
|
||||||
|
print('⚠️ [Provider] BLE error received: $error');
|
||||||
|
print(' Current connection state: ${_deviceInfo.connectionState}');
|
||||||
|
|
||||||
_error = error;
|
_error = error;
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
|
||||||
connectionState: ConnectionState.error,
|
// Only set connection state to error if we're not already connected
|
||||||
);
|
// Data parsing errors after connection shouldn't disconnect us
|
||||||
|
if (_deviceInfo.connectionState != ConnectionState.connected) {
|
||||||
|
print(' Setting connection state to error');
|
||||||
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
|
connectionState: ConnectionState.error,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print(' Keeping connection state as connected (ignoring data parsing error)');
|
||||||
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,22 +94,30 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Start scanning for MeshCore devices
|
/// Start scanning for MeshCore devices
|
||||||
Future<void> startScan() async {
|
Future<void> startScan() async {
|
||||||
|
print('🔍 [Provider] startScan() called');
|
||||||
_isScanning = true;
|
_isScanning = true;
|
||||||
_scannedDevices.clear();
|
_scannedDevices.clear();
|
||||||
_error = null;
|
_error = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
print('✅ [Provider] Scan state initialized, notifying listeners');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await for (final device
|
await for (final device
|
||||||
in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) {
|
in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) {
|
||||||
|
print('📱 [Provider] Device received from scan stream');
|
||||||
if (!_scannedDevices.any((d) => d.remoteId == device.remoteId)) {
|
if (!_scannedDevices.any((d) => d.remoteId == device.remoteId)) {
|
||||||
_scannedDevices.add(device);
|
_scannedDevices.add(device);
|
||||||
|
print('✅ [Provider] Added device to list: ${device.platformName}, total: ${_scannedDevices.length}');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
print(' ⏭️ [Provider] Device already in list, skipping');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('❌ [Provider] Scan error: $e');
|
||||||
_error = 'Scan error: $e';
|
_error = 'Scan error: $e';
|
||||||
} finally {
|
} finally {
|
||||||
|
print('🏁 [Provider] Scan completed');
|
||||||
_isScanning = false;
|
_isScanning = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -108,16 +132,24 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Connect to a device
|
/// Connect to a device
|
||||||
Future<bool> connect(BluetoothDevice device) async {
|
Future<bool> connect(BluetoothDevice device) async {
|
||||||
|
print('🔵 [Provider] connect() called for device: ${device.platformName}');
|
||||||
|
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
deviceId: device.remoteId.toString(),
|
deviceId: device.remoteId.toString(),
|
||||||
deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown',
|
deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown',
|
||||||
connectionState: ConnectionState.connecting,
|
connectionState: ConnectionState.connecting,
|
||||||
);
|
);
|
||||||
_error = null;
|
_error = null;
|
||||||
|
print('✅ [Provider] Device info updated to connecting state');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
print('🔵 [Provider] Calling BLE service connect()...');
|
||||||
final success = await _bleService.connect(device);
|
final success = await _bleService.connect(device);
|
||||||
if (!success) {
|
|
||||||
|
if (success) {
|
||||||
|
print('✅ [Provider] BLE service connect() returned success');
|
||||||
|
} else {
|
||||||
|
print('❌ [Provider] BLE service connect() returned failure');
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
connectionState: ConnectionState.error,
|
connectionState: ConnectionState.error,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -186,12 +186,28 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
print('🔵 [UI] User tapped device: ${device.platformName}');
|
||||||
|
|
||||||
|
// Get app provider reference before popping dialog
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
|
||||||
|
print('🔵 [UI] Closing dialog...');
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
await provider.connect(device);
|
|
||||||
if (context.mounted &&
|
print('🔵 [UI] Calling provider.connect()...');
|
||||||
provider.deviceInfo.isConnected) {
|
final success = await provider.connect(device);
|
||||||
final appProvider = context.read<AppProvider>();
|
print(success
|
||||||
|
? '✅ [UI] provider.connect() returned success'
|
||||||
|
: '❌ [UI] provider.connect() returned failure');
|
||||||
|
|
||||||
|
if (success && provider.deviceInfo.isConnected) {
|
||||||
|
print('✅ [UI] Device is connected, initializing app provider...');
|
||||||
await appProvider.initialize();
|
await appProvider.initialize();
|
||||||
|
print('✅ [UI] App provider initialized');
|
||||||
|
} else {
|
||||||
|
print('❌ [UI] Device not connected after connect() call');
|
||||||
|
print(' Connection state: ${provider.deviceInfo.connectionState}');
|
||||||
|
print(' Error: ${provider.error}');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -318,6 +334,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
final deviceInfo = provider.deviceInfo;
|
final deviceInfo = provider.deviceInfo;
|
||||||
final isConnected = deviceInfo.isConnected;
|
final isConnected = deviceInfo.isConnected;
|
||||||
|
|
||||||
|
print('🎨 [UI] Building status bar - isConnected: $isConnected, state: ${deviceInfo.connectionState}');
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool _isLoadingSampleData = false;
|
bool _isLoadingSampleData = false;
|
||||||
double _gpsUpdateDistance = 10.0;
|
double _gpsUpdateDistance = 10.0;
|
||||||
bool _backgroundTrackingEnabled = false;
|
bool _backgroundTrackingEnabled = false;
|
||||||
final BackgroundLocationService _backgroundLocationService = BackgroundLocationService();
|
final BackgroundLocationService _backgroundLocationService =
|
||||||
|
BackgroundLocationService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -55,14 +56,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 10.0;
|
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 10.0;
|
||||||
_backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false;
|
_backgroundTrackingEnabled =
|
||||||
|
prefs.getBool('background_tracking_enabled') ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize background location service
|
// Initialize background location service
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final appProvider = context.read<AppProvider>();
|
final appProvider = context.read<AppProvider>();
|
||||||
_backgroundLocationService.initialize(appProvider.connectionProvider.bleService);
|
_backgroundLocationService.initialize(
|
||||||
|
appProvider.connectionProvider.bleService,
|
||||||
|
);
|
||||||
|
|
||||||
// Restore background tracking state
|
// Restore background tracking state
|
||||||
if (_backgroundTrackingEnabled) {
|
if (_backgroundTrackingEnabled) {
|
||||||
@@ -76,7 +80,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Future<void> _saveLocationSettings() async {
|
Future<void> _saveLocationSettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
|
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
|
||||||
await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled);
|
await prefs.setBool(
|
||||||
|
'background_tracking_enabled',
|
||||||
|
_backgroundTrackingEnabled,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveThemePreference(AppThemeMode theme) async {
|
Future<void> _saveThemePreference(AppThemeMode theme) async {
|
||||||
@@ -138,8 +145,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final allMessages = [...sarMessages, ...channelMessages];
|
final allMessages = [...sarMessages, ...channelMessages];
|
||||||
|
|
||||||
// Add to providers
|
// Add to providers
|
||||||
final contactsProvider = Provider.of<ContactsProvider>(context, listen: false);
|
final contactsProvider = Provider.of<ContactsProvider>(
|
||||||
final messagesProvider = Provider.of<MessagesProvider>(context, listen: false);
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
final messagesProvider = Provider.of<MessagesProvider>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
|
||||||
contactsProvider.addContacts(contacts);
|
contactsProvider.addContacts(contacts);
|
||||||
messagesProvider.addMessages(allMessages);
|
messagesProvider.addMessages(allMessages);
|
||||||
@@ -186,7 +199,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Failed to start background tracking. Check permissions and BLE connection.'),
|
content: Text(
|
||||||
|
'Failed to start background tracking. Check permissions and BLE connection.',
|
||||||
|
),
|
||||||
duration: Duration(seconds: 3),
|
duration: Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -222,8 +237,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
if (confirmed != true || !mounted) return;
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
final contactsProvider = Provider.of<ContactsProvider>(context, listen: false);
|
final contactsProvider = Provider.of<ContactsProvider>(
|
||||||
final messagesProvider = Provider.of<MessagesProvider>(context, listen: false);
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
final messagesProvider = Provider.of<MessagesProvider>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
|
||||||
contactsProvider.clearContacts();
|
contactsProvider.clearContacts();
|
||||||
messagesProvider.clearAll();
|
messagesProvider.clearAll();
|
||||||
@@ -241,9 +262,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Settings')),
|
||||||
title: const Text('Settings'),
|
|
||||||
),
|
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
// General Settings Section
|
// General Settings Section
|
||||||
@@ -327,8 +346,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Load or clear sample contacts, channel messages, and SAR markers for testing',
|
'Load or clear sample contacts, channel messages, and SAR markers for testing',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -377,9 +396,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -416,14 +435,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('1m', style: Theme.of(context).textTheme.bodySmall),
|
||||||
'1m',
|
Text('100m', style: Theme.of(context).textTheme.bodySmall),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'100m',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -443,7 +456,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
// Update background tracking if active
|
// Update background tracking if active
|
||||||
if (_backgroundTrackingEnabled) {
|
if (_backgroundTrackingEnabled) {
|
||||||
_backgroundLocationService.updateDistanceThreshold(tempDistance);
|
_backgroundLocationService.updateDistanceThreshold(
|
||||||
|
tempDistance,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -467,7 +482,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
RadioListTile<AppThemeMode>(
|
RadioListTile<AppThemeMode>(
|
||||||
title: const Text('Light'),
|
title: const Text('Light'),
|
||||||
subtitle: const Text('Orange light theme'),
|
subtitle: const Text('Blue light theme'),
|
||||||
value: AppThemeMode.light,
|
value: AppThemeMode.light,
|
||||||
groupValue: _selectedTheme,
|
groupValue: _selectedTheme,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -477,7 +492,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
RadioListTile<AppThemeMode>(
|
RadioListTile<AppThemeMode>(
|
||||||
title: const Text('Dark'),
|
title: const Text('Dark'),
|
||||||
subtitle: const Text('Orange dark theme'),
|
subtitle: const Text('Blue dark theme'),
|
||||||
value: AppThemeMode.dark,
|
value: AppThemeMode.dark,
|
||||||
groupValue: _selectedTheme,
|
groupValue: _selectedTheme,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -570,9 +585,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'MeshCore SAR',
|
'MeshCore SAR',
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(
|
||||||
fontWeight: FontWeight.bold,
|
context,
|
||||||
),
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -594,9 +609,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Technologies Used:',
|
'Technologies Used:',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(
|
||||||
fontWeight: FontWeight.bold,
|
context,
|
||||||
),
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
const Text(
|
||||||
|
|||||||
@@ -39,22 +39,39 @@ class MeshCoreBleService {
|
|||||||
/// Scan for MeshCore devices
|
/// Scan for MeshCore devices
|
||||||
Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
|
Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
|
||||||
try {
|
try {
|
||||||
|
print('🔍 [BLE] Starting scan for MeshCore devices...');
|
||||||
|
print(' Service UUID: ${MeshCoreConstants.bleServiceUuid}');
|
||||||
|
print(' Timeout: ${timeout.inSeconds}s');
|
||||||
|
|
||||||
// Start scanning
|
// Start scanning
|
||||||
await FlutterBluePlus.startScan(
|
await FlutterBluePlus.startScan(
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
withServices: [Guid(MeshCoreConstants.bleServiceUuid)],
|
withServices: [Guid(MeshCoreConstants.bleServiceUuid)],
|
||||||
);
|
);
|
||||||
|
print('✅ [BLE] Scan started successfully');
|
||||||
|
|
||||||
// Listen to scan results
|
// Listen to scan results
|
||||||
|
int deviceCount = 0;
|
||||||
await for (final scanResult in FlutterBluePlus.scanResults) {
|
await for (final scanResult in FlutterBluePlus.scanResults) {
|
||||||
|
print('📡 [BLE] Scan results batch received: ${scanResult.length} results');
|
||||||
for (final result in scanResult) {
|
for (final result in scanResult) {
|
||||||
|
print(' Device: ${result.device.platformName} (${result.device.remoteId})');
|
||||||
|
print(' RSSI: ${result.rssi}');
|
||||||
|
print(' Service UUIDs: ${result.advertisementData.serviceUuids}');
|
||||||
|
|
||||||
if (result.advertisementData.serviceUuids
|
if (result.advertisementData.serviceUuids
|
||||||
.contains(Guid(MeshCoreConstants.bleServiceUuid))) {
|
.contains(Guid(MeshCoreConstants.bleServiceUuid))) {
|
||||||
|
deviceCount++;
|
||||||
|
print(' ✅ MeshCore device found! Total: $deviceCount');
|
||||||
yield result.device;
|
yield result.device;
|
||||||
|
} else {
|
||||||
|
print(' ❌ Not a MeshCore device (service UUID mismatch)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print('🏁 [BLE] Scan completed. Found $deviceCount MeshCore devices');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('❌ [BLE] Scan error: $e');
|
||||||
onError?.call('Scan error: $e');
|
onError?.call('Scan error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,64 +79,104 @@ class MeshCoreBleService {
|
|||||||
/// Connect to a MeshCore device
|
/// Connect to a MeshCore device
|
||||||
Future<bool> connect(BluetoothDevice device) async {
|
Future<bool> connect(BluetoothDevice device) async {
|
||||||
try {
|
try {
|
||||||
|
print('🔵 [BLE] Starting connection to device: ${device.platformName} (${device.remoteId})');
|
||||||
_device = device;
|
_device = device;
|
||||||
|
|
||||||
// Connect to device
|
// Connect to device
|
||||||
|
print('🔵 [BLE] Calling device.connect() with 15s timeout...');
|
||||||
await device.connect(
|
await device.connect(
|
||||||
license: License.free,
|
license: License.free,
|
||||||
timeout: const Duration(seconds: 15),
|
timeout: const Duration(seconds: 15),
|
||||||
mtu: 512,
|
mtu: 512,
|
||||||
);
|
);
|
||||||
|
print('✅ [BLE] Device connected successfully');
|
||||||
|
|
||||||
// Discover services
|
// Discover services
|
||||||
|
print('🔵 [BLE] Discovering services...');
|
||||||
final services = await device.discoverServices();
|
final services = await device.discoverServices();
|
||||||
|
print('✅ [BLE] Found ${services.length} services');
|
||||||
|
|
||||||
|
// Log all discovered services for debugging
|
||||||
|
for (final service in services) {
|
||||||
|
print(' 📋 Service: ${service.uuid}');
|
||||||
|
for (final char in service.characteristics) {
|
||||||
|
print(' - Characteristic: ${char.uuid}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find MeshCore service
|
// Find MeshCore service
|
||||||
|
print('🔵 [BLE] Looking for MeshCore service: ${MeshCoreConstants.bleServiceUuid}');
|
||||||
BluetoothService? meshCoreService;
|
BluetoothService? meshCoreService;
|
||||||
for (final service in services) {
|
for (final service in services) {
|
||||||
if (service.uuid.toString().toLowerCase() ==
|
if (service.uuid.toString().toLowerCase() ==
|
||||||
MeshCoreConstants.bleServiceUuid.toLowerCase()) {
|
MeshCoreConstants.bleServiceUuid.toLowerCase()) {
|
||||||
meshCoreService = service;
|
meshCoreService = service;
|
||||||
|
print('✅ [BLE] Found MeshCore service');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meshCoreService == null) {
|
if (meshCoreService == null) {
|
||||||
|
print('❌ [BLE] MeshCore service not found!');
|
||||||
throw Exception('MeshCore service not found');
|
throw Exception('MeshCore service not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find RX and TX characteristics
|
// Find RX and TX characteristics
|
||||||
|
print('🔵 [BLE] Looking for RX and TX characteristics...');
|
||||||
|
print(' RX UUID: ${MeshCoreConstants.bleCharacteristicRxUuid}');
|
||||||
|
print(' TX UUID: ${MeshCoreConstants.bleCharacteristicTxUuid}');
|
||||||
|
|
||||||
for (final characteristic in meshCoreService.characteristics) {
|
for (final characteristic in meshCoreService.characteristics) {
|
||||||
final uuid = characteristic.uuid.toString().toLowerCase();
|
final uuid = characteristic.uuid.toString().toLowerCase();
|
||||||
|
print(' 📋 Checking characteristic: $uuid');
|
||||||
|
|
||||||
if (uuid == MeshCoreConstants.bleCharacteristicRxUuid.toLowerCase()) {
|
if (uuid == MeshCoreConstants.bleCharacteristicRxUuid.toLowerCase()) {
|
||||||
_rxCharacteristic = characteristic;
|
_rxCharacteristic = characteristic;
|
||||||
|
print(' ✅ Found RX characteristic');
|
||||||
} else if (uuid ==
|
} else if (uuid ==
|
||||||
MeshCoreConstants.bleCharacteristicTxUuid.toLowerCase()) {
|
MeshCoreConstants.bleCharacteristicTxUuid.toLowerCase()) {
|
||||||
_txCharacteristic = characteristic;
|
_txCharacteristic = characteristic;
|
||||||
|
print(' ✅ Found TX characteristic');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_rxCharacteristic == null || _txCharacteristic == null) {
|
if (_rxCharacteristic == null || _txCharacteristic == null) {
|
||||||
|
print('❌ [BLE] Required characteristics not found!');
|
||||||
|
print(' RX found: ${_rxCharacteristic != null}');
|
||||||
|
print(' TX found: ${_txCharacteristic != null}');
|
||||||
throw Exception('Required characteristics not found');
|
throw Exception('Required characteristics not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable notifications on TX characteristic
|
// Enable notifications on TX characteristic
|
||||||
|
print('🔵 [BLE] Enabling notifications on TX characteristic...');
|
||||||
await _txCharacteristic!.setNotifyValue(true);
|
await _txCharacteristic!.setNotifyValue(true);
|
||||||
|
print('✅ [BLE] Notifications enabled');
|
||||||
|
|
||||||
// Listen to TX characteristic
|
// Listen to TX characteristic
|
||||||
|
print('🔵 [BLE] Setting up TX characteristic listener...');
|
||||||
_txSubscription = _txCharacteristic!.lastValueStream.listen(
|
_txSubscription = _txCharacteristic!.lastValueStream.listen(
|
||||||
_onDataReceived,
|
_onDataReceived,
|
||||||
onError: (error) => onError?.call('TX notification error: $error'),
|
onError: (error) {
|
||||||
|
print('❌ [BLE] TX notification error: $error');
|
||||||
|
onError?.call('TX notification error: $error');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
print('✅ [BLE] TX listener configured');
|
||||||
|
|
||||||
_isConnected = true;
|
_isConnected = true;
|
||||||
|
print('🔵 [BLE] Notifying connection state change: connected');
|
||||||
onConnectionStateChanged?.call(true);
|
onConnectionStateChanged?.call(true);
|
||||||
|
|
||||||
// Send initial device query
|
// Send initial device query
|
||||||
|
print('🔵 [BLE] Sending initial device query...');
|
||||||
await _sendDeviceQuery();
|
await _sendDeviceQuery();
|
||||||
|
print('✅ [BLE] Device query sent');
|
||||||
|
|
||||||
|
print('✅✅✅ [BLE] Connection completed successfully!');
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('❌❌❌ [BLE] Connection failed: $e');
|
||||||
|
print('Stack trace: ${StackTrace.current}');
|
||||||
onError?.call('Connection error: $e');
|
onError?.call('Connection error: $e');
|
||||||
_isConnected = false;
|
_isConnected = false;
|
||||||
onConnectionStateChanged?.call(false);
|
onConnectionStateChanged?.call(false);
|
||||||
@@ -148,8 +205,29 @@ class MeshCoreBleService {
|
|||||||
throw Exception('Not connected');
|
throw Exception('Not connected');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await _rxCharacteristic!.write(data, withoutResponse: true);
|
print('📝 [BLE] Writing ${data.length} bytes to RX characteristic...');
|
||||||
|
print(' RX Characteristic properties: ${_rxCharacteristic!.properties}');
|
||||||
|
|
||||||
|
// Check if the characteristic supports write without response
|
||||||
|
final supportsWriteWithoutResponse = _rxCharacteristic!.properties.writeWithoutResponse;
|
||||||
|
final supportsWrite = _rxCharacteristic!.properties.write;
|
||||||
|
|
||||||
|
print(' Supports writeWithoutResponse: $supportsWriteWithoutResponse');
|
||||||
|
print(' Supports write: $supportsWrite');
|
||||||
|
|
||||||
|
if (supportsWriteWithoutResponse) {
|
||||||
|
print(' Using write without response');
|
||||||
|
await _rxCharacteristic!.write(data, withoutResponse: true);
|
||||||
|
} else if (supportsWrite) {
|
||||||
|
print(' Using write with response');
|
||||||
|
await _rxCharacteristic!.write(data, withoutResponse: false);
|
||||||
|
} else {
|
||||||
|
throw Exception('Characteristic does not support write operations');
|
||||||
|
}
|
||||||
|
|
||||||
|
print('✅ [BLE] Write successful');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('❌ [BLE] Write error: $e');
|
||||||
onError?.call('Write error: $e');
|
onError?.call('Write error: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -158,37 +236,69 @@ class MeshCoreBleService {
|
|||||||
/// Handle incoming data from TX characteristic
|
/// Handle incoming data from TX characteristic
|
||||||
void _onDataReceived(List<int> data) {
|
void _onDataReceived(List<int> data) {
|
||||||
try {
|
try {
|
||||||
|
print('📥 [BLE] Received ${data.length} bytes from TX characteristic');
|
||||||
|
print(' Raw data: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||||||
|
|
||||||
final reader = BufferReader(Uint8List.fromList(data));
|
final reader = BufferReader(Uint8List.fromList(data));
|
||||||
final responseCode = reader.readByte();
|
final responseCode = reader.readByte();
|
||||||
|
print(' Response code: $responseCode (0x${responseCode.toRadixString(16)})');
|
||||||
|
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||||
|
|
||||||
switch (responseCode) {
|
switch (responseCode) {
|
||||||
case MeshCoreConstants.respContactsStart:
|
case MeshCoreConstants.respContactsStart:
|
||||||
|
print(' → Handling ContactsStart');
|
||||||
_handleContactsStart(reader);
|
_handleContactsStart(reader);
|
||||||
break;
|
break;
|
||||||
case MeshCoreConstants.respContact:
|
case MeshCoreConstants.respContact:
|
||||||
|
print(' → Handling Contact');
|
||||||
_handleContact(reader);
|
_handleContact(reader);
|
||||||
break;
|
break;
|
||||||
case MeshCoreConstants.respEndOfContacts:
|
case MeshCoreConstants.respEndOfContacts:
|
||||||
|
print(' → Handling EndOfContacts');
|
||||||
_handleEndOfContacts(reader);
|
_handleEndOfContacts(reader);
|
||||||
break;
|
break;
|
||||||
case MeshCoreConstants.respContactMsgRecv:
|
case MeshCoreConstants.respContactMsgRecv:
|
||||||
|
print(' → Handling ContactMessage');
|
||||||
_handleContactMessage(reader);
|
_handleContactMessage(reader);
|
||||||
break;
|
break;
|
||||||
case MeshCoreConstants.respChannelMsgRecv:
|
case MeshCoreConstants.respChannelMsgRecv:
|
||||||
|
print(' → Handling ChannelMessage');
|
||||||
_handleChannelMessage(reader);
|
_handleChannelMessage(reader);
|
||||||
break;
|
break;
|
||||||
case MeshCoreConstants.pushTelemetryResponse:
|
case MeshCoreConstants.pushTelemetryResponse:
|
||||||
|
print(' → Handling TelemetryResponse');
|
||||||
_handleTelemetryResponse(reader);
|
_handleTelemetryResponse(reader);
|
||||||
break;
|
break;
|
||||||
|
case MeshCoreConstants.respDeviceInfo:
|
||||||
|
print(' → Handling DeviceInfo');
|
||||||
|
_handleDeviceInfo(reader);
|
||||||
|
break;
|
||||||
|
case MeshCoreConstants.respSelfInfo:
|
||||||
|
print(' → Handling SelfInfo');
|
||||||
|
_handleSelfInfo(reader);
|
||||||
|
break;
|
||||||
|
case MeshCoreConstants.pushAdvert:
|
||||||
|
print(' → Handling Advert push');
|
||||||
|
_handleAdvert(reader);
|
||||||
|
break;
|
||||||
|
case MeshCoreConstants.pushLogRxData:
|
||||||
|
print(' → Handling LogRxData push');
|
||||||
|
_handleLogRxData(reader);
|
||||||
|
break;
|
||||||
case MeshCoreConstants.respOk:
|
case MeshCoreConstants.respOk:
|
||||||
|
print(' → Response: OK');
|
||||||
|
break;
|
||||||
case MeshCoreConstants.respErr:
|
case MeshCoreConstants.respErr:
|
||||||
// Handle OK/Error responses if needed
|
print(' → Response: ERROR');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown response code
|
print(' ⚠️ Unknown response code: $responseCode');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
print('✅ [BLE] Data parsed successfully');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
print('❌ [BLE] Data parsing error: $e');
|
||||||
|
print(' Stack trace: $stackTrace');
|
||||||
onError?.call('Data parsing error: $e');
|
onError?.call('Data parsing error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,22 +415,173 @@ class MeshCoreBleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle DeviceInfo response
|
||||||
|
void _handleDeviceInfo(BufferReader reader) {
|
||||||
|
try {
|
||||||
|
print(' [DeviceInfo] Parsing device info...');
|
||||||
|
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||||
|
|
||||||
|
// DeviceInfo format (based on MeshCore protocol):
|
||||||
|
// - 1 byte: protocol version
|
||||||
|
// - 32 bytes: public key
|
||||||
|
// - 1 byte: device name length
|
||||||
|
// - N bytes: device name (UTF-8)
|
||||||
|
// - remaining: additional info (firmware version, etc.)
|
||||||
|
|
||||||
|
if (reader.remainingBytesCount < 1) {
|
||||||
|
print(' [DeviceInfo] No data to parse');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final protocolVersion = reader.readByte();
|
||||||
|
print(' Protocol version: $protocolVersion');
|
||||||
|
|
||||||
|
if (reader.remainingBytesCount >= 32) {
|
||||||
|
final publicKey = reader.readBytes(32);
|
||||||
|
print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read remaining data as device info details
|
||||||
|
if (reader.hasRemaining) {
|
||||||
|
final remainingData = reader.readRemainingBytes();
|
||||||
|
print(' Additional info: ${remainingData.length} bytes');
|
||||||
|
// Could parse device name, firmware version, etc. here if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
print(' ✅ [DeviceInfo] Parsed successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print(' ❌ [DeviceInfo] Parsing error: $e');
|
||||||
|
onError?.call('DeviceInfo parsing error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle SelfInfo response
|
||||||
|
void _handleSelfInfo(BufferReader reader) {
|
||||||
|
try {
|
||||||
|
print(' [SelfInfo] Parsing self info...');
|
||||||
|
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||||
|
|
||||||
|
// SelfInfo format (from MeshCore protocol):
|
||||||
|
// - 1 byte: protocol version
|
||||||
|
// - 1 byte: device type
|
||||||
|
// - 1 byte: tx power
|
||||||
|
// - 1 byte: max tx power
|
||||||
|
// - 32 bytes: public key
|
||||||
|
// - 4 bytes: adv lat (int32)
|
||||||
|
// - 4 bytes: adv lon (int32)
|
||||||
|
// - 1 byte: manual add contacts flag
|
||||||
|
// - 4 bytes: radio freq (uint32)
|
||||||
|
// - 2 bytes: radio bw (uint16)
|
||||||
|
// - 1 byte: radio sf
|
||||||
|
// - 1 byte: radio cr
|
||||||
|
// - remaining: self name (null-terminated string)
|
||||||
|
|
||||||
|
if (reader.remainingBytesCount < 54) {
|
||||||
|
print(' [SelfInfo] Insufficient data: ${reader.remainingBytesCount} bytes');
|
||||||
|
// Just consume remaining bytes to avoid errors
|
||||||
|
reader.readRemainingBytes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final protocolVersion = reader.readByte();
|
||||||
|
final deviceType = reader.readByte();
|
||||||
|
final txPower = reader.readByte();
|
||||||
|
final maxTxPower = reader.readByte();
|
||||||
|
final publicKey = reader.readBytes(32);
|
||||||
|
final advLat = reader.readInt32LE();
|
||||||
|
final advLon = reader.readInt32LE();
|
||||||
|
final manualAddContacts = reader.readByte();
|
||||||
|
final radioFreq = reader.readUInt32LE();
|
||||||
|
final radioBw = reader.readUInt16LE();
|
||||||
|
final radioSf = reader.readByte();
|
||||||
|
final radioCr = reader.readByte();
|
||||||
|
|
||||||
|
print(' Protocol version: $protocolVersion');
|
||||||
|
print(' Device type: $deviceType');
|
||||||
|
print(' TX power: $txPower / $maxTxPower dBm');
|
||||||
|
print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||||
|
print(' Position: ${advLat / 10000.0}, ${advLon / 10000.0}');
|
||||||
|
print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr');
|
||||||
|
|
||||||
|
if (reader.hasRemaining) {
|
||||||
|
final selfName = String.fromCharCodes(reader.readRemainingBytes().takeWhile((b) => b != 0));
|
||||||
|
print(' Self name: $selfName');
|
||||||
|
}
|
||||||
|
|
||||||
|
print(' ✅ [SelfInfo] Parsed successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print(' ❌ [SelfInfo] Parsing error: $e');
|
||||||
|
// Don't call onError for self info - it's not critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle Advert push
|
||||||
|
void _handleAdvert(BufferReader reader) {
|
||||||
|
try {
|
||||||
|
print(' [Advert] Parsing advert...');
|
||||||
|
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||||
|
|
||||||
|
// Advert format: 32 bytes public key
|
||||||
|
if (reader.remainingBytesCount >= 32) {
|
||||||
|
final publicKey = reader.readBytes(32);
|
||||||
|
print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume any remaining bytes
|
||||||
|
if (reader.hasRemaining) {
|
||||||
|
reader.readRemainingBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
print(' ✅ [Advert] Parsed successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print(' ❌ [Advert] Parsing error: $e');
|
||||||
|
// Don't call onError - adverts are informational
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle LogRxData push
|
||||||
|
void _handleLogRxData(BufferReader reader) {
|
||||||
|
try {
|
||||||
|
print(' [LogRxData] Parsing log rx data...');
|
||||||
|
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||||
|
|
||||||
|
// This is encrypted/encoded data - just consume it
|
||||||
|
final data = reader.readRemainingBytes();
|
||||||
|
print(' Data length: ${data.length} bytes');
|
||||||
|
print(' ✅ [LogRxData] Parsed successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print(' ❌ [LogRxData] Parsing error: $e');
|
||||||
|
// Don't call onError - logs are informational
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send AppStart command
|
/// Send AppStart command
|
||||||
Future<void> _sendAppStart() async {
|
Future<void> _sendAppStart() async {
|
||||||
|
print('📤 [BLE] Preparing AppStart command...');
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(MeshCoreConstants.cmdAppStart);
|
writer.writeByte(MeshCoreConstants.cmdAppStart);
|
||||||
writer.writeByte(1); // appVer
|
writer.writeByte(1); // appVer
|
||||||
writer.writeBytes(Uint8List(6)); // reserved
|
writer.writeBytes(Uint8List(6)); // reserved
|
||||||
writer.writeString('MeshCore SAR'); // appName
|
writer.writeString('MeshCore SAR'); // appName
|
||||||
await _writeData(writer.toBytes());
|
final data = writer.toBytes();
|
||||||
|
print(' Command: ${MeshCoreConstants.cmdAppStart}');
|
||||||
|
print(' Data length: ${data.length} bytes');
|
||||||
|
await _writeData(data);
|
||||||
|
print('✅ [BLE] AppStart command sent');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send DeviceQuery command
|
/// Send DeviceQuery command
|
||||||
Future<void> _sendDeviceQuery() async {
|
Future<void> _sendDeviceQuery() async {
|
||||||
|
print('📤 [BLE] Preparing DeviceQuery command...');
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(MeshCoreConstants.cmdDeviceQuery);
|
writer.writeByte(MeshCoreConstants.cmdDeviceQuery);
|
||||||
writer.writeByte(MeshCoreConstants.supportedCompanionProtocolVersion);
|
writer.writeByte(MeshCoreConstants.supportedCompanionProtocolVersion);
|
||||||
await _writeData(writer.toBytes());
|
final data = writer.toBytes();
|
||||||
|
print(' Command: ${MeshCoreConstants.cmdDeviceQuery}');
|
||||||
|
print(' Protocol version: ${MeshCoreConstants.supportedCompanionProtocolVersion}');
|
||||||
|
print(' Data length: ${data.length} bytes');
|
||||||
|
await _writeData(data);
|
||||||
|
print('✅ [BLE] DeviceQuery command sent');
|
||||||
await _sendAppStart();
|
await _sendAppStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user