react-native-ble-mesh 1.1.1 → 2.0.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/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +70 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +659 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +79 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +125 -0
- package/src/crypto/providers/QuickCryptoProvider.js +134 -0
- package/src/crypto/providers/TweetNaClProvider.js +124 -0
- package/src/crypto/providers/index.js +11 -0
- package/src/errors/MeshError.js +2 -1
- package/src/expo/withBLEMesh.js +102 -0
- package/src/hooks/useMesh.js +30 -9
- package/src/hooks/useMessages.js +2 -0
- package/src/index.js +23 -8
- package/src/mesh/dedup/DedupManager.js +36 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +408 -0
- package/src/mesh/monitor/NetworkMonitor.js +327 -316
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/PeerManager.js +6 -1
- package/src/mesh/router/MessageRouter.js +26 -15
- package/src/mesh/router/RouteTable.js +7 -1
- package/src/mesh/store/StoreAndForwardManager.js +295 -297
- package/src/mesh/store/index.js +1 -1
- package/src/service/BatteryOptimizer.js +282 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +167 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +77 -2
- package/src/service/audio/AudioManager.js +8 -2
- package/src/service/file/FileAssembler.js +106 -0
- package/src/service/file/FileChunker.js +79 -0
- package/src/service/file/FileManager.js +307 -0
- package/src/service/file/FileMessage.js +122 -0
- package/src/service/file/index.js +15 -0
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/transport/BLETransport.js +131 -9
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +305 -0
- package/src/transport/WiFiDirectTransport.js +295 -0
- package/src/transport/adapters/NodeBLEAdapter.js +34 -0
- package/src/transport/adapters/RNBLEAdapter.js +56 -1
- package/src/transport/index.js +6 -0
- package/src/utils/compression.js +291 -291
- package/src/crypto/aead.js +0 -189
- package/src/crypto/chacha20.js +0 -181
- package/src/crypto/hkdf.js +0 -187
- package/src/crypto/hmac.js +0 -143
- package/src/crypto/keys/KeyManager.js +0 -271
- package/src/crypto/keys/KeyPair.js +0 -216
- package/src/crypto/keys/SecureStorage.js +0 -219
- package/src/crypto/keys/index.js +0 -32
- package/src/crypto/noise/handshake.js +0 -410
- package/src/crypto/noise/index.js +0 -27
- package/src/crypto/noise/session.js +0 -253
- package/src/crypto/noise/state.js +0 -268
- package/src/crypto/poly1305.js +0 -113
- package/src/crypto/sha256.js +0 -240
- package/src/crypto/x25519.js +0 -154
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Expo config plugin for react-native-ble-mesh
|
|
5
|
+
* @module expo/withBLEMesh
|
|
6
|
+
*
|
|
7
|
+
* Automatically configures BLE permissions and background modes
|
|
8
|
+
* for iOS and Android when used with Expo.
|
|
9
|
+
*
|
|
10
|
+
* Usage in app.json:
|
|
11
|
+
* {
|
|
12
|
+
* "expo": {
|
|
13
|
+
* "plugins": [
|
|
14
|
+
* ["react-native-ble-mesh", {
|
|
15
|
+
* "bluetoothAlwaysPermission": "Chat with nearby devices via Bluetooth",
|
|
16
|
+
* "backgroundModes": ["bluetooth-central", "bluetooth-peripheral"]
|
|
17
|
+
* }]
|
|
18
|
+
* ]
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default configuration
|
|
25
|
+
* @constant {Object}
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_OPTIONS = {
|
|
28
|
+
/** iOS NSBluetoothAlwaysUsageDescription */
|
|
29
|
+
bluetoothAlwaysPermission: 'This app uses Bluetooth to communicate with nearby devices',
|
|
30
|
+
/** iOS UIBackgroundModes */
|
|
31
|
+
backgroundModes: ['bluetooth-central', 'bluetooth-peripheral'],
|
|
32
|
+
/** Android permissions */
|
|
33
|
+
androidPermissions: [
|
|
34
|
+
'android.permission.BLUETOOTH_SCAN',
|
|
35
|
+
'android.permission.BLUETOOTH_CONNECT',
|
|
36
|
+
'android.permission.BLUETOOTH_ADVERTISE',
|
|
37
|
+
'android.permission.ACCESS_FINE_LOCATION'
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Modifies the iOS Info.plist for BLE permissions.
|
|
43
|
+
* @param {Object} config - Expo config
|
|
44
|
+
* @param {Object} options - Plugin options
|
|
45
|
+
* @returns {Object} Modified config
|
|
46
|
+
*/
|
|
47
|
+
function withBLEMeshIOS(config, options) {
|
|
48
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
49
|
+
|
|
50
|
+
if (!config.ios) { config.ios = {}; }
|
|
51
|
+
if (!config.ios.infoPlist) { config.ios.infoPlist = {}; }
|
|
52
|
+
|
|
53
|
+
// Add Bluetooth usage description
|
|
54
|
+
config.ios.infoPlist.NSBluetoothAlwaysUsageDescription = opts.bluetoothAlwaysPermission;
|
|
55
|
+
config.ios.infoPlist.NSBluetoothPeripheralUsageDescription = opts.bluetoothAlwaysPermission;
|
|
56
|
+
|
|
57
|
+
// Add background modes
|
|
58
|
+
const existingModes = config.ios.infoPlist.UIBackgroundModes || [];
|
|
59
|
+
const newModes = new Set([...existingModes, ...opts.backgroundModes]);
|
|
60
|
+
config.ios.infoPlist.UIBackgroundModes = Array.from(newModes);
|
|
61
|
+
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Modifies the Android manifest for BLE permissions.
|
|
67
|
+
* @param {Object} config - Expo config
|
|
68
|
+
* @param {Object} options - Plugin options
|
|
69
|
+
* @returns {Object} Modified config
|
|
70
|
+
*/
|
|
71
|
+
function withBLEMeshAndroid(config, options) {
|
|
72
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
73
|
+
|
|
74
|
+
if (!config.android) { config.android = {}; }
|
|
75
|
+
if (!config.android.permissions) { config.android.permissions = []; }
|
|
76
|
+
|
|
77
|
+
// Add BLE permissions (avoid duplicates)
|
|
78
|
+
const existingPerms = new Set(config.android.permissions);
|
|
79
|
+
for (const perm of opts.androidPermissions) {
|
|
80
|
+
existingPerms.add(perm);
|
|
81
|
+
}
|
|
82
|
+
config.android.permissions = Array.from(existingPerms);
|
|
83
|
+
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Main Expo config plugin.
|
|
89
|
+
* @param {Object} config - Expo config
|
|
90
|
+
* @param {Object} [options={}] - Plugin options
|
|
91
|
+
* @returns {Object} Modified config
|
|
92
|
+
*/
|
|
93
|
+
function withBLEMesh(config, options = {}) {
|
|
94
|
+
config = withBLEMeshIOS(config, options);
|
|
95
|
+
config = withBLEMeshAndroid(config, options);
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = withBLEMesh;
|
|
100
|
+
module.exports.withBLEMeshIOS = withBLEMeshIOS;
|
|
101
|
+
module.exports.withBLEMeshAndroid = withBLEMeshAndroid;
|
|
102
|
+
module.exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
package/src/hooks/useMesh.js
CHANGED
|
@@ -46,6 +46,7 @@ function useMesh(config = {}) {
|
|
|
46
46
|
|
|
47
47
|
// Create mesh instance ref (persists across renders)
|
|
48
48
|
const meshRef = useRef(null);
|
|
49
|
+
const mountedRef = useRef(true);
|
|
49
50
|
|
|
50
51
|
// State
|
|
51
52
|
const [state, setState] = useState('uninitialized');
|
|
@@ -64,6 +65,7 @@ function useMesh(config = {}) {
|
|
|
64
65
|
// Initialize mesh
|
|
65
66
|
const initialize = useCallback(async (transport) => {
|
|
66
67
|
try {
|
|
68
|
+
if (!mountedRef.current) { return; }
|
|
67
69
|
setState('initializing');
|
|
68
70
|
setError(null);
|
|
69
71
|
|
|
@@ -71,11 +73,15 @@ function useMesh(config = {}) {
|
|
|
71
73
|
|
|
72
74
|
// Setup state change listener
|
|
73
75
|
mesh.on('state-changed', ({ newState }) => {
|
|
74
|
-
|
|
76
|
+
if (mountedRef.current) {
|
|
77
|
+
setState(newState);
|
|
78
|
+
}
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
mesh.on('error', (err) => {
|
|
78
|
-
|
|
82
|
+
if (mountedRef.current) {
|
|
83
|
+
setError(err);
|
|
84
|
+
}
|
|
79
85
|
});
|
|
80
86
|
|
|
81
87
|
// Initialize with storage
|
|
@@ -90,8 +96,10 @@ function useMesh(config = {}) {
|
|
|
90
96
|
|
|
91
97
|
return mesh;
|
|
92
98
|
} catch (err) {
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
if (mountedRef.current) {
|
|
100
|
+
setState('error');
|
|
101
|
+
setError(err);
|
|
102
|
+
}
|
|
95
103
|
throw err;
|
|
96
104
|
}
|
|
97
105
|
}, [getMesh, config.storage]);
|
|
@@ -102,7 +110,9 @@ function useMesh(config = {}) {
|
|
|
102
110
|
try {
|
|
103
111
|
await mesh.start(transport);
|
|
104
112
|
} catch (err) {
|
|
105
|
-
|
|
113
|
+
if (mountedRef.current) {
|
|
114
|
+
setError(err);
|
|
115
|
+
}
|
|
106
116
|
throw err;
|
|
107
117
|
}
|
|
108
118
|
}, [getMesh]);
|
|
@@ -114,7 +124,9 @@ function useMesh(config = {}) {
|
|
|
114
124
|
try {
|
|
115
125
|
await mesh.stop();
|
|
116
126
|
} catch (err) {
|
|
117
|
-
|
|
127
|
+
if (mountedRef.current) {
|
|
128
|
+
setError(err);
|
|
129
|
+
}
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
}, []);
|
|
@@ -129,16 +141,25 @@ function useMesh(config = {}) {
|
|
|
129
141
|
// Ignore destroy errors
|
|
130
142
|
}
|
|
131
143
|
meshRef.current = null;
|
|
132
|
-
|
|
144
|
+
if (mountedRef.current) {
|
|
145
|
+
setState('destroyed');
|
|
146
|
+
}
|
|
133
147
|
}
|
|
134
148
|
}, []);
|
|
135
149
|
|
|
136
150
|
// Cleanup on unmount
|
|
137
151
|
useEffect(() => {
|
|
138
152
|
return () => {
|
|
153
|
+
// Immediately mark as unmounted to prevent state updates
|
|
154
|
+
mountedRef.current = false;
|
|
155
|
+
|
|
139
156
|
if (meshRef.current) {
|
|
140
|
-
meshRef.current
|
|
141
|
-
meshRef.current = null;
|
|
157
|
+
const mesh = meshRef.current;
|
|
158
|
+
meshRef.current = null; // Null ref immediately to prevent new operations
|
|
159
|
+
|
|
160
|
+
mesh.destroy().catch(() => {
|
|
161
|
+
// Ignore cleanup errors
|
|
162
|
+
});
|
|
142
163
|
}
|
|
143
164
|
};
|
|
144
165
|
}, []);
|
package/src/hooks/useMessages.js
CHANGED
|
@@ -114,6 +114,8 @@ function useMessages(mesh, options = {}) {
|
|
|
114
114
|
mesh.off('broadcast-received', handleBroadcast);
|
|
115
115
|
mesh.off('private-message-received', handlePrivate);
|
|
116
116
|
mesh.off('channel-message-received', handleChannel);
|
|
117
|
+
// Clear dedup Set on unmount
|
|
118
|
+
messageIdRef.current.clear();
|
|
117
119
|
};
|
|
118
120
|
}, [mesh, addMessage]);
|
|
119
121
|
|
package/src/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @fileoverview BLE Mesh Network Library
|
|
3
3
|
* @module rn-ble-mesh
|
|
4
4
|
* @description Production-ready BLE Mesh Network with Noise Protocol security
|
|
5
|
-
*
|
|
6
|
-
* This is the definitive React Native library for BitChat-compatible
|
|
5
|
+
*
|
|
6
|
+
* This is the definitive React Native library for BitChat-compatible
|
|
7
7
|
* decentralized mesh networking.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -16,9 +16,7 @@ const { MeshNetwork, BATTERY_MODE, PANIC_TRIGGER, HEALTH_STATUS } = require('./M
|
|
|
16
16
|
const {
|
|
17
17
|
MeshService,
|
|
18
18
|
EmergencyManager,
|
|
19
|
-
BatteryOptimizer
|
|
20
|
-
SessionManager,
|
|
21
|
-
HandshakeManager,
|
|
19
|
+
BatteryOptimizer
|
|
22
20
|
} = require('./service');
|
|
23
21
|
|
|
24
22
|
// Constants
|
|
@@ -27,7 +25,7 @@ const constants = require('./constants');
|
|
|
27
25
|
// Errors
|
|
28
26
|
const errors = require('./errors');
|
|
29
27
|
|
|
30
|
-
// Crypto
|
|
28
|
+
// Crypto — pluggable provider system (tweetnacl / quick-crypto / expo-crypto)
|
|
31
29
|
const crypto = require('./crypto');
|
|
32
30
|
|
|
33
31
|
// Protocol
|
|
@@ -51,6 +49,10 @@ const audio = require('./service/audio');
|
|
|
51
49
|
// Text (from service module)
|
|
52
50
|
const text = require('./service/text');
|
|
53
51
|
|
|
52
|
+
// File transfer
|
|
53
|
+
const file = require('./service/file');
|
|
54
|
+
const { FileManager, FileChunker, FileAssembler, FileMessage, FILE_MESSAGE_TYPE, FILE_TRANSFER_STATE } = file;
|
|
55
|
+
|
|
54
56
|
// React Native hooks
|
|
55
57
|
const hooks = require('./hooks');
|
|
56
58
|
|
|
@@ -133,7 +135,7 @@ const { useMesh, usePeers, useMessages, AppStateManager } = hooks;
|
|
|
133
135
|
|
|
134
136
|
// New PRD-specified components
|
|
135
137
|
const { StoreAndForwardManager } = mesh;
|
|
136
|
-
const { NetworkMonitor } = mesh;
|
|
138
|
+
const { NetworkMonitor, ConnectionQuality, QUALITY_LEVEL } = mesh;
|
|
137
139
|
const { MessageCompressor, compress, decompress } = utils;
|
|
138
140
|
|
|
139
141
|
module.exports = {
|
|
@@ -156,6 +158,10 @@ module.exports = {
|
|
|
156
158
|
// Store and Forward
|
|
157
159
|
StoreAndForwardManager,
|
|
158
160
|
|
|
161
|
+
// Connection Quality
|
|
162
|
+
ConnectionQuality,
|
|
163
|
+
QUALITY_LEVEL,
|
|
164
|
+
|
|
159
165
|
// Compression
|
|
160
166
|
MessageCompressor,
|
|
161
167
|
compress,
|
|
@@ -186,7 +192,7 @@ module.exports = {
|
|
|
186
192
|
// Errors
|
|
187
193
|
...errors,
|
|
188
194
|
|
|
189
|
-
// Crypto
|
|
195
|
+
// Crypto provider system
|
|
190
196
|
crypto,
|
|
191
197
|
|
|
192
198
|
// Protocol serialization
|
|
@@ -229,6 +235,15 @@ module.exports = {
|
|
|
229
235
|
useMessages,
|
|
230
236
|
AppStateManager,
|
|
231
237
|
|
|
238
|
+
// File transfer
|
|
239
|
+
FileManager,
|
|
240
|
+
FileChunker,
|
|
241
|
+
FileAssembler,
|
|
242
|
+
FileMessage,
|
|
243
|
+
FILE_MESSAGE_TYPE,
|
|
244
|
+
FILE_TRANSFER_STATE,
|
|
245
|
+
file,
|
|
246
|
+
|
|
232
247
|
// Hooks module
|
|
233
248
|
hooks
|
|
234
249
|
};
|
|
@@ -55,6 +55,13 @@ class DedupManager {
|
|
|
55
55
|
*/
|
|
56
56
|
this._cache = new MessageCache(config.cacheSize);
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Old Bloom filter kept during grace period after reset
|
|
60
|
+
* @type {BloomFilter|null}
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
this._oldBloomFilter = null;
|
|
64
|
+
|
|
58
65
|
/**
|
|
59
66
|
* Auto-reset threshold for Bloom filter
|
|
60
67
|
* @type {number}
|
|
@@ -85,20 +92,25 @@ class DedupManager {
|
|
|
85
92
|
isDuplicate(messageId) {
|
|
86
93
|
this._stats.checks++;
|
|
87
94
|
|
|
88
|
-
//
|
|
89
|
-
if (!this._bloomFilter.mightContain(messageId)) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
this._stats.bloomPositives++;
|
|
93
|
-
|
|
94
|
-
// Confirm with exact cache lookup (handles false positives)
|
|
95
|
+
// Check cache first (most accurate)
|
|
95
96
|
if (this._cache.has(messageId)) {
|
|
96
97
|
this._stats.cacheHits++;
|
|
97
98
|
this._stats.duplicates++;
|
|
98
99
|
return true;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
//
|
|
102
|
+
// Check current bloom filter
|
|
103
|
+
if (this._bloomFilter.mightContain(messageId)) {
|
|
104
|
+
this._stats.bloomPositives++;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check old bloom filter if in grace period
|
|
109
|
+
if (this._oldBloomFilter && this._oldBloomFilter.mightContain(messageId)) {
|
|
110
|
+
this._stats.bloomPositives++;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
return false;
|
|
103
115
|
}
|
|
104
116
|
|
|
@@ -135,14 +147,28 @@ class DedupManager {
|
|
|
135
147
|
* @private
|
|
136
148
|
*/
|
|
137
149
|
_resetBloomFilter() {
|
|
138
|
-
|
|
150
|
+
// Create a new bloom filter instead of clearing the old one
|
|
151
|
+
// Keep the old filter active for checking during transition
|
|
152
|
+
const oldFilter = this._bloomFilter;
|
|
153
|
+
this._bloomFilter = new BloomFilter(
|
|
154
|
+
oldFilter.size || MESH_CONFIG.BLOOM_FILTER_SIZE,
|
|
155
|
+
oldFilter.hashCount || MESH_CONFIG.BLOOM_HASH_COUNT
|
|
156
|
+
);
|
|
139
157
|
this._stats.resets++;
|
|
140
158
|
|
|
141
|
-
// Re-add all cached entries to
|
|
159
|
+
// Re-add all cached entries to new filter
|
|
142
160
|
const entries = this._cache.getAll();
|
|
143
161
|
for (const messageId of entries) {
|
|
144
162
|
this._bloomFilter.add(messageId);
|
|
145
163
|
}
|
|
164
|
+
|
|
165
|
+
// Keep old filter for a grace period by checking both
|
|
166
|
+
this._oldBloomFilter = oldFilter;
|
|
167
|
+
|
|
168
|
+
// Clear old filter after grace period
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
this._oldBloomFilter = null;
|
|
171
|
+
}, 60000); // 1 minute grace
|
|
146
172
|
}
|
|
147
173
|
|
|
148
174
|
/**
|
|
@@ -73,6 +73,11 @@ class PendingFragmentSet {
|
|
|
73
73
|
* @returns {Uint8Array} Complete assembled payload
|
|
74
74
|
*/
|
|
75
75
|
assemble() {
|
|
76
|
+
const MAX_ASSEMBLY_SIZE = 500 * 1024; // 500KB max for mesh messages
|
|
77
|
+
if (this.totalPayloadLength > MAX_ASSEMBLY_SIZE) {
|
|
78
|
+
throw new Error(`Fragment assembly too large: ${this.totalPayloadLength} bytes exceeds ${MAX_ASSEMBLY_SIZE} byte limit`);
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
const result = new Uint8Array(this.totalPayloadLength);
|
|
77
82
|
let offset = 0;
|
|
78
83
|
|
package/src/mesh/index.js
CHANGED