- fixture.js: FarTol/NearTol/StepTol 容差字段去掉 ×10 换算 - test_data.js: relay_out 列增加 fmtRelay() 着色渲染 - test_op.js: 新增工装配置概览面板 (renderConfigOverview + toggleConfig);新增 fmtRelay();renderLatest 继电器着色 - fixture.html: 标签文本优化 — 触发距离/释放距离/mm/V 单位标注 - test_op.html: 新增配置概览面板 HTML,隐藏旧 test-mode-indicator - vehicle_base_test.html: 标签文本统一(触发/释放距离 + 单位) - .gitignore: 新增,排除 __pycache__/*.pyc/.venv
470 lines
20 KiB
JavaScript
470 lines
20 KiB
JavaScript
// 工装配置页
|
||
|
||
// ─── 频率/峰峰值转换常量 ─────────────────────
|
||
// 协议: 工作频率 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 = `<div style="margin-bottom:3px;">
|
||
<span style="color:#569cd6;">${time}</span>
|
||
<span style="color:${color}; font-weight:600;"> ${label}</span>
|
||
<span style="color:#aaa;"> ${detail || ''}</span>
|
||
${hex ? `<br><span style="color:#808080; padding-left:10px;">${hex}</span>` : ''}
|
||
</div>`;
|
||
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 = '<tr><td colspan="5" style="color:#999;text-align:center;">暂无数据</td></tr>';
|
||
return;
|
||
}
|
||
tbody.innerHTML = baseTests.map(t => `
|
||
<tr onclick="selectBaseTest(${t.id})" data-id="${t.id}"
|
||
class="${selectedBaseTest && selectedBaseTest.id === t.id ? 'selected' : ''}">
|
||
<td>${t.type_num}</td>
|
||
<td>${esc(t.dev_name)}</td>
|
||
<td>${t.SensMin}~${t.SensMax}</td>
|
||
<td>${rawFreqToHz(t.FreMin)}~${rawFreqToHz(t.FreMax)}</td>
|
||
<td>${rawPeakToV(t.PeakMin)}~${rawPeakToV(t.PeakMax)}</td>
|
||
</tr>
|
||
`).join("");
|
||
}
|
||
|
||
function populateDevTypeSelect() {
|
||
const sel = document.getElementById("param-dev-type");
|
||
sel.innerHTML = '<option value="0">-- 请选择车检器基准 --</option>' +
|
||
baseTests.map(t => `<option value="${t.type_num}">${t.type_num} - ${esc(t.dev_name)}</option>`).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 = '<option value="">-- 选择线圈 --</option>' +
|
||
coilList.map(c => `<option value="${c.id}">${esc(c.coil_num || c.name || `#${c.id}`)}</option>`).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 = '<option value="">-- 选择模拟车辆 --</option>' +
|
||
carList.map(c => `<option value="${c.id}">${esc(c.simulate_num || c.name || `#${c.id}`)}</option>`).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}?_=${Date.now()}`);
|
||
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;
|
||
document.getElementById("param-near-tol").value = param.NearTol || 0;
|
||
document.getElementById("param-step-tol").value = param.StepTol || 0;
|
||
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}?_=${Date.now()}`)).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: parseInt(document.getElementById("param-far-tol").value) || 0,
|
||
near_tol: parseInt(document.getElementById("param-near-tol").value) || 0,
|
||
step_tol: parseInt(document.getElementById("param-step-tol").value) || 0,
|
||
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}?_=${Date.now()}`)).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, "<").replace(/>/g, ">"); }
|
||
|
||
init();
|