ota-hub-reactjs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/base/device-whisperer.d.ts +42 -0
- package/dist/base/device-whisperer.js +75 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/message_layers/protobuf-wrapper.d.ts +31 -0
- package/dist/message_layers/protobuf-wrapper.js +142 -0
- package/dist/transport_layers/serial-device-whisperer.d.ts +31 -0
- package/dist/transport_layers/serial-device-whisperer.js +290 -0
- package/dist/transport_layers/websocket-device-whisperer.d.ts +24 -0
- package/dist/transport_layers/websocket-device-whisperer.js +193 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 [OTA Hub and the Hard Stuff team]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
Binary file
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type LogMessage = string | number | boolean | Record<string, any> | any[];
|
|
2
|
+
export type LogLine = {
|
|
3
|
+
level: number;
|
|
4
|
+
message: LogMessage;
|
|
5
|
+
timestamp?: string | Date;
|
|
6
|
+
};
|
|
7
|
+
type PropCreatorProps<T> = (uuid: string) => Partial<T> | undefined;
|
|
8
|
+
export interface AddConnectionProps<T> {
|
|
9
|
+
uuid?: string;
|
|
10
|
+
propCreator?: PropCreatorProps<T>;
|
|
11
|
+
}
|
|
12
|
+
export type DeviceConnectionState = {
|
|
13
|
+
uuid: string;
|
|
14
|
+
deviceMac?: string;
|
|
15
|
+
name: string;
|
|
16
|
+
send: (data: string | Uint8Array) => void | Promise<void>;
|
|
17
|
+
onReceive?: (data: string | Uint8Array) => boolean;
|
|
18
|
+
onConnect?: () => void | Promise<void>;
|
|
19
|
+
onDisconnect?: () => void | Promise<void>;
|
|
20
|
+
autoConnect: boolean;
|
|
21
|
+
isConnected: boolean;
|
|
22
|
+
isConnecting: boolean;
|
|
23
|
+
logs: LogLine[];
|
|
24
|
+
readBufferLeftover: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function createDefaultInitialDeviceState<T extends DeviceConnectionState>(uuid: string, props?: any): T;
|
|
27
|
+
export type DeviceWhispererProps<T extends DeviceConnectionState> = {
|
|
28
|
+
createInitialConnectionState?: (uuid: string) => T;
|
|
29
|
+
};
|
|
30
|
+
export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
|
|
31
|
+
connections: T[];
|
|
32
|
+
connectionsRef: import("react").RefObject<T[]>;
|
|
33
|
+
addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
|
|
34
|
+
removeConnection: (uuid: string) => void;
|
|
35
|
+
connect: (_uuid: string) => void;
|
|
36
|
+
disconnect: (_uuid: string) => void;
|
|
37
|
+
updateConnection: (uuid: string, updater: (c: T) => T) => void;
|
|
38
|
+
reconnectAll: () => void;
|
|
39
|
+
updateConnectionName: (uuid: string, name: string) => void;
|
|
40
|
+
appendLog: (uuid: string, log: LogLine) => void;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { uniqueNamesGenerator, animals } from "unique-names-generator";
|
|
4
|
+
;
|
|
5
|
+
// Initial, generic state for the generic device
|
|
6
|
+
export function createDefaultInitialDeviceState(uuid, props) {
|
|
7
|
+
return {
|
|
8
|
+
uuid,
|
|
9
|
+
autoConnect: true,
|
|
10
|
+
isConnected: false,
|
|
11
|
+
isConnecting: false,
|
|
12
|
+
logs: [],
|
|
13
|
+
readBufferLeftover: "",
|
|
14
|
+
...props
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/* One Device Whisperer is used for all like-devices, such as all Serial with Protobuf.
|
|
18
|
+
Any e.g. Serial without Protobuf, or LoRaWAN etc. devices would be handled via a separate device whisperer
|
|
19
|
+
*/
|
|
20
|
+
export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
|
|
21
|
+
const [connections, setConnections] = useState([]);
|
|
22
|
+
const connectionsRef = useRef(connections);
|
|
23
|
+
const updateConnection = (uuid, updater) => {
|
|
24
|
+
setConnections(prev => {
|
|
25
|
+
const updated = prev.map((c) => c.uuid === uuid ? updater(c) : c);
|
|
26
|
+
connectionsRef.current = updated;
|
|
27
|
+
return updated;
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const updateConnectionName = (uuid, name) => {
|
|
31
|
+
setConnections((prev) => prev.map((c) => (c.uuid === uuid ? { ...c, name } : c)));
|
|
32
|
+
};
|
|
33
|
+
const appendLog = (uuid, log) => {
|
|
34
|
+
if (!log.timestamp) {
|
|
35
|
+
log.timestamp = new Date();
|
|
36
|
+
}
|
|
37
|
+
updateConnection(uuid, (c) => ({
|
|
38
|
+
...c,
|
|
39
|
+
logs: [...c.logs.slice(-199), log],
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
42
|
+
const addConnection = async ({ uuid, propCreator }) => {
|
|
43
|
+
uuid = uuid ?? uniqueNamesGenerator({ dictionaries: [animals] });
|
|
44
|
+
const props = propCreator?.(uuid);
|
|
45
|
+
const newConnection = {
|
|
46
|
+
...createInitialConnectionState(uuid),
|
|
47
|
+
...props
|
|
48
|
+
};
|
|
49
|
+
connectionsRef.current = [...connectionsRef.current, newConnection];
|
|
50
|
+
setConnections(prev => [...prev, newConnection]);
|
|
51
|
+
const anyUpdatedConnection = connectionsRef.current.find(c => c.uuid === uuid);
|
|
52
|
+
if (!anyUpdatedConnection) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
return uuid;
|
|
56
|
+
};
|
|
57
|
+
const removeConnection = (uuid) => {
|
|
58
|
+
setConnections((prev) => prev.filter((c) => c.uuid !== uuid));
|
|
59
|
+
};
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
connectionsRef.current = connections;
|
|
62
|
+
}, [connections]);
|
|
63
|
+
return {
|
|
64
|
+
connections,
|
|
65
|
+
connectionsRef,
|
|
66
|
+
addConnection,
|
|
67
|
+
removeConnection,
|
|
68
|
+
connect: (_uuid) => { },
|
|
69
|
+
disconnect: (_uuid) => { },
|
|
70
|
+
updateConnection,
|
|
71
|
+
reconnectAll: () => { },
|
|
72
|
+
updateConnectionName,
|
|
73
|
+
appendLog
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Re-export core types/classes
|
|
2
|
+
export * from "@/base/device-whisperer.js";
|
|
3
|
+
// Message layer exports
|
|
4
|
+
export * from "@/message_layers/protobuf-wrapper.js";
|
|
5
|
+
// Transport layers
|
|
6
|
+
export * from "@/transport_layers/serial-device-whisperer.js";
|
|
7
|
+
export * from "@/transport_layers/websocket-device-whisperer.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AddConnectionProps, DeviceConnectionState, MultiDeviceWhisperer } from "@/base/device-whisperer.js";
|
|
2
|
+
export type TopicHandlerContext<AppLayer extends DeviceConnectionState> = {
|
|
3
|
+
base: ReturnType<typeof MultiDeviceWhisperer<AppLayer>>;
|
|
4
|
+
uuid: string;
|
|
5
|
+
};
|
|
6
|
+
export type TopicHandlerMap<AppLayer extends DeviceConnectionState, Topic extends string | number, Message = any> = {
|
|
7
|
+
[K in Topic]?: (message: Message, context: TopicHandlerContext<AppLayer>) => void;
|
|
8
|
+
};
|
|
9
|
+
export type ProtobufDeviceWhispererProps<AppLayer extends DeviceConnectionState, Topic extends string | number, MessageRX = any, MessageTX = any> = {
|
|
10
|
+
transportLayer: ReturnType<typeof MultiDeviceWhisperer<AppLayer>>;
|
|
11
|
+
encodeRX: (message: MessageTX) => Uint8Array;
|
|
12
|
+
decodeTX: (bytes: Uint8Array) => MessageRX;
|
|
13
|
+
messageTypeField: keyof MessageRX;
|
|
14
|
+
rxTopicHandlerMap: TopicHandlerMap<AppLayer, Topic, MessageRX>;
|
|
15
|
+
HEADER?: Uint8Array;
|
|
16
|
+
expectLength?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConnectionState, Topic extends string | number, MessageRX = any, MessageTX = any>({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER, }: ProtobufDeviceWhispererProps<AppLayer, Topic, MessageRX, MessageTX>): {
|
|
19
|
+
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppLayer>) => Promise<string>;
|
|
20
|
+
sendProtobuf: (uuid: string, message: MessageTX) => void;
|
|
21
|
+
protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
|
|
22
|
+
connections: AppLayer[];
|
|
23
|
+
connectionsRef: import("react").RefObject<AppLayer[]>;
|
|
24
|
+
removeConnection: (uuid: string) => void;
|
|
25
|
+
connect: (_uuid: string) => void;
|
|
26
|
+
disconnect: (_uuid: string) => void;
|
|
27
|
+
updateConnection: (uuid: string, updater: (c: AppLayer) => AppLayer) => void;
|
|
28
|
+
reconnectAll: () => void;
|
|
29
|
+
updateConnectionName: (uuid: string, name: string) => void;
|
|
30
|
+
appendLog: (uuid: string, log: import("@/base/device-whisperer.js").LogLine) => void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export function ProtobufMultiDeviceWhisperer({ transportLayer, encodeRX, decodeTX, messageTypeField, rxTopicHandlerMap, HEADER = new Uint8Array([]), }) {
|
|
2
|
+
// --- Utils ---
|
|
3
|
+
const concatUint8Arrays = (a, b) => {
|
|
4
|
+
const result = new Uint8Array(a.length + b.length);
|
|
5
|
+
result.set(a);
|
|
6
|
+
result.set(b, a.length);
|
|
7
|
+
return result;
|
|
8
|
+
};
|
|
9
|
+
const wrapLengthPrefixed = (payload) => {
|
|
10
|
+
const length = payload.length;
|
|
11
|
+
const header = new Uint8Array([
|
|
12
|
+
(length >> 8) & 0xff, // Most significant byte first
|
|
13
|
+
length & 0xff // Least significant byte second
|
|
14
|
+
]);
|
|
15
|
+
return concatUint8Arrays(header, payload);
|
|
16
|
+
};
|
|
17
|
+
const tryDecodeLengthPrefixed = (buffer) => {
|
|
18
|
+
if (HEADER.length) {
|
|
19
|
+
const findHeader = (buf) => {
|
|
20
|
+
for (let i = 0; i < buf.length - 1; i++) {
|
|
21
|
+
if (buf[i] === HEADER[0] && buf[i + 1] === HEADER[1])
|
|
22
|
+
return i;
|
|
23
|
+
}
|
|
24
|
+
return -1;
|
|
25
|
+
};
|
|
26
|
+
const headerIndex = findHeader(buffer);
|
|
27
|
+
if (headerIndex === -1)
|
|
28
|
+
return [null, buffer];
|
|
29
|
+
buffer = buffer.slice(headerIndex);
|
|
30
|
+
}
|
|
31
|
+
const st_msg = HEADER.length + 2;
|
|
32
|
+
if (buffer.length < st_msg)
|
|
33
|
+
return [null, buffer];
|
|
34
|
+
const length = (buffer[HEADER.length] << 8) | buffer[HEADER.length + 1];
|
|
35
|
+
if (buffer.length < st_msg + length)
|
|
36
|
+
return [null, buffer];
|
|
37
|
+
const messageBytes = buffer.slice(st_msg, st_msg + length);
|
|
38
|
+
const leftover = buffer.slice(st_msg + length);
|
|
39
|
+
try {
|
|
40
|
+
const decoded = decodeTX(messageBytes);
|
|
41
|
+
return [decoded, leftover];
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [null, buffer.slice(1)];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
// --- New outbound API ---
|
|
48
|
+
const sendProtobuf = (uuid, message) => {
|
|
49
|
+
const encoded = encodeRX(message);
|
|
50
|
+
const wrapped = wrapLengthPrefixed(encoded);
|
|
51
|
+
const conn = transportLayer.connectionsRef.current.find(c => c.uuid === uuid);
|
|
52
|
+
console.log("Sending Protobuf:", message, "bytes: ", [...wrapped]);
|
|
53
|
+
conn?.send?.(wrapped);
|
|
54
|
+
};
|
|
55
|
+
// --- Inbound buffering ---
|
|
56
|
+
const buffers = {};
|
|
57
|
+
const protoBufOnReceiveHandler = (uuid, data) => {
|
|
58
|
+
const bytes = typeof data === "string"
|
|
59
|
+
? new TextEncoder().encode(data)
|
|
60
|
+
: data;
|
|
61
|
+
buffers[uuid] = concatUint8Arrays(buffers[uuid] || new Uint8Array(), bytes);
|
|
62
|
+
let buffer = buffers[uuid];
|
|
63
|
+
while (buffer.length > 0) {
|
|
64
|
+
const [msg, remaining] = tryDecodeLengthPrefixed(buffer);
|
|
65
|
+
if (msg) {
|
|
66
|
+
buffers[uuid] = remaining;
|
|
67
|
+
const topic = msg[messageTypeField];
|
|
68
|
+
const handler = rxTopicHandlerMap[topic];
|
|
69
|
+
if (handler) {
|
|
70
|
+
try {
|
|
71
|
+
handler(msg, { base: transportLayer, uuid });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
transportLayer.appendLog(uuid, {
|
|
75
|
+
level: 0,
|
|
76
|
+
message: `[!] Error in handler for topic "${topic}": ${err}`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
transportLayer.appendLog(uuid, {
|
|
82
|
+
level: 1,
|
|
83
|
+
message: `[!] Unknown Protobuf topic: "${topic}"`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
buffer = buffers[uuid];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// If not protobuf, see if we can log a line of text
|
|
90
|
+
const newlineIdx = buffer.indexOf(10); // '\n'
|
|
91
|
+
const headerIdx = buffer.findIndex((_, i) => HEADER.every((h, idx) => buffer[i] + idx === h));
|
|
92
|
+
if (newlineIdx !== -1 && (headerIdx === -1 || newlineIdx < headerIdx)) {
|
|
93
|
+
const line = buffer.slice(0, newlineIdx);
|
|
94
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
95
|
+
buffers[uuid] = buffer;
|
|
96
|
+
const text = new TextDecoder().decode(line).trim();
|
|
97
|
+
if (text) {
|
|
98
|
+
transportLayer.appendLog(uuid, {
|
|
99
|
+
level: 2,
|
|
100
|
+
message: text
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (headerIdx > 0) {
|
|
106
|
+
const garbage = buffer.slice(0, headerIdx);
|
|
107
|
+
buffer = buffer.slice(headerIdx);
|
|
108
|
+
buffers[uuid] = buffer;
|
|
109
|
+
const preview = Array.from(garbage)
|
|
110
|
+
.slice(0, 16)
|
|
111
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
112
|
+
.join(' ');
|
|
113
|
+
transportLayer.appendLog(uuid, {
|
|
114
|
+
level: 1,
|
|
115
|
+
message: `[!] Skipped invalid bytes before Protobuf header: ${preview}... (${garbage.length} bytes)`
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
buffers[uuid] = buffer;
|
|
122
|
+
};
|
|
123
|
+
// --- Override addConnection to wrap onReceive ---
|
|
124
|
+
const addConnection = async ({ uuid, propCreator }) => {
|
|
125
|
+
return await transportLayer.addConnection({
|
|
126
|
+
uuid,
|
|
127
|
+
propCreator: (id) => {
|
|
128
|
+
const props = propCreator?.(id);
|
|
129
|
+
return {
|
|
130
|
+
onReceive: (data) => protoBufOnReceiveHandler(id, data),
|
|
131
|
+
...props
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
...transportLayer,
|
|
138
|
+
addConnection,
|
|
139
|
+
sendProtobuf,
|
|
140
|
+
protoBufOnReceiveHandler
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FlashOptions, Transport } from "esptool-js";
|
|
2
|
+
import { DeviceConnectionState, AddConnectionProps } from "@/base/device-whisperer.js";
|
|
3
|
+
export type SerialConnectionState = DeviceConnectionState & {
|
|
4
|
+
port?: SerialPort;
|
|
5
|
+
baudrate?: number;
|
|
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"];
|
|
15
|
+
};
|
|
16
|
+
export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: {}): {
|
|
17
|
+
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
18
|
+
removeConnection: (uuid: string) => Promise<void>;
|
|
19
|
+
connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
|
|
20
|
+
disconnect: (uuid: string, timeout?: number) => Promise<void>;
|
|
21
|
+
handleFlashFirmware: (uuid: string, assetsToFlash: {
|
|
22
|
+
blob: Blob;
|
|
23
|
+
address: number;
|
|
24
|
+
}[]) => Promise<void>;
|
|
25
|
+
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
26
|
+
connections: AppOrMessageLayer[];
|
|
27
|
+
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
28
|
+
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
29
|
+
updateConnectionName: (uuid: string, name: string) => void;
|
|
30
|
+
appendLog: (uuid: string, log: import("@/base/device-whisperer.js").LogLine) => void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { ESPLoader, Transport } from "esptool-js";
|
|
2
|
+
import { MultiDeviceWhisperer } from "@/base/device-whisperer.js";
|
|
3
|
+
export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
4
|
+
const base = MultiDeviceWhisperer(props);
|
|
5
|
+
const defaultOnReceive = (uuid, data) => {
|
|
6
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
7
|
+
if (!conn)
|
|
8
|
+
return;
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
let bytes;
|
|
11
|
+
if (typeof data === "string") {
|
|
12
|
+
bytes = new TextEncoder().encode(data);
|
|
13
|
+
}
|
|
14
|
+
else if (data instanceof ArrayBuffer) {
|
|
15
|
+
bytes = new Uint8Array(data);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
bytes = data;
|
|
19
|
+
}
|
|
20
|
+
const asText = decoder.decode(bytes);
|
|
21
|
+
const combined = conn.readBufferLeftover + asText;
|
|
22
|
+
const lines = combined.split("\r\n");
|
|
23
|
+
base.updateConnection(uuid, (c) => ({
|
|
24
|
+
...c,
|
|
25
|
+
readBufferLeftover: lines.pop() || ""
|
|
26
|
+
}));
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (trimmed) {
|
|
30
|
+
base.appendLog(uuid, {
|
|
31
|
+
level: 2,
|
|
32
|
+
message: trimmed,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const defaultSend = async (uuid, data) => {
|
|
38
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
39
|
+
if (!conn)
|
|
40
|
+
return;
|
|
41
|
+
const asString = typeof data === "string"
|
|
42
|
+
? data
|
|
43
|
+
: btoa(String.fromCharCode(...data));
|
|
44
|
+
base.appendLog(uuid, {
|
|
45
|
+
level: 3,
|
|
46
|
+
message: asString,
|
|
47
|
+
});
|
|
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);
|
|
53
|
+
};
|
|
54
|
+
const readLoop = async (uuid, transport) => {
|
|
55
|
+
try {
|
|
56
|
+
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
|
+
const { value, done } = await reader.next();
|
|
65
|
+
if (done || !value)
|
|
66
|
+
break;
|
|
67
|
+
conn.onReceive?.(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
base.appendLog(uuid, {
|
|
72
|
+
level: 0,
|
|
73
|
+
message: `[!] Read loop error: ${e}`
|
|
74
|
+
});
|
|
75
|
+
await disconnect(uuid);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
base.updateConnection(uuid, (c) => ({
|
|
79
|
+
...c,
|
|
80
|
+
transport: null,
|
|
81
|
+
isConnected: false,
|
|
82
|
+
isConnecting: false,
|
|
83
|
+
autoConnect: false,
|
|
84
|
+
}));
|
|
85
|
+
base.appendLog(uuid, {
|
|
86
|
+
level: 0,
|
|
87
|
+
message: "[!] Serial disconnected"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const restartDevice = async (uuid, default_transport) => {
|
|
92
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
93
|
+
const transport = default_transport ?? conn?.transport;
|
|
94
|
+
if (transport) {
|
|
95
|
+
await transport.setRTS(false);
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
97
|
+
await transport.setRTS(true);
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log("No transport yet");
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const connect = async (uuid, baudrate, restart_on_connect = true) => {
|
|
105
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
106
|
+
if (!conn?.port)
|
|
107
|
+
return;
|
|
108
|
+
if (!conn?.transport) {
|
|
109
|
+
await disconnect(uuid);
|
|
110
|
+
}
|
|
111
|
+
;
|
|
112
|
+
base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true }));
|
|
113
|
+
const use_baudrate = baudrate ?? conn.baudrate ?? 115200;
|
|
114
|
+
const transport = new Transport(conn.port, false, false);
|
|
115
|
+
try {
|
|
116
|
+
await transport.connect(use_baudrate);
|
|
117
|
+
if (restart_on_connect) {
|
|
118
|
+
await restartDevice(uuid, transport);
|
|
119
|
+
}
|
|
120
|
+
;
|
|
121
|
+
base.updateConnection(uuid, (c) => ({
|
|
122
|
+
...c,
|
|
123
|
+
transport,
|
|
124
|
+
baudrate: use_baudrate,
|
|
125
|
+
isConnected: true,
|
|
126
|
+
isConnecting: false,
|
|
127
|
+
}));
|
|
128
|
+
base.appendLog(uuid, {
|
|
129
|
+
level: 2,
|
|
130
|
+
message: "[✓] Serial connected"
|
|
131
|
+
});
|
|
132
|
+
await conn.onConnect?.();
|
|
133
|
+
await readLoop(uuid, transport);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
base.updateConnection(uuid, (c) => ({
|
|
137
|
+
...c,
|
|
138
|
+
isConnected: false,
|
|
139
|
+
isConnecting: false
|
|
140
|
+
}));
|
|
141
|
+
base.appendLog(uuid, {
|
|
142
|
+
level: 0,
|
|
143
|
+
message: `[x] Serial connection error: ${err?.message || "Unknown error"}`
|
|
144
|
+
});
|
|
145
|
+
await disconnect(uuid);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const disconnect = async (uuid, timeout = 2000) => {
|
|
149
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
150
|
+
if (conn?.transport) {
|
|
151
|
+
try {
|
|
152
|
+
// Attempt disconnect, but don’t hang if the port is crashed
|
|
153
|
+
await Promise.race([
|
|
154
|
+
conn.transport.disconnect(),
|
|
155
|
+
new Promise((resolve) => setTimeout(resolve, timeout)),
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.warn(`[${uuid}] Serial Disconnect error:`, e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Always clear the transport and reset connection state
|
|
163
|
+
base.updateConnection(uuid, (c) => ({
|
|
164
|
+
...c,
|
|
165
|
+
transport: null,
|
|
166
|
+
isConnected: false,
|
|
167
|
+
isConnecting: false,
|
|
168
|
+
autoConnect: false,
|
|
169
|
+
}));
|
|
170
|
+
await conn?.onDisconnect?.();
|
|
171
|
+
};
|
|
172
|
+
const addConnection = async ({ uuid, propCreator }) => {
|
|
173
|
+
const port = await navigator.serial.requestPort({
|
|
174
|
+
filters: [{ usbVendorId: 0x303a }]
|
|
175
|
+
});
|
|
176
|
+
return await base.addConnection({
|
|
177
|
+
uuid,
|
|
178
|
+
propCreator: (id) => {
|
|
179
|
+
const props = propCreator?.(id);
|
|
180
|
+
return {
|
|
181
|
+
send: props?.send || ((d) => defaultSend(id, d)),
|
|
182
|
+
onReceive: props?.onReceive || ((d) => defaultOnReceive(id, d)),
|
|
183
|
+
port,
|
|
184
|
+
...props
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const removeConnection = async (uuid) => {
|
|
190
|
+
try {
|
|
191
|
+
await disconnect(uuid);
|
|
192
|
+
}
|
|
193
|
+
catch (e) { }
|
|
194
|
+
;
|
|
195
|
+
base.removeConnection(uuid);
|
|
196
|
+
};
|
|
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
|
+
const reconnectAll = async (...connectionProps) => {
|
|
274
|
+
const connections = [...base.connectionsRef.current]; // snapshot first
|
|
275
|
+
await Promise.all(connections.map(async (c) => {
|
|
276
|
+
await disconnect(c.uuid);
|
|
277
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
278
|
+
return connect(c.uuid, ...connectionProps);
|
|
279
|
+
}));
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
...base,
|
|
283
|
+
addConnection,
|
|
284
|
+
removeConnection,
|
|
285
|
+
connect,
|
|
286
|
+
disconnect,
|
|
287
|
+
handleFlashFirmware,
|
|
288
|
+
reconnectAll
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DeviceConnectionState, AddConnectionProps } from "@/base/device-whisperer.js";
|
|
2
|
+
export type WebsocketConnectionState = DeviceConnectionState & {
|
|
3
|
+
ws?: WebSocket;
|
|
4
|
+
};
|
|
5
|
+
export type DeviceObjectResponse = {
|
|
6
|
+
id: string;
|
|
7
|
+
deviceState: string;
|
|
8
|
+
uiState: string;
|
|
9
|
+
deviceConnectedTime: Date | null;
|
|
10
|
+
deviceLastCommTime: Date | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends WebsocketConnectionState>(server_url: string, server_port: number, { ...props }?: {}): {
|
|
13
|
+
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
14
|
+
removeConnection: (uuid: string) => Promise<void>;
|
|
15
|
+
connect: (uuid: string, attempt?: number) => Promise<void>;
|
|
16
|
+
disconnect: (uuid: string) => Promise<void>;
|
|
17
|
+
checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
|
|
18
|
+
reconnectAll: () => Promise<void>;
|
|
19
|
+
connections: AppOrMessageLayer[];
|
|
20
|
+
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
21
|
+
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
22
|
+
updateConnectionName: (uuid: string, name: string) => void;
|
|
23
|
+
appendLog: (uuid: string, log: import("@/base/device-whisperer.js").LogLine) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { MultiDeviceWhisperer } from "@/base/device-whisperer.js";
|
|
2
|
+
export function WebsocketMultiDeviceWhisperer(server_url, server_port, { ...props } = {}) {
|
|
3
|
+
const defaultOnReceive = (uuid, data) => {
|
|
4
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
5
|
+
if (!conn)
|
|
6
|
+
return;
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
let bytes;
|
|
9
|
+
if (typeof data === "string") {
|
|
10
|
+
bytes = new TextEncoder().encode(data);
|
|
11
|
+
}
|
|
12
|
+
else if (data instanceof ArrayBuffer) {
|
|
13
|
+
bytes = new Uint8Array(data);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
bytes = data;
|
|
17
|
+
}
|
|
18
|
+
const asText = decoder.decode(bytes);
|
|
19
|
+
const combined = conn.readBufferLeftover + asText;
|
|
20
|
+
const lines = combined.split("\n");
|
|
21
|
+
base.updateConnection(uuid, (c) => ({
|
|
22
|
+
...c,
|
|
23
|
+
readBufferLeftover: lines.pop() || ""
|
|
24
|
+
}));
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (trimmed) {
|
|
28
|
+
base.appendLog(uuid, {
|
|
29
|
+
level: 2,
|
|
30
|
+
message: trimmed,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const defaultSend = async (uuid, data) => {
|
|
36
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
37
|
+
if (!conn || !conn.ws)
|
|
38
|
+
return;
|
|
39
|
+
const asString = typeof data === "string"
|
|
40
|
+
? data
|
|
41
|
+
: btoa(String.fromCharCode(...data));
|
|
42
|
+
base.appendLog(uuid, {
|
|
43
|
+
level: 3,
|
|
44
|
+
message: asString,
|
|
45
|
+
});
|
|
46
|
+
conn.ws.send(data);
|
|
47
|
+
};
|
|
48
|
+
const connect = async (uuid, attempt = 0) => {
|
|
49
|
+
const MAX_RETRIES = 5;
|
|
50
|
+
const RETRY_DELAY_MS = 2000; // 2 seconds
|
|
51
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
52
|
+
if (!conn)
|
|
53
|
+
return;
|
|
54
|
+
if (conn.ws && conn.ws.readyState === WebSocket.OPEN) {
|
|
55
|
+
console.log(`[client] Closing existing WS before reconnect for ${uuid}`);
|
|
56
|
+
conn.ws.close();
|
|
57
|
+
}
|
|
58
|
+
base.updateConnection(uuid, (c) => ({ ...c, isConnecting: true, autoConnect: true }));
|
|
59
|
+
try {
|
|
60
|
+
const pre = server_port !== 443 ? "ws" : "wss";
|
|
61
|
+
const ws = new WebSocket(`${pre}://${server_url}:${server_port}/ui/${uuid}`);
|
|
62
|
+
ws.binaryType = "arraybuffer";
|
|
63
|
+
ws.onopen = () => {
|
|
64
|
+
base.updateConnection(uuid, (c) => ({
|
|
65
|
+
...c,
|
|
66
|
+
ws,
|
|
67
|
+
isConnected: true,
|
|
68
|
+
isConnecting: false,
|
|
69
|
+
lastAttempt: attempt
|
|
70
|
+
}));
|
|
71
|
+
base.appendLog(uuid, {
|
|
72
|
+
level: 2,
|
|
73
|
+
message: "[✓] WebSocket connected"
|
|
74
|
+
});
|
|
75
|
+
conn?.onConnect?.();
|
|
76
|
+
};
|
|
77
|
+
ws.onmessage = (event) => {
|
|
78
|
+
let data;
|
|
79
|
+
if (typeof event.data === "string")
|
|
80
|
+
data = event.data;
|
|
81
|
+
else if (event.data instanceof ArrayBuffer)
|
|
82
|
+
data = new Uint8Array(event.data);
|
|
83
|
+
else
|
|
84
|
+
data = event.data;
|
|
85
|
+
conn.onReceive?.(data);
|
|
86
|
+
};
|
|
87
|
+
ws.onerror = (err) => {
|
|
88
|
+
base.appendLog(uuid, {
|
|
89
|
+
level: 0,
|
|
90
|
+
message: `[x] WS error: ${err}`
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
ws.onclose = async () => {
|
|
94
|
+
base.updateConnection(uuid, (c) => ({
|
|
95
|
+
...c,
|
|
96
|
+
isConnected: false,
|
|
97
|
+
isConnecting: false,
|
|
98
|
+
ws: undefined
|
|
99
|
+
}));
|
|
100
|
+
base.appendLog(uuid, { level: 0, message: "[!] WS disconnected" });
|
|
101
|
+
const updated_conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
102
|
+
if (!updated_conn) {
|
|
103
|
+
base.appendLog(uuid, { level: 0, message: "[!] Connection lost!" });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Auto reconnect if enabled
|
|
107
|
+
if (updated_conn.autoConnect && attempt < MAX_RETRIES) {
|
|
108
|
+
base.appendLog(uuid, { level: 2, message: `[~] Reconnecting in ${RETRY_DELAY_MS}ms... (attempt ${attempt + 1})` });
|
|
109
|
+
setTimeout(() => connect(uuid, attempt + 1), RETRY_DELAY_MS);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
base.updateConnection(uuid, (c) => ({
|
|
115
|
+
...c,
|
|
116
|
+
isConnected: false,
|
|
117
|
+
isConnecting: false,
|
|
118
|
+
logs: [
|
|
119
|
+
...c.logs,
|
|
120
|
+
{ level: 0, message: `[x] WS connection error: ${err?.message || "Unknown error"}` }
|
|
121
|
+
]
|
|
122
|
+
}));
|
|
123
|
+
await disconnect(uuid);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const disconnect = async (uuid) => {
|
|
127
|
+
const conn = base.connectionsRef.current.find((c) => c.uuid === uuid);
|
|
128
|
+
if (!conn?.ws)
|
|
129
|
+
return;
|
|
130
|
+
base.updateConnection(uuid, (c) => ({
|
|
131
|
+
...c,
|
|
132
|
+
isConnected: false,
|
|
133
|
+
isConnecting: false,
|
|
134
|
+
autoConnect: false,
|
|
135
|
+
ws: undefined,
|
|
136
|
+
readBufferLeftover: ""
|
|
137
|
+
}));
|
|
138
|
+
conn.ws.close();
|
|
139
|
+
await conn?.onDisconnect?.();
|
|
140
|
+
};
|
|
141
|
+
const base = MultiDeviceWhisperer(props);
|
|
142
|
+
const addConnection = async ({ uuid, propCreator }) => {
|
|
143
|
+
return await base.addConnection({
|
|
144
|
+
uuid,
|
|
145
|
+
propCreator: (id) => {
|
|
146
|
+
const props = propCreator?.(id);
|
|
147
|
+
return {
|
|
148
|
+
send: props?.send || ((d) => defaultSend(id, d)),
|
|
149
|
+
onReceive: props?.onReceive || ((d) => defaultOnReceive(id, d)),
|
|
150
|
+
...props
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const removeConnection = async (uuid) => {
|
|
156
|
+
await disconnect(uuid);
|
|
157
|
+
base.removeConnection(uuid);
|
|
158
|
+
};
|
|
159
|
+
const checkForNewDevices = async () => {
|
|
160
|
+
try {
|
|
161
|
+
const url = server_port !== 443
|
|
162
|
+
? `http://${server_url}:${server_port}`
|
|
163
|
+
: `https://${server_url}`;
|
|
164
|
+
const response = await fetch(`${url}/devices`);
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
167
|
+
}
|
|
168
|
+
return (await response.json());
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.error("Failed to fetch devices:", error);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const reconnectAll = async () => {
|
|
176
|
+
for (const c of base.connectionsRef.current) {
|
|
177
|
+
await disconnect(c.uuid);
|
|
178
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
179
|
+
}
|
|
180
|
+
for (const c of base.connectionsRef.current) {
|
|
181
|
+
await connect(c.uuid);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
...base,
|
|
186
|
+
addConnection,
|
|
187
|
+
removeConnection,
|
|
188
|
+
connect,
|
|
189
|
+
disconnect,
|
|
190
|
+
checkForNewDevices,
|
|
191
|
+
reconnectAll
|
|
192
|
+
};
|
|
193
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ota-hub-reactjs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"module": "dist/index.esm.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
|
+
}
|