feat: Add detailed logging for BLE connection and scanning processes

This commit is contained in:
Janez T
2025-10-14 10:33:22 +02:00
parent 2eb2ee4433
commit 62bbb342ec
4 changed files with 375 additions and 49 deletions

View File

@@ -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;
// 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( _deviceInfo = _deviceInfo.copyWith(
connectionState: ConnectionState.error, 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,
); );

View File

@@ -186,12 +186,28 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
color: Colors.white, color: Colors.white,
), ),
onTap: () async { onTap: () async {
Navigator.pop(context); print('🔵 [UI] User tapped device: ${device.platformName}');
await provider.connect(device);
if (context.mounted && // Get app provider reference before popping dialog
provider.deviceInfo.isConnected) {
final appProvider = context.read<AppProvider>(); final appProvider = context.read<AppProvider>();
print('🔵 [UI] Closing dialog...');
Navigator.pop(context);
print('🔵 [UI] Calling provider.connect()...');
final success = await provider.connect(device);
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(

View File

@@ -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
@@ -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(

View File

@@ -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 {
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); 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();
} }