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:
2026-02-27 09:14:58 +02:00
parent 57259c2c2f
commit 4ea8e56485
9 changed files with 198 additions and 75 deletions

View File

@@ -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 */

View File

@@ -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>