@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.
- package/LxmfReactNative.podspec +25 -0
- package/README.md +57 -0
- package/android/build.gradle.kts +33 -0
- 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 +282 -0
- package/android/src/main/kotlin/expo/modules/lxmf/LxmfModule.kt +232 -0
- package/app.plugin.js +40 -0
- package/build/LxmfModule.d.ts +29 -0
- package/build/LxmfModule.js +31 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +2 -0
- package/build/useLxmf.d.ts +81 -0
- package/build/useLxmf.js +252 -0
- package/expo-module.config.json +10 -0
- package/ios/BLEManager.swift +494 -0
- package/ios/LxmfModule.swift +422 -0
- package/package.json +76 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type NativeModuleType = {
|
|
2
|
+
init(dbPath?: string | null): boolean;
|
|
3
|
+
start(identityHex: string, lxmfAddressHex: string, mode: number, announceIntervalMs: number, bleMtuHint: number, tcpInterfaces: {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
}[], displayName: string): Promise<boolean>;
|
|
7
|
+
stop(): Promise<boolean>;
|
|
8
|
+
isRunning(): boolean;
|
|
9
|
+
send(destHex: string, bodyBase64: string): Promise<number>;
|
|
10
|
+
broadcast(destsHex: string[], bodyBase64: string): Promise<number>;
|
|
11
|
+
getStatus(): string | null;
|
|
12
|
+
getBeacons(): string | null;
|
|
13
|
+
fetchMessages(limit: number): string | null;
|
|
14
|
+
setLogLevel(level: number): boolean;
|
|
15
|
+
abiVersion(): number;
|
|
16
|
+
startBLE(): boolean;
|
|
17
|
+
stopBLE(): boolean;
|
|
18
|
+
blePeerCount(): number;
|
|
19
|
+
bleUnpairedRNodeCount(): number;
|
|
20
|
+
};
|
|
21
|
+
declare const LxmfModuleNative: NativeModuleType | null;
|
|
22
|
+
export declare const isLxmfNativeAvailable: boolean;
|
|
23
|
+
export declare const LxmfModule: NativeModuleType;
|
|
24
|
+
/**
|
|
25
|
+
* The raw native module instance, or null when unavailable.
|
|
26
|
+
* In Expo SDK 50+, NativeModule extends the C++ EventEmitter — call addListener() on it directly.
|
|
27
|
+
* Do NOT use NativeEventEmitter from react-native; it does not wire up to Expo module events.
|
|
28
|
+
*/
|
|
29
|
+
export { LxmfModuleNative };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
2
|
+
const MISSING_NATIVE_MESSAGE = "Cannot find native module 'LxmfModule'. Use an Expo development build (not Expo Go) and rebuild native apps after local module changes.";
|
|
3
|
+
const LxmfModuleNative = requireOptionalNativeModule('LxmfModule');
|
|
4
|
+
export const isLxmfNativeAvailable = !!LxmfModuleNative;
|
|
5
|
+
const throwMissingNative = () => {
|
|
6
|
+
throw new Error(MISSING_NATIVE_MESSAGE);
|
|
7
|
+
};
|
|
8
|
+
const missingNativeShim = {
|
|
9
|
+
init: () => throwMissingNative(),
|
|
10
|
+
start: async () => throwMissingNative(),
|
|
11
|
+
stop: async () => throwMissingNative(),
|
|
12
|
+
isRunning: () => false,
|
|
13
|
+
send: async () => throwMissingNative(),
|
|
14
|
+
broadcast: async () => throwMissingNative(),
|
|
15
|
+
getStatus: () => throwMissingNative(),
|
|
16
|
+
getBeacons: () => throwMissingNative(),
|
|
17
|
+
fetchMessages: () => throwMissingNative(),
|
|
18
|
+
setLogLevel: () => throwMissingNative(),
|
|
19
|
+
abiVersion: () => throwMissingNative(),
|
|
20
|
+
startBLE: () => throwMissingNative(),
|
|
21
|
+
stopBLE: () => throwMissingNative(),
|
|
22
|
+
blePeerCount: () => throwMissingNative(),
|
|
23
|
+
bleUnpairedRNodeCount: () => throwMissingNative(),
|
|
24
|
+
};
|
|
25
|
+
export const LxmfModule = LxmfModuleNative ?? missingNativeShim;
|
|
26
|
+
/**
|
|
27
|
+
* The raw native module instance, or null when unavailable.
|
|
28
|
+
* In Expo SDK 50+, NativeModule extends the C++ EventEmitter — call addListener() on it directly.
|
|
29
|
+
* Do NOT use NativeEventEmitter from react-native; it does not wire up to Expo module events.
|
|
30
|
+
*/
|
|
31
|
+
export { LxmfModuleNative };
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface LxmfNodeStatus {
|
|
2
|
+
running: boolean;
|
|
3
|
+
mode: number;
|
|
4
|
+
identityHex: string;
|
|
5
|
+
addressHex: string;
|
|
6
|
+
lifecycle: number;
|
|
7
|
+
epoch: number;
|
|
8
|
+
pendingOutbound: number;
|
|
9
|
+
outboundSent: number;
|
|
10
|
+
inboundAccepted: number;
|
|
11
|
+
announcesReceived: number;
|
|
12
|
+
lxmfMessagesReceived: number;
|
|
13
|
+
blePeerCount: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Beacon {
|
|
16
|
+
destHash: string;
|
|
17
|
+
state: string;
|
|
18
|
+
lastAnnounce: number;
|
|
19
|
+
reconnectAttempts: number;
|
|
20
|
+
}
|
|
21
|
+
export interface LxmfEvent {
|
|
22
|
+
type: 'statusChanged' | 'packetReceived' | 'txReceived' | 'beaconDiscovered' | 'messageReceived' | 'announceReceived' | 'log' | 'error';
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
/** Node transport mode */
|
|
26
|
+
export declare enum LxmfNodeMode {
|
|
27
|
+
/** BLE-only mesh (default) */
|
|
28
|
+
BleOnly = 0,
|
|
29
|
+
/** Connect via FFI's internal TCP (non-standard framing) */
|
|
30
|
+
TcpClient = 1,
|
|
31
|
+
/** Listen via FFI's internal TCP (non-standard framing) */
|
|
32
|
+
TcpServer = 2,
|
|
33
|
+
/** Connect to standard Reticulum daemon (rnsd) via HDLC-framed TCP */
|
|
34
|
+
Reticulum = 3
|
|
35
|
+
}
|
|
36
|
+
export interface TcpInterface {
|
|
37
|
+
host: string;
|
|
38
|
+
port: number;
|
|
39
|
+
}
|
|
40
|
+
export interface UseLxmfOptions {
|
|
41
|
+
autoStart?: boolean;
|
|
42
|
+
identityHex?: string;
|
|
43
|
+
lxmfAddressHex?: string;
|
|
44
|
+
dbPath?: string;
|
|
45
|
+
logLevel?: number;
|
|
46
|
+
/** Transport mode — BLE or Reticulum TCP. Default: BleOnly */
|
|
47
|
+
mode?: LxmfNodeMode;
|
|
48
|
+
/** One or more TCP interfaces to connect to (required for Reticulum mode). */
|
|
49
|
+
tcpInterfaces?: TcpInterface[];
|
|
50
|
+
/** Announce interval in ms. Default: 5000 */
|
|
51
|
+
announceIntervalMs?: number;
|
|
52
|
+
/** BLE MTU hint. Default: 255 */
|
|
53
|
+
bleMtuHint?: number;
|
|
54
|
+
/** Display name broadcast in LXMF announces. Default: "lxmf-mobile" */
|
|
55
|
+
displayName?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function useLxmf(options?: UseLxmfOptions): {
|
|
58
|
+
status: LxmfNodeStatus | null;
|
|
59
|
+
beacons: Beacon[];
|
|
60
|
+
events: LxmfEvent[];
|
|
61
|
+
error: string | null;
|
|
62
|
+
isRunning: boolean;
|
|
63
|
+
isNativeAvailable: boolean;
|
|
64
|
+
start: (overrides?: {
|
|
65
|
+
identityHex?: string;
|
|
66
|
+
lxmfAddressHex?: string;
|
|
67
|
+
mode?: LxmfNodeMode;
|
|
68
|
+
tcpInterfaces?: TcpInterface[];
|
|
69
|
+
displayName?: string;
|
|
70
|
+
}) => Promise<boolean>;
|
|
71
|
+
stop: () => Promise<void>;
|
|
72
|
+
send: (destHex: string, bodyBase64: string) => Promise<number>;
|
|
73
|
+
broadcast: (destsHex: string[], bodyBase64: string) => Promise<number>;
|
|
74
|
+
getStatus: () => LxmfNodeStatus | null;
|
|
75
|
+
getBeacons: () => Beacon[];
|
|
76
|
+
fetchMessages: (limit?: number) => any[];
|
|
77
|
+
setLogLevel: (level: number) => boolean;
|
|
78
|
+
startBLE: () => void;
|
|
79
|
+
stopBLE: () => void;
|
|
80
|
+
bleUnpairedRNodeCount: () => number;
|
|
81
|
+
};
|
package/build/useLxmf.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { isLxmfNativeAvailable, LxmfModule, LxmfModuleNative } from './LxmfModule';
|
|
3
|
+
/** Node transport mode */
|
|
4
|
+
export var LxmfNodeMode;
|
|
5
|
+
(function (LxmfNodeMode) {
|
|
6
|
+
/** BLE-only mesh (default) */
|
|
7
|
+
LxmfNodeMode[LxmfNodeMode["BleOnly"] = 0] = "BleOnly";
|
|
8
|
+
/** Connect via FFI's internal TCP (non-standard framing) */
|
|
9
|
+
LxmfNodeMode[LxmfNodeMode["TcpClient"] = 1] = "TcpClient";
|
|
10
|
+
/** Listen via FFI's internal TCP (non-standard framing) */
|
|
11
|
+
LxmfNodeMode[LxmfNodeMode["TcpServer"] = 2] = "TcpServer";
|
|
12
|
+
/** Connect to standard Reticulum daemon (rnsd) via HDLC-framed TCP */
|
|
13
|
+
LxmfNodeMode[LxmfNodeMode["Reticulum"] = 3] = "Reticulum";
|
|
14
|
+
})(LxmfNodeMode || (LxmfNodeMode = {}));
|
|
15
|
+
function parseJson(value, fallback) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return fallback;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function useLxmf(options = {}) {
|
|
26
|
+
const [status, setStatus] = useState(null);
|
|
27
|
+
const [beacons, setBeacons] = useState([]);
|
|
28
|
+
const [events, setEvents] = useState([]);
|
|
29
|
+
const [error, setError] = useState(null);
|
|
30
|
+
const [running, setRunning] = useState(false);
|
|
31
|
+
const pushEvent = useCallback((type, payload) => {
|
|
32
|
+
const event = { ...payload, type };
|
|
33
|
+
setEvents((prev) => [event, ...prev].slice(0, 200));
|
|
34
|
+
return event;
|
|
35
|
+
}, []);
|
|
36
|
+
const syncStatus = useCallback(() => {
|
|
37
|
+
const parsed = parseJson(LxmfModule.getStatus(), null);
|
|
38
|
+
setStatus(parsed);
|
|
39
|
+
if (parsed && typeof parsed.running === 'boolean') {
|
|
40
|
+
setRunning(parsed.running);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}, []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!isLxmfNativeAvailable) {
|
|
46
|
+
setError("Cannot find native module 'LxmfModule'. Run this app in an Expo development build (not Expo Go).");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const initialized = LxmfModule.init(options.dbPath || null);
|
|
51
|
+
if (!initialized) {
|
|
52
|
+
setError('Failed to initialize LXMF module');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const alreadyRunning = LxmfModule.isRunning();
|
|
56
|
+
setRunning(alreadyRunning);
|
|
57
|
+
if (alreadyRunning) {
|
|
58
|
+
syncStatus();
|
|
59
|
+
}
|
|
60
|
+
setError(null);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
setError(e?.message ?? 'Initialization failed');
|
|
64
|
+
}
|
|
65
|
+
}, [options.dbPath, syncStatus]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!isLxmfNativeAvailable || !LxmfModuleNative) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const mod = LxmfModuleNative;
|
|
71
|
+
const subscriptions = [
|
|
72
|
+
mod.addListener('onStatusChanged', (event) => {
|
|
73
|
+
pushEvent('statusChanged', event);
|
|
74
|
+
if (typeof event.running === 'boolean') {
|
|
75
|
+
setRunning(event.running);
|
|
76
|
+
}
|
|
77
|
+
syncStatus();
|
|
78
|
+
}),
|
|
79
|
+
mod.addListener('onPacketReceived', (event) => {
|
|
80
|
+
pushEvent('packetReceived', event);
|
|
81
|
+
}),
|
|
82
|
+
mod.addListener('onTxReceived', (event) => {
|
|
83
|
+
pushEvent('txReceived', event);
|
|
84
|
+
}),
|
|
85
|
+
mod.addListener('onBeaconDiscovered', (event) => {
|
|
86
|
+
pushEvent('beaconDiscovered', event);
|
|
87
|
+
const latestBeacons = parseJson(LxmfModule.getBeacons(), []);
|
|
88
|
+
setBeacons(latestBeacons);
|
|
89
|
+
}),
|
|
90
|
+
mod.addListener('onMessageReceived', (event) => {
|
|
91
|
+
pushEvent('messageReceived', event);
|
|
92
|
+
}),
|
|
93
|
+
mod.addListener('onAnnounceReceived', (event) => {
|
|
94
|
+
pushEvent('announceReceived', event);
|
|
95
|
+
}),
|
|
96
|
+
mod.addListener('onLog', (event) => {
|
|
97
|
+
pushEvent('log', event);
|
|
98
|
+
if (typeof options.logLevel === 'number' && options.logLevel >= Number(event.level ?? 0)) {
|
|
99
|
+
console.log(`[LXMF] ${String(event.message)}`);
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
mod.addListener('onError', (event) => {
|
|
103
|
+
pushEvent('error', event);
|
|
104
|
+
setError(`${String(event.code)}: ${String(event.message)}`);
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
return () => {
|
|
108
|
+
subscriptions.forEach((sub) => sub.remove());
|
|
109
|
+
};
|
|
110
|
+
}, [options.logLevel, pushEvent, syncStatus]);
|
|
111
|
+
const start = useCallback(async (overrides) => {
|
|
112
|
+
try {
|
|
113
|
+
if (!isLxmfNativeAvailable) {
|
|
114
|
+
setError("Cannot find native module 'LxmfModule'. Run this app in an Expo development build (not Expo Go).");
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const resolvedIdentityHex = overrides?.identityHex ?? options.identityHex;
|
|
118
|
+
const resolvedLxmfAddressHex = overrides?.lxmfAddressHex ?? options.lxmfAddressHex;
|
|
119
|
+
if (!resolvedIdentityHex || !resolvedLxmfAddressHex) {
|
|
120
|
+
setError('Missing identity or LXMF address. Pass them to start() or UseLxmfOptions.');
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const mode = overrides?.mode ?? options.mode ?? LxmfNodeMode.BleOnly;
|
|
124
|
+
const tcpInterfaces = overrides?.tcpInterfaces ?? options.tcpInterfaces ?? [];
|
|
125
|
+
const announceMs = options.announceIntervalMs ?? 5000;
|
|
126
|
+
const bleMtu = options.bleMtuHint ?? 255;
|
|
127
|
+
const displayName = overrides?.displayName ?? options.displayName ?? '';
|
|
128
|
+
if (mode !== LxmfNodeMode.BleOnly && tcpInterfaces.length === 0) {
|
|
129
|
+
setError(`Mode ${mode} requires at least one TCP interface.`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
await LxmfModule.start(resolvedIdentityHex, resolvedLxmfAddressHex, mode, announceMs, bleMtu, tcpInterfaces, displayName);
|
|
133
|
+
setRunning(true);
|
|
134
|
+
syncStatus();
|
|
135
|
+
setError(null);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
setError(e?.message ?? 'Failed to start');
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}, [
|
|
143
|
+
options.identityHex,
|
|
144
|
+
options.lxmfAddressHex,
|
|
145
|
+
options.mode,
|
|
146
|
+
options.tcpInterfaces,
|
|
147
|
+
options.announceIntervalMs,
|
|
148
|
+
options.bleMtuHint,
|
|
149
|
+
options.displayName,
|
|
150
|
+
syncStatus,
|
|
151
|
+
]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!options.autoStart || running)
|
|
154
|
+
return;
|
|
155
|
+
if (!options.identityHex || !options.lxmfAddressHex)
|
|
156
|
+
return;
|
|
157
|
+
start().catch(() => {
|
|
158
|
+
// start() already sets error state on failure
|
|
159
|
+
});
|
|
160
|
+
}, [options.autoStart, options.identityHex, options.lxmfAddressHex, running, start]);
|
|
161
|
+
const stop = useCallback(async () => {
|
|
162
|
+
try {
|
|
163
|
+
await LxmfModule.stop();
|
|
164
|
+
setRunning(false);
|
|
165
|
+
setStatus(null);
|
|
166
|
+
setError(null);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
setError(e?.message ?? 'Failed to stop');
|
|
170
|
+
}
|
|
171
|
+
}, []);
|
|
172
|
+
const send = useCallback(async (destHex, bodyBase64) => {
|
|
173
|
+
try {
|
|
174
|
+
return await LxmfModule.send(destHex, bodyBase64);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
setError(e.message);
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
}, []);
|
|
181
|
+
const broadcast = useCallback(async (destsHex, bodyBase64) => {
|
|
182
|
+
try {
|
|
183
|
+
return await LxmfModule.broadcast(destsHex, bodyBase64);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
setError(e.message);
|
|
187
|
+
return -1;
|
|
188
|
+
}
|
|
189
|
+
}, []);
|
|
190
|
+
const getStatus = useCallback(() => {
|
|
191
|
+
try {
|
|
192
|
+
return syncStatus();
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
setError(`Failed to parse status payload: ${e?.message ?? 'unknown error'}`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}, [syncStatus]);
|
|
199
|
+
const getBeacons = useCallback(() => {
|
|
200
|
+
try {
|
|
201
|
+
const parsed = parseJson(LxmfModule.getBeacons(), []);
|
|
202
|
+
setBeacons(parsed);
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
setError(`Failed to parse beacon payload: ${e?.message ?? 'unknown error'}`);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}, []);
|
|
210
|
+
const fetchMessages = useCallback((limit = 50) => {
|
|
211
|
+
try {
|
|
212
|
+
return parseJson(LxmfModule.fetchMessages(limit), []);
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
setError(`Failed to parse message payload: ${e?.message ?? 'unknown error'}`);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}, []);
|
|
219
|
+
const setLogLevel = useCallback((level) => {
|
|
220
|
+
return LxmfModule.setLogLevel(level);
|
|
221
|
+
}, []);
|
|
222
|
+
const startBLE = useCallback(() => {
|
|
223
|
+
LxmfModule.startBLE();
|
|
224
|
+
}, []);
|
|
225
|
+
const stopBLE = useCallback(() => {
|
|
226
|
+
LxmfModule.stopBLE();
|
|
227
|
+
}, []);
|
|
228
|
+
const bleUnpairedRNodeCount = useCallback(() => {
|
|
229
|
+
return LxmfModule.bleUnpairedRNodeCount();
|
|
230
|
+
}, []);
|
|
231
|
+
return {
|
|
232
|
+
// State
|
|
233
|
+
status,
|
|
234
|
+
beacons,
|
|
235
|
+
events,
|
|
236
|
+
error,
|
|
237
|
+
isRunning: running,
|
|
238
|
+
isNativeAvailable: isLxmfNativeAvailable,
|
|
239
|
+
// Methods
|
|
240
|
+
start,
|
|
241
|
+
stop,
|
|
242
|
+
send,
|
|
243
|
+
broadcast,
|
|
244
|
+
getStatus,
|
|
245
|
+
getBeacons,
|
|
246
|
+
fetchMessages,
|
|
247
|
+
setLogLevel,
|
|
248
|
+
startBLE,
|
|
249
|
+
stopBLE,
|
|
250
|
+
bleUnpairedRNodeCount,
|
|
251
|
+
};
|
|
252
|
+
}
|