ota-hub-reactjs 0.0.7 → 0.0.9

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;
@@ -22,10 +22,12 @@ export type DeviceConnectionState = {
22
22
  isConnecting: boolean;
23
23
  logs: LogLine[];
24
24
  readBufferLeftover: string;
25
+ readBufferLeftoverAsBytes: Uint8Array;
25
26
  };
26
27
  export declare function createDefaultInitialDeviceState<T extends DeviceConnectionState>(uuid: string, props?: any): T;
27
28
  export type DeviceWhispererProps<T extends DeviceConnectionState> = {
28
29
  createInitialConnectionState?: (uuid: string) => T;
30
+ connectOn?: boolean;
29
31
  };
30
32
  export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
31
33
  connections: T[];
@@ -37,6 +39,9 @@ export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({
37
39
  updateConnection: (uuid: string, updater: (c: T) => T) => void;
38
40
  reconnectAll: () => void;
39
41
  updateConnectionName: (uuid: string, name: string) => void;
42
+ getConnection: (uuid: string) => T | undefined;
40
43
  appendLog: (uuid: string, log: LogLine) => void;
44
+ isReady: boolean;
45
+ setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
41
46
  };
42
47
  export {};
@@ -11,6 +11,7 @@ export function createDefaultInitialDeviceState(uuid, props) {
11
11
  isConnecting: false,
12
12
  logs: [],
13
13
  readBufferLeftover: "",
14
+ readBufferLeftoverAsBytes: new Uint8Array([]),
14
15
  ...props
15
16
  };
16
17
  }
@@ -20,6 +21,8 @@ export function createDefaultInitialDeviceState(uuid, props) {
20
21
  export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
21
22
  const [connections, setConnections] = useState([]);
22
23
  const connectionsRef = useRef(connections);
24
+ const [isReady, setIsReady] = useState(false);
25
+ const getConnection = (uuid) => connectionsRef.current.find(c => c.uuid === uuid);
23
26
  const updateConnection = (uuid, updater) => {
24
27
  setConnections(prev => {
25
28
  const updated = prev.map((c) => c.uuid === uuid ? updater(c) : c);
@@ -48,7 +51,7 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
48
51
  };
49
52
  connectionsRef.current = [...connectionsRef.current, newConnection];
50
53
  setConnections(prev => [...prev, newConnection]);
51
- const anyUpdatedConnection = connectionsRef.current.find(c => c.uuid === uuid);
54
+ const anyUpdatedConnection = getConnection(uuid);
52
55
  if (!anyUpdatedConnection) {
53
56
  return "";
54
57
  }
@@ -70,6 +73,9 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
70
73
  updateConnection,
71
74
  reconnectAll: () => { },
72
75
  updateConnectionName,
73
- appendLog
76
+ getConnection,
77
+ appendLog,
78
+ isReady,
79
+ setIsReady
74
80
  };
75
81
  }
package/dist/index.d.ts CHANGED
@@ -2,4 +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";
5
6
  export * from "./types/builds.js";
package/dist/index.js CHANGED
@@ -5,5 +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";
8
9
  // Specific types
9
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 { FlashOptions } from "esptool-js";
2
+ import { SerialConnectionState } from "./serial-device-whisperer.js";
3
+ export type ESP32ConnectionState = SerialConnectionState & {
4
+ isFlashing: boolean;
5
+ flashProgress: number;
6
+ flashError: string;
7
+ };
8
+ export type FlashFirmwareProps = {
9
+ uuid: string;
10
+ firmwareBlob?: Blob;
11
+ fileArray?: FlashOptions["fileArray"];
12
+ };
13
+ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ ...props }?: {}): {
14
+ handleFlashFirmware: (uuid: string, assetsToFlash: {
15
+ blob: Blob;
16
+ address: number;
17
+ }[]) => Promise<void>;
18
+ addConnection: ({ uuid, propCreator }: import("../index.js").AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
19
+ removeConnection: (uuid: string) => Promise<void>;
20
+ connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
21
+ disconnect: (uuid: string, timeout?: number) => Promise<void>;
22
+ reconnectAll: (...connectionProps: any) => Promise<void>;
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("../index.js").LogLine) => void;
29
+ isReady: boolean;
30
+ setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
31
+ };
@@ -0,0 +1,89 @@
1
+ import { useEffect } from "react";
2
+ import { ESPLoader, Transport } from "esptool-js";
3
+ import { SerialMultiDeviceWhisperer } from "./serial-device-whisperer.js";
4
+ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
5
+ const base = SerialMultiDeviceWhisperer(props);
6
+ // This function now handles an entire device flashing session
7
+ const handleFlashFirmware = async (uuid, assetsToFlash) => {
8
+ const conn = base.getConnection(uuid);
9
+ if (!conn || !conn.port || assetsToFlash.length === 0)
10
+ return;
11
+ await base.disconnect(uuid);
12
+ base.updateConnection(uuid, (c) => ({ ...c, isFlashing: true, flashProgress: 0, flashError: undefined }));
13
+ try {
14
+ // --- Connect ONCE ---
15
+ const transport = new Transport(conn.port, true);
16
+ const esploader = new ESPLoader({
17
+ transport,
18
+ baudrate: 921600,
19
+ enableTracing: false
20
+ });
21
+ try {
22
+ await esploader.main();
23
+ }
24
+ catch (e) {
25
+ console.log("failed to esploader.main()", e);
26
+ return;
27
+ }
28
+ ;
29
+ // --- Prepare an ARRAY of files for the library ---
30
+ const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
31
+ const arrayBuffer = await blob.arrayBuffer();
32
+ const binaryString = Array.from(new Uint8Array(arrayBuffer))
33
+ .map((b) => String.fromCharCode(b))
34
+ .join("");
35
+ return { data: binaryString, address };
36
+ }));
37
+ const flashOptions = {
38
+ fileArray, // Pass the whole array here
39
+ flashSize: "keep",
40
+ flashMode: "qio",
41
+ flashFreq: "80m",
42
+ eraseAll: fileArray.length > 1, // Writing more than 1 thing, so likely writing partitions.
43
+ compress: true,
44
+ reportProgress: (fileIndex, written, total) => {
45
+ // You can enhance progress reporting to show which file is being flashed
46
+ const progress = (written / total) * 100;
47
+ console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
48
+ base.updateConnection(uuid, (c) => ({ ...c, flashProgress: progress }));
49
+ },
50
+ };
51
+ // --- Call writeFlash ONCE with all files ---
52
+ try {
53
+ base.updateConnection(uuid, (c) => ({ ...c, flashProgress: -1 }));
54
+ await esploader.writeFlash(flashOptions);
55
+ }
56
+ catch (e) {
57
+ console.log("failed to esploader.writeFlash", e);
58
+ }
59
+ ;
60
+ // --- Disconnect ---
61
+ await esploader.after();
62
+ try {
63
+ await transport.disconnect();
64
+ }
65
+ catch (e) {
66
+ console.log("failed to transport.disconnect", e);
67
+ await conn.port?.readable?.cancel();
68
+ await conn.port?.writable?.close();
69
+ await conn.port?.close();
70
+ }
71
+ base.updateConnection(uuid, (c) => ({ ...c, isFlashing: false, flashProgress: 100 }));
72
+ }
73
+ catch (e) {
74
+ console.error(`[${uuid}] Flashing failed:`, e);
75
+ base.updateConnection(uuid, (c) => ({
76
+ ...c,
77
+ isFlashing: false,
78
+ flashError: e?.message ?? "Unknown error",
79
+ }));
80
+ }
81
+ };
82
+ useEffect(() => {
83
+ base.setIsReady(true); // Ready on page load by default
84
+ }, []);
85
+ return {
86
+ ...base,
87
+ handleFlashFirmware,
88
+ };
89
+ }
@@ -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
+ }
@@ -1,31 +1,23 @@
1
- import { FlashOptions, Transport } from "esptool-js";
1
+ import { Transport } from "esptool-js";
2
2
  import { DeviceConnectionState, AddConnectionProps } from "../base/device-whisperer.js";
3
3
  export type SerialConnectionState = DeviceConnectionState & {
4
4
  port?: SerialPort;
5
5
  baudrate?: number;
6
6
  transport?: Transport;
7
- isFlashing: boolean;
8
- flashProgress: number;
9
- flashError: string;
10
- };
11
- export type FlashFirmwareProps = {
12
- uuid: string;
13
- firmwareBlob?: Blob;
14
- fileArray?: FlashOptions["fileArray"];
7
+ slipReadWrite?: boolean;
15
8
  };
16
9
  export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: {}): {
17
10
  addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
18
11
  removeConnection: (uuid: string) => Promise<void>;
19
12
  connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
20
13
  disconnect: (uuid: string, timeout?: number) => Promise<void>;
21
- handleFlashFirmware: (uuid: string, assetsToFlash: {
22
- blob: Blob;
23
- address: number;
24
- }[]) => Promise<void>;
25
14
  reconnectAll: (...connectionProps: any) => Promise<void>;
26
15
  connections: AppOrMessageLayer[];
27
16
  connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
28
17
  updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
29
18
  updateConnectionName: (uuid: string, name: string) => void;
19
+ getConnection: (uuid: string) => AppOrMessageLayer | undefined;
30
20
  appendLog: (uuid: string, log: import("../base/device-whisperer.js").LogLine) => void;
21
+ isReady: boolean;
22
+ setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
31
23
  };
@@ -1,9 +1,10 @@
1
- import { ESPLoader, Transport } from "esptool-js";
1
+ import { 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,42 +36,106 @@ 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
- const asString = typeof data === "string"
42
- ? data
43
- : btoa(String.fromCharCode(...data));
42
+ // Convert to bytes
43
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
44
+ // Log for debugging
44
45
  base.appendLog(uuid, {
45
46
  level: 3,
46
- message: asString,
47
+ message: typeof data === "string" ? data : btoa(String.fromCharCode(...bytes)),
47
48
  });
48
- const bytes = typeof data === "string"
49
- ? new TextEncoder().encode(data)
50
- : data;
51
- console.log("conn.transport?.write:", conn.transport?.write, bytes);
52
- await conn.transport?.write(bytes);
49
+ if (!conn.transport)
50
+ return;
51
+ await conn.transport.write(bytes);
52
+ return;
53
53
  };
54
54
  const readLoop = async (uuid, transport) => {
55
+ const conn = base.getConnection(uuid);
56
+ if (!conn)
57
+ return;
58
+ let readBuffer = ""; // accumulate ASCII/lines
59
+ let slipBuffer = []; // accumulate SLIP frames
60
+ let inSlipFrame = false; // are we inside a SLIP frame?
61
+ let escapeNext = false;
55
62
  try {
63
+ const reader = transport.rawRead();
56
64
  while (true) {
57
- const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
58
- if (!transport?.rawRead || !conn) {
59
- console.log("Transport failed to load!", conn, conn?.transport, transport?.rawRead);
60
- break;
61
- }
62
- ;
63
- const reader = transport?.rawRead();
64
65
  const { value, done } = await reader.next();
65
66
  if (done || !value)
66
67
  break;
67
- conn.onReceive?.(value);
68
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
69
+ for (let i = 0; i < bytes.length; i++) {
70
+ const b = bytes[i];
71
+ if (inSlipFrame) {
72
+ // SLIP decoding
73
+ if (b === 0xC0) {
74
+ if (slipBuffer.length > 0) {
75
+ // complete SLIP frame received
76
+ const payload = new Uint8Array(slipBuffer);
77
+ try {
78
+ conn.onReceive?.(payload);
79
+ }
80
+ catch (e) {
81
+ console.error("Failed to decode SLIP frame", e);
82
+ base.appendLog(uuid, {
83
+ level: 1,
84
+ message: "Failed to decode message",
85
+ timestamp: new Date(),
86
+ });
87
+ }
88
+ slipBuffer = [];
89
+ }
90
+ inSlipFrame = false;
91
+ escapeNext = false;
92
+ }
93
+ else if (escapeNext) {
94
+ if (b === 0xDC)
95
+ slipBuffer.push(0xC0);
96
+ else if (b === 0xDD)
97
+ slipBuffer.push(0xDB);
98
+ else {
99
+ // protocol violation: discard frame
100
+ slipBuffer = [];
101
+ inSlipFrame = false;
102
+ }
103
+ escapeNext = false;
104
+ }
105
+ else if (b === 0xDB) {
106
+ escapeNext = true;
107
+ }
108
+ else {
109
+ slipBuffer.push(b);
110
+ }
111
+ continue;
112
+ }
113
+ if (b === 0xC0 && conn.slipReadWrite) {
114
+ // start of a SLIP frame
115
+ inSlipFrame = true;
116
+ slipBuffer = [];
117
+ escapeNext = false;
118
+ continue;
119
+ }
120
+ // treat as normal ASCII text
121
+ const char = String.fromCharCode(b);
122
+ readBuffer += char;
123
+ // check for newline
124
+ let newlineIndex;
125
+ while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
126
+ const line = readBuffer.slice(0, newlineIndex).replace(/\r$/, "");
127
+ readBuffer = readBuffer.slice(newlineIndex + 1);
128
+ if (line.trim()) {
129
+ conn.onReceive?.(line);
130
+ }
131
+ }
132
+ }
68
133
  }
69
134
  }
70
135
  catch (e) {
71
136
  base.appendLog(uuid, {
72
137
  level: 0,
73
- message: `[!] Read loop error: ${e}`
138
+ message: `[!] Read loop error: ${e}`,
74
139
  });
75
140
  await disconnect(uuid);
76
141
  }
@@ -84,12 +149,12 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
84
149
  }));
85
150
  base.appendLog(uuid, {
86
151
  level: 0,
87
- message: "[!] Serial disconnected"
152
+ message: "[!] Serial disconnected",
88
153
  });
89
154
  }
90
155
  };
91
156
  const restartDevice = async (uuid, default_transport) => {
92
- const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
157
+ const conn = base.getConnection(uuid);
93
158
  const transport = default_transport ?? conn?.transport;
94
159
  if (transport) {
95
160
  await transport.setRTS(false);
@@ -102,7 +167,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
102
167
  }
103
168
  };
104
169
  const connect = async (uuid, baudrate, restart_on_connect = true) => {
105
- const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
170
+ const conn = base.getConnection(uuid);
106
171
  if (!conn?.port)
107
172
  return;
108
173
  if (!conn?.transport) {
@@ -146,7 +211,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
146
211
  }
147
212
  };
148
213
  const disconnect = async (uuid, timeout = 2000) => {
149
- const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
214
+ const conn = base.getConnection(uuid);
150
215
  if (conn?.transport) {
151
216
  try {
152
217
  // Attempt disconnect, but don’t hang if the port is crashed
@@ -194,82 +259,6 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
194
259
  ;
195
260
  base.removeConnection(uuid);
196
261
  };
197
- // This function now handles an entire device flashing session
198
- const handleFlashFirmware = async (uuid, assetsToFlash) => {
199
- const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
200
- if (!conn || !conn.port || assetsToFlash.length === 0)
201
- return;
202
- await disconnect(uuid);
203
- base.updateConnection(uuid, (c) => ({ ...c, isFlashing: true, flashProgress: 0, flashError: undefined }));
204
- try {
205
- // --- Connect ONCE ---
206
- const transport = new Transport(conn.port, true);
207
- const esploader = new ESPLoader({
208
- transport,
209
- baudrate: 921600,
210
- enableTracing: false
211
- });
212
- try {
213
- await esploader.main();
214
- }
215
- catch (e) {
216
- console.log("failed to esploader.main()", e);
217
- return;
218
- }
219
- ;
220
- // --- Prepare an ARRAY of files for the library ---
221
- const fileArray = await Promise.all(assetsToFlash.map(async ({ blob, address }) => {
222
- const arrayBuffer = await blob.arrayBuffer();
223
- const binaryString = Array.from(new Uint8Array(arrayBuffer))
224
- .map((b) => String.fromCharCode(b))
225
- .join("");
226
- return { data: binaryString, address };
227
- }));
228
- const flashOptions = {
229
- fileArray, // Pass the whole array here
230
- flashSize: "keep",
231
- flashMode: "qio",
232
- flashFreq: "80m",
233
- eraseAll: fileArray.length > 1, // Writing more than 1 thing, so likely writing partitions.
234
- compress: true,
235
- reportProgress: (fileIndex, written, total) => {
236
- // You can enhance progress reporting to show which file is being flashed
237
- const progress = (written / total) * 100;
238
- console.log(`Flashing file ${fileIndex + 1}/${fileArray.length}: ${progress.toFixed(1)}%`);
239
- base.updateConnection(uuid, (c) => ({ ...c, flashProgress: progress }));
240
- },
241
- };
242
- // --- Call writeFlash ONCE with all files ---
243
- try {
244
- base.updateConnection(uuid, (c) => ({ ...c, flashProgress: -1 }));
245
- await esploader.writeFlash(flashOptions);
246
- }
247
- catch (e) {
248
- console.log("failed to esploader.writeFlash", e);
249
- }
250
- ;
251
- // --- Disconnect ---
252
- await esploader.after();
253
- try {
254
- await transport.disconnect();
255
- }
256
- catch (e) {
257
- console.log("failed to transport.disconnect", e);
258
- await conn.port?.readable?.cancel();
259
- await conn.port?.writable?.close();
260
- await conn.port?.close();
261
- }
262
- base.updateConnection(uuid, (c) => ({ ...c, isFlashing: false, flashProgress: 100 }));
263
- }
264
- catch (e) {
265
- console.error(`[${uuid}] Flashing failed:`, e);
266
- base.updateConnection(uuid, (c) => ({
267
- ...c,
268
- isFlashing: false,
269
- flashError: e?.message ?? "Unknown error",
270
- }));
271
- }
272
- };
273
262
  const reconnectAll = async (...connectionProps) => {
274
263
  const connections = [...base.connectionsRef.current]; // snapshot first
275
264
  await Promise.all(connections.map(async (c) => {
@@ -278,13 +267,15 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
278
267
  return connect(c.uuid, ...connectionProps);
279
268
  }));
280
269
  };
270
+ useEffect(() => {
271
+ base.setIsReady(true); // Ready on page load by default
272
+ }, []);
281
273
  return {
282
274
  ...base,
283
275
  addConnection,
284
276
  removeConnection,
285
277
  connect,
286
278
  disconnect,
287
- handleFlashFirmware,
288
279
  reconnectAll
289
280
  };
290
281
  }
@@ -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,
package/package.json CHANGED
@@ -1,53 +1,52 @@
1
- {
2
- "name": "ota-hub-reactjs",
3
- "version": "0.0.7",
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.9",
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
+ }