ota-hub-reactjs 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base/device-whisperer.d.ts +1 -2
- package/dist/base/device-whisperer.js +23 -22
- package/dist/message_layers/protobuf-wrapper.d.ts +0 -1
- package/dist/transport_layers/esp32-device-whisperer.d.ts +4 -3
- package/dist/transport_layers/esp32-device-whisperer.js +26 -40
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +0 -1
- package/dist/transport_layers/mqtt-device-whisperer.js +8 -6
- package/dist/transport_layers/serial-device-whisperer.d.ts +7 -5
- package/dist/transport_layers/serial-device-whisperer.js +70 -28
- package/dist/transport_layers/websocket-device-whisperer.d.ts +1 -2
- package/dist/transport_layers/websocket-device-whisperer.js +9 -7
- package/package.json +1 -2
|
@@ -2,7 +2,7 @@ export type LogMessage = string | number | boolean | Record<string, any> | any[]
|
|
|
2
2
|
export type LogLine = {
|
|
3
3
|
level: number;
|
|
4
4
|
message: LogMessage;
|
|
5
|
-
timestamp?:
|
|
5
|
+
timestamp?: Date;
|
|
6
6
|
};
|
|
7
7
|
type PropCreatorProps<T> = (uuid: string) => Partial<T> | undefined;
|
|
8
8
|
export interface AddConnectionProps<T> {
|
|
@@ -30,7 +30,6 @@ export type DeviceWhispererProps<T extends DeviceConnectionState> = {
|
|
|
30
30
|
};
|
|
31
31
|
export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
|
|
32
32
|
connections: T[];
|
|
33
|
-
connectionsRef: import("react").RefObject<T[]>;
|
|
34
33
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
|
|
35
34
|
removeConnection: (uuid: string) => void;
|
|
36
35
|
connect: (_uuid: string) => void;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import { uniqueNamesGenerator, animals } from "unique-names-generator";
|
|
4
3
|
;
|
|
5
4
|
// Initial, generic state for the generic device
|
|
6
5
|
export function createDefaultInitialDeviceState(uuid, props) {
|
|
@@ -20,53 +19,55 @@ export function createDefaultInitialDeviceState(uuid, props) {
|
|
|
20
19
|
*/
|
|
21
20
|
export function MultiDeviceWhisperer({ createInitialConnectionState = createDefaultInitialDeviceState, } = {}) {
|
|
22
21
|
const [connections, setConnections] = useState([]);
|
|
23
|
-
const connectionsRef = useRef(
|
|
22
|
+
const connectionsRef = useRef(new Map());
|
|
24
23
|
const [isReady, setIsReady] = useState(false);
|
|
25
|
-
|
|
24
|
+
// ---------- GET ----------
|
|
25
|
+
const getConnection = (uuid) => connectionsRef.current.get(uuid);
|
|
26
|
+
// ---------- UPDATE ----------
|
|
26
27
|
const updateConnection = (uuid, updater) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const current = connectionsRef.current.get(uuid);
|
|
29
|
+
if (!current)
|
|
30
|
+
return;
|
|
31
|
+
const next = updater(current);
|
|
32
|
+
connectionsRef.current.set(uuid, next);
|
|
33
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
32
34
|
};
|
|
33
35
|
const updateConnectionName = (uuid, name) => {
|
|
34
|
-
|
|
36
|
+
updateConnection(uuid, (c) => ({ ...c, name }));
|
|
35
37
|
};
|
|
36
38
|
const appendLog = (uuid, log) => {
|
|
37
|
-
if (!log.timestamp)
|
|
39
|
+
if (!log.timestamp)
|
|
38
40
|
log.timestamp = new Date();
|
|
39
|
-
}
|
|
40
41
|
updateConnection(uuid, (c) => ({
|
|
41
42
|
...c,
|
|
42
43
|
logs: [...c.logs.slice(-199), log],
|
|
43
44
|
}));
|
|
44
45
|
};
|
|
46
|
+
// ---------- ADD ----------
|
|
45
47
|
const addConnection = async ({ uuid, propCreator }) => {
|
|
46
|
-
uuid = uuid ??
|
|
48
|
+
uuid = uuid ?? `unnamed_device_${connectionsRef.current.size}`;
|
|
47
49
|
const props = propCreator?.(uuid);
|
|
48
50
|
const newConnection = {
|
|
49
51
|
...createDefaultInitialDeviceState(uuid),
|
|
50
52
|
...createInitialConnectionState(uuid),
|
|
51
53
|
...props
|
|
52
54
|
};
|
|
53
|
-
connectionsRef.current
|
|
54
|
-
setConnections(
|
|
55
|
-
const anyUpdatedConnection = getConnection(uuid);
|
|
56
|
-
if (!anyUpdatedConnection) {
|
|
57
|
-
return "";
|
|
58
|
-
}
|
|
55
|
+
connectionsRef.current.set(uuid, newConnection);
|
|
56
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
59
57
|
return uuid;
|
|
60
58
|
};
|
|
59
|
+
// ---------- REMOVE ----------
|
|
61
60
|
const removeConnection = (uuid) => {
|
|
62
|
-
|
|
61
|
+
connectionsRef.current.delete(uuid);
|
|
62
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
63
63
|
};
|
|
64
|
+
// ---------- EFFECT ----------
|
|
64
65
|
useEffect(() => {
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// initial snapshot
|
|
67
|
+
setConnections(Array.from(connectionsRef.current.values()));
|
|
68
|
+
}, []);
|
|
67
69
|
return {
|
|
68
70
|
connections,
|
|
69
|
-
connectionsRef,
|
|
70
71
|
addConnection,
|
|
71
72
|
removeConnection,
|
|
72
73
|
connect: (_uuid) => { },
|
|
@@ -20,7 +20,6 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
|
|
|
20
20
|
sendProtobuf: (uuid: string, message: MessageTX) => void;
|
|
21
21
|
protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
|
|
22
22
|
connections: AppLayer[];
|
|
23
|
-
connectionsRef: import("react").RefObject<AppLayer[]>;
|
|
24
23
|
removeConnection: (uuid: string) => void;
|
|
25
24
|
connect: (_uuid: string) => void;
|
|
26
25
|
disconnect: (_uuid: string) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ESPLoader, FlashOptions, Transport } from "esptool-js";
|
|
2
|
-
import { AddConnectionProps, DeviceConnectionState } from "../base/device-whisperer.js";
|
|
2
|
+
import { AddConnectionProps, DeviceConnectionState, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
3
3
|
export type ESP32ConnectionState = DeviceConnectionState & {
|
|
4
4
|
port?: SerialPort;
|
|
5
5
|
baudrate?: number;
|
|
@@ -15,7 +15,9 @@ export type FlashFirmwareProps = {
|
|
|
15
15
|
firmwareBlob?: Blob;
|
|
16
16
|
fileArray?: FlashOptions["fileArray"];
|
|
17
17
|
};
|
|
18
|
-
export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ ...props }?: {
|
|
18
|
+
export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP32ConnectionState>({ releasePortByDefault, ...props }?: {
|
|
19
|
+
releasePortByDefault: boolean;
|
|
20
|
+
} & DeviceWhispererProps<AppOrMessageLayer>): {
|
|
19
21
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string>;
|
|
20
22
|
removeConnection: (uuid: string) => Promise<void>;
|
|
21
23
|
connect: (uuid: string, baudrate?: number, restart_on_connect?: boolean) => Promise<void>;
|
|
@@ -26,7 +28,6 @@ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP3
|
|
|
26
28
|
address: number;
|
|
27
29
|
}[]) => Promise<void>;
|
|
28
30
|
connections: AppOrMessageLayer[];
|
|
29
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
30
31
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
31
32
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
32
33
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -1,39 +1,19 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { ESPLoader, Transport } from "esptool-js";
|
|
3
3
|
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
4
|
-
export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
4
|
+
export function ESP32MultiDeviceWhisperer({ releasePortByDefault, ...props } = { releasePortByDefault: true }) {
|
|
5
5
|
const base = MultiDeviceWhisperer(props);
|
|
6
6
|
const defaultOnReceive = (uuid, data) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const text = typeof data === "string"
|
|
8
|
+
? data
|
|
9
|
+
: new TextDecoder().decode(data);
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (!trimmed)
|
|
9
12
|
return;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
else if (data instanceof ArrayBuffer) {
|
|
16
|
-
bytes = new Uint8Array(data);
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
bytes = data;
|
|
20
|
-
}
|
|
21
|
-
const asText = decoder.decode(bytes);
|
|
22
|
-
const combined = conn.readBufferLeftover + asText;
|
|
23
|
-
const lines = combined.split("\r\n");
|
|
24
|
-
base.updateConnection(uuid, (c) => ({
|
|
25
|
-
...c,
|
|
26
|
-
readBufferLeftover: lines.pop() || ""
|
|
27
|
-
}));
|
|
28
|
-
for (const line of lines) {
|
|
29
|
-
const trimmed = line.trim();
|
|
30
|
-
if (trimmed) {
|
|
31
|
-
base.appendLog(uuid, {
|
|
32
|
-
level: 2,
|
|
33
|
-
message: trimmed,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
13
|
+
base.appendLog(uuid, {
|
|
14
|
+
level: 2,
|
|
15
|
+
message: trimmed,
|
|
16
|
+
});
|
|
37
17
|
};
|
|
38
18
|
const defaultSend = async (uuid, data) => {
|
|
39
19
|
const conn = base.getConnection(uuid);
|
|
@@ -52,9 +32,7 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
52
32
|
return;
|
|
53
33
|
};
|
|
54
34
|
const readLoop = async (uuid, transport) => {
|
|
55
|
-
const
|
|
56
|
-
if (!conn)
|
|
57
|
-
return;
|
|
35
|
+
const textDecoder = new TextDecoder();
|
|
58
36
|
let readBuffer = ""; // accumulate ASCII/lines
|
|
59
37
|
let slipBuffer = []; // accumulate SLIP frames
|
|
60
38
|
let inSlipFrame = false; // are we inside a SLIP frame?
|
|
@@ -62,6 +40,12 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
62
40
|
try {
|
|
63
41
|
const reader = transport.rawRead();
|
|
64
42
|
while (true) {
|
|
43
|
+
const conn = base.getConnection(uuid);
|
|
44
|
+
if (!conn) {
|
|
45
|
+
console.log("Kack!");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
;
|
|
65
49
|
const { value, done } = await reader.next();
|
|
66
50
|
if (done || !value)
|
|
67
51
|
break;
|
|
@@ -117,9 +101,8 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
117
101
|
escapeNext = false;
|
|
118
102
|
continue;
|
|
119
103
|
}
|
|
120
|
-
// treat as normal
|
|
121
|
-
|
|
122
|
-
readBuffer += char;
|
|
104
|
+
// treat as normal text (correctly)
|
|
105
|
+
readBuffer += textDecoder.decode(new Uint8Array([b]), { stream: true });
|
|
123
106
|
// check for newline
|
|
124
107
|
let newlineIndex;
|
|
125
108
|
while ((newlineIndex = readBuffer.indexOf("\n")) >= 0) {
|
|
@@ -244,8 +227,8 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
244
227
|
// Always clear the transport and reset connection state
|
|
245
228
|
base.updateConnection(uuid, (c) => ({
|
|
246
229
|
...c,
|
|
247
|
-
port: null,
|
|
248
|
-
transport: null,
|
|
230
|
+
port: releasePortByDefault ? null : c.port,
|
|
231
|
+
transport: releasePortByDefault ? null : c.transport,
|
|
249
232
|
isConnected: false,
|
|
250
233
|
isConnecting: false,
|
|
251
234
|
autoConnect: false,
|
|
@@ -285,8 +268,11 @@ export function ESP32MultiDeviceWhisperer({ ...props } = {}) {
|
|
|
285
268
|
base.removeConnection(uuid);
|
|
286
269
|
};
|
|
287
270
|
const reconnectAll = async (...connectionProps) => {
|
|
288
|
-
const
|
|
289
|
-
await Promise.all(
|
|
271
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
272
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
273
|
+
const c = base.getConnection(id);
|
|
274
|
+
if (!c)
|
|
275
|
+
return;
|
|
290
276
|
await disconnect(c.uuid);
|
|
291
277
|
await new Promise((res) => setTimeout(res, 250));
|
|
292
278
|
return connect(c.uuid, ...connectionProps);
|
|
@@ -22,7 +22,6 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
|
|
|
22
22
|
reconnectAll: () => Promise<void>;
|
|
23
23
|
connectToMQTTServer: () => (() => void) | undefined;
|
|
24
24
|
connections: AppOrMessageLayer[];
|
|
25
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
26
25
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
27
26
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
28
27
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -189,7 +189,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
189
189
|
Error("In MQTT you MUST define a UUID otherwise we don't know what device we're connecting to!");
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
|
-
if (base.
|
|
192
|
+
if (base.connections.some(c => c.uuid === uuid) || addingConnections.current.has(uuid)) {
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
await base.addConnection({
|
|
@@ -221,13 +221,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
221
221
|
base.removeConnection(uuid);
|
|
222
222
|
};
|
|
223
223
|
const reconnectAll = async () => {
|
|
224
|
-
|
|
224
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
225
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
226
|
+
const c = base.getConnection(id);
|
|
227
|
+
if (!c)
|
|
228
|
+
return;
|
|
225
229
|
await disconnect(c.uuid);
|
|
226
230
|
await new Promise((res) => setTimeout(res, 250));
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await connect(c.uuid);
|
|
230
|
-
}
|
|
231
|
+
return connect(c.uuid);
|
|
232
|
+
}));
|
|
231
233
|
};
|
|
232
234
|
useEffect(() => {
|
|
233
235
|
if (!(serverAutoConnect || serverConnectOn))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { DeviceConnectionState, AddConnectionProps } from "../base/device-whisperer.js";
|
|
1
|
+
import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
2
2
|
export declare class UsbTransport {
|
|
3
3
|
device: USBDevice;
|
|
4
|
-
|
|
4
|
+
controlInterface: number;
|
|
5
|
+
dataInterface: number;
|
|
5
6
|
endpointIn: number;
|
|
6
7
|
endpointOut: number;
|
|
7
8
|
private static readonly SET_LINE_CODING;
|
|
@@ -37,14 +38,15 @@ export type SerialConnectionState = DeviceConnectionState & {
|
|
|
37
38
|
baudrate?: number;
|
|
38
39
|
slipReadWrite?: boolean;
|
|
39
40
|
};
|
|
40
|
-
export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?:
|
|
41
|
+
export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends SerialConnectionState>({ ...props }?: DeviceWhispererProps<AppOrMessageLayer>): {
|
|
41
42
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppOrMessageLayer>) => Promise<string | undefined>;
|
|
42
43
|
removeConnection: (uuid: string) => Promise<void>;
|
|
43
44
|
connect: (uuid: string, baudrate?: number) => Promise<void>;
|
|
44
45
|
disconnect: (uuid: string) => Promise<void>;
|
|
45
|
-
reconnectAll: () => Promise<void>;
|
|
46
|
+
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
47
|
+
releasePortByDefault: boolean;
|
|
48
|
+
setReleasePortByDefault: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
46
49
|
connections: AppOrMessageLayer[];
|
|
47
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
48
50
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
49
51
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
50
52
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MultiDeviceWhisperer } from "../base/device-whisperer.js";
|
|
2
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
/*
|
|
4
4
|
┌────────────────────────────────┐
|
|
5
5
|
│ Device Whisperer (Generic) │ (connect/disconnect, state mgmt) - "Abstraction Layer"
|
|
@@ -14,7 +14,8 @@ import { useEffect } from "react";
|
|
|
14
14
|
// usb-transport.ts
|
|
15
15
|
export class UsbTransport {
|
|
16
16
|
constructor(device) {
|
|
17
|
-
this.
|
|
17
|
+
this.controlInterface = 0;
|
|
18
|
+
this.dataInterface = 1;
|
|
18
19
|
this.endpointIn = 0;
|
|
19
20
|
this.endpointOut = 0;
|
|
20
21
|
this.device = device;
|
|
@@ -27,25 +28,40 @@ export class UsbTransport {
|
|
|
27
28
|
if (this.device.configuration === null) {
|
|
28
29
|
await this.device.selectConfiguration(1);
|
|
29
30
|
}
|
|
30
|
-
// Find the CDC Data interface (usually class 10) or just the first one with Bulk endpoints
|
|
31
31
|
const interfaces = this.device.configuration?.interfaces || [];
|
|
32
|
-
let
|
|
33
|
-
|
|
32
|
+
let ctrlIface;
|
|
33
|
+
let dataIface;
|
|
34
|
+
dataIface = interfaces.find(iface => {
|
|
34
35
|
const endpoints = iface.alternate.endpoints;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
return endpoints.some(e => e.direction === 'in' && e.type === 'bulk') &&
|
|
37
|
+
endpoints.some(e => e.direction === 'out' && e.type === 'bulk');
|
|
38
|
+
});
|
|
39
|
+
if (dataIface) {
|
|
40
|
+
this.dataInterface = dataIface.interfaceNumber;
|
|
41
|
+
ctrlIface = interfaces.find(i => i.interfaceNumber === this.dataInterface - 1)
|
|
42
|
+
|| interfaces.find(i => i.alternate.interfaceClass === 2);
|
|
43
|
+
this.controlInterface = ctrlIface ? ctrlIface.interfaceNumber : this.dataInterface;
|
|
44
|
+
}
|
|
45
|
+
if (!dataIface) {
|
|
46
|
+
throw new Error("No serial-compatible Bulk interface found.");
|
|
47
|
+
}
|
|
48
|
+
// Note: On Android, if the OS has claimed Interface 0, this first call will fail.
|
|
49
|
+
try {
|
|
50
|
+
if (this.controlInterface !== this.dataInterface) {
|
|
51
|
+
await this.device.claimInterface(this.controlInterface);
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
catch (e) {
|
|
55
|
+
console.warn("Could not claim Control Interface (OS locked?). Proceeding to Data...", e);
|
|
56
|
+
// We continue, but setSignals might fail later.
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await this.device.claimInterface(this.dataInterface);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
throw new Error(`Failed to claim Data Interface. Android OS has locked the device driver. Try using a 'Vendor Specific' USB Class device or a native Serial App workaround.`);
|
|
44
63
|
}
|
|
45
|
-
|
|
46
|
-
await this.device.claimInterface(this.interfaceNumber);
|
|
47
|
-
// Map endpoints
|
|
48
|
-
const endpoints = targetInterface.alternate.endpoints;
|
|
64
|
+
const endpoints = dataIface.alternate.endpoints;
|
|
49
65
|
this.endpointIn = endpoints.find(e => e.direction === 'in' && e.type === 'bulk').endpointNumber;
|
|
50
66
|
this.endpointOut = endpoints.find(e => e.direction === 'out' && e.type === 'bulk').endpointNumber;
|
|
51
67
|
await this.setBaudRate(baudRate);
|
|
@@ -57,7 +73,6 @@ export class UsbTransport {
|
|
|
57
73
|
async write(data) {
|
|
58
74
|
if (!this.device.opened)
|
|
59
75
|
return;
|
|
60
|
-
// Cast to 'unknown' then 'BufferSource' to satisfy the strict type definition
|
|
61
76
|
await this.device.transferOut(this.endpointOut, data);
|
|
62
77
|
}
|
|
63
78
|
/**
|
|
@@ -85,14 +100,13 @@ export class UsbTransport {
|
|
|
85
100
|
async setSignals({ dtr, rts }) {
|
|
86
101
|
if (!this.device.opened)
|
|
87
102
|
return;
|
|
88
|
-
// CDC-ACM: DTR is bit 0, RTS is bit 1
|
|
89
103
|
const value = (Number(dtr) | (Number(rts) << 1));
|
|
90
104
|
await this.device.controlTransferOut({
|
|
91
105
|
requestType: 'class',
|
|
92
106
|
recipient: 'interface',
|
|
93
107
|
request: UsbTransport.SET_CONTROL_LINE_STATE,
|
|
94
108
|
value: value,
|
|
95
|
-
index: this.
|
|
109
|
+
index: this.controlInterface
|
|
96
110
|
});
|
|
97
111
|
}
|
|
98
112
|
async setBaudRate(baud) {
|
|
@@ -102,20 +116,31 @@ export class UsbTransport {
|
|
|
102
116
|
// 4 bytes: baud (LE), 1 byte: stop bits, 1 byte: parity, 1 byte: data bits
|
|
103
117
|
const buffer = new ArrayBuffer(7);
|
|
104
118
|
const view = new DataView(buffer);
|
|
105
|
-
view.setUint32(0, baud, true);
|
|
106
|
-
view.setUint8(4, 0);
|
|
107
|
-
view.setUint8(5, 0);
|
|
108
|
-
view.setUint8(6, 8);
|
|
119
|
+
view.setUint32(0, baud, true);
|
|
120
|
+
view.setUint8(4, 0);
|
|
121
|
+
view.setUint8(5, 0);
|
|
122
|
+
view.setUint8(6, 8);
|
|
109
123
|
await this.device.controlTransferOut({
|
|
110
124
|
requestType: 'class',
|
|
111
125
|
recipient: 'interface',
|
|
112
126
|
request: UsbTransport.SET_LINE_CODING,
|
|
113
127
|
value: 0,
|
|
114
|
-
index: this.
|
|
128
|
+
index: this.controlInterface
|
|
115
129
|
}, buffer);
|
|
116
130
|
}
|
|
117
131
|
async disconnect() {
|
|
118
132
|
if (this.device.opened) {
|
|
133
|
+
// Release both
|
|
134
|
+
try {
|
|
135
|
+
await this.device.releaseInterface(this.dataInterface);
|
|
136
|
+
}
|
|
137
|
+
catch (e) { }
|
|
138
|
+
if (this.controlInterface !== this.dataInterface) {
|
|
139
|
+
try {
|
|
140
|
+
await this.device.releaseInterface(this.controlInterface);
|
|
141
|
+
}
|
|
142
|
+
catch (e) { }
|
|
143
|
+
}
|
|
119
144
|
await this.device.close();
|
|
120
145
|
}
|
|
121
146
|
}
|
|
@@ -125,6 +150,11 @@ UsbTransport.SET_LINE_CODING = 0x20;
|
|
|
125
150
|
UsbTransport.SET_CONTROL_LINE_STATE = 0x22;
|
|
126
151
|
export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
127
152
|
const base = MultiDeviceWhisperer(props);
|
|
153
|
+
const releasePortByDefaultRef = useRef(false);
|
|
154
|
+
const [releasePortByDefaultState, setReleasePortByDefaultState] = useState(false);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
releasePortByDefaultRef.current = releasePortByDefaultState;
|
|
157
|
+
}, [releasePortByDefaultState]);
|
|
128
158
|
// --- Message Processing (Identical logic, just copied over) ---
|
|
129
159
|
const defaultOnReceive = (uuid, data) => {
|
|
130
160
|
const conn = base.getConnection(uuid);
|
|
@@ -140,7 +170,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
140
170
|
bytes = data;
|
|
141
171
|
const asText = decoder.decode(bytes);
|
|
142
172
|
const combined = conn.readBufferLeftover + asText;
|
|
143
|
-
const lines = combined.split("\
|
|
173
|
+
const lines = combined.split("\n");
|
|
144
174
|
base.updateConnection(uuid, (c) => ({ ...c, readBufferLeftover: lines.pop() || "" }));
|
|
145
175
|
for (const line of lines) {
|
|
146
176
|
if (line.trim()) {
|
|
@@ -281,7 +311,7 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
281
311
|
}
|
|
282
312
|
base.updateConnection(uuid, c => ({
|
|
283
313
|
...c,
|
|
284
|
-
transport: null,
|
|
314
|
+
transport: releasePortByDefaultRef.current ? null : c.transport,
|
|
285
315
|
isConnected: false,
|
|
286
316
|
isConnecting: false
|
|
287
317
|
}));
|
|
@@ -321,7 +351,17 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
321
351
|
await disconnect(uuid);
|
|
322
352
|
base.removeConnection(uuid);
|
|
323
353
|
};
|
|
324
|
-
const reconnectAll = async () => {
|
|
354
|
+
const reconnectAll = async (...connectionProps) => {
|
|
355
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
356
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
357
|
+
const c = base.getConnection(id);
|
|
358
|
+
if (!c)
|
|
359
|
+
return;
|
|
360
|
+
await disconnect(c.uuid);
|
|
361
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
362
|
+
return connect(c.uuid, ...connectionProps);
|
|
363
|
+
}));
|
|
364
|
+
};
|
|
325
365
|
useEffect(() => { base.setIsReady(true); }, []);
|
|
326
366
|
return {
|
|
327
367
|
...base,
|
|
@@ -329,6 +369,8 @@ export function SerialMultiDeviceWhisperer({ ...props } = {}) {
|
|
|
329
369
|
removeConnection,
|
|
330
370
|
connect,
|
|
331
371
|
disconnect,
|
|
332
|
-
reconnectAll
|
|
372
|
+
reconnectAll,
|
|
373
|
+
releasePortByDefault: releasePortByDefaultState,
|
|
374
|
+
setReleasePortByDefault: setReleasePortByDefaultState
|
|
333
375
|
};
|
|
334
376
|
}
|
|
@@ -18,9 +18,8 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
|
|
|
18
18
|
connect: (uuid: string, attempt?: number) => Promise<void>;
|
|
19
19
|
disconnect: (uuid: string) => Promise<void>;
|
|
20
20
|
checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
|
|
21
|
-
reconnectAll: () => Promise<void>;
|
|
21
|
+
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
22
22
|
connections: AppOrMessageLayer[];
|
|
23
|
-
connectionsRef: import("react").RefObject<AppOrMessageLayer[]>;
|
|
24
23
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
25
24
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
26
25
|
getConnection: (uuid: string) => AppOrMessageLayer | undefined;
|
|
@@ -19,7 +19,7 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
19
19
|
}
|
|
20
20
|
const asText = decoder.decode(bytes);
|
|
21
21
|
const combined = conn.readBufferLeftover + asText;
|
|
22
|
-
const lines = combined.split(
|
|
22
|
+
const lines = combined.split(/\r?\n/);
|
|
23
23
|
base.updateConnection(uuid, (c) => ({
|
|
24
24
|
...c,
|
|
25
25
|
readBufferLeftover: lines.pop() || ""
|
|
@@ -173,14 +173,16 @@ export function WebsocketMultiDeviceWhisperer({ server_url, server_port, ...prop
|
|
|
173
173
|
return [];
|
|
174
174
|
}
|
|
175
175
|
};
|
|
176
|
-
const reconnectAll = async () => {
|
|
177
|
-
|
|
176
|
+
const reconnectAll = async (...connectionProps) => {
|
|
177
|
+
const connectionIds = base.connections.map(c => c.uuid);
|
|
178
|
+
await Promise.all(connectionIds.map(async (id) => {
|
|
179
|
+
const c = base.getConnection(id);
|
|
180
|
+
if (!c)
|
|
181
|
+
return;
|
|
178
182
|
await disconnect(c.uuid);
|
|
179
183
|
await new Promise((res) => setTimeout(res, 250));
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await connect(c.uuid);
|
|
183
|
-
}
|
|
184
|
+
return connect(c.uuid, ...connectionProps);
|
|
185
|
+
}));
|
|
184
186
|
};
|
|
185
187
|
useEffect(() => {
|
|
186
188
|
base.setIsReady(true); // Ready on page load by default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ota-hub-reactjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "ReactJS tools for building web apps to flash MCU devices such as esp32, brought to you by OTA Hub.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"esptool-js": "^0.5.5",
|
|
35
35
|
"mqtt": "^5.14.1",
|
|
36
|
-
"unique-names-generator": "^4.7.1",
|
|
37
36
|
"uuid": "^11.1.0"
|
|
38
37
|
},
|
|
39
38
|
"peerDependencies": {
|