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
+4 -4
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 63;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -30,7 +30,7 @@
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; }; 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; }; D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; };
@@ -158,7 +158,6 @@
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
AF077EA0474EDEDE2C72716C = { AF077EA0474EDEDE2C72716C = {
DevelopmentTeam = "";
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
}; };
@@ -173,7 +172,6 @@
); );
mainGroup = 18198ED912AAF495D8AF7763; mainGroup = 18198ED912AAF495D8AF7763;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 54;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
@@ -241,6 +239,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -265,6 +264,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+5
View File
@@ -31,6 +31,11 @@
<key>UIColorName</key> <key>UIColorName</key>
<string>Black</string> <string>Black</string>
</dict> </dict>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
+83 -43
View File
@@ -1,5 +1,15 @@
import Foundation 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: // Binary Protocol Format:
// Header (Fixed 13 bytes): // Header (Fixed 13 bytes):
// - Version: 1 byte // - Version: 1 byte
@@ -36,8 +46,8 @@ struct BinaryProtocol {
data.append(packet.ttl) data.append(packet.ttl)
// Timestamp (8 bytes, big-endian) // Timestamp (8 bytes, big-endian)
withUnsafeBytes(of: packet.timestamp.bigEndian) { bytes in for i in (0..<8).reversed() {
data.append(contentsOf: bytes) data.append(UInt8((packet.timestamp >> (i * 8)) & 0xFF))
} }
// Flags // Flags
@@ -52,9 +62,8 @@ struct BinaryProtocol {
// Payload length (2 bytes, big-endian) // Payload length (2 bytes, big-endian)
let payloadLength = UInt16(packet.payload.count) let payloadLength = UInt16(packet.payload.count)
withUnsafeBytes(of: payloadLength.bigEndian) { bytes in data.append(UInt8((payloadLength >> 8) & 0xFF))
data.append(contentsOf: bytes) data.append(UInt8(payloadLength & 0xFF))
}
// SenderID (exactly 8 bytes) // SenderID (exactly 8 bytes)
let senderBytes = packet.senderID.prefix(senderIDSize) let senderBytes = packet.senderID.prefix(senderIDSize)
@@ -95,8 +104,9 @@ struct BinaryProtocol {
let ttl = data[offset]; offset += 1 let ttl = data[offset]; offset += 1
// Timestamp // Timestamp
let timestamp = data[offset..<offset+8].withUnsafeBytes { bytes in let timestampData = data[offset..<offset+8]
bytes.load(as: UInt64.self).bigEndian let timestamp = timestampData.reduce(0) { result, byte in
(result << 8) | UInt64(byte)
} }
offset += 8 offset += 8
@@ -106,8 +116,9 @@ struct BinaryProtocol {
let hasSignature = (flags & Flags.hasSignature) != 0 let hasSignature = (flags & Flags.hasSignature) != 0
// Payload length // Payload length
let payloadLength = data[offset..<offset+2].withUnsafeBytes { bytes in let payloadLengthData = data[offset..<offset+2]
bytes.load(as: UInt16.self).bigEndian let payloadLength = payloadLengthData.reduce(0) { result, byte in
(result << 8) | UInt16(byte)
} }
offset += 2 offset += 2
@@ -185,8 +196,9 @@ extension BitchatMessage {
// Timestamp // Timestamp
let timestampSeconds = UInt64(timestamp.timeIntervalSince1970) let timestampSeconds = UInt64(timestamp.timeIntervalSince1970)
withUnsafeBytes(of: timestampSeconds.bigEndian) { bytes in // Encode as 8 bytes, big-endian
data.append(contentsOf: bytes) for i in (0..<8).reversed() {
data.append(UInt8((timestampSeconds >> (i * 8)) & 0xFF))
} }
// ID // ID
@@ -208,9 +220,9 @@ extension BitchatMessage {
// Content // Content
if let contentData = content.data(using: .utf8) { if let contentData = content.data(using: .utf8) {
let length = UInt16(min(contentData.count, 65535)) let length = UInt16(min(contentData.count, 65535))
withUnsafeBytes(of: length.bigEndian) { bytes in // Encode length as 2 bytes, big-endian
data.append(contentsOf: bytes) data.append(UInt8((length >> 8) & 0xFF))
} data.append(UInt8(length & 0xFF))
data.append(contentData.prefix(Int(length))) data.append(contentData.prefix(Int(length)))
} else { } else {
data.append(contentsOf: [0, 0]) data.append(contentsOf: [0, 0])
@@ -236,12 +248,21 @@ extension BitchatMessage {
} }
static func fromBinaryPayload(_ data: Data) -> 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 var offset = 0
// Flags // 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 isRelay = (flags & 0x01) != 0
let isPrivate = (flags & 0x02) != 0 let isPrivate = (flags & 0x02) != 0
let hasOriginalSender = (flags & 0x04) != 0 let hasOriginalSender = (flags & 0x04) != 0
@@ -249,64 +270,82 @@ extension BitchatMessage {
let hasSenderPeerID = (flags & 0x10) != 0 let hasSenderPeerID = (flags & 0x10) != 0
// Timestamp // Timestamp
let timestampSeconds = data[offset..<offset+8].withUnsafeBytes { bytes in guard offset + 8 <= dataCopy.count else {
bytes.load(as: UInt64.self).bigEndian return nil
}
let timestampData = dataCopy[offset..<offset+8]
let timestampSeconds = timestampData.reduce(0) { result, byte in
(result << 8) | UInt64(byte)
} }
offset += 8 offset += 8
let timestamp = Date(timeIntervalSince1970: TimeInterval(timestampSeconds)) let timestamp = Date(timeIntervalSince1970: TimeInterval(timestampSeconds))
// ID // ID
let idLength = Int(data[offset]); offset += 1 guard offset < dataCopy.count else {
guard offset + idLength <= data.count else { return nil } return nil
_ = String(data: data[offset..<offset+idLength], encoding: .utf8) ?? UUID().uuidString }
let idLength = Int(dataCopy[offset]); offset += 1
guard offset + idLength <= dataCopy.count else {
return nil
}
let _ = String(data: dataCopy[offset..<offset+idLength], encoding: .utf8) ?? UUID().uuidString
offset += idLength offset += idLength
// Sender // Sender
guard offset < data.count else { return nil } guard offset < dataCopy.count else {
let senderLength = Int(data[offset]); offset += 1 return nil
guard offset + senderLength <= data.count else { return nil } }
let sender = String(data: data[offset..<offset+senderLength], encoding: .utf8) ?? "unknown" let senderLength = Int(dataCopy[offset]); offset += 1
guard offset + senderLength <= dataCopy.count else {
return nil
}
let sender = String(data: dataCopy[offset..<offset+senderLength], encoding: .utf8) ?? "unknown"
offset += senderLength offset += senderLength
// Content // Content
guard offset + 2 <= data.count else { return nil } guard offset + 2 <= dataCopy.count else {
let contentLength = data[offset..<offset+2].withUnsafeBytes { bytes in return nil
Int(bytes.load(as: UInt16.self).bigEndian)
} }
let contentLengthData = dataCopy[offset..<offset+2]
let contentLength = Int(contentLengthData.reduce(0) { result, byte in
(result << 8) | UInt16(byte)
})
offset += 2 offset += 2
guard offset + contentLength <= data.count else { return nil } guard offset + contentLength <= dataCopy.count else {
let content = String(data: data[offset..<offset+contentLength], encoding: .utf8) ?? "" return nil
}
let content = String(data: dataCopy[offset..<offset+contentLength], encoding: .utf8) ?? ""
offset += contentLength offset += contentLength
// Optional fields // Optional fields
var originalSender: String? var originalSender: String?
if hasOriginalSender && offset < data.count { if hasOriginalSender && offset < dataCopy.count {
let length = Int(data[offset]); offset += 1 let length = Int(dataCopy[offset]); offset += 1
if offset + length <= data.count { if offset + length <= dataCopy.count {
originalSender = String(data: data[offset..<offset+length], encoding: .utf8) originalSender = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length offset += length
} }
} }
var recipientNickname: String? var recipientNickname: String?
if hasRecipientNickname && offset < data.count { if hasRecipientNickname && offset < dataCopy.count {
let length = Int(data[offset]); offset += 1 let length = Int(dataCopy[offset]); offset += 1
if offset + length <= data.count { if offset + length <= dataCopy.count {
recipientNickname = String(data: data[offset..<offset+length], encoding: .utf8) recipientNickname = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length offset += length
} }
} }
var senderPeerID: String? var senderPeerID: String?
if hasSenderPeerID && offset < data.count { if hasSenderPeerID && offset < dataCopy.count {
let length = Int(data[offset]); offset += 1 let length = Int(dataCopy[offset]); offset += 1
if offset + length <= data.count { if offset + length <= dataCopy.count {
senderPeerID = String(data: data[offset..<offset+length], encoding: .utf8) senderPeerID = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length offset += length
} }
} }
return BitchatMessage( let message = BitchatMessage(
sender: sender, sender: sender,
content: content, content: content,
timestamp: timestamp, timestamp: timestamp,
@@ -316,5 +355,6 @@ extension BitchatMessage {
recipientNickname: recipientNickname, recipientNickname: recipientNickname,
senderPeerID: senderPeerID senderPeerID: senderPeerID
) )
return message
} }
} }
+16
View File
@@ -33,10 +33,26 @@ struct BitchatPacket: Codable {
self.ttl = ttl self.ttl = ttl
} }
// Convenience initializer for new binary format
init(type: UInt8, ttl: UInt8, senderID: String, payload: Data) {
self.version = 1
self.type = type
self.senderID = senderID.data(using: .utf8)!
self.recipientID = nil
self.timestamp = UInt64(Date().timeIntervalSince1970)
self.payload = payload
self.signature = nil
self.ttl = ttl
}
var data: Data? { var data: Data? {
BinaryProtocol.encode(self) BinaryProtocol.encode(self)
} }
func toBinaryData() -> Data? {
BinaryProtocol.encode(self)
}
static func from(_ data: Data) -> BitchatPacket? { static func from(_ data: Data) -> BitchatPacket? {
BinaryProtocol.decode(data) BinaryProtocol.decode(data)
} }
+216 -221
View File
@@ -20,14 +20,15 @@ class BluetoothMeshService: NSObject {
private var subscribedCentrals: [CBCentral] = [] private var subscribedCentrals: [CBCentral] = []
private var peerNicknames: [String: String] = [:] private var peerNicknames: [String: String] = [:]
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 peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery
weak var delegate: BitchatDelegate? weak var delegate: BitchatDelegate?
private let encryptionService = EncryptionService() private let encryptionService = EncryptionService()
private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent) private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent)
private var processedMessages = Set<String>() private var processedMessages = Set<String>()
private let maxTTL: UInt8 = 5 private let maxTTL: UInt8 = 5
private var hasAnnounced = false private var announcedToPeers = Set<String>() // Track which peers we've announced to
private var announcementTimer: Timer?
private var announcedPeers = Set<String>() // Track peers who have already been announced private var announcedPeers = Set<String>() // Track peers who have already been announced
let myPeerID: String let myPeerID: String
@@ -35,6 +36,7 @@ class BluetoothMeshService: NSObject {
override init() { override init() {
self.myPeerID = UUID().uuidString.prefix(8).lowercased() self.myPeerID = UUID().uuidString.prefix(8).lowercased()
super.init() super.init()
print("[STARTUP] My Bluetooth ID (myPeerID): \(myPeerID)")
centralManager = CBCentralManager(delegate: self, queue: nil) centralManager = CBCentralManager(delegate: self, queue: nil)
peripheralManager = CBPeripheralManager(delegate: self, queue: nil) peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
@@ -66,8 +68,6 @@ class BluetoothMeshService: NSObject {
} }
private func cleanup() { private func cleanup() {
print("[DEBUG] Cleaning up Bluetooth connections...")
// Send leave announcement before disconnecting // Send leave announcement before disconnecting
sendLeaveAnnouncement() sendLeaveAnnouncement()
@@ -98,16 +98,11 @@ class BluetoothMeshService: NSObject {
activePeers.removeAll() activePeers.removeAll()
announcedPeers.removeAll() announcedPeers.removeAll()
// Cancel announcement timer // Clear announcement tracking
announcementTimer?.invalidate() announcedToPeers.removeAll()
announcementTimer = nil
} }
func startServices() { 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 // Start both central and peripheral services
if centralManager.state == .poweredOn { if centralManager.state == .poweredOn {
startScanning() startScanning()
@@ -117,15 +112,11 @@ class BluetoothMeshService: NSObject {
startAdvertising() startAdvertising()
} }
// Send initial announcement immediately if we have peers // Don't send announcement here - wait for connections
if !connectedPeripherals.isEmpty || !subscribedCentrals.isEmpty {
sendInitialAnnouncement()
}
} }
func startAdvertising() { func startAdvertising() {
guard peripheralManager.state == .poweredOn else { guard peripheralManager.state == .poweredOn else {
print("[DEBUG] Cannot advertise - peripheral not powered on")
return return
} }
@@ -133,16 +124,14 @@ class BluetoothMeshService: NSObject {
CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID], CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID],
CBAdvertisementDataLocalNameKey: "bitchat-\(myPeerID)" CBAdvertisementDataLocalNameKey: "bitchat-\(myPeerID)"
] ]
print("[DEBUG] Starting advertising as: bitchat-\(myPeerID)") print("[BLUETOOTH] Starting to advertise as: bitchat-\(myPeerID)")
peripheralManager.startAdvertising(advertisementData) peripheralManager.startAdvertising(advertisementData)
} }
func startScanning() { func startScanning() {
guard centralManager.state == .poweredOn else { guard centralManager.state == .poweredOn else {
print("[DEBUG] Cannot scan - central not powered on")
return return
} }
print("[DEBUG] Starting scan for people...")
centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
} }
@@ -179,15 +168,11 @@ class BluetoothMeshService: NSObject {
if let messageData = message.toBinaryPayload() { if let messageData = message.toBinaryPayload() {
let packet = BitchatPacket( let packet = BitchatPacket(
type: MessageType.message.rawValue, type: MessageType.message.rawValue,
senderID: self.myPeerID.data(using: .utf8)!, ttl: self.maxTTL,
recipientID: recipientID?.data(using: .utf8), senderID: self.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: messageData
payload: messageData,
signature: try? self.encryptionService.sign(messageData),
ttl: self.maxTTL
) )
print("[DEBUG] Sending message: \(content)")
self.broadcastPacket(packet) self.broadcastPacket(packet)
} }
} }
@@ -214,15 +199,11 @@ class BluetoothMeshService: NSObject {
if let messageData = message.toBinaryPayload() { if let messageData = message.toBinaryPayload() {
let packet = BitchatPacket( let packet = BitchatPacket(
type: MessageType.privateMessage.rawValue, type: MessageType.privateMessage.rawValue,
senderID: self.myPeerID.data(using: .utf8)!, ttl: self.maxTTL,
recipientID: recipientPeerID.data(using: .utf8), senderID: self.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: messageData
payload: messageData,
signature: try? self.encryptionService.sign(messageData),
ttl: self.maxTTL
) )
print("[DEBUG] Sending private message to \(recipientNickname): \(content)")
self.broadcastPacket(packet) self.broadcastPacket(packet)
// Don't call didReceiveMessage here - let the view model handle it directly // 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 } 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( let packet = BitchatPacket(
type: MessageType.announce.rawValue, type: MessageType.announce.rawValue,
senderID: myPeerID.data(using: .utf8)!, ttl: 1,
recipientID: nil, senderID: myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: Data(vm.nickname.utf8)
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: maxTTL
) )
if let data = packet.toBinaryData() {
print("[ANNOUNCE] Broadcasting announce packet")
// Try both broadcast and targeted send
broadcastPacket(packet) broadcastPacket(packet)
hasAnnounced = true
// 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() { private func sendLeaveAnnouncement() {
guard let vm = delegate as? ChatViewModel else { return } guard let vm = delegate as? ChatViewModel else { return }
print("[DEBUG] Sending leave announcement")
let packet = BitchatPacket( let packet = BitchatPacket(
type: MessageType.leave.rawValue, type: MessageType.leave.rawValue,
senderID: myPeerID.data(using: .utf8)!, ttl: 1, // Don't relay leave messages
recipientID: nil, senderID: myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: Data(vm.nickname.utf8)
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1 // Don't relay leave messages
) )
broadcastPacket(packet) broadcastPacket(packet)
@@ -270,83 +264,79 @@ class BluetoothMeshService: NSObject {
return peerNicknames return peerNicknames
} }
func getPeerRSSI() -> [String: NSNumber] {
return peerRSSI
}
private func getAllConnectedPeerIDs() -> [String] { private func getAllConnectedPeerIDs() -> [String] {
// Return all active peers, even if they haven't announced yet // Return all active peers, even if they haven't announced yet
let uniquePeers = Set(activePeers.filter { peerID in 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() return Array(uniquePeers).sorted()
} }
private func broadcastPacket(_ packet: BitchatPacket) { private func broadcastPacket(_ packet: BitchatPacket) {
guard let data = packet.data else { guard let data = packet.toBinaryData() else {
print("[DEBUG] Failed to encode packet data") print("[ERROR] Failed to convert packet to binary data")
return 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) // Send to connected peripherals (as central)
for (peerID, peripheral) in connectedPeripherals { var sentToPeripherals = 0
for (_, peripheral) in connectedPeripherals {
if let characteristic = peripheralCharacteristics[peripheral] { if let characteristic = peripheralCharacteristics[peripheral] {
print("[DEBUG] Sending packet type \(packet.type) to peripheral \(peerID)") // Always use withResponse for reliability, especially for background
// Use withResponse for larger data for reliability peripheral.writeValue(data, for: characteristic, type: .withResponse)
let writeType: CBCharacteristicWriteType = data.count > 50000 ? .withResponse : .withoutResponse sentToPeripherals += 1
peripheral.writeValue(data, for: characteristic, type: writeType)
} }
} }
print("[BROADCAST] Sent to \(sentToPeripherals) connected peripherals")
// Send to subscribed centrals (as peripheral) // Send to subscribed centrals (as peripheral)
if characteristic != nil && !subscribedCentrals.isEmpty { if characteristic != nil && !subscribedCentrals.isEmpty {
print("[DEBUG] Sending packet type \(packet.type) to \(subscribedCentrals.count) subscribed centrals") peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: subscribedCentrals)
let success = peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: subscribedCentrals) print("[BROADCAST] Sent to \(subscribedCentrals.count) subscribed centrals")
if !success { } else {
print("[DEBUG] Failed to send to centrals - queue full") print("[BROADCAST] No subscribed centrals to send to")
}
} }
} }
private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String) { 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 } guard packet.ttl > 0 else { return }
let messageID = "\(packet.timestamp)-\(String(data: packet.senderID, encoding: .utf8) ?? "")" let messageID = "\(packet.timestamp)-\(String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "")"
guard !processedMessages.contains(messageID) else { return } guard !processedMessages.contains(messageID) else {
return
}
processedMessages.insert(messageID) processedMessages.insert(messageID)
if processedMessages.count > 1000 { if processedMessages.count > 1000 {
processedMessages.removeAll() processedMessages.removeAll()
} }
let senderID = String(data: packet.senderID, encoding: .utf8) ?? "unknown" let _ = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown"
print("[DEBUG] Received packet type: \(packet.type) from peerID: \(peerID), senderID: \(senderID)")
// For any message type, if we have a nickname in the payload, update it immediately print("[PACKET] Received packet type: \(packet.type) from peerID: \(peerID)")
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 // Note: We'll decode messages in the switch statement below, not here
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
}
}
switch MessageType(rawValue: packet.type) { switch MessageType(rawValue: packet.type) {
case .message: case .message:
if let message = BitchatMessage.fromBinaryPayload(packet.payload) { if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
// Ignore our own messages // 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 return
} }
// Store nickname mapping // 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 peerNicknames[senderID] = message.sender
} }
@@ -363,46 +353,51 @@ class BluetoothMeshService: NSObject {
case .keyExchange: case .keyExchange:
// Use senderID from packet for consistency // Use senderID from packet for consistency
if let senderID = String(data: packet.senderID, encoding: .utf8) { if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
print("[DEBUG] Received key exchange from \(senderID)")
if packet.payload.count > 0 { if packet.payload.count > 0 {
let publicKeyData = packet.payload let publicKeyData = packet.payload
try? encryptionService.addPeerPublicKey(senderID, publicKeyData: publicKeyData) try? encryptionService.addPeerPublicKey(senderID, publicKeyData: publicKeyData)
// Track this peer temporarily // Track this peer temporarily
if senderID != "unknown" && senderID != myPeerID { 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 // Add to active peers immediately on key exchange
activePeers.insert(senderID) activePeers.insert(senderID)
print("[DEBUG] Added peer \(senderID) to active peers after key exchange")
print("[DEBUG] Active peers now: \(activePeers)")
let connectedPeerIDs = self.getAllConnectedPeerIDs() let connectedPeerIDs = self.getAllConnectedPeerIDs()
print("[DEBUG] Connected peer IDs: \(connectedPeerIDs)")
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(connectedPeerIDs) self.delegate?.didUpdatePeerList(connectedPeerIDs)
} }
} }
// Send announce with our nickname immediately // Send announce with our nickname immediately
if let vm = self.delegate as? ChatViewModel { print("[KEY_EXCHANGE] Calling sendAnnouncementToPeer for \(senderID)")
print("[DEBUG] Sending announce to \(senderID)") self.sendAnnouncementToPeer(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)
}
} }
} }
case .announce: case .announce:
print("[ANNOUNCE] Processing announce packet, payload size: \(packet.payload.count)")
if let nickname = String(data: packet.payload, encoding: .utf8), if let nickname = String(data: packet.payload, encoding: .utf8),
let senderID = String(data: packet.senderID, encoding: .utf8) { let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
print("[DEBUG] Received announce from \(senderID): \(nickname)") print("[DEBUG] Received announce from \(senderID): \(nickname), myPeerID: \(myPeerID)")
// Ignore if it's from ourselves // Ignore if it's from ourselves
if senderID == myPeerID { if senderID == myPeerID {
@@ -414,12 +409,16 @@ class BluetoothMeshService: NSObject {
// Store the nickname // Store the nickname
peerNicknames[senderID] = 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 // Add to active peers if not already there
if senderID != "unknown" { if senderID != "unknown" {
if !activePeers.contains(senderID) { if !activePeers.contains(senderID) {
print("[DEBUG] Adding new peer \(senderID) to active peers")
activePeers.insert(senderID) activePeers.insert(senderID)
} }
@@ -437,14 +436,14 @@ class BluetoothMeshService: NSObject {
} }
} }
} else { } else {
print("[DEBUG] Peer \(senderID) is invalid (unknown)")
} }
} else {
print("[ANNOUNCE] Failed to decode announce packet - senderID or nickname invalid")
} }
case .leave: case .leave:
if let nickname = String(data: packet.payload, encoding: .utf8), if let nickname = String(data: packet.payload, encoding: .utf8),
let senderID = String(data: packet.senderID, encoding: .utf8) { let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
print("[DEBUG] Received leave from \(senderID): \(nickname)")
// Remove from active peers // Remove from active peers
activePeers.remove(senderID) activePeers.remove(senderID)
@@ -464,11 +463,11 @@ class BluetoothMeshService: NSObject {
if let message = BitchatMessage.fromBinaryPayload(packet.payload) { if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
// Check if this private message is for us // Check if this private message is for us
if let recipientID = packet.recipientID, if let recipientID = packet.recipientID,
let recipientIDString = String(data: recipientID, encoding: .utf8), let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8),
recipientIDString == myPeerID { recipientIDString == myPeerID {
// Get sender ID // 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 // Ignore our own messages
if senderID == myPeerID { if senderID == myPeerID {
return return
@@ -477,7 +476,6 @@ class BluetoothMeshService: NSObject {
// Store nickname mapping if we don't have it // Store nickname mapping if we don't have it
if peerNicknames[senderID] == nil { if peerNicknames[senderID] == nil {
peerNicknames[senderID] = message.sender peerNicknames[senderID] = message.sender
print("[DEBUG] Updated nickname for \(senderID): \(message.sender)")
// Update peer list to show the new nickname // Update peer list to show the new nickname
DispatchQueue.main.async { 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 // Create a new message with the sender peer ID
let messageWithPeerID = BitchatMessage( let messageWithPeerID = BitchatMessage(
@@ -515,30 +512,32 @@ class BluetoothMeshService: NSObject {
break break
} }
} }
}
} }
extension BluetoothMeshService: CBCentralManagerDelegate { extension BluetoothMeshService: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) { func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("[DEBUG] Central state changed to: \(central.state.rawValue)")
if central.state == .poweredOn { if central.state == .poweredOn {
startScanning() startScanning()
// If we haven't announced yet and we're now powered on, schedule announcement // Scanning will connect to peers and announce then
if !hasAnnounced {
announcementTimer?.invalidate()
announcementTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in
self?.sendInitialAnnouncement()
}
}
} }
} }
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 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 // Connect to any device we discover - we'll filter by service later
if !discoveredPeripherals.contains(peripheral) { if !discoveredPeripherals.contains(peripheral) {
print("[DEBUG] Connecting to peripheral...")
discoveredPeripherals.append(peripheral) discoveredPeripherals.append(peripheral)
peripheral.delegate = self peripheral.delegate = self
central.connect(peripheral, options: nil) central.connect(peripheral, options: nil)
@@ -546,33 +545,20 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
} }
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("[DEBUG] Connected to peripheral: \(peripheral.name ?? "unknown")")
peripheral.delegate = self peripheral.delegate = self
peripheral.discoverServices([BluetoothMeshService.serviceUUID]) peripheral.discoverServices([BluetoothMeshService.serviceUUID])
// Extract peer ID from advertisement or use peripheral identifier // Store peripheral by its system ID temporarily until we get the real peer ID
let peerID: String let tempID = peripheral.identifier.uuidString
if let name = peripheral.name, name.hasPrefix("bitchat-") { connectedPeripherals[tempID] = peripheral
peerID = String(name.dropFirst(8))
} else {
peerID = peripheral.identifier.uuidString.prefix(8).lowercased()
}
connectedPeripherals[peerID] = peripheral print("[BLUETOOTH] Connected to peripheral (temp ID: \(tempID)), waiting for real peer ID...")
print("[DEBUG] Connected to peer: \(peerID)")
// Add to active peers immediately // Request RSSI reading
activePeers.insert(peerID) peripheral.readRSSI()
print("[DEBUG] Active peers: \(activePeers)")
// Update peer list to show we're connecting
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
} }
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 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 { if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
connectedPeripherals.removeValue(forKey: peerID) connectedPeripherals.removeValue(forKey: peerID)
@@ -581,6 +567,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
// Remove from active peers // Remove from active peers
activePeers.remove(peerID) activePeers.remove(peerID)
announcedPeers.remove(peerID) announcedPeers.remove(peerID)
announcedToPeers.remove(peerID)
// Only show disconnect if we have a resolved nickname // Only show disconnect if we have a resolved nickname
if let nickname = peerNicknames[peerID], nickname != peerID { if let nickname = peerNicknames[peerID], nickname != peerID {
@@ -600,7 +587,6 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
// Continue scanning for reconnection // Continue scanning for reconnection
if centralManager.state == .poweredOn { if centralManager.state == .poweredOn {
print("[DEBUG] Restarting scan after disconnect")
// Stop and restart to ensure clean state // Stop and restart to ensure clean state
centralManager.stopScan() centralManager.stopScan()
centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
@@ -622,7 +608,6 @@ extension BluetoothMeshService: CBPeripheralDelegate {
for characteristic in characteristics { for characteristic in characteristics {
if characteristic.uuid == BluetoothMeshService.characteristicUUID { if characteristic.uuid == BluetoothMeshService.characteristicUUID {
print("[DEBUG] Found characteristic, subscribing to notifications...")
peripheral.setNotifyValue(true, for: characteristic) peripheral.setNotifyValue(true, for: characteristic)
peripheralCharacteristics[peripheral] = characteristic peripheralCharacteristics[peripheral] = characteristic
@@ -630,31 +615,47 @@ extension BluetoothMeshService: CBPeripheralDelegate {
let publicKeyData = self.encryptionService.publicKey.rawRepresentation let publicKeyData = self.encryptionService.publicKey.rawRepresentation
let packet = BitchatPacket( let packet = BitchatPacket(
type: MessageType.keyExchange.rawValue, type: MessageType.keyExchange.rawValue,
senderID: self.myPeerID.data(using: .utf8)!, ttl: 1,
recipientID: nil, senderID: self.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: publicKeyData
payload: publicKeyData,
signature: nil,
ttl: 1
) )
if let data = packet.data { if let data = packet.toBinaryData() {
peripheral.writeValue(data, for: characteristic, type: .withResponse) 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 announce packet immediately after key exchange
// Send both broadcast and targeted announce for reliability
if let vm = self.delegate as? ChatViewModel { if let vm = self.delegate as? ChatViewModel {
// First, send broadcast announce
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let announcePacket = BitchatPacket( let announcePacket = BitchatPacket(
type: MessageType.announce.rawValue, type: MessageType.announce.rawValue,
senderID: self.myPeerID.data(using: .utf8)!, ttl: 1,
recipientID: nil, senderID: self.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: Data(vm.nickname.utf8)
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1
) )
if let data = announcePacket.data { 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) peripheral.writeValue(data, for: characteristic, type: .withResponse)
print("[KEY_EXCHANGE] Sent targeted announce to peripheral")
}
} }
} }
} }
@@ -667,8 +668,11 @@ extension BluetoothMeshService: CBPeripheralDelegate {
return return
} }
let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key ?? "unknown" // Use the sender ID from the packet, not our local mapping which might still be a temp ID
handleReceivedPacket(packet, from: peerID) 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?) { 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?) { func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { // Handle notification state updates if needed
print("[DEBUG] Error updating notification state: \(error)") }
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 { } else {
print("[DEBUG] Notification state updated for characteristic: \(characteristic.isNotifying)") // 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 { extension BluetoothMeshService: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
print("[DEBUG] Peripheral state changed to: \(peripheral.state.rawValue)")
switch peripheral.state { switch peripheral.state {
case .poweredOn: case .poweredOn:
print("[DEBUG] Peripheral powered on, setting up...")
setupPeripheral() setupPeripheral()
startAdvertising() startAdvertising()
// If we haven't announced yet and we're now powered on, schedule announcement // Scanning will connect to peers and announce then
if !hasAnnounced {
announcementTimer?.invalidate()
announcementTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in
self?.sendInitialAnnouncement()
}
}
default: default:
break break
} }
@@ -714,13 +737,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
} }
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
print("[DEBUG] Received write request")
for request in requests { for request in requests {
if let data = request.value, if let data = request.value,
let packet = BitchatPacket.from(data) { let packet = BitchatPacket.from(data) {
// Try to identify peer from packet // Try to identify peer from packet
let peerID = String(data: packet.senderID, encoding: .utf8) ?? "unknown" let peerID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown"
print("[DEBUG] Write from peer: \(peerID)")
// Store the central for updates // Store the central for updates
if !subscribedCentrals.contains(request.central) { if !subscribedCentrals.contains(request.central) {
@@ -734,34 +755,29 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
let publicKeyData = self.encryptionService.publicKey.rawRepresentation let publicKeyData = self.encryptionService.publicKey.rawRepresentation
let responsePacket = BitchatPacket( let responsePacket = BitchatPacket(
type: MessageType.keyExchange.rawValue, type: MessageType.keyExchange.rawValue,
senderID: self.myPeerID.data(using: .utf8)!, ttl: 1,
recipientID: peerID.data(using: .utf8), senderID: self.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: publicKeyData
payload: publicKeyData,
signature: nil,
ttl: 1
) )
if let data = responsePacket.data { if let data = responsePacket.toBinaryData() {
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central]) peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central])
print("[KEY_EXCHANGE] Sent key exchange response as peripheral")
} }
// Send announce immediately after key exchange // Send announce immediately after key exchange
// Broadcast announce to ensure all peers get it
if let vm = self.delegate as? ChatViewModel { if let vm = self.delegate as? ChatViewModel {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
print("[DEBUG] Sending announce packet to central after key exchange")
let announcePacket = BitchatPacket( let announcePacket = BitchatPacket(
type: MessageType.announce.rawValue, type: MessageType.announce.rawValue,
senderID: self.myPeerID.data(using: .utf8)!, ttl: 1,
recipientID: peerID.data(using: .utf8), senderID: self.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: Data(vm.nickname.utf8)
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1
) )
if let data = announcePacket.data { if let data = announcePacket.toBinaryData() {
let success = peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: nil) peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: nil)
print("[DEBUG] Announce sent to all centrals: \(success)") print("[ANNOUNCE] Sent broadcast announce as peripheral")
} }
} }
} }
@@ -779,48 +795,26 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
} }
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
print("[DEBUG] Central subscribed to notifications")
if !subscribedCentrals.contains(central) { if !subscribedCentrals.contains(central) {
subscribedCentrals.append(central) subscribedCentrals.append(central)
print("[DEBUG] New central subscribed, total: \(subscribedCentrals.count)")
// Send our public key to the newly connected central // Send our public key to the newly connected central
let publicKeyData = encryptionService.publicKey.rawRepresentation let publicKeyData = encryptionService.publicKey.rawRepresentation
let keyPacket = BitchatPacket( let keyPacket = BitchatPacket(
type: MessageType.keyExchange.rawValue, type: MessageType.keyExchange.rawValue,
senderID: myPeerID.data(using: .utf8)!, ttl: 1,
recipientID: nil, senderID: myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970), payload: publicKeyData
payload: try! JSONEncoder().encode(publicKeyData),
signature: nil,
ttl: 1
) )
if let data = keyPacket.data { if let data = keyPacket.toBinaryData() {
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central]) 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 // We'll send announce when we receive their 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])
}
}
}
} }
// Update peer list to show we're connected (even without peer ID yet) // Update peer list to show we're connected (even without peer ID yet)
print("[DEBUG] Updating peer list after subscription")
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
} }
@@ -839,6 +833,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
for peerID in peersToRemove { for peerID in peersToRemove {
activePeers.remove(peerID) activePeers.remove(peerID)
announcedToPeers.remove(peerID)
if let nickname = peerNicknames[peerID] { if let nickname = peerNicknames[peerID] {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname) self.delegate?.didDisconnectFromPeer(nickname)
+46 -14
View File
@@ -142,6 +142,29 @@ class ChatViewModel: ObservableObject {
return formatter.string(from: date) 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 { func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
var result = AttributedString() var result = AttributedString()
@@ -164,7 +187,19 @@ class ChatViewModel: ObservableObject {
} else { } else {
let sender = AttributedString("<\(message.sender)> ") let sender = AttributedString("<\(message.sender)> ")
var senderStyle = AttributeContainer() 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) senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced)
result.append(sender.mergingAttributes(senderStyle)) result.append(sender.mergingAttributes(senderStyle))
@@ -191,7 +226,6 @@ extension ChatViewModel: BitchatDelegate {
func didReceiveMessage(_ message: BitchatMessage) { func didReceiveMessage(_ message: BitchatMessage) {
if message.isPrivate { if message.isPrivate {
// Handle private message // Handle private message
print("[DEBUG] Received private message from \(message.sender)")
// Use the senderPeerID from the message if available // Use the senderPeerID from the message if available
let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender)
@@ -209,7 +243,6 @@ extension ChatViewModel: BitchatDelegate {
// Mark as unread if not currently viewing this chat // Mark as unread if not currently viewing this chat
if selectedPrivateChatPeer != peerID { if selectedPrivateChatPeer != peerID {
unreadPrivateMessages.insert(peerID) unreadPrivateMessages.insert(peerID)
print("[DEBUG] Added unread message indicator for peer: \(peerID)")
// Show notification banner // Show notification banner
showPrivateMessageNotification(from: message.sender, content: message.content) showPrivateMessageNotification(from: message.sender, content: message.content)
@@ -219,7 +252,6 @@ extension ChatViewModel: BitchatDelegate {
} }
} else if message.sender == nickname { } else if message.sender == nickname {
// Our own message that was echoed back - ignore it since we already added it locally // Our own message that was echoed back - ignore it since we already added it locally
print("[DEBUG] Ignoring our own private message echo")
} }
} else { } else {
// Regular public message // Regular public message
@@ -229,11 +261,17 @@ extension ChatViewModel: BitchatDelegate {
#if os(iOS) #if os(iOS)
// Different haptic feedback for private vs public messages // Different haptic feedback for private vs public messages
if message.isPrivate && message.sender != nickname { if message.isPrivate && message.sender != nickname {
// Medium haptic for private messages // Heavy haptic for private messages - more pronounced
let impactFeedback = UIImpactFeedbackGenerator(style: .medium) let impactFeedback = UIImpactFeedbackGenerator(style: .heavy)
impactFeedback.prepare()
impactFeedback.impactOccurred() 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) let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred() impactFeedback.impactOccurred()
} }
@@ -241,7 +279,6 @@ extension ChatViewModel: BitchatDelegate {
} }
func didConnectToPeer(_ peerID: String) { func didConnectToPeer(_ peerID: String) {
print("[DEBUG] didConnectToPeer called with: \(peerID)")
isConnected = true isConnected = true
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
@@ -251,14 +288,12 @@ extension ChatViewModel: BitchatDelegate {
originalSender: nil originalSender: nil
) )
messages.append(systemMessage) messages.append(systemMessage)
print("[DEBUG] Added join message, total messages: \(messages.count)")
// Force UI update // Force UI update
objectWillChange.send() objectWillChange.send()
} }
func didDisconnectFromPeer(_ peerID: String) { func didDisconnectFromPeer(_ peerID: String) {
print("[DEBUG] didDisconnectFromPeer called with: \(peerID)")
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "\(peerID) has left the channel", content: "\(peerID) has left the channel",
@@ -267,14 +302,12 @@ extension ChatViewModel: BitchatDelegate {
originalSender: nil originalSender: nil
) )
messages.append(systemMessage) messages.append(systemMessage)
print("[DEBUG] Added leave message, total messages: \(messages.count)")
// Force UI update // Force UI update
objectWillChange.send() objectWillChange.send()
} }
func didUpdatePeerList(_ peers: [String]) { func didUpdatePeerList(_ peers: [String]) {
print("[DEBUG] ChatViewModel: Peer list updated with \(peers.count) peers: \(peers)")
connectedPeers = peers connectedPeers = peers
isConnected = !peers.isEmpty 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 we're in a private chat with someone who disconnected, exit the chat
if let currentChatPeer = selectedPrivateChatPeer, if let currentChatPeer = selectedPrivateChatPeer,
!peers.contains(currentChatPeer) { !peers.contains(currentChatPeer) {
print("[DEBUG] Private chat peer disconnected, exiting private chat")
endPrivateChat() endPrivateChat()
} }
} }
+19 -1
View File
@@ -148,7 +148,9 @@ struct ContentView: View {
.font(.system(size: 12, design: .monospaced)) .font(.system(size: 12, design: .monospaced))
} else { } else {
let peerNicknames = viewModel.meshService.getPeerNicknames() let peerNicknames = viewModel.meshService.getPeerNicknames()
let peerRSSI = viewModel.meshService.getPeerRSSI()
let myPeerID = viewModel.meshService.myPeerID 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 ForEach(viewModel.connectedPeers.filter { $0 != myPeerID }.sorted(), id: \.self) { peerID in
let displayName = peerNicknames[peerID] ?? "person-\(peerID.prefix(4))" let displayName = peerNicknames[peerID] ?? "person-\(peerID.prefix(4))"
Button(action: { Button(action: {
@@ -168,6 +170,9 @@ struct ContentView: View {
.frame(width: 6, height: 6) .frame(width: 6, height: 6)
} }
} }
#if os(macOS)
.frame(minWidth: 120)
#endif
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.disabled(peerNicknames[peerID] == nil) .disabled(peerNicknames[peerID] == nil)
@@ -176,10 +181,22 @@ struct ContentView: View {
} label: { } label: {
HStack(spacing: 2) { HStack(spacing: 2) {
// Text // 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)) .font(.system(size: 14, design: .monospaced))
#endif
.foregroundColor(viewModel.isConnected ? textColor : Color.red) .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) // Notification indicator (on the right after default chevron)
if !viewModel.unreadPrivateMessages.isEmpty { if !viewModel.unreadPrivateMessages.isEmpty {
Circle() Circle()
@@ -191,6 +208,7 @@ struct ContentView: View {
} }
#if os(macOS) #if os(macOS)
.menuStyle(.borderlessButton) .menuStyle(.borderlessButton)
.menuIndicator(.visible)
#else #else
.menuStyle(.automatic) .menuStyle(.automatic)
#endif #endif
+82
View File
@@ -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
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
```
### 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
+3
View File
@@ -26,6 +26,9 @@ targets:
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET) LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. 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. NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
UIBackgroundModes:
- bluetooth-central
- bluetooth-peripheral
UILaunchScreen: UILaunchScreen:
UIColorName: Black UIColorName: Black
UISupportedInterfaceOrientations: UISupportedInterfaceOrientations: