fix: Different approach on the COM Port and MQTT Fix

This commit is contained in:
2026-02-27 09:57:49 +02:00
parent 1a5e448e4e
commit 12784462f9

View File

@@ -401,25 +401,37 @@ function StepFlash({ device, onFlashed }) {
// Start reading raw UART output from the device after flash+reset // Start reading raw UART output from the device after flash+reset
const startSerialMonitor = async (port) => { const startSerialMonitor = async (port) => {
serialActiveRef.current = true; serialActiveRef.current = true;
// Give the OS a moment to fully release the port from esptool before we re-open
await new Promise((r) => setTimeout(r, 500)); // Wait for the OS/browser to fully release the port after esptool closed it
await new Promise((r) => setTimeout(r, 1000));
try { try {
await port.open({ baudRate: 115200 }); await port.open({ baudRate: 115200 });
} catch (openErr) { } catch (openErr) {
appendSerial(`[Error opening port: ${openErr.message}]`); appendSerial(`[Error opening port: ${openErr.message}]`);
scrollSerial();
return;
}
// Use getReader() directly — avoids locking issues from pipeTo()
let reader;
try {
reader = port.readable.getReader();
} catch (readerErr) {
appendSerial(`[Error getting reader: ${readerErr.message}]`);
scrollSerial();
try { await port.close(); } catch (_) {}
return; return;
} }
const decoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();
serialReaderRef.current = reader; serialReaderRef.current = reader;
const textDecoder = new TextDecoder();
let lineBuffer = ""; let lineBuffer = "";
try { try {
while (serialActiveRef.current) { while (serialActiveRef.current) {
const { value, done: streamDone } = await reader.read(); const { value, done: streamDone } = await reader.read();
if (streamDone) break; if (streamDone) break;
lineBuffer += value; lineBuffer += textDecoder.decode(value, { stream: true });
const lines = lineBuffer.split(/\r?\n/); const lines = lineBuffer.split(/\r?\n/);
lineBuffer = lines.pop(); // keep incomplete last fragment lineBuffer = lines.pop(); // keep incomplete last fragment
for (const line of lines) { for (const line of lines) {
@@ -431,13 +443,16 @@ function StepFlash({ device, onFlashed }) {
} }
} catch (_) { } catch (_) {
// Reader cancelled on cleanup — expected // Reader cancelled on cleanup — expected
} finally {
try { reader.releaseLock(); } catch (_) {}
} }
}; };
const stopSerialMonitor = async () => { const stopSerialMonitor = async () => {
serialActiveRef.current = false; serialActiveRef.current = false;
try { await serialReaderRef.current?.cancel(); } catch (_) {} try { await serialReaderRef.current?.cancel(); } catch (_) {}
try { portRef.current?.close(); } catch (_) {} try { serialReaderRef.current?.releaseLock(); } catch (_) {}
try { await portRef.current?.close(); } catch (_) {}
}; };
const handleFlash = async () => { const handleFlash = async () => {
@@ -724,31 +739,30 @@ function StepVerify({ device, onVerified }) {
setTimedOut(false); setTimedOut(false);
setError(""); setError("");
const startTime = Date.now(); const startTime = new Date().toISOString();
intervalRef.current = setInterval(async () => { intervalRef.current = setInterval(async () => {
try { try {
const data = await api.get(`/manufacturing/devices/${device.serial_number}`); // Poll the heartbeat endpoint — device is verified when it sends a heartbeat
if (data.mfg_status === "provisioned") { // after we started polling (i.e. after the flash completed)
clearInterval(intervalRef.current); const hbData = await api.get(
clearTimeout(timeoutRef.current); `/mqtt/heartbeats/${device.serial_number}?limit=1&offset=0`
onVerified(data); );
return; if (hbData.heartbeats && hbData.heartbeats.length > 0) {
} const latest = hbData.heartbeats[0];
// Also accept any last_seen update (heartbeat) as evidence of life const receivedAt = latest.received_at;
if (data.last_seen) { if (receivedAt && receivedAt > startTime) {
const ts = new Date(data.last_seen).getTime();
if (ts > startTime) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
// Promote to provisioned // Promote device status to provisioned
try { try {
await api.request(`/manufacturing/devices/${device.serial_number}/status`, { await api.request(`/manufacturing/devices/${device.serial_number}/status`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: "provisioned", note: "Auto-verified via wizard" }), body: JSON.stringify({ status: "provisioned", note: "Auto-verified via wizard" }),
}); });
} catch (_) {} } catch (_) {}
onVerified({ ...data, mfg_status: "provisioned" }); const deviceData = await api.get(`/manufacturing/devices/${device.serial_number}`);
onVerified({ ...deviceData, mfg_status: "provisioned" });
return; return;
} }
} }