Compare commits
1 Commits
f74e21f206
...
bd4d2060ae
| Author | SHA1 | Date | |
|---|---|---|---|
| bd4d2060ae |
@@ -185,10 +185,11 @@
|
||||
document.body.getAttribute('data-api-base') ??
|
||||
(location.pathname.includes('/led-tool') ? '/led-tool' : '');
|
||||
const prefix = api + '/static';
|
||||
const v = '20260520';
|
||||
function loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = url;
|
||||
s.src = url + (url.includes('?') ? '&' : '?') + 'v=' + v;
|
||||
s.onload = () => resolve();
|
||||
s.onerror = () => reject(new Error('Failed to load ' + url));
|
||||
document.body.appendChild(s);
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
let hostConnected = false;
|
||||
let hostConnectedPort = '';
|
||||
let transferBusy = false;
|
||||
let webConnectBusy = false;
|
||||
|
||||
const webSerialOk = window.LedToolWebSerial && window.LedToolWebSerial.supported();
|
||||
let webSerialGrantedCount = 0;
|
||||
|
||||
function setStatus(text, isError) {
|
||||
if (!statusEl) return;
|
||||
@@ -40,6 +42,17 @@
|
||||
setTimeout(() => messageEl.classList.remove('show'), 5000);
|
||||
}
|
||||
|
||||
function errorMessage(e) {
|
||||
if (!e) return 'Unknown error';
|
||||
if (typeof e === 'string') return e;
|
||||
return e.message || String(e);
|
||||
}
|
||||
|
||||
function onWebSerialTransferError(e) {
|
||||
if (webClient) webClient.inRawRepl = false;
|
||||
return errorMessage(e);
|
||||
}
|
||||
|
||||
function formToObject() {
|
||||
const out = {};
|
||||
document.querySelectorAll('[data-setting]').forEach((el) => {
|
||||
@@ -99,7 +112,11 @@
|
||||
} else if (hostOpen) {
|
||||
setStatus(`Host serial: ${hostConnectedPort} (use Download or Upload).`);
|
||||
} else if (webSerialOk) {
|
||||
setStatus('Connect host serial or Web Serial USB, then Download or Upload.');
|
||||
if (webSerialGrantedCount === 1) {
|
||||
setStatus('Connect USB (one remembered port) or host serial, then Download or Upload.');
|
||||
} else {
|
||||
setStatus('Connect host serial or Web Serial USB, then Download or Upload.');
|
||||
}
|
||||
} else {
|
||||
setStatus(`Connect host serial (default ${DEFAULT_HOST_PORT}).`);
|
||||
}
|
||||
@@ -205,21 +222,31 @@
|
||||
}
|
||||
|
||||
async function connectWebSerial() {
|
||||
if (webConnectBusy) return;
|
||||
if (!window.LedToolWebSerial || !window.LedToolWebSerial.supported()) {
|
||||
flash('Web Serial not supported (use Chrome or Edge over HTTPS or localhost).', true);
|
||||
return;
|
||||
}
|
||||
if (usingWebSerial()) {
|
||||
flash('USB already connected.', false);
|
||||
return;
|
||||
}
|
||||
if (usingHostSerial()) {
|
||||
disconnectHostSerial();
|
||||
}
|
||||
log('connectWebSerial: requestPort');
|
||||
setStatus('Opening USB port…');
|
||||
webPort = await window.LedToolWebSerial.requestPort();
|
||||
log('connectWebSerial: port selected', webPort);
|
||||
webClient = new window.LedToolWebSerial.MicroPythonRawRepl();
|
||||
await webClient.connect(webPort);
|
||||
updateUi();
|
||||
flash('USB port open. Use Download or Upload to talk to the device.', false);
|
||||
webConnectBusy = true;
|
||||
try {
|
||||
log('connectWebSerial: requestPort');
|
||||
setStatus('Opening USB port…');
|
||||
webPort = await window.LedToolWebSerial.requestPort();
|
||||
log('connectWebSerial: port selected', webPort);
|
||||
webClient = new window.LedToolWebSerial.MicroPythonRawRepl();
|
||||
await webClient.connect(webPort);
|
||||
updateUi();
|
||||
flash('USB port open. Wait for boot, then Download.', false);
|
||||
} finally {
|
||||
webConnectBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectWebSerial() {
|
||||
@@ -284,9 +311,10 @@
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(LOG, 'download failed', e);
|
||||
if (rawJson) rawJson.value = String(e.message || e);
|
||||
flash(e.message, true);
|
||||
setStatus(e.message, true);
|
||||
const msg = usingWebSerial() ? onWebSerialTransferError(e) : errorMessage(e);
|
||||
if (rawJson) rawJson.value = msg;
|
||||
flash(msg, true);
|
||||
setStatus(msg, true);
|
||||
} finally {
|
||||
transferBusy = false;
|
||||
}
|
||||
@@ -310,8 +338,9 @@
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(LOG, 'upload failed', e);
|
||||
flash(e.message, true);
|
||||
setStatus(e.message, true);
|
||||
const msg = usingWebSerial() ? onWebSerialTransferError(e) : errorMessage(e);
|
||||
flash(msg, true);
|
||||
setStatus(msg, true);
|
||||
} finally {
|
||||
transferBusy = false;
|
||||
}
|
||||
@@ -374,7 +403,19 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshWebSerialGrantHint() {
|
||||
if (!webSerialOk || !window.LedToolWebSerial.getGrantedPorts) return;
|
||||
try {
|
||||
const ports = await window.LedToolWebSerial.getGrantedPorts();
|
||||
webSerialGrantedCount = ports.length;
|
||||
updateUi();
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
log('settings editor ready', { apiBase, webSerialOk });
|
||||
updateUi();
|
||||
loadHostPorts();
|
||||
refreshWebSerialGrantHint();
|
||||
})();
|
||||
|
||||
@@ -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,12 +29,65 @@
|
||||
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 */
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -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 });
|
||||
await this.writeBytes(new Uint8Array([0x0d, 0x03]));
|
||||
await this.drainInput(400);
|
||||
await this.writeBytes(new Uint8Array([0x0d, 0x01]));
|
||||
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 (softReset) {
|
||||
log('enterRawRepl: wait banner+>, soft reset');
|
||||
await this.readUntil(new TextEncoder().encode(RAW_REPL_BANNER + '>'), 15000);
|
||||
await this.writeBytes(new Uint8Array([0x04]));
|
||||
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 sleep(50);
|
||||
}
|
||||
|
||||
let ctrlARetries = 0;
|
||||
const maxCtrlARetries = 3;
|
||||
while (ctrlARetries <= maxCtrlARetries) {
|
||||
try {
|
||||
await this.readUntil(new TextEncoder().encode(SOFT_REBOOT), 15000);
|
||||
log('enterRawRepl: saw soft reboot');
|
||||
await this.sendCtrlA();
|
||||
await this.waitRawReplAfterSoftReboot(bannerBytes, bannerPromptBytes, 20000);
|
||||
break;
|
||||
} catch (e) {
|
||||
log('enterRawRepl: no soft reboot banner', e.message);
|
||||
if (ctrlARetries >= maxCtrlARetries) throw e;
|
||||
ctrlARetries += 1;
|
||||
log('enterRawRepl: retry ctrl-A', ctrlARetries, e.message);
|
||||
this._rxBuf.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (softReset) {
|
||||
log('enterRawRepl: soft reset');
|
||||
await this.writeBytes(new Uint8Array([0x04]));
|
||||
try {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user