import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../models/device_info.dart'; import '../models/contact.dart'; import '../models/message.dart'; import '../models/room_login_state.dart'; import '../services/meshcore_ble_service.dart'; import '../utils/sar_message_parser.dart'; import 'helpers/room_login_manager.dart'; import 'helpers/message_delivery_tracker.dart'; /// Connection Provider - manages MeshCore BLE connection class ConnectionProvider with ChangeNotifier { final MeshCoreBleService _bleService = MeshCoreBleService(); /// Expose BLE service for background location tracking MeshCoreBleService get bleService => _bleService; DeviceInfo _deviceInfo = DeviceInfo(); DeviceInfo get deviceInfo => _deviceInfo; List _scannedDevices = []; List get scannedDevices => _scannedDevices; bool _isScanning = false; bool get isScanning => _isScanning; String? _error; String? get error => _error; // Activity indicators (for blinking) bool _rxActivity = false; bool _txActivity = false; bool get rxActivity => _rxActivity; bool get txActivity => _txActivity; Timer? _rxActivityTimer; Timer? _txActivityTimer; // Packet counters int get rxPacketCount => _bleService.rxPacketCount; int get txPacketCount => _bleService.txPacketCount; // Reconnection state (exposed from BLE service) bool get isReconnecting => _bleService.isReconnecting; int get reconnectionAttempt => _bleService.reconnectionAttempt; int get maxReconnectionAttempts => _bleService.maxReconnectionAttempts; // Message sync state bool _noMoreMessages = false; // Helper instances final RoomLoginManager _roomLoginManager = RoomLoginManager(); final MessageDeliveryTracker _messageDeliveryTracker = MessageDeliveryTracker(); // Expose room login states Map get roomLoginStates => _roomLoginManager.roomLoginStates; // Callbacks for other providers Function(Contact)? onContactReceived; Function(List)? onContactsComplete; Function(Message)? onMessageReceived; Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived; Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)? onBinaryResponse; Function(Uint8List publicKey)? onPathUpdated; Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? onLoginSuccess; Function(Uint8List publicKeyPrefix)? onLoginFail; Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)? onMessageSent; Function(int ackCode, int roundTripTimeMs)? onMessageDelivered; Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse; ConnectionProvider() { _initializeBleService(); } void _initializeBleService() { _bleService.onConnectionStateChanged = (isConnected) { print('🔔 [Provider] Connection state callback fired: $isConnected'); _deviceInfo = _deviceInfo.copyWith( connectionState: isConnected ? ConnectionState.connected : (_bleService.isReconnecting ? ConnectionState.connecting : ConnectionState.disconnected), lastUpdate: DateTime.now(), ); print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}'); print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}'); print(' isReconnecting: ${_bleService.isReconnecting}'); notifyListeners(); print(' Notified listeners'); }; _bleService.onReconnectionAttempt = (attemptNumber, maxAttempts) { print('🔄 [Provider] Reconnection attempt $attemptNumber/$maxAttempts'); // Notify UI to update reconnection status display notifyListeners(); }; _bleService.onError = (error) { print('⚠️ [Provider] BLE error received: $error'); print(' Current connection state: ${_deviceInfo.connectionState}'); _error = error; // Only set connection state to error if we're not already connected // Data parsing errors after connection shouldn't disconnect us if (_deviceInfo.connectionState != ConnectionState.connected) { print(' Setting connection state to error'); _deviceInfo = _deviceInfo.copyWith( connectionState: ConnectionState.error, ); } else { print(' Keeping connection state as connected (ignoring data parsing error)'); } notifyListeners(); }; _bleService.onContactReceived = (contact) { onContactReceived?.call(contact); }; _bleService.onContactsComplete = (contacts) { onContactsComplete?.call(contacts); }; _bleService.onMessageReceived = (message) { // Parse SAR markers final enhancedMessage = SarMessageParser.enhanceMessage(message); onMessageReceived?.call(enhancedMessage); }; _bleService.onTelemetryReceived = (publicKey, lppData) { onTelemetryReceived?.call(publicKey, lppData); }; _bleService.onBinaryResponse = (publicKeyPrefix, tag, responseData) { print('📥 [Provider] Binary response received'); print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); print(' Tag: $tag'); print(' Response data: ${responseData.length} bytes'); onBinaryResponse?.call(publicKeyPrefix, tag, responseData); }; _bleService.onNoMoreMessages = () { print('📥 [Provider] Received NoMoreMessages signal'); _noMoreMessages = true; }; _bleService.onMessageWaiting = () { print('📥 [Provider] PUSH_CODE_MSG_WAITING received - auto-fetching messages via event'); // Automatically fetch messages when push notification received // This is the CORRECT way to receive messages - room server pushes them syncAllMessages(); }; _bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async { 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'); // Update room login state via helper await _roomLoginManager.handleLoginSuccess( publicKeyPrefix: publicKeyPrefix, permissions: permissions, isAdmin: isAdmin, tag: tag, ); notifyListeners(); 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(':')}'); // Update room login state to logged out via helper _roomLoginManager.handleLoginFail( publicKeyPrefix: publicKeyPrefix, ); notifyListeners(); onLoginFail?.call(publicKeyPrefix); }; _bleService.onAdvertReceived = (publicKey) { print('📥 [Provider] Advert received from node'); print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...'); print(' Note: Waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full contact details'); // The companion radio will automatically send PUSH_CODE_NEW_ADVERT if manual_add_contacts=0 // which will trigger onContactReceived callback and add/update the contact }; _bleService.onPathUpdated = (publicKey) { print('📥 [Provider] Path updated for contact'); print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...'); print(' Note: Mesh network discovered a new/better routing path to this contact'); // Forward the callback to ContactsProvider to trigger contact sync onPathUpdated?.call(publicKey); }; _bleService.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) { print('📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms'); // Pop the first pending message ID from the queue (FIFO) via helper final messageId = _messageDeliveryTracker.popPendingMessageId(); if (messageId != null) { print(' Matched with message ID: $messageId'); // Store the ACK tag to message ID mapping for delivery confirmation _messageDeliveryTracker.mapAckTagToMessageId(expectedAckTag, messageId); // Notify callback with message ID onMessageSent?.call(messageId, expectedAckTag, suggestedTimeoutMs); } else { print('⚠️ [Provider] SENT response received but no pending message IDs'); } }; _bleService.onMessageDelivered = (ackCode, roundTripTimeMs) { print('📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms'); onMessageDelivered?.call(ackCode, roundTripTimeMs); }; _bleService.onStatusResponse = (publicKeyPrefix, statusData) { print('📥 [Provider] Status response received from node'); print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); print(' Status data: ${statusData.length} bytes'); // Forward the callback to whoever needs it (e.g., ContactsProvider) onStatusResponse?.call(publicKeyPrefix, statusData); }; _bleService.onDeviceInfoReceived = (deviceInfo) { print('📥 [Provider] Received DeviceInfo:'); print(' Firmware Version: ${deviceInfo['firmwareVersion']}'); print(' Max Contacts: ${deviceInfo['maxContacts']}'); print(' Max Channels: ${deviceInfo['maxChannels']}'); print(' BLE PIN: ${deviceInfo['blePin']}'); print(' Build Date: ${deviceInfo['firmwareBuildDate']}'); print(' Model: ${deviceInfo['manufacturerModel']}'); print(' Version: ${deviceInfo['semanticVersion']}'); _deviceInfo = _deviceInfo.copyWith( firmwareVersion: deviceInfo['firmwareVersion'] as int?, maxContacts: deviceInfo['maxContacts'] as int?, maxChannels: deviceInfo['maxChannels'] as int?, blePin: deviceInfo['blePin'] as int?, firmwareBuildDate: deviceInfo['firmwareBuildDate'] as String?, manufacturerModel: deviceInfo['manufacturerModel'] as String?, semanticVersion: deviceInfo['semanticVersion'] as String?, ); notifyListeners(); print('✅ [Provider] Device info updated with DeviceInfo'); }; _bleService.onSelfInfoReceived = (selfInfo) { print('📥 [Provider] Received SelfInfo:'); print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm'); print(' Radio: freq=${selfInfo['radioFreq']}, bw=${selfInfo['radioBw']}, sf=${selfInfo['radioSf']}, cr=${selfInfo['radioCr']}'); print(' Position: ${selfInfo['advLat'] / 1000000.0}, ${selfInfo['advLon'] / 1000000.0}'); print(' Self Name: ${selfInfo['selfName']}'); _deviceInfo = _deviceInfo.copyWith( deviceType: selfInfo['deviceType'] as int?, txPower: selfInfo['txPower'] as int?, maxTxPower: selfInfo['maxTxPower'] as int?, publicKey: selfInfo['publicKey'] as Uint8List?, advLat: selfInfo['advLat'] as int?, advLon: selfInfo['advLon'] as int?, manualAddContacts: selfInfo['manualAddContacts'] as bool?, radioFreq: selfInfo['radioFreq'] as int?, radioBw: selfInfo['radioBw'] as int?, radioSf: selfInfo['radioSf'] as int?, radioCr: selfInfo['radioCr'] as int?, selfName: selfInfo['selfName'] as String?, ); notifyListeners(); print('✅ [Provider] Device info updated with SelfInfo'); }; // Activity indicators _bleService.onBatteryAndStorage = (millivolts, usedKb, totalKb) { print('📥 [Provider] Received BatteryAndStorage:'); print(' Battery: ${millivolts}mV (${(millivolts / 1000.0).toStringAsFixed(2)}V)'); if (usedKb != null) { print(' Storage Used: ${usedKb}KB'); } if (totalKb != null) { print(' Storage Total: ${totalKb}KB'); if (totalKb > 0 && usedKb != null) { final usedPercent = (usedKb / totalKb) * 100.0; print(' Storage Usage: ${usedPercent.toStringAsFixed(1)}%'); } } _deviceInfo = _deviceInfo.copyWith( batteryMilliVolts: millivolts, storageUsedKb: usedKb, storageTotalKb: totalKb, lastUpdate: DateTime.now(), ); notifyListeners(); print('✅ [Provider] Device info updated with BatteryAndStorage'); }; _bleService.onRxActivity = () { _rxActivity = true; notifyListeners(); // Reset after 100ms _rxActivityTimer?.cancel(); _rxActivityTimer = Timer(const Duration(milliseconds: 100), () { _rxActivity = false; notifyListeners(); }); }; _bleService.onTxActivity = () { _txActivity = true; notifyListeners(); // Reset after 100ms _txActivityTimer?.cancel(); _txActivityTimer = Timer(const Duration(milliseconds: 100), () { _txActivity = false; notifyListeners(); }); }; } /// Start scanning for MeshCore devices Future startScan() async { print('🔍 [Provider] startScan() called'); _isScanning = true; _scannedDevices.clear(); _error = null; notifyListeners(); print('✅ [Provider] Scan state initialized, notifying listeners'); try { await for (final device in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) { print('📱 [Provider] Device received from scan stream'); if (!_scannedDevices.any((d) => d.remoteId == device.remoteId)) { _scannedDevices.add(device); print('✅ [Provider] Added device to list: ${device.platformName}, total: ${_scannedDevices.length}'); notifyListeners(); } else { print(' ⏭️ [Provider] Device already in list, skipping'); } } } catch (e) { print('❌ [Provider] Scan error: $e'); _error = 'Scan error: $e'; } finally { print('🏁 [Provider] Scan completed'); _isScanning = false; notifyListeners(); } } /// Stop scanning Future stopScan() async { await FlutterBluePlus.stopScan(); _isScanning = false; notifyListeners(); } /// Connect to a device Future connect(BluetoothDevice device) async { print('🔵 [Provider] connect() called for device: ${device.platformName}'); _deviceInfo = _deviceInfo.copyWith( deviceId: device.remoteId.toString(), deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown', connectionState: ConnectionState.connecting, ); _error = null; print('✅ [Provider] Device info updated to connecting state'); notifyListeners(); print('🔵 [Provider] Calling BLE service connect()...'); final success = await _bleService.connect(device); if (success) { print('✅ [Provider] BLE service connect() returned success'); } else { print('❌ [Provider] BLE service connect() returned failure'); _deviceInfo = _deviceInfo.copyWith( connectionState: ConnectionState.error, ); notifyListeners(); } return success; } /// Disconnect from device Future disconnect() async { _deviceInfo = _deviceInfo.copyWith( connectionState: ConnectionState.disconnecting, ); notifyListeners(); await _bleService.disconnect(); _deviceInfo = DeviceInfo( connectionState: ConnectionState.disconnected, ); _roomLoginManager.clearRoomLoginStates(); // Clear login states on disconnect notifyListeners(); } /// Cancel ongoing reconnection attempts /// This is useful when the user wants to manually disconnect during reconnection void cancelReconnection() { print('🔴 [Provider] User requested cancellation of reconnection'); disconnect(); } /// Get contacts from device Future getContacts() async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.getContacts(); } catch (e) { _error = 'Failed to get contacts: $e'; notifyListeners(); } } /// Add or update a contact on the companion radio /// /// This manually adds a contact to the radio's internal contact table. /// Useful when a room contact was deleted or never advertised yet. Future addOrUpdateContact(Contact contact) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.addOrUpdateContact(contact); } catch (e) { _error = 'Failed to add/update contact: $e'; notifyListeners(); } } /// Send text message to contact /// /// Returns true if the message was successfully sent to the BLE service. /// Note: This doesn't mean the message was delivered over the mesh network, /// only that it was queued on the companion radio. /// /// [messageId] - optional message ID to track delivery status /// [contact] - optional contact object for path status logging Future sendTextMessage({ required Uint8List contactPublicKey, required String text, String? messageId, Contact? contact, }) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return false; } try { // Log path status if contact info is available if (contact != null) { print('📤 [ConnectionProvider] Sending message to ${contact.advName}'); print(' Type: ${contact.type.displayName}'); print(' Path status: ${contact.pathDescription}'); if (contact.hasPath) { print(' ✅ Using learned path (${contact.outPathLen} bytes)'); } else { print(' ⚠️ No path available - will use flood mode'); } } // IMPORTANT: Track pending message BEFORE sending to avoid race condition // The SENT response can arrive so quickly that if we track after sending, // the callback will fire before we add the message ID to the queue. if (messageId != null) { _messageDeliveryTracker.trackPendingMessage(messageId); print(' Added message ID to pending queue BEFORE sending: $messageId'); } // Send the message await _bleService.sendTextMessage( contactPublicKey: contactPublicKey, text: text, ); return true; } catch (e) { _error = 'Failed to send message: $e'; notifyListeners(); return false; } } /// Send channel message /// /// [messageId] - optional message ID to track delivery status /// Note: Channel messages are ephemeral (not persisted), so they're marked /// as "sent" immediately upon receiving OK response from the device. Future sendChannelMessage({ required int channelIdx, required String text, String? messageId, }) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.sendChannelMessage( channelIdx: channelIdx, text: text, ); // Channel messages are ephemeral (not persisted) - mark as "sent" immediately // They don't have ACK/TAG mechanism like direct messages if (messageId != null) { print('✅ [ConnectionProvider] Channel message sent successfully'); print(' Message ID: $messageId'); print(' onMessageSent callback exists: ${onMessageSent != null}'); // Small delay to ensure the message is in the MessagesProvider list // before we try to mark it as sent await Future.delayed(const Duration(milliseconds: 50)); // Use a dummy ACK tag (0) and timeout (0) for channel messages // This will trigger the callback to mark the message as "sent" print(' Calling onMessageSent callback...'); onMessageSent?.call(messageId, 0, 0); print(' onMessageSent callback completed'); } } catch (e) { _error = 'Failed to send channel message: $e'; notifyListeners(); } } /// Request telemetry from contact /// [zeroHop] - if true, only direct connection (no mesh forwarding) @Deprecated('Use requestBinary() instead for better functionality') Future requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.requestTelemetry(contactPublicKey, zeroHop: zeroHop); } catch (e) { _error = 'Failed to request telemetry: $e'; notifyListeners(); } } /// Send binary request to contact (modern replacement for requestTelemetry) /// /// Supports multiple request types: /// - Telemetry data (use MeshCoreConstants.binaryReqGetTelemetryData) /// - Average/min/max telemetry (use MeshCoreConstants.binaryReqGetAvgMinMax) /// - Access list (use MeshCoreConstants.binaryReqGetAccessList) /// - Neighbors list (use MeshCoreConstants.binaryReqGetNeighbours) /// /// Response arrives via onBinaryResponse callback with matching tag. /// /// Example - request telemetry: /// ```dart /// connectionProvider.onBinaryResponse = (prefix, tag, data) { /// // Parse telemetry data (Cayenne LPP format) /// final telemetry = CayenneLppParser.parse(data); /// }; /// await connectionProvider.requestBinary( /// contactPublicKey: contact.publicKey, /// requestType: MeshCoreConstants.binaryReqGetTelemetryData, /// ); /// ``` Future requestBinary({ required Uint8List contactPublicKey, required int requestType, Uint8List? additionalParams, }) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { // Build request data: request type byte + optional params final requestData = Uint8List.fromList([ requestType, if (additionalParams != null) ...additionalParams, ]); await _bleService.sendBinaryRequest( contactPublicKey: contactPublicKey, requestData: requestData, ); } catch (e) { _error = 'Failed to send binary request: $e'; notifyListeners(); } } /// Get device time from companion radio to detect clock drift Future getDeviceTime() async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.getDeviceTime(); } catch (e) { _error = 'Failed to get device time: $e'; notifyListeners(); } } /// Set device time to current time Future syncDeviceTime() async { if (!_bleService.isConnected) return; try { await _bleService.setDeviceTime(); } catch (e) { _error = 'Failed to sync time: $e'; notifyListeners(); } } /// Set advertised name Future setAdvertName(String name) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.setAdvertName(name); } catch (e) { _error = 'Failed to set name: $e'; notifyListeners(); } } /// Set advertised position Future setAdvertLatLon({ required double latitude, required double longitude, }) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.setAdvertLatLon( latitude: latitude, longitude: longitude, ); } catch (e) { _error = 'Failed to set position: $e'; notifyListeners(); } } /// Send self advertisement to mesh network /// /// Broadcasts the device's current advertisement data (name, location, etc.) /// to the mesh network. Use this after updating position or name to notify /// other nodes of the change. /// /// [floodMode] - if true, broadcast to entire mesh (default for SAR ops) /// if false, only send to direct neighbors (zero-hop) Future sendSelfAdvert({bool floodMode = true}) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.sendSelfAdvert(floodMode: floodMode); } catch (e) { _error = 'Failed to send advertisement: $e'; notifyListeners(); } } /// Set radio parameters Future setRadioParams({ required int frequency, required int bandwidth, required int spreadingFactor, required int codingRate, }) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.setRadioParams( frequency: frequency, bandwidth: bandwidth, spreadingFactor: spreadingFactor, codingRate: codingRate, ); } catch (e) { _error = 'Failed to set radio params: $e'; notifyListeners(); } } /// Set transmit power Future setTxPower(int powerDbm) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.setTxPower(powerDbm); } catch (e) { _error = 'Failed to set TX power: $e'; notifyListeners(); } } /// Set other parameters (telemetry modes, advert location policy) Future 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) Future refreshDeviceInfo() async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { // The device query command triggers a SelfInfo response await _bleService.refreshDeviceInfo(); } catch (e) { _error = 'Failed to refresh device info: $e'; notifyListeners(); } } /// Request battery and storage information /// /// Queries the companion radio for: /// - Battery voltage in millivolts /// - Used storage in KB (if available) /// - Total storage in KB (if available) /// /// Results arrive via onBatteryAndStorage callback and update deviceInfo. Future getBatteryAndStorage() async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.getBatteryAndStorage(); } catch (e) { _error = 'Failed to get battery and storage: $e'; notifyListeners(); } } /// Sync messages from device queue /// Call this repeatedly until no more messages are available Future syncNextMessage() async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return false; } try { await _bleService.syncNextMessage(); return true; } catch (e) { _error = 'Failed to sync message: $e'; notifyListeners(); return false; } } /// Sync all waiting messages from device Future syncAllMessages() async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return 0; } int count = 0; _noMoreMessages = false; // Reset flag try { print('🔄 [Provider] Starting message sync loop...'); print(' Initial _noMoreMessages state: $_noMoreMessages'); // Keep syncing until we get NoMoreMessages response // The device will send ContactMsgRecv or ChannelMsgRecv responses // until it sends NoMoreMessages for (int i = 0; i < 100; i++) { // Safety limit // Check flag BEFORE sending (not after) if (_noMoreMessages) { print('✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests'); break; } print('📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE'); await _bleService.syncNextMessage(); count++; // Small delay to allow response to be processed await Future.delayed(const Duration(milliseconds: 150)); print(' After iteration ${i + 1}: _noMoreMessages=$_noMoreMessages'); } if (!_noMoreMessages && count >= 100) { print('⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages'); } print('🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages'); return count; } catch (e) { print('❌ [Provider] Failed to sync messages: $e'); _error = 'Failed to sync messages: $e'; notifyListeners(); return count; } } /// 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 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(); } } /// Request status from repeater or sensor node /// /// Sends a status request to query operational status of a node. /// Results will be delivered via onStatusResponse callback. /// /// Example usage: /// ```dart /// connectionProvider.onStatusResponse = (publicKeyPrefix, statusData) { /// print('Status from node: ${utf8.decode(statusData)}'); /// }; /// await connectionProvider.requestStatus(repeaterContact.publicKey); /// ``` Future requestStatus(Uint8List contactPublicKey) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.sendStatusRequest(contactPublicKey); } catch (e) { _error = 'Failed to send status request: $e'; notifyListeners(); } } /// Reset routing path for a contact /// /// Clears the learned path to a contact, forcing the next message to use /// flood routing to discover a new route. Useful when: /// - A mobile repeater has moved and the path is broken /// - You want to find a better/shorter route /// - Direct messages are timing out due to path issues /// /// After calling this, the device will automatically fall back to flood mode /// for the next message to this contact, and learn a new path from the response. Future resetPath(Uint8List contactPublicKey) async { if (!_bleService.isConnected) { _error = 'Not connected to device'; notifyListeners(); return; } try { await _bleService.resetPath(contactPublicKey); } catch (e) { _error = 'Failed to reset path: $e'; notifyListeners(); } } /// Clear error message void clearError() { _error = null; notifyListeners(); } /// Get login state for a room by public key prefix RoomLoginState? getRoomLoginState(Uint8List publicKeyPrefix) { return _roomLoginManager.getRoomLoginState(publicKeyPrefix); } /// Check if logged into a specific room bool isLoggedIntoRoom(Uint8List publicKeyPrefix) { return _roomLoginManager.isLoggedIntoRoom(publicKeyPrefix); } @override void dispose() { _rxActivityTimer?.cancel(); _txActivityTimer?.cancel(); _bleService.dispose(); super.dispose(); } }