mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 11:45:21 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user