Fix iOS background BLE, RSSI tracking, and peer nickname display

- Add UIBackgroundModes for bluetooth-central/peripheral to restore background messaging
- Implement RSSI tracking with proper peer ID mapping and UI updates
- Fix announce packet delivery for immediate nickname display
- Convert to binary protocol for 62-69% size reduction
- Fix peer count to exclude self from display
- Add platform-specific UI improvements (font sizes, dropdown styling)
This commit is contained in:
jack
2025-07-03 00:51:59 +02:00
parent 20ebaa109c
commit 4b98aa7918
9 changed files with 482 additions and 291 deletions
+224 -229
View File
@@ -20,14 +20,15 @@ class BluetoothMeshService: NSObject {
private var subscribedCentrals: [CBCentral] = []
private var peerNicknames: [String: String] = [:]
private var activePeers: Set<String> = [] // 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<String>()
private let maxTTL: UInt8 = 5
private var hasAnnounced = false
private var announcementTimer: Timer?
private var announcedToPeers = Set<String>() // Track which peers we've announced to
private var announcedPeers = Set<String>() // 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)