@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-app",
|
|
3
|
+
"main": "expo-router/entry",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "expo start",
|
|
7
|
+
"reset-project": "node ./scripts/reset-project.js",
|
|
8
|
+
"android": "expo run:android",
|
|
9
|
+
"ios": "expo run:ios",
|
|
10
|
+
"web": "expo start --web",
|
|
11
|
+
"lint": "expo lint"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@expo/vector-icons": "^15.0.3",
|
|
15
|
+
"@magicred-1/react-native-lxmf": "file:..",
|
|
16
|
+
"@react-native/virtualized-lists": "0.81.5",
|
|
17
|
+
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
18
|
+
"@react-navigation/elements": "^2.6.3",
|
|
19
|
+
"@react-navigation/native": "^7.1.8",
|
|
20
|
+
"expo": "~54.0.33",
|
|
21
|
+
"expo-constants": "~18.0.13",
|
|
22
|
+
"expo-dev-client": "~6.0.20",
|
|
23
|
+
"expo-font": "~14.0.11",
|
|
24
|
+
"expo-haptics": "~15.0.8",
|
|
25
|
+
"expo-image": "~3.0.11",
|
|
26
|
+
"expo-linking": "~8.0.11",
|
|
27
|
+
"expo-router": "~6.0.23",
|
|
28
|
+
"expo-secure-store": "~15.0.7",
|
|
29
|
+
"expo-splash-screen": "~31.0.13",
|
|
30
|
+
"expo-status-bar": "~3.0.9",
|
|
31
|
+
"expo-symbols": "~1.0.8",
|
|
32
|
+
"expo-system-ui": "~6.0.9",
|
|
33
|
+
"expo-web-browser": "~15.0.10",
|
|
34
|
+
"react": "19.1.0",
|
|
35
|
+
"react-dom": "19.1.0",
|
|
36
|
+
"react-native": "0.81.5",
|
|
37
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
38
|
+
"react-native-reanimated": "~4.1.1",
|
|
39
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
40
|
+
"react-native-screens": "~4.16.0",
|
|
41
|
+
"react-native-web": "~0.21.0",
|
|
42
|
+
"react-native-worklets": "0.5.1",
|
|
43
|
+
"semver": "^7.7.4"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/react": "~19.1.0",
|
|
47
|
+
"eslint": "^9.25.0",
|
|
48
|
+
"eslint-config-expo": "~10.0.0",
|
|
49
|
+
"typescript": "~5.9.2"
|
|
50
|
+
},
|
|
51
|
+
"private": true
|
|
52
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This script is used to reset the project to a blank state.
|
|
5
|
+
* It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
|
|
6
|
+
* You can remove the `reset-project` script from package.json and safely delete this file after running it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const readline = require("readline");
|
|
12
|
+
|
|
13
|
+
const root = process.cwd();
|
|
14
|
+
const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
|
|
15
|
+
const exampleDir = "app-example";
|
|
16
|
+
const newAppDir = "app";
|
|
17
|
+
const exampleDirPath = path.join(root, exampleDir);
|
|
18
|
+
|
|
19
|
+
const indexContent = `import { Text, View } from "react-native";
|
|
20
|
+
|
|
21
|
+
export default function Index() {
|
|
22
|
+
return (
|
|
23
|
+
<View
|
|
24
|
+
style={{
|
|
25
|
+
flex: 1,
|
|
26
|
+
justifyContent: "center",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<Text>Edit app/index.tsx to edit this screen.</Text>
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const layoutContent = `import { Stack } from "expo-router";
|
|
37
|
+
|
|
38
|
+
export default function RootLayout() {
|
|
39
|
+
return <Stack />;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const moveDirectories = async (userInput) => {
|
|
49
|
+
try {
|
|
50
|
+
if (userInput === "y") {
|
|
51
|
+
// Create the app-example directory
|
|
52
|
+
await fs.promises.mkdir(exampleDirPath, { recursive: true });
|
|
53
|
+
console.log(`📁 /${exampleDir} directory created.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Move old directories to new app-example directory or delete them
|
|
57
|
+
for (const dir of oldDirs) {
|
|
58
|
+
const oldDirPath = path.join(root, dir);
|
|
59
|
+
if (fs.existsSync(oldDirPath)) {
|
|
60
|
+
if (userInput === "y") {
|
|
61
|
+
const newDirPath = path.join(root, exampleDir, dir);
|
|
62
|
+
await fs.promises.rename(oldDirPath, newDirPath);
|
|
63
|
+
console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`);
|
|
64
|
+
} else {
|
|
65
|
+
await fs.promises.rm(oldDirPath, { recursive: true, force: true });
|
|
66
|
+
console.log(`❌ /${dir} deleted.`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`➡️ /${dir} does not exist, skipping.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create new /app directory
|
|
74
|
+
const newAppDirPath = path.join(root, newAppDir);
|
|
75
|
+
await fs.promises.mkdir(newAppDirPath, { recursive: true });
|
|
76
|
+
console.log("\n📁 New /app directory created.");
|
|
77
|
+
|
|
78
|
+
// Create index.tsx
|
|
79
|
+
const indexPath = path.join(newAppDirPath, "index.tsx");
|
|
80
|
+
await fs.promises.writeFile(indexPath, indexContent);
|
|
81
|
+
console.log("📄 app/index.tsx created.");
|
|
82
|
+
|
|
83
|
+
// Create _layout.tsx
|
|
84
|
+
const layoutPath = path.join(newAppDirPath, "_layout.tsx");
|
|
85
|
+
await fs.promises.writeFile(layoutPath, layoutContent);
|
|
86
|
+
console.log("📄 app/_layout.tsx created.");
|
|
87
|
+
|
|
88
|
+
console.log("\n✅ Project reset complete. Next steps:");
|
|
89
|
+
console.log(
|
|
90
|
+
`1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
|
|
91
|
+
userInput === "y"
|
|
92
|
+
? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
|
|
93
|
+
: ""
|
|
94
|
+
}`
|
|
95
|
+
);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`❌ Error during script execution: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
rl.question(
|
|
102
|
+
"Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
|
|
103
|
+
(answer) => {
|
|
104
|
+
const userInput = answer.trim().toLowerCase() || "y";
|
|
105
|
+
if (userInput === "y" || userInput === "n") {
|
|
106
|
+
moveDirectories(userInput).finally(() => rl.close());
|
|
107
|
+
} else {
|
|
108
|
+
console.log("❌ Invalid input. Please enter 'Y' or 'N'.");
|
|
109
|
+
rl.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
package/ios/BLEManager.swift
CHANGED
|
@@ -53,6 +53,10 @@ class BLEManager: NSObject {
|
|
|
53
53
|
|
|
54
54
|
private var isRunning = false
|
|
55
55
|
|
|
56
|
+
/// Called when CoreBluetooth signals it is ready to accept more writes.
|
|
57
|
+
/// LxmfModule sets this to re-trigger drainOutgoing() without a timer.
|
|
58
|
+
var onReadyToSend: (() -> Void)?
|
|
59
|
+
|
|
56
60
|
/// Per-launch random token embedded in our advertisement's local name, so
|
|
57
61
|
/// our central role can detect and skip our own peripheral advertisement
|
|
58
62
|
/// (CoreBluetooth does not auto-filter self when running both roles).
|
|
@@ -176,20 +180,30 @@ class BLEManager: NSObject {
|
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
/// Send data to a specific peer by 6-byte pseudo-MAC address.
|
|
179
|
-
///
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
/// Returns false when CoreBluetooth's TX buffer is full — caller should stop
|
|
184
|
+
/// draining and wait for onReadyToSend before resuming.
|
|
185
|
+
@discardableResult
|
|
186
|
+
func sendToPeerAddr(_ addr: Data, data: Data) -> Bool {
|
|
187
|
+
// Peripheral role: push notification to subscribed central.
|
|
188
|
+
// updateValue returns false when the subscriber's buffer is full;
|
|
189
|
+
// peripheralManagerIsReady(toUpdateSubscribers:) fires when it drains.
|
|
182
190
|
if let central = addrToCentral[addr], let txChar = txCharacteristic {
|
|
183
|
-
peripheralManager?.updateValue(data, for: txChar, onSubscribedCentrals: [central])
|
|
184
|
-
return
|
|
191
|
+
let ok = peripheralManager?.updateValue(data, for: txChar, onSubscribedCentrals: [central]) ?? false
|
|
192
|
+
return ok
|
|
185
193
|
}
|
|
186
194
|
|
|
187
|
-
//
|
|
195
|
+
// Central role: write to peer's RX characteristic.
|
|
196
|
+
// canSendWriteWithoutResponse goes false when the internal queue is full;
|
|
197
|
+
// peripheral(_:isReadyToSendWriteWithoutResponse:) fires when it drains.
|
|
188
198
|
if let peripheralUUID = addrToPeripheralUUID[addr],
|
|
189
199
|
let peripheral = connectedPeripherals[peripheralUUID],
|
|
190
200
|
let char = txCharacteristics[peripheralUUID] {
|
|
201
|
+
guard peripheral.canSendWriteWithoutResponse else { return false }
|
|
191
202
|
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
203
|
+
return true
|
|
192
204
|
}
|
|
205
|
+
|
|
206
|
+
return true // peer not found — frame consumed, no retry needed
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
/// Write KISS-framed data to all connected RNodes via NUS TX characteristic.
|
|
@@ -427,6 +441,11 @@ extension BLEManager: CBPeripheralDelegate {
|
|
|
427
441
|
addr.withUnsafeBytes { ptr in
|
|
428
442
|
_ = lxmf_ble_connected(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self))
|
|
429
443
|
}
|
|
444
|
+
// Report negotiated write limit so Rust segments correctly for this peer.
|
|
445
|
+
let writeLimit = peripheral.maximumWriteValueLength(for: .withoutResponse)
|
|
446
|
+
addr.withUnsafeBytes { ptr in
|
|
447
|
+
_ = lxmf_ble_mtu_negotiated(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), UInt32(writeLimit))
|
|
448
|
+
}
|
|
430
449
|
|
|
431
450
|
for char in chars {
|
|
432
451
|
if char.uuid == BLEManager.rxCharUUID {
|
|
@@ -466,6 +485,12 @@ extension BLEManager: CBPeripheralDelegate {
|
|
|
466
485
|
}
|
|
467
486
|
}
|
|
468
487
|
}
|
|
488
|
+
|
|
489
|
+
// Called when writeValue(.withoutResponse) exhausted the internal queue.
|
|
490
|
+
// Re-trigger TX drain so buffered frames get sent now that there's room.
|
|
491
|
+
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
|
|
492
|
+
onReadyToSend?()
|
|
493
|
+
}
|
|
469
494
|
}
|
|
470
495
|
|
|
471
496
|
// MARK: - CBPeripheralManagerDelegate
|
|
@@ -513,6 +538,11 @@ extension BLEManager: CBPeripheralManagerDelegate {
|
|
|
513
538
|
addr.withUnsafeBytes { ptr in
|
|
514
539
|
_ = lxmf_ble_connected(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self))
|
|
515
540
|
}
|
|
541
|
+
// Report negotiated notification limit for this central.
|
|
542
|
+
let writeLimit = central.maximumUpdateValueLength
|
|
543
|
+
addr.withUnsafeBytes { ptr in
|
|
544
|
+
_ = lxmf_ble_mtu_negotiated(ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), UInt32(writeLimit))
|
|
545
|
+
}
|
|
516
546
|
}
|
|
517
547
|
}
|
|
518
548
|
|
|
@@ -527,6 +557,11 @@ extension BLEManager: CBPeripheralManagerDelegate {
|
|
|
527
557
|
}
|
|
528
558
|
}
|
|
529
559
|
|
|
560
|
+
// Called when updateValue returned false and the subscriber buffer has drained.
|
|
561
|
+
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
|
|
562
|
+
onReadyToSend?()
|
|
563
|
+
}
|
|
564
|
+
|
|
530
565
|
// Background restoration
|
|
531
566
|
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String: Any]) {
|
|
532
567
|
// Re-setup services on restoration
|
package/ios/LxmfModule.swift
CHANGED
|
@@ -111,6 +111,12 @@ func lxmf_ble_disconnected(
|
|
|
111
111
|
@_silgen_name("lxmf_ble_peer_count")
|
|
112
112
|
func lxmf_ble_peer_count() -> Int32
|
|
113
113
|
|
|
114
|
+
@_silgen_name("lxmf_ble_mtu_negotiated")
|
|
115
|
+
func lxmf_ble_mtu_negotiated(
|
|
116
|
+
_ peerAddr: UnsafePointer<UInt8>?,
|
|
117
|
+
_ writeLimit: UInt32
|
|
118
|
+
) -> Int32
|
|
119
|
+
|
|
114
120
|
// --- NUS Interface FFI (RNode BLE via Nordic UART Service) ---
|
|
115
121
|
|
|
116
122
|
@_silgen_name("lxmf_nus_receive")
|
|
@@ -135,7 +141,11 @@ public class LxmfModule: Module {
|
|
|
135
141
|
private var txDrainTimer: Timer?
|
|
136
142
|
|
|
137
143
|
// BLE manager for phone-to-phone mesh
|
|
138
|
-
private lazy var bleManager =
|
|
144
|
+
private lazy var bleManager: BLEManager = {
|
|
145
|
+
let mgr = BLEManager()
|
|
146
|
+
mgr.onReadyToSend = { [weak self] in DispatchQueue.main.async { self?.drainOutgoing() } }
|
|
147
|
+
return mgr
|
|
148
|
+
}()
|
|
139
149
|
|
|
140
150
|
public func definition() -> ModuleDefinition {
|
|
141
151
|
Name("LxmfModule")
|
|
@@ -385,7 +395,8 @@ public class LxmfModule: Module {
|
|
|
385
395
|
|
|
386
396
|
let frameData = Data(dataBuf[0..<Int(len)])
|
|
387
397
|
let addr = Data(peerAddr)
|
|
388
|
-
|
|
398
|
+
// Stop draining if CoreBluetooth buffer is full — onReadyToSend re-triggers us.
|
|
399
|
+
guard bleManager.sendToPeerAddr(addr, data: frameData) else { break }
|
|
389
400
|
}
|
|
390
401
|
|
|
391
402
|
// --- NUS: poll for KISS-framed RNode data ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magicred-1/react-native-lxmf",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "LXMF Reticulum mesh networking for React Native + Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -73,6 +73,19 @@
|
|
|
73
73
|
"LxmfReactNative.podspec",
|
|
74
74
|
"expo-module.config.json",
|
|
75
75
|
"app.plugin.js",
|
|
76
|
-
"README.md"
|
|
76
|
+
"README.md",
|
|
77
|
+
"example/app",
|
|
78
|
+
"example/assets",
|
|
79
|
+
"example/components",
|
|
80
|
+
"example/constants",
|
|
81
|
+
"example/hooks",
|
|
82
|
+
"example/scripts",
|
|
83
|
+
"example/app.json",
|
|
84
|
+
"example/package.json",
|
|
85
|
+
"example/metro.config.js",
|
|
86
|
+
"example/tsconfig.json",
|
|
87
|
+
"example/expo-env.d.ts",
|
|
88
|
+
"example/eslint.config.js",
|
|
89
|
+
"example/README.md"
|
|
77
90
|
]
|
|
78
91
|
}
|