@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,422 @@
1
+ import ExpoModulesCore
2
+ import CoreBluetooth
3
+
4
+ // C FFI declarations — linked from the Rust staticlib (liblxmf_rn.a)
5
+ @_silgen_name("lxmf_init")
6
+ func lxmf_init(_ dbPath: UnsafePointer<CChar>?) -> Int32
7
+
8
+ @_silgen_name("lxmf_start")
9
+ func lxmf_start(
10
+ _ identityHex: UnsafePointer<CChar>?,
11
+ _ addressHex: UnsafePointer<CChar>?,
12
+ _ mode: UInt32,
13
+ _ announceIntervalMs: UInt64,
14
+ _ bleMtuHint: UInt16,
15
+ _ tcpInterfacesJson: UnsafePointer<CChar>?,
16
+ _ displayName: UnsafePointer<CChar>?
17
+ ) -> Int32
18
+
19
+ @_silgen_name("lxmf_stop")
20
+ func lxmf_stop() -> Int32
21
+
22
+ @_silgen_name("lxmf_is_running")
23
+ func lxmf_is_running() -> Int32
24
+
25
+ @_silgen_name("lxmf_send")
26
+ func lxmf_send(
27
+ _ destPtr: UnsafePointer<UInt8>?,
28
+ _ bodyPtr: UnsafePointer<UInt8>?,
29
+ _ bodyLen: Int
30
+ ) -> Int64
31
+
32
+ @_silgen_name("lxmf_broadcast")
33
+ func lxmf_broadcast(
34
+ _ destsPtr: UnsafePointer<UInt8>?,
35
+ _ destCount: Int,
36
+ _ bodyPtr: UnsafePointer<UInt8>?,
37
+ _ bodyLen: Int
38
+ ) -> Int64
39
+
40
+ @_silgen_name("lxmf_poll_events")
41
+ func lxmf_poll_events(
42
+ _ timeoutMs: UInt64,
43
+ _ outBuf: UnsafeMutablePointer<UInt8>?,
44
+ _ outCapacity: Int
45
+ ) -> Int32
46
+
47
+ @_silgen_name("lxmf_get_status")
48
+ func lxmf_get_status(
49
+ _ outBuf: UnsafeMutablePointer<UInt8>?,
50
+ _ outCapacity: Int
51
+ ) -> Int32
52
+
53
+ @_silgen_name("lxmf_get_beacons")
54
+ func lxmf_get_beacons(
55
+ _ outBuf: UnsafeMutablePointer<UInt8>?,
56
+ _ outCapacity: Int
57
+ ) -> Int32
58
+
59
+ @_silgen_name("lxmf_on_announce")
60
+ func lxmf_on_announce(
61
+ _ destHashPtr: UnsafePointer<UInt8>?,
62
+ _ appDataPtr: UnsafePointer<UInt8>?,
63
+ _ appDataLen: Int
64
+ ) -> Int32
65
+
66
+ @_silgen_name("lxmf_set_log_level")
67
+ func lxmf_set_log_level(_ level: UInt32) -> Int32
68
+
69
+ @_silgen_name("lxmf_abi_version")
70
+ func lxmf_abi_version() -> UInt32
71
+
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
+ @_silgen_name("lxmf_fetch_messages")
89
+ func lxmf_fetch_messages(
90
+ _ limit: UInt32,
91
+ _ outBuf: UnsafeMutablePointer<UInt8>?,
92
+ _ outCapacity: Int
93
+ ) -> Int32
94
+
95
+ // --- BLE Interface FFI ---
96
+
97
+ @_silgen_name("lxmf_ble_receive")
98
+ func lxmf_ble_receive(
99
+ _ peerAddr: UnsafePointer<UInt8>?,
100
+ _ data: UnsafePointer<UInt8>?,
101
+ _ dataLen: Int
102
+ ) -> Int32
103
+
104
+ @_silgen_name("lxmf_ble_poll_tx")
105
+ func lxmf_ble_poll_tx(
106
+ _ outPeer: UnsafeMutablePointer<UInt8>?,
107
+ _ outData: UnsafeMutablePointer<UInt8>?,
108
+ _ outCapacity: Int
109
+ ) -> Int32
110
+
111
+ @_silgen_name("lxmf_ble_connected")
112
+ func lxmf_ble_connected(
113
+ _ peerAddr: UnsafePointer<UInt8>?
114
+ ) -> Int32
115
+
116
+ @_silgen_name("lxmf_ble_disconnected")
117
+ func lxmf_ble_disconnected(
118
+ _ peerAddr: UnsafePointer<UInt8>?
119
+ ) -> Int32
120
+
121
+ @_silgen_name("lxmf_ble_peer_count")
122
+ func lxmf_ble_peer_count() -> Int32
123
+
124
+ // --- NUS Interface FFI (RNode BLE via Nordic UART Service) ---
125
+
126
+ @_silgen_name("lxmf_nus_receive")
127
+ func lxmf_nus_receive(
128
+ _ data: UnsafePointer<UInt8>?,
129
+ _ dataLen: Int
130
+ ) -> Int32
131
+
132
+ @_silgen_name("lxmf_nus_poll_tx")
133
+ func lxmf_nus_poll_tx(
134
+ _ outData: UnsafeMutablePointer<UInt8>?,
135
+ _ outCapacity: Int
136
+ ) -> Int32
137
+
138
+
139
+ public class LxmfModule: Module {
140
+ // Shared JSON buffer for FFI calls (64KB)
141
+ private var jsonBuf = [UInt8](repeating: 0, count: 65536)
142
+
143
+ // Poll timers
144
+ private var rxPollTimer: Timer?
145
+ private var txDrainTimer: Timer?
146
+
147
+ // BLE manager for phone-to-phone mesh
148
+ private lazy var bleManager = BLEManager()
149
+
150
+ public func definition() -> ModuleDefinition {
151
+ Name("LxmfModule")
152
+
153
+ // --- Events emitted to JavaScript ---
154
+ Events(
155
+ "onPacketReceived",
156
+ "onTxReceived",
157
+ "onBeaconDiscovered",
158
+ "onMessageReceived",
159
+ "onAnnounceReceived",
160
+ "onStatusChanged",
161
+ "onLog",
162
+ "onError",
163
+ "onOutgoingPacket"
164
+ )
165
+
166
+ // --- Lifecycle ---
167
+
168
+ Function("init") { (dbPath: String?) -> Bool in
169
+ let result: Int32
170
+ if let path = dbPath {
171
+ result = path.withCString { lxmf_init($0) }
172
+ } else {
173
+ result = lxmf_init(nil)
174
+ }
175
+ return result == 0
176
+ }
177
+
178
+ AsyncFunction("start") { (
179
+ identityHex: String,
180
+ lxmfAddressHex: String,
181
+ mode: Int,
182
+ announceIntervalMs: Double,
183
+ bleMtuHint: Int,
184
+ tcpInterfaces: [[String: Any]],
185
+ displayName: String
186
+ ) -> Bool in
187
+ // Serialize TCP interfaces to JSON (matches Android pattern)
188
+ let interfacesJson: String
189
+ if let data = try? JSONSerialization.data(withJSONObject: tcpInterfaces),
190
+ let str = String(data: data, encoding: .utf8) {
191
+ interfacesJson = str
192
+ } else {
193
+ interfacesJson = "[]"
194
+ }
195
+
196
+ let result = identityHex.withCString { idPtr in
197
+ lxmfAddressHex.withCString { addrPtr in
198
+ interfacesJson.withCString { ifacesPtr in
199
+ displayName.withCString { namePtr in
200
+ lxmf_start(
201
+ idPtr, addrPtr,
202
+ UInt32(mode), UInt64(announceIntervalMs),
203
+ UInt16(bleMtuHint), ifacesPtr, namePtr
204
+ )
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ if result == 0 {
211
+ self.startPolling()
212
+ self.bleManager.start()
213
+ }
214
+
215
+ return result == 0
216
+ }
217
+
218
+ AsyncFunction("stop") { () -> Bool in
219
+ self.stopPolling()
220
+ self.bleManager.stop()
221
+ return lxmf_stop() == 0
222
+ }
223
+
224
+ Function("isRunning") { () -> Bool in
225
+ return lxmf_is_running() != 0
226
+ }
227
+
228
+ // --- Messaging ---
229
+
230
+ AsyncFunction("send") { (destHex: String, bodyBase64: String) -> Double in
231
+ guard let destBytes = Self.hexToBytes(destHex),
232
+ destBytes.count == 16,
233
+ let bodyData = Data(base64Encoded: bodyBase64) else {
234
+ return -1
235
+ }
236
+
237
+ let opId = destBytes.withUnsafeBufferPointer { destBuf in
238
+ [UInt8](bodyData).withUnsafeBufferPointer { bodyBuf in
239
+ lxmf_send(destBuf.baseAddress, bodyBuf.baseAddress, bodyData.count)
240
+ }
241
+ }
242
+ return Double(opId)
243
+ }
244
+
245
+ AsyncFunction("broadcast") { (destsHex: [String], bodyBase64: String) -> Double in
246
+ guard let bodyData = Data(base64Encoded: bodyBase64) else { return -1 }
247
+
248
+ var flatDests = [UInt8]()
249
+ for hex in destsHex {
250
+ guard let bytes = Self.hexToBytes(hex), bytes.count == 16 else { return -1 }
251
+ flatDests.append(contentsOf: bytes)
252
+ }
253
+
254
+ let opId = flatDests.withUnsafeBufferPointer { destBuf in
255
+ [UInt8](bodyData).withUnsafeBufferPointer { bodyBuf in
256
+ lxmf_broadcast(destBuf.baseAddress, destsHex.count, bodyBuf.baseAddress, bodyData.count)
257
+ }
258
+ }
259
+ return Double(opId)
260
+ }
261
+
262
+ // --- Status & Beacons ---
263
+
264
+ Function("getStatus") { () -> String? in
265
+ return self.callJsonFfi { buf, cap in lxmf_get_status(buf, cap) }
266
+ }
267
+
268
+ Function("getBeacons") { () -> String? in
269
+ return self.callJsonFfi { buf, cap in lxmf_get_beacons(buf, cap) }
270
+ }
271
+
272
+ Function("fetchMessages") { (limit: Int) -> String? in
273
+ return self.callJsonFfi { buf, cap in lxmf_fetch_messages(UInt32(limit), buf, cap) }
274
+ }
275
+
276
+ // --- Configuration ---
277
+
278
+ Function("setLogLevel") { (level: Int) -> Bool in
279
+ return lxmf_set_log_level(UInt32(level)) == 0
280
+ }
281
+
282
+ Function("abiVersion") { () -> Int in
283
+ return Int(lxmf_abi_version())
284
+ }
285
+
286
+ // --- BLE interface control ---
287
+
288
+ Function("startBLE") { () -> Void in
289
+ self.bleManager.start()
290
+ }
291
+
292
+ Function("stopBLE") { () -> Void in
293
+ self.bleManager.stop()
294
+ }
295
+
296
+ Function("blePeerCount") { () -> Int in
297
+ return Int(lxmf_ble_peer_count())
298
+ }
299
+
300
+ Function("bleUnpairedRNodeCount") { () -> Int in
301
+ return self.bleManager.discoveredUnpairedRNodes.count
302
+ }
303
+ }
304
+
305
+ // MARK: - Polling
306
+
307
+ private func startPolling() {
308
+ // Must schedule on main thread — AsyncFunction runs on a background
309
+ // dispatch queue whose RunLoop is not active, so timers would never fire.
310
+ DispatchQueue.main.async { [weak self] in
311
+ guard let self = self else { return }
312
+
313
+ // RX event poll: 80ms interval
314
+ self.rxPollTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
315
+ self?.drainEvents()
316
+ }
317
+
318
+ // TX drain for BLE outgoing: 20ms interval
319
+ self.txDrainTimer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] _ in
320
+ self?.drainOutgoing()
321
+ }
322
+ }
323
+ }
324
+
325
+ private func stopPolling() {
326
+ DispatchQueue.main.async { [weak self] in
327
+ self?.rxPollTimer?.invalidate()
328
+ self?.rxPollTimer = nil
329
+ self?.txDrainTimer?.invalidate()
330
+ self?.txDrainTimer = nil
331
+ }
332
+ }
333
+
334
+ private func drainEvents() {
335
+ let len = jsonBuf.withUnsafeMutableBufferPointer { buf in
336
+ lxmf_poll_events(0, buf.baseAddress, buf.count)
337
+ }
338
+
339
+ guard len > 0 else { return }
340
+
341
+ let jsonData = Data(jsonBuf[0..<Int(len)])
342
+ guard let events = try? JSONSerialization.jsonObject(with: jsonData) as? [[String: Any]] else { return }
343
+
344
+ for event in events {
345
+ guard let type_ = event["type"] as? String else { continue }
346
+
347
+ switch type_ {
348
+ case "statusChanged":
349
+ sendEvent("onStatusChanged", event)
350
+ case "packetReceived":
351
+ sendEvent("onPacketReceived", event)
352
+ case "txReceived":
353
+ sendEvent("onTxReceived", event)
354
+ case "beaconDiscovered":
355
+ sendEvent("onBeaconDiscovered", event)
356
+ case "messageReceived":
357
+ sendEvent("onMessageReceived", event)
358
+ case "announceReceived":
359
+ sendEvent("onAnnounceReceived", event)
360
+ case "log":
361
+ sendEvent("onLog", event)
362
+ case "error":
363
+ sendEvent("onError", event)
364
+ default:
365
+ break
366
+ }
367
+ }
368
+ }
369
+
370
+ private func drainOutgoing() {
371
+ // --- Mesh BLE: poll for peer-addressed frames ---
372
+ var peerAddr = [UInt8](repeating: 0, count: 6)
373
+ var dataBuf = [UInt8](repeating: 0, count: 512)
374
+
375
+ for _ in 0..<8 {
376
+ let len = peerAddr.withUnsafeMutableBufferPointer { peerBuf in
377
+ dataBuf.withUnsafeMutableBufferPointer { dataBuf in
378
+ lxmf_ble_poll_tx(peerBuf.baseAddress, dataBuf.baseAddress, dataBuf.count)
379
+ }
380
+ }
381
+ guard len > 0 else { break }
382
+
383
+ let frameData = Data(dataBuf[0..<Int(len)])
384
+ let addr = Data(peerAddr)
385
+ bleManager.sendToPeerAddr(addr, data: frameData)
386
+ }
387
+
388
+ // --- NUS: poll for KISS-framed RNode data ---
389
+ var nusBuf = [UInt8](repeating: 0, count: 1024)
390
+ for _ in 0..<8 {
391
+ let len = nusBuf.withUnsafeMutableBufferPointer { buf in
392
+ lxmf_nus_poll_tx(buf.baseAddress, buf.count)
393
+ }
394
+ guard len > 0 else { break }
395
+
396
+ let kissData = Data(nusBuf[0..<Int(len)])
397
+ bleManager.sendToNus(kissData)
398
+ }
399
+ }
400
+
401
+ // MARK: - Helpers
402
+
403
+ private func callJsonFfi(_ fn_: (UnsafeMutablePointer<UInt8>?, Int) -> Int32) -> String? {
404
+ let len = jsonBuf.withUnsafeMutableBufferPointer { buf in
405
+ fn_(buf.baseAddress, buf.count)
406
+ }
407
+ guard len > 0 else { return nil }
408
+ return String(bytes: jsonBuf[0..<Int(len)], encoding: .utf8)
409
+ }
410
+
411
+ static func hexToBytes(_ hex: String) -> [UInt8]? {
412
+ let chars = Array(hex)
413
+ guard chars.count % 2 == 0 else { return nil }
414
+ var bytes = [UInt8]()
415
+ bytes.reserveCapacity(chars.count / 2)
416
+ for i in stride(from: 0, to: chars.count, by: 2) {
417
+ guard let byte = UInt8(String(chars[i...i+1]), radix: 16) else { return nil }
418
+ bytes.append(byte)
419
+ }
420
+ return bytes
421
+ }
422
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@magicred-1/react-native-lxmf",
3
+ "version": "0.1.0",
4
+ "description": "LXMF Reticulum mesh networking for React Native + Expo",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./build/index.d.ts",
10
+ "default": "./build/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "expo": {
15
+ "plugin": "./app.plugin.js"
16
+ },
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "build": "node -e \"const ts=require('typescript');const c=ts.readConfigFile('./tsconfig.json',ts.sys.readFile);const p=ts.parseJsonConfigFileContent(c.config,ts.sys,'./');const prog=ts.createProgram(p.fileNames,p.options);const r=prog.emit();const d=ts.getPreEmitDiagnostics(prog).concat(r.diagnostics);if(d.length){d.forEach(x=>console.error(ts.flattenDiagnosticMessageText(x.messageText,'\\n')));process.exit(1)}\"",
20
+ "clean": "rm -rf build",
21
+ "prepare": "npm run build",
22
+ "prepublishOnly": "npm run clean && npm run build",
23
+ "pack:check": "npm pack --dry-run",
24
+ "test": "jest",
25
+ "lint": "eslint src",
26
+ "type-check": "tsc --noEmit",
27
+ "rust:android": "bash ../scripts/build-rust-android.sh",
28
+ "rust:ios": "bash ../scripts/build-rust-ios.sh",
29
+ "build:android": "npm run rust:android && npm run build",
30
+ "build:ios": "npm run rust:ios && npm run build",
31
+ "build:all": "npm run rust:android && npm run rust:ios && npm run build"
32
+ },
33
+ "keywords": [
34
+ "lxmf",
35
+ "reticulum",
36
+ "mesh",
37
+ "networking",
38
+ "ble",
39
+ "react-native",
40
+ "expo"
41
+ ],
42
+ "homepage": "https://anonme.sh",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/anon0mesh/lxmf_react_native_rust.git"
46
+ },
47
+ "license": "MIT",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "peerDependencies": {
52
+ "expo": "*",
53
+ "expo-modules-core": "*",
54
+ "react": "*",
55
+ "react-native": "*"
56
+ },
57
+ "dependencies": {
58
+ "@expo/config-plugins": "^9.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/react": "^19.2.14",
62
+ "@types/react-native": "^0.72.0",
63
+ "expo-modules-core": "^3.0.0",
64
+ "typescript": "^5.0.0"
65
+ },
66
+ "files": [
67
+ "build",
68
+ "ios",
69
+ "android/build.gradle.kts",
70
+ "android/src/main",
71
+ "LxmfReactNative.podspec",
72
+ "expo-module.config.json",
73
+ "app.plugin.js",
74
+ "README.md"
75
+ ]
76
+ }