@magicred-1/react-native-lxmf 0.1.6 → 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.
@@ -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
- startAdvertising()
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
- // Enable notifications on RX characteristic
211
- val rxChar = service.getCharacteristic(RNS_RX_CHAR_UUID)
212
- if (rxChar != null) {
213
- gatt.setCharacteristicNotification(rxChar, true)
214
- val cccd = rxChar.getDescriptor(CCCD_UUID)
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 == RNS_RX_CHAR_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 txChar = service.getCharacteristic(RNS_TX_CHAR_UUID) ?: continue
261
- txChar.value = data
262
- txChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
263
- val ok = gatt.writeCharacteristic(txChar)
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?
@@ -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;
@@ -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; } });
@@ -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,
@@ -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: "lxmf-mesh"
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] ?? []
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicred-1/react-native-lxmf",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "LXMF Reticulum mesh networking for React Native + Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",