|
|
|
|
@@ -3,7 +3,7 @@
|
|
|
|
|
*/
|
|
|
|
|
(function (global) {
|
|
|
|
|
const RAW_REPL_BANNER = 'raw REPL; CTRL-B to exit\r\n';
|
|
|
|
|
const SOFT_REBOOT = 'soft reboot\r\n';
|
|
|
|
|
const SOFT_REBOOT_MARKERS = ['soft reboot\r\n', 'MPY: soft reboot\r\n'];
|
|
|
|
|
const LOG = '[led-tool/serial]';
|
|
|
|
|
|
|
|
|
|
function log(...args) {
|
|
|
|
|
@@ -29,13 +29,66 @@
|
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bytesEndsWith(buf, suffix) {
|
|
|
|
|
if (buf.length < suffix.length) return false;
|
|
|
|
|
for (let i = 0; i < suffix.length; i += 1) {
|
|
|
|
|
if (buf[buf.length - suffix.length + i] !== suffix[i]) return false;
|
|
|
|
|
/** Common ESP32 USB-UART bridges (usbVendorId only per Web Serial API). */
|
|
|
|
|
const ESP32_USB_FILTERS = [
|
|
|
|
|
{ usbVendorId: 0x303a }, /* Espressif */
|
|
|
|
|
{ usbVendorId: 0x10c4 }, /* Silicon Labs CP210x */
|
|
|
|
|
{ usbVendorId: 0x1a86 }, /* WCH CH340 */
|
|
|
|
|
{ usbVendorId: 0x0403 }, /* FTDI */
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
async function deassertDtrRts(port) {
|
|
|
|
|
if (!port || !port.setSignals) return;
|
|
|
|
|
try {
|
|
|
|
|
await port.setSignals({ dataTerminalReady: false, requestToSend: false });
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore — some adapters/browsers reject setSignals */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bytesIndexOf(buf, suffix) {
|
|
|
|
|
if (!suffix.length) return 0;
|
|
|
|
|
if (buf.length < suffix.length) return -1;
|
|
|
|
|
for (let i = 0; i <= buf.length - suffix.length; i += 1) {
|
|
|
|
|
let ok = true;
|
|
|
|
|
for (let j = 0; j < suffix.length; j += 1) {
|
|
|
|
|
if (buf[i + j] !== suffix[j]) {
|
|
|
|
|
ok = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (ok) return i;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bufferEndsWithFriendlyRepl(buf) {
|
|
|
|
|
if (bufferLooksLikeEspRomBoot(buf) && !bufferHasMicroPython(buf)) return false;
|
|
|
|
|
const tail = decodeText(Uint8Array.from(buf.slice(-80)));
|
|
|
|
|
return /(?:\r\n|\n)>>> ?$/.test(tail);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bufferLooksLikeEspRomBoot(buf) {
|
|
|
|
|
if (bufferIndicatesRuntime(buf)) return false;
|
|
|
|
|
const text = decodeText(Uint8Array.from(buf.slice(-512)));
|
|
|
|
|
return /SPI_FAST_FLASH_BOOT|rst:0x/.test(text) && !/entry 0x403/.test(text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** MicroPython is up (banner, REPL, raw REPL, or post-ROM heap lines from boot). */
|
|
|
|
|
function bufferIndicatesRuntime(buf) {
|
|
|
|
|
const text = decodeText(Uint8Array.from(buf));
|
|
|
|
|
if (text.includes('MicroPython')) return true;
|
|
|
|
|
if (text.includes('raw REPL; CTRL-B to exit')) return true;
|
|
|
|
|
if (/(?:\r\n|\n)>>> ?/.test(text)) return true;
|
|
|
|
|
if (text.includes('entry 0x') && (/['"]?free['"]?\s*:/.test(text) || /,\s*'free'/.test(text))) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bufferHasMicroPython(buf) {
|
|
|
|
|
return bufferIndicatesRuntime(buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function decodeText(u8) {
|
|
|
|
|
return new TextDecoder('utf-8', { fatal: false }).decode(u8);
|
|
|
|
|
@@ -84,12 +137,15 @@
|
|
|
|
|
return result.value || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Match suffix anywhere in the buffer; keep bytes after the match for the next read. */
|
|
|
|
|
async readUntil(suffixBytes, timeoutMs = 15000) {
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
if (bytesEndsWith(this._rxBuf, suffixBytes)) {
|
|
|
|
|
const matched = Uint8Array.from(this._rxBuf);
|
|
|
|
|
this._rxBuf.length = 0;
|
|
|
|
|
const idx = bytesIndexOf(this._rxBuf, suffixBytes);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
const end = idx + suffixBytes.length;
|
|
|
|
|
const matched = Uint8Array.from(this._rxBuf.slice(0, end));
|
|
|
|
|
this._rxBuf.splice(0, end);
|
|
|
|
|
logDebug('readUntil matched', JSON.stringify(decodeText(suffixBytes)), 'len', matched.length);
|
|
|
|
|
return matched;
|
|
|
|
|
}
|
|
|
|
|
@@ -106,6 +162,29 @@
|
|
|
|
|
throw new Error(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async readUntilAny(suffixList, timeoutMs = 15000) {
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
for (const suffixBytes of suffixList) {
|
|
|
|
|
const idx = bytesIndexOf(this._rxBuf, suffixBytes);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
const end = idx + suffixBytes.length;
|
|
|
|
|
const matched = Uint8Array.from(this._rxBuf.slice(0, end));
|
|
|
|
|
this._rxBuf.splice(0, end);
|
|
|
|
|
logDebug('readUntilAny matched', JSON.stringify(decodeText(suffixBytes)));
|
|
|
|
|
return matched;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const chunk = await this._readStreamChunk(Math.min(300, Math.max(1, deadline - Date.now())));
|
|
|
|
|
if (chunk && chunk.length) this._appendRx(chunk);
|
|
|
|
|
else await sleep(5);
|
|
|
|
|
}
|
|
|
|
|
const tail = decodeText(Uint8Array.from(this._rxBuf.slice(-240)));
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Timed out waiting for one of ${suffixList.map((s) => JSON.stringify(decodeText(s))).join(', ')} (tail: ${JSON.stringify(tail)})`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async readExact(n, timeoutMs = 10000) {
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
while (this._rxBuf.length < n && Date.now() < deadline) {
|
|
|
|
|
@@ -132,21 +211,38 @@
|
|
|
|
|
flowControl: 'none',
|
|
|
|
|
bufferSize: 65536,
|
|
|
|
|
});
|
|
|
|
|
if (this.port.setSignals) {
|
|
|
|
|
try {
|
|
|
|
|
await this.port.setSignals({ dataTerminalReady: false, requestToSend: false });
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await deassertDtrRts(this.port);
|
|
|
|
|
await sleep(50);
|
|
|
|
|
await deassertDtrRts(this.port);
|
|
|
|
|
this.reader = this.port.readable.getReader();
|
|
|
|
|
this.writer = this.port.writable.getWriter();
|
|
|
|
|
this.portOpen = true;
|
|
|
|
|
await sleep(100);
|
|
|
|
|
await this.drainInput(300);
|
|
|
|
|
await this.collectIncoming(8000);
|
|
|
|
|
if (bufferIndicatesRuntime(this._rxBuf)) {
|
|
|
|
|
log('connect: MicroPython output seen during open');
|
|
|
|
|
}
|
|
|
|
|
log('connect: port open (raw REPL not entered yet)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Read serial into _rxBuf without discarding (for post-open boot). */
|
|
|
|
|
async collectIncoming(maxMs) {
|
|
|
|
|
const deadline = Date.now() + maxMs;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
const chunk = await this._readStreamChunk(Math.min(80, Math.max(1, deadline - Date.now())));
|
|
|
|
|
if (chunk && chunk.length) this._appendRx(chunk);
|
|
|
|
|
else if (
|
|
|
|
|
bufferHasMicroPython(this._rxBuf) ||
|
|
|
|
|
bufferEndsWithFriendlyRepl(this._rxBuf) ||
|
|
|
|
|
bytesIndexOf(this._rxBuf, new TextEncoder().encode(RAW_REPL_BANNER)) >= 0
|
|
|
|
|
) {
|
|
|
|
|
break;
|
|
|
|
|
} else {
|
|
|
|
|
await sleep(15);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async ensureRawRepl() {
|
|
|
|
|
if (this.inRawRepl) return;
|
|
|
|
|
if (this._replLock) {
|
|
|
|
|
@@ -154,7 +250,8 @@
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
log('ensureRawRepl: entering raw REPL');
|
|
|
|
|
this._replLock = this.enterRawRepl(true).finally(() => {
|
|
|
|
|
// USB open already resets the ESP32; skip soft-reset (unlike host led-cli/mpremote).
|
|
|
|
|
this._replLock = this.enterRawRepl(false).finally(() => {
|
|
|
|
|
this._replLock = null;
|
|
|
|
|
});
|
|
|
|
|
await this._replLock;
|
|
|
|
|
@@ -193,6 +290,11 @@
|
|
|
|
|
this.writer = null;
|
|
|
|
|
}
|
|
|
|
|
if (this.port) {
|
|
|
|
|
try {
|
|
|
|
|
await deassertDtrRts(this.port);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
await this.port.close();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
@@ -209,41 +311,147 @@
|
|
|
|
|
await this.writer.write(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async drainInput(maxMs = 400) {
|
|
|
|
|
async drainInput(maxMs = 400, clearBuffer = true) {
|
|
|
|
|
const deadline = Date.now() + maxMs;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
const chunk = await this._readStreamChunk(40);
|
|
|
|
|
if (!chunk || !chunk.length) break;
|
|
|
|
|
if (!clearBuffer && chunk.length) this._appendRx(chunk);
|
|
|
|
|
}
|
|
|
|
|
this._rxBuf.length = 0;
|
|
|
|
|
if (clearBuffer) this._rxBuf.length = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendCtrlA() {
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x0d, 0x01]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** USB open often resets the ESP32; wait through ROM boot until MicroPython is up. */
|
|
|
|
|
async waitForMicroPythonBoot(timeoutMs = 60000) {
|
|
|
|
|
log('enterRawRepl: wait for MicroPython after boot', 'buf', this._rxBuf.length);
|
|
|
|
|
if (bufferHasMicroPython(this._rxBuf) || bufferEndsWithFriendlyRepl(this._rxBuf)) {
|
|
|
|
|
log('enterRawRepl: MicroPython already in buffer');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
let nudgeSent = false;
|
|
|
|
|
let lastLog = Date.now();
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
if (bufferHasMicroPython(this._rxBuf) || bufferEndsWithFriendlyRepl(this._rxBuf)) {
|
|
|
|
|
log('enterRawRepl: MicroPython running');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const text = decodeText(Uint8Array.from(this._rxBuf));
|
|
|
|
|
if (!nudgeSent && text.includes('entry 0x')) {
|
|
|
|
|
nudgeSent = true;
|
|
|
|
|
log('enterRawRepl: past ROM loader, nudge REPL');
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x0d]));
|
|
|
|
|
await sleep(200);
|
|
|
|
|
}
|
|
|
|
|
if (Date.now() - lastLog > 5000) {
|
|
|
|
|
lastLog = Date.now();
|
|
|
|
|
log('enterRawRepl: still waiting…', 'buf', this._rxBuf.length);
|
|
|
|
|
}
|
|
|
|
|
const rem = Math.max(1, deadline - Date.now());
|
|
|
|
|
const chunk = await this._readStreamChunk(Math.min(300, rem));
|
|
|
|
|
if (chunk && chunk.length) this._appendRx(chunk);
|
|
|
|
|
else await sleep(bufferLooksLikeEspRomBoot(this._rxBuf) ? 20 : 10);
|
|
|
|
|
}
|
|
|
|
|
const tail = decodeText(Uint8Array.from(this._rxBuf.slice(-240)));
|
|
|
|
|
throw new Error(`Timed out waiting for MicroPython boot (tail: ${JSON.stringify(tail)})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Wait until raw REPL banner and `>` prompt (not banner alone — device may still be booting). */
|
|
|
|
|
async waitRawReplAfterSoftReboot(bannerBytes, bannerPromptBytes, timeoutMs = 15000) {
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
const rem = Math.max(1, deadline - Date.now());
|
|
|
|
|
const promptAt = bytesIndexOf(this._rxBuf, bannerPromptBytes);
|
|
|
|
|
if (promptAt >= 0) {
|
|
|
|
|
this._rxBuf.splice(0, promptAt + bannerPromptBytes.length);
|
|
|
|
|
log('enterRawRepl: raw REPL prompt ready');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (bufferEndsWithFriendlyRepl(this._rxBuf)) {
|
|
|
|
|
log('enterRawRepl: friendly REPL, ctrl-A');
|
|
|
|
|
await this.sendCtrlA();
|
|
|
|
|
await sleep(30);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (bufferLooksLikeEspRomBoot(this._rxBuf) && !bufferHasMicroPython(this._rxBuf)) {
|
|
|
|
|
logDebug('enterRawRepl: ROM boot in progress…');
|
|
|
|
|
}
|
|
|
|
|
const chunk = await this._readStreamChunk(Math.min(300, rem));
|
|
|
|
|
if (chunk && chunk.length) this._appendRx(chunk);
|
|
|
|
|
else await sleep(10);
|
|
|
|
|
}
|
|
|
|
|
const tail = decodeText(Uint8Array.from(this._rxBuf.slice(-240)));
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Timed out waiting for raw REPL after soft reboot (tail: ${JSON.stringify(tail)})`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Match mpremote SerialTransport.enter_raw_repl(soft_reset). */
|
|
|
|
|
async enterRawRepl(softReset = true) {
|
|
|
|
|
log('enterRawRepl', { softReset });
|
|
|
|
|
const bannerBytes = new TextEncoder().encode(RAW_REPL_BANNER);
|
|
|
|
|
const bannerPromptBytes = new TextEncoder().encode(RAW_REPL_BANNER + '>');
|
|
|
|
|
const softRebootBytes = SOFT_REBOOT_MARKERS.map((m) => new TextEncoder().encode(m));
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!bufferHasMicroPython(this._rxBuf) &&
|
|
|
|
|
!bufferEndsWithFriendlyRepl(this._rxBuf) &&
|
|
|
|
|
bytesIndexOf(this._rxBuf, bannerBytes) < 0
|
|
|
|
|
) {
|
|
|
|
|
await this.waitForMicroPythonBoot(60000);
|
|
|
|
|
} else {
|
|
|
|
|
log('enterRawRepl: already have REPL output in buffer');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bufferEndsWithFriendlyRepl(this._rxBuf)) {
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x0d, 0x03]));
|
|
|
|
|
await this.drainInput(400);
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x0d, 0x01]));
|
|
|
|
|
await sleep(50);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ctrlARetries = 0;
|
|
|
|
|
const maxCtrlARetries = 3;
|
|
|
|
|
while (ctrlARetries <= maxCtrlARetries) {
|
|
|
|
|
try {
|
|
|
|
|
await this.sendCtrlA();
|
|
|
|
|
await this.waitRawReplAfterSoftReboot(bannerBytes, bannerPromptBytes, 20000);
|
|
|
|
|
break;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (ctrlARetries >= maxCtrlARetries) throw e;
|
|
|
|
|
ctrlARetries += 1;
|
|
|
|
|
log('enterRawRepl: retry ctrl-A', ctrlARetries, e.message);
|
|
|
|
|
this._rxBuf.length = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (softReset) {
|
|
|
|
|
log('enterRawRepl: wait banner+>, soft reset');
|
|
|
|
|
await this.readUntil(new TextEncoder().encode(RAW_REPL_BANNER + '>'), 15000);
|
|
|
|
|
log('enterRawRepl: soft reset');
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x04]));
|
|
|
|
|
try {
|
|
|
|
|
await this.readUntil(new TextEncoder().encode(SOFT_REBOOT), 15000);
|
|
|
|
|
await this.readUntilAny(softRebootBytes, 20000);
|
|
|
|
|
log('enterRawRepl: saw soft reboot');
|
|
|
|
|
this._rxBuf.length = 0;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log('enterRawRepl: no soft reboot banner', e.message);
|
|
|
|
|
}
|
|
|
|
|
ctrlARetries = 0;
|
|
|
|
|
while (ctrlARetries <= maxCtrlARetries) {
|
|
|
|
|
try {
|
|
|
|
|
await this.waitRawReplAfterSoftReboot(bannerBytes, bannerPromptBytes, 60000);
|
|
|
|
|
break;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (ctrlARetries >= maxCtrlARetries) throw e;
|
|
|
|
|
ctrlARetries += 1;
|
|
|
|
|
log('enterRawRepl: post-reboot retry ctrl-A', ctrlARetries, e.message);
|
|
|
|
|
this._rxBuf.length = 0;
|
|
|
|
|
await this.sendCtrlA();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.readUntil(new TextEncoder().encode(RAW_REPL_BANNER), 45000);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log('enterRawRepl: retry ctrl-A after', e.message);
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x0d, 0x01]));
|
|
|
|
|
await this.readUntil(new TextEncoder().encode(RAW_REPL_BANNER), 30000);
|
|
|
|
|
}
|
|
|
|
|
this.inRawRepl = true;
|
|
|
|
|
log('enterRawRepl: ready');
|
|
|
|
|
}
|
|
|
|
|
@@ -275,9 +483,34 @@
|
|
|
|
|
await this.readUntil(new Uint8Array([0x04]), 30000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async awaitRawReplExecPrompt(timeoutMs = 60000) {
|
|
|
|
|
const bannerPromptBytes = new TextEncoder().encode(RAW_REPL_BANNER + '>');
|
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
const promptAt = bytesIndexOf(this._rxBuf, bannerPromptBytes);
|
|
|
|
|
if (promptAt >= 0) {
|
|
|
|
|
this._rxBuf.splice(0, promptAt + bannerPromptBytes.length);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (bufferEndsWithFriendlyRepl(this._rxBuf)) {
|
|
|
|
|
log('exec: friendly REPL before prompt, ctrl-A');
|
|
|
|
|
this._rxBuf.length = 0;
|
|
|
|
|
await this.sendCtrlA();
|
|
|
|
|
} else if (bufferLooksLikeEspRomBoot(this._rxBuf)) {
|
|
|
|
|
logDebug('exec: still in ROM boot, waiting…');
|
|
|
|
|
}
|
|
|
|
|
const rem = Math.max(1, deadline - Date.now());
|
|
|
|
|
const chunk = await this._readStreamChunk(Math.min(300, rem));
|
|
|
|
|
if (chunk && chunk.length) this._appendRx(chunk);
|
|
|
|
|
else await sleep(10);
|
|
|
|
|
}
|
|
|
|
|
const tail = decodeText(Uint8Array.from(this._rxBuf.slice(-240)));
|
|
|
|
|
throw new Error(`Timed out waiting for raw REPL prompt (tail: ${JSON.stringify(tail)})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async execRawNoFollow(commandBytes) {
|
|
|
|
|
log('execRawNoFollow', commandBytes.length, 'bytes');
|
|
|
|
|
await this.readUntil(new TextEncoder().encode('>'), 10000);
|
|
|
|
|
await this.awaitRawReplExecPrompt(60000);
|
|
|
|
|
|
|
|
|
|
if (this.useRawPaste) {
|
|
|
|
|
await this.writeBytes(new Uint8Array([0x05, 0x41, 0x01]));
|
|
|
|
|
@@ -388,9 +621,33 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serialSupported() {
|
|
|
|
|
return typeof navigator !== 'undefined' && 'serial' in navigator;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getGrantedPorts() {
|
|
|
|
|
if (!serialSupported()) return [];
|
|
|
|
|
return navigator.serial.getPorts();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestPort() {
|
|
|
|
|
if (!serialSupported()) {
|
|
|
|
|
throw new Error('Web Serial not supported');
|
|
|
|
|
}
|
|
|
|
|
const granted = await navigator.serial.getPorts();
|
|
|
|
|
if (granted.length === 1) {
|
|
|
|
|
log('requestPort: reusing single granted port');
|
|
|
|
|
return granted[0];
|
|
|
|
|
}
|
|
|
|
|
log('requestPort: showing picker', { grantedCount: granted.length });
|
|
|
|
|
return navigator.serial.requestPort({ filters: ESP32_USB_FILTERS });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
global.LedToolWebSerial = {
|
|
|
|
|
supported: () => typeof navigator !== 'undefined' && 'serial' in navigator,
|
|
|
|
|
requestPort: () => navigator.serial.requestPort(),
|
|
|
|
|
supported: serialSupported,
|
|
|
|
|
getGrantedPorts,
|
|
|
|
|
requestPort,
|
|
|
|
|
MicroPythonRawRepl,
|
|
|
|
|
_test: { bytesIndexOf, decodeText },
|
|
|
|
|
};
|
|
|
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
|
|
|
|