feat: Implement smart ping with automatic fallback and enhance telemetry handling

This commit is contained in:
Janez T
2025-10-15 19:37:37 +02:00
parent 91d9326804
commit 24234e7000
7 changed files with 471 additions and 8 deletions

View File

@@ -91,11 +91,23 @@ class AppProvider with ChangeNotifier {
}
};
// When telemetry is received
// When telemetry is received via PUSH_CODE_TELEMETRY_RESPONSE (0x8B)
// Used by older firmware versions for telemetry responses
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
debugPrint('📊 [AppProvider] Telemetry response (0x8B) received - updating contact');
contactsProvider.updateTelemetry(publicKey, lppData);
};
// When binary response is received via PUSH_CODE_BINARY_RESPONSE (0x8C)
// Used by newer firmware versions for telemetry and other binary data
// BOTH callbacks (0x8B and 0x8C) must be handled for device compatibility
connectionProvider.onBinaryResponse = (publicKeyPrefix, tag, responseData) {
debugPrint('📊 [AppProvider] Binary response (0x8C) received - updating contact telemetry');
// Binary response tag 0 = telemetry data (Cayenne LPP format)
// Other tags may be used for different data types in the future
contactsProvider.updateTelemetry(publicKeyPrefix, responseData);
};
// When a contact's routing path is updated in the mesh network
connectionProvider.onPathUpdated = (publicKey) {
debugPrint('🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');

View File

@@ -9,6 +9,22 @@ import '../services/meshcore_ble_service.dart';
import '../utils/sar_message_parser.dart';
import 'helpers/room_login_manager.dart';
import 'helpers/message_delivery_tracker.dart';
import 'helpers/ping_tracker.dart';
/// Result of a ping (telemetry request) operation
class PingResult {
final bool success;
final bool usedFlooding;
final bool timedOut;
final bool retriedWithFlooding;
const PingResult({
required this.success,
required this.usedFlooding,
required this.timedOut,
this.retriedWithFlooding = false,
});
}
/// Connection Provider - manages MeshCore BLE connection
class ConnectionProvider with ChangeNotifier {
@@ -53,6 +69,7 @@ class ConnectionProvider with ChangeNotifier {
// Helper instances
final RoomLoginManager _roomLoginManager = RoomLoginManager();
final MessageDeliveryTracker _messageDeliveryTracker = MessageDeliveryTracker();
final PingTracker _pingTracker = PingTracker();
// Expose room login states
Map<String, RoomLoginState> get roomLoginStates => _roomLoginManager.roomLoginStates;
@@ -133,6 +150,8 @@ class ConnectionProvider with ChangeNotifier {
};
_bleService.onTelemetryReceived = (publicKey, lppData) {
// Mark ping as successful if this was a ping request
_pingTracker.markPingSuccessful(publicKey);
onTelemetryReceived?.call(publicKey, lppData);
};
@@ -141,6 +160,9 @@ class ConnectionProvider with ChangeNotifier {
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
print(' Tag: $tag');
print(' Response data: ${responseData.length} bytes');
// Mark ping as successful if this was a ping request
// Binary responses can also be telemetry responses (newer firmware)
_pingTracker.markPingSuccessful(publicKeyPrefix);
onBinaryResponse?.call(publicKeyPrefix, tag, responseData);
};
@@ -411,6 +433,7 @@ class ConnectionProvider with ChangeNotifier {
connectionState: ConnectionState.disconnected,
);
_roomLoginManager.clearRoomLoginStates(); // Clear login states on disconnect
_pingTracker.clearAll(); // Clear pending pings on disconnect
notifyListeners();
}
@@ -557,8 +580,20 @@ class ConnectionProvider with ChangeNotifier {
}
/// Request telemetry from contact
///
/// COMPATIBILITY NOTE: This method sends CMD_SEND_TELEMETRY_REQ (39).
/// Depending on device firmware version, the response will be either:
/// - PUSH_CODE_TELEMETRY_RESPONSE (0x8B) - older firmware
/// - PUSH_CODE_BINARY_RESPONSE (0x8C) - newer firmware
///
/// Both response types are handled via callbacks:
/// - onTelemetryReceived (for 0x8B)
/// - onBinaryResponse (for 0x8C)
///
/// The app properly handles BOTH response types, so this method is NOT
/// deprecated and should continue to be used for telemetry requests.
///
/// [zeroHop] - if true, only direct connection (no mesh forwarding)
@Deprecated('Use requestBinary() instead for better functionality')
Future<void> requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
@@ -574,6 +609,89 @@ class ConnectionProvider with ChangeNotifier {
}
}
/// Smart ping with automatic fallback to flooding
///
/// Sends a telemetry request (ping) to a contact, and if no response is
/// received within timeout, automatically retries with flooding mode.
///
/// Returns a PingResult with information about the response.
///
/// [contact] - the contact to ping (used to determine if path exists)
/// [onRetryWithFlooding] - optional callback when fallback to flooding occurs
Future<PingResult> smartPing({
required Uint8List contactPublicKey,
required bool hasPath,
Function()? onRetryWithFlooding,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return PingResult(success: false, usedFlooding: false, timedOut: true);
}
// First attempt: Use zeroHop (direct) if we have a path, otherwise use flooding
final bool firstAttemptDirect = hasPath;
try {
// Track the ping request
final pingFuture = _pingTracker.trackPing(
publicKey: contactPublicKey,
wasDirectAttempt: firstAttemptDirect,
);
// Send the ping
await _bleService.requestTelemetry(contactPublicKey, zeroHop: true);
// Wait for response or timeout
final bool gotResponse = await pingFuture;
if (gotResponse) {
// Success on first attempt
return PingResult(
success: true,
usedFlooding: !firstAttemptDirect,
timedOut: false,
);
}
// First attempt timed out - retry with flooding if first was direct
if (firstAttemptDirect) {
print('⚠️ [Provider] Ping timeout on direct attempt, retrying with flooding...');
onRetryWithFlooding?.call();
// Track the retry
final retryFuture = _pingTracker.trackPing(
publicKey: contactPublicKey,
wasDirectAttempt: false,
);
// Retry with flooding (zeroHop=true acts as broadcast to neighbors)
await _bleService.requestTelemetry(contactPublicKey, zeroHop: true);
// Wait for response or timeout
final bool gotRetryResponse = await retryFuture;
return PingResult(
success: gotRetryResponse,
usedFlooding: true,
timedOut: !gotRetryResponse,
retriedWithFlooding: true,
);
}
// First attempt was already flooding and it timed out
return PingResult(
success: false,
usedFlooding: true,
timedOut: true,
);
} catch (e) {
_error = 'Failed to ping contact: $e';
notifyListeners();
return PingResult(success: false, usedFlooding: false, timedOut: true);
}
}
/// Send binary request to contact (modern replacement for requestTelemetry)
///
/// Supports multiple request types:

View File

@@ -161,20 +161,38 @@ class ContactsProvider with ChangeNotifier {
/// Update contact telemetry
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
print('📊 [ContactsProvider] updateTelemetry() called');
print(' Public key prefix (hex): ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
print(' LPP data size: ${lppData.length} bytes');
// Find contact by public key prefix
final contact = _findContactByPrefix(publicKeyPrefix);
if (contact == null) return;
if (contact == null) {
print(' ❌ Contact not found for this prefix');
return;
}
print(' ✅ Found contact: ${contact.advName}');
print(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
try {
// Parse Cayenne LPP data
final telemetry = CayenneLppParser.parse(lppData);
print(' ✅ Parsed new telemetry');
print(' New telemetry timestamp: ${telemetry.timestamp}');
// Update contact with new telemetry
final updatedContact = contact.copyWith(telemetry: telemetry);
_contacts[contact.publicKeyHex] = updatedContact;
print(' ✅ Updated contact in map');
_persistContacts();
print(' ✅ Persisted contacts to storage');
notifyListeners();
print(' ✅ Notified listeners - UI should update');
} catch (e) {
print(' ❌ Failed to parse telemetry: $e');
debugPrint('Failed to parse telemetry: $e');
}
}

View File

@@ -0,0 +1,103 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
/// Helper class to track pending ping (telemetry) requests
/// and implement automatic fallback to flooding if no response received
class PingTracker {
// Map of public key hex string to ping request state
final Map<String, _PingRequest> _pendingPings = {};
// Timeout duration for ping responses (seconds)
static const int _pingTimeoutSeconds = 5;
/// Track a new ping request
/// Returns a Future that completes when either:
/// - A response is received (completes with true)
/// - Timeout occurs (completes with false)
Future<bool> trackPing({
required Uint8List publicKey,
required bool wasDirectAttempt,
}) {
final String keyHex = _publicKeyToHex(publicKey);
// Cancel any existing pending ping for this contact
_pendingPings[keyHex]?.cancel();
// Create new ping request tracker
final completer = Completer<bool>();
final timer = Timer(const Duration(seconds: _pingTimeoutSeconds), () {
// Timeout occurred - mark as failed
_pendingPings.remove(keyHex);
if (!completer.isCompleted) {
completer.complete(false);
}
});
_pendingPings[keyHex] = _PingRequest(
publicKey: publicKey,
wasDirectAttempt: wasDirectAttempt,
timer: timer,
completer: completer,
);
return completer.future;
}
/// Mark a ping as successful (response received)
/// Should be called when telemetry response arrives
void markPingSuccessful(Uint8List publicKey) {
final String keyHex = _publicKeyToHex(publicKey);
final request = _pendingPings.remove(keyHex);
if (request != null) {
request.cancel();
if (!request.completer.isCompleted) {
request.completer.complete(true);
}
}
}
/// Check if there's a pending ping for this contact
bool hasPendingPing(Uint8List publicKey) {
final String keyHex = _publicKeyToHex(publicKey);
return _pendingPings.containsKey(keyHex);
}
/// Get pending ping info (was it a direct attempt?)
bool? wasPingDirect(Uint8List publicKey) {
final String keyHex = _publicKeyToHex(publicKey);
return _pendingPings[keyHex]?.wasDirectAttempt;
}
/// Clear all pending pings (useful on disconnect)
void clearAll() {
for (final request in _pendingPings.values) {
request.cancel();
}
_pendingPings.clear();
}
/// Convert public key to hex string for map key
String _publicKeyToHex(Uint8List publicKey) {
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
}
}
/// Internal class to track a single ping request
class _PingRequest {
final Uint8List publicKey;
final bool wasDirectAttempt;
final Timer timer;
final Completer<bool> completer;
_PingRequest({
required this.publicKey,
required this.wasDirectAttempt,
required this.timer,
required this.completer,
});
void cancel() {
timer.cancel();
}
}