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.
- package/dist/base/device-whisperer.d.ts +7 -1
- package/dist/base/device-whisperer.js +7 -5
- package/dist/message_layers/protobuf-wrapper.d.ts +3 -1
- package/dist/message_layers/protobuf-wrapper.js +12 -14
- package/dist/transport_layers/esp32-device-whisperer.d.ts +3 -1
- package/dist/transport_layers/esp32-device-whisperer.js +65 -37
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +19 -4
- package/dist/transport_layers/mqtt-device-whisperer.js +193 -59
- package/dist/transport_layers/serial-device-whisperer.d.ts +5 -3
- package/dist/transport_layers/serial-device-whisperer.js +53 -41
- package/dist/transport_layers/websocket-device-whisperer.d.ts +3 -1
- package/dist/transport_layers/websocket-device-whisperer.js +27 -19
- package/dist/types/builds.js +0 -2
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
34
|
-
console.log("MQTT Whisperer Reconnecting...");
|
|
136
|
+
const handleReconnectCycle = () => {
|
|
35
137
|
base.setIsReady(false);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log("MQTT Whisperer
|
|
39
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
230
|
+
console.warn("Skipped unsubscribe - client disconnected or unmounted");
|
|
119
231
|
}
|
|
120
232
|
};
|
|
121
233
|
function touchHeartbeat(uuid) {
|
|
122
|
-
if (isUnmountedRef.current)
|
|
123
|
-
return;
|
|
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) => ({
|
|
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) => ({
|
|
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
|
|
177
|
-
|
|
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) ||
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
return
|
|
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
|
-
|
|
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 ===
|
|
37
|
-
endpoints.some(e => e.direction ===
|
|
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 =
|
|
42
|
-
|
|
43
|
-
|
|
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 ===
|
|
66
|
-
this.endpointOut = endpoints.find(e => e.direction ===
|
|
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
|
-
|
|
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 ===
|
|
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 =
|
|
106
|
+
const value = Number(dtr) | (Number(rts) << 1);
|
|
104
107
|
await this.device.controlTransferOut({
|
|
105
|
-
requestType:
|
|
106
|
-
recipient:
|
|
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:
|
|
125
|
-
recipient:
|
|
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) => ({
|
|
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 ===
|
|
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 ===
|
|
226
|
-
slipBuffer.push(
|
|
227
|
-
else if (b ===
|
|
228
|
-
slipBuffer.push(
|
|
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 ===
|
|
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 ===
|
|
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 => ({
|
|
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(() => {
|
|
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;
|