// 测试操作页 — 间隔/超时机制 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 devTypeNameCache = {}; // type_num → dev_name 映射(从 tb_vechicle_base_test) 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) || 1; if (count < 1) return; // 读取车检器序列号 const detectorSerial = document.getElementById("detector-serial").value.trim(); // 读取参数 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; newB2Count = 0; updateRecordCount(); 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 = '
等待测试...
'; document.getElementById("latest-wave").innerHTML = '暂无波动数据...
'; 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, detector_serial: detectorSerial }), }); 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"; // 自动化结束 → 焦点回到车检器序列号输入框(全选),方便下一个车检器测试 setTimeout(focusDetectorSerial, 100); } // ─── 发送下一条 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) { if (data.latest.id !== lastLatestId) { lastLatestId = data.latest.id; if (data.latest.data_source === "B2") { newB2Count++; updateRecordCount(); } } 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); lastWaveId = data.latest_wave.id; } } 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 loadDeviceTypeNames() { try { const resp = await fetch(`/api/vehicle-base-test`); const tests = await resp.json(); devTypeNameCache = {}; tests.forEach(t => { if (t.type_num != null && t.dev_name) { devTypeNameCache[t.type_num] = t.dev_name; } }); } catch (e) { console.error("加载型号名称失败:", e); } } async function loadTestMode() { try { const resp = await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`); const param = await resp.json(); if (param && param.dnt_id) { updateTestModeUI(param.TestMode); renderConfigOverview(param); } 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 waveSection = document.getElementById("wave-section"); if (mode === 1) { waveSection.style.display = ''; } else { waveSection.style.display = 'none'; } } function renderConfigOverview(param) { const panel = document.getElementById("config-overview"); if (!panel) return; panel.style.display = ''; // 测试模式 const modeEl = document.getElementById("cfg-test-mode"); if (param.TestMode === 1) { modeEl.innerHTML = '波动测试'; } else { modeEl.innerHTML = '灵敏度测试'; } // 型号 document.getElementById("cfg-dev-type").textContent = devTypeNameCache[param.DevType] || `0x${(param.DevType || 0).toString(16)}`; // 距离 (DB cm → 显示 mm) document.getElementById("cfg-reset-dis").textContent = param.RestDis != null ? param.RestDis * 10 : '-'; document.getElementById("cfg-minus-dis").textContent = param.MinusDis != null ? param.MinusDis * 10 : '-'; // 触发和释放范围 (SensMin ~ SensMax) document.getElementById("cfg-sens-range").textContent = (param.SensMin != null && param.SensMax != null) ? `${param.SensMin} ~ ${param.SensMax}` : '-'; // 频率范围 (配置值 ×10 = 实际 Hz) if (param.FreMin != null && param.FreMax != null) { document.getElementById("cfg-fre-range").textContent = `${param.FreMin * 10} ~ ${param.FreMax * 10}`; } else { document.getElementById("cfg-fre-range").textContent = '-'; } // 线圈信息 const coil = [param.coil_num, param.coil_name].filter(Boolean).join(' '); document.getElementById("cfg-coil").textContent = coil || '-'; // 模拟车辆信息 const car = [param.simulate_num, param.car_name].filter(Boolean).join(' '); document.getElementById("cfg-car").textContent = car || '-'; // 波动参数 const waveParams = document.getElementById("cfg-wave-params"); if (param.TestMode === 1) { waveParams.style.display = ''; document.getElementById("cfg-near-tol").textContent = param.NearTol ?? '-'; document.getElementById("cfg-far-tol").textContent = param.FarTol ?? '-'; document.getElementById("cfg-step-tol").textContent = param.StepTol ?? '-'; document.getElementById("cfg-back-forth").textContent = param.BackForth ?? '-'; document.getElementById("cfg-near-stay").textContent = param.NearStay ?? '-'; document.getElementById("cfg-far-stay").textContent = param.FarStay ?? '-'; } else { waveParams.style.display = 'none'; } } function toggleConfig() { const body = document.getElementById("config-body"); const toggle = document.getElementById("config-toggle"); if (body.style.display === 'none') { body.style.display = ''; toggle.textContent = '收起 ▲'; } else { body.style.display = 'none'; toggle.textContent = '展开 ▼'; } } async function loadInitialData() { await loadDeviceTypeNames(); await loadTestMode(); refreshDeviceStatus(); newB2Count = 0; updateRecordCount(); try { const resp = await fetch(`/api/automation/${DNT_ID}/progress`); const data = await resp.json(); if (data.latest) { renderLatest(data.latest); lastLatestId = data.latest.id; } if (data.latest_wave) { renderLatestWave(data.latest_wave); lastWaveId = data.latest_wave.id; } } catch (e) { // 初始加载静默失败 } // 页面加载后焦点落到车检器序列号输入框(全选) setTimeout(focusDetectorSerial, 200); } loadInitialData(); // ─── 回车键触发"开始"按钮 ───────────────────── /** 将焦点移到车检器序列号输入框并全选已有文本 */ function focusDetectorSerial() { const input = document.getElementById("detector-serial"); if (input) { input.focus(); input.select(); } } document.addEventListener("DOMContentLoaded", function() { const detectorInput = document.getElementById("detector-serial"); if (detectorInput) { detectorInput.addEventListener("keydown", function(e) { if (e.key === "Enter") { e.preventDefault(); const btn = document.getElementById("btn-auto"); if (btn && !autoRunning) { btn.click(); } } }); } }); // ─── 设备状态异步刷新 ────────────────────────── 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) { // 静默失败 } } // ─── 被动轮询:实时显示上报数据(工装本地按键 / 网页手动指令触发)─── let lastLatestId = 0; // 最新测试数据 ID,用于判断是否有新数据 let lastWaveId = 0; // 最新波动数据 ID let newB2Count = 0; // 本轮新收到的 B2(灵敏度测试) 记录条数 function updateRecordCount() { const el = document.getElementById("new-record-count"); if (el) el.textContent = newB2Count > 0 ? `(${newB2Count} 条新记录)` : ""; } async function refreshLatestData() { // 自动化运行中由 pollProgress 负责渲染,避免冲突 if (autoRunning) return; try { const resp = await fetch(`/api/automation/${DNT_ID}/progress`); const data = await resp.json(); if (data.latest && data.latest.id !== lastLatestId) { lastLatestId = data.latest.id; renderLatest(data.latest); // 仅 B2(灵敏度测试) 记录计数 if (data.latest.data_source === "B2") { newB2Count++; updateRecordCount(); } } if (data.latest_wave && data.latest_wave.id !== lastWaveId) { lastWaveId = data.latest_wave.id; renderLatestWave(data.latest_wave); } } catch (e) { /* 静默失败 */ } } // 最新测试数据每 3 秒轮询 setInterval(refreshLatestData, 3000); // 每 5 秒刷新设备状态 + 测试模式 + 型号名称缓存(工装页修改后能及时同步) async function refreshAll() { await loadDeviceTypeNames(); 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 fmtRelay(s) { if (!s) return '-'; return s .replace(/继电器有输出/g, '✅有输出') .replace(/继电器无输出/g, '❌无输出'); } // ─── 显示最新结果 ────────────────────────────── function renderLatest(data) { const div = document.getElementById("latest-result"); // 优先使用 str_type,为空时从缓存查找 let typeName = data.str_type; if (!typeName && data.sub_type != null) { typeName = devTypeNameCache[data.sub_type] || `Unknown(${data.sub_type})`; } div.innerHTML = `车检器序列号:${data.detector_serial || '-'}
设备型号:${typeName || '-'}
测试模式:${data.test_mode === 1 ? '波动测试' : '灵敏度测试'}
峰峰值:${data.ppvalue?.toFixed(2) || '-'} V
开始工作频率:${data.idle_freq || '-'} Hz
进入工作频率:${data.enter_freq || '-'} Hz
离开工作频率:${data.exit_freq || '-'} Hz
进入距离:${data.enter_dist || '-'} mm
离开距离:${data.exit_dist || '-'} mm
进入速度:${toSpeed(data.enter_speed)} m/s
离开速度:${toSpeed(data.exit_speed)} m/s
是否完成:${data.iffinish === '1' ? '是' : '否'}
故障信息:${data.fault_info || '无'}
继电器:${fmtRelay(data.relay_out) || decodeRelay(data.relay_code)}
时间:${fmtTime(data.create_time)}
`; } // ─── 显示平均值 ──────────────────────────────── 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 = '暂无波动数据...
'; return; } div.innerHTML = `剩余次数:${data.remain_count || 0}
工作频率:${data.work_freq || '-'} Hz
当前距离:${data.curr_dist || '-'} mm
当前速度:${toSpeed(data.speed)} m/s
最近距离:${data.near_dist || '-'} mm
最远距离:${data.far_dist || '-'} mm
进入高度 (B4):${data.b4_enter_dist || '-'} mm
离开高度 (B4):${data.b4_leave_dist || '-'} mm
继电器:${fmtRelay(data.relay_out) || decodeRelay(data.relay_code)}
时间:${fmtTime(data.create_time)}
`; } // ─── 显示本轮测试明细 ────────────────────────── 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) => `