mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-13 09:20:29 +00:00
Refactor code for improved readability and consistency across multiple files
- Updated formatting in `drawing_toolbar.dart` for better readability, including breaking long lines and ensuring consistent indentation. - Enhanced the `recipient_selector_sheet.dart` by adjusting line breaks and improving the layout of text styles. - Refactored `sar_update_sheet.dart` to improve readability, including restructuring widget layouts and ensuring consistent formatting. - Cleaned up imports and removed unnecessary lines in `widget_test.dart`.
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'meshcore_ble_service.dart';
|
||||
@@ -32,7 +30,9 @@ class BackgroundLocationService {
|
||||
/// additional platform-specific configuration is required.
|
||||
Future<bool> startTracking({double distanceThreshold = 10.0}) async {
|
||||
if (!_isInitialized || _bleService == null) {
|
||||
debugPrint('⚠️ [BackgroundLocation] Service not initialized or BLE service null');
|
||||
debugPrint(
|
||||
'⚠️ [BackgroundLocation] Service not initialized or BLE service null',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ class BackgroundLocationService {
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
debugPrint('⚠️ [BackgroundLocation] Location permission permanently denied');
|
||||
debugPrint(
|
||||
'⚠️ [BackgroundLocation] Location permission permanently denied',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,60 +66,77 @@ class BackgroundLocationService {
|
||||
// Start listening to position updates
|
||||
Position? lastPosition;
|
||||
try {
|
||||
_positionSubscription = Geolocator.getPositionStream(
|
||||
locationSettings: LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
distanceFilter: distanceThreshold.toInt(),
|
||||
),
|
||||
).listen((Position position) async {
|
||||
debugPrint('📍 [BackgroundLocation] New position: ${position.latitude}, ${position.longitude}');
|
||||
|
||||
// Calculate distance from last position
|
||||
if (lastPosition != null) {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
lastPosition!.latitude,
|
||||
lastPosition!.longitude,
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
);
|
||||
|
||||
debugPrint(' Distance moved: ${distance.toStringAsFixed(1)}m (threshold: ${distanceThreshold}m)');
|
||||
|
||||
// Skip if haven't moved enough
|
||||
if (distance < distanceThreshold) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last position
|
||||
lastPosition = position;
|
||||
|
||||
// Save to preferences
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
|
||||
// Update device's advertised location
|
||||
if (_bleService != null && _bleService!.isConnected) {
|
||||
try {
|
||||
debugPrint('📤 [BackgroundLocation] Updating device location...');
|
||||
await _bleService!.setAdvertLatLon(
|
||||
latitude: position.latitude,
|
||||
longitude: position.longitude,
|
||||
_positionSubscription =
|
||||
Geolocator.getPositionStream(
|
||||
locationSettings: LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
distanceFilter: distanceThreshold.toInt(),
|
||||
),
|
||||
).listen((Position position) async {
|
||||
debugPrint(
|
||||
'📍 [BackgroundLocation] New position: ${position.latitude}, ${position.longitude}',
|
||||
);
|
||||
|
||||
// Send advertisement to mesh network
|
||||
debugPrint('📡 [BackgroundLocation] Broadcasting self advertisement...');
|
||||
await _bleService!.sendSelfAdvert(floodMode: true);
|
||||
debugPrint('✅ [BackgroundLocation] Location update sent successfully');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [BackgroundLocation] Failed to send location update: $e');
|
||||
}
|
||||
} else {
|
||||
debugPrint('⚠️ [BackgroundLocation] BLE disconnected, cannot send update');
|
||||
}
|
||||
});
|
||||
// Calculate distance from last position
|
||||
if (lastPosition != null) {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
lastPosition!.latitude,
|
||||
lastPosition!.longitude,
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
);
|
||||
|
||||
debugPrint('✅ [BackgroundLocation] Tracking started with ${distanceThreshold}m threshold');
|
||||
debugPrint(
|
||||
' Distance moved: ${distance.toStringAsFixed(1)}m (threshold: ${distanceThreshold}m)',
|
||||
);
|
||||
|
||||
// Skip if haven't moved enough
|
||||
if (distance < distanceThreshold) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last position
|
||||
lastPosition = position;
|
||||
|
||||
// Save to preferences
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
|
||||
// Update device's advertised location
|
||||
if (_bleService != null && _bleService!.isConnected) {
|
||||
try {
|
||||
debugPrint(
|
||||
'📤 [BackgroundLocation] Updating device location...',
|
||||
);
|
||||
await _bleService!.setAdvertLatLon(
|
||||
latitude: position.latitude,
|
||||
longitude: position.longitude,
|
||||
);
|
||||
|
||||
// Send advertisement to mesh network
|
||||
debugPrint(
|
||||
'📡 [BackgroundLocation] Broadcasting self advertisement...',
|
||||
);
|
||||
await _bleService!.sendSelfAdvert(floodMode: true);
|
||||
debugPrint(
|
||||
'✅ [BackgroundLocation] Location update sent successfully',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'❌ [BackgroundLocation] Failed to send location update: $e',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'⚠️ [BackgroundLocation] BLE disconnected, cannot send update',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint(
|
||||
'✅ [BackgroundLocation] Tracking started with ${distanceThreshold}m threshold',
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [BackgroundLocation] Failed to start tracking: $e');
|
||||
@@ -141,7 +160,9 @@ class BackgroundLocationService {
|
||||
Future<void> updateDistanceThreshold(double distance) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_prefKeyDistance, distance);
|
||||
debugPrint('📏 [BackgroundLocation] Distance threshold updated to ${distance}m');
|
||||
debugPrint(
|
||||
'📏 [BackgroundLocation] Distance threshold updated to ${distance}m',
|
||||
);
|
||||
|
||||
// Restart tracking if currently enabled
|
||||
final isEnabled = prefs.getBool(_prefKeyEnabled) ?? false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Type of response expected from a command
|
||||
@@ -95,7 +94,8 @@ class BleCommandQueue {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
// Determine timeout based on response type
|
||||
final cmdTimeout = timeout ??
|
||||
final cmdTimeout =
|
||||
timeout ??
|
||||
(responseType == CommandResponseType.data
|
||||
? const Duration(seconds: 10)
|
||||
: const Duration(seconds: 5));
|
||||
@@ -114,7 +114,9 @@ class BleCommandQueue {
|
||||
_queue.add(command);
|
||||
onQueueSizeChanged?.call(_queue.length);
|
||||
|
||||
debugPrint('📋 [CommandQueue] Enqueued command 0x${commandCode.toRadixString(16).padLeft(2, '0')} (queue size: ${_queue.length})');
|
||||
debugPrint(
|
||||
'📋 [CommandQueue] Enqueued command 0x${commandCode.toRadixString(16).padLeft(2, '0')} (queue size: ${_queue.length})',
|
||||
);
|
||||
|
||||
// Start processing if not already running
|
||||
if (!_isProcessing) {
|
||||
@@ -125,9 +127,13 @@ class BleCommandQueue {
|
||||
return command.completer.future.timeout(
|
||||
cmdTimeout,
|
||||
onTimeout: () {
|
||||
debugPrint('⏱️ [CommandQueue] Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} timed out after ${cmdTimeout.inSeconds}s');
|
||||
debugPrint(
|
||||
'⏱️ [CommandQueue] Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} timed out after ${cmdTimeout.inSeconds}s',
|
||||
);
|
||||
_pendingResponses.remove(commandCode);
|
||||
throw TimeoutException('Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} timed out');
|
||||
throw TimeoutException(
|
||||
'Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} timed out',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -152,7 +158,9 @@ class BleCommandQueue {
|
||||
final remainingDelay = _minDelayMs - elapsed.inMilliseconds;
|
||||
|
||||
if (remainingDelay > 0) {
|
||||
debugPrint('⏸️ [CommandQueue] Waiting ${remainingDelay}ms before next command');
|
||||
debugPrint(
|
||||
'⏸️ [CommandQueue] Waiting ${remainingDelay}ms before next command',
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: remainingDelay));
|
||||
}
|
||||
}
|
||||
@@ -170,7 +178,9 @@ class BleCommandQueue {
|
||||
|
||||
// Execute command (handled by BleCommandSender)
|
||||
// The completer will be completed by completeCommand() when response arrives
|
||||
debugPrint('📤 [CommandQueue] Executing command 0x${command.commandCode.toRadixString(16).padLeft(2, '0')}');
|
||||
debugPrint(
|
||||
'📤 [CommandQueue] Executing command 0x${command.commandCode.toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
|
||||
// For fire-and-forget commands, complete immediately
|
||||
if (command.responseType == CommandResponseType.none) {
|
||||
@@ -209,7 +219,9 @@ class BleCommandQueue {
|
||||
void completeCommand<T>(int responseCode, T data) {
|
||||
final command = _pendingResponses.remove(responseCode);
|
||||
if (command != null) {
|
||||
debugPrint('✅ [CommandQueue] Completing command 0x${command.commandCode.toRadixString(16).padLeft(2, '0')} with response 0x${responseCode.toRadixString(16).padLeft(2, '0')}');
|
||||
debugPrint(
|
||||
'✅ [CommandQueue] Completing command 0x${command.commandCode.toRadixString(16).padLeft(2, '0')} with response 0x${responseCode.toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
if (!command.completer.isCompleted) {
|
||||
command.completer.complete(data);
|
||||
}
|
||||
@@ -219,10 +231,16 @@ class BleCommandQueue {
|
||||
/// Complete a pending command with error
|
||||
///
|
||||
/// Called by BleResponseHandler when RESP_CODE_ERR is received
|
||||
void completeCommandWithError(int commandCode, String error, {int? errorCode}) {
|
||||
void completeCommandWithError(
|
||||
int commandCode,
|
||||
String error, {
|
||||
int? errorCode,
|
||||
}) {
|
||||
final command = _pendingResponses.remove(commandCode);
|
||||
if (command != null) {
|
||||
debugPrint('❌ [CommandQueue] Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} failed: $error (code: $errorCode)');
|
||||
debugPrint(
|
||||
'❌ [CommandQueue] Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} failed: $error (code: $errorCode)',
|
||||
);
|
||||
if (!command.completer.isCompleted) {
|
||||
command.completer.completeError(
|
||||
Exception('Command failed: $error (error code: $errorCode)'),
|
||||
@@ -245,7 +263,9 @@ class BleCommandQueue {
|
||||
|
||||
/// Clear all pending commands (use with caution)
|
||||
void clear() {
|
||||
debugPrint('🗑️ [CommandQueue] Clearing queue (${_queue.length} commands, ${_pendingResponses.length} pending responses)');
|
||||
debugPrint(
|
||||
'🗑️ [CommandQueue] Clearing queue (${_queue.length} commands, ${_pendingResponses.length} pending responses)',
|
||||
);
|
||||
|
||||
// Complete all pending commands with error
|
||||
for (final command in _pendingResponses.values) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import '../meshcore_opcode_names.dart';
|
||||
@@ -130,10 +129,13 @@ class BleCommandSender {
|
||||
|
||||
debugPrint('📤 [TX] Sending command: $opcodeName ($opcodeHex)');
|
||||
debugPrint(' Data size: ${data.length} bytes');
|
||||
debugPrint(' Hex: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||||
debugPrint(
|
||||
' Hex: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}',
|
||||
);
|
||||
|
||||
// Check if the characteristic supports write without response
|
||||
final supportsWriteWithoutResponse = _rxCharacteristic!.properties.writeWithoutResponse;
|
||||
final supportsWriteWithoutResponse =
|
||||
_rxCharacteristic!.properties.writeWithoutResponse;
|
||||
final supportsWrite = _rxCharacteristic!.properties.write;
|
||||
|
||||
if (supportsWriteWithoutResponse) {
|
||||
@@ -160,15 +162,21 @@ class BleCommandSender {
|
||||
}
|
||||
|
||||
/// Log a packet
|
||||
void _logPacket(Uint8List data, PacketDirection direction, {int? responseCode}) {
|
||||
void _logPacket(
|
||||
Uint8List data,
|
||||
PacketDirection direction, {
|
||||
int? responseCode,
|
||||
}) {
|
||||
// Add new packet
|
||||
_packetLogs.add(BlePacketLog(
|
||||
timestamp: DateTime.now(),
|
||||
rawData: data,
|
||||
direction: direction,
|
||||
responseCode: responseCode,
|
||||
description: _getPacketDescription(responseCode),
|
||||
));
|
||||
_packetLogs.add(
|
||||
BlePacketLog(
|
||||
timestamp: DateTime.now(),
|
||||
rawData: data,
|
||||
direction: direction,
|
||||
responseCode: responseCode,
|
||||
description: _getPacketDescription(responseCode),
|
||||
),
|
||||
);
|
||||
|
||||
// Limit log size to prevent memory issues
|
||||
if (_packetLogs.length > _maxLogSize) {
|
||||
|
||||
@@ -17,24 +17,38 @@ import 'ble_command_queue.dart';
|
||||
typedef OnContactCallback = void Function(Contact contact);
|
||||
typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
|
||||
typedef OnMessageCallback = void Function(Message message);
|
||||
typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData);
|
||||
typedef OnTelemetryCallback =
|
||||
void Function(Uint8List publicKey, Uint8List lppData);
|
||||
typedef OnSelfInfoCallback = void Function(Map<String, dynamic> selfInfo);
|
||||
typedef OnDeviceInfoCallback = void Function(Map<String, dynamic> deviceInfo);
|
||||
typedef OnNoMoreMessagesCallback = void Function();
|
||||
typedef OnMessageWaitingCallback = void Function();
|
||||
typedef OnLoginSuccessCallback = void Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag);
|
||||
typedef OnLoginSuccessCallback =
|
||||
void Function(
|
||||
Uint8List publicKeyPrefix,
|
||||
int permissions,
|
||||
bool isAdmin,
|
||||
int tag,
|
||||
);
|
||||
typedef OnLoginFailCallback = void Function(Uint8List publicKeyPrefix);
|
||||
typedef OnAdvertReceivedCallback = void Function(Uint8List publicKey);
|
||||
typedef OnPathUpdatedCallback = void Function(Uint8List publicKey);
|
||||
typedef OnMessageSentCallback = void Function(int expectedAckTag, int suggestedTimeoutMs, bool isFloodMode);
|
||||
typedef OnMessageDeliveredCallback = void Function(int ackCode, int roundTripTimeMs);
|
||||
typedef OnStatusResponseCallback = void Function(Uint8List publicKeyPrefix, Uint8List statusData);
|
||||
typedef OnBinaryResponseCallback = void Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData);
|
||||
typedef OnBatteryAndStorageCallback = void Function(int millivolts, int? usedKb, int? totalKb);
|
||||
typedef OnMessageSentCallback =
|
||||
void Function(int expectedAckTag, int suggestedTimeoutMs, bool isFloodMode);
|
||||
typedef OnMessageDeliveredCallback =
|
||||
void Function(int ackCode, int roundTripTimeMs);
|
||||
typedef OnStatusResponseCallback =
|
||||
void Function(Uint8List publicKeyPrefix, Uint8List statusData);
|
||||
typedef OnBinaryResponseCallback =
|
||||
void Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData);
|
||||
typedef OnBatteryAndStorageCallback =
|
||||
void Function(int millivolts, int? usedKb, int? totalKb);
|
||||
typedef OnErrorCallback = void Function(String error, {int? errorCode});
|
||||
typedef OnContactNotFoundCallback = void Function(Uint8List? contactPublicKey);
|
||||
typedef OnChannelInfoCallback = void Function(int channelIdx, String channelName);
|
||||
typedef OnMessageEchoDetectedCallback = void Function(String messageId, int echoCount, int snrRaw, int rssiDbm);
|
||||
typedef OnChannelInfoCallback =
|
||||
void Function(int channelIdx, String channelName);
|
||||
typedef OnMessageEchoDetectedCallback =
|
||||
void Function(String messageId, int echoCount, int snrRaw, int rssiDbm);
|
||||
|
||||
/// Processes incoming responses from the BLE device
|
||||
class BleResponseHandler {
|
||||
@@ -118,12 +132,18 @@ class BleResponseHandler {
|
||||
final responseCode = reader.readByte();
|
||||
|
||||
// Get opcode name for logging
|
||||
final opcodeName = MeshCoreOpcodeNames.getOpcodeName(responseCode, isTx: false);
|
||||
final opcodeHex = '0x${responseCode.toRadixString(16).padLeft(2, '0').toUpperCase()}';
|
||||
final opcodeName = MeshCoreOpcodeNames.getOpcodeName(
|
||||
responseCode,
|
||||
isTx: false,
|
||||
);
|
||||
final opcodeHex =
|
||||
'0x${responseCode.toRadixString(16).padLeft(2, '0').toUpperCase()}';
|
||||
|
||||
debugPrint('📥 [RX] Received: $opcodeName ($opcodeHex)');
|
||||
debugPrint(' Data size: ${data.length} bytes');
|
||||
debugPrint(' Hex: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||||
debugPrint(
|
||||
' Hex: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}',
|
||||
);
|
||||
debugPrint(' Payload: ${reader.remainingBytesCount} bytes');
|
||||
|
||||
// Log RX packet (before processing so we capture everything)
|
||||
@@ -254,7 +274,9 @@ class BleResponseHandler {
|
||||
try {
|
||||
final contact = FrameParser.parseContact(reader);
|
||||
debugPrint(' ✅ [Contact] Parsed successfully: ${contact.advName}');
|
||||
debugPrint(' outPathLen: ${contact.outPathLen} (${contact.pathDescription})');
|
||||
debugPrint(
|
||||
' outPathLen: ${contact.outPathLen} (${contact.pathDescription})',
|
||||
);
|
||||
_pendingContacts.add(contact);
|
||||
onContactReceived?.call(contact);
|
||||
} catch (e) {
|
||||
@@ -416,7 +438,9 @@ class BleResponseHandler {
|
||||
/// Handle LogRxData push - includes extensive decoding logic
|
||||
void _handleLogRxData(BufferReader reader) {
|
||||
try {
|
||||
debugPrint(' [LogRxData] Parsing log rx data from over-the-air packet...');
|
||||
debugPrint(
|
||||
' [LogRxData] Parsing log rx data from over-the-air packet...',
|
||||
);
|
||||
final data = reader.readRemainingBytes();
|
||||
|
||||
if (data.length < 2) {
|
||||
@@ -445,25 +469,35 @@ class BleResponseHandler {
|
||||
final payloadType = (header >> 2) & 0x0F;
|
||||
final pathLen = rawPacketData[1];
|
||||
|
||||
debugPrint(' Packet type: 0x${payloadType.toRadixString(16).padLeft(2, '0')}');
|
||||
debugPrint(
|
||||
' Packet type: 0x${payloadType.toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
|
||||
if (pathLen > 0 && rawPacketData.length >= 2 + pathLen) {
|
||||
final path = rawPacketData.sublist(2, 2 + pathLen);
|
||||
final pathStr = path.map((b) => '0x${b.toRadixString(16).padLeft(2, '0')}').join(' → ');
|
||||
final pathStr = path
|
||||
.map((b) => '0x${b.toRadixString(16).padLeft(2, '0')}')
|
||||
.join(' → ');
|
||||
debugPrint(' Path ($pathLen hops): $pathStr');
|
||||
|
||||
// Highlight multi-hop packets
|
||||
if (pathLen > 1) {
|
||||
debugPrint(' 🔄 MULTI-HOP PACKET! Original sender: 0x${path[0].toRadixString(16).padLeft(2, '0')}');
|
||||
debugPrint(
|
||||
' 🔄 MULTI-HOP PACKET! Original sender: 0x${path[0].toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
}
|
||||
|
||||
// Check if our node hash is in the path
|
||||
if (_ourNodeHash != null && path.contains(_ourNodeHash!)) {
|
||||
debugPrint(' ✅✅✅ ECHO DETECTED! Path contains our hash (0x${_ourNodeHash!.toRadixString(16).padLeft(2, '0')}) ✅✅✅');
|
||||
debugPrint(
|
||||
' ✅✅✅ ECHO DETECTED! Path contains our hash (0x${_ourNodeHash!.toRadixString(16).padLeft(2, '0')}) ✅✅✅',
|
||||
);
|
||||
if (path[0] == _ourNodeHash) {
|
||||
debugPrint(' 👉 WE are the original sender!');
|
||||
} else {
|
||||
debugPrint(' 👉 Original sender: 0x${path[0].toRadixString(16).padLeft(2, '0')}, WE sent it to the network');
|
||||
debugPrint(
|
||||
' 👉 Original sender: 0x${path[0].toRadixString(16).padLeft(2, '0')}, WE sent it to the network',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(' ℹ️ Does NOT contain our hash (not our message)');
|
||||
@@ -526,7 +560,11 @@ class BleResponseHandler {
|
||||
hash = hash & 0xFFFFFFFF; // Keep 32-bit
|
||||
}
|
||||
if (packet.length > 16) {
|
||||
for (int i = packet.length ~/ 2; i < packet.length ~/ 2 + 8 && i < packet.length; i++) {
|
||||
for (
|
||||
int i = packet.length ~/ 2;
|
||||
i < packet.length ~/ 2 + 8 && i < packet.length;
|
||||
i++
|
||||
) {
|
||||
hash = ((hash << 5) - hash) + packet[i];
|
||||
hash = hash & 0xFFFFFFFF;
|
||||
}
|
||||
@@ -543,7 +581,9 @@ class BleResponseHandler {
|
||||
/// Check if received packet is an echo of a sent message
|
||||
void _checkForEcho(Uint8List rawPacket, int snrRaw, int rssiDbm) {
|
||||
try {
|
||||
debugPrint(' 🔍 [Echo] _checkForEcho called, packet size: ${rawPacket.length} bytes');
|
||||
debugPrint(
|
||||
' 🔍 [Echo] _checkForEcho called, packet size: ${rawPacket.length} bytes',
|
||||
);
|
||||
|
||||
// Need at least header + path_len
|
||||
if (rawPacket.length < 2) {
|
||||
@@ -553,7 +593,9 @@ class BleResponseHandler {
|
||||
|
||||
final header = rawPacket[0];
|
||||
final payloadType = (header >> 2) & 0x0F;
|
||||
debugPrint(' 🔍 [Echo] Payload type: 0x${payloadType.toRadixString(16).padLeft(2, '0')}');
|
||||
debugPrint(
|
||||
' 🔍 [Echo] Payload type: 0x${payloadType.toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
if (payloadType != 0x05) {
|
||||
debugPrint(' ⚠️ [Echo] Not GRP_TXT, ignoring');
|
||||
return; // Only track GRP_TXT
|
||||
@@ -568,10 +610,13 @@ class BleResponseHandler {
|
||||
|
||||
// Extract path for unique echo tracking
|
||||
final path = rawPacket.sublist(2, 2 + pathLen);
|
||||
final pathSignature = path.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||
final pathSignature = path
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join(':');
|
||||
|
||||
// Check if our node hash is in the path (meaning this is our message being rebroadcast)
|
||||
final containsOurHash = _ourNodeHash != null && path.contains(_ourNodeHash!);
|
||||
final containsOurHash =
|
||||
_ourNodeHash != null && path.contains(_ourNodeHash!);
|
||||
if (!containsOurHash) {
|
||||
// This packet doesn't have our hash in the path, so it's not our message
|
||||
return;
|
||||
@@ -599,9 +644,16 @@ class BleResponseHandler {
|
||||
debugPrint(' Unique paths: ${tracker.uniqueEchoPaths.length}');
|
||||
|
||||
// Notify callback
|
||||
onMessageEchoDetected?.call(tracker.messageId, tracker.echoCount, snrRaw, rssiDbm);
|
||||
onMessageEchoDetected?.call(
|
||||
tracker.messageId,
|
||||
tracker.echoCount,
|
||||
snrRaw,
|
||||
rssiDbm,
|
||||
);
|
||||
} else {
|
||||
debugPrint(' ♻️ [Echo] Duplicate path (already counted): $pathSignature');
|
||||
debugPrint(
|
||||
' ♻️ [Echo] Duplicate path (already counted): $pathSignature',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +683,9 @@ class BleResponseHandler {
|
||||
|
||||
// Store by message ID temporarily
|
||||
_sentMessageTrackers[messageId] = tracker;
|
||||
debugPrint(' 📤 [Echo] Tracking message $messageId (will match any GRP_TXT within 10000ms)');
|
||||
debugPrint(
|
||||
' 📤 [Echo] Tracking message $messageId (will match any GRP_TXT within 10000ms)',
|
||||
);
|
||||
debugPrint(' 📊 [Echo] Total trackers: ${_sentMessageTrackers.length}');
|
||||
|
||||
// Cleanup if too many trackers
|
||||
@@ -649,8 +703,12 @@ class BleResponseHandler {
|
||||
/// Set our node hash for packet identification
|
||||
void setOurNodeHash(int nodeHash) {
|
||||
_ourNodeHash = nodeHash;
|
||||
debugPrint(' 🔑 [Echo] Our node hash set to: 0x${nodeHash.toRadixString(16).padLeft(2, '0')}');
|
||||
debugPrint(' ℹ️ [Echo] Will track packets containing our hash in the path');
|
||||
debugPrint(
|
||||
' 🔑 [Echo] Our node hash set to: 0x${nodeHash.toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
debugPrint(
|
||||
' ℹ️ [Echo] Will track packets containing our hash in the path',
|
||||
);
|
||||
}
|
||||
|
||||
/// Associate a captured packet with a sent message
|
||||
@@ -666,7 +724,9 @@ class BleResponseHandler {
|
||||
/// [3+] = rest of path + encrypted payload
|
||||
void _associatePacketWithSentMessage(Uint8List rawPacket) {
|
||||
try {
|
||||
debugPrint(' 🔍 [Echo] _associatePacketWithSentMessage called, packet size: ${rawPacket.length}');
|
||||
debugPrint(
|
||||
' 🔍 [Echo] _associatePacketWithSentMessage called, packet size: ${rawPacket.length}',
|
||||
);
|
||||
|
||||
// Need at least 3 bytes: header + path_len + first path byte
|
||||
if (rawPacket.length < 3) {
|
||||
@@ -677,8 +737,11 @@ class BleResponseHandler {
|
||||
// Check if this is a GRP_TXT packet (payload type = 0x05)
|
||||
final header = rawPacket[0];
|
||||
final payloadType = (header >> 2) & 0x0F;
|
||||
debugPrint(' 🔍 [Echo] Association check - Payload type: 0x${payloadType.toRadixString(16).padLeft(2, '0')}');
|
||||
if (payloadType != 0x05) { // Not a group message
|
||||
debugPrint(
|
||||
' 🔍 [Echo] Association check - Payload type: 0x${payloadType.toRadixString(16).padLeft(2, '0')}',
|
||||
);
|
||||
if (payloadType != 0x05) {
|
||||
// Not a group message
|
||||
debugPrint(' ⚠️ [Echo] Not GRP_TXT, skipping association');
|
||||
return;
|
||||
}
|
||||
@@ -694,16 +757,21 @@ class BleResponseHandler {
|
||||
|
||||
// Extract the path from the packet for unique echo tracking
|
||||
final path = rawPacket.sublist(2, 2 + pathLen);
|
||||
final pathSignature = path.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||
final pathSignature = path
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join(':');
|
||||
|
||||
// Check if our node hash is in the path (meaning this is our message being rebroadcast)
|
||||
final containsOurHash = _ourNodeHash != null && path.contains(_ourNodeHash!);
|
||||
final containsOurHash =
|
||||
_ourNodeHash != null && path.contains(_ourNodeHash!);
|
||||
if (!containsOurHash) {
|
||||
// This packet doesn't have our hash in the path, so it's not our message
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(' ✅ [Echo] Packet contains our hash (0x${_ourNodeHash!.toRadixString(16).padLeft(2, '0')}) in path: $pathSignature');
|
||||
debugPrint(
|
||||
' ✅ [Echo] Packet contains our hash (0x${_ourNodeHash!.toRadixString(16).padLeft(2, '0')}) in path: $pathSignature',
|
||||
);
|
||||
|
||||
// Extract encrypted payload (everything after path)
|
||||
final payloadStart = 2 + pathLen;
|
||||
@@ -754,13 +822,17 @@ class BleResponseHandler {
|
||||
|
||||
/// Remove expired trackers
|
||||
void _cleanupExpiredTrackers() {
|
||||
final expiredCount = _sentMessageTrackers.values.where((t) => t.isExpired).length;
|
||||
final expiredCount = _sentMessageTrackers.values
|
||||
.where((t) => t.isExpired)
|
||||
.length;
|
||||
if (expiredCount > 0) {
|
||||
debugPrint(' 🧹 [Echo] Cleaning up $expiredCount expired tracker(s)');
|
||||
}
|
||||
_sentMessageTrackers.removeWhere((key, tracker) {
|
||||
if (tracker.isExpired && tracker.packetHashHex == 'pending') {
|
||||
debugPrint(' ⏱️ [Echo] Tracker expired without capturing: ${tracker.messageId}');
|
||||
debugPrint(
|
||||
' ⏱️ [Echo] Tracker expired without capturing: ${tracker.messageId}',
|
||||
);
|
||||
}
|
||||
return tracker.isExpired;
|
||||
});
|
||||
@@ -774,7 +846,9 @@ class BleResponseHandler {
|
||||
final sortedEntries = _sentMessageTrackers.entries.toList()
|
||||
..sort((a, b) => a.value.sentTime.compareTo(b.value.sentTime));
|
||||
|
||||
final toRemove = sortedEntries.take(_sentMessageTrackers.length - _maxTrackers);
|
||||
final toRemove = sortedEntries.take(
|
||||
_sentMessageTrackers.length - _maxTrackers,
|
||||
);
|
||||
for (final entry in toRemove) {
|
||||
_sentMessageTrackers.remove(entry.key);
|
||||
}
|
||||
@@ -787,7 +861,9 @@ class BleResponseHandler {
|
||||
try {
|
||||
final contact = FrameParser.parseContact(reader);
|
||||
debugPrint(' ✅ [NewAdvert] Parsed successfully: ${contact.advName}');
|
||||
debugPrint(' outPathLen: ${contact.outPathLen} (${contact.pathDescription})');
|
||||
debugPrint(
|
||||
' outPathLen: ${contact.outPathLen} (${contact.pathDescription})',
|
||||
);
|
||||
onContactReceived?.call(contact);
|
||||
} catch (e) {
|
||||
debugPrint(' ❌ [NewAdvert] Parsing error: $e');
|
||||
@@ -937,7 +1013,7 @@ class BleResponseHandler {
|
||||
final channelIdx = info['channelIdx'] as int;
|
||||
final channelName = info['channelName'] as String;
|
||||
|
||||
debugPrint(' ✅ [ChannelInfo] Channel $channelIdx: "${channelName}"');
|
||||
debugPrint(' ✅ [ChannelInfo] Channel $channelIdx: "$channelName"');
|
||||
onChannelInfoReceived?.call(channelIdx, channelName);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -956,14 +1032,17 @@ class BleResponseHandler {
|
||||
|
||||
// Complete any pending ACK command with error
|
||||
_commandQueue?.completeCommandWithError(
|
||||
MeshCoreConstants.respOk, // Command was expecting OK, got ERR
|
||||
MeshCoreConstants.respOk, // Command was expecting OK, got ERR
|
||||
errorMsg,
|
||||
errorCode: errorCode,
|
||||
);
|
||||
|
||||
// Special handling for ERR_CODE_NOT_FOUND (2) - contact not in radio
|
||||
if (errorCode == 2) { // ERR_CODE_NOT_FOUND
|
||||
debugPrint(' ⚠️ [Error] Contact not found in radio - attempting auto-recovery');
|
||||
if (errorCode == 2) {
|
||||
// ERR_CODE_NOT_FOUND
|
||||
debugPrint(
|
||||
' ⚠️ [Error] Contact not found in radio - attempting auto-recovery',
|
||||
);
|
||||
onContactNotFound?.call(_lastContactPublicKey);
|
||||
}
|
||||
|
||||
@@ -980,14 +1059,20 @@ class BleResponseHandler {
|
||||
}
|
||||
|
||||
/// Log a packet
|
||||
void _logPacket(Uint8List data, PacketDirection direction, {int? responseCode}) {
|
||||
_packetLogs.add(BlePacketLog(
|
||||
timestamp: DateTime.now(),
|
||||
rawData: data,
|
||||
direction: direction,
|
||||
responseCode: responseCode,
|
||||
description: _getPacketDescription(responseCode),
|
||||
));
|
||||
void _logPacket(
|
||||
Uint8List data,
|
||||
PacketDirection direction, {
|
||||
int? responseCode,
|
||||
}) {
|
||||
_packetLogs.add(
|
||||
BlePacketLog(
|
||||
timestamp: DateTime.now(),
|
||||
rawData: data,
|
||||
direction: direction,
|
||||
responseCode: responseCode,
|
||||
description: _getPacketDescription(responseCode),
|
||||
),
|
||||
);
|
||||
|
||||
if (_packetLogs.length > _maxLogSize) {
|
||||
_packetLogs.removeAt(0);
|
||||
|
||||
@@ -12,7 +12,9 @@ class CayenneLppParser {
|
||||
static ContactTelemetry parse(Uint8List data) {
|
||||
debugPrint(' [CayenneLPP] Parsing LPP data...');
|
||||
debugPrint(' Data length: ${data.length} bytes');
|
||||
debugPrint(' Data (hex): ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||||
debugPrint(
|
||||
' Data (hex): ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}',
|
||||
);
|
||||
|
||||
final reader = BufferReader(data);
|
||||
|
||||
@@ -28,13 +30,17 @@ class CayenneLppParser {
|
||||
while (reader.hasRemaining) {
|
||||
try {
|
||||
fieldCount++;
|
||||
debugPrint(' [Field $fieldCount] Position: ${data.length - reader.remainingBytesCount}');
|
||||
debugPrint(
|
||||
' [Field $fieldCount] Position: ${data.length - reader.remainingBytesCount}',
|
||||
);
|
||||
|
||||
final channel = reader.readByte();
|
||||
debugPrint(' Channel: $channel');
|
||||
|
||||
final type = reader.readByte();
|
||||
debugPrint(' Type: $type (0x${type.toRadixString(16).padLeft(2, '0')})');
|
||||
debugPrint(
|
||||
' Type: $type (0x${type.toRadixString(16).padLeft(2, '0')})',
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case MeshCoreConstants.lppDigitalInput:
|
||||
@@ -59,7 +65,9 @@ class CayenneLppParser {
|
||||
if (channel == 0 || channel == 1) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
debugPrint(' → Battery: ${batteryPercentage?.toStringAsFixed(1)}% (${batteryMilliVolts?.toStringAsFixed(0)}mV)');
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -87,14 +95,16 @@ class CayenneLppParser {
|
||||
final rawValue = reader.readInt16BE();
|
||||
temperature = rawValue / 10.0;
|
||||
debugPrint(' Temperature (raw): $rawValue');
|
||||
debugPrint(' Temperature: ${temperature?.toStringAsFixed(1)}°C');
|
||||
debugPrint(
|
||||
' Temperature: ${temperature.toStringAsFixed(1)}°C',
|
||||
);
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppHumiditySensor:
|
||||
final rawValue = reader.readByte();
|
||||
humidity = rawValue / 2.0;
|
||||
debugPrint(' Humidity (raw): $rawValue');
|
||||
debugPrint(' Humidity: ${humidity?.toStringAsFixed(1)}%');
|
||||
debugPrint(' Humidity: ${humidity.toStringAsFixed(1)}%');
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppAccelerometer:
|
||||
@@ -102,14 +112,18 @@ class CayenneLppParser {
|
||||
final y = reader.readInt16BE() / 1000.0;
|
||||
final z = reader.readInt16BE() / 1000.0;
|
||||
debugPrint(' Accelerometer: x=$x, y=$y, z=$z');
|
||||
extraSensorData['accelerometer_$channel'] = {'x': x, 'y': y, 'z': z};
|
||||
extraSensorData['accelerometer_$channel'] = {
|
||||
'x': x,
|
||||
'y': y,
|
||||
'z': z,
|
||||
};
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppBarometer:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
pressure = rawValue / 10.0;
|
||||
debugPrint(' Barometer (raw): $rawValue');
|
||||
debugPrint(' Barometer: ${pressure?.toStringAsFixed(1)} hPa');
|
||||
debugPrint(' Barometer: ${pressure.toStringAsFixed(1)} hPa');
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppVoltageSensor:
|
||||
@@ -120,7 +134,9 @@ class CayenneLppParser {
|
||||
// Treat voltage sensor as battery reading
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
debugPrint(' → Battery: ${batteryPercentage?.toStringAsFixed(1)}% (${batteryMilliVolts?.toStringAsFixed(0)}mV)');
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppGyrometer:
|
||||
@@ -138,14 +154,18 @@ class CayenneLppParser {
|
||||
final lat = rawLat / 1000000.0;
|
||||
final lon = rawLon / 1000000.0;
|
||||
final alt = rawAlt / 100.0;
|
||||
debugPrint(' GPS Location (raw): lat=$rawLat, lon=$rawLon, alt=$rawAlt');
|
||||
debugPrint(' GPS Location: ${lat}°, ${lon}°, altitude=${alt}m');
|
||||
debugPrint(
|
||||
' GPS Location (raw): lat=$rawLat, lon=$rawLon, alt=$rawAlt',
|
||||
);
|
||||
debugPrint(' GPS Location: $lat°, $lon°, altitude=${alt}m');
|
||||
gpsLocation = LatLng(lat, lon);
|
||||
extraSensorData['altitude_$channel'] = alt;
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint(' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes');
|
||||
debugPrint(
|
||||
' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes',
|
||||
);
|
||||
// Unknown type, skip remaining to avoid parsing errors
|
||||
reader.skip(reader.remainingBytesCount);
|
||||
break;
|
||||
@@ -159,9 +179,15 @@ class CayenneLppParser {
|
||||
|
||||
debugPrint(' Parsed $fieldCount fields');
|
||||
debugPrint(' ✅ [CayenneLPP] Parsing complete');
|
||||
debugPrint(' GPS: ${gpsLocation != null ? '${gpsLocation.latitude}°, ${gpsLocation.longitude}°' : 'none'}');
|
||||
debugPrint(' Battery: ${batteryPercentage != null ? '${batteryPercentage.toStringAsFixed(1)}%' : 'none'}');
|
||||
debugPrint(' Temperature: ${temperature != null ? '${temperature.toStringAsFixed(1)}°C' : 'none'}');
|
||||
debugPrint(
|
||||
' GPS: ${gpsLocation != null ? '${gpsLocation.latitude}°, ${gpsLocation.longitude}°' : 'none'}',
|
||||
);
|
||||
debugPrint(
|
||||
' Battery: ${batteryPercentage != null ? '${batteryPercentage.toStringAsFixed(1)}%' : 'none'}',
|
||||
);
|
||||
debugPrint(
|
||||
' Temperature: ${temperature != null ? '${temperature.toStringAsFixed(1)}°C' : 'none'}',
|
||||
);
|
||||
|
||||
// IMPORTANT: Cayenne LPP format does NOT include a timestamp field.
|
||||
// We use DateTime.now() as the timestamp, which represents when the data
|
||||
@@ -173,7 +199,9 @@ class CayenneLppParser {
|
||||
// - The actual age of the telemetry data cannot be determined from the LPP format
|
||||
// - Devices may cache telemetry for hours and send it later when requested
|
||||
final parseTimestamp = DateTime.now();
|
||||
debugPrint(' Timestamp: $parseTimestamp (parse time, NOT device collection time)');
|
||||
debugPrint(
|
||||
' Timestamp: $parseTimestamp (parse time, NOT device collection time)',
|
||||
);
|
||||
|
||||
return ContactTelemetry(
|
||||
gpsLocation: gpsLocation,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -17,7 +16,9 @@ class ContactStorageService {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Convert contacts to JSON
|
||||
final jsonList = contacts.map((contact) => _contactToJson(contact)).toList();
|
||||
final jsonList = contacts
|
||||
.map((contact) => _contactToJson(contact))
|
||||
.toList();
|
||||
|
||||
// Limit to max stored contacts (keep most recent)
|
||||
final limitedList = jsonList.length > _maxStoredContacts
|
||||
@@ -27,7 +28,9 @@ class ContactStorageService {
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_contactsKey, jsonString);
|
||||
|
||||
debugPrint('✅ [ContactStorage] Saved ${limitedList.length} contacts to storage');
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error saving contacts: $e');
|
||||
}
|
||||
@@ -55,16 +58,23 @@ class ContactStorageService {
|
||||
// Filter out contacts with the excluded public key
|
||||
final filteredContacts = excludePublicKey != null
|
||||
? contacts.where((contact) {
|
||||
final matches = _publicKeysMatch(contact.publicKey, excludePublicKey);
|
||||
final matches = _publicKeysMatch(
|
||||
contact.publicKey,
|
||||
excludePublicKey,
|
||||
);
|
||||
if (matches) {
|
||||
debugPrint('ℹ️ [ContactStorage] Excluding contact with matching public key: ${contact.advName}');
|
||||
debugPrint(
|
||||
'ℹ️ [ContactStorage] Excluding contact with matching public key: ${contact.advName}',
|
||||
);
|
||||
}
|
||||
return !matches;
|
||||
}).toList()
|
||||
: contacts;
|
||||
|
||||
debugPrint('✅ [ContactStorage] Loaded ${filteredContacts.length} contacts from storage'
|
||||
'${excludePublicKey != null ? ' (${contacts.length - filteredContacts.length} excluded)' : ''}');
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Loaded ${filteredContacts.length} contacts from storage'
|
||||
'${excludePublicKey != null ? ' (${contacts.length - filteredContacts.length} excluded)' : ''}',
|
||||
);
|
||||
return filteredContacts;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error loading contacts: $e');
|
||||
@@ -99,11 +109,7 @@ class ContactStorageService {
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {
|
||||
'contactCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
}
|
||||
|
||||
final sizeBytes = jsonString.length;
|
||||
@@ -116,11 +122,7 @@ class ContactStorageService {
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error getting storage stats: $e');
|
||||
return {
|
||||
'contactCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +139,9 @@ class ContactStorageService {
|
||||
'advLat': contact.advLat,
|
||||
'advLon': contact.advLon,
|
||||
'lastMod': contact.lastMod,
|
||||
'telemetry': contact.telemetry != null ? _telemetryToJson(contact.telemetry!) : null,
|
||||
'telemetry': contact.telemetry != null
|
||||
? _telemetryToJson(contact.telemetry!)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,7 +149,9 @@ class ContactStorageService {
|
||||
Contact? _contactFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
publicKey: Uint8List.fromList(
|
||||
base64Decode(json['publicKey'] as String),
|
||||
),
|
||||
type: ContactType.fromValue(json['type'] as int),
|
||||
flags: json['flags'] as int,
|
||||
outPathLen: json['outPathLen'] as int,
|
||||
@@ -200,7 +206,8 @@ class ContactStorageService {
|
||||
humidity: json['humidity'] as double?,
|
||||
pressure: json['pressure'] as double?,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['timestampMillis'] as int),
|
||||
json['timestampMillis'] as int,
|
||||
),
|
||||
extraSensorData: json['extraSensorData'] as Map<String, dynamic>?,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -129,7 +129,7 @@ class MbtilesService {
|
||||
final mbtiles = MbTiles(mbtilesPath: file.path);
|
||||
|
||||
// Get metadata from MBTiles
|
||||
final metadata = await mbtiles.getMetadata();
|
||||
final metadata = mbtiles.getMetadata();
|
||||
|
||||
// Convert bounds object to string if available
|
||||
String? boundsStr;
|
||||
@@ -222,7 +222,7 @@ class MbtilesService {
|
||||
final mbtiles = MbTiles(mbtilesPath: file.path);
|
||||
|
||||
// Try to get metadata to check for compression hints
|
||||
final metadata = await mbtiles.getMetadata();
|
||||
final metadata = mbtiles.getMetadata();
|
||||
final format = metadata.format;
|
||||
|
||||
// For Geofabrik files, format is 'pbf' and data is gzipped
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -15,26 +14,41 @@ import 'meshcore_constants.dart';
|
||||
typedef OnContactCallback = void Function(Contact contact);
|
||||
typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
|
||||
typedef OnMessageCallback = void Function(Message message);
|
||||
typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData);
|
||||
typedef OnTelemetryCallback =
|
||||
void Function(Uint8List publicKey, Uint8List lppData);
|
||||
typedef OnSelfInfoCallback = void Function(Map<String, dynamic> selfInfo);
|
||||
typedef OnDeviceInfoCallback = void Function(Map<String, dynamic> deviceInfo);
|
||||
typedef OnNoMoreMessagesCallback = void Function();
|
||||
typedef OnMessageWaitingCallback = void Function();
|
||||
typedef OnLoginSuccessCallback = void Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag);
|
||||
typedef OnLoginSuccessCallback =
|
||||
void Function(
|
||||
Uint8List publicKeyPrefix,
|
||||
int permissions,
|
||||
bool isAdmin,
|
||||
int tag,
|
||||
);
|
||||
typedef OnLoginFailCallback = void Function(Uint8List publicKeyPrefix);
|
||||
typedef OnAdvertReceivedCallback = void Function(Uint8List publicKey);
|
||||
typedef OnPathUpdatedCallback = void Function(Uint8List publicKey);
|
||||
typedef OnMessageSentCallback = void Function(int expectedAckTag, int suggestedTimeoutMs, bool isFloodMode);
|
||||
typedef OnMessageDeliveredCallback = void Function(int ackCode, int roundTripTimeMs);
|
||||
typedef OnMessageEchoDetectedCallback = void Function(String messageId, int echoCount, int snrRaw, int rssiDbm);
|
||||
typedef OnStatusResponseCallback = void Function(Uint8List publicKeyPrefix, Uint8List statusData);
|
||||
typedef OnBinaryResponseCallback = void Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData);
|
||||
typedef OnBatteryAndStorageCallback = void Function(int millivolts, int? usedKb, int? totalKb);
|
||||
typedef OnMessageSentCallback =
|
||||
void Function(int expectedAckTag, int suggestedTimeoutMs, bool isFloodMode);
|
||||
typedef OnMessageDeliveredCallback =
|
||||
void Function(int ackCode, int roundTripTimeMs);
|
||||
typedef OnMessageEchoDetectedCallback =
|
||||
void Function(String messageId, int echoCount, int snrRaw, int rssiDbm);
|
||||
typedef OnStatusResponseCallback =
|
||||
void Function(Uint8List publicKeyPrefix, Uint8List statusData);
|
||||
typedef OnBinaryResponseCallback =
|
||||
void Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData);
|
||||
typedef OnBatteryAndStorageCallback =
|
||||
void Function(int millivolts, int? usedKb, int? totalKb);
|
||||
typedef OnErrorCallback = void Function(String error, {int? errorCode});
|
||||
typedef OnContactNotFoundCallback = void Function(Uint8List? contactPublicKey);
|
||||
typedef OnChannelInfoCallback = void Function(int channelIdx, String channelName);
|
||||
typedef OnChannelInfoCallback =
|
||||
void Function(int channelIdx, String channelName);
|
||||
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
||||
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
|
||||
typedef OnReconnectionAttemptCallback =
|
||||
void Function(int attemptNumber, int maxAttempts);
|
||||
typedef OnRssiUpdateCallback = void Function(int rssi);
|
||||
|
||||
/// MeshCore BLE Service - coordinates BLE communication components
|
||||
@@ -89,7 +103,9 @@ class MeshCoreBleService {
|
||||
onError?.call(error);
|
||||
};
|
||||
_connectionManager.onReconnectionAttempt = (attemptNumber, maxAttempts) {
|
||||
debugPrint('🔄 [Service] Reconnection attempt $attemptNumber/$maxAttempts');
|
||||
debugPrint(
|
||||
'🔄 [Service] Reconnection attempt $attemptNumber/$maxAttempts',
|
||||
);
|
||||
onReconnectionAttempt?.call(attemptNumber, maxAttempts);
|
||||
};
|
||||
_connectionManager.onRssiUpdate = (rssi) {
|
||||
@@ -136,9 +152,10 @@ class MeshCoreBleService {
|
||||
_responseHandler.onMessageWaiting = () {
|
||||
onMessageWaiting?.call();
|
||||
};
|
||||
_responseHandler.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) {
|
||||
onLoginSuccess?.call(publicKeyPrefix, permissions, isAdmin, tag);
|
||||
};
|
||||
_responseHandler.onLoginSuccess =
|
||||
(publicKeyPrefix, permissions, isAdmin, tag) {
|
||||
onLoginSuccess?.call(publicKeyPrefix, permissions, isAdmin, tag);
|
||||
};
|
||||
_responseHandler.onLoginFail = (publicKeyPrefix) {
|
||||
onLoginFail?.call(publicKeyPrefix);
|
||||
};
|
||||
@@ -148,15 +165,17 @@ class MeshCoreBleService {
|
||||
_responseHandler.onPathUpdated = (publicKey) {
|
||||
onPathUpdated?.call(publicKey);
|
||||
};
|
||||
_responseHandler.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) {
|
||||
onMessageSent?.call(expectedAckTag, suggestedTimeoutMs, isFloodMode);
|
||||
};
|
||||
_responseHandler.onMessageSent =
|
||||
(expectedAckTag, suggestedTimeoutMs, isFloodMode) {
|
||||
onMessageSent?.call(expectedAckTag, suggestedTimeoutMs, isFloodMode);
|
||||
};
|
||||
_responseHandler.onMessageDelivered = (ackCode, roundTripTimeMs) {
|
||||
onMessageDelivered?.call(ackCode, roundTripTimeMs);
|
||||
};
|
||||
_responseHandler.onMessageEchoDetected = (messageId, echoCount, snrRaw, rssiDbm) {
|
||||
onMessageEchoDetected?.call(messageId, echoCount, snrRaw, rssiDbm);
|
||||
};
|
||||
_responseHandler.onMessageEchoDetected =
|
||||
(messageId, echoCount, snrRaw, rssiDbm) {
|
||||
onMessageEchoDetected?.call(messageId, echoCount, snrRaw, rssiDbm);
|
||||
};
|
||||
_responseHandler.onStatusResponse = (publicKeyPrefix, statusData) {
|
||||
onStatusResponse?.call(publicKeyPrefix, statusData);
|
||||
};
|
||||
@@ -189,13 +208,18 @@ class MeshCoreBleService {
|
||||
int get txPacketCount => _commandSender.txPacketCount;
|
||||
List<BlePacketLog> get packetLogs {
|
||||
// Merge logs from both sender and handler
|
||||
final allLogs = [..._commandSender.packetLogs, ..._responseHandler.packetLogs];
|
||||
final allLogs = [
|
||||
..._commandSender.packetLogs,
|
||||
..._responseHandler.packetLogs,
|
||||
];
|
||||
allLogs.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
return allLogs;
|
||||
}
|
||||
|
||||
/// Scan for MeshCore devices
|
||||
Stream<ScanResult> scanForDevices({Duration timeout = const Duration(seconds: 10)}) {
|
||||
Stream<ScanResult> scanForDevices({
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) {
|
||||
return _connectionManager.scanForDevices(timeout: timeout);
|
||||
}
|
||||
|
||||
@@ -212,7 +236,9 @@ class MeshCoreBleService {
|
||||
|
||||
// Setup response handler with TX characteristic
|
||||
if (_connectionManager.txCharacteristic != null) {
|
||||
_responseHandler.subscribeToNotifications(_connectionManager.txCharacteristic!);
|
||||
_responseHandler.subscribeToNotifications(
|
||||
_connectionManager.txCharacteristic!,
|
||||
);
|
||||
}
|
||||
|
||||
// Send initial device query and wait for responses
|
||||
@@ -240,20 +266,26 @@ class MeshCoreBleService {
|
||||
Future<void> _sendDeviceQuery() async {
|
||||
// STEP 1: Send device query FIRST to get device capabilities
|
||||
// This is the first command to send per protocol documentation
|
||||
debugPrint('🔍 [Service] Querying device information (CMD_DEVICE_QUERY)...');
|
||||
final deviceInfo = await _commandSender.writeDataAndWaitForResponse<Map<String, dynamic>>(
|
||||
FrameBuilder.buildDeviceQuery(),
|
||||
MeshCoreConstants.respDeviceInfo,
|
||||
debugPrint(
|
||||
'🔍 [Service] Querying device information (CMD_DEVICE_QUERY)...',
|
||||
);
|
||||
final deviceInfo = await _commandSender
|
||||
.writeDataAndWaitForResponse<Map<String, dynamic>>(
|
||||
FrameBuilder.buildDeviceQuery(),
|
||||
MeshCoreConstants.respDeviceInfo,
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [Service] Device info received: firmware=${deviceInfo['firmwareVersion']}',
|
||||
);
|
||||
debugPrint('✅ [Service] Device info received: firmware=${deviceInfo['firmwareVersion']}');
|
||||
|
||||
// STEP 2: Send app start to initialize the app session
|
||||
// This is the first command after connection per protocol documentation
|
||||
debugPrint('🚀 [Service] Sending app start (CMD_APP_START)...');
|
||||
final selfInfo = await _commandSender.writeDataAndWaitForResponse<Map<String, dynamic>>(
|
||||
FrameBuilder.buildAppStart(),
|
||||
MeshCoreConstants.respSelfInfo,
|
||||
);
|
||||
final selfInfo = await _commandSender
|
||||
.writeDataAndWaitForResponse<Map<String, dynamic>>(
|
||||
FrameBuilder.buildAppStart(),
|
||||
MeshCoreConstants.respSelfInfo,
|
||||
);
|
||||
debugPrint('✅ [Service] Self info received: node initialized');
|
||||
|
||||
// STEP 3: Set device clock AFTER initialization
|
||||
@@ -278,7 +310,9 @@ class MeshCoreBleService {
|
||||
Future<void> addOrUpdateContact(Contact contact) async {
|
||||
debugPrint('📝 [BLE] Adding/updating contact on companion radio:');
|
||||
debugPrint(' Name: ${contact.advName}');
|
||||
debugPrint(' Public key prefix: ${contact.publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
debugPrint(
|
||||
' Public key prefix: ${contact.publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||
);
|
||||
debugPrint(' Type: ${contact.type} (${contact.type.value})');
|
||||
|
||||
await _commandSender.writeData(FrameBuilder.buildAddUpdateContact(contact));
|
||||
@@ -300,18 +334,22 @@ class MeshCoreBleService {
|
||||
// Track the last contact for auto-recovery if contact not found
|
||||
_responseHandler.setLastContactPublicKey(contactPublicKey);
|
||||
|
||||
await _commandSender.writeData(FrameBuilder.buildSendTxtMsg(
|
||||
contactPublicKey: contactPublicKey,
|
||||
text: text,
|
||||
textType: textType,
|
||||
attempt: attempt,
|
||||
));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendTxtMsg(
|
||||
contactPublicKey: contactPublicKey,
|
||||
text: text,
|
||||
textType: textType,
|
||||
attempt: attempt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Send flood-mode text message to channel
|
||||
/// Track a sent channel message for echo detection
|
||||
void trackSentChannelMessage(String messageId) {
|
||||
debugPrint('🔵 [MeshCoreBleService] trackSentChannelMessage called for: $messageId');
|
||||
debugPrint(
|
||||
'🔵 [MeshCoreBleService] trackSentChannelMessage called for: $messageId',
|
||||
);
|
||||
_responseHandler.trackSentMessage(messageId, null);
|
||||
}
|
||||
|
||||
@@ -333,20 +371,24 @@ class MeshCoreBleService {
|
||||
|
||||
// Channel messages use fire-and-forget (no ACK expected)
|
||||
// The firmware responds with RESP_CODE_OK but we don't wait for it
|
||||
await _commandSender.writeData(FrameBuilder.buildSendChannelTxtMsg(
|
||||
channelIdx: channelIdx,
|
||||
text: text,
|
||||
textType: textType,
|
||||
));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendChannelTxtMsg(
|
||||
channelIdx: channelIdx,
|
||||
text: text,
|
||||
textType: textType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Request telemetry from contact (deprecated)
|
||||
@Deprecated('Use sendBinaryRequest() instead for better functionality')
|
||||
Future<void> requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async {
|
||||
await _commandSender.writeData(FrameBuilder.buildSendTelemetryReq(
|
||||
contactPublicKey,
|
||||
zeroHop: zeroHop,
|
||||
));
|
||||
Future<void> requestTelemetry(
|
||||
Uint8List contactPublicKey, {
|
||||
bool zeroHop = false,
|
||||
}) async {
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendTelemetryReq(contactPublicKey, zeroHop: zeroHop),
|
||||
);
|
||||
}
|
||||
|
||||
/// Send binary request to contact
|
||||
@@ -354,10 +396,12 @@ class MeshCoreBleService {
|
||||
required Uint8List contactPublicKey,
|
||||
required Uint8List requestData,
|
||||
}) async {
|
||||
await _commandSender.writeData(FrameBuilder.buildSendBinaryReq(
|
||||
contactPublicKey: contactPublicKey,
|
||||
requestData: requestData,
|
||||
));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendBinaryReq(
|
||||
contactPublicKey: contactPublicKey,
|
||||
requestData: requestData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get battery voltage and storage information
|
||||
@@ -388,12 +432,16 @@ class MeshCoreBleService {
|
||||
|
||||
/// Send self advertisement packet to mesh network
|
||||
Future<void> sendSelfAdvert({bool floodMode = true}) async {
|
||||
await _commandSender.writeData(FrameBuilder.buildSendSelfAdvert(floodMode: floodMode));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendSelfAdvert(floodMode: floodMode),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set advertised name
|
||||
Future<void> setAdvertName(String name) async {
|
||||
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetAdvertName(name));
|
||||
await _commandSender.writeDataAndWaitForAck(
|
||||
FrameBuilder.buildSetAdvertName(name),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set advertised latitude and longitude
|
||||
@@ -402,10 +450,12 @@ class MeshCoreBleService {
|
||||
required double longitude,
|
||||
}) async {
|
||||
// This command returns OK (0x00) response, so wait for acknowledgment
|
||||
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetAdvertLatLon(
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
));
|
||||
await _commandSender.writeDataAndWaitForAck(
|
||||
FrameBuilder.buildSetAdvertLatLon(
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set radio parameters
|
||||
@@ -415,17 +465,21 @@ class MeshCoreBleService {
|
||||
required int spreadingFactor,
|
||||
required int codingRate,
|
||||
}) async {
|
||||
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetRadioParams(
|
||||
frequency: frequency,
|
||||
bandwidth: bandwidth,
|
||||
spreadingFactor: spreadingFactor,
|
||||
codingRate: codingRate,
|
||||
));
|
||||
await _commandSender.writeDataAndWaitForAck(
|
||||
FrameBuilder.buildSetRadioParams(
|
||||
frequency: frequency,
|
||||
bandwidth: bandwidth,
|
||||
spreadingFactor: spreadingFactor,
|
||||
codingRate: codingRate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set transmit power
|
||||
Future<void> setTxPower(int powerDbm) async {
|
||||
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetTxPower(powerDbm));
|
||||
await _commandSender.writeDataAndWaitForAck(
|
||||
FrameBuilder.buildSetTxPower(powerDbm),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set other parameters
|
||||
@@ -435,12 +489,14 @@ class MeshCoreBleService {
|
||||
required int advertLocationPolicy,
|
||||
int multiAcks = 0,
|
||||
}) async {
|
||||
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetOtherParams(
|
||||
manualAddContacts: manualAddContacts,
|
||||
telemetryModes: telemetryModes,
|
||||
advertLocationPolicy: advertLocationPolicy,
|
||||
multiAcks: multiAcks,
|
||||
));
|
||||
await _commandSender.writeDataAndWaitForAck(
|
||||
FrameBuilder.buildSetOtherParams(
|
||||
manualAddContacts: manualAddContacts,
|
||||
telemetryModes: telemetryModes,
|
||||
advertLocationPolicy: advertLocationPolicy,
|
||||
multiAcks: multiAcks,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Send login request to room or repeater
|
||||
@@ -453,37 +509,55 @@ class MeshCoreBleService {
|
||||
}
|
||||
|
||||
debugPrint('🔐 [BLE] Preparing login request:');
|
||||
debugPrint(' Room public key prefix: ${roomPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
debugPrint(' Password: ${"*" * password.length} (${password.length} chars)');
|
||||
debugPrint(
|
||||
' Room public key prefix: ${roomPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||
);
|
||||
debugPrint(
|
||||
' Password: ${"*" * password.length} (${password.length} chars)',
|
||||
);
|
||||
|
||||
await _commandSender.writeData(FrameBuilder.buildSendLogin(
|
||||
roomPublicKey: roomPublicKey,
|
||||
password: password,
|
||||
));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendLogin(
|
||||
roomPublicKey: roomPublicKey,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Send status request to repeater or sensor node
|
||||
Future<void> sendStatusRequest(Uint8List contactPublicKey) async {
|
||||
debugPrint('📊 [BLE] Preparing status request:');
|
||||
debugPrint(' Target node public key prefix: ${contactPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
debugPrint(
|
||||
' Target node public key prefix: ${contactPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||
);
|
||||
|
||||
await _commandSender.writeData(FrameBuilder.buildSendStatusReq(contactPublicKey));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildSendStatusReq(contactPublicKey),
|
||||
);
|
||||
}
|
||||
|
||||
/// Reset path for a contact - forces next message to flood and re-learn route
|
||||
Future<void> resetPath(Uint8List contactPublicKey) async {
|
||||
debugPrint('🔄 [BLE] Resetting path for contact:');
|
||||
debugPrint(' Contact public key prefix: ${contactPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
debugPrint(
|
||||
' Contact public key prefix: ${contactPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||
);
|
||||
|
||||
await _commandSender.writeData(FrameBuilder.buildResetPath(contactPublicKey));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildResetPath(contactPublicKey),
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove a contact from the companion radio
|
||||
Future<void> removeContact(Uint8List contactPublicKey) async {
|
||||
debugPrint('🗑️ [BLE] Removing contact from companion radio:');
|
||||
debugPrint(' Public key prefix: ${contactPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
debugPrint(
|
||||
' Public key prefix: ${contactPublicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}',
|
||||
);
|
||||
|
||||
await _commandSender.writeData(FrameBuilder.buildRemoveContact(contactPublicKey));
|
||||
await _commandSender.writeData(
|
||||
FrameBuilder.buildRemoveContact(contactPublicKey),
|
||||
);
|
||||
debugPrint('✅ [BLE] CMD_REMOVE_CONTACT sent');
|
||||
}
|
||||
|
||||
@@ -506,11 +580,13 @@ class MeshCoreBleService {
|
||||
debugPrint(' Channel name: $channelName');
|
||||
debugPrint(' Secret length: ${secret.length} bytes');
|
||||
|
||||
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetChannel(
|
||||
channelIdx: channelIdx,
|
||||
channelName: channelName,
|
||||
secret: secret,
|
||||
));
|
||||
await _commandSender.writeDataAndWaitForAck(
|
||||
FrameBuilder.buildSetChannel(
|
||||
channelIdx: channelIdx,
|
||||
channelName: channelName,
|
||||
secret: secret,
|
||||
),
|
||||
);
|
||||
debugPrint('✅ [BLE] CMD_SET_CHANNEL sent successfully');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/message.dart';
|
||||
@@ -27,7 +26,9 @@ class MessageStorageService {
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_messagesKey, jsonString);
|
||||
|
||||
debugPrint('✅ [MessageStorage] Saved ${limitedList.length} messages to storage');
|
||||
debugPrint(
|
||||
'✅ [MessageStorage] Saved ${limitedList.length} messages to storage',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error saving messages: $e');
|
||||
}
|
||||
@@ -51,7 +52,9 @@ class MessageStorageService {
|
||||
.cast<Message>()
|
||||
.toList();
|
||||
|
||||
debugPrint('✅ [MessageStorage] Loaded ${messages.length} messages from storage');
|
||||
debugPrint(
|
||||
'✅ [MessageStorage] Loaded ${messages.length} messages from storage',
|
||||
);
|
||||
return messages;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error loading messages: $e');
|
||||
@@ -77,11 +80,7 @@ class MessageStorageService {
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {
|
||||
'messageCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
return {'messageCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
}
|
||||
|
||||
final sizeBytes = jsonString.length;
|
||||
@@ -94,11 +93,7 @@ class MessageStorageService {
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error getting storage stats: $e');
|
||||
return {
|
||||
'messageCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
return {'messageCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +139,8 @@ class MessageStorageService {
|
||||
),
|
||||
senderPublicKeyPrefix: json['senderPublicKeyPrefix'] != null
|
||||
? Uint8List.fromList(
|
||||
base64Decode(json['senderPublicKeyPrefix'] as String))
|
||||
base64Decode(json['senderPublicKeyPrefix'] as String),
|
||||
)
|
||||
: null,
|
||||
channelIdx: json['channelIdx'] as int?,
|
||||
pathLen: json['pathLen'] as int,
|
||||
@@ -158,12 +154,13 @@ class MessageStorageService {
|
||||
orElse: () => SarMarkerType.unknown,
|
||||
)
|
||||
: null,
|
||||
sarGpsCoordinates: json['sarGpsLat'] != null &&
|
||||
json['sarGpsLon'] != null
|
||||
sarGpsCoordinates:
|
||||
json['sarGpsLat'] != null && json['sarGpsLon'] != null
|
||||
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)
|
||||
: null,
|
||||
receivedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['receivedAtMillis'] as int),
|
||||
json['receivedAtMillis'] as int,
|
||||
),
|
||||
senderName: json['senderName'] as String?,
|
||||
deliveryStatus: json['deliveryStatus'] != null
|
||||
? MessageDeliveryStatus.values.firstWhere(
|
||||
@@ -176,11 +173,13 @@ class MessageStorageService {
|
||||
roundTripTimeMs: json['roundTripTimeMs'] as int?,
|
||||
deliveredAt: json['deliveredAtMillis'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
json['deliveredAtMillis'] as int)
|
||||
json['deliveredAtMillis'] as int,
|
||||
)
|
||||
: null,
|
||||
recipientPublicKey: json['recipientPublicKey'] != null
|
||||
? Uint8List.fromList(
|
||||
base64Decode(json['recipientPublicKey'] as String))
|
||||
base64Decode(json['recipientPublicKey'] as String),
|
||||
)
|
||||
: null,
|
||||
isRead: json['isRead'] as bool? ?? false,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'package:timezone/data/latest_all.dart' as tz;
|
||||
import '../models/sar_marker.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
@@ -44,7 +43,9 @@ class NotificationService {
|
||||
tz.initializeTimeZones();
|
||||
|
||||
// Android initialization settings
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const androidSettings = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
// iOS initialization settings
|
||||
final darwinSettings = DarwinInitializationSettings(
|
||||
@@ -85,25 +86,34 @@ class NotificationService {
|
||||
try {
|
||||
// iOS permissions
|
||||
final iosPlugin = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
if (iosPlugin != null) {
|
||||
final granted = await iosPlugin.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
critical: true, // Request critical alert permission for urgent SAR notifications
|
||||
critical:
|
||||
true, // Request critical alert permission for urgent SAR notifications
|
||||
);
|
||||
_permissionGranted = granted ?? false;
|
||||
debugPrint('📱 [NotificationService] iOS permissions granted: $_permissionGranted');
|
||||
debugPrint(
|
||||
'📱 [NotificationService] iOS permissions granted: $_permissionGranted',
|
||||
);
|
||||
}
|
||||
|
||||
// Android 13+ permissions
|
||||
final androidPlugin = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
if (androidPlugin != null) {
|
||||
final granted = await androidPlugin.requestNotificationsPermission();
|
||||
_permissionGranted = granted ?? false;
|
||||
debugPrint('🤖 [NotificationService] Android permissions granted: $_permissionGranted');
|
||||
debugPrint(
|
||||
'🤖 [NotificationService] Android permissions granted: $_permissionGranted',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [NotificationService] Error requesting permissions: $e');
|
||||
@@ -114,7 +124,9 @@ class NotificationService {
|
||||
Future<void> _createNotificationChannels() async {
|
||||
try {
|
||||
final androidPlugin = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
|
||||
if (androidPlugin == null) return;
|
||||
|
||||
@@ -152,7 +164,9 @@ class NotificationService {
|
||||
|
||||
/// Handle notification tap (foreground)
|
||||
void _onNotificationResponse(NotificationResponse response) {
|
||||
debugPrint('🔔 [NotificationService] Notification tapped: ${response.payload}');
|
||||
debugPrint(
|
||||
'🔔 [NotificationService] Notification tapped: ${response.payload}',
|
||||
);
|
||||
// TODO: Navigate to map tab and show SAR marker
|
||||
// This would require a callback to the app layer
|
||||
}
|
||||
@@ -166,18 +180,23 @@ class NotificationService {
|
||||
AppLocalizations? localizations,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
debugPrint('⚠️ [NotificationService] Not initialized, skipping notification');
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_permissionGranted) {
|
||||
debugPrint('⚠️ [NotificationService] Permission not granted, skipping notification');
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Permission not granted, skipping notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unique notification ID based on timestamp
|
||||
final notificationId = _sarNotificationId + (DateTime.now().millisecondsSinceEpoch % 1000);
|
||||
final notificationId =
|
||||
_sarNotificationId + (DateTime.now().millisecondsSinceEpoch % 1000);
|
||||
|
||||
// Build notification title and body
|
||||
final title = _buildNotificationTitle(type, localizations);
|
||||
@@ -222,7 +241,8 @@ class NotificationService {
|
||||
badgeNumber: 1,
|
||||
threadIdentifier: 'sar_markers',
|
||||
categoryIdentifier: 'SAR_ALERT',
|
||||
interruptionLevel: InterruptionLevel.critical, // Critical alert (bypasses silent mode)
|
||||
interruptionLevel:
|
||||
InterruptionLevel.critical, // Critical alert (bypasses silent mode)
|
||||
);
|
||||
|
||||
// Combined notification details
|
||||
@@ -250,7 +270,10 @@ class NotificationService {
|
||||
}
|
||||
|
||||
/// Build notification title based on SAR marker type
|
||||
String _buildNotificationTitle(SarMarkerType type, AppLocalizations? localizations) {
|
||||
String _buildNotificationTitle(
|
||||
SarMarkerType type,
|
||||
AppLocalizations? localizations,
|
||||
) {
|
||||
if (localizations == null) {
|
||||
return '🚨 ${type.displayName} Detected';
|
||||
}
|
||||
@@ -330,27 +353,33 @@ class NotificationService {
|
||||
AppLocalizations? localizations,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
debugPrint('⚠️ [NotificationService] Not initialized, skipping notification');
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_permissionGranted) {
|
||||
debugPrint('⚠️ [NotificationService] Permission not granted, skipping notification');
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Permission not granted, skipping notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unique notification ID based on timestamp
|
||||
final notificationId = _messageNotificationId + (DateTime.now().millisecondsSinceEpoch % 1000);
|
||||
final notificationId =
|
||||
_messageNotificationId +
|
||||
(DateTime.now().millisecondsSinceEpoch % 1000);
|
||||
|
||||
// Build notification title and body
|
||||
final title = isChannelMessage
|
||||
? (localizations != null
|
||||
? '${localizations.channel}: ${channelName ?? "Public"}'
|
||||
: 'Channel: ${channelName ?? "Public"}')
|
||||
? '${localizations.channel}: ${channelName ?? "Public"}'
|
||||
: 'Channel: ${channelName ?? "Public"}')
|
||||
: (localizations != null
|
||||
? '${localizations.newMessage} ${localizations.from} $senderName'
|
||||
: 'New message from $senderName');
|
||||
? '${localizations.newMessage} ${localizations.from} $senderName'
|
||||
: 'New message from $senderName');
|
||||
|
||||
final body = messageText.length > 200
|
||||
? '${messageText.substring(0, 200)}...'
|
||||
@@ -381,7 +410,9 @@ class NotificationService {
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
sound: 'default',
|
||||
threadIdentifier: isChannelMessage ? 'channel_messages' : 'direct_messages',
|
||||
threadIdentifier: isChannelMessage
|
||||
? 'channel_messages'
|
||||
: 'direct_messages',
|
||||
subtitle: senderName,
|
||||
);
|
||||
|
||||
@@ -404,7 +435,9 @@ class NotificationService {
|
||||
debugPrint(' Sender: $senderName');
|
||||
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [NotificationService] Error showing message notification: $e');
|
||||
debugPrint(
|
||||
'❌ [NotificationService] Error showing message notification: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +465,9 @@ class NotificationService {
|
||||
Future<bool> areNotificationsEnabled() async {
|
||||
try {
|
||||
final androidPlugin = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
if (androidPlugin != null) {
|
||||
final enabled = await androidPlugin.areNotificationsEnabled();
|
||||
return enabled ?? false;
|
||||
@@ -441,7 +476,9 @@ class NotificationService {
|
||||
// For iOS, assume enabled if permission was granted
|
||||
return _permissionGranted;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [NotificationService] Error checking notification status: $e');
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Error checking notification status: $e',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -451,7 +488,9 @@ class NotificationService {
|
||||
try {
|
||||
return await _notificationsPlugin.pendingNotificationRequests();
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [NotificationService] Error getting pending notifications: $e');
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Error getting pending notifications: $e',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user