feat: Implement command queue for BLE command handling

- Added BleCommandQueue to manage command serialization and responses in BleCommandSender.
- Updated writeData, writeDataAndWaitForAck, and writeDataAndWaitForResponse methods to utilize the command queue.
- Enhanced BleResponseHandler to complete commands based on responses received from the BLE device.
- Introduced new commands for channel management, including getChannel and setChannel.
- Created LocationTrailLayer and TrailControls widgets for displaying and managing location trails on the map.
- Added PermissionRequestDialog to handle location permission requests on app startup.
- Updated LocationTrackingService to allow GPS tracking without a BLE connection.
This commit is contained in:
Janez T
2025-10-18 21:12:02 +02:00
parent f88c9cfdd3
commit c50a260263
35 changed files with 1854 additions and 59 deletions

View File

@@ -5,6 +5,7 @@ import 'connection_provider.dart';
import 'contacts_provider.dart';
import 'messages_provider.dart';
import 'drawing_provider.dart';
import 'channels_provider.dart';
import '../services/tile_cache_service.dart';
import '../services/location_tracking_service.dart';
import '../models/contact.dart';
@@ -16,6 +17,7 @@ class AppProvider with ChangeNotifier {
final ContactsProvider contactsProvider;
final MessagesProvider messagesProvider;
final DrawingProvider drawingProvider;
final ChannelsProvider channelsProvider;
final TileCacheService tileCacheService;
final LocationTrackingService locationTrackingService = LocationTrackingService();
@@ -27,6 +29,7 @@ class AppProvider with ChangeNotifier {
required this.contactsProvider,
required this.messagesProvider,
required this.drawingProvider,
required this.channelsProvider,
required this.tileCacheService,
}) {
_setupCallbacks();
@@ -97,6 +100,12 @@ class AppProvider with ChangeNotifier {
debugPrint('Received ${contacts.length} contacts');
};
// When channel info is received
connectionProvider.onChannelInfoReceived = (channelIdx, channelName) {
channelsProvider.addOrUpdateChannel(channelIdx, channelName);
debugPrint('📻 [AppProvider] Channel $channelIdx: "$channelName"');
};
// When a message is received
connectionProvider.onMessageReceived = (message) {
// Check if message is a drawing broadcast
@@ -242,6 +251,11 @@ class AppProvider with ChangeNotifier {
// Small delay to ensure contacts are fully loaded
await Future.delayed(const Duration(milliseconds: 500));
// Sync all channels to get channel names
debugPrint('📻 [AppProvider] Syncing channels...');
await connectionProvider.syncChannels();
debugPrint('✅ [AppProvider] Channel sync complete');
// Automatically login to all saved rooms
await _autoLoginToRooms();
@@ -253,6 +267,10 @@ class AppProvider with ChangeNotifier {
// Note: Future messages are synced automatically via PUSH_CODE_MSG_WAITING events
// Start location tracking AFTER all initialization is complete
debugPrint('📍 [AppProvider] Starting location tracking after successful initialization');
await _startLocationTracking();
notifyListeners();
} catch (e) {
debugPrint('Initialization error: $e');
@@ -395,11 +413,9 @@ class AppProvider with ChangeNotifier {
final isConnected = connectionProvider.deviceInfo.isConnected;
final wasTracking = locationTrackingService.isTracking;
if (isConnected && !wasTracking) {
// Connection established - start location tracking
debugPrint('🔵 [AppProvider] BLE connected - starting location tracking');
_startLocationTracking();
} else if (!isConnected && wasTracking) {
// Only stop tracking on disconnect - DON'T start on connect
// Location tracking will be started AFTER initialization completes
if (!isConnected && wasTracking) {
// Connection lost - stop location tracking
debugPrint('🔴 [AppProvider] BLE disconnected - stopping location tracking');
_stopLocationTracking();

View File

@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import '../models/channel.dart';
/// Manages channel information from the MeshCore device
class ChannelsProvider with ChangeNotifier {
final Map<int, Channel> _channels = {};
/// Get all channels
List<Channel> get channels => _channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
/// Get a specific channel by index
Channel? getChannel(int index) => _channels[index];
/// Get the display name for a channel
String getChannelDisplayName(int index) {
final channel = _channels[index];
if (channel != null) {
return channel.displayName;
}
// Fallback if channel hasn't been synced yet
return index == 0 ? 'Public' : 'Channel $index';
}
/// Add or update a channel
void addOrUpdateChannel(int index, String name, {int? flags}) {
_channels[index] = Channel(
index: index,
name: name,
flags: flags,
);
notifyListeners();
}
/// Clear all channels
void clear() {
_channels.clear();
notifyListeners();
}
/// Check if channels have been loaded
bool get hasChannels => _channels.isNotEmpty;
/// Get the number of channels
int get channelCount => _channels.length;
@override
void dispose() {
_channels.clear();
super.dispose();
}
}

View File

@@ -123,6 +123,7 @@ class ConnectionProvider with ChangeNotifier {
Function(List<Contact>)? onContactsComplete;
Function(Message)? onMessageReceived;
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
Function(int channelIdx, String channelName)? onChannelInfoReceived;
Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)?
onBinaryResponse;
Function(Uint8List publicKey)? onPathUpdated;
@@ -249,6 +250,10 @@ class ConnectionProvider with ChangeNotifier {
onContactsComplete?.call(contacts);
};
_bleService.onChannelInfoReceived = (channelIdx, channelName) {
onChannelInfoReceived?.call(channelIdx, channelName);
};
_bleService.onMessageReceived = (message) {
// Parse SAR markers
final enhancedMessage = SarMessageParser.enhanceMessage(message);
@@ -628,6 +633,24 @@ class ConnectionProvider with ChangeNotifier {
}
}
/// Sync all channels from device
Future<void> syncChannels({int? maxChannels}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
// Use maxChannels from device info if available, otherwise default to 40
final channelCount = maxChannels ?? _deviceInfo.maxChannels ?? 40;
await _bleService.syncAllChannels(maxChannels: channelCount);
} catch (e) {
_error = 'Failed to sync channels: $e';
notifyListeners();
}
}
/// Add or update a contact on the companion radio
///
/// This manually adds a contact to the radio's internal contact table.

View File

@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
import '../models/location_trail.dart';
class MapProvider with ChangeNotifier {
LatLng? _targetLocation;
@@ -9,11 +10,22 @@ class MapProvider with ChangeNotifier {
// Track which contact paths are currently visible
final Set<String> _visibleContactPaths = {};
// Location trail tracking
LocationTrail? _currentTrail;
bool _isTrailVisible = true;
final List<LocationTrail> _trailHistory = [];
LatLng? get targetLocation => _targetLocation;
double? get targetZoom => _targetZoom;
bool get shouldAnimate => _shouldAnimate;
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
// Trail getters
LocationTrail? get currentTrail => _currentTrail;
bool get isTrailVisible => _isTrailVisible;
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
bool get isTrailActive => _currentTrail?.isActive ?? false;
void navigateToLocation({
required LatLng location,
double zoom = 15.0,
@@ -64,4 +76,80 @@ class MapProvider with ChangeNotifier {
_visibleContactPaths.add(publicKeyHex);
notifyListeners();
}
/// Start a new location trail
void startTrail() {
// End current trail if active
if (_currentTrail != null && _currentTrail!.isActive) {
endTrail();
}
_currentTrail = LocationTrail(
id: DateTime.now().millisecondsSinceEpoch.toString(),
startTime: DateTime.now(),
);
_isTrailVisible = true;
notifyListeners();
}
/// Add a point to the current trail
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
if (_currentTrail == null || !_currentTrail!.isActive) {
startTrail();
}
_currentTrail!.addPoint(TrailPoint(
position: position,
timestamp: DateTime.now(),
accuracy: accuracy,
speed: speed,
));
notifyListeners();
}
/// End the current trail
void endTrail() {
if (_currentTrail != null) {
_currentTrail!.isActive = false;
_currentTrail!.endTime = DateTime.now();
if (_currentTrail!.points.isNotEmpty) {
_trailHistory.add(_currentTrail!);
}
_currentTrail = null;
notifyListeners();
}
}
/// Toggle trail visibility
void toggleTrailVisibility() {
_isTrailVisible = !_isTrailVisible;
notifyListeners();
}
/// Clear the current trail
void clearCurrentTrail() {
if (_currentTrail != null) {
_currentTrail = null;
notifyListeners();
}
}
/// Clear all trail history
void clearAllTrails() {
_currentTrail = null;
_trailHistory.clear();
notifyListeners();
}
/// Get total trail distance in meters
double get totalTrailDistance {
if (_currentTrail == null) return 0;
return _currentTrail!.totalDistance;
}
/// Get trail duration
Duration get trailDuration {
if (_currentTrail == null) return Duration.zero;
return _currentTrail!.duration;
}
}