@ -257,6 +257,9 @@ class MeshCoreConnector extends ChangeNotifier {
int ? _activeChannelIndex ;
int ? _activeChannelIndex ;
List < int > _channelOrder = [ ] ;
List < int > _channelOrder = [ ] ;
int _storageUsedKb = - 1 ;
int _storageTotalKb = - 1 ;
/ / Getters
/ / Getters
MeshCoreConnectionState get state = > _state ;
MeshCoreConnectionState get state = > _state ;
BluetoothDevice ? get device = > _device ;
BluetoothDevice ? get device = > _device ;
@ -338,6 +341,8 @@ class MeshCoreConnector extends ChangeNotifier {
int ? get firmwareVerCode = > _firmwareVerCode ;
int ? get firmwareVerCode = > _firmwareVerCode ;
Map < String , String > ? get currentCustomVars = > _currentCustomVars ;
Map < String , String > ? get currentCustomVars = > _currentCustomVars ;
int ? get batteryMillivolts = > _batteryMillivolts ;
int ? get batteryMillivolts = > _batteryMillivolts ;
int ? get storageUsedKb = > _storageUsedKb ;
int ? get storageTotalKb = > _storageTotalKb ;
int get maxContacts = > _maxContacts ;
int get maxContacts = > _maxContacts ;
int get maxChannels = > _maxChannels ;
int get maxChannels = > _maxChannels ;
Set < String > get knownContactKeys = > Set . unmodifiable ( _knownContactKeys ) ;
Set < String > get knownContactKeys = > Set . unmodifiable ( _knownContactKeys ) ;
@ -3037,14 +3042,23 @@ class MeshCoreConnector extends ChangeNotifier {
/ / [ 1 - 2 ] = battery_mv ( uint16 LE )
/ / [ 1 - 2 ] = battery_mv ( uint16 LE )
/ / [ 3 - 6 ] = storage_used_kb ( uint32 LE )
/ / [ 3 - 6 ] = storage_used_kb ( uint32 LE )
/ / [ 7 - 10 ] = storage_total_kb ( uint32 LE )
/ / [ 7 - 10 ] = storage_total_kb ( uint32 LE )
if ( frame . length > = 3 ) {
try {
_batteryMillivolts = readUint16LE ( frame , 1 ) ;
final reader = BufferReader ( frame ) ;
reader . skipBytes ( 1 ) ;
_batteryMillivolts = reader . readInt16LE ( ) ;
_storageUsedKb = reader . readUInt32LE ( ) ;
_storageTotalKb = reader . readUInt32LE ( ) ;
final volts = ( _batteryMillivolts ! / 1000.0 ) . toStringAsFixed ( 2 ) ;
final volts = ( _batteryMillivolts ! / 1000.0 ) . toStringAsFixed ( 2 ) ;
_appDebugLogService ? . info (
_appDebugLogService ? . info (
' Pulled battery: $ volts V ( $ _batteryMillivolts mV) ' ,
' Pulled battery: $ volts V ( $ _batteryMillivolts mV) ' ,
tag: ' Battery ' ,
tag: ' Battery ' ,
) ;
) ;
notifyListeners ( ) ;
notifyListeners ( ) ;
} catch ( e ) {
_appDebugLogService ? . error (
' Error parsing battery and storage frame: $ e ' ,
tag: ' Connector ' ,
) ;
}
}
}
}
@ -3702,68 +3716,89 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleLogRxData ( Uint8List frame ) {
void _handleLogRxData ( Uint8List frame ) {
if ( frame . length < 4 ) return ;
if ( frame . length < 4 ) return ;
final raw = Uint8List . fromList ( frame . sublist ( 3 ) ) ;
try {
final packet = _parseRawPacket ( raw ) ;
final reader = BufferReader ( frame ) ;
if ( packet = = null | | packet . payloadType ! = _payloadTypeGroupText ) return ;
reader . skipBytes ( 3 ) ; / / Skip header
final payload = packet . payload ;
final raw = reader . readRemainingBytes ( ) ;
if ( payload . length < = _cipherMacSize ) return ;
final packet = _parseRawPacket ( raw ) ;
final channelHash = payload [ 0 ] ;
if ( packet = = null | | packet . payloadType ! = _payloadTypeGroupText ) return ;
final encrypted = Uint8List . fromList ( payload . sublist ( 1 ) ) ;
final payload = packet . payload ;
/ / Use cached channels as fallback if live channels not yet loaded
if ( payload . length < = _cipherMacSize ) return ;
final channelsToSearch = _channels . isNotEmpty ? _channels : _cachedChannels ;
final channelHash = payload [ 0 ] ;
for ( final channel in channelsToSearch ) {
final encrypted = Uint8List . fromList ( payload . sublist ( 1 ) ) ;
if ( channel . isEmpty ) continue ;
final hash = _computeChannelHash ( channel . psk ) ;
/ / Use cached channels as fallback if live channels not yet loaded
if ( hash ! = channelHash ) continue ;
final channelsToSearch = _channels . isNotEmpty
? _channels
final decrypted = _decryptPayload ( channel . psk , encrypted ) ;
: _cachedChannels ;
if ( decrypted = = null | | decrypted . length < 6 ) return ;
for ( final channel in channelsToSearch ) {
if ( channel . isEmpty ) continue ;
final txtType = decrypted [ 4 ] ;
final hash = _computeChannelHash ( channel . psk ) ;
if ( ( txtType > > 2 ) ! = 0 ) {
if ( hash ! = channelHash ) continue ;
return ;
try {
}
final decryptedBytes = _decryptPayload ( channel . psk , encrypted ) ;
if ( decryptedBytes = = null | | decryptedBytes . length < 6 ) return ;
final decrypted = BufferReader ( decryptedBytes ) ;
/ / Skip header + SNR + reserved ( 2 )
decrypted . skipBytes ( 4 ) ;
final txtType = decrypted . readByte ( ) ;
if ( ( txtType > > 2 ) ! = 0 ) {
return ;
}
final timestampRaw = readUint32LE ( decrypted , 0 ) ;
final timestampRaw = decrypted . readUInt32LE ( ) ;
final text = readCString ( decrypted , 5 , decrypted . length - 5 ) ;
final text = decrypted . readString ( ) ;
final parsed = _splitSenderText ( text ) ;
final parsed = _splitSenderText ( text ) ;
final decodedText = Smaz . tryDecodePrefixed ( parsed . text ) ? ? parsed . text ;
final decodedText =
if ( _shouldDropSelfChannelMessage ( parsed . senderName , packet . pathBytes ) ) {
Smaz . tryDecodePrefixed ( parsed . text ) ? ? parsed . text ;
return ;
if ( _shouldDropSelfChannelMessage (
}
parsed . senderName ,
packet . pathBytes ,
) ) {
return ;
}
final pktHash = _computePacketHash ( packet . payloadType , packet . payload ) ;
final pktHash = _computePacketHash (
packet . payloadType ,
packet . payload ,
) ;
final message = ChannelMessage (
final message = ChannelMessage (
senderKey: null ,
senderKey: null ,
senderName: parsed . senderName ,
senderName: parsed . senderName ,
text: decodedText ,
text: decodedText ,
timestamp: DateTime . fromMillisecondsSinceEpoch ( timestampRaw * 1000 ) ,
timestamp: DateTime . fromMillisecondsSinceEpoch ( timestampRaw * 1000 ) ,
isOutgoing: false ,
isOutgoing: false ,
status: ChannelMessageStatus . sent ,
status: ChannelMessageStatus . sent ,
pathLength: packet . isFlood ? packet . hopCount : 0 ,
pathLength: packet . isFlood ? packet . hopCount : 0 ,
pathBytes: packet . pathBytes ,
pathBytes: packet . pathBytes ,
channelIndex: channel . index ,
channelIndex: channel . index ,
packetHash: pktHash ,
packetHash: pktHash ,
) ;
) ;
_updateContactLastMessageAtByName (
_updateContactLastMessageAtByName (
parsed . senderName ,
parsed . senderName ,
message . timestamp ,
message . timestamp ,
pathBytes: message . pathBytes ,
pathBytes: message . pathBytes ,
) ;
) ;
final isNew = _addChannelMessage ( channel . index , message ) ;
final isNew = _addChannelMessage ( channel . index , message ) ;
_maybeIncrementChannelUnread ( message , isNew: isNew ) ;
_maybeIncrementChannelUnread ( message , isNew: isNew ) ;
notifyListeners ( ) ;
notifyListeners ( ) ;
if ( isNew ) {
if ( isNew ) {
final label = channel . name . isEmpty
final label = channel . name . isEmpty
? ' Channel ${ channel . index } '
? ' Channel ${ channel . index } '
: channel . name ;
: channel . name ;
_maybeNotifyChannelMessage ( message , channelName: label ) ;
_maybeNotifyChannelMessage ( message , channelName: label ) ;
}
return ;
} catch ( e ) {
appLogger . warn ( ' Decryption failed for channel ${ channel . index } : $ e ' ) ;
}
}
}
return ;
} catch ( e ) {
appLogger . warn ( ' Error handling log RX data frame: $ e ' ) ;
}
}
}
}
@ -3774,15 +3809,15 @@ class MeshCoreConnector extends ChangeNotifier {
/ / [ 2 - 5 ] = expected_ack_hash ( uint32 )
/ / [ 2 - 5 ] = expected_ack_hash ( uint32 )
/ / [ 6 - 9 ] = estimated_timeout_ms ( uint32 )
/ / [ 6 - 9 ] = estimated_timeout_ms ( uint32 )
if ( frame . length > = 10 ) {
try {
final ackHash = Uint8List . fromList ( frame . sublist ( 2 , 6 ) ) ;
final reader = BufferReader ( frame ) ;
final timeoutMs = readUint32LE ( frame , 6 ) ;
reader . skipBytes ( 2 ) ; / / code + is_flood
final ackHash = reader . readUInt32LE ( ) ;
final timeoutMs = reader . readUInt32LE ( ) ;
/ / Check if this is a CLI command ACK - if so , ignore it
/ / Check if this is a CLI command ACK - if so , ignore it
if ( _lastSentWasCliCommand ) {
if ( _lastSentWasCliCommand ) {
final ackHashHex = ackHash
final ackHashHex = ackHash . toRadixString ( 16 ) . padLeft ( 8 , ' 0 ' ) ;
. map ( ( b ) = > b . toRadixString ( 16 ) . padLeft ( 2 , ' 0 ' ) )
. join ( ) ;
debugPrint ( ' Ignoring CLI command ACK (sent): $ ackHashHex ' ) ;
debugPrint ( ' Ignoring CLI command ACK (sent): $ ackHashHex ' ) ;
_lastSentWasCliCommand = false ;
_lastSentWasCliCommand = false ;
return ;
return ;
@ -3801,7 +3836,8 @@ class MeshCoreConnector extends ChangeNotifier {
if ( _markNextPendingChannelMessageSent ( ) ) {
if ( _markNextPendingChannelMessageSent ( ) ) {
return ;
return ;
}
}
} else {
} catch ( e ) {
appLogger . warn ( ' Error handling message sent frame: $ e ' ) ;
/ / Fallback to old behavior
/ / Fallback to old behavior
for ( var messages in _conversations . values ) {
for ( var messages in _conversations . values ) {
for ( int i = messages . length - 1 ; i > = 0 ; i - - ) {
for ( int i = messages . length - 1 ; i > = 0 ; i - - ) {
@ -3880,9 +3916,11 @@ class MeshCoreConnector extends ChangeNotifier {
/ / [ 1 - 4 ] = ack_hash ( uint32 )
/ / [ 1 - 4 ] = ack_hash ( uint32 )
/ / [ 5 - 8 ] = trip_time_ms ( uint32 )
/ / [ 5 - 8 ] = trip_time_ms ( uint32 )
if ( frame . length > = 9 ) {
try {
final ackHash = Uint8List . fromList ( frame . sublist ( 1 , 5 ) ) ;
final reader = BufferReader ( frame ) ;
final tripTimeMs = readUint32LE ( frame , 5 ) ;
reader . skipBytes ( 1 ) ; / / Skip code
final ackHash = reader . readUInt32LE ( ) ;
final tripTimeMs = reader . readUInt32LE ( ) ;
/ / CLI command ACKs are already filtered in _handleMessageSent , so this should only see real messages
/ / CLI command ACKs are already filtered in _handleMessageSent , so this should only see real messages
@ -3894,7 +3932,8 @@ class MeshCoreConnector extends ChangeNotifier {
if ( _retryService ! = null ) {
if ( _retryService ! = null ) {
_retryService ! . handleAckReceived ( ackHash , tripTimeMs ) ;
_retryService ! . handleAckReceived ( ackHash , tripTimeMs ) ;
}
}
} else {
} catch ( e ) {
appLogger . warn ( ' Error handling send confirmed frame: $ e ' ) ;
/ / Fallback to old behavior
/ / Fallback to old behavior
for ( var messages in _conversations . values ) {
for ( var messages in _conversations . values ) {
for ( int i = messages . length - 1 ; i > = 0 ; i - - ) {
for ( int i = messages . length - 1 ; i > = 0 ; i - - ) {
@ -3909,10 +3948,8 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
}
}
bool _handleRepeaterCommandSent ( Uint8List ackHash , int timeoutMs ) {
bool _handleRepeaterCommandSent ( int ackHash , int timeoutMs ) {
final ackHashHex = ackHash
final ackHashHex = ackHash . toRadixString ( 16 ) . padLeft ( 8 , ' 0 ' ) ;
. map ( ( b ) = > b . toRadixString ( 16 ) . padLeft ( 2 , ' 0 ' ) )
. join ( ) ;
final entry = _pendingRepeaterAcks [ ackHashHex ] ;
final entry = _pendingRepeaterAcks [ ackHashHex ] ;
if ( entry = = null ) return false ;
if ( entry = = null ) return false ;
@ -3930,10 +3967,8 @@ class MeshCoreConnector extends ChangeNotifier {
return true ;
return true ;
}
}
bool _handleRepeaterCommandAck ( Uint8List ackHash , int tripTimeMs ) {
bool _handleRepeaterCommandAck ( int ackHash , int tripTimeMs ) {
final ackHashHex = ackHash
final ackHashHex = ackHash . toRadixString ( 16 ) . padLeft ( 8 , ' 0 ' ) ;
. map ( ( b ) = > b . toRadixString ( 16 ) . padLeft ( 2 , ' 0 ' ) )
. join ( ) ;
final entry = _pendingRepeaterAcks . remove ( ackHashHex ) ;
final entry = _pendingRepeaterAcks . remove ( ackHashHex ) ;
if ( entry = = null ) return false ;
if ( entry = = null ) return false ;
entry . timeout ? . cancel ( ) ;
entry . timeout ? . cancel ( ) ;