@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.
@@ -0,0 +1,25 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'LxmfReactNative'
3
+ s.version = '0.1.0'
4
+ s.summary = 'LXMF Reticulum mesh networking for React Native'
5
+ s.homepage = 'https://github.com/anon0mesh/lxmf_react_native_rust'
6
+ s.license = { type: 'MIT' }
7
+ s.author = { 'anon0mesh' => 'anon0mesh@example.com' }
8
+ s.source = { git: 'https://github.com/anon0mesh/lxmf_react_native_rust.git' }
9
+
10
+ s.platform = :ios, '13.0'
11
+ s.requires_arc = true
12
+ s.swift_version = '5.5'
13
+
14
+ s.source_files = 'ios/**/*.swift'
15
+ s.public_header_files = 'ios/**/*.h'
16
+
17
+ # Link the Rust static library (XCFramework built by scripts/build-rust-ios.sh)
18
+ s.vendored_frameworks = 'ios/RustCore/liblxmf_rn.xcframework'
19
+ s.libraries = 'c++'
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+
23
+ # BLE framework
24
+ s.frameworks = 'CoreBluetooth', 'Foundation'
25
+ end
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @magicred-1/react-native-lxmf
2
+
3
+ LXMF Reticulum mesh networking module for React Native + Expo.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @magicred-1/react-native-lxmf
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { useLxmf, LxmfNodeMode } from '@magicred-1/react-native-lxmf';
15
+
16
+ const lxmf = useLxmf({
17
+ identityHex: 'new',
18
+ lxmfAddressHex: 'new',
19
+ mode: LxmfNodeMode.BleOnly,
20
+ });
21
+ ```
22
+
23
+ ## Package Contents
24
+
25
+ Published package includes:
26
+
27
+ - `build/` JavaScript + TypeScript declarations
28
+ - `android/` native Android module sources and JNI library artifacts
29
+ - `ios/` native iOS Swift sources
30
+ - `LxmfReactNative.podspec`
31
+ - Expo plugin files (`app.plugin.js`, `expo-module.config.json`)
32
+
33
+ ## Build
34
+
35
+ ```bash
36
+ npm run build
37
+ ```
38
+
39
+ ## Verify Package
40
+
41
+ ```bash
42
+ npm run pack:check
43
+ ```
44
+
45
+ ## Publish
46
+
47
+ ```bash
48
+ npm publish
49
+ ```
50
+
51
+ ## Important Native Note
52
+
53
+ The iOS podspec currently references a Rust static library outside the package directory:
54
+
55
+ - `../../rust-core/target/release/liblxmf_rn.a`
56
+
57
+ For public npm distribution, this static library must be bundled into the package (or built during pod install), otherwise iOS consumers will fail to link.
@@ -0,0 +1,33 @@
1
+ plugins {
2
+ id("com.android.library")
3
+ id("expo-module-gradle-plugin")
4
+ }
5
+
6
+ android {
7
+ namespace = "expo.modules.lxmf"
8
+ compileSdk = 36
9
+
10
+ defaultConfig {
11
+ minSdk = 24
12
+ versionCode = 1
13
+ versionName = "1.0.0"
14
+ }
15
+
16
+ compileOptions {
17
+ sourceCompatibility = JavaVersion.VERSION_11
18
+ targetCompatibility = JavaVersion.VERSION_11
19
+ }
20
+
21
+ kotlinOptions {
22
+ jvmTarget = "11"
23
+ }
24
+
25
+ sourceSets {
26
+ getByName("main").java.srcDirs("src/main/kotlin")
27
+ }
28
+ }
29
+
30
+ dependencies {
31
+ compileOnly(project(":expo-modules-core"))
32
+ }
33
+
@@ -0,0 +1,282 @@
1
+ package expo.modules.lxmf
2
+
3
+ import android.bluetooth.*
4
+ import android.bluetooth.le.*
5
+ import android.content.Context
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.os.ParcelUuid
9
+ import android.util.Log
10
+ import java.util.UUID
11
+
12
+ private const val TAG = "LxmfBle"
13
+
14
+ // UUIDs must match ble_iface.rs constants exactly
15
+ val RNS_SERVICE_UUID: UUID = UUID.fromString("5f3bafcd-2bb7-4de0-9c6f-2c5f88b6b8f2")
16
+ val RNS_RX_CHAR_UUID: UUID = UUID.fromString("3b28e4f6-5a30-4a5f-b700-68bb74d1b036")
17
+ val RNS_TX_CHAR_UUID: UUID = UUID.fromString("8b6ded1a-ea65-4a1e-a1f0-5cf69d5dc2ad")
18
+
19
+ // GATT descriptor for enabling notifications
20
+ val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
21
+
22
+ /**
23
+ * BleManager — owns all Android BLE hardware access for the LXMF BLE interface.
24
+ *
25
+ * Responsibilities:
26
+ * - Scan for Reticulum BLE peers (service UUID filter)
27
+ * - Advertise our service UUID so peers can find us
28
+ * - Connect to discovered peers via GATT
29
+ * - Enable notifications on the RX characteristic
30
+ * - Pass received bytes to Rust via nativeBleReceive()
31
+ * - Poll nativeBlePollTx() and write results to TX characteristic
32
+ * - Notify Rust of connect/disconnect events
33
+ *
34
+ * Rust handles: HDLC framing, segmentation, Reticulum packet routing.
35
+ */
36
+ class BleManager(
37
+ private val context: Context,
38
+ private val module: LxmfModule,
39
+ ) {
40
+ private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
41
+ private val adapter: BluetoothAdapter? get() = bluetoothManager?.adapter
42
+ private val mainHandler = Handler(Looper.getMainLooper())
43
+
44
+ // Active GATT connections keyed by MAC address string
45
+ private val connections = mutableMapOf<String, BluetoothGatt>()
46
+ // MACs we are currently trying to connect (avoid duplicate attempts)
47
+ private val connecting = mutableSetOf<String>()
48
+ // Timestamp (ms) when each MAC last disconnected — enforces reconnect cooldown
49
+ private val disconnectedAt = mutableMapOf<String, Long>()
50
+
51
+ private var scanner: BluetoothLeScanner? = null
52
+ private var advertiser: BluetoothLeAdvertiser? = null
53
+ private var isScanning = false
54
+ private var isAdvertising = false
55
+
56
+ // TX polling — every 50 ms drain the Rust TX queue and write to peers
57
+ private val txPollRunnable = object : Runnable {
58
+ override fun run() {
59
+ drainTxQueue()
60
+ mainHandler.postDelayed(this, TX_POLL_INTERVAL_MS)
61
+ }
62
+ }
63
+
64
+ companion object {
65
+ private const val TX_POLL_INTERVAL_MS = 50L
66
+ private const val SCAN_RESTART_DELAY_MS = 30_000L
67
+ /** How long to wait before reconnecting to a peer that just disconnected. */
68
+ private const val RECONNECT_COOLDOWN_MS = 15_000L
69
+ }
70
+
71
+ // ── Lifecycle ────────────────────────────────────────────────────────────
72
+
73
+ fun start() {
74
+ if (adapter == null || !adapter!!.isEnabled) {
75
+ Log.w(TAG, "Bluetooth not available or not enabled")
76
+ return
77
+ }
78
+ startAdvertising()
79
+ startScanning()
80
+ mainHandler.postDelayed(txPollRunnable, TX_POLL_INTERVAL_MS)
81
+ Log.i(TAG, "BleManager started")
82
+ }
83
+
84
+ fun stop() {
85
+ stopScanning()
86
+ stopAdvertising()
87
+ mainHandler.removeCallbacks(txPollRunnable)
88
+ connections.values.forEach { it.disconnect(); it.close() }
89
+ connections.clear()
90
+ connecting.clear()
91
+ Log.i(TAG, "BleManager stopped")
92
+ }
93
+
94
+ fun connectedPeerCount(): Int = module.nativeBlePeerCount()
95
+
96
+ // ── Advertising (so peers can find us) ───────────────────────────────────
97
+
98
+ private fun startAdvertising() {
99
+ advertiser = adapter?.bluetoothLeAdvertiser ?: return
100
+ val settings = AdvertiseSettings.Builder()
101
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
102
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
103
+ .setConnectable(true)
104
+ .build()
105
+ val data = AdvertiseData.Builder()
106
+ .addServiceUuid(ParcelUuid(RNS_SERVICE_UUID))
107
+ .setIncludeDeviceName(false)
108
+ .build()
109
+ advertiser?.startAdvertising(settings, data, advertiseCallback)
110
+ isAdvertising = true
111
+ Log.d(TAG, "BLE advertising started")
112
+ }
113
+
114
+ private fun stopAdvertising() {
115
+ if (isAdvertising) {
116
+ advertiser?.stopAdvertising(advertiseCallback)
117
+ isAdvertising = false
118
+ }
119
+ }
120
+
121
+ private val advertiseCallback = object : AdvertiseCallback() {
122
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
123
+ Log.i(TAG, "BLE advertise started")
124
+ }
125
+ override fun onStartFailure(errorCode: Int) {
126
+ Log.e(TAG, "BLE advertise failed: $errorCode")
127
+ }
128
+ }
129
+
130
+ // ── Scanning (find peers) ─────────────────────────────────────────────────
131
+
132
+ private fun startScanning() {
133
+ if (isScanning) return
134
+ scanner = adapter?.bluetoothLeScanner ?: return
135
+ val filter = ScanFilter.Builder()
136
+ .setServiceUuid(ParcelUuid(RNS_SERVICE_UUID))
137
+ .build()
138
+ val settings = ScanSettings.Builder()
139
+ .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
140
+ .build()
141
+ scanner?.startScan(listOf(filter), settings, scanCallback)
142
+ isScanning = true
143
+ Log.d(TAG, "BLE scan started")
144
+ }
145
+
146
+ private fun stopScanning() {
147
+ if (isScanning) {
148
+ scanner?.stopScan(scanCallback)
149
+ isScanning = false
150
+ }
151
+ }
152
+
153
+ private val scanCallback = object : ScanCallback() {
154
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
155
+ val device = result.device ?: return
156
+ val mac = device.address ?: return
157
+ if (mac in connections || mac in connecting) return
158
+ val lastDisconnect = disconnectedAt[mac] ?: 0L
159
+ if (System.currentTimeMillis() - lastDisconnect < RECONNECT_COOLDOWN_MS) return
160
+ Log.i(TAG, "BLE: found peer $mac, connecting")
161
+ connecting.add(mac)
162
+ device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
163
+ }
164
+ override fun onScanFailed(errorCode: Int) {
165
+ Log.e(TAG, "BLE scan failed: $errorCode")
166
+ isScanning = false
167
+ // Restart scan after delay
168
+ mainHandler.postDelayed({ startScanning() }, SCAN_RESTART_DELAY_MS)
169
+ }
170
+ }
171
+
172
+ // ── GATT callbacks ────────────────────────────────────────────────────────
173
+
174
+ private val gattCallback = object : BluetoothGattCallback() {
175
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
176
+ val mac = gatt.device.address
177
+ when (newState) {
178
+ BluetoothProfile.STATE_CONNECTED -> {
179
+ Log.i(TAG, "BLE GATT connected: $mac")
180
+ connections[mac] = gatt
181
+ connecting.remove(mac)
182
+ gatt.discoverServices()
183
+ }
184
+ BluetoothProfile.STATE_DISCONNECTED -> {
185
+ // Guard against double-fire (Android BLE can call this twice)
186
+ if (mac !in connections && mac !in connecting) return
187
+ Log.i(TAG, "BLE GATT disconnected: $mac (status=$status)")
188
+ connections.remove(mac)
189
+ connecting.remove(mac)
190
+ disconnectedAt[mac] = System.currentTimeMillis()
191
+ gatt.close()
192
+ // Notify Rust
193
+ module.nativeBleDisconnected(macToBytes(mac))
194
+ }
195
+ }
196
+ }
197
+
198
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
199
+ if (status != BluetoothGatt.GATT_SUCCESS) {
200
+ Log.w(TAG, "Service discovery failed on ${gatt.device.address}: $status")
201
+ gatt.disconnect()
202
+ return
203
+ }
204
+ val service = gatt.getService(RNS_SERVICE_UUID)
205
+ if (service == null) {
206
+ Log.w(TAG, "RNS service not found on ${gatt.device.address}")
207
+ gatt.disconnect()
208
+ return
209
+ }
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)
215
+ cccd?.let {
216
+ it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
217
+ gatt.writeDescriptor(it)
218
+ }
219
+ }
220
+ // Notify Rust of new peer
221
+ module.nativeBleConnected(macToBytes(gatt.device.address))
222
+ Log.i(TAG, "BLE peer ready: ${gatt.device.address}")
223
+ }
224
+
225
+ override fun onCharacteristicChanged(
226
+ gatt: BluetoothGatt,
227
+ characteristic: BluetoothGattCharacteristic,
228
+ ) {
229
+ if (characteristic.uuid == RNS_RX_CHAR_UUID) {
230
+ val data = characteristic.value ?: return
231
+ Log.d(TAG, "BLE RX ${data.size}B from ${gatt.device.address}")
232
+ module.nativeBleReceive(macToBytes(gatt.device.address), data)
233
+ }
234
+ }
235
+
236
+ override fun onCharacteristicWrite(
237
+ gatt: BluetoothGatt,
238
+ characteristic: BluetoothGattCharacteristic,
239
+ status: Int,
240
+ ) {
241
+ if (status != BluetoothGatt.GATT_SUCCESS) {
242
+ Log.w(TAG, "BLE TX write failed: $status on ${gatt.device.address}")
243
+ }
244
+ }
245
+ }
246
+
247
+ // ── TX drain — poll Rust and write to peer characteristics ───────────────
248
+
249
+ private fun drainTxQueue() {
250
+ while (true) {
251
+ val json = module.nativeBlePollTx() ?: break
252
+ try {
253
+ val obj = org.json.JSONObject(json)
254
+ val peerHex = obj.getString("peer") // "aabbccddeeff"
255
+ val dataB64 = obj.getString("data")
256
+ val data = android.util.Base64.decode(dataB64, android.util.Base64.DEFAULT)
257
+ val mac = hexToMacString(peerHex) // "AA:BB:CC:DD:EE:FF"
258
+ val gatt = connections[mac] ?: continue
259
+ 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)
264
+ Log.d(TAG, "BLE TX ${data.size}B to $mac ok=$ok")
265
+ } catch (e: Exception) {
266
+ Log.e(TAG, "drainTxQueue parse error: ${e.message}")
267
+ }
268
+ }
269
+ }
270
+
271
+ // ── Helpers ───────────────────────────────────────────────────────────────
272
+
273
+ /** Convert "AA:BB:CC:DD:EE:FF" → ByteArray(6) */
274
+ private fun macToBytes(mac: String): ByteArray {
275
+ return mac.split(":").map { it.toInt(16).toByte() }.toByteArray()
276
+ }
277
+
278
+ /** Convert "aabbccddeeff" → "AA:BB:CC:DD:EE:FF" */
279
+ private fun hexToMacString(hex: String): String {
280
+ return hex.chunked(2).joinToString(":") { it.uppercase() }
281
+ }
282
+ }
@@ -0,0 +1,232 @@
1
+ package expo.modules.lxmf
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.util.Log
6
+ import expo.modules.kotlin.modules.Module
7
+ import expo.modules.kotlin.modules.ModuleDefinition
8
+ import org.json.JSONArray
9
+
10
+ private const val TAG = "LxmfModule"
11
+ private const val POLL_INTERVAL_MS = 500L
12
+
13
+ class LxmfModule : Module() {
14
+ private val pollHandler = Handler(Looper.getMainLooper())
15
+ private val pollRunnable = object : Runnable {
16
+ override fun run() {
17
+ drainAndEmitEvents()
18
+ pollHandler.postDelayed(this, POLL_INTERVAL_MS)
19
+ }
20
+ }
21
+
22
+ // BleManager is created lazily when the app context is available
23
+ private var bleManager: BleManager? = null
24
+
25
+ override fun definition() = ModuleDefinition {
26
+ Name("LxmfModule")
27
+
28
+ Events(
29
+ "onPacketReceived",
30
+ "onTxReceived",
31
+ "onBeaconDiscovered",
32
+ "onMessageReceived",
33
+ "onAnnounceReceived",
34
+ "onStatusChanged",
35
+ "onLog",
36
+ "onError",
37
+ "onOutgoingPacket"
38
+ )
39
+
40
+ OnCreate {
41
+ if (isNativeLibraryLoaded()) {
42
+ pollHandler.postDelayed(pollRunnable, POLL_INTERVAL_MS)
43
+ } else {
44
+ Log.w(TAG, "Skipping event polling because liblxmf_rn is not loaded")
45
+ }
46
+ // Create BleManager with whatever Android context is available in this lifecycle phase
47
+ val ctx = appContext.reactContext?.applicationContext
48
+ ?: appContext.currentActivity?.applicationContext
49
+ if (ctx != null) {
50
+ bleManager = BleManager(ctx, this@LxmfModule)
51
+ }
52
+ }
53
+
54
+ OnDestroy {
55
+ pollHandler.removeCallbacks(pollRunnable)
56
+ bleManager?.stop()
57
+ bleManager = null
58
+ }
59
+
60
+ // Lifecycle
61
+ Function("init") { dbPath: String? ->
62
+ val rc = nativeInit(dbPath)
63
+ if (rc != 0) throw RuntimeException("nativeInit returned $rc")
64
+ true
65
+ }
66
+
67
+ AsyncFunction("start") { identityHex: String, lxmfAddressHex: String, mode: Int,
68
+ announceIntervalMs: Double, bleMtuHint: Int,
69
+ tcpInterfaces: List<Map<String, Any>>, displayName: String ->
70
+ Log.d(TAG, "start() mode=$mode interfaces=$tcpInterfaces name=$displayName")
71
+ val interfacesJson = org.json.JSONArray(tcpInterfaces.map { iface ->
72
+ org.json.JSONObject().apply {
73
+ put("host", iface["host"] ?: "")
74
+ put("port", iface["port"] ?: 0)
75
+ }
76
+ }).toString()
77
+ val rc = nativeStart(identityHex, lxmfAddressHex, mode, announceIntervalMs.toLong(),
78
+ bleMtuHint.toShort(), interfacesJson, displayName)
79
+ if (rc != 0) throw RuntimeException("nativeStart returned $rc")
80
+ true
81
+ }
82
+
83
+ AsyncFunction("stop") {
84
+ val rc = nativeStop()
85
+ if (rc != 0) throw RuntimeException("nativeStop returned $rc")
86
+ true
87
+ }
88
+
89
+ Function("isRunning") {
90
+ nativeIsRunning()
91
+ }
92
+
93
+ // Messaging
94
+ AsyncFunction("send") { destHex: String, bodyBase64: String ->
95
+ nativeSend(destHex, bodyBase64).toDouble()
96
+ }
97
+
98
+ AsyncFunction("broadcast") { destsHex: List<String>, bodyBase64: String ->
99
+ val destsJson = org.json.JSONArray(destsHex).toString()
100
+ nativeBroadcast(destsJson, bodyBase64).toDouble()
101
+ }
102
+
103
+ // Status & State
104
+ Function("getStatus") {
105
+ nativeGetStatus()
106
+ }
107
+
108
+ Function("getBeacons") {
109
+ nativeGetBeacons()
110
+ }
111
+
112
+ Function("fetchMessages") { limit: Int ->
113
+ nativeFetchMessages(limit)
114
+ }
115
+
116
+ // Configuration
117
+ Function("setLogLevel") { level: Int ->
118
+ nativeSetLogLevel(level) == 0
119
+ }
120
+
121
+ Function("abiVersion") {
122
+ nativeAbiVersion()
123
+ }
124
+
125
+ // BLE Control — starts/stops BLE scan+advertise+GATT bridge to Rust
126
+ Function("startBLE") {
127
+ Log.d(TAG, "startBLE()")
128
+ bleManager?.start()
129
+ true
130
+ }
131
+
132
+ Function("stopBLE") {
133
+ Log.d(TAG, "stopBLE()")
134
+ bleManager?.stop()
135
+ true
136
+ }
137
+
138
+ Function("blePeerCount") {
139
+ nativeBlePeerCount()
140
+ }
141
+ }
142
+
143
+ private fun drainAndEmitEvents() {
144
+ if (!isNativeLibraryLoaded()) return
145
+
146
+ val json = try {
147
+ nativePollEvents()
148
+ } catch (e: UnsatisfiedLinkError) {
149
+ Log.e(TAG, "nativePollEvents unavailable: ${e.message}")
150
+ pollHandler.removeCallbacks(pollRunnable)
151
+ return
152
+ } ?: return
153
+
154
+ try {
155
+ val arr = JSONArray(json)
156
+ for (i in 0 until arr.length()) {
157
+ val obj = arr.getJSONObject(i)
158
+ val type = obj.optString("type")
159
+ val eventName = when (type) {
160
+ "statusChanged" -> "onStatusChanged"
161
+ "announceReceived" -> "onAnnounceReceived"
162
+ "messageReceived" -> "onMessageReceived"
163
+ "packetReceived" -> "onPacketReceived"
164
+ "txReceived" -> "onTxReceived"
165
+ "beaconDiscovered" -> "onBeaconDiscovered"
166
+ "log" -> "onLog"
167
+ "error" -> "onError"
168
+ else -> null
169
+ } ?: continue
170
+
171
+ val params = mutableMapOf<String, Any?>()
172
+ val keys = obj.keys()
173
+ while (keys.hasNext()) {
174
+ val key = keys.next()
175
+ if (key != "type") params[key] = obj.get(key)
176
+ }
177
+ sendEvent(eventName, params)
178
+ }
179
+ } catch (e: Exception) {
180
+ Log.e(TAG, "drainAndEmitEvents parse error: ${e.message}")
181
+ }
182
+ }
183
+
184
+ // Native JNI method declarations — types must match Rust JNI signatures exactly
185
+ private external fun nativeInit(dbPath: String?): Int
186
+ private external fun nativeStart(
187
+ identityHex: String,
188
+ lxmfAddressHex: String,
189
+ mode: Int,
190
+ announceIntervalMs: Long,
191
+ bleMtuHint: Short,
192
+ tcpInterfacesJson: String,
193
+ displayName: String
194
+ ): Int
195
+ private external fun nativeStop(): Int
196
+ private external fun nativeIsRunning(): Boolean
197
+ private external fun nativePollEvents(): String?
198
+ private external fun nativeSend(destHex: String, bodyBase64: String): Long
199
+ private external fun nativeBroadcast(destsJson: String, bodyBase64: String): Long
200
+ private external fun nativeGetStatus(): String?
201
+ private external fun nativeGetBeacons(): String?
202
+ private external fun nativeFetchMessages(limit: Int): String?
203
+ private external fun nativeSetLogLevel(level: Int): Int
204
+ private external fun nativeAbiVersion(): Int
205
+
206
+ // BLE JNI — called by BleManager (same package)
207
+ // Must NOT be `internal` — Kotlin mangles internal function names in JVM bytecode,
208
+ // which breaks JNI symbol resolution (produces e.g. nativeBlePollTx$lxmf_react_native_debug).
209
+ external fun nativeBleReceive(peerAddr: ByteArray, data: ByteArray)
210
+ external fun nativeBlePollTx(): String?
211
+ external fun nativeBleConnected(peerAddr: ByteArray)
212
+ external fun nativeBleDisconnected(peerAddr: ByteArray)
213
+ external fun nativeBlePeerCount(): Int
214
+
215
+ companion object {
216
+ @Volatile
217
+ private var nativeLibraryLoaded = false
218
+
219
+ fun isNativeLibraryLoaded(): Boolean = nativeLibraryLoaded
220
+
221
+ init {
222
+ try {
223
+ System.loadLibrary("lxmf_rn")
224
+ nativeLibraryLoaded = true
225
+ Log.i(TAG, "liblxmf_rn loaded successfully")
226
+ } catch (e: UnsatisfiedLinkError) {
227
+ nativeLibraryLoaded = false
228
+ Log.e(TAG, "Failed to load liblxmf_rn: ${e.message}")
229
+ }
230
+ }
231
+ }
232
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,40 @@
1
+ const { withInfoPlist, withAndroidManifest } = require('@expo/config-plugins');
2
+
3
+ function withLxmfPermissions(config) {
4
+ // Android BLE permissions
5
+ config = withAndroidManifest(config, (c) => {
6
+ const manifest = c.modResults.manifest;
7
+ const permissions = manifest['uses-permission'] || [];
8
+ const blePermissions = [
9
+ 'android.permission.BLUETOOTH',
10
+ 'android.permission.BLUETOOTH_ADMIN',
11
+ 'android.permission.BLUETOOTH_SCAN',
12
+ 'android.permission.BLUETOOTH_CONNECT',
13
+ 'android.permission.BLUETOOTH_ADVERTISE',
14
+ 'android.permission.ACCESS_FINE_LOCATION',
15
+ 'android.permission.ACCESS_COARSE_LOCATION',
16
+ ];
17
+ for (const perm of blePermissions) {
18
+ if (!permissions.some((p) => p.$['android:name'] === perm)) {
19
+ permissions.push({ $: { 'android:name': perm } });
20
+ }
21
+ }
22
+ manifest['uses-permission'] = permissions;
23
+ return c;
24
+ });
25
+
26
+ // iOS BLE usage descriptions
27
+ config = withInfoPlist(config, (c) => {
28
+ c.modResults.NSBluetoothAlwaysUsageDescription =
29
+ c.modResults.NSBluetoothAlwaysUsageDescription ||
30
+ 'Used for LXMF mesh networking via BLE';
31
+ c.modResults.NSBluetoothPeripheralUsageDescription =
32
+ c.modResults.NSBluetoothPeripheralUsageDescription ||
33
+ 'Used for LXMF mesh networking via BLE';
34
+ return c;
35
+ });
36
+
37
+ return config;
38
+ }
39
+
40
+ module.exports = withLxmfPermissions;