esp32tool 1.6.3 → 1.6.5

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
@@ -137,6 +137,8 @@ January 2026 – Added Android mobile devices support, standalone CLI.
137
137
 
138
138
  February 2026 - Added IMPROV support, NEW Features: Flash Hex Editor and NVS Parser / Editor
139
139
 
140
+ March 2026 – Auto-detect partition table, flash read speed/time logging, flash erase animation
141
+
140
142
  ---
141
143
 
142
144
  © Adafruit, Nabu Casa & Johann Obermeier
Binary file
package/css/style.css CHANGED
@@ -472,6 +472,29 @@ div.clear {
472
472
  width: 0;
473
473
  }
474
474
 
475
+ .progress-bar.indeterminate > div {
476
+ width: 100% !important;
477
+ background: repeating-linear-gradient(
478
+ -45deg,
479
+ #71ae1e,
480
+ #71ae1e 10px,
481
+ #5a8c18 10px,
482
+ #5a8c18 20px
483
+ );
484
+ background-size: 28px 28px;
485
+ animation: indeterminate-stripes 0.6s linear infinite;
486
+ }
487
+
488
+ @keyframes indeterminate-stripes {
489
+ 0% { background-position: 0 0; }
490
+ 100% { background-position: 28px 0; }
491
+ }
492
+
493
+ #eraseProgress {
494
+ width: 400px;
495
+ margin: 10px auto;
496
+ }
497
+
475
498
  #commands .buttons {
476
499
  display: flex;
477
500
  justify-content: center;
package/dist/cli.js CHANGED
@@ -291,8 +291,19 @@ async function cmdEraseFlash(esploader) {
291
291
  cliLogger.log("Erasing entire flash chip...");
292
292
  // Use stub for erasing
293
293
  const stub = await esploader.runStub();
294
- // Erase flash
295
- await stub.eraseFlash();
294
+ // Show animated progress while erasing
295
+ const frames = ["|", "/", "-", "\\"];
296
+ let frameIdx = 0;
297
+ const spinner = setInterval(() => {
298
+ process.stdout.write(`\rErasing... ${frames[frameIdx++ % frames.length]}`);
299
+ }, 200);
300
+ try {
301
+ await stub.eraseFlash();
302
+ }
303
+ finally {
304
+ clearInterval(spinner);
305
+ process.stdout.write("\r \r");
306
+ }
296
307
  cliLogger.log("Erase complete!");
297
308
  }
298
309
  async function cmdEraseRegion(esploader, offset, size) {
package/dist/console.d.ts CHANGED
@@ -4,10 +4,14 @@ export declare class ESP32ToolConsole {
4
4
  private cancelConnection?;
5
5
  private containerElement;
6
6
  private allowInput;
7
+ private commandHistory;
8
+ private historyIndex;
9
+ private currentInput;
7
10
  constructor(port: SerialPort, containerElement: HTMLElement, allowInput?: boolean);
8
11
  logs(): string;
9
12
  init(): Promise<void>;
10
13
  private _connect;
14
+ private _navigateHistory;
11
15
  private _sendCommand;
12
16
  clear(): void;
13
17
  reset(): Promise<void>;
package/dist/console.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import { ColoredConsole, coloredConsoleStyles } from "./util/console-color.js";
2
2
  import { LineBreakTransformer } from "./util/line-break-transformer.js";
3
+ import { TimestampTransformer } from "./util/timestamp-transformer.js";
3
4
  export class ESP32ToolConsole {
4
5
  constructor(port, containerElement, allowInput = true) {
6
+ // Command history buffer
7
+ this.commandHistory = [];
8
+ this.historyIndex = -1;
9
+ this.currentInput = "";
5
10
  this.port = port;
6
11
  this.containerElement = containerElement;
7
12
  this.allowInput = allowInput;
@@ -137,6 +142,20 @@ export class ESP32ToolConsole {
137
142
  ev.stopPropagation();
138
143
  this._sendCommand();
139
144
  }
145
+ else if (ev.key === "ArrowUp") {
146
+ ev.preventDefault();
147
+ this._navigateHistory(1, input);
148
+ }
149
+ else if (ev.key === "ArrowDown") {
150
+ ev.preventDefault();
151
+ this._navigateHistory(-1, input);
152
+ }
153
+ else {
154
+ // User is editing — reset history navigation to live input
155
+ if (this.historyIndex !== -1) {
156
+ this.historyIndex = -1;
157
+ }
158
+ }
140
159
  });
141
160
  }
142
161
  // Start connection
@@ -174,10 +193,10 @@ export class ESP32ToolConsole {
174
193
  signal: abortSignal,
175
194
  })
176
195
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
196
+ .pipeThrough(new TransformStream(new TimestampTransformer()))
177
197
  .pipeTo(new WritableStream({
178
198
  write: (chunk) => {
179
- const cleaned = chunk.replace(/\r\n$/, "\n");
180
- this.console.addLine(cleaned);
199
+ this.console.addLine(chunk);
181
200
  },
182
201
  }));
183
202
  if (!abortSignal.aborted) {
@@ -196,9 +215,42 @@ export class ESP32ToolConsole {
196
215
  console.log("Finished console read loop");
197
216
  }
198
217
  }
218
+ _navigateHistory(direction, input) {
219
+ if (this.commandHistory.length === 0)
220
+ return;
221
+ // Save current unsent input before navigating away
222
+ if (this.historyIndex === -1) {
223
+ this.currentInput = input.value;
224
+ }
225
+ const nextIndex = this.historyIndex + direction;
226
+ if (nextIndex < 0) {
227
+ // Back to unsent draft
228
+ this.historyIndex = -1;
229
+ input.value = this.currentInput;
230
+ }
231
+ else if (nextIndex < this.commandHistory.length) {
232
+ this.historyIndex = nextIndex;
233
+ input.value = this.commandHistory[this.historyIndex];
234
+ }
235
+ // Move cursor to end
236
+ const len = input.value.length;
237
+ input.setSelectionRange(len, len);
238
+ }
199
239
  async _sendCommand() {
200
240
  const input = this.containerElement.querySelector(".esp32tool-console-input");
201
241
  const command = input.value;
242
+ if (command.trim() !== "") {
243
+ // Avoid consecutive duplicates, cap at 100
244
+ if (this.commandHistory[0] !== command) {
245
+ this.commandHistory.unshift(command);
246
+ if (this.commandHistory.length > 100) {
247
+ this.commandHistory.pop();
248
+ }
249
+ }
250
+ }
251
+ // Reset history navigation state
252
+ this.historyIndex = -1;
253
+ this.currentInput = "";
202
254
  if (!this.port.writable) {
203
255
  this.console.addLine("Terminal disconnected: port not writable");
204
256
  return;
@@ -3491,6 +3491,7 @@ export class ESPLoader extends EventTarget {
3491
3491
  }
3492
3492
  // Flush serial buffers before flash read operation
3493
3493
  await this.flushSerialBuffers();
3494
+ const readStartTime = Date.now();
3494
3495
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
3495
3496
  // Initialize adaptive speed multipliers for WebUSB devices
3496
3497
  if (this.isWebUSB()) {
@@ -3571,6 +3572,7 @@ export class ESPLoader extends EventTarget {
3571
3572
  maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
3572
3573
  }
3573
3574
  const pkt = pack("<IIII", currentAddr, chunkSize, blockSize, maxInFlight);
3575
+ const chunkStartTime = Date.now();
3574
3576
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
3575
3577
  if (res != 0) {
3576
3578
  throw new Error("Failed to read memory: " + res);
@@ -3634,6 +3636,11 @@ export class ESPLoader extends EventTarget {
3634
3636
  newAllData.set(resp, allData.length);
3635
3637
  allData = newAllData;
3636
3638
  chunkSuccess = true;
3639
+ const chunkDuration = Date.now() - chunkStartTime;
3640
+ const speedKBs = (resp.length /
3641
+ 1024 /
3642
+ (chunkDuration / 1000)).toFixed(1);
3643
+ this.logger.debug(`Chunk read took ${chunkDuration} ms (${resp.length} bytes, ${speedKBs} KB/s)`);
3637
3644
  // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
3638
3645
  // Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
3639
3646
  if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
@@ -3734,6 +3741,11 @@ export class ESPLoader extends EventTarget {
3734
3741
  remainingSize -= chunkSize;
3735
3742
  this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
3736
3743
  }
3744
+ const totalDuration = Date.now() - readStartTime;
3745
+ const totalSpeedKBs = (allData.length /
3746
+ 1024 /
3747
+ (totalDuration / 1000)).toFixed(1);
3748
+ this.logger.log(`Read complete: ${allData.length} bytes in ${(totalDuration / 1000).toFixed(1)} s (${totalSpeedKBs} KB/s)`);
3737
3749
  return allData;
3738
3750
  }
3739
3751
  }
@@ -6,14 +6,19 @@ interface ConsoleState {
6
6
  foregroundColor: string | null;
7
7
  backgroundColor: string | null;
8
8
  carriageReturn: boolean;
9
+ lines: string[];
9
10
  secret: boolean;
11
+ blink: boolean;
12
+ rapidBlink: boolean;
10
13
  }
11
14
  export declare class ColoredConsole {
12
15
  targetElement: HTMLElement;
13
16
  state: ConsoleState;
14
17
  constructor(targetElement: HTMLElement);
15
18
  logs(): string;
19
+ processLine(line: string): Element;
20
+ processLines(): void;
16
21
  addLine(line: string): void;
17
22
  }
18
- export declare const coloredConsoleStyles = "\n .log {\n flex: 1;\n background-color: #1c1c1c;\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier,\n monospace;\n font-size: 12px;\n padding: 16px;\n overflow: auto;\n line-height: 1.45;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n color: #ddd;\n }\n\n .log-bold {\n font-weight: bold;\n }\n .log-italic {\n font-style: italic;\n }\n .log-underline {\n text-decoration: underline;\n }\n .log-strikethrough {\n text-decoration: line-through;\n }\n .log-underline.log-strikethrough {\n text-decoration: underline line-through;\n }\n .log-secret {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n }\n .log-secret-redacted {\n opacity: 0;\n width: 1px;\n font-size: 1px;\n }\n .log-fg-black {\n color: rgb(128, 128, 128);\n }\n .log-fg-red {\n color: rgb(255, 0, 0);\n }\n .log-fg-green {\n color: rgb(0, 255, 0);\n }\n .log-fg-yellow {\n color: rgb(255, 255, 0);\n }\n .log-fg-blue {\n color: rgb(0, 0, 255);\n }\n .log-fg-magenta {\n color: rgb(255, 0, 255);\n }\n .log-fg-cyan {\n color: rgb(0, 255, 255);\n }\n .log-fg-white {\n color: rgb(187, 187, 187);\n }\n .log-bg-black {\n background-color: rgb(0, 0, 0);\n }\n .log-bg-red {\n background-color: rgb(255, 0, 0);\n }\n .log-bg-green {\n background-color: rgb(0, 255, 0);\n }\n .log-bg-yellow {\n background-color: rgb(255, 255, 0);\n }\n .log-bg-blue {\n background-color: rgb(0, 0, 255);\n }\n .log-bg-magenta {\n background-color: rgb(255, 0, 255);\n }\n .log-bg-cyan {\n background-color: rgb(0, 255, 255);\n }\n .log-bg-white {\n background-color: rgb(255, 255, 255);\n }\n";
23
+ export declare const coloredConsoleStyles = "\n .log {\n flex: 1;\n background-color: #1c1c1c;\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier,\n monospace;\n font-size: 12px;\n padding: 16px;\n overflow: auto;\n line-height: 1.45;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n color: #ddd;\n }\n\n .log-bold {\n font-weight: bold;\n }\n .log-italic {\n font-style: italic;\n }\n .log-underline {\n text-decoration: underline;\n }\n .log-strikethrough {\n text-decoration: line-through;\n }\n .log-underline.log-strikethrough {\n text-decoration: underline line-through;\n }\n .log-blink {\n animation: blink 1s step-end infinite;\n }\n .log-rapid-blink {\n animation: blink 0.4s step-end infinite;\n }\n @keyframes blink {\n 50% {\n opacity: 0;\n }\n }\n .log-secret {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n }\n .log-secret-redacted {\n opacity: 0;\n width: 1px;\n font-size: 1px;\n }\n .log-fg-black {\n color: rgb(128, 128, 128);\n }\n .log-fg-red {\n color: rgb(255, 0, 0);\n }\n .log-fg-green {\n color: rgb(0, 255, 0);\n }\n .log-fg-yellow {\n color: rgb(255, 255, 0);\n }\n .log-fg-blue {\n color: rgb(0, 0, 255);\n }\n .log-fg-magenta {\n color: rgb(255, 0, 255);\n }\n .log-fg-cyan {\n color: rgb(0, 255, 255);\n }\n .log-fg-white {\n color: rgb(187, 187, 187);\n }\n .log-bg-black {\n background-color: rgb(0, 0, 0);\n }\n .log-bg-red {\n background-color: rgb(255, 0, 0);\n }\n .log-bg-green {\n background-color: rgb(0, 255, 0);\n }\n .log-bg-yellow {\n background-color: rgb(255, 255, 0);\n }\n .log-bg-blue {\n background-color: rgb(0, 0, 255);\n }\n .log-bg-magenta {\n background-color: rgb(255, 0, 255);\n }\n .log-bg-cyan {\n background-color: rgb(0, 255, 255);\n }\n .log-bg-white {\n background-color: rgb(255, 255, 255);\n }\n";
19
24
  export {};
@@ -9,33 +9,25 @@ export class ColoredConsole {
9
9
  foregroundColor: null,
10
10
  backgroundColor: null,
11
11
  carriageReturn: false,
12
+ lines: [],
12
13
  secret: false,
14
+ blink: false,
15
+ rapidBlink: false,
13
16
  };
14
17
  }
15
18
  logs() {
19
+ if (this.state.lines.length > 0) {
20
+ this.processLines();
21
+ }
16
22
  return this.targetElement.innerText;
17
23
  }
18
- addLine(line) {
24
+ processLine(line) {
19
25
  // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
20
26
  // eslint-disable-next-line no-control-regex
21
27
  const re = /(?:\x1B|\\x1B)(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1B\\))/g;
22
28
  let i = 0;
23
- if (this.state.carriageReturn) {
24
- if (line !== "\n") {
25
- // don't remove if \r\n
26
- if (this.targetElement.lastChild) {
27
- this.targetElement.removeChild(this.targetElement.lastChild);
28
- }
29
- }
30
- this.state.carriageReturn = false;
31
- }
32
- const hasBareCR = line.endsWith("\r") && !line.endsWith("\r\n");
33
- if (hasBareCR) {
34
- this.state.carriageReturn = true;
35
- }
36
29
  const lineSpan = document.createElement("span");
37
30
  lineSpan.classList.add("line");
38
- this.targetElement.appendChild(lineSpan);
39
31
  const addSpan = (content) => {
40
32
  if (content === "")
41
33
  return;
@@ -50,6 +42,10 @@ export class ColoredConsole {
50
42
  span.classList.add("log-strikethrough");
51
43
  if (this.state.secret)
52
44
  span.classList.add("log-secret");
45
+ if (this.state.blink)
46
+ span.classList.add("log-blink");
47
+ if (this.state.rapidBlink)
48
+ span.classList.add("log-rapid-blink");
53
49
  if (this.state.foregroundColor !== null)
54
50
  span.classList.add(`log-fg-${this.state.foregroundColor}`);
55
51
  if (this.state.backgroundColor !== null)
@@ -83,6 +79,8 @@ export class ColoredConsole {
83
79
  this.state.foregroundColor = null;
84
80
  this.state.backgroundColor = null;
85
81
  this.state.secret = false;
82
+ this.state.blink = false;
83
+ this.state.rapidBlink = false;
86
84
  break;
87
85
  case 1:
88
86
  this.state.bold = true;
@@ -94,10 +92,15 @@ export class ColoredConsole {
94
92
  this.state.underline = true;
95
93
  break;
96
94
  case 5:
97
- this.state.secret = true;
95
+ this.state.blink = true;
96
+ this.state.rapidBlink = false;
98
97
  break;
99
98
  case 6:
100
- this.state.secret = false;
99
+ this.state.rapidBlink = true;
100
+ this.state.blink = false;
101
+ break;
102
+ case 8:
103
+ this.state.secret = true;
101
104
  break;
102
105
  case 9:
103
106
  this.state.strikethrough = true;
@@ -111,6 +114,13 @@ export class ColoredConsole {
111
114
  case 24:
112
115
  this.state.underline = false;
113
116
  break;
117
+ case 25:
118
+ this.state.blink = false;
119
+ this.state.rapidBlink = false;
120
+ break;
121
+ case 28:
122
+ this.state.secret = false;
123
+ break;
114
124
  case 29:
115
125
  this.state.strikethrough = false;
116
126
  break;
@@ -141,6 +151,9 @@ export class ColoredConsole {
141
151
  case 39:
142
152
  this.state.foregroundColor = null;
143
153
  break;
154
+ case 40:
155
+ this.state.backgroundColor = "black";
156
+ break;
144
157
  case 41:
145
158
  this.state.backgroundColor = "red";
146
159
  break;
@@ -162,23 +175,60 @@ export class ColoredConsole {
162
175
  case 47:
163
176
  this.state.backgroundColor = "white";
164
177
  break;
165
- case 40:
166
- this.state.backgroundColor = "black";
167
- break;
168
178
  case 49:
169
179
  this.state.backgroundColor = null;
170
180
  break;
171
181
  }
172
182
  }
173
183
  }
184
+ addSpan(line.substring(i));
185
+ return lineSpan;
186
+ }
187
+ processLines() {
174
188
  const atBottom = this.targetElement.scrollTop >
175
189
  this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
176
- addSpan(line.substring(i));
190
+ const prevCarriageReturn = this.state.carriageReturn;
191
+ const fragment = document.createDocumentFragment();
192
+ if (this.state.lines.length === 0) {
193
+ return;
194
+ }
195
+ for (const line of this.state.lines) {
196
+ // A lone \r is a pure carriage-return signal — update state but don't
197
+ // create a DOM node for it (it has no renderable content).
198
+ if (line === "\r") {
199
+ this.state.carriageReturn = true;
200
+ continue;
201
+ }
202
+ if (this.state.carriageReturn && line !== "\n") {
203
+ if (fragment.childElementCount) {
204
+ fragment.removeChild(fragment.lastChild);
205
+ }
206
+ }
207
+ const hadCarriageReturn = line.endsWith("\r");
208
+ fragment.appendChild(this.processLine(line.replace(/\r/g, "")));
209
+ this.state.carriageReturn = hadCarriageReturn;
210
+ }
211
+ if (prevCarriageReturn &&
212
+ fragment.childElementCount > 0 &&
213
+ this.targetElement.lastChild) {
214
+ this.targetElement.replaceChild(fragment, this.targetElement.lastChild);
215
+ }
216
+ else {
217
+ this.targetElement.appendChild(fragment);
218
+ }
219
+ this.state.lines = [];
177
220
  // Keep scroll at bottom
178
221
  if (atBottom) {
179
222
  this.targetElement.scrollTop = this.targetElement.scrollHeight;
180
223
  }
181
224
  }
225
+ addLine(line) {
226
+ // Processing of lines is deferred for performance reasons
227
+ if (this.state.lines.length === 0) {
228
+ setTimeout(() => this.processLines(), 0);
229
+ }
230
+ this.state.lines.push(line);
231
+ }
182
232
  }
183
233
  export const coloredConsoleStyles = `
184
234
  .log {
@@ -211,6 +261,17 @@ export const coloredConsoleStyles = `
211
261
  .log-underline.log-strikethrough {
212
262
  text-decoration: underline line-through;
213
263
  }
264
+ .log-blink {
265
+ animation: blink 1s step-end infinite;
266
+ }
267
+ .log-rapid-blink {
268
+ animation: blink 0.4s step-end infinite;
269
+ }
270
+ @keyframes blink {
271
+ 50% {
272
+ opacity: 0;
273
+ }
274
+ }
214
275
  .log-secret {
215
276
  -webkit-user-select: none;
216
277
  -moz-user-select: none;
@@ -5,10 +5,24 @@ export class LineBreakTransformer {
5
5
  transform(chunk, controller) {
6
6
  // Append new chunks to existing chunks.
7
7
  this.chunks += chunk;
8
- // For each line breaks in chunks, send the parsed lines out.
9
- const lines = this.chunks.split("\r\n");
10
- this.chunks = lines.pop();
11
- lines.forEach((line) => controller.enqueue(line + "\r\n"));
8
+ // Split on \r\n, lone \r, or lone \n — capturing the separator so we can
9
+ // distinguish a lone \r (overwrite intent) from a normal newline.
10
+ const re = /\r\n|\r|\n/g;
11
+ let lastIndex = 0;
12
+ let match;
13
+ while ((match = re.exec(this.chunks)) !== null) {
14
+ // If this is a lone \r at the very end of the buffer, leave it so it can
15
+ // be combined with a possible following \n in the next chunk.
16
+ if (match[0] === "\r" && match.index === this.chunks.length - 1) {
17
+ break;
18
+ }
19
+ const line = this.chunks.substring(lastIndex, match.index);
20
+ // Emit with \r suffix only for lone \r (overwrite), \n for everything else.
21
+ const suffix = match[0] === "\r" ? "\r" : "\n";
22
+ controller.enqueue(line + suffix);
23
+ lastIndex = re.lastIndex;
24
+ }
25
+ this.chunks = this.chunks.substring(lastIndex);
12
26
  }
13
27
  flush(controller) {
14
28
  // When the stream is closed, flush any remaining chunks out.
@@ -0,0 +1,5 @@
1
+ export declare class TimestampTransformer implements Transformer<string, string> {
2
+ private deviceHasTimestamps;
3
+ transform(chunk: string, controller: TransformStreamDefaultController<string>): void;
4
+ reset(): void;
5
+ }
@@ -0,0 +1,39 @@
1
+ // Matches lines that already carry a wall-clock or tick timestamp so we don't
2
+ // add a redundant one. Intentionally does NOT match bare log-level prefixes
3
+ // like ESPHome's [I][tag:line]: — those have no time information.
4
+ //
5
+ // Covered formats:
6
+ // (123456) FreeRTOS ms-tick e.g. "(12345) "
7
+ // [HH:MM:SS] wall-clock bracket
8
+ // [HH:MM:SS.mmm] wall-clock bracket with millis
9
+ // I (1234) tag: ESP-IDF log level + tick e.g. "I (1234) wifi: ..."
10
+ // HH:MM:SS.mmm plain wall-clock
11
+ const DEVICE_TIMESTAMP_RE = /^\s*(?:\(\d+\)\s|\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|[DIWEACV] \(\d+\) \w|(?:\d{2}:){2}\d{2}\.\d)/;
12
+ export class TimestampTransformer {
13
+ constructor() {
14
+ this.deviceHasTimestamps = false;
15
+ }
16
+ transform(chunk, controller) {
17
+ // Pass through pure newline / empty sentinel unchanged so that
18
+ // carriage-return overwrite logic in console-color.ts still works.
19
+ if (chunk === "" || chunk === "\n" || chunk === "\r") {
20
+ controller.enqueue(chunk);
21
+ return;
22
+ }
23
+ if (!this.deviceHasTimestamps && DEVICE_TIMESTAMP_RE.test(chunk)) {
24
+ this.deviceHasTimestamps = true;
25
+ }
26
+ if (this.deviceHasTimestamps) {
27
+ controller.enqueue(chunk);
28
+ return;
29
+ }
30
+ const date = new Date();
31
+ const h = date.getHours().toString().padStart(2, "0");
32
+ const m = date.getMinutes().toString().padStart(2, "0");
33
+ const s = date.getSeconds().toString().padStart(2, "0");
34
+ controller.enqueue(`[${h}:${m}:${s}] ${chunk}`);
35
+ }
36
+ reset() {
37
+ this.deviceHasTimestamps = false;
38
+ }
39
+ }