mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-08-12 07:55:33 -06:00
Improve trainer capture metadata and USB flashing
This commit is contained in:
@@ -509,6 +509,34 @@
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.firmwareUsbWifiPanel {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(57,212,160,0.16);
|
||||
background: rgba(57,212,160,0.055);
|
||||
}
|
||||
|
||||
.firmwareUsbWifiToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.firmwareUsbWifiToggle input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.firmwareUsbWifiGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.studioHero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -1186,6 +1214,7 @@
|
||||
.firmwareGrid { grid-template-columns: 1fr; }
|
||||
.firmwareLayout,
|
||||
.firmwareTargetGrid,
|
||||
.firmwareUsbWifiGrid,
|
||||
.firmwareActionsPanel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1591,7 +1620,8 @@
|
||||
<label class="field">
|
||||
<strong>Detected Device</strong>
|
||||
<select id="firmwareDeviceSelect">
|
||||
<option value="">No devices scanned yet</option>
|
||||
<option value="">Choose target...</option>
|
||||
<option value="__browser_usb_flash__">Browser USB Flash (new/recovery)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
@@ -1623,6 +1653,22 @@
|
||||
<button id="cleanFirmwareBtn" type="button">Clear downloaded images</button>
|
||||
<button id="openFirmwareConsoleBtn" type="button">Open firmware console</button>
|
||||
</div>
|
||||
<section class="firmwareUsbWifiPanel" aria-label="Browser USB Wi-Fi setup">
|
||||
<label class="firmwareUsbWifiToggle">
|
||||
<input id="firmwareUsbWifiEnabled" type="checkbox" checked />
|
||||
<span>Set up Wi-Fi after Browser USB Flash</span>
|
||||
</label>
|
||||
<div class="firmwareUsbWifiGrid">
|
||||
<label class="field">
|
||||
<strong>Wi-Fi SSID</strong>
|
||||
<input id="firmwareUsbSsid" type="text" placeholder="Network name" autocomplete="off" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<strong>Wi-Fi Password</strong>
|
||||
<input id="firmwareUsbPassword" type="password" placeholder="Network password" autocomplete="current-password" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<div class="usbFlashHint">Use Browser USB Flash for new satellites, recovery, or devices older than Tater firmware 3.0.3. Chrome or Edge must be used on localhost or HTTPS. In the macOS app, open <code>http://127.0.0.1:8789</code> in Chrome or Edge; port <code>3232</code> is only for OTA updates after firmware is installed.</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -2031,7 +2077,7 @@
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || "")
|
||||
return String(text ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
@@ -2056,6 +2102,12 @@
|
||||
return { label: item.capture_label || "Captured", cls: "" };
|
||||
}
|
||||
|
||||
function formatDetectionProfile(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("_", " ")
|
||||
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
||||
}
|
||||
|
||||
function renderCapturedItems(payload) {
|
||||
const data = payload || { items: [], captured_count: 0, negative_count: 0, personal_count: 0 };
|
||||
uiState.captured = data;
|
||||
@@ -2084,6 +2136,18 @@
|
||||
if (item.wake_word) meta.push(`<span class="pill">${escapeHtml(item.wake_word)}</span>`);
|
||||
if (item.max_probability !== null && item.max_probability !== undefined) meta.push(`<span class="pill">max ${escapeHtml(item.max_probability)}</span>`);
|
||||
if (item.average_probability !== null && item.average_probability !== undefined) meta.push(`<span class="pill">avg ${escapeHtml(item.average_probability)}</span>`);
|
||||
if (item.detection_profile) meta.push(`<span class="pill">profile ${escapeHtml(formatDetectionProfile(item.detection_profile))}</span>`);
|
||||
if (item.peak_probability_cutoff !== null && item.peak_probability_cutoff !== undefined) meta.push(`<span class="pill">peak cutoff ${escapeHtml(item.peak_probability_cutoff)}</span>`);
|
||||
if (item.probability_cutoff !== null && item.probability_cutoff !== undefined) meta.push(`<span class="pill">avg cutoff ${escapeHtml(item.probability_cutoff)}</span>`);
|
||||
if (item.active_window_count !== null && item.active_window_count !== undefined && item.min_active_windows !== null && item.min_active_windows !== undefined) {
|
||||
meta.push(`<span class="pill">windows ${escapeHtml(item.active_window_count)}/${escapeHtml(item.min_active_windows)}</span>`);
|
||||
}
|
||||
if (item.rise_score !== null && item.rise_score !== undefined) meta.push(`<span class="pill">rise ${escapeHtml(item.rise_score)}</span>`);
|
||||
if (item.vad_max_probability !== null && item.vad_max_probability !== undefined) meta.push(`<span class="pill">VAD max ${escapeHtml(item.vad_max_probability)}</span>`);
|
||||
if (item.vad_average_probability !== null && item.vad_average_probability !== undefined) meta.push(`<span class="pill">VAD avg ${escapeHtml(item.vad_average_probability)}</span>`);
|
||||
if (Array.isArray(item.probability_history) && item.probability_history.length) {
|
||||
meta.push(`<span class="pill" title="${escapeAttr(item.probability_history.join(", "))}">history ${escapeHtml(item.probability_history.length)}</span>`);
|
||||
}
|
||||
const formatSummary = item.final_format ? describeFormat(item.final_format) : "16 kHz, mono, 16-bit";
|
||||
const when = formatTimestamp(item.captured_at || item.received_at);
|
||||
const actionDisabled = uiState.reviewBusy ? "disabled" : "";
|
||||
@@ -2449,6 +2513,18 @@
|
||||
return port;
|
||||
}
|
||||
|
||||
async function browserUsbAuthorizedPorts() {
|
||||
if (!window.isSecureContext || !navigator.serial || typeof navigator.serial.getPorts !== "function") {
|
||||
return [];
|
||||
}
|
||||
return navigator.serial.getPorts();
|
||||
}
|
||||
|
||||
function browserUsbStoredPort(selector = "") {
|
||||
const key = String(selector || "default").trim() || "default";
|
||||
return uiState.browserUsb.ports?.[key] || null;
|
||||
}
|
||||
|
||||
function normalizeEsptoolJsModule(module) {
|
||||
if (module?.ESPLoader && module?.Transport) {
|
||||
return module;
|
||||
@@ -2507,19 +2583,15 @@
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
async function browserUsbSetSignals(port, signals, timeoutMs = 1200) {
|
||||
if (!port || typeof port.setSignals !== "function") {
|
||||
return false;
|
||||
}
|
||||
async function browserUsbWithTimeout(promise, timeoutMs, message) {
|
||||
let timeoutId = 0;
|
||||
try {
|
||||
await Promise.race([
|
||||
port.setSignals(signals),
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise((_resolve, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error("Timed out setting USB serial control signals.")), Math.max(1, Number(timeoutMs) || 1));
|
||||
timeoutId = setTimeout(() => reject(new Error(message)), Math.max(1, Number(timeoutMs) || 1));
|
||||
}),
|
||||
]);
|
||||
return true;
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -2527,6 +2599,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function browserUsbSetSignals(port, signals, timeoutMs = 1200) {
|
||||
if (!port || typeof port.setSignals !== "function") {
|
||||
return false;
|
||||
}
|
||||
await browserUsbWithTimeout(
|
||||
port.setSignals(signals),
|
||||
timeoutMs,
|
||||
"Timed out setting USB serial control signals."
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function browserUsbHardResetAfterFlash(transport, loader, port) {
|
||||
try {
|
||||
appendFirmwareLog("Resetting device after USB flash.");
|
||||
@@ -2547,6 +2631,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
function browserUsbSameDevice(left, right) {
|
||||
const leftInfo = left?.getInfo?.() || {};
|
||||
const rightInfo = right?.getInfo?.() || {};
|
||||
const leftVendor = Number(leftInfo.usbVendorId || 0);
|
||||
const leftProduct = Number(leftInfo.usbProductId || 0);
|
||||
const rightVendor = Number(rightInfo.usbVendorId || 0);
|
||||
const rightProduct = Number(rightInfo.usbProductId || 0);
|
||||
return Boolean(leftVendor && leftProduct && leftVendor === rightVendor && leftProduct === rightProduct);
|
||||
}
|
||||
|
||||
async function browserUsbWaitForReconnect(previousPort, selector, timeoutMs = 30000) {
|
||||
const deadline = Date.now() + Math.max(1000, Number(timeoutMs) || 1000);
|
||||
let lastLogAt = 0;
|
||||
while (Date.now() < deadline) {
|
||||
let ports = [];
|
||||
try {
|
||||
ports = await browserUsbAuthorizedPorts();
|
||||
} catch (_error) {
|
||||
ports = [];
|
||||
}
|
||||
const matchingPort = ports.find((port) => browserUsbSameDevice(previousPort, port)) || (ports.length === 1 ? ports[0] : null);
|
||||
if (matchingPort) {
|
||||
const key = String(selector || "default").trim() || "default";
|
||||
uiState.browserUsb.ports[key] = matchingPort;
|
||||
appendFirmwareLog(`Reconnected ${browserUsbPortLabel(matchingPort)} for Wi-Fi setup.`, "USB serial reconnected for Wi-Fi setup...");
|
||||
return matchingPort;
|
||||
}
|
||||
if (Date.now() - lastLogAt > 2500) {
|
||||
lastLogAt = Date.now();
|
||||
appendFirmwareLog("Waiting for Chrome to expose the rebooted USB serial device...", "Waiting for rebooted USB device...");
|
||||
}
|
||||
await sleep(750);
|
||||
}
|
||||
throw new Error("The device rebooted, but Chrome did not expose the reconnected USB serial device. Click Browser USB Flash again and select the device if needed.");
|
||||
}
|
||||
|
||||
async function flashBrowserUsbPort(port, artifact) {
|
||||
const module = await ensureEsptoolJsLoaded();
|
||||
const { ESPLoader, Transport } = module;
|
||||
@@ -2636,6 +2756,344 @@
|
||||
}
|
||||
}
|
||||
|
||||
const IMPROV_SERIAL_HEADER = [0x49, 0x4d, 0x50, 0x52, 0x4f, 0x56];
|
||||
const IMPROV_SERIAL_VERSION = 0x01;
|
||||
const IMPROV_SERIAL_TYPE = {
|
||||
CURRENT_STATE: 0x01,
|
||||
ERROR_STATE: 0x02,
|
||||
RPC: 0x03,
|
||||
RPC_RESPONSE: 0x04,
|
||||
};
|
||||
const IMPROV_SERIAL_COMMAND = {
|
||||
WIFI_SETTINGS: 0x01,
|
||||
GET_CURRENT_STATE: 0x02,
|
||||
GET_DEVICE_INFO: 0x03,
|
||||
};
|
||||
const IMPROV_SERIAL_STATE = {
|
||||
STOPPED: 0x00,
|
||||
AWAITING_AUTHORIZATION: 0x01,
|
||||
AUTHORIZED: 0x02,
|
||||
PROVISIONING: 0x03,
|
||||
PROVISIONED: 0x04,
|
||||
};
|
||||
const IMPROV_SERIAL_ERROR = {
|
||||
NONE: 0x00,
|
||||
INVALID_RPC: 0x01,
|
||||
UNKNOWN_RPC: 0x02,
|
||||
UNABLE_TO_CONNECT: 0x03,
|
||||
NOT_AUTHORIZED: 0x04,
|
||||
UNKNOWN: 0xff,
|
||||
};
|
||||
const IMPROV_TEXT_ENCODER = new TextEncoder();
|
||||
const IMPROV_TEXT_DECODER = new TextDecoder();
|
||||
|
||||
function improvSerialStateLabel(stateValue) {
|
||||
switch (Number(stateValue)) {
|
||||
case IMPROV_SERIAL_STATE.STOPPED:
|
||||
return "Stopped";
|
||||
case IMPROV_SERIAL_STATE.AWAITING_AUTHORIZATION:
|
||||
return "Awaiting authorization";
|
||||
case IMPROV_SERIAL_STATE.AUTHORIZED:
|
||||
return "Ready for Wi-Fi credentials";
|
||||
case IMPROV_SERIAL_STATE.PROVISIONING:
|
||||
return "Connecting to Wi-Fi";
|
||||
case IMPROV_SERIAL_STATE.PROVISIONED:
|
||||
return "Wi-Fi connected";
|
||||
default:
|
||||
return `Unknown state ${stateValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
function improvSerialErrorLabel(errorValue) {
|
||||
switch (Number(errorValue)) {
|
||||
case IMPROV_SERIAL_ERROR.NONE:
|
||||
return "No error";
|
||||
case IMPROV_SERIAL_ERROR.INVALID_RPC:
|
||||
return "Invalid Improv command";
|
||||
case IMPROV_SERIAL_ERROR.UNKNOWN_RPC:
|
||||
return "Unknown Improv command";
|
||||
case IMPROV_SERIAL_ERROR.UNABLE_TO_CONNECT:
|
||||
return "Unable to connect to Wi-Fi";
|
||||
case IMPROV_SERIAL_ERROR.NOT_AUTHORIZED:
|
||||
return "Not authorized";
|
||||
case IMPROV_SERIAL_ERROR.UNKNOWN:
|
||||
return "Unknown Improv error";
|
||||
default:
|
||||
return `Improv error ${errorValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
function improvEncodeString(value) {
|
||||
const bytes = IMPROV_TEXT_ENCODER.encode(String(value || ""));
|
||||
if (bytes.length > 255) {
|
||||
throw new Error("Improv Serial values must be shorter than 256 bytes.");
|
||||
}
|
||||
return [bytes.length, ...bytes];
|
||||
}
|
||||
|
||||
function improvBuildRpcPayload(command, strings = []) {
|
||||
const data = [];
|
||||
(Array.isArray(strings) ? strings : []).forEach((value) => {
|
||||
data.push(...improvEncodeString(value));
|
||||
});
|
||||
if (data.length > 255) {
|
||||
throw new Error("Improv Serial command is too large.");
|
||||
}
|
||||
return [command, data.length, ...data];
|
||||
}
|
||||
|
||||
function improvBuildSerialPacket(type, payload = []) {
|
||||
const bytes = [...IMPROV_SERIAL_HEADER, IMPROV_SERIAL_VERSION, type, payload.length, ...payload];
|
||||
const checksum = bytes.reduce((sum, byte) => (sum + byte) & 0xff, 0);
|
||||
return new Uint8Array([...bytes, checksum, 0x0a]);
|
||||
}
|
||||
|
||||
function improvParseRpcPayload(payload) {
|
||||
const bytes = Array.from(payload || []);
|
||||
const command = Number(bytes[0] || 0);
|
||||
const dataLength = Number(bytes[1] || 0);
|
||||
const dataEnd = Math.min(bytes.length, 2 + dataLength);
|
||||
const strings = [];
|
||||
let offset = 2;
|
||||
while (offset < dataEnd) {
|
||||
const length = Number(bytes[offset] || 0);
|
||||
offset += 1;
|
||||
if (offset + length > dataEnd) {
|
||||
break;
|
||||
}
|
||||
strings.push(IMPROV_TEXT_DECODER.decode(new Uint8Array(bytes.slice(offset, offset + length))));
|
||||
offset += length;
|
||||
}
|
||||
return { command, strings };
|
||||
}
|
||||
|
||||
class ImprovSerialParser {
|
||||
constructor() {
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
feed(chunk) {
|
||||
const bytes = chunk instanceof Uint8Array ? Array.from(chunk) : [];
|
||||
this.buffer.push(...bytes);
|
||||
const packets = [];
|
||||
while (this.buffer.length >= IMPROV_SERIAL_HEADER.length) {
|
||||
const headerIndex = this.buffer.findIndex((byte, index, rows) =>
|
||||
IMPROV_SERIAL_HEADER.every((headerByte, headerOffset) => rows[index + headerOffset] === headerByte)
|
||||
);
|
||||
if (headerIndex < 0) {
|
||||
this.buffer.splice(0, Math.max(0, this.buffer.length - IMPROV_SERIAL_HEADER.length + 1));
|
||||
break;
|
||||
}
|
||||
if (headerIndex > 0) {
|
||||
this.buffer.splice(0, headerIndex);
|
||||
}
|
||||
if (this.buffer.length < 10) {
|
||||
break;
|
||||
}
|
||||
if (this.buffer[6] !== IMPROV_SERIAL_VERSION) {
|
||||
this.buffer.shift();
|
||||
continue;
|
||||
}
|
||||
const type = Number(this.buffer[7] || 0);
|
||||
const payloadLength = Number(this.buffer[8] || 0);
|
||||
const checksumIndex = 9 + payloadLength;
|
||||
if (this.buffer.length <= checksumIndex) {
|
||||
break;
|
||||
}
|
||||
const checksum = this.buffer
|
||||
.slice(0, checksumIndex)
|
||||
.reduce((sum, byte) => (sum + Number(byte || 0)) & 0xff, 0);
|
||||
if (checksum !== Number(this.buffer[checksumIndex] || 0)) {
|
||||
this.buffer.shift();
|
||||
continue;
|
||||
}
|
||||
const payload = new Uint8Array(this.buffer.slice(9, 9 + payloadLength));
|
||||
const nextOffset = this.buffer[checksumIndex + 1] === 0x0a ? checksumIndex + 2 : checksumIndex + 1;
|
||||
this.buffer.splice(0, nextOffset);
|
||||
packets.push({ type, payload });
|
||||
}
|
||||
return packets;
|
||||
}
|
||||
}
|
||||
|
||||
async function openImprovSerialPort(port) {
|
||||
for (let attempt = 1; attempt <= 8; attempt += 1) {
|
||||
try {
|
||||
await port.open({ baudRate: 115200, bufferSize: 255 });
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = String(error?.message || error || "");
|
||||
if (error?.name === "InvalidStateError" || message.toLowerCase().includes("already open")) {
|
||||
return;
|
||||
}
|
||||
if (attempt >= 8) {
|
||||
throw error;
|
||||
}
|
||||
appendFirmwareLog(`Waiting for USB serial to reconnect (${attempt}/8)...`, "Waiting for USB serial...");
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupImprovWifi(port, options) {
|
||||
const ssid = String(options?.ssid || "").trim();
|
||||
const password = String(options?.password || "");
|
||||
if (!ssid) {
|
||||
appendFirmwareLog("Wi-Fi setup skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
appendFirmwareLog("Waiting for the flashed device to restart into Improv Serial.", "Waiting for Improv Serial...");
|
||||
appendFirmwareLog("If the screen stays off, unplug and reconnect USB once; the trainer will keep watching for Improv.");
|
||||
await sleep(10000);
|
||||
await openImprovSerialPort(port);
|
||||
appendFirmwareLog("USB serial reopened. Checking Improv Serial.", "Checking Improv Serial...");
|
||||
|
||||
const parser = new ImprovSerialParser();
|
||||
const reader = port.readable?.getReader?.();
|
||||
const writer = port.writable?.getWriter?.();
|
||||
if (!reader || !writer) {
|
||||
throw new Error("USB serial reader/writer was not available for Wi-Fi setup.");
|
||||
}
|
||||
|
||||
let stopped = false;
|
||||
let currentState = null;
|
||||
let currentError = IMPROV_SERIAL_ERROR.NONE;
|
||||
let lastStateLabel = "";
|
||||
let responseUrls = [];
|
||||
let seenImprovPacket = false;
|
||||
|
||||
const handlePacket = (packet) => {
|
||||
if (!packet || typeof packet !== "object") {
|
||||
return;
|
||||
}
|
||||
seenImprovPacket = true;
|
||||
if (packet.type === IMPROV_SERIAL_TYPE.CURRENT_STATE) {
|
||||
currentState = Number(packet.payload?.[0] ?? -1);
|
||||
const label = improvSerialStateLabel(currentState);
|
||||
if (label !== lastStateLabel) {
|
||||
lastStateLabel = label;
|
||||
appendFirmwareLog(`Improv: ${label}.`, `Wi-Fi setup: ${label}.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (packet.type === IMPROV_SERIAL_TYPE.ERROR_STATE) {
|
||||
currentError = Number(packet.payload?.[0] ?? IMPROV_SERIAL_ERROR.UNKNOWN);
|
||||
if (currentError !== IMPROV_SERIAL_ERROR.NONE) {
|
||||
appendFirmwareLog(`Improv error: ${improvSerialErrorLabel(currentError)}.`, "Wi-Fi setup failed.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (packet.type === IMPROV_SERIAL_TYPE.RPC_RESPONSE) {
|
||||
const response = improvParseRpcPayload(packet.payload);
|
||||
if (response.command === IMPROV_SERIAL_COMMAND.GET_DEVICE_INFO && response.strings.length) {
|
||||
appendFirmwareLog(`Device info: ${response.strings.filter(Boolean).join(" • ")}`);
|
||||
}
|
||||
if (
|
||||
response.command === IMPROV_SERIAL_COMMAND.WIFI_SETTINGS ||
|
||||
response.command === IMPROV_SERIAL_COMMAND.GET_CURRENT_STATE
|
||||
) {
|
||||
responseUrls = response.strings.filter(Boolean);
|
||||
if (responseUrls.length) {
|
||||
appendFirmwareLog(`Device URL: ${responseUrls[0]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const readLoop = (async () => {
|
||||
while (!stopped) {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
if (result?.done) {
|
||||
break;
|
||||
}
|
||||
parser.feed(result?.value).forEach(handlePacket);
|
||||
} catch (error) {
|
||||
if (!stopped) {
|
||||
appendFirmwareLog(`Improv read warning: ${String(error?.message || error)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const sendRpc = async (command, strings = []) => {
|
||||
await browserUsbWithTimeout(
|
||||
writer.write(improvBuildSerialPacket(IMPROV_SERIAL_TYPE.RPC, improvBuildRpcPayload(command, strings))),
|
||||
4000,
|
||||
"Timed out writing Improv Serial command."
|
||||
);
|
||||
};
|
||||
|
||||
let pollTimer = 0;
|
||||
try {
|
||||
await sendRpc(IMPROV_SERIAL_COMMAND.GET_DEVICE_INFO);
|
||||
await sendRpc(IMPROV_SERIAL_COMMAND.GET_CURRENT_STATE);
|
||||
const helloDeadline = Date.now() + 8000;
|
||||
while (!seenImprovPacket && Date.now() < helloDeadline) {
|
||||
if (currentError && currentError !== IMPROV_SERIAL_ERROR.NONE) {
|
||||
throw new Error(improvSerialErrorLabel(currentError));
|
||||
}
|
||||
await sleep(150);
|
||||
}
|
||||
if (!seenImprovPacket) {
|
||||
throw new Error("No Improv Serial response from the flashed firmware. Turn off Wi-Fi setup for images with saved Wi-Fi, or flash an image that includes improv_serial.");
|
||||
}
|
||||
appendFirmwareLog("Connected to Improv Serial.", "Connected to Improv Serial.");
|
||||
await sleep(300);
|
||||
appendFirmwareLog(`Sending Wi-Fi credentials for ${ssid}.`, `Sending Wi-Fi credentials for ${ssid}...`);
|
||||
await sendRpc(IMPROV_SERIAL_COMMAND.WIFI_SETTINGS, [ssid, password]);
|
||||
pollTimer = setInterval(() => {
|
||||
void sendRpc(IMPROV_SERIAL_COMMAND.GET_CURRENT_STATE).catch(() => {});
|
||||
}, 1500);
|
||||
|
||||
const deadline = Date.now() + 45000;
|
||||
while (Date.now() < deadline) {
|
||||
if (currentError && currentError !== IMPROV_SERIAL_ERROR.NONE) {
|
||||
throw new Error(improvSerialErrorLabel(currentError));
|
||||
}
|
||||
if (currentState === IMPROV_SERIAL_STATE.PROVISIONED) {
|
||||
const urlSuffix = responseUrls.length ? ` ${responseUrls[0]}` : "";
|
||||
appendFirmwareLog(`Wi-Fi setup complete.${urlSuffix}`, "Wi-Fi setup complete.");
|
||||
return;
|
||||
}
|
||||
await sleep(300);
|
||||
}
|
||||
throw new Error("Timed out waiting for Improv Serial Wi-Fi setup.");
|
||||
} finally {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
stopped = true;
|
||||
try {
|
||||
await reader.cancel();
|
||||
} catch (_error) {
|
||||
// Ignore reader cancellation cleanup failures.
|
||||
}
|
||||
try {
|
||||
await readLoop;
|
||||
} catch (_error) {
|
||||
// Ignore read-loop cleanup failures.
|
||||
}
|
||||
try {
|
||||
reader.releaseLock();
|
||||
} catch (_error) {
|
||||
// Ignore lock cleanup failures.
|
||||
}
|
||||
try {
|
||||
writer.releaseLock();
|
||||
} catch (_error) {
|
||||
// Ignore lock cleanup failures.
|
||||
}
|
||||
try {
|
||||
await port.close();
|
||||
} catch (_error) {
|
||||
// Ignore close cleanup failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeConsole() {
|
||||
$("consoleOverlay").classList.remove("open");
|
||||
$("consoleOverlay").setAttribute("aria-hidden", "true");
|
||||
@@ -2648,26 +3106,46 @@
|
||||
: "No files selected";
|
||||
}
|
||||
|
||||
const FIRMWARE_USB_DEVICE_VALUE = "__browser_usb_flash__";
|
||||
|
||||
function firmwareUsbSelected() {
|
||||
return $("firmwareDeviceSelect").value === FIRMWARE_USB_DEVICE_VALUE;
|
||||
}
|
||||
|
||||
function syncUsbFirmwareControls() {
|
||||
const enabled = $("firmwareUsbWifiEnabled").checked;
|
||||
$("firmwareUsbSsid").disabled = !enabled;
|
||||
$("firmwareUsbPassword").disabled = !enabled;
|
||||
$("usbFirmwareBtn").textContent = enabled ? "Flash + Set Up Wi-Fi" : "Browser USB Flash";
|
||||
}
|
||||
|
||||
function renderFirmwareDevices(devices, message) {
|
||||
const list = Array.isArray(devices) ? devices : [];
|
||||
uiState.firmware.devices = list;
|
||||
|
||||
if (!list.length) {
|
||||
$("firmwareDeviceSelect").innerHTML = `<option value="">No devices detected</option>`;
|
||||
setPill($("firmwareDetectStatus"), message || "No devices detected", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
$("firmwareDeviceSelect").innerHTML = [
|
||||
`<option value="">Choose detected device...</option>`,
|
||||
`<option value="">Choose target...</option>`,
|
||||
`<option value="${FIRMWARE_USB_DEVICE_VALUE}">Browser USB Flash (new/recovery)</option>`,
|
||||
...list.map((device, index) => {
|
||||
const label = `${device.name || device.host} (${device.host}:${device.port || 3232})`;
|
||||
return `<option value="${index}">${escapeHtml(label)}</option>`;
|
||||
}),
|
||||
].join("");
|
||||
|
||||
if (!list.length) {
|
||||
setPill($("firmwareDetectStatus"), message || "No devices detected", "warn");
|
||||
if (!($("firmwareHost").value || "").trim()) {
|
||||
$("firmwareDeviceSelect").value = FIRMWARE_USB_DEVICE_VALUE;
|
||||
applySelectedFirmwareDevice().catch((error) => {
|
||||
setPill($("firmwareStatus"), "USB settings failed", "warn");
|
||||
console.warn("USB settings load failed", error);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setPill($("firmwareDetectStatus"), `${list.length} detected`, "ok");
|
||||
if (!($("firmwareHost").value || "").trim()) {
|
||||
if (!($("firmwareHost").value || "").trim() && !firmwareUsbSelected()) {
|
||||
$("firmwareDeviceSelect").value = "0";
|
||||
applySelectedFirmwareDevice().catch((error) => {
|
||||
setPill($("firmwareStatus"), "Device settings failed", "warn");
|
||||
@@ -2687,6 +3165,14 @@
|
||||
async function applySelectedFirmwareDevice() {
|
||||
const indexText = $("firmwareDeviceSelect").value;
|
||||
if (indexText === "") return;
|
||||
if (indexText === FIRMWARE_USB_DEVICE_VALUE) {
|
||||
$("firmwareHost").value = "";
|
||||
$("firmwarePort").value = "3232";
|
||||
setPill($("firmwareStatus"), "Browser USB selected", "ok");
|
||||
await refreshFirmwareTemplates();
|
||||
syncButtons();
|
||||
return;
|
||||
}
|
||||
const device = uiState.firmware.devices[Number(indexText)];
|
||||
if (!device) return;
|
||||
await flushFirmwareProfileSave();
|
||||
@@ -2711,6 +3197,7 @@
|
||||
|
||||
function applyFirmwareTemplateTarget(template = selectedFirmwareTemplate()) {
|
||||
if (!template) return;
|
||||
if (firmwareUsbSelected()) return;
|
||||
if (template.target_host) {
|
||||
$("firmwareHost").value = template.target_host;
|
||||
}
|
||||
@@ -3023,6 +3510,9 @@
|
||||
|
||||
function firmwareTemplateQuery() {
|
||||
const params = new URLSearchParams();
|
||||
if (firmwareUsbSelected()) {
|
||||
return "";
|
||||
}
|
||||
const host = ($("firmwareHost").value || "").trim();
|
||||
const port = ($("firmwarePort").value || "3232").trim();
|
||||
if (host) params.set("target_host", host);
|
||||
@@ -3200,12 +3690,20 @@
|
||||
alert("Browser USB flash needs Chrome or Edge on HTTPS or localhost. In the macOS app, use Open in Browser and choose Chrome or Edge.");
|
||||
return;
|
||||
}
|
||||
const ok = confirm(`USB flash the prebuilt ${template.label || template.value} factory firmware?\n\nThis is for new satellites, recovery, or devices older than Tater firmware 3.0.3. It erases flash and writes the factory image over USB.`);
|
||||
const wifiEnabled = $("firmwareUsbWifiEnabled").checked;
|
||||
const wifiSsid = ($("firmwareUsbSsid").value || "").trim();
|
||||
const wifiPassword = $("firmwareUsbPassword").value || "";
|
||||
if (wifiEnabled && !wifiSsid) {
|
||||
alert("Enter a Wi-Fi SSID or turn off Wi-Fi setup before Browser USB Flash.");
|
||||
return;
|
||||
}
|
||||
const actionLabel = wifiEnabled ? "USB flash and set up Wi-Fi" : "USB flash";
|
||||
const ok = confirm(`${actionLabel} for the prebuilt ${template.label || template.value} factory firmware?\n\nThis is for new satellites, recovery, or devices older than Tater firmware 3.0.3. It erases flash and writes the factory image over USB.`);
|
||||
if (!ok) return;
|
||||
|
||||
let port;
|
||||
try {
|
||||
port = await browserUsbSelectPort(template.value);
|
||||
port = browserUsbStoredPort(template.value) || await browserUsbSelectPort(template.value);
|
||||
} catch (error) {
|
||||
alert("USB device selection failed: " + error.message);
|
||||
return;
|
||||
@@ -3233,8 +3731,16 @@
|
||||
});
|
||||
appendFirmwareLogs(artifact.entries || [], "Factory image ready. Starting USB flash...");
|
||||
await flashBrowserUsbPort(port, artifact);
|
||||
appendFirmwareLog("Browser USB flash finished.", "Browser USB flash finished.");
|
||||
setPill($("firmwareStatus"), "USB flash finished", "ok");
|
||||
if (wifiEnabled) {
|
||||
port = await browserUsbWaitForReconnect(port, template.value);
|
||||
await setupImprovWifi(port, { ssid: wifiSsid, password: wifiPassword });
|
||||
appendFirmwareLog("Browser USB flash and Wi-Fi setup finished.", "Browser USB flash and Wi-Fi setup finished.");
|
||||
setPill($("firmwareStatus"), "USB flash + Wi-Fi finished", "ok");
|
||||
} else {
|
||||
appendFirmwareLog("Wi-Fi setup skipped by user.");
|
||||
appendFirmwareLog("Browser USB flash finished.", "Browser USB flash finished.");
|
||||
setPill($("firmwareStatus"), "USB flash finished", "ok");
|
||||
}
|
||||
} catch (error) {
|
||||
appendFirmwareLog(`Browser USB flash failed: ${String(error.message || error)}`, "Browser USB flash failed.");
|
||||
setPill($("firmwareStatus"), "USB flash failed", "err");
|
||||
@@ -3320,6 +3826,11 @@
|
||||
const firmwareTemplate = ($("firmwareTemplate").value || "").trim();
|
||||
const firmwareSelection = selectedFirmwareTemplate();
|
||||
const firmwareAvailable = !firmwareSelection?.prebuilt_firmware || Boolean(firmwareSelection.prebuilt_firmware.available);
|
||||
const usbSelected = firmwareUsbSelected();
|
||||
const usbWifiEnabled = $("firmwareUsbWifiEnabled").checked;
|
||||
const usbWifiSsid = ($("firmwareUsbSsid").value || "").trim();
|
||||
const usbWifiReady = !usbWifiEnabled || Boolean(usbWifiSsid);
|
||||
syncUsbFirmwareControls();
|
||||
|
||||
$("ttsBtn").disabled = !hasPhrase || uiState.uploadBusy;
|
||||
$("uploadBtn").disabled = !hasSession || !hasSelected || uiState.uploadBusy;
|
||||
@@ -3331,11 +3842,11 @@
|
||||
$("clearNegativeBtn").disabled = uiState.reviewBusy || negativeCount === 0;
|
||||
$("refreshSamplesBtn").disabled = uiState.reviewBusy || uiState.uploadBusy;
|
||||
$("refreshFirmwareBtn").disabled = uiState.firmwareBusy;
|
||||
$("saveFirmwareSettingsBtn").disabled = uiState.firmwareBusy || !firmwareHost || !firmwareTemplate;
|
||||
$("saveFirmwareSettingsBtn").disabled = uiState.firmwareBusy || usbSelected || !firmwareHost || !firmwareTemplate;
|
||||
$("cleanFirmwareBtn").disabled = uiState.firmwareBusy;
|
||||
$("openFirmwareConsoleBtn").disabled = false;
|
||||
$("flashFirmwareBtn").disabled = uiState.firmwareBusy || !firmwareHost || !firmwareTemplate || !firmwareAvailable;
|
||||
$("usbFirmwareBtn").disabled = uiState.firmwareBusy || !firmwareTemplate || !firmwareSelection?.prebuilt_firmware?.artifacts?.factory?.path;
|
||||
$("flashFirmwareBtn").disabled = uiState.firmwareBusy || usbSelected || !firmwareHost || !firmwareTemplate || !firmwareAvailable;
|
||||
$("usbFirmwareBtn").disabled = uiState.firmwareBusy || !firmwareTemplate || !firmwareSelection?.prebuilt_firmware?.artifacts?.factory?.path || !usbWifiReady;
|
||||
}
|
||||
|
||||
function refreshSessionUI(session) {
|
||||
@@ -3695,6 +4206,10 @@
|
||||
alert("Device settings failed: " + error.message);
|
||||
});
|
||||
});
|
||||
["firmwareUsbWifiEnabled", "firmwareUsbSsid", "firmwareUsbPassword"].forEach((id) => {
|
||||
$(id).addEventListener("input", syncButtons);
|
||||
$(id).addEventListener("change", syncButtons);
|
||||
});
|
||||
$("refreshFirmwareBtn").addEventListener("click", async () => {
|
||||
try {
|
||||
await refreshFirmwareDevices();
|
||||
|
||||
Reference in New Issue
Block a user