@magicred-1/react-native-lxmf 0.1.7 → 0.1.9
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 +232 -11
- package/android/src/main/kotlin/expo/modules/lxmf/LxmfModule.kt +6 -0
- package/build/LxmfModule.d.ts +1 -0
- package/build/LxmfModule.js +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +2 -1
- package/build/useLxmf.d.ts +1 -0
- package/build/useLxmf.js +14 -0
- package/ios/BLEManager.swift +44 -1
- package/ios/LxmfModule.swift +19 -16
- package/ios/RustCore/liblxmf_rn.xcframework/Info.plist +44 -0
- package/ios/RustCore/liblxmf_rn.xcframework/ios-arm64/liblxmf_rn.a +0 -0
- package/ios/RustCore/liblxmf_rn.xcframework/ios-arm64_x86_64-simulator/liblxmf_rn.a +0 -0
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -3,6 +3,7 @@ package expo.modules.lxmf
|
|
|
3
3
|
import android.bluetooth.*
|
|
4
4
|
import android.bluetooth.le.*
|
|
5
5
|
import android.content.Context
|
|
6
|
+
import android.os.Build
|
|
6
7
|
import android.os.Handler
|
|
7
8
|
import android.os.Looper
|
|
8
9
|
import android.os.ParcelUuid
|
|
@@ -53,6 +54,14 @@ class BleManager(
|
|
|
53
54
|
private var isScanning = false
|
|
54
55
|
private var isAdvertising = false
|
|
55
56
|
|
|
57
|
+
// GATT server (peripheral role) — accepts inbound writes from remote centrals
|
|
58
|
+
// and pushes outbound notifications to subscribed centrals.
|
|
59
|
+
private var gattServer: BluetoothGattServer? = null
|
|
60
|
+
private var serverTxChar: BluetoothGattCharacteristic? = null
|
|
61
|
+
// Centrals that have enabled CCC notifications on our TX char, keyed by MAC.
|
|
62
|
+
// Only these are "registered as peers" with Rust (mirrors iOS subscribedCentrals).
|
|
63
|
+
private val serverSubscribers = mutableMapOf<String, BluetoothDevice>()
|
|
64
|
+
|
|
56
65
|
// TX polling — every 50 ms drain the Rust TX queue and write to peers
|
|
57
66
|
private val txPollRunnable = object : Runnable {
|
|
58
67
|
override fun run() {
|
|
@@ -75,7 +84,10 @@ class BleManager(
|
|
|
75
84
|
Log.w(TAG, "Bluetooth not available or not enabled")
|
|
76
85
|
return
|
|
77
86
|
}
|
|
78
|
-
|
|
87
|
+
// GATT server must be opened (and service added) before advertising,
|
|
88
|
+
// otherwise centrals connect and find no service. startAdvertising()
|
|
89
|
+
// is invoked from onServiceAdded once the service is registered.
|
|
90
|
+
openGattServer()
|
|
79
91
|
startScanning()
|
|
80
92
|
mainHandler.postDelayed(txPollRunnable, TX_POLL_INTERVAL_MS)
|
|
81
93
|
Log.i(TAG, "BleManager started")
|
|
@@ -84,6 +96,7 @@ class BleManager(
|
|
|
84
96
|
fun stop() {
|
|
85
97
|
stopScanning()
|
|
86
98
|
stopAdvertising()
|
|
99
|
+
closeGattServer()
|
|
87
100
|
mainHandler.removeCallbacks(txPollRunnable)
|
|
88
101
|
connections.values.forEach { it.disconnect(); it.close() }
|
|
89
102
|
connections.clear()
|
|
@@ -127,6 +140,193 @@ class BleManager(
|
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
// ── GATT server (peripheral role) ────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
private fun openGattServer() {
|
|
146
|
+
val mgr = bluetoothManager ?: return
|
|
147
|
+
gattServer = mgr.openGattServer(context, gattServerCallback)
|
|
148
|
+
if (gattServer == null) {
|
|
149
|
+
Log.e(TAG, "openGattServer returned null (missing BLUETOOTH_CONNECT?)")
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
val service = BluetoothGattService(
|
|
154
|
+
RNS_SERVICE_UUID,
|
|
155
|
+
BluetoothGattService.SERVICE_TYPE_PRIMARY,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// RX — remote centrals write LXMF frames here. We forward to Rust.
|
|
159
|
+
val rxChar = BluetoothGattCharacteristic(
|
|
160
|
+
RNS_RX_CHAR_UUID,
|
|
161
|
+
BluetoothGattCharacteristic.PROPERTY_WRITE or
|
|
162
|
+
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
|
|
163
|
+
BluetoothGattCharacteristic.PERMISSION_WRITE,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// TX — we push outbound LXMF frames to subscribed centrals via NOTIFY.
|
|
167
|
+
val txChar = BluetoothGattCharacteristic(
|
|
168
|
+
RNS_TX_CHAR_UUID,
|
|
169
|
+
BluetoothGattCharacteristic.PROPERTY_NOTIFY or
|
|
170
|
+
BluetoothGattCharacteristic.PROPERTY_READ,
|
|
171
|
+
BluetoothGattCharacteristic.PERMISSION_READ,
|
|
172
|
+
)
|
|
173
|
+
// CCCD lets the central enable/disable notifications.
|
|
174
|
+
val cccd = BluetoothGattDescriptor(
|
|
175
|
+
CCCD_UUID,
|
|
176
|
+
BluetoothGattDescriptor.PERMISSION_READ or
|
|
177
|
+
BluetoothGattDescriptor.PERMISSION_WRITE,
|
|
178
|
+
)
|
|
179
|
+
txChar.addDescriptor(cccd)
|
|
180
|
+
|
|
181
|
+
service.addCharacteristic(rxChar)
|
|
182
|
+
service.addCharacteristic(txChar)
|
|
183
|
+
serverTxChar = txChar
|
|
184
|
+
|
|
185
|
+
val ok = gattServer?.addService(service) ?: false
|
|
186
|
+
Log.i(TAG, "GATT server opened, addService=$ok")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun closeGattServer() {
|
|
190
|
+
// Notify Rust that all subscribed centrals are gone.
|
|
191
|
+
for ((mac, _) in serverSubscribers) {
|
|
192
|
+
module.nativeBleDisconnected(macToBytes(mac))
|
|
193
|
+
}
|
|
194
|
+
serverSubscribers.clear()
|
|
195
|
+
gattServer?.close()
|
|
196
|
+
gattServer = null
|
|
197
|
+
serverTxChar = null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Push outbound bytes to a subscribed central via NOTIFY.
|
|
202
|
+
* Uses the API 33+ value-bearing variant when available (more reliable on
|
|
203
|
+
* Android 13+); falls back to the deprecated set-then-notify path on older
|
|
204
|
+
* devices. Returns true if the notification was queued.
|
|
205
|
+
*/
|
|
206
|
+
private fun notifyServerSubscriber(
|
|
207
|
+
device: BluetoothDevice,
|
|
208
|
+
txChar: BluetoothGattCharacteristic,
|
|
209
|
+
data: ByteArray,
|
|
210
|
+
): Boolean {
|
|
211
|
+
val server = gattServer ?: return false
|
|
212
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
213
|
+
// BluetoothStatusCodes.SUCCESS == 0
|
|
214
|
+
server.notifyCharacteristicChanged(device, txChar, false, data) == 0
|
|
215
|
+
} else {
|
|
216
|
+
@Suppress("DEPRECATION")
|
|
217
|
+
run {
|
|
218
|
+
txChar.value = data
|
|
219
|
+
server.notifyCharacteristicChanged(device, txChar, false)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private val gattServerCallback = object : BluetoothGattServerCallback() {
|
|
225
|
+
override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
|
|
226
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
227
|
+
Log.i(TAG, "GATT service added; starting advertise")
|
|
228
|
+
startAdvertising()
|
|
229
|
+
} else {
|
|
230
|
+
Log.e(TAG, "GATT addService failed: $status")
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
|
|
235
|
+
val mac = device.address ?: return
|
|
236
|
+
when (newState) {
|
|
237
|
+
BluetoothProfile.STATE_CONNECTED -> {
|
|
238
|
+
// Just an ATT connection — peer hasn't subscribed yet, so
|
|
239
|
+
// it's not a "peer" from Rust's POV. Wait for CCC write.
|
|
240
|
+
Log.d(TAG, "GATT server: $mac connected")
|
|
241
|
+
}
|
|
242
|
+
BluetoothProfile.STATE_DISCONNECTED -> {
|
|
243
|
+
if (serverSubscribers.remove(mac) != null) {
|
|
244
|
+
Log.i(TAG, "GATT server: $mac disconnected (was subscribed)")
|
|
245
|
+
module.nativeBleDisconnected(macToBytes(mac))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
override fun onCharacteristicWriteRequest(
|
|
252
|
+
device: BluetoothDevice,
|
|
253
|
+
requestId: Int,
|
|
254
|
+
characteristic: BluetoothGattCharacteristic,
|
|
255
|
+
preparedWrite: Boolean,
|
|
256
|
+
responseNeeded: Boolean,
|
|
257
|
+
offset: Int,
|
|
258
|
+
value: ByteArray?,
|
|
259
|
+
) {
|
|
260
|
+
if (characteristic.uuid == RNS_RX_CHAR_UUID && value != null && value.isNotEmpty()) {
|
|
261
|
+
// Inbound LXMF frame from a remote central. Push raw bytes to
|
|
262
|
+
// Rust — HDLC deframing happens there. 4KB cap enforced in FFI.
|
|
263
|
+
Log.d(TAG, "GATT server RX ${value.size}B from ${device.address}")
|
|
264
|
+
module.nativeBleReceive(macToBytes(device.address), value)
|
|
265
|
+
}
|
|
266
|
+
if (responseNeeded) {
|
|
267
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
override fun onDescriptorWriteRequest(
|
|
272
|
+
device: BluetoothDevice,
|
|
273
|
+
requestId: Int,
|
|
274
|
+
descriptor: BluetoothGattDescriptor,
|
|
275
|
+
preparedWrite: Boolean,
|
|
276
|
+
responseNeeded: Boolean,
|
|
277
|
+
offset: Int,
|
|
278
|
+
value: ByteArray?,
|
|
279
|
+
) {
|
|
280
|
+
if (descriptor.uuid == CCCD_UUID
|
|
281
|
+
&& descriptor.characteristic?.uuid == RNS_TX_CHAR_UUID
|
|
282
|
+
&& value != null
|
|
283
|
+
) {
|
|
284
|
+
val mac = device.address
|
|
285
|
+
when {
|
|
286
|
+
value.contentEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) -> {
|
|
287
|
+
if (serverSubscribers.put(mac, device) == null) {
|
|
288
|
+
Log.i(TAG, "GATT server: $mac subscribed to TX")
|
|
289
|
+
module.nativeBleConnected(macToBytes(mac))
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
value.contentEquals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE) -> {
|
|
293
|
+
if (serverSubscribers.remove(mac) != null) {
|
|
294
|
+
Log.i(TAG, "GATT server: $mac unsubscribed")
|
|
295
|
+
module.nativeBleDisconnected(macToBytes(mac))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (responseNeeded) {
|
|
301
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
override fun onCharacteristicReadRequest(
|
|
306
|
+
device: BluetoothDevice,
|
|
307
|
+
requestId: Int,
|
|
308
|
+
offset: Int,
|
|
309
|
+
characteristic: BluetoothGattCharacteristic,
|
|
310
|
+
) {
|
|
311
|
+
// TX is read+notify; some centrals do an initial read. Return empty.
|
|
312
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, ByteArray(0))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
override fun onDescriptorReadRequest(
|
|
316
|
+
device: BluetoothDevice,
|
|
317
|
+
requestId: Int,
|
|
318
|
+
offset: Int,
|
|
319
|
+
descriptor: BluetoothGattDescriptor,
|
|
320
|
+
) {
|
|
321
|
+
val state = if (serverSubscribers.containsKey(device.address)) {
|
|
322
|
+
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
323
|
+
} else {
|
|
324
|
+
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
|
|
325
|
+
}
|
|
326
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, state)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
130
330
|
// ── Scanning (find peers) ─────────────────────────────────────────────────
|
|
131
331
|
|
|
132
332
|
private fun startScanning() {
|
|
@@ -207,13 +407,17 @@ class BleManager(
|
|
|
207
407
|
gatt.disconnect()
|
|
208
408
|
return
|
|
209
409
|
}
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
410
|
+
// Subscribe to peer's TX char (their notify path → our inbound).
|
|
411
|
+
// Convention matches iOS BLEManager.swift: RX is write-only on the
|
|
412
|
+
// peripheral, TX is notify-only. Subscribing to RX is a no-op.
|
|
413
|
+
val peerTxChar = service.getCharacteristic(RNS_TX_CHAR_UUID)
|
|
414
|
+
if (peerTxChar != null) {
|
|
415
|
+
gatt.setCharacteristicNotification(peerTxChar, true)
|
|
416
|
+
val cccd = peerTxChar.getDescriptor(CCCD_UUID)
|
|
215
417
|
cccd?.let {
|
|
418
|
+
@Suppress("DEPRECATION")
|
|
216
419
|
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
420
|
+
@Suppress("DEPRECATION")
|
|
217
421
|
gatt.writeDescriptor(it)
|
|
218
422
|
}
|
|
219
423
|
}
|
|
@@ -226,7 +430,8 @@ class BleManager(
|
|
|
226
430
|
gatt: BluetoothGatt,
|
|
227
431
|
characteristic: BluetoothGattCharacteristic,
|
|
228
432
|
) {
|
|
229
|
-
if (characteristic.uuid ==
|
|
433
|
+
if (characteristic.uuid == RNS_TX_CHAR_UUID) {
|
|
434
|
+
@Suppress("DEPRECATION")
|
|
230
435
|
val data = characteristic.value ?: return
|
|
231
436
|
Log.d(TAG, "BLE RX ${data.size}B from ${gatt.device.address}")
|
|
232
437
|
module.nativeBleReceive(macToBytes(gatt.device.address), data)
|
|
@@ -255,12 +460,28 @@ class BleManager(
|
|
|
255
460
|
val dataB64 = obj.getString("data")
|
|
256
461
|
val data = android.util.Base64.decode(dataB64, android.util.Base64.DEFAULT)
|
|
257
462
|
val mac = hexToMacString(peerHex) // "AA:BB:CC:DD:EE:FF"
|
|
463
|
+
|
|
464
|
+
// Peripheral (server) role: peer subscribed to our TX char →
|
|
465
|
+
// push via NOTIFY. Mirrors iOS peripheralManager.updateValue.
|
|
466
|
+
val subscriber = serverSubscribers[mac]
|
|
467
|
+
val txChar = serverTxChar
|
|
468
|
+
if (subscriber != null && txChar != null) {
|
|
469
|
+
val ok = notifyServerSubscriber(subscriber, txChar, data)
|
|
470
|
+
Log.d(TAG, "BLE NOTIFY ${data.size}B to $mac ok=$ok")
|
|
471
|
+
continue
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Central (client) role: write to peer's RX characteristic.
|
|
475
|
+
// RX has WRITE properties; TX is notify-only (writes there
|
|
476
|
+
// would be rejected by the peer). Matches iOS convention.
|
|
258
477
|
val gatt = connections[mac] ?: continue
|
|
259
478
|
val service = gatt.getService(RNS_SERVICE_UUID) ?: continue
|
|
260
|
-
val
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
479
|
+
val peerRxChar = service.getCharacteristic(RNS_RX_CHAR_UUID) ?: continue
|
|
480
|
+
@Suppress("DEPRECATION")
|
|
481
|
+
peerRxChar.value = data
|
|
482
|
+
peerRxChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
483
|
+
@Suppress("DEPRECATION")
|
|
484
|
+
val ok = gatt.writeCharacteristic(peerRxChar)
|
|
264
485
|
Log.d(TAG, "BLE TX ${data.size}B to $mac ok=$ok")
|
|
265
486
|
} catch (e: Exception) {
|
|
266
487
|
Log.e(TAG, "drainTxQueue parse error: ${e.message}")
|
|
@@ -100,6 +100,11 @@ class LxmfModule : Module() {
|
|
|
100
100
|
nativeBroadcast(destsJson, bodyBase64).toDouble()
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// Identity
|
|
104
|
+
Function("getIdentityHex") {
|
|
105
|
+
nativeGetIdentityHex()
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
// Status & State
|
|
104
109
|
Function("getStatus") {
|
|
105
110
|
nativeGetStatus()
|
|
@@ -197,6 +202,7 @@ class LxmfModule : Module() {
|
|
|
197
202
|
private external fun nativePollEvents(): String?
|
|
198
203
|
private external fun nativeSend(destHex: String, bodyBase64: String): Long
|
|
199
204
|
private external fun nativeBroadcast(destsJson: String, bodyBase64: String): Long
|
|
205
|
+
private external fun nativeGetIdentityHex(): String?
|
|
200
206
|
private external fun nativeGetStatus(): String?
|
|
201
207
|
private external fun nativeGetBeacons(): String?
|
|
202
208
|
private external fun nativeFetchMessages(limit: Int): String?
|
package/build/LxmfModule.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type NativeModuleType = {
|
|
|
8
8
|
isRunning(): boolean;
|
|
9
9
|
send(destHex: string, bodyBase64: string): Promise<number>;
|
|
10
10
|
broadcast(destsHex: string[], bodyBase64: string): Promise<number>;
|
|
11
|
+
getIdentityHex(): string | null;
|
|
11
12
|
getStatus(): string | null;
|
|
12
13
|
getBeacons(): string | null;
|
|
13
14
|
fetchMessages(limit: number): string | null;
|
package/build/LxmfModule.js
CHANGED
|
@@ -24,6 +24,7 @@ const missingNativeShim = {
|
|
|
24
24
|
isRunning: () => false,
|
|
25
25
|
send: async () => throwMissingNative(),
|
|
26
26
|
broadcast: async () => throwMissingNative(),
|
|
27
|
+
getIdentityHex: () => throwMissingNative(),
|
|
27
28
|
getStatus: () => throwMissingNative(),
|
|
28
29
|
getBeacons: () => throwMissingNative(),
|
|
29
30
|
fetchMessages: () => throwMissingNative(),
|
package/build/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { LxmfModule, LxmfModuleNative, NativeModuleType } from './LxmfModule';
|
|
1
|
+
export { LxmfModule, LxmfModuleNative, isLxmfNativeAvailable, NativeModuleType } from './LxmfModule';
|
|
2
2
|
export { useLxmf, LxmfNodeMode, type UseLxmfOptions, type TcpInterface, type LxmfNodeStatus, type Beacon, type LxmfEvent } from './useLxmf';
|
package/build/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LxmfNodeMode = exports.useLxmf = exports.LxmfModuleNative = exports.LxmfModule = void 0;
|
|
3
|
+
exports.LxmfNodeMode = exports.useLxmf = exports.isLxmfNativeAvailable = exports.LxmfModuleNative = exports.LxmfModule = void 0;
|
|
4
4
|
var LxmfModule_1 = require("./LxmfModule");
|
|
5
5
|
Object.defineProperty(exports, "LxmfModule", { enumerable: true, get: function () { return LxmfModule_1.LxmfModule; } });
|
|
6
6
|
Object.defineProperty(exports, "LxmfModuleNative", { enumerable: true, get: function () { return LxmfModule_1.LxmfModuleNative; } });
|
|
7
|
+
Object.defineProperty(exports, "isLxmfNativeAvailable", { enumerable: true, get: function () { return LxmfModule_1.isLxmfNativeAvailable; } });
|
|
7
8
|
var useLxmf_1 = require("./useLxmf");
|
|
8
9
|
Object.defineProperty(exports, "useLxmf", { enumerable: true, get: function () { return useLxmf_1.useLxmf; } });
|
|
9
10
|
Object.defineProperty(exports, "LxmfNodeMode", { enumerable: true, get: function () { return useLxmf_1.LxmfNodeMode; } });
|
package/build/useLxmf.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export declare function useLxmf(options?: UseLxmfOptions): {
|
|
|
74
74
|
getStatus: () => LxmfNodeStatus | null;
|
|
75
75
|
getBeacons: () => Beacon[];
|
|
76
76
|
fetchMessages: (limit?: number) => any[];
|
|
77
|
+
getIdentityHex: () => string | null;
|
|
77
78
|
setLogLevel: (level: number) => boolean;
|
|
78
79
|
startBLE: () => void;
|
|
79
80
|
stopBLE: () => void;
|
package/build/useLxmf.js
CHANGED
|
@@ -223,6 +223,19 @@ function useLxmf(options = {}) {
|
|
|
223
223
|
const setLogLevel = (0, react_1.useCallback)((level) => {
|
|
224
224
|
return LxmfModule_1.LxmfModule.setLogLevel(level);
|
|
225
225
|
}, []);
|
|
226
|
+
/**
|
|
227
|
+
* Returns the full 128-char private identity hex for persistence, or null
|
|
228
|
+
* if no node is initialized. Persist to encrypted storage (e.g. expo-secure-store)
|
|
229
|
+
* and pass back via UseLxmfOptions.identityHex on next mount to reuse the identity.
|
|
230
|
+
*/
|
|
231
|
+
const getIdentityHex = (0, react_1.useCallback)(() => {
|
|
232
|
+
try {
|
|
233
|
+
return LxmfModule_1.LxmfModule.getIdentityHex();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}, []);
|
|
226
239
|
const startBLE = (0, react_1.useCallback)(() => {
|
|
227
240
|
LxmfModule_1.LxmfModule.startBLE();
|
|
228
241
|
}, []);
|
|
@@ -248,6 +261,7 @@ function useLxmf(options = {}) {
|
|
|
248
261
|
getStatus,
|
|
249
262
|
getBeacons,
|
|
250
263
|
fetchMessages,
|
|
264
|
+
getIdentityHex,
|
|
251
265
|
setLogLevel,
|
|
252
266
|
startBLE,
|
|
253
267
|
stopBLE,
|
package/ios/BLEManager.swift
CHANGED
|
@@ -53,6 +53,12 @@ class BLEManager: NSObject {
|
|
|
53
53
|
|
|
54
54
|
private var isRunning = false
|
|
55
55
|
|
|
56
|
+
/// Per-launch random token embedded in our advertisement's local name, so
|
|
57
|
+
/// our central role can detect and skip our own peripheral advertisement
|
|
58
|
+
/// (CoreBluetooth does not auto-filter self when running both roles).
|
|
59
|
+
private var instanceTokenHex: String = ""
|
|
60
|
+
private static let advertNamePrefix = "lxmf-mesh-"
|
|
61
|
+
|
|
56
62
|
private static let bondedKey = "lxmf.ble.bondedPeripheralUUIDs"
|
|
57
63
|
|
|
58
64
|
private func loadBondedPeripherals() {
|
|
@@ -76,6 +82,35 @@ class BLEManager: NSObject {
|
|
|
76
82
|
loadBondedPeripherals()
|
|
77
83
|
discoveredUnpairedRNodes.removeAll()
|
|
78
84
|
|
|
85
|
+
// Generate fresh per-launch token for self-loop detection.
|
|
86
|
+
// CoreBluetooth doesn't filter our own peripheral when scanning, so we
|
|
87
|
+
// embed a random token in the advertised local name and skip matches.
|
|
88
|
+
var tokenBytes = [UInt8](repeating: 0, count: 4)
|
|
89
|
+
guard SecRandomCopyBytes(kSecRandomDefault, tokenBytes.count, &tokenBytes) == errSecSuccess else {
|
|
90
|
+
// CSPRNG failed — fall back to UUID bytes so two failing devices
|
|
91
|
+
// don't converge to all-zero tokens (deterministic collision).
|
|
92
|
+
let fallback = UUID().uuid
|
|
93
|
+
tokenBytes = [fallback.0, fallback.1, fallback.2, fallback.3]
|
|
94
|
+
instanceTokenHex = tokenBytes.map { String(format: "%02x", $0) }.joined()
|
|
95
|
+
// TODO(sentinel): remove or downgrade to debug before production
|
|
96
|
+
NSLog("[BLE] instance token (fallback): %@", instanceTokenHex)
|
|
97
|
+
// Use restoration identifiers for background BLE
|
|
98
|
+
centralManager = CBCentralManager(
|
|
99
|
+
delegate: self,
|
|
100
|
+
queue: DispatchQueue(label: "lxmf.ble.central"),
|
|
101
|
+
options: [CBCentralManagerOptionRestoreIdentifierKey: "lxmf-central"]
|
|
102
|
+
)
|
|
103
|
+
peripheralManager = CBPeripheralManager(
|
|
104
|
+
delegate: self,
|
|
105
|
+
queue: DispatchQueue(label: "lxmf.ble.peripheral"),
|
|
106
|
+
options: [CBPeripheralManagerOptionRestoreIdentifierKey: "lxmf-peripheral"]
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
instanceTokenHex = tokenBytes.map { String(format: "%02x", $0) }.joined()
|
|
111
|
+
// TODO(sentinel): remove or downgrade to debug before production
|
|
112
|
+
NSLog("[BLE] instance token: %@", instanceTokenHex)
|
|
113
|
+
|
|
79
114
|
// Use restoration identifiers for background BLE
|
|
80
115
|
centralManager = CBCentralManager(
|
|
81
116
|
delegate: self,
|
|
@@ -231,7 +266,7 @@ class BLEManager: NSObject {
|
|
|
231
266
|
private func startAdvertising() {
|
|
232
267
|
peripheralManager.startAdvertising([
|
|
233
268
|
CBAdvertisementDataServiceUUIDsKey: [BLEManager.meshServiceUUID],
|
|
234
|
-
CBAdvertisementDataLocalNameKey:
|
|
269
|
+
CBAdvertisementDataLocalNameKey: BLEManager.advertNamePrefix + instanceTokenHex
|
|
235
270
|
])
|
|
236
271
|
}
|
|
237
272
|
|
|
@@ -258,6 +293,14 @@ extension BLEManager: CBCentralManagerDelegate {
|
|
|
258
293
|
advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
259
294
|
guard connectedPeripherals[peripheral.identifier] == nil else { return }
|
|
260
295
|
|
|
296
|
+
// Self-loop filter: if the advertised local name carries our own
|
|
297
|
+
// instance token, this is our own peripheral being seen by our own
|
|
298
|
+
// central. Skip — CoreBluetooth does not auto-filter this.
|
|
299
|
+
if let advertName = advertisementData[CBAdvertisementDataLocalNameKey] as? String,
|
|
300
|
+
advertName == BLEManager.advertNamePrefix + instanceTokenHex {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
261
304
|
// Check if this is an RNode (NUS service) that we haven't paired with yet.
|
|
262
305
|
// If so, don't auto-connect — the user needs to pair in iOS Settings first.
|
|
263
306
|
let advertisedServices = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
|
package/ios/LxmfModule.swift
CHANGED
|
@@ -44,6 +44,12 @@ func lxmf_poll_events(
|
|
|
44
44
|
_ outCapacity: Int
|
|
45
45
|
) -> Int32
|
|
46
46
|
|
|
47
|
+
@_silgen_name("lxmf_get_identity_hex")
|
|
48
|
+
func lxmf_get_identity_hex(
|
|
49
|
+
_ outBuf: UnsafeMutablePointer<UInt8>?,
|
|
50
|
+
_ outCapacity: Int
|
|
51
|
+
) -> Int32
|
|
52
|
+
|
|
47
53
|
@_silgen_name("lxmf_get_status")
|
|
48
54
|
func lxmf_get_status(
|
|
49
55
|
_ outBuf: UnsafeMutablePointer<UInt8>?,
|
|
@@ -69,22 +75,6 @@ func lxmf_set_log_level(_ level: UInt32) -> Int32
|
|
|
69
75
|
@_silgen_name("lxmf_abi_version")
|
|
70
76
|
func lxmf_abi_version() -> UInt32
|
|
71
77
|
|
|
72
|
-
@_silgen_name("lxmf_hdlc_encode")
|
|
73
|
-
func lxmf_hdlc_encode(
|
|
74
|
-
_ dataPtr: UnsafePointer<UInt8>?,
|
|
75
|
-
_ dataLen: Int,
|
|
76
|
-
_ outPtr: UnsafeMutablePointer<UInt8>?,
|
|
77
|
-
_ outCapacity: Int
|
|
78
|
-
) -> Int32
|
|
79
|
-
|
|
80
|
-
@_silgen_name("lxmf_kiss_encode")
|
|
81
|
-
func lxmf_kiss_encode(
|
|
82
|
-
_ dataPtr: UnsafePointer<UInt8>?,
|
|
83
|
-
_ dataLen: Int,
|
|
84
|
-
_ outPtr: UnsafeMutablePointer<UInt8>?,
|
|
85
|
-
_ outCapacity: Int
|
|
86
|
-
) -> Int32
|
|
87
|
-
|
|
88
78
|
@_silgen_name("lxmf_fetch_messages")
|
|
89
79
|
func lxmf_fetch_messages(
|
|
90
80
|
_ limit: UInt32,
|
|
@@ -259,6 +249,19 @@ public class LxmfModule: Module {
|
|
|
259
249
|
return Double(opId)
|
|
260
250
|
}
|
|
261
251
|
|
|
252
|
+
// --- Identity ---
|
|
253
|
+
|
|
254
|
+
Function("getIdentityHex") { () -> String? in
|
|
255
|
+
// 128 hex chars (full private key) — small, dedicated buffer to avoid
|
|
256
|
+
// sharing with the larger status JSON buffer.
|
|
257
|
+
var buf = [UInt8](repeating: 0, count: 256)
|
|
258
|
+
let len = buf.withUnsafeMutableBufferPointer { ptr in
|
|
259
|
+
lxmf_get_identity_hex(ptr.baseAddress, ptr.count)
|
|
260
|
+
}
|
|
261
|
+
guard len > 0 else { return nil }
|
|
262
|
+
return String(bytes: buf[0..<Int(len)], encoding: .utf8)
|
|
263
|
+
}
|
|
264
|
+
|
|
262
265
|
// --- Status & Beacons ---
|
|
263
266
|
|
|
264
267
|
Function("getStatus") { () -> String? in
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>AvailableLibraries</key>
|
|
6
|
+
<array>
|
|
7
|
+
<dict>
|
|
8
|
+
<key>BinaryPath</key>
|
|
9
|
+
<string>liblxmf_rn.a</string>
|
|
10
|
+
<key>LibraryIdentifier</key>
|
|
11
|
+
<string>ios-arm64</string>
|
|
12
|
+
<key>LibraryPath</key>
|
|
13
|
+
<string>liblxmf_rn.a</string>
|
|
14
|
+
<key>SupportedArchitectures</key>
|
|
15
|
+
<array>
|
|
16
|
+
<string>arm64</string>
|
|
17
|
+
</array>
|
|
18
|
+
<key>SupportedPlatform</key>
|
|
19
|
+
<string>ios</string>
|
|
20
|
+
</dict>
|
|
21
|
+
<dict>
|
|
22
|
+
<key>BinaryPath</key>
|
|
23
|
+
<string>liblxmf_rn.a</string>
|
|
24
|
+
<key>LibraryIdentifier</key>
|
|
25
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
26
|
+
<key>LibraryPath</key>
|
|
27
|
+
<string>liblxmf_rn.a</string>
|
|
28
|
+
<key>SupportedArchitectures</key>
|
|
29
|
+
<array>
|
|
30
|
+
<string>arm64</string>
|
|
31
|
+
<string>x86_64</string>
|
|
32
|
+
</array>
|
|
33
|
+
<key>SupportedPlatform</key>
|
|
34
|
+
<string>ios</string>
|
|
35
|
+
<key>SupportedPlatformVariant</key>
|
|
36
|
+
<string>simulator</string>
|
|
37
|
+
</dict>
|
|
38
|
+
</array>
|
|
39
|
+
<key>CFBundlePackageType</key>
|
|
40
|
+
<string>XFWK</string>
|
|
41
|
+
<key>XCFrameworkFormatVersion</key>
|
|
42
|
+
<string>1.0</string>
|
|
43
|
+
</dict>
|
|
44
|
+
</plist>
|
|
Binary file
|
|
Binary file
|