ota-hub-reactjs 0.0.19 → 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,8 +31,9 @@ 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[];
@@ -16,7 +16,7 @@ export function createDefaultInitialDeviceState(uuid, props) {
16
16
  /* One Device Whisperer is used for all like-devices, such as all Serial with Protobuf.
17
17
  Any e.g. Serial without Protobuf, or LoRaWAN etc. devices would be handled via a separate device whisperer
18
18
  */
19
- export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
19
+ export function useMultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, setFocussedConnectionOnAddConnection, } = {}) {
20
20
  const [connections, setConnections] = useState([]);
21
21
  const connectionsRef = useRef(new Map());
22
22
  const [isReady, setIsReady] = useState(false);
@@ -54,6 +54,9 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
54
54
  };
55
55
  connectionsRef.current.set(uuid, newConnection);
56
56
  setConnections(Array.from(connectionsRef.current.values()));
57
+ // Automatically set to the latest connection
58
+ if (!!setFocussedConnectionOnAddConnection)
59
+ setFocussedConnection(uuid);
57
60
  return uuid;
58
61
  };
59
62
  // ---------- REMOVE ----------
@@ -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,7 +15,7 @@ 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>): {
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
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;
@@ -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);
@@ -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,7 +16,7 @@ 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
22
  addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
@@ -1,8 +1,8 @@
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
7
  const text = typeof data === "string" ? data : new TextDecoder().decode(data);
8
8
  const trimmed = text.trim();
@@ -35,8 +35,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
35
35
  let slipBuffer = []; // accumulate SLIP frames
36
36
  let inSlipFrame = false; // are we inside a SLIP frame?
37
37
  let escapeNext = false;
38
+ const reader = transport.rawRead();
39
+ base.updateConnection(uuid, (c) => ({ ...c, reader }));
38
40
  try {
39
- const reader = transport.rawRead();
40
41
  while (true) {
41
42
  const conn = base.getConnection(uuid);
42
43
  if (!conn) {
@@ -121,6 +122,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
121
122
  });
122
123
  }
123
124
  finally {
125
+ // Clear reader state so it's fresh for next connect
126
+ base.updateConnection(uuid, (c) => ({ ...c, reader: undefined }));
124
127
  base.appendLog(uuid, {
125
128
  level: 0,
126
129
  message: "[!] Serial disconnected",
@@ -152,6 +155,21 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
152
155
  });
153
156
  base.updateConnection(uuid, (c) => ({ ...c, port }));
154
157
  }
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
+ }
155
173
  base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
156
174
  const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
157
175
  const transport = new Transport(port, false, false);
@@ -182,6 +200,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
182
200
  base.updateConnection(uuid, (c) => ({
183
201
  ...c,
184
202
  transport,
203
+ esp: esploader,
185
204
  baudrate: use_baudrate,
186
205
  isConnected: true,
187
206
  isConnecting: false,
@@ -191,7 +210,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
191
210
  message: "[✓] Serial connected",
192
211
  });
193
212
  await conn.onConnect?.();
194
- 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));
195
216
  }
196
217
  catch (err) {
197
218
  base.updateConnection(uuid, (c) => ({
@@ -208,6 +229,18 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
208
229
  };
209
230
  const disconnect = async (uuid, timeout = 2000) => {
210
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
+ }
211
244
  if (conn?.transport) {
212
245
  try {
213
246
  // Attempt disconnect, but don’t hang if the port is crashed
@@ -223,8 +256,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
223
256
  // Always clear the transport and reset connection state
224
257
  base.updateConnection(uuid, (c) => ({
225
258
  ...c,
226
- port: releasePortByDefault ? null : c.port,
227
- 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
228
262
  isConnected: false,
229
263
  isConnecting: false,
230
264
  autoConnect: false,
@@ -299,8 +333,14 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
299
333
  // This function now handles an entire device flashing session
300
334
  const handleFlashFirmware = async (uuid, assetsToFlash) => {
301
335
  const conn = base.getConnection(uuid);
302
- 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)
303
342
  return;
343
+ // Gracefully cancel reader and completely drop the transport natively
304
344
  await disconnect(uuid);
305
345
  base.updateConnection(uuid, (c) => ({
306
346
  ...c,
@@ -372,6 +412,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
372
412
  isFlashing: false,
373
413
  flashProgress: 100,
374
414
  }));
415
+ // --- Reconnect the standard monitor ---
416
+ await connect(uuid);
375
417
  }
376
418
  catch (e) {
377
419
  console.error(`[${uuid}] Flashing failed:`, e);
@@ -380,6 +422,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
380
422
  isFlashing: false,
381
423
  flashError: e?.message ?? "Unknown error",
382
424
  }));
425
+ // Attempt recovery
426
+ await connect(uuid);
383
427
  }
384
428
  };
385
429
  useEffect(() => {
@@ -13,7 +13,7 @@ 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, serverUrlFnc, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, enableWatchdog, ...props }: {
16
+ export declare function useMQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, enableWatchdog, ...props }: {
17
17
  serverUrl?: string;
18
18
  serverUrlFnc?: () => Promise<string> | string;
19
19
  uuidFromMessage: (topic: string, payload: Buffer<ArrayBufferLike>) => string;
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
2
+ import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
3
3
  import mqtt from "mqtt";
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
- const base = MultiDeviceWhisperer(props);
4
+ export function useMQTTMultiDeviceWhisperer({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid = undefined, pubTopicFromUuid = undefined, serverPort = 443, clientId = undefined, username = undefined, password = undefined, serverAutoConnect = true, serverConnectOn = false, enableWatchdog = true, ...props }) {
5
+ const base = useMultiDeviceWhisperer(props);
6
6
  const clientRef = useRef(null);
7
7
  const isUnmountedRef = useRef(false);
8
8
  // ✨ NEW: Manage our own reconnect timer
@@ -88,7 +88,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, serverUrlFnc, uuidFromMess
88
88
  };
89
89
  const connectToMQTTServer = async () => {
90
90
  if (!serverUrl && !serverUrlFnc) {
91
- console.error("MQTT MultiDeviceWhisperer requires either serverUrl or serverUrlFnc");
91
+ console.error("MQTT useMultiDeviceWhisperer requires either serverUrl or serverUrlFnc");
92
92
  return;
93
93
  }
94
94
  if (reconnectTimeoutRef.current)
@@ -38,7 +38,7 @@ export type SerialConnectionState = DeviceConnectionState & {
38
38
  baudrate?: number;
39
39
  slipReadWrite?: boolean;
40
40
  };
41
- export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: DeviceWhispererProps<AppOrMessageLayer>): {
41
+ export declare function useSerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: DeviceWhispererProps<AppOrMessageLayer>): {
42
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>;
@@ -1,4 +1,4 @@
1
- import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
1
+ import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  /*
4
4
  ┌────────────────────────────────┐
@@ -151,8 +151,8 @@ export class UsbTransport {
151
151
  // Standard CDC-ACM requests
152
152
  UsbTransport.SET_LINE_CODING = 0x20;
153
153
  UsbTransport.SET_CONTROL_LINE_STATE = 0x22;
154
- export function SerialMultiDeviceWhisperer({ ...props } = {}) {
155
- const base = MultiDeviceWhisperer(props);
154
+ export function useSerialMultiDeviceWhisperer({ ...props } = {}) {
155
+ const base = useMultiDeviceWhisperer(props);
156
156
  const releasePortByDefaultRef = useRef(false);
157
157
  const [releasePortByDefaultState, setReleasePortByDefaultState] = useState(false);
158
158
  useEffect(() => {
@@ -9,7 +9,7 @@ export type DeviceObjectResponse = {
9
9
  deviceConnectedTime: Date | null;
10
10
  deviceLastCommTime: Date | null;
11
11
  };
12
- export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends WebsocketConnectionState>({ server_url, server_port, ...props }: {
12
+ export declare function useWebsocketMultiDeviceWhisperer<AppOrMessageLayer extends WebsocketConnectionState>({ server_url, server_port, ...props }: {
13
13
  server_url: string;
14
14
  server_port: number;
15
15
  }): {
@@ -1,7 +1,7 @@
1
1
  import { useEffect } from "react";
2
- import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
3
- export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
4
- const base = MultiDeviceWhisperer(props);
2
+ import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
3
+ export function useWebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
4
+ const base = useMultiDeviceWhisperer(props);
5
5
  const defaultOnReceive = (uuid, data) => {
6
6
  const conn = base.getConnection(uuid);
7
7
  if (!conn)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ota-hub-reactjs",
3
- "version": "0.0.19",
3
+ "version": "0.1.1",
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",