@magicred-1/react-native-lxmf 0.2.0 → 0.2.2
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 +123 -54
- package/android/src/main/kotlin/expo/modules/lxmf/LxmfModule.kt +1 -0
- package/example/README.md +50 -0
- package/example/app/(tabs)/_layout.tsx +28 -0
- package/example/app/(tabs)/index.tsx +874 -0
- package/example/app/_layout.tsx +14 -0
- package/example/app/modal.tsx +29 -0
- package/example/app.json +72 -0
- package/example/assets/images/android-icon-background.png +0 -0
- package/example/assets/images/android-icon-foreground.png +0 -0
- package/example/assets/images/android-icon-monochrome.png +0 -0
- package/example/assets/images/favicon.png +0 -0
- package/example/assets/images/icon.png +0 -0
- package/example/assets/images/partial-react-logo.png +0 -0
- package/example/assets/images/react-logo.png +0 -0
- package/example/assets/images/react-logo@2x.png +0 -0
- package/example/assets/images/react-logo@3x.png +0 -0
- package/example/assets/images/splash-icon.png +0 -0
- package/example/components/external-link.tsx +25 -0
- package/example/components/haptic-tab.tsx +18 -0
- package/example/components/hello-wave.tsx +19 -0
- package/example/components/parallax-scroll-view.tsx +79 -0
- package/example/components/themed-text.tsx +60 -0
- package/example/components/themed-view.tsx +14 -0
- package/example/components/ui/collapsible.tsx +45 -0
- package/example/components/ui/icon-symbol.ios.tsx +32 -0
- package/example/components/ui/icon-symbol.tsx +41 -0
- package/example/constants/theme.ts +53 -0
- package/example/eslint.config.js +10 -0
- package/example/expo-env.d.ts +3 -0
- package/example/hooks/use-color-scheme.ts +1 -0
- package/example/hooks/use-color-scheme.web.ts +21 -0
- package/example/hooks/use-theme-color.ts +21 -0
- package/example/metro.config.js +37 -0
- package/example/package.json +52 -0
- package/example/scripts/reset-project.js +112 -0
- package/example/tsconfig.json +17 -0
- package/ios/BLEManager.swift +41 -6
- package/ios/LxmfModule.swift +13 -2
- package/package.json +15 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -53,6 +53,7 @@ class BleManager(
|
|
|
53
53
|
private var advertiser: BluetoothLeAdvertiser? = null
|
|
54
54
|
private var isScanning = false
|
|
55
55
|
private var isAdvertising = false
|
|
56
|
+
private var isRunning = false
|
|
56
57
|
|
|
57
58
|
// GATT server (peripheral role) — accepts inbound writes from remote centrals
|
|
58
59
|
// and pushes outbound notifications to subscribed centrals.
|
|
@@ -61,6 +62,8 @@ class BleManager(
|
|
|
61
62
|
// Centrals that have enabled CCC notifications on our TX char, keyed by MAC.
|
|
62
63
|
// Only these are "registered as peers" with Rust (mirrors iOS subscribedCentrals).
|
|
63
64
|
private val serverSubscribers = mutableMapOf<String, BluetoothDevice>()
|
|
65
|
+
// Buffer for ATT Long Write (preparedWrite=true) fragments from remote centrals.
|
|
66
|
+
private val preparedWriteBuffer = mutableMapOf<String, java.io.ByteArrayOutputStream>()
|
|
64
67
|
|
|
65
68
|
// TX polling — every 50 ms drain the Rust TX queue and write to peers
|
|
66
69
|
private val txPollRunnable = object : Runnable {
|
|
@@ -80,20 +83,29 @@ class BleManager(
|
|
|
80
83
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
81
84
|
|
|
82
85
|
fun start() {
|
|
86
|
+
if (isRunning) {
|
|
87
|
+
Log.d(TAG, "BleManager already running — skipping duplicate start()")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
83
90
|
if (adapter == null || !adapter!!.isEnabled) {
|
|
84
91
|
Log.w(TAG, "Bluetooth not available or not enabled")
|
|
85
92
|
return
|
|
86
93
|
}
|
|
94
|
+
isRunning = true
|
|
87
95
|
// GATT server must be opened (and service added) before advertising,
|
|
88
96
|
// otherwise centrals connect and find no service. startAdvertising()
|
|
89
97
|
// is invoked from onServiceAdded once the service is registered.
|
|
90
98
|
openGattServer()
|
|
91
99
|
startScanning()
|
|
100
|
+
// Remove any stale callbacks before scheduling — guards against duplicate
|
|
101
|
+
// poll loops if start() is somehow called more than once.
|
|
102
|
+
mainHandler.removeCallbacks(txPollRunnable)
|
|
92
103
|
mainHandler.postDelayed(txPollRunnable, TX_POLL_INTERVAL_MS)
|
|
93
104
|
Log.i(TAG, "BleManager started")
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
fun stop() {
|
|
108
|
+
isRunning = false
|
|
97
109
|
stopScanning()
|
|
98
110
|
stopAdvertising()
|
|
99
111
|
closeGattServer()
|
|
@@ -258,14 +270,36 @@ class BleManager(
|
|
|
258
270
|
value: ByteArray?,
|
|
259
271
|
) {
|
|
260
272
|
if (characteristic.uuid == RNS_RX_CHAR_UUID && value != null && value.isNotEmpty()) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
273
|
+
if (preparedWrite) {
|
|
274
|
+
// ATT Long Write fragment — buffer until onExecuteWrite.
|
|
275
|
+
preparedWriteBuffer.getOrPut(device.address) { java.io.ByteArrayOutputStream() }
|
|
276
|
+
.write(value)
|
|
277
|
+
} else {
|
|
278
|
+
Log.d(TAG, "GATT server RX ${value.size}B from ${device.address}")
|
|
279
|
+
module.nativeBleReceive(macToBytes(device.address), value)
|
|
280
|
+
}
|
|
265
281
|
}
|
|
266
282
|
if (responseNeeded) {
|
|
267
|
-
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
|
|
283
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
override fun onExecuteWrite(device: BluetoothDevice, requestId: Int, execute: Boolean) {
|
|
288
|
+
if (execute) {
|
|
289
|
+
preparedWriteBuffer.remove(device.address)?.let { buf ->
|
|
290
|
+
val data = buf.toByteArray()
|
|
291
|
+
Log.d(TAG, "GATT server RX (assembled) ${data.size}B from ${device.address}")
|
|
292
|
+
module.nativeBleReceive(macToBytes(device.address), data)
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
preparedWriteBuffer.remove(device.address)
|
|
268
296
|
}
|
|
297
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
override fun onMtuChanged(device: BluetoothDevice, mtu: Int) {
|
|
301
|
+
Log.i(TAG, "GATT server MTU changed: ${device.address} -> ${mtu}B")
|
|
302
|
+
module.nativeOnMtuNegotiated(macToBytes(device.address), mtu)
|
|
269
303
|
}
|
|
270
304
|
|
|
271
305
|
override fun onDescriptorWriteRequest(
|
|
@@ -407,37 +441,71 @@ class BleManager(
|
|
|
407
441
|
gatt.disconnect()
|
|
408
442
|
return
|
|
409
443
|
}
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
444
|
+
// Request large MTU before enabling notifications so writes fit in one ATT PDU.
|
|
445
|
+
Log.i(TAG, "BLE services discovered: ${gatt.device.address}, requesting MTU")
|
|
446
|
+
gatt.requestMtu(517)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
450
|
+
val mac = gatt.device.address
|
|
451
|
+
val effectiveMtu = if (status == BluetoothGatt.GATT_SUCCESS) mtu else 23
|
|
452
|
+
Log.i(TAG, "BLE MTU negotiated: $mac -> ${effectiveMtu}B")
|
|
453
|
+
module.nativeOnMtuNegotiated(macToBytes(mac), effectiveMtu)
|
|
454
|
+
// Subscribe to peer's TX notifications; nativeBleConnected fires in onDescriptorWrite
|
|
455
|
+
// after the CCCD write completes — writing before then returns ok=false.
|
|
456
|
+
val service = gatt.getService(RNS_SERVICE_UUID) ?: return
|
|
457
|
+
val peerTxChar = service.getCharacteristic(RNS_TX_CHAR_UUID) ?: return
|
|
458
|
+
gatt.setCharacteristicNotification(peerTxChar, true)
|
|
459
|
+
val cccd = peerTxChar.getDescriptor(CCCD_UUID) ?: return
|
|
460
|
+
@Suppress("DEPRECATION")
|
|
461
|
+
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
462
|
+
@Suppress("DEPRECATION")
|
|
463
|
+
gatt.writeDescriptor(cccd)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
|
|
467
|
+
val mac = gatt.device.address
|
|
468
|
+
if (descriptor.uuid == CCCD_UUID && descriptor.characteristic?.uuid == RNS_TX_CHAR_UUID) {
|
|
469
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
470
|
+
// GATT setup complete — subscribed to peer TX notifications, safe to write now.
|
|
471
|
+
Log.i(TAG, "BLE peer ready (client): $mac")
|
|
472
|
+
module.nativeBleConnected(macToBytes(mac))
|
|
473
|
+
} else {
|
|
474
|
+
// CCCD write failed — can't subscribe to their notifications from client role,
|
|
475
|
+
// but if they already connected to our server and subscribed, the server-side
|
|
476
|
+
// path handles full duplex. Don't disconnect — keep the connection open.
|
|
477
|
+
Log.w(TAG, "BLE CCCD write failed ($status) on $mac — continuing via server role if available")
|
|
422
478
|
}
|
|
423
479
|
}
|
|
424
|
-
// Notify Rust of new peer
|
|
425
|
-
module.nativeBleConnected(macToBytes(gatt.device.address))
|
|
426
|
-
Log.i(TAG, "BLE peer ready: ${gatt.device.address}")
|
|
427
480
|
}
|
|
428
481
|
|
|
482
|
+
// API 33+: platform delivers value directly — characteristic.value is null here.
|
|
483
|
+
@Suppress("OVERRIDE_DEPRECATION")
|
|
429
484
|
override fun onCharacteristicChanged(
|
|
430
485
|
gatt: BluetoothGatt,
|
|
431
486
|
characteristic: BluetoothGattCharacteristic,
|
|
432
487
|
) {
|
|
488
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return
|
|
433
489
|
if (characteristic.uuid == RNS_TX_CHAR_UUID) {
|
|
434
490
|
@Suppress("DEPRECATION")
|
|
435
491
|
val data = characteristic.value ?: return
|
|
436
|
-
Log.d(TAG, "BLE RX ${data.size}B from ${gatt.device.address}")
|
|
492
|
+
Log.d(TAG, "BLE RX(compat) ${data.size}B from ${gatt.device.address}")
|
|
437
493
|
module.nativeBleReceive(macToBytes(gatt.device.address), data)
|
|
438
494
|
}
|
|
439
495
|
}
|
|
440
496
|
|
|
497
|
+
@androidx.annotation.RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
498
|
+
override fun onCharacteristicChanged(
|
|
499
|
+
gatt: BluetoothGatt,
|
|
500
|
+
characteristic: BluetoothGattCharacteristic,
|
|
501
|
+
value: ByteArray,
|
|
502
|
+
) {
|
|
503
|
+
if (characteristic.uuid == RNS_TX_CHAR_UUID) {
|
|
504
|
+
Log.d(TAG, "BLE RX ${value.size}B from ${gatt.device.address}")
|
|
505
|
+
module.nativeBleReceive(macToBytes(gatt.device.address), value)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
441
509
|
override fun onCharacteristicWrite(
|
|
442
510
|
gatt: BluetoothGatt,
|
|
443
511
|
characteristic: BluetoothGattCharacteristic,
|
|
@@ -451,41 +519,42 @@ class BleManager(
|
|
|
451
519
|
|
|
452
520
|
// ── TX drain — poll Rust and write to peer characteristics ───────────────
|
|
453
521
|
|
|
522
|
+
// Android GATT is single-op: concurrent writeCharacteristic() calls return
|
|
523
|
+
// false and drop the packet. Send one frame per 50ms poll tick instead of
|
|
524
|
+
// draining all pending frames at once.
|
|
454
525
|
private fun drainTxQueue() {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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.
|
|
477
|
-
val gatt = connections[mac] ?: continue
|
|
478
|
-
val service = gatt.getService(RNS_SERVICE_UUID) ?: continue
|
|
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)
|
|
485
|
-
Log.d(TAG, "BLE TX ${data.size}B to $mac ok=$ok")
|
|
486
|
-
} catch (e: Exception) {
|
|
487
|
-
Log.e(TAG, "drainTxQueue parse error: ${e.message}")
|
|
526
|
+
val json = module.nativeBlePollTx() ?: return
|
|
527
|
+
try {
|
|
528
|
+
val obj = org.json.JSONObject(json)
|
|
529
|
+
val peerHex = obj.getString("peer") // "aabbccddeeff"
|
|
530
|
+
val dataB64 = obj.getString("data")
|
|
531
|
+
val data = android.util.Base64.decode(dataB64, android.util.Base64.DEFAULT)
|
|
532
|
+
val mac = hexToMacString(peerHex) // "AA:BB:CC:DD:EE:FF"
|
|
533
|
+
|
|
534
|
+
// Peripheral (server) role: peer subscribed to our TX char →
|
|
535
|
+
// push via NOTIFY. Mirrors iOS peripheralManager.updateValue.
|
|
536
|
+
val subscriber = serverSubscribers[mac]
|
|
537
|
+
val txChar = serverTxChar
|
|
538
|
+
if (subscriber != null && txChar != null) {
|
|
539
|
+
val ok = notifyServerSubscriber(subscriber, txChar, data)
|
|
540
|
+
Log.d(TAG, "BLE NOTIFY ${data.size}B to $mac ok=$ok")
|
|
541
|
+
return
|
|
488
542
|
}
|
|
543
|
+
|
|
544
|
+
// Central (client) role: write to peer's RX characteristic.
|
|
545
|
+
// RX has WRITE properties; TX is notify-only (writes there
|
|
546
|
+
// would be rejected by the peer). Matches iOS convention.
|
|
547
|
+
val gatt = connections[mac] ?: return
|
|
548
|
+
val service = gatt.getService(RNS_SERVICE_UUID) ?: return
|
|
549
|
+
val peerRxChar = service.getCharacteristic(RNS_RX_CHAR_UUID) ?: return
|
|
550
|
+
@Suppress("DEPRECATION")
|
|
551
|
+
peerRxChar.value = data
|
|
552
|
+
peerRxChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
553
|
+
@Suppress("DEPRECATION")
|
|
554
|
+
val ok = gatt.writeCharacteristic(peerRxChar)
|
|
555
|
+
Log.d(TAG, "BLE TX ${data.size}B to $mac ok=$ok")
|
|
556
|
+
} catch (e: Exception) {
|
|
557
|
+
Log.e(TAG, "drainTxQueue parse error: ${e.message}")
|
|
489
558
|
}
|
|
490
559
|
}
|
|
491
560
|
|
|
@@ -217,6 +217,7 @@ class LxmfModule : Module() {
|
|
|
217
217
|
external fun nativeBleConnected(peerAddr: ByteArray)
|
|
218
218
|
external fun nativeBleDisconnected(peerAddr: ByteArray)
|
|
219
219
|
external fun nativeBlePeerCount(): Int
|
|
220
|
+
external fun nativeOnMtuNegotiated(peerAddr: ByteArray, attMtu: Int)
|
|
220
221
|
|
|
221
222
|
companion object {
|
|
222
223
|
@Volatile
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Welcome to your Expo app 👋
|
|
2
|
+
|
|
3
|
+
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
|
4
|
+
|
|
5
|
+
## Get started
|
|
6
|
+
|
|
7
|
+
1. Install dependencies
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Start the app
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx expo start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
In the output, you'll find options to open the app in a
|
|
20
|
+
|
|
21
|
+
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
|
22
|
+
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
|
23
|
+
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
|
24
|
+
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
|
25
|
+
|
|
26
|
+
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
|
27
|
+
|
|
28
|
+
## Get a fresh project
|
|
29
|
+
|
|
30
|
+
When you're ready, run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run reset-project
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
|
37
|
+
|
|
38
|
+
## Learn more
|
|
39
|
+
|
|
40
|
+
To learn more about developing your project with Expo, look at the following resources:
|
|
41
|
+
|
|
42
|
+
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
|
43
|
+
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
|
44
|
+
|
|
45
|
+
## Join the community
|
|
46
|
+
|
|
47
|
+
Join our community of developers creating universal apps.
|
|
48
|
+
|
|
49
|
+
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
|
50
|
+
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Tabs } from 'expo-router';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { HapticTab } from '@/components/haptic-tab';
|
|
5
|
+
import { IconSymbol } from '@/components/ui/icon-symbol';
|
|
6
|
+
import { Colors } from '@/constants/theme';
|
|
7
|
+
import { useColorScheme } from '@/hooks/use-color-scheme';
|
|
8
|
+
|
|
9
|
+
export default function TabLayout() {
|
|
10
|
+
const colorScheme = useColorScheme();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Tabs
|
|
14
|
+
screenOptions={{
|
|
15
|
+
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
|
16
|
+
headerShown: false,
|
|
17
|
+
tabBarButton: HapticTab,
|
|
18
|
+
}}>
|
|
19
|
+
<Tabs.Screen
|
|
20
|
+
name="index"
|
|
21
|
+
options={{
|
|
22
|
+
title: 'Home',
|
|
23
|
+
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
</Tabs>
|
|
27
|
+
);
|
|
28
|
+
}
|