esp32tool 1.2.0 → 1.3.0
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/apple-touch-icon.png +0 -0
- package/css/style.css +38 -8
- package/dist/console.d.ts +15 -0
- package/dist/console.js +237 -0
- package/dist/const.d.ts +99 -0
- package/dist/const.js +129 -8
- package/dist/esp_loader.d.ts +119 -3
- package/dist/esp_loader.js +797 -48
- package/dist/util/console-color.d.ts +19 -0
- package/dist/util/console-color.js +272 -0
- package/dist/util/line-break-transformer.d.ts +5 -0
- package/dist/util/line-break-transformer.js +17 -0
- package/dist/web/index.js +1 -1
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +62 -22
- package/js/console.js +269 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +586 -16
- package/js/util/console-color.js +282 -0
- package/js/util/line-break-transformer.js +19 -0
- package/package.cli.json +1 -1
- package/package.json +9 -8
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/console.ts +278 -0
- package/src/const.ts +165 -8
- package/src/esp_loader.ts +971 -50
- package/src/util/console-color.ts +290 -0
- package/src/util/line-break-transformer.ts +20 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
export class ColoredConsole {
|
|
2
|
+
constructor(targetElement) {
|
|
3
|
+
this.targetElement = targetElement;
|
|
4
|
+
this.state = {
|
|
5
|
+
bold: false,
|
|
6
|
+
italic: false,
|
|
7
|
+
underline: false,
|
|
8
|
+
strikethrough: false,
|
|
9
|
+
foregroundColor: null,
|
|
10
|
+
backgroundColor: null,
|
|
11
|
+
carriageReturn: false,
|
|
12
|
+
secret: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
logs() {
|
|
17
|
+
return this.targetElement.innerText;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
addLine(line) {
|
|
21
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
|
|
22
|
+
const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g;
|
|
23
|
+
let i = 0;
|
|
24
|
+
|
|
25
|
+
if (this.state.carriageReturn) {
|
|
26
|
+
if (line !== "\n") {
|
|
27
|
+
// don't remove if \r\n
|
|
28
|
+
if (this.targetElement.lastChild) {
|
|
29
|
+
this.targetElement.removeChild(this.targetElement.lastChild);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.state.carriageReturn = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (line.includes("\r")) {
|
|
36
|
+
this.state.carriageReturn = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lineSpan = document.createElement("span");
|
|
40
|
+
lineSpan.classList.add("line");
|
|
41
|
+
this.targetElement.appendChild(lineSpan);
|
|
42
|
+
|
|
43
|
+
const addSpan = (content) => {
|
|
44
|
+
if (content === "") return;
|
|
45
|
+
|
|
46
|
+
const span = document.createElement("span");
|
|
47
|
+
if (this.state.bold) span.classList.add("log-bold");
|
|
48
|
+
if (this.state.italic) span.classList.add("log-italic");
|
|
49
|
+
if (this.state.underline) span.classList.add("log-underline");
|
|
50
|
+
if (this.state.strikethrough) span.classList.add("log-strikethrough");
|
|
51
|
+
if (this.state.secret) span.classList.add("log-secret");
|
|
52
|
+
if (this.state.foregroundColor !== null)
|
|
53
|
+
span.classList.add(`log-fg-${this.state.foregroundColor}`);
|
|
54
|
+
if (this.state.backgroundColor !== null)
|
|
55
|
+
span.classList.add(`log-bg-${this.state.backgroundColor}`);
|
|
56
|
+
span.appendChild(document.createTextNode(content));
|
|
57
|
+
lineSpan.appendChild(span);
|
|
58
|
+
|
|
59
|
+
if (this.state.secret) {
|
|
60
|
+
const redacted = document.createElement("span");
|
|
61
|
+
redacted.classList.add("log-secret-redacted");
|
|
62
|
+
redacted.appendChild(document.createTextNode("[redacted]"));
|
|
63
|
+
lineSpan.appendChild(redacted);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
while (true) {
|
|
68
|
+
const match = re.exec(line);
|
|
69
|
+
if (match === null) break;
|
|
70
|
+
|
|
71
|
+
const j = match.index;
|
|
72
|
+
addSpan(line.substring(i, j));
|
|
73
|
+
i = j + match[0].length;
|
|
74
|
+
|
|
75
|
+
if (match[1] === undefined) continue;
|
|
76
|
+
|
|
77
|
+
for (const colorCode of match[1].split(";")) {
|
|
78
|
+
switch (parseInt(colorCode)) {
|
|
79
|
+
case 0:
|
|
80
|
+
// reset
|
|
81
|
+
this.state.bold = false;
|
|
82
|
+
this.state.italic = false;
|
|
83
|
+
this.state.underline = false;
|
|
84
|
+
this.state.strikethrough = false;
|
|
85
|
+
this.state.foregroundColor = null;
|
|
86
|
+
this.state.backgroundColor = null;
|
|
87
|
+
this.state.secret = false;
|
|
88
|
+
break;
|
|
89
|
+
case 1:
|
|
90
|
+
this.state.bold = true;
|
|
91
|
+
break;
|
|
92
|
+
case 3:
|
|
93
|
+
this.state.italic = true;
|
|
94
|
+
break;
|
|
95
|
+
case 4:
|
|
96
|
+
this.state.underline = true;
|
|
97
|
+
break;
|
|
98
|
+
case 5:
|
|
99
|
+
this.state.secret = true;
|
|
100
|
+
break;
|
|
101
|
+
case 6:
|
|
102
|
+
this.state.secret = false;
|
|
103
|
+
break;
|
|
104
|
+
case 9:
|
|
105
|
+
this.state.strikethrough = true;
|
|
106
|
+
break;
|
|
107
|
+
case 22:
|
|
108
|
+
this.state.bold = false;
|
|
109
|
+
break;
|
|
110
|
+
case 23:
|
|
111
|
+
this.state.italic = false;
|
|
112
|
+
break;
|
|
113
|
+
case 24:
|
|
114
|
+
this.state.underline = false;
|
|
115
|
+
break;
|
|
116
|
+
case 29:
|
|
117
|
+
this.state.strikethrough = false;
|
|
118
|
+
break;
|
|
119
|
+
case 30:
|
|
120
|
+
this.state.foregroundColor = "black";
|
|
121
|
+
break;
|
|
122
|
+
case 31:
|
|
123
|
+
this.state.foregroundColor = "red";
|
|
124
|
+
break;
|
|
125
|
+
case 32:
|
|
126
|
+
this.state.foregroundColor = "green";
|
|
127
|
+
break;
|
|
128
|
+
case 33:
|
|
129
|
+
this.state.foregroundColor = "yellow";
|
|
130
|
+
break;
|
|
131
|
+
case 34:
|
|
132
|
+
this.state.foregroundColor = "blue";
|
|
133
|
+
break;
|
|
134
|
+
case 35:
|
|
135
|
+
this.state.foregroundColor = "magenta";
|
|
136
|
+
break;
|
|
137
|
+
case 36:
|
|
138
|
+
this.state.foregroundColor = "cyan";
|
|
139
|
+
break;
|
|
140
|
+
case 37:
|
|
141
|
+
this.state.foregroundColor = "white";
|
|
142
|
+
break;
|
|
143
|
+
case 39:
|
|
144
|
+
this.state.foregroundColor = null;
|
|
145
|
+
break;
|
|
146
|
+
case 41:
|
|
147
|
+
this.state.backgroundColor = "red";
|
|
148
|
+
break;
|
|
149
|
+
case 42:
|
|
150
|
+
this.state.backgroundColor = "green";
|
|
151
|
+
break;
|
|
152
|
+
case 43:
|
|
153
|
+
this.state.backgroundColor = "yellow";
|
|
154
|
+
break;
|
|
155
|
+
case 44:
|
|
156
|
+
this.state.backgroundColor = "blue";
|
|
157
|
+
break;
|
|
158
|
+
case 45:
|
|
159
|
+
this.state.backgroundColor = "magenta";
|
|
160
|
+
break;
|
|
161
|
+
case 46:
|
|
162
|
+
this.state.backgroundColor = "cyan";
|
|
163
|
+
break;
|
|
164
|
+
case 47:
|
|
165
|
+
this.state.backgroundColor = "white";
|
|
166
|
+
break;
|
|
167
|
+
case 40:
|
|
168
|
+
this.state.backgroundColor = "black";
|
|
169
|
+
break;
|
|
170
|
+
case 49:
|
|
171
|
+
this.state.backgroundColor = null;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Use percentage-based threshold (5% of viewport height) for better UX across screen sizes
|
|
178
|
+
const scrollThreshold = this.targetElement.offsetHeight * 0.05;
|
|
179
|
+
const atBottom =
|
|
180
|
+
this.targetElement.scrollTop >
|
|
181
|
+
this.targetElement.scrollHeight - this.targetElement.offsetHeight - scrollThreshold;
|
|
182
|
+
|
|
183
|
+
addSpan(line.substring(i));
|
|
184
|
+
|
|
185
|
+
// Keep scroll at bottom
|
|
186
|
+
if (atBottom) {
|
|
187
|
+
this.targetElement.scrollTop = this.targetElement.scrollHeight;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const coloredConsoleStyles = `
|
|
193
|
+
.log {
|
|
194
|
+
flex: 1;
|
|
195
|
+
background-color: #1c1c1c;
|
|
196
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
|
197
|
+
monospace;
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
padding: 16px;
|
|
200
|
+
overflow: auto;
|
|
201
|
+
line-height: 1.45;
|
|
202
|
+
border-radius: 3px;
|
|
203
|
+
white-space: pre-wrap;
|
|
204
|
+
overflow-wrap: break-word;
|
|
205
|
+
color: #ddd;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.log-bold {
|
|
209
|
+
font-weight: bold;
|
|
210
|
+
}
|
|
211
|
+
.log-italic {
|
|
212
|
+
font-style: italic;
|
|
213
|
+
}
|
|
214
|
+
.log-underline {
|
|
215
|
+
text-decoration: underline;
|
|
216
|
+
}
|
|
217
|
+
.log-strikethrough {
|
|
218
|
+
text-decoration: line-through;
|
|
219
|
+
}
|
|
220
|
+
.log-underline.log-strikethrough {
|
|
221
|
+
text-decoration: underline line-through;
|
|
222
|
+
}
|
|
223
|
+
.log-secret {
|
|
224
|
+
-webkit-user-select: none;
|
|
225
|
+
-moz-user-select: none;
|
|
226
|
+
-ms-user-select: none;
|
|
227
|
+
user-select: none;
|
|
228
|
+
}
|
|
229
|
+
.log-secret-redacted {
|
|
230
|
+
opacity: 0;
|
|
231
|
+
width: 1px;
|
|
232
|
+
font-size: 1px;
|
|
233
|
+
}
|
|
234
|
+
.log-fg-black {
|
|
235
|
+
color: rgb(128, 128, 128);
|
|
236
|
+
}
|
|
237
|
+
.log-fg-red {
|
|
238
|
+
color: rgb(255, 0, 0);
|
|
239
|
+
}
|
|
240
|
+
.log-fg-green {
|
|
241
|
+
color: rgb(0, 255, 0);
|
|
242
|
+
}
|
|
243
|
+
.log-fg-yellow {
|
|
244
|
+
color: rgb(255, 255, 0);
|
|
245
|
+
}
|
|
246
|
+
.log-fg-blue {
|
|
247
|
+
color: rgb(0, 0, 255);
|
|
248
|
+
}
|
|
249
|
+
.log-fg-magenta {
|
|
250
|
+
color: rgb(255, 0, 255);
|
|
251
|
+
}
|
|
252
|
+
.log-fg-cyan {
|
|
253
|
+
color: rgb(0, 255, 255);
|
|
254
|
+
}
|
|
255
|
+
.log-fg-white {
|
|
256
|
+
color: rgb(187, 187, 187);
|
|
257
|
+
}
|
|
258
|
+
.log-bg-black {
|
|
259
|
+
background-color: rgb(0, 0, 0);
|
|
260
|
+
}
|
|
261
|
+
.log-bg-red {
|
|
262
|
+
background-color: rgb(255, 0, 0);
|
|
263
|
+
}
|
|
264
|
+
.log-bg-green {
|
|
265
|
+
background-color: rgb(0, 255, 0);
|
|
266
|
+
}
|
|
267
|
+
.log-bg-yellow {
|
|
268
|
+
background-color: rgb(255, 255, 0);
|
|
269
|
+
}
|
|
270
|
+
.log-bg-blue {
|
|
271
|
+
background-color: rgb(0, 0, 255);
|
|
272
|
+
}
|
|
273
|
+
.log-bg-magenta {
|
|
274
|
+
background-color: rgb(255, 0, 255);
|
|
275
|
+
}
|
|
276
|
+
.log-bg-cyan {
|
|
277
|
+
background-color: rgb(0, 255, 255);
|
|
278
|
+
}
|
|
279
|
+
.log-bg-white {
|
|
280
|
+
background-color: rgb(255, 255, 255);
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class LineBreakTransformer {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.chunks = "";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
transform(chunk, controller) {
|
|
7
|
+
// Append new chunks to existing chunks.
|
|
8
|
+
this.chunks += chunk;
|
|
9
|
+
// For each line breaks in chunks, send the parsed lines out.
|
|
10
|
+
const lines = this.chunks.split("\r\n");
|
|
11
|
+
this.chunks = lines.pop();
|
|
12
|
+
lines.forEach((line) => controller.enqueue(line + "\r\n"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
flush(controller) {
|
|
16
|
+
// When the stream is closed, flush any remaining chunks out.
|
|
17
|
+
controller.enqueue(this.chunks);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.cli.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esp32tool",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Flash & Read ESP devices using WebSerial, Electron, and also Android mobile via WebUSB",
|
|
6
6
|
"main": "electron/main.cjs",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
50
50
|
"@rollup/plugin-terser": "^0.4.4",
|
|
51
51
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
52
|
-
"@types/node": "^25.0.
|
|
52
|
+
"@types/node": "^25.0.10",
|
|
53
53
|
"@types/pako": "^2.0.4",
|
|
54
|
-
"@types/serialport": "^
|
|
54
|
+
"@types/serialport": "^10.2.0",
|
|
55
55
|
"@types/w3c-web-serial": "^1.0.7",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"electron": "^39.2.5",
|
|
@@ -59,18 +59,19 @@
|
|
|
59
59
|
"eslint": "^9.39.2",
|
|
60
60
|
"eslint-config-prettier": "^10.1.8",
|
|
61
61
|
"eslint-plugin-prettier": "^5.5.4",
|
|
62
|
-
"prettier": "^3.
|
|
63
|
-
"rollup": "^4.
|
|
62
|
+
"prettier": "^3.8.1",
|
|
63
|
+
"rollup": "^4.56.0",
|
|
64
64
|
"serve": "^14.2.4",
|
|
65
65
|
"typescript": "^5.7.3",
|
|
66
|
-
"typescript-eslint": "^8.53.
|
|
66
|
+
"typescript-eslint": "^8.53.1"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"pako": "^2.1.0",
|
|
70
70
|
"tslib": "^2.8.1",
|
|
71
|
-
"usb": "^2.
|
|
71
|
+
"usb": "^2.17.0"
|
|
72
72
|
},
|
|
73
73
|
"overrides": {
|
|
74
|
-
"tmp": "^0.2.4"
|
|
74
|
+
"tmp": "^0.2.4",
|
|
75
|
+
"tar": "^7.5.6"
|
|
75
76
|
}
|
|
76
77
|
}
|
package/screenshots/desktop.png
CHANGED
|
Binary file
|
package/screenshots/mobile.png
CHANGED
|
Binary file
|
package/src/console.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { ColoredConsole, coloredConsoleStyles } from "./util/console-color.js";
|
|
2
|
+
import { LineBreakTransformer } from "./util/line-break-transformer.js";
|
|
3
|
+
|
|
4
|
+
export class ESP32ToolConsole {
|
|
5
|
+
private port: SerialPort;
|
|
6
|
+
private console?: ColoredConsole;
|
|
7
|
+
private cancelConnection?: () => Promise<void>;
|
|
8
|
+
private containerElement: HTMLElement;
|
|
9
|
+
private allowInput: boolean;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
port: SerialPort,
|
|
13
|
+
containerElement: HTMLElement,
|
|
14
|
+
allowInput: boolean = true,
|
|
15
|
+
) {
|
|
16
|
+
this.port = port;
|
|
17
|
+
this.containerElement = containerElement;
|
|
18
|
+
this.allowInput = allowInput;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public logs(): string {
|
|
22
|
+
return this.console?.logs() || "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async init() {
|
|
26
|
+
// Create console HTML
|
|
27
|
+
this.containerElement.innerHTML = `
|
|
28
|
+
<style>
|
|
29
|
+
.esp32tool-console-wrapper {
|
|
30
|
+
background-color: #1c1c1c;
|
|
31
|
+
color: #ddd;
|
|
32
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
|
33
|
+
monospace;
|
|
34
|
+
line-height: 1.45;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
height: 100%;
|
|
38
|
+
border: 1px solid #333;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
}
|
|
41
|
+
.esp32tool-console-header {
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
align-items: center;
|
|
45
|
+
padding: 8px 12px;
|
|
46
|
+
background-color: #2a2a2a;
|
|
47
|
+
border-bottom: 1px solid #333;
|
|
48
|
+
}
|
|
49
|
+
.esp32tool-console-header h3 {
|
|
50
|
+
margin: 0;
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
}
|
|
54
|
+
.esp32tool-console-controls {
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
}
|
|
58
|
+
.esp32tool-console-controls button {
|
|
59
|
+
padding: 4px 12px;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
background-color: #444;
|
|
62
|
+
color: #ddd;
|
|
63
|
+
border: 1px solid #555;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
.esp32tool-console-controls button:hover {
|
|
68
|
+
background-color: #555;
|
|
69
|
+
}
|
|
70
|
+
.esp32tool-console-form {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
padding: 0 8px 0 16px;
|
|
74
|
+
background-color: #1c1c1c;
|
|
75
|
+
border-top: 1px solid #333;
|
|
76
|
+
}
|
|
77
|
+
.esp32tool-console-input {
|
|
78
|
+
flex: 1;
|
|
79
|
+
padding: 8px;
|
|
80
|
+
margin: 4px 8px;
|
|
81
|
+
border: 0;
|
|
82
|
+
outline: none;
|
|
83
|
+
background-color: #1c1c1c;
|
|
84
|
+
color: #ddd;
|
|
85
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
|
86
|
+
monospace;
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
}
|
|
89
|
+
${coloredConsoleStyles}
|
|
90
|
+
.esp32tool-console-wrapper .log {
|
|
91
|
+
flex: 1;
|
|
92
|
+
margin: 0;
|
|
93
|
+
border-radius: 0;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
96
|
+
<div class="esp32tool-console-wrapper">
|
|
97
|
+
<div class="esp32tool-console-header">
|
|
98
|
+
<h3>ESP Console</h3>
|
|
99
|
+
<div class="esp32tool-console-controls">
|
|
100
|
+
<button id="console-clear-btn">Clear</button>
|
|
101
|
+
<button id="console-reset-btn">Reset Device</button>
|
|
102
|
+
<button id="console-close-btn">Close Console</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="log"></div>
|
|
106
|
+
${
|
|
107
|
+
this.allowInput
|
|
108
|
+
? `<form class="esp32tool-console-form">
|
|
109
|
+
<input class="esp32tool-console-input" autofocus placeholder="Type command and press Enter...">
|
|
110
|
+
</form>`
|
|
111
|
+
: ""
|
|
112
|
+
}
|
|
113
|
+
</div>
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
this.console = new ColoredConsole(
|
|
117
|
+
this.containerElement.querySelector(".log")!,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Setup event listeners
|
|
121
|
+
const clearBtn = this.containerElement.querySelector("#console-clear-btn");
|
|
122
|
+
if (clearBtn) {
|
|
123
|
+
clearBtn.addEventListener("click", () => this.clear());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const resetBtn = this.containerElement.querySelector("#console-reset-btn");
|
|
127
|
+
if (resetBtn) {
|
|
128
|
+
resetBtn.addEventListener("click", () => this.reset());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const closeBtn = this.containerElement.querySelector("#console-close-btn");
|
|
132
|
+
if (closeBtn) {
|
|
133
|
+
closeBtn.addEventListener("click", () => {
|
|
134
|
+
this.containerElement.dispatchEvent(
|
|
135
|
+
new CustomEvent("console-close", { bubbles: true }),
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.allowInput) {
|
|
141
|
+
const input = this.containerElement.querySelector<HTMLInputElement>(
|
|
142
|
+
".esp32tool-console-input",
|
|
143
|
+
)!;
|
|
144
|
+
|
|
145
|
+
this.containerElement.addEventListener("click", () => {
|
|
146
|
+
// Only focus input if user didn't select some text
|
|
147
|
+
if (getSelection()?.toString() === "") {
|
|
148
|
+
input.focus();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const form = this.containerElement.querySelector("form");
|
|
153
|
+
if (form) {
|
|
154
|
+
form.addEventListener("submit", (ev) => {
|
|
155
|
+
ev.preventDefault();
|
|
156
|
+
ev.stopPropagation();
|
|
157
|
+
this._sendCommand();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
input.addEventListener("keydown", (ev) => {
|
|
162
|
+
if (ev.key === "Enter") {
|
|
163
|
+
ev.preventDefault();
|
|
164
|
+
ev.stopPropagation();
|
|
165
|
+
this._sendCommand();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Start connection
|
|
171
|
+
const abortController = new AbortController();
|
|
172
|
+
const connection = this._connect(abortController.signal);
|
|
173
|
+
this.cancelConnection = () => {
|
|
174
|
+
abortController.abort();
|
|
175
|
+
return connection;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async _connect(abortSignal: AbortSignal) {
|
|
180
|
+
console.log("Starting console read loop");
|
|
181
|
+
|
|
182
|
+
// Check if port.readable is available
|
|
183
|
+
if (!this.port.readable) {
|
|
184
|
+
this.console!.addLine("");
|
|
185
|
+
this.console!.addLine("");
|
|
186
|
+
this.console!.addLine(
|
|
187
|
+
`Terminal disconnected: Port readable stream not available`,
|
|
188
|
+
);
|
|
189
|
+
console.error(
|
|
190
|
+
"Port readable stream not available - port may need to be reopened at correct baudrate",
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await this.port
|
|
197
|
+
.readable!.pipeThrough(
|
|
198
|
+
new TextDecoderStream() as ReadableWritablePair<string, Uint8Array>,
|
|
199
|
+
{
|
|
200
|
+
signal: abortSignal,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
|
204
|
+
.pipeTo(
|
|
205
|
+
new WritableStream({
|
|
206
|
+
write: (chunk) => {
|
|
207
|
+
const cleaned = chunk.replace(/\r\n$/, "\n");
|
|
208
|
+
this.console!.addLine(cleaned);
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
if (!abortSignal.aborted) {
|
|
213
|
+
this.console!.addLine("");
|
|
214
|
+
this.console!.addLine("");
|
|
215
|
+
this.console!.addLine("Terminal disconnected");
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
this.console!.addLine("");
|
|
219
|
+
this.console!.addLine("");
|
|
220
|
+
this.console!.addLine(`Terminal disconnected: ${e}`);
|
|
221
|
+
} finally {
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
223
|
+
console.log("Finished console read loop");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private async _sendCommand() {
|
|
228
|
+
const input = this.containerElement.querySelector<HTMLInputElement>(
|
|
229
|
+
".esp32tool-console-input",
|
|
230
|
+
)!;
|
|
231
|
+
const command = input.value;
|
|
232
|
+
if (!this.port.writable) {
|
|
233
|
+
this.console!.addLine("Terminal disconnected: port not writable");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const encoder = new TextEncoder();
|
|
237
|
+
let writer: WritableStreamDefaultWriter<Uint8Array> | undefined;
|
|
238
|
+
try {
|
|
239
|
+
writer = this.port.writable!.getWriter();
|
|
240
|
+
await writer.write(encoder.encode(command + "\r\n"));
|
|
241
|
+
this.console!.addLine(`> ${command}`);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
this.console!.addLine(`Write failed: ${err}`);
|
|
244
|
+
} finally {
|
|
245
|
+
if (writer) {
|
|
246
|
+
try {
|
|
247
|
+
writer.releaseLock();
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error("Ignoring release lock error", err);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
input.value = "";
|
|
254
|
+
input.focus();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public clear() {
|
|
258
|
+
const logElement = this.containerElement.querySelector(".log");
|
|
259
|
+
if (logElement) {
|
|
260
|
+
logElement.innerHTML = "";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public async reset() {
|
|
265
|
+
console.log("Reset device requested from console");
|
|
266
|
+
// Don't use addLine here as stream might already be closed
|
|
267
|
+
// This will be called from script.js with proper reset logic
|
|
268
|
+
const event = new CustomEvent("console-reset");
|
|
269
|
+
this.containerElement.dispatchEvent(event);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public async disconnect() {
|
|
273
|
+
if (this.cancelConnection) {
|
|
274
|
+
await this.cancelConnection();
|
|
275
|
+
this.cancelConnection = undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|