ota-hub-reactjs 0.0.6 → 0.0.8
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/README.md +103 -24
- package/dist/base/device-whisperer.d.ts +5 -1
- package/dist/base/device-whisperer.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/message_layers/protobuf-wrapper.d.ts +3 -0
- package/dist/message_layers/protobuf-wrapper.js +1 -1
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +31 -0
- package/dist/transport_layers/mqtt-device-whisperer.js +239 -0
- package/dist/transport_layers/serial-device-whisperer.d.ts +3 -0
- package/dist/transport_layers/serial-device-whisperer.js +11 -7
- package/dist/transport_layers/websocket-device-whisperer.d.ts +7 -1
- package/dist/transport_layers/websocket-device-whisperer.js +11 -7
- package/dist/types/builds.d.ts +19 -0
- package/dist/types/builds.js +3 -0
- package/package.json +53 -53
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# OTA Hub ReactJS
|
|
2
2
|
|
|
3
|
-
**ReactJS tools for interacting with
|
|
3
|
+
**ReactJS tools for interacting with Microprocessors and IoT devices (such as ESP32) over various transports.**
|
|
4
4
|
|
|
5
5
|
`ota-hub-reactjs` provides a set of React-friendly utilities to:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
7
|
+
- Read from multiple MCUs **in parallel** over a range of transports, including:
|
|
8
|
+
- Serial (USB)
|
|
9
|
+
- MQTT (e.g. with AWS IoT Core)
|
|
10
|
+
- Websockets
|
|
11
|
+
- Flash firmware to ESP32 directly in browser
|
|
12
|
+
- Handle Protobuf and other message layer wrappers
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -18,7 +19,7 @@ npm install ota-hub-reactjs
|
|
|
18
19
|
yarn add ota-hub-reactjs
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
This is a react lib, so you will need the peer dependencies:
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
npm install react react-dom
|
|
@@ -28,9 +29,9 @@ npm install react react-dom
|
|
|
28
29
|
|
|
29
30
|
## Transport layers
|
|
30
31
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
32
|
+
- All transport layers are designed to support multiple connections (thus devices) in parallel
|
|
33
|
+
- You can mix and match tranport layers within your applications as separate dedicated `Device Whisperers`
|
|
34
|
+
- All ConnectionStates are extendible, e.g.
|
|
34
35
|
```ts
|
|
35
36
|
export type EnhancedSerialConnectionState = SerialConnectionState & {
|
|
36
37
|
dataPoints: DataPoint[];
|
|
@@ -43,7 +44,8 @@ npm install react react-dom
|
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
### Serial Communication
|
|
46
|
-
|
|
47
|
+
|
|
48
|
+
Read and write to multiple MCUs in parallel over multiple serial connections.
|
|
47
49
|
|
|
48
50
|
```tsx
|
|
49
51
|
import { SerialMultiDeviceWhisperer, SerialConnectionState } from "ota-hub-reactjs";
|
|
@@ -58,45 +60,122 @@ serialDeviceWhisperer.addConnection(); // Web browser will prompt for which Seri
|
|
|
58
60
|
}/>
|
|
59
61
|
```
|
|
60
62
|
|
|
63
|
+
## MQTT AWS IoT Core Streaming
|
|
64
|
+
|
|
65
|
+
Connect to MCUs that expose WebSocket interfaces for live data streaming.<br />
|
|
66
|
+
_Currently this is for a server that allows multiple devices as clients to connect through, rather than one device as a server itself_
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { MQTTMultiDeviceWhisperer } from "ota-hub-reactjs";
|
|
70
|
+
|
|
71
|
+
import { Sha256 } from "@aws-crypto/sha256-js";
|
|
72
|
+
import { SignatureV4 } from "@aws-sdk/signature-v4";
|
|
73
|
+
import { HttpRequest } from "@aws-sdk/protocol-http";
|
|
74
|
+
|
|
75
|
+
// Get your AWS IoT Core Signed URL
|
|
76
|
+
const createAwsIotWssUrl = async () => {
|
|
77
|
+
const endpoint = process.env.IOT_ENDPOINT!; // Your IoT Core Endpoint
|
|
78
|
+
const region = process.env.IOT_REGION!; // Your IoT Core Region
|
|
79
|
+
|
|
80
|
+
const signer = new SignatureV4({
|
|
81
|
+
service: "iotdevicegateway",
|
|
82
|
+
region,
|
|
83
|
+
credentials: {
|
|
84
|
+
accessKeyId: process.env.IOT_ACCESS_KEY_ID!,
|
|
85
|
+
secretAccessKey: process.env.IOT_SECRET_ACCESS_KEY!,
|
|
86
|
+
},
|
|
87
|
+
sha256: Sha256,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const request = new HttpRequest({
|
|
91
|
+
protocol: "wss:",
|
|
92
|
+
hostname: endpoint,
|
|
93
|
+
method: "GET",
|
|
94
|
+
path: "/mqtt",
|
|
95
|
+
headers: { host: endpoint },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const signed = await signer.presign(request, { expiresIn: 60 });
|
|
99
|
+
const qs = new URLSearchParams(signed.query as Record<string, string>).toString();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
url: `${signed.protocol}//${signed.hostname}${signed.path}?${qs}`,
|
|
103
|
+
clientId: `web-${Date.now()}`,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const { url, clientId } = await createAwsIotWssUrl(); // Best done on a backend
|
|
108
|
+
|
|
109
|
+
const whisperer = MQTTMultiDeviceWhisperer<EnhancedMQTTConnectionState>({
|
|
110
|
+
serverUrl: url,
|
|
111
|
+
clientId: clientId,
|
|
112
|
+
serverPort: 443, // Mosquito etc. are often 8883. IoT Core is MQTT over WS, so 443.
|
|
113
|
+
subTopicFromUuid: (uuid) => `${uuid}/sub_topic`,
|
|
114
|
+
pubTopicFromUuid: (uuid) => `${uuid}/pub_topic`,
|
|
115
|
+
uuidFromMessage: (topic, _p) => topic.split("/")[0],
|
|
116
|
+
autoConnect: false,
|
|
117
|
+
connectOn: true, // connect to MQTT boker on .. can also have this state managed, i.e. when a user clicks a button
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Then as normal:
|
|
121
|
+
const uuid = "Thingname on AWS"
|
|
122
|
+
await whisperer.addConnection({ uuid });
|
|
123
|
+
const given_connection = await whisperer.getConnection(uuid)
|
|
124
|
+
given_connection.send("hello there"); // or stringified JSON or bytes. Sends on default pubTopic (from UUID)
|
|
125
|
+
```
|
|
126
|
+
|
|
61
127
|
## WebSocket Streaming
|
|
128
|
+
|
|
62
129
|
Connect to MCUs that expose WebSocket interfaces for live data streaming.<br />
|
|
63
130
|
_Currently this is for a server that allows multiple devices as clients to connect through, rather than one device as a server itself_
|
|
64
131
|
|
|
65
132
|
```tsx
|
|
66
133
|
import { WebsocketMultiDeviceWhisperer } from "ota-hub-reactjs";
|
|
67
134
|
|
|
68
|
-
const websocketDeviceWhisperer = new WebsocketMultiDeviceWhisperer<EnhancedWebsocketConnectionState>(
|
|
135
|
+
const websocketDeviceWhisperer = new WebsocketMultiDeviceWhisperer<EnhancedWebsocketConnectionState>(
|
|
136
|
+
"ws://192.168.1.100:8080"
|
|
137
|
+
);
|
|
69
138
|
|
|
70
139
|
// then as Serial
|
|
71
140
|
```
|
|
72
141
|
|
|
73
142
|
## Flash Firmware
|
|
74
|
-
|
|
143
|
+
|
|
144
|
+
**Currently Firmware flashing is only available on Serial connections.** Flash multiple MCUs with firmware images using esptool-js under the hood.
|
|
75
145
|
|
|
76
146
|
```ts
|
|
77
147
|
await Promise.all(
|
|
78
|
-
serialDeviceWhisperer.connections
|
|
79
|
-
|
|
148
|
+
serialDeviceWhisperer.connections.map((c) =>
|
|
149
|
+
serialDeviceWhisperer.handleFlashFirmware({ uuid: c.uuid, firmwareBlob: blobToFlash! })
|
|
150
|
+
)
|
|
80
151
|
);
|
|
81
152
|
```
|
|
82
153
|
|
|
83
154
|
# Message Layer Wrappers
|
|
84
|
-
|
|
155
|
+
|
|
156
|
+
Supports Protobuf and other custom message layers for structured communication. **Experimental feature, and those with familiarity with Protobuf might want to handle this themselves and call `.send` and `.onReceive` themselves.**
|
|
85
157
|
|
|
86
158
|
```ts
|
|
87
159
|
import { ProtobufMultiDeviceWhisperer } from "ota-hub-reactjs";
|
|
88
160
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
161
|
+
const protobufSerialDeviceWhisperer = ProtobufMultiDeviceWhisperer<
|
|
162
|
+
EnhancedSerialConnectionState,
|
|
163
|
+
TXType,
|
|
164
|
+
TXFromESP,
|
|
165
|
+
RXToESP
|
|
166
|
+
>({
|
|
167
|
+
transportLayer: serialDeviceWhisperer,
|
|
168
|
+
encodeRX: (msg) => RXToESP.encode(msg).finish(),
|
|
169
|
+
decodeTX: (bytes) => TXFromESP.decode(bytes),
|
|
170
|
+
messageTypeField: "txType",
|
|
171
|
+
rxTopicHandlerMap: logHandlerMap,
|
|
96
172
|
});
|
|
97
173
|
```
|
|
174
|
+
|
|
98
175
|
# Contributing
|
|
176
|
+
|
|
99
177
|
Contributions are welcome! Please submit issues or pull requests via the GitHub repository.
|
|
100
178
|
|
|
101
179
|
# License
|
|
102
|
-
|
|
180
|
+
|
|
181
|
+
MIT License © 2026 OTA-Hub
|
|
@@ -14,7 +14,7 @@ export type DeviceConnectionState = {
|
|
|
14
14
|
deviceMac?: string;
|
|
15
15
|
name: string;
|
|
16
16
|
send: (data: string | Uint8Array) => void | Promise<void>;
|
|
17
|
-
onReceive?: (data: string | Uint8Array) =>
|
|
17
|
+
onReceive?: (data: string | Uint8Array) => void;
|
|
18
18
|
onConnect?: () => void | Promise<void>;
|
|
19
19
|
onDisconnect?: () => void | Promise<void>;
|
|
20
20
|
autoConnect: boolean;
|
|
@@ -26,6 +26,7 @@ export type DeviceConnectionState = {
|
|
|
26
26
|
export declare function createDefaultInitialDeviceState<T extends DeviceConnectionState>(uuid: string, props?: any): T;
|
|
27
27
|
export type DeviceWhispererProps<T extends DeviceConnectionState> = {
|
|
28
28
|
createInitialConnectionState?: (uuid: string) => T;
|
|
29
|
+
connectOn?: boolean;
|
|
29
30
|
};
|
|
30
31
|
export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
|
|
31
32
|
connections: T[];
|
|
@@ -37,6 +38,9 @@ export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({
|
|
|
37
38
|
updateConnection: (uuid: string, updater: (c: T) => T) => void;
|
|
38
39
|
reconnectAll: () => void;
|
|
39
40
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
41
|
+
getConnection: (uuid: string) => T | undefined;
|
|
40
42
|
appendLog: (uuid: string, log: LogLine) => void;
|
|
43
|
+
isReady: boolean;
|
|
44
|
+
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
41
45
|
};
|
|
42
46
|
export {};
|
|
@@ -20,6 +20,8 @@ export function createDefaultInitialDeviceState(uuid, props) {
|
|
|
20
20
|
export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
|
|
21
21
|
const [connections, setConnections] = useState([]);
|
|
22
22
|
const connectionsRef = useRef(connections);
|
|
23
|
+
const [isReady, setIsReady] = useState(false);
|
|
24
|
+
const getConnection = (uuid) => connectionsRef.current.find(c => c.uuid === uuid);
|
|
23
25
|
const updateConnection = (uuid, updater) => {
|
|
24
26
|
setConnections(prev => {
|
|
25
27
|
const updated = prev.map((c) => c.uuid === uuid ? updater(c) : c);
|
|
@@ -48,7 +50,7 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
48
50
|
};
|
|
49
51
|
connectionsRef.current = [...connectionsRef.current, newConnection];
|
|
50
52
|
setConnections(prev => [...prev, newConnection]);
|
|
51
|
-
const anyUpdatedConnection =
|
|
53
|
+
const anyUpdatedConnection = getConnection(uuid);
|
|
52
54
|
if (!anyUpdatedConnection) {
|
|
53
55
|
return "";
|
|
54
56
|
}
|
|
@@ -70,6 +72,9 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
70
72
|
updateConnection,
|
|
71
73
|
reconnectAll: () => { },
|
|
72
74
|
updateConnectionName,
|
|
73
|
-
|
|
75
|
+
getConnection,
|
|
76
|
+
appendLog,
|
|
77
|
+
isReady,
|
|
78
|
+
setIsReady
|
|
74
79
|
};
|
|
75
80
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export * from "./base/device-whisperer.js";
|
|
|
2
2
|
export * from "./message_layers/protobuf-wrapper.js";
|
|
3
3
|
export * from "./transport_layers/serial-device-whisperer.js";
|
|
4
4
|
export * from "./transport_layers/websocket-device-whisperer.js";
|
|
5
|
+
export * from "./transport_layers/mqtt-device-whisperer.js";
|
|
6
|
+
export * from "./types/builds.js";
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,6 @@ export * from "./message_layers/protobuf-wrapper.js";
|
|
|
5
5
|
// Transport layers
|
|
6
6
|
export * from "./transport_layers/serial-device-whisperer.js";
|
|
7
7
|
export * from "./transport_layers/websocket-device-whisperer.js";
|
|
8
|
+
export * from "./transport_layers/mqtt-device-whisperer.js";
|
|
9
|
+
// Specific types
|
|
10
|
+
export * from "./types/builds.js";
|
|
@@ -27,5 +27,8 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
|
|
|
27
27
|
updateConnection: (uuid: string, updater: (c: AppLayer) => AppLayer) => void;
|
|
28
28
|
reconnectAll: () => void;
|
|
29
29
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
30
|
+
getConnection: (uuid: string) => AppLayer | undefined;
|
|
30
31
|
appendLog: (uuid: string, log: import("../base/device-whisperer.js").LogLine) => void;
|
|
32
|
+
isReady: boolean;
|
|
33
|
+
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
31
34
|
};
|
|
@@ -48,7 +48,7 @@ export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeT
|
|
|
48
48
|
const sendProtobuf = (uuid, message) => {
|
|
49
49
|
const encoded = encodeRX(message);
|
|
50
50
|
const wrapped = wrapLengthPrefixed(encoded);
|
|
51
|
-
const conn = transportLayer.
|
|
51
|
+
const conn = transportLayer.getConnection(uuid);
|
|
52
52
|
console.log("Sending Protobuf:", message, "bytes: ", [...wrapped]);
|
|
53
53
|
conn?.send?.(wrapped);
|
|
54
54
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
2
|
+
export type MQTTConnectionState = DeviceConnectionState & {
|
|
3
|
+
pingFunction?: (props?: any) => void;
|
|
4
|
+
touchHeartbeat?: () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTConnectionState>({ serverUrl, uuidFromMessage, subTopicFromUuid, pubTopicFromUuid, serverPort, clientId, username, password, autoConnect, ...props }: {
|
|
7
|
+
serverUrl: string;
|
|
8
|
+
uuidFromMessage: (topic: string, payload: Buffer<ArrayBufferLike>) => string;
|
|
9
|
+
subTopicFromUuid?: (uuid: string) => string;
|
|
10
|
+
pubTopicFromUuid?: (uuid: string) => string;
|
|
11
|
+
serverPort?: number;
|
|
12
|
+
clientId?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
password?: string;
|
|
15
|
+
autoConnect?: boolean;
|
|
16
|
+
} & DeviceWhispererProps<AppOrMessageLayer>): {
|
|
17
|
+
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
|
|
18
|
+
removeConnection: (uuid: string) => Promise<void>;
|
|
19
|
+
connect: (uuid: string) => Promise<void>;
|
|
20
|
+
disconnect: (uuid: string) => Promise<void>;
|
|
21
|
+
reconnectAll: () => Promise<void>;
|
|
22
|
+
connectToMQTTServer: () => (() => void) | undefined;
|
|
23
|
+
connections: AppOrMessageLayer[];
|
|
24
|
+
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
25
|
+
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
26
|
+
updateConnectionName: (uuid: string, name: string) => void;
|
|
27
|
+
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
28
|
+
appendLog: (uuid: string, log: import("../base/device-whisperer.js").LogLine) => void;
|
|
29
|
+
isReady: boolean;
|
|
30
|
+
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
3
|
+
import mqtt from "mqtt";
|
|
4
|
+
export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicFromUuid = undefined, pubTopicFromUuid = undefined, serverPort = 8883, clientId = undefined, username = undefined, password = undefined, autoConnect = true, ...props }) {
|
|
5
|
+
const base = MultiDeviceWhisperer(props);
|
|
6
|
+
const clientRef = useRef(null);
|
|
7
|
+
const isUnmountedRef = useRef(false);
|
|
8
|
+
const watchdogTimers = useRef({});
|
|
9
|
+
const addingConnections = new Set();
|
|
10
|
+
const connectToMQTTServer = () => {
|
|
11
|
+
isUnmountedRef.current = false;
|
|
12
|
+
if (clientRef.current) {
|
|
13
|
+
if (clientRef.current.connected) {
|
|
14
|
+
base.setIsReady(true);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
clientRef.current.end(true);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const new_client = mqtt.connect(serverUrl, {
|
|
21
|
+
port: serverPort,
|
|
22
|
+
clientId: clientId,
|
|
23
|
+
username,
|
|
24
|
+
password,
|
|
25
|
+
clean: true,
|
|
26
|
+
keepalive: 30,
|
|
27
|
+
reconnectPeriod: 3000,
|
|
28
|
+
});
|
|
29
|
+
new_client.on("connect", () => {
|
|
30
|
+
console.log("MQTT Whisperer Connected");
|
|
31
|
+
base.setIsReady(true);
|
|
32
|
+
});
|
|
33
|
+
new_client.on("reconnect", () => {
|
|
34
|
+
console.log("MQTT Whisperer Reconnecting...");
|
|
35
|
+
base.setIsReady(false);
|
|
36
|
+
});
|
|
37
|
+
new_client.on("close", () => {
|
|
38
|
+
console.log("MQTT Whisperer Closed");
|
|
39
|
+
base.setIsReady(false);
|
|
40
|
+
});
|
|
41
|
+
new_client.on("error", (err) => {
|
|
42
|
+
base.setIsReady(false);
|
|
43
|
+
console.error("MQTT Whisperer Error:", err);
|
|
44
|
+
});
|
|
45
|
+
new_client.on("message", (topic, payload) => {
|
|
46
|
+
if (isUnmountedRef.current)
|
|
47
|
+
return;
|
|
48
|
+
const uuid = uuidFromMessage(topic, payload);
|
|
49
|
+
const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
|
|
50
|
+
if (!uuid)
|
|
51
|
+
return;
|
|
52
|
+
const conn = base.getConnection(uuid);
|
|
53
|
+
if (!conn) {
|
|
54
|
+
console.warn("Received message for unknown connection:", uuid);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
conn.touchHeartbeat?.();
|
|
58
|
+
conn.onReceive?.(bytes);
|
|
59
|
+
});
|
|
60
|
+
clientRef.current = new_client;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error("MQTT init failed:", err);
|
|
64
|
+
base.setIsReady(false);
|
|
65
|
+
}
|
|
66
|
+
return () => {
|
|
67
|
+
isUnmountedRef.current = true;
|
|
68
|
+
base.setIsReady(false);
|
|
69
|
+
Object.keys(watchdogTimers.current).forEach((uuid) => {
|
|
70
|
+
const timers = watchdogTimers.current[uuid];
|
|
71
|
+
if (timers) {
|
|
72
|
+
clearTimeout(timers.ping);
|
|
73
|
+
clearTimeout(timers.warn);
|
|
74
|
+
clearTimeout(timers.fail);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
watchdogTimers.current = {};
|
|
78
|
+
if (clientRef.current) {
|
|
79
|
+
clientRef.current.removeAllListeners();
|
|
80
|
+
clientRef.current.end(true);
|
|
81
|
+
}
|
|
82
|
+
clientRef.current = null;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const connect = async (uuid) => {
|
|
86
|
+
const conn = base.getConnection(uuid);
|
|
87
|
+
if (!clientRef.current || isUnmountedRef.current || !conn)
|
|
88
|
+
return;
|
|
89
|
+
const topic = subTopicFromUuid?.(uuid) ?? uuid;
|
|
90
|
+
if (clientRef.current.connected && !clientRef.current.disconnecting) {
|
|
91
|
+
clientRef.current.subscribe(topic, { qos: 1 }, async (err) => {
|
|
92
|
+
if (err)
|
|
93
|
+
console.error("Subscribe failed:", err);
|
|
94
|
+
else {
|
|
95
|
+
console.log("MQTT Subscribed:", topic);
|
|
96
|
+
conn.onConnect?.();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.warn("Skipped subscribe - client disconnected or unmounted");
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const disconnect = async (uuid) => {
|
|
105
|
+
if (!clientRef.current || isUnmountedRef.current)
|
|
106
|
+
return;
|
|
107
|
+
const topic = subTopicFromUuid?.(uuid) ?? uuid;
|
|
108
|
+
if (clientRef.current.connected && !clientRef.current.disconnecting) {
|
|
109
|
+
clientRef.current.unsubscribe(topic, async (err) => {
|
|
110
|
+
if (err)
|
|
111
|
+
console.error("Unsubscribe failed:", err);
|
|
112
|
+
else {
|
|
113
|
+
console.log("MQTT Unsubscribed:", topic);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.warn("Skipped subscribe - client disconnected or unmounted");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function touchHeartbeat(uuid) {
|
|
122
|
+
if (isUnmountedRef.current)
|
|
123
|
+
return; // Stop if unmounted
|
|
124
|
+
clearTimeout(watchdogTimers.current[uuid]?.ping);
|
|
125
|
+
clearTimeout(watchdogTimers.current[uuid]?.warn);
|
|
126
|
+
clearTimeout(watchdogTimers.current[uuid]?.fail);
|
|
127
|
+
const currentConn = base.getConnection(uuid);
|
|
128
|
+
base.updateConnection(uuid, (c) => ({
|
|
129
|
+
...c,
|
|
130
|
+
isConnected: true,
|
|
131
|
+
isConnecting: false
|
|
132
|
+
}));
|
|
133
|
+
const ping = setTimeout(() => {
|
|
134
|
+
if (isUnmountedRef.current)
|
|
135
|
+
return;
|
|
136
|
+
currentConn?.pingFunction?.();
|
|
137
|
+
}, 25000);
|
|
138
|
+
const warn = setTimeout(() => {
|
|
139
|
+
if (isUnmountedRef.current)
|
|
140
|
+
return;
|
|
141
|
+
base.updateConnection(uuid, (c) => ({ ...c, isConnected: false, isConnecting: true }));
|
|
142
|
+
}, 30000);
|
|
143
|
+
const fail = setTimeout(() => {
|
|
144
|
+
if (isUnmountedRef.current)
|
|
145
|
+
return;
|
|
146
|
+
base.updateConnection(uuid, (c) => ({ ...c, isConnected: false, isConnecting: false }));
|
|
147
|
+
}, 60000);
|
|
148
|
+
watchdogTimers.current[uuid] = { ping, warn, fail };
|
|
149
|
+
}
|
|
150
|
+
const defaultOnReceive = (uuid, data) => {
|
|
151
|
+
const conn = base.getConnection(uuid);
|
|
152
|
+
if (!conn)
|
|
153
|
+
return;
|
|
154
|
+
conn.touchHeartbeat?.();
|
|
155
|
+
const decoder = new TextDecoder();
|
|
156
|
+
let bytes;
|
|
157
|
+
if (typeof data === "string") {
|
|
158
|
+
bytes = new TextEncoder().encode(data);
|
|
159
|
+
}
|
|
160
|
+
else if (data instanceof ArrayBuffer) {
|
|
161
|
+
bytes = new Uint8Array(data);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
bytes = data;
|
|
165
|
+
}
|
|
166
|
+
const asText = decoder.decode(bytes);
|
|
167
|
+
base.appendLog(uuid, {
|
|
168
|
+
level: 2,
|
|
169
|
+
message: asText,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
const defaultSend = async (uuid, data) => {
|
|
173
|
+
const conn = base.getConnection(uuid);
|
|
174
|
+
if (!conn)
|
|
175
|
+
return;
|
|
176
|
+
const payload = typeof data === 'string'
|
|
177
|
+
? new TextEncoder().encode(data)
|
|
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!
|
|
184
|
+
};
|
|
185
|
+
const addConnection = async ({ uuid, propCreator }) => {
|
|
186
|
+
if (!clientRef.current || isUnmountedRef.current)
|
|
187
|
+
return;
|
|
188
|
+
if (!uuid) {
|
|
189
|
+
Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (base.connectionsRef.current.some(c => c.uuid === uuid) || addingConnections.has(uuid)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await base.addConnection({
|
|
196
|
+
uuid,
|
|
197
|
+
propCreator: (id) => {
|
|
198
|
+
const props = propCreator?.(id);
|
|
199
|
+
return {
|
|
200
|
+
send: props?.send || ((d) => defaultSend(id, d)),
|
|
201
|
+
onReceive: props?.onReceive || ((d) => defaultOnReceive(id, d)),
|
|
202
|
+
touchHeartbeat: props?.touchHeartbeat || (() => touchHeartbeat(id)),
|
|
203
|
+
...props
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// Delete this adding connections item
|
|
208
|
+
addingConnections.delete(uuid);
|
|
209
|
+
// Connect immediately
|
|
210
|
+
connect(uuid);
|
|
211
|
+
return uuid;
|
|
212
|
+
};
|
|
213
|
+
const removeConnection = async (uuid) => {
|
|
214
|
+
await disconnect(uuid);
|
|
215
|
+
base.removeConnection(uuid);
|
|
216
|
+
};
|
|
217
|
+
const reconnectAll = async () => {
|
|
218
|
+
for (const c of base.connectionsRef.current) {
|
|
219
|
+
await disconnect(c.uuid);
|
|
220
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
221
|
+
}
|
|
222
|
+
for (const c of base.connectionsRef.current) {
|
|
223
|
+
await connect(c.uuid);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (autoConnect || props.connectOn)
|
|
228
|
+
connectToMQTTServer();
|
|
229
|
+
}, [serverUrl, props.connectOn]);
|
|
230
|
+
return {
|
|
231
|
+
...base,
|
|
232
|
+
addConnection,
|
|
233
|
+
removeConnection,
|
|
234
|
+
connect,
|
|
235
|
+
disconnect,
|
|
236
|
+
reconnectAll,
|
|
237
|
+
connectToMQTTServer
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -27,5 +27,8 @@ export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends Ser
|
|
|
27
27
|
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
28
28
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
29
29
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
30
|
+
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
30
31
|
appendLog: (uuid: string, log: import("../base/device-whisperer.js").LogLine) => void;
|
|
32
|
+
isReady: boolean;
|
|
33
|
+
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
31
34
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ESPLoader, Transport } from "esptool-js";
|
|
2
2
|
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
3
|
+
import { useEffect } from "react";
|
|
3
4
|
export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
4
5
|
const base = MultiDeviceWhisperer(props);
|
|
5
6
|
const defaultOnReceive = (uuid, data) => {
|
|
6
|
-
const conn = base.
|
|
7
|
+
const conn = base.getConnection(uuid);
|
|
7
8
|
if (!conn)
|
|
8
9
|
return;
|
|
9
10
|
const decoder = new TextDecoder();
|
|
@@ -35,7 +36,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
};
|
|
37
38
|
const defaultSend = async (uuid, data) => {
|
|
38
|
-
const conn = base.
|
|
39
|
+
const conn = base.getConnection(uuid);
|
|
39
40
|
if (!conn)
|
|
40
41
|
return;
|
|
41
42
|
const asString = typeof data === "string"
|
|
@@ -54,7 +55,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
54
55
|
const readLoop = async (uuid, transport) => {
|
|
55
56
|
try {
|
|
56
57
|
while (true) {
|
|
57
|
-
const conn = base.
|
|
58
|
+
const conn = base.getConnection(uuid);
|
|
58
59
|
if (!transport?.rawRead || !conn) {
|
|
59
60
|
console.log("Transport failed to load!", conn, conn?.transport, transport?.rawRead);
|
|
60
61
|
break;
|
|
@@ -89,7 +90,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
89
90
|
}
|
|
90
91
|
};
|
|
91
92
|
const restartDevice = async (uuid, default_transport) => {
|
|
92
|
-
const conn = base.
|
|
93
|
+
const conn = base.getConnection(uuid);
|
|
93
94
|
const transport = default_transport ?? conn?.transport;
|
|
94
95
|
if (transport) {
|
|
95
96
|
await transport.setRTS(false);
|
|
@@ -102,7 +103,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
102
103
|
}
|
|
103
104
|
};
|
|
104
105
|
const connect = async (uuid, baudrate, restart_on_connect = true) => {
|
|
105
|
-
const conn = base.
|
|
106
|
+
const conn = base.getConnection(uuid);
|
|
106
107
|
if (!conn?.port)
|
|
107
108
|
return;
|
|
108
109
|
if (!conn?.transport) {
|
|
@@ -146,7 +147,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
146
147
|
}
|
|
147
148
|
};
|
|
148
149
|
const disconnect = async (uuid, timeout = 2000) => {
|
|
149
|
-
const conn = base.
|
|
150
|
+
const conn = base.getConnection(uuid);
|
|
150
151
|
if (conn?.transport) {
|
|
151
152
|
try {
|
|
152
153
|
// Attempt disconnect, but don’t hang if the port is crashed
|
|
@@ -196,7 +197,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
196
197
|
};
|
|
197
198
|
// This function now handles an entire device flashing session
|
|
198
199
|
const handleFlashFirmware = async (uuid, assetsToFlash) => {
|
|
199
|
-
const conn = base.
|
|
200
|
+
const conn = base.getConnection(uuid);
|
|
200
201
|
if (!conn || !conn.port || assetsToFlash.length === 0)
|
|
201
202
|
return;
|
|
202
203
|
await disconnect(uuid);
|
|
@@ -278,6 +279,9 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
278
279
|
return connect(c.uuid, ...connectionProps);
|
|
279
280
|
}));
|
|
280
281
|
};
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
base.setIsReady(true); // Ready on page load by default
|
|
284
|
+
}, []);
|
|
281
285
|
return {
|
|
282
286
|
...base,
|
|
283
287
|
addConnection,
|
|
@@ -9,7 +9,10 @@ export type DeviceObjectResponse = {
|
|
|
9
9
|
deviceConnectedTime: Date | null;
|
|
10
10
|
deviceLastCommTime: Date | null;
|
|
11
11
|
};
|
|
12
|
-
export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends WebsocketConnectionState>(server_url
|
|
12
|
+
export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends WebsocketConnectionState>({ server_url, server_port, ...props }: {
|
|
13
|
+
server_url: string;
|
|
14
|
+
server_port: number;
|
|
15
|
+
}): {
|
|
13
16
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
14
17
|
removeConnection: (uuid: string) => Promise<void>;
|
|
15
18
|
connect: (uuid: string, attempt?: number) => Promise<void>;
|
|
@@ -20,5 +23,8 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
|
|
|
20
23
|
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
21
24
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
22
25
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
26
|
+
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
23
27
|
appendLog: (uuid: string, log: import("../base/device-whisperer.js").LogLine) => void;
|
|
28
|
+
isReady: boolean;
|
|
29
|
+
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
24
30
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
2
|
-
export function WebsocketMultiDeviceWhisperer(server_url, server_port,
|
|
3
|
+
export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
|
|
4
|
+
const base = MultiDeviceWhisperer(props);
|
|
3
5
|
const defaultOnReceive = (uuid, data) => {
|
|
4
|
-
const conn = base.
|
|
6
|
+
const conn = base.getConnection(uuid);
|
|
5
7
|
if (!conn)
|
|
6
8
|
return;
|
|
7
9
|
const decoder = new TextDecoder();
|
|
@@ -33,7 +35,7 @@ export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...prop
|
|
|
33
35
|
}
|
|
34
36
|
};
|
|
35
37
|
const defaultSend = async (uuid, data) => {
|
|
36
|
-
const conn = base.
|
|
38
|
+
const conn = base.getConnection(uuid);
|
|
37
39
|
if (!conn || !conn.ws)
|
|
38
40
|
return;
|
|
39
41
|
const asString = typeof data === "string"
|
|
@@ -48,7 +50,7 @@ export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...prop
|
|
|
48
50
|
const connect = async (uuid, attempt = 0) => {
|
|
49
51
|
const MAX_RETRIES = 5;
|
|
50
52
|
const RETRY_DELAY_MS = 2000; // 2 seconds
|
|
51
|
-
const conn = base.
|
|
53
|
+
const conn = base.getConnection(uuid);
|
|
52
54
|
if (!conn)
|
|
53
55
|
return;
|
|
54
56
|
if (conn.ws && conn.ws.readyState === WebSocket.OPEN) {
|
|
@@ -98,7 +100,7 @@ export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...prop
|
|
|
98
100
|
ws: undefined
|
|
99
101
|
}));
|
|
100
102
|
base.appendLog(uuid, { level: 0, message: "[!] WS disconnected" });
|
|
101
|
-
const updated_conn = base.
|
|
103
|
+
const updated_conn = base.getConnection(uuid);
|
|
102
104
|
if (!updated_conn) {
|
|
103
105
|
base.appendLog(uuid, { level: 0, message: "[!] Connection lost!" });
|
|
104
106
|
return;
|
|
@@ -124,7 +126,7 @@ export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...prop
|
|
|
124
126
|
}
|
|
125
127
|
};
|
|
126
128
|
const disconnect = async (uuid) => {
|
|
127
|
-
const conn = base.
|
|
129
|
+
const conn = base.getConnection(uuid);
|
|
128
130
|
if (!conn?.ws)
|
|
129
131
|
return;
|
|
130
132
|
base.updateConnection(uuid, (c) => ({
|
|
@@ -138,7 +140,6 @@ export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...prop
|
|
|
138
140
|
conn.ws.close();
|
|
139
141
|
await conn?.onDisconnect?.();
|
|
140
142
|
};
|
|
141
|
-
const base = MultiDeviceWhisperer(props);
|
|
142
143
|
const addConnection = async ({ uuid, propCreator }) => {
|
|
143
144
|
return await base.addConnection({
|
|
144
145
|
uuid,
|
|
@@ -181,6 +182,9 @@ export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...prop
|
|
|
181
182
|
await connect(c.uuid);
|
|
182
183
|
}
|
|
183
184
|
};
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
base.setIsReady(true); // Ready on page load by default
|
|
187
|
+
}, []);
|
|
184
188
|
return {
|
|
185
189
|
...base,
|
|
186
190
|
addConnection,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Asset {
|
|
2
|
+
browser_download_url: string;
|
|
3
|
+
content_type: string;
|
|
4
|
+
created_at: Date;
|
|
5
|
+
digest: string;
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
size: number;
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Build {
|
|
12
|
+
id: number;
|
|
13
|
+
author: string;
|
|
14
|
+
tag_name: string;
|
|
15
|
+
name: string;
|
|
16
|
+
published_at: Date;
|
|
17
|
+
assets_url: string;
|
|
18
|
+
assets: Asset[];
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ota-hub-reactjs",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"module": "dist/index.js",
|
|
8
|
-
"types": "dist/index.d.ts",
|
|
9
|
-
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
],
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/Hard-Stuff/OTA-Hub-reactjs.git"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"esp32",
|
|
18
|
-
"ota",
|
|
19
|
-
"react",
|
|
20
|
-
"iot",
|
|
21
|
-
"firmware"
|
|
22
|
-
],
|
|
23
|
-
"author": "",
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"bugs": {
|
|
26
|
-
"url": "https://github.com/Hard-Stuff/OTA-Hub-reactjs/issues"
|
|
27
|
-
},
|
|
28
|
-
"homepage": "https://github.com/Hard-Stuff/OTA-Hub-reactjs#readme",
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "tsc",
|
|
31
|
-
"prepublishOnly": "npm run build"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"esptool-js": "^0.5.5",
|
|
35
|
-
"mqtt": "^5.14.
|
|
36
|
-
"unique-names-generator": "^4.7.1",
|
|
37
|
-
"uuid": "^11.1.0"
|
|
38
|
-
},
|
|
39
|
-
"peerDependencies": {
|
|
40
|
-
"react": ">=18.0.0",
|
|
41
|
-
"react-dom": ">=18.0.0"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@types/react": "^19.0.10",
|
|
45
|
-
"@types/react-dom": "^19.0.4",
|
|
46
|
-
"react": "^19.1.1",
|
|
47
|
-
"react-dom": "^19.1.1",
|
|
48
|
-
"ts-proto": "^2.7.5",
|
|
49
|
-
"typescript": "^5.3.0",
|
|
50
|
-
"@types/w3c-web-serial": "^1.0.8"
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ota-hub-reactjs",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Hard-Stuff/OTA-Hub-reactjs.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"esp32",
|
|
18
|
+
"ota",
|
|
19
|
+
"react",
|
|
20
|
+
"iot",
|
|
21
|
+
"firmware"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Hard-Stuff/OTA-Hub-reactjs/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/Hard-Stuff/OTA-Hub-reactjs#readme",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"esptool-js": "^0.5.5",
|
|
35
|
+
"mqtt": "^5.14.1",
|
|
36
|
+
"unique-names-generator": "^4.7.1",
|
|
37
|
+
"uuid": "^11.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=18.0.0",
|
|
41
|
+
"react-dom": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "^19.0.10",
|
|
45
|
+
"@types/react-dom": "^19.0.4",
|
|
46
|
+
"react": "^19.1.1",
|
|
47
|
+
"react-dom": "^19.1.1",
|
|
48
|
+
"ts-proto": "^2.7.5",
|
|
49
|
+
"typescript": "^5.3.0",
|
|
50
|
+
"@types/w3c-web-serial": "^1.0.8"
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
}
|