496 lines
18 KiB
JavaScript
496 lines
18 KiB
JavaScript
// 测试操作页 — 间隔/超时机制
|
||
|
||
let autoRunning = false;
|
||
let autoTotal = 0;
|
||
let autoDone = 0;
|
||
let autoFailed = 0;
|
||
let autoRemaining = 0;
|
||
let autoStartTime = "";
|
||
let localSinceStr = "";
|
||
let currentTestMode = null; // 0=灵敏度, 1=波动, null=未加载
|
||
let currentDeviceState = null; // 当前设备状态 (0=离线 1=在线 2=通信不良 null=未加载)
|
||
|
||
let pollInterval = null;
|
||
let nextCmdTimer = null; // 间隔等待定时器
|
||
let timeoutTimer = null; // 超时定时器
|
||
let timeoutAt = 0; // 超时时刻 (Date.now() + timeoutMs)
|
||
let lastDoneCount = 0; // 上一轮 done 数,检测新响应
|
||
let cmdSentAt = 0; // 最近一次发送 0xB0 时间
|
||
let intervalMs = 10000; // 默认 10s
|
||
let timeoutMs = 5000; // 默认 5s
|
||
|
||
// ─── 设备在线检查 ──────────────────────────────
|
||
|
||
function checkDeviceOnline() {
|
||
if (currentDeviceState !== 1) {
|
||
const stateName = currentDeviceState === 2 ? '通信不良' :
|
||
currentDeviceState === 0 ? '离线' : '未知';
|
||
alert(`设备当前状态为「${stateName}」,无法发送指令`);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ─── 手动指令 ─────────────────────────────────
|
||
|
||
async function sendCmd(cmd) {
|
||
if (!checkDeviceOnline()) return;
|
||
try {
|
||
const resp = await fetch("/api/command", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ dnt_id: DNT_ID, cmd }),
|
||
});
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
console.log(`指令 ${cmd} 已下发: ${data.send_pkg}`);
|
||
}
|
||
} catch (e) {
|
||
alert("发送失败: " + e.message);
|
||
}
|
||
}
|
||
|
||
// ─── 自动化 ────────────────────────────────────
|
||
|
||
async function toggleAuto() {
|
||
if (!autoRunning) {
|
||
await startAuto();
|
||
} else {
|
||
stopAuto();
|
||
}
|
||
}
|
||
|
||
async function startAuto() {
|
||
if (!checkDeviceOnline()) return;
|
||
const count = parseInt(document.getElementById("test-count").value) || 10;
|
||
if (count < 1) return;
|
||
|
||
// 读取参数
|
||
intervalMs = (parseFloat(document.getElementById("interval-sec").value) || 3) * 1000;
|
||
timeoutMs = (parseFloat(document.getElementById("timeout-sec").value) || 10) * 1000;
|
||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||
|
||
// 重置
|
||
autoRunning = true;
|
||
autoTotal = count;
|
||
autoDone = 0;
|
||
autoFailed = 0;
|
||
autoRemaining = count;
|
||
lastDoneCount = 0;
|
||
autoStartTime = new Date().toISOString();
|
||
|
||
const now = new Date();
|
||
localSinceStr = now.getFullYear() + "-" +
|
||
String(now.getMonth() + 1).padStart(2, "0") + "-" +
|
||
String(now.getDate()).padStart(2, "0") + " " +
|
||
String(now.getHours()).padStart(2, "0") + ":" +
|
||
String(now.getMinutes()).padStart(2, "0") + ":" +
|
||
String(now.getSeconds()).padStart(2, "0");
|
||
|
||
// 显示开始时间
|
||
document.getElementById("auto-time").style.display = "block";
|
||
document.getElementById("time-start").textContent = new Date().toLocaleString();
|
||
document.getElementById("time-end").textContent = "-";
|
||
|
||
// 清空显示
|
||
resetAverages();
|
||
document.getElementById("latest-result").innerHTML = '<p class="placeholder">等待测试...</p>';
|
||
document.getElementById("latest-wave").innerHTML = '<p class="placeholder">暂无波动数据...</p>';
|
||
document.getElementById("progress-bar").style.width = "0%";
|
||
document.getElementById("progress-text").textContent = "0/" + count + " (0 失败)";
|
||
document.getElementById("stat-done").textContent = "0";
|
||
document.getElementById("stat-failed").textContent = "0";
|
||
document.getElementById("stat-remaining").textContent = count;
|
||
document.getElementById("auto-status").textContent = "";
|
||
|
||
document.querySelector("#records-table tbody").innerHTML = "";
|
||
document.getElementById("records-empty").style.display = "block";
|
||
|
||
const btn = document.getElementById("btn-auto");
|
||
btn.textContent = "结束";
|
||
btn.className = "btn-stop";
|
||
|
||
// 清除旧记录 + 插入第一条
|
||
try {
|
||
const resp = await fetch("/api/automation/start", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ dnt_id: DNT_ID, count }),
|
||
});
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
cmdSentAt = Date.now();
|
||
timeoutAt = cmdSentAt + timeoutMs;
|
||
armTimeout();
|
||
setStatus(`已发送,超时 ${timeoutMs/1000}s...`);
|
||
}
|
||
} catch (e) {
|
||
console.error("启动失败:", e);
|
||
stopAuto();
|
||
}
|
||
|
||
// 启动轮询
|
||
pollInterval = setInterval(pollProgress, 500);
|
||
}
|
||
|
||
function stopAuto() {
|
||
autoRunning = false;
|
||
clearInterval(pollInterval);
|
||
pollInterval = null;
|
||
clearTimeout(nextCmdTimer);
|
||
nextCmdTimer = null;
|
||
clearTimeout(timeoutTimer);
|
||
timeoutTimer = null;
|
||
document.getElementById("auto-status").textContent = "";
|
||
document.getElementById("time-end").textContent = new Date().toLocaleString();
|
||
|
||
const btn = document.getElementById("btn-auto");
|
||
btn.textContent = "开始";
|
||
btn.className = "btn-start";
|
||
}
|
||
|
||
// ─── 发送下一条 0xB0 ────────────────────────────
|
||
|
||
async function sendNextCmd() {
|
||
if (!autoRunning) return;
|
||
if (autoRemaining <= 0) {
|
||
// 检查是否还有等待完成的
|
||
if (autoDone + autoFailed >= autoTotal) {
|
||
stopAuto();
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const r = await fetch("/api/command", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ dnt_id: DNT_ID, cmd: "B0" }),
|
||
});
|
||
const rd = await r.json();
|
||
if (rd.ok) {
|
||
cmdSentAt = Date.now();
|
||
timeoutAt = cmdSentAt + timeoutMs;
|
||
armTimeout();
|
||
setStatus(`已发送,超时 ${timeoutMs/1000}s...`);
|
||
}
|
||
} catch (e) {
|
||
console.error("发送下一条失败:", e);
|
||
}
|
||
}
|
||
|
||
// ─── 超时定时器 ─────────────────────────────────
|
||
|
||
function armTimeout() {
|
||
clearTimeout(timeoutTimer);
|
||
timeoutTimer = setTimeout(onTimeout, timeoutMs + 500); // +500ms 容差
|
||
}
|
||
|
||
function onTimeout() {
|
||
if (!autoRunning) return;
|
||
// 超时:即使没收到回复也计数失败,立即发下一条
|
||
autoFailed++;
|
||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||
if (autoRemaining < 0) autoRemaining = 0;
|
||
updateUI();
|
||
setStatus(`超时!立即发下一条...`);
|
||
console.log(`超时 (${timeoutMs}ms),失败数: ${autoFailed}`);
|
||
|
||
clearTimeout(nextCmdTimer);
|
||
nextCmdTimer = null;
|
||
|
||
if (autoRemaining > 0) {
|
||
sendNextCmd();
|
||
} else {
|
||
stopAuto();
|
||
}
|
||
}
|
||
|
||
// ─── 轮询 ──────────────────────────────────────
|
||
|
||
async function pollProgress() {
|
||
if (!autoRunning) return;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/automation/${DNT_ID}/progress?since=${encodeURIComponent(localSinceStr)}`);
|
||
const data = await resp.json();
|
||
const stats = data.stats;
|
||
|
||
// ── 先渲染数据(放在所有 return 之前,避免完成时跳过渲染)──
|
||
try { if (data.latest) renderLatest(data.latest); } catch (e) { console.error("renderLatest:", e); }
|
||
try { if (data.averages) renderAverages(data.averages); } catch (e) { console.error("renderAverages:", e); }
|
||
try { if (data.latest_wave) renderLatestWave(data.latest_wave); } catch (e) { console.error("renderLatestWave:", e); }
|
||
try { if (data.records && data.records.length) renderRecords(data.records); } catch (e) { console.error("renderRecords:", e); }
|
||
|
||
// 更新计数
|
||
const newDone = stats.done || 0;
|
||
const newFailed = stats.failed || 0;
|
||
|
||
if (newDone > lastDoneCount) {
|
||
// 收到新回复 → 清除超时,开始间隔等待
|
||
const delta = newDone - lastDoneCount;
|
||
lastDoneCount = newDone;
|
||
autoDone = newDone;
|
||
autoFailed = newFailed;
|
||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||
if (autoRemaining < 0) autoRemaining = 0;
|
||
|
||
clearTimeout(timeoutTimer);
|
||
timeoutTimer = null;
|
||
clearTimeout(nextCmdTimer);
|
||
nextCmdTimer = null;
|
||
|
||
updateUI();
|
||
|
||
if (autoRemaining > 0) {
|
||
const wait = (intervalMs / 1000).toFixed(0);
|
||
setStatus(`收到 ${delta} 条回复,等待 ${wait}s...`);
|
||
nextCmdTimer = setTimeout(() => {
|
||
setStatus("发送中...");
|
||
sendNextCmd();
|
||
}, intervalMs);
|
||
} else {
|
||
setStatus("全部完成");
|
||
stopAuto();
|
||
return;
|
||
}
|
||
} else {
|
||
// 没有新回复,更新超时计数
|
||
autoFailed = newFailed;
|
||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||
if (autoRemaining < 0) autoRemaining = 0;
|
||
// 检查是否全部完成
|
||
if (autoRemaining <= 0 && autoDone + autoFailed >= autoTotal) {
|
||
stopAuto();
|
||
return;
|
||
}
|
||
}
|
||
|
||
} catch (e) {
|
||
console.error("轮询失败:", e);
|
||
}
|
||
}
|
||
|
||
// ─── 页面加载时获取初始数据 ──────────────────────
|
||
|
||
async function loadTestMode() {
|
||
try {
|
||
const resp = await fetch(`/api/fixture/param/${DNT_ID}`);
|
||
const param = await resp.json();
|
||
if (param && param.dnt_id) {
|
||
updateTestModeUI(param.TestMode);
|
||
} else {
|
||
// 没有工装参数时,尝试从最新测试数据获取
|
||
const r2 = await fetch(`/api/automation/${DNT_ID}/progress`);
|
||
const d2 = await r2.json();
|
||
if (d2.latest && d2.latest.test_mode !== undefined) {
|
||
updateTestModeUI(d2.latest.test_mode);
|
||
}
|
||
}
|
||
} catch (e) { /* 静默 */ }
|
||
}
|
||
|
||
function updateTestModeUI(mode) {
|
||
currentTestMode = mode;
|
||
const indicator = document.getElementById("test-mode-indicator");
|
||
const waveSection = document.getElementById("wave-section");
|
||
if (mode === 1) {
|
||
indicator.innerHTML = '当前测试模式:<strong style="color:#e67e22;">波动测试</strong>';
|
||
waveSection.style.display = '';
|
||
} else {
|
||
indicator.innerHTML = '当前测试模式:<strong style="color:#2980b9;">灵敏度测试</strong>';
|
||
waveSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
async function loadInitialData() {
|
||
await loadTestMode();
|
||
refreshDeviceStatus();
|
||
try {
|
||
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
|
||
const data = await resp.json();
|
||
if (data.latest) renderLatest(data.latest);
|
||
if (data.latest_wave) renderLatestWave(data.latest_wave);
|
||
} catch (e) {
|
||
// 初始加载静默失败
|
||
}
|
||
}
|
||
loadInitialData();
|
||
|
||
// ─── 设备状态异步刷新 ──────────────────────────
|
||
|
||
async function refreshDeviceStatus() {
|
||
try {
|
||
const resp = await fetch(`/api/devices/${DNT_ID}/status`);
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
currentDeviceState = data.state;
|
||
const el = document.getElementById("device-status-text");
|
||
if (el) {
|
||
el.textContent = data.state_name;
|
||
if (data.state === 1) {
|
||
el.className = "status-online";
|
||
} else if (data.state === 2) {
|
||
el.className = "status-poor";
|
||
} else {
|
||
el.className = "status-offline";
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 静默失败
|
||
}
|
||
}
|
||
|
||
// 每 5 秒刷新设备状态 + 测试模式(工装页修改后能及时同步)
|
||
async function refreshAll() {
|
||
await loadTestMode();
|
||
refreshDeviceStatus();
|
||
}
|
||
setInterval(refreshAll, 5000);
|
||
|
||
// ─── UI ────────────────────────────────────────
|
||
|
||
function setStatus(msg) {
|
||
document.getElementById("auto-status").textContent = msg;
|
||
}
|
||
|
||
function updateUI() {
|
||
document.getElementById("stat-done").textContent = autoDone;
|
||
document.getElementById("stat-failed").textContent = autoFailed;
|
||
document.getElementById("stat-remaining").textContent = autoRemaining;
|
||
|
||
const total = autoTotal || 1;
|
||
const pct = Math.round((autoDone / total) * 100);
|
||
document.getElementById("progress-bar").style.width = pct + "%";
|
||
document.getElementById("progress-text").textContent =
|
||
`${autoDone}/${autoTotal} (${autoFailed} 失败)`;
|
||
}
|
||
|
||
function toSpeed(v) {
|
||
if (v === null || v === undefined || v === '') return '-';
|
||
return (parseFloat(v) / 10).toFixed(1);
|
||
}
|
||
|
||
function fmtTime(v) {
|
||
if (!v) return '-';
|
||
// Flask jsonify 给 MySQL DATETIME 加 "GMT" 后缀,但实际值是服务器本地时间(UTC+8)
|
||
// 去掉 "GMT" 让 JS 按本地时间解析,避免时区偏移 8 小时
|
||
const cleaned = String(v).replace(/ GMT$/, '');
|
||
const d = new Date(cleaned);
|
||
if (isNaN(d.getTime())) return String(v).substring(0, 19);
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||
const d2 = String(d.getDate()).padStart(2, '0');
|
||
const h = String(d.getHours()).padStart(2, '0');
|
||
const min = String(d.getMinutes()).padStart(2, '0');
|
||
const s = String(d.getSeconds()).padStart(2, '0');
|
||
return `${y}-${m}-${d2} ${h}:${min}:${s}`;
|
||
}
|
||
|
||
const RELAY_MAP = {
|
||
0: '无输出',
|
||
1: '存在信号',
|
||
2: '脉冲信号',
|
||
3: '存在信号; 脉冲信号',
|
||
};
|
||
function decodeRelay(v) {
|
||
if (v === null || v === undefined || v === '') return '-';
|
||
return RELAY_MAP[parseInt(v)] || `0x${parseInt(v).toString(16).toUpperCase().padStart(2, '0')}`;
|
||
}
|
||
|
||
// ─── 显示最新结果 ──────────────────────────────
|
||
|
||
function renderLatest(data) {
|
||
if (data.test_mode !== undefined && data.test_mode !== currentTestMode) {
|
||
updateTestModeUI(data.test_mode);
|
||
}
|
||
const div = document.getElementById("latest-result");
|
||
div.innerHTML = `
|
||
<p>设备型号:<strong>${data.str_type || '-'}</strong></p>
|
||
<p>测试模式:<strong>${data.test_mode === 1 ? '波动测试' : '灵敏度测试'}</strong></p>
|
||
<p>峰峰值:${data.ppvalue?.toFixed(2) || '-'} V</p>
|
||
<p>开始工作频率:${data.idle_freq || '-'} Hz</p>
|
||
<p>进入工作频率:${data.enter_freq || '-'} Hz</p>
|
||
<p>离开工作频率:${data.exit_freq || '-'} Hz</p>
|
||
<p>进入距离:${data.enter_dist || '-'} mm</p>
|
||
<p>离开距离:${data.exit_dist || '-'} mm</p>
|
||
<p>进入速度:${toSpeed(data.enter_speed)} m/s</p>
|
||
<p>离开速度:${toSpeed(data.exit_speed)} m/s</p>
|
||
<p>是否完成:${data.iffinish === '1' ? '是' : '否'}</p>
|
||
<p>故障信息:${data.fault_info || '无'}</p>
|
||
<p>继电器:${decodeRelay(data.relay_code)}</p>
|
||
<p>时间:${fmtTime(data.create_time)}</p>
|
||
`;
|
||
}
|
||
|
||
// ─── 显示平均值 ────────────────────────────────
|
||
|
||
function renderAverages(data) {
|
||
document.getElementById("avg-ppvalue").textContent = data.avg_ppvalue || "-";
|
||
document.getElementById("avg-idle-freq").textContent = data.avg_idle_freq || "-";
|
||
document.getElementById("avg-enter-freq").textContent = data.avg_enter_freq || "-";
|
||
document.getElementById("avg-enter-dist").textContent = data.avg_enter_dist || "-";
|
||
document.getElementById("avg-exit-dist").textContent = data.avg_exit_dist || "-";
|
||
document.getElementById("avg-enter-speed").textContent = data.avg_enter_speed || "-";
|
||
document.getElementById("avg-exit-speed").textContent = data.avg_exit_speed || "-";
|
||
}
|
||
|
||
function resetAverages() {
|
||
["ppvalue", "idle-freq", "enter-freq", "enter-dist", "exit-dist",
|
||
"enter-speed", "exit-speed"].forEach(id => {
|
||
document.getElementById("avg-" + id).textContent = "-";
|
||
});
|
||
}
|
||
|
||
// ─── 显示波动测试数据 ──────────────────────────
|
||
|
||
function renderLatestWave(data) {
|
||
const div = document.getElementById("latest-wave");
|
||
if (!data || !data.work_freq) {
|
||
div.innerHTML = '<p class="placeholder">暂无波动数据...</p>';
|
||
return;
|
||
}
|
||
div.innerHTML = `
|
||
<p>剩余次数:<strong>${data.remain_count || 0}</strong></p>
|
||
<p>工作频率:${data.work_freq || '-'} Hz</p>
|
||
<p>当前距离:${data.curr_dist || '-'} mm</p>
|
||
<p>当前速度:${toSpeed(data.speed)} m/s</p>
|
||
<p>最近距离:${data.near_dist || '-'} mm</p>
|
||
<p>最远距离:${data.far_dist || '-'} mm</p>
|
||
<p>进入高度 (B4):${data.b4_enter_dist || '-'} mm</p>
|
||
<p>离开高度 (B4):${data.b4_leave_dist || '-'} mm</p>
|
||
<p>继电器:${decodeRelay(data.relay_code)}</p>
|
||
<p>时间:${fmtTime(data.create_time)}</p>
|
||
`;
|
||
}
|
||
|
||
// ─── 显示本轮测试明细 ──────────────────────────
|
||
|
||
function renderRecords(records) {
|
||
if (!records || !records.length) {
|
||
document.getElementById("records-empty").style.display = "block";
|
||
document.getElementById("records-table").style.display = "none";
|
||
return;
|
||
}
|
||
document.getElementById("records-empty").style.display = "none";
|
||
document.getElementById("records-table").style.display = "";
|
||
|
||
const tbody = document.querySelector("#records-table tbody");
|
||
tbody.innerHTML = records.map((r, i) => `
|
||
<tr>
|
||
<td>${i + 1}</td>
|
||
<td style="color:${r.sn_state === 2 ? '#27ae60' : r.sn_state === 3 ? '#e74c3c' : '#888'}">
|
||
${r.sn_state === 2 ? 'OK' : r.sn_state === 3 ? '超时' : '?'}
|
||
</td>
|
||
<td>${r.test_mode === 1 ? '波动' : '灵敏度'}</td>
|
||
<td>${r.ppvalue?.toFixed(2) || '-'}</td>
|
||
<td>${r.idle_freq || '-'}</td>
|
||
<td>${r.enter_dist || '-'}</td>
|
||
<td>${r.exit_dist || '-'}</td>
|
||
<td>${toSpeed(r.enter_speed)}</td>
|
||
<td>${fmtTime(r.create_time)}</td>
|
||
</tr>
|
||
`).join("");
|
||
}
|