ota-hub-reactjs 0.0.18 → 0.1.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.
@@ -31,12 +31,13 @@ export type DeviceConnectionState = {
31
31
  export declare function createDefaultInitialDeviceState<T extends DeviceConnectionState>(uuid: string, props?: any): T;
32
32
  export type DeviceWhispererProps<T extends DeviceConnectionState> = {
33
33
  createInitialConnectionState?: (uuid: string) => Partial<T>;
34
+ setFocussedConnectionOnAddConnection?: boolean;
34
35
  };
35
- export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
36
+ export declare function useMultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, setFocussedConnectionOnAddConnection, }?: DeviceWhispererProps<T>): {
36
37
  focussedConnection: string | undefined;
37
38
  setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
38
39
  connections: T[];
39
- addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
40
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<T>) => Promise<string>;
40
41
  removeConnection: (uuid: string) => void;
41
42
  connect: (_uuid: string) => void;
42
43
  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,13 +10,13 @@ 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.
18
17
  Any e.g. Serial without Protobuf, or LoRaWAN etc. devices would be handled via a separate device whisperer
19
18
  */
20
- export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
19
+ export function useMultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, setFocussedConnectionOnAddConnection, } = {}) {
21
20
  const [connections, setConnections] = useState([]);
22
21
  const connectionsRef = useRef(new Map());
23
22
  const [isReady, setIsReady] = useState(false);
@@ -45,16 +44,19 @@ 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()));
57
+ // Automatically set to the latest connection
58
+ if (!!setFocussedConnectionOnAddConnection)
59
+ setFocussedConnection(uuid);
58
60
  return uuid;
59
61
  };
60
62
  // ---------- REMOVE ----------
@@ -82,6 +84,6 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
82
84
  appendLog,
83
85
  isReady,
84
86
  setIsReady,
85
- createInitialConnectionState
87
+ createInitialConnectionState,
86
88
  };
87
89
  }
@@ -1,13 +1,13 @@
1
- import { AddConnectionProps, DeviceConnectionState, MultiDeviceWhisperer } from "../base/device-whisperer.js";
1
+ import { AddConnectionProps, DeviceConnectionState, useMultiDeviceWhisperer } from "../base/device-whisperer.js";
2
2
  export type TopicHandlerContext<AppLayer extends DeviceConnectionState> = {
3
- base: ReturnType<typeof MultiDeviceWhisperer<AppLayer>>;
3
+ base: ReturnType<typeof useMultiDeviceWhisperer<AppLayer>>;
4
4
  uuid: string;
5
5
  };
6
6
  export type TopicHandlerMap<AppLayer extends DeviceConnectionState, Topic extends string | number, Message = any> = {
7
7
  [K in Topic]?: (message: Message, context: TopicHandlerContext<AppLayer>) => void;
8
8
  };
9
9
  export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState, Topic extends string | number, MessageRX = any, MessageTX = any> = {
10
- transportLayer: ReturnType<typeof MultiDeviceWhisperer<AppLayer>>;
10
+ transportLayer: ReturnType<typeof useMultiDeviceWhisperer<AppLayer>>;
11
11
  encodeRX: (message: MessageTX) => Uint8Array;
12
12
  decodeTX: (bytes: Uint8Array) => MessageRX;
13
13
  messageTypeField: keyof MessageRX;
@@ -15,8 +15,8 @@ export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState,
15
15
  HEADER?: Uint8Array;
16
16
  expectLength?: boolean;
17
17
  };
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>;
18
+ export declare function useProtobufMultiDeviceWhisperer<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>;
20
20
  sendProtobuf: (uuid: string, message: MessageTX) => void;
21
21
  protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
22
22
  focussedConnection: string | undefined;
@@ -1,4 +1,4 @@
1
- export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER = new Uint8Array([]), }) {
1
+ export function useProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER = new Uint8Array([]), }) {
2
2
  // --- Utils ---
3
3
  const concatUint8Arrays = (a, b) => {
4
4
  const result = new Uint8Array(a.length + b.length);
@@ -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
  }
@@ -5,6 +5,7 @@ export type ESP32ConnectionState = DeviceConnectionState & {
5
5
  baudrate?: number;
6
6
  transport?: Transport;
7
7
  esp?: ESPLoader;
8
+ reader?: AsyncGenerator<Uint8Array>;
8
9
  slipReadWrite?: boolean;
9
10
  isFlashing: boolean;
10
11
  flashProgress: number;
@@ -15,10 +16,10 @@ export type FlashFirmwareProps = {
15
16
  firmwareBlob?: Blob;
16
17
  fileArray?: FlashOptions["fileArray"];
17
18
  };
18
- export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
19
+ export declare function useESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
19
20
  releasePortByDefault: boolean;
20
21
  } & DeviceWhispererProps<AppOrMessageLayer>): {
21
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
22
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
22
23
  removeConnection: (uuid: string) => Promise<void>;
23
24
  connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
24
25
  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";
4
- export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
5
- const base = MultiDeviceWhisperer(props);
3
+ import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
4
+ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
5
+ const base = useMultiDeviceWhisperer(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;
@@ -37,15 +35,15 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
37
35
  let slipBuffer = []; // accumulate SLIP frames
38
36
  let inSlipFrame = false; // are we inside a SLIP frame?
39
37
  let escapeNext = false;
38
+ const reader = transport.rawRead();
39
+ base.updateConnection(uuid, (c) => ({ ...c, reader }));
40
40
  try {
41
- const reader = transport.rawRead();
42
41
  while (true) {
43
42
  const conn = base.getConnection(uuid);
44
43
  if (!conn) {
45
44
  console.log("Kack!");
46
45
  return;
47
46
  }
48
- ;
49
47
  const { value, done } = await reader.next();
50
48
  if (done || !value)
51
49
  break;
@@ -54,7 +52,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
54
52
  const b = bytes[i];
55
53
  if (inSlipFrame) {
56
54
  // SLIP decoding
57
- if (b === 0xC0) {
55
+ if (b === 0xc0) {
58
56
  if (slipBuffer.length > 0) {
59
57
  // complete SLIP frame received
60
58
  const payload = new Uint8Array(slipBuffer);
@@ -75,10 +73,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
75
73
  escapeNext = false;
76
74
  }
77
75
  else if (escapeNext) {
78
- if (b === 0xDC)
79
- slipBuffer.push(0xC0);
80
- else if (b === 0xDD)
81
- slipBuffer.push(0xDB);
76
+ if (b === 0xdc)
77
+ slipBuffer.push(0xc0);
78
+ else if (b === 0xdd)
79
+ slipBuffer.push(0xdb);
82
80
  else {
83
81
  // protocol violation: discard frame
84
82
  slipBuffer = [];
@@ -86,7 +84,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
86
84
  }
87
85
  escapeNext = false;
88
86
  }
89
- else if (b === 0xDB) {
87
+ else if (b === 0xdb) {
90
88
  escapeNext = true;
91
89
  }
92
90
  else {
@@ -94,7 +92,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
94
92
  }
95
93
  continue;
96
94
  }
97
- if (b === 0xC0 && conn.slipReadWrite) {
95
+ if (b === 0xc0 && conn.slipReadWrite) {
98
96
  // start of a SLIP frame
99
97
  inSlipFrame = true;
100
98
  slipBuffer = [];
@@ -102,7 +100,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
102
100
  continue;
103
101
  }
104
102
  // treat as normal text (correctly)
105
- readBuffer += textDecoder.decode(new Uint8Array([b]), { stream: true });
103
+ readBuffer += textDecoder.decode(new Uint8Array([b]), {
104
+ stream: true,
105
+ });
106
106
  // check for newline
107
107
  let newlineIndex;
108
108
  while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
@@ -122,6 +122,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
122
122
  });
123
123
  }
124
124
  finally {
125
+ // Clear reader state so it's fresh for next connect
126
+ base.updateConnection(uuid, (c) => ({ ...c, reader: undefined }));
125
127
  base.appendLog(uuid, {
126
128
  level: 0,
127
129
  message: "[!] Serial disconnected",
@@ -149,11 +151,25 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
149
151
  let port = conn?.port;
150
152
  if (!port) {
151
153
  port = await navigator.serial.requestPort({
152
- filters: [{ usbVendorId: 0x303a }]
154
+ filters: [{ usbVendorId: 0x303a }],
153
155
  });
154
156
  base.updateConnection(uuid, (c) => ({ ...c, port }));
155
157
  }
156
- ;
158
+ else {
159
+ try {
160
+ const oldInfo = port.getInfo();
161
+ const authorizedPorts = await navigator.serial.getPorts();
162
+ const freshPort = authorizedPorts.find((p) => p.getInfo().usbVendorId === oldInfo.usbVendorId &&
163
+ p.getInfo().usbProductId === oldInfo.usbProductId);
164
+ if (freshPort) {
165
+ port = freshPort; // Swap out the dead pointer
166
+ base.updateConnection(uuid, (c) => ({ ...c, port }));
167
+ }
168
+ }
169
+ catch (e) {
170
+ console.warn(`[${uuid}] Failed to silently heal port reference:`, e);
171
+ }
172
+ }
157
173
  base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
158
174
  const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
159
175
  const transport = new Transport(port, false, false);
@@ -161,7 +177,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
161
177
  const esploader = new ESPLoader({
162
178
  transport,
163
179
  baudrate: use_baudrate,
164
- enableTracing: false
180
+ enableTracing: false,
165
181
  });
166
182
  try {
167
183
  await esploader.main();
@@ -170,7 +186,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
170
186
  console.log("failed to esploader.main()", e);
171
187
  return;
172
188
  }
173
- ;
174
189
  try {
175
190
  const mac = await esploader.chip.readMac(esploader);
176
191
  console.log("Mac from device:", mac);
@@ -182,36 +197,50 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
182
197
  if (restart_on_connect) {
183
198
  await restartDevice(uuid, transport);
184
199
  }
185
- ;
186
200
  base.updateConnection(uuid, (c) => ({
187
201
  ...c,
188
202
  transport,
203
+ esp: esploader,
189
204
  baudrate: use_baudrate,
190
205
  isConnected: true,
191
206
  isConnecting: false,
192
207
  }));
193
208
  base.appendLog(uuid, {
194
209
  level: 2,
195
- message: "[✓] Serial connected"
210
+ message: "[✓] Serial connected",
196
211
  });
197
212
  await conn.onConnect?.();
198
- await readLoop(uuid, transport);
213
+ // FIRE AND FORGET
214
+ // We initiate the read loop but DO NOT await it, returning control immediately.
215
+ readLoop(uuid, transport).catch((e) => console.error(`[${uuid}] Background read loop failed:`, e));
199
216
  }
200
217
  catch (err) {
201
218
  base.updateConnection(uuid, (c) => ({
202
219
  ...c,
203
220
  isConnected: false,
204
- isConnecting: false
221
+ isConnecting: false,
205
222
  }));
206
223
  base.appendLog(uuid, {
207
224
  level: 0,
208
- message: `[x] Serial connection error: ${err?.message || "Unknown error"}`
225
+ message: `[x] Serial connection error: ${err?.message || "Unknown error"}`,
209
226
  });
210
227
  await disconnect(uuid);
211
228
  }
212
229
  };
213
230
  const disconnect = async (uuid, timeout = 2000) => {
214
231
  const conn = base.getConnection(uuid);
232
+ // 1. POLITELY KILL THE READER
233
+ // If we have an active reader generator in state, call return() to
234
+ // gracefully resolve the pending reader.next() in the background loop
235
+ if (conn?.reader && typeof conn.reader.return === "function") {
236
+ try {
237
+ // @ts-ignore - The underlying AsyncGenerator handles return gracefully
238
+ await conn.reader.return();
239
+ }
240
+ catch (e) {
241
+ console.warn(`[${uuid}] Error returning generator lock:`, e);
242
+ }
243
+ }
215
244
  if (conn?.transport) {
216
245
  try {
217
246
  // Attempt disconnect, but don’t hang if the port is crashed
@@ -227,32 +256,56 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
227
256
  // Always clear the transport and reset connection state
228
257
  base.updateConnection(uuid, (c) => ({
229
258
  ...c,
230
- port: releasePortByDefault ? null : c.port,
231
- transport: releasePortByDefault ? null : c.transport,
259
+ port: releasePortByDefault ? undefined : c.port,
260
+ transport: releasePortByDefault ? undefined : c.transport,
261
+ reader: undefined, // Guarantee reader is cleared out
232
262
  isConnected: false,
233
263
  isConnecting: false,
234
264
  autoConnect: false,
235
265
  }));
236
266
  await conn?.onDisconnect?.();
237
267
  };
238
- const addConnection = async ({ uuid, propCreator }) => {
268
+ const addConnection = async ({ uuid, propCreator, }) => {
239
269
  const port = await navigator.serial.requestPort({
240
- filters: [{ usbVendorId: 0x303a }]
270
+ filters: [{ usbVendorId: 0x303a }],
241
271
  });
272
+ let deviceId = uuid;
273
+ // If no UUID provided, try to read the MAC address before registering the connection
274
+ if (!uuid) {
275
+ // 1. Interrogate the port for the MAC address BEFORE registering the connection
276
+ try {
277
+ const tempTransport = new Transport(port, false, false);
278
+ const tempLoader = new ESPLoader({
279
+ transport: tempTransport,
280
+ baudrate: 115200,
281
+ enableTracing: false,
282
+ });
283
+ await tempLoader.main();
284
+ const mac = await tempLoader.chip.readMac(tempLoader);
285
+ if (mac) {
286
+ deviceId = mac; // Swap 'unnamed_device_0' for the actual MAC
287
+ }
288
+ // Disconnect to release the port so connect() can start fresh
289
+ await tempTransport.disconnect();
290
+ }
291
+ catch (e) {
292
+ console.warn("Failed to pre-fetch MAC address. Falling back to default UUID.", e);
293
+ }
294
+ }
295
+ // 2. Register the connection using the real MAC address
242
296
  const return_uuid = await base.addConnection({
243
- uuid,
297
+ uuid: deviceId,
244
298
  propCreator: (id) => {
245
299
  const props = propCreator?.(id);
246
300
  return {
247
301
  send: (d) => defaultSend(id, d),
248
302
  onReceive: (d) => defaultOnReceive(id, d),
249
303
  port,
250
- // Initial connection state
251
304
  ...base.createInitialConnectionState(id),
252
- // From props
253
- ...props
305
+ deviceMac: deviceId, // Lock it in immediately
306
+ ...props,
254
307
  };
255
- }
308
+ },
256
309
  });
257
310
  const conn = base.getConnection(return_uuid);
258
311
  if (conn?.autoConnect)
@@ -264,11 +317,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
264
317
  await disconnect(uuid);
265
318
  }
266
319
  catch (e) { }
267
- ;
268
320
  base.removeConnection(uuid);
269
321
  };
270
322
  const reconnectAll = async (...connectionProps) => {
271
- const connectionIds = base.connections.map(c => c.uuid);
323
+ const connectionIds = base.connections.map((c) => c.uuid);
272
324
  await Promise.all(connectionIds.map(async (id) => {
273
325
  const c = base.getConnection(id);
274
326
  if (!c)
@@ -281,17 +333,28 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
281
333
  // This function now handles an entire device flashing session
282
334
  const handleFlashFirmware = async (uuid, assetsToFlash) => {
283
335
  const conn = base.getConnection(uuid);
284
- if (!conn || !conn.port || assetsToFlash.length === 0)
336
+ // 1. Bail if we don't have the required state
337
+ if (!conn ||
338
+ !conn.transport ||
339
+ !conn.port ||
340
+ !conn.esp ||
341
+ assetsToFlash.length === 0)
285
342
  return;
343
+ // Gracefully cancel reader and completely drop the transport natively
286
344
  await disconnect(uuid);
287
- base.updateConnection(uuid, (c) => ({ ...c, isFlashing: true, flashProgress: 0, flashError: undefined }));
345
+ base.updateConnection(uuid, (c) => ({
346
+ ...c,
347
+ isFlashing: true,
348
+ flashProgress: 0,
349
+ flashError: undefined,
350
+ }));
288
351
  try {
289
352
  // --- Connect ONCE ---
290
353
  const transport = new Transport(conn.port, true);
291
354
  const esploader = new ESPLoader({
292
355
  transport,
293
356
  baudrate: 921600,
294
- enableTracing: false
357
+ enableTracing: false,
295
358
  });
296
359
  try {
297
360
  await esploader.main();
@@ -300,7 +363,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
300
363
  console.log("failed to esploader.main()", e);
301
364
  return;
302
365
  }
303
- ;
304
366
  // --- Prepare an ARRAY of files for the library ---
305
367
  const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
306
368
  const arrayBuffer = await blob.arrayBuffer();
@@ -320,7 +382,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
320
382
  // You can enhance progress reporting to show which file is being flashed
321
383
  const progress = (written / total) * 100;
322
384
  console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
323
- base.updateConnection(uuid, (c) => ({ ...c, flashProgress: progress }));
385
+ base.updateConnection(uuid, (c) => ({
386
+ ...c,
387
+ flashProgress: progress,
388
+ }));
324
389
  },
325
390
  };
326
391
  // --- Call writeFlash ONCE with all files ---
@@ -331,7 +396,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
331
396
  catch (e) {
332
397
  console.log("failed to esploader.writeFlash", e);
333
398
  }
334
- ;
335
399
  // --- Disconnect ---
336
400
  await esploader.after();
337
401
  try {
@@ -343,7 +407,13 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
343
407
  await conn.port?.writable?.close();
344
408
  await conn.port?.close();
345
409
  }
346
- base.updateConnection(uuid, (c) => ({ ...c, isFlashing: false, flashProgress: 100 }));
410
+ base.updateConnection(uuid, (c) => ({
411
+ ...c,
412
+ isFlashing: false,
413
+ flashProgress: 100,
414
+ }));
415
+ // --- Reconnect the standard monitor ---
416
+ await connect(uuid);
347
417
  }
348
418
  catch (e) {
349
419
  console.error(`[${uuid}] Flashing failed:`, e);
@@ -352,6 +422,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
352
422
  isFlashing: false,
353
423
  flashError: e?.message ?? "Unknown error",
354
424
  }));
425
+ // Attempt recovery
426
+ await connect(uuid);
355
427
  }
356
428
  };
357
429
  useEffect(() => {
@@ -364,6 +436,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
364
436
  connect,
365
437
  disconnect,
366
438
  reconnectAll,
367
- handleFlashFirmware
439
+ handleFlashFirmware,
368
440
  };
369
441
  }
@@ -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 useMQTTMultiDeviceWhisperer<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[];