ota-hub-reactjs 0.1.1 → 0.1.2

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.
@@ -5,6 +5,7 @@ export type ESP32ConnectionState = DeviceConnectionState & {
5
5
  baudrate?: number;
6
6
  transport?: Transport;
7
7
  esp?: ESPLoader;
8
+ debugLogging?: false;
8
9
  reader?: AsyncGenerator<Uint8Array>;
9
10
  slipReadWrite?: boolean;
10
11
  isFlashing: boolean;
@@ -178,6 +178,7 @@ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props }
178
178
  transport,
179
179
  baudrate: use_baudrate,
180
180
  enableTracing: false,
181
+ debugLogging: !!conn.debugLogging,
181
182
  });
182
183
  try {
183
184
  await esploader.main();
@@ -229,12 +230,10 @@ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props }
229
230
  };
230
231
  const disconnect = async (uuid, timeout = 2000) => {
231
232
  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
233
+ // 1. Politely kill our high-level generator
235
234
  if (conn?.reader && typeof conn.reader.return === "function") {
236
235
  try {
237
- // @ts-ignore - The underlying AsyncGenerator handles return gracefully
236
+ // @ts-ignore
238
237
  await conn.reader.return();
239
238
  }
240
239
  catch (e) {
@@ -243,7 +242,17 @@ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props }
243
242
  }
244
243
  if (conn?.transport) {
245
244
  try {
246
- // Attempt disconnect, but don’t hang if the port is crashed
245
+ // 2. AGGRESSIVE LOCK REMOVAL (The Fix)
246
+ // Dig into esptool-js's internal state and force the native reader to release.
247
+ // If we don't do this, transport.disconnect() will hang and fail to close the port.
248
+ const internalReader = conn.transport.reader;
249
+ if (internalReader) {
250
+ await internalReader.cancel().catch(() => { });
251
+ if (typeof internalReader.releaseLock === "function") {
252
+ internalReader.releaseLock();
253
+ }
254
+ }
255
+ // 3. Now that locks are cleared, attempt standard disconnect
247
256
  await Promise.race([
248
257
  conn.transport.disconnect(),
249
258
  new Promise((resolve) => setTimeout(resolve, timeout)),
@@ -253,7 +262,23 @@ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props }
253
262
  console.warn(`[${uuid}] Serial Disconnect error:`, e);
254
263
  }
255
264
  }
256
- // Always clear the transport and reset connection state
265
+ // 4. NATIVE SAFETY NET
266
+ // Guarantee the port is closed natively just in case the transport failed
267
+ if (conn?.port) {
268
+ try {
269
+ if (conn.port.readable?.locked) {
270
+ await conn.port.readable.cancel().catch(() => { });
271
+ }
272
+ if (conn.port.writable?.locked) {
273
+ await conn.port.writable.abort().catch(() => { });
274
+ }
275
+ await conn.port.close().catch(() => { });
276
+ }
277
+ catch (e) {
278
+ // It will throw if already closed, which is perfectly fine.
279
+ }
280
+ }
281
+ // 5. Always clear the transport and reset connection state
257
282
  base.updateConnection(uuid, (c) => ({
258
283
  ...c,
259
284
  port: releasePortByDefault ? undefined : c.port,
@@ -332,37 +357,35 @@ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props }
332
357
  };
333
358
  // This function now handles an entire device flashing session
334
359
  const handleFlashFirmware = async (uuid, assetsToFlash) => {
360
+ // Capture the original connection and port reference before disconnect
335
361
  const conn = base.getConnection(uuid);
336
362
  // 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)
363
+ if (!conn || !conn.port || assetsToFlash.length === 0)
342
364
  return;
343
365
  // Gracefully cancel reader and completely drop the transport natively
344
366
  await disconnect(uuid);
367
+ await new Promise((resolve) => setTimeout(resolve, 400));
345
368
  base.updateConnection(uuid, (c) => ({
346
369
  ...c,
370
+ port: conn.port, // Restore the port into state in case disconnect wiped it
347
371
  isFlashing: true,
348
372
  flashProgress: 0,
349
373
  flashError: undefined,
350
374
  }));
375
+ // Declare outside the try block (Fixes variable shadowing)
376
+ let transport;
377
+ let esploader;
351
378
  try {
352
379
  // --- Connect ONCE ---
353
- const transport = new Transport(conn.port, true);
354
- const esploader = new ESPLoader({
380
+ transport = new Transport(conn.port, true);
381
+ esploader = new ESPLoader({
355
382
  transport,
356
383
  baudrate: 921600,
357
384
  enableTracing: false,
385
+ // debugLogging: !!conn.debugLogging,
358
386
  });
359
- try {
360
- await esploader.main();
361
- }
362
- catch (e) {
363
- console.log("failed to esploader.main()", e);
364
- return;
365
- }
387
+ // This may fail, that'll bubble the failure up to the upper try/catch
388
+ await esploader.main();
366
389
  // --- Prepare an ARRAY of files for the library ---
367
390
  const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
368
391
  const arrayBuffer = await blob.arrayBuffer();
@@ -371,32 +394,34 @@ export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props }
371
394
  .join("");
372
395
  return { data: binaryString, address };
373
396
  }));
397
+ let lastRenderTime = 0;
374
398
  const flashOptions = {
375
- fileArray, // Pass the whole array here
399
+ fileArray,
376
400
  flashSize: "keep",
377
401
  flashMode: "qio",
378
402
  flashFreq: "80m",
379
403
  eraseAll: fileArray.length > 1, // Writing more than 1 thing, so likely writing partitions.
380
404
  compress: true,
381
405
  reportProgress: (fileIndex, written, total) => {
382
- // You can enhance progress reporting to show which file is being flashed
383
406
  const progress = (written / total) * 100;
384
- console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
385
- base.updateConnection(uuid, (c) => ({
386
- ...c,
387
- flashProgress: progress,
388
- }));
407
+ const now = performance.now();
408
+ // Throttle UI updates to at most once every 100ms (~10 FPS),
409
+ // but ALWAYS ensure the final 100% tick renders.
410
+ if (now - lastRenderTime > 100 || written === total) {
411
+ lastRenderTime = now;
412
+ // Optional: Only log every 100ms as well to prevent console spam
413
+ console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
414
+ base.updateConnection(uuid, (c) => ({
415
+ ...c,
416
+ flashProgress: progress,
417
+ }));
418
+ }
389
419
  },
390
420
  };
391
421
  // --- Call writeFlash ONCE with all files ---
392
- try {
393
- base.updateConnection(uuid, (c) => ({ ...c, flashProgress: -1 }));
394
- await esploader.writeFlash(flashOptions);
395
- }
396
- catch (e) {
397
- console.log("failed to esploader.writeFlash", e);
398
- }
399
- // --- Disconnect ---
422
+ base.updateConnection(uuid, (c) => ({ ...c, flashProgress: -1 }));
423
+ await esploader.writeFlash(flashOptions);
424
+ // --- Disconnect cleanly ---
400
425
  await esploader.after();
401
426
  try {
402
427
  await transport.disconnect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ota-hub-reactjs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",