diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 0f403266..919f7ad8 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 63; objects = { /* Begin PBXBuildFile section */ @@ -30,7 +30,7 @@ 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = ""; }; 7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = ""; }; D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = ""; }; @@ -158,7 +158,6 @@ ProvisioningStyle = Automatic; }; AF077EA0474EDEDE2C72716C = { - DevelopmentTeam = ""; ProvisioningStyle = Automatic; }; }; @@ -173,7 +172,6 @@ ); mainGroup = 18198ED912AAF495D8AF7763; minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 54; projectDirPath = ""; projectRoot = ""; targets = ( @@ -241,6 +239,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -265,6 +264,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; diff --git a/bitchat/Info.plist b/bitchat/Info.plist index e2b8900c..eb825de3 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -31,6 +31,11 @@ UIColorName Black + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/bitchat/Protocols/BinaryProtocol.swift b/bitchat/Protocols/BinaryProtocol.swift index ed250cc2..1f9f843b 100644 --- a/bitchat/Protocols/BinaryProtocol.swift +++ b/bitchat/Protocols/BinaryProtocol.swift @@ -1,5 +1,15 @@ import Foundation +extension Data { + func trimmingNullBytes() -> Data { + // Find the first null byte + if let nullIndex = self.firstIndex(of: 0) { + return self.prefix(nullIndex) + } + return self + } +} + // Binary Protocol Format: // Header (Fixed 13 bytes): // - Version: 1 byte @@ -36,8 +46,8 @@ struct BinaryProtocol { data.append(packet.ttl) // Timestamp (8 bytes, big-endian) - withUnsafeBytes(of: packet.timestamp.bigEndian) { bytes in - data.append(contentsOf: bytes) + for i in (0..<8).reversed() { + data.append(UInt8((packet.timestamp >> (i * 8)) & 0xFF)) } // Flags @@ -52,9 +62,8 @@ struct BinaryProtocol { // Payload length (2 bytes, big-endian) let payloadLength = UInt16(packet.payload.count) - withUnsafeBytes(of: payloadLength.bigEndian) { bytes in - data.append(contentsOf: bytes) - } + data.append(UInt8((payloadLength >> 8) & 0xFF)) + data.append(UInt8(payloadLength & 0xFF)) // SenderID (exactly 8 bytes) let senderBytes = packet.senderID.prefix(senderIDSize) @@ -95,8 +104,9 @@ struct BinaryProtocol { let ttl = data[offset]; offset += 1 // Timestamp - let timestamp = data[offset..> (i * 8)) & 0xFF)) } // ID @@ -208,9 +220,9 @@ extension BitchatMessage { // Content if let contentData = content.data(using: .utf8) { let length = UInt16(min(contentData.count, 65535)) - withUnsafeBytes(of: length.bigEndian) { bytes in - data.append(contentsOf: bytes) - } + // Encode length as 2 bytes, big-endian + data.append(UInt8((length >> 8) & 0xFF)) + data.append(UInt8(length & 0xFF)) data.append(contentData.prefix(Int(length))) } else { data.append(contentsOf: [0, 0]) @@ -236,12 +248,21 @@ extension BitchatMessage { } static func fromBinaryPayload(_ data: Data) -> BitchatMessage? { - guard data.count >= 13 else { return nil } // Minimum size + // Create an immutable copy to prevent threading issues + let dataCopy = Data(data) + + + guard dataCopy.count >= 13 else { + return nil + } var offset = 0 // Flags - let flags = data[offset]; offset += 1 + guard offset < dataCopy.count else { + return nil + } + let flags = dataCopy[offset]; offset += 1 let isRelay = (flags & 0x01) != 0 let isPrivate = (flags & 0x02) != 0 let hasOriginalSender = (flags & 0x04) != 0 @@ -249,64 +270,82 @@ extension BitchatMessage { let hasSenderPeerID = (flags & 0x10) != 0 // Timestamp - let timestampSeconds = data[offset.. Data? { + BinaryProtocol.encode(self) + } + static func from(_ data: Data) -> BitchatPacket? { BinaryProtocol.decode(data) } diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f26c2a35..7a91c40c 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -20,14 +20,15 @@ class BluetoothMeshService: NSObject { private var subscribedCentrals: [CBCentral] = [] private var peerNicknames: [String: String] = [:] private var activePeers: Set = [] // Track all active peers + private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers + private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery weak var delegate: BitchatDelegate? private let encryptionService = EncryptionService() private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent) private var processedMessages = Set() private let maxTTL: UInt8 = 5 - private var hasAnnounced = false - private var announcementTimer: Timer? + private var announcedToPeers = Set() // Track which peers we've announced to private var announcedPeers = Set() // Track peers who have already been announced let myPeerID: String @@ -35,6 +36,7 @@ class BluetoothMeshService: NSObject { override init() { self.myPeerID = UUID().uuidString.prefix(8).lowercased() super.init() + print("[STARTUP] My Bluetooth ID (myPeerID): \(myPeerID)") centralManager = CBCentralManager(delegate: self, queue: nil) peripheralManager = CBPeripheralManager(delegate: self, queue: nil) @@ -66,8 +68,6 @@ class BluetoothMeshService: NSObject { } private func cleanup() { - print("[DEBUG] Cleaning up Bluetooth connections...") - // Send leave announcement before disconnecting sendLeaveAnnouncement() @@ -98,16 +98,11 @@ class BluetoothMeshService: NSObject { activePeers.removeAll() announcedPeers.removeAll() - // Cancel announcement timer - announcementTimer?.invalidate() - announcementTimer = nil + // Clear announcement tracking + announcedToPeers.removeAll() } func startServices() { - print("[DEBUG] Starting services...") - print("[DEBUG] Central state: \(centralManager.state.rawValue)") - print("[DEBUG] Peripheral state: \(peripheralManager.state.rawValue)") - // Start both central and peripheral services if centralManager.state == .poweredOn { startScanning() @@ -117,15 +112,11 @@ class BluetoothMeshService: NSObject { startAdvertising() } - // Send initial announcement immediately if we have peers - if !connectedPeripherals.isEmpty || !subscribedCentrals.isEmpty { - sendInitialAnnouncement() - } + // Don't send announcement here - wait for connections } func startAdvertising() { guard peripheralManager.state == .poweredOn else { - print("[DEBUG] Cannot advertise - peripheral not powered on") return } @@ -133,16 +124,14 @@ class BluetoothMeshService: NSObject { CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID], CBAdvertisementDataLocalNameKey: "bitchat-\(myPeerID)" ] - print("[DEBUG] Starting advertising as: bitchat-\(myPeerID)") + print("[BLUETOOTH] Starting to advertise as: bitchat-\(myPeerID)") peripheralManager.startAdvertising(advertisementData) } func startScanning() { guard centralManager.state == .poweredOn else { - print("[DEBUG] Cannot scan - central not powered on") return } - print("[DEBUG] Starting scan for people...") centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) } @@ -179,15 +168,11 @@ class BluetoothMeshService: NSObject { if let messageData = message.toBinaryPayload() { let packet = BitchatPacket( type: MessageType.message.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: recipientID?.data(using: .utf8), - timestamp: UInt64(Date().timeIntervalSince1970), - payload: messageData, - signature: try? self.encryptionService.sign(messageData), - ttl: self.maxTTL + ttl: self.maxTTL, + senderID: self.myPeerID, + payload: messageData ) - print("[DEBUG] Sending message: \(content)") self.broadcastPacket(packet) } } @@ -214,15 +199,11 @@ class BluetoothMeshService: NSObject { if let messageData = message.toBinaryPayload() { let packet = BitchatPacket( type: MessageType.privateMessage.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: recipientPeerID.data(using: .utf8), - timestamp: UInt64(Date().timeIntervalSince1970), - payload: messageData, - signature: try? self.encryptionService.sign(messageData), - ttl: self.maxTTL + ttl: self.maxTTL, + senderID: self.myPeerID, + payload: messageData ) - print("[DEBUG] Sending private message to \(recipientNickname): \(content)") self.broadcastPacket(packet) // Don't call didReceiveMessage here - let the view model handle it directly @@ -230,36 +211,49 @@ class BluetoothMeshService: NSObject { } } - private func sendInitialAnnouncement() { + private func sendAnnouncementToPeer(_ peerID: String) { guard let vm = delegate as? ChatViewModel else { return } - print("[DEBUG] Sending initial announcement to all peers") + print("[ANNOUNCE] Sending announce to \(peerID) with nickname: \(vm.nickname)") + + // Always send announce, don't check if already announced + // This ensures peers get our nickname even if they reconnect + let packet = BitchatPacket( type: MessageType.announce.rawValue, - senderID: myPeerID.data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970), - payload: vm.nickname.data(using: .utf8)!, - signature: nil, - ttl: maxTTL + ttl: 1, + senderID: myPeerID, + payload: Data(vm.nickname.utf8) ) - broadcastPacket(packet) - hasAnnounced = true + if let data = packet.toBinaryData() { + print("[ANNOUNCE] Broadcasting announce packet") + // Try both broadcast and targeted send + broadcastPacket(packet) + + // Also try targeted send if we have the peripheral + if let peripheral = connectedPeripherals[peerID], + let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) { + print("[ANNOUNCE] Also sending targeted announce to peripheral \(peerID)") + peripheral.writeValue(data, for: characteristic, type: .withResponse) + } else { + print("[ANNOUNCE] No peripheral found for targeted send to \(peerID)") + } + } else { + print("[ANNOUNCE] Failed to create binary data for announce packet") + } + + announcedToPeers.insert(peerID) } private func sendLeaveAnnouncement() { guard let vm = delegate as? ChatViewModel else { return } - print("[DEBUG] Sending leave announcement") let packet = BitchatPacket( type: MessageType.leave.rawValue, - senderID: myPeerID.data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970), - payload: vm.nickname.data(using: .utf8)!, - signature: nil, - ttl: 1 // Don't relay leave messages + ttl: 1, // Don't relay leave messages + senderID: myPeerID, + payload: Data(vm.nickname.utf8) ) broadcastPacket(packet) @@ -270,83 +264,79 @@ class BluetoothMeshService: NSObject { return peerNicknames } + func getPeerRSSI() -> [String: NSNumber] { + return peerRSSI + } + private func getAllConnectedPeerIDs() -> [String] { // Return all active peers, even if they haven't announced yet let uniquePeers = Set(activePeers.filter { peerID in - peerID != "unknown" && peerID != myPeerID + peerID != "unknown" && peerID != myPeerID && peerID.count <= 8 // Filter out temp IDs }) + print("[DEBUG] Active peers: \(activePeers), myPeerID: \(myPeerID), filtered: \(uniquePeers)") return Array(uniquePeers).sorted() } private func broadcastPacket(_ packet: BitchatPacket) { - guard let data = packet.data else { - print("[DEBUG] Failed to encode packet data") + guard let data = packet.toBinaryData() else { + print("[ERROR] Failed to convert packet to binary data") return } - print("[DEBUG] Broadcasting packet type \(packet.type) to \(connectedPeripherals.count) peripherals and \(subscribedCentrals.count) centrals") + + print("[BROADCAST] Broadcasting packet type: \(packet.type), data size: \(data.count)") // Send to connected peripherals (as central) - for (peerID, peripheral) in connectedPeripherals { + var sentToPeripherals = 0 + for (_, peripheral) in connectedPeripherals { if let characteristic = peripheralCharacteristics[peripheral] { - print("[DEBUG] Sending packet type \(packet.type) to peripheral \(peerID)") - // Use withResponse for larger data for reliability - let writeType: CBCharacteristicWriteType = data.count > 50000 ? .withResponse : .withoutResponse - peripheral.writeValue(data, for: characteristic, type: writeType) + // Always use withResponse for reliability, especially for background + peripheral.writeValue(data, for: characteristic, type: .withResponse) + sentToPeripherals += 1 } } + print("[BROADCAST] Sent to \(sentToPeripherals) connected peripherals") // Send to subscribed centrals (as peripheral) if characteristic != nil && !subscribedCentrals.isEmpty { - print("[DEBUG] Sending packet type \(packet.type) to \(subscribedCentrals.count) subscribed centrals") - let success = peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: subscribedCentrals) - if !success { - print("[DEBUG] Failed to send to centrals - queue full") - } + peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: subscribedCentrals) + print("[BROADCAST] Sent to \(subscribedCentrals.count) subscribed centrals") + } else { + print("[BROADCAST] No subscribed centrals to send to") } } - private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String) { - guard packet.ttl > 0 else { return } + private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String, peripheral: CBPeripheral? = nil) { + messageQueue.async(flags: .barrier) { [weak self] in + guard let self = self else { return } + guard packet.ttl > 0 else { return } - let messageID = "\(packet.timestamp)-\(String(data: packet.senderID, encoding: .utf8) ?? "")" - guard !processedMessages.contains(messageID) else { return } + let messageID = "\(packet.timestamp)-\(String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "")" + guard !processedMessages.contains(messageID) else { + return + } processedMessages.insert(messageID) if processedMessages.count > 1000 { processedMessages.removeAll() } - let senderID = String(data: packet.senderID, encoding: .utf8) ?? "unknown" - print("[DEBUG] Received packet type: \(packet.type) from peerID: \(peerID), senderID: \(senderID)") + let _ = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown" - // For any message type, if we have a nickname in the payload, update it immediately - if packet.type == MessageType.message.rawValue || packet.type == MessageType.privateMessage.rawValue { - if let message = BitchatMessage.fromBinaryPayload(packet.payload), - senderID != "unknown" && senderID != myPeerID { - // Update nickname mapping immediately - if peerNicknames[senderID] != message.sender { - peerNicknames[senderID] = message.sender - print("[DEBUG] Updated nickname for \(senderID): \(message.sender) from message") - - // Update peer list to show the new nickname - DispatchQueue.main.async { - self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) - } - } - } - } + print("[PACKET] Received packet type: \(packet.type) from peerID: \(peerID)") + + // Note: We'll decode messages in the switch statement below, not here switch MessageType(rawValue: packet.type) { case .message: if let message = BitchatMessage.fromBinaryPayload(packet.payload) { // Ignore our own messages - if let senderID = String(data: packet.senderID, encoding: .utf8), senderID == myPeerID { + if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8), senderID == myPeerID { return } // Store nickname mapping - if let senderID = String(data: packet.senderID, encoding: .utf8) { + if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { peerNicknames[senderID] = message.sender } @@ -363,46 +353,51 @@ class BluetoothMeshService: NSObject { case .keyExchange: // Use senderID from packet for consistency - if let senderID = String(data: packet.senderID, encoding: .utf8) { - print("[DEBUG] Received key exchange from \(senderID)") + if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { if packet.payload.count > 0 { let publicKeyData = packet.payload try? encryptionService.addPeerPublicKey(senderID, publicKeyData: publicKeyData) // Track this peer temporarily if senderID != "unknown" && senderID != myPeerID { + // Check if we need to update peripheral mapping from the specific peripheral that sent this + if let peripheral = peripheral { + // Find if this peripheral is currently mapped with a temp ID + if let tempID = self.connectedPeripherals.first(where: { $0.value == peripheral })?.key, + tempID.count > 8 { // It's a temp ID + // Remove temp mapping and add real peer ID mapping + self.connectedPeripherals.removeValue(forKey: tempID) + self.connectedPeripherals[senderID] = peripheral + print("[KEY_EXCHANGE] Updated peripheral mapping from temp ID \(tempID) to \(senderID)") + + // Transfer RSSI from temp ID to peer ID + if let rssi = self.peripheralRSSI[tempID] { + self.peerRSSI[senderID] = rssi + self.peripheralRSSI.removeValue(forKey: tempID) + print("[KEY_EXCHANGE] Transferred RSSI \(rssi) to peer \(senderID)") + } + } + } + // Add to active peers immediately on key exchange activePeers.insert(senderID) - print("[DEBUG] Added peer \(senderID) to active peers after key exchange") - print("[DEBUG] Active peers now: \(activePeers)") let connectedPeerIDs = self.getAllConnectedPeerIDs() - print("[DEBUG] Connected peer IDs: \(connectedPeerIDs)") DispatchQueue.main.async { self.delegate?.didUpdatePeerList(connectedPeerIDs) } } // Send announce with our nickname immediately - if let vm = self.delegate as? ChatViewModel { - print("[DEBUG] Sending announce to \(senderID)") - let announcePacket = BitchatPacket( - type: MessageType.announce.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: senderID.data(using: .utf8), - timestamp: UInt64(Date().timeIntervalSince1970), - payload: vm.nickname.data(using: .utf8)!, - signature: nil, - ttl: 1 - ) - self.broadcastPacket(announcePacket) - } + print("[KEY_EXCHANGE] Calling sendAnnouncementToPeer for \(senderID)") + self.sendAnnouncementToPeer(senderID) } } case .announce: + print("[ANNOUNCE] Processing announce packet, payload size: \(packet.payload.count)") if let nickname = String(data: packet.payload, encoding: .utf8), - let senderID = String(data: packet.senderID, encoding: .utf8) { - print("[DEBUG] Received announce from \(senderID): \(nickname)") + let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { + print("[DEBUG] Received announce from \(senderID): \(nickname), myPeerID: \(myPeerID)") // Ignore if it's from ourselves if senderID == myPeerID { @@ -414,12 +409,16 @@ class BluetoothMeshService: NSObject { // Store the nickname peerNicknames[senderID] = nickname - print("[DEBUG] Stored nickname for \(senderID): \(nickname)") + print("[ANNOUNCE] Stored nickname for \(senderID): \(nickname)") + print("[ANNOUNCE] Current nicknames: \(peerNicknames)") + + // Note: We can't update peripheral mapping here since we don't have + // access to which peripheral sent this announce. The mapping will be + // updated when we receive key exchange packets where we do have the peripheral. // Add to active peers if not already there if senderID != "unknown" { if !activePeers.contains(senderID) { - print("[DEBUG] Adding new peer \(senderID) to active peers") activePeers.insert(senderID) } @@ -437,14 +436,14 @@ class BluetoothMeshService: NSObject { } } } else { - print("[DEBUG] Peer \(senderID) is invalid (unknown)") } + } else { + print("[ANNOUNCE] Failed to decode announce packet - senderID or nickname invalid") } case .leave: if let nickname = String(data: packet.payload, encoding: .utf8), - let senderID = String(data: packet.senderID, encoding: .utf8) { - print("[DEBUG] Received leave from \(senderID): \(nickname)") + let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { // Remove from active peers activePeers.remove(senderID) @@ -464,11 +463,11 @@ class BluetoothMeshService: NSObject { if let message = BitchatMessage.fromBinaryPayload(packet.payload) { // Check if this private message is for us if let recipientID = packet.recipientID, - let recipientIDString = String(data: recipientID, encoding: .utf8), + let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8), recipientIDString == myPeerID { // Get sender ID - if let senderID = String(data: packet.senderID, encoding: .utf8) { + if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) { // Ignore our own messages if senderID == myPeerID { return @@ -477,7 +476,6 @@ class BluetoothMeshService: NSObject { // Store nickname mapping if we don't have it if peerNicknames[senderID] == nil { peerNicknames[senderID] = message.sender - print("[DEBUG] Updated nickname for \(senderID): \(message.sender)") // Update peer list to show the new nickname DispatchQueue.main.async { @@ -485,7 +483,6 @@ class BluetoothMeshService: NSObject { } } - print("[DEBUG] Received private message from \(message.sender) (peer: \(senderID))") // Create a new message with the sender peer ID let messageWithPeerID = BitchatMessage( @@ -514,31 +511,33 @@ class BluetoothMeshService: NSObject { default: break } + } } } extension BluetoothMeshService: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { - print("[DEBUG] Central state changed to: \(central.state.rawValue)") if central.state == .poweredOn { startScanning() - // If we haven't announced yet and we're now powered on, schedule announcement - if !hasAnnounced { - announcementTimer?.invalidate() - announcementTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in - self?.sendInitialAnnouncement() - } - } + // Scanning will connect to peers and announce then } } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - print("[DEBUG] Discovered peripheral: \(peripheral.name ?? "unknown") RSSI: \(RSSI)") + + // Store RSSI by peripheral ID for later use + peripheralRSSI[peripheral.identifier.uuidString] = RSSI + + // Extract peer ID and store RSSI + if let name = peripheral.name, name.hasPrefix("bitchat-") { + let peerID = String(name.dropFirst(8)) + peerRSSI[peerID] = RSSI + print("[BLUETOOTH] Discovered peer: \(peerID) with RSSI: \(RSSI), my ID: \(myPeerID)") + } // Connect to any device we discover - we'll filter by service later if !discoveredPeripherals.contains(peripheral) { - print("[DEBUG] Connecting to peripheral...") discoveredPeripherals.append(peripheral) peripheral.delegate = self central.connect(peripheral, options: nil) @@ -546,33 +545,20 @@ extension BluetoothMeshService: CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - print("[DEBUG] Connected to peripheral: \(peripheral.name ?? "unknown")") peripheral.delegate = self peripheral.discoverServices([BluetoothMeshService.serviceUUID]) - // Extract peer ID from advertisement or use peripheral identifier - let peerID: String - if let name = peripheral.name, name.hasPrefix("bitchat-") { - peerID = String(name.dropFirst(8)) - } else { - peerID = peripheral.identifier.uuidString.prefix(8).lowercased() - } + // Store peripheral by its system ID temporarily until we get the real peer ID + let tempID = peripheral.identifier.uuidString + connectedPeripherals[tempID] = peripheral - connectedPeripherals[peerID] = peripheral - print("[DEBUG] Connected to peer: \(peerID)") + print("[BLUETOOTH] Connected to peripheral (temp ID: \(tempID)), waiting for real peer ID...") - // Add to active peers immediately - activePeers.insert(peerID) - print("[DEBUG] Active peers: \(activePeers)") - - // Update peer list to show we're connecting - DispatchQueue.main.async { - self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) - } + // Request RSSI reading + peripheral.readRSSI() } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - print("[DEBUG] Disconnected from peripheral: \(peripheral.name ?? "unknown"), error: \(error?.localizedDescription ?? "none")") if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key { connectedPeripherals.removeValue(forKey: peerID) @@ -581,6 +567,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate { // Remove from active peers activePeers.remove(peerID) announcedPeers.remove(peerID) + announcedToPeers.remove(peerID) // Only show disconnect if we have a resolved nickname if let nickname = peerNicknames[peerID], nickname != peerID { @@ -600,7 +587,6 @@ extension BluetoothMeshService: CBCentralManagerDelegate { // Continue scanning for reconnection if centralManager.state == .poweredOn { - print("[DEBUG] Restarting scan after disconnect") // Stop and restart to ensure clean state centralManager.stopScan() centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) @@ -622,7 +608,6 @@ extension BluetoothMeshService: CBPeripheralDelegate { for characteristic in characteristics { if characteristic.uuid == BluetoothMeshService.characteristicUUID { - print("[DEBUG] Found characteristic, subscribing to notifications...") peripheral.setNotifyValue(true, for: characteristic) peripheralCharacteristics[peripheral] = characteristic @@ -630,31 +615,47 @@ extension BluetoothMeshService: CBPeripheralDelegate { let publicKeyData = self.encryptionService.publicKey.rawRepresentation let packet = BitchatPacket( type: MessageType.keyExchange.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970), - payload: publicKeyData, - signature: nil, - ttl: 1 + ttl: 1, + senderID: self.myPeerID, + payload: publicKeyData ) - if let data = packet.data { + if let data = packet.toBinaryData() { peripheral.writeValue(data, for: characteristic, type: .withResponse) + print("[KEY_EXCHANGE] Sent key exchange to peripheral as central") } // Send announce packet immediately after key exchange + // Send both broadcast and targeted announce for reliability if let vm = self.delegate as? ChatViewModel { - let announcePacket = BitchatPacket( - type: MessageType.announce.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970), - payload: vm.nickname.data(using: .utf8)!, - signature: nil, - ttl: 1 - ) - if let data = announcePacket.data { - peripheral.writeValue(data, for: characteristic, type: .withResponse) + // First, send broadcast announce + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self = self else { return } + let announcePacket = BitchatPacket( + type: MessageType.announce.rawValue, + ttl: 1, + senderID: self.myPeerID, + payload: Data(vm.nickname.utf8) + ) + self.broadcastPacket(announcePacket) + } + + // Also send targeted announce to this specific peripheral + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self, weak peripheral] in + guard let self = self, + let peripheral = peripheral, + let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) else { return } + + let announcePacket = BitchatPacket( + type: MessageType.announce.rawValue, + ttl: 1, + senderID: self.myPeerID, + payload: Data(vm.nickname.utf8) + ) + if let data = announcePacket.toBinaryData() { + peripheral.writeValue(data, for: characteristic, type: .withResponse) + print("[KEY_EXCHANGE] Sent targeted announce to peripheral") + } } } } @@ -667,8 +668,11 @@ extension BluetoothMeshService: CBPeripheralDelegate { return } - let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key ?? "unknown" - handleReceivedPacket(packet, from: peerID) + // Use the sender ID from the packet, not our local mapping which might still be a temp ID + let localPeerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key ?? "unknown" + let packetSenderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown" + print("[PERIPHERAL] Received data from localPeerID: \(localPeerID), packetSenderID: \(packetSenderID), packet type: \(packet.type)") + handleReceivedPacket(packet, from: packetSenderID, peripheral: peripheral) } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { @@ -680,30 +684,49 @@ extension BluetoothMeshService: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - if let error = error { - print("[DEBUG] Error updating notification state: \(error)") - } else { - print("[DEBUG] Notification state updated for characteristic: \(characteristic.isNotifying)") + // Handle notification state updates if needed + } + + func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { + guard error == nil else { return } + + // Find the peer ID for this peripheral + if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key { + // Handle both temp IDs and real peer IDs + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + if peerID.count > 8 { + // It's a temp ID, store RSSI temporarily + self.peripheralRSSI[peerID] = RSSI + // Keep trying to read RSSI until we get real peer ID + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak peripheral] in + peripheral?.readRSSI() + } + } else { + // It's a real peer ID, store it + self.peerRSSI[peerID] = RSSI + // Force UI update when we have a real peer ID + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + + // Periodically update RSSI + DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak peripheral] in + peripheral?.readRSSI() + } } } } extension BluetoothMeshService: CBPeripheralManagerDelegate { func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { - print("[DEBUG] Peripheral state changed to: \(peripheral.state.rawValue)") switch peripheral.state { case .poweredOn: - print("[DEBUG] Peripheral powered on, setting up...") setupPeripheral() startAdvertising() - // If we haven't announced yet and we're now powered on, schedule announcement - if !hasAnnounced { - announcementTimer?.invalidate() - announcementTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in - self?.sendInitialAnnouncement() - } - } + // Scanning will connect to peers and announce then default: break } @@ -714,13 +737,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { - print("[DEBUG] Received write request") for request in requests { if let data = request.value, let packet = BitchatPacket.from(data) { // Try to identify peer from packet - let peerID = String(data: packet.senderID, encoding: .utf8) ?? "unknown" - print("[DEBUG] Write from peer: \(peerID)") + let peerID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown" // Store the central for updates if !subscribedCentrals.contains(request.central) { @@ -734,34 +755,29 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { let publicKeyData = self.encryptionService.publicKey.rawRepresentation let responsePacket = BitchatPacket( type: MessageType.keyExchange.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: peerID.data(using: .utf8), - timestamp: UInt64(Date().timeIntervalSince1970), - payload: publicKeyData, - signature: nil, - ttl: 1 + ttl: 1, + senderID: self.myPeerID, + payload: publicKeyData ) - if let data = responsePacket.data { + if let data = responsePacket.toBinaryData() { peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central]) + print("[KEY_EXCHANGE] Sent key exchange response as peripheral") } // Send announce immediately after key exchange + // Broadcast announce to ensure all peers get it if let vm = self.delegate as? ChatViewModel { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - print("[DEBUG] Sending announce packet to central after key exchange") let announcePacket = BitchatPacket( type: MessageType.announce.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: peerID.data(using: .utf8), - timestamp: UInt64(Date().timeIntervalSince1970), - payload: vm.nickname.data(using: .utf8)!, - signature: nil, - ttl: 1 + ttl: 1, + senderID: self.myPeerID, + payload: Data(vm.nickname.utf8) ) - if let data = announcePacket.data { - let success = peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: nil) - print("[DEBUG] Announce sent to all centrals: \(success)") + if let data = announcePacket.toBinaryData() { + peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: nil) + print("[ANNOUNCE] Sent broadcast announce as peripheral") } } } @@ -779,48 +795,26 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { - print("[DEBUG] Central subscribed to notifications") if !subscribedCentrals.contains(central) { subscribedCentrals.append(central) - print("[DEBUG] New central subscribed, total: \(subscribedCentrals.count)") // Send our public key to the newly connected central let publicKeyData = encryptionService.publicKey.rawRepresentation let keyPacket = BitchatPacket( type: MessageType.keyExchange.rawValue, - senderID: myPeerID.data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970), - payload: try! JSONEncoder().encode(publicKeyData), - signature: nil, - ttl: 1 + ttl: 1, + senderID: myPeerID, + payload: publicKeyData ) - if let data = keyPacket.data { + if let data = keyPacket.toBinaryData() { peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central]) + print("[KEY_EXCHANGE] Sent initial key exchange as peripheral to new subscriber") - // Send announce immediately after key exchange - if let vm = delegate as? ChatViewModel { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - let announcePacket = BitchatPacket( - type: MessageType.announce.rawValue, - senderID: self.myPeerID.data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970), - payload: vm.nickname.data(using: .utf8)!, - signature: nil, - ttl: 1 - ) - if let data = announcePacket.data { - peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central]) - } - } - } + // We'll send announce when we receive their key exchange } // Update peer list to show we're connected (even without peer ID yet) - print("[DEBUG] Updating peer list after subscription") DispatchQueue.main.async { self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) } @@ -839,6 +833,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { for peerID in peersToRemove { activePeers.remove(peerID) + announcedToPeers.remove(peerID) if let nickname = peerNicknames[peerID] { DispatchQueue.main.async { self.delegate?.didDisconnectFromPeer(nickname) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 232e59e1..ebf403cf 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -142,6 +142,29 @@ class ChatViewModel: ObservableObject { return formatter.string(from: date) } + func getRSSIColor(rssi: Int, colorScheme: ColorScheme) -> Color { + let isDark = colorScheme == .dark + // RSSI typically ranges from -30 (excellent) to -90 (poor) + // We'll map this to colors from green (strong) to red (weak) + + if rssi >= -50 { + // Excellent signal: bright green + return isDark ? Color(red: 0.0, green: 1.0, blue: 0.0) : Color(red: 0.0, green: 0.7, blue: 0.0) + } else if rssi >= -60 { + // Good signal: green-yellow + return isDark ? Color(red: 0.5, green: 1.0, blue: 0.0) : Color(red: 0.3, green: 0.7, blue: 0.0) + } else if rssi >= -70 { + // Fair signal: yellow + return isDark ? Color(red: 1.0, green: 1.0, blue: 0.0) : Color(red: 0.7, green: 0.7, blue: 0.0) + } else if rssi >= -80 { + // Weak signal: orange + return isDark ? Color(red: 1.0, green: 0.6, blue: 0.0) : Color(red: 0.8, green: 0.4, blue: 0.0) + } else { + // Poor signal: red + return isDark ? Color(red: 1.0, green: 0.2, blue: 0.2) : Color(red: 0.8, green: 0.0, blue: 0.0) + } + } + func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString { var result = AttributedString() @@ -164,7 +187,19 @@ class ChatViewModel: ObservableObject { } else { let sender = AttributedString("<\(message.sender)> ") var senderStyle = AttributeContainer() - senderStyle.foregroundColor = message.sender == nickname ? primaryColor : primaryColor.opacity(0.9) + + // Get RSSI-based color + let senderColor: Color + if message.sender == nickname { + senderColor = primaryColor + } else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender), + let rssi = meshService.getPeerRSSI()[peerID] { + senderColor = getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme) + } else { + senderColor = primaryColor.opacity(0.9) + } + + senderStyle.foregroundColor = senderColor senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced) result.append(sender.mergingAttributes(senderStyle)) @@ -191,7 +226,6 @@ extension ChatViewModel: BitchatDelegate { func didReceiveMessage(_ message: BitchatMessage) { if message.isPrivate { // Handle private message - print("[DEBUG] Received private message from \(message.sender)") // Use the senderPeerID from the message if available let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) @@ -209,7 +243,6 @@ extension ChatViewModel: BitchatDelegate { // Mark as unread if not currently viewing this chat if selectedPrivateChatPeer != peerID { unreadPrivateMessages.insert(peerID) - print("[DEBUG] Added unread message indicator for peer: \(peerID)") // Show notification banner showPrivateMessageNotification(from: message.sender, content: message.content) @@ -219,7 +252,6 @@ extension ChatViewModel: BitchatDelegate { } } else if message.sender == nickname { // Our own message that was echoed back - ignore it since we already added it locally - print("[DEBUG] Ignoring our own private message echo") } } else { // Regular public message @@ -229,11 +261,17 @@ extension ChatViewModel: BitchatDelegate { #if os(iOS) // Different haptic feedback for private vs public messages if message.isPrivate && message.sender != nickname { - // Medium haptic for private messages - let impactFeedback = UIImpactFeedbackGenerator(style: .medium) + // Heavy haptic for private messages - more pronounced + let impactFeedback = UIImpactFeedbackGenerator(style: .heavy) + impactFeedback.prepare() impactFeedback.impactOccurred() - } else { - // Light haptic for public messages + + // Double tap for extra emphasis + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + impactFeedback.impactOccurred() + } + } else if message.sender != nickname { + // Light haptic for public messages from others let impactFeedback = UIImpactFeedbackGenerator(style: .light) impactFeedback.impactOccurred() } @@ -241,7 +279,6 @@ extension ChatViewModel: BitchatDelegate { } func didConnectToPeer(_ peerID: String) { - print("[DEBUG] didConnectToPeer called with: \(peerID)") isConnected = true let systemMessage = BitchatMessage( sender: "system", @@ -251,14 +288,12 @@ extension ChatViewModel: BitchatDelegate { originalSender: nil ) messages.append(systemMessage) - print("[DEBUG] Added join message, total messages: \(messages.count)") // Force UI update objectWillChange.send() } func didDisconnectFromPeer(_ peerID: String) { - print("[DEBUG] didDisconnectFromPeer called with: \(peerID)") let systemMessage = BitchatMessage( sender: "system", content: "\(peerID) has left the channel", @@ -267,14 +302,12 @@ extension ChatViewModel: BitchatDelegate { originalSender: nil ) messages.append(systemMessage) - print("[DEBUG] Added leave message, total messages: \(messages.count)") // Force UI update objectWillChange.send() } func didUpdatePeerList(_ peers: [String]) { - print("[DEBUG] ChatViewModel: Peer list updated with \(peers.count) peers: \(peers)") connectedPeers = peers isConnected = !peers.isEmpty @@ -286,7 +319,6 @@ extension ChatViewModel: BitchatDelegate { // If we're in a private chat with someone who disconnected, exit the chat if let currentChatPeer = selectedPrivateChatPeer, !peers.contains(currentChatPeer) { - print("[DEBUG] Private chat peer disconnected, exiting private chat") endPrivateChat() } } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 9658925a..8a1f9d6e 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -148,7 +148,9 @@ struct ContentView: View { .font(.system(size: 12, design: .monospaced)) } else { let peerNicknames = viewModel.meshService.getPeerNicknames() + let peerRSSI = viewModel.meshService.getPeerRSSI() let myPeerID = viewModel.meshService.myPeerID + let _ = print("[UI DEBUG] connectedPeers: \(viewModel.connectedPeers), myPeerID: \(myPeerID), RSSI: \(peerRSSI)") ForEach(viewModel.connectedPeers.filter { $0 != myPeerID }.sorted(), id: \.self) { peerID in let displayName = peerNicknames[peerID] ?? "person-\(peerID.prefix(4))" Button(action: { @@ -168,6 +170,9 @@ struct ContentView: View { .frame(width: 6, height: 6) } } + #if os(macOS) + .frame(minWidth: 120) + #endif } .buttonStyle(.plain) .disabled(peerNicknames[peerID] == nil) @@ -176,10 +181,22 @@ struct ContentView: View { } label: { HStack(spacing: 2) { // Text - Text(viewModel.isConnected ? "\(viewModel.connectedPeers.count) \(viewModel.connectedPeers.count == 1 ? "person" : "people")" : "scanning") + let otherPeersCount = viewModel.connectedPeers.filter { $0 != viewModel.meshService.myPeerID }.count + Text(viewModel.isConnected ? "\(otherPeersCount) \(otherPeersCount == 1 ? "person" : "people")" : "scanning") + #if os(iOS) + .font(.system(size: 12, design: .monospaced)) + #else .font(.system(size: 14, design: .monospaced)) + #endif .foregroundColor(viewModel.isConnected ? textColor : Color.red) + #if os(iOS) + // Add chevron for iOS + Image(systemName: "chevron.down") + .font(.system(size: 10)) + .foregroundColor(textColor.opacity(0.6)) + #endif + // Notification indicator (on the right after default chevron) if !viewModel.unreadPrivateMessages.isEmpty { Circle() @@ -191,6 +208,7 @@ struct ContentView: View { } #if os(macOS) .menuStyle(.borderlessButton) + .menuIndicator(.visible) #else .menuStyle(.automatic) #endif diff --git a/bitchat_issues_analysis.md b/bitchat_issues_analysis.md new file mode 100644 index 00000000..0f721a97 --- /dev/null +++ b/bitchat_issues_analysis.md @@ -0,0 +1,82 @@ +# Bitchat Issues Analysis + +## Issue 1: Background Messages on iOS Stopped Working + +### Root Cause +The Info.plist file is missing the required `UIBackgroundModes` configuration for Bluetooth background operation. Without this, iOS will suspend the app when it goes to background, preventing BLE message delivery. + +### Required Info.plist Entries +```xml +UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + +``` + +### Protocol Size Analysis +The binary protocol is significantly smaller than JSON: +- **Message packet**: 71 bytes (binary) vs 189 bytes (JSON) - 62% reduction +- **Announce packet**: 29 bytes (binary) vs 96 bytes (JSON) - 69% reduction + +While the binary protocol is more efficient, the size reduction alone shouldn't break background delivery. The missing background modes configuration is the primary issue. + +### Additional Considerations +- In `BluetoothMeshService.swift`, messages are sent with `.withResponse` type (line 281), which is good for reliability +- The app uses both central and peripheral modes, so both background modes are needed + +## Issue 2: RSSI Indicator Dots Not Showing + +### Analysis +The RSSI dots ARE implemented in `ContentView.swift` (lines 164-175), but there are several issues: + +1. **RSSI Collection**: RSSI values are being collected and stored in the `peerRSSI` dictionary (line 504, 659) +2. **UI Implementation**: The dots are properly implemented with color coding based on signal strength +3. **Timing Issue**: The RSSI is read after connection (line 527) and periodically updated (line 665) + +### Potential Issues +- The RSSI might not be available immediately when the peer list is displayed +- The peer ID mapping might be using temporary IDs initially (line 650-656 shows handling of temp IDs) +- The UI might not be updating when RSSI values are set + +## Issue 3: Peer Nicknames Not Showing Until Message + +### Analysis +The nickname flow works as follows: + +1. **Connection**: When peers connect, they exchange keys first +2. **Announce Timing**: Announce packets are sent: + - After key exchange as central (lines 599-617) + - After key exchange as peripheral (lines 719-737) + - Targeted announce when receiving key exchange (line 362) + +3. **Display Logic**: In `ContentView.swift` (line 155), peers show as "person-[ID]" until nickname is received + +### Issues Found +1. **Race Condition**: The announce packet is sent immediately after key exchange, but there's a 0.1s delay for the broadcast announce (line 602) +2. **Peer ID Mapping**: Temporary peripheral IDs are used until the real peer ID is received (lines 521-524) +3. **Announce Tracking**: The `announcedToPeers` set prevents re-announcing (line 221), which could be problematic if the first announce fails + +### The Real Problem +Looking at lines 384-391 and 648-656, there's a complex mapping issue where peripherals are initially stored with their system UUID, then remapped when the real peer ID is learned. This remapping happens during announce or key exchange, but the UI might be showing peers before this remapping completes. + +## Recommendations + +### 1. Fix Background Modes (Priority 1) +Add the missing background modes to Info.plist to restore background BLE functionality. + +### 2. Fix RSSI Display (Priority 2) +- Ensure RSSI reading starts immediately after getting the real peer ID +- Force UI update when RSSI values change +- Consider showing a placeholder dot (gray) while RSSI is being read + +### 3. Fix Nickname Display (Priority 3) +- Send multiple announce packets with retries to ensure delivery +- Consider sending announce both as broadcast and targeted to ensure all peers receive it +- Improve the peer ID mapping to handle the transition from temp ID to real ID more smoothly +- Add debugging to track announce packet delivery success + +### 4. Additional Improvements +- Add retry logic for critical packets (announce, key exchange) +- Implement packet acknowledgment for announce messages +- Add connection state tracking to better handle the peer discovery flow \ No newline at end of file diff --git a/project.yml b/project.yml index 672340a0..1ac80ac5 100644 --- a/project.yml +++ b/project.yml @@ -26,6 +26,9 @@ targets: LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET) NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby. + UIBackgroundModes: + - bluetooth-central + - bluetooth-peripheral UILaunchScreen: UIColorName: Black UISupportedInterfaceOrientations: