@magicred-1/ble-mesh 1.2.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 +394 -0
- package/android/build.gradle +94 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/AndroidManifestNew.xml +17 -0
- package/android/src/main/java/com/blemesh/BleMeshModule.kt +1994 -0
- package/android/src/main/java/com/blemesh/BleMeshPackage.kt +16 -0
- package/ios/BleMesh.m +49 -0
- package/ios/BleMesh.swift +1838 -0
- package/kard-network-ble-mesh.podspec +27 -0
- package/lib/commonjs/index.js +275 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +270 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/index.d.ts +178 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +99 -0
- package/src/index.ts +448 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { NativeModules, NativeEventEmitter, Platform, PermissionsAndroid, Permission } from 'react-native';
|
|
2
|
+
|
|
3
|
+
const LINKING_ERROR =
|
|
4
|
+
`The package 'kard-network-ble-mesh' doesn't seem to be linked. Make sure: \n\n` +
|
|
5
|
+
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
|
|
6
|
+
'- You rebuilt the app after installing the package\n' +
|
|
7
|
+
'- You are not using Expo Go (this package requires a development build)\n';
|
|
8
|
+
|
|
9
|
+
const BleMeshModule = NativeModules.BleMesh
|
|
10
|
+
? NativeModules.BleMesh
|
|
11
|
+
: new Proxy(
|
|
12
|
+
{},
|
|
13
|
+
{
|
|
14
|
+
get() {
|
|
15
|
+
throw new Error(LINKING_ERROR);
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const eventEmitter = new NativeEventEmitter(BleMeshModule);
|
|
21
|
+
|
|
22
|
+
// Types
|
|
23
|
+
export interface Peer {
|
|
24
|
+
peerId: string;
|
|
25
|
+
nickname: string;
|
|
26
|
+
isConnected: boolean;
|
|
27
|
+
rssi?: number;
|
|
28
|
+
lastSeen: number;
|
|
29
|
+
isVerified: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Message {
|
|
33
|
+
id: string;
|
|
34
|
+
content: string;
|
|
35
|
+
senderPeerId: string;
|
|
36
|
+
senderNickname: string;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
isPrivate: boolean;
|
|
39
|
+
channel?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FileTransfer {
|
|
43
|
+
id: string;
|
|
44
|
+
fileName: string;
|
|
45
|
+
fileSize: number;
|
|
46
|
+
mimeType: string;
|
|
47
|
+
data: string; // base64 encoded
|
|
48
|
+
senderPeerId: string;
|
|
49
|
+
timestamp: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SolanaTransaction {
|
|
53
|
+
id: string;
|
|
54
|
+
/** Base64-encoded serialized transaction (could be partially signed) */
|
|
55
|
+
serializedTransaction: string;
|
|
56
|
+
/** The peer ID who initiated/sent the transaction */
|
|
57
|
+
senderPeerId: string;
|
|
58
|
+
/** The public key of the first signer (sender) */
|
|
59
|
+
firstSignerPublicKey: string;
|
|
60
|
+
/**
|
|
61
|
+
* The preferred public key of the second signer (optional).
|
|
62
|
+
* If not provided, any peer can sign as the second signer.
|
|
63
|
+
*/
|
|
64
|
+
secondSignerPublicKey?: string;
|
|
65
|
+
/** Description or purpose of the transaction */
|
|
66
|
+
description?: string;
|
|
67
|
+
/** Timestamp when transaction was sent */
|
|
68
|
+
timestamp: number;
|
|
69
|
+
/** Whether this transaction requires a second signer to sign */
|
|
70
|
+
requiresSecondSigner: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface TransactionResponse {
|
|
74
|
+
id: string;
|
|
75
|
+
/** The peer ID who responded */
|
|
76
|
+
responderPeerId: string;
|
|
77
|
+
/** Base64-encoded fully signed transaction (after second signer signs) */
|
|
78
|
+
signedTransaction?: string;
|
|
79
|
+
/** Error message if signing failed */
|
|
80
|
+
error?: string;
|
|
81
|
+
/** Timestamp of response */
|
|
82
|
+
timestamp: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SolanaNonceTransaction {
|
|
86
|
+
recentBlockhash: string;
|
|
87
|
+
nonceAccount: string;
|
|
88
|
+
nonceAuthority: string;
|
|
89
|
+
feePayer: string;
|
|
90
|
+
instructions: Array<{
|
|
91
|
+
programId: string;
|
|
92
|
+
keys: Array<{ pubkey: string; isSigner: boolean; isWritable: boolean }>;
|
|
93
|
+
data: string; // base64 encoded
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PermissionStatus {
|
|
98
|
+
bluetooth: boolean;
|
|
99
|
+
bluetoothAdvertise?: boolean; // Android 12+
|
|
100
|
+
bluetoothConnect?: boolean; // Android 12+
|
|
101
|
+
bluetoothScan?: boolean; // Android 12+
|
|
102
|
+
location: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'scanning';
|
|
106
|
+
|
|
107
|
+
export interface MeshServiceConfig {
|
|
108
|
+
nickname?: string;
|
|
109
|
+
autoRequestPermissions?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Event types
|
|
113
|
+
export type PeerListUpdatedEvent = { peers: Peer[] };
|
|
114
|
+
export type MessageReceivedEvent = { message: Message };
|
|
115
|
+
export type FileReceivedEvent = { file: FileTransfer };
|
|
116
|
+
export type TransactionReceivedEvent = { transaction: SolanaTransaction };
|
|
117
|
+
export type TransactionResponseEvent = { response: TransactionResponse };
|
|
118
|
+
export type ConnectionStateChangedEvent = { state: ConnectionState; peerCount: number };
|
|
119
|
+
export type PermissionsChangedEvent = { permissions: PermissionStatus };
|
|
120
|
+
export type ErrorEvent = { code: string; message: string };
|
|
121
|
+
|
|
122
|
+
// Event listener types
|
|
123
|
+
type EventCallback<T> = (data: T) => void;
|
|
124
|
+
|
|
125
|
+
class BleMeshService {
|
|
126
|
+
private isInitialized = false;
|
|
127
|
+
|
|
128
|
+
// Request all required permissions for BLE mesh networking
|
|
129
|
+
async requestPermissions(): Promise<PermissionStatus> {
|
|
130
|
+
if (Platform.OS === 'android') {
|
|
131
|
+
return this.requestAndroidPermissions();
|
|
132
|
+
} else if (Platform.OS === 'ios') {
|
|
133
|
+
return this.requestIOSPermissions();
|
|
134
|
+
}
|
|
135
|
+
throw new Error('Unsupported platform');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async requestAndroidPermissions(): Promise<PermissionStatus> {
|
|
139
|
+
const apiLevel = Platform.Version as number;
|
|
140
|
+
|
|
141
|
+
let permissions: Permission[] = [];
|
|
142
|
+
|
|
143
|
+
if (apiLevel >= 31) {
|
|
144
|
+
// Android 12+ (API 31+)
|
|
145
|
+
permissions = [
|
|
146
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE as Permission,
|
|
147
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT as Permission,
|
|
148
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN as Permission,
|
|
149
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION as Permission,
|
|
150
|
+
];
|
|
151
|
+
} else {
|
|
152
|
+
// Android 11 and below
|
|
153
|
+
permissions = [
|
|
154
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION as Permission,
|
|
155
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION as Permission,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const results = await PermissionsAndroid.requestMultiple(permissions);
|
|
160
|
+
|
|
161
|
+
const status: PermissionStatus = {
|
|
162
|
+
bluetooth: true,
|
|
163
|
+
location: results[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION] === 'granted',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (apiLevel >= 31) {
|
|
167
|
+
status.bluetoothAdvertise = results[PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE] === 'granted';
|
|
168
|
+
status.bluetoothConnect = results[PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] === 'granted';
|
|
169
|
+
status.bluetoothScan = results[PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] === 'granted';
|
|
170
|
+
status.bluetooth = status.bluetoothAdvertise && status.bluetoothConnect && status.bluetoothScan;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return status;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async requestIOSPermissions(): Promise<PermissionStatus> {
|
|
177
|
+
// On iOS, Bluetooth permissions are requested automatically when starting services
|
|
178
|
+
// The native module handles the permission prompts
|
|
179
|
+
const result = await BleMeshModule.requestPermissions();
|
|
180
|
+
return {
|
|
181
|
+
bluetooth: result.bluetooth,
|
|
182
|
+
location: result.location,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check current permission status without requesting
|
|
187
|
+
async checkPermissions(): Promise<PermissionStatus> {
|
|
188
|
+
if (Platform.OS === 'android') {
|
|
189
|
+
return this.checkAndroidPermissions();
|
|
190
|
+
}
|
|
191
|
+
return BleMeshModule.checkPermissions();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async checkAndroidPermissions(): Promise<PermissionStatus> {
|
|
195
|
+
const apiLevel = Platform.Version as number;
|
|
196
|
+
|
|
197
|
+
const fineLocation = await PermissionsAndroid.check(
|
|
198
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const status: PermissionStatus = {
|
|
202
|
+
bluetooth: true,
|
|
203
|
+
location: fineLocation,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (apiLevel >= 31) {
|
|
207
|
+
const advertise = await PermissionsAndroid.check(
|
|
208
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE
|
|
209
|
+
);
|
|
210
|
+
const connect = await PermissionsAndroid.check(
|
|
211
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT
|
|
212
|
+
);
|
|
213
|
+
const scan = await PermissionsAndroid.check(
|
|
214
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
status.bluetoothAdvertise = advertise;
|
|
218
|
+
status.bluetoothConnect = connect;
|
|
219
|
+
status.bluetoothScan = scan;
|
|
220
|
+
status.bluetooth = advertise && connect && scan;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return status;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Initialize and start the mesh service
|
|
227
|
+
async start(config: MeshServiceConfig = {}): Promise<void> {
|
|
228
|
+
const { nickname = 'anon', autoRequestPermissions = true } = config;
|
|
229
|
+
|
|
230
|
+
if (autoRequestPermissions) {
|
|
231
|
+
const permissions = await this.requestPermissions();
|
|
232
|
+
const hasAllPermissions = permissions.bluetooth && permissions.location;
|
|
233
|
+
|
|
234
|
+
if (!hasAllPermissions) {
|
|
235
|
+
throw new Error('Required permissions not granted. Bluetooth and Location permissions are required.');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await BleMeshModule.start(nickname);
|
|
240
|
+
this.isInitialized = true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Stop the mesh service
|
|
244
|
+
async stop(): Promise<void> {
|
|
245
|
+
if (!this.isInitialized) return;
|
|
246
|
+
await BleMeshModule.stop();
|
|
247
|
+
this.isInitialized = false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Set the user's nickname
|
|
251
|
+
async setNickname(nickname: string): Promise<void> {
|
|
252
|
+
this.ensureInitialized();
|
|
253
|
+
await BleMeshModule.setNickname(nickname);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get the current peer ID
|
|
257
|
+
async getMyPeerId(): Promise<string> {
|
|
258
|
+
this.ensureInitialized();
|
|
259
|
+
return BleMeshModule.getMyPeerId();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Get the current nickname
|
|
263
|
+
async getMyNickname(): Promise<string> {
|
|
264
|
+
this.ensureInitialized();
|
|
265
|
+
return BleMeshModule.getMyNickname();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Get list of currently connected peers
|
|
269
|
+
async getPeers(): Promise<Peer[]> {
|
|
270
|
+
this.ensureInitialized();
|
|
271
|
+
return BleMeshModule.getPeers();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Send a public broadcast message to all peers
|
|
275
|
+
async sendMessage(content: string, channel?: string): Promise<string> {
|
|
276
|
+
this.ensureInitialized();
|
|
277
|
+
return BleMeshModule.sendMessage(content, channel || null);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Send a private encrypted message to a specific peer
|
|
281
|
+
async sendPrivateMessage(content: string, recipientPeerId: string): Promise<string> {
|
|
282
|
+
this.ensureInitialized();
|
|
283
|
+
return BleMeshModule.sendPrivateMessage(content, recipientPeerId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Send a file (broadcast or private)
|
|
287
|
+
async sendFile(
|
|
288
|
+
filePath: string,
|
|
289
|
+
options?: { recipientPeerId?: string; channel?: string }
|
|
290
|
+
): Promise<string> {
|
|
291
|
+
this.ensureInitialized();
|
|
292
|
+
return BleMeshModule.sendFile(filePath, options?.recipientPeerId || null, options?.channel || null);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Send a Solana transaction for any peer to sign as second signer.
|
|
297
|
+
*
|
|
298
|
+
* @param serializedTransaction - Base64-encoded partially signed transaction
|
|
299
|
+
* @param options - Transaction options
|
|
300
|
+
* @param options.firstSignerPublicKey - Public key of the first signer (required)
|
|
301
|
+
* @param options.secondSignerPublicKey - Preferred second signer (optional, any peer can sign if not specified)
|
|
302
|
+
* @param options.description - Description of the transaction
|
|
303
|
+
* @param options.recipientPeerId - Specific peer to send to (optional, broadcasts to all if not specified)
|
|
304
|
+
*/
|
|
305
|
+
async sendTransaction(
|
|
306
|
+
serializedTransaction: string,
|
|
307
|
+
options?: {
|
|
308
|
+
firstSignerPublicKey: string;
|
|
309
|
+
secondSignerPublicKey?: string;
|
|
310
|
+
description?: string;
|
|
311
|
+
recipientPeerId?: string;
|
|
312
|
+
}
|
|
313
|
+
): Promise<string> {
|
|
314
|
+
this.ensureInitialized();
|
|
315
|
+
const txId = Math.random().toString(36).substring(2, 15);
|
|
316
|
+
return BleMeshModule.sendTransaction(
|
|
317
|
+
txId,
|
|
318
|
+
serializedTransaction,
|
|
319
|
+
options?.recipientPeerId || null,
|
|
320
|
+
options?.firstSignerPublicKey,
|
|
321
|
+
options?.secondSignerPublicKey || null,
|
|
322
|
+
options?.description || null
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Respond to a received transaction (sign or reject)
|
|
327
|
+
async respondToTransaction(
|
|
328
|
+
transactionId: string,
|
|
329
|
+
recipientPeerId: string,
|
|
330
|
+
response: {
|
|
331
|
+
signedTransaction?: string;
|
|
332
|
+
error?: string;
|
|
333
|
+
}
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
this.ensureInitialized();
|
|
336
|
+
await BleMeshModule.respondToTransaction(
|
|
337
|
+
transactionId,
|
|
338
|
+
recipientPeerId,
|
|
339
|
+
response.signedTransaction || null,
|
|
340
|
+
response.error || null
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Send a read receipt for a message
|
|
345
|
+
async sendReadReceipt(messageId: string, recipientPeerId: string): Promise<void> {
|
|
346
|
+
this.ensureInitialized();
|
|
347
|
+
await BleMeshModule.sendReadReceipt(messageId, recipientPeerId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if we have an encrypted session with a peer
|
|
351
|
+
async hasEncryptedSession(peerId: string): Promise<boolean> {
|
|
352
|
+
this.ensureInitialized();
|
|
353
|
+
return BleMeshModule.hasEncryptedSession(peerId);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Initiate a Noise handshake with a peer
|
|
357
|
+
async initiateHandshake(peerId: string): Promise<void> {
|
|
358
|
+
this.ensureInitialized();
|
|
359
|
+
await BleMeshModule.initiateHandshake(peerId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async sendSolanaNonceTransaction(
|
|
363
|
+
transaction: SolanaNonceTransaction,
|
|
364
|
+
recipientPeerId: string
|
|
365
|
+
): Promise<string> {
|
|
366
|
+
this.ensureInitialized();
|
|
367
|
+
return BleMeshModule.sendSolanaNonceTransaction(transaction, recipientPeerId);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Get the identity fingerprint for verification
|
|
371
|
+
async getIdentityFingerprint(): Promise<string> {
|
|
372
|
+
this.ensureInitialized();
|
|
373
|
+
return BleMeshModule.getIdentityFingerprint();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Get peer's fingerprint for verification
|
|
377
|
+
async getPeerFingerprint(peerId: string): Promise<string | null> {
|
|
378
|
+
this.ensureInitialized();
|
|
379
|
+
return BleMeshModule.getPeerFingerprint(peerId);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Force a broadcast announce to refresh presence
|
|
383
|
+
async broadcastAnnounce(): Promise<void> {
|
|
384
|
+
this.ensureInitialized();
|
|
385
|
+
await BleMeshModule.broadcastAnnounce();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Event listeners
|
|
389
|
+
onPeerListUpdated(callback: EventCallback<PeerListUpdatedEvent>): () => void {
|
|
390
|
+
const subscription = eventEmitter.addListener('onPeerListUpdated', callback);
|
|
391
|
+
return () => subscription.remove();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
onMessageReceived(callback: EventCallback<MessageReceivedEvent>): () => void {
|
|
395
|
+
const subscription = eventEmitter.addListener('onMessageReceived', callback);
|
|
396
|
+
return () => subscription.remove();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
onFileReceived(callback: EventCallback<FileReceivedEvent>): () => void {
|
|
400
|
+
const subscription = eventEmitter.addListener('onFileReceived', callback);
|
|
401
|
+
return () => subscription.remove();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
onTransactionReceived(callback: EventCallback<TransactionReceivedEvent>): () => void {
|
|
405
|
+
const subscription = eventEmitter.addListener('onTransactionReceived', callback);
|
|
406
|
+
return () => subscription.remove();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
onTransactionResponse(callback: EventCallback<TransactionResponseEvent>): () => void {
|
|
410
|
+
const subscription = eventEmitter.addListener('onTransactionResponse', callback);
|
|
411
|
+
return () => subscription.remove();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
onConnectionStateChanged(callback: EventCallback<ConnectionStateChangedEvent>): () => void {
|
|
415
|
+
const subscription = eventEmitter.addListener('onConnectionStateChanged', callback);
|
|
416
|
+
return () => subscription.remove();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
onReadReceipt(callback: EventCallback<{ messageId: string; fromPeerId: string }>): () => void {
|
|
420
|
+
const subscription = eventEmitter.addListener('onReadReceipt', callback);
|
|
421
|
+
return () => subscription.remove();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
onDeliveryAck(callback: EventCallback<{ messageId: string; fromPeerId: string }>): () => void {
|
|
425
|
+
const subscription = eventEmitter.addListener('onDeliveryAck', callback);
|
|
426
|
+
return () => subscription.remove();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
onError(callback: EventCallback<ErrorEvent>): () => void {
|
|
430
|
+
const subscription = eventEmitter.addListener('onError', callback);
|
|
431
|
+
return () => subscription.remove();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private ensureInitialized(): void {
|
|
435
|
+
if (!this.isInitialized) {
|
|
436
|
+
throw new Error('BleMesh service not initialized. Call start() first.');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Export singleton instance
|
|
442
|
+
export const BleMesh = new BleMeshService();
|
|
443
|
+
|
|
444
|
+
// Also export the class for those who want multiple instances
|
|
445
|
+
export { BleMeshService };
|
|
446
|
+
|
|
447
|
+
// Default export
|
|
448
|
+
export default BleMesh;
|