initial commit

This commit is contained in:
Janez T
2025-10-13 22:28:20 +02:00
commit 823a163123
175 changed files with 12697 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
import 'package:flutter/foundation.dart';
import 'connection_provider.dart';
import 'contacts_provider.dart';
import 'messages_provider.dart';
import '../services/tile_cache_service.dart';
/// Main App Provider - coordinates all other providers
class AppProvider with ChangeNotifier {
final ConnectionProvider connectionProvider;
final ContactsProvider contactsProvider;
final MessagesProvider messagesProvider;
final TileCacheService tileCacheService;
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
AppProvider({
required this.connectionProvider,
required this.contactsProvider,
required this.messagesProvider,
required this.tileCacheService,
}) {
_setupCallbacks();
_initializeTileCache();
_isInitialized = true;
}
/// Initialize tile cache service
Future<void> _initializeTileCache() async {
try {
await tileCacheService.initialize();
debugPrint('Tile cache initialized');
} catch (e) {
debugPrint('Error initializing tile cache: $e');
}
}
/// Setup callbacks between providers
void _setupCallbacks() {
// When a contact is received from BLE
connectionProvider.onContactReceived = (contact) {
contactsProvider.addOrUpdateContact(contact);
};
// When all contacts are received
connectionProvider.onContactsComplete = (contacts) {
contactsProvider.addContacts(contacts);
debugPrint('Received ${contacts.length} contacts');
};
// When a message is received
connectionProvider.onMessageReceived = (message) {
messagesProvider.addMessage(message);
// Optionally update sender name from contacts
if (message.senderPublicKeyPrefix != null) {
final contact = contactsProvider
.findContactByKey(message.senderPublicKeyPrefix!);
if (contact != null) {
final updatedMessage = message.copyWith(senderName: contact.advName);
// Note: You might want to update the message in the list
}
}
};
// When telemetry is received
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
contactsProvider.updateTelemetry(publicKey, lppData);
};
}
/// Initialize the app (load contacts, sync time, etc.)
Future<void> initialize() async {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
// Sync device time
await connectionProvider.syncDeviceTime();
// Load contacts
await connectionProvider.getContacts();
notifyListeners();
} catch (e) {
debugPrint('Initialization error: $e');
}
}
/// Refresh data (contacts, messages)
Future<void> refresh() async {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
await connectionProvider.getContacts();
notifyListeners();
} catch (e) {
debugPrint('Refresh error: $e');
}
}
/// Clear all data
void clearAllData() {
contactsProvider.clearContacts();
messagesProvider.clearAll();
notifyListeners();
}
/// Get app statistics
Map<String, dynamic> get statistics {
return {
'connection': {
'isConnected': connectionProvider.deviceInfo.isConnected,
'deviceName': connectionProvider.deviceInfo.deviceName,
'battery': connectionProvider.deviceInfo.batteryPercent,
},
'contacts': contactsProvider.contactCounts,
'messages': messagesProvider.messageStats,
'sarMarkers': messagesProvider.sarMarkerStats,
};
}
}

View File

@@ -0,0 +1,240 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../models/device_info.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../services/meshcore_ble_service.dart';
import '../services/cayenne_lpp_parser.dart';
import '../utils/sar_message_parser.dart';
/// Connection Provider - manages MeshCore BLE connection
class ConnectionProvider with ChangeNotifier {
final MeshCoreBleService _bleService = MeshCoreBleService();
DeviceInfo _deviceInfo = DeviceInfo();
DeviceInfo get deviceInfo => _deviceInfo;
List<BluetoothDevice> _scannedDevices = [];
List<BluetoothDevice> get scannedDevices => _scannedDevices;
bool _isScanning = false;
bool get isScanning => _isScanning;
String? _error;
String? get error => _error;
// Callbacks for other providers
Function(Contact)? onContactReceived;
Function(List<Contact>)? onContactsComplete;
Function(Message)? onMessageReceived;
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
ConnectionProvider() {
_initializeBleService();
}
void _initializeBleService() {
_bleService.onConnectionStateChanged = (isConnected) {
_deviceInfo = _deviceInfo.copyWith(
connectionState: isConnected
? ConnectionState.connected
: ConnectionState.disconnected,
lastUpdate: DateTime.now(),
);
notifyListeners();
};
_bleService.onError = (error) {
_error = error;
_deviceInfo = _deviceInfo.copyWith(
connectionState: ConnectionState.error,
);
notifyListeners();
};
_bleService.onContactReceived = (contact) {
onContactReceived?.call(contact);
};
_bleService.onContactsComplete = (contacts) {
onContactsComplete?.call(contacts);
};
_bleService.onMessageReceived = (message) {
// Parse SAR markers
final enhancedMessage = SarMessageParser.enhanceMessage(message);
onMessageReceived?.call(enhancedMessage);
};
_bleService.onTelemetryReceived = (publicKey, lppData) {
onTelemetryReceived?.call(publicKey, lppData);
};
}
/// Start scanning for MeshCore devices
Future<void> startScan() async {
_isScanning = true;
_scannedDevices.clear();
_error = null;
notifyListeners();
try {
await for (final device
in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) {
if (!_scannedDevices.any((d) => d.remoteId == device.remoteId)) {
_scannedDevices.add(device);
notifyListeners();
}
}
} catch (e) {
_error = 'Scan error: $e';
} finally {
_isScanning = false;
notifyListeners();
}
}
/// Stop scanning
Future<void> stopScan() async {
await FlutterBluePlus.stopScan();
_isScanning = false;
notifyListeners();
}
/// Connect to a device
Future<bool> connect(BluetoothDevice device) async {
_deviceInfo = _deviceInfo.copyWith(
deviceId: device.remoteId.toString(),
deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown',
connectionState: ConnectionState.connecting,
);
_error = null;
notifyListeners();
final success = await _bleService.connect(device);
if (!success) {
_deviceInfo = _deviceInfo.copyWith(
connectionState: ConnectionState.error,
);
notifyListeners();
}
return success;
}
/// Disconnect from device
Future<void> disconnect() async {
_deviceInfo = _deviceInfo.copyWith(
connectionState: ConnectionState.disconnecting,
);
notifyListeners();
await _bleService.disconnect();
_deviceInfo = DeviceInfo(
connectionState: ConnectionState.disconnected,
);
notifyListeners();
}
/// Get contacts from device
Future<void> getContacts() async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.getContacts();
} catch (e) {
_error = 'Failed to get contacts: $e';
notifyListeners();
}
}
/// Send text message to contact
Future<void> sendTextMessage({
required Uint8List contactPublicKey,
required String text,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.sendTextMessage(
contactPublicKey: contactPublicKey,
text: text,
);
} catch (e) {
_error = 'Failed to send message: $e';
notifyListeners();
}
}
/// Send channel message
Future<void> sendChannelMessage({
required int channelIdx,
required String text,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.sendChannelMessage(
channelIdx: channelIdx,
text: text,
);
} catch (e) {
_error = 'Failed to send channel message: $e';
notifyListeners();
}
}
/// Request telemetry from contact
Future<void> requestTelemetry(Uint8List contactPublicKey) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.requestTelemetry(contactPublicKey);
} catch (e) {
_error = 'Failed to request telemetry: $e';
notifyListeners();
}
}
/// Set device time to current time
Future<void> syncDeviceTime() async {
if (!_bleService.isConnected) return;
try {
await _bleService.setDeviceTime();
} catch (e) {
_error = 'Failed to sync time: $e';
notifyListeners();
}
}
/// Clear error message
void clearError() {
_error = null;
notifyListeners();
}
@override
void dispose() {
_bleService.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,135 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import '../models/contact.dart';
import '../models/contact_telemetry.dart';
import '../services/cayenne_lpp_parser.dart';
/// Contacts Provider - manages contact list and telemetry
class ContactsProvider with ChangeNotifier {
final Map<String, Contact> _contacts = {};
List<Contact> get contacts => _contacts.values.toList();
List<Contact> get chatContacts =>
contacts.where((c) => c.isChat).toList()..sort(_sortByLastSeen);
List<Contact> get repeaters =>
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
List<Contact> get rooms =>
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
/// Get contacts with location (for map display)
List<Contact> get contactsWithLocation =>
contacts.where((c) => c.displayLocation != null).toList();
/// Get chat contacts with location (team members on map)
List<Contact> get chatContactsWithLocation =>
chatContacts.where((c) => c.displayLocation != null).toList();
/// Sort contacts by last seen (most recent first)
int _sortByLastSeen(Contact a, Contact b) {
return b.lastSeenTime.compareTo(a.lastSeenTime);
}
/// Add or update a contact
void addOrUpdateContact(Contact contact) {
_contacts[contact.publicKeyHex] = contact;
notifyListeners();
}
/// Add multiple contacts
void addContacts(List<Contact> contacts) {
for (final contact in contacts) {
_contacts[contact.publicKeyHex] = contact;
}
notifyListeners();
}
/// Update contact telemetry
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
// Find contact by public key prefix
final contact = _findContactByPrefix(publicKeyPrefix);
if (contact == null) return;
try {
// Parse Cayenne LPP data
final telemetry = CayenneLppParser.parse(lppData);
// Update contact with new telemetry
final updatedContact = contact.copyWith(telemetry: telemetry);
_contacts[contact.publicKeyHex] = updatedContact;
notifyListeners();
} catch (e) {
debugPrint('Failed to parse telemetry: $e');
}
}
/// Find contact by public key prefix (6 bytes)
Contact? _findContactByPrefix(Uint8List prefix) {
if (prefix.length < 6) return null;
final prefixHex = prefix
.sublist(0, 6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
for (final contact in contacts) {
if (contact.publicKeyHex.startsWith(prefixHex)) {
return contact;
}
}
return null;
}
/// Find contact by public key
Contact? findContactByKey(Uint8List publicKey) {
final keyHex =
publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
return _contacts[keyHex];
}
/// Find contact by name
Contact? findContactByName(String name) {
return contacts.firstWhere(
(c) => c.advName == name,
orElse: () => contacts.first,
);
}
/// Get contacts with low battery
List<Contact> get lowBatteryContacts {
return contacts.where((c) {
final battery = c.displayBattery;
return battery != null && battery < 20.0;
}).toList();
}
/// Get recently seen contacts (within last 10 minutes)
List<Contact> get recentlySeenContacts {
return contacts.where((c) => c.isRecentlySeen).toList();
}
/// Clear all contacts
void clearContacts() {
_contacts.clear();
notifyListeners();
}
/// Remove a contact
void removeContact(String publicKeyHex) {
_contacts.remove(publicKeyHex);
notifyListeners();
}
/// Get contact count by type
Map<String, int> get contactCounts {
return {
'chat': chatContacts.length,
'repeater': repeaters.length,
'room': rooms.length,
'total': contacts.length,
};
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
class MapProvider with ChangeNotifier {
LatLng? _targetLocation;
double? _targetZoom;
bool _shouldAnimate = false;
LatLng? get targetLocation => _targetLocation;
double? get targetZoom => _targetZoom;
bool get shouldAnimate => _shouldAnimate;
void navigateToLocation({
required LatLng location,
double zoom = 15.0,
bool animate = true,
}) {
_targetLocation = location;
_targetZoom = zoom;
_shouldAnimate = animate;
notifyListeners();
}
void clearNavigation() {
_targetLocation = null;
_targetZoom = null;
_shouldAnimate = false;
// Don't notify listeners to avoid rebuilds
}
void updateZoom(double zoom) {
_targetZoom = zoom;
notifyListeners();
}
}

View File

@@ -0,0 +1,156 @@
import 'package:flutter/foundation.dart';
import '../models/message.dart';
import '../models/sar_marker.dart';
/// Messages Provider - manages message history and SAR markers
class MessagesProvider with ChangeNotifier {
final List<Message> _messages = [];
final Map<String, SarMarker> _sarMarkers = {};
List<Message> get messages => List.unmodifiable(_messages);
List<Message> get contactMessages =>
_messages.where((m) => m.isContactMessage).toList();
List<Message> get channelMessages =>
_messages.where((m) => m.isChannelMessage).toList();
List<Message> get sarMarkerMessages =>
_messages.where((m) => m.isSarMarker).toList();
List<SarMarker> get sarMarkers => _sarMarkers.values.toList();
List<SarMarker> get foundPersonMarkers =>
sarMarkers.where((m) => m.type == SarMarkerType.foundPerson).toList();
List<SarMarker> get fireMarkers =>
sarMarkers.where((m) => m.type == SarMarkerType.fire).toList();
List<SarMarker> get stagingAreaMarkers =>
sarMarkers.where((m) => m.type == SarMarkerType.stagingArea).toList();
/// Add a message
void addMessage(Message message) {
_messages.add(message);
// If it's a SAR marker message, extract and store the marker
if (message.isSarMarker) {
final marker = message.toSarMarker();
if (marker != null) {
_sarMarkers[marker.id] = marker;
}
}
notifyListeners();
}
/// Add multiple messages
void addMessages(List<Message> messages) {
for (final message in messages) {
_messages.add(message);
if (message.isSarMarker) {
final marker = message.toSarMarker();
if (marker != null) {
_sarMarkers[marker.id] = marker;
}
}
}
notifyListeners();
}
/// Get messages for a specific contact
List<Message> getMessagesForContact(String senderKeyShort) {
return _messages
.where((m) =>
m.isContactMessage &&
m.senderKeyShort != null &&
m.senderKeyShort!.startsWith(senderKeyShort))
.toList();
}
/// Get messages for a specific channel
List<Message> getMessagesForChannel(int channelIdx) {
return _messages
.where((m) => m.isChannelMessage && m.channelIdx == channelIdx)
.toList();
}
/// Get recent messages (last N messages)
List<Message> getRecentMessages({int count = 50}) {
final sorted = List<Message>.from(_messages)
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
return sorted.take(count).toList();
}
/// Get messages from last N hours
List<Message> getMessagesSince(Duration duration) {
final cutoff = DateTime.now().subtract(duration);
return _messages.where((m) => m.sentAt.isAfter(cutoff)).toList();
}
/// Search messages by text
List<Message> searchMessages(String query) {
if (query.isEmpty) return [];
final lowerQuery = query.toLowerCase();
return _messages
.where((m) => m.text.toLowerCase().contains(lowerQuery))
.toList();
}
/// Get SAR marker by ID
SarMarker? getSarMarker(String id) {
return _sarMarkers[id];
}
/// Get recent SAR markers (within last hour)
List<SarMarker> getRecentSarMarkers() {
return sarMarkers.where((m) => m.isRecent).toList();
}
/// Remove a SAR marker
void removeSarMarker(String id) {
_sarMarkers.remove(id);
notifyListeners();
}
/// Clear all messages
void clearMessages() {
_messages.clear();
notifyListeners();
}
/// Clear all SAR markers
void clearSarMarkers() {
_sarMarkers.clear();
notifyListeners();
}
/// Clear all data
void clearAll() {
_messages.clear();
_sarMarkers.clear();
notifyListeners();
}
/// Get message statistics
Map<String, int> get messageStats {
return {
'total': _messages.length,
'contact': contactMessages.length,
'channel': channelMessages.length,
'sar': sarMarkerMessages.length,
'sarMarkers': sarMarkers.length,
};
}
/// Get SAR marker statistics
Map<String, int> get sarMarkerStats {
return {
'total': sarMarkers.length,
'foundPerson': foundPersonMarkers.length,
'fire': fireMarkers.length,
'stagingArea': stagingAreaMarkers.length,
};
}
}