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 CHANGED
@@ -1,14 +1,15 @@
1
1
  # OTA Hub ReactJS
2
2
 
3
- **ReactJS tools for interacting with MCUs such as ESP32 over OTA.**
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
- - Flash firmware to microcontrollers (ESP32 and similar)
8
- - Read from multiple MCUs over serial **in parallel**
9
- - Connect to MCUs via wireless transport stacks such as WebSockets for real-time streaming
10
- - Handle Protobuf and other message layer wrappers
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
- Peer dependencies:
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
- - All transport layers are designed to support multiple connections (thus devices) in parallel
32
- - You can mix and match tranport layers within your applications
33
- - All ConnectionStates are extendible, e.g.
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
- Read and write to multiple MCUs concurrently over serial connections.
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>("ws://192.168.1.100:8080");
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
- Flash multiple MCUs with firmware images using esptool-js under the hood. - Currently only implemented in Serial, more to come!
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
- .map(c => serialDeviceWhisperer.handleFlashFirmware({ uuid: c.uuid, firmwareBlob: blobToFlash! }))
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
- Supports Protobuf and other custom message layers for structured communication.
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
- const protobufSerialDeviceWhisperer = ProtobufMultiDeviceWhisperer<EnhancedSerialConnectionState, TXType, TXFromESP, RXToESP>({
91
- transportLayer: serialDeviceWhisperer,
92
- encodeRX: (msg) => RXToESP.encode(msg).finish(),
93
- decodeTX: (bytes) => TXFromESP.decode(bytes),
94
- messageTypeField: "txType",
95
- rxTopicHandlerMap: logHandlerMap
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
- MIT License © 2025 OTA Hub
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) => boolean;
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 = connectionsRef.current.find(c => c.uuid === uuid);
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
- appendLog
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.connectionsRef.current.find(c => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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: string, server_port: number, { ...props }?: {}): {
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, { ...props } = {}) {
3
+ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...props }) {
4
+ const base = MultiDeviceWhisperer(props);
3
5
  const defaultOnReceive = (uuid, data) => {
4
- const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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.connectionsRef.current.find((c) => c.uuid === uuid);
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
+ }
@@ -0,0 +1,3 @@
1
+ ;
2
+ ;
3
+ export {};
package/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "ota-hub-reactjs",
3
- "version": "0.0.6",
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.0",
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
+ }