mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Use LogRxRouteDecoder in AppProvider
This commit is contained in:
@@ -29,6 +29,7 @@ import '../utils/image_message_parser.dart';
|
||||
import '../utils/media_swarm_protocol.dart';
|
||||
import '../utils/message_airtime_estimator.dart';
|
||||
import '../utils/fast_gps_packet.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
class _DirectMessageRouteSession {
|
||||
final PathSelection currentSelection;
|
||||
@@ -2573,12 +2574,10 @@ class AppProvider with ChangeNotifier {
|
||||
if (log.responseCode != 0x88) continue;
|
||||
if (log.rawData.length < 6) continue;
|
||||
|
||||
final raw = log.rawData;
|
||||
final payloadType = (raw[3] >> 2) & 0x0F;
|
||||
final pathLen = raw[4];
|
||||
if (payloadType != expectedPayloadType) continue;
|
||||
if (pathLen != message.pathLen) continue;
|
||||
if (raw.length < 5 + pathLen) continue;
|
||||
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||
if (decoded == null) continue;
|
||||
if (decoded.payloadType != expectedPayloadType) continue;
|
||||
if (decoded.hopCount != message.pathLen) continue;
|
||||
|
||||
final deltaMs =
|
||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||
@@ -2594,11 +2593,9 @@ class AppProvider with ChangeNotifier {
|
||||
|
||||
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
||||
if (log == null) return null;
|
||||
final raw = log.rawData;
|
||||
if (raw.length < 6) return null;
|
||||
final pathLen = raw[4];
|
||||
if (pathLen <= 0 || raw.length < 5 + pathLen) return null;
|
||||
return raw.sublist(5, 5 + pathLen);
|
||||
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||
if (decoded == null || decoded.pathBytes.isEmpty) return null;
|
||||
return decoded.pathBytes;
|
||||
}
|
||||
|
||||
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||
|
||||
@@ -885,6 +885,54 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark unread messages for a specific destination as read.
|
||||
void markDestinationAsRead({
|
||||
required String destinationType,
|
||||
Contact? contact,
|
||||
}) {
|
||||
if (destinationType == 'all') {
|
||||
markAllAsRead();
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasChanges = false;
|
||||
for (int i = 0; i < _messages.length; i++) {
|
||||
final message = _messages[i];
|
||||
if (message.isRead || message.isSentMessage || message.isSystemMessage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final matchesDestination = switch (destinationType) {
|
||||
'channel' => _isChannelMessageForContact(message, contact),
|
||||
'contact' || 'room' => contact != null && _isMessageForDestination(message, contact),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (!matchesDestination) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_messages[i] = message.copyWith(isRead: true);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isChannelMessageForContact(Message message, Contact? contact) {
|
||||
if (!message.isChannelMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final selectedChannelIdx = contact != null && contact.publicKey.length > 1
|
||||
? contact.publicKey[1]
|
||||
: 0;
|
||||
return (message.channelIdx ?? 0) == selectedChannelIdx;
|
||||
}
|
||||
|
||||
/// Mark a specific message as read
|
||||
void markAsRead(String messageId) {
|
||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||
|
||||
@@ -195,7 +195,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
switch (tab) {
|
||||
case _HomeTab.messages:
|
||||
context.read<MessagesProvider>().markAllAsRead();
|
||||
break;
|
||||
case _HomeTab.contacts:
|
||||
context.read<ContactsProvider>().markAllAsViewed();
|
||||
|
||||
@@ -89,9 +89,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
// Load saved message destination
|
||||
_loadSavedDestination();
|
||||
_loadVoiceBitrate();
|
||||
// Mark all messages as read when tab is opened
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<MessagesProvider>().markAllAsRead();
|
||||
_checkForNavigationRequest();
|
||||
});
|
||||
}
|
||||
@@ -309,6 +307,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
_selectedRecipient = recipient;
|
||||
});
|
||||
|
||||
_markCurrentDestinationAsRead();
|
||||
|
||||
_enforceMessageByteLimit();
|
||||
|
||||
// Save to preferences
|
||||
@@ -465,6 +465,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
|
||||
_textController.clear();
|
||||
_focusNode.unfocus();
|
||||
_markCurrentDestinationAsRead();
|
||||
|
||||
if (!mounted) return;
|
||||
} catch (e) {
|
||||
@@ -1641,6 +1642,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
void _markCurrentDestinationAsRead() {
|
||||
context.read<MessagesProvider>().markDestinationAsRead(
|
||||
destinationType: _destinationType,
|
||||
contact: _selectedRecipient,
|
||||
);
|
||||
}
|
||||
|
||||
List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
|
||||
// Get all recent messages
|
||||
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
||||
|
||||
@@ -4,11 +4,13 @@ import '../models/contact.dart';
|
||||
|
||||
class DecodedLogRxRoute {
|
||||
final int payloadType;
|
||||
final int pathDescriptor;
|
||||
final List<int> pathBytes;
|
||||
final int hashSize;
|
||||
|
||||
const DecodedLogRxRoute({
|
||||
required this.payloadType,
|
||||
required this.pathDescriptor,
|
||||
required this.pathBytes,
|
||||
required this.hashSize,
|
||||
});
|
||||
@@ -63,21 +65,49 @@ class LogRxRouteDecoder {
|
||||
}
|
||||
|
||||
if (rawPacketData.length <= index) return null;
|
||||
final pathLen = rawPacketData[index++];
|
||||
if (rawPacketData.length < index + pathLen) return null;
|
||||
final pathBytes = rawPacketData.sublist(index, index + pathLen);
|
||||
final hashSize = inferHashSize(
|
||||
pathBytes,
|
||||
preferredHashSize: preferredHashSize,
|
||||
);
|
||||
final pathDescriptor = rawPacketData[index++];
|
||||
final pathMode = (pathDescriptor & 0xFF) >> 6;
|
||||
final pathByteLen = pathMode == 0
|
||||
? pathDescriptor
|
||||
: descriptorByteLength(pathDescriptor);
|
||||
if (pathByteLen == null || rawPacketData.length < index + pathByteLen) {
|
||||
return null;
|
||||
}
|
||||
final pathBytes = rawPacketData.sublist(index, index + pathByteLen);
|
||||
final hashSize = pathMode == 0
|
||||
? inferHashSize(pathBytes, preferredHashSize: preferredHashSize)
|
||||
: (descriptorHashSize(pathDescriptor) ??
|
||||
inferHashSize(pathBytes, preferredHashSize: preferredHashSize));
|
||||
|
||||
return DecodedLogRxRoute(
|
||||
payloadType: payloadType,
|
||||
pathDescriptor: pathDescriptor,
|
||||
pathBytes: pathBytes,
|
||||
hashSize: hashSize,
|
||||
);
|
||||
}
|
||||
|
||||
static int? descriptorHashSize(int pathDescriptor) {
|
||||
final normalized = pathDescriptor & 0xFF;
|
||||
final mode = normalized >> 6;
|
||||
if (mode == 3) return null;
|
||||
return mode + 1;
|
||||
}
|
||||
|
||||
static int? descriptorHopCount(int pathDescriptor) {
|
||||
final hashSize = descriptorHashSize(pathDescriptor);
|
||||
if (hashSize == null) return null;
|
||||
return (pathDescriptor & 0xFF) & 0x3F;
|
||||
}
|
||||
|
||||
static int? descriptorByteLength(int pathDescriptor) {
|
||||
final hashSize = descriptorHashSize(pathDescriptor);
|
||||
final hopCount = descriptorHopCount(pathDescriptor);
|
||||
if (hashSize == null || hopCount == null) return null;
|
||||
final byteLen = hopCount * hashSize;
|
||||
return byteLen <= 64 ? byteLen : null;
|
||||
}
|
||||
|
||||
static int inferHashSize(List<int> pathBytes, {int? preferredHashSize}) {
|
||||
if (pathBytes.isEmpty) return 1;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../../utils/tictactoe_message_parser.dart';
|
||||
import '../../utils/location_formats.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../utils/message_extensions.dart';
|
||||
import '../../utils/log_rx_route_decoder.dart';
|
||||
import '../../models/message_transfer_details.dart';
|
||||
import 'voice_message_bubble.dart';
|
||||
import 'image_message_bubble.dart';
|
||||
@@ -1269,12 +1270,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
|
||||
// Logged frame format:
|
||||
// [0]=response code 0x88, [1]=snrRaw, [2]=rssi, [3]=packet header, [4]=pathLen
|
||||
final raw = log.rawData;
|
||||
final payloadType = (raw[3] >> 2) & 0x0F;
|
||||
final pathLen = raw[4];
|
||||
if (payloadType != expectedPayloadType) continue;
|
||||
if (pathLen != message.pathLen) continue;
|
||||
if (raw.length < 5 + pathLen) continue;
|
||||
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||
if (decoded == null) continue;
|
||||
if (decoded.payloadType != expectedPayloadType) continue;
|
||||
if (decoded.hopCount != message.pathLen) continue;
|
||||
|
||||
final deltaMs =
|
||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||
@@ -1290,11 +1289,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
|
||||
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
||||
if (log == null) return null;
|
||||
final raw = log.rawData;
|
||||
if (raw.length < 6) return null;
|
||||
final pathLen = raw[4];
|
||||
if (pathLen <= 0 || raw.length < 5 + pathLen) return null;
|
||||
return raw.sublist(5, 5 + pathLen);
|
||||
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||
if (decoded == null || decoded.pathBytes.isEmpty) return null;
|
||||
return decoded.pathBytes;
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(BuildContext context) {
|
||||
|
||||
@@ -516,13 +516,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
for (final log in logs) {
|
||||
if (log.responseCode != 0x88) continue; // pushLogRxData
|
||||
if (log.rawData.length < 6) continue;
|
||||
final raw = log.rawData;
|
||||
final header = raw[3];
|
||||
final payloadType = (header >> 2) & 0x0F;
|
||||
final pathLen = raw[4];
|
||||
if (payloadType != expectedPayloadType) continue;
|
||||
if (pathLen != message.pathLen) continue;
|
||||
if (raw.length < 5 + pathLen) continue;
|
||||
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||
if (decoded == null) continue;
|
||||
if (decoded.payloadType != expectedPayloadType) continue;
|
||||
if (decoded.hopCount != message.pathLen) continue;
|
||||
|
||||
final deltaMs =
|
||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||
@@ -533,9 +530,11 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
}
|
||||
|
||||
if (bestLog == null || bestDeltaMs > 30000) return null;
|
||||
final raw = bestLog.rawData;
|
||||
final pathLen = raw[4];
|
||||
return raw.sublist(5, 5 + pathLen);
|
||||
final decoded = LogRxRouteDecoder.decode(bestLog.rawData);
|
||||
if (decoded == null || decoded.pathBytes.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return decoded.pathBytes;
|
||||
}
|
||||
|
||||
List<MeshMapNode?> _matchNodesFromPathHashes({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/models/message.dart';
|
||||
import 'package:meshcore_sar_app/models/message_contact_location.dart';
|
||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||
@@ -11,6 +12,30 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Contact _buildContact({
|
||||
required List<int> prefix,
|
||||
required ContactType type,
|
||||
String name = 'Test Contact',
|
||||
}) {
|
||||
final key = Uint8List.fromList([
|
||||
...prefix,
|
||||
...List<int>.generate(26, (index) => index),
|
||||
]);
|
||||
|
||||
return Contact(
|
||||
publicKey: key,
|
||||
type: type,
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(64),
|
||||
advName: name,
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
}
|
||||
|
||||
group('MessagesProvider voice detection', () {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
@@ -190,5 +215,60 @@ void main() {
|
||||
expect(restoredVoice?.downloaders.single.requesterKey6, '112233445566');
|
||||
expect(restoredImage?.downloaders.single.requesterName, equals('Bob'));
|
||||
});
|
||||
|
||||
test('marks only the selected contact destination as read', () {
|
||||
final provider = MessagesProvider();
|
||||
final alice = _buildContact(
|
||||
prefix: [0x10, 0x11, 0x12, 0x13, 0x14, 0x15],
|
||||
type: ContactType.chat,
|
||||
name: 'Alice',
|
||||
);
|
||||
final bob = _buildContact(
|
||||
prefix: [0x20, 0x21, 0x22, 0x23, 0x24, 0x25],
|
||||
type: ContactType.chat,
|
||||
name: 'Bob',
|
||||
);
|
||||
|
||||
provider.addMessage(
|
||||
Message(
|
||||
id: 'alice-incoming',
|
||||
messageType: MessageType.contact,
|
||||
pathLen: 1,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 1700000100,
|
||||
text: 'Alice unread',
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: alice.publicKey.sublist(0, 6),
|
||||
),
|
||||
);
|
||||
provider.addMessage(
|
||||
Message(
|
||||
id: 'bob-incoming',
|
||||
messageType: MessageType.contact,
|
||||
pathLen: 1,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 1700000101,
|
||||
text: 'Bob unread',
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: bob.publicKey.sublist(0, 6),
|
||||
),
|
||||
);
|
||||
|
||||
provider.markDestinationAsRead(
|
||||
destinationType: 'contact',
|
||||
contact: alice,
|
||||
);
|
||||
|
||||
final aliceMessage = provider.messages.firstWhere(
|
||||
(message) => message.id == 'alice-incoming',
|
||||
);
|
||||
final bobMessage = provider.messages.firstWhere(
|
||||
(message) => message.id == 'bob-incoming',
|
||||
);
|
||||
|
||||
expect(aliceMessage.isRead, isTrue);
|
||||
expect(bobMessage.isRead, isFalse);
|
||||
expect(provider.unreadCount, equals(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,12 +24,37 @@ void main() {
|
||||
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.payloadType, 0x01);
|
||||
expect(decoded.pathDescriptor, 0x04);
|
||||
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
|
||||
expect(decoded.hashSize, 2);
|
||||
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
||||
expect(decoded.originalSenderHashHex, 'c2ba');
|
||||
});
|
||||
|
||||
test('parses encoded descriptor with 2-byte hashes', () {
|
||||
final packet = Uint8List.fromList([
|
||||
0x88,
|
||||
0x37,
|
||||
0xae,
|
||||
0x05,
|
||||
0x42,
|
||||
0xc2,
|
||||
0xba,
|
||||
0x5f,
|
||||
0xde,
|
||||
0x5c,
|
||||
]);
|
||||
|
||||
final decoded = LogRxRouteDecoder.decode(packet);
|
||||
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.pathDescriptor, 0x42);
|
||||
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
|
||||
expect(decoded.hashSize, 2);
|
||||
expect(decoded.hopCount, 2);
|
||||
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
||||
});
|
||||
|
||||
test('uses preferred hash size when packet length is ambiguous', () {
|
||||
final packet = Uint8List.fromList([
|
||||
0x88,
|
||||
|
||||
Reference in New Issue
Block a user