ota-hub-reactjs 0.0.18 → 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 +1 -1
- package/dist/base/device-whisperer.js +4 -5
- package/dist/message_layers/protobuf-wrapper.d.ts +1 -1
- package/dist/message_layers/protobuf-wrapper.js +12 -14
- package/dist/transport_layers/esp32-device-whisperer.d.ts +1 -1
- package/dist/transport_layers/esp32-device-whisperer.js +65 -37
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +6 -4
- package/dist/transport_layers/mqtt-device-whisperer.js +94 -54
- package/dist/transport_layers/serial-device-whisperer.d.ts +3 -3
- package/dist/transport_layers/serial-device-whisperer.js +53 -41
- package/dist/transport_layers/websocket-device-whisperer.d.ts +1 -1
- package/dist/transport_layers/websocket-device-whisperer.js +27 -19
- package/dist/types/builds.js +0 -2
- package/package.json +1 -1
|
@@ -36,7 +36,7 @@ export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({
|
|
|
36
36
|
focussedConnection: string | undefined;
|
|
37
37
|
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
38
38
|
connections: T[];
|
|
39
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
|
|
39
|
+
addConnection: ({ uuid, propCreator, }: AddConnectionProps<T>) => Promise<string>;
|
|
40
40
|
removeConnection: (uuid: string) => void;
|
|
41
41
|
connect: (_uuid: string) => void;
|
|
42
42
|
disconnect: (_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
|
-
;
|
|
4
3
|
// Initial, generic state for the generic device
|
|
5
4
|
export function createDefaultInitialDeviceState(uuid, props) {
|
|
6
5
|
return {
|
|
@@ -11,7 +10,7 @@ export function createDefaultInitialDeviceState(uuid, props) {
|
|
|
11
10
|
logs: [],
|
|
12
11
|
readBufferLeftover: "",
|
|
13
12
|
readBufferLeftoverAsBytes: new Uint8Array([]),
|
|
14
|
-
...props
|
|
13
|
+
...props,
|
|
15
14
|
};
|
|
16
15
|
}
|
|
17
16
|
/* One Device Whisperer is used for all like-devices, such as all Serial with Protobuf.
|
|
@@ -45,13 +44,13 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
45
44
|
}));
|
|
46
45
|
};
|
|
47
46
|
// ---------- ADD ----------
|
|
48
|
-
const addConnection = async ({ uuid, propCreator }) => {
|
|
47
|
+
const addConnection = async ({ uuid, propCreator, }) => {
|
|
49
48
|
uuid = uuid ?? `unnamed_device_${connectionsRef.current.size}`;
|
|
50
49
|
const props = propCreator?.(uuid);
|
|
51
50
|
const newConnection = {
|
|
52
51
|
...createDefaultInitialDeviceState(uuid),
|
|
53
52
|
...createInitialConnectionState(uuid),
|
|
54
|
-
...props
|
|
53
|
+
...props,
|
|
55
54
|
};
|
|
56
55
|
connectionsRef.current.set(uuid, newConnection);
|
|
57
56
|
setConnections(Array.from(connectionsRef.current.values()));
|
|
@@ -82,6 +81,6 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
82
81
|
appendLog,
|
|
83
82
|
isReady,
|
|
84
83
|
setIsReady,
|
|
85
|
-
createInitialConnectionState
|
|
84
|
+
createInitialConnectionState,
|
|
86
85
|
};
|
|
87
86
|
}
|
|
@@ -16,7 +16,7 @@ export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState,
|
|
|
16
16
|
expectLength?: boolean;
|
|
17
17
|
};
|
|
18
18
|
export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConnectionState, Topic extends string | number, MessageRX = any, MessageTX = any>({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER, }: ProtobufDeviceWhispererProps<AppLayer, Topic, MessageRX, MessageTX>): {
|
|
19
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppLayer>) => Promise<string>;
|
|
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
22
|
focussedConnection: string | undefined;
|
|
@@ -10,7 +10,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
10
10
|
const length = payload.length;
|
|
11
11
|
const header = new Uint8Array([
|
|
12
12
|
(length >> 8) & 0xff, // Most significant byte first
|
|
13
|
-
length & 0xff // Least significant byte second
|
|
13
|
+
length & 0xff, // Least significant byte second
|
|
14
14
|
]);
|
|
15
15
|
return concatUint8Arrays(header, payload);
|
|
16
16
|
};
|
|
@@ -55,9 +55,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
55
55
|
// --- Inbound buffering ---
|
|
56
56
|
const buffers = {};
|
|
57
57
|
const protoBufOnReceiveHandler = (uuid, data) => {
|
|
58
|
-
const bytes = typeof data === "string"
|
|
59
|
-
? new TextEncoder().encode(data)
|
|
60
|
-
: data;
|
|
58
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
61
59
|
buffers[uuid] = concatUint8Arrays(buffers[uuid] || new Uint8Array(), bytes);
|
|
62
60
|
let buffer = buffers[uuid];
|
|
63
61
|
while (buffer.length > 0) {
|
|
@@ -73,14 +71,14 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
73
71
|
catch (err) {
|
|
74
72
|
transportLayer.appendLog(uuid, {
|
|
75
73
|
level: 0,
|
|
76
|
-
message: `[!] Error in handler for topic "${topic}": ${err}
|
|
74
|
+
message: `[!] Error in handler for topic "${topic}": ${err}`,
|
|
77
75
|
});
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
else {
|
|
81
79
|
transportLayer.appendLog(uuid, {
|
|
82
80
|
level: 1,
|
|
83
|
-
message: `[!] Unknown Protobuf topic: "${topic}"
|
|
81
|
+
message: `[!] Unknown Protobuf topic: "${topic}"`,
|
|
84
82
|
});
|
|
85
83
|
}
|
|
86
84
|
buffer = buffers[uuid];
|
|
@@ -97,7 +95,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
97
95
|
if (text) {
|
|
98
96
|
transportLayer.appendLog(uuid, {
|
|
99
97
|
level: 2,
|
|
100
|
-
message: text
|
|
98
|
+
message: text,
|
|
101
99
|
});
|
|
102
100
|
}
|
|
103
101
|
continue;
|
|
@@ -108,11 +106,11 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
108
106
|
buffers[uuid] = buffer;
|
|
109
107
|
const preview = Array.from(garbage)
|
|
110
108
|
.slice(0, 16)
|
|
111
|
-
.map(b => b.toString(16).padStart(2,
|
|
112
|
-
.join(
|
|
109
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
110
|
+
.join(" ");
|
|
113
111
|
transportLayer.appendLog(uuid, {
|
|
114
112
|
level: 1,
|
|
115
|
-
message: `[!] Skipped invalid bytes before Protobuf header: ${preview}... (${garbage.length} bytes)
|
|
113
|
+
message: `[!] Skipped invalid bytes before Protobuf header: ${preview}... (${garbage.length} bytes)`,
|
|
116
114
|
});
|
|
117
115
|
continue;
|
|
118
116
|
}
|
|
@@ -121,22 +119,22 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
121
119
|
buffers[uuid] = buffer;
|
|
122
120
|
};
|
|
123
121
|
// --- Override addConnection to wrap onReceive ---
|
|
124
|
-
const addConnection = async ({ uuid, propCreator }) => {
|
|
122
|
+
const addConnection = async ({ uuid, propCreator, }) => {
|
|
125
123
|
return await transportLayer.addConnection({
|
|
126
124
|
uuid,
|
|
127
125
|
propCreator: (id) => {
|
|
128
126
|
const props = propCreator?.(id);
|
|
129
127
|
return {
|
|
130
128
|
onReceive: (data) => protoBufOnReceiveHandler(id, data),
|
|
131
|
-
...props
|
|
129
|
+
...props,
|
|
132
130
|
};
|
|
133
|
-
}
|
|
131
|
+
},
|
|
134
132
|
});
|
|
135
133
|
};
|
|
136
134
|
return {
|
|
137
135
|
...transportLayer,
|
|
138
136
|
addConnection,
|
|
139
137
|
sendProtobuf,
|
|
140
|
-
protoBufOnReceiveHandler
|
|
138
|
+
protoBufOnReceiveHandler,
|
|
141
139
|
};
|
|
142
140
|
}
|
|
@@ -18,7 +18,7 @@ export type FlashFirmwareProps = {
|
|
|
18
18
|
export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
|
|
19
19
|
releasePortByDefault: boolean;
|
|
20
20
|
} & DeviceWhispererProps<AppOrMessageLayer>): {
|
|
21
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
21
|
+
addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
22
22
|
removeConnection: (uuid: string) => Promise<void>;
|
|
23
23
|
connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
|
|
24
24
|
disconnect: (uuid: string, timeout?: number) => Promise<void>;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { ESPLoader, Transport } from "esptool-js";
|
|
3
|
-
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
3
|
+
import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
|
|
4
4
|
export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
|
|
5
5
|
const base = MultiDeviceWhisperer(props);
|
|
6
6
|
const defaultOnReceive = (uuid, data) => {
|
|
7
|
-
const text = typeof data === "string"
|
|
8
|
-
? data
|
|
9
|
-
: new TextDecoder().decode(data);
|
|
7
|
+
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
10
8
|
const trimmed = text.trim();
|
|
11
9
|
if (!trimmed)
|
|
12
10
|
return;
|
|
@@ -45,7 +43,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
45
43
|
console.log("Kack!");
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
|
-
;
|
|
49
46
|
const { value, done } = await reader.next();
|
|
50
47
|
if (done || !value)
|
|
51
48
|
break;
|
|
@@ -54,7 +51,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
54
51
|
const b = bytes[i];
|
|
55
52
|
if (inSlipFrame) {
|
|
56
53
|
// SLIP decoding
|
|
57
|
-
if (b ===
|
|
54
|
+
if (b === 0xc0) {
|
|
58
55
|
if (slipBuffer.length > 0) {
|
|
59
56
|
// complete SLIP frame received
|
|
60
57
|
const payload = new Uint8Array(slipBuffer);
|
|
@@ -75,10 +72,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
75
72
|
escapeNext = false;
|
|
76
73
|
}
|
|
77
74
|
else if (escapeNext) {
|
|
78
|
-
if (b ===
|
|
79
|
-
slipBuffer.push(
|
|
80
|
-
else if (b ===
|
|
81
|
-
slipBuffer.push(
|
|
75
|
+
if (b === 0xdc)
|
|
76
|
+
slipBuffer.push(0xc0);
|
|
77
|
+
else if (b === 0xdd)
|
|
78
|
+
slipBuffer.push(0xdb);
|
|
82
79
|
else {
|
|
83
80
|
// protocol violation: discard frame
|
|
84
81
|
slipBuffer = [];
|
|
@@ -86,7 +83,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
86
83
|
}
|
|
87
84
|
escapeNext = false;
|
|
88
85
|
}
|
|
89
|
-
else if (b ===
|
|
86
|
+
else if (b === 0xdb) {
|
|
90
87
|
escapeNext = true;
|
|
91
88
|
}
|
|
92
89
|
else {
|
|
@@ -94,7 +91,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
94
91
|
}
|
|
95
92
|
continue;
|
|
96
93
|
}
|
|
97
|
-
if (b ===
|
|
94
|
+
if (b === 0xc0 && conn.slipReadWrite) {
|
|
98
95
|
// start of a SLIP frame
|
|
99
96
|
inSlipFrame = true;
|
|
100
97
|
slipBuffer = [];
|
|
@@ -102,7 +99,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
102
99
|
continue;
|
|
103
100
|
}
|
|
104
101
|
// treat as normal text (correctly)
|
|
105
|
-
readBuffer += textDecoder.decode(new Uint8Array([b]), {
|
|
102
|
+
readBuffer += textDecoder.decode(new Uint8Array([b]), {
|
|
103
|
+
stream: true,
|
|
104
|
+
});
|
|
106
105
|
// check for newline
|
|
107
106
|
let newlineIndex;
|
|
108
107
|
while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
|
|
@@ -149,11 +148,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
149
148
|
let port = conn?.port;
|
|
150
149
|
if (!port) {
|
|
151
150
|
port = await navigator.serial.requestPort({
|
|
152
|
-
filters: [{ usbVendorId: 0x303a }]
|
|
151
|
+
filters: [{ usbVendorId: 0x303a }],
|
|
153
152
|
});
|
|
154
153
|
base.updateConnection(uuid, (c) => ({ ...c, port }));
|
|
155
154
|
}
|
|
156
|
-
;
|
|
157
155
|
base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
|
|
158
156
|
const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
|
|
159
157
|
const transport = new Transport(port, false, false);
|
|
@@ -161,7 +159,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
161
159
|
const esploader = new ESPLoader({
|
|
162
160
|
transport,
|
|
163
161
|
baudrate: use_baudrate,
|
|
164
|
-
enableTracing: false
|
|
162
|
+
enableTracing: false,
|
|
165
163
|
});
|
|
166
164
|
try {
|
|
167
165
|
await esploader.main();
|
|
@@ -170,7 +168,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
170
168
|
console.log("failed to esploader.main()", e);
|
|
171
169
|
return;
|
|
172
170
|
}
|
|
173
|
-
;
|
|
174
171
|
try {
|
|
175
172
|
const mac = await esploader.chip.readMac(esploader);
|
|
176
173
|
console.log("Mac from device:", mac);
|
|
@@ -182,7 +179,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
182
179
|
if (restart_on_connect) {
|
|
183
180
|
await restartDevice(uuid, transport);
|
|
184
181
|
}
|
|
185
|
-
;
|
|
186
182
|
base.updateConnection(uuid, (c) => ({
|
|
187
183
|
...c,
|
|
188
184
|
transport,
|
|
@@ -192,7 +188,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
192
188
|
}));
|
|
193
189
|
base.appendLog(uuid, {
|
|
194
190
|
level: 2,
|
|
195
|
-
message: "[✓] Serial connected"
|
|
191
|
+
message: "[✓] Serial connected",
|
|
196
192
|
});
|
|
197
193
|
await conn.onConnect?.();
|
|
198
194
|
await readLoop(uuid, transport);
|
|
@@ -201,11 +197,11 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
201
197
|
base.updateConnection(uuid, (c) => ({
|
|
202
198
|
...c,
|
|
203
199
|
isConnected: false,
|
|
204
|
-
isConnecting: false
|
|
200
|
+
isConnecting: false,
|
|
205
201
|
}));
|
|
206
202
|
base.appendLog(uuid, {
|
|
207
203
|
level: 0,
|
|
208
|
-
message: `[x] Serial connection error: ${err?.message || "Unknown error"}
|
|
204
|
+
message: `[x] Serial connection error: ${err?.message || "Unknown error"}`,
|
|
209
205
|
});
|
|
210
206
|
await disconnect(uuid);
|
|
211
207
|
}
|
|
@@ -235,24 +231,47 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
235
231
|
}));
|
|
236
232
|
await conn?.onDisconnect?.();
|
|
237
233
|
};
|
|
238
|
-
const addConnection = async ({ uuid, propCreator }) => {
|
|
234
|
+
const addConnection = async ({ uuid, propCreator, }) => {
|
|
239
235
|
const port = await navigator.serial.requestPort({
|
|
240
|
-
filters: [{ usbVendorId: 0x303a }]
|
|
236
|
+
filters: [{ usbVendorId: 0x303a }],
|
|
241
237
|
});
|
|
238
|
+
let deviceId = uuid;
|
|
239
|
+
// If no UUID provided, try to read the MAC address before registering the connection
|
|
240
|
+
if (!uuid) {
|
|
241
|
+
// 1. Interrogate the port for the MAC address BEFORE registering the connection
|
|
242
|
+
try {
|
|
243
|
+
const tempTransport = new Transport(port, false, false);
|
|
244
|
+
const tempLoader = new ESPLoader({
|
|
245
|
+
transport: tempTransport,
|
|
246
|
+
baudrate: 115200,
|
|
247
|
+
enableTracing: false,
|
|
248
|
+
});
|
|
249
|
+
await tempLoader.main();
|
|
250
|
+
const mac = await tempLoader.chip.readMac(tempLoader);
|
|
251
|
+
if (mac) {
|
|
252
|
+
deviceId = mac; // Swap 'unnamed_device_0' for the actual MAC
|
|
253
|
+
}
|
|
254
|
+
// Disconnect to release the port so connect() can start fresh
|
|
255
|
+
await tempTransport.disconnect();
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
console.warn("Failed to pre-fetch MAC address. Falling back to default UUID.", e);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// 2. Register the connection using the real MAC address
|
|
242
262
|
const return_uuid = await base.addConnection({
|
|
243
|
-
uuid,
|
|
263
|
+
uuid: deviceId,
|
|
244
264
|
propCreator: (id) => {
|
|
245
265
|
const props = propCreator?.(id);
|
|
246
266
|
return {
|
|
247
267
|
send: (d) => defaultSend(id, d),
|
|
248
268
|
onReceive: (d) => defaultOnReceive(id, d),
|
|
249
269
|
port,
|
|
250
|
-
// Initial connection state
|
|
251
270
|
...base.createInitialConnectionState(id),
|
|
252
|
-
//
|
|
253
|
-
...props
|
|
271
|
+
deviceMac: deviceId, // Lock it in immediately
|
|
272
|
+
...props,
|
|
254
273
|
};
|
|
255
|
-
}
|
|
274
|
+
},
|
|
256
275
|
});
|
|
257
276
|
const conn = base.getConnection(return_uuid);
|
|
258
277
|
if (conn?.autoConnect)
|
|
@@ -264,11 +283,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
264
283
|
await disconnect(uuid);
|
|
265
284
|
}
|
|
266
285
|
catch (e) { }
|
|
267
|
-
;
|
|
268
286
|
base.removeConnection(uuid);
|
|
269
287
|
};
|
|
270
288
|
const reconnectAll = async (...connectionProps) => {
|
|
271
|
-
const connectionIds = base.connections.map(c => c.uuid);
|
|
289
|
+
const connectionIds = base.connections.map((c) => c.uuid);
|
|
272
290
|
await Promise.all(connectionIds.map(async (id) => {
|
|
273
291
|
const c = base.getConnection(id);
|
|
274
292
|
if (!c)
|
|
@@ -284,14 +302,19 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
284
302
|
if (!conn || !conn.port || assetsToFlash.length === 0)
|
|
285
303
|
return;
|
|
286
304
|
await disconnect(uuid);
|
|
287
|
-
base.updateConnection(uuid, (c) => ({
|
|
305
|
+
base.updateConnection(uuid, (c) => ({
|
|
306
|
+
...c,
|
|
307
|
+
isFlashing: true,
|
|
308
|
+
flashProgress: 0,
|
|
309
|
+
flashError: undefined,
|
|
310
|
+
}));
|
|
288
311
|
try {
|
|
289
312
|
// --- Connect ONCE ---
|
|
290
313
|
const transport = new Transport(conn.port, true);
|
|
291
314
|
const esploader = new ESPLoader({
|
|
292
315
|
transport,
|
|
293
316
|
baudrate: 921600,
|
|
294
|
-
enableTracing: false
|
|
317
|
+
enableTracing: false,
|
|
295
318
|
});
|
|
296
319
|
try {
|
|
297
320
|
await esploader.main();
|
|
@@ -300,7 +323,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
300
323
|
console.log("failed to esploader.main()", e);
|
|
301
324
|
return;
|
|
302
325
|
}
|
|
303
|
-
;
|
|
304
326
|
// --- Prepare an ARRAY of files for the library ---
|
|
305
327
|
const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
|
|
306
328
|
const arrayBuffer = await blob.arrayBuffer();
|
|
@@ -320,7 +342,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
320
342
|
// You can enhance progress reporting to show which file is being flashed
|
|
321
343
|
const progress = (written / total) * 100;
|
|
322
344
|
console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
|
|
323
|
-
base.updateConnection(uuid, (c) => ({
|
|
345
|
+
base.updateConnection(uuid, (c) => ({
|
|
346
|
+
...c,
|
|
347
|
+
flashProgress: progress,
|
|
348
|
+
}));
|
|
324
349
|
},
|
|
325
350
|
};
|
|
326
351
|
// --- Call writeFlash ONCE with all files ---
|
|
@@ -331,7 +356,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
331
356
|
catch (e) {
|
|
332
357
|
console.log("failed to esploader.writeFlash", e);
|
|
333
358
|
}
|
|
334
|
-
;
|
|
335
359
|
// --- Disconnect ---
|
|
336
360
|
await esploader.after();
|
|
337
361
|
try {
|
|
@@ -343,7 +367,11 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
343
367
|
await conn.port?.writable?.close();
|
|
344
368
|
await conn.port?.close();
|
|
345
369
|
}
|
|
346
|
-
base.updateConnection(uuid, (c) => ({
|
|
370
|
+
base.updateConnection(uuid, (c) => ({
|
|
371
|
+
...c,
|
|
372
|
+
isFlashing: false,
|
|
373
|
+
flashProgress: 100,
|
|
374
|
+
}));
|
|
347
375
|
}
|
|
348
376
|
catch (e) {
|
|
349
377
|
console.error(`[${uuid}] Flashing failed:`, e);
|
|
@@ -364,6 +392,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
364
392
|
connect,
|
|
365
393
|
disconnect,
|
|
366
394
|
reconnectAll,
|
|
367
|
-
handleFlashFirmware
|
|
395
|
+
handleFlashFirmware,
|
|
368
396
|
};
|
|
369
397
|
}
|
|
@@ -13,8 +13,9 @@ export type MQTTConnectionState = DeviceConnectionState & {
|
|
|
13
13
|
pingFunction?: (props?: any) => void;
|
|
14
14
|
touchHeartbeat?: () => void;
|
|
15
15
|
};
|
|
16
|
-
export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, ...props }: {
|
|
17
|
-
serverUrl
|
|
16
|
+
export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, enableWatchdog, ...props }: {
|
|
17
|
+
serverUrl?: string;
|
|
18
|
+
serverUrlFnc?: () => Promise<string> | string;
|
|
18
19
|
uuidFromMessage: (topic: string, payload: Buffer<ArrayBufferLike>) => string;
|
|
19
20
|
subTopicFromUuid?: (uuid: string) => string;
|
|
20
21
|
pubTopicFromUuid?: (uuid: string) => string;
|
|
@@ -24,13 +25,14 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
|
|
|
24
25
|
password?: string;
|
|
25
26
|
serverAutoConnect?: boolean;
|
|
26
27
|
serverConnectOn?: boolean;
|
|
28
|
+
enableWatchdog?: boolean;
|
|
27
29
|
} & DeviceWhispererProps<AppOrMessageLayer>): {
|
|
28
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string
|
|
30
|
+
addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
29
31
|
removeConnection: (uuid: string) => Promise<void>;
|
|
30
32
|
connect: (uuid: string) => Promise<void>;
|
|
31
33
|
disconnect: (uuid: string) => Promise<void>;
|
|
32
34
|
reconnectAll: () => Promise<void>;
|
|
33
|
-
connectToMQTTServer: () =>
|
|
35
|
+
connectToMQTTServer: () => Promise<void>;
|
|
34
36
|
focussedConnection: string | undefined;
|
|
35
37
|
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
36
38
|
connections: AppOrMessageLayer[];
|
|
@@ -1,10 +1,12 @@
|
|
|
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
12
|
const topicCallbacksRef = useRef(new Map());
|
|
@@ -84,47 +86,71 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
88
|
};
|
|
87
|
-
const connectToMQTTServer = () => {
|
|
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);
|
|
88
96
|
isUnmountedRef.current = false;
|
|
89
97
|
if (clientRef.current) {
|
|
90
98
|
if (clientRef.current.connected) {
|
|
91
99
|
base.setIsReady(true);
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
102
|
+
clientRef.current.removeAllListeners();
|
|
94
103
|
clientRef.current.end(true);
|
|
95
104
|
}
|
|
96
105
|
try {
|
|
97
|
-
const
|
|
106
|
+
const finalUrl = serverUrlFnc ? await serverUrlFnc() : serverUrl;
|
|
107
|
+
if (!finalUrl)
|
|
108
|
+
throw new Error("Generated MQTT URL was undefined.");
|
|
109
|
+
const options = {
|
|
98
110
|
port: serverPort,
|
|
99
111
|
clientId: clientId,
|
|
100
112
|
username,
|
|
101
113
|
password,
|
|
102
114
|
clean: true,
|
|
103
115
|
keepalive: 30,
|
|
104
|
-
reconnectPeriod:
|
|
105
|
-
}
|
|
116
|
+
reconnectPeriod: 0, // We now handle this ourselves
|
|
117
|
+
};
|
|
118
|
+
const new_client = mqtt.connect(finalUrl, options);
|
|
106
119
|
new_client.on("connect", () => {
|
|
107
120
|
console.log("MQTT Whisperer Connected");
|
|
108
121
|
base.setIsReady(true);
|
|
109
|
-
// Re-subscribe
|
|
122
|
+
// Re-subscribe topic callbacks
|
|
110
123
|
topicCallbacksRef.current.forEach((_callbacks, topic) => {
|
|
111
124
|
new_client.subscribe(topic, { qos: 1 }, (err) => {
|
|
112
125
|
if (err)
|
|
113
126
|
console.error("Topic callback subscribe failed:", err);
|
|
114
127
|
});
|
|
115
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
|
+
});
|
|
116
135
|
});
|
|
117
|
-
|
|
118
|
-
console.log("MQTT Whisperer Reconnecting...");
|
|
119
|
-
base.setIsReady(false);
|
|
120
|
-
});
|
|
121
|
-
new_client.on("close", () => {
|
|
122
|
-
console.log("MQTT Whisperer Closed");
|
|
136
|
+
const handleReconnectCycle = () => {
|
|
123
137
|
base.setIsReady(false);
|
|
124
|
-
|
|
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);
|
|
125
151
|
new_client.on("error", (err) => {
|
|
126
|
-
base.setIsReady(false);
|
|
127
152
|
console.error("MQTT Whisperer Error:", err);
|
|
153
|
+
handleReconnectCycle();
|
|
128
154
|
});
|
|
129
155
|
new_client.on("message", (topic, payload) => {
|
|
130
156
|
if (isUnmountedRef.current)
|
|
@@ -155,26 +181,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
155
181
|
catch (err) {
|
|
156
182
|
console.error("MQTT init failed:", err);
|
|
157
183
|
base.setIsReady(false);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const timers = watchdogTimers.current[uuid];
|
|
164
|
-
if (timers) {
|
|
165
|
-
clearTimeout(timers.ping);
|
|
166
|
-
clearTimeout(timers.warn);
|
|
167
|
-
clearTimeout(timers.fail);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
watchdogTimers.current = {};
|
|
171
|
-
topicCallbacksRef.current.clear();
|
|
172
|
-
if (clientRef.current) {
|
|
173
|
-
clientRef.current.removeAllListeners();
|
|
174
|
-
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);
|
|
175
189
|
}
|
|
176
|
-
|
|
177
|
-
};
|
|
190
|
+
}
|
|
178
191
|
};
|
|
179
192
|
const connect = async (uuid) => {
|
|
180
193
|
const conn = base.getConnection(uuid);
|
|
@@ -200,7 +213,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
200
213
|
return;
|
|
201
214
|
const topic = subTopicFromUuid?.(uuid) ?? uuid;
|
|
202
215
|
// Keep topic subscribed if callbacks or other device-connections still depend on it.
|
|
203
|
-
if (hasCallbackForTopic(topic) ||
|
|
216
|
+
if (hasCallbackForTopic(topic) ||
|
|
217
|
+
hasOtherConnectionUsingTopic(topic, uuid)) {
|
|
204
218
|
return;
|
|
205
219
|
}
|
|
206
220
|
if (clientRef.current.connected && !clientRef.current.disconnecting) {
|
|
@@ -213,12 +227,12 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
213
227
|
});
|
|
214
228
|
}
|
|
215
229
|
else {
|
|
216
|
-
console.warn("Skipped
|
|
230
|
+
console.warn("Skipped unsubscribe - client disconnected or unmounted");
|
|
217
231
|
}
|
|
218
232
|
};
|
|
219
233
|
function touchHeartbeat(uuid) {
|
|
220
|
-
if (isUnmountedRef.current)
|
|
221
|
-
return;
|
|
234
|
+
if (isUnmountedRef.current || !enableWatchdog)
|
|
235
|
+
return;
|
|
222
236
|
clearTimeout(watchdogTimers.current[uuid]?.ping);
|
|
223
237
|
clearTimeout(watchdogTimers.current[uuid]?.warn);
|
|
224
238
|
clearTimeout(watchdogTimers.current[uuid]?.fail);
|
|
@@ -226,7 +240,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
226
240
|
base.updateConnection(uuid, (c) => ({
|
|
227
241
|
...c,
|
|
228
242
|
isConnected: true,
|
|
229
|
-
isConnecting: false
|
|
243
|
+
isConnecting: false,
|
|
230
244
|
}));
|
|
231
245
|
const ping = setTimeout(() => {
|
|
232
246
|
if (isUnmountedRef.current)
|
|
@@ -236,12 +250,20 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
236
250
|
const warn = setTimeout(() => {
|
|
237
251
|
if (isUnmountedRef.current)
|
|
238
252
|
return;
|
|
239
|
-
base.updateConnection(uuid, (c) => ({
|
|
253
|
+
base.updateConnection(uuid, (c) => ({
|
|
254
|
+
...c,
|
|
255
|
+
isConnected: false,
|
|
256
|
+
isConnecting: true,
|
|
257
|
+
}));
|
|
240
258
|
}, 30000);
|
|
241
259
|
const fail = setTimeout(() => {
|
|
242
260
|
if (isUnmountedRef.current)
|
|
243
261
|
return;
|
|
244
|
-
base.updateConnection(uuid, (c) => ({
|
|
262
|
+
base.updateConnection(uuid, (c) => ({
|
|
263
|
+
...c,
|
|
264
|
+
isConnected: false,
|
|
265
|
+
isConnecting: false,
|
|
266
|
+
}));
|
|
245
267
|
}, 60000);
|
|
246
268
|
watchdogTimers.current[uuid] = { ping, warn, fail };
|
|
247
269
|
}
|
|
@@ -274,15 +296,16 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
274
296
|
const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
|
|
275
297
|
await publish(topic, data, uuid);
|
|
276
298
|
};
|
|
277
|
-
const addConnection = async ({ uuid, propCreator }) => {
|
|
299
|
+
const addConnection = async ({ uuid, propCreator, }) => {
|
|
278
300
|
if (!clientRef.current || isUnmountedRef.current)
|
|
279
|
-
return;
|
|
301
|
+
return "";
|
|
280
302
|
if (!uuid) {
|
|
281
303
|
Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
|
|
282
|
-
return;
|
|
304
|
+
return "";
|
|
283
305
|
}
|
|
284
|
-
if (base.connections.some(c => c.uuid === uuid) ||
|
|
285
|
-
|
|
306
|
+
if (base.connections.some((c) => c.uuid === uuid) ||
|
|
307
|
+
addingConnections.current.has(uuid)) {
|
|
308
|
+
return "";
|
|
286
309
|
}
|
|
287
310
|
await base.addConnection({
|
|
288
311
|
uuid,
|
|
@@ -297,12 +320,10 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
297
320
|
touchHeartbeat: () => touchHeartbeat(id),
|
|
298
321
|
// Initial connection state
|
|
299
322
|
...base.createInitialConnectionState(id),
|
|
300
|
-
|
|
301
|
-
...props
|
|
323
|
+
...props,
|
|
302
324
|
};
|
|
303
|
-
}
|
|
325
|
+
},
|
|
304
326
|
});
|
|
305
|
-
// Delete this adding connections item
|
|
306
327
|
addingConnections.current.delete(uuid);
|
|
307
328
|
// Connect immediately
|
|
308
329
|
const conn = base.getConnection(uuid);
|
|
@@ -315,7 +336,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
315
336
|
base.removeConnection(uuid);
|
|
316
337
|
};
|
|
317
338
|
const reconnectAll = async () => {
|
|
318
|
-
const connectionIds = base.connections.map(c => c.uuid);
|
|
339
|
+
const connectionIds = base.connections.map((c) => c.uuid);
|
|
319
340
|
await Promise.all(connectionIds.map(async (id) => {
|
|
320
341
|
const c = base.getConnection(id);
|
|
321
342
|
if (!c)
|
|
@@ -328,8 +349,27 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
328
349
|
useEffect(() => {
|
|
329
350
|
if (!(serverAutoConnect || serverConnectOn))
|
|
330
351
|
return;
|
|
331
|
-
|
|
332
|
-
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
|
+
};
|
|
333
373
|
}, [serverUrl, serverConnectOn]);
|
|
334
374
|
return {
|
|
335
375
|
...base,
|
|
@@ -338,6 +378,6 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
338
378
|
connect,
|
|
339
379
|
disconnect,
|
|
340
380
|
reconnectAll,
|
|
341
|
-
connectToMQTTServer
|
|
381
|
+
connectToMQTTServer,
|
|
342
382
|
};
|
|
343
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,7 +39,7 @@ 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>;
|
|
@@ -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,7 +13,7 @@ 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>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
2
|
+
import { MultiDeviceWhisperer, } from "../base/device-whisperer.js";
|
|
3
3
|
export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
|
|
4
4
|
const base = MultiDeviceWhisperer(props);
|
|
5
5
|
const defaultOnReceive = (uuid, data) => {
|
|
@@ -22,7 +22,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
22
22
|
const lines = combined.split(/\r?\n/);
|
|
23
23
|
base.updateConnection(uuid, (c) => ({
|
|
24
24
|
...c,
|
|
25
|
-
readBufferLeftover: lines.pop() || ""
|
|
25
|
+
readBufferLeftover: lines.pop() || "",
|
|
26
26
|
}));
|
|
27
27
|
for (const line of lines) {
|
|
28
28
|
const trimmed = line.trim();
|
|
@@ -38,9 +38,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
38
38
|
const conn = base.getConnection(uuid);
|
|
39
39
|
if (!conn || !conn.ws)
|
|
40
40
|
return;
|
|
41
|
-
const asString = typeof data === "string"
|
|
42
|
-
? data
|
|
43
|
-
: btoa(String.fromCharCode(...data));
|
|
41
|
+
const asString = typeof data === "string" ? data : btoa(String.fromCharCode(...data));
|
|
44
42
|
base.appendLog(uuid, {
|
|
45
43
|
level: 3,
|
|
46
44
|
message: asString,
|
|
@@ -57,7 +55,11 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
57
55
|
console.log(`[client] Closing existing WS before reconnect for ${uuid}`);
|
|
58
56
|
conn.ws.close();
|
|
59
57
|
}
|
|
60
|
-
base.updateConnection(uuid, (c) => ({
|
|
58
|
+
base.updateConnection(uuid, (c) => ({
|
|
59
|
+
...c,
|
|
60
|
+
isConnecting: true,
|
|
61
|
+
autoConnect: true,
|
|
62
|
+
}));
|
|
61
63
|
try {
|
|
62
64
|
const pre = server_port !== 443 ? "ws" : "wss";
|
|
63
65
|
const ws = new WebSocket(`${pre}://${server_url}:${server_port}/ui/${uuid}`);
|
|
@@ -68,11 +70,11 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
68
70
|
ws,
|
|
69
71
|
isConnected: true,
|
|
70
72
|
isConnecting: false,
|
|
71
|
-
lastAttempt: attempt
|
|
73
|
+
lastAttempt: attempt,
|
|
72
74
|
}));
|
|
73
75
|
base.appendLog(uuid, {
|
|
74
76
|
level: 2,
|
|
75
|
-
message: "[✓] WebSocket connected"
|
|
77
|
+
message: "[✓] WebSocket connected",
|
|
76
78
|
});
|
|
77
79
|
conn?.onConnect?.();
|
|
78
80
|
};
|
|
@@ -89,7 +91,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
89
91
|
ws.onerror = (err) => {
|
|
90
92
|
base.appendLog(uuid, {
|
|
91
93
|
level: 0,
|
|
92
|
-
message: `[x] WS error: ${err}
|
|
94
|
+
message: `[x] WS error: ${err}`,
|
|
93
95
|
});
|
|
94
96
|
};
|
|
95
97
|
ws.onclose = async () => {
|
|
@@ -97,7 +99,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
97
99
|
...c,
|
|
98
100
|
isConnected: false,
|
|
99
101
|
isConnecting: false,
|
|
100
|
-
ws: undefined
|
|
102
|
+
ws: undefined,
|
|
101
103
|
}));
|
|
102
104
|
base.appendLog(uuid, { level: 0, message: "[!] WS disconnected" });
|
|
103
105
|
const updated_conn = base.getConnection(uuid);
|
|
@@ -107,7 +109,10 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
107
109
|
}
|
|
108
110
|
// Auto reconnect if enabled
|
|
109
111
|
if (updated_conn.autoConnect && attempt < MAX_RETRIES) {
|
|
110
|
-
base.appendLog(uuid, {
|
|
112
|
+
base.appendLog(uuid, {
|
|
113
|
+
level: 2,
|
|
114
|
+
message: `[~] Reconnecting in ${RETRY_DELAY_MS}ms... (attempt ${attempt + 1})`,
|
|
115
|
+
});
|
|
111
116
|
setTimeout(() => connect(uuid, attempt + 1), RETRY_DELAY_MS);
|
|
112
117
|
}
|
|
113
118
|
};
|
|
@@ -119,8 +124,11 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
119
124
|
isConnecting: false,
|
|
120
125
|
logs: [
|
|
121
126
|
...c.logs,
|
|
122
|
-
{
|
|
123
|
-
|
|
127
|
+
{
|
|
128
|
+
level: 0,
|
|
129
|
+
message: `[x] WS connection error: ${err?.message || "Unknown error"}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
124
132
|
}));
|
|
125
133
|
await disconnect(uuid);
|
|
126
134
|
}
|
|
@@ -135,12 +143,12 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
135
143
|
isConnecting: false,
|
|
136
144
|
autoConnect: false,
|
|
137
145
|
ws: undefined,
|
|
138
|
-
readBufferLeftover: ""
|
|
146
|
+
readBufferLeftover: "",
|
|
139
147
|
}));
|
|
140
148
|
conn.ws.close();
|
|
141
149
|
await conn?.onDisconnect?.();
|
|
142
150
|
};
|
|
143
|
-
const addConnection = async ({ uuid, propCreator }) => {
|
|
151
|
+
const addConnection = async ({ uuid, propCreator, }) => {
|
|
144
152
|
return await base.addConnection({
|
|
145
153
|
uuid,
|
|
146
154
|
propCreator: (id) => {
|
|
@@ -148,9 +156,9 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
148
156
|
return {
|
|
149
157
|
send: props?.send || ((d) => defaultSend(id, d)),
|
|
150
158
|
onReceive: props?.onReceive || ((d) => defaultOnReceive(id, d)),
|
|
151
|
-
...props
|
|
159
|
+
...props,
|
|
152
160
|
};
|
|
153
|
-
}
|
|
161
|
+
},
|
|
154
162
|
});
|
|
155
163
|
};
|
|
156
164
|
const removeConnection = async (uuid) => {
|
|
@@ -174,7 +182,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
174
182
|
}
|
|
175
183
|
};
|
|
176
184
|
const reconnectAll = async (...connectionProps) => {
|
|
177
|
-
const connectionIds = base.connections.map(c => c.uuid);
|
|
185
|
+
const connectionIds = base.connections.map((c) => c.uuid);
|
|
178
186
|
await Promise.all(connectionIds.map(async (id) => {
|
|
179
187
|
const c = base.getConnection(id);
|
|
180
188
|
if (!c)
|
|
@@ -194,6 +202,6 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
194
202
|
connect,
|
|
195
203
|
disconnect,
|
|
196
204
|
checkForNewDevices,
|
|
197
|
-
reconnectAll
|
|
205
|
+
reconnectAll,
|
|
198
206
|
};
|
|
199
207
|
}
|
package/dist/types/builds.js
CHANGED
package/package.json
CHANGED