@magicred-1/react-native-lxmf 0.2.1 → 0.2.2
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/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 +78 -37
- package/example/app/(tabs)/index.tsx +52 -15
- package/ios/BLEManager.swift +31 -6
- package/ios/LxmfModule.swift +7 -2
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -53,6 +53,7 @@ class BleManager(
|
|
|
53
53
|
private var advertiser: BluetoothLeAdvertiser? = null
|
|
54
54
|
private var isScanning = false
|
|
55
55
|
private var isAdvertising = false
|
|
56
|
+
private var isRunning = false
|
|
56
57
|
|
|
57
58
|
// GATT server (peripheral role) — accepts inbound writes from remote centrals
|
|
58
59
|
// and pushes outbound notifications to subscribed centrals.
|
|
@@ -82,20 +83,29 @@ class BleManager(
|
|
|
82
83
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
83
84
|
|
|
84
85
|
fun start() {
|
|
86
|
+
if (isRunning) {
|
|
87
|
+
Log.d(TAG, "BleManager already running — skipping duplicate start()")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
85
90
|
if (adapter == null || !adapter!!.isEnabled) {
|
|
86
91
|
Log.w(TAG, "Bluetooth not available or not enabled")
|
|
87
92
|
return
|
|
88
93
|
}
|
|
94
|
+
isRunning = true
|
|
89
95
|
// GATT server must be opened (and service added) before advertising,
|
|
90
96
|
// otherwise centrals connect and find no service. startAdvertising()
|
|
91
97
|
// is invoked from onServiceAdded once the service is registered.
|
|
92
98
|
openGattServer()
|
|
93
99
|
startScanning()
|
|
100
|
+
// Remove any stale callbacks before scheduling — guards against duplicate
|
|
101
|
+
// poll loops if start() is somehow called more than once.
|
|
102
|
+
mainHandler.removeCallbacks(txPollRunnable)
|
|
94
103
|
mainHandler.postDelayed(txPollRunnable, TX_POLL_INTERVAL_MS)
|
|
95
104
|
Log.i(TAG, "BleManager started")
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
fun stop() {
|
|
108
|
+
isRunning = false
|
|
99
109
|
stopScanning()
|
|
100
110
|
stopAdvertising()
|
|
101
111
|
closeGattServer()
|
|
@@ -441,7 +451,8 @@ class BleManager(
|
|
|
441
451
|
val effectiveMtu = if (status == BluetoothGatt.GATT_SUCCESS) mtu else 23
|
|
442
452
|
Log.i(TAG, "BLE MTU negotiated: $mac -> ${effectiveMtu}B")
|
|
443
453
|
module.nativeOnMtuNegotiated(macToBytes(mac), effectiveMtu)
|
|
444
|
-
//
|
|
454
|
+
// Subscribe to peer's TX notifications; nativeBleConnected fires in onDescriptorWrite
|
|
455
|
+
// after the CCCD write completes — writing before then returns ok=false.
|
|
445
456
|
val service = gatt.getService(RNS_SERVICE_UUID) ?: return
|
|
446
457
|
val peerTxChar = service.getCharacteristic(RNS_TX_CHAR_UUID) ?: return
|
|
447
458
|
gatt.setCharacteristicNotification(peerTxChar, true)
|
|
@@ -450,22 +461,51 @@ class BleManager(
|
|
|
450
461
|
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
451
462
|
@Suppress("DEPRECATION")
|
|
452
463
|
gatt.writeDescriptor(cccd)
|
|
453
|
-
module.nativeBleConnected(macToBytes(mac))
|
|
454
|
-
Log.i(TAG, "BLE peer ready: $mac")
|
|
455
464
|
}
|
|
456
465
|
|
|
466
|
+
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
|
|
467
|
+
val mac = gatt.device.address
|
|
468
|
+
if (descriptor.uuid == CCCD_UUID && descriptor.characteristic?.uuid == RNS_TX_CHAR_UUID) {
|
|
469
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
470
|
+
// GATT setup complete — subscribed to peer TX notifications, safe to write now.
|
|
471
|
+
Log.i(TAG, "BLE peer ready (client): $mac")
|
|
472
|
+
module.nativeBleConnected(macToBytes(mac))
|
|
473
|
+
} else {
|
|
474
|
+
// CCCD write failed — can't subscribe to their notifications from client role,
|
|
475
|
+
// but if they already connected to our server and subscribed, the server-side
|
|
476
|
+
// path handles full duplex. Don't disconnect — keep the connection open.
|
|
477
|
+
Log.w(TAG, "BLE CCCD write failed ($status) on $mac — continuing via server role if available")
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// API 33+: platform delivers value directly — characteristic.value is null here.
|
|
483
|
+
@Suppress("OVERRIDE_DEPRECATION")
|
|
457
484
|
override fun onCharacteristicChanged(
|
|
458
485
|
gatt: BluetoothGatt,
|
|
459
486
|
characteristic: BluetoothGattCharacteristic,
|
|
460
487
|
) {
|
|
488
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return
|
|
461
489
|
if (characteristic.uuid == RNS_TX_CHAR_UUID) {
|
|
462
490
|
@Suppress("DEPRECATION")
|
|
463
491
|
val data = characteristic.value ?: return
|
|
464
|
-
Log.d(TAG, "BLE RX ${data.size}B from ${gatt.device.address}")
|
|
492
|
+
Log.d(TAG, "BLE RX(compat) ${data.size}B from ${gatt.device.address}")
|
|
465
493
|
module.nativeBleReceive(macToBytes(gatt.device.address), data)
|
|
466
494
|
}
|
|
467
495
|
}
|
|
468
496
|
|
|
497
|
+
@androidx.annotation.RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
498
|
+
override fun onCharacteristicChanged(
|
|
499
|
+
gatt: BluetoothGatt,
|
|
500
|
+
characteristic: BluetoothGattCharacteristic,
|
|
501
|
+
value: ByteArray,
|
|
502
|
+
) {
|
|
503
|
+
if (characteristic.uuid == RNS_TX_CHAR_UUID) {
|
|
504
|
+
Log.d(TAG, "BLE RX ${value.size}B from ${gatt.device.address}")
|
|
505
|
+
module.nativeBleReceive(macToBytes(gatt.device.address), value)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
469
509
|
override fun onCharacteristicWrite(
|
|
470
510
|
gatt: BluetoothGatt,
|
|
471
511
|
characteristic: BluetoothGattCharacteristic,
|
|
@@ -479,41 +519,42 @@ class BleManager(
|
|
|
479
519
|
|
|
480
520
|
// ── TX drain — poll Rust and write to peer characteristics ───────────────
|
|
481
521
|
|
|
522
|
+
// Android GATT is single-op: concurrent writeCharacteristic() calls return
|
|
523
|
+
// false and drop the packet. Send one frame per 50ms poll tick instead of
|
|
524
|
+
// draining all pending frames at once.
|
|
482
525
|
private fun drainTxQueue() {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
continue
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Central (client) role: write to peer's RX characteristic.
|
|
503
|
-
// RX has WRITE properties; TX is notify-only (writes there
|
|
504
|
-
// would be rejected by the peer). Matches iOS convention.
|
|
505
|
-
val gatt = connections[mac] ?: continue
|
|
506
|
-
val service = gatt.getService(RNS_SERVICE_UUID) ?: continue
|
|
507
|
-
val peerRxChar = service.getCharacteristic(RNS_RX_CHAR_UUID) ?: continue
|
|
508
|
-
@Suppress("DEPRECATION")
|
|
509
|
-
peerRxChar.value = data
|
|
510
|
-
peerRxChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
511
|
-
@Suppress("DEPRECATION")
|
|
512
|
-
val ok = gatt.writeCharacteristic(peerRxChar)
|
|
513
|
-
Log.d(TAG, "BLE TX ${data.size}B to $mac ok=$ok")
|
|
514
|
-
} catch (e: Exception) {
|
|
515
|
-
Log.e(TAG, "drainTxQueue parse error: ${e.message}")
|
|
526
|
+
val json = module.nativeBlePollTx() ?: return
|
|
527
|
+
try {
|
|
528
|
+
val obj = org.json.JSONObject(json)
|
|
529
|
+
val peerHex = obj.getString("peer") // "aabbccddeeff"
|
|
530
|
+
val dataB64 = obj.getString("data")
|
|
531
|
+
val data = android.util.Base64.decode(dataB64, android.util.Base64.DEFAULT)
|
|
532
|
+
val mac = hexToMacString(peerHex) // "AA:BB:CC:DD:EE:FF"
|
|
533
|
+
|
|
534
|
+
// Peripheral (server) role: peer subscribed to our TX char →
|
|
535
|
+
// push via NOTIFY. Mirrors iOS peripheralManager.updateValue.
|
|
536
|
+
val subscriber = serverSubscribers[mac]
|
|
537
|
+
val txChar = serverTxChar
|
|
538
|
+
if (subscriber != null && txChar != null) {
|
|
539
|
+
val ok = notifyServerSubscriber(subscriber, txChar, data)
|
|
540
|
+
Log.d(TAG, "BLE NOTIFY ${data.size}B to $mac ok=$ok")
|
|
541
|
+
return
|
|
516
542
|
}
|
|
543
|
+
|
|
544
|
+
// Central (client) role: write to peer's RX characteristic.
|
|
545
|
+
// RX has WRITE properties; TX is notify-only (writes there
|
|
546
|
+
// would be rejected by the peer). Matches iOS convention.
|
|
547
|
+
val gatt = connections[mac] ?: return
|
|
548
|
+
val service = gatt.getService(RNS_SERVICE_UUID) ?: return
|
|
549
|
+
val peerRxChar = service.getCharacteristic(RNS_RX_CHAR_UUID) ?: return
|
|
550
|
+
@Suppress("DEPRECATION")
|
|
551
|
+
peerRxChar.value = data
|
|
552
|
+
peerRxChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
553
|
+
@Suppress("DEPRECATION")
|
|
554
|
+
val ok = gatt.writeCharacteristic(peerRxChar)
|
|
555
|
+
Log.d(TAG, "BLE TX ${data.size}B to $mac ok=$ok")
|
|
556
|
+
} catch (e: Exception) {
|
|
557
|
+
Log.e(TAG, "drainTxQueue parse error: ${e.message}")
|
|
517
558
|
}
|
|
518
559
|
}
|
|
519
560
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
View,
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import * as SecureStore from 'expo-secure-store';
|
|
14
|
-
import { LxmfNodeMode, type LxmfEvent, useLxmf } from '@magicred-1/react-native-lxmf';
|
|
14
|
+
import { LxmfModule, LxmfNodeMode, type LxmfEvent, useLxmf } from '@magicred-1/react-native-lxmf';
|
|
15
15
|
|
|
16
16
|
// Persisted identity blob schema (versioned). Stored in expo-secure-store under
|
|
17
17
|
// IDENTITY_KEY — encrypted at rest on iOS (Keychain) and Android (Keystore-backed).
|
|
@@ -215,6 +215,7 @@ export default function HomeScreen() {
|
|
|
215
215
|
const [sendResult, setSendResult] = useState('');
|
|
216
216
|
|
|
217
217
|
const [unpairedRNodes, setUnpairedRNodes] = useState(0);
|
|
218
|
+
const [liveBleCount, setLiveBleCount] = useState(0);
|
|
218
219
|
|
|
219
220
|
// Identity hydration: read once from secure store on mount. Until hydrated,
|
|
220
221
|
// we pass 'new' so Rust generates a fresh identity (which we'll then persist
|
|
@@ -305,7 +306,21 @@ export default function HomeScreen() {
|
|
|
305
306
|
|
|
306
307
|
const announceEvts = useMemo(() => events.filter(e => e.type === 'announceReceived').slice(0, 20), [events]);
|
|
307
308
|
const msgEvts = useMemo(() => events.filter(e => e.type === 'messageReceived').slice(0, 20), [events]);
|
|
308
|
-
const logEvts = useMemo(() => events.filter(e => e.type === 'log').slice(0,
|
|
309
|
+
const logEvts = useMemo(() => events.filter(e => e.type === 'log').slice(0, 100), [events]);
|
|
310
|
+
|
|
311
|
+
// Deduped peer identity hashes from all announce events (any interface)
|
|
312
|
+
const knownPeerHashes = useMemo(() => {
|
|
313
|
+
const map = new Map<string, { hash: string; name: string; lastSeen: string }>();
|
|
314
|
+
for (const e of events) {
|
|
315
|
+
if (e.type !== 'announceReceived') continue;
|
|
316
|
+
const hash = String(e.destHash ?? e.address ?? '');
|
|
317
|
+
if (!hash) continue;
|
|
318
|
+
if (!map.has(hash)) {
|
|
319
|
+
map.set(hash, { hash, name: e.appData ? String(e.appData) : '', lastSeen: fmtTime(e) });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return Array.from(map.values());
|
|
323
|
+
}, [events]);
|
|
309
324
|
const allEvts = useMemo(() => events.slice(0, 30), [events]);
|
|
310
325
|
|
|
311
326
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
@@ -392,6 +407,15 @@ export default function HomeScreen() {
|
|
|
392
407
|
return () => clearInterval(id);
|
|
393
408
|
}, [bleActive, bleUnpairedRNodeCount]);
|
|
394
409
|
|
|
410
|
+
// Live BLE peer count — poll every second
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (!bleActive) { setLiveBleCount(0); return; }
|
|
413
|
+
const tick = () => { try { setLiveBleCount(LxmfModule.blePeerCount()); } catch {} };
|
|
414
|
+
tick();
|
|
415
|
+
const id = setInterval(tick, 1000);
|
|
416
|
+
return () => clearInterval(id);
|
|
417
|
+
}, [bleActive]);
|
|
418
|
+
|
|
395
419
|
const onSend = useCallback(async () => {
|
|
396
420
|
const d = dest.trim().toLowerCase();
|
|
397
421
|
if (!/^[0-9a-f]{32}$/.test(d)) { setSendResult('Dest = 32 hex chars.'); return; }
|
|
@@ -492,7 +516,7 @@ export default function HomeScreen() {
|
|
|
492
516
|
<Accordion title="BLE Mesh" defaultOpen>
|
|
493
517
|
<Text style={S.hint}>Pair RNodes in iOS Settings > Bluetooth first, then start BLE.</Text>
|
|
494
518
|
<Row label="BLE active" value={bleActive ? 'Yes' : 'No'} />
|
|
495
|
-
<Row label="
|
|
519
|
+
<Row label="Connected peers" value={String(liveBleCount)} />
|
|
496
520
|
{unpairedRNodes > 0 && (
|
|
497
521
|
<Text style={S.warn}>
|
|
498
522
|
Found {unpairedRNodes} unpaired RNode{unpairedRNodes > 1 ? 's' : ''}. Open Settings > Bluetooth, pair the device, then restart BLE.
|
|
@@ -504,15 +528,27 @@ export default function HomeScreen() {
|
|
|
504
528
|
</View>
|
|
505
529
|
</Accordion>
|
|
506
530
|
|
|
507
|
-
{/* ── Peers
|
|
508
|
-
<Accordion title="Peers
|
|
509
|
-
{
|
|
510
|
-
|
|
531
|
+
{/* ── BLE Peers ────────────────────────────────────────────────────── */}
|
|
532
|
+
<Accordion title="BLE Peers" badge={liveBleCount} defaultOpen>
|
|
533
|
+
<Text style={S.hint}>
|
|
534
|
+
Live BLE connections: {liveBleCount}. LXMF identity hashes appear after peer announces.
|
|
535
|
+
</Text>
|
|
536
|
+
{knownPeerHashes.length === 0 ? (
|
|
537
|
+
<Text style={S.muted}>No peer announces received yet.</Text>
|
|
511
538
|
) : (
|
|
512
|
-
|
|
513
|
-
<View key={
|
|
514
|
-
<Text
|
|
515
|
-
<Text style={S.
|
|
539
|
+
knownPeerHashes.map((p) => (
|
|
540
|
+
<View key={p.hash} style={S.itemCard}>
|
|
541
|
+
{p.name ? <Text style={S.itemTitle}>{p.name}</Text> : null}
|
|
542
|
+
<Text selectable style={S.itemBody}>{p.hash}</Text>
|
|
543
|
+
<Text style={S.itemMeta}>last seen: {p.lastSeen}</Text>
|
|
544
|
+
<View style={S.announceActions}>
|
|
545
|
+
<Pressable style={S.copyBtn} onPress={() => copyToClipboard(p.hash)}>
|
|
546
|
+
<Text style={S.copyBtnText}>⎘</Text>
|
|
547
|
+
</Pressable>
|
|
548
|
+
<Pressable style={S.sendToBtn} onPress={() => { setDest(p.hash); setSendResult(''); }}>
|
|
549
|
+
<Text style={S.sendToBtnText}>→ Send</Text>
|
|
550
|
+
</Pressable>
|
|
551
|
+
</View>
|
|
516
552
|
</View>
|
|
517
553
|
))
|
|
518
554
|
)}
|
|
@@ -637,14 +673,15 @@ export default function HomeScreen() {
|
|
|
637
673
|
</Accordion>
|
|
638
674
|
|
|
639
675
|
{/* ── Debug Logs ───────────────────────────────────────────────────── */}
|
|
640
|
-
<Accordion title="Debug Logs" badge={counts.logs} defaultOpen
|
|
676
|
+
<Accordion title="Debug Logs" badge={counts.logs} defaultOpen>
|
|
641
677
|
{logEvts.length === 0 ? (
|
|
642
678
|
<Text style={S.muted}>No logs yet.</Text>
|
|
643
679
|
) : (
|
|
644
680
|
logEvts.map((e, i) => (
|
|
645
|
-
<
|
|
646
|
-
|
|
647
|
-
|
|
681
|
+
<View key={`${evtKey(e, 'lg-')}-${i}`} style={S.logRow}>
|
|
682
|
+
<Text style={S.logTime}>{fmtTime(e)}</Text>
|
|
683
|
+
<Text selectable style={S.logLine}>{String(e.message ?? e.msg ?? evtSummary(e))}</Text>
|
|
684
|
+
</View>
|
|
648
685
|
))
|
|
649
686
|
)}
|
|
650
687
|
</Accordion>
|
package/ios/BLEManager.swift
CHANGED
|
@@ -53,6 +53,10 @@ class BLEManager: NSObject {
|
|
|
53
53
|
|
|
54
54
|
private var isRunning = false
|
|
55
55
|
|
|
56
|
+
/// Called when CoreBluetooth signals it is ready to accept more writes.
|
|
57
|
+
/// LxmfModule sets this to re-trigger drainOutgoing() without a timer.
|
|
58
|
+
var onReadyToSend: (() -> Void)?
|
|
59
|
+
|
|
56
60
|
/// Per-launch random token embedded in our advertisement's local name, so
|
|
57
61
|
/// our central role can detect and skip our own peripheral advertisement
|
|
58
62
|
/// (CoreBluetooth does not auto-filter self when running both roles).
|
|
@@ -176,20 +180,30 @@ class BLEManager: NSObject {
|
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
/// Send data to a specific peer by 6-byte pseudo-MAC address.
|
|
179
|
-
///
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
/// Returns false when CoreBluetooth's TX buffer is full — caller should stop
|
|
184
|
+
/// draining and wait for onReadyToSend before resuming.
|
|
185
|
+
@discardableResult
|
|
186
|
+
func sendToPeerAddr(_ addr: Data, data: Data) -> Bool {
|
|
187
|
+
// Peripheral role: push notification to subscribed central.
|
|
188
|
+
// updateValue returns false when the subscriber's buffer is full;
|
|
189
|
+
// peripheralManagerIsReady(toUpdateSubscribers:) fires when it drains.
|
|
182
190
|
if let central = addrToCentral[addr], let txChar = txCharacteristic {
|
|
183
|
-
peripheralManager?.updateValue(data, for: txChar, onSubscribedCentrals: [central])
|
|
184
|
-
return
|
|
191
|
+
let ok = peripheralManager?.updateValue(data, for: txChar, onSubscribedCentrals: [central]) ?? false
|
|
192
|
+
return ok
|
|
185
193
|
}
|
|
186
194
|
|
|
187
|
-
//
|
|
195
|
+
// Central role: write to peer's RX characteristic.
|
|
196
|
+
// canSendWriteWithoutResponse goes false when the internal queue is full;
|
|
197
|
+
// peripheral(_:isReadyToSendWriteWithoutResponse:) fires when it drains.
|
|
188
198
|
if let peripheralUUID = addrToPeripheralUUID[addr],
|
|
189
199
|
let peripheral = connectedPeripherals[peripheralUUID],
|
|
190
200
|
let char = txCharacteristics[peripheralUUID] {
|
|
201
|
+
guard peripheral.canSendWriteWithoutResponse else { return false }
|
|
191
202
|
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
203
|
+
return true
|
|
192
204
|
}
|
|
205
|
+
|
|
206
|
+
return true // peer not found — frame consumed, no retry needed
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
/// Write KISS-framed data to all connected RNodes via NUS TX characteristic.
|
|
@@ -471,6 +485,12 @@ extension BLEManager: CBPeripheralDelegate {
|
|
|
471
485
|
}
|
|
472
486
|
}
|
|
473
487
|
}
|
|
488
|
+
|
|
489
|
+
// Called when writeValue(.withoutResponse) exhausted the internal queue.
|
|
490
|
+
// Re-trigger TX drain so buffered frames get sent now that there's room.
|
|
491
|
+
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
|
|
492
|
+
onReadyToSend?()
|
|
493
|
+
}
|
|
474
494
|
}
|
|
475
495
|
|
|
476
496
|
// MARK: - CBPeripheralManagerDelegate
|
|
@@ -537,6 +557,11 @@ extension BLEManager: CBPeripheralManagerDelegate {
|
|
|
537
557
|
}
|
|
538
558
|
}
|
|
539
559
|
|
|
560
|
+
// Called when updateValue returned false and the subscriber buffer has drained.
|
|
561
|
+
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
|
|
562
|
+
onReadyToSend?()
|
|
563
|
+
}
|
|
564
|
+
|
|
540
565
|
// Background restoration
|
|
541
566
|
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String: Any]) {
|
|
542
567
|
// Re-setup services on restoration
|
package/ios/LxmfModule.swift
CHANGED
|
@@ -141,7 +141,11 @@ public class LxmfModule: Module {
|
|
|
141
141
|
private var txDrainTimer: Timer?
|
|
142
142
|
|
|
143
143
|
// BLE manager for phone-to-phone mesh
|
|
144
|
-
private lazy var bleManager =
|
|
144
|
+
private lazy var bleManager: BLEManager = {
|
|
145
|
+
let mgr = BLEManager()
|
|
146
|
+
mgr.onReadyToSend = { [weak self] in DispatchQueue.main.async { self?.drainOutgoing() } }
|
|
147
|
+
return mgr
|
|
148
|
+
}()
|
|
145
149
|
|
|
146
150
|
public func definition() -> ModuleDefinition {
|
|
147
151
|
Name("LxmfModule")
|
|
@@ -391,7 +395,8 @@ public class LxmfModule: Module {
|
|
|
391
395
|
|
|
392
396
|
let frameData = Data(dataBuf[0..<Int(len)])
|
|
393
397
|
let addr = Data(peerAddr)
|
|
394
|
-
|
|
398
|
+
// Stop draining if CoreBluetooth buffer is full — onReadyToSend re-triggers us.
|
|
399
|
+
guard bleManager.sendToPeerAddr(addr, data: frameData) else { break }
|
|
395
400
|
}
|
|
396
401
|
|
|
397
402
|
// --- NUS: poll for KISS-framed RNode data ---
|