ota-hub-reactjs 0.0.15 → 0.0.16

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.
@@ -2,7 +2,7 @@ export type LogMessage = string | number | boolean | Record<string, any> | any[]
2
2
  export type LogLine = {
3
3
  level: number;
4
4
  message: LogMessage;
5
- timestamp?: string | Date;
5
+ timestamp?: Date;
6
6
  };
7
7
  type PropCreatorProps<T> = (uuid: string) => Partial<T> | undefined;
8
8
  export interface AddConnectionProps<T> {
@@ -30,7 +30,6 @@ export type DeviceWhispererProps<T extends DeviceConnectionState> = {
30
30
  };
31
31
  export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
32
32
  connections: T[];
33
- connectionsRef: import("react").RefObject<T[]>;
34
33
  addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
35
34
  removeConnection: (uuid: string) => void;
36
35
  connect: (_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
- import { uniqueNamesGenerator, animals } from "unique-names-generator";
4
3
  ;
5
4
  // Initial, generic state for the generic device
6
5
  export function createDefaultInitialDeviceState(uuid, props) {
@@ -20,53 +19,55 @@ export function createDefaultInitialDeviceState(uuid, props) {
20
19
  */
21
20
  export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
22
21
  const [connections, setConnections] = useState([]);
23
- const connectionsRef = useRef(connections);
22
+ const connectionsRef = useRef(new Map());
24
23
  const [isReady, setIsReady] = useState(false);
25
- const getConnection = (uuid) => connectionsRef.current.find(c => c.uuid === uuid);
24
+ // ---------- GET ----------
25
+ const getConnection = (uuid) => connectionsRef.current.get(uuid);
26
+ // ---------- UPDATE ----------
26
27
  const updateConnection = (uuid, updater) => {
27
- setConnections(prev => {
28
- const updated = prev.map((c) => c.uuid === uuid ? updater(c) : c);
29
- connectionsRef.current = updated;
30
- return updated;
31
- });
28
+ const current = connectionsRef.current.get(uuid);
29
+ if (!current)
30
+ return;
31
+ const next = updater(current);
32
+ connectionsRef.current.set(uuid, next);
33
+ setConnections(Array.from(connectionsRef.current.values()));
32
34
  };
33
35
  const updateConnectionName = (uuid, name) => {
34
- setConnections((prev) => prev.map((c) => (c.uuid === uuid ? { ...c, name } : c)));
36
+ updateConnection(uuid, (c) => ({ ...c, name }));
35
37
  };
36
38
  const appendLog = (uuid, log) => {
37
- if (!log.timestamp) {
39
+ if (!log.timestamp)
38
40
  log.timestamp = new Date();
39
- }
40
41
  updateConnection(uuid, (c) => ({
41
42
  ...c,
42
43
  logs: [...c.logs.slice(-199), log],
43
44
  }));
44
45
  };
46
+ // ---------- ADD ----------
45
47
  const addConnection = async ({ uuid, propCreator }) => {
46
- uuid = uuid ?? uniqueNamesGenerator({ dictionaries: [animals] });
48
+ uuid = uuid ?? `unnamed_device_${connectionsRef.current.size}`;
47
49
  const props = propCreator?.(uuid);
48
50
  const newConnection = {
49
51
  ...createDefaultInitialDeviceState(uuid),
50
52
  ...createInitialConnectionState(uuid),
51
53
  ...props
52
54
  };
53
- connectionsRef.current = [...connectionsRef.current, newConnection];
54
- setConnections(prev => [...prev, newConnection]);
55
- const anyUpdatedConnection = getConnection(uuid);
56
- if (!anyUpdatedConnection) {
57
- return "";
58
- }
55
+ connectionsRef.current.set(uuid, newConnection);
56
+ setConnections(Array.from(connectionsRef.current.values()));
59
57
  return uuid;
60
58
  };
59
+ // ---------- REMOVE ----------
61
60
  const removeConnection = (uuid) => {
62
- setConnections((prev) => prev.filter((c) => c.uuid !== uuid));
61
+ connectionsRef.current.delete(uuid);
62
+ setConnections(Array.from(connectionsRef.current.values()));
63
63
  };
64
+ // ---------- EFFECT ----------
64
65
  useEffect(() => {
65
- connectionsRef.current = connections;
66
- }, [connections]);
66
+ // initial snapshot
67
+ setConnections(Array.from(connectionsRef.current.values()));
68
+ }, []);
67
69
  return {
68
70
  connections,
69
- connectionsRef,
70
71
  addConnection,
71
72
  removeConnection,
72
73
  connect: (_uuid) => { },
@@ -20,7 +20,6 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
20
20
  sendProtobuf: (uuid: string, message: MessageTX) => void;
21
21
  protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
22
22
  connections: AppLayer[];
23
- connectionsRef: import("react").RefObject<AppLayer[]>;
24
23
  removeConnection: (uuid: string) => void;
25
24
  connect: (_uuid: string) => void;
26
25
  disconnect: (_uuid: string) => void;
@@ -1,5 +1,5 @@
1
1
  import { ESPLoader, FlashOptions, Transport } from "esptool-js";
2
- import { AddConnectionProps, DeviceConnectionState } from "../base/device-whisperer.js";
2
+ import { AddConnectionProps, DeviceConnectionState, DeviceWhispererProps } from "../base/device-whisperer.js";
3
3
  export type ESP32ConnectionState = DeviceConnectionState & {
4
4
  port?: SerialPort;
5
5
  baudrate?: number;
@@ -15,7 +15,9 @@ export type FlashFirmwareProps = {
15
15
  firmwareBlob?: Blob;
16
16
  fileArray?: FlashOptions["fileArray"];
17
17
  };
18
- export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ ...props }?: {}): {
18
+ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
19
+ releasePortByDefault: boolean;
20
+ } & DeviceWhispererProps<AppOrMessageLayer>): {
19
21
  addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
20
22
  removeConnection: (uuid: string) => Promise<void>;
21
23
  connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
@@ -26,7 +28,6 @@ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP3
26
28
  address: number;
27
29
  }[]) => Promise<void>;
28
30
  connections: AppOrMessageLayer[];
29
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
30
31
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
31
32
  updateConnectionName: (uuid: string, name: string) => void;
32
33
  getConnection: (uuid: string) => AppOrMessageLayer | undefined;
@@ -1,39 +1,19 @@
1
1
  import { useEffect } from "react";
2
2
  import { ESPLoader, Transport } from "esptool-js";
3
3
  import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
4
- export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
4
+ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
5
5
  const base = MultiDeviceWhisperer(props);
6
6
  const defaultOnReceive = (uuid, data) => {
7
- const conn = base.getConnection(uuid);
8
- if (!conn)
7
+ const text = typeof data === "string"
8
+ ? data
9
+ : new TextDecoder().decode(data);
10
+ const trimmed = text.trim();
11
+ if (!trimmed)
9
12
  return;
10
- const decoder = new TextDecoder();
11
- let bytes;
12
- if (typeof data === "string") {
13
- bytes = new TextEncoder().encode(data);
14
- }
15
- else if (data instanceof ArrayBuffer) {
16
- bytes = new Uint8Array(data);
17
- }
18
- else {
19
- bytes = data;
20
- }
21
- const asText = decoder.decode(bytes);
22
- const combined = conn.readBufferLeftover + asText;
23
- const lines = combined.split("\r\n");
24
- base.updateConnection(uuid, (c) => ({
25
- ...c,
26
- readBufferLeftover: lines.pop() || ""
27
- }));
28
- for (const line of lines) {
29
- const trimmed = line.trim();
30
- if (trimmed) {
31
- base.appendLog(uuid, {
32
- level: 2,
33
- message: trimmed,
34
- });
35
- }
36
- }
13
+ base.appendLog(uuid, {
14
+ level: 2,
15
+ message: trimmed,
16
+ });
37
17
  };
38
18
  const defaultSend = async (uuid, data) => {
39
19
  const conn = base.getConnection(uuid);
@@ -52,9 +32,7 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
52
32
  return;
53
33
  };
54
34
  const readLoop = async (uuid, transport) => {
55
- const conn = base.getConnection(uuid);
56
- if (!conn)
57
- return;
35
+ const textDecoder = new TextDecoder();
58
36
  let readBuffer = ""; // accumulate ASCII/lines
59
37
  let slipBuffer = []; // accumulate SLIP frames
60
38
  let inSlipFrame = false; // are we inside a SLIP frame?
@@ -62,6 +40,12 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
62
40
  try {
63
41
  const reader = transport.rawRead();
64
42
  while (true) {
43
+ const conn = base.getConnection(uuid);
44
+ if (!conn) {
45
+ console.log("Kack!");
46
+ return;
47
+ }
48
+ ;
65
49
  const { value, done } = await reader.next();
66
50
  if (done || !value)
67
51
  break;
@@ -117,9 +101,8 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
117
101
  escapeNext = false;
118
102
  continue;
119
103
  }
120
- // treat as normal ASCII text
121
- const char = String.fromCharCode(b);
122
- readBuffer += char;
104
+ // treat as normal text (correctly)
105
+ readBuffer += textDecoder.decode(new Uint8Array([b]), { stream: true });
123
106
  // check for newline
124
107
  let newlineIndex;
125
108
  while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
@@ -244,8 +227,8 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
244
227
  // Always clear the transport and reset connection state
245
228
  base.updateConnection(uuid, (c) => ({
246
229
  ...c,
247
- port: null,
248
- transport: null,
230
+ port: releasePortByDefault ? null : c.port,
231
+ transport: releasePortByDefault ? null : c.transport,
249
232
  isConnected: false,
250
233
  isConnecting: false,
251
234
  autoConnect: false,
@@ -285,8 +268,11 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
285
268
  base.removeConnection(uuid);
286
269
  };
287
270
  const reconnectAll = async (...connectionProps) => {
288
- const connections = [...base.connectionsRef.current]; // snapshot first
289
- await Promise.all(connections.map(async (c) => {
271
+ const connectionIds = base.connections.map(c => c.uuid);
272
+ await Promise.all(connectionIds.map(async (id) => {
273
+ const c = base.getConnection(id);
274
+ if (!c)
275
+ return;
290
276
  await disconnect(c.uuid);
291
277
  await new Promise((res) => setTimeout(res, 250));
292
278
  return connect(c.uuid, ...connectionProps);
@@ -22,7 +22,6 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
22
22
  reconnectAll: () => Promise<void>;
23
23
  connectToMQTTServer: () => (() => void) | undefined;
24
24
  connections: AppOrMessageLayer[];
25
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
26
25
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
27
26
  updateConnectionName: (uuid: string, name: string) => void;
28
27
  getConnection: (uuid: string) => AppOrMessageLayer | undefined;
@@ -189,7 +189,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
189
189
  Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
190
190
  return;
191
191
  }
192
- if (base.connectionsRef.current.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
192
+ if (base.connections.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
193
193
  return;
194
194
  }
195
195
  await base.addConnection({
@@ -221,13 +221,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
221
221
  base.removeConnection(uuid);
222
222
  };
223
223
  const reconnectAll = async () => {
224
- for (const c of base.connectionsRef.current) {
224
+ const connectionIds = base.connections.map(c => c.uuid);
225
+ await Promise.all(connectionIds.map(async (id) => {
226
+ const c = base.getConnection(id);
227
+ if (!c)
228
+ return;
225
229
  await disconnect(c.uuid);
226
230
  await new Promise((res) => setTimeout(res, 250));
227
- }
228
- for (const c of base.connectionsRef.current) {
229
- await connect(c.uuid);
230
- }
231
+ return connect(c.uuid);
232
+ }));
231
233
  };
232
234
  useEffect(() => {
233
235
  if (!(serverAutoConnect || serverConnectOn))
@@ -1,4 +1,4 @@
1
- import { DeviceConnectionState, AddConnectionProps } from "../base/device-whisperer.js";
1
+ import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
2
2
  export declare class UsbTransport {
3
3
  device: USBDevice;
4
4
  controlInterface: number;
@@ -38,14 +38,15 @@ export type SerialConnectionState = DeviceConnectionState & {
38
38
  baudrate?: number;
39
39
  slipReadWrite?: boolean;
40
40
  };
41
- export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: {}): {
41
+ export declare function SerialMultiDeviceWhisperer<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>;
45
45
  disconnect: (uuid: string) => Promise<void>;
46
- reconnectAll: () => Promise<void>;
46
+ reconnectAll: (...connectionProps: any) => Promise<void>;
47
+ releasePortByDefault: boolean;
48
+ setReleasePortByDefault: import("react").Dispatch<import("react").SetStateAction<boolean>>;
47
49
  connections: AppOrMessageLayer[];
48
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
49
50
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
50
51
  updateConnectionName: (uuid: string, name: string) => void;
51
52
  getConnection: (uuid: string) => AppOrMessageLayer | undefined;
@@ -1,5 +1,5 @@
1
1
  import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
2
- import { useEffect } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  /*
4
4
  ┌────────────────────────────────┐
5
5
  │ Device Whisperer (Generic) │ (connect/disconnect, state mgmt) - "Abstraction Layer"
@@ -150,6 +150,11 @@ UsbTransport.SET_LINE_CODING = 0x20;
150
150
  UsbTransport.SET_CONTROL_LINE_STATE = 0x22;
151
151
  export function SerialMultiDeviceWhisperer({ ...props } = {}) {
152
152
  const base = MultiDeviceWhisperer(props);
153
+ const releasePortByDefaultRef = useRef(false);
154
+ const [releasePortByDefaultState, setReleasePortByDefaultState] = useState(false);
155
+ useEffect(() => {
156
+ releasePortByDefaultRef.current = releasePortByDefaultState;
157
+ }, [releasePortByDefaultState]);
153
158
  // --- Message Processing (Identical logic, just copied over) ---
154
159
  const defaultOnReceive = (uuid, data) => {
155
160
  const conn = base.getConnection(uuid);
@@ -165,7 +170,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
165
170
  bytes = data;
166
171
  const asText = decoder.decode(bytes);
167
172
  const combined = conn.readBufferLeftover + asText;
168
- const lines = combined.split("\r\n");
173
+ const lines = combined.split("\n");
169
174
  base.updateConnection(uuid, (c) => ({ ...c, readBufferLeftover: lines.pop() || "" }));
170
175
  for (const line of lines) {
171
176
  if (line.trim()) {
@@ -306,7 +311,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
306
311
  }
307
312
  base.updateConnection(uuid, c => ({
308
313
  ...c,
309
- transport: null,
314
+ transport: releasePortByDefaultRef.current ? null : c.transport,
310
315
  isConnected: false,
311
316
  isConnecting: false
312
317
  }));
@@ -346,7 +351,17 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
346
351
  await disconnect(uuid);
347
352
  base.removeConnection(uuid);
348
353
  };
349
- const reconnectAll = async () => { };
354
+ const reconnectAll = async (...connectionProps) => {
355
+ const connectionIds = base.connections.map(c => c.uuid);
356
+ await Promise.all(connectionIds.map(async (id) => {
357
+ const c = base.getConnection(id);
358
+ if (!c)
359
+ return;
360
+ await disconnect(c.uuid);
361
+ await new Promise((res) => setTimeout(res, 250));
362
+ return connect(c.uuid, ...connectionProps);
363
+ }));
364
+ };
350
365
  useEffect(() => { base.setIsReady(true); }, []);
351
366
  return {
352
367
  ...base,
@@ -354,6 +369,8 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
354
369
  removeConnection,
355
370
  connect,
356
371
  disconnect,
357
- reconnectAll
372
+ reconnectAll,
373
+ releasePortByDefault: releasePortByDefaultState,
374
+ setReleasePortByDefault: setReleasePortByDefaultState
358
375
  };
359
376
  }
@@ -18,9 +18,8 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
18
18
  connect: (uuid: string, attempt?: number) => Promise<void>;
19
19
  disconnect: (uuid: string) => Promise<void>;
20
20
  checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
21
- reconnectAll: () => Promise<void>;
21
+ reconnectAll: (...connectionProps: any) => Promise<void>;
22
22
  connections: AppOrMessageLayer[];
23
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
24
23
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
25
24
  updateConnectionName: (uuid: string, name: string) => void;
26
25
  getConnection: (uuid: string) => AppOrMessageLayer | undefined;
@@ -19,7 +19,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
19
19
  }
20
20
  const asText = decoder.decode(bytes);
21
21
  const combined = conn.readBufferLeftover + asText;
22
- const lines = combined.split("\n");
22
+ const lines = combined.split(/\r?\n/);
23
23
  base.updateConnection(uuid, (c) => ({
24
24
  ...c,
25
25
  readBufferLeftover: lines.pop() || ""
@@ -173,14 +173,16 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
173
173
  return [];
174
174
  }
175
175
  };
176
- const reconnectAll = async () => {
177
- for (const c of base.connectionsRef.current) {
176
+ const reconnectAll = async (...connectionProps) => {
177
+ const connectionIds = base.connections.map(c => c.uuid);
178
+ await Promise.all(connectionIds.map(async (id) => {
179
+ const c = base.getConnection(id);
180
+ if (!c)
181
+ return;
178
182
  await disconnect(c.uuid);
179
183
  await new Promise((res) => setTimeout(res, 250));
180
- }
181
- for (const c of base.connectionsRef.current) {
182
- await connect(c.uuid);
183
- }
184
+ return connect(c.uuid, ...connectionProps);
185
+ }));
184
186
  };
185
187
  useEffect(() => {
186
188
  base.setIsReady(true); // Ready on page load by default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ota-hub-reactjs",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
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",
@@ -33,7 +33,6 @@
33
33
  "dependencies": {
34
34
  "esptool-js": "^0.5.5",
35
35
  "mqtt": "^5.14.1",
36
- "unique-names-generator": "^4.7.1",
37
36
  "uuid": "^11.1.0"
38
37
  },
39
38
  "peerDependencies": {