Files

667 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 测试操作页 — 间隔/超时机制
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 = '<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, 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 = '<strong style="color:#e67e22;">波动测试</strong>';
} else {
modeEl.innerHTML = '<strong style="color:#2980b9;">灵敏度测试</strong>';
}
// 型号
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, '<span style="color:#22c55e;font-weight:600">✅有输出</span>')
.replace(/继电器无输出/g, '<span style="color:#ef4444;font-weight:600">❌无输出</span>');
}
// ─── 显示最新结果 ──────────────────────────────
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 = `
<p>车检器序列号:<strong>${data.detector_serial || '-'}</strong></p>
<p>设备型号:<strong>${typeName || '-'}</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>继电器:${fmtRelay(data.relay_out) || 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>继电器:${fmtRelay(data.relay_out) || 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("");
}