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;
classes = {
};
objectVersion = 54;
objectVersion = 63;
objects = {
/* Begin PBXBuildFile section */
@@ -30,7 +30,7 @@
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<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; };
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>"; };
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>"; };
@@ -158,7 +158,6 @@
ProvisioningStyle = Automatic;
};
AF077EA0474EDEDE2C72716C = {
DevelopmentTeam = "";
ProvisioningStyle = Automatic;
};
};
@@ -173,7 +172,6 @@
);
mainGroup = 18198ED912AAF495D8AF7763;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 54;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -241,6 +239,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -265,6 +264,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+5
View File
@@ -31,6 +31,11 @@
<key>UIColorName</key>
<string>Black</string>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
+83 -43
View File
@@ -1,5 +1,15 @@
import Foundation
extension Data {
func trimmingNullBytes() -> Data {
// Find the first null byte
if let nullIndex = self.firstIndex(of: 0) {
return self.prefix(nullIndex)
}
return self
}
}
// Binary Protocol Format:
// Header (Fixed 13 bytes):
// - Version: 1 byte
@@ -36,8 +46,8 @@ struct BinaryProtocol {
data.append(packet.ttl)
// Timestamp (8 bytes, big-endian)
withUnsafeBytes(of: packet.timestamp.bigEndian) { bytes in
data.append(contentsOf: bytes)
for i in (0..<8).reversed() {
data.append(UInt8((packet.timestamp >> (i * 8)) & 0xFF))
}
// Flags
@@ -52,9 +62,8 @@ struct BinaryProtocol {
// Payload length (2 bytes, big-endian)
let payloadLength = UInt16(packet.payload.count)
withUnsafeBytes(of: payloadLength.bigEndian) { bytes in
data.append(contentsOf: bytes)
}
data.append(UInt8((payloadLength >> 8) & 0xFF))
data.append(UInt8(payloadLength & 0xFF))
// SenderID (exactly 8 bytes)
let senderBytes = packet.senderID.prefix(senderIDSize)
@@ -95,8 +104,9 @@ struct BinaryProtocol {
let ttl = data[offset]; offset += 1
// Timestamp
let timestamp = data[offset..<offset+8].withUnsafeBytes { bytes in
bytes.load(as: UInt64.self).bigEndian
let timestampData = data[offset..<offset+8]
let timestamp = timestampData.reduce(0) { result, byte in
(result << 8) | UInt64(byte)
}
offset += 8
@@ -106,8 +116,9 @@ struct BinaryProtocol {
let hasSignature = (flags & Flags.hasSignature) != 0
// Payload length
let payloadLength = data[offset..<offset+2].withUnsafeBytes { bytes in
bytes.load(as: UInt16.self).bigEndian
let payloadLengthData = data[offset..<offset+2]
let payloadLength = payloadLengthData.reduce(0) { result, byte in
(result << 8) | UInt16(byte)
}
offset += 2
@@ -185,8 +196,9 @@ extension BitchatMessage {
// Timestamp
let timestampSeconds = UInt64(timestamp.timeIntervalSince1970)
withUnsafeBytes(of: timestampSeconds.bigEndian) { bytes in
data.append(contentsOf: bytes)
// Encode as 8 bytes, big-endian
for i in (0..<8).reversed() {
data.append(UInt8((timestampSeconds >> (i * 8)) & 0xFF))
}
// ID
@@ -208,9 +220,9 @@ extension BitchatMessage {
// Content
if let contentData = content.data(using: .utf8) {
let length = UInt16(min(contentData.count, 65535))
withUnsafeBytes(of: length.bigEndian) { bytes in
data.append(contentsOf: bytes)
}
// Encode length as 2 bytes, big-endian
data.append(UInt8((length >> 8) & 0xFF))
data.append(UInt8(length & 0xFF))
data.append(contentData.prefix(Int(length)))
} else {
data.append(contentsOf: [0, 0])
@@ -236,12 +248,21 @@ extension BitchatMessage {
}
static func fromBinaryPayload(_ data: Data) -> BitchatMessage? {
guard data.count >= 13 else { return nil } // Minimum size
// Create an immutable copy to prevent threading issues
let dataCopy = Data(data)
guard dataCopy.count >= 13 else {
return nil
}
var offset = 0
// Flags
let flags = data[offset]; offset += 1
guard offset < dataCopy.count else {
return nil
}
let flags = dataCopy[offset]; offset += 1
let isRelay = (flags & 0x01) != 0
let isPrivate = (flags & 0x02) != 0
let hasOriginalSender = (flags & 0x04) != 0
@@ -249,64 +270,82 @@ extension BitchatMessage {
let hasSenderPeerID = (flags & 0x10) != 0
// Timestamp
let timestampSeconds = data[offset..<offset+8].withUnsafeBytes { bytes in
bytes.load(as: UInt64.self).bigEndian
guard offset + 8 <= dataCopy.count else {
return nil
}
let timestampData = dataCopy[offset..<offset+8]
let timestampSeconds = timestampData.reduce(0) { result, byte in
(result << 8) | UInt64(byte)
}
offset += 8
let timestamp = Date(timeIntervalSince1970: TimeInterval(timestampSeconds))
// ID
let idLength = Int(data[offset]); offset += 1
guard offset + idLength <= data.count else { return nil }
_ = String(data: data[offset..<offset+idLength], encoding: .utf8) ?? UUID().uuidString
guard offset < dataCopy.count else {
return nil
}
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
// Sender
guard offset < data.count else { return nil }
let senderLength = Int(data[offset]); offset += 1
guard offset + senderLength <= data.count else { return nil }
let sender = String(data: data[offset..<offset+senderLength], encoding: .utf8) ?? "unknown"
guard offset < dataCopy.count else {
return nil
}
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
// Content
guard offset + 2 <= data.count else { return nil }
let contentLength = data[offset..<offset+2].withUnsafeBytes { bytes in
Int(bytes.load(as: UInt16.self).bigEndian)
guard offset + 2 <= dataCopy.count else {
return nil
}
let contentLengthData = dataCopy[offset..<offset+2]
let contentLength = Int(contentLengthData.reduce(0) { result, byte in
(result << 8) | UInt16(byte)
})
offset += 2
guard offset + contentLength <= data.count else { return nil }
let content = String(data: data[offset..<offset+contentLength], encoding: .utf8) ?? ""
guard offset + contentLength <= dataCopy.count else {
return nil
}
let content = String(data: dataCopy[offset..<offset+contentLength], encoding: .utf8) ?? ""
offset += contentLength
// Optional fields
var originalSender: String?
if hasOriginalSender && offset < data.count {
let length = Int(data[offset]); offset += 1
if offset + length <= data.count {
originalSender = String(data: data[offset..<offset+length], encoding: .utf8)
if hasOriginalSender && offset < dataCopy.count {
let length = Int(dataCopy[offset]); offset += 1
if offset + length <= dataCopy.count {
originalSender = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length
}
}
var recipientNickname: String?
if hasRecipientNickname && offset < data.count {
let length = Int(data[offset]); offset += 1
if offset + length <= data.count {
recipientNickname = String(data: data[offset..<offset+length], encoding: .utf8)
if hasRecipientNickname && offset < dataCopy.count {
let length = Int(dataCopy[offset]); offset += 1
if offset + length <= dataCopy.count {
recipientNickname = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length
}
}
var senderPeerID: String?
if hasSenderPeerID && offset < data.count {
let length = Int(data[offset]); offset += 1
if offset + length <= data.count {
senderPeerID = String(data: data[offset..<offset+length], encoding: .utf8)
if hasSenderPeerID && offset < dataCopy.count {
let length = Int(dataCopy[offset]); offset += 1
if offset + length <= dataCopy.count {
senderPeerID = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length
}
}
return BitchatMessage(
let message = BitchatMessage(
sender: sender,
content: content,
timestamp: timestamp,
@@ -316,5 +355,6 @@ extension BitchatMessage {
recipientNickname: recipientNickname,
senderPeerID: senderPeerID
)
return message
}
}
+16
View File
@@ -33,10 +33,26 @@ struct BitchatPacket: Codable {
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? {
BinaryProtocol.encode(self)
}
func toBinaryData() -> Data? {
BinaryProtocol.encode(self)
}
static func from(_ data: Data) -> BitchatPacket? {
BinaryProtocol.decode(data)
}
+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)
+46 -14
View File
@@ -142,6 +142,29 @@ class ChatViewModel: ObservableObject {
return formatter.string(from: date)
}
func getRSSIColor(rssi: Int, colorScheme: ColorScheme) -> Color {
let isDark = colorScheme == .dark
// RSSI typically ranges from -30 (excellent) to -90 (poor)
// We'll map this to colors from green (strong) to red (weak)
if rssi >= -50 {
// Excellent signal: bright green
return isDark ? Color(red: 0.0, green: 1.0, blue: 0.0) : Color(red: 0.0, green: 0.7, blue: 0.0)
} else if rssi >= -60 {
// Good signal: green-yellow
return isDark ? Color(red: 0.5, green: 1.0, blue: 0.0) : Color(red: 0.3, green: 0.7, blue: 0.0)
} else if rssi >= -70 {
// Fair signal: yellow
return isDark ? Color(red: 1.0, green: 1.0, blue: 0.0) : Color(red: 0.7, green: 0.7, blue: 0.0)
} else if rssi >= -80 {
// Weak signal: orange
return isDark ? Color(red: 1.0, green: 0.6, blue: 0.0) : Color(red: 0.8, green: 0.4, blue: 0.0)
} else {
// Poor signal: red
return isDark ? Color(red: 1.0, green: 0.2, blue: 0.2) : Color(red: 0.8, green: 0.0, blue: 0.0)
}
}
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
var result = AttributedString()
@@ -164,7 +187,19 @@ class ChatViewModel: ObservableObject {
} else {
let sender = AttributedString("<\(message.sender)> ")
var senderStyle = AttributeContainer()
senderStyle.foregroundColor = message.sender == nickname ? primaryColor : primaryColor.opacity(0.9)
// Get RSSI-based color
let senderColor: Color
if message.sender == nickname {
senderColor = primaryColor
} else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender),
let rssi = meshService.getPeerRSSI()[peerID] {
senderColor = getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme)
} else {
senderColor = primaryColor.opacity(0.9)
}
senderStyle.foregroundColor = senderColor
senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced)
result.append(sender.mergingAttributes(senderStyle))
@@ -191,7 +226,6 @@ extension ChatViewModel: BitchatDelegate {
func didReceiveMessage(_ message: BitchatMessage) {
if message.isPrivate {
// Handle private message
print("[DEBUG] Received private message from \(message.sender)")
// Use the senderPeerID from the message if available
let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender)
@@ -209,7 +243,6 @@ extension ChatViewModel: BitchatDelegate {
// Mark as unread if not currently viewing this chat
if selectedPrivateChatPeer != peerID {
unreadPrivateMessages.insert(peerID)
print("[DEBUG] Added unread message indicator for peer: \(peerID)")
// Show notification banner
showPrivateMessageNotification(from: message.sender, content: message.content)
@@ -219,7 +252,6 @@ extension ChatViewModel: BitchatDelegate {
}
} else if message.sender == nickname {
// Our own message that was echoed back - ignore it since we already added it locally
print("[DEBUG] Ignoring our own private message echo")
}
} else {
// Regular public message
@@ -229,11 +261,17 @@ extension ChatViewModel: BitchatDelegate {
#if os(iOS)
// Different haptic feedback for private vs public messages
if message.isPrivate && message.sender != nickname {
// Medium haptic for private messages
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
// Heavy haptic for private messages - more pronounced
let impactFeedback = UIImpactFeedbackGenerator(style: .heavy)
impactFeedback.prepare()
impactFeedback.impactOccurred()
} else {
// Light haptic for public messages
// Double tap for extra emphasis
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
impactFeedback.impactOccurred()
}
} else if message.sender != nickname {
// Light haptic for public messages from others
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
}
@@ -241,7 +279,6 @@ extension ChatViewModel: BitchatDelegate {
}
func didConnectToPeer(_ peerID: String) {
print("[DEBUG] didConnectToPeer called with: \(peerID)")
isConnected = true
let systemMessage = BitchatMessage(
sender: "system",
@@ -251,14 +288,12 @@ extension ChatViewModel: BitchatDelegate {
originalSender: nil
)
messages.append(systemMessage)
print("[DEBUG] Added join message, total messages: \(messages.count)")
// Force UI update
objectWillChange.send()
}
func didDisconnectFromPeer(_ peerID: String) {
print("[DEBUG] didDisconnectFromPeer called with: \(peerID)")
let systemMessage = BitchatMessage(
sender: "system",
content: "\(peerID) has left the channel",
@@ -267,14 +302,12 @@ extension ChatViewModel: BitchatDelegate {
originalSender: nil
)
messages.append(systemMessage)
print("[DEBUG] Added leave message, total messages: \(messages.count)")
// Force UI update
objectWillChange.send()
}
func didUpdatePeerList(_ peers: [String]) {
print("[DEBUG] ChatViewModel: Peer list updated with \(peers.count) peers: \(peers)")
connectedPeers = peers
isConnected = !peers.isEmpty
@@ -286,7 +319,6 @@ extension ChatViewModel: BitchatDelegate {
// If we're in a private chat with someone who disconnected, exit the chat
if let currentChatPeer = selectedPrivateChatPeer,
!peers.contains(currentChatPeer) {
print("[DEBUG] Private chat peer disconnected, exiting private chat")
endPrivateChat()
}
}
+19 -1
View File
@@ -148,7 +148,9 @@ struct ContentView: View {
.font(.system(size: 12, design: .monospaced))
} else {
let peerNicknames = viewModel.meshService.getPeerNicknames()
let peerRSSI = viewModel.meshService.getPeerRSSI()
let myPeerID = viewModel.meshService.myPeerID
let _ = print("[UI DEBUG] connectedPeers: \(viewModel.connectedPeers), myPeerID: \(myPeerID), RSSI: \(peerRSSI)")
ForEach(viewModel.connectedPeers.filter { $0 != myPeerID }.sorted(), id: \.self) { peerID in
let displayName = peerNicknames[peerID] ?? "person-\(peerID.prefix(4))"
Button(action: {
@@ -168,6 +170,9 @@ struct ContentView: View {
.frame(width: 6, height: 6)
}
}
#if os(macOS)
.frame(minWidth: 120)
#endif
}
.buttonStyle(.plain)
.disabled(peerNicknames[peerID] == nil)
@@ -176,10 +181,22 @@ struct ContentView: View {
} label: {
HStack(spacing: 2) {
// Text
Text(viewModel.isConnected ? "\(viewModel.connectedPeers.count) \(viewModel.connectedPeers.count == 1 ? "person" : "people")" : "scanning")
let otherPeersCount = viewModel.connectedPeers.filter { $0 != viewModel.meshService.myPeerID }.count
Text(viewModel.isConnected ? "\(otherPeersCount) \(otherPeersCount == 1 ? "person" : "people")" : "scanning")
#if os(iOS)
.font(.system(size: 12, design: .monospaced))
#else
.font(.system(size: 14, design: .monospaced))
#endif
.foregroundColor(viewModel.isConnected ? textColor : Color.red)
#if os(iOS)
// Add chevron for iOS
Image(systemName: "chevron.down")
.font(.system(size: 10))
.foregroundColor(textColor.opacity(0.6))
#endif
// Notification indicator (on the right after default chevron)
if !viewModel.unreadPrivateMessages.isEmpty {
Circle()
@@ -191,6 +208,7 @@ struct ContentView: View {
}
#if os(macOS)
.menuStyle(.borderlessButton)
.menuIndicator(.visible)
#else
.menuStyle(.automatic)
#endif
+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)
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
UIBackgroundModes:
- bluetooth-central
- bluetooth-peripheral
UILaunchScreen:
UIColorName: Black
UISupportedInterfaceOrientations: