feat: Enhance HomeScreen with RX/TX indicators and long press functionality

- Added GestureDetector to RX/TX indicators for long press to open PacketLogScreen.
- Refactored RX/TX indicator code for better readability.
- Updated disconnect button to maintain functionality.

feat: Extend MeshCoreBleService with new login and message handling features

- Introduced new callbacks for device info, message waiting, login success, and login fail.
- Implemented handling for new message waiting and login success/fail pushes.
- Enhanced device info parsing to include additional fields such as firmware version, max contacts, and more.

fix: Correct MeshCoreConstants response codes

- Added new response codes for custom variables, advertisement path, and tuning parameters.
This commit is contained in:
Janez T
2025-10-14 21:11:54 +02:00
parent 219eeecacd
commit 9238be1a59
5 changed files with 807 additions and 986 deletions

View File

@@ -49,6 +49,8 @@ class ConnectionProvider with ChangeNotifier {
Function(List<Contact>)? onContactsComplete; Function(List<Contact>)? onContactsComplete;
Function(Message)? onMessageReceived; Function(Message)? onMessageReceived;
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived; Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? onLoginSuccess;
Function(Uint8List publicKeyPrefix)? onLoginFail;
ConnectionProvider() { ConnectionProvider() {
_initializeBleService(); _initializeBleService();
@@ -112,6 +114,25 @@ class ConnectionProvider with ChangeNotifier {
_noMoreMessages = true; _noMoreMessages = true;
}; };
_bleService.onMessageWaiting = () {
print('📥 [Provider] Received MsgWaiting push - auto-fetching messages');
// Automatically fetch messages when push notification received
syncAllMessages();
};
_bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) {
print('📥 [Provider] Login successful to room');
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
print(' Permissions: $permissions, Admin: $isAdmin, Tag: $tag');
onLoginSuccess?.call(publicKeyPrefix, permissions, isAdmin, tag);
};
_bleService.onLoginFail = (publicKeyPrefix) {
print('📥 [Provider] Login failed to room');
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
onLoginFail?.call(publicKeyPrefix);
};
_bleService.onSelfInfoReceived = (selfInfo) { _bleService.onSelfInfoReceived = (selfInfo) {
print('📥 [Provider] Received SelfInfo:'); print('📥 [Provider] Received SelfInfo:');
print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm'); print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm');
@@ -413,6 +434,32 @@ class ConnectionProvider with ChangeNotifier {
} }
} }
/// Set other parameters (telemetry modes, advert location policy)
Future<void> setOtherParams({
required int manualAddContacts,
required int telemetryModes,
required int advertLocationPolicy,
int multiAcks = 0,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.setOtherParams(
manualAddContacts: manualAddContacts,
telemetryModes: telemetryModes,
advertLocationPolicy: advertLocationPolicy,
multiAcks: multiAcks,
);
} catch (e) {
_error = 'Failed to set other params: $e';
notifyListeners();
}
}
/// Request fresh device info (triggers SelfInfo response) /// Request fresh device info (triggers SelfInfo response)
Future<void> refreshDeviceInfo() async { Future<void> refreshDeviceInfo() async {
if (!_bleService.isConnected) { if (!_bleService.isConnected) {
@@ -490,6 +537,45 @@ class ConnectionProvider with ChangeNotifier {
} }
} }
/// Login to a room or repeater
///
/// Sends login request with password. Results will be delivered via
/// onLoginSuccess or onLoginFail callbacks.
///
/// Example usage:
/// ```dart
/// connectionProvider.onLoginSuccess = (pkPrefix, perms, isAdmin, tag) {
/// print('Successfully logged in to room!');
/// };
/// connectionProvider.onLoginFail = (pkPrefix) {
/// print('Login failed - incorrect password');
/// };
/// await connectionProvider.loginToRoom(
/// roomPublicKey: contact.publicKey,
/// password: 'secret123',
/// );
/// ```
Future<void> loginToRoom({
required Uint8List roomPublicKey,
required String password,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.loginToRoom(
roomPublicKey: roomPublicKey,
password: password,
);
} catch (e) {
_error = 'Failed to send login request: $e';
notifyListeners();
}
}
/// Clear error message /// Clear error message
void clearError() { void clearError() {
_error = null; _error = null;

File diff suppressed because it is too large Load Diff

View File

@@ -362,68 +362,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// RX indicator // RX/TX indicators with long press to open packet log
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.rxActivity
? Colors.green
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'RX:${provider.rxPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(width: 12),
// TX indicator
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.txActivity
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'TX:${provider.txPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(width: 12),
// Settings button
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
icon: const Icon(Icons.settings),
iconSize: 20,
tooltip: 'Device Settings',
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
),
const SizedBox(width: 8),
// Disconnect button (prominent, icon only)
// Long press to open packet log viewer
GestureDetector( GestureDetector(
onLongPress: () { onLongPress: () {
Navigator.push( Navigator.push(
@@ -435,23 +374,97 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
); );
}, },
child: FilledButton( child: Row(
onPressed: () async { mainAxisSize: MainAxisSize.min,
await provider.disconnect(); children: [
if (context.mounted) { // RX indicator
context.read<AppProvider>().clearAllData(); Container(
} width: 8,
}, height: 8,
style: FilledButton.styleFrom( decoration: BoxDecoration(
backgroundColor: Colors.red.shade700, shape: BoxShape.circle,
foregroundColor: Colors.white, color: provider.rxActivity
padding: const EdgeInsets.all(10), ? Colors.green
minimumSize: const Size(40, 40), : Colors.grey.withOpacity(0.3),
shape: const CircleBorder(), ),
), ),
child: const Icon(Icons.power_settings_new, size: 20), const SizedBox(width: 4),
Text(
'RX:${provider.rxPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(width: 12),
// TX indicator
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.txActivity
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'TX:${provider.txPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
), ),
), ),
const SizedBox(width: 12),
// Settings button (tap for device settings, long press for packet logs)
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PacketLogScreen(
bleService: provider.bleService,
),
),
);
},
child: Container(
width: 36,
height: 36,
alignment: Alignment.center,
child: const Icon(Icons.settings, size: 20),
),
),
const SizedBox(width: 8),
// Disconnect button (prominent, icon only)
FilledButton(
onPressed: () async {
await provider.disconnect();
if (context.mounted) {
context.read<AppProvider>().clearAllData();
}
},
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.all(10),
minimumSize: const Size(40, 40),
shape: const CircleBorder(),
),
child: const Icon(Icons.power_settings_new, size: 20),
),
], ],
), ),
], ],

View File

@@ -17,7 +17,11 @@ typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
typedef OnMessageCallback = void Function(Message message); 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 OnSelfInfoCallback = void Function(Map<String, dynamic> selfInfo);
typedef OnDeviceInfoCallback = void Function(Map<String, dynamic> deviceInfo);
typedef OnNoMoreMessagesCallback = void Function(); typedef OnNoMoreMessagesCallback = void Function();
typedef OnMessageWaitingCallback = void Function();
typedef OnLoginSuccessCallback = void Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag);
typedef OnLoginFailCallback = void Function(Uint8List publicKeyPrefix);
typedef OnErrorCallback = void Function(String error); typedef OnErrorCallback = void Function(String error);
typedef OnConnectionStateCallback = void Function(bool isConnected); typedef OnConnectionStateCallback = void Function(bool isConnected);
@@ -35,7 +39,11 @@ class MeshCoreBleService {
OnMessageCallback? onMessageReceived; OnMessageCallback? onMessageReceived;
OnTelemetryCallback? onTelemetryReceived; OnTelemetryCallback? onTelemetryReceived;
OnSelfInfoCallback? onSelfInfoReceived; OnSelfInfoCallback? onSelfInfoReceived;
OnDeviceInfoCallback? onDeviceInfoReceived;
OnNoMoreMessagesCallback? onNoMoreMessages; OnNoMoreMessagesCallback? onNoMoreMessages;
OnMessageWaitingCallback? onMessageWaiting;
OnLoginSuccessCallback? onLoginSuccess;
OnLoginFailCallback? onLoginFail;
OnErrorCallback? onError; OnErrorCallback? onError;
// Internal state // Internal state
@@ -350,6 +358,18 @@ class MeshCoreBleService {
print(' → Handling SendConfirmed push'); print(' → Handling SendConfirmed push');
_handleSendConfirmed(reader); _handleSendConfirmed(reader);
break; break;
case MeshCoreConstants.pushMsgWaiting:
print(' → Handling MsgWaiting push');
_handleMsgWaiting(reader);
break;
case MeshCoreConstants.pushLoginSuccess:
print(' → Handling LoginSuccess push');
_handleLoginSuccess(reader);
break;
case MeshCoreConstants.pushLoginFail:
print(' → Handling LoginFail push');
_handleLoginFail(reader);
break;
case MeshCoreConstants.respNoMoreMessages: case MeshCoreConstants.respNoMoreMessages:
print(' → Response: No More Messages'); print(' → Response: No More Messages');
onNoMoreMessages?.call(); onNoMoreMessages?.call();
@@ -359,6 +379,7 @@ class MeshCoreBleService {
break; break;
case MeshCoreConstants.respErr: case MeshCoreConstants.respErr:
print(' → Response: ERROR'); print(' → Response: ERROR');
_handleError(reader);
break; break;
default: default:
print(' ⚠️ Unknown response code: $responseCode'); print(' ⚠️ Unknown response code: $responseCode');
@@ -587,38 +608,76 @@ class MeshCoreBleService {
} }
/// Handle DeviceInfo response /// Handle DeviceInfo response
/// Handle DeviceInfo response (RESP_CODE_DEVICE_INFO)
///
/// Protocol format:
/// - 1 byte: firmware version
/// - 1 byte: max contacts ÷ 2 (ver 3+)
/// - 1 byte: max channels (ver 3+)
/// - 4 bytes: BLE PIN (uint32, ver 3+)
/// - 12 bytes: firmware build date (ASCII null-terminated)
/// - 40 bytes: manufacturer model (ASCII null-terminated)
/// - 20 bytes: semantic version (ASCII null-terminated)
void _handleDeviceInfo(BufferReader reader) { void _handleDeviceInfo(BufferReader reader) {
try { try {
print(' [DeviceInfo] Parsing device info...'); print(' [DeviceInfo] Parsing device info...');
print(' Remaining bytes: ${reader.remainingBytesCount}'); print(' Remaining bytes: ${reader.remainingBytesCount}');
// DeviceInfo format (based on MeshCore protocol):
// - 1 byte: protocol version
// - 32 bytes: public key
// - 1 byte: device name length
// - N bytes: device name (UTF-8)
// - remaining: additional info (firmware version, etc.)
if (reader.remainingBytesCount < 1) { if (reader.remainingBytesCount < 1) {
print(' [DeviceInfo] No data to parse'); print(' [DeviceInfo] No data to parse');
return; return;
} }
final protocolVersion = reader.readByte(); final firmwareVersion = reader.readByte();
print(' Protocol version: $protocolVersion'); print(' Firmware version: $firmwareVersion');
if (reader.remainingBytesCount >= 32) { int? maxContacts;
final publicKey = reader.readBytes(32); int? maxChannels;
print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); int? blePin;
if (reader.remainingBytesCount >= 6) {
final maxContactsDiv2 = reader.readByte();
maxContacts = maxContactsDiv2 * 2;
print(' Max contacts: $maxContacts');
maxChannels = reader.readByte();
print(' Max channels: $maxChannels');
blePin = reader.readUInt32LE();
print(' BLE PIN: $blePin');
} }
// Read remaining data as device info details String? firmwareBuildDate;
if (reader.hasRemaining) { if (reader.remainingBytesCount >= 12) {
final remainingData = reader.readRemainingBytes(); final buildDateBytes = reader.readBytes(12);
print(' Additional info: ${remainingData.length} bytes'); firmwareBuildDate = String.fromCharCodes(buildDateBytes.takeWhile((b) => b != 0));
// Could parse device name, firmware version, etc. here if needed print(' Firmware build date: "$firmwareBuildDate"');
} }
String? manufacturerModel;
if (reader.remainingBytesCount >= 40) {
final modelBytes = reader.readBytes(40);
manufacturerModel = String.fromCharCodes(modelBytes.takeWhile((b) => b != 0));
print(' Manufacturer model: "$manufacturerModel"');
}
String? semanticVersion;
if (reader.remainingBytesCount >= 20) {
final versionBytes = reader.readBytes(20);
semanticVersion = String.fromCharCodes(versionBytes.takeWhile((b) => b != 0));
print(' Semantic version: "$semanticVersion"');
}
// Call callback with parsed data
onDeviceInfoReceived?.call({
'firmwareVersion': firmwareVersion,
'maxContacts': maxContacts,
'maxChannels': maxChannels,
'blePin': blePin,
'firmwareBuildDate': firmwareBuildDate,
'manufacturerModel': manufacturerModel,
'semanticVersion': semanticVersion,
});
print(' ✅ [DeviceInfo] Parsed successfully'); print(' ✅ [DeviceInfo] Parsed successfully');
} catch (e) { } catch (e) {
print(' ❌ [DeviceInfo] Parsing error: $e'); print(' ❌ [DeviceInfo] Parsing error: $e');
@@ -632,20 +691,22 @@ class MeshCoreBleService {
print(' [SelfInfo] Parsing self info...'); print(' [SelfInfo] Parsing self info...');
print(' Remaining bytes: ${reader.remainingBytesCount}'); print(' Remaining bytes: ${reader.remainingBytesCount}');
// SelfInfo format (from MeshCore protocol): // SelfInfo format (RESP_CODE_SELF_INFO):
// - 1 byte: protocol version // - 1 byte: type (ADV_TYPE_*)
// - 1 byte: device type // - 1 byte: tx power (dBm, current)
// - 1 byte: tx power // - 1 byte: max tx power (dBm, max radio supports)
// - 1 byte: max tx power
// - 32 bytes: public key // - 32 bytes: public key
// - 4 bytes: adv lat (int32) // - 4 bytes: adv lat * 1E6 (int32)
// - 4 bytes: adv lon (int32) // - 4 bytes: adv lon * 1E6 (int32)
// - 1 byte: manual add contacts flag // - 1 byte: multi ACKs (0=no extra, 1=send extra ACK)
// - 4 bytes: radio freq (uint32) // - 1 byte: advert location policy (0=don't share, 1=share)
// - 2 bytes: radio bw (uint16) // - 1 byte: telemetry modes (bits 0-1: Base, bits 2-3: Location)
// - 1 byte: radio sf // - 1 byte: manual add contacts (0 or 1)
// - 1 byte: radio cr // - 4 bytes: radio freq * 1000 (uint32)
// - remaining: self name (null-terminated string) // - 4 bytes: radio bw (kHz) * 1000 (uint32)
// - 1 byte: spreading factor
// - 1 byte: coding rate
// - remaining: self name (null-terminated varchar)
if (reader.remainingBytesCount < 54) { if (reader.remainingBytesCount < 54) {
print(' [SelfInfo] Insufficient data: ${reader.remainingBytesCount} bytes'); print(' [SelfInfo] Insufficient data: ${reader.remainingBytesCount} bytes');
@@ -654,35 +715,85 @@ class MeshCoreBleService {
return; return;
} }
final protocolVersion = reader.readByte(); print(' 📍 BYTE-BY-BYTE PARSING DEBUG:');
final deviceType = reader.readByte(); print(' Position before reads: offset=0, remaining=${reader.remainingBytesCount}');
final txPower = reader.readByte();
final maxTxPower = reader.readByte();
final publicKey = reader.readBytes(32);
final advLat = reader.readInt32LE();
final advLon = reader.readInt32LE();
final manualAddContacts = reader.readByte();
final radioFreq = reader.readUInt32LE();
final radioBw = reader.readUInt16LE();
final radioSf = reader.readByte();
final radioCr = reader.readByte();
print(' Protocol version: $protocolVersion'); // NO protocol version byte - it starts with device type!
print(' Device type: $deviceType'); final deviceType = reader.readByte();
print(' TX power: $txPower / $maxTxPower dBm'); print(' [Byte 0] Device type: $deviceType (0x${deviceType.toRadixString(16).padLeft(2, '0')})');
print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
print(' Position: ${advLat / 1000000.0}, ${advLon / 1000000.0}'); final txPower = reader.readByte();
print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr'); print(' [Byte 1] TX power: $txPower dBm (0x${txPower.toRadixString(16).padLeft(2, '0')})');
final maxTxPower = reader.readByte();
print(' [Byte 2] Max TX power: $maxTxPower dBm (0x${maxTxPower.toRadixString(16).padLeft(2, '0')})');
final publicKey = reader.readBytes(32);
print(' [Bytes 3-34] Public key (32 bytes): ${publicKey.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}...');
final advLatBytes = reader.readBytes(4);
final advLat = ByteData.sublistView(Uint8List.fromList(advLatBytes)).getInt32(0, Endian.little);
print(' [Bytes 35-38] Adv Lat (raw bytes): ${advLatBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
print(' [Bytes 35-38] Adv Lat (int32 LE): $advLat');
print(' [Bytes 35-38] Adv Lat (decimal): ${advLat / 1000000.0}°');
final advLonBytes = reader.readBytes(4);
final advLon = ByteData.sublistView(Uint8List.fromList(advLonBytes)).getInt32(0, Endian.little);
print(' [Bytes 39-42] Adv Lon (raw bytes): ${advLonBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
print(' [Bytes 39-42] Adv Lon (int32 LE): $advLon');
print(' [Bytes 39-42] Adv Lon (decimal): ${advLon / 1000000.0}°');
final multiAcks = reader.readByte();
print(' [Byte 43] Multi ACKs: $multiAcks (0x${multiAcks.toRadixString(16).padLeft(2, '0')})');
final advertLocPolicy = reader.readByte();
print(' [Byte 44] Advert Loc Policy: $advertLocPolicy (0x${advertLocPolicy.toRadixString(16).padLeft(2, '0')})');
final telemetryModes = reader.readByte();
print(' [Byte 45] Telemetry Modes: $telemetryModes (0x${telemetryModes.toRadixString(16).padLeft(2, '0')})');
final manualAddContacts = reader.readByte();
print(' [Byte 46] Manual Add Contacts: $manualAddContacts (0x${manualAddContacts.toRadixString(16).padLeft(2, '0')})');
final radioFreqBytes = reader.readBytes(4);
final radioFreq = ByteData.sublistView(Uint8List.fromList(radioFreqBytes)).getUint32(0, Endian.little);
print(' [Bytes 47-50] Radio Freq (raw bytes): ${radioFreqBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
print(' [Bytes 47-50] Radio Freq (uint32 LE): $radioFreq');
print(' [Bytes 47-50] Radio Freq (MHz): ${radioFreq / 1000.0}');
final radioBwBytes = reader.readBytes(4);
final radioBw = ByteData.sublistView(Uint8List.fromList(radioBwBytes)).getUint32(0, Endian.little);
print(' [Bytes 51-54] Radio BW (raw bytes): ${radioBwBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
print(' [Bytes 51-54] Radio BW (uint32 LE): $radioBw');
print(' [Bytes 51-54] Radio BW (kHz): ${radioBw / 1000.0}');
final radioSf = reader.readByte();
print(' [Byte 55] Radio SF: $radioSf (0x${radioSf.toRadixString(16).padLeft(2, '0')})');
final radioCr = reader.readByte();
print(' [Byte 56] Radio CR: $radioCr (0x${radioCr.toRadixString(16).padLeft(2, '0')})');
print(' Remaining bytes after radio params: ${reader.remainingBytesCount}');
String? selfName; String? selfName;
if (reader.hasRemaining) { if (reader.hasRemaining) {
selfName = String.fromCharCodes(reader.readRemainingBytes().takeWhile((b) => b != 0)); final nameBytes = reader.readRemainingBytes();
print(' Self name: $selfName'); print(' Self name bytes (hex): ${nameBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
print(' Self name bytes (ASCII): ${nameBytes.map((b) => b >= 32 && b <= 126 ? String.fromCharCode(b) : '.')}');
selfName = String.fromCharCodes(nameBytes.takeWhile((b) => b != 0));
print(' Self name (parsed): "$selfName"');
} }
print(' ✅ PARSED SUMMARY:');
print(' Type: $deviceType');
print(' TX Power: $txPower / $maxTxPower dBm');
print(' Position: ${advLat / 1000000.0}°, ${advLon / 1000000.0}°');
print(' Flags: multiAcks=$multiAcks, locPolicy=$advertLocPolicy, telemetry=$telemetryModes, manual=$manualAddContacts');
print(' Radio: freq=${radioFreq / 1000.0} MHz, bw=${radioBw / 1000.0} kHz, sf=$radioSf, cr=$radioCr');
print(' Name: "$selfName"');
// Call callback with parsed data // Call callback with parsed data
onSelfInfoReceived?.call({ onSelfInfoReceived?.call({
'protocolVersion': protocolVersion,
'deviceType': deviceType, 'deviceType': deviceType,
'txPower': txPower, 'txPower': txPower,
'maxTxPower': maxTxPower, 'maxTxPower': maxTxPower,
@@ -846,6 +957,132 @@ class MeshCoreBleService {
} }
} }
/// Handle MsgWaiting push (PUSH_CODE_MSG_WAITING)
///
/// This push notification indicates that new messages are waiting
/// in the device queue and should be fetched using syncNextMessage()
void _handleMsgWaiting(BufferReader reader) {
try {
print(' [MsgWaiting] New message(s) waiting in queue');
print(' ✅ [MsgWaiting] Notifying callback to fetch messages');
onMessageWaiting?.call();
} catch (e) {
print(' ❌ [MsgWaiting] Parsing error: $e');
// Don't call onError - this is informational
}
}
/// Handle LoginSuccess push (PUSH_CODE_LOGIN_SUCCESS)
///
/// Protocol format:
/// - 1 byte: permissions (lowest bit = is_admin)
/// - 6 bytes: public key prefix (first 6 bytes)
/// - 4 bytes: tag (int32)
/// - 1 byte: (V7+) new permissions
void _handleLoginSuccess(BufferReader reader) {
try {
print(' [LoginSuccess] Parsing login success...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
if (reader.remainingBytesCount >= 11) {
final permissions = reader.readByte();
final isAdmin = (permissions & 0x01) != 0;
print(' Permissions: $permissions (admin: $isAdmin)');
final publicKeyPrefix = reader.readBytes(6);
print(' Room public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
final tag = reader.readInt32LE();
print(' Tag: $tag');
// V7+ new permissions byte
int? newPermissions;
if (reader.hasRemaining) {
newPermissions = reader.readByte();
print(' New permissions (V7+): $newPermissions');
}
print(' ✅ [LoginSuccess] Successfully logged into room');
onLoginSuccess?.call(publicKeyPrefix, permissions, isAdmin, tag);
} else {
print(' ⚠️ [LoginSuccess] Insufficient data for full parsing');
}
} catch (e) {
print(' ❌ [LoginSuccess] Parsing error: $e');
onError?.call('Login success parsing error: $e');
}
}
/// Handle LoginFail push (PUSH_CODE_LOGIN_FAIL)
///
/// Protocol format:
/// - 1 byte: reserved (zero)
/// - 6 bytes: public key prefix (first 6 bytes)
void _handleLoginFail(BufferReader reader) {
try {
print(' [LoginFail] Parsing login fail...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
if (reader.remainingBytesCount >= 7) {
final reserved = reader.readByte();
print(' Reserved: $reserved');
final publicKeyPrefix = reader.readBytes(6);
print(' Room public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
print(' ❌ [LoginFail] Failed to login to room (incorrect password or access denied)');
onLoginFail?.call(publicKeyPrefix);
} else {
print(' ⚠️ [LoginFail] Insufficient data for full parsing');
}
} catch (e) {
print(' ❌ [LoginFail] Parsing error: $e');
onError?.call('Login fail parsing error: $e');
}
}
/// Handle Error response (RESP_CODE_ERR)
///
/// Protocol format:
/// - 1 byte: error code (ERR_CODE_*)
void _handleError(BufferReader reader) {
try {
print(' [Error] Parsing error response...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
if (reader.hasRemaining) {
final errorCode = reader.readByte();
String errorMsg = 'Error code: $errorCode';
switch (errorCode) {
case MeshCoreConstants.errUnsupportedCmd:
errorMsg = 'Unsupported command';
break;
case MeshCoreConstants.errNotFound:
errorMsg = 'Not found';
break;
case MeshCoreConstants.errTableFull:
errorMsg = 'Table full';
break;
case MeshCoreConstants.errBadState:
errorMsg = 'Bad state';
break;
case MeshCoreConstants.errFileIoError:
errorMsg = 'File I/O error';
break;
case MeshCoreConstants.errIllegalArg:
errorMsg = 'Illegal argument';
break;
}
print(' ❌ [Error] $errorMsg');
onError?.call(errorMsg);
}
} catch (e) {
print(' ❌ [Error] Parsing error: $e');
}
}
/// Send AppStart command /// Send AppStart command
Future<void> _sendAppStart() async { Future<void> _sendAppStart() async {
final writer = BufferWriter(); final writer = BufferWriter();
@@ -1027,6 +1264,53 @@ class MeshCoreBleService {
await _writeData(writer.toBytes()); await _writeData(writer.toBytes());
} }
/// Set other parameters (telemetry modes, advert location policy, manual add contacts)
///
/// Protocol format (CMD_SET_OTHER_PARAMS):
/// - 1 byte: command code (38)
/// - 1 byte: manual add contacts (0 or 1)
/// - 1 byte: telemetry modes (bits 0-1: Base mode, bits 2-3: Location mode)
/// Modes: 0=DENY, 1=apply contact.flags, 2=ALLOW ALL
/// - 1 byte: advert location policy (0=don't share, 1=share)
/// - 1 byte: multi ACKs (0=no extra, 1=send extra ACK)
Future<void> setOtherParams({
required int manualAddContacts, // 0 or 1
required int telemetryModes, // bits 0-1: Base, bits 2-3: Location
required int advertLocationPolicy, // 0=don't share, 1=share
int multiAcks = 0, // 0=no extra, 1=send extra
}) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetOtherParams);
writer.writeByte(manualAddContacts);
writer.writeByte(telemetryModes);
writer.writeByte(advertLocationPolicy);
writer.writeByte(multiAcks);
await _writeData(writer.toBytes());
}
/// Send login request to room or repeater
///
/// Protocol format (CMD_SEND_LOGIN):
/// - 1 byte: command code (26)
/// - 32 bytes: public key (room or repeater)
/// - N bytes: password (remainder of frame, varchar, max 15 bytes)
///
/// Response: PUSH_CODE_LOGIN_SUCCESS (0x85) or PUSH_CODE_LOGIN_FAIL (0x86)
Future<void> loginToRoom({
required Uint8List roomPublicKey,
required String password,
}) async {
if (password.length > 15) {
throw ArgumentError('Password exceeds 15 character limit');
}
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSendLogin);
writer.writeBytes(roomPublicKey); // 32 bytes
writer.writeString(password); // Max 15 bytes
await _writeData(writer.toBytes());
}
/// Log a packet /// Log a packet
void _logPacket(Uint8List data, PacketDirection direction, {int? responseCode}) { void _logPacket(Uint8List data, PacketDirection direction, {int? responseCode}) {
// Add new packet // Add new packet

View File

@@ -69,6 +69,9 @@ class MeshCoreConstants {
static const int respChannelInfo = 18; static const int respChannelInfo = 18;
static const int respSignStart = 19; static const int respSignStart = 19;
static const int respSignature = 20; static const int respSignature = 20;
static const int respCustomVars = 21;
static const int respAdvertPath = 22;
static const int respTuningParams = 21; // Same as respCustomVars per protocol
// Push Codes (Device -> App, unsolicited) // Push Codes (Device -> App, unsolicited)
static const int pushAdvert = 0x80; static const int pushAdvert = 0x80;