ota-hub-reactjs 0.0.15 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base/device-whisperer.d.ts +7 -2
- package/dist/base/device-whisperer.js +26 -22
- package/dist/message_layers/protobuf-wrapper.d.ts +2 -1
- package/dist/transport_layers/esp32-device-whisperer.d.ts +6 -3
- package/dist/transport_layers/esp32-device-whisperer.js +26 -40
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +13 -1
- package/dist/transport_layers/mqtt-device-whisperer.js +110 -14
- package/dist/transport_layers/serial-device-whisperer.d.ts +7 -4
- package/dist/transport_layers/serial-device-whisperer.js +22 -5
- package/dist/transport_layers/websocket-device-whisperer.d.ts +3 -2
- package/dist/transport_layers/websocket-device-whisperer.js +9 -7
- package/package.json +1 -2
|
@@ -2,7 +2,7 @@ export type LogMessage = string | number | boolean | Record<string, any> | any[]
|
|
|
2
2
|
export type LogLine = {
|
|
3
3
|
level: number;
|
|
4
4
|
message: LogMessage;
|
|
5
|
-
timestamp?:
|
|
5
|
+
timestamp?: Date;
|
|
6
6
|
};
|
|
7
7
|
type PropCreatorProps<T> = (uuid: string) => Partial<T> | undefined;
|
|
8
8
|
export interface AddConnectionProps<T> {
|
|
@@ -13,6 +13,10 @@ export type DeviceConnectionState = {
|
|
|
13
13
|
uuid: string;
|
|
14
14
|
deviceMac?: string;
|
|
15
15
|
name: string;
|
|
16
|
+
/**
|
|
17
|
+
* Default device send helper.
|
|
18
|
+
* This is a utility function which may better be replaced with transport specific send functions (e.g. MQTT has publish, Serial has write etc.)
|
|
19
|
+
*/
|
|
16
20
|
send: (data: string | Uint8Array) => void | Promise<void>;
|
|
17
21
|
onReceive?: (data: string | Uint8Array) => void;
|
|
18
22
|
onConnect?: () => void | Promise<void>;
|
|
@@ -29,8 +33,9 @@ export type DeviceWhispererProps<T extends DeviceConnectionState> = {
|
|
|
29
33
|
createInitialConnectionState?: (uuid: string) => Partial<T>;
|
|
30
34
|
};
|
|
31
35
|
export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
|
|
36
|
+
focussedConnection: string | undefined;
|
|
37
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
32
38
|
connections: T[];
|
|
33
|
-
connectionsRef: import("react").RefObject<T[]>;
|
|
34
39
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
|
|
35
40
|
removeConnection: (uuid: string) => void;
|
|
36
41
|
connect: (_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
|
-
import { uniqueNamesGenerator, animals } from "unique-names-generator";
|
|
4
3
|
;
|
|
5
4
|
// Initial, generic state for the generic device
|
|
6
5
|
export function createDefaultInitialDeviceState(uuid, props) {
|
|
@@ -20,53 +19,58 @@ export function createDefaultInitialDeviceState(uuid, props) {
|
|
|
20
19
|
*/
|
|
21
20
|
export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
|
|
22
21
|
const [connections, setConnections] = useState([]);
|
|
23
|
-
const connectionsRef = useRef(
|
|
22
|
+
const connectionsRef = useRef(new Map());
|
|
24
23
|
const [isReady, setIsReady] = useState(false);
|
|
25
|
-
const
|
|
24
|
+
const [focussedConnection, setFocussedConnection] = useState();
|
|
25
|
+
// ---------- GET ----------
|
|
26
|
+
const getConnection = (uuid) => connectionsRef.current.get(uuid);
|
|
27
|
+
// ---------- UPDATE ----------
|
|
26
28
|
const updateConnection = (uuid, updater) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const current = connectionsRef.current.get(uuid);
|
|
30
|
+
if (!current)
|
|
31
|
+
return;
|
|
32
|
+
const next = updater(current);
|
|
33
|
+
connectionsRef.current.set(uuid, next);
|
|
34
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
32
35
|
};
|
|
33
36
|
const updateConnectionName = (uuid, name) => {
|
|
34
|
-
|
|
37
|
+
updateConnection(uuid, (c) => ({ ...c, name }));
|
|
35
38
|
};
|
|
36
39
|
const appendLog = (uuid, log) => {
|
|
37
|
-
if (!log.timestamp)
|
|
40
|
+
if (!log.timestamp)
|
|
38
41
|
log.timestamp = new Date();
|
|
39
|
-
}
|
|
40
42
|
updateConnection(uuid, (c) => ({
|
|
41
43
|
...c,
|
|
42
44
|
logs: [...c.logs.slice(-199), log],
|
|
43
45
|
}));
|
|
44
46
|
};
|
|
47
|
+
// ---------- ADD ----------
|
|
45
48
|
const addConnection = async ({ uuid, propCreator }) => {
|
|
46
|
-
uuid = uuid ??
|
|
49
|
+
uuid = uuid ?? `unnamed_device_${connectionsRef.current.size}`;
|
|
47
50
|
const props = propCreator?.(uuid);
|
|
48
51
|
const newConnection = {
|
|
49
52
|
...createDefaultInitialDeviceState(uuid),
|
|
50
53
|
...createInitialConnectionState(uuid),
|
|
51
54
|
...props
|
|
52
55
|
};
|
|
53
|
-
connectionsRef.current
|
|
54
|
-
setConnections(
|
|
55
|
-
const anyUpdatedConnection = getConnection(uuid);
|
|
56
|
-
if (!anyUpdatedConnection) {
|
|
57
|
-
return "";
|
|
58
|
-
}
|
|
56
|
+
connectionsRef.current.set(uuid, newConnection);
|
|
57
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
59
58
|
return uuid;
|
|
60
59
|
};
|
|
60
|
+
// ---------- REMOVE ----------
|
|
61
61
|
const removeConnection = (uuid) => {
|
|
62
|
-
|
|
62
|
+
connectionsRef.current.delete(uuid);
|
|
63
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
63
64
|
};
|
|
65
|
+
// ---------- EFFECT ----------
|
|
64
66
|
useEffect(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// initial snapshot
|
|
68
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
69
|
+
}, []);
|
|
67
70
|
return {
|
|
71
|
+
focussedConnection,
|
|
72
|
+
setFocussedConnection,
|
|
68
73
|
connections,
|
|
69
|
-
connectionsRef,
|
|
70
74
|
addConnection,
|
|
71
75
|
removeConnection,
|
|
72
76
|
connect: (_uuid) => { },
|
|
@@ -19,8 +19,9 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
|
|
|
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;
|
|
22
|
+
focussedConnection: string | undefined;
|
|
23
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
22
24
|
connections: AppLayer[];
|
|
23
|
-
connectionsRef: import("react").RefObject<AppLayer[]>;
|
|
24
25
|
removeConnection: (uuid: string) => void;
|
|
25
26
|
connect: (_uuid: string) => void;
|
|
26
27
|
disconnect: (_uuid: string) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ESPLoader, FlashOptions, Transport } from "esptool-js";
|
|
2
|
-
import { AddConnectionProps, DeviceConnectionState } from "../base/device-whisperer.js";
|
|
2
|
+
import { AddConnectionProps, DeviceConnectionState, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
3
3
|
export type ESP32ConnectionState = DeviceConnectionState & {
|
|
4
4
|
port?: SerialPort;
|
|
5
5
|
baudrate?: number;
|
|
@@ -15,7 +15,9 @@ export type FlashFirmwareProps = {
|
|
|
15
15
|
firmwareBlob?: Blob;
|
|
16
16
|
fileArray?: FlashOptions["fileArray"];
|
|
17
17
|
};
|
|
18
|
-
export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ ...props }?: {
|
|
18
|
+
export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
|
|
19
|
+
releasePortByDefault: boolean;
|
|
20
|
+
} & DeviceWhispererProps<AppOrMessageLayer>): {
|
|
19
21
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
20
22
|
removeConnection: (uuid: string) => Promise<void>;
|
|
21
23
|
connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
|
|
@@ -25,8 +27,9 @@ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP3
|
|
|
25
27
|
blob: Blob;
|
|
26
28
|
address: number;
|
|
27
29
|
}[]) => Promise<void>;
|
|
30
|
+
focussedConnection: string | undefined;
|
|
31
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
28
32
|
connections: AppOrMessageLayer[];
|
|
29
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
30
33
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
31
34
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
32
35
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -1,39 +1,19 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { ESPLoader, Transport } from "esptool-js";
|
|
3
3
|
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
4
|
-
export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
4
|
+
export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
|
|
5
5
|
const base = MultiDeviceWhisperer(props);
|
|
6
6
|
const defaultOnReceive = (uuid, data) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const text = typeof data === "string"
|
|
8
|
+
? data
|
|
9
|
+
: new TextDecoder().decode(data);
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (!trimmed)
|
|
9
12
|
return;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
else if (data instanceof ArrayBuffer) {
|
|
16
|
-
bytes = new Uint8Array(data);
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
bytes = data;
|
|
20
|
-
}
|
|
21
|
-
const asText = decoder.decode(bytes);
|
|
22
|
-
const combined = conn.readBufferLeftover + asText;
|
|
23
|
-
const lines = combined.split("\r\n");
|
|
24
|
-
base.updateConnection(uuid, (c) => ({
|
|
25
|
-
...c,
|
|
26
|
-
readBufferLeftover: lines.pop() || ""
|
|
27
|
-
}));
|
|
28
|
-
for (const line of lines) {
|
|
29
|
-
const trimmed = line.trim();
|
|
30
|
-
if (trimmed) {
|
|
31
|
-
base.appendLog(uuid, {
|
|
32
|
-
level: 2,
|
|
33
|
-
message: trimmed,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
13
|
+
base.appendLog(uuid, {
|
|
14
|
+
level: 2,
|
|
15
|
+
message: trimmed,
|
|
16
|
+
});
|
|
37
17
|
};
|
|
38
18
|
const defaultSend = async (uuid, data) => {
|
|
39
19
|
const conn = base.getConnection(uuid);
|
|
@@ -52,9 +32,7 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
52
32
|
return;
|
|
53
33
|
};
|
|
54
34
|
const readLoop = async (uuid, transport) => {
|
|
55
|
-
const
|
|
56
|
-
if (!conn)
|
|
57
|
-
return;
|
|
35
|
+
const textDecoder = new TextDecoder();
|
|
58
36
|
let readBuffer = ""; // accumulate ASCII/lines
|
|
59
37
|
let slipBuffer = []; // accumulate SLIP frames
|
|
60
38
|
let inSlipFrame = false; // are we inside a SLIP frame?
|
|
@@ -62,6 +40,12 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
62
40
|
try {
|
|
63
41
|
const reader = transport.rawRead();
|
|
64
42
|
while (true) {
|
|
43
|
+
const conn = base.getConnection(uuid);
|
|
44
|
+
if (!conn) {
|
|
45
|
+
console.log("Kack!");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
;
|
|
65
49
|
const { value, done } = await reader.next();
|
|
66
50
|
if (done || !value)
|
|
67
51
|
break;
|
|
@@ -117,9 +101,8 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
117
101
|
escapeNext = false;
|
|
118
102
|
continue;
|
|
119
103
|
}
|
|
120
|
-
// treat as normal
|
|
121
|
-
|
|
122
|
-
readBuffer += char;
|
|
104
|
+
// treat as normal text (correctly)
|
|
105
|
+
readBuffer += textDecoder.decode(new Uint8Array([b]), { stream: true });
|
|
123
106
|
// check for newline
|
|
124
107
|
let newlineIndex;
|
|
125
108
|
while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
|
|
@@ -244,8 +227,8 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
244
227
|
// Always clear the transport and reset connection state
|
|
245
228
|
base.updateConnection(uuid, (c) => ({
|
|
246
229
|
...c,
|
|
247
|
-
port: null,
|
|
248
|
-
transport: null,
|
|
230
|
+
port: releasePortByDefault ? null : c.port,
|
|
231
|
+
transport: releasePortByDefault ? null : c.transport,
|
|
249
232
|
isConnected: false,
|
|
250
233
|
isConnecting: false,
|
|
251
234
|
autoConnect: false,
|
|
@@ -285,8 +268,11 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
285
268
|
base.removeConnection(uuid);
|
|
286
269
|
};
|
|
287
270
|
const reconnectAll = async (...connectionProps) => {
|
|
288
|
-
const
|
|
289
|
-
await Promise.all(
|
|
271
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
272
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
273
|
+
const c = base.getConnection(id);
|
|
274
|
+
if (!c)
|
|
275
|
+
return;
|
|
290
276
|
await disconnect(c.uuid);
|
|
291
277
|
await new Promise((res) => setTimeout(res, 250));
|
|
292
278
|
return connect(c.uuid, ...connectionProps);
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
2
|
+
type MQTTPayload = string | Uint8Array | ArrayBuffer;
|
|
3
|
+
type MQTTTopicCallback = (payload: MQTTPayload, topic: string) => void;
|
|
2
4
|
export type MQTTConnectionState = DeviceConnectionState & {
|
|
5
|
+
/**
|
|
6
|
+
* Publishes a payload to any explicit MQTT topic.
|
|
7
|
+
*/
|
|
8
|
+
publish?: (topic: string, payload: MQTTPayload) => void | Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Subscribes a callback for a specific topic. Returns an unsubscribe function.
|
|
11
|
+
*/
|
|
12
|
+
subscribeCallbackToTopic?: (topic: string, callback: MQTTTopicCallback) => () => void;
|
|
3
13
|
pingFunction?: (props?: any) => void;
|
|
4
14
|
touchHeartbeat?: () => void;
|
|
5
15
|
};
|
|
@@ -21,8 +31,9 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
|
|
|
21
31
|
disconnect: (uuid: string) => Promise<void>;
|
|
22
32
|
reconnectAll: () => Promise<void>;
|
|
23
33
|
connectToMQTTServer: () => (() => void) | undefined;
|
|
34
|
+
focussedConnection: string | undefined;
|
|
35
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
24
36
|
connections: AppOrMessageLayer[];
|
|
25
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
26
37
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
27
38
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
28
39
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -31,3 +42,4 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
|
|
|
31
42
|
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
32
43
|
createInitialConnectionState: (uuid: string) => Partial<AppOrMessageLayer>;
|
|
33
44
|
};
|
|
45
|
+
export {};
|
|
@@ -7,6 +7,83 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
7
7
|
const isUnmountedRef = useRef(false);
|
|
8
8
|
const watchdogTimers = useRef({});
|
|
9
9
|
const addingConnections = useRef(new Set());
|
|
10
|
+
const topicCallbacksRef = useRef(new Map());
|
|
11
|
+
const hasCallbackForTopic = (topic) => {
|
|
12
|
+
const callbacks = topicCallbacksRef.current.get(topic);
|
|
13
|
+
return !!callbacks && callbacks.size > 0;
|
|
14
|
+
};
|
|
15
|
+
const hasOtherConnectionUsingTopic = (topic, excludingUuid) => {
|
|
16
|
+
return base.connections.some((connection) => {
|
|
17
|
+
if (excludingUuid && connection.uuid === excludingUuid)
|
|
18
|
+
return false;
|
|
19
|
+
const connectionTopic = subTopicFromUuid?.(connection.uuid) ?? connection.uuid;
|
|
20
|
+
return connectionTopic === topic;
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const normalizePayload = (payload) => {
|
|
24
|
+
if (typeof payload === "string") {
|
|
25
|
+
return new TextEncoder().encode(payload);
|
|
26
|
+
}
|
|
27
|
+
if (payload instanceof ArrayBuffer) {
|
|
28
|
+
return new Uint8Array(payload);
|
|
29
|
+
}
|
|
30
|
+
return payload;
|
|
31
|
+
};
|
|
32
|
+
const publish = async (topic, payload, uuidForLogs) => {
|
|
33
|
+
const client = clientRef.current;
|
|
34
|
+
if (!client || isUnmountedRef.current)
|
|
35
|
+
return;
|
|
36
|
+
const bytes = normalizePayload(payload);
|
|
37
|
+
if (uuidForLogs) {
|
|
38
|
+
base.appendLog(uuidForLogs, {
|
|
39
|
+
level: 5,
|
|
40
|
+
message: bytes,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
client.publish(topic, bytes, { qos: 1 }, (err) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
reject(err);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const subscribeCallbackToTopic = (topic, callback) => {
|
|
54
|
+
if (!topic)
|
|
55
|
+
return () => { };
|
|
56
|
+
const topicCallbacks = topicCallbacksRef.current.get(topic) ?? new Set();
|
|
57
|
+
const topicAlreadyRegistered = topicCallbacksRef.current.has(topic);
|
|
58
|
+
topicCallbacks.add(callback);
|
|
59
|
+
topicCallbacksRef.current.set(topic, topicCallbacks);
|
|
60
|
+
const client = clientRef.current;
|
|
61
|
+
if (client?.connected && !client.disconnecting && !topicAlreadyRegistered) {
|
|
62
|
+
client.subscribe(topic, { qos: 1 }, (err) => {
|
|
63
|
+
if (err)
|
|
64
|
+
console.error("Topic callback subscribe failed:", err);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
const currentCallbacks = topicCallbacksRef.current.get(topic);
|
|
69
|
+
if (!currentCallbacks)
|
|
70
|
+
return;
|
|
71
|
+
currentCallbacks.delete(callback);
|
|
72
|
+
if (currentCallbacks.size > 0)
|
|
73
|
+
return;
|
|
74
|
+
topicCallbacksRef.current.delete(topic);
|
|
75
|
+
// Keep topic subscribed when at least one managed connection still uses it.
|
|
76
|
+
if (hasOtherConnectionUsingTopic(topic))
|
|
77
|
+
return;
|
|
78
|
+
const latestClient = clientRef.current;
|
|
79
|
+
if (latestClient?.connected && !latestClient.disconnecting) {
|
|
80
|
+
latestClient.unsubscribe(topic, (err) => {
|
|
81
|
+
if (err)
|
|
82
|
+
console.error("Topic callback unsubscribe failed:", err);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
10
87
|
const connectToMQTTServer = () => {
|
|
11
88
|
isUnmountedRef.current = false;
|
|
12
89
|
if (clientRef.current) {
|
|
@@ -29,6 +106,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
29
106
|
new_client.on("connect", () => {
|
|
30
107
|
console.log("MQTT Whisperer Connected");
|
|
31
108
|
base.setIsReady(true);
|
|
109
|
+
// Re-subscribe custom topic callbacks after reconnect.
|
|
110
|
+
topicCallbacksRef.current.forEach((_callbacks, topic) => {
|
|
111
|
+
new_client.subscribe(topic, { qos: 1 }, (err) => {
|
|
112
|
+
if (err)
|
|
113
|
+
console.error("Topic callback subscribe failed:", err);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
32
116
|
});
|
|
33
117
|
new_client.on("reconnect", () => {
|
|
34
118
|
console.log("MQTT Whisperer Reconnecting...");
|
|
@@ -47,6 +131,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
47
131
|
return;
|
|
48
132
|
const uuid = uuidFromMessage(topic, payload);
|
|
49
133
|
const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
|
|
134
|
+
const topicCallbacks = topicCallbacksRef.current.get(topic);
|
|
135
|
+
topicCallbacks?.forEach((cb) => {
|
|
136
|
+
try {
|
|
137
|
+
cb(bytes, topic);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error("Topic callback failed:", err);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
50
143
|
if (!uuid)
|
|
51
144
|
return;
|
|
52
145
|
const conn = base.getConnection(uuid);
|
|
@@ -75,6 +168,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
75
168
|
}
|
|
76
169
|
});
|
|
77
170
|
watchdogTimers.current = {};
|
|
171
|
+
topicCallbacksRef.current.clear();
|
|
78
172
|
if (clientRef.current) {
|
|
79
173
|
clientRef.current.removeAllListeners();
|
|
80
174
|
clientRef.current.end(true);
|
|
@@ -105,6 +199,10 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
105
199
|
if (!clientRef.current || isUnmountedRef.current)
|
|
106
200
|
return;
|
|
107
201
|
const topic = subTopicFromUuid?.(uuid) ?? uuid;
|
|
202
|
+
// Keep topic subscribed if callbacks or other device-connections still depend on it.
|
|
203
|
+
if (hasCallbackForTopic(topic) || hasOtherConnectionUsingTopic(topic, uuid)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
108
206
|
if (clientRef.current.connected && !clientRef.current.disconnecting) {
|
|
109
207
|
clientRef.current.unsubscribe(topic, async (err) => {
|
|
110
208
|
if (err)
|
|
@@ -173,14 +271,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
173
271
|
const conn = base.getConnection(uuid);
|
|
174
272
|
if (!conn)
|
|
175
273
|
return;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
: data;
|
|
179
|
-
base.appendLog(uuid, {
|
|
180
|
-
level: 5,
|
|
181
|
-
message: payload,
|
|
182
|
-
});
|
|
183
|
-
clientRef.current?.publish(pubTopicFromUuid?.(uuid) ?? uuid, payload); // TS is wrong here!
|
|
274
|
+
const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
|
|
275
|
+
await publish(topic, data, uuid);
|
|
184
276
|
};
|
|
185
277
|
const addConnection = async ({ uuid, propCreator }) => {
|
|
186
278
|
if (!clientRef.current || isUnmountedRef.current)
|
|
@@ -189,7 +281,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
189
281
|
Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
|
|
190
282
|
return;
|
|
191
283
|
}
|
|
192
|
-
if (base.
|
|
284
|
+
if (base.connections.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
|
|
193
285
|
return;
|
|
194
286
|
}
|
|
195
287
|
await base.addConnection({
|
|
@@ -199,6 +291,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
199
291
|
return {
|
|
200
292
|
// Defaults, may be overridden by props
|
|
201
293
|
send: (d) => defaultSend(id, d),
|
|
294
|
+
publish: (topic, payload) => publish(topic, payload, id),
|
|
295
|
+
subscribeCallbackToTopic,
|
|
202
296
|
onReceive: (d) => defaultOnReceive(id, d),
|
|
203
297
|
touchHeartbeat: () => touchHeartbeat(id),
|
|
204
298
|
// Initial connection state
|
|
@@ -221,13 +315,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
221
315
|
base.removeConnection(uuid);
|
|
222
316
|
};
|
|
223
317
|
const reconnectAll = async () => {
|
|
224
|
-
|
|
318
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
319
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
320
|
+
const c = base.getConnection(id);
|
|
321
|
+
if (!c)
|
|
322
|
+
return;
|
|
225
323
|
await disconnect(c.uuid);
|
|
226
324
|
await new Promise((res) => setTimeout(res, 250));
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await connect(c.uuid);
|
|
230
|
-
}
|
|
325
|
+
return connect(c.uuid);
|
|
326
|
+
}));
|
|
231
327
|
};
|
|
232
328
|
useEffect(() => {
|
|
233
329
|
if (!(serverAutoConnect || serverConnectOn))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeviceConnectionState, AddConnectionProps } from "../base/device-whisperer.js";
|
|
1
|
+
import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
2
2
|
export declare class UsbTransport {
|
|
3
3
|
device: USBDevice;
|
|
4
4
|
controlInterface: number;
|
|
@@ -38,14 +38,17 @@ export type SerialConnectionState = DeviceConnectionState & {
|
|
|
38
38
|
baudrate?: number;
|
|
39
39
|
slipReadWrite?: boolean;
|
|
40
40
|
};
|
|
41
|
-
export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?:
|
|
41
|
+
export declare function SerialMultiDeviceWhisperer<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>;
|
|
45
45
|
disconnect: (uuid: string) => Promise<void>;
|
|
46
|
-
reconnectAll: () => Promise<void>;
|
|
46
|
+
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
47
|
+
releasePortByDefault: boolean;
|
|
48
|
+
setReleasePortByDefault: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
49
|
+
focussedConnection: string | undefined;
|
|
50
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
47
51
|
connections: AppOrMessageLayer[];
|
|
48
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
49
52
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
50
53
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
51
54
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
2
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
/*
|
|
4
4
|
┌────────────────────────────────┐
|
|
5
5
|
│ Device Whisperer (Generic) │ (connect/disconnect, state mgmt) - "Abstraction Layer"
|
|
@@ -150,6 +150,11 @@ UsbTransport.SET_LINE_CODING = 0x20;
|
|
|
150
150
|
UsbTransport.SET_CONTROL_LINE_STATE = 0x22;
|
|
151
151
|
export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
152
152
|
const base = MultiDeviceWhisperer(props);
|
|
153
|
+
const releasePortByDefaultRef = useRef(false);
|
|
154
|
+
const [releasePortByDefaultState, setReleasePortByDefaultState] = useState(false);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
releasePortByDefaultRef.current = releasePortByDefaultState;
|
|
157
|
+
}, [releasePortByDefaultState]);
|
|
153
158
|
// --- Message Processing (Identical logic, just copied over) ---
|
|
154
159
|
const defaultOnReceive = (uuid, data) => {
|
|
155
160
|
const conn = base.getConnection(uuid);
|
|
@@ -165,7 +170,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
165
170
|
bytes = data;
|
|
166
171
|
const asText = decoder.decode(bytes);
|
|
167
172
|
const combined = conn.readBufferLeftover + asText;
|
|
168
|
-
const lines = combined.split("\
|
|
173
|
+
const lines = combined.split("\n");
|
|
169
174
|
base.updateConnection(uuid, (c) => ({ ...c, readBufferLeftover: lines.pop() || "" }));
|
|
170
175
|
for (const line of lines) {
|
|
171
176
|
if (line.trim()) {
|
|
@@ -306,7 +311,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
306
311
|
}
|
|
307
312
|
base.updateConnection(uuid, c => ({
|
|
308
313
|
...c,
|
|
309
|
-
transport: null,
|
|
314
|
+
transport: releasePortByDefaultRef.current ? null : c.transport,
|
|
310
315
|
isConnected: false,
|
|
311
316
|
isConnecting: false
|
|
312
317
|
}));
|
|
@@ -346,7 +351,17 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
346
351
|
await disconnect(uuid);
|
|
347
352
|
base.removeConnection(uuid);
|
|
348
353
|
};
|
|
349
|
-
const reconnectAll = async () => {
|
|
354
|
+
const reconnectAll = async (...connectionProps) => {
|
|
355
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
356
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
357
|
+
const c = base.getConnection(id);
|
|
358
|
+
if (!c)
|
|
359
|
+
return;
|
|
360
|
+
await disconnect(c.uuid);
|
|
361
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
362
|
+
return connect(c.uuid, ...connectionProps);
|
|
363
|
+
}));
|
|
364
|
+
};
|
|
350
365
|
useEffect(() => { base.setIsReady(true); }, []);
|
|
351
366
|
return {
|
|
352
367
|
...base,
|
|
@@ -354,6 +369,8 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
354
369
|
removeConnection,
|
|
355
370
|
connect,
|
|
356
371
|
disconnect,
|
|
357
|
-
reconnectAll
|
|
372
|
+
reconnectAll,
|
|
373
|
+
releasePortByDefault: releasePortByDefaultState,
|
|
374
|
+
setReleasePortByDefault: setReleasePortByDefaultState
|
|
358
375
|
};
|
|
359
376
|
}
|
|
@@ -18,9 +18,10 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
|
|
|
18
18
|
connect: (uuid: string, attempt?: number) => Promise<void>;
|
|
19
19
|
disconnect: (uuid: string) => Promise<void>;
|
|
20
20
|
checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
|
|
21
|
-
reconnectAll: () => Promise<void>;
|
|
21
|
+
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
22
|
+
focussedConnection: string | undefined;
|
|
23
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
22
24
|
connections: AppOrMessageLayer[];
|
|
23
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
24
25
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
25
26
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
26
27
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -19,7 +19,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
19
19
|
}
|
|
20
20
|
const asText = decoder.decode(bytes);
|
|
21
21
|
const combined = conn.readBufferLeftover + asText;
|
|
22
|
-
const lines = combined.split(
|
|
22
|
+
const lines = combined.split(/\r?\n/);
|
|
23
23
|
base.updateConnection(uuid, (c) => ({
|
|
24
24
|
...c,
|
|
25
25
|
readBufferLeftover: lines.pop() || ""
|
|
@@ -173,14 +173,16 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
173
173
|
return [];
|
|
174
174
|
}
|
|
175
175
|
};
|
|
176
|
-
const reconnectAll = async () => {
|
|
177
|
-
|
|
176
|
+
const reconnectAll = async (...connectionProps) => {
|
|
177
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
178
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
179
|
+
const c = base.getConnection(id);
|
|
180
|
+
if (!c)
|
|
181
|
+
return;
|
|
178
182
|
await disconnect(c.uuid);
|
|
179
183
|
await new Promise((res) => setTimeout(res, 250));
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await connect(c.uuid);
|
|
183
|
-
}
|
|
184
|
+
return connect(c.uuid, ...connectionProps);
|
|
185
|
+
}));
|
|
184
186
|
};
|
|
185
187
|
useEffect(() => {
|
|
186
188
|
base.setIsReady(true); // Ready on page load by default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ota-hub-reactjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"esptool-js": "^0.5.5",
|
|
35
35
|
"mqtt": "^5.14.1",
|
|
36
|
-
"unique-names-generator": "^4.7.1",
|
|
37
36
|
"uuid": "^11.1.0"
|
|
38
37
|
},
|
|
39
38
|
"peerDependencies": {
|