ota-hub-reactjs 0.0.16 → 0.0.19

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.
@@ -1,52 +1,171 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
2
+ import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
3
3
  import mqtt from "mqtt";
4
- export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicFromUuid = undefined, pubTopicFromUuid = undefined, serverPort = 8883, clientId = undefined, username = undefined, password = undefined, serverAutoConnect = true, serverConnectOn = false, ...props }) {
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
5
  const base = MultiDeviceWhisperer(props);
6
6
  const clientRef = useRef(null);
7
7
  const isUnmountedRef = useRef(false);
8
+ // ✨ NEW: Manage our own reconnect timer
9
+ const reconnectTimeoutRef = useRef(null);
8
10
  const watchdogTimers = useRef({});
9
11
  const addingConnections = useRef(new Set());
10
- const connectToMQTTServer = () => {
12
+ const topicCallbacksRef = useRef(new Map());
13
+ const hasCallbackForTopic = (topic) => {
14
+ const callbacks = topicCallbacksRef.current.get(topic);
15
+ return !!callbacks && callbacks.size > 0;
16
+ };
17
+ const hasOtherConnectionUsingTopic = (topic, excludingUuid) => {
18
+ return base.connections.some((connection) => {
19
+ if (excludingUuid && connection.uuid === excludingUuid)
20
+ return false;
21
+ const connectionTopic = subTopicFromUuid?.(connection.uuid) ?? connection.uuid;
22
+ return connectionTopic === topic;
23
+ });
24
+ };
25
+ const normalizePayload = (payload) => {
26
+ if (typeof payload === "string") {
27
+ return new TextEncoder().encode(payload);
28
+ }
29
+ if (payload instanceof ArrayBuffer) {
30
+ return new Uint8Array(payload);
31
+ }
32
+ return payload;
33
+ };
34
+ const publish = async (topic, payload, uuidForLogs) => {
35
+ const client = clientRef.current;
36
+ if (!client || isUnmountedRef.current)
37
+ return;
38
+ const bytes = normalizePayload(payload);
39
+ if (uuidForLogs) {
40
+ base.appendLog(uuidForLogs, {
41
+ level: 5,
42
+ message: bytes,
43
+ });
44
+ }
45
+ await new Promise((resolve, reject) => {
46
+ client.publish(topic, bytes, { qos: 1 }, (err) => {
47
+ if (err) {
48
+ reject(err);
49
+ return;
50
+ }
51
+ resolve();
52
+ });
53
+ });
54
+ };
55
+ const subscribeCallbackToTopic = (topic, callback) => {
56
+ if (!topic)
57
+ return () => { };
58
+ const topicCallbacks = topicCallbacksRef.current.get(topic) ?? new Set();
59
+ const topicAlreadyRegistered = topicCallbacksRef.current.has(topic);
60
+ topicCallbacks.add(callback);
61
+ topicCallbacksRef.current.set(topic, topicCallbacks);
62
+ const client = clientRef.current;
63
+ if (client?.connected && !client.disconnecting && !topicAlreadyRegistered) {
64
+ client.subscribe(topic, { qos: 1 }, (err) => {
65
+ if (err)
66
+ console.error("Topic callback subscribe failed:", err);
67
+ });
68
+ }
69
+ return () => {
70
+ const currentCallbacks = topicCallbacksRef.current.get(topic);
71
+ if (!currentCallbacks)
72
+ return;
73
+ currentCallbacks.delete(callback);
74
+ if (currentCallbacks.size > 0)
75
+ return;
76
+ topicCallbacksRef.current.delete(topic);
77
+ // Keep topic subscribed when at least one managed connection still uses it.
78
+ if (hasOtherConnectionUsingTopic(topic))
79
+ return;
80
+ const latestClient = clientRef.current;
81
+ if (latestClient?.connected && !latestClient.disconnecting) {
82
+ latestClient.unsubscribe(topic, (err) => {
83
+ if (err)
84
+ console.error("Topic callback unsubscribe failed:", err);
85
+ });
86
+ }
87
+ };
88
+ };
89
+ const connectToMQTTServer = async () => {
90
+ if (!serverUrl && !serverUrlFnc) {
91
+ console.error("MQTT MultiDeviceWhisperer requires either serverUrl or serverUrlFnc");
92
+ return;
93
+ }
94
+ if (reconnectTimeoutRef.current)
95
+ clearTimeout(reconnectTimeoutRef.current);
11
96
  isUnmountedRef.current = false;
12
97
  if (clientRef.current) {
13
98
  if (clientRef.current.connected) {
14
99
  base.setIsReady(true);
15
100
  return;
16
101
  }
102
+ clientRef.current.removeAllListeners();
17
103
  clientRef.current.end(true);
18
104
  }
19
105
  try {
20
- const new_client = mqtt.connect(serverUrl, {
106
+ const finalUrl = serverUrlFnc ? await serverUrlFnc() : serverUrl;
107
+ if (!finalUrl)
108
+ throw new Error("Generated MQTT URL was undefined.");
109
+ const options = {
21
110
  port: serverPort,
22
111
  clientId: clientId,
23
112
  username,
24
113
  password,
25
114
  clean: true,
26
115
  keepalive: 30,
27
- reconnectPeriod: 3000,
28
- });
116
+ reconnectPeriod: 0, // We now handle this ourselves
117
+ };
118
+ const new_client = mqtt.connect(finalUrl, options);
29
119
  new_client.on("connect", () => {
30
120
  console.log("MQTT Whisperer Connected");
31
121
  base.setIsReady(true);
122
+ // Re-subscribe topic callbacks
123
+ topicCallbacksRef.current.forEach((_callbacks, topic) => {
124
+ new_client.subscribe(topic, { qos: 1 }, (err) => {
125
+ if (err)
126
+ console.error("Topic callback subscribe failed:", err);
127
+ });
128
+ });
129
+ // Since we create a brand new client on reconnect, we MUST re-subscribe
130
+ // to all actively managed device connections.
131
+ base.connections.forEach((conn) => {
132
+ const topic = subTopicFromUuid?.(conn.uuid) ?? conn.uuid;
133
+ new_client.subscribe(topic, { qos: 1 });
134
+ });
32
135
  });
33
- new_client.on("reconnect", () => {
34
- console.log("MQTT Whisperer Reconnecting...");
136
+ const handleReconnectCycle = () => {
35
137
  base.setIsReady(false);
36
- });
37
- new_client.on("close", () => {
38
- console.log("MQTT Whisperer Closed");
39
- base.setIsReady(false);
40
- });
138
+ if (isUnmountedRef.current)
139
+ return;
140
+ console.log("MQTT Whisperer closed. Scheduling async reconnect...");
141
+ // Ensure we only queue one reconnect attempt
142
+ if (reconnectTimeoutRef.current)
143
+ clearTimeout(reconnectTimeoutRef.current);
144
+ if (serverAutoConnect || serverConnectOn) {
145
+ reconnectTimeoutRef.current = setTimeout(() => {
146
+ connectToMQTTServer();
147
+ }, 3000); // 3 second backoff
148
+ }
149
+ };
150
+ new_client.on("close", handleReconnectCycle);
41
151
  new_client.on("error", (err) => {
42
- base.setIsReady(false);
43
152
  console.error("MQTT Whisperer Error:", err);
153
+ handleReconnectCycle();
44
154
  });
45
155
  new_client.on("message", (topic, payload) => {
46
156
  if (isUnmountedRef.current)
47
157
  return;
48
158
  const uuid = uuidFromMessage(topic, payload);
49
159
  const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
160
+ const topicCallbacks = topicCallbacksRef.current.get(topic);
161
+ topicCallbacks?.forEach((cb) => {
162
+ try {
163
+ cb(bytes, topic);
164
+ }
165
+ catch (err) {
166
+ console.error("Topic callback failed:", err);
167
+ }
168
+ });
50
169
  if (!uuid)
51
170
  return;
52
171
  const conn = base.getConnection(uuid);
@@ -62,25 +181,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
62
181
  catch (err) {
63
182
  console.error("MQTT init failed:", err);
64
183
  base.setIsReady(false);
65
- }
66
- return () => {
67
- isUnmountedRef.current = true;
68
- base.setIsReady(false);
69
- Object.keys(watchdogTimers.current).forEach((uuid) => {
70
- const timers = watchdogTimers.current[uuid];
71
- if (timers) {
72
- clearTimeout(timers.ping);
73
- clearTimeout(timers.warn);
74
- clearTimeout(timers.fail);
75
- }
76
- });
77
- watchdogTimers.current = {};
78
- if (clientRef.current) {
79
- clientRef.current.removeAllListeners();
80
- clientRef.current.end(true);
184
+ // If fetching the URL fails, try again in 3 seconds
185
+ if (!isUnmountedRef.current && (serverAutoConnect || serverConnectOn)) {
186
+ reconnectTimeoutRef.current = setTimeout(() => {
187
+ connectToMQTTServer();
188
+ }, 3000);
81
189
  }
82
- clientRef.current = null;
83
- };
190
+ }
84
191
  };
85
192
  const connect = async (uuid) => {
86
193
  const conn = base.getConnection(uuid);
@@ -105,6 +212,11 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
105
212
  if (!clientRef.current || isUnmountedRef.current)
106
213
  return;
107
214
  const topic = subTopicFromUuid?.(uuid) ?? uuid;
215
+ // Keep topic subscribed if callbacks or other device-connections still depend on it.
216
+ if (hasCallbackForTopic(topic) ||
217
+ hasOtherConnectionUsingTopic(topic, uuid)) {
218
+ return;
219
+ }
108
220
  if (clientRef.current.connected && !clientRef.current.disconnecting) {
109
221
  clientRef.current.unsubscribe(topic, async (err) => {
110
222
  if (err)
@@ -115,12 +227,12 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
115
227
  });
116
228
  }
117
229
  else {
118
- console.warn("Skipped subscribe - client disconnected or unmounted");
230
+ console.warn("Skipped unsubscribe - client disconnected or unmounted");
119
231
  }
120
232
  };
121
233
  function touchHeartbeat(uuid) {
122
- if (isUnmountedRef.current)
123
- return; // Stop if unmounted
234
+ if (isUnmountedRef.current || !enableWatchdog)
235
+ return;
124
236
  clearTimeout(watchdogTimers.current[uuid]?.ping);
125
237
  clearTimeout(watchdogTimers.current[uuid]?.warn);
126
238
  clearTimeout(watchdogTimers.current[uuid]?.fail);
@@ -128,7 +240,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
128
240
  base.updateConnection(uuid, (c) => ({
129
241
  ...c,
130
242
  isConnected: true,
131
- isConnecting: false
243
+ isConnecting: false,
132
244
  }));
133
245
  const ping = setTimeout(() => {
134
246
  if (isUnmountedRef.current)
@@ -138,12 +250,20 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
138
250
  const warn = setTimeout(() => {
139
251
  if (isUnmountedRef.current)
140
252
  return;
141
- base.updateConnection(uuid, (c) => ({ ...c, isConnected: false, isConnecting: true }));
253
+ base.updateConnection(uuid, (c) => ({
254
+ ...c,
255
+ isConnected: false,
256
+ isConnecting: true,
257
+ }));
142
258
  }, 30000);
143
259
  const fail = setTimeout(() => {
144
260
  if (isUnmountedRef.current)
145
261
  return;
146
- base.updateConnection(uuid, (c) => ({ ...c, isConnected: false, isConnecting: false }));
262
+ base.updateConnection(uuid, (c) => ({
263
+ ...c,
264
+ isConnected: false,
265
+ isConnecting: false,
266
+ }));
147
267
  }, 60000);
148
268
  watchdogTimers.current[uuid] = { ping, warn, fail };
149
269
  }
@@ -173,24 +293,19 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
173
293
  const conn = base.getConnection(uuid);
174
294
  if (!conn)
175
295
  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!
296
+ const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
297
+ await publish(topic, data, uuid);
184
298
  };
185
- const addConnection = async ({ uuid, propCreator }) => {
299
+ const addConnection = async ({ uuid, propCreator, }) => {
186
300
  if (!clientRef.current || isUnmountedRef.current)
187
- return;
301
+ return "";
188
302
  if (!uuid) {
189
303
  Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
190
- return;
304
+ return "";
191
305
  }
192
- if (base.connections.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
193
- return;
306
+ if (base.connections.some((c) => c.uuid === uuid) ||
307
+ addingConnections.current.has(uuid)) {
308
+ return "";
194
309
  }
195
310
  await base.addConnection({
196
311
  uuid,
@@ -199,16 +314,16 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
199
314
  return {
200
315
  // Defaults, may be overridden by props
201
316
  send: (d) => defaultSend(id, d),
317
+ publish: (topic, payload) => publish(topic, payload, id),
318
+ subscribeCallbackToTopic,
202
319
  onReceive: (d) => defaultOnReceive(id, d),
203
320
  touchHeartbeat: () => touchHeartbeat(id),
204
321
  // Initial connection state
205
322
  ...base.createInitialConnectionState(id),
206
- // From props
207
- ...props
323
+ ...props,
208
324
  };
209
- }
325
+ },
210
326
  });
211
- // Delete this adding connections item
212
327
  addingConnections.current.delete(uuid);
213
328
  // Connect immediately
214
329
  const conn = base.getConnection(uuid);
@@ -221,7 +336,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
221
336
  base.removeConnection(uuid);
222
337
  };
223
338
  const reconnectAll = async () => {
224
- const connectionIds = base.connections.map(c => c.uuid);
339
+ const connectionIds = base.connections.map((c) => c.uuid);
225
340
  await Promise.all(connectionIds.map(async (id) => {
226
341
  const c = base.getConnection(id);
227
342
  if (!c)
@@ -234,8 +349,27 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
234
349
  useEffect(() => {
235
350
  if (!(serverAutoConnect || serverConnectOn))
236
351
  return;
237
- const cleanup = connectToMQTTServer();
238
- return cleanup;
352
+ connectToMQTTServer();
353
+ return () => {
354
+ isUnmountedRef.current = true;
355
+ if (reconnectTimeoutRef.current)
356
+ clearTimeout(reconnectTimeoutRef.current);
357
+ Object.keys(watchdogTimers.current).forEach((uuid) => {
358
+ const timers = watchdogTimers.current[uuid];
359
+ if (timers) {
360
+ clearTimeout(timers.ping);
361
+ clearTimeout(timers.warn);
362
+ clearTimeout(timers.fail);
363
+ }
364
+ });
365
+ watchdogTimers.current = {};
366
+ topicCallbacksRef.current.clear();
367
+ if (clientRef.current) {
368
+ clientRef.current.removeAllListeners();
369
+ clientRef.current.end(true);
370
+ }
371
+ clientRef.current = null;
372
+ };
239
373
  }, [serverUrl, serverConnectOn]);
240
374
  return {
241
375
  ...base,
@@ -244,6 +378,6 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
244
378
  connect,
245
379
  disconnect,
246
380
  reconnectAll,
247
- connectToMQTTServer
381
+ connectToMQTTServer,
248
382
  };
249
383
  }
@@ -13,8 +13,8 @@ export declare class UsbTransport {
13
13
  */
14
14
  connect(baudRate?: number): Promise<void>;
15
15
  /**
16
- * Writes bytes to the device
17
- */
16
+ * Writes bytes to the device
17
+ */
18
18
  write(data: Uint8Array): Promise<void>;
19
19
  /**
20
20
  * Reads bytes from the device.
@@ -39,13 +39,15 @@ export type SerialConnectionState = DeviceConnectionState & {
39
39
  slipReadWrite?: boolean;
40
40
  };
41
41
  export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: DeviceWhispererProps<AppOrMessageLayer>): {
42
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
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
46
  reconnectAll: (...connectionProps: any) => Promise<void>;
47
47
  releasePortByDefault: boolean;
48
48
  setReleasePortByDefault: import("react").Dispatch<import("react").SetStateAction<boolean>>;
49
+ focussedConnection: string | undefined;
50
+ setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
49
51
  connections: AppOrMessageLayer[];
50
52
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
51
53
  updateConnectionName: (uuid: string, name: string) => void;
@@ -1,4 +1,4 @@
1
- import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
1
+ import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  /*
4
4
  ┌────────────────────────────────┐
@@ -31,16 +31,19 @@ export class UsbTransport {
31
31
  const interfaces = this.device.configuration?.interfaces || [];
32
32
  let ctrlIface;
33
33
  let dataIface;
34
- dataIface = interfaces.find(iface => {
34
+ dataIface = interfaces.find((iface) => {
35
35
  const endpoints = iface.alternate.endpoints;
36
- return endpoints.some(e => e.direction === 'in' && e.type === 'bulk') &&
37
- endpoints.some(e => e.direction === 'out' && e.type === 'bulk');
36
+ return (endpoints.some((e) => e.direction === "in" && e.type === "bulk") &&
37
+ endpoints.some((e) => e.direction === "out" && e.type === "bulk"));
38
38
  });
39
39
  if (dataIface) {
40
40
  this.dataInterface = dataIface.interfaceNumber;
41
- ctrlIface = interfaces.find(i => i.interfaceNumber === this.dataInterface - 1)
42
- || interfaces.find(i => i.alternate.interfaceClass === 2);
43
- this.controlInterface = ctrlIface ? ctrlIface.interfaceNumber : this.dataInterface;
41
+ ctrlIface =
42
+ interfaces.find((i) => i.interfaceNumber === this.dataInterface - 1) ||
43
+ interfaces.find((i) => i.alternate.interfaceClass === 2);
44
+ this.controlInterface = ctrlIface
45
+ ? ctrlIface.interfaceNumber
46
+ : this.dataInterface;
44
47
  }
45
48
  if (!dataIface) {
46
49
  throw new Error("No serial-compatible Bulk interface found.");
@@ -62,14 +65,14 @@ export class UsbTransport {
62
65
  throw new Error(`Failed to claim Data Interface. Android OS has locked the device driver. Try using a 'Vendor Specific' USB Class device or a native Serial App workaround.`);
63
66
  }
64
67
  const endpoints = dataIface.alternate.endpoints;
65
- this.endpointIn = endpoints.find(e => e.direction === 'in' && e.type === 'bulk').endpointNumber;
66
- this.endpointOut = endpoints.find(e => e.direction === 'out' && e.type === 'bulk').endpointNumber;
68
+ this.endpointIn = endpoints.find((e) => e.direction === "in" && e.type === "bulk").endpointNumber;
69
+ this.endpointOut = endpoints.find((e) => e.direction === "out" && e.type === "bulk").endpointNumber;
67
70
  await this.setBaudRate(baudRate);
68
71
  await this.setSignals({ dtr: false, rts: false });
69
72
  }
70
73
  /**
71
- * Writes bytes to the device
72
- */
74
+ * Writes bytes to the device
75
+ */
73
76
  async write(data) {
74
77
  if (!this.device.opened)
75
78
  return;
@@ -84,7 +87,7 @@ export class UsbTransport {
84
87
  return null;
85
88
  try {
86
89
  const result = await this.device.transferIn(this.endpointIn, length);
87
- if (result.status === 'ok' && result.data) {
90
+ if (result.status === "ok" && result.data) {
88
91
  return new Uint8Array(result.data.buffer);
89
92
  }
90
93
  }
@@ -100,13 +103,13 @@ export class UsbTransport {
100
103
  async setSignals({ dtr, rts }) {
101
104
  if (!this.device.opened)
102
105
  return;
103
- const value = (Number(dtr) | (Number(rts) << 1));
106
+ const value = Number(dtr) | (Number(rts) << 1);
104
107
  await this.device.controlTransferOut({
105
- requestType: 'class',
106
- recipient: 'interface',
108
+ requestType: "class",
109
+ recipient: "interface",
107
110
  request: UsbTransport.SET_CONTROL_LINE_STATE,
108
111
  value: value,
109
- index: this.controlInterface
112
+ index: this.controlInterface,
110
113
  });
111
114
  }
112
115
  async setBaudRate(baud) {
@@ -121,11 +124,11 @@ export class UsbTransport {
121
124
  view.setUint8(5, 0);
122
125
  view.setUint8(6, 8);
123
126
  await this.device.controlTransferOut({
124
- requestType: 'class',
125
- recipient: 'interface',
127
+ requestType: "class",
128
+ recipient: "interface",
126
129
  request: UsbTransport.SET_LINE_CODING,
127
130
  value: 0,
128
- index: this.controlInterface
131
+ index: this.controlInterface,
129
132
  }, buffer);
130
133
  }
131
134
  async disconnect() {
@@ -171,7 +174,10 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
171
174
  const asText = decoder.decode(bytes);
172
175
  const combined = conn.readBufferLeftover + asText;
173
176
  const lines = combined.split("\n");
174
- base.updateConnection(uuid, (c) => ({ ...c, readBufferLeftover: lines.pop() || "" }));
177
+ base.updateConnection(uuid, (c) => ({
178
+ ...c,
179
+ readBufferLeftover: lines.pop() || "",
180
+ }));
175
181
  for (const line of lines) {
176
182
  if (line.trim()) {
177
183
  base.appendLog(uuid, { level: 2, message: line.trim() });
@@ -204,7 +210,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
204
210
  const bytes = await transport.read(64);
205
211
  if (!bytes || bytes.length === 0) {
206
212
  // Small delay to prevent CPU spinning if device sends nothing
207
- await new Promise(r => setTimeout(r, 10));
213
+ await new Promise((r) => setTimeout(r, 10));
208
214
  continue;
209
215
  }
210
216
  // --- Same Decoding Logic as before ---
@@ -212,7 +218,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
212
218
  const b = bytes[i];
213
219
  const currentConn = base.getConnection(uuid); // Refresh ref
214
220
  if (inSlipFrame) {
215
- if (b === 0xC0) {
221
+ if (b === 0xc0) {
216
222
  if (slipBuffer.length > 0) {
217
223
  const payload = new Uint8Array(slipBuffer);
218
224
  currentConn?.onReceive?.(payload);
@@ -222,14 +228,14 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
222
228
  escapeNext = false;
223
229
  }
224
230
  else if (escapeNext) {
225
- if (b === 0xDC)
226
- slipBuffer.push(0xC0);
227
- else if (b === 0xDD)
228
- slipBuffer.push(0xDB);
231
+ if (b === 0xdc)
232
+ slipBuffer.push(0xc0);
233
+ else if (b === 0xdd)
234
+ slipBuffer.push(0xdb);
229
235
  slipBuffer = []; // reset on error?
230
236
  escapeNext = false;
231
237
  }
232
- else if (b === 0xDB) {
238
+ else if (b === 0xdb) {
233
239
  escapeNext = true;
234
240
  }
235
241
  else {
@@ -237,7 +243,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
237
243
  }
238
244
  continue;
239
245
  }
240
- if (b === 0xC0 && currentConn?.slipReadWrite) {
246
+ if (b === 0xc0 && currentConn?.slipReadWrite) {
241
247
  inSlipFrame = true;
242
248
  slipBuffer = [];
243
249
  continue;
@@ -281,18 +287,18 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
281
287
  // Close existing if open
282
288
  if (conn.transport)
283
289
  await disconnect(uuid);
284
- base.updateConnection(uuid, c => ({ ...c, isConnecting: true }));
290
+ base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
285
291
  const transport = new UsbTransport(conn.device);
286
292
  try {
287
293
  await transport.connect(baudrate);
288
294
  // Optional: Auto-restart on connect
289
295
  await restartDevice(uuid, transport);
290
- base.updateConnection(uuid, c => ({
296
+ base.updateConnection(uuid, (c) => ({
291
297
  ...c,
292
298
  transport,
293
299
  baudrate,
294
300
  isConnected: true,
295
- isConnecting: false
301
+ isConnecting: false,
296
302
  }));
297
303
  await conn.onConnect?.();
298
304
  // Start polling loop
@@ -300,7 +306,11 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
300
306
  }
301
307
  catch (err) {
302
308
  console.error(err);
303
- base.updateConnection(uuid, c => ({ ...c, isConnected: false, isConnecting: false }));
309
+ base.updateConnection(uuid, (c) => ({
310
+ ...c,
311
+ isConnected: false,
312
+ isConnecting: false,
313
+ }));
304
314
  await disconnect(uuid);
305
315
  }
306
316
  };
@@ -309,18 +319,18 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
309
319
  if (conn?.transport) {
310
320
  await conn.transport.disconnect();
311
321
  }
312
- base.updateConnection(uuid, c => ({
322
+ base.updateConnection(uuid, (c) => ({
313
323
  ...c,
314
324
  transport: releasePortByDefaultRef.current ? null : c.transport,
315
325
  isConnected: false,
316
- isConnecting: false
326
+ isConnecting: false,
317
327
  }));
318
328
  await conn?.onDisconnect?.();
319
329
  };
320
- const addConnection = async ({ uuid, propCreator }) => {
330
+ const addConnection = async ({ uuid, propCreator, }) => {
321
331
  try {
322
332
  const device = await navigator.usb.requestDevice({
323
- filters: []
333
+ filters: [],
324
334
  });
325
335
  const return_uuid = await base.addConnection({
326
336
  uuid,
@@ -334,9 +344,9 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
334
344
  // Initial connection state
335
345
  ...base.createInitialConnectionState(id),
336
346
  // From props
337
- ...props
347
+ ...props,
338
348
  };
339
- }
349
+ },
340
350
  });
341
351
  const conn = base.getConnection(return_uuid);
342
352
  if (conn?.autoConnect)
@@ -352,7 +362,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
352
362
  base.removeConnection(uuid);
353
363
  };
354
364
  const reconnectAll = async (...connectionProps) => {
355
- const connectionIds = base.connections.map(c => c.uuid);
365
+ const connectionIds = base.connections.map((c) => c.uuid);
356
366
  await Promise.all(connectionIds.map(async (id) => {
357
367
  const c = base.getConnection(id);
358
368
  if (!c)
@@ -362,7 +372,9 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
362
372
  return connect(c.uuid, ...connectionProps);
363
373
  }));
364
374
  };
365
- useEffect(() => { base.setIsReady(true); }, []);
375
+ useEffect(() => {
376
+ base.setIsReady(true);
377
+ }, []);
366
378
  return {
367
379
  ...base,
368
380
  addConnection,
@@ -371,6 +383,6 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
371
383
  disconnect,
372
384
  reconnectAll,
373
385
  releasePortByDefault: releasePortByDefaultState,
374
- setReleasePortByDefault: setReleasePortByDefaultState
386
+ setReleasePortByDefault: setReleasePortByDefaultState,
375
387
  };
376
388
  }
@@ -13,12 +13,14 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
13
13
  server_url: string;
14
14
  server_port: number;
15
15
  }): {
16
- addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
16
+ addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
17
17
  removeConnection: (uuid: string) => Promise<void>;
18
18
  connect: (uuid: string, attempt?: number) => Promise<void>;
19
19
  disconnect: (uuid: string) => Promise<void>;
20
20
  checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
21
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
25
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
24
26
  updateConnectionName: (uuid: string, name: string) => void;