@offline-protocol/mesh-sdk 0.2.0 → 0.2.1
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 +963 -106
- package/android/src/main/java/com/offlineprotocol/BleManager.kt +47 -2
- package/ios/BleManager.swift +61 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# @offline-protocol/mesh-sdk
|
|
2
2
|
|
|
3
|
-
Offline-first mesh networking SDK
|
|
3
|
+
Offline-first mesh networking SDK for React Native. Enables peer-to-peer messaging over BLE, WiFi Direct, and Internet with intelligent transport switching.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Table of Contents
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- [Requirements](#requirements)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Platform Setup](#platform-setup)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Protocol Lifecycle](#protocol-lifecycle)
|
|
12
|
+
- [Configuration](#configuration)
|
|
13
|
+
- [API Reference](#api-reference)
|
|
14
|
+
- [Events](#events)
|
|
15
|
+
- [Types](#types)
|
|
16
|
+
- [DORS (Transport Switching)](#dors-dynamic-offline-relay-switch)
|
|
17
|
+
- [Mesh Networking](#mesh-networking)
|
|
18
|
+
- [Reliability Layer](#reliability-layer)
|
|
19
|
+
- [File Transfer](#file-transfer)
|
|
20
|
+
- [Troubleshooting](#troubleshooting)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
| Platform | Version |
|
|
27
|
+
|----------|---------|
|
|
28
|
+
| React Native | >= 0.70.0 |
|
|
29
|
+
| iOS | >= 13.0 |
|
|
30
|
+
| Android | >= API 26 (Android 8.0) |
|
|
31
|
+
| Node.js | >= 16 |
|
|
32
|
+
|
|
33
|
+
---
|
|
12
34
|
|
|
13
35
|
## Installation
|
|
14
36
|
|
|
@@ -16,15 +38,63 @@ Offline-first mesh networking SDK with intelligent transport switching for React
|
|
|
16
38
|
npm install @offline-protocol/mesh-sdk
|
|
17
39
|
```
|
|
18
40
|
|
|
19
|
-
### iOS
|
|
41
|
+
### iOS
|
|
20
42
|
|
|
21
43
|
```bash
|
|
22
44
|
cd ios && pod install
|
|
23
45
|
```
|
|
24
46
|
|
|
25
|
-
### Android
|
|
47
|
+
### Android
|
|
48
|
+
|
|
49
|
+
Pre-built native libraries are included. No additional setup required.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Platform Setup
|
|
54
|
+
|
|
55
|
+
### iOS Permissions
|
|
56
|
+
|
|
57
|
+
Add to `Info.plist`:
|
|
58
|
+
|
|
59
|
+
```xml
|
|
60
|
+
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
61
|
+
<string>Required for offline mesh communication</string>
|
|
26
62
|
|
|
27
|
-
|
|
63
|
+
<key>NSBluetoothPeripheralUsageDescription</key>
|
|
64
|
+
<string>Required for offline mesh communication</string>
|
|
65
|
+
|
|
66
|
+
<key>UIBackgroundModes</key>
|
|
67
|
+
<array>
|
|
68
|
+
<string>bluetooth-central</string>
|
|
69
|
+
<string>bluetooth-peripheral</string>
|
|
70
|
+
</array>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Android Permissions
|
|
74
|
+
|
|
75
|
+
Add to `AndroidManifest.xml`:
|
|
76
|
+
|
|
77
|
+
```xml
|
|
78
|
+
<!-- Bluetooth (Android 12+) -->
|
|
79
|
+
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
|
80
|
+
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
|
81
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
82
|
+
|
|
83
|
+
<!-- Bluetooth (Android 11 and below) -->
|
|
84
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
85
|
+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
|
86
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
87
|
+
|
|
88
|
+
<!-- WiFi Direct -->
|
|
89
|
+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
90
|
+
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
|
91
|
+
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
|
92
|
+
|
|
93
|
+
<!-- Internet -->
|
|
94
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
28
98
|
|
|
29
99
|
## Quick Start
|
|
30
100
|
|
|
@@ -47,62 +117,404 @@ const messageId = await protocol.sendMessage({
|
|
|
47
117
|
content: 'Hello!',
|
|
48
118
|
priority: MessagePriority.High,
|
|
49
119
|
});
|
|
120
|
+
|
|
121
|
+
await protocol.stop();
|
|
122
|
+
await protocol.destroy();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Protocol Lifecycle
|
|
128
|
+
|
|
129
|
+
### Complete Flow Example
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import {
|
|
133
|
+
OfflineProtocol,
|
|
134
|
+
MessagePriority,
|
|
135
|
+
ProtocolEvent,
|
|
136
|
+
MessageReceivedEvent,
|
|
137
|
+
MessageDeliveredEvent,
|
|
138
|
+
NeighborDiscoveredEvent,
|
|
139
|
+
} from '@offline-protocol/mesh-sdk';
|
|
140
|
+
|
|
141
|
+
// 1. CREATE PROTOCOL INSTANCE
|
|
142
|
+
const protocol = new OfflineProtocol({
|
|
143
|
+
appId: 'my-chat-app',
|
|
144
|
+
userId: 'alice-device-001',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 2. REGISTER EVENT LISTENERS (before starting)
|
|
148
|
+
|
|
149
|
+
// Track discovered peers
|
|
150
|
+
const discoveredPeers = new Map<string, number>(); // peerId -> rssi
|
|
151
|
+
|
|
152
|
+
protocol.on('neighbor_discovered', (event: NeighborDiscoveredEvent) => {
|
|
153
|
+
console.log(`[PEER FOUND] ${event.peer_id} via ${event.transport}, RSSI: ${event.rssi}`);
|
|
154
|
+
discoveredPeers.set(event.peer_id, event.rssi ?? -100);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
protocol.on('neighbor_lost', (event) => {
|
|
158
|
+
console.log(`[PEER LOST] ${event.peer_id}`);
|
|
159
|
+
discoveredPeers.delete(event.peer_id);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Track outgoing messages
|
|
163
|
+
const pendingMessages = new Map<string, { recipient: string; content: string }>();
|
|
164
|
+
|
|
165
|
+
protocol.on('message_sent', (event) => {
|
|
166
|
+
console.log(`[SENT] Message ${event.message_id} to ${event.recipient}`);
|
|
167
|
+
pendingMessages.set(event.message_id, {
|
|
168
|
+
recipient: event.recipient,
|
|
169
|
+
content: event.content,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
protocol.on('message_delivered', (event: MessageDeliveredEvent) => {
|
|
174
|
+
console.log(`[DELIVERED] Message ${event.message_id} in ${event.latency_ms}ms, ${event.hop_count} hops`);
|
|
175
|
+
pendingMessages.delete(event.message_id);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
protocol.on('message_failed', (event) => {
|
|
179
|
+
console.log(`[FAILED] Message ${event.message_id}: ${event.reason} (${event.retry_count} retries)`);
|
|
180
|
+
pendingMessages.delete(event.message_id);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Handle incoming messages
|
|
184
|
+
protocol.on('message_received', (event: MessageReceivedEvent) => {
|
|
185
|
+
console.log(`[RECEIVED] From ${event.sender}: ${event.content}`);
|
|
186
|
+
console.log(` - Message ID: ${event.message_id}`);
|
|
187
|
+
console.log(` - Hop count: ${event.hop_count}`);
|
|
188
|
+
console.log(` - Transport: ${event.transport}`);
|
|
189
|
+
|
|
190
|
+
// Process the message in your app
|
|
191
|
+
handleIncomingMessage(event);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Monitor transport changes
|
|
195
|
+
protocol.on('transport_switched', (event) => {
|
|
196
|
+
console.log(`[TRANSPORT] Switched from ${event.from} to ${event.to}: ${event.reason}`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// 3. START THE PROTOCOL
|
|
200
|
+
await protocol.start();
|
|
201
|
+
// At this point:
|
|
202
|
+
// - BLE scanning begins (discovers nearby devices)
|
|
203
|
+
// - BLE advertising begins (makes this device discoverable)
|
|
204
|
+
// - neighbor_discovered events will start firing as peers are found
|
|
205
|
+
|
|
206
|
+
// 4. WAIT FOR PEERS (optional helper)
|
|
207
|
+
async function waitForPeer(peerId: string, timeoutMs = 30000): Promise<boolean> {
|
|
208
|
+
if (discoveredPeers.has(peerId)) return true;
|
|
209
|
+
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
const timeout = setTimeout(() => resolve(false), timeoutMs);
|
|
212
|
+
|
|
213
|
+
const handler = (event: NeighborDiscoveredEvent) => {
|
|
214
|
+
if (event.peer_id === peerId) {
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
protocol.off('neighbor_discovered', handler);
|
|
217
|
+
resolve(true);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
protocol.on('neighbor_discovered', handler);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 5. SEND A MESSAGE
|
|
226
|
+
async function sendChatMessage(recipientId: string, text: string) {
|
|
227
|
+
try {
|
|
228
|
+
const messageId = await protocol.sendMessage({
|
|
229
|
+
recipient: recipientId,
|
|
230
|
+
content: text,
|
|
231
|
+
priority: MessagePriority.High,
|
|
232
|
+
});
|
|
233
|
+
console.log(`Message queued with ID: ${messageId}`);
|
|
234
|
+
return messageId;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Failed to send message:', error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 6. CLEANUP ON APP EXIT
|
|
242
|
+
async function cleanup() {
|
|
243
|
+
await protocol.stop();
|
|
244
|
+
await protocol.destroy();
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Event Sequence Timeline
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
252
|
+
│ PROTOCOL LIFECYCLE │
|
|
253
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
254
|
+
│ │
|
|
255
|
+
│ new OfflineProtocol(config) │
|
|
256
|
+
│ │ │
|
|
257
|
+
│ ▼ │
|
|
258
|
+
│ protocol.on('...', handler) ← Register all event listeners │
|
|
259
|
+
│ │ │
|
|
260
|
+
│ ▼ │
|
|
261
|
+
│ await protocol.start() │
|
|
262
|
+
│ │ │
|
|
263
|
+
│ ├──► BLE advertising starts (device becomes discoverable) │
|
|
264
|
+
│ ├──► BLE scanning starts (looking for other devices) │
|
|
265
|
+
│ │ │
|
|
266
|
+
│ ▼ │
|
|
267
|
+
│ ┌─────────────────────────────────────────────────────────────┐ │
|
|
268
|
+
│ │ PEER DISCOVERY PHASE │ │
|
|
269
|
+
│ │ │ │
|
|
270
|
+
│ │ neighbor_discovered { peer_id, transport, rssi } │ │
|
|
271
|
+
│ │ neighbor_discovered { peer_id, transport, rssi } │ │
|
|
272
|
+
│ │ ... │ │
|
|
273
|
+
│ │ │ │
|
|
274
|
+
│ │ MeshController evaluates peers, establishes connections │ │
|
|
275
|
+
│ │ (MEMBER for same cluster, BRIDGE for different clusters) │ │
|
|
276
|
+
│ └─────────────────────────────────────────────────────────────┘ │
|
|
277
|
+
│ │ │
|
|
278
|
+
│ ▼ │
|
|
279
|
+
│ ┌─────────────────────────────────────────────────────────────┐ │
|
|
280
|
+
│ │ MESSAGING PHASE │ │
|
|
281
|
+
│ │ │ │
|
|
282
|
+
│ │ protocol.sendMessage({ recipient, content, priority }) │ │
|
|
283
|
+
│ │ │ │ │
|
|
284
|
+
│ │ ▼ │ │
|
|
285
|
+
│ │ message_sent { message_id, recipient, content, ... } │ │
|
|
286
|
+
│ │ │ │ │
|
|
287
|
+
│ │ ├──► [SUCCESS] message_delivered { message_id, ... } │ │
|
|
288
|
+
│ │ │ │ │
|
|
289
|
+
│ │ └──► [FAILURE] message_failed { message_id, reason } │ │
|
|
290
|
+
│ │ │ │
|
|
291
|
+
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
|
|
292
|
+
│ │ │ │
|
|
293
|
+
│ │ INCOMING: message_received { sender, content, ... } │ │
|
|
294
|
+
│ └─────────────────────────────────────────────────────────────┘ │
|
|
295
|
+
│ │ │
|
|
296
|
+
│ │ (peers may come and go) │
|
|
297
|
+
│ │ │
|
|
298
|
+
│ ▼ │
|
|
299
|
+
│ neighbor_lost { peer_id } │
|
|
300
|
+
│ neighbor_discovered { peer_id, ... } ← new peer appears │
|
|
301
|
+
│ │ │
|
|
302
|
+
│ ▼ │
|
|
303
|
+
│ await protocol.stop() │
|
|
304
|
+
│ │ │
|
|
305
|
+
│ ├──► BLE scanning stops │
|
|
306
|
+
│ ├──► BLE advertising stops │
|
|
307
|
+
│ ├──► All connections closed │
|
|
308
|
+
│ │ │
|
|
309
|
+
│ ▼ │
|
|
310
|
+
│ await protocol.destroy() ← Clean up resources │
|
|
311
|
+
│ │
|
|
312
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### What Happens Under the Hood
|
|
316
|
+
|
|
317
|
+
#### On `protocol.start()`
|
|
318
|
+
|
|
319
|
+
1. **Protocol core starts** in Rust
|
|
320
|
+
2. **BLE Manager initializes**:
|
|
321
|
+
- Starts scanning for devices advertising the Offline Protocol service UUID
|
|
322
|
+
- Starts advertising this device with mesh metadata (degree, free slots, battery, uptime)
|
|
323
|
+
3. **Process timer starts** - polls for outgoing fragments every 100ms
|
|
324
|
+
|
|
325
|
+
#### On Peer Discovery
|
|
326
|
+
|
|
327
|
+
1. **BLE scan detects advertisement** from another device
|
|
328
|
+
2. **MeshController.shouldInitiateOutbound()** evaluates the candidate:
|
|
329
|
+
- Checks connection budget (default max: 4)
|
|
330
|
+
- Calculates peer score (RSSI, availability, battery, uptime, stability, load)
|
|
331
|
+
- Determines if this is a cluster bridge opportunity
|
|
332
|
+
3. **If accepted**: BLE connection established, `neighbor_discovered` fires
|
|
333
|
+
4. **If at capacity**: May evict a lower-scoring peer to make room
|
|
334
|
+
|
|
335
|
+
#### On `protocol.sendMessage()`
|
|
336
|
+
|
|
337
|
+
1. **Message created** with unique ID, TTL, timestamp, priority
|
|
338
|
+
2. **message_sent event** fires immediately
|
|
339
|
+
3. **Message queued** for transmission
|
|
340
|
+
4. **DORS selects transport** (BLE, WiFi Direct, or Internet)
|
|
341
|
+
5. **Message sent** to connected peers
|
|
342
|
+
6. **ACK tracking begins** (default 5s timeout)
|
|
343
|
+
7. **On ACK received**: `message_delivered` event fires
|
|
344
|
+
8. **On timeout/max retries**: `message_failed` event fires
|
|
345
|
+
|
|
346
|
+
#### On Incoming Message
|
|
347
|
+
|
|
348
|
+
1. **BLE fragment received** from peer
|
|
349
|
+
2. **Deduplication check** - skip if message ID already seen
|
|
350
|
+
3. **If addressed to this device**: `message_received` event fires
|
|
351
|
+
4. **ACK sent back** to sender
|
|
352
|
+
5. **Hop count incremented** for metrics
|
|
353
|
+
|
|
354
|
+
#### On `protocol.stop()`
|
|
355
|
+
|
|
356
|
+
1. **BLE Manager stops** scanning and advertising
|
|
357
|
+
2. **All peer connections closed**
|
|
358
|
+
3. **neighbor_lost events** fire for each disconnected peer
|
|
359
|
+
4. **Protocol core stops**
|
|
360
|
+
|
|
361
|
+
### Diagnostic Events
|
|
362
|
+
|
|
363
|
+
The SDK emits diagnostic events for debugging:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
protocol.on('diagnostic', (event) => {
|
|
367
|
+
console.log(`[${event.level.toUpperCase()}] ${event.message}`, event.context);
|
|
368
|
+
});
|
|
50
369
|
```
|
|
51
370
|
|
|
371
|
+
---
|
|
372
|
+
|
|
52
373
|
## Configuration
|
|
53
374
|
|
|
375
|
+
### ProtocolConfig
|
|
376
|
+
|
|
54
377
|
```typescript
|
|
55
378
|
interface ProtocolConfig {
|
|
56
379
|
appId: string;
|
|
57
380
|
userId: string;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
381
|
+
transports?: TransportsConfig;
|
|
382
|
+
dors?: DorsConfig;
|
|
383
|
+
network?: NetworkConfig;
|
|
384
|
+
reliability?: ReliabilityConfig;
|
|
385
|
+
fileTransfer?: FileTransferConfig;
|
|
386
|
+
path?: PathConfig;
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### TransportsConfig
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
interface TransportsConfig {
|
|
394
|
+
ble?: {
|
|
395
|
+
enabled: boolean; // default: true
|
|
62
396
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
397
|
+
internet?: {
|
|
398
|
+
enabled: boolean; // default: false
|
|
399
|
+
serverAddress?: string; // WebSocket URL
|
|
400
|
+
autoReconnect?: boolean; // default: true
|
|
401
|
+
reconnectDelay?: number; // ms
|
|
67
402
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
403
|
+
wifiDirect?: {
|
|
404
|
+
enabled: boolean; // default: false (Android only)
|
|
405
|
+
deviceName?: string;
|
|
406
|
+
autoAccept?: boolean;
|
|
407
|
+
groupOwnerIntent?: number; // 0-15
|
|
72
408
|
};
|
|
73
|
-
|
|
74
|
-
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### DorsConfig
|
|
413
|
+
|
|
414
|
+
Controls transport switching behavior.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
interface DorsConfig {
|
|
418
|
+
preferOnline?: boolean; // default: false
|
|
419
|
+
switchHysteresis?: number; // default: 15.0
|
|
420
|
+
switchCooldownSecs?: number; // default: 20
|
|
421
|
+
bleToWifiRetryThreshold?: number; // default: 2
|
|
422
|
+
rssiSwitchThreshold?: number; // default: -85 dBm
|
|
423
|
+
congestionQueueThreshold?: number; // default: 50
|
|
424
|
+
stabilityWindowSecs?: number; // default: 8
|
|
425
|
+
poorSignalDurationSecs?: number; // default: 10
|
|
426
|
+
ttlEscalationThreshold?: number; // default: 2
|
|
427
|
+
congestionDurationSecs?: number; // default: 10
|
|
428
|
+
ttlEscalationHoldSecs?: number; // default: 20
|
|
429
|
+
historyWindowSize?: number; // default: 10
|
|
430
|
+
queueRecoveryRatio?: number; // default: 0.5
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
### NetworkConfig
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
interface NetworkConfig {
|
|
439
|
+
initialTtl?: number; // default: 8
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### ReliabilityConfig
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
interface ReliabilityConfig {
|
|
447
|
+
ack?: {
|
|
448
|
+
defaultTimeoutMs?: number; // default: 5000
|
|
449
|
+
maxPendingAcks?: number; // default: 1000
|
|
75
450
|
};
|
|
451
|
+
retry?: {
|
|
452
|
+
maxRetries?: number; // default: 5
|
|
453
|
+
initialDelayMs?: number; // default: 1000
|
|
454
|
+
maxDelayMs?: number; // default: 30000
|
|
455
|
+
backoffMultiplier?: number; // default: 2.0
|
|
456
|
+
outboxMaxLifetimeMs?: number; // default: 3600000
|
|
457
|
+
};
|
|
458
|
+
dedup?: {
|
|
459
|
+
maxTrackedMessages?: number; // default: 10000
|
|
460
|
+
retentionTimeSecs?: number; // default: 3600
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### PathConfig
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
interface PathConfig {
|
|
469
|
+
forwardToTopK?: number; // default: 3
|
|
470
|
+
maxCongestionLevel?: number; // default: 0.8
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### FileTransferConfig
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
interface FileTransferConfig {
|
|
478
|
+
chunkSize?: number; // default: 32768 (32KB)
|
|
479
|
+
maxFileSize?: number; // default: 104857600 (100MB)
|
|
76
480
|
}
|
|
77
481
|
```
|
|
78
482
|
|
|
79
|
-
|
|
483
|
+
---
|
|
80
484
|
|
|
81
|
-
|
|
485
|
+
## API Reference
|
|
82
486
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- `destroy(): Promise<void>` - Clean up resources
|
|
487
|
+
### Constructor
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
new OfflineProtocol(config: ProtocolConfig)
|
|
491
|
+
```
|
|
89
492
|
|
|
90
|
-
###
|
|
493
|
+
### Lifecycle Methods
|
|
91
494
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
495
|
+
| Method | Returns | Description |
|
|
496
|
+
|--------|---------|-------------|
|
|
497
|
+
| `start()` | `Promise<void>` | Start the protocol and all enabled transports |
|
|
498
|
+
| `stop()` | `Promise<void>` | Stop the protocol and disconnect all peers |
|
|
499
|
+
| `pause()` | `Promise<void>` | Pause for background mode |
|
|
500
|
+
| `resume()` | `Promise<void>` | Resume from paused state |
|
|
501
|
+
| `destroy()` | `Promise<void>` | Clean up all resources |
|
|
502
|
+
| `getState()` | `Promise<ProtocolState>` | Get current state (`Stopped`, `Running`, `Paused`) |
|
|
97
503
|
|
|
98
|
-
|
|
99
|
-
- `transport_switched` - Transport changed (BLE/WiFi/Internet)
|
|
100
|
-
- `neighbor_discovered` - New neighbor found
|
|
101
|
-
- `neighbor_lost` - Neighbor disconnected
|
|
504
|
+
### Messaging
|
|
102
505
|
|
|
103
|
-
|
|
506
|
+
| Method | Returns | Description |
|
|
507
|
+
|--------|---------|-------------|
|
|
508
|
+
| `sendMessage(params: SendMessageParams)` | `Promise<string>` | Send message, returns message ID |
|
|
509
|
+
| `receiveMessage()` | `Promise<MessageReceivedEvent \| null>` | Poll for next received message |
|
|
104
510
|
|
|
105
511
|
```typescript
|
|
512
|
+
interface SendMessageParams {
|
|
513
|
+
recipient: string;
|
|
514
|
+
content: string;
|
|
515
|
+
priority?: MessagePriority; // default: Medium
|
|
516
|
+
}
|
|
517
|
+
|
|
106
518
|
enum MessagePriority {
|
|
107
519
|
Low = 0,
|
|
108
520
|
Medium = 1,
|
|
@@ -111,85 +523,530 @@ enum MessagePriority {
|
|
|
111
523
|
}
|
|
112
524
|
```
|
|
113
525
|
|
|
114
|
-
|
|
526
|
+
### Transport Management
|
|
527
|
+
|
|
528
|
+
| Method | Returns | Description |
|
|
529
|
+
|--------|---------|-------------|
|
|
530
|
+
| `getActiveTransports()` | `Promise<TransportType[]>` | Get list of active transports |
|
|
531
|
+
| `enableTransport(type, config?)` | `Promise<void>` | Enable a transport |
|
|
532
|
+
| `disableTransport(type)` | `Promise<void>` | Disable a transport |
|
|
533
|
+
| `forceTransport(type)` | `Promise<void>` | Force specific transport (override DORS) |
|
|
534
|
+
| `releaseTransportLock()` | `Promise<void>` | Release forced transport, let DORS decide |
|
|
535
|
+
| `getTransportMetrics(type)` | `Promise<TransportMetrics \| null>` | Get transport statistics |
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
type TransportType = 'ble' | 'internet' | 'wifiDirect';
|
|
539
|
+
|
|
540
|
+
interface TransportMetrics {
|
|
541
|
+
packetsSent: number;
|
|
542
|
+
packetsReceived: number;
|
|
543
|
+
bytesSent: number;
|
|
544
|
+
bytesReceived: number;
|
|
545
|
+
errorRate: number;
|
|
546
|
+
avgLatencyMs: number;
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Bluetooth
|
|
551
|
+
|
|
552
|
+
| Method | Returns | Description |
|
|
553
|
+
|--------|---------|-------------|
|
|
554
|
+
| `isBluetoothEnabled()` | `Promise<boolean>` | Check if Bluetooth is enabled |
|
|
555
|
+
| `requestEnableBluetooth()` | `Promise<boolean>` | Request to enable Bluetooth (Android only) |
|
|
556
|
+
| `getBLePeerCount()` | `Promise<number>` | Get number of discovered BLE peers |
|
|
557
|
+
|
|
558
|
+
### Network Topology
|
|
559
|
+
|
|
560
|
+
| Method | Returns | Description |
|
|
561
|
+
|--------|---------|-------------|
|
|
562
|
+
| `getTopology()` | `Promise<NetworkTopology>` | Get network topology snapshot |
|
|
563
|
+
| `getMessageStats()` | `Promise<MessageDeliveryStats[]>` | Get message delivery statistics |
|
|
564
|
+
| `getDeliverySuccessRate()` | `Promise<number>` | Get delivery success rate (0-1) |
|
|
565
|
+
| `getMedianLatency()` | `Promise<number \| null>` | Get median latency in ms |
|
|
566
|
+
| `getMedianHops()` | `Promise<number \| null>` | Get median hop count |
|
|
567
|
+
|
|
568
|
+
### Battery
|
|
115
569
|
|
|
116
|
-
|
|
570
|
+
| Method | Returns | Description |
|
|
571
|
+
|--------|---------|-------------|
|
|
572
|
+
| `setBatteryLevel(level)` | `Promise<void>` | Set battery level (0-100) for mesh decisions |
|
|
573
|
+
| `getBatteryLevel()` | `Promise<number \| null>` | Get current battery level |
|
|
117
574
|
|
|
118
|
-
DORS
|
|
119
|
-
- Signal strength (RSSI)
|
|
120
|
-
- Bandwidth and congestion
|
|
121
|
-
- Energy efficiency
|
|
122
|
-
- Reliability and proximity
|
|
575
|
+
### DORS Configuration
|
|
123
576
|
|
|
124
|
-
|
|
577
|
+
| Method | Returns | Description |
|
|
578
|
+
|--------|---------|-------------|
|
|
579
|
+
| `updateDorsConfig(config)` | `Promise<void>` | Update DORS settings at runtime |
|
|
580
|
+
| `getDorsConfig()` | `Promise<DorsConfig>` | Get current DORS configuration |
|
|
581
|
+
| `shouldEscalateToWifi()` | `Promise<boolean>` | Check if DORS recommends WiFi escalation |
|
|
125
582
|
|
|
126
|
-
|
|
583
|
+
### Reliability Configuration
|
|
127
584
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
585
|
+
| Method | Returns | Description |
|
|
586
|
+
|--------|---------|-------------|
|
|
587
|
+
| `updateAckConfig(config)` | `Promise<void>` | Update ACK settings |
|
|
588
|
+
| `updateRetryConfig(config)` | `Promise<void>` | Update retry settings |
|
|
589
|
+
| `updateDedupConfig(config)` | `Promise<void>` | Update deduplication settings |
|
|
590
|
+
| `getDedupStats()` | `Promise<DedupStats>` | Get deduplication statistics |
|
|
591
|
+
| `getPendingAckCount()` | `Promise<number>` | Get pending ACK count |
|
|
592
|
+
| `getRetryQueueSize()` | `Promise<number>` | Get retry queue size |
|
|
132
593
|
|
|
133
|
-
|
|
134
|
-
1. Devices discover each other via BLE advertisements containing mesh metadata
|
|
135
|
-
2. MeshController evaluates connection candidates based on:
|
|
136
|
-
- Available connection slots
|
|
137
|
-
- Peer scores (RSSI, battery, uptime, stability, load)
|
|
138
|
-
- Cluster membership and free slot estimates
|
|
139
|
-
3. Connection intent determines role:
|
|
140
|
-
- `INTRA_CLUSTER` → MEMBER role (same cluster)
|
|
141
|
-
- `INTER_CLUSTER` → BRIDGE role (different clusters)
|
|
142
|
-
4. When connection budget is full, the system can evict the worst peer to make room for better connections
|
|
594
|
+
### Gradient Routing
|
|
143
595
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
596
|
+
| Method | Returns | Description |
|
|
597
|
+
|--------|---------|-------------|
|
|
598
|
+
| `learnRoute(destination, nextHop, hopCount, quality)` | `Promise<void>` | Learn a route from incoming message |
|
|
599
|
+
| `getBestRoute(destination)` | `Promise<RouteEntry \| null>` | Get best route to destination |
|
|
600
|
+
| `getAllRoutes(destination)` | `Promise<RouteEntry[]>` | Get all routes to destination |
|
|
601
|
+
| `hasRoute(destination)` | `Promise<boolean>` | Check if route exists |
|
|
602
|
+
| `removeNeighborRoutes(neighborId)` | `Promise<void>` | Remove routes through neighbor |
|
|
603
|
+
| `cleanupExpiredRoutes()` | `Promise<void>` | Clean up expired routes |
|
|
604
|
+
| `getRoutingStats()` | `Promise<RoutingStats>` | Get routing table statistics |
|
|
605
|
+
| `updateRoutingConfig(config)` | `Promise<void>` | Update routing configuration |
|
|
149
606
|
|
|
150
|
-
|
|
607
|
+
### Event Listeners
|
|
608
|
+
|
|
609
|
+
| Method | Returns | Description |
|
|
610
|
+
|--------|---------|-------------|
|
|
611
|
+
| `on(eventType, listener)` | `this` | Register event listener |
|
|
612
|
+
| `off(eventType, listener)` | `this` | Remove event listener |
|
|
613
|
+
| `once(eventType, listener)` | `this` | Register one-time listener |
|
|
614
|
+
| `removeAllListeners(eventType?)` | `this` | Remove all listeners |
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## Events
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
### Message Events
|
|
622
|
+
|
|
623
|
+
#### message_sent
|
|
151
624
|
|
|
152
625
|
```typescript
|
|
153
|
-
|
|
154
|
-
|
|
626
|
+
interface MessageSentEvent {
|
|
627
|
+
type: 'message_sent';
|
|
628
|
+
message_id: string;
|
|
629
|
+
sender: string;
|
|
630
|
+
recipient: string;
|
|
631
|
+
content: string;
|
|
632
|
+
priority: 'low' | 'medium' | 'high' | 'critical';
|
|
633
|
+
requires_ack: boolean;
|
|
634
|
+
timestamp: number;
|
|
635
|
+
}
|
|
636
|
+
```
|
|
155
637
|
|
|
156
|
-
|
|
157
|
-
const [protocol, setProtocol] = useState(null);
|
|
158
|
-
const [messages, setMessages] = useState([]);
|
|
638
|
+
#### message_received
|
|
159
639
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
640
|
+
```typescript
|
|
641
|
+
interface MessageReceivedEvent {
|
|
642
|
+
type: 'message_received';
|
|
643
|
+
message_id: string;
|
|
644
|
+
sender: string;
|
|
645
|
+
recipient: string;
|
|
646
|
+
content: string;
|
|
647
|
+
hop_count: number;
|
|
648
|
+
transport: string;
|
|
649
|
+
timestamp: number;
|
|
650
|
+
}
|
|
651
|
+
```
|
|
165
652
|
|
|
166
|
-
|
|
167
|
-
if (event.sender === recipientId) {
|
|
168
|
-
setMessages((prev) => [...prev, {
|
|
169
|
-
id: event.message_id,
|
|
170
|
-
text: event.content,
|
|
171
|
-
sender: event.sender,
|
|
172
|
-
timestamp: event.timestamp,
|
|
173
|
-
}]);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
653
|
+
#### message_delivered
|
|
176
654
|
|
|
177
|
-
|
|
178
|
-
|
|
655
|
+
```typescript
|
|
656
|
+
interface MessageDeliveredEvent {
|
|
657
|
+
type: 'message_delivered';
|
|
658
|
+
message_id: string;
|
|
659
|
+
latency_ms: number;
|
|
660
|
+
hop_count: number;
|
|
661
|
+
transport: string;
|
|
662
|
+
}
|
|
663
|
+
```
|
|
179
664
|
|
|
180
|
-
|
|
181
|
-
}, [userId, recipientId]);
|
|
665
|
+
#### message_failed
|
|
182
666
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
667
|
+
```typescript
|
|
668
|
+
interface MessageFailedEvent {
|
|
669
|
+
type: 'message_failed';
|
|
670
|
+
message_id: string;
|
|
671
|
+
reason: string;
|
|
672
|
+
retry_count: number;
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Network Events
|
|
677
|
+
|
|
678
|
+
#### transport_switched
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
interface TransportSwitchedEvent {
|
|
682
|
+
type: 'transport_switched';
|
|
683
|
+
from: string | null;
|
|
684
|
+
to: string;
|
|
685
|
+
reason: string;
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
#### neighbor_discovered
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
interface NeighborDiscoveredEvent {
|
|
693
|
+
type: 'neighbor_discovered';
|
|
694
|
+
peer_id: string;
|
|
695
|
+
transport: string;
|
|
696
|
+
rssi?: number;
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
#### neighbor_lost
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
interface NeighborLostEvent {
|
|
704
|
+
type: 'neighbor_lost';
|
|
705
|
+
peer_id: string;
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
#### network_metrics
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
interface NetworkMetricsEvent {
|
|
713
|
+
type: 'network_metrics';
|
|
714
|
+
neighbor_count: number;
|
|
715
|
+
relay_count: number;
|
|
716
|
+
delivery_ratio: number;
|
|
717
|
+
avg_latency_ms: number;
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
### File Events
|
|
723
|
+
|
|
724
|
+
#### file_progress
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
interface FileProgressEvent {
|
|
728
|
+
type: 'file_progress';
|
|
729
|
+
file_id: string;
|
|
730
|
+
chunks_sent: number;
|
|
731
|
+
total_chunks: number;
|
|
732
|
+
percentage: number;
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
#### file_received
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
interface FileReceivedEvent {
|
|
740
|
+
type: 'file_received';
|
|
741
|
+
file_id: string;
|
|
742
|
+
file_name: string;
|
|
743
|
+
file_size: number;
|
|
744
|
+
sender: string;
|
|
745
|
+
}
|
|
746
|
+
```
|
|
192
747
|
|
|
193
|
-
|
|
748
|
+
### Diagnostic Events
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
interface DiagnosticEvent {
|
|
752
|
+
type: 'diagnostic';
|
|
753
|
+
level: 'info' | 'warning' | 'error';
|
|
754
|
+
message: string;
|
|
755
|
+
context?: Record<string, unknown>;
|
|
194
756
|
}
|
|
195
757
|
```
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Types
|
|
762
|
+
|
|
763
|
+
### NetworkTopology
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
interface NetworkTopology {
|
|
767
|
+
timestamp: number;
|
|
768
|
+
local_user_id: string;
|
|
769
|
+
nodes: NetworkNode[];
|
|
770
|
+
links: NetworkLink[];
|
|
771
|
+
stats: NetworkStats;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
interface NetworkNode {
|
|
775
|
+
user_id: string;
|
|
776
|
+
role: string; // 'Normal' or 'Relay' from topology API
|
|
777
|
+
connection_count: number;
|
|
778
|
+
battery_level?: number;
|
|
779
|
+
last_seen: number;
|
|
780
|
+
transports: TransportType[];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
interface NetworkLink {
|
|
784
|
+
from: string;
|
|
785
|
+
to: string;
|
|
786
|
+
quality: number; // 0.0 - 1.0
|
|
787
|
+
transport: TransportType;
|
|
788
|
+
rssi?: number;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
interface NetworkStats {
|
|
792
|
+
total_nodes: number;
|
|
793
|
+
relay_nodes: number; // Count of nodes with 'Relay' role in topology
|
|
794
|
+
total_connections: number;
|
|
795
|
+
avg_link_quality: number;
|
|
796
|
+
network_diameter?: number;
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### MessageDeliveryStats
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
interface MessageDeliveryStats {
|
|
804
|
+
message_id: string;
|
|
805
|
+
sender: string;
|
|
806
|
+
recipient: string;
|
|
807
|
+
sent_at: number;
|
|
808
|
+
delivered_at?: number;
|
|
809
|
+
hop_count: number;
|
|
810
|
+
transport?: TransportType;
|
|
811
|
+
retry_count: number;
|
|
812
|
+
latency_ms?: number;
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### RouteEntry
|
|
817
|
+
|
|
818
|
+
```typescript
|
|
819
|
+
interface RouteEntry {
|
|
820
|
+
nextHop: string;
|
|
821
|
+
hopCount: number;
|
|
822
|
+
quality: number; // 0.0 - 1.0
|
|
823
|
+
lastSeenMs: number;
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### RoutingStats
|
|
828
|
+
|
|
829
|
+
```typescript
|
|
830
|
+
interface RoutingStats {
|
|
831
|
+
destinationCount: number;
|
|
832
|
+
routeCount: number;
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### DedupStats
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
interface DedupStats {
|
|
840
|
+
totalTracked: number;
|
|
841
|
+
recentTracked: number;
|
|
842
|
+
capacityUsedPercent: number;
|
|
843
|
+
mode: 'HashMap' | 'BloomFilter';
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### FileProgress
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
interface FileProgress {
|
|
851
|
+
file_id: string;
|
|
852
|
+
file_name: string;
|
|
853
|
+
file_size: number;
|
|
854
|
+
chunks_completed: number;
|
|
855
|
+
total_chunks: number;
|
|
856
|
+
percentage: number;
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### ProtocolState
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
enum ProtocolState {
|
|
864
|
+
Stopped = 0,
|
|
865
|
+
Running = 1,
|
|
866
|
+
Paused = 2,
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## DORS (Dynamic Offline Relay Switch)
|
|
873
|
+
|
|
874
|
+
DORS automatically selects the optimal transport based on real-time conditions.
|
|
875
|
+
|
|
876
|
+
### Scoring Factors
|
|
877
|
+
|
|
878
|
+
| Factor | Description |
|
|
879
|
+
|--------|-------------|
|
|
880
|
+
| Signal Strength | RSSI for BLE/WiFi (-50 to -100 dBm) |
|
|
881
|
+
| Proximity | Hop count to destination |
|
|
882
|
+
| Bandwidth | Transport throughput capability |
|
|
883
|
+
| Congestion | Queue depth and backlog |
|
|
884
|
+
| Energy | Battery impact of transport |
|
|
885
|
+
| Reliability | Historical delivery success rate |
|
|
886
|
+
| Load | Current processing capacity |
|
|
887
|
+
|
|
888
|
+
### Transport Weights
|
|
889
|
+
|
|
890
|
+
**BLE**: Optimized for energy efficiency and mesh scenarios
|
|
891
|
+
- Signal: 30%, Energy: 30%, Congestion: 15%, Proximity: 15%
|
|
892
|
+
|
|
893
|
+
**WiFi Direct**: Optimized for high throughput
|
|
894
|
+
- Bandwidth: 35%, Proximity: 20%, Congestion: 20%, Reliability: 15%
|
|
895
|
+
|
|
896
|
+
**Internet**: Optimized for server connectivity
|
|
897
|
+
- Bandwidth: 35%, Reliability: 30%, Congestion: 15%, Energy: 10%
|
|
898
|
+
|
|
899
|
+
### Switching Safeguards
|
|
900
|
+
|
|
901
|
+
| Safeguard | Default | Description |
|
|
902
|
+
|-----------|---------|-------------|
|
|
903
|
+
| Hysteresis | 15 points | Minimum score improvement to switch |
|
|
904
|
+
| Cooldown | 20 seconds | Wait time between switches |
|
|
905
|
+
| Stability Window | 8 seconds | Transport must be stable before switching |
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## Mesh Networking
|
|
910
|
+
|
|
911
|
+
### Cluster Architecture
|
|
912
|
+
|
|
913
|
+
Devices organize into **clusters** (groups of nearby connected peers). Connections between clusters are handled by **bridge** connections.
|
|
914
|
+
|
|
915
|
+
**Connection Roles:**
|
|
916
|
+
- `MEMBER` - Intra-cluster connection (devices in same neighborhood)
|
|
917
|
+
- `BRIDGE` - Inter-cluster connection (bridges different neighborhoods)
|
|
918
|
+
|
|
919
|
+
### How It Works
|
|
920
|
+
|
|
921
|
+
1. **Discovery**: Devices broadcast BLE advertisements with mesh metadata (degree, free slots, battery, uptime)
|
|
922
|
+
2. **Cluster Detection**: Each device computes a cluster signature from connected peer hashes
|
|
923
|
+
3. **Connection Decisions**: MeshController evaluates candidates - prioritizes bridging different clusters
|
|
924
|
+
4. **Rebalancing**: Periodically swaps lower-quality peers for better candidates or bridge opportunities
|
|
925
|
+
5. **Delivery**: Messages sent to connected peers
|
|
926
|
+
|
|
927
|
+
### Connection Budget
|
|
928
|
+
|
|
929
|
+
- Default: 4 connections per device
|
|
930
|
+
- Minimum: 1 connection maintained
|
|
931
|
+
- Connections are scored and rebalanced every ~15 seconds
|
|
932
|
+
- Bridge candidates get priority when clusters need unifying
|
|
933
|
+
|
|
934
|
+
### Peer Scoring
|
|
935
|
+
|
|
936
|
+
| Factor | Weight | Description |
|
|
937
|
+
|--------|--------|-------------|
|
|
938
|
+
| RSSI | 35% | Signal strength to peer |
|
|
939
|
+
| Availability | 20% | Free connection slots |
|
|
940
|
+
| Uptime | 15% | How long peer has been active |
|
|
941
|
+
| Battery | 15% | Peer's battery level |
|
|
942
|
+
| Stability | 10% | Connection reliability history |
|
|
943
|
+
| Load | 5% | Current processing load |
|
|
944
|
+
|
|
945
|
+
**Bridge Favor**: Candidates from different clusters get a score bonus (`bridgeFavor: 0.1`) to encourage network unification.
|
|
946
|
+
|
|
947
|
+
### Message TTL
|
|
948
|
+
|
|
949
|
+
- Default: 8 hops
|
|
950
|
+
- Messages are dropped when TTL reaches 0
|
|
951
|
+
- Prevents infinite message circulation
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
## Reliability Layer
|
|
956
|
+
|
|
957
|
+
### Acknowledgments
|
|
958
|
+
|
|
959
|
+
- Messages require ACK for delivery confirmation
|
|
960
|
+
- Default timeout: 5 seconds
|
|
961
|
+
- `message_delivered` event fires on ACK receipt
|
|
962
|
+
|
|
963
|
+
### Retry Queue
|
|
964
|
+
|
|
965
|
+
- Failed messages are retried with exponential backoff
|
|
966
|
+
- Initial delay: 1 second
|
|
967
|
+
- Maximum delay: 30 seconds
|
|
968
|
+
- Maximum retries: 5
|
|
969
|
+
|
|
970
|
+
### Deduplication
|
|
971
|
+
|
|
972
|
+
Prevents duplicate message processing:
|
|
973
|
+
|
|
974
|
+
- **Bloom Filter Mode**: Space-efficient, ~1% false positive rate
|
|
975
|
+
- **HashMap Mode**: Exact tracking, configurable capacity
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
## File Transfer
|
|
980
|
+
|
|
981
|
+
### Sending Files
|
|
982
|
+
|
|
983
|
+
```typescript
|
|
984
|
+
const fileId = await protocol.sendFile({
|
|
985
|
+
filePath: '/path/to/file.pdf',
|
|
986
|
+
recipient: 'user456',
|
|
987
|
+
fileName: 'document.pdf', // optional
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
protocol.on('file_progress', (event) => {
|
|
991
|
+
console.log(`${event.percentage}% complete`);
|
|
992
|
+
});
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Managing Transfers
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
const progress = await protocol.getFileProgress(fileId);
|
|
999
|
+
await protocol.cancelFileTransfer(fileId);
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
---
|
|
1003
|
+
|
|
1004
|
+
## Troubleshooting
|
|
1005
|
+
|
|
1006
|
+
### Messages Not Delivering
|
|
1007
|
+
|
|
1008
|
+
1. Verify both devices have protocol started
|
|
1009
|
+
2. Check they're within BLE range (~10-30m)
|
|
1010
|
+
3. Ensure TTL is sufficient for network size
|
|
1011
|
+
4. Monitor `message_failed` events for retry information
|
|
1012
|
+
|
|
1013
|
+
### No Peers Discovered
|
|
1014
|
+
|
|
1015
|
+
1. Verify Bluetooth is enabled: `await protocol.isBluetoothEnabled()`
|
|
1016
|
+
2. Check permissions are granted
|
|
1017
|
+
3. Ensure background modes enabled (iOS)
|
|
1018
|
+
4. Verify devices are within range
|
|
1019
|
+
|
|
1020
|
+
### Frequent Disconnections
|
|
1021
|
+
|
|
1022
|
+
1. Check signal strength via `neighbor_discovered` RSSI
|
|
1023
|
+
2. Increase `stabilityWindowSecs` in DORS config
|
|
1024
|
+
3. Reduce `rebalanceInterval` frequency
|
|
1025
|
+
4. Check for BLE interference
|
|
1026
|
+
|
|
1027
|
+
### High Battery Drain
|
|
1028
|
+
|
|
1029
|
+
1. Reduce connection count (native mesh config)
|
|
1030
|
+
2. Verify DORS is selecting BLE over WiFi Direct
|
|
1031
|
+
3. Check for excessive retry activity
|
|
1032
|
+
4. Use `setBatteryLevel()` to inform mesh decisions
|
|
1033
|
+
|
|
1034
|
+
### Transport Not Switching
|
|
1035
|
+
|
|
1036
|
+
1. Verify transport is enabled in config
|
|
1037
|
+
2. Check hysteresis threshold isn't too high
|
|
1038
|
+
3. Ensure cooldown period has elapsed
|
|
1039
|
+
4. Use `forceTransport()` to test manually
|
|
1040
|
+
|
|
1041
|
+
### Linking Error
|
|
1042
|
+
|
|
1043
|
+
If you see the linking error message:
|
|
1044
|
+
1. Run `pod install` (iOS)
|
|
1045
|
+
2. Rebuild the app after installing
|
|
1046
|
+
3. Verify not using Expo Go (native modules required)
|
|
1047
|
+
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
## License
|
|
1051
|
+
|
|
1052
|
+
ISC
|
|
@@ -113,6 +113,8 @@ class BleManager(
|
|
|
113
113
|
private var gattServer: BluetoothGattServer? = null
|
|
114
114
|
private var messageCharacteristic: BluetoothGattCharacteristic? = null
|
|
115
115
|
private var deviceIdCharacteristic: BluetoothGattCharacteristic? = null
|
|
116
|
+
@Volatile private var isGattServiceReady = false
|
|
117
|
+
@Volatile private var pendingAdvertiseReason: String? = null
|
|
116
118
|
|
|
117
119
|
// Connection registry keeps track of client/server links and desired roles.
|
|
118
120
|
private val connections = MeshConnectionRegistry()
|
|
@@ -412,6 +414,8 @@ class BleManager(
|
|
|
412
414
|
// Close GATT server
|
|
413
415
|
gattServer?.close()
|
|
414
416
|
gattServer = null
|
|
417
|
+
isGattServiceReady = false
|
|
418
|
+
pendingAdvertiseReason = null
|
|
415
419
|
|
|
416
420
|
updateState(TransportState.STOPPED)
|
|
417
421
|
protocol.bleStatusChanged(false)
|
|
@@ -508,6 +512,9 @@ class BleManager(
|
|
|
508
512
|
|
|
509
513
|
private fun setupGattServer() {
|
|
510
514
|
try {
|
|
515
|
+
// Reset flag - service registration is asynchronous
|
|
516
|
+
isGattServiceReady = false
|
|
517
|
+
|
|
511
518
|
gattServer = bluetoothManager.openGattServer(context, gattServerCallback)
|
|
512
519
|
|
|
513
520
|
// Create message characteristic (write without response + notify)
|
|
@@ -530,10 +537,11 @@ class BleManager(
|
|
|
530
537
|
service.addCharacteristic(messageCharacteristic)
|
|
531
538
|
service.addCharacteristic(deviceIdCharacteristic)
|
|
532
539
|
|
|
533
|
-
// Add service to GATT server
|
|
540
|
+
// Add service to GATT server (asynchronous - callback in onServiceAdded)
|
|
534
541
|
gattServer?.addService(service)
|
|
535
542
|
|
|
536
|
-
Log.i(TAG, "GATT server
|
|
543
|
+
Log.i(TAG, "GATT server setup initiated, waiting for service registration callback...")
|
|
544
|
+
emitDiagnostic("info", "GATT server setup initiated")
|
|
537
545
|
} catch (e: SecurityException) {
|
|
538
546
|
Log.e(TAG, "Permission denied while setting up GATT server", e)
|
|
539
547
|
throw e
|
|
@@ -644,6 +652,16 @@ class BleManager(
|
|
|
644
652
|
private fun startAdvertising(reason: String = "manual") {
|
|
645
653
|
if (isAdvertising) return
|
|
646
654
|
|
|
655
|
+
// Wait for GATT service to be ready before advertising
|
|
656
|
+
if (!isGattServiceReady) {
|
|
657
|
+
pendingAdvertiseReason = reason
|
|
658
|
+
if (logThrottler.shouldLog("advert_waiting_gatt", intervalMs = 5000)) {
|
|
659
|
+
Log.i(TAG, "Waiting for GATT service to be ready before advertising (reason: $reason)")
|
|
660
|
+
emitDiagnostic("info", "Waiting for GATT service registration", mapOf("reason" to reason))
|
|
661
|
+
}
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
|
|
647
665
|
try {
|
|
648
666
|
val settings = AdvertiseSettings.Builder()
|
|
649
667
|
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
@@ -1456,6 +1474,33 @@ class BleManager(
|
|
|
1456
1474
|
// MARK: - GATT Server Callback
|
|
1457
1475
|
|
|
1458
1476
|
private val gattServerCallback = object : BluetoothGattServerCallback() {
|
|
1477
|
+
override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
|
|
1478
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1479
|
+
Log.i(TAG, "✅ GATT service added successfully: ${service?.uuid}")
|
|
1480
|
+
emitDiagnostic("info", "GATT service registered successfully", mapOf(
|
|
1481
|
+
"serviceUUID" to (service?.uuid?.toString() ?: "unknown")
|
|
1482
|
+
))
|
|
1483
|
+
isGattServiceReady = true
|
|
1484
|
+
|
|
1485
|
+
// Start advertising now that the service is ready
|
|
1486
|
+
val reason = pendingAdvertiseReason
|
|
1487
|
+
if (reason != null) {
|
|
1488
|
+
pendingAdvertiseReason = null
|
|
1489
|
+
Log.i(TAG, "📡 Starting deferred advertising after GATT service ready")
|
|
1490
|
+
mainHandler.post {
|
|
1491
|
+
startAdvertising("gatt_service_ready")
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
Log.e(TAG, "❌ Error adding GATT service: status=$status")
|
|
1496
|
+
emitDiagnostic("error", "Error adding GATT service", mapOf(
|
|
1497
|
+
"status" to status,
|
|
1498
|
+
"serviceUUID" to (service?.uuid?.toString() ?: "unknown")
|
|
1499
|
+
))
|
|
1500
|
+
isGattServiceReady = false
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1459
1504
|
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
|
|
1460
1505
|
when (newState) {
|
|
1461
1506
|
BluetoothProfile.STATE_CONNECTED -> {
|
package/ios/BleManager.swift
CHANGED
|
@@ -121,6 +121,8 @@ public class BleManager: NSObject, TransportManager {
|
|
|
121
121
|
private var isAdvertising = false
|
|
122
122
|
private var centralReady = false
|
|
123
123
|
private var peripheralReady = false
|
|
124
|
+
private var isGattServiceReady = false
|
|
125
|
+
private var pendingAdvertiseAfterServiceReady = false
|
|
124
126
|
private var subscribedCentrals: Set<UUID> = []
|
|
125
127
|
private var lastMeshAdvertisement: MeshAdvertisementData?
|
|
126
128
|
|
|
@@ -397,6 +399,8 @@ public class BleManager: NSObject, TransportManager {
|
|
|
397
399
|
|
|
398
400
|
centralReady = false
|
|
399
401
|
peripheralReady = false
|
|
402
|
+
isGattServiceReady = false
|
|
403
|
+
pendingAdvertiseAfterServiceReady = false
|
|
400
404
|
|
|
401
405
|
updateState(.stopped)
|
|
402
406
|
emitDiagnostic("info", "BLE transport stopped")
|
|
@@ -705,6 +709,16 @@ public class BleManager: NSObject, TransportManager {
|
|
|
705
709
|
|
|
706
710
|
setupGattServer()
|
|
707
711
|
|
|
712
|
+
// Wait for GATT service to be ready before advertising
|
|
713
|
+
guard isGattServiceReady else {
|
|
714
|
+
pendingAdvertiseAfterServiceReady = true
|
|
715
|
+
if logThrottler.shouldLog(key: "advert_waiting_gatt", interval: 5) {
|
|
716
|
+
print("[BleManager] Waiting for GATT service to be ready before advertising (reason: \(reason))")
|
|
717
|
+
emitDiagnostic("info", "Waiting for GATT service registration", context: ["reason": reason])
|
|
718
|
+
}
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
|
|
708
722
|
let meshData = meshController.advertisement()
|
|
709
723
|
lastMeshAdvertisement = meshData
|
|
710
724
|
var advertisementData: [String: Any] = [
|
|
@@ -763,10 +777,13 @@ public class BleManager: NSObject, TransportManager {
|
|
|
763
777
|
|
|
764
778
|
private func setupGattServer() {
|
|
765
779
|
guard let peripheral = peripheralManager else { return }
|
|
766
|
-
if messageCharacteristic != nil && deviceIdCharacteristic != nil {
|
|
780
|
+
if messageCharacteristic != nil && deviceIdCharacteristic != nil && isGattServiceReady {
|
|
767
781
|
return
|
|
768
782
|
}
|
|
769
783
|
|
|
784
|
+
// Reset flag - service registration is asynchronous
|
|
785
|
+
isGattServiceReady = false
|
|
786
|
+
|
|
770
787
|
// Create message characteristic (write without response + notify)
|
|
771
788
|
messageCharacteristic = CBMutableCharacteristic(
|
|
772
789
|
type: MESSAGE_CHAR_UUID,
|
|
@@ -788,10 +805,10 @@ public class BleManager: NSObject, TransportManager {
|
|
|
788
805
|
let service = CBMutableService(type: SERVICE_UUID, primary: true)
|
|
789
806
|
service.characteristics = [messageCharacteristic!, deviceIdCharacteristic!]
|
|
790
807
|
|
|
791
|
-
// Add service to peripheral manager
|
|
808
|
+
// Add service to peripheral manager (asynchronous - callback in peripheralManager(_:didAdd:error:))
|
|
792
809
|
peripheral.add(service)
|
|
793
|
-
print("[BleManager] GATT server
|
|
794
|
-
emitDiagnostic("info", "GATT server
|
|
810
|
+
print("[BleManager] GATT server setup initiated, waiting for service registration callback...")
|
|
811
|
+
emitDiagnostic("info", "GATT server setup initiated")
|
|
795
812
|
}
|
|
796
813
|
|
|
797
814
|
private func startFragmentPolling() {
|
|
@@ -1523,7 +1540,20 @@ extension BleManager: CBCentralManagerDelegate {
|
|
|
1523
1540
|
pruneMeshObservations(now: now)
|
|
1524
1541
|
meshController.observeAdvertisement(meshMetadata, rssi: Int(rssiValue))
|
|
1525
1542
|
|
|
1526
|
-
|
|
1543
|
+
// When there's no metadata (iOS/Android advertising without service data),
|
|
1544
|
+
// still try to connect - metadata will be exchanged via GATT after connection
|
|
1545
|
+
let decision: MeshController.MeshDecision
|
|
1546
|
+
if meshMetadata == nil {
|
|
1547
|
+
// No metadata in advertisement - allow basic connection to exchange info via GATT
|
|
1548
|
+
decision = MeshController.MeshDecision(
|
|
1549
|
+
intent: .intraCluster,
|
|
1550
|
+
reason: "no_metadata_in_advert",
|
|
1551
|
+
evictPeerId: nil
|
|
1552
|
+
)
|
|
1553
|
+
} else {
|
|
1554
|
+
decision = meshController.shouldInitiateOutbound(metadata: meshMetadata, rssi: Int(rssiValue))
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1527
1557
|
guard decision.intent != .rejected else {
|
|
1528
1558
|
if logThrottler.shouldLog(key: "mesh_skip_\(peripheral.identifier.uuidString)", interval: 15) {
|
|
1529
1559
|
print("[BleManager] Skipping \(peripheral.identifier) due to \(decision.reason)")
|
|
@@ -1892,6 +1922,32 @@ extension BleManager: CBPeripheralManagerDelegate {
|
|
|
1892
1922
|
}
|
|
1893
1923
|
}
|
|
1894
1924
|
|
|
1925
|
+
public func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
1926
|
+
if let error = error {
|
|
1927
|
+
print("[BleManager] ❌ Error adding GATT service: \(error)")
|
|
1928
|
+
emitDiagnostic("error", "Error adding GATT service", context: [
|
|
1929
|
+
"error": error.localizedDescription,
|
|
1930
|
+
"serviceUUID": service.uuid.uuidString
|
|
1931
|
+
])
|
|
1932
|
+
isGattServiceReady = false
|
|
1933
|
+
return
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
print("[BleManager] ✅ GATT service added successfully: \(service.uuid)")
|
|
1937
|
+
emitDiagnostic("info", "GATT service registered successfully", context: [
|
|
1938
|
+
"serviceUUID": service.uuid.uuidString
|
|
1939
|
+
])
|
|
1940
|
+
|
|
1941
|
+
isGattServiceReady = true
|
|
1942
|
+
|
|
1943
|
+
// Start advertising now that the service is ready
|
|
1944
|
+
if pendingAdvertiseAfterServiceReady {
|
|
1945
|
+
pendingAdvertiseAfterServiceReady = false
|
|
1946
|
+
print("[BleManager] 📡 Starting deferred advertising after GATT service ready")
|
|
1947
|
+
startAdvertising(reason: "gatt_service_ready")
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1895
1951
|
public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
1896
1952
|
for request in requests {
|
|
1897
1953
|
print("[BleManager] 📨 GATT WRITE REQUEST from \(request.central.identifier), char: \(request.characteristic.uuid), size: \(request.value?.count ?? 0)")
|
package/package.json
CHANGED