@magicred-1/react-native-lxmf 0.2.41 → 0.2.43
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/LxmfModule.kt +22 -0
- package/android/src/main/kotlin/expo/modules/lxmf/NusManager.kt +104 -11
- package/build/LxmfModule.d.ts +3 -0
- package/build/LxmfModule.js +3 -0
- package/build/useLxmf.d.ts +13 -1
- package/build/useLxmf.js +47 -0
- package/ios/BLEManager.swift +25 -0
- package/ios/LxmfModule.swift +49 -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
|
|
@@ -33,6 +33,10 @@ class LxmfModule : Module() {
|
|
|
33
33
|
"onMessageReceived",
|
|
34
34
|
"onAnnounceReceived",
|
|
35
35
|
"onStatusChanged",
|
|
36
|
+
"onRpcResponse",
|
|
37
|
+
"onMessageQueued",
|
|
38
|
+
"onMessageDelivered",
|
|
39
|
+
"onMessageFailed",
|
|
36
40
|
"onLog",
|
|
37
41
|
"onError",
|
|
38
42
|
"onOutgoingPacket"
|
|
@@ -127,6 +131,11 @@ class LxmfModule : Module() {
|
|
|
127
131
|
nativeFetchMessages(limit)
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
// Beacon RPC
|
|
135
|
+
AsyncFunction("beaconRpc") { destHashHex: String, method: String, paramsJson: String? ->
|
|
136
|
+
nativeBeaconRpc(destHashHex, method, paramsJson).toDouble()
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
// Configuration
|
|
131
140
|
Function("setLogLevel") { level: Int ->
|
|
132
141
|
nativeSetLogLevel(level) == 0
|
|
@@ -158,6 +167,14 @@ class LxmfModule : Module() {
|
|
|
158
167
|
Function("bleUnpairedRNodeCount") {
|
|
159
168
|
nusManager?.unpairedRNodeCount() ?: 0
|
|
160
169
|
}
|
|
170
|
+
|
|
171
|
+
Function("getNusUnpairedRNodes") {
|
|
172
|
+
nusManager?.unpairedRNodesJson() ?: "[]"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Function("pairNusRNode") { mac: String ->
|
|
176
|
+
nusManager?.pairRNode(mac) ?: false
|
|
177
|
+
}
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
private fun drainAndEmitEvents() {
|
|
@@ -183,6 +200,10 @@ class LxmfModule : Module() {
|
|
|
183
200
|
"packetReceived" -> "onPacketReceived"
|
|
184
201
|
"txReceived" -> "onTxReceived"
|
|
185
202
|
"beaconDiscovered" -> "onBeaconDiscovered"
|
|
203
|
+
"rpcResponse" -> "onRpcResponse"
|
|
204
|
+
"messageQueued" -> "onMessageQueued"
|
|
205
|
+
"messageDelivered" -> "onMessageDelivered"
|
|
206
|
+
"messageFailed" -> "onMessageFailed"
|
|
186
207
|
"log" -> "onLog"
|
|
187
208
|
"error" -> "onError"
|
|
188
209
|
else -> null
|
|
@@ -222,6 +243,7 @@ class LxmfModule : Module() {
|
|
|
222
243
|
private external fun nativeGetStatus(): String?
|
|
223
244
|
private external fun nativeGetBeacons(): String?
|
|
224
245
|
private external fun nativeFetchMessages(limit: Int): String?
|
|
246
|
+
private external fun nativeBeaconRpc(destHashHex: String, method: String, paramsJson: String?): Long
|
|
225
247
|
private external fun nativeSetLogLevel(level: Int): Int
|
|
226
248
|
private external fun nativeAbiVersion(): Int
|
|
227
249
|
|
|
@@ -2,12 +2,17 @@ package expo.modules.lxmf
|
|
|
2
2
|
|
|
3
3
|
import android.bluetooth.*
|
|
4
4
|
import android.bluetooth.le.*
|
|
5
|
+
import android.content.BroadcastReceiver
|
|
5
6
|
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.content.IntentFilter
|
|
6
9
|
import android.os.Build
|
|
7
10
|
import android.os.Handler
|
|
8
11
|
import android.os.Looper
|
|
9
12
|
import android.os.ParcelUuid
|
|
10
13
|
import android.util.Log
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import org.json.JSONObject
|
|
11
16
|
import java.util.UUID
|
|
12
17
|
|
|
13
18
|
private const val NUS_TAG = "LxmfNus"
|
|
@@ -43,8 +48,8 @@ class NusManager(
|
|
|
43
48
|
private val txChars = mutableMapOf<String, BluetoothGattCharacteristic>()
|
|
44
49
|
// Negotiated write MTU per connection — used for TX chunking
|
|
45
50
|
private val writeMtu = mutableMapOf<String, Int>()
|
|
46
|
-
//
|
|
47
|
-
private val discoveredUnpaired =
|
|
51
|
+
// Devices found in scan but not OS-paired — keyed by MAC for O(1) lookup
|
|
52
|
+
private val discoveredUnpaired = mutableMapOf<String, BluetoothDevice>()
|
|
48
53
|
// MACs currently attempting connection
|
|
49
54
|
private val connecting = mutableSetOf<String>()
|
|
50
55
|
|
|
@@ -58,17 +63,49 @@ class NusManager(
|
|
|
58
63
|
private const val DEFAULT_WRITE_MTU = 20 // conservative BLE default
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
// ── Bond state receiver ───────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
private val bondReceiver = object : BroadcastReceiver() {
|
|
69
|
+
override fun onReceive(ctx: Context, intent: Intent) {
|
|
70
|
+
if (intent.action != BluetoothDevice.ACTION_BOND_STATE_CHANGED) return
|
|
71
|
+
val device: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
72
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
73
|
+
} else {
|
|
74
|
+
@Suppress("DEPRECATION")
|
|
75
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
76
|
+
}
|
|
77
|
+
val mac = device?.address ?: return
|
|
78
|
+
val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)
|
|
79
|
+
if (bondState == BluetoothDevice.BOND_BONDED) {
|
|
80
|
+
Log.i(NUS_TAG, "NUS: $mac bonded — connecting")
|
|
81
|
+
discoveredUnpaired.remove(mac)
|
|
82
|
+
if (isRunning && mac !in connections && mac !in connecting) {
|
|
83
|
+
connecting.add(mac)
|
|
84
|
+
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
62
91
|
|
|
63
92
|
fun start() {
|
|
64
93
|
if (isRunning) return
|
|
65
94
|
isRunning = true
|
|
95
|
+
val filter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
|
96
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
97
|
+
context.registerReceiver(bondReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
98
|
+
} else {
|
|
99
|
+
@Suppress("UnspecifiedRegisterReceiverFlag")
|
|
100
|
+
context.registerReceiver(bondReceiver, filter)
|
|
101
|
+
}
|
|
66
102
|
startScanning()
|
|
67
103
|
Log.i(NUS_TAG, "NusManager started")
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
fun stop() {
|
|
71
107
|
isRunning = false
|
|
108
|
+
try { context.unregisterReceiver(bondReceiver) } catch (_: Exception) {}
|
|
72
109
|
stopScanning()
|
|
73
110
|
connections.values.forEach { it.disconnect(); it.close() }
|
|
74
111
|
connections.clear()
|
|
@@ -79,9 +116,41 @@ class NusManager(
|
|
|
79
116
|
Log.i(NUS_TAG, "NusManager stopped")
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
/** Number of RNodes visible in scan but not yet OS-paired
|
|
119
|
+
/** Number of RNodes visible in scan but not yet OS-paired. */
|
|
83
120
|
fun unpairedRNodeCount(): Int = discoveredUnpaired.size
|
|
84
121
|
|
|
122
|
+
/** JSON array of unpaired RNodes: [{"mac":"AA:BB:...","name":"RNode_1234"},...] */
|
|
123
|
+
fun unpairedRNodesJson(): String {
|
|
124
|
+
val arr = JSONArray()
|
|
125
|
+
discoveredUnpaired.values.forEach { device ->
|
|
126
|
+
arr.put(JSONObject().apply {
|
|
127
|
+
put("mac", device.address)
|
|
128
|
+
put("name", device.name ?: "")
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
return arr.toString()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initiate OS pairing with an unpaired RNode. Shows system Bluetooth pairing dialog.
|
|
136
|
+
* Returns true if bond initiation succeeded (or device is already bonded and connecting).
|
|
137
|
+
* Requires BLUETOOTH_CONNECT permission (API 31+).
|
|
138
|
+
*/
|
|
139
|
+
fun pairRNode(mac: String): Boolean {
|
|
140
|
+
val device = discoveredUnpaired[mac]
|
|
141
|
+
?: try { adapter?.getRemoteDevice(mac) } catch (_: Exception) { null }
|
|
142
|
+
?: return false
|
|
143
|
+
return if (device.bondState == BluetoothDevice.BOND_BONDED) {
|
|
144
|
+
if (mac !in connections && mac !in connecting) {
|
|
145
|
+
connecting.add(mac)
|
|
146
|
+
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
|
147
|
+
}
|
|
148
|
+
true
|
|
149
|
+
} else {
|
|
150
|
+
device.createBond()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
85
154
|
/** Number of fully connected and ready RNodes. */
|
|
86
155
|
fun connectedRNodeCount(): Int = connections.size
|
|
87
156
|
|
|
@@ -106,15 +175,27 @@ class NusManager(
|
|
|
106
175
|
data: ByteArray,
|
|
107
176
|
mtu: Int,
|
|
108
177
|
) {
|
|
178
|
+
// Android GATT is single-op — only one writeCharacteristic in flight at a time.
|
|
179
|
+
// For NUS, packets are bounded by NusInterface.mtu (244 B) so KISS frames fit in
|
|
180
|
+
// one ATT PDU after MTU negotiation (514 B). The loop is a safety net only.
|
|
109
181
|
var offset = 0
|
|
110
182
|
while (offset < data.size) {
|
|
111
183
|
val end = minOf(offset + mtu, data.size)
|
|
112
184
|
val chunk = data.copyOfRange(offset, end)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
185
|
+
val ok = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
186
|
+
gatt.writeCharacteristic(char, chunk, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) ==
|
|
187
|
+
BluetoothGatt.GATT_SUCCESS
|
|
188
|
+
} else {
|
|
189
|
+
@Suppress("DEPRECATION")
|
|
190
|
+
char.value = chunk
|
|
191
|
+
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
192
|
+
@Suppress("DEPRECATION")
|
|
193
|
+
gatt.writeCharacteristic(char)
|
|
194
|
+
}
|
|
195
|
+
if (!ok) {
|
|
196
|
+
Log.w(NUS_TAG, "NUS TX write rejected at offset $offset — ${data.size - offset}B dropped")
|
|
197
|
+
return
|
|
198
|
+
}
|
|
118
199
|
offset = end
|
|
119
200
|
}
|
|
120
201
|
}
|
|
@@ -157,9 +238,9 @@ class NusManager(
|
|
|
157
238
|
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
|
158
239
|
}
|
|
159
240
|
else -> {
|
|
160
|
-
// Not paired — track for UI
|
|
161
|
-
if (discoveredUnpaired.
|
|
162
|
-
Log.i(NUS_TAG, "NUS: found unpaired RNode $mac
|
|
241
|
+
// Not paired — track for UI; call pairRNode(mac) to initiate pairing
|
|
242
|
+
if (discoveredUnpaired.put(mac, device) == null) {
|
|
243
|
+
Log.i(NUS_TAG, "NUS: found unpaired RNode $mac (${device.name ?: "?"}) — call pairRNode()")
|
|
163
244
|
}
|
|
164
245
|
}
|
|
165
246
|
}
|
|
@@ -252,6 +333,18 @@ class NusManager(
|
|
|
252
333
|
Log.i(NUS_TAG, "NUS RNode ready: $mac (tx=${txChar != null}, rx=${rxChar != null})")
|
|
253
334
|
}
|
|
254
335
|
|
|
336
|
+
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
|
|
337
|
+
val mac = gatt.device.address
|
|
338
|
+
if (descriptor.uuid == CCCD_UUID) {
|
|
339
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
340
|
+
Log.i(NUS_TAG, "NUS RX notifications enabled: $mac — RNode ready")
|
|
341
|
+
} else {
|
|
342
|
+
Log.e(NUS_TAG, "NUS CCCD write failed ($status) on $mac — RX notifications NOT enabled, disconnecting")
|
|
343
|
+
gatt.disconnect()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
255
348
|
// API < 33 compat override
|
|
256
349
|
@Suppress("OVERRIDE_DEPRECATION")
|
|
257
350
|
override fun onCharacteristicChanged(
|
package/build/LxmfModule.d.ts
CHANGED
|
@@ -12,12 +12,15 @@ export type NativeModuleType = {
|
|
|
12
12
|
getStatus(): string | null;
|
|
13
13
|
getBeacons(): string | null;
|
|
14
14
|
fetchMessages(limit: number): string | null;
|
|
15
|
+
beaconRpc(destHashHex: string, method: string, paramsJson?: string | null): Promise<number>;
|
|
15
16
|
setLogLevel(level: number): boolean;
|
|
16
17
|
abiVersion(): number;
|
|
17
18
|
startBLE(): boolean;
|
|
18
19
|
stopBLE(): boolean;
|
|
19
20
|
blePeerCount(): number;
|
|
20
21
|
bleUnpairedRNodeCount(): number;
|
|
22
|
+
getNusUnpairedRNodes(): string;
|
|
23
|
+
pairNusRNode(mac: string): boolean;
|
|
21
24
|
};
|
|
22
25
|
declare const LxmfModuleNative: NativeModuleType | null;
|
|
23
26
|
export declare const isLxmfNativeAvailable: boolean;
|
package/build/LxmfModule.js
CHANGED
|
@@ -34,5 +34,8 @@ const missingNativeShim = {
|
|
|
34
34
|
stopBLE: () => throwMissingNative(),
|
|
35
35
|
blePeerCount: () => throwMissingNative(),
|
|
36
36
|
bleUnpairedRNodeCount: () => throwMissingNative(),
|
|
37
|
+
beaconRpc: async () => throwMissingNative(),
|
|
38
|
+
getNusUnpairedRNodes: () => throwMissingNative(),
|
|
39
|
+
pairNusRNode: () => throwMissingNative(),
|
|
37
40
|
};
|
|
38
41
|
exports.LxmfModule = LxmfModuleNative ?? missingNativeShim;
|
package/build/useLxmf.d.ts
CHANGED
|
@@ -33,8 +33,14 @@ export interface LxmfMessageEvent {
|
|
|
33
33
|
data: string;
|
|
34
34
|
}[];
|
|
35
35
|
}
|
|
36
|
+
export interface RpcResponseEvent {
|
|
37
|
+
id: number;
|
|
38
|
+
method: string;
|
|
39
|
+
resultJson: string;
|
|
40
|
+
isError: boolean;
|
|
41
|
+
}
|
|
36
42
|
export interface LxmfEvent {
|
|
37
|
-
type: 'statusChanged' | 'packetReceived' | 'txReceived' | 'beaconDiscovered' | 'messageReceived' | 'announceReceived' | 'messageQueued' | 'messageDelivered' | 'messageFailed' | 'log' | 'error';
|
|
43
|
+
type: 'statusChanged' | 'packetReceived' | 'txReceived' | 'beaconDiscovered' | 'messageReceived' | 'announceReceived' | 'messageQueued' | 'messageDelivered' | 'messageFailed' | 'log' | 'error' | 'rpcResponse';
|
|
38
44
|
[key: string]: any;
|
|
39
45
|
}
|
|
40
46
|
/** Node transport mode */
|
|
@@ -115,4 +121,10 @@ export declare function useLxmf(options?: UseLxmfOptions): {
|
|
|
115
121
|
startBLE: () => void;
|
|
116
122
|
stopBLE: () => void;
|
|
117
123
|
bleUnpairedRNodeCount: () => number;
|
|
124
|
+
getNusUnpairedRNodes: () => {
|
|
125
|
+
mac: string;
|
|
126
|
+
name: string;
|
|
127
|
+
}[];
|
|
128
|
+
pairNusRNode: (mac: string) => boolean;
|
|
129
|
+
beaconRpc: (destHashHex: string, method: string, params?: unknown) => Promise<number>;
|
|
118
130
|
};
|
package/build/useLxmf.js
CHANGED
|
@@ -109,6 +109,19 @@ function useLxmf(options = {}) {
|
|
|
109
109
|
pushEvent('error', event);
|
|
110
110
|
setError(`${String(event.code)}: ${String(event.message)}`);
|
|
111
111
|
}),
|
|
112
|
+
mod.addListener('onRpcResponse', (event) => {
|
|
113
|
+
pushEvent('rpcResponse', event);
|
|
114
|
+
}),
|
|
115
|
+
mod.addListener('onMessageQueued', (event) => {
|
|
116
|
+
pushEvent('messageQueued', event);
|
|
117
|
+
}),
|
|
118
|
+
mod.addListener('onMessageDelivered', (event) => {
|
|
119
|
+
pushEvent('messageDelivered', event);
|
|
120
|
+
}),
|
|
121
|
+
mod.addListener('onMessageFailed', (event) => {
|
|
122
|
+
pushEvent('messageFailed', event);
|
|
123
|
+
setError(`Message ${String(event.seq)} failed: ${String(event.reason ?? 'unknown')}`);
|
|
124
|
+
}),
|
|
112
125
|
];
|
|
113
126
|
return () => {
|
|
114
127
|
subscriptions.forEach((sub) => sub.remove());
|
|
@@ -253,6 +266,37 @@ function useLxmf(options = {}) {
|
|
|
253
266
|
const bleUnpairedRNodeCount = (0, react_1.useCallback)(() => {
|
|
254
267
|
return LxmfModule_1.LxmfModule.bleUnpairedRNodeCount();
|
|
255
268
|
}, []);
|
|
269
|
+
/** List of RNodes visible in scan but not yet OS-paired. */
|
|
270
|
+
const getNusUnpairedRNodes = (0, react_1.useCallback)(() => {
|
|
271
|
+
try {
|
|
272
|
+
return parseJson(LxmfModule_1.LxmfModule.getNusUnpairedRNodes(), []);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}, []);
|
|
278
|
+
/**
|
|
279
|
+
* Initiate OS Bluetooth pairing with an unpaired RNode (mac = "AA:BB:CC:DD:EE:FF").
|
|
280
|
+
* Shows system pairing dialog. Auto-connects on bond completion via bondReceiver.
|
|
281
|
+
*/
|
|
282
|
+
const pairNusRNode = (0, react_1.useCallback)((mac) => {
|
|
283
|
+
return LxmfModule_1.LxmfModule.pairNusRNode(mac);
|
|
284
|
+
}, []);
|
|
285
|
+
/**
|
|
286
|
+
* Queue a JSON-RPC 2.0 call to a beacon.
|
|
287
|
+
* Returns correlation id; the response arrives as an `rpcResponse` event.
|
|
288
|
+
* `params` is any JSON-serializable value (usually an array).
|
|
289
|
+
*/
|
|
290
|
+
const beaconRpc = (0, react_1.useCallback)(async (destHashHex, method, params) => {
|
|
291
|
+
try {
|
|
292
|
+
const paramsJson = params === undefined ? null : JSON.stringify(params);
|
|
293
|
+
return await LxmfModule_1.LxmfModule.beaconRpc(destHashHex, method, paramsJson);
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
setError(e?.message ?? 'beaconRpc failed');
|
|
297
|
+
return -1;
|
|
298
|
+
}
|
|
299
|
+
}, []);
|
|
256
300
|
return {
|
|
257
301
|
// State
|
|
258
302
|
status,
|
|
@@ -274,5 +318,8 @@ function useLxmf(options = {}) {
|
|
|
274
318
|
startBLE,
|
|
275
319
|
stopBLE,
|
|
276
320
|
bleUnpairedRNodeCount,
|
|
321
|
+
getNusUnpairedRNodes,
|
|
322
|
+
pairNusRNode,
|
|
323
|
+
beaconRpc,
|
|
277
324
|
};
|
|
278
325
|
}
|
package/ios/BLEManager.swift
CHANGED
|
@@ -235,6 +235,31 @@ class BLEManager: NSObject {
|
|
|
235
235
|
return !nusPeripherals.isEmpty
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/// JSON array of discovered-but-not-yet-bonded RNodes.
|
|
239
|
+
/// Uses CoreBluetooth UUID as "mac" (iOS hides real MACs since iOS 13).
|
|
240
|
+
func unpairedRNodesJson() -> String {
|
|
241
|
+
var entries: [[String: String]] = []
|
|
242
|
+
for (uuid, peripheral) in discoveredUnpairedRNodes {
|
|
243
|
+
entries.append(["mac": uuid.uuidString, "name": peripheral.name ?? ""])
|
|
244
|
+
}
|
|
245
|
+
guard let data = try? JSONSerialization.data(withJSONObject: entries),
|
|
246
|
+
let str = String(data: data, encoding: .utf8) else { return "[]" }
|
|
247
|
+
return str
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Initiate connection to a discovered-but-unpaired RNode by its CoreBluetooth UUID string.
|
|
251
|
+
/// CoreBluetooth handles pairing transparently when the peripheral requires encryption.
|
|
252
|
+
/// Returns false if the UUID is unknown (device not seen in scan yet).
|
|
253
|
+
func connectRNode(_ identifierString: String) -> Bool {
|
|
254
|
+
guard let uuid = UUID(uuidString: identifierString),
|
|
255
|
+
let peripheral = discoveredUnpairedRNodes[uuid] else { return false }
|
|
256
|
+
guard connectedPeripherals[uuid] == nil else { return true }
|
|
257
|
+
connectedPeripherals[uuid] = peripheral
|
|
258
|
+
peripheral.delegate = self
|
|
259
|
+
centralManager?.connect(peripheral, options: nil)
|
|
260
|
+
return true
|
|
261
|
+
}
|
|
262
|
+
|
|
238
263
|
/// Derive a 6-byte pseudo-MAC from a CoreBluetooth UUID.
|
|
239
264
|
/// XOR-folds the 16-byte UUID into 6 bytes for stable peer identification.
|
|
240
265
|
static func uuidToAddr(_ uuid: UUID) -> Data {
|
package/ios/LxmfModule.swift
CHANGED
|
@@ -134,6 +134,15 @@ func lxmf_nus_poll_tx(
|
|
|
134
134
|
_ outCapacity: Int
|
|
135
135
|
) -> Int32
|
|
136
136
|
|
|
137
|
+
// --- Beacon RPC FFI ---
|
|
138
|
+
|
|
139
|
+
@_silgen_name("lxmf_beacon_rpc")
|
|
140
|
+
func lxmf_beacon_rpc(
|
|
141
|
+
_ destHashHex: UnsafePointer<CChar>?,
|
|
142
|
+
_ method: UnsafePointer<CChar>?,
|
|
143
|
+
_ paramsJson: UnsafePointer<CChar>?
|
|
144
|
+
) -> Int64
|
|
145
|
+
|
|
137
146
|
|
|
138
147
|
public class LxmfModule: Module {
|
|
139
148
|
// Shared JSON buffer for FFI calls (64KB)
|
|
@@ -161,6 +170,10 @@ public class LxmfModule: Module {
|
|
|
161
170
|
"onMessageReceived",
|
|
162
171
|
"onAnnounceReceived",
|
|
163
172
|
"onStatusChanged",
|
|
173
|
+
"onRpcResponse",
|
|
174
|
+
"onMessageQueued",
|
|
175
|
+
"onMessageDelivered",
|
|
176
|
+
"onMessageFailed",
|
|
164
177
|
"onLog",
|
|
165
178
|
"onError",
|
|
166
179
|
"onOutgoingPacket"
|
|
@@ -340,6 +353,34 @@ public class LxmfModule: Module {
|
|
|
340
353
|
Function("bleUnpairedRNodeCount") { () -> Int in
|
|
341
354
|
return self.bleManager.discoveredUnpairedRNodes.count
|
|
342
355
|
}
|
|
356
|
+
|
|
357
|
+
// --- Beacon RPC ---
|
|
358
|
+
|
|
359
|
+
AsyncFunction("beaconRpc") { (destHashHex: String, method: String, paramsJson: String?) -> Double in
|
|
360
|
+
let id = destHashHex.withCString { destPtr in
|
|
361
|
+
method.withCString { methodPtr in
|
|
362
|
+
if let p = paramsJson {
|
|
363
|
+
return p.withCString { paramsPtr in
|
|
364
|
+
lxmf_beacon_rpc(destPtr, methodPtr, paramsPtr)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return lxmf_beacon_rpc(destPtr, methodPtr, nil)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return Double(id)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// --- RNode pairing (NUS) ---
|
|
374
|
+
|
|
375
|
+
Function("getNusUnpairedRNodes") { () -> String in
|
|
376
|
+
return self.bleManager.unpairedRNodesJson()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// On iOS, "pairing" = connect (CoreBluetooth handles encryption/bonding transparently).
|
|
380
|
+
// The identifier is a CoreBluetooth UUID string, not a MAC (iOS hides MACs since iOS 13).
|
|
381
|
+
Function("pairNusRNode") { (identifier: String) -> Bool in
|
|
382
|
+
return self.bleManager.connectRNode(identifier)
|
|
383
|
+
}
|
|
343
384
|
}
|
|
344
385
|
|
|
345
386
|
// MARK: - Polling
|
|
@@ -397,6 +438,14 @@ public class LxmfModule: Module {
|
|
|
397
438
|
sendEvent("onMessageReceived", event)
|
|
398
439
|
case "announceReceived":
|
|
399
440
|
sendEvent("onAnnounceReceived", event)
|
|
441
|
+
case "rpcResponse":
|
|
442
|
+
sendEvent("onRpcResponse", event)
|
|
443
|
+
case "messageQueued":
|
|
444
|
+
sendEvent("onMessageQueued", event)
|
|
445
|
+
case "messageDelivered":
|
|
446
|
+
sendEvent("onMessageDelivered", event)
|
|
447
|
+
case "messageFailed":
|
|
448
|
+
sendEvent("onMessageFailed", event)
|
|
400
449
|
case "log":
|
|
401
450
|
sendEvent("onLog", event)
|
|
402
451
|
case "error":
|
|
Binary file
|
|
Binary file
|