@magicred-1/react-native-lxmf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LxmfReactNative.podspec +25 -0
- package/README.md +57 -0
- package/android/build.gradle.kts +33 -0
- package/android/src/main/jniLibs/arm64-v8a/liblxmf_rn.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/liblxmf_rn.so +0 -0
- package/android/src/main/jniLibs/x86_64/liblxmf_rn.so +0 -0
- package/android/src/main/kotlin/expo/modules/lxmf/BleManager.kt +282 -0
- package/android/src/main/kotlin/expo/modules/lxmf/LxmfModule.kt +232 -0
- package/app.plugin.js +40 -0
- package/build/LxmfModule.d.ts +29 -0
- package/build/LxmfModule.js +31 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +2 -0
- package/build/useLxmf.d.ts +81 -0
- package/build/useLxmf.js +252 -0
- package/expo-module.config.json +10 -0
- package/ios/BLEManager.swift +494 -0
- package/ios/LxmfModule.swift +422 -0
- package/package.json +76 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreBluetooth
|
|
3
|
+
|
|
4
|
+
/// Dual-role BLE manager for Reticulum mesh networking
|
|
5
|
+
///
|
|
6
|
+
/// Handles two types of BLE connections:
|
|
7
|
+
/// 1. Phone-to-phone mesh: custom GATT service, HDLC+segmentation via ble_iface.rs
|
|
8
|
+
/// 2. RNode (Heltec V3): Nordic UART Service (NUS), KISS framing via nus_iface.rs
|
|
9
|
+
///
|
|
10
|
+
/// BLE data flows:
|
|
11
|
+
/// Mesh: peer writes → lxmf_ble_receive → Rust BleInterface (HDLC)
|
|
12
|
+
/// RNode: NUS notify → lxmf_nus_receive → Rust NusInterface (KISS)
|
|
13
|
+
class BLEManager: NSObject {
|
|
14
|
+
// Phone-to-phone mesh UUIDs (must match ble_iface.rs)
|
|
15
|
+
static let meshServiceUUID = CBUUID(string: "5f3bafcd-2bb7-4de0-9c6f-2c5f88b6b8f2")
|
|
16
|
+
static let rxCharUUID = CBUUID(string: "3b28e4f6-5a30-4a5f-b700-68bb74d1b036")
|
|
17
|
+
static let txCharUUID = CBUUID(string: "8b6ded1a-ea65-4a1e-a1f0-5cf69d5dc2ad")
|
|
18
|
+
|
|
19
|
+
// RNode NUS UUIDs (Nordic UART Service — must match nus_iface.rs)
|
|
20
|
+
static let nusServiceUUID = CBUUID(string: "6e400001-b5a3-f393-e0a9-e50e24dcca9e")
|
|
21
|
+
static let nusTxCharUUID = CBUUID(string: "6e400002-b5a3-f393-e0a9-e50e24dcca9e") // write TO RNode
|
|
22
|
+
static let nusRxCharUUID = CBUUID(string: "6e400003-b5a3-f393-e0a9-e50e24dcca9e") // notify FROM RNode
|
|
23
|
+
|
|
24
|
+
// Central (scanner/client)
|
|
25
|
+
private var centralManager: CBCentralManager!
|
|
26
|
+
private var connectedPeripherals: [UUID: CBPeripheral] = [:]
|
|
27
|
+
private var txCharacteristics: [UUID: CBCharacteristic] = [:]
|
|
28
|
+
|
|
29
|
+
// Peripheral (advertiser/server)
|
|
30
|
+
private var peripheralManager: CBPeripheralManager!
|
|
31
|
+
private var rxCharacteristic: CBMutableCharacteristic?
|
|
32
|
+
private var txCharacteristic: CBMutableCharacteristic?
|
|
33
|
+
private var subscribedCentrals: [CBCentral] = []
|
|
34
|
+
|
|
35
|
+
// Peer address mapping — iOS uses 128-bit UUIDs, Rust uses 6-byte addrs.
|
|
36
|
+
// We derive a 6-byte pseudo-MAC from each UUID and maintain reverse mappings
|
|
37
|
+
// so lxmf_ble_poll_tx frames can be routed to the correct peer.
|
|
38
|
+
private var addrToPeripheralUUID: [Data: UUID] = [:]
|
|
39
|
+
private var addrToCentral: [Data: CBCentral] = [:]
|
|
40
|
+
|
|
41
|
+
// RNode NUS connections — separate from mesh peers
|
|
42
|
+
private var nusPeripherals: [UUID: CBPeripheral] = [:]
|
|
43
|
+
private var nusTxChars: [UUID: CBCharacteristic] = [:] // write TO RNode
|
|
44
|
+
|
|
45
|
+
// Persisted set of peripheral UUIDs that have successfully completed
|
|
46
|
+
// characteristic discovery (i.e. OS-level pairing succeeded).
|
|
47
|
+
// Survives app restarts so we only auto-connect to known-good devices.
|
|
48
|
+
private var bondedPeripherals: Set<UUID> = []
|
|
49
|
+
|
|
50
|
+
// RNode peripherals discovered during scan but not yet paired via iOS Settings.
|
|
51
|
+
// Exposed to the UI so it can prompt the user to pair in Settings first.
|
|
52
|
+
private(set) var discoveredUnpairedRNodes: [UUID: CBPeripheral] = [:]
|
|
53
|
+
|
|
54
|
+
private var isRunning = false
|
|
55
|
+
|
|
56
|
+
private static let bondedKey = "lxmf.ble.bondedPeripheralUUIDs"
|
|
57
|
+
|
|
58
|
+
private func loadBondedPeripherals() {
|
|
59
|
+
if let uuids = UserDefaults.standard.array(forKey: Self.bondedKey) as? [String] {
|
|
60
|
+
bondedPeripherals = Set(uuids.compactMap { UUID(uuidString: $0) })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func saveBondedPeripherals() {
|
|
65
|
+
let uuids = bondedPeripherals.map { $0.uuidString }
|
|
66
|
+
UserDefaults.standard.set(uuids, forKey: Self.bondedKey)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override init() {
|
|
70
|
+
super.init()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func start() {
|
|
74
|
+
guard !isRunning else { return }
|
|
75
|
+
isRunning = true
|
|
76
|
+
loadBondedPeripherals()
|
|
77
|
+
discoveredUnpairedRNodes.removeAll()
|
|
78
|
+
|
|
79
|
+
// Use restoration identifiers for background BLE
|
|
80
|
+
centralManager = CBCentralManager(
|
|
81
|
+
delegate: self,
|
|
82
|
+
queue: DispatchQueue(label: "lxmf.ble.central"),
|
|
83
|
+
options: [CBCentralManagerOptionRestoreIdentifierKey: "lxmf-central"]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
peripheralManager = CBPeripheralManager(
|
|
87
|
+
delegate: self,
|
|
88
|
+
queue: DispatchQueue(label: "lxmf.ble.peripheral"),
|
|
89
|
+
options: [CBPeripheralManagerOptionRestoreIdentifierKey: "lxmf-peripheral"]
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func stop() {
|
|
94
|
+
guard isRunning else { return }
|
|
95
|
+
isRunning = false
|
|
96
|
+
|
|
97
|
+
centralManager?.stopScan()
|
|
98
|
+
for (_, peripheral) in connectedPeripherals {
|
|
99
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
100
|
+
}
|
|
101
|
+
for (_, peripheral) in nusPeripherals {
|
|
102
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
103
|
+
}
|
|
104
|
+
connectedPeripherals.removeAll()
|
|
105
|
+
txCharacteristics.removeAll()
|
|
106
|
+
addrToPeripheralUUID.removeAll()
|
|
107
|
+
addrToCentral.removeAll()
|
|
108
|
+
nusPeripherals.removeAll()
|
|
109
|
+
nusTxChars.removeAll()
|
|
110
|
+
discoveredUnpairedRNodes.removeAll()
|
|
111
|
+
// Don't clear bondedPeripherals — they persist across sessions
|
|
112
|
+
|
|
113
|
+
peripheralManager?.stopAdvertising()
|
|
114
|
+
peripheralManager?.removeAllServices()
|
|
115
|
+
subscribedCentrals.removeAll()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Send data to all connected peers via TX characteristic
|
|
119
|
+
func sendToAll(_ data: Data) {
|
|
120
|
+
// Send via peripheral role (to subscribed centrals)
|
|
121
|
+
if let txChar = txCharacteristic {
|
|
122
|
+
for central in subscribedCentrals {
|
|
123
|
+
peripheralManager?.updateValue(data, for: txChar, onSubscribedCentrals: [central])
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Send via central role (write to connected peripherals' RX)
|
|
128
|
+
for (uuid, char) in txCharacteristics {
|
|
129
|
+
if let peripheral = connectedPeripherals[uuid] {
|
|
130
|
+
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Send data to a specific peer by CoreBluetooth UUID
|
|
136
|
+
func sendToPeer(_ peerUUID: UUID, data: Data) {
|
|
137
|
+
if let peripheral = connectedPeripherals[peerUUID],
|
|
138
|
+
let char = txCharacteristics[peerUUID] {
|
|
139
|
+
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Send data to a specific peer by 6-byte pseudo-MAC address.
|
|
144
|
+
/// Used by drainOutgoing() to route frames from lxmf_ble_poll_tx.
|
|
145
|
+
func sendToPeerAddr(_ addr: Data, data: Data) {
|
|
146
|
+
// Try peripheral role: send notification to a subscribed central
|
|
147
|
+
if let central = addrToCentral[addr], let txChar = txCharacteristic {
|
|
148
|
+
peripheralManager?.updateValue(data, for: txChar, onSubscribedCentrals: [central])
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Try central role: write to connected peripheral's RX characteristic
|
|
153
|
+
if let peripheralUUID = addrToPeripheralUUID[addr],
|
|
154
|
+
let peripheral = connectedPeripherals[peripheralUUID],
|
|
155
|
+
let char = txCharacteristics[peripheralUUID] {
|
|
156
|
+
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Write KISS-framed data to all connected RNodes via NUS TX characteristic.
|
|
161
|
+
/// Called by drainNusOutgoing() in LxmfModule.
|
|
162
|
+
func sendToNus(_ data: Data) {
|
|
163
|
+
for (uuid, char) in nusTxChars {
|
|
164
|
+
if let peripheral = nusPeripherals[uuid] {
|
|
165
|
+
// Chunk data to fit NUS MTU. CoreBluetooth negotiates MTU
|
|
166
|
+
// automatically; maximumWriteValueLength gives the usable size.
|
|
167
|
+
let mtu = peripheral.maximumWriteValueLength(for: .withoutResponse)
|
|
168
|
+
if data.count <= mtu {
|
|
169
|
+
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
170
|
+
} else {
|
|
171
|
+
// Chunk into MTU-sized writes
|
|
172
|
+
var offset = 0
|
|
173
|
+
while offset < data.count {
|
|
174
|
+
let end = min(offset + mtu, data.count)
|
|
175
|
+
let chunk = data[offset..<end]
|
|
176
|
+
peripheral.writeValue(chunk, for: char, type: .withoutResponse)
|
|
177
|
+
offset = end
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Check if any RNode (NUS) peripherals are connected.
|
|
185
|
+
var hasNusConnection: Bool {
|
|
186
|
+
return !nusPeripherals.isEmpty
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Derive a 6-byte pseudo-MAC from a CoreBluetooth UUID.
|
|
190
|
+
/// XOR-folds the 16-byte UUID into 6 bytes for stable peer identification.
|
|
191
|
+
static func uuidToAddr(_ uuid: UUID) -> Data {
|
|
192
|
+
let u = uuid.uuid
|
|
193
|
+
let bytes: [UInt8] = [u.0, u.1, u.2, u.3, u.4, u.5, u.6, u.7,
|
|
194
|
+
u.8, u.9, u.10, u.11, u.12, u.13, u.14, u.15]
|
|
195
|
+
return Data([
|
|
196
|
+
bytes[0] ^ bytes[6] ^ bytes[12],
|
|
197
|
+
bytes[1] ^ bytes[7] ^ bytes[13],
|
|
198
|
+
bytes[2] ^ bytes[8] ^ bytes[14],
|
|
199
|
+
bytes[3] ^ bytes[9] ^ bytes[15],
|
|
200
|
+
bytes[4] ^ bytes[10],
|
|
201
|
+
bytes[5] ^ bytes[11],
|
|
202
|
+
])
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Peripheral Setup
|
|
206
|
+
|
|
207
|
+
private func setupPeripheral() {
|
|
208
|
+
let rxChar = CBMutableCharacteristic(
|
|
209
|
+
type: BLEManager.rxCharUUID,
|
|
210
|
+
properties: [.write, .writeWithoutResponse],
|
|
211
|
+
value: nil,
|
|
212
|
+
permissions: [.writeable]
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
let txChar = CBMutableCharacteristic(
|
|
216
|
+
type: BLEManager.txCharUUID,
|
|
217
|
+
properties: [.notify, .read],
|
|
218
|
+
value: nil,
|
|
219
|
+
permissions: [.readable]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
let service = CBMutableService(type: BLEManager.meshServiceUUID, primary: true)
|
|
223
|
+
service.characteristics = [rxChar, txChar]
|
|
224
|
+
|
|
225
|
+
self.rxCharacteristic = rxChar
|
|
226
|
+
self.txCharacteristic = txChar
|
|
227
|
+
|
|
228
|
+
peripheralManager.add(service)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private func startAdvertising() {
|
|
232
|
+
peripheralManager.startAdvertising([
|
|
233
|
+
CBAdvertisementDataServiceUUIDsKey: [BLEManager.meshServiceUUID],
|
|
234
|
+
CBAdvertisementDataLocalNameKey: "lxmf-mesh"
|
|
235
|
+
])
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// MARK: - Central Setup
|
|
239
|
+
|
|
240
|
+
private func startScanning() {
|
|
241
|
+
centralManager.scanForPeripherals(
|
|
242
|
+
withServices: [BLEManager.meshServiceUUID, BLEManager.nusServiceUUID],
|
|
243
|
+
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MARK: - CBCentralManagerDelegate
|
|
249
|
+
|
|
250
|
+
extension BLEManager: CBCentralManagerDelegate {
|
|
251
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
252
|
+
if central.state == .poweredOn && isRunning {
|
|
253
|
+
startScanning()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
|
258
|
+
advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
259
|
+
guard connectedPeripherals[peripheral.identifier] == nil else { return }
|
|
260
|
+
|
|
261
|
+
// Check if this is an RNode (NUS service) that we haven't paired with yet.
|
|
262
|
+
// If so, don't auto-connect — the user needs to pair in iOS Settings first.
|
|
263
|
+
let advertisedServices = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
|
|
264
|
+
let isNus = advertisedServices.contains(BLEManager.nusServiceUUID)
|
|
265
|
+
|
|
266
|
+
if isNus && !bondedPeripherals.contains(peripheral.identifier) {
|
|
267
|
+
// Track as discovered-but-unpaired so UI can prompt user
|
|
268
|
+
discoveredUnpairedRNodes[peripheral.identifier] = peripheral
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Bonded RNode or mesh peer — connect normally
|
|
273
|
+
connectedPeripherals[peripheral.identifier] = peripheral
|
|
274
|
+
peripheral.delegate = self
|
|
275
|
+
central.connect(peripheral, options: nil)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
279
|
+
// Don't register with Rust yet — wait until service discovery tells us
|
|
280
|
+
// whether this is a mesh peer (→ lxmf_ble_connected) or RNode (→ NUS path).
|
|
281
|
+
connectedPeripherals[peripheral.identifier] = peripheral
|
|
282
|
+
peripheral.discoverServices([BLEManager.meshServiceUUID, BLEManager.nusServiceUUID])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
286
|
+
connectedPeripherals.removeValue(forKey: peripheral.identifier)
|
|
287
|
+
// Retry after short delay for bonded devices
|
|
288
|
+
if isRunning && bondedPeripherals.contains(peripheral.identifier) {
|
|
289
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
|
290
|
+
guard let self = self, self.isRunning else { return }
|
|
291
|
+
central.connect(peripheral, options: nil)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
297
|
+
let isNus = nusPeripherals[peripheral.identifier] != nil
|
|
298
|
+
|
|
299
|
+
// Always remove from connectedPeripherals (used by both mesh and NUS)
|
|
300
|
+
connectedPeripherals.removeValue(forKey: peripheral.identifier)
|
|
301
|
+
|
|
302
|
+
if isNus {
|
|
303
|
+
// RNode NUS disconnect
|
|
304
|
+
nusPeripherals.removeValue(forKey: peripheral.identifier)
|
|
305
|
+
nusTxChars.removeValue(forKey: peripheral.identifier)
|
|
306
|
+
} else {
|
|
307
|
+
// Mesh peer disconnect — notify Rust
|
|
308
|
+
let addr = BLEManager.uuidToAddr(peripheral.identifier)
|
|
309
|
+
addrToPeripheralUUID.removeValue(forKey: addr)
|
|
310
|
+
addr.withUnsafeBytes { ptr in
|
|
311
|
+
_ = lxmf_ble_disconnected(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self))
|
|
312
|
+
}
|
|
313
|
+
txCharacteristics.removeValue(forKey: peripheral.identifier)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Auto-reconnect bonded devices only
|
|
317
|
+
if isRunning && bondedPeripherals.contains(peripheral.identifier) {
|
|
318
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
|
319
|
+
guard let self = self, self.isRunning else { return }
|
|
320
|
+
central.connect(peripheral, options: nil)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Background restoration
|
|
326
|
+
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
|
|
327
|
+
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
|
|
328
|
+
for peripheral in peripherals {
|
|
329
|
+
connectedPeripherals[peripheral.identifier] = peripheral
|
|
330
|
+
peripheral.delegate = self
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// MARK: - CBPeripheralDelegate
|
|
337
|
+
|
|
338
|
+
extension BLEManager: CBPeripheralDelegate {
|
|
339
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
340
|
+
guard let services = peripheral.services else { return }
|
|
341
|
+
for service in services {
|
|
342
|
+
if service.uuid == BLEManager.nusServiceUUID {
|
|
343
|
+
// RNode NUS — discover NUS characteristics
|
|
344
|
+
peripheral.discoverCharacteristics(
|
|
345
|
+
[BLEManager.nusTxCharUUID, BLEManager.nusRxCharUUID],
|
|
346
|
+
for: service
|
|
347
|
+
)
|
|
348
|
+
} else if service.uuid == BLEManager.meshServiceUUID {
|
|
349
|
+
// Phone-to-phone mesh — discover mesh characteristics
|
|
350
|
+
peripheral.discoverCharacteristics(
|
|
351
|
+
[BLEManager.rxCharUUID, BLEManager.txCharUUID],
|
|
352
|
+
for: service
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
359
|
+
guard let chars = service.characteristics else { return }
|
|
360
|
+
|
|
361
|
+
if service.uuid == BLEManager.nusServiceUUID {
|
|
362
|
+
// RNode NUS characteristics — mark as bonded (pairing succeeded)
|
|
363
|
+
bondedPeripherals.insert(peripheral.identifier)
|
|
364
|
+
saveBondedPeripherals()
|
|
365
|
+
discoveredUnpairedRNodes.removeValue(forKey: peripheral.identifier)
|
|
366
|
+
nusPeripherals[peripheral.identifier] = peripheral
|
|
367
|
+
for char in chars {
|
|
368
|
+
if char.uuid == BLEManager.nusTxCharUUID {
|
|
369
|
+
// NUS TX — we write TO the RNode on this char
|
|
370
|
+
nusTxChars[peripheral.identifier] = char
|
|
371
|
+
} else if char.uuid == BLEManager.nusRxCharUUID {
|
|
372
|
+
// NUS RX — subscribe for notifications FROM RNode
|
|
373
|
+
peripheral.setNotifyValue(true, for: char)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Phone-to-phone mesh characteristics — mark as bonded and register with Rust
|
|
380
|
+
bondedPeripherals.insert(peripheral.identifier)
|
|
381
|
+
saveBondedPeripherals()
|
|
382
|
+
let addr = BLEManager.uuidToAddr(peripheral.identifier)
|
|
383
|
+
addrToPeripheralUUID[addr] = peripheral.identifier
|
|
384
|
+
addr.withUnsafeBytes { ptr in
|
|
385
|
+
_ = lxmf_ble_connected(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for char in chars {
|
|
389
|
+
if char.uuid == BLEManager.rxCharUUID {
|
|
390
|
+
// This is the peer's RX — we write to it
|
|
391
|
+
txCharacteristics[peripheral.identifier] = char
|
|
392
|
+
} else if char.uuid == BLEManager.txCharUUID {
|
|
393
|
+
// This is the peer's TX — subscribe for notifications
|
|
394
|
+
peripheral.setNotifyValue(true, for: char)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
400
|
+
guard let value = characteristic.value, !value.isEmpty else { return }
|
|
401
|
+
|
|
402
|
+
if characteristic.uuid == BLEManager.nusRxCharUUID {
|
|
403
|
+
// Inbound data from RNode via NUS — push raw bytes into Rust NusInterface.
|
|
404
|
+
// KISS deframing is handled on the Rust side (stateful).
|
|
405
|
+
value.withUnsafeBytes { dataPtr in
|
|
406
|
+
_ = lxmf_nus_receive(
|
|
407
|
+
dataPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
408
|
+
value.count
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Inbound data from a mesh peer — push into Rust BleInterface
|
|
415
|
+
let addr = BLEManager.uuidToAddr(peripheral.identifier)
|
|
416
|
+
addr.withUnsafeBytes { addrPtr in
|
|
417
|
+
value.withUnsafeBytes { dataPtr in
|
|
418
|
+
_ = lxmf_ble_receive(
|
|
419
|
+
addrPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
420
|
+
dataPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
421
|
+
value.count
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// MARK: - CBPeripheralManagerDelegate
|
|
429
|
+
|
|
430
|
+
extension BLEManager: CBPeripheralManagerDelegate {
|
|
431
|
+
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
432
|
+
if peripheral.state == .poweredOn && isRunning {
|
|
433
|
+
setupPeripheral()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
438
|
+
if error == nil {
|
|
439
|
+
startAdvertising()
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
444
|
+
for request in requests {
|
|
445
|
+
if request.characteristic.uuid == BLEManager.rxCharUUID,
|
|
446
|
+
let value = request.value, !value.isEmpty {
|
|
447
|
+
// Inbound write from a central peer — push into Rust BleInterface
|
|
448
|
+
let addr = BLEManager.uuidToAddr(request.central.identifier)
|
|
449
|
+
addr.withUnsafeBytes { addrPtr in
|
|
450
|
+
value.withUnsafeBytes { dataPtr in
|
|
451
|
+
_ = lxmf_ble_receive(
|
|
452
|
+
addrPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
453
|
+
dataPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
|
|
454
|
+
value.count
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
peripheral.respond(to: request, withResult: .success)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral,
|
|
464
|
+
didSubscribeTo characteristic: CBCharacteristic) {
|
|
465
|
+
if !subscribedCentrals.contains(where: { $0.identifier == central.identifier }) {
|
|
466
|
+
subscribedCentrals.append(central)
|
|
467
|
+
// Register central as a peer with Rust
|
|
468
|
+
let addr = BLEManager.uuidToAddr(central.identifier)
|
|
469
|
+
addrToCentral[addr] = central
|
|
470
|
+
addr.withUnsafeBytes { ptr in
|
|
471
|
+
_ = lxmf_ble_connected(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self))
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral,
|
|
477
|
+
didUnsubscribeFrom characteristic: CBCharacteristic) {
|
|
478
|
+
subscribedCentrals.removeAll { $0.identifier == central.identifier }
|
|
479
|
+
// Notify Rust of central disconnection
|
|
480
|
+
let addr = BLEManager.uuidToAddr(central.identifier)
|
|
481
|
+
addrToCentral.removeValue(forKey: addr)
|
|
482
|
+
addr.withUnsafeBytes { ptr in
|
|
483
|
+
_ = lxmf_ble_disconnected(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self))
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Background restoration
|
|
488
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String: Any]) {
|
|
489
|
+
// Re-setup services on restoration
|
|
490
|
+
if isRunning {
|
|
491
|
+
setupPeripheral()
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|