@@ -2459,70 +2459,93 @@ class MeshCoreConnector extends ChangeNotifier {
24592459 }
24602460
24612461 Message ? _parseContactMessage (Uint8List frame) {
2462- if (frame.isEmpty) return null ;
2463- final code = frame[0 ];
2464- if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
2462+ if (frame.isEmpty) {
2463+ appLogger.warn ('Received empty frame, ignoring' );
24652464 return null ;
24662465 }
2466+ final reader = BufferReader (frame);
24672467
2468- // Companion radio layout:
2469- // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
2470- final prefixOffset = code == respCodeContactMsgRecvV3 ? 4 : 1 ;
2471- const prefixLen = 6 ;
2472- final pathLenOffset = prefixOffset + prefixLen;
2473- final txtTypeOffset = pathLenOffset + 1 ;
2474- final timestampOffset = txtTypeOffset + 1 ;
2475- final baseTextOffset = timestampOffset + 4 ;
2476-
2477- if (frame.length <= baseTextOffset) return null ;
2478- final fourBytePubMSG = frame.sublist (baseTextOffset, baseTextOffset + 4 );
2479- final senderPrefix = frame.sublist (prefixOffset, prefixOffset + prefixLen);
2480- final flags = frame[txtTypeOffset];
2481- final shiftedType = flags >> 2 ;
2482- final rawType = flags;
2483- final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
2484- final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
2485- if (! isPlain && ! isCli) {
2486- return null ;
2487- }
2468+ try {
2469+ final code = reader.readByte ();
2470+ if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
2471+ appLogger.warn (
2472+ 'Unexpected message code: $code , expected contact message receive codes' ,
2473+ );
2474+ return null ;
2475+ }
24882476
2489- // Try base text offset; if empty and there is room for the optional 4-byte extra
2490- // (used by signed/plain variants), try again skipping those bytes.
2491- var text = readCString (
2492- frame,
2493- baseTextOffset,
2494- frame.length - baseTextOffset,
2495- );
2496- if (text.isEmpty && frame.length > baseTextOffset + 4 ) {
2497- text = readCString (
2498- frame,
2499- baseTextOffset + 4 ,
2500- frame.length - (baseTextOffset + 4 ),
2477+ // Companion radio layout:
2478+ // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
2479+ // double snr = 0;
2480+ if (code == respCodeContactMsgRecvV3) {
2481+ // Older firmware layout with SNR as a signed byte after the code
2482+ // snr = reader.readInt8().toDouble() * 4; // SNR in dB, scaled by 4
2483+ reader.skipBytes (1 ); // Skip SNR byte
2484+ reader.skipBytes (2 ); // Skip reserved bytes
2485+ }
2486+
2487+ final senderPrefix = reader.readBytes (6 );
2488+ final pathLength = reader.readByte ();
2489+ final txtType = reader.readByte ();
2490+ final timestampRaw = reader.readInt32LE ();
2491+ final timestamp = DateTime .fromMillisecondsSinceEpoch (
2492+ timestampRaw * 1000 ,
25012493 );
2502- }
2503- if (text.isEmpty) return null ;
2504- final decodedText = isCli ? text : (Smaz .tryDecodePrefixed (text) ?? text);
25052494
2506- final timestampRaw = readUint32LE (frame, timestampOffset);
2507- final pathLenByte = frame[pathLenOffset];
2495+ if (txtType == 2 ) {
2496+ reader.skipBytes (4 ); // Skip extra 4 bytes for signed/plain variants
2497+ }
25082498
2509- final contact = _contacts.cast <Contact ?>().firstWhere (
2510- (c) => c != null && _matchesPrefix (c.publicKey, senderPrefix),
2511- orElse: () => null ,
2512- );
2513- if (contact == null ) return null ;
2514-
2515- return Message (
2516- senderKey: contact.publicKey,
2517- text: decodedText,
2518- timestamp: DateTime .fromMillisecondsSinceEpoch (timestampRaw * 1000 ),
2519- isOutgoing: false ,
2520- isCli: isCli,
2521- status: MessageStatus .delivered,
2522- pathLength: pathLenByte == 0xFF ? 0 : pathLenByte,
2523- pathBytes: Uint8List (0 ),
2524- fourByteRoomContactKey: fourBytePubMSG,
2525- );
2499+ final msgText = reader.readString ();
2500+
2501+ final flags = txtType;
2502+ final shiftedType = flags >> 2 ;
2503+ final rawType = flags;
2504+ final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
2505+ final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
2506+ if (! isPlain && ! isCli) {
2507+ appLogger.warn (
2508+ 'Unknown message type received: txtType=$txtType , shifted=$shiftedType , raw=$rawType ' ,
2509+ );
2510+ return null ;
2511+ }
2512+
2513+ if (msgText.isEmpty) {
2514+ appLogger.warn ('Received message with empty text, ignoring' );
2515+ return null ;
2516+ }
2517+ final decodedText = isCli
2518+ ? msgText
2519+ : (Smaz .tryDecodePrefixed (msgText) ?? msgText);
2520+
2521+ final contact = _contacts.cast <Contact ?>().firstWhere (
2522+ (c) => c != null && _matchesPrefix (c.publicKey, senderPrefix),
2523+ orElse: () => null ,
2524+ );
2525+ if (contact == null ) {
2526+ appLogger.warn (
2527+ 'Received message from unknown contact with prefix: ${senderPrefix .map ((b ) => b .toRadixString (16 ).padLeft (2 , '0' ).toUpperCase ()).join ('' )}' ,
2528+ );
2529+ return null ;
2530+ }
2531+
2532+ return Message (
2533+ senderKey: contact.publicKey,
2534+ text: decodedText,
2535+ timestamp: timestamp,
2536+ isOutgoing: false ,
2537+ isCli: isCli,
2538+ status: MessageStatus .delivered,
2539+ pathLength: pathLength == 0xFF ? 0 : pathLength,
2540+ pathBytes: Uint8List (0 ),
2541+ fourByteRoomContactKey: msgText.length >= 4
2542+ ? Uint8List .fromList (msgText.substring (0 , 4 ).codeUnits)
2543+ : null ,
2544+ );
2545+ } catch (e) {
2546+ appLogger.warn ('Error parsing contact direct message: $e ' );
2547+ return null ;
2548+ }
25262549 }
25272550
25282551 bool _matchesPrefix (Uint8List fullKey, Uint8List prefix) {
0 commit comments