fix: configure gitea webhook auto-deploy, fix NVS CRC, and improve flash UI
- Add deploy-host.sh for webhook-triggered docker redeploy - Update docker-compose.yml and nginx.conf for auto-pull setup - Fix vite.config.js and admin router for deployment environment - Fix NVS CRC seed to use 0xFFFFFFFF to match esp_rom_crc32_le - Add dual-panel flash UI: esptool log + live 115200 serial monitor - Auto-reset device via RTS after flash (no manual power cycle needed) - Clean up Header.jsx debug title text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ export default function Header() {
|
||||
}}
|
||||
>
|
||||
<h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>
|
||||
BellSystems - Control Panel
|
||||
BellCloud™ - Console
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -41,3 +41,5 @@ export default function Header() {
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/* my test string */
|
||||
@@ -360,13 +360,25 @@ function StepSelectDevice({ onSelected }) {
|
||||
function StepFlash({ device, onFlashed }) {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [flashing, setFlashing] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
const [nvsProgress, setNvsProgress] = useState(0);
|
||||
const [fwProgress, setFwProgress] = useState(0);
|
||||
const [log, setLog] = useState([]);
|
||||
const [serial, setSerial] = useState([]);
|
||||
const [error, setError] = useState("");
|
||||
const loaderRef = useRef(null);
|
||||
const portRef = useRef(null);
|
||||
const serialReaderRef = useRef(null);
|
||||
const serialActiveRef = useRef(false);
|
||||
const logEndRef = useRef(null);
|
||||
const serialEndRef = useRef(null);
|
||||
|
||||
const appendLog = (msg) => setLog((prev) => [...prev, msg]);
|
||||
const appendLog = (msg) => setLog((prev) => [...prev, String(msg)]);
|
||||
const appendSerial = (msg) => setSerial((prev) => [...prev, String(msg)]);
|
||||
|
||||
// Auto-scroll both panels to bottom
|
||||
const scrollLog = () => logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
const scrollSerial = () => serialEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
|
||||
const fetchBinary = async (url) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
@@ -382,17 +394,56 @@ function StepFlash({ device, onFlashed }) {
|
||||
const arrayBufferToString = (buf) => {
|
||||
const bytes = new Uint8Array(buf);
|
||||
let str = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
str += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
|
||||
return str;
|
||||
};
|
||||
|
||||
// Start reading raw UART output from the device after flash+reset
|
||||
const startSerialMonitor = async (port) => {
|
||||
serialActiveRef.current = true;
|
||||
try {
|
||||
await port.open({ baudRate: 115200 });
|
||||
} catch (_) {
|
||||
// Port may already be open if esptool left it that way — ignore
|
||||
}
|
||||
const decoder = new TextDecoderStream();
|
||||
const readableStreamClosed = port.readable.pipeTo(decoder.writable);
|
||||
const reader = decoder.readable.getReader();
|
||||
serialReaderRef.current = reader;
|
||||
|
||||
let lineBuffer = "";
|
||||
try {
|
||||
while (serialActiveRef.current) {
|
||||
const { value, done: streamDone } = await reader.read();
|
||||
if (streamDone) break;
|
||||
lineBuffer += value;
|
||||
const lines = lineBuffer.split(/\r?\n/);
|
||||
lineBuffer = lines.pop(); // keep incomplete last fragment
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
appendSerial(line);
|
||||
scrollSerial();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Reader cancelled on cleanup — expected
|
||||
}
|
||||
};
|
||||
|
||||
const stopSerialMonitor = async () => {
|
||||
serialActiveRef.current = false;
|
||||
try { await serialReaderRef.current?.cancel(); } catch (_) {}
|
||||
try { portRef.current?.close(); } catch (_) {}
|
||||
};
|
||||
|
||||
const handleFlash = async () => {
|
||||
setError("");
|
||||
setLog([]);
|
||||
setSerial([]);
|
||||
setNvsProgress(0);
|
||||
setFwProgress(0);
|
||||
setDone(false);
|
||||
|
||||
// 1. Open Web Serial port
|
||||
let port;
|
||||
@@ -400,6 +451,7 @@ function StepFlash({ device, onFlashed }) {
|
||||
setConnecting(true);
|
||||
appendLog("Opening port picker…");
|
||||
port = await navigator.serial.requestPort();
|
||||
portRef.current = port;
|
||||
} catch (err) {
|
||||
setError(err.message || "Port selection cancelled.");
|
||||
setConnecting(false);
|
||||
@@ -427,23 +479,18 @@ function StepFlash({ device, onFlashed }) {
|
||||
baudrate: FLASH_BAUD,
|
||||
terminal: {
|
||||
clean() {},
|
||||
writeLine: (line) => appendLog(line),
|
||||
write: (msg) => appendLog(msg),
|
||||
writeLine: (line) => { appendLog(line); scrollLog(); },
|
||||
write: (msg) => { appendLog(msg); scrollLog(); },
|
||||
},
|
||||
});
|
||||
|
||||
await loaderRef.current.main();
|
||||
appendLog("ESP32 connected.");
|
||||
|
||||
// 4. Flash NVS + firmware with progress callbacks
|
||||
// 4. Flash NVS + firmware
|
||||
const nvsData = arrayBufferToString(nvsBuffer);
|
||||
const fwData = arrayBufferToString(fwBuffer);
|
||||
|
||||
// Track progress by watching the two images in sequence.
|
||||
// esptool-js reports progress as { index, fileIndex, written, total }
|
||||
const totalBytes = nvsBuffer.byteLength + fwBuffer.byteLength;
|
||||
let writtenSoFar = 0;
|
||||
|
||||
await loaderRef.current.writeFlash({
|
||||
fileArray: [
|
||||
{ data: nvsData, address: NVS_ADDRESS },
|
||||
@@ -461,41 +508,46 @@ function StepFlash({ device, onFlashed }) {
|
||||
setNvsProgress(100);
|
||||
setFwProgress((written / total) * 100);
|
||||
}
|
||||
writtenSoFar = written;
|
||||
},
|
||||
calculateMD5Hash: (image) => {
|
||||
// MD5 is optional for progress verification; returning empty disables it
|
||||
return "";
|
||||
},
|
||||
calculateMD5Hash: () => "",
|
||||
});
|
||||
|
||||
setNvsProgress(100);
|
||||
setFwProgress(100);
|
||||
appendLog("Flash complete. Disconnecting…");
|
||||
await transport.disconnect();
|
||||
appendLog("Done.");
|
||||
appendLog("Flash complete. Resetting device…");
|
||||
|
||||
// 5. Update device status → flashed
|
||||
// 5. Hard-reset via RTS — device boots into user code automatically
|
||||
await loaderRef.current.after("hard_reset");
|
||||
appendLog("Hard reset sent. Device is booting…");
|
||||
|
||||
// 6. Update device status → flashed
|
||||
await api.request(`/manufacturing/devices/${device.serial_number}/status`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "flashed", note: "Flashed via browser provisioning wizard" }),
|
||||
});
|
||||
|
||||
setFlashing(false);
|
||||
setDone(true);
|
||||
|
||||
// 7. Open serial monitor at 115200 to show live device output
|
||||
appendSerial("── Serial monitor started (115200 baud) ──");
|
||||
startSerialMonitor(port);
|
||||
|
||||
onFlashed();
|
||||
} catch (err) {
|
||||
setError(err.message || String(err));
|
||||
setFlashing(false);
|
||||
setConnecting(false);
|
||||
try {
|
||||
if (loaderRef.current) await loaderRef.current.transport?.disconnect();
|
||||
} catch (_) {}
|
||||
try { await loaderRef.current?.transport?.disconnect(); } catch (_) {}
|
||||
}
|
||||
};
|
||||
|
||||
const webSerialAvailable = "serial" in navigator;
|
||||
const busy = connecting || flashing;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Device info header */}
|
||||
<div
|
||||
className="rounded-lg border p-5"
|
||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||
@@ -535,42 +587,101 @@ function StepFlash({ device, onFlashed }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.length > 0 && (
|
||||
<div
|
||||
className="rounded-md border p-3 mb-4 font-mono text-xs overflow-y-auto max-h-36 space-y-0.5"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
borderColor: "var(--border-secondary)",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
{log.map((line, i) => (
|
||||
<div key={i}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!flashing && (
|
||||
{!busy && (
|
||||
<button
|
||||
onClick={handleFlash}
|
||||
disabled={!webSerialAvailable || connecting}
|
||||
disabled={!webSerialAvailable}
|
||||
className="flex items-center gap-2 px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
|
||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
{connecting ? "Connecting…" : "Connect & Flash Device"}
|
||||
{done ? "Flash Again" : "Connect & Flash Device"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{flashing && nvsProgress < 100 && fwProgress < 100 && (
|
||||
{flashing && (
|
||||
<p className="text-sm" style={{ color: "var(--text-muted)" }}>
|
||||
Flashing in progress — do not disconnect…
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Two-column output: Flash log + Serial output */}
|
||||
{(log.length > 0 || serial.length > 0) && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Left: flash / esptool output */}
|
||||
<div className="rounded-lg border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
|
||||
<div
|
||||
className="px-3 py-2 text-xs font-semibold uppercase tracking-wide border-b"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-card)",
|
||||
borderColor: "var(--border-primary)",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
Flash Output
|
||||
</div>
|
||||
<div
|
||||
className="p-3 font-mono text-xs overflow-y-auto space-y-0.5"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
color: "var(--text-secondary)",
|
||||
height: "240px",
|
||||
}}
|
||||
>
|
||||
{log.map((line, i) => (
|
||||
<div key={i}>{line}</div>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: live device serial output */}
|
||||
<div className="rounded-lg border overflow-hidden" style={{ borderColor: "var(--border-primary)" }}>
|
||||
<div
|
||||
className="px-3 py-2 text-xs font-semibold uppercase tracking-wide border-b flex items-center justify-between"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-card)",
|
||||
borderColor: "var(--border-primary)",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
<span>Serial Output</span>
|
||||
{serialActiveRef.current && (
|
||||
<span className="flex items-center gap-1.5" style={{ color: "#4dd6c8" }}>
|
||||
<span
|
||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: "#4dd6c8", animation: "pulse 1.5s infinite" }}
|
||||
/>
|
||||
Live
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="p-3 font-mono text-xs overflow-y-auto space-y-0.5"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
color: "#a3e635",
|
||||
height: "240px",
|
||||
}}
|
||||
>
|
||||
{serial.length === 0 ? (
|
||||
<span style={{ color: "var(--text-muted)" }}>
|
||||
{done ? "Waiting for device boot…" : "Available after flash completes."}
|
||||
</span>
|
||||
) : (
|
||||
serial.map((line, i) => (
|
||||
<div key={i}>{line}</div>
|
||||
))
|
||||
)}
|
||||
<div ref={serialEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
|
||||
Flash addresses: NVS at 0x9000 · Firmware at 0x10000 · Baud: {FLASH_BAUD}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user