diff --git a/frontend/src/manufacturing/ProvisioningWizard.jsx b/frontend/src/manufacturing/ProvisioningWizard.jsx index ea004ca..2fc365b 100644 --- a/frontend/src/manufacturing/ProvisioningWizard.jsx +++ b/frontend/src/manufacturing/ProvisioningWizard.jsx @@ -401,25 +401,37 @@ function StepFlash({ device, onFlashed }) { // Start reading raw UART output from the device after flash+reset const startSerialMonitor = async (port) => { 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 { await port.open({ baudRate: 115200 }); } catch (openErr) { 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; } - const decoder = new TextDecoderStream(); - const readableStreamClosed = port.readable.pipeTo(decoder.writable); - const reader = decoder.readable.getReader(); serialReaderRef.current = reader; + const textDecoder = new TextDecoder(); let lineBuffer = ""; try { while (serialActiveRef.current) { const { value, done: streamDone } = await reader.read(); if (streamDone) break; - lineBuffer += value; + lineBuffer += textDecoder.decode(value, { stream: true }); const lines = lineBuffer.split(/\r?\n/); lineBuffer = lines.pop(); // keep incomplete last fragment for (const line of lines) { @@ -431,13 +443,16 @@ function StepFlash({ device, onFlashed }) { } } catch (_) { // Reader cancelled on cleanup — expected + } finally { + try { reader.releaseLock(); } catch (_) {} } }; const stopSerialMonitor = async () => { serialActiveRef.current = false; 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 () => { @@ -724,31 +739,30 @@ function StepVerify({ device, onVerified }) { setTimedOut(false); setError(""); - const startTime = Date.now(); + const startTime = new Date().toISOString(); intervalRef.current = setInterval(async () => { try { - const data = await api.get(`/manufacturing/devices/${device.serial_number}`); - if (data.mfg_status === "provisioned") { - clearInterval(intervalRef.current); - clearTimeout(timeoutRef.current); - onVerified(data); - return; - } - // Also accept any last_seen update (heartbeat) as evidence of life - if (data.last_seen) { - const ts = new Date(data.last_seen).getTime(); - if (ts > startTime) { + // Poll the heartbeat endpoint — device is verified when it sends a heartbeat + // after we started polling (i.e. after the flash completed) + const hbData = await api.get( + `/mqtt/heartbeats/${device.serial_number}?limit=1&offset=0` + ); + if (hbData.heartbeats && hbData.heartbeats.length > 0) { + const latest = hbData.heartbeats[0]; + const receivedAt = latest.received_at; + if (receivedAt && receivedAt > startTime) { clearInterval(intervalRef.current); clearTimeout(timeoutRef.current); - // Promote to provisioned + // Promote device status to provisioned try { await api.request(`/manufacturing/devices/${device.serial_number}/status`, { method: "PATCH", body: JSON.stringify({ status: "provisioned", note: "Auto-verified via wizard" }), }); } catch (_) {} - onVerified({ ...data, mfg_status: "provisioned" }); + const deviceData = await api.get(`/manufacturing/devices/${device.serial_number}`); + onVerified({ ...deviceData, mfg_status: "provisioned" }); return; } }