2026-09-03 21:30:32 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<!--
|
|
|
|
|
|
【临时功能 SPI-SPEEDTEST】SPI 链路测速独立页面
|
|
|
|
|
|
─────────────────────────────────────────────
|
|
|
|
|
|
完全自包含(无外部 css/js 依赖),不被任何其他页面引用。
|
|
|
|
|
|
删除方法:直接删掉本文件即可(固件端点删除方法见
|
|
|
|
|
|
main/monitor/spi_speed_test.cpp 文件头)。
|
|
|
|
|
|
|
|
|
|
|
|
用法:浏览器直接打开本文件,或经 serve.bat 访问
|
2026-09-07 12:19:26 +08:00
|
|
|
|
http://localhost:2625/spi_test.html
|
2026-09-03 21:30:32 +08:00
|
|
|
|
-->
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
|
<title>SPI 测速(临时工具)</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
|
|
|
|
background: #14171c; color: #d8dee6;
|
|
|
|
|
|
max-width: 760px; margin: 0 auto; padding: 24px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
h1 { font-size: 20px; margin-bottom: 4px; }
|
|
|
|
|
|
.sub { color: #7a8494; font-size: 12px; margin-bottom: 20px; }
|
|
|
|
|
|
.card {
|
|
|
|
|
|
background: #1c2129; border: 1px solid #2a313c; border-radius: 8px;
|
|
|
|
|
|
padding: 16px; margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
label { font-size: 13px; color: #9aa5b3; margin-right: 6px; }
|
|
|
|
|
|
input {
|
|
|
|
|
|
background: #12151a; color: #d8dee6; border: 1px solid #2a313c;
|
|
|
|
|
|
border-radius: 4px; padding: 6px 8px; font-size: 14px; width: 130px;
|
|
|
|
|
|
}
|
|
|
|
|
|
input.narrow { width: 70px; }
|
|
|
|
|
|
.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 12px; }
|
|
|
|
|
|
button {
|
|
|
|
|
|
background: #2563eb; color: #fff; border: 0; border-radius: 6px;
|
|
|
|
|
|
padding: 8px 18px; font-size: 14px; cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
button:hover { background: #1d4ed8; }
|
|
|
|
|
|
button:disabled { background: #3a4250; cursor: wait; }
|
|
|
|
|
|
button.alt { background: #374151; }
|
|
|
|
|
|
button.alt:hover { background: #4b5563; }
|
|
|
|
|
|
pre {
|
|
|
|
|
|
background: #12151a; border: 1px solid #2a313c; border-radius: 6px;
|
|
|
|
|
|
padding: 12px; font-size: 14px; line-height: 1.7; white-space: pre-wrap;
|
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
pre.fail { border-color: #b91c1c; color: #fca5a5; }
|
|
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
|
|
|
|
th, td { text-align: left; padding: 5px 8px; border-bottom: 1px solid #2a313c; }
|
|
|
|
|
|
th { color: #7a8494; font-weight: normal; }
|
|
|
|
|
|
td.bad { color: #f87171; }
|
|
|
|
|
|
.hint { color: #7a8494; font-size: 12px; line-height: 1.7; margin-top: 8px; }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<h1>SPI 链路测速</h1>
|
|
|
|
|
|
<div class="sub">临时诊断工具 [SPI-SPEEDTEST] — 探针 0xA1 APP PING,删除时直接删掉本文件</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<label>设备 IP</label><input id="ip" value="192.168.31.127">
|
|
|
|
|
|
<label>帧数</label><input id="frames" class="narrow" value="200">
|
|
|
|
|
|
<label>载荷 B</label><input id="payload" class="narrow" value="236">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<button id="btnRun">开始测速</button>
|
|
|
|
|
|
<button id="btnRtt" class="alt">空载 RTT(500 帧)</button>
|
|
|
|
|
|
<button id="btnStress" class="alt">压力测试(2000 帧满载)</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="hint">
|
|
|
|
|
|
满帧 236B = 等效电机 OTA 0xD1 数据节奏;载荷 0 = 纯握手往返。<br>
|
|
|
|
|
|
判读:失败必须为 0;「最快」是链路物理极限周期;「平均-最快」差值是软件调度开销。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<pre id="out">等待测试…</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead><tr><th>时间</th><th>帧数</th><th>载荷</th><th>失败</th><th>平均</th><th>最快</th><th>吞吐</th></tr></thead>
|
|
|
|
|
|
<tbody id="hist"></tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<div class="hint">本页历史仅存内存,刷新即清空——方便改参数前后对比。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var out = document.getElementById("out");
|
|
|
|
|
|
var hist = document.getElementById("hist");
|
|
|
|
|
|
var btns = [
|
|
|
|
|
|
document.getElementById("btnRun"),
|
|
|
|
|
|
document.getElementById("btnRtt"),
|
|
|
|
|
|
document.getElementById("btnStress")
|
|
|
|
|
|
];
|
|
|
|
|
|
var ipInput = document.getElementById("ip");
|
|
|
|
|
|
|
|
|
|
|
|
var savedIp = null;
|
|
|
|
|
|
try { savedIp = window.localStorage.getItem("spi_test_ip"); } catch (e) {}
|
|
|
|
|
|
if (savedIp) { ipInput.value = savedIp; }
|
|
|
|
|
|
|
|
|
|
|
|
function setBusy(busy) {
|
|
|
|
|
|
for (var i = 0; i < btns.length; i++) { btns[i].disabled = busy; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pad2(n) { return (n < 10 ? "0" : "") + n; }
|
|
|
|
|
|
|
|
|
|
|
|
function addHistory(frames, payload, text) {
|
|
|
|
|
|
/* 从设备中文文本里提取关键数字做历史行 */
|
|
|
|
|
|
var fail = (text.match(/失败:\s*(\d+)/) || [])[1] || "?";
|
|
|
|
|
|
var avg = (text.match(/平均\s*([\d.]+)\s*µs/) || [])[1] || "?";
|
|
|
|
|
|
var min = (text.match(/最快\s*([\d.]+)\s*µs/) || [])[1] || "?";
|
|
|
|
|
|
var tput = (text.match(/等效吞吐:\s*([\d.]+)\s*KB\/s/) || [])[1] || "-";
|
|
|
|
|
|
var d = new Date();
|
|
|
|
|
|
var tr = document.createElement("tr");
|
|
|
|
|
|
var cells = [
|
|
|
|
|
|
pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()),
|
|
|
|
|
|
String(frames), String(payload) + "B", fail,
|
|
|
|
|
|
avg + "µs", min + "µs", tput === "-" ? "-" : tput + "KB/s"
|
|
|
|
|
|
];
|
|
|
|
|
|
for (var i = 0; i < cells.length; i++) {
|
|
|
|
|
|
var td = document.createElement("td");
|
|
|
|
|
|
td.textContent = cells[i];
|
|
|
|
|
|
if (i === 3 && fail !== "0") { td.className = "bad"; }
|
|
|
|
|
|
tr.appendChild(td);
|
|
|
|
|
|
}
|
|
|
|
|
|
hist.insertBefore(tr, hist.firstChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function run(frames, payload) {
|
|
|
|
|
|
var ip = ipInput.value.trim();
|
|
|
|
|
|
try { window.localStorage.setItem("spi_test_ip", ip); } catch (e) {}
|
|
|
|
|
|
var url = "http://" + ip + "/spi/speedtest?frames=" + frames + "&payload=" + payload;
|
|
|
|
|
|
setBusy(true);
|
|
|
|
|
|
out.className = "";
|
|
|
|
|
|
out.textContent = "测试中… (" + frames + " 帧 × " + payload + "B)";
|
|
|
|
|
|
fetch(url)
|
|
|
|
|
|
.then(function (r) {
|
|
|
|
|
|
if (!r.ok) { throw new Error("HTTP " + r.status); }
|
|
|
|
|
|
return r.text();
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(function (text) {
|
|
|
|
|
|
var failed = /失败:\s*[1-9]/.test(text);
|
|
|
|
|
|
out.className = failed ? "fail" : "";
|
|
|
|
|
|
out.textContent = text;
|
|
|
|
|
|
addHistory(frames, payload, text);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
|
out.className = "fail";
|
|
|
|
|
|
out.textContent = "请求失败: " + err.message + "\n请检查设备 IP 与网络连通性。";
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(function () { setBusy(false); });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clampInt(v, lo, hi, dflt) {
|
|
|
|
|
|
var n = parseInt(v, 10);
|
|
|
|
|
|
if (isNaN(n)) { return dflt; }
|
|
|
|
|
|
return Math.min(hi, Math.max(lo, n));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
btns[0].onclick = function () {
|
|
|
|
|
|
run(clampInt(document.getElementById("frames").value, 1, 2000, 200),
|
|
|
|
|
|
clampInt(document.getElementById("payload").value, 0, 236, 236));
|
|
|
|
|
|
};
|
|
|
|
|
|
btns[1].onclick = function () { run(500, 0); };
|
|
|
|
|
|
btns[2].onclick = function () { run(2000, 236); };
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|