ota-hub-reactjs 0.0.18 → 0.0.19

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.
@@ -36,7 +36,7 @@ export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({
36
36
  focussedConnection: string | undefined;
37
37
  setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
38
38
  connections: T[];
39
- addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
39
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<T>) => Promise<string>;
40
40
  removeConnection: (uuid: string) => void;
41
41
  connect: (_uuid: string) => void;
42
42
  disconnect: (_uuid: string) => void;
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { useEffect, useRef, useState } from "react";
3
- ;
4
3
  // Initial, generic state for the generic device
5
4
  export function createDefaultInitialDeviceState(uuid, props) {
6
5
  return {
@@ -11,7 +10,7 @@ export function createDefaultInitialDeviceState(uuid, props) {
11
10
  logs: [],
12
11
  readBufferLeftover: "",
13
12
  readBufferLeftoverAsBytes: new Uint8Array([]),
14
- ...props
13
+ ...props,
15
14
  };
16
15
  }
17
16
  /* One Device Whisperer is used for all like-devices, such as all Serial with Protobuf.
@@ -45,13 +44,13 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
45
44
  }));
46
45
  };
47
46
  // ---------- ADD ----------
48
- const addConnection = async ({ uuid, propCreator }) => {
47
+ const addConnection = async ({ uuid, propCreator, }) => {
49
48
  uuid = uuid ?? `unnamed_device_${connectionsRef.current.size}`;
50
49
  const props = propCreator?.(uuid);
51
50
  const newConnection = {
52
51
  ...createDefaultInitialDeviceState(uuid),
53
52
  ...createInitialConnectionState(uuid),
54
- ...props
53
+ ...props,
55
54
  };
56
55
  connectionsRef.current.set(uuid, newConnection);
57
56
  setConnections(Array.from(connectionsRef.current.values()));
@@ -82,6 +81,6 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
82
81
  appendLog,
83
82
  isReady,
84
83
  setIsReady,
85
- createInitialConnectionState
84
+ createInitialConnectionState,
86
85
  };
87
86
  }
@@ -16,7 +16,7 @@ export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState,
16
16
  expectLength?: boolean;
17
17
  };
18
18
  export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConnectionState, Topic extends string | number, MessageRX = any, MessageTX = any>({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER, }: ProtobufDeviceWhispererProps<AppLayer, Topic, MessageRX, MessageTX>): {
19
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppLayer>) => Promise<string>;
19
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppLayer>) => Promise<string>;
20
20
  sendProtobuf: (uuid: string, message: MessageTX) => void;
21
21
  protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
22
22
  focussedConnection: string | undefined;
@@ -10,7 +10,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
10
10
  const length = payload.length;
11
11
  const header = new Uint8Array([
12
12
  (length >> 8) & 0xff, // Most significant byte first
13
- length & 0xff // Least significant byte second
13
+ length & 0xff, // Least significant byte second
14
14
  ]);
15
15
  return concatUint8Arrays(header, payload);
16
16
  };
@@ -55,9 +55,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
55
55
  // --- Inbound buffering ---
56
56
  const buffers = {};
57
57
  const protoBufOnReceiveHandler = (uuid, data) => {
58
- const bytes = typeof data === "string"
59
- ? new TextEncoder().encode(data)
60
- : data;
58
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
61
59
  buffers[uuid] = concatUint8Arrays(buffers[uuid] || new Uint8Array(), bytes);
62
60
  let buffer = buffers[uuid];
63
61
  while (buffer.length > 0) {
@@ -73,14 +71,14 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
73
71
  catch (err) {
74
72
  transportLayer.appendLog(uuid, {
75
73
  level: 0,
76
- message: `[!] Error in handler for topic "${topic}": ${err}`
74
+ message: `[!] Error in handler for topic "${topic}": ${err}`,
77
75
  });
78
76
  }
79
77
  }
80
78
  else {
81
79
  transportLayer.appendLog(uuid, {
82
80
  level: 1,
83
- message: `[!] Unknown Protobuf topic: "${topic}"`
81
+ message: `[!] Unknown Protobuf topic: "${topic}"`,
84
82
  });
85
83
  }
86
84
  buffer = buffers[uuid];
@@ -97,7 +95,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
97
95
  if (text) {
98
96
  transportLayer.appendLog(uuid, {
99
97
  level: 2,
100
- message: text
98
+ message: text,
101
99
  });
102
100
  }
103
101
  continue;
@@ -108,11 +106,11 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
108
106
  buffers[uuid] = buffer;
109
107
  const preview = Array.from(garbage)
110
108
  .slice(0, 16)
111
- .map(b => b.toString(16).padStart(2, '0'))
112
- .join(' ');
109
+ .map((b) => b.toString(16).padStart(2, "0"))
110
+ .join(" ");
113
111
  transportLayer.appendLog(uuid, {
114
112
  level: 1,
115
- message: `[!] Skipped invalid bytes before Protobuf header: ${preview}... (${garbage.length} bytes)`
113
+ message: `[!] Skipped invalid bytes before Protobuf header: ${preview}... (${garbage.length} bytes)`,
116
114
  });
117
115
  continue;
118
116
  }
@@ -121,22 +119,22 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
121
119
  buffers[uuid] = buffer;
122
120
  };
123
121
  // --- Override addConnection to wrap onReceive ---
124
- const addConnection = async ({ uuid, propCreator }) => {
122
+ const addConnection = async ({ uuid, propCreator, }) => {
125
123
  return await transportLayer.addConnection({
126
124
  uuid,
127
125
  propCreator: (id) => {
128
126
  const props = propCreator?.(id);
129
127
  return {
130
128
  onReceive: (data) => protoBufOnReceiveHandler(id, data),
131
- ...props
129
+ ...props,
132
130
  };
133
- }
131
+ },
134
132
  });
135
133
  };
136
134
  return {
137
135
  ...transportLayer,
138
136
  addConnection,
139
137
  sendProtobuf,
140
- protoBufOnReceiveHandler
138
+ protoBufOnReceiveHandler,
141
139
  };
142
140
  }
@@ -18,7 +18,7 @@ export type FlashFirmwareProps = {
18
18
  export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
19
19
  releasePortByDefault: boolean;
20
20
  } & DeviceWhispererProps<AppOrMessageLayer>): {
21
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
21
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
22
22
  removeConnection: (uuid: string) => Promise<void>;
23
23
  connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
24
24
  disconnect: (uuid: string, timeout?: number) => Promise<void>;
@@ -1,12 +1,10 @@
1
1
  import { useEffect } from "react";
2
2
  import { ESPLoader, Transport } from "esptool-js";
3
- import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
3
+ import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
4
4
  export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
5
5
  const base = MultiDeviceWhisperer(props);
6
6
  const defaultOnReceive = (uuid, data) => {
7
- const text = typeof data === "string"
8
- ? data
9
- : new TextDecoder().decode(data);
7
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
10
8
  const trimmed = text.trim();
11
9
  if (!trimmed)
12
10
  return;
@@ -45,7 +43,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
45
43
  console.log("Kack!");
46
44
  return;
47
45
  }
48
- ;
49
46
  const { value, done } = await reader.next();
50
47
  if (done || !value)
51
48
  break;
@@ -54,7 +51,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
54
51
  const b = bytes[i];
55
52
  if (inSlipFrame) {
56
53
  // SLIP decoding
57
- if (b === 0xC0) {
54
+ if (b === 0xc0) {
58
55
  if (slipBuffer.length > 0) {
59
56
  // complete SLIP frame received
60
57
  const payload = new Uint8Array(slipBuffer);
@@ -75,10 +72,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
75
72
  escapeNext = false;
76
73
  }
77
74
  else if (escapeNext) {
78
- if (b === 0xDC)
79
- slipBuffer.push(0xC0);
80
- else if (b === 0xDD)
81
- slipBuffer.push(0xDB);
75
+ if (b === 0xdc)
76
+ slipBuffer.push(0xc0);
77
+ else if (b === 0xdd)
78
+ slipBuffer.push(0xdb);
82
79
  else {
83
80
  // protocol violation: discard frame
84
81
  slipBuffer = [];
@@ -86,7 +83,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
86
83
  }
87
84
  escapeNext = false;
88
85
  }
89
- else if (b === 0xDB) {
86
+ else if (b === 0xdb) {
90
87
  escapeNext = true;
91
88
  }
92
89
  else {
@@ -94,7 +91,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
94
91
  }
95
92
  continue;
96
93
  }
97
- if (b === 0xC0 && conn.slipReadWrite) {
94
+ if (b === 0xc0 && conn.slipReadWrite) {
98
95
  // start of a SLIP frame
99
96
  inSlipFrame = true;
100
97
  slipBuffer = [];
@@ -102,7 +99,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
102
99
  continue;
103
100
  }
104
101
  // treat as normal text (correctly)
105
- readBuffer += textDecoder.decode(new Uint8Array([b]), { stream: true });
102
+ readBuffer += textDecoder.decode(new Uint8Array([b]), {
103
+ stream: true,
104
+ });
106
105
  // check for newline
107
106
  let newlineIndex;
108
107
  while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
@@ -149,11 +148,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
149
148
  let port = conn?.port;
150
149
  if (!port) {
151
150
  port = await navigator.serial.requestPort({
152
- filters: [{ usbVendorId: 0x303a }]
151
+ filters: [{ usbVendorId: 0x303a }],
153
152
  });
154
153
  base.updateConnection(uuid, (c) => ({ ...c, port }));
155
154
  }
156
- ;
157
155
  base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
158
156
  const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
159
157
  const transport = new Transport(port, false, false);
@@ -161,7 +159,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
161
159
  const esploader = new ESPLoader({
162
160
  transport,
163
161
  baudrate: use_baudrate,
164
- enableTracing: false
162
+ enableTracing: false,
165
163
  });
166
164
  try {
167
165
  await esploader.main();
@@ -170,7 +168,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
170
168
  console.log("failed to esploader.main()", e);
171
169
  return;
172
170
  }
173
- ;
174
171
  try {
175
172
  const mac = await esploader.chip.readMac(esploader);
176
173
  console.log("Mac from device:", mac);
@@ -182,7 +179,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
182
179
  if (restart_on_connect) {
183
180
  await restartDevice(uuid, transport);
184
181
  }
185
- ;
186
182
  base.updateConnection(uuid, (c) => ({
187
183
  ...c,
188
184
  transport,
@@ -192,7 +188,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
192
188
  }));
193
189
  base.appendLog(uuid, {
194
190
  level: 2,
195
- message: "[✓] Serial connected"
191
+ message: "[✓] Serial connected",
196
192
  });
197
193
  await conn.onConnect?.();
198
194
  await readLoop(uuid, transport);
@@ -201,11 +197,11 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
201
197
  base.updateConnection(uuid, (c) => ({
202
198
  ...c,
203
199
  isConnected: false,
204
- isConnecting: false
200
+ isConnecting: false,
205
201
  }));
206
202
  base.appendLog(uuid, {
207
203
  level: 0,
208
- message: `[x] Serial connection error: ${err?.message || "Unknown error"}`
204
+ message: `[x] Serial connection error: ${err?.message || "Unknown error"}`,
209
205
  });
210
206
  await disconnect(uuid);
211
207
  }
@@ -235,24 +231,47 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
235
231
  }));
236
232
  await conn?.onDisconnect?.();
237
233
  };
238
- const addConnection = async ({ uuid, propCreator }) => {
234
+ const addConnection = async ({ uuid, propCreator, }) => {
239
235
  const port = await navigator.serial.requestPort({
240
- filters: [{ usbVendorId: 0x303a }]
236
+ filters: [{ usbVendorId: 0x303a }],
241
237
  });
238
+ let deviceId = uuid;
239
+ // If no UUID provided, try to read the MAC address before registering the connection
240
+ if (!uuid) {
241
+ // 1. Interrogate the port for the MAC address BEFORE registering the connection
242
+ try {
243
+ const tempTransport = new Transport(port, false, false);
244
+ const tempLoader = new ESPLoader({
245
+ transport: tempTransport,
246
+ baudrate: 115200,
247
+ enableTracing: false,
248
+ });
249
+ await tempLoader.main();
250
+ const mac = await tempLoader.chip.readMac(tempLoader);
251
+ if (mac) {
252
+ deviceId = mac; // Swap 'unnamed_device_0' for the actual MAC
253
+ }
254
+ // Disconnect to release the port so connect() can start fresh
255
+ await tempTransport.disconnect();
256
+ }
257
+ catch (e) {
258
+ console.warn("Failed to pre-fetch MAC address. Falling back to default UUID.", e);
259
+ }
260
+ }
261
+ // 2. Register the connection using the real MAC address
242
262
  const return_uuid = await base.addConnection({
243
- uuid,
263
+ uuid: deviceId,
244
264
  propCreator: (id) => {
245
265
  const props = propCreator?.(id);
246
266
  return {
247
267
  send: (d) => defaultSend(id, d),
248
268
  onReceive: (d) => defaultOnReceive(id, d),
249
269
  port,
250
- // Initial connection state
251
270
  ...base.createInitialConnectionState(id),
252
- // From props
253
- ...props
271
+ deviceMac: deviceId, // Lock it in immediately
272
+ ...props,
254
273
  };
255
- }
274
+ },
256
275
  });
257
276
  const conn = base.getConnection(return_uuid);
258
277
  if (conn?.autoConnect)
@@ -264,11 +283,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
264
283
  await disconnect(uuid);
265
284
  }
266
285
  catch (e) { }
267
- ;
268
286
  base.removeConnection(uuid);
269
287
  };
270
288
  const reconnectAll = async (...connectionProps) => {
271
- const connectionIds = base.connections.map(c => c.uuid);
289
+ const connectionIds = base.connections.map((c) => c.uuid);
272
290
  await Promise.all(connectionIds.map(async (id) => {
273
291
  const c = base.getConnection(id);
274
292
  if (!c)
@@ -284,14 +302,19 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
284
302
  if (!conn || !conn.port || assetsToFlash.length === 0)
285
303
  return;
286
304
  await disconnect(uuid);
287
- base.updateConnection(uuid, (c) => ({ ...c, isFlashing: true, flashProgress: 0, flashError: undefined }));
305
+ base.updateConnection(uuid, (c) => ({
306
+ ...c,
307
+ isFlashing: true,
308
+ flashProgress: 0,
309
+ flashError: undefined,
310
+ }));
288
311
  try {
289
312
  // --- Connect ONCE ---
290
313
  const transport = new Transport(conn.port, true);
291
314
  const esploader = new ESPLoader({
292
315
  transport,
293
316
  baudrate: 921600,
294
- enableTracing: false
317
+ enableTracing: false,
295
318
  });
296
319
  try {
297
320
  await esploader.main();
@@ -300,7 +323,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
300
323
  console.log("failed to esploader.main()", e);
301
324
  return;
302
325
  }
303
- ;
304
326
  // --- Prepare an ARRAY of files for the library ---
305
327
  const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
306
328
  const arrayBuffer = await blob.arrayBuffer();
@@ -320,7 +342,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
320
342
  // You can enhance progress reporting to show which file is being flashed
321
343
  const progress = (written / total) * 100;
322
344
  console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
323
- base.updateConnection(uuid, (c) => ({ ...c, flashProgress: progress }));
345
+ base.updateConnection(uuid, (c) => ({
346
+ ...c,
347
+ flashProgress: progress,
348
+ }));
324
349
  },
325
350
  };
326
351
  // --- Call writeFlash ONCE with all files ---
@@ -331,7 +356,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
331
356
  catch (e) {
332
357
  console.log("failed to esploader.writeFlash", e);
333
358
  }
334
- ;
335
359
  // --- Disconnect ---
336
360
  await esploader.after();
337
361
  try {
@@ -343,7 +367,11 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
343
367
  await conn.port?.writable?.close();
344
368
  await conn.port?.close();
345
369
  }
346
- base.updateConnection(uuid, (c) => ({ ...c, isFlashing: false, flashProgress: 100 }));
370
+ base.updateConnection(uuid, (c) => ({
371
+ ...c,
372
+ isFlashing: false,
373
+ flashProgress: 100,
374
+ }));
347
375
  }
348
376
  catch (e) {
349
377
  console.error(`[${uuid}] Flashing failed:`, e);
@@ -364,6 +392,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
364
392
  connect,
365
393
  disconnect,
366
394
  reconnectAll,
367
- handleFlashFirmware
395
+ handleFlashFirmware,
368
396
  };
369
397
  }
@@ -13,8 +13,9 @@ export type MQTTConnectionState = DeviceConnectionState & {
13
13
  pingFunction?: (props?: any) => void;
14
14
  touchHeartbeat?: () => void;
15
15
  };
16
- export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, ...props }: {
17
- serverUrl: string;
16
+ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, enableWatchdog, ...props }: {
17
+ serverUrl?: string;
18
+ serverUrlFnc?: () => Promise<string> | string;
18
19
  uuidFromMessage: (topic: string, payload: Buffer<ArrayBufferLike>) => string;
19
20
  subTopicFromUuid?: (uuid: string) => string;
20
21
  pubTopicFromUuid?: (uuid: string) => string;
@@ -24,13 +25,14 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
24
25
  password?: string;
25
26
  serverAutoConnect?: boolean;
26
27
  serverConnectOn?: boolean;
28
+ enableWatchdog?: boolean;
27
29
  } & DeviceWhispererProps<AppOrMessageLayer>): {
28
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
30
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
29
31
  removeConnection: (uuid: string) => Promise<void>;
30
32
  connect: (uuid: string) => Promise<void>;
31
33
  disconnect: (uuid: string) => Promise<void>;
32
34
  reconnectAll: () => Promise<void>;
33
- connectToMQTTServer: () => (() => void) | undefined;
35
+ connectToMQTTServer: () => Promise<void>;
34
36
  focussedConnection: string | undefined;
35
37
  setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
36
38
  connections: AppOrMessageLayer[];
@@ -1,10 +1,12 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
2
+ import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
3
3
  import mqtt from "mqtt";
4
- export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicFromUuid = undefined, pubTopicFromUuid = undefined, serverPort = 8883, clientId = undefined, username = undefined, password = undefined, serverAutoConnect = true, serverConnectOn = false, ...props }) {
4
+ export function MQTTMultiDeviceWhisperer({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid = undefined, pubTopicFromUuid = undefined, serverPort = 443, clientId = undefined, username = undefined, password = undefined, serverAutoConnect = true, serverConnectOn = false, enableWatchdog = true, ...props }) {
5
5
  const base = MultiDeviceWhisperer(props);
6
6
  const clientRef = useRef(null);
7
7
  const isUnmountedRef = useRef(false);
8
+ // ✨ NEW: Manage our own reconnect timer
9
+ const reconnectTimeoutRef = useRef(null);
8
10
  const watchdogTimers = useRef({});
9
11
  const addingConnections = useRef(new Set());
10
12
  const topicCallbacksRef = useRef(new Map());
@@ -84,47 +86,71 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
84
86
  }
85
87
  };
86
88
  };
87
- const connectToMQTTServer = () => {
89
+ const connectToMQTTServer = async () => {
90
+ if (!serverUrl && !serverUrlFnc) {
91
+ console.error("MQTT MultiDeviceWhisperer requires either serverUrl or serverUrlFnc");
92
+ return;
93
+ }
94
+ if (reconnectTimeoutRef.current)
95
+ clearTimeout(reconnectTimeoutRef.current);
88
96
  isUnmountedRef.current = false;
89
97
  if (clientRef.current) {
90
98
  if (clientRef.current.connected) {
91
99
  base.setIsReady(true);
92
100
  return;
93
101
  }
102
+ clientRef.current.removeAllListeners();
94
103
  clientRef.current.end(true);
95
104
  }
96
105
  try {
97
- const new_client = mqtt.connect(serverUrl, {
106
+ const finalUrl = serverUrlFnc ? await serverUrlFnc() : serverUrl;
107
+ if (!finalUrl)
108
+ throw new Error("Generated MQTT URL was undefined.");
109
+ const options = {
98
110
  port: serverPort,
99
111
  clientId: clientId,
100
112
  username,
101
113
  password,
102
114
  clean: true,
103
115
  keepalive: 30,
104
- reconnectPeriod: 3000,
105
- });
116
+ reconnectPeriod: 0, // We now handle this ourselves
117
+ };
118
+ const new_client = mqtt.connect(finalUrl, options);
106
119
  new_client.on("connect", () => {
107
120
  console.log("MQTT Whisperer Connected");
108
121
  base.setIsReady(true);
109
- // Re-subscribe custom topic callbacks after reconnect.
122
+ // Re-subscribe topic callbacks
110
123
  topicCallbacksRef.current.forEach((_callbacks, topic) => {
111
124
  new_client.subscribe(topic, { qos: 1 }, (err) => {
112
125
  if (err)
113
126
  console.error("Topic callback subscribe failed:", err);
114
127
  });
115
128
  });
129
+ // Since we create a brand new client on reconnect, we MUST re-subscribe
130
+ // to all actively managed device connections.
131
+ base.connections.forEach((conn) => {
132
+ const topic = subTopicFromUuid?.(conn.uuid) ?? conn.uuid;
133
+ new_client.subscribe(topic, { qos: 1 });
134
+ });
116
135
  });
117
- new_client.on("reconnect", () => {
118
- console.log("MQTT Whisperer Reconnecting...");
119
- base.setIsReady(false);
120
- });
121
- new_client.on("close", () => {
122
- console.log("MQTT Whisperer Closed");
136
+ const handleReconnectCycle = () => {
123
137
  base.setIsReady(false);
124
- });
138
+ if (isUnmountedRef.current)
139
+ return;
140
+ console.log("MQTT Whisperer closed. Scheduling async reconnect...");
141
+ // Ensure we only queue one reconnect attempt
142
+ if (reconnectTimeoutRef.current)
143
+ clearTimeout(reconnectTimeoutRef.current);
144
+ if (serverAutoConnect || serverConnectOn) {
145
+ reconnectTimeoutRef.current = setTimeout(() => {
146
+ connectToMQTTServer();
147
+ }, 3000); // 3 second backoff
148
+ }
149
+ };
150
+ new_client.on("close", handleReconnectCycle);
125
151
  new_client.on("error", (err) => {
126
- base.setIsReady(false);
127
152
  console.error("MQTT Whisperer Error:", err);
153
+ handleReconnectCycle();
128
154
  });
129
155
  new_client.on("message", (topic, payload) => {
130
156
  if (isUnmountedRef.current)
@@ -155,26 +181,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
155
181
  catch (err) {
156
182
  console.error("MQTT init failed:", err);
157
183
  base.setIsReady(false);
158
- }
159
- return () => {
160
- isUnmountedRef.current = true;
161
- base.setIsReady(false);
162
- Object.keys(watchdogTimers.current).forEach((uuid) => {
163
- const timers = watchdogTimers.current[uuid];
164
- if (timers) {
165
- clearTimeout(timers.ping);
166
- clearTimeout(timers.warn);
167
- clearTimeout(timers.fail);
168
- }
169
- });
170
- watchdogTimers.current = {};
171
- topicCallbacksRef.current.clear();
172
- if (clientRef.current) {
173
- clientRef.current.removeAllListeners();
174
- clientRef.current.end(true);
184
+ // If fetching the URL fails, try again in 3 seconds
185
+ if (!isUnmountedRef.current && (serverAutoConnect || serverConnectOn)) {
186
+ reconnectTimeoutRef.current = setTimeout(() => {
187
+ connectToMQTTServer();
188
+ }, 3000);
175
189
  }
176
- clientRef.current = null;
177
- };
190
+ }
178
191
  };
179
192
  const connect = async (uuid) => {
180
193
  const conn = base.getConnection(uuid);
@@ -200,7 +213,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
200
213
  return;
201
214
  const topic = subTopicFromUuid?.(uuid) ?? uuid;
202
215
  // Keep topic subscribed if callbacks or other device-connections still depend on it.
203
- if (hasCallbackForTopic(topic) || hasOtherConnectionUsingTopic(topic, uuid)) {
216
+ if (hasCallbackForTopic(topic) ||
217
+ hasOtherConnectionUsingTopic(topic, uuid)) {
204
218
  return;
205
219
  }
206
220
  if (clientRef.current.connected && !clientRef.current.disconnecting) {
@@ -213,12 +227,12 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
213
227
  });
214
228
  }
215
229
  else {
216
- console.warn("Skipped subscribe - client disconnected or unmounted");
230
+ console.warn("Skipped unsubscribe - client disconnected or unmounted");
217
231
  }
218
232
  };
219
233
  function touchHeartbeat(uuid) {
220
- if (isUnmountedRef.current)
221
- return; // Stop if unmounted
234
+ if (isUnmountedRef.current || !enableWatchdog)
235
+ return;
222
236
  clearTimeout(watchdogTimers.current[uuid]?.ping);
223
237
  clearTimeout(watchdogTimers.current[uuid]?.warn);
224
238
  clearTimeout(watchdogTimers.current[uuid]?.fail);
@@ -226,7 +240,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
226
240
  base.updateConnection(uuid, (c) => ({
227
241
  ...c,
228
242
  isConnected: true,
229
- isConnecting: false
243
+ isConnecting: false,
230
244
  }));
231
245
  const ping = setTimeout(() => {
232
246
  if (isUnmountedRef.current)
@@ -236,12 +250,20 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
236
250
  const warn = setTimeout(() => {
237
251
  if (isUnmountedRef.current)
238
252
  return;
239
- base.updateConnection(uuid, (c) => ({ ...c, isConnected: false, isConnecting: true }));
253
+ base.updateConnection(uuid, (c) => ({
254
+ ...c,
255
+ isConnected: false,
256
+ isConnecting: true,
257
+ }));
240
258
  }, 30000);
241
259
  const fail = setTimeout(() => {
242
260
  if (isUnmountedRef.current)
243
261
  return;
244
- base.updateConnection(uuid, (c) => ({ ...c, isConnected: false, isConnecting: false }));
262
+ base.updateConnection(uuid, (c) => ({
263
+ ...c,
264
+ isConnected: false,
265
+ isConnecting: false,
266
+ }));
245
267
  }, 60000);
246
268
  watchdogTimers.current[uuid] = { ping, warn, fail };
247
269
  }
@@ -274,15 +296,16 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
274
296
  const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
275
297
  await publish(topic, data, uuid);
276
298
  };
277
- const addConnection = async ({ uuid, propCreator }) => {
299
+ const addConnection = async ({ uuid, propCreator, }) => {
278
300
  if (!clientRef.current || isUnmountedRef.current)
279
- return;
301
+ return "";
280
302
  if (!uuid) {
281
303
  Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
282
- return;
304
+ return "";
283
305
  }
284
- if (base.connections.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
285
- return;
306
+ if (base.connections.some((c) => c.uuid === uuid) ||
307
+ addingConnections.current.has(uuid)) {
308
+ return "";
286
309
  }
287
310
  await base.addConnection({
288
311
  uuid,
@@ -297,12 +320,10 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
297
320
  touchHeartbeat: () => touchHeartbeat(id),
298
321
  // Initial connection state
299
322
  ...base.createInitialConnectionState(id),
300
- // From props
301
- ...props
323
+ ...props,
302
324
  };
303
- }
325
+ },
304
326
  });
305
- // Delete this adding connections item
306
327
  addingConnections.current.delete(uuid);
307
328
  // Connect immediately
308
329
  const conn = base.getConnection(uuid);
@@ -315,7 +336,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
315
336
  base.removeConnection(uuid);
316
337
  };
317
338
  const reconnectAll = async () => {
318
- const connectionIds = base.connections.map(c => c.uuid);
339
+ const connectionIds = base.connections.map((c) => c.uuid);
319
340
  await Promise.all(connectionIds.map(async (id) => {
320
341
  const c = base.getConnection(id);
321
342
  if (!c)
@@ -328,8 +349,27 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
328
349
  useEffect(() => {
329
350
  if (!(serverAutoConnect || serverConnectOn))
330
351
  return;
331
- const cleanup = connectToMQTTServer();
332
- return cleanup;
352
+ connectToMQTTServer();
353
+ return () => {
354
+ isUnmountedRef.current = true;
355
+ if (reconnectTimeoutRef.current)
356
+ clearTimeout(reconnectTimeoutRef.current);
357
+ Object.keys(watchdogTimers.current).forEach((uuid) => {
358
+ const timers = watchdogTimers.current[uuid];
359
+ if (timers) {
360
+ clearTimeout(timers.ping);
361
+ clearTimeout(timers.warn);
362
+ clearTimeout(timers.fail);
363
+ }
364
+ });
365
+ watchdogTimers.current = {};
366
+ topicCallbacksRef.current.clear();
367
+ if (clientRef.current) {
368
+ clientRef.current.removeAllListeners();
369
+ clientRef.current.end(true);
370
+ }
371
+ clientRef.current = null;
372
+ };
333
373
  }, [serverUrl, serverConnectOn]);
334
374
  return {
335
375
  ...base,
@@ -338,6 +378,6 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
338
378
  connect,
339
379
  disconnect,
340
380
  reconnectAll,
341
- connectToMQTTServer
381
+ connectToMQTTServer,
342
382
  };
343
383
  }
@@ -13,8 +13,8 @@ export declare class UsbTransport {
13
13
  */
14
14
  connect(baudRate?: number): Promise<void>;
15
15
  /**
16
- * Writes bytes to the device
17
- */
16
+ * Writes bytes to the device
17
+ */
18
18
  write(data: Uint8Array): Promise<void>;
19
19
  /**
20
20
  * Reads bytes from the device.
@@ -39,7 +39,7 @@ export type SerialConnectionState = DeviceConnectionState & {
39
39
  slipReadWrite?: boolean;
40
40
  };
41
41
  export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: DeviceWhispererProps<AppOrMessageLayer>): {
42
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
42
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
43
43
  removeConnection: (uuid: string) => Promise<void>;
44
44
  connect: (uuid: string, baudrate?: number) => Promise<void>;
45
45
  disconnect: (uuid: string) => Promise<void>;
@@ -1,4 +1,4 @@
1
- import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
1
+ import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  /*
4
4
  ┌────────────────────────────────┐
@@ -31,16 +31,19 @@ export class UsbTransport {
31
31
  const interfaces = this.device.configuration?.interfaces || [];
32
32
  let ctrlIface;
33
33
  let dataIface;
34
- dataIface = interfaces.find(iface => {
34
+ dataIface = interfaces.find((iface) => {
35
35
  const endpoints = iface.alternate.endpoints;
36
- return endpoints.some(e => e.direction === 'in' && e.type === 'bulk') &&
37
- endpoints.some(e => e.direction === 'out' && e.type === 'bulk');
36
+ return (endpoints.some((e) => e.direction === "in" && e.type === "bulk") &&
37
+ endpoints.some((e) => e.direction === "out" && e.type === "bulk"));
38
38
  });
39
39
  if (dataIface) {
40
40
  this.dataInterface = dataIface.interfaceNumber;
41
- ctrlIface = interfaces.find(i => i.interfaceNumber === this.dataInterface - 1)
42
- || interfaces.find(i => i.alternate.interfaceClass === 2);
43
- this.controlInterface = ctrlIface ? ctrlIface.interfaceNumber : this.dataInterface;
41
+ ctrlIface =
42
+ interfaces.find((i) => i.interfaceNumber === this.dataInterface - 1) ||
43
+ interfaces.find((i) => i.alternate.interfaceClass === 2);
44
+ this.controlInterface = ctrlIface
45
+ ? ctrlIface.interfaceNumber
46
+ : this.dataInterface;
44
47
  }
45
48
  if (!dataIface) {
46
49
  throw new Error("No serial-compatible Bulk interface found.");
@@ -62,14 +65,14 @@ export class UsbTransport {
62
65
  throw new Error(`Failed to claim Data Interface. Android OS has locked the device driver. Try using a 'Vendor Specific' USB Class device or a native Serial App workaround.`);
63
66
  }
64
67
  const endpoints = dataIface.alternate.endpoints;
65
- this.endpointIn = endpoints.find(e => e.direction === 'in' && e.type === 'bulk').endpointNumber;
66
- this.endpointOut = endpoints.find(e => e.direction === 'out' && e.type === 'bulk').endpointNumber;
68
+ this.endpointIn = endpoints.find((e) => e.direction === "in" && e.type === "bulk").endpointNumber;
69
+ this.endpointOut = endpoints.find((e) => e.direction === "out" && e.type === "bulk").endpointNumber;
67
70
  await this.setBaudRate(baudRate);
68
71
  await this.setSignals({ dtr: false, rts: false });
69
72
  }
70
73
  /**
71
- * Writes bytes to the device
72
- */
74
+ * Writes bytes to the device
75
+ */
73
76
  async write(data) {
74
77
  if (!this.device.opened)
75
78
  return;
@@ -84,7 +87,7 @@ export class UsbTransport {
84
87
  return null;
85
88
  try {
86
89
  const result = await this.device.transferIn(this.endpointIn, length);
87
- if (result.status === 'ok' && result.data) {
90
+ if (result.status === "ok" && result.data) {
88
91
  return new Uint8Array(result.data.buffer);
89
92
  }
90
93
  }
@@ -100,13 +103,13 @@ export class UsbTransport {
100
103
  async setSignals({ dtr, rts }) {
101
104
  if (!this.device.opened)
102
105
  return;
103
- const value = (Number(dtr) | (Number(rts) << 1));
106
+ const value = Number(dtr) | (Number(rts) << 1);
104
107
  await this.device.controlTransferOut({
105
- requestType: 'class',
106
- recipient: 'interface',
108
+ requestType: "class",
109
+ recipient: "interface",
107
110
  request: UsbTransport.SET_CONTROL_LINE_STATE,
108
111
  value: value,
109
- index: this.controlInterface
112
+ index: this.controlInterface,
110
113
  });
111
114
  }
112
115
  async setBaudRate(baud) {
@@ -121,11 +124,11 @@ export class UsbTransport {
121
124
  view.setUint8(5, 0);
122
125
  view.setUint8(6, 8);
123
126
  await this.device.controlTransferOut({
124
- requestType: 'class',
125
- recipient: 'interface',
127
+ requestType: "class",
128
+ recipient: "interface",
126
129
  request: UsbTransport.SET_LINE_CODING,
127
130
  value: 0,
128
- index: this.controlInterface
131
+ index: this.controlInterface,
129
132
  }, buffer);
130
133
  }
131
134
  async disconnect() {
@@ -171,7 +174,10 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
171
174
  const asText = decoder.decode(bytes);
172
175
  const combined = conn.readBufferLeftover + asText;
173
176
  const lines = combined.split("\n");
174
- base.updateConnection(uuid, (c) => ({ ...c, readBufferLeftover: lines.pop() || "" }));
177
+ base.updateConnection(uuid, (c) => ({
178
+ ...c,
179
+ readBufferLeftover: lines.pop() || "",
180
+ }));
175
181
  for (const line of lines) {
176
182
  if (line.trim()) {
177
183
  base.appendLog(uuid, { level: 2, message: line.trim() });
@@ -204,7 +210,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
204
210
  const bytes = await transport.read(64);
205
211
  if (!bytes || bytes.length === 0) {
206
212
  // Small delay to prevent CPU spinning if device sends nothing
207
- await new Promise(r => setTimeout(r, 10));
213
+ await new Promise((r) => setTimeout(r, 10));
208
214
  continue;
209
215
  }
210
216
  // --- Same Decoding Logic as before ---
@@ -212,7 +218,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
212
218
  const b = bytes[i];
213
219
  const currentConn = base.getConnection(uuid); // Refresh ref
214
220
  if (inSlipFrame) {
215
- if (b === 0xC0) {
221
+ if (b === 0xc0) {
216
222
  if (slipBuffer.length > 0) {
217
223
  const payload = new Uint8Array(slipBuffer);
218
224
  currentConn?.onReceive?.(payload);
@@ -222,14 +228,14 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
222
228
  escapeNext = false;
223
229
  }
224
230
  else if (escapeNext) {
225
- if (b === 0xDC)
226
- slipBuffer.push(0xC0);
227
- else if (b === 0xDD)
228
- slipBuffer.push(0xDB);
231
+ if (b === 0xdc)
232
+ slipBuffer.push(0xc0);
233
+ else if (b === 0xdd)
234
+ slipBuffer.push(0xdb);
229
235
  slipBuffer = []; // reset on error?
230
236
  escapeNext = false;
231
237
  }
232
- else if (b === 0xDB) {
238
+ else if (b === 0xdb) {
233
239
  escapeNext = true;
234
240
  }
235
241
  else {
@@ -237,7 +243,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
237
243
  }
238
244
  continue;
239
245
  }
240
- if (b === 0xC0 && currentConn?.slipReadWrite) {
246
+ if (b === 0xc0 && currentConn?.slipReadWrite) {
241
247
  inSlipFrame = true;
242
248
  slipBuffer = [];
243
249
  continue;
@@ -281,18 +287,18 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
281
287
  // Close existing if open
282
288
  if (conn.transport)
283
289
  await disconnect(uuid);
284
- base.updateConnection(uuid, c => ({ ...c, isConnecting: true }));
290
+ base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
285
291
  const transport = new UsbTransport(conn.device);
286
292
  try {
287
293
  await transport.connect(baudrate);
288
294
  // Optional: Auto-restart on connect
289
295
  await restartDevice(uuid, transport);
290
- base.updateConnection(uuid, c => ({
296
+ base.updateConnection(uuid, (c) => ({
291
297
  ...c,
292
298
  transport,
293
299
  baudrate,
294
300
  isConnected: true,
295
- isConnecting: false
301
+ isConnecting: false,
296
302
  }));
297
303
  await conn.onConnect?.();
298
304
  // Start polling loop
@@ -300,7 +306,11 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
300
306
  }
301
307
  catch (err) {
302
308
  console.error(err);
303
- base.updateConnection(uuid, c => ({ ...c, isConnected: false, isConnecting: false }));
309
+ base.updateConnection(uuid, (c) => ({
310
+ ...c,
311
+ isConnected: false,
312
+ isConnecting: false,
313
+ }));
304
314
  await disconnect(uuid);
305
315
  }
306
316
  };
@@ -309,18 +319,18 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
309
319
  if (conn?.transport) {
310
320
  await conn.transport.disconnect();
311
321
  }
312
- base.updateConnection(uuid, c => ({
322
+ base.updateConnection(uuid, (c) => ({
313
323
  ...c,
314
324
  transport: releasePortByDefaultRef.current ? null : c.transport,
315
325
  isConnected: false,
316
- isConnecting: false
326
+ isConnecting: false,
317
327
  }));
318
328
  await conn?.onDisconnect?.();
319
329
  };
320
- const addConnection = async ({ uuid, propCreator }) => {
330
+ const addConnection = async ({ uuid, propCreator, }) => {
321
331
  try {
322
332
  const device = await navigator.usb.requestDevice({
323
- filters: []
333
+ filters: [],
324
334
  });
325
335
  const return_uuid = await base.addConnection({
326
336
  uuid,
@@ -334,9 +344,9 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
334
344
  // Initial connection state
335
345
  ...base.createInitialConnectionState(id),
336
346
  // From props
337
- ...props
347
+ ...props,
338
348
  };
339
- }
349
+ },
340
350
  });
341
351
  const conn = base.getConnection(return_uuid);
342
352
  if (conn?.autoConnect)
@@ -352,7 +362,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
352
362
  base.removeConnection(uuid);
353
363
  };
354
364
  const reconnectAll = async (...connectionProps) => {
355
- const connectionIds = base.connections.map(c => c.uuid);
365
+ const connectionIds = base.connections.map((c) => c.uuid);
356
366
  await Promise.all(connectionIds.map(async (id) => {
357
367
  const c = base.getConnection(id);
358
368
  if (!c)
@@ -362,7 +372,9 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
362
372
  return connect(c.uuid, ...connectionProps);
363
373
  }));
364
374
  };
365
- useEffect(() => { base.setIsReady(true); }, []);
375
+ useEffect(() => {
376
+ base.setIsReady(true);
377
+ }, []);
366
378
  return {
367
379
  ...base,
368
380
  addConnection,
@@ -371,6 +383,6 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
371
383
  disconnect,
372
384
  reconnectAll,
373
385
  releasePortByDefault: releasePortByDefaultState,
374
- setReleasePortByDefault: setReleasePortByDefaultState
386
+ setReleasePortByDefault: setReleasePortByDefaultState,
375
387
  };
376
388
  }
@@ -13,7 +13,7 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
13
13
  server_url: string;
14
14
  server_port: number;
15
15
  }): {
16
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
16
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
17
17
  removeConnection: (uuid: string) => Promise<void>;
18
18
  connect: (uuid: string, attempt?: number) => Promise<void>;
19
19
  disconnect: (uuid: string) => Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { useEffect } from "react";
2
- import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
2
+ import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
3
3
  export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
4
4
  const base = MultiDeviceWhisperer(props);
5
5
  const defaultOnReceive = (uuid, data) => {
@@ -22,7 +22,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
22
22
  const lines = combined.split(/\r?\n/);
23
23
  base.updateConnection(uuid, (c) => ({
24
24
  ...c,
25
- readBufferLeftover: lines.pop() || ""
25
+ readBufferLeftover: lines.pop() || "",
26
26
  }));
27
27
  for (const line of lines) {
28
28
  const trimmed = line.trim();
@@ -38,9 +38,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
38
38
  const conn = base.getConnection(uuid);
39
39
  if (!conn || !conn.ws)
40
40
  return;
41
- const asString = typeof data === "string"
42
- ? data
43
- : btoa(String.fromCharCode(...data));
41
+ const asString = typeof data === "string" ? data : btoa(String.fromCharCode(...data));
44
42
  base.appendLog(uuid, {
45
43
  level: 3,
46
44
  message: asString,
@@ -57,7 +55,11 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
57
55
  console.log(`[client] Closing existing WS before reconnect for ${uuid}`);
58
56
  conn.ws.close();
59
57
  }
60
- base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true, autoConnect: true }));
58
+ base.updateConnection(uuid, (c) => ({
59
+ ...c,
60
+ isConnecting: true,
61
+ autoConnect: true,
62
+ }));
61
63
  try {
62
64
  const pre = server_port !== 443 ? "ws" : "wss";
63
65
  const ws = new WebSocket(`${pre}://${server_url}:${server_port}/ui/${uuid}`);
@@ -68,11 +70,11 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
68
70
  ws,
69
71
  isConnected: true,
70
72
  isConnecting: false,
71
- lastAttempt: attempt
73
+ lastAttempt: attempt,
72
74
  }));
73
75
  base.appendLog(uuid, {
74
76
  level: 2,
75
- message: "[✓] WebSocket connected"
77
+ message: "[✓] WebSocket connected",
76
78
  });
77
79
  conn?.onConnect?.();
78
80
  };
@@ -89,7 +91,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
89
91
  ws.onerror = (err) => {
90
92
  base.appendLog(uuid, {
91
93
  level: 0,
92
- message: `[x] WS error: ${err}`
94
+ message: `[x] WS error: ${err}`,
93
95
  });
94
96
  };
95
97
  ws.onclose = async () => {
@@ -97,7 +99,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
97
99
  ...c,
98
100
  isConnected: false,
99
101
  isConnecting: false,
100
- ws: undefined
102
+ ws: undefined,
101
103
  }));
102
104
  base.appendLog(uuid, { level: 0, message: "[!] WS disconnected" });
103
105
  const updated_conn = base.getConnection(uuid);
@@ -107,7 +109,10 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
107
109
  }
108
110
  // Auto reconnect if enabled
109
111
  if (updated_conn.autoConnect && attempt < MAX_RETRIES) {
110
- base.appendLog(uuid, { level: 2, message: `[~] Reconnecting in ${RETRY_DELAY_MS}ms... (attempt ${attempt + 1})` });
112
+ base.appendLog(uuid, {
113
+ level: 2,
114
+ message: `[~] Reconnecting in ${RETRY_DELAY_MS}ms... (attempt ${attempt + 1})`,
115
+ });
111
116
  setTimeout(() => connect(uuid, attempt + 1), RETRY_DELAY_MS);
112
117
  }
113
118
  };
@@ -119,8 +124,11 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
119
124
  isConnecting: false,
120
125
  logs: [
121
126
  ...c.logs,
122
- { level: 0, message: `[x] WS connection error: ${err?.message || "Unknown error"}` }
123
- ]
127
+ {
128
+ level: 0,
129
+ message: `[x] WS connection error: ${err?.message || "Unknown error"}`,
130
+ },
131
+ ],
124
132
  }));
125
133
  await disconnect(uuid);
126
134
  }
@@ -135,12 +143,12 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
135
143
  isConnecting: false,
136
144
  autoConnect: false,
137
145
  ws: undefined,
138
- readBufferLeftover: ""
146
+ readBufferLeftover: "",
139
147
  }));
140
148
  conn.ws.close();
141
149
  await conn?.onDisconnect?.();
142
150
  };
143
- const addConnection = async ({ uuid, propCreator }) => {
151
+ const addConnection = async ({ uuid, propCreator, }) => {
144
152
  return await base.addConnection({
145
153
  uuid,
146
154
  propCreator: (id) => {
@@ -148,9 +156,9 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
148
156
  return {
149
157
  send: props?.send || ((d) => defaultSend(id, d)),
150
158
  onReceive: props?.onReceive || ((d) => defaultOnReceive(id, d)),
151
- ...props
159
+ ...props,
152
160
  };
153
- }
161
+ },
154
162
  });
155
163
  };
156
164
  const removeConnection = async (uuid) => {
@@ -174,7 +182,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
174
182
  }
175
183
  };
176
184
  const reconnectAll = async (...connectionProps) => {
177
- const connectionIds = base.connections.map(c => c.uuid);
185
+ const connectionIds = base.connections.map((c) => c.uuid);
178
186
  await Promise.all(connectionIds.map(async (id) => {
179
187
  const c = base.getConnection(id);
180
188
  if (!c)
@@ -194,6 +202,6 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
194
202
  connect,
195
203
  disconnect,
196
204
  checkForNewDevices,
197
- reconnectAll
205
+ reconnectAll,
198
206
  };
199
207
  }
@@ -1,3 +1 @@
1
- ;
2
- ;
3
1
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ota-hub-reactjs",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",