feat: Enhance MeshCoreBleService with new callbacks and message handling

- Added new callback types for path updates, message sent, message delivered, status responses, binary responses, and battery/storage information.
- Implemented handling for binary responses and path updates, including parsing and notifying via callbacks.
- Updated message sending logic to include acknowledgment and delivery confirmation.
- Enhanced log parsing for received data, including detailed interpretations and analysis.
- Introduced status request functionality to query operational status from repeater or sensor nodes.
- Updated battery and storage information handling to provide detailed metrics and trigger callbacks.
- Deprecated legacy methods in favor of more robust alternatives.
This commit is contained in:
Janez T
2025-10-15 09:31:19 +02:00
parent 05b90d7f1f
commit 9124b53073
17 changed files with 5685 additions and 190 deletions

View File

@@ -1,6 +1,44 @@
import 'dart:typed_data';
import '../services/meshcore_opcode_names.dart';
/// Decoded LOG_RX_DATA packet structure
class LogRxDataInfo {
final int? airtimeMs;
final Uint8List? senderPublicKey;
final int? ackCode;
final List<String> embeddedStrings;
final double entropy;
final bool isLikelyEncrypted;
LogRxDataInfo({
this.airtimeMs,
this.senderPublicKey,
this.ackCode,
this.embeddedStrings = const [],
required this.entropy,
required this.isLikelyEncrypted,
});
/// Get sender public key as hex string (short)
String? get senderKeyShort {
if (senderPublicKey == null || senderPublicKey!.length < 6) return null;
return senderPublicKey!
.sublist(0, 6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(':');
}
String get summary {
final parts = <String>[];
if (airtimeMs != null) parts.add('airtime:${airtimeMs}ms');
if (ackCode != null) parts.add('ACK:$ackCode');
if (senderKeyShort != null) parts.add('from:$senderKeyShort');
if (embeddedStrings.isNotEmpty) parts.add('strings:${embeddedStrings.length}');
if (isLikelyEncrypted) parts.add('encrypted');
return parts.join(', ');
}
}
/// Represents a logged BLE packet with timestamp and metadata
class BlePacketLog {
final DateTime timestamp;
@@ -8,6 +46,7 @@ class BlePacketLog {
final PacketDirection direction;
final int? responseCode;
final String? description;
final LogRxDataInfo? logRxDataInfo; // Decoded LOG_RX_DATA information
BlePacketLog({
required this.timestamp,
@@ -15,6 +54,7 @@ class BlePacketLog {
required this.direction,
this.responseCode,
this.description,
this.logRxDataInfo,
});
/// Convert raw data to hex string for display
@@ -63,7 +103,8 @@ class BlePacketLog {
final dir = direction == PacketDirection.rx ? 'RX' : 'TX';
final code = responseCode != null ? ' [$opcodeDescription]' : '';
final desc = description != null ? ' - $description' : '';
return '${timestamp.toIso8601String()} [$dir]$code ${rawData.length} bytes: $hexData$desc';
final logRxInfo = logRxDataInfo != null ? ' [${logRxDataInfo!.summary}]' : '';
return '${timestamp.toIso8601String()} [$dir]$code ${rawData.length} bytes: $hexData$desc$logRxInfo';
}
}

View File

@@ -16,6 +16,8 @@ class DeviceInfo {
final ConnectionState connectionState;
final int? batteryMilliVolts;
final double? batteryPercentage;
final int? storageUsedKb;
final int? storageTotalKb;
final int? signalRssi;
final double? signalSnr;
final DateTime? lastUpdate;
@@ -54,6 +56,8 @@ class DeviceInfo {
this.connectionState = ConnectionState.disconnected,
this.batteryMilliVolts,
this.batteryPercentage,
this.storageUsedKb,
this.storageTotalKb,
this.signalRssi,
this.signalSnr,
this.lastUpdate,
@@ -112,6 +116,32 @@ class DeviceInfo {
return 'Critical';
}
/// Get storage usage percentage (0-100)
double? get storageUsedPercent {
if (storageUsedKb == null || storageTotalKb == null || storageTotalKb == 0) {
return null;
}
return (storageUsedKb! / storageTotalKb!) * 100.0;
}
/// Get storage available in KB
int? get storageAvailableKb {
if (storageUsedKb == null || storageTotalKb == null) {
return null;
}
return storageTotalKb! - storageUsedKb!;
}
/// Get human-readable storage status
String get storageStatus {
final percent = storageUsedPercent;
if (percent == null) return 'Unknown';
if (percent < 50) return 'Plenty Available';
if (percent < 80) return 'Moderate Usage';
if (percent < 95) return 'Low Space';
return 'Critical - Nearly Full';
}
/// Get signal strength category
String get signalStrength {
if (signalRssi == null) return 'Unknown';
@@ -145,6 +175,8 @@ class DeviceInfo {
ConnectionState? connectionState,
int? batteryMilliVolts,
double? batteryPercentage,
int? storageUsedKb,
int? storageTotalKb,
int? signalRssi,
double? signalSnr,
DateTime? lastUpdate,
@@ -177,6 +209,8 @@ class DeviceInfo {
connectionState: connectionState ?? this.connectionState,
batteryMilliVolts: batteryMilliVolts ?? this.batteryMilliVolts,
batteryPercentage: batteryPercentage ?? this.batteryPercentage,
storageUsedKb: storageUsedKb ?? this.storageUsedKb,
storageTotalKb: storageTotalKb ?? this.storageTotalKb,
signalRssi: signalRssi ?? this.signalRssi,
signalSnr: signalSnr ?? this.signalSnr,
lastUpdate: lastUpdate ?? this.lastUpdate,

View File

@@ -25,6 +25,15 @@ enum MessageType {
channel,
}
/// Message delivery status
enum MessageDeliveryStatus {
sending, // Message is being sent
sent, // Message queued with expected ACK
delivered, // Delivery confirmed (ACK received)
failed, // Delivery failed
received, // Message received from another contact
}
/// MeshCore message model
class Message {
final String id;
@@ -45,6 +54,13 @@ class Message {
final DateTime receivedAt;
final String? senderName;
// Delivery tracking (for sent messages)
final MessageDeliveryStatus deliveryStatus;
final int? expectedAckTag; // Expected ACK/TAG from SENT response
final int? suggestedTimeoutMs; // Suggested timeout from SENT response
final int? roundTripTimeMs; // RTT from SEND_CONFIRMED
final DateTime? deliveredAt; // When delivery was confirmed
Message({
required this.id,
required this.messageType,
@@ -59,6 +75,11 @@ class Message {
this.sarGpsCoordinates,
required this.receivedAt,
this.senderName,
this.deliveryStatus = MessageDeliveryStatus.received,
this.expectedAckTag,
this.suggestedTimeoutMs,
this.roundTripTimeMs,
this.deliveredAt,
});
/// Get sender public key as hex string
@@ -120,6 +141,28 @@ class Message {
);
}
/// Get friendly delivery status description
String get deliveryStatusText {
switch (deliveryStatus) {
case MessageDeliveryStatus.sending:
return 'Sending...';
case MessageDeliveryStatus.sent:
return 'Sent';
case MessageDeliveryStatus.delivered:
if (roundTripTimeMs != null) {
return 'Delivered (${roundTripTimeMs}ms)';
}
return 'Delivered';
case MessageDeliveryStatus.failed:
return 'Failed';
case MessageDeliveryStatus.received:
return '';
}
}
/// Check if this is a sent message (not received)
bool get isSentMessage => deliveryStatus != MessageDeliveryStatus.received;
Message copyWith({
String? id,
MessageType? messageType,
@@ -134,6 +177,11 @@ class Message {
LatLng? sarGpsCoordinates,
DateTime? receivedAt,
String? senderName,
MessageDeliveryStatus? deliveryStatus,
int? expectedAckTag,
int? suggestedTimeoutMs,
int? roundTripTimeMs,
DateTime? deliveredAt,
}) {
return Message(
id: id ?? this.id,
@@ -149,6 +197,11 @@ class Message {
sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates,
receivedAt: receivedAt ?? this.receivedAt,
senderName: senderName ?? this.senderName,
deliveryStatus: deliveryStatus ?? this.deliveryStatus,
expectedAckTag: expectedAckTag ?? this.expectedAckTag,
suggestedTimeoutMs: suggestedTimeoutMs ?? this.suggestedTimeoutMs,
roundTripTimeMs: roundTripTimeMs ?? this.roundTripTimeMs,
deliveredAt: deliveredAt ?? this.deliveredAt,
);
}