ota-hub-reactjs 0.0.19 → 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 +2 -1
- package/dist/base/device-whisperer.js +4 -1
- package/dist/message_layers/protobuf-wrapper.d.ts +4 -4
- package/dist/message_layers/protobuf-wrapper.js +1 -1
- package/dist/transport_layers/esp32-device-whisperer.d.ts +2 -1
- package/dist/transport_layers/esp32-device-whisperer.js +52 -8
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +1 -1
- package/dist/transport_layers/mqtt-device-whisperer.js +4 -4
- package/dist/transport_layers/serial-device-whisperer.d.ts +1 -1
- package/dist/transport_layers/serial-device-whisperer.js +3 -3
- package/dist/transport_layers/websocket-device-whisperer.d.ts +1 -1
- package/dist/transport_layers/websocket-device-whisperer.js +3 -3
- package/package.json +1 -1
|
@@ -31,8 +31,9 @@ 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[];
|
|
@@ -16,7 +16,7 @@ export function createDefaultInitialDeviceState(uuid, props) {
|
|
|
16
16
|
/* One Device Whisperer is used for all like-devices, such as all Serial with Protobuf.
|
|
17
17
|
Any e.g. Serial without Protobuf, or LoRaWAN etc. devices would be handled via a separate device whisperer
|
|
18
18
|
*/
|
|
19
|
-
export function
|
|
19
|
+
export function useMultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, setFocussedConnectionOnAddConnection, } = {}) {
|
|
20
20
|
const [connections, setConnections] = useState([]);
|
|
21
21
|
const connectionsRef = useRef(new Map());
|
|
22
22
|
const [isReady, setIsReady] = useState(false);
|
|
@@ -54,6 +54,9 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
54
54
|
};
|
|
55
55
|
connectionsRef.current.set(uuid, newConnection);
|
|
56
56
|
setConnections(Array.from(connectionsRef.current.values()));
|
|
57
|
+
// Automatically set to the latest connection
|
|
58
|
+
if (!!setFocussedConnectionOnAddConnection)
|
|
59
|
+
setFocussedConnection(uuid);
|
|
57
60
|
return uuid;
|
|
58
61
|
};
|
|
59
62
|
// ---------- REMOVE ----------
|
|
@@ -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,7 +15,7 @@ export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState,
|
|
|
15
15
|
HEADER?: Uint8Array;
|
|
16
16
|
expectLength?: boolean;
|
|
17
17
|
};
|
|
18
|
-
export declare function
|
|
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
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;
|
|
@@ -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);
|
|
@@ -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,7 +16,7 @@ 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
22
|
addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
@@ -1,8 +1,8 @@
|
|
|
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
7
|
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
8
8
|
const trimmed = text.trim();
|
|
@@ -35,8 +35,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
35
35
|
let slipBuffer = []; // accumulate SLIP frames
|
|
36
36
|
let inSlipFrame = false; // are we inside a SLIP frame?
|
|
37
37
|
let escapeNext = false;
|
|
38
|
+
const reader = transport.rawRead();
|
|
39
|
+
base.updateConnection(uuid, (c) => ({ ...c, reader }));
|
|
38
40
|
try {
|
|
39
|
-
const reader = transport.rawRead();
|
|
40
41
|
while (true) {
|
|
41
42
|
const conn = base.getConnection(uuid);
|
|
42
43
|
if (!conn) {
|
|
@@ -121,6 +122,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
finally {
|
|
125
|
+
// Clear reader state so it's fresh for next connect
|
|
126
|
+
base.updateConnection(uuid, (c) => ({ ...c, reader: undefined }));
|
|
124
127
|
base.appendLog(uuid, {
|
|
125
128
|
level: 0,
|
|
126
129
|
message: "[!] Serial disconnected",
|
|
@@ -152,6 +155,21 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
152
155
|
});
|
|
153
156
|
base.updateConnection(uuid, (c) => ({ ...c, port }));
|
|
154
157
|
}
|
|
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
|
+
}
|
|
155
173
|
base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
|
|
156
174
|
const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
|
|
157
175
|
const transport = new Transport(port, false, false);
|
|
@@ -182,6 +200,7 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
182
200
|
base.updateConnection(uuid, (c) => ({
|
|
183
201
|
...c,
|
|
184
202
|
transport,
|
|
203
|
+
esp: esploader,
|
|
185
204
|
baudrate: use_baudrate,
|
|
186
205
|
isConnected: true,
|
|
187
206
|
isConnecting: false,
|
|
@@ -191,7 +210,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
191
210
|
message: "[✓] Serial connected",
|
|
192
211
|
});
|
|
193
212
|
await conn.onConnect?.();
|
|
194
|
-
|
|
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));
|
|
195
216
|
}
|
|
196
217
|
catch (err) {
|
|
197
218
|
base.updateConnection(uuid, (c) => ({
|
|
@@ -208,6 +229,18 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
208
229
|
};
|
|
209
230
|
const disconnect = async (uuid, timeout = 2000) => {
|
|
210
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
|
+
}
|
|
211
244
|
if (conn?.transport) {
|
|
212
245
|
try {
|
|
213
246
|
// Attempt disconnect, but don’t hang if the port is crashed
|
|
@@ -223,8 +256,9 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
223
256
|
// Always clear the transport and reset connection state
|
|
224
257
|
base.updateConnection(uuid, (c) => ({
|
|
225
258
|
...c,
|
|
226
|
-
port: releasePortByDefault ?
|
|
227
|
-
transport: releasePortByDefault ?
|
|
259
|
+
port: releasePortByDefault ? undefined : c.port,
|
|
260
|
+
transport: releasePortByDefault ? undefined : c.transport,
|
|
261
|
+
reader: undefined, // Guarantee reader is cleared out
|
|
228
262
|
isConnected: false,
|
|
229
263
|
isConnecting: false,
|
|
230
264
|
autoConnect: false,
|
|
@@ -299,8 +333,14 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
299
333
|
// This function now handles an entire device flashing session
|
|
300
334
|
const handleFlashFirmware = async (uuid, assetsToFlash) => {
|
|
301
335
|
const conn = base.getConnection(uuid);
|
|
302
|
-
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)
|
|
303
342
|
return;
|
|
343
|
+
// Gracefully cancel reader and completely drop the transport natively
|
|
304
344
|
await disconnect(uuid);
|
|
305
345
|
base.updateConnection(uuid, (c) => ({
|
|
306
346
|
...c,
|
|
@@ -372,6 +412,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
372
412
|
isFlashing: false,
|
|
373
413
|
flashProgress: 100,
|
|
374
414
|
}));
|
|
415
|
+
// --- Reconnect the standard monitor ---
|
|
416
|
+
await connect(uuid);
|
|
375
417
|
}
|
|
376
418
|
catch (e) {
|
|
377
419
|
console.error(`[${uuid}] Flashing failed:`, e);
|
|
@@ -380,6 +422,8 @@ export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = {
|
|
|
380
422
|
isFlashing: false,
|
|
381
423
|
flashError: e?.message ?? "Unknown error",
|
|
382
424
|
}));
|
|
425
|
+
// Attempt recovery
|
|
426
|
+
await connect(uuid);
|
|
383
427
|
}
|
|
384
428
|
};
|
|
385
429
|
useEffect(() => {
|
|
@@ -13,7 +13,7 @@ export type MQTTConnectionState = DeviceConnectionState & {
|
|
|
13
13
|
pingFunction?: (props?: any) => void;
|
|
14
14
|
touchHeartbeat?: () => void;
|
|
15
15
|
};
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function useMQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, serverAutoConnect, serverConnectOn, enableWatchdog, ...props }: {
|
|
17
17
|
serverUrl?: string;
|
|
18
18
|
serverUrlFnc?: () => Promise<string> | string;
|
|
19
19
|
uuidFromMessage: (topic: string, payload: Buffer<ArrayBufferLike>) => string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
|
|
3
3
|
import mqtt from "mqtt";
|
|
4
|
-
export function
|
|
5
|
-
const base =
|
|
4
|
+
export function useMQTTMultiDeviceWhisperer({ serverUrl, serverUrlFnc, uuidFromMessage, subTopicFromUuid = undefined, pubTopicFromUuid = undefined, serverPort = 443, clientId = undefined, username = undefined, password = undefined, serverAutoConnect = true, serverConnectOn = false, enableWatchdog = true, ...props }) {
|
|
5
|
+
const base = useMultiDeviceWhisperer(props);
|
|
6
6
|
const clientRef = useRef(null);
|
|
7
7
|
const isUnmountedRef = useRef(false);
|
|
8
8
|
// ✨ NEW: Manage our own reconnect timer
|
|
@@ -88,7 +88,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, serverUrlFnc, uuidFromMess
|
|
|
88
88
|
};
|
|
89
89
|
const connectToMQTTServer = async () => {
|
|
90
90
|
if (!serverUrl && !serverUrlFnc) {
|
|
91
|
-
console.error("MQTT
|
|
91
|
+
console.error("MQTT useMultiDeviceWhisperer requires either serverUrl or serverUrlFnc");
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
if (reconnectTimeoutRef.current)
|
|
@@ -38,7 +38,7 @@ export type SerialConnectionState = DeviceConnectionState & {
|
|
|
38
38
|
baudrate?: number;
|
|
39
39
|
slipReadWrite?: boolean;
|
|
40
40
|
};
|
|
41
|
-
export declare function
|
|
41
|
+
export declare function useSerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: DeviceWhispererProps<AppOrMessageLayer>): {
|
|
42
42
|
addConnection: ({ uuid, propCreator, }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
|
|
43
43
|
removeConnection: (uuid: string) => Promise<void>;
|
|
44
44
|
connect: (uuid: string, baudrate?: number) => Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
/*
|
|
4
4
|
┌────────────────────────────────┐
|
|
@@ -151,8 +151,8 @@ export class UsbTransport {
|
|
|
151
151
|
// Standard CDC-ACM requests
|
|
152
152
|
UsbTransport.SET_LINE_CODING = 0x20;
|
|
153
153
|
UsbTransport.SET_CONTROL_LINE_STATE = 0x22;
|
|
154
|
-
export function
|
|
155
|
-
const base =
|
|
154
|
+
export function useSerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
155
|
+
const base = useMultiDeviceWhisperer(props);
|
|
156
156
|
const releasePortByDefaultRef = useRef(false);
|
|
157
157
|
const [releasePortByDefaultState, setReleasePortByDefaultState] = useState(false);
|
|
158
158
|
useEffect(() => {
|
|
@@ -9,7 +9,7 @@ export type DeviceObjectResponse = {
|
|
|
9
9
|
deviceConnectedTime: Date | null;
|
|
10
10
|
deviceLastCommTime: Date | null;
|
|
11
11
|
};
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function useWebsocketMultiDeviceWhisperer<AppOrMessageLayer extends WebsocketConnectionState>({ server_url, server_port, ...props }: {
|
|
13
13
|
server_url: string;
|
|
14
14
|
server_port: number;
|
|
15
15
|
}): {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
import {
|
|
3
|
-
export function
|
|
4
|
-
const base =
|
|
2
|
+
import { useMultiDeviceWhisperer, } from "../base/device-whisperer.js";
|
|
3
|
+
export function useWebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
|
|
4
|
+
const base = useMultiDeviceWhisperer(props);
|
|
5
5
|
const defaultOnReceive = (uuid, data) => {
|
|
6
6
|
const conn = base.getConnection(uuid);
|
|
7
7
|
if (!conn)
|
package/package.json
CHANGED