// 工装配置页 // ─── 频率/峰峰值转换常量 ───────────────────── // 协议: 工作频率 f(Hz) = 10 * X, X 为 DB/设备中存储和传输的原始值 // 协议: 峰峰值 V = ((X * 3.3) / 4095) * 4, X 为 DB/设备中存储和传输的原始值(正整数) const FREQ_SCALE = 10; const PEAK_SCALE = 4095 / (4 * 3.3); // ≈ 310.227 function rawFreqToHz(x) { return x * FREQ_SCALE; } function hzToRawFreq(hz) { return Math.round(hz / FREQ_SCALE); } function rawPeakToV(x) { return parseFloat(((x * 3.3) / 4095 * 4).toFixed(2)); } function vToRawPeak(v) { return Math.round(v * PEAK_SCALE); } let baseTests = []; // 所有车检器基准参数 let selectedBaseTest = null; let pollTimer4C = null; // 0x4C 参数查询轮询 let pollTimers = {}; // record_id → timer (指令响应轮询) // ─── 初始化 ───────────────────────────────── async function init() { await loadBaseTests(); await loadCoilList(); await loadCarList(); await loadFixtureParam(); } // ─── Toast 提示 ───────────────────────────── function toast(msg, isError = false) { const el = document.getElementById("toast"); el.textContent = msg; el.className = "msg-toast " + (isError ? "error" : "") + " show"; clearTimeout(el._timeout); el._timeout = setTimeout(() => { el.className = "msg-toast"; }, 3000); } // ─── 通信日志 ─────────────────────────────── function commLog(type, hex, detail) { const el = document.getElementById("comm-log"); if (el.querySelector('div') && el.querySelector('div').textContent === '等待操作…') { el.innerHTML = ''; } const time = new Date().toLocaleTimeString(); const colors = { send: '#4ec9b0', recv: '#ce9178', ok: '#6a9955', fail: '#f44747', info: '#888' }; const labels = { send: '→ 发送', recv: '← 收到', ok: '✓ 成功', fail: '✗ 失败', info: '· 信息' }; const color = colors[type] || '#888'; const label = labels[type] || ''; const html = `
${time} ${label} ${detail || ''} ${hex ? `
${hex}` : ''}
`; el.insertAdjacentHTML('afterbegin', html); } // ─── 解析 Flag 响应 ────────────────────────── const FLAG_CMDS = { '4B': '配置参数', '4D': '出厂初始化', '4E': '设备复位' }; function parseFlagResponse(rcvPkg, cmd) { // 格式: 7F8102{CMD}{FLAG}{XOR}{SUM} → FLAG 在位置 8-9 if (!rcvPkg || rcvPkg.length < 10) return null; const flag = parseInt(rcvPkg.substring(8, 10), 16); return flag; } // ─── 加载车检器测试基准 ────────────────────── async function loadBaseTests() { const search = document.getElementById("ref-search").value; try { const resp = await fetch(`/api/vehicle-base-test?search=${encodeURIComponent(search)}`); baseTests = await resp.json(); renderBaseTestTable(); populateDevTypeSelect(); } catch (e) { console.error("加载基准参数失败:", e); } } function renderBaseTestTable() { const tbody = document.getElementById("ref-table-body"); if (!baseTests.length) { tbody.innerHTML = '暂无数据'; return; } tbody.innerHTML = baseTests.map(t => ` ${t.type_num} ${esc(t.dev_name)} ${t.SensMin}~${t.SensMax} ${rawFreqToHz(t.FreMin)}~${rawFreqToHz(t.FreMax)} ${rawPeakToV(t.PeakMin)}~${rawPeakToV(t.PeakMax)} `).join(""); } function populateDevTypeSelect() { const sel = document.getElementById("param-dev-type"); sel.innerHTML = '' + baseTests.map(t => ``).join(""); if (selectedBaseTest) { sel.value = selectedBaseTest.type_num; } } function selectBaseTest(id) { selectedBaseTest = baseTests.find(t => t.id === id); if (!selectedBaseTest) return; fillFromBaseTest(selectedBaseTest); renderBaseTestTable(); } function fillFromBaseTest(t) { document.getElementById("param-dev-type").value = t.type_num; document.getElementById("param-sens-min").value = t.SensMin; document.getElementById("param-sens-max").value = t.SensMax; document.getElementById("param-fre-min").value = rawFreqToHz(t.FreMin); document.getElementById("param-fre-max").value = rawFreqToHz(t.FreMax); document.getElementById("param-peak-min").value = rawPeakToV(t.PeakMin); document.getElementById("param-peak-max").value = rawPeakToV(t.PeakMax); } function onDevTypeChange() { const typeNum = parseInt(document.getElementById("param-dev-type").value); const matched = baseTests.find(t => t.type_num === typeNum); if (matched) selectBaseTest(matched.id); else { selectedBaseTest = null; renderBaseTestTable(); } } // ─── 线圈列表 ──────────────────────────────── let coilList = []; async function loadCoilList() { try { const resp = await fetch("/api/coil-info"); coilList = await resp.json(); populateCoilSelect(); } catch (e) { console.error("加载线圈列表失败:", e); } } function populateCoilSelect() { const sel = document.getElementById("coil-select"); sel.innerHTML = '' + coilList.map(c => ``).join(""); } function onCoilChange() { const id = parseInt(document.getElementById("coil-select").value); const coil = coilList.find(c => c.id === id); const detail = document.getElementById("coil-detail"); if (coil) { const sizeText = coil.shape === '圆形' ? `半径${coil.radius}cm` : `${coil.length}×${coil.width}cm`; detail.innerHTML = `${coil.shape || '-'} / ${sizeText} / ${coil.turns || 0}圈 / ${coil.resistance || 0}Ω / ${coil.material || ''}`; } else { detail.innerHTML = ''; } } // ─── 模拟车辆列表 ──────────────────────────── let carList = []; async function loadCarList() { try { const resp = await fetch("/api/simulate-car"); carList = await resp.json(); populateCarSelect(); } catch (e) { console.error("加载模拟车辆列表失败:", e); } } function populateCarSelect() { const sel = document.getElementById("car-select"); sel.innerHTML = '' + carList.map(c => ``).join(""); } function onCarChange() { const id = parseInt(document.getElementById("car-select").value); const car = carList.find(c => c.id === id); const detail = document.getElementById("car-detail"); if (car) { const sizeText = car.shape === '圆形' ? `半径${car.radius}cm` : `${car.length}×${car.width}cm`; detail.innerHTML = `${car.shape || '-'} / ${sizeText} / ${car.material || ''}`; } else { detail.innerHTML = ''; } } // ─── 从 DB 加载/刷新/保存 ──────────────────── async function loadFixtureParam() { try { const resp = await fetch(`/api/fixture/param/${DNT_ID}`); const param = await resp.json(); if (param && param.dnt_id) { fillFormFromParam(param); commLog('info', null, '已从数据库加载工装参数'); } } catch (e) { console.error("加载工装参数失败:", e); } } function fillFormFromParam(param) { document.getElementById("param-addr").value = param.Addr || 1; document.getElementById("param-test-mode").value = param.TestMode || 0; document.getElementById("param-reset-dis").value = (param.RestDis || 0) * 10; document.getElementById("param-minus-dis").value = (param.MinusDis || 0) * 10; document.getElementById("param-dev-type").value = param.DevType || 0; document.getElementById("param-sens-min").value = param.SensMin || 0; document.getElementById("param-sens-max").value = param.SensMax || 0; document.getElementById("param-fre-min").value = rawFreqToHz(param.FreMin || 0); document.getElementById("param-fre-max").value = rawFreqToHz(param.FreMax || 0); document.getElementById("param-peak-min").value = rawPeakToV(param.PeakMin || 0); document.getElementById("param-peak-max").value = rawPeakToV(param.PeakMax || 0); document.getElementById("param-far-tol").value = (param.FarTol || 0) * 10; document.getElementById("param-near-tol").value = (param.NearTol || 0) * 10; document.getElementById("param-step-tol").value = (param.StepTol || 0) * 10; document.getElementById("param-back-forth").value = param.BackForth || 0; document.getElementById("param-near-stay").value = param.NearStay || 0; document.getElementById("param-far-stay").value = param.FarStay || 0; const matched = baseTests.find(t => t.type_num === param.DevType); if (matched) { selectedBaseTest = matched; renderBaseTestTable(); } // 设置线圈和模拟车辆选中 if (param.coil_id) { document.getElementById("coil-select").value = param.coil_id; onCoilChange(); } if (param.simulate_car_id) { document.getElementById("car-select").value = param.simulate_car_id; onCarChange(); } // 更新当前关联标签 updateCurrentLabels(param); } async function refreshParams() { const param = await (await fetch(`/api/fixture/param/${DNT_ID}`)).json(); if (param && param.dnt_id) { fillFormFromParam(param); commLog('info', null, '已刷新:从数据库加载工装参数'); toast("已刷新"); } else { toast("数据库中没有该工装的参数,请先查询(0x4C)或保存", true); commLog('info', null, '刷新失败:数据库中没有参数'); } } async function saveToDb() { const data = getFormParams(); const coilId = parseInt(document.getElementById("coil-select").value) || null; const carId = parseInt(document.getElementById("car-select").value) || null; const body = { Addr: data.addr, DevType: data.dev_type, TestMode: data.test_mode, RestDis: data.reset_dis, MinusDis: data.minus_dis, SensMin: data.sens_min, SensMax: data.sens_max, FreMin: data.fre_min, FreMax: data.fre_max, PeakMin: data.peak_min, PeakMax: data.peak_max, FarTol: data.far_tol, NearTol: data.near_tol, StepTol: data.step_tol, BackForth: data.back_forth, NearStay: data.near_stay, FarStay: data.far_stay, }; if (coilId) body.coil_id = coilId; if (carId) body.simulate_car_id = carId; try { const resp = await fetch(`/api/fixture/param/${DNT_ID}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const result = await resp.json(); if (result.ok) { commLog('info', null, '参数已保存到数据库'); toast("已保存到数据库"); updateCurrentLabels(); } else { toast("保存失败: " + (result.error || ""), true); } } catch (e) { toast("保存失败: " + e.message, true); } } /** 更新页面上的当前线圈/车辆标签 */ function updateCurrentLabels(param) { const coilLabel = document.getElementById("current-coil-label"); const carLabel = document.getElementById("current-car-label"); if (param) { coilLabel.textContent = param.coil_name || param.coil_num || '未设置'; carLabel.textContent = param.car_name || param.simulate_num || '未设置'; } else { // 从 DOM 读取当前选择 const coilId = parseInt(document.getElementById("coil-select").value); const carId = parseInt(document.getElementById("car-select").value); const coil = coilId ? coilList.find(c => c.id === coilId) : null; const car = carId ? carList.find(c => c.id === carId) : null; coilLabel.textContent = coil ? (coil.coil_num || coil.name) : '未设置'; carLabel.textContent = car ? (car.simulate_num || car.name) : '未设置'; } } function getFormParams() { return { addr: parseInt(document.getElementById("param-addr").value) || 1, dev_type: parseInt(document.getElementById("param-dev-type").value) || 0, test_mode: parseInt(document.getElementById("param-test-mode").value) || 0, reset_dis: Math.round((parseInt(document.getElementById("param-reset-dis").value) || 0) / 10), minus_dis: Math.round((parseInt(document.getElementById("param-minus-dis").value) || 0) / 10), sens_min: parseInt(document.getElementById("param-sens-min").value) || 0, sens_max: parseInt(document.getElementById("param-sens-max").value) || 0, fre_min: hzToRawFreq(parseFloat(document.getElementById("param-fre-min").value) || 0), fre_max: hzToRawFreq(parseFloat(document.getElementById("param-fre-max").value) || 0), peak_min: vToRawPeak(parseFloat(document.getElementById("param-peak-min").value) || 0), peak_max: vToRawPeak(parseFloat(document.getElementById("param-peak-max").value) || 0), far_tol: Math.round((parseInt(document.getElementById("param-far-tol").value) || 0) / 10), near_tol: Math.round((parseInt(document.getElementById("param-near-tol").value) || 0) / 10), step_tol: Math.round((parseInt(document.getElementById("param-step-tol").value) || 0) / 10), back_forth: parseInt(document.getElementById("param-back-forth").value) || 0, near_stay: parseInt(document.getElementById("param-near-stay").value) || 0, far_stay: parseInt(document.getElementById("param-far-stay").value) || 0, }; } // ─── 轮询 serialnet 响应(通用)───────────── function startRespPolling(recordId, cmd) { stopRespPolling(recordId); let attempts = 0; pollTimers[recordId] = setInterval(async () => { attempts++; try { const resp = await fetch(`/api/fixture/serialnet/${recordId}`); const rec = await resp.json(); if (rec.state === 2) { // 已完成 stopRespPolling(recordId); const rcvPkg = rec.rcv_pkg || ''; if (rcvPkg) { commLog('recv', rcvPkg, ''); // 解析 Flag(0x4B/0x4D/0x4E) if (cmd in FLAG_CMDS) { const flag = parseFlagResponse(rcvPkg, cmd); if (flag === 0) { commLog('ok', null, `${FLAG_CMDS[cmd]} 成功`); toast(`${FLAG_CMDS[cmd]} 成功`); } else { commLog('fail', null, `${FLAG_CMDS[cmd]} 失败 (Flag=${flag})`); toast(`${FLAG_CMDS[cmd]} 失败`, true); } } else { commLog('ok', null, `指令 0x${cmd} 已完成`); toast(`指令 0x${cmd} 已完成`); } // 0x4C 返回后刷新参数 if (cmd === '4C') { // 参数已在后端 upsert,直接从 DB 加载 setTimeout(async () => { const p = await (await fetch(`/api/fixture/param/${DNT_ID}`)).json(); if (p && p.dnt_id) fillFormFromParam(p); }, 500); } } else { commLog('info', null, `指令 0x${cmd} 已完成(无返回数据)`); } } else if (rec.state === 3) { // 超时 stopRespPolling(recordId); commLog('fail', null, `指令 0x${cmd} 超时(设备无响应)`); toast(`指令 0x${cmd} 超时`, true); } // state=0/1 → 继续等待 } catch (e) { // 忽略网络错误 } if (attempts >= 4) { stopRespPolling(recordId); commLog('fail', null, `指令 0x${cmd} 轮询超时(4秒无响应)`); } }, 1000); } function stopRespPolling(recordId) { if (pollTimers[recordId]) { clearInterval(pollTimers[recordId]); delete pollTimers[recordId]; } } // ─── 发送工装指令(通用)──────────────────── async function sendFixtureCmd(cmd) { try { const resp = await fetch("/api/fixture/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dnt_id: DNT_ID, cmd }), }); const data = await resp.json(); if (data.ok) { commLog('send', data.send_pkg, `${cmdName(cmd)} (0x${cmd})`); toast(`指令 0x${cmd} 已下发`); startRespPolling(data.record_id, cmd); } else { commLog('fail', null, `指令 0x${cmd} 发送失败: ${data.error}`); toast(`失败: ${data.error}`, true); } } catch (e) { commLog('fail', null, `发送 0x${cmd} 异常: ${e.message}`); toast("发送失败: " + e.message, true); } } // ─── 发送配置指令 0x4B ─────────────────────── async function sendConfig() { const params = getFormParams(); try { const resp = await fetch("/api/fixture/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dnt_id: DNT_ID, cmd: "4B", params }), }); const data = await resp.json(); if (data.ok) { commLog('send', data.send_pkg, `配置参数 (0x4B)`); toast("配置指令 0x4B 已下发"); startRespPolling(data.record_id, "4B"); // 同时保存到数据库 const coilId = parseInt(document.getElementById("coil-select").value) || null; const carId = parseInt(document.getElementById("car-select").value) || null; const saveBody = { Addr: params.addr, DevType: params.dev_type, TestMode: params.test_mode, RestDis: params.reset_dis, MinusDis: params.minus_dis, SensMin: params.sens_min, SensMax: params.sens_max, FreMin: params.fre_min, FreMax: params.fre_max, PeakMin: params.peak_min, PeakMax: params.peak_max, FarTol: params.far_tol, NearTol: params.near_tol, StepTol: params.step_tol, BackForth: params.back_forth, NearStay: params.near_stay, FarStay: params.far_stay, }; if (coilId) saveBody.coil_id = coilId; if (carId) saveBody.simulate_car_id = carId; await fetch(`/api/fixture/param/${DNT_ID}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(saveBody), }); } else { toast(`失败: ${data.error}`, true); } } catch (e) { toast("配置失败: " + e.message, true); } } function cmdName(cmd) { const names = { '4A': '获取版本号', '4B': '配置参数', '4C': '查询参数', '4D': '出厂初始化', '4E': '设备复位' }; return names[cmd] || cmd; } function esc(s) { return (s || "").replace(/&/g, "&").replace(//g, ">"); } init();