react-native-ble-mesh 1.0.4 → 1.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.
@@ -0,0 +1,787 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview MeshNetwork - Simplified High-Level API
5
+ * @module MeshNetwork
6
+ *
7
+ * Primary entry point for react-native-ble-mesh as specified in the PRD.
8
+ * Provides a developer-friendly API for BitChat-compatible mesh networking.
9
+ *
10
+ * Target: Setup-to-first-message in <15 minutes, <10 lines for basic messaging.
11
+ */
12
+
13
+ const EventEmitter = require('./utils/EventEmitter');
14
+ const { MeshService } = require('./service');
15
+ const { BLETransport, MockTransport } = require('./transport');
16
+ const { MemoryStorage } = require('./storage');
17
+ const { StoreAndForwardManager } = require('./mesh/store');
18
+ const { NetworkMonitor, HEALTH_STATUS } = require('./mesh/monitor');
19
+ const { BatteryOptimizer, BATTERY_MODE } = require('./service/BatteryOptimizer');
20
+ const { EmergencyManager, PANIC_TRIGGER } = require('./service/EmergencyManager');
21
+ const { MessageCompressor } = require('./utils/compression');
22
+ const { EVENTS } = require('./constants');
23
+ const { ValidationError, MeshError } = require('./errors');
24
+
25
+ /**
26
+ * Default MeshNetwork configuration
27
+ * @constant {Object}
28
+ */
29
+ const DEFAULT_CONFIG = Object.freeze({
30
+ /** Display name for this node */
31
+ nickname: 'Anonymous',
32
+ /** Battery mode: 'high' | 'balanced' | 'low' | 'auto' */
33
+ batteryMode: BATTERY_MODE.BALANCED,
34
+ /** Encryption settings */
35
+ encryption: {
36
+ level: 'standard',
37
+ rotateKeysAfter: 1000,
38
+ },
39
+ /** Routing configuration */
40
+ routing: {
41
+ maxHops: 7,
42
+ bloomFilterSize: 10000,
43
+ },
44
+ /** Compression settings */
45
+ compression: {
46
+ enabled: true,
47
+ threshold: 100,
48
+ },
49
+ /** Store and forward settings */
50
+ storeAndForward: {
51
+ enabled: true,
52
+ retentionHours: 24,
53
+ maxCachedMessages: 1000,
54
+ },
55
+ });
56
+
57
+ /**
58
+ * MeshNetwork - High-level API for BitChat-compatible mesh networking.
59
+ *
60
+ * @class MeshNetwork
61
+ * @extends EventEmitter
62
+ * @example
63
+ * // Basic Setup (Minimal)
64
+ * const mesh = new MeshNetwork({
65
+ * nickname: 'Alice',
66
+ * batteryMode: 'balanced',
67
+ * });
68
+ *
69
+ * await mesh.start();
70
+ * await mesh.broadcast('Hello mesh!');
71
+ *
72
+ * mesh.on('messageReceived', (message) => {
73
+ * console.log(`${message.from}: ${message.text}`);
74
+ * });
75
+ */
76
+ class MeshNetwork extends EventEmitter {
77
+ /**
78
+ * Creates a new MeshNetwork instance.
79
+ * @param {Object} [config={}] - Network configuration
80
+ * @param {string} [config.nickname='Anonymous'] - Display name
81
+ * @param {string} [config.batteryMode='balanced'] - Battery mode
82
+ * @param {Object} [config.encryption] - Encryption settings
83
+ * @param {Object} [config.routing] - Routing settings
84
+ * @param {Object} [config.compression] - Compression settings
85
+ * @param {Object} [config.storeAndForward] - Store and forward settings
86
+ */
87
+ constructor(config = {}) {
88
+ super();
89
+
90
+ /**
91
+ * Configuration
92
+ * @type {Object}
93
+ * @private
94
+ */
95
+ this._config = this._mergeConfig(DEFAULT_CONFIG, config);
96
+
97
+ /**
98
+ * Underlying MeshService
99
+ * @type {MeshService}
100
+ * @private
101
+ */
102
+ this._service = new MeshService({
103
+ displayName: this._config.nickname,
104
+ });
105
+
106
+ /**
107
+ * Transport layer
108
+ * @type {Transport|null}
109
+ * @private
110
+ */
111
+ this._transport = null;
112
+
113
+ /**
114
+ * Store and forward manager
115
+ * @type {StoreAndForwardManager|null}
116
+ * @private
117
+ */
118
+ this._storeForward = this._config.storeAndForward.enabled
119
+ ? new StoreAndForwardManager({
120
+ retentionMs: this._config.storeAndForward.retentionHours * 60 * 60 * 1000,
121
+ maxTotalMessages: this._config.storeAndForward.maxCachedMessages,
122
+ })
123
+ : null;
124
+
125
+ /**
126
+ * Network monitor
127
+ * @type {NetworkMonitor}
128
+ * @private
129
+ */
130
+ this._monitor = new NetworkMonitor();
131
+
132
+ /**
133
+ * Battery optimizer
134
+ * @type {BatteryOptimizer}
135
+ * @private
136
+ */
137
+ this._batteryOptimizer = new BatteryOptimizer({
138
+ initialMode: this._config.batteryMode,
139
+ });
140
+
141
+ /**
142
+ * Emergency manager
143
+ * @type {EmergencyManager}
144
+ * @private
145
+ */
146
+ this._emergencyManager = new EmergencyManager();
147
+
148
+ /**
149
+ * Message compressor
150
+ * @type {MessageCompressor}
151
+ * @private
152
+ */
153
+ this._compressor = new MessageCompressor({
154
+ threshold: this._config.compression.threshold,
155
+ });
156
+
157
+ /**
158
+ * Channel manager reference
159
+ * @type {Object|null}
160
+ * @private
161
+ */
162
+ this._channels = null;
163
+
164
+ /**
165
+ * Network state
166
+ * @type {string}
167
+ * @private
168
+ */
169
+ this._state = 'stopped';
170
+
171
+ // Setup event forwarding
172
+ this._setupEventForwarding();
173
+ }
174
+
175
+ // ============================================================================
176
+ // Lifecycle Methods
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Starts the mesh network.
181
+ * @param {Object} [transport] - Optional custom transport
182
+ * @returns {Promise<void>}
183
+ */
184
+ async start(transport) {
185
+ if (this._state === 'running') {
186
+ return;
187
+ }
188
+
189
+ // Create transport if not provided
190
+ this._transport = transport || new BLETransport();
191
+
192
+ // Initialize the service
193
+ await this._service.initialize({
194
+ storage: new MemoryStorage(),
195
+ });
196
+
197
+ // Connect battery optimizer to transport
198
+ this._batteryOptimizer.setTransport(this._transport);
199
+ await this._batteryOptimizer.setMode(this._config.batteryMode);
200
+
201
+ // Register data clearers for panic mode
202
+ this._registerPanicClearers();
203
+
204
+ // Start the service
205
+ await this._service.start(this._transport);
206
+
207
+ // Setup store and forward
208
+ if (this._storeForward) {
209
+ this._setupStoreAndForward();
210
+ }
211
+
212
+ this._state = 'running';
213
+ this.emit('started');
214
+ }
215
+
216
+ /**
217
+ * Stops the mesh network.
218
+ * @returns {Promise<void>}
219
+ */
220
+ async stop() {
221
+ if (this._state !== 'running') {
222
+ return;
223
+ }
224
+
225
+ await this._service.stop();
226
+ this._state = 'stopped';
227
+ this.emit('stopped');
228
+ }
229
+
230
+ /**
231
+ * Destroys the mesh network and cleans up resources.
232
+ * @returns {Promise<void>}
233
+ */
234
+ async destroy() {
235
+ await this.stop();
236
+ await this._service.destroy();
237
+
238
+ if (this._storeForward) {
239
+ this._storeForward.destroy();
240
+ }
241
+ this._monitor.destroy();
242
+ this._batteryOptimizer.destroy();
243
+ this._emergencyManager.destroy();
244
+
245
+ this.removeAllListeners();
246
+ }
247
+
248
+ // ============================================================================
249
+ // Messaging Methods
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Broadcasts a message to all peers.
254
+ * @param {string} text - Message text
255
+ * @returns {Promise<string>} Message ID
256
+ * @throws {Error} If text is invalid
257
+ */
258
+ async broadcast(text) {
259
+ this._validateRunning();
260
+ this._validateMessageText(text);
261
+
262
+ const messageId = await this._service.sendBroadcast(text);
263
+ this._monitor.trackMessageSent('broadcast', messageId);
264
+
265
+ return messageId;
266
+ }
267
+
268
+ /**
269
+ * Sends a direct encrypted message to a specific peer.
270
+ * @param {string} peerId - Target peer ID or nickname
271
+ * @param {string} text - Message text
272
+ * @returns {Promise<string>} Message ID
273
+ * @throws {Error} If peerId or text is invalid
274
+ */
275
+ async sendDirect(peerId, text) {
276
+ this._validateRunning();
277
+ this._validatePeerId(peerId);
278
+ this._validateMessageText(text);
279
+
280
+ try {
281
+ const messageId = await this._service.sendPrivateMessage(peerId, text);
282
+ this._monitor.trackMessageSent(peerId, messageId);
283
+ return messageId;
284
+ } catch (error) {
285
+ // If peer is offline and store-forward is enabled, cache the message
286
+ if (this._storeForward && this._isPeerOffline(peerId)) {
287
+ const payload = this._encodeMessage(text);
288
+ await this._storeForward.cacheForOfflinePeer(peerId, payload);
289
+ this.emit('messageCached', { peerId, text });
290
+ return 'cached';
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ // ============================================================================
297
+ // Channel Methods
298
+ // ============================================================================
299
+
300
+ /**
301
+ * Joins a channel (IRC-style topic-based group chat).
302
+ * @param {string} channelName - Channel name (e.g., '#general')
303
+ * @param {string} [password] - Optional password
304
+ * @returns {Promise<void>}
305
+ */
306
+ async joinChannel(channelName, password) {
307
+ this._validateRunning();
308
+
309
+ const normalized = this._normalizeChannelName(channelName);
310
+ await this._service.joinChannel(normalized, password);
311
+
312
+ this.emit('channelJoined', { channel: normalized });
313
+ }
314
+
315
+ /**
316
+ * Leaves a channel.
317
+ * @param {string} channelName - Channel name
318
+ * @returns {Promise<void>}
319
+ */
320
+ async leaveChannel(channelName) {
321
+ this._validateRunning();
322
+
323
+ const normalized = this._normalizeChannelName(channelName);
324
+ await this._service.leaveChannel(normalized);
325
+
326
+ this.emit('channelLeft', { channel: normalized });
327
+ }
328
+
329
+ /**
330
+ * Sends a message to a channel.
331
+ * @param {string} channelName - Channel name
332
+ * @param {string} text - Message text
333
+ * @returns {Promise<string>} Message ID
334
+ */
335
+ async sendToChannel(channelName, text) {
336
+ this._validateRunning();
337
+
338
+ const normalized = this._normalizeChannelName(channelName);
339
+ return await this._service.sendChannelMessage(normalized, text);
340
+ }
341
+
342
+ /**
343
+ * Gets list of joined channels.
344
+ * @returns {Object[]} Channels
345
+ */
346
+ getChannels() {
347
+ return this._service.getChannels();
348
+ }
349
+
350
+ // ============================================================================
351
+ // Peer Methods
352
+ // ============================================================================
353
+
354
+ /**
355
+ * Gets all known peers.
356
+ * @returns {Object[]} Array of peers
357
+ */
358
+ getPeers() {
359
+ return this._service.getPeers();
360
+ }
361
+
362
+ /**
363
+ * Gets connected peers.
364
+ * @returns {Object[]} Connected peers
365
+ */
366
+ getConnectedPeers() {
367
+ return this._service.getConnectedPeers();
368
+ }
369
+
370
+ /**
371
+ * Gets peers with secure sessions.
372
+ * @returns {Object[]} Secured peers
373
+ */
374
+ getSecuredPeers() {
375
+ return this._service.getSecuredPeers();
376
+ }
377
+
378
+ /**
379
+ * Blocks a peer.
380
+ * @param {string} peerId - Peer ID
381
+ */
382
+ blockPeer(peerId) {
383
+ this._service.blockPeer(peerId);
384
+ this.emit('peerBlocked', { peerId });
385
+ }
386
+
387
+ /**
388
+ * Unblocks a peer.
389
+ * @param {string} peerId - Peer ID
390
+ */
391
+ unblockPeer(peerId) {
392
+ this._service.unblockPeer(peerId);
393
+ this.emit('peerUnblocked', { peerId });
394
+ }
395
+
396
+ // ============================================================================
397
+ // Network Health Methods
398
+ // ============================================================================
399
+
400
+ /**
401
+ * Gets network health metrics.
402
+ * @returns {Object} Health report
403
+ */
404
+ getNetworkHealth() {
405
+ return this._monitor.generateHealthReport();
406
+ }
407
+
408
+ /**
409
+ * Gets detailed health for a specific peer.
410
+ * @param {string} peerId - Peer ID
411
+ * @returns {Object|null} Node health
412
+ */
413
+ getPeerHealth(peerId) {
414
+ return this._monitor.getNodeHealth(peerId);
415
+ }
416
+
417
+ // ============================================================================
418
+ // Battery Management
419
+ // ============================================================================
420
+
421
+ /**
422
+ * Sets the battery mode.
423
+ * @param {string} mode - 'high' | 'balanced' | 'low' | 'auto'
424
+ * @returns {Promise<void>}
425
+ */
426
+ async setBatteryMode(mode) {
427
+ await this._batteryOptimizer.setMode(mode);
428
+ }
429
+
430
+ /**
431
+ * Gets the current battery mode.
432
+ * @returns {string} Current mode
433
+ */
434
+ getBatteryMode() {
435
+ return this._batteryOptimizer.getMode();
436
+ }
437
+
438
+ /**
439
+ * Updates battery level (for auto mode).
440
+ * @param {number} level - Battery level (0-100)
441
+ * @param {boolean} [charging=false] - Whether charging
442
+ */
443
+ updateBatteryLevel(level, charging = false) {
444
+ this._batteryOptimizer.updateBatteryLevel(level, charging);
445
+ }
446
+
447
+ // ============================================================================
448
+ // Security Methods
449
+ // ============================================================================
450
+
451
+ /**
452
+ * Enables panic mode for emergency data wipe.
453
+ * @param {Object} [options={}] - Panic mode options
454
+ * @param {string} [options.trigger='triple_tap'] - Trigger type
455
+ * @param {Function} [options.onWipe] - Callback after wipe
456
+ */
457
+ enablePanicMode(options = {}) {
458
+ this._emergencyManager.enablePanicMode(options);
459
+ this.emit('panicModeEnabled');
460
+ }
461
+
462
+ /**
463
+ * Disables panic mode.
464
+ */
465
+ disablePanicMode() {
466
+ this._emergencyManager.disablePanicMode();
467
+ this.emit('panicModeDisabled');
468
+ }
469
+
470
+ /**
471
+ * Registers a tap for panic mode detection.
472
+ */
473
+ registerPanicTap() {
474
+ this._emergencyManager.registerTap();
475
+ }
476
+
477
+ /**
478
+ * Manually triggers data wipe.
479
+ * @returns {Promise<Object>} Wipe result
480
+ */
481
+ async wipeAllData() {
482
+ return await this._emergencyManager.wipeAllData();
483
+ }
484
+
485
+ // ============================================================================
486
+ // Status Methods
487
+ // ============================================================================
488
+
489
+ /**
490
+ * Gets the network status.
491
+ * @returns {Object} Status
492
+ */
493
+ getStatus() {
494
+ return {
495
+ state: this._state,
496
+ identity: this._service.getIdentity(),
497
+ peers: this.getPeers().length,
498
+ connectedPeers: this.getConnectedPeers().length,
499
+ channels: this.getChannels().length,
500
+ health: this.getNetworkHealth(),
501
+ batteryMode: this.getBatteryMode(),
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Gets the identity information.
507
+ * @returns {Object} Identity
508
+ */
509
+ getIdentity() {
510
+ return this._service.getIdentity();
511
+ }
512
+
513
+ /**
514
+ * Sets the display name/nickname.
515
+ * @param {string} nickname - New nickname
516
+ */
517
+ setNickname(nickname) {
518
+ this._config.nickname = nickname;
519
+ this._service.setDisplayName(nickname);
520
+ }
521
+
522
+ // ============================================================================
523
+ // Private Methods
524
+ // ============================================================================
525
+
526
+ /**
527
+ * Merges configuration with defaults.
528
+ * @param {Object} defaults - Default config
529
+ * @param {Object} custom - Custom config
530
+ * @returns {Object} Merged config
531
+ * @private
532
+ */
533
+ _mergeConfig(defaults, custom) {
534
+ return {
535
+ ...defaults,
536
+ ...custom,
537
+ encryption: { ...defaults.encryption, ...custom.encryption },
538
+ routing: { ...defaults.routing, ...custom.routing },
539
+ compression: { ...defaults.compression, ...custom.compression },
540
+ storeAndForward: { ...defaults.storeAndForward, ...custom.storeAndForward },
541
+ };
542
+ }
543
+
544
+ /**
545
+ * Sets up event forwarding from underlying services.
546
+ * @private
547
+ */
548
+ _setupEventForwarding() {
549
+ // Forward service events with PRD-style naming
550
+ this._service.on('peer-discovered', (peer) => {
551
+ this._monitor.trackPeerDiscovered(peer.id);
552
+ this.emit('peerDiscovered', peer);
553
+ });
554
+
555
+ this._service.on('peer-connected', (peer) => {
556
+ this.emit('peerConnected', peer);
557
+ // Deliver cached messages
558
+ if (this._storeForward && this._storeForward.hasCachedMessages(peer.id)) {
559
+ this._deliverCachedMessages(peer.id);
560
+ }
561
+ });
562
+
563
+ this._service.on('peer-disconnected', (peer) => {
564
+ this._monitor.trackPeerDisconnected(peer.id);
565
+ this.emit('peerDisconnected', peer);
566
+ });
567
+
568
+ this._service.on('message', (message) => {
569
+ this._monitor.trackMessageReceived(message.senderId);
570
+ this.emit('messageReceived', {
571
+ from: message.senderId,
572
+ text: message.content,
573
+ timestamp: message.timestamp,
574
+ type: message.type,
575
+ });
576
+ });
577
+
578
+ this._service.on('private-message', (message) => {
579
+ this.emit('directMessage', {
580
+ from: message.senderId,
581
+ text: message.content,
582
+ timestamp: message.timestamp,
583
+ });
584
+ });
585
+
586
+ this._service.on('channel-message', (message) => {
587
+ this.emit('channelMessage', {
588
+ channel: message.channelId,
589
+ from: message.senderId,
590
+ text: message.content,
591
+ timestamp: message.timestamp,
592
+ });
593
+ });
594
+
595
+ this._service.on('message-delivered', (info) => {
596
+ this._monitor.trackMessageDelivered(info.messageId);
597
+ this.emit('messageDelivered', info);
598
+ });
599
+
600
+ this._service.on('error', (error) => {
601
+ this.emit('error', error);
602
+ });
603
+
604
+ // Forward monitor events
605
+ this._monitor.on('health-changed', (info) => {
606
+ this.emit('networkHealthChanged', info);
607
+ });
608
+
609
+ // Forward emergency events
610
+ this._emergencyManager.on('panic-wipe-completed', (result) => {
611
+ this.emit('dataWiped', result);
612
+ });
613
+ }
614
+
615
+ /**
616
+ * Sets up store and forward integration.
617
+ * @private
618
+ */
619
+ _setupStoreAndForward() {
620
+ // Nothing extra needed - handled in peerConnected event
621
+ }
622
+
623
+ /**
624
+ * Delivers cached messages to a peer.
625
+ * @param {string} peerId - Peer ID
626
+ * @private
627
+ */
628
+ async _deliverCachedMessages(peerId) {
629
+ if (!this._storeForward) return;
630
+
631
+ const sendFn = async (payload) => {
632
+ await this._service._sendRaw(peerId, payload);
633
+ };
634
+
635
+ const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
636
+
637
+ if (result.delivered > 0) {
638
+ this.emit('cachedMessagesDelivered', {
639
+ peerId,
640
+ delivered: result.delivered,
641
+ });
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Registers data clearers for panic mode.
647
+ * @private
648
+ */
649
+ _registerPanicClearers() {
650
+ // Clear service data
651
+ this._emergencyManager.registerClearer(async () => {
652
+ await this._service.destroy();
653
+ });
654
+
655
+ // Clear store and forward cache
656
+ if (this._storeForward) {
657
+ this._emergencyManager.registerClearer(async () => {
658
+ this._storeForward.clear();
659
+ });
660
+ }
661
+
662
+ // Clear monitor data
663
+ this._emergencyManager.registerClearer(() => {
664
+ this._monitor.reset();
665
+ });
666
+ }
667
+
668
+ /**
669
+ * Validates that network is running.
670
+ * @throws {MeshError} If not running
671
+ * @private
672
+ */
673
+ _validateRunning() {
674
+ if (this._state !== 'running') {
675
+ throw new MeshError(
676
+ 'MeshNetwork is not running. Call start() first.',
677
+ 'E900',
678
+ { state: this._state }
679
+ );
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Checks if a peer is offline.
685
+ * @param {string} peerId - Peer ID
686
+ * @returns {boolean} True if offline
687
+ * @private
688
+ */
689
+ _isPeerOffline(peerId) {
690
+ const peer = this._service.getPeer(peerId);
691
+ return !peer || !peer.isConnected;
692
+ }
693
+
694
+ /**
695
+ * Encodes a message for storage.
696
+ * @param {string} text - Message text
697
+ * @returns {Uint8Array} Encoded payload
698
+ * @private
699
+ */
700
+ _encodeMessage(text) {
701
+ return new TextEncoder().encode(text);
702
+ }
703
+
704
+ /**
705
+ * Normalizes channel name (adds # if missing).
706
+ * @param {string} name - Channel name
707
+ * @returns {string} Normalized name
708
+ * @private
709
+ */
710
+ _normalizeChannelName(name) {
711
+ return name.startsWith('#') ? name : `#${name}`;
712
+ }
713
+
714
+ /**
715
+ * Maximum allowed message size in bytes (1MB).
716
+ * @constant {number}
717
+ * @private
718
+ */
719
+ static get MAX_MESSAGE_SIZE() {
720
+ return 1024 * 1024;
721
+ }
722
+
723
+ /**
724
+ * Validates message text input.
725
+ * @param {string} text - Message text to validate
726
+ * @throws {ValidationError} If text is invalid
727
+ * @private
728
+ */
729
+ _validateMessageText(text) {
730
+ if (text === null || text === undefined) {
731
+ throw ValidationError.missingArgument('text');
732
+ }
733
+
734
+ if (typeof text !== 'string') {
735
+ throw ValidationError.invalidType('text', text, 'string');
736
+ }
737
+
738
+ if (text.length === 0) {
739
+ throw ValidationError.invalidArgument('text', text, {
740
+ reason: 'Message text cannot be empty',
741
+ });
742
+ }
743
+
744
+ // Check size limit (UTF-8 encoded)
745
+ const byteLength = new TextEncoder().encode(text).length;
746
+ if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
747
+ throw ValidationError.outOfRange('text', byteLength, {
748
+ min: 1,
749
+ max: MeshNetwork.MAX_MESSAGE_SIZE,
750
+ });
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Validates peer ID input.
756
+ * @param {string} peerId - Peer ID to validate
757
+ * @throws {ValidationError} If peerId is invalid
758
+ * @private
759
+ */
760
+ _validatePeerId(peerId) {
761
+ if (peerId === null || peerId === undefined) {
762
+ throw ValidationError.missingArgument('peerId');
763
+ }
764
+
765
+ if (typeof peerId !== 'string') {
766
+ throw ValidationError.invalidType('peerId', peerId, 'string');
767
+ }
768
+
769
+ if (peerId.trim().length === 0) {
770
+ throw ValidationError.invalidArgument('peerId', peerId, {
771
+ reason: 'Peer ID cannot be empty or whitespace only',
772
+ });
773
+ }
774
+ }
775
+ }
776
+
777
+ // Export convenience constants
778
+ MeshNetwork.BatteryMode = BATTERY_MODE;
779
+ MeshNetwork.PanicTrigger = PANIC_TRIGGER;
780
+ MeshNetwork.HealthStatus = HEALTH_STATUS;
781
+
782
+ module.exports = {
783
+ MeshNetwork,
784
+ BATTERY_MODE,
785
+ PANIC_TRIGGER,
786
+ HEALTH_STATUS,
787
+ };