ota-hub-reactjs 0.0.15 → 0.0.18

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> {
@@ -13,6 +13,10 @@ export type DeviceConnectionState = {
13
13
  uuid: string;
14
14
  deviceMac?: string;
15
15
  name: string;
16
+ /**
17
+ * Default device send helper.
18
+ * This is a utility function which may better be replaced with transport specific send functions (e.g. MQTT has publish, Serial has write etc.)
19
+ */
16
20
  send: (data: string | Uint8Array) => void | Promise<void>;
17
21
  onReceive?: (data: string | Uint8Array) => void;
18
22
  onConnect?: () => void | Promise<void>;
@@ -29,8 +33,9 @@ export type DeviceWhispererProps<T extends DeviceConnectionState> = {
29
33
  createInitialConnectionState?: (uuid: string) => Partial<T>;
30
34
  };
31
35
  export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
36
+ focussedConnection: string | undefined;
37
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
32
38
  connections: T[];
33
- connectionsRef: import("react").RefObject<T[]>;
34
39
  addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
35
40
  removeConnection: (uuid: string) => void;
36
41
  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,58 @@ 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
+ const [focussedConnection, setFocussedConnection] = useState();
25
+ // ---------- GET ----------
26
+ const getConnection = (uuid) => connectionsRef.current.get(uuid);
27
+ // ---------- UPDATE ----------
26
28
  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
- });
29
+ const current = connectionsRef.current.get(uuid);
30
+ if (!current)
31
+ return;
32
+ const next = updater(current);
33
+ connectionsRef.current.set(uuid, next);
34
+ setConnections(Array.from(connectionsRef.current.values()));
32
35
  };
33
36
  const updateConnectionName = (uuid, name) => {
34
- setConnections((prev) => prev.map((c) => (c.uuid === uuid ? { ...c, name } : c)));
37
+ updateConnection(uuid, (c) => ({ ...c, name }));
35
38
  };
36
39
  const appendLog = (uuid, log) => {
37
- if (!log.timestamp) {
40
+ if (!log.timestamp)
38
41
  log.timestamp = new Date();
39
- }
40
42
  updateConnection(uuid, (c) => ({
41
43
  ...c,
42
44
  logs: [...c.logs.slice(-199), log],
43
45
  }));
44
46
  };
47
+ // ---------- ADD ----------
45
48
  const addConnection = async ({ uuid, propCreator }) => {
46
- uuid = uuid ?? uniqueNamesGenerator({ dictionaries: [animals] });
49
+ uuid = uuid ?? `unnamed_device_${connectionsRef.current.size}`;
47
50
  const props = propCreator?.(uuid);
48
51
  const newConnection = {
49
52
  ...createDefaultInitialDeviceState(uuid),
50
53
  ...createInitialConnectionState(uuid),
51
54
  ...props
52
55
  };
53
- connectionsRef.current = [...connectionsRef.current, newConnection];
54
- setConnections(prev => [...prev, newConnection]);
55
- const anyUpdatedConnection = getConnection(uuid);
56
- if (!anyUpdatedConnection) {
57
- return "";
58
- }
56
+ connectionsRef.current.set(uuid, newConnection);
57
+ setConnections(Array.from(connectionsRef.current.values()));
59
58
  return uuid;
60
59
  };
60
+ // ---------- REMOVE ----------
61
61
  const removeConnection = (uuid) => {
62
- setConnections((prev) => prev.filter((c) => c.uuid !== uuid));
62
+ connectionsRef.current.delete(uuid);
63
+ setConnections(Array.from(connectionsRef.current.values()));
63
64
  };
65
+ // ---------- EFFECT ----------
64
66
  useEffect(() => {
65
- connectionsRef.current = connections;
66
- }, [connections]);
67
+ // initial snapshot
68
+ setConnections(Array.from(connectionsRef.current.values()));
69
+ }, []);
67
70
  return {
71
+ focussedConnection,
72
+ setFocussedConnection,
68
73
  connections,
69
- connectionsRef,
70
74
  addConnection,
71
75
  removeConnection,
72
76
  connect: (_uuid) => { },
@@ -19,8 +19,9 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
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;
22
+ focussedConnection: string | undefined;
23
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
22
24
  connections: AppLayer[];
23
- connectionsRef: import("react").RefObject<AppLayer[]>;
24
25
  removeConnection: (uuid: string) => void;
25
26
  connect: (_uuid: string) => void;
26
27
  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>;
@@ -25,8 +27,9 @@ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP3
25
27
  blob: Blob;
26
28
  address: number;
27
29
  }[]) => Promise<void>;
30
+ focussedConnection: string | undefined;
31
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
28
32
  connections: AppOrMessageLayer[];
29
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
30
33
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
31
34
  updateConnectionName: (uuid: string, name: string) => void;
32
35
  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);
@@ -1,5 +1,15 @@
1
1
  import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
2
+ type MQTTPayload = string | Uint8Array | ArrayBuffer;
3
+ type MQTTTopicCallback = (payload: MQTTPayload, topic: string) => void;
2
4
  export type MQTTConnectionState = DeviceConnectionState & {
5
+ /**
6
+ * Publishes a payload to any explicit MQTT topic.
7
+ */
8
+ publish?: (topic: string, payload: MQTTPayload) => void | Promise<void>;
9
+ /**
10
+ * Subscribes a callback for a specific topic. Returns an unsubscribe function.
11
+ */
12
+ subscribeCallbackToTopic?: (topic: string, callback: MQTTTopicCallback) => () => void;
3
13
  pingFunction?: (props?: any) => void;
4
14
  touchHeartbeat?: () => void;
5
15
  };
@@ -21,8 +31,9 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
21
31
  disconnect: (uuid: string) => Promise<void>;
22
32
  reconnectAll: () => Promise<void>;
23
33
  connectToMQTTServer: () => (() => void) | undefined;
34
+ focussedConnection: string | undefined;
35
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
24
36
  connections: AppOrMessageLayer[];
25
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
26
37
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
27
38
  updateConnectionName: (uuid: string, name: string) => void;
28
39
  getConnection: (uuid: string) => AppOrMessageLayer | undefined;
@@ -31,3 +42,4 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
31
42
  setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
32
43
  createInitialConnectionState: (uuid: string) => Partial<AppOrMessageLayer>;
33
44
  };
45
+ export {};
@@ -7,6 +7,83 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
7
7
  const isUnmountedRef = useRef(false);
8
8
  const watchdogTimers = useRef({});
9
9
  const addingConnections = useRef(new Set());
10
+ const topicCallbacksRef = useRef(new Map());
11
+ const hasCallbackForTopic = (topic) => {
12
+ const callbacks = topicCallbacksRef.current.get(topic);
13
+ return !!callbacks && callbacks.size > 0;
14
+ };
15
+ const hasOtherConnectionUsingTopic = (topic, excludingUuid) => {
16
+ return base.connections.some((connection) => {
17
+ if (excludingUuid && connection.uuid === excludingUuid)
18
+ return false;
19
+ const connectionTopic = subTopicFromUuid?.(connection.uuid) ?? connection.uuid;
20
+ return connectionTopic === topic;
21
+ });
22
+ };
23
+ const normalizePayload = (payload) => {
24
+ if (typeof payload === "string") {
25
+ return new TextEncoder().encode(payload);
26
+ }
27
+ if (payload instanceof ArrayBuffer) {
28
+ return new Uint8Array(payload);
29
+ }
30
+ return payload;
31
+ };
32
+ const publish = async (topic, payload, uuidForLogs) => {
33
+ const client = clientRef.current;
34
+ if (!client || isUnmountedRef.current)
35
+ return;
36
+ const bytes = normalizePayload(payload);
37
+ if (uuidForLogs) {
38
+ base.appendLog(uuidForLogs, {
39
+ level: 5,
40
+ message: bytes,
41
+ });
42
+ }
43
+ await new Promise((resolve, reject) => {
44
+ client.publish(topic, bytes, { qos: 1 }, (err) => {
45
+ if (err) {
46
+ reject(err);
47
+ return;
48
+ }
49
+ resolve();
50
+ });
51
+ });
52
+ };
53
+ const subscribeCallbackToTopic = (topic, callback) => {
54
+ if (!topic)
55
+ return () => { };
56
+ const topicCallbacks = topicCallbacksRef.current.get(topic) ?? new Set();
57
+ const topicAlreadyRegistered = topicCallbacksRef.current.has(topic);
58
+ topicCallbacks.add(callback);
59
+ topicCallbacksRef.current.set(topic, topicCallbacks);
60
+ const client = clientRef.current;
61
+ if (client?.connected && !client.disconnecting && !topicAlreadyRegistered) {
62
+ client.subscribe(topic, { qos: 1 }, (err) => {
63
+ if (err)
64
+ console.error("Topic callback subscribe failed:", err);
65
+ });
66
+ }
67
+ return () => {
68
+ const currentCallbacks = topicCallbacksRef.current.get(topic);
69
+ if (!currentCallbacks)
70
+ return;
71
+ currentCallbacks.delete(callback);
72
+ if (currentCallbacks.size > 0)
73
+ return;
74
+ topicCallbacksRef.current.delete(topic);
75
+ // Keep topic subscribed when at least one managed connection still uses it.
76
+ if (hasOtherConnectionUsingTopic(topic))
77
+ return;
78
+ const latestClient = clientRef.current;
79
+ if (latestClient?.connected && !latestClient.disconnecting) {
80
+ latestClient.unsubscribe(topic, (err) => {
81
+ if (err)
82
+ console.error("Topic callback unsubscribe failed:", err);
83
+ });
84
+ }
85
+ };
86
+ };
10
87
  const connectToMQTTServer = () => {
11
88
  isUnmountedRef.current = false;
12
89
  if (clientRef.current) {
@@ -29,6 +106,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
29
106
  new_client.on("connect", () => {
30
107
  console.log("MQTT Whisperer Connected");
31
108
  base.setIsReady(true);
109
+ // Re-subscribe custom topic callbacks after reconnect.
110
+ topicCallbacksRef.current.forEach((_callbacks, topic) => {
111
+ new_client.subscribe(topic, { qos: 1 }, (err) => {
112
+ if (err)
113
+ console.error("Topic callback subscribe failed:", err);
114
+ });
115
+ });
32
116
  });
33
117
  new_client.on("reconnect", () => {
34
118
  console.log("MQTT Whisperer Reconnecting...");
@@ -47,6 +131,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
47
131
  return;
48
132
  const uuid = uuidFromMessage(topic, payload);
49
133
  const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
134
+ const topicCallbacks = topicCallbacksRef.current.get(topic);
135
+ topicCallbacks?.forEach((cb) => {
136
+ try {
137
+ cb(bytes, topic);
138
+ }
139
+ catch (err) {
140
+ console.error("Topic callback failed:", err);
141
+ }
142
+ });
50
143
  if (!uuid)
51
144
  return;
52
145
  const conn = base.getConnection(uuid);
@@ -75,6 +168,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
75
168
  }
76
169
  });
77
170
  watchdogTimers.current = {};
171
+ topicCallbacksRef.current.clear();
78
172
  if (clientRef.current) {
79
173
  clientRef.current.removeAllListeners();
80
174
  clientRef.current.end(true);
@@ -105,6 +199,10 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
105
199
  if (!clientRef.current || isUnmountedRef.current)
106
200
  return;
107
201
  const topic = subTopicFromUuid?.(uuid) ?? uuid;
202
+ // Keep topic subscribed if callbacks or other device-connections still depend on it.
203
+ if (hasCallbackForTopic(topic) || hasOtherConnectionUsingTopic(topic, uuid)) {
204
+ return;
205
+ }
108
206
  if (clientRef.current.connected && !clientRef.current.disconnecting) {
109
207
  clientRef.current.unsubscribe(topic, async (err) => {
110
208
  if (err)
@@ -173,14 +271,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
173
271
  const conn = base.getConnection(uuid);
174
272
  if (!conn)
175
273
  return;
176
- const payload = typeof data === 'string'
177
- ? new TextEncoder().encode(data)
178
- : data;
179
- base.appendLog(uuid, {
180
- level: 5,
181
- message: payload,
182
- });
183
- clientRef.current?.publish(pubTopicFromUuid?.(uuid) ?? uuid, payload); // TS is wrong here!
274
+ const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
275
+ await publish(topic, data, uuid);
184
276
  };
185
277
  const addConnection = async ({ uuid, propCreator }) => {
186
278
  if (!clientRef.current || isUnmountedRef.current)
@@ -189,7 +281,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
189
281
  Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
190
282
  return;
191
283
  }
192
- if (base.connectionsRef.current.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
284
+ if (base.connections.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
193
285
  return;
194
286
  }
195
287
  await base.addConnection({
@@ -199,6 +291,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
199
291
  return {
200
292
  // Defaults, may be overridden by props
201
293
  send: (d) => defaultSend(id, d),
294
+ publish: (topic, payload) => publish(topic, payload, id),
295
+ subscribeCallbackToTopic,
202
296
  onReceive: (d) => defaultOnReceive(id, d),
203
297
  touchHeartbeat: () => touchHeartbeat(id),
204
298
  // Initial connection state
@@ -221,13 +315,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
221
315
  base.removeConnection(uuid);
222
316
  };
223
317
  const reconnectAll = async () => {
224
- for (const c of base.connectionsRef.current) {
318
+ const connectionIds = base.connections.map(c => c.uuid);
319
+ await Promise.all(connectionIds.map(async (id) => {
320
+ const c = base.getConnection(id);
321
+ if (!c)
322
+ return;
225
323
  await disconnect(c.uuid);
226
324
  await new Promise((res) => setTimeout(res, 250));
227
- }
228
- for (const c of base.connectionsRef.current) {
229
- await connect(c.uuid);
230
- }
325
+ return connect(c.uuid);
326
+ }));
231
327
  };
232
328
  useEffect(() => {
233
329
  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,17 @@ 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>>;
49
+ focussedConnection: string | undefined;
50
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
47
51
  connections: AppOrMessageLayer[];
48
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
49
52
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
50
53
  updateConnectionName: (uuid: string, name: string) => void;
51
54
  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,10 @@ 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
+ focussedConnection: string | undefined;
23
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
22
24
  connections: AppOrMessageLayer[];
23
- connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
24
25
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
25
26
  updateConnectionName: (uuid: string, name: string) => void;
26
27
  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.18",
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": {