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