ota-hub-reactjs 0.0.18 → 0.1.1
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 +3 -2
- package/dist/base/device-whisperer.js +8 -6
- package/dist/message_layers/protobuf-wrapper.d.ts +5 -5
- package/dist/message_layers/protobuf-wrapper.js +13 -15
- package/dist/transport_layers/esp32-device-whisperer.d.ts +3 -2
- package/dist/transport_layers/esp32-device-whisperer.js +116 -44
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +6 -4
- package/dist/transport_layers/mqtt-device-whisperer.js +95 -55
- package/dist/transport_layers/serial-device-whisperer.d.ts +4 -4
- package/dist/transport_layers/serial-device-whisperer.js +55 -43
- package/dist/transport_layers/websocket-device-whisperer.d.ts +2 -2
- package/dist/transport_layers/websocket-device-whisperer.js +29 -21
- package/dist/types/builds.js +0 -2
- package/package.json +1 -1
|
@@ -31,12 +31,13 @@ export type DeviceConnectionState = {
|
|
|
31
31
|
export declare function createDefaultInitialDeviceState<T extends DeviceConnectionState>(uuid: string, props?: any): T;
|
|
32
32
|
export type DeviceWhispererProps<T extends DeviceConnectionState> = {
|
|
33
33
|
createInitialConnectionState?: (uuid: string) => Partial<T>;
|
|
34
|
+
setFocussedConnectionOnAddConnection?: boolean;
|
|
34
35
|
};
|
|
35
|
-
export declare function
|
|
36
|
+
export declare function useMultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, setFocussedConnectionOnAddConnection, }?: DeviceWhispererProps<T>): {
|
|
36
37
|
focussedConnection: string | undefined;
|
|
37
38
|
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
38
39
|
connections: T[];
|
|
39
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
|
|
40
|
+
addConnection: ({ uuid, propCreator, }: AddConnectionProps<T>) => Promise<string>;
|
|
40
41
|
removeConnection: (uuid: string) => void;
|
|
41
42
|
connect: (_uuid: string) => void;
|
|
42
43
|
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,13 +10,13 @@ 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.
|
|
18
17
|
Any e.g. Serial without Protobuf, or LoRaWAN etc. devices would be handled via a separate device whisperer
|
|
19
18
|
*/
|
|
20
|
-
export function
|
|
19
|
+
export function useMultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, setFocussedConnectionOnAddConnection, } = {}) {
|
|
21
20
|
const [connections, setConnections] = useState([]);
|
|
22
21
|
const connectionsRef = useRef(new Map());
|
|
23
22
|
const [isReady, setIsReady] = useState(false);
|
|
@@ -45,16 +44,19 @@ 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()));
|
|
57
|
+
// Automatically set to the latest connection
|
|
58
|
+
if (!!setFocussedConnectionOnAddConnection)
|
|
59
|
+
setFocussedConnection(uuid);
|
|
58
60
|
return uuid;
|
|
59
61
|
};
|
|
60
62
|
// ---------- REMOVE ----------
|
|
@@ -82,6 +84,6 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
82
84
|
appendLog,
|
|
83
85
|
isReady,
|
|
84
86
|
setIsReady,
|
|
85
|
-
createInitialConnectionState
|
|
87
|
+
createInitialConnectionState,
|
|
86
88
|
};
|
|
87
89
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { AddConnectionProps, DeviceConnectionState,
|
|
1
|
+
import { AddConnectionProps, DeviceConnectionState, useMultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
2
2
|
export type TopicHandlerContext<AppLayer extends DeviceConnectionState> = {
|
|
3
|
-
base: ReturnType<typeof
|
|
3
|
+
base: ReturnType<typeof useMultiDeviceWhisperer<AppLayer>>;
|
|
4
4
|
uuid: string;
|
|
5
5
|
};
|
|
6
6
|
export type TopicHandlerMap<AppLayer extends DeviceConnectionState, Topic extends string | number, Message = any> = {
|
|
7
7
|
[K in Topic]?: (message: Message, context: TopicHandlerContext<AppLayer>) => void;
|
|
8
8
|
};
|
|
9
9
|
export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState, Topic extends string | number, MessageRX = any, MessageTX = any> = {
|
|
10
|
-
transportLayer: ReturnType<typeof
|
|
10
|
+
transportLayer: ReturnType<typeof useMultiDeviceWhisperer<AppLayer>>;
|
|
11
11
|
encodeRX: (message: MessageTX) => Uint8Array;
|
|
12
12
|
decodeTX: (bytes: Uint8Array) => MessageRX;
|
|
13
13
|
messageTypeField: keyof MessageRX;
|
|
@@ -15,8 +15,8 @@ export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState,
|
|
|
15
15
|
HEADER?: Uint8Array;
|
|
16
16
|
expectLength?: boolean;
|
|
17
17
|
};
|
|
18
|
-
export declare function
|
|
19
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppLayer>) => Promise<string>;
|
|
18
|
+
export declare function useProtobufMultiDeviceWhisperer<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>;
|
|
20
20
|
sendProtobuf: (uuid: string, message: MessageTX) => void;
|
|
21
21
|
protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
|
|
22
22
|
focussedConnection: string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function useProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER = new Uint8Array([]), }) {
|
|
2
2
|
// --- Utils ---
|
|
3
3
|
const concatUint8Arrays = (a, b) => {
|
|
4
4
|
const result = new Uint8Array(a.length + b.length);
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@ export type ESP32ConnectionState = DeviceConnectionState & {
|
|
|
5
5
|
baudrate?: number;
|
|
6
6
|
transport?: Transport;
|
|
7
7
|
esp?: ESPLoader;
|
|
8
|
+
reader?: AsyncGenerator<Uint8Array>;
|
|
8
9
|
slipReadWrite?: boolean;
|
|
9
10
|
isFlashing: boolean;
|
|
10
11
|
flashProgress: number;
|
|
@@ -15,10 +16,10 @@ export type FlashFirmwareProps = {
|
|
|
15
16
|
firmwareBlob?: Blob;
|
|
16
17
|
fileArray?: FlashOptions["fileArray"];
|
|
17
18
|
};
|
|
18
|
-
export declare function
|
|
19
|
+
export declare function useESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
|
|
19
20
|
releasePortByDefault: boolean;
|
|
20
21
|
} & DeviceWhispererProps<AppOrMessageLayer>): {
|
|
21
|
-
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
22
|
+
addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
22
23
|
removeConnection: (uuid: string) => Promise<void>;
|
|
23
24
|
connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
|
|
24
25
|
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 {
|
|
4
|
-
export function
|
|
5
|
-
const base =
|
|
3
|
+
import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
|
|
4
|
+
export function useESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
|
|
5
|
+
const base = useMultiDeviceWhisperer(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;
|
|
@@ -37,15 +35,15 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
37
35
|
let slipBuffer = []; // accumulate SLIP frames
|
|
38
36
|
let inSlipFrame = false; // are we inside a SLIP frame?
|
|
39
37
|
let escapeNext = false;
|
|
38
|
+
const reader = transport.rawRead();
|
|
39
|
+
base.updateConnection(uuid, (c) => ({ ...c, reader }));
|
|
40
40
|
try {
|
|
41
|
-
const reader = transport.rawRead();
|
|
42
41
|
while (true) {
|
|
43
42
|
const conn = base.getConnection(uuid);
|
|
44
43
|
if (!conn) {
|
|
45
44
|
console.log("Kack!");
|
|
46
45
|
return;
|
|
47
46
|
}
|
|
48
|
-
;
|
|
49
47
|
const { value, done } = await reader.next();
|
|
50
48
|
if (done || !value)
|
|
51
49
|
break;
|
|
@@ -54,7 +52,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
54
52
|
const b = bytes[i];
|
|
55
53
|
if (inSlipFrame) {
|
|
56
54
|
// SLIP decoding
|
|
57
|
-
if (b ===
|
|
55
|
+
if (b === 0xc0) {
|
|
58
56
|
if (slipBuffer.length > 0) {
|
|
59
57
|
// complete SLIP frame received
|
|
60
58
|
const payload = new Uint8Array(slipBuffer);
|
|
@@ -75,10 +73,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
75
73
|
escapeNext = false;
|
|
76
74
|
}
|
|
77
75
|
else if (escapeNext) {
|
|
78
|
-
if (b ===
|
|
79
|
-
slipBuffer.push(
|
|
80
|
-
else if (b ===
|
|
81
|
-
slipBuffer.push(
|
|
76
|
+
if (b === 0xdc)
|
|
77
|
+
slipBuffer.push(0xc0);
|
|
78
|
+
else if (b === 0xdd)
|
|
79
|
+
slipBuffer.push(0xdb);
|
|
82
80
|
else {
|
|
83
81
|
// protocol violation: discard frame
|
|
84
82
|
slipBuffer = [];
|
|
@@ -86,7 +84,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
86
84
|
}
|
|
87
85
|
escapeNext = false;
|
|
88
86
|
}
|
|
89
|
-
else if (b ===
|
|
87
|
+
else if (b === 0xdb) {
|
|
90
88
|
escapeNext = true;
|
|
91
89
|
}
|
|
92
90
|
else {
|
|
@@ -94,7 +92,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
94
92
|
}
|
|
95
93
|
continue;
|
|
96
94
|
}
|
|
97
|
-
if (b ===
|
|
95
|
+
if (b === 0xc0 && conn.slipReadWrite) {
|
|
98
96
|
// start of a SLIP frame
|
|
99
97
|
inSlipFrame = true;
|
|
100
98
|
slipBuffer = [];
|
|
@@ -102,7 +100,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
102
100
|
continue;
|
|
103
101
|
}
|
|
104
102
|
// treat as normal text (correctly)
|
|
105
|
-
readBuffer += textDecoder.decode(new Uint8Array([b]), {
|
|
103
|
+
readBuffer += textDecoder.decode(new Uint8Array([b]), {
|
|
104
|
+
stream: true,
|
|
105
|
+
});
|
|
106
106
|
// check for newline
|
|
107
107
|
let newlineIndex;
|
|
108
108
|
while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
|
|
@@ -122,6 +122,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
finally {
|
|
125
|
+
// Clear reader state so it's fresh for next connect
|
|
126
|
+
base.updateConnection(uuid, (c) => ({ ...c, reader: undefined }));
|
|
125
127
|
base.appendLog(uuid, {
|
|
126
128
|
level: 0,
|
|
127
129
|
message: "[!] Serial disconnected",
|
|
@@ -149,11 +151,25 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
149
151
|
let port = conn?.port;
|
|
150
152
|
if (!port) {
|
|
151
153
|
port = await navigator.serial.requestPort({
|
|
152
|
-
filters: [{ usbVendorId: 0x303a }]
|
|
154
|
+
filters: [{ usbVendorId: 0x303a }],
|
|
153
155
|
});
|
|
154
156
|
base.updateConnection(uuid, (c) => ({ ...c, port }));
|
|
155
157
|
}
|
|
156
|
-
|
|
158
|
+
else {
|
|
159
|
+
try {
|
|
160
|
+
const oldInfo = port.getInfo();
|
|
161
|
+
const authorizedPorts = await navigator.serial.getPorts();
|
|
162
|
+
const freshPort = authorizedPorts.find((p) => p.getInfo().usbVendorId === oldInfo.usbVendorId &&
|
|
163
|
+
p.getInfo().usbProductId === oldInfo.usbProductId);
|
|
164
|
+
if (freshPort) {
|
|
165
|
+
port = freshPort; // Swap out the dead pointer
|
|
166
|
+
base.updateConnection(uuid, (c) => ({ ...c, port }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
console.warn(`[${uuid}] Failed to silently heal port reference:`, e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
157
173
|
base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
|
|
158
174
|
const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
|
|
159
175
|
const transport = new Transport(port, false, false);
|
|
@@ -161,7 +177,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
161
177
|
const esploader = new ESPLoader({
|
|
162
178
|
transport,
|
|
163
179
|
baudrate: use_baudrate,
|
|
164
|
-
enableTracing: false
|
|
180
|
+
enableTracing: false,
|
|
165
181
|
});
|
|
166
182
|
try {
|
|
167
183
|
await esploader.main();
|
|
@@ -170,7 +186,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
170
186
|
console.log("failed to esploader.main()", e);
|
|
171
187
|
return;
|
|
172
188
|
}
|
|
173
|
-
;
|
|
174
189
|
try {
|
|
175
190
|
const mac = await esploader.chip.readMac(esploader);
|
|
176
191
|
console.log("Mac from device:", mac);
|
|
@@ -182,36 +197,50 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
182
197
|
if (restart_on_connect) {
|
|
183
198
|
await restartDevice(uuid, transport);
|
|
184
199
|
}
|
|
185
|
-
;
|
|
186
200
|
base.updateConnection(uuid, (c) => ({
|
|
187
201
|
...c,
|
|
188
202
|
transport,
|
|
203
|
+
esp: esploader,
|
|
189
204
|
baudrate: use_baudrate,
|
|
190
205
|
isConnected: true,
|
|
191
206
|
isConnecting: false,
|
|
192
207
|
}));
|
|
193
208
|
base.appendLog(uuid, {
|
|
194
209
|
level: 2,
|
|
195
|
-
message: "[✓] Serial connected"
|
|
210
|
+
message: "[✓] Serial connected",
|
|
196
211
|
});
|
|
197
212
|
await conn.onConnect?.();
|
|
198
|
-
|
|
213
|
+
// FIRE AND FORGET
|
|
214
|
+
// We initiate the read loop but DO NOT await it, returning control immediately.
|
|
215
|
+
readLoop(uuid, transport).catch((e) => console.error(`[${uuid}] Background read loop failed:`, e));
|
|
199
216
|
}
|
|
200
217
|
catch (err) {
|
|
201
218
|
base.updateConnection(uuid, (c) => ({
|
|
202
219
|
...c,
|
|
203
220
|
isConnected: false,
|
|
204
|
-
isConnecting: false
|
|
221
|
+
isConnecting: false,
|
|
205
222
|
}));
|
|
206
223
|
base.appendLog(uuid, {
|
|
207
224
|
level: 0,
|
|
208
|
-
message: `[x] Serial connection error: ${err?.message || "Unknown error"}
|
|
225
|
+
message: `[x] Serial connection error: ${err?.message || "Unknown error"}`,
|
|
209
226
|
});
|
|
210
227
|
await disconnect(uuid);
|
|
211
228
|
}
|
|
212
229
|
};
|
|
213
230
|
const disconnect = async (uuid, timeout = 2000) => {
|
|
214
231
|
const conn = base.getConnection(uuid);
|
|
232
|
+
// 1. POLITELY KILL THE READER
|
|
233
|
+
// If we have an active reader generator in state, call return() to
|
|
234
|
+
// gracefully resolve the pending reader.next() in the background loop
|
|
235
|
+
if (conn?.reader && typeof conn.reader.return === "function") {
|
|
236
|
+
try {
|
|
237
|
+
// @ts-ignore - The underlying AsyncGenerator handles return gracefully
|
|
238
|
+
await conn.reader.return();
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
console.warn(`[${uuid}] Error returning generator lock:`, e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
215
244
|
if (conn?.transport) {
|
|
216
245
|
try {
|
|
217
246
|
// Attempt disconnect, but don’t hang if the port is crashed
|
|
@@ -227,32 +256,56 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
227
256
|
// Always clear the transport and reset connection state
|
|
228
257
|
base.updateConnection(uuid, (c) => ({
|
|
229
258
|
...c,
|
|
230
|
-
port: releasePortByDefault ?
|
|
231
|
-
transport: releasePortByDefault ?
|
|
259
|
+
port: releasePortByDefault ? undefined : c.port,
|
|
260
|
+
transport: releasePortByDefault ? undefined : c.transport,
|
|
261
|
+
reader: undefined, // Guarantee reader is cleared out
|
|
232
262
|
isConnected: false,
|
|
233
263
|
isConnecting: false,
|
|
234
264
|
autoConnect: false,
|
|
235
265
|
}));
|
|
236
266
|
await conn?.onDisconnect?.();
|
|
237
267
|
};
|
|
238
|
-
const addConnection = async ({ uuid, propCreator }) => {
|
|
268
|
+
const addConnection = async ({ uuid, propCreator, }) => {
|
|
239
269
|
const port = await navigator.serial.requestPort({
|
|
240
|
-
filters: [{ usbVendorId: 0x303a }]
|
|
270
|
+
filters: [{ usbVendorId: 0x303a }],
|
|
241
271
|
});
|
|
272
|
+
let deviceId = uuid;
|
|
273
|
+
// If no UUID provided, try to read the MAC address before registering the connection
|
|
274
|
+
if (!uuid) {
|
|
275
|
+
// 1. Interrogate the port for the MAC address BEFORE registering the connection
|
|
276
|
+
try {
|
|
277
|
+
const tempTransport = new Transport(port, false, false);
|
|
278
|
+
const tempLoader = new ESPLoader({
|
|
279
|
+
transport: tempTransport,
|
|
280
|
+
baudrate: 115200,
|
|
281
|
+
enableTracing: false,
|
|
282
|
+
});
|
|
283
|
+
await tempLoader.main();
|
|
284
|
+
const mac = await tempLoader.chip.readMac(tempLoader);
|
|
285
|
+
if (mac) {
|
|
286
|
+
deviceId = mac; // Swap 'unnamed_device_0' for the actual MAC
|
|
287
|
+
}
|
|
288
|
+
// Disconnect to release the port so connect() can start fresh
|
|
289
|
+
await tempTransport.disconnect();
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
console.warn("Failed to pre-fetch MAC address. Falling back to default UUID.", e);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// 2. Register the connection using the real MAC address
|
|
242
296
|
const return_uuid = await base.addConnection({
|
|
243
|
-
uuid,
|
|
297
|
+
uuid: deviceId,
|
|
244
298
|
propCreator: (id) => {
|
|
245
299
|
const props = propCreator?.(id);
|
|
246
300
|
return {
|
|
247
301
|
send: (d) => defaultSend(id, d),
|
|
248
302
|
onReceive: (d) => defaultOnReceive(id, d),
|
|
249
303
|
port,
|
|
250
|
-
// Initial connection state
|
|
251
304
|
...base.createInitialConnectionState(id),
|
|
252
|
-
//
|
|
253
|
-
...props
|
|
305
|
+
deviceMac: deviceId, // Lock it in immediately
|
|
306
|
+
...props,
|
|
254
307
|
};
|
|
255
|
-
}
|
|
308
|
+
},
|
|
256
309
|
});
|
|
257
310
|
const conn = base.getConnection(return_uuid);
|
|
258
311
|
if (conn?.autoConnect)
|
|
@@ -264,11 +317,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
264
317
|
await disconnect(uuid);
|
|
265
318
|
}
|
|
266
319
|
catch (e) { }
|
|
267
|
-
;
|
|
268
320
|
base.removeConnection(uuid);
|
|
269
321
|
};
|
|
270
322
|
const reconnectAll = async (...connectionProps) => {
|
|
271
|
-
const connectionIds = base.connections.map(c => c.uuid);
|
|
323
|
+
const connectionIds = base.connections.map((c) => c.uuid);
|
|
272
324
|
await Promise.all(connectionIds.map(async (id) => {
|
|
273
325
|
const c = base.getConnection(id);
|
|
274
326
|
if (!c)
|
|
@@ -281,17 +333,28 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
281
333
|
// This function now handles an entire device flashing session
|
|
282
334
|
const handleFlashFirmware = async (uuid, assetsToFlash) => {
|
|
283
335
|
const conn = base.getConnection(uuid);
|
|
284
|
-
if
|
|
336
|
+
// 1. Bail if we don't have the required state
|
|
337
|
+
if (!conn ||
|
|
338
|
+
!conn.transport ||
|
|
339
|
+
!conn.port ||
|
|
340
|
+
!conn.esp ||
|
|
341
|
+
assetsToFlash.length === 0)
|
|
285
342
|
return;
|
|
343
|
+
// Gracefully cancel reader and completely drop the transport natively
|
|
286
344
|
await disconnect(uuid);
|
|
287
|
-
base.updateConnection(uuid, (c) => ({
|
|
345
|
+
base.updateConnection(uuid, (c) => ({
|
|
346
|
+
...c,
|
|
347
|
+
isFlashing: true,
|
|
348
|
+
flashProgress: 0,
|
|
349
|
+
flashError: undefined,
|
|
350
|
+
}));
|
|
288
351
|
try {
|
|
289
352
|
// --- Connect ONCE ---
|
|
290
353
|
const transport = new Transport(conn.port, true);
|
|
291
354
|
const esploader = new ESPLoader({
|
|
292
355
|
transport,
|
|
293
356
|
baudrate: 921600,
|
|
294
|
-
enableTracing: false
|
|
357
|
+
enableTracing: false,
|
|
295
358
|
});
|
|
296
359
|
try {
|
|
297
360
|
await esploader.main();
|
|
@@ -300,7 +363,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
300
363
|
console.log("failed to esploader.main()", e);
|
|
301
364
|
return;
|
|
302
365
|
}
|
|
303
|
-
;
|
|
304
366
|
// --- Prepare an ARRAY of files for the library ---
|
|
305
367
|
const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
|
|
306
368
|
const arrayBuffer = await blob.arrayBuffer();
|
|
@@ -320,7 +382,10 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
320
382
|
// You can enhance progress reporting to show which file is being flashed
|
|
321
383
|
const progress = (written / total) * 100;
|
|
322
384
|
console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
|
|
323
|
-
base.updateConnection(uuid, (c) => ({
|
|
385
|
+
base.updateConnection(uuid, (c) => ({
|
|
386
|
+
...c,
|
|
387
|
+
flashProgress: progress,
|
|
388
|
+
}));
|
|
324
389
|
},
|
|
325
390
|
};
|
|
326
391
|
// --- Call writeFlash ONCE with all files ---
|
|
@@ -331,7 +396,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
331
396
|
catch (e) {
|
|
332
397
|
console.log("failed to esploader.writeFlash", e);
|
|
333
398
|
}
|
|
334
|
-
;
|
|
335
399
|
// --- Disconnect ---
|
|
336
400
|
await esploader.after();
|
|
337
401
|
try {
|
|
@@ -343,7 +407,13 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
343
407
|
await conn.port?.writable?.close();
|
|
344
408
|
await conn.port?.close();
|
|
345
409
|
}
|
|
346
|
-
base.updateConnection(uuid, (c) => ({
|
|
410
|
+
base.updateConnection(uuid, (c) => ({
|
|
411
|
+
...c,
|
|
412
|
+
isFlashing: false,
|
|
413
|
+
flashProgress: 100,
|
|
414
|
+
}));
|
|
415
|
+
// --- Reconnect the standard monitor ---
|
|
416
|
+
await connect(uuid);
|
|
347
417
|
}
|
|
348
418
|
catch (e) {
|
|
349
419
|
console.error(`[${uuid}] Flashing failed:`, e);
|
|
@@ -352,6 +422,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
352
422
|
isFlashing: false,
|
|
353
423
|
flashError: e?.message ?? "Unknown error",
|
|
354
424
|
}));
|
|
425
|
+
// Attempt recovery
|
|
426
|
+
await connect(uuid);
|
|
355
427
|
}
|
|
356
428
|
};
|
|
357
429
|
useEffect(() => {
|
|
@@ -364,6 +436,6 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
364
436
|
connect,
|
|
365
437
|
disconnect,
|
|
366
438
|
reconnectAll,
|
|
367
|
-
handleFlashFirmware
|
|
439
|
+
handleFlashFirmware,
|
|
368
440
|
};
|
|
369
441
|
}
|
|
@@ -13,8 +13,9 @@ export type MQTTConnectionState = DeviceConnectionState & {
|
|
|
13
13
|
pingFunction?: (props?: any) => void;
|
|
14
14
|
touchHeartbeat?: () => void;
|
|
15
15
|
};
|
|
16
|
-
export declare function
|
|
17
|
-
serverUrl
|
|
16
|
+
export declare function useMQTTMultiDeviceWhisperer<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[];
|