Fix RSSI nil display issue (#316)

* Add comprehensive RSSI debugging logs

- Log RSSI capture during discovery phase
- Log RSSI reading after peripheral connection
- Log didReadRSSI results including validation and retries
- Log getPeerRSSI() calls and dictionary contents
- Log RSSI transfer from temp IDs to real peer IDs
- Add UI logging to trace nil RSSI values in ContentView
- Track RSSI flow from Bluetooth discovery to UI display

* Add detailed logging for RSSI transfer from temp ID to real peer ID

- Log peripheral mapping updates during announce packet handling
- Track connectedPeripherals state before and after mapping
- Log RSSI transfer from peripheralRSSI to peerRSSI
- Identify whether temp ID is found and properly transferred

* Add peripheral lookup fallback for announce packets

- Log whether peripheral is present when announce received
- Add fallback to look up peripheral if not passed as parameter
- This handles cases where announce is received via relay

* Add comprehensive state logging to getPeerRSSI

- Log connectedPeripherals mapping state
- Log peripheralRSSI dictionary contents
- Log UI-side RSSI lookup attempts with dictionary state
- Track exactly what's in the RSSI lookup tables

* Log peer list shown in UI

* Ensure RSSI transfers even when no temp ID mapping exists

* Add debugging for announce packets without peripheral reference

* Fix build errors and add type annotations

* Fix RSSI mapping for relayed announce packets

- Add fallback logic to match unmapped peripherals with announced peers
- Transfer RSSI from temp UUID to real peer ID when single unmapped peripheral exists
- Implement periodic RSSI updates every 10 seconds for all connected peripherals
- Improve RSSI debugging logs to track mapping state

* Fix RSSI mapping condition for single unmapped peripheral

- Remove peerNicknames.count == 0 condition that was preventing mapping
- Map single unmapped peripheral to announcing peer regardless of nickname state
- Remove temp RSSI entries when transferring to real peer ID
- Improve logging to show successful RSSI transfers

* Improve getPeerRSSI to skip temp IDs and check known peers

- Skip temp IDs (non-16 character) when iterating connectedPeripherals
- Add fallback to check peerNicknames for known peers without peripherals
- Check both peerRSSI and peripheralRSSI for known peers
- Improve RSSI lookup coverage for edge cases

* Remove RSSI debug logging and fix build warnings

- Remove all RSSI-related debug logging added during troubleshooting
- Keep functional RSSI tracking code intact
- Fix unused variable warnings in didReadRSSI and updateAllPeripheralRSSI
- Clean up verbose logging while maintaining core functionality

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-07-24 22:22:19 +02:00
committed by GitHub
co-authored by jack
parent f4e954b837
commit 86726d7033
2 changed files with 194 additions and 13 deletions
+183 -10
View File
@@ -91,6 +91,7 @@ class BluetoothMeshService: NSObject {
private var activePeers: Set<String> = [] // Track all active peers private var activePeers: Set<String> = [] // Track all active peers
private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers
private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery
private var rssiRetryCount: [String: Int] = [:] // Track RSSI retry attempts per peripheral
// Per-peer encryption queues to prevent nonce desynchronization // Per-peer encryption queues to prevent nonce desynchronization
private var peerEncryptionQueues: [String: DispatchQueue] = [:] private var peerEncryptionQueues: [String: DispatchQueue] = [:]
@@ -916,6 +917,11 @@ class BluetoothMeshService: NSObject {
self?.checkPeerAvailability() self?.checkPeerAvailability()
} }
// Start RSSI update timer (every 10 seconds)
Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in
self?.updateAllPeripheralRSSI()
}
// Start write queue cleanup timer (every 30 seconds) // Start write queue cleanup timer (every 30 seconds)
writeQueueTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in writeQueueTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
self?.cleanExpiredWriteQueues() self?.cleanExpiredWriteQueues()
@@ -1548,9 +1554,53 @@ class BluetoothMeshService: NSObject {
} }
func getPeerRSSI() -> [String: NSNumber] { func getPeerRSSI() -> [String: NSNumber] {
// Return actual RSSI values only - no fake defaults var rssiValues = peerRSSI
// UI should handle missing values gracefully
return peerRSSI
// Log connectedPeripherals state
SecureLogger.log("connectedPeripherals has \(connectedPeripherals.count) entries:", category: SecureLogger.session, level: .debug)
for (peerID, peripheral) in connectedPeripherals {
SecureLogger.log(" connectedPeripherals[\(peerID)] = \(peripheral.identifier.uuidString)", category: SecureLogger.session, level: .debug)
}
// Also check peripheralRSSI for any connected peripherals
// This handles cases where RSSI is stored under temp IDs
for (peerID, peripheral) in connectedPeripherals {
// Skip temp IDs when iterating
if peerID.count != 16 {
continue
}
// If we don't have RSSI for this peer ID
if rssiValues[peerID] == nil {
// Check if we have RSSI stored by peripheral ID
let peripheralID = peripheral.identifier.uuidString
if let rssi = peripheralRSSI[peripheralID] {
rssiValues[peerID] = rssi
}
// Also check if RSSI is stored under the peer ID as a temp ID
else if let rssi = peripheralRSSI[peerID] {
rssiValues[peerID] = rssi
}
}
}
// Also check for any known peers that might have RSSI in peerRSSI but not in connectedPeripherals
for (peerID, _) in peerNicknames {
if rssiValues[peerID] == nil && peerID.count == 16 {
// Check if we have RSSI stored directly
if let rssi = peerRSSI[peerID] {
rssiValues[peerID] = rssi
} else if let rssi = peripheralRSSI[peerID] {
rssiValues[peerID] = rssi
}
}
}
return rssiValues
} }
// Emergency disconnect for panic situations // Emergency disconnect for panic situations
@@ -1582,6 +1632,7 @@ class BluetoothMeshService: NSObject {
activePeers.removeAll() activePeers.removeAll()
peerRSSI.removeAll() peerRSSI.removeAll()
peripheralRSSI.removeAll() peripheralRSSI.removeAll()
rssiRetryCount.removeAll()
announcedToPeers.removeAll() announcedToPeers.removeAll()
announcedPeers.removeAll() announcedPeers.removeAll()
// For emergency/panic, reset immediately // For emergency/panic, reset immediately
@@ -2451,6 +2502,8 @@ class BluetoothMeshService: NSObject {
if let nickname = String(data: packet.payload, encoding: .utf8) { if let nickname = String(data: packet.payload, encoding: .utf8) {
let senderID = packet.senderID.hexEncodedString() let senderID = packet.senderID.hexEncodedString()
SecureLogger.log("handleReceivedPacket: Received announce from \(senderID), peripheral is \(peripheral != nil ? "present" : "nil")", category: SecureLogger.session, level: .debug)
// Ignore if it's from ourselves (including previous peer IDs) // Ignore if it's from ourselves (including previous peer IDs)
if isPeerIDOurs(senderID) { if isPeerIDOurs(senderID) {
return return
@@ -2523,17 +2576,75 @@ class BluetoothMeshService: NSObject {
} }
// Update peripheral mapping if we have it // Update peripheral mapping if we have it
if let peripheral = peripheral { // If peripheral is nil (e.g., from relay), try to find it
var peripheralToUpdate = peripheral
if peripheralToUpdate == nil {
// Look for any peripheral that might be this peer
// First check if we already have a mapping for this peer ID
peripheralToUpdate = self.connectedPeripherals[senderID]
if peripheralToUpdate == nil {
SecureLogger.log("handleReceivedPacket: No peripheral passed and no existing mapping for \(senderID)", category: SecureLogger.session, level: .debug)
// Try to find an unidentified peripheral that might be this peer
// This handles case where announce is relayed and we need to update RSSI mapping
var unmappedPeripherals: [(String, CBPeripheral)] = []
for (tempID, peripheral) in self.connectedPeripherals {
// Check if this is a temp ID (UUID format, not a peer ID)
if tempID.count == 36 && tempID.contains("-") { // UUID length with dashes
unmappedPeripherals.append((tempID, peripheral))
SecureLogger.log("handleReceivedPacket: Found unmapped peripheral with temp ID \(tempID)", category: SecureLogger.session, level: .debug)
}
}
// If we have exactly one unmapped peripheral, it's likely this one
if unmappedPeripherals.count == 1 {
let (tempID, peripheral) = unmappedPeripherals[0]
SecureLogger.log("handleReceivedPacket: Single unmapped peripheral \(tempID), mapping to \(senderID)", category: SecureLogger.session, level: .info)
peripheralToUpdate = peripheral
// Remove temp mapping and add real mapping
self.connectedPeripherals.removeValue(forKey: tempID)
self.connectedPeripherals[senderID] = peripheral
// Transfer RSSI if available
if let rssi = self.peripheralRSSI[tempID] {
self.peripheralRSSI.removeValue(forKey: tempID)
self.peripheralRSSI[senderID] = rssi
self.peerRSSI[senderID] = rssi
}
// Also check the peripheral's UUID-based RSSI
let peripheralUUID = peripheral.identifier.uuidString
if peripheralUUID == tempID && peripheralRSSI[peripheralUUID] != nil {
// Already handled above
} else if let rssi = self.peripheralRSSI[peripheralUUID] {
self.peerRSSI[senderID] = rssi
}
} else if unmappedPeripherals.count > 1 {
SecureLogger.log("handleReceivedPacket: Multiple unmapped peripherals (\(unmappedPeripherals.count)), cannot determine which is \(senderID)", category: SecureLogger.session, level: .debug)
// TODO: Could use timing heuristics or other methods to match
}
}
}
if let peripheral = peripheralToUpdate {
SecureLogger.log("handleReceivedPacket: Updating peripheral mapping for announce from \(senderID)", category: SecureLogger.session, level: .debug)
SecureLogger.log("handleReceivedPacket: Current connectedPeripherals: \(self.connectedPeripherals.keys.joined(separator: ", "))", category: SecureLogger.session, level: .debug)
// Find and remove any temp ID mapping for this peripheral // Find and remove any temp ID mapping for this peripheral
var tempIDToRemove: String? = nil var tempIDToRemove: String? = nil
for (id, per) in self.connectedPeripherals { for (id, per) in self.connectedPeripherals {
if per == peripheral && id != senderID { if per == peripheral && id != senderID {
SecureLogger.log("handleReceivedPacket: Found temp ID \(id) for peripheral that announced as \(senderID)", category: SecureLogger.session, level: .debug)
tempIDToRemove = id tempIDToRemove = id
break break
} }
} }
if let tempID = tempIDToRemove { if let tempID = tempIDToRemove {
SecureLogger.log("handleReceivedPacket: Transferring from temp ID \(tempID) to real peer ID \(senderID)", category: SecureLogger.session, level: .debug)
// Remove temp mapping // Remove temp mapping
self.connectedPeripherals.removeValue(forKey: tempID) self.connectedPeripherals.removeValue(forKey: tempID)
// Add real peer ID mapping // Add real peer ID mapping
@@ -2541,6 +2652,17 @@ class BluetoothMeshService: NSObject {
// Update peripheral ID to peer ID mapping // Update peripheral ID to peer ID mapping
self.peerIDByPeripheralID[peripheral.identifier.uuidString] = senderID self.peerIDByPeripheralID[peripheral.identifier.uuidString] = senderID
// Transfer RSSI from temp ID to real peer ID
if let tempRSSI = self.peripheralRSSI[tempID] {
self.peerRSSI[senderID] = tempRSSI
self.peripheralRSSI.removeValue(forKey: tempID)
}
// Also check if RSSI is stored by peripheral UUID
let peripheralUUID = peripheral.identifier.uuidString
if let peripheralStoredRSSI = self.peripheralRSSI[peripheralUUID] {
self.peerRSSI[senderID] = peripheralStoredRSSI
}
// IMPORTANT: Remove old peer ID from activePeers to prevent duplicates // IMPORTANT: Remove old peer ID from activePeers to prevent duplicates
collectionsQueue.sync(flags: .barrier) { collectionsQueue.sync(flags: .barrier) {
if self.activePeers.contains(tempID) { if self.activePeers.contains(tempID) {
@@ -2549,6 +2671,17 @@ class BluetoothMeshService: NSObject {
} }
// Don't notify about disconnect - this is just cleanup of temporary ID // Don't notify about disconnect - this is just cleanup of temporary ID
} else {
SecureLogger.log("handleReceivedPacket: No temp ID found for peripheral, directly mapping \(senderID)", category: SecureLogger.session, level: .debug)
// No temp ID found, just add the mapping
self.connectedPeripherals[senderID] = peripheral
self.peerIDByPeripheralID[peripheral.identifier.uuidString] = senderID
// Check if RSSI is stored under peripheral UUID and transfer it
let peripheralUUID = peripheral.identifier.uuidString
if let peripheralStoredRSSI = self.peripheralRSSI[peripheralUUID] {
self.peerRSSI[senderID] = peripheralStoredRSSI
}
} }
} }
@@ -3387,9 +3520,11 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
if let name = peripheral.name, name.count == 16 { if let name = peripheral.name, name.count == 16 {
// Assume 16-character hex names are peer IDs // Assume 16-character hex names are peer IDs
let peerID = name let peerID = name
SecureLogger.log("Discovery: Found peer ID \(peerID) from peripheral name", category: SecureLogger.session, level: .debug)
// Don't process our own advertisements (including previous peer IDs) // Don't process our own advertisements (including previous peer IDs)
if isPeerIDOurs(peerID) { if isPeerIDOurs(peerID) {
SecureLogger.log("Discovery: Ignoring our own peer ID \(peerID)", category: SecureLogger.session, level: .debug)
return return
} }
@@ -3485,6 +3620,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
// Store peripheral by its system ID temporarily until we get the real peer ID // Store peripheral by its system ID temporarily until we get the real peer ID
connectedPeripherals[tempID] = peripheral connectedPeripherals[tempID] = peripheral
SecureLogger.log("didConnect: Stored peripheral with temp ID \(tempID)", category: SecureLogger.session, level: .debug)
// Update connection state to connected (but not authenticated yet) // Update connection state to connected (but not authenticated yet)
// We don't know the real peer ID yet, so we can't update the state // We don't know the real peer ID yet, so we can't update the state
@@ -3505,6 +3641,9 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
let peripheralID = peripheral.identifier.uuidString let peripheralID = peripheral.identifier.uuidString
// Clean up RSSI retry count
rssiRetryCount.removeValue(forKey: peripheralID)
// Check if this was an intentional disconnect // Check if this was an intentional disconnect
if intentionalDisconnects.contains(peripheralID) { if intentionalDisconnects.contains(peripheralID) {
intentionalDisconnects.remove(peripheralID) intentionalDisconnects.remove(peripheralID)
@@ -3806,23 +3945,45 @@ extension BluetoothMeshService: CBPeripheralDelegate {
} }
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
guard error == nil else { return } let peripheralID = peripheral.identifier.uuidString
if error != nil {
return
}
// Validate RSSI value - 127 means no RSSI available // Validate RSSI value - 127 means no RSSI available
let rssiValue = RSSI.intValue let rssiValue = RSSI.intValue
// Only store valid RSSI values // Only store valid RSSI values
if rssiValue == 127 || rssiValue < -100 || rssiValue > 0 { if rssiValue == 127 || rssiValue < -100 || rssiValue > 0 {
SecureLogger.log("Invalid RSSI value \(rssiValue) from peripheral, will retry", category: SecureLogger.session, level: .debug) // Track retry count
let retryCount = rssiRetryCount[peripheralID] ?? 0
rssiRetryCount[peripheralID] = retryCount + 1
// Retry sooner if we got an invalid value
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak peripheral] in // Retry up to 5 times with exponential backoff
guard let peripheral = peripheral, peripheral.state == .connected else { return } if retryCount < 5 {
peripheral.readRSSI() let delay = min(0.2 * pow(2.0, Double(retryCount)), 2.0) // 0.2s, 0.4s, 0.8s, 1.6s, 2.0s max
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak peripheral] in
guard let peripheral = peripheral, peripheral.state == .connected else {
self?.rssiRetryCount.removeValue(forKey: peripheralID)
return
}
peripheral.readRSSI()
}
} else {
// Give up after 5 retries, reset counter
rssiRetryCount.removeValue(forKey: peripheralID)
} }
return return
} }
// Valid RSSI received, reset retry count
rssiRetryCount.removeValue(forKey: peripheralID)
// Store RSSI by peripheral ID for fallback lookup
peripheralRSSI[peripheralID] = RSSI
// Find the peer ID for this peripheral // Find the peer ID for this peripheral
if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key { if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
// Handle both temp IDs and real peer IDs // Handle both temp IDs and real peer IDs
@@ -3850,6 +4011,7 @@ extension BluetoothMeshService: CBPeripheralDelegate {
guard let peripheral = peripheral, peripheral.state == .connected else { return } guard let peripheral = peripheral, peripheral.state == .connected else { return }
peripheral.readRSSI() peripheral.readRSSI()
} }
} else {
} }
} }
} }
@@ -5532,6 +5694,17 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
} }
} }
// Update RSSI for all connected peripherals
private func updateAllPeripheralRSSI() {
// Read RSSI for all connected peripherals
for (_, peripheral) in connectedPeripherals {
if peripheral.state == .connected {
peripheral.readRSSI()
}
}
}
// Check peer availability based on last heard time // Check peer availability based on last heard time
private func checkPeerAvailability() { private func checkPeerAvailability() {
let now = Date() let now = Date()
+11 -3
View File
@@ -600,19 +600,27 @@ struct ContentView: View {
// Show all connected peers // Show all connected peers
let peersToShow: [String] = viewModel.connectedPeers let peersToShow: [String] = viewModel.connectedPeers
let _ = print("ContentView: Showing \(peersToShow.count) peers: \(peersToShow.joined(separator: ", "))")
// Pre-compute peer data outside ForEach to reduce overhead // Pre-compute peer data outside ForEach to reduce overhead
let peerData = peersToShow.map { peerID in let peerData = peersToShow.map { peerID in
PeerDisplayData( let rssiValue = peerRSSI[peerID]?.intValue
if rssiValue == nil {
print("ContentView: No RSSI for peer \(peerID) in dictionary with \(peerRSSI.count) entries")
print("ContentView: peerRSSI keys: \(peerRSSI.keys.joined(separator: ", "))")
} else {
print("ContentView: RSSI for peer \(peerID) is \(rssiValue!)")
}
return PeerDisplayData(
id: peerID, id: peerID,
displayName: peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))"), displayName: peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))"),
rssi: peerRSSI[peerID]?.intValue, rssi: rssiValue,
isFavorite: viewModel.isFavorite(peerID: peerID), isFavorite: viewModel.isFavorite(peerID: peerID),
isMe: peerID == myPeerID, isMe: peerID == myPeerID,
hasUnreadMessages: viewModel.unreadPrivateMessages.contains(peerID), hasUnreadMessages: viewModel.unreadPrivateMessages.contains(peerID),
encryptionStatus: viewModel.getEncryptionStatus(for: peerID) encryptionStatus: viewModel.getEncryptionStatus(for: peerID)
) )
}.sorted { peer1, peer2 in }.sorted { (peer1: PeerDisplayData, peer2: PeerDisplayData) in
// Sort: favorites first, then alphabetically by nickname // Sort: favorites first, then alphabetically by nickname
if peer1.isFavorite != peer2.isFavorite { if peer1.isFavorite != peer2.isFavorite {
return peer1.isFavorite return peer1.isFavorite